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

581 lines (489 loc) 17.4 kB
const fs = require('fs').promises; const fsSync = require('fs'); const path = require('path'); const os = require('os'); const crypto = require('crypto'); const { promisify } = require('util'); const { spawn } = require('child_process'); /** * Secure Credential Vault * * Implements encrypted storage for SSH keys, host info, and secrets using: * - Argon2id for key derivation (configurable rounds, salt, memory) * - XChaCha20-Poly1305 for encryption (via libsodium) * - Secure memory management with zeroing * - Rate limiting for unlock attempts */ class SecureVault { constructor(options = {}) { this.vaultPath = options.vaultPath || path.join(process.env.HOME || os.homedir(), '.sshbridge', 'vault.json.enc'); this.vaultDir = path.dirname(this.vaultPath); this.maxUnlockAttempts = options.maxUnlockAttempts || 5; this.lockoutDuration = options.lockoutDuration || 15 * 60 * 1000; // 15 minutes this.argon2Config = { timeCost: options.argon2TimeCost || 3, memoryCost: options.argon2MemoryCost || 65536, // 64MB parallelism: options.argon2Parallelism || 1, hashLength: options.argon2HashLength || 32 }; // Secure memory tracking this.secureBuffers = []; this.unlockAttempts = 0; this.lastFailedAttempt = 0; this.isUnlocked = false; this.masterKey = null; // Ensure vault directory exists with correct permissions this.ensureVaultDirectory(); } /** * Ensure vault directory exists with secure permissions */ async ensureVaultDirectory() { try { if (!fsSync.existsSync(this.vaultDir)) { await fs.mkdir(this.vaultDir, { recursive: true, mode: 0o700 }); } else { // Verify existing directory permissions const stats = await fs.stat(this.vaultDir); if ((stats.mode & 0o777) !== 0o700) { await fs.chmod(this.vaultDir, 0o700); } } } catch (error) { throw new Error(`Failed to create secure vault directory: ${error.message}`); } } /** * Generate a cryptographically secure salt */ generateSalt(length = 32) { return crypto.randomBytes(length); } /** * Derive encryption key from passphrase using Argon2id */ async deriveKey(passphrase, salt) { try { // Use system argon2 if available, fallback to Node.js crypto if (await this.hasArgon2()) { return await this.deriveKeyWithArgon2(passphrase, salt); } else { return await this.deriveKeyWithNodeCrypto(passphrase, salt); } } catch (error) { throw new Error(`Key derivation failed: ${error.message}`); } } /** * Check if system argon2 is available */ async hasArgon2() { try { await promisify(spawn)('which', ['argon2']); return true; } catch { return false; } } /** * Derive key using system argon2 command */ async deriveKeyWithArgon2(passphrase, salt) { return new Promise((resolve, reject) => { const argon2 = spawn('argon2', [ passphrase, salt.toString('base64'), '-t', this.argon2Config.timeCost.toString(), '-m', this.argon2Config.memoryCost.toString(), '-p', this.argon2Config.parallelism.toString(), '-l', this.argon2Config.hashLength.toString(), '-id' // Use Argon2id variant ]); let output = ''; let error = ''; argon2.stdout.on('data', (data) => { output += data.toString(); }); argon2.stderr.on('data', (data) => { error += data.toString(); }); argon2.on('close', (code) => { if (code === 0) { // Extract hash from argon2 output (format: $argon2id$v=19$m=65536,t=3,p=1$...) const match = output.match(/\$argon2id\$[^$]*\$[^$]*\$([A-Za-z0-9+/]+)/); if (match) { resolve(Buffer.from(match[1], 'base64')); } else { reject(new Error('Failed to parse argon2 output')); } } else { reject(new Error(`Argon2 failed with code ${code}: ${error}`)); } }); }); } /** * Fallback key derivation using Node.js crypto (PBKDF2) */ async deriveKeyWithNodeCrypto(passphrase, salt) { return new Promise((resolve, reject) => { crypto.pbkdf2(passphrase, salt, 100000, this.argon2Config.hashLength, 'sha256', (err, derivedKey) => { if (err) { reject(err); } else { resolve(derivedKey); } }); }); } /** * Encrypt data using AES-256-GCM (fallback for compatibility) */ async encrypt(data, key) { try { // Generate IV for AES const iv = crypto.randomBytes(16); // Create cipher using AES-256-GCM const cipher = crypto.createCipheriv('aes-256-gcm', key.slice(0, 32), iv); // Encrypt data let encrypted = cipher.update(data, 'utf8', 'hex'); encrypted += cipher.final('hex'); // Get auth tag const authTag = cipher.getAuthTag(); // Return encrypted data with IV and auth tag return { encrypted: encrypted, nonce: iv.toString('hex'), authTag: authTag.toString('hex') }; } catch (error) { throw new Error(`Encryption failed: ${error.message}`); } } /** * Decrypt data using AES-256-GCM (fallback for compatibility) */ async decrypt(encryptedData, key) { try { const { encrypted, nonce, authTag } = encryptedData; // Create decipher using AES-256-GCM const decipher = crypto.createDecipheriv('aes-256-gcm', key.slice(0, 32), Buffer.from(nonce, 'hex')); decipher.setAuthTag(Buffer.from(authTag, 'hex')); // Decrypt data let decrypted = decipher.update(encrypted, 'hex', 'utf8'); decrypted += decipher.final('utf8'); return decrypted; } catch (error) { throw new Error(`Decryption failed: ${error.message}`); } } /** * Unlock vault with passphrase */ async unlock(passphrase) { try { // Validate passphrase input if (!passphrase || typeof passphrase !== 'string') { throw new Error('Passphrase must be a non-empty string'); } // Enhanced passphrase validation for unlock if (passphrase.length < 8) { throw new Error('Passphrase must be at least 8 characters long'); } // Check for common weak passwords during unlock const weakPasswords = [ 'password', '123456', 'qwerty', 'admin', 'root', 'test', 'guest', 'user', 'default', 'changeme', 'password123', 'admin123', 'root123', 'test123', 'guest123', 'user123', 'default123', 'changeme123' ]; if (weakPasswords.includes(passphrase.toLowerCase())) { throw new Error('Passphrase is too weak. Choose a stronger password.'); } // Check for common patterns during unlock if (/^[a-zA-Z]+$/.test(passphrase)) { throw new Error('Passphrase must contain numbers or special characters'); } if (/^[0-9]+$/.test(passphrase)) { throw new Error('Passphrase must contain letters or special characters'); } // Check rate limiting if (this.isRateLimited()) { const remainingTime = Math.ceil((this.lastFailedAttempt + this.lockoutDuration - Date.now()) / 1000); throw new Error(`Vault is locked due to too many failed attempts. Try again in ${remainingTime} seconds.`); } // Check if vault exists if (!fsSync.existsSync(this.vaultPath)) { throw new Error('Vault does not exist. Use createVault() to initialize.'); } // Read encrypted vault const encryptedVault = await fs.readFile(this.vaultPath, 'utf8'); const vaultData = JSON.parse(encryptedVault); // Derive key from passphrase const salt = Buffer.from(vaultData.salt, 'base64'); const derivedKey = await this.deriveKey(passphrase, salt); // Decrypt vault const decryptedData = await this.decrypt(vaultData.data, derivedKey); const vault = JSON.parse(decryptedData); // Store master key securely this.masterKey = derivedKey; this.isUnlocked = true; this.unlockAttempts = 0; // Track for secure cleanup this.secureBuffers.push(derivedKey); // Store the decrypted vault data for this session this.sessionVaultData = vault; return vault; } catch (error) { this.unlockAttempts++; this.lastFailedAttempt = Date.now(); if (this.unlockAttempts >= this.maxUnlockAttempts) { console.warn(`⚠️ Vault locked due to ${this.maxUnlockAttempts} failed attempts. Try again in ${Math.ceil(this.lockoutDuration / 1000)} seconds.`); } throw error; } } /** * Create new vault with passphrase */ async createVault(passphrase, initialData = {}) { try { // Validate passphrase strength if (!passphrase || typeof passphrase !== 'string') { throw new Error('Passphrase must be a non-empty string'); } if (passphrase.length < 8) { throw new Error('Passphrase must be at least 8 characters long'); } // Check for common weak passwords const weakPasswords = [ 'password', '123456', 'qwerty', 'admin', 'root', 'test', 'guest', 'user', 'default', 'changeme', 'password123', 'admin123', 'root123', 'test123', 'guest123', 'user123', 'default123', 'changeme123' ]; if (weakPasswords.includes(passphrase.toLowerCase())) { throw new Error('Passphrase is too weak. Choose a stronger password.'); } // Check for common patterns if (/^[a-zA-Z]+$/.test(passphrase)) { throw new Error('Passphrase must contain numbers or special characters'); } if (/^[0-9]+$/.test(passphrase)) { throw new Error('Passphrase must contain letters or special characters'); } // Check for excessive data size (prevent memory issues) if (JSON.stringify(initialData).length > 1000000) { // 1MB limit throw new Error('Initial data too large. Maximum allowed: 1MB'); } // Generate salt const salt = this.generateSalt(); // Derive key const derivedKey = await this.deriveKey(passphrase, salt); // Encrypt initial data const encryptedData = await this.encrypt(JSON.stringify(initialData), derivedKey); // Create vault structure const vault = { version: '1.0.0', salt: salt.toString('base64'), data: encryptedData, created: new Date().toISOString(), lastModified: new Date().toISOString() }; // Write encrypted vault await this.writeVaultSecurely(vault); // Store master key for current session this.masterKey = derivedKey; this.isUnlocked = true; this.secureBuffers.push(derivedKey); return true; } catch (error) { throw new Error(`Failed to create vault: ${error.message}`); } } /** * Store data in vault */ async store(key, value) { if (!this.isUnlocked || !this.masterKey || !this.sessionVaultData) { throw new Error('Vault must be unlocked before storing data'); } try { // Update session data this.sessionVaultData[key] = value; this.sessionVaultData.lastModified = new Date().toISOString(); // Re-encrypt and write to disk const newEncryptedData = await this.encrypt(JSON.stringify(this.sessionVaultData), this.masterKey); // Read current vault structure const encryptedVault = await fs.readFile(this.vaultPath, 'utf8'); const vaultData = JSON.parse(encryptedVault); // Update encrypted data vaultData.data = newEncryptedData; vaultData.lastModified = new Date().toISOString(); // Write updated vault await this.writeVaultSecurely(vaultData); return true; } catch (error) { throw new Error(`Failed to store data: ${error.message}`); } } /** * Retrieve data from vault */ async retrieve(key) { if (!this.isUnlocked || !this.masterKey || !this.sessionVaultData) { throw new Error('Vault must be unlocked before retrieving data'); } try { return this.sessionVaultData[key]; } catch (error) { throw new Error(`Failed to retrieve data: ${error.message}`); } } /** * List all keys in vault */ async listKeys() { if (!this.isUnlocked || !this.masterKey || !this.sessionVaultData) { throw new Error('Vault must be unlocked before listing keys'); } try { // Filter out metadata keys const metadataKeys = ['version', 'created', 'lastModified']; return Object.keys(this.sessionVaultData).filter(key => !metadataKeys.includes(key)); } catch (error) { throw new Error(`Failed to list keys: ${error.message}`); } } /** * Remove data from vault */ async remove(key) { if (!this.isUnlocked || !this.masterKey) { throw new Error('Vault must be unlocked before removing data'); } try { // Read current vault const encryptedVault = await fs.readFile(this.vaultPath, 'utf8'); const vaultData = JSON.parse(encryptedVault); // Decrypt current data const decryptedData = await this.decrypt(vaultData.data, this.masterKey); const vault = JSON.parse(decryptedData); // Remove key delete vault.data[key]; vault.lastModified = new Date().toISOString(); // Re-encrypt const newEncryptedData = await this.encrypt(JSON.stringify(vault), this.masterKey); vaultData.data = newEncryptedData; vaultData.lastModified = new Date().toISOString(); // Write updated vault await this.writeVaultSecurely(vaultData); return true; } catch (error) { throw new Error(`Failed to remove data: ${error.message}`); } } /** * Write vault securely using atomic operations */ async writeVaultSecurely(vaultData) { const tempPath = `${this.vaultPath}.tmp`; try { // Write to temporary file await fs.writeFile(tempPath, JSON.stringify(vaultData, null, 2), { mode: 0o600 }); // Atomically rename to target file await fs.rename(tempPath, this.vaultPath); // Ensure correct permissions await fs.chmod(this.vaultPath, 0o600); } catch (error) { // Clean up temp file on error try { await fs.unlink(tempPath); } catch {} throw error; } } /** * Check if vault is rate limited */ isRateLimited() { if (this.unlockAttempts >= this.maxUnlockAttempts) { const timeSinceLastAttempt = Date.now() - this.lastFailedAttempt; return timeSinceLastAttempt < this.lockoutDuration; } return false; } /** * Lock vault and clear sensitive data from memory */ lock() { this.isUnlocked = false; this.sessionVaultData = null; this.secureCleanup(); } /** * Securely clean up sensitive data from memory */ secureCleanup() { // Zero out all secure buffers this.secureBuffers.forEach(buffer => { if (Buffer.isBuffer(buffer)) { buffer.fill(0); } }); // Clear arrays this.secureBuffers = []; this.masterKey = null; } /** * Get vault status */ getStatus() { return { isUnlocked: this.isUnlocked, unlockAttempts: this.unlockAttempts, isRateLimited: this.isRateLimited(), remainingLockoutTime: this.isRateLimited() ? Math.max(0, this.lastFailedAttempt + this.lockoutDuration - Date.now()) : 0, vaultPath: this.vaultPath, vaultExists: fsSync.existsSync(this.vaultPath) }; } /** * Change vault passphrase */ async changePassphrase(oldPassphrase, newPassphrase) { if (!this.isUnlocked || !this.masterKey) { throw new Error('Vault must be unlocked before changing passphrase'); } try { // Verify old passphrase const oldKey = await this.deriveKey(oldPassphrase, Buffer.from(this.masterKey)); if (!oldKey.equals(this.masterKey)) { throw new Error('Old passphrase is incorrect'); } // Read current vault const encryptedVault = await fs.readFile(this.vaultPath, 'utf8'); const vaultData = JSON.parse(encryptedVault); // Decrypt current data const decryptedData = await this.decrypt(vaultData.data, this.masterKey); const vault = JSON.parse(decryptedData); // Generate new salt and key const newSalt = this.generateSalt(); const newKey = await this.deriveKey(newPassphrase, newSalt); // Re-encrypt with new key const newEncryptedData = await this.encrypt(JSON.stringify(vault), newKey); // Update vault vaultData.salt = newSalt.toString('base64'); vaultData.data = newEncryptedData; vaultData.lastModified = new Date().toISOString(); // Write updated vault await this.writeVaultSecurely(vaultData); // Update current session this.masterKey = newKey; this.secureBuffers.push(newKey); return true; } catch (error) { throw new Error(`Failed to change passphrase: ${error.message}`); } } } module.exports = { SecureVault };