@dotenvx/dotenvx 1.36.0 → 1.38.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 +16 -1
- package/README.md +127 -1
- package/package.json +1 -1
- package/src/cli/actions/rotate.js +83 -0
- package/src/cli/dotenvx.js +14 -0
- package/src/lib/helpers/append.js +61 -0
- package/src/lib/helpers/errors.js +12 -0
- package/src/lib/helpers/evalKeyValue.js +23 -0
- package/src/lib/helpers/parse.js +9 -11
- package/src/lib/services/rotate.js +160 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,22 @@
|
|
|
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/v1.
|
|
5
|
+
[Unreleased](https://github.com/dotenvx/dotenvx/compare/v1.38.0...main)
|
|
6
|
+
|
|
7
|
+
## [1.38.0](https://github.com/dotenvx/dotenvx/compare/v1.37.0...v1.38.0)
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
* Command substitution failures no longer halt further processing of keys in a .env file ([#533](https://github.com/dotenvx/dotenvx/pull/533))
|
|
12
|
+
* A more helpful error is raised if a command substitution failure occurs ([#533](https://github.com/dotenvx/dotenvx/pull/533))
|
|
13
|
+
|
|
14
|
+
## [1.37.0](https://github.com/dotenvx/dotenvx/compare/v1.36.0...v1.37.0)
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
* add `dotenvx rotate` command 🎉 ([#530](https://github.com/dotenvx/dotenvx/pull/530))
|
|
19
|
+
|
|
20
|
+
also: [our whitepaper](https://dotenvx.com/dotenvx.pdf) is released as a draft.
|
|
6
21
|
|
|
7
22
|
## [1.36.0](https://github.com/dotenvx/dotenvx/compare/v1.35.0...v1.36.0)
|
|
8
23
|
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* multi-environment
|
|
7
7
|
* encrypted envs
|
|
8
8
|
|
|
9
|
-
[Read the whitepaper](https://dotenvx.com/dotenvx.pdf)
|
|
9
|
+
[Read the whitepaper](https://dotenvx.com/dotenvx.pdf?v=README)
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
|
|
@@ -1685,7 +1685,117 @@ More examples
|
|
|
1685
1685
|
```
|
|
1686
1686
|
|
|
1687
1687
|
</details>
|
|
1688
|
+
* <details><summary>`rotate`</summary><br>
|
|
1688
1689
|
|
|
1690
|
+
Rotate public/private keys for `.env` file and re-encrypt all encrypted values.
|
|
1691
|
+
|
|
1692
|
+
```sh
|
|
1693
|
+
$ echo "HELLO=World" > .env
|
|
1694
|
+
$ dotenvx encrypt
|
|
1695
|
+
✔ encrypted (.env)
|
|
1696
|
+
$ dotenvx rotate
|
|
1697
|
+
✔ rotated (.env)
|
|
1698
|
+
```
|
|
1699
|
+
|
|
1700
|
+
</details>
|
|
1701
|
+
* <details><summary>`rotate -f`</summary><br>
|
|
1702
|
+
|
|
1703
|
+
Rotate public/private keys for a specified encrypted `.env` file and re-encrypt all encrypted values.
|
|
1704
|
+
|
|
1705
|
+
```sh
|
|
1706
|
+
$ echo "HELLO=World" > .env
|
|
1707
|
+
$ echo "HELLO=Production" > .env.production
|
|
1708
|
+
|
|
1709
|
+
$ dotenvx encrypt -f .env.production
|
|
1710
|
+
✔ encrypted (.env.production)
|
|
1711
|
+
$ dotenvx rotate -f .env.production
|
|
1712
|
+
✔ rotated (.env.production)
|
|
1713
|
+
```
|
|
1714
|
+
|
|
1715
|
+
</details>
|
|
1716
|
+
* <details><summary>`rotate -fk`</summary><br>
|
|
1717
|
+
|
|
1718
|
+
Specify path to `.env.keys`. This is useful with monorepos.
|
|
1719
|
+
|
|
1720
|
+
```sh
|
|
1721
|
+
$ mkdir -p apps/app1
|
|
1722
|
+
$ echo "HELLO=World" > apps/app1/.env
|
|
1723
|
+
|
|
1724
|
+
$ dotenvx encrypt -fk .env.keys -f apps/app1/.env
|
|
1725
|
+
✔ encrypted (apps/app1/.env)
|
|
1726
|
+
$ dotenvx rotate -fk .env.keys -f apps/app1/.env
|
|
1727
|
+
✔ rotated (apps/app1/.env)
|
|
1728
|
+
```
|
|
1729
|
+
|
|
1730
|
+
</details>
|
|
1731
|
+
* <details><summary>`rotate -k`</summary><br>
|
|
1732
|
+
|
|
1733
|
+
Rotate the contents of a specified key inside an encrypted `.env` file.
|
|
1734
|
+
|
|
1735
|
+
```sh
|
|
1736
|
+
$ echo "HELLO=World\nHOLA=Mundo" > .env
|
|
1737
|
+
$ dotenvx encrypt
|
|
1738
|
+
✔ encrypted (.env)
|
|
1739
|
+
$ dotenvx rotate -k HELLO
|
|
1740
|
+
✔ rotated (.env)
|
|
1741
|
+
```
|
|
1742
|
+
|
|
1743
|
+
Even specify a glob pattern.
|
|
1744
|
+
|
|
1745
|
+
```sh
|
|
1746
|
+
$ echo "HELLO=World\nHOLA=Mundo" > .env
|
|
1747
|
+
$ dotenvx encrypt
|
|
1748
|
+
✔ encrypted (.env)
|
|
1749
|
+
$ dotenvx rotate -k "HE*"
|
|
1750
|
+
✔ rotated (.env)
|
|
1751
|
+
```
|
|
1752
|
+
|
|
1753
|
+
</details>
|
|
1754
|
+
* <details><summary>`rotate -ek`</summary><br>
|
|
1755
|
+
|
|
1756
|
+
Rotate the encrypted contents inside an encrypted `.env` file except for an exluded key.
|
|
1757
|
+
|
|
1758
|
+
```sh
|
|
1759
|
+
$ echo "HELLO=World\nHOLA=Mundo" > .env
|
|
1760
|
+
$ dotenvx encrypt
|
|
1761
|
+
✔ encrypted (.env)
|
|
1762
|
+
$ dotenvx rotate -ek HOLA
|
|
1763
|
+
✔ rotated (.env)
|
|
1764
|
+
```
|
|
1765
|
+
|
|
1766
|
+
Even specify a glob pattern.
|
|
1767
|
+
|
|
1768
|
+
```sh
|
|
1769
|
+
$ echo "HELLO=World\nHOLA=Mundo" > .env
|
|
1770
|
+
$ dotenvx encrypt
|
|
1771
|
+
✔ encrypted (.env)
|
|
1772
|
+
$ dotenvx rotate -ek "HO*"
|
|
1773
|
+
✔ rotated (.env)
|
|
1774
|
+
```
|
|
1775
|
+
|
|
1776
|
+
</details>
|
|
1777
|
+
* <details><summary>`rotate --stdout`</summary><br>
|
|
1778
|
+
|
|
1779
|
+
Rotate the contents of an encrypted `.env` file and send to stdout.
|
|
1780
|
+
|
|
1781
|
+
```sh
|
|
1782
|
+
$ dotenvx rotate --stdout
|
|
1783
|
+
#/-------------------[DOTENV_PUBLIC_KEY]--------------------/
|
|
1784
|
+
#/ public-key encryption for .env files /
|
|
1785
|
+
#/ [how it works](https://dotenvx.com/encryption) /
|
|
1786
|
+
#/----------------------------------------------------------/
|
|
1787
|
+
DOTENV_PUBLIC_KEY="034af93e93708b994c10f236c96ef88e47291066946cce2e8d98c9e02c741ced45"
|
|
1788
|
+
# .env
|
|
1789
|
+
HELLO="encrypted:12345"
|
|
1790
|
+
```
|
|
1791
|
+
|
|
1792
|
+
or send to a file:
|
|
1793
|
+
|
|
1794
|
+
```sh
|
|
1795
|
+
$ dotenvx rotate --stdout > somefile.txt
|
|
1796
|
+
```
|
|
1797
|
+
|
|
1798
|
+
</details>
|
|
1689
1799
|
* <details><summary>`help`</summary><br>
|
|
1690
1800
|
|
|
1691
1801
|
Output help for `dotenvx`.
|
|
@@ -2134,6 +2244,22 @@ More examples
|
|
|
2134
2244
|
|
|
2135
2245
|
## FAQ
|
|
2136
2246
|
|
|
2247
|
+
#### How does encryption work?
|
|
2248
|
+
|
|
2249
|
+
Dotenvx uses Elliptic Curve Integrated Encryption Scheme (ECIES) to encrypt each secret with a unique ephemeral key, while ensuring it can be decrypted using a long-term private key.
|
|
2250
|
+
|
|
2251
|
+
When you initialize encryption, a DOTENV_PUBLIC_KEY (encryption key) and DOTENV_PRIVATE_KEY (decryption key) are generated. The DOTENV_PUBLIC_KEY is used to encrypt secrets, and the DOTENV_PRIVATE_KEY is securely stored in your cloud secrets manager or .env.keys file.
|
|
2252
|
+
|
|
2253
|
+
Your encrypted .env file is then safely committed to code. Even if the file is exposed, secrets remain protected since decryption requires the separate DOTENV_PRIVATE_KEY, which is never stored alongside it. Read [the whitepaper](https://dotenvx.com/dotenvx.pdf?v=README) for more details.
|
|
2254
|
+
|
|
2255
|
+
#### Is it safe to commit an encrypted .env file to code?
|
|
2256
|
+
|
|
2257
|
+
Yes. Dotenvx encrypts secrets using AES-256 with ephemeral keys, ensuring that even if the encrypted .env file is exposed, its contents remain secure. The encryption keys themselves are protected using Secp256k1 elliptic curve cryptography, which is widely used for secure key exchange in technologies like Bitcoin.
|
|
2258
|
+
|
|
2259
|
+
This means that every secret in the .env file is encrypted with a unique AES-256 key, and that key is further encrypted using a public key (Secp256k1). Even if an attacker obtains the encrypted .env file, they would still need the corresponding private key—stored separately in a secrets manager—to decrypt anything.
|
|
2260
|
+
|
|
2261
|
+
Breaking this encryption would require brute-forcing both AES-256 and elliptic curve cryptography, which is computationally infeasible with current technology. Read [the whitepaper](https://dotenvx.com/dotenvx.pdf?v=README) for more details.
|
|
2262
|
+
|
|
2137
2263
|
#### Why am I getting the error `node: .env: not found`?
|
|
2138
2264
|
|
|
2139
2265
|
You are using Node 20 or greater and it adds a differing implementation of `--env-file` flag support. Rather than warn on a missing `.env` file (like dotenv has historically done), it raises an error: `node: .env: not found`.
|
package/package.json
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
const fsx = require('./../../lib/helpers/fsx')
|
|
2
|
+
const { logger } = require('./../../shared/logger')
|
|
3
|
+
|
|
4
|
+
const Rotate = require('./../../lib/services/rotate')
|
|
5
|
+
|
|
6
|
+
const catchAndLog = require('../../lib/helpers/catchAndLog')
|
|
7
|
+
const isIgnoringDotenvKeys = require('../../lib/helpers/isIgnoringDotenvKeys')
|
|
8
|
+
|
|
9
|
+
function rotate () {
|
|
10
|
+
const options = this.opts()
|
|
11
|
+
logger.debug(`options: ${JSON.stringify(options)}`)
|
|
12
|
+
|
|
13
|
+
const envs = this.envs
|
|
14
|
+
|
|
15
|
+
// stdout - should not have a try so that exit codes can surface to stdout
|
|
16
|
+
if (options.stdout) {
|
|
17
|
+
const {
|
|
18
|
+
processedEnvs
|
|
19
|
+
} = new Rotate(envs, options.key, options.excludeKey, options.envKeysFile).run()
|
|
20
|
+
|
|
21
|
+
for (const processedEnv of processedEnvs) {
|
|
22
|
+
console.log(processedEnv.envSrc)
|
|
23
|
+
console.log('')
|
|
24
|
+
console.log(processedEnv.envKeysSrc)
|
|
25
|
+
}
|
|
26
|
+
process.exit(0) // exit early
|
|
27
|
+
} else {
|
|
28
|
+
try {
|
|
29
|
+
const {
|
|
30
|
+
processedEnvs,
|
|
31
|
+
changedFilepaths,
|
|
32
|
+
unchangedFilepaths
|
|
33
|
+
} = new Rotate(envs, options.key, options.excludeKey, options.envKeysFile).run()
|
|
34
|
+
|
|
35
|
+
for (const processedEnv of processedEnvs) {
|
|
36
|
+
logger.verbose(`rotating ${processedEnv.envFilepath} (${processedEnv.filepath})`)
|
|
37
|
+
if (processedEnv.error) {
|
|
38
|
+
if (processedEnv.error.code === 'MISSING_ENV_FILE') {
|
|
39
|
+
logger.warn(processedEnv.error.message)
|
|
40
|
+
logger.help(`? add one with [echo "HELLO=World" > ${processedEnv.envFilepath}] and re-run [dotenvx rotate]`)
|
|
41
|
+
} else {
|
|
42
|
+
logger.warn(processedEnv.error.message)
|
|
43
|
+
if (processedEnv.error.help) {
|
|
44
|
+
logger.help(processedEnv.error.help)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} else if (processedEnv.changed) {
|
|
48
|
+
fsx.writeFileX(processedEnv.filepath, processedEnv.envSrc)
|
|
49
|
+
fsx.writeFileX(processedEnv.envKeysFilepath, processedEnv.envKeysSrc)
|
|
50
|
+
|
|
51
|
+
logger.verbose(`rotated ${processedEnv.envFilepath} (${processedEnv.filepath})`)
|
|
52
|
+
} else {
|
|
53
|
+
logger.verbose(`no changes ${processedEnv.envFilepath} (${processedEnv.filepath})`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (changedFilepaths.length > 0) {
|
|
58
|
+
logger.success(`✔ rotated (${changedFilepaths.join(',')})`)
|
|
59
|
+
} else if (unchangedFilepaths.length > 0) {
|
|
60
|
+
logger.info(`no changes (${unchangedFilepaths})`)
|
|
61
|
+
} else {
|
|
62
|
+
// do nothing - scenario when no .env files found
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
for (const processedEnv of processedEnvs) {
|
|
66
|
+
if (processedEnv.privateKeyAdded) {
|
|
67
|
+
logger.success(`✔ key added to .env.keys (${processedEnv.privateKeyName})`)
|
|
68
|
+
|
|
69
|
+
if (!isIgnoringDotenvKeys()) {
|
|
70
|
+
logger.help('⮕ next run [dotenvx ext gitignore --pattern .env.keys] to gitignore .env.keys')
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
logger.help(`⮕ next run [${processedEnv.privateKeyName}='${processedEnv.privateKey}' dotenvx get] to test decryption locally`)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
} catch (error) {
|
|
77
|
+
catchAndLog(error)
|
|
78
|
+
process.exit(1)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = rotate
|
package/src/cli/dotenvx.js
CHANGED
|
@@ -156,6 +156,20 @@ program.command('ls')
|
|
|
156
156
|
.option('-ef, --exclude-env-file <excludeFilenames...>', 'path(s) to exclude from your env file(s) (default: none)')
|
|
157
157
|
.action(lsAction)
|
|
158
158
|
|
|
159
|
+
// dotenvx rotate
|
|
160
|
+
const rotateAction = require('./actions/rotate')
|
|
161
|
+
program.command('rotate')
|
|
162
|
+
.description('rotate keypair(s) and re-encrypt .env file(s)')
|
|
163
|
+
.option('-f, --env-file <paths...>', 'path(s) to your env file(s)', collectEnvs('envFile'), [])
|
|
164
|
+
.option('-fk, --env-keys-file <path>', 'path to your .env.keys file (default: same path as your env file)')
|
|
165
|
+
.option('-k, --key <keys...>', 'keys(s) to encrypt (default: all keys in file)')
|
|
166
|
+
.option('-ek, --exclude-key <excludeKeys...>', 'keys(s) to exclude from encryption (default: none)')
|
|
167
|
+
.option('--stdout', 'send to stdout')
|
|
168
|
+
.action(function (...args) {
|
|
169
|
+
this.envs = envs
|
|
170
|
+
rotateAction.apply(this, args)
|
|
171
|
+
})
|
|
172
|
+
|
|
159
173
|
// dotenvx help
|
|
160
174
|
program.command('help [command]')
|
|
161
175
|
.description('display help for command')
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const quotes = require('./quotes')
|
|
2
|
+
const dotenvParse = require('./dotenvParse')
|
|
3
|
+
const escapeForRegex = require('./escapeForRegex')
|
|
4
|
+
const escapeDollarSigns = require('./escapeDollarSigns')
|
|
5
|
+
|
|
6
|
+
function append (src, key, appendValue) {
|
|
7
|
+
let output
|
|
8
|
+
let newPart = ''
|
|
9
|
+
|
|
10
|
+
const parsed = dotenvParse(src, true) // skip expanding \n
|
|
11
|
+
const _quotes = quotes(src)
|
|
12
|
+
if (Object.prototype.hasOwnProperty.call(parsed, key)) {
|
|
13
|
+
const quote = _quotes[key]
|
|
14
|
+
const originalValue = parsed[key]
|
|
15
|
+
|
|
16
|
+
newPart += `${key}=${quote}${originalValue},${appendValue}${quote}`
|
|
17
|
+
|
|
18
|
+
const escapedOriginalValue = escapeForRegex(originalValue)
|
|
19
|
+
|
|
20
|
+
// conditionally enforce end of line
|
|
21
|
+
let enforceEndOfLine = ''
|
|
22
|
+
if (escapedOriginalValue === '') {
|
|
23
|
+
enforceEndOfLine = '$' // EMPTY scenario
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const currentPart = new RegExp(
|
|
27
|
+
'^' + // start of line
|
|
28
|
+
'(\\s*)?' + // spaces
|
|
29
|
+
'(export\\s+)?' + // export
|
|
30
|
+
key + // KEY
|
|
31
|
+
'\\s*=\\s*' + // spaces (KEY = value)
|
|
32
|
+
'["\'`]?' + // open quote
|
|
33
|
+
escapedOriginalValue + // escaped value
|
|
34
|
+
'["\'`]?' + // close quote
|
|
35
|
+
enforceEndOfLine
|
|
36
|
+
,
|
|
37
|
+
'gm' // (g)lobal (m)ultiline
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
const saferInput = escapeDollarSigns(newPart) // cleanse user inputted capture groups ($1, $2 etc)
|
|
41
|
+
|
|
42
|
+
// $1 preserves spaces
|
|
43
|
+
// $2 preserves export
|
|
44
|
+
output = src.replace(currentPart, `$1$2${saferInput}`)
|
|
45
|
+
} else {
|
|
46
|
+
newPart += `${key}="${appendValue}"`
|
|
47
|
+
|
|
48
|
+
// append
|
|
49
|
+
if (src.endsWith('\n')) {
|
|
50
|
+
newPart = newPart + '\n'
|
|
51
|
+
} else {
|
|
52
|
+
newPart = '\n' + newPart
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
output = src + newPart
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return output
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = append
|
|
@@ -8,6 +8,7 @@ class Errors {
|
|
|
8
8
|
this.key = options.key
|
|
9
9
|
this.privateKey = options.privateKey
|
|
10
10
|
this.privateKeyName = options.privateKeyName
|
|
11
|
+
this.command = options.command
|
|
11
12
|
|
|
12
13
|
this.message = options.message
|
|
13
14
|
}
|
|
@@ -84,6 +85,17 @@ class Errors {
|
|
|
84
85
|
e.code = code
|
|
85
86
|
return e
|
|
86
87
|
}
|
|
88
|
+
|
|
89
|
+
commandSubstitutionFailed () {
|
|
90
|
+
const code = 'COMMAND_SUBSTITUTION_FAILED'
|
|
91
|
+
const message = `[${code}] could not eval ${this.key} containing command '${this.command}': ${this.message}`
|
|
92
|
+
const help = `[${code}] https://github.com/dotenvx/dotenvx/issues/532`
|
|
93
|
+
|
|
94
|
+
const e = new Error(message)
|
|
95
|
+
e.code = code
|
|
96
|
+
e.help = help
|
|
97
|
+
return e
|
|
98
|
+
}
|
|
87
99
|
}
|
|
88
100
|
|
|
89
101
|
module.exports = Errors
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const { execSync } = require('child_process')
|
|
2
|
+
const chomp = require('./chomp')
|
|
3
|
+
const Errors = require('./errors')
|
|
4
|
+
|
|
5
|
+
function evalKeyValue (key, value, processEnv, runningParsed) {
|
|
6
|
+
// Match everything between the outermost $() using a regex with non-capturing groups
|
|
7
|
+
const matches = value.match(/\$\(([^)]+(?:\)[^(]*)*)\)/g) || []
|
|
8
|
+
return matches.reduce((newValue, match) => {
|
|
9
|
+
const command = match.slice(2, -1) // Extract command by removing $() wrapper
|
|
10
|
+
let result
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
result = execSync(command, { env: { ...processEnv, ...runningParsed } }).toString() // execute command (including runningParsed)
|
|
14
|
+
} catch (e) {
|
|
15
|
+
throw new Errors({ key, command, message: e.message.trim() }).commandSubstitutionFailed()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
result = chomp(result) // chomp it
|
|
19
|
+
return newValue.replace(match, result) // Replace match with result
|
|
20
|
+
}, value)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
module.exports = evalKeyValue
|
package/src/lib/helpers/parse.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
const chomp = require('./chomp')
|
|
2
1
|
const decryptKeyValue = require('./decryptKeyValue')
|
|
2
|
+
const evalKeyValue = require('./evalKeyValue')
|
|
3
3
|
const resolveEscapeSequences = require('./resolveEscapeSequences')
|
|
4
|
-
const { execSync } = require('child_process')
|
|
5
4
|
|
|
6
5
|
class Parse {
|
|
7
6
|
static LINE = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/mg
|
|
@@ -49,7 +48,12 @@ class Parse {
|
|
|
49
48
|
let evaled = false
|
|
50
49
|
if (quote !== "'" && (!this.inProcessEnv(key) || this.processEnv[key] === this.parsed[key])) {
|
|
51
50
|
const priorEvaled = this.parsed[key]
|
|
52
|
-
|
|
51
|
+
// eval
|
|
52
|
+
try {
|
|
53
|
+
this.parsed[key] = this.eval(key, priorEvaled)
|
|
54
|
+
} catch (e) {
|
|
55
|
+
this.errors.push(e)
|
|
56
|
+
}
|
|
53
57
|
if (priorEvaled !== this.parsed[key]) {
|
|
54
58
|
evaled = true
|
|
55
59
|
}
|
|
@@ -133,14 +137,8 @@ class Parse {
|
|
|
133
137
|
return decryptKeyValue(key, value, this.privateKeyName, this.privateKey)
|
|
134
138
|
}
|
|
135
139
|
|
|
136
|
-
eval (value) {
|
|
137
|
-
|
|
138
|
-
const matches = value.match(/\$\(([^)]+(?:\)[^(]*)*)\)/g) || []
|
|
139
|
-
return matches.reduce((newValue, match) => {
|
|
140
|
-
const command = match.slice(2, -1) // Extract command by removing $() wrapper
|
|
141
|
-
const result = chomp(execSync(command, { env: { ...this.processEnv, ...this.runningParsed } }).toString()) // execute command (including runningParsed)
|
|
142
|
-
return newValue.replace(match, result) // Replace match with result
|
|
143
|
-
}, value)
|
|
140
|
+
eval (key, value) {
|
|
141
|
+
return evalKeyValue(key, value, this.processEnv, this.runningParsed)
|
|
144
142
|
}
|
|
145
143
|
|
|
146
144
|
expand (value) {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
const fsx = require('./../helpers/fsx')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const picomatch = require('picomatch')
|
|
4
|
+
|
|
5
|
+
const TYPE_ENV_FILE = 'envFile'
|
|
6
|
+
|
|
7
|
+
const Errors = require('./../helpers/errors')
|
|
8
|
+
const guessPrivateKeyName = require('./../helpers/guessPrivateKeyName')
|
|
9
|
+
const guessPublicKeyName = require('./../helpers/guessPublicKeyName')
|
|
10
|
+
const encryptValue = require('./../helpers/encryptValue')
|
|
11
|
+
const isEncrypted = require('./../helpers/isEncrypted')
|
|
12
|
+
const dotenvParse = require('./../helpers/dotenvParse')
|
|
13
|
+
const replace = require('./../helpers/replace')
|
|
14
|
+
const append = require('./../helpers/append')
|
|
15
|
+
const detectEncoding = require('./../helpers/detectEncoding')
|
|
16
|
+
const determineEnvs = require('./../helpers/determineEnvs')
|
|
17
|
+
const findPrivateKey = require('./../helpers/findPrivateKey')
|
|
18
|
+
const decryptKeyValue = require('./../helpers/decryptKeyValue')
|
|
19
|
+
const keypair = require('./../helpers/keypair')
|
|
20
|
+
|
|
21
|
+
class Rotate {
|
|
22
|
+
constructor (envs = [], key = [], excludeKey = [], envKeysFilepath = null) {
|
|
23
|
+
this.envs = determineEnvs(envs, process.env)
|
|
24
|
+
this.key = key
|
|
25
|
+
this.excludeKey = excludeKey
|
|
26
|
+
this.envKeysFilepath = envKeysFilepath
|
|
27
|
+
|
|
28
|
+
this.processedEnvs = []
|
|
29
|
+
this.changedFilepaths = new Set()
|
|
30
|
+
this.unchangedFilepaths = new Set()
|
|
31
|
+
|
|
32
|
+
this.envKeysSources = {}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
run () {
|
|
36
|
+
// example
|
|
37
|
+
// envs [
|
|
38
|
+
// { type: 'envFile', value: '.env' }
|
|
39
|
+
// ]
|
|
40
|
+
|
|
41
|
+
this.keys = this._keys()
|
|
42
|
+
const excludeKeys = this._excludeKeys()
|
|
43
|
+
|
|
44
|
+
this.exclude = picomatch(excludeKeys)
|
|
45
|
+
this.include = picomatch(this.keys, { ignore: excludeKeys })
|
|
46
|
+
|
|
47
|
+
for (const env of this.envs) {
|
|
48
|
+
if (env.type === TYPE_ENV_FILE) {
|
|
49
|
+
this._rotateEnvFile(env.value)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
processedEnvs: this.processedEnvs,
|
|
55
|
+
changedFilepaths: [...this.changedFilepaths],
|
|
56
|
+
unchangedFilepaths: [...this.unchangedFilepaths]
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_rotateEnvFile (envFilepath) {
|
|
61
|
+
const row = {}
|
|
62
|
+
row.keys = []
|
|
63
|
+
row.type = TYPE_ENV_FILE
|
|
64
|
+
|
|
65
|
+
const filepath = path.resolve(envFilepath)
|
|
66
|
+
row.filepath = filepath
|
|
67
|
+
row.envFilepath = envFilepath
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const encoding = this._detectEncoding(filepath)
|
|
71
|
+
let envSrc = fsx.readFileX(filepath, { encoding })
|
|
72
|
+
const envParsed = dotenvParse(envSrc)
|
|
73
|
+
|
|
74
|
+
const publicKeyName = guessPublicKeyName(envFilepath)
|
|
75
|
+
const privateKeyName = guessPrivateKeyName(envFilepath)
|
|
76
|
+
const existingPrivateKey = findPrivateKey(envFilepath, this.envKeysFilepath)
|
|
77
|
+
|
|
78
|
+
let envKeysFilepath = path.join(path.dirname(filepath), '.env.keys')
|
|
79
|
+
if (this.envKeysFilepath) {
|
|
80
|
+
envKeysFilepath = path.resolve(this.envKeysFilepath)
|
|
81
|
+
}
|
|
82
|
+
const keysEncoding = this._detectEncoding(envKeysFilepath)
|
|
83
|
+
|
|
84
|
+
row.envKeysFilepath = envKeysFilepath
|
|
85
|
+
this.envKeysSources[envKeysFilepath] ||= fsx.readFileX(envKeysFilepath, { encoding: keysEncoding })
|
|
86
|
+
let envKeysSrc = this.envKeysSources[envKeysFilepath]
|
|
87
|
+
|
|
88
|
+
// new keypair
|
|
89
|
+
const nkp = keypair() // generates a fresh keypair in memory
|
|
90
|
+
const newPublicKey = nkp.publicKey
|
|
91
|
+
const newPrivateKey = nkp.privateKey
|
|
92
|
+
|
|
93
|
+
// .env
|
|
94
|
+
envSrc = replace(envSrc, publicKeyName, newPublicKey) // replace publicKey
|
|
95
|
+
row.changed = true // track change
|
|
96
|
+
for (const [key, value] of Object.entries(envParsed)) { // re-encrypt each individual key
|
|
97
|
+
// key excluded - don't re-encrypt it
|
|
98
|
+
if (this.exclude(key)) {
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// key effectively excluded (by not being in the list of includes) - don't re-encrypt it
|
|
103
|
+
if (this.keys.length > 0 && !this.include(key)) {
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (isEncrypted(value)) { // only re-encrypt those already encrypted
|
|
108
|
+
row.keys.push(key) // track key(s)
|
|
109
|
+
|
|
110
|
+
const decryptedValue = decryptKeyValue(key, value, privateKeyName, existingPrivateKey) // get decrypted value
|
|
111
|
+
|
|
112
|
+
const encryptedValue = encryptValue(decryptedValue, newPublicKey) // encrypt with the new publicKey
|
|
113
|
+
|
|
114
|
+
envSrc = replace(envSrc, key, encryptedValue)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
row.envSrc = envSrc
|
|
118
|
+
|
|
119
|
+
// .env.keys - TODO: for dotenvx pro .env.keys file does not exist
|
|
120
|
+
row.privateKeyAdded = true
|
|
121
|
+
row.privateKeyName = privateKeyName
|
|
122
|
+
row.privateKey = newPrivateKey
|
|
123
|
+
envKeysSrc = append(envKeysSrc, privateKeyName, newPrivateKey) // append privateKey
|
|
124
|
+
this.envKeysSources[envKeysFilepath] = envKeysSrc
|
|
125
|
+
row.envKeysSrc = envKeysSrc
|
|
126
|
+
|
|
127
|
+
this.changedFilepaths.add(envFilepath)
|
|
128
|
+
} catch (e) {
|
|
129
|
+
if (e.code === 'ENOENT') {
|
|
130
|
+
row.error = new Errors({ envFilepath, filepath }).missingEnvFile()
|
|
131
|
+
} else {
|
|
132
|
+
row.error = e
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
this.processedEnvs.push(row)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
_keys () {
|
|
140
|
+
if (!Array.isArray(this.key)) {
|
|
141
|
+
return [this.key]
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return this.key
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
_excludeKeys () {
|
|
148
|
+
if (!Array.isArray(this.excludeKey)) {
|
|
149
|
+
return [this.excludeKey]
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return this.excludeKey
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
_detectEncoding (filepath) {
|
|
156
|
+
return detectEncoding(filepath)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = Rotate
|