diff --git a/backend/src/app.rs b/backend/src/app.rs index 63b86a5..7a6ca1f 100644 --- a/backend/src/app.rs +++ b/backend/src/app.rs @@ -34,8 +34,7 @@ pub fn build_app(state: AppState) -> Router { .route( "/api/auth/recover-password", post(handlers::recover_password), - ) - .route("/api/auth/recovery-info", get(handlers::recovery_info)); + ); // Build protected routes (auth required) let protected_routes = Router::new() diff --git a/backend/src/handlers/auth.rs b/backend/src/handlers/auth.rs index cbe8836..66e8a25 100644 --- a/backend/src/handlers/auth.rs +++ b/backend/src/handlers/auth.rs @@ -1,7 +1,4 @@ -use axum::{ - extract::Extension, extract::Query, extract::State, http::StatusCode, response::IntoResponse, - Json, -}; +use axum::{extract::Extension, extract::State, http::StatusCode, response::IntoResponse, Json}; use mongodb::bson::oid::ObjectId; use serde::{Deserialize, Serialize}; use validator::Validate; @@ -19,14 +16,8 @@ pub struct RegisterRequest { pub username: String, #[validate(length(min = 8))] pub password: String, - /// Optional recovery phrase hash (for server-side verification) + /// Optional recovery phrase for password recovery pub recovery_phrase: Option, - /// Wrapped DEK (base64 ciphertext) — encrypted under the password KEK - pub wrapped_dek: Option, - pub wrapped_dek_iv: Option, - /// Wrapped DEK (base64 ciphertext) — encrypted under the recovery KEK - pub recovery_wrapped_dek: Option, - pub recovery_wrapped_dek_iv: Option, } #[derive(Debug, Serialize)] @@ -36,10 +27,6 @@ pub struct AuthResponse { pub user_id: String, pub email: String, pub username: String, - /// Wrapped DEK + IV (for the client to unwrap at login). Absent if the - /// user has no wrapped DEK (legacy/pre-recovery accounts). - pub wrapped_dek: Option, - pub wrapped_dek_iv: Option, } pub async fn register( @@ -83,11 +70,11 @@ pub async fn register( } // Create new user - let mut user = match User::new( + let user = match User::new( req.email.clone(), req.username.clone(), req.password, - req.recovery_phrase.clone(), + req.recovery_phrase, ) { Ok(u) => u, Err(e) => { @@ -102,13 +89,6 @@ pub async fn register( } }; - // Store the wrapped-DEK fields (zero-knowledge: server stores verbatim). - user.wrapped_dek = req.wrapped_dek; - user.wrapped_dek_iv = req.wrapped_dek_iv; - user.recovery_wrapped_dek = req.recovery_wrapped_dek.clone(); - user.recovery_wrapped_dek_iv = req.recovery_wrapped_dek_iv.clone(); - user.recovery_enabled = req.recovery_wrapped_dek.is_some(); - // Get token_version before saving let token_version = user.token_version; @@ -199,10 +179,8 @@ pub async fn register( token, refresh_token, user_id: user_id.to_string(), - email: user.email.clone(), - username: user.username.clone(), - wrapped_dek: user.wrapped_dek.clone(), - wrapped_dek_iv: user.wrapped_dek_iv.clone(), + email: user.email, + username: user.username, }; (StatusCode::CREATED, Json(response)).into_response() @@ -409,10 +387,8 @@ pub async fn login( token, refresh_token, user_id: user_id.to_string(), - email: user.email.clone(), - username: user.username.clone(), - wrapped_dek: user.wrapped_dek.clone(), - wrapped_dek_iv: user.wrapped_dek_iv.clone(), + email: user.email, + username: user.username, }; (StatusCode::OK, Json(response)).into_response() @@ -422,61 +398,10 @@ pub async fn login( pub struct RecoverPasswordRequest { #[validate(email)] pub email: String, - /// The recovery-phrase-derived proof (base64) — the server verifies it - /// against the stored recovery_phrase_hash. #[validate(length(min = 1))] pub recovery_phrase: String, - /// The new auth secret (base64) — the server hashes it as the new password. #[validate(length(min = 8))] pub new_password: String, - /// The DEK re-wrapped under the new password KEK (base64 ciphertext + IV). - pub new_wrapped_dek: Option, - pub new_wrapped_dek_iv: Option, -} - -/// GET /api/auth/recovery-info?email=... — returns the recovery-wrapped DEK so -/// the client can unwrap it with the recovery phrase. Public (the wrapped DEK -/// is useless without the recovery phrase). -#[derive(Debug, Deserialize)] -pub struct RecoveryInfoQuery { - pub email: String, -} - -pub async fn recovery_info( - State(state): State, - Query(query): Query, -) -> impl IntoResponse { - let user = match state.db.find_user_by_email(&query.email).await { - Ok(Some(u)) => u, - Ok(None) => { - // Don't reveal whether the email exists — return disabled. - return ( - StatusCode::OK, - Json(serde_json::json!({ - "recovery_enabled": false, - })), - ) - .into_response(); - } - Err(e) => { - tracing::error!("Recovery info lookup failed: {}", e); - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(serde_json::json!({ "error": "database error" })), - ) - .into_response(); - } - }; - - ( - StatusCode::OK, - Json(serde_json::json!({ - "recovery_enabled": user.recovery_enabled, - "recovery_wrapped_dek": user.recovery_wrapped_dek, - "recovery_wrapped_dek_iv": user.recovery_wrapped_dek_iv, - })), - ) - .into_response() } pub async fn recover_password( @@ -543,14 +468,9 @@ pub async fn recover_password( } } - // Update password + store the new wrapped DEK (re-wrapped client-side under - // the new password KEK). The server never sees the DEK itself. + // Update password match user.update_password(req.new_password) { - Ok(_) => { - // Store the re-wrapped DEK so the new password can unlock it. - user.wrapped_dek = req.new_wrapped_dek; - user.wrapped_dek_iv = req.new_wrapped_dek_iv; - } + Ok(_) => {} Err(e) => { tracing::error!("Failed to update password: {}", e); return ( diff --git a/backend/src/handlers/mod.rs b/backend/src/handlers/mod.rs index 632369f..417a445 100644 --- a/backend/src/handlers/mod.rs +++ b/backend/src/handlers/mod.rs @@ -14,7 +14,7 @@ pub mod users; pub use appointments::{ create_appointment, delete_appointment, get_appointment, list_appointments, update_appointment, }; -pub use auth::{login, logout, recover_password, recovery_info, refresh, register}; +pub use auth::{login, logout, recover_password, refresh, register}; pub use health::{health_check, ready_check}; pub use health_stats::{ create_health_stat, delete_health_stat, get_health_stat, get_health_trends, list_health_stats, diff --git a/backend/src/handlers/users.rs b/backend/src/handlers/users.rs index 0dc0660..664331b 100644 --- a/backend/src/handlers/users.rs +++ b/backend/src/handlers/users.rs @@ -187,10 +187,6 @@ 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, - pub new_wrapped_dek_iv: Option, } pub async fn change_password( @@ -272,11 +268,7 @@ pub async fn change_password( // Update password (this also bumps token_version in memory) match user.update_password(req.new_password) { - 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; - } + Ok(_) => {} Err(e) => { tracing::error!("Failed to hash new password: {}", e); return ( diff --git a/backend/src/models/user.rs b/backend/src/models/user.rs index 74de914..53b9cf8 100644 --- a/backend/src/models/user.rs +++ b/backend/src/models/user.rs @@ -23,20 +23,6 @@ pub struct User { /// Whether password recovery is enabled for this user pub recovery_enabled: bool, - /// Wrapped DEK (data encryption key) — the DEK encrypted under the password - /// KEK. Base64 ciphertext. The server stores this verbatim; only the client - /// can unwrap it. (Phase 2 zero-knowledge recovery.) - #[serde(skip_serializing_if = "Option::is_none")] - pub wrapped_dek: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub wrapped_dek_iv: Option, - - /// Wrapped DEK under the recovery KEK — for password recovery. - #[serde(skip_serializing_if = "Option::is_none")] - pub recovery_wrapped_dek: Option, - #[serde(skip_serializing_if = "Option::is_none")] - pub recovery_wrapped_dek_iv: Option, - /// Token version for invalidating all tokens on password change pub token_version: i32, @@ -89,10 +75,6 @@ impl User { password_hash, recovery_phrase_hash, recovery_enabled, - wrapped_dek: None, - wrapped_dek_iv: None, - recovery_wrapped_dek: None, - recovery_wrapped_dek_iv: None, token_version: 0, created_at: now, last_active: now, diff --git a/web/normogen-web/src/App.tsx b/web/normogen-web/src/App.tsx index 652fa2a..0864a43 100644 --- a/web/normogen-web/src/App.tsx +++ b/web/normogen-web/src/App.tsx @@ -1,7 +1,6 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'; import { LoginPage } from './pages/LoginPage'; import { RegisterPage } from './pages/RegisterPage'; -import { RecoveryPage } from './pages/RecoveryPage'; import { Dashboard } from './pages/Dashboard'; import { ProtectedRoute } from './components/common/ProtectedRoute'; @@ -12,7 +11,6 @@ function App() { {/* Public routes */} } /> } /> - } /> {/* Protected routes */} { - const key = await crypto.subtle.importKey( +/** + * Derive the auth secret (base64) and encryption key (CryptoKey) from a password. + */ +export async function deriveAuthAndEncKeys(password: string): Promise<{ + authSecret: string; + encKey: CryptoKey; +}> { + const passwordKey = await crypto.subtle.importKey( 'raw', - encoder.encode(password) as BufferSource, + encoder.encode(password), 'PBKDF2', false, ['deriveBits'], ); - return crypto.subtle.deriveBits( - { name: 'PBKDF2', salt: encoder.encode(salt) as BufferSource, iterations: PBKDF2_ITERATIONS, hash: 'SHA-256' }, - key, + + // Auth secret: 32 raw bytes, base64-encoded for transport. + const authBits = await crypto.subtle.deriveBits( + { name: 'PBKDF2', salt: encoder.encode(AUTH_SALT), iterations: PBKDF2_ITERATIONS, hash: 'SHA-256' }, + passwordKey, KEY_BITS, ); + const authSecret = base64(new Uint8Array(authBits)); + + // Encryption key: importable AES-GCM key, non-extractable. + const encBits = await crypto.subtle.deriveBits( + { name: 'PBKDF2', salt: encoder.encode(ENC_SALT), iterations: PBKDF2_ITERATIONS, hash: 'SHA-256' }, + passwordKey, + KEY_BITS, + ); + const encKey = await crypto.subtle.importKey( + 'raw', + encBits, + { name: 'AES-GCM' }, + false, + ['encrypt', 'decrypt'], + ); + + return { authSecret, encKey }; } function base64(bytes: Uint8Array): string { @@ -52,182 +67,32 @@ function base64(bytes: Uint8Array): string { return btoa(bin); } -/** Derive the auth secret (base64) from the password — sent to the server. */ -export async function deriveAuthSecret(password: string): Promise { - const bits = await pbkdf2DeriveBits(password, AUTH_SALT); - return base64(new Uint8Array(bits)); -} - -/** Derive a KEK (extractable AES-GCM key) from a password or recovery phrase. */ -async function deriveKEK(secret: string, salt: string): Promise { - const bits = await pbkdf2DeriveBits(secret, salt); - return crypto.subtle.importKey( - 'raw', - bits, - { name: 'AES-GCM' }, - true, // extractable so we can export for wrapping - ['encrypt', 'decrypt'], - ); -} - // --------------------------------------------------------------------------- -// DEK generation + wrapping -// --------------------------------------------------------------------------- - -/** Generate a random AES-256-GCM DEK (extractable for wrapping). */ -export async function generateDek(): Promise { - return crypto.subtle.generateKey( - { name: 'AES-GCM', length: KEY_BITS }, - true, // extractable so we can export raw bytes for wrapping - ['encrypt', 'decrypt'], - ); -} - -/** Export a DEK to raw bytes, encrypt (wrap) it under a KEK, return base64. */ -export async function wrapDek(dek: CryptoKey, kek: CryptoKey): Promise { - const rawDek = await crypto.subtle.exportKey('raw', dek); - return encrypt( - new TextDecoder().decode(new Uint8Array(rawDek)), - kek, - ); -} - -/** Decrypt (unwrap) a wrapped DEK and import as a non-extractable AES-GCM key. */ -export async function unwrapDek(payload: CipherPayload, kek: CryptoKey): Promise { - const rawDekB64 = await decrypt(payload, kek); - // The wrapped DEK was encrypted as a base64 string of the raw key bytes. - const rawDek = Uint8Array.from(atob(rawDekB64), (c) => c.charCodeAt(0)); - return crypto.subtle.importKey( - 'raw', - rawDek as BufferSource, - { name: 'AES-GCM' }, - false, // non-extractable in the session (can't be re-exported) - ['encrypt', 'decrypt'], - ); -} - -/** Re-wrap a DEK under a new KEK (for password change / post-recovery). */ -export async function rewrapDek( - dek: CryptoKey, - newSecret: string, - salt: string = PASSWORD_KEK_SALT, -): Promise { - const newKek = await deriveKEK(newSecret, salt); - return wrapDek(dek, newKek); -} - -// --------------------------------------------------------------------------- -// High-level flows -// --------------------------------------------------------------------------- - -export interface EncryptionSetup { - authSecret: string; - dek: CryptoKey; - passwordWrappedDek: CipherPayload; - recoveryWrappedDek?: CipherPayload; - recoveryKekHash?: string; // base64 hash of the recovery KEK proof (for server verification) -} - -/** - * Full setup at registration: derive auth secret, generate a DEK, wrap it under - * the password KEK and (optionally) the recovery-phrase KEK. - */ -export async function setupEncryption( - password: string, - recoveryPhrase?: string, -): Promise { - const authSecret = await deriveAuthSecret(password); - const dek = await generateDek(); - - const passwordKek = await deriveKEK(password, PASSWORD_KEK_SALT); - const passwordWrappedDek = await wrapDek(dek, passwordKek); - - let recoveryWrappedDek: CipherPayload | undefined; - let recoveryKekHash: string | undefined; - - if (recoveryPhrase) { - const recoveryKek = await deriveKEK(recoveryPhrase, RECOVERY_KEK_SALT); - recoveryWrappedDek = await wrapDek(dek, recoveryKek); - // A proof the server can verify: derive a separate value from the recovery - // phrase and hash it. The server stores this hash; at recovery time the - // client sends the derived value and the server PBKDF2-verifies it. - // We send the recovery auth proof (base64 of a PBKDF2 derivation). - recoveryKekHash = await deriveAuthSecret(recoveryPhrase); // reuse the auth derivation as the proof - } - - return { authSecret, dek, passwordWrappedDek, recoveryWrappedDek, recoveryKekHash }; -} - -export interface UnlockResult { - authSecret: string; - dek: CryptoKey; -} - -/** - * At login: derive the auth secret and unwrap the DEK from the password-wrapped form. - */ -export async function unlockWithPassword( - password: string, - passwordWrappedDek: CipherPayload, -): Promise { - const authSecret = await deriveAuthSecret(password); - const passwordKek = await deriveKEK(password, PASSWORD_KEK_SALT); - const dek = await unwrapDek(passwordWrappedDek, passwordKek); - return { authSecret, dek }; -} - -/** - * At recovery: unwrap the DEK from the recovery-wrapped form using the recovery phrase. - */ -export async function unlockWithRecovery( - recoveryPhrase: string, - recoveryWrappedDek: CipherPayload, -): Promise { - const recoveryKek = await deriveKEK(recoveryPhrase, RECOVERY_KEK_SALT); - return unwrapDek(recoveryWrappedDek, recoveryKek); -} - -// --------------------------------------------------------------------------- -// In-memory key store (unchanged from Phase 1) +// In-memory encryption-key store. +// +// The encKey lives only in memory for the lifetime of the authenticated +// session (the tab). It is deliberately NOT persisted — losing the tab or +// closing the browser requires re-entering the password to re-derive it. // --------------------------------------------------------------------------- let currentEncKey: CryptoKey | null = null; +/** Set the session encryption key (called on login/register). */ export function setEncKey(key: CryptoKey): void { currentEncKey = key; } +/** Get the session encryption key, or null if not authenticated. */ export function getEncKey(): CryptoKey | null { return currentEncKey; } +/** Clear the session encryption key (called on logout). */ export function clearEncKey(): void { currentEncKey = null; } +/** True if a session encryption key is available (i.e. the user can encrypt/decrypt). */ export function hasEncKey(): boolean { return currentEncKey !== null; } - -// --------------------------------------------------------------------------- -// Backward compat: the old deriveAuthAndEncKeys (Phase 1 direct-from-password -// enc key). Kept for tests; new code uses setupEncryption/unlockWithPassword. -// --------------------------------------------------------------------------- - -export async function deriveAuthAndEncKeys(password: string): Promise<{ - authSecret: string; - encKey: CryptoKey; -}> { - const authSecret = await deriveAuthSecret(password); - // Phase 1 derived the enc key directly from the password. For backward compat - // with tests that don't have a wrapped DEK, derive it the old way. - const encBits = await pbkdf2DeriveBits(password, 'normogen-enc-v1'); - const encKey = await crypto.subtle.importKey( - 'raw', - encBits, - { name: 'AES-GCM' }, - false, - ['encrypt', 'decrypt'], - ); - return { authSecret, encKey }; -} diff --git a/web/normogen-web/src/pages/LoginPage.tsx b/web/normogen-web/src/pages/LoginPage.tsx index 4d04138..afba118 100644 --- a/web/normogen-web/src/pages/LoginPage.tsx +++ b/web/normogen-web/src/pages/LoginPage.tsx @@ -103,11 +103,6 @@ export const LoginPage: React.FC = () => { Don't have an account? Sign Up - - - Forgot password? Recover - - diff --git a/web/normogen-web/src/pages/RecoveryPage.tsx b/web/normogen-web/src/pages/RecoveryPage.tsx deleted file mode 100644 index ee4a07b..0000000 --- a/web/normogen-web/src/pages/RecoveryPage.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import { useState, type FC } from 'react'; -import { useNavigate, Link } from 'react-router-dom'; -import { - Container, - Paper, - TextField, - Button, - Typography, - Box, - Alert, - CircularProgress, -} from '@mui/material'; -import { useAuthStore } from '../store/useStore'; - -export const RecoveryPage: FC = () => { - const navigate = useNavigate(); - const { recover, isLoading, error, clearError } = useAuthStore(); - const [email, setEmail] = useState(''); - const [recoveryPhrase, setRecoveryPhrase] = useState(''); - const [newPassword, setNewPassword] = useState(''); - const [confirmPassword, setConfirmPassword] = useState(''); - const [localError, setLocalError] = useState(''); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - clearError(); - setLocalError(''); - - if (newPassword !== confirmPassword) { - setLocalError('Passwords do not match'); - return; - } - if (newPassword.length < 8) { - setLocalError('Password must be at least 8 characters'); - return; - } - - try { - await recover(email, recoveryPhrase, newPassword); - navigate('/login', { replace: true }); - } catch { - /* error surfaced via store */ - } - }; - - return ( - - - - - Recover Account - - - Enter your recovery phrase to reset your password and regain access to your encrypted data. - - - {(error || localError) && ( - - {error || localError} - - )} - - - setEmail(e.target.value)} - disabled={isLoading} - autoFocus - /> - setRecoveryPhrase(e.target.value)} - disabled={isLoading} - /> - setNewPassword(e.target.value)} - disabled={isLoading} - /> - setConfirmPassword(e.target.value)} - disabled={isLoading} - /> - - - - Back to Sign In - - - - - - - ); -}; - -export default RecoveryPage; diff --git a/web/normogen-web/src/services/api.ts b/web/normogen-web/src/services/api.ts index eb8f6be..ca490d0 100644 --- a/web/normogen-web/src/services/api.ts +++ b/web/normogen-web/src/services/api.ts @@ -215,23 +215,6 @@ class ApiService { return response.data; } - // ---- Recovery (zero-knowledge) ---- - - async getRecoveryInfo(email: string): Promise<{ recovery_enabled: boolean; recovery_wrapped_dek?: string; recovery_wrapped_dek_iv?: string }> { - const response = await this.client.get('/auth/recovery-info', { params: { email } }); - return response.data; - } - - async recoverPassword(email: string, recoveryProof: string, newAuthSecret: string, newWrappedDek?: string, newWrappedDekIv?: string): Promise { - await this.client.post('/auth/recover-password', { - email, - recovery_phrase: recoveryProof, - new_password: newAuthSecret, - new_wrapped_dek: newWrappedDek, - new_wrapped_dek_iv: newWrappedDekIv, - }); - } - // ---- Profile (zero-knowledge: opaque encrypted name) ---- async getProfile(): Promise { diff --git a/web/normogen-web/src/store/useStore.ts b/web/normogen-web/src/store/useStore.ts index 4d460fa..3e19a04 100644 --- a/web/normogen-web/src/store/useStore.ts +++ b/web/normogen-web/src/store/useStore.ts @@ -2,16 +2,11 @@ import { create } from 'zustand'; import { devtools, persist } from 'zustand/middleware'; import { deriveAuthAndEncKeys, - setupEncryption, - unlockWithPassword, - unlockWithRecovery, - rewrapDek, setEncKey, clearEncKey, getEncKey, encryptJson, decryptJson, - type CipherPayload, } from '../crypto'; import { User, @@ -36,7 +31,6 @@ interface AuthState { // Actions login: (email: string, password: string) => Promise; register: (username: string, email: string, password: string) => Promise; - recover: (email: string, recoveryPhrase: string, newPassword: string) => Promise; logout: () => Promise; clearError: () => void; loadUser: () => Promise; @@ -124,22 +118,11 @@ export const useAuthStore = create()( login: async (email: string, password: string) => { set({ isLoading: true, error: null }); try { - // Zero-knowledge: derive auth secret (sent to server) + encryption key. + // Zero-knowledge: derive auth secret + encryption key from password. const { authSecret, encKey } = await deriveAuthAndEncKeys(password); setEncKey(encKey); + // Send the derived auth secret (not the raw password) to the server. const response = await apiService.login(email, authSecret); - // If the server returned a wrapped DEK, unwrap it with the password. - if (response.wrapped_dek && response.wrapped_dek_iv) { - try { - const { dek } = await unlockWithPassword(password, { - data: response.wrapped_dek, - iv: response.wrapped_dek_iv, - }); - setEncKey(dek); - } catch { - // Fallback to the PBKDF2-derived key (Phase 1 compat). - } - } set({ user: { user_id: response.user_id, @@ -167,9 +150,6 @@ export const useAuthStore = create()( try { const { authSecret, encKey } = await deriveAuthAndEncKeys(password); setEncKey(encKey); - // For now register uses the Phase 1 key derivation (no recovery phrase - // in the basic register flow). The wrapped-DEK recovery fields are - // optional on the register request. const response = await apiService.register({ username, email, @@ -196,43 +176,6 @@ export const useAuthStore = create()( } }, - recover: async (email: string, recoveryPhrase: string, newPassword: string) => { - set({ isLoading: true, error: null }); - try { - // 1. Fetch the recovery-wrapped DEK from the server. - const info = await apiService.getRecoveryInfo(email); - if (!info.recovery_enabled || !info.recovery_wrapped_dek || !info.recovery_wrapped_dek_iv) { - throw new Error('Recovery is not enabled for this account'); - } - // 2. Unwrap the DEK using the recovery phrase. - const dek = await unlockWithRecovery(recoveryPhrase, { - data: info.recovery_wrapped_dek, - iv: info.recovery_wrapped_dek_iv, - }); - // 3. Re-wrap the DEK under the new password. - const newWrapped = await rewrapDek(dek, newPassword); - // 4. Derive the new auth secret + recovery proof. - const newAuthSecret = (await deriveAuthAndEncKeys(newPassword)).authSecret; - const recoveryProof = (await deriveAuthAndEncKeys(recoveryPhrase)).authSecret; - // 5. Send the recovery request. - await apiService.recoverPassword( - email, - recoveryProof, - newAuthSecret, - newWrapped.data, - newWrapped.iv, - ); - set({ isLoading: false }); - } catch (error: any) { - clearEncKey(); - set({ - error: error.message || 'Recovery failed', - isLoading: false, - }); - throw error; - } - }, - logout: async () => { clearEncKey(); await apiService.logout(); diff --git a/web/normogen-web/src/types/api.ts b/web/normogen-web/src/types/api.ts index cb52fc7..62918ad 100644 --- a/web/normogen-web/src/types/api.ts +++ b/web/normogen-web/src/types/api.ts @@ -40,8 +40,6 @@ export interface AuthTokens { user_id: string; username: string; email?: string; - wrapped_dek?: string; - wrapped_dek_iv?: string; } export interface LoginRequest {