@dotenvx/dotenvx 2.27.0 → 2.28.1
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 +25 -1
- package/README.md +40 -8
- package/package.json +1 -1
- package/src/cli/actions/define.js +17 -0
- package/src/cli/actions/ext/precommit.js +3 -0
- package/src/cli/actions/ext/precommitClean.js +21 -0
- package/src/cli/actions/get.js +2 -1
- package/src/cli/actions/init.js +3 -0
- package/src/cli/actions/lock/down.js +13 -32
- package/src/cli/actions/lock/up.js +13 -30
- package/src/cli/actions/run.js +2 -1
- package/src/cli/commands/custody.js +2 -2
- package/src/cli/commands/ext.js +3 -1
- package/src/cli/commands/fileOptions.js +22 -0
- package/src/cli/dotenvx.js +38 -21
- package/src/lib/custodians/index.js +73 -0
- package/src/lib/{helpers/bitwardenCustody.js → custodians/local/bitwarden.js} +12 -7
- package/src/lib/custodians/local/file.js +18 -0
- package/src/lib/custodians/local/native/backend.js +62 -0
- package/src/lib/custodians/local/native/index.js +28 -0
- package/src/lib/{helpers/storeNativePrivateKey.js → custodians/local/native/store.js} +3 -3
- package/src/lib/{helpers/onePasswordCustody.js → custodians/local/onepassword.js} +9 -5
- package/src/lib/custodians/lock.js +80 -0
- package/src/lib/{providers/armor/index.js → custodians/managed/armor/get.js} +6 -6
- package/src/lib/custodians/managed/armor/index.js +24 -0
- package/src/lib/custodians/managed/armor/store.js +37 -0
- package/src/lib/helpers/installPrecommitFilter.js +50 -0
- package/src/lib/helpers/installPrecommitHook.js +2 -2
- package/src/lib/helpers/lockedValue.js +24 -0
- package/src/lib/helpers/macosKeychain.js +18 -2
- package/src/lib/helpers/matchesStoredKey.js +16 -0
- package/src/lib/helpers/normalizeDotenvConfigPath.js +1 -1
- package/src/lib/helpers/parseWithDecryptor.js +3 -0
- package/src/lib/helpers/prompts.js +20 -1
- package/src/lib/helpers/resolveLockPassword.js +5 -0
- package/src/lib/helpers/selectKeyStorage.js +17 -23
- package/src/lib/helpers/unlockedValue.js +26 -0
- package/src/lib/helpers/withLockedKeys.js +41 -0
- package/src/lib/main.d.ts +6 -0
- package/src/lib/main.js +4 -1
- package/src/lib/providers/index.js +9 -66
- package/src/lib/providers/native/index.js +2 -62
- package/src/lib/providers/provider-worker.js +1 -1
- package/src/lib/resolvers/envs.js +14 -5
- package/src/lib/resolvers/get.js +1 -0
- package/src/lib/services/custodyTransfer.js +2 -2
- package/src/lib/services/init.js +49 -0
- package/src/lib/services/precommit.js +12 -1
- package/src/lib/services/validate.js +1 -0
- package/src/lib/transforms/encrypt.js +6 -49
- package/src/lib/transforms/set.js +6 -49
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
const { execFile, execFileSync } = require('child_process')
|
|
2
|
-
const
|
|
3
|
-
const Session = require('
|
|
4
|
-
const prompts = require('
|
|
5
|
-
const createSpinner = require('
|
|
2
|
+
const matchesStoredKey = require('../../helpers/matchesStoredKey')
|
|
3
|
+
const Session = require('../../../db/session')
|
|
4
|
+
const prompts = require('../../helpers/prompts')
|
|
5
|
+
const createSpinner = require('../../helpers/createSpinner')
|
|
6
6
|
let unlockedSession
|
|
7
7
|
|
|
8
|
-
const armoredKeyDisplay = require('
|
|
8
|
+
const armoredKeyDisplay = require('../../helpers/armoredKeyDisplay')
|
|
9
9
|
|
|
10
10
|
const PREFIX = 'DOTENVX_BITWARDEN_'
|
|
11
11
|
const ID = /^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/i
|
|
@@ -110,7 +110,7 @@ function checkIdentity (status, loc) {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
function verified (publicKey, privateKey) {
|
|
113
|
-
try { if (
|
|
113
|
+
try { if (matchesStoredKey(publicKey, privateKey)) return { [publicKey]: privateKey } } catch {}
|
|
114
114
|
throw failure('Bitwarden private key does not match the .env public key')
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -155,6 +155,7 @@ async function set (publicKey, privateKey) {
|
|
|
155
155
|
if (!item || !ID.test(item.id || '') || item.organizationId) throw failure('Bitwarden did not return a personal vault item')
|
|
156
156
|
const saved = (await run(['get', 'password', item.id])).trim()
|
|
157
157
|
verified(publicKey, saved)
|
|
158
|
+
if (saved !== privateKey) throw failure('could not verify private key in Bitwarden')
|
|
158
159
|
const loc = { item: item.id, userId: status.userId, serverUrl: status.serverUrl || '' }
|
|
159
160
|
new Session().createStore().set(`${PREFIX}${publicKey}`, Buffer.from(JSON.stringify(loc)).toString('base64'))
|
|
160
161
|
}
|
|
@@ -167,4 +168,8 @@ async function remove (publicKey) {
|
|
|
167
168
|
new Session().openStore().delete(`${PREFIX}${publicKey}`)
|
|
168
169
|
}
|
|
169
170
|
|
|
170
|
-
|
|
171
|
+
function enabled (options = {}) {
|
|
172
|
+
return options.noBitwarden !== true && process.env.DOTENVX_NO_BITWARDEN !== 'true'
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = { id: 'bitwarden', name: 'Bitwarden', enabled, store: set, available, configured, get, getSync, set, delete: remove }
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const { mutateKeysSrc } = require('../../helpers/cryptography')
|
|
2
|
+
|
|
3
|
+
// File keys are loaded by envResolution before provider lookup. Creation is
|
|
4
|
+
// staged here so the caller can commit .env.keys with the encrypted env files.
|
|
5
|
+
module.exports = {
|
|
6
|
+
id: 'file',
|
|
7
|
+
name: 'File (.env.keys)',
|
|
8
|
+
enabled: () => true,
|
|
9
|
+
available: () => true,
|
|
10
|
+
store (publicKey, privateKey, context) {
|
|
11
|
+
return mutateKeysSrc({
|
|
12
|
+
keysSrc: context.keysSrc,
|
|
13
|
+
privateKeyName: context.privateKeyName,
|
|
14
|
+
privateKeyValue: privateKey,
|
|
15
|
+
comment: context.comment
|
|
16
|
+
})
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const macosKeychain = require('../../../helpers/macosKeychain')
|
|
2
|
+
const windowsCredentialManager = require('../../../helpers/windowsCredentialManager')
|
|
3
|
+
const linuxSecretService = require('../../../helpers/linuxSecretService')
|
|
4
|
+
|
|
5
|
+
function get (key) {
|
|
6
|
+
if (process.platform === 'win32') {
|
|
7
|
+
return windowsCredentialManager.get(key)
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
if (process.platform === 'linux') {
|
|
11
|
+
return linuxSecretService.get(key)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return macosKeychain.get(key)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function set (key, value, label = key) {
|
|
18
|
+
if (process.platform === 'win32') {
|
|
19
|
+
windowsCredentialManager.set(key, value, label)
|
|
20
|
+
return
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (process.platform === 'linux') {
|
|
24
|
+
linuxSecretService.set(key, value, label)
|
|
25
|
+
return
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
macosKeychain.set(key, value, label)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
index.delete = function (key) {
|
|
32
|
+
if (process.platform === 'win32') {
|
|
33
|
+
windowsCredentialManager.delete(key)
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (process.platform === 'linux') {
|
|
38
|
+
linuxSecretService.delete(key)
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
macosKeychain.delete(key)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function index (publicKeyHex) {
|
|
46
|
+
if (!['darwin', 'linux', 'win32'].includes(process.platform)) return {}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
const privateKeyHex = get(publicKeyHex)
|
|
50
|
+
|
|
51
|
+
if (!privateKeyHex) return {}
|
|
52
|
+
|
|
53
|
+
return { [publicKeyHex]: privateKeyHex }
|
|
54
|
+
} catch {
|
|
55
|
+
return {}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
index.set = set
|
|
60
|
+
index.get = get
|
|
61
|
+
|
|
62
|
+
module.exports = index
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const backend = require('./backend')
|
|
2
|
+
const storePrivateKey = require('./store')
|
|
3
|
+
|
|
4
|
+
const names = {
|
|
5
|
+
darwin: 'macOS Keychain',
|
|
6
|
+
win32: 'Windows Credential Manager',
|
|
7
|
+
linux: 'Linux Secret Service'
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function enabled (options = {}) {
|
|
11
|
+
return !!names[process.platform] && !process.env.CI && options.noNative !== true && options.native !== false && process.env.DOTENVX_NO_NATIVE !== 'true'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
module.exports = {
|
|
15
|
+
id: 'native',
|
|
16
|
+
get name () { return `OS${names[process.platform] ? ` (${names[process.platform]})` : ''}` },
|
|
17
|
+
enabled,
|
|
18
|
+
available: () => true,
|
|
19
|
+
configured: () => true,
|
|
20
|
+
get: backend,
|
|
21
|
+
getSync: backend,
|
|
22
|
+
set: backend.set,
|
|
23
|
+
delete: backend.delete,
|
|
24
|
+
store (publicKey, privateKey, context) {
|
|
25
|
+
if (!storePrivateKey(publicKey, privateKey, context.keysFilepath)) return { fallback: 'file' }
|
|
26
|
+
return { nativePrivateKeyAdded: true }
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
const nativeProvider = require('
|
|
2
|
-
const armoredKeyDisplay = require('
|
|
3
|
-
const { logger } = require('
|
|
1
|
+
const nativeProvider = require('./backend')
|
|
2
|
+
const armoredKeyDisplay = require('../../../helpers/armoredKeyDisplay')
|
|
3
|
+
const { logger } = require('../../../../shared/logger')
|
|
4
4
|
|
|
5
5
|
function storeNativePrivateKey (publicKey, privateKey, keysFilepath) {
|
|
6
6
|
try {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { execFile, execFileSync } = require('child_process')
|
|
2
|
-
const
|
|
3
|
-
const Session = require('
|
|
4
|
-
const armoredKeyDisplay = require('
|
|
2
|
+
const matchesStoredKey = require('../../helpers/matchesStoredKey')
|
|
3
|
+
const Session = require('../../../db/session')
|
|
4
|
+
const armoredKeyDisplay = require('../../helpers/armoredKeyDisplay')
|
|
5
5
|
|
|
6
6
|
const PREFIX = 'DOTENVX_ONEPASSWORD_'
|
|
7
7
|
const ID = /^[a-z0-9]{26}$/i
|
|
@@ -98,7 +98,7 @@ function location (publicKey) {
|
|
|
98
98
|
|
|
99
99
|
function verified (publicKey, privateKey) {
|
|
100
100
|
try {
|
|
101
|
-
if (
|
|
101
|
+
if (matchesStoredKey(publicKey, privateKey)) return { [publicKey]: privateKey }
|
|
102
102
|
} catch {}
|
|
103
103
|
throw failure('1Password private key does not match the .env public key')
|
|
104
104
|
}
|
|
@@ -164,4 +164,8 @@ async function remove (publicKey) {
|
|
|
164
164
|
new Session().openStore().delete(`${PREFIX}${publicKey}`)
|
|
165
165
|
}
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
function enabled (options = {}) {
|
|
168
|
+
return options.no1Password !== true && process.env.DOTENVX_NO_1PASSWORD !== 'true'
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
module.exports = { id: 'onepassword', name: '1Password', enabled, store: set, available, configured, get, getSync, set, delete: remove }
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
const { derive } = require('@dotenvx/primitives')
|
|
2
|
+
const lockedValue = require('../helpers/lockedValue')
|
|
3
|
+
const unlockedValue = require('../helpers/unlockedValue')
|
|
4
|
+
const prompts = require('../helpers/prompts')
|
|
5
|
+
const createSpinner = require('../helpers/createSpinner')
|
|
6
|
+
const matchesKey = require('../helpers/matchesStoredKey')
|
|
7
|
+
const Errors = require('../helpers/errors')
|
|
8
|
+
const resolveLockPassword = require('../helpers/resolveLockPassword')
|
|
9
|
+
|
|
10
|
+
// Context is shared only within a transform; never cache passphrases globally
|
|
11
|
+
// by filename or return them alongside the staged keys.
|
|
12
|
+
const passphrases = new WeakMap()
|
|
13
|
+
|
|
14
|
+
async function promptPassphrase () {
|
|
15
|
+
createSpinner.pause()
|
|
16
|
+
try {
|
|
17
|
+
const passphrase = await prompts.password({ message: 'passphrase', prefix: '⊡', separator: '=' }, {
|
|
18
|
+
input: process.stdin,
|
|
19
|
+
output: process.stderr
|
|
20
|
+
})
|
|
21
|
+
if (!passphrase) throw new Error('passphrase must not be empty')
|
|
22
|
+
return passphrase
|
|
23
|
+
} finally {
|
|
24
|
+
createSpinner.resume()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async function lock (publicKey, privateKey, context) {
|
|
29
|
+
if (derive(privateKey) !== publicKey) throw new Error('private key does not match the .env public key')
|
|
30
|
+
if (!passphrases.has(context)) {
|
|
31
|
+
const password = resolveLockPassword(context)
|
|
32
|
+
if (password === '') throw new Error('passphrase must not be empty')
|
|
33
|
+
passphrases.set(context, password !== undefined ? password : promptPassphrase())
|
|
34
|
+
}
|
|
35
|
+
return lockedValue(privateKey, await passphrases.get(context), publicKey)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isLocked (value) {
|
|
39
|
+
return typeof value === 'string' && value.startsWith('locked:')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function unlockValue (publicKey, ring, passphrase) {
|
|
43
|
+
try {
|
|
44
|
+
const privateKey = unlockedValue(ring[publicKey], passphrase)
|
|
45
|
+
if (derive(privateKey) !== publicKey) throw new Error('key mismatch')
|
|
46
|
+
return { ...ring, [publicKey]: privateKey }
|
|
47
|
+
} catch {
|
|
48
|
+
throw new Errors().invalidPassphrase()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function validateLocked (publicKey, ring) {
|
|
53
|
+
const value = ring && ring[publicKey]
|
|
54
|
+
if (!isLocked(value)) return false
|
|
55
|
+
if (!matchesKey(publicKey, value)) throw new Error('invalid locked private key')
|
|
56
|
+
return true
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function passwordRequired () {
|
|
60
|
+
const error = new Error('[LOCKED_PRIVATE_KEY] supply --lock-password, lockPassword, or DOTENVX_LOCK_PASSWORD to unlock the private key')
|
|
61
|
+
error.code = 'LOCKED_PRIVATE_KEY'
|
|
62
|
+
return error
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function unlock (publicKey, ring, options = {}) {
|
|
66
|
+
if (!validateLocked(publicKey, ring)) return ring
|
|
67
|
+
const password = resolveLockPassword(options)
|
|
68
|
+
if (password !== undefined) return unlockValue(publicKey, ring, password)
|
|
69
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY || process.env.CI) throw passwordRequired()
|
|
70
|
+
return unlockValue(publicKey, ring, await promptPassphrase())
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function unlockSync (publicKey, ring, options = {}) {
|
|
74
|
+
if (!validateLocked(publicKey, ring)) return ring
|
|
75
|
+
const password = resolveLockPassword(options)
|
|
76
|
+
if (password === undefined) throw passwordRequired()
|
|
77
|
+
return unlockValue(publicKey, ring, password)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = { lock, unlock, unlockSync }
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
const Session = require('
|
|
2
|
-
const ArmorKeyring = require('
|
|
3
|
-
const armoredKeyDisplay = require('
|
|
4
|
-
const isNetworkError = require('
|
|
5
|
-
const listenForOpenKey = require('
|
|
6
|
-
const openUrl = require('
|
|
1
|
+
const Session = require('../../../../db/session')
|
|
2
|
+
const ArmorKeyring = require('../../../services/armorKeyring')
|
|
3
|
+
const armoredKeyDisplay = require('../../../helpers/armoredKeyDisplay')
|
|
4
|
+
const isNetworkError = require('../../../helpers/isNetworkError')
|
|
5
|
+
const listenForOpenKey = require('../../../helpers/listenForOpenKey')
|
|
6
|
+
const openUrl = require('../../../helpers/openUrl')
|
|
7
7
|
|
|
8
8
|
async function index (publicKeyHex, options = {}) {
|
|
9
9
|
const sesh = new Session()
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const Session = require('../../../../db/session')
|
|
2
|
+
const get = require('./get')
|
|
3
|
+
const store = require('./store')
|
|
4
|
+
|
|
5
|
+
module.exports = {
|
|
6
|
+
id: 'armored',
|
|
7
|
+
name: '⛨ Armor',
|
|
8
|
+
custody: 'managed',
|
|
9
|
+
enabled: (options = {}) => options.noArmor !== true && options.armor !== false,
|
|
10
|
+
available: () => true,
|
|
11
|
+
async configured (options = {}) {
|
|
12
|
+
return !!options.token || !await new Session().noArmor()
|
|
13
|
+
},
|
|
14
|
+
configuredSync (options = {}) {
|
|
15
|
+
return !!options.token || !new Session().noArmorSync()
|
|
16
|
+
},
|
|
17
|
+
get,
|
|
18
|
+
getSync (publicKey) {
|
|
19
|
+
const { createSyncFn } = require('@dotenvx/tooling')
|
|
20
|
+
const runProviderSync = createSyncFn(require.resolve('../../../providers/provider-worker.js'))
|
|
21
|
+
return runProviderSync(publicKey)
|
|
22
|
+
},
|
|
23
|
+
store
|
|
24
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const Session = require('../../../../db/session')
|
|
2
|
+
const PostArmorUp = require('../../../api/postArmorUp')
|
|
3
|
+
const prompts = require('../../../helpers/prompts')
|
|
4
|
+
const teamChoicesFromMeta = require('../../../helpers/teamChoicesFromMeta')
|
|
5
|
+
const isTeamRequiredError = require('../../../helpers/isTeamRequiredError')
|
|
6
|
+
|
|
7
|
+
async function store (publicKey, privateKey) {
|
|
8
|
+
const sesh = new Session()
|
|
9
|
+
const hostname = sesh.hostname()
|
|
10
|
+
const token = sesh.token()
|
|
11
|
+
const devicePublicKey = sesh.devicePublicKey()
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
await new PostArmorUp(hostname, token, devicePublicKey, publicKey, privateKey, undefined).run()
|
|
15
|
+
} catch (error) {
|
|
16
|
+
if (!isTeamRequiredError(error)) {
|
|
17
|
+
throw error
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const choices = teamChoicesFromMeta(error.meta)
|
|
21
|
+
|
|
22
|
+
let team = choices[0].value
|
|
23
|
+
if (choices.length > 1) {
|
|
24
|
+
team = await prompts.select({
|
|
25
|
+
message: 'Select team',
|
|
26
|
+
choices
|
|
27
|
+
}, {
|
|
28
|
+
input: process.stdin,
|
|
29
|
+
output: process.stderr
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
await new PostArmorUp(hostname, token, devicePublicKey, publicKey, privateKey, team).run()
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = store
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const fs = require('fs')
|
|
2
|
+
const path = require('path')
|
|
3
|
+
const os = require('os')
|
|
4
|
+
const { execFileSync } = require('child_process')
|
|
5
|
+
|
|
6
|
+
const ATTRIBUTE = '.env* filter=dotenvx'
|
|
7
|
+
|
|
8
|
+
function quote (value) {
|
|
9
|
+
return "'" + value.replace(/'/g, "'\\''") + "'"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function installPrecommitFilter (global = false) {
|
|
13
|
+
const git = (...args) => execFileSync('git', args, { encoding: 'utf8' }).trim()
|
|
14
|
+
let attributesPath
|
|
15
|
+
if (global) {
|
|
16
|
+
try {
|
|
17
|
+
attributesPath = git('config', '--global', '--includes', '--path', '--get', 'core.attributesFile')
|
|
18
|
+
} catch (error) {
|
|
19
|
+
if (error.status !== 1) throw error
|
|
20
|
+
}
|
|
21
|
+
if (!attributesPath) {
|
|
22
|
+
attributesPath = path.join(process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config'), 'git/attributes')
|
|
23
|
+
git('config', '--global', 'core.attributesFile', attributesPath)
|
|
24
|
+
}
|
|
25
|
+
} else {
|
|
26
|
+
attributesPath = git('rev-parse', '--git-path', 'info/attributes')
|
|
27
|
+
}
|
|
28
|
+
const hookPath = global ? null : git('rev-parse', '--git-path', 'hooks/pre-commit')
|
|
29
|
+
const current = fs.existsSync(attributesPath) ? fs.readFileSync(attributesPath, 'utf8') : ''
|
|
30
|
+
// Use the installed executable, including standalone builds. Never download a
|
|
31
|
+
// replacement via npx while Git is handling secret input.
|
|
32
|
+
const executable = process.pkg
|
|
33
|
+
? quote(process.execPath)
|
|
34
|
+
: `${quote(process.execPath)} ${quote(path.resolve(__dirname, '../../cli/dotenvx.js'))}`
|
|
35
|
+
|
|
36
|
+
const scope = global ? '--global' : '--local'
|
|
37
|
+
// Git's filter_buffer_or_fd shell-quotes the filename with sq_quote_buf
|
|
38
|
+
// before substituting %f (convert.c). Keep the placeholder unquoted here.
|
|
39
|
+
git('config', scope, 'filter.dotenvx.clean', `${executable} precommit --clean %f`)
|
|
40
|
+
git('config', scope, 'filter.dotenvx.required', 'true')
|
|
41
|
+
fs.mkdirSync(path.dirname(attributesPath), { recursive: true })
|
|
42
|
+
if (!current.split(/\r?\n/).includes(ATTRIBUTE)) {
|
|
43
|
+
fs.appendFileSync(attributesPath, `${current && !current.endsWith('\n') ? '\n' : ''}${ATTRIBUTE}\n`)
|
|
44
|
+
}
|
|
45
|
+
if (global) return attributesPath
|
|
46
|
+
fs.mkdirSync(path.dirname(hookPath), { recursive: true })
|
|
47
|
+
return hookPath
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = installPrecommitFilter
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const crypto = require('crypto')
|
|
2
|
+
|
|
3
|
+
function lockedValue (privateKey, passphrase, publicKey) {
|
|
4
|
+
const salt = crypto.randomBytes(16)
|
|
5
|
+
const iv = crypto.randomBytes(12)
|
|
6
|
+
const key = crypto.scryptSync(passphrase, salt, 32)
|
|
7
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv)
|
|
8
|
+
const ciphertext = Buffer.concat([
|
|
9
|
+
cipher.update(privateKey, 'utf8'),
|
|
10
|
+
cipher.final()
|
|
11
|
+
])
|
|
12
|
+
const tag = cipher.getAuthTag()
|
|
13
|
+
const payload = Buffer.concat([
|
|
14
|
+
Buffer.from([1]),
|
|
15
|
+
salt,
|
|
16
|
+
iv,
|
|
17
|
+
tag,
|
|
18
|
+
ciphertext
|
|
19
|
+
]).toString('base64url')
|
|
20
|
+
|
|
21
|
+
return `locked:${publicKey}:${payload}`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = lockedValue
|
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
const { execFileSync } = require('child_process')
|
|
1
|
+
const { execFileSync, spawnSync } = require('child_process')
|
|
2
2
|
const nativeStoreError = require('./nativeStoreError')
|
|
3
3
|
|
|
4
4
|
const SECURITY_BIN = '/usr/bin/security'
|
|
5
5
|
const SERVICE = 'dotenvx'
|
|
6
6
|
|
|
7
|
+
function quoteArgument (value) {
|
|
8
|
+
if (typeof value !== 'string' || /[\r\n\0]/.test(value)) throw new Error('invalid Keychain argument')
|
|
9
|
+
return '"' + value.replace(/[\\"]/g, '\\$&') + '"'
|
|
10
|
+
}
|
|
11
|
+
|
|
7
12
|
module.exports = {
|
|
8
13
|
get (key) {
|
|
9
14
|
try {
|
|
@@ -20,7 +25,18 @@ module.exports = {
|
|
|
20
25
|
|
|
21
26
|
set (key, value, label) {
|
|
22
27
|
try {
|
|
23
|
-
|
|
28
|
+
const input = ['add-generic-password', '-U', '-s', SERVICE, '-a', key, '-l', label, '-w', value].map(quoteArgument).join(' ') + '\n'
|
|
29
|
+
// security's 4096-byte line buffer must also consume the newline and NUL.
|
|
30
|
+
if (Buffer.byteLength(input, 'utf8') >= 4096) throw new Error('Keychain command too long')
|
|
31
|
+
// Interactive security can exit zero on failure, so inspect stderr too.
|
|
32
|
+
const result = spawnSync(SECURITY_BIN, ['-i'], {
|
|
33
|
+
input,
|
|
34
|
+
timeout: 10000,
|
|
35
|
+
killSignal: 'SIGKILL',
|
|
36
|
+
stdio: ['pipe', 'ignore', 'pipe']
|
|
37
|
+
})
|
|
38
|
+
if (result.error) throw result.error
|
|
39
|
+
if (result.status !== 0 || result.signal || result.stderr.length > 0) throw new Error('Keychain write failed')
|
|
24
40
|
} catch (error) {
|
|
25
41
|
throw nativeStoreError('failed to save private key to macOS Keychain', error)
|
|
26
42
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const { derive } = require('@dotenvx/primitives')
|
|
2
|
+
|
|
3
|
+
// Locked values can be transported without unlocking. Validate the envelope
|
|
4
|
+
// here; authenticate and derive the actual key when unlocking for use.
|
|
5
|
+
function matchesStoredKey (publicKey, value) {
|
|
6
|
+
if (typeof value !== 'string') return false
|
|
7
|
+
if (value.startsWith('locked:')) {
|
|
8
|
+
const parts = value.split(':')
|
|
9
|
+
if (parts.length !== 3 || parts[1] !== publicKey || !/^(02|03)[a-f0-9]{64}$/i.test(publicKey)) return false
|
|
10
|
+
const payload = Buffer.from(parts[2], 'base64url')
|
|
11
|
+
return payload.length === 109 && payload[0] === 1 && payload.toString('base64url') === parts[2]
|
|
12
|
+
}
|
|
13
|
+
try { return derive(value) === publicKey } catch { return false }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
module.exports = matchesStoredKey
|
|
@@ -3,7 +3,7 @@ const TYPE_ENV_FILE = 'envFile'
|
|
|
3
3
|
function normalizeDotenvConfigPath (envs = [], processEnv = process.env) {
|
|
4
4
|
if (envs.some(env => env.type === TYPE_ENV_FILE)) return envs
|
|
5
5
|
|
|
6
|
-
const configuredEnvFiles = processEnv.DOTENV_PATH || processEnv.DOTENV_F
|
|
6
|
+
const configuredEnvFiles = processEnv.DOTENV_FILE || processEnv.DOTENV_PATH || processEnv.DOTENV_F
|
|
7
7
|
if (!configuredEnvFiles) return envs
|
|
8
8
|
|
|
9
9
|
const envFiles = configuredEnvFiles.split(',').map(value => value.trim()).filter(Boolean)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { parse, parseSync, parsearrays, scan, encrypted } = require('@dotenvx/primitives')
|
|
2
2
|
const prepareProxy = require('../proxy/prepareProxy')
|
|
3
|
+
const withLockedKeys = require('./withLockedKeys')
|
|
3
4
|
const SERVER_SIDE_DECRYPTION_REQUIRED = 'SERVER_SIDE_DECRYPTION_REQUIRED'
|
|
4
5
|
|
|
5
6
|
function decryptOptions (error) {
|
|
@@ -43,6 +44,7 @@ parseWithDecryptor.arrays = async function parsearraysWithDecryptor (src, option
|
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
async function parseWith (src, options, parser) {
|
|
47
|
+
options = withLockedKeys(options)
|
|
46
48
|
try {
|
|
47
49
|
return await parser(src, options)
|
|
48
50
|
} catch (error) {
|
|
@@ -64,6 +66,7 @@ async function parseWith (src, options, parser) {
|
|
|
64
66
|
}
|
|
65
67
|
|
|
66
68
|
parseWithDecryptor.sync = function parseWithDecryptorSync (src, options = {}) {
|
|
69
|
+
options = withLockedKeys(options, true)
|
|
67
70
|
try {
|
|
68
71
|
return parseSync(src, options)
|
|
69
72
|
} catch (error) {
|
|
@@ -8,7 +8,8 @@ function choicesForSelect (choices) {
|
|
|
8
8
|
|
|
9
9
|
return {
|
|
10
10
|
name: choice.value,
|
|
11
|
-
message: choice.name || choice.value
|
|
11
|
+
message: choice.name || choice.value,
|
|
12
|
+
...(choice.disabled !== undefined ? { disabled: choice.disabled === true ? '(unavailable)' : choice.disabled } : {})
|
|
12
13
|
}
|
|
13
14
|
})
|
|
14
15
|
}
|
|
@@ -50,6 +51,23 @@ async function select ({ message, choices }, context) {
|
|
|
50
51
|
return answer.value
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
async function confirm ({ message, initial = false }, context) {
|
|
55
|
+
try {
|
|
56
|
+
const answer = await enquirer.prompt({
|
|
57
|
+
type: 'confirm',
|
|
58
|
+
name: 'value',
|
|
59
|
+
message,
|
|
60
|
+
initial,
|
|
61
|
+
...enquirerOptions(context)
|
|
62
|
+
})
|
|
63
|
+
return answer.value === true
|
|
64
|
+
} catch {
|
|
65
|
+
const error = new Error('prompt cancelled')
|
|
66
|
+
error.code = 'PROMPT_CANCELLED'
|
|
67
|
+
throw error
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
53
71
|
async function password ({ message, prefix, separator }, context) {
|
|
54
72
|
const output = (context && context.output) || process.stderr
|
|
55
73
|
|
|
@@ -84,6 +102,7 @@ async function password ({ message, prefix, separator }, context) {
|
|
|
84
102
|
}
|
|
85
103
|
|
|
86
104
|
module.exports = {
|
|
105
|
+
confirm,
|
|
87
106
|
password,
|
|
88
107
|
select
|
|
89
108
|
}
|
|
@@ -1,34 +1,28 @@
|
|
|
1
1
|
const prompts = require('./prompts')
|
|
2
|
-
const
|
|
3
|
-
const bitwardenCustody = require('./bitwardenCustody')
|
|
4
|
-
|
|
5
|
-
const secretStoreNames = {
|
|
6
|
-
darwin: 'macOS Keychain',
|
|
7
|
-
win32: 'Windows Credential Manager',
|
|
8
|
-
linux: 'Linux Secret Service'
|
|
9
|
-
}
|
|
2
|
+
const custodians = require('../custodians')
|
|
10
3
|
|
|
11
4
|
async function selectKeyStorage (options = {}) {
|
|
12
|
-
const
|
|
13
|
-
const defaultStorage = useNative ? 'native' : 'file'
|
|
5
|
+
const defaultStorage = custodians.get('native').enabled(options) ? 'native' : 'file'
|
|
14
6
|
if (process.env.CI || options.noCreate || !process.stdin.isTTY || !process.stderr.isTTY) return defaultStorage
|
|
15
7
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
]
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
22
|
-
if (!options.noBitwarden && process.env.DOTENVX_NO_BITWARDEN !== 'true' && await bitwardenCustody.available()) {
|
|
23
|
-
choices.push({ name: '□ Local Custody (Bitwarden)', value: 'bitwarden' })
|
|
24
|
-
}
|
|
25
|
-
if (!options.noArmor) choices.push({ name: '⛨ Managed Custody (Armor)', value: 'armored' })
|
|
26
|
-
if (choices.length < 2) return choices.length ? choices[0].value : defaultStorage
|
|
8
|
+
const localChoices = await custodians.choices(options)
|
|
9
|
+
|
|
10
|
+
const choices = [{ name: '⛉ Local Custody', value: 'local', disabled: false }]
|
|
11
|
+
const managedChoices = (await custodians.choices(options, 'managed')).filter(choice => !choice.disabled).map(({ name, value }) => ({ name, value }))
|
|
12
|
+
if (managedChoices.length) choices.push({ name: '⛊ Managed Custody', value: 'managed' })
|
|
27
13
|
|
|
28
|
-
|
|
14
|
+
const context = { input: process.stdin, output: process.stderr }
|
|
15
|
+
const custody = await prompts.select({
|
|
29
16
|
message: 'Choose private key storage',
|
|
30
17
|
choices
|
|
31
|
-
},
|
|
18
|
+
}, context)
|
|
19
|
+
|
|
20
|
+
const storage = await prompts.select({
|
|
21
|
+
message: custody === 'local' ? 'Choose local custody' : 'Choose managed custody',
|
|
22
|
+
choices: custody === 'local' ? localChoices : managedChoices
|
|
23
|
+
}, context)
|
|
24
|
+
|
|
25
|
+
return storage
|
|
32
26
|
}
|
|
33
27
|
|
|
34
28
|
module.exports = selectKeyStorage
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const crypto = require('crypto')
|
|
2
|
+
|
|
3
|
+
function unlockedValue (lockedPrivateKey, passphrase) {
|
|
4
|
+
const parts = lockedPrivateKey.split(':')
|
|
5
|
+
const payload = Buffer.from(parts.slice(2).join(':'), 'base64url')
|
|
6
|
+
const version = payload.subarray(0, 1)[0]
|
|
7
|
+
const salt = payload.subarray(1, 17)
|
|
8
|
+
const iv = payload.subarray(17, 29)
|
|
9
|
+
const tag = payload.subarray(29, 45)
|
|
10
|
+
const ciphertext = payload.subarray(45)
|
|
11
|
+
const key = crypto.scryptSync(passphrase, salt, 32)
|
|
12
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv)
|
|
13
|
+
|
|
14
|
+
if (version !== 1) {
|
|
15
|
+
throw new Error('unsupported locked private key version')
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
decipher.setAuthTag(tag)
|
|
19
|
+
|
|
20
|
+
return Buffer.concat([
|
|
21
|
+
decipher.update(ciphertext),
|
|
22
|
+
decipher.final()
|
|
23
|
+
]).toString('utf8')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = unlockedValue
|