inquirer v10 removed the legacy 'list' prompt in favour of 'select', so every list-style prompt — starting with the main menu — died with "Prompt type \"list\" is not registered" on any real run against the declared ^14 dependency. The tests never saw it because they all mock inquirer.prompt, which accepts any type string. Choice shapes and the 'default' option are unchanged; 'select' takes them as-is. Bump to 0.7.2 to ship the fix. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ce5atB2tXDXyz2jd9s1bqE
109 lines
3.4 KiB
TypeScript
109 lines
3.4 KiB
TypeScript
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import inquirer from 'inquirer';
|
|
import { runTool } from './keyman.utils.js';
|
|
import { listVaultKeys } from './keyman.vault.js';
|
|
|
|
/** The two decryption targets. Values, so the label can name the real directory. */
|
|
const LOCAL_MODE = 'local';
|
|
|
|
interface DecryptPlan {
|
|
key: string;
|
|
encryptedKey: string;
|
|
publicKey: string;
|
|
privateKeyOut: string;
|
|
publicKeyOut: string;
|
|
}
|
|
|
|
export async function decryptKeys(sshDir: string, keysDir: string, tmpDir: string, ageKey: string) {
|
|
const vaultKeys = listVaultKeys(keysDir);
|
|
|
|
if (vaultKeys.length === 0) {
|
|
console.log('⚠️ No encrypted keys found.');
|
|
return;
|
|
}
|
|
|
|
const { selectedKeys, decryptMode } = await inquirer.prompt([
|
|
{
|
|
type: 'checkbox',
|
|
name: 'selectedKeys',
|
|
message: 'Select keys to decrypt:',
|
|
choices: vaultKeys,
|
|
},
|
|
{
|
|
type: 'select',
|
|
name: 'decryptMode',
|
|
message: 'Choose decryption location:',
|
|
// 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 ? tmpDir : sshDir;
|
|
|
|
const plans: DecryptPlan[] = selectedKeys.map((key: string) => ({
|
|
key,
|
|
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`),
|
|
}));
|
|
|
|
// Every collision is settled before anything is written. `age -d -o` and the
|
|
// old `cp` both overwrote silently, so decrypting a vault key on top of a
|
|
// newer working key destroyed it with no prompt and no copy — and the user is
|
|
// answering these questions about files that still exist.
|
|
const approved: DecryptPlan[] = [];
|
|
for (const plan of plans) {
|
|
const existing = [plan.privateKeyOut, plan.publicKeyOut].filter((file) => fs.existsSync(file));
|
|
|
|
if (existing.length === 0) {
|
|
approved.push(plan);
|
|
continue;
|
|
}
|
|
|
|
const { overwrite } = await inquirer.prompt<{ overwrite: boolean }>([
|
|
{
|
|
type: 'confirm',
|
|
name: 'overwrite',
|
|
message: `${existing.join(', ')} already present. Overwrite?`,
|
|
default: false,
|
|
},
|
|
]);
|
|
|
|
if (overwrite) {
|
|
approved.push(plan);
|
|
} else {
|
|
console.log(`⏭️ Skipped ${plan.key} — kept what was already there.`);
|
|
}
|
|
}
|
|
|
|
if (approved.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// 0700: ~/.ssh may not exist yet, and it is about to hold a private key.
|
|
fs.mkdirSync(outDir, { recursive: true, mode: 0o700 });
|
|
|
|
for (const plan of approved) {
|
|
await runTool('age', ['-d', '-i', ageKey, '-o', plan.privateKeyOut, plan.encryptedKey]);
|
|
// Immediately, and in-process: age creates its output 0644 regardless of
|
|
// umask, so this used to be a world-readable private key for the length of
|
|
// two process spawns — and stayed 0644 whenever the chmod itself failed.
|
|
fs.chmodSync(plan.privateKeyOut, 0o600);
|
|
|
|
if (fs.existsSync(plan.publicKey)) {
|
|
fs.copyFileSync(plan.publicKey, plan.publicKeyOut);
|
|
} else {
|
|
console.log(
|
|
`⚠️ ${plan.key} has no public key in the vault; only the private key was written.`
|
|
);
|
|
}
|
|
|
|
console.log(`✅ Decrypted: ${plan.privateKeyOut}`);
|
|
}
|
|
}
|