@xec-sh/core
Version:
Universal shell execution engine
201 lines • 7.25 kB
JavaScript
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