/** * Keychain Manager for DashCaddy * Provides secure credential storage using OS-native keychains * Falls back to encrypted file storage if keychain is unavailable */ const { execSync } = 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 async storeWindows(account, value) { const escapedValue = value.replace(/"/g, '""'); const script = ` $password = ConvertTo-SecureString -String "${escapedValue}" -AsPlainText -Force $credential = New-Object System.Management.Automation.PSCredential("${account}", $password) cmdkey /generic:"${SERVICE_NAME}:${account}" /user:"${account}" /pass:"${escapedValue}" `; execSync(`powershell -Command "${script.replace(/\n/g, ' ')}"`, { stdio: 'ignore' }); return true; } async retrieveWindows(account) { try { const script = ` $cred = cmdkey /list:"${SERVICE_NAME}:${account}" if ($cred -match "Password: (.+)") { $matches[1] } `; const result = execSync(`powershell -Command "${script.replace(/\n/g, ' ')}"`, { encoding: 'utf8' }); return result.trim() || null; } catch { return null; } } async deleteWindows(account) { execSync(`cmdkey /delete:"${SERVICE_NAME}:${account}"`, { stdio: 'ignore' }); return true; } // macOS Keychain implementation async storeMacOS(account, value) { // Delete existing entry first try { execSync(`security delete-generic-password -s "${SERVICE_NAME}" -a "${account}"`, { stdio: 'ignore' }); } catch { // Ignore if doesn't exist } // Add new entry execSync(`security add-generic-password -s "${SERVICE_NAME}" -a "${account}" -w "${value}"`, { stdio: 'ignore' }); return true; } async retrieveMacOS(account) { try { const result = execSync(`security find-generic-password -s "${SERVICE_NAME}" -a "${account}" -w`, { encoding: 'utf8' }); return result.trim() || null; } catch { return null; } } async deleteMacOS(account) { execSync(`security delete-generic-password -s "${SERVICE_NAME}" -a "${account}"`, { stdio: 'ignore' }); return true; } // Linux Secret Service implementation async storeLinux(account, value) { try { // Try secret-tool first (libsecret) execSync(`secret-tool store --label="${SERVICE_NAME}:${account}" service "${SERVICE_NAME}" account "${account}"`, { input: value, stdio: ['pipe', 'ignore', 'ignore'] }); return true; } catch { // Fallback to gnome-keyring if available try { const script = ` echo "${value}" | gnome-keyring-daemon --unlock echo "${value}" | gnome-keyring --set-password "${SERVICE_NAME}:${account}" `; execSync(script, { stdio: 'ignore' }); return true; } catch { return false; } } } async retrieveLinux(account) { try { // Try secret-tool first const result = execSync(`secret-tool lookup service "${SERVICE_NAME}" account "${account}"`, { encoding: 'utf8' }); return result.trim() || null; } catch { return null; } } async deleteLinux(account) { try { execSync(`secret-tool clear service "${SERVICE_NAME}" account "${account}"`, { stdio: 'ignore' }); return true; } catch { return false; } } } module.exports = new KeychainManager();