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
35 changes: 29 additions & 6 deletions cli/encryptionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@ type EncryptedData = EncryptedDataV1 | EncryptedDataV2 | EncryptedDataV3;

// Encrypt content for a specific recipient
export async function encryptContent(
content: string,
recipientName: string | null = null,
usePgp: boolean = false
content: string,
recipientName: string | null = null,
usePgp: boolean = false,
refreshGithubKeys: boolean = false
): Promise<Buffer> {
try {
let publicKey: string;
Expand All @@ -60,16 +61,38 @@ export async function encryptContent(
// 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:', '');
console.log(`GitHub key not found locally, fetching from GitHub...`);

try {
const { ensureGitHubKey } = await import('./githubUtils.js');
await ensureGitHubKey(username, false);
await ensureGitHubKey(username, false, refreshGithubKeys);

// Try to get the key again after fetching
friendKey = await getKey('any', recipientName);
} catch (error: any) {
throw new Error(`Failed to fetch GitHub key for ${username}: ${error.message}`);
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}`);
Comment on lines +72 to +79

Copilot AI Oct 22, 2025

Copy link

Choose a reason for hiding this comment

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

This error messaging logic is duplicated from the existing error handling patterns. Consider extracting this into a shared error formatting utility function to maintain consistency and reduce duplication.

Copilot uses AI. Check for mistakes.
throw new Error(`Failed to fetch GitHub key for ${username}`);
}
}

// Also check if we should refresh an existing GitHub key
if (friendKey && recipientName.startsWith('github:') && refreshGithubKeys) {

Copilot AI Oct 22, 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 recipientName.startsWith('github:') is checked multiple times in this function (lines 62 and 84). Consider extracting this into a helper function like isGitHubKey(recipientName) to improve readability and maintainability.

Copilot uses AI. Check for mistakes.
const username = recipientName.replace('github:', '');

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`);
}
}

Expand Down
39 changes: 18 additions & 21 deletions cli/githubUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,15 +155,9 @@ export async function addGitHubKey(
}

// Fetch GPG key
if (!silent) {
console.log(`Fetching GPG key for GitHub user: ${sanitizedUsername}...`);
}
const gpgKeyData = await fetchGitHubGpgKey(sanitizedUsername);

// Import and validate PGP key
if (!silent) {
console.log(`Importing PGP key...`);
}
const keyInfo = await importPgpKey(gpgKeyData);

