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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user