@enbox/dwn-sdk-js 0.4.5 → 0.4.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/README.md +3 -1
- package/dist/browser.mjs +19 -78
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/generated/precompiled-validators.js +4465 -3861
- package/dist/esm/generated/precompiled-validators.js.map +1 -1
- package/dist/esm/src/core/constants.js +50 -0
- package/dist/esm/src/core/constants.js.map +1 -1
- package/dist/esm/src/core/dwn-error.js +28 -11
- package/dist/esm/src/core/dwn-error.js.map +1 -1
- package/dist/esm/src/core/encryption-control.js +785 -0
- package/dist/esm/src/core/encryption-control.js.map +1 -0
- package/dist/esm/src/core/message.js +6 -0
- package/dist/esm/src/core/message.js.map +1 -1
- package/dist/esm/src/core/messages-grant-authorization.js +36 -5
- package/dist/esm/src/core/messages-grant-authorization.js.map +1 -1
- package/dist/esm/src/core/protocol-authorization-action.js +3 -12
- package/dist/esm/src/core/protocol-authorization-action.js.map +1 -1
- package/dist/esm/src/core/protocol-authorization-validation.js +31 -30
- package/dist/esm/src/core/protocol-authorization-validation.js.map +1 -1
- package/dist/esm/src/core/protocol-authorization.js +6 -0
- package/dist/esm/src/core/protocol-authorization.js.map +1 -1
- package/dist/esm/src/core/recording-validation-state-reader.js +3 -3
- package/dist/esm/src/core/recording-validation-state-reader.js.map +1 -1
- package/dist/esm/src/core/replication-apply.js +62 -108
- package/dist/esm/src/core/replication-apply.js.map +1 -1
- package/dist/esm/src/core/validation-state-reader.js +5 -6
- package/dist/esm/src/core/validation-state-reader.js.map +1 -1
- package/dist/esm/src/dwn.js +1 -2
- package/dist/esm/src/dwn.js.map +1 -1
- package/dist/esm/src/event-stream/event-emitter-wake-publisher.js +11 -11
- package/dist/esm/src/event-stream/event-emitter-wake-publisher.js.map +1 -1
- package/dist/esm/src/handlers/messages-query.js +6 -16
- package/dist/esm/src/handlers/messages-query.js.map +1 -1
- package/dist/esm/src/handlers/messages-read.js +4 -3
- package/dist/esm/src/handlers/messages-read.js.map +1 -1
- package/dist/esm/src/handlers/messages-subscribe.js +26 -30
- package/dist/esm/src/handlers/messages-subscribe.js.map +1 -1
- package/dist/esm/src/handlers/protocols-configure.js +6 -4
- package/dist/esm/src/handlers/protocols-configure.js.map +1 -1
- package/dist/esm/src/handlers/records-count.js +85 -10
- package/dist/esm/src/handlers/records-count.js.map +1 -1
- package/dist/esm/src/handlers/records-delete.js +11 -0
- package/dist/esm/src/handlers/records-delete.js.map +1 -1
- package/dist/esm/src/handlers/records-query.js +140 -18
- package/dist/esm/src/handlers/records-query.js.map +1 -1
- package/dist/esm/src/handlers/records-read.js +20 -4
- package/dist/esm/src/handlers/records-read.js.map +1 -1
- package/dist/esm/src/handlers/records-subscribe.js +127 -11
- package/dist/esm/src/handlers/records-subscribe.js.map +1 -1
- package/dist/esm/src/handlers/records-write.js +15 -0
- package/dist/esm/src/handlers/records-write.js.map +1 -1
- package/dist/esm/src/index.js +7 -10
- package/dist/esm/src/index.js.map +1 -1
- package/dist/esm/src/interfaces/protocols-configure.js +66 -9
- package/dist/esm/src/interfaces/protocols-configure.js.map +1 -1
- package/dist/esm/src/interfaces/records-write.js +43 -36
- package/dist/esm/src/interfaces/records-write.js.map +1 -1
- package/dist/esm/src/jose/algorithms/signing/ed25519.js +5 -5
- package/dist/esm/src/jose/algorithms/signing/ed25519.js.map +1 -1
- package/dist/esm/src/jose/jws/general/verifier.js +1 -1
- package/dist/esm/src/jose/jws/general/verifier.js.map +1 -1
- package/dist/esm/src/protocols/encryption.js +32 -274
- package/dist/esm/src/protocols/encryption.js.map +1 -1
- package/dist/esm/src/store/blockstore-level.js +14 -23
- package/dist/esm/src/store/blockstore-level.js.map +1 -1
- package/dist/esm/src/store/blockstore-mock.js +10 -27
- package/dist/esm/src/store/blockstore-mock.js.map +1 -1
- package/dist/esm/src/store/blockstore-utils.js +45 -0
- package/dist/esm/src/store/blockstore-utils.js.map +1 -0
- package/dist/esm/src/store/blockstore.js +2 -0
- package/dist/esm/src/store/blockstore.js.map +1 -0
- package/dist/esm/src/store/data-store-level.js +4 -12
- package/dist/esm/src/store/data-store-level.js.map +1 -1
- package/dist/esm/src/store/level.js +7 -0
- package/dist/esm/src/store/level.js.map +1 -0
- package/dist/esm/src/types/encryption-types.js +4 -1
- package/dist/esm/src/types/encryption-types.js.map +1 -1
- package/dist/esm/src/utils/data-stream.js +25 -0
- package/dist/esm/src/utils/data-stream.js.map +1 -1
- package/dist/esm/src/utils/encryption.js +142 -34
- package/dist/esm/src/utils/encryption.js.map +1 -1
- package/dist/esm/src/utils/grant-key-coverage.js +180 -0
- package/dist/esm/src/utils/grant-key-coverage.js.map +1 -0
- package/dist/esm/src/utils/hd-key.js +1 -2
- package/dist/esm/src/utils/hd-key.js.map +1 -1
- package/dist/esm/src/utils/jws.js +7 -1
- package/dist/esm/src/utils/jws.js.map +1 -1
- package/dist/esm/src/utils/memory-cache.js +29 -8
- package/dist/esm/src/utils/memory-cache.js.map +1 -1
- package/dist/esm/src/utils/protocols.js +27 -0
- package/dist/esm/src/utils/protocols.js.map +1 -1
- package/dist/esm/src/utils/record-limit-occupancy.js +23 -3
- package/dist/esm/src/utils/record-limit-occupancy.js.map +1 -1
- package/dist/esm/src/utils/records.js +29 -0
- package/dist/esm/src/utils/records.js.map +1 -1
- package/dist/esm/src/utils/secp256k1.js +12 -11
- package/dist/esm/src/utils/secp256k1.js.map +1 -1
- package/dist/esm/src/utils/secp256r1.js +12 -20
- package/dist/esm/src/utils/secp256r1.js.map +1 -1
- package/dist/esm/src/utils/time.js +103 -25
- package/dist/esm/src/utils/time.js.map +1 -1
- package/dist/esm/tests/core/constants.spec.js +35 -0
- package/dist/esm/tests/core/constants.spec.js.map +1 -0
- package/dist/esm/tests/core/encryption-control.spec.js +100 -0
- package/dist/esm/tests/core/encryption-control.spec.js.map +1 -0
- package/dist/esm/tests/core/message.spec.js +35 -0
- package/dist/esm/tests/core/message.spec.js.map +1 -1
- package/dist/esm/tests/core/protocol-authorization.spec.js +1 -1
- package/dist/esm/tests/core/protocol-authorization.spec.js.map +1 -1
- package/dist/esm/tests/core/replication-apply.spec.js +157 -74
- package/dist/esm/tests/core/replication-apply.spec.js.map +1 -1
- package/dist/esm/tests/core/replication-replay-property.spec.js +2 -1
- package/dist/esm/tests/core/replication-replay-property.spec.js.map +1 -1
- package/dist/esm/tests/core/validation-read-closure.spec.js +14 -28
- package/dist/esm/tests/core/validation-read-closure.spec.js.map +1 -1
- package/dist/esm/tests/core/validation-state-reader.spec.js +19 -23
- package/dist/esm/tests/core/validation-state-reader.spec.js.map +1 -1
- package/dist/esm/tests/dwn.spec.js +2 -1
- package/dist/esm/tests/dwn.spec.js.map +1 -1
- package/dist/esm/tests/event-emitter-wake-publisher.spec.js +51 -0
- package/dist/esm/tests/event-emitter-wake-publisher.spec.js.map +1 -0
- package/dist/esm/tests/features/protocol-composition.spec.js +1 -1
- package/dist/esm/tests/features/protocol-composition.spec.js.map +1 -1
- package/dist/esm/tests/features/records-record-limit.spec.js +1 -1
- package/dist/esm/tests/features/records-record-limit.spec.js.map +1 -1
- package/dist/esm/tests/fuzz/encryption.fuzz.spec.js +9 -1
- package/dist/esm/tests/fuzz/encryption.fuzz.spec.js.map +1 -1
- package/dist/esm/tests/fuzz/process-message.fuzz.spec.js +1 -1
- package/dist/esm/tests/fuzz/process-message.fuzz.spec.js.map +1 -1
- package/dist/esm/tests/fuzz/protocols-utils.fuzz.spec.js +29 -1
- package/dist/esm/tests/fuzz/protocols-utils.fuzz.spec.js.map +1 -1
- package/dist/esm/tests/fuzz/secp256k1.fuzz.spec.js +2 -2
- package/dist/esm/tests/fuzz/secp256k1.fuzz.spec.js.map +1 -1
- package/dist/esm/tests/fuzz/time.fuzz.spec.js +7 -1
- package/dist/esm/tests/fuzz/time.fuzz.spec.js.map +1 -1
- package/dist/esm/tests/handlers/messages-query.spec.js +123 -11
- package/dist/esm/tests/handlers/messages-query.spec.js.map +1 -1
- package/dist/esm/tests/handlers/messages-read.spec.js +95 -0
- package/dist/esm/tests/handlers/messages-read.spec.js.map +1 -1
- package/dist/esm/tests/handlers/messages-subscribe.spec.js +90 -0
- package/dist/esm/tests/handlers/messages-subscribe.spec.js.map +1 -1
- package/dist/esm/tests/handlers/protocols-configure.spec.js +66 -0
- package/dist/esm/tests/handlers/protocols-configure.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-count.spec.js +370 -0
- package/dist/esm/tests/handlers/records-count.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-delete.spec.js +70 -1
- package/dist/esm/tests/handlers/records-delete.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-query.spec.js +562 -1
- package/dist/esm/tests/handlers/records-query.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-read.spec.js +140 -2
- package/dist/esm/tests/handlers/records-read.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-subscribe.spec.js +241 -1
- package/dist/esm/tests/handlers/records-subscribe.spec.js.map +1 -1
- package/dist/esm/tests/handlers/records-write.spec.js +1187 -29
- package/dist/esm/tests/handlers/records-write.spec.js.map +1 -1
- package/dist/esm/tests/interfaces/protocols-configure.spec.js +290 -2
- package/dist/esm/tests/interfaces/protocols-configure.spec.js.map +1 -1
- package/dist/esm/tests/interfaces/records-write.spec.js +47 -5
- package/dist/esm/tests/interfaces/records-write.spec.js.map +1 -1
- package/dist/esm/tests/protocols/encryption.spec.js +116 -472
- package/dist/esm/tests/protocols/encryption.spec.js.map +1 -1
- package/dist/esm/tests/scenarios/end-to-end-tests.spec.js +13 -27
- package/dist/esm/tests/scenarios/end-to-end-tests.spec.js.map +1 -1
- package/dist/esm/tests/store/blockstore-level.spec.js +33 -10
- package/dist/esm/tests/store/blockstore-level.spec.js.map +1 -1
- package/dist/esm/tests/store/blockstore-mock.spec.js +31 -7
- package/dist/esm/tests/store/blockstore-mock.spec.js.map +1 -1
- package/dist/esm/tests/store/blockstore-utils.spec.js +115 -0
- package/dist/esm/tests/store/blockstore-utils.spec.js.map +1 -0
- package/dist/esm/tests/store/index-level.spec.js +3 -3
- package/dist/esm/tests/store/index-level.spec.js.map +1 -1
- package/dist/esm/tests/test-stores.js +2 -1
- package/dist/esm/tests/test-stores.js.map +1 -1
- package/dist/esm/tests/utils/cid.spec.js +15 -0
- package/dist/esm/tests/utils/cid.spec.js.map +1 -1
- package/dist/esm/tests/utils/data-stream.spec.js +30 -0
- package/dist/esm/tests/utils/data-stream.spec.js.map +1 -1
- package/dist/esm/tests/utils/encryption-control-test-utils.js +111 -0
- package/dist/esm/tests/utils/encryption-control-test-utils.js.map +1 -0
- package/dist/esm/tests/utils/encryption.spec.js +240 -67
- package/dist/esm/tests/utils/encryption.spec.js.map +1 -1
- package/dist/esm/tests/utils/grant-key-coverage.spec.js +262 -0
- package/dist/esm/tests/utils/grant-key-coverage.spec.js.map +1 -0
- package/dist/esm/tests/utils/memory-cache.spec.js +12 -0
- package/dist/esm/tests/utils/memory-cache.spec.js.map +1 -1
- package/dist/esm/tests/utils/secp256r1.spec.js +2 -2
- package/dist/esm/tests/utils/secp256r1.spec.js.map +1 -1
- package/dist/esm/tests/utils/test-data-generator.js +2 -1
- package/dist/esm/tests/utils/test-data-generator.js.map +1 -1
- package/dist/esm/tests/utils/test-validation-state-reader.js +1 -1
- package/dist/esm/tests/utils/test-validation-state-reader.js.map +1 -1
- package/dist/esm/tests/utils/time.spec.js +26 -0
- package/dist/esm/tests/utils/time.spec.js.map +1 -1
- package/dist/esm/tests/validation/json-schemas/encryption/audience.spec.js +45 -0
- package/dist/esm/tests/validation/json-schemas/encryption/audience.spec.js.map +1 -0
- package/dist/esm/tests/validation/json-schemas/encryption/delivery.spec.js +47 -0
- package/dist/esm/tests/validation/json-schemas/encryption/delivery.spec.js.map +1 -0
- package/dist/esm/tests/validation/json-schemas/encryption/grant-key.spec.js +25 -16
- package/dist/esm/tests/validation/json-schemas/encryption/grant-key.spec.js.map +1 -1
- package/dist/esm/tests/validation/json-schemas/protocols/protocols-configure.spec.js +152 -0
- package/dist/esm/tests/validation/json-schemas/protocols/protocols-configure.spec.js.map +1 -1
- package/dist/esm/tests/validation/json-schemas/records/records-write.spec.js +46 -1
- package/dist/esm/tests/validation/json-schemas/records/records-write.spec.js.map +1 -1
- package/dist/types/generated/precompiled-validators.d.ts +4 -4
- package/dist/types/generated/precompiled-validators.d.ts.map +1 -1
- package/dist/types/src/core/constants.d.ts +39 -0
- package/dist/types/src/core/constants.d.ts.map +1 -1
- package/dist/types/src/core/dwn-error.d.ts +28 -11
- package/dist/types/src/core/dwn-error.d.ts.map +1 -1
- package/dist/types/src/core/encryption-control.d.ts +128 -0
- package/dist/types/src/core/encryption-control.d.ts.map +1 -0
- package/dist/types/src/core/message.d.ts +4 -0
- package/dist/types/src/core/message.d.ts.map +1 -1
- package/dist/types/src/core/messages-grant-authorization.d.ts +11 -0
- package/dist/types/src/core/messages-grant-authorization.d.ts.map +1 -1
- package/dist/types/src/core/protocol-authorization-action.d.ts.map +1 -1
- package/dist/types/src/core/protocol-authorization-validation.d.ts.map +1 -1
- package/dist/types/src/core/protocol-authorization.d.ts.map +1 -1
- package/dist/types/src/core/recording-validation-state-reader.d.ts +2 -3
- package/dist/types/src/core/recording-validation-state-reader.d.ts.map +1 -1
- package/dist/types/src/core/replication-apply.d.ts +10 -1
- package/dist/types/src/core/replication-apply.d.ts.map +1 -1
- package/dist/types/src/core/validation-state-reader.d.ts +2 -3
- package/dist/types/src/core/validation-state-reader.d.ts.map +1 -1
- package/dist/types/src/dwn.d.ts.map +1 -1
- package/dist/types/src/event-stream/event-emitter-wake-publisher.d.ts +1 -1
- package/dist/types/src/event-stream/event-emitter-wake-publisher.d.ts.map +1 -1
- package/dist/types/src/handlers/messages-query.d.ts.map +1 -1
- package/dist/types/src/handlers/messages-read.d.ts.map +1 -1
- package/dist/types/src/handlers/messages-subscribe.d.ts.map +1 -1
- package/dist/types/src/handlers/protocols-configure.d.ts +1 -0
- package/dist/types/src/handlers/protocols-configure.d.ts.map +1 -1
- package/dist/types/src/handlers/records-count.d.ts +2 -0
- package/dist/types/src/handlers/records-count.d.ts.map +1 -1
- package/dist/types/src/handlers/records-delete.d.ts.map +1 -1
- package/dist/types/src/handlers/records-query.d.ts +3 -0
- package/dist/types/src/handlers/records-query.d.ts.map +1 -1
- package/dist/types/src/handlers/records-read.d.ts.map +1 -1
- package/dist/types/src/handlers/records-subscribe.d.ts +2 -0
- package/dist/types/src/handlers/records-subscribe.d.ts.map +1 -1
- package/dist/types/src/handlers/records-write.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +10 -17
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/interfaces/protocols-configure.d.ts +13 -1
- package/dist/types/src/interfaces/protocols-configure.d.ts.map +1 -1
- package/dist/types/src/interfaces/records-write.d.ts +5 -1
- package/dist/types/src/interfaces/records-write.d.ts.map +1 -1
- package/dist/types/src/jose/algorithms/signing/ed25519.d.ts.map +1 -1
- package/dist/types/src/protocols/encryption.d.ts +2 -19
- package/dist/types/src/protocols/encryption.d.ts.map +1 -1
- package/dist/types/src/store/blockstore-level.d.ts +11 -11
- package/dist/types/src/store/blockstore-level.d.ts.map +1 -1
- package/dist/types/src/store/blockstore-mock.d.ts +12 -12
- package/dist/types/src/store/blockstore-mock.d.ts.map +1 -1
- package/dist/types/src/store/blockstore-utils.d.ts +23 -0
- package/dist/types/src/store/blockstore-utils.d.ts.map +1 -0
- package/dist/types/src/store/blockstore.d.ts +3 -0
- package/dist/types/src/store/blockstore.d.ts.map +1 -0
- package/dist/types/src/store/data-store-level.d.ts.map +1 -1
- package/dist/types/src/store/level.d.ts +13 -0
- package/dist/types/src/store/level.d.ts.map +1 -0
- package/dist/types/src/types/cache.d.ts +2 -2
- package/dist/types/src/types/cache.d.ts.map +1 -1
- package/dist/types/src/types/encryption-types.d.ts +31 -2
- package/dist/types/src/types/encryption-types.d.ts.map +1 -1
- package/dist/types/src/types/validation-state-reader.d.ts +3 -5
- package/dist/types/src/types/validation-state-reader.d.ts.map +1 -1
- package/dist/types/src/utils/data-stream.d.ts +5 -0
- package/dist/types/src/utils/data-stream.d.ts.map +1 -1
- package/dist/types/src/utils/encryption.d.ts +58 -22
- package/dist/types/src/utils/encryption.d.ts.map +1 -1
- package/dist/types/src/utils/grant-key-coverage.d.ts +36 -0
- package/dist/types/src/utils/grant-key-coverage.d.ts.map +1 -0
- package/dist/types/src/utils/hd-key.d.ts.map +1 -1
- package/dist/types/src/utils/jws.d.ts.map +1 -1
- package/dist/types/src/utils/memory-cache.d.ts +4 -2
- package/dist/types/src/utils/memory-cache.d.ts.map +1 -1
- package/dist/types/src/utils/protocols.d.ts +11 -0
- package/dist/types/src/utils/protocols.d.ts.map +1 -1
- package/dist/types/src/utils/record-limit-occupancy.d.ts +7 -0
- package/dist/types/src/utils/record-limit-occupancy.d.ts.map +1 -1
- package/dist/types/src/utils/records.d.ts +2 -0
- package/dist/types/src/utils/records.d.ts.map +1 -1
- package/dist/types/src/utils/secp256k1.d.ts.map +1 -1
- package/dist/types/src/utils/secp256r1.d.ts.map +1 -1
- package/dist/types/src/utils/time.d.ts +21 -13
- package/dist/types/src/utils/time.d.ts.map +1 -1
- package/dist/types/tests/core/constants.spec.d.ts +2 -0
- package/dist/types/tests/core/constants.spec.d.ts.map +1 -0
- package/dist/types/tests/core/encryption-control.spec.d.ts +2 -0
- package/dist/types/tests/core/encryption-control.spec.d.ts.map +1 -0
- package/dist/types/tests/dwn.spec.d.ts.map +1 -1
- package/dist/types/tests/event-emitter-wake-publisher.spec.d.ts +2 -0
- package/dist/types/tests/event-emitter-wake-publisher.spec.d.ts.map +1 -0
- package/dist/types/tests/handlers/messages-query.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/messages-read.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/messages-subscribe.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/protocols-configure.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/records-count.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/records-delete.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/records-query.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/records-read.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/records-subscribe.spec.d.ts.map +1 -1
- package/dist/types/tests/handlers/records-write.spec.d.ts.map +1 -1
- package/dist/types/tests/scenarios/end-to-end-tests.spec.d.ts.map +1 -1
- package/dist/types/tests/store/blockstore-utils.spec.d.ts +2 -0
- package/dist/types/tests/store/blockstore-utils.spec.d.ts.map +1 -0
- package/dist/types/tests/test-stores.d.ts.map +1 -1
- package/dist/types/tests/utils/encryption-control-test-utils.d.ts +46 -0
- package/dist/types/tests/utils/encryption-control-test-utils.d.ts.map +1 -0
- package/dist/types/tests/utils/grant-key-coverage.spec.d.ts +2 -0
- package/dist/types/tests/utils/grant-key-coverage.spec.d.ts.map +1 -0
- package/dist/types/tests/utils/test-data-generator.d.ts.map +1 -1
- package/dist/types/tests/validation/json-schemas/encryption/audience.spec.d.ts +2 -0
- package/dist/types/tests/validation/json-schemas/encryption/audience.spec.d.ts.map +1 -0
- package/dist/types/tests/validation/json-schemas/encryption/delivery.spec.d.ts +2 -0
- package/dist/types/tests/validation/json-schemas/encryption/delivery.spec.d.ts.map +1 -0
- package/package.json +47 -28
- package/src/core/constants.ts +61 -0
- package/src/core/dwn-error.ts +28 -11
- package/src/core/encryption-control.ts +1249 -0
- package/src/core/message.ts +7 -0
- package/src/core/messages-grant-authorization.ts +60 -9
- package/src/core/protocol-authorization-action.ts +3 -13
- package/src/core/protocol-authorization-validation.ts +66 -46
- package/src/core/protocol-authorization.ts +7 -0
- package/src/core/recording-validation-state-reader.ts +4 -5
- package/src/core/replication-apply.ts +77 -133
- package/src/core/validation-state-reader.ts +6 -8
- package/src/dwn.ts +1 -2
- package/src/event-stream/event-emitter-wake-publisher.ts +10 -14
- package/src/handlers/messages-query.ts +6 -22
- package/src/handlers/messages-read.ts +4 -3
- package/src/handlers/messages-subscribe.ts +34 -30
- package/src/handlers/protocols-configure.ts +7 -4
- package/src/handlers/records-count.ts +111 -10
- package/src/handlers/records-delete.ts +15 -0
- package/src/handlers/records-query.ts +176 -20
- package/src/handlers/records-read.ts +19 -6
- package/src/handlers/records-subscribe.ts +158 -8
- package/src/handlers/records-write.ts +19 -0
- package/src/index.ts +48 -21
- package/src/interfaces/protocols-configure.ts +102 -11
- package/src/interfaces/records-write.ts +49 -43
- package/src/jose/algorithms/signing/ed25519.ts +7 -6
- package/src/jose/jws/general/verifier.ts +3 -3
- package/src/protocols/encryption.ts +50 -420
- package/src/store/blockstore-level.ts +33 -37
- package/src/store/blockstore-mock.ts +24 -38
- package/src/store/blockstore-utils.ts +85 -0
- package/src/store/blockstore.ts +8 -0
- package/src/store/data-store-level.ts +5 -12
- package/src/store/level.ts +19 -0
- package/src/types/cache.ts +3 -3
- package/src/types/encryption-types.ts +39 -2
- package/src/types/validation-state-reader.ts +3 -5
- package/src/utils/data-stream.ts +29 -1
- package/src/utils/encryption.ts +247 -69
- package/src/utils/grant-key-coverage.ts +303 -0
- package/src/utils/hd-key.ts +1 -2
- package/src/utils/jws.ts +8 -2
- package/src/utils/memory-cache.ts +42 -11
- package/src/utils/protocols.ts +32 -0
- package/src/utils/record-limit-occupancy.ts +34 -5
- package/src/utils/records.ts +36 -2
- package/src/utils/secp256k1.ts +12 -12
- package/src/utils/secp256r1.ts +12 -22
- package/src/utils/time.ts +145 -27
- package/src/types/mitt.d.ts +0 -28
|
@@ -0,0 +1,1249 @@
|
|
|
1
|
+
import type { MessageStore } from '../types/message-store.js';
|
|
2
|
+
import type { RecordsPermissionScope } from '../types/permission-types.js';
|
|
3
|
+
import type { RecordsWrite } from '../interfaces/records-write.js';
|
|
4
|
+
import type { ValidationStateReader } from '../types/validation-state-reader.js';
|
|
5
|
+
import type { EncryptionControlAudiencePayload, EncryptionControlDeliveryTags, RoleAudienceKeyId } from '../types/encryption-types.js';
|
|
6
|
+
import type { Filter, PaginationCursor } from '../types/query-types.js';
|
|
7
|
+
import type { ProtocolActionRule, ProtocolDefinition, ProtocolRuleSet } from '../types/protocols-types.js';
|
|
8
|
+
import type { RecordsCountMessage, RecordsFilter, RecordsQueryMessage, RecordsReadMessage, RecordsSubscribeMessage, RecordsWriteMessage, RecordsWriteTags } from '../types/records-types.js';
|
|
9
|
+
|
|
10
|
+
import { checkActor } from './protocol-authorization-action.js';
|
|
11
|
+
import { DwnConstant } from './dwn-constant.js';
|
|
12
|
+
import { Encoder } from '../utils/encoder.js';
|
|
13
|
+
import { Encryption } from '../utils/encryption.js';
|
|
14
|
+
import { EncryptionControlDeliveryRecipientAuthority } from '../types/encryption-types.js';
|
|
15
|
+
import { GrantAuthorization } from './grant-authorization.js';
|
|
16
|
+
import { Jws } from '../utils/jws.js';
|
|
17
|
+
import { lexicographicalCompare } from '../utils/string.js';
|
|
18
|
+
import { Message } from './message.js';
|
|
19
|
+
import { PermissionConditionPublication } from '../types/permission-types.js';
|
|
20
|
+
import { PermissionGrant } from '../protocols/permission-grant.js';
|
|
21
|
+
import { PermissionScopeMatcher } from '../utils/permission-scope.js';
|
|
22
|
+
import { Records } from '../utils/records.js';
|
|
23
|
+
import { selectOccupantRecordIds } from '../utils/record-limit-occupancy.js';
|
|
24
|
+
import { SortDirection } from '../types/query-types.js';
|
|
25
|
+
import { validateJsonSchema } from '../schema-validator.js';
|
|
26
|
+
import { DwnError, DwnErrorCode } from './dwn-error.js';
|
|
27
|
+
import { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
|
|
28
|
+
import { ENCRYPTION_CONTROL_AUDIENCE_PATH, ENCRYPTION_CONTROL_DELIVERY_PATH, isEncryptionControlPath } from './constants.js';
|
|
29
|
+
import { getRoleContextPrefix, getRuleSetAtPath, isCrossProtocolRef, parseCrossProtocolRef } from '../utils/protocols.js';
|
|
30
|
+
import { grantKeyScopeCoversDeliveredScope, isGrantKeyEligibleRecordsScope } from '../utils/grant-key-coverage.js';
|
|
31
|
+
import { ProtocolAction, ProtocolActor } from '../types/protocols-types.js';
|
|
32
|
+
|
|
33
|
+
type RoleAudienceDefinition = {
|
|
34
|
+
protocolDefinition: ProtocolDefinition;
|
|
35
|
+
ruleSet: ProtocolRuleSet;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type EffectiveControlActor = {
|
|
39
|
+
did: string | undefined;
|
|
40
|
+
grant?: PermissionGrant;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type ExactAudienceFilterTuple = Omit<RoleAudienceKeyId, 'keyId'> & {
|
|
44
|
+
keyId?: string;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type AudienceTuple = Omit<RoleAudienceKeyId, 'keyId'>;
|
|
48
|
+
|
|
49
|
+
type ControlReadMessage =
|
|
50
|
+
| RecordsCountMessage
|
|
51
|
+
| RecordsReadMessage
|
|
52
|
+
| RecordsQueryMessage
|
|
53
|
+
| RecordsSubscribeMessage;
|
|
54
|
+
|
|
55
|
+
type ControlFilterMessage =
|
|
56
|
+
| RecordsCountMessage
|
|
57
|
+
| RecordsQueryMessage
|
|
58
|
+
| RecordsSubscribeMessage;
|
|
59
|
+
|
|
60
|
+
type ControlFilterInput<T extends RecordsWriteMessage> = {
|
|
61
|
+
tenant: string;
|
|
62
|
+
incomingMessage: ControlFilterMessage;
|
|
63
|
+
requester: string | undefined;
|
|
64
|
+
recordsWriteMessages: T[];
|
|
65
|
+
validationStateReader: ValidationStateReader;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
type ControlRecordVisibilityInput<T extends RecordsWriteMessage> = Omit<ControlFilterInput<T>, 'recordsWriteMessages'> & {
|
|
69
|
+
recordsWriteMessage: T;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export class EncryptionControl {
|
|
73
|
+
public static isControlMessage(message: RecordsWriteMessage): boolean {
|
|
74
|
+
return isEncryptionControlPath(message.descriptor.protocolPath);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public static isAudienceControlMessage(message: RecordsWriteMessage): boolean {
|
|
78
|
+
return message.descriptor.protocolPath === ENCRYPTION_CONTROL_AUDIENCE_PATH;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public static isExactAudienceFilter(filter: RecordsFilter): boolean {
|
|
82
|
+
return EncryptionControl.getExactAudienceFilterTuple(filter) !== undefined;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public static filterTargetsOnlyControlRecords(filter: RecordsFilter): boolean {
|
|
86
|
+
return typeof filter.protocolPath === 'string' && isEncryptionControlPath(filter.protocolPath);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public static async authorizeControlReadRequest(input: {
|
|
90
|
+
tenant: string;
|
|
91
|
+
incomingMessage: ControlReadMessage;
|
|
92
|
+
requester: string | undefined;
|
|
93
|
+
validationStateReader: ValidationStateReader;
|
|
94
|
+
}): Promise<void> {
|
|
95
|
+
if (input.requester === undefined || input.requester === input.tenant) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await EncryptionControl.getInvokedReadGrants({
|
|
100
|
+
actorDid : input.requester,
|
|
101
|
+
incomingMessage : input.incomingMessage,
|
|
102
|
+
tenant : input.tenant,
|
|
103
|
+
validationStateReader : input.validationStateReader,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
public static buildAudienceRecordFilters(filters: Filter[]): Filter[] {
|
|
108
|
+
const audienceFilters: Filter[] = [];
|
|
109
|
+
for (const filter of filters) {
|
|
110
|
+
const protocolPath = filter.protocolPath;
|
|
111
|
+
if (protocolPath === undefined) {
|
|
112
|
+
audienceFilters.push({ ...filter, protocolPath: ENCRYPTION_CONTROL_AUDIENCE_PATH });
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (typeof protocolPath === 'string') {
|
|
117
|
+
if (protocolPath === ENCRYPTION_CONTROL_AUDIENCE_PATH) {
|
|
118
|
+
audienceFilters.push(filter);
|
|
119
|
+
}
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (Array.isArray(protocolPath) && protocolPath.includes(ENCRYPTION_CONTROL_AUDIENCE_PATH)) {
|
|
124
|
+
audienceFilters.push({ ...filter, protocolPath: ENCRYPTION_CONTROL_AUDIENCE_PATH });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return audienceFilters;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Resolves the current audience record for a role-audience tuple.
|
|
133
|
+
*
|
|
134
|
+
* Current-key projection orders stored audience records by:
|
|
135
|
+
* tenant-authored actual signer first, then oldest `dateCreated`, then `recordId` ascending.
|
|
136
|
+
*/
|
|
137
|
+
public static async resolveCurrentAudienceRecord(input: {
|
|
138
|
+
messageStore: MessageStore;
|
|
139
|
+
tenant: string;
|
|
140
|
+
protocol: string;
|
|
141
|
+
rolePath: string;
|
|
142
|
+
contextId: string;
|
|
143
|
+
}): Promise<RecordsWriteMessage | undefined> {
|
|
144
|
+
const records = await EncryptionControl.queryStoredAudienceRecordsForTuple(input);
|
|
145
|
+
const currentRecordIds = selectOccupantRecordIds({
|
|
146
|
+
records,
|
|
147
|
+
max : 1,
|
|
148
|
+
getScopeKey : (record): string => EncryptionControl.getAudienceTupleKey(EncryptionControl.getRoleAudienceKeyId(record, 'audience')),
|
|
149
|
+
compareRecords : (left, right): number => EncryptionControl.compareAudienceProjectionCandidates(input.tenant, left, right),
|
|
150
|
+
});
|
|
151
|
+
const currentRecordId = currentRecordIds.values().next().value as string | undefined;
|
|
152
|
+
return records.find((record): boolean => record.recordId === currentRecordId);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
public static async projectCurrentAudienceRecords<T extends RecordsWriteMessage>(input: {
|
|
156
|
+
messageStore: MessageStore;
|
|
157
|
+
tenant: string;
|
|
158
|
+
recordsWriteMessages: T[];
|
|
159
|
+
bypassFilters?: Filter[];
|
|
160
|
+
currentAudienceRecordIdCache?: Map<string, string | undefined>;
|
|
161
|
+
}): Promise<T[]> {
|
|
162
|
+
const currentRecordIds = new Set<string>();
|
|
163
|
+
const tupleKeys = new Set<string>();
|
|
164
|
+
|
|
165
|
+
for (const record of input.recordsWriteMessages) {
|
|
166
|
+
if (!EncryptionControl.shouldProjectAudienceRecord(record, input.bypassFilters ?? [])) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const tags = EncryptionControl.getRoleAudienceKeyId(record, 'audience');
|
|
171
|
+
tupleKeys.add(EncryptionControl.getAudienceTupleKey(tags));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
for (const tupleKey of tupleKeys) {
|
|
175
|
+
const tuple = EncryptionControl.parseAudienceTupleKey(tupleKey);
|
|
176
|
+
const currentRecordId = await EncryptionControl.resolveCurrentAudienceRecordId({
|
|
177
|
+
...tuple,
|
|
178
|
+
currentAudienceRecordIdCache : input.currentAudienceRecordIdCache,
|
|
179
|
+
messageStore : input.messageStore,
|
|
180
|
+
tenant : input.tenant,
|
|
181
|
+
});
|
|
182
|
+
if (currentRecordId !== undefined) {
|
|
183
|
+
currentRecordIds.add(currentRecordId);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return input.recordsWriteMessages.filter((record): boolean =>
|
|
188
|
+
!EncryptionControl.shouldProjectAudienceRecord(record, input.bypassFilters ?? []) ||
|
|
189
|
+
currentRecordIds.has(record.recordId)
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
public static async projectCurrentAudienceRecordPage<T extends RecordsWriteMessage>(input: {
|
|
194
|
+
messageStore: MessageStore;
|
|
195
|
+
tenant: string;
|
|
196
|
+
filters: Filter[];
|
|
197
|
+
result: { messages: T[], cursor?: PaginationCursor };
|
|
198
|
+
currentAudienceRecordIdCache?: Map<string, string | undefined>;
|
|
199
|
+
}): Promise<{ messages: T[], cursor?: PaginationCursor }> {
|
|
200
|
+
return {
|
|
201
|
+
messages: await EncryptionControl.projectCurrentAudienceRecords({
|
|
202
|
+
currentAudienceRecordIdCache : input.currentAudienceRecordIdCache,
|
|
203
|
+
messageStore : input.messageStore,
|
|
204
|
+
tenant : input.tenant,
|
|
205
|
+
recordsWriteMessages : input.result.messages,
|
|
206
|
+
bypassFilters : input.filters,
|
|
207
|
+
}),
|
|
208
|
+
cursor: input.result.cursor,
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
public static async filterVisibleControlRecords<T extends RecordsWriteMessage>(input: ControlFilterInput<T>): Promise<T[]> {
|
|
213
|
+
const visibleRecordsWrites: T[] = [];
|
|
214
|
+
for (const recordsWrite of input.recordsWriteMessages) {
|
|
215
|
+
const visible = await EncryptionControl.isVisibleControlRecord({
|
|
216
|
+
incomingMessage : input.incomingMessage,
|
|
217
|
+
recordsWriteMessage : recordsWrite,
|
|
218
|
+
requester : input.requester,
|
|
219
|
+
tenant : input.tenant,
|
|
220
|
+
validationStateReader : input.validationStateReader,
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
if (visible) {
|
|
224
|
+
visibleRecordsWrites.push(recordsWrite);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return visibleRecordsWrites;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
public static async authorizeRead(input: {
|
|
232
|
+
tenant: string;
|
|
233
|
+
incomingMessage: ControlReadMessage;
|
|
234
|
+
requester: string | undefined;
|
|
235
|
+
recordsWriteMessage: RecordsWriteMessage;
|
|
236
|
+
validationStateReader: ValidationStateReader;
|
|
237
|
+
}): Promise<void> {
|
|
238
|
+
if (await EncryptionControl.canRead(input)) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
throw new DwnError(
|
|
243
|
+
DwnErrorCode.EncryptionControlReadUnauthorized,
|
|
244
|
+
'requester is not authorized to read the encryption control record.'
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
public static async canRead(input: {
|
|
249
|
+
tenant: string;
|
|
250
|
+
incomingMessage: ControlReadMessage;
|
|
251
|
+
requester: string | undefined;
|
|
252
|
+
recordsWriteMessage: RecordsWriteMessage;
|
|
253
|
+
validationStateReader: ValidationStateReader;
|
|
254
|
+
}): Promise<boolean> {
|
|
255
|
+
const { requester, recordsWriteMessage, tenant } = input;
|
|
256
|
+
if (!EncryptionControl.isControlMessage(recordsWriteMessage)) {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (requester === tenant) {
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (requester === undefined) {
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const recordType = EncryptionControl.getRecordType(recordsWriteMessage);
|
|
269
|
+
if (recordType === 'delivery') {
|
|
270
|
+
if (requester === recordsWriteMessage.descriptor.recipient ||
|
|
271
|
+
requester === Message.getAuthor(recordsWriteMessage)) {
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return EncryptionControl.canReadDeliveryByDelegatedGrant({
|
|
276
|
+
tenant,
|
|
277
|
+
incomingMessage : input.incomingMessage,
|
|
278
|
+
requester,
|
|
279
|
+
recordsWriteMessage,
|
|
280
|
+
tags : EncryptionControl.getDeliveryTags(recordsWriteMessage),
|
|
281
|
+
validationStateReader : input.validationStateReader,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const tags = EncryptionControl.getRoleAudienceKeyId(recordsWriteMessage, 'audience');
|
|
286
|
+
if (EncryptionControl.exactAudienceRequestMatchesRecord(input.incomingMessage, tags, recordsWriteMessage.recordId)) {
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return EncryptionControl.canEnumerateAudience({
|
|
291
|
+
tenant,
|
|
292
|
+
actorDid : requester,
|
|
293
|
+
incomingMessage : input.incomingMessage,
|
|
294
|
+
tags,
|
|
295
|
+
validationStateReader : input.validationStateReader,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
public static async authorizeWrite(
|
|
300
|
+
tenant: string,
|
|
301
|
+
recordsWrite: RecordsWrite,
|
|
302
|
+
validationStateReader: ValidationStateReader,
|
|
303
|
+
): Promise<void> {
|
|
304
|
+
const recordType = EncryptionControl.getRecordType(recordsWrite.message);
|
|
305
|
+
const tags = EncryptionControl.getRoleAudienceKeyId(recordsWrite.message, recordType);
|
|
306
|
+
const { protocolDefinition, ruleSet } = await EncryptionControl.verifyRoleAudienceDefinition(
|
|
307
|
+
tenant, tags, recordsWrite.message, validationStateReader, recordType
|
|
308
|
+
);
|
|
309
|
+
const actor = await EncryptionControl.resolveEffectiveControlActor(tenant, recordsWrite, validationStateReader);
|
|
310
|
+
|
|
311
|
+
await EncryptionControl.verifyActorCanCreateRole({
|
|
312
|
+
tenant,
|
|
313
|
+
actor,
|
|
314
|
+
tags,
|
|
315
|
+
protocolDefinition,
|
|
316
|
+
ruleSet,
|
|
317
|
+
validationStateReader,
|
|
318
|
+
message: recordsWrite.message,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
public static async validateReferentialIntegrity(
|
|
323
|
+
tenant: string,
|
|
324
|
+
recordsWrite: RecordsWrite,
|
|
325
|
+
validationStateReader: ValidationStateReader,
|
|
326
|
+
): Promise<void> {
|
|
327
|
+
if (!await recordsWrite.isInitialWrite()) {
|
|
328
|
+
throw new DwnError(
|
|
329
|
+
DwnErrorCode.EncryptionControlValidateUnexpectedRecord,
|
|
330
|
+
'encryption control records are immutable.'
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
EncryptionControl.verifyInlineControlRecord(recordsWrite.message);
|
|
335
|
+
|
|
336
|
+
const recordType = EncryptionControl.getRecordType(recordsWrite.message);
|
|
337
|
+
if (recordType === 'audience') {
|
|
338
|
+
const tags = EncryptionControl.getRoleAudienceKeyId(recordsWrite.message, 'audience');
|
|
339
|
+
await EncryptionControl.verifyRoleAudienceDefinition(tenant, tags, recordsWrite.message, validationStateReader, 'audience');
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const tags = EncryptionControl.getDeliveryTags(recordsWrite.message);
|
|
344
|
+
await EncryptionControl.verifyRoleAudienceDefinition(tenant, tags, recordsWrite.message, validationStateReader, 'delivery');
|
|
345
|
+
const audiences = await validationStateReader.queryAudienceRecords({
|
|
346
|
+
tenant,
|
|
347
|
+
protocol : tags.protocol,
|
|
348
|
+
rolePath : tags.rolePath,
|
|
349
|
+
contextId : tags.contextId,
|
|
350
|
+
keyId : tags.keyId,
|
|
351
|
+
});
|
|
352
|
+
if (audiences.length === 0) {
|
|
353
|
+
throw new DwnError(
|
|
354
|
+
DwnErrorCode.EncryptionControlValidateDeliveryAudienceMissing,
|
|
355
|
+
'delivery control record references a missing audience.'
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
public static async preProcessWrite(
|
|
361
|
+
tenant: string,
|
|
362
|
+
message: RecordsWriteMessage,
|
|
363
|
+
validationStateReader: ValidationStateReader,
|
|
364
|
+
): Promise<void> {
|
|
365
|
+
EncryptionControl.verifyInlineControlRecord(message);
|
|
366
|
+
|
|
367
|
+
switch (message.descriptor.protocolPath) {
|
|
368
|
+
case ENCRYPTION_CONTROL_AUDIENCE_PATH:
|
|
369
|
+
await EncryptionControl.preProcessAudience(tenant, message, validationStateReader);
|
|
370
|
+
return;
|
|
371
|
+
case ENCRYPTION_CONTROL_DELIVERY_PATH:
|
|
372
|
+
await EncryptionControl.preProcessDelivery(tenant, message, validationStateReader);
|
|
373
|
+
return;
|
|
374
|
+
default:
|
|
375
|
+
throw new DwnError(
|
|
376
|
+
DwnErrorCode.EncryptionControlValidateUnexpectedRecord,
|
|
377
|
+
`unexpected encryption control path '${message.descriptor.protocolPath}'.`
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
public static async validateRecord(input: {
|
|
383
|
+
tenant: string;
|
|
384
|
+
message: RecordsWriteMessage;
|
|
385
|
+
dataBytes: Uint8Array;
|
|
386
|
+
validationStateReader: ValidationStateReader;
|
|
387
|
+
}): Promise<void> {
|
|
388
|
+
const { tenant, message, dataBytes, validationStateReader } = input;
|
|
389
|
+
|
|
390
|
+
switch (message.descriptor.protocolPath) {
|
|
391
|
+
case ENCRYPTION_CONTROL_AUDIENCE_PATH:
|
|
392
|
+
await EncryptionControl.validateAudience(tenant, message, dataBytes, validationStateReader);
|
|
393
|
+
return;
|
|
394
|
+
case ENCRYPTION_CONTROL_DELIVERY_PATH:
|
|
395
|
+
EncryptionControl.verifyEncryptedDelivery(message);
|
|
396
|
+
return;
|
|
397
|
+
default:
|
|
398
|
+
throw new DwnError(
|
|
399
|
+
DwnErrorCode.EncryptionControlValidateUnexpectedRecord,
|
|
400
|
+
`unexpected encryption control path '${message.descriptor.protocolPath}'.`
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
public static mapErrorToStatusCode(errorCode: string): number | undefined {
|
|
406
|
+
if (errorCode.startsWith('EncryptionControlValidate')) {
|
|
407
|
+
return 400;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return undefined;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
private static async preProcessAudience(
|
|
414
|
+
tenant: string,
|
|
415
|
+
message: RecordsWriteMessage,
|
|
416
|
+
validationStateReader: ValidationStateReader,
|
|
417
|
+
): Promise<void> {
|
|
418
|
+
if (message.encryption !== undefined) {
|
|
419
|
+
throw new DwnError(
|
|
420
|
+
DwnErrorCode.EncryptionControlValidateUnexpectedRecord,
|
|
421
|
+
'audience control records must be plaintext.'
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const tags = EncryptionControl.getRoleAudienceKeyId(message, 'audience');
|
|
426
|
+
await EncryptionControl.verifyRoleAudienceDefinition(tenant, tags, message, validationStateReader, 'audience');
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
private static async preProcessDelivery(
|
|
430
|
+
tenant: string,
|
|
431
|
+
message: RecordsWriteMessage,
|
|
432
|
+
validationStateReader: ValidationStateReader,
|
|
433
|
+
): Promise<void> {
|
|
434
|
+
EncryptionControl.verifyEncryptedDelivery(message);
|
|
435
|
+
|
|
436
|
+
if (message.descriptor.recipient === undefined) {
|
|
437
|
+
throw new DwnError(
|
|
438
|
+
DwnErrorCode.EncryptionControlValidateDeliveryRecipientMissing,
|
|
439
|
+
'delivery control records must have a recipient.'
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const tags = EncryptionControl.getDeliveryTags(message);
|
|
444
|
+
await EncryptionControl.verifyRoleAudienceDefinition(tenant, tags, message, validationStateReader, 'delivery');
|
|
445
|
+
const audiences = await validationStateReader.queryAudienceRecords({
|
|
446
|
+
tenant,
|
|
447
|
+
protocol : tags.protocol,
|
|
448
|
+
rolePath : tags.rolePath,
|
|
449
|
+
contextId : tags.contextId,
|
|
450
|
+
keyId : tags.keyId,
|
|
451
|
+
});
|
|
452
|
+
if (audiences.length === 0) {
|
|
453
|
+
throw new DwnError(
|
|
454
|
+
DwnErrorCode.EncryptionControlValidateDeliveryAudienceMissing,
|
|
455
|
+
'delivery control record references a missing audience.'
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
await EncryptionControl.verifyDeliveryRecipientAuthority({
|
|
460
|
+
tenant,
|
|
461
|
+
message,
|
|
462
|
+
tags,
|
|
463
|
+
validationStateReader,
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
private static async validateAudience(
|
|
468
|
+
tenant: string,
|
|
469
|
+
message: RecordsWriteMessage,
|
|
470
|
+
dataBytes: Uint8Array,
|
|
471
|
+
validationStateReader: ValidationStateReader,
|
|
472
|
+
): Promise<void> {
|
|
473
|
+
if (message.encryption !== undefined) {
|
|
474
|
+
throw new DwnError(
|
|
475
|
+
DwnErrorCode.EncryptionControlValidateUnexpectedRecord,
|
|
476
|
+
'audience control records must be plaintext.'
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
const dataObject = JSON.parse(Encoder.bytesToString(dataBytes)) as EncryptionControlAudiencePayload;
|
|
481
|
+
validateJsonSchema('Audience', dataObject);
|
|
482
|
+
|
|
483
|
+
const tags = EncryptionControl.getRoleAudienceKeyId(message, 'audience');
|
|
484
|
+
EncryptionControl.verifyAudiencePayloadMatchesTags(dataObject, tags);
|
|
485
|
+
|
|
486
|
+
const audienceKeyId = await Encryption.getKeyId(dataObject.publicKeyJwk);
|
|
487
|
+
if (audienceKeyId !== dataObject.keyId) {
|
|
488
|
+
throw new DwnError(
|
|
489
|
+
DwnErrorCode.EncryptionControlValidateAudienceKeyIdMismatch,
|
|
490
|
+
'audience keyId must match publicKeyJwk thumbprint.'
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const { ruleSet } = await EncryptionControl.verifyRoleAudienceDefinition(tenant, tags, message, validationStateReader, 'audience');
|
|
495
|
+
const sealingKeyId = await Encryption.getKeyId(ruleSet.$keyAgreement!.publicKeyJwk);
|
|
496
|
+
if (Object.is(sealingKeyId, dataObject.sealedPrivateKey.keyId)) {
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
throw new DwnError(
|
|
501
|
+
DwnErrorCode.EncryptionControlValidateAudienceSealKeyIdMismatch,
|
|
502
|
+
'audience seal keyId must match the role path $keyAgreement public key thumbprint.'
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
private static verifyEncryptedDelivery(message: RecordsWriteMessage): void {
|
|
507
|
+
if (message.encryption === undefined) {
|
|
508
|
+
throw new DwnError(
|
|
509
|
+
DwnErrorCode.EncryptionControlValidateUnexpectedRecord,
|
|
510
|
+
'delivery control records must be encrypted.'
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
private static verifyInlineControlRecord(message: RecordsWriteMessage): void {
|
|
516
|
+
if (message.descriptor.published === true) {
|
|
517
|
+
throw new DwnError(
|
|
518
|
+
DwnErrorCode.EncryptionControlValidateUnexpectedRecord,
|
|
519
|
+
'encryption control records must not be published.'
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (message.descriptor.dataSize > DwnConstant.maxDataSizeAllowedToBeEncoded) {
|
|
524
|
+
throw new DwnError(
|
|
525
|
+
DwnErrorCode.EncryptionControlValidateUnexpectedRecord,
|
|
526
|
+
'encryption control records must be small enough for inline validation.'
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
private static verifyAudiencePayloadMatchesTags(payload: RoleAudienceKeyId, tags: RoleAudienceKeyId): void {
|
|
532
|
+
if (payload.protocol !== tags.protocol ||
|
|
533
|
+
payload.rolePath !== tags.rolePath ||
|
|
534
|
+
payload.contextId !== tags.contextId ||
|
|
535
|
+
payload.keyId !== tags.keyId) {
|
|
536
|
+
throw new DwnError(
|
|
537
|
+
DwnErrorCode.EncryptionControlValidateAudienceTagsMismatch,
|
|
538
|
+
'audience tags must match payload fields.'
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private static async verifyRoleAudienceDefinition(
|
|
544
|
+
tenant: string,
|
|
545
|
+
tags: RoleAudienceKeyId,
|
|
546
|
+
message: RecordsWriteMessage,
|
|
547
|
+
validationStateReader: ValidationStateReader,
|
|
548
|
+
recordType: 'audience' | 'delivery',
|
|
549
|
+
): Promise<RoleAudienceDefinition> {
|
|
550
|
+
if (tags.protocol !== message.descriptor.protocol) {
|
|
551
|
+
const errorCode = recordType === 'audience'
|
|
552
|
+
? DwnErrorCode.EncryptionControlValidateAudienceTagsMismatch
|
|
553
|
+
: DwnErrorCode.EncryptionControlValidateDeliveryTagsMismatch;
|
|
554
|
+
throw new DwnError(
|
|
555
|
+
errorCode,
|
|
556
|
+
'control record protocol tag must match descriptor protocol.'
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return EncryptionControl.resolveRoleAudienceDefinition({
|
|
561
|
+
tenant,
|
|
562
|
+
tags,
|
|
563
|
+
messageTimestamp: message.descriptor.messageTimestamp,
|
|
564
|
+
validationStateReader,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
private static verifyRoleAudienceContext(rolePath: string, contextId: string): void {
|
|
569
|
+
const parentDepth = rolePath.split('/').length - 1;
|
|
570
|
+
if ((parentDepth === 0 && contextId !== '') || (parentDepth > 0 && contextId === '')) {
|
|
571
|
+
throw new DwnError(
|
|
572
|
+
DwnErrorCode.EncryptionControlValidateAudienceContextIdInvalid,
|
|
573
|
+
'role audience contextId is inconsistent with role path depth.'
|
|
574
|
+
);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private static async resolveEffectiveControlActor(
|
|
579
|
+
tenant: string,
|
|
580
|
+
recordsWrite: RecordsWrite,
|
|
581
|
+
validationStateReader: ValidationStateReader,
|
|
582
|
+
): Promise<EffectiveControlActor> {
|
|
583
|
+
if (recordsWrite.isSignedByAuthorDelegate) {
|
|
584
|
+
const grant = PermissionGrant.parse(recordsWrite.message.authorization.authorDelegatedGrant!);
|
|
585
|
+
await GrantAuthorization.performBaseValidation({
|
|
586
|
+
incomingMessage : recordsWrite.message,
|
|
587
|
+
expectedGrantor : recordsWrite.author!,
|
|
588
|
+
expectedGrantee : recordsWrite.signer!,
|
|
589
|
+
permissionGrant : grant,
|
|
590
|
+
validationStateReader,
|
|
591
|
+
});
|
|
592
|
+
EncryptionControl.verifyGrantConditions(recordsWrite.message, grant);
|
|
593
|
+
return { did: recordsWrite.signer, grant };
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (recordsWrite.isSignedByOwnerDelegate) {
|
|
597
|
+
const grant = PermissionGrant.parse(recordsWrite.message.authorization.ownerDelegatedGrant!);
|
|
598
|
+
await GrantAuthorization.performBaseValidation({
|
|
599
|
+
incomingMessage : recordsWrite.message,
|
|
600
|
+
expectedGrantor : recordsWrite.owner!,
|
|
601
|
+
expectedGrantee : recordsWrite.ownerSignatureSigner!,
|
|
602
|
+
permissionGrant : grant,
|
|
603
|
+
validationStateReader,
|
|
604
|
+
});
|
|
605
|
+
EncryptionControl.verifyGrantConditions(recordsWrite.message, grant);
|
|
606
|
+
return { did: recordsWrite.ownerSignatureSigner, grant };
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (recordsWrite.owner !== undefined) {
|
|
610
|
+
return { did: recordsWrite.owner };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
const directGrantId = recordsWrite.signaturePayload === undefined
|
|
614
|
+
? undefined
|
|
615
|
+
: Message.getPermissionGrantId(recordsWrite.signaturePayload);
|
|
616
|
+
if (directGrantId === undefined) {
|
|
617
|
+
return { did: recordsWrite.author };
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
const grant = await validationStateReader.fetchGrant(tenant, directGrantId);
|
|
621
|
+
await GrantAuthorization.performBaseValidation({
|
|
622
|
+
incomingMessage : recordsWrite.message,
|
|
623
|
+
expectedGrantor : tenant,
|
|
624
|
+
expectedGrantee : recordsWrite.author!,
|
|
625
|
+
permissionGrant : grant,
|
|
626
|
+
validationStateReader,
|
|
627
|
+
});
|
|
628
|
+
EncryptionControl.verifyGrantConditions(recordsWrite.message, grant);
|
|
629
|
+
return { did: recordsWrite.author, grant };
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
private static async verifyActorCanCreateRole(input: {
|
|
633
|
+
tenant: string;
|
|
634
|
+
actor: EffectiveControlActor;
|
|
635
|
+
tags: RoleAudienceKeyId;
|
|
636
|
+
protocolDefinition: ProtocolDefinition;
|
|
637
|
+
ruleSet: ProtocolRuleSet;
|
|
638
|
+
validationStateReader: ValidationStateReader;
|
|
639
|
+
message: RecordsWriteMessage;
|
|
640
|
+
}): Promise<void> {
|
|
641
|
+
const { tenant, actor, tags, protocolDefinition, ruleSet, validationStateReader, message } = input;
|
|
642
|
+
const signaturePayload = Jws.decodePlainObjectPayload(message.authorization.signature);
|
|
643
|
+
if (await EncryptionControl.actorCanCreateRole({
|
|
644
|
+
tenant,
|
|
645
|
+
actor,
|
|
646
|
+
tags,
|
|
647
|
+
protocolDefinition,
|
|
648
|
+
ruleSet,
|
|
649
|
+
validationStateReader,
|
|
650
|
+
invokedRole: signaturePayload.protocolRole,
|
|
651
|
+
})) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
throw new DwnError(
|
|
656
|
+
DwnErrorCode.EncryptionControlValidateAudienceWriterUnauthorized,
|
|
657
|
+
'control records must be written by a DID authorized to create the referenced role.'
|
|
658
|
+
);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
private static async actorCanCreateRole(input: {
|
|
662
|
+
tenant: string;
|
|
663
|
+
actor: EffectiveControlActor;
|
|
664
|
+
tags: RoleAudienceKeyId;
|
|
665
|
+
protocolDefinition: ProtocolDefinition;
|
|
666
|
+
ruleSet: ProtocolRuleSet;
|
|
667
|
+
validationStateReader: ValidationStateReader;
|
|
668
|
+
invokedRole: string | undefined;
|
|
669
|
+
}): Promise<boolean> {
|
|
670
|
+
const { tenant, actor, tags, protocolDefinition, ruleSet, validationStateReader, invokedRole } = input;
|
|
671
|
+
if (actor.did === tenant) {
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
if (actor.did === undefined) {
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (actor.grant !== undefined && EncryptionControl.grantCoversRoleCreate(actor.grant, tags)) {
|
|
680
|
+
return true;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
const recordChain = await EncryptionControl.constructRoleParentChain(tenant, tags, validationStateReader);
|
|
684
|
+
for (const actionRule of ruleSet.$actions ?? []) {
|
|
685
|
+
if (!actionRule.can.includes(ProtocolAction.Create)) {
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
if (actionRule.who === ProtocolActor.Anyone) {
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (await EncryptionControl.roleRuleAllowsActor(
|
|
694
|
+
tenant,
|
|
695
|
+
actor.did,
|
|
696
|
+
actionRule,
|
|
697
|
+
tags,
|
|
698
|
+
protocolDefinition,
|
|
699
|
+
validationStateReader,
|
|
700
|
+
invokedRole,
|
|
701
|
+
)) {
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
if (await checkActor(actor.did, actionRule, recordChain, protocolDefinition)) {
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return false;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
private static async verifyDeliveryRecipientAuthority(input: {
|
|
714
|
+
tenant: string;
|
|
715
|
+
message: RecordsWriteMessage;
|
|
716
|
+
tags: EncryptionControlDeliveryTags;
|
|
717
|
+
validationStateReader: ValidationStateReader;
|
|
718
|
+
}): Promise<void> {
|
|
719
|
+
const { tags } = input;
|
|
720
|
+
|
|
721
|
+
if (tags.recipientAuthority !== EncryptionControlDeliveryRecipientAuthority.RoleHolder) {
|
|
722
|
+
throw new DwnError(
|
|
723
|
+
DwnErrorCode.EncryptionControlValidateDeliveryRecipientAuthorityInvalid,
|
|
724
|
+
`unsupported delivery recipient authority '${tags.recipientAuthority}'.`
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (!await EncryptionControl.deliveryRecipientIsRoleHolder(input)) {
|
|
729
|
+
throw new DwnError(
|
|
730
|
+
DwnErrorCode.EncryptionControlValidateDeliveryRecipientRoleRecordMissing,
|
|
731
|
+
'delivery recipient role record is missing.'
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
private static async deliveryRecipientIsRoleHolder(input: {
|
|
737
|
+
tenant: string;
|
|
738
|
+
message: RecordsWriteMessage;
|
|
739
|
+
tags: EncryptionControlDeliveryTags;
|
|
740
|
+
validationStateReader: ValidationStateReader;
|
|
741
|
+
}): Promise<boolean> {
|
|
742
|
+
return EncryptionControl.hasRoleRecord(
|
|
743
|
+
input.tenant,
|
|
744
|
+
input.message.descriptor.recipient!,
|
|
745
|
+
input.tags.protocol,
|
|
746
|
+
input.tags.rolePath,
|
|
747
|
+
input.tags.contextId,
|
|
748
|
+
input.validationStateReader,
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
private static grantCoversRoleCreate(grant: PermissionGrant, tags: RoleAudienceKeyId): boolean {
|
|
753
|
+
const scope = grant.scope as RecordsPermissionScope;
|
|
754
|
+
if (scope.interface !== DwnInterfaceName.Records || scope.method !== DwnMethodName.Write) {
|
|
755
|
+
return false;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return PermissionScopeMatcher.matches(scope, {
|
|
759
|
+
protocol : tags.protocol,
|
|
760
|
+
protocolPath : tags.rolePath,
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
private static async canEnumerateAudience(input: {
|
|
765
|
+
tenant: string;
|
|
766
|
+
actorDid: string;
|
|
767
|
+
incomingMessage: ControlReadMessage;
|
|
768
|
+
tags: RoleAudienceKeyId;
|
|
769
|
+
validationStateReader: ValidationStateReader;
|
|
770
|
+
}): Promise<boolean> {
|
|
771
|
+
const {
|
|
772
|
+
tenant, actorDid, incomingMessage, tags, validationStateReader
|
|
773
|
+
} = input;
|
|
774
|
+
const grants = await EncryptionControl.getInvokedReadGrants({
|
|
775
|
+
tenant,
|
|
776
|
+
actorDid,
|
|
777
|
+
incomingMessage,
|
|
778
|
+
validationStateReader,
|
|
779
|
+
});
|
|
780
|
+
if (grants.some(grant => EncryptionControl.grantCoversAudienceEnumeration(grant, tags))) {
|
|
781
|
+
return true;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const { protocolDefinition, ruleSet } = await EncryptionControl.resolveRoleAudienceDefinition({
|
|
785
|
+
tenant,
|
|
786
|
+
tags,
|
|
787
|
+
messageTimestamp: incomingMessage.descriptor.messageTimestamp,
|
|
788
|
+
validationStateReader,
|
|
789
|
+
});
|
|
790
|
+
const invokedRole = incomingMessage.authorization === undefined
|
|
791
|
+
? undefined
|
|
792
|
+
: Jws.decodePlainObjectPayload(incomingMessage.authorization.signature).protocolRole;
|
|
793
|
+
|
|
794
|
+
return EncryptionControl.actorCanCreateRole({
|
|
795
|
+
tenant,
|
|
796
|
+
actor: { did: actorDid },
|
|
797
|
+
tags,
|
|
798
|
+
protocolDefinition,
|
|
799
|
+
ruleSet,
|
|
800
|
+
validationStateReader,
|
|
801
|
+
invokedRole,
|
|
802
|
+
});
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
private static async canReadDeliveryByDelegatedGrant(input: {
|
|
806
|
+
tenant: string;
|
|
807
|
+
incomingMessage: ControlReadMessage;
|
|
808
|
+
requester: string;
|
|
809
|
+
recordsWriteMessage: RecordsWriteMessage;
|
|
810
|
+
tags: EncryptionControlDeliveryTags;
|
|
811
|
+
validationStateReader: ValidationStateReader;
|
|
812
|
+
}): Promise<boolean> {
|
|
813
|
+
const { tenant, incomingMessage, requester, recordsWriteMessage, tags, validationStateReader } = input;
|
|
814
|
+
const grants = await EncryptionControl.getInvokedReadGrants({
|
|
815
|
+
tenant,
|
|
816
|
+
actorDid: requester,
|
|
817
|
+
incomingMessage,
|
|
818
|
+
validationStateReader,
|
|
819
|
+
});
|
|
820
|
+
const recipient = recordsWriteMessage.descriptor.recipient;
|
|
821
|
+
let protocolDefinition: ProtocolDefinition | undefined;
|
|
822
|
+
|
|
823
|
+
for (const grant of grants) {
|
|
824
|
+
if (recipient !== grant.grantor && recipient !== grant.grantee) {
|
|
825
|
+
continue;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (!isGrantKeyEligibleRecordsScope(grant.scope) || grant.scope.method !== DwnMethodName.Read) {
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const deliveredScope = {
|
|
833
|
+
protocol : tags.protocol,
|
|
834
|
+
protocolPath : tags.rolePath,
|
|
835
|
+
};
|
|
836
|
+
|
|
837
|
+
if (grantKeyScopeCoversDeliveredScope({
|
|
838
|
+
grantScope: grant.scope,
|
|
839
|
+
deliveredScope,
|
|
840
|
+
})) {
|
|
841
|
+
return true;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
protocolDefinition ??= await validationStateReader.fetchProtocolDefinition(
|
|
845
|
+
tenant,
|
|
846
|
+
tags.protocol,
|
|
847
|
+
incomingMessage.descriptor.messageTimestamp,
|
|
848
|
+
);
|
|
849
|
+
if (grantKeyScopeCoversDeliveredScope({
|
|
850
|
+
grantScope: grant.scope,
|
|
851
|
+
deliveredScope,
|
|
852
|
+
protocolDefinition,
|
|
853
|
+
})) {
|
|
854
|
+
return true;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
return false;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
private static async getInvokedReadGrants(input: {
|
|
862
|
+
tenant: string;
|
|
863
|
+
actorDid: string;
|
|
864
|
+
incomingMessage: ControlReadMessage;
|
|
865
|
+
validationStateReader: ValidationStateReader;
|
|
866
|
+
}): Promise<PermissionGrant[]> {
|
|
867
|
+
if (Message.isSignedByAuthorDelegate(input.incomingMessage)) {
|
|
868
|
+
const grant = PermissionGrant.parse(input.incomingMessage.authorization!.authorDelegatedGrant!);
|
|
869
|
+
await GrantAuthorization.performBaseValidation({
|
|
870
|
+
incomingMessage : input.incomingMessage,
|
|
871
|
+
expectedGrantor : grant.grantor,
|
|
872
|
+
expectedGrantee : input.actorDid,
|
|
873
|
+
permissionGrant : grant,
|
|
874
|
+
validationStateReader : input.validationStateReader,
|
|
875
|
+
});
|
|
876
|
+
return [grant];
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
const { descriptor } = input.incomingMessage;
|
|
880
|
+
if (!('permissionGrantId' in descriptor)) {
|
|
881
|
+
return [];
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const grantId = descriptor.permissionGrantId;
|
|
885
|
+
if (grantId === undefined) {
|
|
886
|
+
return [];
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
const grant = await input.validationStateReader.fetchGrant(input.tenant, grantId);
|
|
890
|
+
await GrantAuthorization.performBaseValidation({
|
|
891
|
+
incomingMessage : input.incomingMessage,
|
|
892
|
+
expectedGrantor : input.tenant,
|
|
893
|
+
expectedGrantee : input.actorDid,
|
|
894
|
+
permissionGrant : grant,
|
|
895
|
+
validationStateReader : input.validationStateReader,
|
|
896
|
+
});
|
|
897
|
+
return [grant];
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
private static grantCoversAudienceEnumeration(grant: PermissionGrant, tags: RoleAudienceKeyId): boolean {
|
|
901
|
+
const scope = grant.scope as RecordsPermissionScope;
|
|
902
|
+
if (scope.interface !== DwnInterfaceName.Records || scope.method !== DwnMethodName.Read) {
|
|
903
|
+
return false;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
return PermissionScopeMatcher.matches(scope, {
|
|
907
|
+
protocol : tags.protocol,
|
|
908
|
+
protocolPath : tags.rolePath,
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
private static async resolveRoleAudienceDefinition(input: {
|
|
913
|
+
tenant: string;
|
|
914
|
+
tags: RoleAudienceKeyId;
|
|
915
|
+
messageTimestamp: string;
|
|
916
|
+
validationStateReader: ValidationStateReader;
|
|
917
|
+
}): Promise<RoleAudienceDefinition> {
|
|
918
|
+
const { tenant, tags, messageTimestamp, validationStateReader } = input;
|
|
919
|
+
EncryptionControl.verifyRoleAudienceContext(tags.rolePath, tags.contextId);
|
|
920
|
+
|
|
921
|
+
const protocolDefinition = await validationStateReader.fetchProtocolDefinition(
|
|
922
|
+
tenant,
|
|
923
|
+
tags.protocol,
|
|
924
|
+
messageTimestamp,
|
|
925
|
+
);
|
|
926
|
+
const ruleSet = getRuleSetAtPath(tags.rolePath, protocolDefinition.structure);
|
|
927
|
+
|
|
928
|
+
if (ruleSet?.$role !== true || ruleSet.$keyAgreement === undefined) {
|
|
929
|
+
throw new DwnError(
|
|
930
|
+
DwnErrorCode.EncryptionControlValidateAudienceRolePathInvalid,
|
|
931
|
+
`role audience path '${tags.rolePath}' must be a role path with $keyAgreement.`
|
|
932
|
+
);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
return { protocolDefinition, ruleSet };
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
private static verifyGrantConditions(message: RecordsWriteMessage, grant: PermissionGrant): void {
|
|
939
|
+
if (grant.conditions?.publication === PermissionConditionPublication.Required && message.descriptor.published !== true) {
|
|
940
|
+
throw new DwnError(
|
|
941
|
+
DwnErrorCode.RecordsGrantAuthorizationConditionPublicationRequired,
|
|
942
|
+
'Permission grant requires message to be published'
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (grant.conditions?.publication === PermissionConditionPublication.Prohibited && message.descriptor.published === true) {
|
|
947
|
+
throw new DwnError(
|
|
948
|
+
DwnErrorCode.RecordsGrantAuthorizationConditionPublicationProhibited,
|
|
949
|
+
'Permission grant prohibits message from being published'
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
private static async constructRoleParentChain(
|
|
955
|
+
tenant: string,
|
|
956
|
+
tags: RoleAudienceKeyId,
|
|
957
|
+
validationStateReader: ValidationStateReader,
|
|
958
|
+
): Promise<RecordsWriteMessage[]> {
|
|
959
|
+
if (tags.contextId === '') {
|
|
960
|
+
return [];
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
const parentRecordId = tags.contextId.split('/').pop();
|
|
964
|
+
if (parentRecordId === undefined || parentRecordId === '') {
|
|
965
|
+
return [];
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
return validationStateReader.constructRecordChain(tenant, parentRecordId);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
private static async roleRuleAllowsActor(
|
|
972
|
+
tenant: string,
|
|
973
|
+
actor: string,
|
|
974
|
+
actionRule: ProtocolActionRule,
|
|
975
|
+
tags: RoleAudienceKeyId,
|
|
976
|
+
protocolDefinition: ProtocolDefinition,
|
|
977
|
+
validationStateReader: ValidationStateReader,
|
|
978
|
+
invokedRole: string | undefined,
|
|
979
|
+
): Promise<boolean> {
|
|
980
|
+
if (actionRule.role === undefined || invokedRole !== actionRule.role) {
|
|
981
|
+
return false;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const resolved = EncryptionControl.resolveRoleReference(actionRule.role, tags.protocol, protocolDefinition);
|
|
985
|
+
if (resolved === undefined) {
|
|
986
|
+
return false;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
return EncryptionControl.hasRoleRecord(tenant, actor, resolved.protocol, resolved.protocolPath, tags.contextId, validationStateReader);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
private static async hasRoleRecord(
|
|
993
|
+
tenant: string,
|
|
994
|
+
actor: string,
|
|
995
|
+
protocol: string,
|
|
996
|
+
rolePath: string,
|
|
997
|
+
contextId: string,
|
|
998
|
+
validationStateReader: ValidationStateReader,
|
|
999
|
+
): Promise<boolean> {
|
|
1000
|
+
const contextIdPrefix = getRoleContextPrefix(rolePath, contextId);
|
|
1001
|
+
if (contextIdPrefix === undefined && rolePath.includes('/')) {
|
|
1002
|
+
return false;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
return validationStateReader.hasMatchingRoleRecord({
|
|
1006
|
+
tenant,
|
|
1007
|
+
protocol,
|
|
1008
|
+
protocolPath : rolePath,
|
|
1009
|
+
recipient : actor,
|
|
1010
|
+
contextIdPrefix,
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
private static resolveRoleReference(
|
|
1015
|
+
roleRef: string,
|
|
1016
|
+
currentProtocol: string,
|
|
1017
|
+
protocolDefinition: ProtocolDefinition,
|
|
1018
|
+
): { protocol: string; protocolPath: string } | undefined {
|
|
1019
|
+
if (!isCrossProtocolRef(roleRef)) {
|
|
1020
|
+
return { protocol: currentProtocol, protocolPath: roleRef };
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const parsed = parseCrossProtocolRef(roleRef);
|
|
1024
|
+
if (parsed === undefined) {
|
|
1025
|
+
return undefined;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const protocol = protocolDefinition.uses?.[parsed.alias];
|
|
1029
|
+
if (protocol === undefined) {
|
|
1030
|
+
return undefined;
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
return { protocol, protocolPath: parsed.protocolPath };
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
private static getRoleAudienceKeyId(message: RecordsWriteMessage, recordType: 'audience' | 'delivery'): RoleAudienceKeyId {
|
|
1037
|
+
return {
|
|
1038
|
+
protocol : EncryptionControl.getRequiredStringTag(message, 'protocol', recordType),
|
|
1039
|
+
rolePath : EncryptionControl.getRequiredStringTag(message, 'rolePath', recordType),
|
|
1040
|
+
contextId : EncryptionControl.getRequiredStringTag(message, 'contextId', recordType),
|
|
1041
|
+
keyId : EncryptionControl.getRequiredStringTag(message, 'keyId', recordType),
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
private static getDeliveryTags(message: RecordsWriteMessage): EncryptionControlDeliveryTags {
|
|
1046
|
+
const tags = EncryptionControl.getRoleAudienceKeyId(message, 'delivery');
|
|
1047
|
+
return {
|
|
1048
|
+
...tags,
|
|
1049
|
+
recipientAuthority: EncryptionControl.getRequiredStringTag(message, 'recipientAuthority', 'delivery') as EncryptionControlDeliveryTags['recipientAuthority'],
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
private static getRecordType(message: RecordsWriteMessage): 'audience' | 'delivery' {
|
|
1054
|
+
return message.descriptor.protocolPath === ENCRYPTION_CONTROL_DELIVERY_PATH ? 'delivery' : 'audience';
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
private static async isVisibleControlRecord<T extends RecordsWriteMessage>(input: ControlRecordVisibilityInput<T>): Promise<boolean> {
|
|
1058
|
+
if (!EncryptionControl.isControlMessage(input.recordsWriteMessage)) {
|
|
1059
|
+
return true;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
try {
|
|
1063
|
+
return await EncryptionControl.canRead({
|
|
1064
|
+
tenant : input.tenant,
|
|
1065
|
+
incomingMessage : input.incomingMessage,
|
|
1066
|
+
requester : input.requester,
|
|
1067
|
+
recordsWriteMessage : input.recordsWriteMessage,
|
|
1068
|
+
validationStateReader : input.validationStateReader,
|
|
1069
|
+
});
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
if (error instanceof DwnError) {
|
|
1072
|
+
return false;
|
|
1073
|
+
}
|
|
1074
|
+
throw error;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
private static exactAudienceFilterMatchesRecord(filter: RecordsFilter, tags: RoleAudienceKeyId, recordId: string): boolean {
|
|
1079
|
+
if (filter.recordId === recordId) {
|
|
1080
|
+
return true;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
const tuple = EncryptionControl.getExactAudienceFilterTuple(filter);
|
|
1084
|
+
if (tuple === undefined) {
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
return tuple.protocol === tags.protocol &&
|
|
1089
|
+
tuple.rolePath === tags.rolePath &&
|
|
1090
|
+
tuple.contextId === tags.contextId &&
|
|
1091
|
+
(tuple.keyId === undefined || tuple.keyId === tags.keyId);
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
private static getExactAudienceFilterTuple(filter: RecordsFilter): ExactAudienceFilterTuple | undefined {
|
|
1095
|
+
if (filter.protocol === undefined || filter.protocolPath !== ENCRYPTION_CONTROL_AUDIENCE_PATH) {
|
|
1096
|
+
return undefined;
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const protocol = EncryptionControl.getExactFilterTag(filter, 'protocol');
|
|
1100
|
+
const rolePath = EncryptionControl.getExactFilterTag(filter, 'rolePath');
|
|
1101
|
+
const contextId = EncryptionControl.getExactFilterTag(filter, 'contextId');
|
|
1102
|
+
if (protocol !== filter.protocol || rolePath === undefined || contextId === undefined) {
|
|
1103
|
+
return undefined;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
const keyId = EncryptionControl.getExactFilterTag(filter, 'keyId');
|
|
1107
|
+
if (keyId === undefined) {
|
|
1108
|
+
return { protocol, rolePath, contextId };
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
return { protocol, rolePath, contextId, keyId };
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
private static exactAudienceRequestMatchesRecord(
|
|
1115
|
+
incomingMessage: ControlReadMessage,
|
|
1116
|
+
tags: RoleAudienceKeyId,
|
|
1117
|
+
recordId: string,
|
|
1118
|
+
): boolean {
|
|
1119
|
+
const { descriptor } = incomingMessage;
|
|
1120
|
+
if ('filter' in descriptor) {
|
|
1121
|
+
return EncryptionControl.exactAudienceFilterMatchesRecord(descriptor.filter, tags, recordId);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
return false;
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
private static getExactFilterTag(filter: RecordsFilter, tag: string): string | undefined {
|
|
1128
|
+
const value = filter.tags?.[tag];
|
|
1129
|
+
return typeof value === 'string' ? value : undefined;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
private static async queryStoredAudienceRecordsForTuple(input: {
|
|
1133
|
+
messageStore: MessageStore;
|
|
1134
|
+
tenant: string;
|
|
1135
|
+
protocol: string;
|
|
1136
|
+
rolePath: string;
|
|
1137
|
+
contextId: string;
|
|
1138
|
+
}): Promise<RecordsWriteMessage[]> {
|
|
1139
|
+
const filter: Filter = {
|
|
1140
|
+
interface : DwnInterfaceName.Records,
|
|
1141
|
+
method : DwnMethodName.Write,
|
|
1142
|
+
isLatestBaseState : true,
|
|
1143
|
+
protocol : input.protocol,
|
|
1144
|
+
protocolPath : ENCRYPTION_CONTROL_AUDIENCE_PATH,
|
|
1145
|
+
'tag.protocol' : input.protocol,
|
|
1146
|
+
'tag.rolePath' : input.rolePath,
|
|
1147
|
+
'tag.contextId' : input.contextId,
|
|
1148
|
+
};
|
|
1149
|
+
const { messages } = await input.messageStore.query(input.tenant, [filter], { dateCreated: SortDirection.Ascending });
|
|
1150
|
+
return messages.filter(Records.isRecordsWrite);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
private static async resolveCurrentAudienceRecordId(input: {
|
|
1154
|
+
messageStore: MessageStore;
|
|
1155
|
+
tenant: string;
|
|
1156
|
+
protocol: string;
|
|
1157
|
+
rolePath: string;
|
|
1158
|
+
contextId: string;
|
|
1159
|
+
currentAudienceRecordIdCache?: Map<string, string | undefined>;
|
|
1160
|
+
}): Promise<string | undefined> {
|
|
1161
|
+
const tupleKey = EncryptionControl.getAudienceTupleKey(input);
|
|
1162
|
+
if (input.currentAudienceRecordIdCache?.has(tupleKey) === true) {
|
|
1163
|
+
return input.currentAudienceRecordIdCache.get(tupleKey);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
const current = await EncryptionControl.resolveCurrentAudienceRecord(input);
|
|
1167
|
+
const currentRecordId = current?.recordId;
|
|
1168
|
+
input.currentAudienceRecordIdCache?.set(tupleKey, currentRecordId);
|
|
1169
|
+
return currentRecordId;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
private static shouldProjectAudienceRecord(message: RecordsWriteMessage, bypassFilters: Filter[]): boolean {
|
|
1173
|
+
return EncryptionControl.isAudienceControlMessage(message) &&
|
|
1174
|
+
!bypassFilters.some((filter): boolean => EncryptionControl.audienceProjectionBypassFilterMatchesRecord(filter, message));
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
private static audienceProjectionBypassFilterMatchesRecord(filter: Filter, message: RecordsWriteMessage): boolean {
|
|
1178
|
+
if (filter.recordId === message.recordId) {
|
|
1179
|
+
return true;
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const tags = EncryptionControl.getRoleAudienceKeyId(message, 'audience');
|
|
1183
|
+
return filter.protocol === tags.protocol &&
|
|
1184
|
+
filter.protocolPath === ENCRYPTION_CONTROL_AUDIENCE_PATH &&
|
|
1185
|
+
filter['tag.protocol'] === tags.protocol &&
|
|
1186
|
+
filter['tag.rolePath'] === tags.rolePath &&
|
|
1187
|
+
filter['tag.contextId'] === tags.contextId &&
|
|
1188
|
+
filter['tag.keyId'] === tags.keyId;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
private static getAudienceTupleKey(input: AudienceTuple): string {
|
|
1192
|
+
return JSON.stringify([input.protocol, input.rolePath, input.contextId]);
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
private static parseAudienceTupleKey(key: string): AudienceTuple {
|
|
1196
|
+
const [protocol, rolePath, contextId] = JSON.parse(key) as string[];
|
|
1197
|
+
return { protocol, rolePath, contextId };
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
public static compareAudienceProjectionCandidates(
|
|
1201
|
+
tenant: string,
|
|
1202
|
+
left: RecordsWriteMessage,
|
|
1203
|
+
right: RecordsWriteMessage,
|
|
1204
|
+
): number {
|
|
1205
|
+
const leftIsTenantSigned = Message.getSigner(left) === tenant;
|
|
1206
|
+
const rightIsTenantSigned = Message.getSigner(right) === tenant;
|
|
1207
|
+
if (leftIsTenantSigned !== rightIsTenantSigned) {
|
|
1208
|
+
return leftIsTenantSigned ? -1 : 1;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
const dateComparison = lexicographicalCompare(left.descriptor.dateCreated, right.descriptor.dateCreated);
|
|
1212
|
+
if (dateComparison !== 0) {
|
|
1213
|
+
return dateComparison;
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
return lexicographicalCompare(left.recordId, right.recordId);
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
private static getRequiredStringTag(message: RecordsWriteMessage, tag: string, recordType: 'audience' | 'delivery'): string {
|
|
1220
|
+
const value = message.descriptor.tags?.[tag];
|
|
1221
|
+
if (typeof value !== 'string') {
|
|
1222
|
+
const errorCode = recordType === 'audience'
|
|
1223
|
+
? DwnErrorCode.EncryptionControlValidateAudienceMissingRequiredTag
|
|
1224
|
+
: DwnErrorCode.EncryptionControlValidateDeliveryMissingRequiredTag;
|
|
1225
|
+
throw new DwnError(
|
|
1226
|
+
errorCode,
|
|
1227
|
+
`${recordType} control records must include string tag '${tag}'.`
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
return value;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
private static getOptionalStringTag(tags: RecordsWriteTags | undefined, tag: string): string | undefined {
|
|
1235
|
+
const value = tags?.[tag];
|
|
1236
|
+
if (value === undefined) {
|
|
1237
|
+
return undefined;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
if (typeof value !== 'string') {
|
|
1241
|
+
throw new DwnError(
|
|
1242
|
+
DwnErrorCode.EncryptionControlValidateDeliveryMissingRequiredTag,
|
|
1243
|
+
`delivery control tag '${tag}' must be a string.`
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
return value;
|
|
1248
|
+
}
|
|
1249
|
+
}
|