@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.
- package/dist/browser.mjs +135 -0
- package/dist/browser.mjs.map +7 -0
- package/dist/esm/generated/precompiled-validators.js +640 -510
- package/dist/esm/generated/precompiled-validators.js.map +1 -1
- package/dist/esm/src/core/auth.js +6 -1
- package/dist/esm/src/core/auth.js.map +1 -1
- package/dist/esm/src/core/dwn-error.js +3 -0
- package/dist/esm/src/core/dwn-error.js.map +1 -1
- package/dist/esm/src/core/protocol-authorization.js +4 -0
- package/dist/esm/src/core/protocol-authorization.js.map +1 -1
- package/dist/esm/src/dwn.js +14 -0
- package/dist/esm/src/dwn.js.map +1 -1
- package/dist/esm/src/handlers/protocols-configure.js.map +1 -1
- package/dist/esm/src/handlers/records-delete.js +13 -0
- package/dist/esm/src/handlers/records-delete.js.map +1 -1
- package/dist/esm/src/handlers/records-subscribe.js +121 -66
- package/dist/esm/src/handlers/records-subscribe.js.map +1 -1
- package/dist/esm/src/handlers/records-write.js +1 -1
- package/dist/esm/src/handlers/records-write.js.map +1 -1
- package/dist/esm/src/index.js +1 -1
- package/dist/esm/src/index.js.map +1 -1
- package/dist/esm/src/interfaces/protocols-configure.js.map +1 -1
- package/dist/esm/src/interfaces/records-delete.js +1 -0
- package/dist/esm/src/interfaces/records-delete.js.map +1 -1
- package/dist/esm/src/interfaces/records-subscribe.js +2 -0
- package/dist/esm/src/interfaces/records-subscribe.js.map +1 -1
- package/dist/esm/src/interfaces/records-write.js +28 -45
- package/dist/esm/src/interfaces/records-write.js.map +1 -1
- package/dist/esm/src/jose/jws/general/verifier.js +9 -1
- package/dist/esm/src/jose/jws/general/verifier.js.map +1 -1
- package/dist/esm/src/smt/smt-utils.js +1 -1
- package/dist/esm/src/smt/smt-utils.js.map +1 -1
- package/dist/esm/src/types/records-types.js.map +1 -1
- package/dist/esm/src/utils/encryption.js +221 -78
- package/dist/esm/src/utils/encryption.js.map +1 -1
- package/dist/esm/src/utils/hd-key.js +6 -7
- package/dist/esm/src/utils/hd-key.js.map +1 -1
- package/dist/esm/src/utils/protocols.js +12 -10
- package/dist/esm/src/utils/protocols.js.map +1 -1
- package/dist/esm/src/utils/records.js +33 -44
- package/dist/esm/src/utils/records.js.map +1 -1
- package/dist/esm/tests/features/protocol-composition.spec.js +26 -21
- package/dist/esm/tests/features/protocol-composition.spec.js.map +1 -1
- package/dist/esm/tests/features/records-tags.spec.js +5 -5
- package/dist/esm/tests/features/records-tags.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-delete.spec.js +120 -2
- package/dist/esm/tests/handlers/records-delete.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-read.spec.js +25 -26
- package/dist/esm/tests/handlers/records-read.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-subscribe.spec.js +103 -0
- package/dist/esm/tests/handlers/records-subscribe.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-write.spec.js +124 -10
- package/dist/esm/tests/handlers/records-write.spec.js.map +1 -1
- package/dist/esm/tests/interfaces/messages-get.spec.js +3 -2
- package/dist/esm/tests/interfaces/messages-get.spec.js.map +1 -1
- package/dist/esm/tests/interfaces/records-write.spec.js +43 -34
- package/dist/esm/tests/interfaces/records-write.spec.js.map +1 -1
- package/dist/esm/tests/scenarios/end-to-end-tests.spec.js +4 -4
- package/dist/esm/tests/scenarios/end-to-end-tests.spec.js.map +1 -1
- package/dist/esm/tests/utils/encryption-callbacks.spec.js +21 -24
- package/dist/esm/tests/utils/encryption-callbacks.spec.js.map +1 -1
- package/dist/esm/tests/utils/encryption.spec.js +69 -66
- package/dist/esm/tests/utils/encryption.spec.js.map +1 -1
- package/dist/esm/tests/utils/filters.spec.js +1 -0
- package/dist/esm/tests/utils/filters.spec.js.map +1 -1
- package/dist/esm/tests/utils/test-data-generator.js +28 -7
- package/dist/esm/tests/utils/test-data-generator.js.map +1 -1
- package/dist/esm/tests/validation/json-schemas/protocols/protocols-configure.spec.js +1 -1
- package/dist/esm/tests/validation/json-schemas/protocols/protocols-configure.spec.js.map +1 -1
- package/dist/types/generated/precompiled-validators.d.ts.map +1 -1
- package/dist/types/src/core/auth.d.ts +3 -1
- package/dist/types/src/core/auth.d.ts.map +1 -1
- package/dist/types/src/core/dwn-error.d.ts +3 -0
- package/dist/types/src/core/dwn-error.d.ts.map +1 -1
- package/dist/types/src/core/protocol-authorization.d.ts.map +1 -1
- package/dist/types/src/dwn.d.ts +12 -0
- package/dist/types/src/dwn.d.ts.map +1 -1
- package/dist/types/src/handlers/protocols-configure.d.ts.map +1 -1
- package/dist/types/src/handlers/records-delete.d.ts.map +1 -1
- package/dist/types/src/handlers/records-subscribe.d.ts +17 -28
- package/dist/types/src/handlers/records-subscribe.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +4 -4
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/interfaces/records-delete.d.ts +4 -0
- package/dist/types/src/interfaces/records-delete.d.ts.map +1 -1
- package/dist/types/src/interfaces/records-subscribe.d.ts +4 -1
- package/dist/types/src/interfaces/records-subscribe.d.ts.map +1 -1
- package/dist/types/src/interfaces/records-write.d.ts +23 -53
- package/dist/types/src/interfaces/records-write.d.ts.map +1 -1
- package/dist/types/src/jose/jws/general/verifier.d.ts.map +1 -1
- package/dist/types/src/types/encryption-types.d.ts +9 -8
- package/dist/types/src/types/encryption-types.d.ts.map +1 -1
- package/dist/types/src/types/protocols-types.d.ts +65 -16
- package/dist/types/src/types/protocols-types.d.ts.map +1 -1
- package/dist/types/src/types/records-types.d.ts +7 -26
- package/dist/types/src/types/records-types.d.ts.map +1 -1
- package/dist/types/src/utils/encryption.d.ts +157 -28
- package/dist/types/src/utils/encryption.d.ts.map +1 -1
- package/dist/types/src/utils/hd-key.d.ts +2 -3
- package/dist/types/src/utils/hd-key.d.ts.map +1 -1
- package/dist/types/src/utils/protocols.d.ts.map +1 -1
- package/dist/types/src/utils/records.d.ts +3 -4
- package/dist/types/src/utils/records.d.ts.map +1 -1
- package/dist/types/tests/features/protocol-composition.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/records-delete.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/records-read.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/records-subscribe.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/records-write.spec.d.ts.map +1 -1
- package/dist/types/tests/utils/test-data-generator.d.ts +7 -0
- package/dist/types/tests/utils/test-data-generator.d.ts.map +1 -1
- package/package.json +10 -21
- package/src/core/auth.ts +12 -1
- package/src/core/dwn-error.ts +3 -0
- package/src/core/protocol-authorization.ts +8 -0
- package/src/dwn.ts +15 -0
- package/src/handlers/protocols-configure.ts +4 -4
- package/src/handlers/records-delete.ts +12 -0
- package/src/handlers/records-subscribe.ts +174 -75
- package/src/handlers/records-write.ts +1 -1
- package/src/index.ts +4 -4
- package/src/interfaces/protocols-configure.ts +5 -5
- package/src/interfaces/records-delete.ts +9 -3
- package/src/interfaces/records-subscribe.ts +6 -1
- package/src/interfaces/records-write.ts +33 -105
- package/src/jose/jws/general/verifier.ts +11 -1
- package/src/smt/smt-utils.ts +1 -1
- package/src/types/encryption-types.ts +9 -8
- package/src/types/protocols-types.ts +72 -18
- package/src/types/records-types.ts +7 -29
- package/src/utils/encryption.ts +346 -88
- package/src/utils/hd-key.ts +9 -10
- package/src/utils/protocols.ts +15 -13
- package/src/utils/records.ts +47 -55
- package/dist/bundles/dwn.js +0 -151
package/src/utils/encryption.ts
CHANGED
|
@@ -1,130 +1,388 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import type { Jwk } from '@enbox/crypto';
|
|
2
|
+
import type { PublicKeyJwk } from '../types/jose-types.js';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
161
|
+
* Decrypts data using an AEAD cipher (A256GCM or XC20P).
|
|
162
|
+
* Expects ciphertext and tag as separate inputs.
|
|
13
163
|
*/
|
|
14
|
-
public static async
|
|
15
|
-
|
|
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
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
328
|
+
const protectedHeaderBase64url = Encoder.stringToBase64Url(JSON.stringify(protectedHeader));
|
|
64
329
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
336
|
+
// Wrap the CEK
|
|
337
|
+
const wrappedKey = await Encryption.ecdhEsWrapKey(
|
|
338
|
+
ephemeralPrivateKey,
|
|
339
|
+
keyInput.publicKey as Jwk,
|
|
340
|
+
encryptionInput.key,
|
|
341
|
+
);
|
|
73
342
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
343
|
+
const recipientHeader: JweRecipientHeader = {
|
|
344
|
+
kid : keyInput.publicKeyId,
|
|
345
|
+
epk : ephemeralPublicKey as PublicKeyJwk,
|
|
346
|
+
derivationScheme : keyInput.derivationScheme,
|
|
347
|
+
};
|
|
77
348
|
|
|
78
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
*
|
|
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
|
|
94
|
-
|
|
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
|
-
*
|
|
376
|
+
* Reads a ReadableStream to completion and returns all bytes concatenated.
|
|
110
377
|
*/
|
|
111
|
-
static
|
|
112
|
-
|
|
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
|
-
}
|
package/src/utils/hd-key.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
*
|
|
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 =
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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
|
+
}
|
package/src/utils/protocols.ts
CHANGED
|
@@ -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 {
|
|
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 (
|
|
122
|
-
await injectKeysViaCallback(
|
|
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
|
-
|
|
128
|
+
childRuleSet.$encryption = {
|
|
128
129
|
rootKeyId: keyDeriver.rootKeyId,
|
|
129
130
|
publicKeyJwk,
|
|
130
131
|
};
|
|
131
|
-
await injectKeysViaCallback(
|
|
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 (
|
|
153
|
-
await addEncryptionProperty(
|
|
154
|
+
if (childRuleSet.$ref !== undefined) {
|
|
155
|
+
await addEncryptionProperty(childRuleSet, derivedPrivateKey);
|
|
154
156
|
continue;
|
|
155
157
|
}
|
|
156
158
|
|
|
157
|
-
const publicKeyJwk = await
|
|
159
|
+
const publicKeyJwk = await X25519.getPublicKey({ key: derivedPrivateKey.derivedPrivateKey }) as PublicKeyJwk;
|
|
158
160
|
|
|
159
|
-
|
|
160
|
-
await addEncryptionProperty(
|
|
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
|
}
|