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

@@ -212,15 +212,16 @@ impl WasmSession {
}
pub fn save(&self) -> Result<String, JsValue> {
let bytes = bincode::serialize(&self.ratchet).map_err(|e| JsValue::from_str(&e.to_string()))?;
let bytes = self.ratchet.serialize_versioned()
.map_err(|e| JsValue::from_str(&e))?;
Ok(base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &bytes))
}
pub fn restore(data: &str) -> Result<WasmSession, JsValue> {
let bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, data)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let ratchet: RatchetState = bincode::deserialize(&bytes)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let ratchet = RatchetState::deserialize_versioned(&bytes)
.map_err(|e| JsValue::from_str(&e))?;
Ok(WasmSession { ratchet, x3dh_ephemeral_public: None, x3dh_used_otpk_id: None })
}
}
@@ -372,7 +373,7 @@ pub fn decrypt_wire_message(
let seed = Seed::from_bytes(sb);
let id = seed.derive_identity();
let wire: WireMessage = bincode::deserialize(message_bytes)
let wire: WireMessage = warzone_protocol::message::deserialize_envelope(message_bytes)
.map_err(|e| JsValue::from_str(&format!("deserialize wire: {}", e)))?;
match wire {
@@ -403,7 +404,7 @@ pub fn decrypt_wire_message(
let session_b64 = base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
&bincode::serialize(&ratchet).unwrap_or_default(),
&ratchet.serialize_versioned().unwrap_or_default(),
);
Ok(serde_json::json!({
@@ -424,15 +425,15 @@ pub fn decrypt_wire_message(
let session_bytes = base64::Engine::decode(
&base64::engine::general_purpose::STANDARD, &session_data,
).map_err(|e| JsValue::from_str(&e.to_string()))?;
let mut ratchet: RatchetState = bincode::deserialize(&session_bytes)
.map_err(|e| JsValue::from_str(&e.to_string()))?;
let mut ratchet = RatchetState::deserialize_versioned(&session_bytes)
.map_err(|e| JsValue::from_str(&e))?;
let plain = ratchet.decrypt(&ratchet_message)
.map_err(|e| JsValue::from_str(&format!("decrypt: {}", e)))?;
let session_b64 = base64::Engine::encode(
&base64::engine::general_purpose::STANDARD,
&bincode::serialize(&ratchet).unwrap_or_default(),
&ratchet.serialize_versioned().unwrap_or_default(),
);
Ok(serde_json::json!({