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:
@@ -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"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user