@enbox/dwn-sdk-js 0.0.3 → 0.0.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 (134) hide show
  1. package/dist/browser.mjs +135 -0
  2. package/dist/browser.mjs.map +7 -0
  3. package/dist/esm/generated/precompiled-validators.js +640 -510
  4. package/dist/esm/generated/precompiled-validators.js.map +1 -1
  5. package/dist/esm/src/core/auth.js +6 -1
  6. package/dist/esm/src/core/auth.js.map +1 -1
  7. package/dist/esm/src/core/dwn-error.js +3 -0
  8. package/dist/esm/src/core/dwn-error.js.map +1 -1
  9. package/dist/esm/src/core/protocol-authorization.js +4 -0
  10. package/dist/esm/src/core/protocol-authorization.js.map +1 -1
  11. package/dist/esm/src/dwn.js +14 -0
  12. package/dist/esm/src/dwn.js.map +1 -1
  13. package/dist/esm/src/handlers/protocols-configure.js.map +1 -1
  14. package/dist/esm/src/handlers/records-delete.js +13 -0
  15. package/dist/esm/src/handlers/records-delete.js.map +1 -1
  16. package/dist/esm/src/handlers/records-subscribe.js +121 -66
  17. package/dist/esm/src/handlers/records-subscribe.js.map +1 -1
  18. package/dist/esm/src/handlers/records-write.js +1 -1
  19. package/dist/esm/src/handlers/records-write.js.map +1 -1
  20. package/dist/esm/src/index.js +1 -1
  21. package/dist/esm/src/index.js.map +1 -1
  22. package/dist/esm/src/interfaces/protocols-configure.js.map +1 -1
  23. package/dist/esm/src/interfaces/records-delete.js +1 -0
  24. package/dist/esm/src/interfaces/records-delete.js.map +1 -1
  25. package/dist/esm/src/interfaces/records-subscribe.js +2 -0
  26. package/dist/esm/src/interfaces/records-subscribe.js.map +1 -1
  27. package/dist/esm/src/interfaces/records-write.js +28 -45
  28. package/dist/esm/src/interfaces/records-write.js.map +1 -1
  29. package/dist/esm/src/jose/jws/general/verifier.js +9 -1
  30. package/dist/esm/src/jose/jws/general/verifier.js.map +1 -1
  31. package/dist/esm/src/smt/smt-utils.js +1 -1
  32. package/dist/esm/src/smt/smt-utils.js.map +1 -1
  33. package/dist/esm/src/types/records-types.js.map +1 -1
  34. package/dist/esm/src/utils/encryption.js +221 -78
  35. package/dist/esm/src/utils/encryption.js.map +1 -1
  36. package/dist/esm/src/utils/hd-key.js +6 -7
  37. package/dist/esm/src/utils/hd-key.js.map +1 -1
  38. package/dist/esm/src/utils/protocols.js +12 -10
  39. package/dist/esm/src/utils/protocols.js.map +1 -1
  40. package/dist/esm/src/utils/records.js +33 -44
  41. package/dist/esm/src/utils/records.js.map +1 -1
  42. package/dist/esm/tests/features/protocol-composition.spec.js +26 -21
  43. package/dist/esm/tests/features/protocol-composition.spec.js.map +1 -1
  44. package/dist/esm/tests/features/records-tags.spec.js +5 -5
  45. package/dist/esm/tests/features/records-tags.spec.js.map +1 -1
  46. package/dist/esm/tests/handlers/records-delete.spec.js +120 -2
  47. package/dist/esm/tests/handlers/records-delete.spec.js.map +1 -1
  48. package/dist/esm/tests/handlers/records-read.spec.js +25 -26
  49. package/dist/esm/tests/handlers/records-read.spec.js.map +1 -1
  50. package/dist/esm/tests/handlers/records-subscribe.spec.js +103 -0
  51. package/dist/esm/tests/handlers/records-subscribe.spec.js.map +1 -1
  52. package/dist/esm/tests/handlers/records-write.spec.js +124 -10
  53. package/dist/esm/tests/handlers/records-write.spec.js.map +1 -1
  54. package/dist/esm/tests/interfaces/messages-get.spec.js +3 -2
  55. package/dist/esm/tests/interfaces/messages-get.spec.js.map +1 -1
  56. package/dist/esm/tests/interfaces/records-write.spec.js +43 -34
  57. package/dist/esm/tests/interfaces/records-write.spec.js.map +1 -1
  58. package/dist/esm/tests/scenarios/end-to-end-tests.spec.js +4 -4
  59. package/dist/esm/tests/scenarios/end-to-end-tests.spec.js.map +1 -1
  60. package/dist/esm/tests/utils/encryption-callbacks.spec.js +21 -24
  61. package/dist/esm/tests/utils/encryption-callbacks.spec.js.map +1 -1
  62. package/dist/esm/tests/utils/encryption.spec.js +69 -66
  63. package/dist/esm/tests/utils/encryption.spec.js.map +1 -1
  64. package/dist/esm/tests/utils/filters.spec.js +1 -0
  65. package/dist/esm/tests/utils/filters.spec.js.map +1 -1
  66. package/dist/esm/tests/utils/test-data-generator.js +28 -7
  67. package/dist/esm/tests/utils/test-data-generator.js.map +1 -1
  68. package/dist/esm/tests/validation/json-schemas/protocols/protocols-configure.spec.js +1 -1
  69. package/dist/esm/tests/validation/json-schemas/protocols/protocols-configure.spec.js.map +1 -1
  70. package/dist/types/generated/precompiled-validators.d.ts.map +1 -1
  71. package/dist/types/src/core/auth.d.ts +3 -1
  72. package/dist/types/src/core/auth.d.ts.map +1 -1
  73. package/dist/types/src/core/dwn-error.d.ts +3 -0
  74. package/dist/types/src/core/dwn-error.d.ts.map +1 -1
  75. package/dist/types/src/core/protocol-authorization.d.ts.map +1 -1
  76. package/dist/types/src/dwn.d.ts +12 -0
  77. package/dist/types/src/dwn.d.ts.map +1 -1
  78. package/dist/types/src/handlers/protocols-configure.d.ts.map +1 -1
  79. package/dist/types/src/handlers/records-delete.d.ts.map +1 -1
  80. package/dist/types/src/handlers/records-subscribe.d.ts +17 -28
  81. package/dist/types/src/handlers/records-subscribe.d.ts.map +1 -1
  82. package/dist/types/src/index.d.ts +4 -4
  83. package/dist/types/src/index.d.ts.map +1 -1
  84. package/dist/types/src/interfaces/records-delete.d.ts +4 -0
  85. package/dist/types/src/interfaces/records-delete.d.ts.map +1 -1
  86. package/dist/types/src/interfaces/records-subscribe.d.ts +4 -1
  87. package/dist/types/src/interfaces/records-subscribe.d.ts.map +1 -1
  88. package/dist/types/src/interfaces/records-write.d.ts +23 -53
  89. package/dist/types/src/interfaces/records-write.d.ts.map +1 -1
  90. package/dist/types/src/jose/jws/general/verifier.d.ts.map +1 -1
  91. package/dist/types/src/types/encryption-types.d.ts +9 -8
  92. package/dist/types/src/types/encryption-types.d.ts.map +1 -1
  93. package/dist/types/src/types/protocols-types.d.ts +65 -16
  94. package/dist/types/src/types/protocols-types.d.ts.map +1 -1
  95. package/dist/types/src/types/records-types.d.ts +7 -26
  96. package/dist/types/src/types/records-types.d.ts.map +1 -1
  97. package/dist/types/src/utils/encryption.d.ts +157 -28
  98. package/dist/types/src/utils/encryption.d.ts.map +1 -1
  99. package/dist/types/src/utils/hd-key.d.ts +2 -3
  100. package/dist/types/src/utils/hd-key.d.ts.map +1 -1
  101. package/dist/types/src/utils/protocols.d.ts.map +1 -1
  102. package/dist/types/src/utils/records.d.ts +3 -4
  103. package/dist/types/src/utils/records.d.ts.map +1 -1
  104. package/dist/types/tests/features/protocol-composition.spec.d.ts.map +1 -1
  105. package/dist/types/tests/handlers/records-delete.spec.d.ts.map +1 -1
  106. package/dist/types/tests/handlers/records-read.spec.d.ts.map +1 -1
  107. package/dist/types/tests/handlers/records-subscribe.spec.d.ts.map +1 -1
  108. package/dist/types/tests/handlers/records-write.spec.d.ts.map +1 -1
  109. package/dist/types/tests/utils/test-data-generator.d.ts +7 -0
  110. package/dist/types/tests/utils/test-data-generator.d.ts.map +1 -1
  111. package/package.json +10 -21
  112. package/src/core/auth.ts +12 -1
  113. package/src/core/dwn-error.ts +3 -0
  114. package/src/core/protocol-authorization.ts +8 -0
  115. package/src/dwn.ts +15 -0
  116. package/src/handlers/protocols-configure.ts +4 -4
  117. package/src/handlers/records-delete.ts +12 -0
  118. package/src/handlers/records-subscribe.ts +174 -75
  119. package/src/handlers/records-write.ts +1 -1
  120. package/src/index.ts +4 -4
  121. package/src/interfaces/protocols-configure.ts +5 -5
  122. package/src/interfaces/records-delete.ts +9 -3
  123. package/src/interfaces/records-subscribe.ts +6 -1
  124. package/src/interfaces/records-write.ts +33 -105
  125. package/src/jose/jws/general/verifier.ts +11 -1
  126. package/src/smt/smt-utils.ts +1 -1
  127. package/src/types/encryption-types.ts +9 -8
  128. package/src/types/protocols-types.ts +72 -18
  129. package/src/types/records-types.ts +7 -29
  130. package/src/utils/encryption.ts +346 -88
  131. package/src/utils/hd-key.ts +9 -10
  132. package/src/utils/protocols.ts +15 -13
  133. package/src/utils/records.ts +47 -55
  134. package/dist/bundles/dwn.js +0 -151
