mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 08:35:57 +00:00
feat: add pure ssh2 remote operations
This commit is contained in:
@@ -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());
|
||||
});
|
||||
Reference in New Issue
Block a user