feat: implement account recovery with encrypted backup bundles

Add recovery code generation (8 codes per setup), Argon2id key derivation,
ChaCha20-Poly1305 encrypted bundles, and server-side zero-knowledge storage.
Each code independently recovers the account. Includes core crypto module,
protobuf service (method IDs 750-752), server domain + handlers, SDK methods,
SQL migration, and CLI commands (/recovery setup, /recovery restore).
This commit is contained in:
2026-03-04 20:12:20 +01:00
parent 5b6d8209f0
commit 12b19b6931
14 changed files with 1120 additions and 1 deletions

View File

@@ -22,8 +22,11 @@ pub mod channel;
pub mod delivery;
pub mod device;
pub mod federation;
pub mod group;
pub mod keys;
pub mod moderation;
pub mod p2p;
pub mod recovery;
pub mod user;
/// Shared server state accessible by all v2 RPC handlers.
@@ -41,6 +44,31 @@ pub struct ServerState {
pub kt_log: Arc<std::sync::Mutex<quicproquo_kt::MerkleLog>>,
pub data_dir: PathBuf,
pub redact_logs: bool,
/// Idempotency dedup: message_id -> (seq, timestamp). TTL-cleaned by cleanup task.
pub seen_message_ids: Arc<DashMap<Vec<u8>, (u64, u64)>>,
/// Banned users: identity_key -> BanRecord.
pub banned_users: Arc<DashMap<Vec<u8>, BanRecord>>,
/// Moderation reports (append-only).
pub moderation_reports: Arc<std::sync::Mutex<Vec<ModerationReport>>>,
}
/// A ban record for a user.
#[derive(Debug, Clone)]
pub struct BanRecord {
pub reason: String,
pub banned_at: u64,
/// 0 = permanent.
pub expires_at: u64,
}
/// A stored moderation report.
#[derive(Debug, Clone)]
pub struct ModerationReport {
pub id: u64,
pub encrypted_report: Vec<u8>,
pub conversation_id: Vec<u8>,
pub reporter_identity: Vec<u8>,
pub timestamp: u64,
}
/// Validate the session token from the request context and return the
@@ -64,6 +92,18 @@ pub fn require_auth(state: &ServerState, ctx: &RequestContext) -> Result<Vec<u8>
if let Some(session) = state.sessions.get(token) {
let now = crate::auth::current_timestamp();
if session.expires_at > now && !session.identity_key.is_empty() {
// Check ban status.
if let Some(ban) = state.banned_users.get(&session.identity_key) {
if ban.expires_at == 0 || ban.expires_at > now {
return Err(HandlerResult::err(
RpcStatus::Forbidden,
"account banned",
));
}
// Ban expired — remove it.
drop(ban);
state.banned_users.remove(&session.identity_key);
}
return Ok(session.identity_key.clone());
}
}
@@ -94,7 +134,7 @@ pub fn domain_err(e: crate::domain::types::DomainError) -> HandlerResult {
| DomainError::BlobHashLength(_)
| DomainError::BadParams(_) => HandlerResult::err(RpcStatus::BadRequest, &e.to_string()),
DomainError::BlobNotFound | DomainError::DeviceNotFound => {
DomainError::BlobNotFound | DomainError::DeviceNotFound | DomainError::GroupNotFound => {
HandlerResult::err(RpcStatus::NotFound, &e.to_string())
}
@@ -190,6 +230,28 @@ pub fn build_registry() -> MethodRegistry<ServerState> {
channel::handle_create_channel,
);
// Group management (410-413)
reg.register(
method_ids::REMOVE_MEMBER,
"RemoveMember",
group::handle_remove_member,
);
reg.register(
method_ids::UPDATE_GROUP_METADATA,
"UpdateGroupMetadata",
group::handle_update_group_metadata,
);
reg.register(
method_ids::LIST_GROUP_MEMBERS,
"ListGroupMembers",
group::handle_list_group_members,
);
reg.register(
method_ids::ROTATE_KEYS,
"RotateKeys",
group::handle_rotate_keys,
);
// User (500-501)
reg.register(
method_ids::RESOLVE_USER,
@@ -276,6 +338,50 @@ pub fn build_registry() -> MethodRegistry<ServerState> {
federation::handle_federation_health,
);
// Moderation (420-424)
reg.register(
method_ids::REPORT_MESSAGE,
"ReportMessage",
moderation::handle_report_message,
);
reg.register(
method_ids::BAN_USER,
"BanUser",
moderation::handle_ban_user,
);
reg.register(
method_ids::UNBAN_USER,
"UnbanUser",
moderation::handle_unban_user,
);
reg.register(
method_ids::LIST_REPORTS,
"ListReports",
moderation::handle_list_reports,
);
reg.register(
method_ids::LIST_BANNED,
"ListBanned",
moderation::handle_list_banned,
);
// Recovery (750-752)
reg.register(
method_ids::STORE_RECOVERY_BUNDLE,
"StoreRecoveryBundle",
recovery::handle_store_recovery_bundle,
);
reg.register(
method_ids::FETCH_RECOVERY_BUNDLE,
"FetchRecoveryBundle",
recovery::handle_fetch_recovery_bundle,
);
reg.register(
method_ids::DELETE_RECOVERY_BUNDLE,
"DeleteRecoveryBundle",
recovery::handle_delete_recovery_bundle,
);
// Account (950)
reg.register(
method_ids::DELETE_ACCOUNT,