fix(security): redact secrets from plaintext persistence

- Strip password, cookies, and headers from download_queue before writing
  to store.bin; secrets remain in-memory for the active session only.
- Move extension pairing token from PersistedSettings to the OS keychain,
  rotating it on upgrade from versions that persisted it as plaintext.
- Add ignores_legacy_extension_pairing_token_field test to confirm serde
  silently drops the old field so existing installs migrate cleanly.
- Document intentional retention of URLs (signed params are the download
  source and cannot be redacted without breaking resume/retry).
This commit is contained in:
NimBold
2026-06-18 08:20:22 +03:30
parent e2dd387a8c
commit 3a76c6f5d7
7 changed files with 105 additions and 13 deletions
+31
View File
@@ -1,5 +1,6 @@
import type { DownloadCategory } from '../bindings/DownloadCategory';
import type { DownloadStatus } from '../bindings/DownloadStatus';
import type { DownloadItem } from '../bindings/DownloadItem';
export type { DownloadCategory } from '../bindings/DownloadCategory';
import { invoke } from '@tauri-apps/api/core';
@@ -78,3 +79,33 @@ export const isMediaUrl = (rawUrl: string): boolean => {
return false;
}
};
/**
* Fields that may carry secrets and therefore must never reach the persisted
* `download_queue` document. These are supplied in-memory for the active
* session (see `enqueue_download` payloads) but are stripped at the
* persistence boundary so `store.bin` contains no plaintext credentials.
*/
const DOWNLOAD_SECRET_FIELDS = ['password', 'cookies', 'headers'] as const;
/**
* Returns a shallow copy of `item` with secret fields removed. Volatile
* progress fields (`fraction`, `speed`, `eta`) are also dropped as in the
* existing persistence path.
*
* Note: `url` is intentionally retained even though it may contain signed
* query parameters — redacting it would break resume/retry since the URL is
* the download source. Ad-hoc credentials entered in the Add Downloads modal
* are therefore session-scoped; site-login passwords (Keychain-backed) are
* unaffected by this redaction.
*/
export const redactDownloadForPersistence = (item: DownloadItem): DownloadItem => {
const copy: DownloadItem = { ...item };
delete copy.fraction;
delete copy.speed;
delete copy.eta;
for (const field of DOWNLOAD_SECRET_FIELDS) {
delete copy[field];
}
return copy;
};