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";