fix: implement vault key decryption on login + fix token refresh via cookie
Three bugs fixed: 1. Phase 30 (auth.ts): After SRP login the encrypted_key_set was returned from the server but the vault key and RSA private key were never unwrapped with the AUK. keyStore.getVaultKey() was always null, causing Tier 1 config-backup diffs to crash with a TypeError. Fix: unwrap vault key and private key using crypto.subtle.unwrapKey after successful SRP verification. Non-fatal: warns to console if decryption fails so login always succeeds. 2. Token refresh (auth.py): The /refresh endpoint required refresh_token in the request body, but the frontend never stored or sent it. After the 15- minute access token TTL, all authenticated API calls would fail silently because the interceptor sent an empty body and received 422 (not 401), so the retry loop never fired. Fix: login/srpVerify now set an httpOnly refresh_token cookie scoped to /api/auth/refresh. The refresh endpoint now accepts the token from either cookie (preferred) or body (legacy). Logout clears both cookies. RefreshRequest.refresh_token is now Optional to allow empty-body calls. 3. Silent token rotation: the /refresh endpoint now also rotates the refresh token cookie on each use (issues a fresh token), reducing the window for stolen refresh token replay.
This commit is contained in:
@@ -145,7 +145,43 @@ export const useAuth = create<AuthState>((set, get) => ({
|
||||
|
||||
// 8. Store AUK and unlock key set
|
||||
keyStore.setAUK(auk)
|
||||
// TODO (Phase 30): Decrypt encrypted_key_set with AUK to get vault key
|
||||
|
||||
// Decrypt encrypted_key_set with AUK to get vault key + RSA private key.
|
||||
// Non-fatal: if decryption fails (e.g. corrupted key set, wrong AUK) we log
|
||||
// a warning and continue. Server-side Transit encryption still works; only
|
||||
// Tier 1 (client-side) encrypted data will be inaccessible until re-auth.
|
||||
if (result.encrypted_key_set) {
|
||||
const ks = result.encrypted_key_set
|
||||
try {
|
||||
const b64 = (s: string) => Uint8Array.from(atob(s), (c) => c.charCodeAt(0))
|
||||
|
||||
// Unwrap vault key (AES-256-GCM) using AUK
|
||||
const vaultKey = await crypto.subtle.unwrapKey(
|
||||
'raw',
|
||||
b64(ks.encrypted_vault_key),
|
||||
auk,
|
||||
{ name: 'AES-GCM', iv: b64(ks.vault_key_nonce) },
|
||||
{ name: 'AES-GCM', length: 256 },
|
||||
false, // non-extractable
|
||||
['encrypt', 'decrypt'],
|
||||
)
|
||||
keyStore.setVaultKey(vaultKey)
|
||||
|
||||
// Unwrap RSA-OAEP private key using AUK
|
||||
const privateKey = await crypto.subtle.unwrapKey(
|
||||
'pkcs8',
|
||||
b64(ks.encrypted_private_key),
|
||||
auk,
|
||||
{ name: 'AES-GCM', iv: b64(ks.private_key_nonce) },
|
||||
{ name: 'RSA-OAEP', hash: 'SHA-256' },
|
||||
false, // non-extractable
|
||||
['decrypt'],
|
||||
)
|
||||
keyStore.setPrivateKey(privateKey)
|
||||
} catch (e) {
|
||||
console.warn('[auth] key set decryption failed (Tier 1 data will be inaccessible):', e)
|
||||
}
|
||||
}
|
||||
|
||||
// 9. Store Secret Key in IndexedDB for future logins on this device
|
||||
await keyStore.storeSecretKey(email, secretKeyBytes)
|
||||
|
||||
Reference in New Issue
Block a user