Files
NetMesh/electron/bridges/privateKeyNormalizer.test.cjs

159 lines
5.7 KiB
JavaScript
Raw Normal View History

const test = require("node:test");
const assert = require("node:assert/strict");
const crypto = require("node:crypto");
const { utils: sshUtils } = require("ssh2");
const {
normalizePrivateKeyForSsh2,
PrivateKeyPassphraseError,
UnsupportedPrivateKeyError,
hasPrivateKeyMaterial,
} = require("./privateKeyNormalizer.cjs");
function genRsa(encoding) {
return crypto.generateKeyPairSync("rsa", {
modulusLength: 2048,
privateKeyEncoding: encoding,
publicKeyEncoding: { type: "spki", format: "pem" },
}).privateKey;
}
const rsaPkcs8 = () => genRsa({ type: "pkcs8", format: "pem" });
const rsaPkcs1 = () => genRsa({ type: "pkcs1", format: "pem" });
const encryptedRsaPkcs8 = (passphrase) =>
genRsa({ type: "pkcs8", format: "pem", cipher: "aes-256-cbc", passphrase });
const ecPkcs8 = () =>
crypto.generateKeyPairSync("ec", {
namedCurve: "prime256v1",
privateKeyEncoding: { type: "pkcs8", format: "pem" },
publicKeyEncoding: { type: "spki", format: "pem" },
}).privateKey;
const ed25519Pkcs8 = () =>
crypto.generateKeyPairSync("ed25519", {
privateKeyEncoding: { type: "pkcs8", format: "pem" },
publicKeyEncoding: { type: "spki", format: "pem" },
}).privateKey;
function parseOk(key) {
const r = sshUtils.parseKey(key);
return r && !(r instanceof Error) ? r : null;
}
test("checks private material on ssh2 parser objects and arrays", () => {
const privateKey = {
isPrivateKey: () => true,
getPrivatePEM: () => "private",
};
const publicKey = {
isPrivateKey: () => false,
getPrivatePEM: () => null,
};
assert.equal(hasPrivateKeyMaterial(privateKey), true);
assert.equal(hasPrivateKeyMaterial(publicKey), false);
assert.equal(hasPrivateKeyMaterial([publicKey, privateKey]), true);
assert.equal(hasPrivateKeyMaterial([publicKey]), false);
});
test("converts unencrypted RSA PKCS#8 into an ssh2-parseable key", () => {
const result = normalizePrivateKeyForSsh2(rsaPkcs8());
assert.equal(result.converted, true);
const parsed = parseOk(result.privateKey);
assert.ok(parsed, "converted key should be parseable by ssh2");
assert.equal(parsed.type, "ssh-rsa");
});
test("converts unencrypted EC PKCS#8 into an ssh2-parseable key", () => {
const result = normalizePrivateKeyForSsh2(ecPkcs8());
assert.equal(result.converted, true);
const parsed = parseOk(result.privateKey);
assert.ok(parsed);
assert.equal(parsed.type, "ecdsa-sha2-nistp256");
});
test("leaves an already-supported PKCS#1 key untouched", () => {
const key = rsaPkcs1();
const result = normalizePrivateKeyForSsh2(key);
assert.equal(result.converted, false);
assert.equal(result.privateKey, key);
});
test("decrypts and converts an encrypted RSA PKCS#8 key with the correct passphrase", () => {
const result = normalizePrivateKeyForSsh2(encryptedRsaPkcs8("secret"), "secret");
assert.equal(result.converted, true);
assert.equal(result.passphrase, undefined);
const parsed = parseOk(result.privateKey);
assert.ok(parsed);
assert.equal(parsed.type, "ssh-rsa");
});
test("throws PrivateKeyPassphraseError for encrypted PKCS#8 with a wrong passphrase", () => {
assert.throws(
() => normalizePrivateKeyForSsh2(encryptedRsaPkcs8("secret"), "wrong"),
(err) => err instanceof PrivateKeyPassphraseError,
);
});
test("throws UnsupportedPrivateKeyError for Ed25519 PKCS#8 with a conversion hint", () => {
assert.throws(
() => normalizePrivateKeyForSsh2(ed25519Pkcs8()),
(err) => err instanceof UnsupportedPrivateKeyError && /ssh-keygen/.test(err.message),
);
});
test("passes through content that is not a PKCS#8 key", () => {
const junk = "not a private key";
const result = normalizePrivateKeyForSsh2(junk);
assert.equal(result.converted, false);
assert.equal(result.privateKey, junk);
});
const indentLines = (key) =>
key.split("\n").map((line) => (line ? " " + line : line)).join("\n");
const dropEndLine = (key) => key.split("\n").slice(0, -2).join("\n");
const legacyEncryptedRsa = (passphrase) =>
crypto.generateKeyPairSync("rsa", {
modulusLength: 2048,
privateKeyEncoding: { type: "pkcs1", format: "pem", cipher: "aes-128-cbc", passphrase },
publicKeyEncoding: { type: "spki", format: "pem" },
}).privateKey;
test("repairs an RSA key whose newlines were collapsed to spaces", () => {
const result = normalizePrivateKeyForSsh2(rsaPkcs1().replace(/\n/g, " "));
assert.equal(result.converted, true);
assert.ok(parseOk(result.privateKey), "repaired key should be parseable by ssh2");
});
test("repairs an RSA key whose newlines became literal backslash-n", () => {
const result = normalizePrivateKeyForSsh2(rsaPkcs1().replace(/\n/g, "\\n"));
assert.equal(result.converted, true);
assert.ok(parseOk(result.privateKey));
});
test("repairs an RSA key whose lines are indented", () => {
const result = normalizePrivateKeyForSsh2(indentLines(rsaPkcs1()));
assert.equal(result.converted, true);
assert.ok(parseOk(result.privateKey));
});
test("repairs and converts a collapsed PKCS#8 key", () => {
const result = normalizePrivateKeyForSsh2(rsaPkcs8().replace(/\n/g, " "));
assert.equal(result.converted, true);
const parsed = parseOk(result.privateKey);
assert.ok(parsed);
assert.equal(parsed.type, "ssh-rsa");
});
test("cannot repair a truncated key and leaves it unchanged", () => {
const truncated = dropEndLine(rsaPkcs1());
const result = normalizePrivateKeyForSsh2(truncated);
assert.equal(result.converted, false);
assert.equal(result.privateKey, truncated);
});
test("does not attempt to repair an encrypted legacy PEM (DEK-Info)", () => {
const collapsed = legacyEncryptedRsa("secret").replace(/\n/g, " ");
const result = normalizePrivateKeyForSsh2(collapsed, "secret");
assert.equal(result.converted, false);
});