dedpaste
Version:
CLI pastebin application using Cloudflare Workers and R2
1,475 lines (1,269 loc) ⢠48.7 kB
JavaScript
// Enhanced interactive mode for key management and encryption
import inquirer from 'inquirer';
import chalk from 'chalk';
import fs from 'fs';
import path from 'path';
import { promises as fsPromises } from 'fs';
import { homedir } from 'os';
// Import our unified key manager with dynamic imports to prevent initialization hanging
// We'll dynamically import modules at the time they're needed rather than at startup
// This prevents slowdowns and hanging during CLI initialization
let unifiedKeyManager;
let runKeyDiagnostics;
let formatDiagnosticsReport;
let encryptContent;
let decryptContent;
let continueWithEncryption;
let safeCheckGpgKeyring;
// Function to initialize all required modules
async function initModules() {
try {
// Import modules dynamically
const keyManagerModule = await import('./unifiedKeyManager.js');
unifiedKeyManager = keyManagerModule;
const diagnosticsModule = await import('./keyDiagnostics.js');
runKeyDiagnostics = diagnosticsModule.runKeyDiagnostics;
formatDiagnosticsReport = diagnosticsModule.formatDiagnosticsReport;
const encryptionModule = await import('./encryptionUtils.js');
encryptContent = encryptionModule.encryptContent;
decryptContent = encryptionModule.decryptContent;
const helpersModule = await import('./encryptionHelpers.js');
continueWithEncryption = helpersModule.continueWithEncryption;
safeCheckGpgKeyring = helpersModule.safeCheckGpgKeyring;
return true;
} catch (error) {
console.error(`Module initialization error: ${error.message}`);
return false;
}
}
/**
* Show enhanced interactive key management menu
* @returns {Promise<Object>} - Operation result
*/
async function enhancedKeyManagement() {
try {
console.log(chalk.blue('Initializing key management system...'));
// Initialize modules first
const modulesLoaded = await initModules();
if (!modulesLoaded) {
return {
success: false,
message: 'Failed to load required modules. Please try again.'
};
}
// Initialize the key manager
const init = await unifiedKeyManager.initialize();
if (!init.success) {
return {
success: false,
message: `Failed to initialize key system: ${init.error}`
};
}
// Main menu loop
let exitMenu = false;
while (!exitMenu) {
const { action } = await inquirer.prompt([{
type: 'list',
name: 'action',
message: 'DedPaste Key Management',
prefix: chalk.magenta('š'),
choices: [
{ name: chalk.green('List and search keys'), value: 'search' },
{ name: chalk.green('Add or import key'), value: 'add' },
{ name: chalk.green('Generate a new key'), value: 'generate' },
{ name: chalk.green('View key details'), value: 'view' },
{ name: chalk.green('Export keys'), value: 'export' },
{ name: chalk.green('Remove keys'), value: 'remove' },
{ name: chalk.green('Run diagnostics'), value: 'diagnostics' },
{ name: chalk.red('Exit'), value: 'exit' }
]
}]);
switch (action) {
case 'search':
await searchAndListKeys();
break;
case 'add':
await addOrImportKey();
break;
case 'generate':
await generateNewKey();
break;
case 'view':
await viewKeyDetails();
break;
case 'export':
await exportKeys();
break;
case 'remove':
await removeKeys();
break;
case 'diagnostics':
await runDiagnostics();
break;
case 'exit':
exitMenu = true;
break;
}
}
return {
success: true,
message: 'Key management session completed'
};
} catch (error) {
return {
success: false,
message: `Error in key management: ${error.message}`
};
}
}
/**
* Search and list keys
* @returns {Promise<void>}
*/
async function searchAndListKeys() {
try {
const { searchMode, query } = await inquirer.prompt([
{
type: 'list',
name: 'searchMode',
message: 'How would you like to find keys?',
choices: [
{ name: 'List all keys', value: 'all' },
{ name: 'Search by name', value: 'name' },
{ name: 'Search by email', value: 'email' },
{ name: 'Search by fingerprint', value: 'fingerprint' }
]
},
{
type: 'input',
name: 'query',
message: 'Enter search term:',
when: (answers) => answers.searchMode !== 'all'
}
]);
// Set up search options
const searchOptions = {
includeGpg: true
};
// Get keys based on search mode
let keys;
if (searchMode === 'all') {
keys = await unifiedKeyManager.searchKeys('', searchOptions);
} else {
keys = await unifiedKeyManager.searchKeys(query, searchOptions);
}
if (keys.length === 0) {
console.log(chalk.yellow('\nNo keys found matching your criteria'));
return;
}
// Group keys by type
const groupedKeys = {
self: [],
friend: [],
pgp: [],
keybase: [],
gpg: []
};
for (const key of keys) {
if (key.source === 'self') {
groupedKeys.self.push(key);
} else if (key.source === 'friend') {
groupedKeys.friend.push(key);
} else if (key.source === 'pgp') {
groupedKeys.pgp.push(key);
} else if (key.source === 'keybase') {
groupedKeys.keybase.push(key);
} else if (key.source === 'gpg') {
groupedKeys.gpg.push(key);
}
}
// Print results in a formatted way
console.log(chalk.bold.green('\n===== Key Search Results ====='));
// Print self key
if (groupedKeys.self.length > 0) {
console.log(chalk.bold.blue('\nSelf Keys:'));
for (const key of groupedKeys.self) {
console.log(`- ${chalk.green('Name:')} Self`);
console.log(` ${chalk.green('Type:')} ${key.type.toUpperCase()}`);
console.log(` ${chalk.green('Fingerprint:')} ${key.fingerprint}`);
if (key.created) {
console.log(` ${chalk.green('Created:')} ${new Date(key.created).toLocaleString()}`);
}
}
}
// Print friend keys
if (groupedKeys.friend.length > 0) {
console.log(chalk.bold.blue('\nFriend Keys:'));
for (const key of groupedKeys.friend) {
console.log(`- ${chalk.green('Name:')} ${key.name}`);
console.log(` ${chalk.green('Type:')} ${key.type.toUpperCase()}`);
console.log(` ${chalk.green('Fingerprint:')} ${key.fingerprint}`);
if (key.lastUsed) {
console.log(` ${chalk.green('Last Used:')} ${new Date(key.lastUsed).toLocaleString()}`);
}
}
}
// Print PGP keys
if (groupedKeys.pgp.length > 0) {
console.log(chalk.bold.blue('\nPGP Keys:'));
for (const key of groupedKeys.pgp) {
console.log(`- ${chalk.green('Name:')} ${key.name}`);
console.log(` ${chalk.green('Type:')} ${key.type.toUpperCase()}`);
console.log(` ${chalk.green('Fingerprint:')} ${key.fingerprint}`);
if (key.email) {
console.log(` ${chalk.green('Email:')} ${key.email}`);
}
if (key.lastUsed) {
console.log(` ${chalk.green('Last Used:')} ${new Date(key.lastUsed).toLocaleString()}`);
}
}
}
// Print Keybase keys
if (groupedKeys.keybase.length > 0) {
console.log(chalk.bold.blue('\nKeybase Keys:'));
for (const key of groupedKeys.keybase) {
console.log(`- ${chalk.green('Name:')} ${key.name}`);
console.log(` ${chalk.green('Type:')} ${key.type.toUpperCase()}`);
console.log(` ${chalk.green('Username:')} ${key.username}`);
console.log(` ${chalk.green('Fingerprint:')} ${key.fingerprint}`);
if (key.email) {
console.log(` ${chalk.green('Email:')} ${key.email}`);
}
if (key.lastUsed) {
console.log(` ${chalk.green('Last Used:')} ${new Date(key.lastUsed).toLocaleString()}`);
}
}
}
// Print GPG keys
if (groupedKeys.gpg.length > 0) {
console.log(chalk.bold.blue('\nGPG Keyring Keys:'));
for (const key of groupedKeys.gpg) {
console.log(`- ${chalk.green('Key ID:')} ${key.id}`);
console.log(` ${chalk.green('Name:')} ${key.name}`);
console.log(` ${chalk.green('Type:')} ${key.type.toUpperCase()}`);
if (key.email) {
console.log(` ${chalk.green('Email:')} ${key.email}`);
}
if (key.created) {
console.log(` ${chalk.green('Created:')} ${new Date(key.created).toLocaleString()}`);
}
if (key.expires) {
console.log(` ${chalk.green('Expires:')} ${new Date(key.expires).toLocaleString()}`);
}
}
}
console.log(chalk.bold.green('\n============================='));
// Ask if user wants to view details of a specific key
const { viewDetails } = await inquirer.prompt([{
type: 'confirm',
name: 'viewDetails',
message: 'Would you like to view details of a specific key?',
default: false
}]);
if (viewDetails) {
await viewKeyDetails(keys);
}
} catch (error) {
console.error(chalk.red(`\nError searching keys: ${error.message}`));
}
}
/**
* Add or import a key
* @returns {Promise<void>}
*/
async function addOrImportKey() {
try {
const { importSource } = await inquirer.prompt([{
type: 'list',
name: 'importSource',
message: 'How would you like to add a key?',
choices: [
{ name: 'Import from file', value: 'file' },
{ name: 'Fetch from PGP keyserver', value: 'pgp-server' },
{ name: 'Fetch from Keybase', value: 'keybase' },
{ name: 'Import from GPG keyring', value: 'gpg-keyring' },
{ name: 'Import to GPG keyring', value: 'gpg-import' },
{ name: 'Paste key content', value: 'paste' }
]
}]);
let options = { source: importSource };
// Handle different import sources
switch (importSource) {
case 'file': {
const { filePath, name } = await inquirer.prompt([
{
type: 'input',
name: 'filePath',
message: 'Enter the path to the key file:',
validate: (input) => {
if (!input) return 'Path cannot be empty';
if (!fs.existsSync(input)) return 'File does not exist';
return true;
}
},
{
type: 'input',
name: 'name',
message: 'Enter a name for this key (leave empty to use default):',
default: ''
}
]);
options.file = filePath;
if (name) options.name = name;
break;
}
case 'pgp-server': {
const { identifier, name } = await inquirer.prompt([
{
type: 'input',
name: 'identifier',
message: 'Enter email address or key ID:',
validate: (input) => input ? true : 'Identifier cannot be empty'
},
{
type: 'input',
name: 'name',
message: 'Enter a name for this key (leave empty to use default):',
default: ''
}
]);
if (identifier.includes('@')) {
options.email = identifier;
} else {
options.keyId = identifier;
}
if (name) options.name = name;
break;
}
case 'keybase': {
const { username, name, verify } = await inquirer.prompt([
{
type: 'input',
name: 'username',
message: 'Enter Keybase username:',
validate: (input) => input ? true : 'Username cannot be empty'
},
{
type: 'input',
name: 'name',
message: 'Enter a name for this key (leave empty to use default):',
default: ''
},
{
type: 'confirm',
name: 'verify',
message: 'Verify Keybase proofs?',
default: true
}
]);
options.username = username;
if (name) options.name = name;
options.verify = verify;
break;
}
case 'gpg-keyring': {
// Check if GPG is available
const gpgInfo = await unifiedKeyManager.initialize();
if (!gpgInfo.gpg || !gpgInfo.gpg.available) {
console.log(chalk.red('\nGPG is not available on your system'));
return;
}
// List available GPG keys for selection
console.log(chalk.blue('\nAvailable GPG keys:'));
const gpgKeys = gpgInfo.gpg.keys.map((key, index) => {
const uid = key.uids.length > 0 ? key.uids[0].uid : 'No user ID';
return {
name: `${key.id} - ${uid}`,
value: key.id,
short: key.id
};
});
if (gpgKeys.length === 0) {
console.log(chalk.yellow('No keys found in GPG keyring'));
return;
}
const { keyId, name } = await inquirer.prompt([
{
type: 'list',
name: 'keyId',
message: 'Select a key from GPG keyring:',
choices: gpgKeys
},
{
type: 'input',
name: 'name',
message: 'Enter a name for this key (leave empty to use default):',
default: ''
}
]);
options.keyId = keyId;
if (name) options.name = name;
break;
}
case 'gpg-import': {
const { content } = await inquirer.prompt([{
type: 'editor',
name: 'content',
message: 'Paste the PGP key to import to GPG keyring:',
validate: (input) => {
if (!input) return 'Key content cannot be empty';
if (!input.includes('-----BEGIN PGP PUBLIC KEY BLOCK-----')) {
return 'Invalid PGP key format';
}
return true;
}
}]);
options.content = content;
break;
}
case 'paste': {
const { content, name } = await inquirer.prompt([
{
type: 'editor',
name: 'content',
message: 'Paste the key content:',
validate: (input) => input ? true : 'Key content cannot be empty'
},
{
type: 'input',
name: 'name',
message: 'Enter a name for this key:',
validate: (input) => input ? true : 'Name cannot be empty'
}
]);
// Change to file source with content
options.source = 'file';
options.content = content;
options.name = name;
break;
}
}
// Show a "working" message for operations that might take time
console.log(chalk.blue('\nImporting key... please wait...'));
// Import the key
const result = await unifiedKeyManager.importKey(options);
if (result.success) {
console.log(chalk.green('\nā Key imported successfully!'));
// Display key details
console.log(chalk.bold('\nKey details:'));
console.log(`- ${chalk.green('Type:')} ${result.type}`);
console.log(`- ${chalk.green('Name:')} ${result.name}`);
if (result.fingerprint) {
console.log(`- ${chalk.green('Fingerprint:')} ${result.fingerprint}`);
}
if (result.email) {
console.log(`- ${chalk.green('Email:')} ${result.email}`);
}
if (result.username) {
console.log(`- ${chalk.green('Username:')} ${result.username}`);
}
if (result.path) {
console.log(`- ${chalk.green('Stored at:')} ${result.path}`);
}
if (result.keyId) {
console.log(`- ${chalk.green('Key ID:')} ${result.keyId}`);
}
if (result.output) {
console.log(`\n${result.output}`);
}
} else {
console.log(chalk.red(`\nā Import failed: ${result.error}`));
}
} catch (error) {
console.error(chalk.red(`\nError importing key: ${error.message}`));
}
}
/**
* Generate a new key pair
* @returns {Promise<void>}
*/
async function generateNewKey() {
try {
const { confirm } = await inquirer.prompt([{
type: 'confirm',
name: 'confirm',
message: 'Generate a new RSA key pair? This will overwrite your existing self key if present.',
default: false
}]);
if (!confirm) return;
console.log(chalk.blue('\nGenerating new key pair... please wait...'));
// Generate the key
const result = await unifiedKeyManager.generateKey();
if (result.success) {
console.log(chalk.green('\nā Key generated successfully!'));
console.log(`- ${chalk.green('Private key:')} ${result.privateKeyPath}`);
console.log(`- ${chalk.green('Public key:')} ${result.publicKeyPath}`);
// Offer to back up the key
const { backup } = await inquirer.prompt([{
type: 'confirm',
name: 'backup',
message: 'Would you like to export your keys to a backup location?',
default: true
}]);
if (backup) {
const { backupDir } = await inquirer.prompt([{
type: 'input',
name: 'backupDir',
message: 'Enter a directory to save backups:',
default: path.join(homedir(), 'dedpaste-backup'),
validate: (input) => input ? true : 'Directory cannot be empty'
}]);
// Create backup directory if it doesn't exist
await fsPromises.mkdir(backupDir, { recursive: true });
// Copy the keys
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const privateKeyBackup = path.join(backupDir, `private-key-${timestamp}.pem`);
const publicKeyBackup = path.join(backupDir, `public-key-${timestamp}.pem`);
await fsPromises.copyFile(result.privateKeyPath, privateKeyBackup);
await fsPromises.copyFile(result.publicKeyPath, publicKeyBackup);
console.log(chalk.green('\nā Keys backed up successfully!'));
console.log(`- ${chalk.green('Private key backup:')} ${privateKeyBackup}`);
console.log(`- ${chalk.green('Public key backup:')} ${publicKeyBackup}`);
console.log(chalk.yellow('\nIMPORTANT: Keep your private key backup secure!'));
}
} else {
console.log(chalk.red(`\nā Key generation failed: ${result.error}`));
}
} catch (error) {
console.error(chalk.red(`\nError generating key: ${error.message}`));
}
}
/**
* View key details
* @param {Array} preloadedKeys - Optional preloaded keys
* @returns {Promise<void>}
*/
async function viewKeyDetails(preloadedKeys = null) {
try {
// If keys are not preloaded, search for all keys
const keys = preloadedKeys || await unifiedKeyManager.searchKeys('', { includeGpg: true });
if (keys.length === 0) {
console.log(chalk.yellow('\nNo keys found'));
return;
}
// Prepare choices for key selection
const keyChoices = keys.map(key => {
let label = '';
if (key.source === 'self') {
label = `${key.name} (Self Key)`;
} else if (key.source === 'friend') {
label = `${key.name} (Friend Key)`;
} else if (key.source === 'pgp') {
label = `${key.name} (PGP Key${key.email ? ` - ${key.email}` : ''})`;
} else if (key.source === 'keybase') {
label = `${key.name} (Keybase - ${key.username || 'unknown'})`;
} else if (key.source === 'gpg') {
const uid = key.uids && key.uids.length > 0 ? key.uids[0] : 'Unknown';
label = `${key.id} (GPG - ${uid})`;
}
return {
name: label,
value: key.id || key.fingerprint,
short: key.id || key.fingerprint
};
});
const { selectedKeyId } = await inquirer.prompt([{
type: 'list',
name: 'selectedKeyId',
message: 'Select a key to view details:',
choices: keyChoices
}]);
// Get detailed key info
const keyInfo = await unifiedKeyManager.getKeyById(selectedKeyId, { includeGpg: true });
if (!keyInfo) {
console.log(chalk.red('\nKey not found'));
return;
}
// Display detailed key information
console.log(chalk.bold.green('\n===== Key Details ====='));
console.log(`- ${chalk.green('ID:')} ${keyInfo.id}`);
console.log(`- ${chalk.green('Name:')} ${keyInfo.name}`);
console.log(`- ${chalk.green('Type:')} ${keyInfo.type.toUpperCase()}`);
console.log(`- ${chalk.green('Source:')} ${keyInfo.source}`);
console.log(`- ${chalk.green('Fingerprint:')} ${keyInfo.fingerprint}`);
if (keyInfo.email) {
console.log(`- ${chalk.green('Email:')} ${keyInfo.email}`);
}
if (keyInfo.username) {
console.log(`- ${chalk.green('Username:')} ${keyInfo.username}`);
}
if (keyInfo.created) {
console.log(`- ${chalk.green('Created:')} ${new Date(keyInfo.created).toLocaleString()}`);
}
if (keyInfo.lastUsed) {
console.log(`- ${chalk.green('Last Used:')} ${new Date(keyInfo.lastUsed).toLocaleString()}`);
}
if (keyInfo.expires) {
console.log(`- ${chalk.green('Expires:')} ${new Date(keyInfo.expires).toLocaleString()}`);
}
if (keyInfo.trust) {
console.log(`- ${chalk.green('Trust Level:')} ${keyInfo.trust}`);
}
if (keyInfo.uids && keyInfo.uids.length > 0) {
console.log(chalk.green('- User IDs:'));
keyInfo.uids.forEach(uid => {
console.log(` - ${uid}`);
});
}
if (keyInfo.path) {
if (typeof keyInfo.path === 'object') {
console.log(`- ${chalk.green('Public Key Path:')} ${keyInfo.path.public}`);
console.log(`- ${chalk.green('Private Key Path:')} ${keyInfo.path.private}`);
} else {
console.log(`- ${chalk.green('Key Path:')} ${keyInfo.path}`);
}
}
console.log(chalk.bold.green('\n========================='));
// Offer to view key content
const { viewContent } = await inquirer.prompt([{
type: 'confirm',
name: 'viewContent',
message: 'Would you like to view the key content?',
default: false
}]);
if (viewContent) {
// Ask about private key for self keys
let viewPrivate = false;
if (keyInfo.source === 'self') {
const { usePrivate } = await inquirer.prompt([{
type: 'confirm',
name: 'usePrivate',
message: 'View private key? (WARNING: Private keys should be kept secure)',
default: false
}]);
viewPrivate = usePrivate;
}
const keyContent = await unifiedKeyManager.readKeyContent(keyInfo, {
private: viewPrivate
});
if (keyContent) {
console.log(chalk.bold.green('\n===== Key Content ====='));
console.log(keyContent);
console.log(chalk.bold.green('\n========================='));
} else {
console.log(chalk.red('\nCould not read key content'));
}
}
} catch (error) {
console.error(chalk.red(`\nError viewing key details: ${error.message}`));
}
}
/**
* Export keys to files
* @returns {Promise<void>}
*/
async function exportKeys() {
try {
// Search for all keys
const keys = await unifiedKeyManager.searchKeys('', { includeGpg: true });
if (keys.length === 0) {
console.log(chalk.yellow('\nNo keys found to export'));
return;
}
// Prepare choices for key selection
const keyChoices = keys.map(key => {
let label = '';
if (key.source === 'self') {
label = `${key.name} (Self Key)`;
} else if (key.source === 'friend') {
label = `${key.name} (Friend Key)`;
} else if (key.source === 'pgp') {
label = `${key.name} (PGP Key${key.email ? ` - ${key.email}` : ''})`;
} else if (key.source === 'keybase') {
label = `${key.name} (Keybase - ${key.username || 'unknown'})`;
} else if (key.source === 'gpg') {
const uid = key.uids && key.uids.length > 0 ? key.uids[0] : 'Unknown';
label = `${key.id} (GPG - ${uid})`;
}
return {
name: label,
value: key.id || key.fingerprint,
short: key.id || key.fingerprint
};
});
const { selectedKeyId } = await inquirer.prompt([{
type: 'list',
name: 'selectedKeyId',
message: 'Select a key to export:',
choices: keyChoices
}]);
// Get detailed key info
const keyInfo = await unifiedKeyManager.getKeyById(selectedKeyId, { includeGpg: true });
if (!keyInfo) {
console.log(chalk.red('\nKey not found'));
return;
}
// Ask about private key for self keys
let exportPrivate = false;
if (keyInfo.source === 'self') {
const { usePrivate } = await inquirer.prompt([{
type: 'confirm',
name: 'usePrivate',
message: 'Export private key? (WARNING: Private keys should be kept secure)',
default: false
}]);
exportPrivate = usePrivate;
}
// Get export location
const { exportDir } = await inquirer.prompt([{
type: 'input',
name: 'exportDir',
message: 'Enter a directory to export the key:',
default: homedir(),
validate: (input) => {
if (!input) return 'Directory cannot be empty';
if (!fs.existsSync(input)) return 'Directory does not exist';
return true;
}
}]);
// Generate filename based on key type and name
let filename;
if (keyInfo.source === 'self') {
filename = exportPrivate ? 'dedpaste_private_key.pem' : 'dedpaste_public_key.pem';
} else if (keyInfo.type === 'pgp' || keyInfo.type === 'keybase') {
filename = `${keyInfo.name.replace(/[^a-z0-9_-]/gi, '_')}.asc`;
} else if (keyInfo.type === 'gpg') {
filename = `gpg_${keyInfo.id.substring(keyInfo.id.length - 8)}.asc`;
} else {
filename = `${keyInfo.name.replace(/[^a-z0-9_-]/gi, '_')}.pem`;
}
const { customFilename } = await inquirer.prompt([{
type: 'input',
name: 'customFilename',
message: 'Enter a filename (leave empty to use default):',
default: filename
}]);
const exportPath = path.join(exportDir, customFilename || filename);
// Read key content
const keyContent = await unifiedKeyManager.readKeyContent(keyInfo, {
private: exportPrivate
});
if (!keyContent) {
console.log(chalk.red('\nCould not read key content'));
return;
}
// Write key to file
await fsPromises.writeFile(exportPath, keyContent);
console.log(chalk.green(`\nā Key exported successfully to: ${exportPath}`));
if (exportPrivate) {
console.log(chalk.yellow('\nWARNING: This file contains a private key. Keep it secure!'));
}
} catch (error) {
console.error(chalk.red(`\nError exporting key: ${error.message}`));
}
}
/**
* Remove keys
* @returns {Promise<void>}
*/
async function removeKeys() {
try {
// Search for all keys except GPG (which we can't remove through our interface)
const keys = await unifiedKeyManager.searchKeys('', { includeGpg: false });
if (keys.length === 0) {
console.log(chalk.yellow('\nNo keys found to remove'));
return;
}
// Prepare choices for key selection
const keyChoices = keys.map(key => {
let label = '';
if (key.source === 'self') {
label = `${key.name} (Self Key)`;
} else if (key.source === 'friend') {
label = `${key.name} (Friend Key)`;
} else if (key.source === 'pgp') {
label = `${key.name} (PGP Key${key.email ? ` - ${key.email}` : ''})`;
} else if (key.source === 'keybase') {
label = `${key.name} (Keybase - ${key.username || 'unknown'})`;
}
return {
name: label,
value: key.id || key.fingerprint,
short: key.id || key.fingerprint
};
});
const { selectedKeyId } = await inquirer.prompt([{
type: 'list',
name: 'selectedKeyId',
message: 'Select a key to remove:',
choices: keyChoices
}]);
// Get detailed key info for confirmation
const keyInfo = await unifiedKeyManager.getKeyById(selectedKeyId);
if (!keyInfo) {
console.log(chalk.red('\nKey not found'));
return;
}
// Special warning for self key
let confirmMessage = `Are you sure you want to remove ${keyInfo.name}?`;
if (keyInfo.source === 'self') {
confirmMessage = chalk.yellow('WARNING: You are about to remove your personal key. This will prevent decryption of messages sent to you. Are you sure?');
}
const { confirm } = await inquirer.prompt([{
type: 'confirm',
name: 'confirm',
message: confirmMessage,
default: false
}]);
if (!confirm) {
console.log(chalk.blue('\nOperation cancelled'));
return;
}
// Remove the key
const result = await unifiedKeyManager.removeKey(selectedKeyId);
if (result.success) {
console.log(chalk.green(`\nā Key removed successfully`));
} else {
console.log(chalk.red(`\nā Failed to remove key: ${result.error}`));
}
} catch (error) {
console.error(chalk.red(`\nError removing key: ${error.message}`));
}
}
/**
* Run key system diagnostics
* @returns {Promise<void>}
*/
async function runDiagnostics() {
try {
console.log(chalk.blue('\nRunning key system diagnostics... please wait...'));
const results = await runKeyDiagnostics();
const report = formatDiagnosticsReport(results);
console.log('\n' + report);
// Offer to fix common issues
if (results.errors.length > 0 || results.warnings.length > 0) {
const { fixIssues } = await inquirer.prompt([{
type: 'confirm',
name: 'fixIssues',
message: 'Would you like to attempt to fix issues automatically?',
default: true
}]);
if (fixIssues) {
await fixDiagnosticIssues(results);
}
}
} catch (error) {
console.error(chalk.red(`\nError running diagnostics: ${error.message}`));
}
}
/**
* Fix common diagnostic issues
* @param {Object} diagnosticResults - Results from runKeyDiagnostics
* @returns {Promise<void>}
*/
async function fixDiagnosticIssues(diagnosticResults) {
try {
// Check for missing directories
let fixedCount = 0;
const missingDirs = Object.entries(diagnosticResults.filesystemChecks.directories)
.filter(([dir, info]) => !info.exists)
.map(([dir]) => dir);
if (missingDirs.length > 0) {
console.log(chalk.blue('\nCreating missing directories...'));
for (const dir of missingDirs) {
try {
await fsPromises.mkdir(dir, { recursive: true });
console.log(chalk.green(`ā Created directory: ${dir}`));
fixedCount++;
} catch (error) {
console.log(chalk.red(`ā Failed to create directory ${dir}: ${error.message}`));
}
}
}
// Check for missing self key
if (!diagnosticResults.keyStats.self) {
console.log(chalk.yellow('\nYou have no self key for encryption/decryption.'));
const { generateKey } = await inquirer.prompt([{
type: 'confirm',
name: 'generateKey',
message: 'Would you like to generate a new key?',
default: true
}]);
if (generateKey) {
await generateNewKey();
fixedCount++;
}
}
// Report results
if (fixedCount > 0) {
console.log(chalk.green(`\nā Fixed ${fixedCount} issues`));
} else {
console.log(chalk.yellow('\nNo issues were fixed automatically'));
}
// Suggest manual actions for remaining issues
const remainingIssues = [...diagnosticResults.errors, ...diagnosticResults.warnings];
if (remainingIssues.length > 0) {
console.log(chalk.yellow('\nRemaining issues that require manual attention:'));
remainingIssues.forEach(issue => {
console.log(`- ${issue}`);
});
console.log(chalk.blue('\nSuggested actions:'));
// Check for permission issues
const permissionIssues = remainingIssues.some(issue => issue.includes('not writable'));
if (permissionIssues) {
console.log('- Check file and directory permissions in the .dedpaste directory');
console.log(` Run: chmod -R u+rw ${path.join(homedir(), '.dedpaste')}`);
}
// Check for missing/corrupted keys
const keyIssues = remainingIssues.some(issue =>
issue.includes('key file not found') ||
issue.includes('Invalid') ||
issue.includes('Error reading')
);
if (keyIssues) {
console.log('- Reimport or regenerate problematic keys');
console.log('- Check if key files were moved or deleted manually');
}
}
} catch (error) {
console.error(chalk.red(`\nError fixing issues: ${error.message}`));
}
}
/**
* Send a message with enhanced interactive features
* @returns {Promise<Object>}
*/
async function enhancedInteractiveSend() {
try {
console.log(chalk.blue('Initializing interactive send mode...'));
// Initialize modules first to prevent hanging
const modulesLoaded = await initModules();
if (!modulesLoaded) {
return {
success: false,
message: 'Failed to load required modules. Please try again.'
};
}
// Initialize the key manager
const init = await unifiedKeyManager.initialize();
if (!init.success) {
return {
success: false,
message: `Failed to initialize key system: ${init.error}`
};
}
// Get message
const { message } = await inquirer.prompt([{
type: 'editor',
name: 'message',
message: 'Enter your message:',
validate: input => input.trim() !== '' ? true : 'Message cannot be empty'
}]);
// Get all recipients (except GPG, which we handle separately)
const allKeys = await unifiedKeyManager.searchKeys('', { includeGpg: false });
if (allKeys.length === 0) {
console.log(chalk.yellow('\nNo keys found. Generate a key first with "dedpaste keys --gen-key"'));
return { success: false, message: 'No keys available' };
}
// Filter keys by type for better organization
const selfKeys = allKeys.filter(key => key.source === 'self');
const friendKeys = allKeys.filter(key => key.source === 'friend');
const pgpKeys = allKeys.filter(key => key.source === 'pgp');
const keybaseKeys = allKeys.filter(key => key.source === 'keybase');
// Create categorized choices
const choices = [];
// Self category
if (selfKeys.length > 0) {
choices.push({ name: chalk.bold.blue('--- Self ---'), value: null, disabled: true });
choices.push(...selfKeys.map(key => ({
name: 'Self',
value: null,
short: 'Self'
})));
}
// Friend category
if (friendKeys.length > 0) {
choices.push({ name: chalk.bold.blue('--- Friends ---'), value: null, disabled: true });
choices.push(...friendKeys.map(key => ({
name: key.name,
value: key.id,
short: key.name
})));
}
// PGP category
if (pgpKeys.length > 0) {
choices.push({ name: chalk.bold.blue('--- PGP Keys ---'), value: null, disabled: true });
choices.push(...pgpKeys.map(key => ({
name: `${key.name}${key.email ? ` <${key.email}>` : ''}`,
value: key.id,
short: key.name
})));
}
// Keybase category
if (keybaseKeys.length > 0) {
choices.push({ name: chalk.bold.blue('--- Keybase ---'), value: null, disabled: true });
choices.push(...keybaseKeys.map(key => ({
name: `${key.name} (${key.username || 'unknown'})`,
value: key.id,
short: key.name
})));
}
// GPG option at the end
choices.push({ name: chalk.bold.blue('--- Other Options ---'), value: null, disabled: true });
choices.push({ name: 'Use GPG keyring directly (select a GPG key)', value: 'gpg', short: 'GPG' });
// Get recipient
const { recipient } = await inquirer.prompt([{
type: 'list',
name: 'recipient',
message: 'Select recipient:',
choices: choices
}]);
let recipientId = recipient;
// Handle GPG keyring selection with improved error handling
if (recipient === 'gpg') {
console.log(chalk.blue('Checking GPG keyring (this may take a moment)...'));
let gpgInfo;
try {
// Use the safe GPG check helper function with an increased timeout for better reliability
const { safeCheckGpgKeyring } = await import('./encryptionHelpers.js');
gpgInfo = await safeCheckGpgKeyring(8000); // 8 second timeout
// Handle timeout or unavailability
if (!gpgInfo.available) {
let errorMessage = '\nGPG is not available on this system.';
if (gpgInfo.timedOut) {
errorMessage = '\nGPG check timed out - this may indicate GPG is hanging or not properly configured.';
} else if (gpgInfo.error) {
errorMessage = `\nGPG error: ${gpgInfo.error}`;
}
console.log(chalk.yellow(errorMessage));
console.log(chalk.blue('Proceeding without GPG integration...'));
// Ask if user wants to continue with another encryption option
const { continueAnyway } = await inquirer.prompt([{
type: 'confirm',
name: 'continueAnyway',
message: 'Do you want to continue with another encryption option?',
default: true
}]);
if (!continueAnyway) {
return { success: false, message: 'Operation canceled by user' };
}
// Provide alternative encryption options
const { altRecipient } = await inquirer.prompt([{
type: 'list',
name: 'altRecipient',
message: 'Select an alternative recipient:',
choices: choices.filter(c => c.value !== 'gpg' && !c.disabled)
}]);
recipientId = altRecipient;
// Continue with the alternative recipient using our helper function
return await continueWithEncryption(recipientId, message);
}
} catch (gpgError) {
console.error(chalk.red(`\nUnexpected error during GPG check: ${gpgError.message}`));
console.log(chalk.blue('Proceeding without GPG integration...'));
// Fall back to alternative options due to the error
const { continueAfterError } = await inquirer.prompt([{
type: 'confirm',
name: 'continueAfterError',
message: 'Would you like to continue with an alternative encryption method?',
default: true
}]);
if (!continueAfterError) {
return { success: false, message: 'Operation canceled due to GPG error' };
}
// Provide alternative encryption options
const { emergencyRecipient } = await inquirer.prompt([{
type: 'list',
name: 'emergencyRecipient',
message: 'Select an alternative recipient:',
choices: choices.filter(c => c.value !== 'gpg' && !c.disabled)
}]);
recipientId = emergencyRecipient;
return await continueWithEncryption(recipientId, message);
}
// Add a function to handle alternative encryption options
const handleAlternativeEncryption = async (reason) => {
console.log(chalk.yellow(`\n${reason}`));
// Ask if user wants to continue with another encryption option
const { continueWithoutGpg } = await inquirer.prompt([{
type: 'confirm',
name: 'continueWithoutGpg',
message: 'Would you like to continue with an alternative encryption method?',
default: true
}]);
if (!continueWithoutGpg) {
return { success: false, message: 'Operation canceled by user' };
}
// Provide alternative encryption options
const { alternativeMethod } = await inquirer.prompt([{
type: 'list',
name: 'alternativeMethod',
message: 'Select an alternative encryption method:',
choices: [
{ name: 'Password-based encryption', value: 'password' },
{ name: 'Standard RSA encryption', value: 'default' },
{ name: 'Create a public paste (no encryption)', value: 'public' }
]
}]);
return await continueWithEncryption(alternativeMethod, message);
};
// Normal flow if GPG is available
if (!gpgInfo || !gpgInfo.available) {
return await handleAlternativeEncryption('GPG is not available on this system.');
}
if (!gpgInfo.keys || gpgInfo.keys.length === 0) {
console.log(chalk.yellow('\nNo keys found in GPG keyring'));
// Ask if user wants to continue with another encryption option
const { continueWithoutGpg } = await inquirer.prompt([{
type: 'confirm',
name: 'continueWithoutGpg',
message: 'Would you like to continue with an alternative encryption method?',
default: true
}]);
if (!continueWithoutGpg) {
return { success: false, message: 'Operation canceled - no GPG keys available' };
}
// Provide alternative encryption options
const { alternativeMethod } = await inquirer.prompt([{
type: 'list',
name: 'alternativeMethod',
message: 'Select an alternative encryption method:',
choices: [
{ name: 'Password-based encryption', value: 'password' },
{ name: 'Create a public paste (no encryption)', value: 'public' }
]
}]);
return await continueWithEncryption(alternativeMethod, message);
}
// Create GPG key choices with friendly display names
// Use try-catch to handle any parsing issues with GPG keys
let gpgChoices = [];
try {
gpgChoices = gpgInfo.keys.map(key => {
// Handle different key UID formats and ensure we always have a readable name
let displayName = 'Unknown';
if (key.uids && key.uids.length > 0) {
const uid = key.uids[0].uid || '';
// Extract email if available
const emailMatch = uid.match(/<([^>]+)>/);
const email = emailMatch ? emailMatch[1] : '';
// Extract name if available
const nameMatch = uid.match(/^([^<]+)/);
const name = nameMatch ? nameMatch[1].trim() : '';
displayName = name ? name : email ? email : uid;
}
return {
name: `${displayName} (${key.id})`,
value: key.id,
short: key.id
};
});
// Sort keys alphabetically by display name for better UX
gpgChoices.sort((a, b) => a.name.localeCompare(b.name));
} catch (parseError) {
console.error('Error parsing GPG keys:', parseError);
return await handleAlternativeEncryption('Error parsing GPG keys. Please try an alternative method.');
}
// Sanity check - make sure we have valid choices
if (!gpgChoices || gpgChoices.length === 0) {
return await handleAlternativeEncryption('No valid GPG keys found. Please try an alternative method.');
}
const { gpgKey } = await inquirer.prompt([{
type: 'list',
name: 'gpgKey',
message: 'Select a GPG key:',
choices: gpgChoices
}]);
recipientId = `gpg:${gpgKey}`;
}
// Get paste options
const { isTemp, isPgp } = await inquirer.prompt([
{
type: 'confirm',
name: 'isTemp',
message: 'Create a one-time paste (deleted after first view)?',
default: false
},
{
type: 'confirm',
name: 'isPgp',
message: 'Use PGP encryption? (If no, RSA/AES hybrid encryption will be used)',
default: recipient === 'gpg' || recipientId?.startsWith('gpg:') || false
}
]);
// Handle different encryption methods
let encryptedContent;
if (recipient === null) {
// Encrypt for self
encryptedContent = await encryptContent(Buffer.from(message), null, isPgp);
} else if (recipient === 'gpg' || recipientId?.startsWith('gpg:')) {
// Export the GPG key and use it directly with timeout protection
const gpgKeyId = recipientId.replace(/^gpg:/, '').trim();
console.log(chalk.blue(`\nPreparing to export GPG key ${gpgKeyId}...`));
// Use the safe export function that has timeout protection
const { safeExportGpgKey } = await import('./encryptionHelpers.js');
const exportResult = await safeExportGpgKey(gpgKeyId, 12000); // 12 second timeout
if (!exportResult.success) {
let errorMessage = '\nFailed to export GPG key';
if (exportResult.timedOut) {
errorMessage = '\nGPG key export operation timed out. This could indicate GPG agent is hanging.';
console.log(chalk.yellow(errorMessage));
console.log(chalk.blue('\nTrying again with alternative encryption...'));
// Fall back to standard encryption if GPG export times out
const { fallbackOption } = await inquirer.prompt([{
type: 'list',
name: 'fallbackOption',
message: 'Select an alternative encryption method:',
choices: [
{ name: 'Encrypt with password', value: 'password' },
{ name: 'Cancel operation', value: 'cancel' }
]
}]);
if (fallbackOption === 'cancel') {
return { success: false, message: 'Operation canceled by user' };
}
// Use password-based encryption as fallback
return await continueWithEncryption('password', message);
} else if (exportResult.error) {
errorMessage = `\nGPG key export error: ${exportResult.error}`;
}
console.log(chalk.red(errorMessage));
return { success: false, message: 'Failed to export GPG key' };
}
const keyContent = exportResult.content;
console.log(chalk.green('\nā GPG key exported successfully!'));
console.log(chalk.blue('\nEncrypting message with PGP...'));
// Use PGP encryption (always with GPG keys)
encryptedContent = await encryptContent(Buffer.from(message), `gpg-user-${gpgKeyId}`, true);
} else {
// Encrypt for recipient
encryptedContent = await encryptContent(Buffer.from(message), recipientId, isPgp);
}
return {
success: true,
content: encryptedContent,
recipient: recipientId,
temp: isTemp,
pgp: isPgp
};
} catch (error) {
return {
success: false,
message: `Error sending message: ${error.message}`
};
}
}
// Add a preload function that can be called from the CLI entry point
// This allows the CLI to avoid hanging on startup
export async function preloadEnhancedMode() {
// Just check if we're in enhanced mode without loading heavy modules
const enhancedMode = process.argv.includes('--enhanced');
if (!enhancedMode) {
// If not in enhanced mode, don't load anything
return false;
}
// Otherwise, preload the modules we'll need
console.log(chalk.blue('Loading enhanced mode modules...'));
return await initModules();
}
export {
enhancedKeyManagement,
searchAndListKeys,
addOrImportKey,
generateNewKey,
viewKeyDetails,
exportKeys,
removeKeys,
runDiagnostics,
enhancedInteractiveSend
};