[Init] Initial commit - NetMesh terminal manager
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
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
This commit is contained in:
281
electron/bridges/sshAuthHelper.passwordOnly.test.cjs
Normal file
281
electron/bridges/sshAuthHelper.passwordOnly.test.cjs
Normal file
@@ -0,0 +1,281 @@
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* Password-only auth must not probe default ~/.ssh keys (issues #266 / #2079).
|
||||
* Jump hosts and SFTP share buildAuthHandler; a wrong host password used to
|
||||
* look fine on direct terminal (startSession still fell back to id_rsa) while
|
||||
* ProxyJump / SFTP failed after password rejection alone.
|
||||
*/
|
||||
|
||||
const test = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const { buildAuthHandler } = require("./sshAuthHelper.cjs");
|
||||
|
||||
const DEFAULT_KEYS = [
|
||||
{
|
||||
keyName: "id_ed25519",
|
||||
keyPath: "/tmp/id_ed25519",
|
||||
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\ned\n-----END OPENSSH PRIVATE KEY-----\n",
|
||||
},
|
||||
{
|
||||
keyName: "id_rsa",
|
||||
keyPath: "/tmp/id_rsa",
|
||||
privateKey: "-----BEGIN RSA PRIVATE KEY-----\nrsa\n-----END RSA PRIVATE KEY-----\n",
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Walk the ssh2-style authHandler and collect method labels.
|
||||
* Object methods expose type (and sometimes username/key); string methods
|
||||
* come from the simple ordered-list path (e.g. password-only).
|
||||
*/
|
||||
function collectAuthMethods(authHandler, maxSteps = 16) {
|
||||
const labels = [];
|
||||
let methodsLeft = null;
|
||||
for (let i = 0; i < maxSteps; i += 1) {
|
||||
let offered = null;
|
||||
authHandler(methodsLeft, false, (method) => {
|
||||
offered = method;
|
||||
});
|
||||
if (offered == null || offered === false) break;
|
||||
if (typeof offered === "string") {
|
||||
labels.push(offered);
|
||||
} else if (offered && typeof offered === "object") {
|
||||
labels.push(offered.type || "unknown");
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
// Keep all common methods available so the handler can walk its full list.
|
||||
methodsLeft = ["publickey", "password", "keyboard-interactive", "agent"];
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
test("buildAuthHandler password-only does not offer default SSH keys (#2079)", () => {
|
||||
const auth = buildAuthHandler({
|
||||
password: "wrong-or-stale-secret",
|
||||
username: "root",
|
||||
defaultKeys: DEFAULT_KEYS,
|
||||
allowAgentFallback: false,
|
||||
});
|
||||
|
||||
const labels = collectAuthMethods(auth.authHandler);
|
||||
assert.ok(labels.includes("password"), `expected password; got ${labels.join(",")}`);
|
||||
assert.ok(labels.includes("keyboard-interactive"), `expected KI; got ${labels.join(",")}`);
|
||||
assert.equal(
|
||||
labels.includes("publickey"),
|
||||
false,
|
||||
`password-only must not probe default keys; offered=${labels.join(",")}`,
|
||||
);
|
||||
assert.equal(labels.includes("agent"), false);
|
||||
});
|
||||
|
||||
test("buildAuthHandler password-only still fires onAuthAttempt for jump/SFTP progress", () => {
|
||||
const attempts = [];
|
||||
const auth = buildAuthHandler({
|
||||
password: "secret",
|
||||
username: "root",
|
||||
// Default keys on disk used to force the dynamic path for progress only;
|
||||
// after #2079 the simple ordered path must still report attempts.
|
||||
defaultKeys: DEFAULT_KEYS,
|
||||
allowAgentFallback: false,
|
||||
onAuthAttempt: (label) => attempts.push(label),
|
||||
});
|
||||
|
||||
collectAuthMethods(auth.authHandler);
|
||||
assert.ok(
|
||||
attempts.some((label) => label === "password" || label.includes("password")),
|
||||
`expected password attempt callback; got ${attempts.join(" | ")}`,
|
||||
);
|
||||
assert.ok(
|
||||
attempts.some((label) => label.includes("keyboard-interactive") || label.includes("exhausted")),
|
||||
`expected KI or exhaustion callback; got ${attempts.join(" | ")}`,
|
||||
);
|
||||
assert.equal(
|
||||
attempts.some((label) => /id_rsa|id_ed25519|default/.test(label)),
|
||||
false,
|
||||
`must not report default-key probes; got ${attempts.join(" | ")}`,
|
||||
);
|
||||
});
|
||||
|
||||
test("buildAuthHandler automatic mode tries agent and default keys before password", () => {
|
||||
const auth = buildAuthHandler({
|
||||
authMethod: "auto",
|
||||
password: "saved-secret",
|
||||
username: "root",
|
||||
defaultKeys: DEFAULT_KEYS,
|
||||
sshAgentSocketOverride: "/tmp/ssh-agent.sock",
|
||||
});
|
||||
|
||||
const labels = collectAuthMethods(auth.authHandler);
|
||||
const agentIndex = labels.indexOf("agent");
|
||||
const keyIndex = labels.indexOf("publickey");
|
||||
const passwordIndex = labels.indexOf("password");
|
||||
assert.ok(agentIndex >= 0, `expected agent; got ${labels.join(",")}`);
|
||||
assert.ok(keyIndex >= 0, `expected publickey; got ${labels.join(",")}`);
|
||||
assert.ok(passwordIndex >= 0, `expected password; got ${labels.join(",")}`);
|
||||
assert.ok(agentIndex < passwordIndex, `agent should precede password; got ${labels.join(",")}`);
|
||||
assert.ok(keyIndex < passwordIndex, `default key should precede password; got ${labels.join(",")}`);
|
||||
assert.ok(
|
||||
labels.every((label, index) => label !== "publickey" || index < passwordIndex),
|
||||
`all default keys should precede password; got ${labels.join(",")}`,
|
||||
);
|
||||
});
|
||||
|
||||
test("buildAuthHandler automatic mode skips an unavailable validated agent", () => {
|
||||
const auth = buildAuthHandler({
|
||||
authMethod: "auto",
|
||||
password: "saved-secret",
|
||||
username: "root",
|
||||
defaultKeys: DEFAULT_KEYS,
|
||||
sshAgentSocketOverride: null,
|
||||
});
|
||||
|
||||
const labels = collectAuthMethods(auth.authHandler);
|
||||
assert.equal(labels.includes("agent"), false, labels.join(","));
|
||||
assert.equal(labels.includes("publickey"), true, labels.join(","));
|
||||
assert.equal(labels.includes("password"), true, labels.join(","));
|
||||
});
|
||||
|
||||
test("buildAuthHandler with no credentials still offers default keys", () => {
|
||||
const auth = buildAuthHandler({
|
||||
username: "root",
|
||||
defaultKeys: DEFAULT_KEYS,
|
||||
allowAgentFallback: false,
|
||||
});
|
||||
|
||||
const labels = collectAuthMethods(auth.authHandler);
|
||||
assert.ok(
|
||||
labels.includes("publickey"),
|
||||
`expected default-key fallback when no explicit auth; offered=${labels.join(",")}`,
|
||||
);
|
||||
});
|
||||
|
||||
test("buildAuthHandler key+password still allows default key fallback after user key", () => {
|
||||
const auth = buildAuthHandler({
|
||||
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nuser\n-----END OPENSSH PRIVATE KEY-----\n",
|
||||
password: "also-have-password",
|
||||
username: "root",
|
||||
defaultKeys: DEFAULT_KEYS,
|
||||
allowAgentFallback: false,
|
||||
});
|
||||
|
||||
const labels = collectAuthMethods(auth.authHandler);
|
||||
assert.ok(labels.includes("publickey"), `expected publickey; offered=${labels.join(",")}`);
|
||||
assert.ok(labels.includes("password"), `expected password; offered=${labels.join(",")}`);
|
||||
// user key + at least one default key => multiple publickey offers
|
||||
const publickeyCount = labels.filter((l) => l === "publickey").length;
|
||||
assert.ok(
|
||||
publickeyCount >= 2,
|
||||
`key auth may still fall back to default keys; offered=${labels.join(",")}`,
|
||||
);
|
||||
});
|
||||
|
||||
test("buildAuthHandler explicit key mode uses only the selected key and stated password fallback", () => {
|
||||
const auth = buildAuthHandler({
|
||||
authMethod: "key",
|
||||
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nuser\n-----END OPENSSH PRIVATE KEY-----\n",
|
||||
password: "fallback-password",
|
||||
username: "root",
|
||||
defaultKeys: DEFAULT_KEYS,
|
||||
allowAgentFallback: true,
|
||||
});
|
||||
|
||||
const labels = collectAuthMethods(auth.authHandler);
|
||||
assert.equal(labels.filter((label) => label === "publickey").length, 1, labels.join(","));
|
||||
assert.equal(labels.includes("agent"), false, labels.join(","));
|
||||
assert.equal(labels.includes("password"), true, labels.join(","));
|
||||
});
|
||||
|
||||
test("buildAuthHandler explicit key mode never substitutes an unrelated default key", () => {
|
||||
const auth = buildAuthHandler({
|
||||
authMethod: "key",
|
||||
username: "root",
|
||||
defaultKeys: DEFAULT_KEYS,
|
||||
allowAgentFallback: true,
|
||||
});
|
||||
|
||||
const labels = collectAuthMethods(auth.authHandler);
|
||||
assert.equal(labels.includes("publickey"), false, labels.join(","));
|
||||
assert.equal(labels.includes("agent"), false, labels.join(","));
|
||||
assert.equal(auth.privateKey, null);
|
||||
});
|
||||
|
||||
test("buildAuthHandler explicit password mode ignores an agent and default keys", () => {
|
||||
const auth = buildAuthHandler({
|
||||
authMethod: "password",
|
||||
password: "host-password",
|
||||
agent: "/tmp/agent.sock",
|
||||
username: "root",
|
||||
defaultKeys: DEFAULT_KEYS,
|
||||
allowAgentFallback: true,
|
||||
});
|
||||
|
||||
const labels = collectAuthMethods(auth.authHandler);
|
||||
assert.equal(labels.includes("agent"), false, labels.join(","));
|
||||
assert.equal(labels.includes("publickey"), false, labels.join(","));
|
||||
assert.equal(labels.includes("password"), true, labels.join(","));
|
||||
});
|
||||
|
||||
test("buildAuthHandler explicit password mode never submits an empty saved password", () => {
|
||||
const auth = buildAuthHandler({
|
||||
authMethod: "password",
|
||||
password: undefined,
|
||||
agent: "/tmp/agent.sock",
|
||||
username: "root",
|
||||
defaultKeys: DEFAULT_KEYS,
|
||||
allowAgentFallback: true,
|
||||
});
|
||||
|
||||
const labels = collectAuthMethods(auth.authHandler);
|
||||
assert.equal(labels.includes("password"), false, labels.join(","));
|
||||
assert.equal(labels.includes("agent"), false, labels.join(","));
|
||||
assert.equal(labels.includes("publickey"), false, labels.join(","));
|
||||
assert.equal(labels.includes("keyboard-interactive"), true, labels.join(","));
|
||||
});
|
||||
|
||||
for (const authMethod of ["password", "key", "certificate"]) {
|
||||
test(`buildAuthHandler explicit ${authMethod} mode ignores unlocked default keys`, () => {
|
||||
const auth = buildAuthHandler({
|
||||
authMethod,
|
||||
password: "fallback-password",
|
||||
privateKey: authMethod === "password" ? undefined : "-----BEGIN OPENSSH PRIVATE KEY-----\nselected\n-----END OPENSSH PRIVATE KEY-----\n",
|
||||
username: "root",
|
||||
allowAgentFallback: false,
|
||||
unlockedEncryptedKeys: [{
|
||||
keyName: "id_unrelated",
|
||||
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nunrelated\n-----END OPENSSH PRIVATE KEY-----\n",
|
||||
passphrase: "key-pass",
|
||||
}],
|
||||
});
|
||||
|
||||
const labels = collectAuthMethods(auth.authHandler);
|
||||
assert.equal(
|
||||
labels.filter((label) => label === "publickey").length,
|
||||
authMethod === "password" ? 0 : 1,
|
||||
labels.join(","),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
test("buildAuthHandler password-only may still attach unlocked encrypted keys for jump retry", () => {
|
||||
const auth = buildAuthHandler({
|
||||
password: "host-password",
|
||||
username: "root",
|
||||
defaultKeys: DEFAULT_KEYS,
|
||||
allowAgentFallback: false,
|
||||
unlockedEncryptedKeys: [{
|
||||
keyName: "id_encrypted",
|
||||
privateKey: "-----BEGIN OPENSSH PRIVATE KEY-----\nenc\n-----END OPENSSH PRIVATE KEY-----\n",
|
||||
passphrase: "key-pass",
|
||||
}],
|
||||
});
|
||||
|
||||
const labels = collectAuthMethods(auth.authHandler);
|
||||
assert.ok(labels.includes("password"), `offered=${labels.join(",")}`);
|
||||
assert.ok(
|
||||
labels.includes("publickey"),
|
||||
`jump-chain retry may still offer unlocked keys; offered=${labels.join(",")}`,
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user