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
JavaScript
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 };