feat: Envelope Encryption und Master-Key aus Datei (#006)
Private Keys liegen spaeter verschluesselt in PostgreSQL. Ein eigener DEK je Geheimnis erlaubt eine Master-Key-Rotation, ohne die Chiffrate selbst anzufassen; das AAD (Schluessel-ID und Fingerprint) bindet jedes Chiffrat an seinen Datensatz, sodass vertauschte Zeilen beim Oeffnen auffallen statt einen fremden Key zu liefern. Die Master-Key-Version steckt im AAD des DEK. masterKeyLaden akzeptiert roh, Base64 und Hex, weil `openssl rand` der naheliegende Weg zum Anlegen ist, und bricht ab, wenn Gruppe oder Welt die Datei lesen duerfen – ein lesbarer Master-Key oeffnet alle Private Keys. SSHKV_MASTER_KEY_UNSICHER_ERLAUBT=1 erlaubt das bewusst. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,198 @@
|
|||||||
|
import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
|
||||||
|
import { open } from "node:fs/promises";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Envelope Encryption fuer Geheimnisse at rest (#006, docs/PLAN.md Abschnitt 3).
|
||||||
|
*
|
||||||
|
* Jedes Geheimnis bekommt einen eigenen zufaelligen Data Encryption Key (DEK). Der DEK
|
||||||
|
* verschluesselt den Klartext, der Master-Key verschluesselt nur den DEK. Dadurch
|
||||||
|
* - genuegt fuer eine Master-Key-Rotation das Umschluesseln der DEKs (`sshkv rewrap`),
|
||||||
|
* die eigentlichen Chiffrate bleiben unberuehrt;
|
||||||
|
* - bindet das AAD (`zusatz`, z. B. `schluessel:<id>:<fingerprint>`) jedes Chiffrat an
|
||||||
|
* seinen Datensatz: Wer in der Datenbank Chiffrate zwischen Zeilen vertauscht, erhaelt
|
||||||
|
* beim Oeffnen einen Fehler statt eines fremden Private Keys.
|
||||||
|
*
|
||||||
|
* Aufbau beider Puffer (`chiffrat` und `dekVerschluesselt`):
|
||||||
|
*
|
||||||
|
* | Version (1 Byte, derzeit 1) | IV (12 Byte) | GCM-Tag (16 Byte) | Daten (n Byte) |
|
||||||
|
*
|
||||||
|
* AES-256-GCM in beiden Stufen. AAD des Klartexts ist `zusatz` (UTF-8), AAD des DEK ist
|
||||||
|
* `"sshkv-dek:v" + masterKeyVersion`, damit ein DEK nicht mit dem Master-Key einer
|
||||||
|
* anderen Version geoeffnet werden kann.
|
||||||
|
*
|
||||||
|
* Framework-frei; der Master-Key selbst darf nur im Worker geladen werden (CLAUDE.md).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const FORMAT_VERSION = 1;
|
||||||
|
const IV_LAENGE = 12;
|
||||||
|
const TAG_LAENGE = 16;
|
||||||
|
const KOPF_LAENGE = 1 + IV_LAENGE + TAG_LAENGE;
|
||||||
|
const SCHLUESSEL_LAENGE = 32;
|
||||||
|
|
||||||
|
export interface Umschlag {
|
||||||
|
chiffrat: Buffer;
|
||||||
|
dekVerschluesselt: Buffer;
|
||||||
|
masterKeyVersion: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Oeffnen ist gescheitert. Bewusst ohne Angabe, welcher Teil nicht passte (Master-Key,
|
||||||
|
* AAD oder Chiffrat): GCM kann das nicht unterscheiden, und Raten hilft niemandem.
|
||||||
|
*/
|
||||||
|
export class EntschluesselungsFehler extends Error {
|
||||||
|
override readonly name = "EntschluesselungsFehler";
|
||||||
|
constructor(
|
||||||
|
meldung = "Entschlüsselung fehlgeschlagen: Master-Key, Zuordnung oder Chiffrat passen nicht zusammen.",
|
||||||
|
) {
|
||||||
|
super(meldung);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fehler beim Laden des Master-Keys. Die Meldung nennt den Pfad, nie den Inhalt. */
|
||||||
|
export class MasterKeyFehler extends Error {
|
||||||
|
override readonly name = "MasterKeyFehler";
|
||||||
|
}
|
||||||
|
|
||||||
|
function masterKeyPruefen(masterKey: Buffer): void {
|
||||||
|
if (masterKey.length !== SCHLUESSEL_LAENGE) {
|
||||||
|
throw new MasterKeyFehler("Der Master-Key muss genau 32 Byte lang sein.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function dekAad(masterKeyVersion: number): Buffer {
|
||||||
|
return Buffer.from(`sshkv-dek:v${masterKeyVersion}`, "utf8");
|
||||||
|
}
|
||||||
|
|
||||||
|
function gcmVerschluesseln(klartext: Buffer, schluessel: Buffer, aad: Buffer): Buffer {
|
||||||
|
const iv = randomBytes(IV_LAENGE);
|
||||||
|
const cipher = createCipheriv("aes-256-gcm", schluessel, iv, { authTagLength: TAG_LAENGE });
|
||||||
|
cipher.setAAD(aad);
|
||||||
|
const daten = Buffer.concat([cipher.update(klartext), cipher.final()]);
|
||||||
|
return Buffer.concat([Buffer.from([FORMAT_VERSION]), iv, cipher.getAuthTag(), daten]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function gcmEntschluesseln(puffer: Buffer, schluessel: Buffer, aad: Buffer): Buffer {
|
||||||
|
if (puffer.length < KOPF_LAENGE || puffer[0] !== FORMAT_VERSION) {
|
||||||
|
throw new EntschluesselungsFehler();
|
||||||
|
}
|
||||||
|
const iv = puffer.subarray(1, 1 + IV_LAENGE);
|
||||||
|
const tag = puffer.subarray(1 + IV_LAENGE, KOPF_LAENGE);
|
||||||
|
const daten = puffer.subarray(KOPF_LAENGE);
|
||||||
|
try {
|
||||||
|
const decipher = createDecipheriv("aes-256-gcm", schluessel, iv, { authTagLength: TAG_LAENGE });
|
||||||
|
decipher.setAAD(aad);
|
||||||
|
decipher.setAuthTag(tag);
|
||||||
|
return Buffer.concat([decipher.update(daten), decipher.final()]);
|
||||||
|
} catch {
|
||||||
|
// Die Original-Ausnahme verraet nichts Geheimes, ist aber englisch und ohne Nutzen.
|
||||||
|
throw new EntschluesselungsFehler();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verschluesselt `klartext` mit einem frischen DEK. Der Klartext gehoert dem Aufrufer und
|
||||||
|
* wird hier nicht ueberschrieben; den DEK loescht die Funktion selbst.
|
||||||
|
*/
|
||||||
|
export function versiegeln(
|
||||||
|
klartext: Buffer,
|
||||||
|
masterKey: Buffer,
|
||||||
|
zusatz: string,
|
||||||
|
masterKeyVersion = 1,
|
||||||
|
): Umschlag {
|
||||||
|
masterKeyPruefen(masterKey);
|
||||||
|
const dek = randomBytes(SCHLUESSEL_LAENGE);
|
||||||
|
try {
|
||||||
|
const chiffrat = gcmVerschluesseln(klartext, dek, Buffer.from(zusatz, "utf8"));
|
||||||
|
const dekVerschluesselt = gcmVerschluesseln(dek, masterKey, dekAad(masterKeyVersion));
|
||||||
|
return { chiffrat, dekVerschluesselt, masterKeyVersion };
|
||||||
|
} finally {
|
||||||
|
dek.fill(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Oeffnet einen Umschlag. `masterKey` muss zur `masterKeyVersion` des Umschlags gehoeren.
|
||||||
|
* Der zurueckgegebene Klartext gehoert dem Aufrufer, der ihn nach Gebrauch mit `fill(0)`
|
||||||
|
* ueberschreiben soll.
|
||||||
|
*/
|
||||||
|
export function oeffnen(umschlag: Umschlag, masterKey: Buffer, zusatz: string): Buffer {
|
||||||
|
masterKeyPruefen(masterKey);
|
||||||
|
const dek = gcmEntschluesseln(umschlag.dekVerschluesselt, masterKey, dekAad(umschlag.masterKeyVersion));
|
||||||
|
try {
|
||||||
|
if (dek.length !== SCHLUESSEL_LAENGE) throw new EntschluesselungsFehler();
|
||||||
|
return gcmEntschluesseln(umschlag.chiffrat, dek, Buffer.from(zusatz, "utf8"));
|
||||||
|
} finally {
|
||||||
|
dek.fill(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mit dieser Variable darf ein Master-Key mit zu offenen Dateirechten geladen werden. */
|
||||||
|
export const UNSICHER_ERLAUBT_VARIABLE = "SSHKV_MASTER_KEY_UNSICHER_ERLAUBT";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Laedt den Master-Key aus einer Datei (`SSHKV_MASTER_KEY_FILE`).
|
||||||
|
*
|
||||||
|
* Akzeptiert 32 rohe Bytes, 44 Zeichen Base64 oder 64 Zeichen Hex (jeweils mit optionalem
|
||||||
|
* Zeilenende), weil `openssl rand -base64 32 > datei` der naheliegende Weg ist, einen
|
||||||
|
* Schluessel anzulegen. Darf Gruppe oder Welt die Datei lesen, wird abgebrochen: Ein
|
||||||
|
* versehentlich lesbarer Master-Key oeffnet jeden gespeicherten Private Key.
|
||||||
|
*/
|
||||||
|
export async function masterKeyLaden(pfad: string): Promise<Buffer> {
|
||||||
|
let inhalt: Buffer;
|
||||||
|
const datei = await open(pfad, "r").catch(() => {
|
||||||
|
throw new MasterKeyFehler(`Die Master-Key-Datei ${pfad} kann nicht geöffnet werden.`);
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
// Rechte am geoeffneten Handle pruefen, nicht am Pfad: sonst koennte die Datei
|
||||||
|
// zwischen Pruefung und Lesen ausgetauscht werden.
|
||||||
|
const status = await datei.stat();
|
||||||
|
if (!status.isFile()) {
|
||||||
|
throw new MasterKeyFehler(`Die Master-Key-Datei ${pfad} ist keine reguläre Datei.`);
|
||||||
|
}
|
||||||
|
if ((status.mode & 0o044) !== 0) {
|
||||||
|
const meldung =
|
||||||
|
`Die Master-Key-Datei ${pfad} ist für Gruppe oder andere lesbar ` +
|
||||||
|
`(Rechte ${(status.mode & 0o777).toString(8)}). Bitte mit „chmod 600“ einschränken.`;
|
||||||
|
if (process.env[UNSICHER_ERLAUBT_VARIABLE] !== "1") throw new MasterKeyFehler(meldung);
|
||||||
|
console.warn(`${meldung} Wird wegen ${UNSICHER_ERLAUBT_VARIABLE}=1 trotzdem geladen.`);
|
||||||
|
}
|
||||||
|
// Groessere Dateien gar nicht erst ganz lesen; 70 Byte reichen fuer Hex plus CRLF.
|
||||||
|
if (status.size > 70) {
|
||||||
|
throw new MasterKeyFehler(`Die Master-Key-Datei ${pfad} ist zu groß für einen 32-Byte-Schlüssel.`);
|
||||||
|
}
|
||||||
|
inhalt = await datei.readFile();
|
||||||
|
} finally {
|
||||||
|
await datei.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const schluessel = masterKeyDekodieren(inhalt);
|
||||||
|
if (!schluessel) {
|
||||||
|
throw new MasterKeyFehler(
|
||||||
|
`Die Master-Key-Datei ${pfad} enthält keinen 32-Byte-Schlüssel ` +
|
||||||
|
"(erwartet: 32 Byte roh, 44 Zeichen Base64 oder 64 Zeichen Hex).",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (schluessel.every((byte) => byte === 0)) {
|
||||||
|
schluessel.fill(0);
|
||||||
|
throw new MasterKeyFehler(`Die Master-Key-Datei ${pfad} enthält nur Nullbytes.`);
|
||||||
|
}
|
||||||
|
return schluessel;
|
||||||
|
} finally {
|
||||||
|
inhalt.fill(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function masterKeyDekodieren(inhalt: Buffer): Buffer | null {
|
||||||
|
if (inhalt.length === SCHLUESSEL_LAENGE) return Buffer.from(inhalt);
|
||||||
|
let ende = inhalt.length;
|
||||||
|
if (inhalt[ende - 1] === 0x0a) ende--;
|
||||||
|
if (inhalt[ende - 1] === 0x0d) ende--;
|
||||||
|
const text = inhalt.subarray(0, ende).toString("latin1");
|
||||||
|
if (/^[0-9a-fA-F]{64}$/.test(text)) return Buffer.from(text, "hex");
|
||||||
|
if (/^[A-Za-z0-9+/]{43}=$/.test(text)) {
|
||||||
|
const schluessel = Buffer.from(text, "base64");
|
||||||
|
return schluessel.length === SCHLUESSEL_LAENGE ? schluessel : null;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
import { randomBytes } from "node:crypto";
|
||||||
|
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
|
||||||
|
import {
|
||||||
|
EntschluesselungsFehler,
|
||||||
|
MasterKeyFehler,
|
||||||
|
masterKeyLaden,
|
||||||
|
oeffnen,
|
||||||
|
UNSICHER_ERLAUBT_VARIABLE,
|
||||||
|
versiegeln,
|
||||||
|
} from "@/lib/krypto/umschlag";
|
||||||
|
|
||||||
|
const ZUSATZ = "schluessel:k_8f3a:SHA256:abc";
|
||||||
|
const KLARTEXT =
|
||||||
|
"-----BEGIN OPENSSH PRIVATE KEY-----\nnur ein Platzhalter\n-----END OPENSSH PRIVATE KEY-----\n";
|
||||||
|
|
||||||
|
describe("versiegeln und oeffnen", () => {
|
||||||
|
const masterKey = randomBytes(32);
|
||||||
|
|
||||||
|
it("Rundreise liefert den Klartext", () => {
|
||||||
|
const umschlag = versiegeln(Buffer.from(KLARTEXT), masterKey, ZUSATZ);
|
||||||
|
expect(umschlag.masterKeyVersion).toBe(1);
|
||||||
|
expect(oeffnen(umschlag, masterKey, ZUSATZ).toString()).toBe(KLARTEXT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("hält das dokumentierte Format ein", () => {
|
||||||
|
const umschlag = versiegeln(Buffer.from(KLARTEXT), masterKey, ZUSATZ, 3);
|
||||||
|
expect(umschlag.chiffrat[0]).toBe(1);
|
||||||
|
expect(umschlag.chiffrat.length).toBe(1 + 12 + 16 + Buffer.byteLength(KLARTEXT));
|
||||||
|
expect(umschlag.dekVerschluesselt.length).toBe(1 + 12 + 16 + 32);
|
||||||
|
expect(umschlag.masterKeyVersion).toBe(3);
|
||||||
|
expect(umschlag.chiffrat.toString("latin1")).not.toContain("Platzhalter");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("liefert bei jedem Versiegeln ein anderes Chiffrat", () => {
|
||||||
|
const a = versiegeln(Buffer.from(KLARTEXT), masterKey, ZUSATZ);
|
||||||
|
const b = versiegeln(Buffer.from(KLARTEXT), masterKey, ZUSATZ);
|
||||||
|
expect(a.chiffrat.equals(b.chiffrat)).toBe(false);
|
||||||
|
expect(a.dekVerschluesselt.equals(b.dekVerschluesselt)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lehnt einen falschen Master-Key ab", () => {
|
||||||
|
const umschlag = versiegeln(Buffer.from(KLARTEXT), masterKey, ZUSATZ);
|
||||||
|
expect(() => oeffnen(umschlag, randomBytes(32), ZUSATZ)).toThrow(EntschluesselungsFehler);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lehnt ein falsches AAD ab (vertauschte Datensätze)", () => {
|
||||||
|
const umschlag = versiegeln(Buffer.from(KLARTEXT), masterKey, ZUSATZ);
|
||||||
|
expect(() => oeffnen(umschlag, masterKey, "schluessel:k_anders:SHA256:abc")).toThrow(
|
||||||
|
EntschluesselungsFehler,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lehnt eine falsche Master-Key-Version ab", () => {
|
||||||
|
const umschlag = versiegeln(Buffer.from(KLARTEXT), masterKey, ZUSATZ, 1);
|
||||||
|
expect(() => oeffnen({ ...umschlag, masterKeyVersion: 2 }, masterKey, ZUSATZ)).toThrow(
|
||||||
|
EntschluesselungsFehler,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(["chiffrat", "dekVerschluesselt"] as const)("erkennt jeden Bit-Flip in %s", (feld) => {
|
||||||
|
const umschlag = versiegeln(Buffer.from(KLARTEXT), masterKey, ZUSATZ);
|
||||||
|
// Version, IV, Tag und Daten: jede Stelle muss auffallen.
|
||||||
|
for (const stelle of [0, 1, 13, 29, umschlag[feld].length - 1]) {
|
||||||
|
const kaputt = Buffer.from(umschlag[feld]);
|
||||||
|
kaputt[stelle]! ^= 0x01;
|
||||||
|
expect(() => oeffnen({ ...umschlag, [feld]: kaputt }, masterKey, ZUSATZ)).toThrow(
|
||||||
|
EntschluesselungsFehler,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lehnt abgeschnittene Puffer ab", () => {
|
||||||
|
const umschlag = versiegeln(Buffer.from(KLARTEXT), masterKey, ZUSATZ);
|
||||||
|
expect(() =>
|
||||||
|
oeffnen({ ...umschlag, chiffrat: umschlag.chiffrat.subarray(0, 20) }, masterKey, ZUSATZ),
|
||||||
|
).toThrow(EntschluesselungsFehler);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("verlangt einen 32-Byte-Master-Key", () => {
|
||||||
|
expect(() => versiegeln(Buffer.from("x"), randomBytes(16), ZUSATZ)).toThrow(MasterKeyFehler);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("nennt in Fehlermeldungen weder Klartext noch Schlüssel", () => {
|
||||||
|
const umschlag = versiegeln(Buffer.from(KLARTEXT), masterKey, ZUSATZ);
|
||||||
|
try {
|
||||||
|
oeffnen(umschlag, randomBytes(32), ZUSATZ);
|
||||||
|
expect.unreachable();
|
||||||
|
} catch (fehler) {
|
||||||
|
const text = String((fehler as Error).message) + String((fehler as Error).stack);
|
||||||
|
expect(text).not.toContain("Platzhalter");
|
||||||
|
expect(text).not.toContain(masterKey.toString("hex"));
|
||||||
|
expect(text).not.toContain(masterKey.toString("base64"));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("masterKeyLaden", () => {
|
||||||
|
let verzeichnis: string;
|
||||||
|
const schluessel = randomBytes(32);
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
verzeichnis = await mkdtemp(join(tmpdir(), "sshkv-masterkey-"));
|
||||||
|
});
|
||||||
|
afterAll(async () => {
|
||||||
|
await rm(verzeichnis, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
afterEach(() => {
|
||||||
|
vi.unstubAllEnvs();
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function datei(name: string, inhalt: Buffer | string, modus = 0o600): Promise<string> {
|
||||||
|
const pfad = join(verzeichnis, name);
|
||||||
|
await writeFile(pfad, inhalt);
|
||||||
|
await chmod(pfad, modus);
|
||||||
|
return pfad;
|
||||||
|
}
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
["roh", schluessel],
|
||||||
|
["base64", schluessel.toString("base64")],
|
||||||
|
["base64 mit Zeilenende", schluessel.toString("base64") + "\n"],
|
||||||
|
["hex", schluessel.toString("hex")],
|
||||||
|
["hex mit CRLF", schluessel.toString("hex") + "\r\n"],
|
||||||
|
])("liest %s", async (name, inhalt) => {
|
||||||
|
const geladen = await masterKeyLaden(await datei(name, inhalt));
|
||||||
|
expect(geladen.equals(schluessel)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lehnt falsche Längen ab", async () => {
|
||||||
|
await expect(masterKeyLaden(await datei("kurz", randomBytes(31)))).rejects.toThrow(MasterKeyFehler);
|
||||||
|
await expect(masterKeyLaden(await datei("lang", randomBytes(33)))).rejects.toThrow(MasterKeyFehler);
|
||||||
|
await expect(masterKeyLaden(await datei("hex-kurz", randomBytes(31).toString("hex")))).rejects.toThrow(
|
||||||
|
"32-Byte-Schlüssel",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lehnt einen Schlüssel aus lauter Nullbytes ab", async () => {
|
||||||
|
await expect(masterKeyLaden(await datei("null", Buffer.alloc(32)))).rejects.toThrow("Nullbytes");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lehnt eine fehlende Datei ab", async () => {
|
||||||
|
await expect(masterKeyLaden(join(verzeichnis, "gibt-es-nicht"))).rejects.toThrow(MasterKeyFehler);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("verweigert Dateien, die Gruppe oder Welt lesen dürfen", async () => {
|
||||||
|
for (const modus of [0o640, 0o604, 0o644]) {
|
||||||
|
const pfad = await datei(`offen-${modus.toString(8)}`, schluessel, modus);
|
||||||
|
await expect(masterKeyLaden(pfad)).rejects.toThrow("chmod 600");
|
||||||
|
}
|
||||||
|
expect((await masterKeyLaden(await datei("nur-lesen", schluessel, 0o400))).equals(schluessel)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("lädt offene Dateien nur mit ausdrücklicher Erlaubnis und warnt dann", async () => {
|
||||||
|
vi.stubEnv(UNSICHER_ERLAUBT_VARIABLE, "1");
|
||||||
|
const warnung = vi.spyOn(console, "warn").mockImplementation(() => {});
|
||||||
|
const geladen = await masterKeyLaden(await datei("offen-erlaubt", schluessel.toString("hex"), 0o644));
|
||||||
|
expect(geladen.equals(schluessel)).toBe(true);
|
||||||
|
expect(warnung).toHaveBeenCalledOnce();
|
||||||
|
const ausgabe = String(warnung.mock.calls[0]?.[0]);
|
||||||
|
expect(ausgabe).not.toContain(schluessel.toString("hex"));
|
||||||
|
expect(ausgabe).not.toContain(schluessel.toString("base64"));
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user