@enbox/dwn-sdk-js 0.4.3 → 0.4.5

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 (204) hide show
  1. package/dist/browser.mjs +15 -15
  2. package/dist/browser.mjs.map +4 -4
  3. package/dist/esm/generated/precompiled-validators.js +5406 -2200
  4. package/dist/esm/generated/precompiled-validators.js.map +1 -1
  5. package/dist/esm/json-schemas/definitions.json +18 -0
  6. package/dist/esm/src/core/constants.js +3 -3
  7. package/dist/esm/src/core/constants.js.map +1 -1
  8. package/dist/esm/src/core/dwn-error.js +29 -1
  9. package/dist/esm/src/core/dwn-error.js.map +1 -1
  10. package/dist/esm/src/core/messages-grant-authorization.js +20 -0
  11. package/dist/esm/src/core/messages-grant-authorization.js.map +1 -1
  12. package/dist/esm/src/core/protocol-authorization-validation.js +80 -3
  13. package/dist/esm/src/core/protocol-authorization-validation.js.map +1 -1
  14. package/dist/esm/src/core/recording-validation-state-reader.js +5 -0
  15. package/dist/esm/src/core/recording-validation-state-reader.js.map +1 -1
  16. package/dist/esm/src/core/replication-apply.js +163 -7
  17. package/dist/esm/src/core/replication-apply.js.map +1 -1
  18. package/dist/esm/src/core/validation-state-reader.js +20 -0
  19. package/dist/esm/src/core/validation-state-reader.js.map +1 -1
  20. package/dist/esm/src/dwn.js +19 -2
  21. package/dist/esm/src/dwn.js.map +1 -1
  22. package/dist/esm/src/handlers/messages-query.js +3 -5
  23. package/dist/esm/src/handlers/messages-query.js.map +1 -1
  24. package/dist/esm/src/handlers/protocols-configure.js +10 -4
  25. package/dist/esm/src/handlers/protocols-configure.js.map +1 -1
  26. package/dist/esm/src/handlers/records-write.js +1 -1
  27. package/dist/esm/src/handlers/records-write.js.map +1 -1
  28. package/dist/esm/src/index.js +2 -1
  29. package/dist/esm/src/index.js.map +1 -1
  30. package/dist/esm/src/interfaces/protocols-configure.js +23 -10
  31. package/dist/esm/src/interfaces/protocols-configure.js.map +1 -1
  32. package/dist/esm/src/interfaces/records-write-signing.js +2 -8
  33. package/dist/esm/src/interfaces/records-write-signing.js.map +1 -1
  34. package/dist/esm/src/interfaces/records-write.js +43 -65
  35. package/dist/esm/src/interfaces/records-write.js.map +1 -1
  36. package/dist/esm/src/protocols/encryption.js +411 -0
  37. package/dist/esm/src/protocols/encryption.js.map +1 -0
  38. package/dist/esm/src/protocols/permission-grant.js +3 -3
  39. package/dist/esm/src/protocols/permission-grant.js.map +1 -1
  40. package/dist/esm/src/protocols/permissions.js +1 -0
  41. package/dist/esm/src/protocols/permissions.js.map +1 -1
  42. package/dist/esm/src/types/message-types.js.map +1 -1
  43. package/dist/esm/src/types/permission-types.js.map +1 -1
  44. package/dist/esm/src/types/protocols-types.js.map +1 -1
  45. package/dist/esm/src/utils/encryption.js +135 -187
  46. package/dist/esm/src/utils/encryption.js.map +1 -1
  47. package/dist/esm/src/utils/hd-key.js +0 -1
  48. package/dist/esm/src/utils/hd-key.js.map +1 -1
  49. package/dist/esm/src/utils/permission-scope.js +9 -6
  50. package/dist/esm/src/utils/permission-scope.js.map +1 -1
  51. package/dist/esm/src/utils/protocols.js +12 -9
  52. package/dist/esm/src/utils/protocols.js.map +1 -1
  53. package/dist/esm/src/utils/records.js +54 -56
  54. package/dist/esm/src/utils/records.js.map +1 -1
  55. package/dist/esm/src/utils/replication.js +27 -4
  56. package/dist/esm/src/utils/replication.js.map +1 -1
  57. package/dist/esm/tests/core/replication-apply.spec.js +141 -1
  58. package/dist/esm/tests/core/replication-apply.spec.js.map +1 -1
  59. package/dist/esm/tests/core/replication-replay-property.spec.js +1 -0
  60. package/dist/esm/tests/core/replication-replay-property.spec.js.map +1 -1
  61. package/dist/esm/tests/core/validation-read-closure.spec.js +111 -0
  62. package/dist/esm/tests/core/validation-read-closure.spec.js.map +1 -1
  63. package/dist/esm/tests/core/validation-state-reader.spec.js +89 -0
  64. package/dist/esm/tests/core/validation-state-reader.spec.js.map +1 -1
  65. package/dist/esm/tests/features/protocol-composition.spec.js +30 -31
  66. package/dist/esm/tests/features/protocol-composition.spec.js.map +1 -1
  67. package/dist/esm/tests/fuzz/encryption.fuzz.spec.js +29 -121
  68. package/dist/esm/tests/fuzz/encryption.fuzz.spec.js.map +1 -1
  69. package/dist/esm/tests/handlers/messages-query.spec.js +71 -3
  70. package/dist/esm/tests/handlers/messages-query.spec.js.map +1 -1
  71. package/dist/esm/tests/handlers/messages-read.spec.js +2 -2
  72. package/dist/esm/tests/handlers/messages-read.spec.js.map +1 -1
  73. package/dist/esm/tests/handlers/records-read.spec.js +9 -135
  74. package/dist/esm/tests/handlers/records-read.spec.js.map +1 -1
  75. package/dist/esm/tests/handlers/records-write.spec.js +279 -19
  76. package/dist/esm/tests/handlers/records-write.spec.js.map +1 -1
  77. package/dist/esm/tests/interfaces/protocols-configure.spec.js +17 -22
  78. package/dist/esm/tests/interfaces/protocols-configure.spec.js.map +1 -1
  79. package/dist/esm/tests/interfaces/records-write.spec.js +170 -150
  80. package/dist/esm/tests/interfaces/records-write.spec.js.map +1 -1
  81. package/dist/esm/tests/protocols/encryption.spec.js +914 -0
  82. package/dist/esm/tests/protocols/encryption.spec.js.map +1 -0
  83. package/dist/esm/tests/protocols/permission-grant.spec.js +55 -0
  84. package/dist/esm/tests/protocols/permission-grant.spec.js.map +1 -1
  85. package/dist/esm/tests/scenarios/end-to-end-tests.spec.js +100 -100
  86. package/dist/esm/tests/scenarios/end-to-end-tests.spec.js.map +1 -1
  87. package/dist/esm/tests/store/message-store-level.spec.js +20 -2
  88. package/dist/esm/tests/store/message-store-level.spec.js.map +1 -1
  89. package/dist/esm/tests/utils/encryption-callbacks.spec.js +88 -125
  90. package/dist/esm/tests/utils/encryption-callbacks.spec.js.map +1 -1
  91. package/dist/esm/tests/utils/encryption.spec.js +125 -255
  92. package/dist/esm/tests/utils/encryption.spec.js.map +1 -1
  93. package/dist/esm/tests/utils/permission-scope.spec.js +5 -1
  94. package/dist/esm/tests/utils/permission-scope.spec.js.map +1 -1
  95. package/dist/esm/tests/utils/records.spec.js +15 -5
  96. package/dist/esm/tests/utils/records.spec.js.map +1 -1
  97. package/dist/esm/tests/utils/test-data-generator.js +7 -44
  98. package/dist/esm/tests/utils/test-data-generator.js.map +1 -1
  99. package/dist/esm/tests/validation/json-schemas/encryption/grant-key.spec.js +63 -0
  100. package/dist/esm/tests/validation/json-schemas/encryption/grant-key.spec.js.map +1 -0
  101. package/dist/types/generated/precompiled-validators.d.ts +140 -92
  102. package/dist/types/generated/precompiled-validators.d.ts.map +1 -1
  103. package/dist/types/src/core/constants.d.ts +2 -2
  104. package/dist/types/src/core/constants.d.ts.map +1 -1
  105. package/dist/types/src/core/core-protocol.d.ts +1 -1
  106. package/dist/types/src/core/core-protocol.d.ts.map +1 -1
  107. package/dist/types/src/core/dwn-error.d.ts +29 -1
  108. package/dist/types/src/core/dwn-error.d.ts.map +1 -1
  109. package/dist/types/src/core/messages-grant-authorization.d.ts +2 -0
  110. package/dist/types/src/core/messages-grant-authorization.d.ts.map +1 -1
  111. package/dist/types/src/core/protocol-authorization-validation.d.ts +2 -2
  112. package/dist/types/src/core/protocol-authorization-validation.d.ts.map +1 -1
  113. package/dist/types/src/core/recording-validation-state-reader.d.ts +9 -0
  114. package/dist/types/src/core/recording-validation-state-reader.d.ts.map +1 -1
  115. package/dist/types/src/core/replication-apply.d.ts +4 -3
  116. package/dist/types/src/core/replication-apply.d.ts.map +1 -1
  117. package/dist/types/src/core/validation-state-reader.d.ts +9 -0
  118. package/dist/types/src/core/validation-state-reader.d.ts.map +1 -1
  119. package/dist/types/src/dwn.d.ts +1 -0
  120. package/dist/types/src/dwn.d.ts.map +1 -1
  121. package/dist/types/src/handlers/messages-query.d.ts.map +1 -1
  122. package/dist/types/src/handlers/protocols-configure.d.ts.map +1 -1
  123. package/dist/types/src/index.d.ts +6 -5
  124. package/dist/types/src/index.d.ts.map +1 -1
  125. package/dist/types/src/interfaces/protocols-configure.d.ts +1 -1
  126. package/dist/types/src/interfaces/protocols-configure.d.ts.map +1 -1
  127. package/dist/types/src/interfaces/records-write-signing.d.ts +4 -8
  128. package/dist/types/src/interfaces/records-write-signing.d.ts.map +1 -1
  129. package/dist/types/src/interfaces/records-write.d.ts +10 -26
  130. package/dist/types/src/interfaces/records-write.d.ts.map +1 -1
  131. package/dist/types/src/protocols/encryption.d.ts +43 -0
  132. package/dist/types/src/protocols/encryption.d.ts.map +1 -0
  133. package/dist/types/src/protocols/permission-grant.d.ts +3 -6
  134. package/dist/types/src/protocols/permission-grant.d.ts.map +1 -1
  135. package/dist/types/src/protocols/permissions.d.ts +2 -1
  136. package/dist/types/src/protocols/permissions.d.ts.map +1 -1
  137. package/dist/types/src/types/encryption-types.d.ts +5 -38
  138. package/dist/types/src/types/encryption-types.d.ts.map +1 -1
  139. package/dist/types/src/types/message-types.d.ts +0 -13
  140. package/dist/types/src/types/message-types.d.ts.map +1 -1
  141. package/dist/types/src/types/permission-types.d.ts +44 -9
  142. package/dist/types/src/types/permission-types.d.ts.map +1 -1
  143. package/dist/types/src/types/protocols-types.d.ts +9 -14
  144. package/dist/types/src/types/protocols-types.d.ts.map +1 -1
  145. package/dist/types/src/types/records-types.d.ts +3 -3
  146. package/dist/types/src/types/records-types.d.ts.map +1 -1
  147. package/dist/types/src/types/validation-state-reader.d.ts +12 -0
  148. package/dist/types/src/types/validation-state-reader.d.ts.map +1 -1
  149. package/dist/types/src/utils/encryption.d.ts +54 -145
  150. package/dist/types/src/utils/encryption.d.ts.map +1 -1
  151. package/dist/types/src/utils/hd-key.d.ts +1 -1
  152. package/dist/types/src/utils/hd-key.d.ts.map +1 -1
  153. package/dist/types/src/utils/permission-scope.d.ts +4 -3
  154. package/dist/types/src/utils/permission-scope.d.ts.map +1 -1
  155. package/dist/types/src/utils/protocols.d.ts +3 -5
  156. package/dist/types/src/utils/protocols.d.ts.map +1 -1
  157. package/dist/types/src/utils/records.d.ts +5 -13
  158. package/dist/types/src/utils/records.d.ts.map +1 -1
  159. package/dist/types/src/utils/replication.d.ts +3 -0
  160. package/dist/types/src/utils/replication.d.ts.map +1 -1
  161. package/dist/types/tests/features/protocol-composition.spec.d.ts.map +1 -1
  162. package/dist/types/tests/handlers/messages-query.spec.d.ts.map +1 -1
  163. package/dist/types/tests/handlers/records-read.spec.d.ts.map +1 -1
  164. package/dist/types/tests/handlers/records-write.spec.d.ts.map +1 -1
  165. package/dist/types/tests/protocols/encryption.spec.d.ts +2 -0
  166. package/dist/types/tests/protocols/encryption.spec.d.ts.map +1 -0
  167. package/dist/types/tests/scenarios/end-to-end-tests.spec.d.ts.map +1 -1
  168. package/dist/types/tests/utils/test-data-generator.d.ts +0 -5
  169. package/dist/types/tests/utils/test-data-generator.d.ts.map +1 -1
  170. package/dist/types/tests/validation/json-schemas/encryption/grant-key.spec.d.ts +2 -0
  171. package/dist/types/tests/validation/json-schemas/encryption/grant-key.spec.d.ts.map +1 -0
  172. package/package.json +3 -3
  173. package/src/core/constants.ts +3 -3
  174. package/src/core/core-protocol.ts +1 -1
  175. package/src/core/dwn-error.ts +29 -1
  176. package/src/core/messages-grant-authorization.ts +27 -0
  177. package/src/core/protocol-authorization-validation.ts +145 -6
  178. package/src/core/recording-validation-state-reader.ts +13 -0
  179. package/src/core/replication-apply.ts +222 -8
  180. package/src/core/validation-state-reader.ts +30 -0
  181. package/src/dwn.ts +24 -2
  182. package/src/handlers/messages-query.ts +6 -5
  183. package/src/handlers/protocols-configure.ts +19 -3
  184. package/src/handlers/records-write.ts +1 -1
  185. package/src/index.ts +15 -5
  186. package/src/interfaces/protocols-configure.ts +39 -12
  187. package/src/interfaces/records-write-signing.ts +5 -12
  188. package/src/interfaces/records-write.ts +75 -92
  189. package/src/protocols/encryption.ts +649 -0
  190. package/src/protocols/permission-grant.ts +4 -5
  191. package/src/protocols/permissions.ts +8 -6
  192. package/src/types/encryption-types.ts +9 -38
  193. package/src/types/message-types.ts +0 -13
  194. package/src/types/permission-types.ts +57 -9
  195. package/src/types/protocols-types.ts +9 -16
  196. package/src/types/records-types.ts +3 -3
  197. package/src/types/validation-state-reader.ts +13 -0
  198. package/src/utils/encryption.ts +213 -283
  199. package/src/utils/hd-key.ts +1 -1
  200. package/src/utils/permission-scope.ts +10 -6
  201. package/src/utils/protocols.ts +16 -15
  202. package/src/utils/records.ts +86 -71
  203. package/src/utils/replication.ts +30 -4
  204. package/dist/esm/tests/vectors/protocol-definitions/chat.json +0 -70
