@keetanetwork/anchor 0.0.13 → 0.0.14

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 (68) 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 +579 -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 +4 -4
  12. package/lib/http-server.js.map +1 -1
  13. package/lib/resolver.d.ts +2 -1
  14. package/lib/resolver.d.ts.map +1 -1
  15. package/lib/resolver.js.map +1 -1
  16. package/lib/utils/asn1.d.ts +2 -1
  17. package/lib/utils/asn1.d.ts.map +1 -1
  18. package/lib/utils/asn1.js.map +1 -1
  19. package/lib/utils/buffer.d.ts +3 -0
  20. package/lib/utils/buffer.d.ts.map +1 -1
  21. package/lib/utils/buffer.js +26 -0
  22. package/lib/utils/buffer.js.map +1 -1
  23. package/lib/utils/guards.d.ts +14 -0
  24. package/lib/utils/guards.d.ts.map +1 -0
  25. package/lib/utils/guards.js +31 -0
  26. package/lib/utils/guards.js.map +1 -0
  27. package/lib/utils/index.d.ts +3 -1
  28. package/lib/utils/index.d.ts.map +1 -1
  29. package/lib/utils/index.js +3 -1
  30. package/lib/utils/index.js.map +1 -1
  31. package/lib/utils/oid.d.ts +7 -0
  32. package/lib/utils/oid.d.ts.map +1 -0
  33. package/lib/utils/oid.js +22 -0
  34. package/lib/utils/oid.js.map +1 -0
  35. package/lib/utils/signing.d.ts.map +1 -1
  36. package/lib/utils/signing.js +26 -2
  37. package/lib/utils/signing.js.map +1 -1
  38. package/npm-shrinkwrap.json +2 -2
  39. package/package.json +1 -1
  40. package/services/fx/server.js +1 -1
  41. package/services/fx/server.js.map +1 -1
  42. package/services/kyc/client.d.ts.map +1 -1
  43. package/services/kyc/client.js +1 -8
  44. package/services/kyc/client.js.map +1 -1
  45. package/services/kyc/common.d.ts +39 -3
  46. package/services/kyc/common.d.ts.map +1 -1
  47. package/services/kyc/common.generated.d.ts +5 -0
  48. package/services/kyc/common.generated.d.ts.map +1 -0
  49. package/services/kyc/common.generated.js +241 -0
  50. package/services/kyc/common.generated.js.map +1 -0
  51. package/services/kyc/common.js +68 -1
  52. package/services/kyc/common.js.map +1 -1
  53. package/services/kyc/iso20022.generated.d.ts +285 -0
  54. package/services/kyc/iso20022.generated.d.ts.map +1 -0
  55. package/services/kyc/iso20022.generated.js +205 -0
  56. package/services/kyc/iso20022.generated.js.map +1 -0
  57. package/services/kyc/oids.generated.d.ts +83 -0
  58. package/services/kyc/oids.generated.d.ts.map +1 -0
  59. package/services/kyc/oids.generated.js +130 -0
  60. package/services/kyc/oids.generated.js.map +1 -0
  61. package/services/kyc/server.d.ts +141 -0
  62. package/services/kyc/server.d.ts.map +1 -0
  63. package/services/kyc/server.js +183 -0
  64. package/services/kyc/server.js.map +1 -0
  65. package/services/kyc/utils/generate-kyc-schema.d.ts +3 -0
  66. package/services/kyc/utils/generate-kyc-schema.d.ts.map +1 -0
  67. package/services/kyc/utils/generate-kyc-schema.js +1154 -0
  68. package/services/kyc/utils/generate-kyc-schema.js.map +1 -0
@@ -1,69 +1,38 @@
1
+ import * as util from 'util';
1
2
  import * as KeetaNetClient from '@keetanetwork/keetanet-client';
3
+ import * as oids from '../services/kyc/oids.generated.js';
2
4
  import * as ASN1 from './utils/asn1.js';
3
- import { Buffer } from './utils/buffer.js';
5
+ import { ASN1toJS } from './utils/asn1.js';
6
+ import { arrayBufferLikeToBuffer, arrayBufferToBuffer, Buffer, bufferToArrayBuffer } from './utils/buffer.js';
4
7
  import crypto from './utils/crypto.js';
5
8
  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
