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:
@@ -184,6 +184,21 @@ pub trait Store: Send + Sync {
|
||||
/// user identity key mapping, and the user record itself.
|
||||
/// Does NOT delete KT log entries (append-only for auditability).
|
||||
fn delete_account(&self, identity_key: &[u8]) -> Result<(), StorageError>;
|
||||
|
||||
// ── Device registry ─────────────────────────────────────────────────────
|
||||
|
||||
/// Register a device for an identity. Returns false if the device already exists.
|
||||
/// Caller must check device_count < 5 before calling.
|
||||
fn register_device(&self, identity_key: &[u8], device_id: &[u8], device_name: &str) -> Result<bool, StorageError>;
|
||||
|
||||
/// List all registered devices for an identity: (device_id, name, registered_at).
|
||||
fn list_devices(&self, identity_key: &[u8]) -> Result<Vec<(Vec<u8>, String, u64)>, StorageError>;
|
||||
|
||||
/// Revoke (remove) a registered device. Returns false if not found.
|
||||
fn revoke_device(&self, identity_key: &[u8], device_id: &[u8]) -> Result<bool, StorageError>;
|
||||
|
||||
/// Return the number of registered devices for an identity.
|
||||
fn device_count(&self, identity_key: &[u8]) -> Result<usize, StorageError>;
|
||||
}
|
||||
|
||||
// ── ChannelKey ───────────────────────────────────────────────────────────────
|
||||
@@ -247,6 +262,8 @@ pub struct FileBackedStore {
|
||||
users: Mutex<HashMap<String, Vec<u8>>>,
|
||||
identity_keys: Mutex<HashMap<String, Vec<u8>>>,
|
||||
endpoints: Mutex<HashMap<Vec<u8>, Vec<u8>>>,
|
||||
/// Device registry: identity_key -> Vec<(device_id, device_name, registered_at)>
|
||||
devices: Mutex<HashMap<Vec<u8>, Vec<(Vec<u8>, String, u64)>>>,
|
||||
}
|
||||
|
||||
impl FileBackedStore {
|
||||
@@ -289,6 +306,7 @@ impl FileBackedStore {
|
||||
users,
|
||||
identity_keys,
|
||||
endpoints: Mutex::new(HashMap::new()),
|
||||
devices: Mutex::new(HashMap::new()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -837,8 +855,49 @@ impl Store for FileBackedStore {
|
||||
ep.remove(identity_key);
|
||||
}
|
||||
|
||||
// Remove devices.
|
||||
{
|
||||
let mut dev = lock(&self.devices)?;
|
||||
dev.remove(identity_key);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn register_device(&self, identity_key: &[u8], device_id: &[u8], device_name: &str) -> Result<bool, StorageError> {
|
||||
let mut map = lock(&self.devices)?;
|
||||
let devices = map.entry(identity_key.to_vec()).or_default();
|
||||
if devices.iter().any(|(id, _, _)| id == device_id) {
|
||||
return Ok(false);
|
||||
}
|
||||
let now = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_secs();
|
||||
devices.push((device_id.to_vec(), device_name.to_string(), now));
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn list_devices(&self, identity_key: &[u8]) -> Result<Vec<(Vec<u8>, String, u64)>, StorageError> {
|
||||
let map = lock(&self.devices)?;
|
||||
Ok(map.get(identity_key).cloned().unwrap_or_default())
|
||||
}
|
||||
|
||||
fn revoke_device(&self, identity_key: &[u8], device_id: &[u8]) -> Result<bool, StorageError> {
|
||||
let mut map = lock(&self.devices)?;
|
||||
if let Some(devices) = map.get_mut(identity_key) {
|
||||
let before = devices.len();
|
||||
devices.retain(|(id, _, _)| id != device_id);
|
||||
Ok(devices.len() < before)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
|
||||
fn device_count(&self, identity_key: &[u8]) -> Result<usize, StorageError> {
|
||||
let map = lock(&self.devices)?;
|
||||
Ok(map.get(identity_key).map(|v| v.len()).unwrap_or(0))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
Reference in New Issue
Block a user