@@ -2,387 +2,317 @@ import type { Jwk } from '@enbox/crypto';
2
2
  import type { PublicKeyJwk } from '../types/jose-types.js';
3
3
 
4
4
  import { concatBytes } from '@noble/ciphers/utils';
5
+ import { AesCtr, AesKw, computeJwkThumbprint, Hkdf, X25519 } from '@enbox/crypto';
6
+
5
7
  import { Encoder } from './encoder.js';
6
8
  import { KeyDerivationScheme } from './hd-key.js';
7
- import { AesGcm, AesKw, ConcatKdf, X25519, XChaCha20Poly1305 } from '@enbox/crypto';
9
+ import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
8
10
 
9
- /**
10
- * Content encryption algorithms supported by the DWN.
11
- * Both are AEAD (Authenticated Encryption with Associated Data) ciphers.
12
- */
13
11
  export enum ContentEncryptionAlgorithm {
14
- /** AES-256 in Galois/Counter Mode. NIST-approved, hardware-accelerated. 96-bit nonce. */
15
- A256GCM = 'A256GCM',
16
- /** XChaCha20-Poly1305. 192-bit nonce (safe to randomize). Constant-time. */
17
- XC20P = 'XC20P',
12
+ A256CTR = 'A256CTR',
18
13
  }
