@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.
- package/LICENSE +27 -0
- package/README.md +276 -0
- package/dist/EdvClient.d.ts +412 -0
- package/dist/EdvClient.d.ts.map +1 -0
- package/dist/EdvClient.js +663 -0
- package/dist/EdvClient.js.map +1 -0
- package/dist/EdvClientCore.d.ts +264 -0
- package/dist/EdvClientCore.d.ts.map +1 -0
- package/dist/EdvClientCore.js +698 -0
- package/dist/EdvClientCore.js.map +1 -0
- package/dist/EdvDocument.d.ts +92 -0
- package/dist/EdvDocument.d.ts.map +1 -0
- package/dist/EdvDocument.js +149 -0
- package/dist/EdvDocument.js.map +1 -0
- package/dist/HttpsTransport.d.ts +87 -0
- package/dist/HttpsTransport.d.ts.map +1 -0
- package/dist/HttpsTransport.js +415 -0
- package/dist/HttpsTransport.js.map +1 -0
- package/dist/IndexHelper.d.ts +163 -0
- package/dist/IndexHelper.d.ts.map +1 -0
- package/dist/IndexHelper.js +539 -0
- package/dist/IndexHelper.js.map +1 -0
- package/dist/LegacyIndexHelperVersion1.d.ts +150 -0
- package/dist/LegacyIndexHelperVersion1.d.ts.map +1 -0
- package/dist/LegacyIndexHelperVersion1.js +475 -0
- package/dist/LegacyIndexHelperVersion1.js.map +1 -0
- package/dist/Transport.d.ts +142 -0
- package/dist/Transport.d.ts.map +1 -0
- package/dist/Transport.js +181 -0
- package/dist/Transport.js.map +1 -0
- package/dist/assert.d.ts +6 -0
- package/dist/assert.d.ts.map +1 -0
- package/dist/assert.js +61 -0
- package/dist/assert.js.map +1 -0
- package/dist/baseX.d.ts +7 -0
- package/dist/baseX.d.ts.map +1 -0
- package/dist/baseX.js +8 -0
- package/dist/baseX.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/util.d.ts +3 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +13 -0
- package/dist/util.js.map +1 -0
- 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
|