UNPKG

fast-envcrypt

Version:

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

179 lines 6.91 kB
import { createDecipheriv, scrypt, pbkdf2 } from "crypto"; import { readFile, writeFile } from "fs/promises"; import { existsSync } from "fs"; import { promisify } from "util"; import { getPassword, clearSensitiveData } from "./utils.js"; const scryptAsync = promisify(scrypt); const pbkdf2Async = promisify(pbkdf2); export async function decryptFile(inputPath, outputPath, password, force = false) { let key = null; let decryptionPassword = null; try { // Check if input file exists if (!existsSync(inputPath)) { throw new Error(`Encrypted 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`); } // Read encrypted data const encryptedContent = await readFile(inputPath, "utf8"); let encryptedData; try { encryptedData = JSON.parse(encryptedContent); } catch (parseError) { throw new Error("Invalid encrypted file format"); } // Validate encrypted data structure if (!encryptedData.algorithm || !encryptedData.data || !encryptedData.salt || !encryptedData.iv) { throw new Error("Corrupted encrypted file - missing required fields"); } // Get password decryptionPassword = password || (await getPassword("Enter decryption password: ")); if (!decryptionPassword) { throw new Error("Password is required for decryption"); } // Convert salt and IV from hex to buffer const salt = Buffer.from(encryptedData.salt, "hex"); const iv = Buffer.from(encryptedData.iv, "hex"); // Recreate the key try { if (encryptedData.keyDerivation === "scrypt") { key = (await scryptAsync(decryptionPassword, salt, 32)); } else if (encryptedData.keyDerivation === "argon2") { // Use pbkdf2 as fallback for argon2 key = (await pbkdf2Async(decryptionPassword, salt, 100000, 32, "sha512")); } else { // Default to pbkdf2 key = (await pbkdf2Async(decryptionPassword, salt, 100000, 32, "sha512")); } } catch (keyError) { throw new Error(`Failed to derive encryption key: ${keyError.message}`); } // Decrypt the data let decrypted; try { const decipher = createDecipheriv(encryptedData.algorithm, key, iv); // Handle authentication tag for GCM mode if (encryptedData.authTag && encryptedData.algorithm.toLowerCase().includes("gcm")) { const authTag = Buffer.from(encryptedData.authTag, "hex"); // Type assertion to allow setAuthTag for GCM mode decipher.setAuthTag(authTag); } decrypted = decipher.update(encryptedData.data, "hex", "utf8"); decrypted += decipher.final("utf8"); } catch (decryptError) { if (decryptError.message.includes("bad decrypt") || decryptError.message.includes("wrong final block length") || decryptError.message.includes("Unsupported state") || decryptError.message.includes("invalid tag")) { throw new Error("Invalid password or corrupted file"); } throw new Error(`Decryption failed: ${decryptError.message}`); } // Validate decrypted content if (!decrypted || decrypted.trim().length === 0) { throw new Error("Decryption resulted in empty content"); } // Save the result await writeFile(outputPath, decrypted, "utf8"); } catch (error) { // Clean up any sensitive data from memory if (key) clearSensitiveData(key); if (decryptionPassword) clearSensitiveData(decryptionPassword); // Handle specific error types 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`); } if (typeof error.message === "string") { throw new Error(error.message); } throw new Error(`Decryption error: ${error.toString()}`); } finally { // Always clean up sensitive data if (key) clearSensitiveData(key); if (decryptionPassword) clearSensitiveData(decryptionPassword); } } /** * Additional utility function for verification */ export async function verifyDecryption(inputPath, password) { try { const encryptedContent = await readFile(inputPath, "utf8"); const encryptedData = JSON.parse(encryptedContent); const salt = Buffer.from(encryptedData.salt, "hex"); const iv = Buffer.from(encryptedData.iv, "hex"); let key; if (encryptedData.keyDerivation === "scrypt") { key = (await scryptAsync(password, salt, 32)); } else { key = (await pbkdf2Async(password, salt, 100000, 32, "sha512")); } const decipher = createDecipheriv(encryptedData.algorithm, key, iv); // Handle authentication tag for GCM mode if (encryptedData.authTag && encryptedData.algorithm.toLowerCase().includes("gcm")) { const authTag = Buffer.from(encryptedData.authTag, "hex"); decipher.setAuthTag(authTag); } decipher.update(encryptedData.data, "hex", "utf8"); decipher.final("utf8"); // Clean up sensitive data clearSensitiveData(key); return true; } catch { return false; } } /** * Get encrypted file information without decrypting */ export async function getEncryptedFileInfo(filePath) { try { const content = await readFile(filePath, "utf8"); const data = JSON.parse(content); // Return only non-sensitive information return { algorithm: data.algorithm, keyDerivation: data.keyDerivation, timestamp: data.timestamp, version: data.version || "unknown", }; } catch (error) { throw new Error(`Cannot read file information: ${error.message}`); } } /** * Check if file is a valid encrypted file */ export async function isValidEncryptedFile(filePath) { try { const info = await getEncryptedFileInfo(filePath); return !!(info.algorithm && info.keyDerivation); } catch { return false; } } //# sourceMappingURL=decrypt.js.map