UNPKG

hidenv

Version:

Beautiful CLI tool to encrypt and decrypt .env files with AES-256-GCM

151 lines (118 loc) 4.38 kB
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); }