@dotenvx/dotenvx 2.2.2 → 2.3.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 +15 -1
- package/README.md +76 -34
- package/package.json +1 -1
- package/src/cli/actions/decrypt.js +2 -3
- package/src/cli/actions/encrypt.js +2 -3
- package/src/cli/actions/get.js +2 -3
- package/src/cli/actions/keychain/up.js +2 -2
- package/src/cli/actions/keypair.js +2 -3
- package/src/cli/actions/lock/down.js +74 -0
- package/src/cli/actions/lock/up.js +80 -0
- package/src/cli/actions/run.js +2 -3
- package/src/cli/actions/set.js +2 -3
- package/src/cli/commands/armor.js +1 -1
- package/src/cli/commands/lock.js +27 -0
- package/src/cli/commands/{keychain.js → native.js} +13 -13
- package/src/cli/dotenvx.js +14 -16
- package/src/lib/helpers/errors.js +13 -0
- package/src/lib/helpers/executeDynamic.js +1 -19
- package/src/lib/main.d.ts +9 -33
- package/src/lib/main.js +3 -3
- package/src/lib/providers/index.js +1 -1
- package/src/lib/services/keychainPull.js +1 -1
- package/src/lib/services/lockDown.js +157 -0
- package/src/lib/services/lockUp.js +150 -0
- package/src/cli/actions/normalizeArmorAliases.js +0 -9
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
|
4
4
|
|
|
5
|
-
[Unreleased](https://github.com/dotenvx/dotenvx/compare/v2.
|
|
5
|
+
[Unreleased](https://github.com/dotenvx/dotenvx/compare/v2.3.0...main)
|
|
6
|
+
|
|
7
|
+
## [2.3.0](https://github.com/dotenvx/dotenvx/compare/v2.2.2...v2.3.0) (2026-07-08)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
* Add `lock up` and `lock down` commands for password protecting private keys ([#875](https://github.com/dotenvx/dotenvx/pull/875))
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
* BREAKING: Rename `keychain` commands to `native` to make way for windows and linux. ([#875](https://github.com/dotenvx/dotenvx/pull/875))
|
|
16
|
+
|
|
17
|
+
### Removed
|
|
18
|
+
|
|
19
|
+
* BREAKING: Remove `--no-ops`. Use `--no-armor`. ([#875](https://github.com/dotenvx/dotenvx/pull/875))
|
|
6
20
|
|
|
7
21
|
## [2.2.2](https://github.com/dotenvx/dotenvx/compare/v2.2.1...v2.2.2) (2026-07-08)
|
|
8
22
|
|
package/README.md
CHANGED
|
@@ -1385,12 +1385,12 @@ $ dotenvx run --token "$DOTENVX_ARMOR_TOKEN" -- yourcommand
|
|
|
1385
1385
|
```
|
|
1386
1386
|
|
|
1387
1387
|
</details>
|
|
1388
|
-
<details><summary>`run --no-
|
|
1388
|
+
<details><summary>`run --no-native`</summary><br>
|
|
1389
1389
|
|
|
1390
|
-
Turn off
|
|
1390
|
+
Turn off OS secret store lookups.
|
|
1391
1391
|
|
|
1392
1392
|
```sh
|
|
1393
|
-
$ dotenvx run --no-
|
|
1393
|
+
$ dotenvx run --no-native -- yourcommand
|
|
1394
1394
|
```
|
|
1395
1395
|
|
|
1396
1396
|
</details>
|
|
@@ -1415,12 +1415,12 @@ World
|
|
|
1415
1415
|
```
|
|
1416
1416
|
|
|
1417
1417
|
</details>
|
|
1418
|
-
<details><summary>`get KEY --no-
|
|
1418
|
+
<details><summary>`get KEY --no-native`</summary><br>
|
|
1419
1419
|
|
|
1420
|
-
Turn off
|
|
1420
|
+
Turn off OS secret store lookups for get.
|
|
1421
1421
|
|
|
1422
1422
|
```sh
|
|
1423
|
-
$ dotenvx get HELLO --no-
|
|
1423
|
+
$ dotenvx get HELLO --no-native
|
|
1424
1424
|
```
|
|
1425
1425
|
|
|
1426
1426
|
</details>
|
|
@@ -1739,12 +1739,12 @@ set HELLO_PLAIN (.env)
|
|
|
1739
1739
|
Keys ending in `_PLAIN` are not encrypted by `dotenvx set` or `dotenvx encrypt`.
|
|
1740
1740
|
|
|
1741
1741
|
</details>
|
|
1742
|
-
<details><summary>`set KEY value --no-
|
|
1742
|
+
<details><summary>`set KEY value --no-native`</summary><br>
|
|
1743
1743
|
|
|
1744
|
-
Turn off
|
|
1744
|
+
Turn off OS secret store lookups for set.
|
|
1745
1745
|
|
|
1746
1746
|
```sh
|
|
1747
|
-
$ dotenvx set HELLO Dotenvx --no-
|
|
1747
|
+
$ dotenvx set HELLO Dotenvx --no-native
|
|
1748
1748
|
```
|
|
1749
1749
|
|
|
1750
1750
|
</details>
|
|
@@ -1779,12 +1779,12 @@ $ dotenvx encrypt -f .env.production
|
|
|
1779
1779
|
```
|
|
1780
1780
|
|
|
1781
1781
|
</details>
|
|
1782
|
-
<details><summary>`encrypt --no-
|
|
1782
|
+
<details><summary>`encrypt --no-native`</summary><br>
|
|
1783
1783
|
|
|
1784
|
-
Turn off
|
|
1784
|
+
Turn off OS secret store lookups for encrypt.
|
|
1785
1785
|
|
|
1786
1786
|
```sh
|
|
1787
|
-
$ dotenvx encrypt --no-
|
|
1787
|
+
$ dotenvx encrypt --no-native
|
|
1788
1788
|
◈ encrypted (.env)
|
|
1789
1789
|
```
|
|
1790
1790
|
|
|
@@ -1918,12 +1918,12 @@ $ dotenvx decrypt
|
|
|
1918
1918
|
```
|
|
1919
1919
|
|
|
1920
1920
|
</details>
|
|
1921
|
-
<details><summary>`decrypt --no-
|
|
1921
|
+
<details><summary>`decrypt --no-native`</summary><br>
|
|
1922
1922
|
|
|
1923
|
-
Turn off
|
|
1923
|
+
Turn off OS secret store lookups for decrypt.
|
|
1924
1924
|
|
|
1925
1925
|
```sh
|
|
1926
|
-
$ dotenvx decrypt --no-
|
|
1926
|
+
$ dotenvx decrypt --no-native
|
|
1927
1927
|
◇ decrypted (.env)
|
|
1928
1928
|
```
|
|
1929
1929
|
|
|
@@ -2039,12 +2039,12 @@ $ dotenvx keypair
|
|
|
2039
2039
|
```
|
|
2040
2040
|
|
|
2041
2041
|
</details>
|
|
2042
|
-
<details><summary>`keypair --no-
|
|
2042
|
+
<details><summary>`keypair --no-native`</summary><br>
|
|
2043
2043
|
|
|
2044
|
-
Turn off
|
|
2044
|
+
Turn off OS secret store lookups for keypair.
|
|
2045
2045
|
|
|
2046
2046
|
```sh
|
|
2047
|
-
$ dotenvx keypair --no-
|
|
2047
|
+
$ dotenvx keypair --no-native
|
|
2048
2048
|
{"DOTENV_PUBLIC_KEY":"<publicKey>","DOTENV_PRIVATE_KEY":"<privateKey>"}
|
|
2049
2049
|
```
|
|
2050
2050
|
|
|
@@ -2323,52 +2323,93 @@ CMD ["/usr/local/bin/dotenvx", "run", "--", "node", "apps/backend/index.js"]
|
|
|
2323
2323
|
```
|
|
2324
2324
|
|
|
2325
2325
|
</details>
|
|
2326
|
-
<details><summary>`
|
|
2326
|
+
<details><summary>`lock`</summary><br>
|
|
2327
2327
|
|
|
2328
|
-
|
|
2328
|
+
Lock private keys with a local passphrase to keep them protected inside `.env.keys`.
|
|
2329
|
+
|
|
2330
|
+
```
|
|
2331
|
+
# example
|
|
2332
|
+
DOTENV_PRIVATE_KEY=locked:02f5b97ad58b49ae324cd4e7937bc19b251d006b31cacf46f789eeaf03f923cedc:AZIPDxKqjPLiGl5b4CqVGbR3CIBDUcqHthGaoeWLoUvxbTHJkj3jGoGWGaxFSDUJGQUmWDaExRzxKpVydYF_7qiWr1ecqksOFho5t3EMwKbqX2-y-LZO9K3a4SJaYAjDJXpn3NwG4vAt1oLmGA
|
|
2333
|
+
```
|
|
2334
|
+
|
|
2335
|
+
</details>
|
|
2336
|
+
<details><summary>`lock up`</summary><br>
|
|
2337
|
+
|
|
2338
|
+
Lock a private key in `.env.keys` with a local passphrase.
|
|
2339
|
+
|
|
2340
|
+
```sh
|
|
2341
|
+
$ dotenvx lock up
|
|
2342
|
+
```
|
|
2343
|
+
|
|
2344
|
+
Here's what a locked key looks like:
|
|
2345
|
+
|
|
2346
|
+
```ini
|
|
2347
|
+
# .env.keys
|
|
2348
|
+
DOTENV_PRIVATE_KEY=locked:02f5b97ad58b49ae324cd4e7937bc19b251d006b31cacf46f789eeaf03f923cedc:AZIPDxKqjPLiGl5b4CqVGbR3CIBDUcqHthGaoeWLoUvxbTHJkj3jGoGWGaxFSDUJGQUmWDaExRzxKpVydYF_7qiWr1ecqksOFho5t3EMwKbqX2-y-LZO9K3a4SJaYAjDJXpn3NwG4vAt1oLmGA
|
|
2349
|
+
```
|
|
2350
|
+
|
|
2351
|
+
Specify files with `-f` and `-fk`.
|
|
2352
|
+
|
|
2353
|
+
```sh
|
|
2354
|
+
$ dotenvx lock up -f .env.production -fk .env.keys
|
|
2355
|
+
```
|
|
2356
|
+
|
|
2357
|
+
</details>
|
|
2358
|
+
<details><summary>`lock down`</summary><br>
|
|
2359
|
+
|
|
2360
|
+
Unlock a private key in `.env.keys` with its local passphrase.
|
|
2361
|
+
|
|
2362
|
+
```sh
|
|
2363
|
+
$ dotenvx lock down
|
|
2364
|
+
```
|
|
2365
|
+
|
|
2366
|
+
</details>
|
|
2367
|
+
<details><summary>`native`</summary><br>
|
|
2368
|
+
|
|
2369
|
+
Move private keys into your OS secret store (macOS Keychain supported).
|
|
2329
2370
|
|
|
2330
2371
|
Currently supported on macOS.
|
|
2331
2372
|
|
|
2332
2373
|
</details>
|
|
2333
|
-
<details><summary>`
|
|
2374
|
+
<details><summary>`native up`</summary><br>
|
|
2334
2375
|
|
|
2335
|
-
Move a private key from `.env.keys` into
|
|
2376
|
+
Move a private key from `.env.keys` into your OS secret store.
|
|
2336
2377
|
|
|
2337
2378
|
```sh
|
|
2338
|
-
$ dotenvx
|
|
2379
|
+
$ dotenvx native up
|
|
2339
2380
|
```
|
|
2340
2381
|
|
|
2341
2382
|
Specify files with `-f` and `-fk`.
|
|
2342
2383
|
|
|
2343
2384
|
```sh
|
|
2344
|
-
$ dotenvx
|
|
2385
|
+
$ dotenvx native up -f .env.production -fk .env.keys
|
|
2345
2386
|
```
|
|
2346
2387
|
|
|
2347
2388
|
</details>
|
|
2348
|
-
<details><summary>`
|
|
2389
|
+
<details><summary>`native down`</summary><br>
|
|
2349
2390
|
|
|
2350
|
-
Move a private key from
|
|
2391
|
+
Move a private key from your OS secret store back into `.env.keys`.
|
|
2351
2392
|
|
|
2352
2393
|
```sh
|
|
2353
|
-
$ dotenvx
|
|
2394
|
+
$ dotenvx native down
|
|
2354
2395
|
```
|
|
2355
2396
|
|
|
2356
2397
|
</details>
|
|
2357
|
-
<details><summary>`
|
|
2398
|
+
<details><summary>`native push`</summary><br>
|
|
2358
2399
|
|
|
2359
|
-
Copy a private key from `.env.keys` into
|
|
2400
|
+
Copy a private key from `.env.keys` into your OS secret store.
|
|
2360
2401
|
|
|
2361
2402
|
```sh
|
|
2362
|
-
$ dotenvx
|
|
2403
|
+
$ dotenvx native push
|
|
2363
2404
|
```
|
|
2364
2405
|
|
|
2365
2406
|
</details>
|
|
2366
|
-
<details><summary>`
|
|
2407
|
+
<details><summary>`native pull`</summary><br>
|
|
2367
2408
|
|
|
2368
|
-
Copy a private key from
|
|
2409
|
+
Copy a private key from your OS secret store into `.env.keys`.
|
|
2369
2410
|
|
|
2370
2411
|
```sh
|
|
2371
|
-
$ dotenvx
|
|
2412
|
+
$ dotenvx native pull
|
|
2372
2413
|
```
|
|
2373
2414
|
|
|
2374
2415
|
</details>
|
|
@@ -2488,7 +2529,8 @@ Commands:
|
|
|
2488
2529
|
prevent including .env files in docker
|
|
2489
2530
|
|
|
2490
2531
|
Professional Security:
|
|
2491
|
-
|
|
2532
|
+
lock ⊡ lock private keys with a local passphrase
|
|
2533
|
+
native ⌥ move private keys into your OS secret store (macOS Keychain supported)
|
|
2492
2534
|
armor ⛨ move private keys into Dotenvx Armor [www.dotenvx.com/armor]
|
|
2493
2535
|
```
|
|
2494
2536
|
|
package/package.json
CHANGED
|
@@ -4,12 +4,11 @@ const { logger } = require('./../../shared/logger')
|
|
|
4
4
|
const catchAndLog = require('../../lib/helpers/catchAndLog')
|
|
5
5
|
const createSpinner = require('../../lib/helpers/createSpinner')
|
|
6
6
|
const Session = require('../../db/session')
|
|
7
|
-
const normalizeArmorAliases = require('./normalizeArmorAliases')
|
|
8
7
|
|
|
9
8
|
const decryptTransform = require('./../../lib/transforms/decrypt')
|
|
10
9
|
|
|
11
10
|
async function decrypt () {
|
|
12
|
-
const options =
|
|
11
|
+
const options = this.opts()
|
|
13
12
|
const spinner = await createSpinner({ ...options, text: 'decrypting' })
|
|
14
13
|
|
|
15
14
|
logger.debug(`options: ${JSON.stringify(options)}`)
|
|
@@ -17,7 +16,7 @@ async function decrypt () {
|
|
|
17
16
|
const envs = this.envs
|
|
18
17
|
const sesh = new Session()
|
|
19
18
|
const noArmor = options.armor === false || (await sesh.noArmor())
|
|
20
|
-
const noKeychain = options.
|
|
19
|
+
const noKeychain = options.native === false || options.noNative === true
|
|
21
20
|
|
|
22
21
|
let errorCount = 0
|
|
23
22
|
|
|
@@ -6,10 +6,9 @@ const encryptTransform = require('./../../lib/transforms/encrypt')
|
|
|
6
6
|
const catchAndLog = require('../../lib/helpers/catchAndLog')
|
|
7
7
|
const createSpinner = require('../../lib/helpers/createSpinner')
|
|
8
8
|
const Session = require('../../db/session')
|
|
9
|
-
const normalizeArmorAliases = require('./normalizeArmorAliases')
|
|
10
9
|
|
|
11
10
|
async function encryptAction () {
|
|
12
|
-
const options =
|
|
11
|
+
const options = this.opts()
|
|
13
12
|
const spinner = await createSpinner({ ...options, text: 'encrypting' })
|
|
14
13
|
const sesh = new Session()
|
|
15
14
|
|
|
@@ -21,7 +20,7 @@ async function encryptAction () {
|
|
|
21
20
|
const fk = options.envKeysFile || '.env.keys'
|
|
22
21
|
const noCreate = options.create === false
|
|
23
22
|
const noArmor = options.armor === false || (!options.token && (await sesh.noArmor()))
|
|
24
|
-
const noKeychain = options.
|
|
23
|
+
const noKeychain = options.native === false || options.noNative === true
|
|
25
24
|
|
|
26
25
|
let errorCount = 0
|
|
27
26
|
|
package/src/cli/actions/get.js
CHANGED
|
@@ -6,11 +6,10 @@ const catchAndLog = require('./../../lib/helpers/catchAndLog')
|
|
|
6
6
|
const createSpinner = require('../../lib/helpers/createSpinner')
|
|
7
7
|
const Session = require('../../db/session')
|
|
8
8
|
const getResolver = require('./../../lib/resolvers/get')
|
|
9
|
-
const normalizeArmorAliases = require('./normalizeArmorAliases')
|
|
10
9
|
const normalizeDotenvConfigConvention = require('../../lib/helpers/normalizeDotenvConfigConvention')
|
|
11
10
|
|
|
12
11
|
async function get (key) {
|
|
13
|
-
const options = normalizeDotenvConfigConvention(
|
|
12
|
+
const options = normalizeDotenvConfigConvention(this.opts())
|
|
14
13
|
const spinner = await createSpinner({ ...options, text: 'decrypting' })
|
|
15
14
|
|
|
16
15
|
logger.debug(`options: ${JSON.stringify(options)}`)
|
|
@@ -33,7 +32,7 @@ async function get (key) {
|
|
|
33
32
|
try {
|
|
34
33
|
const sesh = new Session()
|
|
35
34
|
const noArmor = options.armor === false || (await sesh.noArmor())
|
|
36
|
-
const noKeychain = options.
|
|
35
|
+
const noKeychain = options.native === false || options.noNative === true
|
|
37
36
|
const { parsed, errors } = await getResolver({
|
|
38
37
|
key,
|
|
39
38
|
envs,
|
|
@@ -5,7 +5,7 @@ const armoredKeyDisplay = require('../../../lib/helpers/armoredKeyDisplay')
|
|
|
5
5
|
|
|
6
6
|
async function up () {
|
|
7
7
|
const options = this.opts()
|
|
8
|
-
const spinner = await createSpinner({ ...options, text: '
|
|
8
|
+
const spinner = await createSpinner({ ...options, text: 'storing' })
|
|
9
9
|
|
|
10
10
|
logger.debug(`options: ${JSON.stringify(options)}`)
|
|
11
11
|
|
|
@@ -15,7 +15,7 @@ async function up () {
|
|
|
15
15
|
|
|
16
16
|
if (spinner) spinner.stop()
|
|
17
17
|
if (changed) {
|
|
18
|
-
logger.success(`⌥
|
|
18
|
+
logger.success(`⌥ stored (${keyDisplay})`)
|
|
19
19
|
} else {
|
|
20
20
|
logger.info(`○ no change (${keyDisplay})`)
|
|
21
21
|
}
|
|
@@ -3,10 +3,9 @@ const { logger } = require('./../../shared/logger')
|
|
|
3
3
|
const keypairResolver = require('./../../lib/resolvers/keypair')
|
|
4
4
|
const catchAndLog = require('./../../lib/helpers/catchAndLog')
|
|
5
5
|
const createSpinner = require('../../lib/helpers/createSpinner')
|
|
6
|
-
const normalizeArmorAliases = require('./normalizeArmorAliases')
|
|
7
6
|
|
|
8
7
|
async function keypair (key) {
|
|
9
|
-
const options =
|
|
8
|
+
const options = this.opts()
|
|
10
9
|
const spinner = await createSpinner({ ...options, text: 'retrieving' })
|
|
11
10
|
|
|
12
11
|
logger.debug(`options: ${JSON.stringify(options)}`)
|
|
@@ -21,7 +20,7 @@ async function keypair (key) {
|
|
|
21
20
|
envFile: options.envFile,
|
|
22
21
|
envKeysFile: options.envKeysFile,
|
|
23
22
|
armor: options.armor,
|
|
24
|
-
noKeychain: options.
|
|
23
|
+
noKeychain: options.native === false || options.noNative === true,
|
|
25
24
|
token: options.token,
|
|
26
25
|
command: process.argv.slice(2),
|
|
27
26
|
onStatus: (text) => {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const crypto = require('crypto')
|
|
2
|
+
|
|
3
|
+
const { logger } = require('../../../shared/logger')
|
|
4
|
+
const LockDown = require('./../../../lib/services/lockDown')
|
|
5
|
+
const armoredKeyDisplay = require('../../../lib/helpers/armoredKeyDisplay')
|
|
6
|
+
const prompts = require('../../../lib/helpers/prompts')
|
|
7
|
+
|
|
8
|
+
function unlockedValue (lockedPrivateKey, passphrase) {
|
|
9
|
+
const parts = lockedPrivateKey.split(':')
|
|
10
|
+
const payload = Buffer.from(parts.slice(2).join(':'), 'base64url')
|
|
11
|
+
const version = payload.subarray(0, 1)[0]
|
|
12
|
+
const salt = payload.subarray(1, 17)
|
|
13
|
+
const iv = payload.subarray(17, 29)
|
|
14
|
+
const tag = payload.subarray(29, 45)
|
|
15
|
+
const ciphertext = payload.subarray(45)
|
|
16
|
+
const key = crypto.scryptSync(passphrase, salt, 32)
|
|
17
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv)
|
|
18
|
+
|
|
19
|
+
if (version !== 1) {
|
|
20
|
+
throw new Error('unsupported locked private key version')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
decipher.setAuthTag(tag)
|
|
24
|
+
|
|
25
|
+
return Buffer.concat([
|
|
26
|
+
decipher.update(ciphertext),
|
|
27
|
+
decipher.final()
|
|
28
|
+
]).toString('utf8')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function down () {
|
|
32
|
+
const options = this.opts()
|
|
33
|
+
logger.debug(`options: ${JSON.stringify(options)}`)
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const lockDown = new LockDown(options.envFile, options.envKeysFile)
|
|
37
|
+
const plan = lockDown.plan()
|
|
38
|
+
let results = plan.alreadyUnlocked
|
|
39
|
+
|
|
40
|
+
if (plan.locked.length > 0) {
|
|
41
|
+
const passphrase = await prompts.password({
|
|
42
|
+
message: 'passphrase',
|
|
43
|
+
prefix: '⊡',
|
|
44
|
+
separator: '='
|
|
45
|
+
}, {
|
|
46
|
+
input: process.stdin,
|
|
47
|
+
output: process.stderr
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
results = lockDown.run(lockedPrivateKey => unlockedValue(lockedPrivateKey, passphrase)).results
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
for (const result of results) {
|
|
54
|
+
const keyDisplay = armoredKeyDisplay(result.publicKeyValue) || result.privateKeyName
|
|
55
|
+
|
|
56
|
+
if (result.changed) {
|
|
57
|
+
logger.success(`⊡ unlocked (${keyDisplay})`)
|
|
58
|
+
} else {
|
|
59
|
+
logger.info(`○ no change (${keyDisplay})`)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (error.code === 'PROMPT_CANCELLED') {
|
|
64
|
+
process.exit(130)
|
|
65
|
+
return
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
logger.error(error.message)
|
|
69
|
+
process.exit(1)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = down
|
|
74
|
+
module.exports.unlockedValue = unlockedValue
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const crypto = require('crypto')
|
|
2
|
+
|
|
3
|
+
const { logger } = require('../../../shared/logger')
|
|
4
|
+
const LockUp = require('./../../../lib/services/lockUp')
|
|
5
|
+
const armoredKeyDisplay = require('../../../lib/helpers/armoredKeyDisplay')
|
|
6
|
+
const prompts = require('../../../lib/helpers/prompts')
|
|
7
|
+
|
|
8
|
+
function lockedValue (privateKey, passphrase, publicKey) {
|
|
9
|
+
const salt = crypto.randomBytes(16)
|
|
10
|
+
const iv = crypto.randomBytes(12)
|
|
11
|
+
const key = crypto.scryptSync(passphrase, salt, 32)
|
|
12
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv)
|
|
13
|
+
const ciphertext = Buffer.concat([
|
|
14
|
+
cipher.update(privateKey, 'utf8'),
|
|
15
|
+
cipher.final()
|
|
16
|
+
])
|
|
17
|
+
const tag = cipher.getAuthTag()
|
|
18
|
+
const payload = Buffer.concat([
|
|
19
|
+
Buffer.from([1]),
|
|
20
|
+
salt,
|
|
21
|
+
iv,
|
|
22
|
+
tag,
|
|
23
|
+
ciphertext
|
|
24
|
+
]).toString('base64url')
|
|
25
|
+
|
|
26
|
+
return `locked:${publicKey}:${payload}`
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function up () {
|
|
30
|
+
const options = this.opts()
|
|
31
|
+
logger.debug(`options: ${JSON.stringify(options)}`)
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const lockUp = new LockUp(options.envFile, options.envKeysFile)
|
|
35
|
+
|
|
36
|
+
const plan = lockUp.plan()
|
|
37
|
+
let results = plan.alreadyLocked.map(lockedPrivateKey => ({
|
|
38
|
+
changed: false,
|
|
39
|
+
privateKeyName: lockedPrivateKey.privateKeyName,
|
|
40
|
+
privateKeyValue: undefined,
|
|
41
|
+
lockedPrivateKeyValue: lockedPrivateKey.lockedPrivateKeyValue,
|
|
42
|
+
publicKeyValue: lockedPrivateKey.publicKeyValue,
|
|
43
|
+
alreadyLocked: true
|
|
44
|
+
}))
|
|
45
|
+
|
|
46
|
+
if (plan.matches.length > 0) {
|
|
47
|
+
const passphrase = await prompts.password({
|
|
48
|
+
message: 'passphrase',
|
|
49
|
+
prefix: '⊡',
|
|
50
|
+
separator: '='
|
|
51
|
+
}, {
|
|
52
|
+
input: process.stdin,
|
|
53
|
+
output: process.stderr
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
results = lockUp.run((privateKey, publicKey) => lockedValue(privateKey, passphrase, publicKey)).results
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const result of results) {
|
|
60
|
+
const keyDisplay = armoredKeyDisplay(result.publicKeyValue) || result.privateKeyName
|
|
61
|
+
|
|
62
|
+
if (result.changed) {
|
|
63
|
+
logger.success(`⊡ locked (${keyDisplay})`)
|
|
64
|
+
} else {
|
|
65
|
+
logger.info(`○ no change (${keyDisplay})`)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (error.code === 'PROMPT_CANCELLED') {
|
|
70
|
+
process.exit(130)
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
logger.error(error.message)
|
|
75
|
+
process.exit(1)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = up
|
|
80
|
+
module.exports.lockedValue = lockedValue
|
package/src/cli/actions/run.js
CHANGED
|
@@ -6,7 +6,6 @@ const envsResolver = require('./../../lib/resolvers/envs')
|
|
|
6
6
|
const catchAndLog = require('./../../lib/helpers/catchAndLog')
|
|
7
7
|
const createSpinner = require('../../lib/helpers/createSpinner')
|
|
8
8
|
const Session = require('../../db/session')
|
|
9
|
-
const normalizeArmorAliases = require('./normalizeArmorAliases')
|
|
10
9
|
const normalizeDotenvConfigQuiet = require('../../lib/helpers/normalizeDotenvConfigQuiet')
|
|
11
10
|
const normalizeDotenvConfigConvention = require('../../lib/helpers/normalizeDotenvConfigConvention')
|
|
12
11
|
|
|
@@ -47,7 +46,7 @@ function uniqueInjectedKeys (processedEnvs) {
|
|
|
47
46
|
}
|
|
48
47
|
|
|
49
48
|
async function run () {
|
|
50
|
-
const options = normalizeDotenvConfigConvention(normalizeDotenvConfigQuiet(
|
|
49
|
+
const options = normalizeDotenvConfigConvention(normalizeDotenvConfigQuiet(this.opts()))
|
|
51
50
|
|
|
52
51
|
let commandArgs = this.args
|
|
53
52
|
if (commandArgs.length < 1) {
|
|
@@ -63,7 +62,7 @@ async function run () {
|
|
|
63
62
|
|
|
64
63
|
const sesh = new Session()
|
|
65
64
|
const noArmor = options.armor === false || (!options.token && (await sesh.noArmor()))
|
|
66
|
-
const noKeychain = options.
|
|
65
|
+
const noKeychain = options.native === false || options.noNative === true
|
|
67
66
|
|
|
68
67
|
if (commandArgs.length < 1) {
|
|
69
68
|
if (spinner) spinner.stop()
|
package/src/cli/actions/set.js
CHANGED
|
@@ -8,10 +8,9 @@ const createSpinner = require('../../lib/helpers/createSpinner')
|
|
|
8
8
|
const Errors = require('../../lib/helpers/errors')
|
|
9
9
|
const prompts = require('../../lib/helpers/prompts')
|
|
10
10
|
const Session = require('../../db/session')
|
|
11
|
-
const normalizeArmorAliases = require('./normalizeArmorAliases')
|
|
12
11
|
|
|
13
12
|
async function set (key, value) {
|
|
14
|
-
const options =
|
|
13
|
+
const options = this.opts()
|
|
15
14
|
|
|
16
15
|
let encrypt = true
|
|
17
16
|
let settingMessage = 'encrypting'
|
|
@@ -65,7 +64,7 @@ async function set (key, value) {
|
|
|
65
64
|
const fk = options.envKeysFile || '.env.keys'
|
|
66
65
|
const noCreate = options.create === false
|
|
67
66
|
const noArmor = options.armor === false || (!options.token && (await sesh.noArmor()))
|
|
68
|
-
const noKeychain = options.
|
|
67
|
+
const noKeychain = options.native === false || options.noNative === true
|
|
69
68
|
|
|
70
69
|
let errorCount = 0
|
|
71
70
|
|
|
@@ -3,7 +3,7 @@ const Session = require('./../../db/session')
|
|
|
3
3
|
|
|
4
4
|
function configureArmorCommand (armor) {
|
|
5
5
|
armor
|
|
6
|
-
.description('move private keys
|
|
6
|
+
.description('move private keys into Dotenvx Armor [www.dotenvx.com/armor]')
|
|
7
7
|
.allowUnknownOption()
|
|
8
8
|
.argument('[command]', 'dotenvx-armor command')
|
|
9
9
|
.argument('[args...]', 'dotenvx-armor command arguments')
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
function configureLockCommand (lock) {
|
|
2
|
+
lock
|
|
3
|
+
.description('lock private keys with a local passphrase')
|
|
4
|
+
.action(function () {
|
|
5
|
+
this.help()
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
const upAction = require('./../actions/lock/up')
|
|
9
|
+
lock
|
|
10
|
+
.command('up')
|
|
11
|
+
.description('lock key in .env.keys')
|
|
12
|
+
.option('-f, --env-file <path>', 'path to your env file')
|
|
13
|
+
.option('-fk, --env-keys-file <path>', 'path to your .env.keys file', '.env.keys')
|
|
14
|
+
.action(upAction)
|
|
15
|
+
|
|
16
|
+
const downAction = require('./../actions/lock/down')
|
|
17
|
+
lock
|
|
18
|
+
.command('down')
|
|
19
|
+
.description('unlock key in .env.keys')
|
|
20
|
+
.option('-f, --env-file <path>', 'path to your env file')
|
|
21
|
+
.option('-fk, --env-keys-file <path>', 'path to your .env.keys file', '.env.keys')
|
|
22
|
+
.action(downAction)
|
|
23
|
+
|
|
24
|
+
return lock
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = configureLockCommand
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
function
|
|
2
|
-
|
|
3
|
-
.description('
|
|
1
|
+
function configureNativeCommand (native) {
|
|
2
|
+
native
|
|
3
|
+
.description('move private keys into your OS secret store (macOS Keychain supported)')
|
|
4
4
|
.action(function () {
|
|
5
5
|
this.help()
|
|
6
6
|
})
|
|
7
7
|
|
|
8
8
|
const upAction = require('./../actions/keychain/up')
|
|
9
|
-
|
|
9
|
+
native
|
|
10
10
|
.command('up')
|
|
11
|
-
.description('store key in
|
|
11
|
+
.description('store key in OS secret store')
|
|
12
12
|
.option('-f, --env-file <path>', 'path to your env file')
|
|
13
13
|
.option('-fk, --env-keys-file <path>', 'path to your .env.keys file', '.env.keys')
|
|
14
14
|
.action(upAction)
|
|
15
15
|
|
|
16
16
|
const downAction = require('./../actions/keychain/down')
|
|
17
|
-
|
|
17
|
+
native
|
|
18
18
|
.command('down')
|
|
19
|
-
.description('move key from
|
|
19
|
+
.description('move key from OS secret store to .env.keys')
|
|
20
20
|
.option('-f, --env-file <path>', 'path to your env file')
|
|
21
21
|
.option('-fk, --env-keys-file <path>', 'path to your .env.keys file', '.env.keys')
|
|
22
22
|
.action(downAction)
|
|
23
23
|
|
|
24
24
|
const pushAction = require('./../actions/keychain/push')
|
|
25
|
-
|
|
25
|
+
native
|
|
26
26
|
.command('push')
|
|
27
|
-
.description('push key to
|
|
27
|
+
.description('push key to OS secret store from .env.keys')
|
|
28
28
|
.option('-f, --env-file <path>', 'path to your env file')
|
|
29
29
|
.option('-fk, --env-keys-file <path>', 'path to your .env.keys file', '.env.keys')
|
|
30
30
|
.action(pushAction)
|
|
31
31
|
|
|
32
32
|
const pullAction = require('./../actions/keychain/pull')
|
|
33
|
-
|
|
33
|
+
native
|
|
34
34
|
.command('pull')
|
|
35
|
-
.description('pull key from
|
|
35
|
+
.description('pull key from OS secret store into .env.keys')
|
|
36
36
|
.option('-f, --env-file <path>', 'path to your env file')
|
|
37
37
|
.option('-fk, --env-keys-file <path>', 'path to your .env.keys file', '.env.keys')
|
|
38
38
|
.action(pullAction)
|
|
39
39
|
|
|
40
|
-
return
|
|
40
|
+
return native
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
module.exports =
|
|
43
|
+
module.exports = configureNativeCommand
|
package/src/cli/dotenvx.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/* c8 ignore start */
|
|
4
|
-
const { Command
|
|
4
|
+
const { Command } = require('commander')
|
|
5
5
|
const program = new Command()
|
|
6
6
|
|
|
7
7
|
const { setLogLevel, logger } = require('../shared/logger')
|
|
@@ -73,8 +73,7 @@ program.command('run')
|
|
|
73
73
|
.option('--ignore <errorCodes...>', 'error code(s) to ignore (example: --ignore=MISSING_ENV_FILE)')
|
|
74
74
|
.option('--token <token>', 'set Armor ⛨ token')
|
|
75
75
|
.option('--no-armor', 'disable Dotenvx Armor features')
|
|
76
|
-
.option('--no-
|
|
77
|
-
.addOption(new Option('--no-ops', 'disable dotenvx-ops features (deprecated. use --no-armor)').hideHelp())
|
|
76
|
+
.option('--no-native', 'disable OS secret store features')
|
|
78
77
|
.action(function (...args) {
|
|
79
78
|
this.envs = envs
|
|
80
79
|
return require('./actions/run').apply(this, args)
|
|
@@ -97,8 +96,7 @@ program.command('get')
|
|
|
97
96
|
.option('--pp', 'pretty print output (alias)')
|
|
98
97
|
.option('--format <type>', 'format of the output (json, shell, colon, eval)', 'json')
|
|
99
98
|
.option('--no-armor', 'disable Dotenvx Armor features')
|
|
100
|
-
.option('--no-
|
|
101
|
-
.addOption(new Option('--no-ops', 'disable dotenvx-ops features (deprecated. use --no-armor)').hideHelp())
|
|
99
|
+
.option('--no-native', 'disable OS secret store features')
|
|
102
100
|
.action(function (...args) {
|
|
103
101
|
this.envs = envs
|
|
104
102
|
return require('./actions/get').apply(this, args)
|
|
@@ -118,8 +116,7 @@ program.command('set')
|
|
|
118
116
|
.option('-p, --plain', 'store value as plain text', false)
|
|
119
117
|
.option('--no-create', 'do not create .env file(s) when missing')
|
|
120
118
|
.option('--no-armor', 'disable Dotenvx Armor features')
|
|
121
|
-
.option('--no-
|
|
122
|
-
.addOption(new Option('--no-ops', 'disable dotenvx-ops features (deprecated. use --no-armor)').hideHelp())
|
|
119
|
+
.option('--no-native', 'disable OS secret store features')
|
|
123
120
|
.action(function (...args) {
|
|
124
121
|
this.envs = envs
|
|
125
122
|
return require('./actions/set').apply(this, args)
|
|
@@ -136,8 +133,7 @@ program.command('encrypt')
|
|
|
136
133
|
.option('--token <token>', 'set Armor ⛨ token')
|
|
137
134
|
.option('--no-create', 'do not create .env file(s) when missing')
|
|
138
135
|
.option('--no-armor', 'disable Dotenvx Armor features')
|
|
139
|
-
.option('--no-
|
|
140
|
-
.addOption(new Option('--no-ops', 'disable dotenvx-ops features (deprecated. use --no-armor)').hideHelp())
|
|
136
|
+
.option('--no-native', 'disable OS secret store features')
|
|
141
137
|
.action(function (...args) {
|
|
142
138
|
this.envs = envs
|
|
143
139
|
return require('./actions/encrypt').apply(this, args)
|
|
@@ -151,8 +147,7 @@ program.command('decrypt')
|
|
|
151
147
|
.option('-k, --key <keys...>', 'keys(s) to decrypt (default: all keys in file)')
|
|
152
148
|
.option('-ek, --exclude-key <excludeKeys...>', 'keys(s) to exclude from decryption (default: none)')
|
|
153
149
|
.option('--no-armor', 'disable Dotenvx Armor features')
|
|
154
|
-
.option('--no-
|
|
155
|
-
.addOption(new Option('--no-ops', 'disable dotenvx-ops features (deprecated. use --no-armor)').hideHelp())
|
|
150
|
+
.option('--no-native', 'disable OS secret store features')
|
|
156
151
|
.option('--stdout', 'send to stdout')
|
|
157
152
|
.action(function (...args) {
|
|
158
153
|
this.envs = envs
|
|
@@ -167,8 +162,7 @@ program.command('keypair')
|
|
|
167
162
|
.option('-f, --env-file <path>', 'path(s) to your env file(s)')
|
|
168
163
|
.option('-fk, --env-keys-file <path>', 'path to your .env.keys file (default: same path as your env file)')
|
|
169
164
|
.option('--no-armor', 'disable Dotenvx Armor features')
|
|
170
|
-
.option('--no-
|
|
171
|
-
.addOption(new Option('--no-ops', 'disable dotenvx-ops features (deprecated. use --no-armor)').hideHelp())
|
|
165
|
+
.option('--no-native', 'disable OS secret store features')
|
|
172
166
|
.option('-pp, --pretty-print', 'pretty print output')
|
|
173
167
|
.option('--pp', 'pretty print output (alias)')
|
|
174
168
|
.option('--format <type>', 'format of the output (json, shell, colon)', 'json')
|
|
@@ -268,11 +262,15 @@ program.command('help [command]')
|
|
|
268
262
|
// dotenvx armor
|
|
269
263
|
program.addHelpText('after', ' ')
|
|
270
264
|
program.addHelpText('after', 'Professional Security: ')
|
|
271
|
-
program.addHelpText('after', '
|
|
265
|
+
program.addHelpText('after', ' lock ⊡ lock private keys with a local passphrase')
|
|
266
|
+
program.addHelpText('after', ' native ⌥ move private keys into your OS secret store (macOS Keychain supported)')
|
|
272
267
|
program.addHelpText('after', ' armor ⛨ move private keys into Dotenvx Armor [www.dotenvx.com/armor]')
|
|
273
268
|
|
|
274
|
-
// dotenvx
|
|
275
|
-
require('./commands/
|
|
269
|
+
// dotenvx native
|
|
270
|
+
require('./commands/native')(program.command('native', { hidden: true }))
|
|
271
|
+
|
|
272
|
+
// dotenvx lock
|
|
273
|
+
require('./commands/lock')(program.command('lock', { hidden: true }))
|
|
276
274
|
|
|
277
275
|
// dotenvx armor
|
|
278
276
|
require('./commands/armor')(program.command('armor', { hidden: true }))
|
|
@@ -7,6 +7,7 @@ const ISSUE_BY_CODE = {
|
|
|
7
7
|
DANGEROUS_DEPENDENCY_HOIST: 'https://github.com/dotenvx/dotenvx/issues/622',
|
|
8
8
|
INVALID_COLOR: 'must be 256 colors',
|
|
9
9
|
INVALID_CONVENTION: 'https://github.com/dotenvx/dotenvx/issues/761',
|
|
10
|
+
INVALID_PASSPHRASE: 'try again with the correct passphrase',
|
|
10
11
|
INVALID_PRIVATE_KEY: 'https://github.com/dotenvx/dotenvx/issues/465',
|
|
11
12
|
INVALID_PUBLIC_KEY: 'https://github.com/dotenvx/dotenvx/issues/756',
|
|
12
13
|
MALFORMED_ENCRYPTED_DATA: 'https://github.com/dotenvx/dotenvx/issues/467',
|
|
@@ -157,6 +158,18 @@ class Errors {
|
|
|
157
158
|
return e
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
invalidPassphrase () {
|
|
162
|
+
const code = 'INVALID_PASSPHRASE'
|
|
163
|
+
const message = `[${code}] could not unlock ${this.privateKeyName || 'DOTENV_PRIVATE_KEY'} using passphrase`
|
|
164
|
+
const help = `fix: [${ISSUE_BY_CODE[code]}]`
|
|
165
|
+
|
|
166
|
+
const e = new Error(message)
|
|
167
|
+
e.code = code
|
|
168
|
+
e.help = help
|
|
169
|
+
e.messageWithHelp = `${message}. ${help}`
|
|
170
|
+
return e
|
|
171
|
+
}
|
|
172
|
+
|
|
160
173
|
malformedEncryptedData () {
|
|
161
174
|
const code = 'MALFORMED_ENCRYPTED_DATA'
|
|
162
175
|
const message = `[${code}] could not decrypt ${this.key} because encrypted data appears malformed`
|
|
@@ -42,20 +42,6 @@ function armorBanner () {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function dynamicAttempts (command, forwardedArgs) {
|
|
45
|
-
if (command === 'vlt') {
|
|
46
|
-
return [
|
|
47
|
-
['dotenvx-armor', forwardedArgs],
|
|
48
|
-
['dotenvx-ops', forwardedArgs]
|
|
49
|
-
]
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (command === 'ops') {
|
|
53
|
-
return [
|
|
54
|
-
['dotenvx-armor', forwardedArgs],
|
|
55
|
-
['dotenvx-ops', forwardedArgs]
|
|
56
|
-
]
|
|
57
|
-
}
|
|
58
|
-
|
|
59
45
|
return [[`dotenvx-${command}`, forwardedArgs]]
|
|
60
46
|
}
|
|
61
47
|
|
|
@@ -85,11 +71,7 @@ function executeDynamic (program, command, rawArgs) {
|
|
|
85
71
|
}
|
|
86
72
|
|
|
87
73
|
if (result.error) {
|
|
88
|
-
if (command === '
|
|
89
|
-
console.log(armorBanner())
|
|
90
|
-
} else if (command === 'ops') {
|
|
91
|
-
console.log(armorBanner())
|
|
92
|
-
} else if (command === 'armor') {
|
|
74
|
+
if (command === 'armor') {
|
|
93
75
|
console.log(armorBanner())
|
|
94
76
|
} else {
|
|
95
77
|
logger.info(`error: unknown command '${command}'`)
|
package/src/lib/main.d.ts
CHANGED
|
@@ -180,20 +180,12 @@ export interface DotenvConfigOptions {
|
|
|
180
180
|
noArmor?: boolean;
|
|
181
181
|
|
|
182
182
|
/**
|
|
183
|
-
* Turn off
|
|
183
|
+
* Turn off OS secret store features.
|
|
184
184
|
*
|
|
185
185
|
* @default false
|
|
186
|
-
* @example require('@dotenvx/dotenvx').config({
|
|
186
|
+
* @example require('@dotenvx/dotenvx').config({ noNative: true })
|
|
187
187
|
*/
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Turn off Dotenvx Armor features. Alias for `noArmor`.
|
|
192
|
-
*
|
|
193
|
-
* @default false
|
|
194
|
-
* @example require('@dotenvx/dotenvx').config({ noOps: true })
|
|
195
|
-
*/
|
|
196
|
-
noOps?: boolean;
|
|
188
|
+
noNative?: boolean;
|
|
197
189
|
|
|
198
190
|
}
|
|
199
191
|
|
|
@@ -271,20 +263,12 @@ export interface SetOptions {
|
|
|
271
263
|
noArmor?: boolean;
|
|
272
264
|
|
|
273
265
|
/**
|
|
274
|
-
* Turn off
|
|
266
|
+
* Turn off OS secret store features.
|
|
275
267
|
*
|
|
276
268
|
* @default false
|
|
277
|
-
* @example require('@dotenvx/dotenvx').set(key, value, {
|
|
269
|
+
* @example require('@dotenvx/dotenvx').set(key, value, { noNative: true })
|
|
278
270
|
*/
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
/**
|
|
282
|
-
* Turn off Dotenvx Armor features. Alias for `noArmor`.
|
|
283
|
-
*
|
|
284
|
-
* @default false
|
|
285
|
-
* @example require('@dotenvx/dotenvx').set(key, value, { noOps: true })
|
|
286
|
-
*/
|
|
287
|
-
noOps?: boolean;
|
|
271
|
+
noNative?: boolean;
|
|
288
272
|
|
|
289
273
|
}
|
|
290
274
|
|
|
@@ -364,20 +348,12 @@ export interface GetOptions {
|
|
|
364
348
|
noArmor?: boolean;
|
|
365
349
|
|
|
366
350
|
/**
|
|
367
|
-
* Turn off
|
|
368
|
-
*
|
|
369
|
-
* @default false
|
|
370
|
-
* @example require('@dotenvx/dotenvx').get('KEY', { noKeychain: true })
|
|
371
|
-
*/
|
|
372
|
-
noKeychain?: boolean;
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Turn off Dotenvx Armor features. Alias for `noArmor`.
|
|
351
|
+
* Turn off OS secret store features.
|
|
376
352
|
*
|
|
377
353
|
* @default false
|
|
378
|
-
* @example require('@dotenvx/dotenvx').get('KEY', {
|
|
354
|
+
* @example require('@dotenvx/dotenvx').get('KEY', { noNative: true })
|
|
379
355
|
*/
|
|
380
|
-
|
|
356
|
+
noNative?: boolean;
|
|
381
357
|
|
|
382
358
|
}
|
|
383
359
|
|
package/src/lib/main.js
CHANGED
|
@@ -380,11 +380,11 @@ const ls = function (directory, envFile, excludeEnvFile) {
|
|
|
380
380
|
|
|
381
381
|
function resolveNoArmor (options = {}) {
|
|
382
382
|
const sesh = new Session()
|
|
383
|
-
return options.noArmor === true ||
|
|
383
|
+
return options.noArmor === true || (!options.token && sesh.noArmorSync())
|
|
384
384
|
}
|
|
385
385
|
|
|
386
386
|
function resolveNoKeychain (options = {}) {
|
|
387
|
-
return options.
|
|
387
|
+
return options.noNative === true || options.native === false
|
|
388
388
|
}
|
|
389
389
|
|
|
390
390
|
module.exports = {
|
|
@@ -395,7 +395,7 @@ module.exports = {
|
|
|
395
395
|
set,
|
|
396
396
|
get,
|
|
397
397
|
ls,
|
|
398
|
-
// expose for libs depending on @dotenvx/dotenvx
|
|
398
|
+
// expose for libs depending on @dotenvx/dotenvx
|
|
399
399
|
setLogLevel,
|
|
400
400
|
logger,
|
|
401
401
|
getColor,
|
|
@@ -46,7 +46,7 @@ function armorProviderForOptions (options) {
|
|
|
46
46
|
function useKeychain (options) {
|
|
47
47
|
if (process.platform !== 'darwin') return false
|
|
48
48
|
if (process.env.CI) return false
|
|
49
|
-
return options.
|
|
49
|
+
return options.noNative !== true && options.native !== false && options.noKeychain !== true
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
function useArmor (options, noArmor) {
|
|
@@ -29,7 +29,7 @@ class KeychainPull {
|
|
|
29
29
|
try {
|
|
30
30
|
privateKey = execFileSync(SECURITY_BIN, ['find-generic-password', '-s', SERVICE, '-a', publicKey, '-w'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim()
|
|
31
31
|
} catch {
|
|
32
|
-
throw new Error(`[NOT_FOUND] private key not found in macOS Keychain (${armoredKeyDisplay(publicKey)}). fix: [dotenvx
|
|
32
|
+
throw new Error(`[NOT_FOUND] private key not found in macOS Keychain (${armoredKeyDisplay(publicKey)}). fix: [dotenvx native up]`)
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
const result = upsertEnvKey(privateKeyName, privateKey, envKeysFile)
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const { derive, publickeys, scan } = require('@dotenvx/primitives')
|
|
3
|
+
|
|
4
|
+
const Errors = require('../helpers/errors')
|
|
5
|
+
const upsertEnvKey = require('../helpers/upsertEnvKey')
|
|
6
|
+
|
|
7
|
+
function latestValues (parsed) {
|
|
8
|
+
const result = {}
|
|
9
|
+
|
|
10
|
+
for (const [key, values] of Object.entries(parsed)) {
|
|
11
|
+
result[key] = values[values.length - 1]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return result
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function lockedCandidates (keysSrc) {
|
|
18
|
+
const { parsed } = scan(keysSrc)
|
|
19
|
+
const values = latestValues(parsed)
|
|
20
|
+
const candidates = []
|
|
21
|
+
|
|
22
|
+
for (const [privateKeyName, privateKey] of Object.entries(values)) {
|
|
23
|
+
if (!privateKeyName.startsWith('DOTENV_PRIVATE_KEY')) continue
|
|
24
|
+
if (!privateKey.startsWith('locked:')) continue
|
|
25
|
+
|
|
26
|
+
const parts = privateKey.split(':')
|
|
27
|
+
if (parts.length < 3) continue
|
|
28
|
+
|
|
29
|
+
candidates.push({
|
|
30
|
+
privateKeyName,
|
|
31
|
+
lockedPrivateKeyValue: privateKey,
|
|
32
|
+
publicKeyValue: parts[1]
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return candidates
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function privateKeyring (keysSrc) {
|
|
40
|
+
const { parsed } = scan(keysSrc)
|
|
41
|
+
const values = latestValues(parsed)
|
|
42
|
+
const keyring = {}
|
|
43
|
+
|
|
44
|
+
for (const [privateKeyName, privateKey] of Object.entries(values)) {
|
|
45
|
+
if (!privateKeyName.startsWith('DOTENV_PRIVATE_KEY')) continue
|
|
46
|
+
if (privateKey.startsWith('locked:')) continue
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const publicKey = derive(privateKey)
|
|
50
|
+
keyring[publicKey] = { privateKeyName, privateKeyValue: privateKey }
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return keyring
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class LockDown {
|
|
58
|
+
constructor (envFile = '.env', envKeysFile = '.env.keys') {
|
|
59
|
+
this.envFile = envFile
|
|
60
|
+
this.envKeysFile = envKeysFile
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
lockedCandidates () {
|
|
64
|
+
const keysSrc = fs.readFileSync(this.envKeysFile, 'utf8')
|
|
65
|
+
const candidates = lockedCandidates(keysSrc)
|
|
66
|
+
|
|
67
|
+
if (candidates.length < 1) {
|
|
68
|
+
throw new Errors({ key: 'DOTENV_PRIVATE_KEY' }).missingKey()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return candidates
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
plan () {
|
|
75
|
+
const envSrc = fs.readFileSync(this.envFile, 'utf8')
|
|
76
|
+
const keysSrc = fs.readFileSync(this.envKeysFile, 'utf8')
|
|
77
|
+
const publicKeyValues = publickeys(envSrc)
|
|
78
|
+
const lockedByPublicKey = {}
|
|
79
|
+
const plaintextByPublicKey = privateKeyring(keysSrc)
|
|
80
|
+
const locked = []
|
|
81
|
+
const alreadyUnlocked = []
|
|
82
|
+
|
|
83
|
+
for (const lockedPrivateKey of lockedCandidates(keysSrc)) {
|
|
84
|
+
lockedByPublicKey[lockedPrivateKey.publicKeyValue] = lockedPrivateKey
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const publicKey of publicKeyValues) {
|
|
88
|
+
const lockedPrivateKey = lockedByPublicKey[publicKey]
|
|
89
|
+
if (lockedPrivateKey) {
|
|
90
|
+
locked.push(lockedPrivateKey)
|
|
91
|
+
continue
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const privateKey = plaintextByPublicKey[publicKey]
|
|
95
|
+
if (privateKey) {
|
|
96
|
+
alreadyUnlocked.push({
|
|
97
|
+
changed: false,
|
|
98
|
+
privateKeyName: privateKey.privateKeyName,
|
|
99
|
+
privateKeyValue: privateKey.privateKeyValue,
|
|
100
|
+
lockedPrivateKeyValue: undefined,
|
|
101
|
+
publicKeyValue: publicKey
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (locked.length < 1 && alreadyUnlocked.length < 1) {
|
|
107
|
+
throw new Errors({ key: 'DOTENV_PRIVATE_KEY' }).missingKey()
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return { locked, alreadyUnlocked }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
run (unlockPrivateKey) {
|
|
114
|
+
const { locked, alreadyUnlocked } = this.plan()
|
|
115
|
+
const results = [...alreadyUnlocked]
|
|
116
|
+
|
|
117
|
+
if (locked.length > 0 && !unlockPrivateKey) {
|
|
118
|
+
throw new Error('missing unlock private key function')
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
for (const lockedPrivateKey of locked) {
|
|
122
|
+
let privateKeyValue
|
|
123
|
+
|
|
124
|
+
try {
|
|
125
|
+
privateKeyValue = unlockPrivateKey(lockedPrivateKey.lockedPrivateKeyValue)
|
|
126
|
+
} catch (error) {
|
|
127
|
+
throw new Errors({ privateKeyName: lockedPrivateKey.privateKeyName }).invalidPassphrase()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const publicKeyValue = derive(privateKeyValue)
|
|
131
|
+
|
|
132
|
+
if (publicKeyValue !== lockedPrivateKey.publicKeyValue) continue
|
|
133
|
+
|
|
134
|
+
const result = upsertEnvKey(lockedPrivateKey.privateKeyName, privateKeyValue, this.envKeysFile)
|
|
135
|
+
|
|
136
|
+
results.push({
|
|
137
|
+
changed: result.changed,
|
|
138
|
+
privateKeyName: lockedPrivateKey.privateKeyName,
|
|
139
|
+
privateKeyValue,
|
|
140
|
+
lockedPrivateKeyValue: lockedPrivateKey.lockedPrivateKeyValue,
|
|
141
|
+
publicKeyValue
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (results.length < 1) {
|
|
146
|
+
throw new Errors({ key: 'DOTENV_PRIVATE_KEY' }).missingKey()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
changed: results.some(result => result.changed),
|
|
151
|
+
results
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = LockDown
|
|
157
|
+
module.exports.lockedCandidates = lockedCandidates
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const { derive, publickeys, scan } = require('@dotenvx/primitives')
|
|
3
|
+
|
|
4
|
+
const Errors = require('../helpers/errors')
|
|
5
|
+
const upsertEnvKey = require('../helpers/upsertEnvKey')
|
|
6
|
+
|
|
7
|
+
function latestValues (parsed) {
|
|
8
|
+
const result = {}
|
|
9
|
+
|
|
10
|
+
for (const [key, values] of Object.entries(parsed)) {
|
|
11
|
+
result[key] = values[values.length - 1]
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return result
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function privateKeyring (keysSrc) {
|
|
18
|
+
const { parsed } = scan(keysSrc)
|
|
19
|
+
const values = latestValues(parsed)
|
|
20
|
+
const keyring = {}
|
|
21
|
+
|
|
22
|
+
for (const [privateKeyName, privateKey] of Object.entries(values)) {
|
|
23
|
+
if (!privateKeyName.startsWith('DOTENV_PRIVATE_KEY')) continue
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const publicKey = derive(privateKey)
|
|
27
|
+
keyring[publicKey] = { privateKeyName, privateKeyValue: privateKey }
|
|
28
|
+
} catch {}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return keyring
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function lockedKeyring (keysSrc) {
|
|
35
|
+
const { parsed } = scan(keysSrc)
|
|
36
|
+
const values = latestValues(parsed)
|
|
37
|
+
const keyring = {}
|
|
38
|
+
|
|
39
|
+
for (const [privateKeyName, privateKey] of Object.entries(values)) {
|
|
40
|
+
if (!privateKeyName.startsWith('DOTENV_PRIVATE_KEY')) continue
|
|
41
|
+
if (!privateKey.startsWith('locked:')) continue
|
|
42
|
+
|
|
43
|
+
const parts = privateKey.split(':')
|
|
44
|
+
if (parts.length < 3) continue
|
|
45
|
+
|
|
46
|
+
const publicKey = parts[1]
|
|
47
|
+
keyring[publicKey] = {
|
|
48
|
+
privateKeyName,
|
|
49
|
+
lockedPrivateKeyValue: privateKey,
|
|
50
|
+
publicKeyValue: publicKey
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return keyring
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
class LockUp {
|
|
58
|
+
constructor (envFile = '.env', envKeysFile = '.env.keys') {
|
|
59
|
+
this.envFile = envFile
|
|
60
|
+
this.envKeysFile = envKeysFile
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
plan () {
|
|
64
|
+
const envFile = this.envFile
|
|
65
|
+
const envKeysFile = this.envKeysFile
|
|
66
|
+
|
|
67
|
+
const envSrc = fs.readFileSync(envFile, 'utf8')
|
|
68
|
+
const keysSrc = fs.readFileSync(envKeysFile, 'utf8')
|
|
69
|
+
const ring = privateKeyring(keysSrc)
|
|
70
|
+
const lockedRing = lockedKeyring(keysSrc)
|
|
71
|
+
const matches = []
|
|
72
|
+
const alreadyLocked = []
|
|
73
|
+
|
|
74
|
+
for (const publicKey of publickeys(envSrc)) {
|
|
75
|
+
const privateKey = ring[publicKey]
|
|
76
|
+
if (privateKey) {
|
|
77
|
+
matches.push({
|
|
78
|
+
privateKeyName: privateKey.privateKeyName,
|
|
79
|
+
privateKeyValue: privateKey.privateKeyValue,
|
|
80
|
+
publicKeyValue: publicKey
|
|
81
|
+
})
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const lockedPrivateKey = lockedRing[publicKey]
|
|
86
|
+
if (lockedPrivateKey) {
|
|
87
|
+
alreadyLocked.push(lockedPrivateKey)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (matches.length < 1 && alreadyLocked.length < 1) {
|
|
92
|
+
throw new Errors({ key: 'DOTENV_PRIVATE_KEY' }).missingKey()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { matches, alreadyLocked }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
matches () {
|
|
99
|
+
const { matches } = this.plan()
|
|
100
|
+
|
|
101
|
+
if (matches.length < 1) {
|
|
102
|
+
throw new Errors({ key: 'DOTENV_PRIVATE_KEY' }).missingKey()
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return matches
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
run (lockPrivateKey) {
|
|
109
|
+
if (!lockPrivateKey) {
|
|
110
|
+
throw new Error('missing lock private key function')
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const envKeysFile = this.envKeysFile
|
|
114
|
+
const results = []
|
|
115
|
+
|
|
116
|
+
const { matches, alreadyLocked } = this.plan()
|
|
117
|
+
|
|
118
|
+
for (const privateKey of matches) {
|
|
119
|
+
const lockedPrivateKey = lockPrivateKey(privateKey.privateKeyValue, privateKey.publicKeyValue)
|
|
120
|
+
const result = upsertEnvKey(privateKey.privateKeyName, lockedPrivateKey, envKeysFile)
|
|
121
|
+
|
|
122
|
+
results.push({
|
|
123
|
+
changed: result.changed,
|
|
124
|
+
privateKeyName: privateKey.privateKeyName,
|
|
125
|
+
privateKeyValue: privateKey.privateKeyValue,
|
|
126
|
+
lockedPrivateKeyValue: lockedPrivateKey,
|
|
127
|
+
publicKeyValue: privateKey.publicKeyValue
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
for (const lockedPrivateKey of alreadyLocked) {
|
|
132
|
+
results.push({
|
|
133
|
+
changed: false,
|
|
134
|
+
privateKeyName: lockedPrivateKey.privateKeyName,
|
|
135
|
+
privateKeyValue: undefined,
|
|
136
|
+
lockedPrivateKeyValue: lockedPrivateKey.lockedPrivateKeyValue,
|
|
137
|
+
publicKeyValue: lockedPrivateKey.publicKeyValue,
|
|
138
|
+
alreadyLocked: true
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
changed: results.some(result => result.changed),
|
|
144
|
+
results
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = LockUp
|
|
150
|
+
module.exports.privateKeyring = privateKeyring
|