- }
9
+ import { CertificateAttributeOIDDB, CertificateAttributeSchema, CertificateAttributeFieldNames } from '../services/kyc/iso20022.generated.js';
10
+ import { hasIndexSignature, isErrorLike, hasValueProp, isContextTagged } from './utils/guards.js';
11
+ import { getOID, lookupByOID } from './utils/oid.js';
12
+ import { convertToJSON as convertToJSONUtil } from './utils/json.js';
13
+ import { EncryptedContainer } from './encrypted-container.js';
14
+ import { assertSharableCertificateAttributesContentsSchema } from './certificates.generated.js';
15
+ /**
16
+ * Short alias for printing a debug representation of an object
17
+ */
18
+ const DPO = KeetaNetClient.lib.Utils.Helper.debugPrintableObject.bind(KeetaNetClient.lib.Utils.Helper);
19
+ const KeetaNetAccount = KeetaNetClient.lib.Account;
20
+ function toJSON(data) {
21
+ return (convertToJSONUtil(data));
19
22
  }
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}`));
23
+ // Generic type guard to align decoded values with generated attribute types
24
+ function isAttributeValue(_name, _v) {
25
+ // Runtime schema validation is already performed by BufferStorageASN1; this guard
26
+ // serves to inform TypeScript of the precise type tied to the attribute name.
27
+ return (true);
30
28
  }
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);
29
+ // Helper to apply type guard once and return the properly typed value
30
+ function asAttributeValue(name, v) {
31
+ if (!isAttributeValue(name, v)) {
32
+ throw (new Error('internal error: decoded value did not match expected type'));
33
+ }
34
+ return (v);
61
35
  }
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
36
  /**
68
37
  * Sensitive Attribute Schema
69
38
  *
@@ -107,30 +76,161 @@ const SensitiveAttributeSchemaInternal = [
107
76
  * Database of permitted algorithms and their OIDs
108
77
  */
109
78
  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'
79
+ 'aes-256-gcm': oids.AES_256_GCM,
80
+ 'aes-256-cbc': oids.AES_256_CBC,
81
+ 'sha2-256': oids.SHA2_256,
82
+ 'sha3-256': oids.SHA3_256,
83
+ 'sha256': oids.SHA2_256,
84
+ 'aes256-gcm': oids.AES_256_GCM,
85
+ 'aes256-cbc': oids.AES_256_CBC
114
86
  };
87
+ function assertCertificateAttributeNames(name) {
88
+ if (!(name in CertificateAttributeOIDDB)) {
89
+ throw (new Error(`Unknown attribute name: ${name}`));
90
+ }
91
+ }
92
+ function asCertificateAttributeNames(name) {
93
+ assertCertificateAttributeNames(name);
94
+ return (name);
95
+ }
96
+ function encodeAttribute(name, value) {
97
+ const schema = CertificateAttributeSchema[name];
98
+ const fieldNames = CertificateAttributeFieldNames[name];
99
+ // Date type
100
+ if (schema === ASN1.ValidateASN1.IsDate) {
101
+ if (!(util.types.isDate(value))) {
102
+ throw (new Error('Expected Date value'));
103
+ }
104
+ const asn1 = ASN1.JStoASN1(value);
105
+ const der = asn1.toBER(false);
106
+ return (der);
107
+ }
108
+ const MAX_ASN1_VALUE_DEPTH = 8; // Prevent excessive nesting
109
+ const toASN1Value = (input, depth = 0) => {
110
+ // Only allow primitives and raw binary that ASN1.JStoASN1 understands.
111
+ if (util.types.isDate(input)) {
112
+ return (input);
113
+ }
114
+ if (Buffer.isBuffer(input)) {
115
+ return (input);
116
+ }
117
+ if (input instanceof ArrayBuffer) {
118
+ return (arrayBufferToBuffer(input));
119
+ }
120
+ if (typeof input === 'string') {
121
+ return ({ type: 'string', kind: 'utf8', value: input });
122
+ }
123
+ /* XXX: Why are numbers and booleans encoded as strings? */
124
+ if (typeof input === 'number' || typeof input === 'bigint' || typeof input === 'boolean') {
125
+ return ({ type: 'string', kind: 'utf8', value: String(input) });
126
+ }
127
+ if (Array.isArray(input)) {
128
+ if (depth >= MAX_ASN1_VALUE_DEPTH) {
129
+ throw (new Error('Exceeded maximum ASN.1 value depth'));
130
+ }
131
+ return (input.map(item => toASN1Value(item, depth + 1)));
132
+ }
133
+ throw (new Error(`Unsupported ASN.1 value type: ${typeof input}`));
134
+ };
135
+ // Complex object type
136
+ if (fieldNames && hasIndexSignature(value) && !Array.isArray(value)) {
137
+ const mappedFields = fieldNames.map((fieldName, idx) => {
138
+ const fieldValue = value[fieldName];
139
+ if (fieldValue === undefined) {
140
+ return (undefined);
141
+ }
142
+ const tag = {
143
+ type: 'context',
144
+ kind: 'explicit',
145
+ value: idx,
146
+ contains: toASN1Value(fieldValue)
147
+ };
148
+ return (tag);
149
+ }).filter(function (computedValue) {
150
+ return (computedValue !== undefined);
151
+ });
152
+ const asn1 = ASN1.JStoASN1(mappedFields);
153
+ const der = asn1.toBER(false);
154
+ return (der);
155
+ }
156
+ throw (new Error(`Unsupported attribute value for encoding: ${JSON.stringify(DPO(value))}`));
157
+ }
158
+ // Prepare a value for inclusion in a SensitiveAttribute: pre-encode complex and date types
159
+ function encodeForSensitive(name, value) {
160
+ if (Buffer.isBuffer(value)) {
161
+ return (value);
162
+ }
163
+ if (value instanceof ArrayBuffer) {
164
+ return (arrayBufferToBuffer(value));
165
+ }
166
+ if (typeof value === 'string') {
167
+ const asn1 = ASN1.JStoASN1({ type: 'string', kind: 'utf8', value });
168
+ return (arrayBufferToBuffer(asn1.toBER(false)));
169
+ }
170
+ if (value instanceof Date) {
171
+ const asn1 = ASN1.JStoASN1(value);
172
+ return (arrayBufferToBuffer(asn1.toBER(false)));
173
+ }
174
+ if (typeof value === 'object' && value !== null) {
175
+ if (!name) {
176
+ throw (new Error('attributeName required for complex types'));
177
+ }
178
+ const encoded = encodeAttribute(name, value);
179
+ return (arrayBufferToBuffer(encoded));
180
+ }
181
+ return (Buffer.from(String(value), 'utf-8'));
182
+ }
183
+ async function decodeAttribute(name, value) {
184
+ const schema = CertificateAttributeSchema[name];
185
+ // XXX:TODO Fix depth issue
186
+ // @ts-ignore
187
+ const decodedUnknown = new ASN1.BufferStorageASN1(value, schema).getASN1();
188
+ const fieldNames = CertificateAttributeFieldNames[name];
189
+ let candidate;
190
+ if (fieldNames && Array.isArray(decodedUnknown)) {
191
+ const arr = decodedUnknown;
192
+ const result = {};
193
+ for (let i = 0; i < fieldNames.length; i++) {
194
+ const fieldName = fieldNames[i];
195
+ if (!fieldName) {
196
+ continue;
197
+ }
198
+ const fieldValue = arr[i];
199
+ if (fieldValue === undefined) {
200
+ continue;
201
+ }
202
+ if (isErrorLike(fieldValue)) {
203
+ throw (new Error(`Field ${fieldName} contains an error: ${fieldValue.message}`));
204
+ }
205
+ if (isContextTagged(fieldValue)) {
206
+ // unwrap context tag; prefer nested .value if present
207
+ result[fieldName] = hasValueProp(fieldValue.contains) ? fieldValue.contains.value : fieldValue.contains;
208
+ }
209
+ else if (hasValueProp(fieldValue)) {
210
+ result[fieldName] = fieldValue.value;
211
+ }
212
+ else {
213
+ result[fieldName] = fieldValue;
214
+ }
215
+ }
216
+ candidate = result;
217
+ }
218
+ else if (hasValueProp(decodedUnknown)) {
219
+ candidate = decodedUnknown.value;
220
+ }
221
+ else {
222
+ candidate = decodedUnknown;
223
+ }
224
+ return (asAttributeValue(name, candidate));
225
+ }
115
226
  class SensitiveAttributeBuilder {
116
227
  #account;
117
228
  #value;
118
- constructor(account, value) {
229
+ constructor(account) {
119
230
  this.#account = account;
120
- if (value) {
121
- this.set(value);
122
- }
123
231
  }
124
232
  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
- }
233
+ this.#value = Buffer.isBuffer(value) ? value : arrayBufferLikeToBuffer(value);
134
234
  return (this);
135
235
  }
136
236
  async build() {
@@ -143,15 +243,31 @@ class SensitiveAttributeBuilder {
143
243
  const cipher = 'aes-256-gcm';
144
244
  const key = crypto.randomBytes(32);
145
245
  const nonce = crypto.randomBytes(12);
146
- const encryptedKey = await this.#account.encrypt(key);
246
+ const encryptedKey = await this.#account.encrypt(bufferToArrayBuffer(key));
147
247
  function encrypt(value) {
148
248
  const cipherObject = crypto.createCipheriv(cipher, key, nonce);
149
249
  let retval = cipherObject.update(value);
150
250
  retval = Buffer.concat([retval, cipherObject.final()]);
251
+ /*
252
+ * For AES-GCM, the last 16 bytes are the authentication tag
253
+ */
254
+ if (cipher === 'aes-256-gcm') {
255
+ const getAuthTagFn = Reflect.get(cipherObject, 'getAuthTag');
256
+ if (typeof getAuthTagFn === 'function') {
257
+ const tag = getAuthTagFn.call(cipherObject);
258
+ if (!Buffer.isBuffer(tag)) {
259
+ throw (new Error('getAuthTag did not return a Buffer'));
260
+ }
261
+ retval = Buffer.concat([retval, tag]);
262
+ }
263
+ else {
264
+ throw (new Error('getAuthTag is not available on cipherObject'));
265
+ }
266
+ }
151
267
  return (retval);
152
268
  }
153
269
  const encryptedValue = encrypt(this.#value);
154
- const encryptedSalt = encrypt(salt);
270
+ const encryptedSalt = encrypt(arrayBufferLikeToBuffer(salt));
155
271
  const saltedValue = Buffer.concat([salt, publicKey, encryptedValue, this.#value]);
156
272
  const hashedAndSaltedValue = KeetaNetClient.lib.Utils.Hash.Hash(saltedValue);
157
273
  const attributeStructure = [
@@ -179,23 +295,35 @@ class SensitiveAttributeBuilder {
179
295
  encryptedValue
180
296
  ];
181
297
  const encodedAttributeObject = ASN1.JStoASN1(attributeStructure);
182
- const retval = encodedAttributeObject.toBER();
298
+ // Produce canonical DER as ArrayBuffer
299
+ const retval = encodedAttributeObject.toBER(false);
183
300
  return (retval);
184
301
  }
185
302
  }
186
303
  class SensitiveAttribute {
187
304
  #account;
188
305
  #info;
189
- constructor(account, data) {
306
+ #decoder;
307
+ constructor(account, data, decoder) {
190
308
  this.#account = account;
191
309
  this.#info = this.decode(data);
310
+ if (decoder) {
311
+ this.#decoder = decoder;
312
+ }
192
313
  }
193
314
  decode(data) {
194
315
  if (Buffer.isBuffer(data)) {
195
316
  data = bufferToArrayBuffer(data);
196
317
  }
197
- const dataObject = new ASN1.BufferStorageASN1(data, SensitiveAttributeSchemaInternal);
198
- const decodedAttribute = dataObject.getASN1();
318
+ let decodedAttribute;
319
+ try {
320
+ const dataObject = new ASN1.BufferStorageASN1(data, SensitiveAttributeSchemaInternal);
321
+ decodedAttribute = dataObject.getASN1();
322
+ }
323
+ catch {
324
+ const js = ASN1toJS(data);
325
+ throw (new Error(`SensitiveAttribute.decode: unexpected DER shape ${JSON.stringify(DPO(js))}`));
326
+ }
199
327
  const decodedVersion = decodedAttribute[0] + 1n;
200
328
  if (decodedVersion !== 1n) {
201
329
  throw (new Error(`Unsupported Sensitive Attribute version (${decodedVersion})`));
@@ -217,11 +345,31 @@ class SensitiveAttribute {
217
345
  });
218
346
  }
219
347
  async #decryptValue(value) {
220
- const decryptedKey = await this.#account.decrypt(this.#info.cipher.key);
348
+ const decryptedKey = await this.#account.decrypt(bufferToArrayBuffer(this.#info.cipher.key));
221
349
  const algorithm = this.#info.cipher.algorithm;
222
350
  const iv = this.#info.cipher.iv;
223
351
  const cipher = crypto.createDecipheriv(algorithm, Buffer.from(decryptedKey), iv);
352
+ // For AES-GCM, the last 16 bytes are the authentication tag
353
+ if (algorithm === 'aes-256-gcm') {
354
+ const authTag = value.subarray(value.length - 16);
355
+ const ciphertext = value.subarray(0, value.length - 16);
356
+ // XXX:TODO Fix typescript unsafe calls
357
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
358
+ const setAuthTagFn = Reflect.get(cipher, 'setAuthTag');
359
+ if (typeof setAuthTagFn === 'function') {
360
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
361
+ setAuthTagFn.call(cipher, authTag);
362
+ }
363
+ else {
364
+ throw (new Error('setAuthTag is not available on cipher'));
365
+ }
366
+ const decrypted = cipher.update(ciphertext);
367
+ cipher.final(); // Verify auth tag
368
+ return (decrypted);
369
+ }
370
+ // For other algorithms (like CBC), just decrypt normally
224
371
  const decryptedValue = cipher.update(value);
372
+ cipher.final();
225
373
  return (decryptedValue);
226
374
  }
227
375
  /**
@@ -234,25 +382,33 @@ class SensitiveAttribute {
234
382
  * ArrayBuffer
235
383
  */
236
384
  async get() {
237
- const decryptedValue = await this.#decryptValue(this.#info.encryptedValue);
385
+ const decryptedValue = await this.#decryptValue(arrayBufferLikeToBuffer(this.#info.encryptedValue));
238
386
  return (bufferToArrayBuffer(decryptedValue));
239
387
  }
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() {
388
+ async getValue() {
245
389
  const value = await this.get();
246
- return (Buffer.from(value).toString('utf-8'));
390
+ if (!this.#decoder) {
391
+ /**
392
+ * TypeScript complains that T may not be the correct
393
+ * type here, but gives us no tools to enforce that it
394
+ * is -- it should always be ArrayBuffer if no decoder
395
+ * is provided, but someone could always specify a
396
+ * type parameter in that case and we cannot check
397
+ * that at runtime since T is only a compile-time type.
398
+ */
399
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
400
+ return value;
401
+ }
402
+ return (this.#decoder(value));
247
403
  }
248
404
  /**
249
405
  * Generate a proof that a sensitive attribute is a given value,
250
406
  * which can be validated by a third party using the certificate
251
407
  * and the `validateProof` method
252
408
  */
253
- async proove() {
409
+ async getProof() {
254
410
  const value = await this.get();
255
- const salt = await this.#decryptValue(this.#info.hashedValue.encryptedSalt);
411
+ const salt = await this.#decryptValue(arrayBufferLikeToBuffer(this.#info.hashedValue.encryptedSalt));
256
412
  return ({
257
413
  value: Buffer.from(value).toString('base64'),
258
414
  hash: {
@@ -268,7 +424,8 @@ class SensitiveAttribute {
268
424
  const proofSaltBuffer = Buffer.from(proof.hash.salt, 'base64');
269
425
  const publicKeyBuffer = Buffer.from(this.#account.publicKey.get());
270
426
  const encryptedValue = this.#info.encryptedValue;
271
- const hashedAndSaltedValue = KeetaNetClient.lib.Utils.Hash.Hash(Buffer.concat([proofSaltBuffer, publicKeyBuffer, encryptedValue, plaintextValue]));
427
+ const hashInput = Buffer.concat([proofSaltBuffer, publicKeyBuffer, encryptedValue, plaintextValue]);
428
+ const hashedAndSaltedValue = KeetaNetClient.lib.Utils.Hash.Hash(hashInput);
272
429
  const hashedAndSaltedValueBuffer = Buffer.from(hashedAndSaltedValue);
273
430
  return (this.#info.hashedValue.value.equals(hashedAndSaltedValueBuffer));
274
431
  }
@@ -276,16 +433,6 @@ class SensitiveAttribute {
276
433
  return (toJSON(this.#info));
277
434
  }
278
435
  }
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
436
  /**
290
437
  * ASN.1 Schema for Certificate KYC Attributes Extension
291
438
  *
@@ -338,10 +485,40 @@ export class CertificateBuilder extends KeetaNetClient.lib.Utils.Certificate.Cer
338
485
  super(CertificateBuilder.mapParams(params));
339
486
  }
340
487
  /**
341
- * Set a KYC Attribute to a given value
488
+ * Set a KYC Attribute to a given value.
489
+ * The sensitive flag is required.
490
+ *
491
+ * If an attribute is marked sensitive, the value is encoded
492
+ * into the certificate using a commitment scheme so that the
493
+ * value can be proven later without revealing it.
342
494
  */
343
495
  setAttribute(name, sensitive, value) {
344
- this.#attributes[name] = { sensitive, value };
496
+ // Non-sensitive path: only primitive schema (string/date) allowed
497
+ const schemaValidator = CertificateAttributeSchema[name];
498
+ let encoded;
499
+ if (value instanceof ArrayBuffer) {
500
+ encoded = value;
501
+ }
502
+ else if (name in CertificateAttributeSchema) {
503
+ /* XXX: Why do we have two encoding methods ? */
504
+ encoded = bufferToArrayBuffer(encodeForSensitive(name, value));
505
+ }
506
+ else if (schemaValidator === ASN1.ValidateASN1.IsDate) {
507
+ if (!(value instanceof Date)) {
508
+ throw (new Error('Expected Date value'));
509
+ }
510
+ encoded = encodeAttribute(name, value);
511
+ }
512
+ else if (schemaValidator === ASN1.ValidateASN1.IsString && typeof value === 'string') {
513
+ encoded = encodeAttribute(name, value);
514
+ }
515
+ else {
516
+ throw (new Error('Unsupported non-sensitive value type'));
517
+ }
518
+ this.#attributes[name] = {
519
+ sensitive: sensitive,
520
+ value: encoded
521
+ };
345
522
  }
346
523
  async addExtensions(...args) {
347
524
  const retval = await super.addExtensions(...args);
@@ -357,19 +534,20 @@ export class CertificateBuilder extends KeetaNetClient.lib.Utils.Certificate.Cer
357
534
  * can assume that the attribute is always present in
358
535
  * the object
359
536
  */
360
- // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
537
+ assertCertificateAttributeNames(name);
361
538
  const nameOID = CertificateAttributeOIDDB[name];
362
539
  let value;
363
540
  if (attribute.sensitive) {
364
- const sensitiveAttribute = new SensitiveAttributeBuilder(subject, attribute.value);
365
- value = Buffer.from(await sensitiveAttribute.build());
541
+ const builder = new SensitiveAttributeBuilder(subject);
542
+ builder.set(attribute.value);
543
+ value = arrayBufferToBuffer(await builder.build());
366
544
  }
367
545
  else {
368
546
  if (typeof attribute.value === 'string') {
369
547
  value = Buffer.from(attribute.value, 'utf-8');
370
548
  }
371
549
  else {
372
- value = Buffer.from(attribute.value);
550
+ value = arrayBufferToBuffer(attribute.value);
373
551
  }
374
552
  }
375
553
  certAttributes.push([{
@@ -383,7 +561,7 @@ export class CertificateBuilder extends KeetaNetClient.lib.Utils.Certificate.Cer
383
561
  }]);
384
562
  }
385
563
  if (certAttributes.length > 0) {
386
- retval.push(KeetaNetClient.lib.Utils.Certificate.CertificateBuilder.extension('1.3.6.1.4.1.62675.0.0', certAttributes));
564
+ retval.push(KeetaNetClient.lib.Utils.Certificate.CertificateBuilder.extension(oids.keeta.KYC_ATTRIBUTES, certAttributes));
387
565
  }
388
566
  return (retval);
389
567
  }
@@ -412,6 +590,7 @@ export class CertificateBuilder extends KeetaNetClient.lib.Utils.Certificate.Cer
412
590
  export class Certificate extends KeetaNetClient.lib.Utils.Certificate.Certificate {
413
591
  subjectKey;
414
592
  static Builder = CertificateBuilder;
593
+ static SharableAttributes;
415
594
  /**
416
595
  * User KYC Attributes
417
596
  */
@@ -424,27 +603,56 @@ export class Certificate extends KeetaNetClient.lib.Utils.Certificate.Certificat
424
603
  finalizeConstruction() {
425
604
  /* Do nothing, we call the super method in the constructor */
426
605
  }
606
+ setPlainAttribute(name, value) {
607
+ // @ts-ignore
608
+ this.attributes[name] = { sensitive: false, value };
609
+ }
610
+ setSensitiveAttribute(name, value) {
611
+ this.attributes[name] = {
612
+ sensitive: true,
613
+ value: new SensitiveAttribute(this.subjectKey, value, decodeAttribute.bind(null, name))
614
+ };
615
+ }
616
+ /**
617
+ * Get the underlying value for an attribute.
618
+ *
619
+ * If the attribute is sensitive, this will decrypt it using the
620
+ * subject's private key, otherwise it will return the value.
621
+ */
622
+ async getAttributeValue(attributeName) {
623
+ const attr = this.attributes[attributeName]?.value;
624
+ if (!attr) {
625
+ throw (new Error(`Attribute ${attributeName} is not available`));
626
+ }
627
+ if (attr instanceof SensitiveAttribute) {
628
+ const raw = await attr.get();
629
+ return (await decodeAttribute(attributeName, raw));
630
+ }
631
+ // Non-sensitive: ArrayBuffer or Buffer
632
+ if (attr instanceof ArrayBuffer || Buffer.isBuffer(attr)) {
633
+ return (await decodeAttribute(attributeName, attr));
634
+ }
635
+ throw (new Error(`Attribute ${attributeName} is not a supported type`));
636
+ }
427
637
  processExtension(id, value) {
428
638
  if (super.processExtension(id, value)) {
429
639
  return (true);
430
640
  }
431
- if (id === '1.3.6.1.4.1.62675.0.0') {
641
+ if (id === oids.keeta.KYC_ATTRIBUTES) {
432
642
  const attributesRaw = new ASN1.BufferStorageASN1(value, CertificateKYCAttributeSchemaValidation).getASN1();
433
643
  for (const attribute of attributesRaw) {
434
- const name = lookupByOID(attribute[0].oid, CertificateAttributeOIDDB);
644
+ const nameString = lookupByOID(attribute[0].oid, CertificateAttributeOIDDB);
645
+ const name = asCertificateAttributeNames(nameString);
435
646
  const valueKind = attribute[1].value;
436
647
  const value = bufferToArrayBuffer(attribute[1].contains);
437
648
  switch (valueKind) {
438
649
  case 0:
439
650
  /* Plain Value */
440
- this.attributes[name] = { sensitive: false, value: value };
651
+ this.setPlainAttribute(name, value);
441
652
  break;
442
653
  case 1:
443
654
  /* Sensitive Value */
444
- this.attributes[name] = {
445
- sensitive: true,
446
- value: new SensitiveAttribute(this.subjectKey, value)
447
- };
655
+ this.setSensitiveAttribute(name, value);
448
656
  break;
449
657
  default:
450
658
  assertNever(valueKind);
@@ -455,6 +663,260 @@ export class Certificate extends KeetaNetClient.lib.Utils.Certificate.Certificat
455
663
  return (false);
456
664
  }
457
665
  }
666
+ ;
667
+ export class SharableCertificateAttributes {
668
+ #certificate;
669
+ #attributes = {};
670
+ container;
671
+ populatedFromInit = false;
672
+ static assertCertificateAttributeName = assertCertificateAttributeNames;
673
+ constructor(input, options) {
674
+ let containerBuffer;
675
+ if (typeof input === 'string') {
676
+ /*
677
+ * Attempt to decode as PEM, but also if not PEM, then return
678
+ * the lines as-is (base64) after removing whitespace
679
+ */
680
+ const inputLines = input.split(/\r?\n/);
681
+ let base64Lines;
682
+ for (let beginOffset = 0; beginOffset < inputLines.length; beginOffset++) {
683
+ const line = inputLines[beginOffset]?.trim();
684
+ if (line?.startsWith('-----BEGIN ')) {
685
+ let endIndex = -1;
686
+ const matchingEndLine = line.replace('BEGIN', 'END');
687
+ for (let endOffset = beginOffset + 1; endOffset < inputLines.length; endOffset++) {
688
+ const checkEndLine = inputLines[endOffset]?.trim();
689
+ if (checkEndLine === matchingEndLine) {
690
+ endIndex = endOffset;
691
+ break;
692
+ }
693
+ }
694
+ if (endIndex === -1) {
695
+ throw (new Error('Invalid PEM format: missing END line'));
696
+ }
697
+ base64Lines = inputLines.slice(beginOffset + 1, endIndex);
698
+ break;
699
+ }
700
+ }
701
+ if (base64Lines === undefined) {
702
+ base64Lines = inputLines;
703
+ }
704
+ base64Lines = base64Lines.map(function (line) {
705
+ return (line.trim());
706
+ }).filter(function (line) {
707
+ return (line.length > 0);
708
+ });
709
+ const base64Content = base64Lines.join('');
710
+ containerBuffer = Buffer.from(base64Content, 'base64');
711
+ }
712
+ else {
713
+ containerBuffer = arrayBufferToBuffer(input);
714
+ }
715
+ let principals = options?.principals;
716
+ if (KeetaNetAccount.isInstance(principals)) {
717
+ principals = [principals];
718
+ }
719
+ else if (principals instanceof Set) {
720
+ principals = Array.from(principals);
721
+ }
722
+ else if (principals === undefined) {
723
+ principals = null;
724
+ }
725
+ this.container = EncryptedContainer.fromEncodedBuffer(containerBuffer, principals);
726
+ }
727
+ /**
728
+ * Create a SharableCertificateAttributes from a Certificate
729
+ * and a list of attribute names to include -- if no list is
730
+ * provided, all attributes are included.
731
+ */
732
+ static async fromCertificate(certificate, attributeNames) {
733
+ if (attributeNames === undefined) {
734
+ /*
735
+ * We know the keys are whatever the Certificate says they are, so
736
+ * we can cast here safely
737
+ */
738
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
739
+ attributeNames = Object.keys(certificate.attributes);
740
+ }
741
+ const attributes = {};
742
+ for (const name of attributeNames) {
743
+ const attr = certificate.attributes[name];
744
+ /**
745
+ * Skip missing attributes
746
+ */
747
+ if (!attr) {
748
+ continue;
749
+ }
750
+ if (attr.sensitive) {
751
+ attributes[name] = {
752
+ sensitive: true,
753
+ value: await attr.value.getProof()
754
+ };
755
+ }
756
+ else {
757
+ attributes[name] = {
758
+ sensitive: false,
759
+ value: arrayBufferToBuffer(attr.value).toString('base64')
760
+ };
761
+ }
762
+ }
763
+ const contentsString = JSON.stringify({
764
+ certificate: certificate.toPEM(),
765
+ attributes: attributes
766
+ });
767
+ const temporaryUser = KeetaNetAccount.fromSeed(KeetaNetAccount.generateRandomSeed(), 0);
768
+ const contentsBuffer = Buffer.from(contentsString, 'utf-8');
769
+ const contentsBufferCompressed = await KeetaNetClient.lib.Utils.Buffer.ZlibDeflateAsync(bufferToArrayBuffer(contentsBuffer));
770
+ const container = EncryptedContainer.fromPlaintext(arrayBufferToBuffer(contentsBufferCompressed), [temporaryUser], true);
771
+ const containerBuffer = await container.getEncodedBuffer();
772
+ const retval = new SharableCertificateAttributes(bufferToArrayBuffer(containerBuffer), { principals: temporaryUser });
773
+ await retval.revokeAccess(temporaryUser);
774
+ return (retval);
775
+ }
776
+ async grantAccess(principal) {
777
+ await this.container.grantAccess(principal);
778
+ return (this);
779
+ }
780
+ async revokeAccess(principal) {
781
+ await this.container.revokeAccess(principal);
782
+ return (this);
783
+ }
784
+ get principals() {
785
+ return (this.container.principals);
786
+ }
787
+ async #populate() {
788
+ if (this.populatedFromInit) {
789
+ return;
790
+ }
791
+ this.populatedFromInit = true;
792
+ const contentsBuffer = await this.container.getPlaintext();
793
+ const contentsBufferDecompressed = await KeetaNetClient.lib.Utils.Buffer.ZlibInflateAsync(bufferToArrayBuffer(contentsBuffer));
794
+ const contentsString = Buffer.from(contentsBufferDecompressed).toString('utf-8');
795
+ const contentsJSON = JSON.parse(contentsString);
796
+ const contents = assertSharableCertificateAttributesContentsSchema(contentsJSON);
797
+ this.#certificate = new Certificate(contents.certificate);
798
+ const attributePromises = Object.entries(contents.attributes).map(async ([name, attr]) => {
799
+ /*
800
+ * Get the corresponding attribute from the certificate
801
+ *
802
+ * We actually do not care if `name` is a known attribute
803
+ * because we are not decoding it here, we are just
804
+ * verifying it matches the certificate
805
+ */
806
+ // eslint-disable-next-line @typescript-eslint/consistent-type-assertions
807
+ const certAttribute = this.#certificate?.attributes[name];
808
+ if (!certAttribute) {
809
+ throw (new Error(`Attribute ${name} not found in certificate`));
810
+ }
811
+ if (certAttribute.sensitive !== attr.sensitive) {
812
+ throw (new Error(`Attribute ${name} sensitivity mismatch with certificate`));
813
+ }
814
+ if (!attr.sensitive) {
815
+ if (certAttribute.sensitive) {
816
+ throw (new Error(`Attribute ${name} sensitivity mismatch with certificate`));
817
+ }
818
+ const certValue = certAttribute.value;
819
+ const sharedValue = bufferToArrayBuffer(Buffer.from(attr.value, 'base64'));
820
+ if (sharedValue.byteLength !== certValue.byteLength || !Buffer.from(sharedValue).equals(Buffer.from(certValue))) {
821
+ throw (new Error(`Attribute ${name} value mismatch with certificate`));
822
+ }
823
+ return ([name, {
824
+ sensitive: false,
825
+ value: sharedValue
826
+ }]);
827
+ }
828
+ if (!certAttribute.sensitive) {
829
+ throw (new Error(`Attribute ${name} sensitivity mismatch with certificate`));
830
+ }
831
+ if (!(await certAttribute.value.validateProof(attr.value))) {
832
+ throw (new Error(`Attribute ${name} proof validation failed`));
833
+ }
834
+ const attrValue = bufferToArrayBuffer(Buffer.from(attr.value.value, 'base64'));
835
+ return ([name, {
836
+ sensitive: true,
837
+ value: attrValue
838
+ }]);
839
+ });
840
+ const resolvedAttributes = await Promise.all(attributePromises);
841
+ this.#attributes = Object.fromEntries(resolvedAttributes);
842
+ }
843
+ async getCertificate() {
844
+ await this.#populate();
845
+ if (!this.#certificate) {
846
+ throw (new Error('internal error: certificate not populated'));
847
+ }
848
+ return (this.#certificate);
849
+ }
850
+ async getAttributeBuffer(name) {
851
+ await this.#populate();
852
+ const attr = this.#attributes[name];
853
+ return (attr?.value);
854
+ }
855
+ async getAttribute(name) {
856
+ const buffer = await this.getAttributeBuffer(name);
857
+ if (buffer === undefined) {
858
+ return (undefined);
859
+ }
860
+ const retval = await decodeAttribute(name, buffer);
861
+ /* XXX:TODO: Here is where we would look at a reference value
862
+ * (e.g., URL+hash) and fetch it, and verify it the hash matches
863
+ * the fetched value
864
+ *
865
+ * The schema for references is not yet defined, so this is
866
+ * left as a TODO for now.
867
+ *
868
+ * The return type would also need to be updated to reflect
869
+ * that we would map referenced types to something like
870
+ * { data: ArrayBuffer, contentType: string, source: <url>,
871
+ * hash: <hash> } (where source and hash should be named
872
+ * after whatever the actual schema is)
873
+ */
874
+ return (retval);
875
+ }
876
+ async getAttributeNames(includeUnknown) {
877
+ await this.#populate();
878
+ const names = Object.keys(this.#attributes);
879
+ if (includeUnknown) {
880
+ return (names);
881
+ }
882
+ const knownNames = names.filter(function (name) {
883
+ return (name in CertificateAttributeOIDDB);
884
+ });
885
+ return (knownNames);
886
+ }
887
+ async export(options) {
888
+ options = {
889
+ format: 'arraybuffer',
890
+ ...options
891
+ };
892
+ let principals;
893
+ try {
894
+ principals = this.container.principals;
895
+ }
896
+ catch {
897
+ principals = [];
898
+ }
899
+ if (principals.length === 0) {
900
+ throw (new Error('This container has no authorized users (principals); cannot export'));
901
+ }
902
+ const retvalBuffer = await this.container.getEncodedBuffer();
903
+ if (options.format === 'string') {
904
+ const retvalBase64 = retvalBuffer.toString('base64');
905
+ const retvalLines = ['-----BEGIN KYC CERTIFICATE PROOF-----'];
906
+ retvalLines.push(...retvalBase64.match(/.{1,64}/g) ?? []);
907
+ retvalLines.push('-----END KYC CERTIFICATE PROOF-----');
908
+ return (retvalLines.join('\n'));
909
+ }
910
+ else if (options.format === 'arraybuffer') {
911
+ return (bufferToArrayBuffer(retvalBuffer));
912
+ }
913
+ else {
914
+ throw (new Error(`Unsupported export format: ${String(options.format)}`));
915
+ }
916
+ }
917
+ }
918
+ // @ts-ignore
919
+ Certificate.SharableAttributes = SharableCertificateAttributes;
458
920
  /** @internal */
459
921
  export const _Testing = {
460
922
  SensitiveAttributeBuilder,