Files
featherChat/warzone/crates/warzone-protocol/src/history.rs
Siavash Sameni 653c6c050b 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>
2026-03-27 12:59:54 +04:00

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());
}
}