import fs from 'node:fs'; import path from 'node:path'; import inquirer from 'inquirer'; import { clearDecryptedKeys, writeVaultGitignore } from './keyman.clear.js'; import { loadConfig, resolveConfigPaths } from './keyman.config.js'; import { copyKey } from './keyman.copy.js'; import { decryptKeys } from './keyman.decrypt.js'; import { encryptKeys } from './keyman.encrypt.js'; import { generateKey } from './keyman.generate.js'; import { CURRENT_USER, resolveHomeDir } from './keyman.home.js'; import { listKeys } from './keyman.list.js'; import { extractAgePublicKey } from './keyman.utils.js'; // ๐Ÿ”น Main function to resolve paths and manage flow export async function keyman() { // Load configuration from .keymanrc.json or use defaults const config = loadConfig(); const paths = resolveConfigPaths(config); console.log(`\n๐Ÿ“ Vault Root: ${paths.vaultRoot}`); console.log(`๐Ÿ”‘ Keys Directory: ${paths.keysDir}`); console.log(`๐Ÿ“‚ Temp Directory: ${paths.tmpDir}`); console.log(`๐Ÿ” Age Key: ${paths.keyPath}\n`); // Get USER input const { user } = await inquirer.prompt<{ user: string }>([ { type: 'input', name: 'user', message: `Specify USER (default: ${CURRENT_USER}):`, default: CURRENT_USER, }, ]); const homeDir = resolveHomeDir(user); if (!homeDir) { process.exit(1); } const sshDir = path.join(homeDir, '.ssh'); // 0700 because the vault holds the age identity and, in tmp, plaintext private // keys. keysDir is created here too: decrypt used to read it before anything // created it. for (const dir of [paths.vaultRoot, paths.keysDir, paths.tmpDir]) { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); } writeVaultGitignore(paths.vaultRoot, paths.tmpDir, paths.keyPath); // Resolved on demand, because only generate and encrypt need a recipient, and // remembered once it succeeds. Retried while it has not: creating the identity // mid-session should not mean restarting. let recipient: string | null = null; const ageRecipient = async () => { recipient ??= await extractAgePublicKey(paths.keyPath); if (!recipient) { console.error(` Create one with: age-keygen -o ${paths.keyPath}`); } return recipient; }; // Main loop - keep showing menu until user quits let running = true; while (running) { console.log(`\n${'='.repeat(50)}`); // ๐Ÿ”น Show category selection const { category } = await inquirer.prompt<{ category: string }>([ { type: 'list', name: 'category', message: 'Select operation:', choices: [ { name: '๐Ÿ“‹ List keys', value: 'list' }, { name: '๐Ÿ“ Copy public key', value: 'copy' }, { name: '๐Ÿ†• Generate key', value: 'generate' }, { name: '๐Ÿ”’ Encrypt keys', value: 'encrypt' }, { name: '๐Ÿ”“ Decrypt keys', value: 'decrypt' }, { name: '๐Ÿงน Clear decrypted keys', value: 'clear' }, { name: 'โŒ Quit', value: 'quit' }, ], }, ]); switch (category) { case 'list': await listKeys(sshDir, paths.keysDir, paths.tmpDir); break; case 'copy': await copyKey(sshDir, paths.tmpDir); break; case 'generate': { const pubkey = await ageRecipient(); if (pubkey) { await generateKey(paths.tmpDir, paths.keysDir, pubkey); } break; } case 'encrypt': { const pubkey = await ageRecipient(); if (pubkey) { await encryptKeys(sshDir, paths.keysDir, paths.tmpDir, pubkey); } break; } case 'decrypt': await decryptKeys(sshDir, paths.keysDir, paths.tmpDir, paths.keyPath); break; case 'clear': await clearDecryptedKeys(paths.tmpDir); break; case 'quit': console.log('\n๐Ÿ‘‹ Goodbye!\n'); running = false; break; } } }