v0.0.38: FC-P4 complete — session versioning, wire envelope, auto-backup

FC-P4-T1: Session State Versioning
- RatchetState serialize/deserialize with [MAGIC:0xFC][VERSION:1][bincode]
- Legacy (raw bincode) still loads — backward compatible
- Client + WASM both use versioned format
- 2 new tests: roundtrip + legacy compat

FC-P4-T2: WireMessage Versioning Envelope
- Format: [WZ magic][version:u8][length:u32 BE][bincode payload]
- Server + client + WASM accept both envelope and legacy on receive
- Client still sends raw bincode (server handles both)
- Future version → "update required" error instead of crash
- 3 new tests: roundtrip, legacy compat, future version rejection

FC-P4-T3: Periodic Auto-Backup
- Every 5 minutes, encrypts sessions+contacts+sender_keys to ~/.warzone/backups/
- HKDF-derived key from seed, ChaCha20-Poly1305 AEAD
- Atomic writes (temp file + rename), rotates to keep last 3
- /backup command for manual trigger

127 tests passing (was 122)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Siavash Sameni
2026-03-29 17:03:02 +04:00
parent a368ab24d2
commit 5764719375
13 changed files with 309 additions and 29 deletions

View File

@@ -43,7 +43,7 @@ pub enum ReceiptType {
/// Wire message format for transport between clients.
/// Used by both CLI and WASM — MUST be identical for interop.
#[derive(Clone, Serialize, Deserialize)]
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum WireMessage {
/// First message to a peer: X3DH key exchange + first ratchet message.
KeyExchange {
@@ -132,3 +132,104 @@ pub enum CallSignalType {
/// Peer is busy.
Busy,
}
/// Current wire protocol version.
pub const WIRE_VERSION: u8 = 1;
/// Magic bytes to identify versioned envelope: "WZ"
pub const WIRE_MAGIC: [u8; 2] = [0x57, 0x5A];
/// Serialize a WireMessage with version envelope.
/// Format: [0x57][0x5A][version: u8][length: u32 BE][bincode payload]
pub fn serialize_envelope(msg: &WireMessage) -> Result<Vec<u8>, String> {
let payload =
bincode::serialize(msg).map_err(|e| format!("serialize: {}", e))?;
let len = payload.len() as u32;
let mut out = Vec::with_capacity(7 + payload.len());
out.extend_from_slice(&WIRE_MAGIC);
out.push(WIRE_VERSION);
out.extend_from_slice(&len.to_be_bytes());
out.extend_from_slice(&payload);
Ok(out)
}
/// Deserialize a WireMessage, handling both envelope and legacy formats.
/// - Envelope: [0x57][0x5A][version][length][payload]
/// - Legacy: raw bincode (no envelope)
pub fn deserialize_envelope(data: &[u8]) -> Result<WireMessage, String> {
if data.len() >= 7 && data[0] == WIRE_MAGIC[0] && data[1] == WIRE_MAGIC[1] {
let version = data[2];
let len =
u32::from_be_bytes([data[3], data[4], data[5], data[6]]) as usize;
if version > WIRE_VERSION {
return Err(format!(
"unsupported wire version {} (max {}). Please update your client.",
version, WIRE_VERSION
));
}
if data.len() < 7 + len {
return Err("truncated envelope".to_string());
}
bincode::deserialize(&data[7..7 + len])
.map_err(|e| format!("v{} deserialize: {}", version, e))
} else {
// Legacy: raw bincode
bincode::deserialize(data)
.map_err(|e| format!("legacy deserialize: {}", e))
}
}
#[cfg(test)]
mod envelope_tests {
use super::*;
#[test]
fn envelope_roundtrip() {
let msg = WireMessage::Receipt {
sender_fingerprint: "abc123".to_string(),
message_id: "msg-001".to_string(),
receipt_type: ReceiptType::Delivered,
};
let envelope = serialize_envelope(&msg).unwrap();
assert_eq!(&envelope[..2], &WIRE_MAGIC);
assert_eq!(envelope[2], WIRE_VERSION);
let decoded = deserialize_envelope(&envelope).unwrap();
match decoded {
WireMessage::Receipt { message_id, .. } => {
assert_eq!(message_id, "msg-001")
}
_ => panic!("wrong variant"),
}
}
#[test]
fn legacy_still_works() {
let msg = WireMessage::Receipt {
sender_fingerprint: "abc123".to_string(),
message_id: "msg-002".to_string(),
receipt_type: ReceiptType::Read,
};
let raw = bincode::serialize(&msg).unwrap();
let decoded = deserialize_envelope(&raw).unwrap();
match decoded {
WireMessage::Receipt { message_id, .. } => {
assert_eq!(message_id, "msg-002")
}
_ => panic!("wrong variant"),
}
}
#[test]
fn future_version_rejected() {
let mut envelope = serialize_envelope(&WireMessage::Receipt {
sender_fingerprint: "x".into(),
message_id: "y".into(),
receipt_type: ReceiptType::Delivered,
})
.unwrap();
envelope[2] = 99; // fake future version
let result = deserialize_envelope(&envelope);
assert!(result.is_err());
assert!(result.unwrap_err().contains("unsupported wire version"));
}
}