env-encryption

AES-256-GCM encryption and PBKDF2 key derivation patterns for secure secret storage in Node.js

7 stars

Best use case

env-encryption is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

AES-256-GCM encryption and PBKDF2 key derivation patterns for secure secret storage in Node.js

Teams using env-encryption should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/env-encryption/SKILL.md --create-dirs "https://raw.githubusercontent.com/heldernoid/agentic-build-templates/main/projects/developer-tools/env-file-manager/skills/env-encryption/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/env-encryption/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How env-encryption Compares

Feature / Agentenv-encryptionStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

AES-256-GCM encryption and PBKDF2 key derivation patterns for secure secret storage in Node.js

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# env-encryption skill

## When to use

Use this skill when the user needs to:
- Understand the AES-256-GCM encryption scheme used by env-file-manager
- Implement similar encryption in their own code
- Verify the security properties of the vault format
- Troubleshoot decryption failures or auth tag errors

## Overview

env-file-manager encrypts each variable value independently using AES-256-GCM. The master key is derived from a user passphrase using PBKDF2. The key is never stored - it exists only in memory for the duration of the vault session.

## Encryption Flow

```
passphrase + random salt (32 bytes)
  -> PBKDF2(iterations: 100000, hash: sha256, keylen: 32)
  -> 256-bit master key (in memory only)

For each variable value:
  random IV (12 bytes / 96 bits)
  AES-256-GCM encrypt(plaintext, key, IV)
    -> ciphertext
    -> auth tag (16 bytes / 128 bits)

Stored in vault JSON:
  { iv: base64, ciphertext: base64, tag: base64 }
```

## Key Derivation

PBKDF2 parameters:
- Algorithm: PBKDF2
- Hash: SHA-256
- Iterations: 100,000
- Output key length: 32 bytes (256 bits)
- Salt: 32 random bytes, stored in vault file (not secret)

The salt is generated once per vault and stored in the vault JSON file. It is not secret - its purpose is to prevent precomputation attacks.

The passphrase is never stored. Deriving the key from the same passphrase + salt always produces the same key.

## Node.js Implementation Reference

### Key derivation

```typescript
import { pbkdf2Sync, randomBytes } from 'node:crypto';

function deriveKey(passphrase: string, salt: Buffer): Buffer {
  return pbkdf2Sync(passphrase, salt, 100_000, 32, 'sha256');
}

function generateSalt(): Buffer {
  return randomBytes(32);
}
```

### Encryption

```typescript
import { createCipheriv, randomBytes } from 'node:crypto';

function encrypt(plaintext: string, key: Buffer): {
  iv: string;
  ciphertext: string;
  tag: string;
} {
  const iv = randomBytes(12);
  const cipher = createCipheriv('aes-256-gcm', key, iv);
  const encrypted = Buffer.concat([
    cipher.update(plaintext, 'utf8'),
    cipher.final(),
  ]);
  const tag = cipher.getAuthTag();
  return {
    iv: iv.toString('base64'),
    ciphertext: encrypted.toString('base64'),
    tag: tag.toString('base64'),
  };
}
```

### Decryption

```typescript
import { createDecipheriv } from 'node:crypto';

function decrypt(
  { iv, ciphertext, tag }: { iv: string; ciphertext: string; tag: string },
  key: Buffer,
): string {
  const decipher = createDecipheriv(
    'aes-256-gcm',
    key,
    Buffer.from(iv, 'base64'),
  );
  decipher.setAuthTag(Buffer.from(tag, 'base64'));
  const decrypted = Buffer.concat([
    decipher.update(Buffer.from(ciphertext, 'base64')),
    decipher.final(),
  ]);
  return decrypted.toString('utf8');
}
```

If the key is wrong or the ciphertext has been tampered with, `decipher.final()` throws with "Unsupported state or unable to authenticate data". Catch this error and return an appropriate message to the user - do not log the key or plaintext in the error handler.

## Security Properties

| Property | Value |
|---|---|
| Cipher | AES-256-GCM |
| Key size | 256 bits |
| IV size | 96 bits (random per variable per write) |
| Auth tag | 128 bits |
| KDF | PBKDF2-SHA256 |
| KDF iterations | 100,000 |
| Salt size | 256 bits (random per vault, stored in vault file) |

