bsv-bap 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/.babelrc +20 -0
  2. package/.eslintrc +46 -0
  3. package/LICENSE +25 -0
  4. package/README.md +819 -0
  5. package/babel.config.js +6 -0
  6. package/bun.lockb +0 -0
  7. package/coverage/clover.xml +6 -0
  8. package/coverage/coverage-final.json +1 -0
  9. package/coverage/lcov-report/base.css +224 -0
  10. package/coverage/lcov-report/block-navigation.js +87 -0
  11. package/coverage/lcov-report/favicon.png +0 -0
  12. package/coverage/lcov-report/index.html +101 -0
  13. package/coverage/lcov-report/prettify.css +1 -0
  14. package/coverage/lcov-report/prettify.js +2 -0
  15. package/coverage/lcov-report/sort-arrow-sprite.png +0 -0
  16. package/coverage/lcov-report/sorter.js +196 -0
  17. package/coverage/lcov-report/src/constants.ts.html +113 -0
  18. package/coverage/lcov-report/src/id.ts.html +2207 -0
  19. package/coverage/lcov-report/src/index.html +156 -0
  20. package/coverage/lcov-report/src/index.ts.html +1877 -0
  21. package/coverage/lcov-report/src/utils.ts.html +404 -0
  22. package/coverage/lcov-report/tests/data/index.html +111 -0
  23. package/coverage/lcov-report/tests/data/keys.js.html +86 -0
  24. package/coverage/lcov.info +0 -0
  25. package/dist/jest.config.d.ts +8 -0
  26. package/dist/src/constants.d.ts +8 -0
  27. package/dist/src/id.d.ts +295 -0
  28. package/dist/src/index.d.ts +238 -0
  29. package/dist/src/interface.d.ts +23 -0
  30. package/dist/src/poa.d.ts +6 -0
  31. package/dist/src/utils.d.ts +54 -0
  32. package/dist/typescript-npm-package.cjs.d.ts +554 -0
  33. package/dist/typescript-npm-package.cjs.js +1320 -0
  34. package/dist/typescript-npm-package.cjs.js.map +1 -0
  35. package/dist/typescript-npm-package.esm.d.ts +554 -0
  36. package/dist/typescript-npm-package.esm.js +1312 -0
  37. package/dist/typescript-npm-package.esm.js.map +1 -0
  38. package/dist/typescript-npm-package.umd.d.ts +554 -0
  39. package/dist/typescript-npm-package.umd.js +110193 -0
  40. package/dist/typescript-npm-package.umd.js.map +1 -0
  41. package/jest.config.ts +196 -0
  42. package/jsdoc.json +16 -0
  43. package/package.json +80 -0
  44. package/rollup.config.js +64 -0
  45. package/setup-jest.js +1 -0
  46. package/src/README.md +80 -0
  47. package/src/attributes.json +119 -0
  48. package/src/constants.ts +11 -0
  49. package/src/id.ts +783 -0
  50. package/src/index.ts +631 -0
  51. package/src/interface.ts +26 -0
  52. package/src/poa.ts +9 -0
  53. package/src/utils.ts +111 -0
  54. package/tests/data/ids.json +30 -0
  55. package/tests/data/keys.js +2 -0
  56. package/tests/data/old-ids.json +25 -0
  57. package/tests/data/test-vectors.json +122 -0
  58. package/tests/id.test.js +286 -0
  59. package/tests/index.test.js +335 -0
  60. package/tests/regression.test.js +28 -0
  61. package/tests/utils.test.js +27 -0
  62. package/tsconfig.json +17 -0
