feat: Sprint 10+11 — privacy hardening and multi-device support

Privacy Hardening (Sprint 10):
- Server --redact-logs flag: SHA-256 hashed identity prefixes in audit
  logs, payload_len omitted when enabled
- Client /privacy command suite: redact-keys on|off, auto-clear with
  duration parsing, padding on|off for traffic analysis resistance
- Forward secrecy: /verify-fs checks MLS epoch advancement,
  /rotate-all-keys rotates MLS leaf + hybrid KEM keypair
- Dummy message type (0x09): constant-rate traffic padding every 30s,
  silently discarded by recipients, serialize_dummy() + parse support
- delete_messages_before() for auto-clear in ConversationStore

Multi-Device Support (Sprint 11):
- Device registry: registerDevice @24, listDevices @25, revokeDevice @26
  RPCs with Device struct (deviceId, deviceName, registeredAt)
- Server storage: devices table (migration 008), max 5 per identity,
  E029_DEVICE_LIMIT and E030_DEVICE_NOT_FOUND error codes
- Device cleanup integrated into deleteAccount transaction
- Client REPL: /devices, /register-device <name>, /revoke-device <id>

72 core + 35 server tests pass.
This commit is contained in:
2026-03-04 01:55:23 +01:00
parent 1b61b7ee8f
commit 9244e80ec7
16 changed files with 958 additions and 45 deletions

View File

@@ -27,6 +27,7 @@ pub enum MessageType {
Edit = 0x06,
Delete = 0x07,
FileRef = 0x08,
Dummy = 0x09,
}
impl MessageType {
@@ -40,6 +41,7 @@ impl MessageType {
0x06 => Some(MessageType::Edit),
0x07 => Some(MessageType::Delete),
0x08 => Some(MessageType::FileRef),
0x09 => Some(MessageType::Dummy),
_ => None,
}
}
@@ -84,6 +86,8 @@ pub enum AppMessage {
file_size: u64,
mime_type: Vec<u8>,
},
/// Dummy message for traffic analysis resistance (no user-visible content).
Dummy,
}
/// Generate a new 16-byte message ID (e.g. for Chat/Reply so recipients can reference it).
@@ -203,6 +207,11 @@ pub fn serialize_file_ref(
Ok(serialize(MessageType::FileRef, &payload))
}
/// Serialize a Dummy message (traffic padding — no user content).
pub fn serialize_dummy() -> Vec<u8> {
serialize(MessageType::Dummy, &[])
}
/// Parse bytes into (MessageType, AppMessage). Fails if version/type unknown or payload too short.
pub fn parse(bytes: &[u8]) -> Result<(MessageType, AppMessage), CoreError> {
if bytes.len() < 2 {
@@ -225,6 +234,7 @@ pub fn parse(bytes: &[u8]) -> Result<(MessageType, AppMessage), CoreError> {
MessageType::Edit => parse_edit(payload)?,
MessageType::Delete => parse_delete(payload)?,
MessageType::FileRef => parse_file_ref(payload)?,
MessageType::Dummy => AppMessage::Dummy,
};
Ok((msg_type, app))
}
@@ -502,4 +512,12 @@ mod tests {
_ => panic!("expected FileRef"),
}
}
#[test]
fn roundtrip_dummy() {
let encoded = serialize_dummy();
let (t, msg) = parse(&encoded).unwrap();
assert_eq!(t, MessageType::Dummy);
assert_eq!(msg, AppMessage::Dummy);
}
}

View File

@@ -59,9 +59,9 @@ pub mod opaque_auth;
// ── Public API (always available) ───────────────────────────────────────────
pub use app_message::{
serialize, serialize_chat, serialize_delete, serialize_edit, serialize_file_ref,
serialize_reaction, serialize_read_receipt, serialize_reply, serialize_typing,
parse, generate_message_id,
serialize, serialize_chat, serialize_delete, serialize_dummy, serialize_edit,
serialize_file_ref, serialize_reaction, serialize_read_receipt, serialize_reply,
serialize_typing, parse, generate_message_id,
AppMessage, MessageType, VERSION as APP_MESSAGE_VERSION,
};
pub use error::CoreError;