env-encryption
AES-256-GCM encryption and PBKDF2 key derivation patterns for secure secret storage in Node.js
Works with
---
name: env-encryption
description: AES-256-GCM encryption and PBKDF2 key derivation patterns for secure secret storage in Node.js
license: Apache-2.0
---
# 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`.More Security skills
azure-cost
microsoft/azure-skills
Azure cost management: query costs, forecast spending, optimize to reduce waste. WHEN: \"Azure costs\", \"Azure bill\", \"cost breakdown\", \"how much am I spending\", \"forecast spending\", \"optimize costs\", \"reduce spending\", \"orphaned resources\", \"rightsize VMs\", \"cost spike\", \"reduce storage costs\", \"AKS cost\". DO NOT USE FOR: deploying resources, provisioning, diagnostics, or security audits.
entra-app-registration
microsoft/azure-skills
Guides Microsoft Entra ID app registration, OAuth 2.0 authentication, and MSAL integration. USE FOR: create app registration, register Azure AD app, configure OAuth, set up authentication, add API permissions, generate service principal, MSAL example, console app auth, Entra ID setup, Azure AD authentication. DO NOT USE FOR: Key Vault secrets (use azure-keyvault-expiration-audit), general Azure resource security guidance.
azure-messaging
microsoft/azure-skills
Troubleshoot and resolve issues with Azure Messaging SDKs for Event Hubs and Service Bus. Covers connection failures, authentication errors, message processing issues, and SDK configuration problems. WHEN: event hub SDK error, service bus SDK issue, messaging connection failure, AMQP error, event processor host issue, message lock lost, message lock expired, lock renewal, lock renewal batch, send timeout, receiver disconnected, SDK troubleshooting, azure messaging SDK, event hub consumer, service bus queue issue, topic subscription error, enable logging event hub, service bus logging, eventhub python, servicebus java, eventhub javascript, servicebus dotnet, event hub checkpoint, event hub not receiving messages, service bus dead letter, batch processing lock, session lock expired, idle timeout, connection inactive, link detach, slow reconnect, session error, duplicate events, offset reset, receive batch.

