@ideascol/cli-maker
Version:
A simple library to help create CLIs
238 lines (237 loc) • 12.6 kB
JavaScript
;
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'}`);
}
}