bsv-bap 0.2.0 → 0.3.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/dist/constants.d.ts +2 -0
- package/dist/index.modern.js +2 -2
- package/dist/index.modern.js.map +6 -6
- package/dist/index.module.js +2 -2
- package/dist/index.module.js.map +6 -6
- package/dist/touchid.d.ts +45 -0
- package/dist/utils.d.ts +12 -0
- package/package.json +7 -2
- package/src/cli.ts +593 -323
- package/src/touchid.ts +87 -0
package/src/cli.ts
CHANGED
|
@@ -3,96 +3,184 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { PrivateKey } from "@bsv/sdk";
|
|
6
|
-
import { Command } from "commander";
|
|
7
6
|
import { BAP, bapIdFromAddress, bapIdFromPubkey } from "bsv-bap";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import {
|
|
9
|
+
getTouchIDStatus,
|
|
10
|
+
isTouchIDSupported,
|
|
11
|
+
protectRootKey,
|
|
12
|
+
removeProtection,
|
|
13
|
+
unlockRootKey,
|
|
14
|
+
} from "./touchid.js";
|
|
8
15
|
|
|
9
16
|
// Storage paths
|
|
10
17
|
const CONFIG_DIR = join(homedir(), ".bap");
|
|
11
18
|
const CONFIG_FILE = join(CONFIG_DIR, "identity.json");
|
|
12
19
|
const ACTIVE_FILE = join(CONFIG_DIR, "active");
|
|
13
20
|
|
|
14
|
-
// Stored config shape
|
|
21
|
+
// Stored config shape — rootPk is plaintext, rootPkEncrypted is Touch ID protected.
|
|
22
|
+
// Only one should be present at a time.
|
|
15
23
|
interface StoredConfig {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
24
|
+
rootPk?: string;
|
|
25
|
+
rootPkEncrypted?: string;
|
|
26
|
+
ids: string;
|
|
27
|
+
labels: Record<string, string>;
|
|
28
|
+
createdAt: string;
|
|
20
29
|
}
|
|
21
30
|
|
|
22
31
|
function ensureConfigDir(): void {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
32
|
+
if (!existsSync(CONFIG_DIR)) {
|
|
33
|
+
mkdirSync(CONFIG_DIR, { recursive: true });
|
|
34
|
+
}
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
function loadConfig(): StoredConfig | null {
|
|
29
|
-
|
|
30
|
-
|
|
38
|
+
if (!existsSync(CONFIG_FILE)) return null;
|
|
39
|
+
return JSON.parse(readFileSync(CONFIG_FILE, "utf-8")) as StoredConfig;
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
function createBAP(key: string): BAP {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
// xprv keys start with "xprv" — use BIP32 mode; otherwise Type42
|
|
44
|
+
if (key.startsWith("xprv")) {
|
|
45
|
+
return new BAP(key);
|
|
46
|
+
}
|
|
47
|
+
return new BAP({ rootPk: key });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the root private key from config.
|
|
52
|
+
* If Touch ID protected, triggers biometric auth to decrypt.
|
|
53
|
+
* Returns the plaintext key string (WIF or xprv).
|
|
54
|
+
*/
|
|
55
|
+
async function resolveRootKey(config: StoredConfig): Promise<string> {
|
|
56
|
+
if (config.rootPk) {
|
|
57
|
+
return config.rootPk;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (config.rootPkEncrypted) {
|
|
61
|
+
return unlockRootKey(config.rootPkEncrypted);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Legacy format: older BAP configs used "wif" instead of "rootPk"
|
|
65
|
+
const legacy = config as Record<string, unknown>;
|
|
66
|
+
if (typeof legacy.wif === "string") {
|
|
67
|
+
return legacy.wif;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
throw new Error(
|
|
71
|
+
"Config has neither rootPk nor rootPkEncrypted. File may be corrupt."
|
|
72
|
+
);
|
|
39
73
|
}
|
|
40
74
|
|
|
41
|
-
function loadBAP(): {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
75
|
+
async function loadBAP(): Promise<{
|
|
76
|
+
bap: BAP;
|
|
77
|
+
config: StoredConfig;
|
|
78
|
+
rootPk: string;
|
|
79
|
+
}> {
|
|
80
|
+
const config = loadConfig();
|
|
81
|
+
if (!config) {
|
|
82
|
+
console.error("No identity found. Run 'bap create' first.");
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
const rootPk = await resolveRootKey(config);
|
|
86
|
+
const bap = createBAP(rootPk);
|
|
87
|
+
if (config.ids) {
|
|
88
|
+
bap.importIds(config.ids);
|
|
89
|
+
}
|
|
90
|
+
return { bap, config, rootPk };
|
|
52
91
|
}
|
|
53
92
|
|
|
54
93
|
function saveConfig(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
94
|
+
bap: BAP,
|
|
95
|
+
rootPk: string,
|
|
96
|
+
labels: Record<string, string>,
|
|
97
|
+
createdAt?: string,
|
|
98
|
+
encrypted?: { rootPkEncrypted: string }
|
|
59
99
|
): void {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
100
|
+
ensureConfigDir();
|
|
101
|
+
const config: StoredConfig = {
|
|
102
|
+
ids: bap.exportIds(),
|
|
103
|
+
labels,
|
|
104
|
+
createdAt: createdAt ?? new Date().toISOString(),
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (encrypted) {
|
|
108
|
+
config.rootPkEncrypted = encrypted.rootPkEncrypted;
|
|
109
|
+
} else {
|
|
110
|
+
config.rootPk = rootPk;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
68
114
|
}
|
|
69
115
|
|
|
70
116
|
function getActiveBapId(): string | null {
|
|
71
|
-
|
|
72
|
-
|
|
117
|
+
if (!existsSync(ACTIVE_FILE)) return null;
|
|
118
|
+
return readFileSync(ACTIVE_FILE, "utf-8").trim();
|
|
73
119
|
}
|
|
74
120
|
|
|
75
121
|
function setActiveBapId(bapId: string): void {
|
|
76
|
-
|
|
77
|
-
|
|
122
|
+
ensureConfigDir();
|
|
123
|
+
writeFileSync(ACTIVE_FILE, bapId);
|
|
78
124
|
}
|
|
79
125
|
|
|
80
126
|
function getActiveIdentity(bap: BAP, config: StoredConfig) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
127
|
+
const activeBapId = getActiveBapId();
|
|
128
|
+
const ids = bap.listIds();
|
|
129
|
+
|
|
130
|
+
if (ids.length === 0) {
|
|
131
|
+
console.error("No identities found. Run 'bap create' first.");
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const bapId = activeBapId && ids.includes(activeBapId) ? activeBapId : ids[0];
|
|
136
|
+
const identity = bap.getId(bapId);
|
|
137
|
+
if (!identity) {
|
|
138
|
+
console.error(`Identity ${bapId} not found.`);
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
return { identity, bapId, label: config.labels?.[bapId] };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Attempt to protect a root key with Touch ID.
|
|
146
|
+
* Returns { rootPkEncrypted } on success, or null if Touch ID is unavailable.
|
|
147
|
+
* Prints status messages.
|
|
148
|
+
*
|
|
149
|
+
* Set BAP_NO_TOUCHID=1 to disable (used by tests and non-interactive environments).
|
|
150
|
+
*/
|
|
151
|
+
async function tryProtectWithTouchID(
|
|
152
|
+
rootPk: string
|
|
153
|
+
): Promise<{ rootPkEncrypted: string } | null> {
|
|
154
|
+
if (process.env.BAP_NO_TOUCHID === "1") {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!isTouchIDSupported()) {
|
|
159
|
+
console.log(
|
|
160
|
+
" Touch ID: not available (platform unsupported) -- key stored as plaintext"
|
|
161
|
+
);
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
const status = await getTouchIDStatus(false);
|
|
167
|
+
if (!status.available) {
|
|
168
|
+
console.log(
|
|
169
|
+
` Touch ID: not available (${status.biometryType}) -- key stored as plaintext`
|
|
170
|
+
);
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const rootPkEncrypted = await protectRootKey(rootPk);
|
|
175
|
+
console.log(` Touch ID: protected (${status.biometryType})`);
|
|
176
|
+
return { rootPkEncrypted };
|
|
177
|
+
} catch (err) {
|
|
178
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
179
|
+
console.log(` Touch ID: skipped (${msg})`);
|
|
180
|
+
console.log(" WARNING: Key is stored as plaintext on disk.");
|
|
181
|
+
console.log(" Run 'bap touchid enable' later to protect it with Secure Enclave.");
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
96
184
|
}
|
|
97
185
|
|
|
98
186
|
// --- CLI ---
|
|
@@ -100,312 +188,494 @@ function getActiveIdentity(bap: BAP, config: StoredConfig) {
|
|
|
100
188
|
const program = new Command();
|
|
101
189
|
|
|
102
190
|
program
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
191
|
+
.name("bap")
|
|
192
|
+
.description("BAP - Bitcoin Attestation Protocol CLI")
|
|
193
|
+
.version("0.2.0");
|
|
106
194
|
|
|
107
195
|
// Identity Management
|
|
108
196
|
|
|
109
197
|
program
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
198
|
+
.command("create")
|
|
199
|
+
.description("Create a new identity")
|
|
200
|
+
.option("--name <name>", "Human-readable label for the identity")
|
|
201
|
+
.option("--wif <wif>", "Use an existing WIF key as the master key")
|
|
202
|
+
.option("--no-touchid", "Skip Touch ID protection")
|
|
203
|
+
.action(async (opts) => {
|
|
204
|
+
const config = loadConfig();
|
|
205
|
+
let rootPk: string;
|
|
206
|
+
let labels: Record<string, string>;
|
|
207
|
+
let createdAt: string | undefined;
|
|
208
|
+
let bap: BAP;
|
|
209
|
+
let encrypted: { rootPkEncrypted: string } | undefined;
|
|
210
|
+
|
|
211
|
+
if (config) {
|
|
212
|
+
// Existing master -- add a new identity
|
|
213
|
+
rootPk = await resolveRootKey(config);
|
|
214
|
+
labels = config.labels;
|
|
215
|
+
createdAt = config.createdAt;
|
|
216
|
+
bap = createBAP(rootPk);
|
|
217
|
+
bap.importIds(config.ids);
|
|
218
|
+
|
|
219
|
+
// Preserve existing protection state
|
|
220
|
+
if (config.rootPkEncrypted) {
|
|
221
|
+
encrypted = { rootPkEncrypted: config.rootPkEncrypted };
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
// New master
|
|
225
|
+
rootPk = opts.wif ?? PrivateKey.fromRandom().toWif();
|
|
226
|
+
labels = {};
|
|
227
|
+
bap = createBAP(rootPk);
|
|
228
|
+
|
|
229
|
+
// Protect new key with Touch ID if available
|
|
230
|
+
if (opts.touchid !== false) {
|
|
231
|
+
const protection = await tryProtectWithTouchID(rootPk);
|
|
232
|
+
if (protection) {
|
|
233
|
+
encrypted = protection;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const identity = bap.newId();
|
|
239
|
+
const bapId = identity.bapId;
|
|
240
|
+
|
|
241
|
+
if (opts.name) {
|
|
242
|
+
labels[bapId] = opts.name;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
saveConfig(bap, rootPk, labels, createdAt, encrypted);
|
|
246
|
+
setActiveBapId(bapId);
|
|
247
|
+
|
|
248
|
+
console.log("Identity created:");
|
|
249
|
+
console.log(` BAP ID: ${bapId}`);
|
|
250
|
+
if (opts.name) console.log(` Label: ${opts.name}`);
|
|
251
|
+
console.log(` Root Address: ${identity.rootAddress}`);
|
|
252
|
+
console.log(` Root Path: ${identity.rootPath}`);
|
|
253
|
+
console.log(` Stored at: ${CONFIG_FILE}`);
|
|
254
|
+
console.log("");
|
|
255
|
+
console.log(" IMPORTANT: Back up your identity now with 'bap export > backup.json'.");
|
|
256
|
+
console.log(" If Touch ID is enabled, your key is hardware-bound and cannot be");
|
|
257
|
+
console.log(" recovered from another machine without this backup.");
|
|
258
|
+
});
|
|
152
259
|
|
|
153
260
|
program
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
261
|
+
.command("list")
|
|
262
|
+
.description("List all identities (* = active)")
|
|
263
|
+
.action(async () => {
|
|
264
|
+
const { bap, config } = await loadBAP();
|
|
265
|
+
const ids = bap.listIds();
|
|
266
|
+
const active = getActiveBapId();
|
|
267
|
+
|
|
268
|
+
if (ids.length === 0) {
|
|
269
|
+
console.log("No identities. Run 'bap create' to get started.");
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
for (const bapId of ids) {
|
|
274
|
+
const marker = bapId === active ? " *" : " ";
|
|
275
|
+
const label = config.labels?.[bapId];
|
|
276
|
+
const suffix = label ? ` (${label})` : "";
|
|
277
|
+
console.log(`${marker} ${bapId}${suffix}`);
|
|
278
|
+
}
|
|
279
|
+
});
|
|
173
280
|
|
|
174
281
|
program
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
282
|
+
.command("use")
|
|
283
|
+
.description("Set active identity")
|
|
284
|
+
.argument("<bapId>", "BAP ID to activate")
|
|
285
|
+
.action(async (bapId: string) => {
|
|
286
|
+
const { bap } = await loadBAP();
|
|
287
|
+
const ids = bap.listIds();
|
|
288
|
+
|
|
289
|
+
if (!ids.includes(bapId)) {
|
|
290
|
+
console.error(`Identity ${bapId} not found.`);
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
setActiveBapId(bapId);
|
|
295
|
+
console.log(`Active identity: ${bapId}`);
|
|
296
|
+
});
|
|
190
297
|
|
|
191
298
|
program
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
299
|
+
.command("info")
|
|
300
|
+
.description("Show active identity details")
|
|
301
|
+
.action(async () => {
|
|
302
|
+
const { bap, config } = await loadBAP();
|
|
303
|
+
const { identity, bapId, label } = getActiveIdentity(bap, config);
|
|
304
|
+
|
|
305
|
+
console.log("Active Identity:");
|
|
306
|
+
console.log(` BAP ID: ${bapId}`);
|
|
307
|
+
if (label) console.log(` Label: ${label}`);
|
|
308
|
+
console.log(` Root Address: ${identity.rootAddress}`);
|
|
309
|
+
console.log(` Root Path: ${identity.rootPath}`);
|
|
310
|
+
console.log(` Current Path: ${identity.currentPath}`);
|
|
311
|
+
console.log(` Previous Path: ${identity.previousPath}`);
|
|
312
|
+
console.log(
|
|
313
|
+
` Account Key: ${identity.getAccountKey().toPublicKey().toString()}`
|
|
314
|
+
);
|
|
315
|
+
});
|
|
207
316
|
|
|
208
317
|
program
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
318
|
+
.command("remove")
|
|
319
|
+
.description("Remove an identity")
|
|
320
|
+
.argument("<bapId>", "BAP ID to remove")
|
|
321
|
+
.action(async (bapId: string) => {
|
|
322
|
+
const { bap, config, rootPk } = await loadBAP();
|
|
323
|
+
|
|
324
|
+
if (!bap.getId(bapId)) {
|
|
325
|
+
console.error(`Identity ${bapId} not found.`);
|
|
326
|
+
process.exit(1);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
bap.removeId(bapId);
|
|
330
|
+
if (config.labels) delete config.labels[bapId];
|
|
331
|
+
|
|
332
|
+
// Preserve Touch ID protection state
|
|
333
|
+
const encrypted = config.rootPkEncrypted
|
|
334
|
+
? { rootPkEncrypted: config.rootPkEncrypted }
|
|
335
|
+
: undefined;
|
|
336
|
+
saveConfig(bap, rootPk, config.labels, config.createdAt, encrypted);
|
|
337
|
+
|
|
338
|
+
// Clear active if it was this one
|
|
339
|
+
if (getActiveBapId() === bapId) {
|
|
340
|
+
const remaining = bap.listIds();
|
|
341
|
+
if (remaining.length > 0) {
|
|
342
|
+
setActiveBapId(remaining[0]);
|
|
343
|
+
} else {
|
|
344
|
+
writeFileSync(ACTIVE_FILE, "");
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
console.log(`Removed identity: ${bapId}`);
|
|
349
|
+
});
|
|
236
350
|
|
|
237
351
|
// Backup
|
|
238
352
|
|
|
239
353
|
program
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
354
|
+
.command("export")
|
|
355
|
+
.description("Export master backup (JSON to stdout)")
|
|
356
|
+
.action(async () => {
|
|
357
|
+
const { bap, config } = await loadBAP();
|
|
358
|
+
if (config.rootPkEncrypted) {
|
|
359
|
+
console.error(
|
|
360
|
+
"NOTE: Your key is protected by Secure Enclave (hardware-bound)."
|
|
361
|
+
);
|
|
362
|
+
console.error(
|
|
363
|
+
"This backup is the ONLY recovery path if this machine is lost or wiped."
|
|
364
|
+
);
|
|
365
|
+
console.error("Store it securely.\n");
|
|
366
|
+
}
|
|
367
|
+
const backup = bap.exportForBackup();
|
|
368
|
+
console.log(JSON.stringify(backup, null, 2));
|
|
369
|
+
});
|
|
247
370
|
|
|
248
371
|
program
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
372
|
+
.command("export-account")
|
|
373
|
+
.description("Export account backup for active or specified identity")
|
|
374
|
+
.option("--id <bapId>", "Specific BAP ID to export")
|
|
375
|
+
.action(async (opts) => {
|
|
376
|
+
const { bap, config } = await loadBAP();
|
|
377
|
+
|
|
378
|
+
const identity = opts.id
|
|
379
|
+
? (() => {
|
|
380
|
+
const id = bap.getId(opts.id);
|
|
381
|
+
if (!id) {
|
|
382
|
+
console.error(`Identity ${opts.id} not found.`);
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
return id;
|
|
386
|
+
})()
|
|
387
|
+
: getActiveIdentity(bap, config).identity;
|
|
388
|
+
|
|
389
|
+
const backup = identity.exportAccountBackup();
|
|
390
|
+
console.log(JSON.stringify(backup, null, 2));
|
|
391
|
+
});
|
|
269
392
|
|
|
270
393
|
program
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
394
|
+
.command("import")
|
|
395
|
+
.description("Import from backup file")
|
|
396
|
+
.argument("<file>", "Path to backup JSON file")
|
|
397
|
+
.option("--no-touchid", "Skip Touch ID protection")
|
|
398
|
+
.action(async (file: string, opts) => {
|
|
399
|
+
if (!existsSync(file)) {
|
|
400
|
+
console.error(`File not found: ${file}`);
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const backup = JSON.parse(readFileSync(file, "utf-8"));
|
|
405
|
+
|
|
406
|
+
if (!backup.rootPk && !backup.xprv) {
|
|
407
|
+
console.error("Invalid backup format: missing rootPk or xprv");
|
|
408
|
+
process.exit(1);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
let bap: BAP;
|
|
412
|
+
let rootPk: string;
|
|
413
|
+
|
|
414
|
+
if (backup.rootPk) {
|
|
415
|
+
bap = new BAP({ rootPk: backup.rootPk });
|
|
416
|
+
rootPk = backup.rootPk;
|
|
417
|
+
} else {
|
|
418
|
+
bap = new BAP(backup.xprv);
|
|
419
|
+
rootPk = backup.xprv;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (backup.ids) {
|
|
423
|
+
bap.importIds(backup.ids);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const labels: Record<string, string> = {};
|
|
427
|
+
if (backup.label) {
|
|
428
|
+
// Apply label to first identity as a default
|
|
429
|
+
const ids = bap.listIds();
|
|
430
|
+
if (ids.length > 0) {
|
|
431
|
+
labels[ids[0]] = backup.label;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Protect with Touch ID if available
|
|
436
|
+
let encrypted: { rootPkEncrypted: string } | undefined;
|
|
437
|
+
if (opts.touchid !== false) {
|
|
438
|
+
const protection = await tryProtectWithTouchID(rootPk);
|
|
439
|
+
if (protection) {
|
|
440
|
+
encrypted = protection;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
saveConfig(bap, rootPk, labels, undefined, encrypted);
|
|
445
|
+
|
|
446
|
+
const ids = bap.listIds();
|
|
447
|
+
if (ids.length > 0) {
|
|
448
|
+
setActiveBapId(ids[0]);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
console.log("Backup imported:");
|
|
452
|
+
console.log(` Identities: ${ids.length}`);
|
|
453
|
+
if (backup.label) console.log(` Label: ${backup.label}`);
|
|
454
|
+
console.log(` Stored at: ${CONFIG_FILE}`);
|
|
455
|
+
});
|
|
323
456
|
|
|
324
457
|
// Crypto
|
|
325
458
|
|
|
326
459
|
program
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
460
|
+
.command("encrypt")
|
|
461
|
+
.description("Encrypt data with master key (ECIES)")
|
|
462
|
+
.argument("<data>", "Data to encrypt")
|
|
463
|
+
.action(async (data: string) => {
|
|
464
|
+
const { bap } = await loadBAP();
|
|
465
|
+
console.log(bap.encrypt(data));
|
|
466
|
+
});
|
|
334
467
|
|
|
335
468
|
program
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
469
|
+
.command("decrypt")
|
|
470
|
+
.description("Decrypt ciphertext with master key")
|
|
471
|
+
.argument("<ciphertext>", "Base64 ciphertext to decrypt")
|
|
472
|
+
.action(async (ciphertext: string) => {
|
|
473
|
+
const { bap } = await loadBAP();
|
|
474
|
+
console.log(bap.decrypt(ciphertext));
|
|
475
|
+
});
|
|
343
476
|
|
|
344
477
|
program
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
478
|
+
.command("verify")
|
|
479
|
+
.description("Verify a BSM signature")
|
|
480
|
+
.argument("<message>", "Original message")
|
|
481
|
+
.argument("<signature>", "Base64 signature")
|
|
482
|
+
.argument("<address>", "Signing address")
|
|
483
|
+
.action((message: string, signature: string, address: string) => {
|
|
484
|
+
const bap = new BAP({ rootPk: PrivateKey.fromRandom().toWif() });
|
|
485
|
+
let valid = false;
|
|
486
|
+
try {
|
|
487
|
+
valid = bap.verifySignature(message, address, signature);
|
|
488
|
+
} catch {
|
|
489
|
+
// Invalid signature format -- treat as not valid
|
|
490
|
+
}
|
|
491
|
+
console.log(
|
|
492
|
+
JSON.stringify({ valid, message, address, signature }, null, 2)
|
|
493
|
+
);
|
|
494
|
+
});
|
|
360
495
|
|
|
361
496
|
// API Lookups
|
|
362
497
|
|
|
363
498
|
program
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
499
|
+
.command("lookup")
|
|
500
|
+
.description("Lookup identity on the BAP overlay")
|
|
501
|
+
.argument("<bapId>", "BAP ID to lookup")
|
|
502
|
+
.action(async (bapId: string) => {
|
|
503
|
+
const bap = new BAP({ rootPk: PrivateKey.fromRandom().toWif() });
|
|
504
|
+
const result = await bap.getIdentity(bapId);
|
|
505
|
+
console.log(JSON.stringify(result, null, 2));
|
|
506
|
+
});
|
|
372
507
|
|
|
373
508
|
program
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
509
|
+
.command("lookup-address")
|
|
510
|
+
.description("Lookup identity by Bitcoin address")
|
|
511
|
+
.argument("<address>", "Bitcoin address to lookup")
|
|
512
|
+
.action(async (address: string) => {
|
|
513
|
+
const bap = new BAP({ rootPk: PrivateKey.fromRandom().toWif() });
|
|
514
|
+
const result = await bap.getIdentityFromAddress(address);
|
|
515
|
+
console.log(JSON.stringify(result, null, 2));
|
|
516
|
+
});
|
|
382
517
|
|
|
383
518
|
program
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
519
|
+
.command("attestations")
|
|
520
|
+
.description("Get attestations for an attribute hash")
|
|
521
|
+
.argument("<hash>", "Attribute hash to lookup")
|
|
522
|
+
.action(async (hash: string) => {
|
|
523
|
+
const bap = new BAP({ rootPk: PrivateKey.fromRandom().toWif() });
|
|
524
|
+
const result = await bap.getAttestationsForHash(hash);
|
|
525
|
+
console.log(JSON.stringify(result, null, 2));
|
|
526
|
+
});
|
|
392
527
|
|
|
393
528
|
// Utilities
|
|
394
529
|
|
|
395
530
|
program
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
531
|
+
.command("id-from-address")
|
|
532
|
+
.description("Derive BAP ID from a Bitcoin address")
|
|
533
|
+
.argument("<address>", "Bitcoin address (must be the root/member address)")
|
|
534
|
+
.action((address: string) => {
|
|
535
|
+
console.log(bapIdFromAddress(address));
|
|
536
|
+
});
|
|
402
537
|
|
|
403
538
|
program
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
539
|
+
.command("id-from-pubkey")
|
|
540
|
+
.description("Derive BAP ID from a compressed public key")
|
|
541
|
+
.argument("<pubkey>", "Compressed public key hex (must be the member key)")
|
|
542
|
+
.action((pubkey: string) => {
|
|
543
|
+
console.log(bapIdFromPubkey(pubkey));
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// Touch ID Management
|
|
547
|
+
|
|
548
|
+
const touchid = program
|
|
549
|
+
.command("touchid")
|
|
550
|
+
.description("Manage Touch ID key protection");
|
|
551
|
+
|
|
552
|
+
touchid
|
|
553
|
+
.command("status")
|
|
554
|
+
.description("Check Touch ID availability and protection status")
|
|
555
|
+
.action(async () => {
|
|
556
|
+
const config = loadConfig();
|
|
557
|
+
const hasEncryptedKey = !!config?.rootPkEncrypted;
|
|
558
|
+
const status = await getTouchIDStatus(hasEncryptedKey);
|
|
559
|
+
|
|
560
|
+
console.log("Touch ID Status:");
|
|
561
|
+
console.log(` Available: ${status.available}`);
|
|
562
|
+
console.log(` Biometry: ${status.biometryType}`);
|
|
563
|
+
console.log(` Key Protected: ${status.protected}`);
|
|
564
|
+
|
|
565
|
+
if (config && !hasEncryptedKey && status.available) {
|
|
566
|
+
console.log("\n Your identity key is stored as plaintext.");
|
|
567
|
+
console.log(" Run 'bap touchid enable' to protect it with Touch ID.");
|
|
568
|
+
}
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
touchid
|
|
572
|
+
.command("enable")
|
|
573
|
+
.description("Protect identity key with Touch ID")
|
|
574
|
+
.action(async () => {
|
|
575
|
+
const config = loadConfig();
|
|
576
|
+
if (!config) {
|
|
577
|
+
console.error("No identity found. Run 'bap create' first.");
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (config.rootPkEncrypted) {
|
|
582
|
+
if (config.rootPkEncrypted.startsWith("se:")) {
|
|
583
|
+
console.log("Identity key is already protected with Secure Enclave.");
|
|
584
|
+
} else {
|
|
585
|
+
console.error(
|
|
586
|
+
'Identity key uses the legacy encryption format which is no longer supported. Re-import your backup with "bap import <file>" to migrate.'
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// Support legacy "wif" field from older BAP configs
|
|
593
|
+
const plainKey = config.rootPk ?? (config as Record<string, unknown>).wif as string | undefined;
|
|
594
|
+
if (!plainKey) {
|
|
595
|
+
console.error(
|
|
596
|
+
"Config has no plaintext key to protect. File may be corrupt."
|
|
597
|
+
);
|
|
598
|
+
process.exit(1);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (!isTouchIDSupported()) {
|
|
602
|
+
console.error(
|
|
603
|
+
"Secure Enclave is not available on this platform (requires macOS arm64)."
|
|
604
|
+
);
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
const status = await getTouchIDStatus(false);
|
|
609
|
+
if (!status.available) {
|
|
610
|
+
console.error(
|
|
611
|
+
`Touch ID is not available on this machine (biometry type: ${status.biometryType}).`
|
|
612
|
+
);
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
console.log("WARNING: Secure Enclave keys are hardware-bound to THIS machine.");
|
|
617
|
+
console.log("Export a backup first with 'bap export > backup.json' if you haven't already.\n");
|
|
618
|
+
console.log("Encrypting identity key with Secure Enclave...");
|
|
619
|
+
const rootPkEncrypted = await protectRootKey(plainKey);
|
|
620
|
+
|
|
621
|
+
// Rewrite config: replace rootPk with rootPkEncrypted (sentinel "se:bap-master")
|
|
622
|
+
const newConfig: StoredConfig = {
|
|
623
|
+
rootPkEncrypted,
|
|
624
|
+
ids: config.ids,
|
|
625
|
+
labels: config.labels,
|
|
626
|
+
createdAt: config.createdAt,
|
|
627
|
+
};
|
|
628
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, 2));
|
|
629
|
+
|
|
630
|
+
console.log("Identity key is now protected with Secure Enclave + Touch ID.");
|
|
631
|
+
console.log("The plaintext key has been removed from disk.");
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
touchid
|
|
635
|
+
.command("disable")
|
|
636
|
+
.description("Remove Touch ID protection (stores key as plaintext)")
|
|
637
|
+
.action(async () => {
|
|
638
|
+
const config = loadConfig();
|
|
639
|
+
if (!config) {
|
|
640
|
+
console.error("No identity found. Run 'bap create' first.");
|
|
641
|
+
process.exit(1);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (!config.rootPkEncrypted) {
|
|
645
|
+
console.log(
|
|
646
|
+
"Identity key is not Touch ID protected. Nothing to disable."
|
|
647
|
+
);
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (!config.rootPkEncrypted.startsWith("se:")) {
|
|
652
|
+
console.error(
|
|
653
|
+
"Identity key uses the legacy encryption format which is no longer supported."
|
|
654
|
+
);
|
|
655
|
+
console.error(
|
|
656
|
+
"You will need to re-import your backup. Run 'bap export' if you can still decrypt, or use your backup file."
|
|
657
|
+
);
|
|
658
|
+
process.exit(1);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
console.log("Decrypting identity key (Touch ID required)...");
|
|
662
|
+
const rootPk = await unlockRootKey(config.rootPkEncrypted);
|
|
663
|
+
|
|
664
|
+
// Rewrite config: replace rootPkEncrypted with rootPk
|
|
665
|
+
const newConfig: StoredConfig = {
|
|
666
|
+
rootPk,
|
|
667
|
+
ids: config.ids,
|
|
668
|
+
labels: config.labels,
|
|
669
|
+
createdAt: config.createdAt,
|
|
670
|
+
};
|
|
671
|
+
writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, 2));
|
|
672
|
+
|
|
673
|
+
// Remove the Secure Enclave key and vault file
|
|
674
|
+
await removeProtection();
|
|
675
|
+
|
|
676
|
+
console.log("Secure Enclave protection removed.");
|
|
677
|
+
console.log("WARNING: Your identity key is now stored as plaintext on disk.");
|
|
678
|
+
console.log("Anyone with access to this machine can read it.");
|
|
679
|
+
});
|
|
410
680
|
|
|
411
681
|
program.parse();
|