@sigma-auth/cli 0.0.1 → 0.0.3
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 +17 -0
- package/package.json +2 -2
- package/src/commands.ts +37 -17
- package/src/cookies.ts +7 -1
- package/src/diagnose.ts +191 -0
- package/src/http.ts +12 -2
- package/src/identity.ts +93 -1
- package/src/index.ts +18 -0
- package/src/output.ts +8 -1
- package/src/password.ts +12 -0
package/README.md
CHANGED
|
@@ -33,7 +33,24 @@ bunx @sigma-auth/cli identity create \
|
|
|
33
33
|
| `sigma backup push` | POST ciphertext only |
|
|
34
34
|
| `sigma oauth register` | Session `/api/oauth-clients` or RFC 7591 DCR |
|
|
35
35
|
| `sigma doctor` | Env, files, RFC 8414, session |
|
|
36
|
+
| `sigma diagnose bap` | Public BAP profile; optional `--pubkey` registered-list check |
|
|
37
|
+
| `sigma diagnose identities` | `GET /api/user/bap-ids` (`--pubkey` or session cookie) |
|
|
38
|
+
| `sigma diagnose last-oauth` | Last selected BAP for `--pubkey` `--client-id` |
|
|
39
|
+
| `sigma diagnose client` | Public OAuth client metadata |
|
|
36
40
|
|
|
37
41
|
Password sources (exactly one): `--password-file`, `--password-stdin`, or `SIGMA_BACKUP_PASSWORD`. `--password` on argv is rejected.
|
|
38
42
|
|
|
43
|
+
## Diagnose (no private keys)
|
|
44
|
+
|
|
45
|
+
Public HTTP lookups against `https://auth.sigmaidentity.com`. Use these when a Sigma login shows the wrong faucet, a profile 404s, or last-selected identity looks stuck.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
bunx @sigma-auth/cli diagnose bap --bap-id 3QpdyNb9HScYmWEyfqtRQbKzwyf --json
|
|
49
|
+
bunx @sigma-auth/cli diagnose bap --bap-id 33mGVYzkGE9XMbu346XkUaMHyzwV --pubkey 03a42932… --json
|
|
50
|
+
bunx @sigma-auth/cli diagnose identities --pubkey 03a42932… --json
|
|
51
|
+
bunx @sigma-auth/cli diagnose last-oauth --pubkey 03a42932… --client-id droplit --json
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
`diagnose bap` treats a profile 404 as a successful diagnosis (`found: false`), not a command failure.
|
|
55
|
+
|
|
39
56
|
Contract: `docs/specs/sigma-cli-v1.md` in [sigma-auth](https://github.com/b-open-io/sigma-auth).
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sigma-auth/cli",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "Headless Sigma Auth CLI: create a BAP identity locally, sign in with Bitcoin-Auth, push encrypted backups, register OAuth clients",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "Headless Sigma Auth CLI: create a BAP identity locally, sign in with Bitcoin-Auth, diagnose public identities, push encrypted backups, register OAuth clients",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"sigma": "./src/index.ts"
|
package/src/commands.ts
CHANGED
|
@@ -5,11 +5,12 @@ import { isLegacyBackup, isType42Backup } from "bitcoin-backup";
|
|
|
5
5
|
import type { ParsedArgs } from "./args.ts";
|
|
6
6
|
import { boolFlag, flag, flagList } from "./args.ts";
|
|
7
7
|
import { backupPath, type RuntimeConfig } from "./config.ts";
|
|
8
|
-
import { loadJar, sessionCookieNames } from "./cookies.ts";
|
|
9
|
-
import {
|
|
8
|
+
import { deleteJar, loadJar, sessionCookieNames } from "./cookies.ts";
|
|
9
|
+
import { cryptoFail, usage } from "./error.ts";
|
|
10
10
|
import { ensureDir, pathExists, readText, writeSecretFile } from "./fsutil.ts";
|
|
11
11
|
import { createHttp, requestJson, throwHttp } from "./http.ts";
|
|
12
12
|
import {
|
|
13
|
+
assertBitcoinBackupCiphertext,
|
|
13
14
|
bapFromBackup,
|
|
14
15
|
createMasterBackup,
|
|
15
16
|
decryptMaster,
|
|
@@ -76,7 +77,7 @@ export async function identityCreate(
|
|
|
76
77
|
positional: args.positional,
|
|
77
78
|
flags: { ...args.flags, backup: [out] },
|
|
78
79
|
};
|
|
79
|
-
const code = await authSignIn(signinArgs, cfg, false);
|
|
80
|
+
const code = await authSignIn(signinArgs, cfg, false, password);
|
|
80
81
|
if (code !== 0) {
|
|
81
82
|
return code;
|
|
82
83
|
}
|
|
@@ -157,9 +158,10 @@ export async function backupEncrypt(
|
|
|
157
158
|
export async function authSignIn(
|
|
158
159
|
args: ParsedArgs,
|
|
159
160
|
cfg: RuntimeConfig,
|
|
160
|
-
emit = true
|
|
161
|
+
emit = true,
|
|
162
|
+
resolvedPassword?: string
|
|
161
163
|
): Promise<number> {
|
|
162
|
-
const password = await resolvePassword(args, true);
|
|
164
|
+
const password = resolvedPassword ?? (await resolvePassword(args, true));
|
|
163
165
|
if (!password) {
|
|
164
166
|
usage("password required");
|
|
165
167
|
}
|
|
@@ -183,7 +185,13 @@ export async function authSignIn(
|
|
|
183
185
|
saveCookies: true,
|
|
184
186
|
});
|
|
185
187
|
if (signed.status >= 400) {
|
|
186
|
-
throwHttp(
|
|
188
|
+
throwHttp(
|
|
189
|
+
"/api/auth/sign-in/sigma",
|
|
190
|
+
signed.status,
|
|
191
|
+
signed.json,
|
|
192
|
+
signed.text,
|
|
193
|
+
signed.headers
|
|
194
|
+
);
|
|
187
195
|
}
|
|
188
196
|
const payload = signed.json as {
|
|
189
197
|
user?: { id?: string; pubkey?: string };
|
|
@@ -204,7 +212,14 @@ export async function authSignIn(
|
|
|
204
212
|
await requestJson(client, "POST", "/api/auth/sign-out", {
|
|
205
213
|
withCookies: true,
|
|
206
214
|
});
|
|
207
|
-
|
|
215
|
+
deleteJar(client.cookieJar);
|
|
216
|
+
throwHttp(
|
|
217
|
+
"/api/user/bap-ids",
|
|
218
|
+
registered.status,
|
|
219
|
+
registered.json,
|
|
220
|
+
registered.text,
|
|
221
|
+
registered.headers
|
|
222
|
+
);
|
|
208
223
|
}
|
|
209
224
|
if (!emit) {
|
|
210
225
|
return 0;
|
|
@@ -228,20 +243,14 @@ export async function backupPush(
|
|
|
228
243
|
): Promise<number> {
|
|
229
244
|
const path = backupPath(args, cfg.home);
|
|
230
245
|
const ciphertext = readText(path).replace(/\n+$/, "");
|
|
231
|
-
|
|
232
|
-
throw new CliError(
|
|
233
|
-
7,
|
|
234
|
-
"crypto",
|
|
235
|
-
"refusing to upload plaintext backup (rootPk/xprv/wif/mnemonic present)"
|
|
236
|
-
);
|
|
237
|
-
}
|
|
246
|
+
assertBitcoinBackupCiphertext(ciphertext);
|
|
238
247
|
const client = createHttp(cfg);
|
|
239
248
|
const result = await requestJson(client, "POST", "/api/backup", {
|
|
240
249
|
body: { encryptedBackup: ciphertext },
|
|
241
250
|
withCookies: true,
|
|
242
251
|
});
|
|
243
252
|
if (result.status >= 400) {
|
|
244
|
-
throwHttp("/api/backup", result.status, result.json, result.text);
|
|
253
|
+
throwHttp("/api/backup", result.status, result.json, result.text, result.headers);
|
|
245
254
|
}
|
|
246
255
|
const payload = result.json as { bapId?: string; message?: string };
|
|
247
256
|
if (!emit) {
|
|
@@ -285,7 +294,13 @@ export async function oauthRegister(
|
|
|
285
294
|
withCookies: true,
|
|
286
295
|
});
|
|
287
296
|
if (result.status >= 400) {
|
|
288
|
-
throwHttp(
|
|
297
|
+
throwHttp(
|
|
298
|
+
"/api/oauth-clients",
|
|
299
|
+
result.status,
|
|
300
|
+
result.json,
|
|
301
|
+
result.text,
|
|
302
|
+
result.headers
|
|
303
|
+
);
|
|
289
304
|
}
|
|
290
305
|
const payload = result.json as {
|
|
291
306
|
client?: { clientId?: string; accountPubkey?: string; ownerBapId?: string };
|
|
@@ -327,7 +342,8 @@ export async function oauthRegister(
|
|
|
327
342
|
"/api/auth/oauth2/register",
|
|
328
343
|
result.status,
|
|
329
344
|
result.json,
|
|
330
|
-
result.text
|
|
345
|
+
result.text,
|
|
346
|
+
result.headers
|
|
331
347
|
);
|
|
332
348
|
}
|
|
333
349
|
const payload = result.json as {
|
|
@@ -577,6 +593,10 @@ Commands:
|
|
|
577
593
|
backup push POST ciphertext with session; never decrypt
|
|
578
594
|
oauth register Register an OAuth client (session path or DCR)
|
|
579
595
|
doctor Non-interactive health check
|
|
596
|
+
diagnose bap Public BAP profile (+ --pubkey registered-list check)
|
|
597
|
+
diagnose identities GET /api/user/bap-ids [--pubkey]
|
|
598
|
+
diagnose last-oauth Last selected BAP for --pubkey --client-id
|
|
599
|
+
diagnose client Public OAuth client metadata --client-id
|
|
580
600
|
|
|
581
601
|
Global flags:
|
|
582
602
|
--base-url <url> SIGMA_AUTH_URL (default https://auth.sigmaidentity.com)
|
package/src/cookies.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync,
|
|
1
|
+
import { chmodSync, existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { ensureDir } from "./fsutil.ts";
|
|
4
4
|
|
|
@@ -83,6 +83,12 @@ export function loadJar(path: string): Cookie[] {
|
|
|
83
83
|
return cookies;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
export function deleteJar(path: string): void {
|
|
87
|
+
if (existsSync(path)) {
|
|
88
|
+
unlinkSync(path);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
86
92
|
export function saveJar(path: string, cookies: Cookie[]): void {
|
|
87
93
|
ensureDir(dirname(path));
|
|
88
94
|
const lines = [
|
package/src/diagnose.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import type { ParsedArgs } from "./args.ts";
|
|
2
|
+
import { flag } from "./args.ts";
|
|
3
|
+
import type { RuntimeConfig } from "./config.ts";
|
|
4
|
+
import { usage } from "./error.ts";
|
|
5
|
+
import { createHttp, requestJson, throwHttp } from "./http.ts";
|
|
6
|
+
import { printHuman, printJson, type OutputMode } from "./output.ts";
|
|
7
|
+
|
|
8
|
+
function mode(cfg: RuntimeConfig): OutputMode {
|
|
9
|
+
return { json: cfg.json, quiet: cfg.quiet };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function succeed(cfg: RuntimeConfig, data: Record<string, unknown>, human: string): number {
|
|
13
|
+
if (cfg.json) {
|
|
14
|
+
printJson(true, data);
|
|
15
|
+
} else {
|
|
16
|
+
printHuman(mode(cfg), human);
|
|
17
|
+
}
|
|
18
|
+
return 0;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function record(json: unknown): Record<string, unknown> {
|
|
22
|
+
return json && typeof json === "object" ? (json as Record<string, unknown>) : {};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function diagnoseBap(args: ParsedArgs, cfg: RuntimeConfig): Promise<number> {
|
|
26
|
+
const bapId = flag(args, "bap-id");
|
|
27
|
+
if (!bapId) {
|
|
28
|
+
usage("--bap-id is required");
|
|
29
|
+
}
|
|
30
|
+
const pubkey = flag(args, "pubkey");
|
|
31
|
+
const client = createHttp(cfg);
|
|
32
|
+
const profileRes = await requestJson(
|
|
33
|
+
client,
|
|
34
|
+
"GET",
|
|
35
|
+
`/api/bap/profile?bapId=${encodeURIComponent(bapId)}`
|
|
36
|
+
);
|
|
37
|
+
if (profileRes.status >= 500) {
|
|
38
|
+
throwHttp(
|
|
39
|
+
"/api/bap/profile",
|
|
40
|
+
profileRes.status,
|
|
41
|
+
profileRes.json,
|
|
42
|
+
profileRes.text,
|
|
43
|
+
profileRes.headers
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
const found = profileRes.status === 200 && record(profileRes.json).status === "OK";
|
|
47
|
+
const profile = found ? record(profileRes.json).result ?? record(profileRes.json) : null;
|
|
48
|
+
|
|
49
|
+
let registeredToPubkey: boolean | null = null;
|
|
50
|
+
let identities: unknown[] | undefined;
|
|
51
|
+
if (pubkey) {
|
|
52
|
+
const listRes = await requestJson(
|
|
53
|
+
client,
|
|
54
|
+
"GET",
|
|
55
|
+
`/api/user/bap-ids?pubkey=${encodeURIComponent(pubkey)}`
|
|
56
|
+
);
|
|
57
|
+
if (listRes.status >= 400) {
|
|
58
|
+
throwHttp(
|
|
59
|
+
"/api/user/bap-ids",
|
|
60
|
+
listRes.status,
|
|
61
|
+
listRes.json,
|
|
62
|
+
listRes.text,
|
|
63
|
+
listRes.headers
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
const bapIds = record(listRes.json).bapIds;
|
|
67
|
+
identities = Array.isArray(bapIds) ? bapIds : [];
|
|
68
|
+
registeredToPubkey = identities.some((row) => {
|
|
69
|
+
const item = record(row);
|
|
70
|
+
return item.identity_key === bapId || item.id === bapId || item.bapId === bapId;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let hint: string;
|
|
75
|
+
if (registeredToPubkey === true && found) {
|
|
76
|
+
hint = "registered to this pubkey and has a published profile";
|
|
77
|
+
} else if (registeredToPubkey === true && !found) {
|
|
78
|
+
hint = "registered to this pubkey but no published BAP profile";
|
|
79
|
+
} else if (registeredToPubkey === false && found) {
|
|
80
|
+
hint = "published profile exists but this BAP is not in this pubkey's registered list";
|
|
81
|
+
} else if (registeredToPubkey === false && !found) {
|
|
82
|
+
hint = "not in this pubkey's registered list and no published BAP profile (typical leftover HD identity)";
|
|
83
|
+
} else if (found) {
|
|
84
|
+
hint = "published BAP profile found";
|
|
85
|
+
} else {
|
|
86
|
+
hint = "no published BAP profile (404). Pass --pubkey to also check GET /api/user/bap-ids";
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const data: Record<string, unknown> = {
|
|
90
|
+
bapId,
|
|
91
|
+
found,
|
|
92
|
+
profile,
|
|
93
|
+
hint,
|
|
94
|
+
};
|
|
95
|
+
if (pubkey) {
|
|
96
|
+
data.pubkey = pubkey;
|
|
97
|
+
data.registeredToPubkey = registeredToPubkey;
|
|
98
|
+
data.registeredCount = identities?.length ?? 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const lines = [
|
|
102
|
+
`bapId ${bapId}`,
|
|
103
|
+
`found ${found}`,
|
|
104
|
+
pubkey ? `registeredToPubkey ${registeredToPubkey}` : null,
|
|
105
|
+
hint,
|
|
106
|
+
].filter((line): line is string => line !== null);
|
|
107
|
+
return succeed(cfg, data, lines.join("\n"));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function diagnoseIdentities(
|
|
111
|
+
args: ParsedArgs,
|
|
112
|
+
cfg: RuntimeConfig
|
|
113
|
+
): Promise<number> {
|
|
114
|
+
const pubkey = flag(args, "pubkey");
|
|
115
|
+
const client = createHttp(cfg);
|
|
116
|
+
const path = pubkey
|
|
117
|
+
? `/api/user/bap-ids?pubkey=${encodeURIComponent(pubkey)}`
|
|
118
|
+
: "/api/user/bap-ids";
|
|
119
|
+
const result = await requestJson(client, "GET", path, {
|
|
120
|
+
withCookies: !pubkey,
|
|
121
|
+
});
|
|
122
|
+
if (result.status >= 400) {
|
|
123
|
+
throwHttp(path, result.status, result.json, result.text, result.headers);
|
|
124
|
+
}
|
|
125
|
+
const bapIds = record(result.json).bapIds;
|
|
126
|
+
const list = Array.isArray(bapIds) ? bapIds : [];
|
|
127
|
+
const names = list
|
|
128
|
+
.map((row) => {
|
|
129
|
+
const item = record(row);
|
|
130
|
+
const id = String(item.identity_key ?? item.id ?? "");
|
|
131
|
+
const name = typeof item.name === "string" ? item.name : "";
|
|
132
|
+
const primary = item.is_primary === true ? " (primary)" : "";
|
|
133
|
+
return `${id}${name ? ` ${name}` : ""}${primary}`;
|
|
134
|
+
})
|
|
135
|
+
.join("\n");
|
|
136
|
+
return succeed(
|
|
137
|
+
cfg,
|
|
138
|
+
{ pubkey: pubkey ?? null, count: list.length, bapIds: list },
|
|
139
|
+
names || "(none)"
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function diagnoseLastOauth(
|
|
144
|
+
args: ParsedArgs,
|
|
145
|
+
cfg: RuntimeConfig
|
|
146
|
+
): Promise<number> {
|
|
147
|
+
const pubkey = flag(args, "pubkey");
|
|
148
|
+
const clientId = flag(args, "client-id");
|
|
149
|
+
if (!pubkey || !clientId) {
|
|
150
|
+
usage("--pubkey and --client-id are required");
|
|
151
|
+
}
|
|
152
|
+
const client = createHttp(cfg);
|
|
153
|
+
const path = `/api/user/last-oauth-identity?pubkey=${encodeURIComponent(pubkey)}&clientId=${encodeURIComponent(clientId)}`;
|
|
154
|
+
const result = await requestJson(client, "GET", path);
|
|
155
|
+
if (result.status >= 400) {
|
|
156
|
+
throwHttp(path, result.status, result.json, result.text, result.headers);
|
|
157
|
+
}
|
|
158
|
+
const lastSelectedBapId = record(result.json).lastSelectedBapId ?? null;
|
|
159
|
+
return succeed(
|
|
160
|
+
cfg,
|
|
161
|
+
{ pubkey, clientId, lastSelectedBapId },
|
|
162
|
+
typeof lastSelectedBapId === "string" ? lastSelectedBapId : "(none)"
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function diagnoseClient(
|
|
167
|
+
args: ParsedArgs,
|
|
168
|
+
cfg: RuntimeConfig
|
|
169
|
+
): Promise<number> {
|
|
170
|
+
const clientId = flag(args, "client-id");
|
|
171
|
+
if (!clientId) {
|
|
172
|
+
usage("--client-id is required");
|
|
173
|
+
}
|
|
174
|
+
const client = createHttp(cfg);
|
|
175
|
+
const path = `/api/oauth-clients?clientId=${encodeURIComponent(clientId)}`;
|
|
176
|
+
const result = await requestJson(client, "GET", path);
|
|
177
|
+
if (result.status >= 400) {
|
|
178
|
+
throwHttp(path, result.status, result.json, result.text, result.headers);
|
|
179
|
+
}
|
|
180
|
+
const payload = record(result.json);
|
|
181
|
+
const clients = Array.isArray(payload.clients) ? payload.clients : [];
|
|
182
|
+
const first = clients[0] ? record(clients[0]) : payload;
|
|
183
|
+
if (!first.clientId && !first.name) {
|
|
184
|
+
return succeed(cfg, { clientId, client: null }, "(not found)");
|
|
185
|
+
}
|
|
186
|
+
return succeed(
|
|
187
|
+
cfg,
|
|
188
|
+
{ clientId, client: first },
|
|
189
|
+
`${String(first.clientId ?? clientId)} ${String(first.name ?? "")}`
|
|
190
|
+
);
|
|
191
|
+
}
|
package/src/http.ts
CHANGED
|
@@ -78,13 +78,23 @@ export async function requestJson(
|
|
|
78
78
|
return { status: response.status, headers: response.headers, json, text };
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
export function throwHttp(
|
|
81
|
+
export function throwHttp(
|
|
82
|
+
path: string,
|
|
83
|
+
status: number,
|
|
84
|
+
json: unknown,
|
|
85
|
+
text: string,
|
|
86
|
+
headers?: Headers
|
|
87
|
+
): never {
|
|
82
88
|
const record = json && typeof json === "object" ? (json as Record<string, unknown>) : {};
|
|
83
|
-
|
|
89
|
+
let message =
|
|
84
90
|
(typeof record.error_description === "string" && record.error_description) ||
|
|
85
91
|
(typeof record.message === "string" && record.message) ||
|
|
86
92
|
(typeof record.error === "string" && record.error) ||
|
|
87
93
|
text ||
|
|
88
94
|
`${path} failed with ${status}`;
|
|
95
|
+
const retryAfter = headers?.get("retry-after");
|
|
96
|
+
if (retryAfter) {
|
|
97
|
+
message = `${message} (Retry-After: ${retryAfter})`;
|
|
98
|
+
}
|
|
89
99
|
throw new CliError(exitForHttp(status), codeForHttp(status), message, status);
|
|
90
100
|
}
|
package/src/identity.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HD, Mnemonic, PrivateKey } from "@bsv/sdk";
|
|
1
|
+
import { HD, Mnemonic, PrivateKey, Utils } from "@bsv/sdk";
|
|
2
2
|
import {
|
|
3
3
|
type BapMasterBackup,
|
|
4
4
|
decryptBackup,
|
|
@@ -9,6 +9,12 @@ import {
|
|
|
9
9
|
import { BAP } from "bsv-bap";
|
|
10
10
|
import { cryptoFail } from "./error.ts";
|
|
11
11
|
|
|
12
|
+
/** bitcoin-backup encryptData: salt(16) || iv(12) || AES-GCM ciphertext (16-byte tag). */
|
|
13
|
+
const SALT_LENGTH_BYTES = 16;
|
|
14
|
+
const IV_LENGTH_BYTES = 12;
|
|
15
|
+
const AES_GCM_TAG_BYTES = 16;
|
|
16
|
+
const MIN_ENVELOPE_BYTES = SALT_LENGTH_BYTES + IV_LENGTH_BYTES + AES_GCM_TAG_BYTES;
|
|
17
|
+
|
|
12
18
|
export type PublicIdentity = {
|
|
13
19
|
bapId: string;
|
|
14
20
|
pubkey: string;
|
|
@@ -153,3 +159,89 @@ export function looksLikePlaintextBackup(text: string): boolean {
|
|
|
153
159
|
return false;
|
|
154
160
|
}
|
|
155
161
|
}
|
|
162
|
+
|
|
163
|
+
function looksLikeWif(text: string): boolean {
|
|
164
|
+
return /^[5KL][1-9A-HJ-NP-Za-km-z]{50,51}$/.test(text);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function looksLikeExtendedKey(text: string): boolean {
|
|
168
|
+
return /^(xprv|tprv|yprv|zprv|Yprv|Zprv)/.test(text);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function looksLikeMnemonic(text: string): boolean {
|
|
172
|
+
const words: string[] = [];
|
|
173
|
+
let current = "";
|
|
174
|
+
for (const ch of text) {
|
|
175
|
+
if (ch === " " || ch === "\t" || ch === "\n" || ch === "\r") {
|
|
176
|
+
if (current.length > 0) {
|
|
177
|
+
words.push(current);
|
|
178
|
+
current = "";
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
current += ch;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
if (current.length > 0) {
|
|
185
|
+
words.push(current);
|
|
186
|
+
}
|
|
187
|
+
const wordCount = words.length;
|
|
188
|
+
if (
|
|
189
|
+
wordCount !== 12 &&
|
|
190
|
+
wordCount !== 15 &&
|
|
191
|
+
wordCount !== 18 &&
|
|
192
|
+
wordCount !== 21 &&
|
|
193
|
+
wordCount !== 24
|
|
194
|
+
) {
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
return words.every((word) => /^[a-z]+$/i.test(word));
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function looksLikeJsonObject(text: string): boolean {
|
|
201
|
+
try {
|
|
202
|
+
const parsed = JSON.parse(text) as unknown;
|
|
203
|
+
return parsed !== null && typeof parsed === "object";
|
|
204
|
+
} catch {
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function isBitcoinBackupCiphertext(text: string): boolean {
|
|
210
|
+
if (text.length === 0) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
if (
|
|
214
|
+
looksLikePlaintextBackup(text) ||
|
|
215
|
+
looksLikeJsonObject(text) ||
|
|
216
|
+
looksLikeWif(text) ||
|
|
217
|
+
looksLikeExtendedKey(text) ||
|
|
218
|
+
looksLikeMnemonic(text)
|
|
219
|
+
) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const bytes = Utils.toArray(text, "base64");
|
|
224
|
+
if (bytes.length < MIN_ENVELOPE_BYTES) {
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
return Utils.toBase64(bytes) === text;
|
|
228
|
+
} catch {
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function assertBitcoinBackupCiphertext(text: string): void {
|
|
234
|
+
if (
|
|
235
|
+
looksLikePlaintextBackup(text) ||
|
|
236
|
+
looksLikeWif(text) ||
|
|
237
|
+
looksLikeExtendedKey(text) ||
|
|
238
|
+
looksLikeMnemonic(text)
|
|
239
|
+
) {
|
|
240
|
+
cryptoFail(
|
|
241
|
+
"refusing to upload plaintext backup (rootPk/xprv/wif/mnemonic present)"
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
if (!isBitcoinBackupCiphertext(text)) {
|
|
245
|
+
cryptoFail("refusing to upload: not opaque bitcoin-backup ciphertext");
|
|
246
|
+
}
|
|
247
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -11,6 +11,12 @@ import {
|
|
|
11
11
|
identityInfo,
|
|
12
12
|
oauthRegister,
|
|
13
13
|
} from "./commands.ts";
|
|
14
|
+
import {
|
|
15
|
+
diagnoseBap,
|
|
16
|
+
diagnoseClient,
|
|
17
|
+
diagnoseIdentities,
|
|
18
|
+
diagnoseLastOauth,
|
|
19
|
+
} from "./diagnose.ts";
|
|
14
20
|
import { loadConfig } from "./config.ts";
|
|
15
21
|
import { usage } from "./error.ts";
|
|
16
22
|
import { reportError } from "./output.ts";
|
|
@@ -47,6 +53,18 @@ export async function run(argv: string[]): Promise<number> {
|
|
|
47
53
|
if (group === "doctor") {
|
|
48
54
|
return await doctor(args, cfg);
|
|
49
55
|
}
|
|
56
|
+
if (group === "diagnose" && command === "bap") {
|
|
57
|
+
return await diagnoseBap(args, cfg);
|
|
58
|
+
}
|
|
59
|
+
if (group === "diagnose" && command === "identities") {
|
|
60
|
+
return await diagnoseIdentities(args, cfg);
|
|
61
|
+
}
|
|
62
|
+
if (group === "diagnose" && command === "last-oauth") {
|
|
63
|
+
return await diagnoseLastOauth(args, cfg);
|
|
64
|
+
}
|
|
65
|
+
if (group === "diagnose" && command === "client") {
|
|
66
|
+
return await diagnoseClient(args, cfg);
|
|
67
|
+
}
|
|
50
68
|
if (!group) {
|
|
51
69
|
process.stdout.write(HELP);
|
|
52
70
|
return 0;
|
package/src/output.ts
CHANGED
|
@@ -10,7 +10,14 @@ export function printJson(ok: boolean, data: unknown, error?: unknown): void {
|
|
|
10
10
|
process.stdout.write(`${JSON.stringify({ ok: true, data })}\n`);
|
|
11
11
|
return;
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
const body: Record<string, unknown> = { ok: false };
|
|
14
|
+
if (error !== undefined) {
|
|
15
|
+
body.error = error;
|
|
16
|
+
}
|
|
17
|
+
if (data !== undefined) {
|
|
18
|
+
body.data = data;
|
|
19
|
+
}
|
|
20
|
+
process.stdout.write(`${JSON.stringify(body)}\n`);
|
|
14
21
|
}
|
|
15
22
|
|
|
16
23
|
export function printHuman(mode: OutputMode, text: string): void {
|
package/src/password.ts
CHANGED
|
@@ -4,6 +4,15 @@ import { boolFlag, flag } from "./args.ts";
|
|
|
4
4
|
import { MIN_PASSWORD_LENGTH } from "./config.ts";
|
|
5
5
|
import { cryptoFail, usage } from "./error.ts";
|
|
6
6
|
|
|
7
|
+
function isAllWhitespace(value: string): boolean {
|
|
8
|
+
for (const ch of value) {
|
|
9
|
+
if (!/\s/.test(ch)) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return true;
|
|
14
|
+
}
|
|
15
|
+
|
|
7
16
|
function envPassword(): string | undefined {
|
|
8
17
|
if (!("SIGMA_BACKUP_PASSWORD" in process.env)) {
|
|
9
18
|
return undefined;
|
|
@@ -12,6 +21,9 @@ function envPassword(): string | undefined {
|
|
|
12
21
|
if (value === undefined || value === "") {
|
|
13
22
|
usage("SIGMA_BACKUP_PASSWORD is set but empty");
|
|
14
23
|
}
|
|
24
|
+
if (isAllWhitespace(value)) {
|
|
25
|
+
usage("SIGMA_BACKUP_PASSWORD is whitespace-only");
|
|
26
|
+
}
|
|
15
27
|
return value;
|
|
16
28
|
}
|
|
17
29
|
|