@enbox/dwn-sdk-js 0.4.14 → 0.4.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/browser.mjs +1 -1
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/src/core/constants.js +2 -0
- package/dist/esm/src/core/constants.js.map +1 -1
- package/dist/esm/src/core/dwn-error.js +2 -0
- package/dist/esm/src/core/dwn-error.js.map +1 -1
- package/dist/esm/src/core/protocol-authorization-validation.js +9 -0
- package/dist/esm/src/core/protocol-authorization-validation.js.map +1 -1
- package/dist/esm/src/handlers/protocols-configure.js +157 -7
- package/dist/esm/src/handlers/protocols-configure.js.map +1 -1
- package/dist/esm/src/index.js +2 -2
- package/dist/esm/src/index.js.map +1 -1
- package/dist/esm/src/protocols/encryption.js +41 -10
- package/dist/esm/src/protocols/encryption.js.map +1 -1
- package/dist/esm/src/types/protocols-types.js.map +1 -1
- package/dist/esm/tests/features/protocol-composition.spec.js +5 -1
- package/dist/esm/tests/features/protocol-composition.spec.js.map +1 -1
- package/dist/esm/tests/handlers/protocols-configure.spec.js +244 -1
- package/dist/esm/tests/handlers/protocols-configure.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-read.spec.js +2 -1
- package/dist/esm/tests/handlers/records-read.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-write.spec.js +24 -1
- package/dist/esm/tests/handlers/records-write.spec.js.map +1 -1
- package/dist/esm/tests/protocols/encryption.spec.js +3 -1
- package/dist/esm/tests/protocols/encryption.spec.js.map +1 -1
- package/dist/types/src/core/constants.d.ts +2 -0
- package/dist/types/src/core/constants.d.ts.map +1 -1
- package/dist/types/src/core/dwn-error.d.ts +2 -0
- package/dist/types/src/core/dwn-error.d.ts.map +1 -1
- package/dist/types/src/core/protocol-authorization-validation.d.ts.map +1 -1
- package/dist/types/src/handlers/protocols-configure.d.ts +17 -0
- package/dist/types/src/handlers/protocols-configure.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +2 -2
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/protocols/encryption.d.ts +3 -1
- package/dist/types/src/protocols/encryption.d.ts.map +1 -1
- package/dist/types/src/types/protocols-types.d.ts +2 -1
- package/dist/types/src/types/protocols-types.d.ts.map +1 -1
- package/dist/types/tests/features/protocol-composition.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/protocols-configure.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/records-read.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/records-write.spec.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/constants.ts +2 -0
- package/src/core/dwn-error.ts +2 -0
- package/src/core/protocol-authorization-validation.ts +14 -0
- package/src/handlers/protocols-configure.ts +236 -7
- package/src/index.ts +3 -0
- package/src/protocols/encryption.ts +54 -11
- package/src/types/protocols-types.ts +2 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Filter } from '../types/query-types.js';
|
|
1
2
|
import type { GenericMessageReply } from '../types/message-types.js';
|
|
2
3
|
import type { ProgressToken } from '../types/subscriptions.js';
|
|
3
4
|
import type { RecordsWriteMessage } from '../types/records-types.js';
|
|
@@ -6,12 +7,14 @@ import type { HandlerDependencies, MethodHandler } from '../types/method-handler
|
|
|
6
7
|
import type { ProtocolDefinition, ProtocolRuleSet, ProtocolsConfigureMessage } from '../types/protocols-types.js';
|
|
7
8
|
|
|
8
9
|
import { authenticate } from '../core/auth.js';
|
|
10
|
+
import { isEncryptionControlPath } from '../core/constants.js';
|
|
9
11
|
import { Message } from '../core/message.js';
|
|
10
12
|
import { messageReplyFromError } from '../core/message-reply.js';
|
|
11
13
|
import { ProtocolAuthorization } from '../core/protocol-authorization.js';
|
|
12
14
|
import { ProtocolsConfigure } from '../interfaces/protocols-configure.js';
|
|
13
15
|
import { ProtocolsGrantAuthorization } from '../core/protocols-grant-authorization.js';
|
|
14
16
|
import { RecordsWrite } from '../interfaces/records-write.js';
|
|
17
|
+
import { resolveProtocolTypesForPath } from '../core/protocol-authorization-validation.js';
|
|
15
18
|
import { StorageController } from '../store/storage-controller.js';
|
|
16
19
|
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
|
|
17
20
|
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
|
|
@@ -22,6 +25,7 @@ type StoredInitialWriteConfigValidity = 'valid' | 'invalid' | 'unknown';
|
|
|
22
25
|
const STORED_INITIAL_WRITE_CONFIG_INVALID_CODES = new Set<string>([
|
|
23
26
|
DwnErrorCode.EncryptionControlValidateAudienceRolePathInvalid,
|
|
24
27
|
DwnErrorCode.EncryptionControlValidateAudienceSealKeyIdMismatch,
|
|
28
|
+
DwnErrorCode.ProtocolAuthorizationEncryptionNotAllowed,
|
|
25
29
|
DwnErrorCode.ProtocolAuthorizationEncryptionRequired,
|
|
26
30
|
DwnErrorCode.ProtocolAuthorizationIncorrectDataFormat,
|
|
27
31
|
DwnErrorCode.ProtocolAuthorizationInitialWriteRevalidationNotInitial,
|
|
@@ -91,8 +95,20 @@ export class ProtocolsConfigureHandler implements MethodHandler {
|
|
|
91
95
|
}
|
|
92
96
|
}
|
|
93
97
|
|
|
98
|
+
let newestMessage = await Message.getNewestMessage(existingMessages) as ProtocolsConfigureMessage | undefined;
|
|
99
|
+
let prefetchedRecordsWrites: RecordsWriteMessage[] | undefined;
|
|
100
|
+
try {
|
|
101
|
+
prefetchedRecordsWrites = await this.validateEncryptionPolicyIsImmutable(
|
|
102
|
+
tenant,
|
|
103
|
+
message.descriptor.definition,
|
|
104
|
+
message.descriptor.messageTimestamp,
|
|
105
|
+
newestMessage,
|
|
106
|
+
);
|
|
107
|
+
} catch (e) {
|
|
108
|
+
return messageReplyFromError(e, 400);
|
|
109
|
+
}
|
|
110
|
+
|
|
94
111
|
// find newest message, and if the incoming message is the newest
|
|
95
|
-
let newestMessage = await Message.getNewestMessage(existingMessages);
|
|
96
112
|
let incomingMessageIsNewest = false;
|
|
97
113
|
if (newestMessage === undefined || await Message.isNewer(message, newestMessage)) {
|
|
98
114
|
incomingMessageIsNewest = true;
|
|
@@ -135,7 +151,11 @@ export class ProtocolsConfigureHandler implements MethodHandler {
|
|
|
135
151
|
}
|
|
136
152
|
}
|
|
137
153
|
|
|
138
|
-
await this.purgeRecordsInvalidatedByProtocolConfig(
|
|
154
|
+
await this.purgeRecordsInvalidatedByProtocolConfig(
|
|
155
|
+
tenant,
|
|
156
|
+
message.descriptor.definition.protocol,
|
|
157
|
+
prefetchedRecordsWrites,
|
|
158
|
+
);
|
|
139
159
|
|
|
140
160
|
return messageReply;
|
|
141
161
|
};
|
|
@@ -183,21 +203,230 @@ export class ProtocolsConfigureHandler implements MethodHandler {
|
|
|
183
203
|
}
|
|
184
204
|
}
|
|
185
205
|
|
|
186
|
-
|
|
206
|
+
/**
|
|
207
|
+
* Prevents a protocol URI from changing the at-rest representation of a path
|
|
208
|
+
* after records have been admitted under that path. A policy migration must use
|
|
209
|
+
* a new protocol URI so one store never contains both plaintext and ciphertext
|
|
210
|
+
* records governed by the same protocol version.
|
|
211
|
+
*/
|
|
212
|
+
private async validateEncryptionPolicyIsImmutable(
|
|
213
|
+
tenant: string,
|
|
214
|
+
incomingDefinition: ProtocolDefinition,
|
|
215
|
+
incomingTimestamp: string,
|
|
216
|
+
previousConfiguration: ProtocolsConfigureMessage | undefined,
|
|
217
|
+
): Promise<RecordsWriteMessage[] | undefined> {
|
|
218
|
+
if (previousConfiguration === undefined || !ProtocolsConfigureHandler.hasEncryptionPolicyChange(
|
|
219
|
+
previousConfiguration.descriptor.definition,
|
|
220
|
+
incomingDefinition,
|
|
221
|
+
)) {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const { messages } = await this.deps.messageStore.query(tenant, [{
|
|
226
|
+
interface : DwnInterfaceName.Records,
|
|
227
|
+
method : DwnMethodName.Write,
|
|
228
|
+
protocol : incomingDefinition.protocol,
|
|
229
|
+
}]);
|
|
230
|
+
const recordsWrites = messages as RecordsWriteMessage[];
|
|
231
|
+
const incomingPolicyByPath = new Map<string, boolean>();
|
|
232
|
+
for (const recordsWrite of recordsWrites) {
|
|
233
|
+
const { protocolPath } = recordsWrite.descriptor;
|
|
234
|
+
if (isEncryptionControlPath(protocolPath)) {
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Removing a path does not change its representation policy. Existing
|
|
239
|
+
// records remain governed by the configuration active at their timestamp.
|
|
240
|
+
if (getRuleSetAtPath(protocolPath, incomingDefinition.structure) === undefined) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let incomingEncryptionRequired = incomingPolicyByPath.get(protocolPath);
|
|
245
|
+
if (incomingEncryptionRequired === undefined) {
|
|
246
|
+
const incomingTypes = await resolveProtocolTypesForPath(
|
|
247
|
+
tenant,
|
|
248
|
+
protocolPath,
|
|
249
|
+
incomingDefinition,
|
|
250
|
+
this.deps.validationStateReader,
|
|
251
|
+
incomingTimestamp,
|
|
252
|
+
);
|
|
253
|
+
incomingEncryptionRequired = incomingTypes[getTypeName(protocolPath)]?.encryptionRequired === true;
|
|
254
|
+
incomingPolicyByPath.set(protocolPath, incomingEncryptionRequired);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if ((recordsWrite.encryption !== undefined) === incomingEncryptionRequired) {
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
throw new DwnError(
|
|
262
|
+
DwnErrorCode.ProtocolsConfigureEncryptionPolicyImmutable,
|
|
263
|
+
`cannot change encryption policy for protocol path '${protocolPath}' after records exist; ` +
|
|
264
|
+
'install the changed definition under a new protocol URI.'
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
await this.validateComposedEncryptionPolicyIsImmutable(tenant, incomingDefinition);
|
|
269
|
+
return recordsWrites;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** Prevents a referenced protocol from changing the representation of records stored by composing protocols. */
|
|
273
|
+
private async validateComposedEncryptionPolicyIsImmutable(
|
|
274
|
+
tenant: string,
|
|
275
|
+
incomingDefinition: ProtocolDefinition,
|
|
276
|
+
): Promise<void> {
|
|
277
|
+
const { messages: configurationMessages } = await this.deps.messageStore.query(tenant, [{
|
|
278
|
+
interface : DwnInterfaceName.Protocols,
|
|
279
|
+
isLatestBaseState : true,
|
|
280
|
+
method : DwnMethodName.Configure,
|
|
281
|
+
}]);
|
|
282
|
+
const policiesByProtocol = ProtocolsConfigureHandler.getComposedPathPolicies(
|
|
283
|
+
configurationMessages as ProtocolsConfigureMessage[],
|
|
284
|
+
incomingDefinition,
|
|
285
|
+
);
|
|
286
|
+
if (policiesByProtocol.size === 0) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const { messages: dependentRecordsWrites } = await this.deps.messageStore.query(
|
|
291
|
+
tenant,
|
|
292
|
+
[...policiesByProtocol.keys()].map((protocol): Filter => ({
|
|
293
|
+
interface : DwnInterfaceName.Records,
|
|
294
|
+
method : DwnMethodName.Write,
|
|
295
|
+
protocol,
|
|
296
|
+
})),
|
|
297
|
+
);
|
|
298
|
+
for (const message of dependentRecordsWrites) {
|
|
299
|
+
const recordsWrite = message as RecordsWriteMessage;
|
|
300
|
+
const { protocol, protocolPath } = recordsWrite.descriptor;
|
|
301
|
+
if (protocol === undefined) {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const encryptionRequired = policiesByProtocol.get(protocol)?.get(protocolPath);
|
|
306
|
+
if (encryptionRequired === undefined || (recordsWrite.encryption !== undefined) === encryptionRequired) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
throw new DwnError(
|
|
311
|
+
DwnErrorCode.ProtocolsConfigureEncryptionPolicyImmutable,
|
|
312
|
+
`cannot change encryption policy for protocol path '${protocolPath}' imported by protocol '${protocol}' ` +
|
|
313
|
+
`after records exist; install the changed definition under a new protocol URI.`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** Maps each installed composing protocol to root `$ref` paths whose policy comes from the incoming definition. */
|
|
319
|
+
private static getComposedPathPolicies(
|
|
320
|
+
configurations: ProtocolsConfigureMessage[],
|
|
321
|
+
incomingDefinition: ProtocolDefinition,
|
|
322
|
+
): Map<string, Map<string, boolean>> {
|
|
323
|
+
const policiesByProtocol = new Map<string, Map<string, boolean>>();
|
|
324
|
+
for (const configuration of configurations) {
|
|
325
|
+
const composingDefinition = configuration.descriptor.definition;
|
|
326
|
+
const pathPolicies = ProtocolsConfigureHandler.getImportedPathPolicies(
|
|
327
|
+
composingDefinition,
|
|
328
|
+
incomingDefinition,
|
|
329
|
+
);
|
|
330
|
+
if (pathPolicies.size > 0) {
|
|
331
|
+
policiesByProtocol.set(composingDefinition.protocol, pathPolicies);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return policiesByProtocol;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/** Resolves the effective policy for root `$ref` paths that import one definition. */
|
|
339
|
+
private static getImportedPathPolicies(
|
|
340
|
+
composingDefinition: ProtocolDefinition,
|
|
341
|
+
incomingDefinition: ProtocolDefinition,
|
|
342
|
+
): Map<string, boolean> {
|
|
343
|
+
const pathPolicies = new Map<string, boolean>();
|
|
344
|
+
const importedAliases = new Set(
|
|
345
|
+
Object.entries(composingDefinition.uses ?? {})
|
|
346
|
+
.filter(([, protocol]): boolean => protocol === incomingDefinition.protocol)
|
|
347
|
+
.map(([alias]): string => alias),
|
|
348
|
+
);
|
|
349
|
+
if (importedAliases.size === 0) {
|
|
350
|
+
return pathPolicies;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
for (const [protocolPath, ruleSet] of Object.entries(composingDefinition.structure)) {
|
|
354
|
+
const reference = ruleSet.$ref === undefined ? undefined : parseCrossProtocolRef(ruleSet.$ref);
|
|
355
|
+
if (reference === undefined || !importedAliases.has(reference.alias)) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const typeName = getTypeName(protocolPath);
|
|
360
|
+
pathPolicies.set(protocolPath, incomingDefinition.types[typeName]?.encryptionRequired === true);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return pathPolicies;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/** Returns true when a configure can change a path's stored representation. */
|
|
367
|
+
private static hasEncryptionPolicyChange(
|
|
368
|
+
previousDefinition: ProtocolDefinition,
|
|
369
|
+
incomingDefinition: ProtocolDefinition,
|
|
370
|
+
): boolean {
|
|
371
|
+
const typeNames = new Set([
|
|
372
|
+
...Object.keys(previousDefinition.types),
|
|
373
|
+
...Object.keys(incomingDefinition.types),
|
|
374
|
+
]);
|
|
375
|
+
for (const typeName of typeNames) {
|
|
376
|
+
const previousRequired = previousDefinition.types[typeName]?.encryptionRequired === true;
|
|
377
|
+
const incomingRequired = incomingDefinition.types[typeName]?.encryptionRequired === true;
|
|
378
|
+
if (previousRequired !== incomingRequired) {
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return ProtocolsConfigureHandler.getCompositionEncryptionPolicySignature(previousDefinition) !==
|
|
384
|
+
ProtocolsConfigureHandler.getCompositionEncryptionPolicySignature(incomingDefinition);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** Captures only composition fields that can redirect a path to another type policy. */
|
|
388
|
+
private static getCompositionEncryptionPolicySignature(definition: ProtocolDefinition): string {
|
|
389
|
+
const references: string[] = [];
|
|
390
|
+
const visit = (ruleSet: ProtocolRuleSet, parentPath: string): void => {
|
|
391
|
+
for (const [typeName, value] of Object.entries(ruleSet)) {
|
|
392
|
+
if (typeName.startsWith('$')) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const childRuleSet = value as ProtocolRuleSet;
|
|
397
|
+
const protocolPath = parentPath === '' ? typeName : `${parentPath}/${typeName}`;
|
|
398
|
+
if (childRuleSet.$ref !== undefined) {
|
|
399
|
+
references.push(`${protocolPath}=${childRuleSet.$ref}`);
|
|
400
|
+
}
|
|
401
|
+
visit(childRuleSet, protocolPath);
|
|
402
|
+
}
|
|
403
|
+
};
|
|
404
|
+
visit(definition.structure as ProtocolRuleSet, '');
|
|
405
|
+
|
|
406
|
+
const uses = Object.entries(definition.uses ?? {})
|
|
407
|
+
.sort(([left], [right]): number => left.localeCompare(right));
|
|
408
|
+
references.sort((left, right): number => left.localeCompare(right));
|
|
409
|
+
return JSON.stringify({ references, uses });
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private async purgeRecordsInvalidatedByProtocolConfig(
|
|
413
|
+
tenant: string,
|
|
414
|
+
protocol: string,
|
|
415
|
+
prefetchedRecordsWrites?: RecordsWriteMessage[],
|
|
416
|
+
): Promise<void> {
|
|
187
417
|
const dataStore = this.deps.dataStore;
|
|
188
418
|
if (dataStore === undefined) {
|
|
189
419
|
return;
|
|
190
420
|
}
|
|
191
421
|
|
|
192
|
-
const
|
|
422
|
+
const recordsWrites = prefetchedRecordsWrites ?? (await this.deps.messageStore.query(tenant, [{
|
|
193
423
|
interface : DwnInterfaceName.Records,
|
|
194
424
|
method : DwnMethodName.Write,
|
|
195
425
|
protocol,
|
|
196
|
-
}]);
|
|
426
|
+
}])).messages as RecordsWriteMessage[];
|
|
197
427
|
|
|
198
428
|
const checkedRecordIds = new Set<string>();
|
|
199
|
-
for (const
|
|
200
|
-
const recordsWriteMessage = message as RecordsWriteMessage;
|
|
429
|
+
for (const recordsWriteMessage of recordsWrites) {
|
|
201
430
|
if (checkedRecordIds.has(recordsWriteMessage.recordId)) {
|
|
202
431
|
continue;
|
|
203
432
|
}
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,8 @@ export {
|
|
|
25
25
|
ENCRYPTION_CONTROL_PATHS,
|
|
26
26
|
ENCRYPTION_CONTROL_ROOT_PATH,
|
|
27
27
|
ENCRYPTION_PROTOCOL_URI,
|
|
28
|
+
ENCRYPTION_PROTOCOL_GRANT_KEY_PATH,
|
|
29
|
+
ENCRYPTION_PROTOCOL_WRAPPED_GRANT_KEY_PATH,
|
|
28
30
|
PERMISSIONS_REVOCATION_PATH,
|
|
29
31
|
isEncryptionControlPath,
|
|
30
32
|
} from './core/constants.js';
|
|
@@ -98,6 +100,7 @@ export {
|
|
|
98
100
|
getRoleAudienceContextId,
|
|
99
101
|
getRoleContextPrefix,
|
|
100
102
|
getRuleSetAtPath,
|
|
103
|
+
getTypeName,
|
|
101
104
|
isCrossProtocolRef,
|
|
102
105
|
parseCrossProtocolRef,
|
|
103
106
|
} from './utils/protocols.js';
|
|
@@ -10,12 +10,16 @@ import type { ContentEncryptionAlgorithm, X25519KeyEncryptionBase } from '../uti
|
|
|
10
10
|
|
|
11
11
|
import { DwnConstant } from '../core/dwn-constant.js';
|
|
12
12
|
import { Encoder } from '../utils/encoder.js';
|
|
13
|
-
import { ENCRYPTION_PROTOCOL_URI } from '../core/constants.js';
|
|
14
13
|
import { FilterUtility } from '../utils/filter.js';
|
|
15
14
|
import { Message } from '../core/message.js';
|
|
16
15
|
import { Records } from '../utils/records.js';
|
|
17
16
|
import { validateJsonSchema } from '../schema-validator.js';
|
|
18
17
|
import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
|
|
18
|
+
import {
|
|
19
|
+
ENCRYPTION_PROTOCOL_GRANT_KEY_PATH,
|
|
20
|
+
ENCRYPTION_PROTOCOL_URI,
|
|
21
|
+
ENCRYPTION_PROTOCOL_WRAPPED_GRANT_KEY_PATH,
|
|
22
|
+
} from '../core/constants.js';
|
|
19
23
|
import { grantKeyScopeCoversDeliveredScope, isGrantKeyEligibleRecordsScope, isGrantKeyRecordsScope } from '../utils/grant-key-coverage.js';
|
|
20
24
|
|
|
21
25
|
export const WRAPPED_GRANT_KEY_FORMAT = 'enbox/wrapped-grant-key@1';
|
|
@@ -44,15 +48,21 @@ export function assertWrappedGrantKeyEnvelope(envelope: unknown): asserts envelo
|
|
|
44
48
|
|
|
45
49
|
export class EncryptionProtocol implements CoreProtocol {
|
|
46
50
|
public static readonly uri = ENCRYPTION_PROTOCOL_URI;
|
|
47
|
-
public static readonly grantKeyPath =
|
|
51
|
+
public static readonly grantKeyPath = ENCRYPTION_PROTOCOL_GRANT_KEY_PATH;
|
|
52
|
+
public static readonly wrappedGrantKeyPath = ENCRYPTION_PROTOCOL_WRAPPED_GRANT_KEY_PATH;
|
|
48
53
|
|
|
49
54
|
public static readonly definition: ProtocolDefinition = {
|
|
50
55
|
published : true,
|
|
51
56
|
protocol : EncryptionProtocol.uri,
|
|
52
57
|
types : {
|
|
53
58
|
grantKey: {
|
|
59
|
+
dataFormats : ['application/json'],
|
|
60
|
+
encryptionRequired : true,
|
|
61
|
+
schema : 'https://identity.foundation/dwn/json-schemas/encryption/grant-key.json',
|
|
62
|
+
},
|
|
63
|
+
wrappedGrantKey: {
|
|
54
64
|
dataFormats : ['application/json'],
|
|
55
|
-
schema : 'https://identity.foundation/dwn/json-schemas/encryption/grant-key.json',
|
|
65
|
+
schema : 'https://identity.foundation/dwn/json-schemas/encryption/wrapped-grant-key-envelope.json',
|
|
56
66
|
},
|
|
57
67
|
},
|
|
58
68
|
structure: {
|
|
@@ -71,6 +81,21 @@ export class EncryptionProtocol implements CoreProtocol {
|
|
|
71
81
|
protocolPath : { type: 'string' },
|
|
72
82
|
},
|
|
73
83
|
},
|
|
84
|
+
wrappedGrantKey: {
|
|
85
|
+
$actions: [
|
|
86
|
+
{ can: ['create'], who: 'anyone' },
|
|
87
|
+
{ can: ['read'], of: 'wrappedGrantKey', who: 'recipient' },
|
|
88
|
+
],
|
|
89
|
+
$immutable : true,
|
|
90
|
+
$tags : {
|
|
91
|
+
$allowUndefinedTags : false,
|
|
92
|
+
$requiredTags : ['grantId', 'protocol', 'keyId'],
|
|
93
|
+
grantId : { type: 'string' },
|
|
94
|
+
keyId : { maxLength: 43, minLength: 43, pattern: '^[A-Za-z0-9_-]{43}$', type: 'string' },
|
|
95
|
+
protocol : { type: 'string' },
|
|
96
|
+
protocolPath : { type: 'string' },
|
|
97
|
+
},
|
|
98
|
+
},
|
|
74
99
|
}
|
|
75
100
|
};
|
|
76
101
|
|
|
@@ -87,13 +112,13 @@ export class EncryptionProtocol implements CoreProtocol {
|
|
|
87
112
|
message: RecordsWriteMessage,
|
|
88
113
|
validationStateReader: ValidationStateReader,
|
|
89
114
|
): Promise<void> {
|
|
90
|
-
if (message.descriptor.protocolPath
|
|
115
|
+
if (EncryptionProtocol.isGrantKeyPath(message.descriptor.protocolPath)) {
|
|
91
116
|
await EncryptionProtocol.preProcessGrantKey(tenant, message, validationStateReader);
|
|
92
117
|
}
|
|
93
118
|
}
|
|
94
119
|
|
|
95
120
|
public async validateRecord(message: RecordsWriteMessage, dataBytes: Uint8Array): Promise<void> {
|
|
96
|
-
if (message.descriptor.protocolPath
|
|
121
|
+
if (EncryptionProtocol.isGrantKeyPath(message.descriptor.protocolPath)) {
|
|
97
122
|
EncryptionProtocol.verifyGrantKeyDelivery(message, dataBytes);
|
|
98
123
|
return;
|
|
99
124
|
}
|
|
@@ -109,7 +134,7 @@ export class EncryptionProtocol implements CoreProtocol {
|
|
|
109
134
|
message: RecordsWriteMessage,
|
|
110
135
|
validationStateReader: ValidationStateReader,
|
|
111
136
|
): Promise<void> {
|
|
112
|
-
EncryptionProtocol.
|
|
137
|
+
EncryptionProtocol.verifyGrantKeyRepresentation(message);
|
|
113
138
|
|
|
114
139
|
const grantId = EncryptionProtocol.getRequiredStringTag(message, 'grantId');
|
|
115
140
|
const protocol = EncryptionProtocol.getRequiredStringTag(message, 'protocol');
|
|
@@ -130,21 +155,34 @@ export class EncryptionProtocol implements CoreProtocol {
|
|
|
130
155
|
});
|
|
131
156
|
}
|
|
132
157
|
|
|
133
|
-
private static
|
|
134
|
-
if (message.
|
|
158
|
+
private static verifyGrantKeyRepresentation(message: RecordsWriteMessage): void {
|
|
159
|
+
if (message.descriptor.protocolPath === EncryptionProtocol.grantKeyPath) {
|
|
160
|
+
if (message.encryption === undefined) {
|
|
161
|
+
throw new DwnError(
|
|
162
|
+
DwnErrorCode.EncryptionProtocolValidateEncryptedDeliveryMissingEncryption,
|
|
163
|
+
'grantKey records must be encrypted.'
|
|
164
|
+
);
|
|
165
|
+
}
|
|
135
166
|
return;
|
|
136
167
|
}
|
|
137
168
|
|
|
169
|
+
if (message.encryption !== undefined) {
|
|
170
|
+
throw new DwnError(
|
|
171
|
+
DwnErrorCode.ProtocolAuthorizationEncryptionNotAllowed,
|
|
172
|
+
'wrappedGrantKey records must be plaintext at the DWN record level.'
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
138
176
|
if (message.descriptor.dataSize > DwnConstant.maxDataSizeAllowedToBeEncoded) {
|
|
139
177
|
throw new DwnError(
|
|
140
178
|
DwnErrorCode.EncryptionProtocolValidateEncryptedDeliveryMissingEncryption,
|
|
141
|
-
'
|
|
179
|
+
'wrappedGrantKey records must carry an inline wrapped grantKey envelope.'
|
|
142
180
|
);
|
|
143
181
|
}
|
|
144
182
|
}
|
|
145
183
|
|
|
146
184
|
private static verifyGrantKeyDelivery(message: RecordsWriteMessage, dataBytes: Uint8Array): void {
|
|
147
|
-
if (message.
|
|
185
|
+
if (message.descriptor.protocolPath === EncryptionProtocol.grantKeyPath) {
|
|
148
186
|
return;
|
|
149
187
|
}
|
|
150
188
|
|
|
@@ -159,11 +197,16 @@ export class EncryptionProtocol implements CoreProtocol {
|
|
|
159
197
|
const detail = error instanceof Error ? error.message : String(error);
|
|
160
198
|
throw new DwnError(
|
|
161
199
|
DwnErrorCode.EncryptionProtocolValidateEncryptedDeliveryMissingEncryption,
|
|
162
|
-
`${message.descriptor.protocolPath} records must
|
|
200
|
+
`${message.descriptor.protocolPath} records must carry a valid wrapped grantKey envelope: ${detail}`
|
|
163
201
|
);
|
|
164
202
|
}
|
|
165
203
|
}
|
|
166
204
|
|
|
205
|
+
private static isGrantKeyPath(protocolPath: string): boolean {
|
|
206
|
+
return protocolPath === EncryptionProtocol.grantKeyPath ||
|
|
207
|
+
protocolPath === EncryptionProtocol.wrappedGrantKeyPath;
|
|
208
|
+
}
|
|
209
|
+
|
|
167
210
|
private static getRequiredStringTag(message: RecordsWriteMessage, tag: string): string {
|
|
168
211
|
const value = message.descriptor.tags?.[tag];
|
|
169
212
|
if (typeof value !== 'string') {
|
|
@@ -53,7 +53,8 @@ export type ProtocolType = {
|
|
|
53
53
|
* DID must have an X25519 keyAgreement key; protocol installation will
|
|
54
54
|
* fail if it does not.
|
|
55
55
|
*
|
|
56
|
-
* When `false` or omitted,
|
|
56
|
+
* When `false` or omitted, records of this type **must** be plaintext at the
|
|
57
|
+
* DWN record level. Applications cannot override this protocol policy per write.
|
|
57
58
|
*/
|
|
58
59
|
encryptionRequired?: boolean,
|
|
59
60
|
};
|