Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 56 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ A secure, privacy-focused pastebin CLI application powered by Cloudflare Workers
- **RSA key pairs** - Support for PEM format keys
- **Friend-to-friend encryption** - Manage keys for trusted contacts
- **Keybase integration** - Import keys with proof verification
- **GitHub integration** - Fetch GPG keys directly from GitHub profiles
- **Recipient groups** - Create groups for easy multi-recipient encryption
- **Smart prefix shortcuts** - Use `gh:` and `kb:` for quick GitHub/Keybase access
- **Auto-PGP detection** - No more manual `--pgp` flags - encryption type auto-detected
- **Zero-knowledge architecture** - Server never sees unencrypted content

### 🛠️ Developer Features
Expand Down Expand Up @@ -200,8 +204,49 @@ dedpaste keys --keybase username --keybase-name bob
# Skip verification of proofs
dedpaste keys --keybase username --no-verify

# Send to a Keybase user
# Send to a Keybase user (traditional syntax)
echo "secret" | dedpaste send --encrypt --for keybase:username

# Send to a Keybase user (NEW: short syntax - auto-detects PGP!)
echo "secret" | dedpaste send --encrypt --for kb:username
```

### GitHub Integration

```bash
# Add a GitHub user's GPG key
dedpaste keys --github torvalds

# Add with custom name
dedpaste keys --github torvalds --github-name linus

# Send to a GitHub user (traditional syntax)
echo "secret" | dedpaste send --encrypt --for github:torvalds

# Send to a GitHub user (NEW: short syntax - auto-detects PGP!)
echo "secret" | dedpaste send --encrypt --for gh:torvalds
```

### Recipient Groups (NEW!)

```bash
# Create a group of recipients
dedpaste keys --group-create team gh:alice kb:bob charlie@example.com

# Add members to an existing group
dedpaste keys --group-add team gh:dave

# Remove members from a group
dedpaste keys --group-remove team kb:bob

# List all groups
dedpaste keys --group-list

# Delete a group
dedpaste keys --group-delete team

# Encrypt for an entire group
echo "Team announcement" | dedpaste send --encrypt --for team
```

### Sending Encrypted Pastes (`send` Command)
Expand All @@ -213,6 +258,16 @@ dedpaste send --list-friends
# Send an encrypted message to a friend
echo "Secret message for Alice" | dedpaste send --encrypt --for alice

# NEW: Send to GitHub/Keybase users with short prefixes
echo "Quick message" | dedpaste send --encrypt --for gh:torvalds
echo "Quick message" | dedpaste send --encrypt --for kb:username

# NEW: Send to multiple recipients (space-separated)
echo "Team update" | dedpaste send --encrypt --for gh:alice kb:bob charlie@example.com

# NEW: Send to a group
echo "Group message" | dedpaste send --encrypt --for team

# Send an encrypted one-time message to a friend
echo "Secret one-time message" | dedpaste send --encrypt --for alice --temp

Expand Down
205 changes: 116 additions & 89 deletions cli/encryptionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.');

Copilot AI Oct 23, 2025

Copy link

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.

Suggested change
console.warn('Note: Encrypting separately for each recipient. Content will be duplicated.');
console.warn('Warning: Only the first recipient will receive the encrypted content. Multi-recipient encryption is not yet implemented.');

Copilot uses AI. Check for mistakes.

// 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)) {

Copilot AI Oct 23, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The condition Array.isArray(recipientName) in this check is unreachable because arrays are already handled earlier (lines 54-137), so this path would never execute for array inputs. The condition should just check !recipientName or include a comment explaining this is a defensive check.

Suggested change
if (!recipientName || Array.isArray(recipientName)) {
if (!recipientName) {

Copilot uses AI. Check for mistakes.
throw new Error('PGP encryption requires specifying a single recipient with --for. Self-encryption is not supported for PGP.');
}
return await createPgpEncryptedMessage(content, publicKey, recipientName);
}
Expand Down
Loading