fix: WebAuthn as primary re-auth, key persists indefinitely
- CryptoKey now persists in IndexedDB forever — never auto-deleted - TTL controls when re-verification is needed, not key lifetime - WebAuthn is the primary re-auth method (not a post-expiry fallback) - Password only needed once per device (first-time setup) - Added needsReverification() and markVerified() to crypto.js - Auth prompt adapts message: first-device vs re-verify vs decrypt - Biometric button hidden on first-device setup (no credential yet) https://claude.ai/code/session_01SWSwDfMVij53bCTNSCLMwn
This commit is contained in:
@@ -162,7 +162,7 @@
|
||||
|
||||
<!-- Auth prompt (shown when key cache expired and sync needs decryption) -->
|
||||
<div id="syncAuthPrompt" style="display:none;margin-top:10px;padding:10px;background:#fffbeb;border:1px solid #fcd34d;border-radius:6px">
|
||||
<p style="font-size:12px;font-weight:500;margin:0 0 8px;color:#92400e">Authentication required — new sync data is available</p>
|
||||
<p style="font-size:12px;font-weight:500;margin:0 0 8px;color:#92400e">Authentication required</p>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:end">
|
||||
<input type="password" id="syncAuthPassword" placeholder="Password" autocomplete="current-password"
|
||||
style="flex:1;min-width:120px;font-size:12px;padding:5px 8px;border:1px solid #d1d5db;border-radius:6px">
|
||||
|
||||
+33
-23
@@ -890,24 +890,18 @@ async function initSyncEncryptionUI() {
|
||||
}
|
||||
});
|
||||
|
||||
// Auth prompt — Biometric button
|
||||
// Auth prompt — Biometric button (primary re-auth after first password entry)
|
||||
$('#btnSyncAuthBiometric').addEventListener('click', async () => {
|
||||
setSyncAuthStatus('Waiting for biometric...', 'neutral');
|
||||
const verified = await SilentSendCrypto.webAuthnAuthenticate();
|
||||
if (verified) {
|
||||
// Try to recover wrapped key
|
||||
const wrapped = await SilentSendSync._getWrappedKey();
|
||||
if (wrapped) {
|
||||
const config = await SilentSendSync._getSyncEncryption();
|
||||
const ttlDays = config?.ttlDays ?? 90;
|
||||
await SilentSendCrypto.cacheKey(wrapped.key, wrapped.salt, ttlDays);
|
||||
$('#syncAuthPrompt').style.display = 'none';
|
||||
setSyncEncStatus('Authenticated via biometric. Sync data unlocked.', 'ok');
|
||||
} else {
|
||||
setSyncAuthStatus('Biometric verified but key not found. Enter password.', 'warn');
|
||||
}
|
||||
const config = await SilentSendSync._getSyncEncryption();
|
||||
const ttlDays = config?.ttlDays ?? 90;
|
||||
await SilentSendCrypto.markVerified(ttlDays);
|
||||
$('#syncAuthPrompt').style.display = 'none';
|
||||
setSyncEncStatus('Verified via biometric.', 'ok');
|
||||
} else {
|
||||
setSyncAuthStatus('Biometric verification failed.', 'error');
|
||||
setSyncAuthStatus('Biometric failed. Use password instead.', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -923,20 +917,20 @@ async function showEncryptionConfigured() {
|
||||
else if (config.authMethod === 'totp') parts.push('TOTP');
|
||||
else parts.push('Password');
|
||||
|
||||
if (config.webauthn) parts.push('Biometric');
|
||||
if (config.webauthn) parts.push('Biometric re-auth');
|
||||
|
||||
const ttl = config.ttlDays === -1 ? 'never re-auth'
|
||||
: config.ttlDays === 0 ? 'each session'
|
||||
: `every ${config.ttlDays}d`;
|
||||
const ttl = config.ttlDays === -1 ? 'never re-verify'
|
||||
: config.ttlDays === 0 ? 're-verify each session'
|
||||
: `re-verify every ${config.ttlDays}d`;
|
||||
parts.push(ttl);
|
||||
|
||||
$('#encryptionInfo').textContent = `(${parts.join(' · ')})`;
|
||||
}
|
||||
|
||||
// Check if auth is currently needed
|
||||
// Check if password entry is needed (first time on this device)
|
||||
const needsAuth = await SilentSendSync.needsAuth();
|
||||
if (needsAuth) {
|
||||
showSyncAuthPrompt();
|
||||
showSyncAuthPrompt('first-device');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -947,9 +941,24 @@ function showEncryptionNotConfigured() {
|
||||
$('#totpSetupResult').style.display = 'none';
|
||||
}
|
||||
|
||||
async function showSyncAuthPrompt() {
|
||||
/**
|
||||
* Show the auth prompt.
|
||||
* @param {'first-device'|'reverify'|'decrypt'} mode
|
||||
*/
|
||||
async function showSyncAuthPrompt(mode = 'decrypt') {
|
||||
const config = await SilentSendSync._getSyncEncryption();
|
||||
$('#syncAuthPrompt').style.display = 'block';
|
||||
const promptEl = $('#syncAuthPrompt');
|
||||
promptEl.style.display = 'block';
|
||||
|
||||
// Adjust header message based on context
|
||||
const headerEl = promptEl.querySelector('p');
|
||||
if (mode === 'first-device') {
|
||||
headerEl.textContent = 'First time on this device — enter your sync encryption password';
|
||||
} else if (mode === 'reverify') {
|
||||
headerEl.textContent = 'Re-verification required — use biometric or enter password';
|
||||
} else {
|
||||
headerEl.textContent = 'Authentication required — encrypted sync data needs decryption';
|
||||
}
|
||||
|
||||
// Show TOTP field if needed
|
||||
if (config?.totpSecret) {
|
||||
@@ -958,8 +967,9 @@ async function showSyncAuthPrompt() {
|
||||
$('#syncAuthTOTP').style.display = 'none';
|
||||
}
|
||||
|
||||
// Show biometric button if available
|
||||
if (config?.webauthn && SilentSendCrypto.isWebAuthnAvailable()) {
|
||||
// Show biometric button — available unless this is first-device setup
|
||||
// (no WebAuthn credential exists yet on a new device)
|
||||
if (mode !== 'first-device' && config?.webauthn && SilentSendCrypto.isWebAuthnAvailable()) {
|
||||
const hasCred = await SilentSendCrypto.hasWebAuthnCredential();
|
||||
$('#btnSyncAuthBiometric').style.display = hasCred ? '' : 'none';
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user