Files
quicproquo/crates/quicproquo-server/src/domain/recovery.rs
Christian Nennemann 12b19b6931 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).
2026-03-04 20:12:20 +01:00

77 lines
2.4 KiB
Rust

//! Recovery domain logic — encrypted recovery bundle CRUD.
use std::sync::Arc;
use crate::storage::Store;
use super::types::DomainError;
/// Maximum recovery bundle size: 64 KiB.
const MAX_BUNDLE_SIZE: usize = 64 * 1024;
/// Default TTL for recovery bundles: 90 days.
pub const DEFAULT_TTL_SECS: u64 = 90 * 24 * 60 * 60;
/// Domain service for recovery bundle operations.
pub struct RecoveryService {
pub store: Arc<dyn Store>,
}
impl RecoveryService {
/// Store an encrypted recovery bundle.
///
/// `token_hash` is the SHA-256 of a recovery token derived from the code.
/// `bundle` is the encrypted blob (opaque to server).
/// `ttl_secs` is the time-to-live; 0 uses the default (90 days).
pub fn store_bundle(
&self,
token_hash: &[u8],
bundle: Vec<u8>,
ttl_secs: u64,
) -> Result<(), DomainError> {
if token_hash.len() != 32 {
return Err(DomainError::BadParams(format!(
"token_hash must be 32 bytes, got {}",
token_hash.len()
)));
}
if bundle.is_empty() {
return Err(DomainError::BadParams("recovery bundle must not be empty".into()));
}
if bundle.len() > MAX_BUNDLE_SIZE {
return Err(DomainError::BadParams(format!(
"recovery bundle exceeds max size ({} > {MAX_BUNDLE_SIZE})",
bundle.len()
)));
}
let ttl = if ttl_secs == 0 { DEFAULT_TTL_SECS } else { ttl_secs };
self.store.store_recovery_bundle(token_hash, bundle, ttl)?;
Ok(())
}
/// Fetch an encrypted recovery bundle by token_hash.
pub fn fetch_bundle(&self, token_hash: &[u8]) -> Result<Option<Vec<u8>>, DomainError> {
if token_hash.len() != 32 {
return Err(DomainError::BadParams(format!(
"token_hash must be 32 bytes, got {}",
token_hash.len()
)));
}
let bundle = self.store.get_recovery_bundle(token_hash)?;
Ok(bundle)
}
/// Delete an encrypted recovery bundle by token_hash.
pub fn delete_bundle(&self, token_hash: &[u8]) -> Result<bool, DomainError> {
if token_hash.len() != 32 {
return Err(DomainError::BadParams(format!(
"token_hash must be 32 bytes, got {}",
token_hash.len()
)));
}
let deleted = self.store.delete_recovery_bundle(token_hash)?;
Ok(deleted)
}
}