UNPKG

dedpaste

Version:

CLI pastebin application using Cloudflare Workers and R2

803 lines (701 loc) 23.1 kB
// Unified key management interface import fs from 'fs'; import path from 'path'; import { promises as fsPromises } from 'fs'; import { homedir } from 'os'; import crypto from 'crypto'; import inquirer from 'inquirer'; import chalk from 'chalk'; // Import our specific key management modules import * as keyManager from './keyManager.js'; import * as pgpUtils from './pgpUtils.js'; import * as keybaseUtils from './keybaseUtils.js'; import { checkGpgKeyring, findKeysMatchingCriteria } from './keyDiagnostics.js'; // Constants for key types const KEY_TYPES = { RSA: 'rsa', PGP: 'pgp', KEYBASE: 'keybase', GPG: 'gpg' }; /** * Initialize the key system * @returns {Promise<Object>} - Initialization result */ async function initialize() { try { // Create necessary directories const dirs = await keyManager.ensureDirectories(); // Initialize key database const db = await keyManager.loadKeyDatabase(); // Check GPG keyring const gpgStatus = await checkGpgKeyring(); return { success: true, directories: dirs, database: db, gpg: gpgStatus }; } catch (error) { return { success: false, error: error.message }; } } /** * Search for keys with fuzzy matching * @param {string} query - Search term * @param {Object} options - Search options * @returns {Promise<Array>} - Matching keys */ async function searchKeys(query, options = {}) { try { const db = await keyManager.loadKeyDatabase(); const results = []; // Prepare a regex for fuzzy searching // Escape special regex characters in the query const escapedQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Convert to a fuzzy pattern (allows characters between) const fuzzyPattern = escapedQuery.split('').join('.*'); const regex = new RegExp(fuzzyPattern, 'i'); // Helper function to check if a field matches the query const fieldMatches = (value) => { if (!value) return false; if (typeof value === 'string') { return regex.test(value); } return false; }; // Check self key if requested if (!options.skipSelf && db.keys.self) { const selfKey = db.keys.self; // Construct a friendly name for the self key const selfName = 'Self Key'; // Check if it matches the query if ( fieldMatches(selfName) || fieldMatches(selfKey.fingerprint) ) { results.push({ id: 'self', name: selfName, type: KEY_TYPES.RSA, fingerprint: selfKey.fingerprint, created: selfKey.created, source: 'self', path: selfKey.public }); } } // Check friend keys for (const [name, info] of Object.entries(db.keys.friends || {})) { // Check if it matches the query if ( fieldMatches(name) || fieldMatches(info.fingerprint) ) { results.push({ id: name, name: name, type: KEY_TYPES.RSA, fingerprint: info.fingerprint, created: info.added, lastUsed: info.last_used, source: 'friend', path: info.public }); } } // Check PGP keys for (const [name, info] of Object.entries(db.keys.pgp || {})) { // Check if it matches the query if ( fieldMatches(name) || fieldMatches(info.fingerprint) || fieldMatches(info.email) ) { results.push({ id: name, name: name, type: KEY_TYPES.PGP, fingerprint: info.fingerprint, email: info.email, created: info.added, lastUsed: info.last_used, source: 'pgp', path: info.path }); } } // Check Keybase keys for (const [name, info] of Object.entries(db.keys.keybase || {})) { // Check if it matches the query if ( fieldMatches(name) || fieldMatches(info.fingerprint) || fieldMatches(info.email) || fieldMatches(info.username) ) { results.push({ id: name, name: name, type: KEY_TYPES.KEYBASE, fingerprint: info.fingerprint, email: info.email, username: info.username, created: info.added, lastUsed: info.last_used, source: 'keybase', path: info.path }); } } // Check GPG keys if enabled if (options.includeGpg) { const gpgInfo = await checkGpgKeyring(); if (gpgInfo.available && gpgInfo.keys.length > 0) { for (const key of gpgInfo.keys) { // Extract user information from the first UID const uidInfo = key.uids.length > 0 ? key.uids[0] : { uid: 'Unknown' }; const uidString = uidInfo.uid; // Extract email from UID (if present) let email = null; const emailMatch = uidString.match(/<([^>]+)>/); if (emailMatch) { email = emailMatch[1]; } // Extract name from UID (if present) let name = uidString; if (emailMatch) { name = uidString.replace(/<[^>]+>/, '').trim(); } // Check if it matches the query if ( fieldMatches(key.id) || fieldMatches(uidString) || fieldMatches(email) || fieldMatches(name) ) { results.push({ id: key.id, name: name, type: KEY_TYPES.GPG, fingerprint: key.id, email: email, created: key.created, expires: key.expires, trust: key.trust, uids: key.uids.map(u => u.uid), source: 'gpg', gpgKey: key }); } } } } return results; } catch (error) { console.error(`Error searching keys: ${error.message}`); return []; } } /** * Get a key by ID with full details * @param {string} id - Key identifier * @param {Object} options - Options * @returns {Promise<Object>} - Key details or null if not found */ async function getKeyById(id, options = {}) { try { // Handle special case for 'self' if (id === 'self') { const selfKey = await keyManager.getKey('self'); if (!selfKey) { return null; } return { id: 'self', name: 'Self Key', type: KEY_TYPES.RSA, fingerprint: selfKey.fingerprint, created: selfKey.created, source: 'self', path: { public: selfKey.public, private: selfKey.private }, raw: selfKey }; } // Try to find in all collections const key = await keyManager.getKey('any', id); if (key) { // Determine the type and build response based on it if (key.type === 'pgp') { return { id: id, name: id, type: KEY_TYPES.PGP, fingerprint: key.fingerprint, email: key.email, created: key.added, lastUsed: key.last_used, source: 'pgp', path: key.path, raw: key }; } else if (key.type === 'keybase') { return { id: id, name: id, type: KEY_TYPES.KEYBASE, fingerprint: key.fingerprint, email: key.email, username: key.username, created: key.added, lastUsed: key.last_used, source: 'keybase', path: key.path, raw: key }; } else { // Regular friend key (RSA) return { id: id, name: id, type: KEY_TYPES.RSA, fingerprint: key.fingerprint, created: key.added, lastUsed: key.last_used, source: 'friend', path: key.public, raw: key }; } } // Check GPG if enabled if (options.includeGpg) { const gpgInfo = await checkGpgKeyring(); if (gpgInfo.available && gpgInfo.keys.length > 0) { // Try to find a matching key by ID const gpgKey = gpgInfo.keys.find(k => k.id === id || k.id.endsWith(id)); if (gpgKey) { // Extract user information from the first UID const uidInfo = gpgKey.uids.length > 0 ? gpgKey.uids[0] : { uid: 'Unknown' }; const uidString = uidInfo.uid; // Extract email from UID (if present) let email = null; const emailMatch = uidString.match(/<([^>]+)>/); if (emailMatch) { email = emailMatch[1]; } // Extract name from UID (if present) let name = uidString; if (emailMatch) { name = uidString.replace(/<[^>]+>/, '').trim(); } return { id: gpgKey.id, name: name, type: KEY_TYPES.GPG, fingerprint: gpgKey.id, email: email, created: gpgKey.created, expires: gpgKey.expires, trust: gpgKey.trust, uids: gpgKey.uids.map(u => u.uid), source: 'gpg', gpgKey: gpgKey, raw: gpgKey }; } } } // Not found return null; } catch (error) { console.error(`Error getting key by ID: ${error.message}`); return null; } } /** * Read key content from various sources * @param {Object} key - Key object from getKeyById or searchKeys * @param {Object} options - Options * @returns {Promise<string|null>} - Key content or null on error */ async function readKeyContent(key, options = {}) { try { if (!key) return null; // Handle different key types and sources switch (key.type) { case KEY_TYPES.RSA: if (key.source === 'self') { // Read self key content based on options const keyPath = options.private ? key.path.private : key.path.public; return await fsPromises.readFile(keyPath, 'utf8'); } else { // Read friend key content return await fsPromises.readFile(key.path, 'utf8'); } case KEY_TYPES.PGP: case KEY_TYPES.KEYBASE: // Read PGP or Keybase key content return await fsPromises.readFile(key.path, 'utf8'); case KEY_TYPES.GPG: // Export key from GPG keyring return await exportGpgKey(key.id, options); default: throw new Error(`Unsupported key type: ${key.type}`); } } catch (error) { console.error(`Error reading key content: ${error.message}`); return null; } } /** * Export a key from GPG keyring * @param {string} keyId - GPG key ID * @param {Object} options - Export options * @returns {Promise<string|null>} - Exported key or null on error */ async function exportGpgKey(keyId, options = {}) { try { // Import child_process dynamically const childProcess = await import('child_process'); const { execFile } = childProcess; // Promisify execFile const execFilePromise = (cmd, args) => { return new Promise((resolve) => { execFile(cmd, args, (error, stdout, stderr) => { resolve({ error, stdout, stderr }); }); }); }; // Build export arguments const args = ['--export']; // Add armor (ASCII) output flag if not binary if (!options.binary) { args.push('--armor'); } // Add key ID args.push(keyId); // Execute GPG export const result = await execFilePromise('gpg', args); if (result.error) { console.error(`GPG export error: ${result.error.message}`); return null; } if (!result.stdout) { console.error('GPG export returned empty output'); return null; } return result.stdout; } catch (error) { console.error(`GPG export error: ${error.message}`); return null; } } /** * Import a key from various sources * @param {Object} options - Import options * @returns {Promise<Object>} - Import result */ async function importKey(options) { try { const { source, content, name, file, email, username, verify = true } = options; let keyContent = content; let keyInfo; // If file path is provided, read content from file if (file && !content) { keyContent = await fsPromises.readFile(file, 'utf8'); } switch (source) { case 'file': { // Auto-detect key type if (keyContent.includes('-----BEGIN PGP PUBLIC KEY BLOCK-----')) { // PGP key keyInfo = await pgpUtils.importPgpKey(keyContent); // Use provided name or derive from key const keyName = name || (keyInfo.email ? keyInfo.email : `pgp-${keyInfo.keyId.substring(keyInfo.keyId.length - 8)}`); // Store as PGP key const keyPath = await keyManager.addPgpKey(keyName, keyInfo); return { success: true, type: KEY_TYPES.PGP, name: keyName, fingerprint: keyInfo.keyId, email: keyInfo.email, path: keyPath }; } else if (keyContent.includes('-----BEGIN PUBLIC KEY-----')) { // RSA key const keyName = name || 'imported-friend'; const keyPath = await keyManager.addFriendKey(keyName, keyContent); // Get the fingerprint const db = await keyManager.loadKeyDatabase(); const keyInfo = db.keys.friends[keyName]; return { success: true, type: KEY_TYPES.RSA, name: keyName, fingerprint: keyInfo.fingerprint, path: keyPath }; } else { throw new Error('Unsupported key format. Must be PGP or RSA public key'); } } case 'pgp-server': { if (!email && !options.keyId) { throw new Error('Email or key ID is required for PGP server import'); } const identifier = email || options.keyId; const customName = name || identifier; const result = await pgpUtils.addPgpKeyFromServer(identifier, customName); return { success: true, type: KEY_TYPES.PGP, name: result.name, email: result.email, fingerprint: result.keyId, path: result.path }; } case 'keybase': { if (!username) { throw new Error('Username is required for Keybase import'); } const customName = name || `keybase:${username}`; const result = await keybaseUtils.addKeybaseKey(username, customName, verify); return { success: true, type: KEY_TYPES.KEYBASE, name: result.name, username: result.keybaseUser, email: result.email, fingerprint: result.keyId, path: result.path }; } case 'gpg-import': { if (!keyContent) { throw new Error('Key content is required for GPG import'); } // Write key to temporary file const tempFile = path.join( await fsPromises.mkdtemp(path.join(homedir(), '.dedpaste-temp-')), 'key.asc' ); await fsPromises.writeFile(tempFile, keyContent); // Import key into GPG keyring const childProcess = await import('child_process'); const { execFile } = childProcess; const execFilePromise = (cmd, args) => { return new Promise((resolve) => { execFile(cmd, args, (error, stdout, stderr) => { resolve({ error, stdout, stderr }); }); }); }; const importResult = await execFilePromise('gpg', ['--import', tempFile]); // Clean up temporary file try { await fsPromises.unlink(tempFile); await fsPromises.rmdir(path.dirname(tempFile)); } catch (cleanupError) { console.error(`Warning: Failed to clean up temporary file: ${cleanupError.message}`); } if (importResult.error) { throw new Error(`GPG import failed: ${importResult.error.message}`); } // Extract key ID from import output const keyIdMatch = importResult.stderr.match(/key ([A-F0-9]+):/i); const keyId = keyIdMatch ? keyIdMatch[1] : 'unknown'; return { success: true, type: KEY_TYPES.GPG, keyId: keyId, output: importResult.stderr }; } case 'gpg-keyring': { if (!options.keyId) { throw new Error('Key ID is required for GPG keyring import'); } // Export key from GPG keyring const exportedKey = await exportGpgKey(options.keyId, { armor: true }); if (!exportedKey) { throw new Error(`Failed to export key ${options.keyId} from GPG keyring`); } // Import into our key database keyInfo = await pgpUtils.importPgpKey(exportedKey); // Use provided name or derive from key const keyName = name || (keyInfo.email ? keyInfo.email : `gpg-${keyInfo.keyId.substring(keyInfo.keyId.length - 8)}`); // Store as PGP key const keyPath = await keyManager.addPgpKey(keyName, { key: exportedKey, keyId: keyInfo.keyId, email: keyInfo.email }); return { success: true, type: KEY_TYPES.PGP, name: keyName, fingerprint: keyInfo.keyId, email: keyInfo.email, path: keyPath, source: 'gpg-keyring' }; } default: throw new Error(`Unsupported import source: ${source}`); } } catch (error) { return { success: false, error: error.message }; } } /** * Generate a new key pair * @param {Object} options - Key generation options * @returns {Promise<Object>} - Generation result */ async function generateKey(options = {}) { try { const result = await keyManager.generateKeyPair(); return { success: true, type: KEY_TYPES.RSA, privateKeyPath: result.privateKeyPath, publicKeyPath: result.publicKeyPath }; } catch (error) { return { success: false, error: error.message }; } } /** * Remove a key from the database * @param {string} id - Key identifier * @returns {Promise<Object>} - Removal result */ async function removeKey(id) { try { // Try to remove from any key collection const success = await keyManager.removeKey('any', id); return { success, message: success ? `Key '${id}' removed successfully` : `Key '${id}' not found` }; } catch (error) { return { success: false, error: error.message }; } } /** * Get a printable report of all keys * @param {Object} options - Report options * @returns {Promise<string>} - Formatted report */ async function getKeyReport(options = {}) { try { const db = await keyManager.loadKeyDatabase(); let report = '# DedPaste Key Report\n\n'; // Self key report += '## Self Keys\n\n'; if (db.keys.self) { report += `- Name: Self\n`; report += ` - Fingerprint: ${db.keys.self.fingerprint}\n`; report += ` - Created: ${new Date(db.keys.self.created).toLocaleString()}\n`; report += ` - Private key: ${db.keys.self.private}\n`; report += ` - Public key: ${db.keys.self.public}\n`; } else { report += '- No self key found. Generate one with `dedpaste keys --gen-key`\n'; } // Friend keys const friendNames = Object.keys(db.keys.friends || {}); if (friendNames.length > 0) { report += '\n## Friend Keys\n\n'; for (const name of friendNames) { const friend = db.keys.friends[name]; const lastUsed = friend.last_used ? new Date(friend.last_used).toLocaleString() : 'Never'; report += `- Name: ${name}\n`; report += ` - Fingerprint: ${friend.fingerprint}\n`; report += ` - Last used: ${lastUsed}\n`; report += ` - Path: ${friend.public}\n`; } } // PGP keys const pgpNames = Object.keys(db.keys.pgp || {}); if (pgpNames.length > 0) { report += '\n## PGP Keys\n\n'; for (const name of pgpNames) { const pgp = db.keys.pgp[name]; const lastUsed = pgp.last_used ? new Date(pgp.last_used).toLocaleString() : 'Never'; report += `- Name: ${name}\n`; report += ` - Fingerprint: ${pgp.fingerprint}\n`; if (pgp.email) report += ` - Email: ${pgp.email}\n`; report += ` - Last used: ${lastUsed}\n`; report += ` - Path: ${pgp.path}\n`; } } // Keybase keys const keybaseNames = Object.keys(db.keys.keybase || {}); if (keybaseNames.length > 0) { report += '\n## Keybase Keys\n\n'; for (const name of keybaseNames) { const kb = db.keys.keybase[name]; const lastUsed = kb.last_used ? new Date(kb.last_used).toLocaleString() : 'Never'; report += `- Name: ${name}\n`; report += ` - Keybase username: ${kb.username}\n`; report += ` - Fingerprint: ${kb.fingerprint}\n`; if (kb.email) report += ` - Email: ${kb.email}\n`; report += ` - Last used: ${lastUsed}\n`; report += ` - Path: ${kb.path}\n`; } } // GPG keyring if (options.includeGpg) { const gpgInfo = await checkGpgKeyring(); if (gpgInfo.available) { report += '\n## GPG Keyring\n\n'; report += `- GPG version: ${gpgInfo.version}\n`; report += `- Total keys: ${gpgInfo.keys.length}\n\n`; if (gpgInfo.keys.length > 0) { for (const key of gpgInfo.keys) { const uid = key.uids.length > 0 ? key.uids[0].uid : 'No user ID'; report += `- Key ID: ${key.id}\n`; report += ` - User: ${uid}\n`; if (key.created) report += ` - Created: ${new Date(key.created).toLocaleString()}\n`; if (key.expires) report += ` - Expires: ${new Date(key.expires).toLocaleString()}\n`; if (key.trust) report += ` - Trust: ${key.trust}\n`; report += '\n'; } } } } return report; } catch (error) { return `Error generating key report: ${error.message}`; } } export { KEY_TYPES, initialize, searchKeys, getKeyById, readKeyContent, importKey, generateKey, removeKey, getKeyReport, exportGpgKey };