[keyman] thread the configured keys and tmp directories through encrypt/decrypt
encryptKeys and decryptKeys each took `vaultDir` and rebuilt `<vault>/keys` and `<vault>/tmp` from it, so `keysDir` and `tmpDir` in .keymanrc.json were honoured by main and list and silently ignored by the two operations that write. main was also passing vaultRoot where encrypt expected the keys directory, which put encrypted keys one level above where list looks for them: with any config at all, a key encrypted a second ago was invisible. Both now take keysDir and tmpDir explicitly. The decrypt location prompt names the real directories instead of the hardcoded `vault/tmp` and `~/.ssh`, which meant its labels were also its values — hence LOCAL_MODE. tests/vault-layout.test.ts is the regression: encrypt then list, driven through keyman() with only age and the prompts mocked, against a config using keysDir `encrypted` and tmpDir `plain`. Every unit suite passed through this bug because each was told which directory to use; the seam between them was untested. Verified it fails when main is reverted to pass vaultRoot.
This commit is contained in:
@@ -3,7 +3,8 @@ import path from 'node:path';
|
||||
import inquirer from 'inquirer';
|
||||
import { runTool } from './keyman.utils.js';
|
||||
|
||||
const LOCAL_MODE = 'Local (vault/tmp)';
|
||||
/** The two decryption targets. Values, so the label can name the real directory. */
|
||||
const LOCAL_MODE = 'local';
|
||||
|
||||
interface DecryptPlan {
|
||||
key: string;
|
||||
@@ -13,12 +14,13 @@ interface DecryptPlan {
|
||||
publicKeyOut: string;
|
||||
}
|
||||
|
||||
export async function decryptKeys(sshDir: string, vaultDir: string, ageKey: string) {
|
||||
const keyDir = path.join(vaultDir, 'keys');
|
||||
export async function decryptKeys(sshDir: string, keysDir: string, tmpDir: string, ageKey: string) {
|
||||
// Guarded: nothing creates the keys directory until the first encrypt, so on a
|
||||
// fresh vault this readdir threw instead of reporting an empty vault.
|
||||
const vaultKeys = fs.existsSync(keyDir)
|
||||
? fs.readdirSync(keyDir).filter((key) => fs.existsSync(path.join(keyDir, key, `id_${key}.age`)))
|
||||
const vaultKeys = fs.existsSync(keysDir)
|
||||
? fs
|
||||
.readdirSync(keysDir)
|
||||
.filter((key) => fs.existsSync(path.join(keysDir, key, `id_${key}.age`)))
|
||||
: [];
|
||||
|
||||
if (vaultKeys.length === 0) {
|
||||
@@ -37,16 +39,20 @@ export async function decryptKeys(sshDir: string, vaultDir: string, ageKey: stri
|
||||
type: 'list',
|
||||
name: 'decryptMode',
|
||||
message: 'Choose decryption location:',
|
||||
choices: [LOCAL_MODE, 'SSH (~/.ssh)'],
|
||||
// Named after the directories actually in use, which are configurable.
|
||||
choices: [
|
||||
{ name: `Local (${tmpDir})`, value: LOCAL_MODE },
|
||||
{ name: `SSH (${sshDir})`, value: 'ssh' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const outDir = decryptMode === LOCAL_MODE ? path.join(vaultDir, 'tmp') : sshDir;
|
||||
const outDir = decryptMode === LOCAL_MODE ? tmpDir : sshDir;
|
||||
|
||||
const plans: DecryptPlan[] = selectedKeys.map((key: string) => ({
|
||||
key,
|
||||
encryptedKey: path.join(keyDir, key, `id_${key}.age`),
|
||||
publicKey: path.join(keyDir, key, `id_${key}.pub`),
|
||||
encryptedKey: path.join(keysDir, key, `id_${key}.age`),
|
||||
publicKey: path.join(keysDir, key, `id_${key}.pub`),
|
||||
privateKeyOut: path.join(outDir, `id_${key}`),
|
||||
publicKeyOut: path.join(outDir, `id_${key}.pub`),
|
||||
}));
|
||||
|
||||
@@ -16,12 +16,7 @@ function privateKeysIn(dir: string): string[] {
|
||||
return fs.readdirSync(dir).filter((key) => key.startsWith('id_') && !key.endsWith('.pub'));
|
||||
}
|
||||
|
||||
export async function encryptKeys(
|
||||
sshDir: string,
|
||||
vaultDir: string,
|
||||
tmpDir: string,
|
||||
pubkey: string
|
||||
) {
|
||||
export async function encryptKeys(sshDir: string, keysDir: string, tmpDir: string, pubkey: string) {
|
||||
const sshKeys = privateKeysIn(sshDir);
|
||||
const tmpKeys = privateKeysIn(tmpDir);
|
||||
const keys = [...new Set([...sshKeys, ...tmpKeys])];
|
||||
@@ -42,7 +37,7 @@ export async function encryptKeys(
|
||||
|
||||
for (const key of selectedKeys) {
|
||||
const keyPath = path.join(tmpKeys.includes(key) ? tmpDir : sshDir, key);
|
||||
const vaultPath = path.join(vaultDir, 'keys', key.replace('id_', ''));
|
||||
const vaultPath = path.join(keysDir, key.replace('id_', ''));
|
||||
fs.mkdirSync(vaultPath, { recursive: true, mode: 0o700 });
|
||||
|
||||
// Encrypt key using `age`
|
||||
|
||||
@@ -95,12 +95,12 @@ export async function keyman() {
|
||||
case 'encrypt': {
|
||||
const pubkey = await ageRecipient();
|
||||
if (pubkey) {
|
||||
await encryptKeys(sshDir, paths.vaultRoot, paths.tmpDir, pubkey);
|
||||
await encryptKeys(sshDir, paths.keysDir, paths.tmpDir, pubkey);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'decrypt':
|
||||
await decryptKeys(sshDir, paths.vaultRoot, paths.keyPath);
|
||||
await decryptKeys(sshDir, paths.keysDir, paths.tmpDir, paths.keyPath);
|
||||
break;
|
||||
case 'quit':
|
||||
console.log('\n👋 Goodbye!\n');
|
||||
|
||||
Reference in New Issue
Block a user