hidenv
Version:
Beautiful CLI tool to encrypt and decrypt .env files with AES-256-GCM
151 lines (118 loc) • 4.38 kB
JavaScript
import fs from 'fs';
import crypto from 'crypto';
function encryptValue(value, password, salt) {
const key = crypto.scryptSync(password, salt, 32, {
N: 16384, // Factor de costo (aumenta la dificultad)
r: 8, // Tamaño de bloque
p: 1 // Paralelización
});
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const encrypted = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
const authTag = cipher.getAuthTag();
const payload = Buffer.concat([iv, authTag, encrypted]);
return payload.toString('base64');
}
function decryptValue(encryptedValue, password, salt) {
const payload = Buffer.from(encryptedValue, 'base64');
const iv = payload.subarray(0, 16);
const authTag = payload.subarray(16, 32);
const encrypted = payload.subarray(32);
const key = crypto.scryptSync(password, salt, 32, {
N: 16384,
r: 8,
p: 1
});
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(authTag);
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
return decrypted.toString('utf8');
}
function parseEnvFile(content) {
const lines = content.split('\n');
const envVars = [];
for (const line of lines) {
const trimmedLine = line.trim();
if (trimmedLine === '' || trimmedLine.startsWith('#')) {
envVars.push({ type: 'comment', content: line });
continue;
}
const equalIndex = trimmedLine.indexOf('=');
if (equalIndex === -1) {
envVars.push({ type: 'comment', content: line });
continue;
}
const key = trimmedLine.substring(0, equalIndex).trim();
const value = trimmedLine.substring(equalIndex + 1).trim();
// remove quotes if present
let cleanValue = value;
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
cleanValue = value.slice(1, -1);
}
envVars.push({ type: 'env', key, value: cleanValue, originalQuotes: value !== cleanValue });
}
return envVars;
}
function buildEnvContent(envVars) {
return envVars.map(item => {
if (item.type === 'comment') {
return item.content;
} else {
const value = item.originalQuotes ? `"${item.value}"` : item.value;
return `${item.key}=${value}`;
}
}).join('\n');
}
export function encryptEnv(envPath, password, outputPath) {
const envData = fs.readFileSync(envPath, 'utf-8');
const envVars = parseEnvFile(envData);
const salt = crypto.randomBytes(16);
for (const item of envVars) {
if (item.type === 'env') {
item.value = encryptValue(item.value, password, salt);
}
}
const encryptedContent = buildEnvContent(envVars);
const contentBuffer = Buffer.from(encryptedContent, 'utf-8');
const saltHeader = Buffer.from('secv', 'utf-8'); // 4 bytes exactos
const version = Buffer.from([0x01]);
const contentLength = Buffer.alloc(4);
contentLength.writeUInt32BE(contentBuffer.length, 0);
const finalPayload = Buffer.concat([
saltHeader, // 4 bytes: "secv"
version, // 1 byte: version
salt, // 16 bytes: salt
contentLength, // 4 bytes: content length
contentBuffer // variable: encrypted content
]);
fs.writeFileSync(outputPath, finalPayload);
}
export function decryptEnv(encPath, password, outputPath) {
const payload = fs.readFileSync(encPath);
// validate file format
const magic = payload.subarray(0, 4).toString('utf-8');
if (magic !== 'secv') {
throw new Error('Invalid encrypted file format. Not a valid .env.enc file.');
}
const version = payload[4];
if (version !== 0x01) {
throw new Error('Unsupported file version.');
}
const salt = payload.subarray(5, 21);
const contentLength = payload.readUInt32BE(21);
const encryptedContent = payload.subarray(25, 25 + contentLength);
const contentString = encryptedContent.toString('utf-8');
const envVars = parseEnvFile(contentString);
for (const item of envVars) {
if (item.type === 'env') {
try {
item.value = decryptValue(item.value, password, salt);
} catch (error) {
throw new Error('Failed to decrypt. Check your password.');
}
}
}
const decryptedContent = buildEnvContent(envVars);
fs.writeFileSync(outputPath, decryptedContent);
}