UNPKG

dedpaste

Version:

CLI pastebin application using Cloudflare Workers and R2

503 lines (443 loc) 15.6 kB
// Key diagnostics utility import fs from 'fs'; import path from 'path'; import { promises as fsPromises } from 'fs'; import { homedir } from 'os'; import { loadKeyDatabase, DEFAULT_KEY_DIR, FRIENDS_KEY_DIR, PGP_KEY_DIR, KEYBASE_KEY_DIR } from './keyManager.js'; import { validatePgpKey, decryptWithGpgKeyring } from './pgpUtils.js'; /** * Run diagnostics on the key database and file structure * @returns {Promise<Object>} - Diagnostics results */ async function runKeyDiagnostics() { const results = { status: 'ok', errors: [], warnings: [], keyDatabase: null, filesystemChecks: { directories: {}, files: {} }, keyStats: { self: false, friends: 0, pgp: 0, keybase: 0, total: 0 }, gpgKeyring: { available: false, version: null, keys: [] } }; // Check directories const dirPaths = [ DEFAULT_KEY_DIR, FRIENDS_KEY_DIR, PGP_KEY_DIR, KEYBASE_KEY_DIR ]; for (const dir of dirPaths) { try { const exists = fs.existsSync(dir); results.filesystemChecks.directories[dir] = { exists, writable: exists ? await isDirectoryWritable(dir) : false }; if (!exists) { results.warnings.push(`Directory not found: ${dir}`); } else if (!results.filesystemChecks.directories[dir].writable) { results.warnings.push(`Directory not writable: ${dir}`); } } catch (error) { results.errors.push(`Error checking directory ${dir}: ${error.message}`); } } // Load and check key database try { const db = await loadKeyDatabase(); results.keyDatabase = db; // Check stats results.keyStats.self = db.keys.self !== null; results.keyStats.friends = Object.keys(db.keys.friends || {}).length; results.keyStats.pgp = Object.keys(db.keys.pgp || {}).length; results.keyStats.keybase = Object.keys(db.keys.keybase || {}).length; results.keyStats.total = results.keyStats.friends + results.keyStats.pgp + results.keyStats.keybase + (results.keyStats.self ? 1 : 0); // Verify self key files if (db.keys.self) { results.filesystemChecks.files.self = { private: { path: db.keys.self.private, exists: fs.existsSync(db.keys.self.private), readable: fs.existsSync(db.keys.self.private) ? await isFileReadable(db.keys.self.private) : false }, public: { path: db.keys.self.public, exists: fs.existsSync(db.keys.self.public), readable: fs.existsSync(db.keys.self.public) ? await isFileReadable(db.keys.self.public) : false } }; if (!results.filesystemChecks.files.self.private.exists) { results.errors.push(`Self private key file not found: ${db.keys.self.private}`); } if (!results.filesystemChecks.files.self.public.exists) { results.errors.push(`Self public key file not found: ${db.keys.self.public}`); } } // Verify friend keys results.filesystemChecks.files.friends = {}; for (const [name, info] of Object.entries(db.keys.friends || {})) { const fileInfo = { path: info.public, exists: fs.existsSync(info.public), readable: fs.existsSync(info.public) ? await isFileReadable(info.public) : false }; results.filesystemChecks.files.friends[name] = fileInfo; if (!fileInfo.exists) { results.warnings.push(`Friend public key file not found: ${info.public} (${name})`); } } // Verify PGP keys results.filesystemChecks.files.pgp = {}; for (const [name, info] of Object.entries(db.keys.pgp || {})) { const fileInfo = { path: info.path, exists: fs.existsSync(info.path), readable: fs.existsSync(info.path) ? await isFileReadable(info.path) : false, valid: false }; results.filesystemChecks.files.pgp[name] = fileInfo; if (!fileInfo.exists) { results.warnings.push(`PGP key file not found: ${info.path} (${name})`); } else if (fileInfo.readable) { try { const content = await fsPromises.readFile(info.path, 'utf8'); fileInfo.valid = await validatePgpKey(content); if (!fileInfo.valid) { results.warnings.push(`Invalid PGP key format: ${info.path} (${name})`); } } catch (error) { results.warnings.push(`Error reading PGP key: ${info.path} (${name}): ${error.message}`); } } } // Verify Keybase keys results.filesystemChecks.files.keybase = {}; for (const [name, info] of Object.entries(db.keys.keybase || {})) { const fileInfo = { path: info.path, exists: fs.existsSync(info.path), readable: fs.existsSync(info.path) ? await isFileReadable(info.path) : false, valid: false }; results.filesystemChecks.files.keybase[name] = fileInfo; if (!fileInfo.exists) { results.warnings.push(`Keybase key file not found: ${info.path} (${name})`); } else if (fileInfo.readable) { try { const content = await fsPromises.readFile(info.path, 'utf8'); fileInfo.valid = await validatePgpKey(content); if (!fileInfo.valid) { results.warnings.push(`Invalid Keybase key format: ${info.path} (${name})`); } } catch (error) { results.warnings.push(`Error reading Keybase key: ${info.path} (${name}): ${error.message}`); } } } } catch (error) { results.errors.push(`Error loading key database: ${error.message}`); } // Check GPG keyring try { const gpgInfo = await checkGpgKeyring(); results.gpgKeyring = gpgInfo; if (gpgInfo.available) { results.warnings = results.warnings.filter(warning => { // Remove warnings about missing PGP keys if they're in the GPG keyring if (warning.includes('PGP key file not found')) { const keyId = warning.match(/fingerprint: ([A-F0-9]+)/i); if (keyId && gpgInfo.keys.some(k => k.id.includes(keyId[1]))) { return false; // Remove the warning } } return true; }); } } catch (error) { results.warnings.push(`Error checking GPG keyring: ${error.message}`); } // Set final status if (results.errors.length > 0) { results.status = 'error'; } else if (results.warnings.length > 0) { results.status = 'warning'; } return results; } /** * Check if a directory is writable * @param {string} directory - Directory path * @returns {Promise<boolean>} - True if writable */ async function isDirectoryWritable(directory) { try { // Try to write a temporary file const testFile = path.join(directory, `.test-${Date.now()}`); await fsPromises.writeFile(testFile, 'test'); await fsPromises.unlink(testFile); return true; } catch (error) { return false; } } /** * Check if a file is readable * @param {string} filePath - File path * @returns {Promise<boolean>} - True if readable */ async function isFileReadable(filePath) { try { await fsPromises.access(filePath, fs.constants.R_OK); return true; } catch (error) { return false; } } /** * Check GPG keyring status and get key list * @returns {Promise<Object>} - GPG keyring info */ async function checkGpgKeyring() { const result = { available: false, version: null, keys: [] }; try { // Import child_process dynamically const childProcess = await import('child_process'); const { execFile } = childProcess; // Promisify execFile with timeout and kill functionality const execFilePromise = (cmd, args, timeout = 5000) => { return new Promise((resolve) => { let procKilled = false; let proc; // Set a timeout to avoid hanging const timeoutId = setTimeout(() => { procKilled = true; if (proc && proc.pid) { try { // Force kill the process if it doesn't respond process.kill(proc.pid, 'SIGKILL'); } catch (killError) { // Ignore kill errors - process might have already exited } } resolve({ error: new Error('Command timed out after ' + timeout + 'ms'), stdout: '', stderr: 'Timeout - process killed to prevent hanging', timedOut: true }); }, timeout); try { proc = execFile(cmd, args, { timeout: timeout - 500 }, (error, stdout, stderr) => { if (procKilled) return; // Already handled by timeout clearTimeout(timeoutId); resolve({ error, stdout, stderr, timedOut: false }); }); // Additional safeguards for unresponsive processes proc.on('error', (err) => { if (procKilled) return; // Already handled by timeout clearTimeout(timeoutId); resolve({ error: err, stdout: '', stderr: err.message, timedOut: false }); }); } catch (execError) { if (procKilled) return; // Already handled by timeout clearTimeout(timeoutId); resolve({ error: execError, stdout: '', stderr: execError.message, timedOut: false }); } }); }; // Check if GPG is available const versionCheck = await execFilePromise('gpg', ['--version']); if (versionCheck.error) { return result; // GPG not available } // Extract version const versionMatch = versionCheck.stdout.match(/gpg \(GnuPG\) ([\d.]+)/); if (versionMatch) { result.version = versionMatch[1]; result.available = true; } // List keys with a longer timeout for slow GPG agents const keyList = await execFilePromise('gpg', ['--list-keys', '--with-colons'], 8000); if (!keyList.error && keyList.stdout) { // Parse colon format const lines = keyList.stdout.split('\n'); let currentKey = null; for (const line of lines) { const fields = line.split(':'); if (fields[0] === 'pub') { // Start a new key currentKey = { id: fields[4], type: 'public', created: fields[5] ? new Date(parseInt(fields[5]) * 1000).toISOString() : null, expires: fields[6] && fields[6] !== '' ? new Date(parseInt(fields[6]) * 1000).toISOString() : null, trust: fields[1], uids: [] }; result.keys.push(currentKey); } else if (fields[0] === 'uid' && currentKey) { // Add a user ID to the current key currentKey.uids.push({ uid: fields[9], trust: fields[1] }); } } } } catch (error) { // GPG integration not available } return result; } /** * Generate a user-friendly report from diagnostics results * @param {Object} results - Diagnostics results * @returns {string} - Formatted report */ function formatDiagnosticsReport(results) { let report = `# DedPaste Key System Diagnostic Report\n\n`; // Status summary report += `## Status: ${results.status.toUpperCase()}\n\n`; if (results.errors.length > 0) { report += `### Errors (${results.errors.length}):\n`; results.errors.forEach(error => { report += `- ❌ ${error}\n`; }); report += '\n'; } if (results.warnings.length > 0) { report += `### Warnings (${results.warnings.length}):\n`; results.warnings.forEach(warning => { report += `- ⚠️ ${warning}\n`; }); report += '\n'; } // Key statistics report += `## Key Statistics\n\n`; report += `- Self key: ${results.keyStats.self ? '✅ Present' : '❌ Missing'}\n`; report += `- Friend keys: ${results.keyStats.friends}\n`; report += `- PGP keys: ${results.keyStats.pgp}\n`; report += `- Keybase keys: ${results.keyStats.keybase}\n`; report += `- Total keys: ${results.keyStats.total}\n\n`; // GPG keyring report += `## GPG Keyring\n\n`; if (results.gpgKeyring.available) { report += `- ✅ GPG available (version ${results.gpgKeyring.version})\n`; report += `- Found ${results.gpgKeyring.keys.length} keys in keyring\n`; if (results.gpgKeyring.keys.length > 0) { report += '\n### GPG Keys:\n'; results.gpgKeyring.keys.forEach(key => { const uid = key.uids.length > 0 ? key.uids[0].uid : 'No user ID'; report += `- Key: ${key.id}\n - User: ${uid}\n - Created: ${key.created ? new Date(key.created).toLocaleString() : 'unknown'}\n`; }); } } else { report += `- ❌ GPG not available or not in path\n`; report += ` Consider installing GPG for improved key management\n`; } // Directory checks report += `\n## Directory Checks\n\n`; for (const [dir, info] of Object.entries(results.filesystemChecks.directories)) { const status = info.exists && info.writable ? '✅' : info.exists ? '⚠️' : '❌'; report += `- ${status} ${dir}: ${info.exists ? (info.writable ? 'OK' : 'Not writable') : 'Not found'}\n`; } // File checks (simplified) if (results.filesystemChecks.files.self) { report += `\n## Self Key Files\n\n`; const privateStatus = results.filesystemChecks.files.self.private.exists ? '✅' : '❌'; const publicStatus = results.filesystemChecks.files.self.public.exists ? '✅' : '❌'; report += `- ${privateStatus} Private key: ${results.filesystemChecks.files.self.private.path}\n`; report += `- ${publicStatus} Public key: ${results.filesystemChecks.files.self.public.path}\n`; } // Return the formatted report return report; } /** * Find key by criteria in any key collection * @param {Object} db - Key database * @param {Object} criteria - Search criteria * @returns {Array} - Matching keys */ function findKeysMatchingCriteria(db, criteria) { const matches = []; // Function to check if a key matches criteria const isMatch = (key, info, type, name) => { for (const [field, value] of Object.entries(criteria)) { // Handle regex pattern if (typeof value === 'object' && value instanceof RegExp) { if (!info[field] || !value.test(info[field].toString())) { return false; } } // Handle exact matches else if (info[field] !== value) { return false; } } // All criteria matched return { type, name, info: { ...info } }; }; // Check self key if (db.keys.self) { const match = isMatch(db.keys.self, db.keys.self, 'self', 'self'); if (match) matches.push(match); } // Check friend keys for (const [name, info] of Object.entries(db.keys.friends)) { const match = isMatch(name, info, 'friend', name); if (match) matches.push(match); } // Check PGP keys for (const [name, info] of Object.entries(db.keys.pgp || {})) { const match = isMatch(name, info, 'pgp', name); if (match) matches.push(match); } // Check Keybase keys for (const [name, info] of Object.entries(db.keys.keybase || {})) { const match = isMatch(name, info, 'keybase', name); if (match) matches.push(match); } return matches; } export { runKeyDiagnostics, formatDiagnosticsReport, findKeysMatchingCriteria, checkGpgKeyring };