Files
dashcaddy/dashcaddy-api/src/security/keychain-manager.js
T
Hermes 7b04bc1d3c [grade=A] P1-8: replace 66 console.* calls across 6 remaining files with structured logger
Files changed:
- src/security/crypto-utils.js: 16 calls → log tagged 'crypto'
- src/security/docker-security.js: 15 calls → log tagged 'security'
- src/managers/port-lock-manager.js: 16 calls → log tagged 'portlock'
- src/docker/self-updater.js: 10 calls → log tagged 'updater'
- src/security/event-workers.js: 5 calls → log tagged 'events'
- src/security/keychain-manager.js: 4 calls → log tagged 'keychain'

Fixed 2 bugs found during sweep:
- self-updater.js:161 — arrow expression body had trailing semicolon (SyntaxError)
- port-lock-manager.js:137 — log.error referenced 'port' var out of scope (ReferenceError)

1539/1539 Jest tests pass. All ESLint warnings pre-existing (0 new).
2026-08-10 20:23:58 -07:00

214 lines
6.1 KiB
JavaScript

/**
* 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 { log } = require('../utils/logging');
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 {
log.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<boolean>} 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) {
log.error('keychain', error, { key, operation: 'store' });
return false;
}
}
/**
* Retrieve a credential from the OS keychain
* @param {string} key - Credential identifier
* @returns {Promise<string|null>} 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) {
log.error('keychain', error, { key, operation: 'retrieve' });
return null;
}
}
/**
* Delete a credential from the OS keychain
* @param {string} key - Credential identifier
* @returns {Promise<boolean>} 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) {
log.error('keychain', error, { key, operation: 'delete' });
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();