Secrets Management
intermediateSecure secrets handling with encryption at rest across environments.
environmentsecretssecurityencryption
Tested on⬢20▲16⚛19TS5.9
$ bunx sinew add environment/secretsInteractive demo coming soon
1The Problem
Environment variables in plain text are risky:
.envfiles accidentally committed to git- Secrets visible in CI/CD logs
- No encryption at rest
- Hard to rotate secrets across environments
2The Solution
Use encrypted secrets with a master key. Encrypt sensitive values at rest and decrypt at runtime.
3Files
lib/secrets.ts
lib/secrets.tsTypeScript
import { createCipheriv, createDecipheriv, randomBytes, scrypt } from "crypto";
import { promisify } from "util";
const scryptAsync = promisify(scrypt);
const ALGORITHM = "aes-256-gcm";
const ENCODING = "base64";
async function getKey(password: string, salt: Buffer): Promise<Buffer> {
return (await scryptAsync(password, salt, 32)) as Buffer;
}
export async function encrypt(plaintext: string, masterKey: string): Promise<string> {
const salt = randomBytes(16);
const iv = randomBytes(12);
const key = await getKey(masterKey, salt);
const cipher = createCipheriv(ALGORITHM, key, iv);
let encrypted = cipher.update(plaintext, "utf8", ENCODING);
encrypted += cipher.final(ENCODING);
const authTag = cipher.getAuthTag();
// Format: salt:iv:authTag:encrypted
return [
salt.toString(ENCODING),
iv.toString(ENCODING),
authTag.toString(ENCODING),
encrypted,
].join(":");
}
export async function decrypt(encryptedData: string, masterKey: string): Promise<string> {
const [saltB64, ivB64, authTagB64, encrypted] = encryptedData.split(":");
const salt = Buffer.from(saltB64, ENCODING);
const iv = Buffer.from(ivB64, ENCODING);
const authTag = Buffer.from(authTagB64, ENCODING);
const key = await getKey(masterKey, salt);
const decipher = createDecipheriv(ALGORITHM, key, iv);
decipher.setAuthTag(authTag);
let decrypted = decipher.update(encrypted, ENCODING, "utf8");
decrypted += decipher.final("utf8");
return decrypted;
}
// Cache decrypted secrets
const secretsCache = new Map<string, string>();
export async function getSecret(key: string): Promise<string> {
if (secretsCache.has(key)) {
return secretsCache.get(key)!;
}
const encryptedValue = process.env[key];
if (!encryptedValue) {
throw new Error(`Secret ${key} not found`);
}
// If value starts with "enc:", it's encrypted
if (encryptedValue.startsWith("enc:")) {
const masterKey = process.env.MASTER_KEY;
if (!masterKey) {
throw new Error("MASTER_KEY not set");
}
const decrypted = await decrypt(encryptedValue.slice(4), masterKey);
secretsCache.set(key, decrypted);
return decrypted;
}
// Not encrypted, return as-is (for local development)
return encryptedValue;
}scripts/encrypt-secret.ts
scripts/encrypt-secret.tsTypeScript
#!/usr/bin/env npx ts-node
import { encrypt } from "../lib/secrets";
async function main() {
const [, , masterKey, secretValue] = process.argv;
if (!masterKey || !secretValue) {
console.error("Usage: npx ts-node scripts/encrypt-secret.ts <masterKey> <secretValue>");
process.exit(1);
}
const encrypted = await encrypt(secretValue, masterKey);
console.log(`enc:${encrypted}`);
}
main();lib/env.ts
lib/env.tsTypeScript
import { z } from "zod";
import { getSecret } from "./secrets";
const envSchema = z.object({
NODE_ENV: z.enum(["development", "production", "test"]),
DATABASE_URL: z.string(),
// Add other env vars...
});
let cachedEnv: z.infer<typeof envSchema> | null = null;
export async function getEnv() {
if (cachedEnv) return cachedEnv;
const env = {
NODE_ENV: process.env.NODE_ENV || "development",
DATABASE_URL: await getSecret("DATABASE_URL"),
// Decrypt other secrets as needed
};
const parsed = envSchema.parse(env);
cachedEnv = parsed;
return parsed;
}
// Synchronous version for cases where you can't await
export function getEnvSync() {
if (!cachedEnv) {
throw new Error("Environment not initialized. Call getEnv() first.");
}
return cachedEnv;
}instrumentation.ts
instrumentation.tsTypeScript
export async function register() {
// Initialize environment on app start
const { getEnv } = await import("./lib/env");
await getEnv();
console.info("Environment initialized");
}lib/rotate-secrets.ts
lib/rotate-secrets.tsTypeScript
import { encrypt, decrypt } from "./secrets";
export async function rotateSecrets(
oldMasterKey: string,
newMasterKey: string,
encryptedSecrets: Record<string, string>
): Promise<Record<string, string>> {
const rotated: Record<string, string> = {};
for (const [key, encryptedValue] of Object.entries(encryptedSecrets)) {
// Decrypt with old key
const plaintext = await decrypt(encryptedValue, oldMasterKey);
// Encrypt with new key
const newEncrypted = await encrypt(plaintext, newMasterKey);
rotated[key] = `enc:${newEncrypted}`;
}
return rotated;
}.env.example
.env.exampleBash
# Master key for decrypting secrets (32+ chars recommended)
MASTER_KEY="your-super-secret-master-key-here"
# Encrypted secrets (generate with scripts/encrypt-secret.ts)
DATABASE_URL="enc:salt:iv:authTag:encryptedData"
STRIPE_SECRET_KEY="enc:salt:iv:authTag:encryptedData"4Configuration
Generate a Master Key
openssl rand -base64 32Bash
Encrypt a Secret
npx ts-node scripts/encrypt-secret.ts "your-master-key" "secret-value-to-encrypt"Bash
Output:
enc:salt:iv:authTag:encryptedDataTypeScript
Store Encrypted Secrets
Add the encrypted value to your .env:
DATABASE_URL="enc:Abc123..."Bash
5Usage
Getting Secrets in Code
import { getSecret } from "@/lib/secrets";
// Automatically decrypts if value starts with "enc:"
const dbUrl = await getSecret("DATABASE_URL");TypeScript
Using with Database Clients
import { getEnv } from "@/lib/env";
import { PrismaClient } from "@prisma/client";
let prisma: PrismaClient;
export async function getPrisma() {
if (prisma) return prisma;
const env = await getEnv();
prisma = new PrismaClient({
datasources: {
db: { url: env.DATABASE_URL },
},
});
return prisma;
}TypeScript
6Troubleshooting
"MASTER_KEY not set" error
- Ensure
MASTER_KEYis set in production environment - In development, you can use unencrypted values (without
enc:prefix)
Decryption failing
- Verify the master key matches the one used for encryption
- Check that the encrypted value hasn't been truncated
- Ensure no extra whitespace in environment variables