19
14
 
20
- /**
21
- * Key agreement algorithm used by the DWN.
22
- * ECDH-ES with X25519 key agreement and AES-256 Key Wrap.
23
- */
24
15
  export enum KeyAgreementAlgorithm {
25
- EcdhEsA256kw = 'ECDH-ES+A256KW',
16
+ X25519HkdfSha256A256Kw = 'X25519-HKDF-SHA256+A256KW',
26
17
  }
27
18
 
28
- /** Size of the AES-GCM authentication tag in bytes. */
29
- const AES_GCM_TAG_LENGTH_BYTES = 16;
19
+ export const ROLE_AUDIENCE_DERIVATION_SCHEME = 'roleAudience';
30
20
 
31
- /** Size of the Poly1305 authentication tag in bytes. */
32
- const POLY1305_TAG_LENGTH_BYTES = 16;
21
+ const AES_256_KEY_LENGTH_BYTES = 32;
22
+ const AES_CTR_COUNTER_LENGTH_BYTES = 16;
23
+ const AES_CTR_COUNTER_LENGTH_BITS = 128;
24
+ const KEK_INFO_PREFIX = KeyAgreementAlgorithm.X25519HkdfSha256A256Kw;
33
25
 
34
- /**
35
- * JWE Protected Header for DWN encryption.
36
- */
37
- export type JweProtectedHeader = {
38
- alg: KeyAgreementAlgorithm;
39
- enc: ContentEncryptionAlgorithm;
26
+ export type ProtocolPathKeyEncryption = {
27
+ algorithm: KeyAgreementAlgorithm.X25519HkdfSha256A256Kw;
28
+ keyId: string;
29
+ derivationScheme: KeyDerivationScheme.ProtocolPath;
30
+ ephemeralPublicKey: PublicKeyJwk;
31
+ encryptedKey: string;
40
32
  };
41
33
 
42
- /**
43
- * Per-recipient header in a JWE General JSON Serialization.
44
- */
45
- export type JweRecipientHeader = {
46
- /** Fully qualified key ID of the root key used in key derivation (e.g. did:example:alice#enc). */
47
- kid: string;
48
- /** Ephemeral X25519 public key used for ECDH key agreement. */
49
- epk: PublicKeyJwk;
50
- /** Key derivation scheme used to derive the recipient's key. */
51
- derivationScheme: KeyDerivationScheme;
52
- /** Derived public key. Present when derivationScheme is 'protocolContext'. */
53
- derivedPublicKey?: PublicKeyJwk;
34
+ export type RoleAudienceKeyEncryption = {
35
+ algorithm: KeyAgreementAlgorithm.X25519HkdfSha256A256Kw;
36
+ keyId: string;
37
+ derivationScheme: typeof ROLE_AUDIENCE_DERIVATION_SCHEME;
38
+ protocol: string;
39
+ role: string;
40
+ epoch: number;
41
+ ephemeralPublicKey: PublicKeyJwk;
42
+ encryptedKey: string;
54
43
  };
55
44
 
56
- /**
57
- * A single recipient entry in the JWE General JSON Serialization.
58
- */
59
- export type JweRecipient = {
60
- header: JweRecipientHeader;
61
- encrypted_key: string;
45
+ export type KeyEncryption = ProtocolPathKeyEncryption | RoleAudienceKeyEncryption;
46
+
47
+ export type DwnEncryption = {
48
+ algorithm: ContentEncryptionAlgorithm.A256CTR;
49
+ initializationVector: string;
50
+ keyEncryption: KeyEncryption[];
62
51
  };
63
52
 
64
- /**
65
- * JWE-inspired structure used as the `encryption` property on RecordsWrite messages.
66
- *
67
- * This follows the JWE General JSON Serialization (RFC 7516 Section 7.2) with one adaptation:
68
- * the `ciphertext` is NOT included here because the encrypted record data is stored separately
69
- * in the DataStore (or as inline `encodedData`). Only the key wrapping metadata, IV, and
70
- * authentication tag are stored in this structure.
71
- */
72
- export type JweEncryption = {
73
- /** Base64url-encoded JWE Protected Header. */
74
- protected: string;
75
- /** Base64url-encoded initialization vector for content encryption. */
76
- iv: string;
77
- /** Base64url-encoded authentication tag from the AEAD cipher. */
78
- tag: string;
79
- /** Array of recipient entries, one per recipient or derivation path. */
80
- recipients: JweRecipient[];
53
+ export type ProtocolPathKeyEncryptionInput = {
54
+ keyId: string;
55
+ publicKey: PublicKeyJwk;
56
+ derivationScheme: KeyDerivationScheme.ProtocolPath;
81
57
  };
82
58
 
83
- /**
84
- * Input describing how to encrypt a key for a single recipient.
85
- */
86
- export type KeyEncryptionInput = {
87
- /** Fully qualified key ID of the recipient's root encryption key. */
88
- publicKeyId: string;
89
- /** The recipient's derived X25519 public key. */
59
+ export type RoleAudienceKeyEncryptionInput = {
60
+ keyId: string;
90
61
  publicKey: PublicKeyJwk;
91
- /** Key derivation scheme. */
92
- derivationScheme: KeyDerivationScheme;
93
- /** Algorithm for key agreement. Defaults to ECDH-ES+A256KW. */
94
- algorithm?: KeyAgreementAlgorithm;
62
+ derivationScheme: typeof ROLE_AUDIENCE_DERIVATION_SCHEME;
63
+ protocol: string;
64
+ role: string;
65
+ epoch: number;
95
66
  };
96
67
 
97
- /**
98
- * Input describing how to encrypt record data.
99
- */
68
+ export type KeyEncryptionInput = ProtocolPathKeyEncryptionInput | RoleAudienceKeyEncryptionInput;
69
+
100
70
  export type EncryptionInput = {
101
- /** Content encryption algorithm. Defaults to A256GCM. */
102
71
  algorithm?: ContentEncryptionAlgorithm;
103
- /** The Content Encryption Key (CEK). Must be 32 bytes (256-bit). */
104
72
  key: Uint8Array;
105
- /** Initialization vector. 12 bytes for A256GCM, 24 bytes for XC20P. */
106
73
  initializationVector: Uint8Array;
107
- /** Authentication tag from the AEAD encryption of the record data. */
108
- authenticationTag: Uint8Array;
109
- /** Recipient key encryption inputs. */
110
74
  keyEncryptionInputs: KeyEncryptionInput[];
111
75
  };
112
76
 
113
- /**
114
- * Payload passed to a KeyDecrypter callback for JWE-based key unwrapping.
115
- */
116
- export type JweKeyUnwrapPayload = {
117
- /** The wrapped CEK bytes. */
77
+ export type KeyUnwrapPayload = {
118
78
  encryptedKey: Uint8Array;
119
- /** The ephemeral X25519 public key used for ECDH. */
120
79
  ephemeralPublicKey: PublicKeyJwk;
80
+ keyEncryption: KeyEncryption;
121
81
  };
122
82
 
123
- /**
124
- * Utility class for DWN encryption operations using JWE (RFC 7516).
125
- * Uses ECDH-ES+A256KW key agreement with X25519 and either AES-256-GCM or XChaCha20-Poly1305
126
- * for authenticated content encryption.
127
- */
128
83
  export class Encryption {
129
-
130
- /**
131
- * Encrypts data using an AEAD cipher (A256GCM or XC20P).
132
- * Returns ciphertext with the authentication tag appended.
133
- */
134
- public static async aeadEncrypt(
84
+ public static async encrypt(
135
85
  algorithm: ContentEncryptionAlgorithm,
136
86
  keyBytes: Uint8Array,
137
- iv: Uint8Array,
87
+ initializationVector: Uint8Array,
138
88
  plaintext: Uint8Array,
139
- ): Promise<{ ciphertext: Uint8Array; tag: Uint8Array }> {
140
- if (algorithm === ContentEncryptionAlgorithm.A256GCM) {
141
- const keyJwk: Jwk = { kty: 'oct', k: Encoder.bytesToBase64Url(keyBytes), alg: 'A256GCM' };
142
- // Web Crypto AES-GCM returns ciphertext || tag
143
- const combined = await AesGcm.encrypt({ data: plaintext, iv, key: keyJwk });
144
- const ciphertext = combined.slice(0, combined.length - AES_GCM_TAG_LENGTH_BYTES);
145
- const tag = combined.slice(combined.length - AES_GCM_TAG_LENGTH_BYTES);
146
- return { ciphertext, tag };
147
-
148
- } else if (algorithm === ContentEncryptionAlgorithm.XC20P) {
149
- // @noble/ciphers XChaCha20-Poly1305 returns ciphertext || tag
150
- const combined = await XChaCha20Poly1305.encryptRaw({ data: plaintext, keyBytes, nonce: iv });
151
- const ciphertext = combined.slice(0, combined.length - POLY1305_TAG_LENGTH_BYTES);
152
- const tag = combined.slice(combined.length - POLY1305_TAG_LENGTH_BYTES);
153
- return { ciphertext, tag };
154
-
155
- } else {
156
- throw new Error(`Unsupported content encryption algorithm: ${algorithm as string}`);
157
- }
89
+ ): Promise<Uint8Array> {
90
+ Encryption.validateContentEncryptionParameters(algorithm, keyBytes, initializationVector);
91
+ return AesCtr.encrypt({
92
+ counter : initializationVector,
93
+ data : plaintext,
94
+ key : Encryption.toA256CtrJwk(keyBytes),
95
+ length : AES_CTR_COUNTER_LENGTH_BITS,
96
+ });
158
97
  }
159
98
 
160
- /**
161
- * Decrypts data using an AEAD cipher (A256GCM or XC20P).
162
- * Expects ciphertext and tag as separate inputs.
163
- */
164
- public static async aeadDecrypt(
99
+ public static async decrypt(
165
100
  algorithm: ContentEncryptionAlgorithm,
166
101
  keyBytes: Uint8Array,
167
- iv: Uint8Array,
102
+ initializationVector: Uint8Array,
168
103
  ciphertext: Uint8Array,
169
- tag: Uint8Array,
170
104
  ): Promise<Uint8Array> {
171
- // Both Web Crypto (AES-GCM) and @noble/ciphers (XChaCha20-Poly1305) expect ciphertext || tag
172
- const combined = concatBytes(ciphertext, tag);
173
-
174
- if (algorithm === ContentEncryptionAlgorithm.A256GCM) {
175
- const keyJwk: Jwk = { kty: 'oct', k: Encoder.bytesToBase64Url(keyBytes), alg: 'A256GCM' };
176
- return AesGcm.decrypt({ data: combined, iv, key: keyJwk });
177
-
178
- } else if (algorithm === ContentEncryptionAlgorithm.XC20P) {
179
- return XChaCha20Poly1305.decryptRaw({ data: combined, keyBytes, nonce: iv });
180
-
181
- } else {
182
- throw new Error(`Unsupported content encryption algorithm: ${algorithm as string}`);
183
- }
105
+ Encryption.validateContentEncryptionParameters(algorithm, keyBytes, initializationVector);
106
+ return AesCtr.decrypt({
107
+ counter : initializationVector,
108
+ data : ciphertext,
109
+ key : Encryption.toA256CtrJwk(keyBytes),
110
+ length : AES_CTR_COUNTER_LENGTH_BITS,
111
+ });
184
112
  }
185
113
 
186
- /**
187
- * Encrypts data as a ReadableStream using an AEAD cipher.
188
- * Collects all chunks, encrypts, and returns a new stream of ciphertext || tag.
189
- * The iv and tag are NOT embedded in the stream — they are stored in the JWE structure.
190
- */
191
- public static async aeadEncryptStream(
114
+ public static async encryptStream(
192
115
  algorithm: ContentEncryptionAlgorithm,
193
116
  keyBytes: Uint8Array,
194
- iv: Uint8Array,
117
+ initializationVector: Uint8Array,
195
118
  plaintextStream: ReadableStream<Uint8Array>,
196
- ): Promise<{ ciphertextStream: ReadableStream<Uint8Array>; tag: Uint8Array }> {
119
+ ): Promise<ReadableStream<Uint8Array>> {
197
120
  const plaintext = await Encryption.readStream(plaintextStream);
198
- const { ciphertext, tag } = await Encryption.aeadEncrypt(algorithm, keyBytes, iv, plaintext);
199
- const ciphertextStream = new ReadableStream<Uint8Array>({
200
- start(controller): void {
201
- controller.enqueue(ciphertext);
202
- controller.close();
203
- }
204
- });
205
- return { ciphertextStream, tag };
121
+ const ciphertext = await Encryption.encrypt(algorithm, keyBytes, initializationVector, plaintext);
122
+ return Encryption.toStream(ciphertext);
206
123
  }
207
124
 
208
- /**
209
- * Decrypts a ciphertext stream using an AEAD cipher.
210
- * Returns a ReadableStream of plaintext.
211
- */
212
- public static async aeadDecryptStream(
125
+ public static async decryptStream(
213
126
  algorithm: ContentEncryptionAlgorithm,
214
127
  keyBytes: Uint8Array,
215
- iv: Uint8Array,
128
+ initializationVector: Uint8Array,
216
129
  ciphertextStream: ReadableStream<Uint8Array>,
217
- tag: Uint8Array,
218
130
  ): Promise<ReadableStream<Uint8Array>> {
219
131
  const ciphertext = await Encryption.readStream(ciphertextStream);
220
- const plaintext = await Encryption.aeadDecrypt(algorithm, keyBytes, iv, ciphertext, tag);
221
- return new ReadableStream<Uint8Array>({
222
- start(controller): void {
223
- controller.enqueue(plaintext);
224
- controller.close();
225
- }
226
- });
132
+ const plaintext = await Encryption.decrypt(algorithm, keyBytes, initializationVector, ciphertext);
133
+ return Encryption.toStream(plaintext);
227
134
  }
228
135
 
229
- /**
230
- * Performs ECDH-ES key agreement with X25519 and wraps the CEK using AES-256 Key Wrap.
231
- *
232
- * @param ephemeralPrivateKey - Ephemeral X25519 private key (JWK).
233
- * @param recipientPublicKey - Recipient's X25519 public key (JWK).
234
- * @param cek - The Content Encryption Key to wrap.
235
- * @returns The wrapped CEK bytes.
236
- */
237
- public static async ecdhEsWrapKey(
238
- ephemeralPrivateKey: Jwk,
239
- recipientPublicKey: Jwk,
136
+ public static async wrapKey(
137
+ recipientPublicKey: PublicKeyJwk,
240
138
  cek: Uint8Array,
241
- ): Promise<Uint8Array> {
242
- // 1. ECDH shared secret
243
- const sharedSecret = await X25519.sharedSecret({
244
- privateKeyA : ephemeralPrivateKey,
245
- publicKeyB : recipientPublicKey,
246
- });
139
+ keyInput: KeyEncryptionInput,
140
+ ephemeralPrivateKey?: Jwk,
141
+ ): Promise<{ encryptedKey: Uint8Array; ephemeralPublicKey: PublicKeyJwk }> {
142
+ Encryption.validateContentEncryptionKey(cek);
247
143
 
248
- // 2. Derive KEK via Concat KDF (RFC 7518 Section 4.6.2)
249
- const kek = await ConcatKdf.deriveKey({
250
- sharedSecret,
251
- keyDataLen : 256,
252
- fixedInfo : {
253
- algorithmId : 'A256KW',
254
- partyUInfo : '',
255
- partyVInfo : '',
256
- suppPubInfo : 256,
257
- },
144
+ const privateKey = ephemeralPrivateKey ?? await X25519.generateKey();
145
+ const publicKey = await X25519.getPublicKey({ key: privateKey }) as PublicKeyJwk;
146
+ const sharedSecret = await X25519.sharedSecret({
147
+ privateKeyA : privateKey,
148
+ publicKeyB : recipientPublicKey as Jwk,
258
149
  });
150
+ const kek = await Encryption.deriveKek(sharedSecret, keyInput);
151
+ const cekJwk = Encryption.toA256CtrJwk(cek);
152
+ const kekJwk: Jwk = { alg: 'A256KW', k: Encoder.bytesToBase64Url(kek), kty: 'oct' };
153
+ const encryptedKey = await AesKw.wrapKey({ encryptionKey: kekJwk, unwrappedKey: cekJwk });
259
154
 
260
- // 3. AES-256 Key Wrap
261
- const cekJwk: Jwk = { kty: 'oct', k: Encoder.bytesToBase64Url(cek), alg: 'A256GCM' };
262
- const kekJwk: Jwk = { kty: 'oct', k: Encoder.bytesToBase64Url(kek), alg: 'A256KW' };
263
- const wrappedKey = await AesKw.wrapKey({ unwrappedKey: cekJwk, encryptionKey: kekJwk });
264
-
265
- return wrappedKey;
155
+ return { encryptedKey, ephemeralPublicKey: publicKey };
266
156
  }
267
157
 
268
- /**
269
- * Performs ECDH-ES key agreement with X25519 and unwraps the CEK using AES-256 Key Unwrap.
270
- *
271
- * @param recipientPrivateKey - Recipient's X25519 private key (JWK).
272
- * @param ephemeralPublicKey - Ephemeral X25519 public key from the JWE recipient header (JWK).
273
- * @param wrappedKey - The wrapped CEK bytes.
274
- * @returns The unwrapped CEK bytes.
275
- */
276
- public static async ecdhEsUnwrapKey(
158
+ public static async unwrapKey(
277
159
  recipientPrivateKey: Jwk,
278
- ephemeralPublicKey: Jwk,
279
- wrappedKey: Uint8Array,
160
+ keyEncryption: KeyEncryption,
280
161
  ): Promise<Uint8Array> {
281
- // 1. ECDH shared secret
282
162
  const sharedSecret = await X25519.sharedSecret({
283
163
  privateKeyA : recipientPrivateKey,
284
- publicKeyB : ephemeralPublicKey,
164
+ publicKeyB : keyEncryption.ephemeralPublicKey as Jwk,
285
165
  });
286
-
287
- // 2. Derive KEK via Concat KDF
288
- const kek = await ConcatKdf.deriveKey({
289
- sharedSecret,
290
- keyDataLen : 256,
291
- fixedInfo : {
292
- algorithmId : 'A256KW',
293
- partyUInfo : '',
294
- partyVInfo : '',
295
- suppPubInfo : 256,
296
- },
297
- });
298
-
299
- // 3. AES-256 Key Unwrap
300
- const kekJwk: Jwk = { kty: 'oct', k: Encoder.bytesToBase64Url(kek), alg: 'A256KW' };
166
+ const kek = await Encryption.deriveKek(sharedSecret, keyEncryption);
167
+ const kekJwk: Jwk = { alg: 'A256KW', k: Encoder.bytesToBase64Url(kek), kty: 'oct' };
301
168
  const unwrappedJwk = await AesKw.unwrapKey({
302
- wrappedKeyBytes : wrappedKey,
303
- wrappedKeyAlgorithm : 'A256GCM',
304
169
  decryptionKey : kekJwk,
170
+ wrappedKeyAlgorithm : ContentEncryptionAlgorithm.A256CTR,
171
+ wrappedKeyBytes : Encoder.base64UrlToBytes(keyEncryption.encryptedKey),
305
172
  });
306
173
 
307
174
  return Encoder.base64UrlToBytes(unwrappedJwk.k!);
308
175
  }
309
176
 
310
- /**
311
- * Builds a JWE encryption property structure from encryption input.
312
- * The ciphertext (encrypted record data) is stored separately in the DataStore,
313
- * so only the key wrapping metadata, IV, and authentication tag are included here.
314
- *
315
- * @param encryptionInput - Describes the CEK, IV, and recipient key encryption inputs.
316
- * @param tag - The authentication tag produced by the AEAD cipher during data encryption.
317
- */
318
- public static async buildJwe(
177
+ public static async buildEncryptionProperty(
319
178
  encryptionInput: EncryptionInput,
320
- tag: Uint8Array,
321
- ): Promise<JweEncryption> {
322
- const enc = encryptionInput.algorithm ?? ContentEncryptionAlgorithm.A256GCM;
323
- const protectedHeader: JweProtectedHeader = {
324
- alg: KeyAgreementAlgorithm.EcdhEsA256kw,
325
- enc,
326
- };
327
-
328
- const protectedHeaderBase64url = Encoder.stringToBase64Url(JSON.stringify(protectedHeader));
179
+ ): Promise<DwnEncryption> {
180
+ const algorithm = encryptionInput.algorithm ?? ContentEncryptionAlgorithm.A256CTR;
181
+ Encryption.validateContentEncryptionParameters(algorithm, encryptionInput.key, encryptionInput.initializationVector);
329
182
 
330
- const recipients: JweRecipient[] = [];
183
+ const keyEncryption: KeyEncryption[] = [];
331
184
  for (const keyInput of encryptionInput.keyEncryptionInputs) {
332
- // Generate ephemeral X25519 key pair for each recipient
333
- const ephemeralPrivateKey = await X25519.generateKey();
334
- const ephemeralPublicKey = await X25519.getPublicKey({ key: ephemeralPrivateKey });
335
-
336
- // Wrap the CEK
337
- const wrappedKey = await Encryption.ecdhEsWrapKey(
338
- ephemeralPrivateKey,
339
- keyInput.publicKey as Jwk,
185
+ const { encryptedKey, ephemeralPublicKey } = await Encryption.wrapKey(
186
+ keyInput.publicKey,
340
187
  encryptionInput.key,
188
+ keyInput,
341
189
  );
342
-
343
- const recipientHeader: JweRecipientHeader = {
344
- kid : keyInput.publicKeyId,
345
- epk : ephemeralPublicKey as PublicKeyJwk,
346
- derivationScheme : keyInput.derivationScheme,
190
+ const common = {
191
+ algorithm : KeyAgreementAlgorithm.X25519HkdfSha256A256Kw,
192
+ encryptedKey : Encoder.bytesToBase64Url(encryptedKey),
193
+ ephemeralPublicKey,
194
+ keyId : keyInput.keyId,
347
195
  };
348
-
349
- // Attach derived public key for protocolContext scheme
350
- if (keyInput.derivationScheme === (KeyDerivationScheme.ProtocolContext as KeyDerivationScheme)) {
351
- recipientHeader.derivedPublicKey = keyInput.publicKey;
196
+ if (keyInput.derivationScheme === KeyDerivationScheme.ProtocolPath) {
197
+ keyEncryption.push({
198
+ ...common,
199
+ derivationScheme: KeyDerivationScheme.ProtocolPath,
200
+ });
201
+ } else {
202
+ keyEncryption.push({
203
+ ...common,
204
+ derivationScheme : ROLE_AUDIENCE_DERIVATION_SCHEME,
205
+ epoch : keyInput.epoch,
206
+ protocol : keyInput.protocol,
207
+ role : keyInput.role,
208
+ });
352
209
  }
353
-
354
- recipients.push({
355
- header : recipientHeader,
356
- encrypted_key : Encoder.bytesToBase64Url(wrappedKey),
357
- });
358
210
  }
359
211
 
360
212
  return {
361
- protected : protectedHeaderBase64url,
362
- iv : Encoder.bytesToBase64Url(encryptionInput.initializationVector),
363
- tag : Encoder.bytesToBase64Url(tag),
364
- recipients,
213
+ algorithm,
214
+ initializationVector: Encoder.bytesToBase64Url(encryptionInput.initializationVector),
215
+ keyEncryption,
365
216
  };
366
217
  }
367
218
 
368
- /**
369
- * Parses the JWE protected header from its base64url encoding.
370
- */
371
- public static parseProtectedHeader(protectedBase64url: string): JweProtectedHeader {
372
- return Encoder.base64UrlToObject(protectedBase64url) as JweProtectedHeader;
219
+ public static async getKeyId(publicKeyJwk: PublicKeyJwk): Promise<string> {
220
+ return computeJwkThumbprint({ jwk: publicKeyJwk });
221
+ }
222
+
223
+ public static validateEncryptionProperty(encryption: DwnEncryption): void {
224
+ const initializationVector = Encoder.base64UrlToBytes(encryption.initializationVector);
225
+ if (initializationVector.byteLength !== AES_CTR_COUNTER_LENGTH_BYTES) {
226
+ throw new DwnError(
227
+ DwnErrorCode.RecordsWriteValidateIntegrityEncryptionInitializationVectorInvalid,
228
+ `A256CTR initializationVector must decode to ${AES_CTR_COUNTER_LENGTH_BYTES} bytes.`
229
+ );
230
+ }
231
+
232
+ for (const entry of encryption.keyEncryption) {
233
+ if (entry.ephemeralPublicKey.kty !== 'OKP' || entry.ephemeralPublicKey.crv !== 'X25519') {
234
+ throw new DwnError(
235
+ DwnErrorCode.RecordsWriteValidateIntegrityEncryptionEphemeralPublicKeyInvalid,
236
+ 'ephemeralPublicKey must be an OKP X25519 public key.'
237
+ );
238
+ }
239
+ }
240
+ }
241
+
242
+ private static async deriveKek(
243
+ sharedSecret: Uint8Array,
244
+ keyEncryption: KeyEncryptionInput | KeyEncryption,
245
+ ): Promise<Uint8Array> {
246
+ Encryption.validateSharedSecret(sharedSecret);
247
+ const info = Encryption.getKekInfo(keyEncryption);
248
+ return Hkdf.deriveKeyBytes({
249
+ baseKeyBytes : sharedSecret,
250
+ hash : 'SHA-256',
251
+ info,
252
+ length : 256,
253
+ salt : new Uint8Array(),
254
+ });
255
+ }
256
+
257
+ private static getKekInfo(keyEncryption: KeyEncryptionInput | KeyEncryption): string {
258
+ if (keyEncryption.derivationScheme === ROLE_AUDIENCE_DERIVATION_SCHEME) {
259
+ return `${KEK_INFO_PREFIX}|roleAudience|${keyEncryption.protocol}|${keyEncryption.role}|${keyEncryption.epoch}|${keyEncryption.keyId}`;
260
+ }
261
+
262
+ return `${KEK_INFO_PREFIX}|protocolPath|${keyEncryption.keyId}`;
263
+ }
264
+
265
+ private static validateContentEncryptionParameters(
266
+ algorithm: ContentEncryptionAlgorithm,
267
+ keyBytes: Uint8Array,
268
+ initializationVector: Uint8Array,
269
+ ): void {
270
+ if (algorithm !== ContentEncryptionAlgorithm.A256CTR) {
271
+ throw new Error(`Unsupported content encryption algorithm: ${algorithm as string}`);
272
+ }
273
+
274
+ Encryption.validateContentEncryptionKey(keyBytes);
275
+
276
+ if (initializationVector.byteLength !== AES_CTR_COUNTER_LENGTH_BYTES) {
277
+ throw new Error(`A256CTR initializationVector must be ${AES_CTR_COUNTER_LENGTH_BYTES} bytes.`);
278
+ }
279
+ }
280
+
281
+ private static validateContentEncryptionKey(keyBytes: Uint8Array): void {
282
+ if (keyBytes.byteLength !== AES_256_KEY_LENGTH_BYTES) {
283
+ throw new Error(`A256CTR content encryption key must be ${AES_256_KEY_LENGTH_BYTES} bytes.`);
284
+ }
285
+ }
286
+
287
+ private static validateSharedSecret(sharedSecret: Uint8Array): void {
288
+ if (sharedSecret.every((byte): boolean => byte === 0)) {
289
+ throw new Error('X25519 shared secret MUST NOT be all zeros.');
290
+ }
291
+ }
292
+
293
+ private static toA256CtrJwk(keyBytes: Uint8Array): Jwk {
294
+ return { alg: ContentEncryptionAlgorithm.A256CTR, k: Encoder.bytesToBase64Url(keyBytes), kty: 'oct' };
373
295
  }
374
296
 
375
- /**
376
- * Reads a ReadableStream to completion and returns all bytes concatenated.
377
- */
378
297
  private static async readStream(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
379
298
  const reader = stream.getReader();
380
299
  const chunks: Uint8Array[] = [];
381
300
  for (;;) {
382
301
  const { done, value } = await reader.read();
383
- if (done) { break; }
302
+ if (done) {
303
+ break;
304
+ }
384
305
  chunks.push(value);
385
306
  }
386
307
  return concatBytes(...chunks);
387
308
  }
309
+
310
+ private static toStream(data: Uint8Array): ReadableStream<Uint8Array> {
311
+ return new ReadableStream<Uint8Array>({
312
+ start(controller): void {
313
+ controller.enqueue(data);
314
+ controller.close();
315
+ }
316
+ });
317
+ }
388
318
  }
@@ -6,12 +6,12 @@ import { X25519 } from '@enbox/crypto';
6
6
  import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
7
7
 
8
8
  export enum KeyDerivationScheme {
9
- ProtocolContext = 'protocolContext',
10
9
  ProtocolPath = 'protocolPath',
11
10
  }
12
11
 
13
12
  export type DerivedPrivateJwk = {
14
13
  rootKeyId: string,
14
+ keyId?: string,
15
15
  derivationScheme: KeyDerivationScheme;
16
16
  derivationPath?: string[];
17
17
  derivedPrivateKey: PrivateKeyJwk,
@@ -16,9 +16,9 @@ export type ProtocolScope = {
16
16
  *
17
17
  * Matching fails closed for invalid combinations: `protocolPath` and
18
18
  * `contextId` are mutually exclusive, and either field requires `protocol`.
19
- * `protocol` and `protocolPath` match by exact equality. `contextId` scopes
20
- * match a context subtree: the target context must equal the scoped context or
21
- * begin with the scoped context followed by `/`.
19
+ * `protocol` matches by exact equality. `protocolPath` and `contextId` scopes
20
+ * match subtrees: the target value must equal the scoped value or begin with the
21
+ * scoped value followed by `/`.
22
22
  */
23
23
  export class PermissionScopeMatcher {
24
24
  /**
@@ -38,7 +38,7 @@ export class PermissionScopeMatcher {
38
38
  }
39
39
 
40
40
  if (scope.protocolPath !== undefined) {
41
- return scope.protocolPath === target.protocolPath;
41
+ return PermissionScopeMatcher.matchesSubtree(scope.protocolPath, target.protocolPath);
42
42
  }
43
43
 
44
44
  if (scope.contextId !== undefined) {
@@ -49,7 +49,11 @@ export class PermissionScopeMatcher {
49
49
  }
50
50
 
51
51
  private static matchesContextId(scopeContextId: string, candidateContextId: unknown): boolean {
52
- return typeof candidateContextId === 'string' &&
53
- (candidateContextId === scopeContextId || candidateContextId.startsWith(scopeContextId + '/'));
52
+ return PermissionScopeMatcher.matchesSubtree(scopeContextId, candidateContextId);
53
+ }
54
+
55
+ private static matchesSubtree(scopeValue: string, candidateValue: unknown): boolean {
56
+ return typeof candidateValue === 'string' &&
57
+ (candidateValue === scopeValue || candidateValue.startsWith(scopeValue + '/'));
54
58
  }
55
59
  }