mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-08-28 07:23:06 +00:00
82 lines
3.1 KiB
TypeScript
82 lines
3.1 KiB
TypeScript
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());
|
|
});
|