@@ -1,130 +1,388 @@
1
- import * as crypto from 'crypto';
2
- import * as eciesjs from 'eciesjs';
1
+ import type { Jwk } from '@enbox/crypto';
2
+ import type { PublicKeyJwk } from '../types/jose-types.js';
3
3
 
4
- // compress publicKey for message encryption
5
- eciesjs.ECIES_CONFIG.isEphemeralKeyCompressed = true;
4
+ import { concatBytes } from '@noble/ciphers/utils';
5
+ import { Encoder } from './encoder.js';
6
+ import { KeyDerivationScheme } from './hd-key.js';
7
+ import { AesGcm, AesKw, ConcatKdf, X25519, XChaCha20Poly1305 } from '@enbox/crypto';
6
8
 
7
9
  /**
8
- * Utility class for performing common, non-DWN specific encryption operations.
10
+ * Content encryption algorithms supported by the DWN.
11
+ * Both are AEAD (Authenticated Encryption with Associated Data) ciphers.
12
+ */
13
+ 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',
18
+ }
19
+
20
+ /**
21
+ * Key agreement algorithm used by the DWN.
22
+ * ECDH-ES with X25519 key agreement and AES-256 Key Wrap.
23
+ */
24
+ export enum KeyAgreementAlgorithm {
25
+ EcdhEsA256kw = 'ECDH-ES+A256KW',
26
+ }
27
+
28
+ /** Size of the AES-GCM authentication tag in bytes. */
29
+ const AES_GCM_TAG_LENGTH_BYTES = 16;
30
+
31
+ /** Size of the Poly1305 authentication tag in bytes. */
32
+ const POLY1305_TAG_LENGTH_BYTES = 16;
33
+
34
+ /**
35
+ * JWE Protected Header for DWN encryption.
36
+ */
37
+ export type JweProtectedHeader = {
38
+ alg: KeyAgreementAlgorithm;
39
+ enc: ContentEncryptionAlgorithm;
40
+ };
41
+
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;
54
+ };
55
+
56
+ /**
57
+ * A single recipient entry in the JWE General JSON Serialization.
58
+ */
59
+ export type JweRecipient = {
60
+ header: JweRecipientHeader;
61
+ encrypted_key: string;
62
+ };
63
+
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[];
81
+ };
82
+
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. */
90
+ publicKey: PublicKeyJwk;
91
+ /** Key derivation scheme. */
92
+ derivationScheme: KeyDerivationScheme;
93
+ /** Algorithm for key agreement. Defaults to ECDH-ES+A256KW. */
94
+ algorithm?: KeyAgreementAlgorithm;
95
+ };
96
+
97
+ /**
98
+ * Input describing how to encrypt record data.
99
+ */
100
+ export type EncryptionInput = {
101
+ /** Content encryption algorithm. Defaults to A256GCM. */
102
+ algorithm?: ContentEncryptionAlgorithm;
103
+ /** The Content Encryption Key (CEK). Must be 32 bytes (256-bit). */
104
+ key: Uint8Array;
105
+ /** Initialization vector. 12 bytes for A256GCM, 24 bytes for XC20P. */
106
+ initializationVector: Uint8Array;
107
+ /** Authentication tag from the AEAD encryption of the record data. */
108
+ authenticationTag: Uint8Array;
109
+ /** Recipient key encryption inputs. */
110
+ keyEncryptionInputs: KeyEncryptionInput[];
111
+ };
112
+
113
+ /**
114
+ * Payload passed to a KeyDecrypter callback for JWE-based key unwrapping.
115
+ */
116
+ export type JweKeyUnwrapPayload = {
117
+ /** The wrapped CEK bytes. */
118
+ encryptedKey: Uint8Array;
119
+ /** The ephemeral X25519 public key used for ECDH. */
120
+ ephemeralPublicKey: PublicKeyJwk;
121
+ };
122
+
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.
9
127
  */
