UNPKG

ssh-bridge-ai

Version:

AI-Powered SSH Tool with Bulletproof Connections & Enterprise Sandbox Security + Cursor-like Confirmation - Enable AI assistants to securely SSH into your servers with persistent sessions, keepalive, automatic recovery, sandbox command testing, and user c

326 lines (273 loc) 9.54 kB
const fs = require('fs-extra'); const path = require('path'); const os = require('os'); const crypto = require('crypto'); const logger = require('./utils/logger'); // Simple encryption for local config storage class ConfigEncryption { constructor() { // Use machine-specific key for encryption this.key = this.generateMachineKey(); } generateMachineKey() { // SECURITY: Use cryptographically secure random key instead of predictable machine info return crypto.randomBytes(32); } encrypt(text) { const iv = crypto.randomBytes(16); const cipher = crypto.createCipheriv('aes-256-cbc', this.key, iv); let encrypted = cipher.update(text, 'utf8', 'hex'); encrypted += cipher.final('hex'); return iv.toString('hex') + ':' + encrypted; } decrypt(encryptedText) { try { const parts = encryptedText.split(':'); const iv = Buffer.from(parts.shift(), 'hex'); const encrypted = parts.join(':'); const decipher = crypto.createDecipheriv('aes-256-cbc', this.key, iv); let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } catch (error) { // If decryption fails, return null to indicate corrupted/invalid data return null; } } } class Config { constructor() { this.configDir = path.join(process.env.HOME || os.homedir(), '.sshbridge'); this.configFile = path.join(this.configDir, 'config.json'); this.encryption = new ConfigEncryption(); // SECURITY: Logging configuration - now handled by Logger utility this.logLevel = process.env.SSHBRIDGE_LOG_LEVEL || 'info'; this.ensureConfigDir(); } // SECURITY: Secure logging method using centralized Logger utility log(level, message, data = null) { logger.log(level, message, data); } // SECURITY: Log sensitive information using Logger utility logSensitive(message, data) { logger.logSensitive(message, data); } // SECURITY: Sanitization now handled by centralized Logger utility ensureConfigDir() { if (!fs.existsSync(this.configDir)) { fs.mkdirpSync(this.configDir, { mode: 0o700 }); // SECURITY: Restrictive permissions } // SECURITY: Ensure existing directory has correct permissions try { const stats = fs.statSync(this.configDir); const mode = stats.mode & 0o777; if (mode !== 0o700) { fs.chmodSync(this.configDir, 0o700); this.log('warn', `Fixed config directory permissions from ${mode.toString(8)} to 700`); } } catch (error) { this.log('error', `Failed to set config directory permissions: ${error.message}`); } } // SECURITY: Secure file writing with proper permissions async writeConfigFile(data, filename = null) { const targetFile = filename || this.configFile; try { // SECURITY: Ensure directory exists before writing await this.ensureConfigDir(); // SECURITY: Create temporary file first const tempFile = `${targetFile}.tmp.${Date.now()}`; // Write to temporary file await fs.writeFile(tempFile, data, { mode: 0o600 }); // SECURITY: Restrictive permissions // SECURITY: Atomic move to prevent corruption (use renameSync for reliability) try { fs.renameSync(tempFile, targetFile); } catch (renameError) { // If rename fails, try to copy and then delete try { await fs.copyFile(tempFile, targetFile); await fs.remove(tempFile); } catch (copyError) { // Clean up temp file and throw error try { await fs.remove(tempFile); } catch {} throw new Error(`Failed to write config file: ${copyError.message}`); } } // SECURITY: Ensure final file has correct permissions (use sync method for reliability) try { fs.chmodSync(targetFile, 0o600); } catch (chmodError) { this.log('warn', `Could not set file permissions: ${chmodError.message}`); } // Debug logging removed for cleaner user experience } catch (error) { this.log('error', `Failed to write config file securely: ${error.message}`); throw error; } } // SECURITY: Secure file deletion with overwriting async secureDeleteFile(filePath) { try { if (!(await fs.pathExists(filePath))) { return; // File doesn't exist } // SECURITY: Overwrite file with random data before deletion const stats = await fs.stat(filePath); if (stats.isFile()) { const randomData = crypto.randomBytes(stats.size || 1024); await fs.writeFile(filePath, randomData); // SECURITY: Overwrite again with zeros await fs.writeFile(filePath, Buffer.alloc(stats.size || 1024)); } // SECURITY: Delete the file await fs.remove(filePath); // Debug logging removed for cleaner user experience } catch (error) { this.log('error', `Failed to securely delete file: ${error.message}`); throw error; } } getConfig() { try { if (fs.existsSync(this.configFile)) { const encryptedContent = fs.readFileSync(this.configFile, 'utf8'); // Check if file is already encrypted (contains ':' separator) if (encryptedContent.includes(':') && !encryptedContent.startsWith('{')) { const decrypted = this.encryption.decrypt(encryptedContent); if (decrypted) { return JSON.parse(decrypted); } else { // Corrupted encrypted file, start fresh return {}; } } else { // Legacy unencrypted file, migrate it try { const config = JSON.parse(encryptedContent); // Re-save as encrypted this.setConfig(config); return config; } catch (parseError) { return {}; } } } return {}; } catch (error) { return {}; } } async setConfig(config) { try { const configJson = JSON.stringify(config, null, 2); const encrypted = this.encryption.encrypt(configJson); // SECURITY: Use secure file writing await this.writeConfigFile(encrypted); } catch (error) { this.log('error', `Failed to save encrypted config: ${error.message}`); // SECURITY: No fallback to unencrypted storage for security // SIMPLE ERROR: Clear message for users, detailed for developers const simpleError = 'Configuration save failed. Check folder permissions or run with elevated privileges.'; const detailedError = `Failed to save configuration securely. Please check permissions and try again. Technical details: ${error.message}`; // Log the detailed error for debugging this.log('error', detailedError); // Throw simple error for user throw new Error(simpleError); } } getApiKey() { const config = this.getConfig(); return config.apiKey; } async setApiKey(apiKey) { const config = this.getConfig(); config.apiKey = apiKey; await this.setConfig(config); } getEmail() { const config = this.getConfig(); return config.email; } setEmail(email) { const config = this.getConfig(); config.email = email; this.setConfig(config); } getDefaultSSHKey() { const config = this.getConfig(); return config.defaultSSHKey || path.join(os.homedir(), '.ssh', 'id_rsa'); } setDefaultSSHKey(keyPath) { const config = this.getConfig(); config.defaultSSHKey = keyPath; this.setConfig(config); } clear() { if (fs.existsSync(this.configFile)) { fs.removeSync(this.configFile); } } // Auto-update management setLastUpdateCheck(timestamp) { const config = this.getConfig(); config.lastUpdateCheck = timestamp; this.setConfig(config); } getLastUpdateCheck() { const config = this.getConfig(); return config.lastUpdateCheck || null; } setUpdateNotification(updateInfo) { const config = this.getConfig(); config.updateNotification = updateInfo; this.setConfig(config); } getUpdateNotification() { const config = this.getConfig(); return config.updateNotification || null; } clearUpdateNotification() { const config = this.getConfig(); delete config.updateNotification; this.setConfig(config); } setBackupInfo(backupInfo) { const config = this.getConfig(); config.backupInfo = backupInfo; this.setConfig(config); } getBackupInfo() { const config = this.getConfig(); return config.backupInfo || null; } clearBackupInfo() { const config = this.getConfig(); delete config.backupInfo; this.setConfig(config); } setLastNotificationShown(value) { const config = this.getConfig(); config.lastNotificationShown = value; this.setConfig(config); } getLastNotificationShown() { const config = this.getConfig(); return config.lastNotificationShown; } // User experience mode preferences setPreferredMode(mode) { const config = this.getConfig(); config.preferredMode = mode; this.setConfig(config); } getPreferredMode() { const config = this.getConfig(); return config.preferredMode || null; } clearPreferredMode() { const config = this.getConfig(); delete config.preferredMode; this.setConfig(config); } } module.exports = { Config };