Repository navigation
feat: enhance GitHub/Keybase encryption with smart prefixes and recip… #99
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -42,131 +42,158 @@ interface EncryptedDataV3 { | |||||
|
|
||||||
| type EncryptedData = EncryptedDataV1 | EncryptedDataV2 | EncryptedDataV3; | ||||||
|
|
||||||
| // Encrypt content for a specific recipient | ||||||
| // Encrypt content for a specific recipient (or multiple recipients) | ||||||
| export async function encryptContent( | ||||||
| content: string, | ||||||
| recipientName: string | null = null, | ||||||
| recipientName: string | string[] | null = null, | ||||||
| usePgp: boolean = false, | ||||||
| refreshGithubKeys: boolean = false | ||||||
| ): Promise<Buffer> { | ||||||
| try { | ||||||
| // Handle multiple recipients | ||||||
| if (Array.isArray(recipientName) && recipientName.length > 0) { | ||||||
| // Import resolver | ||||||
| const { resolveRecipients, isPgpRecipient } = await import('./recipientResolver.js'); | ||||||
|
|
||||||
| // Resolve all recipients | ||||||
| const resolved = await resolveRecipients(recipientName, { | ||||||
| silent: false, | ||||||
| autoFetch: true | ||||||
| }); | ||||||
|
|
||||||
| if (resolved.length === 0) { | ||||||
| throw new Error('No valid recipients found'); | ||||||
| } | ||||||
|
|
||||||
| // For multiple recipients, we need to check if all use PGP or all use standard | ||||||
| // For simplicity, we'll use the first recipient to determine encryption method | ||||||
| // In the future, we could enhance PGP to support multiple recipients in one message | ||||||
| const firstRecipient = resolved[0]; | ||||||
| const shouldUsePgp = usePgp || isPgpRecipient(firstRecipient); | ||||||
|
|
||||||
| if (shouldUsePgp) { | ||||||
| // For PGP with multiple recipients, we currently encrypt separately for each | ||||||
| // (Future enhancement: use OpenPGP multi-recipient encryption) | ||||||
| console.warn('Note: Encrypting separately for each recipient. Content will be duplicated.'); | ||||||
|
|
||||||
| // For now, just use the first recipient | ||||||
| // TODO: Implement proper multi-recipient PGP encryption | ||||||
| const keyInfo = firstRecipient.keyInfo!; | ||||||
| const keyPath = typeof keyInfo.path === 'string' ? keyInfo.path : keyInfo.path.public; | ||||||
| const publicKey = await fsPromises.readFile(keyPath, 'utf8'); | ||||||
|
|
||||||
| await updateLastUsed(firstRecipient.identifier); | ||||||
| return await createPgpEncryptedMessage(content, publicKey, firstRecipient.identifier); | ||||||
| } else { | ||||||
| // Standard encryption also only supports one recipient | ||||||
| // Use the first recipient | ||||||
| const keyInfo = firstRecipient.keyInfo!; | ||||||
| const publicKey = await fsPromises.readFile(keyInfo.public!, 'utf8'); | ||||||
| await updateLastUsed(firstRecipient.identifier); | ||||||
|
|
||||||
| // Continue with standard encryption (fall through to standard encryption logic below) | ||||||
| const recipientInfo: RecipientInfo = { | ||||||
| type: firstRecipient.type as any, | ||||||
| name: firstRecipient.identifier, | ||||||
| fingerprint: keyInfo.fingerprint, | ||||||
| username: keyInfo.username, | ||||||
| email: keyInfo.email | ||||||
| }; | ||||||
|
|
||||||
| // Generate a random symmetric key | ||||||
| const symmetricKey = crypto.randomBytes(32); | ||||||
| const iv = crypto.randomBytes(16); | ||||||
| const cipher = crypto.createCipheriv('aes-256-gcm', symmetricKey, iv); | ||||||
|
|
||||||
| let encryptedContent = cipher.update(content, 'utf8'); | ||||||
| encryptedContent = Buffer.concat([encryptedContent, cipher.final()]); | ||||||
| const authTag = cipher.getAuthTag(); | ||||||
|
|
||||||
| // Encrypt the symmetric key with the recipient's public key | ||||||
| const encryptedKey = crypto.publicEncrypt( | ||||||
| { | ||||||
| key: publicKey, | ||||||
| padding: crypto.constants.RSA_PKCS1_OAEP_PADDING, | ||||||
| oaepHash: 'sha256' | ||||||
| }, | ||||||
| symmetricKey | ||||||
| ); | ||||||
|
|
||||||
| const encryptedData: EncryptedDataV2 = { | ||||||
| version: 2, | ||||||
| metadata: { | ||||||
| sender: 'self', | ||||||
| recipient: recipientInfo, | ||||||
| timestamp: new Date().toISOString() | ||||||
| }, | ||||||
| encryptedKey: encryptedKey.toString('base64'), | ||||||
| iv: iv.toString('base64'), | ||||||
| authTag: authTag.toString('base64'), | ||||||
| encryptedContent: encryptedContent.toString('base64') | ||||||
| }; | ||||||
|
|
||||||
| return Buffer.from(JSON.stringify(encryptedData)); | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // Single recipient or self encryption (backward compatible path) | ||||||
| let publicKey: string; | ||||||
| let recipientInfo: RecipientInfo; | ||||||
| let keyType: 'standard' | 'pgp' = 'standard'; | ||||||
|
|
||||||
| if (recipientName) { | ||||||
| // Try to find the key in any key store (friend, PGP, Keybase, or GitHub) | ||||||
| let friendKey = await getKey('any', recipientName); | ||||||
|
|
||||||
| // If not found and it starts with "github:", try to fetch it just-in-time | ||||||
| if (!friendKey && recipientName.startsWith('github:')) { | ||||||
| const username = recipientName.replace('github:', ''); | ||||||
| if (recipientName && typeof recipientName === 'string') { | ||||||
| // Use the resolver for single recipient | ||||||
| const { resolveRecipient, isPgpRecipient } = await import('./recipientResolver.js'); | ||||||
|
|
||||||
| try { | ||||||
| const { ensureGitHubKey } = await import('./githubUtils.js'); | ||||||
| await ensureGitHubKey(username, false, refreshGithubKeys); | ||||||
| const resolved = await resolveRecipient(recipientName, { | ||||||
| silent: false, | ||||||
| autoFetch: true | ||||||
| }); | ||||||
|
|
||||||
| // Try to get the key again after fetching | ||||||
| friendKey = await getKey('any', recipientName); | ||||||
| } catch (error: any) { | ||||||
| console.error(`\nError: Failed to fetch GitHub key for user "${username}"`); | ||||||
| console.error(`Details: ${error.message}`); | ||||||
| console.error(`\nPlease verify:`); | ||||||
| console.error(` 1. The GitHub username is correct`); | ||||||
| console.error(` 2. The user has a GPG key uploaded to GitHub`); | ||||||
| console.error(` 3. You have internet connectivity`); | ||||||
| console.error(`\nTo manually add a GitHub user's key, run:`); | ||||||
| console.error(` dedpaste keys --github ${username}`); | ||||||
| throw new Error(`Failed to fetch GitHub key for ${username}`); | ||||||
| } | ||||||
| if (!resolved.keyInfo) { | ||||||
| throw new Error(`Failed to resolve recipient "${recipientName}"`); | ||||||
| } | ||||||
|
|
||||||
| // Also check if we should refresh an existing GitHub key | ||||||
| if (friendKey && recipientName.startsWith('github:') && refreshGithubKeys) { | ||||||
| const username = recipientName.replace('github:', ''); | ||||||
| const friendKey = resolved.keyInfo; | ||||||
|
|
||||||
| try { | ||||||
| const { ensureGitHubKey } = await import('./githubUtils.js'); | ||||||
| await ensureGitHubKey(username, false, true); | ||||||
|
|
||||||
| // Reload the key after refreshing | ||||||
| friendKey = await getKey('any', recipientName); | ||||||
| } catch (error: any) { | ||||||
| console.warn(`Warning: Failed to refresh GitHub key, using cached version`); | ||||||
| } | ||||||
| // Determine key type | ||||||
| if (friendKey.type === 'pgp' || friendKey.type === 'keybase' || friendKey.type === 'github') { | ||||||
| keyType = 'pgp'; | ||||||
| } | ||||||
|
|
||||||
| if (!friendKey) { | ||||||
| throw new Error(`Recipient "${recipientName}" not found in key database`); | ||||||
| } | ||||||
|
|
||||||
| if (friendKey.type === 'pgp') { | ||||||
| keyType = 'pgp'; | ||||||
| const keyPath = typeof friendKey.path === 'string' ? friendKey.path : friendKey.path.public; | ||||||
| publicKey = await fsPromises.readFile(keyPath, '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 | ||||||
| const keyPath = typeof friendKey.path === 'string' ? friendKey.path : friendKey.path.public; | ||||||
| publicKey = await fsPromises.readFile(keyPath, 'utf8'); | ||||||
| const keyPath = typeof friendKey.path === 'string' ? friendKey.path : friendKey.path.public; | ||||||
| publicKey = await fsPromises.readFile(keyPath, 'utf8'); | ||||||
|
|
||||||
| recipientInfo = { | ||||||
| type: 'keybase', | ||||||
| name: recipientName, | ||||||
| fingerprint: friendKey.fingerprint, | ||||||
| username: friendKey.username, | ||||||
| email: friendKey.email | ||||||
| }; | ||||||
| } else if (friendKey.type === 'github') { | ||||||
| keyType = 'pgp'; // GitHub keys are also PGP keys | ||||||
| const keyPath = typeof friendKey.path === 'string' ? friendKey.path : friendKey.path.public; | ||||||
| publicKey = await fsPromises.readFile(keyPath, 'utf8'); | ||||||
| recipientInfo = { | ||||||
| type: resolved.type as any, | ||||||
| name: recipientName, | ||||||
| fingerprint: friendKey.fingerprint, | ||||||
| username: friendKey.username, | ||||||
| email: friendKey.email | ||||||
| }; | ||||||
|
|
||||||
| recipientInfo = { | ||||||
| type: 'github', | ||||||
| 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); | ||||||
| await updateLastUsed(resolved.identifier); | ||||||
| } 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', | ||||||
| name: '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.'); | ||||||
| if (!recipientName || Array.isArray(recipientName)) { | ||||||
|
||||||
| if (!recipientName || Array.isArray(recipientName)) { | |
| if (!recipientName) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This warning message is misleading because the code doesn't actually encrypt for multiple recipients - it only encrypts for the first recipient (line 86). Either implement true multi-recipient encryption or update the warning to accurately reflect that only the first recipient will receive the encrypted content.