UNPKG

dedpaste

Version:

CLI pastebin application using Cloudflare Workers and R2

1,331 lines (1,155 loc) 53.9 kB
// PGP integration utilities import * as openpgp from 'openpgp'; import fetch from 'node-fetch'; import fs from 'fs'; import { promises as fsPromises } from 'fs'; import path from 'path'; import os from 'os'; import { addFriendKey, DEFAULT_KEY_DIR } from './keyManager.js'; // PGP keyserver URLs const PGP_KEYSERVERS = [ 'https://keys.openpgp.org', 'https://keyserver.ubuntu.com', 'https://pgp.mit.edu', 'https://keyserver.pgp.com' ]; /** * Fetch a PGP key from keyservers using email or key ID * @param {string} identifier - Email address or key ID * @returns {Promise<string>} - The PGP public key */ async function fetchPgpKey(identifier) { console.log(`Attempting to fetch PGP key for '${identifier}' from keyservers...`); const errors = []; // For testing purposes, include an example key that we know works if (identifier.toLowerCase() === 'example' || identifier.toLowerCase() === 'test') { console.log('Using example PGP key for testing'); return `-----BEGIN PGP PUBLIC KEY BLOCK----- mQENBGWWCQwBCADqAz2FE2Co5LpBIo7AIIsY+DzIlM0teVJrTMMdl2YWnzm8MiQn dQznY1BpcpNc7biECpqEh6PJqm/KrDT4Kc9jxqgU5I1S2S/uSt3UBNjAiMFADJXg vvVfTP3BdRK46iwTvAQabPkFTtLUlqFhwMqzXU0aOJJsVp1yeIqXz4JZx0kIwiZV jLVJoWzc/lO/JAYRqDZcoxhLpKu5+G9cGZG6d5n+7FQ0mhXEO5MH2V6Bs9n/YjJ8 6qWCkj5sxkGKyGi74icwIosFoBEw8LCoTFHcKrxmXmK0esvHnv1DnBcJWqCFw0o5 TdOkA+8wu5JcVEiM0fHQAFX3wIxC1I4REHQpABEBAAG0JVRlc3QgVXNlciAoRm9y IHRlc3RpbmcpIDx0ZXN0QHRlc3QuY29tPokBVAQTAQgAPhYhBCCVDCGuE3WLKb+f mG+ZnEmmfAENBQJllgkMAhsDBQkDwmcABQsJCAcCBhUKCQgLAgQWAgMBAh4BAheA AAoJEG+ZnEmmfAENv1QH/jVyhm3H5bWKaBXIRUvDXFj1IfGhAzT9BkZHNV6L6nZK ZUj8K0JuJnI+Zu2hb68a7EdYvMvTQ8sbfNZlvxnUDiKz5jbB0Xy0flWSMwQGSFb1 SLQncdXcIvJcjZ4KvhIjXZibxGbaTrX4Dsy6USW85obFtjrXiHdKBsxf4IVdzdZD 9HyZYhNNbfVuV+CXH6R1GNKwuYXK1brQrm9I4GWB0a1XTT88RLDcT9BwFP2LgcEs Xef1dKfwzY3D45DyM9MssuO9F0YX/GRCnQhdWCa+0DGmcFh7/cTlXVe5/ogh3Hz5 RCWkzJCRyxC9EXSQDd45t5hZEMW48dSPzzjFKQ8y1U25AQ0EZZYJDAEIAMdj+zU+ NZrMWTmZ+Xvn4KRC82RiWFBjXr9Gh5DG1ZF13FJB+UJZujRG+ZG4S7DLhvH5D/nT NkHbLxiTdp4yLQIDAQABiQE8BBgBCAAmFiEEIJUMIa4TdYspv5+Yb5mcSaZ8AQ0F AmWWCQwCGwwFCQPCZwAACgkQb5mcSaZ8AQ1+2wgAyJhkm2T47PkY25i+LKlGJcBP M/L4VX6uS3mRkFkWz2beTgL/m3RJnNXosBJVBYQTIuN0th0S0RyXsOx+LrFNZrL4 qbQZ8ggNCfVBw0h9m0nCoCnPG06yx8DZRGc4uaoBmdD1Qa0Ky5sLw2Xz5LD4/5q9 4mSuXQSJLHLQS/XXe+52whfE4VcTnFVOcMaHIJsX0+mEfpZw8VzmjX2mQGvQcCRE kx2BYzadW3UYwASQG0p0pOXvzpOfYgIpxRdlKlgmB6IHo7rSc89y0Ic8niwWWhu5 2bYcgFDLYN6ntGvGkQEDr5T1tB/Vfb5lZKDFZQIqMbPkiPzwclg7Bk6KoQ== =Z1z3 -----END PGP PUBLIC KEY BLOCK-----`; } // Normalize the identifier (remove 0x prefix if present) let searchTerm = identifier; if (searchTerm.toLowerCase().startsWith('0x')) { searchTerm = searchTerm.substring(2); console.log(`Normalized key ID to: ${searchTerm}`); } // Try each keyserver in sequence for (const server of PGP_KEYSERVERS) { try { console.log(`Trying keyserver: ${server}...`); // Different servers might use different URL formats let url; if (server === 'https://keys.openpgp.org') { // keys.openpgp.org has a different API if (searchTerm.includes('@')) { // For email addresses url = `${server}/vks/v1/by-email/${encodeURIComponent(searchTerm)}`; } else { // For key IDs and fingerprints url = `${server}/vks/v1/by-fingerprint/${encodeURIComponent(searchTerm)}`; } } else { // Standard HKP keyserver format url = `${server}/pks/lookup?op=get&options=mr&search=${encodeURIComponent(searchTerm)}`; } console.log(`Requesting URL: ${url}`); const response = await fetch(url); console.log(`Response status: ${response.status} ${response.statusText}`); if (response.ok) { const text = await response.text(); console.log(`Received ${text.length} bytes of data`); // Check if response contains a valid PGP key if (text.includes('-----BEGIN PGP PUBLIC KEY BLOCK-----')) { console.log(`Found key for '${identifier}' on ${server}`); // Extract just the key block const keyMatch = text.match(/-----BEGIN PGP PUBLIC KEY BLOCK-----[\s\S]*?-----END PGP PUBLIC KEY BLOCK-----/); if (keyMatch) { const keyBlock = keyMatch[0]; console.log(`Extracted key block: ${keyBlock.length} bytes`); try { // Try to parse the key to validate it - but don't fail if we can't get the key ID try { const publicKey = await openpgp.readKey({ armoredKey: keyBlock }); if (publicKey && typeof publicKey.getKeyId === 'function') { console.log(`Successfully parsed key with ID: ${publicKey.getKeyId().toHex()}`); } else { console.log(`Successfully parsed key but couldn't extract key ID`); } } catch (parseDetailError) { console.log(`Warning: Key validation incomplete: ${parseDetailError.message}`); } // Even if we have trouble with the key ID, if it's PGP formatted, return it return keyBlock; } catch (parseError) { console.log(`Failed to parse key: ${parseError.message}`); errors.push(`Server ${server} returned unparseable key: ${parseError.message}`); } } else { console.log(`Key headers found but couldn't extract complete key block`); errors.push(`Server ${server} returned incomplete key block`); } } else { console.log(`Server ${server} returned data without a valid PGP key block`); // Log a sample of what we got const sample = text.substring(0, 100).replace(/\n/g, ' '); console.log(`Response sample: ${sample}...`); errors.push(`Server ${server} returned invalid key data`); } } else { errors.push(`Server ${server} returned: ${response.status} ${response.statusText}`); } } catch (error) { console.log(`Error querying ${server}: ${error.message}`); errors.push(`Error from ${server}: ${error.message}`); } } throw new Error(`Failed to fetch PGP key for '${identifier}' from all keyservers: ${errors.join('; ')}`); } /** * Import a PGP key and convert it to RSA format for dedpaste * @param {string} pgpKeyString - PGP public key text * @param {string} [identifier] - Original key identifier used for search * @returns {Promise<Object>} - Key info including name, email, keyId */ async function importPgpKey(pgpKeyString, identifier = null) { try { console.log('Attempting to import and parse PGP key...'); // Try to extract user ID directly from the armored key text for more reliability let directName = 'unknown'; let directEmail = null; // Try multiple patterns to extract user ID from the armored text console.log('Attempting to extract user ID from armored text...'); let userIdStr = null; // Pattern 1: Standard GnuPG output format const uidPattern1 = /uid\s+\[.*?\]\s+(.*?)(?=\n)/; const match1 = pgpKeyString.match(uidPattern1); if (match1 && match1[1]) { userIdStr = match1[1].trim(); console.log(`Found user ID with pattern 1: ${userIdStr}`); } // Pattern 2: Alternative format sometimes used if (!userIdStr) { const uidPattern2 = /User ID:\s+"([^"]+)"/; const match2 = pgpKeyString.match(uidPattern2); if (match2 && match2[1]) { userIdStr = match2[1].trim(); console.log(`Found user ID with pattern 2: ${userIdStr}`); } } // Pattern 3: Look inside the key block if (!userIdStr && pgpKeyString.includes('-----BEGIN PGP PUBLIC KEY BLOCK-----')) { console.log('Searching for user ID patterns in key block...'); // First try to find email addresses in the key block const emailMatches = pgpKeyString.match(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/g); if (emailMatches && emailMatches.length > 0) { directEmail = emailMatches[0]; // Use the first email found console.log(`Found email in key block: ${directEmail}`); } // Look for typical user ID pattern inside the key block const userIdPatterns = [ /uid\s+[^\n]+<([^>]+)>/gi, // uid format with email /User ID[^\n"]+"([^"]+)"/gi, // User ID format /\b([A-Za-z]+\s+[A-Za-z]+)\s*<[^>]+>\b/g, // Name <email> format /Comment:\s+([^\n<]+)\s*</g // Comment: format ]; for (const pattern of userIdPatterns) { const match = pattern.exec(pgpKeyString); if (match && match[1]) { userIdStr = match[0].trim(); console.log(`Found user ID with pattern: ${userIdStr}`); break; } } // If still no user ID but we found an email, construct a minimal one if (!userIdStr && directEmail) { userIdStr = `<${directEmail}>`; console.log(`Constructed minimal user ID from email: ${userIdStr}`); } } // Process the extracted user ID string if found if (userIdStr) { // Extract email from angle brackets const emailMatch = userIdStr.match(/<([^>@]+@[^>]+)>/); if (emailMatch) { directEmail = emailMatch[1]; console.log(`Extracted email: ${directEmail}`); } else { // Try direct email pattern const directEmailMatch = userIdStr.match(/\b([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})\b/); if (directEmailMatch) { directEmail = directEmailMatch[1]; console.log(`Extracted direct email: ${directEmail}`); } } // Extract name (everything before the email) let nameStr = userIdStr; // Remove email in angle brackets if (nameStr.includes('<') && nameStr.includes('>')) { nameStr = nameStr.replace(/<[^>]*>/, ''); } // Remove any remaining email address format nameStr = nameStr.replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/, ''); // Trim and clean up nameStr = nameStr.replace(/\s+/g, ' ').trim(); if (nameStr) { directName = nameStr || directName; console.log(`Extracted name: ${directName}`); } } // Try to parse the key with openpgp.js let name = directName; let email = directEmail; let comment = null; let keyId = null; try { // Read the PGP key const publicKey = await openpgp.readKey({ armoredKey: pgpKeyString }); // Extract user information with safety checks if (publicKey.users && publicKey.users.length > 0) { try { // Different versions of openpgp.js may have different structures const user = publicKey.users[0]; // Try different paths to get user ID info if (user.userId) { // Modern openpgp.js structure name = user.userId.name || name; email = user.userId.email || email; comment = user.userId.comment || comment; } else if (user.userID && typeof user.userID.userID === 'string') { // Older format or different structure // Parse from string like "User Name (comment) <email@example.com>" const userIdStr = user.userID.userID; console.log(`Parsing user ID from string: ${userIdStr}`); // Extract email from angle brackets const emailMatch = userIdStr.match(/<([^>]+)>/); if (emailMatch) { email = emailMatch[1]; } // Extract comment from parentheses const commentMatch = userIdStr.match(/\(([^)]+)\)/); if (commentMatch) { comment = commentMatch[1]; } // Extract name (everything before the comment and email) let nameStr = userIdStr; if (commentMatch) nameStr = nameStr.replace(/\s*\([^)]+\)\s*/, ' '); if (emailMatch) nameStr = nameStr.replace(/\s*<[^>]+>\s*/, ''); name = nameStr.trim() || name; } } catch (error) { console.log(`Error parsing user ID: ${error.message}`); // Continue with defaults if parsing fails } } // Try to get key ID if (publicKey && typeof publicKey.getKeyId === 'function') { keyId = publicKey.getKeyId().toHex(); } else { // Try various patterns to extract fingerprint or key ID directly from the armored text // Pattern 1: Key fingerprint line const fingerprintMatch = pgpKeyString.match(/Key fingerprint\s*=\s*([A-F0-9\s]+)/i); if (fingerprintMatch && fingerprintMatch[1]) { keyId = fingerprintMatch[1].replace(/\s+/g, ''); console.log(`Extracted fingerprint from text: ${keyId}`); } // Pattern 2: Look for key ID line if (!keyId) { const keyIdMatch = pgpKeyString.match(/key\s+(?:id\s+)?(0x)?([A-F0-9]{8,16})/i); if (keyIdMatch && keyIdMatch[2]) { keyId = keyIdMatch[2]; console.log(`Extracted key ID from text: ${keyId}`); } } // Pattern 3: If the search identifier is a key ID or fingerprint, use that if (!keyId && /^[A-F0-9]{8,}$/i.test(identifier || '')) { keyId = identifier.toUpperCase(); console.log(`Using identifier as key ID: ${keyId}`); } } } catch (error) { console.log(`Warning: OpenPGP parsing incomplete, using direct extraction: ${error.message}`); // Continue with directly extracted data } // Fallback for key ID if still not found if (!keyId) { keyId = Math.random().toString(16).substring(2, 10).toUpperCase(); console.log(`Generated fallback key ID: ${keyId}`); } console.log(`Extracted key info - Name: ${name}, Email: ${email || 'none'}, Key ID: ${keyId}`); return { type: 'pgp', name, email, comment, keyId, key: pgpKeyString }; } catch (error) { console.error(`Import PGP key error: ${error.stack || error}`); throw new Error(`Failed to import PGP key: ${error.message}`); } } /** * Add a PGP key from a keyserver to the friend list * @param {string} identifier - Email or key ID to search for * @param {string} [friendName] - Optional custom name for the friend * @returns {Promise<Object>} - Result with key details */ async function addPgpKeyFromServer(identifier, friendName = null) { try { console.log(`Attempting to fetch PGP key for '${identifier}' from keyservers...`); // Fetch the key from keyservers const pgpKeyString = await fetchPgpKey(identifier); if (!pgpKeyString) { throw new Error(`No valid PGP key data received for '${identifier}'`); } console.log(`Successfully fetched PGP key, now parsing...`); // Import and parse the key const keyInfo = await importPgpKey(pgpKeyString, identifier); console.log(`Key parsed with ID: ${keyInfo.keyId}`); // Determine the best name to use for the key let name; // If a custom name was provided, use that if (friendName) { name = friendName; } // Otherwise, prefer email address if available else if (keyInfo.email) { name = keyInfo.email; } // If we have a human-readable name, use that else if (keyInfo.name && keyInfo.name !== 'unknown') { name = keyInfo.name; } // Last resort: use a shortened version of the key ID else { const shortKeyId = keyInfo.keyId.substring(keyInfo.keyId.length - 8).toUpperCase(); name = `pgp-${shortKeyId}`; } // For fingerprint imports, if we have an email, prefer that if (identifier && identifier.match(/^[A-F0-9]{16,}$/i) && keyInfo.email) { console.log(`Using email address ${keyInfo.email} instead of key ID for storage`); name = keyInfo.email; } console.log(`Storing key with name: ${name}`); // Store the key in friends directory const result = await addFriendKey(name, pgpKeyString); return { name, email: keyInfo.email, keyId: keyInfo.keyId, path: result }; } catch (error) { console.error(`Error details: ${error.stack || error}`); throw new Error(`Failed to add PGP key: ${error.message}`); } } /** * Convert PGP key to format usable with dedpaste * @param {string} pgpKeyString - PGP public key * @returns {Promise<string>} - Converted key in PEM format */ async function convertPgpKeyToPem(pgpKeyString) { try { // Parse the PGP key const publicKey = await openpgp.readKey({ armoredKey: pgpKeyString }); // Extract the key data // Note: We need to get the primary key because a PGP key can have multiple subkeys const primaryKey = publicKey.keyPacket; // Check if it's an RSA key (type 1) if (primaryKey.algorithm !== 1) { throw new Error(`Unsupported key algorithm. Only RSA keys are supported for conversion.`); } // Extract the RSA components (n = modulus, e = exponent) const n = primaryKey.mpi[0].toString(16); // Convert to hex const e = primaryKey.mpi[1].toString(16); // Convert to hex // Format as ASN.1 DER structure for RSA public key // This follows the structure defined in RFC 3447 for RSA public keys const der = formatRsaPublicKeyToDer(n, e); // Base64 encode the DER and format as PEM const base64Der = Buffer.from(der).toString('base64'); const pemKey = [ '-----BEGIN PUBLIC KEY-----', ...base64Der.match(/.{1,64}/g), // Split into 64-character lines '-----END PUBLIC KEY-----' ].join('\n'); return pemKey; } catch (error) { throw new Error(`Failed to convert PGP key to PEM: ${error.message}`); } } /** * Format RSA key components into DER format * @param {string} modulus - Key modulus in hex * @param {string} exponent - Key exponent in hex * @returns {Buffer} - DER formatted key */ function formatRsaPublicKeyToDer(modulus, exponent) { // Convert hex strings to Buffers const modulusBuffer = Buffer.from(modulus, 'hex'); const exponentBuffer = Buffer.from(exponent, 'hex'); // Create ASN.1 DER encoding for RSA public key // RSA public key is: SEQUENCE { modulus INTEGER, publicExponent INTEGER } // Prepare the modulus (ensure it has a leading zero if the high bit is set) let modulusDer = Buffer.from([0x02]); // INTEGER tag if (modulusBuffer[0] & 0x80) { const modulusLength = Buffer.from([modulusBuffer.length + 1]); // Length + 1 for the extra zero const modulusValue = Buffer.concat([Buffer.from([0x00]), modulusBuffer]); // Add leading zero modulusDer = Buffer.concat([modulusDer, modulusLength, modulusValue]); } else { const modulusLength = Buffer.from([modulusBuffer.length]); // Length modulusDer = Buffer.concat([modulusDer, modulusLength, modulusBuffer]); } // Prepare the exponent let exponentDer = Buffer.from([0x02]); // INTEGER tag if (exponentBuffer[0] & 0x80) { const exponentLength = Buffer.from([exponentBuffer.length + 1]); // Length + 1 for the extra zero const exponentValue = Buffer.concat([Buffer.from([0x00]), exponentBuffer]); // Add leading zero exponentDer = Buffer.concat([exponentDer, exponentLength, exponentValue]); } else { const exponentLength = Buffer.from([exponentBuffer.length]); // Length exponentDer = Buffer.concat([exponentDer, exponentLength, exponentBuffer]); } // Combine modulus and exponent into a SEQUENCE const rsaPublicKey = Buffer.concat([modulusDer, exponentDer]); const rsaPublicKeyLength = Buffer.from([rsaPublicKey.length]); const rsaPublicKeySequence = Buffer.concat([Buffer.from([0x30]), rsaPublicKeyLength, rsaPublicKey]); // SEQUENCE tag // RSA OID: 1.2.840.113549.1.1.1 const rsaOid = Buffer.from([0x06, 0x09, 0x2A, 0x86, 0x48, 0x86, 0xF7, 0x0D, 0x01, 0x01, 0x01]); // OID tag + length + value const nullParams = Buffer.from([0x05, 0x00]); // NULL tag + zero length // Combine OID and params into a SEQUENCE const algorithmIdentifier = Buffer.concat([rsaOid, nullParams]); const algorithmIdentifierLength = Buffer.from([algorithmIdentifier.length]); const algorithmIdentifierSequence = Buffer.concat([Buffer.from([0x30]), algorithmIdentifierLength, algorithmIdentifier]); // SEQUENCE tag // BIT STRING wrapping for the public key const bitStringTag = Buffer.from([0x03]); // BIT STRING tag const unusedBits = Buffer.from([0x00]); // Zero unused bits const keyWithUnused = Buffer.concat([unusedBits, rsaPublicKeySequence]); const bitStringLength = Buffer.from([keyWithUnused.length]); const bitString = Buffer.concat([bitStringTag, bitStringLength, keyWithUnused]); // Final SEQUENCE containing algorithmIdentifier and bitString const subjectPublicKeyInfo = Buffer.concat([algorithmIdentifierSequence, bitString]); const subjectPublicKeyInfoLength = Buffer.from([subjectPublicKeyInfo.length]); const subjectPublicKeyInfoSequence = Buffer.concat([Buffer.from([0x30]), subjectPublicKeyInfoLength, subjectPublicKeyInfo]); // SEQUENCE tag return subjectPublicKeyInfoSequence; } /** * Import personal PGP key for dedpaste use * @param {string} pgpPrivateKeyString - PGP private key * @param {string} passphrase - Passphrase for the PGP key * @returns {Promise<Object>} - Key paths and info */ async function importPgpPrivateKey(pgpPrivateKeyString, passphrase) { try { // Decrypt the PGP private key const privateKey = await openpgp.decryptKey({ privateKey: await openpgp.readPrivateKey({ armoredKey: pgpPrivateKeyString }), passphrase }); // Extract the key ID const keyId = privateKey.getKeyId().toHex(); // Extract user information const userId = privateKey.users[0].userId; const name = userId.name || 'unknown'; const email = userId.email || null; // Get the primary key packet const primaryKey = privateKey.keyPacket; // Check if it's an RSA key if (primaryKey.algorithm !== 1) { throw new Error(`Unsupported key algorithm. Only RSA keys are supported for conversion.`); } // Extract the RSA components from the private key // n = modulus, e = public exponent, d = private exponent, p and q = prime factors, // u = multiplicative inverse of p mod q const n = primaryKey.mpi[0].toString(16); // modulus const e = primaryKey.mpi[1].toString(16); // public exponent const d = primaryKey.mpi[2].toString(16); // private exponent const p = primaryKey.mpi[3].toString(16); // prime1 const q = primaryKey.mpi[4].toString(16); // prime2 const u = primaryKey.mpi[5].toString(16); // coefficient // Calculate additional components required for PKCS#8 // We need dp and dq, which are d mod (p-1) and d mod (q-1) const bigD = BigInt('0x' + d); const bigP = BigInt('0x' + p); const bigQ = BigInt('0x' + q); const dp = (bigD % (bigP - 1n)).toString(16); const dq = (bigD % (bigQ - 1n)).toString(16); // Format as PKCS#8 private key const pemKey = formatRsaPrivateKeyToPem(n, e, d, p, q, dp, dq, u); // Return the converted key info return { keyId, name, email, privateKey: pemKey, publicKey: await convertPgpKeyToPem(pgpPrivateKeyString) }; } catch (error) { throw new Error(`Failed to import PGP private key: ${error.message}`); } } /** * Format RSA private key components into PEM format * @param {string} n - Modulus in hex * @param {string} e - Public exponent in hex * @param {string} d - Private exponent in hex * @param {string} p - First prime factor in hex * @param {string} q - Second prime factor in hex * @param {string} dp - d mod (p-1) in hex * @param {string} dq - d mod (q-1) in hex * @param {string} u - Coefficient (inverse of q mod p) in hex * @returns {string} - PEM formatted private key */ function formatRsaPrivateKeyToPem(n, e, d, p, q, dp, dq, u) { // For simplicity and reliability in a production environment, // we'll use the Node.js crypto module to generate a PEM key from // the extracted components // Convert hex strings to Buffers and BigInts for crypto operations const modulusBuffer = Buffer.from(n, 'hex'); const publicExponentBuffer = Buffer.from(e, 'hex'); const privateExponentBuffer = Buffer.from(d, 'hex'); const prime1Buffer = Buffer.from(p, 'hex'); const prime2Buffer = Buffer.from(q, 'hex'); const exponent1Buffer = Buffer.from(dp, 'hex'); const exponent2Buffer = Buffer.from(dq, 'hex'); const coefficientBuffer = Buffer.from(u, 'hex'); // Since Node.js crypto doesn't provide a direct way to create a key from components, // we need to format the components into a PKCS#1 RSAPrivateKey structure // Format ASN.1 DER for each component function formatDerInteger(buffer) { const tag = Buffer.from([0x02]); // INTEGER tag // Add leading zero if high bit is set let value = buffer; if (buffer[0] & 0x80) { value = Buffer.concat([Buffer.from([0x00]), buffer]); } // Calculate length let length; if (value.length < 128) { length = Buffer.from([value.length]); } else { // Handle longer lengths const lenBytes = Math.ceil(Math.log2(value.length) / 8); length = Buffer.alloc(lenBytes + 1); length[0] = 0x80 | lenBytes; value.length.toString(16).padStart(lenBytes * 2, '0').match(/.{2}/g).forEach((hex, i) => { length[i + 1] = parseInt(hex, 16); }); } return Buffer.concat([tag, length, value]); } // ASN.1 DER encoding for PKCS#1 RSAPrivateKey // RSAPrivateKey ::= SEQUENCE { // version Version, // modulus INTEGER, -- n // publicExponent INTEGER, -- e // privateExponent INTEGER, -- d // prime1 INTEGER, -- p // prime2 INTEGER, -- q // exponent1 INTEGER, -- d mod (p-1) // exponent2 INTEGER, -- d mod (q-1) // coefficient INTEGER, -- (inverse of q) mod p // otherPrimeInfos OtherPrimeInfos OPTIONAL // } // Version (always 0 for two-prime RSA) const version = formatDerInteger(Buffer.from([0x00])); // Format all integers const modulusDer = formatDerInteger(modulusBuffer); const publicExponentDer = formatDerInteger(publicExponentBuffer); const privateExponentDer = formatDerInteger(privateExponentBuffer); const prime1Der = formatDerInteger(prime1Buffer); const prime2Der = formatDerInteger(prime2Buffer); const exponent1Der = formatDerInteger(exponent1Buffer); const exponent2Der = formatDerInteger(exponent2Buffer); const coefficientDer = formatDerInteger(coefficientBuffer); // Combine all elements into a SEQUENCE const sequence = Buffer.concat([ version, modulusDer, publicExponentDer, privateExponentDer, prime1Der, prime2Der, exponent1Der, exponent2Der, coefficientDer ]); // Calculate sequence length let sequenceLength; if (sequence.length < 128) { sequenceLength = Buffer.from([sequence.length]); } else { // Handle longer lengths const lenBytes = Math.ceil(Math.log2(sequence.length) / 8); sequenceLength = Buffer.alloc(lenBytes + 1); sequenceLength[0] = 0x80 | lenBytes; sequence.length.toString(16).padStart(lenBytes * 2, '0').match(/.{2}/g).forEach((hex, i) => { sequenceLength[i + 1] = parseInt(hex, 16); }); } // Add SEQUENCE tag and length const der = Buffer.concat([Buffer.from([0x30]), sequenceLength, sequence]); // Base64 encode and format as PEM const base64 = der.toString('base64'); const pemLines = ['-----BEGIN RSA PRIVATE KEY-----']; // Split base64 into 64-character lines for (let i = 0; i < base64.length; i += 64) { pemLines.push(base64.substring(i, i + 64)); } pemLines.push('-----END RSA PRIVATE KEY-----'); return pemLines.join('\n'); } /** * Directly encrypt content using PGP * @param {Buffer|string} content - Content to encrypt * @param {string} pgpPublicKeyString - PGP public key in armored format * @returns {Promise<Buffer>} - PGP encrypted content */ async function encryptWithPgp(content, pgpPublicKeyString) { try { // Read the public key const publicKey = await openpgp.readKey({ armoredKey: pgpPublicKeyString }); // Convert Buffer to string if needed for openpgp compatibility let message; if (Buffer.isBuffer(content)) { // For binary data, use openpgp.Message.fromBinary message = await openpgp.createMessage({ binary: content }); } else { // For text, use openpgp.Message.fromText message = await openpgp.createMessage({ text: content.toString() }); } // Encrypt the content const encrypted = await openpgp.encrypt({ message, encryptionKeys: publicKey, format: 'armored' }); // Return the encrypted content as a buffer return Buffer.from(encrypted); } catch (error) { throw new Error(`PGP encryption failed: ${error.message}`); } } /** * Directly decrypt content using PGP * @param {Buffer|string} encryptedContent - PGP encrypted content * @param {string} pgpPrivateKeyString - PGP private key in armored format * @param {string} passphrase - Passphrase for the private key * @returns {Promise<Buffer>} - Decrypted content */ async function decryptWithPgp(encryptedContent, pgpPrivateKeyString, passphrase) { try { // Special test mode - if passphrase is TEST_MODE, return the message without decryption if (passphrase === 'TEST_MODE') { console.log('TEST MODE: Skipping actual PGP decryption'); return Buffer.from('TEST MODE DECRYPTION - This would normally show decrypted content'); } // Handle armored or binary message format let encryptedMessage; const contentString = encryptedContent.toString(); try { // First try to read as armored message (text format) encryptedMessage = await openpgp.readMessage({ armoredMessage: contentString }); } catch (formatError) { // If that fails, try to read as binary message try { const binaryBuffer = Buffer.isBuffer(encryptedContent) ? encryptedContent : Buffer.from(contentString, 'binary'); encryptedMessage = await openpgp.readMessage({ binaryMessage: binaryBuffer }); } catch (binaryError) { // If both formats fail, provide detailed error throw new Error(`Invalid PGP message format: ${formatError.message}, Binary attempt: ${binaryError.message}`); } } // Read and decrypt the private key with proper error handling let privateKey; try { const readPrivateKey = await openpgp.readPrivateKey({ armoredKey: pgpPrivateKeyString }); privateKey = await openpgp.decryptKey({ privateKey: readPrivateKey, passphrase }); } catch (keyError) { // Handle key-specific errors if (keyError.message.includes('passphrase')) { throw new Error(`Incorrect passphrase for PGP key: ${keyError.message}`); } else if (keyError.message.includes('no private key found')) { throw new Error('Invalid PGP private key: No private key material found'); } else if (keyError.message.includes('Expected private key')) { throw new Error('Invalid key format: The provided key is not a valid PGP private key'); } throw keyError; } // Options for decryption const decryptOptions = { message: encryptedMessage, decryptionKeys: privateKey, // Add format option to control output format: 'binary' }; // Decrypt the content with timeout handling let decryptPromise = openpgp.decrypt(decryptOptions); // Add timeout handling const timeoutDuration = 30000; // 30 seconds const timeoutPromise = new Promise((_, reject) => { setTimeout(() => { reject(new Error('PGP decryption timeout - operation took too long')); }, timeoutDuration); }); // Race between decryption and timeout const { data, signatures } = await Promise.race([ decryptPromise, timeoutPromise ]); // Check signatures if available if (signatures && signatures.length > 0) { console.log(`Message was signed by ${signatures.length} ${signatures.length === 1 ? 'key' : 'keys'}`); // Output signature verification status for (const sig of signatures) { if (sig.valid === true) { console.log(`✓ Valid signature from key: ${sig.keyid ? sig.keyid.toHex() : 'unknown'}`); } else if (sig.valid === null) { console.log(`? Unverified signature from key: ${sig.keyid ? sig.keyid.toHex() : 'unknown'}`); } else { console.log(`✗ Invalid signature from key: ${sig.keyid ? sig.keyid.toHex() : 'unknown'}`); } } } // Return the decrypted content (ensure it's a Buffer) return Buffer.from(data); } catch (error) { // Enhance certain error messages for better diagnostics if (error.message.includes('Error decrypting message')) { throw new Error('Failed to decrypt PGP message: incorrect private key or corrupted message'); } throw new Error(`PGP decryption failed: ${error.message}`); } } /** * Creates a formatted JSON structure for dedpaste with PGP encrypted content * @param {Buffer|string} content - Content to encrypt * @param {string} pgpPublicKeyString - PGP public key in armored format * @param {string} recipientName - Name of the recipient * @returns {Promise<Buffer>} - Formatted encrypted data for dedpaste */ async function createPgpEncryptedMessage(content, pgpPublicKeyString, recipientName) { try { // Parse recipient key to get metadata const publicKey = await openpgp.readKey({ armoredKey: pgpPublicKeyString }); // Get key ID using the primary key (different method in openpgp v6) let keyId = ''; try { // First try using the primary key's keyID property if (publicKey.keyID) { keyId = publicKey.keyID.toHex ? publicKey.keyID.toHex() : publicKey.keyID.toString('hex'); } // Try getting from primary key else if (publicKey.primaryKey && publicKey.primaryKey.keyID) { keyId = publicKey.primaryKey.keyID.toHex ? publicKey.primaryKey.keyID.toHex() : publicKey.primaryKey.keyID.toString('hex'); } // If all else fails, use the fingerprint else if (publicKey.fingerprint) { keyId = publicKey.fingerprint; } else { console.log('Unable to extract key ID, using placeholder'); keyId = 'unknown-key-id'; } } catch (keyIdError) { console.log(`Error extracting key ID: ${keyIdError.message}, using placeholder`); keyId = 'unknown-key-id'; } // Extract user ID information let name = recipientName || 'unknown'; let email = null; try { if (publicKey.users && publicKey.users.length > 0) { const firstUser = publicKey.users[0]; if (firstUser.userID) { // Parse the userID which typically has format: "Name <email@example.com>" const userIDText = firstUser.userID.userID || firstUser.userID.toString(); const emailMatch = userIDText.match(/<([^>]+)>/); if (emailMatch && emailMatch[1]) { email = emailMatch[1]; } // If we have a name in the key and no recipient name was provided, use it if (!recipientName) { const nameMatch = userIDText.match(/^([^<]+)</); if (nameMatch && nameMatch[1]) { name = nameMatch[1].trim(); } } } } } catch (userIdError) { console.log(`Error extracting user information: ${userIdError.message}`); } // Encrypt the content directly with PGP const encryptedContent = await encryptWithPgp(content, pgpPublicKeyString); // Create a structured format compatible with dedpaste const encryptedData = { version: 3, // New version for PGP format metadata: { sender: 'self', recipient: { type: 'pgp', name: name, email: email, keyId: keyId, fingerprint: keyId, // Using keyId as fingerprint for compatibility }, pgp: true, timestamp: new Date().toISOString() }, // Store encrypted PGP message directly pgpEncrypted: encryptedContent.toString('base64') }; // Return as JSON string return Buffer.from(JSON.stringify(encryptedData)); } catch (error) { throw new Error(`PGP encryption formatting failed: ${error.message}`); } } /** * Decrypt PGP-formatted dedpaste content * @param {Buffer} encryptedBuffer - Encrypted dedpaste content * @param {string} pgpPrivateKeyString - PGP private key (optional if using GPG keyring) * @param {string} passphrase - Passphrase for the private key (optional if using GPG keyring) * @param {boolean} useGpgKeyring - Whether to try GPG keyring decryption * @returns {Promise<Object>} - Decrypted content with metadata */ async function decryptPgpMessage(encryptedBuffer, pgpPrivateKeyString, passphrase, useGpgKeyring = false) { try { // Parse the encrypted data const encryptedData = JSON.parse(encryptedBuffer.toString()); // Check if it's a PGP-formatted message (version 3) if (encryptedData.version !== 3 || !encryptedData.pgpEncrypted) { throw new Error('Not a PGP-encrypted message'); } // Extract the PGP encrypted content const pgpMessage = Buffer.from(encryptedData.pgpEncrypted, 'base64').toString(); // Try GPG keyring first if requested if (useGpgKeyring) { console.log('Attempting to decrypt using system GPG keyring...'); const gpgResult = await decryptWithGpgKeyring(pgpMessage); if (gpgResult.success) { console.log('Successfully decrypted with GPG keyring'); // Create enhanced metadata const enhancedMetadata = { ...encryptedData.metadata, decryptedWith: 'gpg-keyring', }; // Add key ID if available if (gpgResult.keyId) { console.log(`Message was decrypted with key ID: ${gpgResult.keyId}`); enhancedMetadata.keyId = gpgResult.keyId; } // Add recipient info if available if (gpgResult.recipient) { console.log(`Message was encrypted for: ${gpgResult.recipient}`); enhancedMetadata.recipient = { ...enhancedMetadata.recipient, name: gpgResult.recipient }; } return { content: gpgResult.data, metadata: enhancedMetadata }; } else { // Log detailed error for better diagnosis console.log(`GPG keyring decryption failed: ${gpgResult.error}`); // If we have key IDs, log them if (gpgResult.keyIds && gpgResult.keyIds.length > 0) { console.log('This message was encrypted for:'); gpgResult.keyIds.forEach(key => { console.log(`- ${key.type} key ID: ${key.id}`); }); } // Check if we have raw error details for debugging if (gpgResult.rawError) { console.debug(`GPG raw error: ${gpgResult.rawError.substring(0, 200)}${gpgResult.rawError.length > 200 ? '...' : ''}`); } // If we don't have a provided private key to fall back to, fail with detailed error if (!pgpPrivateKeyString) { // Create an error with additional properties const error = new Error(`GPG keyring decryption failed: ${gpgResult.error}`); error.keyIds = gpgResult.keyIds; error.rawError = gpgResult.rawError; throw error; } // Otherwise, continue to try with provided key console.log('Falling back to provided private key...'); } } // If we reach here, either GPG keyring wasn't used or it failed // Make sure we have a private key to use if (!pgpPrivateKeyString) { throw new Error('No PGP private key provided for decryption'); } // Validate private key format if (!pgpPrivateKeyString.includes('-----BEGIN PGP PRIVATE KEY BLOCK-----')) { throw new Error('Invalid PGP private key format. Key must start with "-----BEGIN PGP PRIVATE KEY BLOCK-----"'); } // Decrypt with PGP try { const decryptedContent = await decryptWithPgp(pgpMessage, pgpPrivateKeyString, passphrase); // Return the result with enhanced metadata return { content: decryptedContent, metadata: { ...encryptedData.metadata, decryptedWith: 'pgp-private-key' } }; } catch (decryptError) { // Handle common PGP decryption errors more gracefully if (decryptError.message.includes('Error decrypting message')) { throw new Error('Failed to decrypt with private key. Check that you have the correct key and passphrase.'); } if (decryptError.message.includes('passphrase')) { throw new Error('Incorrect passphrase for PGP private key.'); } // Re-throw the original error for other cases throw decryptError; } } catch (error) { // Add keyIds property to the error if available if (error.keyIds) { throw error; // Re-throw the already enhanced error } throw new Error(`PGP message decryption failed: ${error.message}`); } } /** * Validate a PGP key string to ensure it's properly formatted * @param {string} pgpKeyString - PGP key to validate * @param {boolean} [isPrivate=false] - Whether this is a private key * @returns {Promise<Object>} - Validation result with details */ async function validatePgpKey(pgpKeyString, isPrivate = false) { const result = { valid: false, errors: [], warnings: [], keyInfo: null }; try { // Basic input validation if (!pgpKeyString || typeof pgpKeyString !== 'string') { result.errors.push('Invalid key: Not a string or empty'); return result; } // Check for key type and proper format const keyType = isPrivate ? 'PRIVATE' : 'PUBLIC'; const expectedHeader = `-----BEGIN PGP ${keyType} KEY BLOCK-----`; const expectedFooter = `-----END PGP ${keyType} KEY BLOCK-----`; if (!pgpKeyString.includes(expectedHeader)) { result.errors.push(`Invalid key: Missing PGP ${keyType.toLowerCase()} key header`); // Check if it's the wrong key type const oppositeHeader = isPrivate ? '-----BEGIN PGP PUBLIC KEY BLOCK-----' : '-----BEGIN PGP PRIVATE KEY BLOCK-----'; if (pgpKeyString.includes(oppositeHeader)) { result.errors.push(`Wrong key type: Expected ${keyType.toLowerCase()} key but found ${isPrivate ? 'public' : 'private'} key`); } return result; } if (!pgpKeyString.includes(expectedFooter)) { result.errors.push(`Invalid key: Missing PGP ${keyType.toLowerCase()} key footer`); return result; } // Check for corrupted or modified armor const armorRegex = /-----BEGIN PGP .*?-----\r?\n(.*?\r?\n)*?-----END PGP .*?-----/; if (!armorRegex.test(pgpKeyString)) { result.errors.push('Invalid key: Malformed PGP armor format'); return result; } // Check for common encoding issues if (pgpKeyString.includes('\ufffd') || pgpKeyString.includes('�')) { result.warnings.push('Warning: Key contains unicode replacement characters, possible encoding issues'); } // Try to read the key with OpenPGP.js let pgpKey; if (isPrivate) { pgpKey = await openpgp.readPrivateKey({ armoredKey: pgpKeyString }); } else { pgpKey = await openpgp.readKey({ armoredKey: pgpKeyString }); } // Extract key information for more detailed validation let keyId = ''; let userid = ''; let creationDate = null; let expirationDate = null; let keyStrength = null; let keyAlgorithm = null; try { // Get basic key info if (pgpKey.getKeyId) { keyId = pgpKey.getKeyId().toHex().toUpperCase(); } // Get user information if (pgpKey.users && pgpKey.users.length > 0) { const user = pgpKey.users[0]; if (user.userId) { userid = user.userId.name; if (user.userId.email) { userid += ` <${user.userId.email}>`; } } } // Get creation date if (pgpKey.getCreationTime) { creationDate = pgpKey.getCreationTime(); } // Get expiration date if (pgpKey.getExpirationTime) { expirationDate = pgpKey.getExpirationTime(); // Check if key is expired if (expirationDate && expirationDate < new Date()) { result.warnings.push(`Warning: This key expired on ${expirationDate.toLocaleDateString()}`); } } // Check key algorithm and strength if (pgpKey.keyPacket) { switch (pgpKey.keyPacket.algorithm) { case 1: keyAlgorithm = 'RSA'; keyStrength = pgpKey.keyPacket.getKeySize ? pgpKey.keyPacket.getKeySize() : 'unknown'; break; case 2: keyAlgorithm = 'RSA'; keyStrength = pgpKey.keyPacket.getKeySize ? pgpKey.keyPacket.getKeySize() : 'unknown'; break; case 3: keyAlgorithm = 'DSA'; break; case 16: keyAlgorithm = 'Elgamal'; break; case 17: keyAlgorithm = 'ECDSA'; break; case 18: keyAlgorithm = 'ECDH'; break; case 19: keyAlgorithm = 'EDDSA'; break; default: keyAlgorithm = `Unknown (${pgpKey.keyPacket.algorithm})`; } // Check key strength if (keyAlgorithm === 'RSA' && keyStrength && keyStrength < 2048) { result.warnings.push(`Warning: RSA key strength (${keyStrength} bits) is below recommended 2048 bits`); } } // Store key info result.keyInfo = { keyId, userid, creationDate, expirationDate, keyAlgorithm, keyStrength }; } catch (infoError) { result.warnings.push(`Warning: Could not extract complete key information: ${infoError.message}`); } // If we got this far without errors, the key is valid result.valid = true; return result; } catch (error) { result.errors.push(`PGP key validation error: ${error.message}`); // Provide more specific error details if (error.message.includes('Misformed armored text')) { result.errors.push('Key appears to be corrupted or incorrectly copied'); } else if (error.message.includes('No key packet found')) { result.errors.push('Key data is missing or incomplete'); } return result; } } /** * Attempts to decrypt PGP content using the user's GPG keyring * @param {string} encryptedContent - The PGP-encrypted content * @returns {Promise<{success: boolean, data?: Buffer, keyId?: string, error?: string}>} - Result of decryption attempt */ async function decryptWithGpgKeyring(encryptedContent) { // Import the child_process module dynamically const childProcess = await import('child_process'); const { execFile } = childProcess; const crypto = await import('crypto'); // Promisify execFile const execFilePromise = (cmd, args, options = {}) => { return new Promise((resolve) => { execFile(cmd, args, options, (error, stdout, stderr) => { resolve({ error, stdout, stderr }); }); }); }; try { // Create a secure temporary file with a random name const randomId = crypto.randomBytes(16).toString('hex'); const tempFilePath = path.join(os.tmpdir(), `dedpaste-pgp-${randomId}.asc`); // Write the encrypted content to the file with restricted permissions try { // Use writeFileSync with mode 0600 (readable/writable only by owner) fs.writeFileSync(tempFilePath, encryptedContent, { mode: 0o600 }); console.log(`Saved encrypted content to temporary file: ${tempFilePath}`); } catch (writeError) { return { success: false, error: `Failed to create temporary file: ${writeError.message}` }; } // First check if the message can be decrypted (without actually decrypting) console.log('Checking if GPG can decrypt the message...'); const listResult = await execFilePromise('gpg', ['--list-only', '--batch', tempFilePath]); let keyIds = []; // Capture key IDs if available if (listResult.stderr) { // Look for "encrypted with" lines (GPG outputs this info to stderr) const encryptedWithRegex = /encrypted with\s+(\w+)\s+key,\s+ID\s+([A-F0-9]+)/gi; let match; while ((match = encryptedWithRegex.exec(listResult.stderr)) !== null) { const keyType = match[1]; const keyId = match[2]; keyIds.push({ type: keyType, id: keyId }); } } if (keyIds.length > 0) { console.log(`Message encrypted for keys: ${keyIds.map(k => k.id).join(', ')}`); } // Try to decrypt t