env-encryption
AES-256-GCM encryption and PBKDF2 key derivation patterns for secure secret storage in Node.js
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
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/env-encryption/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How env-encryption Compares
| Feature / Agent | env-encryption | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/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
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
## Overview
Skill: Status Page
## Overview
Skill: unit-conversion
## Overview
Skill: recipe-scaler
## Overview
reading-list
Operate the reading-list API to save, manage, tag, search, and export articles.
email-digest
Configure, test, and troubleshoot the reading-list daily email digest delivered via nodemailer.
websocket-realtime
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
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
## Overview
Skill: csv-import
## Overview
Skill: Syntax Highlighting
## Purpose