@enbox/agent 0.1.5 → 0.1.7
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 +85 -785
- 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/permissions-api.js +43 -2
- package/dist/esm/permissions-api.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/store-data.js +3 -0
- package/dist/esm/store-data.js.map +1 -1
- package/dist/esm/sync-engine-level.js +23 -354
- package/dist/esm/sync-engine-level.js.map +1 -1
- package/dist/esm/sync-messages.js +237 -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 -184
- 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/permissions-api.d.ts +6 -1
- package/dist/types/permissions-api.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/store-data.d.ts +4 -0
- package/dist/types/store-data.d.ts.map +1 -1
- 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/dist/types/types/permissions.d.ts +2 -0
- package/dist/types/types/permissions.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/anonymous-dwn-api.ts +263 -0
- package/src/dwn-api.ts +158 -1024
- 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/permissions-api.ts +54 -2
- package/src/protocol-utils.ts +185 -0
- package/src/store-data-protocols.ts +1 -1
- package/src/store-data.ts +5 -2
- package/src/sync-engine-level.ts +25 -414
- package/src/sync-messages.ts +279 -0
- package/src/sync-topological-sort.ts +167 -0
- package/src/test-harness.ts +19 -0
- package/src/types/permissions.ts +2 -0
package/src/dwn-api.ts
CHANGED
|
@@ -3,16 +3,13 @@ import type {
|
|
|
3
3
|
DwnConfig,
|
|
4
4
|
EncryptionInput,
|
|
5
5
|
EncryptionKeyDeriver,
|
|
6
|
-
GenericMessage,
|
|
7
6
|
KeyDecrypter,
|
|
8
7
|
ProtocolDefinition,
|
|
9
|
-
ProtocolRuleSet,
|
|
10
8
|
ProtocolsQueryReply,
|
|
11
9
|
RecordsQueryReply,
|
|
12
|
-
RecordsQueryReplyEntry,
|
|
13
|
-
RecordsReadReply,
|
|
14
10
|
RecordsWrite,
|
|
15
|
-
RecordsWriteMessage
|
|
11
|
+
RecordsWriteMessage,
|
|
12
|
+
} from '@enbox/dwn-sdk-js';
|
|
16
13
|
import type { KeyIdentifier, PrivateKeyJwk, PublicKeyJwk } from '@enbox/crypto';
|
|
17
14
|
|
|
18
15
|
import { TtlCache } from '@enbox/common';
|
|
@@ -22,10 +19,7 @@ import {
|
|
|
22
19
|
DataStoreLevel,
|
|
23
20
|
DataStream,
|
|
24
21
|
Dwn,
|
|
25
|
-
DwnInterfaceName,
|
|
26
22
|
DwnMethodName,
|
|
27
|
-
Encoder,
|
|
28
|
-
Encryption,
|
|
29
23
|
EventEmitterStream,
|
|
30
24
|
Jws,
|
|
31
25
|
KeyDerivationScheme,
|
|
@@ -34,7 +28,7 @@ import {
|
|
|
34
28
|
Protocols,
|
|
35
29
|
Records,
|
|
36
30
|
ResumableTaskStoreLevel,
|
|
37
|
-
StateIndexLevel
|
|
31
|
+
StateIndexLevel,
|
|
38
32
|
} from '@enbox/dwn-sdk-js';
|
|
39
33
|
import { CryptoUtils, X25519 } from '@enbox/crypto';
|
|
40
34
|
import { DidDht, DidJwk, DidResolverCacheLevel, UniversalResolver } from '@enbox/dids';
|
|
@@ -45,22 +39,54 @@ import type {
|
|
|
45
39
|
DwnMessageInstance,
|
|
46
40
|
DwnMessageParams,
|
|
47
41
|
DwnMessageReply,
|
|
48
|
-
DwnMessagesPermissionScope,
|
|
49
42
|
DwnMessageWithData,
|
|
50
|
-
DwnPermissionScope,
|
|
51
|
-
DwnRecordsInterfaces,
|
|
52
|
-
DwnRecordsPermissionScope,
|
|
53
43
|
DwnResponse,
|
|
54
44
|
DwnSigner,
|
|
55
45
|
MessageHandler,
|
|
56
46
|
ProcessDwnRequest,
|
|
57
|
-
SendDwnRequest
|
|
47
|
+
SendDwnRequest,
|
|
58
48
|
} from './types/dwn.js';
|
|
59
49
|
|
|
60
50
|
import { KeyDeliveryProtocolDefinition } from './store-data-protocols.js';
|
|
61
51
|
import { DwnInterface, dwnMessageConstructors } from './types/dwn.js';
|
|
62
52
|
import { getDwnServiceEndpointUrls, isRecordsWrite } from './utils.js';
|
|
63
53
|
|
|
54
|
+
// Re-export type guards for backward compatibility
|
|
55
|
+
export { isDwnMessage, isDwnRequest, isMessagesPermissionScope, isRecordPermissionScope, isRecordsType } from './dwn-type-guards.js';
|
|
56
|
+
|
|
57
|
+
// Import type guards for internal use
|
|
58
|
+
import { isDwnRequest } from './dwn-type-guards.js';
|
|
59
|
+
|
|
60
|
+
// Import extracted encryption functions
|
|
61
|
+
import {
|
|
62
|
+
buildEncryptionInput as buildEncryptionInputFn,
|
|
63
|
+
deriveContextEncryptionInput as deriveContextEncryptionInputFn,
|
|
64
|
+
encryptAndComputeCid as encryptAndComputeCidFn,
|
|
65
|
+
getEncryptionKeyDeriver as getEncryptionKeyDeriverFn,
|
|
66
|
+
getEncryptionKeyInfo as getEncryptionKeyInfoFn,
|
|
67
|
+
getKeyDecrypter as getKeyDecrypterFn,
|
|
68
|
+
ivLength as ivLengthFn,
|
|
69
|
+
maybeDecryptReply as maybeDecryptReplyFn,
|
|
70
|
+
} from './dwn-encryption.js';
|
|
71
|
+
|
|
72
|
+
// Import extracted protocol utilities
|
|
73
|
+
import {
|
|
74
|
+
detectNewParticipants as detectNewParticipantsFn,
|
|
75
|
+
hasRelationalReadAccess as hasRelationalReadAccessFn,
|
|
76
|
+
isMultiPartyContext as isMultiPartyContextFn,
|
|
77
|
+
} from './protocol-utils.js';
|
|
78
|
+
|
|
79
|
+
// Import extracted key delivery functions
|
|
80
|
+
import {
|
|
81
|
+
eagerSendContextKeyRecord as eagerSendContextKeyRecordFn,
|
|
82
|
+
ensureKeyDeliveryProtocol as ensureKeyDeliveryProtocolFn,
|
|
83
|
+
fetchContextKeyRecord as fetchContextKeyRecordFn,
|
|
84
|
+
writeContextKeyRecord as writeContextKeyRecordFn,
|
|
85
|
+
} from './dwn-key-delivery.js';
|
|
86
|
+
|
|
87
|
+
// Import extracted record upgrade function
|
|
88
|
+
import { upgradeExternalRootRecord as upgradeExternalRootRecordFn } from './dwn-record-upgrade.js';
|
|
89
|
+
|
|
64
90
|
type DwnMessageWithBlob<T extends DwnInterface> = {
|
|
65
91
|
message: DwnMessage[T];
|
|
66
92
|
data?: Blob;
|
|
@@ -75,35 +101,6 @@ interface DwnApiCreateDwnParams extends Partial<DwnConfig> {
|
|
|
75
101
|
dataPath?: string;
|
|
76
102
|
}
|
|
77
103
|
|
|
78
|
-
export function isDwnRequest<T extends DwnInterface>(
|
|
79
|
-
dwnRequest: ProcessDwnRequest<DwnInterface>, messageType: T
|
|
80
|
-
): dwnRequest is ProcessDwnRequest<T> {
|
|
81
|
-
return dwnRequest.messageType === messageType;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export function isDwnMessage<T extends DwnInterface>(
|
|
85
|
-
messageType: T, message: GenericMessage
|
|
86
|
-
): message is DwnMessage[T] {
|
|
87
|
-
const incomingMessageInterfaceName = message.descriptor.interface + message.descriptor.method;
|
|
88
|
-
return incomingMessageInterfaceName === messageType;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export function isRecordsType(messageType: DwnInterface): messageType is DwnRecordsInterfaces {
|
|
92
|
-
return messageType === DwnInterface.RecordsDelete ||
|
|
93
|
-
messageType === DwnInterface.RecordsQuery ||
|
|
94
|
-
messageType === DwnInterface.RecordsRead ||
|
|
95
|
-
messageType === DwnInterface.RecordsSubscribe ||
|
|
96
|
-
messageType === DwnInterface.RecordsWrite;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export function isRecordPermissionScope(scope: DwnPermissionScope): scope is DwnRecordsPermissionScope {
|
|
100
|
-
return scope.interface === DwnInterfaceName.Records;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
export function isMessagesPermissionScope(scope: DwnPermissionScope): scope is DwnMessagesPermissionScope {
|
|
104
|
-
return scope.interface === DwnInterfaceName.Messages;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
104
|
export class AgentDwnApi {
|
|
108
105
|
/**
|
|
109
106
|
* Holds the instance of a `Web5PlatformAgent` that represents the current execution context for
|
|
@@ -240,7 +237,7 @@ export class AgentDwnApi {
|
|
|
240
237
|
) {
|
|
241
238
|
const writeParams = request.messageParams as DwnMessageParams[DwnInterface.RecordsWrite];
|
|
242
239
|
// Skip key-delivery protocol writes to avoid infinite recursion (contextKey records are themselves encrypted)
|
|
243
|
-
if (writeParams
|
|
240
|
+
if (writeParams.protocol !== KeyDeliveryProtocolDefinition.protocol) {
|
|
244
241
|
try {
|
|
245
242
|
const protocolDefinition = await this.getProtocolDefinition(
|
|
246
243
|
request.target, writeParams.protocol,
|
|
@@ -258,11 +255,14 @@ export class AgentDwnApi {
|
|
|
258
255
|
const isExternallyAuthored = authorDid !== request.target;
|
|
259
256
|
const isRootRecord = !writeParams.parentContextId;
|
|
260
257
|
const rootPathSegment = writeParams.protocolPath.split('/')[0];
|
|
261
|
-
const isMultiParty =
|
|
258
|
+
const isMultiParty = isMultiPartyContextFn(protocolDefinition, rootPathSegment);
|
|
262
259
|
|
|
263
260
|
if (isExternallyAuthored && isRootRecord && isMultiParty) {
|
|
264
261
|
try {
|
|
265
|
-
await
|
|
262
|
+
await upgradeExternalRootRecordFn(
|
|
263
|
+
this.agent, request.target, recordsWriteMessage,
|
|
264
|
+
this._dwn, this.getSigner.bind(this), this._contextKeyCache,
|
|
265
|
+
);
|
|
266
266
|
} catch (upgradeError: any) {
|
|
267
267
|
console.warn(
|
|
268
268
|
`AgentDwnApi: Reactive root-record upgrade failed for ` +
|
|
@@ -271,7 +271,7 @@ export class AgentDwnApi {
|
|
|
271
271
|
}
|
|
272
272
|
}
|
|
273
273
|
|
|
274
|
-
const newParticipants =
|
|
274
|
+
const newParticipants = detectNewParticipantsFn({
|
|
275
275
|
protocolDefinition,
|
|
276
276
|
protocolPath : writeParams.protocolPath,
|
|
277
277
|
recipient : writeParams.recipient,
|
|
@@ -285,7 +285,7 @@ export class AgentDwnApi {
|
|
|
285
285
|
|| recordsWriteMessage.contextId
|
|
286
286
|
|| recordsWriteMessage.recordId;
|
|
287
287
|
|
|
288
|
-
const { keyId, keyUri } = await this.
|
|
288
|
+
const { keyId, keyUri } = await getEncryptionKeyInfoFn(this.agent, request.target);
|
|
289
289
|
const contextDerivationPath = [
|
|
290
290
|
KeyDerivationScheme.ProtocolContext,
|
|
291
291
|
rootContextId,
|
|
@@ -515,7 +515,7 @@ export class AgentDwnApi {
|
|
|
515
515
|
// Auto-inject encryption keys into protocol definition (Component 5)
|
|
516
516
|
if (isDwnRequest(request, DwnInterface.ProtocolsConfigure) && request.encryption && !rawMessage) {
|
|
517
517
|
const messageParams = request.messageParams!;
|
|
518
|
-
const keyDeriver = await this.
|
|
518
|
+
const keyDeriver = await getEncryptionKeyDeriverFn(this.agent, request.author);
|
|
519
519
|
|
|
520
520
|
// SDK walks the protocol structure and calls our callback for each path.
|
|
521
521
|
// The KMS performs HKDF derivation + public key computation internally.
|
|
@@ -557,19 +557,6 @@ export class AgentDwnApi {
|
|
|
557
557
|
// public key (derivedPublicKey) from existing ProtocolContext-encrypted records
|
|
558
558
|
// in the same context on the target's DWN.
|
|
559
559
|
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
560
|
// Tracks deferred context encryption info for root multi-party records.
|
|
574
561
|
let deferredContextEncryption: {
|
|
575
562
|
dataEncryptionKey: Uint8Array;
|
|
@@ -630,7 +617,7 @@ export class AgentDwnApi {
|
|
|
630
617
|
messageParams.protocol === KeyDeliveryProtocolDefinition.protocol;
|
|
631
618
|
const isMultiPartyContext = isKeyDeliveryProtocol
|
|
632
619
|
? false
|
|
633
|
-
:
|
|
620
|
+
: isMultiPartyContextFn(protocolDefinition, rootPathSegment);
|
|
634
621
|
const isRootRecord = !messageParams.parentContextId;
|
|
635
622
|
|
|
636
623
|
// 4. Get plaintext bytes (normalize from all supported input types)
|
|
@@ -650,29 +637,23 @@ export class AgentDwnApi {
|
|
|
650
637
|
// 5. Generate random DEK and IV (IV size depends on content encryption algorithm)
|
|
651
638
|
const contentEncryptionAlgorithm = ContentEncryptionAlgorithm.A256GCM;
|
|
652
639
|
const dataEncryptionKey = crypto.getRandomValues(new Uint8Array(32));
|
|
653
|
-
const dataEncryptionIV = crypto.getRandomValues(new Uint8Array(
|
|
640
|
+
const dataEncryptionIV = crypto.getRandomValues(new Uint8Array(ivLengthFn(contentEncryptionAlgorithm)));
|
|
654
641
|
|
|
655
642
|
// 6. Build partial EncryptionInput (authenticationTag added after AEAD encryption)
|
|
656
643
|
let encryptionInput: (Omit<EncryptionInput, 'authenticationTag'> & { authenticationTag?: Uint8Array }) | undefined;
|
|
657
644
|
|
|
658
|
-
const buildProtocolPathInput = (): Omit<EncryptionInput, 'authenticationTag'> =>
|
|
645
|
+
const buildProtocolPathInput = (): Omit<EncryptionInput, 'authenticationTag'> => buildEncryptionInputFn(
|
|
659
646
|
dataEncryptionKey, dataEncryptionIV,
|
|
660
647
|
ruleSet.$encryption.rootKeyId, ruleSet.$encryption.publicKeyJwk,
|
|
661
648
|
KeyDerivationScheme.ProtocolPath,
|
|
662
649
|
);
|
|
663
650
|
|
|
664
651
|
if (isCrossDwn && isMultiPartyContext && isRootRecord) {
|
|
665
|
-
// --- Cross-DWN root record in multi-party context
|
|
666
|
-
// External authors cannot derive the target's context key (HKDF requires
|
|
667
|
-
// the private key). Use the target's ProtocolPath public key from their
|
|
668
|
-
// protocol definition. The target's agent will reactively upgrade the record
|
|
669
|
-
// to include a ProtocolContext recipient entry.
|
|
652
|
+
// --- Cross-DWN root record in multi-party context -> Target's ProtocolPath key ---
|
|
670
653
|
encryptionInput = buildProtocolPathInput();
|
|
671
654
|
|
|
672
655
|
} else if (isCrossDwn && isMultiPartyContext && !isRootRecord) {
|
|
673
|
-
// --- Cross-DWN non-root record in multi-party context
|
|
674
|
-
// Read an existing ProtocolContext-encrypted record from the target's DWN
|
|
675
|
-
// and extract the derivedPublicKey (the context public key).
|
|
656
|
+
// --- Cross-DWN non-root record in multi-party context -> derivedPublicKey ---
|
|
676
657
|
const rootContextId = messageParams.parentContextId!.split('/')[0]
|
|
677
658
|
|| messageParams.parentContextId!;
|
|
678
659
|
|
|
@@ -681,7 +662,7 @@ export class AgentDwnApi {
|
|
|
681
662
|
);
|
|
682
663
|
|
|
683
664
|
if (derivedPublicKeyInfo) {
|
|
684
|
-
encryptionInput =
|
|
665
|
+
encryptionInput = buildEncryptionInputFn(
|
|
685
666
|
dataEncryptionKey, dataEncryptionIV,
|
|
686
667
|
derivedPublicKeyInfo.rootKeyId, derivedPublicKeyInfo.derivedPublicKey,
|
|
687
668
|
KeyDerivationScheme.ProtocolContext,
|
|
@@ -693,17 +674,17 @@ export class AgentDwnApi {
|
|
|
693
674
|
}
|
|
694
675
|
|
|
695
676
|
} else if (isCrossDwn) {
|
|
696
|
-
// --- Cross-DWN single-party
|
|
677
|
+
// --- Cross-DWN single-party -> Target's ProtocolPath key ---
|
|
697
678
|
encryptionInput = buildProtocolPathInput();
|
|
698
679
|
|
|
699
680
|
} else if (isMultiPartyContext && !isRootRecord) {
|
|
700
|
-
// --- Local non-root record in a multi-party context
|
|
681
|
+
// --- Local non-root record in a multi-party context -> Context key ---
|
|
701
682
|
const rootContextId = messageParams.parentContextId!.split('/')[0]
|
|
702
683
|
|| messageParams.parentContextId!;
|
|
703
684
|
|
|
704
685
|
let contextKeyInfo = this._contextKeyCache.get(rootContextId);
|
|
705
686
|
if (!contextKeyInfo) {
|
|
706
|
-
const { keyId, keyUri } = await this.
|
|
687
|
+
const { keyId, keyUri } = await getEncryptionKeyInfoFn(this.agent, request.author);
|
|
707
688
|
const contextDerivationPath =
|
|
708
689
|
Records.constructKeyDerivationPathUsingProtocolContextScheme(rootContextId);
|
|
709
690
|
contextKeyInfo = { keyId, keyUri, contextDerivationPath };
|
|
@@ -715,26 +696,26 @@ export class AgentDwnApi {
|
|
|
715
696
|
derivationPath : contextKeyInfo.contextDerivationPath,
|
|
716
697
|
});
|
|
717
698
|
|
|
718
|
-
encryptionInput =
|
|
699
|
+
encryptionInput = buildEncryptionInputFn(
|
|
719
700
|
dataEncryptionKey, dataEncryptionIV,
|
|
720
701
|
contextKeyInfo.keyId, contextPublicKey,
|
|
721
702
|
KeyDerivationScheme.ProtocolContext,
|
|
722
703
|
);
|
|
723
704
|
|
|
724
705
|
} else if (isMultiPartyContext && isRootRecord) {
|
|
725
|
-
// --- Local root record in multi-party context
|
|
706
|
+
// --- Local root record in multi-party context -> Deferred context encryption ---
|
|
726
707
|
// contextId = recordId, which is only known after message creation.
|
|
727
708
|
// Skip encryptionInput here; apply it after create() below.
|
|
728
709
|
encryptionInput = undefined;
|
|
729
710
|
|
|
730
711
|
} else {
|
|
731
|
-
// --- Local single-party
|
|
712
|
+
// --- Local single-party -> ProtocolPath key (existing logic) ---
|
|
732
713
|
encryptionInput = buildProtocolPathInput();
|
|
733
714
|
}
|
|
734
715
|
|
|
735
716
|
// 7. Encrypt data with AEAD and compute CID
|
|
736
717
|
const { encryptedBytes, dataCid, dataSize, authenticationTag } =
|
|
737
|
-
await
|
|
718
|
+
await encryptAndComputeCidFn(plaintextBytes, dataEncryptionKey, dataEncryptionIV, contentEncryptionAlgorithm);
|
|
738
719
|
|
|
739
720
|
// 8. Replace plaintext with encrypted data
|
|
740
721
|
messageParams.dataCid = dataCid;
|
|
@@ -756,7 +737,7 @@ export class AgentDwnApi {
|
|
|
756
737
|
// context keys back to the author without querying the author's DWN.
|
|
757
738
|
if (isCrossDwn && isMultiPartyContext) {
|
|
758
739
|
const { keyId: authorKeyId, keyUri: authorKeyUri } =
|
|
759
|
-
await this.
|
|
740
|
+
await getEncryptionKeyInfoFn(this.agent, request.author);
|
|
760
741
|
const keyDeliveryDerivationPath = [
|
|
761
742
|
KeyDerivationScheme.ProtocolPath,
|
|
762
743
|
KeyDeliveryProtocolDefinition.protocol,
|
|
@@ -794,14 +775,14 @@ export class AgentDwnApi {
|
|
|
794
775
|
|
|
795
776
|
// Deferred context encryption for root multi-party records (Component 9).
|
|
796
777
|
// Now that the message exists, we know recordId = contextId.
|
|
797
|
-
// Following the SDK two-pass pattern: encryptSymmetricEncryptionKey
|
|
778
|
+
// Following the SDK two-pass pattern: encryptSymmetricEncryptionKey -> sign.
|
|
798
779
|
if (deferredContextEncryption && isDwnRequest(request, DwnInterface.RecordsWrite)) {
|
|
799
780
|
const recordsWriteInstance = dwnMessage as unknown as RecordsWrite;
|
|
800
781
|
const contextId = recordsWriteInstance.message.recordId;
|
|
801
782
|
|
|
802
783
|
const { encryptionInput: contextEncryptionInput, keyId, keyUri, contextDerivationPath } =
|
|
803
|
-
await
|
|
804
|
-
request.author, contextId,
|
|
784
|
+
await deriveContextEncryptionInputFn(
|
|
785
|
+
this.agent, request.author, contextId,
|
|
805
786
|
deferredContextEncryption.dataEncryptionKey,
|
|
806
787
|
deferredContextEncryption.dataEncryptionIV,
|
|
807
788
|
);
|
|
@@ -887,228 +868,88 @@ export class AgentDwnApi {
|
|
|
887
868
|
}
|
|
888
869
|
|
|
889
870
|
/**
|
|
890
|
-
*
|
|
891
|
-
*
|
|
892
|
-
* then resolves the corresponding KMS key URI.
|
|
871
|
+
* Constructs an EncryptionKeyDeriver callback for the SDK.
|
|
872
|
+
* Delegates to the standalone function in `dwn-encryption.ts`.
|
|
893
873
|
*
|
|
894
|
-
* @param didUri - The DID URI to
|
|
895
|
-
* @returns
|
|
896
|
-
* and publicKeyJwk. No private key material is returned.
|
|
897
|
-
* @throws If the DID has no keyAgreement verification method or it's not X25519.
|
|
898
|
-
*/
|
|
899
|
-
private async getEncryptionKeyInfo(didUri: string): Promise<{
|
|
900
|
-
keyId: string;
|
|
901
|
-
keyUri: KeyIdentifier;
|
|
902
|
-
publicKeyJwk: PublicKeyJwk;
|
|
903
|
-
}> {
|
|
904
|
-
// 1. Resolve the DID document
|
|
905
|
-
const { didDocument, didResolutionMetadata } = await this.agent.did.resolve(didUri);
|
|
906
|
-
if (!didDocument) {
|
|
907
|
-
throw new Error(
|
|
908
|
-
`AgentDwnApi: Failed to resolve DID '${didUri}': ` +
|
|
909
|
-
`${JSON.stringify(didResolutionMetadata)}`
|
|
910
|
-
);
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
// 2. Find the keyAgreement verification method
|
|
914
|
-
const keyAgreementRefs = didDocument.keyAgreement;
|
|
915
|
-
if (!keyAgreementRefs || keyAgreementRefs.length === 0) {
|
|
916
|
-
throw new Error(
|
|
917
|
-
`AgentDwnApi: DID '${didUri}' does not have a keyAgreement ` +
|
|
918
|
-
`verification method. Create the identity with an X25519 key ` +
|
|
919
|
-
`with keyAgreement purpose to use protocol encryption.`
|
|
920
|
-
);
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
// 3. Resolve the verification method (handle both inline and string refs)
|
|
924
|
-
const keyAgreementRef = keyAgreementRefs[0];
|
|
925
|
-
let verificationMethod;
|
|
926
|
-
if (typeof keyAgreementRef === 'string') {
|
|
927
|
-
const fragment = keyAgreementRef.includes('#')
|
|
928
|
-
? keyAgreementRef.split('#').pop()
|
|
929
|
-
: keyAgreementRef;
|
|
930
|
-
verificationMethod = didDocument.verificationMethod?.find(
|
|
931
|
-
vm => vm.id.endsWith(`#${fragment}`)
|
|
932
|
-
);
|
|
933
|
-
} else {
|
|
934
|
-
verificationMethod = keyAgreementRef;
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
if (!verificationMethod?.publicKeyJwk) {
|
|
938
|
-
throw new Error(
|
|
939
|
-
`AgentDwnApi: keyAgreement verification method for '${didUri}' ` +
|
|
940
|
-
`does not contain a public key in JWK format.`
|
|
941
|
-
);
|
|
942
|
-
}
|
|
943
|
-
|
|
944
|
-
// 4. Verify it's an X25519 key
|
|
945
|
-
const publicKeyJwk = verificationMethod.publicKeyJwk;
|
|
946
|
-
if (publicKeyJwk.crv !== 'X25519') {
|
|
947
|
-
throw new Error(
|
|
948
|
-
`AgentDwnApi: keyAgreement key for '${didUri}' uses curve ` +
|
|
949
|
-
`'${publicKeyJwk.crv}', but DWN encryption requires 'X25519'.`
|
|
950
|
-
);
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
// 5. Compute the KMS key URI (does NOT export the key)
|
|
954
|
-
const keyUri = await this.agent.keyManager.getKeyUri({ key: publicKeyJwk });
|
|
955
|
-
|
|
956
|
-
return {
|
|
957
|
-
keyId : verificationMethod.id,
|
|
958
|
-
keyUri,
|
|
959
|
-
publicKeyJwk : publicKeyJwk as PublicKeyJwk,
|
|
960
|
-
};
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
/**
|
|
964
|
-
* Builds a partial EncryptionInput object for a single key-encryption entry.
|
|
965
|
-
* The `authenticationTag` is NOT set here — the caller must set it after
|
|
966
|
-
* AEAD encryption produces the tag.
|
|
967
|
-
*/
|
|
968
|
-
/**
|
|
969
|
-
* Returns the correct nonce/IV byte length for the given content encryption algorithm.
|
|
970
|
-
* A256GCM uses 96-bit (12-byte) nonces; XC20P uses 192-bit (24-byte) nonces.
|
|
874
|
+
* @param didUri - The DID URI to create the key deriver for
|
|
875
|
+
* @returns An EncryptionKeyDeriver callback object
|
|
971
876
|
*/
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
private buildEncryptionInput(
|
|
977
|
-
dek: Uint8Array,
|
|
978
|
-
iv: Uint8Array,
|
|
979
|
-
publicKeyId: string,
|
|
980
|
-
publicKey: PublicKeyJwk,
|
|
981
|
-
derivationScheme: typeof KeyDerivationScheme.ProtocolPath | typeof KeyDerivationScheme.ProtocolContext,
|
|
982
|
-
): Omit<EncryptionInput, 'authenticationTag'> {
|
|
983
|
-
return {
|
|
984
|
-
initializationVector : iv,
|
|
985
|
-
key : dek,
|
|
986
|
-
keyEncryptionInputs : [{
|
|
987
|
-
publicKeyId,
|
|
988
|
-
publicKey,
|
|
989
|
-
derivationScheme,
|
|
990
|
-
}],
|
|
991
|
-
};
|
|
877
|
+
public async getEncryptionKeyDeriver(
|
|
878
|
+
didUri: string
|
|
879
|
+
): Promise<EncryptionKeyDeriver> {
|
|
880
|
+
return getEncryptionKeyDeriverFn(this.agent, didUri);
|
|
992
881
|
}
|
|
993
882
|
|
|
994
883
|
/**
|
|
995
|
-
*
|
|
996
|
-
*
|
|
997
|
-
*
|
|
884
|
+
* Resolves the keyAgreement verification method for the given DID and returns
|
|
885
|
+
* the key ID, key URI, and public key JWK.
|
|
886
|
+
*
|
|
887
|
+
* @param didUri - The DID URI to look up
|
|
998
888
|
*/
|
|
999
|
-
private async
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
algorithm: ContentEncryptionAlgorithm = ContentEncryptionAlgorithm.A256GCM,
|
|
1004
|
-
): Promise<{ encryptedBytes: Uint8Array; dataCid: string; dataSize: number; authenticationTag: Uint8Array }> {
|
|
1005
|
-
const { ciphertextStream, tag: authenticationTag } = await Encryption.aeadEncryptStream(
|
|
1006
|
-
algorithm, dek, iv, DataStream.fromBytes(plaintextBytes),
|
|
1007
|
-
);
|
|
1008
|
-
const encryptedBytes = await DataStream.toBytes(ciphertextStream);
|
|
1009
|
-
const cidStream = DataStream.fromBytes(encryptedBytes);
|
|
1010
|
-
const dataCid = await Cid.computeDagPbCidFromStream(cidStream);
|
|
1011
|
-
return { encryptedBytes, dataCid, dataSize: encryptedBytes.length, authenticationTag };
|
|
889
|
+
private async getEncryptionKeyInfo(
|
|
890
|
+
didUri: string
|
|
891
|
+
): Promise<{ keyId: string; keyUri: KeyIdentifier; publicKeyJwk: PublicKeyJwk }> {
|
|
892
|
+
return getEncryptionKeyInfoFn(this.agent, didUri);
|
|
1012
893
|
}
|
|
1013
894
|
|
|
1014
895
|
/**
|
|
1015
|
-
*
|
|
1016
|
-
*
|
|
1017
|
-
*
|
|
1018
|
-
* → build EncryptionInput sequence.
|
|
896
|
+
* Constructs a ProtocolPath KeyDecrypter for the given DID.
|
|
897
|
+
*
|
|
898
|
+
* @param didUri - The DID URI to build a decrypter for
|
|
1019
899
|
*/
|
|
1020
|
-
private async
|
|
1021
|
-
didUri: string
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
iv: Uint8Array,
|
|
1025
|
-
): Promise<{ encryptionInput: Omit<EncryptionInput, 'authenticationTag'>; keyId: string; keyUri: KeyIdentifier; contextDerivationPath: string[] }> {
|
|
1026
|
-
const { keyId, keyUri } = await this.getEncryptionKeyInfo(didUri);
|
|
1027
|
-
const contextDerivationPath =
|
|
1028
|
-
Records.constructKeyDerivationPathUsingProtocolContextScheme(contextId);
|
|
1029
|
-
const contextPublicKey = await this.agent.keyManager.derivePublicKey({
|
|
1030
|
-
keyUri,
|
|
1031
|
-
derivationPath: contextDerivationPath,
|
|
1032
|
-
});
|
|
1033
|
-
|
|
1034
|
-
const encryptionInput = this.buildEncryptionInput(
|
|
1035
|
-
dek, iv, keyId, contextPublicKey, KeyDerivationScheme.ProtocolContext,
|
|
1036
|
-
);
|
|
1037
|
-
|
|
1038
|
-
return { encryptionInput, keyId, keyUri, contextDerivationPath };
|
|
900
|
+
private async getKeyDecrypter(
|
|
901
|
+
didUri: string
|
|
902
|
+
): Promise<KeyDecrypter> {
|
|
903
|
+
return getKeyDecrypterFn(this.agent, didUri);
|
|
1039
904
|
}
|
|
1040
905
|
|
|
1041
906
|
/**
|
|
1042
|
-
*
|
|
1043
|
-
*
|
|
907
|
+
* Checks if a protocol path represents a multi-party context.
|
|
908
|
+
*
|
|
909
|
+
* @param protocolDefinition - The full protocol definition
|
|
910
|
+
* @param rootProtocolPath - The root protocol path to check
|
|
1044
911
|
*/
|
|
1045
|
-
private
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
const keyManager = this.agent.keyManager;
|
|
1051
|
-
return {
|
|
1052
|
-
rootKeyId : keyId,
|
|
1053
|
-
derivationScheme,
|
|
1054
|
-
decrypt : async (fullDerivationPath, jwePayload): Promise<Uint8Array> => {
|
|
1055
|
-
return keyManager.jweKeyUnwrap({
|
|
1056
|
-
keyUri,
|
|
1057
|
-
derivationPath : fullDerivationPath,
|
|
1058
|
-
encryptedKey : jwePayload.encryptedKey,
|
|
1059
|
-
ephemeralPublicKey : jwePayload.ephemeralPublicKey,
|
|
1060
|
-
});
|
|
1061
|
-
},
|
|
1062
|
-
};
|
|
912
|
+
private isMultiPartyContext(
|
|
913
|
+
protocolDefinition: ProtocolDefinition,
|
|
914
|
+
rootProtocolPath: string,
|
|
915
|
+
): boolean {
|
|
916
|
+
return isMultiPartyContextFn(protocolDefinition, rootProtocolPath);
|
|
1063
917
|
}
|
|
1064
918
|
|
|
1065
919
|
/**
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1068
|
-
* computation internally. The private key never leaves the KMS.
|
|
920
|
+
* Checks if any `$actions` rule in the protocol grants read access
|
|
921
|
+
* via `who: '<actorType>'` and `of: '<path>'`.
|
|
1069
922
|
*
|
|
1070
|
-
*
|
|
1071
|
-
*
|
|
1072
|
-
* @param
|
|
1073
|
-
* @returns An EncryptionKeyDeriver callback object
|
|
923
|
+
* @param actorType - The actor type to check ('author', 'recipient', or undefined for any)
|
|
924
|
+
* @param ofPath - The protocol path to check
|
|
925
|
+
* @param protocolDefinition - The protocol definition
|
|
1074
926
|
*/
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
return {
|
|
1082
|
-
rootKeyId : keyId,
|
|
1083
|
-
derivationScheme : KeyDerivationScheme.ProtocolPath,
|
|
1084
|
-
derivePublicKey : async (fullDerivationPath: string[]): Promise<PublicKeyJwk> => {
|
|
1085
|
-
return keyManager.derivePublicKey({
|
|
1086
|
-
keyUri,
|
|
1087
|
-
derivationPath: fullDerivationPath,
|
|
1088
|
-
});
|
|
1089
|
-
},
|
|
1090
|
-
};
|
|
927
|
+
private hasRelationalReadAccess(
|
|
928
|
+
actorType: 'author' | 'recipient' | undefined,
|
|
929
|
+
ofPath: string,
|
|
930
|
+
protocolDefinition: ProtocolDefinition,
|
|
931
|
+
): boolean {
|
|
932
|
+
return hasRelationalReadAccessFn(actorType, ofPath, protocolDefinition);
|
|
1091
933
|
}
|
|
1092
934
|
|
|
1093
935
|
/**
|
|
1094
|
-
*
|
|
1095
|
-
* The SDK calls decrypt(path, eciesParams), the KMS performs HKDF + ECIES
|
|
1096
|
-
* decryption internally. The private key never leaves the KMS.
|
|
1097
|
-
*
|
|
1098
|
-
* Analogous to getSigner() for signing operations.
|
|
936
|
+
* Analyses a record write to determine which DIDs need context key delivery.
|
|
1099
937
|
*
|
|
1100
|
-
* @param
|
|
1101
|
-
* @returns
|
|
938
|
+
* @param params - Parameters for participant detection
|
|
939
|
+
* @returns Set of DIDs that need context key delivery
|
|
1102
940
|
*/
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
941
|
+
public detectNewParticipants(params: {
|
|
942
|
+
protocolDefinition: ProtocolDefinition;
|
|
943
|
+
protocolPath: string;
|
|
944
|
+
recipient?: string;
|
|
945
|
+
tenantDid: string;
|
|
946
|
+
authorDid?: string;
|
|
947
|
+
}): Set<string> {
|
|
948
|
+
return detectNewParticipantsFn(params);
|
|
1108
949
|
}
|
|
1109
950
|
|
|
1110
951
|
/**
|
|
1111
|
-
|
|
952
|
+
* Fetches a protocol definition from the local DWN, with caching.
|
|
1112
953
|
* Returns undefined if the protocol is not installed.
|
|
1113
954
|
*
|
|
1114
955
|
* @param tenantDid - The tenant DID to query
|
|
@@ -1146,175 +987,6 @@ export class AgentDwnApi {
|
|
|
1146
987
|
return definition;
|
|
1147
988
|
}
|
|
1148
989
|
|
|
1149
|
-
/**
|
|
1150
|
-
* Checks if a protocol path represents a multi-party context. Returns true
|
|
1151
|
-
* if the root path's subtree contains:
|
|
1152
|
-
* (a) any `$role: true` descendants, OR
|
|
1153
|
-
* (b) any relational `who`/`of` `$actions` rules that grant `read` access
|
|
1154
|
-
* (indicating external authors or recipients need context keys).
|
|
1155
|
-
*
|
|
1156
|
-
* This generalises the earlier `protocolPathHasRoles()` to cover protocols
|
|
1157
|
-
* that use relational access without explicit role definitions.
|
|
1158
|
-
*/
|
|
1159
|
-
private isMultiPartyContext(
|
|
1160
|
-
protocolDefinition: ProtocolDefinition,
|
|
1161
|
-
rootProtocolPath: string,
|
|
1162
|
-
): boolean {
|
|
1163
|
-
const segments = rootProtocolPath.split('/');
|
|
1164
|
-
let ruleSet: ProtocolRuleSet | undefined =
|
|
1165
|
-
protocolDefinition.structure as unknown as ProtocolRuleSet;
|
|
1166
|
-
for (const segment of segments) {
|
|
1167
|
-
ruleSet = ruleSet[segment] as ProtocolRuleSet | undefined;
|
|
1168
|
-
if (!ruleSet) { return false; }
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
// (a) Check for $role descendants in the subtree
|
|
1172
|
-
function hasRoleRecursive(rs: ProtocolRuleSet): boolean {
|
|
1173
|
-
for (const key in rs) {
|
|
1174
|
-
if (!key.startsWith('$')) {
|
|
1175
|
-
const child = rs[key] as ProtocolRuleSet;
|
|
1176
|
-
if (child.$role === true) { return true; }
|
|
1177
|
-
if (hasRoleRecursive(child)) { return true; }
|
|
1178
|
-
}
|
|
1179
|
-
}
|
|
1180
|
-
return false;
|
|
1181
|
-
}
|
|
1182
|
-
|
|
1183
|
-
if (hasRoleRecursive(ruleSet)) {
|
|
1184
|
-
return true;
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
// (b) Check for relational who/of read rules anywhere in the protocol
|
|
1188
|
-
// that reference a path within this subtree. A rule like
|
|
1189
|
-
// { who: 'recipient', of: 'email', can: ['read'] } on any record
|
|
1190
|
-
// type means the email recipient needs a context key.
|
|
1191
|
-
return this.hasRelationalReadAccess(
|
|
1192
|
-
undefined, rootProtocolPath, protocolDefinition,
|
|
1193
|
-
);
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
/**
|
|
1197
|
-
* Checks whether any relational `who`/`of` rule in the protocol grants
|
|
1198
|
-
* `read` access for a given actor type and ancestor path.
|
|
1199
|
-
*
|
|
1200
|
-
* Walks the *entire* protocol structure looking for any `$actions` rule that:
|
|
1201
|
-
* - Has `who` equal to `actorType` ('recipient' or 'author'), or any actor
|
|
1202
|
-
* type if `actorType` is `undefined`
|
|
1203
|
-
* - Has `of` equal to `ofPath`
|
|
1204
|
-
* - Has `can` including 'read'
|
|
1205
|
-
*
|
|
1206
|
-
* The search covers all record types in the protocol, since a relational
|
|
1207
|
-
* rule can appear at any level (e.g. `{ who: 'recipient', of: 'thread',
|
|
1208
|
-
* can: ['read'] }` might be defined on `thread/message`).
|
|
1209
|
-
*
|
|
1210
|
-
* @param actorType - 'author' | 'recipient', or undefined for any
|
|
1211
|
-
* @param ofPath - The protocol path to check (e.g. 'thread', 'email')
|
|
1212
|
-
* @param protocolDefinition - The full protocol definition
|
|
1213
|
-
* @returns true if a matching relational read rule exists
|
|
1214
|
-
*/
|
|
1215
|
-
private hasRelationalReadAccess(
|
|
1216
|
-
actorType: 'author' | 'recipient' | undefined,
|
|
1217
|
-
ofPath: string,
|
|
1218
|
-
protocolDefinition: ProtocolDefinition,
|
|
1219
|
-
): boolean {
|
|
1220
|
-
const structure = protocolDefinition.structure as unknown as ProtocolRuleSet;
|
|
1221
|
-
|
|
1222
|
-
function walkRuleSet(rs: ProtocolRuleSet): boolean {
|
|
1223
|
-
// Check $actions on this node
|
|
1224
|
-
if (rs.$actions) {
|
|
1225
|
-
for (const rule of rs.$actions) {
|
|
1226
|
-
if (
|
|
1227
|
-
rule.who &&
|
|
1228
|
-
rule.who !== 'anyone' &&
|
|
1229
|
-
(actorType === undefined || rule.who === actorType) &&
|
|
1230
|
-
rule.of === ofPath &&
|
|
1231
|
-
rule.can?.includes('read')
|
|
1232
|
-
) {
|
|
1233
|
-
return true;
|
|
1234
|
-
}
|
|
1235
|
-
}
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
// Recurse into child record types
|
|
1239
|
-
for (const key in rs) {
|
|
1240
|
-
if (!key.startsWith('$')) {
|
|
1241
|
-
if (walkRuleSet(rs[key] as ProtocolRuleSet)) {
|
|
1242
|
-
return true;
|
|
1243
|
-
}
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
return false;
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
return walkRuleSet(structure);
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
/**
|
|
1253
|
-
* Analyses a record write to determine which DIDs need context key delivery.
|
|
1254
|
-
*
|
|
1255
|
-
* Returns a set of participant DIDs that should receive `contextKey` records.
|
|
1256
|
-
* The DWN owner (tenantDid) is always excluded — they have ProtocolPath access.
|
|
1257
|
-
*
|
|
1258
|
-
* Cases handled:
|
|
1259
|
-
* 1. `$role` record with a recipient → recipient is a participant
|
|
1260
|
-
* 2. Record has a recipient and a relational read rule grants access
|
|
1261
|
-
* via `{ who: 'recipient', of: '<path>', can: ['read'] }`
|
|
1262
|
-
* 3. Record is authored by an external party → if `{ who: 'author', of:
|
|
1263
|
-
* '<path>', can: ['read'] }` rules grant read access, the author needs
|
|
1264
|
-
* a context key.
|
|
1265
|
-
*
|
|
1266
|
-
* @param params.protocolDefinition - The installed protocol definition
|
|
1267
|
-
* @param params.protocolPath - The written record's protocol path
|
|
1268
|
-
* @param params.recipient - Recipient DID from the record, if any
|
|
1269
|
-
* @param params.tenantDid - The DWN owner's DID (excluded from results)
|
|
1270
|
-
* @param params.authorDid - Author DID if externally authored, undefined otherwise
|
|
1271
|
-
* @returns Set of DIDs that need context key delivery
|
|
1272
|
-
*/
|
|
1273
|
-
detectNewParticipants({ protocolDefinition, protocolPath, recipient, tenantDid, authorDid }: {
|
|
1274
|
-
protocolDefinition: ProtocolDefinition;
|
|
1275
|
-
protocolPath: string;
|
|
1276
|
-
recipient?: string;
|
|
1277
|
-
tenantDid: string;
|
|
1278
|
-
authorDid?: string;
|
|
1279
|
-
}): Set<string> {
|
|
1280
|
-
const participants = new Set<string>();
|
|
1281
|
-
|
|
1282
|
-
// Navigate to the rule set at the given protocol path
|
|
1283
|
-
const pathSegments = protocolPath.split('/');
|
|
1284
|
-
let ruleSet: ProtocolRuleSet | undefined =
|
|
1285
|
-
protocolDefinition.structure as unknown as ProtocolRuleSet;
|
|
1286
|
-
for (const segment of pathSegments) {
|
|
1287
|
-
ruleSet = ruleSet[segment] as ProtocolRuleSet | undefined;
|
|
1288
|
-
if (!ruleSet) { return participants; }
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
// Case 1: $role record → recipient is a participant
|
|
1292
|
-
if (ruleSet.$role === true && recipient) {
|
|
1293
|
-
participants.add(recipient);
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
// Case 2: Record has a recipient → check if relational read rules exist
|
|
1297
|
-
if (recipient && recipient !== tenantDid) {
|
|
1298
|
-
if (this.hasRelationalReadAccess('recipient', protocolPath, protocolDefinition)) {
|
|
1299
|
-
participants.add(recipient);
|
|
1300
|
-
}
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
// Case 3: External author → check if author-based relational read rules exist.
|
|
1304
|
-
// If `{ who: 'author', of: '<path>', can: ['read'] }` is defined anywhere
|
|
1305
|
-
// in the protocol, the external author needs a context key to decrypt.
|
|
1306
|
-
if (authorDid && authorDid !== tenantDid) {
|
|
1307
|
-
if (this.hasRelationalReadAccess('author', protocolPath, protocolDefinition)) {
|
|
1308
|
-
participants.add(authorDid);
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
|
|
1312
|
-
// Remove the DWN owner — they always have ProtocolPath access
|
|
1313
|
-
participants.delete(tenantDid);
|
|
1314
|
-
|
|
1315
|
-
return participants;
|
|
1316
|
-
}
|
|
1317
|
-
|
|
1318
990
|
/**
|
|
1319
991
|
* Fetches a protocol definition from a remote DWN.
|
|
1320
992
|
* Uses an unsigned ProtocolsQuery (public protocols can be queried anonymously).
|
|
@@ -1413,333 +1085,21 @@ export class AgentDwnApi {
|
|
|
1413
1085
|
return undefined;
|
|
1414
1086
|
}
|
|
1415
1087
|
|
|
1416
|
-
/**
|
|
1417
|
-
* Reactively upgrades an externally-authored root record that has only
|
|
1418
|
-
* ProtocolPath encryption by appending a ProtocolContext recipient entry.
|
|
1419
|
-
*
|
|
1420
|
-
* After the upgrade, both the owner (ProtocolPath) and context key holders —
|
|
1421
|
-
* including the external author (ProtocolContext) — can decrypt the record.
|
|
1422
|
-
*
|
|
1423
|
-
* Steps:
|
|
1424
|
-
* 1. Decrypt the DEK using the owner's ProtocolPath-derived private key
|
|
1425
|
-
* 2. Derive the context public key from the owner's #enc key
|
|
1426
|
-
* 3. ECIES-encrypt the same DEK to the context public key
|
|
1427
|
-
* 4. Append the ProtocolContext recipient entry (using PR 0b append mode)
|
|
1428
|
-
* 5. Re-sign the record as owner
|
|
1429
|
-
*
|
|
1430
|
-
* The author's signature payload includes an `encryptionCid` that becomes
|
|
1431
|
-
* stale after step 4. The SDK's `validateIntegrity()` skips the encryptionCid
|
|
1432
|
-
* check on the author's signature when an ownerSignature is present (step 5),
|
|
1433
|
-
* since the owner vouches for the updated encryption property.
|
|
1434
|
-
*
|
|
1435
|
-
* NOTE: An alternative design would deliver the DEK out-of-band via the
|
|
1436
|
-
* key-delivery protocol (as a field on the contextKey record) instead of
|
|
1437
|
-
* mutating the record's encryption property. That avoids the stale
|
|
1438
|
-
* encryptionCid concern entirely but adds complexity to the read path and
|
|
1439
|
-
* the contextKey schema. We chose the in-record approach because it keeps
|
|
1440
|
-
* records self-contained and the read/decrypt path unchanged.
|
|
1441
|
-
*
|
|
1442
|
-
* @param tenantDid - The DWN owner's DID
|
|
1443
|
-
* @param recordsWrite - The RecordsWrite message to upgrade
|
|
1444
|
-
*/
|
|
1445
|
-
private async upgradeExternalRootRecord(
|
|
1446
|
-
tenantDid: string,
|
|
1447
|
-
recordsWrite: RecordsWriteMessage,
|
|
1448
|
-
): Promise<void> {
|
|
1449
|
-
const { encryption } = recordsWrite;
|
|
1450
|
-
if (!encryption) { return; }
|
|
1451
|
-
|
|
1452
|
-
// Verify: has ProtocolPath but NOT ProtocolContext
|
|
1453
|
-
const hasProtocolPath = encryption.recipients.some(
|
|
1454
|
-
(r: { header: { derivationScheme: string } }) => r.header.derivationScheme === KeyDerivationScheme.ProtocolPath
|
|
1455
|
-
);
|
|
1456
|
-
const hasProtocolContext = encryption.recipients.some(
|
|
1457
|
-
(r: { header: { derivationScheme: string } }) => r.header.derivationScheme === KeyDerivationScheme.ProtocolContext
|
|
1458
|
-
);
|
|
1459
|
-
if (!hasProtocolPath || hasProtocolContext) { return; }
|
|
1460
|
-
|
|
1461
|
-
// 1. Decrypt the DEK using the owner's ProtocolPath key
|
|
1462
|
-
const keyDecrypter = await this.getKeyDecrypter(tenantDid);
|
|
1463
|
-
|
|
1464
|
-
// Find the ProtocolPath recipient entry
|
|
1465
|
-
const pathRecipient = encryption.recipients.find(
|
|
1466
|
-
(r: { header: { derivationScheme: string } }) => r.header.derivationScheme === KeyDerivationScheme.ProtocolPath
|
|
1467
|
-
)!;
|
|
1468
|
-
|
|
1469
|
-
const fullDerivationPath = Records.constructKeyDerivationPathUsingProtocolPathScheme(
|
|
1470
|
-
recordsWrite.descriptor,
|
|
1471
|
-
);
|
|
1472
|
-
|
|
1473
|
-
const dataEncryptionKey = await keyDecrypter.decrypt(
|
|
1474
|
-
fullDerivationPath,
|
|
1475
|
-
{
|
|
1476
|
-
encryptedKey : Encoder.base64UrlToBytes(pathRecipient.encrypted_key),
|
|
1477
|
-
ephemeralPublicKey : pathRecipient.header.epk,
|
|
1478
|
-
},
|
|
1479
|
-
);
|
|
1480
|
-
|
|
1481
|
-
// 2. Derive the context public key — contextId = recordId for root records
|
|
1482
|
-
const contextId = recordsWrite.recordId;
|
|
1483
|
-
const encryptionIV = Encoder.base64UrlToBytes(encryption.iv);
|
|
1484
|
-
|
|
1485
|
-
// 3 & 4. Append the ProtocolContext recipient entry using append mode.
|
|
1486
|
-
// Append mode preserves the author's identity and authorization so that
|
|
1487
|
-
// signAsOwner() can be called in step 5.
|
|
1488
|
-
const { encryptionInput: contextEncryptionInput, keyId, keyUri, contextDerivationPath } =
|
|
1489
|
-
await this.deriveContextEncryptionInput(tenantDid, contextId, dataEncryptionKey, encryptionIV);
|
|
1490
|
-
|
|
1491
|
-
// Set the authentication tag from the existing JWE encryption property
|
|
1492
|
-
const fullContextInput = { ...contextEncryptionInput, authenticationTag: Encoder.base64UrlToBytes(encryption.tag) };
|
|
1493
|
-
|
|
1494
|
-
// Parse the message to get a RecordsWrite instance we can mutate
|
|
1495
|
-
const recordsWriteInstance = await dwnMessageConstructors[DwnInterface.RecordsWrite].parse(
|
|
1496
|
-
recordsWrite,
|
|
1497
|
-
) as unknown as RecordsWrite;
|
|
1498
|
-
|
|
1499
|
-
await recordsWriteInstance.encryptSymmetricEncryptionKey(
|
|
1500
|
-
fullContextInput as EncryptionInput,
|
|
1501
|
-
{ append: true },
|
|
1502
|
-
);
|
|
1503
|
-
|
|
1504
|
-
// 5. Re-sign as owner — the author's signature is preserved but its
|
|
1505
|
-
// encryptionCid is now stale; the owner's signature vouches for the
|
|
1506
|
-
// updated encryption property.
|
|
1507
|
-
const signer = await this.getSigner(tenantDid);
|
|
1508
|
-
await recordsWriteInstance.signAsOwner(signer);
|
|
1509
|
-
|
|
1510
|
-
// Store the upgraded message directly via the message store, bypassing
|
|
1511
|
-
// the handler's conflict resolution which doesn't support same-timestamp
|
|
1512
|
-
// owner-augmented replacements. The data is unchanged — only the encryption
|
|
1513
|
-
// metadata and authorization are updated.
|
|
1514
|
-
//
|
|
1515
|
-
// We must also update the state index and event stream to keep sync and
|
|
1516
|
-
// real-time subscribers consistent — without this, the upgraded record
|
|
1517
|
-
// would never propagate to remote DWNs or notify subscribers.
|
|
1518
|
-
const { messageStore, stateIndex, eventStream } = this._dwn.storage;
|
|
1519
|
-
|
|
1520
|
-
// Validate the upgrade only changed encryption and authorization fields.
|
|
1521
|
-
// The descriptor, recordId, contextId, and data must remain identical.
|
|
1522
|
-
// Note: parse() may produce a new descriptor object, so we compare by value.
|
|
1523
|
-
const upgradedMessage = recordsWriteInstance.message as RecordsQueryReplyEntry;
|
|
1524
|
-
if (JSON.stringify(upgradedMessage.descriptor) !== JSON.stringify(recordsWrite.descriptor)) {
|
|
1525
|
-
throw new Error('AgentDwnApi: upgradeExternalRootRecord() must not modify the descriptor.');
|
|
1526
|
-
}
|
|
1527
|
-
if (upgradedMessage.recordId !== recordsWrite.recordId) {
|
|
1528
|
-
throw new Error('AgentDwnApi: upgradeExternalRootRecord() must not modify the recordId.');
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
// Fetch the stored original (which carries encodedData for small payloads)
|
|
1532
|
-
const originalCid = await Message.getCid(recordsWrite);
|
|
1533
|
-
const storedOriginal = await messageStore.get(tenantDid, originalCid) as RecordsQueryReplyEntry | undefined;
|
|
1534
|
-
|
|
1535
|
-
// Build indexes for the upgraded message (mark as latest base state)
|
|
1536
|
-
const isLatestBaseState = true;
|
|
1537
|
-
const upgradedIndexes = await recordsWriteInstance.constructIndexes(isLatestBaseState);
|
|
1538
|
-
|
|
1539
|
-
// Carry over the encoded data from the stored original (the handler
|
|
1540
|
-
// base64url-encodes small payloads into encodedData during processMessage)
|
|
1541
|
-
if (storedOriginal?.encodedData) {
|
|
1542
|
-
upgradedMessage.encodedData = storedOriginal.encodedData;
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
// Use put-before-delete ordering: if a crash occurs after the put but
|
|
1546
|
-
// before the delete, we end up with a duplicate (recoverable via the
|
|
1547
|
-
// isLatestBaseState index) rather than data loss (unrecoverable).
|
|
1548
|
-
const upgradedCid = await Message.getCid(upgradedMessage);
|
|
1549
|
-
await messageStore.put(tenantDid, upgradedMessage, upgradedIndexes);
|
|
1550
|
-
await stateIndex.insert(tenantDid, upgradedCid, upgradedIndexes);
|
|
1551
|
-
|
|
1552
|
-
// Now remove the original message and its state index entry.
|
|
1553
|
-
await messageStore.delete(tenantDid, originalCid);
|
|
1554
|
-
await stateIndex.delete(tenantDid, [originalCid]);
|
|
1555
|
-
|
|
1556
|
-
// Notify real-time subscribers (mirrors handler behavior)
|
|
1557
|
-
if (eventStream !== undefined) {
|
|
1558
|
-
eventStream.emit(tenantDid, { message: upgradedMessage }, upgradedIndexes);
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
// Cache context key info for subsequent writes in this context
|
|
1562
|
-
this._contextKeyCache.set(contextId, { keyId, keyUri, contextDerivationPath });
|
|
1563
|
-
}
|
|
1564
|
-
|
|
1565
|
-
/**
|
|
1566
|
-
* Resolves the appropriate KeyDecrypter for a record's encryption scheme.
|
|
1567
|
-
* Handles both single-party (ProtocolPath) and multi-party (ProtocolContext).
|
|
1568
|
-
*
|
|
1569
|
-
* For ProtocolContext records:
|
|
1570
|
-
* - Context creator: derives key directly from KMS
|
|
1571
|
-
* - Participant: fetches contextKey via key-delivery protocol, caches it
|
|
1572
|
-
*/
|
|
1573
|
-
private async resolveKeyDecrypter(
|
|
1574
|
-
authorDid: string,
|
|
1575
|
-
recordsWrite: RecordsWriteMessage,
|
|
1576
|
-
targetDid?: string,
|
|
1577
|
-
): Promise<KeyDecrypter> {
|
|
1578
|
-
const { encryption } = recordsWrite;
|
|
1579
|
-
|
|
1580
|
-
// Check if the record uses context-derived encryption
|
|
1581
|
-
const hasContextKey = encryption?.recipients.some(
|
|
1582
|
-
(r: { header: { derivationScheme: string } }) => r.header.derivationScheme === KeyDerivationScheme.ProtocolContext
|
|
1583
|
-
);
|
|
1584
|
-
|
|
1585
|
-
if (!hasContextKey || !recordsWrite.contextId) {
|
|
1586
|
-
// Single-party protocol-path encryption
|
|
1587
|
-
return this.getKeyDecrypter(authorDid);
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
// --- Multi-party context encryption ---
|
|
1591
|
-
const contextKeyEntry = encryption!.recipients.find(
|
|
1592
|
-
(r: { header: { derivationScheme: string } }) => r.header.derivationScheme === KeyDerivationScheme.ProtocolContext
|
|
1593
|
-
)!;
|
|
1594
|
-
|
|
1595
|
-
const rootContextId = recordsWrite.contextId.split('/')[0];
|
|
1596
|
-
|
|
1597
|
-
// Case 1: I am the context creator — rootKeyId matches my encryption key
|
|
1598
|
-
const { keyId, keyUri } = await this.getEncryptionKeyInfo(authorDid);
|
|
1599
|
-
if (contextKeyEntry.header.kid === keyId) {
|
|
1600
|
-
return this.buildKmsDecryptCallback(keyId, keyUri, KeyDerivationScheme.ProtocolContext);
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
// Case 2: I am a participant — fetch my context key from the key-delivery protocol
|
|
1604
|
-
const cacheKey = `ctx~${authorDid}~${rootContextId}`;
|
|
1605
|
-
const cached = this._contextDerivedKeyCache.get(cacheKey);
|
|
1606
|
-
if (cached) {
|
|
1607
|
-
return this.buildContextKeyDecrypter(cached);
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
// Fetch context key via the key-delivery protocol — local first, then remote
|
|
1611
|
-
const protocol = recordsWrite.descriptor.protocol!;
|
|
1612
|
-
|
|
1613
|
-
// Try local: I may be the DWN owner with a contextKey addressed to myself
|
|
1614
|
-
let contextDerivedPrivateKey = await this.fetchContextKeyRecord({
|
|
1615
|
-
ownerDid : authorDid,
|
|
1616
|
-
requesterDid : authorDid,
|
|
1617
|
-
sourceProtocol : protocol,
|
|
1618
|
-
sourceContextId : rootContextId,
|
|
1619
|
-
});
|
|
1620
|
-
|
|
1621
|
-
// Try remote: query the DWN owner's DWN for my contextKey record.
|
|
1622
|
-
// For cross-DWN records, targetDid is the DWN owner (e.g., Alice) where the
|
|
1623
|
-
// contextKey was written. For same-DWN records, fall back to the record's
|
|
1624
|
-
// authorization signer.
|
|
1625
|
-
if (!contextDerivedPrivateKey) {
|
|
1626
|
-
const contextOwnerDid = targetDid ?? Jws.getSignerDid(
|
|
1627
|
-
recordsWrite.authorization.signature.signatures[0]
|
|
1628
|
-
);
|
|
1629
|
-
contextDerivedPrivateKey = await this.fetchContextKeyRecord({
|
|
1630
|
-
ownerDid : contextOwnerDid,
|
|
1631
|
-
requesterDid : authorDid,
|
|
1632
|
-
sourceProtocol : protocol,
|
|
1633
|
-
sourceContextId : rootContextId,
|
|
1634
|
-
});
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
if (!contextDerivedPrivateKey) {
|
|
1638
|
-
throw new Error(
|
|
1639
|
-
`AgentDwnApi: Failed to decrypt record '${recordsWrite.recordId}'. ` +
|
|
1640
|
-
`Record uses context-derived encryption but no contextKey record ` +
|
|
1641
|
-
`could be found via the key-delivery protocol.`
|
|
1642
|
-
);
|
|
1643
|
-
}
|
|
1644
|
-
|
|
1645
|
-
this._contextDerivedKeyCache.set(cacheKey, contextDerivedPrivateKey);
|
|
1646
|
-
return this.buildContextKeyDecrypter(contextDerivedPrivateKey);
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
/**
|
|
1651
|
-
* Builds a KeyDecrypter from a context-derived private key.
|
|
1652
|
-
* Uses the raw key directly (since it was shared with us via the key-delivery protocol).
|
|
1653
|
-
*/
|
|
1654
|
-
private buildContextKeyDecrypter(
|
|
1655
|
-
contextKey: DerivedPrivateJwk,
|
|
1656
|
-
): KeyDecrypter {
|
|
1657
|
-
return {
|
|
1658
|
-
rootKeyId : contextKey.rootKeyId,
|
|
1659
|
-
derivationScheme : contextKey.derivationScheme,
|
|
1660
|
-
decrypt : async (fullDerivationPath, jwePayload): Promise<Uint8Array> => {
|
|
1661
|
-
const leafPrivateKeyBytes = await Records.derivePrivateKey(
|
|
1662
|
-
contextKey, fullDerivationPath,
|
|
1663
|
-
);
|
|
1664
|
-
const leafPrivateKeyJwk = await X25519.bytesToPrivateKey({ privateKeyBytes: leafPrivateKeyBytes });
|
|
1665
|
-
return Encryption.ecdhEsUnwrapKey(leafPrivateKeyJwk, jwePayload.ephemeralPublicKey, jwePayload.encryptedKey);
|
|
1666
|
-
},
|
|
1667
|
-
};
|
|
1668
|
-
}
|
|
1669
|
-
|
|
1670
1088
|
/**
|
|
1671
1089
|
* Post-processes a DWN reply, auto-decrypting data if encryption is enabled.
|
|
1672
|
-
* Delegates to the
|
|
1673
|
-
* resolveKeyDecrypter() selects between ProtocolPath and ProtocolContext schemes.
|
|
1090
|
+
* Delegates to the standalone function in `dwn-encryption.ts`.
|
|
1674
1091
|
*/
|
|
1675
1092
|
private async maybeDecryptReply<T extends DwnInterface>(
|
|
1676
1093
|
request: ProcessDwnRequest<T> | SendDwnRequest<T>,
|
|
1677
1094
|
reply: DwnMessageReply[T],
|
|
1678
1095
|
): Promise<void> {
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
if (isDwnRequest(request as ProcessDwnRequest<DwnInterface>, DwnInterface.RecordsRead)) {
|
|
1685
|
-
const readReply = reply as RecordsReadReply;
|
|
1686
|
-
if (readReply.status.code === 200
|
|
1687
|
-
&& readReply.entry?.recordsWrite?.encryption
|
|
1688
|
-
&& readReply.entry?.data) {
|
|
1689
|
-
const keyDecrypter = await this.resolveKeyDecrypter(
|
|
1690
|
-
request.author, readReply.entry.recordsWrite, request.target,
|
|
1691
|
-
);
|
|
1692
|
-
|
|
1693
|
-
try {
|
|
1694
|
-
readReply.entry.data = await Records.decrypt(
|
|
1695
|
-
readReply.entry.recordsWrite,
|
|
1696
|
-
keyDecrypter,
|
|
1697
|
-
readReply.entry.data,
|
|
1698
|
-
);
|
|
1699
|
-
} catch (error: any) {
|
|
1700
|
-
throw new Error(
|
|
1701
|
-
`AgentDwnApi: Failed to decrypt record ` +
|
|
1702
|
-
`'${readReply.entry.recordsWrite.recordId}'. ` +
|
|
1703
|
-
`Original error: ${error.message}`
|
|
1704
|
-
);
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
|
|
1709
|
-
// Auto-decrypt RecordsQuery replies (small records inline as encodedData)
|
|
1710
|
-
if (isDwnRequest(request as ProcessDwnRequest<DwnInterface>, DwnInterface.RecordsQuery)) {
|
|
1711
|
-
const queryReply = reply as RecordsQueryReply;
|
|
1712
|
-
if (queryReply.status.code === 200 && queryReply.entries) {
|
|
1713
|
-
for (const entry of queryReply.entries) {
|
|
1714
|
-
if (entry.encryption && entry.encodedData) {
|
|
1715
|
-
const keyDecrypter = await this.resolveKeyDecrypter(
|
|
1716
|
-
request.author, entry as RecordsWriteMessage, request.target,
|
|
1717
|
-
);
|
|
1718
|
-
|
|
1719
|
-
try {
|
|
1720
|
-
const cipherBytes = Encoder.base64UrlToBytes(entry.encodedData);
|
|
1721
|
-
const cipherStream = DataStream.fromBytes(cipherBytes);
|
|
1722
|
-
const plainStream = await Records.decrypt(
|
|
1723
|
-
entry as RecordsWriteMessage, keyDecrypter, cipherStream,
|
|
1724
|
-
);
|
|
1725
|
-
const plainBytes = await DataStream.toBytes(plainStream);
|
|
1726
|
-
entry.encodedData = Encoder.bytesToBase64Url(plainBytes);
|
|
1727
|
-
} catch (error: any) {
|
|
1728
|
-
throw new Error(
|
|
1729
|
-
`AgentDwnApi: Failed to decrypt record ` +
|
|
1730
|
-
`'${entry.recordId}'. Original error: ${error.message}`
|
|
1731
|
-
);
|
|
1732
|
-
}
|
|
1733
|
-
}
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
}
|
|
1096
|
+
return maybeDecryptReplyFn(
|
|
1097
|
+
request, reply, this.agent,
|
|
1098
|
+
this._contextDerivedKeyCache,
|
|
1099
|
+
this.fetchContextKeyRecord.bind(this),
|
|
1100
|
+
);
|
|
1737
1101
|
}
|
|
1738
1102
|
|
|
1739
|
-
/**
|
|
1740
|
-
* FURTHER REFACTORING NEEDED BELOW THIS LINE
|
|
1741
|
-
*/
|
|
1742
|
-
|
|
1743
1103
|
private async getDwnMessage<T extends DwnInterface>({ author, messageCid }: {
|
|
1744
1104
|
author: string;
|
|
1745
1105
|
messageType: T;
|
|
@@ -1788,70 +1148,28 @@ export class AgentDwnApi {
|
|
|
1788
1148
|
|
|
1789
1149
|
/**
|
|
1790
1150
|
* Ensures the key delivery protocol is installed on the given tenant's DWN,
|
|
1791
|
-
* with `$encryption` keys injected.
|
|
1792
|
-
* as `DwnDataStore.initialize()`.
|
|
1151
|
+
* with `$encryption` keys injected.
|
|
1793
1152
|
*
|
|
1794
1153
|
* @param tenantDid - The DID of the DWN owner
|
|
1795
1154
|
*/
|
|
1796
|
-
async ensureKeyDeliveryProtocol(tenantDid: string): Promise<void> {
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
if (!existing) {
|
|
1805
|
-
// Derive and inject $encryption keys for each type path
|
|
1806
|
-
const keyDeriver = await this.getEncryptionKeyDeriver(tenantDid);
|
|
1807
|
-
const definitionWithKeys = await Protocols.deriveAndInjectPublicEncryptionKeys(
|
|
1808
|
-
KeyDeliveryProtocolDefinition,
|
|
1809
|
-
keyDeriver,
|
|
1810
|
-
);
|
|
1811
|
-
|
|
1812
|
-
const { reply: { status } } = await this.processRequest({
|
|
1813
|
-
author : tenantDid,
|
|
1814
|
-
target : tenantDid,
|
|
1815
|
-
messageType : DwnInterface.ProtocolsConfigure,
|
|
1816
|
-
messageParams : { definition: definitionWithKeys },
|
|
1817
|
-
});
|
|
1818
|
-
|
|
1819
|
-
if (status.code !== 202) {
|
|
1820
|
-
throw new Error(`AgentDwnApi: Failed to install key delivery protocol: ${status.code} - ${status.detail}`);
|
|
1821
|
-
}
|
|
1822
|
-
|
|
1823
|
-
// Invalidate protocol definition cache so subsequent reads pick up the new definition
|
|
1824
|
-
this._protocolDefinitionCache.delete(`${tenantDid}~${protocolUri}`);
|
|
1825
|
-
}
|
|
1826
|
-
|
|
1827
|
-
this._keyDeliveryProtocolInstalledCache.set(tenantDid, true);
|
|
1155
|
+
public async ensureKeyDeliveryProtocol(tenantDid: string): Promise<void> {
|
|
1156
|
+
return ensureKeyDeliveryProtocolFn(
|
|
1157
|
+
this.agent, tenantDid,
|
|
1158
|
+
this.processRequest.bind(this),
|
|
1159
|
+
this.getProtocolDefinition.bind(this),
|
|
1160
|
+
this._keyDeliveryProtocolInstalledCache,
|
|
1161
|
+
this._protocolDefinitionCache,
|
|
1162
|
+
);
|
|
1828
1163
|
}
|
|
1829
1164
|
|
|
1830
1165
|
/**
|
|
1831
1166
|
* Writes a `contextKey` record to the owner's DWN, delivering an encrypted
|
|
1832
1167
|
* context key to a participant.
|
|
1833
1168
|
*
|
|
1834
|
-
*
|
|
1835
|
-
* key on the key-delivery protocol, so only the recipient can decrypt it.
|
|
1836
|
-
* The recipient's key is supplied via `recipientKeyDeliveryPublicKey` (which
|
|
1837
|
-
* the external author attached as `authorKeyDeliveryPublicKey` on the
|
|
1838
|
-
* original cross-DWN record).
|
|
1839
|
-
*
|
|
1840
|
-
* When `recipientKeyDeliveryPublicKey` is not provided (e.g. the owner is
|
|
1841
|
-
* writing a contextKey for themselves), the record is encrypted to the
|
|
1842
|
-
* owner's own ProtocolPath key using the generic `processRequest` path.
|
|
1843
|
-
*
|
|
1844
|
-
* @param params.tenantDid - The DWN owner's DID (who is delivering the key)
|
|
1845
|
-
* @param params.recipientDid - The participant's DID (who will receive the key)
|
|
1846
|
-
* @param params.contextKeyData - The `DerivedPrivateJwk` to deliver
|
|
1847
|
-
* @param params.sourceProtocol - The URI of the source protocol (tag)
|
|
1848
|
-
* @param params.sourceContextId - The root context ID (tag)
|
|
1849
|
-
* @param params.recipientKeyDeliveryPublicKey - The recipient's ProtocolPath-
|
|
1850
|
-
* derived public key for `key-delivery/contextKey`. When provided,
|
|
1851
|
-
* the contextKey record is encrypted directly to this key.
|
|
1169
|
+
* @param params - The write parameters
|
|
1852
1170
|
* @returns The recordId of the written contextKey record
|
|
1853
1171
|
*/
|
|
1854
|
-
async writeContextKeyRecord(
|
|
1172
|
+
public async writeContextKeyRecord(params: {
|
|
1855
1173
|
tenantDid: string;
|
|
1856
1174
|
recipientDid: string;
|
|
1857
1175
|
contextKeyData: DerivedPrivateJwk;
|
|
@@ -1859,85 +1177,12 @@ export class AgentDwnApi {
|
|
|
1859
1177
|
sourceContextId: string;
|
|
1860
1178
|
recipientKeyDeliveryPublicKey?: { rootKeyId: string; publicKeyJwk: PublicKeyJwk };
|
|
1861
1179
|
}): Promise<string> {
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
const plaintextBytes = new TextEncoder().encode(JSON.stringify(contextKeyData));
|
|
1869
|
-
|
|
1870
|
-
// Common contextKey record parameters
|
|
1871
|
-
const contextKeyParams = {
|
|
1872
|
-
protocol : protocolUri,
|
|
1873
|
-
protocolPath : 'contextKey',
|
|
1874
|
-
dataFormat : 'application/json',
|
|
1875
|
-
recipient : recipientDid,
|
|
1876
|
-
tags : { protocol: sourceProtocol, contextId: sourceContextId },
|
|
1877
|
-
};
|
|
1878
|
-
|
|
1879
|
-
let message: any;
|
|
1880
|
-
let status: { code: number; detail: string };
|
|
1881
|
-
|
|
1882
|
-
if (recipientKeyDeliveryPublicKey) {
|
|
1883
|
-
// --- Encrypt to the recipient's ProtocolPath key (cross-DWN delivery) ---
|
|
1884
|
-
// Manually build encryption input targeting the recipient's key so the
|
|
1885
|
-
// record is decryptable only by the recipient.
|
|
1886
|
-
const algorithm = ContentEncryptionAlgorithm.A256GCM;
|
|
1887
|
-
const dataEncryptionKey = crypto.getRandomValues(new Uint8Array(32));
|
|
1888
|
-
const dataEncryptionIV = crypto.getRandomValues(new Uint8Array(AgentDwnApi.ivLength(algorithm)));
|
|
1889
|
-
|
|
1890
|
-
const { encryptedBytes, dataCid, dataSize, authenticationTag } =
|
|
1891
|
-
await this.encryptAndComputeCid(plaintextBytes, dataEncryptionKey, dataEncryptionIV, algorithm);
|
|
1892
|
-
|
|
1893
|
-
const encryptionInput = {
|
|
1894
|
-
...this.buildEncryptionInput(
|
|
1895
|
-
dataEncryptionKey, dataEncryptionIV,
|
|
1896
|
-
recipientKeyDeliveryPublicKey.rootKeyId,
|
|
1897
|
-
recipientKeyDeliveryPublicKey.publicKeyJwk,
|
|
1898
|
-
KeyDerivationScheme.ProtocolPath,
|
|
1899
|
-
),
|
|
1900
|
-
authenticationTag,
|
|
1901
|
-
} as EncryptionInput;
|
|
1902
|
-
|
|
1903
|
-
({ message, reply: { status } } = await this.processRequest({
|
|
1904
|
-
author : tenantDid,
|
|
1905
|
-
target : tenantDid,
|
|
1906
|
-
messageType : DwnInterface.RecordsWrite,
|
|
1907
|
-
messageParams : { ...contextKeyParams, dataCid, dataSize, encryptionInput },
|
|
1908
|
-
dataStream : new Blob([encryptedBytes]),
|
|
1909
|
-
}));
|
|
1910
|
-
} else {
|
|
1911
|
-
// --- Fallback: encrypt to the owner's key (local self-delivery) ---
|
|
1912
|
-
// When no recipient key is provided, use the generic processRequest
|
|
1913
|
-
// encryption path which encrypts to the DWN owner's ProtocolPath key.
|
|
1914
|
-
({ message, reply: { status } } = await this.processRequest({
|
|
1915
|
-
author : tenantDid,
|
|
1916
|
-
target : tenantDid,
|
|
1917
|
-
messageType : DwnInterface.RecordsWrite,
|
|
1918
|
-
messageParams : contextKeyParams,
|
|
1919
|
-
dataStream : new Blob([plaintextBytes], { type: 'application/json' }),
|
|
1920
|
-
encryption : true,
|
|
1921
|
-
}));
|
|
1922
|
-
}
|
|
1923
|
-
|
|
1924
|
-
if (!(message && status.code === 202)) {
|
|
1925
|
-
throw new Error(
|
|
1926
|
-
`AgentDwnApi: Failed to write contextKey record for ${recipientDid}: ${status.code} - ${status.detail}`,
|
|
1927
|
-
);
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
// Eagerly send the contextKey record to the tenant's remote DWN so that
|
|
1931
|
-
// participants can fetch it immediately without waiting for sync.
|
|
1932
|
-
// This is fire-and-forget — sync will guarantee eventual consistency.
|
|
1933
|
-
this.eagerSendContextKeyRecord(tenantDid, message).catch((err: Error) => {
|
|
1934
|
-
console.warn(
|
|
1935
|
-
`AgentDwnApi: Eager send of contextKey record '${message.recordId}' ` +
|
|
1936
|
-
`to remote DWN failed: ${err.message}. Sync will deliver it later.`
|
|
1937
|
-
);
|
|
1938
|
-
});
|
|
1939
|
-
|
|
1940
|
-
return message.recordId;
|
|
1180
|
+
return writeContextKeyRecordFn(
|
|
1181
|
+
this.agent, params,
|
|
1182
|
+
this.processRequest.bind(this),
|
|
1183
|
+
this.ensureKeyDeliveryProtocol.bind(this),
|
|
1184
|
+
this.eagerSendContextKeyRecord.bind(this),
|
|
1185
|
+
);
|
|
1941
1186
|
}
|
|
1942
1187
|
|
|
1943
1188
|
/**
|
|
@@ -1948,142 +1193,31 @@ export class AgentDwnApi {
|
|
|
1948
1193
|
tenantDid: string,
|
|
1949
1194
|
contextKeyMessage: DwnMessage[DwnInterface.RecordsWrite],
|
|
1950
1195
|
): Promise<void> {
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
return;
|
|
1957
|
-
}
|
|
1958
|
-
|
|
1959
|
-
if (dwnEndpointUrls.length === 0) {
|
|
1960
|
-
return;
|
|
1961
|
-
}
|
|
1962
|
-
|
|
1963
|
-
// Read the full message (including data blob) from the local DWN
|
|
1964
|
-
const { data } = await this.getDwnMessage({
|
|
1965
|
-
author : tenantDid,
|
|
1966
|
-
messageType : DwnInterface.RecordsWrite,
|
|
1967
|
-
messageCid : await Message.getCid(contextKeyMessage),
|
|
1968
|
-
});
|
|
1969
|
-
|
|
1970
|
-
await this.sendDwnRpcRequest({
|
|
1971
|
-
targetDid : tenantDid,
|
|
1972
|
-
dwnEndpointUrls,
|
|
1973
|
-
message : contextKeyMessage,
|
|
1974
|
-
data,
|
|
1975
|
-
});
|
|
1196
|
+
return eagerSendContextKeyRecordFn(
|
|
1197
|
+
this.agent, tenantDid, contextKeyMessage,
|
|
1198
|
+
this.getDwnMessage.bind(this),
|
|
1199
|
+
this.sendDwnRpcRequest.bind(this),
|
|
1200
|
+
);
|
|
1976
1201
|
}
|
|
1977
1202
|
|
|
1978
1203
|
/**
|
|
1979
1204
|
* Fetches and decrypts a `contextKey` record from a DWN, returning the
|
|
1980
1205
|
* `DerivedPrivateJwk` payload.
|
|
1981
1206
|
*
|
|
1982
|
-
*
|
|
1983
|
-
* (participant queries the context owner's DWN).
|
|
1984
|
-
*
|
|
1985
|
-
* @param params.ownerDid - The DWN owner's DID (where contextKey records live)
|
|
1986
|
-
* @param params.requesterDid - The DID of the requester (used for signing and decryption)
|
|
1987
|
-
* @param params.sourceProtocol - The URI of the source protocol (tag filter)
|
|
1988
|
-
* @param params.sourceContextId - The root context ID (tag filter)
|
|
1207
|
+
* @param params - The fetch parameters
|
|
1989
1208
|
* @returns The decrypted `DerivedPrivateJwk`, or `undefined` if no matching record found
|
|
1990
1209
|
*/
|
|
1991
|
-
async fetchContextKeyRecord(
|
|
1210
|
+
public async fetchContextKeyRecord(params: {
|
|
1992
1211
|
ownerDid: string;
|
|
1993
1212
|
requesterDid: string;
|
|
1994
1213
|
sourceProtocol: string;
|
|
1995
1214
|
sourceContextId: string;
|
|
1996
1215
|
}): Promise<DerivedPrivateJwk | undefined> {
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
protocolPath : 'contextKey',
|
|
2004
|
-
recipient : requesterDid,
|
|
2005
|
-
tags : { protocol: sourceProtocol, contextId: sourceContextId },
|
|
2006
|
-
};
|
|
2007
|
-
|
|
2008
|
-
/** Parse decrypted bytes into a DerivedPrivateJwk. */
|
|
2009
|
-
const parsePayload = (bytes: Uint8Array): DerivedPrivateJwk =>
|
|
2010
|
-
JSON.parse(new TextDecoder().decode(bytes)) as DerivedPrivateJwk;
|
|
2011
|
-
|
|
2012
|
-
if (isLocal) {
|
|
2013
|
-
// Local query: owner queries their own DWN
|
|
2014
|
-
const { reply } = await this.processRequest({
|
|
2015
|
-
author : requesterDid,
|
|
2016
|
-
target : ownerDid,
|
|
2017
|
-
messageType : DwnInterface.RecordsQuery,
|
|
2018
|
-
messageParams : { filter: contextKeyFilter },
|
|
2019
|
-
});
|
|
2020
|
-
|
|
2021
|
-
if (reply.status.code !== 200 || !reply.entries?.length) {
|
|
2022
|
-
return undefined;
|
|
2023
|
-
}
|
|
2024
|
-
|
|
2025
|
-
// Read the full record to get the data (auto-decrypted by processRequest)
|
|
2026
|
-
const recordId = reply.entries[0].recordId;
|
|
2027
|
-
const { reply: readReply } = await this.processRequest({
|
|
2028
|
-
author : requesterDid,
|
|
2029
|
-
target : ownerDid,
|
|
2030
|
-
messageType : DwnInterface.RecordsRead,
|
|
2031
|
-
messageParams : { filter: { recordId } },
|
|
2032
|
-
encryption : true,
|
|
2033
|
-
});
|
|
2034
|
-
|
|
2035
|
-
const readResult = readReply as RecordsReadReply;
|
|
2036
|
-
if (!readResult.entry?.data) {
|
|
2037
|
-
return undefined;
|
|
2038
|
-
}
|
|
2039
|
-
|
|
2040
|
-
return parsePayload(await DataStream.toBytes(readResult.entry.data));
|
|
2041
|
-
} else {
|
|
2042
|
-
// Remote query: participant queries the context owner's DWN
|
|
2043
|
-
const signer = await this.getSigner(requesterDid);
|
|
2044
|
-
const dwnEndpointUrls = await getDwnServiceEndpointUrls(ownerDid, this.agent.did);
|
|
2045
|
-
|
|
2046
|
-
const recordsQuery = await dwnMessageConstructors[DwnInterface.RecordsQuery].create({
|
|
2047
|
-
signer,
|
|
2048
|
-
filter: contextKeyFilter,
|
|
2049
|
-
});
|
|
2050
|
-
|
|
2051
|
-
const queryReply = await this.sendDwnRpcRequest<DwnInterface.RecordsQuery>({
|
|
2052
|
-
targetDid : ownerDid,
|
|
2053
|
-
dwnEndpointUrls,
|
|
2054
|
-
message : recordsQuery.message,
|
|
2055
|
-
}) as RecordsQueryReply;
|
|
2056
|
-
|
|
2057
|
-
if (queryReply.status.code !== 200 || !queryReply.entries?.length) {
|
|
2058
|
-
return undefined;
|
|
2059
|
-
}
|
|
2060
|
-
|
|
2061
|
-
// Read the full record remotely
|
|
2062
|
-
const recordId = queryReply.entries[0].recordId;
|
|
2063
|
-
const recordsRead = await dwnMessageConstructors[DwnInterface.RecordsRead].create({
|
|
2064
|
-
signer,
|
|
2065
|
-
filter: { recordId },
|
|
2066
|
-
});
|
|
2067
|
-
|
|
2068
|
-
const readReply = await this.sendDwnRpcRequest<DwnInterface.RecordsRead>({
|
|
2069
|
-
targetDid : ownerDid,
|
|
2070
|
-
dwnEndpointUrls,
|
|
2071
|
-
message : recordsRead.message,
|
|
2072
|
-
}) as RecordsReadReply;
|
|
2073
|
-
|
|
2074
|
-
if (!readReply.entry?.data || !readReply.entry?.recordsWrite) {
|
|
2075
|
-
return undefined;
|
|
2076
|
-
}
|
|
2077
|
-
|
|
2078
|
-
// Decrypt the contextKey payload using the requester's key-delivery protocol path key
|
|
2079
|
-
const keyDecrypter = await this.getKeyDecrypter(requesterDid);
|
|
2080
|
-
const decryptedStream = await Records.decrypt(
|
|
2081
|
-
readReply.entry.recordsWrite,
|
|
2082
|
-
keyDecrypter,
|
|
2083
|
-
readReply.entry.data as ReadableStream<Uint8Array>,
|
|
2084
|
-
);
|
|
2085
|
-
|
|
2086
|
-
return parsePayload(await DataStream.toBytes(decryptedStream));
|
|
2087
|
-
}
|
|
1216
|
+
return fetchContextKeyRecordFn(
|
|
1217
|
+
this.agent, params,
|
|
1218
|
+
this.processRequest.bind(this),
|
|
1219
|
+
this.getSigner.bind(this),
|
|
1220
|
+
this.sendDwnRpcRequest.bind(this),
|
|
1221
|
+
);
|
|
2088
1222
|
}
|
|
2089
|
-
}
|
|
1223
|
+
}
|