"use strict"; const test = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const { utils: sshUtils } = require("ssh2"); const { KeyGeneratorService, PASSWORD_SETS, generateDsaNumbers } = require("../src/main/keys/keyGeneratorService"); test("key generator creates modern and explicitly confirmed legacy key pairs", () => { const service = new KeyGeneratorService(); for (const input of [ { type: "ed25519" }, { type: "rsa", bits: 2048 }, { type: "ecdsa", bits: 256 }, { type: "dsa", bits: 1024, legacyConfirmed: true }, { type: "rsa1", bits: 1024, legacyConfirmed: true } ]) { const generated = service.generate({ ...input, comment: "test@core" }); if (input.type === "rsa1") assert.match(generated.privateKey, /^PuTTY-User-Key-File-2: ssh1-rsa/); else assert.match(generated.privateKey, /BEGIN PRIVATE KEY/); assert.ok(generated.publicKey.length > 40); assert.equal(Object.prototype.hasOwnProperty.call(generated, "passphrase"), false); } assert.throws(() => service.generate({ type: "dsa" }), /explicit confirmation/); assert.throws(() => service.generate({ type: "rsa1" }), /explicit confirmation/); }); test("key generator exports OpenSSH, PEM and PPK formats and reports safe passphrase fallback", () => { const service = new KeyGeneratorService(); for (const input of [ { type: "ed25519", privateKeyFormat: "openssh" }, { type: "rsa", bits: 2048, privateKeyFormat: "openssh" }, { type: "ecdsa", bits: 256, privateKeyFormat: "openssh" }, { type: "dsa", bits: 1024, privateKeyFormat: "openssh", legacyConfirmed: true } ]) { const generated = service.generate(input); assert.equal(generated.privateKeyFormat, "openssh"); assert.match(generated.privateKey, /^-----BEGIN OPENSSH PRIVATE KEY-----/); assert.match(generated.publicKeyOpenSsh, /^(ssh-|ecdsa-)/); const parsed = sshUtils.parseKey(generated.privateKey); assert.equal(parsed instanceof Error, false, parsed?.message); } for (const input of [ { type: "ed25519" }, { type: "rsa", bits: 2048 }, { type: "ecdsa", bits: 256 }, { type: "dsa", legacyConfirmed: true }, { type: "rsa1", legacyConfirmed: true } ]) { const generated = service.generate({ ...input, privateKeyFormat: "ppk" }); assert.equal(generated.privateKeyFormat, "ppk"); assert.match(generated.privateKey, /^PuTTY-User-Key-File-2: /); assert.match(generated.privateKey, /\nPrivate-MAC: [a-f0-9]{40}\n$/); } const fallback = service.generate({ type: "ed25519", privateKeyFormat: "openssh", passphrase: "secret" }); assert.equal(fallback.privateKeyFormat, "pem"); assert.match(fallback.privateKey, /BEGIN ENCRYPTED PRIVATE KEY/); assert.match(fallback.notice, /šifrovaný PEM/); }); test("key generator saves only a selected generated pair with restrictive private mode", async (t) => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "chj-keys-")); t.after(() => fs.rmSync(root, { recursive: true, force: true })); const target = path.join(root, "id_ed25519"); const service = new KeyGeneratorService({ selectSavePath: async () => ({ canceled: false, path: target }) }); const generated = service.generate({ type: "ed25519" }); const saved = await service.save(generated.generationId); assert.equal(saved.privatePath, target); assert.match(fs.readFileSync(target, "utf8"), /BEGIN PRIVATE KEY/); assert.match(fs.readFileSync(`${target}.pub`, "utf8"), /^ssh-ed25519 /); if (process.platform !== "win32") assert.equal(fs.statSync(target).mode & 0o777, 0o600); }); test("DSA works without platform DSA key generation (Electron/BoringSSL) and yields a valid 1024/160 key", () => { const nodeCrypto = require("node:crypto"); const original = nodeCrypto.generateKeyPairSync; nodeCrypto.generateKeyPairSync = (type, ...rest) => { if (type === "dsa") throw new Error("Key generation job failed"); return original(type, ...rest); }; try { const service = new KeyGeneratorService({}); for (const privateKeyFormat of ["openssh", "pem", "ppk"]) { const result = service.generate({ type: "dsa", legacyConfirmed: true, privateKeyFormat, comment: "t" }); assert.equal(result.type, "dsa"); assert.match(result.publicKey, /^ssh-dss AAAAB3NzaC1kc3M/); const key = nodeCrypto.createPrivateKey(result.privateKeyPem); assert.deepEqual(key.asymmetricKeyDetails, { modulusLength: 1024, divisorLength: 160 }); const signature = nodeCrypto.sign("sha1", Buffer.from("chj"), key); assert.equal(nodeCrypto.verify("sha1", Buffer.from("chj"), nodeCrypto.createPublicKey(key), signature), true); if (privateKeyFormat === "openssh") assert.match(result.privateKey, /^-----BEGIN OPENSSH PRIVATE KEY-----/); if (privateKeyFormat === "ppk") assert.match(result.privateKey, /^PuTTY-User-Key-File-2: ssh-dss/); } assert.throws(() => service.generate({ type: "dsa" }), /explicit confirmation/); } finally { nodeCrypto.generateKeyPairSync = original; } }); test("DSA domain parameters satisfy q | p-1 and g^q = 1 mod p", () => { const { p, q, g, x, y } = generateDsaNumbers(); const pow = (b, e, m) => { let r = 1n; b %= m; while (e > 0n) { if (e & 1n) r = r * b % m; b = b * b % m; e >>= 1n; } return r; }; assert.equal(p.toString(2).length, 1024); assert.equal(q.toString(2).length, 160); assert.equal((p - 1n) % q, 0n); assert.ok(g > 1n && g < p); assert.equal(pow(g, q, p), 1n); assert.ok(x > 0n && x < q); assert.equal(pow(g, x, p), y); }); test("password generator: length 4-70, selected sets only, grouping every 5th character", () => { const service = new KeyGeneratorService({}); const allowed = (options) => new Set(Object.keys(PASSWORD_SETS).filter((key) => options[key]).flatMap((key) => [...PASSWORD_SETS[key]])); for (const options of [{ length: 4, upper: true }, { length: 70, upper: true, lower: true, digits: true, special: true }, { length: 33, digits: true, special: true }]) { for (let run = 0; run < 50; run += 1) { const { password } = service.generatePassword(options); assert.equal([...password].length, options.length); for (const char of password) assert.ok(allowed(options).has(char), `unexpected ${char}`); for (const key of Object.keys(PASSWORD_SETS).filter((name) => options[name])) assert.ok([...password].some((char) => PASSWORD_SETS[key].includes(char)), `${key} present`); } } assert.equal(PASSWORD_SETS.digits, "123456789"); assert.equal(PASSWORD_SETS.special, "-@)¨§'=+"); for (let run = 0; run < 50; run += 1) { const { password, randomCharacters } = service.generatePassword({ length: 20, upper: true, digits: true, special: true, grouped: true }); assert.match(password, /^[^-]{4}-[^-]{4}-[^-]{4}-[^-]{4}-$/u, password); assert.equal(randomCharacters, 16); } assert.match(service.generatePassword({ length: 12, lower: true, grouped: true }).password, /^[a-z]{4}-[a-z]{4}-[a-z]{2}$/); assert.match(service.generatePassword({ length: 4, lower: true, grouped: true }).password, /^[a-z]{4}$/, "no separator before the 5th character"); for (const bad of [3, 71, 12.5, "x"]) assert.throws(() => service.generatePassword({ length: bad, upper: true }), /4-70/); assert.throws(() => service.generatePassword({ length: 12 }), /at least one character set/); // Even the shortest grouped password keeps 4 random characters, so all four sets always fit. const all = service.generatePassword({ length: 5, upper: true, lower: true, digits: true, special: true, grouped: true }); assert.match(all.password, /^[^-]{4}-$/u); assert.equal(all.randomCharacters, 4); const seen = new Set(); for (let run = 0; run < 200; run += 1) for (const char of service.generatePassword({ length: 20, digits: true }).password) seen.add(char); assert.equal(seen.size, 9, "every digit 1-9 occurs, 0 never"); });