@enbox/agent 0.1.4 → 0.1.6
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 +11 -11
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/anonymous-dwn-api.js +184 -0
- package/dist/esm/anonymous-dwn-api.js.map +1 -0
- package/dist/esm/dwn-api.js +86 -777
- package/dist/esm/dwn-api.js.map +1 -1
- package/dist/esm/dwn-encryption.js +342 -0
- package/dist/esm/dwn-encryption.js.map +1 -0
- package/dist/esm/dwn-key-delivery.js +256 -0
- package/dist/esm/dwn-key-delivery.js.map +1 -0
- package/dist/esm/dwn-record-upgrade.js +119 -0
- package/dist/esm/dwn-record-upgrade.js.map +1 -0
- package/dist/esm/dwn-type-guards.js +23 -0
- package/dist/esm/dwn-type-guards.js.map +1 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/protocol-utils.js +158 -0
- package/dist/esm/protocol-utils.js.map +1 -0
- package/dist/esm/store-data-protocols.js +1 -1
- package/dist/esm/store-data-protocols.js.map +1 -1
- package/dist/esm/sync-engine-level.js +22 -353
- package/dist/esm/sync-engine-level.js.map +1 -1
- package/dist/esm/sync-messages.js +234 -0
- package/dist/esm/sync-messages.js.map +1 -0
- package/dist/esm/sync-topological-sort.js +143 -0
- package/dist/esm/sync-topological-sort.js.map +1 -0
- package/dist/esm/test-harness.js +20 -0
- package/dist/esm/test-harness.js.map +1 -1
- package/dist/types/anonymous-dwn-api.d.ts +140 -0
- package/dist/types/anonymous-dwn-api.d.ts.map +1 -0
- package/dist/types/dwn-api.d.ts +36 -179
- package/dist/types/dwn-api.d.ts.map +1 -1
- package/dist/types/dwn-encryption.d.ts +144 -0
- package/dist/types/dwn-encryption.d.ts.map +1 -0
- package/dist/types/dwn-key-delivery.d.ts +112 -0
- package/dist/types/dwn-key-delivery.d.ts.map +1 -0
- package/dist/types/dwn-record-upgrade.d.ts +33 -0
- package/dist/types/dwn-record-upgrade.d.ts.map +1 -0
- package/dist/types/dwn-type-guards.d.ts +9 -0
- package/dist/types/dwn-type-guards.d.ts.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/protocol-utils.d.ts +70 -0
- package/dist/types/protocol-utils.d.ts.map +1 -0
- package/dist/types/sync-engine-level.d.ts +5 -42
- package/dist/types/sync-engine-level.d.ts.map +1 -1
- package/dist/types/sync-messages.d.ts +76 -0
- package/dist/types/sync-messages.d.ts.map +1 -0
- package/dist/types/sync-topological-sort.d.ts +15 -0
- package/dist/types/sync-topological-sort.d.ts.map +1 -0
- package/dist/types/test-harness.d.ts +10 -0
- package/dist/types/test-harness.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/anonymous-dwn-api.ts +263 -0
- package/src/dwn-api.ts +160 -1015
- package/src/dwn-encryption.ts +481 -0
- package/src/dwn-key-delivery.ts +370 -0
- package/src/dwn-record-upgrade.ts +166 -0
- package/src/dwn-type-guards.ts +43 -0
- package/src/index.ts +6 -0
- package/src/protocol-utils.ts +185 -0
- package/src/store-data-protocols.ts +1 -1
- package/src/sync-engine-level.ts +24 -413
- package/src/sync-messages.ts +277 -0
- package/src/sync-topological-sort.ts +167 -0
- package/src/test-harness.ts +19 -0
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DerivedPrivateJwk,
|
|
3
|
+
EncryptionInput,
|
|
4
|
+
EncryptionKeyDeriver,
|
|
5
|
+
KeyDecrypter,
|
|
6
|
+
RecordsQueryReply,
|
|
7
|
+
RecordsReadReply,
|
|
8
|
+
RecordsWriteMessage,
|
|
9
|
+
} from '@enbox/dwn-sdk-js';
|
|
10
|
+
import type { KeyIdentifier, PublicKeyJwk } from '@enbox/crypto';
|
|
11
|
+
|
|
12
|
+
import type { Web5PlatformAgent } from './types/agent.js';
|
|
13
|
+
import type {
|
|
14
|
+
DwnMessageReply,
|
|
15
|
+
ProcessDwnRequest,
|
|
16
|
+
SendDwnRequest,
|
|
17
|
+
} from './types/dwn.js';
|
|
18
|
+
|
|
19
|
+
import { X25519 } from '@enbox/crypto';
|
|
20
|
+
import {
|
|
21
|
+
Cid,
|
|
22
|
+
ContentEncryptionAlgorithm,
|
|
23
|
+
DataStream,
|
|
24
|
+
Encoder,
|
|
25
|
+
Encryption,
|
|
26
|
+
KeyDerivationScheme,
|
|
27
|
+
Records,
|
|
28
|
+
} from '@enbox/dwn-sdk-js';
|
|
29
|
+
|
|
30
|
+
import { DwnInterface } from './types/dwn.js';
|
|
31
|
+
import { isDwnRequest } from './dwn-type-guards.js';
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Returns the correct nonce/IV byte length for the given content encryption algorithm.
|
|
35
|
+
* A256GCM uses 96-bit (12-byte) nonces; XC20P uses 192-bit (24-byte) nonces.
|
|
36
|
+
*/
|
|
37
|
+
export function ivLength(algorithm: ContentEncryptionAlgorithm): number {
|
|
38
|
+
return algorithm === ContentEncryptionAlgorithm.XC20P ? 24 : 12;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Builds a partial EncryptionInput object for a single key-encryption entry.
|
|
43
|
+
* The `authenticationTag` is NOT set here — the caller must set it after
|
|
44
|
+
* AEAD encryption produces the tag.
|
|
45
|
+
*/
|
|
46
|
+
export function buildEncryptionInput(
|
|
47
|
+
dek: Uint8Array,
|
|
48
|
+
iv: Uint8Array,
|
|
49
|
+
publicKeyId: string,
|
|
50
|
+
publicKey: PublicKeyJwk,
|
|
51
|
+
derivationScheme: typeof KeyDerivationScheme.ProtocolPath | typeof KeyDerivationScheme.ProtocolContext,
|
|
52
|
+
): Omit<EncryptionInput, 'authenticationTag'> {
|
|
53
|
+
return {
|
|
54
|
+
initializationVector : iv,
|
|
55
|
+
key : dek,
|
|
56
|
+
keyEncryptionInputs : [{
|
|
57
|
+
publicKeyId,
|
|
58
|
+
publicKey,
|
|
59
|
+
derivationScheme,
|
|
60
|
+
}],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Encrypts plaintext bytes with AEAD and computes the CID of the resulting ciphertext.
|
|
66
|
+
* Returns everything needed to attach the encrypted data to a DWN message, including
|
|
67
|
+
* the authentication tag.
|
|
68
|
+
*/
|
|
69
|
+
export async function encryptAndComputeCid(
|
|
70
|
+
plaintextBytes: Uint8Array,
|
|
71
|
+
dek: Uint8Array,
|
|
72
|
+
iv: Uint8Array,
|
|
73
|
+
algorithm: ContentEncryptionAlgorithm = ContentEncryptionAlgorithm.A256GCM,
|
|
74
|
+
): Promise<{ encryptedBytes: Uint8Array; dataCid: string; dataSize: number; authenticationTag: Uint8Array }> {
|
|
75
|
+
const { ciphertextStream, tag: authenticationTag } = await Encryption.aeadEncryptStream(
|
|
76
|
+
algorithm, dek, iv, DataStream.fromBytes(plaintextBytes),
|
|
77
|
+
);
|
|
78
|
+
const encryptedBytes = await DataStream.toBytes(ciphertextStream);
|
|
79
|
+
const cidStream = DataStream.fromBytes(encryptedBytes);
|
|
80
|
+
const dataCid = await Cid.computeDagPbCidFromStream(cidStream);
|
|
81
|
+
return { encryptedBytes, dataCid, dataSize: encryptedBytes.length, authenticationTag };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolves the encryption key info for a given DID.
|
|
86
|
+
* Looks up the keyAgreement verification method in the DID document,
|
|
87
|
+
* then resolves the corresponding KMS key URI.
|
|
88
|
+
*
|
|
89
|
+
* @param agent - The platform agent to use for DID resolution and key management
|
|
90
|
+
* @param didUri - The DID URI to resolve encryption key info for
|
|
91
|
+
* @returns keyId (fully qualified verification method ID), keyUri (KMS reference),
|
|
92
|
+
* and publicKeyJwk. No private key material is returned.
|
|
93
|
+
* @throws If the DID has no keyAgreement verification method or it's not X25519.
|
|
94
|
+
*/
|
|
95
|
+
export async function getEncryptionKeyInfo(
|
|
96
|
+
agent: Web5PlatformAgent,
|
|
97
|
+
didUri: string,
|
|
98
|
+
): Promise<{
|
|
99
|
+
keyId: string;
|
|
100
|
+
keyUri: KeyIdentifier;
|
|
101
|
+
publicKeyJwk: PublicKeyJwk;
|
|
102
|
+
}> {
|
|
103
|
+
// 1. Resolve the DID document
|
|
104
|
+
const { didDocument, didResolutionMetadata } = await agent.did.resolve(didUri);
|
|
105
|
+
if (!didDocument) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
`AgentDwnApi: Failed to resolve DID '${didUri}': ` +
|
|
108
|
+
`${JSON.stringify(didResolutionMetadata)}`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 2. Find the keyAgreement verification method
|
|
113
|
+
const keyAgreementRefs = didDocument.keyAgreement;
|
|
114
|
+
if (!keyAgreementRefs || keyAgreementRefs.length === 0) {
|
|
115
|
+
throw new Error(
|
|
116
|
+
`AgentDwnApi: DID '${didUri}' does not have a keyAgreement ` +
|
|
117
|
+
`verification method. Create the identity with an X25519 key ` +
|
|
118
|
+
`with keyAgreement purpose to use protocol encryption.`
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 3. Resolve the verification method (handle both inline and string refs)
|
|
123
|
+
const keyAgreementRef = keyAgreementRefs[0];
|
|
124
|
+
let verificationMethod;
|
|
125
|
+
if (typeof keyAgreementRef === 'string') {
|
|
126
|
+
const fragment = keyAgreementRef.includes('#')
|
|
127
|
+
? keyAgreementRef.split('#').pop()
|
|
128
|
+
: keyAgreementRef;
|
|
129
|
+
verificationMethod = didDocument.verificationMethod?.find(
|
|
130
|
+
vm => vm.id.endsWith(`#${fragment}`)
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
verificationMethod = keyAgreementRef;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!verificationMethod?.publicKeyJwk) {
|
|
137
|
+
throw new Error(
|
|
138
|
+
`AgentDwnApi: keyAgreement verification method for '${didUri}' ` +
|
|
139
|
+
`does not contain a public key in JWK format.`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 4. Verify it's an X25519 key
|
|
144
|
+
const publicKeyJwk = verificationMethod.publicKeyJwk;
|
|
145
|
+
if (publicKeyJwk.crv !== 'X25519') {
|
|
146
|
+
throw new Error(
|
|
147
|
+
`AgentDwnApi: keyAgreement key for '${didUri}' uses curve ` +
|
|
148
|
+
`'${publicKeyJwk.crv}', but DWN encryption requires 'X25519'.`
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 5. Compute the KMS key URI (does NOT export the key)
|
|
153
|
+
const keyUri = await agent.keyManager.getKeyUri({ key: publicKeyJwk });
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
keyId : verificationMethod.id,
|
|
157
|
+
keyUri,
|
|
158
|
+
publicKeyJwk : publicKeyJwk as PublicKeyJwk,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Derives a ProtocolContext public key for a given DID and context ID,
|
|
164
|
+
* then returns a fully-formed EncryptionInput. Consolidates the repeated
|
|
165
|
+
* getEncryptionKeyInfo -> constructKeyDerivationPath -> derivePublicKey
|
|
166
|
+
* -> build EncryptionInput sequence.
|
|
167
|
+
*
|
|
168
|
+
* @param agent - The platform agent
|
|
169
|
+
* @param didUri - The DID URI to derive encryption key for
|
|
170
|
+
* @param contextId - The context ID
|
|
171
|
+
* @param dek - Data encryption key
|
|
172
|
+
* @param iv - Initialization vector
|
|
173
|
+
*/
|
|
174
|
+
export async function deriveContextEncryptionInput(
|
|
175
|
+
agent: Web5PlatformAgent,
|
|
176
|
+
didUri: string,
|
|
177
|
+
contextId: string,
|
|
178
|
+
dek: Uint8Array,
|
|
179
|
+
iv: Uint8Array,
|
|
180
|
+
): Promise<{
|
|
181
|
+
encryptionInput: Omit<EncryptionInput, 'authenticationTag'>;
|
|
182
|
+
keyId: string;
|
|
183
|
+
keyUri: KeyIdentifier;
|
|
184
|
+
contextDerivationPath: string[];
|
|
185
|
+
}> {
|
|
186
|
+
const { keyId, keyUri } = await getEncryptionKeyInfo(agent, didUri);
|
|
187
|
+
const contextDerivationPath =
|
|
188
|
+
Records.constructKeyDerivationPathUsingProtocolContextScheme(contextId);
|
|
189
|
+
const contextPublicKey = await agent.keyManager.derivePublicKey({
|
|
190
|
+
keyUri,
|
|
191
|
+
derivationPath: contextDerivationPath,
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const encryptionInput = buildEncryptionInput(
|
|
195
|
+
dek, iv, keyId, contextPublicKey, KeyDerivationScheme.ProtocolContext,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
return { encryptionInput, keyId, keyUri, contextDerivationPath };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Builds a KMS-backed JWE key unwrap callback. Used for both ProtocolPath
|
|
203
|
+
* and ProtocolContext decryption where the KMS holds the root private key.
|
|
204
|
+
*
|
|
205
|
+
* @param agent - The platform agent with access to the key manager
|
|
206
|
+
* @param keyId - The root key ID
|
|
207
|
+
* @param keyUri - The KMS key URI
|
|
208
|
+
* @param derivationScheme - The key derivation scheme
|
|
209
|
+
*/
|
|
210
|
+
export function buildKmsDecryptCallback(
|
|
211
|
+
agent: Web5PlatformAgent,
|
|
212
|
+
keyId: string,
|
|
213
|
+
keyUri: KeyIdentifier,
|
|
214
|
+
derivationScheme: typeof KeyDerivationScheme.ProtocolPath | typeof KeyDerivationScheme.ProtocolContext,
|
|
215
|
+
): KeyDecrypter {
|
|
216
|
+
const keyManager = agent.keyManager;
|
|
217
|
+
return {
|
|
218
|
+
rootKeyId : keyId,
|
|
219
|
+
derivationScheme,
|
|
220
|
+
decrypt : async (fullDerivationPath, jwePayload): Promise<Uint8Array> => {
|
|
221
|
+
return keyManager.jweKeyUnwrap({
|
|
222
|
+
keyUri,
|
|
223
|
+
derivationPath : fullDerivationPath,
|
|
224
|
+
encryptedKey : jwePayload.encryptedKey,
|
|
225
|
+
ephemeralPublicKey : jwePayload.ephemeralPublicKey,
|
|
226
|
+
});
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Constructs an EncryptionKeyDeriver callback for the SDK.
|
|
233
|
+
* The SDK calls derivePublicKey(path), the KMS performs HKDF + public key
|
|
234
|
+
* computation internally. The private key never leaves the KMS.
|
|
235
|
+
*
|
|
236
|
+
* Analogous to getSigner() for signing operations.
|
|
237
|
+
*
|
|
238
|
+
* @param agent - The platform agent
|
|
239
|
+
* @param didUri - The DID URI to create the key deriver for
|
|
240
|
+
* @returns An EncryptionKeyDeriver callback object
|
|
241
|
+
*/
|
|
242
|
+
export async function getEncryptionKeyDeriver(
|
|
243
|
+
agent: Web5PlatformAgent,
|
|
244
|
+
didUri: string,
|
|
245
|
+
): Promise<EncryptionKeyDeriver> {
|
|
246
|
+
const { keyId, keyUri } = await getEncryptionKeyInfo(agent, didUri);
|
|
247
|
+
const keyManager = agent.keyManager;
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
rootKeyId : keyId,
|
|
251
|
+
derivationScheme : KeyDerivationScheme.ProtocolPath,
|
|
252
|
+
derivePublicKey : async (fullDerivationPath: string[]): Promise<PublicKeyJwk> => {
|
|
253
|
+
return keyManager.derivePublicKey({
|
|
254
|
+
keyUri,
|
|
255
|
+
derivationPath: fullDerivationPath,
|
|
256
|
+
});
|
|
257
|
+
},
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Constructs a ProtocolPath KeyDecrypter.
|
|
263
|
+
*
|
|
264
|
+
* @param agent - The platform agent
|
|
265
|
+
* @param didUri - The DID URI to create the key decrypter for
|
|
266
|
+
* @returns A KeyDecrypter callback object
|
|
267
|
+
*/
|
|
268
|
+
export async function getKeyDecrypter(
|
|
269
|
+
agent: Web5PlatformAgent,
|
|
270
|
+
didUri: string,
|
|
271
|
+
): Promise<KeyDecrypter> {
|
|
272
|
+
const { keyId, keyUri } = await getEncryptionKeyInfo(agent, didUri);
|
|
273
|
+
return buildKmsDecryptCallback(agent, keyId, keyUri, KeyDerivationScheme.ProtocolPath);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Builds a KeyDecrypter from a context-derived private key.
|
|
278
|
+
* Uses the raw key directly (since it was shared with us via the key-delivery protocol).
|
|
279
|
+
*
|
|
280
|
+
* @param contextKey - The derived private key for the context
|
|
281
|
+
*/
|
|
282
|
+
export function buildContextKeyDecrypter(
|
|
283
|
+
contextKey: DerivedPrivateJwk,
|
|
284
|
+
): KeyDecrypter {
|
|
285
|
+
return {
|
|
286
|
+
rootKeyId : contextKey.rootKeyId,
|
|
287
|
+
derivationScheme : contextKey.derivationScheme,
|
|
288
|
+
decrypt : async (fullDerivationPath, jwePayload): Promise<Uint8Array> => {
|
|
289
|
+
const leafPrivateKeyBytes = await Records.derivePrivateKey(
|
|
290
|
+
contextKey, fullDerivationPath,
|
|
291
|
+
);
|
|
292
|
+
const leafPrivateKeyJwk = await X25519.bytesToPrivateKey({ privateKeyBytes: leafPrivateKeyBytes });
|
|
293
|
+
return Encryption.ecdhEsUnwrapKey(leafPrivateKeyJwk, jwePayload.ephemeralPublicKey, jwePayload.encryptedKey);
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Resolves the appropriate KeyDecrypter for a record's encryption scheme.
|
|
300
|
+
* Handles both single-party (ProtocolPath) and multi-party (ProtocolContext).
|
|
301
|
+
*
|
|
302
|
+
* For ProtocolContext records:
|
|
303
|
+
* - Context creator: derives key directly from KMS
|
|
304
|
+
* - Participant: fetches contextKey via key-delivery protocol, caches it
|
|
305
|
+
*
|
|
306
|
+
* @param agent - The platform agent
|
|
307
|
+
* @param authorDid - The DID of the author attempting to decrypt
|
|
308
|
+
* @param recordsWrite - The records write message containing encryption info
|
|
309
|
+
* @param targetDid - The target DID (DWN owner), if known
|
|
310
|
+
* @param contextDerivedKeyCache - Cache for context-derived private keys
|
|
311
|
+
* @param fetchContextKeyRecordFn - Function to fetch context key records from key-delivery protocol
|
|
312
|
+
*/
|
|
313
|
+
export async function resolveKeyDecrypter(
|
|
314
|
+
agent: Web5PlatformAgent,
|
|
315
|
+
authorDid: string,
|
|
316
|
+
recordsWrite: RecordsWriteMessage,
|
|
317
|
+
targetDid: string | undefined,
|
|
318
|
+
contextDerivedKeyCache: { get(key: string): DerivedPrivateJwk | undefined; set(key: string, value: DerivedPrivateJwk): void },
|
|
319
|
+
fetchContextKeyRecordFn: (params: {
|
|
320
|
+
ownerDid: string;
|
|
321
|
+
requesterDid: string;
|
|
322
|
+
sourceProtocol: string;
|
|
323
|
+
sourceContextId: string;
|
|
324
|
+
}) => Promise<DerivedPrivateJwk | undefined>,
|
|
325
|
+
): Promise<KeyDecrypter> {
|
|
326
|
+
const { encryption } = recordsWrite;
|
|
327
|
+
|
|
328
|
+
// Check if the record uses context-derived encryption
|
|
329
|
+
const hasContextKey = encryption?.recipients.some(
|
|
330
|
+
(r: { header: { derivationScheme: string } }) => r.header.derivationScheme === KeyDerivationScheme.ProtocolContext
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
if (!hasContextKey || !recordsWrite.contextId) {
|
|
334
|
+
// Single-party protocol-path encryption
|
|
335
|
+
return getKeyDecrypter(agent, authorDid);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// --- Multi-party context encryption ---
|
|
339
|
+
const contextKeyEntry = encryption!.recipients.find(
|
|
340
|
+
(r: { header: { derivationScheme: string } }) => r.header.derivationScheme === KeyDerivationScheme.ProtocolContext
|
|
341
|
+
)!;
|
|
342
|
+
|
|
343
|
+
const rootContextId = recordsWrite.contextId.split('/')[0];
|
|
344
|
+
|
|
345
|
+
// Case 1: I am the context creator — rootKeyId matches my encryption key
|
|
346
|
+
const { keyId, keyUri } = await getEncryptionKeyInfo(agent, authorDid);
|
|
347
|
+
if (contextKeyEntry.header.kid === keyId) {
|
|
348
|
+
return buildKmsDecryptCallback(agent, keyId, keyUri, KeyDerivationScheme.ProtocolContext);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Case 2: I am a participant — fetch my context key from the key-delivery protocol
|
|
352
|
+
const cacheKey = `ctx~${authorDid}~${rootContextId}`;
|
|
353
|
+
const cached = contextDerivedKeyCache.get(cacheKey);
|
|
354
|
+
if (cached) {
|
|
355
|
+
return buildContextKeyDecrypter(cached);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Fetch context key via the key-delivery protocol — local first, then remote
|
|
359
|
+
const protocol = recordsWrite.descriptor.protocol!;
|
|
360
|
+
|
|
361
|
+
// Try local: I may be the DWN owner with a contextKey addressed to myself
|
|
362
|
+
let contextDerivedPrivateKey = await fetchContextKeyRecordFn({
|
|
363
|
+
ownerDid : authorDid,
|
|
364
|
+
requesterDid : authorDid,
|
|
365
|
+
sourceProtocol : protocol,
|
|
366
|
+
sourceContextId : rootContextId,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
// Try remote: query the DWN owner's DWN for my contextKey record.
|
|
370
|
+
// For cross-DWN records, targetDid is the DWN owner (e.g., Alice) where the
|
|
371
|
+
// contextKey was written. For same-DWN records, fall back to the record's
|
|
372
|
+
// authorization signer.
|
|
373
|
+
if (!contextDerivedPrivateKey) {
|
|
374
|
+
const { Jws } = await import('@enbox/dwn-sdk-js');
|
|
375
|
+
const contextOwnerDid = targetDid ?? Jws.getSignerDid(
|
|
376
|
+
recordsWrite.authorization.signature.signatures[0]
|
|
377
|
+
);
|
|
378
|
+
contextDerivedPrivateKey = await fetchContextKeyRecordFn({
|
|
379
|
+
ownerDid : contextOwnerDid,
|
|
380
|
+
requesterDid : authorDid,
|
|
381
|
+
sourceProtocol : protocol,
|
|
382
|
+
sourceContextId : rootContextId,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (!contextDerivedPrivateKey) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`AgentDwnApi: Failed to decrypt record '${recordsWrite.recordId}'. ` +
|
|
389
|
+
`Record uses context-derived encryption but no contextKey record ` +
|
|
390
|
+
`could be found via the key-delivery protocol.`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
contextDerivedKeyCache.set(cacheKey, contextDerivedPrivateKey);
|
|
395
|
+
return buildContextKeyDecrypter(contextDerivedPrivateKey);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Post-processes a DWN reply, auto-decrypting data if encryption is enabled.
|
|
400
|
+
* Delegates to the SDK's Records.decrypt() with the appropriate KeyDecrypter —
|
|
401
|
+
* resolveKeyDecrypter() selects between ProtocolPath and ProtocolContext schemes.
|
|
402
|
+
*
|
|
403
|
+
* @param request - The original DWN request
|
|
404
|
+
* @param reply - The DWN reply to process
|
|
405
|
+
* @param agent - The platform agent
|
|
406
|
+
* @param contextDerivedKeyCache - Cache for context-derived private keys
|
|
407
|
+
* @param fetchContextKeyRecordFn - Function to fetch context key records
|
|
408
|
+
*/
|
|
409
|
+
export async function maybeDecryptReply<T extends DwnInterface>(
|
|
410
|
+
request: ProcessDwnRequest<T> | SendDwnRequest<T>,
|
|
411
|
+
reply: DwnMessageReply[T],
|
|
412
|
+
agent: Web5PlatformAgent,
|
|
413
|
+
contextDerivedKeyCache: { get(key: string): DerivedPrivateJwk | undefined; set(key: string, value: DerivedPrivateJwk): void },
|
|
414
|
+
fetchContextKeyRecordFn: (params: {
|
|
415
|
+
ownerDid: string;
|
|
416
|
+
requesterDid: string;
|
|
417
|
+
sourceProtocol: string;
|
|
418
|
+
sourceContextId: string;
|
|
419
|
+
}) => Promise<DerivedPrivateJwk | undefined>,
|
|
420
|
+
): Promise<void> {
|
|
421
|
+
if (!('encryption' in request) || !request.encryption) {
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Auto-decrypt RecordsRead replies
|
|
426
|
+
if (isDwnRequest(request as ProcessDwnRequest<DwnInterface>, DwnInterface.RecordsRead)) {
|
|
427
|
+
const readReply = reply as RecordsReadReply;
|
|
428
|
+
if (readReply.status.code === 200
|
|
429
|
+
&& readReply.entry?.recordsWrite?.encryption
|
|
430
|
+
&& readReply.entry?.data) {
|
|
431
|
+
const keyDecrypter = await resolveKeyDecrypter(
|
|
432
|
+
agent, request.author, readReply.entry.recordsWrite, request.target,
|
|
433
|
+
contextDerivedKeyCache, fetchContextKeyRecordFn,
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
readReply.entry.data = await Records.decrypt(
|
|
438
|
+
readReply.entry.recordsWrite,
|
|
439
|
+
keyDecrypter,
|
|
440
|
+
readReply.entry.data,
|
|
441
|
+
);
|
|
442
|
+
} catch (error: any) {
|
|
443
|
+
throw new Error(
|
|
444
|
+
`AgentDwnApi: Failed to decrypt record ` +
|
|
445
|
+
`'${readReply.entry.recordsWrite.recordId}'. ` +
|
|
446
|
+
`Original error: ${error.message}`
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
// Auto-decrypt RecordsQuery replies (small records inline as encodedData)
|
|
453
|
+
if (isDwnRequest(request as ProcessDwnRequest<DwnInterface>, DwnInterface.RecordsQuery)) {
|
|
454
|
+
const queryReply = reply as RecordsQueryReply;
|
|
455
|
+
if (queryReply.status.code === 200 && queryReply.entries) {
|
|
456
|
+
for (const entry of queryReply.entries) {
|
|
457
|
+
if (entry.encryption && entry.encodedData) {
|
|
458
|
+
const keyDecrypter = await resolveKeyDecrypter(
|
|
459
|
+
agent, request.author, entry as RecordsWriteMessage, request.target,
|
|
460
|
+
contextDerivedKeyCache, fetchContextKeyRecordFn,
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
try {
|
|
464
|
+
const cipherBytes = Encoder.base64UrlToBytes(entry.encodedData);
|
|
465
|
+
const cipherStream = DataStream.fromBytes(cipherBytes);
|
|
466
|
+
const plainStream = await Records.decrypt(
|
|
467
|
+
entry as RecordsWriteMessage, keyDecrypter, cipherStream,
|
|
468
|
+
);
|
|
469
|
+
const plainBytes = await DataStream.toBytes(plainStream);
|
|
470
|
+
entry.encodedData = Encoder.bytesToBase64Url(plainBytes);
|
|
471
|
+
} catch (error: any) {
|
|
472
|
+
throw new Error(
|
|
473
|
+
`AgentDwnApi: Failed to decrypt record ` +
|
|
474
|
+
`'${entry.recordId}'. Original error: ${error.message}`
|
|
475
|
+
);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|