Sudo autofill only read host.password and never resolved a host's reference to a Keychain identity (host.identityId). When the account password lived in a referenced identity, the autofill got nothing — while SSH login worked because it goes through resolveHostAuth, which resolves the identity. Add domain resolveHostAutofillPassword (same resolveHostAuth resolution: identity.password ?? host.password, honoring savePassword and dropping undecryptable placeholders) and use it as the terminal autofill password source. Login and autofill now share one resolution path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
156 lines
4.0 KiB
TypeScript
156 lines
4.0 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { resolveBridgeKeyAuth, resolveHostAuth, resolveHostAutofillPassword } from "./sshAuth.ts";
|
|
import type { Host, Identity, SSHKey } from "./models.ts";
|
|
|
|
const referenceKey: SSHKey = {
|
|
id: "key-1",
|
|
label: "Reference key",
|
|
type: "ED25519",
|
|
privateKey: "",
|
|
source: "reference",
|
|
category: "key",
|
|
created: 1,
|
|
filePath: "/Users/alice/.ssh/id_ed25519",
|
|
};
|
|
|
|
test("resolveBridgeKeyAuth passes reference keys as identity file paths", () => {
|
|
assert.deepEqual(
|
|
resolveBridgeKeyAuth({
|
|
key: referenceKey,
|
|
fallbackIdentityFilePaths: ["/legacy/key"],
|
|
passphrase: "saved-passphrase",
|
|
}),
|
|
{
|
|
privateKey: undefined,
|
|
identityFilePaths: ["/Users/alice/.ssh/id_ed25519"],
|
|
passphrase: "saved-passphrase",
|
|
},
|
|
);
|
|
});
|
|
|
|
test("resolveBridgeKeyAuth ignores undecryptable passphrase placeholders", () => {
|
|
assert.equal(
|
|
resolveBridgeKeyAuth({
|
|
key: {
|
|
...referenceKey,
|
|
passphrase: "enc:v1:djEwAAAA",
|
|
},
|
|
}).passphrase,
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
test("resolveBridgeKeyAuth ignores undecryptable private key placeholders", () => {
|
|
assert.equal(
|
|
resolveBridgeKeyAuth({
|
|
key: {
|
|
...referenceKey,
|
|
source: "imported",
|
|
filePath: undefined,
|
|
privateKey: "enc:v1:djEwAAAA",
|
|
},
|
|
}).privateKey,
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
test("resolveBridgeKeyAuth preserves imported key material", () => {
|
|
const importedKey: SSHKey = {
|
|
...referenceKey,
|
|
source: "imported",
|
|
privateKey: "PRIVATE KEY",
|
|
filePath: undefined,
|
|
};
|
|
|
|
assert.deepEqual(
|
|
resolveBridgeKeyAuth({
|
|
key: importedKey,
|
|
fallbackIdentityFilePaths: ["/legacy/key"],
|
|
}),
|
|
{
|
|
privateKey: "PRIVATE KEY",
|
|
identityFilePaths: ["/legacy/key"],
|
|
passphrase: undefined,
|
|
},
|
|
);
|
|
});
|
|
|
|
test("resolveHostAuth respects password auth over stale key selections", () => {
|
|
const host: Host = {
|
|
id: "host-1",
|
|
label: "Host",
|
|
hostname: "example.com",
|
|
username: "root",
|
|
authMethod: "password",
|
|
identityFileId: "key-1",
|
|
};
|
|
|
|
const resolved = resolveHostAuth({
|
|
host,
|
|
keys: [referenceKey],
|
|
identities: [],
|
|
});
|
|
|
|
assert.equal(resolved.authMethod, "password");
|
|
assert.equal(resolved.key, undefined);
|
|
assert.equal(resolved.keyId, undefined);
|
|
});
|
|
|
|
const autofillBaseHost = {
|
|
id: "h1",
|
|
label: "Host",
|
|
hostname: "h.example.test",
|
|
username: "alice",
|
|
} as Host;
|
|
|
|
test("resolveHostAutofillPassword uses the host's own saved password", () => {
|
|
assert.equal(
|
|
resolveHostAutofillPassword({ host: { ...autofillBaseHost, password: "direct-secret" }, keys: [] }),
|
|
"direct-secret",
|
|
);
|
|
});
|
|
|
|
test("resolveHostAutofillPassword resolves a referenced keychain identity's password", () => {
|
|
// host stores no password of its own; the credential lives in a Keychain
|
|
// identity it references (host.identityId) — the #1284 scenario.
|
|
const identity = {
|
|
id: "id-1",
|
|
label: "alice@prod",
|
|
username: "alice",
|
|
authMethod: "password",
|
|
password: "identity-secret",
|
|
created: 1,
|
|
} as Identity;
|
|
assert.equal(
|
|
resolveHostAutofillPassword({
|
|
host: { ...autofillBaseHost, password: undefined, identityId: "id-1" },
|
|
keys: [],
|
|
identities: [identity],
|
|
}),
|
|
"identity-secret",
|
|
);
|
|
});
|
|
|
|
test("resolveHostAutofillPassword returns undefined when the host opts out of saving", () => {
|
|
assert.equal(
|
|
resolveHostAutofillPassword({ host: { ...autofillBaseHost, password: "x", savePassword: false }, keys: [] }),
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
test("resolveHostAutofillPassword returns undefined when no password is available", () => {
|
|
assert.equal(
|
|
resolveHostAutofillPassword({ host: { ...autofillBaseHost, password: undefined }, keys: [] }),
|
|
undefined,
|
|
);
|
|
});
|
|
|
|
test("resolveHostAutofillPassword ignores undecryptable password placeholders", () => {
|
|
assert.equal(
|
|
resolveHostAutofillPassword({ host: { ...autofillBaseHost, password: "enc:v1:djEwAAAA" }, keys: [] }),
|
|
undefined,
|
|
);
|
|
});
|