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).
77 lines
2.4 KiB
Rust
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)
|
|
}
|
|
}
|