@keetanetwork/anchor 0.0.13 → 0.0.15

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 (87) hide show
  1. package/lib/certificates.d.ts +83 -21
  2. package/lib/certificates.d.ts.map +1 -1
  3. package/lib/certificates.generated.d.ts +3 -0
  4. package/lib/certificates.generated.d.ts.map +1 -0
  5. package/lib/certificates.generated.js +120 -0
  6. package/lib/certificates.generated.js.map +1 -0
  7. package/lib/certificates.js +508 -117
  8. package/lib/certificates.js.map +1 -1
  9. package/lib/http-server.d.ts +2 -1
  10. package/lib/http-server.d.ts.map +1 -1
  11. package/lib/http-server.js +5 -5
  12. package/lib/http-server.js.map +1 -1
  13. package/lib/log/index.d.ts +6 -57
  14. package/lib/log/index.d.ts.map +1 -1
  15. package/lib/log/index.js +5 -205
  16. package/lib/log/index.js.map +1 -1
  17. package/lib/log/target_console.d.ts +3 -11
  18. package/lib/log/target_console.d.ts.map +1 -1
  19. package/lib/log/target_console.js +3 -42
  20. package/lib/log/target_console.js.map +1 -1
  21. package/lib/resolver.d.ts +2 -1
  22. package/lib/resolver.d.ts.map +1 -1
  23. package/lib/resolver.js +22 -22
  24. package/lib/resolver.js.map +1 -1
  25. package/lib/utils/asn1.d.ts +10 -1
  26. package/lib/utils/asn1.d.ts.map +1 -1
  27. package/lib/utils/asn1.js +1413 -0
  28. package/lib/utils/asn1.js.map +1 -1
  29. package/lib/utils/buffer.d.ts +3 -0
  30. package/lib/utils/buffer.d.ts.map +1 -1
  31. package/lib/utils/buffer.js +32 -0
  32. package/lib/utils/buffer.js.map +1 -1
  33. package/lib/utils/external.d.ts +43 -0
  34. package/lib/utils/external.d.ts.map +1 -0
  35. package/lib/utils/external.js +115 -0
  36. package/lib/utils/external.js.map +1 -0
  37. package/lib/utils/guards.d.ts +14 -0
  38. package/lib/utils/guards.d.ts.map +1 -0
  39. package/lib/utils/guards.js +31 -0
  40. package/lib/utils/guards.js.map +1 -0
  41. package/lib/utils/index.d.ts +3 -1
  42. package/lib/utils/index.d.ts.map +1 -1
  43. package/lib/utils/index.js +3 -1
  44. package/lib/utils/index.js.map +1 -1
  45. package/lib/utils/oid.d.ts +7 -0
  46. package/lib/utils/oid.d.ts.map +1 -0
  47. package/lib/utils/oid.js +22 -0
  48. package/lib/utils/oid.js.map +1 -0
  49. package/lib/utils/signing.d.ts.map +1 -1
  50. package/lib/utils/signing.js +26 -2
  51. package/lib/utils/signing.js.map +1 -1
  52. package/npm-shrinkwrap.json +76 -216
  53. package/package.json +2 -2
  54. package/services/asset-movement/common.js +7 -7
  55. package/services/fx/server.js +1 -1
  56. package/services/fx/server.js.map +1 -1
  57. package/services/kyc/client.d.ts.map +1 -1
  58. package/services/kyc/client.js +1 -8
  59. package/services/kyc/client.js.map +1 -1
  60. package/services/kyc/common.d.ts +39 -3
  61. package/services/kyc/common.d.ts.map +1 -1
  62. package/services/kyc/common.generated.d.ts +5 -0
  63. package/services/kyc/common.generated.d.ts.map +1 -0
  64. package/services/kyc/common.generated.js +241 -0
  65. package/services/kyc/common.generated.js.map +1 -0
  66. package/services/kyc/common.js +68 -1
  67. package/services/kyc/common.js.map +1 -1
  68. package/services/kyc/iso20022.generated.d.ts +336 -0
  69. package/services/kyc/iso20022.generated.d.ts.map +1 -0
  70. package/services/kyc/iso20022.generated.js +287 -0
  71. package/services/kyc/iso20022.generated.js.map +1 -0
  72. package/services/kyc/oids.generated.d.ts +104 -0
  73. package/services/kyc/oids.generated.d.ts.map +1 -0
  74. package/services/kyc/oids.generated.js +163 -0
  75. package/services/kyc/oids.generated.js.map +1 -0
  76. package/services/kyc/server.d.ts +141 -0
  77. package/services/kyc/server.d.ts.map +1 -0
  78. package/services/kyc/server.js +183 -0
  79. package/services/kyc/server.js.map +1 -0
  80. package/services/kyc/utils/generate-kyc-schema.d.ts +3 -0
  81. package/services/kyc/utils/generate-kyc-schema.d.ts.map +1 -0
  82. package/services/kyc/utils/generate-kyc-schema.js +1342 -0
  83. package/services/kyc/utils/generate-kyc-schema.js.map +1 -0
  84. package/lib/log/common.d.ts +0 -35
  85. package/lib/log/common.d.ts.map +0 -1
  86. package/lib/log/common.js +0 -19
  87. package/lib/log/common.js.map +0 -1
