@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.
Files changed (50) hide show
  1. package/dist/browser.mjs +1 -1
  2. package/dist/browser.mjs.map +4 -4
  3. package/dist/esm/src/core/constants.js +2 -0
  4. package/dist/esm/src/core/constants.js.map +1 -1
  5. package/dist/esm/src/core/dwn-error.js +2 -0
  6. package/dist/esm/src/core/dwn-error.js.map +1 -1
  7. package/dist/esm/src/core/protocol-authorization-validation.js +9 -0
  8. package/dist/esm/src/core/protocol-authorization-validation.js.map +1 -1
  9. package/dist/esm/src/handlers/protocols-configure.js +157 -7
  10. package/dist/esm/src/handlers/protocols-configure.js.map +1 -1
  11. package/dist/esm/src/index.js +2 -2
  12. package/dist/esm/src/index.js.map +1 -1
  13. package/dist/esm/src/protocols/encryption.js +41 -10
  14. package/dist/esm/src/protocols/encryption.js.map +1 -1
  15. package/dist/esm/src/types/protocols-types.js.map +1 -1
  16. package/dist/esm/tests/features/protocol-composition.spec.js +5 -1
  17. package/dist/esm/tests/features/protocol-composition.spec.js.map +1 -1
  18. package/dist/esm/tests/handlers/protocols-configure.spec.js +244 -1
  19. package/dist/esm/tests/handlers/protocols-configure.spec.js.map +1 -1
  20. package/dist/esm/tests/handlers/records-read.spec.js +2 -1
  21. package/dist/esm/tests/handlers/records-read.spec.js.map +1 -1
  22. package/dist/esm/tests/handlers/records-write.spec.js +24 -1
  23. package/dist/esm/tests/handlers/records-write.spec.js.map +1 -1
  24. package/dist/esm/tests/protocols/encryption.spec.js +3 -1
  25. package/dist/esm/tests/protocols/encryption.spec.js.map +1 -1
  26. package/dist/types/src/core/constants.d.ts +2 -0
  27. package/dist/types/src/core/constants.d.ts.map +1 -1
  28. package/dist/types/src/core/dwn-error.d.ts +2 -0
  29. package/dist/types/src/core/dwn-error.d.ts.map +1 -1
  30. package/dist/types/src/core/protocol-authorization-validation.d.ts.map +1 -1
  31. package/dist/types/src/handlers/protocols-configure.d.ts +17 -0
  32. package/dist/types/src/handlers/protocols-configure.d.ts.map +1 -1
  33. package/dist/types/src/index.d.ts +2 -2
  34. package/dist/types/src/index.d.ts.map +1 -1
  35. package/dist/types/src/protocols/encryption.d.ts +3 -1
  36. package/dist/types/src/protocols/encryption.d.ts.map +1 -1
  37. package/dist/types/src/types/protocols-types.d.ts +2 -1
  38. package/dist/types/src/types/protocols-types.d.ts.map +1 -1
  39. package/dist/types/tests/features/protocol-composition.spec.d.ts.map +1 -1
  40. package/dist/types/tests/handlers/protocols-configure.spec.d.ts.map +1 -1
  41. package/dist/types/tests/handlers/records-read.spec.d.ts.map +1 -1
  42. package/dist/types/tests/handlers/records-write.spec.d.ts.map +1 -1
  43. package/package.json +1 -1
  44. package/src/core/constants.ts +2 -0
  45. package/src/core/dwn-error.ts +2 -0
  46. package/src/core/protocol-authorization-validation.ts +14 -0
  47. package/src/handlers/protocols-configure.ts +236 -7
  48. package/src/index.ts +3 -0
  49. package/src/protocols/encryption.ts +54 -11
  50. 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(tenant, message.descriptor.definition.protocol);
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
- private async purgeRecordsInvalidatedByProtocolConfig(tenant: string, protocol: string): Promise<void> {
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 { messages } = await this.deps.messageStore.query(tenant, [{
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 message of messages) {
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 = 'grantKey';
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 === EncryptionProtocol.grantKeyPath) {
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 === EncryptionProtocol.grantKeyPath) {
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.verifyInlineWrappedGrantKeyDelivery(message);
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 verifyInlineWrappedGrantKeyDelivery(message: RecordsWriteMessage): void {
134
- if (message.encryption !== undefined) {
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
- 'plaintext grantKey records must carry an inline wrapped grantKey envelope.'
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.encryption !== undefined) {
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 be encrypted or carry a valid wrapped grantKey envelope: ${detail}`
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, encryption is not required for this type.
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
  };