mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-30 07:05:24 +00:00
fix: unlock passphrase keys once per connect burst (#1737)
This commit is contained in:
1 parent
27525d8f72
commit
d715d097c5
3 files changed
+77
-2
No files matched your search
@@ -11,6 +11,7 @@ import {
|
||||
getFriendlyKeyTypeName,
|
||||
validateKeyPair,
|
||||
isSecurityKeyFile,
|
||||
clearUnlockedKeyCache,
|
||||
} from "../../utils/ssh-key-utils.js";
|
||||
|
||||
const { parsePrivateKey } = sshpk;
|
||||
@@ -226,6 +227,32 @@ describe("PKCS#8 runtime key normalization", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("encrypted OpenSSH keys", () => {
|
||||
const key = sshpk.generatePrivateKey("ed25519");
|
||||
const encrypted = key.toString("openssh", { passphrase: "pw" }).trim();
|
||||
|
||||
it("unlocks once and hands ssh2 a key that needs no passphrase", () => {
|
||||
clearUnlockedKeyCache();
|
||||
const first = preparePrivateKeyForSSH2(encrypted, "pw");
|
||||
const parsed = ssh2.utils.parseKey(first);
|
||||
expect(parsed).not.toBeInstanceOf(Error);
|
||||
expect(parsePrivateKey(first.toString()).fingerprint().toString()).toBe(
|
||||
key.fingerprint().toString(),
|
||||
);
|
||||
expect(preparePrivateKeyForSSH2(encrypted, "pw").toString()).toBe(
|
||||
first.toString(),
|
||||
);
|
||||
});
|
||||
|
||||
it("leaves the key locked for a wrong or missing passphrase", () => {
|
||||
clearUnlockedKeyCache();
|
||||
expect(preparePrivateKeyForSSH2(encrypted, "wrong").toString()).toBe(
|
||||
encrypted,
|
||||
);
|
||||
expect(preparePrivateKeyForSSH2(encrypted).toString()).toBe(encrypted);
|
||||
});
|
||||
});
|
||||
|
||||
describe("security key files", () => {
|
||||
const str = (v: string | Buffer) => {
|
||||
const b = Buffer.from(v);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createPrivateKey } from "node:crypto";
|
||||
import { createHash, createPrivateKey } from "node:crypto";
|
||||
import sshpk from "sshpk";
|
||||
import { getErrorMessage } from "./error-message.js";
|
||||
import ssh2Pkg from "ssh2";
|
||||
@@ -264,12 +264,59 @@ export function isSecurityKeyFile(privateKeyData: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
// Unlocking an OpenSSH key runs bcrypt on the main thread (about a second per
|
||||
// try), and every connect used to do it 3 or more times. Keep the unlocked copy
|
||||
// in memory for a short while so a burst of connects pays for it once.
|
||||
const UNLOCKED_KEY_TTL_MS = 15 * 60 * 1000;
|
||||
const UNLOCKED_KEY_MAX = 64;
|
||||
const unlockedKeys = new Map<string, { text: string; expires: number }>();
|
||||
|
||||
function unlockOpenSSHKey(cleanKey: string, passphrase?: string): string {
|
||||
if (
|
||||
!passphrase ||
|
||||
!cleanKey.startsWith("-----BEGIN OPENSSH PRIVATE KEY-----")
|
||||
)
|
||||
return cleanKey;
|
||||
const id = createHash("sha256")
|
||||
.update(cleanKey)
|
||||
.update("\n")
|
||||
.update(passphrase)
|
||||
.digest("hex");
|
||||
const now = Date.now();
|
||||
const hit = unlockedKeys.get(id);
|
||||
if (hit && hit.expires > now) return hit.text;
|
||||
unlockedKeys.delete(id);
|
||||
let text: string;
|
||||
try {
|
||||
text = parsePrivateKey(cleanKey, "openssh", { passphrase }).toString(
|
||||
"openssh",
|
||||
);
|
||||
} catch {
|
||||
// Wrong passphrase or a cipher sshpk lacks; the normal path reports it.
|
||||
return cleanKey;
|
||||
}
|
||||
if (ssh2Utils.parseKey(text) instanceof Error) return cleanKey;
|
||||
if (unlockedKeys.size >= UNLOCKED_KEY_MAX) {
|
||||
const oldest = unlockedKeys.keys().next().value;
|
||||
if (oldest !== undefined) unlockedKeys.delete(oldest);
|
||||
}
|
||||
unlockedKeys.set(id, { text, expires: now + UNLOCKED_KEY_TTL_MS });
|
||||
return text;
|
||||
}
|
||||
|
||||
export function clearUnlockedKeyCache(): void {
|
||||
unlockedKeys.clear();
|
||||
}
|
||||
|
||||
export function preparePrivateKeyForSSH2(
|
||||
privateKeyData: string,
|
||||
passphrase?: string,
|
||||
): Buffer {
|
||||
if (isSecurityKeyFile(privateKeyData)) throw new Error(SECURITY_KEY_ERROR);
|
||||
let cleanKey = normalizePrivateKeyText(privateKeyData);
|
||||
let cleanKey = unlockOpenSSHKey(
|
||||
normalizePrivateKeyText(privateKeyData),
|
||||
passphrase,
|
||||
);
|
||||
if (
|
||||
/^-----BEGIN (?:ENCRYPTED )?PRIVATE KEY-----/.test(cleanKey) &&
|
||||
ssh2Utils.parseKey(cleanKey, passphrase) instanceof Error
|
||||
|
||||
Reference in new issue
Block a user