@@ -1,69 +1,36 @@
1
1
  import * as KeetaNetClient from '@keetanetwork/keetanet-client';
2
+ import * as oids from '../services/kyc/oids.generated.js';
2
3
  import * as ASN1 from './utils/asn1.js';
3
- import { Buffer } from './utils/buffer.js';
4
+ import { ASN1toJS, contextualizeStructSchema, encodeValueBySchema, normalizeDecodedASN1 } from './utils/asn1.js';
5
+ import { arrayBufferLikeToBuffer, arrayBufferToBuffer, Buffer, bufferToArrayBuffer } from './utils/buffer.js';
4
6
  import crypto from './utils/crypto.js';
5
7
  import { assertNever } from './utils/never.js';
6
- /* -----MOVE TO NODE AND ASN1NAPIRS----- */
7
- function getOID(name, oidDB) {
8
- if (name in oidDB) {
9
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
10
- const oid = oidDB[name];
11
- if (oid === undefined) {
12
- throw (new Error('internal error: OID was undefined'));
13
- }
14
- return (oid);
15
- }
16
- else {
17
- throw (new Error('Unknown algorithm'));
18
- }
8
+ import { CertificateAttributeOIDDB, CertificateAttributeSchema, ReferenceSchema } from '../services/kyc/iso20022.generated.js';
9
+ import { getOID, lookupByOID } from './utils/oid.js';
10
+ import { convertToJSON as convertToJSONUtil } from './utils/json.js';
11
+ import { EncryptedContainer } from './encrypted-container.js';
12
+ import { assertSharableCertificateAttributesContentsSchema } from './certificates.generated.js';
13
+ /**
14
+ * Short alias for printing a debug representation of an object
15
+ */
16
+ const DPO = KeetaNetClient.lib.Utils.Helper.debugPrintableObject.bind(KeetaNetClient.lib.Utils.Helper);
17
+ const KeetaNetAccount = KeetaNetClient.lib.Account;
18
+ function toJSON(data) {
19
+ return (convertToJSONUtil(data));
19
20
  }
20
- function lookupByOID(oid, oidDB) {
21
- for (const [key, value] of Object.entries(oidDB)) {
22
- if (key === oid) {
23
- return (key);
24
- }
25
- if (value === oid) {
26
- return (key);
27
- }
28
- }
29
- throw (new Error(`Unknown OID: ${oid}`));
21
+ // Generic type guard to align decoded values with generated attribute types
22
+ function isAttributeValue(_name, _v) {
23
+ // Runtime schema validation is already performed by BufferStorageASN1; this guard
24
+ // serves to inform TypeScript of the precise type tied to the attribute name.
25
+ return (true);
30
26
  }
31
- /* -----END MOVE TO NODE AND ASN1NAPIRS----- */
32
- function toJSON(data) {
33
- const retval = JSON.parse(JSON.stringify(data, function (key, convertedValue) {
34
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
35
- const value = this[key];
36
- if (typeof value === 'object' && value !== null) {
37
- if ('publicKeyString' in value && typeof value.publicKeyString === 'object' && value.publicKeyString !== null) {
38
- if ('get' in value.publicKeyString && typeof value.publicKeyString.get === 'function') {
39
- /*
40
- * If the value has a publicKeyString property that is an
41
- * object with a get method, we assume it is a KeetaNetAccount
42
- * or similar object and we return the public key string
43
- */
44
- // eslint-disable-next-line @typescript-eslint/no-unsafe-call
45
- const publicKeyString = value.publicKeyString.get();
46
- if (typeof publicKeyString === 'string') {
47
- return (publicKeyString);
48
- }
49
- }
50
- }
51
- }
52
- if (Buffer.isBuffer(value)) {
53
- return (value.toString('base64'));
54
- }
55
- if (typeof value === 'bigint') {
56
- return (value.toString());
57
- }
58
- return (convertedValue);
59
- }));
60
- return (retval);
27
+ // Helper to apply type guard once and return the properly typed value
28
+ function asAttributeValue(name, v) {
29
+ if (!isAttributeValue(name, v)) {
30
+ throw (new Error('internal error: decoded value did not match expected type'));
31
+ }
32
+ return (v);
61
33
  }
62
- /*
63
- * Because our public interfaces are ArrayBuffers we often need to convert
64
- * Buffers to ArrayBuffers -- an alias to the Node function to do that
65
- */
66
- const bufferToArrayBuffer = KeetaNetClient.lib.Utils.Helper.bufferToArrayBuffer.bind(KeetaNetClient.lib.Utils.Helper);
67
34
  /**
68
35
  * Sensitive Attribute Schema
69
36
  *
@@ -107,30 +74,88 @@ const SensitiveAttributeSchemaInternal = [
107
74
  * Database of permitted algorithms and their OIDs
108
75
  */
109
76
  const sensitiveAttributeOIDDB = {
110
- 'aes-256-gcm': '2.16.840.1.101.3.4.1.46',
111
- 'aes-256-cbc': '2.16.840.1.101.3.4.1.42',
112
- 'sha2-256': '2.16.840.1.101.3.4.2.1',
113
- 'sha3-256': '2.16.840.1.101.3.4.2.8'
77
+ 'aes-256-gcm': oids.AES_256_GCM,
78
+ 'aes-256-cbc': oids.AES_256_CBC,
79
+ 'sha2-256': oids.SHA2_256,
80
+ 'sha3-256': oids.SHA3_256,
81
+ 'sha256': oids.SHA2_256,
82
+ 'aes256-gcm': oids.AES_256_GCM,
83
+ 'aes256-cbc': oids.AES_256_CBC
114
84
  };
85
+ function assertCertificateAttributeNames(name) {
86
+ if (!(name in CertificateAttributeOIDDB)) {
87
+ throw (new Error(`Unknown attribute name: ${name}`));
88
+ }
89
+ }
90
+ function asCertificateAttributeNames(name) {
91
+ assertCertificateAttributeNames(name);
92
+ return (name);
93
+ }
94
+ const DOCUMENT_SCHEMA_ATTRIBUTES = new Set([
95
+ 'document',
96
+ 'documentDriversLicenseFront',
97
+ 'documentDriversLicenseBack',
98
+ 'documentPassport'
99
+ ]);
100
+ function resolveSchema(name, schema) {
101
+ if (DOCUMENT_SCHEMA_ATTRIBUTES.has(name)) {
102
+ return (contextualizeStructSchema(ReferenceSchema));
103
+ }
104
+ return (contextualizeStructSchema(schema));
105
+ }
106
+ function encodeAttribute(name, value) {
107
+ const schema = resolveSchema(name, CertificateAttributeSchema[name]);
108
+ const encodedJS = encodeValueBySchema(schema, value, { attributeName: name });
109
+ if (encodedJS === undefined) {
110
+ throw (new Error(`Unsupported attribute value for encoding: ${JSON.stringify(DPO(value))}`));
111
+ }
112
+ const asn1Object = ASN1.JStoASN1(encodedJS);
113
+ if (!asn1Object) {
114
+ throw (new Error(`Failed to encode value for attribute ${name}`));
115
+ }
116
+ return (asn1Object.toBER(false));
117
+ }
118
+ // Prepare a value for inclusion in a SensitiveAttribute: pre-encode complex and date types
119
+ function encodeForSensitive(name, value) {
120
+ if (Buffer.isBuffer(value)) {
121
+ return (value);
122
+ }
123
+ if (value instanceof ArrayBuffer) {
124
+ return (arrayBufferToBuffer(value));
125
+ }
126
+ if (typeof value === 'string') {
127
+ const asn1 = ASN1.JStoASN1({ type: 'string', kind: 'utf8', value });
128
+ return (arrayBufferToBuffer(asn1.toBER(false)));
129
+ }
130
+ if (value instanceof Date) {
131
+ const asn1 = ASN1.JStoASN1(value);
132
+ return (arrayBufferToBuffer(asn1.toBER(false)));
133
+ }
134
+ if (typeof value === 'object' && value !== null) {
135
+ if (!name) {
136
+ throw (new Error('attributeName required for complex types'));
137
+ }
138
+ const encoded = encodeAttribute(name, value);
139
+ return (arrayBufferToBuffer(encoded));
140
+ }
141
+ return (Buffer.from(String(value), 'utf-8'));
142
+ }
143
+ async function decodeAttribute(name, value) {
144
+ const schema = resolveSchema(name, CertificateAttributeSchema[name]);
145
+ // XXX:TODO Fix depth issue
146
+ // @ts-ignore
147
+ const decodedUnknown = new ASN1.BufferStorageASN1(value, schema).getASN1();
148
+ const candidate = normalizeDecodedASN1(decodedUnknown);
149
+ return (asAttributeValue(name, candidate));
150
+ }
115
151
  class SensitiveAttributeBuilder {
116
152
  #account;
117
153
  #value;
118
- constructor(account, value) {
154
+ constructor(account) {
119
155
  this.#account = account;
120
- if (value) {
121
- this.set(value);
122
- }
123
156
  }
124
157
  set(value) {
125
- if (Buffer.isBuffer(value)) {
126
- this.#value = value;
127
- }
128
- else if (typeof value === 'string') {
129
- this.#value = Buffer.from(value, 'utf-8');
130
- }
131
- else {
132
- this.#value = Buffer.from(value);
133
- }
158
+ this.#value = Buffer.isBuffer(value) ? value : arrayBufferLikeToBuffer(value);
134
159
  return (this);
135
160
  }
136
161
  async build() {
@@ -143,15 +168,31 @@ class SensitiveAttributeBuilder {
143
168
  const cipher = 'aes-256-gcm';
144
169
  const key = crypto.randomBytes(32);
145
170
  const nonce = crypto.randomBytes(12);
146
- const encryptedKey = await this.#account.encrypt(key);
171
+ const encryptedKey = await this.#account.encrypt(bufferToArrayBuffer(key));
147
172
  function encrypt(value) {
148
173
  const cipherObject = crypto.createCipheriv(cipher, key, nonce);
149
174
  let retval = cipherObject.update(value);
150
175
  retval = Buffer.concat([retval, cipherObject.final()]);
176
+ /*
177
+ * For AES-GCM, the last 16 bytes are the authentication tag
178
+ */
179
+ if (cipher === 'aes-256-gcm') {
180
+ const getAuthTagFn = Reflect.get(cipherObject, 'getAuthTag');
181
+ if (typeof getAuthTagFn === 'function') {
182
+ const tag = getAuthTagFn.call(cipherObject);
183
+ if (!Buffer.isBuffer(tag)) {
184
+ throw (new Error('getAuthTag did not return a Buffer'));
185
+ }
186
+ retval = Buffer.concat([retval, tag]);
187
+ }
188
+ else {
189
+ throw (new Error('getAuthTag is not available on cipherObject'));
190
+ }
191
+ }
151
192
  return (retval);
152
193
  }
153
194
  const encryptedValue = encrypt(this.#value);
154
- const encryptedSalt = encrypt(salt);
195
+ const encryptedSalt = encrypt(arrayBufferLikeToBuffer(salt));
155
196
  const saltedValue = Buffer.concat([salt, publicKey, encryptedValue, this.#value]);
156
197
  const hashedAndSaltedValue = KeetaNetClient.lib.Utils.Hash.Hash(saltedValue);
157
198
  const attributeStructure = [
@@ -179,23 +220,35 @@ class SensitiveAttributeBuilder {
179
220
  encryptedValue
180
221
  ];
181
222
  const encodedAttributeObject = ASN1.JStoASN1(attributeStructure);
182
- const retval = encodedAttributeObject.toBER();
223
+ // Produce canonical DER as ArrayBuffer
224
+ const retval = encodedAttributeObject.toBER(false);
183
225
  return (retval);
184
226
  }
185
227
  }
186
228
  class SensitiveAttribute {
187
229
  #account;
188
230
  #info;
189
- constructor(account, data) {
231
+ #decoder;
232
+ constructor(account, data, decoder) {
190
233
  this.#account = account;
191
234
  this.#info = this.decode(data);
235
+ if (decoder) {
236
+ this.#decoder = decoder;
237
+ }
192
238
  }
193
239
  decode(data) {
194
240
  if (Buffer.isBuffer(data)) {
195
241
  data = bufferToArrayBuffer(data);
196
242
  }
197
- const dataObject = new ASN1.BufferStorageASN1(data, SensitiveAttributeSchemaInternal);
198
- const decodedAttribute = dataObject.getASN1();
243
+ let decodedAttribute;
244
+ try {
245
+ const dataObject = new ASN1.BufferStorageASN1(data, SensitiveAttributeSchemaInternal);
246
+ decodedAttribute = dataObject.getASN1();
247
+ }
248
+ catch {
249
+ const js = ASN1toJS(data);
250
+ throw (new Error(`SensitiveAttribute.decode: unexpected DER shape ${JSON.stringify(DPO(js))}`));
251
+ }
199
252
  const decodedVersion = decodedAttribute[0] + 1n;
200
253
  if (decodedVersion !== 1n) {
201
254
  throw (new Error(`Unsupported Sensitive Attribute version (${decodedVersion})`));
@@ -217,11 +270,31 @@ class SensitiveAttribute {
217
270
  });
218
271
  }
219
272
  async #decryptValue(value) {
220
- const decryptedKey = await this.#account.decrypt(this.#info.cipher.key);
273
+ const decryptedKey = await this.#account.decrypt(bufferToArrayBuffer(this.#info.cipher.key));
221
274
  const algorithm = this.#info.cipher.algorithm;
222
275
  const iv = this.#info.cipher.iv;
223
276
  const cipher = crypto.createDecipheriv(algorithm, Buffer.from(decryptedKey), iv);
277
+ // For AES-GCM, the last 16 bytes are the authentication tag
278
+ if (algorithm === 'aes-256-gcm') {
279
+ const authTag = value.subarray(value.length - 16);
280
+ const ciphertext = value.subarray(0, value.length - 16);
281
+ // XXX:TODO Fix typescript unsafe calls
282
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
283
+ const setAuthTagFn = Reflect.get(cipher, 'setAuthTag');
284
+ if (typeof setAuthTagFn === 'function') {
285
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
286
+ setAuthTagFn.call(cipher, authTag);
287
+ }
288
+ else {
289
+ throw (new Error('setAuthTag is not available on cipher'));
290
+ }
291
+ const decrypted = cipher.update(ciphertext);
292
+ cipher.final(); // Verify auth tag
293
+ return (decrypted);
294
+ }
295
+ // For other algorithms (like CBC), just decrypt normally
224
296
  const decryptedValue = cipher.update(value);
297
+ cipher.final();
225
298
  return (decryptedValue);
226
299
  }
227
300
  /**
@@ -234,25 +307,33 @@ class SensitiveAttribute {
234
307
  * ArrayBuffer
235
308
  */
236
309
  async get() {
237
- const decryptedValue = await this.#decryptValue(this.#info.encryptedValue);
310
+ const decryptedValue = await this.#decryptValue(arrayBufferLikeToBuffer(this.#info.encryptedValue));
238
311
  return (bufferToArrayBuffer(decryptedValue));
239
312
  }
240
- /**
241
- * Get the value of the sensitive attribute as a string after being
242
- * interpreted as UTF-8 ( @see SensitiveAttribute.get for more information)
243
- */
244
- async getString() {
313
+ async getValue() {
245
314
  const value = await this.get();
246
- return (Buffer.from(value).toString('utf-8'));
315
+ if (!this.#decoder) {
316
+ /**
317
+ * TypeScript complains that T may not be the correct
318
+ * type here, but gives us no tools to enforce that it
319
+ * is -- it should always be ArrayBuffer if no decoder
320
+ * is provided, but someone could always specify a
321
+ * type parameter in that case and we cannot check
322
+ * that at runtime since T is only a compile-time type.
323
+ */
324
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
325
+ return value;
326
+ }
327
+ return (this.#decoder(value));
247
328
  }
248
329
  /**
249
330
  * Generate a proof that a sensitive attribute is a given value,
250
331
  * which can be validated by a third party using the certificate
251
332
  * and the `validateProof` method
252
333
  */
253
- async proove() {
334
+ async getProof() {
254
335
  const value = await this.get();
255
- const salt = await this.#decryptValue(this.#info.hashedValue.encryptedSalt);
336
+ const salt = await this.#decryptValue(arrayBufferLikeToBuffer(this.#info.hashedValue.encryptedSalt));
256
337
  return ({
257
338
  value: Buffer.from(value).toString('base64'),
258
339
  hash: {
@@ -268,7 +349,8 @@ class SensitiveAttribute {
268
349
  const proofSaltBuffer = Buffer.from(proof.hash.salt, 'base64');
269
350
  const publicKeyBuffer = Buffer.from(this.#account.publicKey.get());
270
351
  const encryptedValue = this.#info.encryptedValue;
271
- const hashedAndSaltedValue = KeetaNetClient.lib.Utils.Hash.Hash(Buffer.concat([proofSaltBuffer, publicKeyBuffer, encryptedValue, plaintextValue]));
352
+ const hashInput = Buffer.concat([proofSaltBuffer, publicKeyBuffer, encryptedValue, plaintextValue]);
353
+ const hashedAndSaltedValue = KeetaNetClient.lib.Utils.Hash.Hash(hashInput);
272
354
  const hashedAndSaltedValueBuffer = Buffer.from(hashedAndSaltedValue);
273
355
  return (this.#info.hashedValue.value.equals(hashedAndSaltedValueBuffer));
274
356
  }
@@ -276,16 +358,6 @@ class SensitiveAttribute {
276
358
  return (toJSON(this.#info));
277
359
  }
278
360
  }
279
- /**
280
- * Database of attributes
281
- */
282
- const CertificateAttributeOIDDB = {
283
- 'fullName': '1.3.6.1.4.1.62675.1.0',
284
- 'dateOfBirth': '1.3.6.1.4.1.62675.1.1',
285
- 'address': '1.3.6.1.4.1.62675.1.2',
286
- 'email': '1.3.6.1.4.1.62675.1.3',
287
- 'phoneNumber': '1.3.6.1.4.1.62675.1.4'
288
- };
289
361
  /**
290
362
  * ASN.1 Schema for Certificate KYC Attributes Extension
291
363
  *
@@ -338,10 +410,40 @@ export class CertificateBuilder extends KeetaNetClient.lib.Utils.Certificate.Cer
338
410
  super(CertificateBuilder.mapParams(params));
339
411
  }
340
412
  /**
341
- * Set a KYC Attribute to a given value
413
+ * Set a KYC Attribute to a given value.
414
+ * The sensitive flag is required.
415
+ *
416
+ * If an attribute is marked sensitive, the value is encoded
417
+ * into the certificate using a commitment scheme so that the
418
+ * value can be proven later without revealing it.
342
419
  */
343
420
  setAttribute(name, sensitive, value) {
344
- this.#attributes[name] = { sensitive, value };
421
+ // Non-sensitive path: only primitive schema (string/date) allowed
422
+ const schemaValidator = CertificateAttributeSchema[name];
423
+ let encoded;
424
+ if (value instanceof ArrayBuffer) {
425
+ encoded = value;
426
+ }
427
+ else if (name in CertificateAttributeSchema) {
428
+ /* XXX: Why do we have two encoding methods ? */
429
+ encoded = bufferToArrayBuffer(encodeForSensitive(name, value));
430
+ }
431
+ else if (schemaValidator === ASN1.ValidateASN1.IsDate) {
432
+ if (!(value instanceof Date)) {
433
+ throw (new Error('Expected Date value'));
434
+ }
435
+ encoded = encodeAttribute(name, value);
436
+ }
437
+ else if (schemaValidator === ASN1.ValidateASN1.IsString && typeof value === 'string') {
438
+ encoded = encodeAttribute(name, value);
439
+ }
440
+ else {
441
+ throw (new Error('Unsupported non-sensitive value type'));
442
+ }
443
+ this.#attributes[name] = {
444
+ sensitive: sensitive,
445
+ value: encoded
446
+ };
345
447
  }
346
448
  async addExtensions(...args) {
347
449
  const retval = await super.addExtensions(...args);
@@ -357,19 +459,20 @@ export class CertificateBuilder extends KeetaNetClient.lib.Utils.Certificate.Cer
357
459
  * can assume that the attribute is always present in
358
460
  * the object
359
461
  */
360
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
462
+ assertCertificateAttributeNames(name);
361
463
  const nameOID = CertificateAttributeOIDDB[name];
362
464
  let value;
363
465
  if (attribute.sensitive) {
364
- const sensitiveAttribute = new SensitiveAttributeBuilder(subject, attribute.value);
365
- value = Buffer.from(await sensitiveAttribute.build());
466
+ const builder = new SensitiveAttributeBuilder(subject);
467
+ builder.set(attribute.value);
468
+ value = arrayBufferToBuffer(await builder.build());
366
469
  }
367
470
  else {
368
471
  if (typeof attribute.value === 'string') {
369
472
  value = Buffer.from(attribute.value, 'utf-8');
370
473
  }
371
474
  else {
372
- value = Buffer.from(attribute.value);
475
+ value = arrayBufferToBuffer(attribute.value);
373
476
  }
374
477
  }
375
478
  certAttributes.push([{
@@ -383,7 +486,7 @@ export class CertificateBuilder extends KeetaNetClient.lib.Utils.Certificate.Cer
383
486
  }]);
384
487
  }
385
488
  if (certAttributes.length > 0) {
386
- retval.push(KeetaNetClient.lib.Utils.Certificate.CertificateBuilder.extension('1.3.6.1.4.1.62675.0.0', certAttributes));
489
+ retval.push(KeetaNetClient.lib.Utils.Certificate.CertificateBuilder.extension(oids.keeta.KYC_ATTRIBUTES, certAttributes));
387
490
  }
388
491
  return (retval);
389
492
  }
@@ -412,6 +515,7 @@ export class CertificateBuilder extends KeetaNetClient.lib.Utils.Certificate.Cer
412
515
  export class Certificate extends KeetaNetClient.lib.Utils.Certificate.Certificate {
413
516
  subjectKey;
414
517
  static Builder = CertificateBuilder;
518
+ static SharableAttributes;
415
519
  /**
416
520
  * User KYC Attributes
417
521
  */
@@ -424,27 +528,60 @@ export class Certificate extends KeetaNetClient.lib.Utils.Certificate.Certificat
424
528
  finalizeConstruction() {
425
529
  /* Do nothing, we call the super method in the constructor */
426
530
  }
531
+ setPlainAttribute(name, value) {
532
+ // @ts-ignore
533
+ this.attributes[name] = { sensitive: false, value };
534
+ }
535
+ setSensitiveAttribute(name, value) {
536
+ const decodeForSensitive = async (data) => {
537
+ const bufferInput = Buffer.isBuffer(data) ? bufferToArrayBuffer(data) : data;
538
+ return (await decodeAttribute(name, bufferInput));
539
+ };
540
+ this.attributes[name] = {
541
+ sensitive: true,
542
+ value: new SensitiveAttribute(this.subjectKey, value, decodeForSensitive)
543
+ };
544
+ }
545
+ /**
546
+ * Get the underlying value for an attribute.
547
+ *
548
+ * If the attribute is sensitive, this will decrypt it using the
549
+ * subject's private key, otherwise it will return the value.
550
+ */
551
+ async getAttributeValue(attributeName) {
552
+ const attr = this.attributes[attributeName]?.value;
553
+ if (!attr) {
554
+ throw (new Error(`Attribute ${attributeName} is not available`));
555
+ }
556
+ if (attr instanceof SensitiveAttribute) {
557
+ const raw = await attr.get();
558
+ return (await decodeAttribute(attributeName, raw));
559
+ }
560
+ // Non-sensitive: ArrayBuffer or Buffer
561
+ if (attr instanceof ArrayBuffer || Buffer.isBuffer(attr)) {
562
+ return (await decodeAttribute(attributeName, attr));
563
+ }
564
+ throw (new Error(`Attribute ${attributeName} is not a supported type`));
565
+ }
427
566
  processExtension(id, value) {
428
567
  if (super.processExtension(id, value)) {
429
568
  return (true);
430
569
  }
431
- if (id === '1.3.6.1.4.1.62675.0.0') {
570
+ if (id === oids.keeta.KYC_ATTRIBUTES) {
432
571
  const attributesRaw = new ASN1.BufferStorageASN1(value, CertificateKYCAttributeSchemaValidation).getASN1();
433
572
  for (const attribute of attributesRaw) {
434
- const name = lookupByOID(attribute[0].oid, CertificateAttributeOIDDB);
573
+ const nameString = lookupByOID(attribute[0].oid, CertificateAttributeOIDDB);
574
+ const name = asCertificateAttributeNames(nameString);
435
575
  const valueKind = attribute[1].value;
436
576
  const value = bufferToArrayBuffer(attribute[1].contains);
437
577
  switch (valueKind) {
438
578
  case 0:
439
579
  /* Plain Value */
440
- this.attributes[name] = { sensitive: false, value: value };
580
+ this.setPlainAttribute(name, value);
441
581
  break;
442
582
  case 1:
443
583
  /* Sensitive Value */
444
- this.attributes[name] = {
445
- sensitive: true,
446
- value: new SensitiveAttribute(this.subjectKey, value)
447
- };
584
+ this.setSensitiveAttribute(name, value);
448
585
  break;
449
586
  default:
450
587
  assertNever(valueKind);
@@ -455,6 +592,260 @@ export class Certificate extends KeetaNetClient.lib.Utils.Certificate.Certificat
455
592
  return (false);
456
593
  }
457
594
  }
595
+ ;
596
+ export class SharableCertificateAttributes {
597
+ #certificate;
598
+ #attributes = {};
599
+ container;
600
+ populatedFromInit = false;
601
+ static assertCertificateAttributeName = assertCertificateAttributeNames;
602
+ constructor(input, options) {
603
+ let containerBuffer;
604
+ if (typeof input === 'string') {
605
+ /*
606
+ * Attempt to decode as PEM, but also if not PEM, then return
607
+ * the lines as-is (base64) after removing whitespace
608
+ */
609
+ const inputLines = input.split(/\r?\n/);
610
+ let base64Lines;
611
+ for (let beginOffset = 0; beginOffset < inputLines.length; beginOffset++) {
612
+ const line = inputLines[beginOffset]?.trim();
613
+ if (line?.startsWith('-----BEGIN ')) {
614
+ let endIndex = -1;
615
+ const matchingEndLine = line.replace('BEGIN', 'END');
616
+ for (let endOffset = beginOffset + 1; endOffset < inputLines.length; endOffset++) {
617
+ const checkEndLine = inputLines[endOffset]?.trim();
618
+ if (checkEndLine === matchingEndLine) {
619
+ endIndex = endOffset;
620
+ break;
621
+ }
622
+ }
623
+ if (endIndex === -1) {
624
+ throw (new Error('Invalid PEM format: missing END line'));
625
+ }
626
+ base64Lines = inputLines.slice(beginOffset + 1, endIndex);
627
+ break;
628
+ }
629
+ }
630
+ if (base64Lines === undefined) {
631
+ base64Lines = inputLines;
632
+ }
633
+ base64Lines = base64Lines.map(function (line) {
634
+ return (line.trim());
635
+ }).filter(function (line) {
636
+ return (line.length > 0);
637
+ });
638
+ const base64Content = base64Lines.join('');
639
+ containerBuffer = Buffer.from(base64Content, 'base64');
640
+ }
641
+ else {
642
+ containerBuffer = arrayBufferToBuffer(input);
643
+ }
644
+ let principals = options?.principals;
645
+ if (KeetaNetAccount.isInstance(principals)) {
646
+ principals = [principals];
647
+ }
648
+ else if (principals instanceof Set) {
649
+ principals = Array.from(principals);
650
+ }
651
+ else if (principals === undefined) {
652
+ principals = null;
653
+ }
654
+ this.container = EncryptedContainer.fromEncodedBuffer(containerBuffer, principals);
655
+ }
656
+ /**
657
+ * Create a SharableCertificateAttributes from a Certificate
658
+ * and a list of attribute names to include -- if no list is
659
+ * provided, all attributes are included.
660
+ */
661
+ static async fromCertificate(certificate, attributeNames) {
662
+ if (attributeNames === undefined) {
663
+ /*
664
+ * We know the keys are whatever the Certificate says they are, so
665
+ * we can cast here safely
666
+ */
667
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
668
+ attributeNames = Object.keys(certificate.attributes);
669
+ }
670
+ const attributes = {};
671
+ for (const name of attributeNames) {
672
+ const attr = certificate.attributes[name];
673
+ /**
674
+ * Skip missing attributes
675
+ */
676
+ if (!attr) {
677
+ continue;
678
+ }
679
+ if (attr.sensitive) {
680
+ attributes[name] = {
681
+ sensitive: true,
682
+ value: await attr.value.getProof()
683
+ };
684
+ }
685
+ else {
686
+ attributes[name] = {
687
+ sensitive: false,
688
+ value: arrayBufferToBuffer(attr.value).toString('base64')
689
+ };
690
+ }
691
+ }
692
+ const contentsString = JSON.stringify({
693
+ certificate: certificate.toPEM(),
694
+ attributes: attributes
695
+ });
696
+ const temporaryUser = KeetaNetAccount.fromSeed(KeetaNetAccount.generateRandomSeed(), 0);
697
+ const contentsBuffer = Buffer.from(contentsString, 'utf-8');
698
+ const contentsBufferCompressed = await KeetaNetClient.lib.Utils.Buffer.ZlibDeflateAsync(bufferToArrayBuffer(contentsBuffer));
699
+ const container = EncryptedContainer.fromPlaintext(arrayBufferToBuffer(contentsBufferCompressed), [temporaryUser], true);
700
+ const containerBuffer = await container.getEncodedBuffer();
701
+ const retval = new SharableCertificateAttributes(bufferToArrayBuffer(containerBuffer), { principals: temporaryUser });
702
+ await retval.revokeAccess(temporaryUser);
703
+ return (retval);
704
+ }
705
+ async grantAccess(principal) {
706
+ await this.container.grantAccess(principal);
707
+ return (this);
708
+ }
709
+ async revokeAccess(principal) {
710
+ await this.container.revokeAccess(principal);
711
+ return (this);
712
+ }
713
+ get principals() {
714
+ return (this.container.principals);
715
+ }
716
+ async #populate() {
717
+ if (this.populatedFromInit) {
718
+ return;
719
+ }
720
+ this.populatedFromInit = true;
721
+ const contentsBuffer = await this.container.getPlaintext();
722
+ const contentsBufferDecompressed = await KeetaNetClient.lib.Utils.Buffer.ZlibInflateAsync(bufferToArrayBuffer(contentsBuffer));
723
+ const contentsString = Buffer.from(contentsBufferDecompressed).toString('utf-8');
724
+ const contentsJSON = JSON.parse(contentsString);
725
+ const contents = assertSharableCertificateAttributesContentsSchema(contentsJSON);
726
+ this.#certificate = new Certificate(contents.certificate);
727
+ const attributePromises = Object.entries(contents.attributes).map(async ([name, attr]) => {
728
+ /*
729
+ * Get the corresponding attribute from the certificate
730
+ *
731
+ * We actually do not care if `name` is a known attribute
732
+ * because we are not decoding it here, we are just
733
+ * verifying it matches the certificate
734
+ */
735
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
736
+ const certAttribute = this.#certificate?.attributes[name];
737
+ if (!certAttribute) {
738
+ throw (new Error(`Attribute ${name} not found in certificate`));
739
+ }
740
+ if (certAttribute.sensitive !== attr.sensitive) {
741
+ throw (new Error(`Attribute ${name} sensitivity mismatch with certificate`));
742
+ }
743
+ if (!attr.sensitive) {
744
+ if (certAttribute.sensitive) {
745
+ throw (new Error(`Attribute ${name} sensitivity mismatch with certificate`));
746
+ }
747
+ const certValue = certAttribute.value;
748
+ const sharedValue = bufferToArrayBuffer(Buffer.from(attr.value, 'base64'));
749
+ if (sharedValue.byteLength !== certValue.byteLength || !Buffer.from(sharedValue).equals(Buffer.from(certValue))) {
750
+ throw (new Error(`Attribute ${name} value mismatch with certificate`));
751
+ }
752
+ return ([name, {
753
+ sensitive: false,
754
+ value: sharedValue
755
+ }]);
756
+ }
757
+ if (!certAttribute.sensitive) {
758
+ throw (new Error(`Attribute ${name} sensitivity mismatch with certificate`));
759
+ }
760
+ if (!(await certAttribute.value.validateProof(attr.value))) {
761
+ throw (new Error(`Attribute ${name} proof validation failed`));
762
+ }
763
+ const attrValue = bufferToArrayBuffer(Buffer.from(attr.value.value, 'base64'));
764
+ return ([name, {
765
+ sensitive: true,
766
+ value: attrValue
767
+ }]);
768
+ });
769
+ const resolvedAttributes = await Promise.all(attributePromises);
770
+ this.#attributes = Object.fromEntries(resolvedAttributes);
771
+ }
772
+ async getCertificate() {
773
+ await this.#populate();
774
+ if (!this.#certificate) {
775
+ throw (new Error('internal error: certificate not populated'));
776
+ }
777
+ return (this.#certificate);
778
+ }
779
+ async getAttributeBuffer(name) {
780
+ await this.#populate();
781
+ const attr = this.#attributes[name];
782
+ return (attr?.value);
783
+ }
784
+ async getAttribute(name) {
785
+ const buffer = await this.getAttributeBuffer(name);
786
+ if (buffer === undefined) {
787
+ return (undefined);
788
+ }
789
+ const retval = await decodeAttribute(name, buffer);
790
+ /* XXX:TODO: Here is where we would look at a reference value
791
+ * (e.g., URL+hash) and fetch it, and verify it the hash matches
792
+ * the fetched value
793
+ *
794
+ * The schema for references is not yet defined, so this is
795
+ * left as a TODO for now.
796
+ *
797
+ * The return type would also need to be updated to reflect
798
+ * that we would map referenced types to something like
799
+ * { data: ArrayBuffer, contentType: string, source: <url>,
800
+ * hash: <hash> } (where source and hash should be named
801
+ * after whatever the actual schema is)
802
+ */
803
+ return (retval);
804
+ }
805
+ async getAttributeNames(includeUnknown) {
806
+ await this.#populate();
807
+ const names = Object.keys(this.#attributes);
808
+ if (includeUnknown) {
809
+ return (names);
810
+ }
811
+ const knownNames = names.filter(function (name) {
812
+ return (name in CertificateAttributeOIDDB);
813
+ });
814
+ return (knownNames);
815
+ }
816
+ async export(options) {
817
+ options = {
818
+ format: 'arraybuffer',
819
+ ...options
820
+ };
821
+ let principals;
822
+ try {
823
+ principals = this.container.principals;
824
+ }
825
+ catch {
826
+ principals = [];
827
+ }
828
+ if (principals.length === 0) {
829
+ throw (new Error('This container has no authorized users (principals); cannot export'));
830
+ }
831
+ const retvalBuffer = await this.container.getEncodedBuffer();
832
+ if (options.format === 'string') {
833
+ const retvalBase64 = retvalBuffer.toString('base64');
834
+ const retvalLines = ['-----BEGIN KYC CERTIFICATE PROOF-----'];
835
+ retvalLines.push(...retvalBase64.match(/.{1,64}/g) ?? []);
836
+ retvalLines.push('-----END KYC CERTIFICATE PROOF-----');
837
+ return (retvalLines.join('\n'));
838
+ }
839
+ else if (options.format === 'arraybuffer') {
840
+ return (bufferToArrayBuffer(retvalBuffer));
841
+ }
842
+ else {
843
+ throw (new Error(`Unsupported export format: ${String(options.format)}`));
844
+ }
845
+ }
846
+ }
847
+ // @ts-ignore
848
+ Certificate.SharableAttributes = SharableCertificateAttributes;
458
849
  /** @internal */
459
850
  export const _Testing = {
460
851
  SensitiveAttributeBuilder,