10
128
  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(
135
+ algorithm: ContentEncryptionAlgorithm,
136
+ keyBytes: Uint8Array,
137
+ iv: Uint8Array,
138
+ 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
+ }
158
+ }
159
+
11
160
  /**
12
- * Encrypts the given plaintext stream using AES-256-CTR algorithm.
161
+ * Decrypts data using an AEAD cipher (A256GCM or XC20P).
162
+ * Expects ciphertext and tag as separate inputs.
13
163
  */
14
- public static async aes256CtrEncrypt(
15
- key: Uint8Array, initializationVector: Uint8Array, plaintextStream: ReadableStream<Uint8Array>
164
+ public static async aeadDecrypt(
165
+ algorithm: ContentEncryptionAlgorithm,
166
+ keyBytes: Uint8Array,
167
+ iv: Uint8Array,
168
+ ciphertext: Uint8Array,
169
+ tag: Uint8Array,
170
+ ): 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
+ }
184
+ }
185
+
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(
192
+ algorithm: ContentEncryptionAlgorithm,
193
+ keyBytes: Uint8Array,
194
+ iv: Uint8Array,
195
+ plaintextStream: ReadableStream<Uint8Array>,
196
+ ): Promise<{ ciphertextStream: ReadableStream<Uint8Array>; tag: Uint8Array }> {
197
+ 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 };
206
+ }
207
+
208
+ /**
209
+ * Decrypts a ciphertext stream using an AEAD cipher.
210
+ * Returns a ReadableStream of plaintext.
211
+ */
212
+ public static async aeadDecryptStream(
213
+ algorithm: ContentEncryptionAlgorithm,
214
+ keyBytes: Uint8Array,
215
+ iv: Uint8Array,
216
+ ciphertextStream: ReadableStream<Uint8Array>,
217
+ tag: Uint8Array,
16
218
  ): Promise<ReadableStream<Uint8Array>> {
17
- const cipher = crypto.createCipheriv('aes-256-ctr', key, initializationVector);
219
+ 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
+ });
227
+ }
18
228
 
