import { createCipheriv, createDecipheriv, createHash, createPrivateKey, createPublicKey, generateKeyPair, randomBytes, timingSafeEqual, } from "node:crypto"; import { promisify } from "node:util"; import bcryptPbkdf from "bcrypt-pbkdf"; /** * OpenSSH-Schluesselformate (#006): Erzeugen, Schreiben und Lesen von Private Keys im * Format `openssh-key-v1` sowie Lesen von Public-Key-Zeilen. * * Bewusst ohne `ssh-keygen`: Der Aufruf braeuchte temporaere Dateien mit dem Private Key * auf der Platte des Servers, und genau die wollen wir nicht (siehe docs/PLAN.md). Node * `crypto` erzeugt die Schluessel, die Serialisierung ist hier von Hand nachgebaut * (Referenz: PROTOCOL.key und sshkey.c in OpenSSH). Die Tests pruefen das Ergebnis gegen * das echte `ssh-keygen`. * * Fehlermeldungen sind feste Saetze. Nichts aus der Eingabe wird in eine Meldung * eingesetzt, weil ein Private Key sonst ueber Log oder Oberflaeche nach aussen geraten * koennte. */ const erzeugeSchluesselpaar = promisify(generateKeyPair); export type ErzeugbarerSchluesseltyp = "ED25519" | "RSA4096"; export type PrivateSchluesseltyp = "ED25519" | "RSA"; export type PublicSchluesseltyp = | "ssh-ed25519" | "ssh-rsa" | "ecdsa-sha2-nistp256" | "ecdsa-sha2-nistp384" | "ecdsa-sha2-nistp521" | "sk-ssh-ed25519@openssh.com" | "sk-ecdsa-sha2-nistp256@openssh.com"; export const PUBLIC_SCHLUESSELTYPEN: readonly PublicSchluesseltyp[] = [ "ssh-ed25519", "ssh-rsa", "ecdsa-sha2-nistp256", "ecdsa-sha2-nistp384", "ecdsa-sha2-nistp521", "sk-ssh-ed25519@openssh.com", "sk-ecdsa-sha2-nistp256@openssh.com", ]; /** Unterhalb davon gilt RSA als gebrochen bzw. wird von aktuellem OpenSSH abgelehnt. */ export const RSA_MINDEST_BITS = 2048; /** * Runden fuer bcrypt_pbkdf beim Schreiben. `ssh-keygen` nimmt inzwischen 24; 16 war der * fruehere Standard und haelt das Verschluesseln im Worker unter einer Sekunde, obwohl * bcrypt-pbkdf reines JavaScript ist. Beim Lesen gilt, was in der Datei steht. */ const BCRYPT_RUNDEN = 16; const BCRYPT_SALT_LAENGE = 16; /** Grenze gegen absichtlich teure Dateien beim Import (ssh-keygen -a erlaubt beliebig viel). */ const BCRYPT_RUNDEN_HOECHSTENS = 1000; const MAGIC = Buffer.from("openssh-key-v1\0", "latin1"); const PEM_ANFANG = "-----BEGIN OPENSSH PRIVATE KEY-----"; const PEM_ENDE = "-----END OPENSSH PRIVATE KEY-----"; export type SchluesselFormatFehlerCode = "ungueltig" | "nicht_unterstuetzt" | "passphrase_fehlt" | "passphrase_falsch" | "zu_schwach"; /** * Fehler beim Lesen oder Erzeugen eines Schluessels. `code` erlaubt der Oberflaeche, * gezielt zu reagieren (etwa nach der Passphrase zu fragen), ohne Meldungstexte zu * vergleichen. Die Meldung enthaelt nie Schluesselmaterial. */ export class SchluesselFormatFehler extends Error { override readonly name = "SchluesselFormatFehler"; constructor( readonly code: SchluesselFormatFehlerCode, meldung: string, ) { super(meldung); } } function ungueltig(meldung = "Der Schlüssel ist beschädigt oder unvollständig."): SchluesselFormatFehler { return new SchluesselFormatFehler("ungueltig", meldung); } // --------------------------------------------------------------------------- // SSH-Wire-Format (RFC 4251, Abschnitt 5) // --------------------------------------------------------------------------- function uint32(wert: number): Buffer { const puffer = Buffer.alloc(4); puffer.writeUInt32BE(wert >>> 0); return puffer; } function sshString(inhalt: Buffer | string): Buffer { const daten = typeof inhalt === "string" ? Buffer.from(inhalt, "utf8") : inhalt; return Buffer.concat([uint32(daten.length), daten]); } /** Vorzeichenbehaftete Big-Endian-Zahl ohne fuehrende Nullen, bei gesetztem Hochbit mit 0x00 davor. */ function mpint(betrag: Buffer): Buffer { let start = 0; while (start < betrag.length && betrag[start] === 0) start++; const ohneNullen = betrag.subarray(start); if (ohneNullen.length > 0 && (ohneNullen[0]! & 0x80) !== 0) { return sshString(Buffer.concat([Buffer.from([0]), ohneNullen])); } return sshString(ohneNullen); } /** * Liest SSH-Wire-Daten. Jeder Zugriff hinter das Ende wirft einen SchluesselFormatFehler, * damit abgeschnittene Eingaben nie zu RangeErrors mit Pufferausschnitten werden. */ class Leser { private position = 0; constructor(private readonly puffer: Buffer) {} uint32(): number { if (this.position + 4 > this.puffer.length) throw ungueltig(); const wert = this.puffer.readUInt32BE(this.position); this.position += 4; return wert; } bytes(laenge: number): Buffer { if (laenge < 0 || this.position + laenge > this.puffer.length) throw ungueltig(); const teil = this.puffer.subarray(this.position, this.position + laenge); this.position += laenge; return teil; } string(): Buffer { return this.bytes(this.uint32()); } text(): string { return this.string().toString("utf8"); } /** Liefert den Betrag einer nicht-negativen mpint ohne fuehrende Nullen. */ mpint(): Buffer { const roh = this.string(); if (roh.length > 0 && (roh[0]! & 0x80) !== 0) throw ungueltig(); let start = 0; while (start < roh.length && roh[start] === 0) start++; return roh.subarray(start); } rest(): Buffer { return this.puffer.subarray(this.position); } amEnde(): boolean { return this.position === this.puffer.length; } } function bitsVonBetrag(betrag: Buffer): number { if (betrag.length === 0) return 0; return (betrag.length - 1) * 8 + (32 - Math.clz32(betrag[0]!)); } function alsBigInt(betrag: Buffer): bigint { return betrag.length === 0 ? 0n : BigInt("0x" + betrag.toString("hex")); } // --------------------------------------------------------------------------- // Fingerprint und Public Keys // --------------------------------------------------------------------------- /** SHA-256-Fingerprint im Format von `ssh-keygen -l`: Base64 ohne Padding. */ export function fingerprintSha256(blob: Buffer): string { return "SHA256:" + createHash("sha256").update(blob).digest("base64").replace(/=+$/, ""); } /** Schluessellaenge eines `ssh-rsa`-Blobs in Bit (Laenge des Moduls n). */ export function bitsVonRsa(blob: Buffer): number { const leser = new Leser(blob); if (leser.text() !== "ssh-rsa") { throw new SchluesselFormatFehler("ungueltig", "Der Schlüssel ist kein RSA-Schlüssel."); } leser.mpint(); // e return bitsVonBetrag(leser.mpint()); } const ECDSA_PUNKTLAENGE: Record = { nistp256: 65, nistp384: 97, nistp521: 133, }; /** * Prueft den Aufbau eines Public-Key-Blobs. Erwartet wird genau der Typ `typ`; zusaetzliche * Bytes am Ende gelten als Fehler, weil OpenSSH sie ebenfalls ablehnt. */ function publicBlobPruefen(typ: PublicSchluesseltyp, blob: Buffer, rsaMindestBits: number): number | null { const leser = new Leser(blob); if (leser.text() !== typ) { throw new SchluesselFormatFehler( "ungueltig", "Der Schlüsseltyp in der Zeile passt nicht zum Inhalt des Schlüssels.", ); } let bits: number | null = null; switch (typ) { case "ssh-ed25519": if (leser.string().length !== 32) throw ungueltig(); bits = 256; break; case "sk-ssh-ed25519@openssh.com": if (leser.string().length !== 32) throw ungueltig(); leser.string(); // application, z. B. "ssh:" bits = 256; break; case "ssh-rsa": { const e = leser.mpint(); const n = leser.mpint(); if (e.length === 0 || n.length === 0 || (e[e.length - 1]! & 1) === 0) throw ungueltig(); bits = bitsVonBetrag(n); if (bits < rsaMindestBits) { throw new SchluesselFormatFehler( "zu_schwach", `RSA-Schlüssel müssen mindestens ${rsaMindestBits} Bit lang sein.`, ); } break; } case "ecdsa-sha2-nistp256": case "ecdsa-sha2-nistp384": case "ecdsa-sha2-nistp521": case "sk-ecdsa-sha2-nistp256@openssh.com": { const erwarteteKurve = typ.startsWith("sk-") ? "nistp256" : typ.slice("ecdsa-sha2-".length); if (leser.text() !== erwarteteKurve) throw ungueltig(); const punkt = leser.string(); if (punkt.length !== ECDSA_PUNKTLAENGE[erwarteteKurve] || punkt[0] !== 0x04) throw ungueltig(); if (typ.startsWith("sk-")) leser.string(); // application bits = Number(erwarteteKurve.slice("nistp".length)); break; } } if (!leser.amEnde()) throw ungueltig(); return bits; } export interface PublicKeyInfo { typ: PublicSchluesseltyp; blob: Buffer; kommentar: string; fingerprint: string; /** Schluessellaenge in Bit (RSA: Modul, ECDSA: Kurve, ed25519: 256). */ bits: number | null; } export interface PublicKeyLeseOptionen { /** * Mindestlaenge fuer RSA. Standard ist RSA_MINDEST_BITS (Import). Wer vorhandene * authorized_keys nur beobachtet, setzt 0, damit alte Schluessel sichtbar bleiben. */ rsaMindestBits?: number; } const BASE64 = /^[A-Za-z0-9+/]+={0,2}$/; function istPublicTyp(wert: string): wert is PublicSchluesseltyp { return (PUBLIC_SCHLUESSELTYPEN as readonly string[]).includes(wert); } /** Steuerzeichen im Kommentar wuerden authorized_keys-Zeilen aufbrechen. */ function kommentarPruefen(kommentar: string): void { if (/[\x00-\x1f\x7f]/.test(kommentar)) { throw new SchluesselFormatFehler("ungueltig", "Der Kommentar enthält unzulässige Steuerzeichen."); } } /** * Liest eine Public-Key-Zeile `typ base64 [kommentar]` (ohne authorized_keys-Optionen). */ export function publicKeyZeileLesen(zeile: string, optionen: PublicKeyLeseOptionen = {}): PublicKeyInfo { const treffer = /^(\S+)[ \t]+(\S+)(?:[ \t]+(.*))?$/.exec(zeile.trim()); if (!treffer) { throw new SchluesselFormatFehler("ungueltig", "Die Zeile ist kein Public Key im OpenSSH-Format."); } const typText = treffer[1]!; const base64 = treffer[2]!; const kommentar = (treffer[3] ?? "").trim(); if (!istPublicTyp(typText)) { throw new SchluesselFormatFehler("nicht_unterstuetzt", "Dieser Schlüsseltyp wird nicht unterstützt."); } if (!BASE64.test(base64) || base64.length % 4 !== 0) { throw new SchluesselFormatFehler("ungueltig", "Der Public Key ist nicht korrekt Base64-kodiert."); } const blob = Buffer.from(base64, "base64"); kommentarPruefen(kommentar); const bits = publicBlobPruefen(typText, blob, optionen.rsaMindestBits ?? RSA_MINDEST_BITS); return { typ: typText, blob, kommentar, fingerprint: fingerprintSha256(blob), bits }; } function publicKeyZeileBauen(blob: Buffer, typ: string, kommentar: string): string { const basis = `${typ} ${blob.toString("base64")}`; return kommentar ? `${basis} ${kommentar}` : basis; } // --------------------------------------------------------------------------- // Private Keys: Hilfen // --------------------------------------------------------------------------- /** 48 Byte aus bcrypt_pbkdf: 32 Byte AES-Schluessel, 16 Byte Zaehler-Startwert. */ function bcryptAbleiten(passphrase: string, salt: Buffer, runden: number): Buffer { const passwortBytes = Buffer.from(passphrase, "utf8"); const ergebnis = Buffer.alloc(48); try { const rueckgabe = bcryptPbkdf.pbkdf( passwortBytes, passwortBytes.length, salt, salt.length, ergebnis, ergebnis.length, runden, ); if (rueckgabe !== 0) throw ungueltig(); return ergebnis; } finally { passwortBytes.fill(0); } } /** OpenSSH bricht die Base64-Zeilen nach 70 Zeichen um. */ function pemKodieren(datei: Buffer): string { const base64 = datei.toString("base64"); const zeilen: string[] = []; for (let i = 0; i < base64.length; i += 70) zeilen.push(base64.slice(i, i + 70)); return `${PEM_ANFANG}\n${zeilen.join("\n")}\n${PEM_ENDE}\n`; } function pemDekodieren(text: string): Buffer { const normalisiert = text.replace(/\r\n?/g, "\n").trim(); if (!normalisiert.startsWith(PEM_ANFANG)) { if (/-----BEGIN [A-Z ]*PRIVATE KEY-----/.test(normalisiert)) { throw new SchluesselFormatFehler( "nicht_unterstuetzt", "Nur Private Keys im OpenSSH-Format werden unterstützt („BEGIN OPENSSH PRIVATE KEY“).", ); } throw new SchluesselFormatFehler("ungueltig", "Der Text ist kein Private Key im OpenSSH-Format."); } if (!normalisiert.endsWith(PEM_ENDE)) { throw ungueltig("Der Private Key ist unvollständig (Endzeile fehlt)."); } const rumpf = normalisiert .slice(PEM_ANFANG.length, normalisiert.length - PEM_ENDE.length) .replace(/\s+/g, ""); if (!BASE64.test(rumpf) || rumpf.length % 4 !== 0) { throw ungueltig("Der Private Key ist nicht korrekt Base64-kodiert."); } return Buffer.from(rumpf, "base64"); } /** PKCS#8-Huelle fuer einen ed25519-Seed (RFC 8410), damit Node daraus den Public Key ableitet. */ const ED25519_PKCS8_PRAEFIX = Buffer.from("302e020100300506032b657004220420", "hex"); function ed25519PublicAusSeed(seed: Buffer): Buffer { const der = Buffer.concat([ED25519_PKCS8_PRAEFIX, seed]); try { const jwk = createPublicKey(createPrivateKey({ key: der, format: "der", type: "pkcs8" })).export({ format: "jwk", }); return Buffer.from(jwk.x ?? "", "base64url"); } catch { throw ungueltig(); } finally { der.fill(0); } } function gleich(a: Buffer, b: Buffer): boolean { return a.length === b.length && timingSafeEqual(a, b); } // --------------------------------------------------------------------------- // Private Keys: Erzeugen und Schreiben // --------------------------------------------------------------------------- export interface SchluesselpaarOptionen { kommentar: string; /** Leer oder nicht gesetzt: unverschluesselt, wie `ssh-keygen -N ''`. */ passphrase?: string; } export interface ErzeugtesSchluesselpaar { /** z. B. "ssh-ed25519 AAAA… kommentar" */ publicKeyZeile: string; /** "-----BEGIN OPENSSH PRIVATE KEY-----…" */ privateKeyOpenssh: string; /** "SHA256:…" ohne Padding */ fingerprint: string; } /** * Baut die Datei `openssh-key-v1` (PROTOCOL.key). `privatTeil` sind die typabhaengigen * Felder ohne Kommentar. */ function privateKeySchreiben( publicBlob: Buffer, privatTeil: Buffer, kommentar: string, passphrase: string | undefined, ): string { const verschluesseln = passphrase !== undefined && passphrase !== ""; const blockgroesse = verschluesseln ? 16 : 8; // Zweimal derselbe Zufallswert: Beim Lesen verraet ein Unterschied die falsche Passphrase. const checkint = randomBytes(4); const ohnePadding = Buffer.concat([checkint, checkint, privatTeil, sshString(kommentar)]); const paddingLaenge = (blockgroesse - (ohnePadding.length % blockgroesse)) % blockgroesse; const padding = Buffer.from(Array.from({ length: paddingLaenge }, (_, i) => i + 1)); const abschnitt = Buffer.concat([ohnePadding, padding]); ohnePadding.fill(0); let cipherName = "none"; let kdfName = "none"; let kdfOptionen = Buffer.alloc(0); let inhalt = abschnitt; if (verschluesseln) { const salt = randomBytes(BCRYPT_SALT_LAENGE); const abgeleitet = bcryptAbleiten(passphrase, salt, BCRYPT_RUNDEN); try { const cipher = createCipheriv("aes-256-ctr", abgeleitet.subarray(0, 32), abgeleitet.subarray(32, 48)); inhalt = Buffer.concat([cipher.update(abschnitt), cipher.final()]); } finally { abgeleitet.fill(0); } cipherName = "aes256-ctr"; kdfName = "bcrypt"; kdfOptionen = Buffer.concat([sshString(salt), uint32(BCRYPT_RUNDEN)]); } const datei = Buffer.concat([ MAGIC, sshString(cipherName), sshString(kdfName), sshString(kdfOptionen), uint32(1), sshString(publicBlob), sshString(inhalt), ]); const pem = pemKodieren(datei); datei.fill(0); abschnitt.fill(0); return pem; } /** * Erzeugt ein neues Schluesselpaar. Asynchron, weil RSA-4096 sonst den Worker fuer * spuerbare Zeit blockiert. Der Private Key liegt danach als String vor; den kann * JavaScript nicht ueberschreiben – Aufrufer sollen ihn sofort versiegeln. */ export async function schluesselpaarErzeugen( typ: ErzeugbarerSchluesseltyp, { kommentar, passphrase }: SchluesselpaarOptionen, ): Promise { kommentarPruefen(kommentar); let typName: string; let publicBlob: Buffer; let privatTeil: Buffer; if (typ === "ED25519") { typName = "ssh-ed25519"; const { privateKey } = await erzeugeSchluesselpaar("ed25519"); const jwk = privateKey.export({ format: "jwk" }); const oeffentlich = Buffer.from(jwk.x ?? "", "base64url"); const seed = Buffer.from(jwk.d ?? "", "base64url"); // OpenSSH speichert 64 Byte: Seed gefolgt vom Public Key (Format von ref10/NaCl). const geheim = Buffer.concat([seed, oeffentlich]); publicBlob = Buffer.concat([sshString(typName), sshString(oeffentlich)]); privatTeil = Buffer.concat([sshString(typName), sshString(oeffentlich), sshString(geheim)]); seed.fill(0); geheim.fill(0); } else { typName = "ssh-rsa"; const { privateKey } = await erzeugeSchluesselpaar("rsa", { modulusLength: 4096, publicExponent: 0x10001, }); const jwk = privateKey.export({ format: "jwk" }); const feld = (wert: string | undefined) => Buffer.from(wert ?? "", "base64url"); const n = feld(jwk.n); const e = feld(jwk.e); const d = feld(jwk.d); const p = feld(jwk.p); const q = feld(jwk.q); const iqmp = feld(jwk.qi); // Public-Teil: e vor n (RFC 4253). Private-Teil: n e d iqmp p q (sshkey.c). JWK `qi` // ist q^-1 mod p und damit genau das `iqmp` von OpenSSH. publicBlob = Buffer.concat([sshString(typName), mpint(e), mpint(n)]); privatTeil = Buffer.concat([ sshString(typName), mpint(n), mpint(e), mpint(d), mpint(iqmp), mpint(p), mpint(q), ]); for (const geheim of [d, p, q, iqmp]) geheim.fill(0); } try { const privateKeyOpenssh = privateKeySchreiben(publicBlob, privatTeil, kommentar, passphrase); return { publicKeyZeile: publicKeyZeileBauen(publicBlob, typName, kommentar), privateKeyOpenssh, fingerprint: fingerprintSha256(publicBlob), }; } finally { privatTeil.fill(0); } } // --------------------------------------------------------------------------- // Private Keys: Lesen // --------------------------------------------------------------------------- export interface PrivateKeyInfo { typ: PrivateSchluesseltyp; bits: number; publicKeyZeile: string; kommentar: string; fingerprint: string; /** War die Datei mit einer Passphrase geschuetzt? */ verschluesselt: boolean; } export interface PrivateKeyLeseOptionen { rsaMindestBits?: number; } /** * Liest einen Private Key im OpenSSH-Format, entschluesselt ihn bei Bedarf und prueft, * dass Private und Public Key zusammengehoeren. Gibt nur oeffentliche Angaben zurueck. */ export function privateKeyLesen( openssh: string, passphrase?: string, optionen: PrivateKeyLeseOptionen = {}, ): PrivateKeyInfo { const datei = pemDekodieren(openssh); const zuLoeschen: Buffer[] = [datei]; try { const leser = new Leser(datei); if (!gleich(leser.bytes(MAGIC.length), MAGIC)) { throw new SchluesselFormatFehler("ungueltig", "Der Text ist kein Private Key im OpenSSH-Format."); } const cipherName = leser.text(); const kdfName = leser.text(); const kdfOptionen = leser.string(); if (leser.uint32() !== 1) { throw new SchluesselFormatFehler( "nicht_unterstuetzt", "Dateien mit mehr als einem Schlüssel werden nicht unterstützt.", ); } const publicBlob = Buffer.from(leser.string()); const inhalt = leser.string(); if (!leser.amEnde()) throw ungueltig(); const publicTyp = new Leser(publicBlob).text(); if (publicTyp !== "ssh-ed25519" && publicTyp !== "ssh-rsa") { throw new SchluesselFormatFehler( "nicht_unterstuetzt", "Es werden nur Private Keys vom Typ ed25519 und RSA unterstützt.", ); } // Der Public Key liegt unverschluesselt vor; eine zu kurze RSA-Laenge faellt so auf, // bevor jemand nach der Passphrase gefragt wird. const bits = publicBlobPruefen(publicTyp, publicBlob, optionen.rsaMindestBits ?? RSA_MINDEST_BITS) ?? 0; let abschnitt: Buffer; let blockgroesse: number; const verschluesselt = cipherName !== "none"; if (cipherName === "none") { if (kdfName !== "none" || kdfOptionen.length !== 0) throw ungueltig(); abschnitt = inhalt; blockgroesse = 8; } else if (cipherName === "aes256-ctr") { if (kdfName !== "bcrypt") { throw new SchluesselFormatFehler( "nicht_unterstuetzt", "Das Verfahren zur Ableitung des Schlüssels wird nicht unterstützt.", ); } if (passphrase === undefined || passphrase === "") { throw new SchluesselFormatFehler( "passphrase_fehlt", "Der Private Key ist mit einer Passphrase geschützt.", ); } const kdfLeser = new Leser(kdfOptionen); const salt = kdfLeser.string(); const runden = kdfLeser.uint32(); if (!kdfLeser.amEnde() || salt.length === 0 || runden < 1 || runden > BCRYPT_RUNDEN_HOECHSTENS) { throw ungueltig(); } blockgroesse = 16; if (inhalt.length % blockgroesse !== 0) throw ungueltig(); const abgeleitet = bcryptAbleiten(passphrase, salt, runden); try { const decipher = createDecipheriv( "aes-256-ctr", abgeleitet.subarray(0, 32), abgeleitet.subarray(32, 48), ); abschnitt = Buffer.concat([decipher.update(inhalt), decipher.final()]); zuLoeschen.push(abschnitt); } finally { abgeleitet.fill(0); } } else { throw new SchluesselFormatFehler( "nicht_unterstuetzt", "Die Verschlüsselung dieses Private Keys wird nicht unterstützt (erwartet: aes256-ctr).", ); } if (abschnitt.length % blockgroesse !== 0) throw ungueltig(); const privat = new Leser(abschnitt); if (privat.uint32() !== privat.uint32()) { throw verschluesselt ? new SchluesselFormatFehler("passphrase_falsch", "Die Passphrase ist falsch.") : ungueltig(); } if (privat.text() !== publicTyp) throw ungueltig(); let typ: PrivateSchluesseltyp; const erwartet = new Leser(publicBlob); erwartet.text(); if (publicTyp === "ssh-ed25519") { typ = "ED25519"; const oeffentlich = privat.string(); const geheim = privat.string(); if (geheim.length !== 64 || !gleich(oeffentlich, erwartet.string())) throw ungueltig(); if (!gleich(geheim.subarray(32), oeffentlich)) throw ungueltig(); if (!gleich(ed25519PublicAusSeed(geheim.subarray(0, 32)), oeffentlich)) throw ungueltig(); } else { typ = "RSA"; const n = privat.mpint(); const e = privat.mpint(); const d = privat.mpint(); privat.mpint(); // iqmp const p = privat.mpint(); const q = privat.mpint(); const erwartetesE = erwartet.mpint(); const erwartetesN = erwartet.mpint(); if (!gleich(n, erwartetesN) || !gleich(e, erwartetesE) || d.length === 0) throw ungueltig(); // Billige Plausibilitaet: Ein Private Key, dessen Primfaktoren nicht n ergeben, ist kaputt. if (alsBigInt(p) * alsBigInt(q) !== alsBigInt(n)) throw ungueltig(); } const kommentar = privat.text(); kommentarPruefen(kommentar); const padding = privat.rest(); if (padding.length >= blockgroesse || padding.some((wert, i) => wert !== i + 1)) throw ungueltig(); return { typ, bits, publicKeyZeile: publicKeyZeileBauen(publicBlob, publicTyp, kommentar), kommentar, fingerprint: fingerprintSha256(publicBlob), verschluesselt, }; } finally { for (const puffer of zuLoeschen) puffer.fill(0); } }