Files
NetMesh/domain/knownHosts.test.ts
zhaolei 3c72efcb7f
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
[Init] Initial commit - NetMesh terminal manager
2026-09-13 18:24:01 +08:00

261 lines
7.9 KiB
TypeScript

import test from "node:test";
import assert from "node:assert/strict";
import crypto from "node:crypto";
import type { KnownHost } from "./models";
import {
fingerprintFromPublicKey,
normalizeKnownHost,
normalizeKnownHosts,
upsertKnownHost,
} from "./knownHosts";
const knownHost = (overrides: Partial<KnownHost> = {}): KnownHost => ({
id: "kh-existing",
hostname: "10.2.0.32",
port: 22,
keyType: "ssh-ed25519",
publicKey: "ssh-ed25519 old-key",
fingerprint: "old-fingerprint",
discoveredAt: 100,
...overrides,
});
test("upsertKnownHost updates an existing host key instead of appending a duplicate", () => {
const existing = knownHost({ convertedToHostId: "host-1" });
const incoming = knownHost({
id: "kh-new",
publicKey: "ssh-ed25519 new-key",
fingerprint: "new-fingerprint",
discoveredAt: 200,
});
const result = upsertKnownHost([existing], incoming);
assert.equal(result.length, 1);
assert.deepEqual(result[0], {
...existing,
publicKey: "ssh-ed25519 new-key",
fingerprint: "new-fingerprint",
lastSeen: 200,
});
});
test("upsertKnownHost updates by id even when the incoming key type is unknown", () => {
const existing = knownHost({
id: "kh-1",
keyType: "ssh-ed25519",
publicKey: "SHA256:old-key",
fingerprint: "old-fingerprint",
discoveredAt: 100,
});
const incoming = knownHost({
id: "kh-1",
keyType: "unknown",
publicKey: undefined,
fingerprint: "new-fingerprint",
discoveredAt: 200,
});
const result = upsertKnownHost([existing], incoming);
assert.equal(result.length, 1);
assert.equal(result[0].id, "kh-1");
assert.equal(result[0].keyType, "unknown");
assert.equal(result[0].fingerprint, "new-fingerprint");
assert.equal(result[0].lastSeen, 200);
});
test("upsertKnownHost prefers the matching id over an earlier selector match", () => {
const duplicate = knownHost({
id: "kh-duplicate",
fingerprint: "duplicate-fingerprint",
discoveredAt: 50,
});
const target = knownHost({
id: "kh-target",
fingerprint: "target-fingerprint",
discoveredAt: 100,
});
const incoming = knownHost({
id: "kh-target",
fingerprint: "new-fingerprint",
discoveredAt: 200,
});
const result = upsertKnownHost([duplicate, target], incoming);
assert.equal(result.length, 2);
assert.equal(result[0].fingerprint, "duplicate-fingerprint");
assert.equal(result[1].id, "kh-target");
assert.equal(result[1].fingerprint, "new-fingerprint");
});
test("upsertKnownHost appends genuinely new host keys", () => {
const existing = knownHost();
const incoming = knownHost({
id: "kh-other",
hostname: "10.2.0.33",
fingerprint: "other-fingerprint",
});
const result = upsertKnownHost([existing], incoming);
assert.deepEqual(result, [existing, incoming]);
});
// --- Fingerprint derivation -------------------------------------------------
const makeRawPublicKey = (keyType: string, body = "trusted imported host key") => {
const type = Buffer.from(keyType);
const length = Buffer.alloc(4);
length.writeUInt32BE(type.length, 0);
return Buffer.concat([length, type, Buffer.from(body)]);
};
test("fingerprintFromPublicKey matches Node's SHA-256 over a base64-decoded OpenSSH line", () => {
const rawKey = makeRawPublicKey("ssh-ed25519");
const base64Body = rawKey.toString("base64");
const expected = crypto.createHash("sha256").update(rawKey).digest("base64").replace(/=+$/g, "");
assert.equal(fingerprintFromPublicKey(`ssh-ed25519 ${base64Body}`), expected);
assert.equal(
fingerprintFromPublicKey(`ssh-ed25519 ${base64Body} comment-tail`),
expected,
"trailing comment is ignored",
);
});
test("fingerprintFromPublicKey strips a SHA256: prefix and trailing padding", () => {
assert.equal(fingerprintFromPublicKey("SHA256:abc123=="), "abc123");
assert.equal(fingerprintFromPublicKey("sha256:abc123"), "abc123");
});
test("fingerprintFromPublicKey returns empty string on missing input", () => {
assert.equal(fingerprintFromPublicKey(undefined), "");
assert.equal(fingerprintFromPublicKey(null), "");
assert.equal(fingerprintFromPublicKey(""), "");
});
// --- Migration --------------------------------------------------------------
test("normalizeKnownHost backfills fingerprint when only publicKey is stored", () => {
const rawKey = makeRawPublicKey("ssh-ed25519");
const base64Body = rawKey.toString("base64");
const expected = crypto.createHash("sha256").update(rawKey).digest("base64").replace(/=+$/g, "");
const stored: KnownHost = {
id: "kh-1",
hostname: "vps-1.example.com",
port: 22,
keyType: "ssh-ed25519",
publicKey: `ssh-ed25519 ${base64Body}`,
discoveredAt: 1,
};
const migrated = normalizeKnownHost(stored);
assert.notEqual(migrated, stored, "should return a new object when fingerprint is added");
assert.equal(migrated.fingerprint, expected);
assert.equal(migrated.keyType, "ssh-ed25519");
});
test("normalizeKnownHost backfills keyType from an OpenSSH-format publicKey", () => {
const rawKey = makeRawPublicKey("ssh-rsa");
const base64Body = rawKey.toString("base64");
const stored: KnownHost = {
id: "kh-1",
hostname: "vps-1.example.com",
port: 22,
keyType: "",
publicKey: `ssh-rsa ${base64Body}`,
discoveredAt: 1,
};
const migrated = normalizeKnownHost(stored);
assert.equal(migrated.keyType, "ssh-rsa");
});
test("normalizeKnownHost returns the same reference when nothing needs backfilling", () => {
const rawKey = makeRawPublicKey("ssh-ed25519");
const fp = crypto.createHash("sha256").update(rawKey).digest("base64").replace(/=+$/g, "");
const stored: KnownHost = {
id: "kh-1",
hostname: "vps-1.example.com",
port: 22,
keyType: "ssh-ed25519",
publicKey: `ssh-ed25519 ${rawKey.toString("base64")}`,
fingerprint: fp,
discoveredAt: 1,
};
assert.equal(normalizeKnownHost(stored), stored);
});
test("normalizeKnownHost is a no-op when publicKey is opaque and nothing else is known", () => {
const stored: KnownHost = {
id: "kh-1",
hostname: "vps-1.example.com",
port: 22,
keyType: "unknown",
publicKey: "SHA256:already-just-a-fingerprint",
discoveredAt: 1,
};
const migrated = normalizeKnownHost(stored);
// The SHA256: prefix becomes the fingerprint; keyType stays as "unknown" since
// we cannot recover it from a bare fingerprint.
assert.equal(migrated.fingerprint, "already-just-a-fingerprint");
assert.equal(migrated.keyType, "unknown");
});
test("normalizeKnownHosts returns the same array reference when nothing needs migration", () => {
const rawKey = makeRawPublicKey("ssh-ed25519");
const fp = crypto.createHash("sha256").update(rawKey).digest("base64").replace(/=+$/g, "");
const list: KnownHost[] = [{
id: "kh-1",
hostname: "vps-1.example.com",
port: 22,
keyType: "ssh-ed25519",
publicKey: `ssh-ed25519 ${rawKey.toString("base64")}`,
fingerprint: fp,
discoveredAt: 1,
}];
assert.equal(normalizeKnownHosts(list), list);
});
test("normalizeKnownHosts migrates each entry that needs backfilling", () => {
const rawKeyA = makeRawPublicKey("ssh-ed25519", "host-a-key");
const rawKeyB = makeRawPublicKey("ssh-rsa", "host-b-key");
const fpA = crypto.createHash("sha256").update(rawKeyA).digest("base64").replace(/=+$/g, "");
const fpB = crypto.createHash("sha256").update(rawKeyB).digest("base64").replace(/=+$/g, "");
const list: KnownHost[] = [
{
id: "kh-1",
hostname: "vps-1.example.com",
port: 22,
keyType: "ssh-ed25519",
publicKey: `ssh-ed25519 ${rawKeyA.toString("base64")}`,
discoveredAt: 1,
},
{
id: "kh-2",
hostname: "vps-2.example.com",
port: 22,
keyType: "",
publicKey: `ssh-rsa ${rawKeyB.toString("base64")}`,
discoveredAt: 2,
},
];
const migrated = normalizeKnownHosts(list);
assert.notEqual(migrated, list);
assert.equal(migrated[0].fingerprint, fpA);
assert.equal(migrated[1].fingerprint, fpB);
assert.equal(migrated[1].keyType, "ssh-rsa");
});