UNPKG

dedpaste

Version:

CLI pastebin application using Cloudflare Workers and R2

1,287 lines (1,095 loc) 79.5 kB
#!/usr/bin/env node import { program } from 'commander'; import fetch from 'node-fetch'; import { lookup } from 'mime-types'; import fs from 'fs'; import path from 'path'; import { fileURLToPath } from 'url'; import { createRequire } from 'module'; import { promises as fsPromises } from 'fs'; import { homedir, tmpdir } from 'os'; import inquirer from 'inquirer'; // Import clipboardy with error handling let clipboard; try { clipboard = await import('clipboardy'); // Clipboard module loaded successfully } catch (error) { console.error(`Failed to load clipboardy: ${error.message}`); // Fallback implementation if clipboard fails to load clipboard = { writeSync: (text) => { console.error('Clipboard access is not available. URL could not be copied.'); console.log(`Manual copy: ${text}`); } }; } // Import our core modules import { generateKeyPair, addFriendKey, listKeys, getKey, removeKey, updateLastUsed, addPgpKey, addKeybaseKey } from './keyManager.js'; import { encryptContent, decryptContent } from './encryptionUtils.js'; import { interactiveKeyManagement, interactiveListKeys, interactiveAddFriend, interactiveRemoveKey, interactiveExportKey, interactiveSend } from './interactiveMode.js'; // Import PGP and Keybase utilities import { fetchPgpKey, importPgpKey, addPgpKeyFromServer } from './pgpUtils.js'; import { fetchKeybaseUser, fetchKeybasePgpKey, verifyKeybaseProofs, addKeybaseKey as fetchAndAddKeybaseKey } from './keybaseUtils.js'; const require = createRequire(import.meta.url); const packageJson = require('../package.json'); // Default API URL - can be changed via environment variable const API_URL = process.env.DEDPASTE_API_URL || 'https://paste.d3d.dev'; program .name('dedpaste') .description('CLI client for DedPaste, a simple pastebin service') .version(packageJson.version) .addHelpText('before', ` DedPaste - Secure pastebin with end-to-end encryption USAGE: $ dedpaste Create a paste from stdin $ dedpaste < file.txt Create a paste from a file $ dedpaste --file path/to/file Create a paste from a specific file $ dedpaste --temp Create a one-time paste (deleted after viewing) $ dedpaste --encrypt Create an encrypted paste $ dedpaste keys Manage encryption keys $ dedpaste keys:enhanced Manage keys with enhanced interactive UI $ dedpaste send Create and send an encrypted paste $ dedpaste get <url-or-id> Retrieve and display a paste $ dedpaste completion Generate shell completion scripts EXAMPLES: $ echo "Hello, world!" | dedpaste $ dedpaste --file secret.txt --temp --encrypt $ dedpaste keys --gen-key $ dedpaste keys:enhanced # Use the enhanced UI for key management $ dedpaste send --encrypt --for alice --temp $ dedpaste get https://paste.d3d.dev/AbCdEfGh $ dedpaste completion --bash > ~/.dedpaste-completion.bash `); // Add a command to manage keys // Add a special direct command for enhanced mode program .command('keys:enhanced') .description('Manage encryption keys in enhanced interactive TUI mode') .action(async () => { try { const { spawn } = await import('child_process'); const { fileURLToPath } = await import('url'); const path = await import('path'); // Get the path to the current module directory const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const enhancedLauncherPath = path.join(__dirname, 'enhancedModeLauncher.js'); console.log('Starting enhanced mode...'); // Spawn the launcher as a separate process const enhancedProcess = spawn('node', [enhancedLauncherPath], { stdio: 'inherit', env: process.env }); // Handle process events process.on('SIGINT', () => { console.log('\nTerminating enhanced mode...'); enhancedProcess.kill('SIGTERM'); // Allow clean exit setTimeout(() => process.exit(0), 300); }); // Wait for the process to complete with a timeout await new Promise((resolve, reject) => { // Set a timeout to prevent hanging const timeout = setTimeout(() => { console.log('\nEnhanced mode is taking too long. Terminating...'); enhancedProcess.kill('SIGTERM'); reject(new Error('Enhanced mode timed out')); }, 60000); // 60 second timeout enhancedProcess.on('exit', (code) => { clearTimeout(timeout); if (code === 0) { resolve(); } else { reject(new Error(`Enhanced mode exited with code ${code}`)); } }); enhancedProcess.on('error', (err) => { clearTimeout(timeout); reject(err); }); }); } catch (error) { console.error(`Error running enhanced mode: ${error.message}`); process.exit(1); } }); program .command('keys') .description('Manage encryption keys for secure communication') .option('--interactive', 'Use interactive menu-driven mode for key management') .option('--enhanced', 'Use enhanced interactive TUI mode with color and advanced features') .option('--list', 'List all your keys and friends\' keys with fingerprints') .option('--add-friend <name>', 'Add a friend\'s public key (requires --key-file)') .option('--key-file <path>', 'Path to key file for import/export operations') .option('--export', 'Export your public key to share with friends') .option('--remove <name>', 'Remove a friend\'s key from your keyring') .option('--gen-key', 'Generate a new RSA key pair for encryption') .option('--my-key', 'Output your public key to the console for sharing') .option('--diagnostics', 'Run diagnostics on your key configuration and show a report') .option('--search <query>', 'Search for keys by name, email, or fingerprint') .option('--details <id>', 'Show detailed information about a specific key') .option('--backup <dir>', 'Backup all keys to the specified directory') // PGP options .option('--pgp-key <email-or-id>', 'Fetch and add a PGP key from a keyserver') .option('--pgp-name <name>', 'Custom name for the PGP key (optional)') .option('--import-pgp-key <path>', 'Import a PGP private key for encryption/decryption') .option('--pgp-passphrase <phrase>', 'Passphrase for PGP private key') .option('--native-pgp', 'Use native PGP encryption instead of converting to PEM') .option('--from-gpg <key-id>', 'Import a key from your GPG keyring') .option('--gpg-import <path>', 'Import a key to your GPG keyring') // Keybase options .option('--keybase <username>', 'Fetch and add a Keybase user\'s PGP key') .option('--keybase-name <name>', 'Custom name for the Keybase user\'s key (optional)') .option('--no-verify', 'Skip verification of Keybase proofs') // Debugging and logging options .option('--verbose', 'Enable verbose logging (same as --log-level debug)') .option('--debug', 'Enable debug mode with extensive logging (same as --log-level trace)') .option('--log-level <level>', 'Set logging level (error, warn, info, debug, trace)') .option('--log-file <path>', 'Log to the specified file') .option('--no-log-file', 'Disable logging to file') .addHelpText('after', ` Examples: $ dedpaste keys --gen-key # Generate a new key pair $ dedpaste keys --list # List all your keys $ dedpaste keys --add-friend alice --key-file alice_public.pem # Add a friend's key $ dedpaste keys --my-key # Display your public key $ dedpaste keys --interactive # Use interactive mode $ dedpaste keys:enhanced # Use enhanced UI (recommended) PGP Integration: $ dedpaste keys --pgp-key user@example.com # Add a PGP key from keyservers $ dedpaste keys --pgp-key 0x1234ABCD # Add a PGP key using key ID $ dedpaste keys --pgp-key user@example.com --pgp-name alice # Add with custom name Keybase Integration: $ dedpaste keys --keybase username # Add a Keybase user's key $ dedpaste keys --keybase username --keybase-name bob # Add with custom name $ dedpaste keys --keybase username --no-verify # Skip verification of proofs Key Storage: - Your keys are stored in ~/.dedpaste/keys/ - Friend keys are stored in ~/.dedpaste/friends/ - PGP keys are stored in ~/.dedpaste/pgp/ - Keybase keys are stored in ~/.dedpaste/keybase/ - Key database is at ~/.dedpaste/keydb.json `) .action(async (options) => { try { // Initialize logger based on options let logLevel = 'info'; if (options.verbose) logLevel = 'debug'; if (options.debug) logLevel = 'trace'; if (options.logLevel) logLevel = options.logLevel; const logOptions = { level: logLevel, logToFile: options.logFile !== false, logFile: options.logFile || 'dedpaste.log' }; // Import and initialize logger const { initialize: initLogger } = await import('./logger.js'); const logger = await initLogger(logOptions); // Log the start of execution logger.info('Starting key management operation', { options }); // Enhanced interactive mode takes precedence but we need to ensure it doesn't hang if (options.enhanced) { logger.debug('Enhanced mode requested - using non-blocking startup', { options }); console.log('Starting enhanced mode...'); console.log('Debug info: enhanced flag =', options.enhanced); // Create a completely isolated process for enhanced mode // This avoids all potential circular dependencies and module loading issues try { // Use child_process to launch the enhanced mode in a separate process // This guarantees that any hanging in module initialization won't affect the main CLI const childProcess = await import('child_process'); const { spawn } = childProcess; // Get the current module directory where our launcher is located const cliDir = path.dirname(fileURLToPath(import.meta.url)); const launcherPath = path.join(cliDir, 'enhancedModeLauncher.js'); // Verify that the launcher exists if (!fs.existsSync(launcherPath)) { logger.error('Enhanced mode launcher not found', { path: launcherPath }); throw new Error(`Enhanced mode launcher not found at: ${launcherPath}`); } // Spawn the launcher as a separate process with the right working directory const enhancedProcess = spawn('node', [launcherPath, '--debug'], { stdio: 'inherit', env: process.env, cwd: cliDir // Use CLI directory as working directory }); // Process termination handling process.on('SIGINT', () => { console.log('\nTerminating enhanced mode...'); enhancedProcess.kill('SIGTERM'); // Allow clean exit setTimeout(() => process.exit(0), 300); }); // Wait for the process to complete with a timeout await new Promise((resolve, reject) => { // Set a timeout to prevent hanging const timeout = setTimeout(() => { console.log('\nEnhanced mode is taking too long. Terminating...'); enhancedProcess.kill('SIGTERM'); reject(new Error('Enhanced mode timed out')); }, 30000); // 30 second timeout enhancedProcess.on('exit', (code) => { clearTimeout(timeout); if (code === 0) { resolve(); } else { reject(new Error(`Enhanced mode exited with code ${code}`)); } }); enhancedProcess.on('error', (err) => { clearTimeout(timeout); reject(err); }); }); // No temp file to clean up since we're using a dedicated launcher file } catch (enhancedError) { logger.error('Enhanced mode failed', { error: enhancedError.message, stack: enhancedError.stack }); console.error(`Error running enhanced mode: ${enhancedError.message}`); console.error('Stack trace:', enhancedError.stack); console.log('Falling back to standard interactive mode...'); // Fall back to regular interactive mode const result = await interactiveKeyManagement(); if (result.message) { console.log(result.message); } } return; } // Regular interactive mode if (options.interactive) { logger.debug('Entering interactive mode'); const result = await interactiveKeyManagement(); if (result.message) { console.log(result.message); } return; } // Run diagnostics if (options.diagnostics) { logger.debug('Running key diagnostics'); const { runKeyDiagnostics, formatDiagnosticsReport } = await import('./keyDiagnostics.js'); console.log('Running key system diagnostics... Please wait...'); const results = await runKeyDiagnostics(); const report = formatDiagnosticsReport(results); console.log(report); if (results.status !== 'ok') { logger.warn('Diagnostics found issues', { errors: results.errors.length, warnings: results.warnings.length }); // Suggest fixing issues console.log('\nTo fix issues automatically, use:'); console.log('dedpaste keys --enhanced'); console.log('Then select "Run diagnostics" and follow the prompts.'); } return; } // Search for keys if (options.search) { logger.debug('Searching for keys', { query: options.search }); const { searchKeys } = await import('./unifiedKeyManager.js'); const keys = await searchKeys(options.search, { includeGpg: true }); if (keys.length === 0) { console.log(`No keys found matching "${options.search}"`); return; } console.log(`\nFound ${keys.length} keys matching "${options.search}":\n`); for (const key of keys) { console.log(`- ${key.name} (${key.type.toUpperCase()})`); if (key.email) { console.log(` Email: ${key.email}`); } if (key.username) { console.log(` Username: ${key.username}`); } console.log(` Fingerprint: ${key.fingerprint}`); console.log(` Source: ${key.source}`); if (key.created) { console.log(` Created: ${new Date(key.created).toLocaleString()}`); } console.log(''); } return; } // Show detailed key information if (options.details) { logger.debug('Showing key details', { keyId: options.details }); const { getKeyById, readKeyContent } = await import('./unifiedKeyManager.js'); const keyInfo = await getKeyById(options.details, { includeGpg: true }); if (!keyInfo) { console.error(`Key "${options.details}" not found`); process.exit(1); } console.log(`\nKey details for "${keyInfo.name}":\n`); console.log(`- ID: ${keyInfo.id}`); console.log(`- Name: ${keyInfo.name}`); console.log(`- Type: ${keyInfo.type.toUpperCase()}`); console.log(`- Source: ${keyInfo.source}`); console.log(`- Fingerprint: ${keyInfo.fingerprint}`); if (keyInfo.email) { console.log(`- Email: ${keyInfo.email}`); } if (keyInfo.username) { console.log(`- Username: ${keyInfo.username}`); } if (keyInfo.created) { console.log(`- Created: ${new Date(keyInfo.created).toLocaleString()}`); } if (keyInfo.lastUsed) { console.log(`- Last Used: ${new Date(keyInfo.lastUsed).toLocaleString()}`); } if (keyInfo.path) { if (typeof keyInfo.path === 'object') { console.log(`- Public Key Path: ${keyInfo.path.public}`); console.log(`- Private Key Path: ${keyInfo.path.private}`); } else { console.log(`- Key Path: ${keyInfo.path}`); } } // Ask if user wants to see key content if (keyInfo.source === 'self' || keyInfo.source === 'friend' || keyInfo.source === 'pgp' || keyInfo.source === 'keybase') { const { default: inquirer } = await import('inquirer'); const { showContent } = await inquirer.prompt([{ type: 'confirm', name: 'showContent', message: 'Show key content?', default: false }]); if (showContent) { // For self keys, ask if they want to see private key let showPrivate = false; if (keyInfo.source === 'self') { const { viewPrivate } = await inquirer.prompt([{ type: 'confirm', name: 'viewPrivate', message: 'Show private key? (This is sensitive information)', default: false }]); showPrivate = viewPrivate; } // Read the key content const keyContent = await readKeyContent(keyInfo, { private: showPrivate }); if (keyContent) { console.log('\n--- Key Content ---\n'); console.log(keyContent); console.log('\n-------------------\n'); } else { console.error('Could not read key content'); } } } return; } // Backup keys if (options.backup) { logger.debug('Backing up keys', { directory: options.backup }); const backupDir = options.backup; // Create backup directory if it doesn't exist await fsPromises.mkdir(backupDir, { recursive: true }); // Get timestamp for backup files const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); // Get all keys const db = await loadKeyDatabase(); let backupCount = 0; // Backup self key if it exists if (db.keys.self) { const privateKeyDest = path.join(backupDir, `self-private-${timestamp}.pem`); const publicKeyDest = path.join(backupDir, `self-public-${timestamp}.pem`); await fsPromises.copyFile(db.keys.self.private, privateKeyDest); await fsPromises.copyFile(db.keys.self.public, publicKeyDest); backupCount += 2; console.log(`Backed up self key to ${privateKeyDest} and ${publicKeyDest}`); } // Backup friend keys for (const [name, info] of Object.entries(db.keys.friends)) { const friendKeyDest = path.join(backupDir, `friend-${name}-${timestamp}.pem`); await fsPromises.copyFile(info.public, friendKeyDest); backupCount++; console.log(`Backed up friend key "${name}" to ${friendKeyDest}`); } // Backup PGP keys for (const [name, info] of Object.entries(db.keys.pgp || {})) { const pgpKeyDest = path.join(backupDir, `pgp-${name}-${timestamp}.asc`); await fsPromises.copyFile(info.path, pgpKeyDest); backupCount++; console.log(`Backed up PGP key "${name}" to ${pgpKeyDest}`); } // Backup Keybase keys for (const [name, info] of Object.entries(db.keys.keybase || {})) { const keybaseKeyDest = path.join(backupDir, `keybase-${name}-${timestamp}.asc`); await fsPromises.copyFile(info.path, keybaseKeyDest); backupCount++; console.log(`Backed up Keybase key "${name}" to ${keybaseKeyDest}`); } // Backup key database const dbDest = path.join(backupDir, `keydb-${timestamp}.json`); await fsPromises.copyFile(path.join(homedir(), '.dedpaste', 'keydb.json'), dbDest); console.log(`Backed up key database to ${dbDest}`); console.log(`\nBackup complete: ${backupCount} keys backed up to ${backupDir}`); return; } // Import a key from GPG keyring if (options.fromGpg) { logger.debug('Importing key from GPG keyring', { keyId: options.fromGpg }); const { importKey } = await import('./unifiedKeyManager.js'); const name = options.pgpName || options.fromGpg; console.log(`Importing key ${options.fromGpg} from GPG keyring...`); const result = await importKey({ source: 'gpg-keyring', keyId: options.fromGpg, name: name }); if (result.success) { console.log(`\n✓ Imported key from GPG keyring: ${result.name}`); console.log(` - Type: ${result.type}`); console.log(` - Fingerprint: ${result.fingerprint}`); if (result.email) console.log(` - Email: ${result.email}`); console.log(` - Stored at: ${result.path}`); } else { console.error(`Error importing key from GPG keyring: ${result.error}`); process.exit(1); } return; } // Import a key to GPG keyring if (options.gpgImport) { logger.debug('Importing key to GPG keyring', { file: options.gpgImport }); const { importKey } = await import('./unifiedKeyManager.js'); console.log(`Importing key from ${options.gpgImport} to GPG keyring...`); const result = await importKey({ source: 'gpg-import', file: options.gpgImport }); if (result.success) { console.log(`\n✓ Key imported to GPG keyring successfully`); console.log(` - Key ID: ${result.keyId}`); console.log('\nOutput from GPG:'); console.log(result.output); } else { console.error(`Error importing key to GPG keyring: ${result.error}`); process.exit(1); } return; } // List keys if (options.list) { logger.debug('Listing all keys'); await interactiveListKeys(); return; } // Add a friend's key if (options.addFriend) { if (!options.keyFile) { console.error('Error: --key-file is required when adding a friend'); process.exit(1); } logger.debug('Adding friend key', { name: options.addFriend, file: options.keyFile }); try { const keyContent = await fsPromises.readFile(options.keyFile, 'utf8'); const keyPath = await addFriendKey(options.addFriend, keyContent); console.log(`Added ${options.addFriend}'s public key at ${keyPath}`); } catch (error) { logger.error('Failed to add friend key', { error: error.message }); console.error(`Error adding friend's key: ${error.message}`); process.exit(1); } return; } // Export public key if (options.export) { logger.debug('Exporting public key'); const result = await interactiveExportKey(); if (result.message) { console.log(result.message); } return; } // Remove a key if (options.remove) { logger.debug('Removing key', { name: options.remove }); try { // Try to remove from any collection const success = await removeKey('any', options.remove); if (success) { console.log(`Removed ${options.remove}'s key successfully`); } else { console.error(`Key "${options.remove}" not found`); process.exit(1); } } catch (error) { logger.error('Failed to remove key', { error: error.message }); console.error(`Error removing key: ${error.message}`); process.exit(1); } return; } // Fetch and add a PGP key from keyservers if (options.pgpKey) { logger.debug('Fetching PGP key', { identifier: options.pgpKey }); try { console.log(`Fetching PGP key for "${options.pgpKey}" from keyservers...`); const name = options.pgpName || options.pgpKey; const result = await addPgpKeyFromServer(options.pgpKey, name); console.log(` ✓ Added PGP key: - Name: ${result.name} - Email: ${result.email || 'Not specified'} - Key ID: ${result.keyId} - Stored at: ${result.path} `); } catch (error) { logger.error('Failed to fetch PGP key', { error: error.message }); console.error(`Error fetching PGP key: ${error.message}`); process.exit(1); } return; } // Import a PGP private key if (options.importPgpKey) { // Check if passphrase was provided if (!options.pgpPassphrase) { console.error('Error: --pgp-passphrase is required when importing a PGP private key'); process.exit(1); } logger.debug('Importing PGP private key', { file: options.importPgpKey }); try { // Read the PGP private key file const pgpPrivateKeyContent = await fsPromises.readFile(options.importPgpKey, 'utf8'); // Import the PGP private key console.log(`Importing PGP private key from ${options.importPgpKey}...`); const result = await importPgpPrivateKey(pgpPrivateKeyContent, options.pgpPassphrase); // Save the private and public key to the PGP key directory const { PGP_KEY_DIR } = await ensureDirectories(); const privateKeyPath = path.join(PGP_KEY_DIR, `private.pem`); const publicKeyPath = path.join(PGP_KEY_DIR, `public.pem`); // Write the keys to files await fsPromises.writeFile(privateKeyPath, result.privateKey); await fsPromises.writeFile(publicKeyPath, result.publicKey); // Update the key database const db = await loadKeyDatabase(); db.keys.pgp.self = { private: privateKeyPath, public: publicKeyPath, original: options.importPgpKey, fingerprint: result.keyId, name: result.name, email: result.email, created: new Date().toISOString() }; await saveKeyDatabase(db); console.log(` ✓ Imported PGP private key: - Name: ${result.name} - Email: ${result.email || 'Not specified'} - Key ID: ${result.keyId} - Converted private key: ${privateKeyPath} - Converted public key: ${publicKeyPath} - Original PGP key: ${options.importPgpKey} `); } catch (error) { logger.error('Failed to import PGP private key', { error: error.message }); console.error(`Error importing PGP private key: ${error.message}`); process.exit(1); } return; } // Fetch and add a Keybase user's key if (options.keybase) { logger.debug('Fetching Keybase key', { username: options.keybase }); try { console.log(`Fetching Keybase key for user "${options.keybase}"...`); if (options.verify) { console.log('Verifying user proofs on Keybase...'); } const name = options.keybaseName || `keybase:${options.keybase}`; const result = await fetchAndAddKeybaseKey(options.keybase, name, options.verify); console.log(` ✓ Added Keybase key: - Name: ${result.name} - Keybase username: ${result.username} - Email: ${result.email || 'Not specified'} - Key ID: ${result.keyId} - Stored at: ${result.path} `); } catch (error) { logger.error('Failed to fetch Keybase key', { error: error.message }); console.error(`Error fetching Keybase key: ${error.message}`); process.exit(1); } return; } // Generate a new key pair if (options.genKey) { logger.debug('Generating new key pair'); const { privateKeyPath, publicKeyPath } = await generateKeyPair(); console.log(` ✓ Generated new key pair: - Private key: ${privateKeyPath} - Public key: ${publicKeyPath} `); return; } // Output public key to console if (options.myKey) { logger.debug('Showing self public key'); const selfKey = await getKey('self'); if (!selfKey) { console.error('No personal key found. Generate one with --gen-key first.'); process.exit(1); } try { const publicKeyContent = await fsPromises.readFile(selfKey.public, 'utf8'); console.log('\nYour public key:'); console.log('----------------'); console.log(publicKeyContent); console.log('----------------'); console.log('\nShare this key with your friends so they can send you encrypted pastes.'); } catch (error) { logger.error('Failed to read public key', { error: error.message }); console.error(`Error reading public key: ${error.message}`); process.exit(1); } return; } // If no specific action is provided, show help program.commands.find(cmd => cmd.name() === 'keys').help(); } catch (error) { console.error('Error:', error.message); process.exit(1); } }); // Add a command to send a paste program .command('send') .description('Create and send an encrypted paste to friends') .option('-t, --temp', 'Create a one-time paste that is deleted after being viewed') .option('--type <content-type>', 'Specify the content type of the paste (e.g., application/json)') .option('-f, --file <path>', 'Upload a file from the specified path instead of stdin') .option('-o, --output', 'Print only the URL (without any additional text, useful for scripts)') .option('-e, --encrypt', 'Encrypt the content before uploading (requires key setup)') .option('--for <friend>', 'Encrypt for a specific friend (requires adding their key first)') .option('--list-friends', 'List available friends you can encrypt messages for') .option('--key-file <path>', 'Path to public key for encryption (alternative to stored keys)') .option('--gen-key', 'Generate a new key pair for encryption if you don\'t have one') .option('--interactive', 'Use interactive mode with guided prompts for message creation') .option('--enhanced', 'Use enhanced interactive mode with advanced key selection features') .option('--debug', 'Debug mode: show encrypted content without uploading') .option('-c, --copy', 'Copy the URL to clipboard automatically') // PGP options .option('--pgp', 'Use PGP encryption instead of hybrid RSA/AES') .option('--pgp-key-file <path>', 'Use a specific PGP public key file for encryption') .option('--pgp-armor', 'Output ASCII-armored PGP instead of binary format') .addHelpText('after', ` Examples: $ echo "Secret message" | dedpaste send --encrypt # Encrypt for yourself (RSA/AES) $ echo "For Alice only" | dedpaste send --encrypt --for alice # Encrypt for a friend $ dedpaste send --file secret.txt --encrypt --temp # Encrypt a file as one-time paste $ dedpaste send --interactive --encrypt # Interactive encrypted message $ dedpaste send --enhanced --encrypt # Enhanced interactive mode with full key support $ dedpaste send --list-friends # List available recipients PGP Options: $ dedpaste send --encrypt --for alice@example.com --pgp # Encrypt for PGP key (REQUIRED: specify recipient) $ dedpaste send --encrypt --for alice --pgp --pgp-key-file friend.asc # Use specific PGP key file $ dedpaste send --enhanced --encrypt --pgp # Use enhanced mode with GPG keyring support IMPORTANT: PGP encryption always requires specifying a recipient with --for Encryption: - Standard encryption uses RSA for key exchange and AES-256-GCM for content - PGP encryption (--pgp) uses OpenPGP standard compatible with GnuPG/GPG - Each paste uses a different key for forward secrecy - Encrypted pastes include metadata about sender and recipient - Use --debug to test encryption without uploading `) .action(async (options) => { // Ensure temp flag is properly set by checking command line arguments if (!options.temp && (process.argv.includes('--temp') || process.argv.includes('-t'))) { options.temp = true; } try { // List friends if requested if (options.listFriends) { const db = await listKeys(); const friendNames = Object.keys(db.keys.friends); if (friendNames.length === 0) { console.log('No friend keys found. Add one with "dedpaste keys add-friend"'); return; } console.log('\nAvailable friends:'); for (const name of friendNames) { const friend = db.keys.friends[name]; const lastUsed = new Date(friend.last_used).toLocaleString(); console.log(` - ${name} (last used: ${lastUsed})`); } return; } let content; let contentType; let recipientName = options.for; // Interactive mode if (options.interactive) { // Check if we should use the enhanced interactive mode if (options.enhanced) { console.log('Using enhanced interactive mode for sending...'); const { enhancedInteractiveSend } = await import('./enhancedInteractiveMode.js'); const result = await enhancedInteractiveSend(); if (!result.success) { console.error(`Error: ${result.message}`); process.exit(1); } content = result.content; recipientName = result.recipient; options.temp = result.temp; options.pgp = result.pgp; // Use PGP flag from enhanced mode contentType = 'text/plain'; } else { // Use standard interactive mode const result = await interactiveSend(); content = result.content; recipientName = result.recipient; options.temp = result.temp; contentType = 'text/plain'; } } else { // Fix for file option parsing - ensure it's correctly recognized if (!options.file && (process.argv.includes('--file') || process.argv.includes('-f'))) { // If Commander didn't parse it correctly, get the file path manually const fileArgIndex = Math.max( process.argv.indexOf('--file'), process.argv.indexOf('-f') ); if (fileArgIndex !== -1 && fileArgIndex < process.argv.length - 1) { options.file = process.argv[fileArgIndex + 1]; } } // Additional PGP flag check for send command if (process.argv.includes('--pgp') && !options.pgp) { options.pgp = true; } // Determine if we're reading from a file or stdin if (options.file) { // Read from the specified file if (!fs.existsSync(options.file)) { console.error(`Error: File '${options.file}' does not exist`); process.exit(1); } content = fs.readFileSync(options.file); // Try to detect the content type from the file extension contentType = options.type || lookup(options.file) || 'text/plain'; } else { // Read from stdin const stdinBuffer = []; for await (const chunk of process.stdin) { stdinBuffer.push(chunk); } if (stdinBuffer.length === 0) { console.error('Error: No input provided. Pipe content to dedpaste or use --file option.'); process.exit(1); } content = Buffer.concat(stdinBuffer); contentType = options.type || 'text/plain'; } } // Parse command line arguments and options // Check if encryption is requested (manually check for the flag) const shouldEncrypt = process.argv.includes('--encrypt') || process.argv.includes('-e'); // Ensure file option is recognized correctly - check both parsed options and raw arguments if (!options.file && (process.argv.includes('--file') || process.argv.includes('-f'))) { // If Commander didn't parse it correctly, get the file path manually const fileArgIndex = Math.max( process.argv.indexOf('--file'), process.argv.indexOf('-f') ); if (fileArgIndex !== -1 && fileArgIndex < process.argv.length - 1) { options.file = process.argv[fileArgIndex + 1]; } } // Handle encryption if requested if (shouldEncrypt) { // Generate new keys if requested if (options.genKey) { console.log('Generating new key pair'); const { publicKeyPath, privateKeyPath } = await generateKeyPair(); console.log(` ✓ Generated new key pair: - Private key: ${privateKeyPath} - Public key: ${publicKeyPath} `); } // Encrypt the content try { // Check if PGP mode is requested const usePgp = options.pgp; // Validate encryption parameters // For PGP encryption, always require a recipient if (usePgp && !recipientName) { console.error(`Error: PGP encryption requires specifying a recipient with --for`); console.error(`Please use: dedpaste send --encrypt --pgp --for <recipient_name>`); process.exit(1); } // If PGP key file is provided, read it and use it directly if (options.pgpKeyFile) { if (!recipientName) { console.error(`Error: When using --pgp-key-file, you must specify a recipient with --for`); process.exit(1); } const pgpKeyContent = await fsPromises.readFile(options.pgpKeyFile, 'utf8'); content = await createPgpEncryptedMessage(content, pgpKeyContent, recipientName); } else { // Use the standard encryption flow with PGP option content = await encryptContent(content, recipientName, usePgp); } // Log PGP mode if used if (usePgp || options.pgpKeyFile) { console.log('Using PGP encryption'); } // Set content type to application/json for encrypted content contentType = 'application/json'; } catch (error) { console.error(`Encryption failed: ${error.message}`); // Provide helpful suggestions for common errors if (error.message.includes('PGP key detected') && !options.pgp) { console.log('\nSuggestion: This key appears to be a PGP key. Try adding the --pgp flag:'); console.log(`dedpaste send --encrypt --for ${recipientName} --pgp`); } else if (error.message.includes('requires specifying a recipient')) { console.log('\nFor PGP encryption, always specify a recipient:'); console.log(`dedpaste send --encrypt --pgp --for <recipient_name>`); } process.exit(1); } } else { console.log('No encryption requested'); } // Determine the endpoint based on whether it's a temporary paste and encrypted let endpoint; if (shouldEncrypt) { endpoint = options.temp ? '/e/temp' : '/e/upload'; } else { endpoint = options.temp ? '/temp' : '/upload'; } // In debug mode, just show the encrypted content if (options.debug && shouldEncrypt) { console.log('Debug mode: Showing encrypted content without uploading'); console.log('Encrypted content (JSON):'); console.log(content.toString()); return; } // Make the API request try { const response = await fetch(`${API_URL}${endpoint}`, { method: 'POST', headers: { 'Content-Type': contentType, 'User-Agent': `dedpaste-cli/${packageJson.version}` }, body: content }); if (!response.ok) { console.error(`Error: ${response.status} ${response.statusText}`); const errorText = await response.text(); console.error(errorText); process.exit(1); } const url = await response.text(); // Copy to clipboard if requested if (options.copy) { try { const cleanUrl = url.trim(); if (clipboard.default) { clipboard.default.writeSync(cleanUrl); } else { clipboard.writeSync(cleanUrl); } } catch (error) { console.error(`Unable to copy to clipboard: ${error.message}`); } } // Output the result if (options.output) { console.log(url.trim()); } else { let encryptionMessage = ''; if (shouldEncrypt) { if (recipientName) { encryptionMessage = `🔒 This paste is encrypted for ${recipientName} and can only be decrypted with their private key\n`; } else { encryptionMessage = '🔒 This paste is encrypted and can only be decrypted with your private key\n'; } } console.log(` ✓ Paste created successfully! ${options.temp ? '⚠️ This is a one-time paste that will be deleted after first view\n' : ''} ${encryptionMessage} ${options.copy ? '📋 URL copied to clipboard: ' : '📋 '} ${url.trim()} `); } } catch (error) { console.error(`Network error: ${error.message}`); console.error('If you just want to test encryption without uploading, use the --debug flag'); process.exit(1); } } catch (error) { console.error('Error:', error.message); process.exit(1); } }); // Update the 'get' command to use the new decryption function program .command('get') .description('Retrieve and decrypt a paste by URL or ID') .argument('<url-or-id>', 'URL or ID of the paste to retrieve (e.g., https://paste.d3d.dev/AbCdEfGh or just AbCdEfGh)') .option('--key-file <path>', 'Path to private key for decryption (if not using default key)') .option('--pgp-key-file <path>', 'Path to PGP private key for decryption') .option('--pgp-passphrase <passphrase>', 'Passphrase for PGP private key') .option('--use-gpg-keyring', 'Try to decrypt PGP messages using the system GPG keyring', true) .option('--no-gpg-keyring', 'Disable automatic GPG keyring decryption') .option('--show-metadata', 'Show detailed metadata about the encrypted message') .option('--interactive', 'Use interactive mode for decryption (will prompt for passphrase)') .addHelpText('after', ` Examples: $ dedpaste get https://paste.d3d.dev/AbCdEfGh # Get a regular paste by URL $ dedpaste get AbCdEfGh # Get a regular paste by ID $ dedpaste get https://paste.d3d.dev/e/AbCdEfGh # Get and decrypt an encrypted paste $ dedpaste get e/AbCdEfGh # Get and decrypt an encrypted paste by ID $ dedpaste get e/AbCdEfGh --interactive # Use interactive mode with password prompt $ dedpaste get e/AbCdEfGh --show-metadata # Show detailed metadata about the paste PGP Decryption: $ dedpaste get e/AbCdEfGh --pgp-key-file my.pgp # Decrypt with PGP private key $ dedpaste get e/AbCdEfGh --pgp-key-file my.pgp --pgp-passphrase "secret" $ dedpaste get e/AbCdEfGh # Automatically try GPG keyring for PGP content $ dedpaste get e/AbCdEfGh --no-gpg-keyring # Disable automatic GPG keyring usage $ dedpaste get e/AbCdEfGh --interactive # Interactive mode with passphrase prompt URL Format: - Regular pastes: https://paste.d3d.dev/{id} - Encrypted pastes: https://paste.d3d.dev/e/{id} - The CLI automatically detects encrypted pastes and attempts decryption Decryption: - Encrypted pastes are automatically decrypted if you have the correct private key - PGP encrypted pastes will first try the system GPG keyring (if available) - If GPG keyring fails, a PGP private key and passphrase will be required - Metadata about sender and creation time is displayed when available - One-time pastes are deleted from the server after viewing GPG Keyring Integration: - By default, dedpaste will attempt to use your GPG keyring for PGP-encrypted pastes - This allows decryption without explicitly providing a private key file - Your system's gpg command is used to perform the decryption - To disable this feature, use the --no-gpg-keyring flag `) .action(async (urlOrId, options) => { try { // Extract ID and check if it's an encrypted paste let id = urlOrId; let isEncrypted = false; // Parse the URL or ID if (urlOrId.startsWith('http')) { // It's a URL const url = new URL(urlOrId); const path = url.pathname; // Check if it's an encrypted paste const encryptedMatch = path.match(/^\/e\/([a-zA-Z0-9]{8})$/); if (encryptedMatch) { id = encryptedMatch[1]; isEncrypted = true; } else { const regularMatch = path.match(/^\/([a-zA-Z0-9]{8})$/); if (regularMatch) { id = regularMatch[1]; } else { console.error('Invalid paste URL format'); process.exit(1); } } } else if (urlOrId.startsWith('e/') && urlOrId.length === 10) { // It's an encrypted ID in the format "e/AbCdEfGh" id = urlOrId.substring(2); isEncrypted = true; } else if (id.length === 8) { // It's just a regular ID isEncrypted = false; } else { console.error('Invalid paste ID format'); console.error('Valid formats: https://paste.d3d.dev/AbCdEfGh, AbCdEfGh, https://paste.d3d.dev/e/AbCdEfGh, e/AbCdEfGh'); process.exit(1); } // Determine the URL to fetch const fetchUrl = isEncrypted ? `${API_URL}/e/${id}` : `${API_URL}/${id}`; console.log(`Fetching paste from ${fetchUrl}...`); // Fetch the paste const response = await fetch(fetchUrl); if (!response.ok) { console.error(`Error: ${response.status} ${response.statusText}`); const errorText = await response.text(); console.error(errorText); // Provide more helpful error messages if (response.status === 404) { console.error('The paste was not found. It may have been deleted or expired.'); } else if (response.status === 429) { console.error('Too many requests. Please try again later.'); } process.exit(1); } // Get the content const content = await response.arrayBuffer(); const contentBuffer = Buffer.from(content); // If it's encrypted, decrypt it if (isEncrypted) { console.log('🔒 This paste is encrypted'); try { let result; // Determine whether to use GPG keyring const useGpgKeyring = options.useGpgKeyring !== false; // Check if we're in interactive mode if (options.interactive) { console.log('Using interactive mode for decryption...'); // Import the inquirer module const inquirer = await import('inquirer'); // Check if PGP key file is provided or should be prompted let pgpKeyFile = options.pgpKeyFile; let passphrase = options.pgpPassphrase; // If no key file is provided but we're in interactive mode, ask if user wants to use one if (!pgpKeyFile) { const { usePgpKey } = await inquirer.default.prompt([{ type: 'confirm', name: 'usePgpKey', message: 'Do you want to use a PGP private key file for decryption?', default: false }]); if (usePgpKey) { const { keyFile } = await inquirer.default.prompt([{ type: 'input', name: 'keyFile', message: 'Enter the path to your PGP private key file:', validate: (input) => { if (!input) return 'Path cannot be empty'; if (!fs.existsSync(input)) return 'File does not exist'; return true;