package/src/id.ts ADDED
@@ -0,0 +1,783 @@
1
+ import { BSM, Hash, type HD, ECIES, PublicKey } from "@bsv/sdk";
2
+ import {
3
+ MAX_INT,
4
+ SIGNING_PATH_PREFIX,
5
+ BAP_SERVER,
6
+ BAP_BITCOM_ADDRESS,
7
+ AIP_BITCOM_ADDRESS,
8
+ ENCRYPTION_PATH,
9
+ } from "./constants";
10
+ import { Utils } from "./utils";
11
+ import type { Identity } from "./interface";
12
+ import { Utils as BSVUtils } from "@bsv/sdk";
13
+ const { toArray, toHex, toBase58, toUTF8, toBase64 } = BSVUtils;
14
+ const { bitcoreDecrypt, bitcoreEncrypt } = ECIES;
15
+
16
+ /**
17
+ * BAP_ID class
18
+ *
19
+ * This class should be used in conjunction with the BAP class
20
+ *
21
+ * @type {BAP_ID}
22
+ */
23
+ class BAP_ID {
24
+ #HDPrivateKey: HD;
25
+ #BAP_SERVER: string = BAP_SERVER;
26
+ #BAP_TOKEN = "";
27
+ #rootPath: string;
28
+ #previousPath: string;
29
+ #currentPath: string;
30
+ #idSeed: string;
31
+
32
+ idName: string;
33
+ description: string;
34
+
35
+ rootAddress: string;
36
+ identityKey: string;
37
+ identityAttributes: { [key: string]: any };
38
+
39
+ constructor(
40
+ HDPrivateKey: HD,
41
+ identityAttributes: { [key: string]: any } = {},
42
+ idSeed = "",
43
+ ) {
44
+ this.#idSeed = idSeed;
45
+ if (idSeed) {
46
+ // create a new HDPrivateKey based on the seed
47
+ const seedHex = toHex(Hash.sha256(idSeed, "utf8"));
48
+ const seedPath = Utils.getSigningPathFromHex(seedHex);
49
+ this.#HDPrivateKey = HDPrivateKey.derive(seedPath);
50
+ } else {
51
+ this.#HDPrivateKey = HDPrivateKey;
52
+ }
53
+
54
+ this.idName = "ID 1";
55
+ this.description = "";
56
+
57
+ this.#rootPath = `${SIGNING_PATH_PREFIX}/0/0/0`;
58
+ this.#previousPath = `${SIGNING_PATH_PREFIX}/0/0/0`;
59
+ this.#currentPath = `${SIGNING_PATH_PREFIX}/0/0/1`;
60
+
61
+ const rootChild = this.#HDPrivateKey.derive(this.#rootPath);
62
+ this.rootAddress = rootChild.privKey.toPublicKey().toAddress();
63
+ this.identityKey = this.deriveIdentityKey(this.rootAddress);
64
+
65
+ // unlink the object
66
+ identityAttributes = { ...identityAttributes };
67
+ this.identityAttributes = this.parseAttributes(identityAttributes);
68
+ }
69
+
70
+ set BAP_SERVER(bapServer) {
71
+ this.#BAP_SERVER = bapServer;
72
+ }
73
+
74
+ get BAP_SERVER(): string {
75
+ return this.#BAP_SERVER;
76
+ }
77
+
78
+ set BAP_TOKEN(token) {
79
+ this.#BAP_TOKEN = token;
80
+ }
81
+
82
+ get BAP_TOKEN(): string {
83
+ return this.#BAP_TOKEN;
84
+ }
85
+
86
+ deriveIdentityKey(address: string): string {
87
+ // base58( ripemd160 ( sha256 ( rootAddress ) ) )
88
+ const rootAddressHash = toHex(Hash.sha256(address, "utf8"));
89
+
90
+ return toBase58(Hash.ripemd160(rootAddressHash, "hex"));
91
+ }
92
+
93
+ /**
94
+ * Helper function to parse identity attributes
95
+ *
96
+ * @param identityAttributes
97
+ * @returns {{}}
98
+ */
99
+ parseAttributes(identityAttributes: { [key: string]: any } | string): {
100
+ [key: string]: any;
101
+ } {
102
+ if (typeof identityAttributes === "string") {
103
+ return this.parseStringUrns(identityAttributes);
104
+ }
105
+
106
+ for (const key in identityAttributes) {
107
+ if (!identityAttributes[key].value || !identityAttributes[key].nonce) {
108
+ throw new Error("Invalid identity attribute");
109
+ }
110
+ }
111
+
112
+ return identityAttributes || {};
113
+ }
114
+
115
+ /**
116
+ * Parse a text of urn string into identity attributes
117
+ *
118
+ * urn:bap:id:name:John Doe:e2c6fb4063cc04af58935737eaffc938011dff546d47b7fbb18ed346f8c4d4fa
119
+ * urn:bap:id:birthday:1990-05-22:e61f23cbbb2284842d77965e2b0e32f0ca890b1894ca4ce652831347ee3596d9
120
+ * urn:bap:id:over18:1:480ca17ccaacd671b28dc811332525f2f2cd594d8e8e7825de515ce5d52d30e8
121
+ *
122
+ * @param urnIdentityAttributes
123
+ */
124
+ parseStringUrns(urnIdentityAttributes: string): { [key: string]: any } {
125
+ const identityAttributes: { [key: string]: any } = {};
126
+ // avoid forEach
127
+
128
+ const attributesRaw = urnIdentityAttributes
129
+ .replace(/^\s+/g, "")
130
+ .replace(/\r/gm, "")
131
+ .split("\n");
132
+
133
+ for (const line of attributesRaw) {
134
+ // remove any whitespace from the string (trim)
135
+ const attribute = line.replace(/^\s+/g, "").replace(/\s+$/g, "");
136
+ const urn = attribute.split(":");
137
+ if (
138
+ urn[0] === "urn" &&
139
+ urn[1] === "bap" &&
140
+ urn[2] === "id" &&
141
+ urn[3] &&
142
+ urn[4] &&
143
+ urn[5]
144
+ ) {
145
+ identityAttributes[urn[3]] = {
146
+ value: urn[4],
147
+ nonce: urn[5],
148
+ };
149
+ }
150
+ }
151
+
152
+ return identityAttributes;
153
+ }
154
+
155
+ /**
156
+ * Returns the identity key
157
+ *
158
+ * @returns {*|string}
159
+ */
160
+ getIdentityKey(): string {
161
+ return this.identityKey;
162
+ }
163
+
164
+ /**
165
+ * Returns all the attributes in the identity
166
+ *
167
+ * @returns {*}
168
+ */
169
+ getAttributes(): { [key: string]: any } {
170
+ return this.identityAttributes;
171
+ }
172
+
173
+ /**
174
+ * Get the value of the given attribute
175
+ *
176
+ * @param attributeName
177
+ * @returns {{}|null}
178
+ */
179
+ getAttribute(attributeName: string): any {
180
+ if (this.identityAttributes[attributeName]) {
181
+ return this.identityAttributes[attributeName];
182
+ }
183
+
184
+ return null;
185
+ }
186
+
187
+ /**
188
+ * Set the value of the given attribute
189
+ *
190
+ * If an empty value ('' || null || false) is given, the attribute is removed from the ID
191
+ *
192
+ * @param attributeName string
193
+ * @param attributeValue any
194
+ * @returns {{}|null}
195
+ */
196
+ setAttribute(attributeName: string, attributeValue: any): void {
197
+ if (attributeValue) {
198
+ if (this.identityAttributes[attributeName]) {
199
+ this.identityAttributes[attributeName].value = attributeValue;
200
+ } else {
201
+ this.addAttribute(attributeName, attributeValue);
202
+ }
203
+ }
204
+ }
205
+
206
+ /**
207
+ * Unset the given attribute from the ID
208
+ *
209
+ * @param attributeName
210
+ * @returns {{}|null}
211
+ */
212
+ unsetAttribute(attributeName: string): void {
213
+ delete this.identityAttributes[attributeName];
214
+ }
215
+
216
+ /**
217
+ * Get all attribute urn's for this id
218
+ *
219
+ * @returns {string}
220
+ */
221
+ getAttributeUrns(): string {
222
+ let urns = "";
223
+ for (const key in this.identityAttributes) {
224
+ const urn = this.getAttributeUrn(key);
225
+ if (urn) {
226
+ urns += `${urn}\n`;
227
+ }
228
+ }
229
+
230
+ return urns;
231
+ }
232
+
233
+ /**
234
+ * Create an return the attribute urn for the given attribute
235
+ *
236
+ * @param attributeName
237
+ * @returns {string|null}
238
+ */
239
+ getAttributeUrn(attributeName: string) {
240
+ const attribute = this.identityAttributes[attributeName];
241
+ if (attribute) {
242
+ return `urn:bap:id:${attributeName}:${attribute.value}:${attribute.nonce}`;
243
+ }
244
+
245
+ return null;
246
+ }
247
+
248
+ /**
249
+ * Add an attribute to this identity
250
+ *
251
+ * @param attributeName
252
+ * @param value
253
+ * @param nonce
254
+ */
255
+ addAttribute(attributeName: string, value: any, nonce = ""): void {
256
+ if (!nonce) {
257
+ nonce = Utils.getRandomString();
258
+ }
259
+
260
+ this.identityAttributes[attributeName] = {
261
+ value,
262
+ nonce,
263
+ };
264
+ }
265
+
266
+ /**
267
+ * This should be called with the last part of the signing path (/.../.../...)
268
+ * This library assumes the first part is m/424150'/0'/0' as defined at the top of this file
269
+ *
270
+ * @param path The second path of the signing path in the format [0-9]{0,9}/[0-9]{0,9}/[0-9]{0,9}
271
+ */
272
+ set rootPath(path) {
273
+ if (this.#HDPrivateKey) {
274
+ if (path.split("/").length < 5) {
275
+ path = `${SIGNING_PATH_PREFIX}${path}`;
276
+ }
277
+
278
+ if (!this.validatePath(path)) {
279
+ throw new Error(`invalid signing path given ${path}`);
280
+ }
281
+
282
+ this.#rootPath = path;
283
+
284
+ const derivedChild = this.#HDPrivateKey.derive(path);
285
+ this.rootAddress = derivedChild.pubKey.toAddress();
286
+ // Identity keys should be derivatives of the root address - this allows checking
287
+ // of the creation transaction
288
+ this.identityKey = this.deriveIdentityKey(this.rootAddress);
289
+
290
+ // we also set this previousPath / currentPath to the root as we seem to be (re)setting this ID
291
+ this.#previousPath = path;
292
+ this.#currentPath = path;
293
+ }
294
+ }
295
+
296
+ get rootPath(): string {
297
+ return this.#rootPath;
298
+ }
299
+
300
+ getRootPath(): string {
301
+ return this.#rootPath;
302
+ }
303
+
304
+ /**
305
+ * This should be called with the last part of the signing path (/.../.../...)
306
+ * This library assumes the first part is m/424150'/0'/0' as defined at the top of this file
307
+ *
308
+ * @param path The second path of the signing path in the format [0-9]{0,9}/[0-9]{0,9}/[0-9]{0,9}
309
+ */
310
+ set currentPath(path) {
311
+ if (path.split("/").length < 5) {
312
+ path = `${SIGNING_PATH_PREFIX}${path}`;
313
+ }
314
+
315
+ if (!this.validatePath(path)) {
316
+ throw new Error("invalid signing path given");
317
+ }
318
+
319
+ this.#previousPath = this.#currentPath;
320
+ this.#currentPath = path;
321
+ }
322
+
323
+ get currentPath(): string {
324
+ return this.#currentPath;
325
+ }
326
+
327
+ get previousPath(): string {
328
+ return this.#previousPath;
329
+ }
330
+
331
+ /**
332
+ * This can be used to break the deterministic way child keys are created to make it harder for
333
+ * an attacker to steal the identites when the root key is compromised. This does however require
334
+ * the seeds to be stored at all times. If the seed is lost, the identity will not be recoverable.
335
+ */
336
+ get idSeed(): string {
337
+ return this.#idSeed;
338
+ }
339
+
340
+ /**
341
+ * Increment current path to a new path
342
+ *
343
+ * @returns {*}
344
+ */
345
+ incrementPath(): void {
346
+ this.currentPath = Utils.getNextPath(this.currentPath);
347
+ }
348
+
349
+ /**
350
+ * Check whether the given path is a valid path for use with this class
351
+ * The signing paths used here always have a length of 3
352
+ *
353
+ * @param path The last part of the signing path (example "/0/0/1")
354
+ * @returns {boolean}
355
+ */
356
+ validatePath(path: string) {
357
+ /* eslint-disable max-len */
358
+ if (
359
+ path.match(
360
+ /\/[0-9]{1,10}'?\/[0-9]{1,10}'?\/[0-9]{1,10}'?\/[0-9]{1,10}'?\/[0-9]{1,10}'?\/[0-9]{1,10}'?/,
361
+ )
362
+ ) {
363
+ const pathValues = path.split("/");
364
+ if (
365
+ pathValues.length === 7 &&
366
+ Number(pathValues[1].replace("'", "")) <= MAX_INT &&
367
+ Number(pathValues[2].replace("'", "")) <= MAX_INT &&
368
+ Number(pathValues[3].replace("'", "")) <= MAX_INT &&
369
+ Number(pathValues[4].replace("'", "")) <= MAX_INT &&
370
+ Number(pathValues[5].replace("'", "")) <= MAX_INT &&
371
+ Number(pathValues[6].replace("'", "")) <= MAX_INT
372
+ ) {
373
+ return true;
374
+ }
375
+ }
376
+
377
+ return false;
378
+ }
379
+
380
+ /**
381
+ * Get the OP_RETURN for the initial ID transaction (signed with root address)
382
+ *
383
+ * @returns {[]}
384
+ */
385
+ getInitialIdTransaction() {
386
+ return this.getIdTransaction(this.#rootPath);
387
+ }
388
+
389
+ /**
390
+ * Get the OP_RETURN for the ID transaction of the current address / path
391
+ *
392
+ * @returns {[]}
393
+ */
394
+ getIdTransaction(previousPath = "") {
395
+ if (this.#currentPath === this.#rootPath) {
396
+ throw new Error(
397
+ "Current path equals rootPath. ID was probably not initialized properly",
398
+ );
399
+ }
400
+
401
+ const opReturn = [
402
+ Buffer.from(BAP_BITCOM_ADDRESS).toString("hex"),
403
+ Buffer.from("ID").toString("hex"),
404
+ Buffer.from(this.identityKey).toString("hex"),
405
+ Buffer.from(this.getCurrentAddress()).toString("hex"),
406
+ ];
407
+
408
+ previousPath = previousPath || this.#previousPath;
409
+
410
+ return this.signOpReturnWithAIP(opReturn, previousPath);
411
+ }
412
+
413
+ /**
414
+ * Get address for given path
415
+ *
416
+ * @param path
417
+ * @returns {*}
418
+ */
419
+ getAddress(path: string): string {
420
+ const derivedChild = this.#HDPrivateKey.derive(path);
421
+ return derivedChild.privKey.toPublicKey().toAddress();
422
+ }
423
+
424
+ /**
425
+ * Get current signing address
426
+ *
427
+ * @returns {*}
428
+ */
429
+ getCurrentAddress(): string {
430
+ return this.getAddress(this.#currentPath);
431
+ }
432
+
433
+ /**
434
+ * Get the public key for encrypting data for this identity
435
+ */
436
+ getEncryptionPublicKey(): string {
437
+ const HDPrivateKey = this.#HDPrivateKey.derive(this.#rootPath);
438
+ const encryptionKey = HDPrivateKey.derive(ENCRYPTION_PATH).privKey;
439
+ // @ts-ignore
440
+ return encryptionKey.toPublicKey().toString();
441
+ }
442
+
443
+ /**
444
+ * Get the public key for encrypting data for this identity, using a seed for the encryption
445
+ */
446
+ getEncryptionPublicKeyWithSeed(seed: string): string {
447
+ const encryptionKey = this.getEncryptionPrivateKeyWithSeed(seed);
448
+ // @ts-ignore
449
+ return encryptionKey.toPublicKey().toString("hex");
450
+ }
451
+
452
+ /**
453
+ * Encrypt the given string data with the identity encryption key
454
+ * @param stringData
455
+ * @param counterPartyPublicKey Optional public key of the counterparty
456
+ * @return string Base64
457
+ */
458
+ encrypt(stringData: string, counterPartyPublicKey?: string): string {
459
+ const HDPrivateKey = this.#HDPrivateKey.derive(this.#rootPath);
460
+ const encryptionKey = HDPrivateKey.derive(ENCRYPTION_PATH).privKey;
461
+ const publicKey = encryptionKey.toPublicKey();
462
+ const pubKey = counterPartyPublicKey
463
+ ? PublicKey.fromString(counterPartyPublicKey)
464
+ : publicKey;
465
+ return toBase64(bitcoreEncrypt(toArray(stringData), pubKey));
466
+ }
467
+
468
+ /**
469
+ * Decrypt the given ciphertext with the identity encryption key
470
+ * @param ciphertext
471
+ * @param counterPartyPublicKey Optional public key of the counterparty
472
+ */
473
+ decrypt(ciphertext: string, counterPartyPublicKey?: string): string {
474
+ const HDPrivateKey = this.#HDPrivateKey.derive(this.#rootPath);
475
+ const encryptionKey = HDPrivateKey.derive(ENCRYPTION_PATH).privKey;
476
+ // const ecies = new ECIES();
477
+
478
+ // if (counterPartyPublicKey) {
479
+ // return toUTF8(bitcoreDecrypt(toArray(Buffer.from(ciphertext, 'base64'), 'base64'), encryptionKey))
480
+ // }
481
+
482
+ // TODO: It seems the counterPartyPublicKey is not being used here
483
+ return toUTF8(
484
+ bitcoreDecrypt(
485
+ toArray(Buffer.from(ciphertext, "base64"), "base64"),
486
+ encryptionKey,
487
+ ),
488
+ );
489
+ // ecies.privateKey(encryptionKey);
490
+ // if (counterPartyPublicKey) {
491
+ // ecies.publicKey(counterPartyPublicKey);
492
+ // }
493
+ // return ecies.decrypt(Buffer.from(ciphertext, 'base64')).toString();
494
+ }
495
+
496
+ /**
497
+ * Encrypt the given string data with the identity encryption key
498
+ * @param stringData
499
+ * @param seed String seed
500
+ * @param counterPartyPublicKey Optional public key of the counterparty
501
+ * @return string Base64
502
+ */
503
+ encryptWithSeed(
504
+ stringData: string,
505
+ seed: string,
506
+ counterPartyPublicKey?: string,
507
+ ): string {
508
+ const encryptionKey = this.getEncryptionPrivateKeyWithSeed(seed);
509
+ const publicKey = encryptionKey.toPublicKey();
510
+
511
+ // const ecies = new ECIES();
512
+ if (counterPartyPublicKey) {
513
+ // ecies.privateKey(encryptionKey);
514
+ // ecies.publicKey(counterPartyPublicKey);
515
+ return toBase64(
516
+ bitcoreEncrypt(
517
+ toArray(stringData),
518
+ PublicKey.fromString(counterPartyPublicKey),
519
+ ),
520
+ );
521
+ }
522
+ // ecies.publicKey(publicKey);
523
+ return toBase64(bitcoreEncrypt(toArray(stringData), publicKey));
524
+ // return ecies.encrypt(stringData).toString('base64');
525
+ }
526
+
527
+ /**
528
+ * Decrypt the given ciphertext with the identity encryption key
529
+ * @param ciphertext
530
+ * @param seed String seed
531
+ * @param counterPartyPublicKey Public key of the counterparty
532
+ */
533
+ decryptWithSeed(
534
+ ciphertext: string,
535
+ seed: string,
536
+ counterPartyPublicKey?: string,
537
+ ): string {
538
+ const encryptionKey = this.getEncryptionPrivateKeyWithSeed(seed);
539
+ // const ecies = new ECIES();
540
+ // ecies.privateKey(encryptionKey);
541
+ // if (counterPartyPublicKey) {
542
+ // ecies.publicKey(counterPartyPublicKey);
543
+ // TODOL: It seems the counterPartyPublicKey is not being used here
544
+ return toUTF8(
545
+ bitcoreDecrypt(
546
+ toArray(Buffer.from(ciphertext, "base64"), "base64"),
547
+ encryptionKey,
548
+ ),
549
+ );
550
+ // }
551
+
552
+ // return ecies.decrypt(Buffer.from(ciphertext, 'base64')).toString();
553
+ }
554
+
555
+ private getEncryptionPrivateKeyWithSeed(seed: string) {
556
+ const pathHex = toHex(Hash.sha256(seed, "utf8"));
557
+ const path = Utils.getSigningPathFromHex(pathHex);
558
+
559
+ const HDPrivateKey = this.#HDPrivateKey.derive(this.#rootPath);
560
+ return HDPrivateKey.derive(path).privKey;
561
+ }
562
+
563
+ /**
564
+ * Get an attestation string for the given urn for this identity
565
+ *
566
+ * @param urn
567
+ * @returns {string}
568
+ */
569
+ getAttestation(urn: string) {
570
+ const urnHash = Hash.sha256(urn, "utf8");
571
+ return `bap:attest:${toHex(urnHash)}:${this.getIdentityKey()}`;
572
+ }
573
+
574
+ /**
575
+ * Generate and return the attestation hash for the given attribute of this identity
576
+ *
577
+ * @param attribute Attribute name (name, email etc.)
578
+ * @returns {string}
579
+ */
580
+ getAttestationHash(attribute: string) {
581
+ const urn = this.getAttributeUrn(attribute);
582
+ if (!urn) return null;
583
+
584
+ const attestation = this.getAttestation(urn);
585
+ const attestationHash = Hash.sha256(attestation, "utf8");
586
+
587
+ return toHex(attestationHash);
588
+ }
589
+
590
+ /**
591
+ * Sign a message with the current signing address of this identity
592
+ *
593
+ * @param message
594
+ * @param signingPath
595
+ * @returns {{address, signature}}
596
+ */
597
+ signMessage(message: string | Buffer, signingPath = "") {
598
+ let msg: Buffer;
599
+ if (!(message instanceof Buffer)) {
600
+ msg = Buffer.from(message);
601
+ } else {
602
+ msg = message;
603
+ }
604
+
605
+ signingPath = signingPath || this.#currentPath;
606
+ const childPk = this.#HDPrivateKey.derive(signingPath).privKey;
607
+ const address = childPk.toAddress();
608
+ const signature = BSM.sign(toArray(msg), childPk).toCompact(
609
+ 0,
610
+ true,
611
+ "base64",
612
+ ) as string;
613
+
614
+ return { address, signature };
615
+ }
616
+
617
+ /**
618
+ * Sign a message using a key based on the given string seed
619
+ *
620
+ * This works by creating a private key from the root key of this identity. It will always
621
+ * work with the rootPath / rootKey, to be deterministic. It will not change even if the keys
622
+ * are rotated for this ID.
623
+ *
624
+ * This is used in for instance deterministic login systems, that do not support BAP.
625
+ *
626
+ * @param message
627
+ * @param seed {string} String seed that will be used to generate a path
628
+ */
629
+ signMessageWithSeed(
630
+ message: string,
631
+ seed: string,
632
+ ): { address: string; signature: string } {
633
+ const pathHex = toHex(Hash.sha256(seed, "utf8"));
634
+ const path = Utils.getSigningPathFromHex(pathHex);
635
+
636
+ const HDPrivateKey = this.#HDPrivateKey.derive(this.#rootPath);
637
+ const derivedChild = HDPrivateKey.derive(path);
638
+ const address = derivedChild.privKey.toPublicKey().toAddress();
639
+ const signature = BSM.sign(
640
+ toArray(Buffer.from(message)),
641
+ derivedChild.privKey,
642
+ ).toCompact(0, true, "base64") as string;
643
+
644
+ return { address, signature };
645
+ }
646
+
647
+ /**
648
+ * Sign an op_return hex array with AIP
649
+ * @param opReturn {array}
650
+ * @param signingPath {string}
651
+ * @param outputType {string}
652
+ * @return {[]}
653
+ */
654
+ signOpReturnWithAIP(
655
+ opReturn: string[],
656
+ signingPath = "",
657
+ outputType: BufferEncoding = "hex",
658
+ ): string[] {
659
+ const aipMessageBuffer = this.getAIPMessageBuffer(opReturn);
660
+ const { address, signature } = this.signMessage(
661
+ aipMessageBuffer,
662
+ signingPath,
663
+ );
664
+
665
+ return opReturn.concat([
666
+ Buffer.from("|").toString(outputType),
667
+ Buffer.from(AIP_BITCOM_ADDRESS).toString(outputType),
668
+ Buffer.from("BITCOIN_ECDSA").toString(outputType),
669
+ Buffer.from(address).toString(outputType),
670
+ Buffer.from(signature, "base64").toString(outputType),
671
+ ]);
672
+ }
673
+
674
+ /**
675
+ * Construct an AIP buffer from the op return data
676
+ * @param opReturn
677
+ * @returns {Buffer}
678
+ */
679
+ getAIPMessageBuffer(opReturn: string[]): Buffer {
680
+ const buffers = [];
681
+ if (opReturn[0].replace("0x", "") !== "6a") {
682
+ // include OP_RETURN in constructing the signature buffer
683
+ buffers.push(Buffer.from("6a", "hex"));
684
+ }
685
+ for (const op of opReturn) {
686
+ buffers.push(Buffer.from(op.replace("0x", ""), "hex"));
687
+ }
688
+ // add a trailing "|" - this is the AIP way
689
+ buffers.push(Buffer.from("|"));
690
+
691
+ return Buffer.concat([...buffers]);
692
+ }
693
+
694
+ /**
695
+ * Get all signing keys for this identity
696
+ */
697
+ async getIdSigningKeys(): Promise<any> {
698
+ const signingKeys = await this.getApiData("/signing-keys", {
699
+ idKey: this.identityKey,
700
+ });
701
+ console.log("getIdSigningKeys", signingKeys);
702
+
703
+ return signingKeys;
704
+ }
705
+
706
+ /**
707
+ * Get all attestations for the given attribute
708
+ *
709
+ * @param attribute
710
+ */
711
+ async getAttributeAttestations(attribute: string): Promise<any> {
712
+ // This function needs to make a call to a BAP server to get all the attestations for this
713
+ // identity for the given attribute
714
+ const attestationHash = this.getAttestationHash(attribute);
715
+
716
+ // get all BAP ATTEST records for the given attestationHash
717
+ const attestations = await this.getApiData("/attestations", {
718
+ hash: attestationHash,
719
+ });
720
+ console.log("getAttestations", attribute, attestationHash, attestations);
721
+
722
+ return attestations;
723
+ }
724
+
725
+ /**
726
+ * Helper function to get attestation from a BAP API server
727
+ *
728
+ * @param apiUrl
729
+ * @param apiData
730
+ * @returns {Promise<any>}
731
+ */
732
+ async getApiData(apiUrl: string, apiData: any): Promise<any> {
733
+ const url = `${this.#BAP_SERVER}${apiUrl}`;
734
+ const response = await fetch(url, {
735
+ method: "post",
736
+ headers: {
737
+ "Content-type": "application/json; charset=utf-8",
738
+ token: this.#BAP_TOKEN,
739
+ format: "json",
740
+ },
741
+ body: JSON.stringify(apiData),
742
+ });
743
+ return response.json();
744
+ }
745
+
746
+ /**
747
+ * Import an identity from a JSON object
748
+ *
749
+ * @param identity{{}}
750
+ */
751
+ import(identity: Identity): void {
752
+ this.idName = identity.name;
753
+ this.description = identity.description || "";
754
+ this.identityKey = identity.identityKey;
755
+ this.#rootPath = identity.rootPath;
756
+ this.rootAddress = identity.rootAddress;
757
+ this.#previousPath = identity.previousPath;
758
+ this.#currentPath = identity.currentPath;
759
+ this.#idSeed = identity.idSeed || "";
760
+ this.identityAttributes = this.parseAttributes(identity.identityAttributes);
761
+ }
762
+
763
+ /**
764
+ * Export this identity to a JSON object
765
+ * @returns {{}}
766
+ */
767
+ export(): Identity {
768
+ return {
769
+ name: this.idName,
770
+ description: this.description,
771
+ identityKey: this.identityKey,
772
+ rootPath: this.#rootPath,
773
+ rootAddress: this.rootAddress,
774
+ previousPath: this.#previousPath,
775
+ currentPath: this.#currentPath,
776
+ idSeed: this.#idSeed,
777
+ identityAttributes: this.getAttributes(),
778
+ lastIdPath: "",
779
+ };
780
+ }
781
+ }
782
+
783
+ export { BAP_ID };