App-Sperre mit Passwort und/oder FIDO2-Sicherheitsschlüssel (PRF/hmac-secret): Vault zusätzlich verschlüsselt, Sperrbildschirm, Strg+Shift+L, automatische Sperre
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+60
-2
@@ -31,16 +31,63 @@ const DEFAULTS = {
|
||||
updateToken: '',
|
||||
updateAutoCheck: true,
|
||||
updatePrerelease: false,
|
||||
autoLock: 0,
|
||||
language: 'auto',
|
||||
},
|
||||
};
|
||||
|
||||
const merge = (parsed) => ({ ...structuredClone(DEFAULTS), ...parsed, settings: { ...DEFAULTS.settings, ...(parsed.settings || {}) } });
|
||||
|
||||
// AES-256-GCM; Ergebnis als base64-Felder für JSON
|
||||
function box(key, plain) {
|
||||
const iv = crypto.randomBytes(12);
|
||||
const c = crypto.createCipheriv('aes-256-gcm', key, iv);
|
||||
const ct = Buffer.concat([c.update(plain), c.final()]);
|
||||
return { iv: iv.toString('base64'), tag: c.getAuthTag().toString('base64'), ct: ct.toString('base64') };
|
||||
}
|
||||
function unbox(key, b) {
|
||||
const d = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(b.iv, 'base64'));
|
||||
d.setAuthTag(Buffer.from(b.tag, 'base64'));
|
||||
return Buffer.concat([d.update(Buffer.from(b.ct, 'base64')), d.final()]);
|
||||
}
|
||||
|
||||
class Store {
|
||||
constructor() {
|
||||
this.dir = app.getPath('userData');
|
||||
this.file = path.join(this.dir, 'vault.dat');
|
||||
this.data = structuredClone(DEFAULTS);
|
||||
this.encrypted = false;
|
||||
// App-Sperre: Inhalt zusätzlich mit zufälligem Datenschlüssel (DEK, AES-256-GCM) verschlüsselt.
|
||||
// Der DEK liegt je Entsperrmethode verpackt vor: Passwort (scrypt) und/oder FIDO2-Schlüssel (PRF/hmac-secret).
|
||||
this.lock = null; // { password: { salt, N, wrap } | null, fido: [{ id, label, credId, prfSalt, wrap }] }
|
||||
this.dek = null;
|
||||
this.sealed = null; // verschlüsselter Inhalt, solange nach dem Start noch nicht entsperrt
|
||||
this.locked = false;
|
||||
}
|
||||
|
||||
get lockEnabled() { return !!(this.lock && (this.lock.password || this.lock.fido.length)); }
|
||||
|
||||
// Entsperren mit einem Schlüssel, der den DEK verpackt hat (wirft bei falschem Schlüssel)
|
||||
unlockWith(kek, wrap) {
|
||||
const dek = unbox(kek, wrap);
|
||||
if (this.sealed) {
|
||||
const parsed = JSON.parse(unbox(dek, this.sealed).toString('utf8'));
|
||||
this.data = merge(parsed);
|
||||
this.sealed = null;
|
||||
}
|
||||
this.dek = dek;
|
||||
this.locked = false;
|
||||
}
|
||||
|
||||
// Neue Entsperrmethode: verpackt den (ggf. neu erzeugten) DEK mit kek
|
||||
wrapDek(kek) {
|
||||
if (!this.dek) this.dek = crypto.randomBytes(32);
|
||||
return box(kek, this.dek);
|
||||
}
|
||||
|
||||
// Letzte Methode entfernt → Sperre aus, Inhalt wieder nur per Betriebssystem verschlüsselt
|
||||
dropLockIfEmpty() {
|
||||
if (!this.lockEnabled) { this.lock = null; this.dek = null; }
|
||||
}
|
||||
|
||||
canEncrypt() {
|
||||
@@ -64,12 +111,23 @@ class Store {
|
||||
json = raw.toString('utf8');
|
||||
}
|
||||
const parsed = JSON.parse(json);
|
||||
this.data = { ...structuredClone(DEFAULTS), ...parsed, settings: { ...DEFAULTS.settings, ...(parsed.settings || {}) } };
|
||||
if (parsed.mrtermLock) {
|
||||
// Gesperrt: nur Darstellungs-Einstellungen (meta) sind bis zum Entsperren bekannt
|
||||
this.lock = parsed.lock;
|
||||
this.sealed = parsed.data;
|
||||
this.locked = true;
|
||||
this.data = merge({ settings: parsed.meta || {} });
|
||||
} else this.data = merge(parsed);
|
||||
return this.data;
|
||||
}
|
||||
|
||||
save() {
|
||||
const json = JSON.stringify(this.data, null, 2);
|
||||
if (this.sealed) return; // Inhalt noch nicht entschlüsselt – nichts überschreiben
|
||||
let json = JSON.stringify(this.data, null, 2);
|
||||
if (this.lockEnabled && this.dek) {
|
||||
const { language, appTheme, accent } = this.data.settings;
|
||||
json = JSON.stringify({ mrtermLock: 1, meta: { language, appTheme, accent }, lock: this.lock, data: box(this.dek, Buffer.from(json)) });
|
||||
}
|
||||
const tmp = this.file + '.tmp';
|
||||
if (this.canEncrypt()) {
|
||||
fs.writeFileSync(tmp, Buffer.concat([Buffer.from('ENC1'), safeStorage.encryptString(json)]));
|
||||
|
||||
Reference in New Issue
Block a user