UNPKG

@xec-sh/core

Version:

Universal shell execution engine

201 lines 7.25 kB
import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { chmod, unlink, writeFile } from 'node:fs/promises'; import { scryptSync, randomBytes, createCipheriv, createDecipheriv } from 'node:crypto'; export class SecurePasswordHandler { constructor() { this.tempFiles = new Set(); this.encryptedPasswords = new Map(); this.isDisposed = false; this.encryptionKey = randomBytes(32); } encryptPassword(password) { const salt = randomBytes(32); const iv = randomBytes(16); const key = scryptSync(this.encryptionKey, salt, 32); const cipher = createCipheriv('aes-256-gcm', key, iv); const encrypted = Buffer.concat([ cipher.update(password, 'utf8'), cipher.final(), cipher.getAuthTag() ]); return { encrypted, salt, iv }; } decryptPassword(encrypted, salt, iv) { const key = scryptSync(this.encryptionKey, salt, 32); const authTag = encrypted.subarray(-16); const data = encrypted.subarray(0, -16); const decipher = createDecipheriv('aes-256-gcm', key, iv); decipher.setAuthTag(authTag); return decipher.update(data) + decipher.final('utf8'); } storePassword(id, password) { if (this.isDisposed) { throw new Error('SecurePasswordHandler has been disposed'); } const { encrypted, salt, iv } = this.encryptPassword(password); const combined = Buffer.concat([salt, iv, encrypted]); this.encryptedPasswords.set(id, combined); } retrievePassword(id) { if (this.isDisposed) { throw new Error('SecurePasswordHandler has been disposed'); } const combined = this.encryptedPasswords.get(id); if (!combined) return null; const salt = combined.subarray(0, 32); const iv = combined.subarray(32, 48); const encrypted = combined.subarray(48); return this.decryptPassword(encrypted, salt, iv); } async createAskPassScript(password) { if (this.isDisposed) { throw new Error('SecurePasswordHandler has been disposed'); } const scriptId = randomBytes(8).toString('hex'); const scriptPath = join(tmpdir(), `askpass-${scriptId}.sh`); this.storePassword(scriptId, password); const escapedPassword = password.replace(/'/g, "'\\''"); const scriptContent = `#!/bin/sh # Temporary askpass script - auto-generated # This file will be deleted after use echo '${escapedPassword}' `; try { await writeFile(scriptPath, scriptContent); await chmod(scriptPath, 0o700); this.tempFiles.add(scriptPath); return scriptPath; } catch (error) { this.encryptedPasswords.delete(scriptId); throw new Error(`Failed to create askpass script: ${error}`); } } async cleanup() { const cleanupPromises = Array.from(this.tempFiles).map(async (file) => { try { await unlink(file); this.tempFiles.delete(file); } catch { } }); await Promise.all(cleanupPromises); this.securelyDisposePasswords(); } securelyDisposePasswords() { for (const buffer of this.encryptedPasswords.values()) { buffer.fill(0); } this.encryptedPasswords.clear(); if (this.encryptionKey) { this.encryptionKey.fill(0); } } async dispose() { if (this.isDisposed) return; await this.cleanup(); this.isDisposed = true; } createSecureEnv(askpassPath, baseEnv) { if (this.isDisposed) { throw new Error('SecurePasswordHandler has been disposed'); } const match = askpassPath.match(/askpass-([a-f0-9]+)\.sh$/); if (!match) { throw new Error('Invalid askpass script path'); } const scriptId = match[1]; if (!scriptId) { throw new Error('Invalid askpass script path: missing script ID'); } const password = this.retrievePassword(scriptId); if (!password) { throw new Error('Password not found for askpass script'); } return { ...baseEnv, SUDO_ASKPASS: askpassPath, [`SUDO_PASS_${scriptId}`]: password, SUDO_LECTURE: 'no', SUDO_ASKPASS_REQUIRE: '1' }; } static maskPassword(command, password) { if (!password) return command; const masked = command.replace(new RegExp(password.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '***MASKED***'); return masked; } static async checkSecureMethodsAvailable() { return { askpass: true, stdin: true, keyring: false }; } static generatePassword(length = 32) { const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;:,.<>?'; const bytes = randomBytes(length * 2); let password = ''; const requirements = [ 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', 'abcdefghijklmnopqrstuvwxyz', '0123456789', '!@#$%^&*()_+-=[]{}|;:,.<>?' ]; for (const req of requirements) { const byte = bytes[password.length]; if (byte !== undefined) { const idx = byte % req.length; password += req[idx]; } } for (let i = password.length; i < length; i++) { const byte = bytes[i]; if (byte !== undefined) { password += chars[byte % chars.length]; } } const shuffled = password.split(''); for (let i = shuffled.length - 1; i > 0; i--) { const byte = bytes[i + length]; if (byte !== undefined) { const j = byte % (i + 1); const temp = shuffled[i]; const swapElement = shuffled[j]; if (temp !== undefined && swapElement !== undefined) { shuffled[i] = swapElement; shuffled[j] = temp; } } } return shuffled.join(''); } static validatePassword(password) { const issues = []; if (password.length < 8) { issues.push('Password should be at least 8 characters long'); } if (!/[A-Z]/.test(password)) { issues.push('Password should contain at least one uppercase letter'); } if (!/[a-z]/.test(password)) { issues.push('Password should contain at least one lowercase letter'); } if (!/[0-9]/.test(password)) { issues.push('Password should contain at least one number'); } if (!/[^A-Za-z0-9]/.test(password)) { issues.push('Password should contain at least one special character'); } return { isValid: issues.length === 0, issues }; } } //# sourceMappingURL=secure-password.js.map