envlope 0.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/CHANGELOG.md +32 -0
- package/LICENSE +21 -0
- package/README.md +735 -0
- package/dist/cli.cjs +702 -0
- package/dist/cli.js +675 -0
- package/package.json +62 -0
package/README.md
ADDED
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
# envlope
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/envlope)
|
|
4
|
+
[](https://www.npmjs.com/package/envlope)
|
|
5
|
+
[](./LICENSE)
|
|
6
|
+
[](https://nodejs.org)
|
|
7
|
+
|
|
8
|
+
> Encrypt your `.env` files with a key, push them to git safely, unlock with the same key.
|
|
9
|
+
|
|
10
|
+
No server. No accounts. No telemetry. Just a CLI that turns your `.env` into an AES-256-GCM ciphertext blob safe to commit alongside your code — and back again, with a key only your team knows.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## TL;DR
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npx envlope init
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
That's it. The command encrypts `.env` → `.env.encrypted`, adds `.env` to `.gitignore`, and prints a key. Save the key. Commit the encrypted file. A teammate runs `npx envlope decrypt`, pastes the key, and they have your `.env`.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Table of contents
|
|
25
|
+
|
|
26
|
+
- [Why envlope](#why-envlope)
|
|
27
|
+
- [How envlope compares](#how-envlope-compares)
|
|
28
|
+
- [Install](#install)
|
|
29
|
+
- [Quick start](#quick-start)
|
|
30
|
+
- [Commands](#commands)
|
|
31
|
+
- [`envlope init`](#envlope-init)
|
|
32
|
+
- [`envlope encrypt`](#envlope-encrypt)
|
|
33
|
+
- [`envlope decrypt`](#envlope-decrypt)
|
|
34
|
+
- [`envlope status`](#envlope-status)
|
|
35
|
+
- [`envlope view`](#envlope-view)
|
|
36
|
+
- [Common workflows](#common-workflows)
|
|
37
|
+
- [Sharing one key across multiple repos](#sharing-one-key-across-multiple-repos)
|
|
38
|
+
- [Multiple env files (dev/staging/prod)](#multiple-env-files-devstagingprod)
|
|
39
|
+
- [Using `ENVLOPE_KEY` for CI/scripts](#using-envlope_key-for-ciscripts)
|
|
40
|
+
- [`--json` for automation](#--json-for-automation)
|
|
41
|
+
- [Updating `.env` values](#updating-env-values)
|
|
42
|
+
- [Rotating the key](#rotating-the-key)
|
|
43
|
+
- [Recovering from a lost key](#recovering-from-a-lost-key)
|
|
44
|
+
- [Security model](#security-model)
|
|
45
|
+
- [Troubleshooting](#troubleshooting)
|
|
46
|
+
- [How it works (technical)](#how-it-works-technical)
|
|
47
|
+
- [Development](#development)
|
|
48
|
+
- [Changelog](#changelog)
|
|
49
|
+
- [License](#license)
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Why envlope
|
|
54
|
+
|
|
55
|
+
- **Zero infrastructure.** No vault, no service to pay for, no account to set up. The key lives wherever you put it.
|
|
56
|
+
- **Git-native.** The encrypted file is a single line of text. Diff-friendly, branch-friendly, PR-friendly.
|
|
57
|
+
- **Modern crypto.** AES-256-GCM with a fresh random IV per encryption. The ciphertext is computationally indistinguishable from random.
|
|
58
|
+
- **Designed for teams.** One shared key unlocks every repo your team uses. New teammate? Share one key, they're in.
|
|
59
|
+
- **CI-friendly.** `ENVLOPE_KEY` env var, `--json` output mode, and `--strict` exit codes mean it slots into pipelines cleanly.
|
|
60
|
+
- **Trivially auditable.** ~400 lines of TypeScript, four small dependencies, all of which you've seen before.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## How envlope compares
|
|
65
|
+
|
|
66
|
+
| Feature | **envlope** | dotenvx | sops | git-crypt |
|
|
67
|
+
| -------------------------------- | ------------------ | ------------------ | ------------------ | ------------------ |
|
|
68
|
+
| Setup time | seconds | ~1 min | ~5 min | ~10 min |
|
|
69
|
+
| Server required | No | No | No | No |
|
|
70
|
+
| Symmetric key | ✓ | ✓ | ✓ | ✓ |
|
|
71
|
+
| Asymmetric / multi-recipient | ✗ | ✓ | ✓ | ✓ |
|
|
72
|
+
| Multi-file (`.env.staging` etc.) | ✓ | ✓ | ✓ | ✓ |
|
|
73
|
+
| CI integration | env var, `--json` | ✓ | ✓ | ✗ |
|
|
74
|
+
| Built-in status / drift checks | ✓ | ✗ | ✗ | ✗ |
|
|
75
|
+
| Built-in update notifier | ✓ | ✗ | ✗ | ✗ |
|
|
76
|
+
| Lines of code (rough) | ~400 | ~10k | ~50k | ~5k |
|
|
77
|
+
| Dependency footprint | 4 deps | 30+ | huge | none (Go binary) |
|
|
78
|
+
|
|
79
|
+
**Pick envlope when:** you want the minimum-viable encrypted-env workflow, you trust one shared key, your team is small-to-medium, and you value being able to audit the whole tool in 20 minutes.
|
|
80
|
+
|
|
81
|
+
**Pick something else when:** you need per-teammate keys (sops, dotenvx), enterprise compliance with audit logs (sops), or transparent file-level encryption inside git itself (git-crypt).
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
## Install
|
|
86
|
+
|
|
87
|
+
You have three options, ordered from least to most setup:
|
|
88
|
+
|
|
89
|
+
### 1. No install — use `npx` (recommended for occasional use)
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npx envlope <command>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
`npx` fetches envlope from npm the first time and caches it locally. Subsequent runs are instant. No global state, no `node_modules` clutter.
|
|
96
|
+
|
|
97
|
+
### 2. Global install — bare `envlope` command everywhere
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npm install -g envlope
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Now you can run `envlope <command>` in any directory without the `npx` prefix.
|
|
104
|
+
|
|
105
|
+
### 3. Project dev dependency
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
npm install --save-dev envlope
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Use inside the project via `npx envlope` or add it to your `package.json` scripts:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"scripts": {
|
|
116
|
+
"env:status": "envlope status",
|
|
117
|
+
"env:encrypt": "envlope encrypt",
|
|
118
|
+
"env:decrypt": "envlope decrypt"
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Quick start
|
|
126
|
+
|
|
127
|
+
### Step 1 — encrypt your `.env`
|
|
128
|
+
|
|
129
|
+
In a project with a `.env` file you want to share with your team:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
npx envlope init
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
Output:
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
✓ Encrypted .env → .env.encrypted
|
|
139
|
+
✓ Created .gitignore (added .env, .env.local, .env.encrypted.bak)
|
|
140
|
+
|
|
141
|
+
SAVE THIS KEY — it cannot be recovered:
|
|
142
|
+
────────────────────────────────────────────────────────────────
|
|
143
|
+
envlope_key_IiE85vhkdQLrNxksL9JWrWYOPHW35+gmJX366q8bapU=
|
|
144
|
+
────────────────────────────────────────────────────────────────
|
|
145
|
+
Save it in your password manager and share with teammates over a secure channel.
|
|
146
|
+
|
|
147
|
+
Next:
|
|
148
|
+
$ git add .env.encrypted .gitignore
|
|
149
|
+
$ git commit -m "Add encrypted env"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Step 2 — commit and push the encrypted file
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
git add .env.encrypted .gitignore
|
|
156
|
+
git commit -m "Add encrypted env"
|
|
157
|
+
git push
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Your plaintext `.env` stays on your machine (it's gitignored automatically). Only `.env.encrypted` goes to the remote.
|
|
161
|
+
|
|
162
|
+
### Step 3 — your teammate clones and decrypts
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
git clone <your-repo>
|
|
166
|
+
cd <your-repo>
|
|
167
|
+
npx envlope decrypt
|
|
168
|
+
# Enter your envlope key: ****************************************
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
They paste the key (you sent it via 1Password, Signal, or another secure channel), and `.env` appears locally.
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Commands
|
|
176
|
+
|
|
177
|
+
Every command accepts an optional positional `[file]` argument (default: `.env`), and every command supports `--json` for structured output.
|
|
178
|
+
|
|
179
|
+
### `envlope init`
|
|
180
|
+
|
|
181
|
+
Encrypt an env file for the first time. Generates a fresh random key (or accepts an existing one via `--key`), writes `<file>.encrypted`, and ensures the plaintext file is in `.gitignore`.
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
# Generate a new random key
|
|
185
|
+
npx envlope init
|
|
186
|
+
|
|
187
|
+
# Reuse an existing key (multi-repo flow)
|
|
188
|
+
npx envlope init --key envlope_key_<paste-here>
|
|
189
|
+
|
|
190
|
+
# Init for a specific env file
|
|
191
|
+
npx envlope init .env.production
|
|
192
|
+
|
|
193
|
+
# Re-init without the confirmation prompt (for scripts / CI)
|
|
194
|
+
npx envlope init --yes
|
|
195
|
+
|
|
196
|
+
# JSON output for scripts
|
|
197
|
+
npx envlope init --json
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Options:**
|
|
201
|
+
|
|
202
|
+
| Flag | Description |
|
|
203
|
+
| ----------------- | ---------------------------------------------------------------------------------------------------- |
|
|
204
|
+
| `[file]` | Positional. The env file to encrypt. Default: `.env`. |
|
|
205
|
+
| `-k, --key <key>` | Use a specific key instead of generating a new one. Useful for sharing one key across multiple repos. |
|
|
206
|
+
| `-y, --yes` | Skip the confirmation prompt when `<file>.encrypted` already exists. |
|
|
207
|
+
| `--json` | Emit a single JSON object instead of human-readable text. |
|
|
208
|
+
|
|
209
|
+
**Behavior notes:**
|
|
210
|
+
|
|
211
|
+
- If `<file>.encrypted` already exists, the command warns you, asks for confirmation, and **backs up the old ciphertext to `<file>.encrypted.bak`** before replacing it. That backup is automatically gitignored.
|
|
212
|
+
- The generated key prints **once** to stdout. The tool does not save it anywhere. If you lose it, the encrypted file is unrecoverable.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
### `envlope encrypt`
|
|
217
|
+
|
|
218
|
+
Re-encrypt an env file after editing values locally. Requires the existing key — refuses keys that don't match the current encrypted file, so accidental key rotation can't happen here.
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
# Prompts for the key (hidden input)
|
|
222
|
+
npx envlope encrypt
|
|
223
|
+
|
|
224
|
+
# Pass the key inline
|
|
225
|
+
npx envlope encrypt --key envlope_key_<paste-here>
|
|
226
|
+
|
|
227
|
+
# Encrypt a specific env file
|
|
228
|
+
npx envlope encrypt .env.production --key ...
|
|
229
|
+
|
|
230
|
+
# Use ENVLOPE_KEY env var instead of --key
|
|
231
|
+
export ENVLOPE_KEY=envlope_key_<paste-here>
|
|
232
|
+
npx envlope encrypt
|
|
233
|
+
|
|
234
|
+
# JSON output
|
|
235
|
+
npx envlope encrypt --json
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Options:**
|
|
239
|
+
|
|
240
|
+
| Flag | Description |
|
|
241
|
+
| ----------------- | ---------------------------------------------------------------------------------------- |
|
|
242
|
+
| `[file]` | Positional. The env file to re-encrypt. Default: `.env`. |
|
|
243
|
+
| `-k, --key <key>` | The envlope key (otherwise read from `ENVLOPE_KEY` env var, otherwise prompted). |
|
|
244
|
+
| `--json` | Emit a single JSON object instead of human-readable text. |
|
|
245
|
+
|
|
246
|
+
**Behavior notes:**
|
|
247
|
+
|
|
248
|
+
- Before re-encrypting, the command verifies the provided key actually matches the current `<file>.encrypted`. If the key has been rotated (via `envlope init`), the old key is rejected with a clear error.
|
|
249
|
+
- Errors cleanly if no `<file>.encrypted` exists yet (use `init` first).
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
### `envlope decrypt`
|
|
254
|
+
|
|
255
|
+
Decrypt `<file>.encrypted` back to `<file>` using the team's shared key.
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
# Prompts for the key (hidden input)
|
|
259
|
+
npx envlope decrypt
|
|
260
|
+
|
|
261
|
+
# Pass the key inline
|
|
262
|
+
npx envlope decrypt --key envlope_key_<paste-here>
|
|
263
|
+
|
|
264
|
+
# Decrypt a specific file
|
|
265
|
+
npx envlope decrypt .env.production --key ...
|
|
266
|
+
|
|
267
|
+
# Use ENVLOPE_KEY env var
|
|
268
|
+
export ENVLOPE_KEY=envlope_key_<paste-here>
|
|
269
|
+
npx envlope decrypt
|
|
270
|
+
|
|
271
|
+
# Skip the "overwrite existing .env?" prompt
|
|
272
|
+
npx envlope decrypt --key ... --yes
|
|
273
|
+
|
|
274
|
+
# JSON output
|
|
275
|
+
npx envlope decrypt --json --key ... --yes
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Options:**
|
|
279
|
+
|
|
280
|
+
| Flag | Description |
|
|
281
|
+
| ----------------- | ---------------------------------------------------------------------------------- |
|
|
282
|
+
| `[file]` | Positional. The env file to decrypt. Default: `.env`. |
|
|
283
|
+
| `-k, --key <key>` | The envlope key (otherwise read from `ENVLOPE_KEY` env var, otherwise prompted). |
|
|
284
|
+
| `-y, --yes` | Overwrite an existing decrypted file without prompting. |
|
|
285
|
+
| `--json` | Emit a single JSON object instead of human-readable text. |
|
|
286
|
+
|
|
287
|
+
**Behavior notes:**
|
|
288
|
+
|
|
289
|
+
- If the plaintext file already exists, the command prompts before overwriting (unless `--yes`).
|
|
290
|
+
- A wrong key fails fast with `Invalid key — decryption failed.` — no stack trace, no leaked information.
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
### `envlope status`
|
|
295
|
+
|
|
296
|
+
Quick health check for a project's env file. Reports whether the plaintext and encrypted files exist, whether they're in sync, when the ciphertext was last updated, and whether `.gitignore` is protecting the plaintext.
|
|
297
|
+
|
|
298
|
+
```bash
|
|
299
|
+
npx envlope status
|
|
300
|
+
|
|
301
|
+
# Output:
|
|
302
|
+
# File: .env
|
|
303
|
+
# ✓ .env exists
|
|
304
|
+
# ✓ .env.encrypted exists (last encrypted 2h ago)
|
|
305
|
+
# ✓ .env and .env.encrypted are in sync
|
|
306
|
+
# ✓ .gitignore protects .env
|
|
307
|
+
|
|
308
|
+
# Check a specific file
|
|
309
|
+
npx envlope status .env.production
|
|
310
|
+
|
|
311
|
+
# JSON output for CI
|
|
312
|
+
npx envlope status --json
|
|
313
|
+
|
|
314
|
+
# Exit code 1 if out of sync (good for CI guards)
|
|
315
|
+
npx envlope status --strict
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**Options:**
|
|
319
|
+
|
|
320
|
+
| Flag | Description |
|
|
321
|
+
| ---------- | -------------------------------------------------------------------------------------- |
|
|
322
|
+
| `[file]` | Positional. The env file to check. Default: `.env`. |
|
|
323
|
+
| `--json` | Emit a single JSON object instead of human-readable text. |
|
|
324
|
+
| `--strict` | Exit with code 1 if the plaintext file is newer than the encrypted file (drift check). |
|
|
325
|
+
|
|
326
|
+
No key is required — `status` only inspects file metadata, never reads the encrypted contents.
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
### `envlope view`
|
|
331
|
+
|
|
332
|
+
Print the decrypted value of a single variable to stdout, without ever writing the plaintext env file to disk. Perfect for shell scripts that need exactly one secret.
|
|
333
|
+
|
|
334
|
+
```bash
|
|
335
|
+
# Print one value
|
|
336
|
+
npx envlope view DATABASE_URL
|
|
337
|
+
|
|
338
|
+
# In a script — assign without touching disk
|
|
339
|
+
DATABASE_URL=$(npx envlope view DATABASE_URL)
|
|
340
|
+
|
|
341
|
+
# Pass the key inline (or via ENVLOPE_KEY env var)
|
|
342
|
+
npx envlope view DATABASE_URL --key envlope_key_<paste-here>
|
|
343
|
+
|
|
344
|
+
# View a variable from a specific env file
|
|
345
|
+
npx envlope view STRIPE_KEY .env.production --key ...
|
|
346
|
+
|
|
347
|
+
# JSON output: {"variable":"DATABASE_URL","value":"postgres://..."}
|
|
348
|
+
npx envlope view DATABASE_URL --json
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
**Options:**
|
|
352
|
+
|
|
353
|
+
| Flag | Description |
|
|
354
|
+
| ----------------- | ------------------------------------------------------------------------------------------ |
|
|
355
|
+
| `<variable>` | **Required.** The variable name to look up. |
|
|
356
|
+
| `[file]` | Positional. The env file to read from. Default: `.env`. |
|
|
357
|
+
| `-k, --key <key>` | The envlope key (otherwise read from `ENVLOPE_KEY` env var, otherwise prompted). |
|
|
358
|
+
| `--json` | Emit `{"variable":"...", "value":"..."}` instead of just the value. |
|
|
359
|
+
|
|
360
|
+
**Behavior notes:**
|
|
361
|
+
|
|
362
|
+
- Returns exit code 1 if the variable isn't found in the env file, with a clear error message.
|
|
363
|
+
- Reads the encrypted file in memory only — the plaintext env never touches disk.
|
|
364
|
+
- Parsing is intentionally simple: `KEY=VALUE` per line. Comments (`#`) and blank lines are skipped. Quoted values and multi-line values are not interpreted specially; the entire string after the first `=` is returned verbatim.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Common workflows
|
|
369
|
+
|
|
370
|
+
### Sharing one key across multiple repos
|
|
371
|
+
|
|
372
|
+
If your team works across many projects, you usually don't want a separate key for every `.env`. Generate one key the first time, then reuse it everywhere with `--key`:
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
# First project — generate and save the key
|
|
376
|
+
cd ~/projects/repo-1
|
|
377
|
+
npx envlope init
|
|
378
|
+
|
|
379
|
+
# Every other project — paste the same key
|
|
380
|
+
cd ~/projects/repo-2
|
|
381
|
+
npx envlope init --key envlope_key_<paste-here>
|
|
382
|
+
|
|
383
|
+
cd ~/projects/repo-3
|
|
384
|
+
npx envlope init --key envlope_key_<paste-here>
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Now one shared key unlocks every repo's secrets. Onboarding a new teammate becomes **one** key, not twenty.
|
|
388
|
+
|
|
389
|
+
---
|
|
390
|
+
|
|
391
|
+
### Multiple env files (dev/staging/prod)
|
|
392
|
+
|
|
393
|
+
Each env file is encrypted independently. You can use the same key across all of them, or rotate per environment.
|
|
394
|
+
|
|
395
|
+
```bash
|
|
396
|
+
# Encrypt each environment independently
|
|
397
|
+
npx envlope init .env.development
|
|
398
|
+
npx envlope init .env.staging
|
|
399
|
+
npx envlope init .env.production
|
|
400
|
+
|
|
401
|
+
# Or share one key across them
|
|
402
|
+
KEY=envlope_key_<paste-here>
|
|
403
|
+
npx envlope init .env.development --key $KEY
|
|
404
|
+
npx envlope init .env.staging --key $KEY
|
|
405
|
+
npx envlope init .env.production --key $KEY
|
|
406
|
+
|
|
407
|
+
# View a single secret from production
|
|
408
|
+
npx envlope view DATABASE_URL .env.production --key $KEY
|
|
409
|
+
|
|
410
|
+
# Check sync state of one file
|
|
411
|
+
npx envlope status .env.production
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
Each `<file>.encrypted` is its own ciphertext; teammates only need the key for the environments they have access to.
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
### Using `ENVLOPE_KEY` (set the key once, every command picks it up)
|
|
419
|
+
|
|
420
|
+
Once `ENVLOPE_KEY` is set, every command works without `--key`.
|
|
421
|
+
|
|
422
|
+
> ⚠️ **`ENVLOPE_KEY` is an OS-level environment variable. It does NOT go inside your `.env` file.**
|
|
423
|
+
>
|
|
424
|
+
> Your `.env` is what envlope *encrypts* — it never reads it. Putting the key inside `.env` would be circular (you'd need to decrypt `.env` to read the key that decrypts `.env`). Set it at the shell/OS level instead, as shown below.
|
|
425
|
+
|
|
426
|
+
The key resolution priority is:
|
|
427
|
+
|
|
428
|
+
1. `--key <key>` CLI flag (highest)
|
|
429
|
+
2. `ENVLOPE_KEY` environment variable
|
|
430
|
+
3. Interactive prompt (only when running in a TTY)
|
|
431
|
+
|
|
432
|
+
#### Windows (PowerShell)
|
|
433
|
+
|
|
434
|
+
```powershell
|
|
435
|
+
# Current PowerShell session only (resets when the window closes)
|
|
436
|
+
$env:ENVLOPE_KEY = "envlope_key_<paste-here>"
|
|
437
|
+
|
|
438
|
+
# Persistent for your user account (survives reboots, takes effect in NEW terminals)
|
|
439
|
+
[Environment]::SetEnvironmentVariable("ENVLOPE_KEY", "envlope_key_<paste-here>", "User")
|
|
440
|
+
|
|
441
|
+
# Verify it's set in a new PowerShell window
|
|
442
|
+
$env:ENVLOPE_KEY
|
|
443
|
+
|
|
444
|
+
# Remove it later
|
|
445
|
+
[Environment]::SetEnvironmentVariable("ENVLOPE_KEY", $null, "User")
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
You can also set it through the GUI: **Win+R → `sysdm.cpl` → Advanced → Environment Variables → New**.
|
|
449
|
+
|
|
450
|
+
#### Windows (CMD)
|
|
451
|
+
|
|
452
|
+
```cmd
|
|
453
|
+
:: Current session only
|
|
454
|
+
set ENVLOPE_KEY=envlope_key_<paste-here>
|
|
455
|
+
|
|
456
|
+
:: Persistent for your user
|
|
457
|
+
setx ENVLOPE_KEY "envlope_key_<paste-here>"
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
#### macOS / Linux (bash / zsh)
|
|
461
|
+
|
|
462
|
+
```bash
|
|
463
|
+
# Current shell session only
|
|
464
|
+
export ENVLOPE_KEY="envlope_key_<paste-here>"
|
|
465
|
+
|
|
466
|
+
# Persistent — add this line to ~/.bashrc, ~/.zshrc, or ~/.config/fish/config.fish
|
|
467
|
+
echo 'export ENVLOPE_KEY="envlope_key_<paste-here>"' >> ~/.zshrc
|
|
468
|
+
source ~/.zshrc
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
#### Real-world dev workflow (Vite, Next.js, NestJS, etc.)
|
|
472
|
+
|
|
473
|
+
Add an `envlope decrypt` call to your `package.json` scripts using npm's `pre*` lifecycle hooks. Teammates set `ENVLOPE_KEY` once on their machine, then run their dev server normally — decryption happens automatically:
|
|
474
|
+
|
|
475
|
+
```json
|
|
476
|
+
{
|
|
477
|
+
"scripts": {
|
|
478
|
+
"predev": "envlope decrypt --yes",
|
|
479
|
+
"dev": "vite",
|
|
480
|
+
|
|
481
|
+
"prestart": "envlope decrypt --yes",
|
|
482
|
+
"start": "nest start",
|
|
483
|
+
|
|
484
|
+
"prebuild": "envlope decrypt --yes",
|
|
485
|
+
"build": "next build"
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
A new teammate's flow becomes:
|
|
491
|
+
|
|
492
|
+
```bash
|
|
493
|
+
git clone <repo>
|
|
494
|
+
cd <repo>
|
|
495
|
+
npm install
|
|
496
|
+
# (one-time: set ENVLOPE_KEY using one of the methods above)
|
|
497
|
+
npm run dev # ← envlope auto-decrypts .env, then vite starts
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
#### GitHub Actions / CI
|
|
501
|
+
|
|
502
|
+
```yaml
|
|
503
|
+
- name: Decrypt env
|
|
504
|
+
env:
|
|
505
|
+
ENVLOPE_KEY: ${{ secrets.ENVLOPE_KEY }}
|
|
506
|
+
run: npx envlope decrypt --yes
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
Store the key as a repository secret (`Settings → Secrets and variables → Actions`), reference it via `${{ secrets.ENVLOPE_KEY }}`.
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
### `--json` for automation
|
|
514
|
+
|
|
515
|
+
Every command emits a single line of JSON to stdout when `--json` is passed. Errors emit `{"error": "...", "code": 1}` on the same channel.
|
|
516
|
+
|
|
517
|
+
```bash
|
|
518
|
+
# Check sync state in CI
|
|
519
|
+
RESULT=$(npx envlope status --json)
|
|
520
|
+
IN_SYNC=$(echo "$RESULT" | jq -r .in_sync)
|
|
521
|
+
if [ "$IN_SYNC" != "true" ]; then
|
|
522
|
+
echo "Env file out of sync — re-encrypt before committing"
|
|
523
|
+
exit 1
|
|
524
|
+
fi
|
|
525
|
+
|
|
526
|
+
# Init in CI and capture the new key
|
|
527
|
+
NEW=$(npx envlope init --yes --json)
|
|
528
|
+
KEY=$(echo "$NEW" | jq -r .key)
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
In `--json` mode the CLI never prompts. Pass `--key` (or set `ENVLOPE_KEY`), and pass `--yes` for any destructive operation, otherwise the command exits with a clear error.
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
### Updating `.env` values
|
|
536
|
+
|
|
537
|
+
After you edit `.env` locally, sync the encrypted file before committing:
|
|
538
|
+
|
|
539
|
+
```bash
|
|
540
|
+
# Edit .env however you like
|
|
541
|
+
echo "NEW_VAR=value" >> .env
|
|
542
|
+
|
|
543
|
+
# Re-encrypt with your existing key
|
|
544
|
+
npx envlope encrypt --key envlope_key_<paste-here>
|
|
545
|
+
|
|
546
|
+
# Commit the updated ciphertext
|
|
547
|
+
git add .env.encrypted
|
|
548
|
+
git commit -m "Update env vars"
|
|
549
|
+
git push
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
When teammates pull, they re-run `envlope decrypt` to pick up the changes.
|
|
553
|
+
|
|
554
|
+
Tip: run `envlope status` before committing to confirm `.env` and `.env.encrypted` are in sync.
|
|
555
|
+
|
|
556
|
+
---
|
|
557
|
+
|
|
558
|
+
### Rotating the key
|
|
559
|
+
|
|
560
|
+
If someone leaves the team, or you suspect the key was exposed, generate a new one. **This invalidates the old key** — anyone still holding it can no longer decrypt new versions.
|
|
561
|
+
|
|
562
|
+
```bash
|
|
563
|
+
# In the project where the encrypted file lives:
|
|
564
|
+
npx envlope init
|
|
565
|
+
# Warning: .env.encrypted already exists. Generating a new key will replace it...
|
|
566
|
+
# ✓ Backed up old .env.encrypted → .env.encrypted.bak
|
|
567
|
+
# Generate a new key and re-encrypt? (y/N) y
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
The old ciphertext is automatically backed up to `.env.encrypted.bak` (gitignored) before being replaced — so if you regret the rotation in the next few seconds, `cp .env.encrypted.bak .env.encrypted` restores the previous state.
|
|
571
|
+
|
|
572
|
+
Output prints the new key. Save it. Share the new key with the remaining team via a secure channel. Old key is now useless against the freshly-encrypted file.
|
|
573
|
+
|
|
574
|
+
For non-interactive use (scripts/CI):
|
|
575
|
+
|
|
576
|
+
```bash
|
|
577
|
+
npx envlope init --yes
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
---
|
|
581
|
+
|
|
582
|
+
### Recovering from a lost key
|
|
583
|
+
|
|
584
|
+
If everyone on the team has lost the key, the encrypted file cannot be recovered — that's the whole security premise.
|
|
585
|
+
|
|
586
|
+
You can, however, start over from your current local `.env`:
|
|
587
|
+
|
|
588
|
+
```bash
|
|
589
|
+
# Make sure your local .env is up to date
|
|
590
|
+
# Then run init again — it will prompt to replace the encrypted file
|
|
591
|
+
npx envlope init --yes
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
This generates a fresh key and a fresh encrypted file. The old ciphertext goes to `.env.encrypted.bak` (where it's still unrecoverable without the lost key, but at least preserved on disk). Old encrypted versions in your git history remain unrecoverable.
|
|
595
|
+
|
|
596
|
+
---
|
|
597
|
+
|
|
598
|
+
## Security model
|
|
599
|
+
|
|
600
|
+
**The key IS the secret.** Once your team has the key, they can decrypt any past, present, or future version of the encrypted file. Treat it like a master password:
|
|
601
|
+
|
|
602
|
+
- Store it in a password manager (1Password, Bitwarden, etc.), not in plaintext anywhere.
|
|
603
|
+
- Share over a secure channel (encrypted DM, password manager sharing, in person) — never in a public Slack channel, email body, or commit message.
|
|
604
|
+
- Rotate the key (`envlope init`) whenever someone leaves the team or you suspect the key was exposed.
|
|
605
|
+
- The tool does **not** back up the key. If everyone loses it, the encrypted file is computationally unrecoverable.
|
|
606
|
+
|
|
607
|
+
**What about pushing the encrypted file to a public repo?** Yes, that's exactly what envlope is designed for. AES-256-GCM with a 256-bit random key has no known practical attack — making the ciphertext public exposes nothing as long as the key stays private.
|
|
608
|
+
|
|
609
|
+
**What envlope does NOT protect against:**
|
|
610
|
+
|
|
611
|
+
- A compromised teammate who has the key — they can decrypt everything.
|
|
612
|
+
- A keylogger on a teammate's machine that captures the key when they type it.
|
|
613
|
+
- A leaked `.env` file (the plaintext one) committed to git by accident — the encryption only protects what you encrypt.
|
|
614
|
+
- An attacker with shell access to a machine that has the decrypted `.env` sitting on disk.
|
|
615
|
+
|
|
616
|
+
These are the same trade-offs as every shared-secret system. envlope protects **at-rest** secrets that travel through your repo.
|
|
617
|
+
|
|
618
|
+
---
|
|
619
|
+
|
|
620
|
+
## Troubleshooting
|
|
621
|
+
|
|
622
|
+
### `Invalid key — decryption failed.`
|
|
623
|
+
|
|
624
|
+
The key you provided doesn't decrypt the current `.env.encrypted`. Either:
|
|
625
|
+
|
|
626
|
+
1. You typed/pasted the key wrong (most common — re-check from your password manager).
|
|
627
|
+
2. A teammate rotated the key via `envlope init` and you have the old one — ask them for the current key.
|
|
628
|
+
|
|
629
|
+
### `This key does not match the current .env.encrypted.`
|
|
630
|
+
|
|
631
|
+
Same root cause as above, but triggered by `encrypt`. The tool refuses to re-encrypt with a stale key because doing so would silently grant access to whoever still holds that key.
|
|
632
|
+
|
|
633
|
+
### `No key provided. Pass --key, set ENVLOPE_KEY env var, or run interactively.`
|
|
634
|
+
|
|
635
|
+
You ran `encrypt`, `decrypt`, or `view` without a `--key`, without `ENVLOPE_KEY` in the environment, and stdin wasn't a TTY (so the tool couldn't prompt). Use one of the three options the message suggests.
|
|
636
|
+
|
|
637
|
+
### I added `ENVLOPE_KEY=...` to my `.env` file but envlope still prompts for the key
|
|
638
|
+
|
|
639
|
+
**`ENVLOPE_KEY` is an OS environment variable, not a line in `.env`.** envlope doesn't read your `.env` file looking for its own key — that'd be circular (you'd need to decrypt `.env` to read the key to decrypt `.env`). See the [Using `ENVLOPE_KEY`](#using-envlope_key-set-the-key-once-every-command-picks-it-up) section above for how to set it correctly per platform (`$env:ENVLOPE_KEY` in PowerShell, `export` in bash, etc.).
|
|
640
|
+
|
|
641
|
+
### `No .env file found in this directory.`
|
|
642
|
+
|
|
643
|
+
You ran `envlope init` or `envlope encrypt` in a directory without a `.env` file. Create one first, or pass a different file as a positional arg (`envlope encrypt .env.staging`).
|
|
644
|
+
|
|
645
|
+
### `No .env.encrypted file found in this directory.`
|
|
646
|
+
|
|
647
|
+
You ran a command that requires an existing encrypted file but it isn't here. Either you're in the wrong directory, or no one has run `envlope init` for this project yet.
|
|
648
|
+
|
|
649
|
+
### `Variable 'XYZ' not found in .env.encrypted.`
|
|
650
|
+
|
|
651
|
+
From `envlope view`. The variable name doesn't exist in the decrypted env file. Check spelling and that you're targeting the right file.
|
|
652
|
+
|
|
653
|
+
### `envlope: command not found` after `npm i envlope`
|
|
654
|
+
|
|
655
|
+
Local installs don't put binaries on your PATH. Either use `npx envlope <command>`, or install globally with `npm install -g envlope`.
|
|
656
|
+
|
|
657
|
+
### Re-init prompts fail in scripts / CI
|
|
658
|
+
|
|
659
|
+
The confirmation prompt for re-init requires a TTY. In non-interactive environments, pass `--yes`:
|
|
660
|
+
|
|
661
|
+
```bash
|
|
662
|
+
npx envlope init --yes
|
|
663
|
+
npx envlope decrypt --key ... --yes
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
In `--json` mode the tool refuses to prompt at all — pass `--yes` explicitly or provide all required keys/files via flags or env vars.
|
|
667
|
+
|
|
668
|
+
---
|
|
669
|
+
|
|
670
|
+
## How it works (technical)
|
|
671
|
+
|
|
672
|
+
- **Algorithm:** AES-256-GCM (authenticated encryption — provides both confidentiality and tamper-detection)
|
|
673
|
+
- **Key:** 32 random bytes from `crypto.randomBytes`, base64-encoded with an `envlope_key_` prefix
|
|
674
|
+
- **IV:** 12 random bytes per encryption — every encrypt produces a different ciphertext, even for identical plaintext
|
|
675
|
+
- **Auth tag:** 16-byte GCM tag — any tampering with the ciphertext is detected at decrypt time and rejected
|
|
676
|
+
- **Key derivation:** none. The key is already high-entropy random bytes; no PBKDF2/Argon2 needed.
|
|
677
|
+
|
|
678
|
+
The encrypted file is a single line:
|
|
679
|
+
|
|
680
|
+
```
|
|
681
|
+
envlope:1:<base64(iv || ciphertext || tag)>
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
The `envlope:1:` prefix is a version tag — if the format ever changes in a backwards-incompatible way (it won't on a whim), old files will be readable by version-aware tooling.
|
|
685
|
+
|
|
686
|
+
**Crypto implementation lives in [`src/crypto.ts`](./src/crypto.ts)** — ~50 lines, uses only Node's built-in `node:crypto` module. No third-party crypto dependencies.
|
|
687
|
+
|
|
688
|
+
**No background processes, no network calls (except the optional update-notifier version check), no telemetry.** The tool only reads/writes:
|
|
689
|
+
|
|
690
|
+
- `.env` (or whichever env file you pointed it at — the plaintext)
|
|
691
|
+
- `.env.encrypted` (the ciphertext)
|
|
692
|
+
- `.env.encrypted.bak` (created automatically when `init` replaces an existing ciphertext)
|
|
693
|
+
- `.gitignore` (to ensure plaintext files never accidentally end up in git)
|
|
694
|
+
|
|
695
|
+
That's it.
|
|
696
|
+
|
|
697
|
+
---
|
|
698
|
+
|
|
699
|
+
## Development
|
|
700
|
+
|
|
701
|
+
```bash
|
|
702
|
+
# Clone and install
|
|
703
|
+
git clone <repo>
|
|
704
|
+
cd envlope
|
|
705
|
+
npm install
|
|
706
|
+
|
|
707
|
+
# Run tests (vitest)
|
|
708
|
+
npm test
|
|
709
|
+
|
|
710
|
+
# Watch mode while developing
|
|
711
|
+
npm run test:watch
|
|
712
|
+
|
|
713
|
+
# Run the CLI from source
|
|
714
|
+
npm run dev -- <command>
|
|
715
|
+
|
|
716
|
+
# Type-check
|
|
717
|
+
npm run typecheck
|
|
718
|
+
|
|
719
|
+
# Build production bundle to dist/
|
|
720
|
+
npm run build
|
|
721
|
+
```
|
|
722
|
+
|
|
723
|
+
Test coverage spans crypto round-trips, key format validation, multi-file flows, env var resolution, JSON output, and end-to-end CLI tests against temp directories. See [`tests/`](./tests/).
|
|
724
|
+
|
|
725
|
+
---
|
|
726
|
+
|
|
727
|
+
## Changelog
|
|
728
|
+
|
|
729
|
+
See [CHANGELOG.md](./CHANGELOG.md) for the full version history.
|
|
730
|
+
|
|
731
|
+
---
|
|
732
|
+
|
|
733
|
+
## License
|
|
734
|
+
|
|
735
|
+
[MIT](./LICENSE) — do whatever you want, just don't blame me.
|