jm-tech-power-official/Flash Download
A desktop download manager that opens up to 32 connections against one file and can send each of them out a different proxy route.
Public
Uncategorized
Clone
jm-tech-power-official/flash-download.jmproj
src/main/engine/secrets.js
'use strict';
const fs = require('fs/promises');
const path = require('path');
/**
* Encrypted side-store for proxy credentials, so they never sit in state.json as
* plaintext. Values are encrypted with a codec whose key is held by the OS —
* Electron's safeStorage: DPAPI on Windows, Keychain on macOS, libsecret/kwallet
* on Linux. The ciphertext blobs live in their own file; state.json carries only
* the non-secret route fields.
*
* The codec is injected rather than imported so the engine stays electron-free
* and testable. When no codec is available (e.g. a Linux session with no
* keyring), nothing is written — credentials are kept in memory for the session
* only and never degraded to plaintext on disk.
*
* The live route records in Manager are the single source of truth; this store
* reads credentials onto them at load and rebuilds its whole file from them at
* save, so a removed route's secret disappears with no extra bookkeeping.
*/
class SecretStore {
constructor({ filePath, codec }) {
this.filePath = filePath;
this.codec = codec || null;
}
available() {
try { return !!(this.codec && this.codec.available()); }
catch { return false; }
}
/** Decrypt the stored blobs and assign username/password onto matching routes. */
async loadInto(routes) {
if (!this.available()) return;
let raw;
try { raw = JSON.parse(await fs.readFile(this.filePath, 'utf8')); }
catch { return; } // missing or unreadable — nothing to restore
for (const [id, blob] of Object.entries(raw.secrets || {})) {
const route = routes.find((r) => r.id === id);
if (!route) continue;
try {
const creds = JSON.parse(this.codec.decrypt(blob));
route.username = creds.username || null;
route.password = creds.password || null;
} catch {
// Written on another machine, or the OS key changed: leave the route
// without credentials rather than crashing. The user re-enters them.
}
}
}
/** Rebuild the encrypted file from whatever credentials the routes now hold. */
async saveFrom(routes) {
if (!this.available()) {
// Never leave stale ciphertext that no longer matches, and never write
// plaintext. Best effort removal; in-memory credentials still work.
await fs.rm(this.filePath, { force: true }).catch(() => {});
return;
}
const secrets = {};
for (const route of routes) {
if (route.type === 'direct') continue;
if (!route.username && !route.password) continue;
try {
secrets[route.id] = this.codec.encrypt(JSON.stringify({
username: route.username || null,
password: route.password || null,
}));
} catch {
// Encryption failed for this one — skip it rather than fall back to
// plaintext. The credential stays in memory for the session.
}
}
await fs.mkdir(path.dirname(this.filePath), { recursive: true });
const body = JSON.stringify({ v: 1, secrets }, null, 2);
await fs.writeFile(this.filePath, body, { mode: 0o600 }).catch(() => {});
}
}
/** Build the Electron-backed codec. Returns null when safeStorage is absent. */
function electronCodec(safeStorage) {
if (!safeStorage) return null;
return {
available: () => safeStorage.isEncryptionAvailable(),
encrypt: (plain) => safeStorage.encryptString(plain).toString('base64'),
decrypt: (b64) => safeStorage.decryptString(Buffer.from(b64, 'base64')),
};
}
module.exports = { SecretStore, electronCodec };