dedpaste
Version:
CLI pastebin application using Cloudflare Workers and R2
1,331 lines (1,155 loc) • 53.9 kB
JavaScript
// 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