Files
Netcatty/domain/sshAuth.ts
bincxz 4b07b4826a fix(terminal): resolve sudo autofill password through identity references (#1284)
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>
2026-06-07 13:21:54 +08:00

141 lines
3.8 KiB
TypeScript

import type { Host, Identity, SSHKey } from "./models";
import { sanitizeCredentialValue } from "./credentials";
type HostAuthMethod = "password" | "key" | "certificate";
type HostAuthOverride = {
authMethod?: HostAuthMethod;
username?: string;
password?: string;
keyId?: string;
passphrase?: string;
};
type ResolvedHostAuth = {
identity?: Identity;
authMethod: HostAuthMethod;
username: string;
password?: string;
keyId?: string;
key?: SSHKey;
passphrase?: string;
identityFilePath?: string;
};
const inferAuthMethod = (opts: {
explicit?: HostAuthMethod;
keyId?: string;
password?: string;
hostAuthMethod?: HostAuthMethod;
key?: SSHKey;
}): HostAuthMethod => {
if (opts.explicit) return opts.explicit;
if (opts.keyId) {
if (opts.hostAuthMethod === "key" || opts.hostAuthMethod === "certificate") {
return opts.hostAuthMethod;
}
return opts.key?.certificate ? "certificate" : "key";
}
if (opts.hostAuthMethod) return opts.hostAuthMethod;
if (opts.password) return "password";
return "password";
};
export const resolveHostAuth = (args: {
host: Host;
keys: SSHKey[];
identities?: Identity[];
override?: HostAuthOverride | null;
}): ResolvedHostAuth => {
const { host, keys, identities = [], override } = args;
const identity = host.identityId
? identities.find((i) => i.id === host.identityId)
: undefined;
const username =
override?.username?.trim() ||
identity?.username?.trim() ||
host.username?.trim() ||
"";
const selectedAuthMethod = (
override?.authMethod ||
identity?.authMethod ||
host.authMethod
) as HostAuthMethod | undefined;
// Don't load key when password auth is selected.
// This ensures the user's auth method selection is strictly respected.
const keyId = selectedAuthMethod === "password"
? undefined
: (override?.keyId || identity?.keyId || host.identityFileId || undefined);
const key = keyId ? keys.find((k) => k.id === keyId) : undefined;
const password = override?.password ?? identity?.password ?? host.password;
const authMethod = inferAuthMethod({
explicit: override?.authMethod,
hostAuthMethod: (identity?.authMethod || host.authMethod) as HostAuthMethod | undefined,
keyId,
password,
key,
});
const passphrase = override?.passphrase || key?.passphrase || undefined;
const identityFilePath = key?.source === 'reference' && key.filePath
? key.filePath
: undefined;
return {
identity,
authMethod,
username,
password,
keyId,
key,
passphrase,
identityFilePath,
};
};
/**
* Resolve the password to use for sudo autofill the same way SSH login does
* (through resolveHostAuth), so a password stored in a referenced Keychain
* identity (host.identityId) is found — not just host.password (issue #1284).
* Returns undefined when the host opts out of saving its password, or none is
* available (pure key auth, or an undecryptable placeholder).
*/
export const resolveHostAutofillPassword = (args: {
host: Host;
keys: SSHKey[];
identities?: Identity[];
}): string | undefined => {
if (args.host.savePassword === false) return undefined;
return sanitizeCredentialValue(resolveHostAuth(args).password) || undefined;
};
export const resolveBridgeKeyAuth = (args: {
key?: SSHKey | null;
fallbackIdentityFilePaths?: string[];
passphrase?: string;
}): {
privateKey?: string;
identityFilePaths?: string[];
passphrase?: string;
} => {
const { key, fallbackIdentityFilePaths, passphrase } = args;
const identityFilePaths = key?.source === "reference" && key.filePath
? [key.filePath]
: fallbackIdentityFilePaths;
return {
privateKey: key?.source === "reference" ? undefined : sanitizeCredentialValue(key?.privateKey),
identityFilePaths,
passphrase: sanitizeCredentialValue(passphrase ?? key?.passphrase),
};
};