### Why AES-256-GCM

AES-GCM provides both confidentiality and authentication (AEAD). If a ciphertext is tampered with, decryption will fail with an auth tag mismatch before any plaintext is returned. This prevents padding oracle attacks and detects corruption.

### Why PBKDF2

PBKDF2 with 100,000 iterations significantly increases the cost of brute-forcing the passphrase. A modern GPU running bcrypt at 100k iterations can attempt millions of passphrases per second with a simple symmetric cipher; PBKDF2 at this iteration count slows that to a manageable rate for strong passphrases.

For higher security requirements, consider increasing iterations to 600,000 (current NIST recommendation as of 2023) at the cost of slower unlock times.

### What is NOT protected

- The vault salt (stored in plaintext - this is by design)
- Variable key names (stored as JSON keys - only values are encrypted)
- The number of variables in a vault (visible from the JSON structure)

## Vault File Format Reference

```json
{
  "version": 1,
  "project": "my-app",
  "environment": "production",
  "kdf": {
    "algorithm": "pbkdf2",
    "iterations": 100000,
    "hash": "sha256",
    "salt": "<base64 32 bytes>"
  },
  "variables": {
    "DATABASE_URL": {
      "iv": "<base64 12 bytes>",
      "ciphertext": "<base64 N bytes>",
      "tag": "<base64 16 bytes>",
      "updated_at": "2026-01-15T12:00:00Z"
    }
  }
}
```

## Troubleshooting

**"Unable to authenticate data" on decrypt** - Either the passphrase is wrong (key mismatch) or the vault file has been corrupted or tampered with. There is no bypass - the auth tag check is mandatory for security.

**Different ciphertext for the same value** - This is correct and expected. Each encryption call uses a new random IV, producing a different ciphertext even for identical plaintexts.

**Slow unlock** - PBKDF2 with 100,000 iterations takes roughly 100-300ms on modern hardware. This is intentional. If unlock speed is critical for your use case, reduce iterations - but this weakens brute-force resistance.

**Vault file corruption** - If the vault JSON file is corrupted, decryption will fail. Restore from the S3 backup copy with `env-mgr pull`.

Related Skills

file-encryption

7
from heldernoid/agentic-build-templates

AES-256-GCM file encryption and PBKDF2 key derivation as used in health-records-vault. Use when you need to understand or implement the encryption model, derive a key from a password, encrypt or decrypt a file manually, restore an encrypted backup, or verify the cryptographic integrity of a .enc file. Triggers include "AES-256-GCM", "PBKDF2", "decrypt .enc file", "restore backup", "encryption key derivation", "IV", "salt", or any task about the cryptographic internals of the vault.

Skill: Uptime Monitoring

7
from heldernoid/agentic-build-templates

## Overview

Skill: Status Page

7
from heldernoid/agentic-build-templates

## Overview

Skill: unit-conversion

7
from heldernoid/agentic-build-templates

## Overview

Skill: recipe-scaler

7
from heldernoid/agentic-build-templates

## Overview

reading-list

7
from heldernoid/agentic-build-templates

Operate the reading-list API to save, manage, tag, search, and export articles.

email-digest

7
from heldernoid/agentic-build-templates

Configure, test, and troubleshoot the reading-list daily email digest delivered via nodemailer.

websocket-realtime

7
from heldernoid/agentic-build-templates

Use the WebSocket connection in poll-builder to receive live vote updates. Use when you need to stream real-time poll results, monitor a poll for new votes, or build a live dashboard. Triggers include "live results", "real-time updates", "stream votes", "watch poll", or "WebSocket".

poll-builder

7
from heldernoid/agentic-build-templates

Self-hosted poll creation tool with real-time results. Use when you need to create a poll, check vote counts, close a poll, export results, or get the shareable link for a poll. Triggers include "create poll", "vote", "poll results", "survey", "collect votes", "share poll", or any task involving polling or voting.

Skill: personal-finance

7
from heldernoid/agentic-build-templates

## Overview

Skill: csv-import

7
from heldernoid/agentic-build-templates

## Overview

Skill: Syntax Highlighting

7
from heldernoid/agentic-build-templates

## Purpose