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>
59 lines
1.8 KiB
Rust
59 lines
1.8 KiB
Rust
//! 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());
|
|
}
|
|
}
|