19
- const transform = new TransformStream<Uint8Array, Uint8Array>({
20
- transform(chunk, controller): void {
21
- controller.enqueue(new Uint8Array(cipher.update(chunk)));
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,
240
+ cek: Uint8Array,
241
+ ): Promise<Uint8Array> {
242
+ // 1. ECDH shared secret
243
+ const sharedSecret = await X25519.sharedSecret({
244
+ privateKeyA : ephemeralPrivateKey,
245
+ publicKeyB : recipientPublicKey,
246
+ });
247
+
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,
22
257
  },
23
- flush(controller): void {
24
- const finalChunk = cipher.final();
25
- if (finalChunk.length > 0) { controller.enqueue(new Uint8Array(finalChunk)); }
26
- }
27
258
  });
28
259
 
29
- return plaintextStream.pipeThrough(transform);
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;
30
266
  }
31
267
 
32
268
  /**
33
- * Decrypts the given cipher stream using AES-256-CTR algorithm.
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.
34
275
  */
35
- public static async aes256CtrDecrypt(
36
- key: Uint8Array, initializationVector: Uint8Array, cipherStream: ReadableStream<Uint8Array>
37
- ): Promise<ReadableStream<Uint8Array>> {
38
- const decipher = crypto.createDecipheriv('aes-256-ctr', key, initializationVector);
276
+ public static async ecdhEsUnwrapKey(
277
+ recipientPrivateKey: Jwk,
278
+ ephemeralPublicKey: Jwk,
279
+ wrappedKey: Uint8Array,
280
+ ): Promise<Uint8Array> {
281
+ // 1. ECDH shared secret
282
+ const sharedSecret = await X25519.sharedSecret({
283
+ privateKeyA : recipientPrivateKey,
284
+ publicKeyB : ephemeralPublicKey,
285
+ });
39
286
 
40
- const transform = new TransformStream<Uint8Array, Uint8Array>({
41
- transform(chunk, controller): void {
42
- controller.enqueue(new Uint8Array(decipher.update(chunk)));
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,
43
296
  },
44
- flush(controller): void {
45
- const finalChunk = decipher.final();
46
- if (finalChunk.length > 0) { controller.enqueue(new Uint8Array(finalChunk)); }
47
- }
48
297
  });
49
298
 
50
- return cipherStream.pipeThrough(transform);
299
+ // 3. AES-256 Key Unwrap
300
+ const kekJwk: Jwk = { kty: 'oct', k: Encoder.bytesToBase64Url(kek), alg: 'A256KW' };
301
+ const unwrappedJwk = await AesKw.unwrapKey({
302
+ wrappedKeyBytes : wrappedKey,
303
+ wrappedKeyAlgorithm : 'A256GCM',
304
+ decryptionKey : kekJwk,
305
+ });
306
+
307
+ return Encoder.base64UrlToBytes(unwrappedJwk.k!);
51
308
  }
52
309
 
53
310
  /**
54
- * Encrypts the given plaintext using ECIES (Elliptic Curve Integrated Encryption Scheme)
55
- * with SECP256K1 for the asymmetric calculations, HKDF as the key-derivation function,
56
- * and AES-GCM for the symmetric encryption and MAC algorithms.
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.
57
317
  */
58
- public static async eciesSecp256k1Encrypt(publicKeyBytes: Uint8Array, plaintext: Uint8Array): Promise<EciesEncryptionOutput> {
59
- // underlying library requires Buffer as input
60
- const publicKey = Buffer.from(publicKeyBytes);
61
- const plaintextBuffer = Buffer.from(plaintext);
318
+ public static async buildJwe(
319
+ 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
+ };
62
327
 
63
- const cryptogram = eciesjs.encrypt(publicKey, plaintextBuffer);
328
+ const protectedHeaderBase64url = Encoder.stringToBase64Url(JSON.stringify(protectedHeader));
64
329
 
65
- // split cryptogram returned into constituent parts
66
- let start = 0;
67
- let end = Encryption.isEphemeralKeyCompressed ? 33 : 65;
68
- const ephemeralPublicKey = cryptogram.subarray(start, end);
330
+ const recipients: JweRecipient[] = [];
331
+ 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 });
69
335
 
