v0.0.12: Encrypted backup/restore + history module
Protocol: - history.rs: derive_history_key (HKDF from seed, info="warzone-history") - encrypt_history / decrypt_history (ChaCha20-Poly1305, WZH1 magic) - 2 new tests (roundtrip + wrong seed), total 19/19 CLI: - `warzone backup [output.wzb]` — exports all sessions + pre-keys as encrypted blob (only your seed can decrypt) - `warzone restore <input.wzb>` — imports backup, merges (no overwrite) - Backup format: WZH1 magic + nonce + encrypted JSON Storage: - export_all() — dumps sessions + pre-keys as base64 JSON - import_all() — merges backup data (skip existing entries) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
//! Local sled database: sessions, pre-keys, message history.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use base64::Engine as _;
|
||||
use warzone_protocol::ratchet::RatchetState;
|
||||
use warzone_protocol::types::Fingerprint;
|
||||
use x25519_dalek::StaticSecret;
|
||||
@@ -108,4 +109,73 @@ impl LocalDb {
|
||||
None => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Export all data as JSON (for encrypted backup).
|
||||
pub fn export_all(&self) -> Result<serde_json::Value> {
|
||||
let mut sessions = serde_json::Map::new();
|
||||
for item in self.sessions.iter() {
|
||||
if let Ok((k, v)) = item {
|
||||
let key = String::from_utf8_lossy(&k).to_string();
|
||||
sessions.insert(key, serde_json::json!(base64::Engine::encode(
|
||||
&base64::engine::general_purpose::STANDARD, &v
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
let mut pre_keys = serde_json::Map::new();
|
||||
for item in self.pre_keys.iter() {
|
||||
if let Ok((k, v)) = item {
|
||||
let key = String::from_utf8_lossy(&k).to_string();
|
||||
pre_keys.insert(key, serde_json::json!(base64::Engine::encode(
|
||||
&base64::engine::general_purpose::STANDARD, &v
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"version": 1,
|
||||
"sessions": sessions,
|
||||
"pre_keys": pre_keys,
|
||||
}))
|
||||
}
|
||||
|
||||
/// Import data from JSON backup (merges, doesn't overwrite existing).
|
||||
pub fn import_all(&self, data: &serde_json::Value) -> Result<usize> {
|
||||
let mut count = 0;
|
||||
|
||||
if let Some(sessions) = data.get("sessions").and_then(|v| v.as_object()) {
|
||||
for (key, val) in sessions {
|
||||
if let Some(b64) = val.as_str() {
|
||||
if let Ok(bytes) = base64::Engine::decode(
|
||||
&base64::engine::general_purpose::STANDARD, b64
|
||||
) {
|
||||
// Only import if not already present
|
||||
if self.sessions.get(key.as_bytes())?.is_none() {
|
||||
self.sessions.insert(key.as_bytes(), bytes)?;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(pre_keys) = data.get("pre_keys").and_then(|v| v.as_object()) {
|
||||
for (key, val) in pre_keys {
|
||||
if let Some(b64) = val.as_str() {
|
||||
if let Ok(bytes) = base64::Engine::decode(
|
||||
&base64::engine::general_purpose::STANDARD, b64
|
||||
) {
|
||||
if self.pre_keys.get(key.as_bytes())?.is_none() {
|
||||
self.pre_keys.insert(key.as_bytes(), bytes)?;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.sessions.flush()?;
|
||||
self.pre_keys.flush()?;
|
||||
Ok(count)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user