Simple Encryption for .env files in Node.js

Fields MarshallFields Marshall
4 min read

Encrypting your .env file in a Node.js project helps protect sensitive environment variables, such as API keys and database URLs, especially when sharing code or deploying applications. This approach allows you to keep these details secure while still allowing others to pull down and build the project from scratch, as they can decrypt the file with a provided password.

Even a simple password can add a basic layer of protection if full security isn't a priority, making this method an easy yet effective way to prevent accidental exposure of sensitive information.

If you want to encrypt you .env files with a password with the crypto module built in to node then use this script and follow this process.

postinstall.js

const crypto = require('node:crypto');
const fs = require('node:fs');
const readline = require('node:readline');

// Check if running in production based on CONTEXT or VERCEL_ENV
if (
  process.env.VERCEL_ENV?.toLowerCase() === 'production' ||
  process.env.CONTEXT?.toLowerCase() === 'production'
) {
  console.log(
    'Production environment detected. Skipping encryption/decryption.'
  )
  process.exit(0) // Exit the script without running any further
}

// Continue with the rest of your script if not in production
console.log(
  'Not in production environment. Proceeding with encryption/decryption.'
)

console.log('process.env')
console.log(process.env)

// Helper function to prompt for a password
function promptPassword(question) {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
    terminal: true
  });

  return new Promise((resolve) => {
    rl.question(question, (password) => {
      rl.close();
      resolve(password);
    });
  });
}

// Function to encrypt the .env file
async function encryptFile(password) {
  const envContent = fs.readFileSync('.env', 'utf8');
  const iv = crypto.randomBytes(16);
  const key = crypto.scryptSync(password, 'salt', 32);
  const cipher = crypto.createCipheriv('aes-256-cbc', key, iv);
  let encrypted = cipher.update(envContent, 'utf8', 'hex');
  encrypted += cipher.final('hex');

  const encryptedContent = `${iv.toString('hex')}:${encrypted}`;
  fs.writeFileSync('.env.enc', encryptedContent);
  console.log('.env file encrypted and saved to .env.enc');
}

// Function to decrypt the .env.enc file
async function decryptFile(password) {
  const encryptedContent = fs.readFileSync('.env.enc', 'utf8');
  const [ivHex, encryptedData] = encryptedContent.split(':');
  const key = crypto.scryptSync(password, 'salt', 32);
  const decipher = crypto.createDecipheriv('aes-256-cbc', key, Buffer.from(ivHex, 'hex'));

  let decrypted = decipher.update(encryptedData, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  fs.writeFileSync('.env', decrypted);
  console.log('.env file restored from .env.enc');
}

// Main logic to check file existence and perform encryption/decryption
(async () => {
  const envExists = fs.existsSync('.env');
  const envEncExists = fs.existsSync('.env.enc');

  if (envExists && !envEncExists) {
    // If .env exists but .env.enc does not, encrypt .env
    console.log('.env exists but .env.enc does not. Encrypting .env...');
    const password = await promptPassword('Enter password to encrypt .env: ');
    await encryptFile(password);
  } else if (envEncExists && !envExists) {
    // If .env.enc exists but .env does not, decrypt .env.enc
    console.log('.env.enc exists but .env does not. Decrypting .env.enc...');
    const password = await promptPassword('Enter password to decrypt .env.enc: ');
    try {
      await decryptFile(password);
    } catch (error) {
      console.error('Decryption failed. Please check the password and try again.');
    }
  } else {
    console.log('No action needed. Either both or neither .env/.env.enc files exist.');
  }
})();

you would then modify the package.json file to include 1 line

"scripts": {
    "build": "hugo && node postinstall.js",
    "dev": "npm run server",
    "server": "node postinstall.js && hugo server",
    "postinstall": "node postinstall.js"
  }

now you can check in your code with your private information encrypted.

How it works

This postinstall.js script helps keep sensitive information in your project safe by automatically encrypting and decrypting environment variables stored in a .env file.

Here’s How It Works:

  1. Encryption and Decryption in One Place: The script has both functions needed to protect your .env file — it can either turn it into a secure, unreadable file called .env.enc (encryption) or turn .env.enc back into a readable .env file (decryption).

  2. Automatic Checks:

    • If it finds only .env, it knows you need to encrypt it and will prompt you for a password. This password "locks" the file so others can’t easily read it.

    • If it finds only .env.enc, it knows you want to decrypt it and will prompt for the password again to "unlock" it.

  3. Password Prompt: The script asks for a password whenever it encrypts or decrypts, so you keep control over your sensitive information.

  4. Integrated into Your Project: By adding this to your project’s postinstall script, it will run automatically after installing packages. This way, your environment files stay safe without extra manual steps.

In short, the script handles everything you need to protect your environment variables with simple commands and prompts!

By implementing this method, developers can enhance the security of their environment variables, safeguarding sensitive information throughout the development cycle.

0
Subscribe to my newsletter

Read articles from Fields Marshall directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Fields Marshall
Fields Marshall