/** * Keychain Manager for DashCaddy * Provides secure credential storage using OS-native keychains * Falls back to encrypted file storage if keychain is unavailable */ const { execSync, execFileSync } = require('child_process'); const os = require('os'); const crypto = require('crypto'); const SERVICE_NAME = 'DashCaddy'; const ACCOUNT_PREFIX = 'dashcaddy'; class KeychainManager { constructor() { this.platform = os.platform(); this.available = this.checkAvailability(); } /** * Check if OS keychain is available * @returns {boolean} */ checkAvailability() { try { if (this.platform === 'win32') { // Check if PowerShell is available execSync('powershell -Command "Get-Command Get-Credential"', { stdio: 'ignore' }); return true; } else if (this.platform === 'darwin') { // Check if security command is available execSync('which security', { stdio: 'ignore' }); return true; } else if (this.platform === 'linux') { // Check if secret-tool (libsecret) is available try { execSync('which secret-tool', { stdio: 'ignore' }); return true; } catch { // Try gnome-keyring execSync('which gnome-keyring-daemon', { stdio: 'ignore' }); return true; } } return false; } catch { console.warn('[Keychain] OS keychain not available, will use encrypted file storage'); return false; } } /** * Store a credential in the OS keychain * @param {string} key - Credential identifier * @param {string} value - Credential value * @returns {Promise} Success status */ async store(key, value) { if (!this.available) { return false; } const account = `${ACCOUNT_PREFIX}.${key}`; try { if (this.platform === 'win32') { return await this.storeWindows(account, value); } else if (this.platform === 'darwin') { return await this.storeMacOS(account, value); } else if (this.platform === 'linux') { return await this.storeLinux(account, value); } return false; } catch (error) { console.error(`[Keychain] Failed to store ${key}:`, error.message); return false; } } /** * Retrieve a credential from the OS keychain * @param {string} key - Credential identifier * @returns {Promise} Credential value or null */ async retrieve(key) { if (!this.available) { return null; } const account = `${ACCOUNT_PREFIX}.${key}`; try { if (this.platform === 'win32') { return await this.retrieveWindows(account); } else if (this.platform === 'darwin') { return await this.retrieveMacOS(account); } else if (this.platform === 'linux') { return await this.retrieveLinux(account); } return null; } catch (error) { console.error(`[Keychain] Failed to retrieve ${key}:`, error.message); return null; } } /** * Delete a credential from the OS keychain * @param {string} key - Credential identifier * @returns {Promise} Success status */ async delete(key) { if (!this.available) { return false; } const account = `${ACCOUNT_PREFIX}.${key}`; try { if (this.platform === 'win32') { return await this.deleteWindows(account); } else if (this.platform === 'darwin') { return await this.deleteMacOS(account); } else if (this.platform === 'linux') { return await this.deleteLinux(account); } return false; } catch (error) { console.error(`[Keychain] Failed to delete ${key}:`, error.message); return false; } } // Windows Credential Manager implementation (uses execFileSync to prevent injection) async storeWindows(account, value) { execFileSync('cmdkey', [`/generic:${SERVICE_NAME}:${account}`, `/user:${account}`, `/pass:${value}`], { stdio: 'ignore' }); return true; } async retrieveWindows(account) { try { const result = execFileSync('cmdkey', [`/list:${SERVICE_NAME}:${account}`], { encoding: 'utf8' }); const match = result.match(/Password:\s*(.+)/); return match ? match[1].trim() : null; } catch { return null; } } async deleteWindows(account) { execFileSync('cmdkey', [`/delete:${SERVICE_NAME}:${account}`], { stdio: 'ignore' }); return true; } // macOS Keychain implementation (uses execFileSync to prevent injection) async storeMacOS(account, value) { try { execFileSync('security', ['delete-generic-password', '-s', SERVICE_NAME, '-a', account], { stdio: 'ignore' }); } catch { // Ignore if doesn't exist } execFileSync('security', ['add-generic-password', '-s', SERVICE_NAME, '-a', account, '-w', value], { stdio: 'ignore' }); return true; } async retrieveMacOS(account) { try { const result = execFileSync('security', ['find-generic-password', '-s', SERVICE_NAME, '-a', account, '-w'], { encoding: 'utf8' }); return result.trim() || null; } catch { return null; } } async deleteMacOS(account) { execFileSync('security', ['delete-generic-password', '-s', SERVICE_NAME, '-a', account], { stdio: 'ignore' }); return true; } // Linux Secret Service implementation (uses execFileSync + stdin to prevent injection) async storeLinux(account, value) { try { execFileSync('secret-tool', ['store', `--label=${SERVICE_NAME}:${account}`, 'service', SERVICE_NAME, 'account', account], { input: value, stdio: ['pipe', 'ignore', 'ignore'] }); return true; } catch { return false; } } async retrieveLinux(account) { try { const result = execFileSync('secret-tool', ['lookup', 'service', SERVICE_NAME, 'account', account], { encoding: 'utf8' }); return result.trim() || null; } catch { return null; } } async deleteLinux(account) { try { execFileSync('secret-tool', ['clear', 'service', SERVICE_NAME, 'account', account], { stdio: 'ignore' }); return true; } catch { return false; } } } module.exports = new KeychainManager();