@interop/edv-client 17.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +27 -0
  2. package/README.md +276 -0
  3. package/dist/EdvClient.d.ts +412 -0
  4. package/dist/EdvClient.d.ts.map +1 -0
  5. package/dist/EdvClient.js +663 -0
  6. package/dist/EdvClient.js.map +1 -0
  7. package/dist/EdvClientCore.d.ts +264 -0
  8. package/dist/EdvClientCore.d.ts.map +1 -0
  9. package/dist/EdvClientCore.js +698 -0
  10. package/dist/EdvClientCore.js.map +1 -0
  11. package/dist/EdvDocument.d.ts +92 -0
  12. package/dist/EdvDocument.d.ts.map +1 -0
  13. package/dist/EdvDocument.js +149 -0
  14. package/dist/EdvDocument.js.map +1 -0
  15. package/dist/HttpsTransport.d.ts +87 -0
  16. package/dist/HttpsTransport.d.ts.map +1 -0
  17. package/dist/HttpsTransport.js +415 -0
  18. package/dist/HttpsTransport.js.map +1 -0
  19. package/dist/IndexHelper.d.ts +163 -0
  20. package/dist/IndexHelper.d.ts.map +1 -0
  21. package/dist/IndexHelper.js +539 -0
  22. package/dist/IndexHelper.js.map +1 -0
  23. package/dist/LegacyIndexHelperVersion1.d.ts +150 -0
  24. package/dist/LegacyIndexHelperVersion1.d.ts.map +1 -0
  25. package/dist/LegacyIndexHelperVersion1.js +475 -0
  26. package/dist/LegacyIndexHelperVersion1.js.map +1 -0
  27. package/dist/Transport.d.ts +142 -0
  28. package/dist/Transport.d.ts.map +1 -0
  29. package/dist/Transport.js +181 -0
  30. package/dist/Transport.js.map +1 -0
  31. package/dist/assert.d.ts +6 -0
  32. package/dist/assert.d.ts.map +1 -0
  33. package/dist/assert.js +61 -0
  34. package/dist/assert.js.map +1 -0
  35. package/dist/baseX.d.ts +7 -0
  36. package/dist/baseX.d.ts.map +1 -0
  37. package/dist/baseX.js +8 -0
  38. package/dist/baseX.js.map +1 -0
  39. package/dist/index.d.ts +9 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +9 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/util.d.ts +3 -0
  44. package/dist/util.d.ts.map +1 -0
  45. package/dist/util.js +13 -0
  46. package/dist/util.js.map +1 -0
  47. package/package.json +112 -0
