@dotenvx/dotenvx 0.4.0 → 0.5.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/README.md +1 -1
- package/package.json +1 -1
- package/src/cli/dotenvx.js +86 -32
- package/src/cli/helpers.js +35 -13
- package/src/lib/main.js +5 -0
package/README.md
CHANGED
|
@@ -158,7 +158,7 @@ $ dotenvx run --env-file=.env.local --env-file=.env -- node index.js
|
|
|
158
158
|
## Encrypt Your Env Files
|
|
159
159
|
|
|
160
160
|
```
|
|
161
|
-
dotenvx encrypt
|
|
161
|
+
$ dotenvx encrypt
|
|
162
162
|
```
|
|
163
163
|
|
|
164
164
|
> This will encrypt your `.env` file to a `.env.vault` file. Commit your `.env.vault` file safely to code.
|
package/package.json
CHANGED
package/src/cli/dotenvx.js
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const fs = require('fs')
|
|
4
4
|
const { Command } = require('commander')
|
|
5
|
-
const dotenv = require('dotenv')
|
|
6
5
|
const program = new Command()
|
|
7
6
|
|
|
8
7
|
// constants
|
|
@@ -59,40 +58,97 @@ program.command('run')
|
|
|
59
58
|
logger.debug('configuring options')
|
|
60
59
|
logger.debug(options)
|
|
61
60
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
// load from .env.vault file
|
|
62
|
+
if (process.env.DOTENV_KEY && process.env.DOTENV_KEY.length > 0) {
|
|
63
|
+
const filepath = helpers.resolvePath('.env.vault')
|
|
64
|
+
|
|
65
|
+
if (!fs.existsSync(filepath)) {
|
|
66
|
+
logger.error(`you set DOTENV_KEY but your .env.vault file is missing: ${filepath}`)
|
|
67
|
+
} else {
|
|
68
|
+
logger.verbose(`injecting encrypted env from ${filepath}`)
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
logger.debug(`reading encrypted env from ${filepath}`)
|
|
72
|
+
const src = fs.readFileSync(filepath, { encoding: ENCODING })
|
|
73
|
+
|
|
74
|
+
logger.debug(`parsing encrypted env from ${filepath}`)
|
|
75
|
+
const parsedVault = main.parse(src)
|
|
76
|
+
|
|
77
|
+
logger.debug(`decrypting encrypted env from ${filepath}`)
|
|
78
|
+
// handle scenario for comma separated keys - for use with key rotation
|
|
79
|
+
// example: DOTENV_KEY="dotenv://:key_1234@dotenv.org/vault/.env.vault?environment=prod,dotenv://:key_7890@dotenv.org/vault/.env.vault?environment=prod"
|
|
80
|
+
const dotenvKeys = process.env.DOTENV_KEY.split(',')
|
|
81
|
+
const length = dotenvKeys.length
|
|
82
|
+
|
|
83
|
+
let decrypted
|
|
84
|
+
for (let i = 0; i < length; i++) {
|
|
85
|
+
try {
|
|
86
|
+
// Get full dotenvKey
|
|
87
|
+
const dotenvKey = dotenvKeys[i].trim()
|
|
88
|
+
|
|
89
|
+
const key = helpers._parseEncryptionKeyFromDotenvKey(dotenvKey)
|
|
90
|
+
const ciphertext = helpers._parseCipherTextFromDotenvKeyAndParsedVault(dotenvKey, parsedVault)
|
|
91
|
+
|
|
92
|
+
// Decrypt
|
|
93
|
+
decrypted = main.decrypt(ciphertext, key)
|
|
94
|
+
|
|
95
|
+
break
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// last key
|
|
98
|
+
if (i + 1 >= length) {
|
|
99
|
+
throw error
|
|
100
|
+
}
|
|
101
|
+
// try next key
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
logger.debug(decrypted)
|
|
105
|
+
|
|
106
|
+
logger.debug(`parsing decrypted env from ${filepath}`)
|
|
107
|
+
const parsed = main.parse(decrypted)
|
|
108
|
+
|
|
109
|
+
logger.debug(`writing decrypted env from ${filepath}`)
|
|
110
|
+
const result = main.write(process.env, parsed, options.overload)
|
|
111
|
+
|
|
112
|
+
logger.info(`injecting ${result.written.size} environment ${helpers.pluralize('variable', result.written.size)} from encrypted .env.vault`)
|
|
113
|
+
} catch (e) {
|
|
114
|
+
logger.error(e)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
// convert to array if needed
|
|
119
|
+
let optionEnvFile = options.envFile
|
|
120
|
+
if (!Array.isArray(optionEnvFile)) {
|
|
121
|
+
optionEnvFile = [optionEnvFile]
|
|
122
|
+
}
|
|
67
123
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const written = new Set()
|
|
124
|
+
const readableFilepaths = new Set()
|
|
125
|
+
const written = new Set()
|
|
71
126
|
|
|
72
|
-
|
|
73
|
-
|
|
127
|
+
for (const envFilepath of optionEnvFile) {
|
|
128
|
+
const filepath = helpers.resolvePath(envFilepath)
|
|
74
129
|
|
|
75
|
-
|
|
130
|
+
logger.verbose(`injecting env from ${filepath}`)
|
|
76
131
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
132
|
+
try {
|
|
133
|
+
logger.debug(`reading env from ${filepath}`)
|
|
134
|
+
const src = fs.readFileSync(filepath, { encoding: ENCODING })
|
|
80
135
|
|
|
81
|
-
|
|
82
|
-
|
|
136
|
+
logger.debug(`parsing env from ${filepath}`)
|
|
137
|
+
const parsed = main.parse(src)
|
|
83
138
|
|
|
84
|
-
|
|
85
|
-
|
|
139
|
+
logger.debug(`writing env from ${filepath}`)
|
|
140
|
+
const result = main.write(process.env, parsed, options.overload)
|
|
86
141
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
142
|
+
readableFilepaths.add(envFilepath)
|
|
143
|
+
result.written.forEach(key => written.add(key))
|
|
144
|
+
} catch (e) {
|
|
145
|
+
logger.warn(e)
|
|
146
|
+
}
|
|
91
147
|
}
|
|
92
|
-
}
|
|
93
148
|
|
|
94
|
-
|
|
95
|
-
|
|
149
|
+
if (readableFilepaths.size > 0) {
|
|
150
|
+
logger.info(`injecting ${written.size} environment ${helpers.pluralize('variable', written.size)} from ${[...readableFilepaths]}`)
|
|
151
|
+
}
|
|
96
152
|
}
|
|
97
153
|
|
|
98
154
|
// Extract command and arguments after '--'
|
|
@@ -103,7 +159,7 @@ program.command('run')
|
|
|
103
159
|
} else {
|
|
104
160
|
const subCommand = process.argv.slice(commandIndex + 1)
|
|
105
161
|
|
|
106
|
-
helpers.executeCommand(subCommand, env)
|
|
162
|
+
helpers.executeCommand(subCommand, process.env)
|
|
107
163
|
}
|
|
108
164
|
})
|
|
109
165
|
|
|
@@ -124,7 +180,7 @@ program.command('encrypt')
|
|
|
124
180
|
try {
|
|
125
181
|
logger.verbose(`generating .env.keys from ${optionEnvFile}`)
|
|
126
182
|
|
|
127
|
-
const dotenvKeys = (
|
|
183
|
+
const dotenvKeys = (main.configDotenv({ path: '.env.keys' }).parsed || {})
|
|
128
184
|
|
|
129
185
|
for (const envFilepath of optionEnvFile) {
|
|
130
186
|
const filepath = helpers.resolvePath(envFilepath)
|
|
@@ -169,8 +225,8 @@ program.command('encrypt')
|
|
|
169
225
|
try {
|
|
170
226
|
logger.verbose(`generating .env.vault from ${optionEnvFile}`)
|
|
171
227
|
|
|
172
|
-
const dotenvKeys = (
|
|
173
|
-
const dotenvVaults = (
|
|
228
|
+
const dotenvKeys = (main.configDotenv({ path: '.env.keys' }).parsed || {})
|
|
229
|
+
const dotenvVaults = (main.configDotenv({ path: '.env.vault' }).parsed || {})
|
|
174
230
|
|
|
175
231
|
for (const envFilepath of optionEnvFile) {
|
|
176
232
|
const filepath = helpers.resolvePath(envFilepath)
|
|
@@ -211,8 +267,6 @@ program.command('encrypt')
|
|
|
211
267
|
}
|
|
212
268
|
|
|
213
269
|
logger.info(`encrypted ${optionEnvFile} to .env.vault`)
|
|
214
|
-
|
|
215
|
-
// logger.info(`encrypting`)
|
|
216
270
|
})
|
|
217
271
|
|
|
218
272
|
program.parse(process.argv)
|
package/src/cli/helpers.js
CHANGED
|
@@ -4,6 +4,7 @@ const crypto = require('crypto')
|
|
|
4
4
|
const { spawn } = require('child_process')
|
|
5
5
|
const xxhash = require('xxhashjs')
|
|
6
6
|
const XXHASH_SEED = 0xABCD
|
|
7
|
+
const NONCE_BYTES = 12
|
|
7
8
|
|
|
8
9
|
const main = require('./../lib/main')
|
|
9
10
|
|
|
@@ -69,17 +70,17 @@ const generateDotenvKey = function (environment) {
|
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
const encryptFile = function (filepath, dotenvKey, encoding) {
|
|
72
|
-
const key =
|
|
73
|
+
const key = _parseEncryptionKeyFromDotenvKey(dotenvKey)
|
|
73
74
|
const message = fs.readFileSync(filepath, encoding)
|
|
74
75
|
|
|
75
|
-
const ciphertext =
|
|
76
|
+
const ciphertext = encrypt(key, message)
|
|
76
77
|
|
|
77
78
|
return ciphertext
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
const encrypt = function (key, message) {
|
|
81
82
|
// set up nonce
|
|
82
|
-
const nonce =
|
|
83
|
+
const nonce = crypto.randomBytes(NONCE_BYTES)
|
|
83
84
|
|
|
84
85
|
// set up cipher
|
|
85
86
|
const cipher = crypto.createCipheriv('aes-256-gcm', key, nonce)
|
|
@@ -98,11 +99,11 @@ const encrypt = function (key, message) {
|
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
const changed = function (ciphertext, dotenvKey, filepath, encoding) {
|
|
101
|
-
const key =
|
|
102
|
+
const key = _parseEncryptionKeyFromDotenvKey(dotenvKey)
|
|
102
103
|
const decrypted = main.decrypt(ciphertext, key)
|
|
103
104
|
const raw = fs.readFileSync(filepath, encoding)
|
|
104
105
|
|
|
105
|
-
return
|
|
106
|
+
return hash(decrypted) !== hash(raw)
|
|
106
107
|
}
|
|
107
108
|
|
|
108
109
|
const hash = function (str) {
|
|
@@ -111,7 +112,12 @@ const hash = function (str) {
|
|
|
111
112
|
|
|
112
113
|
const _parseEncryptionKeyFromDotenvKey = function (dotenvKey) {
|
|
113
114
|
// Parse DOTENV_KEY. Format is a URI
|
|
114
|
-
|
|
115
|
+
let uri
|
|
116
|
+
try {
|
|
117
|
+
uri = new URL(dotenvKey)
|
|
118
|
+
} catch (e) {
|
|
119
|
+
throw new Error(`INVALID_DOTENV_KEY: ${e.message}`)
|
|
120
|
+
}
|
|
115
121
|
|
|
116
122
|
// Get decrypt key
|
|
117
123
|
const key = uri.password
|
|
@@ -122,12 +128,29 @@ const _parseEncryptionKeyFromDotenvKey = function (dotenvKey) {
|
|
|
122
128
|
return Buffer.from(key.slice(-64), 'hex')
|
|
123
129
|
}
|
|
124
130
|
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
-
|
|
131
|
+
const _parseCipherTextFromDotenvKeyAndParsedVault = function (dotenvKey, parsedVault) {
|
|
132
|
+
// Parse DOTENV_KEY. Format is a URI
|
|
133
|
+
let uri
|
|
134
|
+
try {
|
|
135
|
+
uri = new URL(dotenvKey)
|
|
136
|
+
} catch (e) {
|
|
137
|
+
throw new Error(`INVALID_DOTENV_KEY: ${e.message}`)
|
|
138
|
+
}
|
|
128
139
|
|
|
129
|
-
|
|
130
|
-
|
|
140
|
+
// Get environment
|
|
141
|
+
const environment = uri.searchParams.get('environment')
|
|
142
|
+
if (!environment) {
|
|
143
|
+
throw new Error('INVALID_DOTENV_KEY: Missing environment part')
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Get ciphertext payload
|
|
147
|
+
const environmentKey = `DOTENV_VAULT_${environment.toUpperCase()}`
|
|
148
|
+
const ciphertext = parsedVault[environmentKey] // DOTENV_VAULT_PRODUCTION
|
|
149
|
+
if (!ciphertext) {
|
|
150
|
+
throw new Error(`NOT_FOUND_DOTENV_ENVIRONMENT: cannot locate environment ${environmentKey} in your .env.vault file`)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return ciphertext
|
|
131
154
|
}
|
|
132
155
|
|
|
133
156
|
module.exports = {
|
|
@@ -142,6 +165,5 @@ module.exports = {
|
|
|
142
165
|
changed,
|
|
143
166
|
hash,
|
|
144
167
|
_parseEncryptionKeyFromDotenvKey,
|
|
145
|
-
|
|
146
|
-
_nonceBytes
|
|
168
|
+
_parseCipherTextFromDotenvKeyAndParsedVault
|
|
147
169
|
}
|
package/src/lib/main.js
CHANGED
|
@@ -9,6 +9,10 @@ const decrypt = function (encrypted, keyStr) {
|
|
|
9
9
|
return dotenv.decrypt(encrypted, keyStr)
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
const configDotenv = function (options) {
|
|
13
|
+
return dotenv.configDotenv(options)
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
const parse = function (src) {
|
|
13
17
|
const result = dotenv.parse(src)
|
|
14
18
|
|
|
@@ -57,6 +61,7 @@ const write = function (processEnv = {}, parsed = {}, overload = false) {
|
|
|
57
61
|
|
|
58
62
|
module.exports = {
|
|
59
63
|
config,
|
|
64
|
+
configDotenv,
|
|
60
65
|
decrypt,
|
|
61
66
|
parse,
|
|
62
67
|
write
|