Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion packages/app/src/main/services/LocalSettingsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,25 @@ export class LocalSettingsService {
try {
const raw = await readFile(this.filePath, "utf8");
const settings = normalizeUserLocalSettings(tryParseJson(raw));
return { exists: true, settings: this.decryptApiKeys(settings) };
const decrypted = this.decryptApiKeys(settings);
if (this.secureStorage.needsMigration) {
this.migrateEncryption(decrypted);
}
return { exists: true, settings: decrypted };
} catch (error) {
logger.info("settings", `settings file not available, using defaults (${this.filePath})`);
return { exists: false, settings: normalizeUserLocalSettings(DEFAULT_USER_LOCAL_SETTINGS) };
}
}

private migrateEncryption(settings: UserLocalSettings): void {
logger.info("settings", "auto-migrating plaintext API keys to encrypted storage");
const toWrite = this.encryptApiKeys(settings);
mkdir(dirname(this.filePath), { recursive: true })
.then(() => writeFile(this.filePath, `${JSON.stringify(toWrite, null, 2)}\n`, "utf8"))
.catch((error) => logger.warn("settings", "failed to migrate API key encryption", error));
}

async read(): Promise<{ exists: boolean; settings: UserLocalSettings }> {
await this.writeQueue.catch(() => undefined);
return this.readFromDisk();
Expand Down
18 changes: 13 additions & 5 deletions packages/app/src/main/services/SecureStorageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,25 @@ const ENCRYPTED_PREFIX = "enc:";
* sensitive strings before persisting them to disk.
*
* Encrypted values are stored as "enc:<base64>".
* Unrecognized values (no prefix) are treated as invalid and discarded.
* Legacy plaintext values are accepted on read and will be encrypted
* automatically on the next write (auto-migration).
*/
export class SecureStorageService {
private _needsMigration = false;

get needsMigration(): boolean {
return this._needsMigration;
}

isAvailable(): boolean {
return safeStorage.isEncryptionAvailable();
}

encrypt(plaintext: string | null): string | null {
if (plaintext == null || plaintext === "") return plaintext;
if (!this.isAvailable()) {
logger.warn("secure-storage", "encryption unavailable, value will not be persisted securely");
return null;
logger.warn("secure-storage", "encryption unavailable, storing plaintext");
return plaintext;
}
const encrypted = safeStorage.encryptString(plaintext);
return `${ENCRYPTED_PREFIX}${encrypted.toString("base64")}`;
Expand All @@ -28,8 +35,9 @@ export class SecureStorageService {
decrypt(stored: string | null): string | null {
if (stored == null || stored === "") return stored;
if (!stored.startsWith(ENCRYPTED_PREFIX)) {
logger.warn("secure-storage", "discarding unencrypted value — re-enter the key in settings");
return null;
logger.info("secure-storage", "detected plaintext value, will encrypt on next write");
this._needsMigration = true;
return stored;
}
if (!this.isAvailable()) {
logger.warn("secure-storage", "decryption unavailable, cannot read encrypted value");
Expand Down
Loading