From a65d1193d919680cbcda46a3a6cfa551d1f60313 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Oct 2025 16:17:23 +0000 Subject: [PATCH] feat: Add GitHub GPG key integration with JIT fetching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements GitHub public key fetching to enable encryption for GitHub users. This feature allows users to fetch and use GPG keys from any GitHub user's public profile, with support for just-in-time fetching during encryption. New Features: - Fetch GitHub user GPG keys via dedpaste keys --github - Just-in-time key fetching when encrypting with --for github:username - Custom naming support with --github-name option - User verification with --verify flag - Full integration with enhanced interactive mode Implementation Details: - Created cli/githubUtils.ts with GitHub API integration - Updated keyManager.ts to store GitHub keys in ~/.dedpaste/github/ - Extended encryptionUtils.ts with JIT GitHub key fetching - Added GitHub type definitions and database schema - Updated CLI help text and examples - Full support in enhanced interactive mode Usage: dedpaste keys --github username dedpaste send --encrypt --for github:username --pgp The implementation follows the existing Keybase integration pattern and supports the github:username prefix for key resolution. Resolves #94 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- cli/encryptionUtils.ts | 35 +++++- cli/enhancedInteractiveMode.ts | 80 ++++++++++-- cli/githubUtils.ts | 224 +++++++++++++++++++++++++++++++++ cli/index.js | 48 ++++++- cli/index.ts | 63 ++++++++-- cli/keyManager.ts | 95 +++++++++++--- cli/unifiedKeyManager.ts | 31 ++++- package-lock.json | 4 +- src/types/index.ts | 30 ++++- 9 files changed, 561 insertions(+), 49 deletions(-) create mode 100644 cli/githubUtils.ts diff --git a/cli/encryptionUtils.ts b/cli/encryptionUtils.ts index c56c104..cb57fed 100644 --- a/cli/encryptionUtils.ts +++ b/cli/encryptionUtils.ts @@ -54,8 +54,25 @@ export async function encryptContent( let keyType: 'standard' | 'pgp' = 'standard'; if (recipientName) { - // Try to find the key in any key store (friend, PGP, or Keybase) - const friendKey = await getKey('any', 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:', ''); + console.log(`GitHub key not found locally, fetching from GitHub...`); + + try { + const { ensureGitHubKey } = await import('./githubUtils.js'); + await ensureGitHubKey(username, false); + + // 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}`); + } + } + if (!friendKey) { throw new Error(`Recipient "${recipientName}" not found in key database`); } @@ -75,7 +92,7 @@ export async function encryptContent( 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'); - + recipientInfo = { type: 'keybase', name: recipientName, @@ -83,6 +100,18 @@ export async function encryptContent( 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: 'github', + name: recipientName, + fingerprint: friendKey.fingerprint, + username: friendKey.username, + email: friendKey.email + }; } else { // Standard RSA key publicKey = await fsPromises.readFile(friendKey.public!, 'utf8'); diff --git a/cli/enhancedInteractiveMode.ts b/cli/enhancedInteractiveMode.ts index 92876d7..3f732a7 100644 --- a/cli/enhancedInteractiveMode.ts +++ b/cli/enhancedInteractiveMode.ts @@ -99,8 +99,8 @@ interface SearchOptions { interface KeySearchResult { id: string; name: string; - type: 'self' | 'friend' | 'pgp' | 'keybase' | 'gpg'; - source: 'self' | 'friend' | 'pgp' | 'keybase' | 'gpg'; + type: 'self' | 'friend' | 'pgp' | 'keybase' | 'github' | 'gpg'; + source: 'self' | 'friend' | 'pgp' | 'keybase' | 'github' | 'gpg'; fingerprint: string; email?: string; username?: string; @@ -113,7 +113,7 @@ interface KeySearchResult { } interface ImportKeyOptions { - source: 'file' | 'pgp-server' | 'keybase' | 'gpg-keyring' | 'gpg-import' | 'paste'; + source: 'file' | 'pgp-server' | 'keybase' | 'github' | 'gpg-keyring' | 'gpg-import' | 'paste'; file?: string; content?: string; name?: string; @@ -350,9 +350,10 @@ export async function searchAndListKeys(): Promise { friend: [], pgp: [], keybase: [], + github: [], gpg: [] }; - + for (const key of keys) { if (key.source === 'self') { groupedKeys.self.push(key); @@ -362,6 +363,8 @@ export async function searchAndListKeys(): Promise { groupedKeys.pgp.push(key); } else if (key.source === 'keybase') { groupedKeys.keybase.push(key); + } else if (key.source === 'github') { + groupedKeys.github.push(key); } else if (key.source === 'gpg') { groupedKeys.gpg.push(key); } @@ -428,7 +431,24 @@ export async function searchAndListKeys(): Promise { } } } - + + // Print GitHub keys + if (groupedKeys.github.length > 0) { + console.log(chalk.bold.blue('\nGitHub Keys:')); + for (const key of groupedKeys.github) { + console.log(`- ${chalk.green('Name:')} ${key.name}`); + console.log(` ${chalk.green('Type:')} ${key.type.toUpperCase()}`); + console.log(` ${chalk.green('Username:')} ${key.username || 'unknown'}`); + console.log(` ${chalk.green('Fingerprint:')} ${key.fingerprint}`); + if (key.email) { + console.log(` ${chalk.green('Email:')} ${key.email}`); + } + if (key.lastUsed) { + console.log(` ${chalk.green('Last Used:')} ${key.lastUsed.toLocaleString()}`); + } + } + } + // Print GPG keys if (groupedKeys.gpg.length > 0) { console.log(chalk.bold.blue('\nGPG Keyring Keys:')); @@ -480,6 +500,7 @@ export async function addOrImportKey(): Promise { { name: 'Import from file', value: 'file' }, { name: 'Fetch from PGP keyserver', value: 'pgp-server' }, { name: 'Fetch from Keybase', value: 'keybase' }, + { name: 'Fetch from GitHub', value: 'github' }, { name: 'Import from GPG keyring', value: 'gpg-keyring' }, { name: 'Import to GPG keyring', value: 'gpg-import' }, { name: 'Paste key content', value: 'paste' } @@ -568,7 +589,35 @@ export async function addOrImportKey(): Promise { options.verify = keybaseAnswers.verify; break; } - + + case 'github': { + const githubAnswers = await inquirer.prompt([ + { + type: 'input', + name: 'username', + message: 'Enter GitHub username:', + validate: (input: string) => input ? true : 'Username cannot be empty' + }, + { + type: 'input', + name: 'name', + message: 'Enter a name for this key (leave empty to use default):', + default: '' + }, + { + type: 'confirm', + name: 'verify', + message: 'Verify GitHub user before importing?', + default: false + } + ]); + + options.username = githubAnswers.username; + if (githubAnswers.name) options.name = githubAnswers.name; + options.verify = githubAnswers.verify; + break; + } + case 'gpg-keyring': { // Check if GPG is available const gpgInfo = await modules.unifiedKeyManager?.initialize(); @@ -796,6 +845,8 @@ export async function viewKeyDetails(preloadedKeys: KeySearchResult[] | null = n label = `${key.name} (PGP Key${key.email ? ` - ${key.email}` : ''})`; } else if (key.source === 'keybase') { label = `${key.name} (Keybase - ${key.username || 'unknown'})`; + } else if (key.source === 'github') { + label = `${key.name} (GitHub - ${key.username || 'unknown'})`; } else if (key.source === 'gpg') { const uid = key.uids && key.uids.length > 0 ? key.uids[0].uid || 'Unknown' : 'Unknown'; label = `${key.id} (GPG - ${uid})`; @@ -936,6 +987,8 @@ export async function exportKeys(): Promise { label = `${key.name} (PGP Key${key.email ? ` - ${key.email}` : ''})`; } else if (key.source === 'keybase') { label = `${key.name} (Keybase - ${key.username || 'unknown'})`; + } else if (key.source === 'github') { + label = `${key.name} (GitHub - ${key.username || 'unknown'})`; } else if (key.source === 'gpg') { const uid = key.uids && key.uids.length > 0 ? key.uids[0].uid || 'Unknown' : 'Unknown'; label = `${key.id} (GPG - ${uid})`; @@ -992,7 +1045,7 @@ export async function exportKeys(): Promise { let filename: string; if (keyInfo.source === 'self') { filename = exportPrivate ? 'dedpaste_private_key.pem' : 'dedpaste_public_key.pem'; - } else if (keyInfo.type === 'pgp' || keyInfo.type === 'keybase') { + } else if (keyInfo.type === 'pgp' || keyInfo.type === 'keybase' || keyInfo.type === 'github') { filename = `${keyInfo.name.replace(/[^a-z0-9_-]/gi, '_')}.asc`; } else if (keyInfo.type === 'gpg') { filename = `gpg_${keyInfo.id.substring(keyInfo.id.length - 8)}.asc`; @@ -1282,6 +1335,7 @@ export async function enhancedInteractiveSend(): Promise { const friendKeys = allKeys.filter(key => key.source === 'friend'); const pgpKeys = allKeys.filter(key => key.source === 'pgp'); const keybaseKeys = allKeys.filter(key => key.source === 'keybase'); + const githubKeys = allKeys.filter(key => key.source === 'github'); // Create categorized choices const choices: Array<{ name: string; value: string | null; short?: string; disabled?: boolean }> = []; @@ -1325,7 +1379,17 @@ export async function enhancedInteractiveSend(): Promise { short: key.name }))); } - + + // GitHub category + if (githubKeys.length > 0) { + choices.push({ name: chalk.bold.blue('--- GitHub ---'), value: null, disabled: true }); + choices.push(...githubKeys.map(key => ({ + name: `${key.name} (${key.username || 'unknown'})`, + value: key.id, + short: key.name + }))); + } + // GPG option at the end choices.push({ name: chalk.bold.blue('--- Other Options ---'), value: null, disabled: true }); choices.push({ name: 'Use GPG keyring directly (select a GPG key)', value: 'gpg', short: 'GPG' }); diff --git a/cli/githubUtils.ts b/cli/githubUtils.ts new file mode 100644 index 0000000..a4bdd20 --- /dev/null +++ b/cli/githubUtils.ts @@ -0,0 +1,224 @@ +// GitHub integration utilities +import fetch from 'node-fetch'; +import { promises as fsPromises } from 'fs'; +import path from 'path'; +import { importPgpKey } from './pgpUtils.js'; +import { addGitHubKey as addGitHubKeyToManager } from './keyManager.js'; +import type { GitHubUser } from '../src/types/index.js'; + +// GitHub URLs +const GITHUB_BASE_URL = 'https://github.com'; +const GITHUB_API_URL = 'https://api.github.com'; + +/** + * Fetch GitHub user's GPG public keys + * Uses the simple .gpg endpoint for direct PGP import + * @param username GitHub username + * @returns PGP armored public key data + */ +export async function fetchGitHubGpgKey(username: string): Promise { + const url = `${GITHUB_BASE_URL}/${username}.gpg`; + + try { + const response = await fetch(url); + + if (!response.ok) { + if (response.status === 404) { + throw new Error( + `GitHub user "${username}" not found or has no GPG keys.\n` + + `Users can add GPG keys at https://github.com/settings/keys` + ); + } + throw new Error(`Failed to fetch GPG key: ${response.status} ${response.statusText}`); + } + + const keyData = await response.text(); + + if (!keyData || keyData.trim().length === 0) { + throw new Error( + `User "${username}" has no GPG keys on GitHub.\n` + + `GPG keys can be added at https://github.com/settings/keys` + ); + } + + // Validate it looks like a PGP key + if (!keyData.includes('-----BEGIN PGP PUBLIC KEY BLOCK-----')) { + throw new Error( + `Invalid GPG key data received for user "${username}". ` + + `Please ensure the user has a valid GPG key on GitHub.` + ); + } + + return keyData; + } catch (error: any) { + if (error.message.includes('fetch failed') || error.code === 'ENOTFOUND') { + throw new Error( + `Network error: Unable to connect to GitHub. ` + + `Please check your internet connection.` + ); + } + throw error; + } +} + +/** + * Fetch GitHub user metadata + * @param username GitHub username + * @returns User information + */ +export async function fetchGitHubUser(username: string): Promise { + const url = `${GITHUB_API_URL}/users/${username}`; + + try { + const response = await fetch(url); + + if (!response.ok) { + if (response.status === 404) { + throw new Error(`GitHub user "${username}" not found`); + } + throw new Error(`Failed to fetch user info: ${response.status} ${response.statusText}`); + } + + const data: any = await response.json(); + + return { + username: data.login, + name: data.name || undefined, + bio: data.bio || undefined, + location: data.location || undefined, + publicRepos: data.public_repos, + followers: data.followers, + following: data.following, + createdAt: data.created_at ? new Date(data.created_at) : undefined, + updatedAt: data.updated_at ? new Date(data.updated_at) : undefined + }; + } catch (error: any) { + if (error.message.includes('fetch failed') || error.code === 'ENOTFOUND') { + throw new Error( + `Network error: Unable to connect to GitHub API. ` + + `Please check your internet connection.` + ); + } + throw error; + } +} + +/** + * Verify GitHub user exists and is active + * @param username GitHub username + * @returns true if user exists and is valid + */ +export async function verifyGitHubUser(username: string): Promise { + try { + const user = await fetchGitHubUser(username); + return !!user.username; + } catch (error) { + return false; + } +} + +/** + * Main function: Fetch and add GitHub GPG key to local storage + * @param username GitHub username + * @param customName Optional custom name for the key + * @param verify Whether to verify the user first + * @param silent Whether to suppress console output + */ +export async function addGitHubKey( + username: string, + customName?: string, + verify: boolean = false, + silent: boolean = false +): Promise<{ name: string; fingerprint: string; email?: string }> { + // Validate username + if (!username || username.trim().length === 0) { + throw new Error('GitHub username cannot be empty'); + } + + // Sanitize username (GitHub usernames are alphanumeric + hyphens) + const sanitizedUsername = username.trim(); + if (!/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(sanitizedUsername)) { + throw new Error( + `Invalid GitHub username: "${username}". ` + + `Usernames can only contain alphanumeric characters and hyphens.` + ); + } + + // Optional verification + if (verify && !silent) { + console.log(`Verifying GitHub user: ${sanitizedUsername}...`); + const isValid = await verifyGitHubUser(sanitizedUsername); + if (!isValid) { + throw new Error(`Cannot verify GitHub user: ${sanitizedUsername}`); + } + console.log(`✓ User verified`); + } + + // 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 + const keyName = customName || `github:${sanitizedUsername}`; + + // Add to key manager + await addGitHubKeyToManager(keyName, sanitizedUsername, keyInfo, gpgKeyData); + + if (!silent) { + console.log(`✓ GitHub key added: ${keyName}`); + console.log(` Fingerprint: ${keyInfo.fingerprint}`); + if (keyInfo.email) { + console.log(` Email: ${keyInfo.email}`); + } + } + + return { + name: keyName, + fingerprint: keyInfo.fingerprint, + email: keyInfo.email + }; +} + +/** + * Fetch GitHub key just-in-time (used during encryption) + * Checks if key exists in database, fetches if not + * @param username GitHub username (without github: prefix) + * @param silent Whether to suppress console output + * @returns Key name and metadata + */ +export async function ensureGitHubKey( + username: string, + silent: 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 + }; + } + + // Key doesn't exist, fetch it + if (!silent) { + console.log(`GitHub key not found locally, fetching from GitHub...`); + } + + return await addGitHubKey(username, undefined, false, silent); +} diff --git a/cli/index.js b/cli/index.js index e64dadc..16d0214 100755 --- a/cli/index.js +++ b/cli/index.js @@ -155,6 +155,9 @@ program .option('--keybase ', 'Fetch and add a Keybase user\'s PGP key') .option('--keybase-name ', 'Custom name for the Keybase user\'s key (optional)') .option('--no-verify', 'Skip verification of Keybase proofs') + // GitHub options + .option('--github ', 'Fetch and add a GitHub user\'s GPG public key') + .option('--github-name ', 'Custom name for the GitHub user\'s key (optional)') // 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)') @@ -179,12 +182,17 @@ Keybase Integration: $ dedpaste keys --keybase username # Add a Keybase user's key $ dedpaste keys --keybase username --keybase-name bob # Add with custom name $ dedpaste keys --keybase username --no-verify # Skip verification of proofs - + +GitHub Integration: + $ dedpaste keys --github username # Add a GitHub user's GPG key + $ dedpaste keys --github username --github-name bob # Add with custom name + Key Storage: - Your keys are stored in ~/.dedpaste/keys/ - Friend keys are stored in ~/.dedpaste/friends/ - PGP keys are stored in ~/.dedpaste/pgp/ - Keybase keys are stored in ~/.dedpaste/keybase/ + - GitHub keys are stored in ~/.dedpaste/github/ - Key database is at ~/.dedpaste/keydb.json `) .action(async (options) => { @@ -650,6 +658,32 @@ Key Storage: } return; } + // Fetch and add a GitHub user's key + if (options.github) { + logger.debug('Fetching GitHub key', { username: options.github }); + try { + const { addGitHubKey } = await import('./githubUtils.js'); + console.log(`Fetching GPG key for GitHub user "${options.github}"...`); + if (options.verify) { + console.log('Verifying GitHub user...'); + } + const customName = options.githubName; + const result = await addGitHubKey(options.github, customName, options.verify); + console.log(` +✓ Added GitHub key: + - Name: ${result.name} + - GitHub username: ${options.github} + - Email: ${result.email || 'Not specified'} + - Fingerprint: ${result.fingerprint} +`); + } + catch (error) { + logger.error('Failed to fetch GitHub key', { error: error.message }); + console.error(`Error fetching GitHub key: ${error.message}`); + process.exit(1); + } + return; + } // Generate a new key pair if (options.genKey) { logger.debug('Generating new key pair'); @@ -723,14 +757,20 @@ Examples: $ dedpaste send --interactive --encrypt # Interactive encrypted message $ dedpaste send --enhanced --encrypt # Enhanced interactive mode with full key support $ dedpaste send --list-friends # List available recipients - + PGP Options: $ dedpaste send --encrypt --for alice@example.com --pgp # Encrypt for PGP key (REQUIRED: specify recipient) $ dedpaste send --encrypt --for alice --pgp --pgp-key-file friend.asc # Use specific PGP key file $ dedpaste send --enhanced --encrypt --pgp # Use enhanced mode with GPG keyring support - + +Keybase Integration: + $ dedpaste send --encrypt --for keybase:username --pgp # Encrypt for Keybase user + +GitHub Integration: + $ dedpaste send --encrypt --for github:username --pgp # Encrypt for GitHub user (auto-fetches GPG key) + IMPORTANT: PGP encryption always requires specifying a recipient with --for - + Encryption: - Standard encryption uses RSA for key exchange and AES-256-GCM for content - PGP encryption (--pgp) uses OpenPGP standard compatible with GnuPG/GPG diff --git a/cli/index.ts b/cli/index.ts index dde7ccd..a87f5f2 100644 --- a/cli/index.ts +++ b/cli/index.ts @@ -297,6 +297,9 @@ program .option('--keybase ', 'Fetch and add a Keybase user\'s PGP key') .option('--keybase-name ', 'Custom name for the Keybase user\'s key (optional)') .option('--no-verify', 'Skip verification of Keybase proofs') + // GitHub options + .option('--github ', 'Fetch and add a GitHub user\'s GPG public key') + .option('--github-name ', 'Custom name for the GitHub user\'s key (optional)') // 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)') @@ -321,12 +324,17 @@ Keybase Integration: $ dedpaste keys --keybase username # Add a Keybase user's key $ dedpaste keys --keybase username --keybase-name bob # Add with custom name $ dedpaste keys --keybase username --no-verify # Skip verification of proofs - + +GitHub Integration: + $ dedpaste keys --github username # Add a GitHub user's GPG key + $ dedpaste keys --github username --github-name bob # Add with custom name + Key Storage: - Your keys are stored in ~/.dedpaste/keys/ - Friend keys are stored in ~/.dedpaste/friends/ - PGP keys are stored in ~/.dedpaste/pgp/ - Keybase keys are stored in ~/.dedpaste/keybase/ + - GitHub keys are stored in ~/.dedpaste/github/ - Key database is at ~/.dedpaste/keydb.json `) .action(async (options: KeysOptions) => { @@ -846,17 +854,17 @@ Key Storage: // Fetch and add a Keybase user's key if (options.keybase) { logger.debug('Fetching Keybase key', { username: options.keybase }); - + try { console.log(`Fetching Keybase key for user "${options.keybase}"...`); - + if (options.verify) { console.log('Verifying user proofs on Keybase...'); } - + const name = options.keybaseName || `keybase:${options.keybase}`; const result = await fetchAndAddKeybaseKey(options.keybase, name, options.verify); - + console.log(` ✓ Added Keybase key: - Name: ${result.name} @@ -872,7 +880,38 @@ Key Storage: } return; } - + + // Fetch and add a GitHub user's key + if (options.github) { + logger.debug('Fetching GitHub key', { username: options.github }); + + try { + const { addGitHubKey } = await import('./githubUtils.js'); + + console.log(`Fetching GPG key for GitHub user "${options.github}"...`); + + if (options.verify) { + console.log('Verifying GitHub user...'); + } + + const customName = options.githubName; + const result = await addGitHubKey(options.github, customName, options.verify); + + console.log(` +✓ Added GitHub key: + - Name: ${result.name} + - GitHub username: ${options.github} + - Email: ${result.email || 'Not specified'} + - Fingerprint: ${result.fingerprint} +`); + } catch (error: any) { + logger.error('Failed to fetch GitHub key', { error: error.message }); + console.error(`Error fetching GitHub key: ${error.message}`); + process.exit(1); + } + return; + } + // Generate a new key pair if (options.genKey) { logger.debug('Generating new key pair'); @@ -950,14 +989,20 @@ Examples: $ dedpaste send --interactive --encrypt # Interactive encrypted message $ dedpaste send --enhanced --encrypt # Enhanced interactive mode with full key support $ dedpaste send --list-friends # List available recipients - + PGP Options: $ dedpaste send --encrypt --for alice@example.com --pgp # Encrypt for PGP key (REQUIRED: specify recipient) $ dedpaste send --encrypt --for alice --pgp --pgp-key-file friend.asc # Use specific PGP key file $ dedpaste send --enhanced --encrypt --pgp # Use enhanced mode with GPG keyring support - + +Keybase Integration: + $ dedpaste send --encrypt --for keybase:username --pgp # Encrypt for Keybase user + +GitHub Integration: + $ dedpaste send --encrypt --for github:username --pgp # Encrypt for GitHub user (auto-fetches GPG key) + IMPORTANT: PGP encryption always requires specifying a recipient with --for - + Encryption: - Standard encryption uses RSA for key exchange and AES-256-GCM for content - PGP encryption (--pgp) uses OpenPGP standard compatible with GnuPG/GPG diff --git a/cli/keyManager.ts b/cli/keyManager.ts index 473294d..4d11dbd 100644 --- a/cli/keyManager.ts +++ b/cli/keyManager.ts @@ -12,6 +12,7 @@ export const FRIENDS_KEY_DIR = path.join(homedir(), '.dedpaste', 'friends'); export const KEY_DB_PATH = path.join(homedir(), '.dedpaste', 'keydb.json'); export const PGP_KEY_DIR = path.join(homedir(), '.dedpaste', 'pgp'); export const KEYBASE_KEY_DIR = path.join(homedir(), '.dedpaste', 'keybase'); +export const GITHUB_KEY_DIR = path.join(homedir(), '.dedpaste', 'github'); // Directory structure interface interface KeyDirectories { @@ -19,6 +20,7 @@ interface KeyDirectories { FRIENDS_KEY_DIR: string; PGP_KEY_DIR: string; KEYBASE_KEY_DIR: string; + GITHUB_KEY_DIR: string; } // Ensure directories exist @@ -27,7 +29,8 @@ export async function ensureDirectories(): Promise { await fsPromises.mkdir(FRIENDS_KEY_DIR, { recursive: true }); await fsPromises.mkdir(PGP_KEY_DIR, { recursive: true }); await fsPromises.mkdir(KEYBASE_KEY_DIR, { recursive: true }); - return { DEFAULT_KEY_DIR, FRIENDS_KEY_DIR, PGP_KEY_DIR, KEYBASE_KEY_DIR }; + await fsPromises.mkdir(GITHUB_KEY_DIR, { recursive: true }); + return { DEFAULT_KEY_DIR, FRIENDS_KEY_DIR, PGP_KEY_DIR, KEYBASE_KEY_DIR, GITHUB_KEY_DIR }; } // Initialize key database if it doesn't exist @@ -38,20 +41,21 @@ async function initKeyDatabase(): Promise { self: null, friends: {}, pgp: {}, - keybase: {} + keybase: {}, + github: {} }, default_friend: null, last_used: null }; - + await fsPromises.writeFile(KEY_DB_PATH, JSON.stringify(defaultDb, null, 2)); return defaultDb; } - + // Read existing database const dbContent = await fsPromises.readFile(KEY_DB_PATH, 'utf8'); const db = JSON.parse(dbContent) as KeyDatabase; - + // Ensure new properties exist (for upgrades) if (!db.keys.pgp) { db.keys.pgp = {}; @@ -59,7 +63,10 @@ async function initKeyDatabase(): Promise { if (!db.keys.keybase) { db.keys.keybase = {}; } - + if (!db.keys.github) { + db.keys.github = {}; + } + return db; } @@ -179,14 +186,17 @@ export async function getKey(type: string, name?: string): Promise { // Remove from database delete db.keys.keybase[name]; + await saveKeyDatabase(db); + return true; + } else if (type === 'github' && db.keys.github[name]) { + // Remove the key file + const githubPath = typeof db.keys.github[name].path === 'string' ? db.keys.github[name].path : db.keys.github[name].path.public; + await fsPromises.unlink(githubPath); + + // Remove from database + delete db.keys.github[name]; + await saveKeyDatabase(db); return true; } else if (type === 'any') { @@ -237,9 +257,11 @@ export async function removeKey(type: string, name: string): Promise { return await removeKey('pgp', name); } else if (db.keys.keybase[name]) { return await removeKey('keybase', name); + } else if (db.keys.github[name]) { + return await removeKey('github', name); } } - + return false; } @@ -307,10 +329,10 @@ interface KeybaseKeyAddInfo { export async function addKeybaseKey(name: string, keyInfo: KeybaseKeyAddInfo): Promise { const { KEYBASE_KEY_DIR } = await ensureDirectories(); const keyPath = path.join(KEYBASE_KEY_DIR, `${name}.asc`); - + // Write key to file await fsPromises.writeFile(keyPath, keyInfo.key); - + // Update key database const db = await loadKeyDatabase(); db.keys.keybase[name] = { @@ -323,7 +345,52 @@ export async function addKeybaseKey(name: string, keyInfo: KeybaseKeyAddInfo): P addedDate: new Date().toISOString(), lastUsed: new Date().toISOString() }; - + + await saveKeyDatabase(db); + return keyPath; +} + +// GitHub key information interface +interface GitHubKeyAddInfo { + fingerprint: string; + email?: string; + created?: Date; +} + +/** + * Add a GitHub key to the database + * @param name - Name for the key (e.g., "github:username") + * @param username - GitHub username + * @param keyInfo - Key information from PGP import + * @param keyData - Raw PGP key data + * @returns Path to the stored key + */ +export async function addGitHubKey( + name: string, + username: string, + keyInfo: GitHubKeyAddInfo, + keyData: string +): Promise { + const { GITHUB_KEY_DIR } = await ensureDirectories(); + const keyPath = path.join(GITHUB_KEY_DIR, `${name}.asc`); + + // Write key to file + await fsPromises.writeFile(keyPath, keyData); + + // Update key database + const db = await loadKeyDatabase(); + db.keys.github[name] = { + type: 'github', + name: name, + path: keyPath, + username: username, + fingerprint: keyInfo.fingerprint, + email: keyInfo.email, + created: keyInfo.created, + addedDate: new Date().toISOString(), + lastUsed: new Date().toISOString() + }; + await saveKeyDatabase(db); return keyPath; } \ No newline at end of file diff --git a/cli/unifiedKeyManager.ts b/cli/unifiedKeyManager.ts index 1a8520f..fb28d04 100644 --- a/cli/unifiedKeyManager.ts +++ b/cli/unifiedKeyManager.ts @@ -11,6 +11,7 @@ import chalk from 'chalk'; import * as keyManager from './keyManager.js'; import * as pgpUtils from './pgpUtils.js'; import * as keybaseUtils from './keybaseUtils.js'; +import * as githubUtils from './githubUtils.js'; import { checkGpgKeyring, findKeysMatchingCriteria } from './keyDiagnostics.js'; import { KeyDatabase, KeyInfo } from '../src/types/index.js'; @@ -19,6 +20,7 @@ export const KEY_TYPES = { RSA: 'rsa', PGP: 'pgp', KEYBASE: 'keybase', + GITHUB: 'github', GPG: 'gpg' } as const; @@ -35,7 +37,7 @@ export interface KeyDetails { fingerprint: string; created?: string | Date | null; lastUsed?: string | Date | null; - source: 'self' | 'friend' | 'pgp' | 'keybase' | 'gpg'; + source: 'self' | 'friend' | 'pgp' | 'keybase' | 'github' | 'gpg'; path?: string | { public: string; private: string }; email?: string; username?: string; @@ -643,11 +645,11 @@ export async function importKey(options: ImportOptions): Promise { if (!username) { throw new Error('Username is required for Keybase import'); } - + const customName = name || `keybase:${username}`; - + const result = await keybaseUtils.addKeybaseKey(username, customName, verify); - + return { success: true, type: KEY_TYPES.KEYBASE, @@ -658,7 +660,26 @@ export async function importKey(options: ImportOptions): Promise { path: result.path }; } - + + case 'github': { + if (!username) { + throw new Error('Username is required for GitHub import'); + } + + const customName = name || `github:${username}`; + + const result = await githubUtils.addGitHubKey(username, customName, verify); + + return { + success: true, + type: KEY_TYPES.GITHUB, + name: result.name, + username: username, + email: result.email, + fingerprint: result.fingerprint + }; + } + case 'gpg-import': { if (!keyContent) { throw new Error('Key content is required for GPG import'); diff --git a/package-lock.json b/package-lock.json index d2712c1..68cde03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "dedpaste", - "version": "1.17.0", + "version": "1.19.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "dedpaste", - "version": "1.17.0", + "version": "1.19.1", "hasInstallScript": true, "license": "ISC", "dependencies": { diff --git a/src/types/index.ts b/src/types/index.ts index e121ed9..b3c66b6 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -2,7 +2,7 @@ // Key types export interface KeyInfo { - type: "self" | "friend" | "pgp" | "keybase" | "gpg"; + type: "self" | "friend" | "pgp" | "keybase" | "github" | "gpg"; name: string; path: string | { public: string; private: string }; public?: string; @@ -14,7 +14,7 @@ export interface KeyInfo { lastUsed?: string | Date | null; addedDate?: string; id?: string; - source?: "self" | "friend" | "pgp" | "keybase" | "gpg"; + source?: "self" | "friend" | "pgp" | "keybase" | "github" | "gpg"; expires?: string | Date; trust?: string; uids?: Array<{ uid?: string; trust?: string }>; @@ -26,6 +26,7 @@ export interface KeyDatabase { friends: Record; pgp: Record; keybase: Record; + github: Record; }; default_friend: string | null; last_used: string | null; @@ -39,7 +40,7 @@ export interface EncryptionResult { } export interface RecipientInfo { - type: "self" | "friend" | "pgp" | "keybase"; + type: "self" | "friend" | "pgp" | "keybase" | "github"; name: string; fingerprint: string; username?: string; @@ -117,6 +118,26 @@ export interface KeybaseProof { humanUrl?: string; } +// GitHub types +export interface GitHubUser { + username: string; + name?: string; + bio?: string; + location?: string; + publicRepos?: number; + followers?: number; + following?: number; + createdAt?: Date; + updatedAt?: Date; +} + +export interface GitHubKeyInfo { + id: number; + key: string; + title?: string; + createdAt?: Date; +} + // Interactive mode types export interface InteractiveChoice { name: string; @@ -300,7 +321,7 @@ export interface SearchOptions { } export interface KeySearchResult extends KeyInfo { - source: "self" | "friend" | "pgp" | "keybase" | "gpg"; + source: "self" | "friend" | "pgp" | "keybase" | "github" | "gpg"; } export interface ImportKeyOptions { @@ -308,6 +329,7 @@ export interface ImportKeyOptions { | "file" | "pgp-server" | "keybase" + | "github" | "gpg-keyring" | "gpg-import" | "paste";