feat: TOTP as standalone re-auth method alongside WebAuthn/password

Re-verification (TTL expired, key still cached) now accepts any ONE of:
- WebAuthn (biometric/PIN)
- TOTP code alone (no password needed)
- Password alone (no TOTP needed)

First-device setup still requires password (+ TOTP if configured) since
the password is needed to derive the encryption key.

Added reverifyWithTOTP() and reverifyWithPassword() to sync.js.
Auth prompt UI adapts: first-device shows password+TOTP fields,
re-verify shows all three methods as alternatives.

https://claude.ai/code/session_01SWSwDfMVij53bCTNSCLMwn
This commit is contained in:
Claude
2026-03-26 18:42:05 +00:00
parent 7fd70e0891
commit 51a25a208b
3 changed files with 165 additions and 26 deletions
+73 -2
View File
@@ -139,8 +139,8 @@ const SilentSendSync = {
/**
* Authenticate with password (+ optional TOTP) and cache the key.
* Called from the UI — typically only needed ONCE per device.
* After this, the key persists in IndexedDB and WebAuthn handles
* any re-verification.
* After this, the key persists in IndexedDB and re-verification
* can use WebAuthn or TOTP alone.
*
* @param {string} password
* @param {string} [totpCode] — required if TOTP is configured
@@ -189,6 +189,77 @@ const SilentSendSync = {
return { success: true };
},
/**
* Re-verify identity using TOTP code alone (no password needed).
* Only works when the key is already cached (not first-device setup).
* Resets the TTL timer on success.
*
* @param {string} totpCode — 6-digit TOTP code
* @returns {{ success: boolean, reason?: string }}
*/
async reverifyWithTOTP(totpCode) {
const config = await this._getSyncEncryption();
if (!config?.enabled) return { success: false, reason: 'Encryption not enabled.' };
if (!config.totpSecret) return { success: false, reason: 'TOTP not configured.' };
// Must have a cached key — TOTP can't derive one
const cached = await SilentSendCrypto.getCachedKey();
if (!cached) return { success: false, reason: 'No cached key. Password required for first setup.' };
// Validate the TOTP code
const valid = await SilentSendCrypto.validateTOTP(config.totpSecret, totpCode);
if (!valid) return { success: false, reason: 'Invalid TOTP code.' };
// Reset the TTL timer
await SilentSendCrypto.markVerified(config.ttlDays ?? 90);
return { success: true };
},
/**
* Re-verify identity using password alone (no TOTP needed).
* Only works when the key is already cached (not first-device setup).
* Resets the TTL timer on success.
*
* @param {string} password
* @returns {{ success: boolean, reason?: string }}
*/
async reverifyWithPassword(password) {
const config = await this._getSyncEncryption();
if (!config?.enabled) return { success: false, reason: 'Encryption not enabled.' };
// Must have a cached key
const cached = await SilentSendCrypto.getCachedKey();
if (!cached) return { success: false, reason: 'No cached key. Full authentication required.' };
// Verify password against the verification blob
const { key } = await SilentSendCrypto.deriveAndReturnKey(password, config.salt);
if (config.verificationBlob) {
try {
await SilentSendCrypto.decryptWithKey(config.verificationBlob, key);
} catch {
return { success: false, reason: 'Wrong password.' };
}
}
// Reset the TTL timer
await SilentSendCrypto.markVerified(config.ttlDays ?? 90);
return { success: true };
},
/**
* Check if re-verification is needed (TTL expired but key exists).
* Different from needsAuth() which checks if the key is missing entirely.
*/
async needsReverification() {
const config = await this._getSyncEncryption();
if (!config?.enabled) return false;
const cached = await SilentSendCrypto.getCachedKey();
if (!cached) return false; // no key = needs full auth, not re-verify
return SilentSendCrypto.needsReverification();
},
/**
* Set up sync encryption for the first time.
* @param {{ password: string, enableTOTP?: boolean, authMethod?: string, ttlDays?: number, enableWebAuthn?: boolean }}