feat: add pure ssh2 remote operations

This commit is contained in:
云服务部-叶林立
2026-08-21 19:51:43 +08:00
parent d3bf562189
commit 0ac50eb581
62 changed files with 3701 additions and 47 deletions
+81
View File
@@ -0,0 +1,81 @@
import assert from "node:assert/strict";
import { randomBytes } from "node:crypto";
import { chmodSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";
import { mkdtempSync } from "node:fs";
import type { PiSshConfig, VaultPaths } from "../src/config.ts";
import { decryptConfig, encryptConfig, loadVault, rotateVaultKey, saveVault } from "../src/vault.ts";
function fixture(): PiSshConfig {
return {
version: 1,
hosts: {
packaging: {
label: "Packaging",
hostName: "192.0.2.10",
user: "builder",
port: 22,
auth: { type: "password", password: "server-secret", method: "auto" },
hostKey: { algorithm: "ssh-ed25519", fingerprint: "SHA256:fixture" },
},
},
};
}
function paths(): VaultPaths {
const directory = mkdtempSync(join(tmpdir(), "pi-ssh-vault-"));
return {
directory,
encryptedConfig: join(directory, "hosts.enc"),
key: join(directory, "vault.key"),
};
}
test("encrypts and authenticates the complete configuration", () => {
const key = randomBytes(32);
const encrypted = encryptConfig(fixture(), key);
assert.doesNotMatch(encrypted, /server-secret|192\.0\.2\.10|builder/);
assert.deepEqual(decryptConfig(encrypted, key), fixture());
const envelope = JSON.parse(encrypted);
envelope.ciphertext = `${envelope.ciphertext.slice(0, -2)}AA`;
assert.throws(() => decryptConfig(JSON.stringify(envelope), key), /integrity verification/);
assert.throws(() => decryptConfig(encrypted, randomBytes(32)), /integrity verification/);
});
test("writes an owner-only key and encrypted config without plaintext", () => {
const target = paths();
saveVault(fixture(), target);
assert.deepEqual(loadVault(target), fixture());
assert.doesNotMatch(readFileSync(target.encryptedConfig, "utf8"), /server-secret/);
assert.equal(Buffer.from(readFileSync(target.key, "utf8").trim(), "base64").length, 32);
if (process.platform !== "win32") {
assert.equal(statSync(target.directory).mode & 0o777, 0o700);
assert.equal(statSync(target.encryptedConfig).mode & 0o777, 0o600);
assert.equal(statSync(target.key).mode & 0o777, 0o600);
}
});
test("fails closed when vault files are missing or too broadly readable", () => {
const target = paths();
assert.throws(() => loadVault(target), /not configured/);
mkdirSync(target.directory, { recursive: true });
writeFileSync(target.key, `${randomBytes(32).toString("base64")}\n`, { mode: 0o600 });
assert.throws(() => loadVault(target), /not configured/);
saveVault(fixture(), target);
if (process.platform !== "win32") {
chmodSync(target.encryptedConfig, 0o644);
assert.throws(() => loadVault(target), /permissions/);
}
});
test("rotates the adjacent key while preserving the encrypted configuration", () => {
const target = paths();
saveVault(fixture(), target);
const oldKey = readFileSync(target.key, "utf8");
rotateVaultKey(target);
assert.notEqual(readFileSync(target.key, "utf8"), oldKey);
assert.deepEqual(loadVault(target), fixture());
});