dedpaste
Version:
CLI pastebin application using Cloudflare Workers and R2
327 lines (285 loc) • 12.3 kB
JavaScript
// Enhanced encryption and decryption utilities
import crypto from 'crypto';
import { promises as fsPromises } from 'fs';
import { getKey, updateLastUsed } from './keyManager.js';
import { createPgpEncryptedMessage, decryptPgpMessage } from './pgpUtils.js';
// Encrypt content for a specific recipient
async function encryptContent(content, recipientName = null, usePgp = false) {
try {
let publicKey;
let recipientInfo;
let keyType = 'standard';
if (recipientName) {
// Try to find the key in any key store (friend, PGP, or Keybase)
const friendKey = await getKey('any', recipientName);
if (!friendKey) {
throw new Error(`Recipient "${recipientName}" not found in key database`);
}
if (friendKey.type === 'pgp') {
keyType = 'pgp';
publicKey = await fsPromises.readFile(friendKey.path, 'utf8');
recipientInfo = {
type: 'pgp',
name: recipientName,
fingerprint: friendKey.fingerprint,
email: friendKey.email
};
} else if (friendKey.type === 'keybase') {
keyType = 'pgp'; // Keybase keys are also PGP keys
publicKey = await fsPromises.readFile(friendKey.path, 'utf8');
recipientInfo = {
type: 'keybase',
name: recipientName,
fingerprint: friendKey.fingerprint,
username: friendKey.username,
email: friendKey.email
};
} else {
// Standard RSA key
publicKey = await fsPromises.readFile(friendKey.public, 'utf8');
recipientInfo = {
type: 'friend',
name: recipientName,
fingerprint: friendKey.fingerprint
};
}
// Update last used timestamp
await updateLastUsed(recipientName);
} else {
// Encrypt for self
const selfKey = await getKey('self');
if (!selfKey) {
throw new Error('No personal key found. Generate one first.');
}
publicKey = await fsPromises.readFile(selfKey.public, 'utf8');
recipientInfo = {
type: 'self',
fingerprint: selfKey.fingerprint
};
}
// If usePgp is true or the key is a PGP key, use PGP encryption
if (usePgp || keyType === 'pgp') {
// PGP encryption requires a recipient
if (!recipientName) {
throw new Error('PGP encryption requires specifying a recipient with --for. Self-encryption is not supported for PGP.');
}
return await createPgpEncryptedMessage(content, publicKey, recipientName);
}
// Otherwise use standard RSA/AES hybrid encryption
// Generate a random symmetric key
const symmetricKey = crypto.randomBytes(32); // 256 bits for AES-256
const iv = crypto.randomBytes(16); // 128 bits for AES IV
// Encrypt the content with the symmetric key
const cipher = crypto.createCipheriv('aes-256-gcm', symmetricKey, iv);
let encryptedContent = cipher.update(content);
encryptedContent = Buffer.concat([encryptedContent, cipher.final()]);
const authTag = cipher.getAuthTag();
// Encrypt the symmetric key with the recipient's public key
let encryptedSymmetricKey;
try {
// Check if the public key is in PEM format
if (!publicKey.includes('-----BEGIN PUBLIC KEY-----') &&
!publicKey.includes('-----BEGIN RSA PUBLIC KEY-----')) {
console.log('Public key is not in expected PEM format, attempting to convert...');
// Try to parse it as a PGP key and extract the RSA key
if (publicKey.includes('-----BEGIN PGP PUBLIC KEY BLOCK-----')) {
throw new Error('PGP key detected. For PGP keys, use the --pgp flag to enable PGP encryption mode.');
} else {
throw new Error('Unsupported key format. Key must be in PEM format.');
}
}
console.log('Using RSA encryption with PEM format public key');
encryptedSymmetricKey = crypto.publicEncrypt(
{
key: publicKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
},
symmetricKey
);
} catch (error) {
throw new Error(`RSA encryption failed: ${error.message}`);
}
// Combine everything into a single structure with metadata
const encryptedData = {
version: 2, // Standard version with metadata
metadata: {
sender: 'self',
recipient: recipientInfo,
timestamp: new Date().toISOString()
},
encryptedKey: encryptedSymmetricKey.toString('base64'),
iv: iv.toString('base64'),
authTag: authTag.toString('base64'),
encryptedContent: encryptedContent.toString('base64')
};
// Return as JSON string
return Buffer.from(JSON.stringify(encryptedData));
} catch (error) {
throw new Error(`Encryption error: ${error.message}`);
}
}
// Decrypt content
async function decryptContent(encryptedBuffer, pgpPrivateKeyPath = null, pgpPassphrase = null, useGpgKeyring = true) {
try {
// Parse the encrypted data
const encryptedData = JSON.parse(encryptedBuffer.toString());
// Handle different versions
if (encryptedData.version === 1) {
// Legacy format - try to decrypt with personal key
return decryptLegacyContent(encryptedData);
} else if (encryptedData.version === 2) {
// Standard format with metadata
return decryptV2Content(encryptedData);
} else if (encryptedData.version === 3) {
// PGP encrypted format
// If using GPG keyring is enabled, try that first without requiring a private key path
if (useGpgKeyring) {
try {
console.log('Attempting to decrypt with system GPG keyring...');
// We don't need a private key file or passphrase for this method
const result = await decryptPgpMessage(encryptedBuffer, null, null, true);
// If successful, return the result
if (result && result.content) {
return result;
}
} catch (gpgError) {
// If GPG keyring decryption fails, log the error and fall back to using private key
console.log(`GPG keyring decryption failed: ${gpgError.message}`);
console.log('Falling back to private key decryption if available...');
// Check if we have more detailed information about why GPG failed
if (gpgError.keyIds && gpgError.keyIds.length > 0) {
console.log('This message was encrypted for the following key IDs:');
gpgError.keyIds.forEach(key => {
console.log(`- ${key.type} key: ${key.id}`);
});
}
}
}
// If we reach here, either GPG keyring wasn't used or it failed, so use private key
if (!pgpPrivateKeyPath) {
// Try to find the user's PGP private key in pgp directory
const selfKey = await getKey('self', null, 'pgp');
if (!selfKey) {
throw new Error('PGP encrypted message detected but no PGP private key found. Try importing a key with --import-pgp-key or use --use-gpg-keyring to decrypt with GPG.');
}
pgpPrivateKeyPath = selfKey.private;
}
// Prompt for passphrase if not provided
if (!pgpPassphrase) {
// Try to prompt for passphrase if running in interactive mode
try {
const inquirer = await import('inquirer');
const { passphrase } = await inquirer.default.prompt([{
type: 'password',
name: 'passphrase',
message: 'Enter PGP key passphrase:',
mask: '*'
}]);
pgpPassphrase = passphrase;
} catch (promptError) {
// If we can't prompt (e.g., non-interactive environment), throw error
throw new Error('PGP passphrase required for decryption. Please provide with --pgp-passphrase option.');
}
}
// Read the PGP private key
const privateKeyContent = await fsPromises.readFile(pgpPrivateKeyPath, 'utf8');
// Check for test mode
if (pgpPassphrase === 'TEST_MODE') {
console.log('Using TEST_MODE for PGP decryption');
}
// Decrypt with PGP using the provided private key
return await decryptPgpMessage(encryptedBuffer, privateKeyContent, pgpPassphrase, false);
} else {
throw new Error(`Unsupported encryption version: ${encryptedData.version}`);
}
} catch (error) {
// Enhance error message for common issues
if (error.message.includes('Unexpected token') || error.message.includes('JSON')) {
throw new Error('Invalid encrypted message format. This may not be a dedpaste encrypted message.');
}
if (error.message.includes('Bad passphrase')) {
throw new Error('Incorrect passphrase for PGP key. Please try again with the correct passphrase.');
}
throw new Error(`Decryption error: ${error.message}`);
}
}
// Decrypt legacy content (version 1)
async function decryptLegacyContent(encryptedData) {
// Get personal private key
const selfKey = await getKey('self');
if (!selfKey) {
throw new Error('No personal key found for decryption');
}
const privateKey = await fsPromises.readFile(selfKey.private, 'utf8');
// Decrypt the symmetric key with the private key
const encryptedSymmetricKey = Buffer.from(encryptedData.encryptedKey, 'base64');
const symmetricKey = crypto.privateDecrypt(
{
key: privateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
},
encryptedSymmetricKey
);
// Decrypt the content with the symmetric key
const iv = Buffer.from(encryptedData.iv, 'base64');
const authTag = Buffer.from(encryptedData.authTag, 'base64');
const encryptedContent = Buffer.from(encryptedData.encryptedContent, 'base64');
const decipher = crypto.createDecipheriv('aes-256-gcm', symmetricKey, iv);
decipher.setAuthTag(authTag);
let decryptedContent = decipher.update(encryptedContent);
decryptedContent = Buffer.concat([decryptedContent, decipher.final()]);
return {
content: decryptedContent,
metadata: { version: 1, legacy: true }
};
}
// Decrypt version 2 content
async function decryptV2Content(encryptedData) {
const metadata = encryptedData.metadata;
let privateKey;
// Get self key for comparison
const selfKey = await getKey('self');
if (!selfKey) {
throw new Error('No personal key found for decryption');
}
// Determine which key to use
if (metadata.recipient.type === 'self') {
// Encrypted for self
privateKey = await fsPromises.readFile(selfKey.private, 'utf8');
} else if (metadata.recipient.type === 'friend' && metadata.recipient.name === 'self') {
// Encrypted by a friend for us
privateKey = await fsPromises.readFile(selfKey.private, 'utf8');
} else if (metadata.recipient.fingerprint && metadata.recipient.fingerprint === selfKey.fingerprint) {
// Fingerprint matches our key, even if the name doesn't match
console.log(`Note: This paste was labeled for "${metadata.recipient.name}" but the fingerprint matches your key.`);
privateKey = await fsPromises.readFile(selfKey.private, 'utf8');
} else {
throw new Error(`This paste was encrypted for ${metadata.recipient.name}, not for you`);
}
// Decrypt the symmetric key with the private key
const encryptedSymmetricKey = Buffer.from(encryptedData.encryptedKey, 'base64');
const symmetricKey = crypto.privateDecrypt(
{
key: privateKey,
padding: crypto.constants.RSA_PKCS1_OAEP_PADDING,
},
encryptedSymmetricKey
);
// Decrypt the content with the symmetric key
const iv = Buffer.from(encryptedData.iv, 'base64');
const authTag = Buffer.from(encryptedData.authTag, 'base64');
const encryptedContent = Buffer.from(encryptedData.encryptedContent, 'base64');
const decipher = crypto.createDecipheriv('aes-256-gcm', symmetricKey, iv);
decipher.setAuthTag(authTag);
let decryptedContent = decipher.update(encryptedContent);
decryptedContent = Buffer.concat([decryptedContent, decipher.final()]);
return {
content: decryptedContent,
metadata: encryptedData.metadata
};
}
// Export functions
export {
encryptContent,
decryptContent
};