@@ -0,0 +1,698 @@
1
+ /*!
2
+ * Copyright (c) 2018-2025 Digital Bazaar, Inc. All rights reserved.
3
+ */
4
+ import { base58btc } from './baseX.js';
5
+ import { assert, assertDocId, assertDocument, assertTransport } from './assert.js';
6
+ import { Cipher } from '@interop/minimal-cipher';
7
+ import { getRandomBytes } from './util.js';
8
+ import { IndexHelper } from './IndexHelper.js';
9
+ import { LegacyIndexHelperVersion1 } from './LegacyIndexHelperVersion1.js';
10
+ // 1 MiB = 1048576
11
+ const DEFAULT_CHUNK_SIZE = 1048576;
12
+ export class EdvClientCore {
13
+ hmac;
14
+ id;
15
+ keyAgreementKey;
16
+ keyResolver;
17
+ cipher;
18
+ indexHelper;
19
+ /**
20
+ * Creates the core of an EdvClient. The core must be coupled with a
21
+ * Transport layer.
22
+ *
23
+ * @param {object} options - The options to use.
24
+ * @param {object} [options.hmac] - A default HMAC API for blinding
25
+ * indexable attributes.
26
+ * @param {string} [options.id] - The ID of the EDV.
27
+ * @param {object} [options.keyAgreementKey] - A default KeyAgreementKey
28
+ * API for deriving shared KEKs for wrapping content encryption keys.
29
+ * @param {Function} [options.keyResolver] - A default function that returns
30
+ * a Promise that resolves a key ID to a DH public key.
31
+ * @param {string} [options.cipherVersion='recommended'] - Sets the cipher
32
+ * version to either "recommended" or "fips".
33
+ * @param {string} [options._attributeVersion=2] - Sets the blinded attribute
34
+ * version to use; for internal use only.
35
+ *
36
+ * @returns {EdvClientCore} An EdvClientCore instance.
37
+ */
38
+ constructor({ hmac, id, keyAgreementKey, keyResolver, cipherVersion = 'recommended', _attributeVersion = 2 } = {}) {
39
+ if (id !== undefined) {
40
+ assert(id, 'id', 'string');
41
+ }
42
+ this.hmac = hmac;
43
+ this.id = id;
44
+ this.keyAgreementKey = keyAgreementKey;
45
+ this.keyResolver = keyResolver;
46
+ this.cipher = new Cipher({ version: cipherVersion });
47
+ if (_attributeVersion === 2) {
48
+ this.indexHelper = new IndexHelper();
49
+ }
50
+ else if (_attributeVersion === 1) {
51
+ this.indexHelper = new LegacyIndexHelperVersion1();
52
+ }
53
+ else {
54
+ throw new Error(`Unsupported "_attributeVersion" "${_attributeVersion}".`);
55
+ }
56
+ }
57
+ /**
58
+ * Ensures that future documents inserted or updated using this Edv
59
+ * instance will be indexed according to the given attribute, provided that
60
+ * they contain that attribute. Compound indexes can be specified by
61
+ * providing an array for `attribute`.
62
+ *
63
+ * @param {object} options - The options to use.
64
+ * @param {string|Array} options.attribute - The attribute name or an array of
65
+ * attribute names to create a unique compound index.
66
+ * @param {boolean} [options.unique=false] - Should be `true` if the index is
67
+ * considered unique, `false` if not.
68
+ */
69
+ ensureIndex({ attribute, unique = false } = {}) {
70
+ this.indexHelper.ensureIndex({ attribute, unique, hmac: this.hmac });
71
+ }
72
+ /**
73
+ * Encrypts and inserts a document into the EDV if it does not already
74
+ * exist. If a document matching its ID already exists, a `DuplicateError` is
75
+ * thrown. If a `stream` is given, the document will be inserted, then
76
+ * the stream will be read, chunked, and stored. Finally, the document will
77
+ * be updated to include meta data about the stored data from the stream,
78
+ * including a message digest.
79
+ *
80
+ * @param {object} options - The options to use.
81
+ * @param {object} options.doc - The document to insert.
82
+ * @param {ReadableStream} [options.stream] - A WHATWG Readable stream to read
83
+ * from to associate chunked data with this document.
84
+ * @param {number} [options.chunkSize=1048576] - The size, in bytes, of the
85
+ * chunks to break the incoming stream data into.
86
+ * @param {object[]} [options.recipients=[]] - A set of JWE recipients
87
+ * to encrypt the document for; if not present, a default recipient will
88
+ * be added using `this.keyAgreementKey` and if no `keyAgreementKey` is
89
+ * set, an error will be thrown.
90
+ * @param {Function} [options.keyResolver=this.keyResolver] - A function that
91
+ * returns a Promise that resolves a key ID to a DH public key.
92
+ * @param {object} [options.keyAgreementKey=this.keyAgreementKey] - A
93
+ * KeyAgreementKey API for deriving shared KEKs for wrapping content
94
+ * encryption keys.
95
+ * @param {object} [options.hmac=this.hmac] - An HMAC API for blinding
96
+ * indexable attributes.
97
+ * @param {object} options.transport - The Transport instance to use.
98
+ *
99
+ * @returns {Promise<object>} - Resolves to the inserted document.
100
+ */
101
+ async insert({ doc, stream, chunkSize, recipients = [], keyResolver = this.keyResolver, keyAgreementKey = this.keyAgreementKey, hmac = this.hmac, transport } = {}) {
102
+ assertDocument(doc);
103
+ assertTransport(transport);
104
+ doc = { ...doc };
105
+ // auto generate document ID
106
+ if (doc.id === undefined) {
107
+ doc.id = await EdvClientCore.generateId();
108
+ }
109
+ // if no recipients specified, add default
110
+ if (recipients.length === 0 && keyAgreementKey) {
111
+ recipients = this._createDefaultRecipients(keyAgreementKey);
112
+ }
113
+ // if a stream was specified, indicate a new stream of data will be
114
+ // associated with this document
115
+ if (stream) {
116
+ // specify stream information
117
+ doc.stream = {
118
+ pending: true
119
+ };
120
+ keyResolver = _createCachedKeyResolver(keyResolver);
121
+ }
122
+ const encrypted = await this._encrypt({
123
+ doc,
124
+ recipients,
125
+ keyResolver,
126
+ hmac,
127
+ update: false
128
+ });
129
+ // send encrypted doc to EDV server
130
+ await transport.insert({ encrypted });
131
+ let result = encrypted;
132
+ encrypted.content = doc.content;
133
+ encrypted.meta = doc.meta;
134
+ if (doc.stream !== undefined) {
135
+ encrypted.stream = doc.stream;
136
+ }
137
+ // if a `stream` was given, update it
138
+ if (stream) {
139
+ result = await this._updateStream({
140
+ doc: encrypted,
141
+ stream,
142
+ chunkSize,
143
+ recipients: recipients.slice(),
144
+ keyResolver,
145
+ keyAgreementKey,
146
+ transport
147
+ });
148
+ }
149
+ return result;
150
+ }
151
+ /**
152
+ * Encrypts and updates a document in the EDV. If the document does not
153
+ * already exist, it is created. If a `stream` is provided, the document
154
+ * will be updated twice, once using the given update and a second time
155
+ * once the stream has been read, chunked, and stored to include meta data
156
+ * information such as the stream data's message digest.
157
+ *
158
+ * @param {object} options - The options to use.
159
+ * @param {object} options.doc - The document to insert.
160
+ * @param {ReadableStream} [options.stream] - A WHATWG Readable stream to read
161
+ * from to associate chunked data with this document.
162
+ * @param {number} [options.chunkSize=1048576] - The size, in bytes, of the
163
+ * chunks to break the incoming stream data into.
164
+ * @param {object} [options.recipients=[]] - A set of JWE recipients to
165
+ * encrypt the document for; if present, recipients will be added to any
166
+ * existing recipients; to remove existing recipients, modify the
167
+ * `encryptedDoc.jwe.recipients` field.
168
+ * @param {Function} [options.keyResolver=this.keyResolver] - A function that
169
+ * returns a Promise that resolves a key ID to a DH public key.
170
+ * @param {object} [options.keyAgreementKey=this.keyAgreementKey] - A
171
+ * KeyAgreementKey API for deriving shared KEKs for wrapping content
172
+ * encryption keys.
173
+ * @param {object} [options.hmac=this.hmac] - An HMAC API for blinding
174
+ * indexable attributes.
175
+ * @param {object} options.transport - The Transport instance to use.
176
+ *
177
+ * @returns {Promise<object>} - Resolves to the updated document.
178
+ */
179
+ async update({ doc, stream, chunkSize, recipients = [], keyResolver = this.keyResolver, keyAgreementKey = this.keyAgreementKey, hmac = this.hmac, transport } = {}) {
180
+ assertDocument(doc);
181
+ assertDocId(doc.id);
182
+ assertTransport(transport);
183
+ // if no recipients specified, add default
184
+ if (recipients.length === 0 && keyAgreementKey) {
185
+ recipients = this._createDefaultRecipients(keyAgreementKey);
186
+ }
187
+ // if a stream was specified, indicate a new stream of data will be
188
+ // associated with this document
189
+ if (stream) {
190
+ // specify stream information
191
+ doc.stream = {
192
+ pending: true
193
+ };
194
+ keyResolver = _createCachedKeyResolver(keyResolver);
195
+ }
196
+ const encrypted = await this._encrypt({
197
+ doc,
198
+ recipients,
199
+ keyResolver,
200
+ hmac,
201
+ update: true
202
+ });
203
+ // send encrypted doc to EDV server
204
+ await transport.update({ encrypted });
205
+ let result = encrypted;
206
+ encrypted.content = doc.content;
207
+ encrypted.meta = doc.meta;
208
+ if (doc.stream !== undefined) {
209
+ encrypted.stream = doc.stream;
210
+ }
211
+ // if a `stream` was given, update it
212
+ if (stream) {
213
+ result = await this._updateStream({
214
+ doc: encrypted,
215
+ stream,
216
+ chunkSize,
217
+ recipients: recipients.slice(),
218
+ keyResolver,
219
+ keyAgreementKey,
220
+ hmac,
221
+ transport
222
+ });
223
+ }
224
+ return result;
225
+ }
226
+ /**
227
+ * Updates an index for the given document, without updating the document
228
+ * contents itself. An index entry will be updated and sent to the EDV; its
229
+ * sequence number must match the document's current sequence number or the
230
+ * update will be rejected with an `InvalidStateError`. Recovery from this
231
+ * error requires fetching the latest document and trying again.
232
+ *
233
+ * Note: If the index does not exist or the document does not have an
234
+ * existing entry for the index, it will be added.
235
+ *
236
+ * @param {object} options - The options to use.
237
+ * @param {object} options.doc - The document to create or update an index
238
+ * for.
239
+ * @param {object} [options.hmac=this.hmac] - An HMAC API for blinding
240
+ * indexable attributes.
241
+ * @param {object} options.transport - The Transport instance to use.
242
+ *
243
+ * @returns {Promise} - Resolves once the operation completes.
244
+ */
245
+ async updateIndex({ doc, hmac = this.hmac, transport } = {}) {
246
+ assertDocument(doc);
247
+ assertDocId(doc.id);
248
+ assertTransport(transport);
249
+ _checkIndexing(hmac);
250
+ const entry = await this.indexHelper.createEntry({ hmac, doc });
251
+ // send index entry to EDV server
252
+ await transport.updateIndex({ docId: doc.id, entry });
253
+ }
254
+ /**
255
+ * Deletes a document from the EDV.
256
+ *
257
+ * @param {object} options - The options to use.
258
+ * @param {object} options.doc - The document to delete.
259
+ * @param {object} [options.recipients=[]] - A set of JWE recipients to
260
+ * encrypt the document for; if present, recipients will be added to
261
+ * any existing recipients; to remove existing recipients, modify
262
+ * the `encryptedDoc.jwe.recipients` field.
263
+ * @param {Function} [options.keyResolver=this.keyResolver] - A function that
264
+ * returns a Promise that resolves a key ID to a DH public key.
265
+ * @param {object} [options.keyAgreementKey=this.keyAgreementKey] - A
266
+ * KeyAgreementKey API for deriving shared KEKs for wrapping content
267
+ * encryption keys.
268
+ * @param {object} options.transport - The Transport instance to use.
269
+ *
270
+ * @returns {Promise<boolean>} - Resolves to `true` if the document was
271
+ * deleted.
272
+ */
273
+ async delete({ doc, recipients = [], keyResolver = this.keyResolver, keyAgreementKey = this.keyAgreementKey, transport } = {}) {
274
+ assertDocument(doc);
275
+ assertDocId(doc.id);
276
+ assertTransport(transport);
277
+ // clear document, preserving only its `id`, `sequence`, and previous
278
+ // encrypted data (to be used to preserve recipients)
279
+ const { id, sequence, jwe } = doc;
280
+ doc = { id, sequence, jwe, content: {}, meta: { deleted: true } };
281
+ await EdvClientCore.prototype.update.call(this, {
282
+ doc,
283
+ recipients,
284
+ keyResolver,
285
+ keyAgreementKey,
286
+ transport
287
+ });
288
+ return true;
289
+ }
290
+ /**
291
+ * Gets a document from the EDV by its ID.
292
+ *
293
+ * @param {object} options - The options to use.
294
+ * @param {string} options.id - The ID of the document to get.
295
+ * @param {object} [options.keyAgreementKey=this.keyAgreementKey] - A
296
+ * KeyAgreementKey API for deriving a shared KEK to unwrap the content
297
+ * encryption key.
298
+ * @param {object} options.transport - The Transport instance to use.
299
+ *
300
+ * @returns {Promise<object>} - Resolves to the document.
301
+ */
302
+ async get({ id, keyAgreementKey = this.keyAgreementKey, transport } = {}) {
303
+ assert(id, 'id', 'string');
304
+ assertTransport(transport);
305
+ // get encrypted doc from EDV server
306
+ const encryptedDoc = await transport.get({ id });
307
+ return this._decrypt({ encryptedDoc, keyAgreementKey });
308
+ }
309
+ /**
310
+ * Gets a `ReadableStream` to read the chunked data associated with a
311
+ * document.
312
+ *
313
+ * @param {object} options - The options to use.
314
+ * @param {object} options.doc - The document to get a stream for.
315
+ * @param {object} [options.keyAgreementKey=this.keyAgreementKey] - A
316
+ * KeyAgreementKey API for deriving a shared KEK to unwrap the content
317
+ * encryption key.
318
+ * @param {object} options.transport - The Transport instance to use.
319
+ *
320
+ * @returns {Promise<ReadableStream>} - Resolves to a `ReadableStream` to read
321
+ * the chunked data from.
322
+ */
323
+ async getStream({ doc, keyAgreementKey = this.keyAgreementKey, transport } = {}) {
324
+ assert(doc, 'doc', 'object');
325
+ assertDocId(doc.id);
326
+ assert(doc.stream, 'doc.stream', 'object');
327
+ assertTransport(transport);
328
+ const { cipher } = this;
329
+ const { id: docId } = doc;
330
+ const state = doc.stream;
331
+ let chunkIndex = 0;
332
+ const stream = new ReadableStream({
333
+ async pull(controller) {
334
+ // Note: user will call `read` on the decrypt stream... which will
335
+ // trigger a pull here
336
+ if (chunkIndex >= state.chunks) {
337
+ // done
338
+ controller.close();
339
+ return;
340
+ }
341
+ // get next chunk and enqueue it for reading
342
+ const chunk = await transport.getChunk({ docId, chunkIndex });
343
+ chunkIndex++;
344
+ controller.enqueue(chunk);
345
+ }
346
+ });
347
+ const decryptStream = await cipher.createDecryptStream({ keyAgreementKey });
348
+ return stream.pipeThrough(decryptStream);
349
+ }
350
+ /**
351
+ * Counts how many documents match a query in an EDV.
352
+ *
353
+ * @see find - For more detailed documentation on the search options.
354
+ *
355
+ * @param {object} options - The options to use.
356
+ * @param {object} [options.keyAgreementKey=this.keyAgreementKey] - A
357
+ * KeyAgreementKey API for deriving a shared KEK to unwrap the content
358
+ * encryption key.
359
+ * @param {object} [options.hmac=this.hmac] - An HMAC API for blinding
360
+ * indexable attributes.
361
+ * @param {object|Array} [options.equals] - An object with key-value
362
+ * attribute pairs to match or an array of such objects.
363
+ * @param {string|Array} [options.has] - A string with an attribute name to
364
+ * match or an array of such strings.
365
+ * @param {object} options.transport - The Transport instance to use.
366
+ *
367
+ * @returns {Promise<number>} - Resolves to the number of matching documents.
368
+ */
369
+ async count({ keyAgreementKey = this.keyAgreementKey, hmac = this.hmac, equals, has, transport } = {}) {
370
+ const { count } = await EdvClientCore.prototype.find.call(this, {
371
+ keyAgreementKey,
372
+ hmac,
373
+ equals,
374
+ has,
375
+ count: true,
376
+ transport
377
+ });
378
+ return count;
379
+ }
380
+ /**
381
+ * Finds documents based on their attributes. Currently, matching can be
382
+ * performed using an `equals` or a `has` filter (but not both at once).
383
+ *
384
+ * The `equals` filter is an object with key-value attribute pairs. Any
385
+ * document that matches *all* given key-value attribute pairs will be
386
+ * returned. If equals is an array, it may contain multiple such filters --
387
+ * whereby the results will be all documents that matched any one of the
388
+ * filters. If the document's value for a matching a key is an array and
389
+ * the array contains a matching value, the document will be considered
390
+ * a match (provided that other key-value attribute pairs also match).
391
+ *
392
+ * The `has` filter is a string representing the attribute name or an
393
+ * array of such strings. If an array is used, then the results will only
394
+ * contain documents that possess *all* of the attributes listed.
395
+ *
396
+ * @param {object} options - The options to use.
397
+ * @param {object} [options.keyAgreementKey=this.keyAgreementKey] - A
398
+ * KeyAgreementKey API for deriving a shared KEK to unwrap the content
399
+ * encryption key.
400
+ * @param {object} [options.hmac=this.hmac] - An HMAC API for blinding
401
+ * indexable attributes.
402
+ * @param {object|Array} [options.equals] - An object with key-value
403
+ * attribute pairs to match or an array of such objects.
404
+ * @param {string|Array} [options.has] - A string with an attribute name to
405
+ * match or an array of such strings.
406
+ * @param {boolean} [options.returnDocuments] - Set to `false` to
407
+ * request only document IDs from the server (not full documents); note
408
+ * that a server that does not accept this option will return full
409
+ * documents, so either return value is possible.
410
+ * @param {boolean} [options.count] - Set to `false` to find all documents
411
+ * that match a query or to `true` to give a count of documents.
412
+ * @param {number} [options.limit] - Set to limit the number of documents
413
+ * to be returned from a query (min=1, max=1000).
414
+ * @param {object} options.transport - The Transport instance to use.
415
+ *
416
+ * @returns {Promise<object>} - Resolves to the matching documents:
417
+ * {documents: [...]}.
418
+ */
419
+ async find({ keyAgreementKey = this.keyAgreementKey, hmac = this.hmac, equals, has, returnDocuments, count = false, limit, transport } = {}) {
420
+ assertTransport(transport);
421
+ _checkIndexing(hmac);
422
+ if (limit !== undefined &&
423
+ !(Number.isSafeInteger(limit) && limit >= 1 && limit <= 1000)) {
424
+ throw new Error('"limit" must be an integer >= 1 and <= 1000.');
425
+ }
426
+ const query = await this.indexHelper.buildQuery({ hmac, equals, has });
427
+ if (returnDocuments !== undefined) {
428
+ query.returnDocuments = !!returnDocuments;
429
+ }
430
+ if (count) {
431
+ query.count = true;
432
+ }
433
+ if (limit !== undefined) {
434
+ query.limit = limit;
435
+ }
436
+ // find results
437
+ const result = await transport.find({ query });
438
+ if (count === true) {
439
+ return result;
440
+ }
441
+ // decrypt documents (if full documents are returned)
442
+ const { documents, documentIds, hasMore } = result;
443
+ let rval;
444
+ if (documentIds) {
445
+ rval = { documentIds };
446
+ }
447
+ else if (documents) {
448
+ const decryptedDocs = await Promise.all(documents.map((encryptedDoc) => this._decrypt({ encryptedDoc, keyAgreementKey })));
449
+ rval = { documents: decryptedDocs };
450
+ }
451
+ if (hasMore !== undefined) {
452
+ rval.hasMore = hasMore;
453
+ }
454
+ return rval;
455
+ }
456
+ /**
457
+ * Gets the configuration for an EDV.
458
+ *
459
+ * @param {object} options - The options to use.
460
+ * @param {string} [options.id] - The ID of the EDV config.
461
+ * @param {object} options.transport - The Transport instance to use.
462
+ *
463
+ * @returns {Promise<object>} - Resolves to the configuration for the EDV.
464
+ */
465
+ async getConfig({ id, transport } = {}) {
466
+ assertTransport(transport);
467
+ return transport.getConfig({ id });
468
+ }
469
+ /**
470
+ * Updates an EDV configuration. The new configuration `sequence` must
471
+ * be incremented by `1` over the previous configuration or the update will
472
+ * fail.
473
+ *
474
+ * @param {object} options - The options to use.
475
+ * @param {object} options.config - The new EDV config.
476
+ * @param {object} options.transport - The Transport instance to use.
477
+ *
478
+ * @returns {Promise} - Resolves once the operation completes.
479
+ */
480
+ async updateConfig({ config, transport } = {}) {
481
+ assertTransport(transport);
482
+ if (!(config && typeof config === 'object')) {
483
+ throw new TypeError('"config" must be an object.');
484
+ }
485
+ if (!(config.controller && typeof config.controller === 'string')) {
486
+ throw new TypeError('"config.controller" must be a string.');
487
+ }
488
+ return transport.updateConfig({ config });
489
+ }
490
+ /**
491
+ * Generates a multibase encoded random 128-bit identifier for a document.
492
+ *
493
+ * @returns {Promise<string>} - Resolves to the identifier.
494
+ */
495
+ static async generateId() {
496
+ // 128-bit random number, multibase encoded
497
+ // 0x00 = identity tag, 0x10 = length (16 bytes) + 16 random bytes
498
+ const buf = new Uint8Array(18);
499
+ buf[0] = 0x00;
500
+ buf[1] = 0x10;
501
+ const random = new Uint8Array(buf.buffer, buf.byteOffset + 2, 16);
502
+ await getRandomBytes(random);
503
+ // multibase encoding for base58 starts with 'z'
504
+ return 'z' + base58btc.encode(buf);
505
+ }
506
+ // expose `generateId` on instance as well
507
+ async generateId() {
508
+ return EdvClientCore.generateId();
509
+ }
510
+ // helper to create default recipients
511
+ _createDefaultRecipients(keyAgreementKey) {
512
+ return keyAgreementKey
513
+ ? [
514
+ {
515
+ header: {
516
+ kid: keyAgreementKey.id,
517
+ // only supported algorithm
518
+ alg: 'ECDH-ES+A256KW'
519
+ }
520
+ }
521
+ ]
522
+ : [];
523
+ }
524
+ // helper that decrypts an encrypted doc to include its (cleartext) content
525
+ async _decrypt({ encryptedDoc, keyAgreementKey }) {
526
+ // validate `encryptedDoc`
527
+ assert(encryptedDoc, 'encryptedDoc', 'object');
528
+ assert(encryptedDoc.id, 'encryptedDoc.id', 'string');
529
+ assert(encryptedDoc.jwe, 'encryptedDoc.jwe', 'object');
530
+ // decrypt doc content
531
+ const { cipher } = this;
532
+ const { jwe } = encryptedDoc;
533
+ const data = await cipher.decryptObject({ jwe, keyAgreementKey });
534
+ if (data === null) {
535
+ throw new Error('Decryption failed.');
536
+ }
537
+ const { content, meta, stream } = data;
538
+ // append decrypted content, meta, and stream
539
+ const doc = { ...encryptedDoc, content, meta };
540
+ if (stream !== undefined) {
541
+ doc.stream = stream;
542
+ }
543
+ return doc;
544
+ }
545
+ // helper that creates an encrypted doc using a doc's (clear) content,
546
+ // meta, and stream ... and blinding any attributes for indexing
547
+ async _encrypt({ doc, recipients, keyResolver, hmac, update }) {
548
+ const encrypted = { ...doc };
549
+ if (!encrypted.meta) {
550
+ encrypted.meta = {};
551
+ }
552
+ /* Note: There is an assumption that EDVs will be ported in their
553
+ entirety. If the contents of a single EDV document is to be copied to
554
+ another EDV, it should receive a new EDV document ID on the target EDV. No
555
+ EDV document with the same ID should live on more than one EDV unless those
556
+ EDVs are intended to be mirrors of one another. This reduces
557
+ synchronization issues to a sequence number instead of something more
558
+ complicated involving digests and other synchronization complexities. */
559
+ if (update) {
560
+ if ('sequence' in encrypted) {
561
+ // Sequence is limited to MAX_SAFE_INTEGER - 1 to avoid unexpected
562
+ // behavior when a client attempts to increment the sequence number.
563
+ if (!Number.isSafeInteger(encrypted.sequence)) {
564
+ throw new Error('"sequence" must be a non-negative safe integer.');
565
+ }
566
+ if (!(encrypted.sequence < Number.MAX_SAFE_INTEGER - 1)) {
567
+ throw new Error('"sequence" is too large.');
568
+ }
569
+ encrypted.sequence++;
570
+ }
571
+ else {
572
+ encrypted.sequence = 0;
573
+ }
574
+ }
575
+ else {
576
+ // sequence must be zero for new docs
577
+ if ('sequence' in encrypted && encrypted.sequence !== 0) {
578
+ throw new Error(`Invalid "sequence" for a new document: ${encrypted.sequence}.`);
579
+ }
580
+ encrypted.sequence = 0;
581
+ }
582
+ const { cipher, indexHelper } = this;
583
+ // include existing recipients
584
+ if (encrypted.jwe && encrypted.jwe.recipients) {
585
+ const prev = encrypted.jwe.recipients.slice();
586
+ if (recipients) {
587
+ // add any new recipients
588
+ for (const recipient of recipients) {
589
+ if (!_findRecipient(prev, recipient)) {
590
+ prev.push(recipient);
591
+ }
592
+ }
593
+ }
594
+ recipients = prev;
595
+ }
596
+ else if (!(Array.isArray(recipients) && recipients.length > 0)) {
597
+ throw new TypeError('"recipients" must be a non-empty array.');
598
+ }
599
+ // update indexed entries and jwe
600
+ const { content, meta, stream } = doc;
601
+ const obj = { content, meta };
602
+ if (stream !== undefined) {
603
+ obj.stream = stream;
604
+ }
605
+ const [indexed, jwe] = await Promise.all([
606
+ hmac
607
+ ? indexHelper.updateEntry({ hmac, doc: encrypted })
608
+ : doc.indexed || [],
609
+ cipher.encryptObject({ obj, recipients, keyResolver })
610
+ ]);
611
+ delete encrypted.content;
612
+ delete encrypted.meta;
613
+ if (encrypted.stream) {
614
+ encrypted.stream = {
615
+ sequence: encrypted.stream.sequence,
616
+ chunks: encrypted.stream.chunks
617
+ };
618
+ }
619
+ encrypted.indexed = indexed;
620
+ encrypted.jwe = jwe;
621
+ return encrypted;
622
+ }
623
+ // helper that creates or updates a stream of data associated with a doc
624
+ async _updateStream({ doc, stream, chunkSize = DEFAULT_CHUNK_SIZE, recipients, keyResolver, keyAgreementKey, hmac, transport }) {
625
+ const { cipher } = this;
626
+ const encryptStream = await cipher.createEncryptStream({
627
+ recipients,
628
+ keyResolver,
629
+ chunkSize
630
+ });
631
+ // // TODO: tee `stream` to digest stream as well
632
+ // const [forDigest, forStorage] = stream.tee();
633
+ // const digestPromise = forDigest.pipeTo(_createDigestStream());
634
+ // pipe user supplied `stream` through the encrypt stream
635
+ //const readable = forStorage.pipeThrough(encryptStream);
636
+ const readable = stream.pipeThrough(encryptStream);
637
+ const reader = readable.getReader();
638
+ // continually read from encrypt stream and upload result
639
+ const { id: docId } = doc;
640
+ let value;
641
+ let done;
642
+ let chunks = 0;
643
+ while (!done) {
644
+ // read next encrypted chunk
645
+ ;
646
+ ({ value, done } = await reader.read());
647
+ if (!value) {
648
+ break;
649
+ }
650
+ // create chunk
651
+ chunks++;
652
+ const chunk = {
653
+ sequence: doc.sequence,
654
+ ...value
655
+ };
656
+ // TODO: in theory could do encryption and sending in parallel, they
657
+ // are safely independent operations, consider this optimization
658
+ await transport.storeChunk({ docId, chunk });
659
+ }
660
+ // TODO: await digest from tee'd stream
661
+ // const contentHash = await digestPromise();
662
+ // write total number of chunks and digest of plaintext in doc update
663
+ doc.stream = {
664
+ sequence: doc.sequence,
665
+ chunks
666
+ //contentHash
667
+ };
668
+ return EdvClientCore.prototype.update.call(this, {
669
+ doc,
670
+ recipients,
671
+ keyResolver,
672
+ keyAgreementKey,
673
+ hmac,
674
+ transport
675
+ });
676
+ }
677
+ }
678
+ function _checkIndexing(hmac) {
679
+ if (!hmac) {
680
+ throw Error('Indexing disabled; no HMAC specified.');
681
+ }
682
+ }
683
+ function _findRecipient(recipients, recipient) {
684
+ const { kid, alg } = recipient.header;
685
+ return recipients.find((r) => r.header.kid === kid && r.header.alg === alg);
686
+ }
687
+ function _createCachedKeyResolver(keyResolver) {
688
+ const cache = new Map();
689
+ return async ({ id }) => {
690
+ let promise = cache.get(id);
691
+ if (promise) {
692
+ return promise;
693
+ }
694
+ cache.set(id, (promise = keyResolver({ id })));
695
+ return promise;
696
+ };
697
+ }
698
+ //# sourceMappingURL=EdvClientCore.js.map