70
- start = end;
71
- end += eciesjs.ECIES_CONFIG.symmetricNonceLength;
72
- const initializationVector = cryptogram.subarray(start, end);
336
+ // Wrap the CEK
337
+ const wrappedKey = await Encryption.ecdhEsWrapKey(
338
+ ephemeralPrivateKey,
339
+ keyInput.publicKey as Jwk,
340
+ encryptionInput.key,
341
+ );
73
342
 
74
- start = end;
75
- end += 16; // eciesjs.consts.AEAD_TAG_LENGTH
76
- const messageAuthenticationCode = cryptogram.subarray(start, end);
343
+ const recipientHeader: JweRecipientHeader = {
344
+ kid : keyInput.publicKeyId,
345
+ epk : ephemeralPublicKey as PublicKeyJwk,
346
+ derivationScheme : keyInput.derivationScheme,
347
+ };
77
348
 
78
- const ciphertext = cryptogram.subarray(end);
349
+ // Attach derived public key for protocolContext scheme
350
+ if (keyInput.derivationScheme === (KeyDerivationScheme.ProtocolContext as KeyDerivationScheme)) {
351
+ recipientHeader.derivedPublicKey = keyInput.publicKey;
352
+ }
353
+
354
+ recipients.push({
355
+ header : recipientHeader,
356
+ encrypted_key : Encoder.bytesToBase64Url(wrappedKey),
357
+ });
358
+ }
79
359
 
