Files
sshkeyvault/src/lib/ssh/openssh.ts
T
florian.wildeandClaude Opus 5.5 6b20566676 feat: OpenSSH-Schluessel erzeugen, schreiben und lesen (#006)
Private Keys sollen nie als Datei auf dem sshkv-Server liegen, auch nicht
kurz. ssh-keygen braucht aber genau das. Deshalb erzeugt Node crypto die
Schluessel (ed25519, RSA-4096), und ein eigener Serializer schreibt das
Format openssh-key-v1 – unverschluesselt oder mit aes256-ctr und
bcrypt_pbkdf (16 Runden, 16 Byte Salt), Checkint und Padding wie OpenSSH.

Der Parser liest auch von ssh-keygen erzeugte Dateien, prueft, dass
Private und Public Key zusammengehoeren, und meldet fehlende oder falsche
Passphrasen ueber eigene Codes. Fehlermeldungen sind feste Saetze ohne
Eingabe darin, damit kein Schluesselmaterial in Logs geraet. RSA unter
2048 Bit wird beim Import abgelehnt; wer nur beobachtet, kann die Grenze
ausschalten.

Die Tests pruefen beide Richtungen gegen das echte ssh-keygen (-y, -lf).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 09:16:42 +02:00

683 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<string, number> = {
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<ErzeugtesSchluesselpaar> {
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);
}
}