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:
@@ -9,7 +9,7 @@ use rusqlite::{params, Connection};
|
||||
use crate::storage::{StorageError, Store};
|
||||
|
||||
/// Schema version after introducing the migration runner (existing DBs had 1).
|
||||
const SCHEMA_VERSION: i32 = 8;
|
||||
const SCHEMA_VERSION: i32 = 9;
|
||||
|
||||
/// Migrations: (migration_number, SQL). Files named NNN_name.sql, applied in order when N > user_version.
|
||||
const MIGRATIONS: &[(i32, &str)] = &[
|
||||
@@ -20,6 +20,7 @@ const MIGRATIONS: &[(i32, &str)] = &[
|
||||
(6, include_str!("../migrations/005_signing_key.sql")),
|
||||
(7, include_str!("../migrations/006_kt_log.sql")),
|
||||
(8, include_str!("../migrations/007_add_expiry.sql")),
|
||||
(9, include_str!("../migrations/008_devices.sql")),
|
||||
];
|
||||
|
||||
/// Runs pending migrations on an open connection: applies any migration whose number is greater
|
||||
@@ -693,6 +694,12 @@ impl Store for SqlStore {
|
||||
params![identity_key],
|
||||
);
|
||||
|
||||
// 8. Delete devices.
|
||||
let _ = conn.execute(
|
||||
"DELETE FROM devices WHERE identity_key = ?1",
|
||||
params![identity_key],
|
||||
);
|
||||
|
||||
// Do NOT delete KT log entries — append-only for auditability.
|
||||
|
||||
Ok(())
|
||||
@@ -710,6 +717,69 @@ impl Store for SqlStore {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn register_device(&self, identity_key: &[u8], device_id: &[u8], device_name: &str) -> Result<bool, StorageError> {
|
||||
let conn = self.lock_conn()?;
|
||||
// Check if device already exists.
|
||||
let exists: bool = conn
|
||||
.query_row(
|
||||
"SELECT EXISTS(SELECT 1 FROM devices WHERE identity_key = ?1 AND device_id = ?2)",
|
||||
params![identity_key, device_id],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|e| StorageError::Db(e.to_string()))?;
|
||||
if exists {
|
||||
return Ok(false);
|
||||
}
|
||||
conn.execute(
|
||||
"INSERT INTO devices (identity_key, device_id, device_name) VALUES (?1, ?2, ?3)",
|
||||
params![identity_key, device_id, device_name],
|
||||
)
|
||||
.map_err(|e| StorageError::Db(e.to_string()))?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
fn list_devices(&self, identity_key: &[u8]) -> Result<Vec<(Vec<u8>, String, u64)>, StorageError> {
|
||||
let conn = self.lock_conn()?;
|
||||
let mut stmt = conn
|
||||
.prepare("SELECT device_id, device_name, registered_at FROM devices WHERE identity_key = ?1 ORDER BY registered_at ASC")
|
||||
.map_err(|e| StorageError::Db(e.to_string()))?;
|
||||
let rows = stmt
|
||||
.query_map(params![identity_key], |row| {
|
||||
Ok((
|
||||
row.get::<_, Vec<u8>>(0)?,
|
||||
row.get::<_, String>(1)?,
|
||||
row.get::<_, i64>(2)? as u64,
|
||||
))
|
||||
})
|
||||
.map_err(|e| StorageError::Db(e.to_string()))?
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map_err(|e| StorageError::Db(e.to_string()))?;
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
fn revoke_device(&self, identity_key: &[u8], device_id: &[u8]) -> Result<bool, StorageError> {
|
||||
let conn = self.lock_conn()?;
|
||||
let deleted = conn
|
||||
.execute(
|
||||
"DELETE FROM devices WHERE identity_key = ?1 AND device_id = ?2",
|
||||
params![identity_key, device_id],
|
||||
)
|
||||
.map_err(|e| StorageError::Db(e.to_string()))?;
|
||||
Ok(deleted > 0)
|
||||
}
|
||||
|
||||
fn device_count(&self, identity_key: &[u8]) -> Result<usize, StorageError> {
|
||||
let conn = self.lock_conn()?;
|
||||
let count: i64 = conn
|
||||
.query_row(
|
||||
"SELECT COUNT(*) FROM devices WHERE identity_key = ?1",
|
||||
params![identity_key],
|
||||
|row| row.get(0),
|
||||
)
|
||||
.map_err(|e| StorageError::Db(e.to_string()))?;
|
||||
Ok(count as usize)
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience extension for `rusqlite::OptionalExtension`.
|
||||
|
||||
Reference in New Issue
Block a user