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

10
warzone/Cargo.lock generated
View File

@@ -2956,7 +2956,7 @@ dependencies = [
[[package]] [[package]]
name = "warzone-client" name = "warzone-client"
version = "0.0.37" version = "0.0.38"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@@ -2989,7 +2989,7 @@ dependencies = [
[[package]] [[package]]
name = "warzone-mule" name = "warzone-mule"
version = "0.0.37" version = "0.0.38"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"clap", "clap",
@@ -2998,7 +2998,7 @@ dependencies = [
[[package]] [[package]]
name = "warzone-protocol" name = "warzone-protocol"
version = "0.0.37" version = "0.0.38"
dependencies = [ dependencies = [
"base64", "base64",
"bincode", "bincode",
@@ -3023,7 +3023,7 @@ dependencies = [
[[package]] [[package]]
name = "warzone-server" name = "warzone-server"
version = "0.0.37" version = "0.0.38"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"axum", "axum",
@@ -3053,7 +3053,7 @@ dependencies = [
[[package]] [[package]]
name = "warzone-wasm" name = "warzone-wasm"
version = "0.0.37" version = "0.0.38"
dependencies = [ dependencies = [
"base64", "base64",
"bincode", "bincode",

View File

@@ -9,7 +9,7 @@ members = [
] ]
[workspace.package] [workspace.package]
version = "0.0.37" version = "0.0.38"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
rust-version = "1.75" rust-version = "1.75"

View File

@@ -54,7 +54,8 @@ impl LocalDb {
/// Save a ratchet session for a peer. /// Save a ratchet session for a peer.
pub fn save_session(&self, peer: &Fingerprint, state: &RatchetState) -> Result<()> { pub fn save_session(&self, peer: &Fingerprint, state: &RatchetState) -> Result<()> {
let key = peer.to_hex(); let key = peer.to_hex();
let data = bincode::serialize(state).context("failed to serialize session")?; let data = state.serialize_versioned()
.map_err(|e| anyhow::anyhow!("{}", e))?;
self.sessions.insert(key.as_bytes(), data)?; self.sessions.insert(key.as_bytes(), data)?;
self.sessions.flush()?; self.sessions.flush()?;
Ok(()) Ok(())
@@ -73,8 +74,8 @@ impl LocalDb {
let key = peer.to_hex(); let key = peer.to_hex();
match self.sessions.get(key.as_bytes())? { match self.sessions.get(key.as_bytes())? {
Some(data) => { Some(data) => {
let state = bincode::deserialize(&data) let state = RatchetState::deserialize_versioned(&data)
.context("failed to deserialize session")?; .map_err(|e| anyhow::anyhow!("{}", e))?;
Ok(Some(state)) Ok(Some(state))
} }
None => Ok(None), None => Ok(None),
@@ -272,6 +273,87 @@ impl LocalDb {
})) }))
} }
/// Create an encrypted backup of all session data.
/// Returns the backup file path.
pub fn create_backup(&self, seed: &[u8; 32]) -> Result<std::path::PathBuf> {
use std::io::Write;
let backup_dir = crate::keystore::data_dir().join("backups");
std::fs::create_dir_all(&backup_dir)?;
// Collect all data
let mut data = serde_json::Map::new();
// Sessions
let mut sessions = serde_json::Map::new();
for item in self.sessions.iter() {
if let Ok((key, value)) = item {
let k = String::from_utf8_lossy(&key).to_string();
sessions.insert(k, serde_json::Value::String(base64::Engine::encode(
&base64::engine::general_purpose::STANDARD, &value
)));
}
}
data.insert("sessions".into(), serde_json::Value::Object(sessions));
// Contacts
let mut contacts = serde_json::Map::new();
for item in self.contacts.iter() {
if let Ok((key, value)) = item {
let k = String::from_utf8_lossy(&key).to_string();
if let Ok(v) = serde_json::from_slice::<serde_json::Value>(&value) {
contacts.insert(k, v);
}
}
}
data.insert("contacts".into(), serde_json::Value::Object(contacts));
// Sender keys
let mut sender_keys = serde_json::Map::new();
for item in self.sender_keys.iter() {
if let Ok((key, value)) = item {
let k = String::from_utf8_lossy(&key).to_string();
sender_keys.insert(k, serde_json::Value::String(base64::Engine::encode(
&base64::engine::general_purpose::STANDARD, &value
)));
}
}
data.insert("sender_keys".into(), serde_json::Value::Object(sender_keys));
// Serialize and encrypt
let plaintext = serde_json::to_vec(&serde_json::Value::Object(data))?;
let key_bytes = warzone_protocol::crypto::hkdf_derive(seed, b"", b"warzone-backup", 32);
let mut key = [0u8; 32];
key.copy_from_slice(&key_bytes);
let encrypted = warzone_protocol::crypto::aead_encrypt(&key, &plaintext, b"warzone-backup-aad");
// Write to temp file then rename (atomic)
let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S").to_string();
let filename = format!("backup_{}.wzbk", timestamp);
let path = backup_dir.join(&filename);
let tmp_path = backup_dir.join(format!(".{}.tmp", filename));
let mut file = std::fs::File::create(&tmp_path)?;
file.write_all(&encrypted)?;
file.sync_all()?;
std::fs::rename(&tmp_path, &path)?;
// Rotate: keep last 3 backups
let mut backups: Vec<_> = std::fs::read_dir(&backup_dir)?
.filter_map(|e| e.ok())
.filter(|e| e.file_name().to_string_lossy().ends_with(".wzbk"))
.collect();
backups.sort_by_key(|e| e.file_name());
while backups.len() > 3 {
if let Some(old) = backups.first() {
let _ = std::fs::remove_file(old.path());
backups.remove(0);
}
}
Ok(path)
}
/// Import data from JSON backup (merges, doesn't overwrite existing). /// Import data from JSON backup (merges, doesn't overwrite existing).
pub fn import_all(&self, data: &serde_json::Value) -> Result<usize> { pub fn import_all(&self, data: &serde_json::Value) -> Result<usize> {
let mut count = 0; let mut count = 0;

View File

@@ -47,6 +47,7 @@ impl App {
" /info Show your fingerprint", " /info Show your fingerprint",
" /eth Show Ethereum address", " /eth Show Ethereum address",
" /seed Show recovery mnemonic (24 words)", " /seed Show recovery mnemonic (24 words)",
" /backup Create encrypted backup now",
" /peer <fp>, /p Set DM peer by fingerprint", " /peer <fp>, /p Set DM peer by fingerprint",
" /peer @alias Set DM peer by alias", " /peer @alias Set DM peer by alias",
" /reply, /r Reply to last DM sender", " /reply, /r Reply to last DM sender",
@@ -189,6 +190,19 @@ impl App {
} }
return; return;
} }
if text == "/backup" {
if let Ok(seed) = crate::keystore::load_seed_raw() {
match db.create_backup(&seed) {
Ok(path) => {
self.add_message(ChatLine { sender: "system".into(), text: format!("Backup saved: {}", path.display()), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
}
Err(e) => {
self.add_message(ChatLine { sender: "system".into(), text: format!("Backup failed: {}", e), is_system: true, is_self: false, message_id: None, timestamp: Local::now() });
}
}
}
return;
}
if text == "/friend" || text == "/friends" { if text == "/friend" || text == "/friends" {
// Fetch encrypted friend list from server, decrypt locally // Fetch encrypted friend list from server, decrypt locally
let url = format!("{}/v1/friends", client.base_url); let url = format!("{}/v1/friends", client.base_url);

View File

@@ -48,6 +48,23 @@ pub async fn run_tui(
network::poll_loop(poll_messages, poll_receipts, poll_pending_files, poll_fp, poll_identity, poll_db, poll_client, poll_last_dm, poll_connected).await; network::poll_loop(poll_messages, poll_receipts, poll_pending_files, poll_fp, poll_identity, poll_db, poll_client, poll_last_dm, poll_connected).await;
}); });
// Spawn periodic backup task (every 5 minutes)
{
let backup_db = db.clone();
tokio::spawn(async move {
let mut interval = tokio::time::interval(std::time::Duration::from_secs(300));
loop {
interval.tick().await;
if let Ok(seed) = crate::keystore::load_seed_raw() {
match backup_db.create_backup(&seed) {
Ok(path) => tracing::debug!("Auto-backup created: {}", path.display()),
Err(e) => tracing::warn!("Auto-backup failed: {}", e),
}
}
}
});
}
// Auto-join #ops if no peer set (create if needed) // Auto-join #ops if no peer set (create if needed)
if app.peer_fp.is_none() { if app.peer_fp.is_none() {
let fp_clean: String = our_fp.chars().filter(|c| c.is_ascii_hexdigit()).collect::<String>().to_lowercase(); let fp_clean: String = our_fp.chars().filter(|c| c.is_ascii_hexdigit()).collect::<String>().to_lowercase();

View File

@@ -93,7 +93,7 @@ pub fn process_incoming(
eth_cache: &EthCache, eth_cache: &EthCache,
last_dm_peer: &Arc<Mutex<Option<String>>>, last_dm_peer: &Arc<Mutex<Option<String>>>,
) { ) {
match bincode::deserialize::<WireMessage>(raw) { match warzone_protocol::message::deserialize_envelope(raw) {
Ok(wire) => process_wire_message(wire, identity, db, messages, receipts, pending_files, our_fp, client, last_dm_peer, eth_cache), Ok(wire) => process_wire_message(wire, identity, db, messages, receipts, pending_files, our_fp, client, last_dm_peer, eth_cache),
Err(_) => {} Err(_) => {}
} }

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "warzone-protocol" name = "warzone-protocol"
version = "0.0.37" version = "0.0.38"
edition = "2021" edition = "2021"
license = "MIT" license = "MIT"
description = "Core crypto & wire protocol for featherChat (Warzone messenger)" description = "Core crypto & wire protocol for featherChat (Warzone messenger)"

View File

@@ -43,7 +43,7 @@ pub enum ReceiptType {
/// Wire message format for transport between clients. /// Wire message format for transport between clients.
/// Used by both CLI and WASM — MUST be identical for interop. /// Used by both CLI and WASM — MUST be identical for interop.
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub enum WireMessage { pub enum WireMessage {
/// First message to a peer: X3DH key exchange + first ratchet message. /// First message to a peer: X3DH key exchange + first ratchet message.
KeyExchange { KeyExchange {
@@ -132,3 +132,104 @@ pub enum CallSignalType {
/// Peer is busy. /// Peer is busy.
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"));
}
}

View File

@@ -11,15 +11,20 @@ use crate::errors::ProtocolError;
const MAX_SKIP: u32 = 1000; const MAX_SKIP: u32 = 1000;
/// Current serialization version for [`RatchetState`].
const RATCHET_VERSION: u8 = 1;
/// Magic byte to distinguish versioned from unversioned (legacy) data.
const RATCHET_MAGIC: u8 = 0xFC;
/// A message produced by the ratchet. /// A message produced by the ratchet.
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RatchetMessage { pub struct RatchetMessage {
pub header: RatchetHeader, pub header: RatchetHeader,
pub ciphertext: Vec<u8>, pub ciphertext: Vec<u8>,
} }
/// Header included with each ratchet message. /// Header included with each ratchet message.
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RatchetHeader { pub struct RatchetHeader {
/// Current DH ratchet public key. /// Current DH ratchet public key.
pub dh_public: [u8; 32], pub dh_public: [u8; 32],
@@ -208,6 +213,37 @@ impl RatchetState {
Ok(()) Ok(())
} }
/// Serialize with version prefix: `[MAGIC][VERSION][bincode data]`.
///
/// Use [`deserialize_versioned`](Self::deserialize_versioned) to restore.
pub fn serialize_versioned(&self) -> Result<Vec<u8>, String> {
let data = bincode::serialize(self)
.map_err(|e| format!("serialize: {}", e))?;
let mut out = Vec::with_capacity(2 + data.len());
out.push(RATCHET_MAGIC);
out.push(RATCHET_VERSION);
out.extend_from_slice(&data);
Ok(out)
}
/// Deserialize with version awareness. Handles:
/// - Versioned format: `[0xFC][version][bincode]`
/// - Legacy format: raw bincode (no prefix)
pub fn deserialize_versioned(data: &[u8]) -> Result<Self, String> {
if data.len() >= 2 && data[0] == RATCHET_MAGIC {
let version = data[1];
match version {
1 => bincode::deserialize(&data[2..])
.map_err(|e| format!("v1 deserialize: {}", e)),
_ => Err(format!("unknown ratchet version: {}", version)),
}
} else {
// Legacy: try raw bincode (pre-versioning data)
bincode::deserialize(data)
.map_err(|e| format!("legacy deserialize: {}", e))
}
}
fn dh_ratchet_step(&mut self) -> Result<(), ProtocolError> { fn dh_ratchet_step(&mut self) -> Result<(), ProtocolError> {
let their_pub = self let their_pub = self
.dh_remote .dh_remote
@@ -312,6 +348,35 @@ mod tests {
assert_eq!(bob.decrypt(&m2).unwrap(), b"two"); assert_eq!(bob.decrypt(&m2).unwrap(), b"two");
} }
#[test]
fn versioned_serialize_roundtrip() {
let (mut alice, mut bob) = make_pair();
let msg = alice.encrypt(b"test versioning").unwrap();
// Save alice with versioned format
let serialized = alice.serialize_versioned().unwrap();
assert_eq!(serialized[0], 0xFC); // magic byte
assert_eq!(serialized[1], 1); // version 1
// Restore and use
let mut restored = RatchetState::deserialize_versioned(&serialized).unwrap();
let msg2 = restored.encrypt(b"after restore").unwrap();
let plain = bob.decrypt(&msg).unwrap();
assert_eq!(plain, b"test versioning");
let plain2 = bob.decrypt(&msg2).unwrap();
assert_eq!(plain2, b"after restore");
}
#[test]
fn legacy_deserialize_works() {
let (alice, _) = make_pair();
// Serialize with raw bincode (legacy format)
let legacy = bincode::serialize(&alice).unwrap();
// Should still deserialize with versioned reader
let restored = RatchetState::deserialize_versioned(&legacy).unwrap();
assert_eq!(bincode::serialize(&restored).unwrap(), legacy);
}
#[test] #[test]
fn many_messages() { fn many_messages() {
let (mut alice, mut bob) = make_pair(); let (mut alice, mut bob) = make_pair();

View File

@@ -9,9 +9,9 @@ use warzone_protocol::message::WireMessage;
use crate::errors::AppResult; use crate::errors::AppResult;
use crate::state::AppState; use crate::state::AppState;
/// Try to extract the message ID from raw bincode-serialized WireMessage bytes. /// Try to extract the message ID from raw WireMessage bytes (envelope or legacy).
fn extract_message_id(data: &[u8]) -> Option<String> { fn extract_message_id(data: &[u8]) -> Option<String> {
if let Ok(wire) = bincode::deserialize::<WireMessage>(data) { if let Ok(wire) = warzone_protocol::message::deserialize_envelope(data) {
match wire { match wire {
WireMessage::KeyExchange { id, .. } => Some(id), WireMessage::KeyExchange { id, .. } => Some(id),
WireMessage::Message { id, .. } => Some(id), WireMessage::Message { id, .. } => Some(id),

View File

@@ -50,7 +50,7 @@ async fn pwa_manifest() -> impl IntoResponse {
async fn service_worker() -> impl IntoResponse { async fn service_worker() -> impl IntoResponse {
([(header::CONTENT_TYPE, "application/javascript")], r##" ([(header::CONTENT_TYPE, "application/javascript")], r##"
const CACHE = 'wz-v19'; const CACHE = 'wz-v20';
const SHELL = ['/', '/wasm/warzone_wasm.js', '/wasm/warzone_wasm_bg.wasm', '/icon.svg', '/manifest.json']; const SHELL = ['/', '/wasm/warzone_wasm.js', '/wasm/warzone_wasm_bg.wasm', '/icon.svg', '/manifest.json'];
self.addEventListener('install', e => { self.addEventListener('install', e => {
@@ -287,7 +287,7 @@ let pollTimer = null;
let ws = null; // WebSocket connection let ws = null; // WebSocket connection
let wasmReady = false; let wasmReady = false;
const VERSION = '0.0.37'; const VERSION = '0.0.38';
let DEBUG = true; // toggle with /debug command let DEBUG = true; // toggle with /debug command
// ── Receipt tracking ── // ── Receipt tracking ──

View File

@@ -21,9 +21,9 @@ use warzone_protocol::message::WireMessage;
use crate::state::AppState; use crate::state::AppState;
/// Try to extract the message ID from raw bincode-serialized WireMessage bytes. /// Try to extract the message ID from raw WireMessage bytes (envelope or legacy).
fn extract_message_id(data: &[u8]) -> Option<String> { fn extract_message_id(data: &[u8]) -> Option<String> {
if let Ok(wire) = bincode::deserialize::<WireMessage>(data) { if let Ok(wire) = warzone_protocol::message::deserialize_envelope(data) {
match wire { match wire {
WireMessage::KeyExchange { id, .. } => Some(id), WireMessage::KeyExchange { id, .. } => Some(id),
WireMessage::Message { id, .. } => Some(id), WireMessage::Message { id, .. } => Some(id),
@@ -147,7 +147,7 @@ async fn handle_socket(socket: WebSocket, state: AppState, fingerprint: String)
} }
// Call signal side effects // Call signal side effects
if let Ok(WireMessage::CallSignal { ref id, ref sender_fingerprint, ref signal_type, .. }) = bincode::deserialize::<WireMessage>(message) { if let Ok(WireMessage::CallSignal { ref id, ref sender_fingerprint, ref signal_type, .. }) = warzone_protocol::message::deserialize_envelope(message) {
use warzone_protocol::message::CallSignalType; use warzone_protocol::message::CallSignalType;
let now = chrono::Utc::now().timestamp(); let now = chrono::Utc::now().timestamp();
match signal_type { match signal_type {

View File

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