@dotenvx/dotenvx 2.24.1 → 2.26.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 +13 -1
- package/README.md +12 -0
- package/package.json +1 -1
- package/src/cli/actions/encrypt.js +6 -3
- package/src/cli/actions/keypair.js +2 -0
- package/src/cli/actions/set.js +4 -1
- package/src/cli/dotenvx.js +6 -0
- package/src/lib/helpers/bitwardenCustody.js +162 -0
- package/src/lib/helpers/linuxSecretService.js +13 -6
- package/src/lib/helpers/macosKeychain.js +17 -7
- package/src/lib/helpers/nativeStoreError.js +10 -0
- package/src/lib/helpers/onePasswordCustody.js +159 -0
- package/src/lib/helpers/selectKeyStorage.js +34 -0
- package/src/lib/helpers/storeNativePrivateKey.js +24 -0
- package/src/lib/helpers/windowsCredentialManager.js +9 -6
- package/src/lib/main.d.ts +7 -0
- package/src/lib/main.js +2 -0
- package/src/lib/providers/index.js +16 -0
- package/src/lib/services/keychainUp.js +3 -0
- package/src/lib/transforms/encrypt.js +13 -19
- package/src/lib/transforms/set.js +17 -20
package/CHANGELOG.md
CHANGED
|
@@ -2,7 +2,19 @@
|
|
|
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.26.0...main)
|
|
6
|
+
|
|
7
|
+
## [2.25.0](https://github.com/dotenvx/dotenvx/compare/v2.25.0...v2.26.0) (2026-09-15)
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
* Add support for storing private key to bitwarden or 1password ([#968](https://github.com/dotenvx/dotenvx/pull/968))
|
|
12
|
+
|
|
13
|
+
## [2.25.0](https://github.com/dotenvx/dotenvx/compare/v2.24.1...v2.25.0) (2026-09-14)
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
* Store private key to os secret store rather than `.env.keys` by default ([#967](https://github.com/dotenvx/dotenvx/pull/967))
|
|
6
18
|
|
|
7
19
|
## [2.24.1](https://github.com/dotenvx/dotenvx/compare/v2.24.0...v2.24.1) (2026-09-11)
|
|
8
20
|
|
package/README.md
CHANGED
|
@@ -872,6 +872,18 @@ $ dotenvx encrypt
|
|
|
872
872
|
|
|
873
873
|
> A `DOTENV_PUBLIC_KEY` (encryption key) and a `DOTENV_PRIVATE_KEY` (decryption key) are generated using the same public-key cryptography as [Bitcoin](https://en.bitcoin.it/wiki/Secp256k1).
|
|
874
874
|
|
|
875
|
+
New private keys created by `encrypt` and encrypted `set` default to your OS secret store: macOS Keychain, Windows Credential Manager, or Linux Secret Service. The interactive picker offers Local Custody through your OS secret store, Local Custody through 1Password when available, and Managed Custody through Armor when logged in. When only the OS store is available, it is selected automatically. Existing `.env.keys` files are not automatically migrated.
|
|
876
|
+
|
|
877
|
+
Linux requires `secret-tool` (typically the `libsecret-tools` package), a running Secret Service such as GNOME Keyring, and a user D-Bus session. When native tooling or the service is missing, dotenvx reports the fallback to `.env.keys`. Locked, denied, timed-out, or unverified storage operations fail without falling back to a plaintext key file.
|
|
878
|
+
|
|
879
|
+
CI uses file storage. To explicitly use file storage elsewhere, run `dotenvx encrypt --no-native --no-armor --no-1password` (or pass all three flags to `set`). For deployment, continue supplying the private key through your platform's secret injection.
|
|
880
|
+
|
|
881
|
+
The 1Password option appears when CLI version 2 is installed and an account is configured (or `OP_SERVICE_ACCOUNT_TOKEN` is set). Detection does not sign in or unlock your vault. Selecting it authenticates as needed, uses the default vault selected by the 1Password CLI, and verifies the saved key before writing the encrypted env file. An unavailable, denied, or cancelled 1Password operation fails without falling back to local key storage.
|
|
882
|
+
|
|
883
|
+
1Password custody stores the key in your default vault and saves only its account/item reference in dotenvx's per-user settings. `run`, `get`, and `keypair` then retrieve it automatically, subject to 1Password authorization. `--no-1password` (where supported) or `DOTENVX_NO_1PASSWORD=true` disables that integration. The local reference is needed for automatic lookup on this machine; on another machine, the key is still accessible in the vault but its locator must also be configured for automatic lookup. No plaintext private key is written to `.env.keys` or passed in command arguments. The “Local Custody” label refers to using your own password manager; 1Password vaults may sync off-machine.
|
|
884
|
+
|
|
885
|
+
To move an existing key into the OS store, run `dotenvx native up`. It verifies the stored key before removing it from `.env.keys`. Use `dotenvx native push` to keep a file copy, `dotenvx native pull` to export a copy, or `dotenvx native down` to move it back to `.env.keys`. Add `-f .env.production` for a particular env file. Keep a recoverable copy in your team's secret manager or backup before replacing or losing the machine.
|
|
886
|
+
|
|
875
887
|
More examples
|
|
876
888
|
|
|
877
889
|
<details><summary>`.env`</summary><br>
|
package/package.json
CHANGED
|
@@ -23,11 +23,14 @@ async function encryptAction () {
|
|
|
23
23
|
const noArmor = options.armor === false || (!options.token && (await sesh.noArmor()))
|
|
24
24
|
const noKeychain = options.native === false || options.noNative === true
|
|
25
25
|
|
|
26
|
+
const noBitwarden = options.bitwarden === false || options.noBitwarden === true
|
|
27
|
+
const no1Password = options['1password'] === false || options.no1Password === true
|
|
28
|
+
|
|
26
29
|
let errorCount = 0
|
|
27
30
|
|
|
28
31
|
// stdout - should not have a try so that exit codes can surface to stdout
|
|
29
32
|
if (options.stdout) {
|
|
30
|
-
const { processedEnvs } = await encryptTransform({ envs, ik, ek, fk, noArmor, noCreate, noKeychain })
|
|
33
|
+
const { processedEnvs } = await encryptTransform({ envs, ik, ek, fk, noArmor, noCreate, noKeychain, no1Password, noBitwarden })
|
|
31
34
|
|
|
32
35
|
if (spinner) spinner.stop()
|
|
33
36
|
for (const processedEnv of processedEnvs) {
|
|
@@ -35,7 +38,7 @@ async function encryptAction () {
|
|
|
35
38
|
errorCount += 1
|
|
36
39
|
logger.error(processedEnv.error.messageWithHelp || processedEnv.error.message)
|
|
37
40
|
}
|
|
38
|
-
if (processedEnv.envSrc) {
|
|
41
|
+
if (!processedEnv.error && processedEnv.envSrc) {
|
|
39
42
|
console.log(processedEnv.envSrc)
|
|
40
43
|
}
|
|
41
44
|
}
|
|
@@ -48,7 +51,7 @@ async function encryptAction () {
|
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
try {
|
|
51
|
-
const { keysSrc, processedEnvs, changedFilepaths, unchangedFilepaths } = await encryptTransform({ envs, ik, ek, fk, noArmor, noCreate, noKeychain })
|
|
54
|
+
const { keysSrc, processedEnvs, changedFilepaths, unchangedFilepaths } = await encryptTransform({ envs, ik, ek, fk, noArmor, noCreate, noKeychain, no1Password, noBitwarden })
|
|
52
55
|
|
|
53
56
|
if (keysSrc) {
|
|
54
57
|
await fsx.writeFileX(fk, keysSrc)
|
|
@@ -22,6 +22,8 @@ async function keypair (key) {
|
|
|
22
22
|
envKeysFile: options.envKeysFile,
|
|
23
23
|
armor: options.armor,
|
|
24
24
|
noKeychain: options.native === false || options.noNative === true,
|
|
25
|
+
noBitwarden: options.bitwarden === false || options.noBitwarden === true,
|
|
26
|
+
no1Password: options['1password'] === false || options.no1Password === true,
|
|
25
27
|
token: options.token,
|
|
26
28
|
command: process.argv.slice(2),
|
|
27
29
|
onStatus: (text) => {
|
package/src/cli/actions/set.js
CHANGED
|
@@ -61,10 +61,13 @@ async function set (key, value) {
|
|
|
61
61
|
const noArmor = options.armor === false || (!options.token && (await sesh.noArmor()))
|
|
62
62
|
const noKeychain = options.native === false || options.noNative === true
|
|
63
63
|
|
|
64
|
+
const noBitwarden = options.bitwarden === false || options.noBitwarden === true
|
|
65
|
+
const no1Password = options['1password'] === false || options.no1Password === true
|
|
66
|
+
|
|
64
67
|
let errorCount = 0
|
|
65
68
|
|
|
66
69
|
try {
|
|
67
|
-
const { keysSrc, processedEnvs, changedFilepaths, unchangedFilepaths } = await setTransform({ envs, key, value, fk, noArmor, noCreate, encrypt, noKeychain })
|
|
70
|
+
const { keysSrc, processedEnvs, changedFilepaths, unchangedFilepaths } = await setTransform({ envs, key, value, fk, noArmor, noCreate, encrypt, noKeychain, no1Password, noBitwarden })
|
|
68
71
|
|
|
69
72
|
if (keysSrc) {
|
|
70
73
|
await fsx.writeFileX(fk, keysSrc)
|
package/src/cli/dotenvx.js
CHANGED
|
@@ -131,6 +131,8 @@ program.command('set')
|
|
|
131
131
|
.option('--no-create', 'do not create .env file(s) when missing')
|
|
132
132
|
.option('--no-armor', 'disable Dotenvx Armor features')
|
|
133
133
|
.option('--no-native', 'disable OS secret store features')
|
|
134
|
+
.option('--no-1password', 'disable 1Password features')
|
|
135
|
+
.option('--no-bitwarden', 'disable Bitwarden features')
|
|
134
136
|
.action(function (...args) {
|
|
135
137
|
this.envs = envs
|
|
136
138
|
return require('./actions/set').apply(this, args)
|
|
@@ -161,6 +163,8 @@ function configureEncryptCommand (command) {
|
|
|
161
163
|
.option('--no-create', 'do not create .env file(s) when missing')
|
|
162
164
|
.option('--no-armor', 'disable Dotenvx Armor features')
|
|
163
165
|
.option('--no-native', 'disable OS secret store features')
|
|
166
|
+
.option('--no-1password', 'disable 1Password features')
|
|
167
|
+
.option('--no-bitwarden', 'disable Bitwarden features')
|
|
164
168
|
.action(function (...args) {
|
|
165
169
|
this.envs = envs
|
|
166
170
|
return require('./actions/encrypt').apply(this, args)
|
|
@@ -198,6 +202,8 @@ program.command('keypair')
|
|
|
198
202
|
.option('-fk, --env-keys-file <path>', 'path(s) to your .env.keys file(s) (default: same path as your env file)', collectEnvKeys)
|
|
199
203
|
.option('--no-armor', 'disable Dotenvx Armor features')
|
|
200
204
|
.option('--no-native', 'disable OS secret store features')
|
|
205
|
+
.option('--no-1password', 'disable 1Password features')
|
|
206
|
+
.option('--no-bitwarden', 'disable Bitwarden features')
|
|
201
207
|
.option('-pp, --pretty-print', 'pretty print output')
|
|
202
208
|
.option('--pp', 'pretty print output (alias)')
|
|
203
209
|
.option('--format <type>', 'format of the output (json, shell, colon)', 'json')
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
const { execFile, execFileSync } = require('child_process')
|
|
2
|
+
const { derive } = require('@dotenvx/primitives')
|
|
3
|
+
const Session = require('../../db/session')
|
|
4
|
+
const prompts = require('./prompts')
|
|
5
|
+
const createSpinner = require('./createSpinner')
|
|
6
|
+
let unlockedSession
|
|
7
|
+
|
|
8
|
+
const armoredKeyDisplay = require('./armoredKeyDisplay')
|
|
9
|
+
|
|
10
|
+
const PREFIX = 'DOTENVX_BITWARDEN_'
|
|
11
|
+
const ID = /^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/i
|
|
12
|
+
const COMMAND_OPTIONS = { encoding: 'utf8', windowsHide: true, timeout: 120000, killSignal: 'SIGKILL', maxBuffer: 1024 * 1024 }
|
|
13
|
+
|
|
14
|
+
function failure (message) {
|
|
15
|
+
const error = new Error(message)
|
|
16
|
+
error.code = 'BITWARDEN_FAILED'
|
|
17
|
+
return error
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function commandFailure () {
|
|
21
|
+
return failure('Bitwarden CLI operation failed; check sign-in and unlock your vault with BW_SESSION')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function run (args, input, timeout = COMMAND_OPTIONS.timeout, env = commandEnv()) {
|
|
25
|
+
return new Promise((resolve, reject) => {
|
|
26
|
+
const child = execFile('bw', [...args, '--nointeraction'], { ...COMMAND_OPTIONS, timeout, env }, (error, stdout) => {
|
|
27
|
+
if (error) reject(commandFailure())
|
|
28
|
+
else resolve(stdout)
|
|
29
|
+
})
|
|
30
|
+
if (child.stdin) {
|
|
31
|
+
child.stdin.on('error', () => {})
|
|
32
|
+
child.stdin.end(input)
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parse (value) {
|
|
38
|
+
try { return JSON.parse(value) } catch {
|
|
39
|
+
throw failure('Bitwarden CLI returned an invalid response')
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function available () {
|
|
44
|
+
try { return /^\d+\.\d+\.\d+/.test((await run(['--version'], undefined, 2000)).trim()) } catch { return false }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function configured () {
|
|
48
|
+
const store = new Session().openStore()
|
|
49
|
+
return !!store && Object.keys(store.store).some(key => key.startsWith(PREFIX))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function location (publicKey) {
|
|
53
|
+
const store = new Session().openStore()
|
|
54
|
+
const value = store && store.get(`${PREFIX}${publicKey}`)
|
|
55
|
+
if (!value) return null
|
|
56
|
+
// Base64 keeps the nonsecret JSON locator safe in the dotenv settings file.
|
|
57
|
+
const loc = parse(Buffer.from(String(value), 'base64').toString('utf8'))
|
|
58
|
+
if (!loc || !ID.test(loc.item || '') || !ID.test(loc.userId || '') || typeof loc.serverUrl !== 'string') {
|
|
59
|
+
throw failure('invalid Bitwarden private-key reference in dotenvx settings')
|
|
60
|
+
}
|
|
61
|
+
return loc
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function commandEnv () {
|
|
65
|
+
return { ...process.env, ...(unlockedSession ? { BW_SESSION: unlockedSession } : {}) }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function authenticate (loc) {
|
|
69
|
+
let status = parse(await run(['status']))
|
|
70
|
+
if (loc && status && status.userId && (status.userId !== loc.userId || (status.serverUrl || '') !== loc.serverUrl)) {
|
|
71
|
+
throw failure('Bitwarden account or server does not match the stored private-key reference')
|
|
72
|
+
}
|
|
73
|
+
if (status && status.status === 'unlocked') return checkIdentity(status, loc)
|
|
74
|
+
unlockedSession = undefined
|
|
75
|
+
if (status && status.status === 'unauthenticated') throw failure('Sign in to Bitwarden once with bw login, then retry; dotenvx will prompt to unlock your vault')
|
|
76
|
+
if (!status || status.status !== 'locked') throw commandFailure()
|
|
77
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY || process.env.CI) {
|
|
78
|
+
throw failure('Bitwarden is locked; set an unlocked BW_SESSION for noninteractive use')
|
|
79
|
+
}
|
|
80
|
+
let password
|
|
81
|
+
createSpinner.pause()
|
|
82
|
+
try {
|
|
83
|
+
password = await prompts.password({ message: 'Bitwarden master password', prefix: '◇', separator: '=' }, { input: process.stdin, output: process.stderr })
|
|
84
|
+
} finally {
|
|
85
|
+
createSpinner.resume()
|
|
86
|
+
}
|
|
87
|
+
const passwordEnv = 'DOTENVX_BITWARDEN_PASSWORD'
|
|
88
|
+
const session = (await run(['unlock', '--passwordenv', passwordEnv, '--raw'], undefined, COMMAND_OPTIONS.timeout, { ...process.env, [passwordEnv]: password })).trim()
|
|
89
|
+
if (!session) throw commandFailure()
|
|
90
|
+
unlockedSession = session
|
|
91
|
+
try {
|
|
92
|
+
status = parse(await run(['status']))
|
|
93
|
+
return checkIdentity(status, loc)
|
|
94
|
+
} catch (error) {
|
|
95
|
+
unlockedSession = undefined
|
|
96
|
+
throw error
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function requireSession () {
|
|
101
|
+
if (!unlockedSession && !process.env.BW_SESSION) throw failure('Bitwarden requires an unlocked BW_SESSION; run bw login, then bw unlock --raw and set BW_SESSION')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function checkIdentity (status, loc) {
|
|
105
|
+
if (!status || status.status !== 'unlocked' || !ID.test(status.userId || '')) throw commandFailure()
|
|
106
|
+
if (loc && (status.userId !== loc.userId || (status.serverUrl || '') !== loc.serverUrl)) {
|
|
107
|
+
throw failure('Bitwarden account or server does not match the stored private-key reference')
|
|
108
|
+
}
|
|
109
|
+
return status
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function verified (publicKey, privateKey) {
|
|
113
|
+
try { if (derive(privateKey) === publicKey) return { [publicKey]: privateKey } } catch {}
|
|
114
|
+
throw failure('Bitwarden private key does not match the .env public key')
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function get (publicKey) {
|
|
118
|
+
const loc = location(publicKey)
|
|
119
|
+
if (!loc) return {}
|
|
120
|
+
await authenticate(loc)
|
|
121
|
+
return verified(publicKey, (await run(['get', 'password', loc.item])).trim())
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getSync (publicKey) {
|
|
125
|
+
const loc = location(publicKey)
|
|
126
|
+
if (!loc) return {}
|
|
127
|
+
requireSession()
|
|
128
|
+
function read (args) {
|
|
129
|
+
try { return execFileSync('bw', [...args, '--nointeraction'], { ...COMMAND_OPTIONS, env: commandEnv(), stdio: ['ignore', 'pipe', 'pipe'] }) } catch { throw commandFailure() }
|
|
130
|
+
}
|
|
131
|
+
checkIdentity(parse(read(['status'])), loc)
|
|
132
|
+
return verified(publicKey, read(['get', 'password', loc.item]).trim())
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function set (publicKey, privateKey) {
|
|
136
|
+
verified(publicKey, privateKey)
|
|
137
|
+
if (location(publicKey)) {
|
|
138
|
+
await get(publicKey)
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
const status = await authenticate()
|
|
142
|
+
const template = {
|
|
143
|
+
organizationId: null,
|
|
144
|
+
collectionIds: [],
|
|
145
|
+
folderId: null,
|
|
146
|
+
type: 1,
|
|
147
|
+
name: `dotenvx (${armoredKeyDisplay(publicKey)})`,
|
|
148
|
+
notes: null,
|
|
149
|
+
favorite: false,
|
|
150
|
+
fields: [{ name: 'public_key', value: publicKey, type: 0 }],
|
|
151
|
+
login: { username: 'private_key', password: privateKey, totp: null, uris: [] }
|
|
152
|
+
}
|
|
153
|
+
// Pass encoded JSON on stdin, never secrets in command arguments or files.
|
|
154
|
+
const item = parse(await run(['create', 'item'], Buffer.from(JSON.stringify(template)).toString('base64')))
|
|
155
|
+
if (!item || !ID.test(item.id || '') || item.organizationId) throw failure('Bitwarden did not return a personal vault item')
|
|
156
|
+
const saved = (await run(['get', 'password', item.id])).trim()
|
|
157
|
+
verified(publicKey, saved)
|
|
158
|
+
const loc = { item: item.id, userId: status.userId, serverUrl: status.serverUrl || '' }
|
|
159
|
+
new Session().createStore().set(`${PREFIX}${publicKey}`, Buffer.from(JSON.stringify(loc)).toString('base64'))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
module.exports = { available, configured, get, getSync, set }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { execFileSync } = require('child_process')
|
|
2
|
+
const nativeStoreError = require('./nativeStoreError')
|
|
2
3
|
|
|
3
4
|
const SECRET_TOOL_BIN = 'secret-tool'
|
|
4
5
|
const SERVICE = 'dotenvx'
|
|
@@ -10,11 +11,13 @@ function attributes (publicKey) {
|
|
|
10
11
|
function get (publicKey) {
|
|
11
12
|
try {
|
|
12
13
|
return execFileSync(SECRET_TOOL_BIN, ['lookup', ...attributes(publicKey)], {
|
|
14
|
+
timeout: 10000,
|
|
15
|
+
killSignal: 'SIGKILL',
|
|
13
16
|
encoding: 'utf8',
|
|
14
17
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
15
18
|
}).trim() || null
|
|
16
|
-
} catch {
|
|
17
|
-
throw
|
|
19
|
+
} catch (error) {
|
|
20
|
+
throw nativeStoreError('failed to read private key from Linux Secret Service', error, true)
|
|
18
21
|
}
|
|
19
22
|
}
|
|
20
23
|
|
|
@@ -22,11 +25,13 @@ function set (publicKey, privateKey, label) {
|
|
|
22
25
|
try {
|
|
23
26
|
execFileSync(SECRET_TOOL_BIN, ['store', `--label=${label}`, ...attributes(publicKey)], {
|
|
24
27
|
input: privateKey,
|
|
28
|
+
timeout: 10000,
|
|
29
|
+
killSignal: 'SIGKILL',
|
|
25
30
|
encoding: 'utf8',
|
|
26
31
|
stdio: ['pipe', 'ignore', 'pipe']
|
|
27
32
|
})
|
|
28
|
-
} catch {
|
|
29
|
-
throw
|
|
33
|
+
} catch (error) {
|
|
34
|
+
throw nativeStoreError('failed to save private key to Linux Secret Service', error, true)
|
|
30
35
|
}
|
|
31
36
|
}
|
|
32
37
|
|
|
@@ -36,10 +41,12 @@ module.exports = {
|
|
|
36
41
|
delete (publicKey) {
|
|
37
42
|
try {
|
|
38
43
|
execFileSync(SECRET_TOOL_BIN, ['clear', ...attributes(publicKey)], {
|
|
44
|
+
timeout: 10000,
|
|
45
|
+
killSignal: 'SIGKILL',
|
|
39
46
|
stdio: ['ignore', 'ignore', 'pipe']
|
|
40
47
|
})
|
|
41
|
-
} catch {
|
|
42
|
-
throw
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw nativeStoreError('failed to delete private key from Linux Secret Service', error, true)
|
|
43
50
|
}
|
|
44
51
|
}
|
|
45
52
|
}
|
|
@@ -1,26 +1,36 @@
|
|
|
1
1
|
const { execFileSync } = require('child_process')
|
|
2
|
+
const nativeStoreError = require('./nativeStoreError')
|
|
2
3
|
|
|
3
4
|
const SECURITY_BIN = '/usr/bin/security'
|
|
4
5
|
const SERVICE = 'dotenvx'
|
|
5
6
|
|
|
6
7
|
module.exports = {
|
|
7
8
|
get (key) {
|
|
8
|
-
|
|
9
|
+
try {
|
|
10
|
+
return execFileSync(SECURITY_BIN, ['find-generic-password', '-s', SERVICE, '-a', key, '-w'], {
|
|
11
|
+
timeout: 10000,
|
|
12
|
+
killSignal: 'SIGKILL',
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
15
|
+
}).trim()
|
|
16
|
+
} catch (error) {
|
|
17
|
+
throw nativeStoreError('failed to read private key from macOS Keychain', error)
|
|
18
|
+
}
|
|
9
19
|
},
|
|
10
20
|
|
|
11
21
|
set (key, value, label) {
|
|
12
22
|
try {
|
|
13
|
-
execFileSync(SECURITY_BIN, ['add-generic-password', '-U', '-s', SERVICE, '-a', key, '-l', label, '-w', value], { stdio: 'ignore' })
|
|
14
|
-
} catch {
|
|
15
|
-
throw
|
|
23
|
+
execFileSync(SECURITY_BIN, ['add-generic-password', '-U', '-s', SERVICE, '-a', key, '-l', label, '-w', value], { timeout: 10000, killSignal: 'SIGKILL', stdio: 'ignore' })
|
|
24
|
+
} catch (error) {
|
|
25
|
+
throw nativeStoreError('failed to save private key to macOS Keychain', error)
|
|
16
26
|
}
|
|
17
27
|
},
|
|
18
28
|
|
|
19
29
|
delete (key) {
|
|
20
30
|
try {
|
|
21
|
-
execFileSync(SECURITY_BIN, ['delete-generic-password', '-s', SERVICE, '-a', key], { stdio: 'ignore' })
|
|
22
|
-
} catch {
|
|
23
|
-
throw
|
|
31
|
+
execFileSync(SECURITY_BIN, ['delete-generic-password', '-s', SERVICE, '-a', key], { timeout: 10000, killSignal: 'SIGKILL', stdio: 'ignore' })
|
|
32
|
+
} catch (error) {
|
|
33
|
+
throw nativeStoreError('failed to delete private key from macOS Keychain', error)
|
|
24
34
|
}
|
|
25
35
|
}
|
|
26
36
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Never attach subprocess errors: their command/output may contain private keys.
|
|
2
|
+
function nativeStoreError (message, error, linux = false) {
|
|
3
|
+
const unavailable = error.code === 'ENOENT' || (linux &&
|
|
4
|
+
/org\.freedesktop\.DBus\.Error\.(ServiceUnknown|NoServer)|Cannot autolaunch D-Bus without X11|Failed to connect to socket .*: No such file or directory/.test(String(error.stderr || '')))
|
|
5
|
+
const result = new Error(message)
|
|
6
|
+
result.code = unavailable ? 'NATIVE_UNAVAILABLE' : 'NATIVE_ACCESS_FAILED'
|
|
7
|
+
return result
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
module.exports = nativeStoreError
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
const { execFile, execFileSync } = require('child_process')
|
|
2
|
+
const { derive } = require('@dotenvx/primitives')
|
|
3
|
+
const Session = require('../../db/session')
|
|
4
|
+
const armoredKeyDisplay = require('./armoredKeyDisplay')
|
|
5
|
+
|
|
6
|
+
const PREFIX = 'DOTENVX_ONEPASSWORD_'
|
|
7
|
+
const ID = /^[a-z0-9]{26}$/i
|
|
8
|
+
const COMMAND_OPTIONS = { encoding: 'utf8', windowsHide: true, timeout: 120000, killSignal: 'SIGKILL', maxBuffer: 1024 * 1024 }
|
|
9
|
+
|
|
10
|
+
function failure (message) {
|
|
11
|
+
const error = new Error(message)
|
|
12
|
+
error.code = '1PASSWORD_FAILED'
|
|
13
|
+
return error
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function commandError (args, stderr) {
|
|
17
|
+
const step = args[0] === 'item'
|
|
18
|
+
? 'create the key item'
|
|
19
|
+
: {
|
|
20
|
+
whoami: 'check the signed-in account',
|
|
21
|
+
signin: 'sign in',
|
|
22
|
+
read: 'read the private key'
|
|
23
|
+
}[args[0]] || 'check availability'
|
|
24
|
+
if (/account is not signed in/i.test(String(stderr || ''))) {
|
|
25
|
+
const error = failure(`1Password could not ${step}: account is not signed in. Sign in with op signin or enable the desktop app CLI integration`)
|
|
26
|
+
error.reason = 'NOT_SIGNED_IN'
|
|
27
|
+
return error
|
|
28
|
+
}
|
|
29
|
+
return failure(`1Password could not ${step}; check sign-in and vault permissions`)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function run (args, input, timeout = COMMAND_OPTIONS.timeout) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
// On Unix, Node supplies a socket for stdin. op only detects piped JSON
|
|
35
|
+
// through a real pipe, so cat bridges the socket without writing a file.
|
|
36
|
+
const pipeInput = input !== undefined && process.platform !== 'win32'
|
|
37
|
+
const command = pipeInput ? '/bin/sh' : 'op'
|
|
38
|
+
const commandArgs = pipeInput ? ['-c', 'cat | op "$@"', 'dotenvx-op', ...args] : args
|
|
39
|
+
let timer
|
|
40
|
+
const child = execFile(command, commandArgs, {
|
|
41
|
+
...COMMAND_OPTIONS,
|
|
42
|
+
timeout: pipeInput ? 0 : timeout,
|
|
43
|
+
...(pipeInput ? { detached: true } : {})
|
|
44
|
+
}, (error, stdout, stderr) => {
|
|
45
|
+
clearTimeout(timer)
|
|
46
|
+
// Subprocess errors may include secrets from stdin or stdout.
|
|
47
|
+
if (error) reject(commandError(args, stderr))
|
|
48
|
+
else resolve(stdout)
|
|
49
|
+
})
|
|
50
|
+
if (pipeInput) {
|
|
51
|
+
timer = setTimeout(() => {
|
|
52
|
+
// Kill the whole pipeline, including op, if authentication times out.
|
|
53
|
+
try { process.kill(-child.pid, 'SIGKILL') } catch {}
|
|
54
|
+
}, timeout)
|
|
55
|
+
timer.unref()
|
|
56
|
+
}
|
|
57
|
+
if (child.stdin) {
|
|
58
|
+
child.stdin.on('error', () => {})
|
|
59
|
+
child.stdin.end(input)
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseResponse (value) {
|
|
65
|
+
try { return JSON.parse(value) } catch {
|
|
66
|
+
throw failure('1Password CLI returned an invalid response')
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function available () {
|
|
71
|
+
try {
|
|
72
|
+
const version = await run(['--version'], undefined, 2000)
|
|
73
|
+
if (!/^2\./.test(version.trim())) return false
|
|
74
|
+
if (process.env.OP_SERVICE_ACCOUNT_TOKEN) return true
|
|
75
|
+
// Account discovery is local; authenticate only after the user selects 1Password.
|
|
76
|
+
const accounts = parseResponse(await run(['account', 'list', '--format=json'], undefined, 2000))
|
|
77
|
+
return Array.isArray(accounts) && accounts.length > 0
|
|
78
|
+
} catch {
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function configured () {
|
|
84
|
+
const store = new Session().openStore()
|
|
85
|
+
return !!store && Object.keys(store.store).some(key => key.startsWith(PREFIX))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function location (publicKey) {
|
|
89
|
+
const store = new Session().openStore()
|
|
90
|
+
const value = store && store.get(`${PREFIX}${publicKey}`)
|
|
91
|
+
if (!value) return null
|
|
92
|
+
const [account, reference] = String(value).split('|')
|
|
93
|
+
if (!ID.test(account) || !/^op:\/\/[a-z0-9]{26}\/[a-z0-9]{26}\/(?:private_key|password)$/i.test(reference || '')) {
|
|
94
|
+
throw failure('invalid 1Password private-key reference in dotenvx settings')
|
|
95
|
+
}
|
|
96
|
+
return { account, reference }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function verified (publicKey, privateKey) {
|
|
100
|
+
try {
|
|
101
|
+
if (derive(privateKey) === publicKey) return { [publicKey]: privateKey }
|
|
102
|
+
} catch {}
|
|
103
|
+
throw failure('1Password private key does not match the .env public key')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async function get (publicKey) {
|
|
107
|
+
const loc = location(publicKey)
|
|
108
|
+
if (!loc) return {}
|
|
109
|
+
const privateKey = await run(['read', loc.reference, '--no-newline', `--account=${loc.account}`])
|
|
110
|
+
return verified(publicKey, privateKey.trim())
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getSync (publicKey) {
|
|
114
|
+
const loc = location(publicKey)
|
|
115
|
+
if (!loc) return {}
|
|
116
|
+
let privateKey
|
|
117
|
+
try {
|
|
118
|
+
privateKey = execFileSync('op', ['read', loc.reference, '--no-newline', `--account=${loc.account}`], {
|
|
119
|
+
...COMMAND_OPTIONS, stdio: ['ignore', 'pipe', 'pipe']
|
|
120
|
+
}).trim()
|
|
121
|
+
} catch {
|
|
122
|
+
throw failure('1Password CLI could not read the private key; check sign-in and vault permissions')
|
|
123
|
+
}
|
|
124
|
+
return verified(publicKey, privateKey)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function set (publicKey, privateKey) {
|
|
128
|
+
let identity
|
|
129
|
+
try {
|
|
130
|
+
identity = parseResponse(await run(['whoami', '--format=json']))
|
|
131
|
+
} catch (error) {
|
|
132
|
+
if (error.reason !== 'NOT_SIGNED_IN' || process.env.OP_SERVICE_ACCOUNT_TOKEN) throw error
|
|
133
|
+
// whoami checks authentication but does not initiate the desktop app flow.
|
|
134
|
+
// Discard signin output; never print or evaluate a returned session token.
|
|
135
|
+
await run(['signin'])
|
|
136
|
+
identity = parseResponse(await run(['whoami', '--format=json']))
|
|
137
|
+
}
|
|
138
|
+
const account = identity.account_uuid
|
|
139
|
+
if (!ID.test(account || '')) throw failure('could not identify the signed-in 1Password account')
|
|
140
|
+
const item = parseResponse(await run(['item', 'create', '-', '--format=json', `--account=${account}`], JSON.stringify({
|
|
141
|
+
title: `dotenvx (${armoredKeyDisplay(publicKey)})`,
|
|
142
|
+
category: 'PASSWORD',
|
|
143
|
+
tags: ['dotenvx'],
|
|
144
|
+
fields: [
|
|
145
|
+
{ id: 'password', label: 'private_key', type: 'CONCEALED', purpose: 'PASSWORD', value: privateKey },
|
|
146
|
+
{ id: 'public_key', label: 'public_key', type: 'STRING', value: publicKey }
|
|
147
|
+
]
|
|
148
|
+
})))
|
|
149
|
+
if (!ID.test(item.id || '')) throw failure('1Password did not return a saved item ID')
|
|
150
|
+
const vault = item.vault && item.vault.id
|
|
151
|
+
if (!ID.test(vault || '')) throw failure('1Password did not return the saved item vault ID')
|
|
152
|
+
const reference = `op://${vault}/${item.id}/password`
|
|
153
|
+
const saved = await run(['read', reference, '--no-newline', `--account=${account}`])
|
|
154
|
+
if (saved.trim() !== privateKey) throw failure('could not verify private key in 1Password')
|
|
155
|
+
// Only a nonsecret locator is persisted locally, never the private key.
|
|
156
|
+
new Session().createStore().set(`${PREFIX}${publicKey}`, `${account}|${reference}`)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
module.exports = { available, configured, get, getSync, set }
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const prompts = require('./prompts')
|
|
2
|
+
const onePasswordCustody = require('./onePasswordCustody')
|
|
3
|
+
const bitwardenCustody = require('./bitwardenCustody')
|
|
4
|
+
|
|
5
|
+
const secretStoreNames = {
|
|
6
|
+
darwin: 'macOS Keychain',
|
|
7
|
+
win32: 'Windows Credential Manager',
|
|
8
|
+
linux: 'Linux Secret Service'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function selectKeyStorage (options = {}) {
|
|
12
|
+
const useNative = !options.noKeychain && !process.env.CI && ['darwin', 'linux', 'win32'].includes(process.platform)
|
|
13
|
+
const defaultStorage = useNative ? 'native' : 'file'
|
|
14
|
+
if (process.env.CI || options.noCreate || !process.stdin.isTTY || !process.stderr.isTTY) return defaultStorage
|
|
15
|
+
|
|
16
|
+
const choices = [
|
|
17
|
+
...(useNative ? [{ name: `□ Local Custody (${secretStoreNames[process.platform]})`, value: 'native' }] : [])
|
|
18
|
+
]
|
|
19
|
+
if (!options.no1Password && process.env.DOTENVX_NO_1PASSWORD !== 'true' && await onePasswordCustody.available()) {
|
|
20
|
+
choices.push({ name: '□ Local Custody (1Password)', value: 'onepassword' })
|
|
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
|
|
27
|
+
|
|
28
|
+
return prompts.select({
|
|
29
|
+
message: 'Choose private key storage',
|
|
30
|
+
choices
|
|
31
|
+
}, { input: process.stdin, output: process.stderr })
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = selectKeyStorage
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const nativeProvider = require('../providers/native')
|
|
2
|
+
const armoredKeyDisplay = require('./armoredKeyDisplay')
|
|
3
|
+
const { logger } = require('../../shared/logger')
|
|
4
|
+
|
|
5
|
+
function storeNativePrivateKey (publicKey, privateKey, keysFilepath) {
|
|
6
|
+
try {
|
|
7
|
+
nativeProvider.set(publicKey, privateKey, `dotenvx (${armoredKeyDisplay(publicKey)})`)
|
|
8
|
+
} catch (error) {
|
|
9
|
+
if (error.code !== 'NATIVE_UNAVAILABLE') throw error
|
|
10
|
+
logger.warn(`OS secret store unavailable; saving private key to ${keysFilepath}`)
|
|
11
|
+
return false
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// A successful write must be readable before committing the encrypted file.
|
|
15
|
+
// Read failures must never fall back to plaintext storage.
|
|
16
|
+
if (nativeProvider.get(publicKey) !== privateKey) {
|
|
17
|
+
const error = new Error('could not verify private key in OS secret store')
|
|
18
|
+
error.code = 'NATIVE_VERIFY_FAILED'
|
|
19
|
+
throw error
|
|
20
|
+
}
|
|
21
|
+
return true
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
module.exports = storeNativePrivateKey
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { execFileSync } = require('child_process')
|
|
2
|
+
const nativeStoreError = require('./nativeStoreError')
|
|
2
3
|
|
|
3
4
|
const POWERSHELL_BIN = 'powershell.exe'
|
|
4
5
|
const SERVICE = 'dotenvx'
|
|
@@ -128,6 +129,8 @@ function target (publicKey) {
|
|
|
128
129
|
function run (payload) {
|
|
129
130
|
return execFileSync(POWERSHELL_BIN, ['-NoProfile', '-NonInteractive', '-EncodedCommand', encodedScript], {
|
|
130
131
|
input: JSON.stringify(payload),
|
|
132
|
+
timeout: 10000,
|
|
133
|
+
killSignal: 'SIGKILL',
|
|
131
134
|
encoding: 'utf8',
|
|
132
135
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
133
136
|
windowsHide: true
|
|
@@ -137,16 +140,16 @@ function run (payload) {
|
|
|
137
140
|
function get (publicKey) {
|
|
138
141
|
try {
|
|
139
142
|
return run({ action: 'read', target: target(publicKey) }).trim() || null
|
|
140
|
-
} catch {
|
|
141
|
-
throw
|
|
143
|
+
} catch (error) {
|
|
144
|
+
throw nativeStoreError('failed to read private key from Windows Credential Manager', error)
|
|
142
145
|
}
|
|
143
146
|
}
|
|
144
147
|
|
|
145
148
|
function set (publicKey, privateKey) {
|
|
146
149
|
try {
|
|
147
150
|
run({ action: 'write', target: target(publicKey), username: publicKey, secret: privateKey })
|
|
148
|
-
} catch {
|
|
149
|
-
throw
|
|
151
|
+
} catch (error) {
|
|
152
|
+
throw nativeStoreError('failed to save private key to Windows Credential Manager', error)
|
|
150
153
|
}
|
|
151
154
|
}
|
|
152
155
|
|
|
@@ -156,8 +159,8 @@ module.exports = {
|
|
|
156
159
|
delete (publicKey) {
|
|
157
160
|
try {
|
|
158
161
|
run({ action: 'delete', target: target(publicKey) })
|
|
159
|
-
} catch {
|
|
160
|
-
throw
|
|
162
|
+
} catch (error) {
|
|
163
|
+
throw nativeStoreError('failed to delete private key from Windows Credential Manager', error)
|
|
161
164
|
}
|
|
162
165
|
}
|
|
163
166
|
}
|
package/src/lib/main.d.ts
CHANGED
|
@@ -293,6 +293,12 @@ export interface SetOptions {
|
|
|
293
293
|
*/
|
|
294
294
|
noNative?: boolean;
|
|
295
295
|
|
|
296
|
+
/** Disable 1Password custody and secret-reference resolution. */
|
|
297
|
+
no1Password?: boolean;
|
|
298
|
+
|
|
299
|
+
/** Disable Bitwarden custody and secret-reference resolution. */
|
|
300
|
+
noBitwarden?: boolean;
|
|
301
|
+
|
|
296
302
|
}
|
|
297
303
|
|
|
298
304
|
export type SetProcessedEnv = {
|
|
@@ -306,6 +312,7 @@ export type SetProcessedEnv = {
|
|
|
306
312
|
publicKey?: string;
|
|
307
313
|
privateKey?: string;
|
|
308
314
|
localPrivateKeyAdded?: boolean;
|
|
315
|
+
nativePrivateKeyAdded?: boolean;
|
|
309
316
|
remotePrivateKeyAdded?: boolean;
|
|
310
317
|
privateKeyName?: string;
|
|
311
318
|
error?: Error;
|
package/src/lib/main.js
CHANGED
|
@@ -2,6 +2,8 @@ const Session = require('./../../db/session')
|
|
|
2
2
|
|
|
3
3
|
const armorProvider = require('./armor/index')
|
|
4
4
|
const nativeProvider = require('./native/index')
|
|
5
|
+
const onePasswordCustody = require('../helpers/onePasswordCustody')
|
|
6
|
+
const bitwardenCustody = require('../helpers/bitwardenCustody')
|
|
5
7
|
|
|
6
8
|
function syncArmorProvider (publicKeyHex) {
|
|
7
9
|
const { createSyncFn } = require('@dotenvx/tooling')
|
|
@@ -49,6 +51,14 @@ function useNative (options) {
|
|
|
49
51
|
return options.noNative !== true && options.native !== false && options.noKeychain !== true
|
|
50
52
|
}
|
|
51
53
|
|
|
54
|
+
function useOnePassword (options) {
|
|
55
|
+
return options.no1Password !== true && process.env.DOTENVX_NO_1PASSWORD !== 'true' && onePasswordCustody.configured()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function useBitwarden (options) {
|
|
59
|
+
return options.noBitwarden !== true && process.env.DOTENVX_NO_BITWARDEN !== 'true' && bitwardenCustody.configured()
|
|
60
|
+
}
|
|
61
|
+
|
|
52
62
|
function useArmor (options, noArmor) {
|
|
53
63
|
return options.noArmor !== true && options.armor !== false && !noArmor
|
|
54
64
|
}
|
|
@@ -71,6 +81,9 @@ async function providers (options = {}) {
|
|
|
71
81
|
providerFns.push(nativeProvider)
|
|
72
82
|
}
|
|
73
83
|
|
|
84
|
+
if (useOnePassword(options)) providerFns.push(onePasswordCustody.get)
|
|
85
|
+
if (useBitwarden(options)) providerFns.push(bitwardenCustody.get)
|
|
86
|
+
|
|
74
87
|
if (options.noArmor !== true && options.armor !== false) {
|
|
75
88
|
const sesh = new Session()
|
|
76
89
|
const noArmor = !options.token && await sesh.noArmor()
|
|
@@ -93,6 +106,9 @@ providers.sync = function providersSync (options = {}) {
|
|
|
93
106
|
providerFns.push(nativeProvider)
|
|
94
107
|
}
|
|
95
108
|
|
|
109
|
+
if (useOnePassword(options)) providerFns.push(onePasswordCustody.getSync)
|
|
110
|
+
if (useBitwarden(options)) providerFns.push(bitwardenCustody.getSync)
|
|
111
|
+
|
|
96
112
|
if (options.noArmor !== true && options.armor !== false) {
|
|
97
113
|
const sesh = new Session()
|
|
98
114
|
const noArmor = !options.token && sesh.noArmorSync()
|
|
@@ -54,6 +54,9 @@ class KeychainUp {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
nativeProvider.set(publicKey, privateKey, label)
|
|
57
|
+
if (nativeProvider.get(publicKey) !== privateKey) {
|
|
58
|
+
throw new Error('could not verify private key in OS secret store; .env.keys unchanged')
|
|
59
|
+
}
|
|
57
60
|
removeEnvKey(privateKeyName, envKeysFile)
|
|
58
61
|
|
|
59
62
|
return {
|
|
@@ -16,27 +16,17 @@ const teamChoicesFromMeta = require('../helpers/teamChoicesFromMeta')
|
|
|
16
16
|
const isTeamRequiredError = require('../helpers/isTeamRequiredError')
|
|
17
17
|
const Session = require('../../db/session')
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{ name: '◫ File (.env.keys)', value: 'file' },
|
|
24
|
-
{ name: '⛨ Armor (armor.dotenvx.com)', value: 'armored' }
|
|
25
|
-
]
|
|
26
|
-
}, {
|
|
27
|
-
input: process.stdin,
|
|
28
|
-
output: process.stderr
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
return selected
|
|
32
|
-
}
|
|
19
|
+
const selectKeyStorage = require('../helpers/selectKeyStorage')
|
|
20
|
+
const storeNativePrivateKey = require('../helpers/storeNativePrivateKey')
|
|
21
|
+
const onePasswordCustody = require('../helpers/onePasswordCustody')
|
|
22
|
+
const bitwardenCustody = require('../helpers/bitwardenCustody')
|
|
33
23
|
|
|
34
24
|
async function encryptTransform (options = {}) {
|
|
35
25
|
const envs = options.envs || []
|
|
36
26
|
const ik = options.ik
|
|
37
27
|
const ek = options.ek
|
|
38
28
|
const fk = options.fk || '.env.keys'
|
|
39
|
-
let
|
|
29
|
+
let storage
|
|
40
30
|
const noCreate = options.noCreate
|
|
41
31
|
|
|
42
32
|
const processedEnvs = []
|
|
@@ -85,9 +75,7 @@ async function encryptTransform (options = {}) {
|
|
|
85
75
|
let publicKey = publickeys(row.envSrc)[0]
|
|
86
76
|
|
|
87
77
|
if (!publicKey) {
|
|
88
|
-
|
|
89
|
-
noArmor = await selectKeyStorage() !== 'armored'
|
|
90
|
-
}
|
|
78
|
+
storage = storage || await selectKeyStorage(options)
|
|
91
79
|
|
|
92
80
|
// upsert public key to .env file
|
|
93
81
|
const kp = keypair() // local
|
|
@@ -99,7 +87,13 @@ async function encryptTransform (options = {}) {
|
|
|
99
87
|
|
|
100
88
|
const comment = path.basename(envFilepath)
|
|
101
89
|
|
|
102
|
-
if (
|
|
90
|
+
if (storage === 'bitwarden') {
|
|
91
|
+
await bitwardenCustody.set(publicKey, privateKey)
|
|
92
|
+
} else if (storage === 'onepassword') {
|
|
93
|
+
await onePasswordCustody.set(publicKey, privateKey)
|
|
94
|
+
} else if (storage === 'native' && storeNativePrivateKey(publicKey, privateKey, fk)) {
|
|
95
|
+
row.nativePrivateKeyAdded = true
|
|
96
|
+
} else if (storage !== 'armored') {
|
|
103
97
|
const mutated = mutateKeysSrc({ keysSrc, privateKeyName, privateKeyValue: privateKey, comment })
|
|
104
98
|
keysSrc = mutated.keysSrc
|
|
105
99
|
} else {
|
|
@@ -16,27 +16,18 @@ const teamChoicesFromMeta = require('../helpers/teamChoicesFromMeta')
|
|
|
16
16
|
const isTeamRequiredError = require('../helpers/isTeamRequiredError')
|
|
17
17
|
const Session = require('../../db/session')
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
{ name: '◫ File (.env.keys)', value: 'file' },
|
|
24
|
-
{ name: '⛨ Armor (armor.dotenvx.com)', value: 'armored' }
|
|
25
|
-
]
|
|
26
|
-
}, {
|
|
27
|
-
input: process.stdin,
|
|
28
|
-
output: process.stderr
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
return selected
|
|
32
|
-
}
|
|
19
|
+
const selectKeyStorage = require('../helpers/selectKeyStorage')
|
|
20
|
+
const storeNativePrivateKey = require('../helpers/storeNativePrivateKey')
|
|
21
|
+
const onePasswordCustody = require('../helpers/onePasswordCustody')
|
|
22
|
+
const bitwardenCustody = require('../helpers/bitwardenCustody')
|
|
33
23
|
|
|
34
24
|
async function setTransform (options = {}) {
|
|
35
25
|
const envs = options.envs || []
|
|
36
26
|
const key = options.key
|
|
37
27
|
const value = options.value
|
|
38
28
|
const fk = options.fk || '.env.keys'
|
|
39
|
-
|
|
29
|
+
const noArmor = options.noArmor
|
|
30
|
+
let storage
|
|
40
31
|
const noKeychain = options.noKeychain
|
|
41
32
|
const noCreate = options.noCreate
|
|
42
33
|
const noEncrypt = !options.encrypt || isPlainKey(key)
|
|
@@ -88,9 +79,7 @@ async function setTransform (options = {}) {
|
|
|
88
79
|
|
|
89
80
|
// only create if missing public key and encryption needed
|
|
90
81
|
if (!publicKey && !noEncrypt) {
|
|
91
|
-
|
|
92
|
-
noArmor = await selectKeyStorage() !== 'armored'
|
|
93
|
-
}
|
|
82
|
+
storage = storage || await selectKeyStorage(options)
|
|
94
83
|
|
|
95
84
|
// upsert public key to .env file
|
|
96
85
|
const kp = keypair() // local
|
|
@@ -102,7 +91,13 @@ async function setTransform (options = {}) {
|
|
|
102
91
|
|
|
103
92
|
const comment = path.basename(envFilepath)
|
|
104
93
|
|
|
105
|
-
if (
|
|
94
|
+
if (storage === 'bitwarden') {
|
|
95
|
+
await bitwardenCustody.set(publicKey, privateKey)
|
|
96
|
+
} else if (storage === 'onepassword') {
|
|
97
|
+
await onePasswordCustody.set(publicKey, privateKey)
|
|
98
|
+
} else if (storage === 'native' && storeNativePrivateKey(publicKey, privateKey, fk)) {
|
|
99
|
+
row.nativePrivateKeyAdded = true
|
|
100
|
+
} else if (storage !== 'armored') {
|
|
106
101
|
const mutated = mutateKeysSrc({ keysSrc, privateKeyName, privateKeyValue: privateKey, comment })
|
|
107
102
|
keysSrc = mutated.keysSrc
|
|
108
103
|
} else {
|
|
@@ -152,7 +147,9 @@ async function setTransform (options = {}) {
|
|
|
152
147
|
all: true,
|
|
153
148
|
envKeysFile: fk,
|
|
154
149
|
noArmor,
|
|
155
|
-
noKeychain
|
|
150
|
+
noKeychain,
|
|
151
|
+
no1Password: options.no1Password,
|
|
152
|
+
noBitwarden: options.noBitwarden
|
|
156
153
|
})
|
|
157
154
|
|
|
158
155
|
const before = parsed[key]
|