getmyenv 0.9.8 → 0.10.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 +2 -2
- package/dist/commands/backup.d.ts +1 -1
- package/dist/commands/backup.js +7 -2
- package/dist/commands/claim.js +7 -8
- package/dist/commands/restore.d.ts +1 -1
- package/dist/commands/restore.js +18 -8
- package/dist/lib/recovery-code.d.ts +19 -0
- package/dist/lib/recovery-code.js +57 -0
- package/dist/lib/upload.js +6 -3
- package/dist/lib/vault.d.ts +11 -0
- package/dist/lib/vault.js +36 -11
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -23,7 +23,7 @@ npx getmyenv run staging -- npm start # or pick one
|
|
|
23
23
|
- `export [-o file | --stdout] [--yes]`: for tools that need a file. The file is added to `.gitignore` and gets owner-only permissions on macOS and Linux.
|
|
24
24
|
- `guest [--name <name>]`: a project without an account. It works for 30 days. Claim it within 60.
|
|
25
25
|
- `claim`: claim a guest project. Reads `.getmyenv/claim.json`, or asks for the claim code and guest passphrase that `guest` printed once at a terminal.
|
|
26
|
-
- `backup [-o file]`: encrypted backup of open contexts (default `<project>.getmyenv-backup`). It opens with the Vault password in effect when it was made.
|
|
26
|
+
- `backup [-o file]`: encrypted backup of open contexts (default `<project>.getmyenv-backup`). It opens with the Vault password or the recovery code in effect when it was made.
|
|
27
27
|
- `restore <file> [--dry-run] [--yes]`: restore a backup into this folder's project. Values go into open contexts with the same slug and replace values with the same name. Read-only and missing contexts are skipped.
|
|
28
28
|
- `start`: same as bare `getmyenv`. `start --template <id or link>` adds a template's variable names and asks for each value.
|
|
29
29
|
- `lock`: forget every unlocked context key on this machine. `unlink [--yes]`, `help [command]`
|
|
@@ -35,7 +35,7 @@ Prompts and status lines go to stderr, so `export --stdout > .env` stays clean.
|
|
|
35
35
|
Commit `.getmyenv/project.json`. `token.json` and `claim.json` stay local and are gitignored.
|
|
36
36
|
|
|
37
37
|
<!-- cli:help -->
|
|
38
|
-
Output of `npx getmyenv --help` (0.
|
|
38
|
+
Output of `npx getmyenv --help` (0.10.0):
|
|
39
39
|
|
|
40
40
|
```text
|
|
41
41
|
Usage: getmyenv [options] [command]
|
|
@@ -2,6 +2,6 @@ type BackupOpts = {
|
|
|
2
2
|
output?: string;
|
|
3
3
|
apiUrl?: string;
|
|
4
4
|
};
|
|
5
|
-
/** Encrypted backup of open contexts. Opens offline with the current Vault password. */
|
|
5
|
+
/** Encrypted backup of open contexts. Opens offline with the current Vault password or recovery code. */
|
|
6
6
|
export declare function backupCommand(opts: BackupOpts): Promise<void>;
|
|
7
7
|
export {};
|
package/dist/commands/backup.js
CHANGED
|
@@ -4,7 +4,7 @@ import { createBackupEnvelope, parsePayload, parseWrappedKey, serializeBackup, }
|
|
|
4
4
|
import { slugify } from "@getmyenv/shared";
|
|
5
5
|
import { ensureTokenAndLink } from "../lib/bootstrap.js";
|
|
6
6
|
import { writePrivateFile } from "../lib/fs.js";
|
|
7
|
-
/** Encrypted backup of open contexts. Opens offline with the current Vault password. */
|
|
7
|
+
/** Encrypted backup of open contexts. Opens offline with the current Vault password or recovery code. */
|
|
8
8
|
export async function backupCommand(opts) {
|
|
9
9
|
const { client } = await ensureTokenAndLink({ apiUrl: opts.apiUrl });
|
|
10
10
|
const data = await client.request("GET", "/api/cli/backup");
|
|
@@ -15,6 +15,9 @@ export async function backupCommand(opts) {
|
|
|
15
15
|
projectName: data.projectName,
|
|
16
16
|
publicKey: data.publicKey,
|
|
17
17
|
wrappedPrivateKey: parseWrappedKey(JSON.stringify(data.wrappedPrivateKey)),
|
|
18
|
+
recoveryWrappedKey: data.recoveryWrappedKey
|
|
19
|
+
? parseWrappedKey(JSON.stringify(data.recoveryWrappedKey))
|
|
20
|
+
: null,
|
|
18
21
|
contexts: data.contexts,
|
|
19
22
|
keys: data.keys,
|
|
20
23
|
tags: data.tags,
|
|
@@ -29,5 +32,7 @@ export async function backupCommand(opts) {
|
|
|
29
32
|
path.join(process.cwd(), `${slugify(data.projectName) || "project"}.getmyenv-backup`);
|
|
30
33
|
writePrivateFile(out, serializeBackup(envelope), { overwrite: false });
|
|
31
34
|
console.log(`${pc.green("✓")} Encrypted backup: ${out} (${data.values.length} values, ${data.contexts.length} open contexts)`);
|
|
32
|
-
console.log(pc.dim(
|
|
35
|
+
console.log(pc.dim(data.recoveryWrappedKey
|
|
36
|
+
? "It opens with your current Vault password or recovery code. Read-only contexts are backed up from the dashboard."
|
|
37
|
+
: "It opens with your current Vault password. Read-only contexts are backed up from the dashboard."));
|
|
33
38
|
}
|
package/dist/commands/claim.js
CHANGED
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import pc from "picocolors";
|
|
2
2
|
import { GUEST_GRACE_DAYS } from "@getmyenv/shared";
|
|
3
|
-
import {
|
|
3
|
+
import { openSealedKey, sealContextKey, unwrapPrivateKey, wipe, } from "@getmyenv/crypto";
|
|
4
4
|
import { clearClaimFile, ensureGitignoreHasEnv, promptHidden, readClaimFile, readLine, readProjectMeta, writeClaimFile, writeProjectMeta, } from "../lib/fs.js";
|
|
5
5
|
import { readFolderToken, writeFolderToken } from "../lib/credentials.js";
|
|
6
6
|
import { ApiClient, ApiError, resolveApiUrl } from "../lib/api.js";
|
|
7
7
|
import { authorizeFolder, openBrowser, sleep } from "../lib/authorize.js";
|
|
8
|
-
import {
|
|
9
|
-
async function
|
|
10
|
-
console.error(pc.dim("Choose a Vault password. It stays on this machine. We store an encrypted key blob only.\n"));
|
|
8
|
+
import { createAccountKeys } from "../lib/recovery-code.js";
|
|
9
|
+
async function askNewAccountKeys() {
|
|
11
10
|
try {
|
|
12
|
-
return await
|
|
11
|
+
return await createAccountKeys();
|
|
13
12
|
}
|
|
14
13
|
catch (err) {
|
|
15
14
|
throw new Error(`${err instanceof Error ? err.message : "Vault password required."} Run claim again.`);
|
|
@@ -38,10 +37,10 @@ async function finishClaim(owned, claim) {
|
|
|
38
37
|
let keys;
|
|
39
38
|
try {
|
|
40
39
|
if (!ownerPublicKey) {
|
|
41
|
-
const created = await
|
|
40
|
+
const created = await askNewAccountKeys();
|
|
42
41
|
wipe(created.privateKey);
|
|
43
42
|
ownerPublicKey = created.publicKey;
|
|
44
|
-
keys =
|
|
43
|
+
keys = created.keys;
|
|
45
44
|
}
|
|
46
45
|
const shares = vault.claim.shares.map((s) => {
|
|
47
46
|
const key = openSealedKey(s.sealedKey, guestKey, s.contextId);
|
|
@@ -63,7 +62,7 @@ async function finishClaim(owned, claim) {
|
|
|
63
62
|
}
|
|
64
63
|
clearClaimFile();
|
|
65
64
|
ensureGitignoreHasEnv();
|
|
66
|
-
console.error(`\n${pc.green("✓")} Claimed ${pc.bold(claim.projectName)}. ${keys ? "Vault password set." : "It opens with your Vault password."}`);
|
|
65
|
+
console.error(`\n${pc.green("✓")} Claimed ${pc.bold(claim.projectName)}. ${keys ? "Vault password and recovery code set." : "It opens with your Vault password."}`);
|
|
67
66
|
console.error(pc.dim("claim.json was removed. Read-only contexts like Production are now available in the dashboard.\n"));
|
|
68
67
|
}
|
|
69
68
|
/** A signed-in folder token for the claimed project: reuse this folder's, or sign in. */
|
|
@@ -5,7 +5,7 @@ type RestoreOpts = {
|
|
|
5
5
|
};
|
|
6
6
|
/**
|
|
7
7
|
* Restore a backup into this folder's project. The backup opens locally with
|
|
8
|
-
* the Vault password it was made with. Values are encrypted again with this
|
|
8
|
+
* the Vault password or recovery code it was made with. Values are encrypted again with this
|
|
9
9
|
* project's context keys. Read-only and unknown contexts are skipped.
|
|
10
10
|
*/
|
|
11
11
|
export declare function restoreCommand(file: string, opts: RestoreOpts): Promise<void>;
|
package/dist/commands/restore.js
CHANGED
|
@@ -5,10 +5,10 @@ import { ensureTokenAndLink } from "../lib/bootstrap.js";
|
|
|
5
5
|
import { findContext } from "../lib/context.js";
|
|
6
6
|
import { promptConfirm } from "../lib/fs.js";
|
|
7
7
|
import { resolveVaultPassword } from "../lib/guest-key.js";
|
|
8
|
-
import {
|
|
8
|
+
import { writeKey } from "../lib/vault.js";
|
|
9
9
|
/**
|
|
10
10
|
* Restore a backup into this folder's project. The backup opens locally with
|
|
11
|
-
* the Vault password it was made with. Values are encrypted again with this
|
|
11
|
+
* the Vault password or recovery code it was made with. Values are encrypted again with this
|
|
12
12
|
* project's context keys. Read-only and unknown contexts are skipped.
|
|
13
13
|
*/
|
|
14
14
|
export async function restoreCommand(file, opts) {
|
|
@@ -32,16 +32,23 @@ export async function restoreCommand(file, opts) {
|
|
|
32
32
|
if (who.token.kind !== "folder") {
|
|
33
33
|
throw new Error("Restore needs a signed-in folder token. Guest and server tokens cannot restore.");
|
|
34
34
|
}
|
|
35
|
-
const
|
|
35
|
+
const secret = await resolveVaultPassword({
|
|
36
36
|
isGuest: false,
|
|
37
|
-
prompt:
|
|
37
|
+
prompt: envelope.recoveryWrappedKey
|
|
38
|
+
? "Vault password or recovery code for this backup (hidden): "
|
|
39
|
+
: "Vault password for this backup (hidden): ",
|
|
38
40
|
});
|
|
39
41
|
let opened;
|
|
40
42
|
try {
|
|
41
|
-
opened = await restoreValuesFromBackup(
|
|
43
|
+
opened = await restoreValuesFromBackup(secret, envelope);
|
|
42
44
|
}
|
|
43
|
-
catch {
|
|
44
|
-
|
|
45
|
+
catch (err) {
|
|
46
|
+
const msg = err instanceof Error ? err.message : "";
|
|
47
|
+
if (/^(That recovery code|This backup|A recovery code)/.test(msg))
|
|
48
|
+
throw new Error(msg);
|
|
49
|
+
throw new Error(envelope.recoveryWrappedKey
|
|
50
|
+
? "Could not open the backup. Check the Vault password or recovery code."
|
|
51
|
+
: "Could not open the backup. Check the Vault password.");
|
|
45
52
|
}
|
|
46
53
|
console.error(`Backup checked: ${opened.values.length} values in ${envelope.contexts.length} contexts.\n`);
|
|
47
54
|
const targets = new Map();
|
|
@@ -73,11 +80,13 @@ export async function restoreCommand(file, opts) {
|
|
|
73
80
|
}
|
|
74
81
|
}
|
|
75
82
|
const values = [];
|
|
83
|
+
const keyVersions = {};
|
|
76
84
|
for (const v of opened.values) {
|
|
77
85
|
if (!writable(v.context))
|
|
78
86
|
continue;
|
|
79
87
|
const target = targets.get(v.context);
|
|
80
|
-
const key = await
|
|
88
|
+
const { key, keyVersion } = await writeKey(linked, target);
|
|
89
|
+
keyVersions[v.context] = keyVersion;
|
|
81
90
|
values.push({
|
|
82
91
|
key: v.key,
|
|
83
92
|
context: v.context,
|
|
@@ -89,6 +98,7 @@ export async function restoreCommand(file, opts) {
|
|
|
89
98
|
keys: envelope.keys,
|
|
90
99
|
tags: envelope.tags ?? {},
|
|
91
100
|
values,
|
|
101
|
+
keyVersions,
|
|
92
102
|
});
|
|
93
103
|
const skipped = [...perContext.keys()].filter((slug) => !writable(slug));
|
|
94
104
|
console.log(`${pc.green("✓")} Restored ${res.restored} values into ${res.projectName}`);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type WrappedPrivateKey } from "@getmyenv/crypto";
|
|
2
|
+
export declare const NO_TTY_FOR_VAULT = "No terminal to set a Vault password. Set it in the dashboard, Settings, or run this command in a terminal.";
|
|
3
|
+
export type NewAccountKeys = {
|
|
4
|
+
publicKey: string;
|
|
5
|
+
/** Caller wipes it. */
|
|
6
|
+
privateKey: Uint8Array;
|
|
7
|
+
keys: {
|
|
8
|
+
publicKey: string;
|
|
9
|
+
wrappedPrivateKey: WrappedPrivateKey;
|
|
10
|
+
recoveryWrappedKey: WrappedPrivateKey;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
/** The user types the code back. Nothing is saved until it matches. */
|
|
14
|
+
export declare function confirmRecoveryCode(code: string, ask?: (question: string) => Promise<string>): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* New account keys: a Vault password typed twice, then a recovery code shown
|
|
17
|
+
* once and typed back. Needs a terminal. Checks both wraps open the same key.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createAccountKeys(): Promise<NewAccountKeys>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
import { assertRecoveryWrap, createRecoveryWrap, createUserKeys, formatRecoveryCode, recoveryCodesMatch, wipe, } from "@getmyenv/crypto";
|
|
3
|
+
import { FACTS } from "@getmyenv/shared";
|
|
4
|
+
import { readLine } from "./fs.js";
|
|
5
|
+
import { promptNewVaultPassword } from "./guest-key.js";
|
|
6
|
+
export const NO_TTY_FOR_VAULT = "No terminal to set a Vault password. Set it in the dashboard, Settings, or run this command in a terminal.";
|
|
7
|
+
const TYPE_BACK_ATTEMPTS = 3;
|
|
8
|
+
function printRecoveryCode(code) {
|
|
9
|
+
const shown = formatRecoveryCode(code);
|
|
10
|
+
const width = shown.length + 4;
|
|
11
|
+
console.error("");
|
|
12
|
+
console.error(pc.bold("Your recovery code"));
|
|
13
|
+
console.error(`┌${"─".repeat(width)}┐`);
|
|
14
|
+
console.error(`│ ${pc.bold(shown)} │`);
|
|
15
|
+
console.error(`└${"─".repeat(width)}┘`);
|
|
16
|
+
console.error(pc.dim(`${FACTS.recoveryCode} Write it down or save it in a password manager.\n`));
|
|
17
|
+
}
|
|
18
|
+
/** The user types the code back. Nothing is saved until it matches. */
|
|
19
|
+
export async function confirmRecoveryCode(code, ask = readLine) {
|
|
20
|
+
for (let attempt = 1; attempt <= TYPE_BACK_ATTEMPTS; attempt++) {
|
|
21
|
+
const typed = await ask("Type the recovery code to confirm: ");
|
|
22
|
+
if (recoveryCodesMatch(code, typed))
|
|
23
|
+
return;
|
|
24
|
+
if (attempt < TYPE_BACK_ATTEMPTS)
|
|
25
|
+
console.error(pc.red("That does not match. Try again.\n"));
|
|
26
|
+
}
|
|
27
|
+
throw new Error("The recovery code did not match. Nothing was saved. Run the command again.");
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* New account keys: a Vault password typed twice, then a recovery code shown
|
|
31
|
+
* once and typed back. Needs a terminal. Checks both wraps open the same key.
|
|
32
|
+
*/
|
|
33
|
+
export async function createAccountKeys() {
|
|
34
|
+
if (!process.stdin.isTTY)
|
|
35
|
+
throw new Error(NO_TTY_FOR_VAULT);
|
|
36
|
+
console.error(pc.dim("Choose a Vault password. It stays on this machine. We store an encrypted key blob only.\n"));
|
|
37
|
+
const created = await createUserKeys(await promptNewVaultPassword());
|
|
38
|
+
try {
|
|
39
|
+
const recovery = await createRecoveryWrap(created.privateKey);
|
|
40
|
+
printRecoveryCode(recovery.code);
|
|
41
|
+
await confirmRecoveryCode(recovery.code);
|
|
42
|
+
await assertRecoveryWrap(recovery.code, recovery.wrapped, created.publicKey);
|
|
43
|
+
return {
|
|
44
|
+
publicKey: created.publicKey,
|
|
45
|
+
privateKey: created.privateKey,
|
|
46
|
+
keys: {
|
|
47
|
+
publicKey: created.publicKey,
|
|
48
|
+
wrappedPrivateKey: created.wrapped,
|
|
49
|
+
recoveryWrappedKey: recovery.wrapped,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
wipe(created.privateKey);
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
}
|
package/dist/lib/upload.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { encryptSecret, serializePayload } from "@getmyenv/crypto";
|
|
2
2
|
import { FACTS } from "@getmyenv/shared";
|
|
3
3
|
import { ApiError } from "./api.js";
|
|
4
|
-
import {
|
|
4
|
+
import { writeKey } from "./vault.js";
|
|
5
5
|
/** Fail fast before prompting or encrypting. Mirrors the server's `checkWrite`, which still enforces it. */
|
|
6
6
|
export function assertWritable(linked, ctx) {
|
|
7
7
|
if (linked.who.token.kind === "server") {
|
|
@@ -23,17 +23,20 @@ export async function uploadValues(input) {
|
|
|
23
23
|
.filter(([, v]) => v === "")
|
|
24
24
|
.map(([k]) => k);
|
|
25
25
|
let values = [];
|
|
26
|
+
let keyVersion;
|
|
26
27
|
if (filled.length > 0) {
|
|
27
|
-
const
|
|
28
|
+
const write = await writeKey(linked, context);
|
|
29
|
+
keyVersion = write.keyVersion;
|
|
28
30
|
values = filled.map(([name, value]) => ({
|
|
29
31
|
name,
|
|
30
|
-
ciphertext: serializePayload(encryptSecret(key, name, value)),
|
|
32
|
+
ciphertext: serializePayload(encryptSecret(write.key, name, value)),
|
|
31
33
|
...(input.expiresAt !== undefined ? { expiresAt: input.expiresAt } : {}),
|
|
32
34
|
}));
|
|
33
35
|
}
|
|
34
36
|
const body = {
|
|
35
37
|
context: context.slug,
|
|
36
38
|
values,
|
|
39
|
+
...(keyVersion !== undefined ? { keyVersion } : {}),
|
|
37
40
|
...(input.prune ? { prune: true } : {}),
|
|
38
41
|
...(declare.length > 0 ? { declare } : {}),
|
|
39
42
|
};
|
package/dist/lib/vault.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export type VaultContext = {
|
|
|
18
18
|
protection: Protection;
|
|
19
19
|
hasKey: boolean;
|
|
20
20
|
sealedKey: SealedKey | null;
|
|
21
|
+
/** Version of the key in sealedKey. Sent back with every write. */
|
|
22
|
+
keyVersion: number;
|
|
21
23
|
};
|
|
22
24
|
export type VaultInfo = {
|
|
23
25
|
holder: "user" | "guest" | "token";
|
|
@@ -45,6 +47,15 @@ export type KeyOpts = {
|
|
|
45
47
|
create?: boolean;
|
|
46
48
|
};
|
|
47
49
|
export declare function fetchVault(linked: Linked): Promise<VaultInfo>;
|
|
50
|
+
/**
|
|
51
|
+
* The context key and its version, for a write. The server refuses ciphertext
|
|
52
|
+
* written with a key that was replaced since, so the version comes from the
|
|
53
|
+
* same vault response as the sealed key.
|
|
54
|
+
*/
|
|
55
|
+
export declare function writeKey(linked: Linked, target: KeyTarget): Promise<{
|
|
56
|
+
key: Uint8Array;
|
|
57
|
+
keyVersion: number;
|
|
58
|
+
}>;
|
|
48
59
|
/** The context key, unlocking with the Vault password when needed. */
|
|
49
60
|
export declare function contextKey(linked: Linked, target: KeyTarget, opts?: KeyOpts): Promise<Uint8Array>;
|
|
50
61
|
/** Unlock with the Vault password and return the private key. Caller wipes it. */
|
package/dist/lib/vault.js
CHANGED
|
@@ -4,11 +4,28 @@ import { VAULT_UNLOCK_DURATION } from "@getmyenv/shared";
|
|
|
4
4
|
import { ApiError } from "./api.js";
|
|
5
5
|
import { resolveVaultPassword } from "./guest-key.js";
|
|
6
6
|
import { keychainAccount, keychainEnabled, readCachedContextKey, storeCachedContextKeys, } from "./keychain.js";
|
|
7
|
+
import { createAccountKeys } from "./recovery-code.js";
|
|
7
8
|
/** One unlock per process: several contexts in one command prompt once. */
|
|
8
9
|
const memo = new Map();
|
|
10
|
+
/** Key version of each memo entry opened from the vault response. Keys from the keychain have none. */
|
|
11
|
+
const versions = new Map();
|
|
9
12
|
export function fetchVault(linked) {
|
|
10
13
|
return linked.client.request("GET", "/api/cli/vault");
|
|
11
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* The context key and its version, for a write. The server refuses ciphertext
|
|
17
|
+
* written with a key that was replaced since, so the version comes from the
|
|
18
|
+
* same vault response as the sealed key.
|
|
19
|
+
*/
|
|
20
|
+
export async function writeKey(linked, target) {
|
|
21
|
+
if (!versions.has(target.id))
|
|
22
|
+
memo.delete(target.id);
|
|
23
|
+
const key = await contextKey(linked, { ...target, sealedKey: undefined }, { create: true });
|
|
24
|
+
const keyVersion = versions.get(target.id);
|
|
25
|
+
if (keyVersion === undefined)
|
|
26
|
+
throw new Error(`No key version for ${target.slug}. Run the command again.`);
|
|
27
|
+
return { key, keyVersion };
|
|
28
|
+
}
|
|
12
29
|
/** The context key, unlocking with the Vault password when needed. */
|
|
13
30
|
export async function contextKey(linked, target, opts = {}) {
|
|
14
31
|
const cached = memo.get(target.id);
|
|
@@ -45,10 +62,14 @@ export async function contextKey(linked, target, opts = {}) {
|
|
|
45
62
|
if (!key) {
|
|
46
63
|
throw new Error(member
|
|
47
64
|
? `Waiting for the owner to send the key for ${target.slug}. It arrives the next time they unlock the vault in the dashboard.`
|
|
48
|
-
: `
|
|
65
|
+
: `This account has no key for ${target.slug}. If you reset your vault, reset ${target.slug} in the dashboard to set new values.`);
|
|
49
66
|
}
|
|
50
67
|
for (const [id, k] of opened)
|
|
51
68
|
memo.set(id, k);
|
|
69
|
+
for (const c of vault.contexts) {
|
|
70
|
+
if (opened.has(c.id))
|
|
71
|
+
versions.set(c.id, c.keyVersion);
|
|
72
|
+
}
|
|
52
73
|
if (account)
|
|
53
74
|
await remember(account, vault, opened);
|
|
54
75
|
return key;
|
|
@@ -109,6 +130,7 @@ async function initContextKey(linked, vault, ctx) {
|
|
|
109
130
|
const key = generateContextKey();
|
|
110
131
|
try {
|
|
111
132
|
await linked.client.request("POST", "/api/cli/vault", {
|
|
133
|
+
publicKey: vault.publicKey,
|
|
112
134
|
shares: [{ contextId: ctx.id, sealedKey: sealContextKey(key, vault.publicKey, ctx.id) }],
|
|
113
135
|
});
|
|
114
136
|
}
|
|
@@ -131,22 +153,16 @@ async function holderPrivateKey(linked, vault, opts) {
|
|
|
131
153
|
if (!opts.create) {
|
|
132
154
|
throw new Error("No Vault password yet. Set it in the dashboard, or set a value: npx getmyenv set KEY");
|
|
133
155
|
}
|
|
134
|
-
|
|
135
|
-
console.error(pc.dim("Choose a Vault password. It stays on this machine. We store an encrypted key blob only.\n"));
|
|
136
|
-
}
|
|
137
|
-
const password = await resolveVaultPassword({ isGuest, create: true });
|
|
138
|
-
const created = await createUserKeys(password);
|
|
156
|
+
const created = isGuest ? await createGuestKeys() : await createAccountKeys();
|
|
139
157
|
try {
|
|
140
|
-
await linked.client.request("POST", "/api/cli/vault", {
|
|
141
|
-
keys: { publicKey: created.publicKey, wrappedPrivateKey: created.wrapped },
|
|
142
|
-
});
|
|
158
|
+
await linked.client.request("POST", "/api/cli/vault", { keys: created.keys });
|
|
143
159
|
}
|
|
144
160
|
catch (err) {
|
|
145
161
|
wipe(created.privateKey);
|
|
146
162
|
throw err;
|
|
147
163
|
}
|
|
148
164
|
vault.publicKey = created.publicKey;
|
|
149
|
-
vault.wrappedPrivateKey = created.
|
|
165
|
+
vault.wrappedPrivateKey = created.keys.wrappedPrivateKey;
|
|
150
166
|
return created.privateKey;
|
|
151
167
|
}
|
|
152
168
|
const interactive = process.stdin.isTTY && !isGuest;
|
|
@@ -164,10 +180,19 @@ async function holderPrivateKey(linked, vault, opts) {
|
|
|
164
180
|
console.error(pc.red("Wrong Vault password. Try again.\n"));
|
|
165
181
|
continue;
|
|
166
182
|
}
|
|
167
|
-
throw new Error("Could not unlock. Check your Vault password.");
|
|
183
|
+
throw new Error("Could not unlock. Check your Vault password. Forgot it? Use your recovery code in the dashboard, Settings.");
|
|
168
184
|
}
|
|
169
185
|
}
|
|
170
186
|
}
|
|
187
|
+
/** Guest keys wrap with the claim.json passphrase. No recovery code. */
|
|
188
|
+
async function createGuestKeys() {
|
|
189
|
+
const created = await createUserKeys(await resolveVaultPassword({ isGuest: true, create: true }));
|
|
190
|
+
return {
|
|
191
|
+
publicKey: created.publicKey,
|
|
192
|
+
privateKey: created.privateKey,
|
|
193
|
+
keys: { publicKey: created.publicKey, wrappedPrivateKey: created.wrapped },
|
|
194
|
+
};
|
|
195
|
+
}
|
|
171
196
|
/** Unlock with the Vault password and return the private key. Caller wipes it. */
|
|
172
197
|
export async function unlockPrivateKey(linked, opts = {}) {
|
|
173
198
|
const vault = await fetchVault(linked);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "getmyenv",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "CLI for getmyenv, an encrypted store for environment variables and secrets. We store ciphertext only.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
"access": "public"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@getmyenv/crypto": "0.
|
|
37
|
-
"@getmyenv/shared": "0.
|
|
36
|
+
"@getmyenv/crypto": "0.5.0",
|
|
37
|
+
"@getmyenv/shared": "0.9.0",
|
|
38
38
|
"@napi-rs/keyring": "^2.1.0",
|
|
39
39
|
"commander": "^15.0.0",
|
|
40
40
|
"dotenv": "^16.4.7",
|