feat: encrypted sync with password/TOTP/WebAuthn + smart reveal
Sync encryption: - AES-256-GCM encryption for all sync channels (browser sync, gist, custom URL, folder sync, sync codes) - Password with optional TOTP (RFC 6238) second factor - Configurable auth TTL: session, 30/90/180/365 days, or never - CryptoKey cached in IndexedDB — auth only needed when cache expires AND new data exists (lastModified check runs before auth prompt) - WebAuthn (biometric/PIN) as low-friction re-authentication gate - Full options UI for setup, password change, and inline auth prompt Smart reveal: - Track which substitute values were actually sent outbound per session - Reveal mode only replaces values that were genuinely substituted, preventing false positives (e.g. AI using the word "user" won't be replaced with a real username that maps to "user") https://claude.ai/code/session_01SWSwDfMVij53bCTNSCLMwn
This commit is contained in:
@@ -100,6 +100,92 @@
|
||||
<h2>Sync Between Browsers</h2>
|
||||
<p class="section-desc">Keep your identities, mappings, and settings in sync across browsers.</p>
|
||||
|
||||
<!-- Sync Encryption -->
|
||||
<div style="margin-bottom:20px;padding:12px;background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px">
|
||||
<h3 style="font-size:13px;font-weight:600;margin:0 0 8px;display:flex;align-items:center;gap:6px">
|
||||
<span>🔒</span> Sync Encryption
|
||||
</h3>
|
||||
<p class="section-desc" style="margin-bottom:10px">
|
||||
Encrypt your sync data with a password, TOTP, or both. Authentication is only required when new data arrives and your cached key has expired.
|
||||
</p>
|
||||
|
||||
<div id="syncEncryptionSetup">
|
||||
<!-- Shown when encryption is NOT set up -->
|
||||
<div id="encryptionNotConfigured">
|
||||
<div style="display:flex;flex-direction:column;gap:8px">
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:end">
|
||||
<div style="flex:1;min-width:160px">
|
||||
<label style="font-size:11px;color:#6b7280;display:block;margin-bottom:2px">Password</label>
|
||||
<input type="password" id="syncEncPassword" placeholder="Encryption password" autocomplete="new-password"
|
||||
style="width:100%;box-sizing:border-box;font-size:12px;padding:5px 8px;border:1px solid #d1d5db;border-radius:6px">
|
||||
</div>
|
||||
<div style="flex:1;min-width:160px">
|
||||
<label style="font-size:11px;color:#6b7280;display:block;margin-bottom:2px">Confirm Password</label>
|
||||
<input type="password" id="syncEncPasswordConfirm" placeholder="Confirm password" autocomplete="new-password"
|
||||
style="width:100%;box-sizing:border-box;font-size:12px;padding:5px 8px;border:1px solid #d1d5db;border-radius:6px">
|
||||
</div>
|
||||
</div>
|
||||
<div style="display:flex;gap:12px;flex-wrap:wrap;align-items:center">
|
||||
<label style="font-size:12px;display:flex;align-items:center;gap:4px;cursor:pointer">
|
||||
<input type="checkbox" id="syncEncTOTP"> Enable TOTP (authenticator app)
|
||||
</label>
|
||||
<label style="font-size:12px;display:flex;align-items:center;gap:4px;cursor:pointer" id="syncEncWebAuthnLabel">
|
||||
<input type="checkbox" id="syncEncWebAuthn"> Biometric/PIN re-auth
|
||||
</label>
|
||||
<div style="display:flex;align-items:center;gap:4px">
|
||||
<label style="font-size:12px;white-space:nowrap">Re-auth every</label>
|
||||
<select id="syncEncTTL" style="font-size:12px;padding:3px 6px;border:1px solid #d1d5db;border-radius:4px">
|
||||
<option value="0">Each session</option>
|
||||
<option value="30">30 days</option>
|
||||
<option value="90" selected>90 days</option>
|
||||
<option value="180">180 days</option>
|
||||
<option value="365">1 year</option>
|
||||
<option value="-1">Never</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-primary" id="btnSetupEncryption">Enable Encryption</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shown when encryption IS set up -->
|
||||
<div id="encryptionConfigured" style="display:none">
|
||||
<div style="display:flex;align-items:center;gap:8px;flex-wrap:wrap">
|
||||
<span style="font-size:12px;color:#10b981;font-weight:500">✓ Encryption active</span>
|
||||
<span id="encryptionInfo" style="font-size:11px;color:#6b7280"></span>
|
||||
<button class="btn btn-sm" id="btnChangeEncPassword">Change Password</button>
|
||||
<button class="btn btn-sm btn-danger" id="btnDisableEncryption">Disable</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<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">
|
||||
<input type="text" id="syncAuthTOTP" placeholder="TOTP code" autocomplete="one-time-code" inputmode="numeric" maxlength="6"
|
||||
style="width:80px;font-size:12px;padding:5px 8px;border:1px solid #d1d5db;border-radius:6px;display:none">
|
||||
<button class="btn btn-primary btn-sm" id="btnSyncAuth">Unlock</button>
|
||||
<button class="btn btn-sm" id="btnSyncAuthBiometric" style="display:none">Biometric</button>
|
||||
</div>
|
||||
<div id="syncAuthStatus" style="font-size:11px;margin-top:4px;min-height:14px"></div>
|
||||
</div>
|
||||
|
||||
<!-- TOTP setup result -->
|
||||
<div id="totpSetupResult" style="display:none;margin-top:10px;padding:10px;background:#f0fdf4;border:1px solid #86efac;border-radius:6px">
|
||||
<p style="font-size:12px;font-weight:500;margin:0 0 6px;color:#166534">TOTP Secret — save this in your authenticator app:</p>
|
||||
<code id="totpSecretDisplay" style="font-size:13px;font-weight:600;letter-spacing:2px;display:block;margin-bottom:6px;word-break:break-all"></code>
|
||||
<p style="font-size:11px;color:#6b7280;margin:0">Or use the otpauth URI: <code id="totpURIDisplay" style="font-size:10px;word-break:break-all"></code></p>
|
||||
<button class="btn btn-sm" id="btnDismissTOTP" style="margin-top:8px">Done — I've saved it</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="syncEncStatus" style="font-size:12px;margin-top:6px;min-height:14px"></div>
|
||||
</div>
|
||||
|
||||
<div class="setting-row">
|
||||
<div>
|
||||
<label>Browser account sync</label>
|
||||
|
||||
+298
-26
@@ -25,6 +25,9 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
renderDomains();
|
||||
renderLog();
|
||||
|
||||
// --- Sync Encryption UI ---
|
||||
await initSyncEncryptionUI();
|
||||
|
||||
// --- Sync section ---
|
||||
$('#browserSync').addEventListener('change', async (e) => {
|
||||
await Storage.saveSettings({ browserSync: e.target.checked });
|
||||
@@ -38,6 +41,11 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
|
||||
$('#btnGenerateSyncCode').addEventListener('click', async () => {
|
||||
const code = await SilentSendSync.exportSyncCode();
|
||||
if (code?.needsAuth) {
|
||||
setSyncStatus('Authentication required to encrypt sync code.', 'warn');
|
||||
showSyncAuthPrompt();
|
||||
return;
|
||||
}
|
||||
const data = await SilentSendSync._getAllData();
|
||||
$('#syncCodeText').value = code;
|
||||
$('#syncCodeDisplay').style.display = 'block';
|
||||
@@ -69,7 +77,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
if (!code) return;
|
||||
const force = $('#syncForce').checked;
|
||||
const result = await SilentSendSync.importSyncCode(code, { force });
|
||||
if (result.success) {
|
||||
if (result.needsAuth) {
|
||||
setSyncStatus('Authentication required to decrypt this sync code.', 'warn');
|
||||
showSyncAuthPrompt();
|
||||
} else if (result.success) {
|
||||
setSyncStatus(`Imported successfully (data from ${result.importTime}).`, 'ok');
|
||||
$('#syncImportSection').style.display = 'none';
|
||||
$('#syncImportText').value = '';
|
||||
@@ -136,7 +147,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
if (!token) { setGistSyncStatus('Enter your GitHub PAT first.', 'warn'); return; }
|
||||
setGistSyncStatus('Pushing…', 'neutral');
|
||||
const r = await SilentSendSync.pushToGist(token);
|
||||
if (r.success) {
|
||||
if (r.needsAuth) {
|
||||
setGistSyncStatus('Authentication required to encrypt.', 'warn');
|
||||
showSyncAuthPrompt();
|
||||
} else if (r.success) {
|
||||
setGistSyncStatus(`Pushed. Gist ID: ${r.gistId.slice(0, 12)}…`, 'ok');
|
||||
} else {
|
||||
setGistSyncStatus('Push failed: ' + r.reason, 'error');
|
||||
@@ -148,7 +162,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
if (!token) { setGistSyncStatus('Enter your GitHub PAT first.', 'warn'); return; }
|
||||
setGistSyncStatus('Pulling…', 'neutral');
|
||||
const r = await SilentSendSync.pullFromGist(token);
|
||||
if (!r.success) {
|
||||
if (r.needsAuth) {
|
||||
setGistSyncStatus('Authentication required to decrypt.', 'warn');
|
||||
showSyncAuthPrompt();
|
||||
} else if (!r.success) {
|
||||
setGistSyncStatus('Pull failed: ' + r.reason, 'error');
|
||||
} else if (r.imported) {
|
||||
setGistSyncStatus(`Pulled (${r.time}). Refreshing…`, 'ok');
|
||||
@@ -169,7 +186,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
const headers = parseHeadersField($('#customSyncHeaders').value);
|
||||
setUrlSyncStatus('Pushing…', 'neutral');
|
||||
const r = await SilentSendSync.pushToUrl({ url, headers });
|
||||
if (r.success) {
|
||||
if (r.needsAuth) {
|
||||
setUrlSyncStatus('Authentication required to encrypt.', 'warn');
|
||||
showSyncAuthPrompt();
|
||||
} else if (r.success) {
|
||||
setUrlSyncStatus('Pushed successfully.', 'ok');
|
||||
} else {
|
||||
setUrlSyncStatus('Push failed: ' + r.reason, 'error');
|
||||
@@ -182,7 +202,10 @@ document.addEventListener('DOMContentLoaded', async () => {
|
||||
const headers = parseHeadersField($('#customSyncHeaders').value);
|
||||
setUrlSyncStatus('Pulling…', 'neutral');
|
||||
const r = await SilentSendSync.pullFromUrl({ url, headers });
|
||||
if (!r.success) {
|
||||
if (r.needsAuth) {
|
||||
setUrlSyncStatus('Authentication required to decrypt.', 'warn');
|
||||
showSyncAuthPrompt();
|
||||
} else if (!r.success) {
|
||||
setUrlSyncStatus('Pull failed: ' + r.reason, 'error');
|
||||
} else if (r.imported) {
|
||||
setUrlSyncStatus(`Pulled (${r.time}). Refreshing…`, 'ok');
|
||||
@@ -604,17 +627,21 @@ async function pickSyncFolder() {
|
||||
async function writeToSyncFile() {
|
||||
if (!syncDirHandle) return;
|
||||
try {
|
||||
// Re-verify permission is still granted (required after browser restart)
|
||||
const perm = await syncDirHandle.requestPermission({ mode: 'readwrite' });
|
||||
if (perm !== 'granted') return;
|
||||
|
||||
const data = await SilentSendSync._getAllData();
|
||||
|
||||
// Encrypt if enabled
|
||||
const encResult = await SilentSendSync._encryptForSync(data);
|
||||
if (encResult.needsAuth) return; // skip silently — will sync after auth
|
||||
const payload = encResult.data || data;
|
||||
|
||||
const fileHandle = await syncDirHandle.getFileHandle(SYNC_FILE_NAME, { create: true });
|
||||
const writable = await fileHandle.createWritable();
|
||||
await writable.write(JSON.stringify(data, null, 2));
|
||||
await writable.write(JSON.stringify(payload, null, 2));
|
||||
await writable.close();
|
||||
} catch (e) {
|
||||
// Permission denied or folder removed — don't spam errors
|
||||
console.warn('[Silent Send] writeToSyncFile failed:', e.message);
|
||||
}
|
||||
}
|
||||
@@ -623,32 +650,48 @@ async function checkFileSyncUpdate() {
|
||||
if (!syncDirHandle) return;
|
||||
try {
|
||||
const perm = await syncDirHandle.queryPermission({ mode: 'readwrite' });
|
||||
if (perm === 'prompt') {
|
||||
// Need a user gesture to re-request — skip silently
|
||||
return;
|
||||
}
|
||||
if (perm === 'prompt') return;
|
||||
if (perm !== 'granted') return;
|
||||
|
||||
const fileHandle = await syncDirHandle.getFileHandle(SYNC_FILE_NAME);
|
||||
const file = await fileHandle.getFile();
|
||||
const data = JSON.parse(await file.text());
|
||||
let data = JSON.parse(await file.text());
|
||||
|
||||
if (!data.version || !data.lastModified) return;
|
||||
// Check timestamp before requiring auth
|
||||
const remoteMod = data.lastModified;
|
||||
if (!remoteMod) return;
|
||||
|
||||
const local = await SilentSendSync._getAllData();
|
||||
if (data.lastModified > (local.lastModified || 0)) {
|
||||
await SilentSendSync._applyData(data, 'file');
|
||||
mappings = await Storage.getMappings();
|
||||
settings = await Storage.getSettings();
|
||||
$('#browserSync').checked = settings.browserSync === true;
|
||||
renderMappings();
|
||||
renderDomains();
|
||||
renderLog();
|
||||
setFileSyncStatus(
|
||||
'Auto-synced from folder (' + new Date(data.lastModified).toLocaleString() + ').',
|
||||
'ok'
|
||||
);
|
||||
if (remoteMod <= (local.lastModified || 0)) return;
|
||||
|
||||
// New data exists — decrypt if encrypted
|
||||
if (data._ssEncrypted) {
|
||||
const decResult = await SilentSendSync._decryptFromSync(data);
|
||||
if (decResult.needsAuth) {
|
||||
setFileSyncStatus('New sync data available — authentication required.', 'warn');
|
||||
showSyncAuthPrompt();
|
||||
return;
|
||||
}
|
||||
if (!decResult.data) {
|
||||
setFileSyncStatus('Failed to decrypt sync file.', 'error');
|
||||
return;
|
||||
}
|
||||
data = decResult.data;
|
||||
}
|
||||
|
||||
if (!data.version) return;
|
||||
|
||||
await SilentSendSync._applyData(data, 'file');
|
||||
mappings = await Storage.getMappings();
|
||||
settings = await Storage.getSettings();
|
||||
$('#browserSync').checked = settings.browserSync === true;
|
||||
renderMappings();
|
||||
renderDomains();
|
||||
renderLog();
|
||||
setFileSyncStatus(
|
||||
'Auto-synced from folder (' + new Date(data.lastModified).toLocaleString() + ').',
|
||||
'ok'
|
||||
);
|
||||
} catch (e) {
|
||||
if (e.name !== 'NotFoundError') {
|
||||
console.warn('[Silent Send] checkFileSyncUpdate failed:', e.message);
|
||||
@@ -709,6 +752,235 @@ function setSyncStatus(msg, type) {
|
||||
el.style.color = type === 'ok' ? '#10b981' : type === 'warn' ? '#f59e0b' : type === 'error' ? '#dc2626' : '#6b7280';
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------
|
||||
// Sync Encryption UI
|
||||
// ----------------------------------------------------------------
|
||||
|
||||
async function initSyncEncryptionUI() {
|
||||
const isEnabled = await SilentSendSync.isEncryptionEnabled();
|
||||
|
||||
if (isEnabled) {
|
||||
showEncryptionConfigured();
|
||||
} else {
|
||||
showEncryptionNotConfigured();
|
||||
}
|
||||
|
||||
// Hide WebAuthn option if not available
|
||||
if (!SilentSendCrypto.isWebAuthnAvailable()) {
|
||||
const label = $('#syncEncWebAuthnLabel');
|
||||
if (label) label.style.display = 'none';
|
||||
}
|
||||
|
||||
// Setup encryption button
|
||||
$('#btnSetupEncryption').addEventListener('click', async () => {
|
||||
const password = $('#syncEncPassword').value;
|
||||
const confirm = $('#syncEncPasswordConfirm').value;
|
||||
|
||||
if (!password) {
|
||||
setSyncEncStatus('Enter a password.', 'warn');
|
||||
return;
|
||||
}
|
||||
if (password !== confirm) {
|
||||
setSyncEncStatus('Passwords do not match.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const enableTOTP = $('#syncEncTOTP').checked;
|
||||
const enableWebAuthn = $('#syncEncWebAuthn').checked;
|
||||
const ttlDays = parseInt($('#syncEncTTL').value, 10);
|
||||
|
||||
setSyncEncStatus('Setting up encryption...', 'neutral');
|
||||
|
||||
const result = await SilentSendSync.setupEncryption({
|
||||
password,
|
||||
enableTOTP,
|
||||
authMethod: enableTOTP ? 'both' : 'password',
|
||||
ttlDays,
|
||||
enableWebAuthn,
|
||||
});
|
||||
|
||||
if (!result.success) {
|
||||
setSyncEncStatus('Setup failed: ' + result.reason, 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Show TOTP secret if enabled
|
||||
if (result.totpSecret) {
|
||||
$('#totpSecretDisplay').textContent = result.totpSecret;
|
||||
$('#totpURIDisplay').textContent = result.totpURI;
|
||||
$('#totpSetupResult').style.display = 'block';
|
||||
}
|
||||
|
||||
// Clear password fields
|
||||
$('#syncEncPassword').value = '';
|
||||
$('#syncEncPasswordConfirm').value = '';
|
||||
|
||||
showEncryptionConfigured();
|
||||
setSyncEncStatus('Encryption enabled. All sync data will be encrypted.', 'ok');
|
||||
});
|
||||
|
||||
// Dismiss TOTP setup
|
||||
$('#btnDismissTOTP').addEventListener('click', () => {
|
||||
$('#totpSetupResult').style.display = 'none';
|
||||
});
|
||||
|
||||
// Disable encryption
|
||||
$('#btnDisableEncryption').addEventListener('click', async () => {
|
||||
if (!window.confirm('Disable sync encryption? Existing encrypted sync data will become unreadable.')) return;
|
||||
await SilentSendSync.disableEncryption();
|
||||
showEncryptionNotConfigured();
|
||||
setSyncEncStatus('Encryption disabled.', 'neutral');
|
||||
});
|
||||
|
||||
// Change password
|
||||
$('#btnChangeEncPassword').addEventListener('click', async () => {
|
||||
const oldPassword = window.prompt('Enter current password:');
|
||||
if (!oldPassword) return;
|
||||
|
||||
// Verify old password
|
||||
const authResult = await SilentSendSync.authenticate(oldPassword);
|
||||
if (!authResult.success) {
|
||||
setSyncEncStatus('Wrong current password.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const newPassword = window.prompt('Enter new password:');
|
||||
if (!newPassword) return;
|
||||
const confirmNew = window.prompt('Confirm new password:');
|
||||
if (newPassword !== confirmNew) {
|
||||
setSyncEncStatus('New passwords do not match.', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current config to preserve TOTP and other settings
|
||||
const config = await SilentSendSync._getSyncEncryption();
|
||||
const result = await SilentSendSync.setupEncryption({
|
||||
password: newPassword,
|
||||
enableTOTP: !!config.totpSecret,
|
||||
authMethod: config.authMethod,
|
||||
ttlDays: config.ttlDays,
|
||||
enableWebAuthn: config.webauthn,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
setSyncEncStatus('Password changed successfully.', 'ok');
|
||||
} else {
|
||||
setSyncEncStatus('Failed: ' + result.reason, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Auth prompt — Unlock button
|
||||
$('#btnSyncAuth').addEventListener('click', async () => {
|
||||
const password = $('#syncAuthPassword').value;
|
||||
const totpCode = $('#syncAuthTOTP').value;
|
||||
|
||||
if (!password) {
|
||||
setSyncAuthStatus('Enter your password.', 'warn');
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await SilentSendSync.authenticate(password, totpCode || undefined);
|
||||
if (result.success) {
|
||||
$('#syncAuthPrompt').style.display = 'none';
|
||||
$('#syncAuthPassword').value = '';
|
||||
$('#syncAuthTOTP').value = '';
|
||||
setSyncEncStatus('Authenticated. Sync data unlocked.', 'ok');
|
||||
} else {
|
||||
setSyncAuthStatus(result.reason, 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// Auth prompt — Biometric button
|
||||
$('#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');
|
||||
}
|
||||
} else {
|
||||
setSyncAuthStatus('Biometric verification failed.', 'error');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function showEncryptionConfigured() {
|
||||
$('#encryptionNotConfigured').style.display = 'none';
|
||||
$('#encryptionConfigured').style.display = 'block';
|
||||
|
||||
const config = await SilentSendSync._getSyncEncryption();
|
||||
if (config) {
|
||||
const parts = [];
|
||||
if (config.authMethod === 'both') parts.push('Password + TOTP');
|
||||
else if (config.authMethod === 'totp') parts.push('TOTP');
|
||||
else parts.push('Password');
|
||||
|
||||
if (config.webauthn) parts.push('Biometric');
|
||||
|
||||
const ttl = config.ttlDays === -1 ? 'never re-auth'
|
||||
: config.ttlDays === 0 ? 'each session'
|
||||
: `every ${config.ttlDays}d`;
|
||||
parts.push(ttl);
|
||||
|
||||
$('#encryptionInfo').textContent = `(${parts.join(' · ')})`;
|
||||
}
|
||||
|
||||
// Check if auth is currently needed
|
||||
const needsAuth = await SilentSendSync.needsAuth();
|
||||
if (needsAuth) {
|
||||
showSyncAuthPrompt();
|
||||
}
|
||||
}
|
||||
|
||||
function showEncryptionNotConfigured() {
|
||||
$('#encryptionNotConfigured').style.display = 'block';
|
||||
$('#encryptionConfigured').style.display = 'none';
|
||||
$('#syncAuthPrompt').style.display = 'none';
|
||||
$('#totpSetupResult').style.display = 'none';
|
||||
}
|
||||
|
||||
async function showSyncAuthPrompt() {
|
||||
const config = await SilentSendSync._getSyncEncryption();
|
||||
$('#syncAuthPrompt').style.display = 'block';
|
||||
|
||||
// Show TOTP field if needed
|
||||
if (config?.totpSecret) {
|
||||
$('#syncAuthTOTP').style.display = '';
|
||||
} else {
|
||||
$('#syncAuthTOTP').style.display = 'none';
|
||||
}
|
||||
|
||||
// Show biometric button if available
|
||||
if (config?.webauthn && SilentSendCrypto.isWebAuthnAvailable()) {
|
||||
const hasCred = await SilentSendCrypto.hasWebAuthnCredential();
|
||||
$('#btnSyncAuthBiometric').style.display = hasCred ? '' : 'none';
|
||||
} else {
|
||||
$('#btnSyncAuthBiometric').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function setSyncEncStatus(msg, type) {
|
||||
const el = $('#syncEncStatus');
|
||||
if (!el) return;
|
||||
el.textContent = msg;
|
||||
el.style.color = type === 'ok' ? '#10b981' : type === 'warn' ? '#f59e0b' : type === 'error' ? '#dc2626' : '#6b7280';
|
||||
}
|
||||
|
||||
function setSyncAuthStatus(msg, type) {
|
||||
const el = $('#syncAuthStatus');
|
||||
if (!el) return;
|
||||
el.textContent = msg;
|
||||
el.style.color = type === 'ok' ? '#10b981' : type === 'warn' ? '#f59e0b' : type === 'error' ? '#dc2626' : '#6b7280';
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = str;
|
||||
|
||||
Reference in New Issue
Block a user