Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
237 lines
6.8 KiB
JavaScript
237 lines
6.8 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 } = 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<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) {
|
|
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<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) {
|
|
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<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) {
|
|
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();
|