Files
Multi-Hoster-Upload/lib/secret-store.js
T
Sucukdeluxe 2ba0106ef0
CI / verify (push) Waiting to run
release: v2.1.12 reliability and security hardening
Verify update artifacts with exact metadata and SHA-512, require host-confirmed upload completion before cleanup, harden credentials and backups, improve queue recovery and skipped-state reporting, expand Windows path coverage, and add CI packaging checks.
2026-08-11 22:36:21 +02:00

105 lines
3.4 KiB
JavaScript

// Wraps Electron's safeStorage (OS-level credential encryption: DPAPI on
// Windows, Keychain on macOS, libsecret on Linux) to keep hoster passwords and
// API keys out of the plaintext electron-config.json.
//
// On Windows the DPAPI key is tied to the current user profile, so credentials
// encrypted here are only readable by the same Windows user. For backups we
// export to plaintext (the .mhu envelope has its own AES-GCM layer) so moving
// between machines/users works transparently.
const SENTINEL = 'enc:v1:';
const CRED_FIELDS = ['password', 'apiKey'];
class SecretStoreError extends Error {
constructor(code, message, cause) {
super(message);
this.name = 'SecretStoreError';
this.code = code;
if (cause !== undefined) this.cause = cause;
}
}
let _safeStorageCache = undefined;
function getSafeStorage() {
if (_safeStorageCache !== undefined) return _safeStorageCache;
try {
const { safeStorage } = require('electron');
if (safeStorage && typeof safeStorage.isEncryptionAvailable === 'function'
&& safeStorage.isEncryptionAvailable()) {
_safeStorageCache = safeStorage;
return _safeStorageCache;
}
} catch {}
_safeStorageCache = null;
return null;
}
function isEncrypted(value) {
return typeof value === 'string' && value.startsWith(SENTINEL);
}
function getAvailabilityStatus() {
return getSafeStorage() ? 'available' : 'unavailable';
}
function encryptField(value, options = {}) {
if (!value || typeof value !== 'string') return value;
if (isEncrypted(value)) return value;
const ss = getSafeStorage();
if (!ss) {
if (options.allowPlaintext === true) return value;
throw new SecretStoreError('SECRET_STORE_UNAVAILABLE', 'Sicherer Zugangsdaten-Speicher ist nicht verfügbar');
}
try {
const buf = ss.encryptString(value);
return SENTINEL + buf.toString('base64');
} catch (cause) {
if (options.allowPlaintext === true) return value;
throw new SecretStoreError('SECRET_STORE_ENCRYPT_FAILED', 'Zugangsdaten konnten nicht sicher verschlüsselt werden', cause);
}
}
function decryptField(value) {
if (!value || typeof value !== 'string') return value;
if (!isEncrypted(value)) return value;
const ss = getSafeStorage();
if (!ss) {
throw new SecretStoreError('SECRET_STORE_UNAVAILABLE', 'Sicherer Zugangsdaten-Speicher ist nicht verfügbar');
}
try {
const buf = Buffer.from(value.slice(SENTINEL.length), 'base64');
return ss.decryptString(buf);
} catch (cause) {
throw new SecretStoreError('SECRET_STORE_DECRYPT_FAILED', 'Gespeicherte Zugangsdaten konnten nicht entschlüsselt werden', cause);
}
}
function mapHosterAccounts(config, fn) {
if (!config || !config.hosters || typeof config.hosters !== 'object') return config;
for (const accounts of Object.values(config.hosters)) {
if (!Array.isArray(accounts)) continue;
for (const acc of accounts) {
if (!acc || typeof acc !== 'object') continue;
for (const f of CRED_FIELDS) {
if (acc[f]) acc[f] = fn(acc[f]);
}
}
}
return config;
}
function encryptCredentials(config, options = {}) {
return mapHosterAccounts(config, value => encryptField(value, options));
}
function decryptCredentials(config) { return mapHosterAccounts(config, decryptField); }
module.exports = {
SecretStoreError,
getAvailabilityStatus,
encryptField,
decryptField,
encryptCredentials,
decryptCredentials,
isEncrypted
};