UNPKG

@ideascol/cli-maker

Version:
238 lines (237 loc) 12.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createRotatePassphraseCommand = createRotatePassphraseCommand; const interfaces_1 = require("./interfaces"); const colors_1 = require("./colors"); const setup_1 = require("./setup"); const fs_1 = __importDefault(require("fs")); const path_1 = __importDefault(require("path")); const os_1 = __importDefault(require("os")); const crypto_1 = __importDefault(require("crypto")); function createRotatePassphraseCommand(cliName) { return { name: 'rotate-passphrase', description: 'Rotate the passphrase used to encrypt configuration values', params: [ { name: 'config-file', description: 'Specific config file to rotate passphrase for (optional)', required: false, type: interfaces_1.ParamType.Text }, { name: 'create-backup', description: 'Create backup file before rotation (less secure but allows recovery)', required: false, type: interfaces_1.ParamType.Boolean }, { name: 'secure-delete-backup', description: 'Securely delete backup after successful rotation (only works with --create-backup)', required: false, type: interfaces_1.ParamType.Boolean, } ], action: async (args) => { const configFileName = args['config-file']; console.log(`\n${colors_1.Colors.BgYellow}${colors_1.Colors.FgBlack} ROTATE PASSPHRASE ${colors_1.Colors.Reset} ${colors_1.Colors.FgYellow}Updating encryption passphrase${colors_1.Colors.Reset}\n`); const configFile = buildConfigPath(cliName, configFileName); // Check if config file exists if (!fs_1.default.existsSync(configFile)) { console.log(`${colors_1.Colors.Error}❌ Config file not found: ${configFile}${colors_1.Colors.Reset}\n`); console.log(`${colors_1.Colors.FgGray}Run the setup command first to create a configuration.${colors_1.Colors.Reset}\n`); return; } // Load raw config to check if it has encrypted fields const rawConfig = JSON.parse(fs_1.default.readFileSync(configFile, 'utf-8')); const hasEncryptedFields = Object.values(rawConfig).some((value) => value && typeof value === 'object' && (value.__enc || value.__b64)); if (!hasEncryptedFields) { console.log(`${colors_1.Colors.Warning}⚠️ No encrypted fields found in config file.${colors_1.Colors.Reset}\n`); console.log(`${colors_1.Colors.FgGray}The configuration doesn't contain any password fields that require encryption.${colors_1.Colors.Reset}\n`); return; } try { // Get current passphrase console.log(`${colors_1.Colors.FgCyan}Enter current passphrase to decrypt existing configuration:${colors_1.Colors.Reset}`); const currentPassphrase = await (0, setup_1.hiddenPrompt)('Current passphrase: '); if (!currentPassphrase) { console.log(`${colors_1.Colors.Error}❌ Current passphrase is required.${colors_1.Colors.Reset}\n`); return; } // Try to decrypt with current passphrase to validate it const decryptedConfig = await tryDecryptConfig(rawConfig, currentPassphrase); if (!decryptedConfig) { console.log(`${colors_1.Colors.Error}❌ Invalid current passphrase. Unable to decrypt configuration.${colors_1.Colors.Reset}\n`); return; } // Get new passphrase console.log(`\n${colors_1.Colors.FgCyan}Enter new passphrase for encryption:${colors_1.Colors.Reset}`); const newPassphrase = await (0, setup_1.hiddenPrompt)('New passphrase: '); if (!newPassphrase) { console.log(`${colors_1.Colors.Error}❌ New passphrase is required.${colors_1.Colors.Reset}\n`); return; } if (newPassphrase === currentPassphrase) { console.log(`${colors_1.Colors.Warning}⚠️ New passphrase is the same as current passphrase.${colors_1.Colors.Reset}\n`); return; } // Confirm new passphrase console.log(`${colors_1.Colors.FgCyan}Confirm new passphrase:${colors_1.Colors.Reset}`); const confirmPassphrase = await (0, setup_1.hiddenPrompt)('Confirm new passphrase: '); if (newPassphrase !== confirmPassphrase) { console.log(`${colors_1.Colors.Error}❌ Passphrases do not match.${colors_1.Colors.Reset}\n`); return; } // Re-encrypt with new passphrase const reencryptedConfig = reencryptConfig(rawConfig, decryptedConfig, newPassphrase); let backupFile = null; // Handle backup based on security preferences (default: no backup for security) if (args['create-backup']) { backupFile = `${configFile}.backup.${Date.now()}`; fs_1.default.copyFileSync(configFile, backupFile); console.log(`${colors_1.Colors.FgGray}📁 Backup created: ${backupFile}${colors_1.Colors.Reset}`); if (args['secure-delete-backup']) { console.log(`${colors_1.Colors.FgYellow}⚠️ Backup will be securely deleted after successful rotation.${colors_1.Colors.Reset}`); } } else { console.log(`${colors_1.Colors.FgGreen}🔒 No backup created (secure by default).${colors_1.Colors.Reset}`); if (args['secure-delete-backup']) { console.log(`${colors_1.Colors.FgYellow}⚠️ --secure-delete-backup ignored (no backup created).${colors_1.Colors.Reset}`); } } // Write new encrypted config fs_1.default.writeFileSync(configFile, JSON.stringify(reencryptedConfig, null, 2), 'utf-8'); // Securely delete backup if requested if (backupFile && args['secure-delete-backup']) { try { secureDeleteFile(backupFile); console.log(`${colors_1.Colors.FgGray}🗑️ Backup securely deleted.${colors_1.Colors.Reset}`); } catch (error) { console.log(`${colors_1.Colors.Warning}⚠️ Warning: Could not securely delete backup file: ${backupFile}${colors_1.Colors.Reset}`); console.log(`${colors_1.Colors.FgGray}Please manually delete it: rm "${backupFile}"${colors_1.Colors.Reset}`); } } console.log(`\n${colors_1.Colors.Success}✅ Passphrase rotated successfully!${colors_1.Colors.Reset}`); console.log(`${colors_1.Colors.FgGray}Configuration file: ${configFile}${colors_1.Colors.Reset}`); console.log(`${colors_1.Colors.FgGray}All encrypted fields have been re-encrypted with the new passphrase.${colors_1.Colors.Reset}`); if (backupFile && !args['secure-delete-backup']) { console.log(`${colors_1.Colors.FgYellow}⚠️ Security reminder: Backup file contains old passphrase encryption.${colors_1.Colors.Reset}`); console.log(`${colors_1.Colors.FgGray}Consider deleting it manually: rm "${backupFile}"${colors_1.Colors.Reset}`); } console.log(''); } catch (error) { console.log(`${colors_1.Colors.Error}❌ Error rotating passphrase: ${error instanceof Error ? error.message : 'Unknown error'}${colors_1.Colors.Reset}\n`); } } }; } function buildConfigPath(cliName, customName) { const safeName = (customName || `${cliName}-config.json`).replace(/[^a-z0-9._-]/gi, '-'); const home = os_1.default.homedir(); return path_1.default.join(home, '.cli-maker', safeName); } async function tryDecryptConfig(rawConfig, passphrase) { try { const decrypted = { ...rawConfig }; for (const [key, value] of Object.entries(rawConfig)) { if (value && typeof value === 'object') { if (value.__enc && value.data) { // Encrypted field const decryptedValue = decryptWithPassphrase(value, passphrase); if (decryptedValue === undefined) { return null; // Failed to decrypt } decrypted[key] = decryptedValue; } else if (value.__b64 && value.value) { // Base64 encoded (no passphrase encryption) try { decrypted[key] = Buffer.from(value.value, 'base64').toString('utf-8'); } catch { decrypted[key] = undefined; } } } } return decrypted; } catch { return null; } } function reencryptConfig(rawConfig, decryptedConfig, newPassphrase) { const reencrypted = { ...rawConfig }; // Re-encrypt fields that were originally encrypted for (const [key, value] of Object.entries(rawConfig)) { if (value && typeof value === 'object' && (value.__enc || value.__b64)) { // This field was encrypted/encoded, so re-encrypt it with new passphrase const decryptedValue = decryptedConfig[key]; if (typeof decryptedValue === 'string') { reencrypted[key] = encryptWithPassphrase(decryptedValue, newPassphrase); } } } return reencrypted; } function encryptWithPassphrase(plain, passphrase) { const salt = crypto_1.default.randomBytes(16); const iv = crypto_1.default.randomBytes(12); const key = crypto_1.default.pbkdf2Sync(passphrase, salt, 120000, 32, 'sha256'); const cipher = crypto_1.default.createCipheriv('aes-256-gcm', key, iv); const encrypted = Buffer.concat([cipher.update(plain, 'utf8'), cipher.final()]); const tag = cipher.getAuthTag(); return { __enc: true, alg: 'aes-256-gcm', iv: iv.toString('base64'), salt: salt.toString('base64'), tag: tag.toString('base64'), data: encrypted.toString('base64'), }; } function decryptWithPassphrase(payload, passphrase) { try { const salt = Buffer.from(payload.salt, 'base64'); const iv = Buffer.from(payload.iv, 'base64'); const tag = Buffer.from(payload.tag, 'base64'); const data = Buffer.from(payload.data, 'base64'); const key = crypto_1.default.pbkdf2Sync(passphrase, salt, 120000, 32, 'sha256'); const decipher = crypto_1.default.createDecipheriv('aes-256-gcm', key, iv); decipher.setAuthTag(tag); const decrypted = Buffer.concat([decipher.update(data), decipher.final()]); return decrypted.toString('utf8'); } catch { return undefined; } } function secureDeleteFile(filePath) { try { // Get file stats to know the size const stats = fs_1.default.statSync(filePath); const fileSize = stats.size; // Overwrite file with random data multiple times for security const passes = 3; for (let i = 0; i < passes; i++) { const randomData = crypto_1.default.randomBytes(fileSize); fs_1.default.writeFileSync(filePath, randomData); fs_1.default.fsyncSync(fs_1.default.openSync(filePath, 'r+')); } // Finally overwrite with zeros const zeroData = Buffer.alloc(fileSize, 0); fs_1.default.writeFileSync(filePath, zeroData); fs_1.default.fsyncSync(fs_1.default.openSync(filePath, 'r+')); // Delete the file fs_1.default.unlinkSync(filePath); } catch (error) { throw new Error(`Failed to securely delete file: ${error instanceof Error ? error.message : 'Unknown error'}`); } }