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:
58
warzone/crates/warzone-protocol/src/history.rs
Normal file
58
warzone/crates/warzone-protocol/src/history.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
//! Encrypted message history: backup and restore.
|
||||
//!
|
||||
//! History key derived from seed via HKDF (info="warzone-history").
|
||||
//! Format: MAGIC(4) + nonce(12) + ciphertext (ChaCha20-Poly1305).
|
||||
|
||||
use crate::crypto::{aead_decrypt, aead_encrypt, hkdf_derive};
|
||||
use crate::errors::ProtocolError;
|
||||
|
||||
const HISTORY_MAGIC: &[u8; 4] = b"WZH1";
|
||||
|
||||
/// Derive history encryption key from seed.
|
||||
pub fn derive_history_key(seed: &[u8; 32]) -> [u8; 32] {
|
||||
let derived = hkdf_derive(seed, b"", b"warzone-history", 32);
|
||||
let mut key = [0u8; 32];
|
||||
key.copy_from_slice(&derived);
|
||||
key
|
||||
}
|
||||
|
||||
/// Encrypt a history blob (JSON messages serialized to bytes).
|
||||
pub fn encrypt_history(seed: &[u8; 32], plaintext: &[u8]) -> Vec<u8> {
|
||||
let key = derive_history_key(seed);
|
||||
let encrypted = aead_encrypt(&key, plaintext, HISTORY_MAGIC);
|
||||
let mut result = Vec::with_capacity(4 + encrypted.len());
|
||||
result.extend_from_slice(HISTORY_MAGIC);
|
||||
result.extend_from_slice(&encrypted);
|
||||
result
|
||||
}
|
||||
|
||||
/// Decrypt a history blob.
|
||||
pub fn decrypt_history(seed: &[u8; 32], data: &[u8]) -> Result<Vec<u8>, ProtocolError> {
|
||||
if data.len() < 4 || &data[..4] != HISTORY_MAGIC {
|
||||
return Err(ProtocolError::DecryptionFailed);
|
||||
}
|
||||
let key = derive_history_key(seed);
|
||||
aead_decrypt(&key, &data[4..], HISTORY_MAGIC)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn roundtrip() {
|
||||
let seed = [42u8; 32];
|
||||
let messages = b"[{\"from\":\"alice\",\"text\":\"hello\"}]";
|
||||
let encrypted = encrypt_history(&seed, messages);
|
||||
let decrypted = decrypt_history(&seed, &encrypted).unwrap();
|
||||
assert_eq!(decrypted, messages);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_seed_fails() {
|
||||
let seed = [42u8; 32];
|
||||
let wrong = [99u8; 32];
|
||||
let encrypted = encrypt_history(&seed, b"secret");
|
||||
assert!(decrypt_history(&wrong, &encrypted).is_err());
|
||||
}
|
||||
}
|
||||
@@ -9,3 +9,4 @@ pub mod ratchet;
|
||||
pub mod message;
|
||||
pub mod session;
|
||||
pub mod store;
|
||||
pub mod history;
|
||||
|
||||
Reference in New Issue
Block a user