// Generate key name
Expand Down Expand Up @@ -196,35 +190,38 @@ export async function addGitHubKey(

/**
* Fetch GitHub key just-in-time (used during encryption)
* Checks if key exists in database, fetches if not
* Checks if key exists in database, fetches if not or if stale
* @param username GitHub username (without github: prefix)
* @param silent Whether to suppress console output
* @param forceRefresh Whether to force refresh even if cached
* @returns Key name and metadata
*/
export async function ensureGitHubKey(
username: string,
silent: boolean = false
silent: boolean = false,
forceRefresh: boolean = false
): Promise<{ name: string; fingerprint: string; email?: string }> {
const keyName = `github:${username}`;

// Check if key already exists
const { getKey } = await import('./keyManager.js');
const existingKey = await getKey('github', keyName);

if (existingKey) {
if (!silent) {
console.log(`Using cached GitHub key: ${keyName}`);
}
return {
name: keyName,
fingerprint: existingKey.fingerprint,
email: existingKey.email
};
}
if (existingKey && !forceRefresh) {
// Check if key is stale (older than 24 hours)
const lastFetched = existingKey.lastFetched ? new Date(existingKey.lastFetched) : null;
const now = new Date();
const maxAge = 24 * 60 * 60 * 1000; // 24 hours in milliseconds

Copilot AI Oct 22, 2025

Copy link

Choose a reason for hiding this comment

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

The 24-hour cache duration is hardcoded. Consider extracting this as a named constant (e.g., GITHUB_KEY_CACHE_DURATION_MS) at the module level to make it configurable and easier to maintain.

Copilot uses AI. Check for mistakes.

// Key doesn't exist, fetch it
if (!silent) {
console.log(`GitHub key not found locally, fetching from GitHub...`);
const isStale = lastFetched ? (now.getTime() - lastFetched.getTime() > maxAge) : true;

if (!isStale) {
return {
name: keyName,
fingerprint: existingKey.fingerprint,
email: existingKey.email
};
}
}

return await addGitHubKey(username, undefined, false, silent);
Expand Down
5 changes: 4 additions & 1 deletion cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ program
// GitHub options
.option('--github <username>', 'Fetch and add a GitHub user\'s GPG public key')
.option('--github-name <name>', 'Custom name for the GitHub user\'s key (optional)')
.option('--refresh-github-keys', 'Force refresh of cached GitHub keys (re-fetch from GitHub)')
// Debugging and logging options
.option('--verbose', 'Enable verbose logging (same as --log-level debug)')
.option('--debug', 'Enable debug mode with extensive logging (same as --log-level trace)')
Expand Down Expand Up @@ -676,6 +677,7 @@ Key Storage:
- Email: ${result.email || 'Not specified'}
- Fingerprint: ${result.fingerprint}
`);
process.exit(0);
}
catch (error) {
logger.error('Failed to fetch GitHub key', { error: error.message });
Expand Down Expand Up @@ -749,6 +751,7 @@ program
.option('--pgp', 'Use PGP encryption instead of hybrid RSA/AES')
.option('--pgp-key-file <path>', 'Use a specific PGP public key file for encryption')
.option('--pgp-armor', 'Output ASCII-armored PGP instead of binary format')
.option('--refresh-github-keys', 'Force refresh of cached GitHub keys when encrypting')
.addHelpText('after', `
Examples:
$ echo "Secret message" | dedpaste send --encrypt # Encrypt for yourself (RSA/AES)
Expand Down Expand Up @@ -903,7 +906,7 @@ Encryption:
}
else {
// Use the standard encryption flow with PGP option
const encryptResult = await encryptContent(content.toString('utf8'), recipientName, usePgp);
const encryptResult = await encryptContent(content.toString('utf8'), recipientName, usePgp, options.refreshGithubKeys);
content = typeof encryptResult === 'string' ? Buffer.from(encryptResult) : encryptResult;
}
// Log PGP mode if used
Expand Down
11 changes: 10 additions & 1 deletion cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,7 @@ interface SendOptions {
pgp?: boolean;
pgpKeyFile?: string;
pgpArmor?: boolean;
refreshGithubKeys?: boolean;
}

interface GetOptions {
Expand Down Expand Up @@ -302,6 +303,7 @@ program
// GitHub options
.option('--github <username>', 'Fetch and add a GitHub user\'s GPG public key')
.option('--github-name <name>', 'Custom name for the GitHub user\'s key (optional)')
.option('--refresh-github-keys', 'Force refresh of cached GitHub keys (re-fetch from GitHub)')
// Debugging and logging options
.option('--verbose', 'Enable verbose logging (same as --log-level debug)')
.option('--debug', 'Enable debug mode with extensive logging (same as --log-level trace)')
Expand Down Expand Up @@ -906,6 +908,7 @@ Key Storage:
- Email: ${result.email || 'Not specified'}
- Fingerprint: ${result.fingerprint}
`);
process.exit(0);
} catch (error: any) {
logger.error('Failed to fetch GitHub key', { error: error.message });
console.error(`Error fetching GitHub key: ${error.message}`);
Expand Down Expand Up @@ -983,6 +986,7 @@ program
.option('--pgp', 'Use PGP encryption instead of hybrid RSA/AES')
.option('--pgp-key-file <path>', 'Use a specific PGP public key file for encryption')
.option('--pgp-armor', 'Output ASCII-armored PGP instead of binary format')
.option('--refresh-github-keys', 'Force refresh of cached GitHub keys when encrypting')
.addHelpText('after', `
Examples:
$ echo "Secret message" | dedpaste send --encrypt # Encrypt for yourself (RSA/AES)
Expand Down Expand Up @@ -1154,7 +1158,12 @@ Encryption:
content = typeof pgpResult === 'string' ? Buffer.from(pgpResult) : pgpResult;
} else {
// Use the standard encryption flow with PGP option
const encryptResult = await encryptContent(content.toString('utf8'), recipientName, usePgp);
const encryptResult = await encryptContent(
content.toString('utf8'),
recipientName,
usePgp,
options.refreshGithubKeys
);
content = typeof encryptResult === 'string' ? Buffer.from(encryptResult) : encryptResult;
}

Expand Down
4 changes: 3 additions & 1 deletion cli/keyManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@ interface GitHubKeyAddInfo {
fingerprint: string;
email?: string;
created?: Date;
lastFetched?: string;
}

/**
Expand Down Expand Up @@ -388,7 +389,8 @@ export async function addGitHubKey(
email: keyInfo.email,
created: keyInfo.created,
addedDate: new Date().toISOString(),
lastUsed: new Date().toISOString()
lastUsed: new Date().toISOString(),
lastFetched: keyInfo.lastFetched || new Date().toISOString()
};

await saveKeyDatabase(db);
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export interface KeyInfo {
created?: string | Date;
lastUsed?: string | Date | null;
addedDate?: string;
lastFetched?: string;
id?: string;
source?: "self" | "friend" | "pgp" | "keybase" | "github" | "gpg";
expires?: string | Date;
Expand Down