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>
82 lines
2.7 KiB
TypeScript
82 lines
2.7 KiB
TypeScript
import { generateKeyPairSync } from "node:crypto";
|
|
|
|
/** Kleine Wire-Format-Helfer, um gezielt kaputte oder seltene Blobs zu bauen (#006). */
|
|
export function sshString(inhalt: Buffer | string): Buffer {
|
|
const daten = typeof inhalt === "string" ? Buffer.from(inhalt) : inhalt;
|
|
const laenge = Buffer.alloc(4);
|
|
laenge.writeUInt32BE(daten.length);
|
|
return Buffer.concat([laenge, daten]);
|
|
}
|
|
|
|
export function mpint(betrag: Buffer): Buffer {
|
|
return sshString((betrag[0]! & 0x80) !== 0 ? Buffer.concat([Buffer.from([0]), betrag]) : betrag);
|
|
}
|
|
|
|
export function zeile(typ: string, blob: Buffer, kommentar = "test"): string {
|
|
return `${typ} ${blob.toString("base64")} ${kommentar}`;
|
|
}
|
|
|
|
export function rsaBlob(bits: number): Buffer {
|
|
const { publicKey } = generateKeyPairSync("rsa", { modulusLength: bits });
|
|
const jwk = publicKey.export({ format: "jwk" });
|
|
return Buffer.concat([
|
|
sshString("ssh-rsa"),
|
|
mpint(Buffer.from(jwk.e!, "base64url")),
|
|
mpint(Buffer.from(jwk.n!, "base64url")),
|
|
]);
|
|
}
|
|
|
|
const KURVEN = { nistp256: "P-256", nistp384: "P-384", nistp521: "P-521" } as const;
|
|
|
|
export function ecdsaPunkt(kurve: keyof typeof KURVEN): Buffer {
|
|
const { publicKey } = generateKeyPairSync("ec", { namedCurve: KURVEN[kurve] });
|
|
const jwk = publicKey.export({ format: "jwk" });
|
|
return Buffer.concat([
|
|
Buffer.from([4]),
|
|
Buffer.from(jwk.x!, "base64url"),
|
|
Buffer.from(jwk.y!, "base64url"),
|
|
]);
|
|
}
|
|
|
|
export function ecdsaBlob(kurve: keyof typeof KURVEN): Buffer {
|
|
return Buffer.concat([sshString(`ecdsa-sha2-${kurve}`), sshString(kurve), sshString(ecdsaPunkt(kurve))]);
|
|
}
|
|
|
|
export function ed25519Roh(): Buffer {
|
|
const { publicKey } = generateKeyPairSync("ed25519");
|
|
return Buffer.from(publicKey.export({ format: "jwk" }).x!, "base64url");
|
|
}
|
|
|
|
export function ed25519Blob(): Buffer {
|
|
return Buffer.concat([sshString("ssh-ed25519"), sshString(ed25519Roh())]);
|
|
}
|
|
|
|
export function skEd25519Blob(): Buffer {
|
|
return Buffer.concat([sshString("sk-ssh-ed25519@openssh.com"), sshString(ed25519Roh()), sshString("ssh:")]);
|
|
}
|
|
|
|
export function skEcdsaBlob(): Buffer {
|
|
return Buffer.concat([
|
|
sshString("sk-ecdsa-sha2-nistp256@openssh.com"),
|
|
sshString("nistp256"),
|
|
sshString(ecdsaPunkt("nistp256")),
|
|
sshString("ssh:"),
|
|
]);
|
|
}
|
|
|
|
/** Base64-Rumpf eines PEM-Textes, um Fehlermeldungen auf Schluesselmaterial zu pruefen. */
|
|
export function pemRumpf(pem: string): string {
|
|
return pem
|
|
.split("\n")
|
|
.filter((z) => z && !z.startsWith("-----"))
|
|
.join("");
|
|
}
|
|
|
|
/** Ob `text` irgendein 16-Zeichen-Stueck aus `material` enthaelt. */
|
|
export function enthaeltMaterial(text: string, material: string): boolean {
|
|
for (let i = 0; i + 16 <= material.length; i += 4) {
|
|
if (text.includes(material.slice(i, i + 16))) return true;
|
|
}
|
|
return false;
|
|
}
|