feat: zero-knowledge recovery (Phase 2) — wrapped-DEK model
Introduces a wrapped-DEK recovery model so a forgotten password doesn't lose all encrypted data. The encryption key becomes a random DEK (not derived from the password); the DEK is wrapped under both a password-derived KEK and a recovery-phrase-derived KEK, and both wrapped forms are stored on the server. Crypto (crypto/keys.ts): - DEK generation (random AES-256-GCM), KEK derivation (PBKDF2 password/recovery), wrapDek/unwrapDek/rewrapDek. - setupEncryption(password, recoveryPhrase?) — generates a DEK, wraps under both KEKs, returns wrapped forms + recovery proof. - unlockWithPassword(password, wrappedDek) — derives password KEK, unwraps DEK. - unlockWithRecovery(phrase, wrappedDek) — derives recovery KEK, unwraps DEK. Backend: - User model: wrapped_dek, wrapped_dek_iv, recovery_wrapped_dek, recovery_wrapped_dek_iv fields. - RegisterRequest accepts wrapped-DEK fields; stored verbatim. - AuthResponse returns wrapped_dek + wrapped_dek_iv (for login unwrapping). - New GET /api/auth/recovery-info?email= — returns recovery-wrapped DEK. - RecoverPasswordRequest gains new_wrapped_dek + new_wrapped_dek_iv. - change-password also accepts + stores re-wrapped DEK. Frontend: - Auth store: login unwraps DEK from response; new recover() action fetches recovery-wrapped DEK, unwraps with phrase, re-wraps under new password. - RecoveryPage (new): email + recovery phrase + new password flow. - LoginPage: 'Forgot password? Recover' link. App.tsx: /recover route. Verified: backend 21 tests, 0 warnings; frontend build clean, 20 tests.
This commit is contained in:
parent
62ef1abb84
commit
7a641dec00
13 changed files with 528 additions and 67 deletions
|
|
@ -187,6 +187,10 @@ pub struct ChangePasswordRequest {
|
|||
pub current_password: String,
|
||||
#[validate(length(min = 8))]
|
||||
pub new_password: String,
|
||||
/// Re-wrapped DEK under the new password KEK (zero-knowledge: the client
|
||||
/// re-wraps the DEK before sending).
|
||||
pub new_wrapped_dek: Option<String>,
|
||||
pub new_wrapped_dek_iv: Option<String>,
|
||||
}
|
||||
|
||||
pub async fn change_password(
|
||||
|
|
@ -268,7 +272,11 @@ pub async fn change_password(
|
|||
|
||||
// Update password (this also bumps token_version in memory)
|
||||
match user.update_password(req.new_password) {
|
||||
Ok(_) => {}
|
||||
Ok(_) => {
|
||||
// Store the re-wrapped DEK (zero-knowledge: client re-wrapped it).
|
||||
user.wrapped_dek = req.new_wrapped_dek;
|
||||
user.wrapped_dek_iv = req.new_wrapped_dek_iv;
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to hash new password: {}", e);
|
||||
return (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue