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

@@ -870,6 +870,105 @@ pub async fn delete_account(
Ok(success)
}
/// Register a device for the authenticated identity.
pub async fn register_device(
client: &node_service::Client,
device_id: &[u8],
device_name: &str,
) -> anyhow::Result<bool> {
let mut req = client.register_device_request();
{
let mut p = req.get();
p.set_device_id(device_id);
p.set_device_name(device_name);
let mut auth = p.reborrow().init_auth();
set_auth(&mut auth)?;
}
let resp = req
.send()
.promise
.await
.context("register_device RPC failed")?;
let success = resp
.get()
.context("register_device: bad response")?
.get_success();
Ok(success)
}
/// List all registered devices for the authenticated identity.
pub async fn list_devices(
client: &node_service::Client,
) -> anyhow::Result<Vec<(Vec<u8>, String, u64)>> {
let mut req = client.list_devices_request();
{
let mut p = req.get();
let mut auth = p.reborrow().init_auth();
set_auth(&mut auth)?;
}
let resp = req
.send()
.promise
.await
.context("list_devices RPC failed")?;
let devices = resp
.get()
.context("list_devices: bad response")?
.get_devices()
.context("list_devices: missing devices field")?;
let mut result = Vec::with_capacity(devices.len() as usize);
for i in 0..devices.len() {
let entry = devices.get(i);
let device_id = entry
.get_device_id()
.context("list_devices: missing device_id")?
.to_vec();
let device_name = entry
.get_device_name()
.context("list_devices: missing device_name")?
.to_str()
.unwrap_or("")
.to_string();
let registered_at = entry.get_registered_at();
result.push((device_id, device_name, registered_at));
}
Ok(result)
}
/// Revoke (remove) a registered device.
pub async fn revoke_device(
client: &node_service::Client,
device_id: &[u8],
) -> anyhow::Result<bool> {
let mut req = client.revoke_device_request();
{
let mut p = req.get();
p.set_device_id(device_id);
let mut auth = p.reborrow().init_auth();
set_auth(&mut auth)?;
}
let resp = req
.send()
.promise
.await
.context("revoke_device RPC failed")?;
let success = resp
.get()
.context("revoke_device: bad response")?
.get_success();
Ok(success)
}
/// Return the current Unix timestamp in milliseconds.
pub fn current_timestamp_ms() -> u64 {
std::time::SystemTime::now()