fast-envcrypt
Version:
Secure AES-256 .env file encryption CLI tool with argon2/scrypt support
217 lines • 7.49 kB
JavaScript
import crypto from "crypto";
import argon2 from "argon2";
import { createInterface } from "readline";
import { stdin, stdout } from "process";
const ALGORITHM = "aes-256-cbc";
export async function generateKey(password, salt, algo = "argon2") {
if (algo === "argon2") {
return await argon2.hash(password, {
type: argon2.argon2id,
salt,
hashLength: 32,
raw: true,
memoryCost: 2 ** 16,
timeCost: 5,
parallelism: 1,
});
}
else if (algo === "scrypt") {
return crypto.scryptSync(password, salt, 32);
}
throw new Error(`Unknown algorithm: ${algo}`);
}
export async function encrypt(text, password, algo = "argon2") {
const iv = crypto.randomBytes(16);
const salt = crypto.randomBytes(16);
const key = await generateKey(password, salt, algo);
const cipher = crypto.createCipheriv(ALGORITHM, key, iv);
const encrypted = Buffer.concat([cipher.update(text), cipher.final()]);
return [algo, salt.toString("hex"), iv.toString("hex"), encrypted.toString("hex")].join(":");
}
export async function decrypt(data, password) {
const [algoStr, saltHex, ivHex, encryptedHex] = data.split(":");
const algo = algoStr;
const salt = Buffer.from(saltHex, "hex");
const iv = Buffer.from(ivHex, "hex");
const encrypted = Buffer.from(encryptedHex, "hex");
const key = await generateKey(password, salt, algo);
const decipher = crypto.createDecipheriv(ALGORITHM, key, iv);
const decrypted = Buffer.concat([decipher.update(encrypted), decipher.final()]);
return decrypted.toString();
}
/**
* Secure password input - FIXED VERSION
*/
export async function getPassword(prompt) {
return new Promise((resolve, reject) => {
let password = "";
let isRawMode = false;
// Display prompt
stdout.write(prompt);
try {
// Check if we're in a TTY (interactive terminal)
if (!stdin.isTTY) {
// Non-interactive mode - read from stdin normally
const rl = createInterface({ input: stdin, output: stdout });
rl.question("", (answer) => {
rl.close();
resolve(answer);
});
return;
}
// Interactive mode - hide password input
stdin.setRawMode(true);
stdin.resume();
stdin.setEncoding("utf8");
isRawMode = true;
const onData = (chunk) => {
const data = chunk.toString();
for (let i = 0; i < data.length; i++) {
const char = data[i];
const charCode = char.charCodeAt(0);
switch (charCode) {
case 3: // Ctrl+C
cleanup();
process.exit(0);
break;
case 13: // Enter
case 10: // Line Feed
cleanup();
stdout.write("\n");
resolve(password);
return;
case 127: // Backspace
case 8: // Backspace (Windows)
if (password.length > 0) {
password = password.slice(0, -1);
stdout.write("\b \b");
}
break;
case 4: // Ctrl+D
cleanup();
stdout.write("\n");
resolve(password);
return;
default:
// Only accept printable ASCII characters
if (charCode >= 32 && charCode <= 126) {
password += char;
stdout.write("*");
}
break;
}
}
};
const onError = (error) => {
cleanup();
reject(error);
};
const cleanup = () => {
if (isRawMode && stdin.isTTY) {
stdin.setRawMode(false);
}
stdin.removeListener("data", onData);
stdin.removeListener("error", onError);
stdin.pause();
};
stdin.on("data", onData);
stdin.on("error", onError);
}
catch (error) {
if (isRawMode && stdin.isTTY) {
stdin.setRawMode(false);
}
reject(error);
}
});
}
export function validateEnvFile(content) {
if (!content || !content.trim()) {
return false;
}
// At least one valid KEY=VALUE line must exist
const lines = content.split("\n");
return lines.some((line) => {
const trimmed = line.trim();
return (trimmed && !trimmed.startsWith("#") && trimmed.includes("=") && trimmed.indexOf("=") > 0 // KEY must not be empty
);
});
}
/**
* Clear sensitive data from memory (best effort)
*/
export function clearSensitiveData(obj) {
try {
if (typeof obj === "string") {
// JavaScript strings are immutable, but we can try to overwrite the reference
obj = "";
}
else if (Buffer.isBuffer(obj)) {
// Fill buffer with zeros
obj.fill(0);
}
else if (obj instanceof Uint8Array) {
// Clear typed arrays
obj.fill(0);
}
else if (typeof obj === "object" && obj !== null) {
// Clear object properties
Object.keys(obj).forEach((key) => {
if (obj.hasOwnProperty(key)) {
clearSensitiveData(obj[key]);
delete obj[key];
}
});
}
}
catch (error) {
// Ignore errors during cleanup
console.warn("Warning: Could not clear sensitive data from memory");
}
}
/**
* Sanitize file path for security
*/
export function sanitizePath(filePath) {
// Remove dangerous path traversal attempts
return filePath.replace(/\.\./g, "").replace(/[<>:"|?*]/g, "");
}
/**
* Generate a secure random string
*/
export function generateRandomString(length = 32) {
return crypto.randomBytes(length).toString("hex");
}
/**
* Secure memory allocation for sensitive data
*/
export function secureBuffer(size) {
const buffer = Buffer.alloc(size);
// Fill with random data first
crypto.randomFillSync(buffer);
return buffer;
}
/**
* Constant-time string comparison to prevent timing attacks
*/
export function constantTimeCompare(a, b) {
if (a.length !== b.length) {
return false;
}
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
}
/**
* Get file size in human readable format
*/
export function formatFileSize(bytes) {
const sizes = ["Bytes", "KB", "MB", "GB"];
if (bytes === 0)
return "0 Bytes";
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return Math.round((bytes / Math.pow(1024, i)) * 100) / 100 + " " + sizes[i];
}
//# sourceMappingURL=utils.js.map