@faizahmed/secret-keystore 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +1203 -0
- package/SECURITY.md +505 -0
- package/bin/cli.js +969 -0
- package/package.json +77 -0
- package/src/attestation/attestation-client.js +146 -0
- package/src/attestation/attestation-manager.js +339 -0
- package/src/attestation/cms-unwrap.js +166 -0
- package/src/attestation/index.js +66 -0
- package/src/attestation/key-pair.js +129 -0
- package/src/config.js +130 -0
- package/src/content-operations.js +494 -0
- package/src/errors.js +372 -0
- package/src/index.d.ts +641 -0
- package/src/index.js +438 -0
- package/src/keystore.js +678 -0
- package/src/kms.js +858 -0
- package/src/object-operations.js +232 -0
- package/src/options.js +541 -0
- package/src/path-matcher.js +319 -0
- package/src/rotate.js +92 -0
- package/src/yaml-utils.js +265 -0
package/README.md
ADDED
|
@@ -0,0 +1,1203 @@
|
|
|
1
|
+
# secret-keystore
|
|
2
|
+
|
|
3
|
+
A secure secrets management library for Node.js applications using AWS KMS encryption.
|
|
4
|
+
|
|
5
|
+
[](https://github.com/faizahmedfarooqui/secret-keystore/actions/workflows/ci.yml)
|
|
6
|
+
[](https://www.npmjs.com/package/@faizahmed/secret-keystore)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+

|
|
9
|
+
[](./LICENSE)
|
|
10
|
+
|
|
11
|
+
**Available on npm:** [`@faizahmed/secret-keystore`](https://www.npmjs.com/package/@faizahmed/secret-keystore)
|
|
12
|
+
|
|
13
|
+
> **Design principle:** the only thing a developer ever handles is a **KMS Key ID** — which is *not* a secret. No private keys, no passphrases, no key material. AWS KMS holds all key material server-side and authorizes access via IAM, and decrypted values live **only in memory** (never in `process.env`, never on disk) to keep the blast radius of any RCE as small as possible.
|
|
14
|
+
|
|
15
|
+
## Table of Contents
|
|
16
|
+
|
|
17
|
+
- [Features](#features)
|
|
18
|
+
- [Important Limitations](#important-limitations)
|
|
19
|
+
- [Prerequisites](#prerequisites)
|
|
20
|
+
- [Installation](#installation)
|
|
21
|
+
- [From npm Registry](#from-npm-registry)
|
|
22
|
+
- [Local Development / Docker Builds](#local-development--docker-builds)
|
|
23
|
+
- [Optional: YAML Support](#optional-yaml-support)
|
|
24
|
+
- [Quick Start](#quick-start)
|
|
25
|
+
- [CLI Reference](#cli-reference)
|
|
26
|
+
- [Library API](#library-api)
|
|
27
|
+
- [Runtime Keystore](#runtime-keystore)
|
|
28
|
+
- [Zero-Config Loader: `config()`](#zero-config-loader-config)
|
|
29
|
+
- [Configuration Options](#configuration-options)
|
|
30
|
+
- [How It Works](#how-it-works)
|
|
31
|
+
- [Examples](#examples)
|
|
32
|
+
- [Nitro Enclave Attestation](#nitro-enclave-attestation)
|
|
33
|
+
- [Error Handling](#error-handling)
|
|
34
|
+
- [TypeScript Support](#typescript-support)
|
|
35
|
+
- [Troubleshooting](#troubleshooting)
|
|
36
|
+
- [Development](#development)
|
|
37
|
+
- [Security](#security)
|
|
38
|
+
|
|
39
|
+
## Features
|
|
40
|
+
|
|
41
|
+
- **Multi-Format Support** — Encrypt secrets in `.env`, JSON, and YAML files
|
|
42
|
+
- **Zero-Config Loader** — `config()` discovers and cascades your `.env` files, decrypts them, and loads everything into an in-memory keystore in one call
|
|
43
|
+
- **Encrypt & Decrypt CLI** — Full command-line round-trip (`encrypt` and `decrypt`)
|
|
44
|
+
- **Pattern Matching** — Use glob patterns (`**`) to select keys at any depth
|
|
45
|
+
- **AWS KMS Encryption** — Supports symmetric and asymmetric (RSA) keys; uses envelope encryption for RSA (no plaintext size limit)
|
|
46
|
+
- **Key-ID-Only** — Developers only ever share a non-secret KMS Key ID; no key material to leak
|
|
47
|
+
- **IAM Role by Default** — Uses IAM roles for authentication (explicit credentials require opt-in)
|
|
48
|
+
- **Secure In-Memory Storage** — Decrypted values stored with AES-256-GCM encryption in memory
|
|
49
|
+
- **Never in `process.env`** — Decrypted secrets are only accessible via the keystore API
|
|
50
|
+
- **TTL & Auto-Refresh** — Automatic secret expiry and re-decryption
|
|
51
|
+
- **Nitro Enclave Attestation** — Full attestation lifecycle with automatic 5-minute refresh
|
|
52
|
+
- **Dual API** — Content-based (convenient) + Object-based (flexible)
|
|
53
|
+
- **Comment Preservation** — Preserves comments and formatting in config files
|
|
54
|
+
- **Security-First Dependencies** — Minimal dependencies for security-sensitive operations
|
|
55
|
+
- **Battle-Tested** — Offline test suite (`node:test`, mocked KMS) with CI across Node 18/20/22, plus lint, format, and type-definition checks
|
|
56
|
+
|
|
57
|
+
## Important Limitations
|
|
58
|
+
|
|
59
|
+
> ⚠️ **This library is for SERVER-SIDE use only.** It will NOT work with client-side code.
|
|
60
|
+
|
|
61
|
+
### ❌ Does NOT Work With
|
|
62
|
+
|
|
63
|
+
| Scenario | Reason |
|
|
64
|
+
|----------|--------|
|
|
65
|
+
| **Next.js `NEXT_PUBLIC_*` variables** | These are embedded into client-side JavaScript at build time and exposed to browsers. Browsers cannot access AWS KMS. |
|
|
66
|
+
| **Next.js Client Components** | Client components run in the browser, which has no access to AWS KMS or server-side Node.js APIs. |
|
|
67
|
+
| **NestJS with Client-Side Rendering** | Any code that runs in the browser cannot decrypt KMS-encrypted values. |
|
|
68
|
+
| **React/Vue/Angular frontend apps** | Frontend JavaScript runs in users' browsers, not on servers with AWS access. |
|
|
69
|
+
| **Static Site Generation (SSG) at build time** | Build-time secrets would be embedded in static HTML/JS, defeating the purpose. |
|
|
70
|
+
|
|
71
|
+
### Why These Don't Work
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
75
|
+
│ CLIENT-SIDE (Browser) │
|
|
76
|
+
│ │
|
|
77
|
+
│ ❌ No AWS credentials │
|
|
78
|
+
│ ❌ No access to AWS KMS API │
|
|
79
|
+
│ ❌ No Node.js crypto module │
|
|
80
|
+
│ ❌ NEXT_PUBLIC_* variables are bundled into JS at BUILD time │
|
|
81
|
+
│ ❌ Cannot make authenticated AWS API calls │
|
|
82
|
+
│ │
|
|
83
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
84
|
+
|
|
85
|
+
┌─────────────────────────────────────────────────────────────────────────────┐
|
|
86
|
+
│ SERVER-SIDE (Node.js) │
|
|
87
|
+
│ │
|
|
88
|
+
│ ✅ Has AWS IAM role or credentials │
|
|
89
|
+
│ ✅ Can call AWS KMS Decrypt API │
|
|
90
|
+
│ ✅ Has Node.js crypto module │
|
|
91
|
+
│ ✅ Secrets decrypted at RUNTIME in memory │
|
|
92
|
+
│ ✅ Values never sent to browser │
|
|
93
|
+
│ │
|
|
94
|
+
└─────────────────────────────────────────────────────────────────────────────┘
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### ✅ Works With
|
|
98
|
+
|
|
99
|
+
| Scenario | Example |
|
|
100
|
+
|----------|---------|
|
|
101
|
+
| **Next.js API Routes** | `src/app/api/*/route.ts` |
|
|
102
|
+
| **Next.js Server Components** | Components without `"use client"` |
|
|
103
|
+
| **Next.js Server Actions** | Functions with `"use server"` |
|
|
104
|
+
| **NestJS Services** | Backend services running on Node.js |
|
|
105
|
+
| **Express/Fastify APIs** | Any Node.js backend |
|
|
106
|
+
| **AWS Lambda** | Serverless functions |
|
|
107
|
+
| **Background Jobs** | Cron jobs, workers, etc. |
|
|
108
|
+
|
|
109
|
+
### Next.js Example: What Works vs What Doesn't
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// ❌ WRONG: Client Component - will NOT work
|
|
113
|
+
"use client";
|
|
114
|
+
import { getSecret } from "@/lib/keystore";
|
|
115
|
+
|
|
116
|
+
export function ClientComponent() {
|
|
117
|
+
// This will fail - browsers can't access AWS KMS
|
|
118
|
+
const secret = await getSecret("API_KEY"); // ❌ Error!
|
|
119
|
+
return <div>{secret}</div>;
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
// ❌ WRONG: NEXT_PUBLIC_* variables - CANNOT be encrypted
|
|
125
|
+
// These are embedded in client JS at build time
|
|
126
|
+
const apiKey = process.env.NEXT_PUBLIC_API_KEY; // Already exposed to browser!
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// ✅ CORRECT: API Route (Server-Side)
|
|
131
|
+
// src/app/api/data/route.ts
|
|
132
|
+
import { getSecret } from "@/lib/keystore";
|
|
133
|
+
|
|
134
|
+
export async function GET() {
|
|
135
|
+
// This works - runs on server with AWS access
|
|
136
|
+
const apiKey = await getSecret("API_KEY"); // ✅ Decrypted on server
|
|
137
|
+
|
|
138
|
+
const data = await fetchExternalAPI(apiKey);
|
|
139
|
+
return Response.json(data); // Only send non-sensitive data to client
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// ✅ CORRECT: Server Component (no "use client")
|
|
145
|
+
// src/app/dashboard/page.tsx
|
|
146
|
+
import { getSecret } from "@/lib/keystore";
|
|
147
|
+
|
|
148
|
+
export default async function DashboardPage() {
|
|
149
|
+
// This works - Server Components run on the server
|
|
150
|
+
const dbPassword = await getSecret("DB_PASSWORD"); // ✅ Server-side only
|
|
151
|
+
|
|
152
|
+
const data = await fetchFromDatabase(dbPassword);
|
|
153
|
+
return <Dashboard data={data} />; // Render with data, not secrets
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Summary
|
|
158
|
+
|
|
159
|
+
| Variable Type | Can Encrypt? | Where It Runs |
|
|
160
|
+
|---------------|--------------|---------------|
|
|
161
|
+
| Regular env vars (`DB_PASSWORD`) | ✅ Yes | Server only |
|
|
162
|
+
| `NEXT_PUBLIC_*` vars | ❌ No | Bundled into client JS |
|
|
163
|
+
| Server Component code | ✅ Yes | Server only |
|
|
164
|
+
| Client Component code | ❌ No | Browser |
|
|
165
|
+
| API Route code | ✅ Yes | Server only |
|
|
166
|
+
|
|
167
|
+
## Prerequisites
|
|
168
|
+
|
|
169
|
+
- Node.js >= 18.0.0
|
|
170
|
+
- AWS account with KMS access
|
|
171
|
+
- AWS IAM role (recommended) or explicit credentials for local development
|
|
172
|
+
|
|
173
|
+
## Installation
|
|
174
|
+
|
|
175
|
+
### From npm Registry
|
|
176
|
+
|
|
177
|
+
The package is published to the public npm registry. Install with:
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
# npm
|
|
181
|
+
npm install @faizahmed/secret-keystore
|
|
182
|
+
|
|
183
|
+
# pnpm
|
|
184
|
+
pnpm add @faizahmed/secret-keystore
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
> **Note:** The package includes `@aws-sdk/client-kms` as a dependency.
|
|
188
|
+
|
|
189
|
+
### Local Development / Docker Builds
|
|
190
|
+
|
|
191
|
+
When working with local development or Docker builds where `file:` references don't work (e.g., the path is outside the Docker build context), you can pack the library into a tarball:
|
|
192
|
+
|
|
193
|
+
#### Step 1: Pack the Library
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# From the secret-keystore directory
|
|
197
|
+
|
|
198
|
+
# npm
|
|
199
|
+
npm pack
|
|
200
|
+
|
|
201
|
+
# pnpm
|
|
202
|
+
pnpm pack
|
|
203
|
+
|
|
204
|
+
# This creates: faizahmedfarooqui-secret-keystore-1.0.0.tgz (scoped package name)
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Or use the provided script:
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
# Pack and move to a specific directory (e.g., consumer project)
|
|
211
|
+
npm run pack:local # or: pnpm run pack:local
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
#### Step 2: Install the Tarball
|
|
215
|
+
|
|
216
|
+
In your consumer project:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# Copy the tarball to your project
|
|
220
|
+
cp ../secret-keystore/faizahmedfarooqui-secret-keystore-1.0.0.tgz ./
|
|
221
|
+
|
|
222
|
+
# Install from tarball
|
|
223
|
+
# npm
|
|
224
|
+
npm install ./faizahmedfarooqui-secret-keystore-1.0.0.tgz
|
|
225
|
+
|
|
226
|
+
# pnpm
|
|
227
|
+
pnpm add ./faizahmedfarooqui-secret-keystore-1.0.0.tgz
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Or add a script to your consumer's `package.json`:
|
|
231
|
+
|
|
232
|
+
```json
|
|
233
|
+
{
|
|
234
|
+
"scripts": {
|
|
235
|
+
"pack:keystore": "cd ../secret-keystore && npm pack --pack-destination ../your-project",
|
|
236
|
+
"install:keystore": "npm run pack:keystore && npm install ./faizahmedfarooqui-secret-keystore-*.tgz"
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
#### Step 3: Update package.json Reference
|
|
242
|
+
|
|
243
|
+
After installing, your `package.json` will reference the local tarball:
|
|
244
|
+
|
|
245
|
+
```json
|
|
246
|
+
{
|
|
247
|
+
"dependencies": {
|
|
248
|
+
"@faizahmed/secret-keystore": "file:./faizahmedfarooqui-secret-keystore-1.0.0.tgz"
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
#### Docker Build Configuration
|
|
254
|
+
|
|
255
|
+
For Docker builds, copy the tarball into the build context:
|
|
256
|
+
|
|
257
|
+
```dockerfile
|
|
258
|
+
# Stage 1: Install Dependencies
|
|
259
|
+
FROM node:22 AS deps
|
|
260
|
+
WORKDIR /app
|
|
261
|
+
|
|
262
|
+
# Copy package files
|
|
263
|
+
COPY package.json yarn.lock ./
|
|
264
|
+
|
|
265
|
+
# Copy the local tarball (must be in the Docker build context)
|
|
266
|
+
COPY faizahmedfarooqui-secret-keystore-1.0.0.tgz ./
|
|
267
|
+
|
|
268
|
+
# Install dependencies (tarball is referenced in package.json)
|
|
269
|
+
RUN yarn install --frozen-lockfile
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
> **Important:** The tarball file must be in your project directory (Docker build context). You cannot reference paths outside the build context like `../secret-keystore`.
|
|
273
|
+
|
|
274
|
+
#### Complete Workflow Example
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
# 1. In the keystore library directory
|
|
278
|
+
cd secret-keystore
|
|
279
|
+
npm pack # or: pnpm pack
|
|
280
|
+
mv faizahmedfarooqui-secret-keystore-*.tgz ../your-consumer-project/
|
|
281
|
+
|
|
282
|
+
# 2. In your consumer project
|
|
283
|
+
cd ../your-consumer-project
|
|
284
|
+
|
|
285
|
+
# Update package.json to reference the tarball
|
|
286
|
+
# "dependencies": { "@faizahmed/secret-keystore": "file:./faizahmedfarooqui-secret-keystore-1.0.0.tgz" }
|
|
287
|
+
|
|
288
|
+
npm install # or: pnpm install
|
|
289
|
+
|
|
290
|
+
# 3. Commit the tarball to your repo (for CI/CD)
|
|
291
|
+
git add faizahmedfarooqui-secret-keystore-*.tgz
|
|
292
|
+
git commit -m "Add @faizahmed/secret-keystore tarball for Docker builds"
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
### Optional: YAML Support
|
|
296
|
+
|
|
297
|
+
For YAML files with complex features (anchors, aliases, multi-line strings), install `js-yaml`:
|
|
298
|
+
|
|
299
|
+
```bash
|
|
300
|
+
# npm
|
|
301
|
+
npm install js-yaml
|
|
302
|
+
|
|
303
|
+
# pnpm
|
|
304
|
+
pnpm add js-yaml
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Without `js-yaml`, the library uses a simple built-in parser that handles basic YAML structures. If your YAML uses advanced features without `js-yaml` installed, you'll get a clear error message.
|
|
308
|
+
|
|
309
|
+
## Quick Start
|
|
310
|
+
|
|
311
|
+
### Step 1: Prepare Your Configuration
|
|
312
|
+
|
|
313
|
+
Create a `.env` file with your secrets:
|
|
314
|
+
|
|
315
|
+
```env
|
|
316
|
+
# AWS Configuration (never encrypted)
|
|
317
|
+
KMS_KEY_ID=arn:aws:kms:us-east-1:123456789012:key/abcd-1234
|
|
318
|
+
AWS_REGION=us-east-1
|
|
319
|
+
|
|
320
|
+
# Your secrets (will be encrypted)
|
|
321
|
+
DB_PASSWORD=mysecretpassword
|
|
322
|
+
API_KEY=sk-1234567890abcdef
|
|
323
|
+
JWT_SECRET=super-secret-jwt-key
|
|
324
|
+
|
|
325
|
+
# Non-sensitive values
|
|
326
|
+
DB_HOST=localhost
|
|
327
|
+
PORT=3000
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Step 2: Encrypt Your Secrets
|
|
331
|
+
|
|
332
|
+
Run the CLI to encrypt secrets:
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
# Encrypt specific keys (kms-key-id is REQUIRED)
|
|
336
|
+
npx @faizahmed/secret-keystore encrypt \
|
|
337
|
+
--kms-key-id="arn:aws:kms:us-east-1:123456789012:key/abcd-1234" \
|
|
338
|
+
--keys="DB_PASSWORD,API_KEY,JWT_SECRET"
|
|
339
|
+
|
|
340
|
+
# Or encrypt all keys (except reserved ones)
|
|
341
|
+
npx @faizahmed/secret-keystore encrypt \
|
|
342
|
+
--kms-key-id="alias/my-key"
|
|
343
|
+
|
|
344
|
+
# For local development, use explicit credentials
|
|
345
|
+
npx @faizahmed/secret-keystore encrypt \
|
|
346
|
+
--kms-key-id="alias/my-key" \
|
|
347
|
+
--use-credentials
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Your `.env` file is updated in-place:
|
|
351
|
+
|
|
352
|
+
```env
|
|
353
|
+
# AWS Configuration (never encrypted)
|
|
354
|
+
KMS_KEY_ID=arn:aws:kms:us-east-1:123456789012:key/abcd-1234
|
|
355
|
+
AWS_REGION=us-east-1
|
|
356
|
+
|
|
357
|
+
# Encrypted values
|
|
358
|
+
DB_PASSWORD=ENC[AQICAHh2nZPq...base64...]
|
|
359
|
+
API_KEY=ENC[AQICAHh2nZPq...base64...]
|
|
360
|
+
JWT_SECRET=ENC[AQICAHh2nZPq...base64...]
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Step 3: Use in Your Application
|
|
364
|
+
|
|
365
|
+
```javascript
|
|
366
|
+
const { createSecretKeyStore } = require('@faizahmed/secret-keystore');
|
|
367
|
+
const fs = require('node:fs');
|
|
368
|
+
|
|
369
|
+
async function bootstrap() {
|
|
370
|
+
// Read your config file
|
|
371
|
+
const content = fs.readFileSync('./.env', 'utf-8');
|
|
372
|
+
const kmsKeyId = process.env.KMS_KEY_ID; // You extract the KMS key
|
|
373
|
+
|
|
374
|
+
// Initialize the keystore
|
|
375
|
+
const keyStore = await createSecretKeyStore(
|
|
376
|
+
{ type: 'env', content },
|
|
377
|
+
kmsKeyId, // REQUIRED
|
|
378
|
+
{
|
|
379
|
+
paths: ['DB_PASSWORD', 'API_KEY', 'JWT_SECRET'],
|
|
380
|
+
aws: { region: process.env.AWS_REGION }
|
|
381
|
+
}
|
|
382
|
+
);
|
|
383
|
+
|
|
384
|
+
// Access decrypted secrets from the keystore
|
|
385
|
+
const dbPassword = keyStore.get('DB_PASSWORD'); // → "mysecretpassword"
|
|
386
|
+
const apiKey = keyStore.get('API_KEY'); // → "sk-1234567890abcdef"
|
|
387
|
+
|
|
388
|
+
// process.env still contains encrypted values (safe!)
|
|
389
|
+
console.log(process.env.DB_PASSWORD); // → "ENC[AQICAHh...encrypted...]"
|
|
390
|
+
|
|
391
|
+
// Start your application
|
|
392
|
+
connectToDatabase({ password: dbPassword });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
bootstrap();
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
## CLI Reference
|
|
399
|
+
|
|
400
|
+
```bash
|
|
401
|
+
npx @faizahmed/secret-keystore <encrypt|decrypt> [options]
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
### Commands
|
|
405
|
+
|
|
406
|
+
| Command | Description |
|
|
407
|
+
|---------|-------------|
|
|
408
|
+
| `encrypt` | Encrypt selected values in a config file (writes `ENC[...]` in place or to `--output`) |
|
|
409
|
+
| `decrypt` | Decrypt the `ENC[...]` values in a config file (writes plaintext in place or to `--output`) |
|
|
410
|
+
| `run` | Decrypt and launch a command with secrets injected into **the child process's** environment |
|
|
411
|
+
| `rotate` | Re-encrypt a file under a new KMS Key ID (requires `--old-kms-key-id`) |
|
|
412
|
+
| `edit` | Decrypt → open in `$EDITOR` → re-encrypt on save (via a `0600` temp file, then shredded) |
|
|
413
|
+
| `init` | Scaffold a starter `.env` |
|
|
414
|
+
| `keys` | List the keys/paths in a file — **names only, never values** |
|
|
415
|
+
| `status` | Show which keys are encrypted vs plaintext — **names only, never values** |
|
|
416
|
+
| `import` | Encrypt an existing plaintext `.env` in place (migration from plain dotenv) |
|
|
417
|
+
|
|
418
|
+
All commands share the options below and auto-detect the format from the file extension.
|
|
419
|
+
|
|
420
|
+
### Options
|
|
421
|
+
|
|
422
|
+
| Option | Required | Default | Description |
|
|
423
|
+
|--------|----------|---------|-------------|
|
|
424
|
+
| `--kms-key-id=<id>` | **Yes** | — | KMS Key ID (ARN, UUID, or alias) |
|
|
425
|
+
| `--old-kms-key-id=<id>` | For `rotate` | — | The current key the file is encrypted with (rotate's source key) |
|
|
426
|
+
| `--path=<path>` | No | `./.env` | Path to the config file |
|
|
427
|
+
| `--format=<format>` | No | auto-detect | File format: `env`, `json`, `yaml` |
|
|
428
|
+
| `--keys=<keys>` | No | All keys | Comma-separated list of keys to encrypt |
|
|
429
|
+
| `--patterns=<patterns>` | No | — | Glob patterns (e.g., `**.password,**.secret`) |
|
|
430
|
+
| `--exclude=<keys>` | No | — | Keys/paths to exclude from encryption |
|
|
431
|
+
| `--region=<region>` | No | From env | AWS region |
|
|
432
|
+
| `--output=<path>` | No | Overwrite input | Output file path |
|
|
433
|
+
| `--use-credentials` | No | — | Use explicit AWS credentials instead of IAM role |
|
|
434
|
+
| `--dry-run` | No | — | Preview what would be encrypted |
|
|
435
|
+
| `--help, -h` | — | — | Show help message |
|
|
436
|
+
| `--version, -v` | — | — | Show version number |
|
|
437
|
+
|
|
438
|
+
### Authentication
|
|
439
|
+
|
|
440
|
+
By default, the CLI uses **IAM role** for AWS authentication. This is recommended for production.
|
|
441
|
+
|
|
442
|
+
To use explicit credentials (e.g., for local development):
|
|
443
|
+
|
|
444
|
+
```bash
|
|
445
|
+
# Set credentials in environment
|
|
446
|
+
export AWS_ACCESS_KEY_ID=your-access-key
|
|
447
|
+
export AWS_SECRET_ACCESS_KEY=your-secret-key
|
|
448
|
+
|
|
449
|
+
# Run with --use-credentials flag
|
|
450
|
+
npx @faizahmed/secret-keystore encrypt \
|
|
451
|
+
--kms-key-id="alias/my-key" \
|
|
452
|
+
--use-credentials
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
### Reserved Keys
|
|
456
|
+
|
|
457
|
+
These keys are **never encrypted** (required for encryption/decryption process):
|
|
458
|
+
|
|
459
|
+
- `KMS_KEY_ID`
|
|
460
|
+
- `AWS_REGION`
|
|
461
|
+
- `AWS_ACCESS_KEY_ID`
|
|
462
|
+
- `AWS_SECRET_ACCESS_KEY`
|
|
463
|
+
- `AWS_SESSION_TOKEN`
|
|
464
|
+
|
|
465
|
+
### CLI Examples
|
|
466
|
+
|
|
467
|
+
```bash
|
|
468
|
+
# Encrypt all keys in .env
|
|
469
|
+
npx @faizahmed/secret-keystore encrypt \
|
|
470
|
+
--kms-key-id="alias/my-key"
|
|
471
|
+
|
|
472
|
+
# Encrypt specific keys only
|
|
473
|
+
npx @faizahmed/secret-keystore encrypt \
|
|
474
|
+
--kms-key-id="arn:aws:kms:us-east-1:123456789:key/abc-123" \
|
|
475
|
+
--keys="DB_PASSWORD,API_KEY"
|
|
476
|
+
|
|
477
|
+
# Encrypt YAML file with patterns
|
|
478
|
+
npx @faizahmed/secret-keystore encrypt \
|
|
479
|
+
--path="./secrets.yaml" \
|
|
480
|
+
--kms-key-id="alias/my-key" \
|
|
481
|
+
--patterns="**.password,**.secret_key"
|
|
482
|
+
|
|
483
|
+
# Dry run to preview changes
|
|
484
|
+
npx @faizahmed/secret-keystore encrypt \
|
|
485
|
+
--kms-key-id="alias/my-key" \
|
|
486
|
+
--dry-run
|
|
487
|
+
|
|
488
|
+
# Output to a different file
|
|
489
|
+
npx @faizahmed/secret-keystore encrypt \
|
|
490
|
+
--path="./.env" \
|
|
491
|
+
--output="./.env.encrypted" \
|
|
492
|
+
--kms-key-id="alias/my-key"
|
|
493
|
+
|
|
494
|
+
# Decrypt all encrypted values in place
|
|
495
|
+
npx @faizahmed/secret-keystore decrypt \
|
|
496
|
+
--path="./.env" \
|
|
497
|
+
--kms-key-id="alias/my-key"
|
|
498
|
+
|
|
499
|
+
# Decrypt to a separate plaintext file
|
|
500
|
+
npx @faizahmed/secret-keystore decrypt \
|
|
501
|
+
--path="./.env.encrypted" \
|
|
502
|
+
--output="./.env" \
|
|
503
|
+
--kms-key-id="alias/my-key"
|
|
504
|
+
|
|
505
|
+
# Run your app with secrets injected into its environment (no plaintext on disk)
|
|
506
|
+
npx @faizahmed/secret-keystore run \
|
|
507
|
+
--kms-key-id="alias/my-key" -- node server.js
|
|
508
|
+
|
|
509
|
+
# Rotate a file from an old key to a new key (re-encrypts only encrypted values)
|
|
510
|
+
npx @faizahmed/secret-keystore rotate \
|
|
511
|
+
--old-kms-key-id="alias/old-key" \
|
|
512
|
+
--kms-key-id="alias/new-key"
|
|
513
|
+
|
|
514
|
+
# Edit an encrypted file in your $EDITOR (re-encrypts on save)
|
|
515
|
+
npx @faizahmed/secret-keystore edit \
|
|
516
|
+
--kms-key-id="alias/my-key" --path="./.env"
|
|
517
|
+
|
|
518
|
+
# Inspect a file without revealing any values
|
|
519
|
+
npx @faizahmed/secret-keystore status --path="./.env"
|
|
520
|
+
npx @faizahmed/secret-keystore keys --path="./.env"
|
|
521
|
+
|
|
522
|
+
# Migrate an existing plaintext .env to encrypted, in place
|
|
523
|
+
npx @faizahmed/secret-keystore import \
|
|
524
|
+
--kms-key-id="alias/my-key" --path="./.env"
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
> **`run` vs `config()`:** `run` is for apps you don't want to modify — it injects secrets into the spawned **child's** environment (so the child's `env` *can* see them; the parent never does). `config()` keeps secrets in your app's in-memory store and out of `env` entirely, but requires the app to call it. Both avoid writing plaintext to disk.
|
|
528
|
+
|
|
529
|
+
> **Tip:** For running your app, prefer `run` or the in-memory [`config()` loader](#zero-config-loader-config) over decrypting to a plaintext file on disk.
|
|
530
|
+
|
|
531
|
+
## Library API
|
|
532
|
+
|
|
533
|
+
### Single Value Operations
|
|
534
|
+
|
|
535
|
+
```javascript
|
|
536
|
+
const { encryptKMSValue, decryptKMSValue } = require('@faizahmed/secret-keystore');
|
|
537
|
+
|
|
538
|
+
// Encrypt a single value
|
|
539
|
+
const ciphertext = await encryptKMSValue(
|
|
540
|
+
'my-secret-password',
|
|
541
|
+
'arn:aws:kms:us-east-1:123456789:key/abc-123', // kmsKeyId (REQUIRED)
|
|
542
|
+
{ aws: { region: 'us-east-1' } }
|
|
543
|
+
);
|
|
544
|
+
// Returns: "ENC[AQICAHh...]"
|
|
545
|
+
|
|
546
|
+
// Decrypt a single value
|
|
547
|
+
const plaintext = await decryptKMSValue(
|
|
548
|
+
'ENC[AQICAHh...]',
|
|
549
|
+
'arn:aws:kms:us-east-1:123456789:key/abc-123',
|
|
550
|
+
{ aws: { region: 'us-east-1' } }
|
|
551
|
+
);
|
|
552
|
+
// Returns: "my-secret-password"
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Multiple Values Operations
|
|
556
|
+
|
|
557
|
+
```javascript
|
|
558
|
+
const { encryptKMSValues, decryptKMSValues } = require('@faizahmed/secret-keystore');
|
|
559
|
+
|
|
560
|
+
// Encrypt multiple values
|
|
561
|
+
const result = await encryptKMSValues(
|
|
562
|
+
{ DB_PASSWORD: 'secret123', API_KEY: 'sk-12345' },
|
|
563
|
+
'alias/my-key',
|
|
564
|
+
{ aws: { region: 'us-east-1' } }
|
|
565
|
+
);
|
|
566
|
+
// result.values = { DB_PASSWORD: 'ENC[...]', API_KEY: 'ENC[...]' }
|
|
567
|
+
// result.encrypted = ['DB_PASSWORD', 'API_KEY']
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Content-Based Operations
|
|
571
|
+
|
|
572
|
+
For working with file content (preserves comments and formatting):
|
|
573
|
+
|
|
574
|
+
```javascript
|
|
575
|
+
const {
|
|
576
|
+
encryptKMSEnvContent,
|
|
577
|
+
encryptKMSJsonContent,
|
|
578
|
+
encryptKMSYamlContent,
|
|
579
|
+
decryptKMSEnvContent,
|
|
580
|
+
decryptKMSJsonContent,
|
|
581
|
+
decryptKMSYamlContent
|
|
582
|
+
} = require('@faizahmed/secret-keystore');
|
|
583
|
+
const fs = require('node:fs');
|
|
584
|
+
|
|
585
|
+
// Encrypt ENV content
|
|
586
|
+
const envContent = fs.readFileSync('./.env', 'utf-8');
|
|
587
|
+
const kmsKeyId = process.env.KMS_KEY_ID;
|
|
588
|
+
|
|
589
|
+
const result = await encryptKMSEnvContent(envContent, kmsKeyId, {
|
|
590
|
+
paths: ['DB_PASSWORD', 'API_KEY'],
|
|
591
|
+
aws: { region: 'us-east-1' }
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
fs.writeFileSync('./.env', result.content);
|
|
595
|
+
|
|
596
|
+
// Encrypt YAML content with patterns
|
|
597
|
+
const yamlContent = fs.readFileSync('./secrets.yaml', 'utf-8');
|
|
598
|
+
const yamlResult = await encryptKMSYamlContent(yamlContent, kmsKeyId, {
|
|
599
|
+
patterns: ['**.password', '**.secret_key'],
|
|
600
|
+
preserve: { comments: true, formatting: true }
|
|
601
|
+
});
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
### Object-Based Operations
|
|
605
|
+
|
|
606
|
+
For advanced use cases where you handle parsing/serialization:
|
|
607
|
+
|
|
608
|
+
```javascript
|
|
609
|
+
const { encryptKMSObject, decryptKMSObject } = require('@faizahmed/secret-keystore');
|
|
610
|
+
const yaml = require('js-yaml');
|
|
611
|
+
const fs = require('node:fs');
|
|
612
|
+
|
|
613
|
+
// Parse YAML yourself
|
|
614
|
+
const config = yaml.load(fs.readFileSync('./secrets.yaml', 'utf-8'));
|
|
615
|
+
const kmsKeyId = config.kms.key_id;
|
|
616
|
+
|
|
617
|
+
// Encrypt object
|
|
618
|
+
const result = await encryptKMSObject(config, kmsKeyId, {
|
|
619
|
+
patterns: ['**.password', '**.secret_key'],
|
|
620
|
+
exclude: { paths: ['kms.key_id'] }
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// Serialize and write yourself
|
|
624
|
+
fs.writeFileSync('./secrets.yaml', yaml.dump(result.object));
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### Function Summary
|
|
628
|
+
|
|
629
|
+
| Function | Purpose |
|
|
630
|
+
|----------|---------|
|
|
631
|
+
| `encryptKMSValue(plaintext, kmsKeyId, options?)` | Encrypt single value using KMS |
|
|
632
|
+
| `decryptKMSValue(ciphertext, kmsKeyId, options?)` | Decrypt single value using KMS |
|
|
633
|
+
| `encryptKMSValues(values, kmsKeyId, options?)` | Encrypt flat key-value pairs using KMS |
|
|
634
|
+
| `decryptKMSValues(values, kmsKeyId, options?)` | Decrypt flat key-value pairs using KMS |
|
|
635
|
+
| `encryptKMSObject(obj, kmsKeyId, options?)` | Encrypt nested object using KMS |
|
|
636
|
+
| `decryptKMSObject(obj, kmsKeyId, options?)` | Decrypt nested object using KMS |
|
|
637
|
+
| `encryptKMSEnvContent(content, kmsKeyId, options?)` | Encrypt ENV string using KMS |
|
|
638
|
+
| `decryptKMSEnvContent(content, kmsKeyId, options?)` | Decrypt ENV string using KMS |
|
|
639
|
+
| `encryptKMSJsonContent(content, kmsKeyId, options?)` | Encrypt JSON string using KMS |
|
|
640
|
+
| `decryptKMSJsonContent(content, kmsKeyId, options?)` | Decrypt JSON string using KMS |
|
|
641
|
+
| `encryptKMSYamlContent(content, kmsKeyId, options?)` | Encrypt YAML string using KMS |
|
|
642
|
+
| `decryptKMSYamlContent(content, kmsKeyId, options?)` | Decrypt YAML string using KMS |
|
|
643
|
+
| `isJsYamlAvailable()` | Check if `js-yaml` is installed |
|
|
644
|
+
| `parseYaml(content)` | Parse YAML to object (uses js-yaml if available) |
|
|
645
|
+
| `serializeYaml(obj)` | Serialize object to YAML string |
|
|
646
|
+
| `config(options)` | Discover + cascade `.env` files, decrypt, and load into an in-memory keystore |
|
|
647
|
+
| `resolveEnvFiles(options?)` | Resolve the ordered list of `.env` files for the cascade |
|
|
648
|
+
| `rotateKMSContent(content, format, oldKeyId, newKeyId, options?)` | Re-encrypt a file's encrypted values under a new KMS Key ID |
|
|
649
|
+
|
|
650
|
+
> **Note:** `kmsKeyId` is **REQUIRED** in all functions. The library does not search content for it.
|
|
651
|
+
|
|
652
|
+
## Runtime Keystore
|
|
653
|
+
|
|
654
|
+
### Zero-Config Loader: `config()`
|
|
655
|
+
|
|
656
|
+
The fastest way to load secrets at runtime. `config()` discovers and **cascades** your `.env`
|
|
657
|
+
files, decrypts the `ENC[...]` values via KMS, and loads everything into an in-memory
|
|
658
|
+
[`SecretKeyStore`](#createsecretkeystoresource-kmskeyid-options) — in a single call.
|
|
659
|
+
|
|
660
|
+
```javascript
|
|
661
|
+
const { config } = require('@faizahmed/secret-keystore');
|
|
662
|
+
|
|
663
|
+
// Discovers .env, .env.local, .env.<NODE_ENV>, .env.<NODE_ENV>.local (later wins)
|
|
664
|
+
const secrets = await config({ kmsKeyId: 'alias/my-key' });
|
|
665
|
+
|
|
666
|
+
const dbPassword = secrets.get('DB_PASSWORD'); // decrypted, in-memory only
|
|
667
|
+
const all = secrets.getAll();
|
|
668
|
+
|
|
669
|
+
process.on('SIGTERM', () => secrets.destroy());
|
|
670
|
+
```
|
|
671
|
+
|
|
672
|
+
**Cascade order** (later files override earlier ones):
|
|
673
|
+
|
|
674
|
+
```
|
|
675
|
+
.env → .env.local → .env.<NODE_ENV> → .env.<NODE_ENV>.local
|
|
676
|
+
```
|
|
677
|
+
|
|
678
|
+
**Security by design:**
|
|
679
|
+
|
|
680
|
+
- Decrypted values live **only** in the returned keystore's memory — never written to disk, and **never** placed in `process.env`. This deliberately keeps secrets off `env`, so an attacker with code execution can't simply dump them.
|
|
681
|
+
- `kmsKeyId` is **required and explicit** — there is no environment-variable fallback.
|
|
682
|
+
- Plaintext (non-`ENC[...]`) values are passed through unchanged; only encrypted values hit KMS.
|
|
683
|
+
|
|
684
|
+
**Options:**
|
|
685
|
+
|
|
686
|
+
| Option | Default | Description |
|
|
687
|
+
|--------|---------|-------------|
|
|
688
|
+
| `kmsKeyId` | — (**required**) | KMS Key ID (ARN, UUID, or alias) |
|
|
689
|
+
| `cwd` | `process.cwd()` | Base directory for file discovery |
|
|
690
|
+
| `path` | — | Explicit file path(s); skips the cascade when set |
|
|
691
|
+
| `nodeEnv` | `process.env.NODE_ENV` | Environment name used in the cascade |
|
|
692
|
+
| `populateProcessEnv` | `false` | Opt-in: also copy decrypted values into `process.env` (**discouraged** — widens RCE blast radius; logs a warning) |
|
|
693
|
+
| `processEnv` | `process.env` | Target object when `populateProcessEnv` is enabled |
|
|
694
|
+
|
|
695
|
+
`config()` also accepts all [keystore options](#layered-options-structure) (TTL, `autoRefresh`, `security`, `aws`, etc.), which are forwarded to the underlying keystore.
|
|
696
|
+
|
|
697
|
+
> **Migrating from `dotenv`?** `dotenv` populates `process.env`; `config()` deliberately does not. Read secrets from the returned store (`secrets.get('KEY')`) instead. If you must have `process.env` behavior, set `populateProcessEnv: true` and accept the larger blast radius.
|
|
698
|
+
|
|
699
|
+
### `createSecretKeyStore(source, kmsKeyId, options?)`
|
|
700
|
+
|
|
701
|
+
Creates and initializes a secure in-memory keystore with decrypted secrets. Use this directly when you already have the file content/object in hand; use [`config()`](#zero-config-loader-config) when you want automatic file discovery and cascading.
|
|
702
|
+
|
|
703
|
+
```javascript
|
|
704
|
+
const { createSecretKeyStore } = require('@faizahmed/secret-keystore');
|
|
705
|
+
const fs = require('node:fs');
|
|
706
|
+
|
|
707
|
+
const content = fs.readFileSync('./.env', 'utf-8');
|
|
708
|
+
const kmsKeyId = process.env.KMS_KEY_ID;
|
|
709
|
+
|
|
710
|
+
const keyStore = await createSecretKeyStore(
|
|
711
|
+
{ type: 'env', content },
|
|
712
|
+
kmsKeyId,
|
|
713
|
+
{
|
|
714
|
+
paths: ['DB_PASSWORD', 'API_KEY', 'JWT_SECRET'],
|
|
715
|
+
aws: { region: 'us-east-1' },
|
|
716
|
+
security: { inMemoryEncryption: true },
|
|
717
|
+
access: { ttl: 3600000, autoRefresh: true }
|
|
718
|
+
}
|
|
719
|
+
);
|
|
720
|
+
|
|
721
|
+
// Use secrets
|
|
722
|
+
const dbPassword = keyStore.get('DB_PASSWORD');
|
|
723
|
+
|
|
724
|
+
// Cleanup on shutdown
|
|
725
|
+
process.on('SIGTERM', () => keyStore.destroy());
|
|
726
|
+
```
|
|
727
|
+
|
|
728
|
+
### Source Types
|
|
729
|
+
|
|
730
|
+
```javascript
|
|
731
|
+
// ENV file content
|
|
732
|
+
{ type: 'env', content: 'KEY=value\nKEY2=value2' }
|
|
733
|
+
|
|
734
|
+
// JSON content
|
|
735
|
+
{ type: 'json', content: '{"key": "value"}' }
|
|
736
|
+
|
|
737
|
+
// YAML content
|
|
738
|
+
{ type: 'yaml', content: 'key: value' }
|
|
739
|
+
|
|
740
|
+
// Pre-parsed object
|
|
741
|
+
{ type: 'object', object: { key: 'value' } }
|
|
742
|
+
|
|
743
|
+
// Flat key-value pairs
|
|
744
|
+
{ type: 'values', values: { KEY: 'value' } }
|
|
745
|
+
```
|
|
746
|
+
|
|
747
|
+
### KeyStore Methods
|
|
748
|
+
|
|
749
|
+
| Method | Returns | Description |
|
|
750
|
+
|--------|---------|-------------|
|
|
751
|
+
| `get(key)` | `string \| undefined` | Get a decrypted secret |
|
|
752
|
+
| `getSection(path)` | `object \| undefined` | Get a nested section |
|
|
753
|
+
| `getAll()` | `Record<string, string>` | Get all decrypted secrets |
|
|
754
|
+
| `has(key)` | `boolean` | Check if a key exists |
|
|
755
|
+
| `keys()` | `string[]` | Get all available key names |
|
|
756
|
+
| `isInitialized()` | `boolean` | Check if keystore is ready |
|
|
757
|
+
| `getMetadata()` | `object` | Get keystore metadata |
|
|
758
|
+
| `getAccessStats(key)` | `object \| null` | Get access statistics for a key |
|
|
759
|
+
| `refresh()` | `Promise<void>` | Re-decrypt all secrets |
|
|
760
|
+
| `clear()` | `void` | Clear all secrets from memory |
|
|
761
|
+
| `clearKey(key)` | `void` | Clear a specific key |
|
|
762
|
+
| `destroy()` | `void` | Destroy keystore and wipe memory |
|
|
763
|
+
|
|
764
|
+
### TTL and Auto-Refresh
|
|
765
|
+
|
|
766
|
+
| `ttl` | `autoRefresh` | Behavior |
|
|
767
|
+
|-------|---------------|----------|
|
|
768
|
+
| `null` | — | Secrets never expire |
|
|
769
|
+
| `3600000` | `true` | Auto re-decrypt on next `get()` after expiry |
|
|
770
|
+
| `3600000` | `false` | Throw error, user calls `refresh()` manually |
|
|
771
|
+
|
|
772
|
+
## Configuration Options
|
|
773
|
+
|
|
774
|
+
### Layered Options Structure
|
|
775
|
+
|
|
776
|
+
```javascript
|
|
777
|
+
{
|
|
778
|
+
// AWS Configuration
|
|
779
|
+
aws: {
|
|
780
|
+
credentials: {
|
|
781
|
+
accessKeyId: string,
|
|
782
|
+
secretAccessKey: string,
|
|
783
|
+
sessionToken?: string
|
|
784
|
+
},
|
|
785
|
+
region: string
|
|
786
|
+
},
|
|
787
|
+
|
|
788
|
+
// Attestation (Nitro Enclaves) - Full lifecycle managed internally
|
|
789
|
+
attestation: {
|
|
790
|
+
enabled: boolean, // Default: false
|
|
791
|
+
required: boolean, // Default: false
|
|
792
|
+
fallbackToStandard: boolean, // Default: true
|
|
793
|
+
endpoint: string, // Attestation endpoint URL (e.g., Anjuna)
|
|
794
|
+
timeout: number, // Request timeout (ms), Default: 10000
|
|
795
|
+
userData: string // Optional user data for attestation
|
|
796
|
+
},
|
|
797
|
+
|
|
798
|
+
// Path Selection
|
|
799
|
+
paths: string[], // Explicit paths
|
|
800
|
+
patterns: string[], // Glob patterns (** only)
|
|
801
|
+
exclude: {
|
|
802
|
+
paths: string[],
|
|
803
|
+
patterns: string[]
|
|
804
|
+
},
|
|
805
|
+
|
|
806
|
+
// Content Preservation
|
|
807
|
+
preserve: {
|
|
808
|
+
comments: boolean, // Default: true
|
|
809
|
+
formatting: boolean, // Default: true
|
|
810
|
+
anchors: boolean // Default: true (YAML)
|
|
811
|
+
},
|
|
812
|
+
|
|
813
|
+
// Keystore-specific
|
|
814
|
+
security: {
|
|
815
|
+
inMemoryEncryption: boolean, // Default: true
|
|
816
|
+
secureWipe: boolean // Default: true
|
|
817
|
+
},
|
|
818
|
+
access: {
|
|
819
|
+
ttl: number | null, // Secret expiry (ms)
|
|
820
|
+
autoRefresh: boolean, // Default: true
|
|
821
|
+
accessLimit: number, // Max access count
|
|
822
|
+
clearOnAccess: boolean // Default: false
|
|
823
|
+
},
|
|
824
|
+
validation: {
|
|
825
|
+
noProcessEnvLeak: boolean, // Default: true
|
|
826
|
+
throwOnMissingKey: boolean // Default: false
|
|
827
|
+
},
|
|
828
|
+
|
|
829
|
+
// Logging
|
|
830
|
+
logger: Logger,
|
|
831
|
+
logLevel: 'debug' | 'info' | 'warn' | 'error' | 'silent'
|
|
832
|
+
}
|
|
833
|
+
```
|
|
834
|
+
|
|
835
|
+
## How It Works
|
|
836
|
+
|
|
837
|
+
### KMS Key Types: Symmetric vs Asymmetric
|
|
838
|
+
|
|
839
|
+
The library detects the key type via AWS KMS `DescribeKey` and chooses the right encryption method:
|
|
840
|
+
|
|
841
|
+
| Key type | Encryption method | Plaintext size limit |
|
|
842
|
+
|----------|-------------------|----------------------|
|
|
843
|
+
| **Symmetric** (default CMK) | Direct KMS Encrypt/Decrypt | 4 KB per value |
|
|
844
|
+
| **Asymmetric (RSA)** | Envelope encryption | No limit |
|
|
845
|
+
|
|
846
|
+
**Envelope encryption (RSA only):** For RSA keys, plaintext is too large for direct RSA encryption (e.g. ~190 bytes for RSA_2048). The library generates a random AES-256 data key (DEK), encrypts your secret with the DEK (AES-256-GCM), and encrypts only the DEK with KMS. The stored value is `ENC[base64(envelope)]` where the envelope contains the KMS-encrypted DEK plus IV, ciphertext, and auth tag. Decryption: KMS decrypts the DEK, then the library decrypts the payload with the DEK. Existing values encrypted with symmetric keys or with direct RSA (small payloads) remain valid; new encrypts with RSA use envelope format automatically.
|
|
847
|
+
|
|
848
|
+
### Build-Time: Encrypting Secrets
|
|
849
|
+
|
|
850
|
+
```mermaid
|
|
851
|
+
sequenceDiagram
|
|
852
|
+
participant Dev as Developer
|
|
853
|
+
participant CLI as CLI
|
|
854
|
+
participant KMS as AWS KMS
|
|
855
|
+
participant File as Config File
|
|
856
|
+
|
|
857
|
+
Dev->>CLI: npx encrypt --kms-key-id="..." --keys="DB_PASS"
|
|
858
|
+
CLI->>File: Read config file
|
|
859
|
+
File-->>CLI: DB_PASS=mysecretpassword
|
|
860
|
+
CLI->>KMS: Encrypt("mysecretpassword")
|
|
861
|
+
KMS-->>CLI: AQICAHh...encrypted...
|
|
862
|
+
CLI->>File: Write ENC[AQICAHh...]
|
|
863
|
+
CLI-->>Dev: ✅ Encrypted 1 key
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
### Runtime: Decrypting Secrets
|
|
867
|
+
|
|
868
|
+
```mermaid
|
|
869
|
+
sequenceDiagram
|
|
870
|
+
participant App as Your App
|
|
871
|
+
participant KS as KeyStore
|
|
872
|
+
participant KMS as AWS KMS
|
|
873
|
+
participant Mem as Secure Memory
|
|
874
|
+
|
|
875
|
+
App->>KS: createSecretKeyStore(source, kmsKeyId)
|
|
876
|
+
KS->>KS: Parse content
|
|
877
|
+
KS->>KMS: Decrypt(ENC[AQICAHh...])
|
|
878
|
+
KMS-->>KS: mysecretpassword
|
|
879
|
+
KS->>Mem: Store with AES-256-GCM
|
|
880
|
+
KS-->>App: KeyStore ready
|
|
881
|
+
|
|
882
|
+
App->>KS: keyStore.get('DB_PASS')
|
|
883
|
+
KS->>Mem: Retrieve & decrypt
|
|
884
|
+
Mem-->>KS: mysecretpassword
|
|
885
|
+
KS-->>App: "mysecretpassword"
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
### Key Points
|
|
889
|
+
|
|
890
|
+
| Stage | `process.env.DB_PASS` | `keyStore.get('DB_PASS')` |
|
|
891
|
+
|-------|----------------------|---------------------------|
|
|
892
|
+
| Before encryption | `mysecretpassword` | — |
|
|
893
|
+
| After encryption | `ENC[AQICAHh...]` | — |
|
|
894
|
+
| At runtime (after init) | `ENC[AQICAHh...]` ✓ | `mysecretpassword` ✓ |
|
|
895
|
+
|
|
896
|
+
> **Security:** Decrypted values are **never** stored in `process.env`. They exist only in the keystore's secure memory with additional AES-256-GCM encryption.
|
|
897
|
+
|
|
898
|
+
## Examples
|
|
899
|
+
|
|
900
|
+
Complete working sample applications are available in the `examples/` directory:
|
|
901
|
+
|
|
902
|
+
| Framework | Path | Description |
|
|
903
|
+
|-----------|------|-------------|
|
|
904
|
+
| **NestJS** | [`examples/nestjs/`](./examples/nestjs/) | Full NestJS app with global KeyStoreModule |
|
|
905
|
+
| **Next.js** | [`examples/nextjs/`](./examples/nextjs/) | Next.js 14 App Router with Server Components |
|
|
906
|
+
|
|
907
|
+
### Running Examples Locally
|
|
908
|
+
|
|
909
|
+
```bash
|
|
910
|
+
# 1. Install the main package dependencies
|
|
911
|
+
cd secret-keystore
|
|
912
|
+
pnpm install
|
|
913
|
+
|
|
914
|
+
# 2. Install example dependencies
|
|
915
|
+
cd examples/nestjs # or examples/nextjs
|
|
916
|
+
pnpm install
|
|
917
|
+
|
|
918
|
+
# 3. Configure environment
|
|
919
|
+
cp .env.example .env # or .env.local for Next.js
|
|
920
|
+
|
|
921
|
+
# 4. Update KMS_KEY_ID in .env with your actual KMS key
|
|
922
|
+
|
|
923
|
+
# 5. Encrypt secrets
|
|
924
|
+
pnpm run encrypt:keys
|
|
925
|
+
|
|
926
|
+
# 6. Run the app
|
|
927
|
+
pnpm run start:dev # NestJS
|
|
928
|
+
pnpm run dev # Next.js
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
See each example's README for detailed setup instructions.
|
|
932
|
+
|
|
933
|
+
## Nitro Enclave Attestation
|
|
934
|
+
|
|
935
|
+
For maximum security in AWS Nitro Enclaves, the library provides **full attestation lifecycle management**.
|
|
936
|
+
|
|
937
|
+
### How It Works
|
|
938
|
+
|
|
939
|
+
When attestation is enabled:
|
|
940
|
+
|
|
941
|
+
1. **Key Pair Generation** — Library generates ephemeral RSA-4096 key pair
|
|
942
|
+
2. **Document Fetch** — Fetches attestation document from Anjuna/Nitro endpoint (includes public key)
|
|
943
|
+
3. **Attested Decrypt** — Sends KMS Decrypt with `Recipient` parameter containing attestation document
|
|
944
|
+
4. **CMS Unwrap** — KMS returns `CiphertextForRecipient` (CMS EnvelopedData), library unwraps with private key
|
|
945
|
+
5. **Auto-Refresh** — If document expires (5-min AWS limit), library automatically regenerates and retries
|
|
946
|
+
|
|
947
|
+
```mermaid
|
|
948
|
+
sequenceDiagram
|
|
949
|
+
participant App as Your App
|
|
950
|
+
participant AM as AttestationManager
|
|
951
|
+
participant Anjuna as Nitro/Anjuna
|
|
952
|
+
participant KMS as AWS KMS
|
|
953
|
+
|
|
954
|
+
App->>AM: decryptWithAttestation()
|
|
955
|
+
AM->>AM: Generate RSA-4096 key pair
|
|
956
|
+
AM->>Anjuna: Request attestation doc (with public key)
|
|
957
|
+
Anjuna-->>AM: Attestation document
|
|
958
|
+
AM->>KMS: Decrypt + Recipient{AttestationDoc}
|
|
959
|
+
KMS-->>AM: CiphertextForRecipient (CMS EnvelopedData)
|
|
960
|
+
AM->>AM: Unwrap CMS with private key (PKIjs)
|
|
961
|
+
AM-->>App: Decrypted plaintext
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
### Enabling Attestation
|
|
965
|
+
|
|
966
|
+
```javascript
|
|
967
|
+
const { createSecretKeyStore } = require('@faizahmed/secret-keystore');
|
|
968
|
+
|
|
969
|
+
const keyStore = await createSecretKeyStore(
|
|
970
|
+
{ type: 'env', content },
|
|
971
|
+
kmsKeyId,
|
|
972
|
+
{
|
|
973
|
+
attestation: {
|
|
974
|
+
enabled: true,
|
|
975
|
+
required: true, // Fail if attestation unavailable
|
|
976
|
+
endpoint: 'http://localhost:8080/attestation', // Anjuna/Nitro endpoint
|
|
977
|
+
timeout: 10000,
|
|
978
|
+
userData: 'optional-user-data'
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
);
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
### Using AttestationManager Directly
|
|
985
|
+
|
|
986
|
+
For advanced use cases, you can use the `AttestationManager` directly:
|
|
987
|
+
|
|
988
|
+
```javascript
|
|
989
|
+
const { createAttestationManager } = require('@faizahmed/secret-keystore');
|
|
990
|
+
const { KMSClient } = require('@aws-sdk/client-kms');
|
|
991
|
+
|
|
992
|
+
// Create and initialize the manager
|
|
993
|
+
const attestationManager = await createAttestationManager({
|
|
994
|
+
endpoint: 'http://localhost:8080/attestation',
|
|
995
|
+
timeout: 10000,
|
|
996
|
+
logger: console
|
|
997
|
+
});
|
|
998
|
+
|
|
999
|
+
// Use for KMS decrypt with attestation
|
|
1000
|
+
const kmsClient = new KMSClient({ region: 'us-east-1' });
|
|
1001
|
+
const plaintext = await attestationManager.decryptWithAttestation(
|
|
1002
|
+
kmsClient,
|
|
1003
|
+
ciphertextBlob,
|
|
1004
|
+
kmsKeyId,
|
|
1005
|
+
{ encryptionContext: { ... } }
|
|
1006
|
+
);
|
|
1007
|
+
|
|
1008
|
+
// Check status
|
|
1009
|
+
console.log(attestationManager.getStatus());
|
|
1010
|
+
// { initialized: true, hasDocument: true, documentAge: 45000, ... }
|
|
1011
|
+
|
|
1012
|
+
// Cleanup
|
|
1013
|
+
attestationManager.destroy();
|
|
1014
|
+
```
|
|
1015
|
+
|
|
1016
|
+
### 5-Minute Auto-Refresh
|
|
1017
|
+
|
|
1018
|
+
AWS KMS requires attestation documents to be less than 5 minutes old. The library handles this automatically:
|
|
1019
|
+
|
|
1020
|
+
| Scenario | Library Behavior |
|
|
1021
|
+
|----------|------------------|
|
|
1022
|
+
| First request | Initialize (generate key pair, fetch doc) |
|
|
1023
|
+
| Document < 5 min old | Use cached document |
|
|
1024
|
+
| Document expired (KMS rejects) | Regenerate key pair, fetch new doc, retry once |
|
|
1025
|
+
| Anjuna/Nitro unavailable | Throw `ATTESTATION_FETCH_FAILED` error |
|
|
1026
|
+
|
|
1027
|
+
> **Note:** Attestation is only available inside AWS Nitro Enclaves. Outside enclaves, enable `fallbackToStandard: true` to use standard KMS decrypt.
|
|
1028
|
+
|
|
1029
|
+
## Error Handling
|
|
1030
|
+
|
|
1031
|
+
The library provides a comprehensive error hierarchy:
|
|
1032
|
+
|
|
1033
|
+
```javascript
|
|
1034
|
+
const {
|
|
1035
|
+
createSecretKeyStore,
|
|
1036
|
+
SecretKeyStoreError, // Base error class
|
|
1037
|
+
KmsError, // AWS KMS errors
|
|
1038
|
+
AttestationError, // Attestation failures
|
|
1039
|
+
ContentError, // Content parsing errors
|
|
1040
|
+
PathError, // Path resolution errors
|
|
1041
|
+
EncryptionError, // Encryption failures
|
|
1042
|
+
DecryptionError, // Decryption failures
|
|
1043
|
+
KeystoreError, // Keystore operation errors
|
|
1044
|
+
ValidationError // Validation failures
|
|
1045
|
+
} = require('@faizahmed/secret-keystore');
|
|
1046
|
+
|
|
1047
|
+
try {
|
|
1048
|
+
const keyStore = await createSecretKeyStore(source, kmsKeyId, options);
|
|
1049
|
+
const secret = keyStore.get('DB_PASSWORD');
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
if (error instanceof KmsError) {
|
|
1052
|
+
console.error('KMS Error:', error.code, error.message);
|
|
1053
|
+
} else if (error instanceof KeystoreError) {
|
|
1054
|
+
console.error('Keystore Error:', error.code, error.message);
|
|
1055
|
+
} else if (error instanceof AttestationError) {
|
|
1056
|
+
console.error('Attestation Error:', error.code, error.message);
|
|
1057
|
+
}
|
|
1058
|
+
}
|
|
1059
|
+
```
|
|
1060
|
+
|
|
1061
|
+
### Error Codes
|
|
1062
|
+
|
|
1063
|
+
| Category | Codes |
|
|
1064
|
+
|----------|-------|
|
|
1065
|
+
| **KMS** | `KMS_KEY_NOT_FOUND`, `KMS_KEY_DISABLED`, `KMS_ACCESS_DENIED`, `KMS_INVALID_CIPHERTEXT`, `KMS_THROTTLED` |
|
|
1066
|
+
| **Attestation** | `ATTESTATION_INIT_FAILED`, `ATTESTATION_DOCUMENT_EXPIRED`, `ATTESTATION_KEY_PAIR_FAILED`, `ATTESTATION_FETCH_FAILED`, `ATTESTATION_CMS_UNWRAP_FAILED` |
|
|
1067
|
+
| **Keystore** | `KEYSTORE_NOT_INITIALIZED`, `KEYSTORE_DESTROYED`, `SECRET_NOT_FOUND`, `SECRET_EXPIRED` |
|
|
1068
|
+
|
|
1069
|
+
## TypeScript Support
|
|
1070
|
+
|
|
1071
|
+
The package includes TypeScript definitions:
|
|
1072
|
+
|
|
1073
|
+
```typescript
|
|
1074
|
+
import * as fs from 'node:fs';
|
|
1075
|
+
import {
|
|
1076
|
+
createSecretKeyStore,
|
|
1077
|
+
SecretKeyStore,
|
|
1078
|
+
KeystoreSource,
|
|
1079
|
+
KeystoreOptions,
|
|
1080
|
+
encryptKMSEnvContent,
|
|
1081
|
+
encryptKMSYamlContent,
|
|
1082
|
+
ContentResult,
|
|
1083
|
+
// Attestation exports
|
|
1084
|
+
AttestationManager,
|
|
1085
|
+
createAttestationManager
|
|
1086
|
+
} from '@faizahmed/secret-keystore';
|
|
1087
|
+
|
|
1088
|
+
const source: KeystoreSource = {
|
|
1089
|
+
type: 'env',
|
|
1090
|
+
content: fs.readFileSync('./.env', 'utf-8')
|
|
1091
|
+
};
|
|
1092
|
+
|
|
1093
|
+
const options: KeystoreOptions = {
|
|
1094
|
+
paths: ['DB_PASSWORD', 'API_KEY'],
|
|
1095
|
+
security: { inMemoryEncryption: true },
|
|
1096
|
+
access: { ttl: 3600000, autoRefresh: true }
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
const keyStore: SecretKeyStore = await createSecretKeyStore(
|
|
1100
|
+
source,
|
|
1101
|
+
kmsKeyId,
|
|
1102
|
+
options
|
|
1103
|
+
);
|
|
1104
|
+
|
|
1105
|
+
const password: string | undefined = keyStore.get('DB_PASSWORD');
|
|
1106
|
+
```
|
|
1107
|
+
|
|
1108
|
+
## Troubleshooting
|
|
1109
|
+
|
|
1110
|
+
### "kms-key-id is REQUIRED"
|
|
1111
|
+
|
|
1112
|
+
The `--kms-key-id` option is required for all CLI operations. Provide your KMS key:
|
|
1113
|
+
|
|
1114
|
+
```bash
|
|
1115
|
+
npx @faizahmed/secret-keystore encrypt --kms-key-id="your-kms-key-id"
|
|
1116
|
+
```
|
|
1117
|
+
|
|
1118
|
+
### "Could not load credentials"
|
|
1119
|
+
|
|
1120
|
+
The package uses IAM roles by default. If you're seeing this error:
|
|
1121
|
+
|
|
1122
|
+
**In production (EC2/ECS/EKS/Lambda):**
|
|
1123
|
+
- Ensure your instance/task/pod has an IAM role attached
|
|
1124
|
+
- Verify the IAM role has `kms:Encrypt` and `kms:Decrypt` permissions
|
|
1125
|
+
|
|
1126
|
+
**In local development:**
|
|
1127
|
+
- Use `--use-credentials` flag with the CLI
|
|
1128
|
+
- Set `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables
|
|
1129
|
+
|
|
1130
|
+
### "AccessDeniedException"
|
|
1131
|
+
|
|
1132
|
+
Your AWS credentials don't have permission to use the KMS key:
|
|
1133
|
+
|
|
1134
|
+
```json
|
|
1135
|
+
{
|
|
1136
|
+
"Effect": "Allow",
|
|
1137
|
+
"Action": ["kms:Encrypt", "kms:Decrypt", "kms:DescribeKey"],
|
|
1138
|
+
"Resource": "arn:aws:kms:REGION:ACCOUNT:key/KEY-ID"
|
|
1139
|
+
}
|
|
1140
|
+
```
|
|
1141
|
+
|
|
1142
|
+
### Already encrypted values being skipped
|
|
1143
|
+
|
|
1144
|
+
This is expected behavior. The library automatically detects and skips values that are already encrypted (prefixed with `ENC[`) to prevent double-encryption.
|
|
1145
|
+
|
|
1146
|
+
## Development
|
|
1147
|
+
|
|
1148
|
+
This project uses [pnpm](https://pnpm.io) and the built-in Node test runner — no heavyweight test framework, zero runtime test dependencies.
|
|
1149
|
+
|
|
1150
|
+
```bash
|
|
1151
|
+
# Install dev dependencies
|
|
1152
|
+
pnpm install
|
|
1153
|
+
|
|
1154
|
+
# Run the test suite (node:test, fully offline — KMS is mocked)
|
|
1155
|
+
pnpm test
|
|
1156
|
+
|
|
1157
|
+
# Tests with coverage (enforces a minimum threshold)
|
|
1158
|
+
pnpm test:coverage
|
|
1159
|
+
|
|
1160
|
+
# Lint, format check, and type-definition check
|
|
1161
|
+
pnpm lint
|
|
1162
|
+
pnpm format:check
|
|
1163
|
+
pnpm typecheck
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
**Quality gates.** Every push and pull request runs CI across **Node 18, 20, and 22**:
|
|
1167
|
+
|
|
1168
|
+
| Gate | Tool | What it checks |
|
|
1169
|
+
|------|------|----------------|
|
|
1170
|
+
| Tests | `node:test` + [`aws-sdk-client-mock`](https://github.com/m-radzikowski/aws-sdk-client-mock) | 95 tests covering both symmetric and RSA-envelope KMS paths, content/object/keystore/CLI/`config()`/rotation |
|
|
1171
|
+
| Coverage | `c8` | Minimum coverage thresholds enforced |
|
|
1172
|
+
| Lint | ESLint | Code correctness |
|
|
1173
|
+
| Format | Prettier | Consistent style |
|
|
1174
|
+
| Types | `tsc --strict` | `index.d.ts` compiles and stays in sync with runtime error codes |
|
|
1175
|
+
|
|
1176
|
+
All tests run without an AWS account or network access — KMS is mocked with a reversible stand-in that exercises both the direct-encrypt (symmetric) and envelope (RSA) code paths.
|
|
1177
|
+
|
|
1178
|
+
## Security
|
|
1179
|
+
|
|
1180
|
+
This package provides multiple layers of protection:
|
|
1181
|
+
|
|
1182
|
+
| Layer | Protection |
|
|
1183
|
+
|-------|------------|
|
|
1184
|
+
| **IAM Role Default** | Uses IAM roles by default — no credentials to manage |
|
|
1185
|
+
| **Encryption at Rest** | Secrets in config files are KMS-encrypted ciphertext |
|
|
1186
|
+
| **Access Control** | IAM policies + KMS key policies control decryption |
|
|
1187
|
+
| **Runtime Isolation** | Decrypted values never in `process.env` |
|
|
1188
|
+
| **Memory Protection** | Additional AES-256-GCM encryption in keystore memory |
|
|
1189
|
+
| **Full Attestation** | Complete Nitro Enclave attestation lifecycle with auto-refresh |
|
|
1190
|
+
| **Security-First Dependencies** | Minimal third-party dependencies for security operations |
|
|
1191
|
+
|
|
1192
|
+
### Attestation Highlights
|
|
1193
|
+
|
|
1194
|
+
- **Fully Managed** — Library handles ephemeral key pairs, document fetching, and CMS unwrapping
|
|
1195
|
+
- **Auto-Refresh** — Automatically regenerates attestation materials on 5-minute expiry
|
|
1196
|
+
- **CMS Support** — Unwraps KMS `CiphertextForRecipient` using PKIjs/asn1js
|
|
1197
|
+
- **Zero Config** — Just enable attestation and point to your Anjuna/Nitro endpoint
|
|
1198
|
+
|
|
1199
|
+
📖 **[Read the full Security documentation →](./SECURITY.md)**
|
|
1200
|
+
|
|
1201
|
+
## License
|
|
1202
|
+
|
|
1203
|
+
MIT — see [LICENSE](LICENSE).
|