Merge pull request #21 from outis1one/claude/read-repo-YMu21

security: require encryption for all sync channels, reduce activity l…
This commit is contained in:
Outis
2026-03-27 00:27:10 -04:00
committed by GitHub
5 changed files with 50 additions and 21 deletions
+1 -1
View File
@@ -105,7 +105,7 @@
url: location.href, url: location.href,
}); });
// Trim // Trim
if (log.length > 200) log.length = 200; if (log.length > 100) log.length = 100;
await api.storage.local.set({ ss_activity_log: log }); await api.storage.local.set({ ss_activity_log: log });
} }
} }
+1 -1
View File
@@ -38,7 +38,7 @@ const DEFAULT_SETTINGS = {
autoDetect: true, autoDetect: true,
autoRedactDetected: true, autoRedactDetected: true,
autoAddDetected: true, autoAddDetected: true,
maxLogEntries: 200, maxLogEntries: 100,
customDomains: [], customDomains: [],
categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'password', 'general'], categories: ['name', 'email', 'phone', 'address', 'ssn', 'dob', 'domain', 'password', 'general'],
browserSync: false, browserSync: false,
+24 -11
View File
@@ -12,8 +12,9 @@
* 5. Custom HTTP endpoint — any URL supporting GET + PUT (WebDAV, * 5. Custom HTTP endpoint — any URL supporting GET + PUT (WebDAV,
* self-hosted server, cloud function, etc.). * self-hosted server, cloud function, etc.).
* *
* Encryption: all sync channels can optionally encrypt data with a * Encryption: all sync channels REQUIRE encryption with a password
* password (AES-256-GCM) and/or require TOTP verification. * (AES-256-GCM) and/or TOTP verification. Syncing without encryption
* is not permitted — users must set up encryption before enabling sync.
* Authentication is cached with a configurable TTL so the user only * Authentication is cached with a configurable TTL so the user only
* needs to authenticate when the cache expires and new data exists. * needs to authenticate when the cache expires and new data exists.
* *
@@ -370,6 +371,9 @@ const SilentSendSync = {
const StorageModule = (await import('./storage.js')).default; const StorageModule = (await import('./storage.js')).default;
await StorageModule.decryptAllData(); await StorageModule.decryptAllData();
// Disable all sync channels since encryption is mandatory for sync
await StorageModule.saveSettings({ browserSync: false });
await api.storage.local.remove('ss_sync_encryption'); await api.storage.local.remove('ss_sync_encryption');
await SilentSendCrypto.clearCachedKey(); await SilentSendCrypto.clearCachedKey();
await SilentSendCrypto.clearWebAuthnCredential(); await SilentSendCrypto.clearWebAuthnCredential();
@@ -415,7 +419,7 @@ const SilentSendSync = {
*/ */
async _encryptForSync(data) { async _encryptForSync(data) {
const config = await this._getSyncEncryption(); const config = await this._getSyncEncryption();
if (!config?.enabled) return { data, encrypted: false }; if (!config?.enabled) return { data: null, encrypted: false, needsEncryption: true };
const keyInfo = await this._getEncryptionKey(); const keyInfo = await this._getEncryptionKey();
if (!keyInfo) { if (!keyInfo) {
@@ -507,12 +511,15 @@ const SilentSendSync = {
async exportSyncCode() { async exportSyncCode() {
const data = await this._getAllData(); const data = await this._getAllData();
// Encrypt if enabled // Encrypt (mandatory)
const result = await this._encryptForSync(data); const result = await this._encryptForSync(data);
if (result.needsEncryption) {
return { needsEncryption: true };
}
if (result.needsAuth) { if (result.needsAuth) {
return { needsAuth: true }; return { needsAuth: true };
} }
const payload = result.data || data; const payload = result.data;
const json = JSON.stringify(payload); const json = JSON.stringify(payload);
return btoa(unescape(encodeURIComponent(json))); return btoa(unescape(encodeURIComponent(json)));
@@ -568,10 +575,10 @@ const SilentSendSync = {
try { try {
const data = await this._getAllData(); const data = await this._getAllData();
// Encrypt if enabled // Encrypt (mandatory)
const result = await this._encryptForSync(data); const result = await this._encryptForSync(data);
if (result.needsAuth) return; // silently skip — will sync on next auth if (result.needsEncryption || result.needsAuth) return; // skip — encryption required
const payload = result.data || data; const payload = result.data;
const json = JSON.stringify(payload); const json = JSON.stringify(payload);
@@ -637,12 +644,15 @@ const SilentSendSync = {
try { try {
const data = await this._getAllData(); const data = await this._getAllData();
// Encrypt if enabled // Encrypt (mandatory)
const encResult = await this._encryptForSync(data); const encResult = await this._encryptForSync(data);
if (encResult.needsEncryption) {
return { success: false, needsEncryption: true, reason: 'Encryption must be enabled before syncing.' };
}
if (encResult.needsAuth) { if (encResult.needsAuth) {
return { success: false, needsAuth: true, reason: 'Authentication required.' }; return { success: false, needsAuth: true, reason: 'Authentication required.' };
} }
const payload = encResult.data || data; const payload = encResult.data;
const content = JSON.stringify(payload, null, 2); const content = JSON.stringify(payload, null, 2);
const stored = await api.storage.local.get('ss_gist_id'); const stored = await api.storage.local.get('ss_gist_id');
@@ -738,10 +748,13 @@ const SilentSendSync = {
const data = await this._getAllData(); const data = await this._getAllData();
const encResult = await this._encryptForSync(data); const encResult = await this._encryptForSync(data);
if (encResult.needsEncryption) {
return { success: false, needsEncryption: true, reason: 'Encryption must be enabled before syncing.' };
}
if (encResult.needsAuth) { if (encResult.needsAuth) {
return { success: false, needsAuth: true, reason: 'Authentication required.' }; return { success: false, needsAuth: true, reason: 'Authentication required.' };
} }
const payload = encResult.data || data; const payload = encResult.data;
const resp = await fetch(url, { const resp = await fetch(url, {
method, method,
+2 -2
View File
@@ -92,7 +92,7 @@
<label>Max log entries</label> <label>Max log entries</label>
<p class="setting-desc">Number of activity log entries to keep</p> <p class="setting-desc">Number of activity log entries to keep</p>
</div> </div>
<input type="number" id="maxLogEntries" class="input-small" min="10" max="1000" value="200"> <input type="number" id="maxLogEntries" class="input-small" min="10" max="1000" value="100">
</div> </div>
</section> </section>
@@ -106,7 +106,7 @@
<span>&#128274;</span> Sync Encryption <span>&#128274;</span> Sync Encryption
</h3> </h3>
<p class="section-desc" style="margin-bottom:10px"> <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. Encryption is required for all sync channels. Set up a password (and optionally TOTP) before enabling any sync method. Authentication is only required when new data arrives and your cached key has expired.
</p> </p>
<div id="syncEncryptionSetup"> <div id="syncEncryptionSetup">
+22 -6
View File
@@ -30,7 +30,7 @@ document.addEventListener('DOMContentLoaded', async () => {
$('#autoDetect').checked = settings.autoDetect !== false; $('#autoDetect').checked = settings.autoDetect !== false;
$('#autoRedactDetected').checked = settings.autoRedactDetected !== false; $('#autoRedactDetected').checked = settings.autoRedactDetected !== false;
$('#autoAddDetected').checked = settings.autoAddDetected !== false; $('#autoAddDetected').checked = settings.autoAddDetected !== false;
$('#maxLogEntries').value = settings.maxLogEntries || 200; $('#maxLogEntries').value = settings.maxLogEntries || 100;
$('#browserSync').checked = settings.browserSync === true; $('#browserSync').checked = settings.browserSync === true;
renderMappings(); renderMappings();
@@ -51,17 +51,28 @@ document.addEventListener('DOMContentLoaded', async () => {
// --- Sync section --- // --- Sync section ---
$('#browserSync').addEventListener('change', async (e) => { $('#browserSync').addEventListener('change', async (e) => {
await Storage.saveSettings({ browserSync: e.target.checked });
if (e.target.checked) { if (e.target.checked) {
const encEnabled = await SilentSendSync.isEncryptionEnabled();
if (!encEnabled) {
e.target.checked = false;
setSyncStatus('Encryption must be enabled before syncing. Set up encryption first.', 'error');
return;
}
await Storage.saveSettings({ browserSync: true });
await SilentSendSync.pushToSyncStorage(); await SilentSendSync.pushToSyncStorage();
setSyncStatus('Browser account sync enabled. Your settings will sync automatically.', 'ok'); setSyncStatus('Browser account sync enabled. Your settings will sync automatically.', 'ok');
} else { } else {
await Storage.saveSettings({ browserSync: false });
setSyncStatus('Browser account sync disabled.', 'neutral'); setSyncStatus('Browser account sync disabled.', 'neutral');
} }
}); });
$('#btnGenerateSyncCode').addEventListener('click', async () => { $('#btnGenerateSyncCode').addEventListener('click', async () => {
const code = await SilentSendSync.exportSyncCode(); const code = await SilentSendSync.exportSyncCode();
if (code?.needsEncryption) {
setSyncStatus('Encryption must be enabled before syncing. Set up encryption first.', 'error');
return;
}
if (code?.needsAuth) { if (code?.needsAuth) {
setSyncStatus('Authentication required to encrypt sync code.', 'warn'); setSyncStatus('Authentication required to encrypt sync code.', 'warn');
showSyncAuthPrompt(); showSyncAuthPrompt();
@@ -168,7 +179,9 @@ document.addEventListener('DOMContentLoaded', async () => {
if (!token) { setGistSyncStatus('Enter your GitHub PAT first.', 'warn'); return; } if (!token) { setGistSyncStatus('Enter your GitHub PAT first.', 'warn'); return; }
setGistSyncStatus('Pushing…', 'neutral'); setGistSyncStatus('Pushing…', 'neutral');
const r = await SilentSendSync.pushToGist(token); const r = await SilentSendSync.pushToGist(token);
if (r.needsAuth) { if (r.needsEncryption) {
setGistSyncStatus('Encryption must be enabled before syncing.', 'error');
} else if (r.needsAuth) {
setGistSyncStatus('Authentication required to encrypt.', 'warn'); setGistSyncStatus('Authentication required to encrypt.', 'warn');
showSyncAuthPrompt(); showSyncAuthPrompt();
} else if (r.success) { } else if (r.success) {
@@ -207,7 +220,9 @@ document.addEventListener('DOMContentLoaded', async () => {
const headers = parseHeadersField($('#customSyncHeaders').value); const headers = parseHeadersField($('#customSyncHeaders').value);
setUrlSyncStatus('Pushing…', 'neutral'); setUrlSyncStatus('Pushing…', 'neutral');
const r = await SilentSendSync.pushToUrl({ url, headers }); const r = await SilentSendSync.pushToUrl({ url, headers });
if (r.needsAuth) { if (r.needsEncryption) {
setUrlSyncStatus('Encryption must be enabled before syncing.', 'error');
} else if (r.needsAuth) {
setUrlSyncStatus('Authentication required to encrypt.', 'warn'); setUrlSyncStatus('Authentication required to encrypt.', 'warn');
showSyncAuthPrompt(); showSyncAuthPrompt();
} else if (r.success) { } else if (r.success) {
@@ -282,7 +297,7 @@ document.addEventListener('DOMContentLoaded', async () => {
}); });
$('#maxLogEntries').addEventListener('change', async (e) => { $('#maxLogEntries').addEventListener('change', async (e) => {
await Storage.saveSettings({ maxLogEntries: parseInt(e.target.value, 10) || 200 }); await Storage.saveSettings({ maxLogEntries: parseInt(e.target.value, 10) || 100 });
}); });
// Add mapping // Add mapping
@@ -959,8 +974,9 @@ async function initSyncEncryptionUI() {
$('#btnDisableEncryption').addEventListener('click', async () => { $('#btnDisableEncryption').addEventListener('click', async () => {
if (!window.confirm('Disable sync encryption? Existing encrypted sync data will become unreadable.')) return; if (!window.confirm('Disable sync encryption? Existing encrypted sync data will become unreadable.')) return;
await SilentSendSync.disableEncryption(); await SilentSendSync.disableEncryption();
$('#browserSync').checked = false;
showEncryptionNotConfigured(); showEncryptionNotConfigured();
setSyncEncStatus('Encryption disabled.', 'neutral'); setSyncEncStatus('Encryption disabled. All sync channels have been turned off.', 'neutral');
}); });
// Change password // Change password