80
360
  return {
81
- ciphertext,
82
- ephemeralPublicKey,
83
- initializationVector,
84
- messageAuthenticationCode
361
+ protected : protectedHeaderBase64url,
362
+ iv : Encoder.bytesToBase64Url(encryptionInput.initializationVector),
363
+ tag : Encoder.bytesToBase64Url(tag),
364
+ recipients,
85
365
  };
86
366
  }
87
367
 
88
368
  /**
89
- * Decrypt the given plaintext using ECIES (Elliptic Curve Integrated Encryption Scheme)
90
- * with SECP256K1 for the asymmetric calculations, HKDF as the key-derivation function,
91
- * and AES-GCM for the symmetric encryption and MAC algorithms.
369
+ * Parses the JWE protected header from its base64url encoding.
92
370
  */
93
- public static async eciesSecp256k1Decrypt(input: EciesEncryptionInput): Promise<Uint8Array> {
94
- // underlying library requires Buffer as input
95
- const privateKeyBuffer = Buffer.from(input.privateKey);
96
- const eciesEncryptionOutput = Buffer.concat([
97
- input.ephemeralPublicKey,
98
- input.initializationVector,
99
- input.messageAuthenticationCode,
100
- input.ciphertext
101
- ]);
102
-
103
- const plaintext = eciesjs.decrypt(privateKeyBuffer, eciesEncryptionOutput);
104
-
105
- return plaintext;
371
+ public static parseProtectedHeader(protectedBase64url: string): JweProtectedHeader {
372
+ return Encoder.base64UrlToObject(protectedBase64url) as JweProtectedHeader;
106
373
  }
107
374
 
108
375
  /**
109
- * Expose eciesjs library configuration
376
+ * Reads a ReadableStream to completion and returns all bytes concatenated.
110
377
  */
111
- static get isEphemeralKeyCompressed():boolean {
112
- return eciesjs.ECIES_CONFIG.isEphemeralKeyCompressed;
378
+ private static async readStream(stream: ReadableStream<Uint8Array>): Promise<Uint8Array> {
379
+ const reader = stream.getReader();
380
+ const chunks: Uint8Array[] = [];
381
+ for (;;) {
382
+ const { done, value } = await reader.read();
383
+ if (done) { break; }
384
+ chunks.push(value);
385
+ }
386
+ return concatBytes(...chunks);
113
387
  }
114
388
  }
115
-
116
- export type EciesEncryptionOutput = {
117
- initializationVector: Uint8Array;
118
- ephemeralPublicKey: Uint8Array;
119
- ciphertext: Uint8Array;
120
- messageAuthenticationCode: Uint8Array;
121
- };
122
-
123
- export type EciesEncryptionInput = EciesEncryptionOutput & {
124
- privateKey: Uint8Array;
125
- };
126
-
127
- export enum EncryptionAlgorithm {
128
- Aes256Ctr = 'A256CTR',
129
- EciesSecp256k1 = 'ECIES-ES256K'
130
- }
@@ -2,7 +2,7 @@ import type { PrivateKeyJwk, PublicKeyJwk } from '../types/jose-types.js';
2
2
 
3
3
  import { Encoder } from './encoder.js';
4
4
  import { getWebcryptoSubtle } from '@noble/ciphers/webcrypto';
5
- import { Secp256k1 } from './secp256k1.js';
5
+ import { X25519 } from '@enbox/crypto';
6
6
  import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
7
7
 
8
8
  export enum KeyDerivationScheme {
@@ -32,18 +32,18 @@ export type DerivedPrivateJwk = {
32
32
  export class HdKey {
33
33
  /**
34
34
  * Derives a descendant private key.
35
- * NOTE: currently only supports SECP256K1 keys.
35
+ * Uses X25519 keys for encryption key derivation.
36
36
  */
37
37
  public static async derivePrivateKey(ancestorKey: DerivedPrivateJwk, subDerivationPath: string[]): Promise<DerivedPrivateJwk> {
38
- const ancestorPrivateKey = Secp256k1.privateJwkToBytes(ancestorKey.derivedPrivateKey);
38
+ const ancestorPrivateKey = await X25519.privateKeyToBytes({ privateKey: ancestorKey.derivedPrivateKey });
39
39
  const ancestorPrivateKeyDerivationPath = ancestorKey.derivationPath ?? [];
40
40
  const derivedPrivateKeyBytes = await HdKey.derivePrivateKeyBytes(ancestorPrivateKey, subDerivationPath);
41
- const derivedPrivateKeyJwk = await Secp256k1.privateKeyToJwk(derivedPrivateKeyBytes);
41
+ const derivedPrivateKeyJwk = await X25519.bytesToPrivateKey({ privateKeyBytes: derivedPrivateKeyBytes });
42
42
  const derivedDescendantPrivateKey: DerivedPrivateJwk = {
43
43
  rootKeyId : ancestorKey.rootKeyId,
44
44
  derivationScheme : ancestorKey.derivationScheme,
45
45
  derivationPath : [...ancestorPrivateKeyDerivationPath, ...subDerivationPath],
46
- derivedPrivateKey : derivedPrivateKeyJwk
46
+ derivedPrivateKey : derivedPrivateKeyJwk as PrivateKeyJwk
47
47
  };
48
48
 
49
49
  return derivedDescendantPrivateKey;
@@ -51,13 +51,13 @@ export class HdKey {
51
51
 
52
52
  /**
53
53
  * Derives a descendant public key from an ancestor private key.
54
- * NOTE: currently only supports SECP256K1 keys.
54
+ * Uses X25519 keys for encryption key derivation.
55
55
  */
56
56
  public static async derivePublicKey(ancestorKey: DerivedPrivateJwk, subDerivationPath: string[]): Promise<PublicKeyJwk> {
57
57
  const derivedDescendantPrivateKey = await HdKey.derivePrivateKey(ancestorKey, subDerivationPath);
58
- const derivedDescendantPublicKey = await Secp256k1.getPublicJwk(derivedDescendantPrivateKey.derivedPrivateKey);
58
+ const derivedDescendantPublicKey = await X25519.getPublicKey({ key: derivedDescendantPrivateKey.derivedPrivateKey });
59
59
 
60
- return derivedDescendantPublicKey;
60
+ return derivedDescendantPublicKey as PublicKeyJwk;
61
61
  }
62
62
 
63
63
  /**
@@ -82,7 +82,6 @@ export class HdKey {
82
82
 
83
83
  /**
84
84
  * Derives a key using HMAC-based Extract-and-Expand Key Derivation Function (HKDF) as defined in RFC 5869.
85
- * TODO: Consolidate HKDF implementation and usage with web5-js - https://github.com/enboxorg/enbox/issues/742
86
85
  */
87
86
  public static async deriveKeyUsingHkdf(params: {
88
87
  hashAlgorithm: 'SHA-256' | 'SHA-384' | 'SHA-512',
@@ -123,4 +122,4 @@ export class HdKey {
123
122
  throw new DwnError(DwnErrorCode.HdKeyDerivationPathInvalid, `Invalid key derivation path: ${pathSegments}`);
124
123
  }
125
124
  }
126
- }
125
+ }
@@ -1,9 +1,9 @@
1
1
  import type { DerivedPrivateJwk } from '../utils/hd-key.js';
2
2
  import type { EncryptionKeyDeriver } from '../types/encryption-types.js';
3
- import type { PrivateKeyJwk } from '../types/jose-types.js';
3
+ import type { PrivateKeyJwk, PublicKeyJwk } from '../types/jose-types.js';
4
4
  import type { ProtocolDefinition, ProtocolRuleSet } from '../types/protocols-types.js';
5
5
 
6
- import { Secp256k1 } from './secp256k1.js';
6
+ import { X25519 } from '@enbox/crypto';
7
7
  import { HdKey, KeyDerivationScheme } from '../utils/hd-key.js';
8
8
 
9
9
  /**
@@ -115,25 +115,26 @@ export class Protocols {
115
115
  for (const key in ruleSet) {
116
116
  if (!key.startsWith('$')) {
117
117
  const currentPath = [...parentPath, key];
118
+ const childRuleSet = ruleSet[key] as ProtocolRuleSet;
118
119
 
119
120
  // Skip $ref nodes — they are governed by the referenced protocol's encryption keys.
120
121
  // Still recurse into children, which belong to the composing protocol.
121
- if (ruleSet[key].$ref !== undefined) {
122
- await injectKeysViaCallback(ruleSet[key], currentPath);
122
+ if (childRuleSet.$ref !== undefined) {
123
+ await injectKeysViaCallback(childRuleSet, currentPath);
123
124
  continue;
124
125
  }
125
126
 
126
127
  const publicKeyJwk = await keyDeriver.derivePublicKey(currentPath);
127
- ruleSet[key].$encryption = {
128
+ childRuleSet.$encryption = {
128
129
  rootKeyId: keyDeriver.rootKeyId,
129
130
  publicKeyJwk,
130
131
  };
131
- await injectKeysViaCallback(ruleSet[key], currentPath);
132
+ await injectKeysViaCallback(childRuleSet, currentPath);
132
133
  }
133
134
  }
134
135
  }
135
136
 
136
- await injectKeysViaCallback(clone.structure, basePath);
137
+ await injectKeysViaCallback(clone.structure as ProtocolRuleSet, basePath);
137
138
  return clone;
138
139
  }
139
140
 
@@ -146,18 +147,19 @@ export class Protocols {
146
147
  // if we encounter a nested rule set (a property name that doesn't begin with '$'), recursively inject the `$encryption` property
147
148
  if (!key.startsWith('$')) {
148
149
  const derivedPrivateKey = await HdKey.derivePrivateKey(parentKey, [key]);
150
+ const childRuleSet = ruleSet[key] as ProtocolRuleSet;
149
151
 
150
152
  // Skip $ref nodes — they are governed by the referenced protocol's encryption keys.
151
153
  // Still recurse into children, which belong to the composing protocol.
152
- if (ruleSet[key].$ref !== undefined) {
153
- await addEncryptionProperty(ruleSet[key], derivedPrivateKey);
154
+ if (childRuleSet.$ref !== undefined) {
155
+ await addEncryptionProperty(childRuleSet, derivedPrivateKey);
154
156
  continue;
155
157
  }
156
158
 
157
- const publicKeyJwk = await Secp256k1.getPublicJwk(derivedPrivateKey.derivedPrivateKey);
159
+ const publicKeyJwk = await X25519.getPublicKey({ key: derivedPrivateKey.derivedPrivateKey }) as PublicKeyJwk;
158
160
 
159
- ruleSet[key].$encryption = { rootKeyId, publicKeyJwk };
160
- await addEncryptionProperty(ruleSet[key], derivedPrivateKey);
161
+ childRuleSet.$encryption = { rootKeyId, publicKeyJwk };
162
+ await addEncryptionProperty(childRuleSet, derivedPrivateKey);
161
163
  }
162
164
  }
163
165
  }
@@ -169,7 +171,7 @@ export class Protocols {
169
171
  rootKeyId
170
172
  };
171
173
  const protocolLevelDerivedKey = await HdKey.derivePrivateKey(rootKey, [KeyDerivationScheme.ProtocolPath, protocolDefinition.protocol]);
172
- await addEncryptionProperty(clone.structure, protocolLevelDerivedKey);
174
+ await addEncryptionProperty(clone.structure as ProtocolRuleSet, protocolLevelDerivedKey);
173
175
 
174
176
  return clone;
175
177
  }