UNPKG

fast-envcrypt

Version:

Secure AES-256 .env file encryption CLI tool with argon2/scrypt support

185 lines 7.15 kB
import readline from "readline"; import { createCipheriv, randomBytes, scrypt, pbkdf2 } from "crypto"; import { readFile, writeFile } from "fs/promises"; import { existsSync } from "fs"; import { promisify } from "util"; import { getPassword, validateEnvFile, clearSensitiveData } from "./utils.js"; import chalk from "chalk"; const scryptAsync = promisify(scrypt); const pbkdf2Async = promisify(pbkdf2); function promptPassword() { return new Promise((resolve) => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.stdoutMuted = true; rl.question("🔐 Enter password: ", (password) => { rl.close(); console.log(); resolve(password); }); rl._writeToOutput = function _writeToOutput(stringToWrite) { if (rl.stdoutMuted) rl._output.write("*"); else rl._output.write(stringToWrite); }; }); } export async function encryptFile(inputPath, outputPath, password, algorithm = "argon2", force = false) { let key = null; let encryptionPassword = null; try { // Check if input file exists if (!existsSync(inputPath)) { throw new Error(`Input file not found: ${inputPath}`); } // Check if output file exists if (!force && existsSync(outputPath)) { throw new Error(`Output file already exists: ${outputPath}. Use --force to overwrite`); } // Get password securely encryptionPassword = password || (await getPassword("Enter encryption password: ")); if (!encryptionPassword || encryptionPassword.length < 6) { throw new Error("Password must be at least 6 characters long"); } // Read and validate file content const envContent = await readFile(inputPath, "utf8"); if (!envContent.trim()) { throw new Error("ENV file is empty or does not exist"); } if (!validateEnvFile(envContent)) { console.warn(chalk.yellow("⚠️ Warning: File does not appear to be a valid .env file")); } // Generate cryptographic materials const salt = randomBytes(32); const iv = randomBytes(16); // Derive encryption key try { if (algorithm === "scrypt") { key = (await scryptAsync(encryptionPassword, salt, 32)); } else if (algorithm === "argon2") { // For argon2, we'll use pbkdf2 as fallback since argon2 requires additional dependency // In production, you should use actual argon2 library key = (await pbkdf2Async(encryptionPassword, salt, 100000, 32, "sha512")); } else { // Default to pbkdf2 key = (await pbkdf2Async(encryptionPassword, salt, 100000, 32, "sha512")); } } catch (keyError) { throw new Error(`Failed to derive encryption key: ${keyError.message}`); } // Encrypt the content let encrypted; let authTag; try { // Use AES-256-GCM for authenticated encryption const cipher = createCipheriv("aes-256-gcm", key, iv); encrypted = cipher.update(envContent, "utf8", "hex"); encrypted += cipher.final("hex"); // Get authentication tag for GCM mode authTag = cipher.getAuthTag().toString("hex"); } catch (encryptError) { throw new Error(`Encryption failed: ${encryptError.message}`); } // Prepare encrypted data structure const encryptedData = { algorithm: "aes-256-gcm", keyDerivation: algorithm === "scrypt" ? "scrypt" : "pbkdf2", salt: salt.toString("hex"), iv: iv.toString("hex"), data: encrypted, authTag: authTag, timestamp: Date.now(), version: "1.0.0", }; // Save encrypted data const jsonOutput = JSON.stringify(encryptedData, null, 2); await writeFile(outputPath, jsonOutput, "utf8"); // Verify the encrypted file was written correctly if (!existsSync(outputPath)) { throw new Error("Failed to write encrypted file"); } console.log(chalk.green(`✅ File encrypted successfully`)); console.log(chalk.cyan(`📊 Original size: ${envContent.length} bytes`)); console.log(chalk.cyan(`📊 Encrypted size: ${jsonOutput.length} bytes`)); console.log(chalk.cyan(`🔐 Algorithm: ${encryptedData.algorithm}`)); console.log(chalk.cyan(`🔑 Key derivation: ${encryptedData.keyDerivation}`)); } catch (error) { // Clean up sensitive data from memory if (key) clearSensitiveData(key); if (encryptionPassword) clearSensitiveData(encryptionPassword); if (error.code === "ENOENT") { throw new Error(`File not found: ${inputPath}`); } else if (error.code === "EACCES") { throw new Error(`Permission denied: ${error.path}`); } else if (error.code === "ENOSPC") { throw new Error(`No space left on device`); } throw new Error(`Encryption error: ${error.message}`); } finally { // Always clean up sensitive data if (key) clearSensitiveData(key); if (encryptionPassword) clearSensitiveData(encryptionPassword); } } /** * Encrypt a string directly (utility function) */ export async function encryptString(content, password, algorithm = "argon2") { const salt = randomBytes(32); const iv = randomBytes(16); let key; if (algorithm === "scrypt") { key = (await scryptAsync(password, salt, 32)); } else { key = (await pbkdf2Async(password, salt, 100000, 32, "sha512")); } const cipher = createCipheriv("aes-256-gcm", key, iv); let encrypted = cipher.update(content, "utf8", "hex"); encrypted += cipher.final("hex"); const authTag = cipher.getAuthTag().toString("hex"); // Clear sensitive data clearSensitiveData(key); return { algorithm: "aes-256-gcm", keyDerivation: algorithm === "scrypt" ? "scrypt" : "pbkdf2", salt: salt.toString("hex"), iv: iv.toString("hex"), data: encrypted, authTag: authTag, timestamp: Date.now(), version: "1.0.0", }; } /** * Get file encryption info without decrypting */ export async function getEncryptionInfo(filePath) { try { const content = await readFile(filePath, "utf8"); const data = JSON.parse(content); // Return non-sensitive information only return { algorithm: data.algorithm, keyDerivation: data.keyDerivation, timestamp: data.timestamp, version: data.version, }; } catch (error) { throw new Error(`Cannot read encryption info: ${error.message}`); } } //# sourceMappingURL=encrypt.js.map