From c93593c5073c6ba0381d5d18f9225e190e2e4f3f Mon Sep 17 00:00:00 2001 From: Marat Shakirov Date: Mon, 9 Sep 2024 17:37:13 +0200 Subject: [PATCH] release: publish affinidi messaging --- .github/workflows/on-push.yaml | 15 + .gitignore | 6 + Cargo.lock | 8063 +++++++++++++++ Cargo.toml | 24 + LICENSE | 201 + NOTICE.txt | 1 + README.md | 53 +- SECURITY.md | 1 + .../.github/workflows/release.yml | 281 + .../.github/workflows/verify.yml | 262 + affinidi-messaging-didcomm/.gitignore | 40 + affinidi-messaging-didcomm/CHANGELOG.md | 7 + affinidi-messaging-didcomm/Cargo.toml | 67 + affinidi-messaging-didcomm/LICENSE | 201 + affinidi-messaging-didcomm/README.md | 259 + .../Specs/DidcommSDK/0.3.1/DidcommSDK.podspec | 23 + .../Specs/DidcommSDK/0.3.2/DidcommSDK.podspec | 23 + .../Specs/DidcommSDK/0.3.3/DidcommSDK.podspec | 23 + .../Specs/DidcommSDK/0.3.4/DidcommSDK.podspec | 23 + .../benches/pack_encrypted.rs | 702 ++ .../benches/pack_signed.rs | 61 + affinidi-messaging-didcomm/docs/release.md | 18 + affinidi-messaging-didcomm/renovate.json | 5 + affinidi-messaging-didcomm/src/algorithms.rs | 33 + affinidi-messaging-didcomm/src/document.rs | 431 + affinidi-messaging-didcomm/src/envelope.rs | 133 + affinidi-messaging-didcomm/src/error.rs | 202 + affinidi-messaging-didcomm/src/jwe/decrypt.rs | 1753 ++++ affinidi-messaging-didcomm/src/jwe/encrypt.rs | 461 + .../src/jwe/envelope.rs | 175 + affinidi-messaging-didcomm/src/jwe/mod.rs | 141 + affinidi-messaging-didcomm/src/jwe/parse.rs | 220 + affinidi-messaging-didcomm/src/jwk.rs | 82 + .../src/jws/envelope.rs | 145 + affinidi-messaging-didcomm/src/jws/mod.rs | 89 + affinidi-messaging-didcomm/src/jws/parse.rs | 621 ++ affinidi-messaging-didcomm/src/jws/sign.rs | 570 ++ affinidi-messaging-didcomm/src/jws/verify.rs | 683 ++ affinidi-messaging-didcomm/src/lib.rs | 116 + .../src/message/attachment.rs | 333 + .../src/message/from_prior/mod.rs | 97 + .../src/message/from_prior/pack.rs | 372 + .../src/message/from_prior/unpack.rs | 210 + .../src/message/message.rs | 316 + affinidi-messaging-didcomm/src/message/mod.rs | 22 + .../src/message/pack_encrypted/anoncrypt.rs | 309 + .../src/message/pack_encrypted/authcrypt.rs | 454 + .../src/message/pack_encrypted/mod.rs | 3034 ++++++ .../src/message/pack_plaintext.rs | 181 + .../src/message/pack_signed.rs | 474 + .../src/message/unpack/anoncrypt.rs | 191 + .../src/message/unpack/authcrypt.rs | 161 + .../src/message/unpack/mod.rs | 2194 ++++ .../src/message/unpack/plaintext.rs | 26 + .../src/message/unpack/sign.rs | 165 + .../src/protocols/mod.rs | 1 + .../src/protocols/routing/forward.rs | 12 + .../src/protocols/routing/mod.rs | 417 + affinidi-messaging-didcomm/src/secrets/mod.rs | 108 + .../src/secrets/resolvers/example.rs | 36 + .../src/secrets/resolvers/mod.rs | 3 + .../src/test_vectors/common.rs | 62 + .../src/test_vectors/encrypted.rs | 107 + .../src/test_vectors/from_prior.rs | 54 + .../src/test_vectors/from_prior_jwt.rs | 5 + .../src/test_vectors/message.rs | 137 + .../src/test_vectors/mod.rs | 16 + .../src/test_vectors/plaintext.rs | 288 + .../src/test_vectors/secrets/alice.rs | 91 + .../src/test_vectors/secrets/bob.rs | 141 + .../src/test_vectors/secrets/charlie.rs | 38 + .../secrets/charlie_rotated_to_alice.rs | 25 + .../src/test_vectors/secrets/mediator1.rs | 68 + .../src/test_vectors/secrets/mediator2.rs | 68 + .../src/test_vectors/secrets/mediator3.rs | 68 + .../src/test_vectors/secrets/mod.rs | 35 + .../src/test_vectors/signed.rs | 38 + .../src/utils/crypto.rs | 258 + affinidi-messaging-didcomm/src/utils/mod.rs | 2 + affinidi-messaging-didcomm/src/utils/serde.rs | 3 + affinidi-messaging-didcomm/uniffi/.gitignore | 9 + affinidi-messaging-didcomm/uniffi/Cargo.toml | 36 + affinidi-messaging-didcomm/uniffi/LICENSE | 176 + affinidi-messaging-didcomm/uniffi/README.md | 31 + affinidi-messaging-didcomm/uniffi/build.rs | 3 + .../uniffi/src/common.rs | 89 + .../uniffi/src/did/did_resolver.rs | 21 + .../uniffi/src/did/did_resolver_adapter.rs | 35 + .../uniffi/src/did/mod.rs | 6 + .../uniffi/src/did/resolvers/example.rs | 33 + .../uniffi/src/did/resolvers/mod.rs | 3 + .../uniffi/src/didcomm.udl | 352 + .../uniffi/src/didcomm/from_prior.rs | 126 + .../uniffi/src/didcomm/mod.rs | 34 + .../uniffi/src/didcomm/pack_encrypted.rs | 158 + .../uniffi/src/didcomm/pack_plaintext.rs | 49 + .../uniffi/src/didcomm/pack_signed.rs | 132 + .../uniffi/src/didcomm/protocols/mod.rs | 1 + .../src/didcomm/protocols/routing/mod.rs | 264 + .../uniffi/src/didcomm/unpack.rs | 119 + affinidi-messaging-didcomm/uniffi/src/lib.rs | 25 + .../uniffi/src/secrets/mod.rs | 6 + .../uniffi/src/secrets/resolvers/example.rs | 46 + .../uniffi/src/secrets/resolvers/mod.rs | 3 + .../uniffi/src/secrets/secrets_resolver.rs | 35 + .../src/secrets/secrets_resolver_adapter.rs | 53 + .../uniffi/src/test_helper.rs | 180 + affinidi-messaging-didcomm/wasm/.gitignore | 6 + affinidi-messaging-didcomm/wasm/Cargo.toml | 56 + affinidi-messaging-didcomm/wasm/LICENSE | 176 + affinidi-messaging-didcomm/wasm/Makefile | 41 + affinidi-messaging-didcomm/wasm/README.md | 271 + .../wasm/demo/.gitignore | 1 + .../wasm/demo/package-lock.json | 978 ++ .../wasm/demo/package.json | 21 + .../wasm/demo/src/advanced-params.ts | 67 + .../wasm/demo/src/attachments.ts | 73 + .../wasm/demo/src/main.ts | 386 + .../wasm/demo/src/test-vectors.ts | 376 + .../wasm/demo/tslint.json | 12 + .../wasm/src/did/did_doc.rs | 93 + .../wasm/src/did/did_resolver.rs | 66 + .../wasm/src/did/mod.rs | 5 + affinidi-messaging-didcomm/wasm/src/error.rs | 71 + affinidi-messaging-didcomm/wasm/src/lib.rs | 18 + .../wasm/src/message/from_prior/mod.rs | 76 + .../wasm/src/message/from_prior/pack.rs | 68 + .../wasm/src/message/from_prior/unpack.rs | 57 + .../wasm/src/message/mod.rs | 254 + .../wasm/src/message/pack_encrypted.rs | 226 + .../wasm/src/message/pack_plaintext.rs | 48 + .../wasm/src/message/pack_signed.rs | 99 + .../wasm/src/message/protocols/mod.rs | 1 + .../wasm/src/message/protocols/routing/mod.rs | 155 + .../wasm/src/message/unpack.rs | 200 + .../wasm/src/secrets/mod.rs | 5 + .../wasm/src/secrets/secret.rs | 36 + .../wasm/src/secrets/secrets_resolver.rs | 132 + affinidi-messaging-didcomm/wasm/src/utils.rs | 10 + .../wasm/tests-js/.gitignore | 1 + .../wasm/tests-js/.prettierrc.json | 1 + .../wasm/tests-js/jest-puppeteer.config.js | 8 + .../wasm/tests-js/jest.config.js | 7 + .../wasm/tests-js/jest.config.puppeteer.js | 7 + .../wasm/tests-js/package-lock.json | 9097 +++++++++++++++++ .../wasm/tests-js/package.json | 29 + .../src/forward/try-parse-forward.test.ts | 23 + .../src/forward/wrap-in-forward.test.ts | 42 + .../wasm/tests-js/src/from_prior/new.test.ts | 10 + .../wasm/tests-js/src/from_prior/pack.test.ts | 54 + .../tests-js/src/from_prior/unpack.test.ts | 43 + .../wasm/tests-js/src/message/errors.test.ts | 266 + .../wasm/tests-js/src/message/new.test.ts | 41 + .../message/pack-encrypted.anoncrypt.test.ts | 124 + .../message/pack-encrypted.authcrypt.test.ts | 219 + .../src/message/pack-plaintext.test.ts | 43 + .../tests-js/src/message/pack-signed.test.ts | 152 + .../wasm/tests-js/src/message/unpack.test.ts | 113 + .../wasm/tests-js/src/test-vectors/common.ts | 3 + .../src/test-vectors/did_doc/alice.ts | 118 + .../tests-js/src/test-vectors/did_doc/bob.ts | 129 + .../src/test-vectors/did_doc/charlie.ts | 30 + .../src/test-vectors/did_doc/index.ts | 3 + .../tests-js/src/test-vectors/did_resolver.ts | 32 + .../tests-js/src/test-vectors/from_prior.ts | 17 + .../src/test-vectors/from_prior_jwt.ts | 7 + .../wasm/tests-js/src/test-vectors/index.ts | 9 + .../wasm/tests-js/src/test-vectors/message.ts | 80 + .../tests-js/src/test-vectors/plaintext.ts | 35 + .../src/test-vectors/secrets/alice.ts | 68 + .../tests-js/src/test-vectors/secrets/bob.ts | 110 + .../src/test-vectors/secrets/charlie.ts | 28 + .../secrets/charlie_rotated_to_alice.ts | 88 + .../src/test-vectors/secrets/index.ts | 4 + .../src/test-vectors/secrets_resolver.ts | 56 + .../wasm/tests-js/tslint.json | 7 + .../wrappers/swift/README.md | 36 + .../wrappers/swift/didcomm.swiftdoc | Bin 0 -> 604 bytes .../wrappers/swift/didcomm.swiftsourceinfo | Bin 0 -> 65252 bytes .../wrappers/swift/didcomm/didcomm.swift | 3551 +++++++ .../swift/didcomm/didcomm.swiftmodule | Bin 0 -> 315740 bytes .../wrappers/swift/didcomm/didcommFFI.h | 200 + .../swift/didcomm/didcommFFI.modulemap | 6 + .../wrappers/swift/didcomm/libdidcomm.dylib | Bin 0 -> 674256 bytes .../project.pbxproj | 406 + .../DidcommExampleiOS/AppDelegate.swift | 16 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 98 + .../Assets.xcassets/Contents.json | 6 + .../DidcommExampleiOS/Constants.swift | 154 + .../DidcommExampleiOS/Info.plist | 5 + .../DidcommExampleiOS/ViewController.swift | 132 + .../swift/examples/DidcommExampleiOS/Podfile | 10 + .../examples/DidcommExampleiOS/Podfile.lock | 16 + .../wrappers/swift/examples/README.md | 15 + .../wrappers/swift/examples/base.swift | 1 + affinidi-messaging-mediator/.gitignore | 4 + affinidi-messaging-mediator/CHANGELOG.md | 66 + affinidi-messaging-mediator/Cargo.toml | 68 + affinidi-messaging-mediator/LICENCE-APACHE | 201 + affinidi-messaging-mediator/README.md | 68 + .../conf/mediator.toml | 145 + .../conf/secrets.json-example | 23 + .../examples/create_local_certs.rs | 278 + .../examples/generate_secrets.rs | 145 + .../examples/send_message.rs | 250 + .../src/common/config.rs | 641 ++ .../src/common/errors.rs | 329 + .../src/common/jwt_auth.rs | 166 + affinidi-messaging-mediator/src/common/mod.rs | 3 + .../src/database/delete.rs | 59 + .../src/database/fetch.rs | 100 + .../src/database/get.rs | 97 + .../src/database/handlers.rs | 143 + .../src/database/list.rs | 140 + .../src/database/mod.rs | 14 + .../src/database/session.rs | 211 + .../src/database/stats.rs | 188 + .../src/database/store.rs | 126 + .../src/database/streaming.rs | 367 + .../src/handlers/authenticate.rs | 285 + .../src/handlers/inbox_fetch.rs | 64 + .../src/handlers/message_delete.rs | 69 + .../src/handlers/message_inbound.rs | 61 + .../src/handlers/message_list.rs | 71 + .../src/handlers/message_outbound.rs | 77 + .../src/handlers/mod.rs | 66 + .../src/handlers/websocket.rs | 132 + .../src/handlers/well_known_did_fetch.rs | 29 + affinidi-messaging-mediator/src/lib.rs | 99 + affinidi-messaging-mediator/src/main.rs | 172 + .../src/messages/inbound.rs | 206 + .../src/messages/mod.rs | 196 + .../src/messages/protocols/message_pickup.rs | 586 ++ .../src/messages/protocols/mod.rs | 3 + .../src/messages/protocols/ping.rs | 124 + .../src/messages/protocols/routing.rs | 132 + .../src/resolvers/affinidi_secrets.rs | 45 + .../src/resolvers/mod.rs | 1 + affinidi-messaging-mediator/src/tasks/mod.rs | 3 + .../src/tasks/statistics.rs | 32 + .../src/tasks/websocket_streaming.rs | 235 + affinidi-messaging-processor/.gitignore | 1 + affinidi-messaging-processor/CHANGELOG.md | 27 + affinidi-messaging-processor/Cargo.toml | 13 + affinidi-messaging-processor/README.md | 4 + .../redis-functions/atm-functions.lua | 283 + affinidi-messaging-processor/src/main.rs | 3 + affinidi-messaging-sdk/.gitignore | 1 + affinidi-messaging-sdk/CHANGELOG.md | 76 + affinidi-messaging-sdk/Cargo.toml | 47 + affinidi-messaging-sdk/README.md | 269 + affinidi-messaging-sdk/certs/mediator-key.pem | 20 + .../examples/message_pickup.rs | 153 + affinidi-messaging-sdk/examples/ping.rs | 180 + .../examples/send_message.rs | 140 + .../examples/send_message_to_me.rs | 154 + .../src/authentication/mod.rs | 179 + affinidi-messaging-sdk/src/config.rs | 250 + affinidi-messaging-sdk/src/conversions/mod.rs | 36 + affinidi-messaging-sdk/src/errors.rs | 24 + affinidi-messaging-sdk/src/lib.rs | 195 + affinidi-messaging-sdk/src/messages/delete.rs | 78 + affinidi-messaging-sdk/src/messages/fetch.rs | 156 + affinidi-messaging-sdk/src/messages/get.rs | 85 + affinidi-messaging-sdk/src/messages/list.rs | 67 + affinidi-messaging-sdk/src/messages/mod.rs | 152 + affinidi-messaging-sdk/src/messages/pack.rs | 81 + .../src/messages/sending.rs | 32 + affinidi-messaging-sdk/src/messages/unpack.rs | 52 + .../src/messages/well_known_did.rs | 49 + .../src/protocols/message_pickup.rs | 643 ++ affinidi-messaging-sdk/src/protocols/mod.rs | 17 + .../src/protocols/trust_ping.rs | 126 + affinidi-messaging-sdk/src/resolvers/mod.rs | 1 + .../src/resolvers/secrets_resolver.rs | 45 + .../src/transports/http/mod.rs | 1 + .../src/transports/http/sending.rs | 62 + affinidi-messaging-sdk/src/transports/mod.rs | 31 + .../src/transports/websockets/mod.rs | 95 + .../src/transports/websockets/sending.rs | 48 + .../src/transports/websockets/ws_handler.rs | 336 + deny.toml | 64 + 283 files changed, 59505 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/on-push.yaml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 NOTICE.txt create mode 100644 SECURITY.md create mode 100644 affinidi-messaging-didcomm/.github/workflows/release.yml create mode 100644 affinidi-messaging-didcomm/.github/workflows/verify.yml create mode 100644 affinidi-messaging-didcomm/.gitignore create mode 100644 affinidi-messaging-didcomm/CHANGELOG.md create mode 100644 affinidi-messaging-didcomm/Cargo.toml create mode 100644 affinidi-messaging-didcomm/LICENSE create mode 100644 affinidi-messaging-didcomm/README.md create mode 100644 affinidi-messaging-didcomm/Specs/DidcommSDK/0.3.1/DidcommSDK.podspec create mode 100644 affinidi-messaging-didcomm/Specs/DidcommSDK/0.3.2/DidcommSDK.podspec create mode 100644 affinidi-messaging-didcomm/Specs/DidcommSDK/0.3.3/DidcommSDK.podspec create mode 100644 affinidi-messaging-didcomm/Specs/DidcommSDK/0.3.4/DidcommSDK.podspec create mode 100644 affinidi-messaging-didcomm/benches/pack_encrypted.rs create mode 100644 affinidi-messaging-didcomm/benches/pack_signed.rs create mode 100644 affinidi-messaging-didcomm/docs/release.md create mode 100644 affinidi-messaging-didcomm/renovate.json create mode 100644 affinidi-messaging-didcomm/src/algorithms.rs create mode 100644 affinidi-messaging-didcomm/src/document.rs create mode 100644 affinidi-messaging-didcomm/src/envelope.rs create mode 100644 affinidi-messaging-didcomm/src/error.rs create mode 100644 affinidi-messaging-didcomm/src/jwe/decrypt.rs create mode 100644 affinidi-messaging-didcomm/src/jwe/encrypt.rs create mode 100644 affinidi-messaging-didcomm/src/jwe/envelope.rs create mode 100644 affinidi-messaging-didcomm/src/jwe/mod.rs create mode 100644 affinidi-messaging-didcomm/src/jwe/parse.rs create mode 100644 affinidi-messaging-didcomm/src/jwk.rs create mode 100644 affinidi-messaging-didcomm/src/jws/envelope.rs create mode 100644 affinidi-messaging-didcomm/src/jws/mod.rs create mode 100644 affinidi-messaging-didcomm/src/jws/parse.rs create mode 100644 affinidi-messaging-didcomm/src/jws/sign.rs create mode 100644 affinidi-messaging-didcomm/src/jws/verify.rs create mode 100644 affinidi-messaging-didcomm/src/lib.rs create mode 100644 affinidi-messaging-didcomm/src/message/attachment.rs create mode 100644 affinidi-messaging-didcomm/src/message/from_prior/mod.rs create mode 100644 affinidi-messaging-didcomm/src/message/from_prior/pack.rs create mode 100644 affinidi-messaging-didcomm/src/message/from_prior/unpack.rs create mode 100644 affinidi-messaging-didcomm/src/message/message.rs create mode 100644 affinidi-messaging-didcomm/src/message/mod.rs create mode 100644 affinidi-messaging-didcomm/src/message/pack_encrypted/anoncrypt.rs create mode 100644 affinidi-messaging-didcomm/src/message/pack_encrypted/authcrypt.rs create mode 100644 affinidi-messaging-didcomm/src/message/pack_encrypted/mod.rs create mode 100644 affinidi-messaging-didcomm/src/message/pack_plaintext.rs create mode 100644 affinidi-messaging-didcomm/src/message/pack_signed.rs create mode 100644 affinidi-messaging-didcomm/src/message/unpack/anoncrypt.rs create mode 100644 affinidi-messaging-didcomm/src/message/unpack/authcrypt.rs create mode 100644 affinidi-messaging-didcomm/src/message/unpack/mod.rs create mode 100644 affinidi-messaging-didcomm/src/message/unpack/plaintext.rs create mode 100644 affinidi-messaging-didcomm/src/message/unpack/sign.rs create mode 100644 affinidi-messaging-didcomm/src/protocols/mod.rs create mode 100644 affinidi-messaging-didcomm/src/protocols/routing/forward.rs create mode 100644 affinidi-messaging-didcomm/src/protocols/routing/mod.rs create mode 100644 affinidi-messaging-didcomm/src/secrets/mod.rs create mode 100644 affinidi-messaging-didcomm/src/secrets/resolvers/example.rs create mode 100644 affinidi-messaging-didcomm/src/secrets/resolvers/mod.rs create mode 100644 affinidi-messaging-didcomm/src/test_vectors/common.rs create mode 100644 affinidi-messaging-didcomm/src/test_vectors/encrypted.rs create mode 100644 affinidi-messaging-didcomm/src/test_vectors/from_prior.rs create mode 100644 affinidi-messaging-didcomm/src/test_vectors/from_prior_jwt.rs create mode 100644 affinidi-messaging-didcomm/src/test_vectors/message.rs create mode 100644 affinidi-messaging-didcomm/src/test_vectors/mod.rs create mode 100644 affinidi-messaging-didcomm/src/test_vectors/plaintext.rs create mode 100644 affinidi-messaging-didcomm/src/test_vectors/secrets/alice.rs create mode 100644 affinidi-messaging-didcomm/src/test_vectors/secrets/bob.rs create mode 100644 affinidi-messaging-didcomm/src/test_vectors/secrets/charlie.rs create mode 100644 affinidi-messaging-didcomm/src/test_vectors/secrets/charlie_rotated_to_alice.rs create mode 100644 affinidi-messaging-didcomm/src/test_vectors/secrets/mediator1.rs create mode 100644 affinidi-messaging-didcomm/src/test_vectors/secrets/mediator2.rs create mode 100644 affinidi-messaging-didcomm/src/test_vectors/secrets/mediator3.rs create mode 100644 affinidi-messaging-didcomm/src/test_vectors/secrets/mod.rs create mode 100644 affinidi-messaging-didcomm/src/test_vectors/signed.rs create mode 100644 affinidi-messaging-didcomm/src/utils/crypto.rs create mode 100644 affinidi-messaging-didcomm/src/utils/mod.rs create mode 100644 affinidi-messaging-didcomm/src/utils/serde.rs create mode 100644 affinidi-messaging-didcomm/uniffi/.gitignore create mode 100644 affinidi-messaging-didcomm/uniffi/Cargo.toml create mode 100644 affinidi-messaging-didcomm/uniffi/LICENSE create mode 100644 affinidi-messaging-didcomm/uniffi/README.md create mode 100644 affinidi-messaging-didcomm/uniffi/build.rs create mode 100644 affinidi-messaging-didcomm/uniffi/src/common.rs create mode 100644 affinidi-messaging-didcomm/uniffi/src/did/did_resolver.rs create mode 100644 affinidi-messaging-didcomm/uniffi/src/did/did_resolver_adapter.rs create mode 100644 affinidi-messaging-didcomm/uniffi/src/did/mod.rs create mode 100644 affinidi-messaging-didcomm/uniffi/src/did/resolvers/example.rs create mode 100644 affinidi-messaging-didcomm/uniffi/src/did/resolvers/mod.rs create mode 100644 affinidi-messaging-didcomm/uniffi/src/didcomm.udl create mode 100644 affinidi-messaging-didcomm/uniffi/src/didcomm/from_prior.rs create mode 100644 affinidi-messaging-didcomm/uniffi/src/didcomm/mod.rs create mode 100644 affinidi-messaging-didcomm/uniffi/src/didcomm/pack_encrypted.rs create mode 100644 affinidi-messaging-didcomm/uniffi/src/didcomm/pack_plaintext.rs create mode 100644 affinidi-messaging-didcomm/uniffi/src/didcomm/pack_signed.rs create mode 100644 affinidi-messaging-didcomm/uniffi/src/didcomm/protocols/mod.rs create mode 100644 affinidi-messaging-didcomm/uniffi/src/didcomm/protocols/routing/mod.rs create mode 100644 affinidi-messaging-didcomm/uniffi/src/didcomm/unpack.rs create mode 100644 affinidi-messaging-didcomm/uniffi/src/lib.rs create mode 100644 affinidi-messaging-didcomm/uniffi/src/secrets/mod.rs create mode 100644 affinidi-messaging-didcomm/uniffi/src/secrets/resolvers/example.rs create mode 100644 affinidi-messaging-didcomm/uniffi/src/secrets/resolvers/mod.rs create mode 100644 affinidi-messaging-didcomm/uniffi/src/secrets/secrets_resolver.rs create mode 100644 affinidi-messaging-didcomm/uniffi/src/secrets/secrets_resolver_adapter.rs create mode 100644 affinidi-messaging-didcomm/uniffi/src/test_helper.rs create mode 100644 affinidi-messaging-didcomm/wasm/.gitignore create mode 100644 affinidi-messaging-didcomm/wasm/Cargo.toml create mode 100644 affinidi-messaging-didcomm/wasm/LICENSE create mode 100644 affinidi-messaging-didcomm/wasm/Makefile create mode 100644 affinidi-messaging-didcomm/wasm/README.md create mode 100644 affinidi-messaging-didcomm/wasm/demo/.gitignore create mode 100644 affinidi-messaging-didcomm/wasm/demo/package-lock.json create mode 100644 affinidi-messaging-didcomm/wasm/demo/package.json create mode 100644 affinidi-messaging-didcomm/wasm/demo/src/advanced-params.ts create mode 100644 affinidi-messaging-didcomm/wasm/demo/src/attachments.ts create mode 100644 affinidi-messaging-didcomm/wasm/demo/src/main.ts create mode 100644 affinidi-messaging-didcomm/wasm/demo/src/test-vectors.ts create mode 100644 affinidi-messaging-didcomm/wasm/demo/tslint.json create mode 100644 affinidi-messaging-didcomm/wasm/src/did/did_doc.rs create mode 100644 affinidi-messaging-didcomm/wasm/src/did/did_resolver.rs create mode 100644 affinidi-messaging-didcomm/wasm/src/did/mod.rs create mode 100644 affinidi-messaging-didcomm/wasm/src/error.rs create mode 100644 affinidi-messaging-didcomm/wasm/src/lib.rs create mode 100644 affinidi-messaging-didcomm/wasm/src/message/from_prior/mod.rs create mode 100644 affinidi-messaging-didcomm/wasm/src/message/from_prior/pack.rs create mode 100644 affinidi-messaging-didcomm/wasm/src/message/from_prior/unpack.rs create mode 100644 affinidi-messaging-didcomm/wasm/src/message/mod.rs create mode 100644 affinidi-messaging-didcomm/wasm/src/message/pack_encrypted.rs create mode 100644 affinidi-messaging-didcomm/wasm/src/message/pack_plaintext.rs create mode 100644 affinidi-messaging-didcomm/wasm/src/message/pack_signed.rs create mode 100644 affinidi-messaging-didcomm/wasm/src/message/protocols/mod.rs create mode 100644 affinidi-messaging-didcomm/wasm/src/message/protocols/routing/mod.rs create mode 100644 affinidi-messaging-didcomm/wasm/src/message/unpack.rs create mode 100644 affinidi-messaging-didcomm/wasm/src/secrets/mod.rs create mode 100644 affinidi-messaging-didcomm/wasm/src/secrets/secret.rs create mode 100644 affinidi-messaging-didcomm/wasm/src/secrets/secrets_resolver.rs create mode 100644 affinidi-messaging-didcomm/wasm/src/utils.rs create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/.gitignore create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/.prettierrc.json create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/jest-puppeteer.config.js create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/jest.config.js create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/jest.config.puppeteer.js create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/package-lock.json create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/package.json create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/forward/try-parse-forward.test.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/forward/wrap-in-forward.test.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/from_prior/new.test.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/from_prior/pack.test.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/from_prior/unpack.test.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/message/errors.test.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/message/new.test.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/message/pack-encrypted.anoncrypt.test.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/message/pack-encrypted.authcrypt.test.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/message/pack-plaintext.test.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/message/pack-signed.test.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/message/unpack.test.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/common.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/did_doc/alice.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/did_doc/bob.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/did_doc/charlie.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/did_doc/index.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/did_resolver.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/from_prior.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/from_prior_jwt.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/index.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/message.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/plaintext.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets/alice.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets/bob.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets/charlie.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets/charlie_rotated_to_alice.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets/index.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets_resolver.ts create mode 100644 affinidi-messaging-didcomm/wasm/tests-js/tslint.json create mode 100644 affinidi-messaging-didcomm/wrappers/swift/README.md create mode 100644 affinidi-messaging-didcomm/wrappers/swift/didcomm.swiftdoc create mode 100644 affinidi-messaging-didcomm/wrappers/swift/didcomm.swiftsourceinfo create mode 100644 affinidi-messaging-didcomm/wrappers/swift/didcomm/didcomm.swift create mode 100644 affinidi-messaging-didcomm/wrappers/swift/didcomm/didcomm.swiftmodule create mode 100644 affinidi-messaging-didcomm/wrappers/swift/didcomm/didcommFFI.h create mode 100644 affinidi-messaging-didcomm/wrappers/swift/didcomm/didcommFFI.modulemap create mode 100755 affinidi-messaging-didcomm/wrappers/swift/didcomm/libdidcomm.dylib create mode 100644 affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS.xcodeproj/project.pbxproj create mode 100644 affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/AppDelegate.swift create mode 100644 affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/Assets.xcassets/Contents.json create mode 100644 affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/Constants.swift create mode 100644 affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/Info.plist create mode 100644 affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/ViewController.swift create mode 100644 affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/Podfile create mode 100644 affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/Podfile.lock create mode 100644 affinidi-messaging-didcomm/wrappers/swift/examples/README.md create mode 100644 affinidi-messaging-didcomm/wrappers/swift/examples/base.swift create mode 100644 affinidi-messaging-mediator/.gitignore create mode 100644 affinidi-messaging-mediator/CHANGELOG.md create mode 100644 affinidi-messaging-mediator/Cargo.toml create mode 100644 affinidi-messaging-mediator/LICENCE-APACHE create mode 100644 affinidi-messaging-mediator/README.md create mode 100644 affinidi-messaging-mediator/conf/mediator.toml create mode 100644 affinidi-messaging-mediator/conf/secrets.json-example create mode 100644 affinidi-messaging-mediator/examples/create_local_certs.rs create mode 100644 affinidi-messaging-mediator/examples/generate_secrets.rs create mode 100644 affinidi-messaging-mediator/examples/send_message.rs create mode 100644 affinidi-messaging-mediator/src/common/config.rs create mode 100644 affinidi-messaging-mediator/src/common/errors.rs create mode 100644 affinidi-messaging-mediator/src/common/jwt_auth.rs create mode 100644 affinidi-messaging-mediator/src/common/mod.rs create mode 100644 affinidi-messaging-mediator/src/database/delete.rs create mode 100644 affinidi-messaging-mediator/src/database/fetch.rs create mode 100644 affinidi-messaging-mediator/src/database/get.rs create mode 100644 affinidi-messaging-mediator/src/database/handlers.rs create mode 100644 affinidi-messaging-mediator/src/database/list.rs create mode 100644 affinidi-messaging-mediator/src/database/mod.rs create mode 100644 affinidi-messaging-mediator/src/database/session.rs create mode 100644 affinidi-messaging-mediator/src/database/stats.rs create mode 100644 affinidi-messaging-mediator/src/database/store.rs create mode 100644 affinidi-messaging-mediator/src/database/streaming.rs create mode 100644 affinidi-messaging-mediator/src/handlers/authenticate.rs create mode 100644 affinidi-messaging-mediator/src/handlers/inbox_fetch.rs create mode 100644 affinidi-messaging-mediator/src/handlers/message_delete.rs create mode 100644 affinidi-messaging-mediator/src/handlers/message_inbound.rs create mode 100644 affinidi-messaging-mediator/src/handlers/message_list.rs create mode 100644 affinidi-messaging-mediator/src/handlers/message_outbound.rs create mode 100644 affinidi-messaging-mediator/src/handlers/mod.rs create mode 100644 affinidi-messaging-mediator/src/handlers/websocket.rs create mode 100644 affinidi-messaging-mediator/src/handlers/well_known_did_fetch.rs create mode 100644 affinidi-messaging-mediator/src/lib.rs create mode 100644 affinidi-messaging-mediator/src/main.rs create mode 100644 affinidi-messaging-mediator/src/messages/inbound.rs create mode 100644 affinidi-messaging-mediator/src/messages/mod.rs create mode 100644 affinidi-messaging-mediator/src/messages/protocols/message_pickup.rs create mode 100644 affinidi-messaging-mediator/src/messages/protocols/mod.rs create mode 100644 affinidi-messaging-mediator/src/messages/protocols/ping.rs create mode 100644 affinidi-messaging-mediator/src/messages/protocols/routing.rs create mode 100644 affinidi-messaging-mediator/src/resolvers/affinidi_secrets.rs create mode 100644 affinidi-messaging-mediator/src/resolvers/mod.rs create mode 100644 affinidi-messaging-mediator/src/tasks/mod.rs create mode 100644 affinidi-messaging-mediator/src/tasks/statistics.rs create mode 100644 affinidi-messaging-mediator/src/tasks/websocket_streaming.rs create mode 100644 affinidi-messaging-processor/.gitignore create mode 100644 affinidi-messaging-processor/CHANGELOG.md create mode 100644 affinidi-messaging-processor/Cargo.toml create mode 100644 affinidi-messaging-processor/README.md create mode 100644 affinidi-messaging-processor/redis-functions/atm-functions.lua create mode 100644 affinidi-messaging-processor/src/main.rs create mode 100644 affinidi-messaging-sdk/.gitignore create mode 100644 affinidi-messaging-sdk/CHANGELOG.md create mode 100644 affinidi-messaging-sdk/Cargo.toml create mode 100644 affinidi-messaging-sdk/README.md create mode 100644 affinidi-messaging-sdk/certs/mediator-key.pem create mode 100644 affinidi-messaging-sdk/examples/message_pickup.rs create mode 100644 affinidi-messaging-sdk/examples/ping.rs create mode 100644 affinidi-messaging-sdk/examples/send_message.rs create mode 100644 affinidi-messaging-sdk/examples/send_message_to_me.rs create mode 100644 affinidi-messaging-sdk/src/authentication/mod.rs create mode 100644 affinidi-messaging-sdk/src/config.rs create mode 100644 affinidi-messaging-sdk/src/conversions/mod.rs create mode 100644 affinidi-messaging-sdk/src/errors.rs create mode 100644 affinidi-messaging-sdk/src/lib.rs create mode 100644 affinidi-messaging-sdk/src/messages/delete.rs create mode 100644 affinidi-messaging-sdk/src/messages/fetch.rs create mode 100644 affinidi-messaging-sdk/src/messages/get.rs create mode 100644 affinidi-messaging-sdk/src/messages/list.rs create mode 100644 affinidi-messaging-sdk/src/messages/mod.rs create mode 100644 affinidi-messaging-sdk/src/messages/pack.rs create mode 100644 affinidi-messaging-sdk/src/messages/sending.rs create mode 100644 affinidi-messaging-sdk/src/messages/unpack.rs create mode 100644 affinidi-messaging-sdk/src/messages/well_known_did.rs create mode 100644 affinidi-messaging-sdk/src/protocols/message_pickup.rs create mode 100644 affinidi-messaging-sdk/src/protocols/mod.rs create mode 100644 affinidi-messaging-sdk/src/protocols/trust_ping.rs create mode 100644 affinidi-messaging-sdk/src/resolvers/mod.rs create mode 100644 affinidi-messaging-sdk/src/resolvers/secrets_resolver.rs create mode 100644 affinidi-messaging-sdk/src/transports/http/mod.rs create mode 100644 affinidi-messaging-sdk/src/transports/http/sending.rs create mode 100644 affinidi-messaging-sdk/src/transports/mod.rs create mode 100644 affinidi-messaging-sdk/src/transports/websockets/mod.rs create mode 100644 affinidi-messaging-sdk/src/transports/websockets/sending.rs create mode 100644 affinidi-messaging-sdk/src/transports/websockets/ws_handler.rs create mode 100644 deny.toml diff --git a/.github/workflows/on-push.yaml b/.github/workflows/on-push.yaml new file mode 100644 index 0000000..fe88781 --- /dev/null +++ b/.github/workflows/on-push.yaml @@ -0,0 +1,15 @@ +name: on-push + +on: + push: + branches: + - main + pull_request_target: + types: + - opened + - synchronize + +jobs: + rust-workflow: + uses: affinidi/pipeline-rust/.github/workflows/on-push.yaml@main + secrets: inherit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2d0999 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +/target +dump.rdb +.DS_Store +/**/secrets.json-generated +/**/secrets.json +.vscode \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..14a60cb --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,8063 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "abnf" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "087113bd50d9adce24850eed5d0476c7d199d532fce8fab5173650331e09033a" +dependencies = [ + "abnf-core", + "nom", +] + +[[package]] +name = "abnf-core" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c44e09c43ae1c368fb91a03a566472d0087c26cf7e1b9e8e289c14ede681dd7d" +dependencies = [ + "nom", +] + +[[package]] +name = "addr2line" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e4503c46a5c0c7844e948c9a4d6acd9f50cccb4de1c48eb9e291ea17470c678" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "adler2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" + +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "affinidi-did-resolver-cache-sdk" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ddbcc736617dc961d6f0392015f7c496a9db60a5ecadd5ab935d9fc2b370a5" +dependencies = [ + "blake2", + "did-peer", + "futures-util", + "moka", + "rand", + "serde", + "serde_json", + "ssi", + "thiserror", + "tokio", + "tokio-tungstenite 0.23.1", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "affinidi-messaging-didcomm" +version = "0.5.0" +dependencies = [ + "affinidi-did-resolver-cache-sdk", + "anyhow", + "askar-crypto", + "async-trait", + "base64 0.22.1", + "bs58 0.5.1", + "criterion", + "getrandom", + "lazy_static", + "serde", + "serde-enum-str", + "serde_json", + "sha2 0.10.8", + "ssi", + "thiserror", + "tokio", + "tracing", + "tracing-test", + "uuid", + "varint", +] + +[[package]] +name = "affinidi-messaging-mediator" +version = "0.2.1" +dependencies = [ + "affinidi-did-resolver-cache-sdk", + "affinidi-messaging-didcomm", + "affinidi-messaging-sdk", + "async-convert", + "async-trait", + "aws-config", + "aws-sdk-dynamodb", + "aws-sdk-memorydb", + "aws-sdk-secretsmanager", + "aws-sdk-ssm", + "axum", + "axum-extra", + "axum-server", + "base64 0.22.1", + "chrono", + "deadpool-redis", + "did-peer", + "hostname", + "http 1.1.0", + "itertools 0.13.0", + "jsonwebtoken", + "rand", + "rcgen", + "redis", + "regex", + "reqwest 0.12.7", + "ring", + "rustls 0.23.12", + "serde", + "serde_json", + "sha256", + "ssi", + "thiserror", + "time", + "tokio", + "tokio-stream", + "toml 0.8.19", + "tower-http", + "tracing", + "tracing-subscriber", + "uuid", +] + +[[package]] +name = "affinidi-messaging-processing" +version = "0.1.1" + +[[package]] +name = "affinidi-messaging-sdk" +version = "0.1.3" +dependencies = [ + "affinidi-did-resolver-cache-sdk", + "affinidi-messaging-didcomm", + "async-trait", + "base64 0.22.1", + "clap 4.5.16", + "futures-util", + "http 1.1.0", + "jsonwebtoken", + "reqwest 0.12.7", + "rustls 0.23.12", + "rustls-native-certs 0.8.0", + "rustls-pemfile 2.1.3", + "serde", + "serde_json", + "sha256", + "ssi", + "thiserror", + "tokio", + "tokio-stream", + "tokio-tungstenite 0.23.1", + "tracing", + "tracing-subscriber", + "url", + "uuid", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anstream" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1" + +[[package]] +name = "anstyle-parse" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + +[[package]] +name = "anyhow" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" + +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + +[[package]] +name = "arrayref" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d151e35f61089500b617991b791fc8bfd237ae50cd5950803758a179b41e67a" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "askar-crypto" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39746932b19e345196a089e61a0f0175fc4d673db4b624424d8babf505e48a3d" +dependencies = [ + "aead", + "aes", + "aes-gcm", + "base64 0.21.7", + "blake2", + "block-modes", + "bls12_381", + "cbc", + "chacha20", + "chacha20poly1305", + "cipher", + "crypto_box", + "curve25519-dalek", + "digest 0.10.7", + "ed25519-dalek", + "elliptic-curve", + "group 0.13.0", + "hkdf", + "hmac", + "k256", + "p256", + "p384", + "rand", + "serde", + "serde-json-core", + "sha2 0.10.8", + "subtle", + "x25519-dalek", + "zeroize", +] + +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener 2.5.3", + "futures-core", +] + +[[package]] +name = "async-channel" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-convert" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d416feee97712e43152cd42874de162b8f9b77295b1c85e5d92725cc8310bae" +dependencies = [ + "async-trait", +] + +[[package]] +name = "async-executor" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7ebdfa2ebdab6b1760375fa7d6f382b9f486eac35fc994625a00e89280bdbb7" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand 2.1.1", + "futures-lite 2.3.0", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c" +dependencies = [ + "async-channel 2.3.1", + "async-executor", + "async-io 2.3.4", + "async-lock 3.4.0", + "blocking", + "futures-lite 2.3.0", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af" +dependencies = [ + "async-lock 2.8.0", + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-lite 1.13.0", + "log", + "parking", + "polling 2.8.0", + "rustix 0.37.27", + "slab", + "socket2 0.4.10", + "waker-fn", +] + +[[package]] +name = "async-io" +version = "2.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444b0228950ee6501b3568d3c93bf1176a1fdbc3b758dcd9475046d30f4dc7e8" +dependencies = [ + "async-lock 3.4.0", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite 2.3.0", + "parking", + "polling 3.7.3", + "rustix 0.38.35", + "slab", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "async-lock" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" +dependencies = [ + "event-listener 2.5.3", +] + +[[package]] +name = "async-lock" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18" +dependencies = [ + "event-listener 5.3.1", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-std" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-attributes", + "async-channel 1.9.0", + "async-global-executor", + "async-io 1.13.0", + "async-lock 2.8.0", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite 1.13.0", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" + +[[package]] +name = "aws-config" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e95816a168520d72c0e7680c405a5a8c1fb6a035b4bc4b9d7b0de8e1a941697" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.1.1", + "hex", + "http 0.2.12", + "ring", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60e8f6b615cb5fc60a98132268508ad104310f0cfb25a1c22eee76efdf9154da" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ae74d9bd0a7530e8afd1770739ad34b36838829d6ad61818f9230f683f5ad77" +dependencies = [ + "aws-lc-sys", + "mirai-annotations", + "paste", + "untrusted 0.7.1", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0e249228c6ad2d240c2dc94b714d711629d52bad946075d8e9b2f5391f0703" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", + "libc", + "paste", +] + +[[package]] +name = "aws-runtime" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2424565416eef55906f9f8cece2072b6b6a76075e3ff81483ebe938a89a4c05f" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.1.1", + "http 0.2.12", + "http-body 0.4.6", + "once_cell", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-dynamodb" +version = "1.43.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6befba9ce7b81b58b1249c854da754e2236dbee548a736b96230216ebf9bcadc" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.1.1", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-memorydb" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b6ae3864d75f02b2f22675b41fd8477b895f635030c3547932581896008f9db" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-secretsmanager" +version = "1.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2039325a02aa048e510442bb006129ec64c0c775666db905c315d2e714aeb2c0" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.1.1", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssm" +version = "1.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe324f268b6db8242d41c572f831e5338b716b1cf765d786fa40b0aea50ff82" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.1.1", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5879bec6e74b648ce12f6085e7245417bc5f6d672781028384d2e494be3eb6d" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ef4cd9362f638c22a3b959fd8df292e7e47fdf170270f86246b97109b5f2f7d" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b1e2735d2ab28b35ecbb5496c9d41857f52a0d6a0075bbf6a8af306045ea6f6" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "http 0.2.12", + "once_cell", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df1b0fa6be58efe9d4ccc257df0a53b89cd8909e86591a13ca54817c87517be" +dependencies = [ + "aws-credential-types", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.12", + "http 1.1.0", + "once_cell", + "percent-encoding", + "sha2 0.10.8", + "time", + "tracing", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62220bc6e97f946ddd51b5f1361f78996e704677afc518a4ff66b7a72ea1378c" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-http" +version = "0.60.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01dbcb6e2588fd64cfb6d7529661b06466419e4c54ed1c62d6510d2d0350a728" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http-body 0.4.6", + "once_cell", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4683df9469ef09468dad3473d129960119a0d3593617542b7d52086c8486f2d6" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1ce695746394772e7000b39fe073095db6d45a862d0767dd5ad0ac0d7f8eb87" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand 2.1.1", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "http-body 1.0.1", + "httparse", + "hyper 0.14.30", + "hyper-rustls 0.24.2", + "once_cell", + "pin-project-lite", + "pin-utils", + "rustls 0.21.12", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e086682a53d3aa241192aa110fa8dfce98f2f5ac2ead0de84d41582c7e8fdb96" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.1.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "273dcdfd762fae3e1650b8024624e7cd50e484e37abdab73a7a706188ad34543" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.1.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d123fbc2a4adc3c301652ba8e149bf4bc1d1725affb9784eb20c953ace06bf55" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5221b91b3e441e6675310829fd8984801b772cb1546ef6c0e54dec9f1ac13fef" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + +[[package]] +name = "axum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a6c9af12842a67734c9a2e355436e5d03b22383ed60cf13cd0c18fbfe3dcbcf" +dependencies = [ + "async-trait", + "axum-core", + "base64 0.21.7", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sha1", + "sync_wrapper 1.0.1", + "tokio", + "tokio-tungstenite 0.21.0", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15c63fd72d41492dc4f497196f5da1fb04fb7529e631d73630d1b491e47a2e3" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 0.1.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0be6ea09c9b96cb5076af0de2e383bd2bc0c18f827cf1967bdd353e0b910d733" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "headers", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-server" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56bac90848f6a9393ac03c63c640925c4b7c8ca21654de40d53f55964667c7d8" +dependencies = [ + "arc-swap", + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "pin-project-lite", + "rustls 0.23.12", + "rustls-pemfile 2.1.3", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.0", + "tower", + "tower-service", +] + +[[package]] +name = "backtrace" +version = "0.3.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc23269a4f8976d0a4d2e7109211a419fe30e8d88d677cd60b6bc79c5732e0a" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide 0.7.4", + "object", + "rustc-demangle", +] + +[[package]] +name = "base-x" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bech32" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf9ff0bbfd639f15c74af777d81383cf53efb7c93613f6cab67c6c11e05bbf8b" + +[[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags 2.6.0", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash 1.1.0", + "shlex", + "syn 2.0.77", + "which", +] + +[[package]] +name = "bit_utils" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8114dca45a04a4bdaf03de9976a3131cf954827757f971d84cd431a9f2cc26d" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + +[[package]] +name = "bitvec" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7774144344a4faa177370406a7ff5f1da24303817368584c6206c8303eb07848" +dependencies = [ + "funty 1.1.0", + "radium 0.6.2", + "tap", + "wyz 0.2.0", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty 2.0.0", + "radium 0.7.0", + "tap", + "wyz 0.5.1", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "blake2b_simd" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" +dependencies = [ + "arrayref", + "arrayvec 0.5.2", + "constant_time_eq 0.1.5", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23285ad32269793932e830392f2fe2f83e26488fd3ec778883a93c8323735780" +dependencies = [ + "arrayref", + "arrayvec 0.7.6", + "constant_time_eq 0.3.1", +] + +[[package]] +name = "blake2s_simd" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94230421e395b9920d23df13ea5d77a20e1725331f90fbbf6df6040b33f756ae" +dependencies = [ + "arrayref", + "arrayvec 0.7.6", + "constant_time_eq 0.3.1", +] + +[[package]] +name = "blake3" +version = "1.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d82033247fd8e890df8f740e407ad4d038debb9eb1f40533fffb32e7d17dc6f7" +dependencies = [ + "arrayref", + "arrayvec 0.7.6", + "cc", + "cfg-if", + "constant_time_eq 0.3.1", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-modes" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e2211b0817f061502a8dd9f11a37e879e79763e3c698d2418cf824d8cb2f21e" + +[[package]] +name = "block-padding" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8894febbff9f758034a5b8e12d87918f56dfc64a8e1fe757d65e29041538d93" +dependencies = [ + "generic-array", +] + +[[package]] +name = "blocking" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea" +dependencies = [ + "async-channel 2.3.1", + "async-task", + "futures-io", + "futures-lite 2.3.0", + "piper", +] + +[[package]] +name = "bls12_381" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bc6d6292be3a19e6379786dac800f551e5865a5bb51ebbe3064ab80433f403" +dependencies = [ + "ff 0.13.0", + "group 0.13.0", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "bls12_381_plus" +version = "0.8.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ed5f583bb45888c562e1f117f7f20ce86a640f1163c38fe665ac7a71190861" +dependencies = [ + "arrayref", + "elliptic-curve", + "ff 0.13.0", + "group 0.13.0", + "hex", + "pairing", + "rand_core", + "serde", + "sha2 0.10.8", + "subtle", + "zeroize", +] + +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" +dependencies = [ + "sha2 0.9.9", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "btree-range-map" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be5c9672446d3800bcbcaabaeba121fe22f1fb25700c4562b22faf76d377c33" +dependencies = [ + "btree-slab", + "cc-traits", + "range-traits", + "serde", + "slab", +] + +[[package]] +name = "btree-slab" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2b56d3029f075c4fa892428a098425b86cef5c89ae54073137ece416aef13c" +dependencies = [ + "cc-traits", + "slab", + "smallvec", +] + +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8318a53db07bb3f8dca91a600466bdb3f2eaadeedfdbcf02e1accbad9271ba50" + +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "cached" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af4dfac631a8e77b2f327f7852bb6172771f5279c4512efe79fad6067b37be3d" +dependencies = [ + "hashbrown 0.11.2", + "once_cell", +] + +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-license" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "653542a7f5db653bf79ee4b6455c23f8e6b8a9c38c6310fbe14528728c14bd19" +dependencies = [ + "ansi_term", + "anyhow", + "atty", + "cargo_metadata", + "clap 3.2.25", + "csv", + "getopts", + "semver", + "serde", + "serde_derive", + "serde_json", + "toml 0.5.11", +] + +[[package]] +name = "cargo-platform" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.15.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee4243f1f26fc7a42710e7439c149e2b10b05472f88090acce52632f231a73a" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cbc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b52a9543ae338f279b96b0b9fed9c8093744685043739079ce85cd58f289a6" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cc-traits" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "060303ef31ef4a522737e1b1ab68c67916f2a787bb2f4f54f383279adba962b5" +dependencies = [ + "slab", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead", + "chacha20", + "cipher", + "poly1305", + "zeroize", +] + +[[package]] +name = "chrono" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-targets 0.52.6", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half 2.4.1", +] + +[[package]] +name = "cid" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ed9c8b2d17acb8110c46f1da5bf4a696d745e1474a16db0cd2b49cd0249bf2" +dependencies = [ + "core2", + "multibase 0.9.1", + "multihash", + "serde", + "serde_bytes", + "unsigned-varint", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_derive 3.2.25", + "clap_lex 0.2.4", + "indexmap 1.9.3", + "once_cell", + "strsim 0.10.0", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap" +version = "4.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6719fffa43d0d87e5fd8caeab59be1554fb028cd30edc88fc4369b17971019" +dependencies = [ + "clap_builder", + "clap_derive 4.5.13", +] + +[[package]] +name = "clap_builder" +version = "4.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216aec2b177652e3846684cbfe25c9964d18ec45234f0f5da5157b207ed1aab6" +dependencies = [ + "anstream", + "anstyle", + "clap_lex 0.7.2", + "strsim 0.11.1", +] + +[[package]] +name = "clap_derive" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae6371b8bdc8b7d3959e9cf7b22d4435ef3e79e138688421ec654acf8c81b008" +dependencies = [ + "heck 0.4.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "clap_derive" +version = "4.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "clap_lex" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97" + +[[package]] +name = "clear_on_drop" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38508a63f4979f0048febc9966fadbd48e5dab31fd0ec6a3f151bbf4a74f7423" +dependencies = [ + "cc", +] + +[[package]] +name = "cmake" +version = "0.1.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" +dependencies = [ + "cc", +] + +[[package]] +name = "colorchoice" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" + +[[package]] +name = "combination" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "337cdbf3f1a0e643b4a7d1a2ffa39d22342fb6ee25739b5cfb997c28b3586422" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "futures-core", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "contextual" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05ca71f324d19e85a2e976be04b5ecbb193253794a75adfe2e5044c8bef03f6a" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" +dependencies = [ + "memchr", +] + +[[package]] +name = "cpufeatures" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51e852e6dc9a5bed1fae92dd2375037bf2b768725bf3be87811edee3249d09ad" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap 4.5.16", + "criterion-plot", + "futures", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-bigint" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c6a1d5fa1de37e071642dfa44ec552ca5b299adb128fab16138e24b548fd21" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "rand_core", + "typenum", +] + +[[package]] +name = "crypto_box" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16182b4f39a82ec8a6851155cc4c0cda3065bb1db33651726a29e1951de0f009" +dependencies = [ + "aead", + "crypto_secretbox", + "curve25519-dalek", + "salsa20", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto_secretbox" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" +dependencies = [ + "aead", + "cipher", + "generic-array", + "poly1305", + "salsa20", + "subtle", + "zeroize", +] + +[[package]] +name = "csv" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe" +dependencies = [ + "csv-core", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70" +dependencies = [ + "memchr", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + +[[package]] +name = "darling" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" +dependencies = [ + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core 0.20.10", + "darling_macro 0.20.10", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn 1.0.109", +] + +[[package]] +name = "darling_core" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "109c1ca6e6b7f82cc233a97004ea8ed7ca123a9af07a8230878fcfda9b158bf0" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.77", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core 0.13.4", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" +dependencies = [ + "darling_core 0.14.4", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core 0.20.10", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "data-encoding" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8566979429cf69b49a5c740c60791108e86440e8be149bbea4fe54d2c32d6e2" + +[[package]] +name = "data-encoding-macro" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1559b6cba622276d6d63706db152618eeb15b89b3e4041446b05876e352e639" +dependencies = [ + "data-encoding", + "data-encoding-macro-internal", +] + +[[package]] +name = "data-encoding-macro-internal" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "332d754c0af53bc87c108fed664d121ecf59207ec4196041f04d6ab9002ad33f" +dependencies = [ + "data-encoding", + "syn 1.0.109", +] + +[[package]] +name = "deadpool" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6541a3916932fe57768d4be0b1ffb5ec7cbf74ca8c903fdfd5c0fe8aa958f0ed" +dependencies = [ + "deadpool-runtime", + "num_cpus", + "tokio", +] + +[[package]] +name = "deadpool-redis" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ed4f481f6a4770b95e09b91e183ee5ed6e1d4a34c0b09814012b3ee5e585f70" +dependencies = [ + "deadpool", + "redis", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +dependencies = [ + "tokio", +] + +[[package]] +name = "decoded-char" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5440d1dc8ea7cae44cda3c64568db29bfa2434aba51ae66a50c00488841a65a3" + +[[package]] +name = "der" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" +dependencies = [ + "const-oid 0.7.1", + "crypto-bigint 0.3.2", + "pem-rfc7468 0.3.1", +] + +[[package]] +name = "der" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468 0.7.0", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "did-ethr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0417aae3db3cdf06f9d13002337e9d148fa89e8361c08396b3373476c6ab0d3" +dependencies = [ + "hex", + "iref", + "serde_json", + "ssi-caips", + "ssi-dids-core", + "ssi-jwk", + "static-iref", + "thiserror", +] + +[[package]] +name = "did-ion" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e02db43d4a30864120023d8e65bec1a762b51efa2e17337c7e1553d83ba96a3b" +dependencies = [ + "base64 0.12.3", + "iref", + "json-patch", + "reqwest 0.11.27", + "serde", + "serde_jcs", + "serde_json", + "sha2 0.10.8", + "ssi-dids-core", + "ssi-jwk", + "ssi-jws", + "ssi-jwt", + "ssi-verification-methods", + "thiserror", +] + +[[package]] +name = "did-jwk" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defbdf936331b247c070cbc77a29470343bc1e76be09f4f78a9e6615964b697e" +dependencies = [ + "async-trait", + "iref", + "multibase 0.8.0", + "serde_jcs", + "serde_json", + "ssi-crypto", + "ssi-dids-core", + "ssi-jwk", + "ssi-verification-methods", + "static-iref", + "thiserror", +] + +[[package]] +name = "did-method-key" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b380da1739823f33e0d1df47f9c4b04ffa3746e42cdfbf2bf15a296eeac3c7d" +dependencies = [ + "bs58 0.4.0", + "iref", + "k256", + "multibase 0.9.1", + "p256", + "serde_json", + "simple_asn1 0.5.4", + "ssi-dids-core", + "ssi-json-ld", + "ssi-jwk", + "ssi-multicodec", + "static-iref", + "thiserror", +] + +[[package]] +name = "did-peer" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdaf529e4e724473ba2c051fe36eb0bcb95afa40f205fb469db42ce6e66ddc77" +dependencies = [ + "base64 0.22.1", + "iref", + "serde", + "serde-wasm-bindgen", + "serde_json", + "ssi", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", +] + +[[package]] +name = "did-pkh" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7032b64c01971dbd6db7bf13d00364221795524a950c46637e0fb7d0483dcffa" +dependencies = [ + "async-trait", + "bech32", + "bs58 0.4.0", + "chrono", + "iref", + "serde", + "serde_json", + "ssi-caips", + "ssi-crypto", + "ssi-dids-core", + "ssi-jwk", + "static-iref", + "thiserror", +] + +[[package]] +name = "did-tz" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c04e7f709c8d7e26323ac898ed4e9aa2a2628fe8c2223556ffeec01eeb42870b" +dependencies = [ + "async-trait", + "bs58 0.4.0", + "chrono", + "iref", + "json-patch", + "reqwest 0.11.27", + "serde", + "serde_json", + "ssi-core", + "ssi-dids-core", + "ssi-jwk", + "ssi-jws", + "static-iref", + "thiserror", + "url", +] + +[[package]] +name = "did-web" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6cf0ab6bab0b86cdadda93e2a60d6f231e76199536810be43fd61e22bf0e7e9" +dependencies = [ + "http 0.2.12", + "reqwest 0.11.27", + "ssi-dids-core", + "thiserror", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common", + "subtle", +] + +[[package]] +name = "document-features" +version = "0.2.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb6969eaabd2421f8a2775cfd2471a2b634372b4a25d41e3bd647b79912850a0" +dependencies = [ + "litrs", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.9", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki 0.7.3", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8 0.10.2", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand_core", + "serde", + "sha2 0.10.8", + "subtle", + "zeroize", +] + +[[package]] +name = "educe" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0042ff8246a363dbe77d2ceedb073339e85a804b9a47636c6e016a9a32c05f" +dependencies = [ + "enum-ordinalize 3.1.15", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "educe" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4bd92664bf78c4d3dba9b7cdafce6fa15b13ed3ed16175218196942e99168a8" +dependencies = [ + "enum-ordinalize 4.3.0", + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "either" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint 0.5.5", + "digest 0.10.7", + "ff 0.13.0", + "generic-array", + "group 0.13.0", + "hkdf", + "pem-rfc7468 0.7.0", + "pkcs8 0.10.2", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-ordinalize" +version = "3.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf1fa3f06bbff1ea5b1a9c7b14aa992a39657db60a2759457328d7e058f49ee" +dependencies = [ + "num-bigint", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "enum-ordinalize" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "env_logger" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" +dependencies = [ + "humantime", + "is-terminal", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "event-listener" +version = "5.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f214dc438f977e6d4e3500aaa277f5ad94ca83fbbd9b1a15713ce2344ccc5a1" +dependencies = [ + "event-listener 5.3.1", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c02a5121d4ea3eb16a80748c74f5549a5665e4c21333c6098f283870fbdea6" + +[[package]] +name = "ff" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f40b2dcd8bc322217a5f6559ae5f9e9d1de202a2ecee2e9eafcbece7562a4f" +dependencies = [ + "rand_core", + "subtle", +] + +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "bitvec 1.0.1", + "rand_core", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "fixed-hash" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "flate2" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "324a1be68054ef05ad64b861cc9eaf1d623d2d8cb25b4bf2cb9cdd902b4bf253" +dependencies = [ + "crc32fast", + "miniz_oxide 0.8.0", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "funty" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" + +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-lite" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" +dependencies = [ + "fastrand 2.1.1", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "group" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912" +dependencies = [ + "byteorder", + "ff 0.10.1", + "rand_core", + "subtle", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.0", + "rand_core", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.5.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e8ac6999421f49a846c2d4411f337e53497d8ec55d67753beffa43c5d9205" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.1.0", + "indexmap 2.5.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b43ede17f21864e81be2fa654110bf1e793774238d86ef8555c37e6519c0403" + +[[package]] +name = "half" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.11", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.11", + "allocator-api2", +] + +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +dependencies = [ + "base64 0.21.7", + "bytes", + "headers-core", + "http 1.1.0", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http 1.1.0", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hermit-abi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex_fmt" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b07f60793ff0a4d9cef0f18e63b5357e06209987153a64648c972c1e5aff336f" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "home" +version = "0.5.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "hostname" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" +dependencies = [ + "cfg-if", + "libc", + "windows", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.1.0", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fcc0b4a115bf80b728eb8ea024ad5bd707b615bfed49e0665b6e0f86fd082d9" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "hyper" +version = "0.14.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a152ddd61dfaec7273fe8419ab357f33aee0d914c5f4efbf0d96fa749eea5ec9" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.7", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.30", + "log", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee4be2c948921a1a5320b629c4193916ed787a7f7f293fd3f7f5a6c9de74155" +dependencies = [ + "futures-util", + "http 1.1.0", + "hyper 1.4.1", + "hyper-util", + "rustls 0.23.12", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.0", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.30", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper 1.4.1", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cde7055719c54e36e95e8719f95883f22072a48ede39db7fc17a4e1d5281e9b9" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http 1.1.0", + "http-body 1.0.1", + "hyper 1.4.1", + "pin-project-lite", + "socket2 0.5.7", + "tokio", + "tower", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core", + "rand_xoshiro", + "sized-chunks", + "typenum", + "version_check", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", + "serde", +] + +[[package]] +name = "indoc" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" + +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + +[[package]] +name = "iref" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374372d9ca7331cec26f307b12552554849143e6b2077be3553576aa9aa8258c" +dependencies = [ + "iref-core", +] + +[[package]] +name = "iref-core" +version = "3.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b10559a0d518effd4f2cee107f40f83acf8583dcd3e6760b9b60293b0d2c2a70" +dependencies = [ + "base64 0.22.1", + "pct-str", + "serde", + "smallvec", + "static-regular-grammar", + "thiserror", +] + +[[package]] +name = "is-terminal" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261f68e344040fbd0edea105bef17c66edf46f984ddb1115b775ce31be948f4b" +dependencies = [ + "hermit-abi 0.4.0", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "jobserver" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json-ld" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5e03a47a0118ce03ca3a843d1cc1d6d89c91c0b035dab53ed57ad07068b4a3" +dependencies = [ + "contextual", + "futures", + "iref", + "json-ld-compaction", + "json-ld-context-processing", + "json-ld-core", + "json-ld-expansion", + "json-ld-serialization", + "json-ld-syntax", + "json-syntax", + "locspan", + "rdf-types", + "thiserror", +] + +[[package]] +name = "json-ld-compaction" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fec236664e058a0a43e46ad28bbf00858a7cede40ad17ffdced395a9628a928" +dependencies = [ + "contextual", + "educe 0.4.23", + "futures", + "indexmap 2.5.0", + "iref", + "json-ld-context-processing", + "json-ld-core", + "json-ld-expansion", + "json-ld-syntax", + "json-syntax", + "langtag", + "mown", + "rdf-types", + "thiserror", +] + +[[package]] +name = "json-ld-context-processing" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aaf38d5a714ed573340f921a67c212935335e055ff2bb785337d38f6be282a4" +dependencies = [ + "contextual", + "futures", + "iref", + "json-ld-core", + "json-ld-syntax", + "mown", + "owning_ref", + "rdf-types", + "thiserror", +] + +[[package]] +name = "json-ld-core" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a82b473e2951a3cc3fed5061eb62dfbe3f984bdfb9e918214ad571674f09c8b" +dependencies = [ + "contextual", + "educe 0.4.23", + "futures", + "hashbrown 0.13.2", + "indexmap 2.5.0", + "iref", + "json-ld-syntax", + "json-syntax", + "langtag", + "linked-data", + "log", + "mime", + "once_cell", + "permutohedron", + "pretty_dtoa", + "rdf-types", + "ryu-js", + "serde", + "smallvec", + "static-iref", + "thiserror", +] + +[[package]] +name = "json-ld-expansion" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cf1f43e51f7360a889f83b2ccb075d21b8229d325f3c80764fb0bce49f1ce" +dependencies = [ + "contextual", + "educe 0.4.23", + "futures", + "indexmap 2.5.0", + "iref", + "json-ld-context-processing", + "json-ld-core", + "json-ld-syntax", + "json-syntax", + "langtag", + "mown", + "rdf-types", + "thiserror", +] + +[[package]] +name = "json-ld-serialization" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fe6e8a89c6aff8c955a6b9128be4da54f1d64b9564bcd647995610c657e2a4e" +dependencies = [ + "indexmap 2.5.0", + "iref", + "json-ld-core", + "json-syntax", + "linked-data", + "rdf-types", + "thiserror", + "xsd-types", +] + +[[package]] +name = "json-ld-syntax" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a52919be632986e14dd1dbcfe12ce4573db02bf7044a09e7ec882a1ec2af7e6" +dependencies = [ + "contextual", + "decoded-char", + "educe 0.4.23", + "hashbrown 0.13.2", + "indexmap 2.5.0", + "iref", + "json-syntax", + "langtag", + "locspan", + "rdf-types", + "serde", + "smallvec", + "thiserror", +] + +[[package]] +name = "json-number" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c54d19ae7e6fc83aafa649707655a9a0ac956a0f62793bde4cfd193b0693fdf" +dependencies = [ + "lexical", + "ryu-js", + "serde", + "smallvec", +] + +[[package]] +name = "json-patch" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3fa5a61630976fc4c353c70297f2e93f1930e3ccee574d59d618ccbd5154ce" +dependencies = [ + "serde", + "serde_json", + "treediff", +] + +[[package]] +name = "json-syntax" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "044a68aba3f96d712f492b72be25e10f96201eaaca3207a7d6e68d6d5105fda9" +dependencies = [ + "contextual", + "decoded-char", + "hashbrown 0.12.3", + "indexmap 1.9.3", + "json-number", + "locspan", + "locspan-derive", + "ryu-js", + "serde", + "smallstr", + "smallvec", + "utf8-decode", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1 0.6.2", +] + +[[package]] +name = "k256" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2 0.10.8", + "signature", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "keccak-hash" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae0386ec98c26dd721aaa3412bf3a817156ff3ee7cb6959503f8d1095f4ccc51" +dependencies = [ + "primitive-types", + "tiny-keccak", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "langtag" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecb4c689a30e48ebeaa14237f34037e300dd072e6ad21a9ec72e810ff3c6600" +dependencies = [ + "static-regular-grammar", + "thiserror", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "lexical" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7aefb36fd43fef7003334742cbf77b243fcd36418a1d1bdd480d613a67968f6" +dependencies = [ + "lexical-core", +] + +[[package]] +name = "lexical-core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f" +dependencies = [ + "lexical-parse-integer", + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-parse-integer" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9" +dependencies = [ + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-util" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lexical-write-float" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862" +dependencies = [ + "lexical-util", + "lexical-write-integer", + "static_assertions", +] + +[[package]] +name = "lexical-write-integer" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446" +dependencies = [ + "lexical-util", + "static_assertions", +] + +[[package]] +name = "libc" +version = "0.2.158" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439" + +[[package]] +name = "libipld" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9c3aa309c260aa2f174bac968901eddc546e9d85950c28eae6a7bec402f926" +dependencies = [ + "async-trait", + "cached", + "fnv", + "libipld-cbor", + "libipld-cbor-derive", + "libipld-core", + "libipld-json", + "libipld-macro", + "log", + "multihash", + "parking_lot", + "thiserror", +] + +[[package]] +name = "libipld-cbor" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8dd1ab68c9d26f20c7d0dfea6eecbae8c00359875210001b33ca27d4a02f3d09" +dependencies = [ + "byteorder", + "libipld-core", + "thiserror", +] + +[[package]] +name = "libipld-cbor-derive" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69ec2f49393a1347a2d95ebcb248ff75d0d47235919b678036c010a8cd927375" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "libipld-core" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d44790246ec6b7314cba745992c23d479d018073e66d49ae40ae1b64e5dd8eb5" +dependencies = [ + "anyhow", + "cid", + "core2", + "multibase 0.9.1", + "multihash", + "serde", + "thiserror", +] + +[[package]] +name = "libipld-json" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18aa481a87f084d98473dd9ece253a9569c762b75f6bbba8217d54e48c9d63b3" +dependencies = [ + "libipld-core", + "multihash", + "serde", + "serde_json", +] + +[[package]] +name = "libipld-macro" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852c011562ae5059b67c3a917f9f5945af5a68df8e39ede4444fff33274d25e2" +dependencies = [ + "libipld-core", +] + +[[package]] +name = "libloading" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" +dependencies = [ + "cfg-if", + "windows-targets 0.52.6", +] + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "linked-data" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04fd672ff31f4a6007acbdd33e1b65e1144081ec09b4189b3d20da3997603e90" +dependencies = [ + "educe 0.4.23", + "im", + "iref", + "json-syntax", + "linked-data-derive", + "rdf-types", + "serde", + "static-iref", + "thiserror", + "xsd-types", +] + +[[package]] +name = "linked-data-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "887a1903665b47c2dcff59682d8de1be46813819ae0337d8eaa885035762fbee" +dependencies = [ + "iref", + "proc-macro-error", + "proc-macro2", + "quote", + "static-iref", + "syn 2.0.77", + "thiserror", +] + +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + +[[package]] +name = "lock_api" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "locspan" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33890449fcfac88e94352092944bf321f55e5deb4e289a6f51c87c55731200a0" +dependencies = [ + "serde", +] + +[[package]] +name = "locspan-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88991223b049a3d29ca1f60c05639581336a0f3ee4bf8a659dddecc11c4961a" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "log" +version = "0.4.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +dependencies = [ + "value-bag", +] + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8a240ddb74feaf34a79a7add65a741f3167852fba007066dcac1ca548d89c08" +dependencies = [ + "adler", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80e04d1dcff3aae0704555fe5fee3bcfaf3d1fdf8a7e521d5b9d2b42acb52cec" +dependencies = [ + "hermit-abi 0.3.9", + "libc", + "wasi", + "windows-sys 0.52.0", +] + +[[package]] +name = "mirai-annotations" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" + +[[package]] +name = "moka" +version = "0.12.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32cf62eb4dd975d2dde76432fb1075c49e3ee2331cf36f1f8fd4b66550d32b6f" +dependencies = [ + "async-lock 3.4.0", + "async-trait", + "crossbeam-channel", + "crossbeam-epoch", + "crossbeam-utils", + "event-listener 5.3.1", + "futures-util", + "once_cell", + "parking_lot", + "quanta", + "rustc_version", + "smallvec", + "tagptr", + "thiserror", + "triomphe", + "uuid", +] + +[[package]] +name = "mown" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7627d8bbeb17edbf1c3f74b21488e4af680040da89713b4217d0010e9cbd97e" + +[[package]] +name = "multibase" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b78c60039650ff12e140ae867ef5299a58e19dded4d334c849dc7177083667e2" +dependencies = [ + "base-x", + "data-encoding", + "data-encoding-macro", +] + +[[package]] +name = "multibase" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404" +dependencies = [ + "base-x", + "data-encoding", + "data-encoding-macro", +] + +[[package]] +name = "multihash" +version = "0.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c346cf9999c631f002d8f977c4eaeaa0e6386f16007202308d0b3757522c2cc" +dependencies = [ + "blake2b_simd 1.0.2", + "blake2s_simd", + "blake3", + "core2", + "digest 0.10.7", + "multihash-derive", + "serde", + "serde-big-array", + "sha2 0.10.8", + "sha3", + "unsigned-varint", +] + +[[package]] +name = "multihash-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6d4752e6230d8ef7adf7bd5d8c4b1f6561c1014c5ba9a37445ccefe18aa1db" +dependencies = [ + "proc-macro-crate", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] + +[[package]] +name = "native-tls" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint-dig" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151" +dependencies = [ + "byteorder", + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand", + "smallvec", + "zeroize", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi 0.3.9", + "libc", +] + +[[package]] +name = "object" +version = "0.36.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" + +[[package]] +name = "oorandom" +version = "11.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl" +version = "0.10.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" +dependencies = [ + "bitflags 2.6.0", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "300.3.1+3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7259953d42a81bf137fbbd73bd30a8e1914d6dce43c2b90ed575783a22608b91" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" +dependencies = [ + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-float" +version = "3.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1e1c390732d15f1d48471625cd92d154e66db2c56645e29a9cd26f4699f72dc" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-float" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a91171844676f8c7990ce64959210cd2eaef32c2612c50f9fae9f8aaa6065a6" +dependencies = [ + "num-traits", + "rand", + "serde", +] + +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + +[[package]] +name = "outref" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4030760ffd992bef45b0ae3f10ce1aba99e33464c90d14dd7c039884963ddc7a" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "owning_ref" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ff55baddef9e4ad00f88b6c743a2a8062d4c6ade126c2a528644b8e444d52ce" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.8", +] + +[[package]] +name = "p384" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70786f51bcc69f6a4c0360e063a4cac5419ef7c5cd5b3c99ad70f3be5ba79209" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.8", +] + +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group 0.13.0", +] + +[[package]] +name = "parking" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb813b8af86854136c6922af0598d719255ecb2179515e6e7730d468f05c9cae" + +[[package]] +name = "parking_lot" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pct-str" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf1bdcc492c285a50bed60860dfa00b50baf1f60c73c7d6b435b01a2a11fd6ff" +dependencies = [ + "thiserror", + "utf8-decode", +] + +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + +[[package]] +name = "pem-rfc7468" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01de5d978f34aa4b2296576379fcc416034702fd94117c56ffd8a1a767cefb30" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "permutohedron" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b687ff7b5da449d39e418ad391e5e08da53ec334903ddbb921db208908fc372c" + +[[package]] +name = "pin-project" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bf43b791c5b9e34c3d182969b4abb522f9343702850a2e57f460d00d09b4b3" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "piper" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066" +dependencies = [ + "atomic-waker", + "fastrand 2.1.1", + "futures-io", +] + +[[package]] +name = "pkcs1" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a78f66c04ccc83dd4486fd46c33896f4e17b24a7a3a6400dedc48ed0ddd72320" +dependencies = [ + "der 0.5.1", + "pkcs8 0.8.0", + "zeroize", +] + +[[package]] +name = "pkcs8" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" +dependencies = [ + "der 0.5.1", + "spki 0.5.4", + "zeroize", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.9", + "spki 0.7.3", +] + +[[package]] +name = "pkg-config" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "plotters" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15b6eccb8484002195a3e44fe65a4ce8e93a625797a063735536fd59cb01cf3" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "414cec62c6634ae900ea1c56128dfe87cf63e7caece0852ec76aba307cebadb7" + +[[package]] +name = "plotters-svg" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81b30686a7d9c3e010b84284bdd26a29f2138574f52f5eb6f794fc0ad924e705" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "polling" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" +dependencies = [ + "autocfg", + "bitflags 1.3.2", + "cfg-if", + "concurrent-queue", + "libc", + "log", + "pin-project-lite", + "windows-sys 0.48.0", +] + +[[package]] +name = "polling" +version = "3.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2790cd301dec6cd3b7a025e4815cf825724a51c98dccfe6a3e55f05ffb6511" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi 0.4.0", + "pin-project-lite", + "rustix 0.38.35", + "tracing", + "windows-sys 0.59.0", +] + +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "pretty_dtoa" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a239bcdfda2c685fda1add3b4695c06225f50075e3cfb5b954e91545587edff2" +dependencies = [ + "ryu_floating_decimal", +] + +[[package]] +name = "prettyplease" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479cf940fbbb3426c32c5d5176f62ad57549a0bb84773423ba8be9d089f5faba" +dependencies = [ + "proc-macro2", + "syn 2.0.77", +] + +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "primitive-types" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06345ee39fbccfb06ab45f3a1a5798d9dafa04cb8921a76d227040003a234b0e" +dependencies = [ + "fixed-hash", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" +dependencies = [ + "thiserror", + "toml 0.5.11", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quanta" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5167a477619228a0b284fac2674e3c388cba90631d7b7de620e6f1fcd08da5" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + +[[package]] +name = "quinn" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b22d8e7369034b9a7132bc2008cac12f2013c8132b45e0554e6e20e2617f2156" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash 2.0.0", + "rustls 0.23.12", + "socket2 0.5.7", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba92fb39ec7ad06ca2582c0ca834dfeadcaf06ddfc8e635c80aa7e1c05315fdd" +dependencies = [ + "bytes", + "rand", + "ring", + "rustc-hash 2.0.0", + "rustls 0.23.12", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bffec3605b73c6f1754535084a85229fa8a30f86014e6c81aeec4abb68b0285" +dependencies = [ + "libc", + "once_cell", + "socket2 0.5.7", + "tracing", + "windows-sys 0.52.0", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "643f8f41a8ebc4c5dc4515c82bb8abd397b527fc20fd681b7c011c2aee5d44fb" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", + "serde", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core", +] + +[[package]] +name = "range-traits" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d20581732dd76fa913c7dff1a2412b714afe3573e94d41c34719de73337cc8ab" + +[[package]] +name = "raw-btree" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a9a77e61cd9f37af08f952c1c072081563e129c0949ac4ede30f9e8b6b4b74f" + +[[package]] +name = "raw-cpuid" +version = "11.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb9ee317cfe3fbd54b36a511efc1edd42e216903c9cd575e686dd68a2ba90d8d" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rcgen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54077e1872c46788540de1ea3d7f4ccb1983d12f9aa909b234468676c1a36779" +dependencies = [ + "aws-lc-rs", + "pem", + "rustls-pki-types", + "time", + "yasna", +] + +[[package]] +name = "rdf-types" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8d685353ee1b343c708b6be7751d98a7df8e1d0caaeee67c754318854c01d5" +dependencies = [ + "contextual", + "educe 0.5.11", + "indexmap 2.5.0", + "iref", + "langtag", + "raw-btree", + "replace_with", + "slab", + "static-iref", + "thiserror", +] + +[[package]] +name = "redis" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e902a69d09078829137b4a5d9d082e0490393537badd7c91a3d69d14639e115f" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "combine", + "futures-util", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "rustls 0.23.12", + "rustls-native-certs 0.7.3", + "rustls-pemfile 2.1.3", + "rustls-pki-types", + "ryu", + "sha1_smol", + "socket2 0.5.7", + "tokio", + "tokio-rustls 0.26.0", + "tokio-util", + "url", +] + +[[package]] +name = "redox_syscall" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a908a6e00f1fdd0dfd9c0eb08ce85126f6d8bbda50017e74bc4a4b7d4a926a4" +dependencies = [ + "bitflags 2.6.0", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.7", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.4", +] + +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "replace_with" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a8614ee435691de62bcffcf4a66d91b3594bf1428a5722e79103249a095690" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.26", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.30", + "hyper-rustls 0.24.2", + "hyper-tls 0.5.0", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls 0.21.12", + "rustls-pemfile 1.0.4", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration 0.5.1", + "tokio", + "tokio-native-tls", + "tokio-rustls 0.24.1", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", +] + +[[package]] +name = "reqwest" +version = "0.12.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.4.6", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "hyper 1.4.1", + "hyper-rustls 0.27.2", + "hyper-tls 0.6.0", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls 0.23.12", + "rustls-pemfile 2.1.3", + "rustls-pki-types", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "system-configuration 0.6.1", + "tokio", + "tokio-native-tls", + "tokio-rustls 0.26.0", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "ripemd160" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eca4ecc81b7f313189bf73ce724400a07da2a6dac19588b03c8bd76a2dcc251" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "rsa" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cf22754c49613d2b3b119f0e5d46e34a2c628a937e3024b8762de4e7d8c710b" +dependencies = [ + "byteorder", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-iter", + "num-traits", + "pkcs1", + "pkcs8 0.8.0", + "rand_core", + "smallvec", + "subtle", + "zeroize", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + +[[package]] +name = "rustix" +version = "0.38.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys 0.4.14", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" +dependencies = [ + "aws-lc-rs", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki 0.102.7", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile 1.0.4", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-native-certs" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.1.3", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a" +dependencies = [ + "openssl-probe", + "rustls-pemfile 2.1.3", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" +dependencies = [ + "base64 0.22.1", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted 0.9.0", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84678086bd54edf2b415183ed7a94d0efb049f1b646a33e22a36f3794be6ae56" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + +[[package]] +name = "ryu" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" + +[[package]] +name = "ryu-js" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6518fc26bced4d53678a22d6e423e9d8716377def84545fe328236e3af070e7f" + +[[package]] +name = "ryu_floating_decimal" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "700de91d5fd6091442d00fdd9ee790af6d4f0f480562b0f5a1e8f59e90aafe73" + +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted 0.9.0", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der 0.7.9", + "generic-array", + "pkcs8 0.10.2", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "self_cell" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d369a96f978623eb3dc28807c4852d6cc617fed53da5d3c400feff1ef34a714a" + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde-attributes" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eb8ec7724e4e524b2492b510e66957fe1a2c76c26a6975ec80823f2439da685" +dependencies = [ + "darling_core 0.14.4", + "serde-rename-rule", + "syn 1.0.109", +] + +[[package]] +name = "serde-big-array" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd31f59f6fe2b0c055371bb2f16d7f0aa7d8881676c04a55b1596d1a17cd10a4" +dependencies = [ + "serde", +] + +[[package]] +name = "serde-enum-str" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26416dc95fcd46b0e4b12a3758043a229a6914050aaec2e8191949753ed4e9aa" +dependencies = [ + "darling 0.14.4", + "proc-macro2", + "quote", + "serde-attributes", + "syn 1.0.109", +] + +[[package]] +name = "serde-json-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c9e1ab533c0bc414c34920ec7e5f097101d126ed5eac1a1aac711222e0bbb33" +dependencies = [ + "ryu", + "serde", +] + +[[package]] +name = "serde-rename-rule" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "794e44574226fc701e3be5c651feb7939038fc67fb73f6f4dd5c4ba90fd3be70" + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_bytes" +version = "0.11.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "387cc504cb06bb40a96c8e04e951fe01854cf6bc921053c954e4a606d9675c6a" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half 1.8.3", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.209" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "serde_jcs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cacecf649bc1a7c5f0e299cc813977c6a78116abda2b93b1ee01735b71ead9a8" +dependencies = [ + "ryu-js", + "serde", + "serde_json", +] + +[[package]] +name = "serde_json" +version = "1.0.127" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_spanned" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb5b1b31579f3811bf615c144393417496f152e12ac8b7663bf664f4a815306d" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" +dependencies = [ + "base64 0.13.1", + "serde", + "serde_with_macros 1.5.2", +] + +[[package]] +name = "serde_with" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe" +dependencies = [ + "base64 0.13.1", + "chrono", + "hex", + "indexmap 1.9.3", + "serde", + "serde_json", + "serde_with_macros 2.3.3", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling 0.13.4", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "serde_with_macros" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f" +dependencies = [ + "darling 0.20.10", + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha256" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18278f6a914fa3070aa316493f7d2ddfb9ac86ebc06fa3b83bffda487e9065b0" +dependencies = [ + "async-trait", + "bytes", + "hex", + "sha2 0.10.8", + "tokio", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core", +] + +[[package]] +name = "simple_asn1" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eb4ea60fb301dc81dfc113df680571045d375ab7345d171c5dc7d7e13107a80" +dependencies = [ + "chrono", + "num-bigint", + "num-traits", + "thiserror", +] + +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time", +] + +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallstr" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b1aefdf380735ff8ded0b15f31aab05daf1f70216c01c02a12926badd1df9d" +dependencies = [ + "serde", + "smallvec", +] + +[[package]] +name = "smallvec" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" + +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "socket2" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce305eb0b4296696835b71df73eb912e0f1ffd2556a501fcede6e0c50349191c" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" +dependencies = [ + "base64ct", + "der 0.5.1", +] + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.9", +] + +[[package]] +name = "sshkeys" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45287473d24bf7ad9ebad1aff097ad0424c16cd9430549170c3a67c5b05705bd" +dependencies = [ + "base64 0.22.1", + "byteorder", + "sha2 0.10.8", +] + +[[package]] +name = "ssi" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8889299d60ee48b92907821a37eb93254aa41f704144403670f2f9c4344ec42e" +dependencies = [ + "document-features", + "ssi-caips", + "ssi-claims", + "ssi-core", + "ssi-crypto", + "ssi-dids", + "ssi-eip712", + "ssi-json-ld", + "ssi-jwk", + "ssi-jws", + "ssi-multicodec", + "ssi-rdf", + "ssi-security", + "ssi-ssh", + "ssi-status", + "ssi-ucan", + "ssi-verification-methods", + "ssi-zcap-ld", + "xsd-types", +] + +[[package]] +name = "ssi-bbs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbca03cf1d1cb0253f58cd47e843bdf2ad3626dc2a4a1a831684945507c1a" +dependencies = [ + "rand", + "ssi-claims-core", + "ssi-crypto", + "zkryptium", +] + +[[package]] +name = "ssi-caips" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01098e02eb5601a1bca45ce4d78d659505350effdc7a5df7bff860182bd78a85" +dependencies = [ + "bs58 0.4.0", + "linked-data", + "serde", + "ssi-jwk", + "thiserror", + "xsd-types", +] + +[[package]] +name = "ssi-claims" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95e16edaf82196adec53c4fa06a401c84e332623b19b8b5bfc74b327d135eee" +dependencies = [ + "educe 0.4.23", + "iref", + "json-syntax", + "linked-data", + "locspan", + "pin-project", + "rdf-types", + "serde", + "serde_json", + "ssi-claims-core", + "ssi-core", + "ssi-crypto", + "ssi-data-integrity", + "ssi-dids-core", + "ssi-eip712", + "ssi-json-ld", + "ssi-jwk", + "ssi-jws", + "ssi-jwt", + "ssi-sd-jwt", + "ssi-security", + "ssi-vc", + "ssi-vc-jose-cose", + "ssi-verification-methods", + "thiserror", +] + +[[package]] +name = "ssi-claims-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ca10ead311b434b7ec0055575fe839d7c7c20fb62fafbf05e989b9572795909" +dependencies = [ + "chrono", + "educe 0.4.23", + "serde", + "ssi-core", + "ssi-crypto", + "ssi-eip712", + "ssi-json-ld", + "thiserror", +] + +[[package]] +name = "ssi-contexts" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728dde2f51db62c4667686139f2958b59ab920f8d9d8531b59fc5a01ef0a3896" + +[[package]] +name = "ssi-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a9bcf4043b59fdb6990c9fb34e6e613f457d0c1341f8c017a9feb3082365e8" +dependencies = [ + "async-trait", + "pin-project", + "serde", + "thiserror", +] + +[[package]] +name = "ssi-crypto" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7bc78e7fc34a598ce6a1f0e4693db6b936512a7e7ff6e9f12231546e95a3408" +dependencies = [ + "async-trait", + "bs58 0.4.0", + "digest 0.9.0", + "getrandom", + "hex", + "iref", + "k256", + "keccak-hash", + "pin-project", + "ripemd160", + "serde", + "sha2 0.10.8", + "static-iref", + "thiserror", + "zeroize", +] + +[[package]] +name = "ssi-data-integrity" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c01440ec2362473c57d554166543efbe907df1dcbe6249b350d303d6d3d3f2" +dependencies = [ + "chrono", + "iref", + "json-syntax", + "linked-data", + "rdf-types", + "serde", + "serde_json", + "ssi-claims-core", + "ssi-core", + "ssi-crypto", + "ssi-data-integrity-core", + "ssi-data-integrity-suites", + "ssi-di-sd-primitives", + "ssi-eip712", + "ssi-json-ld", + "ssi-jwk", + "ssi-jws", + "ssi-rdf", + "ssi-security", + "ssi-verification-methods", + "thiserror", +] + +[[package]] +name = "ssi-data-integrity-core" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b041f6a250551a018e3efabf5c404c4f5d6682edcabd0e6b930baf7a3d3c94b6" +dependencies = [ + "chrono", + "contextual", + "derivative", + "digest 0.10.7", + "educe 0.4.23", + "futures", + "hex", + "iref", + "json-syntax", + "linked-data", + "locspan", + "multibase 0.9.1", + "rdf-types", + "self_cell", + "serde", + "serde_json", + "ssi-claims-core", + "ssi-core", + "ssi-crypto", + "ssi-json-ld", + "ssi-jwk", + "ssi-jws", + "ssi-rdf", + "ssi-security", + "ssi-verification-methods", + "static-iref", + "thiserror", + "xsd-types", +] + +[[package]] +name = "ssi-data-integrity-suites" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deab6070a99ecaff8a3a4168859582bb52d4279633cecd5788b9858a27f041aa" +dependencies = [ + "async-trait", + "base64 0.12.3", + "chrono", + "contextual", + "derivative", + "educe 0.4.23", + "futures", + "getrandom", + "hex", + "iref", + "json-syntax", + "k256", + "lazy_static", + "linked-data", + "locspan", + "multibase 0.9.1", + "p256", + "pin-project", + "rand", + "rdf-types", + "self_cell", + "serde", + "serde_cbor", + "serde_json", + "ssi-bbs", + "ssi-caips", + "ssi-claims-core", + "ssi-contexts", + "ssi-core", + "ssi-crypto", + "ssi-data-integrity-core", + "ssi-di-sd-primitives", + "ssi-eip712", + "ssi-json-ld", + "ssi-jwk", + "ssi-jws", + "ssi-multicodec", + "ssi-rdf", + "ssi-security", + "ssi-verification-methods", + "static-iref", + "thiserror", + "xsd-types", +] + +[[package]] +name = "ssi-di-sd-primitives" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d82a3693551b8363c1ce74ce4e35ce51244495da0a1e80fd94f881c871e5674" +dependencies = [ + "base64 0.12.3", + "digest 0.10.7", + "getrandom", + "hex", + "hmac", + "iref", + "linked-data", + "rdf-types", + "serde", + "sha2 0.10.8", + "ssi-json-ld", + "ssi-rdf", + "thiserror", + "uuid", +] + +[[package]] +name = "ssi-dids" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60c37820fbb7df6b202817d43781c8c9488ed88a127e0b4ffa73ce70b507f407" +dependencies = [ + "did-ethr", + "did-ion", + "did-jwk", + "did-method-key", + "did-pkh", + "did-tz", + "did-web", + "ssi-dids-core", + "ssi-jwk", + "thiserror", +] + +[[package]] +name = "ssi-dids-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b00e795981717258410174f3d31157846d13271e2251766b59c802b9553bd2f" +dependencies = [ + "async-trait", + "iref", + "percent-encoding", + "pin-project", + "reqwest 0.11.27", + "serde", + "serde_json", + "serde_urlencoded", + "ssi-claims-core", + "ssi-core", + "ssi-crypto", + "ssi-json-ld", + "ssi-jwk", + "ssi-jws", + "ssi-verification-methods-core", + "static-iref", + "thiserror", +] + +[[package]] +name = "ssi-eip712" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54107b8d19db3e8c7e65d04910a118172d636ecd1819f4691fa6c0b2428d62bc" +dependencies = [ + "hex", + "indexmap 2.5.0", + "iref", + "json-syntax", + "keccak-hash", + "linked-data", + "rdf-types", + "serde", + "serde_jcs", + "serde_json", + "thiserror", +] + +[[package]] +name = "ssi-json-ld" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15dd9afa49d9c980304622d0d548f7b7f6e42bf5212504caff55e5e0af218c45" +dependencies = [ + "async-std", + "combination", + "futures", + "iref", + "json-ld", + "json-syntax", + "lazy_static", + "linked-data", + "locspan", + "rdf-types", + "serde", + "ssi-contexts", + "ssi-crypto", + "ssi-rdf", + "static-iref", + "thiserror", + "xsd-types", +] + +[[package]] +name = "ssi-jwk" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9548df4ddf50fe2ce3ad91f8505d1babf0e18cfc28b7b3d0e5d096990764d195" +dependencies = [ + "base64 0.12.3", + "blake2b_simd 0.5.11", + "bs58 0.4.0", + "ed25519-dalek", + "getrandom", + "json-syntax", + "k256", + "lazy_static", + "linked-data", + "multibase 0.9.1", + "num-bigint", + "num-derive", + "num-traits", + "p256", + "rand", + "rsa", + "serde", + "serde_jcs", + "serde_json", + "simple_asn1 0.5.4", + "ssi-claims-core", + "ssi-crypto", + "ssi-multicodec", + "thiserror", + "zeroize", +] + +[[package]] +name = "ssi-jws" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcacab32207ac1ba86912e735cc4c46d7f66a0cbfbe82a10adcfa248927c44c7" +dependencies = [ + "base64 0.12.3", + "blake2", + "clear_on_drop", + "ed25519-dalek", + "hex", + "iref", + "k256", + "linked-data", + "p256", + "rand", + "rsa", + "serde", + "serde_json", + "sha2 0.10.8", + "sha3", + "ssi-claims-core", + "ssi-core", + "ssi-crypto", + "ssi-jwk", + "thiserror", +] + +[[package]] +name = "ssi-jwt" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa87eaf3b943eed8185ebb94b0b5a462c8ebc6b27fff0c4e0c474d51ee369a39" +dependencies = [ + "async-trait", + "chrono", + "hashbrown 0.14.5", + "iref", + "json-syntax", + "ordered-float 4.2.2", + "serde", + "serde_json", + "serde_with 2.3.3", + "slab", + "ssi-claims-core", + "ssi-core", + "ssi-crypto", + "ssi-jwk", + "ssi-jws", + "thiserror", +] + +[[package]] +name = "ssi-multicodec" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fec8cbdadfc90aec4839fd79f03aa887364f5f210c3aecbf9c6f7d76ab79055" +dependencies = [ + "csv", + "ed25519-dalek", + "k256", + "p256", + "p384", + "thiserror", + "unsigned-varint", +] + +[[package]] +name = "ssi-rdf" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a07a869c976c5c84c1fb605a95a153da92a3a0472f4233ea776282f27ef15b11" +dependencies = [ + "combination", + "indexmap 2.5.0", + "iref", + "linked-data", + "rdf-types", + "serde", + "ssi-crypto", +] + +[[package]] +name = "ssi-sd-jwt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78be0bc0774fc8409e8820b94e0eb4bef3c168f7791ac4e28d379cb135708a03" +dependencies = [ + "base64 0.12.3", + "rand", + "serde", + "serde_json", + "sha2 0.10.8", + "ssi-jwk", + "ssi-jws", + "ssi-jwt", + "thiserror", +] + +[[package]] +name = "ssi-security" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f870a28eabccd4ae0e807122859bb83e36b87dd096ba01215abce524bddfc247" +dependencies = [ + "iref", + "linked-data", + "multibase 0.9.1", + "serde", + "static-iref", + "xsd-types", +] + +[[package]] +name = "ssi-ssh" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f475a6cd9c30cbe61c556b03fb26e9e82075c3ba28e27a2da360ac778a00b4a" +dependencies = [ + "sshkeys", + "ssi-jwk", + "thiserror", +] + +[[package]] +name = "ssi-status" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d72aecae429a1b88a22cd1caf90c61f24eafc97432e67a24eb8d2e09fde0eb0" +dependencies = [ + "base64 0.12.3", + "flate2", + "iref", + "log", + "multibase 0.9.1", + "parking_lot", + "rdf-types", + "reqwest 0.12.7", + "serde", + "serde_json", + "ssi-claims-core", + "ssi-data-integrity", + "ssi-json-ld", + "ssi-jwk", + "ssi-jws", + "ssi-jwt", + "ssi-vc", + "ssi-verification-methods", + "thiserror", + "xsd-types", +] + +[[package]] +name = "ssi-ucan" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc87074ce94abeadbe67b7b5863b398f065b19b65b7cb875d70cbe853b91cc5d" +dependencies = [ + "base64 0.12.3", + "bs58 0.4.0", + "chrono", + "hex", + "iref", + "libipld", + "multibase 0.9.1", + "serde", + "serde_json", + "serde_with 1.14.0", + "ssi-caips", + "ssi-core", + "ssi-dids-core", + "ssi-jwk", + "ssi-jws", + "ssi-jwt", + "ssi-verification-methods", + "thiserror", +] + +[[package]] +name = "ssi-vc" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b934a74bd79137a4084aff2c0eafc5f0f3468ea4058032db44d3686cceaa0bd" +dependencies = [ + "base64 0.12.3", + "bitvec 0.20.4", + "chrono", + "educe 0.4.23", + "flate2", + "iref", + "json-syntax", + "linked-data", + "rdf-types", + "reqwest 0.11.27", + "serde", + "serde_json", + "ssi-claims-core", + "ssi-core", + "ssi-data-integrity", + "ssi-dids-core", + "ssi-json-ld", + "ssi-jwt", + "ssi-rdf", + "ssi-verification-methods", + "static-iref", + "thiserror", + "xsd-types", +] + +[[package]] +name = "ssi-vc-jose-cose" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e7fdc17c83808bf91c47647a40b9e91af052ee930da77f3336536f3250f35e1" +dependencies = [ + "serde", + "serde_json", + "ssi-claims-core", + "ssi-json-ld", + "ssi-jws", + "ssi-vc", + "thiserror", + "xsd-types", +] + +[[package]] +name = "ssi-verification-methods" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc89660a8bcf21a153792b846c1431e13544e4892660a6863f092bb5f2556d79" +dependencies = [ + "async-trait", + "derivative", + "ed25519-dalek", + "educe 0.4.23", + "futures", + "hex", + "iref", + "json-syntax", + "k256", + "linked-data", + "multibase 0.9.1", + "p256", + "pin-project", + "rand_core", + "rdf-types", + "serde", + "serde_json", + "sha2 0.10.8", + "sha3", + "ssi-caips", + "ssi-claims-core", + "ssi-core", + "ssi-crypto", + "ssi-eip712", + "ssi-jwk", + "ssi-jws", + "ssi-multicodec", + "ssi-security", + "ssi-verification-methods-core", + "static-iref", + "thiserror", +] + +[[package]] +name = "ssi-verification-methods-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a96a8b0881b0198076ceb078b48e02bf83fe42f1a0e1c500c0546218cf964d6" +dependencies = [ + "bs58 0.4.0", + "educe 0.4.23", + "hex", + "iref", + "linked-data", + "multibase 0.9.1", + "rdf-types", + "serde", + "serde_json", + "ssi-claims-core", + "ssi-core", + "ssi-crypto", + "ssi-json-ld", + "ssi-jwk", + "ssi-jws", + "static-iref", + "thiserror", +] + +[[package]] +name = "ssi-zcap-ld" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f39422229a20f7f26cad86fdc277cb165786695cbf1b389398a0f777a39333" +dependencies = [ + "async-trait", + "iref", + "json-syntax", + "rdf-types", + "serde", + "serde_json", + "ssi-claims", + "ssi-core", + "ssi-dids-core", + "ssi-eip712", + "ssi-json-ld", + "ssi-jwk", + "ssi-rdf", + "ssi-verification-methods", + "static-iref", + "thiserror", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static-iref" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cc4068497ae43896d41174586dcdc2153a1af2c82856fb308bfaaddc28e5549" +dependencies = [ + "iref", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "static-regular-grammar" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f4a6c40247579acfbb138c3cd7de3dab113ab4ac6227f1b7de7d626ee667957" +dependencies = [ + "abnf", + "btree-range-map", + "ciborium", + "hex_fmt", + "indoc", + "proc-macro-error", + "proc-macro2", + "quote", + "serde", + "sha2 0.10.8", + "syn 2.0.77", + "thiserror", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys 0.5.0", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "system-configuration-sys 0.6.0", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tagptr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b2093cf4c8eb1e67749a6762251bc9cd836b6fc171623bd0a9d324d37af2417" + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" +dependencies = [ + "cfg-if", + "fastrand 2.1.1", + "once_cell", + "rustix 0.38.35", + "windows-sys 0.59.0", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9" + +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "time" +version = "0.3.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445e881f4f6d382d5f27c034e25eb92edd7c784ceab92a0937db7f2e9471b938" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.5.7", + "tokio-macros", + "windows-sys 0.52.0", +] + +[[package]] +name = "tokio-macros" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7bc40d0e5a97695bb96e27995cd3a08538541b0a846f65bba7a359f36700d4" +dependencies = [ + "rustls 0.23.12", + "rustls-pki-types", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c83b561d025642014097b66e6c1bb422783339e0909e4429cde4749d1990bc38" +dependencies = [ + "futures-util", + "log", + "tokio", + "tungstenite 0.21.0", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" +dependencies = [ + "futures-util", + "log", + "rustls 0.23.12", + "rustls-native-certs 0.7.3", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.0", + "tungstenite 0.23.0", +] + +[[package]] +name = "tokio-util" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml" +version = "0.8.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1ed1f98e3fdc28d6d910e6737ae6ab1a93bf1985935a1193e68f93eeb68d24e" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583c44c02ad26b0c3f3066fe629275e50627026c51ac2e595cca4c230ce1ce1d" +dependencies = [ + "indexmap 2.5.0", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.6.0", + "bytes", + "http 1.1.0", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "tracing-test" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "557b891436fe0d5e0e363427fc7f217abf9ccd510d5136549847bdcbcd011d68" +dependencies = [ + "tracing-core", + "tracing-subscriber", + "tracing-test-macro", +] + +[[package]] +name = "tracing-test-macro" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04659ddb06c87d233c566112c1c9c5b9e98256d9af50ec3bc9c8327f873a7568" +dependencies = [ + "quote", + "syn 2.0.77", +] + +[[package]] +name = "treediff" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "761e8d5ad7ce14bb82b7e61ccc0ca961005a275a060b9644a2431aa11553c2ff" +dependencies = [ + "serde_json", +] + +[[package]] +name = "triomphe" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859eb650cfee7434994602c3a68b25d77ad9e68c8a6cd491616ef86661382eb3" + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ef1a641ea34f399a848dea702823bbecfb4c486f911735368f1f137cb8257e1" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand", + "sha1", + "thiserror", + "url", + "utf-8", +] + +[[package]] +name = "tungstenite" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand", + "rustls 0.23.12", + "rustls-pki-types", + "sha1", + "thiserror", + "utf-8", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "unicode-normalization" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" + +[[package]] +name = "unicode-xid" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" + +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + +[[package]] +name = "unsigned-varint" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6889a77d49f1f013504cec6bf97a2c730394adedaeb1deb5ea08949a50541105" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-decode" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca61eb27fa339aa08826a29f03e87b99b4d8f0fc2255306fd266bb1b6a9de498" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +dependencies = [ + "getrandom", + "rand", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "value-bag" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101" + +[[package]] +name = "varint" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c37be5674e0be9c72790681999b0e6f09f03d3fc5d8904cfe2db615aad65887b" +dependencies = [ + "bit_utils", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5" +dependencies = [ + "cfg-if", + "once_cell", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.77", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484" + +[[package]] +name = "web-sys" +version = "0.3.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.35", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.6.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68a9bda4691f099d435ad181000724da8e5899daa10713c2d432552b9ccd3a6f" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wyz" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85e60b0d1b5f99db2556934e21937020776a5d31520bf169e851ac44e6420214" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core", + "zeroize", +] + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "xsd-types" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de477815bf53cc3ed3156443b7a83ff1e85b81d67df415b791f9cf830f5f4eda" +dependencies = [ + "chrono", + "iref", + "lazy_static", + "num-bigint", + "num-rational", + "num-traits", + "once_cell", + "ordered-float 3.9.2", + "pretty_dtoa", + "serde", + "static-iref", + "static-regular-grammar", + "thiserror", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "zkryptium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c734c171ed591a19dc1127351eb1b4d91864d3e53b2b6e9992bffcb7febf364a" +dependencies = [ + "bls12_381_plus", + "cargo-license", + "digest 0.10.7", + "dotenv", + "elliptic-curve", + "env_logger", + "ff 0.13.0", + "group 0.10.0", + "hex", + "hkdf", + "log", + "rand", + "serde", + "serde_json", + "sha2 0.10.8", + "sha3", + "thiserror", + "zeroize", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..207eb3b --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[workspace] +members = [ + "affinidi-messaging-sdk", + "affinidi-messaging-mediator", + "affinidi-messaging-processor", + "affinidi-messaging-didcomm", +] +resolver = "2" + +[workspace.package] +version = "0.1.0" +edition = "2021" +authors = ["Glenn Gore "] +description = "Affinidi Trust Network - Affinidi Trusted Messaging" +readme = "README.md" +homepage = "https://affinidi.com/" +keywords = ["did", "ssi", "ATM"] +publish = false +license = "Apache-2.0" + +[workspace.dependencies] +affinidi-did-resolver-cache-sdk = { version = "0.1.5" } +did-peer = { version = "0.1.5" } +clap = { version = "4.5", features = ["derive"] } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d51557b --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [2024] [Affinidi Pte. Ltd.] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/NOTICE.txt b/NOTICE.txt new file mode 100644 index 0000000..8b88c74 --- /dev/null +++ b/NOTICE.txt @@ -0,0 +1 @@ +This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the this notice when they are further distributing this code. diff --git a/README.md b/README.md index f6580fe..6cfbef9 100644 --- a/README.md +++ b/README.md @@ -1 +1,52 @@ -# affinidi-messaging +# Affinidi Messaging + +[[_TOC_]] + +## Overview + +A secure, private and trusted messaging framework based on DIDComm Messaging protocol. + +DIDComm Messaging protocol is built on top of the decentralised design of a Decentralised Identifier (DID) for secure and privacy-preserving digital communication. By following the DID design, it utilities public key cryptography to ensure the secure and private transport of messages to the intended recipient, establishing trust. + +This messaging framework is built using [Rust](https://www.rust-lang.org/) language. + +> **IMPORTANT:** +> Affinidi Messaging is provided "as is" without any warranties or guarantees, and by using this framework, users agree to assume all risks associated with its deployment and use including implementing security, and privacy measures in their applications. Affinidi assumes no liability for any issues arising from the use or modification of the project. + +## Crate Structure + +`affinidi-messaging` is the overall crate. It currently has the following sub-crates embedded in it: + +- **affinidi-messaging-sdk** - a Software Development Kit (SDK) to simplify the implementation of Affinidi Messaging into your application. +- **affinidi-messaging-mediator** - Affinidi Messaging Mediator Service is used for message handling and relaying. +- **affinidi-messaging-processor** - Affinidi Messaging Processor and Management Service. +- **affinidi-messaging-didcomm** - Affinidi Messaging DIDComm implementation, a modified version of [didcomm-rust](https://github.com/sicpa-dlab/didcomm-rust) project. + +It also depends on external Affinidi crates: + +- [affinidi-did-resolver-cache-sdk](https://crates.io/crates/affinidi-did-resolver-cache-sdk) +- [did-peer](https://crates.io/crates/did-peer) + +both sourced [here](https://github.com/affinidi/affinidi-did-resolver). + +## Prerequisites + +Refer to [affinidi-messaging-mediator - Prerequisites](./affinidi-messaging-mediator#prerequisites). + +## Running affinidi-messaging-mediator service + +Refer to [affinidi-messaging-mediator - Running affinidi-messaging-mediator service](./affinidi-messaging-mediator#running-affinidi-messaging-mediator-service). + +## Examples + +Go to the [affinidi-messaging-sdk example](./affinidi-messaging-sdk/examples/) crate to run the available sample codes and learn more about Affinidi Messaging. + +## Support & Feedback + +If you face any issues or have suggestions, please don't hesitate to contact us using [this link](https://www.affinidi.com/get-in-touch). + +### Reporting Technical Issues + +If you have a technical issue with the Affinidi Messaging GitHub repo, you can also create an issue directly in GitHub. + +If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/affinidi/affinidi-messaging/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..4cafa3e --- /dev/null +++ b/SECURITY.md @@ -0,0 +1 @@ +To report a security issue, please email with a description of the issue, the steps you took to create the issue, affected versions, and, if known, mitigations for the issue. Our vulnerability management team will respond within 5 working days of your email. If the issue is confirmed as a vulnerability, we will open a Security Advisory. This project follows a 120 day disclosure timeline. diff --git a/affinidi-messaging-didcomm/.github/workflows/release.yml b/affinidi-messaging-didcomm/.github/workflows/release.yml new file mode 100644 index 0000000..10209d0 --- /dev/null +++ b/affinidi-messaging-didcomm/.github/workflows/release.yml @@ -0,0 +1,281 @@ +name: release + +on: + push: + branches: + - stable + workflow_dispatch: + inputs: + devN: + description: 'development release number' + required: false + default: '0' + +env: + PKG_NAME: didcomm + PKG_NAME_NODEJS: didcomm-node + + +jobs: + + checks: + name: check releases + if: github.ref == 'refs/heads/stable' + runs-on: ubuntu-latest + outputs: + current_version: ${{ steps.current_version.outputs.current_version }} + release_info: ${{ steps.release_info.outputs.release_info }} + asset_crate_url: ${{ steps.release_info.outputs.asset_crate_url }} + asset_npm_pkg_bundler_url: ${{ steps.release_info.outputs.asset_npm_pkg_bundler_url }} + asset_npm_pkg_nodejs_url: ${{ steps.release_info.outputs.asset_npm_pkg_nodejs_url }} + upload_url: ${{ steps.release_info.outputs.upload_url }} + already_in_crates_io: ${{ steps.check_in_crates_io.outputs.already_in_crates_io != '' }} + already_in_npm_bundler: ${{ steps.check_in_npm.outputs.already_in_npm_bundler != '' }} + already_in_npm_nodejs: ${{ steps.check_in_npm.outputs.already_in_npm_nodejs != '' }} + + steps: + - uses: actions/checkout@v3 + + - name: Get current version + id: current_version + run: | + version="$(cargo -q metadata --no-deps \ + | jq -r '.packages[] | select(.name == "${{ env.PKG_NAME }}") | .version')" + echo "$version" + echo "current_version=$version" >> $GITHUB_OUTPUT + shell: bash + + - name: Get release info + id: release_info + run: | + release_info="$(curl -s https://api.github.com/repos/${{ github.repository }}/releases \ + | jq '.[] | select(.name == "v${{ steps.current_version.outputs.current_version }}")')" + echo "release_info=$release_info" >> $GITHUB_OUTPUT + echo "$release_info" + + asset_crate_url="$(echo "$release_info" \ + | jq -r '.assets[] | select(.name | match("^${{ env.PKG_NAME }}.*\\.crate$")) | .browser_download_url')" + echo "asset_crate_url=$asset_crate_url" >> $GITHUB_OUTPUT + echo "$asset_crate_url" + + asset_npm_pkg_bundler_url="$(echo "$release_info" \ + | jq -r '.assets[] | select(.name | match("^${{ env.PKG_NAME }}-${{ steps.current_version.outputs.current_version }}\\.tgz$")) | .browser_download_url')" + echo "asset_npm_pkg_bundler_url=$asset_npm_pkg_bundler_url" >> $GITHUB_OUTPUT + echo "$asset_npm_pkg_bundler_url" + + asset_npm_pkg_nodejs_url="$(echo "$release_info" \ + | jq -r '.assets[] | select(.name | match("^${{ env.PKG_NAME_NODEJS }}-${{ steps.current_version.outputs.current_version }}\\.tgz$")) | .browser_download_url')" + echo "asset_npm_pkg_nodejs_url=$asset_npm_pkg_nodejs_url" >> $GITHUB_OUTPUT + echo "$asset_npm_pkg_nodejs_url" + + upload_url="$(echo "$release_info" | jq -r '.upload_url')" + echo "upload_url=$upload_url" >> $GITHUB_OUTPUT + echo "$upload_url" + shell: bash + + - name: check if already deployed to crates.io + id: check_in_crates_io + run: | + out="$(curl -s https://crates.io/api/v1/crates/${{ env.PKG_NAME }} | jq -r '.versions[] | .num' \ + | grep '^${{ steps.current_version.outputs.current_version }}$')" + echo "in crates.io check: $out" + echo "already_in_crates_io=$out" >> $GITHUB_OUTPUT + shell: bash {0} # to opt-out of default fail-fast behavior + + - name: check if already deployed to npm + id: check_in_npm + run: | + out="$(npm view ${{ env.PKG_NAME }}@${{ steps.current_version.outputs.current_version }} --json 2>/dev/null \ + | jq -r '.versions | select (.!=null)')" + echo "in npm check for ${{ env.PKG_NAME }} : $out" + echo "already_in_npm_bundler=$out" >> $GITHUB_OUTPUT + + out="$(npm view ${{ env.PKG_NAME_NODEJS }}@${{ steps.current_version.outputs.current_version }} --json 2>/dev/null \ + | jq -r '.versions | select (.!=null)')" + echo "in npm check for ${{ env.PKG_NAME_NODEJS }}: $out" + echo "already_in_npm_nodejs=$out" >> $GITHUB_OUTPUT + shell: bash {0} # to opt-out of default fail-fast behavior + + + release: + name: Release + if: github.ref == 'refs/heads/stable' + runs-on: ubuntu-latest + needs: checks + steps: + - uses: actions/checkout@v3 + + - name: Set rustup profile & toolchain + run: | + rustup set profile minimal + rustup toolchain install stable + shell: bash + + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + shell: bash + + - name: Create GitHub Release + id: create_release + if: ${{ !needs.checks.outputs.release_info }} + uses: actions/create-release@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + tag_name: v${{ needs.checks.outputs.current_version }} + release_name: v${{ needs.checks.outputs.current_version }} + + - name: Set upload url + id: upload_url + if: ${{ !(needs.checks.outputs.asset_crate_url && needs.checks.outputs.asset_npm_pkg_bundler_url && needs.checks.outputs.asset_npm_pkg_nodejs_url) }} + run: | + if [[ -n "${{ needs.checks.outputs.upload_url }}" ]]; then + echo "value=${{ needs.checks.outputs.upload_url }}" >> $GITHUB_OUTPUT + else + echo "value=${{ steps.create_release.outputs.upload_url }}" >> $GITHUB_OUTPUT + fi + + - name: package and verify (crate) + id: build_assets_crate + if: ${{ !needs.checks.outputs.asset_crate_url }} + run: | + cargo package + + # TODO + # - verify that it's not more than crates.io limit (10 MB) + # - explore whether we need to upload another artifact (without extension) + ls -la target/package + cargo package --list + + asset_name="$(find target/package -name '*.crate' -printf '%f')" + echo "asset_name=$asset_name" >> $GITHUB_OUTPUT + shell: bash + + - name: upload to GitHub (crate) + if: ${{ !needs.checks.outputs.asset_crate_url }} + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.upload_url.outputs.value }} + asset_path: target/package/${{ steps.build_assets_crate.outputs.asset_name }} + asset_name: ${{ steps.build_assets_crate.outputs.asset_name }} + asset_content_type: application/octet-stream # TODO check for less generic type + + - name: package and verify (npm bundler) + id: build_assets_npm_bundler + if: ${{ !needs.checks.outputs.asset_npm_pkg_bundler_url }} + run: | + # build, install (verify) and pack + make -C wasm build install pack + asset_name="$(find wasm/pkg -name '*.tgz' -printf '%f')" + echo "asset_name=$asset_name" >> $GITHUB_OUTPUT + shell: bash + + - name: upload to GitHub (npm bundler) + if: ${{ !needs.checks.outputs.asset_npm_pkg_bundler_url }} + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.upload_url.outputs.value }} + asset_path: wasm/pkg/${{ steps.build_assets_npm_bundler.outputs.asset_name }} + asset_name: ${{ steps.build_assets_npm_bundler.outputs.asset_name }} + asset_content_type: application/x-gtar + + - name: package and verify (npm nodejs) + id: build_assets_npm_nodejs + if: ${{ !needs.checks.outputs.asset_npm_pkg_nodejs_url }} + run: | + # build, install (verify) and pack + WASM_TARGET=nodejs PKG_NAME=${{ env.PKG_NAME_NODEJS }} make -C wasm build install pack + asset_name="$(find wasm/pkg -name '*.tgz' -printf '%f')" + echo "asset_name=$asset_name" >> $GITHUB_OUTPUT + shell: bash + + - name: upload to GitHub (npm nodejs) + if: ${{ !needs.checks.outputs.asset_npm_pkg_nodejs_url }} + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.upload_url.outputs.value }} + asset_path: wasm/pkg/${{ steps.build_assets_npm_nodejs.outputs.asset_name }} + asset_name: ${{ steps.build_assets_npm_nodejs.outputs.asset_name }} + asset_content_type: application/x-gtar + + # NOTE looks like there is no option to skip packaging here + # and use already prepared artifacts + + - name: publish to crates.io + if: needs.checks.outputs.already_in_crates_io == 'false' + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: cargo publish + shell: bash + + - name: publish to npm (bundler) + if: needs.checks.outputs.already_in_npm_bundler == 'false' + env: + NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} + run: | + # build and publish + make -C wasm build publish + shell: bash + + - name: publish to npm (nodejs) + if: needs.checks.outputs.already_in_npm_nodejs == 'false' + env: + NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} + run: | + # build and publish + WASM_TARGET=nodejs PKG_NAME=${{ env.PKG_NAME_NODEJS }} make -C wasm build publish + shell: bash + + + deploy-dev: + name: publish dev to crates.io + if: github.ref != 'refs/heads/stable' && github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set rustup profile & toolchain + run: | + rustup set profile minimal + rustup toolchain install stable + shell: bash + + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + shell: bash + + - name: set dev version + run: | + sed -i -r "0,/version/{s~^version = (['\"])(.+)['\"]~version = \1\2-0.dev.${{ github.event.inputs.devN }}\1~}" ./Cargo.toml + grep version ./Cargo.toml + sed -i -r "0,/version/{s~^version = (['\"])(.+)['\"]~version = \1\2-0.dev.${{ github.event.inputs.devN }}\1~}" ./wasm/Cargo.toml + grep version ./wasm/Cargo.toml + shell: bash + + - name: build, verify and publish (crates.io) + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + run: | + cargo package --allow-dirty + ls -la target/package + cargo package --allow-dirty --list + cargo publish --allow-dirty + shell: bash + + - name: build, verify and publish (npm bundler) + env: + NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} + run: WASM_OPTS_PUBLISH="--tag dev" make -C wasm build install publish + shell: bash + + - name: build, verify and publish (npm nodejs) + env: + NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} + run: WASM_TARGET=nodejs PKG_NAME=${{ env.PKG_NAME_NODEJS }} WASM_OPTS_PUBLISH="--tag dev" make -C wasm build install publish + shell: bash diff --git a/affinidi-messaging-didcomm/.github/workflows/verify.yml b/affinidi-messaging-didcomm/.github/workflows/verify.yml new file mode 100644 index 0000000..9ebb268 --- /dev/null +++ b/affinidi-messaging-didcomm/.github/workflows/verify.yml @@ -0,0 +1,262 @@ +name: verify + +on: + pull_request: + + +env: + PKG_NAME: didcomm + PKG_NAME_JS_TMP: didcomm-js + PKG_NAME_NODEJS: didcomm-node + + +jobs: + + release-ready: + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' && github.event.pull_request.base.ref == 'stable' + defaults: + run: + shell: bash + + steps: + - uses: actions/checkout@v3 + + - name: Get current version + id: current_version + run: | + version="$(cargo -q metadata --no-deps \ + | jq -r '.packages[] | select(.name == "${{ env.PKG_NAME }}") | .version')" + echo "$version" + echo "current_version=$version" >> $GITHUB_OUTPUT + + cd wasm + version="$(cargo -q metadata --no-deps \ + | jq -r '.packages[] | select(.name == "${{ env.PKG_NAME_JS_TMP }}") | .version')" + echo "$version" + echo "current_wasm_version=$version" >> $GITHUB_OUTPUT + + - name: Check version format + run: | + # verify the version has "MAJOR.MINOR.PATCH" parts only + echo "${{ steps.current_version.outputs.current_version }}" | grep -e '^[0-9]\+\.[0-9]\+\.[0-9]\+$' + + # TODO improve (DRY): copy-paste from release.yml + - name: Get release info + id: release_info + run: | + release_info="$(curl -s https://api.github.com/repos/${{ github.repository }}/releases \ + | jq '.[] | select(.name == "v${{ steps.current_version.outputs.current_version }}")')" + echo "release_info=$release_info" >> $GITHUB_OUTPUT + echo "$release_info" + + - name: check version bumped + # TODO check if greater than latest tag / release (?) + if: steps.release_info.outputs.release_info + run: exit 1 + + - name: check rust and wasm versions are the same + if: steps.current_version.outputs.current_version != steps.current_version.outputs.current_wasm_version + run: exit 1 + + - name: check it can be packaged (crate) + run: | + cargo package + # TODO verify that it's not more than crates.io limit (10 MB) + ls -la target/package + cargo package --list + + - name: check it can be packaged (npm) + run: | + curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + + # verify bundler target + make -C wasm build install + # verify nodejs target + WASM_TARGET=nodejs PKG_NAME=${{ env.PKG_NAME_NODEJS }} make -C wasm build install + + verify: + strategy: + matrix: + os: [ macos-latest, windows-latest, ubuntu-latest ] + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set rustup profile & toolchain + run: | + rustup set profile minimal + rustup toolchain install stable + + - name: Get timestamp for cache + id: date + run: echo "yearmo=$(date +%Y%m)" >> $GITHUB_OUTPUT + - uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}-${{steps.date.outputs.yearmo}} + + - name: Cargo fmt + # TODO enable that once https://github.com/rust-lang/rustfmt/issues/4477 + # is resolved + if: runner.os != 'Windows' + run: cargo fmt --all -- --check + + - name: Debug build + run: cargo build --verbose + + - name: Test + run: cargo test --verbose + + verify-wasm: + strategy: + matrix: + os: [ macos-latest, windows-latest, ubuntu-latest ] + node: [ 14, 16 ] + fail-fast: false + runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash + working-directory: wasm + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set rustup profile & toolchain + run: | + rustup set profile minimal + rustup toolchain install stable + + - name: Get timestamp for cache + id: date + run: echo "yearmo=$(date +%Y%m)" >> $GITHUB_OUTPUT + - uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}-${{steps.date.outputs.yearmo}} + + - name: Cargo fmt + # TODO enable that once https://github.com/rust-lang/rustfmt/issues/4477 + # is resolved + if: runner.os != 'Windows' + run: cargo fmt --all -- --check + + - name: Cargo checks + run: cargo check --all-targets + + # TODO caching, makes sense for demo and tests-js where lock files are presented + - name: Set up Node.js + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node }} + + - name: Install wasm-pack + if: runner.os != 'Windows' + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + + - name: Install wasm-pack Windows + if: runner.os == 'Windows' + run: npm install -g wasm-pack@0.10.1 + + - name: Build pkg (bundler) + run: make + + - name: Install tests-js (bundler) + run: cd tests-js && npm install + + - name: Lint tests-js + run: cd tests-js && npm run check + + - name: Test tests-js (bundler) + if: false # TODO that check is not supported yet for bundler build + run: cd tests-js && npm test + + - name: Test tests-js in browser (bundler) + if: false # TODO that check is not supported yet for bundler build + run: cd tests-js && npm run test-puppeteer + + - name: Install demo (bundler) + run: cd demo && npm install + + - name: Lint demo + run: cd demo && npm run check + + - name: Test demo (bundler) + if: false # TODO that check is not supported yet for bundler build + run: cd demo && npm run start + + - name: Build pkg (nodejs) + run: WASM_TARGET=nodejs make + + - name: Install tests-js (nodejs) + run: cd tests-js && rm -rf node_modules && npm install + + - name: Test tests-js (nodejs) + run: cd tests-js && npm test + + - name: Test tests-js in browser (nodejs) + run: cd tests-js && npm run test-puppeteer + + - name: Install demo (nodejs) + run: cd demo && rm -rf node_modules && npm install + + - name: Test demo (nodejs) + run: cd demo && npm run start + + verify-uniffi: + strategy: + matrix: + os: [ macos-latest, windows-latest, ubuntu-latest ] + runs-on: ${{ matrix.os }} + defaults: + run: + shell: bash + working-directory: uniffi + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Set rustup profile & toolchain + run: | + rustup set profile minimal + rustup toolchain install stable + + - name: Get timestamp for cache + id: date + run: echo "yearmo=$(date +%Y%m)" >> $GITHUB_OUTPUT + - uses: actions/cache@v3 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml') }}-${{steps.date.outputs.yearmo}} + + - name: Cargo fmt + # TODO enable that once https://github.com/rust-lang/rustfmt/issues/4477 + # is resolved + if: runner.os != 'Windows' + run: cargo fmt --all -- --check + + - name: Debug build + run: cargo build --verbose + + - name: Test + run: cargo test --verbose diff --git a/affinidi-messaging-didcomm/.gitignore b/affinidi-messaging-didcomm/.gitignore new file mode 100644 index 0000000..658c8d7 --- /dev/null +++ b/affinidi-messaging-didcomm/.gitignore @@ -0,0 +1,40 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + + +# Added by cargo + +/target + + +# Added by cargo +# +# already existing elements were commented out + +#/target +#Cargo.lock +# + +# vim +*.swp + +.idea + +# Mac OS X Finder +.DS_Store + +# Xcode +xcuserdata/ +project.xcworkspace/ +DerivedData/ + +# Cocoapods +Pods/ diff --git a/affinidi-messaging-didcomm/CHANGELOG.md b/affinidi-messaging-didcomm/CHANGELOG.md new file mode 100644 index 0000000..4dce860 --- /dev/null +++ b/affinidi-messaging-didcomm/CHANGELOG.md @@ -0,0 +1,7 @@ +# Affinidi Trust Network - Affinidi Trusted Messaging - DIDComm Implementation + +## Changelog history + +### 29th June 2024 + +* Added a method to Message so that you can change the body after initial creation. Helps with variable implementations of the message itself diff --git a/affinidi-messaging-didcomm/Cargo.toml b/affinidi-messaging-didcomm/Cargo.toml new file mode 100644 index 0000000..4d81ee6 --- /dev/null +++ b/affinidi-messaging-didcomm/Cargo.toml @@ -0,0 +1,67 @@ +[[bench]] +name = 'pack_signed' +harness = false + +[[bench]] +name = 'pack_encrypted' +harness = false + +[package] +name = 'affinidi-messaging-didcomm' +version = '0.5.0' +authors = [ + 'Vyacheslav Gudkov ', + 'Glenn Gore ', +] +edition = '2018' +description = 'DIDComm for Rust' +license = 'Apache-2.0' +repository = 'https://github.com/sicpa-dlab/didcomm-rust' +readme = 'README.md' + +[dependencies] +affinidi-did-resolver-cache-sdk.workspace = true +anyhow = '1.0' +base64 = '0.22' +async-trait = '0.1' +thiserror = '1.0' +serde_json = '1.0' +serde-enum-str = '0.4' +sha2 = '0.10' +bs58 = "0.5" +varint = "0.9.0" +lazy_static = { version = "1.4.0", optional = true } +askar-crypto = "0.3.1" +ssi = "0.8" +tracing = { version = "0.1", features = [ + "max_level_debug", + "release_max_level_warn", +] } + +[dependencies.serde] +version = '1.0' +features = ['derive'] + +[dependencies.uuid] +version = "1.8" +features = ["v4"] + +[dev-dependencies] +lazy_static = '1.4.0' +tracing-test = "0.2" + +[dev-dependencies.tokio] +version = '1.9' +features = ['rt', 'macros'] + +[dev-dependencies.getrandom] +version = '0.2' +features = ['js'] + +[dev-dependencies.criterion] +version = '0.5' +features = ['async_futures'] + +[features] +uniffi = [] +testvectors = ["lazy_static"] diff --git a/affinidi-messaging-didcomm/LICENSE b/affinidi-messaging-didcomm/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/affinidi-messaging-didcomm/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/affinidi-messaging-didcomm/README.md b/affinidi-messaging-didcomm/README.md new file mode 100644 index 0000000..a88fe1e --- /dev/null +++ b/affinidi-messaging-didcomm/README.md @@ -0,0 +1,259 @@ +# DIDComm Rust + JavaScript/TypeScript + Swift + +**IMPORTANT** + +This project is a modified version of the [didcomm-rust](https://github.com/sicpa-dlab/didcomm-rust) project from GitHub. + +--- + +[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) +[![Unit Tests](https://github.com/sicpa-dlab/didcomm-rust/workflows/verify/badge.svg)](https://github.com/sicpa-dlab/didcomm-rust/actions/workflows/verify.yml) +[![Rust Package](https://img.shields.io/crates/v/didcomm)](https://crates.io/crates/didcomm/) + +The repository consists of the following main components: +- Basic [DIDComm v2](https://identity.foundation/didcomm-messaging/spec) support in Rust. +- [Wasm](https://webassembly.org/) - based DIDComm JavaScript/TypeScript, see [wasm](/wasm). +- [uniffi-rs](https://github.com/mozilla/uniffi-rs) - based wrappers + - [uniffi](/uniffi) - callback-based Rust wrapper with uniffi-rs support + - [wrappers/swift](/wrappers/swift) - Swift wrapper generated via uniffi-rs + +The docs below are provided for the main DIDComm Rust. + +See [wasm/README.md](/wasm/README.md) for DIDComm JavaScript/TypeScript docs. + +See [wrappers/swift/README.md](/wrappers/swift/README.md) for DIDComm Swift docs. + +## Usage + +To use `didcomm`, add this to your `Cargo.toml`: + +```toml +[dependencies] +didcomm = "0.4" +``` + +## Run examples + +Use `cargo run --example {example-name}` for example `cargo run --example basic`. + +## Assumptions and Limitations +- Rust 2018 edition is required. +- In order to use the library, `SecretsResolver` and `DIDResolver` traits must be implemented on the application level. + Implementation of that traits is out of DIDComm library scope, but we provide 2 simple implementation `ExampleDIDResolver` + and `ExampleSecretsResolver` that allows resolve locally known DID docs and secrets for tests/demo purposes. + - Verification materials are expected in JWK, Base58 and Multibase (internally Base58 only) formats. + - In Base58 and Multibase formats, keys using only X25519 and Ed25519 curves are supported. + - For private keys in Base58 and Multibase formats, the verification material value contains both private and public parts (concatenated bytes). + - In Multibase format, bytes of the verification material value is prefixed with the corresponding Multicodec code. + - Key IDs (kids) used in `SecretsResolver` must match the corresponding key IDs from DID Doc verification methods. + - Key IDs (kids) in DID Doc verification methods and secrets must be a full [DID Fragment](https://www.w3.org/TR/did-core/#fragment), that is `did#key-id`. + - Verification methods referencing another DID Document are not supported (see [Referring to Verification Methods](https://www.w3.org/TR/did-core/#referring-to-verification-methods)). +- The following curves and algorithms are supported: + - Encryption: + - Curves: X25519, P-256 + - Content encryption algorithms: + - XC20P (to be used with ECDH-ES only, default for anoncrypt), + - A256GCM (to be used with ECDH-ES only), + - A256CBC-HS512 (default for authcrypt) + - Key wrapping algorithms: ECDH-ES+A256KW, ECDH-1PU+A256KW + - Signing: + - Curves: Ed25519, Secp256k1, P-256 + - Algorithms: EdDSA (with crv=Ed25519), ES256, ES256K +- Forward protocol is implemented and used by default. +- DID rotation (`fromPrior` field) is supported. +- DIDComm has been implemented under the following [Assumptions](https://hackmd.io/i3gLqgHQR2ihVFV5euyhqg) + + +## Examples + +See [examples](examples/) for details. + +A general usage of the API is the following: +- Sender Side: + - Build a `Message` (plaintext, payload). + - Convert a message to a DIDComm Message for further transporting by calling one of the following: + - `Message::pack_encrypted` to build an Encrypted DIDComm message + - `Message::pack_signed` to build a Signed DIDComm message + - `Message::pack_plaintext` to build a Plaintext DIDComm message +- Receiver side: + - Call `Message::unpack` on receiver side that will decrypt the message, verify signature if needed + and return a `Message` for further processing on the application level. + +### 1. Build an Encrypted DIDComm message for the given recipient + +This is the most common DIDComm message to be used in most of the applications. + +A DIDComm encrypted message is an encrypted JWM (JSON Web Messages) that +- hides its content from all but authorized recipients +- (optionally) discloses and proves the sender to only those recipients +- provides message integrity guarantees + +It is important in privacy-preserving routing. It is what normally moves over network transports in DIDComm +applications, and is the safest format for storing DIDComm data at rest. + +See `Message::pack_encrypted` documentation for more details. + +**Authentication encryption** example (most common case): + +```rust +// --- Build message from ALICE to BOB --- +let msg = Message::build( + "example-1".to_owned(), + "example/v1".to_owned(), + json!("example-body"), +) +.to(ALICE_DID.to_owned()) +.from(BOB_DID.to_owned()) +.finalize(); + +// --- Pack encrypted and authenticated message --- +let did_resolver = ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); +let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + +let (msg, metadata) = msg + .pack_encrypted( + BOB_DID, + Some(ALICE_DID), + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + .expect("Unable pack_encrypted"); + +println!("Encryption metadata is\n{:?}\n", metadata); + +// --- Send message --- +println!("Sending message \n{}\n", msg); + +// --- Unpacking message --- +let did_resolver = ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); +let secrets_resolver = ExampleSecretsResolver::new(BOB_SECRETS.clone()); + +let (msg, metadata) = Message::unpack( + &msg, + &did_resolver, + &secrets_resolver, + &UnpackOptions::default(), +) +.await +.expect("Unable unpack"); + +println!("Receved message is \n{:?}\n", msg); +println!("Receved message unpack metadata is \n{:?}\n", metadata); +``` + +**Anonymous encryption** example: + +```rust +let (msg, metadata) = msg + .pack_encrypted( + BOB_DID, + None, // Keep sender as None here + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + .expect("Unable pack_encrypted"); +``` + +**Encryption with non-repudiation** example: + +```rust +let (msg, metadata) = msg + .pack_encrypted( + BOB_DID, + Some(ALICE_DID), + Some(ALICE_DID), // Provide information about signer here + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + .expect("Unable pack_encrypted"); +``` + +### 2. Build an unencrypted but Signed DIDComm message + +Signed messages are only necessary when +- the origin of plaintext must be provable to third parties +- or the sender can’t be proven to the recipient by authenticated encryption because the recipient is not known in advance (e.g., in a +broadcast scenario). + +Adding a signature when one is not needed can degrade rather than enhance security because it +relinquishes the sender’s ability to speak off the record. + +See `Message::pack_signed` documentation for more details. + +```rust +// ALICE +let msg = Message::build( + "example-1".to_owned(), + "example/v1".to_owned(), + json!("example-body"), +) +.to(ALICE_DID.to_owned()) +.from(BOB_DID.to_owned()) +.finalize(); + +let (msg, metadata) = msg + .pack_signed(ALICE_DID, &did_resolver, &secrets_resolver) + .await + .expect("Unable pack_signed"); + +// BOB +let (msg, metadata) = Message::unpack( + &msg, + &did_resolver, + &secrets_resolver, + &UnpackOptions::default(), +) +.await +.expect("Unable unpack"); +``` + +### 3. Build a Plaintext DIDComm message + +A DIDComm message in its plaintext form that +- is not packaged into any protective envelope +- lacks confidentiality and integrity guarantees +- repudiable + +They are therefore not normally transported across security boundaries. + +```rust +// ALICE +let msg = Message::build( + "example-1".to_owned(), + "example/v1".to_owned(), + json!("example-body"), +) +.to(ALICE_DID.to_owned()) +.from(BOB_DID.to_owned()) +.finalize(); + +let msg = msg + .pack_plaintext(&did_resolver) + .expect("Unable pack_plaintext"); + +// BOB +let (msg, metadata) = Message::unpack( + &msg, + &did_resolver, + &secrets_resolver, + &UnpackOptions::default(), +) +.await +.expect("Unable unpack"); +``` + +## Contribution +PRs are welcome! + +The following CI checks are run against every PR: +- No warnings from `cargo check --all-targets` +- All tests must pass with `cargo test` +- Code must be formatted by `cargo fmt --all` diff --git a/affinidi-messaging-didcomm/Specs/DidcommSDK/0.3.1/DidcommSDK.podspec b/affinidi-messaging-didcomm/Specs/DidcommSDK/0.3.1/DidcommSDK.podspec new file mode 100644 index 0000000..0833204 --- /dev/null +++ b/affinidi-messaging-didcomm/Specs/DidcommSDK/0.3.1/DidcommSDK.podspec @@ -0,0 +1,23 @@ +Pod::Spec.new do |spec| + + spec.name = "DidcommSDK" + spec.version = "0.3.1" + spec.summary = "Didcomm v2 created from rust." + + spec.description = "Didcomm v2 created from rust. UNIFFI was used to convert from rust to Swift." + spec.homepage = "https://github.com/sicpa-dlab/didcomm-rust" + + spec.license = { :type => 'Apache License 2.0', :file => 'LICENSE.txt' } + + spec.authors = { "Sicpa-Dlab" => "dlab@sicpa.com" } + spec.platforms = { :ios => "10.0" } + spec.source = { :http => 'https://github.com/sicpa-dlab/didcomm-rust/releases/download/v0.3.1/didcomm-swift-0.3.1.tar.gz'} + spec.swift_version = '4.0' + + spec.ios.vendored_library = '*.a' + spec.source_files = ['didcomm.swift', 'didcommFFI.h'] + + spec.pod_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64 i386' } + spec.user_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64 i386' } + +end diff --git a/affinidi-messaging-didcomm/Specs/DidcommSDK/0.3.2/DidcommSDK.podspec b/affinidi-messaging-didcomm/Specs/DidcommSDK/0.3.2/DidcommSDK.podspec new file mode 100644 index 0000000..6246785 --- /dev/null +++ b/affinidi-messaging-didcomm/Specs/DidcommSDK/0.3.2/DidcommSDK.podspec @@ -0,0 +1,23 @@ +Pod::Spec.new do |spec| + + spec.name = "DidcommSDK" + spec.version = "0.3.2" + spec.summary = "Didcomm v2 created from rust." + + spec.description = "Didcomm v2 created from rust. UNIFFI was used to convert from rust to Swift." + spec.homepage = "https://github.com/sicpa-dlab/didcomm-rust" + + spec.license = { :type => 'Apache License 2.0', :file => 'LICENSE.txt' } + + spec.authors = { "Sicpa-Dlab" => "dlab@sicpa.com" } + spec.platforms = { :ios => "10.0" } + spec.source = { :http => 'https://github.com/sicpa-dlab/didcomm-rust/releases/download/v0.3.2/didcomm-swift-0.3.2.tar.gz'} + spec.swift_version = '4.0' + + spec.ios.vendored_library = '*.a' + spec.source_files = ['didcomm.swift', 'didcommFFI.h'] + + spec.pod_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64 i386' } + spec.user_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64 i386' } + +end diff --git a/affinidi-messaging-didcomm/Specs/DidcommSDK/0.3.3/DidcommSDK.podspec b/affinidi-messaging-didcomm/Specs/DidcommSDK/0.3.3/DidcommSDK.podspec new file mode 100644 index 0000000..fbbe18d --- /dev/null +++ b/affinidi-messaging-didcomm/Specs/DidcommSDK/0.3.3/DidcommSDK.podspec @@ -0,0 +1,23 @@ +Pod::Spec.new do |spec| + + spec.name = "DidcommSDK" + spec.version = "0.3.3" + spec.summary = "Didcomm v2 created from rust." + + spec.description = "Didcomm v2 created from rust. UNIFFI was used to convert from rust to Swift." + spec.homepage = "https://github.com/sicpa-dlab/didcomm-rust" + + spec.license = { :type => 'Apache License 2.0', :file => 'LICENSE.txt' } + + spec.authors = { "Sicpa-Dlab" => "dlab@sicpa.com" } + spec.platforms = { :ios => "10.0" } + spec.source = { :http => 'https://github.com/sicpa-dlab/didcomm-rust/releases/download/v0.3.3/didcomm-swift-0.3.3.tar.gz'} + spec.swift_version = '4.0' + + spec.ios.vendored_library = '*.a' + spec.source_files = ['didcomm.swift', 'didcommFFI.h'] + + spec.pod_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64 i386' } + spec.user_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64 i386' } + +end diff --git a/affinidi-messaging-didcomm/Specs/DidcommSDK/0.3.4/DidcommSDK.podspec b/affinidi-messaging-didcomm/Specs/DidcommSDK/0.3.4/DidcommSDK.podspec new file mode 100644 index 0000000..c54f41c --- /dev/null +++ b/affinidi-messaging-didcomm/Specs/DidcommSDK/0.3.4/DidcommSDK.podspec @@ -0,0 +1,23 @@ +Pod::Spec.new do |spec| + + spec.name = "DidcommSDK" + spec.version = "0.3.4" + spec.summary = "Didcomm v2 created from rust." + + spec.description = "Didcomm v2 created from rust. UNIFFI was used to convert from rust to Swift." + spec.homepage = "https://github.com/sicpa-dlab/didcomm-rust" + + spec.license = { :type => 'Apache License 2.0', :file => 'LICENSE.txt' } + + spec.authors = { "Sicpa-Dlab" => "dlab@sicpa.com" } + spec.platforms = { :ios => "10.0" } + spec.source = { :http => 'https://github.com/sicpa-dlab/didcomm-rust/releases/download/v0.3.4/didcomm-swift-0.3.4.tar.gz'} + spec.swift_version = '4.0' + + spec.ios.vendored_library = '*.a' + spec.source_files = ['didcomm.swift', 'didcommFFI.h'] + + spec.pod_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64 i386' } + spec.user_target_xcconfig = { 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'arm64 i386' } + +end diff --git a/affinidi-messaging-didcomm/benches/pack_encrypted.rs b/affinidi-messaging-didcomm/benches/pack_encrypted.rs new file mode 100644 index 0000000..7ce2701 --- /dev/null +++ b/affinidi-messaging-didcomm/benches/pack_encrypted.rs @@ -0,0 +1,702 @@ +// Allows share test vectors between unit and integration tests +#[allow(clippy::single_component_path_imports)] +pub(crate) use affinidi_messaging_didcomm; + +#[allow(unused_imports, dead_code)] +#[path = "../src/test_vectors/mod.rs"] +mod test_vectors; + +use criterion::{async_executor::FuturesExecutor, criterion_group, criterion_main, Criterion}; + +use affinidi_messaging_didcomm::{ + algorithms::AnonCryptAlg, did::resolvers::ExampleDIDResolver, + secrets::resolvers::ExampleSecretsResolver, PackEncryptedOptions, +}; + +use test_vectors::{ + ALICE_AUTH_METHOD_25519, ALICE_AUTH_METHOD_P256, ALICE_AUTH_METHOD_SECPP256K1, ALICE_DID, + ALICE_DID_DOC, ALICE_SECRETS, BOB_DID, BOB_DID_DOC, BOB_SECRET_KEY_AGREEMENT_KEY_P256_1, + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2, MESSAGE_SIMPLE, +}; + +// Here we have an async function to benchmark +async fn pack_encrypted( + to: &str, + from: Option<&str>, + sign_by: Option<&str>, + did_resolver: &ExampleDIDResolver, + secrets_resolver: &ExampleSecretsResolver, + opts: &PackEncryptedOptions, +) { + MESSAGE_SIMPLE + .pack_encrypted(to, from, sign_by, did_resolver, secrets_resolver, opts) + .await + .expect("Unable pack_encrypted"); +} + +fn benchmarks(c: &mut Criterion) { + { + let from = Some(ALICE_DID); + let to = &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id; + let sign_by = None; + + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }; + + c.bench_function("pack_encrypted_authcrypt_ed25519_1key", move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }); + } + + { + let from = Some(ALICE_DID); + let to = &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id; + let sign_by = None; + + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + ..PackEncryptedOptions::default() + }; + + c.bench_function( + "pack_encrypted_authcrypt_ed25519_1key_anoncrypt_a256cbc", + move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }, + ); + } + + { + let from = Some(ALICE_DID); + let to = &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id; + let sign_by = None; + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: AnonCryptAlg::A256gcmEcdhEsA256kw, + ..PackEncryptedOptions::default() + }; + + c.bench_function( + "pack_encrypted_authcrypt_ed25519_1key_anoncrypt_a256gsm", + move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }, + ); + } + + { + let from = Some(ALICE_DID); + let to = &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id; + let sign_by = None; + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: AnonCryptAlg::Xc20pEcdhEsA256kw, + ..PackEncryptedOptions::default() + }; + + c.bench_function( + "pack_encrypted_authcrypt_ed25519_1key_anoncrypt_xc20p", + move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }, + ); + } + + { + let from = Some(ALICE_DID); + let to = BOB_DID; + let sign_by = None; + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }; + + c.bench_function("pack_encrypted_authcrypt_ed25519_3keys", move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }); + } + + { + let from = Some(ALICE_DID); + let to = BOB_DID; + let sign_by = None; + + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + ..PackEncryptedOptions::default() + }; + + c.bench_function( + "pack_encrypted_authcrypt_ed25519_3keys_anoncrypt_a256cbc", + move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }, + ); + } + + { + let from = Some(ALICE_DID); + let to = BOB_DID; + let sign_by = None; + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: AnonCryptAlg::A256gcmEcdhEsA256kw, + ..PackEncryptedOptions::default() + }; + + c.bench_function( + "pack_encrypted_authcrypt_ed25519_3keys_anoncrypt_a256gsm", + move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }, + ); + } + + { + let from = Some(ALICE_DID); + let to = BOB_DID; + let sign_by = None; + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: AnonCryptAlg::Xc20pEcdhEsA256kw, + ..PackEncryptedOptions::default() + }; + + c.bench_function( + "pack_encrypted_authcrypt_ed25519_3keys_anoncrypt_xc20p", + move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }, + ); + } + + { + let from = None; + let to = &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id; + let sign_by = None; + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + ..PackEncryptedOptions::default() + }; + + c.bench_function("pack_encrypted_anoncrypt_ed25519_a256cbc_1key", move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }); + } + + { + let from = None; + let to = &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id; + let sign_by = None; + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: AnonCryptAlg::A256gcmEcdhEsA256kw, + ..PackEncryptedOptions::default() + }; + + c.bench_function("pack_encrypted_anoncrypt_ed25519_a256gcm_1key", move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }); + } + + { + let from = None; + let to = &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id; + let sign_by = None; + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: AnonCryptAlg::Xc20pEcdhEsA256kw, + ..PackEncryptedOptions::default() + }; + + c.bench_function("pack_encrypted_anoncrypt_ed25519_xc20p_1key", move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }); + } + + { + let from = None; + let to = BOB_DID; + let sign_by = None; + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }; + + c.bench_function("pack_encrypted_anoncrypt_ed25519_a256cbc_3keys", move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }); + } + + { + let from = None; + let to = BOB_DID; + let sign_by = None; + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: AnonCryptAlg::A256gcmEcdhEsA256kw, + ..PackEncryptedOptions::default() + }; + + c.bench_function("pack_encrypted_anoncrypt_ed25519_a256gsm_3keys", move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }); + } + + { + let from = None; + let to = BOB_DID; + let sign_by = None; + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: AnonCryptAlg::Xc20pEcdhEsA256kw, + ..PackEncryptedOptions::default() + }; + + c.bench_function("pack_encrypted_anoncrypt_ed25519_xc20p_3keys", move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }); + } + + { + let from = Some(ALICE_DID); + let to = &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id; + let sign_by = None; + + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }; + + c.bench_function("pack_encrypted_authcrypt_p256_1key", move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }); + } + + { + let from = Some(ALICE_DID); + let to = &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id; + let sign_by = None; + + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + ..PackEncryptedOptions::default() + }; + + c.bench_function( + "pack_encrypted_authcrypt_p256_1key_anoncrypt_a256cbc", + move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }, + ); + } + + { + let from = Some(ALICE_DID); + let to = &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id; + let sign_by = None; + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: AnonCryptAlg::A256gcmEcdhEsA256kw, + ..PackEncryptedOptions::default() + }; + + c.bench_function( + "pack_encrypted_authcrypt_p256_1key_anoncrypt_a256gsm", + move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }, + ); + } + + { + let from = Some(ALICE_DID); + let to = &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id; + let sign_by = None; + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: AnonCryptAlg::Xc20pEcdhEsA256kw, + ..PackEncryptedOptions::default() + }; + + c.bench_function( + "pack_encrypted_authcrypt_p256_1key_anoncrypt_xc20p", + move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }, + ); + } + + { + let from = Some(ALICE_DID); + let to = &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id; + let sign_by = Some(ALICE_AUTH_METHOD_25519.id.as_str()); + + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }; + + c.bench_function( + "pack_encrypted_authcrypt_ed25519_1key_sign_x25519", + move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }, + ); + } + + { + let from = Some(ALICE_DID); + let to = &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id; + let sign_by = Some(ALICE_AUTH_METHOD_P256.id.as_str()); + + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }; + + c.bench_function( + "pack_encrypted_authcrypt_ed25519_1key_sign_p256", + move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }, + ); + } + + { + let from = Some(ALICE_DID); + let to = &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id; + let sign_by = Some(ALICE_AUTH_METHOD_SECPP256K1.id.as_str()); + + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }; + + c.bench_function( + "pack_encrypted_authcrypt_ed25519_1key_sign_k256", + move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }, + ); + } + + { + let from = Some(ALICE_DID); + let to = &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id; + let sign_by = Some(ALICE_AUTH_METHOD_25519.id.as_str()); + + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + ..PackEncryptedOptions::default() + }; + + c.bench_function( + "pack_encrypted_authcrypt_ed25519_1key_anoncrypt_a256cbc_sign_x25519", + move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }, + ); + } + + { + let from = Some(ALICE_DID); + let to = &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id; + let sign_by = Some(ALICE_AUTH_METHOD_P256.id.as_str()); + + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + ..PackEncryptedOptions::default() + }; + + c.bench_function( + "pack_encrypted_authcrypt_ed25519_1key_anoncrypt_a256cbc_sign_p256", + move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }, + ); + } + + { + let from = Some(ALICE_DID); + let to = &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id; + let sign_by = Some(ALICE_AUTH_METHOD_SECPP256K1.id.as_str()); + + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + ..PackEncryptedOptions::default() + }; + + c.bench_function( + "pack_encrypted_authcrypt_ed25519_1key_anoncrypt_a256cbc_sign_k256", + move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }, + ); + } + + { + let from = Some(ALICE_DID); + let to = &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id; + let sign_by = Some(ALICE_AUTH_METHOD_25519.id.as_str()); + + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + ..PackEncryptedOptions::default() + }; + + c.bench_function( + "pack_encrypted_authcrypt_p256_1key_anoncrypt_a256cbc_sign_e25519", + move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }, + ); + } + + { + let from = Some(ALICE_DID); + let to = &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id; + let sign_by = Some(ALICE_AUTH_METHOD_P256.id.as_str()); + + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + ..PackEncryptedOptions::default() + }; + + c.bench_function( + "pack_encrypted_authcrypt_p256_1key_anoncrypt_a256cbc_sign_p256", + move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }, + ); + } + + { + let from = Some(ALICE_DID); + let to = &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id; + let sign_by = Some(ALICE_AUTH_METHOD_SECPP256K1.id.as_str()); + + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let opts = PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + ..PackEncryptedOptions::default() + }; + + c.bench_function( + "pack_encrypted_authcrypt_p256_1key_anoncrypt_a256cbc_sign_k256", + move |b| { + b.to_async(FuturesExecutor).iter(|| { + pack_encrypted(to, from, sign_by, &did_resolver, &secrets_resolver, &opts) + }); + }, + ); + } +} + +criterion_group!(benches, benchmarks); +criterion_main!(benches); diff --git a/affinidi-messaging-didcomm/benches/pack_signed.rs b/affinidi-messaging-didcomm/benches/pack_signed.rs new file mode 100644 index 0000000..6898f2b --- /dev/null +++ b/affinidi-messaging-didcomm/benches/pack_signed.rs @@ -0,0 +1,61 @@ +// Allows share test vectors between unit and integration tests +#[allow(clippy::single_component_path_imports)] +pub(crate) use affinidi_messaging_didcomm; + +#[allow(unused_imports, dead_code)] +#[path = "../src/test_vectors/mod.rs"] +mod test_vectors; + +use affinidi_messaging_didcomm::{ + did::resolvers::ExampleDIDResolver, secrets::resolvers::ExampleSecretsResolver, +}; +use criterion::{async_executor::FuturesExecutor, criterion_group, criterion_main, Criterion}; + +use test_vectors::{ + ALICE_AUTH_METHOD_25519, ALICE_AUTH_METHOD_P256, ALICE_AUTH_METHOD_SECPP256K1, ALICE_DID_DOC, + ALICE_SECRETS, MESSAGE_SIMPLE, +}; + +// Here we have an async function to benchmark +async fn pack_signed( + sign_by: &str, + did_resolver: &ExampleDIDResolver, + secrets_resolver: &ExampleSecretsResolver, +) { + MESSAGE_SIMPLE + .pack_signed(sign_by, did_resolver, secrets_resolver) + .await + .expect("Unable pack_signed"); +} + +fn benchmarks(c: &mut Criterion) { + let sign_by = &ALICE_AUTH_METHOD_25519.id; + let did_resolver = ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone()]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + c.bench_function("pack_signed_ed25519", move |b| { + b.to_async(FuturesExecutor) + .iter(|| pack_signed(sign_by, &did_resolver, &secrets_resolver)); + }); + + let sign_by = &ALICE_AUTH_METHOD_P256.id; + let did_resolver = ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone()]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + c.bench_function("pack_signed_p256", move |b| { + b.to_async(FuturesExecutor) + .iter(|| pack_signed(sign_by, &did_resolver, &secrets_resolver)); + }); + + let sign_by = &ALICE_AUTH_METHOD_SECPP256K1.id; + let did_resolver = ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone()]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + c.bench_function("pack_signed_k256", move |b| { + b.to_async(FuturesExecutor) + .iter(|| pack_signed(sign_by, &did_resolver, &secrets_resolver)); + }); +} + +criterion_group!(benches, benchmarks); +criterion_main!(benches); diff --git a/affinidi-messaging-didcomm/docs/release.md b/affinidi-messaging-didcomm/docs/release.md new file mode 100644 index 0000000..758256c --- /dev/null +++ b/affinidi-messaging-didcomm/docs/release.md @@ -0,0 +1,18 @@ +## Release + +Assumptions: + +* `main` branch can wait until release PR is merged + +The steps: + +1. **release**: + 1. **review and adjust if needed the release version in `main`** to match the changes from the latest release following the [SemVer rules](https://semver.org/#summary). + 2. [create](https://github.com/sicpa-dlab/didcomm-rust/compare/stable...main) a **PR from `main` to `stable`** (you may likely want to name it as `release-`) + 3. once merged [release pipeline](https://github.com/sicpa-dlab/didcomm-rust/actions/workflows/release.yml) will publish the release: + * to [crates.io](https://crates.io/crates/didcomm) + * to NPM: + * as Bundler(Webpack) compatible [package](https://www.npmjs.com/package/didcomm) + * as Node.js (CommonJS) compatible [package](https://www.npmjs.com/package/didcomm-node) +2. **bump next release version in `main`** + * **Note** decision about the next release version should be based on the same [SemVer](https://semver.org/) rules and the expected changes. Usually it would be either a MINOR or MAJOR (if incompatible changes are planned) release. diff --git a/affinidi-messaging-didcomm/renovate.json b/affinidi-messaging-didcomm/renovate.json new file mode 100644 index 0000000..f45d8f1 --- /dev/null +++ b/affinidi-messaging-didcomm/renovate.json @@ -0,0 +1,5 @@ +{ + "extends": [ + "config:base" + ] +} diff --git a/affinidi-messaging-didcomm/src/algorithms.rs b/affinidi-messaging-didcomm/src/algorithms.rs new file mode 100644 index 0000000..568ab8a --- /dev/null +++ b/affinidi-messaging-didcomm/src/algorithms.rs @@ -0,0 +1,33 @@ +use serde::{Deserialize, Serialize}; + +/// Algorithms for anonymous encryption +#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] +pub enum AnonCryptAlg { + /// AES256-CBC + HMAC-SHA512 with a 512 bit key content encryption, + /// ECDH-ES key agreement with A256KW key wrapping + A256cbcHs512EcdhEsA256kw, + + /// XChaCha20Poly1305 with a 256 bit key content encryption, + /// ECDH-ES key agreement with A256KW key wrapping + #[default] + Xc20pEcdhEsA256kw, + + /// A256GCM_ECDH_ES_A256KW: XChaCha20Poly1305 with a 256 bit key content encryption, + /// ECDH-ES key agreement with A256KW key wrapping + A256gcmEcdhEsA256kw, +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] +pub enum AuthCryptAlg { + /// AES256-CBC + HMAC-SHA512 with a 512 bit key content encryption, + /// ECDH-1PU key agreement with A256KW key wrapping + #[default] + A256cbcHs512Ecdh1puA256kw, +} + +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] +pub enum SignAlg { + EdDSA, + ES256, + ES256K, +} diff --git a/affinidi-messaging-didcomm/src/document.rs b/affinidi-messaging-didcomm/src/document.rs new file mode 100644 index 0000000..de21165 --- /dev/null +++ b/affinidi-messaging-didcomm/src/document.rs @@ -0,0 +1,431 @@ +//! Extension trait for SSI Document +//! Contains various helper functions to work with DIDComm + +use std::io::Cursor; + +use crate::{ + error::{err_msg, Error, ErrorKind, Result, ResultExt, ToResult}, + jwk::FromJwkValue, + secrets::{Secret, SecretMaterial, SecretType}, + utils::crypto::{AsKnownKeyPair, AsKnownKeyPairSecret, KnownKeyAlg, KnownKeyPair}, +}; +use askar_crypto::{ + alg::{ed25519::Ed25519KeyPair, k256::K256KeyPair, p256::P256KeyPair, x25519::X25519KeyPair}, + repr::{KeyPublicBytes, KeySecretBytes}, +}; +use base64::prelude::*; +use serde_json::{json, Value}; +use ssi::{ + dids::document::DIDVerificationMethod, jwk::Params, multicodec::MultiEncodedBuf, + security::MultibaseBuf, JWK, +}; +use tracing::warn; +use varint::{VarintRead, VarintWrite}; + +/// Older left over functions from original DIDComm crate +pub(crate) fn did_or_url(did_or_url: &str) -> (&str, Option<&str>) { + // TODO: does it make sense to validate DID here? + + match did_or_url.split_once('#') { + Some((did, _)) => (did, Some(did_or_url)), + None => (did_or_url, None), + } +} + +pub(crate) fn is_did(did: &str) -> bool { + let parts: Vec<_> = did.split(':').collect(); + return parts.len() >= 3 && parts.first().unwrap() == &"did"; +} + +pub(crate) trait DIDCommVerificationMethodExt { + /// Create a JWK from the verification method + fn get_jwk(&self) -> Option; +} + +impl DIDCommVerificationMethodExt for DIDVerificationMethod { + fn get_jwk(&self) -> Option { + if self.type_ == "Multikey" { + let key = if let Some(key) = self.properties.get("publicKeyMultibase") { + if let Some(key) = key.as_str() { + key.to_string() + } else { + return None; + } + } else { + return None; + }; + + let decoded = if let Ok((_, decoded)) = MultibaseBuf::new(key.clone()).decode() { + decoded + } else { + return None; + }; + + let multi_encoded = if let Ok(m) = MultiEncodedBuf::new(decoded) { + m + } else { + return None; + }; + + match JWK::from_multicodec(&multi_encoded) { + Ok(jwk) => Some(jwk), + Err(_) => { + warn!("Failed to parse JWK from multicodec ({})", key); + None + } + } + } else { + None + } + } +} + +impl AsKnownKeyPair for DIDVerificationMethod { + fn key_alg(&self, jwk: &JWK) -> KnownKeyAlg { + match &jwk.params { + Params::EC(ec) => match ec.curve.clone().unwrap_or("".to_string()).as_str() { + "P-256" => KnownKeyAlg::P256, + "secp256k1" => KnownKeyAlg::K256, + _ => KnownKeyAlg::Unsupported, + }, + Params::OKP(okp) => match okp.curve.as_str() { + "Ed25519" => KnownKeyAlg::Ed25519, + "X25519" => KnownKeyAlg::X25519, + _ => KnownKeyAlg::Unsupported, + }, + _ => KnownKeyAlg::Unsupported, + } + } + + fn as_key_pair(&self, jwk: &JWK) -> Result { + match &jwk.params { + Params::EC(ec) => { + let jwk_value = json!({"kty": "EC", "crv": ec.curve, "x": ec.x_coordinate,"y": ec.y_coordinate}); + match ec.curve.clone().unwrap_or("".to_string()).as_str() { + "P-256" => P256KeyPair::from_jwk_value(&jwk_value) + .kind(ErrorKind::Malformed, "Unable parse jwk") + .map(KnownKeyPair::P256), + "secp256k1" => K256KeyPair::from_jwk_value(&jwk_value) + .kind(ErrorKind::Malformed, "Unable parse jwk") + .map(KnownKeyPair::K256), + _ => Err(err_msg( + ErrorKind::Unsupported, + "Unsupported key type or curve", + )), + } + } + Params::OKP(okp) => { + let jwk_value = json!({"kty": "OKP", "crv": okp.curve, "x": okp.public_key}); + match okp.curve.as_str() { + "Ed25519" => Ed25519KeyPair::from_jwk_value(&jwk_value) + .kind(ErrorKind::Malformed, "Unable parse jwk") + .map(KnownKeyPair::Ed25519), + "X25519" => X25519KeyPair::from_jwk_value(&jwk_value) + .kind(ErrorKind::Malformed, "Unable parse jwk") + .map(KnownKeyPair::X25519), + _ => Err(err_msg( + ErrorKind::Unsupported, + "Unsupported key type or curve", + )), + } + } + Params::RSA(_) => Err(err_msg( + ErrorKind::Unsupported, + "Unsupported key type or curve (RSA)", + )), + Params::Symmetric(_) => Err(err_msg( + ErrorKind::Unsupported, + "Unsupported key type or curve (Symmetric)", + )), + } + } +} + +impl AsKnownKeyPairSecret for Secret { + fn key_alg(&self) -> KnownKeyAlg { + match (&self.type_, &self.secret_material) { + ( + SecretType::JsonWebKey2020, + SecretMaterial::JWK { + private_key_jwk: ref value, + }, + ) => match (value["kty"].as_str(), value["crv"].as_str()) { + (Some(kty), Some(crv)) if kty == "EC" && crv == "P-256" => KnownKeyAlg::P256, + (Some(kty), Some(crv)) if kty == "EC" && crv == "secp256k1" => KnownKeyAlg::K256, + (Some(kty), Some(crv)) if kty == "OKP" && crv == "Ed25519" => KnownKeyAlg::Ed25519, + (Some(kty), Some(crv)) if kty == "OKP" && crv == "X25519" => KnownKeyAlg::X25519, + _ => KnownKeyAlg::Unsupported, + }, + ( + SecretType::X25519KeyAgreementKey2019, + SecretMaterial::Base58 { + private_key_base58: _, + }, + ) => KnownKeyAlg::X25519, + ( + SecretType::Ed25519VerificationKey2018, + SecretMaterial::Base58 { + private_key_base58: _, + }, + ) => KnownKeyAlg::Ed25519, + ( + SecretType::X25519KeyAgreementKey2020, + SecretMaterial::Multibase { + private_key_multibase: _, + }, + ) => KnownKeyAlg::X25519, + ( + SecretType::Ed25519VerificationKey2020, + SecretMaterial::Multibase { + private_key_multibase: _, + }, + ) => KnownKeyAlg::Ed25519, + _ => KnownKeyAlg::Unsupported, + } + } + + fn as_key_pair(&self) -> Result { + match (&self.type_, &self.secret_material) { + ( + SecretType::JsonWebKey2020, + SecretMaterial::JWK { + private_key_jwk: ref value, + }, + ) => match (value["kty"].as_str(), value["crv"].as_str()) { + (Some(kty), Some(crv)) if kty == "EC" && crv == "P-256" => { + P256KeyPair::from_jwk_value(value) + .kind(ErrorKind::Malformed, "Unable parse jwk") + .map(KnownKeyPair::P256) + } + (Some(kty), Some(crv)) if kty == "EC" && crv == "secp256k1" => { + K256KeyPair::from_jwk_value(value) + .kind(ErrorKind::Malformed, "Unable parse jwk") + .map(KnownKeyPair::K256) + } + (Some(kty), Some(crv)) if kty == "OKP" && crv == "Ed25519" => { + Ed25519KeyPair::from_jwk_value(value) + .kind(ErrorKind::Malformed, "Unable parse jwk") + .map(KnownKeyPair::Ed25519) + } + (Some(kty), Some(crv)) if kty == "OKP" && crv == "X25519" => { + X25519KeyPair::from_jwk_value(value) + .kind(ErrorKind::Malformed, "Unable parse jwk") + .map(KnownKeyPair::X25519) + } + _ => Err(err_msg( + ErrorKind::Unsupported, + "Unsupported key type or curve", + )), + }, + + ( + SecretType::X25519KeyAgreementKey2019, + SecretMaterial::Base58 { + private_key_base58: ref value, + }, + ) => { + let decoded_value = bs58::decode(value) + .into_vec() + .to_didcomm("Wrong base58 value in secret material")?; + + let key_pair = X25519KeyPair::from_secret_bytes(&decoded_value).map_err(|err| { + Error::msg( + ErrorKind::Malformed, + format!( + "{}: {}", + "Unable parse x25519 secret material", + err.message() + ), + ) + })?; + + let mut jwk = json!({ + "kty": "OKP", + "crv": "X25519", + }); + + key_pair.with_public_bytes(|buf| { + jwk["x"] = Value::String(BASE64_URL_SAFE_NO_PAD.encode(buf)) + }); + + key_pair.with_secret_bytes(|buf| { + if let Some(sk) = buf { + jwk["d"] = Value::String(BASE64_URL_SAFE_NO_PAD.encode(sk)) + } + }); + + X25519KeyPair::from_jwk_value(&jwk) + .kind(ErrorKind::Malformed, "Unable parse base58 secret material") + .map(KnownKeyPair::X25519) + } + + ( + SecretType::Ed25519VerificationKey2018, + SecretMaterial::Base58 { + private_key_base58: ref value, + }, + ) => { + let decoded_value = bs58::decode(value) + .into_vec() + .to_didcomm("Wrong base58 value in secret material")?; + + let curve25519_point_size = 32; + let (d_value, x_value) = decoded_value.split_at(curve25519_point_size); + let base64_url_d_value = BASE64_URL_SAFE_NO_PAD.encode(d_value); + let base64_url_x_value = BASE64_URL_SAFE_NO_PAD.encode(x_value); + + let jwk = json!({"kty": "OKP", + "crv": "Ed25519", + "x": base64_url_x_value, + "d": base64_url_d_value + }); + + Ed25519KeyPair::from_jwk_value(&jwk) + .kind(ErrorKind::Malformed, "Unable parse base58 secret material") + .map(KnownKeyPair::Ed25519) + } + + ( + SecretType::X25519KeyAgreementKey2020, + SecretMaterial::Multibase { + private_key_multibase: ref value, + }, + ) => { + if !value.starts_with('z') { + Err(err_msg( + ErrorKind::IllegalArgument, + "Multibase must start with 'z'", + ))? + } + let decoded_multibase_value = bs58::decode(&value[1..]) + .into_vec() + .to_didcomm("Wrong multibase value in secret material")?; + + let (codec, decoded_value) = _from_multicodec(&decoded_multibase_value)?; + if codec != Codec::X25519Priv { + Err(err_msg( + ErrorKind::IllegalArgument, + "Wrong codec in multibase secret material", + ))? + } + + let key_pair = X25519KeyPair::from_secret_bytes(decoded_value).map_err(|err| { + Error::msg( + ErrorKind::Malformed, + format!( + "{}: {}", + "Unable parse x25519 secret material", + err.message() + ), + ) + })?; + + let mut jwk = json!({ + "kty": "OKP", + "crv": "X25519", + }); + + key_pair.with_public_bytes(|buf| { + jwk["x"] = Value::String(BASE64_URL_SAFE_NO_PAD.encode(buf)) + }); + + key_pair.with_secret_bytes(|buf| { + if let Some(sk) = buf { + jwk["d"] = Value::String(BASE64_URL_SAFE_NO_PAD.encode(sk)) + } + }); + + X25519KeyPair::from_jwk_value(&jwk) + .kind( + ErrorKind::Malformed, + "Unable parse multibase secret material", + ) + .map(KnownKeyPair::X25519) + } + + ( + SecretType::Ed25519VerificationKey2020, + SecretMaterial::Multibase { + private_key_multibase: ref value, + }, + ) => { + if !value.starts_with('z') { + Err(err_msg( + ErrorKind::IllegalArgument, + "Multibase must start with 'z'", + ))? + } + let decoded_multibase_value = bs58::decode(&value[1..]) + .into_vec() + .to_didcomm("Wrong multibase value in secret material")?; + + let (codec, decoded_value) = _from_multicodec(&decoded_multibase_value)?; + if codec != Codec::Ed25519Priv { + Err(err_msg( + ErrorKind::IllegalArgument, + "Wrong codec in multibase secret material", + ))? + } + + let curve25519_point_size = 32; + let (d_value, x_value) = decoded_value.split_at(curve25519_point_size); + let base64_url_d_value = BASE64_URL_SAFE_NO_PAD.encode(d_value); + let base64_url_x_value = BASE64_URL_SAFE_NO_PAD.encode(x_value); + + let jwk = json!({ + "kty": "OKP", + "crv": "Ed25519", + "x": base64_url_x_value, + "d": base64_url_d_value + }); + + Ed25519KeyPair::from_jwk_value(&jwk) + .kind( + ErrorKind::Malformed, + "Unable parse multibase secret material", + ) + .map(KnownKeyPair::Ed25519) + } + + _ => Err(err_msg( + ErrorKind::Unsupported, + "Unsupported secret method type and material combination", + )), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Codec { + X25519Pub, + Ed25519Pub, + X25519Priv, + Ed25519Priv, +} + +impl Codec { + fn codec_by_prefix(value: u32) -> Result { + match value { + 0xEC => Ok(Codec::X25519Pub), + 0xED => Ok(Codec::Ed25519Pub), + 0x1302 => Ok(Codec::X25519Priv), + 0x1300 => Ok(Codec::Ed25519Priv), + _ => Err(err_msg(ErrorKind::IllegalArgument, "Unsupported prefix")), + } + } +} + +fn _from_multicodec(value: &[u8]) -> Result<(Codec, &[u8])> { + let mut val: Cursor> = Cursor::new(value.to_vec()); + let prefix_int = val + .read_unsigned_varint_32() + .kind(ErrorKind::InvalidState, "Cannot read varint")?; + let codec = Codec::codec_by_prefix(prefix_int)?; + + let mut prefix: Cursor> = Cursor::new(Vec::new()); + prefix + .write_unsigned_varint_32(prefix_int) + .kind(ErrorKind::InvalidState, "Cannot write varint")?; + + return Ok((codec, value.split_at(prefix.into_inner().len()).1)); +} diff --git a/affinidi-messaging-didcomm/src/envelope.rs b/affinidi-messaging-didcomm/src/envelope.rs new file mode 100644 index 0000000..6c9c0a4 --- /dev/null +++ b/affinidi-messaging-didcomm/src/envelope.rs @@ -0,0 +1,133 @@ +use std::str::FromStr; + +use crate::{ + error::{err_msg, Error, ErrorKind, Result, ToResult}, + secrets::SecretsResolver, + utils::crypto::KnownKeyPair, + UnpackMetadata, +}; +use affinidi_did_resolver_cache_sdk::DIDCacheClient; +use serde::Deserialize; +use ssi::dids::Document; + +use crate::{ + jwe::{envelope::Jwe, ParsedJWE}, + jws::{Jws, ParsedJWS}, + Message, +}; + +/// High level wrapper so we can serialize and deserialize the envelope types +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum Envelope { + Jwe(Jwe), + Jws(Jws), + Message(Message), +} +impl FromStr for Envelope { + type Err = Error; + + fn from_str(s: &str) -> Result { + serde_json::from_str(s).to_didcomm("Unable deserialize envelope") + } +} + +impl Envelope { + pub fn parse(&self) -> Result { + match self { + Envelope::Jwe(jwe) => Ok(ParsedEnvelope::Jwe(jwe.to_owned().parse()?)), + Envelope::Jws(jws) => Ok(ParsedEnvelope::Jws(jws.parse()?)), + Envelope::Message(msg) => Ok(ParsedEnvelope::Message(msg.to_owned())), + } + } +} + +#[derive(Debug, Clone)] +pub enum ParsedEnvelope { + Jwe(ParsedJWE), + Jws(ParsedJWS), + Message(Message), +} + +impl ParsedEnvelope { + pub fn verify_didcomm(self) -> Result { + match self { + ParsedEnvelope::Jwe(jwe) => Ok(ParsedEnvelope::Jwe(jwe.verify_didcomm()?)), + ParsedEnvelope::Jws(_) => Ok(self), + ParsedEnvelope::Message(_) => Ok(self), + } + } + + pub fn get_type(&self) -> &str { + match self { + ParsedEnvelope::Jwe(_) => "JWE", + ParsedEnvelope::Jws(_) => "JWS", + ParsedEnvelope::Message(_) => "JMS", + } + } +} + +/// Higher level Envelope that holds all required information pertaining to a DIDComm Message +#[derive(Debug, Default)] +pub struct MetaEnvelope { + pub envelope: Option, // The raw envelope + pub parsed_envelope: Option, // The parsed envelope + pub metadata: UnpackMetadata, + pub from_kid: Option, // Key ID of Sender + pub from_did: Option, // DID of Sender (did:method:identifier) + pub from_ddoc: Option, // DID Document of Sender + pub from_key: Option, // Key of Sender + pub to_kid: Option, + pub to_did: Option, + pub to_kids_found: Vec, +} + +impl MetaEnvelope { + pub async fn new( + msg: &str, + did_resolver: &DIDCacheClient, + secrets_resolver: &S, + ) -> Result + where + S: SecretsResolver + Send, + { + let mut envelope = Self::default(); + envelope.envelope = Some(Envelope::from_str(msg)?); + envelope.parsed_envelope = Some( + envelope + .envelope + .as_ref() + .unwrap() + .parse()? + .verify_didcomm()?, + ); + + envelope._from(did_resolver, secrets_resolver).await?; + + Ok(envelope) + } + + async fn _from( + &mut self, + did_resolver: &DIDCacheClient, + secrets_resolver: &dyn SecretsResolver, + ) -> Result<&Self> { + match self.parsed_envelope.as_ref() { + Some(ParsedEnvelope::Jwe(jwe)) => { + jwe.to_owned() + .fill_envelope_from(self, did_resolver, secrets_resolver) + .await?; + } + Some(ParsedEnvelope::Jws(_)) => {} + Some(ParsedEnvelope::Message(_)) => {} + _ => { + return Err(err_msg( + ErrorKind::Malformed, + "Unable to fill envelope from", + )) + } + }; + + Ok(self) + } +} diff --git a/affinidi-messaging-didcomm/src/error.rs b/affinidi-messaging-didcomm/src/error.rs new file mode 100644 index 0000000..56cdd74 --- /dev/null +++ b/affinidi-messaging-didcomm/src/error.rs @@ -0,0 +1,202 @@ +use std::fmt; + +use serde::Serialize; +use serde_json::error::Category; + +#[derive(thiserror::Error, Debug, Copy, Clone, Eq, PartialEq, Serialize)] +pub enum ErrorKind { + #[error("DID not resolved")] + DIDNotResolved, + + #[error("DID URL not found")] + DIDUrlNotFound, + + #[error("Secret not found")] + SecretNotFound, + + #[error("Malformed")] + Malformed, + + #[error("IO error")] + IoError, + + #[error("Invalid state")] + InvalidState, + + #[error("No compatible crypto")] + NoCompatibleCrypto, + + #[error("Unsupported crypto or method")] + Unsupported, + + #[error("Illegal argument")] + IllegalArgument, + + #[error("Too many crypto operations")] + TooManyCryptoOperations, +} + +#[derive(Debug, thiserror::Error)] +#[error("{kind}: {source:#}")] +pub struct Error { + kind: ErrorKind, + pub source: anyhow::Error, +} + +impl Error { + pub fn kind(&self) -> ErrorKind { + self.kind + } + + pub fn new(kind: ErrorKind, source: E) -> Error + where + E: std::error::Error + Send + Sync + 'static, + { + Error { + kind, + source: anyhow::Error::new(source), + } + } + + pub fn msg(kind: ErrorKind, msg: D) -> Error + where + D: fmt::Display + fmt::Debug + Send + Sync + 'static, + { + Error { + kind, + source: anyhow::Error::msg(msg), + } + } +} + +pub type Result = std::result::Result; + +pub trait ResultExt { + fn kind(self, kind: ErrorKind, msg: D) -> Result + where + D: fmt::Display + fmt::Debug + Send + Sync + 'static; +} + +impl ResultExt for std::result::Result +where + E: std::error::Error + Send + Sync + 'static, +{ + fn kind(self, kind: ErrorKind, msg: D) -> Result + where + D: fmt::Display + fmt::Debug + Send + Sync + 'static, + { + self.map_err(|e| Error { + kind, + source: anyhow::Error::new(e).context(msg), + }) + } +} + +pub trait ResultExtNoContext { + fn to_error_kind(self, kind: ErrorKind) -> std::result::Result; + + fn kind_no_context(self, kind: ErrorKind, msg: D) -> Result + where + D: fmt::Display + fmt::Debug + Send + Sync + 'static; +} + +impl ResultExtNoContext for std::result::Result { + fn to_error_kind(self, kind: ErrorKind) -> std::result::Result { + self.map_err(|_| kind) + } + + fn kind_no_context(self, kind: ErrorKind, msg: D) -> Result + where + D: fmt::Display + fmt::Debug + Send + Sync + 'static, + { + self.map_err(|_| Error::msg(kind, msg)) + } +} + +pub trait ResultContext { + fn context(self, msg: D) -> Result + where + D: fmt::Display + fmt::Debug + Send + Sync + 'static; +} + +impl ResultContext for Result { + fn context(self, msg: D) -> Result + where + D: fmt::Display + fmt::Debug + Send + Sync + 'static, + { + self.map_err(|e| { + let Error { kind, source } = e; + + Error { + kind, + source: source.context(msg), + } + }) + } +} + +pub trait ToResult { + fn to_didcomm(self, msg: D) -> Result + where + D: fmt::Display + fmt::Debug + Send + Sync + 'static; +} + +impl ToResult for serde_json::Result { + fn to_didcomm(self, msg: D) -> Result + where + D: fmt::Display + fmt::Debug + Send + Sync + 'static, + { + ResultContext::context(self.map_err(|e| e.into()), msg) + } +} + +impl ToResult for bs58::decode::Result { + fn to_didcomm(self, msg: D) -> Result + where + D: fmt::Display + fmt::Debug + Send + Sync + 'static, + { + ResultContext::context(self.map_err(|e| e.into()), msg) + } +} + +impl ToResult for bs58::encode::Result { + fn to_didcomm(self, msg: D) -> Result + where + D: fmt::Display + fmt::Debug + Send + Sync + 'static, + { + ResultContext::context(self.map_err(|e| e.into()), msg) + } +} + +impl From for Error { + fn from(err: serde_json::Error) -> Self { + match err.classify() { + Category::Io | Category::Eof => Error::msg(ErrorKind::InvalidState, err.to_string()), + _ => Error::msg(ErrorKind::Malformed, err.to_string()), + } + } +} + +impl From for Error { + fn from(err: bs58::decode::Error) -> Self { + match err { + bs58::decode::Error::BufferTooSmall => { + Error::msg(ErrorKind::InvalidState, err.to_string()) + } + _ => Error::msg(ErrorKind::Malformed, err.to_string()), + } + } +} + +impl From for Error { + fn from(err: bs58::encode::Error) -> Self { + Error::msg(ErrorKind::InvalidState, err.to_string()) + } +} + +pub fn err_msg(kind: ErrorKind, msg: D) -> Error +where + D: fmt::Display + fmt::Debug + Send + Sync + 'static, +{ + Error::msg(kind, msg) +} diff --git a/affinidi-messaging-didcomm/src/jwe/decrypt.rs b/affinidi-messaging-didcomm/src/jwe/decrypt.rs new file mode 100644 index 0000000..2721f53 --- /dev/null +++ b/affinidi-messaging-didcomm/src/jwe/decrypt.rs @@ -0,0 +1,1753 @@ +use crate::{ + error::{err_msg, Error, ErrorKind, Result, ResultContext, ResultExt}, + jwe::ParsedJWE, + jwk::{FromJwkValue, ToJwkValue}, + utils::crypto::{JoseKDF, KeyWrap}, +}; +use askar_crypto::{ + buffer::SecretBytes, + encrypt::KeyAeadInPlace, + kdf::{FromKeyDerivation, KeyExchange}, + repr::{KeyGen, KeySecretBytes}, +}; +use base64::prelude::*; + +impl ParsedJWE { + pub(crate) fn decrypt( + &self, + sender: Option<(&str, &KE)>, + recipient: (&str, &KE), + ) -> Result> + where + CE: KeyAeadInPlace + KeySecretBytes, + KDF: JoseKDF, + KE: KeyExchange + KeyGen + ToJwkValue + FromJwkValue, + KW: KeyWrap + FromKeyDerivation, + { + let (skid, skey) = match sender { + Some((skid, skey)) => (Some(skid), Some(skey)), + None => (None, None), + }; + + let (kid, key) = recipient; + + if skid.map(str::as_bytes) != self.apu.as_deref() { + Err(err_msg(ErrorKind::InvalidState, "Wrong skid used"))? + } + + let encrypted_key = { + let encrypted_key = self + .jwe + .recipients + .iter() + .find(|r| r.header.kid == kid) + .ok_or_else(|| err_msg(ErrorKind::InvalidState, "Recipient not found"))? + .encrypted_key + .to_string(); + + BASE64_URL_SAFE_NO_PAD + .decode(encrypted_key) + .kind(ErrorKind::Malformed, "Unable decode encrypted_key")? + }; + + let epk = KE::from_jwk_value(&self.protected.epk).context("Unable instantiate epk")?; + + let tag = BASE64_URL_SAFE_NO_PAD + .decode(self.jwe.tag.clone()) + .kind(ErrorKind::Malformed, "Unable decode tag")?; + + let kw = KDF::derive_key( + &epk, + skey, + key, + self.protected.alg.as_str().as_bytes(), + self.apu.as_deref().unwrap_or(&[]), + &self.apv, + &tag, + true, + ) + .kind(ErrorKind::InvalidState, "Unable derive kw")?; + + let cek: CE = kw + .unwrap_key(&encrypted_key) + .kind(ErrorKind::Malformed, "Unable unwrap cek")?; + + let ciphertext = BASE64_URL_SAFE_NO_PAD + .decode(&self.jwe.ciphertext) + .kind(ErrorKind::Malformed, "Unable decode ciphertext")?; + + let iv = BASE64_URL_SAFE_NO_PAD + .decode(&self.jwe.iv) + .kind(ErrorKind::Malformed, "Unable decode iv")?; + + let plaintext = { + let mut buf = SecretBytes::with_capacity(ciphertext.len() + tag.len()); + buf.extend_from_slice(&ciphertext); + buf.extend_from_slice(&tag); + + cek.decrypt_in_place(&mut buf, &iv, self.jwe.protected.as_bytes()) + .map_err(|err| { + Error::msg( + ErrorKind::Malformed, + format!("{}: {}", "Unable decrypt content", err.message()), + ) + })?; + + buf.to_vec() + }; + + Ok(plaintext) + } +} + +#[cfg(test)] +mod tests { + use askar_crypto::{ + alg::{ + aes::{A128Kw, A256CbcHs512, A256Gcm, A256Kw, AesKey}, + chacha20::{Chacha20Key, XC20P}, + p256::P256KeyPair, + x25519::X25519KeyPair, + }, + encrypt::KeyAeadInPlace, + kdf::{ecdh_1pu::Ecdh1PU, ecdh_es::EcdhEs, FromKeyDerivation, KeyExchange}, + repr::{KeyGen, KeySecretBytes}, + }; + + use crate::{ + error::{Error, ErrorKind}, + jwe::{self, test_support::*}, + jwk::{FromJwkValue, ToJwkValue}, + utils::crypto::{JoseKDF, KeyWrap}, + }; + + #[test] + fn decrypt_works() { + // from RFC: https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-04#appendix-B + _decrypt_works::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + Some((ALICE_KID_ECDH_1PU_APP_B, ALICE_KEY_ECDH_1PU_APP_B)), + (BOB_KID_ECDH_1PU_APP_B, BOB_KEY_ECDH_1PU_APP_B), + MSG_ECDH_1PU_APP_B, + PAYLOAD_ECDH_1PU_APP_B, + ); + + // from RFC: https://tools.ietf.org/html/draft-madden-jose-ecdh-1pu-04#appendix-B + _decrypt_works::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + Some((ALICE_KID_ECDH_1PU_APP_B, ALICE_KEY_ECDH_1PU_APP_B)), + (CHARLIE_KID_ECDH_1PU_APP_B, CHARLIE_KEY_ECDH_1PU_APP_B), + MSG_ECDH_1PU_APP_B, + PAYLOAD_ECDH_1PU_APP_B, + ); + + _decrypt_works::< + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_1, BOB_KEY_X25519_1), + MSG_ANONCRYPT_X25519_XC20P, + PAYLOAD, + ); + + _decrypt_works::< + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_2, BOB_KEY_X25519_2), + MSG_ANONCRYPT_X25519_XC20P, + PAYLOAD, + ); + + _decrypt_works::< + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_3, BOB_KEY_X25519_3), + MSG_ANONCRYPT_X25519_XC20P, + PAYLOAD, + ); + + _decrypt_works::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_1, BOB_KEY_X25519_1), + MSG_ANONCRYPT_X25519_A256CBC, + PAYLOAD, + ); + + _decrypt_works::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_2, BOB_KEY_X25519_2), + MSG_ANONCRYPT_X25519_A256CBC, + PAYLOAD, + ); + + _decrypt_works::, EcdhEs<'_, X25519KeyPair>, X25519KeyPair, AesKey>( + None, + (BOB_KID_X25519_1, BOB_KEY_X25519_1), + MSG_ANONCRYPT_X25519_A256GSM, + PAYLOAD, + ); + + _decrypt_works::, EcdhEs<'_, X25519KeyPair>, X25519KeyPair, AesKey>( + None, + (BOB_KID_X25519_2, BOB_KEY_X25519_2), + MSG_ANONCRYPT_X25519_A256GSM, + PAYLOAD, + ); + + _decrypt_works::, EcdhEs<'_, P256KeyPair>, P256KeyPair, AesKey>( + None, + (BOB_KID_P256_1, BOB_KEY_P256_1), + MSG_ANONCRYPT_P256_XC20P, + PAYLOAD, + ); + + _decrypt_works::, EcdhEs<'_, P256KeyPair>, P256KeyPair, AesKey>( + None, + (BOB_KID_P256_2, BOB_KEY_P256_2), + MSG_ANONCRYPT_P256_XC20P, + PAYLOAD, + ); + + _decrypt_works::, EcdhEs<'_, P256KeyPair>, P256KeyPair, AesKey>( + None, + (BOB_KID_P256_1, BOB_KEY_P256_1), + MSG_ANONCRYPT_P256_A256CBC, + PAYLOAD, + ); + + _decrypt_works::, EcdhEs<'_, P256KeyPair>, P256KeyPair, AesKey>( + None, + (BOB_KID_P256_2, BOB_KEY_P256_2), + MSG_ANONCRYPT_P256_A256CBC, + PAYLOAD, + ); + + _decrypt_works::, EcdhEs<'_, P256KeyPair>, P256KeyPair, AesKey>( + None, + (BOB_KID_P256_1, BOB_KEY_P256_1), + MSG_ANONCRYPT_P256_A256GSM, + PAYLOAD, + ); + + _decrypt_works::, EcdhEs<'_, P256KeyPair>, P256KeyPair, AesKey>( + None, + (BOB_KID_P256_2, BOB_KEY_P256_2), + MSG_ANONCRYPT_P256_A256GSM, + PAYLOAD, + ); + + _decrypt_works::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + Some((ALICE_KID_X25519_1, ALICE_PKEY_X25519_1)), + (BOB_KID_X25519_1, BOB_KEY_X25519_1), + MSG_AUTHCRYPT_X25519_A256CBC, + PAYLOAD, + ); + + _decrypt_works::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + Some((ALICE_KID_X25519_1, ALICE_PKEY_X25519_1)), + (BOB_KID_X25519_2, BOB_KEY_X25519_2), + MSG_AUTHCRYPT_X25519_A256CBC, + PAYLOAD, + ); + + _decrypt_works::, Ecdh1PU<'_, P256KeyPair>, P256KeyPair, AesKey>( + Some((ALICE_KID_P256_1, ALICE_PKEY_P256_1)), + (BOB_KID_P256_1, BOB_KEY_P256_1), + MSG_AUTHCRYPT_P256_A256CBC, + PAYLOAD, + ); + + _decrypt_works::, Ecdh1PU<'_, P256KeyPair>, P256KeyPair, AesKey>( + Some((ALICE_KID_P256_1, ALICE_PKEY_P256_1)), + (BOB_KID_P256_2, BOB_KEY_P256_2), + MSG_AUTHCRYPT_P256_A256CBC, + PAYLOAD, + ); + + /// TODO: P-384 and P-521 support after solving https://github.com/hyperledger/aries-askar/issues/10 + + fn _decrypt_works( + sender: Option<(&str, &str)>, + recipient: (&str, &str), + msg: &str, + payload: &str, + ) where + CE: KeyAeadInPlace + KeySecretBytes, + KDF: JoseKDF, + KE: KeyExchange + KeyGen + ToJwkValue + FromJwkValue, + KW: KeyWrap + FromKeyDerivation, + { + let res = _decrypt::(sender, recipient, msg); + let res = res.expect("res is err"); + assert_eq!(res, payload.as_bytes()); + } + } + + #[test] + fn decrypt_works_authcrypt_different_skid() { + let res = _decrypt::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + Some((ALICE_KID_X25519_1, ALICE_PKEY_P256_1)), + (BOB_KID_P256_1, BOB_KEY_P256_1), + MSG_AUTHCRYPT_P256_A256CBC, + ); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::InvalidState); + assert_eq!(format!("{}", err), "Invalid state: Wrong skid used"); + } + + #[test] + fn decrypt_works_authcrypt_no_skid() { + let res = _decrypt::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + None, + (BOB_KID_P256_1, BOB_KEY_P256_1), + MSG_AUTHCRYPT_P256_A256CBC, + ); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::InvalidState); + assert_eq!(format!("{}", err), "Invalid state: Wrong skid used"); + } + + #[test] + fn decrypt_works_anoncrypt_skid_present() { + let res = + _decrypt::, EcdhEs<'_, P256KeyPair>, P256KeyPair, AesKey>( + Some((ALICE_KID_P256_1, ALICE_PKEY_P256_1)), + (BOB_KID_P256_1, BOB_KEY_P256_1), + MSG_ANONCRYPT_P256_A256CBC, + ); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::InvalidState); + assert_eq!(format!("{}", err), "Invalid state: Wrong skid used"); + } + + #[test] + fn decrypt_works_recipient_not_found() { + let res = _decrypt::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + Some((ALICE_KID_P256_1, ALICE_PKEY_P256_1)), + (BOB_KID_X25519_1, BOB_KEY_P256_1), + MSG_AUTHCRYPT_P256_A256CBC, + ); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::InvalidState); + assert_eq!(format!("{}", err), "Invalid state: Recipient not found"); + } + + #[test] + fn decrypt_works_undecodable_encrypted_key() { + let res = _decrypt::< + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_1, BOB_KEY_X25519_1), + MSG_ANONCRYPT_X25519_XC20P_UNDECODABLE_EC, + ); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + assert_eq!( + format!("{}", err), + "Malformed: Unable decode encrypted_key: Invalid symbol 33, offset 0." + ); + } + + #[test] + fn decrypt_works_undecodable_tag() { + let res = _decrypt::< + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_1, BOB_KEY_X25519_1), + MSG_ANONCRYPT_X25519_XC20P_UNDECODABLE_TAG, + ); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: Unable decode tag: Invalid symbol 33, offset 0." + ); + } + + #[test] + fn decrypt_works_undecodable_iv() { + let res = _decrypt::< + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_1, BOB_KEY_X25519_1), + MSG_ANONCRYPT_X25519_XC20P_UNDECODABLE_IV, + ); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: Unable decode iv: Invalid symbol 33, offset 0." + ); + } + + #[test] + fn decrypt_works_undecodable_ciphertext() { + let res = _decrypt::< + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_1, BOB_KEY_X25519_1), + MSG_ANONCRYPT_X25519_XC20P_UNDECODABLE_CIPHERTEXT, + ); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: Unable decode ciphertext: Invalid symbol 33, offset 0." + ); + } + + #[test] + #[ignore = "https://github.com/hyperledger/aries-askar/issues/28"] + // There is no key type or curve checking for FromJwk implementations in askar crypto + // Most probably it can't open invalid curve attack vectors as invalid points should be + // found by rust crypto, but still looks dangerous. + fn decrypt_works_epk_different_curve() { + let res = _decrypt::< + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_1, BOB_KEY_X25519_1), + MSG_ANONCRYPT_X25519_XC20P_EPK_DIFFERENT_CURVE, + ); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + assert_eq!( + format!("{}", err), + "Malformed: Uanble instantiate epk: Unable produce jwk" + ); + } + + #[test] + fn decrypt_works_epk_wrong_point() { + let res = + _decrypt::, EcdhEs<'_, P256KeyPair>, P256KeyPair, AesKey>( + None, + (BOB_KID_P256_1, BOB_KEY_P256_1), + MSG_ANONCRYPT_P256_XC20P_EPK_WRONG_POINT, + ); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: Unable instantiate epk: Unable produce jwk: Invalid key data", + ); + } + + #[test] + fn decrypt_works_different_recipient_key() { + _decrypt_works_different_recipient_key::< + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_1, BOB_KEY_X25519_2), + MSG_ANONCRYPT_X25519_XC20P, + ); + + _decrypt_works_different_recipient_key::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_1, BOB_KEY_X25519_2), + MSG_ANONCRYPT_X25519_A256CBC, + ); + + _decrypt_works_different_recipient_key::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_1, BOB_KEY_X25519_2), + MSG_ANONCRYPT_X25519_A256GSM, + ); + + _decrypt_works_different_recipient_key::< + Chacha20Key, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + None, + (BOB_KID_P256_1, BOB_KEY_P256_2), + MSG_ANONCRYPT_P256_XC20P, + ); + + _decrypt_works_different_recipient_key::< + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + None, + (BOB_KID_P256_1, BOB_KEY_P256_2), + MSG_ANONCRYPT_P256_A256CBC, + ); + + _decrypt_works_different_recipient_key::< + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + None, + (BOB_KID_P256_1, BOB_KEY_P256_2), + MSG_ANONCRYPT_P256_A256GSM, + ); + + _decrypt_works_different_recipient_key::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + Some((ALICE_KID_X25519_1, ALICE_PKEY_X25519_1)), + (BOB_KID_X25519_1, BOB_KEY_X25519_2), + MSG_AUTHCRYPT_X25519_A256CBC, + ); + + _decrypt_works_different_recipient_key::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + Some((ALICE_KID_P256_1, ALICE_PKEY_P256_1)), + (BOB_KID_P256_1, BOB_KEY_P256_2), + MSG_AUTHCRYPT_P256_A256CBC, + ); + + fn _decrypt_works_different_recipient_key( + sender: Option<(&str, &str)>, + recipient: (&str, &str), + msg: &str, + ) where + CE: KeyAeadInPlace + KeySecretBytes, + KDF: JoseKDF, + KE: KeyExchange + KeyGen + ToJwkValue + FromJwkValue, + KW: KeyWrap + FromKeyDerivation, + { + let res = _decrypt::(sender, recipient, msg); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: Unable unwrap cek: Malformed: Unable decrypt key: Encryption error: Unable decrypt key: Encryption error", + ); + } + } + + #[test] + fn decrypt_works_changed_enc_key() { + _decrypt_works_changed_enc_key::< + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_1, BOB_KEY_X25519_1), + MSG_ANONCRYPT_X25519_XC20P_CHANGED_ENC_KEY, + ); + + _decrypt_works_changed_enc_key::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_1, BOB_KEY_X25519_1), + MSG_ANONCRYPT_X25519_A256CBC_CHANGED_ENC_KEY, + ); + + _decrypt_works_changed_enc_key::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_1, BOB_KEY_X25519_1), + MSG_ANONCRYPT_X25519_A256GSM_CHANGED_ENC_KEY, + ); + + _decrypt_works_changed_enc_key::< + Chacha20Key, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + None, + (BOB_KID_P256_1, BOB_KEY_P256_1), + MSG_ANONCRYPT_P256_XC20P_CHANGED_ENC_KEY, + ); + + _decrypt_works_changed_enc_key::< + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + None, + (BOB_KID_P256_1, BOB_KEY_P256_1), + MSG_ANONCRYPT_P256_A256CBC_CHANGED_ENC_KEY, + ); + + _decrypt_works_changed_enc_key::< + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + None, + (BOB_KID_P256_1, BOB_KEY_P256_1), + MSG_ANONCRYPT_P256_A256GSM_CHANGED_ENC_KEY, + ); + + _decrypt_works_changed_enc_key::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + Some((ALICE_KID_X25519_1, ALICE_PKEY_X25519_1)), + (BOB_KID_X25519_1, BOB_KEY_X25519_1), + MSG_AUTHCRYPT_X25519_A256CBC_CHANGED_ENC_KEY, + ); + + _decrypt_works_changed_enc_key::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + Some((ALICE_KID_P256_1, ALICE_PKEY_P256_1)), + (BOB_KID_P256_1, BOB_KEY_P256_1), + MSG_AUTHCRYPT_P256_A256CBC_CHANGED_ENC_KEY, + ); + + fn _decrypt_works_changed_enc_key( + sender: Option<(&str, &str)>, + recipient: (&str, &str), + msg: &str, + ) where + CE: KeyAeadInPlace + KeySecretBytes, + KDF: JoseKDF, + KE: KeyExchange + KeyGen + ToJwkValue + FromJwkValue, + KW: KeyWrap + FromKeyDerivation, + { + let res = _decrypt::(sender, recipient, msg); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: Unable unwrap cek: Malformed: Unable decrypt key: Encryption error: Unable decrypt key: Encryption error", + ); + } + } + + #[test] + fn decrypt_works_changed_second_enc_key() { + decrypt_works_changed_second_enc_key::< + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_2, BOB_KEY_X25519_2), + MSG_ANONCRYPT_X25519_XC20P_CHANGED_ENC_KEY, + PAYLOAD, + ); + + decrypt_works_changed_second_enc_key::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_2, BOB_KEY_X25519_2), + MSG_ANONCRYPT_X25519_A256CBC_CHANGED_ENC_KEY, + PAYLOAD, + ); + + decrypt_works_changed_second_enc_key::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_2, BOB_KEY_X25519_2), + MSG_ANONCRYPT_X25519_A256GSM_CHANGED_ENC_KEY, + PAYLOAD, + ); + + decrypt_works_changed_second_enc_key::< + Chacha20Key, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + None, + (BOB_KID_P256_2, BOB_KEY_P256_2), + MSG_ANONCRYPT_P256_XC20P_CHANGED_ENC_KEY, + PAYLOAD, + ); + + decrypt_works_changed_second_enc_key::< + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + None, + (BOB_KID_P256_2, BOB_KEY_P256_2), + MSG_ANONCRYPT_P256_A256CBC_CHANGED_ENC_KEY, + PAYLOAD, + ); + + decrypt_works_changed_second_enc_key::< + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + None, + (BOB_KID_P256_2, BOB_KEY_P256_2), + MSG_ANONCRYPT_P256_A256GSM_CHANGED_ENC_KEY, + PAYLOAD, + ); + + decrypt_works_changed_second_enc_key::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + Some((ALICE_KID_X25519_1, ALICE_PKEY_X25519_1)), + (BOB_KID_X25519_2, BOB_KEY_X25519_2), + MSG_AUTHCRYPT_X25519_A256CBC_CHANGED_ENC_KEY, + PAYLOAD, + ); + + decrypt_works_changed_second_enc_key::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + Some((ALICE_KID_P256_1, ALICE_PKEY_P256_1)), + (BOB_KID_P256_2, BOB_KEY_P256_2), + MSG_AUTHCRYPT_P256_A256CBC_CHANGED_ENC_KEY, + PAYLOAD, + ); + + fn decrypt_works_changed_second_enc_key( + sender: Option<(&str, &str)>, + recipient: (&str, &str), + msg: &str, + payload: &str, + ) where + CE: KeyAeadInPlace + KeySecretBytes, + KDF: JoseKDF, + KE: KeyExchange + KeyGen + ToJwkValue + FromJwkValue, + KW: KeyWrap + FromKeyDerivation, + { + let res = _decrypt::(sender, recipient, msg); + let res = res.expect("res is err"); + assert_eq!(&res, payload.as_bytes()); + } + } + + #[test] + fn decrypt_works_changed_ciphertext() { + _decrypt_works_changed_ciphertext::< + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_1, BOB_KEY_X25519_1), + MSG_ANONCRYPT_X25519_XC20P_CHANGED_CIPHERTEXT, + ); + + _decrypt_works_changed_ciphertext::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_1, BOB_KEY_X25519_1), + MSG_ANONCRYPT_X25519_A256CBC_CHANGED_CIPHERTEXT, + ); + + _decrypt_works_changed_ciphertext::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + (BOB_KID_X25519_1, BOB_KEY_X25519_1), + MSG_ANONCRYPT_X25519_A256GSM_CHANGED_CIPHERTEXT, + ); + + _decrypt_works_changed_ciphertext::< + Chacha20Key, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + None, + (BOB_KID_P256_1, BOB_KEY_P256_1), + MSG_ANONCRYPT_P256_XC20P_CHANGED_CIPHERTEXT, + ); + + _decrypt_works_changed_ciphertext::< + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + None, + (BOB_KID_P256_1, BOB_KEY_P256_1), + MSG_ANONCRYPT_P256_A256CBC_CHANGED_CIPHERTEXT, + ); + + _decrypt_works_changed_ciphertext::< + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + None, + (BOB_KID_P256_1, BOB_KEY_P256_1), + MSG_ANONCRYPT_P256_A256GSM_CHANGED_CIPHERTEXT, + ); + + _decrypt_works_changed_ciphertext::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + Some((ALICE_KID_X25519_1, ALICE_PKEY_X25519_1)), + (BOB_KID_X25519_1, BOB_KEY_X25519_1), + MSG_AUTHCRYPT_X25519_A256CBC_CHANGED_CIPHERTEXT, + ); + + _decrypt_works_changed_ciphertext::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + Some((ALICE_KID_P256_1, ALICE_PKEY_P256_1)), + (BOB_KID_P256_1, BOB_KEY_P256_1), + MSG_AUTHCRYPT_P256_A256CBC_CHANGED_CIPHERTEXT, + ); + + fn _decrypt_works_changed_ciphertext( + sender: Option<(&str, &str)>, + recipient: (&str, &str), + msg: &str, + ) where + CE: KeyAeadInPlace + KeySecretBytes, + KDF: JoseKDF, + KE: KeyExchange + KeyGen + ToJwkValue + FromJwkValue, + KW: KeyWrap + FromKeyDerivation, + { + let res = _decrypt::(sender, recipient, msg); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: Unable decrypt content: AEAD decryption error", + ); + } + } + + fn _decrypt( + sender: Option<(&str, &str)>, + recipient: (&str, &str), + msg: &str, + ) -> Result, Error> + where + CE: KeyAeadInPlace + KeySecretBytes, + KDF: JoseKDF, + KE: KeyExchange + KeyGen + ToJwkValue + FromJwkValue, + KW: KeyWrap + FromKeyDerivation, + { + let _sender = sender.map(|(kid, k)| (kid, KE::from_jwk(k).expect("Unable from_jwk"))); + let sender = _sender.as_ref().map(|(kid, k)| (*kid, k)); + + let recipient = ( + recipient.0, + &KE::from_jwk(recipient.1).expect("Unable from_jwk"), + ); + + let msg = jwe::parse(msg).expect("Unable parse"); + + msg.decrypt::(sender, recipient) + } + + const PAYLOAD: &str = r#"{"id":"1234567890","typ":"application/didcomm-plain+json","type":"http://example.com/protocols/lets_do_lunch/1.0/proposal","from":"did:example:alice","to":["did:example:bob"],"created_time":1516269022,"expires_time":1516385931,"body":{"messagespecificattribute":"and its value"}}"#; + + const MSG_ANONCRYPT_X25519_XC20P: &str = r#" + { + "ciphertext":"KWS7gJU7TbyJlcT9dPkCw-ohNigGaHSukR9MUqFM0THbCTCNkY-g5tahBFyszlKIKXs7qOtqzYyWbPou2q77XlAeYs93IhF6NvaIjyNqYklvj-OtJt9W2Pj5CLOMdsR0C30wchGoXd6wEQZY4ttbzpxYznqPmJ0b9KW6ZP-l4_DSRYe9B-1oSWMNmqMPwluKbtguC-riy356Xbu2C9ShfWmpmjz1HyJWQhZfczuwkWWlE63g26FMskIZZd_jGpEhPFHKUXCFwbuiw_Iy3R0BIzmXXdK_w7PZMMPbaxssl2UeJmLQgCAP8j8TukxV96EKa6rGgULvlo7qibjJqsS5j03bnbxkuxwbfyu3OxwgVzFWlyHbUH6p", + "protected":"eyJlcGsiOnsia3R5IjoiT0tQIiwiY3J2IjoiWDI1NTE5IiwieCI6IkpIanNtSVJaQWFCMHpSR193TlhMVjJyUGdnRjAwaGRIYlc1cmo4ZzBJMjQifSwiYXB2IjoiTmNzdUFuclJmUEs2OUEtcmtaMEw5WFdVRzRqTXZOQzNaZzc0QlB6NTNQQSIsInR5cCI6ImFwcGxpY2F0aW9uL2RpZGNvbW0tZW5jcnlwdGVkK2pzb24iLCJlbmMiOiJYQzIwUCIsImFsZyI6IkVDREgtRVMrQTI1NktXIn0", + "recipients":[ + { + "encrypted_key":"3n1olyBR3nY7ZGAprOx-b7wYAKza6cvOYjNwVg3miTnbLwPP_FmE1A", + "header":{ + "kid":"did:example:bob#key-x25519-1" + } + }, + { + "encrypted_key":"j5eSzn3kCrIkhQAWPnEwrFPMW6hG0zF_y37gUvvc5gvlzsuNX4hXrQ", + "header":{ + "kid":"did:example:bob#key-x25519-2" + } + }, + { + "encrypted_key":"TEWlqlq-ao7Lbynf0oZYhxs7ZB39SUWBCK4qjqQqfeItfwmNyDm73A", + "header":{ + "kid":"did:example:bob#key-x25519-3" + } + } + ], + "tag":"6ylC_iAs4JvDQzXeY6MuYQ", + "iv":"ESpmcyGiZpRjc5urDela21TOOTW8Wqd1" + } + "#; + + const MSG_ANONCRYPT_X25519_XC20P_UNDECODABLE_EC: &str = r#" + { + "ciphertext":"KWS7gJU7TbyJlcT9dPkCw-ohNigGaHSukR9MUqFM0THbCTCNkY-g5tahBFyszlKIKXs7qOtqzYyWbPou2q77XlAeYs93IhF6NvaIjyNqYklvj-OtJt9W2Pj5CLOMdsR0C30wchGoXd6wEQZY4ttbzpxYznqPmJ0b9KW6ZP-l4_DSRYe9B-1oSWMNmqMPwluKbtguC-riy356Xbu2C9ShfWmpmjz1HyJWQhZfczuwkWWlE63g26FMskIZZd_jGpEhPFHKUXCFwbuiw_Iy3R0BIzmXXdK_w7PZMMPbaxssl2UeJmLQgCAP8j8TukxV96EKa6rGgULvlo7qibjJqsS5j03bnbxkuxwbfyu3OxwgVzFWlyHbUH6p", + "protected":"eyJlcGsiOnsia3R5IjoiT0tQIiwiY3J2IjoiWDI1NTE5IiwieCI6IkpIanNtSVJaQWFCMHpSR193TlhMVjJyUGdnRjAwaGRIYlc1cmo4ZzBJMjQifSwiYXB2IjoiTmNzdUFuclJmUEs2OUEtcmtaMEw5WFdVRzRqTXZOQzNaZzc0QlB6NTNQQSIsInR5cCI6ImFwcGxpY2F0aW9uL2RpZGNvbW0tZW5jcnlwdGVkK2pzb24iLCJlbmMiOiJYQzIwUCIsImFsZyI6IkVDREgtRVMrQTI1NktXIn0", + "recipients":[ + { + "encrypted_key":"!3n1olyBR3nY7ZGAprOx-b7wYAKza6cvOYjNwVg3miTnbLwPP_FmE1A", + "header":{ + "kid":"did:example:bob#key-x25519-1" + } + }, + { + "encrypted_key":"j5eSzn3kCrIkhQAWPnEwrFPMW6hG0zF_y37gUvvc5gvlzsuNX4hXrQ", + "header":{ + "kid":"did:example:bob#key-x25519-2" + } + }, + { + "encrypted_key":"TEWlqlq-ao7Lbynf0oZYhxs7ZB39SUWBCK4qjqQqfeItfwmNyDm73A", + "header":{ + "kid":"did:example:bob#key-x25519-3" + } + } + ], + "tag":"6ylC_iAs4JvDQzXeY6MuYQ", + "iv":"ESpmcyGiZpRjc5urDela21TOOTW8Wqd1" + } + "#; + + const MSG_ANONCRYPT_X25519_XC20P_UNDECODABLE_TAG: &str = r#" + { + "ciphertext":"KWS7gJU7TbyJlcT9dPkCw-ohNigGaHSukR9MUqFM0THbCTCNkY-g5tahBFyszlKIKXs7qOtqzYyWbPou2q77XlAeYs93IhF6NvaIjyNqYklvj-OtJt9W2Pj5CLOMdsR0C30wchGoXd6wEQZY4ttbzpxYznqPmJ0b9KW6ZP-l4_DSRYe9B-1oSWMNmqMPwluKbtguC-riy356Xbu2C9ShfWmpmjz1HyJWQhZfczuwkWWlE63g26FMskIZZd_jGpEhPFHKUXCFwbuiw_Iy3R0BIzmXXdK_w7PZMMPbaxssl2UeJmLQgCAP8j8TukxV96EKa6rGgULvlo7qibjJqsS5j03bnbxkuxwbfyu3OxwgVzFWlyHbUH6p", + "protected":"eyJlcGsiOnsia3R5IjoiT0tQIiwiY3J2IjoiWDI1NTE5IiwieCI6IkpIanNtSVJaQWFCMHpSR193TlhMVjJyUGdnRjAwaGRIYlc1cmo4ZzBJMjQifSwiYXB2IjoiTmNzdUFuclJmUEs2OUEtcmtaMEw5WFdVRzRqTXZOQzNaZzc0QlB6NTNQQSIsInR5cCI6ImFwcGxpY2F0aW9uL2RpZGNvbW0tZW5jcnlwdGVkK2pzb24iLCJlbmMiOiJYQzIwUCIsImFsZyI6IkVDREgtRVMrQTI1NktXIn0", + "recipients":[ + { + "encrypted_key":"3n1olyBR3nY7ZGAprOx-b7wYAKza6cvOYjNwVg3miTnbLwPP_FmE1A", + "header":{ + "kid":"did:example:bob#key-x25519-1" + } + }, + { + "encrypted_key":"j5eSzn3kCrIkhQAWPnEwrFPMW6hG0zF_y37gUvvc5gvlzsuNX4hXrQ", + "header":{ + "kid":"did:example:bob#key-x25519-2" + } + }, + { + "encrypted_key":"TEWlqlq-ao7Lbynf0oZYhxs7ZB39SUWBCK4qjqQqfeItfwmNyDm73A", + "header":{ + "kid":"did:example:bob#key-x25519-3" + } + } + ], + "tag":"!6ylC_iAs4JvDQzXeY6MuYQ", + "iv":"ESpmcyGiZpRjc5urDela21TOOTW8Wqd1" + } + "#; + + const MSG_ANONCRYPT_X25519_XC20P_UNDECODABLE_IV: &str = r#" + { + "ciphertext":"KWS7gJU7TbyJlcT9dPkCw-ohNigGaHSukR9MUqFM0THbCTCNkY-g5tahBFyszlKIKXs7qOtqzYyWbPou2q77XlAeYs93IhF6NvaIjyNqYklvj-OtJt9W2Pj5CLOMdsR0C30wchGoXd6wEQZY4ttbzpxYznqPmJ0b9KW6ZP-l4_DSRYe9B-1oSWMNmqMPwluKbtguC-riy356Xbu2C9ShfWmpmjz1HyJWQhZfczuwkWWlE63g26FMskIZZd_jGpEhPFHKUXCFwbuiw_Iy3R0BIzmXXdK_w7PZMMPbaxssl2UeJmLQgCAP8j8TukxV96EKa6rGgULvlo7qibjJqsS5j03bnbxkuxwbfyu3OxwgVzFWlyHbUH6p", + "protected":"eyJlcGsiOnsia3R5IjoiT0tQIiwiY3J2IjoiWDI1NTE5IiwieCI6IkpIanNtSVJaQWFCMHpSR193TlhMVjJyUGdnRjAwaGRIYlc1cmo4ZzBJMjQifSwiYXB2IjoiTmNzdUFuclJmUEs2OUEtcmtaMEw5WFdVRzRqTXZOQzNaZzc0QlB6NTNQQSIsInR5cCI6ImFwcGxpY2F0aW9uL2RpZGNvbW0tZW5jcnlwdGVkK2pzb24iLCJlbmMiOiJYQzIwUCIsImFsZyI6IkVDREgtRVMrQTI1NktXIn0", + "recipients":[ + { + "encrypted_key":"3n1olyBR3nY7ZGAprOx-b7wYAKza6cvOYjNwVg3miTnbLwPP_FmE1A", + "header":{ + "kid":"did:example:bob#key-x25519-1" + } + }, + { + "encrypted_key":"j5eSzn3kCrIkhQAWPnEwrFPMW6hG0zF_y37gUvvc5gvlzsuNX4hXrQ", + "header":{ + "kid":"did:example:bob#key-x25519-2" + } + }, + { + "encrypted_key":"TEWlqlq-ao7Lbynf0oZYhxs7ZB39SUWBCK4qjqQqfeItfwmNyDm73A", + "header":{ + "kid":"did:example:bob#key-x25519-3" + } + } + ], + "tag":"6ylC_iAs4JvDQzXeY6MuYQ", + "iv":"!ESpmcyGiZpRjc5urDela21TOOTW8Wqd1" + } + "#; + + const MSG_ANONCRYPT_X25519_XC20P_UNDECODABLE_CIPHERTEXT: &str = r#" + { + "ciphertext":"!KWS7gJU7TbyJlcT9dPkCw-ohNigGaHSukR9MUqFM0THbCTCNkY-g5tahBFyszlKIKXs7qOtqzYyWbPou2q77XlAeYs93IhF6NvaIjyNqYklvj-OtJt9W2Pj5CLOMdsR0C30wchGoXd6wEQZY4ttbzpxYznqPmJ0b9KW6ZP-l4_DSRYe9B-1oSWMNmqMPwluKbtguC-riy356Xbu2C9ShfWmpmjz1HyJWQhZfczuwkWWlE63g26FMskIZZd_jGpEhPFHKUXCFwbuiw_Iy3R0BIzmXXdK_w7PZMMPbaxssl2UeJmLQgCAP8j8TukxV96EKa6rGgULvlo7qibjJqsS5j03bnbxkuxwbfyu3OxwgVzFWlyHbUH6p", + "protected":"eyJlcGsiOnsia3R5IjoiT0tQIiwiY3J2IjoiWDI1NTE5IiwieCI6IkpIanNtSVJaQWFCMHpSR193TlhMVjJyUGdnRjAwaGRIYlc1cmo4ZzBJMjQifSwiYXB2IjoiTmNzdUFuclJmUEs2OUEtcmtaMEw5WFdVRzRqTXZOQzNaZzc0QlB6NTNQQSIsInR5cCI6ImFwcGxpY2F0aW9uL2RpZGNvbW0tZW5jcnlwdGVkK2pzb24iLCJlbmMiOiJYQzIwUCIsImFsZyI6IkVDREgtRVMrQTI1NktXIn0", + "recipients":[ + { + "encrypted_key":"3n1olyBR3nY7ZGAprOx-b7wYAKza6cvOYjNwVg3miTnbLwPP_FmE1A", + "header":{ + "kid":"did:example:bob#key-x25519-1" + } + }, + { + "encrypted_key":"j5eSzn3kCrIkhQAWPnEwrFPMW6hG0zF_y37gUvvc5gvlzsuNX4hXrQ", + "header":{ + "kid":"did:example:bob#key-x25519-2" + } + }, + { + "encrypted_key":"TEWlqlq-ao7Lbynf0oZYhxs7ZB39SUWBCK4qjqQqfeItfwmNyDm73A", + "header":{ + "kid":"did:example:bob#key-x25519-3" + } + } + ], + "tag":"6ylC_iAs4JvDQzXeY6MuYQ", + "iv":"ESpmcyGiZpRjc5urDela21TOOTW8Wqd1" + } + "#; + + const MSG_ANONCRYPT_X25519_XC20P_EPK_DIFFERENT_CURVE: &str = r#" + { + "ciphertext":"KWS7gJU7TbyJlcT9dPkCw-ohNigGaHSukR9MUqFM0THbCTCNkY-g5tahBFyszlKIKXs7qOtqzYyWbPou2q77XlAeYs93IhF6NvaIjyNqYklvj-OtJt9W2Pj5CLOMdsR0C30wchGoXd6wEQZY4ttbzpxYznqPmJ0b9KW6ZP-l4_DSRYe9B-1oSWMNmqMPwluKbtguC-riy356Xbu2C9ShfWmpmjz1HyJWQhZfczuwkWWlE63g26FMskIZZd_jGpEhPFHKUXCFwbuiw_Iy3R0BIzmXXdK_w7PZMMPbaxssl2UeJmLQgCAP8j8TukxV96EKa6rGgULvlo7qibjJqsS5j03bnbxkuxwbfyu3OxwgVzFWlyHbUH6p", + "protected":"eyJlcGsiOnsiY3J2IjoiUC0yNTYiLCJrdHkiOiJFQyIsIngiOiJfTlRkWTQ2b1MzRm00X21JbG1XZUYyT0dTU2x1NTV1MkNJWVYyd3N0aENnIiwieSI6IkZwQnF5aVNzRHQ1bmNveVpNYW9Pci1hTE1yY01hbXRpZlNhSy1FQWtfc2sifSwiYXB2IjoiTmNzdUFuclJmUEs2OUEtcmtaMEw5WFdVRzRqTXZOQzNaZzc0QlB6NTNQQSIsInR5cCI6ImFwcGxpY2F0aW9uL2RpZGNvbW0tZW5jcnlwdGVkK2pzb24iLCJlbmMiOiJYQzIwUCIsImFsZyI6IkVDREgtRVMrQTI1NktXIn0=", + "recipients":[ + { + "encrypted_key":"3n1olyBR3nY7ZGAprOx-b7wYAKza6cvOYjNwVg3miTnbLwPP_FmE1A", + "header":{ + "kid":"did:example:bob#key-x25519-1" + } + }, + { + "encrypted_key":"j5eSzn3kCrIkhQAWPnEwrFPMW6hG0zF_y37gUvvc5gvlzsuNX4hXrQ", + "header":{ + "kid":"did:example:bob#key-x25519-2" + } + }, + { + "encrypted_key":"TEWlqlq-ao7Lbynf0oZYhxs7ZB39SUWBCK4qjqQqfeItfwmNyDm73A", + "header":{ + "kid":"did:example:bob#key-x25519-3" + } + } + ], + "tag":"6ylC_iAs4JvDQzXeY6MuYQ", + "iv":"ESpmcyGiZpRjc5urDela21TOOTW8Wqd1" + } + "#; + + const MSG_ANONCRYPT_X25519_XC20P_CHANGED_ENC_KEY: &str = r#" + { + "ciphertext":"KWS7gJU7TbyJlcT9dPkCw-ohNigGaHSukR9MUqFM0THbCTCNkY-g5tahBFyszlKIKXs7qOtqzYyWbPou2q77XlAeYs93IhF6NvaIjyNqYklvj-OtJt9W2Pj5CLOMdsR0C30wchGoXd6wEQZY4ttbzpxYznqPmJ0b9KW6ZP-l4_DSRYe9B-1oSWMNmqMPwluKbtguC-riy356Xbu2C9ShfWmpmjz1HyJWQhZfczuwkWWlE63g26FMskIZZd_jGpEhPFHKUXCFwbuiw_Iy3R0BIzmXXdK_w7PZMMPbaxssl2UeJmLQgCAP8j8TukxV96EKa6rGgULvlo7qibjJqsS5j03bnbxkuxwbfyu3OxwgVzFWlyHbUH6p", + "protected":"eyJlcGsiOnsia3R5IjoiT0tQIiwiY3J2IjoiWDI1NTE5IiwieCI6IkpIanNtSVJaQWFCMHpSR193TlhMVjJyUGdnRjAwaGRIYlc1cmo4ZzBJMjQifSwiYXB2IjoiTmNzdUFuclJmUEs2OUEtcmtaMEw5WFdVRzRqTXZOQzNaZzc0QlB6NTNQQSIsInR5cCI6ImFwcGxpY2F0aW9uL2RpZGNvbW0tZW5jcnlwdGVkK2pzb24iLCJlbmMiOiJYQzIwUCIsImFsZyI6IkVDREgtRVMrQTI1NktXIn0", + "recipients":[ + { + "encrypted_key":"2n1olyBR3nY7ZGAprOx-b7wYAKza6cvOYjNwVg3miTnbLwPP_FmE1A", + "header":{ + "kid":"did:example:bob#key-x25519-1" + } + }, + { + "encrypted_key":"j5eSzn3kCrIkhQAWPnEwrFPMW6hG0zF_y37gUvvc5gvlzsuNX4hXrQ", + "header":{ + "kid":"did:example:bob#key-x25519-2" + } + }, + { + "encrypted_key":"TEWlqlq-ao7Lbynf0oZYhxs7ZB39SUWBCK4qjqQqfeItfwmNyDm73A", + "header":{ + "kid":"did:example:bob#key-x25519-3" + } + } + ], + "tag":"6ylC_iAs4JvDQzXeY6MuYQ", + "iv":"ESpmcyGiZpRjc5urDela21TOOTW8Wqd1" + } + "#; + + const MSG_ANONCRYPT_X25519_XC20P_CHANGED_CIPHERTEXT: &str = r#" + { + "ciphertext":"WWS7gJU7TbyJlcT9dPkCw-ohNigGaHSukR9MUqFM0THbCTCNkY-g5tahBFyszlKIKXs7qOtqzYyWbPou2q77XlAeYs93IhF6NvaIjyNqYklvj-OtJt9W2Pj5CLOMdsR0C30wchGoXd6wEQZY4ttbzpxYznqPmJ0b9KW6ZP-l4_DSRYe9B-1oSWMNmqMPwluKbtguC-riy356Xbu2C9ShfWmpmjz1HyJWQhZfczuwkWWlE63g26FMskIZZd_jGpEhPFHKUXCFwbuiw_Iy3R0BIzmXXdK_w7PZMMPbaxssl2UeJmLQgCAP8j8TukxV96EKa6rGgULvlo7qibjJqsS5j03bnbxkuxwbfyu3OxwgVzFWlyHbUH6p", + "protected":"eyJlcGsiOnsia3R5IjoiT0tQIiwiY3J2IjoiWDI1NTE5IiwieCI6IkpIanNtSVJaQWFCMHpSR193TlhMVjJyUGdnRjAwaGRIYlc1cmo4ZzBJMjQifSwiYXB2IjoiTmNzdUFuclJmUEs2OUEtcmtaMEw5WFdVRzRqTXZOQzNaZzc0QlB6NTNQQSIsInR5cCI6ImFwcGxpY2F0aW9uL2RpZGNvbW0tZW5jcnlwdGVkK2pzb24iLCJlbmMiOiJYQzIwUCIsImFsZyI6IkVDREgtRVMrQTI1NktXIn0", + "recipients":[ + { + "encrypted_key":"3n1olyBR3nY7ZGAprOx-b7wYAKza6cvOYjNwVg3miTnbLwPP_FmE1A", + "header":{ + "kid":"did:example:bob#key-x25519-1" + } + }, + { + "encrypted_key":"j5eSzn3kCrIkhQAWPnEwrFPMW6hG0zF_y37gUvvc5gvlzsuNX4hXrQ", + "header":{ + "kid":"did:example:bob#key-x25519-2" + } + }, + { + "encrypted_key":"TEWlqlq-ao7Lbynf0oZYhxs7ZB39SUWBCK4qjqQqfeItfwmNyDm73A", + "header":{ + "kid":"did:example:bob#key-x25519-3" + } + } + ], + "tag":"6ylC_iAs4JvDQzXeY6MuYQ", + "iv":"ESpmcyGiZpRjc5urDela21TOOTW8Wqd1" + } + "#; + + const MSG_ANONCRYPT_X25519_A256CBC: &str = r#" + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC1FUytBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwiYXB1IjpudWxsLCJhcHYiOiJiV3ZqdDZGM2hsUzlSeE56ZVFCQVBFOGJRdnBiQnhUa3gzS0VOUEY2aTlFIiwiZXBrIjp7ImNydiI6IlgyNTUxOSIsImt0eSI6Ik9LUCIsIngiOiJ3NEpZU0dkc1BXZldkeHZSLS12R2FTTHdZX0dTRTRwVlFhUmRRMEpLU0ZJIn19", + "recipients":[{ + "header":{ + "kid":"did:example:bob#key-x25519-1" + }, + "encrypted_key":"EPhEGPspLjEvJ_v1W5zlJGfg88huhrTEUQCfKzKXmZjp6Y7Vv9Rv0mQgb7XyeZrxKHwgMo5Vxoref9ngeT17WUuHAEEPDteF" + },{ + "header":{ + "kid":"did:example:bob#key-x25519-2" + }, + "encrypted_key":"XchtfMRjcs24QNpWBk81zW74mQFR8ungyaBlpGjaOFHWf5dlCcrGvZLIT-UEY--S_UZEVknNwOOQ-lq4F5MGtkDVOpd-HoxD" + }], + "iv":"nzmtYMd1crLyY4rRWUAL1A", + "ciphertext":"bDM_50XL_ArVWWgpiMZO2NFFDZqc0jFBL1RFFESE_saPogffoyDEafYFYD4OlCH9yiEOIHpZZFHrgSx66xrPrkAXfl-d3Ppin2mhx0EgiV4h8yqiN1J_dQ-b_gTsP5djIj3VxMF4mkg34oIRxuaL71DQbhWgsUw-yH16KaBHkXhQnj7T4j6lQeSrP9qNYhMD0UbXcaVzT2AvmwdhRuOuI17DrfwQMVsZnh7Zh9WwJVPwUw7pto0_YpqUacq4kq3z9ZJ1pfFEstVnRwRAosjf0UCwRzCG6nw8OJYDqS3v3_2leRsjuAk-Ro4OMt5mPki0TIBeWl8JP-5rU9kGr2o7DMUtLcNoM5NHOeKiw4BgI04lFRD-azqNXJQwlBV9Uzlq", + "tag":"PytY0PYyjAXno1ykdMVE75LKdZA6d8yH1Ju0jZf0n8c" + } + "#; + + const MSG_ANONCRYPT_X25519_A256CBC_CHANGED_ENC_KEY: &str = r#" + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC1FUytBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwiYXB1IjpudWxsLCJhcHYiOiJiV3ZqdDZGM2hsUzlSeE56ZVFCQVBFOGJRdnBiQnhUa3gzS0VOUEY2aTlFIiwiZXBrIjp7ImNydiI6IlgyNTUxOSIsImt0eSI6Ik9LUCIsIngiOiJ3NEpZU0dkc1BXZldkeHZSLS12R2FTTHdZX0dTRTRwVlFhUmRRMEpLU0ZJIn19", + "recipients":[{ + "header":{ + "kid":"did:example:bob#key-x25519-1" + }, + "encrypted_key":"PPhEGPspLjEvJ_v1W5zlJGfg88huhrTEUQCfKzKXmZjp6Y7Vv9Rv0mQgb7XyeZrxKHwgMo5Vxoref9ngeT17WUuHAEEPDteF" + },{ + "header":{ + "kid":"did:example:bob#key-x25519-2" + }, + "encrypted_key":"XchtfMRjcs24QNpWBk81zW74mQFR8ungyaBlpGjaOFHWf5dlCcrGvZLIT-UEY--S_UZEVknNwOOQ-lq4F5MGtkDVOpd-HoxD" + }], + "iv":"nzmtYMd1crLyY4rRWUAL1A", + "ciphertext":"bDM_50XL_ArVWWgpiMZO2NFFDZqc0jFBL1RFFESE_saPogffoyDEafYFYD4OlCH9yiEOIHpZZFHrgSx66xrPrkAXfl-d3Ppin2mhx0EgiV4h8yqiN1J_dQ-b_gTsP5djIj3VxMF4mkg34oIRxuaL71DQbhWgsUw-yH16KaBHkXhQnj7T4j6lQeSrP9qNYhMD0UbXcaVzT2AvmwdhRuOuI17DrfwQMVsZnh7Zh9WwJVPwUw7pto0_YpqUacq4kq3z9ZJ1pfFEstVnRwRAosjf0UCwRzCG6nw8OJYDqS3v3_2leRsjuAk-Ro4OMt5mPki0TIBeWl8JP-5rU9kGr2o7DMUtLcNoM5NHOeKiw4BgI04lFRD-azqNXJQwlBV9Uzlq", + "tag":"PytY0PYyjAXno1ykdMVE75LKdZA6d8yH1Ju0jZf0n8c" + } + "#; + + const MSG_ANONCRYPT_X25519_A256CBC_CHANGED_CIPHERTEXT: &str = r#" + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC1FUytBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwiYXB1IjpudWxsLCJhcHYiOiJiV3ZqdDZGM2hsUzlSeE56ZVFCQVBFOGJRdnBiQnhUa3gzS0VOUEY2aTlFIiwiZXBrIjp7ImNydiI6IlgyNTUxOSIsImt0eSI6Ik9LUCIsIngiOiJ3NEpZU0dkc1BXZldkeHZSLS12R2FTTHdZX0dTRTRwVlFhUmRRMEpLU0ZJIn19", + "recipients":[{ + "header":{ + "kid":"did:example:bob#key-x25519-1" + }, + "encrypted_key":"EPhEGPspLjEvJ_v1W5zlJGfg88huhrTEUQCfKzKXmZjp6Y7Vv9Rv0mQgb7XyeZrxKHwgMo5Vxoref9ngeT17WUuHAEEPDteF" + },{ + "header":{ + "kid":"did:example:bob#key-x25519-2" + }, + "encrypted_key":"XchtfMRjcs24QNpWBk81zW74mQFR8ungyaBlpGjaOFHWf5dlCcrGvZLIT-UEY--S_UZEVknNwOOQ-lq4F5MGtkDVOpd-HoxD" + }], + "iv":"nzmtYMd1crLyY4rRWUAL1A", + "ciphertext":"DDM_50XL_ArVWWgpiMZO2NFFDZqc0jFBL1RFFESE_saPogffoyDEafYFYD4OlCH9yiEOIHpZZFHrgSx66xrPrkAXfl-d3Ppin2mhx0EgiV4h8yqiN1J_dQ-b_gTsP5djIj3VxMF4mkg34oIRxuaL71DQbhWgsUw-yH16KaBHkXhQnj7T4j6lQeSrP9qNYhMD0UbXcaVzT2AvmwdhRuOuI17DrfwQMVsZnh7Zh9WwJVPwUw7pto0_YpqUacq4kq3z9ZJ1pfFEstVnRwRAosjf0UCwRzCG6nw8OJYDqS3v3_2leRsjuAk-Ro4OMt5mPki0TIBeWl8JP-5rU9kGr2o7DMUtLcNoM5NHOeKiw4BgI04lFRD-azqNXJQwlBV9Uzlq", + "tag":"PytY0PYyjAXno1ykdMVE75LKdZA6d8yH1Ju0jZf0n8c" + } + "#; + + const MSG_ANONCRYPT_X25519_A256GSM: &str = r#" + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC1FUytBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIiwiYXB1IjpudWxsLCJhcHYiOiJiV3ZqdDZGM2hsUzlSeE56ZVFCQVBFOGJRdnBiQnhUa3gzS0VOUEY2aTlFIiwiZXBrIjp7ImNydiI6IlgyNTUxOSIsImt0eSI6Ik9LUCIsIngiOiJQRUNaUGJMdGNPYUppTFZXa3drUW1GS1RFQmExRUVBRDZhWDIxLWR3VGlrIn19", + "recipients":[{ + "header":{ + "kid":"did:example:bob#key-x25519-1" + }, + "encrypted_key":"jGcdRf7qopP0eNmvoNxi1pGBoaP3aO4CuJLutWTL0mudQhgzWgageQ" + },{ + "header":{ + "kid":"did:example:bob#key-x25519-2" + }, + "encrypted_key":"VqW0WFtTA_UnMfSwe4cxMDSCwDr65eb8D_xqNufGD01gah8lGd9Glg" + }], + "iv":"o0gOvMWbBiNzkAFk", + "ciphertext":"X357FFJqN7dW72W4q_qFSKudBtz9bkbqOfFvANVi5JgqUdVDSGFdUvJxDnPw8EkiF-DY4tYoFaONo5vOUjZniJ9R0Qg9W77pfa-J8ZgNp_lyfu5drRyGXcel95I2F5GuD3ZOGhNkeCMF2BHiOk5hQMcGsM2aJzbSnEnWjrjd0nzFAF6YGOz5UaUxUVJQOVgtzXvvcWgoU8LfGqKe5wm66Ul-PP8OmNSvLjJow0fXg9iP7qU1dwc6Qc2o_Pu80RMNUWw_A_dGaWc_lL4UbO5K7z2oPtqmDI0GTnu4LQHOcaJ3jdy-YyI_yTcnfzrFRPgHnNOhWG0OHQKywiDW5HUVu46MryOk2unqo25UMaInSubThYB3kzAQ", + "tag":"_kECR4ioqoMVe9m42fdDcQ" + } + "#; + + const MSG_ANONCRYPT_X25519_A256GSM_CHANGED_ENC_KEY: &str = r#" + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC1FUytBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIiwiYXB1IjpudWxsLCJhcHYiOiJiV3ZqdDZGM2hsUzlSeE56ZVFCQVBFOGJRdnBiQnhUa3gzS0VOUEY2aTlFIiwiZXBrIjp7ImNydiI6IlgyNTUxOSIsImt0eSI6Ik9LUCIsIngiOiJQRUNaUGJMdGNPYUppTFZXa3drUW1GS1RFQmExRUVBRDZhWDIxLWR3VGlrIn19", + "recipients":[{ + "header":{ + "kid":"did:example:bob#key-x25519-1" + }, + "encrypted_key":"GGcdRf7qopP0eNmvoNxi1pGBoaP3aO4CuJLutWTL0mudQhgzWgageQ" + },{ + "header":{ + "kid":"did:example:bob#key-x25519-2" + }, + "encrypted_key":"VqW0WFtTA_UnMfSwe4cxMDSCwDr65eb8D_xqNufGD01gah8lGd9Glg" + }], + "iv":"o0gOvMWbBiNzkAFk", + "ciphertext":"X357FFJqN7dW72W4q_qFSKudBtz9bkbqOfFvANVi5JgqUdVDSGFdUvJxDnPw8EkiF-DY4tYoFaONo5vOUjZniJ9R0Qg9W77pfa-J8ZgNp_lyfu5drRyGXcel95I2F5GuD3ZOGhNkeCMF2BHiOk5hQMcGsM2aJzbSnEnWjrjd0nzFAF6YGOz5UaUxUVJQOVgtzXvvcWgoU8LfGqKe5wm66Ul-PP8OmNSvLjJow0fXg9iP7qU1dwc6Qc2o_Pu80RMNUWw_A_dGaWc_lL4UbO5K7z2oPtqmDI0GTnu4LQHOcaJ3jdy-YyI_yTcnfzrFRPgHnNOhWG0OHQKywiDW5HUVu46MryOk2unqo25UMaInSubThYB3kzAQ", + "tag":"_kECR4ioqoMVe9m42fdDcQ" + } + "#; + + const MSG_ANONCRYPT_X25519_A256GSM_CHANGED_CIPHERTEXT: &str = r#" + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC1FUytBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIiwiYXB1IjpudWxsLCJhcHYiOiJiV3ZqdDZGM2hsUzlSeE56ZVFCQVBFOGJRdnBiQnhUa3gzS0VOUEY2aTlFIiwiZXBrIjp7ImNydiI6IlgyNTUxOSIsImt0eSI6Ik9LUCIsIngiOiJQRUNaUGJMdGNPYUppTFZXa3drUW1GS1RFQmExRUVBRDZhWDIxLWR3VGlrIn19", + "recipients":[{ + "header":{ + "kid":"did:example:bob#key-x25519-1" + }, + "encrypted_key":"jGcdRf7qopP0eNmvoNxi1pGBoaP3aO4CuJLutWTL0mudQhgzWgageQ" + },{ + "header":{ + "kid":"did:example:bob#key-x25519-2" + }, + "encrypted_key":"VqW0WFtTA_UnMfSwe4cxMDSCwDr65eb8D_xqNufGD01gah8lGd9Glg" + }], + "iv":"o0gOvMWbBiNzkAFk", + "ciphertext":"3357FFJqN7dW72W4q_qFSKudBtz9bkbqOfFvANVi5JgqUdVDSGFdUvJxDnPw8EkiF-DY4tYoFaONo5vOUjZniJ9R0Qg9W77pfa-J8ZgNp_lyfu5drRyGXcel95I2F5GuD3ZOGhNkeCMF2BHiOk5hQMcGsM2aJzbSnEnWjrjd0nzFAF6YGOz5UaUxUVJQOVgtzXvvcWgoU8LfGqKe5wm66Ul-PP8OmNSvLjJow0fXg9iP7qU1dwc6Qc2o_Pu80RMNUWw_A_dGaWc_lL4UbO5K7z2oPtqmDI0GTnu4LQHOcaJ3jdy-YyI_yTcnfzrFRPgHnNOhWG0OHQKywiDW5HUVu46MryOk2unqo25UMaInSubThYB3kzAQ", + "tag":"_kECR4ioqoMVe9m42fdDcQ" + } + "#; + + const MSG_ANONCRYPT_P256_XC20P: &str = r#" + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC1FUytBMjU2S1ciLCJlbmMiOiJYQzIwUCIsImFwdSI6bnVsbCwiYXB2Ijoiei1McXB2VlhEYl9zR1luM21qUUxwdXUyQ1FMZXdZdVpvVFdPSVhQSDNGTSIsImVwayI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6IkNVMklaXzEzZ0JhR2VxczRpUm9CclJoOVBHbzBab2lEQlFSNkdHQUJWbmciLCJ5IjoiaDJUTkh1dU5STXZNZW51TDBZcDJsU1h3dzRMNUFpc0ZKSDRXMVVzSlQ5MCJ9fQ", + "recipients":[{ + "header":{ + "kid":"did:example:bob#key-p256-1" + } + ,"encrypted_key":"scQxV9YQ4mQrUHgl6yAnBFDXNZAiIs_15bmoErUmoYm0HtuRclPoQg" + },{ + "header":{ + "kid":"did:example:bob#key-p256-2" + }, + "encrypted_key":"CqZ-HDH2j0NC-eoUueNLKyAuMQXjQyw8bJHYM2f-lxJVm3eXCdmm2g" + }], + "iv":"Vg1uyuQKrU6Kw8OJK38WCpYFxW0suAP9", + "ciphertext":"2nIm3xQcFR3HXbUPF1HS_D92OGVDvL0nIi6O5ol5tnMIa09NxJtbVAYIG7ZrkT9314PqXn_Rq77hgGE6FAOgO7aNYLyUJh0JCC_i2p_XOWuk20BYyBsmmRvVpg0DY3I1Lb-Vg1pT9pEy09gsMSLhbfqk0_TFJB1rcqzR8W0YZB5mX_53nMRf1ZatDEg4rDogSekWEGTBnlTNRua8-zoI4573SfgJ-ONt7Z_KbGO-sdRkmqXhfYNcbUyoMF9JSa-kraVuWHZP9hTz8-7R020EXfb4jodMWVOMMAiJYk1Cd7tetHXpLPdtuokaapofmtL_SNftAX2CB6ULf0axrHUNtvUyjAPvpgvSuvQuMrDlaXn16MQJ_q55", + "tag":"etLTQvKsTvF629fykLiUDg" + } + "#; + + const MSG_ANONCRYPT_P256_XC20P_CHANGED_ENC_KEY: &str = r#" + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC1FUytBMjU2S1ciLCJlbmMiOiJYQzIwUCIsImFwdSI6bnVsbCwiYXB2Ijoiei1McXB2VlhEYl9zR1luM21qUUxwdXUyQ1FMZXdZdVpvVFdPSVhQSDNGTSIsImVwayI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6IkNVMklaXzEzZ0JhR2VxczRpUm9CclJoOVBHbzBab2lEQlFSNkdHQUJWbmciLCJ5IjoiaDJUTkh1dU5STXZNZW51TDBZcDJsU1h3dzRMNUFpc0ZKSDRXMVVzSlQ5MCJ9fQ", + "recipients":[{ + "header":{ + "kid":"did:example:bob#key-p256-1" + } + ,"encrypted_key":"ccQxV9YQ4mQrUHgl6yAnBFDXNZAiIs_15bmoErUmoYm0HtuRclPoQg" + },{ + "header":{ + "kid":"did:example:bob#key-p256-2" + }, + "encrypted_key":"CqZ-HDH2j0NC-eoUueNLKyAuMQXjQyw8bJHYM2f-lxJVm3eXCdmm2g" + }], + "iv":"Vg1uyuQKrU6Kw8OJK38WCpYFxW0suAP9", + "ciphertext":"2nIm3xQcFR3HXbUPF1HS_D92OGVDvL0nIi6O5ol5tnMIa09NxJtbVAYIG7ZrkT9314PqXn_Rq77hgGE6FAOgO7aNYLyUJh0JCC_i2p_XOWuk20BYyBsmmRvVpg0DY3I1Lb-Vg1pT9pEy09gsMSLhbfqk0_TFJB1rcqzR8W0YZB5mX_53nMRf1ZatDEg4rDogSekWEGTBnlTNRua8-zoI4573SfgJ-ONt7Z_KbGO-sdRkmqXhfYNcbUyoMF9JSa-kraVuWHZP9hTz8-7R020EXfb4jodMWVOMMAiJYk1Cd7tetHXpLPdtuokaapofmtL_SNftAX2CB6ULf0axrHUNtvUyjAPvpgvSuvQuMrDlaXn16MQJ_q55", + "tag":"etLTQvKsTvF629fykLiUDg" + } + "#; + + const MSG_ANONCRYPT_P256_XC20P_EPK_WRONG_POINT: &str = r#" + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC1FUytBMjU2S1ciLCJlbmMiOiJYQzIwUCIsImFwdSI6bnVsbCwiYXB2Ijoiei1McXB2VlhEYl9zR1luM21qUUxwdXUyQ1FMZXdZdVpvVFdPSVhQSDNGTSIsImVwayI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6IkZSQW1UQmljUFZJXy1aRnF2WEJwNzZhV2pZM0gzYlpGZlhocHRUNm1ETnciLCJ5IjoiLXZ0LTFIaHRvVjBwN2xrbGIxTnRvMWRhU0lqQnV3cVZzbGIwcC1uOWRrdyJ9fQ==", + "recipients":[{ + "header":{ + "kid":"did:example:bob#key-p256-1" + } + ,"encrypted_key":"scQxV9YQ4mQrUHgl6yAnBFDXNZAiIs_15bmoErUmoYm0HtuRclPoQg" + },{ + "header":{ + "kid":"did:example:bob#key-p256-2" + }, + "encrypted_key":"CqZ-HDH2j0NC-eoUueNLKyAuMQXjQyw8bJHYM2f-lxJVm3eXCdmm2g" + }], + "iv":"Vg1uyuQKrU6Kw8OJK38WCpYFxW0suAP9", + "ciphertext":"2nIm3xQcFR3HXbUPF1HS_D92OGVDvL0nIi6O5ol5tnMIa09NxJtbVAYIG7ZrkT9314PqXn_Rq77hgGE6FAOgO7aNYLyUJh0JCC_i2p_XOWuk20BYyBsmmRvVpg0DY3I1Lb-Vg1pT9pEy09gsMSLhbfqk0_TFJB1rcqzR8W0YZB5mX_53nMRf1ZatDEg4rDogSekWEGTBnlTNRua8-zoI4573SfgJ-ONt7Z_KbGO-sdRkmqXhfYNcbUyoMF9JSa-kraVuWHZP9hTz8-7R020EXfb4jodMWVOMMAiJYk1Cd7tetHXpLPdtuokaapofmtL_SNftAX2CB6ULf0axrHUNtvUyjAPvpgvSuvQuMrDlaXn16MQJ_q55", + "tag":"etLTQvKsTvF629fykLiUDg" + } + "#; + + const MSG_ANONCRYPT_P256_XC20P_CHANGED_CIPHERTEXT: &str = r#" + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC1FUytBMjU2S1ciLCJlbmMiOiJYQzIwUCIsImFwdSI6bnVsbCwiYXB2Ijoiei1McXB2VlhEYl9zR1luM21qUUxwdXUyQ1FMZXdZdVpvVFdPSVhQSDNGTSIsImVwayI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6IkNVMklaXzEzZ0JhR2VxczRpUm9CclJoOVBHbzBab2lEQlFSNkdHQUJWbmciLCJ5IjoiaDJUTkh1dU5STXZNZW51TDBZcDJsU1h3dzRMNUFpc0ZKSDRXMVVzSlQ5MCJ9fQ", + "recipients":[{ + "header":{ + "kid":"did:example:bob#key-p256-1" + } + ,"encrypted_key":"scQxV9YQ4mQrUHgl6yAnBFDXNZAiIs_15bmoErUmoYm0HtuRclPoQg" + },{ + "header":{ + "kid":"did:example:bob#key-p256-2" + }, + "encrypted_key":"CqZ-HDH2j0NC-eoUueNLKyAuMQXjQyw8bJHYM2f-lxJVm3eXCdmm2g" + }], + "iv":"Vg1uyuQKrU6Kw8OJK38WCpYFxW0suAP9", + "ciphertext":"nnIm3xQcFR3HXbUPF1HS_D92OGVDvL0nIi6O5ol5tnMIa09NxJtbVAYIG7ZrkT9314PqXn_Rq77hgGE6FAOgO7aNYLyUJh0JCC_i2p_XOWuk20BYyBsmmRvVpg0DY3I1Lb-Vg1pT9pEy09gsMSLhbfqk0_TFJB1rcqzR8W0YZB5mX_53nMRf1ZatDEg4rDogSekWEGTBnlTNRua8-zoI4573SfgJ-ONt7Z_KbGO-sdRkmqXhfYNcbUyoMF9JSa-kraVuWHZP9hTz8-7R020EXfb4jodMWVOMMAiJYk1Cd7tetHXpLPdtuokaapofmtL_SNftAX2CB6ULf0axrHUNtvUyjAPvpgvSuvQuMrDlaXn16MQJ_q55", + "tag":"etLTQvKsTvF629fykLiUDg" + } + "#; + + const MSG_ANONCRYPT_P256_A256CBC: &str = r#" + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC1FUytBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwiYXB1IjpudWxsLCJhcHYiOiJ6LUxxcHZWWERiX3NHWW4zbWpRTHB1dTJDUUxld1l1Wm9UV09JWFBIM0ZNIiwiZXBrIjp7ImNydiI6IlAtMjU2Iiwia3R5IjoiRUMiLCJ4IjoiLXh2cnltSzFLNnN3R0tLZ2VERUJyQmdhSnhWSFd1R09nTU1wTWc2OFRzYyIsInkiOiJGa3hqeUdzdVlQRVZuS3lSQ1l2a3J2ejl6cno3OTFMX2dCcWtzOWc2Y2R3In19", + "recipients":[{ + "header":{ + "kid":"did:example:bob#key-p256-1" + }, + "encrypted_key":"ReussjPSY0lT5zdj_XZgBTylAxa2yBL4Sc6rM7E42p-PsDJF60sdhAj2tm9Zg6POBI09YAy44Sygdw3rbLvcKshj6M9uD3Nc" + },{ + "header":{ + "kid":"did:example:bob#key-p256-2" + }, + "encrypted_key":"s9jBWrES_9Fvnjxof3EDlDM_i3Ds3ry6pXeIOh2n8kp15L3e6CQ2yyNeEmOwuFhVwfUxuRQbM3mVlKLtySh6UtmyzSm0UZpl" + }], + "iv":"GIBpOTfWoimvyheRyaDlVQ", + "ciphertext":"Dk1zcI0w1zyJjaLuaWzbOOLxsaiin1yMqHo2pI--7dJpgNID4YJCbdorekPJO5dcN8JX97DimOsZZyTde5wjZGWhRzTfHwIZT4OK9--gETjifddt2JUk0Dp72lpV35Ie6_5xU8KZEBlcajO5StBWg2sWua3hEpXTq3yJDAEMTp0Sz-CKqMlQ4frqPHJapjOz010-xVIR8cmmLoVOIA2wb5pD3uK8LLYwxUJzR-Eq5dlD6k1TtDCdZQEY38AT7a3QJ4Q49fIkZ7epREn9qLobqZwiFmbnWFfGbGY_1tPZg6eXmCTzqYxqLqXFLMgNLWOHPQPO-G77z1KzzkIbBV7xGuCxofVdjXGhHGGeUh7cymg9OGtAghMLDFvCd3kXT-FS", + "tag":"8FKQxyMz5f7gojQNK3Jtr5xmI3-12_VA4CJteW3fH0A" + } + "#; + + const MSG_ANONCRYPT_P256_A256CBC_CHANGED_ENC_KEY: &str = r#" + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC1FUytBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwiYXB1IjpudWxsLCJhcHYiOiJ6LUxxcHZWWERiX3NHWW4zbWpRTHB1dTJDUUxld1l1Wm9UV09JWFBIM0ZNIiwiZXBrIjp7ImNydiI6IlAtMjU2Iiwia3R5IjoiRUMiLCJ4IjoiLXh2cnltSzFLNnN3R0tLZ2VERUJyQmdhSnhWSFd1R09nTU1wTWc2OFRzYyIsInkiOiJGa3hqeUdzdVlQRVZuS3lSQ1l2a3J2ejl6cno3OTFMX2dCcWtzOWc2Y2R3In19", + "recipients":[{ + "header":{ + "kid":"did:example:bob#key-p256-1" + }, + "encrypted_key":"eeussjPSY0lT5zdj_XZgBTylAxa2yBL4Sc6rM7E42p-PsDJF60sdhAj2tm9Zg6POBI09YAy44Sygdw3rbLvcKshj6M9uD3Nc" + },{ + "header":{ + "kid":"did:example:bob#key-p256-2" + }, + "encrypted_key":"s9jBWrES_9Fvnjxof3EDlDM_i3Ds3ry6pXeIOh2n8kp15L3e6CQ2yyNeEmOwuFhVwfUxuRQbM3mVlKLtySh6UtmyzSm0UZpl" + }], + "iv":"GIBpOTfWoimvyheRyaDlVQ", + "ciphertext":"Dk1zcI0w1zyJjaLuaWzbOOLxsaiin1yMqHo2pI--7dJpgNID4YJCbdorekPJO5dcN8JX97DimOsZZyTde5wjZGWhRzTfHwIZT4OK9--gETjifddt2JUk0Dp72lpV35Ie6_5xU8KZEBlcajO5StBWg2sWua3hEpXTq3yJDAEMTp0Sz-CKqMlQ4frqPHJapjOz010-xVIR8cmmLoVOIA2wb5pD3uK8LLYwxUJzR-Eq5dlD6k1TtDCdZQEY38AT7a3QJ4Q49fIkZ7epREn9qLobqZwiFmbnWFfGbGY_1tPZg6eXmCTzqYxqLqXFLMgNLWOHPQPO-G77z1KzzkIbBV7xGuCxofVdjXGhHGGeUh7cymg9OGtAghMLDFvCd3kXT-FS", + "tag":"8FKQxyMz5f7gojQNK3Jtr5xmI3-12_VA4CJteW3fH0A" + } + "#; + + const MSG_ANONCRYPT_P256_A256CBC_CHANGED_CIPHERTEXT: &str = r#" + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC1FUytBMjU2S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwiYXB1IjpudWxsLCJhcHYiOiJ6LUxxcHZWWERiX3NHWW4zbWpRTHB1dTJDUUxld1l1Wm9UV09JWFBIM0ZNIiwiZXBrIjp7ImNydiI6IlAtMjU2Iiwia3R5IjoiRUMiLCJ4IjoiLXh2cnltSzFLNnN3R0tLZ2VERUJyQmdhSnhWSFd1R09nTU1wTWc2OFRzYyIsInkiOiJGa3hqeUdzdVlQRVZuS3lSQ1l2a3J2ejl6cno3OTFMX2dCcWtzOWc2Y2R3In19", + "recipients":[{ + "header":{ + "kid":"did:example:bob#key-p256-1" + }, + "encrypted_key":"ReussjPSY0lT5zdj_XZgBTylAxa2yBL4Sc6rM7E42p-PsDJF60sdhAj2tm9Zg6POBI09YAy44Sygdw3rbLvcKshj6M9uD3Nc" + },{ + "header":{ + "kid":"did:example:bob#key-p256-2" + }, + "encrypted_key":"s9jBWrES_9Fvnjxof3EDlDM_i3Ds3ry6pXeIOh2n8kp15L3e6CQ2yyNeEmOwuFhVwfUxuRQbM3mVlKLtySh6UtmyzSm0UZpl" + }], + "iv":"GIBpOTfWoimvyheRyaDlVQ", + "ciphertext":"kk1zcI0w1zyJjaLuaWzbOOLxsaiin1yMqHo2pI--7dJpgNID4YJCbdorekPJO5dcN8JX97DimOsZZyTde5wjZGWhRzTfHwIZT4OK9--gETjifddt2JUk0Dp72lpV35Ie6_5xU8KZEBlcajO5StBWg2sWua3hEpXTq3yJDAEMTp0Sz-CKqMlQ4frqPHJapjOz010-xVIR8cmmLoVOIA2wb5pD3uK8LLYwxUJzR-Eq5dlD6k1TtDCdZQEY38AT7a3QJ4Q49fIkZ7epREn9qLobqZwiFmbnWFfGbGY_1tPZg6eXmCTzqYxqLqXFLMgNLWOHPQPO-G77z1KzzkIbBV7xGuCxofVdjXGhHGGeUh7cymg9OGtAghMLDFvCd3kXT-FS", + "tag":"8FKQxyMz5f7gojQNK3Jtr5xmI3-12_VA4CJteW3fH0A" + } + "#; + + const MSG_ANONCRYPT_P256_A256GSM: &str = r#" + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC1FUytBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIiwiYXB1IjpudWxsLCJhcHYiOiJ6LUxxcHZWWERiX3NHWW4zbWpRTHB1dTJDUUxld1l1Wm9UV09JWFBIM0ZNIiwiZXBrIjp7ImNydiI6IlAtMjU2Iiwia3R5IjoiRUMiLCJ4IjoidDZYaDNGTTNna21SZ2dyUTBwMElxb3RDejdNV2tPRWRwNXZRQkNwWW9ZcyIsInkiOiJfNzgzWjRHbGFkQ095VDBFSFczU3FuVldRazNocWlSX2MwVjJGMURZVzhjIn19", + "recipients":[{ + "header":{ + "kid":"did:example:bob#key-p256-1" + }, + "encrypted_key":"ZDH8GpSLzcO3r1Gw-QQPGRCIP3a_LXzeiOCqgecz7-YHBnAPEyF87Q" + },{ + "header":{ + "kid":"did:example:bob#key-p256-2" + }, + "encrypted_key":"J2xoNA3fLYgPVkocnXr4KafSLOhVbMZipRCEkPBuUIF4z2hpP0tdmg" + }], + "iv":"MR5gOGVOZvqHVO7n", + "ciphertext":"QdzPSf5z2yrNYYXgfUAe6-7K3QpppzP3rmzkrlBgmmD_ePdcY0riNvhwBm2Bt_oc0ebK6aq-TKkAoI4P__ISe2TH8RwPuc0HjSkUZtjW8Y8x-wnuED9HEX-HuEAZlM2aFuVy3R_104kbwkAeFtrTTN00ylclOr0ldTXXjTsio58j0Yqrq7Iz6VuScYkdVfJStEswfa4e3BEO_vRAKpr2jVsj6HL5RqZUpCF-DWy_pDsu-IRpYinglvLAfkT6O9g8QApLULOdO4r8TlD1ns-Jx9lLTZZLaILSJZSuzrV6kXgwIOUnyKeA6q2NNbddoxwgpxKGfJ8Bk-Fu0uC8UP_xHVZPRiUd9PyFY6e8dcFNoumu7g4iVN7L", + "tag":"E5tdRcTYnxaf2p5e4wgNGQ" + } + "#; + + const MSG_ANONCRYPT_P256_A256GSM_CHANGED_ENC_KEY: &str = r#" + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC1FUytBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIiwiYXB1IjpudWxsLCJhcHYiOiJ6LUxxcHZWWERiX3NHWW4zbWpRTHB1dTJDUUxld1l1Wm9UV09JWFBIM0ZNIiwiZXBrIjp7ImNydiI6IlAtMjU2Iiwia3R5IjoiRUMiLCJ4IjoidDZYaDNGTTNna21SZ2dyUTBwMElxb3RDejdNV2tPRWRwNXZRQkNwWW9ZcyIsInkiOiJfNzgzWjRHbGFkQ095VDBFSFczU3FuVldRazNocWlSX2MwVjJGMURZVzhjIn19", + "recipients":[{ + "header":{ + "kid":"did:example:bob#key-p256-1" + }, + "encrypted_key":"DDH8GpSLzcO3r1Gw-QQPGRCIP3a_LXzeiOCqgecz7-YHBnAPEyF87Q" + },{ + "header":{ + "kid":"did:example:bob#key-p256-2" + }, + "encrypted_key":"J2xoNA3fLYgPVkocnXr4KafSLOhVbMZipRCEkPBuUIF4z2hpP0tdmg" + }], + "iv":"MR5gOGVOZvqHVO7n", + "ciphertext":"QdzPSf5z2yrNYYXgfUAe6-7K3QpppzP3rmzkrlBgmmD_ePdcY0riNvhwBm2Bt_oc0ebK6aq-TKkAoI4P__ISe2TH8RwPuc0HjSkUZtjW8Y8x-wnuED9HEX-HuEAZlM2aFuVy3R_104kbwkAeFtrTTN00ylclOr0ldTXXjTsio58j0Yqrq7Iz6VuScYkdVfJStEswfa4e3BEO_vRAKpr2jVsj6HL5RqZUpCF-DWy_pDsu-IRpYinglvLAfkT6O9g8QApLULOdO4r8TlD1ns-Jx9lLTZZLaILSJZSuzrV6kXgwIOUnyKeA6q2NNbddoxwgpxKGfJ8Bk-Fu0uC8UP_xHVZPRiUd9PyFY6e8dcFNoumu7g4iVN7L", + "tag":"E5tdRcTYnxaf2p5e4wgNGQ" + } + "#; + + const MSG_ANONCRYPT_P256_A256GSM_CHANGED_CIPHERTEXT: &str = r#" + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC1FUytBMjU2S1ciLCJlbmMiOiJBMjU2R0NNIiwiYXB1IjpudWxsLCJhcHYiOiJ6LUxxcHZWWERiX3NHWW4zbWpRTHB1dTJDUUxld1l1Wm9UV09JWFBIM0ZNIiwiZXBrIjp7ImNydiI6IlAtMjU2Iiwia3R5IjoiRUMiLCJ4IjoidDZYaDNGTTNna21SZ2dyUTBwMElxb3RDejdNV2tPRWRwNXZRQkNwWW9ZcyIsInkiOiJfNzgzWjRHbGFkQ095VDBFSFczU3FuVldRazNocWlSX2MwVjJGMURZVzhjIn19", + "recipients":[{ + "header":{ + "kid":"did:example:bob#key-p256-1" + }, + "encrypted_key":"ZDH8GpSLzcO3r1Gw-QQPGRCIP3a_LXzeiOCqgecz7-YHBnAPEyF87Q" + },{ + "header":{ + "kid":"did:example:bob#key-p256-2" + }, + "encrypted_key":"J2xoNA3fLYgPVkocnXr4KafSLOhVbMZipRCEkPBuUIF4z2hpP0tdmg" + }], + "iv":"MR5gOGVOZvqHVO7n", + "ciphertext":"ddzPSf5z2yrNYYXgfUAe6-7K3QpppzP3rmzkrlBgmmD_ePdcY0riNvhwBm2Bt_oc0ebK6aq-TKkAoI4P__ISe2TH8RwPuc0HjSkUZtjW8Y8x-wnuED9HEX-HuEAZlM2aFuVy3R_104kbwkAeFtrTTN00ylclOr0ldTXXjTsio58j0Yqrq7Iz6VuScYkdVfJStEswfa4e3BEO_vRAKpr2jVsj6HL5RqZUpCF-DWy_pDsu-IRpYinglvLAfkT6O9g8QApLULOdO4r8TlD1ns-Jx9lLTZZLaILSJZSuzrV6kXgwIOUnyKeA6q2NNbddoxwgpxKGfJ8Bk-Fu0uC8UP_xHVZPRiUd9PyFY6e8dcFNoumu7g4iVN7L", + "tag":"E5tdRcTYnxaf2p5e4wgNGQ" + } + "#; + + const MSG_AUTHCRYPT_X25519_A256CBC: &str = r#" + { + "ciphertext":"MJezmxJ8DzUB01rMjiW6JViSaUhsZBhMvYtezkhmwts1qXWtDB63i4-FHZP6cJSyCI7eU-gqH8lBXO_UVuviWIqnIUrTRLaumanZ4q1dNKAnxNL-dHmb3coOqSvy3ZZn6W17lsVudjw7hUUpMbeMbQ5W8GokK9ZCGaaWnqAzd1ZcuGXDuemWeA8BerQsfQw_IQm-aUKancldedHSGrOjVWgozVL97MH966j3i9CJc3k9jS9xDuE0owoWVZa7SxTmhl1PDetmzLnYIIIt-peJtNYGdpd-FcYxIFycQNRUoFEr77h4GBTLbC-vqbQHJC1vW4O2LEKhnhOAVlGyDYkNbA4DSL-LMwKxenQXRARsKSIMn7z-ZIqTE-VCNj9vbtgR", + "protected":"eyJlcGsiOnsia3R5IjoiT0tQIiwiY3J2IjoiWDI1NTE5IiwieCI6IkdGY01vcEpsamY0cExaZmNoNGFfR2hUTV9ZQWY2aU5JMWRXREd5VkNhdzAifSwiYXB2IjoiTmNzdUFuclJmUEs2OUEtcmtaMEw5WFdVRzRqTXZOQzNaZzc0QlB6NTNQQSIsInNraWQiOiJkaWQ6ZXhhbXBsZTphbGljZSNrZXkteDI1NTE5LTEiLCJhcHUiOiJaR2xrT21WNFlXMXdiR1U2WVd4cFkyVWphMlY1TFhneU5UVXhPUzB4IiwidHlwIjoiYXBwbGljYXRpb24vZGlkY29tbS1lbmNyeXB0ZWQranNvbiIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJhbGciOiJFQ0RILTFQVStBMjU2S1cifQ", + "recipients":[ + { + "encrypted_key":"o0FJASHkQKhnFo_rTMHTI9qTm_m2mkJp-wv96mKyT5TP7QjBDuiQ0AMKaPI_RLLB7jpyE-Q80Mwos7CvwbMJDhIEBnk2qHVB", + "header":{ + "kid":"did:example:bob#key-x25519-1" + } + }, + { + "encrypted_key":"rYlafW0XkNd8kaXCqVbtGJ9GhwBC3lZ9AihHK4B6J6V2kT7vjbSYuIpr1IlAjvxYQOw08yqEJNIwrPpB0ouDzKqk98FVN7rK", + "header":{ + "kid":"did:example:bob#key-x25519-2" + } + }, + { + "encrypted_key":"aqfxMY2sV-njsVo-_9Ke9QbOf6hxhGrUVh_m-h_Aq530w3e_4IokChfKWG1tVJvXYv_AffY7vxj0k5aIfKZUxiNmBwC_QsNo", + "header":{ + "kid":"did:example:bob#key-x25519-3" + } + } + ], + "tag":"uYeo7IsZjN7AnvBjUZE5lNryNENbf6_zew_VC-d4b3U", + "iv":"o02OXDQ6_-sKz2PX_6oyJg" + } + "#; + + const MSG_AUTHCRYPT_X25519_A256CBC_CHANGED_ENC_KEY: &str = r#" + { + "ciphertext":"MJezmxJ8DzUB01rMjiW6JViSaUhsZBhMvYtezkhmwts1qXWtDB63i4-FHZP6cJSyCI7eU-gqH8lBXO_UVuviWIqnIUrTRLaumanZ4q1dNKAnxNL-dHmb3coOqSvy3ZZn6W17lsVudjw7hUUpMbeMbQ5W8GokK9ZCGaaWnqAzd1ZcuGXDuemWeA8BerQsfQw_IQm-aUKancldedHSGrOjVWgozVL97MH966j3i9CJc3k9jS9xDuE0owoWVZa7SxTmhl1PDetmzLnYIIIt-peJtNYGdpd-FcYxIFycQNRUoFEr77h4GBTLbC-vqbQHJC1vW4O2LEKhnhOAVlGyDYkNbA4DSL-LMwKxenQXRARsKSIMn7z-ZIqTE-VCNj9vbtgR", + "protected":"eyJlcGsiOnsia3R5IjoiT0tQIiwiY3J2IjoiWDI1NTE5IiwieCI6IkdGY01vcEpsamY0cExaZmNoNGFfR2hUTV9ZQWY2aU5JMWRXREd5VkNhdzAifSwiYXB2IjoiTmNzdUFuclJmUEs2OUEtcmtaMEw5WFdVRzRqTXZOQzNaZzc0QlB6NTNQQSIsInNraWQiOiJkaWQ6ZXhhbXBsZTphbGljZSNrZXkteDI1NTE5LTEiLCJhcHUiOiJaR2xrT21WNFlXMXdiR1U2WVd4cFkyVWphMlY1TFhneU5UVXhPUzB4IiwidHlwIjoiYXBwbGljYXRpb24vZGlkY29tbS1lbmNyeXB0ZWQranNvbiIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJhbGciOiJFQ0RILTFQVStBMjU2S1cifQ", + "recipients":[ + { + "encrypted_key":"F0FJASHkQKhnFo_rTMHTI9qTm_m2mkJp-wv96mKyT5TP7QjBDuiQ0AMKaPI_RLLB7jpyE-Q80Mwos7CvwbMJDhIEBnk2qHVB", + "header":{ + "kid":"did:example:bob#key-x25519-1" + } + }, + { + "encrypted_key":"rYlafW0XkNd8kaXCqVbtGJ9GhwBC3lZ9AihHK4B6J6V2kT7vjbSYuIpr1IlAjvxYQOw08yqEJNIwrPpB0ouDzKqk98FVN7rK", + "header":{ + "kid":"did:example:bob#key-x25519-2" + } + }, + { + "encrypted_key":"aqfxMY2sV-njsVo-_9Ke9QbOf6hxhGrUVh_m-h_Aq530w3e_4IokChfKWG1tVJvXYv_AffY7vxj0k5aIfKZUxiNmBwC_QsNo", + "header":{ + "kid":"did:example:bob#key-x25519-3" + } + } + ], + "tag":"uYeo7IsZjN7AnvBjUZE5lNryNENbf6_zew_VC-d4b3U", + "iv":"o02OXDQ6_-sKz2PX_6oyJg" + } + "#; + + const MSG_AUTHCRYPT_X25519_A256CBC_CHANGED_CIPHERTEXT: &str = r#" + { + "ciphertext":"JJezmxJ8DzUB01rMjiW6JViSaUhsZBhMvYtezkhmwts1qXWtDB63i4-FHZP6cJSyCI7eU-gqH8lBXO_UVuviWIqnIUrTRLaumanZ4q1dNKAnxNL-dHmb3coOqSvy3ZZn6W17lsVudjw7hUUpMbeMbQ5W8GokK9ZCGaaWnqAzd1ZcuGXDuemWeA8BerQsfQw_IQm-aUKancldedHSGrOjVWgozVL97MH966j3i9CJc3k9jS9xDuE0owoWVZa7SxTmhl1PDetmzLnYIIIt-peJtNYGdpd-FcYxIFycQNRUoFEr77h4GBTLbC-vqbQHJC1vW4O2LEKhnhOAVlGyDYkNbA4DSL-LMwKxenQXRARsKSIMn7z-ZIqTE-VCNj9vbtgR", + "protected":"eyJlcGsiOnsia3R5IjoiT0tQIiwiY3J2IjoiWDI1NTE5IiwieCI6IkdGY01vcEpsamY0cExaZmNoNGFfR2hUTV9ZQWY2aU5JMWRXREd5VkNhdzAifSwiYXB2IjoiTmNzdUFuclJmUEs2OUEtcmtaMEw5WFdVRzRqTXZOQzNaZzc0QlB6NTNQQSIsInNraWQiOiJkaWQ6ZXhhbXBsZTphbGljZSNrZXkteDI1NTE5LTEiLCJhcHUiOiJaR2xrT21WNFlXMXdiR1U2WVd4cFkyVWphMlY1TFhneU5UVXhPUzB4IiwidHlwIjoiYXBwbGljYXRpb24vZGlkY29tbS1lbmNyeXB0ZWQranNvbiIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJhbGciOiJFQ0RILTFQVStBMjU2S1cifQ", + "recipients":[ + { + "encrypted_key":"o0FJASHkQKhnFo_rTMHTI9qTm_m2mkJp-wv96mKyT5TP7QjBDuiQ0AMKaPI_RLLB7jpyE-Q80Mwos7CvwbMJDhIEBnk2qHVB", + "header":{ + "kid":"did:example:bob#key-x25519-1" + } + }, + { + "encrypted_key":"rYlafW0XkNd8kaXCqVbtGJ9GhwBC3lZ9AihHK4B6J6V2kT7vjbSYuIpr1IlAjvxYQOw08yqEJNIwrPpB0ouDzKqk98FVN7rK", + "header":{ + "kid":"did:example:bob#key-x25519-2" + } + }, + { + "encrypted_key":"aqfxMY2sV-njsVo-_9Ke9QbOf6hxhGrUVh_m-h_Aq530w3e_4IokChfKWG1tVJvXYv_AffY7vxj0k5aIfKZUxiNmBwC_QsNo", + "header":{ + "kid":"did:example:bob#key-x25519-3" + } + } + ], + "tag":"uYeo7IsZjN7AnvBjUZE5lNryNENbf6_zew_VC-d4b3U", + "iv":"o02OXDQ6_-sKz2PX_6oyJg" + } + "#; + + const MSG_AUTHCRYPT_P256_A256CBC: &str = r#" + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC0xUFUrQTI1NktXIiwiZW5jIjoiQTI1NkNCQy1IUzUxMiIsInNraWQiOiJkaWQ6ZXhhbXBsZTphbGljZSNrZXktcDI1Ni0xIiwiYXB1IjoiWkdsa09tVjRZVzF3YkdVNllXeHBZMlVqYTJWNUxYQXlOVFl0TVEiLCJhcHYiOiJ6LUxxcHZWWERiX3NHWW4zbWpRTHB1dTJDUUxld1l1Wm9UV09JWFBIM0ZNIiwiZXBrIjp7ImNydiI6IlAtMjU2Iiwia3R5IjoiRUMiLCJ4IjoiX05UZFk0Nm9TM0ZtNF9tSWxtV2VGMk9HU1NsdTU1dTJDSVlWMndzdGhDZyIsInkiOiJGcEJxeWlTc0R0NW5jb3laTWFvT3ItYUxNcmNNYW10aWZTYUstRUFrX3NrIn19", + "recipients":[{ + "header":{ + "kid":"did:example:bob#key-p256-1" + }, + "encrypted_key":"VokcBeiCnIR5xCpml6oXn2nEmK31hg0aYOWG0EHlgbv19g2oyOPxsxgxuDEkk-oEXj6WNfs_mebxUQLCKpbHh7tTyqnceoh-" + },{ + "header":{ + "kid":"did:example:bob#key-p256-2" + }, + "encrypted_key":"LGQRr4jN-DYlR3Vj2NrJOHcT6iZ3FblERiQNd3UxpAmXF9pI9LyG0gWlE9CTEWNN_EK_yvXPg6TCmsVwoA7VNC7tj74oz66t" + }], + "iv":"7xtMiG3E7Rjhw_6JEo4S7w", + "ciphertext":"ebalFD4UYdpFi5p4KDjLYfgJubV5byaCV6V-4qvqF74N4OD_zAZDd7rQxWqds67VwYX2Yw9oTYe_H6WivrCKieHFjqC01gUCbFiqS-lqe3O3WFNnwVX-WRNC7Tsha0azlulJECUXlqyKsuqp-VGYcO-OKyRfzuN_KhBbKAlfxVNxbJX-9ecb0ZqtiZRcOGvqrcDRCgQ8ApdcwGelBLs6V0bBIYnsMHkxi1eK4dMmhBpMoKR9GwQxSwBnKEs5BMO-NTRyAWWKHZXraC8nWSBivqGR9TocrD5H0xK5Ys9eUPJr453BbwTTX2BFdZQlrv4zBwHsaqsHdWPhuQyzjmQFgbmOPOtmnwSurVMRO8A1fZtbWiKy9CYqffqL8XyWmH-S","tag":"84reUHzWkfJsr6rtPMEV40gL8wswp2--V-cwmHOQt5o" + } + "#; + + const MSG_AUTHCRYPT_P256_A256CBC_CHANGED_ENC_KEY: &str = r#" + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC0xUFUrQTI1NktXIiwiZW5jIjoiQTI1NkNCQy1IUzUxMiIsInNraWQiOiJkaWQ6ZXhhbXBsZTphbGljZSNrZXktcDI1Ni0xIiwiYXB1IjoiWkdsa09tVjRZVzF3YkdVNllXeHBZMlVqYTJWNUxYQXlOVFl0TVEiLCJhcHYiOiJ6LUxxcHZWWERiX3NHWW4zbWpRTHB1dTJDUUxld1l1Wm9UV09JWFBIM0ZNIiwiZXBrIjp7ImNydiI6IlAtMjU2Iiwia3R5IjoiRUMiLCJ4IjoiX05UZFk0Nm9TM0ZtNF9tSWxtV2VGMk9HU1NsdTU1dTJDSVlWMndzdGhDZyIsInkiOiJGcEJxeWlTc0R0NW5jb3laTWFvT3ItYUxNcmNNYW10aWZTYUstRUFrX3NrIn19", + "recipients":[{ + "header":{ + "kid":"did:example:bob#key-p256-1" + }, + "encrypted_key":"ookcBeiCnIR5xCpml6oXn2nEmK31hg0aYOWG0EHlgbv19g2oyOPxsxgxuDEkk-oEXj6WNfs_mebxUQLCKpbHh7tTyqnceoh-" + },{ + "header":{ + "kid":"did:example:bob#key-p256-2" + }, + "encrypted_key":"LGQRr4jN-DYlR3Vj2NrJOHcT6iZ3FblERiQNd3UxpAmXF9pI9LyG0gWlE9CTEWNN_EK_yvXPg6TCmsVwoA7VNC7tj74oz66t" + }], + "iv":"7xtMiG3E7Rjhw_6JEo4S7w", + "ciphertext":"ebalFD4UYdpFi5p4KDjLYfgJubV5byaCV6V-4qvqF74N4OD_zAZDd7rQxWqds67VwYX2Yw9oTYe_H6WivrCKieHFjqC01gUCbFiqS-lqe3O3WFNnwVX-WRNC7Tsha0azlulJECUXlqyKsuqp-VGYcO-OKyRfzuN_KhBbKAlfxVNxbJX-9ecb0ZqtiZRcOGvqrcDRCgQ8ApdcwGelBLs6V0bBIYnsMHkxi1eK4dMmhBpMoKR9GwQxSwBnKEs5BMO-NTRyAWWKHZXraC8nWSBivqGR9TocrD5H0xK5Ys9eUPJr453BbwTTX2BFdZQlrv4zBwHsaqsHdWPhuQyzjmQFgbmOPOtmnwSurVMRO8A1fZtbWiKy9CYqffqL8XyWmH-S","tag":"84reUHzWkfJsr6rtPMEV40gL8wswp2--V-cwmHOQt5o" + } + "#; + + const MSG_AUTHCRYPT_P256_A256CBC_CHANGED_CIPHERTEXT: &str = r#" + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC0xUFUrQTI1NktXIiwiZW5jIjoiQTI1NkNCQy1IUzUxMiIsInNraWQiOiJkaWQ6ZXhhbXBsZTphbGljZSNrZXktcDI1Ni0xIiwiYXB1IjoiWkdsa09tVjRZVzF3YkdVNllXeHBZMlVqYTJWNUxYQXlOVFl0TVEiLCJhcHYiOiJ6LUxxcHZWWERiX3NHWW4zbWpRTHB1dTJDUUxld1l1Wm9UV09JWFBIM0ZNIiwiZXBrIjp7ImNydiI6IlAtMjU2Iiwia3R5IjoiRUMiLCJ4IjoiX05UZFk0Nm9TM0ZtNF9tSWxtV2VGMk9HU1NsdTU1dTJDSVlWMndzdGhDZyIsInkiOiJGcEJxeWlTc0R0NW5jb3laTWFvT3ItYUxNcmNNYW10aWZTYUstRUFrX3NrIn19", + "recipients":[{ + "header":{ + "kid":"did:example:bob#key-p256-1" + }, + "encrypted_key":"VokcBeiCnIR5xCpml6oXn2nEmK31hg0aYOWG0EHlgbv19g2oyOPxsxgxuDEkk-oEXj6WNfs_mebxUQLCKpbHh7tTyqnceoh-" + },{ + "header":{ + "kid":"did:example:bob#key-p256-2" + }, + "encrypted_key":"LGQRr4jN-DYlR3Vj2NrJOHcT6iZ3FblERiQNd3UxpAmXF9pI9LyG0gWlE9CTEWNN_EK_yvXPg6TCmsVwoA7VNC7tj74oz66t" + }], + "iv":"7xtMiG3E7Rjhw_6JEo4S7w", + "ciphertext":"bbalFD4UYdpFi5p4KDjLYfgJubV5byaCV6V-4qvqF74N4OD_zAZDd7rQxWqds67VwYX2Yw9oTYe_H6WivrCKieHFjqC01gUCbFiqS-lqe3O3WFNnwVX-WRNC7Tsha0azlulJECUXlqyKsuqp-VGYcO-OKyRfzuN_KhBbKAlfxVNxbJX-9ecb0ZqtiZRcOGvqrcDRCgQ8ApdcwGelBLs6V0bBIYnsMHkxi1eK4dMmhBpMoKR9GwQxSwBnKEs5BMO-NTRyAWWKHZXraC8nWSBivqGR9TocrD5H0xK5Ys9eUPJr453BbwTTX2BFdZQlrv4zBwHsaqsHdWPhuQyzjmQFgbmOPOtmnwSurVMRO8A1fZtbWiKy9CYqffqL8XyWmH-S","tag":"84reUHzWkfJsr6rtPMEV40gL8wswp2--V-cwmHOQt5o" + } + "#; + + const MSG_ECDH_1PU_APP_B: &str = r#" + { + "protected":"eyJhbGciOiJFQ0RILTFQVStBMTI4S1ciLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwiYXB1IjoiUVd4cFkyVSIsImFwdiI6IlFtOWlJR0Z1WkNCRGFHRnliR2xsIiwiZXBrIjp7Imt0eSI6Ik9LUCIsImNydiI6IlgyNTUxOSIsIngiOiJrOW9mX2NwQWFqeTBwb1c1Z2FpeFhHczluSGt3ZzFBRnFVQUZhMzlkeUJjIn19", + "recipients":[ + { + "header":{ + "kid":"bob-key-2" + }, + "encrypted_key":"pOMVA9_PtoRe7xXW1139NzzN1UhiFoio8lGto9cf0t8PyU-sjNXH8-LIRLycq8CHJQbDwvQeU1cSl55cQ0hGezJu2N9IY0QN" + }, + { + "header":{ + "kid":"2021-05-06" + }, + "encrypted_key": "56GVudgRLIMEElQ7DpXsijJVRSWUSDNdbWkdV3g0GUNq6hcT_GkxwnxlPIWrTXCqRpVKQC8fe4z3PQ2YH2afvjQ28aiCTWFE" + } + ], + "iv":"AAECAwQFBgcICQoLDA0ODw", + "ciphertext":"Az2IWsISEMDJvyc5XRL-3-d-RgNBOGolCsxFFoUXFYw", + "tag":"HLb4fTlm8spGmij3RyOs2gJ4DpHM4hhVRwdF_hGb3WQ" + } + "#; + + const PAYLOAD_ECDH_1PU_APP_B: &str = "Three is a magic number."; + + const ALICE_KID_ECDH_1PU_APP_B: &str = "Alice"; + + const ALICE_KEY_ECDH_1PU_APP_B: &str = r#" + { + "kty": "OKP", + "crv": "X25519", + "x": "Knbm_BcdQr7WIoz-uqit9M0wbcfEr6y-9UfIZ8QnBD4", + "d": "i9KuFhSzEBsiv3PKVL5115OCdsqQai5nj_Flzfkw5jU" + } + "#; + + const BOB_KID_ECDH_1PU_APP_B: &str = "bob-key-2"; + + const BOB_KEY_ECDH_1PU_APP_B: &str = r#" + { + "kty": "OKP", + "crv": "X25519", + "x": "BT7aR0ItXfeDAldeeOlXL_wXqp-j5FltT0vRSG16kRw", + "d": "1gDirl_r_Y3-qUa3WXHgEXrrEHngWThU3c9zj9A2uBg" + } + "#; + + const CHARLIE_KID_ECDH_1PU_APP_B: &str = "2021-05-06"; + + const CHARLIE_KEY_ECDH_1PU_APP_B: &str = r#" + { + "kty": "OKP", + "crv": "X25519", + "x": "q-LsvU772uV_2sPJhfAIq-3vnKNVefNoIlvyvg1hrnE", + "d": "Jcv8gklhMjC0b-lsk5onBbppWAx5ncNtbM63Jr9xBQE" + } + "#; +} diff --git a/affinidi-messaging-didcomm/src/jwe/encrypt.rs b/affinidi-messaging-didcomm/src/jwe/encrypt.rs new file mode 100644 index 0000000..c8633f4 --- /dev/null +++ b/affinidi-messaging-didcomm/src/jwe/encrypt.rs @@ -0,0 +1,461 @@ +use askar_crypto::{ + buffer::SecretBytes, + encrypt::{KeyAeadInPlace, KeyAeadMeta}, + kdf::{FromKeyDerivation, KeyExchange}, + random, + repr::{KeyGen, ToSecretBytes}, +}; +use base64::prelude::*; +use sha2::{Digest, Sha256}; + +use crate::{ + error::{Error, ErrorKind, Result, ResultExt}, + jwe::envelope::{Algorithm, EncAlgorithm, Jwe, PerRecipientHeader, ProtectedHeader, Recipient}, + jwk::ToJwkValue, + utils::crypto::{JoseKDF, KeyWrap}, +}; + +pub(crate) fn encrypt( + plaintext: &[u8], + alg: Algorithm, + enc: EncAlgorithm, + sender: Option<(&str, &KE)>, // (skid, sender key) + recipients: &[(&str, &KE)], // (kid, recipient key) +) -> Result +where + CE: KeyAeadInPlace + KeyAeadMeta + KeyGen + ToSecretBytes, + KDF: JoseKDF, + KE: KeyExchange + KeyGen + ToJwkValue, + KW: KeyWrap + FromKeyDerivation, +{ + let (skid, skey) = match sender { + Some((skid, skey)) => (Some(skid.to_string()), Some(skey)), + None => (None, None), + }; + + let mut rng = random::default_rng(); + let cek = CE::generate(&mut rng).map_err(|err| { + Error::msg( + ErrorKind::InvalidState, + format!("{}: {}", "Unable generate cek", err.message()), + ) + })?; + + let apv = { + let mut kids = recipients.iter().map(|r| r.0).collect::>(); + kids.sort(); + Sha256::digest(kids.join(".").as_bytes()) + }; + + let epk = KE::generate(&mut rng).map_err(|err| { + Error::msg( + ErrorKind::InvalidState, + format!("{}: {}", "Unable generate epk", err.message()), + ) + })?; + + let protected = { + let epk = epk.to_jwk_public_value()?; + let apu = skid.clone().map(|skid| BASE64_URL_SAFE_NO_PAD.encode(skid)); + let apv = BASE64_URL_SAFE_NO_PAD.encode(apv); + + let p = ProtectedHeader { + typ: Some("application/didcomm-encrypted+json".into()), + alg: alg.clone(), + enc, + skid: skid.clone(), + apu, + apv, + epk, + }; + + let p = serde_json::to_string(&p) + .kind(ErrorKind::InvalidState, "Unable serialize protected header")?; + + BASE64_URL_SAFE_NO_PAD.encode(p) + }; + + let mut buf = { + let mut buf = SecretBytes::with_capacity(plaintext.len() + cek.aead_params().tag_length); + + buf.extend_from_slice(plaintext); + buf + }; + + let (ciphertext, tag, tag_raw, iv) = { + // TODO: use `rng` based version when available + let iv = CE::random_nonce(); + + let ciphertext_len = cek + .encrypt_in_place(&mut buf, &iv[..], protected.as_bytes()) + .map_err(|err| { + Error::msg( + ErrorKind::InvalidState, + format!("{}: {}", "Unable encrypt content", err.message()), + ) + })?; + + let ciphertext = &buf.as_ref()[0..ciphertext_len]; + let tag_raw = &buf.as_ref()[ciphertext_len..]; + + let ciphertext = BASE64_URL_SAFE_NO_PAD.encode(ciphertext); + let tag = BASE64_URL_SAFE_NO_PAD.encode(tag_raw); + let iv = BASE64_URL_SAFE_NO_PAD.encode(iv); + + (ciphertext, tag, tag_raw, iv) + }; + + let encrypted_keys = { + let mut encrypted_keys: Vec<(&str, String)> = Vec::with_capacity(recipients.len()); + + for (kid, key) in recipients { + let kw = KDF::derive_key( + &epk, + skey, + key, + alg.as_str().as_bytes(), + skid.as_ref().map(|s| s.as_bytes()).unwrap_or(&[]), + apv.as_slice(), + tag_raw, + false, + ) + .kind(ErrorKind::InvalidState, "Unable derive kw")?; //TODO Check this test and move to decrypt + + let encrypted_key = kw + .wrap_key(&cek) + .kind(ErrorKind::InvalidState, "Unable wrap key")?; + + let encrypted_key = BASE64_URL_SAFE_NO_PAD.encode(&encrypted_key); + encrypted_keys.push((kid, encrypted_key)); + } + + encrypted_keys + }; + + let recipients: Vec<_> = encrypted_keys + .iter() + .map(|(kid, encrypted_key)| Recipient { + header: PerRecipientHeader { + kid: kid.to_string(), + }, + encrypted_key: encrypted_key.to_string(), + }) + .collect(); + + let jwe = Jwe { + protected, + recipients, + iv, + ciphertext, + tag, + }; + + let jwe = serde_json::to_string(&jwe).kind(ErrorKind::InvalidState, "Unable serialize jwe")?; + + Ok(jwe) +} + +#[cfg(test)] +mod tests { + use askar_crypto::{ + alg::{ + aes::{A256CbcHs512, A256Gcm, A256Kw, AesKey}, + chacha20::{Chacha20Key, XC20P}, + p256::P256KeyPair, + x25519::X25519KeyPair, + }, + encrypt::{KeyAeadInPlace, KeyAeadMeta}, + jwk::FromJwk, + kdf::{ecdh_1pu::Ecdh1PU, ecdh_es::EcdhEs, FromKeyDerivation, KeyExchange}, + repr::{KeyGen, KeyPublicBytes, KeySecretBytes, ToPublicBytes, ToSecretBytes}, + }; + + use crate::{ + error::ErrorKind, + jwe::{ + self, + envelope::{Algorithm, EncAlgorithm}, + test_support::*, + }, + jwk::{FromJwkValue, ToJwkValue}, + utils::crypto::{JoseKDF, KeyWrap}, + }; + + #[test] + fn encrypt_works() { + _encrypt_works::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + Some((ALICE_KID_X25519_1, ALICE_KEY_X25519_1, ALICE_PKEY_X25519_1)), + &[(BOB_KID_X25519_1, BOB_KEY_X25519_1, BOB_PKEY_X25519_1)], + Algorithm::Ecdh1puA256kw, + EncAlgorithm::A256cbcHs512, + ); + + _encrypt_works::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + Some((ALICE_KID_X25519_1, ALICE_KEY_X25519_1, ALICE_PKEY_X25519_1)), + &[ + (BOB_KID_X25519_1, BOB_KEY_X25519_1, BOB_PKEY_X25519_1), + (BOB_KID_X25519_2, BOB_KEY_X25519_2, BOB_PKEY_X25519_2), + (BOB_KID_X25519_3, BOB_KEY_X25519_3, BOB_PKEY_X25519_3), + ], + Algorithm::Ecdh1puA256kw, + EncAlgorithm::A256cbcHs512, + ); + + _encrypt_works::, Ecdh1PU<'_, P256KeyPair>, P256KeyPair, AesKey>( + Some((ALICE_KID_P256_1, ALICE_KEY_P256_1, ALICE_PKEY_P256_1)), + &[(BOB_KID_P256_1, BOB_KEY_P256_1, BOB_PKEY_P256_1)], + Algorithm::Ecdh1puA256kw, + EncAlgorithm::A256cbcHs512, + ); + + _encrypt_works::, Ecdh1PU<'_, P256KeyPair>, P256KeyPair, AesKey>( + Some((ALICE_KID_P256_1, ALICE_KEY_P256_1, ALICE_PKEY_P256_1)), + &[ + (BOB_KID_P256_1, BOB_KEY_P256_1, BOB_PKEY_P256_1), + (BOB_KID_P256_2, BOB_KEY_P256_2, BOB_PKEY_P256_2), + ], + Algorithm::Ecdh1puA256kw, + EncAlgorithm::A256cbcHs512, + ); + + _encrypt_works::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + &[(BOB_KID_X25519_1, BOB_KEY_X25519_1, BOB_PKEY_X25519_1)], + Algorithm::EcdhEsA256kw, + EncAlgorithm::A256cbcHs512, + ); + + _encrypt_works::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + &[ + (BOB_KID_X25519_1, BOB_KEY_X25519_1, BOB_PKEY_X25519_1), + (BOB_KID_X25519_2, BOB_KEY_X25519_2, BOB_PKEY_X25519_2), + (BOB_KID_X25519_3, BOB_KEY_X25519_3, BOB_PKEY_X25519_3), + ], + Algorithm::EcdhEsA256kw, + EncAlgorithm::A256cbcHs512, + ); + + _encrypt_works::, EcdhEs<'_, P256KeyPair>, P256KeyPair, AesKey>( + None, + &[(BOB_KID_P256_1, BOB_KEY_P256_1, BOB_PKEY_P256_1)], + Algorithm::EcdhEsA256kw, + EncAlgorithm::A256cbcHs512, + ); + + _encrypt_works::, EcdhEs<'_, P256KeyPair>, P256KeyPair, AesKey>( + None, + &[ + (BOB_KID_P256_1, BOB_KEY_P256_1, BOB_PKEY_P256_1), + (BOB_KID_P256_2, BOB_KEY_P256_2, BOB_PKEY_P256_2), + ], + Algorithm::EcdhEsA256kw, + EncAlgorithm::A256cbcHs512, + ); + + _encrypt_works::, EcdhEs<'_, X25519KeyPair>, X25519KeyPair, AesKey>( + None, + &[(BOB_KID_X25519_1, BOB_KEY_X25519_1, BOB_PKEY_X25519_1)], + Algorithm::EcdhEsA256kw, + EncAlgorithm::A256Gcm, + ); + + _encrypt_works::, EcdhEs<'_, X25519KeyPair>, X25519KeyPair, AesKey>( + None, + &[ + (BOB_KID_X25519_1, BOB_KEY_X25519_1, BOB_PKEY_X25519_1), + (BOB_KID_X25519_2, BOB_KEY_X25519_2, BOB_PKEY_X25519_2), + (BOB_KID_X25519_3, BOB_KEY_X25519_3, BOB_PKEY_X25519_3), + ], + Algorithm::EcdhEsA256kw, + EncAlgorithm::A256Gcm, + ); + + _encrypt_works::, EcdhEs<'_, P256KeyPair>, P256KeyPair, AesKey>( + None, + &[(BOB_KID_P256_1, BOB_KEY_P256_1, BOB_PKEY_P256_1)], + Algorithm::EcdhEsA256kw, + EncAlgorithm::A256Gcm, + ); + + _encrypt_works::, EcdhEs<'_, P256KeyPair>, P256KeyPair, AesKey>( + None, + &[ + (BOB_KID_P256_1, BOB_KEY_P256_1, BOB_PKEY_P256_1), + (BOB_KID_P256_2, BOB_KEY_P256_2, BOB_PKEY_P256_2), + ], + Algorithm::EcdhEsA256kw, + EncAlgorithm::A256Gcm, + ); + + _encrypt_works::< + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + &[(BOB_KID_X25519_1, BOB_KEY_X25519_1, BOB_PKEY_X25519_1)], + Algorithm::EcdhEsA256kw, + EncAlgorithm::Xc20P, + ); + + _encrypt_works::< + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + None, + &[ + (BOB_KID_X25519_1, BOB_KEY_X25519_1, BOB_PKEY_X25519_1), + (BOB_KID_X25519_2, BOB_KEY_X25519_2, BOB_PKEY_X25519_2), + (BOB_KID_X25519_3, BOB_KEY_X25519_3, BOB_PKEY_X25519_3), + ], + Algorithm::EcdhEsA256kw, + EncAlgorithm::Xc20P, + ); + + _encrypt_works::, EcdhEs<'_, P256KeyPair>, P256KeyPair, AesKey>( + None, + &[(BOB_KID_P256_1, BOB_KEY_P256_1, BOB_PKEY_P256_1)], + Algorithm::EcdhEsA256kw, + EncAlgorithm::Xc20P, + ); + + _encrypt_works::, EcdhEs<'_, P256KeyPair>, P256KeyPair, AesKey>( + None, + &[ + (BOB_KID_P256_1, BOB_KEY_P256_1, BOB_PKEY_P256_1), + (BOB_KID_P256_2, BOB_KEY_P256_2, BOB_PKEY_P256_2), + ], + Algorithm::EcdhEsA256kw, + EncAlgorithm::Xc20P, + ); + + _encrypt_works::, EcdhEs<'_, P256KeyPair>, P256KeyPair, AesKey>( + None, + &[ + (BOB_KID_P256_1, BOB_KEY_P256_1, BOB_PKEY_P256_1), + (BOB_KID_P256_2, BOB_KEY_P256_2, BOB_PKEY_P256_2), + ], + Algorithm::Other("otherAlg".to_owned()), + EncAlgorithm::A256Gcm, + ); + /// TODO: P-384 and P-521 support after solving https://github.com/hyperledger/aries-askar/issues/10 + + fn _encrypt_works( + alice: Option<(&str, &str, &str)>, + bob: &[(&str, &str, &str)], + alg: Algorithm, + enc_alg: EncAlgorithm, + ) where + CE: KeyAeadInPlace + KeyAeadMeta + KeyGen + ToSecretBytes + KeySecretBytes, + KDF: JoseKDF, + KE: KeyExchange + KeyGen + ToJwkValue + FromJwkValue + ToPublicBytes + KeyPublicBytes, + KW: KeyWrap + FromKeyDerivation, + { + let alice = alice.map(|a| { + ( + a.0, + KE::from_jwk(a.1).expect("Unable from_jwk"), + KE::from_jwk(a.2).expect("Unable from_jwk"), + ) + }); + + let alice_kid = alice.as_ref().map(|a| a.0); + let alice_priv = alice.as_ref().map(|a| (a.0, &a.1)); + let alice_pub = alice.as_ref().map(|a| (a.0, &a.2)); + + let bob = bob + .iter() + .map(|b| { + ( + b.0, + KE::from_jwk(b.1).expect("Unable from_jwk"), + KE::from_jwk(b.2).expect("Unable from_jwk"), + ) + }) + .collect::>(); + + let bob_priv: Vec<_> = bob.iter().map(|b| (b.0, &b.1)).collect(); + let bob_pub: Vec<_> = bob.iter().map(|b| (b.0, &b.2)).collect(); + + let plaintext = "Some plaintext."; + + let msg = jwe::encrypt::( + plaintext.as_bytes(), + alg.clone(), + enc_alg.clone(), + alice_priv, + &bob_pub, + ) + .expect("Unable encrypt"); + + let msg = jwe::parse(&msg).expect("Unable parse"); + + assert_eq!(msg.protected.alg, alg); + assert_eq!(msg.protected.enc, enc_alg); + assert_eq!(msg.jwe.recipients.len(), bob.len()); + + assert_eq!(msg.apu.as_deref(), alice_kid.map(str::as_bytes)); + + for recipient in &msg.jwe.recipients { + let bob_edge_priv = bob_priv + .iter() + .find(|b| recipient.header.kid == b.0) + .expect("recipient not found."); + + let plaintext_ = msg + .decrypt::(alice_pub, *bob_edge_priv) + .expect("unable decrypt."); + + assert_eq!(plaintext_, plaintext.as_bytes()); + } + } + } + + #[test] + fn encrypt_works_no_sender() { + let bob_kid = BOB_KID_X25519_1; + let bob_pkey = X25519KeyPair::from_jwk(BOB_PKEY_X25519_1).expect("unable from_jwk"); + let plaintext = "Some plaintext."; + + let res = jwe::encrypt::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + plaintext.as_bytes(), + Algorithm::Ecdh1puA256kw, + EncAlgorithm::A256cbcHs512, + None, + &[(bob_kid, &bob_pkey)], + ); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::InvalidState); + assert_eq!(format!("{}", err), "Invalid state: Unable derive kw: Invalid state: No sender key for ecdh-1pu: No sender key for ecdh-1pu"); + } +} diff --git a/affinidi-messaging-didcomm/src/jwe/envelope.rs b/affinidi-messaging-didcomm/src/jwe/envelope.rs new file mode 100644 index 0000000..1b2618e --- /dev/null +++ b/affinidi-messaging-didcomm/src/jwe/envelope.rs @@ -0,0 +1,175 @@ +use serde::{Deserialize, Serialize}; +use serde_enum_str::{Deserialize_enum_str, Serialize_enum_str}; +use serde_json::Value; + +/// Subset of JWE in generic json serialization form used for authcrypt +/// and anoncrypt message types. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct Jwe { + /// BASE64URL(UTF8(JWE Protected Header)) + /// Note: this field value is used as AAD for JWE Ciphertext + pub protected: String, + + /// Array of recipient-specific objects + pub recipients: Vec, + + /// BASE64URL(JWE Initialization Vector) + pub iv: String, + + /// BASE64URL(JWE Ciphertext) + pub ciphertext: String, + + /// BASE64URL(JWE Authentication Tag) + pub tag: String, +} + +/// Protected header for authcrypt/anoncrypt-specific JWE. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct ProtectedHeader { + /// Must be `application/didcomm-encrypted+json` or `didcomm-encrypted+json` for now. + /// Something like `application/didcomm-encrypted+cbor` can be introduced in the + /// future. + pub typ: Option, + + /// Cryptographic algorithm used to encrypt or determine the value of the CEK. + pub alg: Algorithm, + + /// Identifies the content encryption algorithm used to perform authenticated encryption + /// on the plaintext to produce the ciphertext and the Authentication Tag. + pub enc: EncAlgorithm, + + /// Sender KID as DID Url. + /// If absent implementations MUST be able to resolve the sender kid from the `apu` header. + #[serde(skip_serializing_if = "Option::is_none")] + pub skid: Option, + + /// BASE64URL("skid" header value), + #[serde(skip_serializing_if = "Option::is_none")] + pub apu: Option, + + /// BASE64URL(SHA256(CONCAT('.', SORT([recipients[0].kid, ..., recipients[n].kid]))))) + pub apv: String, + + /// EPK generated once for all recipients. + /// It MUST be of the same type and curve as all recipient keys since kdf + /// with the sender key must be on the same curve. + pub epk: Value, +} +/// Recipient part of authcrypt/anoncrypt-specific JWE +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct Recipient { + /// Per-recipient header + /// Note it isn't serialized and not integrity protected + pub header: PerRecipientHeader, + + /// BASE64URL(JWE Encrypted Key) + pub encrypted_key: String, +} + +/// Per-recipient header part of authcrypt/anoncrypt-specific JWE +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct PerRecipientHeader { + /// Recipient KID as DID URL + pub kid: String, +} + +/// Represents possible values for `alg` header. +/// Cryptographic algorithm used to encrypt or determine the value of the CEK. +#[derive(Deserialize_enum_str, Serialize_enum_str, Debug, Clone, Eq, PartialEq)] +pub enum Algorithm { + #[serde(rename = "ECDH-1PU+A256KW")] + Ecdh1puA256kw, + + #[serde(rename = "ECDH-ES+A256KW")] + EcdhEsA256kw, + + #[serde(other)] + Other(String), +} + +impl Algorithm { + pub(crate) fn as_str(&self) -> &str { + match self { + Algorithm::Ecdh1puA256kw => "ECDH-1PU+A256KW", + Algorithm::EcdhEsA256kw => "ECDH-ES+A256KW", + Algorithm::Other(ref s) => s, + } + } +} + +/// Represents possible values for `enc` header. +/// Identifies the content encryption algorithm used to perform authenticated encryption +/// on the plaintext to produce the ciphertext and the Authentication Tag. +#[derive(Deserialize_enum_str, Serialize_enum_str, Debug, Clone, Eq, PartialEq)] +pub enum EncAlgorithm { + #[serde(rename = "A256CBC-HS512")] + A256cbcHs512, + + #[serde(rename = "XC20P")] + Xc20P, + + #[serde(rename = "A256GCM")] + A256Gcm, + + #[serde(other)] + Other(String), +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn algorithm_serialize_works() { + let alg = Algorithm::Ecdh1puA256kw; + let alg = serde_json::to_string(&alg).expect("unable serialize."); + assert_eq!(alg, "\"ECDH-1PU+A256KW\""); + + let alg = Algorithm::Other("Unknown".into()); + let alg = serde_json::to_string(&alg).expect("unable serialize."); + assert_eq!(alg, "\"Unknown\""); + } + + #[test] + fn algorithm_deserialize_works() { + let alg: Algorithm = + serde_json::from_str("\"ECDH-1PU+A256KW\"").expect("unable deserialize."); + + assert_eq!(alg, Algorithm::Ecdh1puA256kw); + + let alg: Algorithm = serde_json::from_str("\"Unknown\"").expect("unable deserialize."); + assert_eq!(alg, Algorithm::Other("Unknown".into())); + + let alg: Algorithm = serde_json::from_str("\"Unknown 2\"").expect("unable deserialize."); + assert_eq!(alg, Algorithm::Other("Unknown 2".into())); + } + + #[test] + fn enc_algorithm_serialize_works() { + let enc_alg = EncAlgorithm::A256cbcHs512; + let enc_alg = serde_json::to_string(&enc_alg).expect("unable serialize."); + assert_eq!(enc_alg, "\"A256CBC-HS512\""); + + let enc_alg = EncAlgorithm::Other("Unknown".into()); + let enc_alg = serde_json::to_string(&enc_alg).expect("unable serialize."); + assert_eq!(enc_alg, "\"Unknown\""); + } + + #[test] + fn enc_algorithm_deserialize_works() { + let enc_alg: EncAlgorithm = + serde_json::from_str("\"A256CBC-HS512\"").expect("unable deserialize."); + + assert_eq!(enc_alg, EncAlgorithm::A256cbcHs512); + + let enc_alg: EncAlgorithm = + serde_json::from_str("\"Unknown\"").expect("unable deserialize."); + + assert_eq!(enc_alg, EncAlgorithm::Other("Unknown".into())); + + let enc_alg: EncAlgorithm = + serde_json::from_str("\"Unknown 2\"").expect("unable deserialize."); + + assert_eq!(enc_alg, EncAlgorithm::Other("Unknown 2".into())); + } +} diff --git a/affinidi-messaging-didcomm/src/jwe/mod.rs b/affinidi-messaging-didcomm/src/jwe/mod.rs new file mode 100644 index 0000000..5dab8e4 --- /dev/null +++ b/affinidi-messaging-didcomm/src/jwe/mod.rs @@ -0,0 +1,141 @@ +// TODO: remove allow +#[allow(dead_code)] +mod encrypt; + +// TODO: remove allow +#[allow(dead_code)] +mod decrypt; + +// TODO: remove allow +#[allow(dead_code)] +mod parse; + +// TODO: remove allow +#[allow(dead_code)] +pub(crate) mod envelope; + +// TODO: remove allow +#[allow(unused_imports)] +pub(crate) use encrypt::encrypt; + +// TODO: remove allow +#[allow(unused_imports)] +pub(crate) use parse::{parse, ParsedJWE}; + +// TODO: remove allow +#[allow(unused_imports)] +pub(crate) use envelope::{Algorithm, EncAlgorithm}; + +#[cfg(test)] +pub(crate) mod test_support { + pub(crate) const ALICE_KID_X25519_1: &str = "did:example:alice#key-x25519-1"; + + pub(crate) const ALICE_KEY_X25519_1: &str = r#"{ + "kty":"OKP", + "d":"r-jK2cO3taR8LQnJB1_ikLBTAnOtShJOsHXRUWT-aZA", + "crv":"X25519", + "x":"avH0O2Y4tqLAq8y9zpianr8ajii5m4F_mICrzNlatXs" + }"#; + + pub(crate) const ALICE_PKEY_X25519_1: &str = r#"{ + "kty":"OKP", + "crv":"X25519", + "x":"avH0O2Y4tqLAq8y9zpianr8ajii5m4F_mICrzNlatXs" + }"#; + + pub(crate) const ALICE_KID_P256_1: &str = "did:example:alice#key-p256-1"; + + pub(crate) const ALICE_KEY_P256_1: &str = r#"{ + "kty":"EC", + "d":"sB0bYtpaXyp-h17dDpMx91N3Du1AdN4z1FUq02GbmLw", + "crv":"P-256", + "x":"L0crjMN1g0Ih4sYAJ_nGoHUck2cloltUpUVQDhF2nHE", + "y":"SxYgE7CmEJYi7IDhgK5jI4ZiajO8jPRZDldVhqFpYoo" + }"#; + + pub(crate) const ALICE_PKEY_P256_1: &str = r#"{ + "kty":"EC", + "crv":"P-256", + "x":"L0crjMN1g0Ih4sYAJ_nGoHUck2cloltUpUVQDhF2nHE", + "y":"SxYgE7CmEJYi7IDhgK5jI4ZiajO8jPRZDldVhqFpYoo" + }"#; + + pub(crate) const BOB_KID_X25519_1: &str = "did:example:bob#key-x25519-1"; + + pub(crate) const BOB_KEY_X25519_1: &str = r#"{ + "kty":"OKP", + "d":"b9NnuOCB0hm7YGNvaE9DMhwH_wjZA1-gWD6dA0JWdL0", + "crv":"X25519", + "x":"GDTrI66K0pFfO54tlCSvfjjNapIs44dzpneBgyx0S3E" + }"#; + + pub(crate) const BOB_PKEY_X25519_1: &str = r#"{ + "kty":"OKP", + "crv":"X25519", + "x":"GDTrI66K0pFfO54tlCSvfjjNapIs44dzpneBgyx0S3E" + }"#; + + pub(crate) const BOB_KID_X25519_2: &str = "did:example:bob#key-x25519-2"; + + pub(crate) const BOB_KEY_X25519_2: &str = r#"{ + "kty":"OKP", + "d":"p-vteoF1gopny1HXywt76xz_uC83UUmrgszsI-ThBKk", + "crv":"X25519", + "x":"UT9S3F5ep16KSNBBShU2wh3qSfqYjlasZimn0mB8_VM" + }"#; + + pub(crate) const BOB_PKEY_X25519_2: &str = r#"{ + "kty":"OKP", + "crv":"X25519", + "x":"UT9S3F5ep16KSNBBShU2wh3qSfqYjlasZimn0mB8_VM" + }"#; + + pub(crate) const BOB_KID_X25519_3: &str = "did:example:bob#key-x25519-3"; + + pub(crate) const BOB_KEY_X25519_3: &str = r#"{ + "kty":"OKP", + "d":"f9WJeuQXEItkGM8shN4dqFr5fLQLBasHnWZ-8dPaSo0", + "crv":"X25519", + "x":"82k2BTUiywKv49fKLZa-WwDi8RBf0tB0M8bvSAUQ3yY" + }"#; + + pub(crate) const BOB_PKEY_X25519_3: &str = r#"{ + "kty":"OKP", + "crv":"X25519", + "x":"82k2BTUiywKv49fKLZa-WwDi8RBf0tB0M8bvSAUQ3yY" + }"#; + + pub(crate) const BOB_KID_P256_1: &str = "did:example:bob#key-p256-1"; + + pub(crate) const BOB_KEY_P256_1: &str = r#"{ + "kty":"EC", + "d":"PgwHnlXxt8pwR6OCTUwwWx-P51BiLkFZyqHzquKddXQ", + "crv":"P-256", + "x":"FQVaTOksf-XsCUrt4J1L2UGvtWaDwpboVlqbKBY2AIo", + "y":"6XFB9PYo7dyC5ViJSO9uXNYkxTJWn0d_mqJ__ZYhcNY" + }"#; + + pub(crate) const BOB_PKEY_P256_1: &str = r#"{ + "kty":"EC", + "crv":"P-256", + "x":"FQVaTOksf-XsCUrt4J1L2UGvtWaDwpboVlqbKBY2AIo", + "y":"6XFB9PYo7dyC5ViJSO9uXNYkxTJWn0d_mqJ__ZYhcNY" + }"#; + + pub(crate) const BOB_KID_P256_2: &str = "did:example:bob#key-p256-2"; + + pub(crate) const BOB_KEY_P256_2: &str = r#"{ + "kty":"EC", + "d":"agKz7HS8mIwqO40Q2dwm_Zi70IdYFtonN5sZecQoxYU", + "crv":"P-256", + "x":"n0yBsGrwGZup9ywKhzD4KoORGicilzIUyfcXb1CSwe0", + "y":"ov0buZJ8GHzV128jmCw1CaFbajZoFFmiJDbMrceCXIw" + }"#; + + pub(crate) const BOB_PKEY_P256_2: &str = r#"{ + "kty":"EC", + "crv":"P-256", + "x":"n0yBsGrwGZup9ywKhzD4KoORGicilzIUyfcXb1CSwe0", + "y":"ov0buZJ8GHzV128jmCw1CaFbajZoFFmiJDbMrceCXIw" + }"#; +} diff --git a/affinidi-messaging-didcomm/src/jwe/parse.rs b/affinidi-messaging-didcomm/src/jwe/parse.rs new file mode 100644 index 0000000..776df07 --- /dev/null +++ b/affinidi-messaging-didcomm/src/jwe/parse.rs @@ -0,0 +1,220 @@ +use crate::document::{did_or_url, DIDCommVerificationMethodExt}; +use crate::envelope::MetaEnvelope; +use crate::error::ToResult; +use crate::utils::crypto::AsKnownKeyPair; +use crate::{ + error::{err_msg, ErrorKind, Result, ResultExt}, + jwe::envelope::{Jwe, ProtectedHeader}, +}; +use affinidi_did_resolver_cache_sdk::document::DocumentExt; +use affinidi_did_resolver_cache_sdk::DIDCacheClient; +use base64::prelude::*; +use sha2::{Digest, Sha256}; +use tracing::debug; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ParsedJWE { + pub jwe: Jwe, + pub protected: ProtectedHeader, + pub apu: Option>, + pub apv: Vec, + pub to_kids: Vec, +} + +pub fn parse(jwe: &str) -> Result { + Jwe::from_str(jwe)?.parse() +} + +impl Jwe { + pub(crate) fn from_str(s: &str) -> Result { + serde_json::from_str(s).to_didcomm("Unable parse jwe") + } + + pub(crate) fn parse(self) -> Result { + // Strip off base64 padding + let decoded = BASE64_URL_SAFE_NO_PAD + .decode(self.protected.replace('=', "")) + .kind(ErrorKind::Malformed, "Unable decode protected header")?; + + let protected: ProtectedHeader = + serde_json::from_slice(&decoded).to_didcomm("Unable parse protected header")?; + + let apv = BASE64_URL_SAFE_NO_PAD + .decode(protected.apv.clone()) + .kind(ErrorKind::Malformed, "Unable decode apv")?; + + let apu = protected + .apu + .as_ref() + .map(|apu| BASE64_URL_SAFE_NO_PAD.decode(apu)) + .transpose() + .kind(ErrorKind::Malformed, "Unable decode apu")?; + + let to_kids = self + .recipients + .clone() + .iter() + .map(|r| r.header.kid.clone()) + .collect(); + let jwe = ParsedJWE { + jwe: self, + protected, + apu, + apv, + to_kids, + }; + + Ok(jwe) + } +} + +impl ParsedJWE { + /// Verifies that apv and apu filled according DID Comm specification. + pub(crate) fn verify_didcomm(self) -> Result { + let did_comm_apv = { + let mut kids = self + .jwe + .recipients + .iter() + .map(|r| r.header.kid.clone()) + .collect::>(); + + kids.sort(); + Sha256::digest(kids.join(".").as_bytes()) + }; + + if self.apv != did_comm_apv.as_slice() { + Err(err_msg(ErrorKind::Malformed, "APV mismatch"))?; + } + + let did_comm_apu = self + .apu + .as_deref() + .map(std::str::from_utf8) + .transpose() + .kind(ErrorKind::Malformed, "Invalid utf8 for apu")?; + + match (did_comm_apu, self.protected.skid.clone()) { + (Some(apu), Some(skid)) if apu != skid => { + Err(err_msg(ErrorKind::Malformed, "APU mismatch"))? + } + (None, Some(_)) => Err(err_msg(ErrorKind::Malformed, "SKID present, but no apu"))?, + _ => (), + }; + + Ok(self) + } + + /// Populates various from_* fields for the Envelope + pub async fn fill_envelope_from( + &self, + envelope: &mut MetaEnvelope, + did_resolver: &DIDCacheClient, + secrets_resolver: &dyn crate::secrets::SecretsResolver, + ) -> Result<&Self> { + debug!("Checking if APU exists in JWE?"); + if let Some(apu) = &self.apu { + debug!("Found APU in JWE. Algorithm type ({})", &self.protected.alg); + let from_kid = + std::str::from_utf8(apu).kind(ErrorKind::Malformed, "apu is invalid utf8")?; + + let (from_did, from_url) = did_or_url(from_kid); + + if from_url.is_none() { + Err(err_msg( + ErrorKind::Malformed, + "Sender key can't be resolved to key agreement", + ))?; + } + + envelope.from_did = Some(from_did.into()); + + envelope.from_ddoc = Some( + did_resolver + .resolve(from_did) + .await + .map_err(|e| err_msg(ErrorKind::DIDNotResolved, e))? + .doc, + ); + + if let Some(doc) = &envelope.from_ddoc { + // Valid DID Document + if doc.contains_key_agreement(from_kid) { + // Do we have a key agreement? + envelope.from_kid = Some(from_kid.into()); + } else { + Err(err_msg( + ErrorKind::DIDUrlNotFound, + "Sender kid not found in did", + ))?; + } + + // COnvert keys from the verification method that matches the sender key-id + if let Some(vm) = doc.get_verification_method(from_kid) { + let jwk = vm.get_jwk().ok_or_else(|| { + err_msg( + ErrorKind::Malformed, + "Can't convert verification method to a JWK", + ) + })?; + let key_pair = vm.as_key_pair(&jwk).map_err(|e| { + err_msg( + ErrorKind::Malformed, + format!("Can't convert verification method to a key pair: {}", e), + ) + })?; + envelope.from_key = Some(key_pair); + } else { + Err(err_msg( + ErrorKind::DIDUrlNotFound, + "Sender verification method not found in did", + ))?; + } + } + + envelope.metadata.authenticated = true; + envelope.metadata.encrypted = true; + envelope.metadata.encrypted_from_kid = Some(from_kid.into()); + } else { + debug!("APU not found in JWE. Setting anonymous sender"); + envelope.metadata.anonymous_sender = true; + } + + // Process info relating to the recipients + envelope.to_kid = Some( + self.to_kids + .first() + .ok_or_else(|| err_msg(ErrorKind::Malformed, "No recipient keys found"))? + .into(), + ); + + let (to_did, _) = did_or_url(envelope.to_kid.as_ref().unwrap()); + envelope.to_did = Some(to_did.into()); + + if self.to_kids.iter().any(|k| { + let (k_did, k_url) = did_or_url(k); + (k_did != to_did) || (k_url.is_none()) + }) { + Err(err_msg( + ErrorKind::Malformed, + "Recipient keys are outside of one did or can't be resolved to key agreement", + ))?; + } + + envelope.metadata.encrypted_to_kids = + Some(self.to_kids.iter().map(|k| k.to_owned()).collect()); + + debug!("envelope\n{:#?}", envelope); + + envelope.to_kids_found = secrets_resolver.find_secrets(&self.to_kids).await?; + + if envelope.to_kids_found.is_empty() { + Err(err_msg( + ErrorKind::SecretNotFound, + "No recipient secrets found", + ))?; + } + + Ok(self) + } +} diff --git a/affinidi-messaging-didcomm/src/jwk.rs b/affinidi-messaging-didcomm/src/jwk.rs new file mode 100644 index 0000000..2e150ef --- /dev/null +++ b/affinidi-messaging-didcomm/src/jwk.rs @@ -0,0 +1,82 @@ +use crate::error::{Error, ErrorKind, Result, ResultExt}; +use askar_crypto::{ + alg::{ed25519::Ed25519KeyPair, k256::K256KeyPair, p256::P256KeyPair, x25519::X25519KeyPair}, + jwk::{FromJwk, ToJwk}, +}; +use serde_json::Value; + +pub(crate) trait FromJwkValue: FromJwk { + /// Import the key from a JWK string reference + fn from_jwk_value(jwk: &Value) -> Result { + let jwk = serde_json::to_string(jwk) + .kind(ErrorKind::InvalidState, "Unable produce jwk string")?; + + Self::from_jwk(&jwk).map_err(|err| { + Error::msg( + ErrorKind::Malformed, + format!("{}: {}", "Unable produce jwk", err.message()), + ) + }) + } +} + +pub(crate) trait ToJwkValue: ToJwk { + fn to_jwk_public_value(&self) -> Result { + let jwk = self.to_jwk_public(None).map_err(|err| { + Error::msg( + ErrorKind::Malformed, + format!("{}: {}", "Unable produce jwk string", err.message()), + ) + })?; + + let jwk: Value = + serde_json::from_str(&jwk).kind(ErrorKind::InvalidState, "Unable produce jwk value")?; + + Ok(jwk) + } +} + +impl FromJwkValue for Ed25519KeyPair {} +impl FromJwkValue for P256KeyPair {} +impl FromJwkValue for X25519KeyPair {} +impl FromJwkValue for K256KeyPair {} + +impl ToJwkValue for Ed25519KeyPair {} +impl ToJwkValue for P256KeyPair {} +impl ToJwkValue for X25519KeyPair {} +impl ToJwkValue for K256KeyPair {} + +#[cfg(test)] +mod tests { + use askar_crypto::alg::ed25519::Ed25519KeyPair; + use serde_json::json; + + use super::*; + + #[test] + fn from_to_jwk_value_works() { + let jwk = json!({ + "crv":"Ed25519", + "d":"nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A", + "key_ops":["sign","verify"], + "kid":"FdFYFzERwC2uCBB46pZQi4GG85LujR8obt-KWRBICVQ", + "kty":"OKP", + "x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + }); + + let key = Ed25519KeyPair::from_jwk_value(&jwk).expect("unable from_jwk_value"); + + let pub_jwk = key + .to_jwk_public_value() + .expect("unable to_jwk_public_value"); + + assert_eq!( + pub_jwk, + json!({ + "crv":"Ed25519", + "kty":"OKP", + "x":"11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo" + }) + ); + } +} diff --git a/affinidi-messaging-didcomm/src/jws/envelope.rs b/affinidi-messaging-didcomm/src/jws/envelope.rs new file mode 100644 index 0000000..62c6598 --- /dev/null +++ b/affinidi-messaging-didcomm/src/jws/envelope.rs @@ -0,0 +1,145 @@ +use askar_crypto::sign::SignatureType; +use serde::{Deserialize, Serialize}; +use serde_enum_str::{Deserialize_enum_str, Serialize_enum_str}; + +use crate::error::{err_msg, ErrorKind, Result}; + +/// Subset of JWS in generic json serialization used for signed message type. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Jws { + /// Array of signatures + pub signatures: Vec, + + /// BASE64URL(JWS Payload) + pub payload: String, +} + +/// Represents a signature or MAC over the JWS Payload and +/// the JWS Protected Header. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Signature { + /// JWS unprotected header + /// Note it isn't serialized and not integrity protected + pub header: Header, + + /// BASE64URL(UTF8(JWS Protected Header)) + pub protected: String, + + /// BASE64URL(JWS signature) + /// Note JWS signature input is ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' || BASE64URL(JWS Payload)). + pub signature: String, +} + +/// JWS protected header. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct ProtectedHeader { + /// Must be `application/didcomm-signed+json` or `didcomm-signed+json` for now. + /// Something like `application/didcomm-signed+cbor` can be introduced in the + /// future. + pub typ: String, + + /// Cryptographic algorithm used to produce signature. + pub alg: Algorithm, +} + +/// JWS unprotected header. +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] +pub struct Header { + /// KID used to produce signature as DID URL. + pub kid: String, +} + +/// Header of compactly serialized JWS. +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +pub(crate) struct CompactHeader { + /// Media type of this complete JWS. + pub typ: String, + + /// Cryptographic algorithm used to produce signature. + pub alg: Algorithm, + + /// KID used to produce signature as DID URL. + pub kid: String, +} + +/// Represents possible values for `alg` header. +/// Cryptographic algorithm used to produce signature over JWS payload. +#[derive(Deserialize_enum_str, Serialize_enum_str, Debug, Clone, Eq, PartialEq)] +pub enum Algorithm { + #[serde(rename = "EdDSA")] + EdDSA, + + #[serde(rename = "ES256")] + Es256, + + #[serde(rename = "ES256K")] + Es256K, + + #[serde(other)] + Other(String), +} + +impl Algorithm { + pub(crate) fn sig_type(&self) -> Result { + let sig_type = match self { + Algorithm::EdDSA => SignatureType::EdDSA, + Algorithm::Es256 => SignatureType::ES256, + Algorithm::Es256K => SignatureType::ES256K, + Algorithm::Other(_) => Err(err_msg( + ErrorKind::Unsupported, + "Unsupported signature type", + ))?, + }; + + Ok(sig_type) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn algorithm_serialize_works() { + let alg = Algorithm::EdDSA; + let alg = serde_json::to_string(&alg).expect("Unable serialize"); + assert_eq!(alg, "\"EdDSA\""); + + let alg = Algorithm::Es256; + let alg = serde_json::to_string(&alg).expect("Unable serialize"); + assert_eq!(alg, "\"ES256\""); + + let alg = Algorithm::Es256K; + let alg = serde_json::to_string(&alg).expect("Unable serialize"); + assert_eq!(alg, "\"ES256K\""); + + let alg = Algorithm::Other("Unknown".into()); + let alg = serde_json::to_string(&alg).expect("Unable serialize"); + assert_eq!(alg, "\"Unknown\""); + + let alg = Algorithm::Other("Unknown 2".into()); + let alg = serde_json::to_string(&alg).expect("Unable serialize"); + assert_eq!(alg, "\"Unknown 2\""); + } + + #[test] + fn algorithm_deserialize_works() { + let alg: Algorithm = serde_json::from_str("\"EdDSA\"").expect("Unable deserialize"); + + assert_eq!(alg, Algorithm::EdDSA); + + let alg: Algorithm = serde_json::from_str("\"ES256\"").expect("Unable deserialize"); + + assert_eq!(alg, Algorithm::Es256); + + let alg: Algorithm = serde_json::from_str("\"ES256K\"").expect("Unable deserialize"); + + assert_eq!(alg, Algorithm::Es256K); + + let alg: Algorithm = serde_json::from_str("\"Unknown\"").expect("Unable deserialize"); + assert_eq!(alg, Algorithm::Other("Unknown".into())); + + let alg: Algorithm = serde_json::from_str("\"Unknown 2\"").expect("Unable deserialize"); + assert_eq!(alg, Algorithm::Other("Unknown 2".into())); + } +} diff --git a/affinidi-messaging-didcomm/src/jws/mod.rs b/affinidi-messaging-didcomm/src/jws/mod.rs new file mode 100644 index 0000000..ee0ad5f --- /dev/null +++ b/affinidi-messaging-didcomm/src/jws/mod.rs @@ -0,0 +1,89 @@ +// TODO: Remove allow +#[allow(dead_code)] +mod envelope; + +// TODO: Remove allow +#[allow(dead_code)] +mod parse; + +// TODO: Remove allow +#[allow(dead_code)] +mod sign; + +// TODO: Remove allow +#[allow(dead_code)] +mod verify; + +// TODO: Remove allow +#[allow(unused_imports)] +pub(crate) use envelope::{Algorithm, CompactHeader, Header, Jws, ProtectedHeader, Signature}; + +// TODO: Remove allow +#[allow(unused_imports)] +pub(crate) use sign::{sign, sign_compact}; + +// TODO: Remove allow +#[allow(unused_imports)] +pub(crate) use parse::{parse, parse_compact, ParsedCompactJWS, ParsedJWS}; + +#[cfg(test)] +mod tests { + use askar_crypto::{alg::ed25519::Ed25519KeyPair, jwk::FromJwk}; + + use crate::jws::{self, Algorithm}; + + #[test] + fn demo_works() { + // Identifier of Alice key + let alice_kid = "did:example:alice#key-1"; + + // Alice private key + let alice_key = Ed25519KeyPair::from_jwk( + r#" + { + "kty":"OKP", + "d":"pFRUKkyzx4kHdJtFSnlPA9WzqkDT1HWV0xZ5OYZd2SY", + "crv":"Ed25519", + "x":"G-boxFB6vOZBu-wXkm-9Lh79I8nf9Z50cILaOgKKGww" + } + "#, + ) + .expect("Unable from_jwk"); + + // Alice public key + let alice_pkey = Ed25519KeyPair::from_jwk( + r#" + { + "kty":"OKP", + "crv":"Ed25519", + "x":"G-boxFB6vOZBu-wXkm-9Lh79I8nf9Z50cILaOgKKGww" + } + "#, + ) + .expect("Unable from_jwk"); + + // Message payload + let payload = "Hello World!"; + + // Produce signed message + + let msg = jws::sign( + payload.as_bytes(), + (alice_kid, &alice_key), + Algorithm::EdDSA, + ) + .expect("unable sign"); + + // Parse message + + let msg = jws::parse(&msg).expect("Unable parse"); + + // Verify signature + + let valid = msg + .verify((alice_kid, &alice_pkey)) + .expect("Unable verify."); + + assert!(valid); + } +} diff --git a/affinidi-messaging-didcomm/src/jws/parse.rs b/affinidi-messaging-didcomm/src/jws/parse.rs new file mode 100644 index 0000000..22e418f --- /dev/null +++ b/affinidi-messaging-didcomm/src/jws/parse.rs @@ -0,0 +1,621 @@ +use crate::error::ToResult; +use crate::{ + error::{err_msg, ErrorKind, Result, ResultExt}, + jws::envelope::{CompactHeader, Jws, ProtectedHeader}, +}; +use base64::prelude::*; +use serde::Serialize; + +#[derive(Debug, PartialEq, Eq, Clone, Serialize)] +pub struct ParsedJWS { + pub jws: Jws, + pub protected: Vec, +} + +pub fn parse(jws: &str) -> Result { + Jws::from_str(jws)?.parse() +} + +impl Jws { + pub(crate) fn from_str(s: &str) -> Result { + serde_json::from_str(s).to_didcomm("Unable parse jws") + } + + pub(crate) fn parse(&self) -> Result { + let protected = { + let len = self.signatures.len(); + let mut protected = Vec::::with_capacity(len); + let mut buf = Vec::with_capacity(len); + buf.resize(len, vec![]); + + for (i, b) in buf.iter_mut().enumerate() { + let signature = self + .signatures + .get(i) + .ok_or_else(|| err_msg(ErrorKind::InvalidState, "Invalid signature index"))?; + + BASE64_URL_SAFE_NO_PAD + .decode_vec(&signature.protected, b) + .kind(ErrorKind::Malformed, "Unable decode protected header")?; + + let p: ProtectedHeader = + serde_json::from_slice(b).to_didcomm("Unable parse protected header")?; + + protected.push(p); + } + + protected + }; + + Ok(ParsedJWS { + jws: self.clone(), + protected, + }) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub(crate) struct ParsedCompactJWS { + pub(crate) header: String, + pub(crate) parsed_header: CompactHeader, + pub(crate) payload: String, + pub(crate) signature: String, +} + +pub(crate) fn parse_compact(compact_jws: &str) -> Result { + let segments: Vec<&str> = compact_jws.split('.').collect(); + if segments.len() != 3 { + return Err(err_msg( + ErrorKind::Malformed, + "Unable to parse compactly serialized JWS", + )); + } + + let header = segments[0]; + let payload = segments[1]; + let signature = segments[2]; + + let mut buf: Vec = Vec::new(); + BASE64_URL_SAFE_NO_PAD + .decode_vec(header, &mut buf) + .kind(ErrorKind::Malformed, "Unable decode header")?; + + let parsed_header: CompactHeader = + serde_json::from_slice(buf.as_slice()).kind(ErrorKind::Malformed, "Unable parse header")?; + + Ok(ParsedCompactJWS { + header: header.into(), + parsed_header, + payload: payload.into(), + signature: signature.into(), + }) +} + +#[cfg(test)] +mod tests { + use crate::jws::{CompactHeader, ParsedCompactJWS}; + use crate::{ + error::ErrorKind, + jws::{ + self, + envelope::{Algorithm, Header, Jws, ProtectedHeader, Signature}, + ParsedJWS, + }, + }; + + #[test] + fn parse_works() { + let msg = r#" + { + "payload":"eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures":[ + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRWREU0EifQ", + "signature":"FW33NnvOHV0Ted9-F7GZbkia-vYAfBKtH4oBxbrttWAhBZ6UFJMxcGjL3lwOl4YohI3kyyd08LHPWNMgP2EVCQ", + "header":{ + "kid":"did:example:alice#key-1" + } + } + ] + } + "#; + + let res = jws::parse(msg); + let res = res.expect("res is err"); + + let exp = ParsedJWS { + jws: Jws { + signatures: vec![Signature { + header: Header { kid: "did:example:alice#key-1".into() }, + protected: "eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRWREU0EifQ".into(), + signature: "FW33NnvOHV0Ted9-F7GZbkia-vYAfBKtH4oBxbrttWAhBZ6UFJMxcGjL3lwOl4YohI3kyyd08LHPWNMgP2EVCQ".into(), + }], + payload: "eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19".into(), + }, + protected: vec![ProtectedHeader { + typ: "application/didcomm-signed+json".into(), + alg: Algorithm::EdDSA, + }], + }; + + assert_eq!(res, exp); + } + + #[test] + fn parse_works_unknown_fields() { + let msg = r#" + { + "payload":"eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures":[ + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRWREU0EifQ", + "signature":"FW33NnvOHV0Ted9-F7GZbkia-vYAfBKtH4oBxbrttWAhBZ6UFJMxcGjL3lwOl4YohI3kyyd08LHPWNMgP2EVCQ", + "header":{ + "kid":"did:example:alice#key-1" + } + } + ], + "extra":"value" + } + "#; + + let res = jws::parse(msg); + let res = res.expect("res is err"); + + let exp = ParsedJWS { + jws: Jws { + signatures: vec![Signature { + header: Header { kid: "did:example:alice#key-1".into() }, + protected: "eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRWREU0EifQ".into(), + signature: "FW33NnvOHV0Ted9-F7GZbkia-vYAfBKtH4oBxbrttWAhBZ6UFJMxcGjL3lwOl4YohI3kyyd08LHPWNMgP2EVCQ".into(), + }], + payload: "eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19".into(), + }, + protected: vec![ProtectedHeader { + typ: "application/didcomm-signed+json".into(), + alg: Algorithm::EdDSA, + }], + }; + + assert_eq!(res, exp); + } + + #[test] + fn parse_works_protected_unknown_fields() { + let msg = r#" + { + "payload":"eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures":[ + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRWREU0EiLCJleHRyYSI6InZhbHVlIn0", + "signature":"FW33NnvOHV0Ted9-F7GZbkia-vYAfBKtH4oBxbrttWAhBZ6UFJMxcGjL3lwOl4YohI3kyyd08LHPWNMgP2EVCQ", + "header":{ + "kid":"did:example:alice#key-1" + } + } + ] + } + "#; + + let res = jws::parse(msg); + let res = res.expect("res is err"); + + let exp = ParsedJWS { + jws: Jws { + signatures: vec![Signature { + header: Header { kid: "did:example:alice#key-1".into() }, + protected: "eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRWREU0EiLCJleHRyYSI6InZhbHVlIn0".into(), + signature: "FW33NnvOHV0Ted9-F7GZbkia-vYAfBKtH4oBxbrttWAhBZ6UFJMxcGjL3lwOl4YohI3kyyd08LHPWNMgP2EVCQ".into(), + }], + payload: "eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19".into(), + }, + protected: vec![ProtectedHeader { + typ: "application/didcomm-signed+json".into(), + alg: Algorithm::EdDSA, + }], + }; + + assert_eq!(res, exp); + } + + #[test] + fn parse_works_multiple_signatures() { + let msg = r#" + { + "payload":"eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures":[ + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRWREU0EifQ", + "signature":"FW33NnvOHV0Ted9-F7GZbkia-vYAfBKtH4oBxbrttWAhBZ6UFJMxcGjL3lwOl4YohI3kyyd08LHPWNMgP2EVCQ", + "header":{ + "kid":"did:example:alice#key-1" + } + }, + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRWREU0EifQ", + "signature":"FW33NnvOHV0Ted9-F7GZbkia-vYAfBKtH4oBxbrttWAhBZ6UFJMxcGjL3lwOl4YohI3kyyd08LHPWNMgP2EVCQ", + "header":{ + "kid":"did:example:alice#key-2" + } + } + ] + } + "#; + + let res = jws::parse(msg); + let res = res.expect("res is err"); + + let exp = ParsedJWS { + jws: Jws { + signatures: vec![ + Signature { + header: Header { kid: "did:example:alice#key-1".into() }, + protected: "eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRWREU0EifQ".into(), + signature: "FW33NnvOHV0Ted9-F7GZbkia-vYAfBKtH4oBxbrttWAhBZ6UFJMxcGjL3lwOl4YohI3kyyd08LHPWNMgP2EVCQ".into(), + }, + Signature { + header: Header { kid: "did:example:alice#key-2".into() }, + protected: "eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRWREU0EifQ".into(), + signature: "FW33NnvOHV0Ted9-F7GZbkia-vYAfBKtH4oBxbrttWAhBZ6UFJMxcGjL3lwOl4YohI3kyyd08LHPWNMgP2EVCQ".into(), + } + ], + payload: "eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19".into(), + }, + protected: vec![ + ProtectedHeader { + typ: "application/didcomm-signed+json".into(), + alg: Algorithm::EdDSA, + }, + ProtectedHeader { + typ: "application/didcomm-signed+json".into(), + alg: Algorithm::EdDSA, + } + ], + }; + + assert_eq!(res, exp); + } + + #[test] + fn parse_works_unparsable() { + let msg = r#" + { + "payload":"eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures":[ + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRWREU0EifQ", + "signature":"FW33NnvOHV0Ted9-F7GZbkia-vYAfBKtH4oBxbrttWAhBZ6UFJMxcGjL3lwOl4YohI3kyyd08LHPWNMgP2EVCQ", + "header":{ + "kid":"did:example:alice#key-1", + } + } + ] + } + "#; + + let res = jws::parse(msg); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: Unable parse jws: trailing comma at line 10 column 19" + ); + } + + #[test] + fn parse_works_misstructured() { + let msg = r#" + { + "payload":"eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures":[ + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRWREU0EifQ", + "signature":"FW33NnvOHV0Ted9-F7GZbkia-vYAfBKtH4oBxbrttWAhBZ6UFJMxcGjL3lwOl4YohI3kyyd08LHPWNMgP2EVCQ", + "header":{ + } + } + ] + } + "#; + + let res = jws::parse(msg); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: Unable parse jws: missing field `kid` at line 9 column 17" + ); + } + + #[test] + fn parse_works_undecodable_protected() { + let msg = r#" + { + "payload":"eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures":[ + { + "protected":"!eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRWREU0EifQ", + "signature":"FW33NnvOHV0Ted9-F7GZbkia-vYAfBKtH4oBxbrttWAhBZ6UFJMxcGjL3lwOl4YohI3kyyd08LHPWNMgP2EVCQ", + "header":{ + "kid":"did:example:alice#key-1" + } + } + ] + } + "#; + + let res = jws::parse(msg); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: Unable decode protected header: Invalid symbol 33, offset 0." + ); + } + + #[test] + fn parse_works_unparsable_protected() { + let msg = r#" + { + "payload":"eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures":[ + { + "protected":"ey4idHlwIjoiYXBwbGljYXRpb24vZGlkY29tbS1zaWduZWQranNvbiIsImFsZyI6IkVkRFNBIn0", + "signature":"FW33NnvOHV0Ted9-F7GZbkia-vYAfBKtH4oBxbrttWAhBZ6UFJMxcGjL3lwOl4YohI3kyyd08LHPWNMgP2EVCQ", + "header":{ + "kid":"did:example:alice#key-1" + } + } + ] + } + "#; + + let res = jws::parse(msg); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: Unable parse protected header: key must be a string at line 1 column 2" + ); + } + + #[test] + fn parse_works_misstructured_protected() { + let msg = r#" + { + "payload":"eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures":[ + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIn0", + "signature":"FW33NnvOHV0Ted9-F7GZbkia-vYAfBKtH4oBxbrttWAhBZ6UFJMxcGjL3lwOl4YohI3kyyd08LHPWNMgP2EVCQ", + "header":{ + "kid":"did:example:alice#key-1" + } + } + ] + } + "#; + + let res = jws::parse(msg); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: Unable parse protected header: missing field `alg` at line 1 column 41" + ); + } + + #[test] + fn parse_compact_works() { + let msg = + "eyJ0eXAiOiJleGFtcGxlLXR5cC0xIiwiYWxnIjoiRWREU0EiLCJraWQiOiJkaWQ6ZXhhbXBsZTphbGlj\ + ZSNrZXktMSJ9\ + .\ + eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0\ + eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3Nh\ + bCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVh\ + dGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNz\ + YWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19\ + .\ + iMi3kOWHTWoKiuTT4JxD9CkcUwSby9ekpOQk0Xdm9_H6jDpLPuhfX4U2EYgdPIJERl95MIecEhrufvO4\ + bHgtCg"; + + let res = jws::parse_compact(msg); + let res = res.expect("res is err"); + + let exp = ParsedCompactJWS { + header: "eyJ0eXAiOiJleGFtcGxlLXR5cC0xIiwiYWxnIjoiRWREU0EiLCJraWQiOiJkaWQ6ZXhhbXBsZTphbGlj\ + ZSNrZXktMSJ9".into(), + parsed_header: CompactHeader { + typ: "example-typ-1".into(), + alg: Algorithm::EdDSA, + kid: "did:example:alice#key-1".into(), + }, + payload: "eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0\ + eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3Nh\ + bCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVh\ + dGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNz\ + YWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19".into(), + signature: "iMi3kOWHTWoKiuTT4JxD9CkcUwSby9ekpOQk0Xdm9_H6jDpLPuhfX4U2EYgdPIJERl95MIecEhrufvO4\ + bHgtCg".into(), + }; + + assert_eq!(res, exp); + } + + #[test] + fn parse_compact_works_header_unknown_fields() { + let msg = + "eyJ0eXAiOiJleGFtcGxlLXR5cC0xIiwiYWxnIjoiRWREU0EiLCJraWQiOiJkaWQ6ZXhhbXBsZTphbGlj\ + ZSNrZXktMSIsImV4dHJhIjoidmFsdWUifQ\ + .\ + eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0\ + eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3Nh\ + bCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVh\ + dGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNz\ + YWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19\ + .\ + iMi3kOWHTWoKiuTT4JxD9CkcUwSby9ekpOQk0Xdm9_H6jDpLPuhfX4U2EYgdPIJERl95MIecEhrufvO4\ + bHgtCg"; + + let res = jws::parse_compact(msg); + let res = res.expect("res is err"); + + let exp = ParsedCompactJWS { + header: "eyJ0eXAiOiJleGFtcGxlLXR5cC0xIiwiYWxnIjoiRWREU0EiLCJraWQiOiJkaWQ6ZXhhbXBsZTphbGlj\ + ZSNrZXktMSIsImV4dHJhIjoidmFsdWUifQ".into(), + parsed_header: CompactHeader { + typ: "example-typ-1".into(), + alg: Algorithm::EdDSA, + kid: "did:example:alice#key-1".into(), + }, + payload: "eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0\ + eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3Nh\ + bCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVh\ + dGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNz\ + YWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19".into(), + signature: "iMi3kOWHTWoKiuTT4JxD9CkcUwSby9ekpOQk0Xdm9_H6jDpLPuhfX4U2EYgdPIJERl95MIecEhrufvO4\ + bHgtCg".into(), + }; + + assert_eq!(res, exp); + } + + #[test] + fn parse_compact_works_too_few_segments() { + let msg = + "eyJ0eXAiOiJleGFtcGxlLXR5cC0xIiwiYWxnIjoiRWREU0EiLCJraWQiOiJkaWQ6ZXhhbXBsZTphbGlj\ + ZSNrZXktMSJ9\ + .\ + eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0\ + eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3Nh\ + bCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVh\ + dGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNz\ + YWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19"; + + let res = jws::parse_compact(msg); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: Unable to parse compactly serialized JWS" + ); + } + + #[test] + fn parse_compact_works_too_many_segments() { + let msg = + "eyJ0eXAiOiJleGFtcGxlLXR5cC0xIiwiYWxnIjoiRWREU0EiLCJraWQiOiJkaWQ6ZXhhbXBsZTphbGlj\ + ZSNrZXktMSJ9\ + .\ + eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0\ + eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3Nh\ + bCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVh\ + dGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNz\ + YWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19\ + .\ + iMi3kOWHTWoKiuTT4JxD9CkcUwSby9ekpOQk0Xdm9_H6jDpLPuhfX4U2EYgdPIJERl95MIecEhrufvO4\ + bHgtCg\ + .\ + eyJ0eXAiOiJleGFtcGxlLXR5cC0xIiwiYWxnIjoiRWREU0EiLCJraWQiOiJkaWQ6ZXhhbXBsZTphbGlj\ + ZSNrZXktMSJ9"; + + let res = jws::parse_compact(msg); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: Unable to parse compactly serialized JWS" + ); + } + + #[test] + fn parse_compact_works_undecodable_header() { + let msg = + "!eyJ0eXAiOiJleGFtcGxlLXR5cC0xIiwiYWxnIjoiRWREU0EiLCJraWQiOiJkaWQ6ZXhhbXBsZTphbGlj\ + ZSNrZXktMSJ9\ + .\ + eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0\ + eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3Nh\ + bCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVh\ + dGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNz\ + YWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19\ + .\ + iMi3kOWHTWoKiuTT4JxD9CkcUwSby9ekpOQk0Xdm9_H6jDpLPuhfX4U2EYgdPIJERl95MIecEhrufvO4\ + bHgtCg"; + + let res = jws::parse_compact(msg); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: Unable decode header: Invalid symbol 33, offset 0." + ); + } + + #[test] + fn parse_compact_works_unparsable_header() { + let msg = + "ey4idHlwIjoiZXhhbXBsZS10eXAtMSIsImFsZyI6IkVkRFNBIiwia2lkIjoiZGlkOmV4YW1wbGU6YWxp\ + Y2Uja2V5LTEifQ\ + .\ + eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0\ + eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3Nh\ + bCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVh\ + dGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNz\ + YWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19\ + .\ + iMi3kOWHTWoKiuTT4JxD9CkcUwSby9ekpOQk0Xdm9_H6jDpLPuhfX4U2EYgdPIJERl95MIecEhrufvO4\ + bHgtCg"; + + let res = jws::parse_compact(msg); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: Unable parse header: key must be a string at line 1 column 2" + ); + } + + #[test] + fn parse_compact_works_misstructured_header() { + let msg = "eyJ0eXAiOiJleGFtcGxlLXR5cC0xIiwia2lkIjoiZGlkOmV4YW1wbGU6YWxpY2Uja2V5LTEifQ\ + .\ + eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0\ + eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3Nh\ + bCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVh\ + dGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNz\ + YWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19\ + .\ + iMi3kOWHTWoKiuTT4JxD9CkcUwSby9ekpOQk0Xdm9_H6jDpLPuhfX4U2EYgdPIJERl95MIecEhrufvO4\ + bHgtCg"; + + let res = jws::parse_compact(msg); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: Unable parse header: missing field `alg` at line 1 column 55" + ); + } +} diff --git a/affinidi-messaging-didcomm/src/jws/sign.rs b/affinidi-messaging-didcomm/src/jws/sign.rs new file mode 100644 index 0000000..5c3c396 --- /dev/null +++ b/affinidi-messaging-didcomm/src/jws/sign.rs @@ -0,0 +1,570 @@ +use askar_crypto::sign::KeySign; +use base64::prelude::*; + +use crate::{ + error::{Error, ErrorKind, Result, ResultExt}, + jws::envelope::{Algorithm, CompactHeader, Header, Jws, ProtectedHeader, Signature}, +}; + +pub(crate) fn sign( + payload: &[u8], + signer: (&str, &Key), + alg: Algorithm, +) -> Result { + let (kid, key) = signer; + + let sig_type = alg.sig_type()?; + + let protected = { + let protected = ProtectedHeader { + typ: "application/didcomm-signed+json".into(), + alg, + }; + + let protected = serde_json::to_string(&protected) + .kind(ErrorKind::InvalidState, "Unable serialize protected header")?; + + BASE64_URL_SAFE_NO_PAD.encode(protected) + }; + + let payload = BASE64_URL_SAFE_NO_PAD.encode(payload); + + let signature = { + // JWS Signing Input + // The input to the digital signature or MAC computation. Its value + // is ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' || BASE64URL(JWS Payload)). + let sign_input = format!("{}.{}", protected, payload); + + let signature = key + .create_signature(sign_input.as_bytes(), Some(sig_type)) + .map_err(|err| { + Error::msg( + ErrorKind::InvalidState, + format!("{}: {}", "Unable create signature", err.message()), + ) + })?; + + BASE64_URL_SAFE_NO_PAD.encode(&signature) + }; + + let signature = Signature { + header: Header { kid: kid.into() }, + protected, + signature, + }; + + let jws = Jws { + signatures: vec![signature], + payload, + }; + + let jws = serde_json::to_string(&jws).kind(ErrorKind::InvalidState, "Unable serialize jws")?; + + Ok(jws) +} + +pub(crate) fn sign_compact( + payload: &[u8], + signer: (&str, &Key), + typ: &str, + alg: Algorithm, +) -> Result { + let (kid, key) = signer; + + let sig_type = alg.sig_type()?; + + let header = { + let header = CompactHeader { + typ: typ.into(), + alg, + kid: kid.into(), + }; + + let header = serde_json::to_string(&header) + .kind(ErrorKind::InvalidState, "Unable serialize header")?; + + BASE64_URL_SAFE_NO_PAD.encode(header) + }; + + let payload = BASE64_URL_SAFE_NO_PAD.encode(payload); + + let signature = { + // JWS Signing Input + // The input to the digital signature or MAC computation. Its value + // is ASCII(BASE64URL(UTF8(JWS Protected Header)) || '.' || BASE64URL(JWS Payload)). + let sign_input = format!("{}.{}", header, payload); + + let signature = key + .create_signature(sign_input.as_bytes(), Some(sig_type)) + .map_err(|err| { + Error::msg( + ErrorKind::InvalidState, + format!("{}: {}", "Unable create signature", err.message()), + ) + })?; + + BASE64_URL_SAFE_NO_PAD.encode(&signature) + }; + + let compact_jws = format!("{}.{}.{}", header, payload, signature); + + Ok(compact_jws) +} + +#[cfg(test)] +mod tests { + use askar_crypto::{ + alg::{ed25519::Ed25519KeyPair, k256::K256KeyPair, p256::P256KeyPair}, + jwk::FromJwk, + sign::{KeySigVerify, KeySign}, + }; + use base64::prelude::*; + + use crate::{ + error::{ErrorKind, Result}, + jws::{self, envelope::Algorithm}, + }; + + #[test] + fn sign_works() { + _sign_works::( + ALICE_KID_ED25519, + ALICE_KEY_ED25519, + ALICE_PKEY_ED25519, + Algorithm::EdDSA, + PAYLOAD, + ); + + _sign_works::( + ALICE_KID_P256, + ALICE_KEY_P256, + ALICE_PKEY_P256, + Algorithm::Es256, + PAYLOAD, + ); + + _sign_works::( + ALICE_KID_K256, + ALICE_KEY_K256, + ALICE_PKEY_K256, + Algorithm::Es256K, + PAYLOAD, + ); + + fn _sign_works( + kid: &str, + key: &str, + pkey: &str, + alg: Algorithm, + payload: &str, + ) { + let res = _sign::(kid, key, alg.clone(), payload); + + let msg = res.expect("Unable _sign"); + + let msg = jws::parse(&msg).expect("Unable parse"); + + assert_eq!(msg.jws.payload, BASE64_URL_SAFE_NO_PAD.encode(payload)); + + assert_eq!(msg.jws.signatures.len(), 1); + assert_eq!(msg.jws.signatures[0].header.kid, kid); + + assert_eq!(msg.protected.len(), 1); + assert_eq!(msg.protected[0].alg, alg); + assert_eq!(msg.protected[0].typ, "application/didcomm-signed+json"); + + let pkey = K::from_jwk(pkey).expect("Unable from_jwk"); + let valid = msg.verify::((kid, &pkey)).expect("Unable verify"); + + assert!(valid); + } + } + + #[test] + fn sign_works_incompatible_alg() { + _sign_works_incompatible_alg::( + ALICE_KID_ED25519, + ALICE_KEY_ED25519, + Algorithm::Es256, + PAYLOAD, + ); + + _sign_works_incompatible_alg::( + ALICE_KID_ED25519, + ALICE_KEY_ED25519, + Algorithm::Es256K, + PAYLOAD, + ); + + _sign_works_incompatible_alg::( + ALICE_KID_P256, + ALICE_KEY_P256, + Algorithm::Es256K, + PAYLOAD, + ); + + _sign_works_incompatible_alg::( + ALICE_KID_P256, + ALICE_KEY_P256, + Algorithm::EdDSA, + PAYLOAD, + ); + + _sign_works_incompatible_alg::( + ALICE_KID_P256, + ALICE_KEY_P256, + Algorithm::Es256K, + PAYLOAD, + ); + + _sign_works_incompatible_alg::( + ALICE_KID_K256, + ALICE_KEY_K256, + Algorithm::Es256, + PAYLOAD, + ); + + _sign_works_incompatible_alg::( + ALICE_KID_K256, + ALICE_KEY_K256, + Algorithm::EdDSA, + PAYLOAD, + ); + + _sign_works_incompatible_alg::( + ALICE_KID_K256, + ALICE_KEY_K256, + Algorithm::Es256, + PAYLOAD, + ); + + fn _sign_works_incompatible_alg( + kid: &str, + key: &str, + alg: Algorithm, + payload: &str, + ) { + let res = _sign::(kid, key, alg.clone(), payload); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::InvalidState); + + assert_eq!( + format!("{}", err), + "Invalid state: Unable create signature: Unsupported signature type" + ); + } + } + + #[test] + fn sign_works_unknown_alg() { + _sign_works_unknown_alg::( + ALICE_KID_ED25519, + ALICE_KEY_ED25519, + Algorithm::Other("bls".to_owned()), + PAYLOAD, + ); + + _sign_works_unknown_alg::( + ALICE_KID_P256, + ALICE_KEY_P256, + Algorithm::Other("bls".to_owned()), + PAYLOAD, + ); + + _sign_works_unknown_alg::( + ALICE_KID_K256, + ALICE_KEY_K256, + Algorithm::Other("bls".to_owned()), + PAYLOAD, + ); + + fn _sign_works_unknown_alg( + kid: &str, + key: &str, + alg: Algorithm, + payload: &str, + ) { + let res = _sign::(kid, key, alg.clone(), payload); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Unsupported); + + assert_eq!( + format!("{}", err), + "Unsupported crypto or method: Unsupported signature type" + ); + } + } + + #[test] + fn sign_compact_works() { + _sign_compact_works::( + ALICE_KID_ED25519, + ALICE_KEY_ED25519, + ALICE_PKEY_ED25519, + "example-typ-1", + Algorithm::EdDSA, + PAYLOAD, + ); + + _sign_compact_works::( + ALICE_KID_P256, + ALICE_KEY_P256, + ALICE_PKEY_P256, + "example-typ-2", + Algorithm::Es256, + PAYLOAD, + ); + + _sign_compact_works::( + ALICE_KID_K256, + ALICE_KEY_K256, + ALICE_PKEY_K256, + "example-typ-3", + Algorithm::Es256K, + PAYLOAD, + ); + + fn _sign_compact_works( + kid: &str, + key: &str, + pkey: &str, + typ: &str, + alg: Algorithm, + payload: &str, + ) { + let res = _sign_compact::(kid, key, typ, alg.clone(), payload); + + let msg = res.expect("Unable _sign_compact"); + + let msg = jws::parse_compact(&msg).expect("Unable parse_compact"); + + assert_eq!(msg.payload, BASE64_URL_SAFE_NO_PAD.encode(payload)); + + assert_eq!(msg.parsed_header.typ, typ); + assert_eq!(msg.parsed_header.alg, alg); + assert_eq!(msg.parsed_header.kid, kid); + + let pkey = K::from_jwk(pkey).expect("Unable from_jwk"); + let valid = msg.verify::(&pkey).expect("Unable verify"); + + assert!(valid); + } + } + + #[test] + fn sign_compact_works_incompatible_alg() { + _sign_compact_works_incompatible_alg::( + ALICE_KID_ED25519, + ALICE_KEY_ED25519, + "example-typ-1", + Algorithm::Es256, + PAYLOAD, + ); + + _sign_compact_works_incompatible_alg::( + ALICE_KID_ED25519, + ALICE_KEY_ED25519, + "example-typ-1", + Algorithm::Es256K, + PAYLOAD, + ); + + _sign_compact_works_incompatible_alg::( + ALICE_KID_P256, + ALICE_KEY_P256, + "example-typ-1", + Algorithm::Es256K, + PAYLOAD, + ); + + _sign_compact_works_incompatible_alg::( + ALICE_KID_P256, + ALICE_KEY_P256, + "example-typ-1", + Algorithm::EdDSA, + PAYLOAD, + ); + + _sign_compact_works_incompatible_alg::( + ALICE_KID_P256, + ALICE_KEY_P256, + "example-typ-1", + Algorithm::Es256K, + PAYLOAD, + ); + + _sign_compact_works_incompatible_alg::( + ALICE_KID_K256, + ALICE_KEY_K256, + "example-typ-1", + Algorithm::Es256, + PAYLOAD, + ); + + _sign_compact_works_incompatible_alg::( + ALICE_KID_K256, + ALICE_KEY_K256, + "example-typ-1", + Algorithm::EdDSA, + PAYLOAD, + ); + + _sign_compact_works_incompatible_alg::( + ALICE_KID_K256, + ALICE_KEY_K256, + "example-typ-1", + Algorithm::Es256, + PAYLOAD, + ); + + fn _sign_compact_works_incompatible_alg( + kid: &str, + key: &str, + typ: &str, + alg: Algorithm, + payload: &str, + ) { + let res = _sign_compact::(kid, key, typ, alg.clone(), payload); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::InvalidState); + + assert_eq!( + format!("{}", err), + "Invalid state: Unable create signature: Unsupported signature type" + ); + } + } + + #[test] + fn sign_compact_works_unknown_alg() { + _sign_compact_works_unknown_alg::( + ALICE_KID_ED25519, + ALICE_KEY_ED25519, + "example-typ-1", + Algorithm::Other("bls".to_owned()), + PAYLOAD, + ); + + _sign_compact_works_unknown_alg::( + ALICE_KID_P256, + ALICE_KEY_P256, + "example-typ-1", + Algorithm::Other("bls".to_owned()), + PAYLOAD, + ); + + _sign_compact_works_unknown_alg::( + ALICE_KID_K256, + ALICE_KEY_K256, + "example-typ-1", + Algorithm::Other("bls".to_owned()), + PAYLOAD, + ); + + fn _sign_compact_works_unknown_alg( + kid: &str, + key: &str, + typ: &str, + alg: Algorithm, + payload: &str, + ) { + let res = _sign_compact::(kid, key, typ, alg.clone(), payload); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Unsupported); + + assert_eq!( + format!("{}", err), + "Unsupported crypto or method: Unsupported signature type" + ); + } + } + + fn _sign( + kid: &str, + key: &str, + alg: Algorithm, + payload: &str, + ) -> Result { + let key = K::from_jwk(key).expect("Unable from_jwk"); + jws::sign(payload.as_bytes(), (kid, &key), alg.clone()) + } + + fn _sign_compact( + kid: &str, + key: &str, + typ: &str, + alg: Algorithm, + payload: &str, + ) -> Result { + let key = K::from_jwk(key).expect("Unable from_jwk"); + jws::sign_compact(payload.as_bytes(), (kid, &key), typ, alg.clone()) + } + + const ALICE_KID_ED25519: &str = "did:example:alice#key-1"; + + const ALICE_KEY_ED25519: &str = r#" + { + "kty":"OKP", + "d":"pFRUKkyzx4kHdJtFSnlPA9WzqkDT1HWV0xZ5OYZd2SY", + "crv":"Ed25519", + "x":"G-boxFB6vOZBu-wXkm-9Lh79I8nf9Z50cILaOgKKGww" + } + "#; + + const ALICE_PKEY_ED25519: &str = r#" + { + "kty":"OKP", + "crv":"Ed25519", + "x":"G-boxFB6vOZBu-wXkm-9Lh79I8nf9Z50cILaOgKKGww" + } + "#; + + const ALICE_KID_P256: &str = "did:example:alice#key-2"; + + const ALICE_KEY_P256: &str = r#" + { + "kty":"EC", + "d":"7TCIdt1rhThFtWcEiLnk_COEjh1ZfQhM4bW2wz-dp4A", + "crv":"P-256", + "x":"2syLh57B-dGpa0F8p1JrO6JU7UUSF6j7qL-vfk1eOoY", + "y":"BgsGtI7UPsObMRjdElxLOrgAO9JggNMjOcfzEPox18w" + } + "#; + + const ALICE_PKEY_P256: &str = r#" + { + "kty":"EC", + "crv":"P-256", + "x":"2syLh57B-dGpa0F8p1JrO6JU7UUSF6j7qL-vfk1eOoY", + "y":"BgsGtI7UPsObMRjdElxLOrgAO9JggNMjOcfzEPox18w" + } + "#; + + const ALICE_KID_K256: &str = "did:example:alice#key-3"; + + const ALICE_KEY_K256: &str = r#" + { + "kty":"EC", + "d":"N3Hm1LXA210YVGGsXw_GklMwcLu_bMgnzDese6YQIyA", + "crv":"secp256k1", + "x":"aToW5EaTq5mlAf8C5ECYDSkqsJycrW-e1SQ6_GJcAOk", + "y":"JAGX94caA21WKreXwYUaOCYTBMrqaX4KWIlsQZTHWCk" + } + "#; + + const ALICE_PKEY_K256: &str = r#" + { + "kty":"EC", + "d":"N3Hm1LXA210YVGGsXw_GklMwcLu_bMgnzDese6YQIyA", + "crv":"secp256k1", + "x":"aToW5EaTq5mlAf8C5ECYDSkqsJycrW-e1SQ6_GJcAOk", + "y":"JAGX94caA21WKreXwYUaOCYTBMrqaX4KWIlsQZTHWCk" + } + "#; + + const PAYLOAD: &str = r#"{"id":"1234567890","typ":"application/didcomm-plain+json","type":"http://example.com/protocols/lets_do_lunch/1.0/proposal","from":"did:example:alice","to":["did:example:bob"],"created_time":1516269022,"expires_time":1516385931,"body":{"messagespecificattribute":"and its value"}}"#; +} diff --git a/affinidi-messaging-didcomm/src/jws/verify.rs b/affinidi-messaging-didcomm/src/jws/verify.rs new file mode 100644 index 0000000..f23c636 --- /dev/null +++ b/affinidi-messaging-didcomm/src/jws/verify.rs @@ -0,0 +1,683 @@ +use askar_crypto::sign::KeySigVerify; + +use crate::{ + error::{err_msg, Error, ErrorKind, Result, ResultExt}, + jws::{ParsedCompactJWS, ParsedJWS}, +}; +use base64::prelude::*; + +impl ParsedJWS { + pub(crate) fn verify(&self, signer: (&str, &Key)) -> Result { + let (kid, key) = signer; + + let (i, signature) = self + .jws + .signatures + .iter() + .enumerate() + .find(|(_, sig)| sig.header.kid == kid) + .ok_or_else(|| err_msg(ErrorKind::InvalidState, "KID not found"))?; + + let protected = self + .protected + .get(i) + .ok_or_else(|| err_msg(ErrorKind::InvalidState, "Invalid protected header index"))?; + + let sig_type = protected.alg.sig_type()?; + let sign_input = format!("{}.{}", signature.protected, self.jws.payload); + + let signature = BASE64_URL_SAFE_NO_PAD + .decode(&signature.signature) + .kind(ErrorKind::Malformed, "Unable decode signature")?; + + let valid = key + .verify_signature(sign_input.as_bytes(), &signature, Some(sig_type)) + .map_err(|err| { + Error::msg( + ErrorKind::Malformed, + format!("{}: {}", "Unable verify signature", err.message()), + ) + })?; + + Ok(valid) + } +} + +impl ParsedCompactJWS { + pub(crate) fn verify(&self, key: &Key) -> Result { + let sig_type = self.parsed_header.alg.sig_type()?; + let sign_input = format!("{}.{}", self.header, self.payload); + + let signature = BASE64_URL_SAFE_NO_PAD + .decode(&self.signature) + .kind(ErrorKind::Malformed, "Unable decode signature")?; + + let valid = key + .verify_signature(sign_input.as_bytes(), &signature, Some(sig_type)) + .map_err(|err| { + Error::msg( + ErrorKind::Malformed, + format!("{}: {}", "Unable verify signature", err.message()), + ) + })?; + + Ok(valid) + } +} + +#[cfg(test)] +mod tests { + use askar_crypto::{ + alg::{ed25519::Ed25519KeyPair, k256::K256KeyPair, p256::P256KeyPair}, + jwk::FromJwk, + sign::KeySigVerify, + }; + + use crate::{ + error::{Error, ErrorKind}, + jws, + }; + + #[test] + fn verify_works() { + _verify_works::(ALICE_KID_ED25519, ALICE_PKEY_ED25519, ALICE_MSG_ED25519); + _verify_works::(ALICE_KID_P256, ALICE_PKEY_P256, ALICE_MSG_P256); + _verify_works::(ALICE_KID_K256, ALICE_PKEY_K256, ALICE_MSG_K256); + + fn _verify_works(kid: &str, key: &str, msg: &str) { + let res = _verify::(kid, key, msg); + let res = res.expect("res is err"); + assert!(res); + } + } + + #[test] + fn verify_works_multiple_signatures() { + _verify_works_multiple_signatures::( + ALICE_KID_ED25519, + ALICE_PKEY_ED25519, + ALICE_MSG_ED25519_P256_K256, + ); + + _verify_works_multiple_signatures::( + ALICE_KID_P256, + ALICE_PKEY_P256, + ALICE_MSG_ED25519_P256_K256, + ); + + _verify_works_multiple_signatures::( + ALICE_KID_K256, + ALICE_PKEY_K256, + ALICE_MSG_ED25519_P256_K256, + ); + + fn _verify_works_multiple_signatures( + kid: &str, + key: &str, + msg: &str, + ) { + let res = _verify::(kid, key, msg); + let res = res.expect("res is err"); + assert!(res); + } + } + + #[test] + fn verify_works_different_key() { + _verify_works_different_key::( + ALICE_KID_ED25519, + ALICE_PKEY_ED25519, + BOB_MSG_ED25519, + ); + + _verify_works_different_key::(ALICE_KID_P256, ALICE_PKEY_P256, BOB_MSG_P256); + + _verify_works_different_key::(ALICE_KID_K256, ALICE_PKEY_K256, BOB_MSG_K256); + + fn _verify_works_different_key(kid: &str, key: &str, msg: &str) { + let res = _verify::(kid, key, msg); + let res = res.expect("res is err"); + assert!(!res); + } + } + + #[test] + fn verify_works_changed_payload() { + _verify_works_changed_payload::( + ALICE_KID_ED25519, + ALICE_PKEY_ED25519, + ALICE_MSG_ED25519_CHANGED_PAYLOAD, + ); + + _verify_works_changed_payload::( + ALICE_KID_P256, + ALICE_PKEY_P256, + ALICE_MSG_P256_CHANGED_PAYLOAD, + ); + + _verify_works_changed_payload::( + ALICE_KID_K256, + ALICE_PKEY_K256, + ALICE_MSG_K256_CHANGED_PAYLOAD, + ); + + fn _verify_works_changed_payload( + kid: &str, + key: &str, + msg: &str, + ) { + let res = _verify::(kid, key, msg); + let res = res.expect("res is err"); + assert!(!res); + } + } + + #[test] + fn verify_works_different_curve() { + _verify_works_different_curve::( + ALICE_KID_P256, + ALICE_PKEY_ED25519, + ALICE_MSG_P256, + ); + + _verify_works_different_curve::( + ALICE_KID_K256, + ALICE_PKEY_ED25519, + ALICE_MSG_K256, + ); + + _verify_works_different_curve::( + ALICE_KID_ED25519, + ALICE_PKEY_P256, + BOB_MSG_ED25519, + ); + + _verify_works_different_curve::(ALICE_KID_K256, ALICE_PKEY_P256, BOB_MSG_K256); + + _verify_works_different_curve::( + ALICE_KID_ED25519, + ALICE_PKEY_K256, + BOB_MSG_ED25519, + ); + + _verify_works_different_curve::(ALICE_KID_P256, ALICE_PKEY_K256, BOB_MSG_P256); + + fn _verify_works_different_curve( + kid: &str, + key: &str, + msg: &str, + ) { + let res = _verify::(kid, key, msg); + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: Unable verify signature: Unsupported signature type" + ); + } + } + + #[test] + fn verify_works_kid_not_found() { + _verify_works_kid_not_found::( + ALICE_KID_P256, + ALICE_PKEY_ED25519, + ALICE_MSG_ED25519, + ); + + _verify_works_kid_not_found::(ALICE_KID_K256, ALICE_PKEY_P256, ALICE_MSG_P256); + + _verify_works_kid_not_found::( + ALICE_KID_ED25519, + ALICE_PKEY_K256, + ALICE_MSG_K256, + ); + + fn _verify_works_kid_not_found(kid: &str, key: &str, msg: &str) { + let res = _verify::(kid, key, msg); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::InvalidState); + assert_eq!(format!("{}", err), "Invalid state: KID not found"); + } + } + + #[test] + fn verify_works_undecodable_sig() { + _verify_works_undecodable_sig::( + ALICE_KID_ED25519, + ALICE_PKEY_ED25519, + ALICE_MSG_ED25519_UNDECODABLE_SIG, + ); + + _verify_works_undecodable_sig::( + ALICE_KID_P256, + ALICE_PKEY_P256, + ALICE_MSG_P256_UNDECODABLE_SIG, + ); + + _verify_works_undecodable_sig::( + ALICE_KID_K256, + ALICE_PKEY_K256, + ALICE_MSG_K256_UNDECODABLE_SIG, + ); + + fn _verify_works_undecodable_sig( + kid: &str, + key: &str, + msg: &str, + ) { + let res = _verify::(kid, key, msg); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: Unable decode signature: Invalid symbol 33, offset 0." + ); + } + } + + #[test] + fn verify_compact_works() { + let res = _verify_compact::(ALICE_PKEY_ED25519, ALICE_COMPACT_MSG_ED25519); + let res = res.expect("res is err"); + assert!(res); + } + + #[test] + fn verify_compact_works_different_key() { + // use Dave's public key instead of Alice's one + let res = + _verify_compact::(CHARLIE_PKEY_ED25519, ALICE_COMPACT_MSG_ED25519); + let res = res.expect("res is err"); + assert!(!res); + } + + #[test] + fn verify_compact_works_changed_payload() { + let res = _verify_compact::( + ALICE_PKEY_ED25519, + ALICE_COMPACT_MSG_ED25519_CHANGED_PAYLOAD, + ); + + let res = res.expect("res is err"); + assert!(!res); + } + + #[test] + fn verify_compact_works_different_curve() { + let res = _verify_compact::(ALICE_PKEY_P256, ALICE_COMPACT_MSG_ED25519); + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: Unable verify signature: Unsupported signature type" + ); + } + + #[test] + fn verify_compact_works_undecodable_sig() { + let res = _verify_compact::( + ALICE_PKEY_ED25519, + ALICE_COMPACT_MSG_ED25519_UNDECODABLE_SIG, + ); + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: Unable decode signature: Invalid symbol 33, offset 0." + ); + } + + fn _verify( + kid: &str, + key: &str, + msg: &str, + ) -> Result { + let key = Key::from_jwk(key).expect("unable from_jwk."); + + let msg = jws::parse(msg).expect("unable parse."); + + msg.verify((kid, &key)) + } + + fn _verify_compact(key: &str, msg: &str) -> Result { + let key = Key::from_jwk(key).expect("unable from_jwk."); + + let msg = jws::parse_compact(msg).expect("unable parse."); + + msg.verify(&key) + } + + const ALICE_KID_ED25519: &str = "did:example:alice#key-1"; + + const ALICE_KEY_ED25519: &str = r#" + { + "kty":"OKP", + "d":"pFRUKkyzx4kHdJtFSnlPA9WzqkDT1HWV0xZ5OYZd2SY", + "crv":"Ed25519", + "x":"G-boxFB6vOZBu-wXkm-9Lh79I8nf9Z50cILaOgKKGww" + } + "#; + + const ALICE_PKEY_ED25519: &str = r#" + { + "kty":"OKP", + "crv":"Ed25519", + "x":"G-boxFB6vOZBu-wXkm-9Lh79I8nf9Z50cILaOgKKGww" + } + "#; + + const ALICE_MSG_ED25519: &str = r#" + { + "payload":"eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures":[ + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRWREU0EifQ", + "signature":"FW33NnvOHV0Ted9-F7GZbkia-vYAfBKtH4oBxbrttWAhBZ6UFJMxcGjL3lwOl4YohI3kyyd08LHPWNMgP2EVCQ", + "header":{ + "kid":"did:example:alice#key-1" + } + } + ] + } + "#; + + const ALICE_MSG_ED25519_CHANGED_PAYLOAD: &str = r#" + { + "payload":"eyJpZCI6IjAyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures":[ + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRWREU0EifQ", + "signature":"FW33NnvOHV0Ted9-F7GZbkia-vYAfBKtH4oBxbrttWAhBZ6UFJMxcGjL3lwOl4YohI3kyyd08LHPWNMgP2EVCQ", + "header":{ + "kid":"did:example:alice#key-1" + } + } + ] + } + "#; + + const ALICE_MSG_ED25519_UNDECODABLE_SIG: &str = r#" + { + "payload":"eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures":[ + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRWREU0EifQ", + "signature":"!FW33NnvOHV0Ted9-F7GZbkia-vYAfBKtH4oBxbrttWAhBZ6UFJMxcGjL3lwOl4YohI3kyyd08LHPWNMgP2EVCQ", + "header":{ + "kid":"did:example:alice#key-1" + } + } + ] + } + "#; + + const ALICE_KID_P256: &str = "did:example:alice#key-2"; + + const ALICE_KEY_P256: &str = r#" + { + "kty":"EC", + "d":"7TCIdt1rhThFtWcEiLnk_COEjh1ZfQhM4bW2wz-dp4A", + "crv":"P-256", + "x":"2syLh57B-dGpa0F8p1JrO6JU7UUSF6j7qL-vfk1eOoY", + "y":"BgsGtI7UPsObMRjdElxLOrgAO9JggNMjOcfzEPox18w" + } + "#; + + const ALICE_PKEY_P256: &str = r#" + { + "kty":"EC", + "crv":"P-256", + "x":"2syLh57B-dGpa0F8p1JrO6JU7UUSF6j7qL-vfk1eOoY", + "y":"BgsGtI7UPsObMRjdElxLOrgAO9JggNMjOcfzEPox18w" + } + "#; + + const ALICE_MSG_P256: &str = r#" + { + "payload":"eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures":[ + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRVMyNTYifQ", + "signature":"gcW3lVifhyR48mLHbbpnGZQuziskR5-wXf6IoBlpa9SzERfSG9I7oQ9pssmHZwbvJvyMvxskpH5oudw1W3X5Qg", + "header":{ + "kid":"did:example:alice#key-2" + } + } + ] + } + "#; + + const ALICE_MSG_P256_CHANGED_PAYLOAD: &str = r#" + { + "payload":"eyJpZCI6IjAyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures":[ + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRVMyNTYifQ", + "signature":"gcW3lVifhyR48mLHbbpnGZQuziskR5-wXf6IoBlpa9SzERfSG9I7oQ9pssmHZwbvJvyMvxskpH5oudw1W3X5Qg", + "header":{ + "kid":"did:example:alice#key-2" + } + } + ] + } + "#; + + const ALICE_MSG_P256_UNDECODABLE_SIG: &str = r#" + { + "payload":"eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures":[ + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRVMyNTYifQ", + "signature":"!gcW3lVifhyR48mLHbbpnGZQuziskR5-wXf6IoBlpa9SzERfSG9I7oQ9pssmHZwbvJvyMvxskpH5oudw1W3X5Qg", + "header":{ + "kid":"did:example:alice#key-2" + } + } + ] + } + "#; + + const ALICE_KID_K256: &str = "did:example:alice#key-3"; + + const ALICE_KEY_K256: &str = r#" + { + "kty":"EC", + "d":"N3Hm1LXA210YVGGsXw_GklMwcLu_bMgnzDese6YQIyA", + "crv":"secp256k1", + "x":"aToW5EaTq5mlAf8C5ECYDSkqsJycrW-e1SQ6_GJcAOk", + "y":"JAGX94caA21WKreXwYUaOCYTBMrqaX4KWIlsQZTHWCk" + } + "#; + + const ALICE_PKEY_K256: &str = r#" + { + "kty":"EC", + "d":"N3Hm1LXA210YVGGsXw_GklMwcLu_bMgnzDese6YQIyA", + "crv":"secp256k1", + "x":"aToW5EaTq5mlAf8C5ECYDSkqsJycrW-e1SQ6_GJcAOk", + "y":"JAGX94caA21WKreXwYUaOCYTBMrqaX4KWIlsQZTHWCk" + } + "#; + + const ALICE_MSG_K256: &str = r#" + { + "payload":"eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures":[ + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRVMyNTZLIn0", + "signature":"EGjhIcts6tqiJgqtxaTiTY3EUvL-_rLjn9lxaZ4eRUwa1-CS1nknZoyJWbyY5NQnUafWh5nvCtQpdpMyzH3blw", + "header":{ + "kid":"did:example:alice#key-3" + } + } + ] + } + "#; + + const ALICE_MSG_K256_CHANGED_PAYLOAD: &str = r#" + { + "payload":"eyJpZCI6IjAyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures":[ + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRVMyNTZLIn0", + "signature":"EGjhIcts6tqiJgqtxaTiTY3EUvL-_rLjn9lxaZ4eRUwa1-CS1nknZoyJWbyY5NQnUafWh5nvCtQpdpMyzH3blw", + "header":{ + "kid":"did:example:alice#key-3" + } + } + ] + } + "#; + + const ALICE_MSG_K256_UNDECODABLE_SIG: &str = r#" + { + "payload":"eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures":[ + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRVMyNTZLIn0", + "signature":"!EGjhIcts6tqiJgqtxaTiTY3EUvL-_rLjn9lxaZ4eRUwa1-CS1nknZoyJWbyY5NQnUafWh5nvCtQpdpMyzH3blw", + "header":{ + "kid":"did:example:alice#key-3" + } + } + ] + } + "#; + + const ALICE_MSG_ED25519_P256_K256: &str = r#" + { + "payload":"eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures":[ + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRWREU0EifQ", + "signature":"FW33NnvOHV0Ted9-F7GZbkia-vYAfBKtH4oBxbrttWAhBZ6UFJMxcGjL3lwOl4YohI3kyyd08LHPWNMgP2EVCQ", + "header":{ + "kid":"did:example:alice#key-1" + } + }, + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRVMyNTYifQ", + "signature":"gcW3lVifhyR48mLHbbpnGZQuziskR5-wXf6IoBlpa9SzERfSG9I7oQ9pssmHZwbvJvyMvxskpH5oudw1W3X5Qg", + "header":{ + "kid":"did:example:alice#key-2" + } + }, + { + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRVMyNTZLIn0", + "signature":"EGjhIcts6tqiJgqtxaTiTY3EUvL-_rLjn9lxaZ4eRUwa1-CS1nknZoyJWbyY5NQnUafWh5nvCtQpdpMyzH3blw", + "header":{ + "kid":"did:example:alice#key-3" + } + } + ] + } + "#; + + const ALICE_COMPACT_MSG_ED25519: &str = + "eyJ0eXAiOiJleGFtcGxlLXR5cC0xIiwiYWxnIjoiRWREU0EiLCJraWQiOiJkaWQ6ZXhhbXBsZTphbGlj\ + ZSNrZXktMSJ9\ + .\ + eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0\ + eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3Nh\ + bCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVh\ + dGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNz\ + YWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19\ + .\ + iMi3kOWHTWoKiuTT4JxD9CkcUwSby9ekpOQk0Xdm9_H6jDpLPuhfX4U2EYgdPIJERl95MIecEhrufvO4\ + bHgtCg"; + + const ALICE_COMPACT_MSG_ED25519_CHANGED_PAYLOAD: &str = + "eyJ0eXAiOiJleGFtcGxlLXR5cC0xIiwiYWxnIjoiRWREU0EiLCJraWQiOiJkaWQ6ZXhhbXBsZTphbGlj\ + ZSNrZXktMSJ9\ + .\ + eyJpZCI6IjAyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0\ + eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3Nh\ + bCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVh\ + dGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNz\ + YWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19\ + .\ + iMi3kOWHTWoKiuTT4JxD9CkcUwSby9ekpOQk0Xdm9_H6jDpLPuhfX4U2EYgdPIJERl95MIecEhrufvO4\ + bHgtCg"; + + const ALICE_COMPACT_MSG_ED25519_UNDECODABLE_SIG: &str = + "eyJ0eXAiOiJleGFtcGxlLXR5cC0xIiwiYWxnIjoiRWREU0EiLCJraWQiOiJkaWQ6ZXhhbXBsZTphbGlj\ + ZSNrZXktMSJ9\ + .\ + eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0\ + eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3Nh\ + bCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVh\ + dGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNz\ + YWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19\ + .\ + !iMi3kOWHTWoKiuTT4JxD9CkcUwSby9ekpOQk0Xdm9_H6jDpLPuhfX4U2EYgdPIJERl95MIecEhrufvO4\ + bHgtCg"; + + /* + Bob key Ed25519 + {"crv":"Ed25519","kty":"OKP","x":"ECEdOp9caYYVMgGomcV-bHjvJcRvh68COWd_GO-npGI","d":"CKMS_Kt8x6to6jD88d6EGo_H_fSG3L2SAEccLBGQG3U"} + */ + + const BOB_MSG_ED25519: &str = r#" + { + "signatures":[{ + "header":{ + "kid":"did:example:alice#key-1" + }, + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRWREU0EifQ", + "signature":"cc2lCOjvv0asYfTCRYHDeFecet0m8u9y2SeCINttTsk7uqjVTWz6fQpjcH-uaZs1UxsXYrFqJDX6QfnUUlb-Dw" + }], + "payload":"eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19" + }"#; + + /* + Bob key P-256: + {"crv":"P-256","kty":"EC","x":"v1rA6xXLkpDdjoyUXxoCaSJGnKr8SCHGaXQriEqw_mA","y":"b461bvXFQf3wq8XPg9ys5_bVnzBM1iCuJCOHwzmh2zY","d":"lp9UPAQDVv6xTheeEoVJ6pHKxjAM05y3lQOGOOC_OsM"} + */ + + const BOB_MSG_P256: &str = r#" + { + "signatures":[{ + "header":{ + "kid":"did:example:alice#key-2" + }, + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRVMyNTYifQ", + "signature":"CCib-Bcx-eL93AHHydN5ofpcdBaPpbmg9uy86LHw3CwcWyGCt5eiCTXkVRWYiXqNOjWn03N_de-nmJsNKyaw1w" + }], + "payload":"eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19" + } + "#; + + /* + Bob key secp256k1 + {"crv":"secp256k1","kty":"EC","x":"FCdNdYmCb37E84HSXaXsJHNawM-njeWTjr1g-PIN5dU","y":"Ds3RWwX770Yoyr0gEgKfxxACGRQEZg1s8BnE51CvNp8","d":"QOa9-22EOgjEa9CKJMKSrEQwsssJTQUlNyuhHLE7r1M"}" + */ + + const BOB_MSG_K256: &str = r#" + { + "signatures":[{ + "header":{ + "kid":"did:example:alice#key-3" + }, + "protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRVMyNTZLIn0", + "signature":"Jym1ZSzMJBq9hjOx0I0wG03I0nf2NySLt8GostuIQ3JE9hGluwhBBGeFaSATJt4OUEFsB_k0YuwPGSwbo3nKUw" + }], + "payload":"eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19" + } + "#; + + const CHARLIE_PKEY_ED25519: &str = r#" + { + "kty":"OKP", + "crv":"Ed25519", + "x":"VDXDwuGKVq91zxU6q7__jLDUq8_C5cuxECgd-1feFTE" + } + "#; +} diff --git a/affinidi-messaging-didcomm/src/lib.rs b/affinidi-messaging-didcomm/src/lib.rs new file mode 100644 index 0000000..d0f788f --- /dev/null +++ b/affinidi-messaging-didcomm/src/lib.rs @@ -0,0 +1,116 @@ +mod jwe; +mod jwk; +mod jws; +mod message; +mod utils; + +// Allows share test vectors between unit and integration tests +#[cfg(test)] +pub(crate) use crate as affinidi_messaging_didcomm; + +#[cfg(test)] +mod test_vectors; + +#[cfg(feature = "testvectors")] +pub(crate) use crate as affinidi_messaging_didcomm; + +#[cfg(feature = "testvectors")] +pub mod test_vectors; + +pub mod algorithms; +pub(crate) mod document; +pub mod envelope; +pub mod error; +pub mod protocols; +pub mod secrets; + +pub use message::{ + Attachment, AttachmentBuilder, AttachmentData, Base64AttachmentData, FromPrior, + JsonAttachmentData, LinksAttachmentData, Message, MessageBuilder, MessagingServiceMetadata, + PackEncryptedMetadata, PackEncryptedOptions, PackSignedMetadata, UnpackMetadata, UnpackOptions, +}; + +#[cfg(test)] +mod tests { + use affinidi_did_resolver_cache_sdk::config::ClientConfigBuilder; + use affinidi_did_resolver_cache_sdk::DIDCacheClient; + use serde_json::json; + + use crate::{ + secrets::resolvers::ExampleSecretsResolver, Message, PackEncryptedOptions, UnpackOptions, + }; + + #[tokio::test] + #[ignore = "will be fixed after https://github.com/sicpa-dlab/didcomm-gemini/issues/71"] + async fn demo_works() { + // --- Build message --- + + let sender = "did:example:1"; + let recipient = "did:example:2"; + + let msg = Message::build( + "example-1".into(), + "example/v1".into(), + json!("example-body"), + ) + .to(recipient.into()) + .from(sender.into()) + .finalize(); + + // --- Packing message --- + + let sender_did_resolver = DIDCacheClient::new(ClientConfigBuilder::default().build()) + .await + .unwrap(); + let sender_secrets_resolver = ExampleSecretsResolver::new(vec![]); + + let (packed_msg, metadata) = msg + .pack_encrypted( + recipient, + Some(sender), + None, + &sender_did_resolver, + &sender_secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + .expect("pack is ok."); + + // --- Send message using service endpoint --- + + let service_endpoint = metadata + .messaging_service + .expect("messagin service present.") + .service_endpoint; + + println!( + "Sending message {} through {}", + packed_msg, service_endpoint + ); + + // --- Unpacking message --- + + let recipient_did_resolver = DIDCacheClient::new(ClientConfigBuilder::default().build()) + .await + .unwrap(); + let recipient_secrets_resolver = ExampleSecretsResolver::new(vec![]); + + let (msg, metadata) = Message::unpack_string( + &packed_msg, + &recipient_did_resolver, + &recipient_secrets_resolver, + &UnpackOptions::default(), + ) + .await + .expect("unpack is ok."); + + assert!(metadata.encrypted); + assert!(metadata.authenticated); + assert!(metadata.encrypted_from_kid.is_some()); + assert!(metadata.encrypted_from_kid.unwrap().starts_with(recipient)); + + assert_eq!(msg.from, Some(sender.into())); + assert_eq!(msg.to, Some(vec![recipient.into()])); + assert_eq!(msg.body, json!("example-body")); + } +} diff --git a/affinidi-messaging-didcomm/src/message/attachment.rs b/affinidi-messaging-didcomm/src/message/attachment.rs new file mode 100644 index 0000000..327173e --- /dev/null +++ b/affinidi-messaging-didcomm/src/message/attachment.rs @@ -0,0 +1,333 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct Attachment { + /// A JSON object that gives access to the actual content of the attachment. + /// Can be based on base64, json or external links. + pub data: AttachmentData, + + /// Identifies attached content within the scope of a given message. + /// Recommended on appended attachment descriptors. Possible but generally unused + /// on embedded attachment descriptors. Never required if no references to the attachment + /// exist; if omitted, then there is no way to refer to the attachment later in the thread, + /// in error messages, and so forth. Because id is used to compose URIs, it is recommended + /// that this name be brief and avoid spaces and other characters that require URI escaping. + #[serde(skip_serializing_if = "Option::is_none")] + pub id: Option, + + /// A human-readable description of the content. + #[serde(skip_serializing_if = "Option::is_none")] + pub description: Option, + + /// A hint about the name that might be used if this attachment is persisted as a file. + /// It is not required, and need not be unique. If this field is present and mime-type is not, + /// the extension on the filename may be used to infer a MIME type. + #[serde(skip_serializing_if = "Option::is_none")] + pub filename: Option, + + /// Describes the MIME type of the attached content. + #[serde(skip_serializing_if = "Option::is_none")] + pub media_type: Option, + + /// Describes the format of the attachment if the mime_type is not sufficient. + #[serde(skip_serializing_if = "Option::is_none")] + pub format: Option, + + /// A hint about when the content in this attachment was last modified + /// in UTC Epoch Seconds (seconds since 1970-01-01T00:00:00Z UTC). + #[serde(skip_serializing_if = "Option::is_none")] + pub lastmod_time: Option, + + /// Mostly relevant when content is included by reference instead of by value. + /// Lets the receiver guess how expensive it will be, in time, bandwidth, and storage, + /// to fully fetch the attachment. + #[serde(skip_serializing_if = "Option::is_none")] + pub byte_count: Option, +} + +impl Attachment { + pub fn base64(base64: String) -> AttachmentBuilder { + AttachmentBuilder::new(AttachmentData::Base64 { + value: Base64AttachmentData { base64, jws: None }, + }) + } + + pub fn json(json: Value) -> AttachmentBuilder { + AttachmentBuilder::new(AttachmentData::Json { + value: JsonAttachmentData { json, jws: None }, + }) + } + + pub fn links(links: Vec, hash: String) -> AttachmentBuilder { + AttachmentBuilder::new(AttachmentData::Links { + value: LinksAttachmentData { + links, + hash, + jws: None, + }, + }) + } +} + +pub struct AttachmentBuilder { + data: AttachmentData, + id: Option, + description: Option, + filename: Option, + media_type: Option, + format: Option, + lastmod_time: Option, + byte_count: Option, +} + +impl AttachmentBuilder { + fn new(data: AttachmentData) -> Self { + AttachmentBuilder { + data, + id: None, + description: None, + filename: None, + media_type: None, + format: None, + lastmod_time: None, + byte_count: None, + } + } + + pub fn id(mut self, id: String) -> Self { + self.id = Some(id); + self + } + + pub fn description(mut self, description: String) -> Self { + self.description = Some(description); + self + } + + pub fn filename(mut self, filename: String) -> Self { + self.filename = Some(filename); + self + } + + pub fn media_type(mut self, media_type: String) -> Self { + self.media_type = Some(media_type); + self + } + + pub fn format(mut self, format: String) -> Self { + self.format = Some(format); + self + } + + pub fn lastmod_time(mut self, lastmod_time: u64) -> Self { + self.lastmod_time = Some(lastmod_time); + self + } + + pub fn byte_count(mut self, byte_count: u64) -> Self { + self.byte_count = Some(byte_count); + self + } + + pub fn jws(mut self, jws: String) -> Self { + match self.data { + AttachmentData::Base64 { ref mut value } => value.jws = Some(jws), + AttachmentData::Json { ref mut value } => value.jws = Some(jws), + AttachmentData::Links { ref mut value } => value.jws = Some(jws), + } + + self + } + + pub fn finalize(self) -> Attachment { + Attachment { + data: self.data, + id: self.id, + description: self.description, + filename: self.filename, + media_type: self.media_type, + format: self.format, + lastmod_time: self.lastmod_time, + byte_count: self.byte_count, + } + } +} + +// Attention: we are using untagged enum serialization variant. +// Serde will try to match the data against each variant in order and the +// first one that deserializes successfully is the one returned. +// It should work as we always have discrimination here. + +/// Represents attachment data in Base64, embedded Json or Links form. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +#[serde(untagged)] +pub enum AttachmentData { + Base64 { + #[serde(flatten)] + value: Base64AttachmentData, + }, + Json { + #[serde(flatten)] + value: JsonAttachmentData, + }, + Links { + #[serde(flatten)] + value: LinksAttachmentData, + }, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct Base64AttachmentData { + /// Base64-encoded data, when representing arbitrary content inline. + pub base64: String, + + /// A JSON Web Signature over the content of the attachment. + #[serde(skip_serializing_if = "Option::is_none")] + pub jws: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct JsonAttachmentData { + /// Directly embedded JSON data. + pub json: Value, + + /// A JSON Web Signature over the content of the attachment. + #[serde(skip_serializing_if = "Option::is_none")] + pub jws: Option, +} + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct LinksAttachmentData { + /// A list of one or more locations at which the content may be fetched. + pub links: Vec, + + /// The hash of the content encoded in multi-hash format. Used as an integrity check for the attachment. + pub hash: String, + + /// A JSON Web Signature over the content of the attachment. + #[serde(skip_serializing_if = "Option::is_none")] + pub jws: Option, +} + +#[cfg(test)] +mod tests { + use core::panic; + use serde_json::json; + + use super::*; + + #[test] + fn attachment_base64_works() { + let attachment = Attachment::base64("ZXhhbXBsZQ==".to_owned()) + .id("example-1".to_owned()) + .description("example-1-description".to_owned()) + .filename("attachment-1".to_owned()) + .media_type("message/example".to_owned()) + .format("json".to_owned()) + .lastmod_time(10000) + .byte_count(200) + .jws("jws".to_owned()) + .finalize(); + + let data = match attachment.data { + AttachmentData::Base64 { ref value } => value, + _ => panic!("data isn't base64."), + }; + + assert_eq!(data.base64, "ZXhhbXBsZQ=="); + assert_eq!(data.jws, Some("jws".to_owned())); + assert_eq!(attachment.id, Some("example-1".to_owned())); + + assert_eq!( + attachment.description, + Some("example-1-description".to_owned()) + ); + + assert_eq!(attachment.filename, Some("attachment-1".to_owned())); + assert_eq!(attachment.media_type, Some("message/example".to_owned())); + assert_eq!(attachment.format, Some("json".to_owned())); + assert_eq!(attachment.lastmod_time, Some(10000)); + assert_eq!(attachment.byte_count, Some(200)); + } + + #[test] + fn attachment_json_works() { + let attachment = Attachment::json(json!("example")) + .id("example-1".to_owned()) + .description("example-1-description".to_owned()) + .filename("attachment-1".to_owned()) + .media_type("message/example".to_owned()) + .format("json".to_owned()) + .lastmod_time(10000) + .byte_count(200) + .jws("jws".to_owned()) + .finalize(); + + let data = match attachment.data { + AttachmentData::Json { ref value } => value, + _ => panic!("data isn't json."), + }; + + assert_eq!(data.json, json!("example")); + assert_eq!(data.jws, Some("jws".to_owned())); + assert_eq!(attachment.id, Some("example-1".to_owned())); + + assert_eq!( + attachment.description, + Some("example-1-description".to_owned()) + ); + + assert_eq!(attachment.filename, Some("attachment-1".to_owned())); + assert_eq!(attachment.media_type, Some("message/example".to_owned())); + assert_eq!(attachment.format, Some("json".to_owned())); + assert_eq!(attachment.lastmod_time, Some(10000)); + assert_eq!(attachment.byte_count, Some(200)); + } + + #[test] + fn attachment_links_works() { + let attachment = Attachment::links( + vec!["http://example1".to_owned(), "https://example2".to_owned()], + "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c".to_owned(), + ) + .id("example-1".to_owned()) + .description("example-1-description".to_owned()) + .filename("attachment-1".to_owned()) + .media_type("message/example".to_owned()) + .format("json".to_owned()) + .lastmod_time(10000) + .byte_count(200) + .jws("jws".to_owned()) + .finalize(); + + let data = match attachment.data { + AttachmentData::Links { ref value } => value, + _ => panic!("data isn't links."), + }; + + assert_eq!( + data.links, + vec!["http://example1".to_owned(), "https://example2".to_owned()] + ); + + assert_eq!( + data.hash, + "50d858e0985ecc7f60418aaf0cc5ab587f42c2570a884095a9e8ccacd0f6545c".to_owned() + ); + + assert_eq!(data.jws, Some("jws".to_owned())); + assert_eq!(attachment.id, Some("example-1".to_owned())); + + assert_eq!( + attachment.description, + Some("example-1-description".to_owned()) + ); + + assert_eq!(attachment.filename, Some("attachment-1".to_owned())); + assert_eq!(attachment.media_type, Some("message/example".to_owned())); + assert_eq!(attachment.format, Some("json".to_owned())); + assert_eq!(attachment.lastmod_time, Some(10000)); + assert_eq!(attachment.byte_count, Some(200)); + } +} diff --git a/affinidi-messaging-didcomm/src/message/from_prior/mod.rs b/affinidi-messaging-didcomm/src/message/from_prior/mod.rs new file mode 100644 index 0000000..6d71316 --- /dev/null +++ b/affinidi-messaging-didcomm/src/message/from_prior/mod.rs @@ -0,0 +1,97 @@ +use serde::{Deserialize, Serialize}; + +mod pack; +mod unpack; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +/// `from_prior` header value as a plaintext. +/// https://identity.foundation/didcomm-messaging/spec/#did-rotation +pub struct FromPrior { + pub iss: String, + + pub sub: String, + + #[serde(skip_serializing_if = "Option::is_none")] + pub aud: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub exp: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub nbf: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub iat: Option, + + #[serde(skip_serializing_if = "Option::is_none")] + pub jti: Option, +} + +const JWT_TYP: &str = "JWT"; + +impl FromPrior { + pub fn build(iss: String, sub: String) -> FromPriorBuilder { + FromPriorBuilder::new(iss, sub) + } +} + +pub struct FromPriorBuilder { + iss: String, + sub: String, + aud: Option, + exp: Option, + nbf: Option, + iat: Option, + jti: Option, +} + +impl FromPriorBuilder { + fn new(iss: String, sub: String) -> Self { + FromPriorBuilder { + iss, + sub, + aud: None, + exp: None, + nbf: None, + iat: None, + jti: None, + } + } + + pub fn aud(mut self, aud: String) -> Self { + self.aud = Some(aud); + self + } + + pub fn exp(mut self, exp: u64) -> Self { + self.exp = Some(exp); + self + } + + pub fn nbf(mut self, nbf: u64) -> Self { + self.nbf = Some(nbf); + self + } + + pub fn iat(mut self, iat: u64) -> Self { + self.iat = Some(iat); + self + } + + pub fn jti(mut self, jti: String) -> Self { + self.jti = Some(jti); + self + } + + pub fn finalize(self) -> FromPrior { + FromPrior { + iss: self.iss, + sub: self.sub, + aud: self.aud, + exp: self.exp, + nbf: self.nbf, + iat: self.iat, + jti: self.jti, + } + } +} diff --git a/affinidi-messaging-didcomm/src/message/from_prior/pack.rs b/affinidi-messaging-didcomm/src/message/from_prior/pack.rs new file mode 100644 index 0000000..38a97e3 --- /dev/null +++ b/affinidi-messaging-didcomm/src/message/from_prior/pack.rs @@ -0,0 +1,372 @@ +use affinidi_did_resolver_cache_sdk::DIDCacheClient; + +use crate::{ + document::{did_or_url, is_did}, + error::{err_msg, ErrorKind, Result, ResultContext, ResultExt}, + jws::{self, Algorithm}, + message::from_prior::JWT_TYP, + secrets::SecretsResolver, + utils::crypto::{AsKnownKeyPairSecret, KnownKeyPair}, + FromPrior, +}; + +impl FromPrior { + /// Packs a plaintext `from_prior` value into a signed JWT. + /// https://identity.foundation/didcomm-messaging/spec/#did-rotation + /// + /// # Parameters + /// - `issuer_kid` (optional) identifier of the issuer key being used to sign `from_prior` JWT value. + /// - `did_resolver` instance of `DIDResolver` to resolve DIDs. + /// - `secrets_resolver` instance of `SecretsResolver` to resolve issuer DID keys secrets. + /// + /// # Returns + /// Tuple (signed `from_prior` JWT, identifier of the issuer key actually used to sign `from_prior`) + /// + /// # Errors + /// - `Malformed` `from_prior` plaintext value has invalid format. + /// - `IllegalArgument` `issuer_kid` is invalid or does not consist with `from_prior` plaintext value. + /// - `DIDNotResolved` Issuer DID not found. + /// - `DIDUrlNotFound` Issuer authentication verification method is not found. + /// - `SecretNotFound` Issuer secret is not found. + /// - `Unsupported` Used crypto or method is unsupported. + /// - `InvalidState` Indicates a library error. + pub async fn pack<'dr, 'sr>( + &self, + issuer_kid: Option<&str>, + did_resolver: &DIDCacheClient, + secrets_resolver: &'sr (dyn SecretsResolver + 'sr + Sync), + ) -> Result<(String, String)> { + self.validate_pack(issuer_kid)?; + + let from_prior_str = serde_json::to_string(self) + .kind(ErrorKind::InvalidState, "Unable serialize message")?; + + let did_doc = match did_resolver.resolve(&self.iss).await { + Ok(result) => result.doc, + Err(err) => { + return Err(err_msg( + ErrorKind::DIDNotResolved, + format!("from_prior issuer DIDDoc is not found. Reason: {}", err), + )); + } + }; + + let authentication_kids: Vec = if let Some(issuer_kid) = issuer_kid { + let (did, kid) = did_or_url(issuer_kid); + + let kid = kid.ok_or_else(|| { + err_msg( + ErrorKind::IllegalArgument, + "issuer_kid content is not DID URL", + ) + })?; + + if did != self.iss { + Err(err_msg( + ErrorKind::IllegalArgument, + "from_prior issuer kid does not belong to from_prior `iss`", + ))? + } + + let kid = did_doc + .verification_relationships + .authentication + .iter() + .find(|a| a.id().resolve(&did_doc.id.as_did()).to_string() == kid) + .ok_or_else(|| { + err_msg( + ErrorKind::DIDUrlNotFound, + "Provided issuer_kid is not found in DIDDoc", + ) + })?; + + vec![kid.id().resolve(did_doc.id.as_did()).to_string()] + } else { + did_doc + .verification_relationships + .authentication + .iter() + .map(|s| s.id().resolve(did_doc.id.as_did()).to_string()) + .collect() + }; + + let kid = secrets_resolver + .find_secrets(&authentication_kids) + .await + .context("Unable to find secrets")? + .first() + .ok_or_else(|| { + err_msg( + ErrorKind::SecretNotFound, + "No from_prior issuer secrets found", + ) + })? + .to_string(); + + let secret = secrets_resolver + .get_secret(&kid) + .await + .context("Unable to find secret")? + .ok_or_else(|| { + err_msg( + ErrorKind::SecretNotFound, + "from_prior issuer secret not found", + ) + })?; + + let sign_key = secret + .as_key_pair() + .context("Unable to instantiate from_prior issuer key")?; + + let from_prior_jwt = match sign_key { + KnownKeyPair::Ed25519(ref key) => jws::sign_compact( + from_prior_str.as_bytes(), + (&kid, key), + JWT_TYP, + Algorithm::EdDSA, + ), + KnownKeyPair::P256(ref key) => jws::sign_compact( + from_prior_str.as_bytes(), + (&kid, key), + JWT_TYP, + Algorithm::Es256, + ), + KnownKeyPair::K256(ref key) => jws::sign_compact( + from_prior_str.as_bytes(), + (&kid, key), + JWT_TYP, + Algorithm::Es256K, + ), + _ => Err(err_msg(ErrorKind::Unsupported, "Unsupported signature alg"))?, + } + .context("Unable to produce signature")?; + + Ok((from_prior_jwt, kid)) + } + + pub(crate) fn validate_pack(&self, issuer_kid: Option<&str>) -> Result<()> { + if !is_did(&self.iss) || did_or_url(&self.iss).1.is_some() { + Err(err_msg( + ErrorKind::Malformed, + "from_prior `iss` must be a non-fragment DID", + ))?; + } + + if !is_did(&self.sub) || did_or_url(&self.sub).1.is_some() { + Err(err_msg( + ErrorKind::Malformed, + "from_prior `sub` must be a non-fragment DID", + ))?; + } + + if self.iss == self.sub { + Err(err_msg( + ErrorKind::Malformed, + "from_prior `iss` and `sub` values must not be equal", + ))?; + } + + if let Some(issuer_kid) = issuer_kid { + let (did, kid) = did_or_url(issuer_kid); + + if kid.is_none() { + Err(err_msg( + ErrorKind::IllegalArgument, + "issuer_kid content is not DID URL", + ))?; + }; + + if did != self.iss { + Err(err_msg( + ErrorKind::IllegalArgument, + "from_prior issuer kid does not belong to from_prior `iss`", + ))?; + } + } + + Ok(()) + } +} + +// The following needs to be refactored to work with new DID Document model + +#[cfg(test)] +mod tests { + use affinidi_did_resolver_cache_sdk::{config::ClientConfigBuilder, DIDCacheClient}; + + use crate::{ + document::did_or_url, + error::ErrorKind, + secrets::resolvers::ExampleSecretsResolver, + test_vectors::{ + ALICE_DID, ALICE_SECRETS, ALICE_SECRET_AUTH_KEY_ED25519, CHARLIE_DID, + CHARLIE_ROTATED_TO_ALICE_SECRETS, CHARLIE_SECRET_AUTH_KEY_ED25519, FROM_PRIOR_FULL, + FROM_PRIOR_INVALID_EQUAL_ISS_AND_SUB, FROM_PRIOR_INVALID_ISS, FROM_PRIOR_INVALID_SUB, + FROM_PRIOR_MINIMAL, + }, + FromPrior, + }; + + #[tokio::test] + async fn from_prior_pack_works_with_issuer_kid() { + _from_prior_pack_works_with_issuer_kid(&FROM_PRIOR_MINIMAL).await; + _from_prior_pack_works_with_issuer_kid(&FROM_PRIOR_FULL).await; + + async fn _from_prior_pack_works_with_issuer_kid(from_prior: &FromPrior) { + let did_resolver = DIDCacheClient::new(ClientConfigBuilder::default().build()) + .await + .unwrap(); + let charlie_rotated_to_alice_secrets_resolver = + ExampleSecretsResolver::new(CHARLIE_ROTATED_TO_ALICE_SECRETS.clone()); + + let (from_prior_jwt, pack_kid) = from_prior + .pack( + Some(&CHARLIE_SECRET_AUTH_KEY_ED25519.id), + &did_resolver, + &charlie_rotated_to_alice_secrets_resolver, + ) + .await + .expect("Unable to pack FromPrior"); + + assert_eq!(pack_kid, CHARLIE_SECRET_AUTH_KEY_ED25519.id); + + let (unpacked_from_prior, unpack_kid) = + FromPrior::unpack(&from_prior_jwt, &did_resolver) + .await + .expect("Unable to unpack FromPrior JWT"); + + assert_eq!(&unpacked_from_prior, from_prior); + assert_eq!(unpack_kid, CHARLIE_SECRET_AUTH_KEY_ED25519.id); + } + } + + #[tokio::test] + async fn from_prior_pack_works_without_issuer_kid() { + _from_prior_pack_works_without_issuer_kid(&FROM_PRIOR_MINIMAL).await; + _from_prior_pack_works_without_issuer_kid(&FROM_PRIOR_FULL).await; + + async fn _from_prior_pack_works_without_issuer_kid(from_prior: &FromPrior) { + let did_resolver = DIDCacheClient::new(ClientConfigBuilder::default().build()) + .await + .unwrap(); + let charlie_rotated_to_alice_secrets_resolver = + ExampleSecretsResolver::new(CHARLIE_ROTATED_TO_ALICE_SECRETS.clone()); + + let (from_prior_jwt, pack_kid) = from_prior + .pack( + None, + &did_resolver, + &charlie_rotated_to_alice_secrets_resolver, + ) + .await + .expect("Unable to pack FromPrior"); + + let (did, kid) = did_or_url(&pack_kid); + assert!(kid.is_some()); + assert_eq!(did, CHARLIE_DID); + + let (unpacked_from_prior, unpack_kid) = + FromPrior::unpack(&from_prior_jwt, &did_resolver) + .await + .expect("Unable to unpack FromPrior JWT"); + + assert_eq!(&unpacked_from_prior, from_prior); + assert_eq!(unpack_kid, pack_kid); + } + } + + #[tokio::test] + async fn from_prior_pack_works_wrong_issuer_kid() { + _from_prior_pack_works_wrong_issuer_kid( + &FROM_PRIOR_FULL, + &ALICE_SECRET_AUTH_KEY_ED25519.id, + ErrorKind::IllegalArgument, + "Illegal argument: from_prior issuer kid does not belong to from_prior `iss`", + ) + .await; + + _from_prior_pack_works_wrong_issuer_kid( + &FROM_PRIOR_FULL, + ALICE_DID, + ErrorKind::IllegalArgument, + "Illegal argument: issuer_kid content is not DID URL", + ) + .await; + + _from_prior_pack_works_wrong_issuer_kid( + &FROM_PRIOR_FULL, + "invalid", + ErrorKind::IllegalArgument, + "Illegal argument: issuer_kid content is not DID URL", + ) + .await; + + async fn _from_prior_pack_works_wrong_issuer_kid( + from_prior: &FromPrior, + issuer_kid: &str, + err_kind: ErrorKind, + err_mgs: &str, + ) { + let did_resolver = DIDCacheClient::new(ClientConfigBuilder::default().build()) + .await + .unwrap(); + let alice_secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let err = from_prior + .pack(Some(issuer_kid), &did_resolver, &alice_secrets_resolver) + .await + .expect_err("res is ok"); + + assert_eq!(err.kind(), err_kind); + assert_eq!(format!("{}", err), err_mgs); + } + } + + #[tokio::test] + async fn from_prior_pack_works_invalid() { + _from_prior_pack_works_invalid( + &FROM_PRIOR_INVALID_ISS, + ErrorKind::Malformed, + "Malformed: from_prior `iss` must be a non-fragment DID", + ) + .await; + + _from_prior_pack_works_invalid( + &FROM_PRIOR_INVALID_SUB, + ErrorKind::Malformed, + "Malformed: from_prior `sub` must be a non-fragment DID", + ) + .await; + + _from_prior_pack_works_invalid( + &FROM_PRIOR_INVALID_EQUAL_ISS_AND_SUB, + ErrorKind::Malformed, + "Malformed: from_prior `iss` and `sub` values must not be equal", + ) + .await; + + async fn _from_prior_pack_works_invalid( + from_prior: &FromPrior, + err_kind: ErrorKind, + err_mgs: &str, + ) { + let did_resolver = DIDCacheClient::new(ClientConfigBuilder::default().build()) + .await + .unwrap(); + let charlie_rotated_to_alice_secrets_resolver = + ExampleSecretsResolver::new(CHARLIE_ROTATED_TO_ALICE_SECRETS.clone()); + + let err = from_prior + .pack( + None, + &did_resolver, + &charlie_rotated_to_alice_secrets_resolver, + ) + .await + .expect_err("res is ok"); + + assert_eq!(err.kind(), err_kind); + assert_eq!(format!("{}", err), err_mgs); + } + } +} diff --git a/affinidi-messaging-didcomm/src/message/from_prior/unpack.rs b/affinidi-messaging-didcomm/src/message/from_prior/unpack.rs new file mode 100644 index 0000000..966a543 --- /dev/null +++ b/affinidi-messaging-didcomm/src/message/from_prior/unpack.rs @@ -0,0 +1,210 @@ +use crate::{ + document::{did_or_url, DIDCommVerificationMethodExt}, + error::{err_msg, ErrorKind, Result, ResultContext, ResultExt}, + jws, + utils::crypto::AsKnownKeyPair, + FromPrior, +}; +use affinidi_did_resolver_cache_sdk::DIDCacheClient; +use askar_crypto::alg::{ed25519::Ed25519KeyPair, k256::K256KeyPair, p256::P256KeyPair}; +use base64::prelude::*; + +impl FromPrior { + /// Unpacks a plaintext value from a signed `from_prior` JWT. + /// https://identity.foundation/didcomm-messaging/spec/#did-rotation + /// + /// # Parameters + /// - `from_prior_jwt` signed `from_prior` JWT. + /// - `did_resolver` instance of `DIDResolver` to resolve DIDs. + /// + /// # Returns + /// Tuple (plaintext `from_prior` value, identifier of the issuer key used to sign `from_prior`) + /// + /// # Errors + /// - `Malformed` Signed `from_prior` JWT is malformed. + /// - `DIDNotResolved` Issuer DID not found. + /// - `DIDUrlNotFound` Issuer authentication verification method is not found. + /// - `Unsupported` Used crypto or method is unsupported. + pub async fn unpack( + from_prior_jwt: &str, + did_resolver: &DIDCacheClient, + ) -> Result<(FromPrior, String)> { + let parsed = jws::parse_compact(from_prior_jwt)?; + + let typ = &parsed.parsed_header.typ; + let alg = parsed.parsed_header.alg.clone(); + let kid = &parsed.parsed_header.kid; + + if typ != "JWT" { + Err(err_msg( + ErrorKind::Malformed, + "from_prior is malformed: typ is not JWT", + ))?; + } + + let (did, did_url) = did_or_url(kid); + + if did_url.is_none() { + Err(err_msg( + ErrorKind::Malformed, + "from_prior kid is not DID URL", + ))? + } + + let did_doc = match did_resolver.resolve(did).await { + Ok(response) => response.doc, + Err(err) => { + return Err(err_msg( + ErrorKind::DIDNotResolved, + format!( + "from_prior issuer DID ({}) couldn't be resolved. Reason: {}", + did, err + ), + )); + } + }; + + let kid = did_doc + .verification_relationships + .authentication + .iter() + .find(|a| a.id().resolve(&did_doc.id.as_did()).as_str() == kid) + .ok_or_else(|| { + err_msg( + ErrorKind::DIDUrlNotFound, + "Provided issuer_kid is not found in DIDDoc", + ) + })?; + + // TODO: dropping a reference here otherwise + let _kid = kid.id(); + let kid = _kid.resolve(&did_doc.id.as_did()); + let kid = kid.as_str(); + + let key = did_doc + .verification_method + .iter() + .find(|&vm| vm.id == kid) + .ok_or_else(|| { + err_msg( + ErrorKind::DIDUrlNotFound, + "from_prior issuer verification method not found in DIDDoc", + ) + })?; + + let jwk = key + .get_jwk() + .ok_or_else(|| err_msg(ErrorKind::Unsupported, "Couldn't convert key to jwk"))?; + + let valid = match alg { + jws::Algorithm::EdDSA => { + let key = key + .as_ed25519(&jwk) + .context("Unable to instantiate from_prior issuer key")?; + parsed + .verify::(&key) + .context("Unable to verify from_prior signature")? + } + jws::Algorithm::Es256 => { + let key = key + .as_p256(&jwk) + .context("Unable to instantiate from_prior issuer key")?; + + parsed + .verify::(&key) + .context("Unable to verify from_prior signature")? + } + jws::Algorithm::Es256K => { + let key = key + .as_k256(&jwk) + .context("Unable to instantiate from_prior issuer key")?; + + parsed + .verify::(&key) + .context("Unable to verify from_prior signature")? + } + jws::Algorithm::Other(_) => Err(err_msg( + ErrorKind::Unsupported, + "Unsupported signature algorithm", + ))?, + }; + + if !valid { + Err(err_msg(ErrorKind::Malformed, "Wrong from_prior signature"))? + } + + let payload = BASE64_URL_SAFE_NO_PAD.decode(parsed.payload).kind( + ErrorKind::Malformed, + "from_prior payload is not a valid base64", + )?; + + let payload = String::from_utf8(payload).kind( + ErrorKind::Malformed, + "Decoded from_prior payload is not a valid UTF-8", + )?; + + let from_prior: FromPrior = serde_json::from_str(&payload) + .kind(ErrorKind::Malformed, "Unable to parse from_prior")?; + + Ok((from_prior, kid.into())) + } +} + +#[cfg(test)] +mod tests { + use affinidi_did_resolver_cache_sdk::{config::ClientConfigBuilder, DIDCacheClient}; + + use crate::{ + error::ErrorKind, + test_vectors::{ + CHARLIE_SECRET_AUTH_KEY_ED25519, FROM_PRIOR_FULL, FROM_PRIOR_JWT_FULL, + FROM_PRIOR_JWT_INVALID, FROM_PRIOR_JWT_INVALID_SIGNATURE, + }, + FromPrior, + }; + + #[tokio::test] + async fn from_prior_unpack_works() { + let did_resolver = DIDCacheClient::new(ClientConfigBuilder::default().build()) + .await + .unwrap(); + + let (from_prior, issuer_kid) = FromPrior::unpack(FROM_PRIOR_JWT_FULL, &did_resolver) + .await + .expect("unpack FromPrior failed"); + + assert_eq!(&from_prior, &*FROM_PRIOR_FULL); + assert_eq!(issuer_kid, CHARLIE_SECRET_AUTH_KEY_ED25519.id); + } + + #[tokio::test] + async fn from_prior_unpack_works_invalid() { + let did_resolver = DIDCacheClient::new(ClientConfigBuilder::default().build()) + .await + .unwrap(); + + let err = FromPrior::unpack(FROM_PRIOR_JWT_INVALID, &did_resolver) + .await + .expect_err("res is ok"); + + assert_eq!(err.kind(), ErrorKind::Malformed); + assert_eq!( + format!("{}", err), + "Malformed: Unable to parse compactly serialized JWS" + ); + } + + #[tokio::test] + async fn from_prior_unpack_works_invalid_signature() { + let did_resolver = DIDCacheClient::new(ClientConfigBuilder::default().build()) + .await + .unwrap(); + + let err = FromPrior::unpack(FROM_PRIOR_JWT_INVALID_SIGNATURE, &did_resolver) + .await + .expect_err("res is ok"); + + assert_eq!(err.kind(), ErrorKind::Malformed); + assert_eq!(format!("{}", err), "Malformed: Unable to verify from_prior signature: Unable decode signature: Invalid last symbol 104, offset 85."); + } +} diff --git a/affinidi-messaging-didcomm/src/message/message.rs b/affinidi-messaging-didcomm/src/message/message.rs new file mode 100644 index 0000000..0fee3a7 --- /dev/null +++ b/affinidi-messaging-didcomm/src/message/message.rs @@ -0,0 +1,316 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; + +use super::Attachment; +use crate::error::{err_msg, ErrorKind, Result}; + +/// Wrapper for plain message. Provides helpers for message building and packing/unpacking. +#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct Message { + /// Message id. Must be unique to the sender. + pub id: String, + + /// Optional, if present it must be "application/didcomm-plain+json" + #[serde(default = "default_typ")] + pub typ: String, + + /// Message type attribute value MUST be a valid Message Type URI, + /// that when resolved gives human readable information about the message. + /// The attribute’s value also informs the content of the message, + /// or example the presence of other attributes and how they should be processed. + #[serde(rename = "type")] + pub type_: String, + + /// Message body. + pub body: Value, + + /// Sender identifier. The from attribute MUST be a string that is a valid DID + /// or DID URL (without the fragment component) which identifies the sender of the message. + #[serde(skip_serializing_if = "Option::is_none")] + pub from: Option, + + /// Identifier(s) for recipients. MUST be an array of strings where each element + /// is a valid DID or DID URL (without the fragment component) that identifies a member + /// of the message’s intended audience. + #[serde(skip_serializing_if = "Option::is_none")] + pub to: Option>, + + /// Uniquely identifies the thread that the message belongs to. + /// If not included the id property of the message MUST be treated as the value of the `thid`. + #[serde(skip_serializing_if = "Option::is_none")] + pub thid: Option, + + /// If the message is a child of a thread the `pthid` + /// will uniquely identify which thread is the parent. + #[serde(skip_serializing_if = "Option::is_none")] + pub pthid: Option, + + /// Custom message headers. + #[serde(flatten)] + #[serde(skip_serializing_if = "HashMap::is_empty")] + pub extra_headers: HashMap, + + /// The attribute is used for the sender + /// to express when they created the message, expressed in + /// UTC Epoch Seconds (seconds since 1970-01-01T00:00:00Z UTC). + /// This attribute is informative to the recipient, and may be relied on by protocols. + #[serde(skip_serializing_if = "Option::is_none")] + pub created_time: Option, + + /// The expires_time attribute is used for the sender to express when they consider + /// the message to be expired, expressed in UTC Epoch Seconds (seconds since 1970-01-01T00:00:00Z UTC). + /// This attribute signals when the message is considered no longer valid by the sender. + /// When omitted, the message is considered to have no expiration by the sender. + #[serde(skip_serializing_if = "Option::is_none")] + pub expires_time: Option, + + /// from_prior is a compactly serialized signed JWT containing FromPrior value + #[serde(skip_serializing_if = "Option::is_none")] + pub from_prior: Option, + + /// Message attachments + #[serde(skip_serializing_if = "Option::is_none")] + pub attachments: Option>, +} + +const PLAINTEXT_TYP: &str = "application/didcomm-plain+json"; + +fn default_typ() -> String { + PLAINTEXT_TYP.to_string() +} + +impl Message { + pub fn build(id: String, type_: String, body: Value) -> MessageBuilder { + MessageBuilder::new(id, type_, body) + } + + /*pub(crate) fn from_str(s: &str) -> Result { + serde_json::from_str(s).to_didcomm("Unable deserialize jwm") + }*/ + + pub(crate) fn validate(self) -> Result { + if self.typ != PLAINTEXT_TYP { + Err(err_msg( + ErrorKind::Malformed, + format!("`typ` must be \"{}\"", PLAINTEXT_TYP), + ))?; + } + Ok(self) + } +} + +pub struct MessageBuilder { + id: String, + type_: String, + body: Value, + from: Option, + to: Option>, + thid: Option, + pthid: Option, + extra_headers: HashMap, + created_time: Option, + expires_time: Option, + from_prior: Option, + attachments: Option>, +} + +impl MessageBuilder { + fn new(id: String, type_: String, body: Value) -> Self { + MessageBuilder { + id, + type_, + body, + from: None, + to: None, + thid: None, + pthid: None, + extra_headers: HashMap::new(), + created_time: None, + expires_time: None, + from_prior: None, + attachments: None, + } + } + + pub fn body(mut self, body: Value) -> Self { + self.body = body; + self + } + + pub fn to(mut self, to: String) -> Self { + if let Some(ref mut sto) = self.to { + sto.push(to); + self + } else { + self.to = Some(vec![to]); + self + } + } + + pub fn to_many(mut self, to: Vec) -> Self { + if let Some(ref mut sto) = self.to { + let mut to = to; + sto.append(&mut to); + self + } else { + self.to = Some(to); + self + } + } + + pub fn from(mut self, from: String) -> Self { + self.from = Some(from); + self + } + + pub fn thid(mut self, thid: String) -> Self { + self.thid = Some(thid); + self + } + + pub fn pthid(mut self, pthid: String) -> Self { + self.pthid = Some(pthid); + self + } + + pub fn header(mut self, key: String, value: Value) -> Self { + self.extra_headers.insert(key, value); + self + } + + pub fn created_time(mut self, created_time: u64) -> Self { + self.created_time = Some(created_time); + self + } + + pub fn expires_time(mut self, expires_time: u64) -> Self { + self.expires_time = Some(expires_time); + self + } + + pub fn from_prior(mut self, from_prior: String) -> Self { + self.from_prior = Some(from_prior); + self + } + + pub fn attachment(mut self, attachment: Attachment) -> Self { + if let Some(ref mut attachments) = self.attachments { + attachments.push(attachment); + self + } else { + self.attachments = Some(vec![attachment]); + self + } + } + + pub fn attachments(mut self, attachments: Vec) -> Self { + if let Some(ref mut sattachments) = self.attachments { + let mut attachments = attachments; + sattachments.append(&mut attachments); + self + } else { + self.attachments = Some(attachments); + self + } + } + + pub fn finalize(self) -> Message { + Message { + id: self.id, + typ: PLAINTEXT_TYP.to_owned(), + type_: self.type_, + body: self.body, + to: self.to, + thid: self.thid, + pthid: self.pthid, + from: self.from, + extra_headers: self.extra_headers, + created_time: self.created_time, + expires_time: self.expires_time, + from_prior: self.from_prior, + attachments: self.attachments, + } + } +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + + #[test] + fn message_build_works() { + let message = Message::build( + "example-1".into(), + "example/v1".into(), + json!("example-body"), + ) + .to("did:example:1".into()) + .to_many(vec!["did:example:2".into(), "did:example:3".into()]) + .from("did:example:4".into()) + .thid("example-thread-1".into()) + .pthid("example-parent-thread-1".into()) + .header("example-header-1".into(), json!("example-header-1-value")) + .header("example-header-2".into(), json!("example-header-2-value")) + .created_time(10000) + .expires_time(20000) + .attachment( + Attachment::base64("ZXhhbXBsZQ==".into()) + .id("attachment1".into()) + .finalize(), + ) + .attachments(vec![ + Attachment::json(json!("example")) + .id("attachment2".into()) + .finalize(), + Attachment::json(json!("example")) + .id("attachment3".into()) + .finalize(), + ]) + .finalize(); + + assert_eq!(message.id, "example-1"); + assert_eq!(message.typ, "application/didcomm-plain+json"); + assert_eq!(message.type_, "example/v1"); + assert_eq!(message.body, json!("example-body")); + assert_eq!(message.from, Some("did:example:4".into())); + assert_eq!(message.thid, Some("example-thread-1".into())); + assert_eq!(message.pthid, Some("example-parent-thread-1".into())); + assert_eq!(message.created_time, Some(10000)); + assert_eq!(message.expires_time, Some(20000)); + + assert_eq!( + message.to, + Some(vec![ + "did:example:1".into(), + "did:example:2".into(), + "did:example:3".into() + ]) + ); + + let extra_headers = message.extra_headers; + assert_eq!(extra_headers.len(), 2); + + assert!(extra_headers.contains_key("example-header-1")); + + assert_eq!( + extra_headers[&"example-header-1".to_owned()], + "example-header-1-value" + ); + + assert!(extra_headers.contains_key("example-header-2")); + + assert_eq!( + extra_headers[&"example-header-2".to_owned()], + "example-header-2-value" + ); + + let attachments = message.attachments.expect("attachments is some."); + assert_eq!(attachments.len(), 3); + assert_eq!(attachments[0].id, Some("attachment1".into())); + assert_eq!(attachments[1].id, Some("attachment2".into())); + assert_eq!(attachments[2].id, Some("attachment3".into())); + } +} diff --git a/affinidi-messaging-didcomm/src/message/mod.rs b/affinidi-messaging-didcomm/src/message/mod.rs new file mode 100644 index 0000000..2ec78bd --- /dev/null +++ b/affinidi-messaging-didcomm/src/message/mod.rs @@ -0,0 +1,22 @@ +mod attachment; +mod from_prior; +#[allow(clippy::module_inception)] +mod message; +mod pack_encrypted; +mod pack_plaintext; +mod pack_signed; +mod unpack; + +pub use attachment::{ + Attachment, AttachmentBuilder, AttachmentData, Base64AttachmentData, JsonAttachmentData, + LinksAttachmentData, +}; + +pub use from_prior::FromPrior; + +pub use message::{Message, MessageBuilder}; +pub use pack_encrypted::{MessagingServiceMetadata, PackEncryptedMetadata, PackEncryptedOptions}; +pub use pack_signed::PackSignedMetadata; +pub use unpack::{UnpackMetadata, UnpackOptions}; + +pub(crate) use pack_encrypted::anoncrypt; diff --git a/affinidi-messaging-didcomm/src/message/pack_encrypted/anoncrypt.rs b/affinidi-messaging-didcomm/src/message/pack_encrypted/anoncrypt.rs new file mode 100644 index 0000000..ff766bc --- /dev/null +++ b/affinidi-messaging-didcomm/src/message/pack_encrypted/anoncrypt.rs @@ -0,0 +1,309 @@ +use affinidi_did_resolver_cache_sdk::{document::DocumentExt, DIDCacheClient}; +use askar_crypto::{ + alg::{ + aes::{A256CbcHs512, A256Gcm, A256Kw, AesKey}, + chacha20::{Chacha20Key, XC20P}, + k256::K256KeyPair, + p256::P256KeyPair, + x25519::X25519KeyPair, + }, + kdf::ecdh_es::EcdhEs, +}; + +use crate::{ + algorithms::AnonCryptAlg, + document::{did_or_url, DIDCommVerificationMethodExt}, + error::{err_msg, ErrorKind, Result, ResultContext}, + jwe, + utils::crypto::{AsKnownKeyPair, KnownKeyAlg}, +}; + +pub(crate) async fn anoncrypt<'dr, 'sr>( + to: &str, + did_resolver: &DIDCacheClient, + msg: &[u8], + enc_alg_anon: &AnonCryptAlg, + to_kids_limit: usize, +) -> Result<(String, Vec)> /* (msg, to_kids) */ { + let (to_did, to_kid) = did_or_url(to); + + // TODO: Avoid resolving of same dids multiple times + // Now we resolve separately in authcrypt, anoncrypt and sign + let to_ddoc = match did_resolver.resolve(to_did).await { + Ok(response) => response.doc, + Err(_) => { + return Err(err_msg( + ErrorKind::DIDNotResolved, + "Recipient did not found", + )); + } + }; + + // Initial list of recipient key ids is all key_agreements of recipient did doc + // or one key if url was explicitly provided + let to_kids = to_ddoc.find_key_agreement(to_kid); + + if to_kids.is_empty() { + Err(err_msg( + ErrorKind::DIDUrlNotFound, + "No recipient key agreements found", + ))? + } + if to_kids.len() > to_kids_limit { + Err(err_msg( + ErrorKind::TooManyCryptoOperations, + format!( + "Too many keys in did. Keys limit is '{}' but found '{}' key(s).", + to_kids_limit, + to_kids.len() + ), + ))? + } + + // Resolve materials for recipient keys + let to_keys = to_kids + .into_iter() + .map(|kid| { + to_ddoc + .verification_method + .iter() + .find(|vm| vm.id == kid) + .ok_or_else(|| { + // TODO: support external keys + err_msg( + ErrorKind::Unsupported, + "External keys are unsupported in this version", + ) + }) + }) + .collect::>>()?; + + // Looking for first supported key to determine what key alg to use + let key_alg = to_keys + .iter() + .filter_map(|key| { + if let Some(jwk) = key.get_jwk() { + match key.key_alg(&jwk) { + KnownKeyAlg::Unsupported => None, + alg => Some(alg), + } + } else { + None + } + }) + .next() + .ok_or_else(|| { + err_msg( + ErrorKind::InvalidState, + "No key agreement keys found for recipient", + ) + })?; + + // Keep only keys with determined key alg + let to_keys: Vec<_> = to_keys + .iter() + .filter(|key| { + if let Some(jwk) = key.get_jwk() { + key.key_alg(&jwk) == key_alg + } else { + false + } + }) + .collect(); + + let msg = match key_alg { + KnownKeyAlg::X25519 => { + let _to_keys = to_keys + .iter() + .map(|vm| { + if let Some(jwk) = vm.get_jwk() { + vm.as_x25519(&jwk).map(|k| (&vm.id, k)) + } else { + Err(err_msg( + ErrorKind::NoCompatibleCrypto, + "Couldn't create JWK for x25519", + )) + } + }) + .collect::>>()?; + + let to_keys: Vec<_> = _to_keys + .iter() + .map(|(id, key)| (id.as_str(), key)) + .collect(); + + match enc_alg_anon { + AnonCryptAlg::A256cbcHs512EcdhEsA256kw => jwe::encrypt::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + msg, + jwe::Algorithm::EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + None, + &to_keys, + ) + .context("Unable produce anoncrypt envelope")?, + AnonCryptAlg::Xc20pEcdhEsA256kw => jwe::encrypt::< + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + msg, + jwe::Algorithm::EcdhEsA256kw, + jwe::EncAlgorithm::Xc20P, + None, + &to_keys, + ) + .context("Unable produce anoncrypt envelope")?, + AnonCryptAlg::A256gcmEcdhEsA256kw => jwe::encrypt::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + msg, + jwe::Algorithm::EcdhEsA256kw, + jwe::EncAlgorithm::A256Gcm, + None, + &to_keys, + ) + .context("Unable produce anoncrypt envelope")?, + } + } + KnownKeyAlg::P256 => { + let _to_keys = to_keys + .iter() + .map(|vm| { + if let Some(jwk) = vm.get_jwk() { + vm.as_p256(&jwk).map(|k| (&vm.id, k)) + } else { + Err(err_msg( + ErrorKind::NoCompatibleCrypto, + "Couldn't create JWK for p256", + )) + } + }) + .collect::>>()?; + + let to_keys: Vec<_> = _to_keys + .iter() + .map(|(id, key)| (id.as_str(), key)) + .collect(); + + match enc_alg_anon { + AnonCryptAlg::A256cbcHs512EcdhEsA256kw => jwe::encrypt::< + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + msg, + jwe::Algorithm::EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + None, + &to_keys, + ) + .context("Unable produce anoncrypt envelope")?, + AnonCryptAlg::Xc20pEcdhEsA256kw => jwe::encrypt::< + Chacha20Key, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + msg, + jwe::Algorithm::EcdhEsA256kw, + jwe::EncAlgorithm::Xc20P, + None, + &to_keys, + ) + .context("Unable produce anoncrypt envelope")?, + AnonCryptAlg::A256gcmEcdhEsA256kw => jwe::encrypt::< + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + msg, + jwe::Algorithm::EcdhEsA256kw, + jwe::EncAlgorithm::A256Gcm, + None, + &to_keys, + ) + .context("Unable produce anoncrypt envelope")?, + } + } + KnownKeyAlg::K256 => { + let _to_keys = to_keys + .iter() + .map(|vm| { + if let Some(jwk) = vm.get_jwk() { + vm.as_k256(&jwk).map(|k| (&vm.id, k)) + } else { + Err(err_msg( + ErrorKind::NoCompatibleCrypto, + "Couldn't create JWK for k256", + )) + } + }) + .collect::>>()?; + + let to_keys: Vec<_> = _to_keys + .iter() + .map(|(id, key)| (id.as_str(), key)) + .collect(); + + match enc_alg_anon { + AnonCryptAlg::A256cbcHs512EcdhEsA256kw => jwe::encrypt::< + AesKey, + EcdhEs<'_, K256KeyPair>, + K256KeyPair, + AesKey, + >( + msg, + jwe::Algorithm::EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + None, + &to_keys, + ) + .context("Unable produce anoncrypt envelope")?, + AnonCryptAlg::Xc20pEcdhEsA256kw => jwe::encrypt::< + Chacha20Key, + EcdhEs<'_, K256KeyPair>, + K256KeyPair, + AesKey, + >( + msg, + jwe::Algorithm::EcdhEsA256kw, + jwe::EncAlgorithm::Xc20P, + None, + &to_keys, + ) + .context("Unable produce anoncrypt envelope")?, + AnonCryptAlg::A256gcmEcdhEsA256kw => jwe::encrypt::< + AesKey, + EcdhEs<'_, K256KeyPair>, + K256KeyPair, + AesKey, + >( + msg, + jwe::Algorithm::EcdhEsA256kw, + jwe::EncAlgorithm::A256Gcm, + None, + &to_keys, + ) + .context("Unable produce anoncrypt envelope")?, + } + } + _ => Err(err_msg( + ErrorKind::InvalidState, + "Unsupported recipient key agreement alg", + ))?, + }; + + let to_kids: Vec<_> = to_keys.into_iter().map(|vm| vm.id.to_string()).collect(); + Ok((msg, to_kids)) +} diff --git a/affinidi-messaging-didcomm/src/message/pack_encrypted/authcrypt.rs b/affinidi-messaging-didcomm/src/message/pack_encrypted/authcrypt.rs new file mode 100644 index 0000000..e6ca2c8 --- /dev/null +++ b/affinidi-messaging-didcomm/src/message/pack_encrypted/authcrypt.rs @@ -0,0 +1,454 @@ +use affinidi_did_resolver_cache_sdk::{document::DocumentExt, DIDCacheClient}; +use askar_crypto::{ + alg::{ + aes::{A256CbcHs512, A256Gcm, A256Kw, AesKey}, + chacha20::{Chacha20Key, XC20P}, + k256::K256KeyPair, + p256::P256KeyPair, + x25519::X25519KeyPair, + }, + kdf::{ecdh_1pu::Ecdh1PU, ecdh_es::EcdhEs}, +}; + +use crate::{ + algorithms::{AnonCryptAlg, AuthCryptAlg}, + document::{did_or_url, DIDCommVerificationMethodExt}, + error::{err_msg, ErrorKind, Result, ResultContext}, + jwe, + secrets::SecretsResolver, + utils::crypto::{AsKnownKeyPair, AsKnownKeyPairSecret, KnownKeyAlg}, +}; + +#[allow(clippy::too_many_arguments)] +pub(crate) async fn authcrypt<'sr>( + to: &str, + from: &str, + did_resolver: &DIDCacheClient, + secrets_resolver: &'sr (dyn SecretsResolver + 'sr + Sync), + msg: &[u8], + enc_alg_auth: &AuthCryptAlg, + enc_alg_anon: &AnonCryptAlg, + protect_sender: bool, + to_kids_limit: usize, +) -> Result<(String, String, Vec)> /* (msg, from_kid, to_kids) */ { + let (to_did, to_kid) = did_or_url(to); + + // TODO: Avoid resolving of same dids multiple times + // Now we resolve separately in authcrypt, anoncrypt and sign + let to_ddoc = match did_resolver.resolve(to_did).await { + Ok(response) => response.doc, + Err(_) => { + return Err(err_msg( + ErrorKind::DIDNotResolved, + "Recipient did not found", + )); + } + }; + + let (from_did, from_kid) = did_or_url(from); + + let from_ddoc = match did_resolver.resolve(from_did).await { + Ok(response) => response.doc, + Err(_) => { + return Err(err_msg(ErrorKind::DIDNotResolved, "Sender did not found")); + } + }; + + // Initial list of sender keys is all key_agreements of sender did doc + // or filtered to keep only provided key + let from_kids = from_ddoc.find_key_agreement(from_kid); + + if from_kids.is_empty() { + Err(err_msg( + ErrorKind::DIDUrlNotFound, + "No sender key agreements found", + ))? + } + + // Keep only sender keys present in the wallet + let from_kids = secrets_resolver + .find_secrets(&from_kids) + .await + .context("Unable find secrets")?; + + if from_kids.is_empty() { + Err(err_msg( + ErrorKind::SecretNotFound, + "No sender secrets found", + ))? + } + + // Resolve materials for sender keys + let from_keys = from_kids + .into_iter() + .map(|kid| { + from_ddoc + .verification_method + .iter() + .find(|vm| vm.id == kid) + .ok_or_else(|| { + // TODO: support external keys + err_msg( + ErrorKind::Malformed, + format!( + "No verification material found for sender key agreement {}", + kid + ), + ) + }) + }) + .collect::>>()?; + + // Initial list of recipient keys is all key_agreements of recipient did doc + // or filtered to keep only provided key + let to_kids = to_ddoc.find_key_agreement(to_kid); + + if to_kids.is_empty() { + Err(err_msg( + ErrorKind::DIDUrlNotFound, + "No recipient key agreements found", + ))? + } + + if to_kids.len() > to_kids_limit { + Err(err_msg( + ErrorKind::TooManyCryptoOperations, + format!( + "Too many keys in did. Keys limit is '{}' but found '{}' key(s).", + to_kids_limit, + to_kids.len() + ), + ))? + } + + // Resolve materials for recipient keys + let to_keys = to_kids + .into_iter() + .map(|kid| { + to_ddoc + .verification_method + .iter() + .find(|vm| vm.id == kid) + .ok_or_else(|| { + // TODO: support external keys + err_msg( + ErrorKind::Malformed, + format!( + "No verification material found for recipient key agreement {}", + kid + ), + ) + }) + }) + .collect::>>()?; + + // Looking for first sender key that has supported crypto and intersects with recipient keys + // by key alg + let from_key = from_keys + .iter() + .filter(|key| { + if let Some(jwk) = key.get_jwk() { + key.key_alg(&jwk) != KnownKeyAlg::Unsupported + } else { + false + } + }) + .find(|from_key| { + if let Some(from_jwk) = from_key.get_jwk() { + to_keys.iter().any(|to_key| { + if let Some(to_jwk) = to_key.get_jwk() { + to_key.key_alg(&to_jwk) == from_key.key_alg(&from_jwk) + } else { + false + } + }) + } else { + false + } + }) + .copied() + .ok_or_else(|| { + err_msg( + ErrorKind::NoCompatibleCrypto, + "No common keys between sender and recipient found", + ) + })?; + + // Resolve secret for found sender key + let from_priv_key = secrets_resolver + .get_secret(&from_key.id) + .await + .context("Unable resolve sender secret")? + .ok_or_else(|| err_msg(ErrorKind::InvalidState, "Sender secret not found"))?; + + let from_jwk = from_key.get_jwk().unwrap(); + let key_alg = from_key.key_alg(&from_jwk); + + // Keep only recipient keys compatible with sender key + let to_keys: Vec<_> = to_keys + .into_iter() + .filter(|key| { + if let Some(jwk) = key.get_jwk() { + key.key_alg(&jwk) == key_alg + } else { + false + } + }) + .collect(); + + let msg = match key_alg { + KnownKeyAlg::X25519 => { + let _to_keys = to_keys + .iter() + .map(|vm| { + if let Some(jwk) = vm.get_jwk() { + vm.as_x25519(&jwk).map(|k| (&vm.id, k)) + } else { + Err(err_msg( + ErrorKind::NoCompatibleCrypto, + "Couldn't create JWK for x25519", + )) + } + }) + .collect::>>()?; + + let to_keys: Vec<_> = _to_keys + .iter() + .map(|(id, key)| (id.as_str(), key)) + .collect(); + + let msg = match enc_alg_auth { + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw => jwe::encrypt::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + msg, + jwe::Algorithm::Ecdh1puA256kw, + jwe::EncAlgorithm::A256cbcHs512, + Some((&from_key.id, &from_priv_key.as_x25519()?)), + &to_keys, + ) + .context("Unable produce authcrypt envelope")?, + }; + + if protect_sender { + match enc_alg_anon { + AnonCryptAlg::A256cbcHs512EcdhEsA256kw => jwe::encrypt::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + msg.as_bytes(), + jwe::Algorithm::EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + None, + &to_keys, + ) + .context("Unable produce authcrypt envelope")?, + AnonCryptAlg::Xc20pEcdhEsA256kw => jwe::encrypt::< + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + msg.as_bytes(), + jwe::Algorithm::EcdhEsA256kw, + jwe::EncAlgorithm::Xc20P, + None, + &to_keys, + ) + .context("Unable produce authcrypt envelope")?, + AnonCryptAlg::A256gcmEcdhEsA256kw => jwe::encrypt::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + msg.as_bytes(), + jwe::Algorithm::EcdhEsA256kw, + jwe::EncAlgorithm::A256Gcm, + None, + &to_keys, + ) + .context("Unable produce authcrypt envelope")?, + } + } else { + msg + } + } + KnownKeyAlg::P256 => { + let _to_keys = to_keys + .iter() + .map(|vm| { + if let Some(jwk) = vm.get_jwk() { + vm.as_p256(&jwk).map(|k| (&vm.id, k)) + } else { + Err(err_msg( + ErrorKind::NoCompatibleCrypto, + "Couldn't create JWK for p_256", + )) + } + }) + .collect::>>()?; + + let to_keys: Vec<_> = _to_keys + .iter() + .map(|(id, key)| (id.as_str(), key)) + .collect(); + + let msg = match enc_alg_auth { + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw => jwe::encrypt::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + msg, + jwe::Algorithm::Ecdh1puA256kw, + jwe::EncAlgorithm::A256cbcHs512, + Some((&from_key.id, &from_priv_key.as_p256()?)), + &to_keys, + ) + .context("Unable produce authcrypt envelope")?, + }; + + if protect_sender { + match enc_alg_anon { + AnonCryptAlg::A256cbcHs512EcdhEsA256kw => jwe::encrypt::< + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + msg.as_bytes(), + jwe::Algorithm::EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + None, + &to_keys, + ) + .context("Unable produce authcrypt envelope")?, + AnonCryptAlg::Xc20pEcdhEsA256kw => jwe::encrypt::< + Chacha20Key, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + msg.as_bytes(), + jwe::Algorithm::EcdhEsA256kw, + jwe::EncAlgorithm::Xc20P, + None, + &to_keys, + ) + .context("Unable produce authcrypt envelope")?, + AnonCryptAlg::A256gcmEcdhEsA256kw => jwe::encrypt::< + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + msg.as_bytes(), + jwe::Algorithm::EcdhEsA256kw, + jwe::EncAlgorithm::A256Gcm, + None, + &to_keys, + ) + .context("Unable produce authcrypt envelope")?, + } + } else { + msg + } + } + KnownKeyAlg::K256 => { + let _to_keys = to_keys + .iter() + .map(|vm| { + if let Some(jwk) = vm.get_jwk() { + vm.as_k256(&jwk).map(|k| (&vm.id, k)) + } else { + Err(err_msg( + ErrorKind::NoCompatibleCrypto, + "Couldn't create JWK for k_256", + )) + } + }) + .collect::>>()?; + + let to_keys: Vec<_> = _to_keys + .iter() + .map(|(id, key)| (id.as_str(), key)) + .collect(); + + let msg = match enc_alg_auth { + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw => jwe::encrypt::< + AesKey, + Ecdh1PU<'_, K256KeyPair>, + K256KeyPair, + AesKey, + >( + msg, + jwe::Algorithm::Ecdh1puA256kw, + jwe::EncAlgorithm::A256cbcHs512, + Some((&from_key.id, &from_priv_key.as_k256()?)), + &to_keys, + ) + .context("Unable produce authcrypt envelope")?, + }; + + if protect_sender { + match enc_alg_anon { + AnonCryptAlg::A256cbcHs512EcdhEsA256kw => jwe::encrypt::< + AesKey, + EcdhEs<'_, K256KeyPair>, + K256KeyPair, + AesKey, + >( + msg.as_bytes(), + jwe::Algorithm::EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + None, + &to_keys, + ) + .context("Unable produce authcrypt envelope")?, + AnonCryptAlg::Xc20pEcdhEsA256kw => jwe::encrypt::< + Chacha20Key, + EcdhEs<'_, K256KeyPair>, + K256KeyPair, + AesKey, + >( + msg.as_bytes(), + jwe::Algorithm::EcdhEsA256kw, + jwe::EncAlgorithm::Xc20P, + None, + &to_keys, + ) + .context("Unable produce authcrypt envelope")?, + AnonCryptAlg::A256gcmEcdhEsA256kw => jwe::encrypt::< + AesKey, + EcdhEs<'_, K256KeyPair>, + K256KeyPair, + AesKey, + >( + msg.as_bytes(), + jwe::Algorithm::EcdhEsA256kw, + jwe::EncAlgorithm::A256Gcm, + None, + &to_keys, + ) + .context("Unable produce authcrypt envelope")?, + } + } else { + msg + } + } + _ => Err(err_msg( + ErrorKind::Unsupported, + "Unsupported recipient key agreement method", + ))?, + }; + + let to_kids: Vec<_> = to_keys.into_iter().map(|vm| vm.id.to_string()).collect(); + Ok((msg, from_key.id.to_string(), to_kids)) +} diff --git a/affinidi-messaging-didcomm/src/message/pack_encrypted/mod.rs b/affinidi-messaging-didcomm/src/message/pack_encrypted/mod.rs new file mode 100644 index 0000000..fcf4e43 --- /dev/null +++ b/affinidi-messaging-didcomm/src/message/pack_encrypted/mod.rs @@ -0,0 +1,3034 @@ +mod anoncrypt; +mod authcrypt; + +use affinidi_did_resolver_cache_sdk::DIDCacheClient; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; + +use crate::{ + algorithms::{AnonCryptAlg, AuthCryptAlg}, + document::{did_or_url, is_did}, + error::{err_msg, ErrorKind, Result, ResultContext}, + protocols::routing::wrap_in_forward_if_needed, + secrets::SecretsResolver, + Message, PackSignedMetadata, +}; + +pub(crate) use self::anoncrypt::anoncrypt; + +use self::authcrypt::authcrypt; + +impl Message { + /// Produces `DIDComm Encrypted Message` + /// https://identity.foundation/didcomm-messaging/spec/#didcomm-encrypted-message. + /// + /// A DIDComm encrypted message is an encrypted JWM (JSON Web Messages) and + /// hides its content from all but authorized recipients, discloses (optionally) and proves + /// the sender to exactly and only those recipients, and provides integrity guarantees. + /// It is important in privacy-preserving routing. It is what normally moves over network + /// transports in DIDComm applications, and is the safest format for storing DIDComm data at rest. + /// + /// Encryption is done as following: + /// - Encryption is done via the keys from the `keyAgreement` verification relationship in the DID Doc + /// - if `to` is a DID, then multiplex encryption is done for all keys from the + /// receiver's `keyAgreement` verification relationship + /// which are compatible the sender's key. + /// - if `to` is a key ID, then encryption is done for the receiver's `keyAgreement` + /// verification method identified by the given key ID. + /// - if `from` is a DID, then sender `keyAgreement` will be negotiated based on recipient preference and + /// sender-recipient crypto compatibility. + /// - if `from` is a key ID, then the sender's `keyAgreement` verification method + /// identified by the given key ID is used. + /// - if `from` is None, then anonymous encryption is done and there will be no sender authentication property. + /// + /// It's possible to add non-repudiation by providing `sign_by` parameter. + /// + /// # Params + /// - `to` recipient DID or key ID the sender uses encryption. + /// - `from` a sender DID or key ID. If set message will be repudiable authenticated or anonymous otherwise. + /// Must match `from` header in Plaintext if the header is set. + /// - `sign_by` if `Some` message will be additionally signed to provide additional non-repudiable authentication + /// by provided DID/Key. Signed messages are only necessary when the origin of plaintext must be provable + /// to third parties, or when the sender can’t be proven to the recipient by authenticated encryption because + /// the recipient is not known in advance (e.g., in a broadcast scenario). + /// Adding a signature when one is not needed can degrade rather than enhance security because + /// it relinquishes the sender’s ability to speak off the record. + /// - `did_resolver` instance of `DIDResolver` to resolve DIDs. + /// - `secrets_resolver` instance of SecretsResolver` to resolve sender DID keys secrets. + /// - `options` allow fine configuration of packing process and have implemented `Default`. + /// + /// # Returns + /// Tuple `(encrypted_message, metadata)`. + /// - `encrypted_message` A DIDComm encrypted message as a JSON string. + /// - `metadata` additional metadata about this `pack` execution like used keys identifiers, + /// used messaging service. + /// + /// # Errors + /// - `DIDNotResolved` Sender or recipient DID not found. + /// - `DIDUrlNotFound` DID doesn't contain mentioned DID Urls (for ex., key id) + /// - `SecretNotFound` Sender secret is not found. + /// - `NoCompatibleCrypto` No compatible keys are found between sender and recipient. + /// - `Unsupported` Used crypto or method is unsupported. + /// - `InvalidState` Indicates library error. + /// - `IOError` IO error during DID or secrets resolving + /// + /// TODO: verify and update errors list + pub async fn pack_encrypted<'sr>( + &self, + to: &str, + from: Option<&str>, + sign_by: Option<&str>, + did_resolver: &DIDCacheClient, + secrets_resolver: &'sr (dyn SecretsResolver + 'sr + Sync), + options: &PackEncryptedOptions, + ) -> Result<(String, PackEncryptedMetadata)> { + self._validate_pack_encrypted(to, from, sign_by)?; + // TODO: Think how to avoid resolving of did multiple times + // and perform async operations in parallel + + // TODO: + // 1. Extract JWE-related steps to a separate method, so that pack_encrypted uses + // the extarcted method for JWE steps and wrap_in_forward_if_needed for Routing steps. + // 2. Make anoncrypt/authcrypt separate non-public modules (not sub-modules), so that + // both pack_encrypted and Routing implementation use them (to avoid cross dependencies + // between message::pack_encrypted and protocols::routing modules). + + let (msg, sign_by_kid) = if let Some(sign_by) = sign_by { + let (msg, PackSignedMetadata { sign_by_kid }) = self + .pack_signed(sign_by, did_resolver, secrets_resolver) + .await + .context("Unable produce sign envelope")?; + + (msg, Some(sign_by_kid)) + } else { + let msg = self + .pack_plaintext(did_resolver) + .await + .context("Unable produce plaintext")?; + (msg, None) + }; + + let (msg, from_kid, to_kids) = if let Some(from) = from { + let (msg, from_kid, to_kids) = authcrypt( + to, + from, + did_resolver, + secrets_resolver, + msg.as_bytes(), + &options.enc_alg_auth, + &options.enc_alg_anon, + options.protect_sender, + options.to_kids_limit, + ) + .await?; + + (msg, Some(from_kid), to_kids) + } else { + let (msg, to_kids) = anoncrypt( + to, + did_resolver, + msg.as_bytes(), + &options.enc_alg_anon, + options.to_kids_limit, + ) + .await?; + + (msg, None, to_kids) + }; + + let (msg, messaging_service) = + match wrap_in_forward_if_needed(&msg, to, did_resolver, options).await? { + Some((forward_msg, messaging_service)) => (forward_msg, Some(messaging_service)), + None => (msg, None), + }; + + let metadata = PackEncryptedMetadata { + messaging_service, + from_kid, + sign_by_kid, + to_kids, + }; + + Ok((msg, metadata)) + } + + fn _validate_pack_encrypted( + &self, + to: &str, + from: Option<&str>, + sign_by: Option<&str>, + ) -> Result<()> { + if !is_did(to) { + Err(err_msg( + ErrorKind::IllegalArgument, + "`to` value is not a valid DID or DID URL", + ))?; + } + + match from { + Some(from) if !is_did(from) => Err(err_msg( + ErrorKind::IllegalArgument, + "`from` value is not a valid DID or DID URL", + ))?, + _ => {} + } + + match sign_by { + Some(sign_by) if !is_did(sign_by) => Err(err_msg( + ErrorKind::IllegalArgument, + "`sign_from` value is not a valid DID or DID URL", + ))?, + _ => {} + } + + let (to_did, _) = did_or_url(to); + + match self.to { + Some(ref sto) if !sto.contains(&to_did.into()) => { + Err(err_msg( + ErrorKind::IllegalArgument, + "`message.to` value does not contain `to` value's DID", + ))?; + } + _ => {} + } + + match (from, &self.from) { + (Some(from), Some(ref sfrom)) if did_or_url(from).0 != sfrom => Err(err_msg( + ErrorKind::IllegalArgument, + "`message.from` value is not equal to `from` value's DID", + ))?, + _ => {} + } + + Ok(()) + } +} + +/// Allow fine configuration of packing process. +#[derive(Debug, PartialEq, Eq, Deserialize, Clone)] +pub struct PackEncryptedOptions { + /// If `true` and message is authenticated than information about sender will be protected from mediators, but + /// additional re-encryption will be required. For anonymous messages this property will be ignored. + #[serde(default)] + pub protect_sender: bool, + + /// Whether the encrypted messages need to be wrapped into `Forward` messages to be sent to Mediators + /// as defined by the `Forward` protocol. + #[serde(default = "crate::utils::serde::_true")] + pub forward: bool, + + /// if forward is enabled these optional headers can be passed to the wrapping `Forward` messages. + /// If forward is disabled this property will be ignored. + pub forward_headers: Option>, + + /// Identifier (DID URL) of messaging service (https://identity.foundation/didcomm-messaging/spec/#did-document-service-endpoint). + /// If DID doc contains multiple messaging services it allows specify what service to use. + /// If not present first service will be used. + pub messaging_service: Option, + + /// Algorithm used for authenticated encryption + #[serde(default)] + pub enc_alg_auth: AuthCryptAlg, + + /// Algorithm used for anonymous encryption + #[serde(default)] + pub enc_alg_anon: AnonCryptAlg, + + /// limit amount of to_kids for message packing. When limit is reached the error is thrown. + pub to_kids_limit: usize, +} + +impl Default for PackEncryptedOptions { + fn default() -> Self { + PackEncryptedOptions { + protect_sender: false, + forward: true, + forward_headers: None, + messaging_service: None, + enc_alg_auth: AuthCryptAlg::default(), + enc_alg_anon: AnonCryptAlg::default(), + to_kids_limit: 100, + } + } +} + +/// Additional metadata about this `encrypt` method execution like used keys identifiers, +/// used messaging service. +#[derive(Debug, PartialEq, Eq, Clone, Serialize)] +pub struct PackEncryptedMetadata { + /// Information about messaging service used for message preparation. + /// Practically `service_endpoint` field can be used to transport the message. + pub messaging_service: Option, + + /// Identifier (DID URL) of sender key used for message encryption. + pub from_kid: Option, + + /// Identifier (DID URL) of sender key used for message sign. + pub sign_by_kid: Option, + + /// Identifiers (DID URLs) of recipient keys used for message encryption. + pub to_kids: Vec, +} + +/// Information about messaging service used for message preparation. +/// Practically `service_endpoint` field can be used to transport the message. +#[derive(Debug, PartialEq, Eq, Clone, Serialize)] +pub struct MessagingServiceMetadata { + /// Identifier (DID URL) of used messaging service. + pub id: String, + + /// Service endpoint of used messaging service. + pub service_endpoint: String, +} + +/* +#[cfg(test)] +mod tests { + use base64::prelude::*; + use ssi::did::DIDMethods; + use std::{collections::HashMap, iter::FromIterator}; + + use askar_crypto::{ + alg::{ + aes::{A256CbcHs512, A256Gcm, A256Kw, AesKey}, + chacha20::{Chacha20Key, XC20P}, + ed25519::Ed25519KeyPair, + k256::K256KeyPair, + p256::P256KeyPair, + x25519::X25519KeyPair, + }, + encrypt::KeyAeadInPlace, + kdf::{ecdh_1pu::Ecdh1PU, ecdh_es::EcdhEs, FromKeyDerivation, KeyExchange}, + repr::{KeyGen, KeySecretBytes}, + sign::KeySigVerify, + }; + + use serde_json::{json, Value}; + + use crate::{ + algorithms::AnonCryptAlg, + did::{resolvers::ExampleDIDResolver, VerificationMaterial, VerificationMethod}, + error::ErrorKind, + jwe, + jwk::{FromJwkValue, ToJwkValue}, + jws, + message::MessagingServiceMetadata, + protocols::routing::{try_parse_forward, wrap_in_forward}, + secrets::{resolvers::ExampleSecretsResolver, Secret, SecretMaterial}, + test_vectors::{ + ALICE_AUTH_METHOD_25519, ALICE_AUTH_METHOD_P256, ALICE_AUTH_METHOD_SECPP256K1, + ALICE_DID, ALICE_DID_DOC, ALICE_DID_DOC_WITH_NO_SECRETS, ALICE_SECRETS, + ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256, ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519, + BOB_DID, BOB_DID_COMM_MESSAGING_SERVICE, BOB_DID_DOC, BOB_DID_DOC_NO_SECRETS, + BOB_SECRETS, BOB_SECRET_KEY_AGREEMENT_KEY_P256_1, BOB_SECRET_KEY_AGREEMENT_KEY_P256_2, + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1, BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2, + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3, BOB_SERVICE, CHARLIE_DID, CHARLIE_DID_DOC, + CHARLIE_ROTATED_TO_ALICE_SECRETS, CHARLIE_SECRETS, CHARLIE_SECRET_AUTH_KEY_ED25519, + CHARLIE_SECRET_KEY_AGREEMENT_KEY_X25519, CHARLIE_SERVICE, FROM_PRIOR_FULL, + MEDIATOR1_DID_DOC, MEDIATOR1_SECRETS, MEDIATOR2_DID_DOC, MEDIATOR2_SECRETS, + MEDIATOR2_VERIFICATION_METHOD_KEY_AGREEM_X25519_1, + MEDIATOR3_DID_COMM_MESSAGING_SERVICE, MEDIATOR3_DID_DOC, MEDIATOR3_SECRETS, + MESSAGE_FROM_PRIOR_FULL, MESSAGE_SIMPLE, PLAINTEXT_MSG_SIMPLE, + }, + utils::{ + crypto::{JoseKDF, KeyWrap}, + did::did_or_url, + }, + Message, PackEncryptedMetadata, PackEncryptedOptions, UnpackOptions, + }; + + #[tokio::test] + async fn pack_encrypted_works_authcrypt() { + _pack_encrypted_works_authcrypt::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3, + ], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519, + ) + .await; + + _pack_encrypted_works_authcrypt::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519, + ) + .await; + + _pack_encrypted_works_authcrypt::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519, + ) + .await; + + _pack_encrypted_works_authcrypt::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2, + ], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256, + ) + .await; + + _pack_encrypted_works_authcrypt::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_P256_1], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256, + ) + .await; + + _pack_encrypted_works_authcrypt::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_P256_2], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256, + ) + .await; + + _pack_encrypted_works_authcrypt::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2, + ], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256, + ) + .await; + + _pack_encrypted_works_authcrypt::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_P256_1], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256, + ) + .await; + + async fn _pack_encrypted_works_authcrypt( + to: &str, + to_keys: Vec<&Secret>, + from: &str, + from_key: &VerificationMethod, + ) where + CE: KeyAeadInPlace + KeySecretBytes, + KDF: JoseKDF, + KE: KeyExchange + KeyGen + ToJwkValue + FromJwkValue, + KW: KeyWrap + FromKeyDerivation, + { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let (msg, metadata) = MESSAGE_SIMPLE + .pack_encrypted( + to, + Some(from), + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await + .expect("encrypt is ok."); + + assert_eq!( + metadata, + PackEncryptedMetadata { + messaging_service: None, + from_kid: Some(from_key.id.clone()), + sign_by_kid: None, + to_kids: to_keys.iter().map(|s| s.id.clone()).collect::>(), + } + ); + + let msg = _verify_authcrypt::(&msg, to_keys, from_key); + _verify_plaintext(&msg, PLAINTEXT_MSG_SIMPLE); + } + } + + #[tokio::test] + async fn pack_encrypted_works_authcrypt_protected_sender() { + _pack_encrypted_works_authcrypt_protected_sender::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3, + ], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + ) + .await; + + _pack_encrypted_works_authcrypt_protected_sender::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3, + ], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519, + AnonCryptAlg::A256gcmEcdhEsA256kw, + jwe::EncAlgorithm::A256Gcm, + ) + .await; + + _pack_encrypted_works_authcrypt_protected_sender::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3, + ], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519, + AnonCryptAlg::Xc20pEcdhEsA256kw, + jwe::EncAlgorithm::Xc20P, + ) + .await; + + _pack_encrypted_works_authcrypt_protected_sender::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + ) + .await; + + _pack_encrypted_works_authcrypt_protected_sender::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + ) + .await; + + _pack_encrypted_works_authcrypt_protected_sender::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2, + ], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + ) + .await; + + _pack_encrypted_works_authcrypt_protected_sender::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2, + ], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256, + AnonCryptAlg::A256gcmEcdhEsA256kw, + jwe::EncAlgorithm::A256Gcm, + ) + .await; + + _pack_encrypted_works_authcrypt_protected_sender::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + Chacha20Key, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2, + ], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256, + AnonCryptAlg::Xc20pEcdhEsA256kw, + jwe::EncAlgorithm::Xc20P, + ) + .await; + + _pack_encrypted_works_authcrypt_protected_sender::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_P256_1], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + ) + .await; + + _pack_encrypted_works_authcrypt_protected_sender::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_P256_2], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + ) + .await; + + _pack_encrypted_works_authcrypt_protected_sender::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2, + ], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + ) + .await; + + _pack_encrypted_works_authcrypt_protected_sender::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2, + ], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + ) + .await; + + async fn _pack_encrypted_works_authcrypt_protected_sender< + CE, + KDF, + KE, + KW, + ACE, + AKDF, + AKE, + AKW, + >( + to: &str, + to_keys: Vec<&Secret>, + from: &str, + from_key: &VerificationMethod, + enc_alg_anon: AnonCryptAlg, + enc_alg_anon_jwe: jwe::EncAlgorithm, + ) where + CE: KeyAeadInPlace + KeySecretBytes, + KDF: JoseKDF, + KE: KeyExchange + KeyGen + ToJwkValue + FromJwkValue, + KW: KeyWrap + FromKeyDerivation, + ACE: KeyAeadInPlace + KeySecretBytes, + AKDF: JoseKDF, + AKE: KeyExchange + KeyGen + ToJwkValue + FromJwkValue, + AKW: KeyWrap + FromKeyDerivation, + { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let (msg, metadata) = MESSAGE_SIMPLE + .pack_encrypted( + to, + Some(from), + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon, + ..PackEncryptedOptions::default() + }, + ) + .await + .expect("encrypt is ok."); + + assert_eq!( + metadata, + PackEncryptedMetadata { + messaging_service: None, + from_kid: Some(from_key.id.clone()), + sign_by_kid: None, + to_kids: to_keys.iter().map(|s| s.id.clone()).collect::>(), + } + ); + + let msg = + _verify_anoncrypt::(&msg, to_keys.clone(), enc_alg_anon_jwe); + let msg = _verify_authcrypt::(&msg, to_keys, from_key); + _verify_plaintext(&msg, PLAINTEXT_MSG_SIMPLE); + } + } + + #[tokio::test] + async fn pack_encrypted_works_authcrypt_protected_sender_signed() { + _pack_encrypted_works_authcrypt_protected_sender_signed::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + Ed25519KeyPair, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3, + ], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + ALICE_DID, + &ALICE_AUTH_METHOD_25519, + jws::Algorithm::EdDSA, + ) + .await; + + _pack_encrypted_works_authcrypt_protected_sender_signed::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + P256KeyPair, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2, + ], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + &ALICE_AUTH_METHOD_P256.id, + &ALICE_AUTH_METHOD_P256, + jws::Algorithm::Es256, + ) + .await; + + _pack_encrypted_works_authcrypt_protected_sender_signed::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + K256KeyPair, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2, + ], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + &ALICE_AUTH_METHOD_SECPP256K1.id, + &ALICE_AUTH_METHOD_SECPP256K1, + jws::Algorithm::Es256K, + ) + .await; + + #[allow(clippy::too_many_arguments)] + async fn _pack_encrypted_works_authcrypt_protected_sender_signed< + CE, + KDF, + KE, + KW, + ACE, + AKDF, + AKE, + AKW, + SK, + >( + to: &str, + to_keys: Vec<&Secret>, + from: &str, + from_key: &VerificationMethod, + enc_alg_anon: AnonCryptAlg, + enc_alg_anon_jwe: jwe::EncAlgorithm, + sign_by: &str, + sign_by_key: &VerificationMethod, + sign_alg: jws::Algorithm, + ) where + CE: KeyAeadInPlace + KeySecretBytes, + KDF: JoseKDF, + KE: KeyExchange + KeyGen + ToJwkValue + FromJwkValue, + KW: KeyWrap + FromKeyDerivation, + ACE: KeyAeadInPlace + KeySecretBytes, + AKDF: JoseKDF, + AKE: KeyExchange + KeyGen + ToJwkValue + FromJwkValue, + AKW: KeyWrap + FromKeyDerivation, + SK: KeySigVerify + FromJwkValue, + { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let (msg, metadata) = MESSAGE_SIMPLE + .pack_encrypted( + to, + Some(from), + Some(sign_by), + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon, + ..PackEncryptedOptions::default() + }, + ) + .await + .expect("encrypt is ok."); + + assert_eq!( + metadata, + PackEncryptedMetadata { + messaging_service: None, + from_kid: Some(from_key.id.clone()), + sign_by_kid: Some(sign_by_key.id.clone()), + to_kids: to_keys.iter().map(|s| s.id.clone()).collect::>(), + } + ); + + let msg = + _verify_anoncrypt::(&msg, to_keys.clone(), enc_alg_anon_jwe); + let msg = _verify_authcrypt::(&msg, to_keys, from_key); + let msg = _verify_signed::(&msg, sign_by_key, sign_alg); + _verify_plaintext(&msg, PLAINTEXT_MSG_SIMPLE); + } + } + + #[tokio::test] + async fn pack_encrypted_works_authcrypt_sign() { + _pack_encrypted_works_authcrypt_sign::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + Ed25519KeyPair, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3, + ], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519, + ALICE_DID, + &ALICE_AUTH_METHOD_25519, + jws::Algorithm::EdDSA, + ) + .await; + + _pack_encrypted_works_authcrypt_sign::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + Ed25519KeyPair, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519, + &ALICE_AUTH_METHOD_25519.id, + &ALICE_AUTH_METHOD_25519, + jws::Algorithm::EdDSA, + ) + .await; + + _pack_encrypted_works_authcrypt_sign::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + P256KeyPair, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519, + &ALICE_AUTH_METHOD_P256.id, + &ALICE_AUTH_METHOD_P256, + jws::Algorithm::Es256, + ) + .await; + + _pack_encrypted_works_authcrypt_sign::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + K256KeyPair, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2, + ], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256, + &ALICE_AUTH_METHOD_SECPP256K1.id, + &ALICE_AUTH_METHOD_SECPP256K1, + jws::Algorithm::Es256K, + ) + .await; + + async fn _pack_encrypted_works_authcrypt_sign( + to: &str, + to_keys: Vec<&Secret>, + from: &str, + from_key: &VerificationMethod, + sign_by: &str, + sign_by_key: &VerificationMethod, + sign_alg: jws::Algorithm, + ) where + CE: KeyAeadInPlace + KeySecretBytes, + KDF: JoseKDF, + KE: KeyExchange + KeyGen + ToJwkValue + FromJwkValue, + KW: KeyWrap + FromKeyDerivation, + SK: KeySigVerify + FromJwkValue, + { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let (msg, metadata) = MESSAGE_SIMPLE + .pack_encrypted( + to, + Some(from), + Some(sign_by), + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await + .expect("encrypt is ok."); + + assert_eq!( + metadata, + PackEncryptedMetadata { + messaging_service: None, + from_kid: Some(from_key.id.clone()), + sign_by_kid: Some(sign_by_key.id.clone()), + to_kids: to_keys.iter().map(|s| s.id.clone()).collect::>(), + } + ); + + let msg = _verify_authcrypt::(&msg, to_keys, from_key); + let msg = _verify_signed::(&msg, sign_by_key, sign_alg); + _verify_plaintext(&msg, PLAINTEXT_MSG_SIMPLE); + } + } + + #[tokio::test] + async fn pack_encrypted_works_anoncrypt() { + _pack_encrypted_works_anoncrypt::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3, + ], + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + ) + .await; + + _pack_encrypted_works_anoncrypt::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3, + ], + AnonCryptAlg::A256gcmEcdhEsA256kw, + jwe::EncAlgorithm::A256Gcm, + ) + .await; + + _pack_encrypted_works_anoncrypt::< + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3, + ], + AnonCryptAlg::Xc20pEcdhEsA256kw, + jwe::EncAlgorithm::Xc20P, + ) + .await; + + _pack_encrypted_works_anoncrypt::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2], + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + ) + .await; + + _pack_encrypted_works_anoncrypt::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2], + AnonCryptAlg::A256gcmEcdhEsA256kw, + jwe::EncAlgorithm::A256Gcm, + ) + .await; + + _pack_encrypted_works_anoncrypt::< + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2], + AnonCryptAlg::Xc20pEcdhEsA256kw, + jwe::EncAlgorithm::Xc20P, + ) + .await; + + _pack_encrypted_works_anoncrypt::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2], + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + ) + .await; + + _pack_encrypted_works_anoncrypt::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2], + AnonCryptAlg::A256gcmEcdhEsA256kw, + jwe::EncAlgorithm::A256Gcm, + ) + .await; + + _pack_encrypted_works_anoncrypt::< + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2], + AnonCryptAlg::Xc20pEcdhEsA256kw, + jwe::EncAlgorithm::Xc20P, + ) + .await; + + _pack_encrypted_works_anoncrypt::< + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_P256_1], + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + ) + .await; + + _pack_encrypted_works_anoncrypt::< + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_P256_1], + AnonCryptAlg::A256gcmEcdhEsA256kw, + jwe::EncAlgorithm::A256Gcm, + ) + .await; + + _pack_encrypted_works_anoncrypt::< + Chacha20Key, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_P256_1], + AnonCryptAlg::Xc20pEcdhEsA256kw, + jwe::EncAlgorithm::Xc20P, + ) + .await; + + _pack_encrypted_works_anoncrypt::< + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_P256_2], + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + ) + .await; + + _pack_encrypted_works_anoncrypt::< + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_P256_2], + AnonCryptAlg::A256gcmEcdhEsA256kw, + jwe::EncAlgorithm::A256Gcm, + ) + .await; + + _pack_encrypted_works_anoncrypt::< + Chacha20Key, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_P256_2], + AnonCryptAlg::Xc20pEcdhEsA256kw, + jwe::EncAlgorithm::Xc20P, + ) + .await; + + async fn _pack_encrypted_works_anoncrypt( + to: &str, + to_keys: Vec<&Secret>, + enc_alg: AnonCryptAlg, + enc_alg_jwe: jwe::EncAlgorithm, + ) where + CE: KeyAeadInPlace + KeySecretBytes, + KDF: JoseKDF, + KE: KeyExchange + KeyGen + ToJwkValue + FromJwkValue, + KW: KeyWrap + FromKeyDerivation, + { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let (msg, metadata) = MESSAGE_SIMPLE + .pack_encrypted( + to, + None, + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + enc_alg_anon: enc_alg, + ..PackEncryptedOptions::default() + }, + ) + .await + .expect("encrypt is ok."); + + assert_eq!( + metadata, + PackEncryptedMetadata { + messaging_service: None, + from_kid: None, + sign_by_kid: None, + to_kids: to_keys.iter().map(|s| s.id.clone()).collect::>(), + } + ); + + let msg = _verify_anoncrypt::(&msg, to_keys, enc_alg_jwe); + _verify_plaintext(&msg, PLAINTEXT_MSG_SIMPLE); + } + } + + #[tokio::test] + async fn pack_encrypted_works_anoncrypt_sign() { + _pack_encrypted_works_anoncrypt_sign::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + Ed25519KeyPair, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3, + ], + ALICE_DID, + &ALICE_AUTH_METHOD_25519, + jws::Algorithm::EdDSA, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + ) + .await; + + _pack_encrypted_works_anoncrypt_sign::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + Ed25519KeyPair, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3, + ], + ALICE_DID, + &ALICE_AUTH_METHOD_25519, + jws::Algorithm::EdDSA, + AnonCryptAlg::A256gcmEcdhEsA256kw, + jwe::EncAlgorithm::A256Gcm, + ) + .await; + + _pack_encrypted_works_anoncrypt_sign::< + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + Ed25519KeyPair, + >( + BOB_DID, + vec![ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3, + ], + ALICE_DID, + &ALICE_AUTH_METHOD_25519, + jws::Algorithm::EdDSA, + AnonCryptAlg::Xc20pEcdhEsA256kw, + jwe::EncAlgorithm::Xc20P, + ) + .await; + + _pack_encrypted_works_anoncrypt_sign::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + Ed25519KeyPair, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2], + &ALICE_AUTH_METHOD_25519.id, + &ALICE_AUTH_METHOD_25519, + jws::Algorithm::EdDSA, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + ) + .await; + + _pack_encrypted_works_anoncrypt_sign::< + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + P256KeyPair, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_P256_1], + &ALICE_AUTH_METHOD_P256.id, + &ALICE_AUTH_METHOD_P256, + jws::Algorithm::Es256, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + ) + .await; + + _pack_encrypted_works_anoncrypt_sign::< + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + K256KeyPair, + >( + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + vec![&BOB_SECRET_KEY_AGREEMENT_KEY_P256_1], + &ALICE_AUTH_METHOD_SECPP256K1.id, + &ALICE_AUTH_METHOD_SECPP256K1, + jws::Algorithm::Es256K, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + jwe::EncAlgorithm::A256cbcHs512, + ) + .await; + + async fn _pack_encrypted_works_anoncrypt_sign( + to: &str, + to_keys: Vec<&Secret>, + sign_by: &str, + sign_by_key: &VerificationMethod, + sign_alg: jws::Algorithm, + enc_alg: AnonCryptAlg, + enc_alg_jwe: jwe::EncAlgorithm, + ) where + CE: KeyAeadInPlace + KeySecretBytes, + KDF: JoseKDF, + KE: KeyExchange + KeyGen + ToJwkValue + FromJwkValue, + KW: KeyWrap + FromKeyDerivation, + SK: KeySigVerify + FromJwkValue, + { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let (msg, metadata) = MESSAGE_SIMPLE + .pack_encrypted( + to, + None, + Some(sign_by), + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + enc_alg_anon: enc_alg, + ..PackEncryptedOptions::default() + }, + ) + .await + .expect("encrypt is ok."); + + assert_eq!( + metadata, + PackEncryptedMetadata { + messaging_service: None, + from_kid: None, + sign_by_kid: Some(sign_by_key.id.clone()), + to_kids: to_keys.iter().map(|s| s.id.clone()).collect::>(), + } + ); + + let msg = _verify_anoncrypt::(&msg, to_keys, enc_alg_jwe); + let msg = _verify_signed::(&msg, sign_by_key, sign_alg); + _verify_plaintext(&msg, PLAINTEXT_MSG_SIMPLE); + } + } + + #[tokio::test] + async fn pack_encrypted_works_single_mediator() { + _pack_encrypted_works_single_mediator(BOB_DID, None, None).await; + + _pack_encrypted_works_single_mediator(BOB_DID, None, Some(ALICE_DID)).await; + + _pack_encrypted_works_single_mediator(BOB_DID, Some(ALICE_DID), None).await; + + _pack_encrypted_works_single_mediator(BOB_DID, Some(ALICE_DID), Some(ALICE_DID)).await; + + _pack_encrypted_works_single_mediator( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + None, + None, + ) + .await; + + _pack_encrypted_works_single_mediator( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + None, + Some(ALICE_DID), + ) + .await; + + _pack_encrypted_works_single_mediator( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + Some(ALICE_DID), + None, + ) + .await; + + _pack_encrypted_works_single_mediator( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + Some(ALICE_DID), + Some(ALICE_DID), + ) + .await; + + async fn _pack_encrypted_works_single_mediator( + to: &str, + from: Option<&str>, + sign_by: Option<&str>, + ) { + let mut did_resolver = ExampleDIDResolver::new(vec![ + ALICE_DID_DOC.clone(), + BOB_DID_DOC.clone(), + MEDIATOR1_DID_DOC.clone(), + ]); + + let alice_secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let bob_secrets_resolver = ExampleSecretsResolver::new(BOB_SECRETS.clone()); + + let mediator1_secrets_resolver = ExampleSecretsResolver::new(MEDIATOR1_SECRETS.clone()); + + let (msg, pack_metadata) = MESSAGE_SIMPLE + .pack_encrypted( + to, + from, + sign_by, + &did_resolver, + &alice_secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + .expect("Unable encrypt"); + + assert_eq!( + pack_metadata.messaging_service.as_ref(), + Some(&MessagingServiceMetadata { + id: BOB_SERVICE.id.clone(), + service_endpoint: BOB_DID_COMM_MESSAGING_SERVICE.uri.clone(), + }) + ); + + assert_eq!( + pack_metadata.from_kid.map(|k| did_or_url(&k).0.to_owned()), + from.map(|d| d.to_owned()) + ); + assert_eq!( + pack_metadata + .sign_by_kid + .map(|k| did_or_url(&k).0.to_owned()), + sign_by.map(|d| d.to_owned()) + ); + + match did_or_url(to) { + (_, Some(to_kid)) => { + assert_eq!( + pack_metadata + .to_kids + .iter() + .map(|k| k.as_str()) + .collect::>(), + vec![to_kid] + ) + } + (to_did, None) => { + for metadata_to_kid in pack_metadata.to_kids { + assert_eq!(did_or_url(&metadata_to_kid).0, to_did); + } + } + } + + let did_method_resolver = DIDMethods::default(); + let (unpacked_msg_mediator1, unpack_metadata_mediator1) = Message::unpack_string( + &msg, + &mut did_resolver, + &did_method_resolver, + &mediator1_secrets_resolver, + &UnpackOptions::default(), + ) + .await + .expect("Unable unpack"); + + let forward = + try_parse_forward(&unpacked_msg_mediator1).expect("Message is not Forward"); + + assert_eq!(forward.msg, &unpacked_msg_mediator1); + assert_eq!(&forward.next, to); + + assert!(unpack_metadata_mediator1.encrypted); + assert!(!unpack_metadata_mediator1.authenticated); + assert!(!unpack_metadata_mediator1.non_repudiation); + assert!(unpack_metadata_mediator1.anonymous_sender); + assert!(!unpack_metadata_mediator1.re_wrapped_in_forward); + + let forwarded_msg = serde_json::to_string(&forward.forwarded_msg) + .expect("Unable serialize forwarded message"); + + let (unpacked_msg, unpack_metadata) = Message::unpack_string( + &forwarded_msg, + &mut did_resolver, + &did_method_resolver, + &bob_secrets_resolver, + &UnpackOptions::default(), + ) + .await + .expect("Unable unpack"); + + assert_eq!(&unpacked_msg, &*MESSAGE_SIMPLE); + + assert!(unpack_metadata.encrypted); + assert_eq!( + unpack_metadata.authenticated, + from.is_some() || sign_by.is_some() + ); + assert_eq!(unpack_metadata.non_repudiation, sign_by.is_some()); + assert_eq!(unpack_metadata.anonymous_sender, from.is_none()); + assert!(!unpack_metadata.re_wrapped_in_forward); + } + } + + #[tokio::test] + async fn pack_encrypted_works_multiple_mediators_alternative_endpoints() { + _pack_encrypted_works_multiple_mediators_alternative_endpoints(CHARLIE_DID, None, None) + .await; + + _pack_encrypted_works_multiple_mediators_alternative_endpoints( + CHARLIE_DID, + None, + Some(ALICE_DID), + ) + .await; + + _pack_encrypted_works_multiple_mediators_alternative_endpoints( + CHARLIE_DID, + Some(ALICE_DID), + None, + ) + .await; + + _pack_encrypted_works_multiple_mediators_alternative_endpoints( + CHARLIE_DID, + Some(ALICE_DID), + Some(ALICE_DID), + ) + .await; + + _pack_encrypted_works_multiple_mediators_alternative_endpoints( + &CHARLIE_SECRET_KEY_AGREEMENT_KEY_X25519.id, + None, + None, + ) + .await; + + _pack_encrypted_works_multiple_mediators_alternative_endpoints( + &CHARLIE_SECRET_KEY_AGREEMENT_KEY_X25519.id, + None, + Some(ALICE_DID), + ) + .await; + + _pack_encrypted_works_multiple_mediators_alternative_endpoints( + &CHARLIE_SECRET_KEY_AGREEMENT_KEY_X25519.id, + Some(ALICE_DID), + None, + ) + .await; + + _pack_encrypted_works_multiple_mediators_alternative_endpoints( + &CHARLIE_SECRET_KEY_AGREEMENT_KEY_X25519.id, + Some(ALICE_DID), + Some(ALICE_DID), + ) + .await; + + async fn _pack_encrypted_works_multiple_mediators_alternative_endpoints( + to: &str, + from: Option<&str>, + sign_by: Option<&str>, + ) { + let msg = Message::build( + "1234567890".to_owned(), + "http://example.com/protocols/lets_do_lunch/1.0/proposal".to_owned(), + json!({"messagespecificattribute": "and its value"}), + ) + .from(ALICE_DID.to_owned()) + .to(CHARLIE_DID.to_owned()) + .created_time(1516269022) + .expires_time(1516385931) + .finalize(); + + let mut did_resolver = ExampleDIDResolver::new(vec![ + ALICE_DID_DOC.clone(), + CHARLIE_DID_DOC.clone(), + MEDIATOR1_DID_DOC.clone(), + MEDIATOR2_DID_DOC.clone(), + MEDIATOR3_DID_DOC.clone(), + ]); + + let alice_secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let charlie_secrets_resolver = ExampleSecretsResolver::new(CHARLIE_SECRETS.clone()); + + let mediator1_secrets_resolver = ExampleSecretsResolver::new(MEDIATOR1_SECRETS.clone()); + + let mediator2_secrets_resolver = ExampleSecretsResolver::new(MEDIATOR2_SECRETS.clone()); + + let mediator3_secrets_resolver = ExampleSecretsResolver::new(MEDIATOR3_SECRETS.clone()); + + let (packed_msg, pack_metadata) = msg + .pack_encrypted( + to, + from, + sign_by, + &did_resolver, + &alice_secrets_resolver, + &PackEncryptedOptions { + forward_headers: Some(HashMap::from_iter([ + ("example-header-1".into(), json!("example-header-1-value")), + ("example-header-2".into(), json!("example-header-2-value")), + ])), + ..PackEncryptedOptions::default() + }, + ) + .await + .expect("Unable encrypt"); + + assert_eq!( + pack_metadata.messaging_service.as_ref(), + Some(&MessagingServiceMetadata { + id: CHARLIE_SERVICE.id.clone(), + service_endpoint: MEDIATOR3_DID_COMM_MESSAGING_SERVICE.uri.clone(), + }) + ); + + assert_eq!( + pack_metadata.from_kid.map(|k| did_or_url(&k).0.to_owned()), + from.map(|d| d.to_owned()) + ); + assert_eq!( + pack_metadata + .sign_by_kid + .map(|k| did_or_url(&k).0.to_owned()), + sign_by.map(|d| d.to_owned()) + ); + + match did_or_url(to) { + (_, Some(to_kid)) => { + assert_eq!( + pack_metadata + .to_kids + .iter() + .map(|k| k.as_str()) + .collect::>(), + vec![to_kid] + ) + } + (to_did, None) => { + for metadata_to_kid in pack_metadata.to_kids { + assert_eq!(did_or_url(&metadata_to_kid).0, to_did); + } + } + } + + let did_method_resolver = DIDMethods::default(); + let (unpacked_msg_mediator3, unpack_metadata_mediator3) = Message::unpack_string( + &packed_msg, + &mut did_resolver, + &did_method_resolver, + &mediator3_secrets_resolver, + &UnpackOptions::default(), + ) + .await + .expect("Unable unpack"); + + let forward_at_mediator3 = + try_parse_forward(&unpacked_msg_mediator3).expect("Message is not Forward"); + + assert_eq!(forward_at_mediator3.msg, &unpacked_msg_mediator3); + + assert_eq!( + &forward_at_mediator3.msg.extra_headers, + &HashMap::from_iter([ + ("example-header-1".into(), json!("example-header-1-value")), + ("example-header-2".into(), json!("example-header-2-value")), + ]) + ); + + assert_eq!( + &forward_at_mediator3.next, + "did:example:mediator2#key-x25519-1" + ); + + assert!(unpack_metadata_mediator3.encrypted); + assert!(!unpack_metadata_mediator3.authenticated); + assert!(!unpack_metadata_mediator3.non_repudiation); + assert!(unpack_metadata_mediator3.anonymous_sender); + assert!(!unpack_metadata_mediator3.re_wrapped_in_forward); + + let forwarded_msg_at_mediator3 = + serde_json::to_string(&forward_at_mediator3.forwarded_msg) + .expect("Unable serialize forwarded message"); + + let (unpacked_msg_mediator2, unpack_metadata_mediator2) = Message::unpack_string( + &forwarded_msg_at_mediator3, + &mut did_resolver, + &did_method_resolver, + &mediator2_secrets_resolver, + &UnpackOptions::default(), + ) + .await + .expect("Unable unpack"); + + let forward_at_mediator2 = + try_parse_forward(&unpacked_msg_mediator2).expect("Message is not Forward"); + + assert_eq!(forward_at_mediator2.msg, &unpacked_msg_mediator2); + + assert_eq!( + &forward_at_mediator2.msg.extra_headers, + &HashMap::from_iter([ + ("example-header-1".into(), json!("example-header-1-value")), + ("example-header-2".into(), json!("example-header-2-value")), + ]) + ); + + assert_eq!( + &forward_at_mediator2.next, + "did:example:mediator1#key-x25519-1" + ); + + assert!(unpack_metadata_mediator2.encrypted); + assert!(!unpack_metadata_mediator2.authenticated); + assert!(!unpack_metadata_mediator2.non_repudiation); + assert!(unpack_metadata_mediator2.anonymous_sender); + assert!(!unpack_metadata_mediator2.re_wrapped_in_forward); + + let forwarded_msg_at_mediator2 = + serde_json::to_string(&forward_at_mediator2.forwarded_msg) + .expect("Unable serialize forwarded message"); + + let (unpacked_msg_mediator1, unpack_metadata_mediator1) = Message::unpack_string( + &forwarded_msg_at_mediator2, + &mut did_resolver, + &did_method_resolver, + &mediator1_secrets_resolver, + &UnpackOptions::default(), + ) + .await + .expect("Unable unpack"); + + let forward_at_mediator1 = + try_parse_forward(&unpacked_msg_mediator1).expect("Message is not Forward"); + + assert_eq!(forward_at_mediator1.msg, &unpacked_msg_mediator1); + + assert_eq!( + &forward_at_mediator1.msg.extra_headers, + &HashMap::from_iter([ + ("example-header-1".into(), json!("example-header-1-value")), + ("example-header-2".into(), json!("example-header-2-value")), + ]) + ); + + assert_eq!(&forward_at_mediator1.next, to); + + assert!(unpack_metadata_mediator1.encrypted); + assert!(!unpack_metadata_mediator1.authenticated); + assert!(!unpack_metadata_mediator1.non_repudiation); + assert!(unpack_metadata_mediator1.anonymous_sender); + assert!(!unpack_metadata_mediator1.re_wrapped_in_forward); + + let forwarded_msg_at_mediator1 = + serde_json::to_string(&forward_at_mediator1.forwarded_msg) + .expect("Unable serialize forwarded message"); + + let (unpacked_msg, unpack_metadata) = Message::unpack_string( + &forwarded_msg_at_mediator1, + &mut did_resolver, + &did_method_resolver, + &charlie_secrets_resolver, + &UnpackOptions::default(), + ) + .await + .expect("Unable unpack"); + + assert_eq!(&unpacked_msg, &msg); + + assert!(unpack_metadata.encrypted); + assert_eq!( + unpack_metadata.authenticated, + from.is_some() || sign_by.is_some() + ); + assert_eq!(unpack_metadata.non_repudiation, sign_by.is_some()); + assert_eq!(unpack_metadata.anonymous_sender, from.is_none()); + assert!(!unpack_metadata.re_wrapped_in_forward); + } + } + + #[tokio::test] + async fn wrap_in_forward_works_mediator_unknown_to_sender() { + _wrap_in_forward_works_mediator_unknown_to_sender(BOB_DID, None, None).await; + + _wrap_in_forward_works_mediator_unknown_to_sender(BOB_DID, None, Some(ALICE_DID)).await; + + _wrap_in_forward_works_mediator_unknown_to_sender(BOB_DID, Some(ALICE_DID), None).await; + + _wrap_in_forward_works_mediator_unknown_to_sender( + BOB_DID, + Some(ALICE_DID), + Some(ALICE_DID), + ) + .await; + + _wrap_in_forward_works_mediator_unknown_to_sender( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + None, + None, + ) + .await; + + _wrap_in_forward_works_mediator_unknown_to_sender( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + None, + Some(ALICE_DID), + ) + .await; + + _wrap_in_forward_works_mediator_unknown_to_sender( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + Some(ALICE_DID), + None, + ) + .await; + + _wrap_in_forward_works_mediator_unknown_to_sender( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + Some(ALICE_DID), + Some(ALICE_DID), + ) + .await; + + async fn _wrap_in_forward_works_mediator_unknown_to_sender( + to: &str, + from: Option<&str>, + sign_by: Option<&str>, + ) { + let mut did_resolver = ExampleDIDResolver::new(vec![ + ALICE_DID_DOC.clone(), + BOB_DID_DOC.clone(), + MEDIATOR1_DID_DOC.clone(), + MEDIATOR2_DID_DOC.clone(), + ]); + + let alice_secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let bob_secrets_resolver = ExampleSecretsResolver::new(BOB_SECRETS.clone()); + + let mediator1_secrets_resolver = ExampleSecretsResolver::new(MEDIATOR1_SECRETS.clone()); + + let mediator2_secrets_resolver = ExampleSecretsResolver::new(MEDIATOR2_SECRETS.clone()); + + let (msg, pack_metadata) = MESSAGE_SIMPLE + .pack_encrypted( + to, + from, + sign_by, + &did_resolver, + &alice_secrets_resolver, + &PackEncryptedOptions { + messaging_service: Some(BOB_SERVICE.id.clone()), + ..PackEncryptedOptions::default() + }, + ) + .await + .expect("Unable encrypt"); + + assert_eq!( + pack_metadata.messaging_service.as_ref(), + Some(&MessagingServiceMetadata { + id: BOB_SERVICE.id.clone(), + service_endpoint: BOB_DID_COMM_MESSAGING_SERVICE.uri.clone(), + }) + ); + + let did_method_resolver = DIDMethods::default(); + let (unpacked_msg_mediator1, unpack_metadata_mediator1) = Message::unpack_string( + &msg, + &mut did_resolver, + &did_method_resolver, + &mediator1_secrets_resolver, + &UnpackOptions::default(), + ) + .await + .expect("Unable unpack"); + + let forward_at_mediator1 = + try_parse_forward(&unpacked_msg_mediator1).expect("Message is not Forward"); + + assert_eq!(forward_at_mediator1.msg, &unpacked_msg_mediator1); + assert_eq!(&forward_at_mediator1.next, to); + + assert!(unpack_metadata_mediator1.encrypted); + assert!(!unpack_metadata_mediator1.authenticated); + assert!(!unpack_metadata_mediator1.non_repudiation); + assert!(unpack_metadata_mediator1.anonymous_sender); + assert!(!unpack_metadata_mediator1.re_wrapped_in_forward); + + let forwarded_msg_at_mediator1 = + serde_json::to_string(&forward_at_mediator1.forwarded_msg) + .expect("Unable serialize forwarded message"); + + let forward_msg_for_mediator2 = wrap_in_forward( + &forwarded_msg_at_mediator1, + None, + &forward_at_mediator1.next, + &[MEDIATOR2_VERIFICATION_METHOD_KEY_AGREEM_X25519_1.id.clone()], + &AnonCryptAlg::default(), + &did_resolver, + ) + .await + .expect("Unable wrap in forward"); + + let (unpacked_msg_mediator2, unpack_metadata_mediator2) = Message::unpack_string( + &forward_msg_for_mediator2, + &mut did_resolver, + &did_method_resolver, + &mediator2_secrets_resolver, + &UnpackOptions::default(), + ) + .await + .expect("Unable unpack"); + + let forward_at_mediator2 = + try_parse_forward(&unpacked_msg_mediator2).expect("Message is not Forward"); + + assert_eq!(forward_at_mediator2.msg, &unpacked_msg_mediator2); + assert_eq!(&forward_at_mediator2.next, to); + + assert!(unpack_metadata_mediator2.encrypted); + assert!(!unpack_metadata_mediator2.authenticated); + assert!(!unpack_metadata_mediator2.non_repudiation); + assert!(unpack_metadata_mediator2.anonymous_sender); + assert!(!unpack_metadata_mediator2.re_wrapped_in_forward); + + let forwarded_msg_at_mediator2 = + serde_json::to_string(&forward_at_mediator2.forwarded_msg) + .expect("Unable serialize forwarded message"); + + let (unpacked_msg, unpack_metadata) = Message::unpack_string( + &forwarded_msg_at_mediator2, + &mut did_resolver, + &did_method_resolver, + &bob_secrets_resolver, + &UnpackOptions::default(), + ) + .await + .expect("Unable unpack"); + + assert_eq!(&unpacked_msg, &*MESSAGE_SIMPLE); + + assert!(unpack_metadata.encrypted); + assert_eq!( + unpack_metadata.authenticated, + from.is_some() || sign_by.is_some() + ); + assert_eq!(unpack_metadata.non_repudiation, sign_by.is_some()); + assert_eq!(unpack_metadata.anonymous_sender, from.is_none()); + assert!(!unpack_metadata.re_wrapped_in_forward); + } + } + + // TODO: Add negative tests for Routing protocol + + #[tokio::test] + async fn pack_encrypted_works_from_not_did_or_did_url() { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let res = MESSAGE_SIMPLE + .pack_encrypted( + BOB_DID, + "not-a-did".into(), + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::IllegalArgument); + + assert_eq!( + format!("{}", err), + "Illegal argument: `from` value is not a valid DID or DID URL" + ); + } + + #[tokio::test] + async fn pack_encrypted_works_to_not_did_or_did_url() { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let res = MESSAGE_SIMPLE + .pack_encrypted( + "not-a-did", + None, + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::IllegalArgument); + + assert_eq!( + format!("{}", err), + "Illegal argument: `to` value is not a valid DID or DID URL" + ); + } + + #[tokio::test] + async fn pack_encrypted_works_sign_by_not_did_or_did_url() { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let res = MESSAGE_SIMPLE + .pack_encrypted( + BOB_DID, + ALICE_DID.into(), + "not-a-did".into(), + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::IllegalArgument); + + assert_eq!( + format!("{}", err), + "Illegal argument: `sign_from` value is not a valid DID or DID URL" + ); + } + + #[tokio::test] + async fn pack_encrypted_works_from_differs_msg_from() { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let mut msg = MESSAGE_SIMPLE.clone(); + msg.from = CHARLIE_DID.to_string().into(); + let res = msg + .pack_encrypted( + BOB_DID, + ALICE_DID.into(), + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::IllegalArgument); + + assert_eq!( + format!("{}", err), + "Illegal argument: `message.from` value is not equal to `from` value's DID" + ); + } + + #[tokio::test] + async fn pack_encrypted_works_to_differs_msg_to() { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let mut msg = MESSAGE_SIMPLE.clone(); + msg.to = Some(vec![CHARLIE_DID.to_string()]); + let res = msg + .pack_encrypted( + BOB_DID, + ALICE_DID.into(), + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::IllegalArgument); + + assert_eq!( + format!("{}", err), + "Illegal argument: `message.to` value does not contain `to` value's DID" + ); + } + + #[tokio::test] + async fn pack_encrypted_works_to_presented_in_msg_to() { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let mut msg = MESSAGE_SIMPLE.clone(); + msg.to = Some(vec![CHARLIE_DID.to_string(), BOB_DID.to_string()]); + let _ = msg + .pack_encrypted( + BOB_DID, + ALICE_DID.into(), + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + } + + #[tokio::test] + async fn pack_encrypted_works_from_not_did_or_did_url_in_msg() { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let mut msg = MESSAGE_SIMPLE.clone(); + msg.from = "not-a-did".to_string().into(); + let res = msg + .pack_encrypted( + BOB_DID, + "not-a-did".into(), + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::IllegalArgument); + + assert_eq!( + format!("{}", err), + "Illegal argument: `from` value is not a valid DID or DID URL" + ); + } + + #[tokio::test] + async fn pack_encrypted_works_to_not_did_or_did_url_in_msg() { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let mut msg = MESSAGE_SIMPLE.clone(); + msg.to = Some(vec!["not-a-did".to_string()]); + let res = msg + .pack_encrypted( + "not-a-did", + ALICE_DID.into(), + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::IllegalArgument); + + assert_eq!( + format!("{}", err), + "Illegal argument: `to` value is not a valid DID or DID URL" + ); + } + + #[tokio::test] + async fn pack_encrypted_works_from_did_url_from_msg_did_positive() { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let _ = MESSAGE_SIMPLE + .pack_encrypted( + BOB_DID, + "did:example:alice#key-x25519-1".into(), + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + } + + #[tokio::test] + async fn pack_encrypted_works_to_did_url_to_msg_did_positive() { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let mut msg = MESSAGE_SIMPLE.clone(); + msg.to = Some(vec![ALICE_DID.to_string(), BOB_DID.to_string()]); + let _ = msg + .pack_encrypted( + "did:example:bob#key-x25519-1", + None, + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + } + + #[tokio::test] + async fn pack_encrypted_works_sign_by_differs_msg_from_positive() { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let _ = MESSAGE_SIMPLE + .pack_encrypted( + BOB_DID, + ALICE_DID.into(), + CHARLIE_DID.into(), + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + } + + #[tokio::test] + async fn pack_encrypted_works_from_did_from_msg_did_url() { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let mut msg = MESSAGE_SIMPLE.clone(); + msg.from = "did:example:alice#key-x25519-1".to_string().into(); + + let res = msg + .pack_encrypted( + BOB_DID, + ALICE_DID.into(), + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::IllegalArgument); + + assert_eq!( + format!("{}", err), + "Illegal argument: `message.from` value is not equal to `from` value's DID" + ); + } + + #[tokio::test] + async fn pack_encrypted_works_to_did_to_msg_did_url() { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let mut msg = MESSAGE_SIMPLE.clone(); + msg.to = Some(vec!["did:example:bob#key-x25519-1".into()]); + let res = msg + .pack_encrypted( + BOB_DID, + None, + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::IllegalArgument); + + assert_eq!( + format!("{}", err), + "Illegal argument: `message.to` value does not contain `to` value's DID" + ); + } + + #[tokio::test] + async fn pack_encrypted_works_from_unknown_did() { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let mut msg = MESSAGE_SIMPLE.clone(); + msg.from = "did:example:unknown".to_string().into(); + let res = msg + .pack_encrypted( + BOB_DID, + "did:example:unknown".into(), + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::DIDNotResolved); + + assert_eq!(format!("{}", err), "DID not resolved: Sender did not found"); + } + + #[tokio::test] + async fn pack_encrypted_works_from_unknown_did_url() { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let from = ALICE_DID.to_string() + "#unknown-key"; + let res = MESSAGE_SIMPLE + .pack_encrypted( + BOB_DID, + from.as_str().into(), + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::DIDUrlNotFound); + + assert_eq!( + format!("{}", err), + "DID URL not found: No sender key agreements found" + ); + } + + #[tokio::test] + async fn pack_encrypted_works_to_unknown_did() { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let mut msg = MESSAGE_SIMPLE.clone(); + msg.to = Some(vec!["did:example:unknown".into()]); + let res = msg + .pack_encrypted( + "did:example:unknown", + None, + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::DIDNotResolved); + + assert_eq!( + format!("{}", err), + "DID not resolved: Recipient did not found" + ); + } + + #[tokio::test] + async fn pack_encrypted_works_to_unknown_did_url() { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let to = BOB_DID.to_string() + "#unknown-key"; + let res = MESSAGE_SIMPLE + .pack_encrypted( + to.as_str(), + ALICE_DID.into(), + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::DIDUrlNotFound); + + assert_eq!( + format!("{}", err), + "DID URL not found: No recipient key agreements found" + ); + } + + #[tokio::test] + async fn pack_encrypted_works_sign_by_unknown_did_url() { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let sign_by = ALICE_DID.to_string() + "#unknown-key"; + let res = MESSAGE_SIMPLE + .pack_encrypted( + BOB_DID, + ALICE_DID.into(), + sign_by.as_str().into(), + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::DIDUrlNotFound); + + assert_eq!( + format!("{}", err), + "DID URL not found: Unable produce sign envelope: Signer key id not found in did doc" + ); + } + + #[tokio::test] + async fn pack_encrypted_works_from_not_in_secrets() { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let res = MESSAGE_SIMPLE + .pack_encrypted( + BOB_DID, + "did:example:alice#key-x25519-not-in-secrets-1".into(), + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::SecretNotFound); + + assert_eq!( + format!("{}", err), + "Secret not found: No sender secrets found" + ); + } + + #[tokio::test] + async fn pack_encrypted_works_sign_by_not_in_secrets() { + let did_resolver = ExampleDIDResolver::new(vec![ + ALICE_DID_DOC_WITH_NO_SECRETS.clone(), + BOB_DID_DOC.clone(), + ]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let res = MESSAGE_SIMPLE + .pack_encrypted( + BOB_DID, + ALICE_DID.into(), + "did:example:alice#key-not-in-secrets-1".into(), + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::SecretNotFound); + + assert_eq!( + format!("{}", err), + "Secret not found: Unable produce sign envelope: No signer secrets found" + ); + } + + #[tokio::test] + async fn pack_encrypted_works_to_not_in_secrets_positive() { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC_NO_SECRETS.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let to = "did:example:bob#key-x25519-not-secrets-1"; + let _ = MESSAGE_SIMPLE + .pack_encrypted( + to, + ALICE_DID.into(), + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + } + + #[tokio::test] + async fn pack_encrypted_works_to_from_different_curves() { + _pack_encrypted_works_to_from_different_curves( + "did:example:alice#key-x25519-1".into(), + "did:example:bob#key-p256-1", + ) + .await; + _pack_encrypted_works_to_from_different_curves( + "did:example:alice#key-x25519-1".into(), + "did:example:bob#key-p384-1", + ) + .await; + _pack_encrypted_works_to_from_different_curves( + "did:example:alice#key-x25519-1".into(), + "did:example:bob#key-p521-1", + ) + .await; + _pack_encrypted_works_to_from_different_curves( + "did:example:alice#key-p256-1".into(), + "did:example:bob#key-p384-1", + ) + .await; + _pack_encrypted_works_to_from_different_curves( + "did:example:alice#key-p256-1".into(), + "did:example:bob#key-p521-1", + ) + .await; + _pack_encrypted_works_to_from_different_curves( + "did:example:alice#key-p521-1".into(), + "did:example:bob#key-p384-1", + ) + .await; + + async fn _pack_encrypted_works_to_from_different_curves(from: Option<&str>, to: &str) { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let res = MESSAGE_SIMPLE + .pack_encrypted( + to, + from, + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::NoCompatibleCrypto); + + assert_eq!( + format!("{}", err), + "No compatible crypto: No common keys between sender and recipient found" + ); + } + } + + #[tokio::test] + async fn pack_encrypted_works_from_prior() { + let mut did_resolver = ExampleDIDResolver::new(vec![ + ALICE_DID_DOC.clone(), + BOB_DID_DOC.clone(), + CHARLIE_DID_DOC.clone(), + ]); + let charlie_rotated_to_alice_secrets_resolver = + ExampleSecretsResolver::new(CHARLIE_ROTATED_TO_ALICE_SECRETS.clone()); + let bob_secrets_resolver = ExampleSecretsResolver::new(BOB_SECRETS.clone()); + + let (packed_msg, _pack_metadata) = MESSAGE_FROM_PRIOR_FULL + .pack_encrypted( + BOB_DID, + Some(ALICE_DID), + None, + &did_resolver, + &charlie_rotated_to_alice_secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await + .expect("Unable pack_encrypted"); + + let did_method_resolver = DIDMethods::default(); + let (unpacked_msg, unpack_metadata) = Message::unpack_string( + &packed_msg, + &mut did_resolver, + &did_method_resolver, + &bob_secrets_resolver, + &UnpackOptions::default(), + ) + .await + .expect("Unable unpack"); + + assert_eq!(&unpacked_msg, &*MESSAGE_FROM_PRIOR_FULL); + assert_eq!( + unpack_metadata.from_prior_issuer_kid.as_ref(), + Some(&CHARLIE_SECRET_AUTH_KEY_ED25519.id) + ); + assert_eq!(unpack_metadata.from_prior.as_ref(), Some(&*FROM_PRIOR_FULL)); + } + + fn _verify_authcrypt( + msg: &str, + to_keys: Vec<&Secret>, + from_key: &VerificationMethod, + ) -> String + where + CE: KeyAeadInPlace + KeySecretBytes, + KDF: JoseKDF, + KE: KeyExchange + KeyGen + ToJwkValue + FromJwkValue, + KW: KeyWrap + FromKeyDerivation, + { + let msg = jwe::parse(msg).expect("Unable parse jwe"); + + assert_eq!( + msg.jwe + .recipients + .iter() + .map(|r| r.header.kid.to_string()) + .collect::>(), + to_keys.iter().map(|s| s.id.clone()).collect::>() + ); + + assert_eq!( + msg.protected.typ, + Some("application/didcomm-encrypted+json".into()) + ); + + assert_eq!(msg.protected.alg, jwe::Algorithm::Ecdh1puA256kw); + assert_eq!(msg.protected.enc, jwe::EncAlgorithm::A256cbcHs512); + assert_eq!(msg.protected.skid, Some(from_key.id.to_string())); + + let mut common_msg: Option> = None; + + for to_key in to_keys { + let from_kid = &from_key.id; + let to_kid = &to_key.id; + + let from_key = match from_key.verification_material { + VerificationMaterial::JWK { + public_key_jwk: ref value, + } => KE::from_jwk_value(value).expect("Unable from_jwk_value"), + _ => panic!("Unexpected verification method"), + }; + + let to_key = match to_key.secret_material { + SecretMaterial::JWK { + private_key_jwk: ref value, + } => KE::from_jwk_value(value).expect("Unable from_jwk_value"), + _ => panic!("Unexpected verification method"), + }; + + let msg = msg + .decrypt::(Some((from_kid, &from_key)), (to_kid, &to_key)) + .expect("Unable decrypt msg"); + + common_msg = if let Some(ref res) = common_msg { + assert_eq!(res, &msg); + Some(msg) + } else { + Some(msg) + }; + } + + let msg = common_msg.expect("No result gotten"); + String::from_utf8(msg).expect("Unable from_utf8") + } + + fn _verify_anoncrypt( + msg: &str, + to_keys: Vec<&Secret>, + enc_alg: jwe::EncAlgorithm, + ) -> String + where + CE: KeyAeadInPlace + KeySecretBytes, + KDF: JoseKDF, + KE: KeyExchange + KeyGen + ToJwkValue + FromJwkValue, + KW: KeyWrap + FromKeyDerivation, + { + let msg = jwe::parse(msg).expect("Unable parse jwe"); + + assert_eq!( + msg.jwe + .recipients + .iter() + .map(|r| r.header.kid.to_string()) + .collect::>(), + to_keys.iter().map(|s| s.id.clone()).collect::>() + ); + + assert_eq!( + msg.protected.typ, + Some("application/didcomm-encrypted+json".into()) + ); + + assert_eq!(msg.protected.alg, jwe::Algorithm::EcdhEsA256kw); + assert_eq!(msg.protected.enc, enc_alg); + assert_eq!(msg.protected.skid, None); + + let mut common_msg: Option> = None; + + for to_key in to_keys { + let to_kid = &to_key.id; + + let to_key = match to_key.secret_material { + SecretMaterial::JWK { + private_key_jwk: ref value, + } => KE::from_jwk_value(value).expect("Unable from_jwk_value"), + _ => panic!("Unexpected verification method"), + }; + + let msg = msg + .decrypt::(None, (to_kid, &to_key)) + .expect("Unable decrypt msg"); + + common_msg = if let Some(ref res) = common_msg { + assert_eq!(res, &msg); + Some(msg) + } else { + Some(msg) + }; + } + + let msg = common_msg.expect("No result gotten"); + String::from_utf8(msg).expect("Unable from_utf8") + } + + fn _verify_signed( + msg: &str, + sign_key: &VerificationMethod, + alg: jws::Algorithm, + ) -> String { + let msg = jws::parse(msg).expect("Unable parse"); + + assert_eq!( + msg.protected, + vec![jws::ProtectedHeader { + typ: "application/didcomm-signed+json".into(), + alg, + }] + ); + + assert_eq!(msg.jws.signatures.len(), 1); + + assert_eq!( + msg.jws.signatures[0].header, + jws::Header { + kid: sign_key.id.clone() + } + ); + + let sign_key_id = &sign_key.id; + + let sign_key = match sign_key.verification_material { + VerificationMaterial::JWK { + public_key_jwk: ref value, + } => Key::from_jwk_value(value).expect("Unable from_jwk_value"), + _ => panic!("Unexpected verification_material"), + }; + + let valid = msg.verify((sign_key_id, &sign_key)).expect("Unable verify"); + assert!(valid); + + let payload = BASE64_URL_SAFE_NO_PAD + .decode(msg.jws.payload) + .expect("Unable decode_config"); + + String::from_utf8(payload).expect("Unable from_utf8") + } + + fn _verify_plaintext(msg: &str, exp_msg: &str) { + let msg: Value = serde_json::from_str(msg).expect("Unable from_str"); + let exp_msg: Value = serde_json::from_str(exp_msg).expect("Unable from_str"); + assert_eq!(msg, exp_msg) + } +} + */ diff --git a/affinidi-messaging-didcomm/src/message/pack_plaintext.rs b/affinidi-messaging-didcomm/src/message/pack_plaintext.rs new file mode 100644 index 0000000..11d8b4f --- /dev/null +++ b/affinidi-messaging-didcomm/src/message/pack_plaintext.rs @@ -0,0 +1,181 @@ +use affinidi_did_resolver_cache_sdk::DIDCacheClient; + +use crate::{ + error::{err_msg, ErrorKind, Result, ResultExt}, + FromPrior, Message, +}; + +impl Message { + /// Produces `DIDComm Plaintext Messages` + /// https://identity.foundation/didcomm-messaging/spec/#didcomm-plaintext-messages. + /// + /// A DIDComm message in its plaintext form, not packaged into any protective envelope, + /// is known as a DIDComm plaintext message. Plaintext messages lack confidentiality and integrity + /// guarantees, and are repudiable. They are therefore not normally transported across security boundaries. + /// However, this may be a helpful format to inspect in debuggers, since it exposes underlying semantics, + /// and it is the format used in this spec to give examples of headers and other internals. + /// Depending on ambient security, plaintext may or may not be an appropriate format for DIDComm data at rest. + /// + /// # Returns + /// - a DIDComm plaintext message s JSON string + /// + /// # Errors + /// - `Malformed` Signed `from_prior` JWT is malformed. + /// - `DIDNotResolved` `from_prior` issuer DID not found. + /// - `DIDUrlNotFound` `from_prior` issuer authentication verification method is not found. + /// - `Unsupported` Crypto or method used for signing `from_prior` is unsupported. + /// - `InvalidState` Indicates a library error. + pub async fn pack_plaintext<'sr>(&self, did_resolver: &DIDCacheClient) -> Result { + let (from_prior, from_prior_issuer_kid) = match self.from_prior { + Some(ref from_prior) => { + let (from_prior, from_prior_issuer_kid) = + FromPrior::unpack(from_prior, did_resolver).await?; + (Some(from_prior), Some(from_prior_issuer_kid)) + } + None => (None, None), + }; + + self._validate_pack_plaintext(from_prior.as_ref(), from_prior_issuer_kid.as_deref())?; + + let msg = serde_json::to_string(self) + .kind(ErrorKind::InvalidState, "Unable to serialize message")?; + + Ok(msg) + } + + fn _validate_pack_plaintext( + &self, + from_prior: Option<&FromPrior>, + from_prior_issuer_kid: Option<&str>, + ) -> Result<()> { + if let Some(from_prior) = from_prior { + from_prior.validate_pack(from_prior_issuer_kid)?; + + if let Some(ref from) = self.from { + if &from_prior.sub != from { + Err(err_msg( + ErrorKind::Malformed, + "from_prior `sub` value is not equal to message `from` value", + ))?; + } + } + } + + Ok(()) + } +} + +/* +#[cfg(test)] +mod tests { + use serde_json::Value; + use ssi::did::DIDMethods; + + use crate::{ + did::resolvers::ExampleDIDResolver, + error::ErrorKind, + secrets::resolvers::ExampleSecretsResolver, + test_vectors::{ + ALICE_DID_DOC, BOB_DID_DOC, BOB_SECRETS, CHARLIE_DID_DOC, + CHARLIE_SECRET_AUTH_KEY_ED25519, FROM_PRIOR_FULL, MESSAGE_ATTACHMENT_BASE64, + MESSAGE_ATTACHMENT_JSON, MESSAGE_ATTACHMENT_LINKS, MESSAGE_ATTACHMENT_MULTI_1, + MESSAGE_ATTACHMENT_MULTI_2, MESSAGE_FROM_PRIOR_FULL, + MESSAGE_FROM_PRIOR_MISMATCHED_SUB_AND_FROM, MESSAGE_MINIMAL, MESSAGE_SIMPLE, + PLAINTEXT_MSG_ATTACHMENT_BASE64, PLAINTEXT_MSG_ATTACHMENT_JSON, + PLAINTEXT_MSG_ATTACHMENT_LINKS, PLAINTEXT_MSG_ATTACHMENT_MULTI_1, + PLAINTEXT_MSG_ATTACHMENT_MULTI_2, PLAINTEXT_MSG_MINIMAL, PLAINTEXT_MSG_SIMPLE, + }, + Message, UnpackOptions, + }; + + #[tokio::test] + async fn pack_plaintext_works() { + _pack_plaintext_works(&MESSAGE_SIMPLE, PLAINTEXT_MSG_SIMPLE).await; + _pack_plaintext_works(&MESSAGE_MINIMAL, PLAINTEXT_MSG_MINIMAL).await; + + _pack_plaintext_works(&MESSAGE_ATTACHMENT_BASE64, PLAINTEXT_MSG_ATTACHMENT_BASE64).await; + + _pack_plaintext_works(&MESSAGE_ATTACHMENT_JSON, PLAINTEXT_MSG_ATTACHMENT_JSON).await; + _pack_plaintext_works(&MESSAGE_ATTACHMENT_LINKS, PLAINTEXT_MSG_ATTACHMENT_LINKS).await; + + _pack_plaintext_works( + &MESSAGE_ATTACHMENT_MULTI_1, + PLAINTEXT_MSG_ATTACHMENT_MULTI_1, + ) + .await; + + _pack_plaintext_works( + &MESSAGE_ATTACHMENT_MULTI_2, + PLAINTEXT_MSG_ATTACHMENT_MULTI_2, + ) + .await; + + async fn _pack_plaintext_works(msg: &Message, exp_msg: &str) { + let did_resolver = ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone()]); + + let msg = msg + .pack_plaintext(&did_resolver) + .await + .expect("Unable pack_plaintext"); + + let msg: Value = serde_json::from_str(&msg).expect("Unable from_str"); + let exp_msg: Value = serde_json::from_str(exp_msg).expect("Unable from_str"); + assert_eq!(msg, exp_msg); + } + } + + #[tokio::test] + async fn pack_plaintext_works_from_prior() { + let mut did_resolver = ExampleDIDResolver::new(vec![ + ALICE_DID_DOC.clone(), + BOB_DID_DOC.clone(), + CHARLIE_DID_DOC.clone(), + ]); + let bob_secrets_resolver = ExampleSecretsResolver::new(BOB_SECRETS.clone()); + + let packed_msg = MESSAGE_FROM_PRIOR_FULL + .pack_plaintext(&did_resolver) + .await + .expect("Unable pack_plaintext"); + + let did_method_resolver = DIDMethods::default(); + let (unpacked_msg, unpack_metadata) = Message::unpack_string( + &packed_msg, + &mut did_resolver, + &did_method_resolver, + &bob_secrets_resolver, + &UnpackOptions::default(), + ) + .await + .expect("Unable unpack"); + + assert_eq!(&unpacked_msg, &*MESSAGE_FROM_PRIOR_FULL); + assert_eq!( + unpack_metadata.from_prior_issuer_kid.as_ref(), + Some(&CHARLIE_SECRET_AUTH_KEY_ED25519.id) + ); + assert_eq!(unpack_metadata.from_prior.as_ref(), Some(&*FROM_PRIOR_FULL)); + } + + #[tokio::test] + async fn pack_plaintext_works_mismatched_from_prior_sub_and_message_from() { + let did_resolver = ExampleDIDResolver::new(vec![ + ALICE_DID_DOC.clone(), + BOB_DID_DOC.clone(), + CHARLIE_DID_DOC.clone(), + ]); + + let err = MESSAGE_FROM_PRIOR_MISMATCHED_SUB_AND_FROM + .pack_plaintext(&did_resolver) + .await + .expect_err("res is ok"); + + assert_eq!(err.kind(), ErrorKind::Malformed); + + assert_eq!( + format!("{}", err), + "Malformed: from_prior `sub` value is not equal to message `from` value" + ); + } +} +*/ diff --git a/affinidi-messaging-didcomm/src/message/pack_signed.rs b/affinidi-messaging-didcomm/src/message/pack_signed.rs new file mode 100644 index 0000000..9baa45b --- /dev/null +++ b/affinidi-messaging-didcomm/src/message/pack_signed.rs @@ -0,0 +1,474 @@ +use affinidi_did_resolver_cache_sdk::{document::DocumentExt, DIDCacheClient}; +use serde::Serialize; + +use crate::{ + document::{did_or_url, is_did}, + error::{err_msg, ErrorKind, Result, ResultContext}, + jws::{self, Algorithm}, + secrets::SecretsResolver, + utils::crypto::{AsKnownKeyPairSecret, KnownKeyPair}, + Message, +}; + +impl Message { + /// Produces `DIDComm Signed Message` + /// https://identity.foundation/didcomm-messaging/spec/#didcomm-signed-message. + /// + /// Signed messages are not necessary to provide message integrity (tamper evidence), + /// or to prove the sender to the recipient. Both of these guarantees automatically occur + /// with the authenticated encryption in DIDComm encrypted messages. Signed messages are only + /// necessary when the origin of plaintext must be provable to third parties, + /// or when the sender can’t be proven to the recipient by authenticated encryption because + /// the recipient is not known in advance (e.g., in a broadcast scenario). + /// We therefore expect signed messages to be used in a few cases, but not as a matter of course. + /// + /// # Parameters + /// - `sign_by` a DID or key ID the sender uses for signing + /// - `did_resolver` instance of `DIDResolver` to resolve DIDs. + /// - `secrets_resolver` instance of SecretsResolver` to resolve sender DID keys secrets + /// + /// # Returns + /// Tuple (signed_message, metadata) + /// - `signed_message` a DIDComm signed message as JSON string + /// - `metadata` additional metadata about this `encrypt` execution like used keys identifiers and algorithms. + /// + /// # Errors + /// - `DIDNotResolved` Sender or recipient DID not found. + /// - `DIDUrlNotFound` DID doesn't contain mentioned DID Urls (for ex., key id) + /// - `SecretNotFound` Sender secret is not found. + /// - `Unsupported` Used crypto or method is unsupported. + /// - `InvalidState` Indicates library error. + /// - `IOError` IO error during DID or secrets resolving + /// + /// TODO: verify and update errors list + pub async fn pack_signed<'sr>( + &self, + sign_by: &str, + did_resolver: &DIDCacheClient, + secrets_resolver: &'sr (dyn SecretsResolver + 'sr + Sync), + ) -> Result<(String, PackSignedMetadata)> { + self._validate_pack_signed(sign_by)?; + + let (did, key_id) = did_or_url(sign_by); + + let did_doc = match did_resolver.resolve(did).await { + Ok(result) => result.doc, + Err(e) => { + return Err(err_msg( + ErrorKind::DIDNotResolved, + format!("Couldn't resolve ({}). Reason: {}", did, e), + )) + } + }; + + let authentications: Vec = if let Some(key_id) = key_id { + if did_doc.contains_authentication(key_id) { + vec![key_id.to_string()] + } else { + return Err(err_msg( + ErrorKind::DIDUrlNotFound, + "Signer key id not found in did doc", + )); + } + } else { + let _did = did_doc.id.as_did(); + did_doc + .verification_relationships + .authentication + .iter() + .map(|s| s.id().resolve(_did).to_string()) + .collect() + }; + + let key_id = secrets_resolver + .find_secrets(&authentications) + .await + .context("Unable find secrets")? + .first() + .ok_or_else(|| err_msg(ErrorKind::SecretNotFound, "No signer secrets found"))? + .to_string(); + + let secret = secrets_resolver + .get_secret(&key_id) + .await + .context("Unable get secret")? + .ok_or_else(|| err_msg(ErrorKind::SecretNotFound, "Signer secret not found"))?; + + let sign_key = secret + .as_key_pair() + .context("Unable instantiate sign key")?; + + let payload = self.pack_plaintext(did_resolver).await?; + + let msg = match sign_key { + KnownKeyPair::Ed25519(ref key) => { + jws::sign(payload.as_bytes(), (&key_id, key), Algorithm::EdDSA) + } + KnownKeyPair::P256(ref key) => { + jws::sign(payload.as_bytes(), (&key_id, key), Algorithm::Es256) + } + KnownKeyPair::K256(ref key) => { + jws::sign(payload.as_bytes(), (&key_id, key), Algorithm::Es256K) + } + _ => Err(err_msg(ErrorKind::Unsupported, "Unsupported signature alg"))?, + } + .context("Unable produce signatire")?; + + let metadata = PackSignedMetadata { + sign_by_kid: key_id.to_owned(), + }; + + Ok((msg, metadata)) + } + + fn _validate_pack_signed(&self, sign_by: &str) -> Result<()> { + if !is_did(sign_by) { + Err(err_msg( + ErrorKind::IllegalArgument, + "`sign_from` value is not a valid DID or DID URL", + ))?; + } + + Ok(()) + } +} + +/// Additional metadata about this `pack` method execution like used key identifiers. +#[derive(Debug, PartialEq, Eq, Clone, Serialize)] +pub struct PackSignedMetadata { + /// Identifier (DID URL) of sign key. + pub sign_by_kid: String, +} + +/* +#[cfg(test)] +mod tests { + use askar_crypto::{ + alg::{ed25519::Ed25519KeyPair, k256::K256KeyPair, p256::P256KeyPair}, + sign::KeySigVerify, + }; + use base64::prelude::*; + use serde_json::Value; + use ssi::did::DIDMethods; + + use crate::{ + did::{ + resolvers::{ExampleDIDResolver, MockDidResolver}, + DIDResolver, VerificationMaterial, + }, + error::{err_msg, ErrorKind}, + jwk::FromJwkValue, + jws::{self, Algorithm, Header, ProtectedHeader}, + secrets::{ + resolvers::ExampleSecretsResolver, Secret, SecretMaterial, SecretType, SecretsResolver, + }, + test_vectors::{ + ALICE_AUTH_METHOD_25519, ALICE_AUTH_METHOD_P256, ALICE_AUTH_METHOD_SECPP256K1, + ALICE_DID, ALICE_DID_DOC, ALICE_DID_DOC_WITH_NO_SECRETS, ALICE_SECRETS, BOB_DID_DOC, + BOB_SECRETS, CHARLIE_DID_DOC, CHARLIE_ROTATED_TO_ALICE_SECRETS, + CHARLIE_SECRET_AUTH_KEY_ED25519, FROM_PRIOR_FULL, MESSAGE_FROM_PRIOR_FULL, + MESSAGE_SIMPLE, PLAINTEXT_MSG_SIMPLE, + }, + Message, PackSignedMetadata, UnpackOptions, + }; + + #[tokio::test] + async fn pack_signed_works() { + let did_resolver = ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone()]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + _pack_signed_works::( + &did_resolver, + &secrets_resolver, + &MESSAGE_SIMPLE, + ALICE_DID, + &ALICE_AUTH_METHOD_25519.id, + Algorithm::EdDSA, + PLAINTEXT_MSG_SIMPLE, + &ALICE_DID_DOC.verification_method[4].verification_material, + ) + .await; + + _pack_signed_works::( + &did_resolver, + &secrets_resolver, + &MESSAGE_SIMPLE, + &ALICE_AUTH_METHOD_25519.id, + &ALICE_AUTH_METHOD_25519.id, + Algorithm::EdDSA, + PLAINTEXT_MSG_SIMPLE, + &ALICE_DID_DOC.verification_method[4].verification_material, + ) + .await; + + _pack_signed_works::( + &did_resolver, + &secrets_resolver, + &MESSAGE_SIMPLE, + &ALICE_AUTH_METHOD_P256.id, + &ALICE_AUTH_METHOD_P256.id, + Algorithm::Es256, + PLAINTEXT_MSG_SIMPLE, + &ALICE_DID_DOC.verification_method[5].verification_material, + ) + .await; + + _pack_signed_works::( + &did_resolver, + &secrets_resolver, + &MESSAGE_SIMPLE, + &ALICE_AUTH_METHOD_SECPP256K1.id, + &ALICE_AUTH_METHOD_SECPP256K1.id, + Algorithm::Es256K, + PLAINTEXT_MSG_SIMPLE, + &ALICE_DID_DOC.verification_method[6].verification_material, + ) + .await; + + #[allow(clippy::too_many_arguments)] + async fn _pack_signed_works<'dr, 'sr, Key: KeySigVerify + FromJwkValue>( + did_resolver: &'dr (dyn DIDResolver + 'dr + Sync), + secrets_resolver: &'sr (dyn SecretsResolver + 'sr + Sync), + message: &Message, + sign_by: &str, + sign_by_kid: &str, + alg: Algorithm, + plaintext: &str, + verification_material: &VerificationMaterial, + ) { + let (msg, metadata) = message + .pack_signed(sign_by, did_resolver, secrets_resolver) + .await + .expect("Unable pack_signed"); + + assert_eq!( + metadata, + PackSignedMetadata { + sign_by_kid: sign_by_kid.into(), + } + ); + + let msg = jws::parse(&msg).expect("Unable parse"); + + assert_eq!( + msg.protected, + vec![ProtectedHeader { + typ: "application/didcomm-signed+json".into(), + alg, + }] + ); + + let payload: Value = { + let payload = BASE64_URL_SAFE_NO_PAD + .decode(&msg.jws.payload) + .expect("Unable decode_config"); + + serde_json::from_slice(&payload).expect("Unable from_str") + }; + + let exp_payload: Value = serde_json::from_str(plaintext).expect("Unable from_str"); + + assert_eq!(payload, exp_payload); + assert_eq!(msg.jws.signatures.len(), 1); + + assert_eq!( + msg.jws.signatures[0].header, + Header { + kid: sign_by_kid.into() + } + ); + + let signer_key = match verification_material { + VerificationMaterial::JWK { + public_key_jwk: ref value, + } => Key::from_jwk_value(value).expect("Unable from_jwk_value"), + _ => panic!("Unexpected verification_material"), + }; + + let valid = msg + .verify((sign_by_kid, &signer_key)) + .expect("Unable verify"); + + assert!(valid); + } + } + + #[tokio::test] + async fn pack_signed_works_signer_did_not_found() { + let did_resolver = ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone()]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let res = MESSAGE_SIMPLE + .pack_signed("did:example:unknown", &did_resolver, &secrets_resolver) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::DIDNotResolved); + + assert_eq!(format!("{}", err), "DID not resolved: Signer did not found"); + } + + #[tokio::test] + async fn pack_signed_works_signer_is_not_did_our_did_url() { + let mut did_doc = ALICE_DID_DOC.clone(); + did_doc.id = "not-a-did".into(); + let did_resolver = ExampleDIDResolver::new(vec![did_doc]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let res = MESSAGE_SIMPLE + .pack_signed("not-a-did", &did_resolver, &secrets_resolver) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::IllegalArgument); + + assert_eq!( + format!("{}", err), + "Illegal argument: `sign_from` value is not a valid DID or DID URL" + ); + } + + #[tokio::test] + async fn pack_signed_works_signer_did_url_not_found() { + let did_resolver = ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone()]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let res = MESSAGE_SIMPLE + .pack_signed( + &format!("{}#unkown", ALICE_DID), + &did_resolver, + &secrets_resolver, + ) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::DIDUrlNotFound); + + assert_eq!( + format!("{}", err), + "DID URL not found: Signer key id not found in did doc" + ); + } + + #[tokio::test] + async fn pack_signed_works_signer_did_resolving_err() { + let did_resolver = + MockDidResolver::new(vec![Err(err_msg(ErrorKind::InvalidState, "Mock error"))]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let res = MESSAGE_SIMPLE + .pack_signed(ALICE_DID, &did_resolver, &secrets_resolver) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::InvalidState); + + assert_eq!( + format!("{}", err), + "Invalid state: Unable resolve signer did: Mock error" + ); + } + + #[tokio::test] + async fn pack_signed_works_signer_secrets_not_found() { + let did_resolver = ExampleDIDResolver::new(vec![ALICE_DID_DOC_WITH_NO_SECRETS.clone()]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let res = MESSAGE_SIMPLE + .pack_signed( + "did:example:alice#key-not-in-secrets-1", + &did_resolver, + &secrets_resolver, + ) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::SecretNotFound); + + assert_eq!( + format!("{}", err), + "Secret not found: No signer secrets found" + ); + } + + #[tokio::test] + async fn pack_signed_works_unable_instantiate_sign_key() { + let mut did_doc = ALICE_DID_DOC.clone(); + did_doc + .authentication + .push("did:example:alice#key-d25519-1".into()); + let mut secrets = ALICE_SECRETS.clone(); + secrets.push(Secret { + id: "did:example:alice#key-d25519-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: serde_json::json!({ + "kty": "EC", + "d": "sB0bYtpaXyp-h17dDpMx91N3Du1AdN4z1FUq02GbmLw", + "crv": "A-256", + "x": "L0crjMN1g0Ih4sYAJ_nGoHUck2cloltUpUVQDhF2nHE", + "y": "SxYgE7CmEJYi7IDhgK5jI4ZiajO8jPRZDldVhqFpYoo", + }), + }, + }); + let did_resolver = ExampleDIDResolver::new(vec![did_doc]); + let secrets_resolver = ExampleSecretsResolver::new(secrets); + + let res = MESSAGE_SIMPLE + .pack_signed( + "did:example:alice#key-d25519-1", + &did_resolver, + &secrets_resolver, + ) + .await; + + let err = res.expect_err("res is ok"); + assert_eq!(err.kind(), ErrorKind::Unsupported); + + assert_eq!( + format!("{}", err), + "Unsupported crypto or method: Unable instantiate sign key: Unsupported key type or curve" + ); + } + + #[tokio::test] + async fn pack_signed_works_from_prior() { + let mut did_resolver = ExampleDIDResolver::new(vec![ + ALICE_DID_DOC.clone(), + BOB_DID_DOC.clone(), + CHARLIE_DID_DOC.clone(), + ]); + let charlie_rotated_to_alice_secrets_resolver = + ExampleSecretsResolver::new(CHARLIE_ROTATED_TO_ALICE_SECRETS.clone()); + let bob_secrets_resolver = ExampleSecretsResolver::new(BOB_SECRETS.clone()); + + let (packed_msg, _pack_metadata) = MESSAGE_FROM_PRIOR_FULL + .pack_signed( + ALICE_DID, + &did_resolver, + &charlie_rotated_to_alice_secrets_resolver, + ) + .await + .expect("Unable pack_signed"); + + let did_method_resolver = DIDMethods::default(); + let (unpacked_msg, unpack_metadata) = Message::unpack_string( + &packed_msg, + &mut did_resolver, + &did_method_resolver, + &bob_secrets_resolver, + &UnpackOptions::default(), + ) + .await + .expect("Unable unpack"); + + assert_eq!(&unpacked_msg, &*MESSAGE_FROM_PRIOR_FULL); + assert_eq!( + unpack_metadata.from_prior_issuer_kid.as_ref(), + Some(&CHARLIE_SECRET_AUTH_KEY_ED25519.id) + ); + assert_eq!(unpack_metadata.from_prior.as_ref(), Some(&*FROM_PRIOR_FULL)); + } +} +*/ diff --git a/affinidi-messaging-didcomm/src/message/unpack/anoncrypt.rs b/affinidi-messaging-didcomm/src/message/unpack/anoncrypt.rs new file mode 100644 index 0000000..4ba6c91 --- /dev/null +++ b/affinidi-messaging-didcomm/src/message/unpack/anoncrypt.rs @@ -0,0 +1,191 @@ +use askar_crypto::{ + alg::{ + aes::{A256CbcHs512, A256Gcm, A256Kw, AesKey}, + chacha20::{Chacha20Key, XC20P}, + k256::K256KeyPair, + p256::P256KeyPair, + x25519::X25519KeyPair, + }, + kdf::ecdh_es::EcdhEs, +}; +use std::str::FromStr; + +use crate::{ + algorithms::AnonCryptAlg, + document::did_or_url, + envelope::{Envelope, MetaEnvelope, ParsedEnvelope}, + error::{err_msg, ErrorKind, Result, ResultExt}, + jwe, + secrets::SecretsResolver, + utils::crypto::{AsKnownKeyPairSecret, KnownKeyPair}, + UnpackOptions, +}; + +pub(crate) async fn _try_unpack_anoncrypt( + jwe: &ParsedEnvelope, + secrets_resolver: &dyn SecretsResolver, + opts: &UnpackOptions, + envelope: &mut MetaEnvelope, +) -> Result> { + let jwe = match jwe { + ParsedEnvelope::Jwe(jwe) => jwe, + _ => return Ok(None), + }; + + if jwe.protected.alg != jwe::Algorithm::EcdhEsA256kw { + return Ok(None); + } + + let to_kid = jwe + .to_kids + .first() + .ok_or_else(|| err_msg(ErrorKind::Malformed, "No recipient keys found"))?; + + let (to_did, _) = did_or_url(to_kid); + + if jwe.to_kids.iter().any(|k| { + let (k_did, k_url) = did_or_url(k); + (k_did != to_did) || (k_url.is_none()) + }) { + Err(err_msg( + ErrorKind::Malformed, + "Recipient keys are outside of one did or can't be resolved to key agreement", + ))?; + } + + envelope.metadata.encrypted_to_kids = Some(jwe.to_kids.iter().map(|k| k.to_owned()).collect()); + envelope.metadata.encrypted = true; + envelope.metadata.anonymous_sender = true; + + let to_kids_found = secrets_resolver.find_secrets(&jwe.to_kids).await?; + + if to_kids_found.is_empty() { + Err(err_msg( + ErrorKind::SecretNotFound, + "No recipient secrets found", + ))?; + } + + let mut payload: Option> = None; + + for to_kid in to_kids_found { + let to_key = secrets_resolver.get_secret(&to_kid).await?.ok_or_else(|| { + err_msg( + ErrorKind::InvalidState, + "Recipient secret not found after existence checking", + ) + })?; + let to_key = to_key.as_key_pair()?; + + let _payload = match (to_key, &jwe.protected.enc) { + (KnownKeyPair::X25519(ref to_key), jwe::EncAlgorithm::A256cbcHs512) => { + envelope.metadata.enc_alg_anon = Some(AnonCryptAlg::A256cbcHs512EcdhEsA256kw); + + jwe.decrypt::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >(None, (&to_kid, to_key))? + } + (KnownKeyPair::X25519(ref to_key), jwe::EncAlgorithm::Xc20P) => { + envelope.metadata.enc_alg_anon = Some(AnonCryptAlg::Xc20pEcdhEsA256kw); + + jwe.decrypt::< + Chacha20Key, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >(None, (&to_kid, to_key))? + } + (KnownKeyPair::X25519(ref to_key), jwe::EncAlgorithm::A256Gcm) => { + envelope.metadata.enc_alg_anon = Some(AnonCryptAlg::A256gcmEcdhEsA256kw); + + jwe.decrypt::< + AesKey, + EcdhEs<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >(None, (&to_kid, to_key))? + } + (KnownKeyPair::P256(ref to_key), jwe::EncAlgorithm::A256cbcHs512) => { + envelope.metadata.enc_alg_anon = Some(AnonCryptAlg::A256cbcHs512EcdhEsA256kw); + + jwe.decrypt::< + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >(None, (&to_kid, to_key))? + } + (KnownKeyPair::P256(ref to_key), jwe::EncAlgorithm::Xc20P) => { + envelope.metadata.enc_alg_anon = Some(AnonCryptAlg::Xc20pEcdhEsA256kw); + + jwe.decrypt::< + Chacha20Key, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >(None, (&to_kid, to_key))? + } + (KnownKeyPair::P256(ref to_key), jwe::EncAlgorithm::A256Gcm) => { + envelope.metadata.enc_alg_anon = Some(AnonCryptAlg::A256gcmEcdhEsA256kw); + + jwe.decrypt::< + AesKey, + EcdhEs<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >(None, (&to_kid, to_key))? + } + (KnownKeyPair::K256(ref to_key), jwe::EncAlgorithm::A256cbcHs512) => { + envelope.metadata.enc_alg_anon = Some(AnonCryptAlg::A256cbcHs512EcdhEsA256kw); + + jwe.decrypt::< + AesKey, + EcdhEs<'_, K256KeyPair>, + K256KeyPair, + AesKey, + >(None, (&to_kid, to_key))? + } + (KnownKeyPair::K256(ref to_key), jwe::EncAlgorithm::Xc20P) => { + envelope.metadata.enc_alg_anon = Some(AnonCryptAlg::Xc20pEcdhEsA256kw); + + jwe.decrypt::< + Chacha20Key, + EcdhEs<'_, K256KeyPair>, + K256KeyPair, + AesKey, + >(None, (&to_kid, to_key))? + } + (KnownKeyPair::K256(ref to_key), jwe::EncAlgorithm::A256Gcm) => { + envelope.metadata.enc_alg_anon = Some(AnonCryptAlg::A256gcmEcdhEsA256kw); + + jwe.decrypt::< + AesKey, + EcdhEs<'_, K256KeyPair>, + K256KeyPair, + AesKey, + >(None, (&to_kid, to_key))? + } + _ => Err(err_msg( + ErrorKind::Unsupported, + "Unsupported recipient key agreement method", + ))?, + }; + + payload = Some(_payload); + + if !opts.expect_decrypt_by_all_keys { + break; + } + } + + let payload = payload.ok_or_else(|| err_msg(ErrorKind::InvalidState, "Payload is none"))?; + + let payload = String::from_utf8(payload) + .kind(ErrorKind::Malformed, "Anoncrypt payload is invalid utf8")?; + + let e = Envelope::from_str(&payload)?.parse()?.verify_didcomm()?; + Ok(Some(e)) +} diff --git a/affinidi-messaging-didcomm/src/message/unpack/authcrypt.rs b/affinidi-messaging-didcomm/src/message/unpack/authcrypt.rs new file mode 100644 index 0000000..d3c42dd --- /dev/null +++ b/affinidi-messaging-didcomm/src/message/unpack/authcrypt.rs @@ -0,0 +1,161 @@ +use affinidi_did_resolver_cache_sdk::DIDCacheClient; +use askar_crypto::{ + alg::{ + aes::{A256CbcHs512, A256Kw, AesKey}, + k256::K256KeyPair, + p256::P256KeyPair, + x25519::X25519KeyPair, + }, + kdf::ecdh_1pu::Ecdh1PU, +}; +use std::str::FromStr; +use tracing::{debug, event, Level}; + +use crate::envelope::{Envelope, MetaEnvelope, ParsedEnvelope}; +use crate::{ + algorithms::AuthCryptAlg, + error::{err_msg, ErrorKind, Result, ResultExt}, + jwe, + secrets::SecretsResolver, + utils::crypto::{AsKnownKeyPairSecret, KnownKeyPair}, + UnpackOptions, +}; + +pub(crate) async fn _try_unpack_authcrypt( + jwe: &ParsedEnvelope, + did_resolver: &DIDCacheClient, + secrets_resolver: &dyn SecretsResolver, + opts: &UnpackOptions, + envelope: &mut MetaEnvelope, + present_crypto_operations_count: usize, +) -> Result> { + let jwe = match jwe { + ParsedEnvelope::Jwe(jwe) => jwe, + _ => return Ok(None), + }; + + event!( + Level::DEBUG, + "expecting (ECDH-1PU+A256KW) jwe.protected.alg({})", + jwe.protected.alg + ); + if jwe.protected.alg != jwe::Algorithm::Ecdh1puA256kw { + return Ok(None); + } + + if jwe.apu.is_some() && envelope.from_kid.is_none() { + debug!("Recalculating envelope meta-data from APU"); + jwe.fill_envelope_from(envelope, did_resolver, secrets_resolver) + .await?; + } + + let mut payload: Option> = None; + let mut crypto_operations_count = present_crypto_operations_count; + + debug!("{} to_kids found", &envelope.to_kids_found.len()); + for to_kid in &envelope.to_kids_found { + crypto_operations_count += 1; + if crypto_operations_count >= opts.crypto_operations_limit_per_message { + return Err(err_msg( + ErrorKind::TooManyCryptoOperations, + format!( + "Limit is reached. limit = {}, executed crypto operations {}", + opts.crypto_operations_limit_per_message, crypto_operations_count + ), + )); + } + let to_key = secrets_resolver.get_secret(to_kid).await?.ok_or_else(|| { + err_msg( + ErrorKind::InvalidState, + "Recipient secret not found after existence checking", + ) + })?; + let to_key = to_key.as_key_pair()?; + + let _payload = match ( + envelope.from_key.as_ref().unwrap(), + &to_key, + &jwe.protected.enc, + ) { + ( + KnownKeyPair::X25519(ref from_key), + KnownKeyPair::X25519(ref to_key), + jwe::EncAlgorithm::A256cbcHs512, + ) => { + envelope.metadata.enc_alg_auth = Some(AuthCryptAlg::A256cbcHs512Ecdh1puA256kw); + + jwe.decrypt::< + AesKey, + Ecdh1PU<'_, X25519KeyPair>, + X25519KeyPair, + AesKey, + >(Some((envelope.from_kid.as_ref().unwrap(), from_key)), (to_kid, to_key))? + } + ( + KnownKeyPair::P256(ref from_key), + KnownKeyPair::P256(ref to_key), + jwe::EncAlgorithm::A256cbcHs512, + ) => { + envelope.metadata.enc_alg_auth = Some(AuthCryptAlg::A256cbcHs512Ecdh1puA256kw); + + jwe.decrypt::< + AesKey, + Ecdh1PU<'_, P256KeyPair>, + P256KeyPair, + AesKey, + >(Some((envelope.from_kid.as_ref().unwrap(), from_key)), (to_kid, to_key))? + } + (KnownKeyPair::X25519(_), KnownKeyPair::P256(_), _) => Err(err_msg( + ErrorKind::Malformed, + "Incompatible sender and recipient key agreement curves", + ))?, + (KnownKeyPair::P256(_), KnownKeyPair::X25519(_), _) => Err(err_msg( + ErrorKind::Malformed, + "Incompatible sender and recipient key agreement curves", + ))?, + ( + KnownKeyPair::K256(ref from_key), + KnownKeyPair::K256(ref to_key), + jwe::EncAlgorithm::A256cbcHs512, + ) => { + envelope.metadata.enc_alg_auth = Some(AuthCryptAlg::A256cbcHs512Ecdh1puA256kw); + + jwe.decrypt::< + AesKey, + Ecdh1PU<'_, K256KeyPair>, + K256KeyPair, + AesKey, + >(Some((envelope.from_kid.as_ref().unwrap(), from_key)), (to_kid, to_key))? + } + (KnownKeyPair::X25519(_), KnownKeyPair::K256(_), _) => Err(err_msg( + ErrorKind::Malformed, + "Incompatible sender and recipient key agreement curves", + ))?, + (KnownKeyPair::K256(_), KnownKeyPair::X25519(_), _) => Err(err_msg( + ErrorKind::Malformed, + "Incompatible sender and recipient key agreement curves", + ))?, + _ => Err(err_msg( + ErrorKind::Unsupported, + "Unsupported key agreement method", + ))?, + }; + + payload = Some(_payload); + + if !opts.expect_decrypt_by_all_keys { + break; + } + } + debug!("payload = {}", payload.is_some()); + + let payload = payload.ok_or_else(|| err_msg(ErrorKind::InvalidState, "Payload is none"))?; + + let payload = String::from_utf8(payload) + .kind(ErrorKind::Malformed, "Authcrypt payload is invalid utf8")?; + debug!("payload = {}", payload); + + let e = Envelope::from_str(&payload)?.parse()?.verify_didcomm()?; + debug!("returning envelope type ({})", e.get_type()); + Ok(Some(e)) +} diff --git a/affinidi-messaging-didcomm/src/message/unpack/mod.rs b/affinidi-messaging-didcomm/src/message/unpack/mod.rs new file mode 100644 index 0000000..7b1d11b --- /dev/null +++ b/affinidi-messaging-didcomm/src/message/unpack/mod.rs @@ -0,0 +1,2194 @@ +use affinidi_did_resolver_cache_sdk::DIDCacheClient; +use serde::{Deserialize, Serialize}; + +use anoncrypt::_try_unpack_anoncrypt; +use authcrypt::_try_unpack_authcrypt; +use sign::_try_unpack_sign; +use std::str::FromStr; +use tracing::debug; + +use crate::{ + algorithms::{AnonCryptAlg, AuthCryptAlg, SignAlg}, + document::did_or_url, + envelope::{Envelope, MetaEnvelope, ParsedEnvelope}, + error::{err_msg, ErrorKind, Result, ResultExt}, + jws::Jws, + message::unpack::plaintext::_try_unpack_plaintext, + protocols::routing::try_parse_forward, + secrets::SecretsResolver, + FromPrior, Message, +}; + +mod anoncrypt; +mod authcrypt; +mod plaintext; +mod sign; + +impl Message { + pub async fn unpack_string( + msg: &str, + did_resolver: &DIDCacheClient, + secrets_resolver: &S, + options: &UnpackOptions, + ) -> Result<(Message, UnpackMetadata)> + where + S: SecretsResolver, + { + let mut envelope = MetaEnvelope::new(msg, did_resolver, secrets_resolver).await?; + + Self::unpack(&mut envelope, did_resolver, secrets_resolver, options).await + } + /// Unpacks the packed message by doing decryption and verifying the signatures. + /// This method supports all DID Comm message types (encrypted, signed, plaintext). + /// + /// If unpack options expect a particular property (for example that a message is encrypted) + /// and the packed message doesn't meet the criteria (it's not encrypted), then a MessageUntrusted + /// error will be returned. + /// + /// # Params + /// - `packed_msg` the message as JSON string to be unpacked + /// - `did_resolver` instance of `DIDResolver` to resolve DIDs + /// - `secrets_resolver` instance of SecretsResolver` to resolve sender DID keys secrets + /// - `options` allow fine configuration of unpacking process and imposing additional restrictions + /// to message to be trusted. + /// + /// # Returns + /// Tuple `(message, metadata)`. + /// - `message` plain message instance + /// - `metadata` additional metadata about this `unpack` execution like used keys identifiers, + /// trust context, algorithms and etc. + /// + /// # Errors + /// - `DIDNotResolved` Sender or recipient DID not found. + /// - `DIDUrlNotFound` DID doesn't contain mentioned DID Urls (for ex., key id) + /// - `MessageMalformed` message doesn't correspond to DID Comm or has invalid encryption or signatures. + /// - `Unsupported` Used crypto or method is unsupported. + /// - `SecretNotFound` No recipient secrets found. + /// - `InvalidState` Indicates library error. + /// - `IOError` IO error during DID or secrets resolving. + /// + /// TODO: verify and update errors list + pub async fn unpack( + envelope: &mut MetaEnvelope, + did_resolver: &DIDCacheClient, + secrets_resolver: &S, + options: &UnpackOptions, + ) -> Result<(Message, UnpackMetadata)> + where + S: SecretsResolver, + { + //let mut msg = msg; + let mut anoncrypted: Option; + let mut forwarded_msg: String; + + let mut parsed_jwe = if let Some(parsed) = &envelope.parsed_envelope { + parsed.clone() + } else { + return Err(err_msg( + ErrorKind::InvalidState, + "Parsed envelope not found", + )); + }; + + let mut crypto_operations_count: usize = 0; + + loop { + crypto_operations_count += 1; + if crypto_operations_count >= options.crypto_operations_limit_per_message { + return Err(err_msg( + ErrorKind::TooManyCryptoOperations, + format!( + "Limit is reached. limit = {}, executed crypto operations {}", + options.crypto_operations_limit_per_message, crypto_operations_count + ), + )); + } + anoncrypted = + _try_unpack_anoncrypt(&parsed_jwe, secrets_resolver, options, envelope).await?; + + if options.unwrap_re_wrapping_forward && anoncrypted.is_some() { + let forwarded_msg_opt = Self::_try_unwrap_forwarded_message( + &anoncrypted.clone().unwrap(), + did_resolver, + secrets_resolver, + ) + .await?; + + if forwarded_msg_opt.is_some() { + forwarded_msg = forwarded_msg_opt.unwrap(); + envelope.metadata.re_wrapped_in_forward = true; + + parsed_jwe = Envelope::from_str(&forwarded_msg)? + .parse()? + .verify_didcomm()?; + continue; + } + } + + break; + } + let parsed_jwe = anoncrypted.clone().unwrap_or(parsed_jwe); + debug!("metadata = {:#?}", envelope.metadata); + + let authcrypted = _try_unpack_authcrypt( + &parsed_jwe, + did_resolver, + secrets_resolver, + options, + envelope, + crypto_operations_count, + ) + .await?; + + let parsed_jwe = authcrypted.unwrap_or(parsed_jwe); + + let signed = _try_unpack_sign(&parsed_jwe, did_resolver, options, envelope).await?; + let parsed_jwe = signed.unwrap_or(parsed_jwe); + + let msg = _try_unpack_plaintext(&parsed_jwe, did_resolver, envelope) + .await? + .ok_or_else(|| { + err_msg( + ErrorKind::Malformed, + "Message is not a valid JWE, JWS or JWM", + ) + })?; + + Ok((msg, envelope.metadata.to_owned())) + } + + async fn _try_unwrap_forwarded_message( + msg: &ParsedEnvelope, + did_resolver: &DIDCacheClient, + secrets_resolver: &dyn SecretsResolver, + ) -> Result> { + let plaintext = match msg { + ParsedEnvelope::Message(m) => m.clone(), + _ => return Ok(None), + }; + + if let Some(forward_msg) = try_parse_forward(&plaintext) { + if has_key_agreement_secret(&forward_msg.next, did_resolver, secrets_resolver).await? { + // TODO: Think how to avoid extra serialization of forwarded_msg here. + // (This serializtion is a double work because forwarded_msg will then + // be deserialized in _try_unpack_anoncrypt.) + let forwarded_msg = serde_json::to_string(&forward_msg.forwarded_msg).kind( + ErrorKind::InvalidState, + "Unable serialize forwarded message", + )?; + + return Ok(Some(forwarded_msg)); + } + } + Ok(None) + } +} + +/// Allows fine customization of unpacking process +#[derive(Debug, PartialEq, Eq, Deserialize, Clone)] +pub struct UnpackOptions { + /// Whether the plaintext must be decryptable by all keys resolved by the secrets resolver. False by default. + #[serde(default)] + pub expect_decrypt_by_all_keys: bool, + + /// If `true` and the packed message is a `Forward` + /// wrapping a plaintext packed for the given recipient, then both Forward and packed plaintext are unpacked automatically, + /// and the unpacked plaintext will be returned instead of unpacked Forward. + /// False by default. + #[serde(default)] + pub unwrap_re_wrapping_forward: bool, + + /// the amount of crypto operations per unpack operation + /// default is 1_000 but can be updated + pub crypto_operations_limit_per_message: usize, +} + +impl Default for UnpackOptions { + fn default() -> Self { + UnpackOptions { + expect_decrypt_by_all_keys: false, + unwrap_re_wrapping_forward: true, + crypto_operations_limit_per_message: 1_000, + } + } +} + +/// Additional metadata about this `unpack` method execution like trust predicates +/// and used keys identifiers. +#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] +pub struct UnpackMetadata { + /// Whether the plaintext has been encrypted + pub encrypted: bool, + + /// Whether the plaintext has been authenticated + pub authenticated: bool, + + /// Whether the plaintext has been signed + pub non_repudiation: bool, + + /// Whether the sender ID was hidden or protected + pub anonymous_sender: bool, + + /// Whether the plaintext was re-wrapped in a forward message by a mediator + pub re_wrapped_in_forward: bool, + + /// Key ID of the sender used for authentication encryption if the plaintext has been authenticated and encrypted + pub encrypted_from_kid: Option, + + /// Target key IDS for encryption if the plaintext has been encrypted + pub encrypted_to_kids: Option>, + + /// Key ID used for signature if the plaintext has been signed + pub sign_from: Option, + + /// Key ID used for from_prior header signature if from_prior header is present + pub from_prior_issuer_kid: Option, + + /// Algorithm used for authenticated encryption + pub enc_alg_auth: Option, + + /// Algorithm used for anonymous encryption + pub enc_alg_anon: Option, + + /// Algorithm used for message signing + pub sign_alg: Option, + + /// If the plaintext has been signed, the JWS is returned for non-repudiation purposes + pub signed_message: Option, + + /// If plaintext contains from_prior header, its unpacked value is returned + pub from_prior: Option, +} + +async fn has_key_agreement_secret( + did_or_kid: &str, + did_resolver: &DIDCacheClient, + secrets_resolver: &dyn SecretsResolver, +) -> Result { + let kids = match did_or_url(did_or_kid) { + (_, Some(kid)) => { + vec![kid.to_owned()] + } + (did, None) => { + let did_doc = match did_resolver.resolve(did).await { + Ok(result) => result.doc, + Err(e) => { + return Err(err_msg( + ErrorKind::DIDNotResolved, + format!("Couldn't resolve did({}). Reason: {}", did, e), + )) + } + }; + did_doc + .verification_relationships + .key_agreement + .iter() + .map(|key| key.id().resolve(did_doc.id.as_did()).to_string()) + .collect() + } + }; + + let kids = kids.iter().map(|k| k.to_owned()).collect::>(); + + let secrets_ids = secrets_resolver.find_secrets(&kids).await?; + + Ok(!secrets_ids.is_empty()) +} + +/* +#[cfg(test)] +mod test { + use tracing::debug; + use tracing_test::traced_test; + + use crate::{ + did::resolvers::ExampleDIDResolver, + message::MessagingServiceMetadata, + protocols::routing::wrap_in_forward, + secrets::resolvers::ExampleSecretsResolver, + test_vectors::{ + remove_field, remove_protected_field, update_field, update_protected_field, + ALICE_AUTH_METHOD_25519, ALICE_AUTH_METHOD_P256, ALICE_AUTH_METHOD_SECPP256K1, + ALICE_DID, ALICE_DID_DOC, ALICE_SECRETS, ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256, + ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519, BOB_DID, BOB_DID_COMM_MESSAGING_SERVICE, + BOB_DID_DOC, BOB_SECRETS, BOB_SECRET_KEY_AGREEMENT_KEY_P256_1, + BOB_SECRET_KEY_AGREEMENT_KEY_P256_2, BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1, + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2, BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3, + BOB_SERVICE, CHARLIE_AUTH_METHOD_25519, CHARLIE_DID_DOC, ENCRYPTED_MSG_ANON_XC20P_1, + ENCRYPTED_MSG_ANON_XC20P_2, ENCRYPTED_MSG_AUTH_P256, ENCRYPTED_MSG_AUTH_P256_SIGNED, + ENCRYPTED_MSG_AUTH_X25519, FROM_PRIOR_FULL, + INVALID_ENCRYPTED_MSG_ANON_P256_EPK_WRONG_POINT, + INVALID_PLAINTEXT_MSG_ATTACHMENTS_AS_INT_ARRAY, + INVALID_PLAINTEXT_MSG_ATTACHMENTS_AS_STRING, + INVALID_PLAINTEXT_MSG_ATTACHMENTS_EMPTY_DATA, + INVALID_PLAINTEXT_MSG_ATTACHMENTS_LINKS_NO_HASH, + INVALID_PLAINTEXT_MSG_ATTACHMENTS_NO_DATA, INVALID_PLAINTEXT_MSG_ATTACHMENTS_NULL_DATA, + INVALID_PLAINTEXT_MSG_ATTACHMENTS_WRONG_DATA, + INVALID_PLAINTEXT_MSG_ATTACHMENTS_WRONG_ID, INVALID_PLAINTEXT_MSG_EMPTY, + INVALID_PLAINTEXT_MSG_EMPTY_ATTACHMENTS, INVALID_PLAINTEXT_MSG_NO_BODY, + INVALID_PLAINTEXT_MSG_NO_ID, INVALID_PLAINTEXT_MSG_NO_TYPE, + INVALID_PLAINTEXT_MSG_STRING, INVALID_PLAINTEXT_MSG_WRONG_TYP, MEDIATOR1_DID_DOC, + MEDIATOR1_SECRETS, MESSAGE_ATTACHMENT_BASE64, MESSAGE_ATTACHMENT_JSON, + MESSAGE_ATTACHMENT_LINKS, MESSAGE_ATTACHMENT_MULTI_1, MESSAGE_ATTACHMENT_MULTI_2, + MESSAGE_FROM_PRIOR_FULL, MESSAGE_MINIMAL, MESSAGE_SIMPLE, PLAINTEXT_FROM_PRIOR, + PLAINTEXT_FROM_PRIOR_INVALID_SIGNATURE, PLAINTEXT_INVALID_FROM_PRIOR, + PLAINTEXT_MSG_ATTACHMENT_BASE64, PLAINTEXT_MSG_ATTACHMENT_JSON, + PLAINTEXT_MSG_ATTACHMENT_LINKS, PLAINTEXT_MSG_ATTACHMENT_MULTI_1, + PLAINTEXT_MSG_ATTACHMENT_MULTI_2, PLAINTEXT_MSG_MINIMAL, PLAINTEXT_MSG_SIMPLE, + PLAINTEXT_MSG_SIMPLE_NO_TYP, SIGNED_MSG_ALICE_KEY_1, SIGNED_MSG_ALICE_KEY_2, + SIGNED_MSG_ALICE_KEY_3, + }, + PackEncryptedOptions, + }; + + use super::*; + + #[tokio::test] + async fn unpack_works_plaintext() { + let plaintext_metadata = UnpackMetadata { + anonymous_sender: false, + authenticated: false, + non_repudiation: false, + encrypted: false, + enc_alg_auth: None, + enc_alg_anon: None, + sign_alg: None, + encrypted_from_kid: None, + encrypted_to_kids: None, + sign_from: None, + signed_message: None, + from_prior_issuer_kid: None, + from_prior: None, + re_wrapped_in_forward: false, + }; + + _verify_unpack(PLAINTEXT_MSG_SIMPLE, &MESSAGE_SIMPLE, &plaintext_metadata).await; + _verify_unpack( + PLAINTEXT_MSG_SIMPLE_NO_TYP, + &MESSAGE_SIMPLE, + &plaintext_metadata, + ) + .await; + + _verify_unpack(PLAINTEXT_MSG_MINIMAL, &MESSAGE_MINIMAL, &plaintext_metadata).await; + + _verify_unpack( + PLAINTEXT_MSG_ATTACHMENT_BASE64, + &MESSAGE_ATTACHMENT_BASE64, + &plaintext_metadata, + ) + .await; + + _verify_unpack( + PLAINTEXT_MSG_ATTACHMENT_JSON, + &MESSAGE_ATTACHMENT_JSON, + &plaintext_metadata, + ) + .await; + + _verify_unpack( + PLAINTEXT_MSG_ATTACHMENT_LINKS, + &MESSAGE_ATTACHMENT_LINKS, + &plaintext_metadata, + ) + .await; + + _verify_unpack( + PLAINTEXT_MSG_ATTACHMENT_MULTI_1, + &MESSAGE_ATTACHMENT_MULTI_1, + &plaintext_metadata, + ) + .await; + + _verify_unpack( + PLAINTEXT_MSG_ATTACHMENT_MULTI_2, + &MESSAGE_ATTACHMENT_MULTI_2, + &plaintext_metadata, + ) + .await; + } + + #[tokio::test] + async fn unpack_works_plaintext_2way() { + _unpack_works_plaintext_2way(&MESSAGE_SIMPLE).await; + _unpack_works_plaintext_2way(&MESSAGE_MINIMAL).await; + _unpack_works_plaintext_2way(&MESSAGE_ATTACHMENT_BASE64).await; + _unpack_works_plaintext_2way(&MESSAGE_ATTACHMENT_JSON).await; + _unpack_works_plaintext_2way(&MESSAGE_ATTACHMENT_LINKS).await; + _unpack_works_plaintext_2way(&MESSAGE_ATTACHMENT_MULTI_1).await; + _unpack_works_plaintext_2way(&MESSAGE_ATTACHMENT_MULTI_2).await; + + async fn _unpack_works_plaintext_2way(msg: &Message) { + let did_resolver = ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone()]); + + let packed = msg + .pack_plaintext(&did_resolver) + .await + .expect("Unable pack_plaintext"); + + _verify_unpack( + &packed, + msg, + &UnpackMetadata { + anonymous_sender: false, + authenticated: false, + non_repudiation: false, + encrypted: false, + enc_alg_auth: None, + enc_alg_anon: None, + sign_alg: None, + encrypted_from_kid: None, + encrypted_to_kids: None, + sign_from: None, + signed_message: None, + from_prior_issuer_kid: None, + from_prior: None, + re_wrapped_in_forward: false, + }, + ) + .await; + } + } + + #[tokio::test] + async fn unpack_works_signed() { + let sign_metadata = UnpackMetadata { + anonymous_sender: false, + authenticated: true, + non_repudiation: true, + encrypted: false, + enc_alg_auth: None, + enc_alg_anon: None, + sign_alg: None, + encrypted_from_kid: None, + encrypted_to_kids: None, + sign_from: None, + signed_message: None, + from_prior_issuer_kid: None, + from_prior: None, + re_wrapped_in_forward: false, + }; + + _verify_unpack( + SIGNED_MSG_ALICE_KEY_1, + &MESSAGE_SIMPLE, + &UnpackMetadata { + sign_from: Some("did:example:alice#key-1".into()), + sign_alg: Some(SignAlg::EdDSA), + signed_message: Some(serde_json::from_str(SIGNED_MSG_ALICE_KEY_1).unwrap()), + ..sign_metadata.clone() + }, + ) + .await; + + _verify_unpack( + SIGNED_MSG_ALICE_KEY_2, + &MESSAGE_SIMPLE, + &UnpackMetadata { + sign_from: Some("did:example:alice#key-2".into()), + sign_alg: Some(SignAlg::ES256), + signed_message: Some(serde_json::from_str(SIGNED_MSG_ALICE_KEY_2).unwrap()), + ..sign_metadata.clone() + }, + ) + .await; + + _verify_unpack( + SIGNED_MSG_ALICE_KEY_3, + &MESSAGE_SIMPLE, + &UnpackMetadata { + sign_from: Some("did:example:alice#key-3".into()), + sign_alg: Some(SignAlg::ES256K), + signed_message: Some(serde_json::from_str(SIGNED_MSG_ALICE_KEY_3).unwrap()), + ..sign_metadata.clone() + }, + ) + .await; + } + + #[tokio::test] + async fn unpack_works_signed_2way() { + _unpack_works_signed_2way( + &MESSAGE_SIMPLE, + ALICE_DID, + &ALICE_AUTH_METHOD_25519.id, + SignAlg::EdDSA, + ) + .await; + + _unpack_works_signed_2way( + &MESSAGE_SIMPLE, + &ALICE_AUTH_METHOD_25519.id, + &ALICE_AUTH_METHOD_25519.id, + SignAlg::EdDSA, + ) + .await; + + _unpack_works_signed_2way( + &MESSAGE_SIMPLE, + &ALICE_AUTH_METHOD_P256.id, + &ALICE_AUTH_METHOD_P256.id, + SignAlg::ES256, + ) + .await; + + _unpack_works_signed_2way( + &MESSAGE_SIMPLE, + &ALICE_AUTH_METHOD_SECPP256K1.id, + &ALICE_AUTH_METHOD_SECPP256K1.id, + SignAlg::ES256K, + ) + .await; + + async fn _unpack_works_signed_2way( + message: &Message, + sign_by: &str, + sign_by_kid: &str, + sign_alg: SignAlg, + ) { + let did_resolver = ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone()]); + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let (msg, _) = message + .pack_signed(sign_by, &did_resolver, &secrets_resolver) + .await + .expect("Unable pack_signed"); + + _verify_unpack( + &msg, + &MESSAGE_SIMPLE, + &UnpackMetadata { + sign_from: Some(sign_by_kid.into()), + sign_alg: Some(sign_alg), + signed_message: Some(serde_json::from_str(&msg).unwrap()), + anonymous_sender: false, + authenticated: true, + non_repudiation: true, + encrypted: false, + enc_alg_auth: None, + enc_alg_anon: None, + encrypted_from_kid: None, + encrypted_to_kids: None, + from_prior_issuer_kid: None, + from_prior: None, + re_wrapped_in_forward: false, + }, + ) + .await; + } + } + + #[tokio::test] + async fn unpack_works_anoncrypt() { + let metadata = UnpackMetadata { + anonymous_sender: true, + authenticated: false, + non_repudiation: false, + encrypted: true, + enc_alg_auth: None, + enc_alg_anon: None, + sign_alg: None, + encrypted_from_kid: None, + encrypted_to_kids: None, + sign_from: None, + signed_message: None, + from_prior_issuer_kid: None, + from_prior: None, + re_wrapped_in_forward: false, + }; + + _verify_unpack( + ENCRYPTED_MSG_ANON_XC20P_1, + &MESSAGE_SIMPLE, + &UnpackMetadata { + enc_alg_anon: Some(AnonCryptAlg::Xc20pEcdhEsA256kw), + encrypted_to_kids: Some(vec![ + "did:example:bob#key-x25519-1".into(), + "did:example:bob#key-x25519-2".into(), + "did:example:bob#key-x25519-3".into(), + ]), + ..metadata.clone() + }, + ) + .await; + + _verify_unpack( + ENCRYPTED_MSG_ANON_XC20P_2, + &MESSAGE_SIMPLE, + &UnpackMetadata { + enc_alg_anon: Some(AnonCryptAlg::Xc20pEcdhEsA256kw), + encrypted_to_kids: Some(vec![ + "did:example:bob#key-p256-1".into(), + "did:example:bob#key-p256-2".into(), + ]), + ..metadata.clone() + }, + ) + .await; + + // TODO: Check P-384 curve support + // TODO: Check P-521 curve support + } + + #[tokio::test] + async fn unpack_works_unwrap_re_wrapping_forward_on() { + _unpack_works_unwrap_re_wrapping_forward_on(BOB_DID, None, None).await; + + _unpack_works_unwrap_re_wrapping_forward_on(BOB_DID, None, Some(ALICE_DID)).await; + + _unpack_works_unwrap_re_wrapping_forward_on(BOB_DID, Some(ALICE_DID), None).await; + + _unpack_works_unwrap_re_wrapping_forward_on(BOB_DID, Some(ALICE_DID), Some(ALICE_DID)) + .await; + + _unpack_works_unwrap_re_wrapping_forward_on( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + None, + None, + ) + .await; + + _unpack_works_unwrap_re_wrapping_forward_on( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + None, + Some(ALICE_DID), + ) + .await; + + _unpack_works_unwrap_re_wrapping_forward_on( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + Some(ALICE_DID), + None, + ) + .await; + + _unpack_works_unwrap_re_wrapping_forward_on( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + Some(ALICE_DID), + Some(ALICE_DID), + ) + .await; + + async fn _unpack_works_unwrap_re_wrapping_forward_on( + to: &str, + from: Option<&str>, + sign_by: Option<&str>, + ) { + let mut did_resolver = ExampleDIDResolver::new(vec![ + ALICE_DID_DOC.clone(), + BOB_DID_DOC.clone(), + MEDIATOR1_DID_DOC.clone(), + ]); + + let alice_secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let bob_secrets_resolver = ExampleSecretsResolver::new(BOB_SECRETS.clone()); + + let mediator1_secrets_resolver = ExampleSecretsResolver::new(MEDIATOR1_SECRETS.clone()); + + let (msg, pack_metadata) = MESSAGE_SIMPLE + .pack_encrypted( + to, + from, + sign_by, + &did_resolver, + &alice_secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + .expect("Unable encrypt"); + + assert_eq!( + pack_metadata.messaging_service.as_ref(), + Some(&MessagingServiceMetadata { + id: BOB_SERVICE.id.clone(), + service_endpoint: BOB_DID_COMM_MESSAGING_SERVICE.uri.clone(), + }) + ); + + let did_method_resolver = DIDMethods::default(); + let (unpacked_msg_mediator1, unpack_metadata_mediator1) = Message::unpack_string( + &msg, + &mut did_resolver, + &did_method_resolver, + &mediator1_secrets_resolver, + &UnpackOptions::default(), + ) + .await + .expect("Unable unpack"); + + let forward = + try_parse_forward(&unpacked_msg_mediator1).expect("Message is not Forward"); + + assert_eq!(forward.msg, &unpacked_msg_mediator1); + assert_eq!(&forward.next, to); + + assert!(unpack_metadata_mediator1.encrypted); + assert!(!unpack_metadata_mediator1.authenticated); + assert!(!unpack_metadata_mediator1.non_repudiation); + assert!(unpack_metadata_mediator1.anonymous_sender); + assert!(!unpack_metadata_mediator1.re_wrapped_in_forward); + + let forwarded_msg = serde_json::to_string(&forward.forwarded_msg) + .expect("Unable serialize forwarded message"); + + let re_wrapping_forward_msg = wrap_in_forward( + &forwarded_msg, + None, + to, + &[to.to_owned()], + &AnonCryptAlg::default(), + &did_resolver, + ) + .await + .expect("Unable wrap in forward"); + + let (unpacked_msg, unpack_metadata) = Message::unpack_string( + &re_wrapping_forward_msg, + &mut did_resolver, + &did_method_resolver, + &bob_secrets_resolver, + &UnpackOptions::default(), + ) + .await + .expect("Unable unpack"); + + assert_eq!(&unpacked_msg, &*MESSAGE_SIMPLE); + assert!(unpack_metadata.re_wrapped_in_forward); + } + } + + #[tokio::test] + async fn unpack_works_unwrap_re_wrapping_forward_off() { + _unpack_works_unwrap_re_wrapping_forward_off(BOB_DID, None, None).await; + + _unpack_works_unwrap_re_wrapping_forward_off(BOB_DID, None, Some(ALICE_DID)).await; + + _unpack_works_unwrap_re_wrapping_forward_off(BOB_DID, Some(ALICE_DID), None).await; + + _unpack_works_unwrap_re_wrapping_forward_off(BOB_DID, Some(ALICE_DID), Some(ALICE_DID)) + .await; + + _unpack_works_unwrap_re_wrapping_forward_off( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + None, + None, + ) + .await; + + _unpack_works_unwrap_re_wrapping_forward_off( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + None, + Some(ALICE_DID), + ) + .await; + + _unpack_works_unwrap_re_wrapping_forward_off( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + Some(ALICE_DID), + None, + ) + .await; + + _unpack_works_unwrap_re_wrapping_forward_off( + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + Some(ALICE_DID), + Some(ALICE_DID), + ) + .await; + + async fn _unpack_works_unwrap_re_wrapping_forward_off( + to: &str, + from: Option<&str>, + sign_by: Option<&str>, + ) { + let mut did_resolver = ExampleDIDResolver::new(vec![ + ALICE_DID_DOC.clone(), + BOB_DID_DOC.clone(), + MEDIATOR1_DID_DOC.clone(), + ]); + + let alice_secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let bob_secrets_resolver = ExampleSecretsResolver::new(BOB_SECRETS.clone()); + + let mediator1_secrets_resolver = ExampleSecretsResolver::new(MEDIATOR1_SECRETS.clone()); + + let (msg, pack_metadata) = MESSAGE_SIMPLE + .pack_encrypted( + to, + from, + sign_by, + &did_resolver, + &alice_secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + .expect("Unable encrypt"); + + assert_eq!( + pack_metadata.messaging_service.as_ref(), + Some(&MessagingServiceMetadata { + id: BOB_SERVICE.id.clone(), + service_endpoint: BOB_DID_COMM_MESSAGING_SERVICE.uri.clone(), + }) + ); + + let did_method_resolver = DIDMethods::default(); + let (unpacked_msg_mediator1, unpack_metadata_mediator1) = Message::unpack_string( + &msg, + &mut did_resolver, + &did_method_resolver, + &mediator1_secrets_resolver, + &UnpackOptions { + unwrap_re_wrapping_forward: false, + ..UnpackOptions::default() + }, + ) + .await + .expect("Unable unpack"); + + let forward_at_mediator1 = + try_parse_forward(&unpacked_msg_mediator1).expect("Message is not Forward"); + + assert_eq!(forward_at_mediator1.msg, &unpacked_msg_mediator1); + assert_eq!(&forward_at_mediator1.next, to); + + assert!(unpack_metadata_mediator1.encrypted); + assert!(!unpack_metadata_mediator1.authenticated); + assert!(!unpack_metadata_mediator1.non_repudiation); + assert!(unpack_metadata_mediator1.anonymous_sender); + assert!(!unpack_metadata_mediator1.re_wrapped_in_forward); + + let forwarded_msg_at_mediator1 = + serde_json::to_string(&forward_at_mediator1.forwarded_msg) + .expect("Unable serialize forwarded message"); + + let re_wrapping_forward_msg = wrap_in_forward( + &forwarded_msg_at_mediator1, + None, + to, + &[to.to_owned()], + &AnonCryptAlg::default(), + &did_resolver, + ) + .await + .expect("Unable wrap in forward"); + + let (unpacked_once_msg, unpack_once_metadata) = Message::unpack_string( + &re_wrapping_forward_msg, + &mut did_resolver, + &did_method_resolver, + &bob_secrets_resolver, + &UnpackOptions { + unwrap_re_wrapping_forward: false, + ..UnpackOptions::default() + }, + ) + .await + .expect("Unable unpack"); + + let forward_at_bob = + try_parse_forward(&unpacked_once_msg).expect("Message is not Forward"); + + assert_eq!(forward_at_bob.msg, &unpacked_once_msg); + assert_eq!(&forward_at_bob.next, to); + + assert!(unpack_once_metadata.encrypted); + assert!(!unpack_once_metadata.authenticated); + assert!(!unpack_once_metadata.non_repudiation); + assert!(unpack_once_metadata.anonymous_sender); + assert!(!unpack_once_metadata.re_wrapped_in_forward); + + let forwarded_msg_at_bob = serde_json::to_string(&forward_at_bob.forwarded_msg) + .expect("Unable serialize forwarded message"); + + let (unpacked_twice_msg, unpack_twice_metadata) = Message::unpack_string( + &forwarded_msg_at_bob, + &mut did_resolver, + &did_method_resolver, + &bob_secrets_resolver, + &UnpackOptions { + unwrap_re_wrapping_forward: false, + ..UnpackOptions::default() + }, + ) + .await + .expect("Unable unpack"); + + assert_eq!(&unpacked_twice_msg, &*MESSAGE_SIMPLE); + + assert!(unpack_twice_metadata.encrypted); + assert_eq!( + unpack_twice_metadata.authenticated, + from.is_some() || sign_by.is_some() + ); + assert_eq!(unpack_twice_metadata.non_repudiation, sign_by.is_some()); + assert_eq!(unpack_twice_metadata.anonymous_sender, from.is_none()); + assert!(!unpack_twice_metadata.re_wrapped_in_forward); + } + } + + #[tokio::test] + async fn unpack_works_anoncrypted_2way() { + _unpack_works_anoncrypted_2way( + &MESSAGE_SIMPLE, + BOB_DID, + &[ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3.id, + ], + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + ) + .await; + + _unpack_works_anoncrypted_2way( + &MESSAGE_SIMPLE, + BOB_DID, + &[ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3.id, + ], + AnonCryptAlg::Xc20pEcdhEsA256kw, + ) + .await; + + _unpack_works_anoncrypted_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id], + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + ) + .await; + + _unpack_works_anoncrypted_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id], + AnonCryptAlg::A256gcmEcdhEsA256kw, + ) + .await; + + _unpack_works_anoncrypted_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id], + AnonCryptAlg::Xc20pEcdhEsA256kw, + ) + .await; + + _unpack_works_anoncrypted_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id], + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + ) + .await; + + _unpack_works_anoncrypted_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id], + AnonCryptAlg::A256gcmEcdhEsA256kw, + ) + .await; + + _unpack_works_anoncrypted_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id], + AnonCryptAlg::Xc20pEcdhEsA256kw, + ) + .await; + + _unpack_works_anoncrypted_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id], + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + ) + .await; + + _unpack_works_anoncrypted_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id], + AnonCryptAlg::A256gcmEcdhEsA256kw, + ) + .await; + + _unpack_works_anoncrypted_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id], + AnonCryptAlg::Xc20pEcdhEsA256kw, + ) + .await; + + _unpack_works_anoncrypted_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id], + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + ) + .await; + + _unpack_works_anoncrypted_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id], + AnonCryptAlg::A256gcmEcdhEsA256kw, + ) + .await; + + _unpack_works_anoncrypted_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id], + AnonCryptAlg::Xc20pEcdhEsA256kw, + ) + .await; + + async fn _unpack_works_anoncrypted_2way( + msg: &Message, + to: &str, + to_kids: &[&str], + enc_alg: AnonCryptAlg, + ) { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let (packed, _) = msg + .pack_encrypted( + to, + None, + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + enc_alg_anon: enc_alg.clone(), + ..PackEncryptedOptions::default() + }, + ) + .await + .expect("Unable pack_encrypted"); + + _verify_unpack( + &packed, + msg, + &UnpackMetadata { + sign_from: None, + sign_alg: None, + signed_message: None, + anonymous_sender: true, + authenticated: false, + non_repudiation: false, + encrypted: true, + enc_alg_auth: None, + enc_alg_anon: Some(enc_alg), + encrypted_from_kid: None, + encrypted_to_kids: Some(to_kids.iter().map(|&k| k.to_owned()).collect()), + from_prior_issuer_kid: None, + from_prior: None, + re_wrapped_in_forward: false, + }, + ) + .await; + } + } + + #[tokio::test] + async fn pack_encrypted_works_anoncrypted_signed() { + _pack_encrypted_works_anoncrypted_signed( + &MESSAGE_SIMPLE, + BOB_DID, + &[ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3.id, + ], + ALICE_DID, + &ALICE_AUTH_METHOD_25519.id, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + SignAlg::EdDSA, + ) + .await; + + _pack_encrypted_works_anoncrypted_signed( + &MESSAGE_SIMPLE, + BOB_DID, + &[ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3.id, + ], + ALICE_DID, + &ALICE_AUTH_METHOD_25519.id, + AnonCryptAlg::A256gcmEcdhEsA256kw, + SignAlg::EdDSA, + ) + .await; + + _pack_encrypted_works_anoncrypted_signed( + &MESSAGE_SIMPLE, + BOB_DID, + &[ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3.id, + ], + ALICE_DID, + &ALICE_AUTH_METHOD_25519.id, + AnonCryptAlg::Xc20pEcdhEsA256kw, + SignAlg::EdDSA, + ) + .await; + + _pack_encrypted_works_anoncrypted_signed( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id], + &ALICE_AUTH_METHOD_25519.id, + &ALICE_AUTH_METHOD_25519.id, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + SignAlg::EdDSA, + ) + .await; + + _pack_encrypted_works_anoncrypted_signed( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id], + &ALICE_AUTH_METHOD_P256.id, + &ALICE_AUTH_METHOD_P256.id, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + SignAlg::ES256, + ) + .await; + + _pack_encrypted_works_anoncrypted_signed( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id], + &ALICE_AUTH_METHOD_SECPP256K1.id, + &ALICE_AUTH_METHOD_SECPP256K1.id, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + SignAlg::ES256K, + ) + .await; + + async fn _pack_encrypted_works_anoncrypted_signed( + msg: &Message, + to: &str, + to_kids: &[&str], + sign_by: &str, + sign_by_kid: &str, + enc_alg: AnonCryptAlg, + sign_alg: SignAlg, + ) { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let (packed, _) = msg + .pack_encrypted( + to, + None, + Some(sign_by), + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + enc_alg_anon: enc_alg.clone(), + ..PackEncryptedOptions::default() + }, + ) + .await + .expect("Unable pack_encrypted"); + + _verify_unpack_undeterministic( + &packed, + msg, + &UnpackMetadata { + sign_from: Some(sign_by_kid.into()), + sign_alg: Some(sign_alg), + signed_message: None, + anonymous_sender: true, + authenticated: true, + non_repudiation: true, + encrypted: true, + enc_alg_auth: None, + enc_alg_anon: Some(enc_alg), + encrypted_from_kid: None, + encrypted_to_kids: Some(to_kids.iter().map(|&k| k.to_owned()).collect()), + from_prior_issuer_kid: None, + from_prior: None, + re_wrapped_in_forward: false, + }, + ) + .await; + } + } + + #[traced_test] + #[tokio::test] + async fn unpack_works_authcrypt() { + let metadata = UnpackMetadata { + anonymous_sender: false, + authenticated: true, + non_repudiation: false, + encrypted: true, + enc_alg_auth: None, + enc_alg_anon: None, + sign_alg: None, + encrypted_from_kid: None, + encrypted_to_kids: None, + sign_from: None, + signed_message: None, + from_prior_issuer_kid: None, + from_prior: None, + re_wrapped_in_forward: false, + }; + + debug!("testing ENCRYPTED_MSG_AUTH_X25519"); + _verify_unpack( + ENCRYPTED_MSG_AUTH_X25519, + &MESSAGE_SIMPLE, + &UnpackMetadata { + enc_alg_auth: Some(AuthCryptAlg::A256cbcHs512Ecdh1puA256kw), + encrypted_from_kid: Some("did:example:alice#key-x25519-1".into()), + encrypted_to_kids: Some(vec![ + "did:example:bob#key-x25519-1".into(), + "did:example:bob#key-x25519-2".into(), + "did:example:bob#key-x25519-3".into(), + ]), + ..metadata.clone() + }, + ) + .await; + + debug!("testing ENCRYPTED_MSG_AUTH_P256"); + _verify_unpack( + ENCRYPTED_MSG_AUTH_P256, + &MESSAGE_SIMPLE, + &UnpackMetadata { + enc_alg_auth: Some(AuthCryptAlg::A256cbcHs512Ecdh1puA256kw), + encrypted_from_kid: Some("did:example:alice#key-p256-1".into()), + encrypted_to_kids: Some(vec![ + "did:example:bob#key-p256-1".into(), + "did:example:bob#key-p256-2".into(), + ]), + non_repudiation: true, + sign_from: Some("did:example:alice#key-1".into()), + sign_alg: Some(SignAlg::EdDSA), + signed_message: Some(serde_json::from_str(ENCRYPTED_MSG_AUTH_P256_SIGNED).unwrap()), + ..metadata.clone() + }, + ) + .await; + + // TODO: Check hidden sender case + // TODO: Check P-384 curve support + // TODO: Check P-521 curve support + } + + #[tokio::test] + async fn unpack_works_authcrypted_2way() { + _unpack_works_authcrypted_2way( + &MESSAGE_SIMPLE, + BOB_DID, + &[ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3.id, + ], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519.id, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + ) + .await; + + _unpack_works_authcrypted_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519.id, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + ) + .await; + + _unpack_works_authcrypted_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519.id, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + ) + .await; + + _unpack_works_authcrypted_2way( + &MESSAGE_SIMPLE, + BOB_DID, + &[ + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id, + ], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + ) + .await; + + _unpack_works_authcrypted_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + ) + .await; + + _unpack_works_authcrypted_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + ) + .await; + + _unpack_works_authcrypted_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + ) + .await; + + async fn _unpack_works_authcrypted_2way( + msg: &Message, + to: &str, + to_kids: &[&str], + from: &str, + from_kid: &str, + enc_alg: AuthCryptAlg, + ) { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let (packed, _) = msg + .pack_encrypted( + to, + Some(from), + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await + .expect("Unable pack_encrypted"); + + _verify_unpack( + &packed, + msg, + &UnpackMetadata { + sign_from: None, + sign_alg: None, + signed_message: None, + anonymous_sender: false, + authenticated: true, + non_repudiation: false, + encrypted: true, + enc_alg_auth: Some(enc_alg), + enc_alg_anon: None, + encrypted_from_kid: Some(from_kid.into()), + encrypted_to_kids: Some(to_kids.iter().map(|&k| k.to_owned()).collect()), + from_prior_issuer_kid: None, + from_prior: None, + re_wrapped_in_forward: false, + }, + ) + .await; + } + } + + #[tokio::test] + async fn unpack_works_authcrypted_protected_sender_2way() { + debug!("testing anonCryptAlg(A256cbcHs512EcdhEsA256kw) authCryptAlg(A256cbcHs512Ecdh1puA256kw)"); + _unpack_works_authcrypted_protected_sender_2way( + &MESSAGE_SIMPLE, + BOB_DID, + &[ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3.id, + ], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519.id, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + ) + .await; + + _unpack_works_authcrypted_protected_sender_2way( + &MESSAGE_SIMPLE, + BOB_DID, + &[ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3.id, + ], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519.id, + AnonCryptAlg::A256gcmEcdhEsA256kw, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + ) + .await; + + _unpack_works_authcrypted_protected_sender_2way( + &MESSAGE_SIMPLE, + BOB_DID, + &[ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3.id, + ], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519.id, + AnonCryptAlg::Xc20pEcdhEsA256kw, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + ) + .await; + + _unpack_works_authcrypted_protected_sender_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519.id, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + ) + .await; + + _unpack_works_authcrypted_protected_sender_2way( + &MESSAGE_SIMPLE, + BOB_DID, + &[ + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id, + ], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + ) + .await; + + _unpack_works_authcrypted_protected_sender_2way( + &MESSAGE_SIMPLE, + BOB_DID, + &[ + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id, + ], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + AnonCryptAlg::A256gcmEcdhEsA256kw, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + ) + .await; + + _unpack_works_authcrypted_protected_sender_2way( + &MESSAGE_SIMPLE, + BOB_DID, + &[ + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id, + ], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + AnonCryptAlg::Xc20pEcdhEsA256kw, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + ) + .await; + + _unpack_works_authcrypted_protected_sender_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + AnonCryptAlg::Xc20pEcdhEsA256kw, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + ) + .await; + + _unpack_works_authcrypted_protected_sender_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + ) + .await; + + async fn _unpack_works_authcrypted_protected_sender_2way( + msg: &Message, + to: &str, + to_kids: &[&str], + from: &str, + from_kid: &str, + enc_alg_anon: AnonCryptAlg, + enc_alg_auth: AuthCryptAlg, + ) { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let (packed, _) = msg + .pack_encrypted( + to, + Some(from), + None, + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: enc_alg_anon.clone(), + ..PackEncryptedOptions::default() + }, + ) + .await + .expect("Unable pack_encrypted"); + + _verify_unpack( + &packed, + msg, + &UnpackMetadata { + sign_from: None, + sign_alg: None, + signed_message: None, + anonymous_sender: true, + authenticated: true, + non_repudiation: false, + encrypted: true, + enc_alg_auth: Some(enc_alg_auth), + enc_alg_anon: Some(enc_alg_anon), + encrypted_from_kid: Some(from_kid.into()), + encrypted_to_kids: Some(to_kids.iter().map(|&k| k.to_owned()).collect()), + from_prior_issuer_kid: None, + from_prior: None, + re_wrapped_in_forward: false, + }, + ) + .await; + } + } + + #[tokio::test] + async fn unpack_works_authcrypted_protected_sender_signed_2way() { + _unpack_works_authcrypted_protected_sender_signed_2way( + &MESSAGE_SIMPLE, + BOB_DID, + &[ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3.id, + ], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519.id, + &ALICE_AUTH_METHOD_P256.id, + &ALICE_AUTH_METHOD_P256.id, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + SignAlg::ES256, + ) + .await; + + _unpack_works_authcrypted_protected_sender_signed_2way( + &MESSAGE_SIMPLE, + BOB_DID, + &[ + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id, + ], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_AUTH_METHOD_25519.id, + &ALICE_AUTH_METHOD_25519.id, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + SignAlg::EdDSA, + ) + .await; + + _unpack_works_authcrypted_protected_sender_signed_2way( + &MESSAGE_SIMPLE, + BOB_DID, + &[ + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id, + ], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_AUTH_METHOD_SECPP256K1.id, + &ALICE_AUTH_METHOD_SECPP256K1.id, + AnonCryptAlg::A256cbcHs512EcdhEsA256kw, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + SignAlg::ES256K, + ) + .await; + + #[allow(clippy::too_many_arguments)] + async fn _unpack_works_authcrypted_protected_sender_signed_2way( + msg: &Message, + to: &str, + _: &[&str], + from: &str, + _: &str, + sign_by: &str, + _: &str, + enc_alg_anon: AnonCryptAlg, + _: AuthCryptAlg, + _: SignAlg, + ) { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let (_, _) = msg + .pack_encrypted( + to, + Some(from), + Some(sign_by), + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + protect_sender: true, + enc_alg_anon: enc_alg_anon.clone(), + ..PackEncryptedOptions::default() + }, + ) + .await + .expect("Unable pack_encrypted"); + } + } + + #[tokio::test] + async fn unpack_works_authcrypted_signed_2way() { + _unpack_works_authcrypted_signed_2way( + &MESSAGE_SIMPLE, + BOB_DID, + &[ + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3.id, + ], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519.id, + ALICE_DID, + &ALICE_AUTH_METHOD_25519.id, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + SignAlg::EdDSA, + ) + .await; + + _unpack_works_authcrypted_signed_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519.id, + &ALICE_AUTH_METHOD_25519.id, + &ALICE_AUTH_METHOD_25519.id, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + SignAlg::EdDSA, + ) + .await; + + _unpack_works_authcrypted_signed_2way( + &MESSAGE_SIMPLE, + &BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + &[&BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id], + ALICE_DID, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519.id, + &ALICE_AUTH_METHOD_P256.id, + &ALICE_AUTH_METHOD_P256.id, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + SignAlg::ES256, + ) + .await; + + _unpack_works_authcrypted_signed_2way( + &MESSAGE_SIMPLE, + BOB_DID, + &[ + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + &BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.id, + ], + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + &ALICE_AUTH_METHOD_SECPP256K1.id, + &ALICE_AUTH_METHOD_SECPP256K1.id, + AuthCryptAlg::A256cbcHs512Ecdh1puA256kw, + SignAlg::ES256K, + ) + .await; + + #[allow(clippy::too_many_arguments)] + async fn _unpack_works_authcrypted_signed_2way( + msg: &Message, + to: &str, + _: &[&str], + from: &str, + _: &str, + sign_by: &str, + _: &str, + _: AuthCryptAlg, + _: SignAlg, + ) { + let did_resolver = + ExampleDIDResolver::new(vec![ALICE_DID_DOC.clone(), BOB_DID_DOC.clone()]); + + let secrets_resolver = ExampleSecretsResolver::new(ALICE_SECRETS.clone()); + + let (_, _) = msg + .pack_encrypted( + to, + Some(from), + Some(sign_by), + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + ) + .await + .expect("encrypt is ok."); + } + } + + #[tokio::test] + async fn unpack_works_invalid_epk_point() { + _verify_unpack_malformed( + INVALID_ENCRYPTED_MSG_ANON_P256_EPK_WRONG_POINT, + "Malformed: Unable instantiate epk: Unable produce jwk: Invalid key data", + ) + .await; + } + + #[tokio::test] + async fn unpack_works_malformed_anoncrypt_msg() { + _verify_unpack_malformed( + update_field(ENCRYPTED_MSG_ANON_XC20P_1, "protected", "invalid").as_str(), + "Malformed: Unable decode protected header: Invalid last symbol 100, offset 6.", + ) + .await; + + _verify_unpack_malformed( + remove_field(ENCRYPTED_MSG_ANON_XC20P_1, "protected").as_str(), + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + update_field(ENCRYPTED_MSG_ANON_XC20P_1, "iv", "invalid").as_str(), + "Malformed: Unable decode iv: Invalid last symbol 100, offset 6.", + ) + .await; + + _verify_unpack_malformed( + remove_field(ENCRYPTED_MSG_ANON_XC20P_1, "iv").as_str(), + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + update_field(ENCRYPTED_MSG_ANON_XC20P_1, "ciphertext", "invalid").as_str(), + "Malformed: Unable decode ciphertext: Invalid last symbol 100, offset 6.", + ) + .await; + + _verify_unpack_malformed( + remove_field(ENCRYPTED_MSG_ANON_XC20P_1, "ciphertext").as_str(), + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + update_field(ENCRYPTED_MSG_ANON_XC20P_1, "tag", "invalid").as_str(), + "Malformed: Unable decode tag: Invalid last symbol 100, offset 6.", + ) + .await; + + _verify_unpack_malformed( + remove_field(ENCRYPTED_MSG_ANON_XC20P_1, "tag").as_str(), + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + update_protected_field(ENCRYPTED_MSG_ANON_XC20P_1, "apv", "invalid").as_str(), + "Malformed: Unable decode apv: Invalid last symbol 100, offset 6.", + ) + .await; + + _verify_unpack_malformed( + remove_protected_field(ENCRYPTED_MSG_ANON_XC20P_1, "apv").as_str(), + "Malformed: Unable parse protected header: missing field `apv` at line 1 column 166", + ) + .await; + } + + #[tokio::test] + async fn unpack_works_malformed_authcrypt_msg() { + _verify_unpack_malformed( + update_field(ENCRYPTED_MSG_AUTH_X25519, "protected", "invalid").as_str(), + "Malformed: Unable decode protected header: Invalid last symbol 100, offset 6.", + ) + .await; + + _verify_unpack_malformed( + remove_field(ENCRYPTED_MSG_AUTH_X25519, "protected").as_str(), + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + update_field(ENCRYPTED_MSG_AUTH_X25519, "iv", "invalid").as_str(), + "Malformed: Unable decode iv: Invalid last symbol 100, offset 6.", + ) + .await; + + _verify_unpack_malformed( + remove_field(ENCRYPTED_MSG_AUTH_X25519, "iv").as_str(), + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + update_field(ENCRYPTED_MSG_AUTH_X25519, "ciphertext", "invalid").as_str(), + "Malformed: Unable decode ciphertext: Invalid last symbol 100, offset 6.", + ) + .await; + + _verify_unpack_malformed( + remove_field(ENCRYPTED_MSG_AUTH_X25519, "ciphertext").as_str(), + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + update_field(ENCRYPTED_MSG_AUTH_X25519, "tag", "invalid").as_str(), + "Malformed: Unable decode tag: Invalid last symbol 100, offset 6.", + ) + .await; + + _verify_unpack_malformed( + remove_field(ENCRYPTED_MSG_AUTH_X25519, "tag").as_str(), + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + update_protected_field(ENCRYPTED_MSG_AUTH_X25519, "apv", "invalid").as_str(), + "Malformed: Unable decode apv: Invalid last symbol 100, offset 6.", + ) + .await; + + _verify_unpack_malformed( + remove_protected_field(ENCRYPTED_MSG_AUTH_X25519, "apv").as_str(), + "Malformed: Unable parse protected header: missing field `apv` at line 1 column 264", + ) + .await; + + _verify_unpack_malformed( + update_protected_field(ENCRYPTED_MSG_AUTH_X25519, "apu", "invalid").as_str(), + "Malformed: Unable decode apu: Invalid last symbol 100, offset 6.", + ) + .await; + + _verify_unpack_malformed( + remove_protected_field(ENCRYPTED_MSG_AUTH_X25519, "apu").as_str(), + "Malformed: SKID present, but no apu", + ) + .await; + } + + #[tokio::test] + async fn unpack_works_malformed_signed_msg() { + _verify_unpack_malformed( + update_field(SIGNED_MSG_ALICE_KEY_1, "payload", "invalid").as_str(), + "Malformed: Wrong signature", + ) + .await; + + _verify_unpack_malformed( + remove_field(SIGNED_MSG_ALICE_KEY_1, "payload").as_str(), + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + update_field(SIGNED_MSG_ALICE_KEY_1, "signatures", "invalid").as_str(), + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + remove_field(SIGNED_MSG_ALICE_KEY_1, "signatures").as_str(), + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + } + + #[tokio::test] + async fn unpack_works_malformed_plaintext_msg() { + _verify_unpack_malformed( + INVALID_PLAINTEXT_MSG_EMPTY, + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + INVALID_PLAINTEXT_MSG_STRING, + "Malformed: Unable deserialize envelope: expected value at line 2 column 1", + ) + .await; + + _verify_unpack_malformed( + INVALID_PLAINTEXT_MSG_NO_ID, + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + INVALID_PLAINTEXT_MSG_NO_TYPE, + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + INVALID_PLAINTEXT_MSG_NO_BODY, + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + INVALID_PLAINTEXT_MSG_WRONG_TYP, + "Malformed: `typ` must be \"application/didcomm-plain+json\"", + ) + .await; + + _verify_unpack_malformed( + INVALID_PLAINTEXT_MSG_EMPTY_ATTACHMENTS, + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + INVALID_PLAINTEXT_MSG_ATTACHMENTS_NO_DATA, + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + INVALID_PLAINTEXT_MSG_ATTACHMENTS_EMPTY_DATA, + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + INVALID_PLAINTEXT_MSG_ATTACHMENTS_LINKS_NO_HASH, + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + INVALID_PLAINTEXT_MSG_ATTACHMENTS_AS_STRING, + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + INVALID_PLAINTEXT_MSG_ATTACHMENTS_AS_INT_ARRAY, + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + INVALID_PLAINTEXT_MSG_ATTACHMENTS_WRONG_DATA, + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + INVALID_PLAINTEXT_MSG_ATTACHMENTS_WRONG_ID, + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + + _verify_unpack_malformed( + INVALID_PLAINTEXT_MSG_ATTACHMENTS_NULL_DATA, + "Malformed: Unable deserialize envelope: data did not match any variant of untagged enum Envelope", + ) + .await; + } + + #[tokio::test] + async fn unpack_plaintext_works_from_prior() { + let exp_metadata = UnpackMetadata { + anonymous_sender: false, + authenticated: false, + non_repudiation: false, + encrypted: false, + enc_alg_auth: None, + enc_alg_anon: None, + sign_alg: None, + encrypted_from_kid: None, + encrypted_to_kids: None, + sign_from: None, + signed_message: None, + from_prior_issuer_kid: Some(CHARLIE_AUTH_METHOD_25519.id.clone()), + from_prior: Some(FROM_PRIOR_FULL.clone()), + re_wrapped_in_forward: false, + }; + + _verify_unpack( + PLAINTEXT_FROM_PRIOR, + &MESSAGE_FROM_PRIOR_FULL, + &exp_metadata, + ) + .await; + } + + #[tokio::test] + async fn unpack_plaintext_works_invalid_from_prior() { + _verify_unpack_returns_error( + PLAINTEXT_INVALID_FROM_PRIOR, + ErrorKind::Malformed, + "Malformed: Unable to parse compactly serialized JWS", + ) + .await; + } + + #[tokio::test] + async fn unpack_plaintext_works_invalid_from_prior_signature() { + _verify_unpack_returns_error( + PLAINTEXT_FROM_PRIOR_INVALID_SIGNATURE, + ErrorKind::Malformed, + "Malformed: Unable to verify from_prior signature: Unable decode signature: Invalid last symbol 66, offset 85.", + ) + .await; + } + + async fn _verify_unpack(msg: &str, exp_msg: &Message, exp_metadata: &UnpackMetadata) { + let mut did_resolver = ExampleDIDResolver::new(vec![ + ALICE_DID_DOC.clone(), + BOB_DID_DOC.clone(), + CHARLIE_DID_DOC.clone(), + ]); + + let secrets_resolver = ExampleSecretsResolver::new(BOB_SECRETS.clone()); + + let did_method_resolver = DIDMethods::default(); + let (msg, metadata) = Message::unpack_string( + msg, + &mut did_resolver, + &did_method_resolver, + &secrets_resolver, + &UnpackOptions::default(), + ) + .await + .expect("unpack is ok."); + + assert_eq!(&msg, exp_msg); + assert_eq!(&metadata, exp_metadata); + } + + // Same as `_verify_unpack`, but skips indeterministic values from metadata checking + async fn _verify_unpack_undeterministic( + msg: &str, + exp_msg: &Message, + exp_metadata: &UnpackMetadata, + ) { + let mut did_resolver = ExampleDIDResolver::new(vec![ + ALICE_DID_DOC.clone(), + BOB_DID_DOC.clone(), + CHARLIE_DID_DOC.clone(), + ]); + + let secrets_resolver = ExampleSecretsResolver::new(BOB_SECRETS.clone()); + let did_method_resolver = DIDMethods::default(); + let (msg, mut metadata) = Message::unpack_string( + msg, + &mut did_resolver, + &did_method_resolver, + &secrets_resolver, + &UnpackOptions::default(), + ) + .await + .expect("unpack is ok."); + + assert_eq!(&msg, exp_msg); + + metadata + .signed_message + .clone_from(&exp_metadata.signed_message); + assert_eq!(&metadata, exp_metadata); + } + + async fn _verify_unpack_malformed(msg: &str, exp_error_str: &str) { + _verify_unpack_returns_error(msg, ErrorKind::Malformed, exp_error_str).await + } + + async fn _verify_unpack_returns_error(msg: &str, exp_err_kind: ErrorKind, exp_err_msg: &str) { + let mut did_resolver = ExampleDIDResolver::new(vec![ + ALICE_DID_DOC.clone(), + BOB_DID_DOC.clone(), + CHARLIE_DID_DOC.clone(), + ]); + + let secrets_resolver = ExampleSecretsResolver::new(BOB_SECRETS.clone()); + + let did_method_resolver = DIDMethods::default(); + let err = Message::unpack_string( + msg, + &mut did_resolver, + &did_method_resolver, + &secrets_resolver, + &UnpackOptions::default(), + ) + .await + .expect_err("res is ok"); + + assert_eq!(err.kind(), exp_err_kind); + assert_eq!(format!("{}", err), exp_err_msg); + } +} +*/ diff --git a/affinidi-messaging-didcomm/src/message/unpack/plaintext.rs b/affinidi-messaging-didcomm/src/message/unpack/plaintext.rs new file mode 100644 index 0000000..f768413 --- /dev/null +++ b/affinidi-messaging-didcomm/src/message/unpack/plaintext.rs @@ -0,0 +1,26 @@ +use affinidi_did_resolver_cache_sdk::DIDCacheClient; + +use crate::envelope::{MetaEnvelope, ParsedEnvelope}; +use crate::error::Result; +use crate::{FromPrior, Message}; + +pub(crate) async fn _try_unpack_plaintext( + msg: &ParsedEnvelope, + did_resolver: &DIDCacheClient, + envelope: &mut MetaEnvelope, +) -> Result> { + let msg = match msg { + ParsedEnvelope::Message(msg) => msg.clone().validate()?, + _ => return Ok(None), + }; + + if let Some(from_prior) = &msg.from_prior { + let (unpacked_from_prior, from_prior_issuer_kid) = + FromPrior::unpack(from_prior, did_resolver).await?; + + envelope.metadata.from_prior = Some(unpacked_from_prior); + envelope.metadata.from_prior_issuer_kid = Some(from_prior_issuer_kid); + }; + + Ok(Some(msg)) +} diff --git a/affinidi-messaging-didcomm/src/message/unpack/sign.rs b/affinidi-messaging-didcomm/src/message/unpack/sign.rs new file mode 100644 index 0000000..83dc799 --- /dev/null +++ b/affinidi-messaging-didcomm/src/message/unpack/sign.rs @@ -0,0 +1,165 @@ +use affinidi_did_resolver_cache_sdk::document::DocumentExt; +use affinidi_did_resolver_cache_sdk::DIDCacheClient; +use askar_crypto::alg::{ed25519::Ed25519KeyPair, k256::K256KeyPair, p256::P256KeyPair}; +use tracing::debug; + +use crate::document::{did_or_url, DIDCommVerificationMethodExt}; +use crate::envelope::{Envelope, MetaEnvelope, ParsedEnvelope}; +use crate::{ + algorithms::SignAlg, + error::{err_msg, ErrorKind, Result, ResultContext, ResultExt}, + jws, + utils::crypto::AsKnownKeyPair, + UnpackOptions, +}; +use base64::prelude::*; +use std::str::FromStr; + +pub(crate) async fn _try_unpack_sign( + msg: &ParsedEnvelope, + did_resolver: &DIDCacheClient, + _opts: &UnpackOptions, + envelope: &mut MetaEnvelope, +) -> Result> { + debug!( + "Is this a signed envelope? expect(JWS) actual({})", + msg.get_type() + ); + let parsed_jws: &jws::ParsedJWS = match msg { + ParsedEnvelope::Jws(jws) => jws, + _ => return Ok(None), + }; + debug!("Trying to unpack signed envelope"); + + if parsed_jws.protected.len() != 1 { + Err(err_msg( + ErrorKind::Malformed, + "Wrong amount of signatures for jws", + ))? + } + + let alg = &parsed_jws + .protected + .first() + .ok_or_else(|| { + err_msg( + ErrorKind::InvalidState, + "Unexpected absence of first protected header", + ) + })? + .alg; + + let signer_kid = &parsed_jws + .jws + .signatures + .first() + .ok_or_else(|| { + err_msg( + ErrorKind::InvalidState, + "Unexpected absence of first signature", + ) + })? + .header + .kid; + + let (signer_did, signer_url) = did_or_url(signer_kid); + + if signer_url.is_none() { + Err(err_msg( + ErrorKind::Malformed, + "Signer key can't be resolved to key agreement", + ))? + } + + let signer_ddoc = match did_resolver.resolve(signer_did).await { + Ok(response) => response.doc, + Err(_) => return Err(err_msg(ErrorKind::DIDNotResolved, "Signer did not found"))?, + }; + + if !signer_ddoc.contains_authentication(signer_kid) { + return Err(err_msg( + ErrorKind::DIDUrlNotFound, + "Signer kid not found in did", + )); + } + + let signer_key = signer_ddoc + .verification_method + .iter() + .find(|&vm| &vm.id.to_string() == signer_kid) + .ok_or_else(|| { + err_msg( + ErrorKind::DIDUrlNotFound, + "Sender verification method not found in did", + ) + })?; + + let signer_key_jwk = if let Some(jwk) = signer_key.get_jwk() { + jwk + } else { + return Err(err_msg( + ErrorKind::NoCompatibleCrypto, + "Couldn't convert signing key to JWK", + )); + }; + + let valid = match alg { + jws::Algorithm::EdDSA => { + envelope.metadata.sign_alg = Some(SignAlg::EdDSA); + + let signer_key = signer_key + .as_ed25519(&signer_key_jwk) + .context("Unable instantiate signer key")?; + + parsed_jws + .verify::((signer_kid, &signer_key)) + .context("Unable verify sign envelope")? + } + jws::Algorithm::Es256 => { + envelope.metadata.sign_alg = Some(SignAlg::ES256); + + let signer_key = signer_key + .as_p256(&signer_key_jwk) + .context("Unable instantiate signer key")?; + + parsed_jws + .verify::((signer_kid, &signer_key)) + .context("Unable verify sign envelope")? + } + jws::Algorithm::Es256K => { + envelope.metadata.sign_alg = Some(SignAlg::ES256K); + + let signer_key = signer_key + .as_k256(&signer_key_jwk) + .context("Unable instantiate signer key")?; + + parsed_jws + .verify::((signer_kid, &signer_key)) + .context("Unable verify sign envelope")? + } + jws::Algorithm::Other(_) => Err(err_msg( + ErrorKind::Unsupported, + "Unsupported signature algorithm", + ))?, + }; + + if !valid { + Err(err_msg(ErrorKind::Malformed, "Wrong signature"))? + } + + // TODO: More precise error conversion + let payload = BASE64_URL_SAFE_NO_PAD + .decode(&parsed_jws.jws.payload) + .kind(ErrorKind::Malformed, "Signed payload is invalid base64")?; + + let payload = + String::from_utf8(payload).kind(ErrorKind::Malformed, "Signed payload is invalid utf8")?; + + envelope.metadata.authenticated = true; + envelope.metadata.non_repudiation = true; + envelope.metadata.sign_from = Some(signer_kid.into()); + envelope.metadata.signed_message = Some(parsed_jws.jws.clone()); + + let e = Envelope::from_str(&payload)?.parse()?.verify_didcomm()?; + Ok(Some(e)) +} diff --git a/affinidi-messaging-didcomm/src/protocols/mod.rs b/affinidi-messaging-didcomm/src/protocols/mod.rs new file mode 100644 index 0000000..e78cad0 --- /dev/null +++ b/affinidi-messaging-didcomm/src/protocols/mod.rs @@ -0,0 +1 @@ +pub mod routing; diff --git a/affinidi-messaging-didcomm/src/protocols/routing/forward.rs b/affinidi-messaging-didcomm/src/protocols/routing/forward.rs new file mode 100644 index 0000000..7222e7f --- /dev/null +++ b/affinidi-messaging-didcomm/src/protocols/routing/forward.rs @@ -0,0 +1,12 @@ +use serde::Serialize; +use serde_json::Value; + +use crate::Message; + +/// Utility structure providing convinient access to Forward plaintext message fields. +#[derive(Debug, PartialEq, Eq, Serialize, Clone)] +pub struct ParsedForward<'a> { + pub msg: &'a Message, + pub next: String, + pub forwarded_msg: Value, +} diff --git a/affinidi-messaging-didcomm/src/protocols/routing/mod.rs b/affinidi-messaging-didcomm/src/protocols/routing/mod.rs new file mode 100644 index 0000000..5c2aa64 --- /dev/null +++ b/affinidi-messaging-didcomm/src/protocols/routing/mod.rs @@ -0,0 +1,417 @@ +mod forward; + +use std::collections::HashMap; + +use affinidi_did_resolver_cache_sdk::DIDCacheClient; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use ssi::dids::{ + document::{service::Endpoint, Service}, + Document, +}; +use tracing::warn; +use uuid::Uuid; + +use crate::{ + algorithms::AnonCryptAlg, + document::is_did, + error::{err_msg, ErrorKind, Result, ResultExt}, + message::{anoncrypt, MessagingServiceMetadata}, + Attachment, AttachmentData, Message, PackEncryptedOptions, +}; + +pub use self::forward::ParsedForward; + +pub(crate) const FORWARD_MSG_TYPE: &str = "https://didcomm.org/routing/2.0/forward"; + +pub(crate) const DIDCOMM_V2_PROFILE: &str = "didcomm/v2"; + +/// Properties for DIDCommMessagingService +/// (https://identity.foundation/didcomm-messaging/spec/#did-document-service-endpoint). +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct DIDCommMessagingService { + pub uri: String, + + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub accept: Option>, + + #[serde(default)] + pub routing_keys: Vec, +} + +/// Service type must match `DIDCommMessaging` +fn check_service_type(service: &Service) -> Result<()> { + // Check of the service_endpoint is of type DIDCommMessaging + if service + .type_ + .first() + .map(|t| t == "DIDCommMessaging") + .is_some() + { + Ok(()) + } else { + Err(err_msg( + ErrorKind::InvalidState, + format!( + "Service ({}) is not type `DIDCommMessaging. Instead it is ({})", + service.id, + service.type_.first().unwrap_or(&"".to_string()) + ), + )) + } +} + +/// Checks if a defined service meets the criteria for a DIDComm v2 service +fn check_service(service: &Service) -> Result> { + check_service_type(service)?; + + if let Some(service_endpoint) = &service.service_endpoint { + // Only accepts DIDComm Version 2 specification + // Check that this service endpoint supports didcomm/v2 + + let endpoint = service_endpoint.into_iter().find(|endpoint| { + if let Endpoint::Map(ref value) = endpoint { + if let Some(accept) = value.get("accept") { + let a: Vec = match serde_json::from_value(accept.clone()) { + Ok(value) => value, + Err(e) => { + warn!( + "Error parsing accept field ({:?}) on service id({}): {}", + accept, service.id, e + ); + return false; + } + }; + + a.contains(&DIDCOMM_V2_PROFILE.to_string()) + } else { + // If accept is not defined, then it is assumed that it accepts all profiles + true + } + } else { + false + } + }); + + if let Some(endpoint) = endpoint { + let value = match endpoint { + Endpoint::Map(map) => map, + _ => { + return Err(err_msg( + ErrorKind::InvalidState, + format!( + "Service ({}) has an invalid serviceEndpoint definition", + service.id + ), + )); + } + }; + + let found_service: DIDCommMessagingService = serde_json::from_value(value.clone()) + .map_err(|_| { + err_msg( + ErrorKind::InvalidState, + format!( + "Service ({}) has an invalid serviceEndpoint definition", + service.id + ), + ) + })?; + + Ok(Some((service.id.to_string(), found_service))) + } else { + Err(err_msg( + ErrorKind::IllegalArgument, + "Service with the specified ID does not accept didcomm/v2 profile", + )) + } + } else { + // if there is no serviceEndpoint, then we can't proceed + + Err(err_msg( + ErrorKind::InvalidState, + format!( + "Service ({}) has no serviceEndpoint definitions", + service.id + ), + )) + } +} + +/// Returns a Service Endpoint definition if it exists +/// If a service_id is specified, then we look for that explicitly +/// service_id: is a DID URL fragment that refers to a specific service endpoint in the DID document. +/// E.g. did:peer:2...#service-1 should be specified as service_id = "service-1" +/// +/// Returns +/// - Ok(None) if no service endpoint can be found and no service_id was given +/// - Ok(DIDCommMessagingService) - service endpoint found +/// - Err(err) - service_id provided, but couldn't find/match to it. +fn find_did_comm_service( + did_doc: &Document, + service_id: Option<&str>, +) -> Result> { + match service_id { + Some(service_id) => { + let service = did_doc.service(service_id).ok_or_else(|| { + err_msg( + ErrorKind::InvalidState, + "Service with the specified ID not found in the DID document", + ) + })?; + + check_service(service) + } + None => { + let service = did_doc + .service + .iter() + .find_map(|service| match check_service(service) { + Ok(service) => service, + Err(_) => None, + }); + + Ok(service) + } + } +} + +async fn resolve_did_comm_services_chain( + to: &str, + service_id: Option<&str>, + resolver: &DIDCacheClient, +) -> Result> { + let result = resolver.resolve(to).await.map_err(|e| { + err_msg( + ErrorKind::DIDNotResolved, + format!("Couldn't resolve DID({}). Reason: {}", to, e), + ) + })?; + + let service = find_did_comm_service(&result.doc, service_id)?; + + if service.is_none() { + return Ok(vec![]); + } + + let mut service = service.unwrap(); + + let mut services = vec![service.clone()]; + let mut service_endpoint = &service.1.uri; + + while is_did(service_endpoint) { + // Now alternative endpoints recursion is not supported + // (because it should not be used according to the specification) + if services.len() > 1 { + return Err(err_msg( + ErrorKind::InvalidState, + "DID doc defines alternative endpoints recursively", + )); + } + + let resolved = resolver.resolve(service_endpoint).await.map_err(|e| { + err_msg( + ErrorKind::DIDNotResolved, + format!("Couldn't resolve DID({}). Reason: {}", to, e), + ) + })?; + + service = find_did_comm_service(&resolved.doc, None)?.ok_or_else(|| { + err_msg( + // TODO: Think on introducing a more appropriate error kind + ErrorKind::InvalidState, + "Referenced mediator does not provide any DIDCommMessaging services", + ) + })?; + + services.insert(0, service.clone()); + service_endpoint = &service.1.uri; + } + + Ok(services) +} + +fn generate_message_id() -> String { + Uuid::new_v4().to_string() +} + +fn build_forward_message( + forwarded_msg: &str, + next: &str, + headers: Option<&HashMap>, +) -> Result { + let body = json!({ "next": next }); + + // TODO: Think how to avoid extra deserialization of forwarded_msg here. + // (This deserializtion is a double work because the whole Forward message with the attachments + // will then be serialized.) + let attachment = Attachment::json( + serde_json::from_str(forwarded_msg) + .kind(ErrorKind::Malformed, "Unable deserialize forwarded message")?, + ) + .finalize(); + + let mut msg_builder = Message::build(generate_message_id(), FORWARD_MSG_TYPE.to_owned(), body); + + if let Some(headers) = headers { + for (name, value) in headers { + msg_builder = msg_builder.header(name.to_owned(), value.to_owned()); + } + } + + msg_builder = msg_builder.attachment(attachment); + + let msg = msg_builder.finalize(); + + serde_json::to_string(&msg).kind(ErrorKind::InvalidState, "Unable serialize forward message") +} + +/// Tries to parse plaintext message into `ParsedForward` structure if the message is Forward. +/// (https://identity.foundation/didcomm-messaging/spec/#messages) +/// +/// # Parameters +/// - `msg` plaintext message to try to parse into `ParsedForward` structure +/// +/// # Returns +/// `Some` with `ParsedForward` structure if `msg` is Forward message, otherwise `None`. +pub fn try_parse_forward(msg: &Message) -> Option { + if msg.type_ != FORWARD_MSG_TYPE { + return None; + } + + let next = match msg.body { + Value::Object(ref body) => match body.get("next") { + Some(Value::String(ref next)) => Some(next), + _ => None, + }, + _ => None, + }; + + next?; + + let next = next.unwrap(); + + let json_attachment_data = match msg.attachments { + Some(ref attachments) => match &attachments[..] { + [attachment, ..] => match &attachment.data { + AttachmentData::Json { ref value } => Some(value), + _ => None, + }, + _ => None, + }, + None => None, + }; + + json_attachment_data?; + + let forwarded_msg = &json_attachment_data.unwrap().json; + + Some(ParsedForward { + msg, + next: next.clone(), + forwarded_msg: forwarded_msg.clone(), + }) +} + +/// Wraps an anoncrypt or authcrypt message into a Forward onion (nested Forward messages). +/// https://identity.foundation/didcomm-messaging/spec/#messages +/// +/// # Parameters +/// - `msg` Anoncrypt or authcrypt message to wrap into Forward onion. +/// - `headers` (optional) Additional headers to each Forward message of the onion. +/// - `to` Recipient (a key identifier or DID) of the message being wrapped into Forward onion. +/// - `routing_keys` Routing keys (each one is a key identifier or DID) to use for encryption of +/// Forward messages in the onion. The keys must be ordered along the route (so in the opposite +/// direction to the wrapping steps). +/// - `enc_alg_anon` Algorithm to use for wrapping into each Forward message of the onion. +/// - `did_resolver` instance of `DIDResolver` to resolve DIDs. +/// - `to_kids_limit` maximum number of kids in a single recipient did. +/// +/// # Returns +/// `Result` with the message wrapped into Forward onion or `Error`. +/// +/// # Errors +/// - `Malformed` The message to wrap is malformed. +/// - `DIDNotResolved` Issuer DID not found. +/// - `DIDUrlNotFound` Issuer authentication verification method is not found. +/// - `Unsupported` Used crypto or method is unsupported. +/// - `InvalidState` Indicates a library error. +pub async fn wrap_in_forward( + msg: &str, + headers: Option<&HashMap>, + to: &str, + routing_keys: &[String], + enc_alg_anon: &AnonCryptAlg, + did_resolver: &DIDCacheClient, + to_kids_limit: usize, +) -> Result { + let mut tos = routing_keys.to_vec(); + + let mut nexts = tos.clone(); + nexts.remove(0); + nexts.push(to.to_owned()); + + tos.reverse(); + nexts.reverse(); + + let mut msg = msg.to_owned(); + + for (to_, next_) in tos.iter().zip(nexts.iter()) { + msg = build_forward_message(&msg, next_, headers)?; + msg = anoncrypt(to_, did_resolver, msg.as_bytes(), enc_alg_anon, to_kids_limit) + .await? + .0; + } + + Ok(msg) +} + +pub(crate) async fn wrap_in_forward_if_needed( + msg: &str, + to: &str, + did_resolver: &DIDCacheClient, + options: &PackEncryptedOptions, +) -> Result> { + if !options.forward { + return Ok(None); + } + + let services_chain = + resolve_did_comm_services_chain(to, options.messaging_service.as_deref(), did_resolver) + .await?; + + if services_chain.is_empty() { + return Ok(None); + } + + let mut routing_keys = services_chain[1..] + .iter() + .map(|service| service.1.uri.clone()) + .collect::>(); + + routing_keys.append(&mut services_chain.last().unwrap().1.routing_keys.clone()); + + if routing_keys.is_empty() { + return Ok(None); + } + + let forward_msg = wrap_in_forward( + msg, + options.forward_headers.as_ref(), + to, + &routing_keys, + &options.enc_alg_anon, + did_resolver, + options.to_kids_limit, + ) + .await?; + + let messaging_service = MessagingServiceMetadata { + id: services_chain.last().unwrap().0.clone(), + service_endpoint: services_chain.first().unwrap().1.uri.clone(), + }; + + Ok(Some((forward_msg, messaging_service))) +} diff --git a/affinidi-messaging-didcomm/src/secrets/mod.rs b/affinidi-messaging-didcomm/src/secrets/mod.rs new file mode 100644 index 0000000..69a2947 --- /dev/null +++ b/affinidi-messaging-didcomm/src/secrets/mod.rs @@ -0,0 +1,108 @@ +//! Set of interfaces that allow access to DID Document secrets + +pub mod resolvers; + +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +use crate::error::Result; + +/// Interface for secrets resolver. +/// Resolves secrets such as private keys to be used for signing and encryption. +#[cfg(feature = "uniffi")] +#[async_trait] +pub trait SecretsResolver: Sync { + /// Finds secret (usually private key) identified by the given key ID. + /// + /// # Parameters + /// - `secret_id` the ID (in form of DID URL) identifying a secret + /// + /// # Returns + /// A secret (usually private key) or None of there is no secret for the given ID + /// + /// # Errors + /// - IOError + /// - InvalidState + async fn get_secret(&self, secret_id: &str) -> Result>; + + /// Find all secrets that have one of the given IDs. + /// Return secrets only for key IDs for which a secret is present. + /// + /// # Parameters + /// - `secret_ids` the IDs find secrets for + /// + /// # Returns + /// possible empty list of all secrets that have one of the given IDs. + async fn find_secrets<'a>(&self, secret_ids: &'a [&'a str]) -> Result>; +} + +/// Interface for secrets resolver. +/// Resolves secrets such as private keys to be used for signing and encryption. +#[cfg(not(feature = "uniffi"))] +#[async_trait] +pub trait SecretsResolver: Sync + Send { + /// Finds secret (usually private key) identified by the given key ID. + /// + /// # Parameters + /// - `secret_id` the ID (in form of DID URL) identifying a secret + /// + /// # Returns + /// A secret (usually private key) or None of there is no secret for the given ID + /// + /// # Errors + /// - IOError + /// - InvalidState + async fn get_secret(&self, secret_id: &str) -> Result>; + + /// Find all secrets that have one of the given IDs. + /// Return secrets only for key IDs for which a secret is present. + /// + /// # Parameters + /// - `secret_ids` the IDs find secrets for + /// + /// # Returns + /// possible empty list of all secrets that have one of the given IDs. + async fn find_secrets(&self, secret_ids: &[String]) -> Result>; +} + +/// Represents secret. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct Secret { + /// A key ID identifying a secret (private key). + pub id: String, + + /// Must have the same semantics as type ('type' field) of the corresponding method in DID Doc containing a public key. + #[serde(rename = "type")] + pub type_: SecretType, + + /// Value of the secret (private key) + #[serde(flatten)] + pub secret_material: SecretMaterial, +} + +/// Must have the same semantics as type ('type' field) of the corresponding method in DID Doc containing a public key. +#[derive(Debug, Clone, Deserialize, Serialize)] +pub enum SecretType { + JsonWebKey2020, + X25519KeyAgreementKey2019, + X25519KeyAgreementKey2020, + Ed25519VerificationKey2018, + Ed25519VerificationKey2020, + EcdsaSecp256k1VerificationKey2019, + Other, +} + +/// Represents secret crypto material. +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(untagged)] +pub enum SecretMaterial { + #[serde(rename_all = "camelCase")] + JWK { private_key_jwk: Value }, + + #[serde(rename_all = "camelCase")] + Multibase { private_key_multibase: String }, + + #[serde(rename_all = "camelCase")] + Base58 { private_key_base58: String }, +} diff --git a/affinidi-messaging-didcomm/src/secrets/resolvers/example.rs b/affinidi-messaging-didcomm/src/secrets/resolvers/example.rs new file mode 100644 index 0000000..0218f29 --- /dev/null +++ b/affinidi-messaging-didcomm/src/secrets/resolvers/example.rs @@ -0,0 +1,36 @@ +use async_trait::async_trait; + +use crate::{ + error::Result, + secrets::{Secret, SecretsResolver}, +}; + +pub struct ExampleSecretsResolver { + known_secrets: Vec, +} + +impl ExampleSecretsResolver { + pub fn new(known_secrets: Vec) -> Self { + ExampleSecretsResolver { known_secrets } + } +} + +#[cfg_attr(feature = "uniffi", async_trait)] +#[cfg_attr(not(feature = "uniffi"), async_trait)] +impl SecretsResolver for ExampleSecretsResolver { + async fn get_secret(&self, secret_id: &str) -> Result> { + Ok(self + .known_secrets + .iter() + .find(|s| s.id == secret_id) + .cloned()) + } + + async fn find_secrets(&self, secret_ids: &[String]) -> Result> { + Ok(secret_ids + .iter() + .filter(|sid| self.known_secrets.iter().any(|s| s.id == sid.to_string())) + .map(|sid| sid.to_string()) + .collect()) + } +} diff --git a/affinidi-messaging-didcomm/src/secrets/resolvers/mod.rs b/affinidi-messaging-didcomm/src/secrets/resolvers/mod.rs new file mode 100644 index 0000000..a3f0d0f --- /dev/null +++ b/affinidi-messaging-didcomm/src/secrets/resolvers/mod.rs @@ -0,0 +1,3 @@ +mod example; + +pub use example::ExampleSecretsResolver; diff --git a/affinidi-messaging-didcomm/src/test_vectors/common.rs b/affinidi-messaging-didcomm/src/test_vectors/common.rs new file mode 100644 index 0000000..cf002f8 --- /dev/null +++ b/affinidi-messaging-didcomm/src/test_vectors/common.rs @@ -0,0 +1,62 @@ +use base64::prelude::*; +use serde_json::{Map, Value}; + +pub const ALICE_DID: &str = "did:example:alice"; +pub const BOB_DID: &str = "did:example:bob"; +pub const CHARLIE_DID: &str = "did:key:z6MkhKzjHrZKpxHqmW9x1BVxgKZ9n7N1WXE3jTtJC26PYASp"; + +pub fn update_field(msg: &str, field: &str, value: &str) -> String { + let parsed: Value = serde_json::from_str(msg).unwrap(); + let mut msg_dict: Map = parsed.as_object().unwrap().clone(); + msg_dict.insert(String::from(field), value.into()); + serde_json::to_string(&msg_dict).unwrap() +} + +pub fn remove_field(msg: &str, field: &str) -> String { + let parsed: Value = serde_json::from_str(msg).unwrap(); + let mut msg_dict: Map = parsed.as_object().unwrap().clone(); + msg_dict.remove(field); + serde_json::to_string(&msg_dict).unwrap() +} + +pub fn update_protected_field(msg: &str, field: &str, value: &str) -> String { + let parsed: Value = serde_json::from_str(msg).unwrap(); + let mut msg_dict: Map = parsed.as_object().unwrap().clone(); + + let mut buffer = Vec::::new(); + BASE64_URL_SAFE_NO_PAD + .decode_vec( + msg_dict.get("protected").unwrap().as_str().unwrap(), + &mut buffer, + ) + .unwrap(); + let parsed_protected: Value = serde_json::from_slice(&buffer).unwrap(); + let mut protected_dict: Map = parsed_protected.as_object().unwrap().clone(); + protected_dict.insert(String::from(field), value.into()); + let protected_str = serde_json::to_string(&protected_dict).unwrap(); + println!("{}", &protected_str); + let protected_str_base64 = BASE64_URL_SAFE_NO_PAD.encode(protected_str); + msg_dict.insert(String::from("protected"), protected_str_base64.into()); + serde_json::to_string(&msg_dict).unwrap() +} + +pub fn remove_protected_field(msg: &str, field: &str) -> String { + let parsed: Value = serde_json::from_str(msg).unwrap(); + let mut msg_dict: Map = parsed.as_object().unwrap().clone(); + + let mut buffer = Vec::::new(); + BASE64_URL_SAFE_NO_PAD + .decode_vec( + msg_dict.get("protected").unwrap().as_str().unwrap(), + &mut buffer, + ) + .unwrap(); + let parsed_protected: Value = serde_json::from_slice(&buffer).unwrap(); + let mut protected_dict: Map = parsed_protected.as_object().unwrap().clone(); + protected_dict.remove(field); + let protected_str = serde_json::to_string(&protected_dict).unwrap(); + let protected_str_base64 = BASE64_URL_SAFE_NO_PAD.encode(protected_str); + + msg_dict.insert(String::from("protected"), protected_str_base64.into()); + serde_json::to_string(&msg_dict).unwrap() +} diff --git a/affinidi-messaging-didcomm/src/test_vectors/encrypted.rs b/affinidi-messaging-didcomm/src/test_vectors/encrypted.rs new file mode 100644 index 0000000..396696e --- /dev/null +++ b/affinidi-messaging-didcomm/src/test_vectors/encrypted.rs @@ -0,0 +1,107 @@ +pub const ENCRYPTED_MSG_ANON_XC20P_1: &str = r#" +{ + "ciphertext": "KWS7gJU7TbyJlcT9dPkCw-ohNigGaHSukR9MUqFM0THbCTCNkY-g5tahBFyszlKIKXs7qOtqzYyWbPou2q77XlAeYs93IhF6NvaIjyNqYklvj-OtJt9W2Pj5CLOMdsR0C30wchGoXd6wEQZY4ttbzpxYznqPmJ0b9KW6ZP-l4_DSRYe9B-1oSWMNmqMPwluKbtguC-riy356Xbu2C9ShfWmpmjz1HyJWQhZfczuwkWWlE63g26FMskIZZd_jGpEhPFHKUXCFwbuiw_Iy3R0BIzmXXdK_w7PZMMPbaxssl2UeJmLQgCAP8j8TukxV96EKa6rGgULvlo7qibjJqsS5j03bnbxkuxwbfyu3OxwgVzFWlyHbUH6p", + "protected": "eyJlcGsiOnsia3R5IjoiT0tQIiwiY3J2IjoiWDI1NTE5IiwieCI6IkpIanNtSVJaQWFCMHpSR193TlhMVjJyUGdnRjAwaGRIYlc1cmo4ZzBJMjQifSwiYXB2IjoiTmNzdUFuclJmUEs2OUEtcmtaMEw5WFdVRzRqTXZOQzNaZzc0QlB6NTNQQSIsInR5cCI6ImFwcGxpY2F0aW9uL2RpZGNvbW0tZW5jcnlwdGVkK2pzb24iLCJlbmMiOiJYQzIwUCIsImFsZyI6IkVDREgtRVMrQTI1NktXIn0", + "recipients": [{ + "encrypted_key": "3n1olyBR3nY7ZGAprOx-b7wYAKza6cvOYjNwVg3miTnbLwPP_FmE1A", + "header": { + "kid": "did:example:bob#key-x25519-1" + } + },{ + "encrypted_key": "j5eSzn3kCrIkhQAWPnEwrFPMW6hG0zF_y37gUvvc5gvlzsuNX4hXrQ", + "header": { + "kid": "did:example:bob#key-x25519-2" + } + },{ + "encrypted_key": "TEWlqlq-ao7Lbynf0oZYhxs7ZB39SUWBCK4qjqQqfeItfwmNyDm73A", + "header": { + "kid": "did:example:bob#key-x25519-3" + } + }], + "tag": "6ylC_iAs4JvDQzXeY6MuYQ", + "iv": "ESpmcyGiZpRjc5urDela21TOOTW8Wqd1" +} +"#; + +pub const ENCRYPTED_MSG_ANON_XC20P_2: &str = r#" +{ + "ciphertext": "912eTUDRKTzhUUqxosPogT1bs9w9wv4s4HmoWkaeU9Uj92V4ENpk-_ZPNSvPyXYLfFj0nc9V2-ux5jq8hqUd17WJpXEM1ReMUjtnTqeUzVa7_xtfkbfhaOZdL8OfgNquPDH1bYcBshN9O9lMT0V52gmGaAB45k4I2PNHcc0A5XWzditCYi8wOkPDm5A7pA39Au5uUNiFQjRYDrz1YvJwV9cdca54vYsBfV1q4c8ncQsv5tNnFYQ1s4rAG7RbyWdAjkC89kE_hIoRRkWZhFyNSfdvRtlUJDlM19uml7lwBWWPnqkmQ3ubiBGmVct3pjrcDvjissOw8Dwkn4E1V1gafec-jDBy4Rndai_RdGjnXjMJs7nRv3Ot", + "protected": "eyJlcGsiOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJFczdpUDNFaExDSGxBclAwS2NZRmNxRXlCYXByMks2WU9BOVc4ZU84YXU4IiwieSI6Ik42QWw3RVR3Q2RwQzZOamRlY3IyS1hBZzFVZVp5X3VmSFJRS3A5RzZLR2sifSwiYXB2Ijoiei1McXB2VlhEYl9zR1luM21qUUxwdXUyQ1FMZXdZdVpvVFdPSVhQSDNGTSIsInR5cCI6ImFwcGxpY2F0aW9uL2RpZGNvbW0tZW5jcnlwdGVkK2pzb24iLCJlbmMiOiJYQzIwUCIsImFsZyI6IkVDREgtRVMrQTI1NktXIn0", + "recipients": [{ + "encrypted_key": "G-UFZ1ebuhlWZTrMj214YcEvHl6hyfsFtWv4hj-NPNi9gpi99rRs3Q", + "header": { + "kid": "did:example:bob#key-p256-1" + } + },{ + "encrypted_key": "gVdbFdXAxEgrtj9Uw2xiEucQukpiAOA3Jp7Ecmb6L7G5c3IIcAAHgQ", + "header": { + "kid": "did:example:bob#key-p256-2" + } + }], + "tag": "t8ioLvZhsCp7A93jvdf3wA", + "iv": "JrIpD5q5ifMq6PT06pYh6QhCQ6LgnGpF" +} +"#; + +pub const ENCRYPTED_MSG_AUTH_X25519: &str = r#" +{ + "ciphertext": "MJezmxJ8DzUB01rMjiW6JViSaUhsZBhMvYtezkhmwts1qXWtDB63i4-FHZP6cJSyCI7eU-gqH8lBXO_UVuviWIqnIUrTRLaumanZ4q1dNKAnxNL-dHmb3coOqSvy3ZZn6W17lsVudjw7hUUpMbeMbQ5W8GokK9ZCGaaWnqAzd1ZcuGXDuemWeA8BerQsfQw_IQm-aUKancldedHSGrOjVWgozVL97MH966j3i9CJc3k9jS9xDuE0owoWVZa7SxTmhl1PDetmzLnYIIIt-peJtNYGdpd-FcYxIFycQNRUoFEr77h4GBTLbC-vqbQHJC1vW4O2LEKhnhOAVlGyDYkNbA4DSL-LMwKxenQXRARsKSIMn7z-ZIqTE-VCNj9vbtgR", + "protected": "eyJlcGsiOnsia3R5IjoiT0tQIiwiY3J2IjoiWDI1NTE5IiwieCI6IkdGY01vcEpsamY0cExaZmNoNGFfR2hUTV9ZQWY2aU5JMWRXREd5VkNhdzAifSwiYXB2IjoiTmNzdUFuclJmUEs2OUEtcmtaMEw5WFdVRzRqTXZOQzNaZzc0QlB6NTNQQSIsInNraWQiOiJkaWQ6ZXhhbXBsZTphbGljZSNrZXkteDI1NTE5LTEiLCJhcHUiOiJaR2xrT21WNFlXMXdiR1U2WVd4cFkyVWphMlY1TFhneU5UVXhPUzB4IiwidHlwIjoiYXBwbGljYXRpb24vZGlkY29tbS1lbmNyeXB0ZWQranNvbiIsImVuYyI6IkEyNTZDQkMtSFM1MTIiLCJhbGciOiJFQ0RILTFQVStBMjU2S1cifQ", + "recipients": [{ + "encrypted_key": "o0FJASHkQKhnFo_rTMHTI9qTm_m2mkJp-wv96mKyT5TP7QjBDuiQ0AMKaPI_RLLB7jpyE-Q80Mwos7CvwbMJDhIEBnk2qHVB", + "header": { + "kid": "did:example:bob#key-x25519-1" + } + },{ + "encrypted_key": "rYlafW0XkNd8kaXCqVbtGJ9GhwBC3lZ9AihHK4B6J6V2kT7vjbSYuIpr1IlAjvxYQOw08yqEJNIwrPpB0ouDzKqk98FVN7rK", + "header": { + "kid": "did:example:bob#key-x25519-2" + } + },{ + "encrypted_key": "aqfxMY2sV-njsVo-_9Ke9QbOf6hxhGrUVh_m-h_Aq530w3e_4IokChfKWG1tVJvXYv_AffY7vxj0k5aIfKZUxiNmBwC_QsNo", + "header": { + "kid": "did:example:bob#key-x25519-3" + } + }], + "tag": "uYeo7IsZjN7AnvBjUZE5lNryNENbf6_zew_VC-d4b3U", + "iv": "o02OXDQ6_-sKz2PX_6oyJg" +} +"#; + +pub const ENCRYPTED_MSG_AUTH_P256: &str = r#" +{ + "ciphertext": "WCufCs2lMZfkxQ0JCK92lPtLFgwWk_FtRWOMj52bQISa94nEbIYqHDUohIbvLMgbSjRcJVusZO04UthDuOpSSTcV5GBi3O0cMrjyI_PZnTb1yikLXpXma1bT10D2r5TPtzRMxXF3nFsr9y0JKV1TsMtn70Df2fERx2bAGxcflmd-A2sMlSTT8b7QqPtn17Yb-pA8gr4i0Bqb2WfDzwnbfewbukpRmPA2hsEs9oLKypbniAafSpoiQjfb19oDfsYaWWXqsdjTYMflqH__DqSmW52M-SUp6or0xU0ujbHmOkRkcdh9PsR5YsPuIWAqYa2hfjz_KIrGTxvCos0DMiZ4Lh_lPIYQqBufSdFH5AGChoekFbQ1vcyIyYMFugzOHOgZ2TwEzv94GCgokBHQR4_qaU_f4Mva64KPwqOYdm5f4KX16afTJa-IV7ar7__2L-A-LyxmC5KIHeGOedV9kzZBLC7TuzRAuE3vY7pkhLB1jPE6XpTeKXldljaeOSEVcbFUQtsHOSPz9JXuhqZ1fdAx8qV7hUnSAd_YMMDR3S6SXtem8ak2m98WPvKIxhCbcto7W2qoNYMT7MPvvid-QzUvTdKtyovCvLzhyYJzMjZxmn9-EnGhZ5ITPL_xFfLyKxhSSUVz3kSwK9xuOj3KpJnrrD7xrp5FKzEaJVIHWrUW90V_9QVLjriThZ36fA3ipvs8ZJ8QSTnGAmuIQ6Z2u_r4KsjL_mGAgn47qyqRm-OSLEUE4_2qB0Q9Z7EBKakCH8VPt09hTMDR62aYZYwtmpNs9ISu0VPvFjh8UmKbFcQsVrz90-x-r-Q1fTX9JaIFcDy7aqKcI-ai3tVF_HDR60Jaiw", + "protected": "eyJlcGsiOnsia3R5IjoiRUMiLCJjcnYiOiJQLTI1NiIsIngiOiJObHJ3UHZ0SUluZWNpeUVrYTRzMi00czhPalRidEZFQVhmTC12Z2x5enFvIiwieSI6ImhiMnZkWE5zSzVCQ2U3LVhaQ0dfLTY0R21UT19rNUlNWFBaQ00xdGFUQmcifSwiYXB2Ijoiei1McXB2VlhEYl9zR1luM21qUUxwdXUyQ1FMZXdZdVpvVFdPSVhQSDNGTSIsInNraWQiOiJkaWQ6ZXhhbXBsZTphbGljZSNrZXktcDI1Ni0xIiwiYXB1IjoiWkdsa09tVjRZVzF3YkdVNllXeHBZMlVqYTJWNUxYQXlOVFl0TVEiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiZW5jIjoiQTI1NkNCQy1IUzUxMiIsImFsZyI6IkVDREgtMVBVK0EyNTZLVyJ9", + "recipients": [{ + "encrypted_key": "ZIL6Leligq1Xps_229nlo1xB_tGxOEVoEEMF-XTOltI0QXjyUoq_pFQBCAnVdcWNH5bmaiuzCYOmZ9lkyXBkfHO90KkGgODG", + "header": { + "kid": "did:example:bob#key-p256-1" + } + },{ + "encrypted_key": "sOjs0A0typIRSshhQoiJPoM4o7YpR5LA8SSieHZzmMyIDdD8ww-4JyyQhqFYuvfS4Yt37VF4z7Nd0OjYVNRL-iqPnoJ3iCOr", + "header": { + "kid": "did:example:bob#key-p256-2" + } + }], + "tag": "nIpa3EQ29hgCkA2cBPde2HpKXK4_bvmL2x7h39rtVEc", + "iv": "mLqi1bZLz7VwqtVVFsDiLg" +} +"#; + +pub const ENCRYPTED_MSG_AUTH_P256_SIGNED: &str = r#"{"payload":"eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19","signatures":[{"protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRWREU0EifQ","signature":"FW33NnvOHV0Ted9-F7GZbkia-vYAfBKtH4oBxbrttWAhBZ6UFJMxcGjL3lwOl4YohI3kyyd08LHPWNMgP2EVCQ","header":{"kid":"did:example:alice#key-1"}}]}"#; + +pub const INVALID_ENCRYPTED_MSG_ANON_P256_EPK_WRONG_POINT: &str = r#" +{ + "protected": "eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC1FUytBMjU2S1ciLCJlbmMiOiJYQzIwUCIsImFwdSI6bnVsbCwiYXB2Ijoiei1McXB2VlhEYl9zR1luM21qUUxwdXUyQ1FMZXdZdVpvVFdPSVhQSDNGTSIsImVwayI6eyJjcnYiOiJQLTI1NiIsImt0eSI6IkVDIiwieCI6IkZSQW1UQmljUFZJXy1aRnF2WEJwNzZhV2pZM0gzYlpGZlhocHRUNm1ETnciLCJ5IjoiLXZ0LTFIaHRvVjBwN2xrbGIxTnRvMWRhU0lqQnV3cVZzbGIwcC1uOWRrdyJ9fQ==", + "recipients": [{ + "header": {"kid": "did:example:bob#key-p256-1"}, + "encrypted_key": "scQxV9YQ4mQrUHgl6yAnBFDXNZAiIs_15bmoErUmoYm0HtuRclPoQg" + },{ + "header": {"kid": "did:example:bob#key-p256-2"}, + "encrypted_key": "CqZ-HDH2j0NC-eoUueNLKyAuMQXjQyw8bJHYM2f-lxJVm3eXCdmm2g" + }], + "iv": "Vg1uyuQKrU6Kw8OJK38WCpYFxW0suAP9", + "ciphertext": "2nIm3xQcFR3HXbUPF1HS_D92OGVDvL0nIi6O5ol5tnMIa09NxJtbVAYIG7ZrkT9314PqXn_Rq77hgGE6FAOgO7aNYLyUJh0JCC_i2p_XOWuk20BYyBsmmRvVpg0DY3I1Lb-Vg1pT9pEy09gsMSLhbfqk0_TFJB1rcqzR8W0YZB5mX_53nMRf1ZatDEg4rDogSekWEGTBnlTNRua8-zoI4573SfgJ-ONt7Z_KbGO-sdRkmqXhfYNcbUyoMF9JSa-kraVuWHZP9hTz8-7R020EXfb4jodMWVOMMAiJYk1Cd7tetHXpLPdtuokaapofmtL_SNftAX2CB6ULf0axrHUNtvUyjAPvpgvSuvQuMrDlaXn16MQJ_q55", + "tag": "etLTQvKsTvF629fykLiUDg" +} +"#; diff --git a/affinidi-messaging-didcomm/src/test_vectors/from_prior.rs b/affinidi-messaging-didcomm/src/test_vectors/from_prior.rs new file mode 100644 index 0000000..e74a57b --- /dev/null +++ b/affinidi-messaging-didcomm/src/test_vectors/from_prior.rs @@ -0,0 +1,54 @@ +use lazy_static::lazy_static; + +use super::common::{ALICE_DID, CHARLIE_DID}; + +use crate::affinidi_messaging_didcomm::FromPrior; + +lazy_static! { + pub static ref FROM_PRIOR_MINIMAL: FromPrior = + FromPrior::build(CHARLIE_DID.into(), ALICE_DID.into()).finalize(); +} + +lazy_static! { + pub static ref FROM_PRIOR_FULL: FromPrior = + FromPrior::build(CHARLIE_DID.into(), ALICE_DID.into()) + .aud("123".into()) + .exp(1234) + .nbf(12345) + .iat(123456) + .jti("dfg".into()) + .finalize(); +} + +lazy_static! { + pub static ref FROM_PRIOR_INVALID_ISS: FromPrior = + FromPrior::build("invalid".into(), ALICE_DID.into()) + .aud("123".into()) + .exp(1234) + .nbf(12345) + .iat(123456) + .jti("dfg".into()) + .finalize(); +} + +lazy_static! { + pub static ref FROM_PRIOR_INVALID_SUB: FromPrior = + FromPrior::build(CHARLIE_DID.into(), "invalid".into()) + .aud("123".into()) + .exp(1234) + .nbf(12345) + .iat(123456) + .jti("dfg".into()) + .finalize(); +} + +lazy_static! { + pub static ref FROM_PRIOR_INVALID_EQUAL_ISS_AND_SUB: FromPrior = + FromPrior::build(ALICE_DID.into(), ALICE_DID.into()) + .aud("123".into()) + .exp(1234) + .nbf(12345) + .iat(123456) + .jti("dfg".into()) + .finalize(); +} diff --git a/affinidi-messaging-didcomm/src/test_vectors/from_prior_jwt.rs b/affinidi-messaging-didcomm/src/test_vectors/from_prior_jwt.rs new file mode 100644 index 0000000..ad1f3e6 --- /dev/null +++ b/affinidi-messaging-didcomm/src/test_vectors/from_prior_jwt.rs @@ -0,0 +1,5 @@ +pub const FROM_PRIOR_JWT_FULL: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2hLempIclpLcHhIcW1XOXgxQlZ4Z0taOW43TjFXWEUzalR0SkMyNlBZQVNwI3o2TWtoS3pqSHJaS3B4SHFtVzl4MUJWeGdLWjluN04xV1hFM2pUdEpDMjZQWUFTcCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtoS3pqSHJaS3B4SHFtVzl4MUJWeGdLWjluN04xV1hFM2pUdEpDMjZQWUFTcCIsInN1YiI6ImRpZDpleGFtcGxlOmFsaWNlIiwiYXVkIjoiMTIzIiwiZXhwIjoxMjM0LCJuYmYiOjEyMzQ1LCJpYXQiOjEyMzQ1NiwianRpIjoiZGZnIn0.XF7C48Wbwgfrq5pdRDl7zxcGkEAJQ6TEDMMAMJ0UyIBafTnbLpkUnfMqt2dKmNLk5vAq0DKzrhTmiW1-BAVoBg"; + +pub const FROM_PRIOR_JWT_INVALID: &str = "invalid"; + +pub const FROM_PRIOR_JWT_INVALID_SIGNATURE: &str = "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDprZXk6ejZNa2hLempIclpLcHhIcW1XOXgxQlZ4Z0taOW43TjFXWEUzalR0SkMyNlBZQVNwI3o2TWtoS3pqSHJaS3B4SHFtVzl4MUJWeGdLWjluN04xV1hFM2pUdEpDMjZQWUFTcCJ9.eyJpc3MiOiJkaWQ6a2V5Ono2TWtoS3pqSHJaS3B4SHFtVzl4MUJWeGdLWjluN04xV1hFM2pUdEpDMjZQWUFTcCIsInN1YiI6ImRpZDpleGFtcGxlOmFsaWNlIiwiYXVkIjoiMTIzIiwiZXhwIjoxMjM0LCJuYmYiOjEyMzQ1LCJpYXQiOjEyMzQ1NiwianRpIjoiZGZnIn0.XF7C48Wbwgfrq5pdRDl7zxcGkEAJQ6TEDMMAMJ0UyIBafTnbLpkUnfMqt2dKmNLk5vAq0DKzrhTmiW1-BAVoBh"; diff --git a/affinidi-messaging-didcomm/src/test_vectors/message.rs b/affinidi-messaging-didcomm/src/test_vectors/message.rs new file mode 100644 index 0000000..16b1083 --- /dev/null +++ b/affinidi-messaging-didcomm/src/test_vectors/message.rs @@ -0,0 +1,137 @@ +use crate::affinidi_messaging_didcomm::{Attachment, Message, MessageBuilder}; + +use super::common::{ALICE_DID, BOB_DID}; +use lazy_static::lazy_static; +use serde_json::json; + +lazy_static! { + pub static ref MESSAGE_SIMPLE: Message = _message().finalize(); +} + +lazy_static! { + pub static ref MESSAGE_MINIMAL: Message = Message::build( + "1234567890".to_owned(), + "http://example.com/protocols/lets_do_lunch/1.0/proposal".to_owned(), + json!({}), + ) + .finalize(); +} + +lazy_static! { + pub static ref MESSAGE_FROM_PRIOR_FULL: Message = _message() + .from_prior("eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpleGFtcGxlOmNoYXJsaWUja2V5LTEifQ.eyJpc3MiOiJkaWQ6ZXhhbXBsZTpjaGFybGllIiwic3ViIjoiZGlkOmV4YW1wbGU6YWxpY2UiLCJhdWQiOiIxMjMiLCJleHAiOjEyMzQsIm5iZiI6MTIzNDUsImlhdCI6MTIzNDU2LCJqdGkiOiJkZmcifQ.ir0tegXiGJIZIMagO5P853KwhzGTEw0OpFFAyarUV-nQrtbI_ELbxT9l7jPBoPve_-60ifGJ9v3ArmFjELFlDA".into()) + .finalize(); +} + +lazy_static! { + pub static ref MESSAGE_FROM_PRIOR_MISMATCHED_SUB_AND_FROM: Message = + Message::build( + "1234567890".to_owned(), + "http://example.com/protocols/lets_do_lunch/1.0/proposal".to_owned(), + json!({"messagespecificattribute": "and its value"}), + ) + .from(BOB_DID.to_owned()) + .to(ALICE_DID.to_owned()) + .created_time(1516269022) + .expires_time(1516385931) + .from_prior("eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpleGFtcGxlOmNoYXJsaWUja2V5LTEifQ.eyJpc3MiOiJkaWQ6ZXhhbXBsZTpjaGFybGllIiwic3ViIjoiZGlkOmV4YW1wbGU6YWxpY2UiLCJhdWQiOiIxMjMiLCJleHAiOjEyMzQsIm5iZiI6MTIzNDUsImlhdCI6MTIzNDU2LCJqdGkiOiJkZmcifQ.ir0tegXiGJIZIMagO5P853KwhzGTEw0OpFFAyarUV-nQrtbI_ELbxT9l7jPBoPve_-60ifGJ9v3ArmFjELFlDA".into()) + .finalize(); +} + +lazy_static! { + pub static ref MESSAGE_ATTACHMENT_BASE64: Message = _message() + .attachment( + Attachment::base64("qwerty".to_owned()) + .id("23".to_owned()) + .finalize(), + ) + .finalize(); +} + +lazy_static! { + pub static ref MESSAGE_ATTACHMENT_LINKS: Message = _message() + .attachment( + Attachment::links( + ["1".to_owned(), "2".to_owned(), "3".to_owned()].into(), + "qwerty".into(), + ) + .id("23".to_owned()) + .finalize(), + ) + .finalize(); +} + +lazy_static! { + pub static ref MESSAGE_ATTACHMENT_JSON: Message = _message() + .attachment( + Attachment::json(json!({"foo": "bar", "links": [2, 3]})) + .id("23".to_owned()) + .finalize(), + ) + .finalize(); +} + +lazy_static! { + pub static ref MESSAGE_ATTACHMENT_MULTI_1: Message = _message() + .attachments( + [ + Attachment::json(json!({"foo": "bar", "links": [2, 3]})) + .id("23".to_owned()) + .finalize(), + Attachment::base64("qwerty".to_owned()) + .id("24".to_owned()) + .finalize(), + Attachment::links( + ["1".to_owned(), "2".to_owned(), "3".to_owned()].into(), + "qwerty".into(), + ) + .id("25".to_owned()) + .finalize(), + ] + .into(), + ) + .finalize(); +} + +lazy_static! { + pub static ref MESSAGE_ATTACHMENT_MULTI_2: Message = _message() + .attachments( + [ + Attachment::links( + ["1".to_owned(), "2".to_owned(), "3".to_owned()].into(), + "qwerty".into(), + ) + .id("23".to_owned()) + .finalize(), + Attachment::base64("qwerty".to_owned()) + .id("24".to_owned()) + .finalize(), + Attachment::links( + [ + "1".to_owned(), + "2".to_owned(), + "3".to_owned(), + "4".to_owned(), + ] + .into(), + "qwerty2".into(), + ) + .id("25".to_owned()) + .finalize(), + ] + .into(), + ) + .finalize(); +} + +fn _message() -> MessageBuilder { + Message::build( + "1234567890".to_owned(), + "http://example.com/protocols/lets_do_lunch/1.0/proposal".to_owned(), + json!({"messagespecificattribute": "and its value"}), + ) + .from(ALICE_DID.to_owned()) + .to(BOB_DID.to_owned()) + .created_time(1516269022) + .expires_time(1516385931) +} diff --git a/affinidi-messaging-didcomm/src/test_vectors/mod.rs b/affinidi-messaging-didcomm/src/test_vectors/mod.rs new file mode 100644 index 0000000..846294e --- /dev/null +++ b/affinidi-messaging-didcomm/src/test_vectors/mod.rs @@ -0,0 +1,16 @@ +mod common; +mod encrypted; +mod from_prior; +mod from_prior_jwt; +mod message; +mod plaintext; +mod secrets; +mod signed; + +pub use common::*; + +pub use from_prior::*; + +pub use from_prior_jwt::*; + +pub use secrets::*; diff --git a/affinidi-messaging-didcomm/src/test_vectors/plaintext.rs b/affinidi-messaging-didcomm/src/test_vectors/plaintext.rs new file mode 100644 index 0000000..bc222bf --- /dev/null +++ b/affinidi-messaging-didcomm/src/test_vectors/plaintext.rs @@ -0,0 +1,288 @@ +pub const PLAINTEXT_MSG_SIMPLE: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "from": "did:example:alice", + "to": ["did:example:bob"], + "created_time": 1516269022, + "expires_time": 1516385931, + "body": {"messagespecificattribute": "and its value"} +} +"#; + +pub const PLAINTEXT_MSG_SIMPLE_NO_TYP: &str = r#" +{ + "id": "1234567890", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "from": "did:example:alice", + "to": ["did:example:bob"], + "created_time": 1516269022, + "expires_time": 1516385931, + "body": {"messagespecificattribute": "and its value"} +} +"#; + +pub const PLAINTEXT_MSG_MINIMAL: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "body": {} +} +"#; + +pub const PLAINTEXT_FROM_PRIOR: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "from": "did:example:alice", + "to": ["did:example:bob"], + "created_time": 1516269022, + "expires_time": 1516385931, + "from_prior": "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpleGFtcGxlOmNoYXJsaWUja2V5LTEifQ.eyJpc3MiOiJkaWQ6ZXhhbXBsZTpjaGFybGllIiwic3ViIjoiZGlkOmV4YW1wbGU6YWxpY2UiLCJhdWQiOiIxMjMiLCJleHAiOjEyMzQsIm5iZiI6MTIzNDUsImlhdCI6MTIzNDU2LCJqdGkiOiJkZmcifQ.ir0tegXiGJIZIMagO5P853KwhzGTEw0OpFFAyarUV-nQrtbI_ELbxT9l7jPBoPve_-60ifGJ9v3ArmFjELFlDA", + "body": {"messagespecificattribute": "and its value"} +} +"#; + +pub const PLAINTEXT_INVALID_FROM_PRIOR: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "from": "did:example:alice", + "to": ["did:example:bob"], + "created_time": 1516269022, + "expires_time": 1516385931, + "from_prior": "invalid", + "body": {"messagespecificattribute": "and its value"} +} +"#; + +pub const PLAINTEXT_FROM_PRIOR_INVALID_SIGNATURE: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "from": "did:example:alice", + "to": ["did:example:bob"], + "created_time": 1516269022, + "expires_time": 1516385931, + "from_prior": "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpleGFtcGxlOmNoYXJsaWUja2V5LTEifQ.eyJpc3MiOiJkaWQ6ZXhhbXBsZTpjaGFybGllIiwic3ViIjoiZGlkOmV4YW1wbGU6YWxpY2UiLCJhdWQiOiIxMjMiLCJleHAiOjEyMzQsIm5iZiI6MTIzNDUsImlhdCI6MTIzNDU2LCJqdGkiOiJkZmcifQ.ir0tegXiGJIZIMagO5P853KwhzGTEw0OpFFAyarUV-nQrtbI_ELbxT9l7jPBoPve_-60ifGJ9v3ArmFjELFlDB", + "body": {"messagespecificattribute": "and its value"} +} +"#; + +pub const PLAINTEXT_MSG_ATTACHMENT_BASE64: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "from": "did:example:alice", + "to": ["did:example:bob"], + "created_time": 1516269022, + "expires_time": 1516385931, + "body": {"messagespecificattribute": "and its value"}, + "attachments": [{"id": "23", "data": {"base64": "qwerty"}}] +} +"#; + +pub const PLAINTEXT_MSG_ATTACHMENT_LINKS: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "from": "did:example:alice", + "to": ["did:example:bob"], + "created_time": 1516269022, + "expires_time": 1516385931, + "body": {"messagespecificattribute": "and its value"}, + "attachments": [ + {"id": "23", "data": {"links": ["1", "2", "3"], "hash": "qwerty"}} + ] +} +"#; + +pub const PLAINTEXT_MSG_ATTACHMENT_JSON: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "from": "did:example:alice", + "to": ["did:example:bob"], + "created_time": 1516269022, + "expires_time": 1516385931, + "body": {"messagespecificattribute": "and its value"}, + "attachments": [ + {"id": "23", "data": {"json": {"foo": "bar", "links": [2, 3]}}} + ] +} +"#; + +pub const PLAINTEXT_MSG_ATTACHMENT_MULTI_1: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "from": "did:example:alice", + "to": ["did:example:bob"], + "created_time": 1516269022, + "expires_time": 1516385931, + "body": {"messagespecificattribute": "and its value"}, + "attachments": [ + {"id": "23", "data": {"json": {"foo": "bar", "links": [2, 3]}}}, + {"id": "24", "data": {"base64": "qwerty"}}, + {"id": "25", "data": {"links": ["1", "2", "3"], "hash": "qwerty"}} + ] +} +"#; + +pub const PLAINTEXT_MSG_ATTACHMENT_MULTI_2: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "from": "did:example:alice", + "to": ["did:example:bob"], + "created_time": 1516269022, + "expires_time": 1516385931, + "body": {"messagespecificattribute": "and its value"}, + "attachments": [ + {"id": "23", "data": {"links": ["1", "2", "3"], "hash": "qwerty"}}, + {"id": "24", "data": {"base64": "qwerty"}}, + {"id": "25", "data": {"links": ["1", "2", "3", "4"], "hash": "qwerty2"}} + ] +} +"#; + +pub const INVALID_PLAINTEXT_MSG_EMPTY: &str = r#" +{} +"#; + +pub const INVALID_PLAINTEXT_MSG_STRING: &str = r#" +aaaa +"#; + +pub const INVALID_PLAINTEXT_MSG_NO_ID: &str = r#" +{ + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "body": {} +} +"#; + +pub const INVALID_PLAINTEXT_MSG_NO_TYPE: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "body": {} +} +"#; + +pub const INVALID_PLAINTEXT_MSG_NO_BODY: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal" +} +"#; + +pub const INVALID_PLAINTEXT_MSG_WRONG_TYP: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json-unknown", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "body": {} +} +"#; + +pub const INVALID_PLAINTEXT_MSG_EMPTY_ATTACHMENTS: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "body": {}, + "attachments": [{}] +} +"#; + +pub const INVALID_PLAINTEXT_MSG_ATTACHMENTS_NO_DATA: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "body": {}, + "attachments": [{"id": "23"}] +} +"#; + +pub const INVALID_PLAINTEXT_MSG_ATTACHMENTS_EMPTY_DATA: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "body": {}, + "attachments": [{"id": "23", "data": {}}] +} +"#; + +pub const INVALID_PLAINTEXT_MSG_ATTACHMENTS_LINKS_NO_HASH: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "body": {}, + "attachments": [{"id": "23", "data": {"links": ["231", "212"]}}] +} +"#; + +pub const INVALID_PLAINTEXT_MSG_ATTACHMENTS_AS_STRING: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "body": {}, + "attachments": "131" +} +"#; + +pub const INVALID_PLAINTEXT_MSG_ATTACHMENTS_AS_INT_ARRAY: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "body": {}, + "attachments": [2131] +} +"#; + +pub const INVALID_PLAINTEXT_MSG_ATTACHMENTS_WRONG_DATA: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "body": {}, + "attachments": [{"id": "1", "data": "invalid"}] +} +"#; + +pub const INVALID_PLAINTEXT_MSG_ATTACHMENTS_WRONG_ID: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "body": {}, + "attachments": [{"id": 2}] +} +"#; + +pub const INVALID_PLAINTEXT_MSG_ATTACHMENTS_NULL_DATA: &str = r#" +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "body": {}, + "attachments": [{"id": "1", "data": null}] +} +"#; diff --git a/affinidi-messaging-didcomm/src/test_vectors/secrets/alice.rs b/affinidi-messaging-didcomm/src/test_vectors/secrets/alice.rs new file mode 100644 index 0000000..7c752eb --- /dev/null +++ b/affinidi-messaging-didcomm/src/test_vectors/secrets/alice.rs @@ -0,0 +1,91 @@ +use lazy_static::lazy_static; +use serde_json::json; + +use crate::affinidi_messaging_didcomm::secrets::{Secret, SecretMaterial, SecretType}; + +lazy_static! { + pub static ref ALICE_SECRET_AUTH_KEY_ED25519: Secret = Secret { + id: "did:example:alice#key-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!({ + "kty": "OKP", + "d": "pFRUKkyzx4kHdJtFSnlPA9WzqkDT1HWV0xZ5OYZd2SY", + "crv": "Ed25519", + "x": "G-boxFB6vOZBu-wXkm-9Lh79I8nf9Z50cILaOgKKGww", + }) + }, + }; + pub static ref ALICE_SECRET_AUTH_KEY_P256: Secret = Secret { + id: "did:example:alice#key-2".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!({ + "kty": "EC", + "d": "7TCIdt1rhThFtWcEiLnk_COEjh1ZfQhM4bW2wz-dp4A", + "crv": "P-256", + "x": "2syLh57B-dGpa0F8p1JrO6JU7UUSF6j7qL-vfk1eOoY", + "y": "BgsGtI7UPsObMRjdElxLOrgAO9JggNMjOcfzEPox18w", + }) + }, + }; + pub static ref ALICE_SECRET_AUTH_KEY_SECP256K1: Secret = Secret { + id: "did:example:alice#key-3".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!({ + "kty": "EC", + "d": "N3Hm1LXA210YVGGsXw_GklMwcLu_bMgnzDese6YQIyA", + "crv": "secp256k1", + "x": "aToW5EaTq5mlAf8C5ECYDSkqsJycrW-e1SQ6_GJcAOk", + "y": "JAGX94caA21WKreXwYUaOCYTBMrqaX4KWIlsQZTHWCk", + }) + }, + }; + pub static ref ALICE_SECRET_KEY_AGREEMENT_KEY_X25519: Secret = Secret { + id: "did:example:alice#key-x25519-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!({ + "kty": "OKP", + "d": "r-jK2cO3taR8LQnJB1_ikLBTAnOtShJOsHXRUWT-aZA", + "crv": "X25519", + "x": "avH0O2Y4tqLAq8y9zpianr8ajii5m4F_mICrzNlatXs", + }) + }, + }; + pub static ref ALICE_SECRET_KEY_AGREEMENT_KEY_P256: Secret = Secret { + id: "did:example:alice#key-p256-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!({ + "kty": "EC", + "d": "sB0bYtpaXyp-h17dDpMx91N3Du1AdN4z1FUq02GbmLw", + "crv": "P-256", + "x": "L0crjMN1g0Ih4sYAJ_nGoHUck2cloltUpUVQDhF2nHE", + "y": "SxYgE7CmEJYi7IDhgK5jI4ZiajO8jPRZDldVhqFpYoo", + }) + }, + }; + pub static ref ALICE_SECRET_KEY_AGREEMENT_KEY_P521: Secret = Secret { + id: "did:example:alice#key-p521-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!({ + "kty": "EC", + "d": "AQCQKE7rZpxPnX9RgjXxeywrAMp1fJsyFe4cir1gWj-8t8xWaM_E2qBkTTzyjbRBu-JPXHe_auT850iYmE34SkWi", + "crv": "P-521", + "x": "AHBEVPRhAv-WHDEvxVM9S0px9WxxwHL641Pemgk9sDdxvli9VpKCBdra5gg_4kupBDhz__AlaBgKOC_15J2Byptz", + "y": "AciGcHJCD_yMikQvlmqpkBbVqqbg93mMVcgvXBYAQPP-u9AF7adybwZrNfHWCKAQwGF9ugd0Zhg7mLMEszIONFRk", + }) + }, + }; + pub static ref ALICE_SECRETS: Vec = vec![ + ALICE_SECRET_AUTH_KEY_ED25519.clone(), + ALICE_SECRET_AUTH_KEY_P256.clone(), + ALICE_SECRET_AUTH_KEY_SECP256K1.clone(), + ALICE_SECRET_KEY_AGREEMENT_KEY_X25519.clone(), + ALICE_SECRET_KEY_AGREEMENT_KEY_P256.clone(), + ALICE_SECRET_KEY_AGREEMENT_KEY_P521.clone(), + ]; +} diff --git a/affinidi-messaging-didcomm/src/test_vectors/secrets/bob.rs b/affinidi-messaging-didcomm/src/test_vectors/secrets/bob.rs new file mode 100644 index 0000000..c478c20 --- /dev/null +++ b/affinidi-messaging-didcomm/src/test_vectors/secrets/bob.rs @@ -0,0 +1,141 @@ +use lazy_static::lazy_static; +use serde_json::json; + +use crate::affinidi_messaging_didcomm::secrets::{Secret, SecretMaterial, SecretType}; + +lazy_static! { + pub static ref BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1: Secret = Secret { + id: "did:example:bob#key-x25519-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!( + { + "kty": "OKP", + "d": "b9NnuOCB0hm7YGNvaE9DMhwH_wjZA1-gWD6dA0JWdL0", + "crv": "X25519", + "x": "GDTrI66K0pFfO54tlCSvfjjNapIs44dzpneBgyx0S3E", + }) + }, + }; + pub static ref BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2: Secret = Secret { + id: "did:example:bob#key-x25519-2".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!( + { + "kty": "OKP", + "d": "p-vteoF1gopny1HXywt76xz_uC83UUmrgszsI-ThBKk", + "crv": "X25519", + "x": "UT9S3F5ep16KSNBBShU2wh3qSfqYjlasZimn0mB8_VM", + }) + }, + }; + pub static ref BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3: Secret = Secret { + id: "did:example:bob#key-x25519-3".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!( + { + "kty": "OKP", + "d": "f9WJeuQXEItkGM8shN4dqFr5fLQLBasHnWZ-8dPaSo0", + "crv": "X25519", + "x": "82k2BTUiywKv49fKLZa-WwDi8RBf0tB0M8bvSAUQ3yY", + }) + }, + }; + pub static ref BOB_SECRET_KEY_AGREEMENT_KEY_P256_1: Secret = Secret { + id: "did:example:bob#key-p256-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!( + { + "kty": "EC", + "d": "PgwHnlXxt8pwR6OCTUwwWx-P51BiLkFZyqHzquKddXQ", + "crv": "P-256", + "x": "FQVaTOksf-XsCUrt4J1L2UGvtWaDwpboVlqbKBY2AIo", + "y": "6XFB9PYo7dyC5ViJSO9uXNYkxTJWn0d_mqJ__ZYhcNY", + }) + }, + }; + pub static ref BOB_SECRET_KEY_AGREEMENT_KEY_P256_2: Secret = Secret { + id: "did:example:bob#key-p256-2".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!( + { + "kty": "EC", + "d": "agKz7HS8mIwqO40Q2dwm_Zi70IdYFtonN5sZecQoxYU", + "crv": "P-256", + "x": "n0yBsGrwGZup9ywKhzD4KoORGicilzIUyfcXb1CSwe0", + "y": "ov0buZJ8GHzV128jmCw1CaFbajZoFFmiJDbMrceCXIw", + }) + }, + }; + pub static ref BOB_SECRET_KEY_AGREEMENT_KEY_P384_1: Secret = Secret { + id: "did:example:bob#key-p384-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!( + { + "kty": "EC", + "d": "ajqcWbYA0UDBKfAhkSkeiVjMMt8l-5rcknvEv9t_Os6M8s-HisdywvNCX4CGd_xY", + "crv": "P-384", + "x": "MvnE_OwKoTcJVfHyTX-DLSRhhNwlu5LNoQ5UWD9Jmgtdxp_kpjsMuTTBnxg5RF_Y", + "y": "X_3HJBcKFQEG35PZbEOBn8u9_z8V1F9V1Kv-Vh0aSzmH-y9aOuDJUE3D4Hvmi5l7", + }) + }, + }; + pub static ref BOB_SECRET_KEY_AGREEMENT_KEY_P384_2: Secret = Secret { + id: "did:example:bob#key-p384-2".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!( + { + "kty": "EC", + "d": "OiwhRotK188BtbQy0XBO8PljSKYI6CCD-nE_ZUzK7o81tk3imDOuQ-jrSWaIkI-T", + "crv": "P-384", + "x": "2x3HOTvR8e-Tu6U4UqMd1wUWsNXMD0RgIunZTMcZsS-zWOwDgsrhYVHmv3k_DjV3", + "y": "W9LLaBjlWYcXUxOf6ECSfcXKaC3-K9z4hCoP0PS87Q_4ExMgIwxVCXUEB6nf0GDd", + }) + }, + }; + pub static ref BOB_SECRET_KEY_AGREEMENT_KEY_P521_1: Secret = Secret { + id: "did:example:bob#key-p521-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!( + { + "kty": "EC", + "d": "AV5ocjvy7PkPgNrSuvCxtG70NMj6iTabvvjSLbsdd8OdI9HlXYlFR7RdBbgLUTruvaIRhjEAE9gNTH6rWUIdfuj6", + "crv": "P-521", + "x": "Af9O5THFENlqQbh2Ehipt1Yf4gAd9RCa3QzPktfcgUIFADMc4kAaYVViTaDOuvVS2vMS1KZe0D5kXedSXPQ3QbHi", + "y": "ATZVigRQ7UdGsQ9j-omyff6JIeeUv3CBWYsZ0l6x3C_SYqhqVV7dEG-TafCCNiIxs8qeUiXQ8cHWVclqkH4Lo1qH", + }) + }, + }; + pub static ref BOB_SECRET_KEY_AGREEMENT_KEY_P521_2: Secret = Secret { + id: "did:example:bob#key-p521-2".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!( + { + "kty": "EC", + "d": "ABixMEZHsyT7SRw-lY5HxdNOofTZLlwBHwPEJ3spEMC2sWN1RZQylZuvoyOBGJnPxg4-H_iVhNWf_OtgYODrYhCk", + "crv": "P-521", + "x": "ATp_WxCfIK_SriBoStmA0QrJc2pUR1djpen0VdpmogtnKxJbitiPq-HJXYXDKriXfVnkrl2i952MsIOMfD2j0Ots", + "y": "AEJipR0Dc-aBZYDqN51SKHYSWs9hM58SmRY1MxgXANgZrPaq1EeGMGOjkbLMEJtBThdjXhkS5VlXMkF0cYhZELiH", + }) + }, + }; + pub static ref BOB_SECRETS: Vec = vec![ + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1.clone(), + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.clone(), + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3.clone(), + BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.clone(), + BOB_SECRET_KEY_AGREEMENT_KEY_P256_2.clone(), + BOB_SECRET_KEY_AGREEMENT_KEY_P384_1.clone(), + BOB_SECRET_KEY_AGREEMENT_KEY_P384_2.clone(), + BOB_SECRET_KEY_AGREEMENT_KEY_P521_1.clone(), + BOB_SECRET_KEY_AGREEMENT_KEY_P521_2.clone(), + ]; +} diff --git a/affinidi-messaging-didcomm/src/test_vectors/secrets/charlie.rs b/affinidi-messaging-didcomm/src/test_vectors/secrets/charlie.rs new file mode 100644 index 0000000..1ca33a7 --- /dev/null +++ b/affinidi-messaging-didcomm/src/test_vectors/secrets/charlie.rs @@ -0,0 +1,38 @@ +use lazy_static::lazy_static; +use serde_json::json; + +use crate::affinidi_messaging_didcomm::secrets::{Secret, SecretMaterial, SecretType}; + +lazy_static! { + pub static ref CHARLIE_SECRET_KEY_AGREEMENT_KEY_X25519: Secret = Secret { + id: "did:example:charlie#key-x25519-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!({ + "kty": "OKP", + "crv": "X25519", + "x": "nTiVFj7DChMsETDdxd5dIzLAJbSQ4j4UG6ZU1ogLNlw", + "d": "Z-BsgFe-eCvhuZlCBX5BV2XiDE2M92gkaORCe68YdZI", + }) + }, + }; + pub static ref CHARLIE_SECRET_AUTH_KEY_ED25519: Secret = Secret { + id: "did:key:z6MkhKzjHrZKpxHqmW9x1BVxgKZ9n7N1WXE3jTtJC26PYASp#z6MkhKzjHrZKpxHqmW9x1BVxgKZ9n7N1WXE3jTtJC26PYASp".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!({ + "kty": "OKP", + "use": "sig", + "alg": "EdDSA", + "kid": "9deaf520-700d-4806-a559-3212cf92567d", + "crv": "Ed25519", + "x": "KrathNH2Ijma8XsC_jstmWPL7RCaGYOCSCn00WdKozU", + "d": "QxH6U2ZvAe4G2zqbBjKSfGOYdyGAvIpZiPSq7Z9a9ZM" + }) + }, + }; + pub static ref CHARLIE_SECRETS: Vec = vec![ + CHARLIE_SECRET_KEY_AGREEMENT_KEY_X25519.clone(), + CHARLIE_SECRET_AUTH_KEY_ED25519.clone(), + ]; +} diff --git a/affinidi-messaging-didcomm/src/test_vectors/secrets/charlie_rotated_to_alice.rs b/affinidi-messaging-didcomm/src/test_vectors/secrets/charlie_rotated_to_alice.rs new file mode 100644 index 0000000..08788f0 --- /dev/null +++ b/affinidi-messaging-didcomm/src/test_vectors/secrets/charlie_rotated_to_alice.rs @@ -0,0 +1,25 @@ +use lazy_static::lazy_static; + +use crate::affinidi_messaging_didcomm::secrets::Secret; + +use super::{ + alice::{ + ALICE_SECRET_AUTH_KEY_ED25519, ALICE_SECRET_AUTH_KEY_P256, ALICE_SECRET_AUTH_KEY_SECP256K1, + ALICE_SECRET_KEY_AGREEMENT_KEY_P256, ALICE_SECRET_KEY_AGREEMENT_KEY_P521, + ALICE_SECRET_KEY_AGREEMENT_KEY_X25519, + }, + charlie::{CHARLIE_SECRET_AUTH_KEY_ED25519, CHARLIE_SECRET_KEY_AGREEMENT_KEY_X25519}, +}; + +lazy_static! { + pub static ref CHARLIE_ROTATED_TO_ALICE_SECRETS: Vec = vec![ + CHARLIE_SECRET_KEY_AGREEMENT_KEY_X25519.clone(), + CHARLIE_SECRET_AUTH_KEY_ED25519.clone(), + ALICE_SECRET_AUTH_KEY_ED25519.clone(), + ALICE_SECRET_AUTH_KEY_P256.clone(), + ALICE_SECRET_AUTH_KEY_SECP256K1.clone(), + ALICE_SECRET_KEY_AGREEMENT_KEY_X25519.clone(), + ALICE_SECRET_KEY_AGREEMENT_KEY_P256.clone(), + ALICE_SECRET_KEY_AGREEMENT_KEY_P521.clone(), + ]; +} diff --git a/affinidi-messaging-didcomm/src/test_vectors/secrets/mediator1.rs b/affinidi-messaging-didcomm/src/test_vectors/secrets/mediator1.rs new file mode 100644 index 0000000..d2275e2 --- /dev/null +++ b/affinidi-messaging-didcomm/src/test_vectors/secrets/mediator1.rs @@ -0,0 +1,68 @@ +use lazy_static::lazy_static; +use serde_json::json; + +use crate::affinidi_messaging_didcomm::secrets::{Secret, SecretMaterial, SecretType}; + +lazy_static! { + pub static ref MEDIATOR1_SECRET_KEY_AGREEMENT_KEY_X25519_1: Secret = Secret { + id: "did:example:mediator1#key-x25519-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!( + { + "kty": "OKP", + "d": "b9NnuOCB0hm7YGNvaE9DMhwH_wjZA1-gWD6dA0JWdL0", + "crv": "X25519", + "x": "GDTrI66K0pFfO54tlCSvfjjNapIs44dzpneBgyx0S3E", + }) + }, + }; + pub static ref MEDIATOR1_SECRET_KEY_AGREEMENT_KEY_P256_1: Secret = Secret { + id: "did:example:mediator1#key-p256-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!( + { + "kty": "EC", + "d": "PgwHnlXxt8pwR6OCTUwwWx-P51BiLkFZyqHzquKddXQ", + "crv": "P-256", + "x": "FQVaTOksf-XsCUrt4J1L2UGvtWaDwpboVlqbKBY2AIo", + "y": "6XFB9PYo7dyC5ViJSO9uXNYkxTJWn0d_mqJ__ZYhcNY", + }) + }, + }; + pub static ref MEDIATOR1_SECRET_KEY_AGREEMENT_KEY_P384_1: Secret = Secret { + id: "did:example:mediator1#key-p384-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!( + { + "kty": "EC", + "d": "ajqcWbYA0UDBKfAhkSkeiVjMMt8l-5rcknvEv9t_Os6M8s-HisdywvNCX4CGd_xY", + "crv": "P-384", + "x": "MvnE_OwKoTcJVfHyTX-DLSRhhNwlu5LNoQ5UWD9Jmgtdxp_kpjsMuTTBnxg5RF_Y", + "y": "X_3HJBcKFQEG35PZbEOBn8u9_z8V1F9V1Kv-Vh0aSzmH-y9aOuDJUE3D4Hvmi5l7", + }) + }, + }; + pub static ref MEDIATOR1_SECRET_KEY_AGREEMENT_KEY_P521_1: Secret = Secret { + id: "did:example:mediator1#key-p521-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!( + { + "kty": "EC", + "d": "AV5ocjvy7PkPgNrSuvCxtG70NMj6iTabvvjSLbsdd8OdI9HlXYlFR7RdBbgLUTruvaIRhjEAE9gNTH6rWUIdfuj6", + "crv": "P-521", + "x": "Af9O5THFENlqQbh2Ehipt1Yf4gAd9RCa3QzPktfcgUIFADMc4kAaYVViTaDOuvVS2vMS1KZe0D5kXedSXPQ3QbHi", + "y": "ATZVigRQ7UdGsQ9j-omyff6JIeeUv3CBWYsZ0l6x3C_SYqhqVV7dEG-TafCCNiIxs8qeUiXQ8cHWVclqkH4Lo1qH", + }) + }, + }; + pub static ref MEDIATOR1_SECRETS: Vec = vec![ + MEDIATOR1_SECRET_KEY_AGREEMENT_KEY_X25519_1.clone(), + MEDIATOR1_SECRET_KEY_AGREEMENT_KEY_P256_1.clone(), + MEDIATOR1_SECRET_KEY_AGREEMENT_KEY_P384_1.clone(), + MEDIATOR1_SECRET_KEY_AGREEMENT_KEY_P521_1.clone(), + ]; +} diff --git a/affinidi-messaging-didcomm/src/test_vectors/secrets/mediator2.rs b/affinidi-messaging-didcomm/src/test_vectors/secrets/mediator2.rs new file mode 100644 index 0000000..04fa55d --- /dev/null +++ b/affinidi-messaging-didcomm/src/test_vectors/secrets/mediator2.rs @@ -0,0 +1,68 @@ +use lazy_static::lazy_static; +use serde_json::json; + +use crate::affinidi_messaging_didcomm::secrets::{Secret, SecretMaterial, SecretType}; + +lazy_static! { + pub static ref MEDIATOR2_SECRET_KEY_AGREEMENT_KEY_X25519_1: Secret = Secret { + id: "did:example:mediator2#key-x25519-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!( + { + "kty": "OKP", + "d": "b9NnuOCB0hm7YGNvaE9DMhwH_wjZA1-gWD6dA0JWdL0", + "crv": "X25519", + "x": "GDTrI66K0pFfO54tlCSvfjjNapIs44dzpneBgyx0S3E", + }) + }, + }; + pub static ref MEDIATOR2_SECRET_KEY_AGREEMENT_KEY_P256_1: Secret = Secret { + id: "did:example:mediator2#key-p256-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!( + { + "kty": "EC", + "d": "PgwHnlXxt8pwR6OCTUwwWx-P51BiLkFZyqHzquKddXQ", + "crv": "P-256", + "x": "FQVaTOksf-XsCUrt4J1L2UGvtWaDwpboVlqbKBY2AIo", + "y": "6XFB9PYo7dyC5ViJSO9uXNYkxTJWn0d_mqJ__ZYhcNY", + }) + }, + }; + pub static ref MEDIATOR2_SECRET_KEY_AGREEMENT_KEY_P384_1: Secret = Secret { + id: "did:example:mediator2#key-p384-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!( + { + "kty": "EC", + "d": "ajqcWbYA0UDBKfAhkSkeiVjMMt8l-5rcknvEv9t_Os6M8s-HisdywvNCX4CGd_xY", + "crv": "P-384", + "x": "MvnE_OwKoTcJVfHyTX-DLSRhhNwlu5LNoQ5UWD9Jmgtdxp_kpjsMuTTBnxg5RF_Y", + "y": "X_3HJBcKFQEG35PZbEOBn8u9_z8V1F9V1Kv-Vh0aSzmH-y9aOuDJUE3D4Hvmi5l7", + }) + }, + }; + pub static ref MEDIATOR2_SECRET_KEY_AGREEMENT_KEY_P521_1: Secret = Secret { + id: "did:example:mediator2#key-p521-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!( + { + "kty": "EC", + "d": "AV5ocjvy7PkPgNrSuvCxtG70NMj6iTabvvjSLbsdd8OdI9HlXYlFR7RdBbgLUTruvaIRhjEAE9gNTH6rWUIdfuj6", + "crv": "P-521", + "x": "Af9O5THFENlqQbh2Ehipt1Yf4gAd9RCa3QzPktfcgUIFADMc4kAaYVViTaDOuvVS2vMS1KZe0D5kXedSXPQ3QbHi", + "y": "ATZVigRQ7UdGsQ9j-omyff6JIeeUv3CBWYsZ0l6x3C_SYqhqVV7dEG-TafCCNiIxs8qeUiXQ8cHWVclqkH4Lo1qH", + }) + }, + }; + pub static ref MEDIATOR2_SECRETS: Vec = vec![ + MEDIATOR2_SECRET_KEY_AGREEMENT_KEY_X25519_1.clone(), + MEDIATOR2_SECRET_KEY_AGREEMENT_KEY_P256_1.clone(), + MEDIATOR2_SECRET_KEY_AGREEMENT_KEY_P384_1.clone(), + MEDIATOR2_SECRET_KEY_AGREEMENT_KEY_P521_1.clone(), + ]; +} diff --git a/affinidi-messaging-didcomm/src/test_vectors/secrets/mediator3.rs b/affinidi-messaging-didcomm/src/test_vectors/secrets/mediator3.rs new file mode 100644 index 0000000..d26326f --- /dev/null +++ b/affinidi-messaging-didcomm/src/test_vectors/secrets/mediator3.rs @@ -0,0 +1,68 @@ +use lazy_static::lazy_static; +use serde_json::json; + +use crate::affinidi_messaging_didcomm::secrets::{Secret, SecretMaterial, SecretType}; + +lazy_static! { + pub static ref MEDIATOR3_SECRET_KEY_AGREEMENT_KEY_X25519_1: Secret = Secret { + id: "did:example:mediator3#key-x25519-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!( + { + "kty": "OKP", + "d": "b9NnuOCB0hm7YGNvaE9DMhwH_wjZA1-gWD6dA0JWdL0", + "crv": "X25519", + "x": "GDTrI66K0pFfO54tlCSvfjjNapIs44dzpneBgyx0S3E", + }) + }, + }; + pub static ref MEDIATOR3_SECRET_KEY_AGREEMENT_KEY_P256_1: Secret = Secret { + id: "did:example:mediator3#key-p256-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!( + { + "kty": "EC", + "d": "PgwHnlXxt8pwR6OCTUwwWx-P51BiLkFZyqHzquKddXQ", + "crv": "P-256", + "x": "FQVaTOksf-XsCUrt4J1L2UGvtWaDwpboVlqbKBY2AIo", + "y": "6XFB9PYo7dyC5ViJSO9uXNYkxTJWn0d_mqJ__ZYhcNY", + }) + }, + }; + pub static ref MEDIATOR3_SECRET_KEY_AGREEMENT_KEY_P384_1: Secret = Secret { + id: "did:example:mediator3#key-p384-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!( + { + "kty": "EC", + "d": "ajqcWbYA0UDBKfAhkSkeiVjMMt8l-5rcknvEv9t_Os6M8s-HisdywvNCX4CGd_xY", + "crv": "P-384", + "x": "MvnE_OwKoTcJVfHyTX-DLSRhhNwlu5LNoQ5UWD9Jmgtdxp_kpjsMuTTBnxg5RF_Y", + "y": "X_3HJBcKFQEG35PZbEOBn8u9_z8V1F9V1Kv-Vh0aSzmH-y9aOuDJUE3D4Hvmi5l7", + }) + }, + }; + pub static ref MEDIATOR3_SECRET_KEY_AGREEMENT_KEY_P521_1: Secret = Secret { + id: "did:example:mediator3#key-p521-1".into(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!( + { + "kty": "EC", + "d": "AV5ocjvy7PkPgNrSuvCxtG70NMj6iTabvvjSLbsdd8OdI9HlXYlFR7RdBbgLUTruvaIRhjEAE9gNTH6rWUIdfuj6", + "crv": "P-521", + "x": "Af9O5THFENlqQbh2Ehipt1Yf4gAd9RCa3QzPktfcgUIFADMc4kAaYVViTaDOuvVS2vMS1KZe0D5kXedSXPQ3QbHi", + "y": "ATZVigRQ7UdGsQ9j-omyff6JIeeUv3CBWYsZ0l6x3C_SYqhqVV7dEG-TafCCNiIxs8qeUiXQ8cHWVclqkH4Lo1qH", + }) + }, + }; + pub static ref MEDIATOR3_SECRETS: Vec = vec![ + MEDIATOR3_SECRET_KEY_AGREEMENT_KEY_X25519_1.clone(), + MEDIATOR3_SECRET_KEY_AGREEMENT_KEY_P256_1.clone(), + MEDIATOR3_SECRET_KEY_AGREEMENT_KEY_P384_1.clone(), + MEDIATOR3_SECRET_KEY_AGREEMENT_KEY_P521_1.clone(), + ]; +} diff --git a/affinidi-messaging-didcomm/src/test_vectors/secrets/mod.rs b/affinidi-messaging-didcomm/src/test_vectors/secrets/mod.rs new file mode 100644 index 0000000..de14887 --- /dev/null +++ b/affinidi-messaging-didcomm/src/test_vectors/secrets/mod.rs @@ -0,0 +1,35 @@ +mod alice; +mod bob; +mod charlie; +mod charlie_rotated_to_alice; +mod mediator1; +mod mediator2; +mod mediator3; + +// TODO: Remove allow +#[allow(unused_imports)] +pub use mediator1::*; + +// TODO: Remove allow +#[allow(unused_imports)] +pub use mediator2::*; + +// TODO: Remove allow +#[allow(unused_imports)] +pub use mediator3::*; + +// TODO: Remove allow +#[allow(unused_imports)] +pub use alice::*; + +// TODO: Remove allow +#[allow(unused_imports)] +pub use bob::*; + +// TODO: Remove allow +#[allow(unused_imports)] +pub use charlie::*; + +// TODO: Remove allow +#[allow(unused_imports)] +pub use charlie_rotated_to_alice::*; diff --git a/affinidi-messaging-didcomm/src/test_vectors/signed.rs b/affinidi-messaging-didcomm/src/test_vectors/signed.rs new file mode 100644 index 0000000..f301ee4 --- /dev/null +++ b/affinidi-messaging-didcomm/src/test_vectors/signed.rs @@ -0,0 +1,38 @@ +pub const SIGNED_MSG_ALICE_KEY_1: &str = r#" +{ + "payload": "eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures": [{ + "protected": "eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRWREU0EifQ", + "signature": "FW33NnvOHV0Ted9-F7GZbkia-vYAfBKtH4oBxbrttWAhBZ6UFJMxcGjL3lwOl4YohI3kyyd08LHPWNMgP2EVCQ", + "header": { + "kid": "did:example:alice#key-1" + } + }] +} +"#; + +pub const SIGNED_MSG_ALICE_KEY_2: &str = r#" +{ + "payload": "eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures": [{ + "protected": "eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRVMyNTYifQ", + "signature": "gcW3lVifhyR48mLHbbpnGZQuziskR5-wXf6IoBlpa9SzERfSG9I7oQ9pssmHZwbvJvyMvxskpH5oudw1W3X5Qg", + "header": { + "kid": "did:example:alice#key-2" + } + }] +} +"#; + +pub const SIGNED_MSG_ALICE_KEY_3: &str = r#" +{ + "payload": "eyJpZCI6IjEyMzQ1Njc4OTAiLCJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXBsYWluK2pzb24iLCJ0eXBlIjoiaHR0cDovL2V4YW1wbGUuY29tL3Byb3RvY29scy9sZXRzX2RvX2x1bmNoLzEuMC9wcm9wb3NhbCIsImZyb20iOiJkaWQ6ZXhhbXBsZTphbGljZSIsInRvIjpbImRpZDpleGFtcGxlOmJvYiJdLCJjcmVhdGVkX3RpbWUiOjE1MTYyNjkwMjIsImV4cGlyZXNfdGltZSI6MTUxNjM4NTkzMSwiYm9keSI6eyJtZXNzYWdlc3BlY2lmaWNhdHRyaWJ1dGUiOiJhbmQgaXRzIHZhbHVlIn19", + "signatures": [{ + "protected": "eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLXNpZ25lZCtqc29uIiwiYWxnIjoiRVMyNTZLIn0", + "signature": "EGjhIcts6tqiJgqtxaTiTY3EUvL-_rLjn9lxaZ4eRUwa1-CS1nknZoyJWbyY5NQnUafWh5nvCtQpdpMyzH3blw", + "header": { + "kid": "did:example:alice#key-3" + } + }] +} +"#; diff --git a/affinidi-messaging-didcomm/src/utils/crypto.rs b/affinidi-messaging-didcomm/src/utils/crypto.rs new file mode 100644 index 0000000..670ddf3 --- /dev/null +++ b/affinidi-messaging-didcomm/src/utils/crypto.rs @@ -0,0 +1,258 @@ +use askar_crypto::{ + alg::{ + aes::{A128Kw, A256Kw, AesKey}, + ed25519::Ed25519KeyPair, + k256::K256KeyPair, + p256::P256KeyPair, + x25519::X25519KeyPair, + }, + buffer::SecretBytes, + encrypt::KeyAeadInPlace, + kdf::{ecdh_1pu::Ecdh1PU, ecdh_es::EcdhEs, FromKeyDerivation, KeyExchange}, + repr::{KeySecretBytes, ToSecretBytes}, +}; +use ssi::JWK; + +use crate::error::{err_msg, Error, ErrorKind, Result}; + +/// Note this trait is compatible with KW algorithms only +pub(crate) trait KeyWrap: KeyAeadInPlace { + fn wrap_key(&self, key: &K) -> Result { + let params = self.aead_params(); + + let key_len = key.secret_bytes_length().map_err(|err| { + Error::msg( + ErrorKind::InvalidState, + format!("{}: {}", "Unable get key len", err.message()), + ) + })?; + + let mut buf = SecretBytes::with_capacity(key_len + params.tag_length); + + key.write_secret_bytes(&mut buf).map_err(|err| { + Error::msg( + ErrorKind::InvalidState, + format!("{}: {}", "Unable encrypt", err.message()), + ) + })?; + + self.encrypt_in_place(&mut buf, &[], &[]).map_err(|err| { + Error::msg( + ErrorKind::InvalidState, + format!("{}: {}", "Unable encrypt", err.message()), + ) + })?; + + Ok(buf) + } + + fn unwrap_key(&self, ciphertext: &[u8]) -> Result { + let mut buf = SecretBytes::from_slice(ciphertext); + + self.decrypt_in_place(&mut buf, &[], &[]).map_err(|err| { + Error::msg( + ErrorKind::Malformed, + format!("{}: {}", "Unable decrypt key", err.message()), + ) + })?; + + let key = K::from_secret_bytes(buf.as_ref()).map_err(|err| { + Error::msg( + ErrorKind::Malformed, + format!("{}: {}", "Unable create key", err.message()), + ) + })?; + + Ok(key) + } +} + +impl KeyWrap for AesKey {} + +impl KeyWrap for AesKey {} + +#[allow(clippy::too_many_arguments)] +pub(crate) trait JoseKDF { + fn derive_key( + ephem_key: &Key, + send_key: Option<&Key>, + recip_key: &Key, + alg: &[u8], + apu: &[u8], + apv: &[u8], + cc_tag: &[u8], + receive: bool, + ) -> Result; +} + +impl JoseKDF + for Ecdh1PU<'_, Key> +{ + fn derive_key( + ephem_key: &Key, + send_key: Option<&Key>, + recip_key: &Key, + alg: &[u8], + apu: &[u8], + apv: &[u8], + cc_tag: &[u8], + receive: bool, + ) -> Result { + let send_key = send_key + .ok_or_else(|| err_msg(ErrorKind::InvalidState, "No sender key for ecdh-1pu"))?; + let deriviation = Ecdh1PU::new( + ephem_key, send_key, recip_key, alg, apu, apv, cc_tag, receive, + ); + + let kw = KW::from_key_derivation(deriviation).map_err(|err| { + Error::msg( + ErrorKind::InvalidState, + format!("{}: {}", "Unable derive kw", err.message()), + ) + })?; + + Ok(kw) + } +} + +impl JoseKDF + for EcdhEs<'_, Key> +{ + fn derive_key( + ephem_key: &Key, + _send_key: Option<&Key>, + recip_key: &Key, + alg: &[u8], + apu: &[u8], + apv: &[u8], + _cc_tag: &[u8], + receive: bool, + ) -> Result { + let deriviation = EcdhEs::new(ephem_key, recip_key, alg, apu, apv, receive); + + let kw = KW::from_key_derivation(deriviation).map_err(|err| { + Error::msg( + ErrorKind::InvalidState, + format!("{}: {}", "Unable derive kw", err.message()), + ) + })?; + + Ok(kw) + } +} + +#[derive(Debug, PartialEq, Eq)] +pub(crate) enum KnownKeyAlg { + Ed25519, + X25519, + P256, + K256, + Unsupported, +} + +#[derive(Debug)] +pub enum KnownKeyPair { + Ed25519(Ed25519KeyPair), + X25519(X25519KeyPair), + P256(P256KeyPair), + K256(K256KeyPair), +} + +pub trait AsKnownKeyPair { + fn key_alg(&self, jwk: &JWK) -> KnownKeyAlg; + fn as_key_pair(&self, jwk: &JWK) -> Result; + + fn as_ed25519(&self, jwk: &JWK) -> Result { + if self.key_alg(jwk) != KnownKeyAlg::Ed25519 { + Err(err_msg(ErrorKind::InvalidState, "Unexpected key alg"))? + } + + match self.as_key_pair(jwk)? { + KnownKeyPair::Ed25519(k) => Ok(k), + _ => Err(err_msg(ErrorKind::InvalidState, "Unexpected key pair type"))?, + } + } + + fn as_x25519(&self, jwk: &JWK) -> Result { + if self.key_alg(jwk) != KnownKeyAlg::X25519 { + Err(err_msg(ErrorKind::InvalidState, "Unexpected key alg"))? + } + + match self.as_key_pair(jwk)? { + KnownKeyPair::X25519(k) => Ok(k), + _ => Err(err_msg(ErrorKind::InvalidState, "Unexpected key pair type"))?, + } + } + + fn as_p256(&self, jwk: &JWK) -> Result { + if self.key_alg(jwk) != KnownKeyAlg::P256 { + Err(err_msg(ErrorKind::InvalidState, "Unexpected key alg"))? + } + + match self.as_key_pair(jwk)? { + KnownKeyPair::P256(k) => Ok(k), + _ => Err(err_msg(ErrorKind::InvalidState, "Unexpected key pair type"))?, + } + } + + fn as_k256(&self, jwk: &JWK) -> Result { + if self.key_alg(jwk) != KnownKeyAlg::K256 { + Err(err_msg(ErrorKind::InvalidState, "Unexpected key alg"))? + } + + match self.as_key_pair(jwk)? { + KnownKeyPair::K256(k) => Ok(k), + _ => Err(err_msg(ErrorKind::InvalidState, "Unexpected key pair type"))?, + } + } +} + +/// Older trait form original crate +pub trait AsKnownKeyPairSecret { + fn key_alg(&self) -> KnownKeyAlg; + fn as_key_pair(&self) -> Result; + + fn as_ed25519(&self) -> Result { + if self.key_alg() != KnownKeyAlg::Ed25519 { + Err(err_msg(ErrorKind::InvalidState, "Unexpected key alg"))? + } + + match self.as_key_pair()? { + KnownKeyPair::Ed25519(k) => Ok(k), + _ => Err(err_msg(ErrorKind::InvalidState, "Unexpected key pair type"))?, + } + } + + fn as_x25519(&self) -> Result { + if self.key_alg() != KnownKeyAlg::X25519 { + Err(err_msg(ErrorKind::InvalidState, "Unexpected key alg"))? + } + + match self.as_key_pair()? { + KnownKeyPair::X25519(k) => Ok(k), + _ => Err(err_msg(ErrorKind::InvalidState, "Unexpected key pair type"))?, + } + } + + fn as_p256(&self) -> Result { + if self.key_alg() != KnownKeyAlg::P256 { + Err(err_msg(ErrorKind::InvalidState, "Unexpected key alg"))? + } + + match self.as_key_pair()? { + KnownKeyPair::P256(k) => Ok(k), + _ => Err(err_msg(ErrorKind::InvalidState, "Unexpected key pair type"))?, + } + } + + fn as_k256(&self) -> Result { + if self.key_alg() != KnownKeyAlg::K256 { + Err(err_msg(ErrorKind::InvalidState, "Unexpected key alg"))? + } + + match self.as_key_pair()? { + KnownKeyPair::K256(k) => Ok(k), + _ => Err(err_msg(ErrorKind::InvalidState, "Unexpected key pair type"))?, + } + } +} diff --git a/affinidi-messaging-didcomm/src/utils/mod.rs b/affinidi-messaging-didcomm/src/utils/mod.rs new file mode 100644 index 0000000..78dad66 --- /dev/null +++ b/affinidi-messaging-didcomm/src/utils/mod.rs @@ -0,0 +1,2 @@ +pub(crate) mod crypto; +pub(crate) mod serde; diff --git a/affinidi-messaging-didcomm/src/utils/serde.rs b/affinidi-messaging-didcomm/src/utils/serde.rs new file mode 100644 index 0000000..fa661d2 --- /dev/null +++ b/affinidi-messaging-didcomm/src/utils/serde.rs @@ -0,0 +1,3 @@ +pub(crate) fn _true() -> bool { + true +} diff --git a/affinidi-messaging-didcomm/uniffi/.gitignore b/affinidi-messaging-didcomm/uniffi/.gitignore new file mode 100644 index 0000000..a7dc619 --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/.gitignore @@ -0,0 +1,9 @@ +/target +**/*.rs.bk +Cargo.lock +bin/ +pkg/ +wasm-pack.log +uniffi-rs +uniffi-swift +uniffi-kotlin diff --git a/affinidi-messaging-didcomm/uniffi/Cargo.toml b/affinidi-messaging-didcomm/uniffi/Cargo.toml new file mode 100644 index 0000000..545b922 --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = 'didcomm-uniffi' +version = '0.4.1' +authors = ['Vyacheslav Gudkov '] +edition = '2018' +description = 'FFI wrapper for DIDComm' +license = 'Apache-2.0' +repository = 'https://github.com/sicpa-dlab/didcomm-rust' + +[lib] +crate-type = ['cdylib'] + +[dependencies.didcomm_core] +path = '..' +features = ['uniffi'] +package = "didcomm" + +[dev-dependencies.didcomm_core] +path = '..' +features = ['testvectors'] +package = "didcomm" + +[dependencies] +uniffi = "0.27" +lazy_static = "1.3" +futures = { version = "0.3.17", features = ["thread-pool"] } +num_cpus = "1.8.0" +async-trait = '0.1' +serde_json = '1.0' + +[dev-dependencies.tokio] +version = '1.9' +features = ['rt', 'macros'] + +[build-dependencies] +uniffi_build = { version = "0.27", features = ["builtin-bindgen"] } diff --git a/affinidi-messaging-didcomm/uniffi/LICENSE b/affinidi-messaging-didcomm/uniffi/LICENSE new file mode 100644 index 0000000..1b5ec8b --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/LICENSE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/affinidi-messaging-didcomm/uniffi/README.md b/affinidi-messaging-didcomm/uniffi/README.md new file mode 100644 index 0000000..5852555 --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/README.md @@ -0,0 +1,31 @@ +## DIDComm FFI +DIDComm bindings based on [uniffi-rs](https://github.com/mozilla/uniffi-rs). + +It's used for [DIDComm Swift](../wrappers/swift) wrapper. + +DIDComm uniffi has the following specific comparing to the core DIDComm crate: +- Callback-based versions of pack/unpack functions and DID/Secret resolver interfaces instead of async onces. +These pack/unpack functions wrap the corresponding async futures and execute them in a thread pool executor. +The result is passed to a callback function. +- The language specific bidnings are generated for the API exposed in [didcomm.udl](src/didcomm.udl) file. + +### Swift Build +1. Build: +``` +cargo build --release +``` + +2. Install uniffi_bindgen: +``` +cargo install uniffi_bindgen --version $(cargo pkgid uniffi | cut -f 2 -d '@') +``` + +3. Generate Swift binding: +``` +uniffi-bindgen generate src/didcomm.udl --language swift -o ../wrappers/swift/didcomm +``` + +4. Compile a Swift module: +``` +swiftc -module-name didcomm -emit-library -o ../wrappers/swift/didcomm/libdidcomm.dylib -emit-module -emit-module-path ../wrappers/swift/didcomm -parse-as-library -L ./target/debug/ -ldidcomm_uniffi -Xcc -fmodule-map-file=../wrappers/swift/didcomm/didcommFFI.modulemap ../wrappers/swift/didcomm/didcomm.swift +``` \ No newline at end of file diff --git a/affinidi-messaging-didcomm/uniffi/build.rs b/affinidi-messaging-didcomm/uniffi/build.rs new file mode 100644 index 0000000..0834284 --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/build.rs @@ -0,0 +1,3 @@ +fn main() { + uniffi_build::generate_scaffolding("./src/didcomm.udl").unwrap(); +} diff --git a/affinidi-messaging-didcomm/uniffi/src/common.rs b/affinidi-messaging-didcomm/uniffi/src/common.rs new file mode 100644 index 0000000..f4f47aa --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/common.rs @@ -0,0 +1,89 @@ +use std::{ + cell::RefCell, + cmp, + sync::{Arc, Mutex}, +}; + +use crate::UniffiCustomTypeConverter; +use didcomm_core::error::{err_msg, ErrorKind, Result, ResultExt, ResultExtNoContext, ToResult}; +use futures::{channel::oneshot, executor::ThreadPool}; +use lazy_static::lazy_static; + +pub enum ErrorCode { + Success = 0, + Error = 1, +} + +lazy_static! { + // Global (lazy inited) instance of future executor + pub(crate) static ref EXECUTOR: ThreadPool = ThreadPool::builder() + .pool_size(cmp::max(8, num_cpus::get())) + .create() + .unwrap(); +} + +// We use `JsonValue` in our UDL. It moves to and from Uniffi bindings via a string. +pub type JsonValue = serde_json::Value; + +// We must implement the UniffiCustomTypeConverter trait. +impl UniffiCustomTypeConverter for JsonValue { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result { + Ok(serde_json::from_str(&val).to_didcomm("Invalid json value")?) + } + + fn from_custom(obj: Self) -> Self::Builtin { + serde_json::to_string(&obj).expect("unable serialize json value") + } +} + +pub(crate) struct OnResultReceiver(oneshot::Receiver>); + +impl OnResultReceiver { + pub(crate) async fn get(self) -> Result { + self.0 + .await + .kind(ErrorKind::InvalidState, "unable receive callback result")? + } +} + +pub struct OnResult(Mutex>>>>); + +impl OnResult { + pub(crate) fn new() -> (Arc, OnResultReceiver) { + let (sender, receiver) = oneshot::channel::>(); + let on_result = Arc::new(OnResult(Mutex::new(RefCell::new(Some(sender))))); + (on_result, OnResultReceiver(receiver)) + } + + pub fn success(&self, result: T) -> std::result::Result<(), ErrorKind> { + let sender = self + .0 + .lock() + .to_error_kind(ErrorKind::InvalidState)? + .replace(None); + match sender { + Some(sender) => sender + .send(Ok(result)) + .to_error_kind(ErrorKind::InvalidState)?, + None => Err(ErrorKind::InvalidState)?, + }; + Ok(()) + } + + pub fn error(&self, err: ErrorKind, msg: String) -> std::result::Result<(), ErrorKind> { + let sender = self + .0 + .lock() + .to_error_kind(ErrorKind::InvalidState)? + .replace(None); + match sender { + Some(sender) => sender + .send(Err(err_msg(err, msg))) + .to_error_kind(ErrorKind::InvalidState)?, + None => Err(ErrorKind::InvalidState)?, + }; + Ok(()) + } +} diff --git a/affinidi-messaging-didcomm/uniffi/src/did/did_resolver.rs b/affinidi-messaging-didcomm/uniffi/src/did/did_resolver.rs new file mode 100644 index 0000000..8d0cfa3 --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/did/did_resolver.rs @@ -0,0 +1,21 @@ +use std::sync::Arc; + +use didcomm_core::did::DIDDoc; + +use crate::{common::OnResult, ErrorCode}; + +/// Represents DID Doc resolver (https://www.w3.org/TR/did-core/#did-resolution). +pub trait DIDResolver: Sync + Send { + /// Resolves a DID document by the given DID. + /// + /// # Params + /// - `did` a DID to be resolved. + /// - `cb` a callback with a result + /// + /// # Returns + /// A result code + /// + fn resolve(&self, did: String, cb: Arc) -> ErrorCode; +} + +pub type OnDIDResolverResult = OnResult>; diff --git a/affinidi-messaging-didcomm/uniffi/src/did/did_resolver_adapter.rs b/affinidi-messaging-didcomm/uniffi/src/did/did_resolver_adapter.rs new file mode 100644 index 0000000..ae23846 --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/did/did_resolver_adapter.rs @@ -0,0 +1,35 @@ +use std::sync::Arc; + +use didcomm_core::{ + did::{DIDDoc, DIDResolver as _DIDResolver}, + error::{ErrorKind, Result, ResultExt}, +}; + +use async_trait::async_trait; + +use crate::{DIDResolver, OnDIDResolverResult}; + +pub(crate) struct DIDResolverAdapter { + did_resolver: Arc>, +} + +impl DIDResolverAdapter { + pub fn new(did_resolver: Arc>) -> Self { + DIDResolverAdapter { did_resolver } + } +} + +#[async_trait] +impl _DIDResolver for DIDResolverAdapter { + async fn resolve(&self, did: &str) -> Result> { + let (cb, receiver) = OnDIDResolverResult::new(); + + self.did_resolver.resolve(String::from(did), cb); + + let res = receiver + .get() + .await + .kind(ErrorKind::InvalidState, "can not resolve DID Doc")?; + Ok(res) + } +} diff --git a/affinidi-messaging-didcomm/uniffi/src/did/mod.rs b/affinidi-messaging-didcomm/uniffi/src/did/mod.rs new file mode 100644 index 0000000..f5de379 --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/did/mod.rs @@ -0,0 +1,6 @@ +pub mod resolvers; + +pub(crate) mod did_resolver; +pub(crate) mod did_resolver_adapter; + +pub use did_resolver::{DIDResolver, OnDIDResolverResult}; diff --git a/affinidi-messaging-didcomm/uniffi/src/did/resolvers/example.rs b/affinidi-messaging-didcomm/uniffi/src/did/resolvers/example.rs new file mode 100644 index 0000000..7cbbceb --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/did/resolvers/example.rs @@ -0,0 +1,33 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use didcomm_core::did::DIDDoc; + +use crate::{common::ErrorCode, did::DIDResolver, OnDIDResolverResult}; + +/// Allows resolve pre-defined did's for `example` and other methods. +pub struct ExampleDIDResolver { + known_dids: Vec, +} + +impl ExampleDIDResolver { + pub fn new(known_dids: Vec) -> Self { + ExampleDIDResolver { known_dids } + } +} + +#[async_trait] +impl DIDResolver for ExampleDIDResolver { + fn resolve(&self, did: String, cb: Arc) -> ErrorCode { + let diddoc = self + .known_dids + .iter() + .find(|ddoc| ddoc.id == did) + .map(|ddoc| ddoc.clone()); + + match cb.success(diddoc) { + Ok(_) => ErrorCode::Success, + Err(_) => ErrorCode::Error, + } + } +} diff --git a/affinidi-messaging-didcomm/uniffi/src/did/resolvers/mod.rs b/affinidi-messaging-didcomm/uniffi/src/did/resolvers/mod.rs new file mode 100644 index 0000000..9b8abc6 --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/did/resolvers/mod.rs @@ -0,0 +1,3 @@ +mod example; + +pub use example::ExampleDIDResolver; diff --git a/affinidi-messaging-didcomm/uniffi/src/didcomm.udl b/affinidi-messaging-didcomm/uniffi/src/didcomm.udl new file mode 100644 index 0000000..93d0f77 --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/didcomm.udl @@ -0,0 +1,352 @@ +[Custom] +typedef string JsonValue; + +// 1. MAIN DIDCOMM INTERFACE + +interface DIDComm { + constructor(DIDResolver did_resolver, SecretsResolver secret_resolver); + + ErrorCode pack_plaintext([ByRef] Message msg, OnPackPlaintextResult cb); + ErrorCode pack_signed([ByRef] Message msg, string sign_by, OnPackSignedResult cb); + ErrorCode pack_encrypted([ByRef] Message msg, string to, string? from, string? sign_by, [ByRef] PackEncryptedOptions options, OnPackEncryptedResult cb); + ErrorCode unpack(string msg, [ByRef] UnpackOptions options, OnUnpackResult cb); + + ErrorCode pack_from_prior([ByRef] FromPrior msg, string? issuer_kid, OnFromPriorPackResult cb); + ErrorCode unpack_from_prior(string from_prior_jwt, OnFromPriorUnpackResult cb); + + ErrorCode wrap_in_forward(string msg, [ByRef] record headers, string to, [ByRef] sequence routing_keys, [ByRef] AnonCryptAlg enc_alg_anon, OnWrapInForwardResult cb); + +}; + +// 2. MESSAGE + +dictionary Message { + string id; + string typ; + string type_; + JsonValue body; + string? from; + sequence? to; + string? thid; + string? pthid; + record extra_headers; + u64? created_time; + u64? expires_time; + string? from_prior; + sequence? attachments; +}; + +dictionary Attachment { + AttachmentData data; + string? id; + string? description; + string? filename; + string? media_type; + string? format; + u64? lastmod_time; + u64? byte_count; +}; + +[Enum] +interface AttachmentData { + Base64(Base64AttachmentData value); + Json(JsonAttachmentData value); + Links(LinksAttachmentData value); +}; + + +dictionary Base64AttachmentData { + string base64; + string? jws; +}; + +dictionary JsonAttachmentData { + JsonValue json; + string? jws; +}; + +dictionary LinksAttachmentData { + sequence links; + string hash; + string? jws; +}; + + + +// 3. ERRORS + +[Error] +enum ErrorKind { + "DIDNotResolved", + "DIDUrlNotFound", + "SecretNotFound", + "Malformed", + "IoError", + "InvalidState", + "NoCompatibleCrypto", + "Unsupported", + "IllegalArgument", +}; + +enum ErrorCode { + "Success", + "Error", +}; + + + +// 4. DID RESOLVER + +callback interface DIDResolver { + ErrorCode resolve(string did, OnDIDResolverResult cb); +}; + +interface OnDIDResolverResult { + [Throws=ErrorKind] + void success(DIDDoc? result); + + [Throws=ErrorKind] + void error(ErrorKind err, string msg); +}; + +dictionary DIDDoc { + string id; + sequence key_agreement; + sequence authentication; + sequence verification_method; + sequence service; +}; + +dictionary VerificationMethod { + string id; + VerificationMethodType type_; + string controller; + VerificationMaterial verification_material; +}; + +[Enum] +interface VerificationMaterial { + JWK(JsonValue public_key_jwk); + Multibase(string public_key_multibase); + Base58(string public_key_base58); +}; + +enum VerificationMethodType { + "JsonWebKey2020", + "X25519KeyAgreementKey2019", + "Ed25519VerificationKey2018", + "EcdsaSecp256k1VerificationKey2019", + "X25519KeyAgreementKey2020", + "Ed25519VerificationKey2020", + "Other", +}; + +dictionary Service { + string id; + ServiceKind service_endpoint; +}; + +[Enum] +interface ServiceKind { + DIDCommMessaging(DIDCommMessagingService value); + Other(JsonValue value); +}; + +dictionary DIDCommMessagingService { + string uri; + sequence? accept; + sequence routing_keys; +}; + +interface ExampleDIDResolver { + constructor(sequence known_dids); + ErrorCode resolve(string did, OnDIDResolverResult cb); +}; + + + + +// 5. SECRETS RESOLVER + +callback interface SecretsResolver { + ErrorCode get_secret(string secretid, OnGetSecretResult cb); // should be in camel case + ErrorCode find_secrets(sequence secretids, OnFindSecretsResult cb); // should be in camel case +}; + +interface OnGetSecretResult { + [Throws=ErrorKind] + void success(Secret? result); + + [Throws=ErrorKind] + void error(ErrorKind err, string msg); +}; + +interface OnFindSecretsResult { + [Throws=ErrorKind] + void success(sequence result); + + [Throws=ErrorKind] + void error(ErrorKind err, string msg); +}; + +dictionary Secret { + string id; + SecretType type_; + SecretMaterial secret_material; +}; + +[Enum] +interface SecretMaterial { + JWK(JsonValue private_key_jwk); + Multibase(string private_key_multibase); + Base58(string private_key_base58); +}; + +enum SecretType { + "JsonWebKey2020", + "X25519KeyAgreementKey2019", + "Ed25519VerificationKey2018", + "EcdsaSecp256k1VerificationKey2019", + "X25519KeyAgreementKey2020", + "Ed25519VerificationKey2020", + "Other", +}; + +interface ExampleSecretsResolver { + constructor(sequence known_secrets); + ErrorCode get_secret(string secret_id, OnGetSecretResult cb); + ErrorCode find_secrets(sequence secret_ids, OnFindSecretsResult cb); +}; + + + + +// 6. PACK SIGNED + +callback interface OnPackSignedResult { + void success(string result, PackSignedMetadata metadata); + void error(ErrorKind err, string msg); +}; + +dictionary PackSignedMetadata { + string sign_by_kid; +}; + + + +// 7. PACK ENCRYPTED + +callback interface OnPackEncryptedResult { + void success(string result, PackEncryptedMetadata metadata); + void error(ErrorKind err, string msg); +}; + +dictionary PackEncryptedMetadata { + MessagingServiceMetadata? messaging_service; + string? from_kid; + string? sign_by_kid; + sequence to_kids; +}; + +dictionary MessagingServiceMetadata { + string id; + string service_endpoint; +}; + +enum AuthCryptAlg { + "A256cbcHs512Ecdh1puA256kw", +}; + +enum AnonCryptAlg { + "A256cbcHs512EcdhEsA256kw", + "Xc20pEcdhEsA256kw", + "A256gcmEcdhEsA256kw", +}; + +dictionary PackEncryptedOptions { + boolean protect_sender; + boolean forward; + record? forward_headers; + string? messaging_service; + AuthCryptAlg enc_alg_auth; + AnonCryptAlg enc_alg_anon; +}; + + + +// 8. PACK PLAINTEXT + +callback interface OnPackPlaintextResult { + void success(string result); + void error(ErrorKind err, string msg); +}; + + + +// 9. UNPACK + +callback interface OnUnpackResult { + void success(Message result, UnpackMetadata metadata); + void error(ErrorKind err, string msg); +}; + +dictionary UnpackMetadata { + boolean encrypted; + boolean authenticated; + boolean non_repudiation; + boolean anonymous_sender; + boolean re_wrapped_in_forward; + string? encrypted_from_kid; + sequence? encrypted_to_kids; + string? sign_from; + string? from_prior_issuer_kid; + AuthCryptAlg? enc_alg_auth; + AnonCryptAlg? enc_alg_anon; + SignAlg? sign_alg; + string? signed_message; + FromPrior? from_prior; +}; + +dictionary UnpackOptions { + boolean expect_decrypt_by_all_keys; + boolean unwrap_re_wrapping_forward; +}; + +enum SignAlg { + "EdDSA", + "ES256", + "ES256K", +}; + +// 10. FROM PRIOR +dictionary FromPrior { + string iss; + string sub; + string? aud; + u64? exp; + u64? nbf; + u64? iat; + string? jti; +}; + +callback interface OnFromPriorPackResult { + void success(string frompriorjwt, string kid); // should be in camel case + void error(ErrorKind err, string msg); +}; + +callback interface OnFromPriorUnpackResult { + void success(FromPrior fromprior, string kid); // should be in camel case + void error(ErrorKind err, string msg); +}; + + +// 11. FORWARD + +callback interface OnWrapInForwardResult { + void success(string result); + void error(ErrorKind err, string msg); +}; + + + +namespace didcomm {}; diff --git a/affinidi-messaging-didcomm/uniffi/src/didcomm/from_prior.rs b/affinidi-messaging-didcomm/uniffi/src/didcomm/from_prior.rs new file mode 100644 index 0000000..3f370f7 --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/didcomm/from_prior.rs @@ -0,0 +1,126 @@ +use didcomm_core::{error::ErrorKind, FromPrior}; + +use crate::DIDComm; + +use crate::common::{ErrorCode, EXECUTOR}; +use crate::did_resolver_adapter::DIDResolverAdapter; +use crate::secrets_resolver_adapter::SecretsResolverAdapter; + +pub trait OnFromPriorPackResult: Sync + Send { + fn success(&self, from_prior_jwt: String, kid: String); + fn error(&self, err: ErrorKind, err_msg: String); +} + +pub trait OnFromPriorUnpackResult: Sync + Send { + fn success(&self, from_prior: FromPrior, kid: String); + fn error(&self, err: ErrorKind, err_msg: String); +} + +impl DIDComm { + pub fn pack_from_prior( + &self, + msg: &FromPrior, + issuer_kid: Option, + cb: Box, + ) -> ErrorCode { + let msg = msg.clone(); + let did_resolver = DIDResolverAdapter::new(self.did_resolver.clone()); + let secret_resolver = SecretsResolverAdapter::new(self.secret_resolver.clone()); + + let future = async move { + msg.pack(issuer_kid.as_deref(), &did_resolver, &secret_resolver) + .await + }; + EXECUTOR.spawn_ok(async move { + match future.await { + Ok((from_prior_jwt, kid)) => cb.success(from_prior_jwt, kid), + Err(err) => cb.error(err.kind(), err.to_string()), + } + }); + + ErrorCode::Success + } + + pub fn unpack_from_prior( + &self, + from_prior_jwt: String, + cb: Box, + ) -> ErrorCode { + let did_resolver = DIDResolverAdapter::new(self.did_resolver.clone()); + + let future = async move { FromPrior::unpack(&from_prior_jwt, &did_resolver).await }; + EXECUTOR.spawn_ok(async move { + match future.await { + Ok((from_prior_jwt, kid)) => cb.success(from_prior_jwt, kid), + Err(err) => cb.error(err.kind(), err.to_string()), + } + }); + + ErrorCode::Success + } +} + +#[cfg(test)] +mod tests { + use didcomm_core::FromPrior; + + use crate::{ + test_helper::{create_did_resolver, get_ok, FromPriorPackResult, FromPriorUnpackResult}, + DIDComm, ExampleSecretsResolver, + }; + use didcomm_core::test_vectors::{ + ALICE_DID, CHARLIE_DID, CHARLIE_ROTATED_TO_ALICE_SECRETS, CHARLIE_SECRET_AUTH_KEY_ED25519, + }; + + #[tokio::test] + async fn pack_from_prior_works() { + let (cb, receiver) = FromPriorPackResult::new(); + + let from_prior = FromPrior::build(CHARLIE_DID.into(), ALICE_DID.into()).finalize(); + + DIDComm::new( + create_did_resolver(), + Box::new(ExampleSecretsResolver::new( + CHARLIE_ROTATED_TO_ALICE_SECRETS.clone(), + )), + ) + .pack_from_prior( + &from_prior, + Some(CHARLIE_SECRET_AUTH_KEY_ED25519.id.clone()), + cb, + ); + + let (_, kid) = get_ok(receiver).await; + assert_eq!(kid, CHARLIE_SECRET_AUTH_KEY_ED25519.id.clone()); + } + + #[tokio::test] + async fn unpack_from_prior_works() { + let (cb, receiver) = FromPriorPackResult::new(); + let did_comm = DIDComm::new( + create_did_resolver(), + Box::new(ExampleSecretsResolver::new( + CHARLIE_ROTATED_TO_ALICE_SECRETS.clone(), + )), + ); + + let from_prior = FromPrior::build(CHARLIE_DID.into(), ALICE_DID.into()) + .exp(1234) + .finalize(); + did_comm.pack_from_prior( + &from_prior, + Some(CHARLIE_SECRET_AUTH_KEY_ED25519.id.clone()), + cb, + ); + let (res, _) = get_ok(receiver).await; + + let (cb, receiver) = FromPriorUnpackResult::new(); + did_comm.unpack_from_prior(res, cb); + let (res, kid) = get_ok(receiver).await; + + assert_eq!(kid, CHARLIE_SECRET_AUTH_KEY_ED25519.id.clone()); + assert_eq!(CHARLIE_DID.clone(), res.iss); + assert_eq!(ALICE_DID.clone(), res.sub); + assert_eq!(Some(1234), res.exp); + } +} diff --git a/affinidi-messaging-didcomm/uniffi/src/didcomm/mod.rs b/affinidi-messaging-didcomm/uniffi/src/didcomm/mod.rs new file mode 100644 index 0000000..d6f91e7 --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/didcomm/mod.rs @@ -0,0 +1,34 @@ +mod from_prior; +mod pack_encrypted; +mod pack_plaintext; +mod pack_signed; +mod protocols; +mod unpack; + +pub use from_prior::{OnFromPriorPackResult, OnFromPriorUnpackResult}; +pub use pack_encrypted::OnPackEncryptedResult; +pub use pack_plaintext::OnPackPlaintextResult; +pub use pack_signed::OnPackSignedResult; +pub use protocols::routing::OnWrapInForwardResult; +pub use unpack::OnUnpackResult; + +use std::sync::Arc; + +use crate::{DIDResolver, SecretsResolver}; + +pub struct DIDComm { + did_resolver: Arc>, + secret_resolver: Arc>, +} + +impl DIDComm { + pub fn new( + did_resolver: Box, + secret_resolver: Box, + ) -> Self { + DIDComm { + did_resolver: Arc::new(did_resolver), + secret_resolver: Arc::new(secret_resolver), + } + } +} diff --git a/affinidi-messaging-didcomm/uniffi/src/didcomm/pack_encrypted.rs b/affinidi-messaging-didcomm/uniffi/src/didcomm/pack_encrypted.rs new file mode 100644 index 0000000..2f86b73 --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/didcomm/pack_encrypted.rs @@ -0,0 +1,158 @@ +use didcomm_core::error::ErrorKind; +use didcomm_core::{Message, PackEncryptedMetadata, PackEncryptedOptions}; + +use crate::common::{ErrorCode, EXECUTOR}; +use crate::did_resolver_adapter::DIDResolverAdapter; +use crate::secrets::secrets_resolver_adapter::SecretsResolverAdapter; +use crate::DIDComm; + +pub trait OnPackEncryptedResult: Sync + Send { + fn success(&self, result: String, metadata: PackEncryptedMetadata); + fn error(&self, err: ErrorKind, err_msg: String); +} + +impl DIDComm { + pub fn pack_encrypted<'a, 'b>( + &self, + msg: &'a Message, + to: String, + from: Option, + sign_by: Option, + options: &'b PackEncryptedOptions, + cb: Box, + ) -> ErrorCode { + let msg = msg.clone(); + let options = options.clone(); + let did_resolver = DIDResolverAdapter::new(self.did_resolver.clone()); + let secret_resolver = SecretsResolverAdapter::new(self.secret_resolver.clone()); + + let future = async move { + msg.pack_encrypted( + &to, + from.as_deref(), + sign_by.as_deref(), + &did_resolver, + &secret_resolver, + &options, + ) + .await + }; + EXECUTOR.spawn_ok(async move { + match future.await { + Ok((result, metadata)) => cb.success(result, metadata), + Err(err) => cb.error(err.kind(), err.to_string()), + } + }); + + ErrorCode::Success + } +} + +#[cfg(test)] +mod tests { + use crate::test_helper::{ + create_did_resolver, create_secrets_resolver, get_error, get_ok, PackResult, + }; + use crate::DIDComm; + use didcomm_core::error::ErrorKind; + use didcomm_core::test_vectors::{ALICE_DID, BOB_DID, MESSAGE_SIMPLE}; + use didcomm_core::{Message, PackEncryptedOptions}; + use serde_json::json; + + #[tokio::test] + async fn pack_encrypted_works() { + let (cb, receiver) = PackResult::new(); + + DIDComm::new(create_did_resolver(), create_secrets_resolver()).pack_encrypted( + &MESSAGE_SIMPLE, + String::from(BOB_DID), + Some(String::from(ALICE_DID)), + Some(String::from(ALICE_DID)), + &PackEncryptedOptions::default(), + cb, + ); + + let res = get_ok(receiver).await; + assert!(res.contains("ciphertext")); + } + + #[tokio::test] + async fn pack_encrypted_works_did_not_found() { + let msg = Message::build( + "example-1".to_owned(), + "example/v1".to_owned(), + json!("example-body"), + ) + .to(String::from("did:unknown:bob")) + .from(ALICE_DID.to_owned()) + .finalize(); + + let (cb, receiver) = PackResult::new(); + + DIDComm::new(create_did_resolver(), create_secrets_resolver()).pack_encrypted( + &msg, + String::from("did:unknown:bob"), + Some(String::from(ALICE_DID)), + Some(String::from(ALICE_DID)), + &PackEncryptedOptions::default(), + cb, + ); + + let res = get_error(receiver).await; + assert_eq!(res.kind(), ErrorKind::DIDNotResolved); + } + + #[tokio::test] + async fn pack_encrypted_works_did_url_not_found() { + let (cb, receiver) = PackResult::new(); + + DIDComm::new(create_did_resolver(), create_secrets_resolver()).pack_encrypted( + &MESSAGE_SIMPLE, + String::from(format!("{}#unknown-fragment", BOB_DID)), + Some(String::from(ALICE_DID)), + Some(String::from(ALICE_DID)), + &PackEncryptedOptions::default(), + cb, + ); + + let res = get_error(receiver).await; + assert_eq!(res.kind(), ErrorKind::DIDUrlNotFound); + } + + #[tokio::test] + async fn pack_encrypted_works_secret_not_found() { + let (cb, receiver) = PackResult::new(); + + DIDComm::new(create_did_resolver(), create_secrets_resolver()).pack_encrypted( + &MESSAGE_SIMPLE, + String::from(BOB_DID), + Some(String::from(format!( + "{}#key-x25519-not-in-secrets-1", + ALICE_DID + ))), + Some(String::from(ALICE_DID)), + &PackEncryptedOptions::default(), + cb, + ); + + let res = get_error(receiver).await; + assert_eq!(res.kind(), ErrorKind::SecretNotFound); + } + + #[tokio::test] + async fn pack_encrypted_works_illegal_argument() { + let (cb, receiver) = PackResult::new(); + + DIDComm::new(create_did_resolver(), create_secrets_resolver()).pack_encrypted( + &MESSAGE_SIMPLE, + String::from("not-a-did"), + Some(String::from(ALICE_DID)), + Some(String::from(ALICE_DID)), + &PackEncryptedOptions::default(), + cb, + ); + + let res = get_error(receiver).await; + assert_eq!(res.kind(), ErrorKind::IllegalArgument); + } +} diff --git a/affinidi-messaging-didcomm/uniffi/src/didcomm/pack_plaintext.rs b/affinidi-messaging-didcomm/uniffi/src/didcomm/pack_plaintext.rs new file mode 100644 index 0000000..8b7126c --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/didcomm/pack_plaintext.rs @@ -0,0 +1,49 @@ +use didcomm_core::error::ErrorKind; +use didcomm_core::Message; + +use crate::common::{ErrorCode, EXECUTOR}; +use crate::did_resolver_adapter::DIDResolverAdapter; +use crate::DIDComm; + +pub trait OnPackPlaintextResult: Sync + Send { + fn success(&self, result: String); + fn error(&self, err: ErrorKind, err_msg: String); +} + +impl DIDComm { + pub fn pack_plaintext(&self, msg: &Message, cb: Box) -> ErrorCode { + let msg = msg.clone(); + let did_resolver = DIDResolverAdapter::new(self.did_resolver.clone()); + + let future = async move { msg.pack_plaintext(&did_resolver).await }; + + EXECUTOR.spawn_ok(async move { + match future.await { + Ok(result) => cb.success(result), + Err(err) => cb.error(err.kind(), err.to_string()), + } + }); + + ErrorCode::Success + } +} + +#[cfg(test)] +mod tests { + + use crate::DIDComm; + + use crate::test_helper::{create_did_resolver, create_secrets_resolver, get_ok, PackResult}; + use didcomm_core::test_vectors::MESSAGE_SIMPLE; + + #[tokio::test] + async fn pack_plaintext_works() { + let (cb, receiver) = PackResult::new(); + + DIDComm::new(create_did_resolver(), create_secrets_resolver()) + .pack_plaintext(&MESSAGE_SIMPLE, cb); + + let res = get_ok(receiver).await; + assert!(res.contains("body")); + } +} diff --git a/affinidi-messaging-didcomm/uniffi/src/didcomm/pack_signed.rs b/affinidi-messaging-didcomm/uniffi/src/didcomm/pack_signed.rs new file mode 100644 index 0000000..9981307 --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/didcomm/pack_signed.rs @@ -0,0 +1,132 @@ +use didcomm_core::Message; +use didcomm_core::{error::ErrorKind, PackSignedMetadata}; + +use crate::common::{ErrorCode, EXECUTOR}; +use crate::did_resolver_adapter::DIDResolverAdapter; +use crate::secrets::secrets_resolver_adapter::SecretsResolverAdapter; +use crate::DIDComm; + +pub trait OnPackSignedResult: Sync + Send { + fn success(&self, result: String, metadata: PackSignedMetadata); + fn error(&self, err: ErrorKind, err_msg: String); +} + +impl DIDComm { + pub fn pack_signed( + &self, + msg: &Message, + sign_by: String, + cb: Box, + ) -> ErrorCode { + let msg = msg.clone(); + let did_resolver = DIDResolverAdapter::new(self.did_resolver.clone()); + let secret_resolver = SecretsResolverAdapter::new(self.secret_resolver.clone()); + + let future = async move { + msg.pack_signed(&sign_by, &did_resolver, &secret_resolver) + .await + }; + + EXECUTOR.spawn_ok(async move { + match future.await { + Ok((result, metadata)) => cb.success(result, metadata), + Err(err) => cb.error(err.kind(), err.to_string()), + } + }); + + ErrorCode::Success + } +} + +#[cfg(test)] +mod tests { + use didcomm_core::error::ErrorKind; + use didcomm_core::Message; + use serde_json::json; + + use crate::test_helper::{ + create_did_resolver, create_secrets_resolver, get_error, get_ok, PackResult, + }; + use crate::DIDComm; + + use didcomm_core::test_vectors::{ALICE_DID, MESSAGE_SIMPLE}; + + #[tokio::test] + async fn pack_signed_works() { + let (cb, receiver) = PackResult::new(); + + DIDComm::new(create_did_resolver(), create_secrets_resolver()).pack_signed( + &MESSAGE_SIMPLE, + String::from(ALICE_DID), + cb, + ); + + let res = get_ok(receiver).await; + assert!(res.contains("payload")); + } + + #[tokio::test] + async fn pack_signed_works_did_not_found() { + let msg = Message::build( + "example-1".to_owned(), + "example/v1".to_owned(), + json!("example-body"), + ) + .to(String::from("did:unknown:bob")) + .from(ALICE_DID.to_owned()) + .finalize(); + + let (cb, receiver) = PackResult::new(); + + DIDComm::new(create_did_resolver(), create_secrets_resolver()).pack_signed( + &msg, + String::from("did:unknown:alice"), + cb, + ); + + let res = get_error(receiver).await; + assert_eq!(res.kind(), ErrorKind::DIDNotResolved); + } + + #[tokio::test] + async fn pack_signed_works_did_url_not_found() { + let (cb, receiver) = PackResult::new(); + + DIDComm::new(create_did_resolver(), create_secrets_resolver()).pack_signed( + &MESSAGE_SIMPLE, + String::from(format!("{}#unknown-fragment", ALICE_DID)), + cb, + ); + + let res = get_error(receiver).await; + assert_eq!(res.kind(), ErrorKind::DIDUrlNotFound); + } + + #[tokio::test] + async fn pack_signed_works_secret_not_found() { + let (cb, receiver) = PackResult::new(); + + DIDComm::new(create_did_resolver(), create_secrets_resolver()).pack_signed( + &MESSAGE_SIMPLE, + String::from(format!("{}#key-not-in-secrets-1", ALICE_DID)), + cb, + ); + + let res = get_error(receiver).await; + assert_eq!(res.kind(), ErrorKind::SecretNotFound); + } + + #[tokio::test] + async fn pack_signed_works_illegal_argument() { + let (cb, receiver) = PackResult::new(); + + DIDComm::new(create_did_resolver(), create_secrets_resolver()).pack_signed( + &MESSAGE_SIMPLE, + String::from("not-a-did"), + cb, + ); + + let res = get_error(receiver).await; + assert_eq!(res.kind(), ErrorKind::IllegalArgument); + } +} diff --git a/affinidi-messaging-didcomm/uniffi/src/didcomm/protocols/mod.rs b/affinidi-messaging-didcomm/uniffi/src/didcomm/protocols/mod.rs new file mode 100644 index 0000000..e78cad0 --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/didcomm/protocols/mod.rs @@ -0,0 +1 @@ +pub mod routing; diff --git a/affinidi-messaging-didcomm/uniffi/src/didcomm/protocols/routing/mod.rs b/affinidi-messaging-didcomm/uniffi/src/didcomm/protocols/routing/mod.rs new file mode 100644 index 0000000..4444272 --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/didcomm/protocols/routing/mod.rs @@ -0,0 +1,264 @@ +use std::collections::HashMap; + +use didcomm_core::{ + algorithms::AnonCryptAlg, error::ErrorKind, protocols::routing::wrap_in_forward, +}; +use serde_json::Value; + +use crate::common::EXECUTOR; +use crate::{did_resolver_adapter::DIDResolverAdapter, DIDComm, ErrorCode}; + +pub trait OnWrapInForwardResult: Sync + Send { + fn success(&self, result: String); + fn error(&self, err: ErrorKind, err_msg: String); +} + +impl DIDComm { + pub fn wrap_in_forward( + &self, + msg: String, + headers: &HashMap, + to: String, + routing_keys: &Vec, + enc_alg_anon: &AnonCryptAlg, + cb: Box, + ) -> ErrorCode { + let did_resolver = DIDResolverAdapter::new(self.did_resolver.clone()); + let headers = headers.clone(); + let routing_keys = routing_keys.clone(); + let enc_alg_anon = enc_alg_anon.clone(); + + let future = async move { + wrap_in_forward( + &msg, + Some(&headers), + &to, + &routing_keys, + &enc_alg_anon, + &did_resolver, + ) + .await + }; + + EXECUTOR.spawn_ok(async move { + match future.await { + Ok(result) => cb.success(result), + Err(err) => cb.error(err.kind(), err.to_string()), + } + }); + + ErrorCode::Success + } +} + +#[cfg(test)] +mod tests { + + use std::collections::HashMap; + use std::iter::FromIterator; + + use crate::test_helper::{ + create_did_resolver, create_secrets_resolver, get_ok, PackResult, UnpackResult, + WrapInForwardResult, + }; + use crate::DIDComm; + use didcomm_core::algorithms::AnonCryptAlg; + use didcomm_core::protocols::routing::try_parse_forward; + use didcomm_core::test_vectors::{ + ALICE_DID, BOB_DID, CHARLIE_DID, MEDIATOR2_VERIFICATION_METHOD_KEY_AGREEM_X25519_1, + MESSAGE_SIMPLE, + }; + use didcomm_core::{Message, PackEncryptedOptions, UnpackOptions}; + use serde_json::json; + + #[tokio::test] + async fn pack_encrypted_works_single_mediator() { + let didcomm = DIDComm::new(create_did_resolver(), create_secrets_resolver()); + let msg = MESSAGE_SIMPLE.clone(); + + // ALICE + let (cb, receiver) = PackResult::new(); + didcomm.pack_encrypted( + &msg, + String::from(BOB_DID), + Some(String::from(ALICE_DID)), + Some(String::from(ALICE_DID)), + &PackEncryptedOptions::default(), + cb, + ); + let packed = get_ok(receiver).await; + + // MEDIATOR 1 + let (cb, receiver) = UnpackResult::new(); + didcomm.unpack( + packed, + &UnpackOptions { + expect_decrypt_by_all_keys: true, + unwrap_re_wrapping_forward: false, + }, + cb, + ); + let unpacked_mediator1 = get_ok(receiver).await; + let forward = try_parse_forward(&unpacked_mediator1).expect("Message is not Forward"); + let forwarded_msg = serde_json::to_string(&forward.forwarded_msg) + .expect("Unable serialize forwarded message"); + + // BOB + let (cb, receiver) = UnpackResult::new(); + didcomm.unpack(forwarded_msg, &UnpackOptions::default(), cb); + let unpacked_msg = get_ok(receiver).await; + + assert_eq!(unpacked_msg, msg); + } + + #[tokio::test] + async fn pack_encrypted_works_multiple_mediators_alternative_endpoints() { + let didcomm = DIDComm::new(create_did_resolver(), create_secrets_resolver()); + let msg = Message::build( + "1234567890".to_owned(), + "http://example.com/protocols/lets_do_lunch/1.0/proposal".to_owned(), + json!({"messagespecificattribute": "and its value"}), + ) + .from(ALICE_DID.to_owned()) + .to(CHARLIE_DID.to_owned()) + .finalize(); + + // ALICE + let (cb, receiver) = PackResult::new(); + didcomm.pack_encrypted( + &msg, + String::from(CHARLIE_DID), + Some(String::from(ALICE_DID)), + Some(String::from(ALICE_DID)), + &PackEncryptedOptions::default(), + cb, + ); + let packed = get_ok(receiver).await; + + // MEDIATOR 3 + let (cb, receiver) = UnpackResult::new(); + didcomm.unpack( + packed, + &UnpackOptions { + expect_decrypt_by_all_keys: true, + unwrap_re_wrapping_forward: false, + }, + cb, + ); + let unpacked_msg_mediator3 = get_ok(receiver).await; + let forward_at_mediator3 = + try_parse_forward(&unpacked_msg_mediator3).expect("Message is not Forward"); + let forward_msg_at_mediator3 = serde_json::to_string(&forward_at_mediator3.forwarded_msg) + .expect("Unable serialize forwarded message"); + + // MEDIATOR 2 + let (cb, receiver) = UnpackResult::new(); + didcomm.unpack( + forward_msg_at_mediator3, + &UnpackOptions { + expect_decrypt_by_all_keys: true, + unwrap_re_wrapping_forward: false, + }, + cb, + ); + let unpacked_msg_mediator2 = get_ok(receiver).await; + let forward_at_mediator2 = + try_parse_forward(&unpacked_msg_mediator2).expect("Message is not Forward"); + let forward_msg_at_mediator2 = serde_json::to_string(&forward_at_mediator2.forwarded_msg) + .expect("Unable serialize forwarded message"); + + // MEDIATOR 1 + let (cb, receiver) = UnpackResult::new(); + didcomm.unpack( + forward_msg_at_mediator2, + &UnpackOptions { + expect_decrypt_by_all_keys: true, + unwrap_re_wrapping_forward: false, + }, + cb, + ); + let unpacked_msg_mediator1 = get_ok(receiver).await; + let forward_at_mediator1 = + try_parse_forward(&unpacked_msg_mediator1).expect("Message is not Forward"); + let forward_msg_at_mediator1 = serde_json::to_string(&forward_at_mediator1.forwarded_msg) + .expect("Unable serialize forwarded message"); + + // CHARLIE + let (cb, receiver) = UnpackResult::new(); + didcomm.unpack(forward_msg_at_mediator1, &UnpackOptions::default(), cb); + let unpacked_msg = get_ok(receiver).await; + + assert_eq!(unpacked_msg, msg); + } + + #[tokio::test] + async fn wrap_in_forward_works_mediator_unknown_by_sender() { + let didcomm = DIDComm::new(create_did_resolver(), create_secrets_resolver()); + let msg = MESSAGE_SIMPLE.clone(); + + // ALICE + let (cb, receiver) = PackResult::new(); + didcomm.pack_encrypted( + &msg, + String::from(BOB_DID), + Some(String::from(ALICE_DID)), + Some(String::from(ALICE_DID)), + &PackEncryptedOptions::default(), + cb, + ); + let packed = get_ok(receiver).await; + + // MEDIATOR 1 + let (cb, receiver) = UnpackResult::new(); + didcomm.unpack( + packed, + &UnpackOptions { + expect_decrypt_by_all_keys: true, + unwrap_re_wrapping_forward: false, + }, + cb, + ); + let unpacked_mediator1 = get_ok(receiver).await; + let forward_at_mediator1 = + try_parse_forward(&unpacked_mediator1).expect("Message is not Forward"); + let forwarded_msg_at_mediator1 = serde_json::to_string(&forward_at_mediator1.forwarded_msg) + .expect("Unable serialize forwarded message"); + + let (cb, receiver) = WrapInForwardResult::new(); + didcomm.wrap_in_forward( + forwarded_msg_at_mediator1, + &HashMap::from_iter([ + ("example-header-1".into(), json!("example-header-1-value")), + ("example-header-2".into(), json!("example-header-2-value")), + ]), + forward_at_mediator1.next, + &vec![MEDIATOR2_VERIFICATION_METHOD_KEY_AGREEM_X25519_1.id.clone()], + &AnonCryptAlg::default(), + cb, + ); + let msg_for_mediator2 = get_ok(receiver).await; + + // MEDIATOR 2 + let (cb, receiver) = UnpackResult::new(); + didcomm.unpack( + msg_for_mediator2, + &UnpackOptions { + expect_decrypt_by_all_keys: true, + unwrap_re_wrapping_forward: false, + }, + cb, + ); + let unpacked_msg_mediator2 = get_ok(receiver).await; + let forward_at_mediator2 = + try_parse_forward(&unpacked_msg_mediator2).expect("Message is not Forward"); + let forwarded_msg_at_mediator2 = serde_json::to_string(&forward_at_mediator2.forwarded_msg) + .expect("Unable serialize forwarded message"); + + // BOB + let (cb, receiver) = UnpackResult::new(); + didcomm.unpack(forwarded_msg_at_mediator2, &UnpackOptions::default(), cb); + let unpacked_msg = get_ok(receiver).await; + + assert_eq!(unpacked_msg, msg); + } +} diff --git a/affinidi-messaging-didcomm/uniffi/src/didcomm/unpack.rs b/affinidi-messaging-didcomm/uniffi/src/didcomm/unpack.rs new file mode 100644 index 0000000..67c069e --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/didcomm/unpack.rs @@ -0,0 +1,119 @@ +use didcomm_core::{error::ErrorKind, Message, UnpackMetadata, UnpackOptions}; + +use crate::common::EXECUTOR; +use crate::did_resolver_adapter::DIDResolverAdapter; +use crate::DIDComm; +use crate::{secrets_resolver_adapter::SecretsResolverAdapter, ErrorCode}; + +pub trait OnUnpackResult: Sync + Send { + fn success(&self, result: Message, metadata: UnpackMetadata); + fn error(&self, err: ErrorKind, err_msg: String); +} + +impl DIDComm { + pub fn unpack<'a>( + &self, + msg: String, + options: &'a UnpackOptions, + cb: Box, + ) -> ErrorCode { + let msg = msg.clone(); + let options = options.clone(); + let did_resolver = DIDResolverAdapter::new(self.did_resolver.clone()); + let secret_resolver = SecretsResolverAdapter::new(self.secret_resolver.clone()); + + let future = + async move { Message::unpack(&msg, &did_resolver, &secret_resolver, &options).await }; + EXECUTOR.spawn_ok(async move { + match future.await { + Ok((result, metadata)) => cb.success(result, metadata), + Err(err) => cb.error(err.kind(), err.to_string()), + } + }); + + ErrorCode::Success + } +} + +#[cfg(test)] +mod tests { + use crate::test_helper::{ + create_did_resolver, create_secrets_resolver, get_error, get_ok, PackResult, UnpackResult, + }; + use crate::DIDComm; + use didcomm_core::error::ErrorKind; + use didcomm_core::{PackEncryptedOptions, UnpackOptions}; + + use didcomm_core::test_vectors::{ALICE_DID, BOB_DID, MESSAGE_SIMPLE}; + + #[tokio::test] + async fn unpack_works_plaintext() { + let msg = MESSAGE_SIMPLE.clone(); + let didcomm = DIDComm::new(create_did_resolver(), create_secrets_resolver()); + + let (cb, receiver) = PackResult::new(); + didcomm.pack_plaintext(&MESSAGE_SIMPLE, cb); + let res = get_ok(receiver).await; + + let (cb, receiver) = UnpackResult::new(); + didcomm.unpack(res, &UnpackOptions::default(), cb); + let res = get_ok(receiver).await; + + assert_eq!(res, msg); + } + + #[tokio::test] + async fn unpack_works_signed() { + let msg = MESSAGE_SIMPLE.clone(); + let didcomm = DIDComm::new(create_did_resolver(), create_secrets_resolver()); + + let (cb, receiver) = PackResult::new(); + didcomm.pack_signed(&msg, String::from(ALICE_DID), cb); + let res = get_ok(receiver).await; + + let (cb, receiver) = UnpackResult::new(); + didcomm.unpack(res, &UnpackOptions::default(), cb); + let res = get_ok(receiver).await; + + assert_eq!(res, msg); + } + + #[tokio::test] + async fn unpack_works_encrypted() { + let msg = MESSAGE_SIMPLE.clone(); + let didcomm = DIDComm::new(create_did_resolver(), create_secrets_resolver()); + + let (cb, receiver) = PackResult::new(); + didcomm.pack_encrypted( + &msg, + String::from(BOB_DID), + Some(String::from(ALICE_DID)), + Some(String::from(ALICE_DID)), + &PackEncryptedOptions { + forward: false, + ..PackEncryptedOptions::default() + }, + cb, + ); + let res = get_ok(receiver).await; + + let (cb, receiver) = UnpackResult::new(); + didcomm.unpack(res, &UnpackOptions::default(), cb); + let res = get_ok(receiver).await; + + assert_eq!(res, msg); + } + + #[tokio::test] + async fn unpack_works_malformed() { + let (cb, receiver) = UnpackResult::new(); + DIDComm::new(create_did_resolver(), create_secrets_resolver()).unpack( + String::from("invalid message"), + &UnpackOptions::default(), + cb, + ); + let res = get_error(receiver).await; + + assert_eq!(res.kind(), ErrorKind::Malformed); + } +} diff --git a/affinidi-messaging-didcomm/uniffi/src/lib.rs b/affinidi-messaging-didcomm/uniffi/src/lib.rs new file mode 100644 index 0000000..4204016 --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/lib.rs @@ -0,0 +1,25 @@ +mod common; +mod did; +mod didcomm; +mod secrets; + +pub use common::ErrorCode; +pub use common::JsonValue; +pub use did::resolvers::*; +pub use did::*; +pub use didcomm::*; +pub use didcomm_core::algorithms::*; +pub use didcomm_core::did::{ + DIDCommMessagingService, DIDDoc, Service, ServiceKind, VerificationMaterial, + VerificationMethod, VerificationMethodType, +}; +pub use didcomm_core::error::*; +pub use didcomm_core::secrets::{Secret, SecretMaterial, SecretType}; +pub use didcomm_core::*; +pub use secrets::resolvers::*; +pub use secrets::*; + +#[cfg(test)] +mod test_helper; + +uniffi::include_scaffolding!("didcomm"); diff --git a/affinidi-messaging-didcomm/uniffi/src/secrets/mod.rs b/affinidi-messaging-didcomm/uniffi/src/secrets/mod.rs new file mode 100644 index 0000000..d97a502 --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/secrets/mod.rs @@ -0,0 +1,6 @@ +pub mod resolvers; + +pub(crate) mod secrets_resolver; +pub(crate) mod secrets_resolver_adapter; + +pub use secrets_resolver::{OnFindSecretsResult, OnGetSecretResult, SecretsResolver}; diff --git a/affinidi-messaging-didcomm/uniffi/src/secrets/resolvers/example.rs b/affinidi-messaging-didcomm/uniffi/src/secrets/resolvers/example.rs new file mode 100644 index 0000000..bd6e862 --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/secrets/resolvers/example.rs @@ -0,0 +1,46 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use didcomm_core::secrets::Secret; + +use crate::{common::ErrorCode, secrets::SecretsResolver, OnFindSecretsResult, OnGetSecretResult}; + +/// Allows resolve pre-defined did's for `example` and other methods. +pub struct ExampleSecretsResolver { + known_secrets: Vec, +} + +impl ExampleSecretsResolver { + pub fn new(known_secrets: Vec) -> Self { + ExampleSecretsResolver { known_secrets } + } +} + +#[async_trait] +impl SecretsResolver for ExampleSecretsResolver { + fn get_secret(&self, secret_id: String, cb: Arc) -> ErrorCode { + let secret = self + .known_secrets + .iter() + .find(|s| s.id == secret_id) + .map(|s| s.clone()); + + match cb.success(secret) { + Ok(_) => ErrorCode::Success, + Err(_) => ErrorCode::Error, + } + } + + fn find_secrets(&self, secret_ids: Vec, cb: Arc) -> ErrorCode { + let res = secret_ids + .iter() + .filter(|sid| self.known_secrets.iter().find(|s| s.id == **sid).is_some()) + .map(|sid| sid.clone()) + .collect(); + + match cb.success(res) { + Ok(_) => ErrorCode::Success, + Err(_) => ErrorCode::Error, + } + } +} diff --git a/affinidi-messaging-didcomm/uniffi/src/secrets/resolvers/mod.rs b/affinidi-messaging-didcomm/uniffi/src/secrets/resolvers/mod.rs new file mode 100644 index 0000000..a3f0d0f --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/secrets/resolvers/mod.rs @@ -0,0 +1,3 @@ +mod example; + +pub use example::ExampleSecretsResolver; diff --git a/affinidi-messaging-didcomm/uniffi/src/secrets/secrets_resolver.rs b/affinidi-messaging-didcomm/uniffi/src/secrets/secrets_resolver.rs new file mode 100644 index 0000000..0da676a --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/secrets/secrets_resolver.rs @@ -0,0 +1,35 @@ +use std::sync::Arc; + +use didcomm_core::secrets::Secret; + +use crate::common::{ErrorCode, OnResult}; + +/// Interface for secrets resolver. +/// Resolves secrets such as private keys to be used for signing and encryption. +pub trait SecretsResolver: Sync + Send { + /// Finds secret (usually private key) identified by the given key ID. + /// + /// # Parameters + /// - `secret_id` the ID (in form of DID URL) identifying a secret + /// - `cb` a callback with a result + /// + /// # Returns + /// A secret (usually private key) or None of there is no secret for the given ID + /// + fn get_secret(&self, secret_id: String, cb: Arc) -> ErrorCode; + + /// Find all secrets that have one of the given IDs. + /// Return secrets only for key IDs for which a secret is present. + /// + /// # Parameters + /// - `secret_ids` the IDs find secrets for + /// - `cb` a callback with a result + /// + /// # Returns + /// A secret (usually private key) or None of there is no secret for the given ID + /// + fn find_secrets(&self, secret_ids: Vec, cb: Arc) -> ErrorCode; +} + +pub type OnGetSecretResult = OnResult>; +pub type OnFindSecretsResult = OnResult>; diff --git a/affinidi-messaging-didcomm/uniffi/src/secrets/secrets_resolver_adapter.rs b/affinidi-messaging-didcomm/uniffi/src/secrets/secrets_resolver_adapter.rs new file mode 100644 index 0000000..03a38c5 --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/secrets/secrets_resolver_adapter.rs @@ -0,0 +1,53 @@ +use std::sync::Arc; + +use async_trait::async_trait; +use didcomm_core::error::{ErrorKind, Result, ResultExt}; +use didcomm_core::secrets::{Secret, SecretsResolver as _SecretsResolver}; + +use crate::secrets_resolver::{OnFindSecretsResult, OnGetSecretResult}; + +use super::SecretsResolver; + +pub struct SecretsResolverAdapter { + secrets_resolver: Arc>, +} + +impl SecretsResolverAdapter { + pub fn new(secrets_resolver: Arc>) -> Self { + SecretsResolverAdapter { secrets_resolver } + } +} + +#[async_trait] +impl _SecretsResolver for SecretsResolverAdapter { + async fn get_secret(&self, secret_id: &str) -> Result> { + let (cb, receiver) = OnGetSecretResult::new(); + + self.secrets_resolver + .get_secret(String::from(secret_id), cb); + + let res = receiver + .get() + .await + .kind(ErrorKind::InvalidState, "can not get secret")?; + Ok(res) + } + + async fn find_secrets<'a>(&self, secret_ids: &'a [&'a str]) -> Result> { + let (cb, receiver) = OnFindSecretsResult::new(); + + self.secrets_resolver + .find_secrets(secret_ids.iter().map(|&s| String::from(s)).collect(), cb); + + let res = receiver + .get() + .await + .kind(ErrorKind::InvalidState, "can not get secret")?; + + Ok(secret_ids + .iter() + .filter(|&&sid| res.iter().find(|&s| s == sid).is_some()) + .map(|sid| *sid) + .collect()) + } +} diff --git a/affinidi-messaging-didcomm/uniffi/src/test_helper.rs b/affinidi-messaging-didcomm/uniffi/src/test_helper.rs new file mode 100644 index 0000000..c8019c9 --- /dev/null +++ b/affinidi-messaging-didcomm/uniffi/src/test_helper.rs @@ -0,0 +1,180 @@ +use std::cell::RefCell; +use std::fmt; +use std::sync::Mutex; + +use didcomm_core::error::{err_msg, Error, ErrorKind, Result}; +use didcomm_core::{FromPrior, Message, PackEncryptedMetadata, PackSignedMetadata, UnpackMetadata}; +use futures::channel::oneshot::{self, Receiver}; + +use crate::{ + DIDResolver, ExampleDIDResolver, ExampleSecretsResolver, OnFromPriorPackResult, + OnFromPriorUnpackResult, OnPackEncryptedResult, OnPackPlaintextResult, OnPackSignedResult, + OnUnpackResult, OnWrapInForwardResult, SecretsResolver, +}; +use didcomm_core::test_vectors::{ + ALICE_DID_DOC_WITH_NO_SECRETS, ALICE_SECRETS, BOB_DID_DOC, BOB_SECRETS, CHARLIE_DID_DOC, + CHARLIE_SECRETS, MEDIATOR1_DID_DOC, MEDIATOR1_SECRETS, MEDIATOR2_DID_DOC, MEDIATOR2_SECRETS, + MEDIATOR3_DID_DOC, MEDIATOR3_SECRETS, +}; + +pub(crate) async fn get_ok(receiver: Receiver>) -> T { + receiver + .await + .expect("unable receive result") + .expect("result is error") +} + +pub(crate) async fn get_error(receiver: Receiver>) -> Error { + receiver + .await + .expect("unable receive result") + .err() + .expect("result is ok") +} + +pub(crate) fn create_secrets_resolver() -> Box { + Box::new(ExampleSecretsResolver::new( + ALICE_SECRETS + .clone() + .into_iter() + .chain(BOB_SECRETS.clone().into_iter()) + .chain(CHARLIE_SECRETS.clone().into_iter()) + .chain(MEDIATOR1_SECRETS.clone().into_iter()) + .chain(MEDIATOR2_SECRETS.clone().into_iter()) + .chain(MEDIATOR3_SECRETS.clone().into_iter()) + .collect(), + )) +} + +pub(crate) fn create_did_resolver() -> Box { + Box::new(ExampleDIDResolver::new(vec![ + ALICE_DID_DOC_WITH_NO_SECRETS.clone(), + BOB_DID_DOC.clone(), + CHARLIE_DID_DOC.clone(), + MEDIATOR1_DID_DOC.clone(), + MEDIATOR2_DID_DOC.clone(), + MEDIATOR3_DID_DOC.clone(), + ])) +} + +pub(crate) struct TestResult +where + T: fmt::Debug + 'static, +{ + sender: Mutex>>>>, +} + +impl TestResult +where + T: fmt::Debug + 'static, +{ + pub(crate) fn new() -> (Box, Receiver>) { + let (sender, receiver) = oneshot::channel::>(); + ( + Box::new(TestResult { + sender: Mutex::new(RefCell::new(Some(sender))), + }), + receiver, + ) + } + + fn _success(&self, result: T) { + self.sender + .lock() + .expect("Unable lock") + .replace(None) + .expect("Callback has been already called") + .send(Ok(result)) + .expect("Unable send"); + } + + fn _error(&self, err: ErrorKind, msg: String) { + self.sender + .lock() + .expect("Unable lock") + .replace(None) + .expect("Callback has been already called") + .send(Err(err_msg(err, msg))) + .expect("Unable send"); + } +} + +pub(crate) type PackResult = TestResult; + +impl OnPackPlaintextResult for PackResult { + fn success(&self, result: String) { + self._success(result); + } + + fn error(&self, err: ErrorKind, msg: String) { + self._error(err, msg); + } +} + +impl OnPackSignedResult for PackResult { + fn success(&self, result: String, _metadata: PackSignedMetadata) { + self._success(result); + } + + fn error(&self, err: ErrorKind, msg: String) { + self._error(err, msg); + } +} + +impl OnPackEncryptedResult for PackResult { + fn success(&self, result: String, _metadata: PackEncryptedMetadata) { + self._success(result); + } + + fn error(&self, err: ErrorKind, msg: String) { + self._error(err, msg); + } +} + +pub(crate) type UnpackResult = TestResult; + +impl OnUnpackResult for UnpackResult { + fn success(&self, result: Message, _metadata: UnpackMetadata) { + self._success(result); + } + + fn error(&self, err: ErrorKind, msg: String) { + self._error(err, msg); + } +} + +pub(crate) type FromPriorPackResult = TestResult<(String, String)>; + +impl OnFromPriorPackResult for FromPriorPackResult { + fn success(&self, from_prior_jwt: String, kid: String) { + self._success((from_prior_jwt, kid)); + } + + fn error(&self, err: ErrorKind, msg: String) { + self._error(err, msg); + } +} + +pub(crate) type FromPriorUnpackResult = TestResult<(FromPrior, String)>; + +impl OnFromPriorUnpackResult for FromPriorUnpackResult { + fn success(&self, from_prior: FromPrior, kid: String) { + self._success((from_prior, kid)); + } + + fn error(&self, err: ErrorKind, msg: String) { + self._error(err, msg); + } +} + +pub(crate) type WrapInForwardResult = TestResult; + +impl OnWrapInForwardResult for WrapInForwardResult { + fn success(&self, result: String) { + self._success(result); + } + + fn error(&self, err: ErrorKind, msg: String) { + self._error(err, msg); + } +} diff --git a/affinidi-messaging-didcomm/wasm/.gitignore b/affinidi-messaging-didcomm/wasm/.gitignore new file mode 100644 index 0000000..4e30131 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/.gitignore @@ -0,0 +1,6 @@ +/target +**/*.rs.bk +Cargo.lock +bin/ +pkg/ +wasm-pack.log diff --git a/affinidi-messaging-didcomm/wasm/Cargo.toml b/affinidi-messaging-didcomm/wasm/Cargo.toml new file mode 100644 index 0000000..9b6c7a6 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/Cargo.toml @@ -0,0 +1,56 @@ +[package] +name = 'didcomm-js' +version = '0.4.1' +authors = ['Vyacheslav Gudkov '] +edition = '2018' +description = 'WASM based javascript wrapper for DIDComm' +license = 'Apache-2.0' +repository = 'https://github.com/sicpa-dlab/didcomm-rust/tree/main/wasm' +homepage = 'https://github.com/sicpa-dlab/didcomm-rust/tree/main/wasm#readme' +readme = 'README.md' + +[lib] +crate-type = ['cdylib', 'rlib'] + +[features] +default = ['console_error_panic_hook'] + +[dependencies] +async-trait = '0.1' +wasm-bindgen-futures = '0.4' +js-sys = '0.3' +serde_json = '1.0' + +[dependencies.didcomm] +path = '..' + +[dev-dependencies.getrandom] +version = '0.2' +features = ['js'] + +[dependencies.wasm-bindgen] +version = '0.2' +features = ['serde-serialize'] + +[dependencies.console_error_panic_hook] +version = '0.1' +optional = true + +[dependencies.serde] +version = '1.0' +features = ['derive'] + +[dependencies.wee_alloc] +version = '0.4' +optional = true + +[dependencies.uuid] +version = "1.8" +features = ["v4", "wasm-bindgen"] + +[dev-dependencies] +wasm-bindgen-test = '0.3' + +[profile.release] +opt-level = 's' +lto = true diff --git a/affinidi-messaging-didcomm/wasm/LICENSE b/affinidi-messaging-didcomm/wasm/LICENSE new file mode 100644 index 0000000..1b5ec8b --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/LICENSE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS diff --git a/affinidi-messaging-didcomm/wasm/Makefile b/affinidi-messaging-didcomm/wasm/Makefile new file mode 100644 index 0000000..7ddd5af --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/Makefile @@ -0,0 +1,41 @@ +PKG_NAME ?= didcomm +WASM_TARGET ?= bundler +WASM_OPTS_PUBLISH ?= + +ifndef PROJECT_DIR +PROJECT_DIR := $(shell git rev-parse --show-toplevel) +endif + +WASM_DIR_NAME := wasm +WASM_DIR := $(PROJECT_DIR)/$(WASM_DIR_NAME) + +WASM_PKG_DIR_NAME := pkg +WASM_PKG_DIR := $(WASM_DIR)/$(WASM_PKG_DIR_NAME) + +.PHONY: all \ + pkg_clean build install publish clean + +all: build + +pkg_clean: + rm -rf $(WASM_PKG_DIR) + +build: $(WASM_DIR) pkg_clean + cd $< + wasm-pack build --target $(WASM_TARGET) --out-name index + sed -i -r "s~\"name\": \".+\"~\"name\": \"${PKG_NAME}\"~" $(WASM_PKG_DIR_NAME)/package.json + +install: $(WASM_PKG_DIR) + cd $< && npm install . + +pack: $(WASM_PKG_DIR) + cd $(WASM_DIR) + wasm-pack pack + +publish: $(WASM_PKG_DIR) + cd $(WASM_DIR) + echo '//registry.npmjs.org/:_authToken=${NPM_TOKEN}' > $) crate. It compiles +to `wasm32` and exposes Javascript/Typescript API with [wasm-bindgen](https://github.com/rustwasm/wasm-bindgen) help. +Also [wasmp-pack](https://github.com/rustwasm/wasm-pack) helps in packaging and publishing. + +## Usage + +To use `didcomm` install it with npm + +```sh +npm install didcomm --save # If you plan use webpack or other bundler + +npm install didcomm-node --save # If you plan use it without bundlers in NodeJS + +``` + +## Run demo + +```sh +WASM_TARGET=nodejs make # builds NodeJS package in pkg directory +cd ./demo +npm install +npm run start +``` + +## Assumptions and Limitations + +- This library requires `wasm32` compatible environment (modern browsers and recent NodeJS are supported). +- In order to use the library, `SecretsResolver` and `DIDResolver` interfaces must be implemented on the application level. + Demo application provides 2 simple implementations `ExampleDIDResolver` + and `ExampleSecretsResolver` that allows resolve locally known DID docs and secrets for tests/demo purposes. + - Verification materials are expected in JWK, Base58 and Multibase (internally Base58 only) formats. + - In Base58 and Multibase formats, keys using only X25519 and Ed25519 curves are supported. + - For private keys in Base58 and Multibase formats, the verification material value contains both private and public parts (concatenated bytes). + - In Multibase format, bytes of the verification material value is prefixed with the corresponding Multicodec code. + - Key IDs (kids) used in `SecretsResolver` must match the corresponding key IDs from DID Doc verification methods. + - Key IDs (kids) in DID Doc verification methods and secrets must be a full [DID Fragment](https://www.w3.org/TR/did-core/#fragment), that is `did#key-id`. + - Verification methods referencing another DID Document are not supported (see [Referring to Verification Methods](https://www.w3.org/TR/did-core/#referring-to-verification-methods)). +- The following curves and algorithms are supported: + - Encryption: + - Curves: X25519, P-256 + - Content encryption algorithms: + - XC20P (to be used with ECDH-ES only, default for anoncrypt), + - A256GCM (to be used with ECDH-ES only), + - A256CBC-HS512 (default for authcrypt) + - Key wrapping algorithms: ECDH-ES+A256KW, ECDH-1PU+A256KW + - Signing: + - Curves: Ed25519, Secp256k1, P-256 + - Algorithms: EdDSA (with crv=Ed25519), ES256, ES256K +- Forward protocol is implemented and used by default. +- DID rotation (`fromPrior` field) is supported. +- DIDComm has been implemented under the following [Assumptions](https://hackmd.io/i3gLqgHQR2ihVFV5euyhqg) + +## Examples + +A general usage of the API is the following: + +- Sender Side: + - Build a `Message` (plaintext, payload). + - Convert a message to a DIDComm Message for further transporting by calling one of the following: + - `Message.pack_encrypted` to build an Encrypted DIDComm message + - `Message.pack_signed` to build a Signed DIDComm message + - `Message.pack_plaintext` to build a Plaintext DIDComm message +- Receiver side: + - Call `Message.unpack` on receiver side that will decrypt the message, verify signature if needed + and return a `Message` for further processing on the application level. + +### 1. Build an Encrypted DIDComm message for the given recipient + +This is the most common DIDComm message to be used in most of the applications. + +A DIDComm encrypted message is an encrypted JWM (JSON Web Messages) that + +- hides its content from all but authorized recipients +- (optionally) discloses and proves the sender to only those recipients +- provides message integrity guarantees + +It is important in privacy-preserving routing. It is what normally moves over network transports in DIDComm +applications, and is the safest format for storing DIDComm data at rest. + +See `Message::pack_encrypted` documentation for more details. + +**Authentication encryption** example (most common case): + +```typescript +// --- Build message from ALICE to BOB --- +const msg = new Message({ + id: "1234567890", + typ: "application/didcomm-plain+json", + type: "http://example.com/protocols/lets_do_lunch/1.0/proposal", + from: "did:example:alice", + to: ["did:example:bob"], + created_time: 1516269022, + expires_time: 1516385931, + body: { messagespecificattribute: "and its value" }, +}); + +// --- Packing encrypted and authenticated message --- + +let didResolver = new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]); +let secretsResolver = new ExampleSecretsResolver(ALICE_SECRETS); + +const [encryptedMsg, encryptMetadata] = await msg.pack_encrypted( + BOB_DID, + ALICE_DID, + null, + didResolver, + secretsResolver, + { + forward: false, // Forward wrapping is unsupported in current version + } +); + +console.log("Encryption metadata is\n", encryptMetadata); + +// --- Send message --- +console.log("Sending message\n", encryptedMsg); + +// --- Unpacking message --- +didResolver = new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]); +secretsResolver = new ExampleSecretsResolver(BOB_SECRETS); + +const [unpackedMsg, unpackMetadata] = await Message.unpack( + encrypted_msg, + didResolver, + secretsResolver, + {} +); + +console.log("Receved message is\n", unpackedMsg.as_value()); +console.log("Receved message unpack metadata is\n", unpackMetadata); +``` + +**Anonymous encryption** example: + +```typescript +let [encryptedMsg, encryptMetadata] = await msg.pack_encrypted( + BOB_DID, + null, // Keep sender as None here + null, + didResolver, + secretsResolver, + { + forward: false, // Forward wrapping is unsupported in current version + } +); +``` + +**Encryption with non-repudiation** example: + +```typescript +let [encrypted_msg, encrypt_metadata] = await msg.pack_encrypted( + BOB_DID, + ALICE_DID, + ALICE_DID, // Provide information about signer here + did_resolver, + secrets_resolver, + { + forward: false, // Forward wrapping is unsupported in current version + } +); +``` + +### 2. Build an unencrypted but Signed DIDComm message + +Signed messages are only necessary when + +- the origin of plaintext must be provable to third parties +- or the sender can’t be proven to the recipient by authenticated encryption because the recipient is not known in advance (e.g., in a + broadcast scenario). + +Adding a signature when one is not needed can degrade rather than enhance security because it +relinquishes the sender’s ability to speak off the record. + +See `Message.pack_signed` documentation for more details. + +```typescript +let [signed, metadata] = await msg.pack_signed( + ALICE_DID, + didResolver, + secretsResolver +); +``` + +### 3. Build a Plaintext DIDComm message + +A DIDComm message in its plaintext form that + +- is not packaged into any protective envelope +- lacks confidentiality and integrity guarantees +- repudiable + +They are therefore not normally transported across security boundaries. + +```typescript +let plaintext = msg.pack_plaintext(didResolver).expect("Unable pack_plaintext"); +``` + +## How to build + +Install `wasm-pack` from https://rustwasm.github.io/wasm-pack/installer/ and then + +```bash +make # Will output modules best-suited to be bundled with webpack +WASM_TARGET=nodejs make # Will output modules that can be directly consumed by NodeJS +WASM_TARGET=web make # Will output modules that can be directly consumed in browser without bundler usage +``` + +### How to build with `wasm-pack build` + +```bash +wasm-pack build # Will output modules best-suited to be bundled with webpack +wasm-pack build --target=nodejs # Will output modules that can be directly consumed by NodeJS +wasm-pack build --target=web # Will output modules that can be directly consumed in browser without bundler usage +``` + +## How to test in NodeJS + +```bash +WASM_TARGET=nodejs make +cd ./tests-js +npm install +npm test +``` + +## How to test in Browser + +```bash +WASM_TARGET=nodejs make +cd ./tests-js +npm install +npm run test-puppeteer +``` + +_Note tests will be executed with jest+puppeteer in Chromium installed inside node_modules._ + +## Hot to publish to NPM with `wasm-pack publish` + +``` +wasm-pack publish +``` + +## 🔋 Batteries Included + +- [`wasm-bindgen`](https://github.com/rustwasm/wasm-bindgen) for communicating + between WebAssembly and JavaScript. +- [`console_error_panic_hook`](https://github.com/rustwasm/console_error_panic_hook) + for logging panic messages to the developer console. +- [`wee_alloc`](https://github.com/rustwasm/wee_alloc), an allocator optimized + for small code size. + +## Contribution + +PRs are welcome! + +The following CI checks are run against every PR: + +- No warnings from `cargo check --all-targets` +- No warnings from `npm run check` in `tests-js` directory +- No warnings from `npm run check` in `demo` directory +- All tests must pass with `npm test` in `tests-js` directory +- Rust code must be formatted by `cargo fmt --all` +- Javascript/Typescript code must be formatted by prettier `npx prettier --write .` diff --git a/affinidi-messaging-didcomm/wasm/demo/.gitignore b/affinidi-messaging-didcomm/wasm/demo/.gitignore new file mode 100644 index 0000000..07e6e47 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/demo/.gitignore @@ -0,0 +1 @@ +/node_modules diff --git a/affinidi-messaging-didcomm/wasm/demo/package-lock.json b/affinidi-messaging-didcomm/wasm/demo/package-lock.json new file mode 100644 index 0000000..79f4250 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/demo/package-lock.json @@ -0,0 +1,978 @@ +{ + "name": "didcomm-demo", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "didcomm-demo", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "didcomm": "file:../pkg", + "typescript": "^4.5.2" + }, + "devDependencies": { + "@types/node": "^16.11.10", + "ts-node": "^10.4.0", + "tslint": "^6.1.3" + } + }, + "../pkg": { + "name": "didcomm", + "version": "0.0.1" + }, + "node_modules/@babel/code-frame": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.0.tgz", + "integrity": "sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.16.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", + "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.15.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cspotcode/source-map-consumer": { + "version": "0.8.0", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-consumer": "0.8.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.9", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "16.11.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.10.tgz", + "integrity": "sha512-3aRnHa1KlOEEhJ6+CvyHKK5vE9BcLGjtUpwvqYLRvYNQKMfabu3BwfJaA/SLW8dxe28LsNDjtHwePTuzn3gmOA==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.5.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/didcomm": { + "resolved": "../pkg", + "link": true + }, + "node_modules/diff": { + "version": "4.0.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-core-module": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "dev": true, + "license": "ISC" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ts-node": { + "version": "10.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.7.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tslint": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz", + "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", + "deprecated": "TSLint has been deprecated in favor of ESLint. Please see https://github.com/palantir/tslint/issues/4534 for more information.", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.3", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.13.0", + "tsutils": "^2.29.0" + }, + "bin": { + "tslint": "bin/tslint" + }, + "engines": { + "node": ">=4.8.0" + }, + "peerDependencies": { + "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 4.0.0-dev" + } + }, + "node_modules/tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "peerDependencies": { + "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev" + } + }, + "node_modules/typescript": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", + "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "node_modules/yn": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.16.0.tgz", + "integrity": "sha512-IF4EOMEV+bfYwOmNxGzSnjR2EmQod7f1UXOpZM3l4i4o4QNwzjtJAu/HxdjHq0aYBvdqMuQEY1eg0nqW9ZPORA==", + "dev": true, + "requires": { + "@babel/highlight": "^7.16.0" + } + }, + "@babel/helper-validator-identifier": { + "version": "7.15.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.15.7.tgz", + "integrity": "sha512-K4JvCtQqad9OY2+yTU8w+E82ywk/fe+ELNlt1G8z3bVGlZfn/hOcQQsUhGhW/N+tb3fxK800wLtKOE/aM0m72w==", + "dev": true + }, + "@babel/highlight": { + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.16.0.tgz", + "integrity": "sha512-t8MH41kUQylBtu2+4IQA3atqevA2lRgqA2wyVB/YiWmsDSuylZZuXOUy9ric30hfzauEFfdsuk/eXTRrGrfd0g==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.15.7", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + } + }, + "@cspotcode/source-map-consumer": { + "version": "0.8.0", + "dev": true + }, + "@cspotcode/source-map-support": { + "version": "0.7.0", + "dev": true, + "requires": { + "@cspotcode/source-map-consumer": "0.8.0" + } + }, + "@tsconfig/node10": { + "version": "1.0.8", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.9", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.1", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.2", + "dev": true + }, + "@types/node": { + "version": "16.11.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.10.tgz", + "integrity": "sha512-3aRnHa1KlOEEhJ6+CvyHKK5vE9BcLGjtUpwvqYLRvYNQKMfabu3BwfJaA/SLW8dxe28LsNDjtHwePTuzn3gmOA==", + "dev": true + }, + "acorn": { + "version": "8.5.0", + "dev": true + }, + "acorn-walk": { + "version": "8.2.0", + "dev": true + }, + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "arg": { + "version": "4.1.3", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "create-require": { + "version": "1.1.1", + "dev": true + }, + "didcomm": { + "version": "file:../pkg" + }, + "diff": { + "version": "4.0.2", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-core-module": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz", + "integrity": "sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "make-error": { + "version": "1.3.6", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "dev": true + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "resolve": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", + "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "ts-node": { + "version": "10.4.0", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "0.7.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "yn": "3.1.1" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "tslint": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz", + "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.3", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.13.0", + "tsutils": "^2.29.0" + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + } + }, + "typescript": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", + "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "yn": { + "version": "3.1.1", + "dev": true + } + } +} diff --git a/affinidi-messaging-didcomm/wasm/demo/package.json b/affinidi-messaging-didcomm/wasm/demo/package.json new file mode 100644 index 0000000..6353bb4 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/demo/package.json @@ -0,0 +1,21 @@ +{ + "name": "didcomm-demo", + "version": "0.4.0", + "description": "JS demo for didcomm", + "scripts": { + "start": "ts-node src/main.ts", + "test": "echo \"Error: no test specified\" && exit 1", + "check": "tslint -c tslint.json 'src/**/*.ts'" + }, + "author": "", + "license": "Apache-2.0", + "dependencies": { + "didcomm": "file:../pkg", + "typescript": "^4.5.2" + }, + "devDependencies": { + "@types/node": "^16.11.10", + "ts-node": "^10.4.0", + "tslint": "^6.1.3" + } +} diff --git a/affinidi-messaging-didcomm/wasm/demo/src/advanced-params.ts b/affinidi-messaging-didcomm/wasm/demo/src/advanced-params.ts new file mode 100644 index 0000000..0fef556 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/demo/src/advanced-params.ts @@ -0,0 +1,67 @@ +import { Message } from "didcomm"; +import { + ALICE_DID, + ALICE_DID_DOC, + ALICE_SECRETS, + BOB_DID, + BOB_DID_DOC, + BOB_SECRETS, +} from "./test-vectors"; +import { + ExampleDIDResolver, + ExampleSecretsResolver, +} from "../../tests-js/src/test-vectors"; + +async function main() { + // --- Building message from ALICE to BOB --- + const msg = new Message({ + id: "1234567890", + typ: "application/didcomm-plain+json", + type: "http://example.com/protocols/lets_do_lunch/1.0/proposal", + from: "did:example:alice", + to: ["did:example:bob"], + created_time: 1516269022, + expires_time: 1516385931, + body: { messagespecificattribute: "and its value" }, + }); + + // --- Packing encrypted and authenticated message --- + let didResolver = new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]); + let secretsResolver = new ExampleSecretsResolver(ALICE_SECRETS); + + const [encryptedMsg, encryptMetadata] = await msg.pack_encrypted( + "did:example:bob#key-p256-1", + "did:example:alice#key-p256-1", + "did:example:alice#key-2", + didResolver, + secretsResolver, + { + forward: false, // TODO: should be true by default + protect_sender: true, + enc_alg_anon: "A256cbcHs512EcdhEsA256kw", + messaging_service: "did:example:bob#didcomm-1", + enc_alg_auth: "A256cbcHs512Ecdh1puA256kw", + } + ); + + console.log("Encryption metadata is\n", encryptMetadata); + + // --- Sending message --- + console.log("Sending message\n", encryptedMsg); + + // --- Unpacking message --- + didResolver = new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]); + secretsResolver = new ExampleSecretsResolver(BOB_SECRETS); + + const [unpackedMsg, unpackMetadata] = await Message.unpack( + encryptedMsg, + didResolver, + secretsResolver, + {} + ); + + console.log("Reveived message is\n", unpackedMsg.as_value()); + console.log("Reveived message unpack metadata is\n", unpackMetadata); +} + +main().catch((e) => console.log(e)); diff --git a/affinidi-messaging-didcomm/wasm/demo/src/attachments.ts b/affinidi-messaging-didcomm/wasm/demo/src/attachments.ts new file mode 100644 index 0000000..734dc3f --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/demo/src/attachments.ts @@ -0,0 +1,73 @@ +import { Message } from "didcomm"; +import { + ALICE_DID, + ALICE_DID_DOC, + ALICE_SECRETS, + BOB_DID, + BOB_DID_DOC, + BOB_SECRETS, +} from "./test-vectors"; +import { + ExampleDIDResolver, + ExampleSecretsResolver, +} from "../../tests-js/src/test-vectors"; + +async function main() { + // --- Building message from ALICE to BOB --- + const msg = new Message({ + id: "1234567890", + typ: "application/didcomm-plain+json", + type: "http://example.com/protocols/lets_do_lunch/1.0/proposal", + from: "did:example:alice", + to: ["did:example:bob"], + created_time: 1516269022, + expires_time: 1516385931, + attachments: [ + { + data: { + json: { foo: "bar" }, + }, + id: "123", + description: "example", + media_type: "application/didcomm-encrypted+json", + }, + ], + body: { messagespecificattribute: "and its value" }, + }); + + // --- Packing encrypted and authenticated message --- + let didResolver = new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]); + let secretsResolver = new ExampleSecretsResolver(ALICE_SECRETS); + + const [encryptedMsg, encryptMetadata] = await msg.pack_encrypted( + BOB_DID, + ALICE_DID, + ALICE_DID, + didResolver, + secretsResolver, + { + forward: false, // TODO: should be true by default + } + ); + + console.log("Encryption metadata is\n", encryptMetadata); + + // --- Sending message --- + console.log("Sending message\n", encryptedMsg); + + // --- Unpacking message --- + didResolver = new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]); + secretsResolver = new ExampleSecretsResolver(BOB_SECRETS); + + const [unpackedMsg, unpackMetadata] = await Message.unpack( + encryptedMsg, + didResolver, + secretsResolver, + {} + ); + + console.log("Reveived message is\n", unpackedMsg.as_value()); + console.log("Reveived message unpack metadata is\n", unpackMetadata); +} + +main().catch((e) => console.log(e)); diff --git a/affinidi-messaging-didcomm/wasm/demo/src/main.ts b/affinidi-messaging-didcomm/wasm/demo/src/main.ts new file mode 100644 index 0000000..599bcf7 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/demo/src/main.ts @@ -0,0 +1,386 @@ +import { + ALICE_DID, + ALICE_DID_DOC, + ALICE_SECRETS, + BOB_DID, + BOB_DID_DOC, + BOB_SECRETS, +} from "./test-vectors"; + +import { Message, DIDDoc, DIDResolver, Secret, SecretsResolver } from "didcomm"; +import { + CHARLIE_DID, + CHARLIE_DID_DOC, + CHARLIE_SECRETS, +} from "../../tests-js/src/test-vectors"; + +class ExampleDIDResolver implements DIDResolver { + knownDids: DIDDoc[]; + + constructor(knownDids: DIDDoc[]) { + this.knownDids = knownDids; + } + + async resolve(did: string): Promise { + return this.knownDids.find((ddoc) => ddoc.id === did) || null; + } +} + +class ExampleSecretsResolver implements SecretsResolver { + knownSecrets: Secret[]; + + constructor(knownSecrets: Secret[]) { + this.knownSecrets = knownSecrets; + } + + async get_secret(secretId: string): Promise { + return this.knownSecrets.find((secret) => secret.id === secretId) || null; + } + + async find_secrets(secretIds: string[]): Promise { + return secretIds.filter((id) => + this.knownSecrets.find((secret) => secret.id === id) + ); + } +} + +async function main() { + console.log( + "=================== NON REPUDIABLE ENCRYPTION ===================\n" + ); + await nonRepudiableEncryption(); + console.log("\n=================== MULTI RECIPIENT ===================\n"); + await multiRecipient(); + console.log( + "\n=================== REPUDIABLE AUTHENTICATED ENCRYPTION ===================\n" + ); + await repudiableAuthentcatedEncryption(); + console.log( + "\n=================== REPUDIABLE NON AUTHENTICATED ENCRYPTION ===================\n" + ); + await repudiableNonAuthentcatedEncryption(); + console.log("\n=================== SIGNED UNENCRYPTED ===================\n"); + await signedUnencrypteed(); + console.log("\n=================== PLAINTEXT ==================="); + await plaintext(); +} + +async function nonRepudiableEncryption() { + // --- Building message from ALICE to BOB --- + const msg = new Message({ + id: "1234567890", + typ: "application/didcomm-plain+json", + type: "http://example.com/protocols/lets_do_lunch/1.0/proposal", + from: "did:example:alice", + to: ["did:example:bob"], + created_time: 1516269022, + expires_time: 1516385931, + body: { messagespecificattribute: "and its value" }, + }); + + // --- Packing encrypted and authenticated message --- + let didResolver = new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]); + let secretsResolver = new ExampleSecretsResolver(ALICE_SECRETS); + + const [encryptedMsg, encryptMetadata] = await msg.pack_encrypted( + BOB_DID, + ALICE_DID, + ALICE_DID, + didResolver, + secretsResolver, + { + forward: false, // TODO: should be true by default + } + ); + + console.log("Encryption metadata is\n", encryptMetadata); + + // --- Sending message --- + console.log("Sending message\n", encryptedMsg); + + // --- Unpacking message --- + didResolver = new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]); + secretsResolver = new ExampleSecretsResolver(BOB_SECRETS); + + const [unpackedMsg, unpackMetadata] = await Message.unpack( + encryptedMsg, + didResolver, + secretsResolver, + {} + ); + + console.log("Reveived message is\n", unpackedMsg.as_value()); + console.log("Reveived message unpack metadata is\n", unpackMetadata); +} + +async function multiRecipient() { + // --- Building message from ALICE to BOB and Charlie --- + const msg = new Message({ + id: "1234567890", + typ: "application/didcomm-plain+json", + type: "http://example.com/protocols/lets_do_lunch/1.0/proposal", + from: "did:example:alice", + to: ["did:example:bob", "did:example:charlie"], + created_time: 1516269022, + expires_time: 1516385931, + body: { messagespecificattribute: "and its value" }, + }); + + let didResolver = new ExampleDIDResolver([ + ALICE_DID_DOC, + BOB_DID_DOC, + CHARLIE_DID_DOC, + ]); + let secretsResolver = new ExampleSecretsResolver(ALICE_SECRETS); + + // --- Packing encrypted and authenticated message for Bob --- + const [encryptedMsgBob, encryptMetadataBob] = await msg.pack_encrypted( + BOB_DID, + ALICE_DID, + null, + didResolver, + secretsResolver, + { + forward: false, // TODO: should be true by default + } + ); + + console.log("Encryption metadata for Bob is\n", encryptMetadataBob); + + // --- Sending message --- + console.log("Sending message to Bob\n", encryptedMsgBob); + + // --- Packing encrypted and authenticated message for Charlie --- + const [encryptedMsgCharlie, encryptMetadataCharlie] = + await msg.pack_encrypted( + CHARLIE_DID, + ALICE_DID, + null, + didResolver, + secretsResolver, + { + forward: false, // TODO: should be true by default + } + ); + + console.log("Encryption metadata for Charle is\n", encryptMetadataCharlie); + + // --- Sending message --- + console.log("Sending message to Charle\n", encryptedMsgCharlie); + + // --- Unpacking message for Bob --- + didResolver = new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]); + secretsResolver = new ExampleSecretsResolver(BOB_SECRETS); + + const [unpackedMsgBob, unpackMetadataBob] = await Message.unpack( + encryptedMsgBob, + didResolver, + secretsResolver, + {} + ); + + console.log("Reveived message for Bob is\n", unpackedMsgBob.as_value()); + console.log( + "Reveived message unpack metadata for Bob is\n", + unpackMetadataBob + ); + + // --- Unpacking message for Charlie --- + didResolver = new ExampleDIDResolver([ALICE_DID_DOC, CHARLIE_DID_DOC]); + secretsResolver = new ExampleSecretsResolver(CHARLIE_SECRETS); + + const [unpackedMsgCharlie, unpackMetadataCharlie] = await Message.unpack( + encryptedMsgCharlie, + didResolver, + secretsResolver, + {} + ); + + console.log( + "Reveived message for Charlie is\n", + unpackedMsgCharlie.as_value() + ); + console.log( + "Reveived message unpack metadata for Charlie is\n", + unpackMetadataCharlie + ); +} + +async function repudiableAuthentcatedEncryption() { + // --- Building message from ALICE to BOB --- + const msg = new Message({ + id: "1234567890", + typ: "application/didcomm-plain+json", + type: "http://example.com/protocols/lets_do_lunch/1.0/proposal", + from: "did:example:alice", + to: ["did:example:bob"], + created_time: 1516269022, + expires_time: 1516385931, + body: { messagespecificattribute: "and its value" }, + }); + + // --- Packing encrypted and authenticated message --- + let didResolver = new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]); + let secretsResolver = new ExampleSecretsResolver(ALICE_SECRETS); + + const [encryptedMsg, encryptMetadata] = await msg.pack_encrypted( + BOB_DID, + ALICE_DID, + null, + didResolver, + secretsResolver, + { + forward: false, // TODO: should be true by default + } + ); + + console.log("Encryption metadata is\n", encryptMetadata); + + // --- Sending message --- + console.log("Sending message\n", encryptedMsg); + + // --- Unpacking message --- + didResolver = new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]); + secretsResolver = new ExampleSecretsResolver(BOB_SECRETS); + + const [unpackedMsg, unpackMetadata] = await Message.unpack( + encryptedMsg, + didResolver, + secretsResolver, + {} + ); + + console.log("Reveived message is\n", unpackedMsg.as_value()); + console.log("Reveived message unpack metadata is\n", unpackMetadata); +} + +async function repudiableNonAuthentcatedEncryption() { + // --- Building message from ALICE to BOB --- + const msg = new Message({ + id: "1234567890", + typ: "application/didcomm-plain+json", + type: "http://example.com/protocols/lets_do_lunch/1.0/proposal", + from: "did:example:alice", + to: ["did:example:bob"], + created_time: 1516269022, + expires_time: 1516385931, + body: { messagespecificattribute: "and its value" }, + }); + + // --- Packing encrypted and authenticated message --- + let didResolver = new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]); + let secretsResolver = new ExampleSecretsResolver(ALICE_SECRETS); + + const [encryptedMsg, encryptMetadata] = await msg.pack_encrypted( + BOB_DID, + null, + null, + didResolver, + secretsResolver, + { + forward: false, // TODO: should be true by default + } + ); + + console.log("Encryption metadata is\n", encryptMetadata); + + // --- Sending message --- + console.log("Sending message\n", encryptedMsg); + + // --- Unpacking message --- + didResolver = new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]); + secretsResolver = new ExampleSecretsResolver(BOB_SECRETS); + + const [unpackedMsg, unpackMetadata] = await Message.unpack( + encryptedMsg, + didResolver, + secretsResolver, + {} + ); + + console.log("Reveived message is\n", unpackedMsg.as_value()); + console.log("Reveived message unpack metadata is\n", unpackMetadata); +} + +async function signedUnencrypteed() { + // --- Building message from ALICE to BOB --- + const msg = new Message({ + id: "1234567890", + typ: "application/didcomm-plain+json", + type: "http://example.com/protocols/lets_do_lunch/1.0/proposal", + from: "did:example:alice", + to: ["did:example:bob"], + created_time: 1516269022, + expires_time: 1516385931, + body: { messagespecificattribute: "and its value" }, + }); + + // --- Packing encrypted and authenticated message --- + let didResolver = new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]); + let secretsResolver = new ExampleSecretsResolver(ALICE_SECRETS); + + const [signedMsg, signMetadata] = await msg.pack_signed( + ALICE_DID, + didResolver, + secretsResolver + ); + + console.log("Encryption metadata is\n", signMetadata); + + // --- Sending message --- + console.log("Sending message\n", signedMsg); + + // --- Unpacking message --- + didResolver = new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]); + secretsResolver = new ExampleSecretsResolver(BOB_SECRETS); + + const [unpackedMsg, unpackMetadata] = await Message.unpack( + signedMsg, + didResolver, + secretsResolver, + {} + ); + + console.log("Reveived message is\n", unpackedMsg.as_value()); + console.log("Reveived message unpack metadata is\n", unpackMetadata); +} + +async function plaintext() { + // --- Building message from ALICE to BOB --- + const msg = new Message({ + id: "1234567890", + typ: "application/didcomm-plain+json", + type: "http://example.com/protocols/lets_do_lunch/1.0/proposal", + from: "did:example:alice", + to: ["did:example:bob"], + created_time: 1516269022, + expires_time: 1516385931, + body: { messagespecificattribute: "and its value" }, + }); + + // --- Packing encrypted and authenticated message --- + let didResolver = new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]); + + const plaintextMsg = await msg.pack_plaintext(didResolver); + + // --- Sending message --- + console.log("Sending message\n", plaintextMsg); + + // --- Unpacking message --- + didResolver = new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]); + const secretsResolver = new ExampleSecretsResolver(BOB_SECRETS); + + const [unpackedMsg, unpackMetadata] = await Message.unpack( + plaintextMsg, + didResolver, + secretsResolver, + {} + ); + + console.log("Reveived message is\n", unpackedMsg.as_value()); + console.log("Reveived message unpack metadata is\n", unpackMetadata); +} + +main().catch((e) => console.log(e)); + +// TODO: add examples for Forward (routing) and Mediators diff --git a/affinidi-messaging-didcomm/wasm/demo/src/test-vectors.ts b/affinidi-messaging-didcomm/wasm/demo/src/test-vectors.ts new file mode 100644 index 0000000..670fa5f --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/demo/src/test-vectors.ts @@ -0,0 +1,376 @@ +export const ALICE_DID = "did:example:alice"; + +export const ALICE_DID_DOC = { + id: "did:example:alice", + keyAgreement: [ + "did:example:alice#key-x25519-not-in-secrets-1", + "did:example:alice#key-x25519-1", + "did:example:alice#key-p256-1", + "did:example:alice#key-p521-1", + ], + authentication: [ + "did:example:alice#key-1", + "did:example:alice#key-2", + "did:example:alice#key-3", + ], + verificationMethod: [ + { + id: "did:example:alice#key-x25519-1", + type: "JsonWebKey2020", + controller: "did:example:alice#key-x25519-1", + publicKeyJwk: { + crv: "X25519", + kty: "OKP", + x: "avH0O2Y4tqLAq8y9zpianr8ajii5m4F_mICrzNlatXs", + }, + }, + { + id: "did:example:alice#key-p256-1", + type: "JsonWebKey2020", + controller: "did:example:alice#key-p256-1", + publicKeyJwk: { + crv: "P-256", + kty: "EC", + x: "L0crjMN1g0Ih4sYAJ_nGoHUck2cloltUpUVQDhF2nHE", + y: "SxYgE7CmEJYi7IDhgK5jI4ZiajO8jPRZDldVhqFpYoo", + }, + }, + { + id: "did:example:alice#key-p521-1", + type: "JsonWebKey2020", + controller: "did:example:alice#key-p521-1", + publicKeyJwk: { + crv: "P-521", + kty: "EC", + x: "AHBEVPRhAv-WHDEvxVM9S0px9WxxwHL641Pemgk9sDdxvli9VpKCBdra5gg_4kupBDhz__AlaBgKOC_15J2Byptz", + y: "AciGcHJCD_yMikQvlmqpkBbVqqbg93mMVcgvXBYAQPP-u9AF7adybwZrNfHWCKAQwGF9ugd0Zhg7mLMEszIONFRk", + }, + }, + { + id: "did:example:alice#key-not-in-secrets-1", + type: "JsonWebKey2020", + controller: "did:example:alice#key-not-in-secrets-1", + publicKeyJwk: { + crv: "Ed25519", + kty: "OKP", + x: "G-boxFB6vOZBu-wXkm-9Lh79I8nf9Z50cILaOgKKGww", + }, + }, + { + id: "did:example:alice#key-1", + type: "JsonWebKey2020", + controller: "did:example:alice#key-1", + publicKeyJwk: { + crv: "Ed25519", + kty: "OKP", + x: "G-boxFB6vOZBu-wXkm-9Lh79I8nf9Z50cILaOgKKGww", + }, + }, + { + id: "did:example:alice#key-2", + type: "JsonWebKey2020", + controller: "did:example:alice#key-2", + publicKeyJwk: { + crv: "P-256", + kty: "EC", + x: "2syLh57B-dGpa0F8p1JrO6JU7UUSF6j7qL-vfk1eOoY", + y: "BgsGtI7UPsObMRjdElxLOrgAO9JggNMjOcfzEPox18w", + }, + }, + { + id: "did:example:alice#key-3", + type: "JsonWebKey2020", + controller: "did:example:alice#key-3", + publicKeyJwk: { + crv: "secp256k1", + kty: "EC", + x: "aToW5EaTq5mlAf8C5ECYDSkqsJycrW-e1SQ6_GJcAOk", + y: "JAGX94caA21WKreXwYUaOCYTBMrqaX4KWIlsQZTHWCk", + }, + }, + ], + service: [], +}; + +export const ALICE_SECRETS = [ + { + id: "did:example:alice#key-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "Ed25519", + d: "pFRUKkyzx4kHdJtFSnlPA9WzqkDT1HWV0xZ5OYZd2SY", + kty: "OKP", + x: "G-boxFB6vOZBu-wXkm-9Lh79I8nf9Z50cILaOgKKGww", + }, + }, + { + id: "did:example:alice#key-2", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "P-256", + d: "7TCIdt1rhThFtWcEiLnk_COEjh1ZfQhM4bW2wz-dp4A", + kty: "EC", + x: "2syLh57B-dGpa0F8p1JrO6JU7UUSF6j7qL-vfk1eOoY", + y: "BgsGtI7UPsObMRjdElxLOrgAO9JggNMjOcfzEPox18w", + }, + }, + { + id: "did:example:alice#key-3", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "secp256k1", + d: "N3Hm1LXA210YVGGsXw_GklMwcLu_bMgnzDese6YQIyA", + kty: "EC", + x: "aToW5EaTq5mlAf8C5ECYDSkqsJycrW-e1SQ6_GJcAOk", + y: "JAGX94caA21WKreXwYUaOCYTBMrqaX4KWIlsQZTHWCk", + }, + }, + { + id: "did:example:alice#key-x25519-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "X25519", + d: "r-jK2cO3taR8LQnJB1_ikLBTAnOtShJOsHXRUWT-aZA", + kty: "OKP", + x: "avH0O2Y4tqLAq8y9zpianr8ajii5m4F_mICrzNlatXs", + }, + }, + { + id: "did:example:alice#key-p256-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "P-256", + d: "sB0bYtpaXyp-h17dDpMx91N3Du1AdN4z1FUq02GbmLw", + kty: "EC", + x: "L0crjMN1g0Ih4sYAJ_nGoHUck2cloltUpUVQDhF2nHE", + y: "SxYgE7CmEJYi7IDhgK5jI4ZiajO8jPRZDldVhqFpYoo", + }, + }, + { + id: "did:example:alice#key-p521-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "P-521", + d: "AQCQKE7rZpxPnX9RgjXxeywrAMp1fJsyFe4cir1gWj-8t8xWaM_E2qBkTTzyjbRBu-JPXHe_auT850iYmE34SkWi", + kty: "EC", + x: "AHBEVPRhAv-WHDEvxVM9S0px9WxxwHL641Pemgk9sDdxvli9VpKCBdra5gg_4kupBDhz__AlaBgKOC_15J2Byptz", + y: "AciGcHJCD_yMikQvlmqpkBbVqqbg93mMVcgvXBYAQPP-u9AF7adybwZrNfHWCKAQwGF9ugd0Zhg7mLMEszIONFRk", + }, + }, +]; + +export const BOB_DID = "did:example:bob"; + +export const BOB_DID_DOC = { + id: "did:example:bob", + keyAgreement: [ + "did:example:bob#key-x25519-1", + "did:example:bob#key-x25519-2", + "did:example:bob#key-x25519-3", + "did:example:bob#key-p256-1", + "did:example:bob#key-p256-2", + "did:example:bob#key-p384-1", + "did:example:bob#key-p384-2", + "did:example:bob#key-p521-1", + "did:example:bob#key-p521-2", + ], + authentication: [], + verificationMethod: [ + { + id: "did:example:bob#key-x25519-1", + type: "JsonWebKey2020", + controller: "did:example:bob#key-x25519-1", + publicKeyJwk: { + crv: "X25519", + kty: "OKP", + x: "GDTrI66K0pFfO54tlCSvfjjNapIs44dzpneBgyx0S3E", + }, + }, + { + id: "did:example:bob#key-x25519-2", + type: "JsonWebKey2020", + controller: "did:example:bob#key-x25519-2", + publicKeyJwk: { + crv: "X25519", + kty: "OKP", + x: "UT9S3F5ep16KSNBBShU2wh3qSfqYjlasZimn0mB8_VM", + }, + }, + { + id: "did:example:bob#key-x25519-3", + type: "JsonWebKey2020", + controller: "did:example:bob#key-x25519-3", + publicKeyJwk: { + crv: "X25519", + kty: "OKP", + x: "82k2BTUiywKv49fKLZa-WwDi8RBf0tB0M8bvSAUQ3yY", + }, + }, + { + id: "did:example:bob#key-p256-1", + type: "JsonWebKey2020", + controller: "did:example:bob#key-p256-1", + publicKeyJwk: { + crv: "P-256", + kty: "EC", + x: "FQVaTOksf-XsCUrt4J1L2UGvtWaDwpboVlqbKBY2AIo", + y: "6XFB9PYo7dyC5ViJSO9uXNYkxTJWn0d_mqJ__ZYhcNY", + }, + }, + { + id: "did:example:bob#key-p256-2", + type: "JsonWebKey2020", + controller: "did:example:bob#key-p256-2", + publicKeyJwk: { + crv: "P-256", + kty: "EC", + x: "n0yBsGrwGZup9ywKhzD4KoORGicilzIUyfcXb1CSwe0", + y: "ov0buZJ8GHzV128jmCw1CaFbajZoFFmiJDbMrceCXIw", + }, + }, + { + id: "did:example:bob#key-p384-1", + type: "JsonWebKey2020", + controller: "did:example:bob#key-p384-1", + publicKeyJwk: { + crv: "P-384", + kty: "EC", + x: "MvnE_OwKoTcJVfHyTX-DLSRhhNwlu5LNoQ5UWD9Jmgtdxp_kpjsMuTTBnxg5RF_Y", + y: "X_3HJBcKFQEG35PZbEOBn8u9_z8V1F9V1Kv-Vh0aSzmH-y9aOuDJUE3D4Hvmi5l7", + }, + }, + { + id: "did:example:bob#key-p384-2", + type: "JsonWebKey2020", + controller: "did:example:bob#key-p384-2", + publicKeyJwk: { + crv: "P-384", + kty: "EC", + x: "2x3HOTvR8e-Tu6U4UqMd1wUWsNXMD0RgIunZTMcZsS-zWOwDgsrhYVHmv3k_DjV3", + y: "W9LLaBjlWYcXUxOf6ECSfcXKaC3-K9z4hCoP0PS87Q_4ExMgIwxVCXUEB6nf0GDd", + }, + }, + { + id: "did:example:bob#key-p521-1", + type: "JsonWebKey2020", + controller: "did:example:bob#key-p521-1", + publicKeyJwk: { + crv: "P-521", + kty: "EC", + x: "Af9O5THFENlqQbh2Ehipt1Yf4gAd9RCa3QzPktfcgUIFADMc4kAaYVViTaDOuvVS2vMS1KZe0D5kXedSXPQ3QbHi", + y: "ATZVigRQ7UdGsQ9j-omyff6JIeeUv3CBWYsZ0l6x3C_SYqhqVV7dEG-TafCCNiIxs8qeUiXQ8cHWVclqkH4Lo1qH", + }, + }, + { + id: "did:example:bob#key-p521-2", + type: "JsonWebKey2020", + controller: "did:example:bob#key-p521-2", + publicKeyJwk: { + crv: "P-521", + kty: "EC", + x: "ATp_WxCfIK_SriBoStmA0QrJc2pUR1djpen0VdpmogtnKxJbitiPq-HJXYXDKriXfVnkrl2i952MsIOMfD2j0Ots", + y: "AEJipR0Dc-aBZYDqN51SKHYSWs9hM58SmRY1MxgXANgZrPaq1EeGMGOjkbLMEJtBThdjXhkS5VlXMkF0cYhZELiH", + }, + }, + ], + service: [], +}; + +export const BOB_SECRETS = [ + { + id: "did:example:bob#key-x25519-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "X25519", + d: "b9NnuOCB0hm7YGNvaE9DMhwH_wjZA1-gWD6dA0JWdL0", + kty: "OKP", + x: "GDTrI66K0pFfO54tlCSvfjjNapIs44dzpneBgyx0S3E", + }, + }, + { + id: "did:example:bob#key-x25519-2", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "X25519", + d: "p-vteoF1gopny1HXywt76xz_uC83UUmrgszsI-ThBKk", + kty: "OKP", + x: "UT9S3F5ep16KSNBBShU2wh3qSfqYjlasZimn0mB8_VM", + }, + }, + { + id: "did:example:bob#key-x25519-3", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "X25519", + d: "f9WJeuQXEItkGM8shN4dqFr5fLQLBasHnWZ-8dPaSo0", + kty: "OKP", + x: "82k2BTUiywKv49fKLZa-WwDi8RBf0tB0M8bvSAUQ3yY", + }, + }, + { + id: "did:example:bob#key-p256-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "P-256", + d: "PgwHnlXxt8pwR6OCTUwwWx-P51BiLkFZyqHzquKddXQ", + kty: "EC", + x: "FQVaTOksf-XsCUrt4J1L2UGvtWaDwpboVlqbKBY2AIo", + y: "6XFB9PYo7dyC5ViJSO9uXNYkxTJWn0d_mqJ__ZYhcNY", + }, + }, + { + id: "did:example:bob#key-p256-2", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "P-256", + d: "agKz7HS8mIwqO40Q2dwm_Zi70IdYFtonN5sZecQoxYU", + kty: "EC", + x: "n0yBsGrwGZup9ywKhzD4KoORGicilzIUyfcXb1CSwe0", + y: "ov0buZJ8GHzV128jmCw1CaFbajZoFFmiJDbMrceCXIw", + }, + }, + { + id: "did:example:bob#key-p384-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "P-384", + d: "ajqcWbYA0UDBKfAhkSkeiVjMMt8l-5rcknvEv9t_Os6M8s-HisdywvNCX4CGd_xY", + kty: "EC", + x: "MvnE_OwKoTcJVfHyTX-DLSRhhNwlu5LNoQ5UWD9Jmgtdxp_kpjsMuTTBnxg5RF_Y", + y: "X_3HJBcKFQEG35PZbEOBn8u9_z8V1F9V1Kv-Vh0aSzmH-y9aOuDJUE3D4Hvmi5l7", + }, + }, + { + id: "did:example:bob#key-p384-2", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "P-384", + d: "OiwhRotK188BtbQy0XBO8PljSKYI6CCD-nE_ZUzK7o81tk3imDOuQ-jrSWaIkI-T", + kty: "EC", + x: "2x3HOTvR8e-Tu6U4UqMd1wUWsNXMD0RgIunZTMcZsS-zWOwDgsrhYVHmv3k_DjV3", + y: "W9LLaBjlWYcXUxOf6ECSfcXKaC3-K9z4hCoP0PS87Q_4ExMgIwxVCXUEB6nf0GDd", + }, + }, + { + id: "did:example:bob#key-p521-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "P-521", + d: "AV5ocjvy7PkPgNrSuvCxtG70NMj6iTabvvjSLbsdd8OdI9HlXYlFR7RdBbgLUTruvaIRhjEAE9gNTH6rWUIdfuj6", + kty: "EC", + x: "Af9O5THFENlqQbh2Ehipt1Yf4gAd9RCa3QzPktfcgUIFADMc4kAaYVViTaDOuvVS2vMS1KZe0D5kXedSXPQ3QbHi", + y: "ATZVigRQ7UdGsQ9j-omyff6JIeeUv3CBWYsZ0l6x3C_SYqhqVV7dEG-TafCCNiIxs8qeUiXQ8cHWVclqkH4Lo1qH", + }, + }, + { + id: "did:example:bob#key-p521-2", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "P-521", + d: "ABixMEZHsyT7SRw-lY5HxdNOofTZLlwBHwPEJ3spEMC2sWN1RZQylZuvoyOBGJnPxg4-H_iVhNWf_OtgYODrYhCk", + kty: "EC", + x: "ATp_WxCfIK_SriBoStmA0QrJc2pUR1djpen0VdpmogtnKxJbitiPq-HJXYXDKriXfVnkrl2i952MsIOMfD2j0Ots", + y: "AEJipR0Dc-aBZYDqN51SKHYSWs9hM58SmRY1MxgXANgZrPaq1EeGMGOjkbLMEJtBThdjXhkS5VlXMkF0cYhZELiH", + }, + }, +]; diff --git a/affinidi-messaging-didcomm/wasm/demo/tslint.json b/affinidi-messaging-didcomm/wasm/demo/tslint.json new file mode 100644 index 0000000..2562f1a --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/demo/tslint.json @@ -0,0 +1,12 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": { + "no-console": false, + "max-classes-per-file": false + }, + "rulesDirectory": [] +} \ No newline at end of file diff --git a/affinidi-messaging-didcomm/wasm/src/did/did_doc.rs b/affinidi-messaging-didcomm/wasm/src/did/did_doc.rs new file mode 100644 index 0000000..e140534 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/src/did/did_doc.rs @@ -0,0 +1,93 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const DID_DOC_TS: &'static str = r#" +/** + * Represents DID Document (https://www.w3.org/TR/did-core/) + */ +type DIDDoc = { + /** + * DID for the given DID Doc + */ + id: string, + + /** + * DID URLs of verification methods used for key agreement. + * See https://www.w3.org/TR/did-core/#verification-methods. + */ + keyAgreement: Array, + + /** + * Returns DID URLs of verification methods used for authentication. + * See https://www.w3.org/TR/did-core/#authentication + */ + authentication: Array, + + /** + * All local verification methods including embedded to + * key agreement and authentication sections. + * See https://www.w3.org/TR/did-core/#verification-methods. + */ + verificationMethod: Array, + + /** + * All services (https://www.w3.org/TR/did-core/#services) + */ + service: Array, +} +"#; + +#[wasm_bindgen(typescript_custom_section)] +const VERIFICATION_METHOD_TS: &'static str = r#" +/** + * Represents verification method record in DID Document + * (https://www.w3.org/TR/did-core/#verification-methods). + */ +type VerificationMethod = { + id: string, + type: VerificationMethodType, + controller: string, + publicKeyJwk?: any, + publicKeyMultibase?: string, + publicKeyBase58?: string, +} +"#; + +#[wasm_bindgen(typescript_custom_section)] +const VERIFICATION_METHOD_TYPE_TS: &'static str = r#" +type VerificationMethodType = "JsonWebKey2020" | "X25519KeyAgreementKey2019" + | "Ed25519VerificationKey2018" | "EcdsaSecp256k1VerificationKey2019" | string +"#; + +#[wasm_bindgen(typescript_custom_section)] +const SERVICE_TS: &'static str = r#" +/** + * Represents service record in DID Document (https://www.w3.org/TR/did-core/#services). + */ +type Service = { + id: string, + type: string, + serviceEndpoint: ServiceKind, +} +"#; + +#[wasm_bindgen(typescript_custom_section)] +const SERVICE_KIND_TS: &'static str = r#" +/** + * Represents additional service properties defined for specific Service type. + */ +type ServiceKind = DIDCommMessagingService | any +"#; + +#[wasm_bindgen(typescript_custom_section)] +const DIDCOMM_MESSAGING_SERVICE_TS: &'static str = r#" +/** + * Properties for DIDCommMessagingService + * (https://identity.foundation/didcomm-messaging/spec/#did-document-service-endpoint). + */ +type DIDCommMessagingService = { + uri: string, + accept?: Array, + routing_keys: Array, +} +"#; diff --git a/affinidi-messaging-didcomm/wasm/src/did/did_resolver.rs b/affinidi-messaging-didcomm/wasm/src/did/did_resolver.rs new file mode 100644 index 0000000..282aa99 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/src/did/did_resolver.rs @@ -0,0 +1,66 @@ +use async_trait::async_trait; +use didcomm::{ + did::{DIDDoc, DIDResolver as _DIDResolver}, + error::{ErrorKind, Result as _Result, ResultContext, ResultExt}, +}; +use wasm_bindgen::prelude::*; + +use crate::error::FromJsResult; + +#[wasm_bindgen] +extern "C" { + pub type DIDResolver; + + #[wasm_bindgen(structural, method, catch)] + pub async fn resolve(this: &DIDResolver, did: &str) -> Result; +} + +#[wasm_bindgen(typescript_custom_section)] +const DID_RESOLVER_TS: &'static str = r#" +/** + * Represents DID Doc resolver (https://www.w3.org/TR/did-core/#did-resolution). + */ +interface DIDResolver { + /** + * Resolves a DID document by the given DID. + * + * @param `did` a DID to be resolved. + * + * @returns An instance of resolved DID DOC or null if DID is not found. + * + * @throws DIDCommMalformed - Resolved DID Doc looks malformed + * @throws DIDCommIoError - IO error in resolving process + * @throws DIDCommInvalidState - Code error or unexpected state was detected + * + * Note to throw compatible error use code like this + * + * ``` + * let e = Error("Unble perform io operation"); + * e.name = "DIDCommIoError" + * throw e + * ``` + */ + resolve(did: string): Promise; +} +"#; + +pub(crate) struct JsDIDResolver(pub(crate) DIDResolver); + +#[async_trait(?Send)] +impl _DIDResolver for JsDIDResolver { + async fn resolve(&self, did: &str) -> _Result> { + let ddoc = self + .0 + .resolve(did) + .await + .from_js() + .context("Unable resolve did")?; + + let ddoc: Option = ddoc.into_serde().kind( + ErrorKind::InvalidState, + "Unable deserialize DIDDoc from JsValue", + )?; + + Ok(ddoc) + } +} diff --git a/affinidi-messaging-didcomm/wasm/src/did/mod.rs b/affinidi-messaging-didcomm/wasm/src/did/mod.rs new file mode 100644 index 0000000..74ecd2b --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/src/did/mod.rs @@ -0,0 +1,5 @@ +mod did_doc; +mod did_resolver; + +pub use did_resolver::DIDResolver; +pub(crate) use did_resolver::JsDIDResolver; diff --git a/affinidi-messaging-didcomm/wasm/src/error.rs b/affinidi-messaging-didcomm/wasm/src/error.rs new file mode 100644 index 0000000..6453dcf --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/src/error.rs @@ -0,0 +1,71 @@ +use didcomm::error::{err_msg as _err_msg, ErrorKind as _ErrorKind, Result as _Result}; +use js_sys::{Error as JsError, JsString}; +use wasm_bindgen::{prelude::*, JsCast}; + +// Alows convertion of didcomm error to javascript error +pub(crate) trait JsResult { + fn as_js(self) -> Result; +} + +impl JsResult for _Result { + fn as_js(self) -> Result { + self.map_err(|e| { + let name = match e.kind() { + _ErrorKind::DIDNotResolved => "DIDCommDIDNotResolved", + _ErrorKind::DIDUrlNotFound => "DIDCommDIDUrlNotFound", + _ErrorKind::Malformed => "DIDCommMalformed", + _ErrorKind::IoError => "DIDCommIoError", + _ErrorKind::InvalidState => "DIDCommInvalidState", + _ErrorKind::NoCompatibleCrypto => "DIDCommNoCompatibleCrypto", + _ErrorKind::Unsupported => "DIDCommUnsupported", + _ErrorKind::IllegalArgument => "DIDCommIllegalArgument", + _ErrorKind::SecretNotFound => "DIDCommSecretNotFound", + }; + + let e = JsError::new(&format!("{}", e)); + e.set_name(name); + e + }) + } +} + +// Alows convertion of javascript error to didcomm error +pub(crate) trait FromJsResult { + fn from_js(self) -> _Result; +} + +impl FromJsResult for Result { + fn from_js(self) -> _Result { + self.map_err(|e| { + // String was thrown + if let Some(e) = e.dyn_ref::() { + return _err_msg( + _ErrorKind::InvalidState, + e.as_string().unwrap_or(format!("{:?}", e)), + ); + } + + // Error instance was thrown + if let Some(e) = e.dyn_ref::() { + let kind = match e.name().as_string().as_deref() { + Some("DIDCommDIDNotResolved") => _ErrorKind::DIDNotResolved, + Some("DIDCommDIDUrlNotFound") => _ErrorKind::DIDUrlNotFound, + Some("DIDCommMalformed") => _ErrorKind::Malformed, + Some("DIDCommIoError") => _ErrorKind::IoError, + Some("DIDCommInvalidState") => _ErrorKind::InvalidState, + Some("DIDCommNoCompatibleCrypto") => _ErrorKind::NoCompatibleCrypto, + Some("DIDCommUnsupported") => _ErrorKind::Unsupported, + Some("DIDCommIllegalArgument") => _ErrorKind::IllegalArgument, + _ => _ErrorKind::InvalidState, + }; + + let message = e.message().as_string().unwrap_or(format!("{:?}", e)); + + return _err_msg(kind, message); + } + + // Something unusual was thrown + _err_msg(_ErrorKind::InvalidState, format!("{:?}", e)) + }) + } +} diff --git a/affinidi-messaging-didcomm/wasm/src/lib.rs b/affinidi-messaging-didcomm/wasm/src/lib.rs new file mode 100644 index 0000000..fe3e5ff --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/src/lib.rs @@ -0,0 +1,18 @@ +mod did; +mod error; +mod message; +mod secrets; +mod utils; + +pub use crate::{ + did::DIDResolver, + message::{FromPrior, Message}, + secrets::SecretsResolver, +}; +use crate::{did::JsDIDResolver, secrets::JsSecretsResolver}; + +// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global +// allocator. +#[cfg(feature = "wee_alloc")] +#[global_allocator] +static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT; diff --git a/affinidi-messaging-didcomm/wasm/src/message/from_prior/mod.rs b/affinidi-messaging-didcomm/wasm/src/message/from_prior/mod.rs new file mode 100644 index 0000000..802370b --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/src/message/from_prior/mod.rs @@ -0,0 +1,76 @@ +mod pack; +mod unpack; + +use didcomm::error::{ErrorKind, ResultExt}; +use std::rc::Rc; +use wasm_bindgen::prelude::*; + +use crate::{error::JsResult, utils::set_panic_hook}; + +#[wasm_bindgen] +/// Allows building of `from_prior` message header according +/// to DIDComm DID Rotation procedure +/// https://identity.foundation/didcomm-messaging/spec/#did-rotation. +pub struct FromPrior(pub(crate) Rc); + +#[wasm_bindgen] +impl FromPrior { + #[wasm_bindgen(constructor)] + /// Instantiates FromPrior from plain object + pub fn new(value: IFromPrior) -> Result { + // TODO: Better place? + set_panic_hook(); + + let msg = value + .into_serde() + .kind(ErrorKind::Malformed, "Unable deserialize FromPrior") + .as_js()?; + + Ok(FromPrior(Rc::new(msg))) + } + + #[wasm_bindgen(skip_typescript)] + pub fn as_value(&self) -> Result { + let msg = JsValue::from_serde(&*self.0) + .kind(ErrorKind::Malformed, "Unable serialize FromPrior") + .as_js()?; + + Ok(msg) + } +} + +#[wasm_bindgen(typescript_custom_section)] +const MESSAGE_AS_VALUE_TS: &'static str = r#" +interface FromPrior { + /** + * @returns FromPrior representation as plain object + */ + as_value(): IFromPrior; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IFromPrior")] + pub type IFromPrior; +} + +#[wasm_bindgen(typescript_custom_section)] +const IMESSAGE_TS: &'static str = r#" +type IFromPrior = { + /** + * new DID after rotation + */ + iss: string, + + /** + * prior DID + */ + sub: string, + + /** + * Datetime of the DID rotation + */ + iat?: number, +} +"#; diff --git a/affinidi-messaging-didcomm/wasm/src/message/from_prior/pack.rs b/affinidi-messaging-didcomm/wasm/src/message/from_prior/pack.rs new file mode 100644 index 0000000..5b80cc5 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/src/message/from_prior/pack.rs @@ -0,0 +1,68 @@ +use js_sys::{Array, Promise}; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::future_to_promise; + +use crate::{ + error::JsResult, DIDResolver, FromPrior, JsDIDResolver, JsSecretsResolver, SecretsResolver, +}; + +#[wasm_bindgen] +impl FromPrior { + #[wasm_bindgen(skip_typescript)] + pub fn pack( + &self, + issuer_kid: Option, + did_resolver: DIDResolver, + secrets_resolver: SecretsResolver, + ) -> Promise { + let from_prior = self.0.clone(); + let did_resolver = JsDIDResolver(did_resolver); + let secrets_resolver = JsSecretsResolver(secrets_resolver); + + future_to_promise(async move { + let (msg, metadata) = from_prior + .pack(issuer_kid.as_deref(), &did_resolver, &secrets_resolver) + .await + .as_js()?; + + let res = { + let res = Array::new_with_length(2); + res.set(0, msg.into()); + res.set(1, metadata.into()); + res + }; + + Ok(res.into()) + }) + } +} + +#[wasm_bindgen(typescript_custom_section)] +const FROM_PRIOR_PACK_TS: &'static str = r#" +interface FromPrior { + /** + * Packs a plaintext `from_prior` value into a signed JWT. + * https://identity.foundation/didcomm-messaging/spec/#did-rotation + * + * @param issuer_kid (optional) identifier of the issuer key being used to sign `from_prior` JWT value + * @param did_resolver instance of `DIDResolver` to resolve DIDs + * @param secrets_resolver instance of `SecretsResolver` to resolve issuer DID keys secrets + * + * @returns promise resolving to a tuple of the signed `from_prior` JWT and the identifier of the issuer key + * actually used to sign `from_prior` + * + * @throws DIDCommMalformed `from_prior` plaintext value has invalid format. + * @throws DIDCommIllegalArgument `issuer_kid` is invalid or does not consist with `from_prior` plaintext value. + * @throws DIDCommDIDNotResolved Issuer DID not found. + * @throws DIDCommDIDUrlNotFound Issuer authentication verification method is not found. + * @throws DIDCommSecretNotFound Issuer secret is not found. + * @throws DIDCommUnsupported Used crypto or method is unsupported. + * @throws DIDCommInvalidState Indicates a library error. + */ + pack( + issuer_kid: string | null, + did_resolver: DIDResolver, + secrets_resolver: SecretsResolver, + ): Promise<[string, string]>; +} +"#; diff --git a/affinidi-messaging-didcomm/wasm/src/message/from_prior/unpack.rs b/affinidi-messaging-didcomm/wasm/src/message/from_prior/unpack.rs new file mode 100644 index 0000000..76c419c --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/src/message/from_prior/unpack.rs @@ -0,0 +1,57 @@ +use js_sys::{Array, Promise}; +use std::rc::Rc; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::future_to_promise; + +use crate::{error::JsResult, utils::set_panic_hook, DIDResolver, FromPrior, JsDIDResolver}; + +#[wasm_bindgen(skip_typescript)] +impl FromPrior { + #[wasm_bindgen(skip_typescript)] + pub fn unpack(from_prior: String, did_resolver: DIDResolver) -> Promise { + // TODO: Better place? + set_panic_hook(); + + let did_resolver = JsDIDResolver(did_resolver); + + future_to_promise(async move { + let (msg, metadata) = didcomm::FromPrior::unpack(&from_prior, &did_resolver) + .await + .as_js()?; + + let res = { + let res = Array::new_with_length(2); + res.set(0, FromPrior(Rc::new(msg)).into()); + res.set(1, metadata.into()); + res + }; + + Ok(res.into()) + }) + } +} + +#[wasm_bindgen(typescript_custom_section)] +const FROM_PRIOR_UNPACK_TS: &'static str = r#" +export namespace FromPrior { + /** + * Unpacks a plaintext value from a signed `from_prior` JWT. + * https://identity.foundation/didcomm-messaging/spec/#did-rotation + * + * @param from_prior_jwt signed `from_prior` JWT + * @param did_resolver instance of `DIDResolver` to resolve DIDs + * + * @returns promise resolving to a tuple of the plaintext `from_prior` value and the identifier + * of the issuer key used to sign `from_prior` + * + * @throws DIDCommMalformed Signed `from_prior` JWT is malformed. + * @throws DIDCommDIDNotResolved Issuer DID not found. + * @throws DIDCommDIDUrlNotFound Issuer authentication verification method is not found. + * @throws DIDCommUnsupported Used crypto or method is unsupported. + */ + function unpack( + from_prior: string, + did_resolver: DIDResolver, + ): Promise<[FromPrior, string]>; +} +"#; diff --git a/affinidi-messaging-didcomm/wasm/src/message/mod.rs b/affinidi-messaging-didcomm/wasm/src/message/mod.rs new file mode 100644 index 0000000..b02ddb2 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/src/message/mod.rs @@ -0,0 +1,254 @@ +mod from_prior; +mod pack_encrypted; +mod pack_plaintext; +mod pack_signed; +mod protocols; +mod unpack; + +use didcomm::error::{ErrorKind, ResultExt}; +use std::rc::Rc; +use wasm_bindgen::prelude::*; + +use crate::{error::JsResult, utils::set_panic_hook}; +pub use from_prior::FromPrior; + +#[wasm_bindgen] +/// Wrapper for plain message. Provides helpers for message building and packing/unpacking. +pub struct Message(pub(crate) Rc); + +#[wasm_bindgen] +impl Message { + #[wasm_bindgen(constructor)] + /// Instantiates message from plain object + pub fn new(value: IMessage) -> Result { + // TODO: Better place? + set_panic_hook(); + + let msg = value + .into_serde() + .kind(ErrorKind::Malformed, "Unable deserialize Message") + .as_js()?; + + Ok(Message(Rc::new(msg))) + } + + #[wasm_bindgen(skip_typescript)] + pub fn as_value(&self) -> Result { + let msg = JsValue::from_serde(&*self.0) + .kind(ErrorKind::Malformed, "Unable serialize Message") + .as_js()?; + + Ok(msg) + } +} + +#[wasm_bindgen(typescript_custom_section)] +const MESSAGE_AS_VALUE_TS: &'static str = r#" +interface Message { + /** + * @returns message representation as plain object + */ + as_value(): IMessage; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IMessage")] + pub type IMessage; +} + +#[wasm_bindgen(typescript_custom_section)] +const IMESSAGE_TS: &'static str = r#" +type IMessage = { + /** + * Message id. Must be unique to the sender. + */ + id: string, + + /** + * Must be "application/didcomm-plain+json" + */ + typ: string, + + /** + * Message type attribute value MUST be a valid Message Type URI, + * that when resolved gives human readable information about the message. + * The attribute’s value also informs the content of the message, + * or example the presence of other attributes and how they should be processed. + */ + type: string, + + /** + * Message body. + */ + body: any, + + /** + * Sender identifier. The from attribute MUST be a string that is a valid DID + * or DID URL (without the fragment component) which identifies the sender of the message. + */ + from?: string, + + /** + * Identifier(s) for recipients. MUST be an array of strings where each element + * is a valid DID or DID URL (without the fragment component) that identifies a member + * of the message’s intended audience. + */ + to?: Array, + + /** + * Uniquely identifies the thread that the message belongs to. + * If not included the id property of the message MUST be treated as the value of the `thid`. + */ + thid?: string, + + /** + * If the message is a child of a thread the `pthid` + * will uniquely identify which thread is the parent. + */ + pthid?: string, + + /** + * Custom message headers. + */ + [extra_header: string]: any + + /** + * The attribute is used for the sender + * to express when they created the message, expressed in + * UTC Epoch Seconds (seconds since 1970-01-01T00:00:00Z UTC). + * This attribute is informative to the recipient, and may be relied on by protocols. + */ + created_time?: number, + + /** + * The expires_time attribute is used for the sender to express when they consider + * the message to be expired, expressed in UTC Epoch Seconds (seconds since 1970-01-01T00:00:00Z UTC). + * This attribute signals when the message is considered no longer valid by the sender. + * When omitted, the message is considered to have no expiration by the sender. + */ + expires_time?: number, + + /** + * from_prior is a compactly serialized signed JWT containing FromPrior value + */ + from_prior?: string, + + /** + * Message attachments + */ + attachments?: Array, +}; +"#; + +#[wasm_bindgen(typescript_custom_section)] +const ATTACHMENT_TS: &'static str = r#" +type Attachment = { + /** + * A JSON object that gives access to the actual content of the attachment. + * Can be based on base64, json or external links. + */ + data: AttachmentData, + + /** + * Identifies attached content within the scope of a given message. + * Recommended on appended attachment descriptors. Possible but generally unused + * on embedded attachment descriptors. Never required if no references to the attachment + * exist; if omitted, then there is no way to refer to the attachment later in the thread, + * in error messages, and so forth. Because id is used to compose URIs, it is recommended + * that this name be brief and avoid spaces and other characters that require URI escaping. + */ + id?: string, + + /** + * A human-readable description of the content. + */ + description?: string, + + /** + * A hint about the name that might be used if this attachment is persisted as a file. + * It is not required, and need not be unique. If this field is present and mime-type is not, + * the extension on the filename may be used to infer a MIME type. + */ + filename?: string, + + /** + * Describes the MIME type of the attached content. + */ + media_type?: string, + + /** + * Describes the format of the attachment if the mime_type is not sufficient. + */ + format?: string, + + /** + * A hint about when the content in this attachment was last modified + * in UTC Epoch Seconds (seconds since 1970-01-01T00:00:00Z UTC). + */ + lastmod_time?: number, + + /** + * Mostly relevant when content is included by reference instead of by value. + * Lets the receiver guess how expensive it will be, in time, bandwidth, and storage, + * to fully fetch the attachment. + */ + byte_count?: number, +} +"#; + +#[wasm_bindgen(typescript_custom_section)] +const ATTACHMENT_DATA_TS: &'static str = r#" +type AttachmentData = Base64AttachmentData | JsonAttachmentData | LinksAttachmentData +"#; + +#[wasm_bindgen(typescript_custom_section)] +const BASE64_ATTACHMENT_DATA_TS: &'static str = r#" +type Base64AttachmentData = { + /** + * Base64-encoded data, when representing arbitrary content inline. + */ + base64: string, + + /** + * A JSON Web Signature over the content of the attachment. + */ + jws?: string, +} +"#; + +#[wasm_bindgen(typescript_custom_section)] +const JSON_ATTACHMENT_DATA_TS: &'static str = r#" +type JsonAttachmentData = { + /** + * Directly embedded JSON data. + */ + json: any, + + /** + * A JSON Web Signature over the content of the attachment. + */ + jws?: string, +} +"#; + +#[wasm_bindgen(typescript_custom_section)] +const LINKS_ATTACHMENT_DATA_TS: &'static str = r#" +type LinksAttachmentData = { + /** + * A list of one or more locations at which the content may be fetched. + */ + links: Array, + + /** + * The hash of the content encoded in multi-hash format. Used as an integrity check for the attachment. + */ + hash: string, + + /** + * A JSON Web Signature over the content of the attachment. + */ + jws?: string, +} +"#; diff --git a/affinidi-messaging-didcomm/wasm/src/message/pack_encrypted.rs b/affinidi-messaging-didcomm/wasm/src/message/pack_encrypted.rs new file mode 100644 index 0000000..5bfde02 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/src/message/pack_encrypted.rs @@ -0,0 +1,226 @@ +use didcomm::{ + error::{ErrorKind, ResultExt}, + PackEncryptedOptions, +}; +use js_sys::{Array, Promise}; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::future_to_promise; + +use crate::{ + error::JsResult, DIDResolver, JsDIDResolver, JsSecretsResolver, Message, SecretsResolver, +}; + +#[wasm_bindgen] +impl Message { + #[wasm_bindgen(skip_typescript)] + pub fn pack_encrypted( + &self, + to: String, + from: Option, + sign_by: Option, + did_resolver: DIDResolver, + secrets_resolver: SecretsResolver, + options: JsValue, + ) -> Promise { + let msg = self.0.clone(); + let did_resolver = JsDIDResolver(did_resolver); + let secrets_resolver = JsSecretsResolver(secrets_resolver); + + future_to_promise(async move { + let options: PackEncryptedOptions = options + .into_serde() + .kind(ErrorKind::Malformed, "Options param is malformed") + .as_js()?; + + let (msg, metadata) = msg + .pack_encrypted( + &to, + from.as_deref(), + sign_by.as_deref(), + &did_resolver, + &secrets_resolver, + &options, + ) + .await + .as_js()?; + + let metadata = JsValue::from_serde(&metadata) + .kind( + ErrorKind::InvalidState, + "Unable serialize PackEncryptedMetadata", + ) + .as_js()?; + + let res = { + let res = Array::new_with_length(2); + res.set(0, msg.into()); + res.set(1, metadata); + res + }; + + Ok(res.into()) + }) + } +} + +#[wasm_bindgen(typescript_custom_section)] +const MESSAGE_PACK_ENCRYPTED_TS: &'static str = r#" +interface Message { + /** + * Produces `DIDComm Encrypted Message` + * https://identity.foundation/didcomm-messaging/spec/#didcomm-encrypted-message. + * + * A DIDComm encrypted message is an encrypted JWM (JSON Web Messages) and + * hides its content from all but authorized recipients, discloses (optionally) and proves + * the sender to exactly and only those recipients, and provides integrity guarantees. + * It is important in privacy-preserving routing. It is what normally moves over network + * transports in DIDComm applications, and is the safest format for storing DIDComm data at rest. + * + * Encryption is done as following: + * - Encryption is done via the keys from the `keyAgreement` verification relationship in the DID Doc + * - if `to` is a DID, then multiplex encryption is done for all keys from the + * receiver's `keyAgreement` verification relationship + * which are compatible the sender's key. + * - if `to` is a key ID, then encryption is done for the receiver's `keyAgreement` + * verification method identified by the given key ID. + * - if `from` is a DID, then sender `keyAgreement` will be negotiated based on recipient preference and + * sender-recipient crypto compatibility. + * - if `from` is a key ID, then the sender's `keyAgreement` verification method + * identified by the given key ID is used. + * - if `from` is None, then anonymous encryption is done and there will be no sender authentication property. + * + * It's possible to add non-repudiation by providing `sign_by` parameter. + * + * @param `to` recipient DID or key ID the sender uses encryption. + * @param `from` a sender DID or key ID. If set message will be repudiable authenticated or anonymous otherwise. + * Must match `from` header in Plaintext if the header is set. + * @param `sign_by` if `Some` message will be additionally signed to provide additional non-repudiable authentication + * by provided DID/Key. Signed messages are only necessary when the origin of plaintext must be provable + * to third parties, or when the sender can’t be proven to the recipient by authenticated encryption because + * the recipient is not known in advance (e.g., in a broadcast scenario). + * Adding a signature when one is not needed can degrade rather than enhance security because + * it relinquishes the sender’s ability to speak off the record. + * @param `did_resolver` instance of `DIDResolver` to resolve DIDs. + * @param `secrets_resolver` instance of SecretsResolver` to resolve sender DID keys secrets. + * @param `options` allow fine configuration of packing process. + * + * @returns Tuple `[encrypted_message, metadata]`. + * - `encrypted_message` A DIDComm encrypted message as a JSON string. + * - `metadata` additional metadata about this `pack` execution like used keys identifiers, + * used messaging service. + * + * @throws DIDCommDIDNotResolved + * @throws DIDCommDIDUrlNotFound + * @throws DIDCommMalformed + * @throws DIDCommIoError + * @throws DIDCommInvalidState + * @throws DIDCommNoCompatibleCrypto + * @throws DIDCommUnsupported + * @throws DIDCommIllegalArgument + */ + pack_encrypted( + to: string, + from: string | null, + sign_by: string | null, + did_resolver: DIDResolver, + secrets_resolver: SecretsResolver, + options: PackEncryptedOptions, + ): Promise<[string, PackEncryptedMetadata]>; +} +"#; + +#[wasm_bindgen(typescript_custom_section)] +const PACK_ENCRYPTED_OPTIONS_TS: &'static str = r#" +/** + * Allow fine configuration of packing process. + */ +type PackEncryptedOptions = { + /** + * If `true` and message is authenticated than information about sender will be protected from mediators, but + * additional re-encryption will be required. For anonymous messages this property will be ignored. + * Default false. + */ + protect_sender?: boolean, + + /** + * Whether the encrypted messages need to be wrapped into `Forward` messages to be sent to Mediators + * as defined by the `Forward` protocol. + * Default true. + */ + forward?: boolean, + + /** + * if forward is enabled these optional headers can be passed to the wrapping `Forward` messages. + * If forward is disabled this property will be ignored. + */ + forward_headers?: Array<[string, string]>, + + /** + * Identifier (DID URL) of messaging service (https://identity.foundation/didcomm-messaging/spec/#did-document-service-endpoint). + * If DID contains multiple messaging services it allows specify what service to use. + * If not present first service will be used. + */ + messaging_service?: string, + + /** + * Algorithm used for authenticated encryption. + * Default "A256cbcHs512Ecdh1puA256kw" + */ + enc_alg_auth?: "A256cbcHs512Ecdh1puA256kw", + + /** + * Algorithm used for anonymous encryption. + * Default "Xc20pEcdhEsA256kw" + */ + enc_alg_anon?: "A256cbcHs512EcdhEsA256kw" | "Xc20pEcdhEsA256kw" | "A256gcmEcdhEsA256kw", +} +"#; + +#[wasm_bindgen(typescript_custom_section)] +const PACK_ENCRYPTED_METADATA_TS: &'static str = r#" +/** + * Additional metadata about this `encrypt` method execution like used keys identifiers, + * used messaging service. + */ +type PackEncryptedMetadata = { + /** + * Information about messaging service used for message preparation. + * Practically `service_endpoint` field can be used to transport the message. + */ + messaging_service?: MessagingServiceMetadata, + + /** + * Identifier (DID URL) of sender key used for message encryption. + */ + from_kid?: string, + + /** + * Identifier (DID URL) of sender key used for message sign. + */ + sign_by_kid?: string, + + /** + * Identifiers (DID URLs) of recipient keys used for message encryption. + */ + to_kids: Array, +} +"#; + +#[wasm_bindgen(typescript_custom_section)] +const MESSAGING_SERVICE_METADATA_TS: &'static str = r#" +/** + * Information about messaging service used for message preparation. + * Practically `service_endpoint` field can be used to transport the message. + */ +type MessagingServiceMetadata = { + /** + * Identifier (DID URL) of used messaging service. + */ + id: string, + + /** + * Service endpoint of used messaging service. + */ + service_endpoint: string, +} +"#; diff --git a/affinidi-messaging-didcomm/wasm/src/message/pack_plaintext.rs b/affinidi-messaging-didcomm/wasm/src/message/pack_plaintext.rs new file mode 100644 index 0000000..8d35c43 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/src/message/pack_plaintext.rs @@ -0,0 +1,48 @@ +use js_sys::Promise; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::future_to_promise; + +use crate::{error::JsResult, DIDResolver, JsDIDResolver, Message}; + +#[wasm_bindgen(skip_typescript)] +impl Message { + #[wasm_bindgen(skip_typescript)] + pub fn pack_plaintext(&self, did_resolver: DIDResolver) -> Promise { + let msg = self.0.clone(); + let did_resolver = JsDIDResolver(did_resolver); + + future_to_promise(async move { + let msg = msg.pack_plaintext(&did_resolver).await.as_js()?; + + Ok(msg.into()) + }) + } +} + +#[wasm_bindgen(typescript_custom_section)] +const MESSAGE_PACK_PLAYNTEXT_TS: &'static str = r#" +interface Message { + /** + * Produces `DIDComm Plaintext Messages` + * https://identity.foundation/didcomm-messaging/spec/#didcomm-plaintext-messages. + * + * A DIDComm message in its plaintext form, not packaged into any protective envelope, + * is known as a DIDComm plaintext message. Plaintext messages lack confidentiality and integrity + * guarantees, and are repudiable. They are therefore not normally transported across security boundaries. + * However, this may be a helpful format to inspect in debuggers, since it exposes underlying semantics, + * and it is the format used in this spec to give examples of headers and other internals. + * Depending on ambient security, plaintext may or may not be an appropriate format for DIDComm data at rest. + * + * @param `did_resolver` instance of `DIDResolver` to resolve DIDs. + * + * @returns a DIDComm plaintext message s JSON string + * + * @throws DIDCommDIDNotResolved + * @throws DIDCommDIDUrlNotFound + * @throws DIDCommIoError + * @throws DIDCommInvalidState + * @throws DIDCommIllegalArgument + */ + pack_plaintext(did_resolver: DIDResolver): Promise; +} +"#; diff --git a/affinidi-messaging-didcomm/wasm/src/message/pack_signed.rs b/affinidi-messaging-didcomm/wasm/src/message/pack_signed.rs new file mode 100644 index 0000000..ff4577f --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/src/message/pack_signed.rs @@ -0,0 +1,99 @@ +use didcomm::error::{ErrorKind, ResultExt}; +use js_sys::{Array, Promise}; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::future_to_promise; + +use crate::{ + error::JsResult, DIDResolver, JsDIDResolver, JsSecretsResolver, Message, SecretsResolver, +}; + +#[wasm_bindgen(skip_typescript)] +impl Message { + #[wasm_bindgen(skip_typescript)] + pub fn pack_signed( + &self, + sign_by: String, + did_resolver: DIDResolver, + secrets_resolver: SecretsResolver, + ) -> Promise { + let msg = self.0.clone(); + let did_resolver = JsDIDResolver(did_resolver); + let secrets_resolver = JsSecretsResolver(secrets_resolver); + + future_to_promise(async move { + let (msg, metadata) = msg + .pack_signed(&sign_by, &did_resolver, &secrets_resolver) + .await + .as_js()?; + + let metadata = JsValue::from_serde(&metadata) + .kind( + ErrorKind::InvalidState, + "Unable serialize PackSignedMetadata", + ) + .as_js()?; + + let res = { + let res = Array::new_with_length(2); + res.set(0, msg.into()); + res.set(1, metadata); + res + }; + + Ok(res.into()) + }) + } +} + +#[wasm_bindgen(typescript_custom_section)] +const MESSAGE_PACK_ENCRYPTED_TS: &'static str = r#" +interface Message { + /** + * Produces `DIDComm Signed Message` + * https://identity.foundation/didcomm-messaging/spec/#didcomm-signed-message. + * + * Signed messages are not necessary to provide message integrity (tamper evidence), + * or to prove the sender to the recipient. Both of these guarantees automatically occur + * with the authenticated encryption in DIDComm encrypted messages. Signed messages are only + * necessary when the origin of plaintext must be provable to third parties, + * or when the sender can’t be proven to the recipient by authenticated encryption because + * the recipient is not known in advance (e.g., in a broadcast scenario). + * We therefore expect signed messages to be used in a few cases, but not as a matter of course. + * + * @param `sign_by` a DID or key ID the sender uses for signing + * @param `did_resolver` instance of `DIDResolver` to resolve DIDs. + * @param `secrets_resolver` instance of SecretsResolver` to resolve sender DID keys secrets + * + * @returns Tuple (signed_message, metadata) + * - `signed_message` a DIDComm signed message as JSON string + * - `metadata` additional metadata about this `encrypt` execution like used keys identifiers and algorithms. + * + * @throws DIDCommDIDNotResolved + * @throws DIDCommDIDUrlNotFound + * @throws DIDCommMalformed + * @throws DIDCommIoError + * @throws DIDCommInvalidState + * @throws DIDCommNoCompatibleCrypto + * @throws DIDCommUnsupported + * @throws DIDCommIllegalArgument + */ + pack_signed( + sign_by: string, + did_resolver: DIDResolver, + secrets_resolver: SecretsResolver, + ): Promise<[string, PackSignedMetadata]>; +} +"#; + +#[wasm_bindgen(typescript_custom_section)] +const PACK_SIGNED_METADATA_TS: &'static str = r#" +/** + * Additional metadata about this `pack` method execution like used key identifiers. + */ +type PackSignedMetadata = { + /** + * Identifier (DID URL) of sign key. + */ + sign_by_kid: String, +} +"#; diff --git a/affinidi-messaging-didcomm/wasm/src/message/protocols/mod.rs b/affinidi-messaging-didcomm/wasm/src/message/protocols/mod.rs new file mode 100644 index 0000000..e78cad0 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/src/message/protocols/mod.rs @@ -0,0 +1 @@ +pub mod routing; diff --git a/affinidi-messaging-didcomm/wasm/src/message/protocols/routing/mod.rs b/affinidi-messaging-didcomm/wasm/src/message/protocols/routing/mod.rs new file mode 100644 index 0000000..f7823a7 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/src/message/protocols/routing/mod.rs @@ -0,0 +1,155 @@ +use std::{collections::HashMap, rc::Rc}; + +use crate::{did::JsDIDResolver, error::JsResult, DIDResolver}; +use crate::{utils::set_panic_hook, Message}; +use didcomm::{ + algorithms::AnonCryptAlg, + error::{ErrorKind, ResultExt}, + protocols::routing::{try_parse_forward, wrap_in_forward}, +}; +use js_sys::Promise; +use serde_json::Value; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::future_to_promise; + +#[wasm_bindgen] +pub struct ParsedForward(pub(crate) Rc>); + +#[wasm_bindgen] +impl ParsedForward { + #[wasm_bindgen(skip_typescript)] + pub fn as_value(&self) -> Result { + let msg = JsValue::from_serde(&*self.0) + .kind(ErrorKind::Malformed, "Unable serialize ParsedForward") + .as_js()?; + + Ok(msg) + } +} + +#[wasm_bindgen] +impl Message { + #[wasm_bindgen(skip_typescript)] + pub fn wrap_in_forward( + msg: String, + headers: JsValue, + to: String, + routing_keys: JsValue, + enc_alg_anon: JsValue, + did_resolver: DIDResolver, + ) -> Promise { + // TODO: Better place? + set_panic_hook(); + + let did_resolver = JsDIDResolver(did_resolver); + future_to_promise(async move { + let headers: HashMap = headers + .into_serde() + .kind(ErrorKind::Malformed, "headers param is malformed") + .as_js()?; + + let routing_keys: Vec = routing_keys + .into_serde() + .kind(ErrorKind::Malformed, "routing_keys param is malformed") + .as_js()?; + + let enc_alg_anon: AnonCryptAlg = enc_alg_anon + .into_serde() + .kind(ErrorKind::Malformed, "enc_alg_anon param is malformed") + .as_js()?; + + let res = wrap_in_forward( + &msg, + Some(&headers), + &to, + &routing_keys, + &enc_alg_anon, + &did_resolver, + ) + .await + .as_js()?; + + Ok(res.into()) + }) + } + + #[wasm_bindgen(skip_typescript)] + pub fn try_parse_forward(&self) -> Result { + let msg = self.0.clone(); + let parsed_message = try_parse_forward(&msg); + Ok(JsValue::from_serde(&parsed_message) + .kind( + ErrorKind::Malformed, + "Unable serialize parsed forward message", + ) + .as_js()?) + } +} + +#[wasm_bindgen(typescript_custom_section)] +const PARSED_FORWARD_AS_VALUE_TS: &'static str = r#" +interface ParsedForward { + as_value(): IParsedForward; +} +"#; + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(typescript_type = "IParsedForward")] + pub type IParsedForward; +} + +#[wasm_bindgen(typescript_custom_section)] +const IPARSED_FORWARD_TS: &'static str = r#" +type IParsedForward = { + msg: Message, + next: string, + forwarded_msg: any +} +"#; + +#[wasm_bindgen(typescript_custom_section)] +const MESSAGE_WRAP_IN_FORWARD_TS: &'static str = r#" +export namespace Message { + /** + * Resolves recipient DID DOC Service and Builds Forward envelops if needed. + * + * Wraps the given packed DIDComm message in Forward messages for every routing key. + * + * @param msg the message to be wrapped in Forward messages + * @param headers optional headers for Forward message + * @param to recipient's DID (DID URL) + * @param routing_keys list of routing keys + * @param enc_alg_anon The encryption algorithm to be used for anonymous encryption (anon_crypt) + * @param did_resolver instance of `DIDResolver` to resolve DIDs. + * + * @returns a top-level packed Forward message as JSON string + * + * @throws DIDCommDIDNotResolved + * @throws DIDCommDIDUrlNotFound + * @throws DIDCommIoError + * @throws DIDCommInvalidState + * @throws DIDCommIllegalArgument + */ + function wrap_in_forward( + msg: string, + headers: Record, + to: string, + routing_keys: Array, + enc_alg_anon: string, + did_resolver: DIDResolver, + ): Promise; +} +"#; + +#[wasm_bindgen(typescript_custom_section)] +const MESSAGE_TRY_PARSE_FORWARD_TS: &'static str = r#" +interface Message { + /** + * Tries to parse the Message to a Forward message + * + * @returns a parsed message or null + */ + try_parse_forward(): ParsedForward; +} +"#; diff --git a/affinidi-messaging-didcomm/wasm/src/message/unpack.rs b/affinidi-messaging-didcomm/wasm/src/message/unpack.rs new file mode 100644 index 0000000..dcd5331 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/src/message/unpack.rs @@ -0,0 +1,200 @@ +use std::rc::Rc; + +use didcomm::{ + error::{ErrorKind, ResultExt}, + UnpackOptions, +}; +use js_sys::{Array, Promise}; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::future_to_promise; + +use crate::{ + error::JsResult, utils::set_panic_hook, DIDResolver, JsDIDResolver, JsSecretsResolver, Message, + SecretsResolver, +}; + +#[wasm_bindgen(skip_typescript)] +impl Message { + #[wasm_bindgen(skip_typescript)] + pub fn unpack( + msg: String, + did_resolver: DIDResolver, + secrets_resolver: SecretsResolver, + options: JsValue, + ) -> Promise { + // TODO: Better place? + set_panic_hook(); + + let did_resolver = JsDIDResolver(did_resolver); + let secrets_resolver = JsSecretsResolver(secrets_resolver); + + future_to_promise(async move { + let options: UnpackOptions = options + .into_serde() + .kind(ErrorKind::Malformed, "Options param is malformed") + .as_js()?; + + let (msg, metadata) = + didcomm::Message::unpack(&msg, &did_resolver, &secrets_resolver, &options) + .await + .as_js()?; + + let metadata = JsValue::from_serde(&metadata) + .kind(ErrorKind::InvalidState, "Unable serialize UnpackMetadata") + .as_js()?; + + let res = { + let res = Array::new_with_length(2); + res.set(0, Message(Rc::new(msg)).into()); + res.set(1, metadata); + res + }; + + Ok(res.into()) + }) + } +} + +#[wasm_bindgen(typescript_custom_section)] +const MESSAGE_UNPACK_TS: &'static str = r#" +export namespace Message { + /** + * Unpacks the packed message by doing decryption and verifying the signatures. + * This method supports all DID Comm message types (encrypted, signed, plaintext). + * + * If unpack options expect a particular property (for example that a message is encrypted) + * and the packed message doesn't meet the criteria (it's not encrypted), then a MessageUntrusted + * error will be returned. + * + * @param `packed_msg` the message as JSON string to be unpacked + * @param `did_resolver` instance of `DIDResolver` to resolve DIDs + * @param `secrets_resolver` instance of SecretsResolver` to resolve sender DID keys secrets + * @param `options` allow fine configuration of unpacking process and imposing additional restrictions + * to message to be trusted. + * + * @returns Tuple `[message, metadata]`. + * - `message` plain message instance + * - `metadata` additional metadata about this `unpack` execution like used keys identifiers, + * trust context, algorithms and etc. + * + * @throws DIDCommDIDNotResolved + * @throws DIDCommDIDUrlNotFound + * @throws DIDCommMalformed + * @throws DIDCommIoError + * @throws DIDCommInvalidState + * @throws DIDCommNoCompatibleCrypto + * @throws DIDCommUnsupported + * @throws DIDCommIllegalArgument + */ + function unpack( + msg: string, + did_resolver: DIDResolver, + secrets_resolver: SecretsResolver, + options: UnpackOptions, + ): Promise<[Message, UnpackMetadata]>; +} +"#; + +#[wasm_bindgen(typescript_custom_section)] +const PACK_UNPACK_OPTIONS_TS: &'static str = r#" +/** + * Allows fine customization of unpacking process + */ +type UnpackOptions = { + /** + * Whether the plaintext must be decryptable by all keys resolved by the secrets resolver. + * False by default. + */ + expect_decrypt_by_all_keys?: boolean, + + /** + * If `true` and the packed message is a `Forward` + * wrapping a plaintext packed for the given recipient, then both Forward and packed plaintext are unpacked automatically, + * and the unpacked plaintext will be returned instead of unpacked Forward. + * False by default. + */ + unwrap_re_wrapping_forward?: boolean, +} +"#; + +#[wasm_bindgen(typescript_custom_section)] +const UNPACK_METADATA_TS: &'static str = r#" +/** + * Additional metadata about this `unpack` method execution like trust predicates + * and used keys identifiers. + */ +type UnpackMetadata = { + /** + * Whether the plaintext has been encrypted. + */ + encrypted: boolean, + + /** + * Whether the plaintext has been authenticated. + */ + authenticated: boolean, + + /** + * Whether the plaintext has been signed. + */ + non_repudiation: boolean, + + /** + * Whether the sender ID was hidden or protected. + */ + anonymous_sender: boolean, + + /** + * Whether the plaintext was re-wrapped in a forward message by a mediator. + */ + re_wrapped_in_forward: boolean, + + /** + * Key ID of the sender used for authentication encryption + * if the plaintext has been authenticated and encrypted. + */ + encrypted_from_kid?: string, + + /** + * Target key IDS for encryption if the plaintext has been encrypted. + */ + encrypted_to_kids?: Array, + + /** + * Key ID used for signature if the plaintext has been signed. + */ + sign_from: string, + + /** + * Key ID used for from_prior header signature if from_prior header is present + */ + from_prior_issuer_kid?: string, + + /** + * Algorithm used for authenticated encryption. + * Default "A256cbcHs512Ecdh1puA256kw" + */ + enc_alg_auth?: "A256cbcHs512Ecdh1puA256kw", + + /** + * Algorithm used for anonymous encryption. + * Default "Xc20pEcdhEsA256kw" + */ + enc_alg_anon?: "A256cbcHs512EcdhEsA256kw" | "Xc20pEcdhEsA256kw" | "A256gcmEcdhEsA256kw", + + /** + * Algorithm used for message signing. + */ + sign_alg?: "EdDSA" | "ES256" | "ES256K", + + /** + * If the plaintext has been signed, the JWS is returned for non-repudiation purposes. + */ + signed_message?: string, + + /** + * If plaintext contains from_prior header, its unpacked value is returned + */ + from_prior?: IFromPrior, +} +"#; diff --git a/affinidi-messaging-didcomm/wasm/src/secrets/mod.rs b/affinidi-messaging-didcomm/wasm/src/secrets/mod.rs new file mode 100644 index 0000000..fca5d2c --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/src/secrets/mod.rs @@ -0,0 +1,5 @@ +mod secret; +mod secrets_resolver; + +pub(crate) use secrets_resolver::JsSecretsResolver; +pub use secrets_resolver::SecretsResolver; diff --git a/affinidi-messaging-didcomm/wasm/src/secrets/secret.rs b/affinidi-messaging-didcomm/wasm/src/secrets/secret.rs new file mode 100644 index 0000000..f0053d7 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/src/secrets/secret.rs @@ -0,0 +1,36 @@ +use wasm_bindgen::prelude::*; + +#[wasm_bindgen(typescript_custom_section)] +const SECRET_TS: &'static str = r#" +/** + * Represents secret. + */ +type Secret = { + /** + * A key ID identifying a secret (private key). + */ + id: string, + + /** + * Must have the same semantics as type ('type' field) of the corresponding method in DID Doc containing a public key. + */ + type: SecretType, + + /** + * Possible value of the secret (private key) + */ + privateKeyJwk?: any, + privateKeyMultibase?: string, + privateKeyBase58?: string, +} +"#; + +#[wasm_bindgen(typescript_custom_section)] +const SECRET_TYPE_TS: &'static str = r#" +/** + * Must have the same semantics as type ('type' field) of the corresponding method in DID Doc containing a public key. + */ +type SecretType = + "JsonWebKey2020" | "X25519KeyAgreementKey2019" + | "Ed25519VerificationKey2018" | "EcdsaSecp256k1VerificationKey2019" | string +"#; diff --git a/affinidi-messaging-didcomm/wasm/src/secrets/secrets_resolver.rs b/affinidi-messaging-didcomm/wasm/src/secrets/secrets_resolver.rs new file mode 100644 index 0000000..f4b1be2 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/src/secrets/secrets_resolver.rs @@ -0,0 +1,132 @@ +use async_trait::async_trait; +use didcomm::{ + error::{err_msg, ErrorKind, Result as _Result, ResultContext, ResultExt}, + secrets::{Secret, SecretsResolver as _SecretsResolver}, +}; +use js_sys::Array; +use wasm_bindgen::{prelude::*, JsCast}; + +use crate::error::FromJsResult; + +#[wasm_bindgen] +extern "C" { + pub type SecretsResolver; + + // Promise resolves to JsValue(object) that can be deserialized to Secret + #[wasm_bindgen(structural, method, catch)] + pub async fn get_secret(this: &SecretsResolver, secret_id: &str) -> Result; + + // Promise resolves to JsValue(object) that can be casted to Array + #[wasm_bindgen(structural, method, catch)] + pub async fn find_secrets( + this: &SecretsResolver, + secret_ids: Array, + ) -> Result; +} + +#[wasm_bindgen(typescript_custom_section)] +const SECRET_RESOLVER_TS: &'static str = r#" +/** + * Interface for secrets resolver. + * Resolves secrets such as private keys to be used for signing and encryption. + */ +interface SecretsResolver { + /** + * Finds secret (usually private key) identified by the given key ID. + * + * @param `secret_id` the ID (in form of DID URL) identifying a secret + * + * @returns A secret (usually private key) or None of there is no secret for the given ID + * + * @throws DIDCommIoError - IO error in resolving process + * @throws DIDCommInvalidState - Code error or unexpected state was detected + * + * ``` + * let e = Error("Unble perform io operation"); + * e.name = "DIDCommIoError" + * throw e + * ``` + */ + get_secret(secret_id: string): Promise; + + /** + * Find all secrets that have one of the given IDs. + * Return secrets only for key IDs for which a secret is present. + * + * @param `secret_ids` the IDs find secrets for + * + * @returns possible empty list of all secrets that have one of the given IDs. + * + * @throws DIDCommIoError - IO error in resolving process + * @throws DIDCommInvalidState - Code error or unexpected state was detected + * + * Note to throw compatible error use code like this + * + * ``` + * let e = Error("Unble perform io operation"); + * e.name = "DIDCommIoError" + * throw e + * ``` + */ + find_secrets(secret_ids: Array): Promise>; +} +"#; + +// TODO: think is it possible to avoid ownership on DIDResolver +pub(crate) struct JsSecretsResolver(pub(crate) SecretsResolver); + +#[async_trait(?Send)] +impl _SecretsResolver for JsSecretsResolver { + async fn get_secret(&self, secret_id: &str) -> _Result> { + // TODO: better error conversion + let secret = self + .0 + .get_secret(secret_id) + .await + .from_js() + .context("Unable get secret")?; + + let secret: Option = secret.into_serde().kind( + ErrorKind::InvalidState, + "Unable deserialize Secret from JsValue", + )?; + + Ok(secret) + } + + async fn find_secrets<'a>(&self, secret_ids: &'a [&'a str]) -> _Result> { + let _secret_ids = secret_ids + .into_iter() + .map(|s| JsValue::from_str(s)) + .collect::(); + + // TODO: better error conversion + let found = self + .0 + .find_secrets(_secret_ids) + .await + .from_js() + .context("Unable find secrets")?; + + let found: Vec<_> = found + .dyn_into::() + .map_err(|_| { + err_msg( + ErrorKind::InvalidState, + "Unable covert secret ids JsValue to Array", + ) + })? + .iter() + .map(|v| v.as_string()) + .flatten() + .collect(); + + let found: Vec<_> = secret_ids + .iter() + .filter(|&s| found.iter().find(|_s| _s == s).is_some()) + .map(|&s| s) + .collect(); + + Ok(found) + } +} diff --git a/affinidi-messaging-didcomm/wasm/src/utils.rs b/affinidi-messaging-didcomm/wasm/src/utils.rs new file mode 100644 index 0000000..b1d7929 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/src/utils.rs @@ -0,0 +1,10 @@ +pub fn set_panic_hook() { + // When the `console_error_panic_hook` feature is enabled, we can call the + // `set_panic_hook` function at least once during initialization, and then + // we will get better error messages if our code ever panics. + // + // For more details see + // https://github.com/rustwasm/console_error_panic_hook#readme + #[cfg(feature = "console_error_panic_hook")] + console_error_panic_hook::set_once(); +} diff --git a/affinidi-messaging-didcomm/wasm/tests-js/.gitignore b/affinidi-messaging-didcomm/wasm/tests-js/.gitignore new file mode 100644 index 0000000..07e6e47 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/.gitignore @@ -0,0 +1 @@ +/node_modules diff --git a/affinidi-messaging-didcomm/wasm/tests-js/.prettierrc.json b/affinidi-messaging-didcomm/wasm/tests-js/.prettierrc.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/.prettierrc.json @@ -0,0 +1 @@ +{} diff --git a/affinidi-messaging-didcomm/wasm/tests-js/jest-puppeteer.config.js b/affinidi-messaging-didcomm/wasm/tests-js/jest-puppeteer.config.js new file mode 100644 index 0000000..23cf1a6 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/jest-puppeteer.config.js @@ -0,0 +1,8 @@ +module.exports = { + launch: { + dumpio: true, + headless: true, + args: ["--disable-infobars"], + }, + browserContext: "default", +}; diff --git a/affinidi-messaging-didcomm/wasm/tests-js/jest.config.js b/affinidi-messaging-didcomm/wasm/tests-js/jest.config.js new file mode 100644 index 0000000..06a3def --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/jest.config.js @@ -0,0 +1,7 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +module.exports = { + testEnvironment: "node", + transform: { + "^.+\\.ts?$": "ts-jest", + }, +}; diff --git a/affinidi-messaging-didcomm/wasm/tests-js/jest.config.puppeteer.js b/affinidi-messaging-didcomm/wasm/tests-js/jest.config.puppeteer.js new file mode 100644 index 0000000..062411d --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/jest.config.puppeteer.js @@ -0,0 +1,7 @@ +/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */ +module.exports = { + preset: "jest-puppeteer", + transform: { + "^.+\\.ts?$": "ts-jest", + }, +}; diff --git a/affinidi-messaging-didcomm/wasm/tests-js/package-lock.json b/affinidi-messaging-didcomm/wasm/tests-js/package-lock.json new file mode 100644 index 0000000..b7e4437 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/package-lock.json @@ -0,0 +1,9097 @@ +{ + "name": "didcomm-tests", + "version": "0.3.4", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "didcomm-tests", + "version": "0.3.4", + "license": "Apache-2.0", + "dependencies": { + "didcomm": "file:../pkg", + "typescript": "^4.5.2" + }, + "devDependencies": { + "@types/jest": "^27.0.2", + "@types/jest-environment-puppeteer": "^4.4.1", + "@types/puppeteer": "^5.4.4", + "jest": "^27.3.1", + "jest-puppeteer": "^6.0.0", + "prettier": "2.4.1", + "puppeteer": "^19.6.3", + "ts-jest": "^27.0.7", + "ts-node": "^10.4.0", + "tslint": "^6.1.3" + } + }, + "../pkg": { + "name": "didcomm", + "version": "0.0.1" + }, + "node_modules/@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.16.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.16.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.16.0", + "@babel/generator": "^7.16.0", + "@babel/helper-compilation-targets": "^7.16.0", + "@babel/helper-module-transforms": "^7.16.0", + "@babel/helpers": "^7.16.0", + "@babel/parser": "^7.16.0", + "@babel/template": "^7.16.0", + "@babel/traverse": "^7.16.0", + "@babel/types": "^7.16.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0", + "source-map": "^0.5.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/source-map": { + "version": "0.5.7", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.16.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.16.0", + "@babel/helper-validator-option": "^7.14.5", + "browserslist": "^4.17.5", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.16.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.16.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.16.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.16.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.16.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.0", + "@babel/helper-replace-supers": "^7.16.0", + "@babel/helper-simple-access": "^7.16.0", + "@babel/helper-split-export-declaration": "^7.16.0", + "@babel/helper-validator-identifier": "^7.15.7", + "@babel/template": "^7.16.0", + "@babel/traverse": "^7.16.0", + "@babel/types": "^7.16.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.16.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.16.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.16.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.16.0", + "@babel/helper-optimise-call-expression": "^7.16.0", + "@babel/traverse": "^7.16.0", + "@babel/types": "^7.16.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.16.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.16.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.16.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.16.0", + "@babel/traverse": "^7.16.3", + "@babel/types": "^7.16.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.16.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@cspotcode/source-map-consumer": { + "version": "0.8.0", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-consumer": "0.8.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@hapi/hoek": { + "version": "9.2.1", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@hapi/topo": { + "version": "5.1.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.2.5", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^27.3.1", + "jest-util": "^27.3.1", + "slash": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/core": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^27.3.1", + "@jest/reporters": "^27.3.1", + "@jest/test-result": "^27.3.1", + "@jest/transform": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-changed-files": "^27.3.0", + "jest-config": "^27.3.1", + "jest-haste-map": "^27.3.1", + "jest-message-util": "^27.3.1", + "jest-regex-util": "^27.0.6", + "jest-resolve": "^27.3.1", + "jest-resolve-dependencies": "^27.3.1", + "jest-runner": "^27.3.1", + "jest-runtime": "^27.3.1", + "jest-snapshot": "^27.3.1", + "jest-util": "^27.3.1", + "jest-validate": "^27.3.1", + "jest-watcher": "^27.3.1", + "micromatch": "^4.0.4", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/environment": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/node": "*", + "jest-mock": "^27.3.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.2.5", + "@sinonjs/fake-timers": "^8.0.1", + "@types/node": "*", + "jest-message-util": "^27.3.1", + "jest-mock": "^27.3.0", + "jest-util": "^27.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.3.1", + "@jest/types": "^27.2.5", + "expect": "^27.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^27.3.1", + "@jest/test-result": "^27.3.1", + "@jest/transform": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.4", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^4.0.3", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "jest-haste-map": "^27.3.1", + "jest-resolve": "^27.3.1", + "jest-util": "^27.3.1", + "jest-worker": "^27.3.1", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^4.0.1", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^8.1.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/source-map": { + "version": "27.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.4", + "source-map": "^0.6.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^27.3.1", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^27.3.1", + "jest-runtime": "^27.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.1.0", + "@jest/types": "^27.2.5", + "babel-plugin-istanbul": "^6.0.0", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^27.3.1", + "jest-regex-util": "^27.0.6", + "jest-util": "^27.3.1", + "micromatch": "^4.0.4", + "pirates": "^4.0.1", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jest/types": { + "version": "27.2.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sideway/address": { + "version": "4.1.2", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0" + } + }, + "node_modules/@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true + }, + "node_modules/@sideway/pinpoint": { + "version": "2.0.0", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@sinonjs/commons": { + "version": "1.8.3", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "8.1.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.9", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.1.16", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.14.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.3.0" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "27.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-diff": "^27.0.0", + "pretty-format": "^27.0.0" + } + }, + "node_modules/@types/jest-environment-puppeteer": { + "version": "4.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": ">=24 <=26", + "@types/puppeteer": "*", + "jest-environment-node": ">=24 <=26" + } + }, + "node_modules/@types/jest-environment-puppeteer/node_modules/@jest/environment": { + "version": "26.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/@types/jest-environment-puppeteer/node_modules/@jest/fake-timers": { + "version": "26.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^26.6.2", + "@sinonjs/fake-timers": "^6.0.1", + "@types/node": "*", + "jest-message-util": "^26.6.2", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/@types/jest-environment-puppeteer/node_modules/@jest/types": { + "version": "26.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/@types/jest-environment-puppeteer/node_modules/@sinonjs/fake-timers": { + "version": "6.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@types/jest-environment-puppeteer/node_modules/@types/yargs": { + "version": "15.0.14", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/jest-environment-puppeteer/node_modules/jest-environment-node": { + "version": "26.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/@types/jest-environment-puppeteer/node_modules/jest-message-util": { + "version": "26.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "@jest/types": "^26.6.2", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.2", + "pretty-format": "^26.6.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.2" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/@types/jest-environment-puppeteer/node_modules/jest-mock": { + "version": "26.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^26.6.2", + "@types/node": "*" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/@types/jest-environment-puppeteer/node_modules/jest-util": { + "version": "26.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">= 10.14.2" + } + }, + "node_modules/@types/jest-environment-puppeteer/node_modules/pretty-format": { + "version": "26.6.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@types/node": { + "version": "16.11.7", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/prettier": { + "version": "2.4.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/puppeteer": { + "version": "5.4.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "16.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "20.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/abab": { + "version": "2.0.5", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/acorn": { + "version": "8.5.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-globals": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" + } + }, + "node_modules/acorn-globals/node_modules/acorn": { + "version": "7.4.1", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/arr-union": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/axios": { + "version": "0.21.4", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/babel-jest": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.0.0", + "babel-preset-jest": "^27.2.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "slash": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.1.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "27.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.0.0", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "27.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^27.2.0", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-process-hrtime": { + "version": "1.0.0", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/browserslist": { + "version": "4.17.6", + "dev": true, + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001274", + "electron-to-chromium": "^1.3.886", + "escalade": "^3.1.1", + "node-releases": "^2.0.1", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001279", + "dev": true, + "license": "CC-BY-4.0", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "node_modules/ci-info": { + "version": "3.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.2", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "7.0.4", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clone-deep": { + "version": "0.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "for-own": "^0.1.3", + "is-plain-object": "^2.0.1", + "kind-of": "^3.0.2", + "lazy-cache": "^1.0.3", + "shallow-clone": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "1.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.1" + } + }, + "node_modules/cosmiconfig": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.0.0.tgz", + "integrity": "sha512-da1EafcpH6b/TD8vDRaWV7xFINlHlF6zKsGwS1TsuVJTZRkquaS5HTMq7uq6h31619QjbsYl21gVDOm32KM1vQ==", + "dev": true, + "dependencies": { + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dev": true, + "dependencies": { + "node-fetch": "2.6.7" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssom": { + "version": "0.4.4", + "dev": true, + "license": "MIT" + }, + "node_modules/cssstyle": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "dev": true, + "license": "MIT" + }, + "node_modules/cwd": { + "version": "0.10.0", + "dev": true, + "license": "MIT", + "dependencies": { + "find-pkg": "^0.1.2", + "fs-exists-sync": "^0.1.0" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/data-urls": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.3.1", + "dev": true, + "license": "MIT" + }, + "node_modules/dedent": { + "version": "0.7.0", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-is": { + "version": "0.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1082910", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1082910.tgz", + "integrity": "sha512-RqoZ2GmqaNxyx+99L/RemY5CkwG9D0WEfOKxekwCRXOGrDCep62ngezEJUVMq6rISYQ+085fJnWDQqGHlxVNww==", + "dev": true + }, + "node_modules/didcomm": { + "resolved": "../pkg", + "link": true + }, + "node_modules/diff": { + "version": "4.0.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "27.0.6", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/domexception": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "webidl-conversions": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/domexception/node_modules/webidl-conversions": { + "version": "5.0.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.3.894", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.8.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/escodegen": { + "version": "2.0.0", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-tilde": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "os-homedir": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.2.5", + "ansi-styles": "^5.0.0", + "jest-get-type": "^27.3.1", + "jest-matcher-utils": "^27.3.1", + "jest-message-util": "^27.3.1", + "jest-regex-util": "^27.0.6" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/expect-puppeteer": { + "version": "6.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/expect/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.1", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-file-up": { + "version": "0.1.3", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-exists-sync": "^0.1.0", + "resolve-dir": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-pkg": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "find-file-up": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-process": { + "version": "1.4.5", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "commander": "^5.1.0", + "debug": "^4.1.1" + }, + "bin": { + "find-process": "bin/find-process.js" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "0.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/form-data": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "node_modules/fs-exists-sync": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.0", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-modules": { + "version": "0.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^0.1.4", + "is-windows": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "0.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.0", + "ini": "^1.3.4", + "is-windows": "^0.2.0", + "which": "^1.2.12" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.8", + "dev": true, + "license": "ISC" + }, + "node_modules/has": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^1.0.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/import-local": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "1.3.8", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "dev": true, + "license": "MIT" + }, + "node_modules/is-ci": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ci-info": "^2.0.0" + }, + "bin": { + "is-ci": "bin.js" + } + }, + "node_modules/is-ci/node_modules/ci-info": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.8.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/is-windows": { + "version": "0.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "4.0.3", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.0.5", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^27.3.1", + "import-local": "^3.0.2", + "jest-cli": "^27.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "27.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.2.5", + "execa": "^5.0.0", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-circus": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.3.1", + "@jest/test-result": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "expect": "^27.3.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.3.1", + "jest-matcher-utils": "^27.3.1", + "jest-message-util": "^27.3.1", + "jest-runtime": "^27.3.1", + "jest-snapshot": "^27.3.1", + "jest-util": "^27.3.1", + "pretty-format": "^27.3.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-cli": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^27.3.1", + "@jest/test-result": "^27.3.1", + "@jest/types": "^27.2.5", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "import-local": "^3.0.2", + "jest-config": "^27.3.1", + "jest-util": "^27.3.1", + "jest-validate": "^27.3.1", + "prompts": "^2.0.1", + "yargs": "^16.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^27.3.1", + "@jest/types": "^27.2.5", + "babel-jest": "^27.3.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.1", + "graceful-fs": "^4.2.4", + "jest-circus": "^27.3.1", + "jest-environment-jsdom": "^27.3.1", + "jest-environment-node": "^27.3.1", + "jest-get-type": "^27.3.1", + "jest-jasmine2": "^27.3.1", + "jest-regex-util": "^27.0.6", + "jest-resolve": "^27.3.1", + "jest-runner": "^27.3.1", + "jest-util": "^27.3.1", + "jest-validate": "^27.3.1", + "micromatch": "^4.0.4", + "pretty-format": "^27.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-dev-server": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "cwd": "^0.10.0", + "find-process": "^1.4.5", + "prompts": "^2.4.1", + "spawnd": "^6.0.0", + "tree-kill": "^1.2.2", + "wait-on": "^6.0.0" + } + }, + "node_modules/jest-diff": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^27.0.6", + "jest-get-type": "^27.3.1", + "pretty-format": "^27.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "27.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-each": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.2.5", + "chalk": "^4.0.0", + "jest-get-type": "^27.3.1", + "jest-util": "^27.3.1", + "pretty-format": "^27.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.3.1", + "@jest/fake-timers": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/node": "*", + "jest-mock": "^27.3.0", + "jest-util": "^27.3.1", + "jsdom": "^16.6.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.3.1", + "@jest/fake-timers": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/node": "*", + "jest-mock": "^27.3.0", + "jest-util": "^27.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-environment-puppeteer": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.1", + "cwd": "^0.10.0", + "jest-dev-server": "^6.0.0", + "jest-environment-node": "^27.0.1", + "merge-deep": "^3.0.3" + } + }, + "node_modules/jest-get-type": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.2.5", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.4", + "jest-regex-util": "^27.0.6", + "jest-serializer": "^27.0.6", + "jest-util": "^27.3.1", + "jest-worker": "^27.3.1", + "micromatch": "^4.0.4", + "walker": "^1.0.7" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-jasmine2": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^27.3.1", + "@jest/source-map": "^27.0.6", + "@jest/test-result": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "expect": "^27.3.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.3.1", + "jest-matcher-utils": "^27.3.1", + "jest-message-util": "^27.3.1", + "jest-runtime": "^27.3.1", + "jest-snapshot": "^27.3.1", + "jest-util": "^27.3.1", + "pretty-format": "^27.3.1", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-leak-detector": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^27.3.1", + "pretty-format": "^27.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^27.3.1", + "jest-get-type": "^27.3.1", + "pretty-format": "^27.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.2.5", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.4", + "pretty-format": "^27.3.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-mock": { + "version": "27.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.2.5", + "@types/node": "*" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-puppeteer": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "expect-puppeteer": "^6.0.0", + "jest-environment-puppeteer": "^6.0.0" + }, + "peerDependencies": { + "puppeteer": ">= 1.5.0" + } + }, + "node_modules/jest-regex-util": { + "version": "27.0.6", + "dev": true, + "license": "MIT", + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.2.5", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^27.3.1", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^27.3.1", + "jest-validate": "^27.3.1", + "resolve": "^1.20.0", + "resolve.exports": "^1.1.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.2.5", + "jest-regex-util": "^27.0.6", + "jest-snapshot": "^27.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-runner": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^27.3.1", + "@jest/environment": "^27.3.1", + "@jest/test-result": "^27.3.1", + "@jest/transform": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-docblock": "^27.0.6", + "jest-environment-jsdom": "^27.3.1", + "jest-environment-node": "^27.3.1", + "jest-haste-map": "^27.3.1", + "jest-leak-detector": "^27.3.1", + "jest-message-util": "^27.3.1", + "jest-resolve": "^27.3.1", + "jest-runtime": "^27.3.1", + "jest-util": "^27.3.1", + "jest-worker": "^27.3.1", + "source-map-support": "^0.5.6", + "throat": "^6.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^27.3.1", + "@jest/environment": "^27.3.1", + "@jest/globals": "^27.3.1", + "@jest/source-map": "^27.0.6", + "@jest/test-result": "^27.3.1", + "@jest/transform": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "execa": "^5.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^27.3.1", + "jest-message-util": "^27.3.1", + "jest-mock": "^27.3.0", + "jest-regex-util": "^27.0.6", + "jest-resolve": "^27.3.1", + "jest-snapshot": "^27.3.1", + "jest-util": "^27.3.1", + "jest-validate": "^27.3.1", + "slash": "^3.0.0", + "strip-bom": "^4.0.0", + "yargs": "^16.2.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-serializer": { + "version": "27.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "graceful-fs": "^4.2.4" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.7.2", + "@babel/generator": "^7.7.2", + "@babel/parser": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.0.0", + "@jest/transform": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/babel__traverse": "^7.0.4", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^27.3.1", + "graceful-fs": "^4.2.4", + "jest-diff": "^27.3.1", + "jest-get-type": "^27.3.1", + "jest-haste-map": "^27.3.1", + "jest-matcher-utils": "^27.3.1", + "jest-message-util": "^27.3.1", + "jest-resolve": "^27.3.1", + "jest-util": "^27.3.1", + "natural-compare": "^1.4.0", + "pretty-format": "^27.3.1", + "semver": "^7.3.2" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.3.5", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.2.5", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.4", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-validate": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.2.5", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^27.3.1", + "leven": "^3.1.0", + "pretty-format": "^27.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "jest-util": "^27.3.1", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-worker": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/joi": { + "version": "17.4.2", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.0", + "@sideway/formula": "^3.0.0", + "@sideway/pinpoint": "^2.0.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdom": { + "version": "16.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "abab": "^2.0.5", + "acorn": "^8.2.4", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.3.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "form-data": "^3.0.0", + "html-encoding-sniffer": "^2.0.1", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.5.0", + "ws": "^7.4.6", + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kind-of": { + "version": "3.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lazy-cache": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-deep": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "arr-union": "^3.1.0", + "clone-deep": "^0.2.4", + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.51.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.34", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.51.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mixin-object": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "for-in": "^0.1.3", + "is-extendable": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-object/node_modules/for-in": { + "version": "0.1.8", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "dependencies": { + "minimist": "^1.2.5" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, + "node_modules/ms": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "dev": true, + "license": "MIT" + }, + "node_modules/node-modules-regexp": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/node-releases": { + "version": "2.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nwsapi": { + "version": "2.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.8.3", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/parse5": { + "version": "6.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "node-modules-regexp": "^1.0.0" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.1.2", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "2.4.1", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/pretty-format": { + "version": "27.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^27.2.5", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "dev": true, + "license": "MIT" + }, + "node_modules/psl": { + "version": "1.8.0", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/puppeteer": { + "version": "19.6.3", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-19.6.3.tgz", + "integrity": "sha512-K03xTtGDwS6cBXX/EoqoZxglCUKcX2SLIl92fMnGMRjYpPGXoAV2yKEh3QXmXzKqfZXd8TxjjFww+tEttWv8kw==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "cosmiconfig": "8.0.0", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "puppeteer-core": "19.6.3" + }, + "engines": { + "node": ">=14.1.0" + } + }, + "node_modules/puppeteer-core": { + "version": "19.6.3", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-19.6.3.tgz", + "integrity": "sha512-8MbhioSlkDaHkmolpQf9Z7ui7jplFfOFTnN8d5kPsCazRRTNIH6/bVxPskn0v5Gh9oqOBlknw0eHH0/OBQAxpQ==", + "dev": true, + "dependencies": { + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.1082910", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "proxy-from-env": "1.1.0", + "rimraf": "3.0.2", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.11.0" + }, + "engines": { + "node": ">=14.1.0" + } + }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "node_modules/react-is": { + "version": "17.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.20.0", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-dir": { + "version": "0.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^1.2.2", + "global-modules": "^0.2.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rxjs": { + "version": "7.4.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "~2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "5.0.1", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.0", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shallow-clone": { + "version": "0.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.1", + "kind-of": "^2.0.1", + "lazy-cache": "^0.2.3", + "mixin-object": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone/node_modules/kind-of": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone/node_modules/lazy-cache": { + "version": "0.2.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.5", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.20", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/spawnd": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "exit": "^0.1.2", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/string-length": { + "version": "4.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/terminal-link": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/throat": { + "version": "6.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/tmpl": { + "version": "1.0.5", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-jest": { + "version": "27.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^27.0.0", + "json5": "2.x", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "7.x", + "yargs-parser": "20.x" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@types/jest": "^27.0.0", + "babel-jest": ">=27.0.0 <28", + "jest": "^27.0.0", + "typescript": ">=3.8 <5.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@types/jest": { + "optional": true + }, + "babel-jest": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.3.5", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-node": { + "version": "10.4.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.7.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/ts-node/node_modules/acorn-walk": { + "version": "8.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/tslib": { + "version": "2.1.0", + "dev": true, + "license": "0BSD" + }, + "node_modules/tslint": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz", + "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", + "deprecated": "TSLint has been deprecated in favor of ESLint. Please see https://github.com/palantir/tslint/issues/4534 for more information.", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.3", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.13.0", + "tsutils": "^2.29.0" + }, + "bin": { + "tslint": "bin/tslint" + }, + "engines": { + "node": ">=4.8.0" + }, + "peerDependencies": { + "typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 4.0.0-dev" + } + }, + "node_modules/tslint/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tslint/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tslint/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/tslint/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "node_modules/tslint/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/tslint/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/tslint/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/tslint/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/tslint/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/tslint/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "peerDependencies": { + "typescript": ">=2.1.0 || >=2.1.0-dev || >=2.2.0-dev || >=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >= 3.0.0-dev || >= 3.1.0-dev" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typedarray-to-buffer": { + "version": "3.1.5", + "dev": true, + "license": "MIT", + "dependencies": { + "is-typedarray": "^1.0.0" + } + }, + "node_modules/typescript": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", + "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/v8-to-istanbul": { + "version": "8.1.0", + "dev": true, + "license": "ISC", + "dependencies": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/source-map": { + "version": "0.7.3", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/w3c-hr-time": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "browser-process-hrtime": "^1.0.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/wait-on": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^0.21.1", + "joi": "^17.4.0", + "lodash": "^4.17.21", + "minimist": "^1.2.5", + "rxjs": "^7.1.0" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/webidl-conversions": { + "version": "6.1.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=10.4" + } + }, + "node_modules/whatwg-encoding": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.4.24" + } + }, + "node_modules/whatwg-mimetype": { + "version": "2.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "8.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "3.0.3", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "node_modules/ws": { + "version": "7.5.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "3.0.0", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "16.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + }, + "dependencies": { + "@babel/code-frame": { + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", + "dev": true, + "requires": { + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/compat-data": { + "version": "7.16.0", + "dev": true + }, + "@babel/core": { + "version": "7.16.0", + "dev": true, + "requires": { + "@babel/code-frame": "^7.16.0", + "@babel/generator": "^7.16.0", + "@babel/helper-compilation-targets": "^7.16.0", + "@babel/helper-module-transforms": "^7.16.0", + "@babel/helpers": "^7.16.0", + "@babel/parser": "^7.16.0", + "@babel/template": "^7.16.0", + "@babel/traverse": "^7.16.0", + "@babel/types": "^7.16.0", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.1.2", + "semver": "^6.3.0", + "source-map": "^0.5.0" + }, + "dependencies": { + "source-map": { + "version": "0.5.7", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", + "dev": true, + "requires": { + "@babel/types": "^7.23.0", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.16.3", + "dev": true, + "requires": { + "@babel/compat-data": "^7.16.0", + "@babel/helper-validator-option": "^7.14.5", + "browserslist": "^4.17.5", + "semver": "^6.3.0" + } + }, + "@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "requires": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.16.0", + "dev": true, + "requires": { + "@babel/types": "^7.16.0" + } + }, + "@babel/helper-module-imports": { + "version": "7.16.0", + "dev": true, + "requires": { + "@babel/types": "^7.16.0" + } + }, + "@babel/helper-module-transforms": { + "version": "7.16.0", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.16.0", + "@babel/helper-replace-supers": "^7.16.0", + "@babel/helper-simple-access": "^7.16.0", + "@babel/helper-split-export-declaration": "^7.16.0", + "@babel/helper-validator-identifier": "^7.15.7", + "@babel/template": "^7.16.0", + "@babel/traverse": "^7.16.0", + "@babel/types": "^7.16.0" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.16.0", + "dev": true, + "requires": { + "@babel/types": "^7.16.0" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.14.5", + "dev": true + }, + "@babel/helper-replace-supers": { + "version": "7.16.0", + "dev": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.16.0", + "@babel/helper-optimise-call-expression": "^7.16.0", + "@babel/traverse": "^7.16.0", + "@babel/types": "^7.16.0" + } + }, + "@babel/helper-simple-access": { + "version": "7.16.0", + "dev": true, + "requires": { + "@babel/types": "^7.16.0" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", + "dev": true, + "requires": { + "@babel/types": "^7.22.5" + } + }, + "@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.14.5", + "dev": true + }, + "@babel/helpers": { + "version": "7.16.3", + "dev": true, + "requires": { + "@babel/template": "^7.16.0", + "@babel/traverse": "^7.16.3", + "@babel/types": "^7.16.0" + } + }, + "@babel/highlight": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", + "dev": true + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.16.0", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/template": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" + } + }, + "@babel/traverse": { + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", + "debug": "^4.1.0", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "dev": true + }, + "@cspotcode/source-map-consumer": { + "version": "0.8.0", + "dev": true + }, + "@cspotcode/source-map-support": { + "version": "0.7.0", + "dev": true, + "requires": { + "@cspotcode/source-map-consumer": "0.8.0" + } + }, + "@hapi/hoek": { + "version": "9.2.1", + "dev": true + }, + "@hapi/topo": { + "version": "5.1.0", + "dev": true, + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "dev": true + }, + "@jest/console": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/types": "^27.2.5", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^27.3.1", + "jest-util": "^27.3.1", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/console": "^27.3.1", + "@jest/reporters": "^27.3.1", + "@jest/test-result": "^27.3.1", + "@jest/transform": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-changed-files": "^27.3.0", + "jest-config": "^27.3.1", + "jest-haste-map": "^27.3.1", + "jest-message-util": "^27.3.1", + "jest-regex-util": "^27.0.6", + "jest-resolve": "^27.3.1", + "jest-resolve-dependencies": "^27.3.1", + "jest-runner": "^27.3.1", + "jest-runtime": "^27.3.1", + "jest-snapshot": "^27.3.1", + "jest-util": "^27.3.1", + "jest-validate": "^27.3.1", + "jest-watcher": "^27.3.1", + "micromatch": "^4.0.4", + "rimraf": "^3.0.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "@jest/environment": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/fake-timers": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/node": "*", + "jest-mock": "^27.3.0" + } + }, + "@jest/fake-timers": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/types": "^27.2.5", + "@sinonjs/fake-timers": "^8.0.1", + "@types/node": "*", + "jest-message-util": "^27.3.1", + "jest-mock": "^27.3.0", + "jest-util": "^27.3.1" + } + }, + "@jest/globals": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/environment": "^27.3.1", + "@jest/types": "^27.2.5", + "expect": "^27.3.1" + } + }, + "@jest/reporters": { + "version": "27.3.1", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^27.3.1", + "@jest/test-result": "^27.3.1", + "@jest/transform": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.2", + "graceful-fs": "^4.2.4", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^4.0.3", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.0.2", + "jest-haste-map": "^27.3.1", + "jest-resolve": "^27.3.1", + "jest-util": "^27.3.1", + "jest-worker": "^27.3.1", + "slash": "^3.0.0", + "source-map": "^0.6.0", + "string-length": "^4.0.1", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^8.1.0" + } + }, + "@jest/source-map": { + "version": "27.0.6", + "dev": true, + "requires": { + "callsites": "^3.0.0", + "graceful-fs": "^4.2.4", + "source-map": "^0.6.0" + } + }, + "@jest/test-result": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/console": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/test-result": "^27.3.1", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^27.3.1", + "jest-runtime": "^27.3.1" + } + }, + "@jest/transform": { + "version": "27.3.1", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/types": "^27.2.5", + "babel-plugin-istanbul": "^6.0.0", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.0.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^27.3.1", + "jest-regex-util": "^27.0.6", + "jest-util": "^27.3.1", + "micromatch": "^4.0.4", + "pirates": "^4.0.1", + "slash": "^3.0.0", + "source-map": "^0.6.1", + "write-file-atomic": "^3.0.0" + } + }, + "@jest/types": { + "version": "27.2.5", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", + "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "dev": true + }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.19.tgz", + "integrity": "sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@sideway/address": { + "version": "4.1.2", + "dev": true, + "requires": { + "@hapi/hoek": "^9.0.0" + } + }, + "@sideway/formula": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", + "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", + "dev": true + }, + "@sideway/pinpoint": { + "version": "2.0.0", + "dev": true + }, + "@sinonjs/commons": { + "version": "1.8.3", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "8.1.0", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@tootallnate/once": { + "version": "1.1.2", + "dev": true + }, + "@tsconfig/node10": { + "version": "1.0.8", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.9", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.1", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.2", + "dev": true + }, + "@types/babel__core": { + "version": "7.1.16", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.3", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.1", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.14.2", + "dev": true, + "requires": { + "@babel/types": "^7.3.0" + } + }, + "@types/graceful-fs": { + "version": "4.1.5", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.3", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.1", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "27.0.2", + "dev": true, + "requires": { + "jest-diff": "^27.0.0", + "pretty-format": "^27.0.0" + } + }, + "@types/jest-environment-puppeteer": { + "version": "4.4.1", + "dev": true, + "requires": { + "@jest/types": ">=24 <=26", + "@types/puppeteer": "*", + "jest-environment-node": ">=24 <=26" + }, + "dependencies": { + "@jest/environment": { + "version": "26.6.2", + "dev": true, + "requires": { + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2" + } + }, + "@jest/fake-timers": { + "version": "26.6.2", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@sinonjs/fake-timers": "^6.0.1", + "@types/node": "*", + "jest-message-util": "^26.6.2", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2" + } + }, + "@jest/types": { + "version": "26.6.2", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^15.0.0", + "chalk": "^4.0.0" + } + }, + "@sinonjs/fake-timers": { + "version": "6.0.1", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, + "@types/yargs": { + "version": "15.0.14", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "jest-environment-node": { + "version": "26.6.2", + "dev": true, + "requires": { + "@jest/environment": "^26.6.2", + "@jest/fake-timers": "^26.6.2", + "@jest/types": "^26.6.2", + "@types/node": "*", + "jest-mock": "^26.6.2", + "jest-util": "^26.6.2" + } + }, + "jest-message-util": { + "version": "26.6.2", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "@jest/types": "^26.6.2", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.2", + "pretty-format": "^26.6.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.2" + } + }, + "jest-mock": { + "version": "26.6.2", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*" + } + }, + "jest-util": { + "version": "26.6.2", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "@types/node": "*", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "is-ci": "^2.0.0", + "micromatch": "^4.0.2" + } + }, + "pretty-format": { + "version": "26.6.2", + "dev": true, + "requires": { + "@jest/types": "^26.6.2", + "ansi-regex": "^5.0.0", + "ansi-styles": "^4.0.0", + "react-is": "^17.0.1" + } + } + } + }, + "@types/node": { + "version": "16.11.7", + "dev": true + }, + "@types/prettier": { + "version": "2.4.1", + "dev": true + }, + "@types/puppeteer": { + "version": "5.4.4", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/stack-utils": { + "version": "2.0.1", + "dev": true + }, + "@types/yargs": { + "version": "16.0.4", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "20.2.1", + "dev": true + }, + "@types/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==", + "dev": true, + "optional": true, + "requires": { + "@types/node": "*" + } + }, + "abab": { + "version": "2.0.5", + "dev": true + }, + "acorn": { + "version": "8.5.0", + "dev": true + }, + "acorn-globals": { + "version": "6.0.0", + "dev": true, + "requires": { + "acorn": "^7.1.1", + "acorn-walk": "^7.1.1" + }, + "dependencies": { + "acorn": { + "version": "7.4.1", + "dev": true + } + } + }, + "acorn-walk": { + "version": "7.2.0", + "dev": true + }, + "agent-base": { + "version": "6.0.2", + "dev": true, + "requires": { + "debug": "4" + } + }, + "ansi-escapes": { + "version": "4.3.2", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + } + }, + "ansi-regex": { + "version": "5.0.1", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.2", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "4.1.3", + "dev": true + }, + "argparse": { + "version": "1.0.10", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "arr-union": { + "version": "3.1.0", + "dev": true + }, + "asynckit": { + "version": "0.4.0", + "dev": true + }, + "axios": { + "version": "0.21.4", + "dev": true, + "requires": { + "follow-redirects": "^1.14.0" + } + }, + "babel-jest": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/transform": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.0.0", + "babel-preset-jest": "^27.2.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "dependencies": { + "istanbul-lib-instrument": { + "version": "5.1.0", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + } + } + } + }, + "babel-plugin-jest-hoist": { + "version": "27.2.0", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.0.0", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "27.2.0", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^27.2.0", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "dev": true + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "browser-process-hrtime": { + "version": "1.0.0", + "dev": true + }, + "browserslist": { + "version": "4.17.6", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001274", + "electron-to-chromium": "^1.3.886", + "escalade": "^3.1.1", + "node-releases": "^2.0.1", + "picocolors": "^1.0.0" + } + }, + "bs-logger": { + "version": "0.2.6", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, + "bser": { + "version": "2.1.1", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true + }, + "buffer-from": { + "version": "1.1.2", + "dev": true + }, + "builtin-modules": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", + "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", + "dev": true + }, + "callsites": { + "version": "3.1.0", + "dev": true + }, + "camelcase": { + "version": "5.3.1", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001279", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "char-regex": { + "version": "1.0.2", + "dev": true + }, + "chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "ci-info": { + "version": "3.2.0", + "dev": true + }, + "cjs-module-lexer": { + "version": "1.2.2", + "dev": true + }, + "cliui": { + "version": "7.0.4", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "clone-deep": { + "version": "0.2.4", + "dev": true, + "requires": { + "for-own": "^0.1.3", + "is-plain-object": "^2.0.1", + "kind-of": "^3.0.2", + "lazy-cache": "^1.0.3", + "shallow-clone": "^0.1.2" + } + }, + "co": { + "version": "4.6.0", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.1", + "dev": true + }, + "color-convert": { + "version": "2.0.1", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commander": { + "version": "5.1.0", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "dev": true + }, + "convert-source-map": { + "version": "1.8.0", + "dev": true, + "requires": { + "safe-buffer": "~5.1.1" + } + }, + "cosmiconfig": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.0.0.tgz", + "integrity": "sha512-da1EafcpH6b/TD8vDRaWV7xFINlHlF6zKsGwS1TsuVJTZRkquaS5HTMq7uq6h31619QjbsYl21gVDOm32KM1vQ==", + "dev": true, + "requires": { + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" + }, + "dependencies": { + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + } + } + }, + "create-require": { + "version": "1.1.1", + "dev": true + }, + "cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dev": true, + "requires": { + "node-fetch": "2.6.7" + } + }, + "cross-spawn": { + "version": "7.0.3", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "cssom": { + "version": "0.4.4", + "dev": true + }, + "cssstyle": { + "version": "2.3.0", + "dev": true, + "requires": { + "cssom": "~0.3.6" + }, + "dependencies": { + "cssom": { + "version": "0.3.8", + "dev": true + } + } + }, + "cwd": { + "version": "0.10.0", + "dev": true, + "requires": { + "find-pkg": "^0.1.2", + "fs-exists-sync": "^0.1.0" + } + }, + "data-urls": { + "version": "2.0.0", + "dev": true, + "requires": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "decimal.js": { + "version": "10.3.1", + "dev": true + }, + "dedent": { + "version": "0.7.0", + "dev": true + }, + "deep-is": { + "version": "0.1.4", + "dev": true + }, + "deepmerge": { + "version": "4.2.2", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "dev": true + }, + "detect-newline": { + "version": "3.1.0", + "dev": true + }, + "devtools-protocol": { + "version": "0.0.1082910", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1082910.tgz", + "integrity": "sha512-RqoZ2GmqaNxyx+99L/RemY5CkwG9D0WEfOKxekwCRXOGrDCep62ngezEJUVMq6rISYQ+085fJnWDQqGHlxVNww==", + "dev": true + }, + "didcomm": { + "version": "file:../pkg" + }, + "diff": { + "version": "4.0.2", + "dev": true + }, + "diff-sequences": { + "version": "27.0.6", + "dev": true + }, + "domexception": { + "version": "2.0.1", + "dev": true, + "requires": { + "webidl-conversions": "^5.0.0" + }, + "dependencies": { + "webidl-conversions": { + "version": "5.0.0", + "dev": true + } + } + }, + "electron-to-chromium": { + "version": "1.3.894", + "dev": true + }, + "emittery": { + "version": "0.8.1", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "dev": true + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "requires": { + "once": "^1.4.0" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "escalade": { + "version": "3.1.1", + "dev": true + }, + "escape-string-regexp": { + "version": "2.0.0", + "dev": true + }, + "escodegen": { + "version": "2.0.0", + "dev": true, + "requires": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + } + }, + "esprima": { + "version": "4.0.1", + "dev": true + }, + "estraverse": { + "version": "5.3.0", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "dev": true + }, + "execa": { + "version": "5.1.1", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "exit": { + "version": "0.1.2", + "dev": true + }, + "expand-tilde": { + "version": "1.2.2", + "dev": true, + "requires": { + "os-homedir": "^1.0.1" + } + }, + "expect": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/types": "^27.2.5", + "ansi-styles": "^5.0.0", + "jest-get-type": "^27.3.1", + "jest-matcher-utils": "^27.3.1", + "jest-message-util": "^27.3.1", + "jest-regex-util": "^27.0.6" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "dev": true + } + } + }, + "expect-puppeteer": { + "version": "6.0.0", + "dev": true + }, + "extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "requires": { + "@types/yauzl": "^2.9.1", + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "dependencies": { + "get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "requires": { + "pump": "^3.0.0" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "dev": true + }, + "fb-watchman": { + "version": "2.0.1", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, + "fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "requires": { + "pend": "~1.2.0" + } + }, + "fill-range": { + "version": "7.0.1", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-file-up": { + "version": "0.1.3", + "dev": true, + "requires": { + "fs-exists-sync": "^0.1.0", + "resolve-dir": "^0.1.0" + } + }, + "find-pkg": { + "version": "0.1.2", + "dev": true, + "requires": { + "find-file-up": "^0.1.2" + } + }, + "find-process": { + "version": "1.4.5", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "commander": "^5.1.0", + "debug": "^4.1.1" + } + }, + "find-up": { + "version": "4.1.0", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "dev": true + }, + "for-in": { + "version": "1.0.2", + "dev": true + }, + "for-own": { + "version": "0.1.5", + "dev": true, + "requires": { + "for-in": "^1.0.1" + } + }, + "form-data": { + "version": "3.0.1", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "fs-exists-sync": { + "version": "0.1.0", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "dev": true + }, + "fsevents": { + "version": "2.3.2", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "dev": true + }, + "gensync": { + "version": "1.0.0-beta.2", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "dev": true + }, + "get-package-type": { + "version": "0.1.0", + "dev": true + }, + "get-stream": { + "version": "6.0.1", + "dev": true + }, + "glob": { + "version": "7.2.0", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "global-modules": { + "version": "0.2.3", + "dev": true, + "requires": { + "global-prefix": "^0.1.4", + "is-windows": "^0.2.0" + } + }, + "global-prefix": { + "version": "0.1.5", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.0", + "ini": "^1.3.4", + "is-windows": "^0.2.0", + "which": "^1.2.12" + }, + "dependencies": { + "which": { + "version": "1.3.1", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "globals": { + "version": "11.12.0", + "dev": true + }, + "graceful-fs": { + "version": "4.2.8", + "dev": true + }, + "has": { + "version": "1.0.3", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-flag": { + "version": "4.0.0", + "dev": true + }, + "homedir-polyfill": { + "version": "1.0.3", + "dev": true, + "requires": { + "parse-passwd": "^1.0.0" + } + }, + "html-encoding-sniffer": { + "version": "2.0.1", + "dev": true, + "requires": { + "whatwg-encoding": "^1.0.5" + } + }, + "html-escaper": { + "version": "2.0.2", + "dev": true + }, + "http-proxy-agent": { + "version": "4.0.1", + "dev": true, + "requires": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + } + }, + "https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "requires": { + "agent-base": "6", + "debug": "4" + } + }, + "human-signals": { + "version": "2.1.0", + "dev": true + }, + "iconv-lite": { + "version": "0.4.24", + "dev": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + } + } + }, + "import-local": { + "version": "3.0.3", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "dev": true + }, + "ini": { + "version": "1.3.8", + "dev": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "dev": true + }, + "is-ci": { + "version": "2.0.0", + "dev": true, + "requires": { + "ci-info": "^2.0.0" + }, + "dependencies": { + "ci-info": { + "version": "2.0.0", + "dev": true + } + } + }, + "is-core-module": { + "version": "2.8.0", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, + "is-extendable": { + "version": "0.1.1", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true + }, + "is-generator-fn": { + "version": "2.1.0", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "dev": true + }, + "is-plain-object": { + "version": "2.0.4", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + }, + "is-potential-custom-element-name": { + "version": "1.0.1", + "dev": true + }, + "is-stream": { + "version": "2.0.1", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "dev": true + }, + "is-windows": { + "version": "0.2.0", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.0", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "4.0.3", + "dev": true, + "requires": { + "@babel/core": "^7.7.5", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.0.0", + "semver": "^6.3.0" + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "3.0.5", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/core": "^27.3.1", + "import-local": "^3.0.2", + "jest-cli": "^27.3.1" + } + }, + "jest-changed-files": { + "version": "27.3.0", + "dev": true, + "requires": { + "@jest/types": "^27.2.5", + "execa": "^5.0.0", + "throat": "^6.0.1" + } + }, + "jest-circus": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/environment": "^27.3.1", + "@jest/test-result": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "expect": "^27.3.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.3.1", + "jest-matcher-utils": "^27.3.1", + "jest-message-util": "^27.3.1", + "jest-runtime": "^27.3.1", + "jest-snapshot": "^27.3.1", + "jest-util": "^27.3.1", + "pretty-format": "^27.3.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3", + "throat": "^6.0.1" + } + }, + "jest-cli": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/core": "^27.3.1", + "@jest/test-result": "^27.3.1", + "@jest/types": "^27.2.5", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "import-local": "^3.0.2", + "jest-config": "^27.3.1", + "jest-util": "^27.3.1", + "jest-validate": "^27.3.1", + "prompts": "^2.0.1", + "yargs": "^16.2.0" + } + }, + "jest-config": { + "version": "27.3.1", + "dev": true, + "requires": { + "@babel/core": "^7.1.0", + "@jest/test-sequencer": "^27.3.1", + "@jest/types": "^27.2.5", + "babel-jest": "^27.3.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.1", + "graceful-fs": "^4.2.4", + "jest-circus": "^27.3.1", + "jest-environment-jsdom": "^27.3.1", + "jest-environment-node": "^27.3.1", + "jest-get-type": "^27.3.1", + "jest-jasmine2": "^27.3.1", + "jest-regex-util": "^27.0.6", + "jest-resolve": "^27.3.1", + "jest-runner": "^27.3.1", + "jest-util": "^27.3.1", + "jest-validate": "^27.3.1", + "micromatch": "^4.0.4", + "pretty-format": "^27.3.1" + } + }, + "jest-dev-server": { + "version": "6.0.0", + "dev": true, + "requires": { + "chalk": "^4.1.2", + "cwd": "^0.10.0", + "find-process": "^1.4.5", + "prompts": "^2.4.1", + "spawnd": "^6.0.0", + "tree-kill": "^1.2.2", + "wait-on": "^6.0.0" + } + }, + "jest-diff": { + "version": "27.3.1", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^27.0.6", + "jest-get-type": "^27.3.1", + "pretty-format": "^27.3.1" + } + }, + "jest-docblock": { + "version": "27.0.6", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/types": "^27.2.5", + "chalk": "^4.0.0", + "jest-get-type": "^27.3.1", + "jest-util": "^27.3.1", + "pretty-format": "^27.3.1" + } + }, + "jest-environment-jsdom": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/environment": "^27.3.1", + "@jest/fake-timers": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/node": "*", + "jest-mock": "^27.3.0", + "jest-util": "^27.3.1", + "jsdom": "^16.6.0" + } + }, + "jest-environment-node": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/environment": "^27.3.1", + "@jest/fake-timers": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/node": "*", + "jest-mock": "^27.3.0", + "jest-util": "^27.3.1" + } + }, + "jest-environment-puppeteer": { + "version": "6.0.0", + "dev": true, + "requires": { + "chalk": "^4.1.1", + "cwd": "^0.10.0", + "jest-dev-server": "^6.0.0", + "jest-environment-node": "^27.0.1", + "merge-deep": "^3.0.3" + } + }, + "jest-get-type": { + "version": "27.3.1", + "dev": true + }, + "jest-haste-map": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/types": "^27.2.5", + "@types/graceful-fs": "^4.1.2", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.4", + "jest-regex-util": "^27.0.6", + "jest-serializer": "^27.0.6", + "jest-util": "^27.3.1", + "jest-worker": "^27.3.1", + "micromatch": "^4.0.4", + "walker": "^1.0.7" + } + }, + "jest-jasmine2": { + "version": "27.3.1", + "dev": true, + "requires": { + "@babel/traverse": "^7.1.0", + "@jest/environment": "^27.3.1", + "@jest/source-map": "^27.0.6", + "@jest/test-result": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "expect": "^27.3.1", + "is-generator-fn": "^2.0.0", + "jest-each": "^27.3.1", + "jest-matcher-utils": "^27.3.1", + "jest-message-util": "^27.3.1", + "jest-runtime": "^27.3.1", + "jest-snapshot": "^27.3.1", + "jest-util": "^27.3.1", + "pretty-format": "^27.3.1", + "throat": "^6.0.1" + } + }, + "jest-leak-detector": { + "version": "27.3.1", + "dev": true, + "requires": { + "jest-get-type": "^27.3.1", + "pretty-format": "^27.3.1" + } + }, + "jest-matcher-utils": { + "version": "27.3.1", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^27.3.1", + "jest-get-type": "^27.3.1", + "pretty-format": "^27.3.1" + } + }, + "jest-message-util": { + "version": "27.3.1", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^27.2.5", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "micromatch": "^4.0.4", + "pretty-format": "^27.3.1", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-mock": { + "version": "27.3.0", + "dev": true, + "requires": { + "@jest/types": "^27.2.5", + "@types/node": "*" + } + }, + "jest-pnp-resolver": { + "version": "1.2.2", + "dev": true, + "requires": {} + }, + "jest-puppeteer": { + "version": "6.0.0", + "dev": true, + "requires": { + "expect-puppeteer": "^6.0.0", + "jest-environment-puppeteer": "^6.0.0" + } + }, + "jest-regex-util": { + "version": "27.0.6", + "dev": true + }, + "jest-resolve": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/types": "^27.2.5", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^27.3.1", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^27.3.1", + "jest-validate": "^27.3.1", + "resolve": "^1.20.0", + "resolve.exports": "^1.1.0", + "slash": "^3.0.0" + } + }, + "jest-resolve-dependencies": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/types": "^27.2.5", + "jest-regex-util": "^27.0.6", + "jest-snapshot": "^27.3.1" + } + }, + "jest-runner": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/console": "^27.3.1", + "@jest/environment": "^27.3.1", + "@jest/test-result": "^27.3.1", + "@jest/transform": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.8.1", + "exit": "^0.1.2", + "graceful-fs": "^4.2.4", + "jest-docblock": "^27.0.6", + "jest-environment-jsdom": "^27.3.1", + "jest-environment-node": "^27.3.1", + "jest-haste-map": "^27.3.1", + "jest-leak-detector": "^27.3.1", + "jest-message-util": "^27.3.1", + "jest-resolve": "^27.3.1", + "jest-runtime": "^27.3.1", + "jest-util": "^27.3.1", + "jest-worker": "^27.3.1", + "source-map-support": "^0.5.6", + "throat": "^6.0.1" + } + }, + "jest-runtime": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/console": "^27.3.1", + "@jest/environment": "^27.3.1", + "@jest/globals": "^27.3.1", + "@jest/source-map": "^27.0.6", + "@jest/test-result": "^27.3.1", + "@jest/transform": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/yargs": "^16.0.0", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "execa": "^5.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.4", + "jest-haste-map": "^27.3.1", + "jest-message-util": "^27.3.1", + "jest-mock": "^27.3.0", + "jest-regex-util": "^27.0.6", + "jest-resolve": "^27.3.1", + "jest-snapshot": "^27.3.1", + "jest-util": "^27.3.1", + "jest-validate": "^27.3.1", + "slash": "^3.0.0", + "strip-bom": "^4.0.0", + "yargs": "^16.2.0" + } + }, + "jest-serializer": { + "version": "27.0.6", + "dev": true, + "requires": { + "@types/node": "*", + "graceful-fs": "^4.2.4" + } + }, + "jest-snapshot": { + "version": "27.3.1", + "dev": true, + "requires": { + "@babel/core": "^7.7.2", + "@babel/generator": "^7.7.2", + "@babel/parser": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.0.0", + "@jest/transform": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/babel__traverse": "^7.0.4", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^27.3.1", + "graceful-fs": "^4.2.4", + "jest-diff": "^27.3.1", + "jest-get-type": "^27.3.1", + "jest-haste-map": "^27.3.1", + "jest-matcher-utils": "^27.3.1", + "jest-message-util": "^27.3.1", + "jest-resolve": "^27.3.1", + "jest-util": "^27.3.1", + "natural-compare": "^1.4.0", + "pretty-format": "^27.3.1", + "semver": "^7.3.2" + }, + "dependencies": { + "semver": { + "version": "7.3.5", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "jest-util": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/types": "^27.2.5", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.4", + "picomatch": "^2.2.3" + } + }, + "jest-validate": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/types": "^27.2.5", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^27.3.1", + "leven": "^3.1.0", + "pretty-format": "^27.3.1" + }, + "dependencies": { + "camelcase": { + "version": "6.2.0", + "dev": true + } + } + }, + "jest-watcher": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/test-result": "^27.3.1", + "@jest/types": "^27.2.5", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "jest-util": "^27.3.1", + "string-length": "^4.0.1" + } + }, + "jest-worker": { + "version": "27.3.1", + "dev": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "joi": { + "version": "17.4.2", + "dev": true, + "requires": { + "@hapi/hoek": "^9.0.0", + "@hapi/topo": "^5.0.0", + "@sideway/address": "^4.1.0", + "@sideway/formula": "^3.0.0", + "@sideway/pinpoint": "^2.0.0" + } + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "js-yaml": { + "version": "3.14.1", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsdom": { + "version": "16.7.0", + "dev": true, + "requires": { + "abab": "^2.0.5", + "acorn": "^8.2.4", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.3.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "form-data": "^3.0.0", + "html-encoding-sniffer": "^2.0.1", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.5.0", + "ws": "^7.4.6", + "xml-name-validator": "^3.0.0" + } + }, + "jsesc": { + "version": "2.5.2", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true + }, + "kind-of": { + "version": "3.2.2", + "dev": true, + "requires": { + "is-buffer": "^1.1.5" + } + }, + "kleur": { + "version": "3.0.3", + "dev": true + }, + "lazy-cache": { + "version": "1.0.4", + "dev": true + }, + "leven": { + "version": "3.1.0", + "dev": true + }, + "levn": { + "version": "0.3.0", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "locate-path": { + "version": "5.0.0", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "lodash": { + "version": "4.17.21", + "dev": true + }, + "lodash.memoize": { + "version": "4.1.2", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "make-dir": { + "version": "3.1.0", + "dev": true, + "requires": { + "semver": "^6.0.0" + } + }, + "make-error": { + "version": "1.3.6", + "dev": true + }, + "makeerror": { + "version": "1.0.12", + "dev": true, + "requires": { + "tmpl": "1.0.5" + } + }, + "merge-deep": { + "version": "3.0.3", + "dev": true, + "requires": { + "arr-union": "^3.1.0", + "clone-deep": "^0.2.4", + "kind-of": "^3.0.2" + } + }, + "merge-stream": { + "version": "2.0.0", + "dev": true + }, + "micromatch": { + "version": "4.0.4", + "dev": true, + "requires": { + "braces": "^3.0.1", + "picomatch": "^2.2.3" + } + }, + "mime-db": { + "version": "1.51.0", + "dev": true + }, + "mime-types": { + "version": "2.1.34", + "dev": true, + "requires": { + "mime-db": "1.51.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "dev": true + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "dev": true + }, + "mixin-object": { + "version": "2.0.1", + "dev": true, + "requires": { + "for-in": "^0.1.3", + "is-extendable": "^0.1.1" + }, + "dependencies": { + "for-in": { + "version": "0.1.8", + "dev": true + } + } + }, + "mkdirp": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "dev": true, + "requires": { + "minimist": "^1.2.5" + } + }, + "mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, + "ms": { + "version": "2.1.2", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "dev": true + }, + "node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "requires": { + "whatwg-url": "^5.0.0" + }, + "dependencies": { + "tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "requires": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } + }, + "node-int64": { + "version": "0.4.0", + "dev": true + }, + "node-modules-regexp": { + "version": "1.0.0", + "dev": true + }, + "node-releases": { + "version": "2.0.1", + "dev": true + }, + "normalize-path": { + "version": "3.0.0", + "dev": true + }, + "npm-run-path": { + "version": "4.0.1", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, + "nwsapi": { + "version": "2.2.0", + "dev": true + }, + "once": { + "version": "1.4.0", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "onetime": { + "version": "5.1.2", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "optionator": { + "version": "0.8.3", + "dev": true, + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "os-homedir": { + "version": "1.0.2", + "dev": true + }, + "p-limit": { + "version": "2.3.0", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "p-try": { + "version": "2.2.0", + "dev": true + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, + "parse-passwd": { + "version": "1.0.0", + "dev": true + }, + "parse5": { + "version": "6.0.1", + "dev": true + }, + "path-exists": { + "version": "4.0.0", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "picocolors": { + "version": "1.0.0", + "dev": true + }, + "picomatch": { + "version": "2.3.0", + "dev": true + }, + "pirates": { + "version": "4.0.1", + "dev": true, + "requires": { + "node-modules-regexp": "^1.0.0" + } + }, + "pkg-dir": { + "version": "4.2.0", + "dev": true, + "requires": { + "find-up": "^4.0.0" + } + }, + "prelude-ls": { + "version": "1.1.2", + "dev": true + }, + "prettier": { + "version": "2.4.1", + "dev": true + }, + "pretty-format": { + "version": "27.3.1", + "dev": true, + "requires": { + "@jest/types": "^27.2.5", + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "dev": true + } + } + }, + "progress": { + "version": "2.0.3", + "dev": true + }, + "prompts": { + "version": "2.4.2", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, + "proxy-from-env": { + "version": "1.1.0", + "dev": true + }, + "psl": { + "version": "1.8.0", + "dev": true + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "punycode": { + "version": "2.1.1", + "dev": true + }, + "puppeteer": { + "version": "19.6.3", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-19.6.3.tgz", + "integrity": "sha512-K03xTtGDwS6cBXX/EoqoZxglCUKcX2SLIl92fMnGMRjYpPGXoAV2yKEh3QXmXzKqfZXd8TxjjFww+tEttWv8kw==", + "dev": true, + "requires": { + "cosmiconfig": "8.0.0", + "https-proxy-agent": "5.0.1", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "puppeteer-core": "19.6.3" + } + }, + "puppeteer-core": { + "version": "19.6.3", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-19.6.3.tgz", + "integrity": "sha512-8MbhioSlkDaHkmolpQf9Z7ui7jplFfOFTnN8d5kPsCazRRTNIH6/bVxPskn0v5Gh9oqOBlknw0eHH0/OBQAxpQ==", + "dev": true, + "requires": { + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.1082910", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "proxy-from-env": "1.1.0", + "rimraf": "3.0.2", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.11.0" + }, + "dependencies": { + "ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "requires": {} + } + } + }, + "querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", + "dev": true + }, + "react-is": { + "version": "17.0.2", + "dev": true + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "require-directory": { + "version": "2.1.1", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "resolve": { + "version": "1.20.0", + "dev": true, + "requires": { + "is-core-module": "^2.2.0", + "path-parse": "^1.0.6" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + } + }, + "resolve-dir": { + "version": "0.1.1", + "dev": true, + "requires": { + "expand-tilde": "^1.2.2", + "global-modules": "^0.2.3" + } + }, + "resolve-from": { + "version": "5.0.0", + "dev": true + }, + "resolve.exports": { + "version": "1.1.0", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "rxjs": { + "version": "7.4.0", + "dev": true, + "requires": { + "tslib": "~2.1.0" + } + }, + "safe-buffer": { + "version": "5.1.2", + "dev": true + }, + "safer-buffer": { + "version": "2.1.2", + "dev": true + }, + "saxes": { + "version": "5.0.1", + "dev": true, + "requires": { + "xmlchars": "^2.2.0" + } + }, + "semver": { + "version": "6.3.0", + "dev": true + }, + "shallow-clone": { + "version": "0.1.2", + "dev": true, + "requires": { + "is-extendable": "^0.1.1", + "kind-of": "^2.0.1", + "lazy-cache": "^0.2.3", + "mixin-object": "^2.0.1" + }, + "dependencies": { + "kind-of": { + "version": "2.0.1", + "dev": true, + "requires": { + "is-buffer": "^1.0.2" + } + }, + "lazy-cache": { + "version": "0.2.7", + "dev": true + } + } + }, + "shebang-command": { + "version": "2.0.0", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "dev": true + }, + "signal-exit": { + "version": "3.0.5", + "dev": true + }, + "sisteransi": { + "version": "1.0.5", + "dev": true + }, + "slash": { + "version": "3.0.0", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "dev": true + }, + "source-map-support": { + "version": "0.5.20", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "spawnd": { + "version": "6.0.0", + "dev": true, + "requires": { + "exit": "^0.1.2", + "signal-exit": "^3.0.3", + "tree-kill": "^1.2.2" + } + }, + "sprintf-js": { + "version": "1.0.3", + "dev": true + }, + "stack-utils": { + "version": "2.0.5", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "requires": { + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true + } + } + }, + "string-length": { + "version": "4.0.2", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, + "string-width": { + "version": "4.2.3", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-bom": { + "version": "4.0.0", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-hyperlinks": { + "version": "2.2.0", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + } + }, + "symbol-tree": { + "version": "3.2.4", + "dev": true + }, + "tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, + "requires": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "requires": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + } + }, + "terminal-link": { + "version": "2.1.1", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + } + }, + "test-exclude": { + "version": "6.0.0", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, + "throat": { + "version": "6.0.1", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "tmpl": { + "version": "1.0.5", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "tough-cookie": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz", + "integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==", + "dev": true, + "requires": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + } + }, + "tr46": { + "version": "2.1.0", + "dev": true, + "requires": { + "punycode": "^2.1.1" + } + }, + "tree-kill": { + "version": "1.2.2", + "dev": true + }, + "ts-jest": { + "version": "27.0.7", + "dev": true, + "requires": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^27.0.0", + "json5": "2.x", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "7.x", + "yargs-parser": "20.x" + }, + "dependencies": { + "semver": { + "version": "7.3.5", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + } + } + }, + "ts-node": { + "version": "10.4.0", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "0.7.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "yn": "3.1.1" + }, + "dependencies": { + "acorn-walk": { + "version": "8.2.0", + "dev": true + } + } + }, + "tslib": { + "version": "2.1.0", + "dev": true + }, + "tslint": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/tslint/-/tslint-6.1.3.tgz", + "integrity": "sha512-IbR4nkT96EQOvKE2PW/djGz8iGNeJ4rF2mBfiYaR/nvUWYKJhLwimoJKgjIFEIDibBtOevj7BqCRL4oHeWWUCg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "builtin-modules": "^1.1.1", + "chalk": "^2.3.0", + "commander": "^2.12.1", + "diff": "^4.0.1", + "glob": "^7.1.1", + "js-yaml": "^3.13.1", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.3", + "resolve": "^1.3.2", + "semver": "^5.3.0", + "tslib": "^1.13.0", + "tsutils": "^2.29.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=", + "dev": true + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + }, + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "tsutils": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz", + "integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "type-check": { + "version": "0.3.2", + "dev": true, + "requires": { + "prelude-ls": "~1.1.2" + } + }, + "type-detect": { + "version": "4.0.8", + "dev": true + }, + "type-fest": { + "version": "0.21.3", + "dev": true + }, + "typedarray-to-buffer": { + "version": "3.1.5", + "dev": true, + "requires": { + "is-typedarray": "^1.0.0" + } + }, + "typescript": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", + "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==" + }, + "unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "requires": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "dev": true + }, + "url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dev": true, + "requires": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "v8-to-istanbul": { + "version": "8.1.0", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0", + "source-map": "^0.7.3" + }, + "dependencies": { + "source-map": { + "version": "0.7.3", + "dev": true + } + } + }, + "w3c-hr-time": { + "version": "1.0.2", + "dev": true, + "requires": { + "browser-process-hrtime": "^1.0.0" + } + }, + "w3c-xmlserializer": { + "version": "2.0.0", + "dev": true, + "requires": { + "xml-name-validator": "^3.0.0" + } + }, + "wait-on": { + "version": "6.0.0", + "dev": true, + "requires": { + "axios": "^0.21.1", + "joi": "^17.4.0", + "lodash": "^4.17.21", + "minimist": "^1.2.5", + "rxjs": "^7.1.0" + } + }, + "walker": { + "version": "1.0.8", + "dev": true, + "requires": { + "makeerror": "1.0.12" + } + }, + "webidl-conversions": { + "version": "6.1.0", + "dev": true + }, + "whatwg-encoding": { + "version": "1.0.5", + "dev": true, + "requires": { + "iconv-lite": "0.4.24" + } + }, + "whatwg-mimetype": { + "version": "2.3.0", + "dev": true + }, + "whatwg-url": { + "version": "8.7.0", + "dev": true, + "requires": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + } + }, + "which": { + "version": "2.0.2", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "word-wrap": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "dev": true + }, + "write-file-atomic": { + "version": "3.0.3", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" + } + }, + "ws": { + "version": "7.5.5", + "dev": true, + "requires": {} + }, + "xml-name-validator": { + "version": "3.0.0", + "dev": true + }, + "xmlchars": { + "version": "2.2.0", + "dev": true + }, + "y18n": { + "version": "5.0.8", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "dev": true + }, + "yargs": { + "version": "16.2.0", + "dev": true, + "requires": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + } + }, + "yargs-parser": { + "version": "20.2.9", + "dev": true + }, + "yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "requires": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "yn": { + "version": "3.1.1", + "dev": true + } + } +} diff --git a/affinidi-messaging-didcomm/wasm/tests-js/package.json b/affinidi-messaging-didcomm/wasm/tests-js/package.json new file mode 100644 index 0000000..d94cbc5 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/package.json @@ -0,0 +1,29 @@ +{ + "name": "didcomm-tests", + "version": "0.3.4", + "description": "JS tests for didcomm", + "scripts": { + "test": "jest", + "test-puppeteer": "jest --config=jest.config.puppeteer.js", + "check": "tslint -c tslint.json 'src/**/*.ts'", + "format": "prettier --write ." + }, + "author": "", + "license": "Apache-2.0", + "dependencies": { + "didcomm": "file:../pkg", + "typescript": "^4.5.2" + }, + "devDependencies": { + "@types/jest": "^27.0.2", + "@types/jest-environment-puppeteer": "^4.4.1", + "@types/puppeteer": "^5.4.4", + "jest": "^27.3.1", + "jest-puppeteer": "^6.0.0", + "prettier": "2.4.1", + "puppeteer": "^19.6.3", + "ts-jest": "^27.0.7", + "ts-node": "^10.4.0", + "tslint": "^6.1.3" + } +} diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/forward/try-parse-forward.test.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/forward/try-parse-forward.test.ts new file mode 100644 index 0000000..831cae6 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/forward/try-parse-forward.test.ts @@ -0,0 +1,23 @@ +import { Message, ParsedForward } from "didcomm"; +import { MESSAGE_SIMPLE, FORWARD_MESSAGE } from "../test-vectors"; + +// TODO: more tests +test.each([ + { + case: "Not Forward", + message: MESSAGE_SIMPLE, + }, +])("Message.try-parse-forward handles $case", async ({ message }) => { + const res = message.try_parse_forward(); + expect(res).toBeNull(); +}); + +test.each([ + { + case: "Forward", + message: FORWARD_MESSAGE, + }, +])("Message.try-parse-forward handles $case", async ({ message }) => { + const res = message.try_parse_forward(); + expect(res).toEqual(expect.anything()); +}); diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/forward/wrap-in-forward.test.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/forward/wrap-in-forward.test.ts new file mode 100644 index 0000000..c140be3 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/forward/wrap-in-forward.test.ts @@ -0,0 +1,42 @@ +import { Message } from "didcomm"; +import { + IMESSAGE_SIMPLE, + ExampleDIDResolver, + ALICE_DID_DOC, + BOB_DID_DOC, + ALICE_DID, +} from "../test-vectors"; + +// TODO: more tests +test.each([ + { + case: "Simple", + message: IMESSAGE_SIMPLE, + headers: { header1: "aaa", header2: "bbb" }, + to: ALICE_DID, + routing_keys: ["did:example:bob#key-x25519-1"], + enc_alg_anon: "A256cbcHs512EcdhEsA256kw", + did_resolver: new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]), + }, +])( + "Message.wrap-in-forward handles $case", + async ({ + message, + headers, + to, + routing_keys, + enc_alg_anon, + did_resolver, + }) => { + const res = await Message.wrap_in_forward( + JSON.stringify(message), + headers, + to, + routing_keys, + enc_alg_anon, + did_resolver + ); + expect(typeof res).toStrictEqual("string"); + expect(res).toContain("ciphertext"); + } +); diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/from_prior/new.test.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/from_prior/new.test.ts new file mode 100644 index 0000000..38afee5 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/from_prior/new.test.ts @@ -0,0 +1,10 @@ +import { FromPrior } from "didcomm"; +import { IFROM_PRIOR_FULL, IFROM_PRIOR_MINIMAL } from "../test-vectors"; + +test.each([ + { val: IFROM_PRIOR_MINIMAL, case: "Minimal" }, + { val: IFROM_PRIOR_FULL, case: "Full" }, +])("FromPrior.new works for $case", ({ val }) => { + const fromPrior = new FromPrior(val); + expect(fromPrior.as_value()).toStrictEqual(val); +}); diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/from_prior/pack.test.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/from_prior/pack.test.ts new file mode 100644 index 0000000..9d6604c --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/from_prior/pack.test.ts @@ -0,0 +1,54 @@ +import { FromPrior } from "didcomm"; +import { + ALICE_DID_DOC, + CHARLIE_DID_DOC, + CHARLIE_SECRETS, + CHARLIE_SECRET_AUTH_KEY_ED25519, + ExampleDIDResolver, + ExampleSecretsResolver, + FROM_PRIOR_FULL, + FROM_PRIOR_MINIMAL, +} from "../test-vectors"; + +test.each([ + { + fromPrior: FROM_PRIOR_MINIMAL, + issuerKid: null, + expKid: CHARLIE_SECRET_AUTH_KEY_ED25519.id, + case: "Minimal", + }, + { + fromPrior: FROM_PRIOR_FULL, + issuerKid: null, + expKid: CHARLIE_SECRET_AUTH_KEY_ED25519.id, + case: "Full", + }, + { + fromPrior: FROM_PRIOR_FULL, + issuerKid: CHARLIE_SECRET_AUTH_KEY_ED25519.id, + expKid: CHARLIE_SECRET_AUTH_KEY_ED25519.id, + case: "Explicit key", + }, +])( + "FromPrior.pack works for $case", + async ({ fromPrior, issuerKid, expKid }) => { + const didResolver = new ExampleDIDResolver([ + ALICE_DID_DOC, + CHARLIE_DID_DOC, + ]); + + const secretsResolver = new ExampleSecretsResolver(CHARLIE_SECRETS); + + const [packed, kid] = await fromPrior.pack( + issuerKid, + didResolver, + secretsResolver + ); + + expect(typeof packed).toStrictEqual("string"); + expect(kid).toStrictEqual(expKid); + + const [unpacked, _] = await FromPrior.unpack(packed, didResolver); + expect(unpacked.as_value()).toStrictEqual(fromPrior.as_value()); + } +); diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/from_prior/unpack.test.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/from_prior/unpack.test.ts new file mode 100644 index 0000000..5b1ab4a --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/from_prior/unpack.test.ts @@ -0,0 +1,43 @@ +import { FromPrior } from "didcomm"; +import { + ALICE_DID_DOC, + CHARLIE_DID_DOC, + CHARLIE_SECRET_AUTH_KEY_ED25519, + ExampleDIDResolver, + FROM_PRIOR_JWT_FULL, + FROM_PRIOR_JWT_INVALID, + FROM_PRIOR_JWT_INVALID_SIGNATURE, + IFROM_PRIOR_FULL, +} from "../test-vectors"; + +test("FromPrior.unpack works", async () => { + const didResolver = new ExampleDIDResolver([ALICE_DID_DOC, CHARLIE_DID_DOC]); + + const [fromPrior, issuerKid] = await FromPrior.unpack( + FROM_PRIOR_JWT_FULL, + didResolver + ); + + // TODO: Use toStrictEq check after reduction of FromPrior fields + expect(fromPrior.as_value()).toMatchObject(IFROM_PRIOR_FULL); + + expect(issuerKid).toStrictEqual(CHARLIE_SECRET_AUTH_KEY_ED25519.id); +}); + +test.each([ + { + jwt: FROM_PRIOR_JWT_INVALID, + exp_err: "Malformed: Unable to parse compactly serialized JWS", + case: "Malformed", + }, + { + jwt: FROM_PRIOR_JWT_INVALID_SIGNATURE, + exp_err: + "Malformed: Unable to verify from_prior signature: Unable decode signature: Invalid last symbol 66, offset 85.", + case: "Invalid signature", + }, +])("FromPrior.unpack handles $case", async ({ jwt, exp_err }) => { + const didResolver = new ExampleDIDResolver([ALICE_DID_DOC, CHARLIE_DID_DOC]); + const res = FromPrior.unpack(jwt, didResolver); + await expect(res).rejects.toThrowError(exp_err); +}); diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/message/errors.test.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/message/errors.test.ts new file mode 100644 index 0000000..0224566 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/message/errors.test.ts @@ -0,0 +1,266 @@ +import { + ALICE_DID, + ALICE_DID_DOC, + ALICE_SECRETS, + BOB_DID, + BOB_DID_DOC, + ExampleDIDResolver, + ExampleSecretsResolver, + MESSAGE_SIMPLE, + MockDIDResolver, + MockSecretsResolver, +} from "../test-vectors"; + +test.each([ + { + err: (() => { + const e = Error("Some Malformed error"); + e.name = "DIDCommMalformed"; + return e; + })(), + exp_err: + "Malformed: Unable resolve recipient did: Unable resolve did: Some Malformed error", + case: "Malformed", + }, + { + err: (() => { + const e = Error("Some IoError error"); + e.name = "DIDCommIoError"; + return e; + })(), + exp_err: + "IO error: Unable resolve recipient did: Unable resolve did: Some IoError error", + case: "IoError", + }, + { + err: (() => { + const e = Error("Some InvalidState error"); + e.name = "DIDCommInvalidState"; + return e; + })(), + exp_err: + "Invalid state: Unable resolve recipient did: Unable resolve did: Some InvalidState error", + case: "InvalidState", + }, + { + err: (() => { + return Error("Unknown error"); + })(), + exp_err: + "Invalid state: Unable resolve recipient did: Unable resolve did: Unknown error", + case: "Error", + }, + { + err: (() => { + return "String error"; + })(), + exp_err: + "Invalid state: Unable resolve recipient did: Unable resolve did: String error", + case: "String", + }, + { + err: (() => { + return 123; + })(), + exp_err: + "Invalid state: Unable resolve recipient did: Unable resolve did: JsValue(123)", + case: "Unusual", + }, +])( + "DIDReslver.resolve exception is propogated for $case", + async ({ err, exp_err }) => { + const didResolver = new MockDIDResolver( + [ + () => { + throw err; + }, + ], + new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]) + ); + + const secretsResolver = new ExampleSecretsResolver(ALICE_SECRETS); + + const res = MESSAGE_SIMPLE.pack_encrypted( + BOB_DID, + ALICE_DID, + null, + didResolver, + secretsResolver, + { + forward: false, + } + ); + + await expect(res).rejects.toThrowError(exp_err); + } +); + +test.each([ + { + err: (() => { + const e = Error("Some Malformed error"); + e.name = "DIDCommMalformed"; + return e; + })(), + exp_err: + "Malformed: Unable resolve sender secret: Unable get secret: Some Malformed error", + case: "Malformed", + }, + { + err: (() => { + const e = Error("Some IoError error"); + e.name = "DIDCommIoError"; + return e; + })(), + exp_err: + "IO error: Unable resolve sender secret: Unable get secret: Some IoError error", + case: "IoError", + }, + { + err: (() => { + const e = Error("Some InvalidState error"); + e.name = "DIDCommInvalidState"; + return e; + })(), + exp_err: + "Invalid state: Unable resolve sender secret: Unable get secret: Some InvalidState error", + case: "InvalidState", + }, + { + err: (() => { + return Error("Unknown error"); + })(), + exp_err: + "Invalid state: Unable resolve sender secret: Unable get secret: Unknown error", + case: "Error", + }, + { + err: (() => { + return "String error"; + })(), + exp_err: + "Invalid state: Unable resolve sender secret: Unable get secret: String error", + case: "String", + }, + { + err: (() => { + return 123; + })(), + exp_err: + "Invalid state: Unable resolve sender secret: Unable get secret: JsValue(123)", + case: "Unusual", + }, +])( + "Secrets.get_secret exception is propogated for $case", + async ({ err, exp_err }) => { + const didResolver = new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]); + + const secretsResolver = new MockSecretsResolver( + [ + () => { + throw err; + }, + ], + [], + new ExampleSecretsResolver(ALICE_SECRETS) + ); + + const res = MESSAGE_SIMPLE.pack_encrypted( + BOB_DID, + ALICE_DID, + null, + didResolver, + secretsResolver, + { + forward: false, + } + ); + + await expect(res).rejects.toThrowError(exp_err); + } +); + +test.each([ + { + err: (() => { + const e = Error("Some Malformed error"); + e.name = "DIDCommMalformed"; + return e; + })(), + exp_err: + "Malformed: Unable find secrets: Unable find secrets: Some Malformed error", + case: "Malformed", + }, + { + err: (() => { + const e = Error("Some IoError error"); + e.name = "DIDCommIoError"; + return e; + })(), + exp_err: + "IO error: Unable find secrets: Unable find secrets: Some IoError error", + case: "IoError", + }, + { + err: (() => { + const e = Error("Some InvalidState error"); + e.name = "DIDCommInvalidState"; + return e; + })(), + exp_err: + "Invalid state: Unable find secrets: Unable find secrets: Some InvalidState error", + case: "InvalidState", + }, + { + err: (() => { + return Error("Unknown error"); + })(), + exp_err: + "Invalid state: Unable find secrets: Unable find secrets: Unknown error", + case: "Error", + }, + { + err: (() => { + return "String error"; + })(), + exp_err: + "Invalid state: Unable find secrets: Unable find secrets: String error", + case: "String", + }, + { + err: (() => { + return 123; + })(), + exp_err: + "Invalid state: Unable find secrets: Unable find secrets: JsValue(123)", + case: "Unusual", + }, +])( + "Secrets.find_secrets exception is propogated for $case", + async ({ err, exp_err }) => { + const didResolver = new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]); + + const secretsResolver = new MockSecretsResolver( + [], + [ + () => { + throw err; + }, + ], + new ExampleSecretsResolver(ALICE_SECRETS) + ); + + const res = MESSAGE_SIMPLE.pack_encrypted( + BOB_DID, + ALICE_DID, + null, + didResolver, + secretsResolver, + { + forward: false, + } + ); + + await expect(res).rejects.toThrowError(exp_err); + } +); diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/message/new.test.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/message/new.test.ts new file mode 100644 index 0000000..1e1976c --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/message/new.test.ts @@ -0,0 +1,41 @@ +import { Message, IMessage } from "didcomm"; + +test("Message.new works", () => { + const val: IMessage = { + id: "example-1", + typ: "application/didcomm-plain+json", + type: "example/v1", + body: "example-body", + from: "did:example:4", + to: ["did:example:1", "did:example:2", "did:example:3"], + thid: "example-thread-1", + pthid: "example-parent-thread-1", + "example-header-1": "example-header-1-value", + "example-header-2": "example-header-2-value", + created_time: 10000, + expires_time: 20000, + attachments: [ + { + data: { + base64: "ZXhhbXBsZQ==", + }, + id: "attachment1", + }, + { + data: { + json: "example", + }, + id: "attachment2", + }, + { + data: { + json: "example", + }, + id: "attachment3", + }, + ], + }; + + const msg = new Message(val); + expect(msg.as_value()).toStrictEqual(val); +}); diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/message/pack-encrypted.anoncrypt.test.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/message/pack-encrypted.anoncrypt.test.ts new file mode 100644 index 0000000..1583c65 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/message/pack-encrypted.anoncrypt.test.ts @@ -0,0 +1,124 @@ +import { + ALICE_AUTH_METHOD_25519, + ALICE_AUTH_METHOD_P256, + ALICE_AUTH_METHOD_SECP256K1, + ALICE_DID, + ALICE_DID_DOC, + ALICE_SECRETS, + BOB_DID, + BOB_DID_DOC, + BOB_SECRET_KEY_AGREEMENT_KEY_P256_1, + BOB_SECRET_KEY_AGREEMENT_KEY_P384_1, + BOB_SECRET_KEY_AGREEMENT_KEY_P521_1, + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1, + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2, + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3, + BOB_SECRETS, + BOB_VERIFICATION_METHOD_KEY_AGREEM_P256_1, + BOB_VERIFICATION_METHOD_KEY_AGREEM_P256_2, + BOB_VERIFICATION_METHOD_KEY_AGREEM_P384_1, + BOB_VERIFICATION_METHOD_KEY_AGREEM_P384_2, + BOB_VERIFICATION_METHOD_KEY_AGREEM_P521_1, + BOB_VERIFICATION_METHOD_KEY_AGREEM_P521_2, + BOB_VERIFICATION_METHOD_KEY_AGREEM_X25519_1, + BOB_VERIFICATION_METHOD_KEY_AGREEM_X25519_2, + BOB_VERIFICATION_METHOD_KEY_AGREEM_X25519_3, + ExampleDIDResolver, + ExampleSecretsResolver, + MESSAGE_SIMPLE, +} from "../test-vectors"; +import { Message } from "didcomm"; + +test.each([ + { + message: MESSAGE_SIMPLE, + signBy: null, + to: BOB_DID, + expMetadata: { + messaging_service: null, + from_kid: null, + sign_by_kid: null, + to_kids: [ + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1.id, + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3.id, + ], + }, + case: "Simple message X25519", + }, + { + message: MESSAGE_SIMPLE, + signBy: null, + to: BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + expMetadata: { + messaging_service: null, + from_kid: null, + sign_by_kid: null, + to_kids: [BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id], + }, + case: "Simple message P256", + }, + { + message: MESSAGE_SIMPLE, + signBy: ALICE_AUTH_METHOD_25519.id, + to: BOB_DID, + expMetadata: { + messaging_service: null, + from_kid: null, + sign_by_kid: ALICE_AUTH_METHOD_25519.id, + to_kids: [ + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1.id, + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3.id, + ], + }, + case: "Simple message X25519 signed", + }, + { + message: MESSAGE_SIMPLE, + signBy: ALICE_AUTH_METHOD_P256.id, + to: BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + expMetadata: { + messaging_service: null, + from_kid: null, + sign_by_kid: ALICE_AUTH_METHOD_P256.id, + to_kids: [BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id], + }, + case: "Simple message P256 signed", + }, +])( + "Message.pack-encrypted anoncrypt works for $case", + async ({ message, signBy, to, expMetadata }) => { + const didResolver = new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]); + let secretResolver = new ExampleSecretsResolver(ALICE_SECRETS); + + const [encrypted, metadata] = await message.pack_encrypted( + to, + null, + signBy, + didResolver, + secretResolver, + { + protect_sender: false, + forward: false, + forward_headers: null, + messaging_service: null, + enc_alg_anon: "A256cbcHs512EcdhEsA256kw", + } + ); + + expect(typeof encrypted).toStrictEqual("string"); + expect(metadata).toStrictEqual(expMetadata); + + secretResolver = new ExampleSecretsResolver(BOB_SECRETS); + + const [unpacked, _] = await Message.unpack( + encrypted, + didResolver, + secretResolver, + {} + ); + + expect(unpacked.as_value()).toStrictEqual(message.as_value()); + } +); diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/message/pack-encrypted.authcrypt.test.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/message/pack-encrypted.authcrypt.test.ts new file mode 100644 index 0000000..9e91f47 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/message/pack-encrypted.authcrypt.test.ts @@ -0,0 +1,219 @@ +import { + ALICE_AUTH_METHOD_25519, + ALICE_AUTH_METHOD_P256, + ALICE_AUTH_METHOD_SECP256K1, + ALICE_DID, + ALICE_DID_DOC, + ALICE_SECRETS, + ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256, + ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519, + ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519_NOT_IN_SECRET, + BOB_DID, + BOB_DID_DOC, + BOB_SECRET_KEY_AGREEMENT_KEY_P256_1, + BOB_SECRET_KEY_AGREEMENT_KEY_P384_1, + BOB_SECRET_KEY_AGREEMENT_KEY_P521_1, + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1, + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2, + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3, + BOB_SECRETS, + BOB_VERIFICATION_METHOD_KEY_AGREEM_P256_1, + BOB_VERIFICATION_METHOD_KEY_AGREEM_P256_2, + BOB_VERIFICATION_METHOD_KEY_AGREEM_P384_1, + BOB_VERIFICATION_METHOD_KEY_AGREEM_P384_2, + BOB_VERIFICATION_METHOD_KEY_AGREEM_P521_1, + BOB_VERIFICATION_METHOD_KEY_AGREEM_P521_2, + BOB_VERIFICATION_METHOD_KEY_AGREEM_X25519_1, + BOB_VERIFICATION_METHOD_KEY_AGREEM_X25519_2, + BOB_VERIFICATION_METHOD_KEY_AGREEM_X25519_3, + CHARLIE_DID, + CHARLIE_DID_DOC, + ExampleDIDResolver, + ExampleSecretsResolver, + MESSAGE_SIMPLE, + MockDIDResolver, + MockSecretsResolver, +} from "../test-vectors"; +import { Message } from "didcomm"; + +test.each([ + { + message: MESSAGE_SIMPLE, + from: ALICE_DID, + to: BOB_DID, + expMetadata: { + messaging_service: null, + from_kid: ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519.id, + sign_by_kid: null, + to_kids: [ + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1.id, + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2.id, + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3.id, + ], + }, + case: "Simple message X25519", + }, + { + message: MESSAGE_SIMPLE, + from: ALICE_DID, + to: BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id, + expMetadata: { + messaging_service: null, + from_kid: ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256.id, + sign_by_kid: null, + to_kids: [BOB_SECRET_KEY_AGREEMENT_KEY_P256_1.id], + }, + case: "Simple message P256", + }, +])( + "Message.pack-encrypted authcrypt works for $case", + async ({ message, from, to, expMetadata }) => { + const didResolver = new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]); + let secretResolver = new ExampleSecretsResolver(ALICE_SECRETS); + + const [encrypted, metadata] = await message.pack_encrypted( + to, + from, + null, + didResolver, + secretResolver, + { + protect_sender: false, + forward: false, + forward_headers: null, + messaging_service: null, + enc_alg_anon: "A256cbcHs512EcdhEsA256kw", + } + ); + + expect(typeof encrypted).toStrictEqual("string"); + expect(metadata).toStrictEqual(expMetadata); + + secretResolver = new ExampleSecretsResolver(BOB_SECRETS); + + const [unpacked, _] = await Message.unpack( + encrypted, + didResolver, + secretResolver, + {} + ); + + expect(unpacked.as_value()).toStrictEqual(message.as_value()); + } +); + +test.each([ + { + case: "from is not a did or did url", + didResolver: new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]), + secretsResolver: new ExampleSecretsResolver(ALICE_SECRETS), + message: MESSAGE_SIMPLE, + from: "not-a-did", + to: BOB_DID, + signBy: ALICE_DID, + expError: "Illegal argument: `from` value is not a valid DID or DID URL", + }, + { + case: "Signer DID not a did", + didResolver: new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]), + secretsResolver: new ExampleSecretsResolver(ALICE_SECRETS), + message: MESSAGE_SIMPLE, + from: ALICE_DID, + to: "not-a-did", + signBy: ALICE_DID, + expError: "Illegal argument: `to` value is not a valid DID or DID URL", + }, + { + case: "Signer DID URL not found", + didResolver: new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]), + secretsResolver: new ExampleSecretsResolver(ALICE_SECRETS), + message: MESSAGE_SIMPLE, + from: ALICE_DID, + to: BOB_DID, + signBy: "not-a-did", + expError: + "Illegal argument: `sign_from` value is not a valid DID or DID URL", + }, + { + case: "from differs message from", + didResolver: new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]), + secretsResolver: new ExampleSecretsResolver(ALICE_SECRETS), + message: MESSAGE_SIMPLE, + from: BOB_DID, + to: BOB_DID, + signBy: ALICE_DID, + expError: + "Illegal argument: `message.from` value is not equal to `from` value's DID", + }, + { + case: "to differs message to", + didResolver: new ExampleDIDResolver([ALICE_DID_DOC, CHARLIE_DID_DOC]), + secretsResolver: new ExampleSecretsResolver(ALICE_SECRETS), + message: MESSAGE_SIMPLE, + from: ALICE_DID, + to: CHARLIE_DID, + signBy: ALICE_DID, + expError: + "Illegal argument: `message.to` value does not contain `to` value's DID", + }, + { + case: "from unknown did", + didResolver: new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]), + secretsResolver: new ExampleSecretsResolver(ALICE_SECRETS), + message: new Message({ + id: "1234567890", + typ: "application/didcomm-plain+json", + type: "http://example.com/protocols/lets_do_lunch/1.0/proposal", + from: "did:example:unknown", + to: ["did:example:bob"], + created_time: 1516269022, + expires_time: 1516385931, + body: { messagespecificattribute: "and its value" }, + }), + from: "did:example:unknown", + to: BOB_DID, + signBy: ALICE_DID, + expError: "DID not resolved: Sender did not found", + }, + { + case: "from unknown did url", + didResolver: new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]), + secretsResolver: new ExampleSecretsResolver(ALICE_SECRETS), + message: MESSAGE_SIMPLE, + from: ALICE_DID + "#unknown-key", + to: BOB_DID, + signBy: ALICE_DID, + expError: "DID URL not found: No sender key agreements found", + }, + { + case: "from unknown did url", + didResolver: new ExampleDIDResolver([ALICE_DID_DOC, BOB_DID_DOC]), + secretsResolver: new ExampleSecretsResolver(ALICE_SECRETS), + message: MESSAGE_SIMPLE, + from: "did:example:alice#key-x25519-not-in-secrets-1", + to: BOB_DID, + signBy: null, + expError: "Secret not found: No sender secrets found", + }, +])( + "Message.pack-encrypted handles $case", + async ({ + didResolver, + secretsResolver, + message, + from, + to, + signBy, + expError, + }) => { + const res = message.pack_encrypted( + to, + from, + signBy, + didResolver, + secretsResolver, + {} + ); + await expect(res).rejects.toThrowError(expError); + } +); diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/message/pack-plaintext.test.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/message/pack-plaintext.test.ts new file mode 100644 index 0000000..e4e46b6 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/message/pack-plaintext.test.ts @@ -0,0 +1,43 @@ +import { + MESSAGE_SIMPLE, + ExampleDIDResolver, + ALICE_DID_DOC, + PLAINTEXT_MSG_SIMPLE, + MESSAGE_MINIMAL, + PLAINTEXT_MSG_MINIMAL, + MESSAGE_FROM_PRIOR, + PLAINTEXT_FROM_PRIOR, + BOB_DID_DOC, + CHARLIE_DID_DOC, +} from "../test-vectors"; + +test.each([ + { + message: MESSAGE_SIMPLE, + expPlaintext: PLAINTEXT_MSG_SIMPLE, + case: "Simple", + }, + { + message: MESSAGE_MINIMAL, + expPlaintext: PLAINTEXT_MSG_MINIMAL, + case: "Minimal", + }, + { + message: MESSAGE_FROM_PRIOR, + expPlaintext: PLAINTEXT_FROM_PRIOR, + case: "FromPrior", + }, +])( + "Message.pack-plaintext works for $case", + async ({ message, expPlaintext }) => { + const didResolver = new ExampleDIDResolver([ + ALICE_DID_DOC, + BOB_DID_DOC, + CHARLIE_DID_DOC, + ]); + + const plaintext = await message.pack_plaintext(didResolver); + + expect(JSON.parse(plaintext)).toStrictEqual(JSON.parse(expPlaintext)); + } +); diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/message/pack-signed.test.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/message/pack-signed.test.ts new file mode 100644 index 0000000..f219795 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/message/pack-signed.test.ts @@ -0,0 +1,152 @@ +import { + MESSAGE_SIMPLE, + ExampleDIDResolver, + ALICE_DID_DOC, + PLAINTEXT_MSG_SIMPLE, + MESSAGE_MINIMAL, + MockDIDResolver, + MockSecretsResolver, + PLAINTEXT_MSG_MINIMAL, + ExampleSecretsResolver, + ALICE_SECRETS, + ALICE_DID, + ALICE_AUTH_METHOD_25519, + ALICE_AUTH_METHOD_P256, + ALICE_AUTH_METHOD_SECP256K1, +} from "../test-vectors"; + +import { FromPrior, Message, PackSignedMetadata } from "didcomm"; + +test.each([ + { + message: MESSAGE_SIMPLE, + signBy: ALICE_DID, + expMetadata: { sign_by_kid: ALICE_AUTH_METHOD_25519.id }, + case: "Simple message ED25519", + }, + { + message: MESSAGE_SIMPLE, + signBy: ALICE_AUTH_METHOD_25519.id, + expMetadata: { sign_by_kid: ALICE_AUTH_METHOD_25519.id }, + case: "Simple message ED25519", + }, + { + message: MESSAGE_SIMPLE, + signBy: ALICE_AUTH_METHOD_P256.id, + expMetadata: { sign_by_kid: ALICE_AUTH_METHOD_P256.id }, + case: "Simple message P256", + }, + { + message: MESSAGE_SIMPLE, + signBy: ALICE_AUTH_METHOD_SECP256K1.id, + expMetadata: { sign_by_kid: ALICE_AUTH_METHOD_SECP256K1.id }, + case: "Simple message K256", + }, +])( + "Message.pack-signed works for $case", + async ({ message, signBy, expMetadata }) => { + const didResolver = new ExampleDIDResolver([ALICE_DID_DOC]); + const secretResolver = new ExampleSecretsResolver(ALICE_SECRETS); + + const [signed, metadata] = await message.pack_signed( + signBy, + didResolver, + secretResolver + ); + + expect(typeof signed).toStrictEqual("string"); + expect(metadata).toStrictEqual(expMetadata); + + const [unpacked, _] = await Message.unpack( + signed, + didResolver, + secretResolver, + {} + ); + expect(unpacked.as_value()).toStrictEqual(message.as_value()); + } +); + +test.each([ + { + case: "Signer DID not found", + didResolver: new ExampleDIDResolver([ALICE_DID_DOC]), + secretsResolver: new ExampleSecretsResolver(ALICE_SECRETS), + message: MESSAGE_SIMPLE, + sign_by: "did:example:unknown", + expError: "DID not resolved: Signer did not found", + }, + { + case: "Signer DID not a did", + didResolver: new ExampleDIDResolver([ALICE_DID_DOC]), + secretsResolver: new ExampleSecretsResolver(ALICE_SECRETS), + message: MESSAGE_SIMPLE, + sign_by: "not-a-did", + expError: + "Illegal argument: `sign_from` value is not a valid DID or DID URL", + }, + { + case: "Signer DID URL not found", + didResolver: new ExampleDIDResolver([ALICE_DID_DOC]), + secretsResolver: new ExampleSecretsResolver(ALICE_SECRETS), + message: MESSAGE_SIMPLE, + sign_by: `${ALICE_DID}#unknown`, + expError: "DID URL not found: Signer key id not found in did doc", + }, + { + case: "DIDResolver error", + didResolver: new MockDIDResolver( + [ + () => { + throw Error("Unknown error"); + }, + ], + new ExampleDIDResolver([ALICE_DID_DOC]) + ), + secretsResolver: new ExampleSecretsResolver(ALICE_SECRETS), + message: MESSAGE_SIMPLE, + sign_by: ALICE_DID, + expError: + "Invalid state: Unable resolve signer did: Unable resolve did: Unknown error", + }, + { + case: "SecretsResolver::get_secrets error", + didResolver: new ExampleDIDResolver([ALICE_DID_DOC]), + secretsResolver: new MockSecretsResolver( + [ + () => { + throw Error("Unknown error"); + }, + ], + [], + new ExampleSecretsResolver(ALICE_SECRETS) + ), + message: MESSAGE_SIMPLE, + sign_by: ALICE_DID, + expError: + "Invalid state: Unable get secret: Unable get secret: Unknown error", + }, + { + case: "SecretsResolver::find_secrets error", + didResolver: new ExampleDIDResolver([ALICE_DID_DOC]), + secretsResolver: new MockSecretsResolver( + [], + [ + () => { + throw Error("Unknown error"); + }, + ], + new ExampleSecretsResolver(ALICE_SECRETS) + ), + message: MESSAGE_SIMPLE, + sign_by: ALICE_DID, + expError: + "Invalid state: Unable find secrets: Unable find secrets: Unknown error", + }, +])( + "Message.pack-signed handles $case", + async ({ didResolver, secretsResolver, message, sign_by, expError }) => { + const res = message.pack_signed(sign_by, didResolver, secretsResolver); + await expect(res).rejects.toThrowError(expError); + } +); diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/message/unpack.test.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/message/unpack.test.ts new file mode 100644 index 0000000..99fdc9e --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/message/unpack.test.ts @@ -0,0 +1,113 @@ +import { Message } from "didcomm"; +import { + ALICE_DID_DOC, + BOB_DID_DOC, + BOB_SECRETS, + CHARLIE_DID_DOC, + ExampleDIDResolver, + ExampleSecretsResolver, + IMESSAGE_FROM_PRIOR, + IMESSAGE_MINIMAL, + IMESSAGE_SIMPLE, + PLAINTEXT_FROM_PRIOR, + PLAINTEXT_MSG_MINIMAL, + PLAINTEXT_MSG_SIMPLE, +} from "../test-vectors"; + +test.each([ + { + case: "Minimal", + msg: PLAINTEXT_MSG_MINIMAL, + options: {}, + expMsg: IMESSAGE_MINIMAL, + expMetadata: { + anonymous_sender: false, + authenticated: false, + enc_alg_anon: null, + enc_alg_auth: null, + encrypted: false, + encrypted_from_kid: null, + encrypted_to_kids: null, + from_prior: null, + from_prior_issuer_kid: null, + non_repudiation: false, + re_wrapped_in_forward: false, + sign_alg: null, + sign_from: null, + signed_message: null, + }, + }, + { + case: "Simple", + msg: PLAINTEXT_MSG_SIMPLE, + options: {}, + expMsg: IMESSAGE_SIMPLE, + expMetadata: { + anonymous_sender: false, + authenticated: false, + enc_alg_anon: null, + enc_alg_auth: null, + encrypted: false, + encrypted_from_kid: null, + encrypted_to_kids: null, + from_prior: null, + from_prior_issuer_kid: null, + non_repudiation: false, + re_wrapped_in_forward: false, + sign_alg: null, + sign_from: null, + signed_message: null, + }, + }, + { + case: "FromPrior", + msg: PLAINTEXT_FROM_PRIOR, + options: {}, + expMsg: IMESSAGE_FROM_PRIOR, + expMetadata: { + anonymous_sender: false, + authenticated: false, + enc_alg_anon: null, + enc_alg_auth: null, + encrypted: false, + encrypted_from_kid: null, + encrypted_to_kids: null, + from_prior: { + aud: "123", + exp: 1234, + iat: 123456, + iss: "did:example:charlie", + jti: "dfg", + nbf: 12345, + sub: "did:example:alice", + }, + from_prior_issuer_kid: "did:example:charlie#key-1", + non_repudiation: false, + re_wrapped_in_forward: false, + sign_alg: null, + sign_from: null, + signed_message: null, + }, + }, +])( + "Message.unpack works for $case", + async ({ msg, options, expMsg, expMetadata }) => { + const didResolver = new ExampleDIDResolver([ + ALICE_DID_DOC, + BOB_DID_DOC, + CHARLIE_DID_DOC, + ]); + + const secretsResolver = new ExampleSecretsResolver(BOB_SECRETS); + + const [unpacked, metadata] = await Message.unpack( + msg, + didResolver, + secretsResolver, + options + ); + + expect(unpacked.as_value()).toStrictEqual(expMsg); + expect(metadata).toStrictEqual(expMetadata); + } +); diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/common.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/common.ts new file mode 100644 index 0000000..608e6a4 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/common.ts @@ -0,0 +1,3 @@ +export const ALICE_DID = "did:example:alice"; +export const BOB_DID = "did:example:bob"; +export const CHARLIE_DID = "did:example:charlie"; diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/did_doc/alice.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/did_doc/alice.ts new file mode 100644 index 0000000..32bb07b --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/did_doc/alice.ts @@ -0,0 +1,118 @@ +import { DIDDoc, VerificationMethod } from "didcomm"; + +export const ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519_NOT_IN_SECRET: VerificationMethod = + { + id: "did:example:alice#key-x25519-not-in-secrets-1", + type: "JsonWebKey2020", + controller: "did:example:alice#key-x25519-not-in-secrets-1", + publicKeyJwk: { + crv: "X25519", + kty: "OKP", + x: "avH0O2Y4tqLAq8y9zpianr8ajii5m4F_mICrzNlatXs", + }, + }; + +export const ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519: VerificationMethod = { + id: "did:example:alice#key-x25519-1", + type: "JsonWebKey2020", + controller: "did:example:alice#key-x25519-1", + publicKeyJwk: { + crv: "X25519", + kty: "OKP", + x: "avH0O2Y4tqLAq8y9zpianr8ajii5m4F_mICrzNlatXs", + }, +}; + +export const ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256: VerificationMethod = { + id: "did:example:alice#key-p256-1", + type: "JsonWebKey2020", + controller: "did:example:alice#key-p256-1", + publicKeyJwk: { + crv: "P-256", + kty: "EC", + x: "L0crjMN1g0Ih4sYAJ_nGoHUck2cloltUpUVQDhF2nHE", + y: "SxYgE7CmEJYi7IDhgK5jI4ZiajO8jPRZDldVhqFpYoo", + }, +}; + +export const ALICE_VERIFICATION_METHOD_KEY_AGREEM_P521: VerificationMethod = { + id: "did:example:alice#key-p521-1", + type: "JsonWebKey2020", + controller: "did:example:alice#key-p521-1", + publicKeyJwk: { + crv: "P-521", + kty: "EC", + x: "AHBEVPRhAv-WHDEvxVM9S0px9WxxwHL641Pemgk9sDdxvli9VpKCBdra5gg_4kupBDhz__AlaBgKOC_15J2Byptz", + y: "AciGcHJCD_yMikQvlmqpkBbVqqbg93mMVcgvXBYAQPP-u9AF7adybwZrNfHWCKAQwGF9ugd0Zhg7mLMEszIONFRk", + }, +}; + +export const ALICE_AUTH_METHOD_25519: VerificationMethod = { + id: "did:example:alice#key-1", + type: "JsonWebKey2020", + controller: "did:example:alice#key-1", + publicKeyJwk: { + crv: "Ed25519", + kty: "OKP", + x: "G-boxFB6vOZBu-wXkm-9Lh79I8nf9Z50cILaOgKKGww", + }, +}; + +export const ALICE_AUTH_METHOD_P256: VerificationMethod = { + id: "did:example:alice#key-2", + type: "JsonWebKey2020", + controller: "did:example:alice#key-2", + publicKeyJwk: { + crv: "P-256", + kty: "EC", + x: "2syLh57B-dGpa0F8p1JrO6JU7UUSF6j7qL-vfk1eOoY", + y: "BgsGtI7UPsObMRjdElxLOrgAO9JggNMjOcfzEPox18w", + }, +}; + +export const ALICE_AUTH_METHOD_SECP256K1: VerificationMethod = { + id: "did:example:alice#key-3", + type: "JsonWebKey2020", + controller: "did:example:alice#key-3", + publicKeyJwk: { + crv: "secp256k1", + kty: "EC", + x: "aToW5EaTq5mlAf8C5ECYDSkqsJycrW-e1SQ6_GJcAOk", + y: "JAGX94caA21WKreXwYUaOCYTBMrqaX4KWIlsQZTHWCk", + }, +}; + +export const ALICE_DID_DOC: DIDDoc = { + id: "did:example:alice", + keyAgreement: [ + "did:example:alice#key-x25519-not-in-secrets-1", + "did:example:alice#key-x25519-1", + "did:example:alice#key-p256-1", + "did:example:alice#key-p521-1", + ], + authentication: [ + "did:example:alice#key-1", + "did:example:alice#key-2", + "did:example:alice#key-3", + ], + verificationMethod: [ + ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519_NOT_IN_SECRET, + ALICE_VERIFICATION_METHOD_KEY_AGREEM_X25519, + ALICE_VERIFICATION_METHOD_KEY_AGREEM_P256, + ALICE_VERIFICATION_METHOD_KEY_AGREEM_P521, + ALICE_AUTH_METHOD_25519, + ALICE_AUTH_METHOD_P256, + ALICE_AUTH_METHOD_SECP256K1, + ], + service: [ + { + id: "service1", + type: "DIDCommMessaging", + serviceEndpoint: { + uri: "https://example.com/path", + accept: ["didcomm/v2", "didcomm/aip2;env=rfc587"], + routingKeys: ["did:example:somemediator#somekey"], + }, + }, + ], +}; diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/did_doc/bob.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/did_doc/bob.ts new file mode 100644 index 0000000..f969e05 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/did_doc/bob.ts @@ -0,0 +1,129 @@ +import { DIDDoc, VerificationMethod } from "didcomm"; + +export const BOB_VERIFICATION_METHOD_KEY_AGREEM_X25519_1: VerificationMethod = { + id: "did:example:bob#key-x25519-1", + type: "JsonWebKey2020", + controller: "did:example:bob#key-x25519-1", + publicKeyJwk: { + crv: "X25519", + kty: "OKP", + x: "GDTrI66K0pFfO54tlCSvfjjNapIs44dzpneBgyx0S3E", + }, +}; + +export const BOB_VERIFICATION_METHOD_KEY_AGREEM_X25519_2: VerificationMethod = { + id: "did:example:bob#key-x25519-2", + type: "JsonWebKey2020", + controller: "did:example:bob#key-x25519-2", + publicKeyJwk: { + crv: "X25519", + kty: "OKP", + x: "UT9S3F5ep16KSNBBShU2wh3qSfqYjlasZimn0mB8_VM", + }, +}; + +export const BOB_VERIFICATION_METHOD_KEY_AGREEM_X25519_3: VerificationMethod = { + id: "did:example:bob#key-x25519-3", + type: "JsonWebKey2020", + controller: "did:example:bob#key-x25519-3", + publicKeyJwk: { + crv: "X25519", + kty: "OKP", + x: "82k2BTUiywKv49fKLZa-WwDi8RBf0tB0M8bvSAUQ3yY", + }, +}; + +export const BOB_VERIFICATION_METHOD_KEY_AGREEM_P256_1: VerificationMethod = { + id: "did:example:bob#key-p256-1", + type: "JsonWebKey2020", + controller: "did:example:bob#key-p256-1", + publicKeyJwk: { + crv: "P-256", + kty: "EC", + x: "FQVaTOksf-XsCUrt4J1L2UGvtWaDwpboVlqbKBY2AIo", + y: "6XFB9PYo7dyC5ViJSO9uXNYkxTJWn0d_mqJ__ZYhcNY", + }, +}; +export const BOB_VERIFICATION_METHOD_KEY_AGREEM_P256_2: VerificationMethod = { + id: "did:example:bob#key-p256-2", + type: "JsonWebKey2020", + controller: "did:example:bob#key-p256-2", + publicKeyJwk: { + crv: "P-256", + kty: "EC", + x: "n0yBsGrwGZup9ywKhzD4KoORGicilzIUyfcXb1CSwe0", + y: "ov0buZJ8GHzV128jmCw1CaFbajZoFFmiJDbMrceCXIw", + }, +}; +export const BOB_VERIFICATION_METHOD_KEY_AGREEM_P384_1: VerificationMethod = { + id: "did:example:bob#key-p384-1", + type: "JsonWebKey2020", + controller: "did:example:bob#key-p384-1", + publicKeyJwk: { + crv: "P-384", + kty: "EC", + x: "MvnE_OwKoTcJVfHyTX-DLSRhhNwlu5LNoQ5UWD9Jmgtdxp_kpjsMuTTBnxg5RF_Y", + y: "X_3HJBcKFQEG35PZbEOBn8u9_z8V1F9V1Kv-Vh0aSzmH-y9aOuDJUE3D4Hvmi5l7", + }, +}; +export const BOB_VERIFICATION_METHOD_KEY_AGREEM_P384_2: VerificationMethod = { + id: "did:example:bob#key-p384-2", + type: "JsonWebKey2020", + controller: "did:example:bob#key-p384-2", + publicKeyJwk: { + crv: "P-384", + kty: "EC", + x: "2x3HOTvR8e-Tu6U4UqMd1wUWsNXMD0RgIunZTMcZsS-zWOwDgsrhYVHmv3k_DjV3", + y: "W9LLaBjlWYcXUxOf6ECSfcXKaC3-K9z4hCoP0PS87Q_4ExMgIwxVCXUEB6nf0GDd", + }, +}; +export const BOB_VERIFICATION_METHOD_KEY_AGREEM_P521_1: VerificationMethod = { + id: "did:example:bob#key-p521-1", + type: "JsonWebKey2020", + controller: "did:example:bob#key-p521-1", + publicKeyJwk: { + crv: "P-521", + kty: "EC", + x: "Af9O5THFENlqQbh2Ehipt1Yf4gAd9RCa3QzPktfcgUIFADMc4kAaYVViTaDOuvVS2vMS1KZe0D5kXedSXPQ3QbHi", + y: "ATZVigRQ7UdGsQ9j-omyff6JIeeUv3CBWYsZ0l6x3C_SYqhqVV7dEG-TafCCNiIxs8qeUiXQ8cHWVclqkH4Lo1qH", + }, +}; +export const BOB_VERIFICATION_METHOD_KEY_AGREEM_P521_2: VerificationMethod = { + id: "did:example:bob#key-p521-2", + type: "JsonWebKey2020", + controller: "did:example:bob#key-p521-2", + publicKeyJwk: { + crv: "P-521", + kty: "EC", + x: "ATp_WxCfIK_SriBoStmA0QrJc2pUR1djpen0VdpmogtnKxJbitiPq-HJXYXDKriXfVnkrl2i952MsIOMfD2j0Ots", + y: "AEJipR0Dc-aBZYDqN51SKHYSWs9hM58SmRY1MxgXANgZrPaq1EeGMGOjkbLMEJtBThdjXhkS5VlXMkF0cYhZELiH", + }, +}; + +export const BOB_DID_DOC: DIDDoc = { + id: "did:example:bob", + keyAgreement: [ + "did:example:bob#key-x25519-1", + "did:example:bob#key-x25519-2", + "did:example:bob#key-x25519-3", + "did:example:bob#key-p256-1", + "did:example:bob#key-p256-2", + "did:example:bob#key-p384-1", + "did:example:bob#key-p384-2", + "did:example:bob#key-p521-1", + "did:example:bob#key-p521-2", + ], + authentication: [], + verificationMethod: [ + BOB_VERIFICATION_METHOD_KEY_AGREEM_X25519_1, + BOB_VERIFICATION_METHOD_KEY_AGREEM_X25519_2, + BOB_VERIFICATION_METHOD_KEY_AGREEM_X25519_3, + BOB_VERIFICATION_METHOD_KEY_AGREEM_P256_1, + BOB_VERIFICATION_METHOD_KEY_AGREEM_P256_2, + BOB_VERIFICATION_METHOD_KEY_AGREEM_P384_1, + BOB_VERIFICATION_METHOD_KEY_AGREEM_P384_2, + BOB_VERIFICATION_METHOD_KEY_AGREEM_P521_1, + BOB_VERIFICATION_METHOD_KEY_AGREEM_P521_2, + ], + service: [], +}; diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/did_doc/charlie.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/did_doc/charlie.ts new file mode 100644 index 0000000..134c66b --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/did_doc/charlie.ts @@ -0,0 +1,30 @@ +import { DIDDoc } from "didcomm"; + +export const CHARLIE_DID_DOC: DIDDoc = { + id: "did:example:charlie", + keyAgreement: ["did:example:charlie#key-x25519-1"], + authentication: ["did:example:charlie#key-1"], + verificationMethod: [ + { + id: "did:example:charlie#key-x25519-1", + type: "JsonWebKey2020", + controller: "did:example:charlie#key-x25519-1", + publicKeyJwk: { + crv: "X25519", + kty: "OKP", + x: "nTiVFj7DChMsETDdxd5dIzLAJbSQ4j4UG6ZU1ogLNlw", + }, + }, + { + id: "did:example:charlie#key-1", + type: "JsonWebKey2020", + controller: "did:example:charlie#key-1", + publicKeyJwk: { + crv: "Ed25519", + kty: "OKP", + x: "VDXDwuGKVq91zxU6q7__jLDUq8_C5cuxECgd-1feFTE", + }, + }, + ], + service: [], +}; diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/did_doc/index.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/did_doc/index.ts new file mode 100644 index 0000000..2de041a --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/did_doc/index.ts @@ -0,0 +1,3 @@ +export * from "./alice"; +export * from "./bob"; +export * from "./charlie"; diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/did_resolver.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/did_resolver.ts new file mode 100644 index 0000000..a3ccf52 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/did_resolver.ts @@ -0,0 +1,32 @@ +import { DIDResolver, DIDDoc } from "didcomm"; + +export class ExampleDIDResolver implements DIDResolver { + knownDids: DIDDoc[]; + + constructor(knownDids: DIDDoc[]) { + this.knownDids = knownDids; + } + + async resolve(did: string): Promise { + const res = this.knownDids.find((ddoc) => ddoc.id === did); + return res ? res : null; + } +} + +type MockResolve = (did: string) => DIDDoc | null; + +/* tslint:disable:max-classes-per-file */ +export class MockDIDResolver implements DIDResolver { + handlers: MockResolve[]; + fallback: DIDResolver; + + constructor(handlers: MockResolve[], fallback: DIDResolver) { + this.handlers = handlers; + this.fallback = fallback; + } + + async resolve(did: string): Promise { + const handler = this.handlers.pop(); + return handler ? handler(did) : await this.fallback.resolve(did); + } +} diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/from_prior.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/from_prior.ts new file mode 100644 index 0000000..424882f --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/from_prior.ts @@ -0,0 +1,17 @@ +import { FromPrior, IFromPrior } from "didcomm"; +import { ALICE_DID, BOB_DID, CHARLIE_DID } from "."; + +export const IFROM_PRIOR_MINIMAL: IFromPrior = { + iss: CHARLIE_DID, + sub: ALICE_DID, +}; + +export const FROM_PRIOR_MINIMAL = new FromPrior(IFROM_PRIOR_MINIMAL); + +export const IFROM_PRIOR_FULL = { + iss: CHARLIE_DID, + sub: ALICE_DID, + iat: 123456, +}; + +export const FROM_PRIOR_FULL = new FromPrior(IFROM_PRIOR_FULL); diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/from_prior_jwt.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/from_prior_jwt.ts new file mode 100644 index 0000000..02361a3 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/from_prior_jwt.ts @@ -0,0 +1,7 @@ +export const FROM_PRIOR_JWT_FULL = + "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpleGFtcGxlOmNoYXJsaWUja2V5LTEifQ.eyJpc3MiOiJkaWQ6ZXhhbXBsZTpjaGFybGllIiwic3ViIjoiZGlkOmV4YW1wbGU6YWxpY2UiLCJhdWQiOiIxMjMiLCJleHAiOjEyMzQsIm5iZiI6MTIzNDUsImlhdCI6MTIzNDU2LCJqdGkiOiJkZmcifQ.ir0tegXiGJIZIMagO5P853KwhzGTEw0OpFFAyarUV-nQrtbI_ELbxT9l7jPBoPve_-60ifGJ9v3ArmFjELFlDA"; + +export const FROM_PRIOR_JWT_INVALID = "invalid"; + +export const FROM_PRIOR_JWT_INVALID_SIGNATURE = + "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpleGFtcGxlOmNoYXJsaWUja2V5LTEifQ.eyJpc3MiOiJkaWQ6ZXhhbXBsZTpjaGFybGllIiwic3ViIjoiZGlkOmV4YW1wbGU6YWxpY2UiLCJhdWQiOiIxMjMiLCJleHAiOjEyMzQsIm5iZiI6MTIzNDUsImlhdCI6MTIzNDU2LCJqdGkiOiJkZmcifQ.ir0tegXiGJIZIMagO5P853KwhzGTEw0OpFFAyarUV-nQrtbI_ELbxT9l7jPBoPve_-60ifGJ9v3ArmFjELFlDB"; diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/index.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/index.ts new file mode 100644 index 0000000..128c18c --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/index.ts @@ -0,0 +1,9 @@ +export * from "./common"; +export * from "./did_doc"; +export * from "./did_resolver"; +export * from "./from_prior"; +export * from "./from_prior_jwt"; +export * from "./message"; +export * from "./plaintext"; +export * from "./secrets"; +export * from "./secrets_resolver"; diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/message.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/message.ts new file mode 100644 index 0000000..c3c7e52 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/message.ts @@ -0,0 +1,80 @@ +import { Message } from "didcomm"; + +export const IMESSAGE_SIMPLE = { + id: "1234567890", + typ: "application/didcomm-plain+json", + type: "http://example.com/protocols/lets_do_lunch/1.0/proposal", + from: "did:example:alice", + to: ["did:example:bob"], + created_time: 1516269022, + expires_time: 1516385931, + body: { messagespecificattribute: "and its value" }, +}; + +export const MESSAGE_SIMPLE = new Message(IMESSAGE_SIMPLE); + +export const IMESSAGE_MINIMAL = { + id: "1234567890", + typ: "application/didcomm-plain+json", + type: "http://example.com/protocols/lets_do_lunch/1.0/proposal", + body: {}, +}; + +export const MESSAGE_MINIMAL = new Message(IMESSAGE_MINIMAL); + +export const IMESSAGE_FROM_PRIOR = { + id: "1234567890", + typ: "application/didcomm-plain+json", + type: "http://example.com/protocols/lets_do_lunch/1.0/proposal", + from: "did:example:alice", + to: ["did:example:bob"], + created_time: 1516269022, + expires_time: 1516385931, + from_prior: + "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpleGFtcGxlOmNoYXJsaWUja2V5LTEifQ.eyJpc3MiOiJkaWQ6ZXhhbXBsZTpjaGFybGllIiwic3ViIjoiZGlkOmV4YW1wbGU6YWxpY2UiLCJhdWQiOiIxMjMiLCJleHAiOjEyMzQsIm5iZiI6MTIzNDUsImlhdCI6MTIzNDU2LCJqdGkiOiJkZmcifQ.ir0tegXiGJIZIMagO5P853KwhzGTEw0OpFFAyarUV-nQrtbI_ELbxT9l7jPBoPve_-60ifGJ9v3ArmFjELFlDA", + body: { messagespecificattribute: "and its value" }, +}; + +export const MESSAGE_FROM_PRIOR = new Message(IMESSAGE_FROM_PRIOR); + +export const IFORWARD_MESSAGE = { + id: "8404000a-1c6d-4c8c-8c60-e383128d9677", + typ: "application/didcomm-plain+json", + type: "https://didcomm.org/routing/2.0/forward", + body: { + next: "did:example:bob", + }, + attachments: [ + { + data: { + json: { + ciphertext: + "ajYDBYyuuftb0f-pCj9iz7uhSJFK95F_WsXcXSKN2HrfPdojdRb9Ss_xI0zJnTC97yRmO9vmfyR8-MkQ_1gh-KyEHZe6UTM7JWpSWC9onReNLTOLaMoM09W8Fb45ZFbqaqZ1Kt3qvKIXEu2BwrZ2jLRu7r2Lo-cDJhDwzhHux27gd-j9Dhvtct3B2AMzXdu2J4fLqIdz9h0XkiI3PB4tLYsgY6KwDMtLyePDbb747bqViqWoBFgDLX2zgL3R9Okxt7RG4-1vqRHfURgcONofWMpFHEFq3WaplipogvuwouP3hJv3OMppBz2KTo1ULg3WWAdrac7laa2XQ6UE1PUo6Cq7IH7mdVoZwRc2v__swib6_WLTZMTW", + iv: "hC-Frpywx0Pix6Lak-Rwlpw0IbG28rGo", + protected: + "eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC1FUytBMjU2S1ciLCJlbmMiOiJYQzIwUCIsImFwdiI6Ik5jc3VBbnJSZlBLNjlBLXJrWjBMOVhXVUc0ak12TkMzWmc3NEJQejUzUEEiLCJlcGsiOnsiY3J2IjoiWDI1NTE5Iiwia3R5IjoiT0tQIiwieCI6Ikg0QURobjA0S1VnX1dXQWpiR0s3eTJ3QkQtTmtsbUhXa0lHUl9jeGtKMXcifX0", + recipients: [ + { + encrypted_key: + "Bjk-DOK_2omU_LN13TEGs3WBAwWaimaAQVvtIdE4mmCW83M8kOWKfw", + header: { kid: "did:example:bob#key-x25519-1" }, + }, + { + encrypted_key: + "SuPR0JolzyGPeNiaj9EoD822TsHXRLJbkyQgOnF_MG-DfPdQ5y2Eeg", + header: { kid: "did:example:bob#key-x25519-2" }, + }, + { + encrypted_key: + "6H5qA6Hic0L2B_lzg6q37VbkmHoi8d82seRxswtXp9c1FpTg8cG76w", + header: { kid: "did:example:bob#key-x25519-3" }, + }, + ], + tag: "j4VLGYCa70LhHyDDLUDzKw", + }, + }, + }, + ], +}; + +export const FORWARD_MESSAGE = new Message(IFORWARD_MESSAGE); diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/plaintext.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/plaintext.ts new file mode 100644 index 0000000..0113c79 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/plaintext.ts @@ -0,0 +1,35 @@ +export const PLAINTEXT_MSG_SIMPLE = ` +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "from": "did:example:alice", + "to": ["did:example:bob"], + "created_time": 1516269022, + "expires_time": 1516385931, + "body": {"messagespecificattribute": "and its value"} +} +`; + +export const PLAINTEXT_MSG_MINIMAL = ` +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "body": {} +} +`; + +export const PLAINTEXT_FROM_PRIOR = ` +{ + "id": "1234567890", + "typ": "application/didcomm-plain+json", + "type": "http://example.com/protocols/lets_do_lunch/1.0/proposal", + "from": "did:example:alice", + "to": ["did:example:bob"], + "created_time": 1516269022, + "expires_time": 1516385931, + "from_prior": "eyJ0eXAiOiJKV1QiLCJhbGciOiJFZERTQSIsImtpZCI6ImRpZDpleGFtcGxlOmNoYXJsaWUja2V5LTEifQ.eyJpc3MiOiJkaWQ6ZXhhbXBsZTpjaGFybGllIiwic3ViIjoiZGlkOmV4YW1wbGU6YWxpY2UiLCJhdWQiOiIxMjMiLCJleHAiOjEyMzQsIm5iZiI6MTIzNDUsImlhdCI6MTIzNDU2LCJqdGkiOiJkZmcifQ.ir0tegXiGJIZIMagO5P853KwhzGTEw0OpFFAyarUV-nQrtbI_ELbxT9l7jPBoPve_-60ifGJ9v3ArmFjELFlDA", + "body": {"messagespecificattribute": "and its value"} +} +`; diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets/alice.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets/alice.ts new file mode 100644 index 0000000..214a3d8 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets/alice.ts @@ -0,0 +1,68 @@ +import { Secret } from "didcomm"; + +export const ALICE_SECRETS: Secret[] = [ + { + id: "did:example:alice#key-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "Ed25519", + d: "pFRUKkyzx4kHdJtFSnlPA9WzqkDT1HWV0xZ5OYZd2SY", + kty: "OKP", + x: "G-boxFB6vOZBu-wXkm-9Lh79I8nf9Z50cILaOgKKGww", + }, + }, + { + id: "did:example:alice#key-2", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "P-256", + d: "7TCIdt1rhThFtWcEiLnk_COEjh1ZfQhM4bW2wz-dp4A", + kty: "EC", + x: "2syLh57B-dGpa0F8p1JrO6JU7UUSF6j7qL-vfk1eOoY", + y: "BgsGtI7UPsObMRjdElxLOrgAO9JggNMjOcfzEPox18w", + }, + }, + { + id: "did:example:alice#key-3", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "secp256k1", + d: "N3Hm1LXA210YVGGsXw_GklMwcLu_bMgnzDese6YQIyA", + kty: "EC", + x: "aToW5EaTq5mlAf8C5ECYDSkqsJycrW-e1SQ6_GJcAOk", + y: "JAGX94caA21WKreXwYUaOCYTBMrqaX4KWIlsQZTHWCk", + }, + }, + { + id: "did:example:alice#key-x25519-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "X25519", + d: "r-jK2cO3taR8LQnJB1_ikLBTAnOtShJOsHXRUWT-aZA", + kty: "OKP", + x: "avH0O2Y4tqLAq8y9zpianr8ajii5m4F_mICrzNlatXs", + }, + }, + { + id: "did:example:alice#key-p256-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "P-256", + d: "sB0bYtpaXyp-h17dDpMx91N3Du1AdN4z1FUq02GbmLw", + kty: "EC", + x: "L0crjMN1g0Ih4sYAJ_nGoHUck2cloltUpUVQDhF2nHE", + y: "SxYgE7CmEJYi7IDhgK5jI4ZiajO8jPRZDldVhqFpYoo", + }, + }, + { + id: "did:example:alice#key-p521-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "P-521", + d: "AQCQKE7rZpxPnX9RgjXxeywrAMp1fJsyFe4cir1gWj-8t8xWaM_E2qBkTTzyjbRBu-JPXHe_auT850iYmE34SkWi", + kty: "EC", + x: "AHBEVPRhAv-WHDEvxVM9S0px9WxxwHL641Pemgk9sDdxvli9VpKCBdra5gg_4kupBDhz__AlaBgKOC_15J2Byptz", + y: "AciGcHJCD_yMikQvlmqpkBbVqqbg93mMVcgvXBYAQPP-u9AF7adybwZrNfHWCKAQwGF9ugd0Zhg7mLMEszIONFRk", + }, + }, +]; diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets/bob.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets/bob.ts new file mode 100644 index 0000000..4a87428 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets/bob.ts @@ -0,0 +1,110 @@ +import { Secret } from "didcomm"; + +export const BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1: Secret = { + id: "did:example:bob#key-x25519-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "X25519", + d: "b9NnuOCB0hm7YGNvaE9DMhwH_wjZA1-gWD6dA0JWdL0", + kty: "OKP", + x: "GDTrI66K0pFfO54tlCSvfjjNapIs44dzpneBgyx0S3E", + }, +}; +export const BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2: Secret = { + id: "did:example:bob#key-x25519-2", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "X25519", + d: "p-vteoF1gopny1HXywt76xz_uC83UUmrgszsI-ThBKk", + kty: "OKP", + x: "UT9S3F5ep16KSNBBShU2wh3qSfqYjlasZimn0mB8_VM", + }, +}; +export const BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3: Secret = { + id: "did:example:bob#key-x25519-3", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "X25519", + d: "f9WJeuQXEItkGM8shN4dqFr5fLQLBasHnWZ-8dPaSo0", + kty: "OKP", + x: "82k2BTUiywKv49fKLZa-WwDi8RBf0tB0M8bvSAUQ3yY", + }, +}; +export const BOB_SECRET_KEY_AGREEMENT_KEY_P256_1: Secret = { + id: "did:example:bob#key-p256-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "P-256", + d: "PgwHnlXxt8pwR6OCTUwwWx-P51BiLkFZyqHzquKddXQ", + kty: "EC", + x: "FQVaTOksf-XsCUrt4J1L2UGvtWaDwpboVlqbKBY2AIo", + y: "6XFB9PYo7dyC5ViJSO9uXNYkxTJWn0d_mqJ__ZYhcNY", + }, +}; +export const BOB_SECRET_KEY_AGREEMENT_KEY_P256_2: Secret = { + id: "did:example:bob#key-p256-2", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "P-256", + d: "agKz7HS8mIwqO40Q2dwm_Zi70IdYFtonN5sZecQoxYU", + kty: "EC", + x: "n0yBsGrwGZup9ywKhzD4KoORGicilzIUyfcXb1CSwe0", + y: "ov0buZJ8GHzV128jmCw1CaFbajZoFFmiJDbMrceCXIw", + }, +}; +export const BOB_SECRET_KEY_AGREEMENT_KEY_P384_1: Secret = { + id: "did:example:bob#key-p384-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "P-384", + d: "ajqcWbYA0UDBKfAhkSkeiVjMMt8l-5rcknvEv9t_Os6M8s-HisdywvNCX4CGd_xY", + kty: "EC", + x: "MvnE_OwKoTcJVfHyTX-DLSRhhNwlu5LNoQ5UWD9Jmgtdxp_kpjsMuTTBnxg5RF_Y", + y: "X_3HJBcKFQEG35PZbEOBn8u9_z8V1F9V1Kv-Vh0aSzmH-y9aOuDJUE3D4Hvmi5l7", + }, +}; +export const BOB_SECRET_KEY_AGREEMENT_KEY_P384_2: Secret = { + id: "did:example:bob#key-p384-2", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "P-384", + d: "OiwhRotK188BtbQy0XBO8PljSKYI6CCD-nE_ZUzK7o81tk3imDOuQ-jrSWaIkI-T", + kty: "EC", + x: "2x3HOTvR8e-Tu6U4UqMd1wUWsNXMD0RgIunZTMcZsS-zWOwDgsrhYVHmv3k_DjV3", + y: "W9LLaBjlWYcXUxOf6ECSfcXKaC3-K9z4hCoP0PS87Q_4ExMgIwxVCXUEB6nf0GDd", + }, +}; +export const BOB_SECRET_KEY_AGREEMENT_KEY_P521_1: Secret = { + id: "did:example:bob#key-p521-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "P-521", + d: "AV5ocjvy7PkPgNrSuvCxtG70NMj6iTabvvjSLbsdd8OdI9HlXYlFR7RdBbgLUTruvaIRhjEAE9gNTH6rWUIdfuj6", + kty: "EC", + x: "Af9O5THFENlqQbh2Ehipt1Yf4gAd9RCa3QzPktfcgUIFADMc4kAaYVViTaDOuvVS2vMS1KZe0D5kXedSXPQ3QbHi", + y: "ATZVigRQ7UdGsQ9j-omyff6JIeeUv3CBWYsZ0l6x3C_SYqhqVV7dEG-TafCCNiIxs8qeUiXQ8cHWVclqkH4Lo1qH", + }, +}; +export const BOB_SECRET_KEY_AGREEMENT_KEY_P521_2: Secret = { + id: "did:example:bob#key-p521-2", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "P-521", + d: "ABixMEZHsyT7SRw-lY5HxdNOofTZLlwBHwPEJ3spEMC2sWN1RZQylZuvoyOBGJnPxg4-H_iVhNWf_OtgYODrYhCk", + kty: "EC", + x: "ATp_WxCfIK_SriBoStmA0QrJc2pUR1djpen0VdpmogtnKxJbitiPq-HJXYXDKriXfVnkrl2i952MsIOMfD2j0Ots", + y: "AEJipR0Dc-aBZYDqN51SKHYSWs9hM58SmRY1MxgXANgZrPaq1EeGMGOjkbLMEJtBThdjXhkS5VlXMkF0cYhZELiH", + }, +}; + +export const BOB_SECRETS: Secret[] = [ + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_1, + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_2, + BOB_SECRET_KEY_AGREEMENT_KEY_X25519_3, + BOB_SECRET_KEY_AGREEMENT_KEY_P256_1, + BOB_SECRET_KEY_AGREEMENT_KEY_P256_2, + BOB_SECRET_KEY_AGREEMENT_KEY_P521_1, + BOB_SECRET_KEY_AGREEMENT_KEY_P521_2, + BOB_SECRET_KEY_AGREEMENT_KEY_P384_1, + BOB_SECRET_KEY_AGREEMENT_KEY_P384_2, +]; diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets/charlie.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets/charlie.ts new file mode 100644 index 0000000..c60c1a6 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets/charlie.ts @@ -0,0 +1,28 @@ +import { Secret } from "didcomm"; + +export const CHARLIE_SECRET_KEY_AGREEMENT_KEY_X25519: Secret = { + id: "did:example:charlie#key-x25519-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "X25519", + d: "Z-BsgFe-eCvhuZlCBX5BV2XiDE2M92gkaORCe68YdZI", + kty: "OKP", + x: "nTiVFj7DChMsETDdxd5dIzLAJbSQ4j4UG6ZU1ogLNlw", + }, +}; + +export const CHARLIE_SECRET_AUTH_KEY_ED25519: Secret = { + id: "did:example:charlie#key-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "Ed25519", + d: "T2azVap7CYD_kB8ilbnFYqwwYb5N-GcD6yjGEvquZXg", + kty: "OKP", + x: "VDXDwuGKVq91zxU6q7__jLDUq8_C5cuxECgd-1feFTE", + }, +}; + +export const CHARLIE_SECRETS: Secret[] = [ + CHARLIE_SECRET_KEY_AGREEMENT_KEY_X25519, + CHARLIE_SECRET_AUTH_KEY_ED25519, +]; diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets/charlie_rotated_to_alice.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets/charlie_rotated_to_alice.ts new file mode 100644 index 0000000..e43c1be --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets/charlie_rotated_to_alice.ts @@ -0,0 +1,88 @@ +import { Secret } from "didcomm"; + +export const CHARLIE_ROTATED_TO_ALICE_SECRETS: Secret[] = [ + { + id: "did:example:charlie#key-x25519-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "X25519", + d: "Z-BsgFe-eCvhuZlCBX5BV2XiDE2M92gkaORCe68YdZI", + kty: "OKP", + x: "nTiVFj7DChMsETDdxd5dIzLAJbSQ4j4UG6ZU1ogLNlw", + }, + }, + { + id: "did:example:charlie#key-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "Ed25519", + d: "T2azVap7CYD_kB8ilbnFYqwwYb5N-GcD6yjGEvquZXg", + kty: "OKP", + x: "VDXDwuGKVq91zxU6q7__jLDUq8_C5cuxECgd-1feFTE", + }, + }, + { + id: "did:example:alice#key-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "Ed25519", + d: "pFRUKkyzx4kHdJtFSnlPA9WzqkDT1HWV0xZ5OYZd2SY", + kty: "OKP", + x: "G-boxFB6vOZBu-wXkm-9Lh79I8nf9Z50cILaOgKKGww", + }, + }, + { + id: "did:example:alice#key-2", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "P-256", + d: "7TCIdt1rhThFtWcEiLnk_COEjh1ZfQhM4bW2wz-dp4A", + kty: "EC", + x: "2syLh57B-dGpa0F8p1JrO6JU7UUSF6j7qL-vfk1eOoY", + y: "BgsGtI7UPsObMRjdElxLOrgAO9JggNMjOcfzEPox18w", + }, + }, + { + id: "did:example:alice#key-3", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "secp256k1", + d: "N3Hm1LXA210YVGGsXw_GklMwcLu_bMgnzDese6YQIyA", + kty: "EC", + x: "aToW5EaTq5mlAf8C5ECYDSkqsJycrW-e1SQ6_GJcAOk", + y: "JAGX94caA21WKreXwYUaOCYTBMrqaX4KWIlsQZTHWCk", + }, + }, + { + id: "did:example:alice#key-x25519-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "X25519", + d: "r-jK2cO3taR8LQnJB1_ikLBTAnOtShJOsHXRUWT-aZA", + kty: "OKP", + x: "avH0O2Y4tqLAq8y9zpianr8ajii5m4F_mICrzNlatXs", + }, + }, + { + id: "did:example:alice#key-p256-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "P-256", + d: "sB0bYtpaXyp-h17dDpMx91N3Du1AdN4z1FUq02GbmLw", + kty: "EC", + x: "L0crjMN1g0Ih4sYAJ_nGoHUck2cloltUpUVQDhF2nHE", + y: "SxYgE7CmEJYi7IDhgK5jI4ZiajO8jPRZDldVhqFpYoo", + }, + }, + { + id: "did:example:alice#key-p521-1", + type: "JsonWebKey2020", + privateKeyJwk: { + crv: "P-521", + d: "AQCQKE7rZpxPnX9RgjXxeywrAMp1fJsyFe4cir1gWj-8t8xWaM_E2qBkTTzyjbRBu-JPXHe_auT850iYmE34SkWi", + kty: "EC", + x: "AHBEVPRhAv-WHDEvxVM9S0px9WxxwHL641Pemgk9sDdxvli9VpKCBdra5gg_4kupBDhz__AlaBgKOC_15J2Byptz", + y: "AciGcHJCD_yMikQvlmqpkBbVqqbg93mMVcgvXBYAQPP-u9AF7adybwZrNfHWCKAQwGF9ugd0Zhg7mLMEszIONFRk", + }, + }, +]; diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets/index.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets/index.ts new file mode 100644 index 0000000..d5e5ece --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets/index.ts @@ -0,0 +1,4 @@ +export * from "./alice"; +export * from "./bob"; +export * from "./charlie"; +export * from "./charlie_rotated_to_alice"; diff --git a/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets_resolver.ts b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets_resolver.ts new file mode 100644 index 0000000..ee49e64 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/src/test-vectors/secrets_resolver.ts @@ -0,0 +1,56 @@ +import { Secret, SecretsResolver } from "didcomm"; + +export class ExampleSecretsResolver implements SecretsResolver { + knownSecrets: Secret[]; + + constructor(knownSecrets: Secret[]) { + this.knownSecrets = knownSecrets; + } + + async get_secret(secretId: string): Promise { + const res = this.knownSecrets.find((secret) => secret.id === secretId); + return res ? res : null; + } + + async find_secrets(secretIds: string[]): Promise { + return secretIds.filter((id) => + this.knownSecrets.find((secret) => secret.id === id) + ); + } +} + +type MockGet = (secretId: string) => Secret | null; +type MockFind = (secretIds: string[]) => string[]; + +/* tslint:disable:max-classes-per-file */ +export class MockSecretsResolver implements SecretsResolver { + getHandlers: MockGet[]; + findHandlers: MockFind[]; + fallback: SecretsResolver; + + constructor( + getHandlers: MockGet[], + findHandlers: MockFind[], + fallback: SecretsResolver + ) { + this.getHandlers = getHandlers; + this.findHandlers = findHandlers; + this.fallback = fallback; + } + + async get_secret(secretId: string): Promise { + const handler = this.getHandlers.pop(); + + return handler + ? handler(secretId) + : await this.fallback.get_secret(secretId); + } + + async find_secrets(secretIds: string[]): Promise { + const handler = this.findHandlers.pop(); + + return handler + ? handler(secretIds) + : await this.fallback.find_secrets(secretIds); + } +} diff --git a/affinidi-messaging-didcomm/wasm/tests-js/tslint.json b/affinidi-messaging-didcomm/wasm/tests-js/tslint.json new file mode 100644 index 0000000..9e97bd2 --- /dev/null +++ b/affinidi-messaging-didcomm/wasm/tests-js/tslint.json @@ -0,0 +1,7 @@ +{ + "defaultSeverity": "error", + "extends": ["tslint:recommended"], + "jsRules": {}, + "rules": {}, + "rulesDirectory": [] +} diff --git a/affinidi-messaging-didcomm/wrappers/swift/README.md b/affinidi-messaging-didcomm/wrappers/swift/README.md new file mode 100644 index 0000000..7b12c40 --- /dev/null +++ b/affinidi-messaging-didcomm/wrappers/swift/README.md @@ -0,0 +1,36 @@ +## Creating .a file for iOS. + +Opens [Cargo.toml](./../../uniffi/Cargo.toml) file to change the `create-type`: +``` +[lib] +crate-type = [ + 'lib', + 'staticlib' +] +``` + +Go to UNIFFI folder [here](./../../uniffi/) and run: +```bash +cargo build --release --target aarch64-apple-ios +cargo build --release --target x86_64-apple-ios + +lipo -create target/aarch64-apple-ios/release/libdidcomm_uniffi.a target/x86_64-apple-ios/release/libdidcomm_uniffi.a -output target/libDidcommiOS.a +``` +We created a libDidcommiOS.a that runs both for iOS Simulator (arm64) and iOS Device (aarch64). + +See the available architectures: +``` +rustup target list +``` + +Verify the architecture: +``` +lipo -info target/libDidcommiOS.a +``` +Output: Architectures in the fat file: target/libDidcommiOS.a are: x86_64 arm64 + +### Also reed: + +[Dealing with Rust to build PactSwiftMockServer](https://gist.github.com/surpher/bbf88e191e9d1f01ab2e2bbb85f9b528) +[Cross-compiling for Xcode](https://github.com/thombles/dw2019rust/blob/master/modules/02%20-%20Cross-compiling%20for%20Xcode.md) + diff --git a/affinidi-messaging-didcomm/wrappers/swift/didcomm.swiftdoc b/affinidi-messaging-didcomm/wrappers/swift/didcomm.swiftdoc new file mode 100644 index 0000000000000000000000000000000000000000..5549acc510992849a7735ac1da9fe4e6c6b31701 GIT binary patch literal 604 zcmb79&ubGw6rSCx(P@wvJXRPB%5G+VrcD}Uk!0;j6a)6=LDt<#GqTwY*=^c-(on%e z4<2$6dMN0@L&P9C^bo8J3SNX9`WN^Y^bhF4m(=_L4)bQ-eBXQD_vX*Rr;KzNAr&E% zkt$mC^C|vVUmVo&aTR~D@G-%IQ~Y6wGqtaieL;_8{gnjLxnDtf`q1$lhFK)be^_^I`<#oHBy}t1CFrOJo zNCIlUsu;3byB#v}WSciq(q)~5#}O$RYlcmfWD15}w5iVk!(J&FE5x56_NrwWmQlQ8 zlx)=joqz{^+-jk3kndE=&&sR1H~96}^_*Zv9G<@IyrhrGf3T|Z=xbtu?~+azRN77`xulzSn63BwaF3P{Sl4hiDz zh_qPRjDy6OdQ9UfgjpzHQXv1;4-K=PCQk_LO%}0^m$HC7-z8fSZ#1|8yb0>(YwmXe Np3As-bN+hM_nY*36nLR!8`TzIBvU~fy?=$c8nRjM4-`{i7jgH2I zTtoni-eqh8moJ9;lm?z<(q*Pn}e zUzOZ@#g3QVZzcEKwWAlL+SkAL(eIZf_gV@9XYcJhdhXrP|5|qQ@Q##mdw2AHZ&9D6 zRI}GQCp7zSNxpEev+qSiuIPEyjy~&l^!(G=YsC%(+@FQr=c1nC_j^0~?An3f_4=-G z_Iqnb|7FQN5$HvI>i0q*e=h2C)uP_3l6&oSBEF7Bjmg}>gmiC4NDusO(O}%ng)c_A zns-TcXEke8Z|9m8jkeYU5Ilx7!F^IiMTsvedromdRaA|y(qCL&78TnszMng)r=LD7 z@s<_#We@tfqhtF;N9D06?iiP=pR1oIv0tpa7ftW{;{3ew(o*s~?&l`N<;2DG^-}7- zrQW=9|6I4HpNoaupiy%&3U+pB)AMa=H$CEGT?MY(7>_T(mk=M9AD`!P=OudM-376+ zo_J48Vy@fm&5w8GCFIA%$9X-mE^ll>ey%Se4%fuk=z@=jMR#2?tIxUJKXWj?E)9!* zn0Fq#W&hoZ$4C}(M(`248;<|&u*CX=bcBq-8dRow;z)%|6n?0jphL?p_TE z>8s(k!x}XrBsvUtP6t9xXt?dLVMvf)!f>DHPsl<tD$F?CVd7HLbLfy z*kC?ro?O5&Gy&2f2#OT(6HpPdxHN<_itC9dnVu% zmD(hZRggKT;kLuJbG-`%5x<(bUxLgLO}g1(kD!j%M+JwfbeOrfK&GKa#t!?O>lrOd zklxH)GlY<);8vBB*&I`qleNIk(%3}!aXn<3L8gg#z^^p?NHIaRY6xuBa7T&>?)Bhq zu8Er+_F^(2T{U`w`(?;<*T~pmC%Im4LBy}-yu2!fkPE@B%F6{DQ{`owR6_b`Y!bn( zgMM$wsPfVd+s^g62qJ~uuR*4dhT9H1!S(vtaQ_0Cz8Y>jtQFFP>e;m?Z6V`=jH>S1 zVZ(u!S(tIB7Qa7-Dn&kTfwc$ODD%vd3hVKr!_W-;5tL* z3CO7O(heKI^|}foh23e8c~Zk|ht1@A{cX6bA@h`m+YVd7^?KQG-vOCtH0frCy*7!E zk2HEBxYr@`iAKf_`WK`uO zQcQ3^5AMSn?np7geGuG7G~AJ5g1Z6I?Mn@Jq?q7#f%_{Bcchr$&Ib2U4R@rN;GP5S zV;b&AF~PkM+$X@T>Wl5L0Xgu^)l=&U?u#MQN+V;36>&W~Kg8dVX$x*uy*$k^RlRi2 zAf%nfCSmuvkU1YRs(KkICb(n3-9Zz+9o8imb1;pb;O-8Yei|7&Y%JHao2MOzOf3xJ_s`MV42fwJsCSkV&GBJ=*rFW#5;O+|USPgfinBX1+?l=v1q?q6y5AH;8tLmj4 z*0lh0SB;+F?g5#x8W}q*o9o$?ldmB&5!|YB@+HSq<)lj?=3p9|gx!BZe-dO=@JB}PVd8zSp@F06f&Q3OjW)gnMcT*&{MVhf@*OYAxj~1o_N5oG;Vg-zH;~>pr?wH zpn4B7D>O28SltAlnJpUbmK;-Y&#ZwTAPo2Aka=3eJ(puD?uK)* z&KQQfEo5HQaJS-^iu=-9wC`cKZ-C4$4fnMiQ*l3py7NXD?x!L1He}9FlruYQ=K?~$ zfu3ExELcd$j~W>}>?W>vp~5EdXf0%Z2Dhs2+`%zbd(!?2LVnfQB)E4&{}g1JhzI;i z>)tC14NTQ$9O774$e9nSCypN%Vx3*XeUf7;?$)rI>h%>w{A%Xz1DW3;qiSP&a7>jZ zJr-kLTHih|-(5sVJ;4Q^H4`GjMtH2LsetP4TUE-$aUhmdz6qq5Bo`xbipHF_eM9BT-9A2O=E z{1f^VQMww^z8Do;z5kpAfpyMJKN&L3#RGn&+>v5}>P~P^(QrqK3GOGreTjxUQcQ5a z4eqHL?np7geG=T$G~AJ5g1gD%*yE?+juaEz7l1oQ!yPFmxKqJBL&F^@Cb)CK?bUEc ziV5yz;Lg=>M~VsVt>Dhna7T&>?hnA7ui=gq6Wpi4?bC2aiV5y(w-QneZdLnlhdI#> z&Ve5ONr$gNY?Ockf}X9-ycJ^Ygnv)z*+ zb3X)BeepPsskqy~?yX_CyF%tE$f){DJFNC)LOz6^DlJ7sE`!X+8W}rm6W5CtMEq(_ zlNTWKDY#Yr<kz#^72i%7=+>v5}`wDO$)^JCP3GOxEKBD1{6cgO9fcr}gcchr$9=;R% zT*0ksGwiVMQFnfX9{x#(xn7=v%qa+{+66nT(;hTrtf=0#; zdztIm`3l~H%m8qoEdt|Ln&104rfOqPo{lT3X z#_nXujDd`*?p(|<759C>#)aYD0-1@BQPmwgEb%>zt)Zt%OOYlQLB^+%vBTzZy&-~# zU(M~{Qpgm7TUB=!b4*ot*6$}|w#FtA+!p8;L8iHQU>$QcQ4pqg$f)w$4$F8S>uDN2VfRnqu7ZrJ?i}TqDo*bn!2Z23c7Fw# z`H)f7or4@xai_uVE5dM(h0IluQPmwg?ClS*<^?@fT8cDz4>HR&GIrQG9~ydt1QEZQ z>rNNQtN^#F?zH2Ws_tC#5g}_dHi_Uepnof5RCOm(OmG)~`!)@Cq?q7d1n%24+>v5} z+lh3$3*4%5W``|?-aXK>tCve4vlcR{vSf$d{0ZjV8a-k60dU_38CAV}onxvv{rxfa z_=T~%1;XC~8CAVJlVd9Gn}BT%!@UYJPeDdiFYU0EpJ8nhdaAS(X>u22Uen0fVIOk6 z!Gefi&GqtY$m|BUs$L%En5te@9E7hxW0MHZ`8gr4Lq=6EBgF)FdvNd3a7T&>?qqQ9 z)o@3O3GSKT-lyS?6cgNQ!TpAYJ5o$=_xys8cfhTxA9h$7%E<@NvujWMkog!gsybkY zU3vs_M~$Aa`!R5T0vT0%azDpZak}9!At%Gwy#_MhK}OY{+{G~!_gvV0Dh&5x$ovKw zReNHG?Kq0H+D7WM6lt;(GR-tHcGxdm&m)NV)!d#mI)*h@aI4xA!ZB5Q67w~D1sa<~ zaJ`{_He^)oNu-$IP6Kyy4R@rN;GPZcb2QwMVuJe?aJSHKM~Vq<_i^kq1-GgU+hHE$ zWjpBE)%keHbcBqm&f8%heuK4Ojh?W35x6@+Mpfr)IHrnI8*ujxV|Ope^oNY9&UfRO ziu-+FF=4omKqekCsyc6no&6pB0MJvVrAU(&kQt_tvBNUC-XKB5uft8hXFz5IxX(5{ zqqkIMGRGP#u+`tg$Dy%Fh}S}YBxF=|K2l6@-vaJY8tzCj!QB;c8v|}tS+K)aL2n%N z?CQ?FkeLV>ResxH7yn2|fksc*eGuG}Afu`~`#GkH(+fXfEjWzb??5IOGOD_>k7FwC zfw0>bhI=SviXfw^J9gMDKfw6Is$NEl3GPgAZ`N=}iV5y=aBtCYM~Vq<2hwdT zxK-uM4qFesr=e$8FE>KwS;(l$k{!18FU+SkdcyAG;NA`yRlWR_W2!hc2KQ@W>~05{ z-H=h$%QhTSajyZkFAVo1ka-I-s(NXMCI5|m6wp(prAU)B$b6xZvBPS(p56TN8ps?1 zx2j$)aO4?+JhWK{JsQcQ5W5dKkctMbbZn+?6=(6cMwHIVs66E{0- zr^7*h)#wSkUjp|@$f(Nq6C6{;sR-OZhOv7dWPXN>s(jaQOvU{&uwTM(zXh4!A)_ka zcGyv;gVaL-vMb+TL#Ba7#tyrno}p(qmr8&PKE19i-)@en%6D!92WhRbN#tb?^czD) zRlXy|1oxfbK10JDDJHm|0e2G(cchr${si1jHQbS6f_p}N2RR$usxoYgAun4&&#un< zAagEcROQqTYtz_4`f2op-H(I&Jjkf({05Gx;xwj_gY*bvcRpl#LPk~Rb2z5rZUwvh zhT-lE85d+!b>0r^)YL(SKu?vHB2BtLCRrn6hvjm;BtgWl=CP(9GO6HJ^=Ty>11ud> z|LJB9GFoGk5WXAwX^>IX`A9Lr{S>&RiPYz>uC&*j^8CBgmpJOWSYk^Gv5}dk?s)G~AJ5g8NT!S8KQ<#RPZva~z~b!yPFm zxJQ9|j)pr@OmKe)?s?!=)jd0`1Z8j$^z7QI*^pTb8CCtV!+M|VAnP=G!tUq6eKllM zZPjBOQ^l!O8wa^FjNKPNW));qZB-YJsknQ=?)$=U$3fByr|)h6cgMRf%_#5cchr$o&oM18tzCj!MzyVFKf6X#RT^w;C@BJ9VsTb4}g28 zhC5PBaFg~9@~VbAVvLh_2KQ?k?np6V_egN>(r`zL3GQle@78cfiV5!3;C@}h9VsTb zUjX+W4R@rN;QkWado|pVVuHJ@gM+*UZdKo6hn<5q_H*dj^~G%K13m_D+u2Wj-crsCcJyQ9KzZ-q=($l#xJ z(0+Eun3@F*9NjJz0)5zFi)41LM$s+mN6J?MY0d7@aT*xtGhjh?5HTO8kc#Ta$ z_y*{Ygp8^$juaEz+rT|a!yPFmxQDwjcLleqEZAXZAx*|X&#nx%gv><9sPfwmYaQbt z1sXkJ_hfKSf{d!}jN+IoPV=H2WJVafZ-h)PWK?zMI*zHhTf%N%815*@6hQ|6q=S|- zJM7_D%4w}jz-6f%!P2LGgkwmEiK=fN0T zLrImEf~Xs0_Go16ut{7mRuJ*4xg9Ko%sz0RsgTL#n5rGTCfPxbX>1Zy4?+J;$TSfT zOyS#MySZLZg`Rj+e~5#;qv5v0x^O)^?rD(Oui>`C=5akc?k6Ghfri@-dz0&R6$xNU zOYx{ll7oD#;kLs%bG^=jNWnc7GM{O^P~R<+-098te0RB6(8hJ!o|J-am71O1H}yIDKv#UG57Q=s{Nt16mrb?4*XFAANVbY{4WR7U;?#MBf-BUQ0 zWRsUZ$b6~cp20B{_thM;OA~*UgLG)BE+;cNrb?5;z@ngMS5EGMOh?GTbm9701z4Ylo?V)(gZ@N~ z-S==zW%quL*|mdTKxUGL`(uu&xE-*Y>h-osleO16$TG;N(&P?~snVqTbq;b1^z72) z4d~wt8C5yi%`uhTzjDm(-3cecU#{W)3;GmOad+i{-{j;-WD`UZ!8Kzx32eqMQLsmGmJmR?z2npIg` zUYX_1n>E(wuP&*|iuF}imRCmO|L9VGVQo@UVj6ugqPQ$SGbcN{D!mM(*!Fw6L8lhD z3lh3x#+OxqXOyqXoA0giPDu2XJRQdAV(cbE+B41fmah{j{j43NG8|$m6&M)>> z6_=N}W4(A#TUuW2&-Rt&`zqZDmA;9U-iiue{;;z2^2#~h%6wO9JT-SjG4A5VmE-Dn z#rTU0%SO9g?$`nmlVN^;wXZVM<#r+Tq>{oU1njDr7?Xq$T$K}D?r5sx%OB!H~-%+vsi-r2AI+)ab$7XEPql}l{c@b6bX__6Do*%+!Knu=>^5( z{l#U4$+cC!rPBebSEXnr+QpTX-P5B?&P!>S6pKH$dOsE zuoBgD_4eQBT27GEors)9_GSC>Dt%RcBcD^^{ndGSNQAgbmhZJj%yX(nq|aqM&Eh`% zqrJov&(mx|Om2BTlBuR*36pHzeC${+la!(wZ4`&h3Yx|K2{DyEZ~i}uNDR%)q`aiK zvC!~UCZ&xWnL_QFGKi(@IYrYxppCjIg4UAfjtdk$US??C)>QOl1T9wnefzW!d*h7c zsjJNR4FNbwn@xMCg|RiJ$m=hPDK4ujul2_b#l5d`!lgM?=`pai&6Mq_hNYs`#-|qN zr=a$x#8tD(M(b#Nxsk-4yj)RRQ_Aywnc3MnNl9)?HlQ4PGRuY|ZE0=Ksi;cNhNTX2 z{^)%@sq4g)stFT(mBj_L$|3_%tBcC>CwPkUX~w_9V%P4MpA+dY%s;@K@uv2|e{FVdQc6xzN>%!l$%tjU`R62z3eaSiyJN;y`>RsCB_-Ka z-l}T<1QGePq~zMXe^P`>3i9fKupVjOc&^jMs0D||P20LO(J}=Zt9V-I(5M&%1^e7d z`{W~S6C_=?vi9mSa0hhT=g%22jB%J#$m0o3*>SZMzRd9H)d^&nrz}0=4Z7xJ$}A6U z;8=5oMlaJ7lrL^~btBX?giYY5pWUnKh?2$&*r=P8$eJ8)Ck_DJ#S}cYJ19 zhOf$~4x+I@n?x=ACy77OJDagb{d~sz(?i)^1~H3o9kdCwc41L{;n5qTSXd&TRk0|@ zFM~`y(rxeg8|W}4n0%%(n>fl_Qczx5>dUW1$B>;Tha!-UN4ysYgWhGFso7id~R)kPAkx ztno^E?%32}skGu}RhCzk=arXaxua=kn^odP)8U(2MH>QZ1AuNqG}&kgTydFYtn8cW zemV=T=VzUdhYk*C2OQ{qjdBovaDYiAn!R2}F6ktfPmfEgc7aYo)A)4)O|Lf|e~~(N zg1HoE{M>E~Vqj8vNsX`473XIiTbc`fhATJCl~X}u8*h{|KmAR+<o1YI zmCc;bS6*ytraoZGedjMM)fa%W+ybcpq#+J?#iF9<{Jv6vfjn`$(I38jpc zBrRP->0r@{S-PO(6Ow!&TIeF8t7A63i`j>gCbk$9m+a* zzmyQJc+{lgN}nI&x{O#>x!i7VV8G;Ol`=a<8mo=TPOZ)MWM`NXF=@^i)G73T=E!4U zM~lJ=815vof3`V$svHH;xw>PG-ZfCYT`Ugi(ZlCdSt}XqWoe)4OLu#WA&*g6Xx};> z{IRXZe{Hlj<~~5O%bJhtn1OLIg&pYIoxuvA%acSprcJI5bYFOtio{J?x}&Y6?e@^Y zaG;N%i**i?uQOhNa<&=9y}=gcw$5`zmXS^L!KB?#lA&Dm)nBtM!Za z_ZSSWM+SCm@PS+##OTnPQ>P$5aLw4|@+i&YNt^2}ttj#FHb^v8DT#%mV@)s`tzr4< z@)XPZ;B?+pW2Cf*2DIY*kzSUfqLaF}FA7 zn)@Fnb`P-dQU>)~?vvu77!=f1!!?2hnYO*fm+^9tmg$5iy1Hx*1`1=1`8sM7pRQ-; z)>I_2qL98a^CzPpH>Z=wC5?g64OSL=Oa8$KxHar?l8nZlE97p~G#1m1$g%QAGIEa% zHu`Y0HM;7J@1G#2hb82_o5qsCtP?1&@&9UYJdsh3SY1C(9!f|ZLP9}tiLcCC>NBPz zt67{gr|%v(Bg70-yz8jAjY2YI{y6@?1QwR4o+0%fU3i5ruPPOTFE~k(Ym-V!XiKUs z5r&&3mv+F|0)u4DUi3gwS1AmU4Q%Yd1}SWsP*d>(#Nej$3?OocXd1V>V~3TL_zJxx zNtK1w)U{$BzE5NUP8wd6XcTK7U?iF`G>r34b>Wck*wI`gW8IRn0C+ne3FB zf$in4L+5BKVK!YddhKNvo7}vIVlKDXayZT^&2gdZPBQI{d)HYqDAv;3WQFuL#kRdo zTCf9{4%?=E?|50xoe^qBXA`6=4P8jZocwpqqT!k?+@eJw5hrt*H)B5xT6rH57 z`A_A8LleN_83^l=83c;LA5VX7cm@L1KY=&2jv zJaN8!Pb@s<=8*=vD38mXV7S3(@?&@Q{}#O5oy9_AgeS&2TL@%kGghK%J&$Fp+8Raf z2P^b{a_+-UQafdKwowzL7EbAvw5Ioqh@g{Pdf+p^wTYJMop8)p)jJ~iOJ7~ND@jf# z-8{z?JFBdGPFV_FTcNXX(XnU1omP;y5F}U_>-NYNqun&e#-p9DSxx$yv0T2lb*-iU zRHg@QGmWzIJoM_3`2$9MPG6NbWsg47;nHx&`i*>3LQYIec!0A0nH^=WHa-{1zKu zl84Hg&M&bP^XcMTuit0x0$ae8`7I8{j%6m96NIkbnA%*UVNCgVW6l|1b&xAI`ZgHV z#msM0)~zYt9Pz|v@5j8Ws2e(%a`Cjmqi*L|pC3+Uqi)fX;v|bV&V1=2Nm%0mFt694 z5ic~l$aEJxROchu#S>m@D-TArPV_b`ve(YPedrJ=V5x10j`ro{`@I-3RZzFMJG6;N zG@?PBg${DvvR&&sNNlEIcXY8IMXRO)n?xac{l>-(xFGP4ZG~6{iN-`|acfsRkcr|E8xD+zjOMmRc9jf+P(QH}s{!eD) zO_GV_} zogc)u?kq*xn4z%sIK#1Eq|lj6{H{ZTZ@rj_n`@+0P{S_?(H_A;?*3rR<F=%m#q(TeCiGpd_Yky5RdZMl8wk zY@Fh88Ks2|_3(v?7Ee5Elok=5P1~)j6LimpgRCE&w{f&&lc`S7_AJ_8or_Hy`E*$; z8Y>&o*r^dcv#QwW^*oq&m`0^Zp0W|N9~=tq`eeiF?K_k+Zga-!`}JT^{7(a8lVjLan?~Vt$}(WvSwp47p$w>uu{JH^uS_kqMJK<4C?8ghPPSBk6u0L54llut0r^0di6g>@_5bEb@D7; zU+&|)*nd+|73`yrGK#HJ3&)?&Qb5Gr>UqNpcSznRzB>16^=L7~wf!$giwK8uUN7=+NE9t3HaIn+Y-fm1m+;m4c|6T9u`WSr*NLpF$tZh zkAleZoJeb;lD3g{msJMG4iV1K9WuPLK9tGSxm>8 zUop*B)->3tAC{sCr=obj2NB=7^rHlMrX6S+L#H-@Cb6dCGZxUR1qbux-dDXBCboob zZ!?N?O~rPmyDQ3qmy@tuXzWUaO<<-wPJ__ezuQIVcWtY?+g1caGPCT;m3DiIFPe#JnV1`;;YZC~)|NAk&(b!+8B6oy(yHTzhopC4)Q z6?G&8n;G-u#WSp|##@5c3%ea9r_eYA^?A&`2mhS|#aLr7J5btz$4S08vb&IWfrgzH z+v3z!asX(t*4Diti23ZGn~c2+&zeQaKg~n8!0-xH{1QVfRjj zSpA}n4;@!I$(PP%4tX#aGQVTXF|50ap+4{2lt6dY2ozr)n!H2KcIBFmwah%lLO*)Z z9e-y^>0v)A;KNC&SvF>CVoQ4loevxb<pS|Z1puw zdN9k(ZJx+vYi#yH^4N_@ZaTFw6&^WnsZ)!sC(^u4#QYO`__3SG+La)yeCbGH>i;sb z4V$jm&O)QjqIoxthBtla^KuakI~u(VYWR43{a+W;-Xv(KePc-WwQ%?*xb#?6*=zaa_XT9W6t=5mTfSG;5^Y`{knAzqWQG1SqQTtb(A1@2&n=kJ8 z#abndJ#T>p5_bZvmn<`lcQFFH(t4vA!T0f>zv18o%vR9?OVh$cY`iV@((-0FIqzZN zew*BRN{XbU_o29BWY;eDrMS66myRACWs&@KKyY<8&A!9jjn@z>o( zt085kb&z3lAN7M+6pD_D4^7Jbl39#(`jbH=cqwR5rT1r}37*O-y~ z^1q>(rT-{B>#(X1XF>n^;?w)()=aleGOmifHYBDe_&7U~`}YSXeqpSNi1Mggp@_5k z28|NhmeHJ!Yuec-B|z9FCuDnu{bs<7;4=E<2*^qPymrZQV`wHU4_yQ{CfUhiA0S?u zvbL#4YMAx`CI`d8YwG0BgxEtfS;VB`sM?$mY+C-4C+``St$%IV_f2OhK{ff(nx+5W zWjQ0e$>Pzb7u?JnbF<2!;UtbLdpt2n(opt-9#@U|H5lP=V>`{UIJKV|d!qhx9VI5y z*a*9wjdDVpoBG5#duLPYnBX5Qzx)MdXYu?a@~BhY!WZOEqtb=v-kB;HrEM}jv2)ov zIegAZg_S-Z-92Tjlk+z7Jj(Y(@lbUi6Y6gY7KDG*$IL@8c(ZU?)^UrkA;wrU65h_t zIJ$fqi$(n43mS9^>?95MpLZyiMOrGX*0!?_rW5#b8)@kMF3IQ(&7o00- z*(a7x4>j1l5u^@`-M|vIS^WR@Qb&b6m9v@u1her!)#p|$re{98_e{eLWKJy0p7ijm zU3A9)KH~ZEpUsQ`B_(sa(y>nlFE-#s8unU4@QpFIs}k>$8E;Xdw3NHpyJgrN^v@^y zPK4;hpSx|JDfg1Pd2`5^kgjt}&x8u7`EF{r^1lld*H4UTTx)CBRu2^t5w44US>!_sM z!OUv)*C(fwB(vRsVB&}YUQb?PxpoMP9`xphxLE4?; z*WIL*-At%=+SW4F;v3QK9@5RXzCjE-mFFy0^%|DN+&XB8&tNIr_3RzX3Z#@(k37U; zpDVAttg5mc+nXyjqY>Hg;uh+@Zz_-~oi>B;8t{JlTtdX)fWW9JTgO!;rw; z_T7C_o=3Z_U9oj9FLew!Nbfz~yNW_W)9i?m14bQp(3Z~BE*mRQz6tdGMe5MJ7O;~Hy!(N4+38_!qY_4!!%10z z!AEY1kG))1ZXTD8M_2=^TTYVuSyR2EyDE%+HiMZpsBIUw(KwhZYDmO_R{y8589oe) zq!_$&Yk@om4sijnA$MF1^8v&e8wX5IfQCq@dL%iz%RWnctbXm+9fLQl=|?^I3!q|s z2piSTgF(qp96L2Y${Fi0G1mHeySt*={C;~Jw3EAUyOKHsjp1%~dEDsik>z=_Qbfs2 zOPEt!RYV{ClNFzl?t-~dK;a01oI+}FEYA4vrKa-;c$43 zX{FvXywKW88TFBQS!sKVzZ7=sJzdfyYL9k08*X_reKyPFj9Is>G8}K_f)yCTS=;|x z5xSEM-5r~3xHZg5)`f#$0{o}KI2d*g^$mTH`gKf{wwrIlMSev3B`hMt4$n%G2j9XI z%NK;dhuARpribLwh-Qvt93Ect>N1zlGPi#Y6f=30uF6FjH9B$r@FDvMWXr*%58 zTpo&BCYSC++GZB!m4>DeZL^&uXY-pc$gQrpMkQH^XP@ITQ=D6HgPAjbH<2m zBV_l#sWWwXhLtB{&tuT7lMPBI@y>hsBI_g$tbATjnRN-Iym!ApBN;)Mim39tN8uZc zT4rcrz0H=7goO0L)>a^z)s>=*J^ST~rCjxUjD<4mc$4?!r5;m}6rKcQmqRnAP}1~< z12b?ZJ8>*|T?b|PVisEI5Bs)RMp(+-EY_5GiD{QV`pu0|%vMp~)LY1EAmbzzt=BD+ zOR#>y(;D6hFjS;;IF`dgle#KtwqFMcLvss%jqvfwD-@k+V7IB|!yI2QrE15awDXyT zB0U0&;Jg$1l*!d>oBoQtY8dFH~A6qnKTb2Sz<;t;q^s{qGR2N3Pzp#h)p$2?t?d z1(9#85!IT1dCmS?8u>8w>b5^Oq$65W(`NN(yJLOglUByJ@r*Gief6UkWyrggOkoV> zx)CUSbx3tOL0aZvJA;viFvKl=tqZPWY*^XNV!LEa{eldMO+QE%(Ppoz zR?u%QM$amSM~i+th3#mlO0R&4OE#=-oFGw#^g4mPcKDbJ)a93n`><$Ue@|6`#Tlsi z+M_Vb?V-a=X4lL)Rm?QFl<4OkOTp+QOZWG@MIITOx^3YUf~$@>;>{iS7^pAZf5YS? z7AbS-6Iy{)o$yW#+9*27Eg!$qm9B8fU7*OwU?urabCI_Yz%3&N{z3<_CQ9|5c|JL& zE`yEC#1*}J43bw1lw$zhK8o(-K^m?j`b$DR@{D;6ZtW9Xc|wd{>!{ zd%7bE+hSLpA4!!G4kJ1PbdqPD+rKU;GzOOU$Ns?=oSJ~-nZ!=NhYm=U@5BX*(!VN? zbdASBo?SmYfzH^1B!Q`vwN%{(F(=u+;q1M}DAPPv`KKkb5mI}5hgMrJmV#79E0KL6 z0d<3Z%7qOyjfThgL<04>;%%j)NkbH??qZf1jMRS`>KtKVz3~1Y*AA1-cAJ{Gtgve} zbQd+NL-d1z_+s5uq=l2bl(%5YGKn@Y$#Vbye7|G@Y)7^@}Yn<4W6aPteibnMlq0L3xfu|$0Jj$@0d<1$zYLYxNS z`s4Ijtcbqea?1%h>eAc>m0)~jiE3_>1@+e-jeUuFn1g0$9Us=WyTw;%+OqiU9re_= zon<0*Kl31joMhj$v3=wrW1wCY~~ML@1~cHL(M4lRvF75 zYaoZ0rY5!>8QNO~UPh~fGEEcBN#6VLaSvU0GZiMy0;;^9MpR~3M!fi>Zk%mZ$eWhQ@oc=~5n&x7Kfcb(_qLl&(s zP8@&3GS*a0PTZ!ANwkl856WNE_We;&Le!~lD(C;@4_`dWVt(k3B?+?ULG5#)U(lo0 zKF*?gq)`L6F)j~`$wPdrioZreyUh!bt4BV4_CllQ3#f;DslD$0R0AEK;V+;aEnY)= zT~q4WZL-N_@jSZztj|lCl2mgwz9AF!f617pNTH)A&vaQv;9~r0t#k)j^p5{Np!HBW zdNv;p3fGT}NT;O{f8z4KloQr92Fu!f7g*~cCo-NZ8WfUH@}|-7o=V!kILJ3w&e_#V zib}|wE%--rXp!m)6P)DRlb`)A58{*`AP9a}kQV3D2bkWHdrPK^?l9#0 zHrU&mO1}m7Bg9WV)j!Kxl`R_@yHl}xyG~dtpX#Nh;6{qAP;!u8d&j590}Z?HmeJ{> zll+!6bkY^L3zh+C<0<=_3)H=A{=jhH6VJgn9{I@A?Jn{9t4hn+--QT%29!GC`Cn}? zeD5)%O=F&;eyxT~5vLDx?RzcYf{QfAeIjxQ7>v27X^X4|j zZ^yYvKb-VTl5;uX5~few~GJ{GD_xkK;>R$3ziw8%}XJIukMp z=PsNryBJT)aeWknn>pQt6d@0r?tka^J!pG`V-~KPa30{lji)HHw81eQALDm3I>h5g zAKbGcAAWy?`vw^2rr@N<0$lgv{26DvUW63m{0t{OT3v|o7fyOiMN4~TALCy2|HZ)8 z<7`Gp9{gB@`$qi;DaUC(68amzp2RiFg};}IhGHJhLpVpkr+*{PmvOd=Hfs^G8Na{5 z*$FK}KF-f^(xYQ6AwHb+_yE@@sRtIvm;8EeJic^^b2If~^Wy}+Uq#zG)6p26;ng_l z@h-0P+k2aFw!O%BwuN82QXefp7U2F%obiLi6O>upuQL6n$jyV1_Cta));mA2<0ky9 zB(xDY|G-I)S;@xrLw+@W8ULS~LdX!D;@E`yE~(-d@)h@!)9_tQob=d=Yol~RF2dO$ z!+1vjFUI|wINJ<0o~7VApWpA~*9OCa^5wXH*z}tsU-I7<3`e@-+<{Yd#En4Nz$uPZ zxZj6!;7H?1Ev_5*J$*86l=1vJTp#84$NAMjjQ`Iajq-u>UYzuJonOz(G=3-IT7Yv_ zmhrpk7^Dr(A91FRHJ+*de+!sDn~;+@OUDs%AI>w!6S5ZPPMoTv4LrpQaq5nrIhHgL z3#Hl^9q0f3@kp z%UQ=BapGD3@%Va-zrTN0`K6`a{`tn5Vqf}pXaDN5;(~(W{{8y(rys1UU}AHM3#x>a mxc2kYr{L<+qUUW68W8>O=TG3^JJYzQ&%{B?JkJa9=>Gr@) -> RustBuffer { + try! rustCall { ffi_didcomm_812c_rustbuffer_from_bytes(ForeignBytes(bufferPointer: ptr), $0) } + } + + // Frees the buffer in place. + // The buffer must not be used after this is called. + func deallocate() { + try! rustCall { ffi_didcomm_812c_rustbuffer_free(self, $0) } + } +} + +private extension ForeignBytes { + init(bufferPointer: UnsafeBufferPointer) { + self.init(len: Int32(bufferPointer.count), data: bufferPointer.baseAddress) + } +} + +// For every type used in the interface, we provide helper methods for conveniently +// lifting and lowering that type from C-compatible data, and for reading and writing +// values of that type in a buffer. + +// Helper classes/extensions that don't change. +// Someday, this will be in a libray of its own. + +private extension Data { + init(rustBuffer: RustBuffer) { + // TODO: This copies the buffer. Can we read directly from a + // Rust buffer? + self.init(bytes: rustBuffer.data!, count: Int(rustBuffer.len)) + } +} + +// A helper class to read values out of a byte buffer. +private class Reader { + let data: Data + var offset: Data.Index + + init(data: Data) { + self.data = data + offset = 0 + } + + // Reads an integer at the current offset, in big-endian order, and advances + // the offset on success. Throws if reading the integer would move the + // offset past the end of the buffer. + func readInt() throws -> T { + let range = offset ..< offset + MemoryLayout.size + guard data.count >= range.upperBound else { + throw UniffiInternalError.bufferOverflow + } + if T.self == UInt8.self { + let value = data[offset] + offset += 1 + return value as! T + } + var value: T = 0 + let _ = withUnsafeMutableBytes(of: &value) { data.copyBytes(to: $0, from: range) } + offset = range.upperBound + return value.bigEndian + } + + // Reads an arbitrary number of bytes, to be used to read + // raw bytes, this is useful when lifting strings + func readBytes(count: Int) throws -> [UInt8] { + let range = offset ..< (offset + count) + guard data.count >= range.upperBound else { + throw UniffiInternalError.bufferOverflow + } + var value = [UInt8](repeating: 0, count: count) + value.withUnsafeMutableBufferPointer { buffer in + data.copyBytes(to: buffer, from: range) + } + offset = range.upperBound + return value + } + + // Reads a float at the current offset. + @inlinable + func readFloat() throws -> Float { + return Float(bitPattern: try readInt()) + } + + // Reads a float at the current offset. + @inlinable + func readDouble() throws -> Double { + return Double(bitPattern: try readInt()) + } + + // Indicates if the offset has reached the end of the buffer. + @inlinable + func hasRemaining() -> Bool { + return offset < data.count + } +} + +// A helper class to write values into a byte buffer. +private class Writer { + var bytes: [UInt8] + var offset: Array.Index + + init() { + bytes = [] + offset = 0 + } + + func writeBytes(_ byteArr: S) where S: Sequence, S.Element == UInt8 { + bytes.append(contentsOf: byteArr) + } + + // Writes an integer in big-endian order. + // + // Warning: make sure what you are trying to write + // is in the correct type! + func writeInt(_ value: T) { + var value = value.bigEndian + withUnsafeBytes(of: &value) { bytes.append(contentsOf: $0) } + } + + @inlinable + func writeFloat(_ value: Float) { + writeInt(value.bitPattern) + } + + @inlinable + func writeDouble(_ value: Double) { + writeInt(value.bitPattern) + } +} + +// Types conforming to `Serializable` can be read and written in a bytebuffer. +private protocol Serializable { + func write(into: Writer) + static func read(from: Reader) throws -> Self +} + +// Types confirming to `ViaFfi` can be transferred back-and-for over the FFI. +// This is analogous to the Rust trait of the same name. +private protocol ViaFfi: Serializable { + associatedtype FfiType + static func lift(_ v: FfiType) throws -> Self + func lower() -> FfiType +} + +// Types conforming to `Primitive` pass themselves directly over the FFI. +private protocol Primitive {} + +private extension Primitive { + typealias FfiType = Self + + static func lift(_ v: Self) throws -> Self { + return v + } + + func lower() -> Self { + return self + } +} + +// Types conforming to `ViaFfiUsingByteBuffer` lift and lower into a bytebuffer. +// Use this for complex types where it's hard to write a custom lift/lower. +private protocol ViaFfiUsingByteBuffer: Serializable {} + +private extension ViaFfiUsingByteBuffer { + typealias FfiType = RustBuffer + + static func lift(_ buf: FfiType) throws -> Self { + let reader = Reader(data: Data(rustBuffer: buf)) + let value = try Self.read(from: reader) + if reader.hasRemaining() { + throw UniffiInternalError.incompleteData + } + buf.deallocate() + return value + } + + func lower() -> FfiType { + let writer = Writer() + write(into: writer) + return RustBuffer(bytes: writer.bytes) + } +} + +// An error type for FFI errors. These errors occur at the UniFFI level, not +// the library level. +private enum UniffiInternalError: LocalizedError { + case bufferOverflow + case incompleteData + case unexpectedOptionalTag + case unexpectedEnumCase + case unexpectedNullPointer + case unexpectedRustCallStatusCode + case unexpectedRustCallError + case unexpectedStaleHandle + case rustPanic(_ message: String) + + public var errorDescription: String? { + switch self { + case .bufferOverflow: return "Reading the requested value would read past the end of the buffer" + case .incompleteData: return "The buffer still has data after lifting its containing value" + case .unexpectedOptionalTag: return "Unexpected optional tag; should be 0 or 1" + case .unexpectedEnumCase: return "Raw enum value doesn't match any cases" + case .unexpectedNullPointer: return "Raw pointer value was null" + case .unexpectedRustCallStatusCode: return "Unexpected RustCallStatus code" + case .unexpectedRustCallError: return "CALL_ERROR but no errorClass specified" + case .unexpectedStaleHandle: return "The object in the handle map has been dropped already" + case let .rustPanic(message): return message + } + } +} + +private let CALL_SUCCESS: Int8 = 0 +private let CALL_ERROR: Int8 = 1 +private let CALL_PANIC: Int8 = 2 + +private extension RustCallStatus { + init() { + self.init( + code: CALL_SUCCESS, + errorBuf: RustBuffer( + capacity: 0, + len: 0, + data: nil + ) + ) + } +} + +private func rustCall(_ callback: (UnsafeMutablePointer) -> T) throws -> T { + try makeRustCall(callback, errorHandler: { + $0.deallocate() + return UniffiInternalError.unexpectedRustCallError + }) +} + +private func rustCallWithError(_: E.Type, _ callback: (UnsafeMutablePointer) -> T) throws -> T { + try makeRustCall(callback, errorHandler: { try E.lift($0) }) +} + +private func makeRustCall(_ callback: (UnsafeMutablePointer) -> T, errorHandler: (RustBuffer) throws -> Error) throws -> T { + var callStatus = RustCallStatus() + let returnedVal = callback(&callStatus) + switch callStatus.code { + case CALL_SUCCESS: + return returnedVal + + case CALL_ERROR: + throw try errorHandler(callStatus.errorBuf) + + case CALL_PANIC: + // When the rust code sees a panic, it tries to construct a RustBuffer + // with the message. But if that code panics, then it just sends back + // an empty buffer. + if callStatus.errorBuf.len > 0 { + throw UniffiInternalError.rustPanic(try String.lift(callStatus.errorBuf)) + } else { + callStatus.errorBuf.deallocate() + throw UniffiInternalError.rustPanic("Rust panic") + } + + default: + throw UniffiInternalError.unexpectedRustCallStatusCode + } +} + +// Protocols for converters we'll implement in templates + +private protocol FfiConverter { + associatedtype SwiftType + associatedtype FfiType + + static func lift(_ ffiValue: FfiType) throws -> SwiftType + static func lower(_ value: SwiftType) -> FfiType + + static func read(from: Reader) throws -> SwiftType + static func write(_ value: SwiftType, into: Writer) +} + +private protocol FfiConverterUsingByteBuffer: FfiConverter where FfiType == RustBuffer { + // Empty, because we want to declare some helper methods in the extension below. +} + +extension FfiConverterUsingByteBuffer { + static func lower(_ value: SwiftType) -> FfiType { + let writer = Writer() + Self.write(value, into: writer) + return RustBuffer(bytes: writer.bytes) + } + + static func lift(_ buf: FfiType) throws -> SwiftType { + let reader = Reader(data: Data(rustBuffer: buf)) + let value = try Self.read(from: reader) + if reader.hasRemaining() { + throw UniffiInternalError.incompleteData + } + buf.deallocate() + return value + } +} + +// Helpers for structural types. Note that because of canonical_names, it /should/ be impossible +// to make another `FfiConverterSequence` etc just using the UDL. +private enum FfiConverterSequence { + static func write(_ value: [T], into buf: Writer, writeItem: (T, Writer) -> Void) { + let len = Int32(value.count) + buf.writeInt(len) + for item in value { + writeItem(item, buf) + } + } + + static func read(from buf: Reader, readItem: (Reader) throws -> T) throws -> [T] { + let len: Int32 = try buf.readInt() + var seq = [T]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try readItem(buf)) + } + return seq + } +} + +private enum FfiConverterOptional { + static func write(_ value: T?, into buf: Writer, writeItem: (T, Writer) -> Void) { + guard let value = value else { + buf.writeInt(Int8(0)) + return + } + buf.writeInt(Int8(1)) + writeItem(value, buf) + } + + static func read(from buf: Reader, readItem: (Reader) throws -> T) throws -> T? { + switch try buf.readInt() as Int8 { + case 0: return nil + case 1: return try readItem(buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +private enum FfiConverterDictionary { + static func write(_ value: [String: T], into buf: Writer, writeItem: (String, T, Writer) -> Void) { + let len = Int32(value.count) + buf.writeInt(len) + for (key, value) in value { + writeItem(key, value, buf) + } + } + + static func read(from buf: Reader, readItem: (Reader) throws -> (String, T)) throws -> [String: T] { + let len: Int32 = try buf.readInt() + var dict = [String: T]() + dict.reserveCapacity(Int(len)) + for _ in 0 ..< len { + let (key, value) = try readItem(buf) + dict[key] = value + } + return dict + } +} + +// Public interface members begin here. + +private extension NSLock { + func withLock(f: () throws -> T) rethrows -> T { + lock() + defer { self.unlock() } + return try f() + } +} + +private typealias Handle = UInt64 +private class ConcurrentHandleMap { + private var leftMap: [Handle: T] = [:] + private var counter: [Handle: UInt64] = [:] + private var rightMap: [ObjectIdentifier: Handle] = [:] + + private let lock = NSLock() + private var currentHandle: Handle = 0 + private let stride: Handle = 1 + + func insert(obj: T) -> Handle { + lock.withLock { + let id = ObjectIdentifier(obj as AnyObject) + let handle = rightMap[id] ?? { + currentHandle += stride + let handle = currentHandle + leftMap[handle] = obj + rightMap[id] = handle + return handle + }() + counter[handle] = (counter[handle] ?? 0) + 1 + return handle + } + } + + func get(handle: Handle) -> T? { + lock.withLock { + leftMap[handle] + } + } + + func delete(handle: Handle) { + remove(handle: handle) + } + + @discardableResult + func remove(handle: Handle) -> T? { + lock.withLock { + defer { counter[handle] = (counter[handle] ?? 1) - 1 } + guard counter[handle] == 1 else { return leftMap[handle] } + let obj = leftMap.removeValue(forKey: handle) + if let obj = obj { + rightMap.removeValue(forKey: ObjectIdentifier(obj as AnyObject)) + } + return obj + } + } +} + +// Magic number for the Rust proxy to call using the same mechanism as every other method, +// to free the callback once it's dropped by Rust. +private let IDX_CALLBACK_FREE: Int32 = 0 + +private class FfiConverterCallbackInterface { + fileprivate let handleMap = ConcurrentHandleMap() + + func drop(handle: Handle) { + handleMap.remove(handle: handle) + } + + func lift(_ handle: Handle) throws -> CallbackInterface { + guard let callback = handleMap.get(handle: handle) else { + throw UniffiInternalError.unexpectedStaleHandle + } + return callback + } + + func read(from buf: Reader) throws -> CallbackInterface { + let handle: Handle = try buf.readInt() + return try lift(handle) + } + + func lower(_ v: CallbackInterface) -> Handle { + let handle = handleMap.insert(obj: v) + return handle + // assert(handleMap.get(handle: obj) == v, "Handle map is not returning the object we just placed there. This is a bug in the HandleMap.") + } + + func write(_ v: CallbackInterface, into buf: Writer) { + buf.writeInt(lower(v)) + } +} + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum AttachmentData { + case base64(value: Base64AttachmentData) + case json(value: JsonAttachmentData) + case links(value: LinksAttachmentData) +} + +extension AttachmentData: ViaFfiUsingByteBuffer, ViaFfi { + fileprivate static func read(from buf: Reader) throws -> AttachmentData { + let variant: Int32 = try buf.readInt() + switch variant { + case 1: return .base64( + value: try Base64AttachmentData.read(from: buf) + ) + case 2: return .json( + value: try JsonAttachmentData.read(from: buf) + ) + case 3: return .links( + value: try LinksAttachmentData.read(from: buf) + ) + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + fileprivate func write(into buf: Writer) { + switch self { + case let .base64(value): + buf.writeInt(Int32(1)) + value.write(into: buf) + + case let .json(value): + buf.writeInt(Int32(2)) + value.write(into: buf) + + case let .links(value): + buf.writeInt(Int32(3)) + value.write(into: buf) + } + } +} + +extension AttachmentData: Equatable, Hashable {} + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum ErrorCode { + case success + case error +} + +extension ErrorCode: ViaFfiUsingByteBuffer, ViaFfi { + fileprivate static func read(from buf: Reader) throws -> ErrorCode { + let variant: Int32 = try buf.readInt() + switch variant { + case 1: return .success + case 2: return .error + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + fileprivate func write(into buf: Writer) { + switch self { + case .success: + buf.writeInt(Int32(1)) + + case .error: + buf.writeInt(Int32(2)) + } + } +} + +extension ErrorCode: Equatable, Hashable {} + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum VerificationMaterial { + case jwk(publicKeyJwk: String) + case multibase(publicKeyMultibase: String) + case base58(publicKeyBase58: String) +} + +extension VerificationMaterial: ViaFfiUsingByteBuffer, ViaFfi { + fileprivate static func read(from buf: Reader) throws -> VerificationMaterial { + let variant: Int32 = try buf.readInt() + switch variant { + case 1: return .jwk( + publicKeyJwk: try String.read(from: buf) + ) + case 2: return .multibase( + publicKeyMultibase: try String.read(from: buf) + ) + case 3: return .base58( + publicKeyBase58: try String.read(from: buf) + ) + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + fileprivate func write(into buf: Writer) { + switch self { + case let .jwk(publicKeyJwk): + buf.writeInt(Int32(1)) + publicKeyJwk.write(into: buf) + + case let .multibase(publicKeyMultibase): + buf.writeInt(Int32(2)) + publicKeyMultibase.write(into: buf) + + case let .base58(publicKeyBase58): + buf.writeInt(Int32(3)) + publicKeyBase58.write(into: buf) + } + } +} + +extension VerificationMaterial: Equatable, Hashable {} + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum VerificationMethodType { + case jsonWebKey2020 + case x25519KeyAgreementKey2019 + case ed25519VerificationKey2018 + case ecdsaSecp256k1VerificationKey2019 + case x25519KeyAgreementKey2020 + case ed25519VerificationKey2020 + case other +} + +extension VerificationMethodType: ViaFfiUsingByteBuffer, ViaFfi { + fileprivate static func read(from buf: Reader) throws -> VerificationMethodType { + let variant: Int32 = try buf.readInt() + switch variant { + case 1: return .jsonWebKey2020 + case 2: return .x25519KeyAgreementKey2019 + case 3: return .ed25519VerificationKey2018 + case 4: return .ecdsaSecp256k1VerificationKey2019 + case 5: return .x25519KeyAgreementKey2020 + case 6: return .ed25519VerificationKey2020 + case 7: return .other + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + fileprivate func write(into buf: Writer) { + switch self { + case .jsonWebKey2020: + buf.writeInt(Int32(1)) + + case .x25519KeyAgreementKey2019: + buf.writeInt(Int32(2)) + + case .ed25519VerificationKey2018: + buf.writeInt(Int32(3)) + + case .ecdsaSecp256k1VerificationKey2019: + buf.writeInt(Int32(4)) + + case .x25519KeyAgreementKey2020: + buf.writeInt(Int32(5)) + + case .ed25519VerificationKey2020: + buf.writeInt(Int32(6)) + + case .other: + buf.writeInt(Int32(7)) + } + } +} + +extension VerificationMethodType: Equatable, Hashable {} + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum ServiceKind { + case didCommMessaging(value: DidCommMessagingService) + case other(value: String) +} + +extension ServiceKind: ViaFfiUsingByteBuffer, ViaFfi { + fileprivate static func read(from buf: Reader) throws -> ServiceKind { + let variant: Int32 = try buf.readInt() + switch variant { + case 1: return .didCommMessaging( + value: try DidCommMessagingService.read(from: buf) + ) + case 2: return .other( + value: try String.read(from: buf) + ) + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + fileprivate func write(into buf: Writer) { + switch self { + case let .didCommMessaging(value): + buf.writeInt(Int32(1)) + value.write(into: buf) + + case let .other(value): + buf.writeInt(Int32(2)) + value.write(into: buf) + } + } +} + +extension ServiceKind: Equatable, Hashable {} + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum SecretMaterial { + case jwk(privateKeyJwk: String) + case multibase(privateKeyMultibase: String) + case base58(privateKeyBase58: String) +} + +extension SecretMaterial: ViaFfiUsingByteBuffer, ViaFfi { + fileprivate static func read(from buf: Reader) throws -> SecretMaterial { + let variant: Int32 = try buf.readInt() + switch variant { + case 1: return .jwk( + privateKeyJwk: try String.read(from: buf) + ) + case 2: return .multibase( + privateKeyMultibase: try String.read(from: buf) + ) + case 3: return .base58( + privateKeyBase58: try String.read(from: buf) + ) + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + fileprivate func write(into buf: Writer) { + switch self { + case let .jwk(privateKeyJwk): + buf.writeInt(Int32(1)) + privateKeyJwk.write(into: buf) + + case let .multibase(privateKeyMultibase): + buf.writeInt(Int32(2)) + privateKeyMultibase.write(into: buf) + + case let .base58(privateKeyBase58): + buf.writeInt(Int32(3)) + privateKeyBase58.write(into: buf) + } + } +} + +extension SecretMaterial: Equatable, Hashable {} + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum SecretType { + case jsonWebKey2020 + case x25519KeyAgreementKey2019 + case ed25519VerificationKey2018 + case ecdsaSecp256k1VerificationKey2019 + case x25519KeyAgreementKey2020 + case ed25519VerificationKey2020 + case other +} + +extension SecretType: ViaFfiUsingByteBuffer, ViaFfi { + fileprivate static func read(from buf: Reader) throws -> SecretType { + let variant: Int32 = try buf.readInt() + switch variant { + case 1: return .jsonWebKey2020 + case 2: return .x25519KeyAgreementKey2019 + case 3: return .ed25519VerificationKey2018 + case 4: return .ecdsaSecp256k1VerificationKey2019 + case 5: return .x25519KeyAgreementKey2020 + case 6: return .ed25519VerificationKey2020 + case 7: return .other + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + fileprivate func write(into buf: Writer) { + switch self { + case .jsonWebKey2020: + buf.writeInt(Int32(1)) + + case .x25519KeyAgreementKey2019: + buf.writeInt(Int32(2)) + + case .ed25519VerificationKey2018: + buf.writeInt(Int32(3)) + + case .ecdsaSecp256k1VerificationKey2019: + buf.writeInt(Int32(4)) + + case .x25519KeyAgreementKey2020: + buf.writeInt(Int32(5)) + + case .ed25519VerificationKey2020: + buf.writeInt(Int32(6)) + + case .other: + buf.writeInt(Int32(7)) + } + } +} + +extension SecretType: Equatable, Hashable {} + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum AuthCryptAlg { + case a256cbcHs512Ecdh1puA256kw +} + +extension AuthCryptAlg: ViaFfiUsingByteBuffer, ViaFfi { + fileprivate static func read(from buf: Reader) throws -> AuthCryptAlg { + let variant: Int32 = try buf.readInt() + switch variant { + case 1: return .a256cbcHs512Ecdh1puA256kw + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + fileprivate func write(into buf: Writer) { + switch self { + case .a256cbcHs512Ecdh1puA256kw: + buf.writeInt(Int32(1)) + } + } +} + +extension AuthCryptAlg: Equatable, Hashable {} + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum AnonCryptAlg { + case a256cbcHs512EcdhEsA256kw + case xc20pEcdhEsA256kw + case a256gcmEcdhEsA256kw +} + +extension AnonCryptAlg: ViaFfiUsingByteBuffer, ViaFfi { + fileprivate static func read(from buf: Reader) throws -> AnonCryptAlg { + let variant: Int32 = try buf.readInt() + switch variant { + case 1: return .a256cbcHs512EcdhEsA256kw + case 2: return .xc20pEcdhEsA256kw + case 3: return .a256gcmEcdhEsA256kw + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + fileprivate func write(into buf: Writer) { + switch self { + case .a256cbcHs512EcdhEsA256kw: + buf.writeInt(Int32(1)) + + case .xc20pEcdhEsA256kw: + buf.writeInt(Int32(2)) + + case .a256gcmEcdhEsA256kw: + buf.writeInt(Int32(3)) + } + } +} + +extension AnonCryptAlg: Equatable, Hashable {} + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum SignAlg { + case edDsa + case es256 + case es256k +} + +extension SignAlg: ViaFfiUsingByteBuffer, ViaFfi { + fileprivate static func read(from buf: Reader) throws -> SignAlg { + let variant: Int32 = try buf.readInt() + switch variant { + case 1: return .edDsa + case 2: return .es256 + case 3: return .es256k + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + fileprivate func write(into buf: Writer) { + switch self { + case .edDsa: + buf.writeInt(Int32(1)) + + case .es256: + buf.writeInt(Int32(2)) + + case .es256k: + buf.writeInt(Int32(3)) + } + } +} + +extension SignAlg: Equatable, Hashable {} + +public protocol DIDCommProtocol { + func packPlaintext(msg: Message, cb: OnPackPlaintextResult) -> ErrorCode + func packSigned(msg: Message, signBy: String, cb: OnPackSignedResult) -> ErrorCode + func packEncrypted(msg: Message, to: String, from: String?, signBy: String?, options: PackEncryptedOptions, cb: OnPackEncryptedResult) -> ErrorCode + func unpack(msg: String, options: UnpackOptions, cb: OnUnpackResult) -> ErrorCode + func packFromPrior(msg: FromPrior, issuerKid: String?, cb: OnFromPriorPackResult) -> ErrorCode + func unpackFromPrior(fromPriorJwt: String, cb: OnFromPriorUnpackResult) -> ErrorCode + func wrapInForward(msg: String, headers: [String: String], to: String, routingKeys: [String], encAlgAnon: AnonCryptAlg, cb: OnWrapInForwardResult) -> ErrorCode +} + +public class DidComm: DIDCommProtocol { + fileprivate let pointer: UnsafeMutableRawPointer + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `ViaFfi` without making this `required` and we can't + // make it `required` without making it `public`. + required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + public convenience init(didResolver: DidResolver, secretResolver: SecretsResolver) { + self.init(unsafeFromRawPointer: try! + + rustCall { + didcomm_812c_DIDComm_new(ffiConverterCallbackInterfaceDidResolver.lower(didResolver), ffiConverterCallbackInterfaceSecretsResolver.lower(secretResolver), $0) + }) + } + + deinit { + try! rustCall { ffi_didcomm_812c_DIDComm_object_free(pointer, $0) } + } + + public func packPlaintext(msg: Message, cb: OnPackPlaintextResult) -> ErrorCode { + let _retval = try! + rustCall { + didcomm_812c_DIDComm_pack_plaintext(self.pointer, msg.lower(), ffiConverterCallbackInterfaceOnPackPlaintextResult.lower(cb), $0) + } + return try! ErrorCode.lift(_retval) + } + + public func packSigned(msg: Message, signBy: String, cb: OnPackSignedResult) -> ErrorCode { + let _retval = try! + rustCall { + didcomm_812c_DIDComm_pack_signed(self.pointer, msg.lower(), signBy.lower(), ffiConverterCallbackInterfaceOnPackSignedResult.lower(cb), $0) + } + return try! ErrorCode.lift(_retval) + } + + public func packEncrypted(msg: Message, to: String, from: String?, signBy: String?, options: PackEncryptedOptions, cb: OnPackEncryptedResult) -> ErrorCode { + let _retval = try! + rustCall { + didcomm_812c_DIDComm_pack_encrypted(self.pointer, msg.lower(), to.lower(), FfiConverterOptionString.lower(from), FfiConverterOptionString.lower(signBy), options.lower(), ffiConverterCallbackInterfaceOnPackEncryptedResult.lower(cb), $0) + } + return try! ErrorCode.lift(_retval) + } + + public func unpack(msg: String, options: UnpackOptions, cb: OnUnpackResult) -> ErrorCode { + let _retval = try! + rustCall { + didcomm_812c_DIDComm_unpack(self.pointer, msg.lower(), options.lower(), ffiConverterCallbackInterfaceOnUnpackResult.lower(cb), $0) + } + return try! ErrorCode.lift(_retval) + } + + public func packFromPrior(msg: FromPrior, issuerKid: String?, cb: OnFromPriorPackResult) -> ErrorCode { + let _retval = try! + rustCall { + didcomm_812c_DIDComm_pack_from_prior(self.pointer, msg.lower(), FfiConverterOptionString.lower(issuerKid), ffiConverterCallbackInterfaceOnFromPriorPackResult.lower(cb), $0) + } + return try! ErrorCode.lift(_retval) + } + + public func unpackFromPrior(fromPriorJwt: String, cb: OnFromPriorUnpackResult) -> ErrorCode { + let _retval = try! + rustCall { + didcomm_812c_DIDComm_unpack_from_prior(self.pointer, fromPriorJwt.lower(), ffiConverterCallbackInterfaceOnFromPriorUnpackResult.lower(cb), $0) + } + return try! ErrorCode.lift(_retval) + } + + public func wrapInForward(msg: String, headers: [String: String], to: String, routingKeys: [String], encAlgAnon: AnonCryptAlg, cb: OnWrapInForwardResult) -> ErrorCode { + let _retval = try! + rustCall { + didcomm_812c_DIDComm_wrap_in_forward(self.pointer, msg.lower(), FfiConverterDictionaryJsonValue.lower(headers), to.lower(), FfiConverterSequenceString.lower(routingKeys), encAlgAnon.lower(), ffiConverterCallbackInterfaceOnWrapInForwardResult.lower(cb), $0) + } + return try! ErrorCode.lift(_retval) + } +} + +private extension DidComm { + typealias FfiType = UnsafeMutableRawPointer + + static func read(from buf: Reader) throws -> Self { + let v: UInt64 = try buf.readInt() + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if ptr == nil { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + func write(into buf: Writer) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + buf.writeInt(UInt64(bitPattern: Int64(Int(bitPattern: lower())))) + } + + static func lift(_ pointer: UnsafeMutableRawPointer) throws -> Self { + return Self(unsafeFromRawPointer: pointer) + } + + func lower() -> UnsafeMutableRawPointer { + return pointer + } +} + +// Ideally this would be `fileprivate`, but Swift says: +// """ +// 'private' modifier cannot be used with extensions that declare protocol conformances +// """ +extension DidComm: ViaFfi, Serializable {} + +public protocol OnDIDResolverResultProtocol { + func success(result: DidDoc?) throws + func error(err: ErrorKind, msg: String) throws +} + +public class OnDidResolverResult: OnDIDResolverResultProtocol { + fileprivate let pointer: UnsafeMutableRawPointer + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `ViaFfi` without making this `required` and we can't + // make it `required` without making it `public`. + required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + deinit { + try! rustCall { ffi_didcomm_812c_OnDIDResolverResult_object_free(pointer, $0) } + } + + public func success(result: DidDoc?) throws { + try + rustCallWithError(ErrorKind.self) { + didcomm_812c_OnDIDResolverResult_success(self.pointer, FfiConverterOptionRecordDidDoc.lower(result), $0) + } + } + + public func error(err: ErrorKind, msg: String) throws { + try + rustCallWithError(ErrorKind.self) { + didcomm_812c_OnDIDResolverResult_error(self.pointer, err.lower(), msg.lower(), $0) + } + } +} + +private extension OnDidResolverResult { + typealias FfiType = UnsafeMutableRawPointer + + static func read(from buf: Reader) throws -> Self { + let v: UInt64 = try buf.readInt() + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if ptr == nil { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + func write(into buf: Writer) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + buf.writeInt(UInt64(bitPattern: Int64(Int(bitPattern: lower())))) + } + + static func lift(_ pointer: UnsafeMutableRawPointer) throws -> Self { + return Self(unsafeFromRawPointer: pointer) + } + + func lower() -> UnsafeMutableRawPointer { + return pointer + } +} + +// Ideally this would be `fileprivate`, but Swift says: +// """ +// 'private' modifier cannot be used with extensions that declare protocol conformances +// """ +extension OnDidResolverResult: ViaFfi, Serializable {} + +public protocol ExampleDIDResolverProtocol { + func resolve(did: String, cb: OnDidResolverResult) -> ErrorCode +} + +public class ExampleDidResolver: ExampleDIDResolverProtocol { + fileprivate let pointer: UnsafeMutableRawPointer + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `ViaFfi` without making this `required` and we can't + // make it `required` without making it `public`. + required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + public convenience init(knownDids: [DidDoc]) { + self.init(unsafeFromRawPointer: try! + + rustCall { + didcomm_812c_ExampleDIDResolver_new(FfiConverterSequenceRecordDidDoc.lower(knownDids), $0) + }) + } + + deinit { + try! rustCall { ffi_didcomm_812c_ExampleDIDResolver_object_free(pointer, $0) } + } + + public func resolve(did: String, cb: OnDidResolverResult) -> ErrorCode { + let _retval = try! + rustCall { + didcomm_812c_ExampleDIDResolver_resolve(self.pointer, did.lower(), cb.lower(), $0) + } + return try! ErrorCode.lift(_retval) + } +} + +private extension ExampleDidResolver { + typealias FfiType = UnsafeMutableRawPointer + + static func read(from buf: Reader) throws -> Self { + let v: UInt64 = try buf.readInt() + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if ptr == nil { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + func write(into buf: Writer) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + buf.writeInt(UInt64(bitPattern: Int64(Int(bitPattern: lower())))) + } + + static func lift(_ pointer: UnsafeMutableRawPointer) throws -> Self { + return Self(unsafeFromRawPointer: pointer) + } + + func lower() -> UnsafeMutableRawPointer { + return pointer + } +} + +// Ideally this would be `fileprivate`, but Swift says: +// """ +// 'private' modifier cannot be used with extensions that declare protocol conformances +// """ +extension ExampleDidResolver: ViaFfi, Serializable {} + +public protocol OnGetSecretResultProtocol { + func success(result: Secret?) throws + func error(err: ErrorKind, msg: String) throws +} + +public class OnGetSecretResult: OnGetSecretResultProtocol { + fileprivate let pointer: UnsafeMutableRawPointer + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `ViaFfi` without making this `required` and we can't + // make it `required` without making it `public`. + required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + deinit { + try! rustCall { ffi_didcomm_812c_OnGetSecretResult_object_free(pointer, $0) } + } + + public func success(result: Secret?) throws { + try + rustCallWithError(ErrorKind.self) { + didcomm_812c_OnGetSecretResult_success(self.pointer, FfiConverterOptionRecordSecret.lower(result), $0) + } + } + + public func error(err: ErrorKind, msg: String) throws { + try + rustCallWithError(ErrorKind.self) { + didcomm_812c_OnGetSecretResult_error(self.pointer, err.lower(), msg.lower(), $0) + } + } +} + +private extension OnGetSecretResult { + typealias FfiType = UnsafeMutableRawPointer + + static func read(from buf: Reader) throws -> Self { + let v: UInt64 = try buf.readInt() + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if ptr == nil { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + func write(into buf: Writer) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + buf.writeInt(UInt64(bitPattern: Int64(Int(bitPattern: lower())))) + } + + static func lift(_ pointer: UnsafeMutableRawPointer) throws -> Self { + return Self(unsafeFromRawPointer: pointer) + } + + func lower() -> UnsafeMutableRawPointer { + return pointer + } +} + +// Ideally this would be `fileprivate`, but Swift says: +// """ +// 'private' modifier cannot be used with extensions that declare protocol conformances +// """ +extension OnGetSecretResult: ViaFfi, Serializable {} + +public protocol OnFindSecretsResultProtocol { + func success(result: [String]) throws + func error(err: ErrorKind, msg: String) throws +} + +public class OnFindSecretsResult: OnFindSecretsResultProtocol { + fileprivate let pointer: UnsafeMutableRawPointer + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `ViaFfi` without making this `required` and we can't + // make it `required` without making it `public`. + required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + deinit { + try! rustCall { ffi_didcomm_812c_OnFindSecretsResult_object_free(pointer, $0) } + } + + public func success(result: [String]) throws { + try + rustCallWithError(ErrorKind.self) { + didcomm_812c_OnFindSecretsResult_success(self.pointer, FfiConverterSequenceString.lower(result), $0) + } + } + + public func error(err: ErrorKind, msg: String) throws { + try + rustCallWithError(ErrorKind.self) { + didcomm_812c_OnFindSecretsResult_error(self.pointer, err.lower(), msg.lower(), $0) + } + } +} + +private extension OnFindSecretsResult { + typealias FfiType = UnsafeMutableRawPointer + + static func read(from buf: Reader) throws -> Self { + let v: UInt64 = try buf.readInt() + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if ptr == nil { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + func write(into buf: Writer) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + buf.writeInt(UInt64(bitPattern: Int64(Int(bitPattern: lower())))) + } + + static func lift(_ pointer: UnsafeMutableRawPointer) throws -> Self { + return Self(unsafeFromRawPointer: pointer) + } + + func lower() -> UnsafeMutableRawPointer { + return pointer + } +} + +// Ideally this would be `fileprivate`, but Swift says: +// """ +// 'private' modifier cannot be used with extensions that declare protocol conformances +// """ +extension OnFindSecretsResult: ViaFfi, Serializable {} + +public protocol ExampleSecretsResolverProtocol { + func getSecret(secretId: String, cb: OnGetSecretResult) -> ErrorCode + func findSecrets(secretIds: [String], cb: OnFindSecretsResult) -> ErrorCode +} + +public class ExampleSecretsResolver: ExampleSecretsResolverProtocol { + fileprivate let pointer: UnsafeMutableRawPointer + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `ViaFfi` without making this `required` and we can't + // make it `required` without making it `public`. + required init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + public convenience init(knownSecrets: [Secret]) { + self.init(unsafeFromRawPointer: try! + + rustCall { + didcomm_812c_ExampleSecretsResolver_new(FfiConverterSequenceRecordSecret.lower(knownSecrets), $0) + }) + } + + deinit { + try! rustCall { ffi_didcomm_812c_ExampleSecretsResolver_object_free(pointer, $0) } + } + + public func getSecret(secretId: String, cb: OnGetSecretResult) -> ErrorCode { + let _retval = try! + rustCall { + didcomm_812c_ExampleSecretsResolver_get_secret(self.pointer, secretId.lower(), cb.lower(), $0) + } + return try! ErrorCode.lift(_retval) + } + + public func findSecrets(secretIds: [String], cb: OnFindSecretsResult) -> ErrorCode { + let _retval = try! + rustCall { + didcomm_812c_ExampleSecretsResolver_find_secrets(self.pointer, FfiConverterSequenceString.lower(secretIds), cb.lower(), $0) + } + return try! ErrorCode.lift(_retval) + } +} + +private extension ExampleSecretsResolver { + typealias FfiType = UnsafeMutableRawPointer + + static func read(from buf: Reader) throws -> Self { + let v: UInt64 = try buf.readInt() + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if ptr == nil { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + func write(into buf: Writer) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + buf.writeInt(UInt64(bitPattern: Int64(Int(bitPattern: lower())))) + } + + static func lift(_ pointer: UnsafeMutableRawPointer) throws -> Self { + return Self(unsafeFromRawPointer: pointer) + } + + func lower() -> UnsafeMutableRawPointer { + return pointer + } +} + +// Ideally this would be `fileprivate`, but Swift says: +// """ +// 'private' modifier cannot be used with extensions that declare protocol conformances +// """ +extension ExampleSecretsResolver: ViaFfi, Serializable {} + +public struct Message { + public var id: String + public var typ: String + public var type: String + public var body: String + public var from: String? + public var to: [String]? + public var thid: String? + public var pthid: String? + public var extraHeaders: [String: String] + public var createdTime: UInt64? + public var expiresTime: UInt64? + public var fromPrior: String? + public var attachments: [Attachment]? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(id: String, typ: String, type: String, body: String, from: String?, to: [String]?, thid: String?, pthid: String?, extraHeaders: [String: String], createdTime: UInt64?, expiresTime: UInt64?, fromPrior: String?, attachments: [Attachment]?) { + self.id = id + self.typ = typ + self.type = type + self.body = body + self.from = from + self.to = to + self.thid = thid + self.pthid = pthid + self.extraHeaders = extraHeaders + self.createdTime = createdTime + self.expiresTime = expiresTime + self.fromPrior = fromPrior + self.attachments = attachments + } +} + +extension Message: Equatable, Hashable { + public static func == (lhs: Message, rhs: Message) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.typ != rhs.typ { + return false + } + if lhs.type != rhs.type { + return false + } + if lhs.body != rhs.body { + return false + } + if lhs.from != rhs.from { + return false + } + if lhs.to != rhs.to { + return false + } + if lhs.thid != rhs.thid { + return false + } + if lhs.pthid != rhs.pthid { + return false + } + if lhs.extraHeaders != rhs.extraHeaders { + return false + } + if lhs.createdTime != rhs.createdTime { + return false + } + if lhs.expiresTime != rhs.expiresTime { + return false + } + if lhs.fromPrior != rhs.fromPrior { + return false + } + if lhs.attachments != rhs.attachments { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(typ) + hasher.combine(type) + hasher.combine(body) + hasher.combine(from) + hasher.combine(to) + hasher.combine(thid) + hasher.combine(pthid) + hasher.combine(extraHeaders) + hasher.combine(createdTime) + hasher.combine(expiresTime) + hasher.combine(fromPrior) + hasher.combine(attachments) + } +} + +private extension Message { + static func read(from buf: Reader) throws -> Message { + return try Message( + id: String.read(from: buf), + typ: String.read(from: buf), + type: String.read(from: buf), + body: String.read(from: buf), + from: FfiConverterOptionString.read(from: buf), + to: FfiConverterOptionSequenceString.read(from: buf), + thid: FfiConverterOptionString.read(from: buf), + pthid: FfiConverterOptionString.read(from: buf), + extraHeaders: FfiConverterDictionaryJsonValue.read(from: buf), + createdTime: FfiConverterOptionUInt64.read(from: buf), + expiresTime: FfiConverterOptionUInt64.read(from: buf), + fromPrior: FfiConverterOptionString.read(from: buf), + attachments: FfiConverterOptionSequenceRecordAttachment.read(from: buf) + ) + } + + func write(into buf: Writer) { + id.write(into: buf) + typ.write(into: buf) + type.write(into: buf) + body.write(into: buf) + FfiConverterOptionString.write(from, into: buf) + FfiConverterOptionSequenceString.write(to, into: buf) + FfiConverterOptionString.write(thid, into: buf) + FfiConverterOptionString.write(pthid, into: buf) + FfiConverterDictionaryJsonValue.write(extraHeaders, into: buf) + FfiConverterOptionUInt64.write(createdTime, into: buf) + FfiConverterOptionUInt64.write(expiresTime, into: buf) + FfiConverterOptionString.write(fromPrior, into: buf) + FfiConverterOptionSequenceRecordAttachment.write(attachments, into: buf) + } +} + +extension Message: ViaFfiUsingByteBuffer, ViaFfi {} + +public struct Attachment { + public var data: AttachmentData + public var id: String? + public var description: String? + public var filename: String? + public var mediaType: String? + public var format: String? + public var lastmodTime: UInt64? + public var byteCount: UInt64? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(data: AttachmentData, id: String?, description: String?, filename: String?, mediaType: String?, format: String?, lastmodTime: UInt64?, byteCount: UInt64?) { + self.data = data + self.id = id + self.description = description + self.filename = filename + self.mediaType = mediaType + self.format = format + self.lastmodTime = lastmodTime + self.byteCount = byteCount + } +} + +extension Attachment: Equatable, Hashable { + public static func == (lhs: Attachment, rhs: Attachment) -> Bool { + if lhs.data != rhs.data { + return false + } + if lhs.id != rhs.id { + return false + } + if lhs.description != rhs.description { + return false + } + if lhs.filename != rhs.filename { + return false + } + if lhs.mediaType != rhs.mediaType { + return false + } + if lhs.format != rhs.format { + return false + } + if lhs.lastmodTime != rhs.lastmodTime { + return false + } + if lhs.byteCount != rhs.byteCount { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(data) + hasher.combine(id) + hasher.combine(description) + hasher.combine(filename) + hasher.combine(mediaType) + hasher.combine(format) + hasher.combine(lastmodTime) + hasher.combine(byteCount) + } +} + +private extension Attachment { + static func read(from buf: Reader) throws -> Attachment { + return try Attachment( + data: AttachmentData.read(from: buf), + id: FfiConverterOptionString.read(from: buf), + description: FfiConverterOptionString.read(from: buf), + filename: FfiConverterOptionString.read(from: buf), + mediaType: FfiConverterOptionString.read(from: buf), + format: FfiConverterOptionString.read(from: buf), + lastmodTime: FfiConverterOptionUInt64.read(from: buf), + byteCount: FfiConverterOptionUInt64.read(from: buf) + ) + } + + func write(into buf: Writer) { + data.write(into: buf) + FfiConverterOptionString.write(id, into: buf) + FfiConverterOptionString.write(description, into: buf) + FfiConverterOptionString.write(filename, into: buf) + FfiConverterOptionString.write(mediaType, into: buf) + FfiConverterOptionString.write(format, into: buf) + FfiConverterOptionUInt64.write(lastmodTime, into: buf) + FfiConverterOptionUInt64.write(byteCount, into: buf) + } +} + +extension Attachment: ViaFfiUsingByteBuffer, ViaFfi {} + +public struct Base64AttachmentData { + public var base64: String + public var jws: String? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(base64: String, jws: String?) { + self.base64 = base64 + self.jws = jws + } +} + +extension Base64AttachmentData: Equatable, Hashable { + public static func == (lhs: Base64AttachmentData, rhs: Base64AttachmentData) -> Bool { + if lhs.base64 != rhs.base64 { + return false + } + if lhs.jws != rhs.jws { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(base64) + hasher.combine(jws) + } +} + +private extension Base64AttachmentData { + static func read(from buf: Reader) throws -> Base64AttachmentData { + return try Base64AttachmentData( + base64: String.read(from: buf), + jws: FfiConverterOptionString.read(from: buf) + ) + } + + func write(into buf: Writer) { + base64.write(into: buf) + FfiConverterOptionString.write(jws, into: buf) + } +} + +extension Base64AttachmentData: ViaFfiUsingByteBuffer, ViaFfi {} + +public struct JsonAttachmentData { + public var json: String + public var jws: String? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(json: String, jws: String?) { + self.json = json + self.jws = jws + } +} + +extension JsonAttachmentData: Equatable, Hashable { + public static func == (lhs: JsonAttachmentData, rhs: JsonAttachmentData) -> Bool { + if lhs.json != rhs.json { + return false + } + if lhs.jws != rhs.jws { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(json) + hasher.combine(jws) + } +} + +private extension JsonAttachmentData { + static func read(from buf: Reader) throws -> JsonAttachmentData { + return try JsonAttachmentData( + json: String.read(from: buf), + jws: FfiConverterOptionString.read(from: buf) + ) + } + + func write(into buf: Writer) { + json.write(into: buf) + FfiConverterOptionString.write(jws, into: buf) + } +} + +extension JsonAttachmentData: ViaFfiUsingByteBuffer, ViaFfi {} + +public struct LinksAttachmentData { + public var links: [String] + public var hash: String + public var jws: String? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(links: [String], hash: String, jws: String?) { + self.links = links + self.hash = hash + self.jws = jws + } +} + +extension LinksAttachmentData: Equatable, Hashable { + public static func == (lhs: LinksAttachmentData, rhs: LinksAttachmentData) -> Bool { + if lhs.links != rhs.links { + return false + } + if lhs.hash != rhs.hash { + return false + } + if lhs.jws != rhs.jws { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(links) + hasher.combine(hash) + hasher.combine(jws) + } +} + +private extension LinksAttachmentData { + static func read(from buf: Reader) throws -> LinksAttachmentData { + return try LinksAttachmentData( + links: FfiConverterSequenceString.read(from: buf), + hash: String.read(from: buf), + jws: FfiConverterOptionString.read(from: buf) + ) + } + + func write(into buf: Writer) { + FfiConverterSequenceString.write(links, into: buf) + hash.write(into: buf) + FfiConverterOptionString.write(jws, into: buf) + } +} + +extension LinksAttachmentData: ViaFfiUsingByteBuffer, ViaFfi {} + +public struct DidDoc { + public var id: String + public var keyAgreement: [String] + public var authentication: [String] + public var verificationMethod: [VerificationMethod] + public var service: [Service] + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(id: String, keyAgreement: [String], authentication: [String], verificationMethod: [VerificationMethod], service: [Service]) { + self.id = id + self.keyAgreement = keyAgreement + self.authentication = authentication + self.verificationMethod = verificationMethod + self.service = service + } +} + +extension DidDoc: Equatable, Hashable { + public static func == (lhs: DidDoc, rhs: DidDoc) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.keyAgreement != rhs.keyAgreement { + return false + } + if lhs.authentication != rhs.authentication { + return false + } + if lhs.verificationMethod != rhs.verificationMethod { + return false + } + if lhs.service != rhs.service { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(keyAgreement) + hasher.combine(authentication) + hasher.combine(verificationMethod) + hasher.combine(service) + } +} + +private extension DidDoc { + static func read(from buf: Reader) throws -> DidDoc { + return try DidDoc( + id: String.read(from: buf), + keyAgreement: FfiConverterSequenceString.read(from: buf), + authentication: FfiConverterSequenceString.read(from: buf), + verificationMethod: FfiConverterSequenceRecordVerificationMethod.read(from: buf), + service: FfiConverterSequenceRecordService.read(from: buf) + ) + } + + func write(into buf: Writer) { + id.write(into: buf) + FfiConverterSequenceString.write(keyAgreement, into: buf) + FfiConverterSequenceString.write(authentication, into: buf) + FfiConverterSequenceRecordVerificationMethod.write(verificationMethod, into: buf) + FfiConverterSequenceRecordService.write(service, into: buf) + } +} + +extension DidDoc: ViaFfiUsingByteBuffer, ViaFfi {} + +public struct VerificationMethod { + public var id: String + public var type: VerificationMethodType + public var controller: String + public var verificationMaterial: VerificationMaterial + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(id: String, type: VerificationMethodType, controller: String, verificationMaterial: VerificationMaterial) { + self.id = id + self.type = type + self.controller = controller + self.verificationMaterial = verificationMaterial + } +} + +extension VerificationMethod: Equatable, Hashable { + public static func == (lhs: VerificationMethod, rhs: VerificationMethod) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.type != rhs.type { + return false + } + if lhs.controller != rhs.controller { + return false + } + if lhs.verificationMaterial != rhs.verificationMaterial { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(type) + hasher.combine(controller) + hasher.combine(verificationMaterial) + } +} + +private extension VerificationMethod { + static func read(from buf: Reader) throws -> VerificationMethod { + return try VerificationMethod( + id: String.read(from: buf), + type: VerificationMethodType.read(from: buf), + controller: String.read(from: buf), + verificationMaterial: VerificationMaterial.read(from: buf) + ) + } + + func write(into buf: Writer) { + id.write(into: buf) + type.write(into: buf) + controller.write(into: buf) + verificationMaterial.write(into: buf) + } +} + +extension VerificationMethod: ViaFfiUsingByteBuffer, ViaFfi {} + +public struct Service { + public var id: String + public var serviceEndpoint: ServiceKind + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(id: String, serviceEndpoint: ServiceKind) { + self.id = id + self.serviceEndpoint = serviceEndpoint + } +} + +extension Service: Equatable, Hashable { + public static func == (lhs: Service, rhs: Service) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.serviceEndpoint != rhs.serviceEndpoint { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(serviceEndpoint) + } +} + +private extension Service { + static func read(from buf: Reader) throws -> Service { + return try Service( + id: String.read(from: buf), + serviceEndpoint: ServiceKind.read(from: buf) + ) + } + + func write(into buf: Writer) { + id.write(into: buf) + serviceEndpoint.write(into: buf) + } +} + +extension Service: ViaFfiUsingByteBuffer, ViaFfi {} + +public struct DidCommMessagingService { + public var uri: String + public var accept: [String]? + public var routingKeys: [String] + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(uri: String, accept: [String]?, routingKeys: [String]) { + self.uri = uri + self.accept = accept + self.routingKeys = routingKeys + } +} + +extension DidCommMessagingService: Equatable, Hashable { + public static func == (lhs: DidCommMessagingService, rhs: DidCommMessagingService) -> Bool { + if lhs.uri != rhs.uri { + return false + } + if lhs.accept != rhs.accept { + return false + } + if lhs.routingKeys != rhs.routingKeys { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(uri) + hasher.combine(accept) + hasher.combine(routingKeys) + } +} + +private extension DidCommMessagingService { + static func read(from buf: Reader) throws -> DidCommMessagingService { + return try DidCommMessagingService( + uri: String.read(from: buf), + accept: FfiConverterOptionSequenceString.read(from: buf), + routingKeys: FfiConverterSequenceString.read(from: buf) + ) + } + + func write(into buf: Writer) { + uri.write(into: buf) + FfiConverterOptionSequenceString.write(accept, into: buf) + FfiConverterSequenceString.write(routingKeys, into: buf) + } +} + +extension DidCommMessagingService: ViaFfiUsingByteBuffer, ViaFfi {} + +public struct Secret { + public var id: String + public var type: SecretType + public var secretMaterial: SecretMaterial + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(id: String, type: SecretType, secretMaterial: SecretMaterial) { + self.id = id + self.type = type + self.secretMaterial = secretMaterial + } +} + +extension Secret: Equatable, Hashable { + public static func == (lhs: Secret, rhs: Secret) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.type != rhs.type { + return false + } + if lhs.secretMaterial != rhs.secretMaterial { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(type) + hasher.combine(secretMaterial) + } +} + +private extension Secret { + static func read(from buf: Reader) throws -> Secret { + return try Secret( + id: String.read(from: buf), + type: SecretType.read(from: buf), + secretMaterial: SecretMaterial.read(from: buf) + ) + } + + func write(into buf: Writer) { + id.write(into: buf) + type.write(into: buf) + secretMaterial.write(into: buf) + } +} + +extension Secret: ViaFfiUsingByteBuffer, ViaFfi {} + +public struct PackSignedMetadata { + public var signByKid: String + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(signByKid: String) { + self.signByKid = signByKid + } +} + +extension PackSignedMetadata: Equatable, Hashable { + public static func == (lhs: PackSignedMetadata, rhs: PackSignedMetadata) -> Bool { + if lhs.signByKid != rhs.signByKid { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(signByKid) + } +} + +private extension PackSignedMetadata { + static func read(from buf: Reader) throws -> PackSignedMetadata { + return try PackSignedMetadata( + signByKid: String.read(from: buf) + ) + } + + func write(into buf: Writer) { + signByKid.write(into: buf) + } +} + +extension PackSignedMetadata: ViaFfiUsingByteBuffer, ViaFfi {} + +public struct PackEncryptedMetadata { + public var messagingService: MessagingServiceMetadata? + public var fromKid: String? + public var signByKid: String? + public var toKids: [String] + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(messagingService: MessagingServiceMetadata?, fromKid: String?, signByKid: String?, toKids: [String]) { + self.messagingService = messagingService + self.fromKid = fromKid + self.signByKid = signByKid + self.toKids = toKids + } +} + +extension PackEncryptedMetadata: Equatable, Hashable { + public static func == (lhs: PackEncryptedMetadata, rhs: PackEncryptedMetadata) -> Bool { + if lhs.messagingService != rhs.messagingService { + return false + } + if lhs.fromKid != rhs.fromKid { + return false + } + if lhs.signByKid != rhs.signByKid { + return false + } + if lhs.toKids != rhs.toKids { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(messagingService) + hasher.combine(fromKid) + hasher.combine(signByKid) + hasher.combine(toKids) + } +} + +private extension PackEncryptedMetadata { + static func read(from buf: Reader) throws -> PackEncryptedMetadata { + return try PackEncryptedMetadata( + messagingService: FfiConverterOptionRecordMessagingServiceMetadata.read(from: buf), + fromKid: FfiConverterOptionString.read(from: buf), + signByKid: FfiConverterOptionString.read(from: buf), + toKids: FfiConverterSequenceString.read(from: buf) + ) + } + + func write(into buf: Writer) { + FfiConverterOptionRecordMessagingServiceMetadata.write(messagingService, into: buf) + FfiConverterOptionString.write(fromKid, into: buf) + FfiConverterOptionString.write(signByKid, into: buf) + FfiConverterSequenceString.write(toKids, into: buf) + } +} + +extension PackEncryptedMetadata: ViaFfiUsingByteBuffer, ViaFfi {} + +public struct MessagingServiceMetadata { + public var id: String + public var serviceEndpoint: String + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(id: String, serviceEndpoint: String) { + self.id = id + self.serviceEndpoint = serviceEndpoint + } +} + +extension MessagingServiceMetadata: Equatable, Hashable { + public static func == (lhs: MessagingServiceMetadata, rhs: MessagingServiceMetadata) -> Bool { + if lhs.id != rhs.id { + return false + } + if lhs.serviceEndpoint != rhs.serviceEndpoint { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(id) + hasher.combine(serviceEndpoint) + } +} + +private extension MessagingServiceMetadata { + static func read(from buf: Reader) throws -> MessagingServiceMetadata { + return try MessagingServiceMetadata( + id: String.read(from: buf), + serviceEndpoint: String.read(from: buf) + ) + } + + func write(into buf: Writer) { + id.write(into: buf) + serviceEndpoint.write(into: buf) + } +} + +extension MessagingServiceMetadata: ViaFfiUsingByteBuffer, ViaFfi {} + +public struct PackEncryptedOptions { + public var protectSender: Bool + public var forward: Bool + public var forwardHeaders: [String: String]? + public var messagingService: String? + public var encAlgAuth: AuthCryptAlg + public var encAlgAnon: AnonCryptAlg + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(protectSender: Bool, forward: Bool, forwardHeaders: [String: String]?, messagingService: String?, encAlgAuth: AuthCryptAlg, encAlgAnon: AnonCryptAlg) { + self.protectSender = protectSender + self.forward = forward + self.forwardHeaders = forwardHeaders + self.messagingService = messagingService + self.encAlgAuth = encAlgAuth + self.encAlgAnon = encAlgAnon + } +} + +extension PackEncryptedOptions: Equatable, Hashable { + public static func == (lhs: PackEncryptedOptions, rhs: PackEncryptedOptions) -> Bool { + if lhs.protectSender != rhs.protectSender { + return false + } + if lhs.forward != rhs.forward { + return false + } + if lhs.forwardHeaders != rhs.forwardHeaders { + return false + } + if lhs.messagingService != rhs.messagingService { + return false + } + if lhs.encAlgAuth != rhs.encAlgAuth { + return false + } + if lhs.encAlgAnon != rhs.encAlgAnon { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(protectSender) + hasher.combine(forward) + hasher.combine(forwardHeaders) + hasher.combine(messagingService) + hasher.combine(encAlgAuth) + hasher.combine(encAlgAnon) + } +} + +private extension PackEncryptedOptions { + static func read(from buf: Reader) throws -> PackEncryptedOptions { + return try PackEncryptedOptions( + protectSender: Bool.read(from: buf), + forward: Bool.read(from: buf), + forwardHeaders: FfiConverterOptionDictionaryJsonValue.read(from: buf), + messagingService: FfiConverterOptionString.read(from: buf), + encAlgAuth: AuthCryptAlg.read(from: buf), + encAlgAnon: AnonCryptAlg.read(from: buf) + ) + } + + func write(into buf: Writer) { + protectSender.write(into: buf) + forward.write(into: buf) + FfiConverterOptionDictionaryJsonValue.write(forwardHeaders, into: buf) + FfiConverterOptionString.write(messagingService, into: buf) + encAlgAuth.write(into: buf) + encAlgAnon.write(into: buf) + } +} + +extension PackEncryptedOptions: ViaFfiUsingByteBuffer, ViaFfi {} + +public struct UnpackMetadata { + public var encrypted: Bool + public var authenticated: Bool + public var nonRepudiation: Bool + public var anonymousSender: Bool + public var reWrappedInForward: Bool + public var encryptedFromKid: String? + public var encryptedToKids: [String]? + public var signFrom: String? + public var fromPriorIssuerKid: String? + public var encAlgAuth: AuthCryptAlg? + public var encAlgAnon: AnonCryptAlg? + public var signAlg: SignAlg? + public var signedMessage: String? + public var fromPrior: FromPrior? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(encrypted: Bool, authenticated: Bool, nonRepudiation: Bool, anonymousSender: Bool, reWrappedInForward: Bool, encryptedFromKid: String?, encryptedToKids: [String]?, signFrom: String?, fromPriorIssuerKid: String?, encAlgAuth: AuthCryptAlg?, encAlgAnon: AnonCryptAlg?, signAlg: SignAlg?, signedMessage: String?, fromPrior: FromPrior?) { + self.encrypted = encrypted + self.authenticated = authenticated + self.nonRepudiation = nonRepudiation + self.anonymousSender = anonymousSender + self.reWrappedInForward = reWrappedInForward + self.encryptedFromKid = encryptedFromKid + self.encryptedToKids = encryptedToKids + self.signFrom = signFrom + self.fromPriorIssuerKid = fromPriorIssuerKid + self.encAlgAuth = encAlgAuth + self.encAlgAnon = encAlgAnon + self.signAlg = signAlg + self.signedMessage = signedMessage + self.fromPrior = fromPrior + } +} + +extension UnpackMetadata: Equatable, Hashable { + public static func == (lhs: UnpackMetadata, rhs: UnpackMetadata) -> Bool { + if lhs.encrypted != rhs.encrypted { + return false + } + if lhs.authenticated != rhs.authenticated { + return false + } + if lhs.nonRepudiation != rhs.nonRepudiation { + return false + } + if lhs.anonymousSender != rhs.anonymousSender { + return false + } + if lhs.reWrappedInForward != rhs.reWrappedInForward { + return false + } + if lhs.encryptedFromKid != rhs.encryptedFromKid { + return false + } + if lhs.encryptedToKids != rhs.encryptedToKids { + return false + } + if lhs.signFrom != rhs.signFrom { + return false + } + if lhs.fromPriorIssuerKid != rhs.fromPriorIssuerKid { + return false + } + if lhs.encAlgAuth != rhs.encAlgAuth { + return false + } + if lhs.encAlgAnon != rhs.encAlgAnon { + return false + } + if lhs.signAlg != rhs.signAlg { + return false + } + if lhs.signedMessage != rhs.signedMessage { + return false + } + if lhs.fromPrior != rhs.fromPrior { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(encrypted) + hasher.combine(authenticated) + hasher.combine(nonRepudiation) + hasher.combine(anonymousSender) + hasher.combine(reWrappedInForward) + hasher.combine(encryptedFromKid) + hasher.combine(encryptedToKids) + hasher.combine(signFrom) + hasher.combine(fromPriorIssuerKid) + hasher.combine(encAlgAuth) + hasher.combine(encAlgAnon) + hasher.combine(signAlg) + hasher.combine(signedMessage) + hasher.combine(fromPrior) + } +} + +private extension UnpackMetadata { + static func read(from buf: Reader) throws -> UnpackMetadata { + return try UnpackMetadata( + encrypted: Bool.read(from: buf), + authenticated: Bool.read(from: buf), + nonRepudiation: Bool.read(from: buf), + anonymousSender: Bool.read(from: buf), + reWrappedInForward: Bool.read(from: buf), + encryptedFromKid: FfiConverterOptionString.read(from: buf), + encryptedToKids: FfiConverterOptionSequenceString.read(from: buf), + signFrom: FfiConverterOptionString.read(from: buf), + fromPriorIssuerKid: FfiConverterOptionString.read(from: buf), + encAlgAuth: FfiConverterOptionEnumAuthCryptAlg.read(from: buf), + encAlgAnon: FfiConverterOptionEnumAnonCryptAlg.read(from: buf), + signAlg: FfiConverterOptionEnumSignAlg.read(from: buf), + signedMessage: FfiConverterOptionString.read(from: buf), + fromPrior: FfiConverterOptionRecordFromPrior.read(from: buf) + ) + } + + func write(into buf: Writer) { + encrypted.write(into: buf) + authenticated.write(into: buf) + nonRepudiation.write(into: buf) + anonymousSender.write(into: buf) + reWrappedInForward.write(into: buf) + FfiConverterOptionString.write(encryptedFromKid, into: buf) + FfiConverterOptionSequenceString.write(encryptedToKids, into: buf) + FfiConverterOptionString.write(signFrom, into: buf) + FfiConverterOptionString.write(fromPriorIssuerKid, into: buf) + FfiConverterOptionEnumAuthCryptAlg.write(encAlgAuth, into: buf) + FfiConverterOptionEnumAnonCryptAlg.write(encAlgAnon, into: buf) + FfiConverterOptionEnumSignAlg.write(signAlg, into: buf) + FfiConverterOptionString.write(signedMessage, into: buf) + FfiConverterOptionRecordFromPrior.write(fromPrior, into: buf) + } +} + +extension UnpackMetadata: ViaFfiUsingByteBuffer, ViaFfi {} + +public struct UnpackOptions { + public var expectDecryptByAllKeys: Bool + public var unwrapReWrappingForward: Bool + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(expectDecryptByAllKeys: Bool, unwrapReWrappingForward: Bool) { + self.expectDecryptByAllKeys = expectDecryptByAllKeys + self.unwrapReWrappingForward = unwrapReWrappingForward + } +} + +extension UnpackOptions: Equatable, Hashable { + public static func == (lhs: UnpackOptions, rhs: UnpackOptions) -> Bool { + if lhs.expectDecryptByAllKeys != rhs.expectDecryptByAllKeys { + return false + } + if lhs.unwrapReWrappingForward != rhs.unwrapReWrappingForward { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(expectDecryptByAllKeys) + hasher.combine(unwrapReWrappingForward) + } +} + +private extension UnpackOptions { + static func read(from buf: Reader) throws -> UnpackOptions { + return try UnpackOptions( + expectDecryptByAllKeys: Bool.read(from: buf), + unwrapReWrappingForward: Bool.read(from: buf) + ) + } + + func write(into buf: Writer) { + expectDecryptByAllKeys.write(into: buf) + unwrapReWrappingForward.write(into: buf) + } +} + +extension UnpackOptions: ViaFfiUsingByteBuffer, ViaFfi {} + +public struct FromPrior { + public var iss: String + public var sub: String + public var aud: String? + public var exp: UInt64? + public var nbf: UInt64? + public var iat: UInt64? + public var jti: String? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(iss: String, sub: String, aud: String?, exp: UInt64?, nbf: UInt64?, iat: UInt64?, jti: String?) { + self.iss = iss + self.sub = sub + self.aud = aud + self.exp = exp + self.nbf = nbf + self.iat = iat + self.jti = jti + } +} + +extension FromPrior: Equatable, Hashable { + public static func == (lhs: FromPrior, rhs: FromPrior) -> Bool { + if lhs.iss != rhs.iss { + return false + } + if lhs.sub != rhs.sub { + return false + } + if lhs.aud != rhs.aud { + return false + } + if lhs.exp != rhs.exp { + return false + } + if lhs.nbf != rhs.nbf { + return false + } + if lhs.iat != rhs.iat { + return false + } + if lhs.jti != rhs.jti { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(iss) + hasher.combine(sub) + hasher.combine(aud) + hasher.combine(exp) + hasher.combine(nbf) + hasher.combine(iat) + hasher.combine(jti) + } +} + +private extension FromPrior { + static func read(from buf: Reader) throws -> FromPrior { + return try FromPrior( + iss: String.read(from: buf), + sub: String.read(from: buf), + aud: FfiConverterOptionString.read(from: buf), + exp: FfiConverterOptionUInt64.read(from: buf), + nbf: FfiConverterOptionUInt64.read(from: buf), + iat: FfiConverterOptionUInt64.read(from: buf), + jti: FfiConverterOptionString.read(from: buf) + ) + } + + func write(into buf: Writer) { + iss.write(into: buf) + sub.write(into: buf) + FfiConverterOptionString.write(aud, into: buf) + FfiConverterOptionUInt64.write(exp, into: buf) + FfiConverterOptionUInt64.write(nbf, into: buf) + FfiConverterOptionUInt64.write(iat, into: buf) + FfiConverterOptionString.write(jti, into: buf) + } +} + +extension FromPrior: ViaFfiUsingByteBuffer, ViaFfi {} + +public enum ErrorKind { + // Simple error enums only carry a message + case DidNotResolved(message: String) + + // Simple error enums only carry a message + case DidUrlNotFound(message: String) + + // Simple error enums only carry a message + case SecretNotFound(message: String) + + // Simple error enums only carry a message + case Malformed(message: String) + + // Simple error enums only carry a message + case IoError(message: String) + + // Simple error enums only carry a message + case InvalidState(message: String) + + // Simple error enums only carry a message + case NoCompatibleCrypto(message: String) + + // Simple error enums only carry a message + case Unsupported(message: String) + + // Simple error enums only carry a message + case IllegalArgument(message: String) +} + +extension ErrorKind: ViaFfiUsingByteBuffer, ViaFfi { + fileprivate static func read(from buf: Reader) throws -> ErrorKind { + let variant: Int32 = try buf.readInt() + switch variant { + case 1: return .DidNotResolved( + message: try String.read(from: buf) + ) + + case 2: return .DidUrlNotFound( + message: try String.read(from: buf) + ) + + case 3: return .SecretNotFound( + message: try String.read(from: buf) + ) + + case 4: return .Malformed( + message: try String.read(from: buf) + ) + + case 5: return .IoError( + message: try String.read(from: buf) + ) + + case 6: return .InvalidState( + message: try String.read(from: buf) + ) + + case 7: return .NoCompatibleCrypto( + message: try String.read(from: buf) + ) + + case 8: return .Unsupported( + message: try String.read(from: buf) + ) + + case 9: return .IllegalArgument( + message: try String.read(from: buf) + ) + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + fileprivate func write(into buf: Writer) { + switch self { + case let .DidNotResolved(message): + buf.writeInt(Int32(1)) + message.write(into: buf) + case let .DidUrlNotFound(message): + buf.writeInt(Int32(2)) + message.write(into: buf) + case let .SecretNotFound(message): + buf.writeInt(Int32(3)) + message.write(into: buf) + case let .Malformed(message): + buf.writeInt(Int32(4)) + message.write(into: buf) + case let .IoError(message): + buf.writeInt(Int32(5)) + message.write(into: buf) + case let .InvalidState(message): + buf.writeInt(Int32(6)) + message.write(into: buf) + case let .NoCompatibleCrypto(message): + buf.writeInt(Int32(7)) + message.write(into: buf) + case let .Unsupported(message): + buf.writeInt(Int32(8)) + message.write(into: buf) + case let .IllegalArgument(message): + buf.writeInt(Int32(9)) + message.write(into: buf) + } + } +} + +extension ErrorKind: Equatable, Hashable {} + +extension ErrorKind: Error {} + +// Declaration and FfiConverters for DidResolver Callback Interface + +public protocol DidResolver: AnyObject { + func resolve(did: String, cb: OnDidResolverResult) -> ErrorCode +} + +// The ForeignCallback that is passed to Rust. +private let foreignCallbackCallbackInterfaceDidResolver: ForeignCallback = + { (handle: Handle, method: Int32, args: RustBuffer) -> RustBuffer in + func invokeResolve(_ swiftCallbackInterface: DidResolver, _ args: RustBuffer) throws -> RustBuffer { + defer { args.deallocate() } + + let reader = Reader(data: Data(rustBuffer: args)) + let result = swiftCallbackInterface.resolve( + did: try String.read(from: reader), + cb: try OnDidResolverResult.read(from: reader) + ) + let writer = Writer() + result.write(into: writer) + return RustBuffer(bytes: writer.bytes) + // TODO: catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 + } + + let cb = try! ffiConverterCallbackInterfaceDidResolver.lift(handle) + switch method { + case IDX_CALLBACK_FREE: + ffiConverterCallbackInterfaceDidResolver.drop(handle: handle) + return RustBuffer() + case 1: return try! invokeResolve(cb, args) + + // This should never happen, because an out of bounds method index won't + // ever be used. Once we can catch errors, we should return an InternalError. + // https://github.com/mozilla/uniffi-rs/issues/351 + default: return RustBuffer() + } + } + +// The ffiConverter which transforms the Callbacks in to Handles to pass to Rust. +private let ffiConverterCallbackInterfaceDidResolver: FfiConverterCallbackInterface = { + try! rustCall { (err: UnsafeMutablePointer) in + ffi_didcomm_812c_DIDResolver_init_callback(foreignCallbackCallbackInterfaceDidResolver, err) + } + return FfiConverterCallbackInterface() +}() + +// Declaration and FfiConverters for SecretsResolver Callback Interface + +public protocol SecretsResolver: AnyObject { + func getSecret(secretid: String, cb: OnGetSecretResult) -> ErrorCode + func findSecrets(secretids: [String], cb: OnFindSecretsResult) -> ErrorCode +} + +// The ForeignCallback that is passed to Rust. +private let foreignCallbackCallbackInterfaceSecretsResolver: ForeignCallback = + { (handle: Handle, method: Int32, args: RustBuffer) -> RustBuffer in + func invokeGetSecret(_ swiftCallbackInterface: SecretsResolver, _ args: RustBuffer) throws -> RustBuffer { + defer { args.deallocate() } + + let reader = Reader(data: Data(rustBuffer: args)) + let result = swiftCallbackInterface.getSecret( + secretid: try String.read(from: reader), + cb: try OnGetSecretResult.read(from: reader) + ) + let writer = Writer() + result.write(into: writer) + return RustBuffer(bytes: writer.bytes) + // TODO: catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 + } + func invokeFindSecrets(_ swiftCallbackInterface: SecretsResolver, _ args: RustBuffer) throws -> RustBuffer { + defer { args.deallocate() } + + let reader = Reader(data: Data(rustBuffer: args)) + let result = swiftCallbackInterface.findSecrets( + secretids: try FfiConverterSequenceString.read(from: reader), + cb: try OnFindSecretsResult.read(from: reader) + ) + let writer = Writer() + result.write(into: writer) + return RustBuffer(bytes: writer.bytes) + // TODO: catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 + } + + let cb = try! ffiConverterCallbackInterfaceSecretsResolver.lift(handle) + switch method { + case IDX_CALLBACK_FREE: + ffiConverterCallbackInterfaceSecretsResolver.drop(handle: handle) + return RustBuffer() + case 1: return try! invokeGetSecret(cb, args) + case 2: return try! invokeFindSecrets(cb, args) + + // This should never happen, because an out of bounds method index won't + // ever be used. Once we can catch errors, we should return an InternalError. + // https://github.com/mozilla/uniffi-rs/issues/351 + default: return RustBuffer() + } + } + +// The ffiConverter which transforms the Callbacks in to Handles to pass to Rust. +private let ffiConverterCallbackInterfaceSecretsResolver: FfiConverterCallbackInterface = { + try! rustCall { (err: UnsafeMutablePointer) in + ffi_didcomm_812c_SecretsResolver_init_callback(foreignCallbackCallbackInterfaceSecretsResolver, err) + } + return FfiConverterCallbackInterface() +}() + +// Declaration and FfiConverters for OnPackSignedResult Callback Interface + +public protocol OnPackSignedResult: AnyObject { + func success(result: String, metadata: PackSignedMetadata) + func error(err: ErrorKind, msg: String) +} + +// The ForeignCallback that is passed to Rust. +private let foreignCallbackCallbackInterfaceOnPackSignedResult: ForeignCallback = + { (handle: Handle, method: Int32, args: RustBuffer) -> RustBuffer in + func invokeSuccess(_ swiftCallbackInterface: OnPackSignedResult, _ args: RustBuffer) throws -> RustBuffer { + defer { args.deallocate() } + + let reader = Reader(data: Data(rustBuffer: args)) + swiftCallbackInterface.success( + result: try String.read(from: reader), + metadata: try PackSignedMetadata.read(from: reader) + ) + return RustBuffer() + // TODO: catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 + } + func invokeError(_ swiftCallbackInterface: OnPackSignedResult, _ args: RustBuffer) throws -> RustBuffer { + defer { args.deallocate() } + + let reader = Reader(data: Data(rustBuffer: args)) + swiftCallbackInterface.error( + err: try ErrorKind.read(from: reader), + msg: try String.read(from: reader) + ) + return RustBuffer() + // TODO: catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 + } + + let cb = try! ffiConverterCallbackInterfaceOnPackSignedResult.lift(handle) + switch method { + case IDX_CALLBACK_FREE: + ffiConverterCallbackInterfaceOnPackSignedResult.drop(handle: handle) + return RustBuffer() + case 1: return try! invokeSuccess(cb, args) + case 2: return try! invokeError(cb, args) + + // This should never happen, because an out of bounds method index won't + // ever be used. Once we can catch errors, we should return an InternalError. + // https://github.com/mozilla/uniffi-rs/issues/351 + default: return RustBuffer() + } + } + +// The ffiConverter which transforms the Callbacks in to Handles to pass to Rust. +private let ffiConverterCallbackInterfaceOnPackSignedResult: FfiConverterCallbackInterface = { + try! rustCall { (err: UnsafeMutablePointer) in + ffi_didcomm_812c_OnPackSignedResult_init_callback(foreignCallbackCallbackInterfaceOnPackSignedResult, err) + } + return FfiConverterCallbackInterface() +}() + +// Declaration and FfiConverters for OnPackEncryptedResult Callback Interface + +public protocol OnPackEncryptedResult: AnyObject { + func success(result: String, metadata: PackEncryptedMetadata) + func error(err: ErrorKind, msg: String) +} + +// The ForeignCallback that is passed to Rust. +private let foreignCallbackCallbackInterfaceOnPackEncryptedResult: ForeignCallback = + { (handle: Handle, method: Int32, args: RustBuffer) -> RustBuffer in + func invokeSuccess(_ swiftCallbackInterface: OnPackEncryptedResult, _ args: RustBuffer) throws -> RustBuffer { + defer { args.deallocate() } + + let reader = Reader(data: Data(rustBuffer: args)) + swiftCallbackInterface.success( + result: try String.read(from: reader), + metadata: try PackEncryptedMetadata.read(from: reader) + ) + return RustBuffer() + // TODO: catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 + } + func invokeError(_ swiftCallbackInterface: OnPackEncryptedResult, _ args: RustBuffer) throws -> RustBuffer { + defer { args.deallocate() } + + let reader = Reader(data: Data(rustBuffer: args)) + swiftCallbackInterface.error( + err: try ErrorKind.read(from: reader), + msg: try String.read(from: reader) + ) + return RustBuffer() + // TODO: catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 + } + + let cb = try! ffiConverterCallbackInterfaceOnPackEncryptedResult.lift(handle) + switch method { + case IDX_CALLBACK_FREE: + ffiConverterCallbackInterfaceOnPackEncryptedResult.drop(handle: handle) + return RustBuffer() + case 1: return try! invokeSuccess(cb, args) + case 2: return try! invokeError(cb, args) + + // This should never happen, because an out of bounds method index won't + // ever be used. Once we can catch errors, we should return an InternalError. + // https://github.com/mozilla/uniffi-rs/issues/351 + default: return RustBuffer() + } + } + +// The ffiConverter which transforms the Callbacks in to Handles to pass to Rust. +private let ffiConverterCallbackInterfaceOnPackEncryptedResult: FfiConverterCallbackInterface = { + try! rustCall { (err: UnsafeMutablePointer) in + ffi_didcomm_812c_OnPackEncryptedResult_init_callback(foreignCallbackCallbackInterfaceOnPackEncryptedResult, err) + } + return FfiConverterCallbackInterface() +}() + +// Declaration and FfiConverters for OnPackPlaintextResult Callback Interface + +public protocol OnPackPlaintextResult: AnyObject { + func success(result: String) + func error(err: ErrorKind, msg: String) +} + +// The ForeignCallback that is passed to Rust. +private let foreignCallbackCallbackInterfaceOnPackPlaintextResult: ForeignCallback = + { (handle: Handle, method: Int32, args: RustBuffer) -> RustBuffer in + func invokeSuccess(_ swiftCallbackInterface: OnPackPlaintextResult, _ args: RustBuffer) throws -> RustBuffer { + defer { args.deallocate() } + + let reader = Reader(data: Data(rustBuffer: args)) + swiftCallbackInterface.success( + result: try String.read(from: reader) + ) + return RustBuffer() + // TODO: catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 + } + func invokeError(_ swiftCallbackInterface: OnPackPlaintextResult, _ args: RustBuffer) throws -> RustBuffer { + defer { args.deallocate() } + + let reader = Reader(data: Data(rustBuffer: args)) + swiftCallbackInterface.error( + err: try ErrorKind.read(from: reader), + msg: try String.read(from: reader) + ) + return RustBuffer() + // TODO: catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 + } + + let cb = try! ffiConverterCallbackInterfaceOnPackPlaintextResult.lift(handle) + switch method { + case IDX_CALLBACK_FREE: + ffiConverterCallbackInterfaceOnPackPlaintextResult.drop(handle: handle) + return RustBuffer() + case 1: return try! invokeSuccess(cb, args) + case 2: return try! invokeError(cb, args) + + // This should never happen, because an out of bounds method index won't + // ever be used. Once we can catch errors, we should return an InternalError. + // https://github.com/mozilla/uniffi-rs/issues/351 + default: return RustBuffer() + } + } + +// The ffiConverter which transforms the Callbacks in to Handles to pass to Rust. +private let ffiConverterCallbackInterfaceOnPackPlaintextResult: FfiConverterCallbackInterface = { + try! rustCall { (err: UnsafeMutablePointer) in + ffi_didcomm_812c_OnPackPlaintextResult_init_callback(foreignCallbackCallbackInterfaceOnPackPlaintextResult, err) + } + return FfiConverterCallbackInterface() +}() + +// Declaration and FfiConverters for OnUnpackResult Callback Interface + +public protocol OnUnpackResult: AnyObject { + func success(result: Message, metadata: UnpackMetadata) + func error(err: ErrorKind, msg: String) +} + +// The ForeignCallback that is passed to Rust. +private let foreignCallbackCallbackInterfaceOnUnpackResult: ForeignCallback = + { (handle: Handle, method: Int32, args: RustBuffer) -> RustBuffer in + func invokeSuccess(_ swiftCallbackInterface: OnUnpackResult, _ args: RustBuffer) throws -> RustBuffer { + defer { args.deallocate() } + + let reader = Reader(data: Data(rustBuffer: args)) + swiftCallbackInterface.success( + result: try Message.read(from: reader), + metadata: try UnpackMetadata.read(from: reader) + ) + return RustBuffer() + // TODO: catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 + } + func invokeError(_ swiftCallbackInterface: OnUnpackResult, _ args: RustBuffer) throws -> RustBuffer { + defer { args.deallocate() } + + let reader = Reader(data: Data(rustBuffer: args)) + swiftCallbackInterface.error( + err: try ErrorKind.read(from: reader), + msg: try String.read(from: reader) + ) + return RustBuffer() + // TODO: catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 + } + + let cb = try! ffiConverterCallbackInterfaceOnUnpackResult.lift(handle) + switch method { + case IDX_CALLBACK_FREE: + ffiConverterCallbackInterfaceOnUnpackResult.drop(handle: handle) + return RustBuffer() + case 1: return try! invokeSuccess(cb, args) + case 2: return try! invokeError(cb, args) + + // This should never happen, because an out of bounds method index won't + // ever be used. Once we can catch errors, we should return an InternalError. + // https://github.com/mozilla/uniffi-rs/issues/351 + default: return RustBuffer() + } + } + +// The ffiConverter which transforms the Callbacks in to Handles to pass to Rust. +private let ffiConverterCallbackInterfaceOnUnpackResult: FfiConverterCallbackInterface = { + try! rustCall { (err: UnsafeMutablePointer) in + ffi_didcomm_812c_OnUnpackResult_init_callback(foreignCallbackCallbackInterfaceOnUnpackResult, err) + } + return FfiConverterCallbackInterface() +}() + +// Declaration and FfiConverters for OnFromPriorPackResult Callback Interface + +public protocol OnFromPriorPackResult: AnyObject { + func success(frompriorjwt: String, kid: String) + func error(err: ErrorKind, msg: String) +} + +// The ForeignCallback that is passed to Rust. +private let foreignCallbackCallbackInterfaceOnFromPriorPackResult: ForeignCallback = + { (handle: Handle, method: Int32, args: RustBuffer) -> RustBuffer in + func invokeSuccess(_ swiftCallbackInterface: OnFromPriorPackResult, _ args: RustBuffer) throws -> RustBuffer { + defer { args.deallocate() } + + let reader = Reader(data: Data(rustBuffer: args)) + swiftCallbackInterface.success( + frompriorjwt: try String.read(from: reader), + kid: try String.read(from: reader) + ) + return RustBuffer() + // TODO: catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 + } + func invokeError(_ swiftCallbackInterface: OnFromPriorPackResult, _ args: RustBuffer) throws -> RustBuffer { + defer { args.deallocate() } + + let reader = Reader(data: Data(rustBuffer: args)) + swiftCallbackInterface.error( + err: try ErrorKind.read(from: reader), + msg: try String.read(from: reader) + ) + return RustBuffer() + // TODO: catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 + } + + let cb = try! ffiConverterCallbackInterfaceOnFromPriorPackResult.lift(handle) + switch method { + case IDX_CALLBACK_FREE: + ffiConverterCallbackInterfaceOnFromPriorPackResult.drop(handle: handle) + return RustBuffer() + case 1: return try! invokeSuccess(cb, args) + case 2: return try! invokeError(cb, args) + + // This should never happen, because an out of bounds method index won't + // ever be used. Once we can catch errors, we should return an InternalError. + // https://github.com/mozilla/uniffi-rs/issues/351 + default: return RustBuffer() + } + } + +// The ffiConverter which transforms the Callbacks in to Handles to pass to Rust. +private let ffiConverterCallbackInterfaceOnFromPriorPackResult: FfiConverterCallbackInterface = { + try! rustCall { (err: UnsafeMutablePointer) in + ffi_didcomm_812c_OnFromPriorPackResult_init_callback(foreignCallbackCallbackInterfaceOnFromPriorPackResult, err) + } + return FfiConverterCallbackInterface() +}() + +// Declaration and FfiConverters for OnFromPriorUnpackResult Callback Interface + +public protocol OnFromPriorUnpackResult: AnyObject { + func success(fromprior: FromPrior, kid: String) + func error(err: ErrorKind, msg: String) +} + +// The ForeignCallback that is passed to Rust. +private let foreignCallbackCallbackInterfaceOnFromPriorUnpackResult: ForeignCallback = + { (handle: Handle, method: Int32, args: RustBuffer) -> RustBuffer in + func invokeSuccess(_ swiftCallbackInterface: OnFromPriorUnpackResult, _ args: RustBuffer) throws -> RustBuffer { + defer { args.deallocate() } + + let reader = Reader(data: Data(rustBuffer: args)) + swiftCallbackInterface.success( + fromprior: try FromPrior.read(from: reader), + kid: try String.read(from: reader) + ) + return RustBuffer() + // TODO: catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 + } + func invokeError(_ swiftCallbackInterface: OnFromPriorUnpackResult, _ args: RustBuffer) throws -> RustBuffer { + defer { args.deallocate() } + + let reader = Reader(data: Data(rustBuffer: args)) + swiftCallbackInterface.error( + err: try ErrorKind.read(from: reader), + msg: try String.read(from: reader) + ) + return RustBuffer() + // TODO: catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 + } + + let cb = try! ffiConverterCallbackInterfaceOnFromPriorUnpackResult.lift(handle) + switch method { + case IDX_CALLBACK_FREE: + ffiConverterCallbackInterfaceOnFromPriorUnpackResult.drop(handle: handle) + return RustBuffer() + case 1: return try! invokeSuccess(cb, args) + case 2: return try! invokeError(cb, args) + + // This should never happen, because an out of bounds method index won't + // ever be used. Once we can catch errors, we should return an InternalError. + // https://github.com/mozilla/uniffi-rs/issues/351 + default: return RustBuffer() + } + } + +// The ffiConverter which transforms the Callbacks in to Handles to pass to Rust. +private let ffiConverterCallbackInterfaceOnFromPriorUnpackResult: FfiConverterCallbackInterface = { + try! rustCall { (err: UnsafeMutablePointer) in + ffi_didcomm_812c_OnFromPriorUnpackResult_init_callback(foreignCallbackCallbackInterfaceOnFromPriorUnpackResult, err) + } + return FfiConverterCallbackInterface() +}() + +// Declaration and FfiConverters for OnWrapInForwardResult Callback Interface + +public protocol OnWrapInForwardResult: AnyObject { + func success(result: String) + func error(err: ErrorKind, msg: String) +} + +// The ForeignCallback that is passed to Rust. +private let foreignCallbackCallbackInterfaceOnWrapInForwardResult: ForeignCallback = + { (handle: Handle, method: Int32, args: RustBuffer) -> RustBuffer in + func invokeSuccess(_ swiftCallbackInterface: OnWrapInForwardResult, _ args: RustBuffer) throws -> RustBuffer { + defer { args.deallocate() } + + let reader = Reader(data: Data(rustBuffer: args)) + swiftCallbackInterface.success( + result: try String.read(from: reader) + ) + return RustBuffer() + // TODO: catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 + } + func invokeError(_ swiftCallbackInterface: OnWrapInForwardResult, _ args: RustBuffer) throws -> RustBuffer { + defer { args.deallocate() } + + let reader = Reader(data: Data(rustBuffer: args)) + swiftCallbackInterface.error( + err: try ErrorKind.read(from: reader), + msg: try String.read(from: reader) + ) + return RustBuffer() + // TODO: catch errors and report them back to Rust. + // https://github.com/mozilla/uniffi-rs/issues/351 + } + + let cb = try! ffiConverterCallbackInterfaceOnWrapInForwardResult.lift(handle) + switch method { + case IDX_CALLBACK_FREE: + ffiConverterCallbackInterfaceOnWrapInForwardResult.drop(handle: handle) + return RustBuffer() + case 1: return try! invokeSuccess(cb, args) + case 2: return try! invokeError(cb, args) + + // This should never happen, because an out of bounds method index won't + // ever be used. Once we can catch errors, we should return an InternalError. + // https://github.com/mozilla/uniffi-rs/issues/351 + default: return RustBuffer() + } + } + +// The ffiConverter which transforms the Callbacks in to Handles to pass to Rust. +private let ffiConverterCallbackInterfaceOnWrapInForwardResult: FfiConverterCallbackInterface = { + try! rustCall { (err: UnsafeMutablePointer) in + ffi_didcomm_812c_OnWrapInForwardResult_init_callback(foreignCallbackCallbackInterfaceOnWrapInForwardResult, err) + } + return FfiConverterCallbackInterface() +}() + +extension UInt64: Primitive, ViaFfi { + fileprivate static func read(from buf: Reader) throws -> Self { + return try lift(buf.readInt()) + } + + fileprivate func write(into buf: Writer) { + buf.writeInt(lower()) + } +} + +extension Bool: ViaFfi { + fileprivate typealias FfiType = Int8 + + fileprivate static func read(from buf: Reader) throws -> Self { + return try lift(buf.readInt()) + } + + fileprivate func write(into buf: Writer) { + buf.writeInt(lower()) + } + + fileprivate static func lift(_ v: FfiType) throws -> Self { + return v != 0 + } + + fileprivate func lower() -> FfiType { + return self ? 1 : 0 + } +} + +extension String: ViaFfi { + fileprivate typealias FfiType = RustBuffer + + fileprivate static func lift(_ v: FfiType) throws -> Self { + defer { + v.deallocate() + } + if v.data == nil { + return String() + } + let bytes = UnsafeBufferPointer(start: v.data!, count: Int(v.len)) + return String(bytes: bytes, encoding: String.Encoding.utf8)! + } + + fileprivate func lower() -> FfiType { + return utf8CString.withUnsafeBufferPointer { ptr in + // The swift string gives us int8_t, we want uint8_t. + ptr.withMemoryRebound(to: UInt8.self) { ptr in + // The swift string gives us a trailing null byte, we don't want it. + let buf = UnsafeBufferPointer(rebasing: ptr.prefix(upTo: ptr.count - 1)) + return RustBuffer.from(buf) + } + } + } + + fileprivate static func read(from buf: Reader) throws -> Self { + let len: Int32 = try buf.readInt() + return String(bytes: try buf.readBytes(count: Int(len)), encoding: String.Encoding.utf8)! + } + + fileprivate func write(into buf: Writer) { + let len = Int32(utf8.count) + buf.writeInt(len) + buf.writeBytes(utf8) + } +} + +// Helper code for DidComm class is found in ObjectTemplate.swift +// Helper code for ExampleDidResolver class is found in ObjectTemplate.swift +// Helper code for ExampleSecretsResolver class is found in ObjectTemplate.swift +// Helper code for OnDidResolverResult class is found in ObjectTemplate.swift +// Helper code for OnFindSecretsResult class is found in ObjectTemplate.swift +// Helper code for OnGetSecretResult class is found in ObjectTemplate.swift +// Helper code for Attachment record is found in RecordTemplate.swift +// Helper code for Base64AttachmentData record is found in RecordTemplate.swift +// Helper code for DidCommMessagingService record is found in RecordTemplate.swift +// Helper code for DidDoc record is found in RecordTemplate.swift +// Helper code for FromPrior record is found in RecordTemplate.swift +// Helper code for JsonAttachmentData record is found in RecordTemplate.swift +// Helper code for LinksAttachmentData record is found in RecordTemplate.swift +// Helper code for Message record is found in RecordTemplate.swift +// Helper code for MessagingServiceMetadata record is found in RecordTemplate.swift +// Helper code for PackEncryptedMetadata record is found in RecordTemplate.swift +// Helper code for PackEncryptedOptions record is found in RecordTemplate.swift +// Helper code for PackSignedMetadata record is found in RecordTemplate.swift +// Helper code for Secret record is found in RecordTemplate.swift +// Helper code for Service record is found in RecordTemplate.swift +// Helper code for UnpackMetadata record is found in RecordTemplate.swift +// Helper code for UnpackOptions record is found in RecordTemplate.swift +// Helper code for VerificationMethod record is found in RecordTemplate.swift +// Helper code for AnonCryptAlg enum is found in EnumTemplate.swift +// Helper code for AttachmentData enum is found in EnumTemplate.swift +// Helper code for AuthCryptAlg enum is found in EnumTemplate.swift +// Helper code for ErrorCode enum is found in EnumTemplate.swift +// Helper code for SecretMaterial enum is found in EnumTemplate.swift +// Helper code for SecretType enum is found in EnumTemplate.swift +// Helper code for ServiceKind enum is found in EnumTemplate.swift +// Helper code for SignAlg enum is found in EnumTemplate.swift +// Helper code for VerificationMaterial enum is found in EnumTemplate.swift +// Helper code for VerificationMethodType enum is found in EnumTemplate.swift +// Helper code for ErrorKind error is found in ErrorTemplate.swift + +private enum FfiConverterOptionUInt64: FfiConverterUsingByteBuffer { + typealias SwiftType = UInt64? + + static func write(_ value: SwiftType, into buf: Writer) { + FfiConverterOptional.write(value, into: buf) { item, buf in + item.write(into: buf) + } + } + + static func read(from buf: Reader) throws -> SwiftType { + try FfiConverterOptional.read(from: buf) { buf in + try UInt64.read(from: buf) + } + } +} + +private enum FfiConverterOptionString: FfiConverterUsingByteBuffer { + typealias SwiftType = String? + + static func write(_ value: SwiftType, into buf: Writer) { + FfiConverterOptional.write(value, into: buf) { item, buf in + item.write(into: buf) + } + } + + static func read(from buf: Reader) throws -> SwiftType { + try FfiConverterOptional.read(from: buf) { buf in + try String.read(from: buf) + } + } +} + +private enum FfiConverterOptionRecordDidDoc: FfiConverterUsingByteBuffer { + typealias SwiftType = DidDoc? + + static func write(_ value: SwiftType, into buf: Writer) { + FfiConverterOptional.write(value, into: buf) { item, buf in + item.write(into: buf) + } + } + + static func read(from buf: Reader) throws -> SwiftType { + try FfiConverterOptional.read(from: buf) { buf in + try DidDoc.read(from: buf) + } + } +} + +private enum FfiConverterOptionRecordFromPrior: FfiConverterUsingByteBuffer { + typealias SwiftType = FromPrior? + + static func write(_ value: SwiftType, into buf: Writer) { + FfiConverterOptional.write(value, into: buf) { item, buf in + item.write(into: buf) + } + } + + static func read(from buf: Reader) throws -> SwiftType { + try FfiConverterOptional.read(from: buf) { buf in + try FromPrior.read(from: buf) + } + } +} + +private enum FfiConverterOptionRecordMessagingServiceMetadata: FfiConverterUsingByteBuffer { + typealias SwiftType = MessagingServiceMetadata? + + static func write(_ value: SwiftType, into buf: Writer) { + FfiConverterOptional.write(value, into: buf) { item, buf in + item.write(into: buf) + } + } + + static func read(from buf: Reader) throws -> SwiftType { + try FfiConverterOptional.read(from: buf) { buf in + try MessagingServiceMetadata.read(from: buf) + } + } +} + +private enum FfiConverterOptionRecordSecret: FfiConverterUsingByteBuffer { + typealias SwiftType = Secret? + + static func write(_ value: SwiftType, into buf: Writer) { + FfiConverterOptional.write(value, into: buf) { item, buf in + item.write(into: buf) + } + } + + static func read(from buf: Reader) throws -> SwiftType { + try FfiConverterOptional.read(from: buf) { buf in + try Secret.read(from: buf) + } + } +} + +private enum FfiConverterOptionEnumAnonCryptAlg: FfiConverterUsingByteBuffer { + typealias SwiftType = AnonCryptAlg? + + static func write(_ value: SwiftType, into buf: Writer) { + FfiConverterOptional.write(value, into: buf) { item, buf in + item.write(into: buf) + } + } + + static func read(from buf: Reader) throws -> SwiftType { + try FfiConverterOptional.read(from: buf) { buf in + try AnonCryptAlg.read(from: buf) + } + } +} + +private enum FfiConverterOptionEnumAuthCryptAlg: FfiConverterUsingByteBuffer { + typealias SwiftType = AuthCryptAlg? + + static func write(_ value: SwiftType, into buf: Writer) { + FfiConverterOptional.write(value, into: buf) { item, buf in + item.write(into: buf) + } + } + + static func read(from buf: Reader) throws -> SwiftType { + try FfiConverterOptional.read(from: buf) { buf in + try AuthCryptAlg.read(from: buf) + } + } +} + +private enum FfiConverterOptionEnumSignAlg: FfiConverterUsingByteBuffer { + typealias SwiftType = SignAlg? + + static func write(_ value: SwiftType, into buf: Writer) { + FfiConverterOptional.write(value, into: buf) { item, buf in + item.write(into: buf) + } + } + + static func read(from buf: Reader) throws -> SwiftType { + try FfiConverterOptional.read(from: buf) { buf in + try SignAlg.read(from: buf) + } + } +} + +private enum FfiConverterOptionSequenceString: FfiConverterUsingByteBuffer { + typealias SwiftType = [String]? + + static func write(_ value: SwiftType, into buf: Writer) { + FfiConverterOptional.write(value, into: buf) { item, buf in + FfiConverterSequenceString.write(item, into: buf) + } + } + + static func read(from buf: Reader) throws -> SwiftType { + try FfiConverterOptional.read(from: buf) { buf in + try FfiConverterSequenceString.read(from: buf) + } + } +} + +private enum FfiConverterOptionSequenceRecordAttachment: FfiConverterUsingByteBuffer { + typealias SwiftType = [Attachment]? + + static func write(_ value: SwiftType, into buf: Writer) { + FfiConverterOptional.write(value, into: buf) { item, buf in + FfiConverterSequenceRecordAttachment.write(item, into: buf) + } + } + + static func read(from buf: Reader) throws -> SwiftType { + try FfiConverterOptional.read(from: buf) { buf in + try FfiConverterSequenceRecordAttachment.read(from: buf) + } + } +} + +private enum FfiConverterOptionDictionaryJsonValue: FfiConverterUsingByteBuffer { + typealias SwiftType = [String: String]? + + static func write(_ value: SwiftType, into buf: Writer) { + FfiConverterOptional.write(value, into: buf) { item, buf in + FfiConverterDictionaryJsonValue.write(item, into: buf) + } + } + + static func read(from buf: Reader) throws -> SwiftType { + try FfiConverterOptional.read(from: buf) { buf in + try FfiConverterDictionaryJsonValue.read(from: buf) + } + } +} + +private enum FfiConverterSequenceString: FfiConverterUsingByteBuffer { + typealias SwiftType = [String] + + static func write(_ value: SwiftType, into buf: Writer) { + FfiConverterSequence.write(value, into: buf) { item, buf in + item.write(into: buf) + } + } + + static func read(from buf: Reader) throws -> SwiftType { + try FfiConverterSequence.read(from: buf) { buf in + try String.read(from: buf) + } + } +} + +private enum FfiConverterSequenceRecordAttachment: FfiConverterUsingByteBuffer { + typealias SwiftType = [Attachment] + + static func write(_ value: SwiftType, into buf: Writer) { + FfiConverterSequence.write(value, into: buf) { item, buf in + item.write(into: buf) + } + } + + static func read(from buf: Reader) throws -> SwiftType { + try FfiConverterSequence.read(from: buf) { buf in + try Attachment.read(from: buf) + } + } +} + +private enum FfiConverterSequenceRecordDidDoc: FfiConverterUsingByteBuffer { + typealias SwiftType = [DidDoc] + + static func write(_ value: SwiftType, into buf: Writer) { + FfiConverterSequence.write(value, into: buf) { item, buf in + item.write(into: buf) + } + } + + static func read(from buf: Reader) throws -> SwiftType { + try FfiConverterSequence.read(from: buf) { buf in + try DidDoc.read(from: buf) + } + } +} + +private enum FfiConverterSequenceRecordSecret: FfiConverterUsingByteBuffer { + typealias SwiftType = [Secret] + + static func write(_ value: SwiftType, into buf: Writer) { + FfiConverterSequence.write(value, into: buf) { item, buf in + item.write(into: buf) + } + } + + static func read(from buf: Reader) throws -> SwiftType { + try FfiConverterSequence.read(from: buf) { buf in + try Secret.read(from: buf) + } + } +} + +private enum FfiConverterSequenceRecordService: FfiConverterUsingByteBuffer { + typealias SwiftType = [Service] + + static func write(_ value: SwiftType, into buf: Writer) { + FfiConverterSequence.write(value, into: buf) { item, buf in + item.write(into: buf) + } + } + + static func read(from buf: Reader) throws -> SwiftType { + try FfiConverterSequence.read(from: buf) { buf in + try Service.read(from: buf) + } + } +} + +private enum FfiConverterSequenceRecordVerificationMethod: FfiConverterUsingByteBuffer { + typealias SwiftType = [VerificationMethod] + + static func write(_ value: SwiftType, into buf: Writer) { + FfiConverterSequence.write(value, into: buf) { item, buf in + item.write(into: buf) + } + } + + static func read(from buf: Reader) throws -> SwiftType { + try FfiConverterSequence.read(from: buf) { buf in + try VerificationMethod.read(from: buf) + } + } +} + +private enum FfiConverterDictionaryJsonValue: FfiConverterUsingByteBuffer { + typealias SwiftType = [String: String] + + static func write(_ value: SwiftType, into buf: Writer) { + FfiConverterDictionary.write(value, into: buf) { key, value, buf in + key.write(into: buf) + value.write(into: buf) + } + } + + static func read(from buf: Reader) throws -> SwiftType { + try FfiConverterDictionary.read(from: buf) { buf in + (try String.read(from: buf), + try String.read(from: buf)) + } + } +} + +/** + * Top level initializers and tear down methods. + * + * This is generated by uniffi. + */ +public enum DidcommLifecycle { + /** + * Initialize the FFI and Rust library. This should be only called once per application. + */ + func initialize() { + // No initialization code needed + } +} diff --git a/affinidi-messaging-didcomm/wrappers/swift/didcomm/didcomm.swiftmodule b/affinidi-messaging-didcomm/wrappers/swift/didcomm/didcomm.swiftmodule new file mode 100644 index 0000000000000000000000000000000000000000..f56babea780c14c63078a13c2745dc9bc6a8f851 GIT binary patch literal 315740 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�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 +#include + +// The following structs are used to implement the lowest level +// of the FFI, and thus useful to multiple uniffied crates. +// We ensure they are declared exactly once, with a header guard, UNIFFI_SHARED_H. +#ifdef UNIFFI_SHARED_H + // We also try to prevent mixing versions of shared uniffi header structs. + // If you add anything to the #else block, you must increment the version suffix in UNIFFI_SHARED_HEADER_V3 + #ifndef UNIFFI_SHARED_HEADER_V3 + #error Combining helper code from multiple versions of uniffi is not supported + #endif // ndef UNIFFI_SHARED_HEADER_V3 +#else +#define UNIFFI_SHARED_H +#define UNIFFI_SHARED_HEADER_V3 +// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️ +// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V3 in this file. ⚠️ + +typedef struct RustBuffer +{ + int32_t capacity; + int32_t len; + uint8_t *_Nullable data; +} RustBuffer; + +typedef RustBuffer (*ForeignCallback)(uint64_t, int32_t, RustBuffer); + +typedef struct ForeignBytes +{ + int32_t len; + const uint8_t *_Nullable data; +} ForeignBytes; + +// Error definitions +typedef struct RustCallStatus { + int8_t code; + RustBuffer errorBuf; +} RustCallStatus; + +// ⚠️ Attention: If you change this #else block (ending in `#endif // def UNIFFI_SHARED_H`) you *must* ⚠️ +// ⚠️ increment the version suffix in all instances of UNIFFI_SHARED_HEADER_V3 in this file. ⚠️ +#endif // def UNIFFI_SHARED_H + +void ffi_didcomm_812c_DIDComm_object_free( + void*_Nonnull ptr, + RustCallStatus *_Nonnull out_status + ); +void*_Nonnull didcomm_812c_DIDComm_new( + uint64_t did_resolver,uint64_t secret_resolver, + RustCallStatus *_Nonnull out_status + ); +RustBuffer didcomm_812c_DIDComm_pack_plaintext( + void*_Nonnull ptr,RustBuffer msg,uint64_t cb, + RustCallStatus *_Nonnull out_status + ); +RustBuffer didcomm_812c_DIDComm_pack_signed( + void*_Nonnull ptr,RustBuffer msg,RustBuffer sign_by,uint64_t cb, + RustCallStatus *_Nonnull out_status + ); +RustBuffer didcomm_812c_DIDComm_pack_encrypted( + void*_Nonnull ptr,RustBuffer msg,RustBuffer to,RustBuffer from,RustBuffer sign_by,RustBuffer options,uint64_t cb, + RustCallStatus *_Nonnull out_status + ); +RustBuffer didcomm_812c_DIDComm_unpack( + void*_Nonnull ptr,RustBuffer msg,RustBuffer options,uint64_t cb, + RustCallStatus *_Nonnull out_status + ); +RustBuffer didcomm_812c_DIDComm_pack_from_prior( + void*_Nonnull ptr,RustBuffer msg,RustBuffer issuer_kid,uint64_t cb, + RustCallStatus *_Nonnull out_status + ); +RustBuffer didcomm_812c_DIDComm_unpack_from_prior( + void*_Nonnull ptr,RustBuffer from_prior_jwt,uint64_t cb, + RustCallStatus *_Nonnull out_status + ); +RustBuffer didcomm_812c_DIDComm_wrap_in_forward( + void*_Nonnull ptr,RustBuffer msg,RustBuffer headers,RustBuffer to,RustBuffer routing_keys,RustBuffer enc_alg_anon,uint64_t cb, + RustCallStatus *_Nonnull out_status + ); +void ffi_didcomm_812c_OnDIDResolverResult_object_free( + void*_Nonnull ptr, + RustCallStatus *_Nonnull out_status + ); +void didcomm_812c_OnDIDResolverResult_success( + void*_Nonnull ptr,RustBuffer result, + RustCallStatus *_Nonnull out_status + ); +void didcomm_812c_OnDIDResolverResult_error( + void*_Nonnull ptr,RustBuffer err,RustBuffer msg, + RustCallStatus *_Nonnull out_status + ); +void ffi_didcomm_812c_ExampleDIDResolver_object_free( + void*_Nonnull ptr, + RustCallStatus *_Nonnull out_status + ); +void*_Nonnull didcomm_812c_ExampleDIDResolver_new( + RustBuffer known_dids, + RustCallStatus *_Nonnull out_status + ); +RustBuffer didcomm_812c_ExampleDIDResolver_resolve( + void*_Nonnull ptr,RustBuffer did,void*_Nonnull cb, + RustCallStatus *_Nonnull out_status + ); +void ffi_didcomm_812c_OnGetSecretResult_object_free( + void*_Nonnull ptr, + RustCallStatus *_Nonnull out_status + ); +void didcomm_812c_OnGetSecretResult_success( + void*_Nonnull ptr,RustBuffer result, + RustCallStatus *_Nonnull out_status + ); +void didcomm_812c_OnGetSecretResult_error( + void*_Nonnull ptr,RustBuffer err,RustBuffer msg, + RustCallStatus *_Nonnull out_status + ); +void ffi_didcomm_812c_OnFindSecretsResult_object_free( + void*_Nonnull ptr, + RustCallStatus *_Nonnull out_status + ); +void didcomm_812c_OnFindSecretsResult_success( + void*_Nonnull ptr,RustBuffer result, + RustCallStatus *_Nonnull out_status + ); +void didcomm_812c_OnFindSecretsResult_error( + void*_Nonnull ptr,RustBuffer err,RustBuffer msg, + RustCallStatus *_Nonnull out_status + ); +void ffi_didcomm_812c_ExampleSecretsResolver_object_free( + void*_Nonnull ptr, + RustCallStatus *_Nonnull out_status + ); +void*_Nonnull didcomm_812c_ExampleSecretsResolver_new( + RustBuffer known_secrets, + RustCallStatus *_Nonnull out_status + ); +RustBuffer didcomm_812c_ExampleSecretsResolver_get_secret( + void*_Nonnull ptr,RustBuffer secret_id,void*_Nonnull cb, + RustCallStatus *_Nonnull out_status + ); +RustBuffer didcomm_812c_ExampleSecretsResolver_find_secrets( + void*_Nonnull ptr,RustBuffer secret_ids,void*_Nonnull cb, + RustCallStatus *_Nonnull out_status + ); +void ffi_didcomm_812c_DIDResolver_init_callback( + ForeignCallback _Nonnull callback_stub, + RustCallStatus *_Nonnull out_status + ); +void ffi_didcomm_812c_SecretsResolver_init_callback( + ForeignCallback _Nonnull callback_stub, + RustCallStatus *_Nonnull out_status + ); +void ffi_didcomm_812c_OnPackSignedResult_init_callback( + ForeignCallback _Nonnull callback_stub, + RustCallStatus *_Nonnull out_status + ); +void ffi_didcomm_812c_OnPackEncryptedResult_init_callback( + ForeignCallback _Nonnull callback_stub, + RustCallStatus *_Nonnull out_status + ); +void ffi_didcomm_812c_OnPackPlaintextResult_init_callback( + ForeignCallback _Nonnull callback_stub, + RustCallStatus *_Nonnull out_status + ); +void ffi_didcomm_812c_OnUnpackResult_init_callback( + ForeignCallback _Nonnull callback_stub, + RustCallStatus *_Nonnull out_status + ); +void ffi_didcomm_812c_OnFromPriorPackResult_init_callback( + ForeignCallback _Nonnull callback_stub, + RustCallStatus *_Nonnull out_status + ); +void ffi_didcomm_812c_OnFromPriorUnpackResult_init_callback( + ForeignCallback _Nonnull callback_stub, + RustCallStatus *_Nonnull out_status + ); +void ffi_didcomm_812c_OnWrapInForwardResult_init_callback( + ForeignCallback _Nonnull callback_stub, + RustCallStatus *_Nonnull out_status + ); +RustBuffer ffi_didcomm_812c_rustbuffer_alloc( + int32_t size, + RustCallStatus *_Nonnull out_status + ); +RustBuffer ffi_didcomm_812c_rustbuffer_from_bytes( + ForeignBytes bytes, + RustCallStatus *_Nonnull out_status + ); +void ffi_didcomm_812c_rustbuffer_free( + RustBuffer buf, + RustCallStatus *_Nonnull out_status + ); +RustBuffer ffi_didcomm_812c_rustbuffer_reserve( + RustBuffer buf,int32_t additional, + RustCallStatus *_Nonnull out_status + ); diff --git a/affinidi-messaging-didcomm/wrappers/swift/didcomm/didcommFFI.modulemap b/affinidi-messaging-didcomm/wrappers/swift/didcomm/didcommFFI.modulemap new file mode 100644 index 0000000..c116ae2 --- /dev/null +++ b/affinidi-messaging-didcomm/wrappers/swift/didcomm/didcommFFI.modulemap @@ -0,0 +1,6 @@ +// This file was autogenerated by some hot garbage in the `uniffi` crate. +// Trust me, you don't want to mess with it! +module didcommFFI { + header "didcommFFI.h" + export * +} \ No newline at end of file diff --git a/affinidi-messaging-didcomm/wrappers/swift/didcomm/libdidcomm.dylib b/affinidi-messaging-didcomm/wrappers/swift/didcomm/libdidcomm.dylib new file mode 100755 index 0000000000000000000000000000000000000000..c82a77b91a4212e41da513d5a6302b723a778884 GIT binary patch literal 674256 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

  • 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ⅅ)42ez`dw3ai!=ZVP#X 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ⅇX_URO5ONGL-Hv{b%3cq7Ldv5k1sl8w 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 4AU6Ne`l7fSHZN%Y0vyEU%FzCc8h;E2 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 YV?cYEu5+T+2B*u#dptL0|Cavme`y{K{Qv*} literal 0 HcmV?d00001 diff --git a/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS.xcodeproj/project.pbxproj b/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS.xcodeproj/project.pbxproj new file mode 100644 index 0000000..935a480 --- /dev/null +++ b/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS.xcodeproj/project.pbxproj @@ -0,0 +1,406 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 55; + objects = { + +/* Begin PBXBuildFile section */ + 7F7D7A7F5AE6A2F751897BC6 /* Pods_DidcommExampleiOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7C5EA3B686372C5A9173BD54 /* Pods_DidcommExampleiOS.framework */; }; + 9E43C92A2881D6FA007BB64B /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43C9292881D6FA007BB64B /* AppDelegate.swift */; }; + 9E43C92E2881D6FA007BB64B /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9E43C92D2881D6FA007BB64B /* ViewController.swift */; }; + 9E43C9332881D6FC007BB64B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9E43C9322881D6FC007BB64B /* Assets.xcassets */; }; + 9EA01F7C2881DBDA00EDF97D /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9EA01F7B2881DBDA00EDF97D /* Constants.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 7C5EA3B686372C5A9173BD54 /* Pods_DidcommExampleiOS.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_DidcommExampleiOS.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 9E43C9262881D6FA007BB64B /* DidcommExampleiOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = DidcommExampleiOS.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 9E43C9292881D6FA007BB64B /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 9E43C92D2881D6FA007BB64B /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; + 9E43C9322881D6FC007BB64B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 9E43C9372881D6FC007BB64B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 9EA01F7B2881DBDA00EDF97D /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; + B3680BA7F4BC87A02E82C95A /* Pods-DidcommExampleiOS.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DidcommExampleiOS.debug.xcconfig"; path = "Target Support Files/Pods-DidcommExampleiOS/Pods-DidcommExampleiOS.debug.xcconfig"; sourceTree = ""; }; + FFC1BDA8D219A7B6B0442219 /* Pods-DidcommExampleiOS.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-DidcommExampleiOS.release.xcconfig"; path = "Target Support Files/Pods-DidcommExampleiOS/Pods-DidcommExampleiOS.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 9E43C9232881D6FA007BB64B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 7F7D7A7F5AE6A2F751897BC6 /* Pods_DidcommExampleiOS.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7D899DDC733B6FC3411B2735 /* Pods */ = { + isa = PBXGroup; + children = ( + B3680BA7F4BC87A02E82C95A /* Pods-DidcommExampleiOS.debug.xcconfig */, + FFC1BDA8D219A7B6B0442219 /* Pods-DidcommExampleiOS.release.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; + 7FEB5E2BC1EC4E8C158D01AD /* Frameworks */ = { + isa = PBXGroup; + children = ( + 7C5EA3B686372C5A9173BD54 /* Pods_DidcommExampleiOS.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + 9E43C91D2881D6FA007BB64B = { + isa = PBXGroup; + children = ( + 9E43C9282881D6FA007BB64B /* DidcommExampleiOS */, + 9E43C9272881D6FA007BB64B /* Products */, + 7D899DDC733B6FC3411B2735 /* Pods */, + 7FEB5E2BC1EC4E8C158D01AD /* Frameworks */, + ); + sourceTree = ""; + }; + 9E43C9272881D6FA007BB64B /* Products */ = { + isa = PBXGroup; + children = ( + 9E43C9262881D6FA007BB64B /* DidcommExampleiOS.app */, + ); + name = Products; + sourceTree = ""; + }; + 9E43C9282881D6FA007BB64B /* DidcommExampleiOS */ = { + isa = PBXGroup; + children = ( + 9E43C9292881D6FA007BB64B /* AppDelegate.swift */, + 9E43C92D2881D6FA007BB64B /* ViewController.swift */, + 9E43C9322881D6FC007BB64B /* Assets.xcassets */, + 9E43C9372881D6FC007BB64B /* Info.plist */, + 9EA01F7B2881DBDA00EDF97D /* Constants.swift */, + ); + path = DidcommExampleiOS; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 9E43C9252881D6FA007BB64B /* DidcommExampleiOS */ = { + isa = PBXNativeTarget; + buildConfigurationList = 9E43C93A2881D6FC007BB64B /* Build configuration list for PBXNativeTarget "DidcommExampleiOS" */; + buildPhases = ( + 525EAB56A4503CEDB6B8977F /* [CP] Check Pods Manifest.lock */, + 9E43C9222881D6FA007BB64B /* Sources */, + 9E43C9232881D6FA007BB64B /* Frameworks */, + 9E43C9242881D6FA007BB64B /* Resources */, + 75CE7A77AE90C4A03552946B /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = DidcommExampleiOS; + productName = DidcommExampleiOS; + productReference = 9E43C9262881D6FA007BB64B /* DidcommExampleiOS.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 9E43C91E2881D6FA007BB64B /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1340; + LastUpgradeCheck = 1340; + TargetAttributes = { + 9E43C9252881D6FA007BB64B = { + CreatedOnToolsVersion = 13.4.1; + }; + }; + }; + buildConfigurationList = 9E43C9212881D6FA007BB64B /* Build configuration list for PBXProject "DidcommExampleiOS" */; + compatibilityVersion = "Xcode 13.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 9E43C91D2881D6FA007BB64B; + productRefGroup = 9E43C9272881D6FA007BB64B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 9E43C9252881D6FA007BB64B /* DidcommExampleiOS */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 9E43C9242881D6FA007BB64B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9E43C9332881D6FC007BB64B /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 525EAB56A4503CEDB6B8977F /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-DidcommExampleiOS-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + 75CE7A77AE90C4A03552946B /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-DidcommExampleiOS/Pods-DidcommExampleiOS-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-DidcommExampleiOS/Pods-DidcommExampleiOS-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-DidcommExampleiOS/Pods-DidcommExampleiOS-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 9E43C9222881D6FA007BB64B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 9E43C92E2881D6FA007BB64B /* ViewController.swift in Sources */, + 9E43C92A2881D6FA007BB64B /* AppDelegate.swift in Sources */, + 9EA01F7C2881DBDA00EDF97D /* Constants.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 9E43C9382881D6FC007BB64B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 9E43C9392881D6FC007BB64B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 15.5; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 9E43C93B2881D6FC007BB64B /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = B3680BA7F4BC87A02E82C95A /* Pods-DidcommExampleiOS.debug.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = G42Z863W7H; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = DidcommExampleiOS/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = ""; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.rodrigofranzoi.DidcommExampleiOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 9E43C93C2881D6FC007BB64B /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = FFC1BDA8D219A7B6B0442219 /* Pods-DidcommExampleiOS.release.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = G42Z863W7H; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = DidcommExampleiOS/Info.plist; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen; + INFOPLIST_KEY_UIMainStoryboardFile = ""; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.rodrigofranzoi.DidcommExampleiOS; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 9E43C9212881D6FA007BB64B /* Build configuration list for PBXProject "DidcommExampleiOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9E43C9382881D6FC007BB64B /* Debug */, + 9E43C9392881D6FC007BB64B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 9E43C93A2881D6FC007BB64B /* Build configuration list for PBXNativeTarget "DidcommExampleiOS" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 9E43C93B2881D6FC007BB64B /* Debug */, + 9E43C93C2881D6FC007BB64B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 9E43C91E2881D6FA007BB64B /* Project object */; +} diff --git a/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/AppDelegate.swift b/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/AppDelegate.swift new file mode 100644 index 0000000..a8ce349 --- /dev/null +++ b/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/AppDelegate.swift @@ -0,0 +1,16 @@ +import UIKit + +@main +class AppDelegate: UIResponder, UIApplicationDelegate { + + var window: UIWindow? + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + window = UIWindow(frame: UIScreen.main.bounds) + let viewController = ViewController() + window?.backgroundColor = .white + window?.makeKeyAndVisible() + window?.rootViewController = viewController + return true + } +} diff --git a/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/Assets.xcassets/AccentColor.colorset/Contents.json b/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/Assets.xcassets/AppIcon.appiconset/Contents.json b/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..9221b9b --- /dev/null +++ b/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,98 @@ +{ + "images" : [ + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "20x20" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "29x29" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "40x40" + }, + { + "idiom" : "iphone", + "scale" : "2x", + "size" : "60x60" + }, + { + "idiom" : "iphone", + "scale" : "3x", + "size" : "60x60" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "20x20" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "29x29" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "40x40" + }, + { + "idiom" : "ipad", + "scale" : "1x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "76x76" + }, + { + "idiom" : "ipad", + "scale" : "2x", + "size" : "83.5x83.5" + }, + { + "idiom" : "ios-marketing", + "scale" : "1x", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/Assets.xcassets/Contents.json b/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/Constants.swift b/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/Constants.swift new file mode 100644 index 0000000..90e55c9 --- /dev/null +++ b/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/Constants.swift @@ -0,0 +1,154 @@ +import DidcommSDK + +let ALICE_DID = "did:example:alice" + +let ALICE_DID_DOC = DidDoc(did: "did:example:alice", + keyAgreements: [ + "did:example:alice#key-x25519-not-in-secrets-1", + "did:example:alice#key-x25519-1", + "did:example:alice#key-p256-1", + "did:example:alice#key-p521-1", + ], + authentications: [ + "did:example:alice#key-1", + "did:example:alice#key-2", + "did:example:alice#key-3", + ], + verificationMethods: [ + VerificationMethod(id: "did:example:alice#key-x25519-1", + type: .jsonWebKey2020, + controller: "did:example:alice#key-x25519-1", + verificationMaterial: .jwk(value: "{\"crv\": \"X25519\", \"kty\": \"OKP\", \"x\": \"avH0O2Y4tqLAq8y9zpianr8ajii5m4F_mICrzNlatXs\"}")), + VerificationMethod(id: "did:example:alice#key-p256-1", + type: .jsonWebKey2020, + controller: "did:example:alice#key-p256-1", + verificationMaterial: .jwk(value: "{\"crv\": \"P-256\", \"kty\": \"EC\", \"x\": \"L0crjMN1g0Ih4sYAJ_nGoHUck2cloltUpUVQDhF2nHE\", \"y\": \"SxYgE7CmEJYi7IDhgK5jI4ZiajO8jPRZDldVhqFpYoo\"}")), + VerificationMethod(id: "did:example:alice#key-p521-1", + type: .jsonWebKey2020, + controller: "did:example:alice#key-p521-1", + verificationMaterial: .jwk(value: "{\"crv\": \"P-521\", \"kty\": \"EC\", \"x\": \"AHBEVPRhAv-WHDEvxVM9S0px9WxxwHL641Pemgk9sDdxvli9VpKCBdra5gg_4kupBDhz__AlaBgKOC_15J2Byptz\", \"y\": \"AciGcHJCD_yMikQvlmqpkBbVqqbg93mMVcgvXBYAQPP-u9AF7adybwZrNfHWCKAQwGF9ugd0Zhg7mLMEszIONFRk\"}")), + VerificationMethod(id: "did:example:alice#key-not-in-secrets-1", + type: .jsonWebKey2020, + controller: "did:example:alice#key-not-in-secrets-1", + verificationMaterial: .jwk(value: "{ \"crv\": \"Ed25519\", \"kty\": \"OKP\", \"x\": \"G-boxFB6vOZBu-wXkm-9Lh79I8nf9Z50cILaOgKKGww\" }")), + VerificationMethod(id: "did:example:alice#key-1", + type: .jsonWebKey2020, + controller: "did:example:alice#key-1", + verificationMaterial: .jwk(value: "{ \"crv\": \"Ed25519\", \"kty\": \"OKP\", \"x\": \"G-boxFB6vOZBu-wXkm-9Lh79I8nf9Z50cILaOgKKGww\" }")), + VerificationMethod(id: "did:example:alice#key-2", + type: .jsonWebKey2020, + controller: "did:example:alice#key-2", + verificationMaterial: .jwk(value: "{ \"crv\": \"P-256\", \"kty\": \"EC\", \"x\": \"2syLh57B-dGpa0F8p1JrO6JU7UUSF6j7qL-vfk1eOoY\", \"y\": \"BgsGtI7UPsObMRjdElxLOrgAO9JggNMjOcfzEPox18w\" }")), + VerificationMethod(id: "did:example:alice#key-2", + type: .jsonWebKey2020, + controller: "did:example:alice#key-2", + verificationMaterial: .jwk(value: "{ \"crv\": \"secp256k1\", \"kty\": \"EC\", \"x\": \"aToW5EaTq5mlAf8C5ECYDSkqsJycrW-e1SQ6_GJcAOk\", \"y\": \"JAGX94caA21WKreXwYUaOCYTBMrqaX4KWIlsQZTHWCk\" }")), + + ], + services: []) + +let ALICE_SECRETS = [ + Secret(id: "did:example:alice#key-1", + type: .jsonWebKey2020, + secretMaterial: .jwk(value: "{ \"crv\": \"Ed25519\", \"d\": \"pFRUKkyzx4kHdJtFSnlPA9WzqkDT1HWV0xZ5OYZd2SY\", \"kty\": \"OKP\", \"x\": \"G-boxFB6vOZBu-wXkm-9Lh79I8nf9Z50cILaOgKKGww\" }")), + Secret(id: "did:example:alice#key-2", + type: .jsonWebKey2020, + secretMaterial: .jwk(value: "{ \"crv\": \"P-256\", \"d\": \"7TCIdt1rhThFtWcEiLnk_COEjh1ZfQhM4bW2wz-dp4A\", \"kty\": \"EC\", \"x\": \"2syLh57B-dGpa0F8p1JrO6JU7UUSF6j7qL-vfk1eOoY\", \"y\": \"BgsGtI7UPsObMRjdElxLOrgAO9JggNMjOcfzEPox18w\" }")), + Secret(id: "did:example:alice#key-3", + type: .jsonWebKey2020, + secretMaterial: .jwk(value: "{ \"crv\": \"secp256k1\", \"d\": \"N3Hm1LXA210YVGGsXw_GklMwcLu_bMgnzDese6YQIyA\", \"kty\": \"EC\", \"x\": \"aToW5EaTq5mlAf8C5ECYDSkqsJycrW-e1SQ6_GJcAOk\", \"y\": \"JAGX94caA21WKreXwYUaOCYTBMrqaX4KWIlsQZTHWCk\" }")), + Secret(id: "did:example:alice#key-x25519-1", + type: .jsonWebKey2020, + secretMaterial: .jwk(value: "{ \"crv\": \"X25519\", \"d\": \"r-jK2cO3taR8LQnJB1_ikLBTAnOtShJOsHXRUWT-aZA\", \"kty\": \"OKP\", \"x\": \"avH0O2Y4tqLAq8y9zpianr8ajii5m4F_mICrzNlatXs\" }")), + Secret(id: "did:example:alice#key-p256-1", + type: .jsonWebKey2020, + secretMaterial: .jwk(value: "{ \"crv\": \"P-256\", \"d\": \"sB0bYtpaXyp-h17dDpMx91N3Du1AdN4z1FUq02GbmLw\", \"kty\": \"EC\", \"x\": \"L0crjMN1g0Ih4sYAJ_nGoHUck2cloltUpUVQDhF2nHE\", \"y\": \"SxYgE7CmEJYi7IDhgK5jI4ZiajO8jPRZDldVhqFpYoo\" }")), + Secret(id: "did:example:alice#key-p521-1", + type: .jsonWebKey2020, + secretMaterial: .jwk(value: "{ \"crv\": \"P-521\", \"d\": \"AQCQKE7rZpxPnX9RgjXxeywrAMp1fJsyFe4cir1gWj-8t8xWaM_E2qBkTTzyjbRBu-JPXHe_auT850iYmE34SkWi\", \"kty\": \"EC\", \"x\": \"AHBEVPRhAv-WHDEvxVM9S0px9WxxwHL641Pemgk9sDdxvli9VpKCBdra5gg_4kupBDhz__AlaBgKOC_15J2Byptz\", \"y\": \"AciGcHJCD_yMikQvlmqpkBbVqqbg93mMVcgvXBYAQPP-u9AF7adybwZrNfHWCKAQwGF9ugd0Zhg7mLMEszIONFRk\" }")), + ] + +let BOB_DID = "did:example:bob" + +let BOB_DID_DOC = DidDoc(did: "did:example:bob", + keyAgreements: [ + "did:example:bob#key-x25519-1", + "did:example:bob#key-x25519-2", + "did:example:bob#key-x25519-3", + "did:example:bob#key-p256-1", + "did:example:bob#key-p256-2", + "did:example:bob#key-p384-1", + "did:example:bob#key-p384-2", + "did:example:bob#key-p521-1", + "did:example:bob#key-p521-2", + ], + authentications: [], + verificationMethods: [ + VerificationMethod(id: "did:example:bob#key-x25519-1", + type: .jsonWebKey2020, + controller: "did:example:bob#key-x25519-1", + verificationMaterial: .jwk(value: "{ \"crv\": \"X25519\", \"kty\": \"OKP\", \"x\": \"GDTrI66K0pFfO54tlCSvfjjNapIs44dzpneBgyx0S3E\" }") ), + VerificationMethod(id: "did:example:bob#key-x25519-2", + type: .jsonWebKey2020, + controller: "did:example:bob#key-x25519-2", + verificationMaterial: .jwk(value: "{ \"crv\": \"X25519\", \"kty\": \"OKP\", \"x\": \"UT9S3F5ep16KSNBBShU2wh3qSfqYjlasZimn0mB8_VM\" }") ), + VerificationMethod(id: "did:example:bob#key-x25519-3", + type: .jsonWebKey2020, + controller: "did:example:bob#key-x25519-3", + verificationMaterial: .jwk(value: "{ \"crv\": \"X25519\", \"kty\": \"OKP\", \"x\": \"82k2BTUiywKv49fKLZa-WwDi8RBf0tB0M8bvSAUQ3yY\" }") ), + VerificationMethod(id: "did:example:bob#key-p256-1", + type: .jsonWebKey2020, + controller: "did:example:bob#key-p256-1", + verificationMaterial: .jwk(value: "{ \"crv\": \"P-256\", \"kty\": \"EC\", \"x\": \"FQVaTOksf-XsCUrt4J1L2UGvtWaDwpboVlqbKBY2AIo\", \"x\": \"6XFB9PYo7dyC5ViJSO9uXNYkxTJWn0d_mqJ__ZYhcNY\" }") ), + VerificationMethod(id: "did:example:bob#key-p256-2", + type: .jsonWebKey2020, + controller: "did:example:bob#key-p256-2", + verificationMaterial: .jwk(value: "{ \"crv\": \"P-256\", \"kty\": \"EC\", \"x\": \"n0yBsGrwGZup9ywKhzD4KoORGicilzIUyfcXb1CSwe0\", \"x\": \"ov0buZJ8GHzV128jmCw1CaFbajZoFFmiJDbMrceCXIw\" }") ), + VerificationMethod(id: "did:example:bob#key-p384-1", + type: .jsonWebKey2020, + controller: "did:example:bob#key-p384-1", + verificationMaterial: .jwk(value: "{ \"crv\": \"P-384\", \"kty\": \"EC\", \"x\": \"MvnE_OwKoTcJVfHyTX-DLSRhhNwlu5LNoQ5UWD9Jmgtdxp_kpjsMuTTBnxg5RF_Y\", \"x\": \"X_3HJBcKFQEG35PZbEOBn8u9_z8V1F9V1Kv-Vh0aSzmH-y9aOuDJUE3D4Hvmi5l7\" }") ), + VerificationMethod(id: "did:example:bob#key-p384-2", + type: .jsonWebKey2020, + controller: "did:example:bob#key-p384-2", + verificationMaterial: .jwk(value: "{ \"crv\": \"P-384\", \"kty\": \"EC\", \"x\": \"2x3HOTvR8e-Tu6U4UqMd1wUWsNXMD0RgIunZTMcZsS-zWOwDgsrhYVHmv3k_DjV3\", \"x\": \"W9LLaBjlWYcXUxOf6ECSfcXKaC3-K9z4hCoP0PS87Q_4ExMgIwxVCXUEB6nf0GDd\" }") ), + VerificationMethod(id: "did:example:bob#key-p521-1", + type: .jsonWebKey2020, + controller: "did:example:bob#key-p521-1", + verificationMaterial: .jwk(value: "{ \"crv\": \"P-521\", \"kty\": \"EC\", \"x\": \"Af9O5THFENlqQbh2Ehipt1Yf4gAd9RCa3QzPktfcgUIFADMc4kAaYVViTaDOuvVS2vMS1KZe0D5kXedSXPQ3QbHi\", \"x\": \"ATZVigRQ7UdGsQ9j-omyff6JIeeUv3CBWYsZ0l6x3C_SYqhqVV7dEG-TafCCNiIxs8qeUiXQ8cHWVclqkH4Lo1qH\" }") ), + VerificationMethod(id: "did:example:bob#key-p521-2", + type: .jsonWebKey2020, + controller: "did:example:bob#key-p521-2", + verificationMaterial: .jwk(value: "{ \"crv\": \"P-521\", \"kty\": \"EC\", \"x\": \"ATp_WxCfIK_SriBoStmA0QrJc2pUR1djpen0VdpmogtnKxJbitiPq-HJXYXDKriXfVnkrl2i952MsIOMfD2j0Ots\", \"x\": \"AEJipR0Dc-aBZYDqN51SKHYSWs9hM58SmRY1MxgXANgZrPaq1EeGMGOjkbLMEJtBThdjXhkS5VlXMkF0cYhZELiH\" }") ), + ], + services: []) + +let BOB_SECRETS = [ + Secret(id: "did:example:bob#key-x25519-1", + type: .jsonWebKey2020, + secretMaterial: .jwk(value: "{ \"crv\": \"X25519\", \"d\": \"b9NnuOCB0hm7YGNvaE9DMhwH_wjZA1-gWD6dA0JWdL0\", \"kty\": \"OKP\", \"x\": \"GDTrI66K0pFfO54tlCSvfjjNapIs44dzpneBgyx0S3E\" }")), + Secret(id: "did:example:bob#key-x25519-2", + type: .jsonWebKey2020, + secretMaterial: .jwk(value: "{ \"crv\": \"X25519\", \"d\": \"p-vteoF1gopny1HXywt76xz_uC83UUmrgszsI-ThBKk\", \"kty\": \"OKP\", \"x\": \"UT9S3F5ep16KSNBBShU2wh3qSfqYjlasZimn0mB8_VM\" }")), + Secret(id: "did:example:bob#key-x25519-3", + type: .jsonWebKey2020, + secretMaterial: .jwk(value: "{ \"crv\": \"X25519\", \"d\": \"f9WJeuQXEItkGM8shN4dqFr5fLQLBasHnWZ-8dPaSo0\", \"kty\": \"OKP\", \"x\": \"82k2BTUiywKv49fKLZa-WwDi8RBf0tB0M8bvSAUQ3yY\" }")), + Secret(id: "did:example:bob#key-p256-1", + type: .jsonWebKey2020, + secretMaterial: .jwk(value: "{ \"crv\": \"P-256\", \"d\": \"PgwHnlXxt8pwR6OCTUwwWx-P51BiLkFZyqHzquKddXQ\", \"kty\": \"EC\", \"x\": \"FQVaTOksf-XsCUrt4J1L2UGvtWaDwpboVlqbKBY2AIo\", \"y\": \"6XFB9PYo7dyC5ViJSO9uXNYkxTJWn0d_mqJ__ZYhcNY\" }")), + Secret(id: "did:example:bob#key-p256-2", + type: .jsonWebKey2020, + secretMaterial: .jwk(value: "{ \"crv\": \"P-256\", \"d\": \"agKz7HS8mIwqO40Q2dwm_Zi70IdYFtonN5sZecQoxYU\", \"kty\": \"EC\", \"x\": \"n0yBsGrwGZup9ywKhzD4KoORGicilzIUyfcXb1CSwe0\", \"y\": \"ov0buZJ8GHzV128jmCw1CaFbajZoFFmiJDbMrceCXIw\" }")), + Secret(id: "did:example:bob#key-p384-1", + type: .jsonWebKey2020, + secretMaterial: .jwk(value: "{ \"crv\": \"P-384\", \"d\": \"ajqcWbYA0UDBKfAhkSkeiVjMMt8l-5rcknvEv9t_Os6M8s-HisdywvNCX4CGd_xY\", \"kty\": \"EC\", \"x\": \"MvnE_OwKoTcJVfHyTX-DLSRhhNwlu5LNoQ5UWD9Jmgtdxp_kpjsMuTTBnxg5RF_Y\", \"y\": \"X_3HJBcKFQEG35PZbEOBn8u9_z8V1F9V1Kv-Vh0aSzmH-y9aOuDJUE3D4Hvmi5l7\" }")), + Secret(id: "did:example:bob#key-p384-2", + type: .jsonWebKey2020, + secretMaterial: .jwk(value: "{ \"crv\": \"P-384\", \"d\": \"OiwhRotK188BtbQy0XBO8PljSKYI6CCD-nE_ZUzK7o81tk3imDOuQ-jrSWaIkI-T\", \"kty\": \"EC\", \"x\": \"2x3HOTvR8e-Tu6U4UqMd1wUWsNXMD0RgIunZTMcZsS-zWOwDgsrhYVHmv3k_DjV3\", \"y\": \"W9LLaBjlWYcXUxOf6ECSfcXKaC3-K9z4hCoP0PS87Q_4ExMgIwxVCXUEB6nf0GDd\" }")), + Secret(id: "did:example:bob#key-p521-1", + type: .jsonWebKey2020, + secretMaterial: .jwk(value: "{ \"crv\": \"P-521\", \"d\": \"AV5ocjvy7PkPgNrSuvCxtG70NMj6iTabvvjSLbsdd8OdI9HlXYlFR7RdBbgLUTruvaIRhjEAE9gNTH6rWUIdfuj6\", \"kty\": \"EC\", \"x\": \"Af9O5THFENlqQbh2Ehipt1Yf4gAd9RCa3QzPktfcgUIFADMc4kAaYVViTaDOuvVS2vMS1KZe0D5kXedSXPQ3QbHi\", \"y\": \"ATZVigRQ7UdGsQ9j-omyff6JIeeUv3CBWYsZ0l6x3C_SYqhqVV7dEG-TafCCNiIxs8qeUiXQ8cHWVclqkH4Lo1qH\" }")), + Secret(id: "did:example:bob#key-p521-2", + type: .jsonWebKey2020, + secretMaterial: .jwk(value: "{ \"crv\": \"P-521\", \"d\": \"ABixMEZHsyT7SRw-lY5HxdNOofTZLlwBHwPEJ3spEMC2sWN1RZQylZuvoyOBGJnPxg4-H_iVhNWf_OtgYODrYhCk\", \"kty\": \"EC\", \"x\": \"ATp_WxCfIK_SriBoStmA0QrJc2pUR1djpen0VdpmogtnKxJbitiPq-HJXYXDKriXfVnkrl2i952MsIOMfD2j0Ots\", \"y\": \"AEJipR0Dc-aBZYDqN51SKHYSWs9hM58SmRY1MxgXANgZrPaq1EeGMGOjkbLMEJtBThdjXhkS5VlXMkF0cYhZELiH\" }")), + ] diff --git a/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/Info.plist b/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/Info.plist new file mode 100644 index 0000000..0c67376 --- /dev/null +++ b/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/Info.plist @@ -0,0 +1,5 @@ + + + + + diff --git a/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/ViewController.swift b/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/ViewController.swift new file mode 100644 index 0000000..a552b6f --- /dev/null +++ b/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/DidcommExampleiOS/ViewController.swift @@ -0,0 +1,132 @@ +import UIKit +import DidcommSDK + +class ViewController: UIViewController { + + let msg = Message(id: "1234567890", + typ: "application/didcomm-plain+json", + type: "http://example.com/protocols/lets_do_lunch/1.0/proposal", + body: "{\"messagespecificattribute\": \"and its value\"}", + from: "did:example:alice", + to: ["did:example:bob"], + thid: nil, + pthid: nil, + extraHeaders: [:], + createdTime: 1516269022, + expiresTime: 1516385931, + fromPrior: nil, + attachments: nil) + + override func viewDidLoad() { + super.viewDidLoad() + placeLabel() + nonRepudiableEncryption(msg) + plainText(msg) + packSignedUnencrypted(msg) + } + + private func placeLabel() { + let title = UILabel() + title.text = "Didcomm Swift Demo Initialized:\nSee Xcode logs." + title.numberOfLines = 2 + title.sizeToFit() + title.textAlignment = .center + title.center = self.view.center + self.view.addSubview(title) + } + + private func packSignedUnencrypted(_ msg : Message) { + let didResolver = ExampleDidResolver(knownDids: [ALICE_DID_DOC, BOB_DID_DOC]) + let secretsResolver = ExampleSecretsResolver(knownSecrets: ALICE_SECRETS) + let _ = DidComm(didResolver: didResolver, secretResolver: secretsResolver) + .packSigned(msg: msg, signBy: ALICE_DID, cb: self) + } + + private func plainText(_ msg : Message) { + let didResolver = ExampleDidResolver(knownDids: [ALICE_DID_DOC, BOB_DID_DOC]) + let secretsResolver = ExampleSecretsResolver(knownSecrets: []) + let _ = DidComm(didResolver: didResolver, secretResolver: secretsResolver) + .packPlaintext(msg: msg, cb: self) + } + + private func nonRepudiableEncryption(_ msg : Message) { + + let didResolver = ExampleDidResolver(knownDids: [ALICE_DID_DOC, BOB_DID_DOC]) + let secretsResolver = ExampleSecretsResolver(knownSecrets: ALICE_SECRETS) + + /** + This is the standard options for Encrypting. + */ + let options = PackEncryptedOptions(protectSender: false, + forward: false, + forwardHeaders: [:], + messagingService: nil, + encAlgAuth: .a256cbcHs512Ecdh1puA256kw, + encAlgAnon: .xc20pEcdhEsA256kw) + + let _ = DidComm(didResolver: didResolver, secretResolver: secretsResolver) + .packEncrypted(msg: msg, + to: BOB_DID, + from: ALICE_DID, + signBy: ALICE_DID, + options: options, + cb: self) + } + + + + /** + Helper function to unpack a message. + */ + private func unpackDids(msg: String, + knownDids: [DidDoc], + knownSecrets: [Secret], + packOpt: UnpackOptions = .init(expectDecryptByAllKeys: false, + unwrapReWrappingForward: false)) { + + let didResolver = ExampleDidResolver(knownDids: knownDids) + let secretsResolver = ExampleSecretsResolver(knownSecrets: knownSecrets) + let _ = DidComm(didResolver: didResolver, secretResolver: secretsResolver) + .unpack(msg: msg, options: packOpt, cb: self) + } +} + + + +extension ViewController: OnPackEncryptedResult, OnPackPlaintextResult, OnPackSignedResult, OnUnpackResult { + + func success(result: String, metadata: PackSignedMetadata) { + print("[PackSigned] SUCESS\n") + print("Result: ", result) + print("Metadata: ", metadata) + + self.unpackDids(msg: result, knownDids: [ALICE_DID_DOC, BOB_DID_DOC], knownSecrets: []) + } + + func success(result: String) { + print("[OnPackPlaintext] SUCESS\n") + print("Result: ", result) + + self.unpackDids(msg: result, knownDids: [ALICE_DID_DOC, BOB_DID_DOC], knownSecrets: []) + } + + func success(result: String, metadata: PackEncryptedMetadata) { + print("[PackEncrypted] SUCESS:\n") + print("Result: ", result) + print("Metadata: ", metadata) + + self.unpackDids(msg: result, knownDids: [ALICE_DID_DOC, BOB_DID_DOC], knownSecrets: BOB_SECRETS) + } + + func success(result: Message, metadata: UnpackMetadata) { + print("[Unpack] SUCESS:\n") + print("Result: ", result) + } + + /// This function implements the error protocol for `OnPackEncryptedResult`, `OnPackPlaintextResult`, `OnPackSignedResult` and `OnUnpackResult`. + func error(err: ErrorKind, msg: String) { + print("[ERROR]:\n") + print(err) + print(msg) + } +} diff --git a/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/Podfile b/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/Podfile new file mode 100644 index 0000000..d1314bb --- /dev/null +++ b/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/Podfile @@ -0,0 +1,10 @@ +# Uncomment the next line to define a global platform for your project +platform :ios, '13.0' +source 'git@github.com:sicpa-dlab/didcomm-rust.git' + +target 'DidcommExampleiOS' do + # Comment the next line if you don't want to use dynamic frameworks + use_frameworks! + pod 'DidcommSDK', '0.3.1' + +end diff --git a/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/Podfile.lock b/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/Podfile.lock new file mode 100644 index 0000000..21afc9a --- /dev/null +++ b/affinidi-messaging-didcomm/wrappers/swift/examples/DidcommExampleiOS/Podfile.lock @@ -0,0 +1,16 @@ +PODS: + - DidcommSDK (0.3.1) + +DEPENDENCIES: + - DidcommSDK (= 0.3.1) + +SPEC REPOS: + "git@github.com:rodrigofranzoi/didcomm-rust.git": + - DidcommSDK + +SPEC CHECKSUMS: + DidcommSDK: 99be16fe25310f791171bbccac576419272e217a + +PODFILE CHECKSUM: db7d8bde5395a5700b4977ece27e4bd4af0b4a89 + +COCOAPODS: 1.11.3 diff --git a/affinidi-messaging-didcomm/wrappers/swift/examples/README.md b/affinidi-messaging-didcomm/wrappers/swift/examples/README.md new file mode 100644 index 0000000..22cee42 --- /dev/null +++ b/affinidi-messaging-didcomm/wrappers/swift/examples/README.md @@ -0,0 +1,15 @@ +## Example for iOS + +This is a short description of how to run the example of DidcommSDK. You can find the source code [here](./DidcommExampleiOS). + +Make sure you have pods installed: + +Run on [DidcommExampleiOS](./DidcommExampleiOS) folder. + +```bash +pod install +``` + +Then open `DidcommExampleiOS.xcworkspace` created installing pods. + +Now you can run DidcommExampleiOS for any iOS device. See Xcode logs to see the SDK working. \ No newline at end of file diff --git a/affinidi-messaging-didcomm/wrappers/swift/examples/base.swift b/affinidi-messaging-didcomm/wrappers/swift/examples/base.swift new file mode 100644 index 0000000..a2bd024 --- /dev/null +++ b/affinidi-messaging-didcomm/wrappers/swift/examples/base.swift @@ -0,0 +1 @@ +// TBD \ No newline at end of file diff --git a/affinidi-messaging-mediator/.gitignore b/affinidi-messaging-mediator/.gitignore new file mode 100644 index 0000000..c4d5f81 --- /dev/null +++ b/affinidi-messaging-mediator/.gitignore @@ -0,0 +1,4 @@ +/target +/conf/keys +/conf/secrets.json +dump.rdb \ No newline at end of file diff --git a/affinidi-messaging-mediator/CHANGELOG.md b/affinidi-messaging-mediator/CHANGELOG.md new file mode 100644 index 0000000..51004ac --- /dev/null +++ b/affinidi-messaging-mediator/CHANGELOG.md @@ -0,0 +1,66 @@ +# Affinidi Trust Network - Affinidi Trusted Messaging - Mediator Service + +## Changelog history + +## 3rd September 2024 + +* Added the ability to configure the Mediator DID Cache settings via mediator configuration + +## 20th July 2024 + +* Message Pickup Message-Delivery and Messages Received implemented + +## 24th June 2024 + +* Added streaming.enabled configuration option + +### 21st June 2024 + +* Refactored process() on inbound messages so that there is an option to store the message response - some responses are live only, if no connection then no need to do anything more. +* Added delta difference on statistics reporting + +### 20th June 2024 + +* Updating mediator statistics and metrics to improve clarity + * Added websocket connections + * DELETED_BYTES + * SENT_BYTES +* Improved websocket handling of closed connections with error handling +* Added authenticated session metrics (created and success) + +### 15th June 2024 + +* Statistics reporting thread implemented + +### 13th June 2024 + +* WebSocket implementation handler implemented. More work to be done. + +### 12th June 2024 + +* Removed unused configuration options (allow/deny lists) +* Added method to load Mediator secrets from AWS Secrets Manager +* Added method to load the mediator DID from AWS Systems Manager Parameter Store +* Added method to load the jwt secret key from AWS Secrets Manager + +### 9th June 2024 + +* Refactored inbound message handling so it returns more relevant information to clients + +### 8th June 2024 + +* Refactored namespace for several SDK structs +* implementing ability to get messages via REST API + +### 7th June 2024 + +* Added authentication check on database session.state, protects against replay attack. +* Added Sha256 hash of DID to Session Struct. Optimisation +* Changed Mediator GLOBAL Stats from u64 to i64 in case of negative values +* Removed unused common/Stats module +* improved delete_message return type to DeleteMessageResponse Struct, with returns for success and errors +* Renamed send_message() to send_didcomm_message() + +### 6th June 2024 + +* Modified list_messages() to accept a DID (mainly for future use where you could check multiple DID's owned by you) diff --git a/affinidi-messaging-mediator/Cargo.toml b/affinidi-messaging-mediator/Cargo.toml new file mode 100644 index 0000000..4cc8307 --- /dev/null +++ b/affinidi-messaging-mediator/Cargo.toml @@ -0,0 +1,68 @@ +[package] +name = "affinidi-messaging-mediator" +version = "0.2.1" +description = "DIDComm Mediator service for Affinidi Secure Messaging" +edition.workspace = true +authors.workspace = true +readme.workspace = true +homepage.workspace = true +keywords.workspace = true +publish.workspace = true +license.workspace = true + +[dependencies] +async-convert = "1" +async-trait = "0.1" +affinidi-messaging-sdk = { version = "0.1.3" } +affinidi-did-resolver-cache-sdk = { version = "0.1.5" } +aws-config = "1.5" +aws-sdk-dynamodb = "1.29" +aws-sdk-memorydb = "1.26.0" +aws-sdk-secretsmanager = "1.26.0" +aws-sdk-ssm = "1.26.0" +axum = { version = "0.7", features = ["ws"] } +axum-extra = { version = "0.9.3", features = ["typed-header"] } +axum-server = { version = "0.7", features = ["tls-rustls"] } +base64 = "0.22" +chrono = "0.4" +deadpool-redis = { version = "0.16", features = ["rt_tokio_1"] } +affinidi-messaging-didcomm = { version = "0.5", path = "../affinidi-messaging-didcomm" } #TODO: switch to crates.io +hostname = "0.4" +http = "1" +jsonwebtoken = "9.3" +itertools = "0.13" +rand = "0.8" +redis = { version = "0.26", features = [ + "tokio-rustls-comp", + "tls-rustls-insecure", +] } +regex = "1" +ring = { version = "0.17", features = ["std"] } +rustls = { version = "0.23", default-features = false, features = [ + "aws_lc_rs", + "tls12", +] } +serde = { version = "1.0", features = ["derive", "rc"] } +serde_json = "1.0" +sha256 = "1.5" +ssi = "0.8" +thiserror = "1.0" +tokio = { version = "1.37.0", features = ["full"] } +tokio-stream = "0.1" +toml = "0.8" +tower-http = { version = "0.5", features = ["cors", "trace", "limit"] } +tracing = { version = "0.1", features = [ + "max_level_debug", + "release_max_level_info", +] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +uuid = { version = "1.8.0", features = ["v4", "fast-rng"] } + +[dev-dependencies] +did-peer.workspace = true +rcgen = { version = "0.13", default-features = false, features = [ + "aws_lc_rs", + "pem", +] } +reqwest = { version = "0.12", features = ["rustls-tls-manual-roots", "json"] } +time = "0.3" diff --git a/affinidi-messaging-mediator/LICENCE-APACHE b/affinidi-messaging-mediator/LICENCE-APACHE new file mode 100644 index 0000000..d51557b --- /dev/null +++ b/affinidi-messaging-mediator/LICENCE-APACHE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [2024] [Affinidi Pte. Ltd.] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/affinidi-messaging-mediator/README.md b/affinidi-messaging-mediator/README.md new file mode 100644 index 0000000..81ac08d --- /dev/null +++ b/affinidi-messaging-mediator/README.md @@ -0,0 +1,68 @@ +# Affinidi Messaging - Mediator Service + +[[_TOC_]] + +## Overview + +A DIDComm Messaging v2 mediator & relay service that listens to send &receive messages over https. + +## Dependencies + +To run the mediator, it requires these packages that is also part of the Affinidi Messaging project. + +1. [affinidi-messaging-didcomm](../affinidi-messaging-didcomm/) - Affinidi Messaging DIDComm implementation, a modified version of [didcomm-rust](https://github.com/sicpa-dlab/didcomm-rust) project. +2. [affinidi-messaging-sdk](../affinidi-messaging-sdk/) - a Software Development Kit (SDK) to simplify the implementation of Affinidi Messaging into your application. + +## Prerequisites + +To build and run this project, you need to set up the following: + +1. Install Rust on your machine if you haven't installed it yet using [this guide](https://www.rust-lang.org/tools/install). +2. Install the Docker on your machine if you haven't installed it yet using [this guide](https://docs.docker.com/desktop/). We will need this to run Redis instance for the mediator. + +## Running affinidi-messaging-mediator service + +1. Run Redis docker container using the command below: + + ```bash + docker run --name=redis-local --publish=6379:6379 --hostname=redis --restart=on-failure --detach redis:latest + ``` + +2. Navigate to the `affinidi-messaging-mediator` subfolder and create certificates for `affinidi-messaging-mediator` service: + + ```bash + cd affinidi-messaging-mediator + cargo run --example create_local_certs + ``` + + This will generate certificate files in the `affinidi-messaging-mediator/conf/keys` folder. You should use `client.chain` file to override the default SSL certificates in `affinidi-messaging-sdk`, like: + + ```rust + let mut config = Config::builder() + .with_ssl_certificates(&mut vec![ + "../affinidi-messaging-mediator/conf/keys/client.chain".into() + ]) + ``` + +3. In the same `affinidi-messaging-mediator` subfolder run the following command to generate DID and the corresponding keys: + + ```bash + cargo run --example generate_secrets + ``` + + This will generate `affinidi-messaging-mediator/conf/secrets.json-generated` file containing a did:peer together with the pair of keys for verification and encryption. Use the generated did:peer as a value for `` placeholder in following commands as well as in [affinidi-messaging-sdk - Examples](../affinidi-messaging-sdk#examples). + +4. Save the generated `secrets.json-generated` file as `affinidi-messaging-mediator/conf/secrets.json`. + +5. Start `affinidi-messaging-mediator` service via: + + ```bash + cd affinidi-messaging-mediator + export MEDIATOR_DID=did:// + export REDIS_URL=redis://@localhost:6379 + cargo run + ``` + +## Examples + +Refer to [affinidi-messaging-sdk - Examples](../affinidi-messaging-sdk#examples). diff --git a/affinidi-messaging-mediator/conf/mediator.toml b/affinidi-messaging-mediator/conf/mediator.toml new file mode 100644 index 0000000..49d9bf5 --- /dev/null +++ b/affinidi-messaging-mediator/conf/mediator.toml @@ -0,0 +1,145 @@ +### log_level: trace debug info warn error +### default: info +log_level = "debug" + +### listen_address: : that this service will listen on. +### Default: 0.0.0.0:7037 +listen_address = "${LISTEN_ADDRESS:0.0.0.0:7037}" + +### mediator_did: DID of the mediator +### REQUIRED: DID of the mediator +### Supported formats: +### - did://did:peer:: +### - aws_parameter_store:// - Load DID from AWS Systems Manager Parameter Store +mediator_did = "${MEDIATOR_DID:did://did:peer:2.Vz6MkiToqovww7vYtxm1xNM15u9JzqzUFZ1k7s7MazYJUyAxv.EzQ3shQLqRUza6AMJFbPuMdvFRFWm1wKviQRnQSC1fScovJN4s.SeyJ0IjoiRElEQ29tbU1lc3NhZ2luZyIsInMiOnsidXJpIjoiaHR0cHM6Ly8xMjcuMC4wLjE6NzAzNyIsImEiOlsiZGlkY29tbS92MiJdLCJyIjpbXX19}" +#mediator_did = "${MEDIATOR_DID:aws_parameter_store:///dev/atn/atm/global/did}" + +### mediator_secrets: Secrets of the mediator +### REQUIRED: Location of secrets +### Supported formats: +### - file:// - Load secrets from a file +### - aws_secrets:// - Load secrets from AWS Secrets Manager +### See secrets.json-example for the format of the secrets +### NOTE: The key identifiers (id) must match the key identifiers in the mediator_did +#mediator_secrets = "${MEDIATOR_SECRETS:aws_secrets://dev/atn/atm/mediator/global/secrets}" +mediator_secrets = "${MEDIATOR_SECRETS:file://./conf/secrets.json}" + +[server] +### http_size_limit: Maximum size of a http request payload in bytes +### Default: 10485760 (10MB) +### It is recommended to use infrastructure level limitation instead of application level limitations +http_size_limit = "${HTTP_SIZE_LIMIT:10485760}" + +### ws_size_limit: Maximum size of a http request payload in bytes +### Default: 10485760 (10MB) +### It is recommended to use infrastructure level limitation instead of application level limitations +ws_size_limit = "${WS_SIZE_LIMIT:10485760}" + +[database] +### database_url: URL of the Redis compatable database +### Default: redis://127.0.0.1/ +database_url = "${REDIS_URL:redis://127.0.0.1/}" + +### lua_scripts: path to lua scripts used in Redis +### Default: file://../affinidi-messaging-processor/redis-functions/atm-functions.lua +lua_scripts = "${MEDIATOR_LUA_SCRIPTS:file://../affinidi-messaging-processor/redis-functions/atm-functions.lua}" + +### database_pool_size: Number of connections to the database +### Default: 10 +database_pool_size = "${DATABASE_POOL_SIZE:10}" + +### database_timeout: Timeout for database operations in seconds +### Default: 2 +database_timeout = "${DATABASE_TIMEOUT:2}" + +### max_message_size: Maximum size of a message in bytes +### Default: 1048576 (1MB) +max_message_size = "${MAX_MESSAGE_SIZE:1048576}" + +### max_queued_messages: How many messages will we queue for a single recipient or a single sender +### Default: 100 +### NOTE: This is a per-recipient or per-sender limit +max_queued_messages = "${MAX_QUEUED_MESSAGES:100}" + +### message_expiry_minutes: Time to live in minutes for messages stored +### Messages will expire after this limit +### Default: 10080 (7 days) +message_expiry_minutes = "${MESSAGE_EXPIRY_MINUTES:10080}" + +[security] +### If true (default) SSL is enabled. +### default: true +### NOTE: do not use `false` in prod unless TLS is terminated outside of the service +use_ssl = "${USE_SSL:true}" + +### Note: If you comment out the following SSL configs, will default back to non-TLS mode. +### Useful if doing testing/debugging without requiring SSL certificate checks +### ssl_certificate_file: file that contains the SSL certificate for this service +ssl_certificate_file = "${SSL_CERTIFICATE_FILE:conf/keys/end.cert}" + +### ssl_key_file: file that contains the SSL certificate key +ssl_key_file = "${SSL_KEY_FILE:conf/keys/end.key}" + +### jwt_authorization_secret +### REQUIRED: Key string that is used to sign JWT tokens +### Supported Formats: +### - string:// - Use the key as is +### - aws_secrets:// - Load the key from AWS Secrets Manager +jwt_authorization_secret = "${AUTHORIZATION_SECRET:string://MFECAQEwBQYDK2VwBCIEIGaI0daTTfC-ys6xGZFrR2gXWGzOPr1UdEnRldEVmS6lgSEA_f9ZL1dQ3r7cALSXRZtZUkI_5sOca-6-SgLnbuPdD3k}" +#jwt_authorization_secret = "${AUTHORIZATION_SECRET:aws_secrets://dev/atn/atm/mediator/global/jwt_secret}" + +### cors_allow_origin: Comma separated list of origins that are allowed to access this service +### Default: * +### NOTE: Use * to allow all origins, otherwise for production you should limit the origins +### Example: "https://affinidi.com,https://example2.com" +# cors_allow_origin = "${CORS_ALLOW_ORIGIN:https://affinidi.com}" + +[streaming] +### enabled: If true, can live stream messages to subscribed recipients via WebSockets +### Default: true +enabled = "${STREAMING_ENABLED:true}" + +### uuid: Each subscriber needs a unique identifier, this is used to setup session tracking in the backend +### Default: hostname" +### Supported Formats: +### - hostname:// +### - string:// - Use the UUID as is +### NOTE: Having multiple subscribers with the same UUID will cause issues +uuid = "${STREAMING_UUID:hostname://}" + +[did_resolver] +### service_address: Address of the DID resolver service +### Default: None (Uses local DID resolver +#address = "${DID_RESOLVER_ADDRESS:ws://127.0.0.1:8080/did/v1/ws}" + +### cache_capacity: Number of DID's to cache in memory +### Default: 1000 +cache_capacity = "${DID_RESOLVER_CACHE_CAPACITY:1000}" + +### cache_ttl: Time to live in seconds for DID's in the cache +### Default: 300 (5 minutes) +cache_ttl = "${DID_RESOLVER_CACHE_TTL:300}" + +### network_timeout: Timeout in milliseconds for DID Resolver operations +### Default: 5000 milliseconds (you need to account for 3rd party DID resolution latency (blockchains, www etc) +network_timeout = "${DID_RESOLVER_NETWORK_TIMEOUT:5000}" + +### network_limit: Maximum number of concurrent requests to the DID resolver +### Default: 100 +network_limit = "${DID_RESOLVER_NETWORK_LIMIT:100}" + +[other] +### to_recipients_limit: Maximum number of recipients in a single message +### Default: 100 +### NOTE: Protects against a DOS attack where a single message can become a bomb with thousands of recipients +to_recipients_limit = "${TO_RECIPIENTS_LIMIT:100}" + +### approximate maximum amount of crypto operations per one message +### it is used to protect the mediator from DoS type attacts. +### Default: 1000 +crypto_operations_per_message_limit = "${CRYPTO_OPERATIONS_PER_MESSAGE_LIMIT:1000}" + +### to_keys_per_recipient_limit: Maximum number of keys in a single recipient did +### Default: 100 +### NOTE: Protects against a DOS attack where a single message can become a bomb with thousands of recipients and thousands keys of them +to_keys_per_recipient_limit = "${TO_KEYS_PER_DID_LIMIT:100}" \ No newline at end of file diff --git a/affinidi-messaging-mediator/conf/secrets.json-example b/affinidi-messaging-mediator/conf/secrets.json-example new file mode 100644 index 0000000..c4acc05 --- /dev/null +++ b/affinidi-messaging-mediator/conf/secrets.json-example @@ -0,0 +1,23 @@ +[ + { + "id": "did:peer:2.Vz6Mkp9f4p6rSJkgbxTpPXL861PSN6EB996fQv5vCq5Q9C5Me.EzQ3shpFNDUgbePPhbLmwNcTiNSAhcb511urztSmT7aavVamJ3.SeyJ0IjoiRElEQ29tbU1lc3NhZ2luZyIsInMiOnsidXJpIjoiaHR0cHM6Ly8xMjcuMC4wLjE6NzAzNyIsImEiOlsiZGlkY29tbS92MiJdLCJyIjpbXX19#key-1", + "type": "JsonWebKey2020", + "privateKeyJwk": { + "crv": "Ed25519", + "d": "...", + "kty": "OKP", + "x": "kBJx18jGP3bW_7vkmvSUiYzbFLwch0VBI-XRVowAONU" + } + }, + { + "id": "did:peer:2.Vz6Mkp9f4p6rSJkgbxTpPXL861PSN6EB996fQv5vCq5Q9C5Me.EzQ3shpFNDUgbePPhbLmwNcTiNSAhcb511urztSmT7aavVamJ3.SeyJ0IjoiRElEQ29tbU1lc3NhZ2luZyIsInMiOnsidXJpIjoiaHR0cHM6Ly8xMjcuMC4wLjE6NzAzNyIsImEiOlsiZGlkY29tbS92MiJdLCJyIjpbXX19#key-2", + "type": "JsonWebKey2020", + "privateKeyJwk": { + "crv": "secp256k1", + "d": "...", + "kty": "EC", + "x": "jsGSSTNv153VzdENREUACvlyLxh5UGPeE39UDXGyx9Q", + "y": "JGD1od91aLLz478tGgvR2LUs8cMK9FKzBa6bL5fX1bE" + } + } +] \ No newline at end of file diff --git a/affinidi-messaging-mediator/examples/create_local_certs.rs b/affinidi-messaging-mediator/examples/create_local_certs.rs new file mode 100644 index 0000000..debbe14 --- /dev/null +++ b/affinidi-messaging-mediator/examples/create_local_certs.rs @@ -0,0 +1,278 @@ +use std::collections::HashMap; +use std::env; +use std::fs::{self, File}; +use std::io::Write; +use std::net::IpAddr; +use std::path::PathBuf; +use std::str::FromStr; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::Duration; + +use rcgen::{ + BasicConstraints, CertificateParams, CertificateRevocationListParams, CertifiedKey, + DistinguishedName, DnType, ExtendedKeyUsagePurpose, Ia5String, IsCa, KeyIdMethod, KeyPair, + KeyUsagePurpose, RevocationReason, RevokedCertParams, RsaKeySize, SanType, SerialNumber, + SignatureAlgorithm, PKCS_ED25519, PKCS_RSA_SHA256, PKCS_RSA_SHA384, PKCS_RSA_SHA512, +}; +use time::OffsetDateTime; + +/* + * This code was "borrowed" from the rustls examples + * https://github.com/rustls/rustls/blob/main/rustls/examples/internal/test_ca.rs + */ +fn main() -> Result<(), Box> { + let mut certified_keys = HashMap::with_capacity(ROLES.len() * SIG_ALGS.len()); + for role in ROLES { + for alg in SIG_ALGS { + // Generate a key pair and serialize it to a PEM encoded file. + let key_pair = alg.key_pair(); + let mut key_pair_file = File::create(role.key_file_path(alg))?; + key_pair_file.write_all(key_pair.serialize_pem().as_bytes())?; + + // Issue a certificate for the key pair. For trust anchors, this will be self-signed. + // Otherwise we dig out the issuer and issuer_key for the issuer, which should have + // been produced in earlier iterations based on the careful ordering of roles. + let cert = match role { + Role::TrustAnchor => role.params(alg).self_signed(&key_pair)?, + Role::Intermediate => { + let issuer: &CertifiedKey = + certified_keys.get(&(Role::TrustAnchor, alg.inner)).unwrap(); + role.params(alg) + .signed_by(&key_pair, &issuer.cert, &issuer.key_pair)? + } + Role::EndEntity | Role::Client => { + let issuer = certified_keys + .get(&(Role::Intermediate, alg.inner)) + .unwrap(); + role.params(alg) + .signed_by(&key_pair, &issuer.cert, &issuer.key_pair)? + } + }; + + // Serialize the issued certificate to a PEM encoded file. + let mut cert_file = File::create(role.cert_pem_file_path(alg))?; + cert_file.write_all(cert.pem().as_bytes())?; + // And to a DER encoded file. + let mut cert_file = File::create(role.cert_der_file_path(alg))?; + cert_file.write_all(cert.der())?; + + // If we're not a trust anchor, generate a CRL for the certificate we just issued. + if role != Role::TrustAnchor { + // The CRL will be signed by the issuer of the certificate being revoked. For + // intermediates this will be the trust anchor, and for client/EE certs this will + // be the intermediate. + let issuer = match role { + Role::Intermediate => { + certified_keys.get(&(Role::TrustAnchor, alg.inner)).unwrap() + } + Role::EndEntity | Role::Client => certified_keys + .get(&(Role::Intermediate, alg.inner)) + .unwrap(), + _ => panic!("unexpected role for CRL generation: {role:?}"), + }; + let crl = crl_for_serial(cert.params().serial_number.clone().unwrap()) + .signed_by(&issuer.cert, &issuer.key_pair)?; + let mut crl_file = File::create( + alg.output_directory() + .join(format!("{}.revoked.crl.pem", role.label())), + )?; + crl_file.write_all(crl.pem().unwrap().as_bytes())?; + } + + // When we're issuing end entity or client certs we have a bit of extra work to do + // now that we have full chains in hand. + if matches!(role, Role::EndEntity | Role::Client) { + let root = &certified_keys + .get(&(Role::TrustAnchor, alg.inner)) + .unwrap() + .cert; + let intermediate = &certified_keys + .get(&(Role::Intermediate, alg.inner)) + .unwrap() + .cert; + + // Write the PEM chain and full chain files for the end entity and client certs. + // Chain files include the intermediate and root certs, while full chain files include + // the end entity or client cert as well. + for f in [ + ("chain", &[intermediate, root][..]), + ("fullchain", &[&cert, intermediate, root][..]), + ] { + let mut chain_file = File::create(alg.output_directory().join(format!( + "{}.{}", + role.label(), + f.0 + )))?; + for cert in f.1 { + chain_file.write_all(cert.pem().as_bytes())?; + } + } + } + + certified_keys.insert((role, alg.inner), CertifiedKey { cert, key_pair }); + } + } + + Ok(()) +} + +fn crl_for_serial(serial_number: SerialNumber) -> CertificateRevocationListParams { + let now = OffsetDateTime::now_utc(); + CertificateRevocationListParams { + this_update: now, + next_update: now + Duration::from_secs(60 * 60 * 24 * 5), + crl_number: SerialNumber::from(1234), + issuing_distribution_point: None, + revoked_certs: vec![RevokedCertParams { + serial_number, + revocation_time: now, + reason_code: Some(RevocationReason::KeyCompromise), + invalidity_date: None, + }], + key_identifier_method: KeyIdMethod::Sha256, + } +} + +// Note: these are ordered such that the data dependencies for issuance are satisfied. +const ROLES: [Role; 4] = [ + Role::TrustAnchor, + Role::Intermediate, + Role::EndEntity, + Role::Client, +]; + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +enum Role { + Client, + EndEntity, + Intermediate, + TrustAnchor, +} + +impl Role { + fn params(&self, alg: &'static SigAlgContext) -> CertificateParams { + let mut params = CertificateParams::default(); + params.distinguished_name = self.common_name(alg); + params.use_authority_key_identifier_extension = true; + let serial = SERIAL_NUMBER.fetch_add(1, Ordering::SeqCst); + params.serial_number = Some(SerialNumber::from_slice(&serial.to_be_bytes()[..])); + + match self { + Self::TrustAnchor | Self::Intermediate => { + params.is_ca = IsCa::Ca(BasicConstraints::Unconstrained); + params.key_usages = ISSUER_KEY_USAGES.to_vec(); + params.extended_key_usages = ISSUER_EXTENDED_KEY_USAGES.to_vec(); + } + Self::EndEntity | Self::Client => { + params.is_ca = IsCa::NoCa; + params.key_usages = EE_KEY_USAGES.to_vec(); + params.subject_alt_names = vec![ + SanType::DnsName(Ia5String::try_from("localhost".to_string()).unwrap()), + SanType::IpAddress(IpAddr::from_str("127.0.0.1").unwrap()), + SanType::IpAddress(IpAddr::from_str("::1").unwrap()), + ]; + } + } + + // Client certificates additionally get the client auth EKU. + if *self == Self::Client { + params.extended_key_usages = vec![ExtendedKeyUsagePurpose::ClientAuth]; + } + + params + } + + fn common_name(&self, alg: &'static SigAlgContext) -> DistinguishedName { + let mut distinguished_name = DistinguishedName::new(); + distinguished_name.push( + DnType::CommonName, + match self { + Self::Client => "Affinidi Secure Messaging Client".to_owned(), + Self::EndEntity => "localhost".to_owned(), + Self::Intermediate => { + format!("Affinidi {} level 2 intermediate", alg.issuer_cn) + } + Self::TrustAnchor => format!("Affinidi {} CA", alg.issuer_cn), + }, + ); + distinguished_name + } + + fn key_file_path(&self, alg: &'static SigAlgContext) -> PathBuf { + alg.output_directory().join(format!("{}.key", self.label())) + } + + fn cert_pem_file_path(&self, alg: &'static SigAlgContext) -> PathBuf { + alg.output_directory() + .join(format!("{}.cert", self.label())) + } + + fn cert_der_file_path(&self, alg: &'static SigAlgContext) -> PathBuf { + alg.output_directory().join(format!("{}.der", self.label())) + } + + fn label(&self) -> &'static str { + match self { + Self::Client => "client", + Self::EndEntity => "end", + Self::Intermediate => "inter", + Self::TrustAnchor => "ca", + } + } +} + +// Note: for convenience we use the RSA sigalg digest algorithm to inform the RSA modulus +// size, mapping SHA256 to RSA 2048, SHA384 to RSA 3072, and SHA512 to RSA 4096. +static SIG_ALGS: &[SigAlgContext] = &[SigAlgContext { + inner: &PKCS_ED25519, + issuer_cn: "EdDSA", +}]; + +struct SigAlgContext { + pub(crate) inner: &'static SignatureAlgorithm, + pub(crate) issuer_cn: &'static str, +} + +impl SigAlgContext { + fn output_directory(&self) -> PathBuf { + let output_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("conf/keys"); + //.join(self.issuer_cn.to_lowercase().replace(' ', "-")); + fs::create_dir_all(&output_dir).unwrap(); + output_dir + } + + fn key_pair(&self) -> KeyPair { + if *self.inner == PKCS_RSA_SHA256 { + KeyPair::generate_rsa_for(&PKCS_RSA_SHA256, RsaKeySize::_2048) + } else if *self.inner == PKCS_RSA_SHA384 { + KeyPair::generate_rsa_for(&PKCS_RSA_SHA384, RsaKeySize::_3072) + } else if *self.inner == PKCS_RSA_SHA512 { + KeyPair::generate_rsa_for(&PKCS_RSA_SHA512, RsaKeySize::_4096) + } else { + KeyPair::generate_for(self.inner) + } + .unwrap() + } +} + +const ISSUER_KEY_USAGES: &[KeyUsagePurpose; 7] = &[ + KeyUsagePurpose::CrlSign, + KeyUsagePurpose::KeyCertSign, + KeyUsagePurpose::DigitalSignature, + KeyUsagePurpose::ContentCommitment, + KeyUsagePurpose::KeyEncipherment, + KeyUsagePurpose::DataEncipherment, + KeyUsagePurpose::KeyAgreement, +]; + +const ISSUER_EXTENDED_KEY_USAGES: &[ExtendedKeyUsagePurpose; 2] = &[ + ExtendedKeyUsagePurpose::ServerAuth, + ExtendedKeyUsagePurpose::ClientAuth, +]; + +const EE_KEY_USAGES: &[KeyUsagePurpose; 2] = &[ + KeyUsagePurpose::DigitalSignature, + KeyUsagePurpose::ContentCommitment, +]; + +static SERIAL_NUMBER: AtomicU64 = AtomicU64::new(1); diff --git a/affinidi-messaging-mediator/examples/generate_secrets.rs b/affinidi-messaging-mediator/examples/generate_secrets.rs new file mode 100644 index 0000000..ed25ca0 --- /dev/null +++ b/affinidi-messaging-mediator/examples/generate_secrets.rs @@ -0,0 +1,145 @@ +use std::fs::File; +use std::io::Write; + +use did_peer::{ + DIDPeer, DIDPeerCreateKeys, DIDPeerKeys, DIDPeerService, PeerServiceEndPoint, + PeerServiceEndPointLong, +}; +use serde_json::json; +use ssi::{ + dids::DIDKey, + jwk::{Params, JWK}, +}; + +struct LocalDidPeerKeys { + v_d: Option, + v_x: Option, + e_d: Option, + e_x: Option, + e_y: Option, +} + +#[tokio::main] +async fn main() -> std::io::Result<()> { + // Generate keys for encryption and verification + let v_ed25519_key = JWK::generate_ed25519().unwrap(); + + let e_secp256k1_key = JWK::generate_secp256k1(); + + let mut local_did_peer_keys = LocalDidPeerKeys { + v_d: None, + v_x: None, + e_d: None, + e_x: None, + e_y: None, + }; + + // Print the private keys in case you want to save them for later + println!("Private keys:"); + if let Params::OKP(map) = v_ed25519_key.clone().params { + println!( + "V: private-key (d): {} {}", + map.curve, + String::from(map.private_key.clone().unwrap()) + ); + + println!( + "V: public-key (x): {} {}", + map.curve, + String::from(map.public_key.clone()) + ); + + local_did_peer_keys.v_d = Some(String::from(map.private_key.clone().unwrap())); + local_did_peer_keys.v_x = Some(String::from(map.public_key.clone())); + } + println!(); + + if let Params::EC(map) = e_secp256k1_key.clone().params { + println!( + "E: private-key: {} {}", + map.curve.clone().unwrap(), + String::from(map.ecc_private_key.clone().unwrap()) + ); + println!( + "E: public-key (x): {} {}", + map.curve.clone().unwrap(), + String::from(map.x_coordinate.clone().unwrap()) + ); + println!( + "E: public-key (y): {} {}", + map.curve.clone().unwrap(), + String::from(map.y_coordinate.clone().unwrap()) + ); + + local_did_peer_keys.e_d = Some(String::from(map.ecc_private_key.clone().unwrap())); + local_did_peer_keys.e_x = Some(String::from(map.x_coordinate.clone().unwrap())); + local_did_peer_keys.e_y = Some(String::from(map.y_coordinate.clone().unwrap())); + } + println!(); + + // Create the did:key DID's for each key above + let v_did_key = DIDKey::generate(&v_ed25519_key).unwrap(); + let e_did_key = DIDKey::generate(&e_secp256k1_key).unwrap(); + + // Put these keys in order and specify the type of each key (we strip the did:key: from the front) + let keys = vec![ + DIDPeerCreateKeys { + purpose: DIDPeerKeys::Verification, + type_: None, + public_key_multibase: Some(v_did_key[8..].to_string()), + }, + DIDPeerCreateKeys { + purpose: DIDPeerKeys::Encryption, + type_: None, + public_key_multibase: Some(e_did_key[8..].to_string()), + }, + ]; + + // Create a service definition + let services = vec![DIDPeerService { + id: None, + _type: "dm".into(), + service_end_point: PeerServiceEndPoint::Long(PeerServiceEndPointLong { + uri: "https://localhost:7037/".into(), + accept: vec!["didcomm/v2".into()], + routing_keys: vec![], + }), + }]; + + // Create the did:peer DID + let (did_peer, _) = + DIDPeer::create_peer_did(&keys, Some(&services)).expect("Failed to create did:peer"); + + println!("{}", did_peer); + + let secrets_json = json!([ + { + "id": format!("{}#key-1", did_peer), + "type": "JsonWebKey2020", + "privateKeyJwk": { + "crv": "Ed25519", + "d": local_did_peer_keys.v_d, + "kty": "OKP", + "x": local_did_peer_keys.v_x + } + }, + { + "id": format!("{}#key-2", did_peer), + "type": "JsonWebKey2020", + "privateKeyJwk": { + "crv": "secp256k1", + "d": local_did_peer_keys.e_d, + "kty": "EC", + "x": local_did_peer_keys.e_x, + "y": local_did_peer_keys.e_y, + } + } + ]); + + let json_string = serde_json::to_string_pretty(&secrets_json)?; + + let mut file = File::create("./conf/secrets.json-generated")?; + file.write_all(json_string.as_bytes())?; + + Ok(()) +} diff --git a/affinidi-messaging-mediator/examples/send_message.rs b/affinidi-messaging-mediator/examples/send_message.rs new file mode 100644 index 0000000..8dd6034 --- /dev/null +++ b/affinidi-messaging-mediator/examples/send_message.rs @@ -0,0 +1,250 @@ +use affinidi_did_resolver_cache_sdk::DIDCacheClient; +use affinidi_messaging_didcomm::{ + secrets::{Secret, SecretMaterial, SecretType}, + Message, PackEncryptedOptions, +}; +use affinidi_messaging_mediator::{ + common::errors::SuccessResponse, + handlers::authenticate::{AuthenticationChallenge, AuthorizationResponse}, + resolvers::affinidi_secrets::AffinidiSecrets, +}; +use reqwest::{Certificate, Client}; +use serde_json::json; +use std::{ + fs, + io::{self, Read}, + time::SystemTime, +}; +use uuid::Uuid; + +static MY_DID: &str = "did:peer:2.Vz6MkgWJfVmPELozq6aCycK3CpxHN8Upphn3WSuQkWY6iqsjF.EzQ3shfb7vwQaTJqFkt8nRfo7Nu98tmeYpdDfWgrqQitDaqXRz"; +static MEDIATOR_DID: &str = "did:peer:2.Vz6MkiXGPX2fvUinqRETvsbS2PDjwSksnoU9X94eFwUjRbbZJ.EzQ3shXbp9EFX7JzH2rPVfEfAEAYA4ifv4qY5sLcRgZxLHY42W.SeyJ0IjoiRElEQ29tbU1lc3NhZ2luZyIsInMiOnsidXJpIjoiaHR0cHM6Ly8xMjcuMC4wLjE6NzAzNyIsImEiOlsiZGlkY29tbS92MiJdLCJyIjpbXX19"; + +#[tokio::main] +async fn main() -> std::io::Result<()> { + let did_resolver = DIDCacheClient::new( + affinidi_did_resolver_cache_sdk::config::ClientConfigBuilder::default().build(), + ) + .await + .unwrap(); + + let v1_secret = Secret { + id: [MY_DID.to_string(), "#key-1".to_string()].concat(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!({ + "crv": "Ed25519", + "d": "LLWCf83n8VsUYq31zlZRe0NNMCcn1N4Dh85dGpIqSFw", + "kty": "OKP", + "x": "Hn8T4ZjjT0oJ6rjhqox8AykwC3GDFsJF6KkaYZExwQo" + }), + }, + }; + + let e1_secret = Secret { + id: [MY_DID.to_string(), "#key-2".to_string()].concat(), + type_: SecretType::JsonWebKey2020, + secret_material: SecretMaterial::JWK { + private_key_jwk: json!({ + "crv": "secp256k1", + "d": "oi-dXG4EqfNODFPjv2vkieoLdbQZH9k6dwPDV8HDoms", + "kty": "EC", + "x": "DhfaXbhwo0KkOiyA5V1K1RZx6Ikr86h_lX5GOwxjmjE", + "y": "PpYqybOwMsm64vftt-7gBCQPIUbglMmyy_6rloSSAPk" + }), + }, + }; + // Load Secret's + let secrets_resolver = AffinidiSecrets::new(vec![v1_secret, e1_secret]); + + // Set a process wide default crypto provider. + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + let client = init_client()?; + + // Build the ping message + let msg = create_ping(MEDIATOR_DID, true); + + println!("Ping message is\n{:#?}\n", msg); + + let (msg, metadata) = msg + .pack_encrypted( + MEDIATOR_DID, + Some(MY_DID), + Some(MY_DID), + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + .expect("Unable pack_encrypted"); + + println!("Encryption metadata is\n{:?}\n", metadata); + + // --- Sending message by Alice --- + println!("Alice is sending message \n{}\n", msg); + + // Authenticate + // Step 1. Get the challenge + println!("Authenticating (step 1/2) :: Get challenge..."); + let res = client + .post("https://localhost:7037/atm/v1/authenticate/challenge") + .header("Content-Type", "application/json") + .body(format!("{{\"did\": \"{}\"}}", MY_DID).to_string()) + .send() + .await + .map_err(|e| error(format!("Could not get: {:?}", e)))?; + let body = res.text().await.unwrap(); + let body = serde_json::from_str::>(&body) + .ok() + .unwrap(); + + println!("Authenticating (step 2/2) :: Create challenge response..."); + let auth_response = create_auth_challenge_response(MEDIATOR_DID, body.data.as_ref().unwrap()); + println!("Auth response = {:#?}", auth_response); + let (auth_msg, _) = auth_response + .pack_encrypted( + MEDIATOR_DID, + Some(MY_DID), + Some(MY_DID), + &did_resolver, + &secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + .expect("Unable pack_encrypted"); + + // Step 2. Send the challenge response + let res = client + .post("https://localhost:7037/atm/v1/authenticate") + .header("Content-Type", "application/json") + .body(auth_msg) + .send() + .await + .map_err(|e| error(format!("Could not get: {:?}", e)))?; + println!("Status:\n{}", res.status()); + let body = res.text().await.unwrap(); + println!("Body:\n{}", body); + let tokens = serde_json::from_str::>(&body) + .ok() + .unwrap() + .data + .unwrap(); + + // Send the Ping message + let res = client + .post("https://localhost:7037/atm/v1/inbound") + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", tokens.access_token)) + .body(msg) + .send() + .await + .map_err(|e| error(format!("Could not get: {:?}", e)))?; + println!("Status:\n{}", res.status()); + //println!("Headers:\n{:#?}", res.headers()); + + let body = res.text().await.unwrap(); + println!("Body:\n{}", body); + + println!(); + + /* + let mut did_method_resolver = DIDMethods::default(); + did_method_resolver.insert(Box::new(DIDPeer)); + + let a = Message::unpack_string( + &msg, + &mut did_resolver, + &did_method_resolver, + &secrets_resolver, + &UnpackOptions::default(), + ) + .await; + + println!("Unpacked message is\n{:#?}\n", a.unwrap().0);*/ + + Ok(()) +} + +/// Creates an Affinidi Trusted Messaging Authentication Challenge Response Message +/// # Arguments +/// * `to_did` - The DID of the recipient +/// * `challenge` - The challenge that was sent +/// # Returns +/// A DIDComm message to be sent +/// +/// Notes: +/// - This message will expire after 5 minutes +fn create_auth_challenge_response(to_did: &str, body: &AuthenticationChallenge) -> Message { + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + Message::build( + Uuid::new_v4().into(), + "https://affinidi.com/atm/1.0/authenticate".to_owned(), + json!(body), + ) + .to(to_did.to_owned()) + .from(MY_DID.to_owned()) + .created_time(now) + .expires_time(now + 60) + .finalize() +} + +/// Creates a DIDComm trust ping message +/// # Arguments +/// * `to_did` - The DID of the recipient +/// * `response` - Whether a response is requested +/// # Returns +/// A DIDComm message to be sent +/// +/// Notes: +/// - This message will expire after 5 minutes +fn create_ping(to_did: &str, response: bool) -> Message { + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + Message::build( + Uuid::new_v4().into(), + "https://didcomm.org/trust-ping/2.0/ping".to_owned(), + json!(format!("response_requested: {}", response)), + ) + .to(to_did.to_owned()) + .from(MY_DID.to_owned()) + .created_time(now) + .expires_time(now + 300) + .finalize() +} + +fn init_client() -> Result { + let certs = load_certs("conf/keys/client.chain")?; + + let mut client = reqwest::ClientBuilder::new() + .use_rustls_tls() + .https_only(true) + .user_agent("Affinidi Trusted Messaging"); + + for cert in certs { + client = client.add_root_certificate(cert); + } + + let client = client.build().unwrap(); + + // Build the hyper client from the HTTPS connector + Ok(client) +} + +fn load_certs(path: &str) -> io::Result> { + let mut f = + fs::File::open(path).map_err(|e| error(format!("failed to open {}: {}", path, e)))?; + let mut buf = Vec::new(); + f.read_to_end(&mut buf)?; + + reqwest::Certificate::from_pem_bundle(&buf) + .map_err(|e| error(format!("failed to read {}: {}", path, e))) +} + +fn error(err: String) -> io::Error { + io::Error::new(io::ErrorKind::Other, err) +} diff --git a/affinidi-messaging-mediator/src/common/config.rs b/affinidi-messaging-mediator/src/common/config.rs new file mode 100644 index 0000000..1a749fc --- /dev/null +++ b/affinidi-messaging-mediator/src/common/config.rs @@ -0,0 +1,641 @@ +use super::errors::MediatorError; +use crate::resolvers::affinidi_secrets::AffinidiSecrets; +use affinidi_did_resolver_cache_sdk::config::{ClientConfig, ClientConfigBuilder}; +use async_convert::{async_trait, TryFrom}; +use aws_config::{self, BehaviorVersion, Region, SdkConfig}; +use aws_sdk_secretsmanager; +use aws_sdk_ssm::types::ParameterType; +use base64::prelude::*; +use http::HeaderValue; +use jsonwebtoken::{DecodingKey, EncodingKey}; +use regex::{Captures, Regex}; +use ring::signature::{Ed25519KeyPair, KeyPair}; +use serde::{Deserialize, Serialize}; +use tower_http::cors::{Any, CorsLayer}; + +use std::{ + env, fmt, + fs::{self, File}, + io::{self, BufRead}, + path::Path, +}; +use tracing::{event, info, Level}; +use tracing_subscriber::filter::LevelFilter; + +#[derive(Debug, Serialize, Deserialize)] +pub struct ServerConfig { + pub http_size_limit: String, + pub ws_size_limit: String, +} + +/// Database Struct contains database and storage of messages related configuration details +#[derive(Debug, Serialize, Deserialize)] +pub struct DatabaseConfig { + pub database_url: String, + pub lua_scripts: String, + pub database_pool_size: String, + pub database_timeout: String, + pub max_message_size: String, + pub max_queued_messages: String, + pub message_expiry_minutes: String, +} + +/// SecurityConfig Struct contains security related configuration details +#[derive(Debug, Serialize, Deserialize)] +pub struct SecurityConfig { + pub use_ssl: String, + pub ssl_certificate_file: String, + pub ssl_key_file: String, + pub jwt_authorization_secret: String, + pub cors_allow_origin: Option, +} + +/// StreamingConfig Struct contains live streaming related configuration details +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct StreamingConfig { + pub enabled: String, + pub uuid: String, +} + +/// DIDResolverConfig Struct contains live streaming related configuration details +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct DIDResolverConfig { + pub address: Option, + pub cache_capacity: String, + pub cache_ttl: String, + pub network_timeout: String, + pub network_limit: String, +} + +/// OtherConfig Struct contains other configuration options +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct OtherConfig { + pub to_recipients_limit: String, + pub crypto_operations_per_message_limit: String, + pub to_keys_per_recipient_limit: String, +} + +impl DIDResolverConfig { + pub fn convert(&self) -> ClientConfig { + let mut config = ClientConfigBuilder::default() + .with_cache_capacity(self.cache_capacity.parse().unwrap_or(1000)) + .with_cache_ttl(self.cache_ttl.parse().unwrap_or(300)) + .with_network_timeout(self.network_timeout.parse().unwrap_or(5)) + .with_network_cache_limit_count(self.network_limit.parse().unwrap_or(100)); + + if let Some(address) = &self.address { + config = config.with_network_mode(address); + } + + config.build() + } +} + +/// ConfigRaw Struct is used to deserialize the configuration file +/// We then convert this to the Config Struct +#[derive(Debug, Serialize, Deserialize)] +pub struct ConfigRaw { + pub log_level: String, + pub listen_address: String, + pub mediator_did: String, + pub mediator_secrets: String, + pub server: ServerConfig, + pub database: DatabaseConfig, + pub security: SecurityConfig, + pub streaming: StreamingConfig, + pub did_resolver: DIDResolverConfig, + pub other: OtherConfig, +} + +#[derive(Clone)] +pub struct Config { + pub log_level: LevelFilter, + pub listen_address: String, + pub mediator_did: String, + pub mediator_secrets: AffinidiSecrets, + pub database_url: String, + pub lua_scripts: String, + pub database_pool_size: usize, + pub database_timeout: u32, + pub http_size_limit: u32, + pub ws_size_limit: u32, + pub max_message_size: u32, + pub max_queued_messages: u32, + pub message_expiry_minutes: u32, + pub use_ssl: bool, + pub ssl_certificate_file: String, + pub ssl_key_file: String, + pub jwt_encoding_key: Option, + pub jwt_decoding_key: Option, + pub streaming_enabled: bool, + pub streaming_uuid: String, + pub did_resolver_config: ClientConfig, + pub to_recipients_limit: usize, + pub cors_allow_origin: CorsLayer, + pub crypto_operations_per_message_limit: usize, + pub to_keys_per_recipient_limit: usize, +} + +impl fmt::Debug for Config { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Config") + .field("log_level", &self.log_level) + .field("listen_address", &self.listen_address) + .field("mediator_did", &self.mediator_did) + .field("mediator_did_doc", &"Hidden") + .field( + "mediator_secrets", + &format!("({}) secrets loaded", self.mediator_secrets.len()), + ) + .field("use_ssl", &self.use_ssl) + .field("cors_allow_origin", &self.cors_allow_origin) + .field("database_url", &self.database_url) + .field("database_pool_size", &self.database_pool_size) + .field("database_timeout", &self.database_timeout) + .field("max_message_size", &self.max_message_size) + .field("max_queued_messages", &self.max_queued_messages) + .field("message_expiry_minutes", &self.message_expiry_minutes) + .field("ssl_certificate_file", &self.ssl_certificate_file) + .field("ssl_key_file", &self.ssl_key_file) + .field("jwt_encoding_key?", &self.jwt_encoding_key.is_some()) + .field("jwt_decoding_key?", &self.jwt_decoding_key.is_some()) + .field("streaming_enabled?", &self.streaming_enabled) + .field("streaming_uuid", &self.streaming_uuid) + .field("DID Resolver config", &self.did_resolver_config) + .field("to_recipients_limit", &self.to_recipients_limit) + .field("http_size_limit", &self.http_size_limit) + .field("ws_size_limit", &self.ws_size_limit) + .field( + "crypto_operations_per_message_limit", + &self.crypto_operations_per_message_limit, + ) + .field("to_keys_per_recipient_limit", &self.to_keys_per_recipient_limit) + .finish() + } +} + +impl Default for Config { + fn default() -> Self { + let did_resolver_config = ClientConfigBuilder::default() + .with_cache_capacity(1000) + .with_cache_ttl(300) + .with_network_timeout(5) + .with_network_cache_limit_count(100) + .build(); + + Config { + log_level: LevelFilter::INFO, + listen_address: "".into(), + mediator_did: "".into(), + mediator_secrets: AffinidiSecrets::new(vec![]), + database_url: "redis://127.0.0.1/".into(), + lua_scripts: "".into(), + database_pool_size: 10, + database_timeout: 2, + max_message_size: 1048576, + max_queued_messages: 100, + message_expiry_minutes: 10080, + use_ssl: true, + ssl_certificate_file: "".into(), + ssl_key_file: "".into(), + jwt_encoding_key: None, + jwt_decoding_key: None, + streaming_enabled: true, + streaming_uuid: "".into(), + did_resolver_config, + to_recipients_limit: 100, + cors_allow_origin: CorsLayer::new().allow_origin(Any), + ws_size_limit: 10485760, + http_size_limit: 10485760, + crypto_operations_per_message_limit: 1_000, + to_keys_per_recipient_limit: 100, + } + } +} + +#[async_trait] +impl TryFrom for Config { + type Error = MediatorError; + + async fn try_from(raw: ConfigRaw) -> Result { + // Set up AWS Configuration + let region = match env::var("AWS_REGION") { + Ok(region) => Region::new(region), + Err(_) => Region::new("ap-southeast-1"), + }; + let aws_config = aws_config::defaults(BehaviorVersion::v2024_03_28()) + .region(region) + .load() + .await; + + let mut config = Config { + log_level: match raw.log_level.as_str() { + "trace" => LevelFilter::TRACE, + "debug" => LevelFilter::DEBUG, + "info" => LevelFilter::INFO, + "warn" => LevelFilter::WARN, + "error" => LevelFilter::ERROR, + _ => LevelFilter::INFO, + }, + listen_address: raw.listen_address, + mediator_did: read_did_config(&raw.mediator_did, &aws_config).await?, + database_url: raw.database.database_url, + lua_scripts: read_lua_scripts(&raw.database.lua_scripts).await?, + database_pool_size: raw.database.database_pool_size.parse().unwrap_or(10), + database_timeout: raw.database.database_timeout.parse().unwrap_or(2), + max_message_size: raw.database.max_message_size.parse().unwrap_or(1048576), + max_queued_messages: raw.database.max_queued_messages.parse().unwrap_or(100), + message_expiry_minutes: raw.database.message_expiry_minutes.parse().unwrap_or(10080), + use_ssl: raw.security.use_ssl.parse().unwrap_or(true), + ssl_certificate_file: raw.security.ssl_certificate_file, + ssl_key_file: raw.security.ssl_key_file, + streaming_enabled: raw.streaming.enabled.parse().unwrap_or(true), + did_resolver_config: raw.did_resolver.convert(), + to_recipients_limit: raw.other.to_recipients_limit.parse().unwrap_or(100), + http_size_limit: raw.server.http_size_limit.parse().unwrap_or(10485760), + ws_size_limit: raw.server.ws_size_limit.parse().unwrap_or(10485760), + crypto_operations_per_message_limit: raw + .other + .crypto_operations_per_message_limit + .parse() + .unwrap_or(1_000), + to_keys_per_recipient_limit: raw.other.to_keys_per_recipient_limit.parse().unwrap_or(100), + ..Default::default() + }; + + if let Some(cors_allow_origin) = &raw.security.cors_allow_origin { + config.cors_allow_origin = + CorsLayer::new().allow_origin(parse_cors_allow_origin(cors_allow_origin)?); + } + + // Load mediator secrets + config.mediator_secrets = load_secrets(&raw.mediator_secrets, &aws_config).await?; + + // Create the JWT encoding and decoding keys + let jwt_secret = + config_jwt_secret(&raw.security.jwt_authorization_secret, &aws_config).await?; + + config.jwt_encoding_key = Some(EncodingKey::from_ed_der(&jwt_secret)); + + let pair = Ed25519KeyPair::from_pkcs8(&jwt_secret).map_err(|err| { + event!(Level::ERROR, "Could not create JWT key pair. {}", err); + MediatorError::ConfigError( + "NA".into(), + format!("Could not create JWT key pair. {}", err), + ) + })?; + config.jwt_decoding_key = Some(DecodingKey::from_ed_der(pair.public_key().as_ref())); + + // Get Subscriber unique hostname + if config.streaming_enabled { + config.streaming_uuid = get_hostname(&raw.streaming.uuid)?; + } + + Ok(config) + } +} + +fn parse_cors_allow_origin(cors_allow_origin: &str) -> Result, MediatorError> { + let origins: Vec = cors_allow_origin + .split(',') + .map(|o| o.parse::().unwrap()) + .collect(); + + Ok(origins) +} + +/// Loads the secret data into the Config file. +async fn load_secrets( + secrets: &str, + aws_config: &SdkConfig, +) -> Result { + let parts: Vec<&str> = secrets.split("://").collect(); + if parts.len() != 2 { + return Err(MediatorError::ConfigError( + "NA".into(), + "Invalid `mediator_secrets` format".into(), + )); + } + info!("Loading secrets method({}) path({})", parts[0], parts[1]); + let content: String = match parts[0] { + "file" => read_file_lines(parts[1])?.concat(), + "aws_secrets" => { + let asm = aws_sdk_secretsmanager::Client::new(aws_config); + + let response = asm + .get_secret_value() + .secret_id(parts[1]) + .send() + .await + .map_err(|e| { + event!(Level::ERROR, "Could not get secret value. {}", e); + MediatorError::ConfigError( + "NA".into(), + format!("Could not get secret value. {}", e), + ) + })?; + response.secret_string.ok_or_else(|| { + event!(Level::ERROR, "No secret string found in response"); + MediatorError::ConfigError("NA".into(), "No secret string found in response".into()) + })? + } + _ => { + return Err(MediatorError::ConfigError( + "NA".into(), + "Invalid `mediator_secrets` format! Expecting file:// or aws_secrets:// ...".into(), + )) + } + }; + + Ok(AffinidiSecrets::new( + serde_json::from_str(&content).map_err(|err| { + event!( + Level::ERROR, + "Could not parse `mediator_secrets` JSON content. {}", + err + ); + MediatorError::ConfigError( + "NA".into(), + format!("Could not parse `mediator_secrets` JSON content. {}", err), + ) + })?, + )) +} + +async fn read_lua_scripts(lua_scripts: &str) -> Result { + let parts: Vec<&str> = lua_scripts.split("://").collect(); + if parts.len() != 2 { + return Err(MediatorError::ConfigError( + "NA".into(), + "Invalid `lua_scripts` format".into(), + )); + } + + info!( + "Loading lua_scripts method({}) path({})", + parts[0], parts[1] + ); + let content: String = match parts[0] { + // "file" => read_file_lines(parts[1], false)?.concat(), + "file" => fs::read_to_string(parts[1]).map_err(|err| { + event!(Level::ERROR, "Could not open file({}). {}", parts[1], err); + MediatorError::ConfigError( + "NA".into(), + format!("Could not open file({}). {}", parts[1], err), + ) + })?, + "inline" => parts[1].to_string(), + _ => { + return Err(MediatorError::ConfigError( + "NA".into(), + "Invalid `lua_scripts` format! Expecting file:// or inline:// ...".into(), + )) + } + }; + Ok(content) +} + +/// Read the primary configuration file for the mediator +/// Returns a ConfigRaw struct, that still needs to be processed for additional information +/// and conversion to Config struct +pub fn read_config_file(file_name: &str) -> Result { + // Read configuration file parameters + event!(Level::INFO, "Config file({})", file_name); + let raw_config = read_file_lines(file_name)?; + + event!(Level::DEBUG, "raw_config = {:?}", raw_config); + let config_with_vars = expand_env_vars(&raw_config)?; + match toml::from_str(&config_with_vars.join("\n")) { + Ok(config) => Ok(config), + Err(err) => { + event!( + Level::ERROR, + "Could not parse configuration settings. {:?}", + err + ); + Err(MediatorError::ConfigError( + "NA".into(), + format!("Could not parse configuration settings. Reason: {:?}", err), + )) + } + } +} + +/// Reads a file and returns a vector of strings, one for each line in the file. +/// It also strips any lines starting with a # (comments) +/// You can join the Vec back into a single string with `.join("\n")` +/// ```ignore +/// let lines = read_file_lines("file.txt")?; +/// let file_contents = lines.join("\n"); +/// ``` +fn read_file_lines

    (file_name: P) -> Result, MediatorError> +where + P: AsRef, +{ + let file = File::open(file_name.as_ref()).map_err(|err| { + event!( + Level::ERROR, + "Could not open file({}). {}", + file_name.as_ref().display(), + err + ); + MediatorError::ConfigError( + "NA".into(), + format!( + "Could not open file({}). {}", + file_name.as_ref().display(), + err + ), + ) + })?; + + let mut lines = Vec::new(); + for line in io::BufReader::new(file).lines().map_while(Result::ok) { + // Strip comments out + if !line.starts_with('#') { + lines.push(line); + } + } + + Ok(lines) +} + +/// Replaces all strings ${VAR_NAME:default_value} +/// with the corresponding environment variables (e.g. value of ${VAR_NAME}) +/// or with `default_value` if the variable is not defined. +fn expand_env_vars(raw_config: &Vec) -> Result, MediatorError> { + let re = Regex::new(r"\$\{(?P[A-Z_]{1,}[0-9A-Z_]*):(?P.*)\}").map_err( + |e| { + MediatorError::ConfigError( + "NA".into(), + format!("Couldn't create ENV Regex. Reason: {}", e), + ) + }, + )?; + let mut result: Vec = Vec::new(); + for line in raw_config { + result.push( + re.replace_all(line, |caps: &Captures| match env::var(&caps["env_var"]) { + Ok(val) => val, + Err(_) => (caps["default_value"]).into(), + }) + .into_owned(), + ); + } + Ok(result) +} + +/// Converts the mediator_did config to a valid DID depending on source +async fn read_did_config( + did_config: &str, + aws_config: &SdkConfig, +) -> Result { + let parts: Vec<&str> = did_config.split("://").collect(); + if parts.len() != 2 { + return Err(MediatorError::ConfigError( + "NA".into(), + "Invalid `mediator_did` format".into(), + )); + } + let content: String = match parts[0] { + "did" => parts[1].to_string(), + "aws_parameter_store" => { + let ssm = aws_sdk_ssm::Client::new(aws_config); + + let response = ssm + .get_parameter() + .set_name(Some(parts[1].to_string())) + .send() + .await + .map_err(|e| { + event!(Level::ERROR, "Could not get mediator_did parameter. {}", e); + MediatorError::ConfigError( + "NA".into(), + format!("Could not get mediator_did parameter. {}", e), + ) + })?; + let parameter = response.parameter.ok_or_else(|| { + event!(Level::ERROR, "No parameter string found in response"); + MediatorError::ConfigError( + "NA".into(), + "No parameter string found in response".into(), + ) + })?; + + if let Some(_type) = parameter.r#type { + if _type != ParameterType::String { + return Err(MediatorError::ConfigError( + "NA".into(), + "Expected String parameter type".into(), + )); + } + } else { + return Err(MediatorError::ConfigError( + "NA".into(), + "Unknown parameter type".into(), + )); + } + + parameter.value.ok_or_else(|| { + event!( + Level::ERROR, + "Parameter ({:?}) found, but no parameter value found in response", + parameter.name + ); + MediatorError::ConfigError( + "NA".into(), + format!( + "Parameter ({:?}) found, but no parameter value found in response", + parameter.name + ), + ) + })? + } + _ => { + return Err(MediatorError::ConfigError( + "NA".into(), + "Invalid MEDIATOR_SECRETS format! Expecting file:// or aws_secrets:// ...".into(), + )) + } + }; + + Ok(content) +} + +/// Converts the jwt_authorization_secret config to a valid JWT secret +/// Can take a basic string, or fetch from AWS Secrets Manager +async fn config_jwt_secret( + jwt_secret: &str, + aws_config: &SdkConfig, +) -> Result, MediatorError> { + let parts: Vec<&str> = jwt_secret.split("://").collect(); + if parts.len() != 2 { + return Err(MediatorError::ConfigError( + "NA".into(), + "Invalid `jwt_authorization_secret` format".into(), + )); + } + let content: String = match parts[0] { + "string" => parts[1].to_string(), + "aws_secrets" => { + info!("Loading JWT secret from AWS Secrets Manager"); + let asm = aws_sdk_secretsmanager::Client::new(aws_config); + + let response = asm + .get_secret_value() + .secret_id(parts[1]) + .send() + .await + .map_err(|e| { + event!(Level::ERROR, "Could not get secret value. {}", e); + MediatorError::ConfigError( + "NA".into(), + format!("Could not get secret value. {}", e), + ) + })?; + response.secret_string.ok_or_else(|| { + event!(Level::ERROR, "No secret string found in response"); + MediatorError::ConfigError("NA".into(), "No secret string found in response".into()) + })? + } + _ => return Err(MediatorError::ConfigError( + "NA".into(), + "Invalid `jwt_authorization_secret` format! Expecting string:// or aws_secrets:// ..." + .into(), + )), + }; + + BASE64_URL_SAFE_NO_PAD.decode(content).map_err(|err| { + event!(Level::ERROR, "Could not create JWT key pair. {}", err); + MediatorError::ConfigError( + "NA".into(), + format!("Could not create JWT key pair. {}", err), + ) + }) +} + +fn get_hostname(host_name: &str) -> Result { + if host_name.starts_with("hostname://") { + Ok(hostname::get() + .map_err(|e| { + MediatorError::ConfigError( + "NA".into(), + format!("Couldn't get hostname. Reason: {}", e), + ) + })? + .into_string() + .map_err(|e| { + MediatorError::ConfigError( + "NA".into(), + format!("Couldn't get hostname. Reason: {:?}", e), + ) + })?) + } else if host_name.starts_with("string://") { + Ok(host_name.split_at(9).1.to_string()) + } else { + Err(MediatorError::ConfigError( + "NA".into(), + "Invalid hostname format!".into(), + )) + } +} diff --git a/affinidi-messaging-mediator/src/common/errors.rs b/affinidi-messaging-mediator/src/common/errors.rs new file mode 100644 index 0000000..77fb0c2 --- /dev/null +++ b/affinidi-messaging-mediator/src/common/errors.rs @@ -0,0 +1,329 @@ +use affinidi_messaging_sdk::messages::GenericDataStruct; +use axum::{ + http::StatusCode, + response::{IntoResponse, Response}, + Json, +}; +use rand::{distributions::Alphanumeric, Rng}; +use serde::{Deserialize, Serialize}; +use std::fmt; +use thiserror::Error; +use tracing::{event, Level}; + +type SessId = String; + +pub struct AppError(MediatorError); + +impl From for AppError +where + E: Into, +{ + fn from(err: E) -> Self { + Self(err.into()) + } +} + +/// MediatorError the first String is always the session_id +#[derive(Error, Debug)] +pub enum MediatorError { + #[error("Error in handling errors! {1}")] + ErrorHandlingError(SessId, String), + #[error("{1}")] + InternalError(SessId, String), + #[error("Couldn't parse ({1}). Reason: {2}")] + ParseError(SessId, String, String), + #[error("Permission Error: {1}")] + PermissionError(SessId, String), + #[error("Request is invalid: {1}")] + RequestDataError(SessId, String), + #[error("Service Limit exceeded: {1}")] + ServiceLimitError(SessId, String), + #[error("Unauthorized: {1}")] + Unauthorized(SessId, String), + #[error("DID Error: did({1}) Error: {2}")] + DIDError(SessId, String, String), + #[error("Configuration Error: {1}")] + ConfigError(SessId, String), + #[error("Database Error: {1}")] + DatabaseError(SessId, String), + #[error("Message unpack error: {1}")] + MessageUnpackError(SessId, String), + #[error("MessageExpired: expiry({1}) now({2})")] + MessageExpired(SessId, String, String), + #[error("Message pack error: {1}")] + MessagePackError(SessId, String), + #[error("Feature not implemented: {1}")] + NotImplemented(SessId, String), + #[error("Authorization Session ({0}) error: {1}")] + SessionError(SessId, String), + #[error("Anonymous message error: {1}")] + AnonymousMessageError(SessId, String), +} + +impl IntoResponse for AppError { + fn into_response(self) -> Response { + let response = match self.0 { + MediatorError::ErrorHandlingError(session_id, msg) => { + let response = ErrorResponse { + httpCode: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), + sessionId: session_id.to_string(), + errorCode: 1, + errorCodeStr: "ErrorHandlingError".to_string(), + message: msg.to_string(), + }; + event!(Level::WARN, "{}", response.to_string()); + response + } + MediatorError::InternalError(session_id, msg) => { + let response = ErrorResponse { + httpCode: StatusCode::INTERNAL_SERVER_ERROR.as_u16(), + sessionId: session_id.to_string(), + errorCode: 2, + errorCodeStr: "InternalError".to_string(), + message: msg.to_string(), + }; + event!(Level::WARN, "{}", response.to_string()); + response + } + MediatorError::ParseError(session_id, _, msg) => { + let response = ErrorResponse { + httpCode: StatusCode::BAD_REQUEST.as_u16(), + sessionId: session_id.to_string(), + errorCode: 3, + errorCodeStr: "BadRequest: ParseError".to_string(), + message: msg.to_string(), + }; + event!(Level::WARN, "{}", response.to_string()); + response + } + MediatorError::PermissionError(session_id, msg) => { + let response = ErrorResponse { + httpCode: StatusCode::FORBIDDEN.as_u16(), + sessionId: session_id.to_string(), + errorCode: 4, + errorCodeStr: "Forbidden: PermissionError".to_string(), + message: msg.to_string(), + }; + event!(Level::WARN, "{}", response.to_string()); + response + } + MediatorError::RequestDataError(session_id, msg) => { + let response = ErrorResponse { + httpCode: StatusCode::BAD_REQUEST.as_u16(), + sessionId: session_id.to_string(), + errorCode: 5, + errorCodeStr: "BadRequest: RequestDataError".to_string(), + message: format!("Bad Request: ({})", msg), + }; + event!(Level::WARN, "{}", response.to_string()); + response + } + MediatorError::ServiceLimitError(session_id, msg) => { + let response = ErrorResponse { + httpCode: StatusCode::BAD_REQUEST.as_u16(), + sessionId: session_id.to_string(), + errorCode: 6, + errorCodeStr: "BadRequest: ServiceLimitError".to_string(), + message: msg.to_string(), + }; + event!(Level::WARN, "{}", response.to_string()); + response + } + MediatorError::Unauthorized(session_id, msg) => { + let response = ErrorResponse { + httpCode: StatusCode::UNAUTHORIZED.as_u16(), + sessionId: session_id.to_string(), + errorCode: 7, + errorCodeStr: "Unauthorized".to_string(), + message: format!("Unauthorized access: {}", msg), + }; + event!(Level::WARN, "{}", response.to_string()); + response + } + MediatorError::DIDError(session_id, did, msg) => { + let response = ErrorResponse { + httpCode: StatusCode::BAD_REQUEST.as_u16(), + sessionId: session_id.to_string(), + errorCode: 8, + errorCodeStr: "DIDError".to_string(), + message: format!("did({}) Error: {}", did, msg), + }; + event!(Level::WARN, "{}", response.to_string()); + response + } + MediatorError::ConfigError(session_id, message) => { + let response = ErrorResponse { + httpCode: StatusCode::SERVICE_UNAVAILABLE.as_u16(), + sessionId: session_id.to_string(), + errorCode: 9, + errorCodeStr: "ConfigError".to_string(), + message, + }; + event!(Level::WARN, "{}", response.to_string()); + response + } + MediatorError::DatabaseError(session_id, message) => { + let response = ErrorResponse { + httpCode: StatusCode::SERVICE_UNAVAILABLE.as_u16(), + sessionId: session_id.to_string(), + errorCode: 10, + errorCodeStr: "DatabaseError".to_string(), + message, + }; + event!(Level::WARN, "{}", response.to_string()); + response + } + MediatorError::MessageUnpackError(session_id, message) => { + let response = ErrorResponse { + httpCode: StatusCode::BAD_REQUEST.as_u16(), + sessionId: session_id.to_string(), + errorCode: 11, + errorCodeStr: "MessageUnpackError".to_string(), + message, + }; + event!(Level::WARN, "{}", response.to_string()); + response + } + MediatorError::MessageExpired(session_id, expired, now) => { + let response = ErrorResponse { + httpCode: StatusCode::UNPROCESSABLE_ENTITY.as_u16(), + sessionId: session_id.to_string(), + errorCode: 12, + errorCodeStr: "MessageExpired".to_string(), + message: format!("Message expired: expiry({}) now({})", expired, now), + }; + event!(Level::WARN, "{}", response.to_string()); + response + } + MediatorError::MessagePackError(session_id, message) => { + let response = ErrorResponse { + httpCode: StatusCode::BAD_REQUEST.as_u16(), + sessionId: session_id.to_string(), + errorCode: 13, + errorCodeStr: "MessagePackError".to_string(), + message, + }; + event!(Level::WARN, "{}", response.to_string()); + response + } + MediatorError::NotImplemented(session_id, message) => { + let response = ErrorResponse { + httpCode: StatusCode::NOT_IMPLEMENTED.as_u16(), + sessionId: session_id.to_string(), + errorCode: 14, + errorCodeStr: "NotImplemented".to_string(), + message, + }; + event!(Level::WARN, "{}", response.to_string()); + response + } + MediatorError::SessionError(session_id, message) => { + let response = ErrorResponse { + httpCode: StatusCode::NOT_ACCEPTABLE.as_u16(), + sessionId: session_id.to_string(), + errorCode: 15, + errorCodeStr: "SessionError".to_string(), + message, + }; + event!(Level::WARN, "{}", response.to_string()); + response + } + MediatorError::AnonymousMessageError(session_id, message) => { + let response = ErrorResponse { + httpCode: StatusCode::NOT_ACCEPTABLE.as_u16(), + sessionId: session_id.to_string(), + errorCode: 16, + errorCodeStr: "AnonymousMessageError".to_string(), + message, + }; + event!(Level::WARN, "{}", response.to_string()); + response + } + }; + ( + StatusCode::from_u16(response.httpCode).ok().unwrap(), + Json(response), + ) + .into_response() + } +} + +#[derive(Clone, Debug, Serialize)] +pub struct Session { + pub session_id: String, // Unique session transaction ID + pub remote_addr: String, // Remote Socket address + pub authenticated: bool, // Has this session been authenticated? + pub challenge_sent: Option, // Challenge sent to the client + pub did: String, // DID of the client + pub did_hash: String, // Sha256 hash of the DID +} + +#[derive(Serialize, Debug)] +#[allow(non_snake_case)] +pub struct ErrorResponse { + pub sessionId: String, + pub httpCode: u16, + pub errorCode: u16, + pub errorCodeStr: String, + pub message: String, +} + +impl fmt::Display for ErrorResponse { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}: httpcode({}) errorCode({}), errorCodeStr({}) message({})", + self.sessionId, self.httpCode, self.errorCode, self.errorCodeStr, self.message, + ) + } +} +#[derive(Serialize, Deserialize, Debug)] +#[allow(non_snake_case)] +pub struct SuccessResponse { + pub sessionId: String, + pub httpCode: u16, + pub errorCode: i32, + pub errorCodeStr: String, + pub message: String, + #[serde(bound(deserialize = ""))] + pub data: Option, +} + +impl fmt::Display for SuccessResponse { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "{}: httpcode({}) errorCode({}), errorCodeStr({}) message({})", + self.sessionId, self.httpCode, self.errorCode, self.errorCodeStr, self.message, + ) + } +} + +impl SuccessResponse { + pub fn response( + session_id: &str, + http_code: StatusCode, + msg: &str, + data: Option, + ) -> Json> { + let response = SuccessResponse { + sessionId: session_id.to_string(), + httpCode: http_code.as_u16(), + errorCode: 0, + errorCodeStr: "Ok".to_string(), + message: msg.to_string(), + data, + }; + event!(Level::INFO, "{response}"); + Json(response) + } +} + +// Creates a random transaction identifier for each transaction +pub fn create_session_id() -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(12) + .map(char::from) + .collect() +} diff --git a/affinidi-messaging-mediator/src/common/jwt_auth.rs b/affinidi-messaging-mediator/src/common/jwt_auth.rs new file mode 100644 index 0000000..2f581fb --- /dev/null +++ b/affinidi-messaging-mediator/src/common/jwt_auth.rs @@ -0,0 +1,166 @@ +use super::errors::{ErrorResponse, Session}; +use crate::{database::session::SessionClaims, SharedData}; +use axum::{ + async_trait, + extract::{FromRef, FromRequestParts}, + response::{IntoResponse, Response}, + Json, RequestPartsExt, +}; +use axum_extra::{ + headers::{authorization::Bearer, Authorization}, + TypedHeader, +}; +use http::{request::Parts, StatusCode}; +use jsonwebtoken::{TokenData, Validation}; +use serde::{Deserialize, Serialize}; +use serde_json::json; +use sha256::digest; +use std::{ + fmt::{Debug, Display}, + net::SocketAddr, +}; +use tracing::{error, event, info, warn, Level}; + +// Payload contents of the JWT +// All times are in seconds since UNIX EPOCH +#[derive(Debug, Serialize, Deserialize)] +pub struct TokenPayload { + pub aud: Vec, // Intended Audience + pub client_id: String, + pub exp: u64, // What does this JWT Expire + pub iat: u64, // Issued at this time + pub iss: String, // Who issued this JWT? + pub jti: String, // JWT ID + pub nbf: u64, // JWT is not valid before this time + pub scp: Vec, // ??? + pub sub: String, // subject - who this JWT refers to +} + +#[derive(Debug)] +pub enum AuthError { + WrongCredentials, + MissingCredentials, + InvalidToken, + ExpiredToken, + InternalServerError(String), +} + +impl Display for AuthError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AuthError::WrongCredentials => write!(f, "Wrong credentials"), + AuthError::MissingCredentials => write!(f, "Missing credentials"), + AuthError::InvalidToken => write!(f, "Invalid token"), + AuthError::ExpiredToken => write!(f, "Expired token"), + AuthError::InternalServerError(message) => { + write!(f, "Internal Server Error: {}", message) + } + } + } +} + +impl IntoResponse for AuthError { + fn into_response(self) -> Response { + let status = match self { + AuthError::WrongCredentials => StatusCode::UNAUTHORIZED, + AuthError::MissingCredentials => StatusCode::BAD_REQUEST, + AuthError::InvalidToken => StatusCode::BAD_REQUEST, + AuthError::ExpiredToken => StatusCode::UNAUTHORIZED, + AuthError::InternalServerError(_) => StatusCode::INTERNAL_SERVER_ERROR, + }; + let body = Json(json!(ErrorResponse { + sessionId: "UNAUTHORIZED".into(), + httpCode: status.as_u16(), + errorCode: status.as_u16(), + errorCodeStr: status.to_string(), + message: self.to_string(), + })); + (status, body).into_response() + } +} + +#[async_trait] +impl FromRequestParts for Session +where + SharedData: FromRef, + S: Send + Sync + Debug, +{ + type Rejection = AuthError; + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + let state = parts + .extract_with_state::(_state) + .await + .map_err(|e| { + error!("Couldn't get SharedData state! Reason: {}", e); + AuthError::InternalServerError(format!( + "Couldn't get SharedData state! Reason: {}", + e + )) + })?; + + let remote_addr = if let Some(address) = parts + .extensions + .get::>() + .map(|ci| ci.0) + { + address.to_string() + } else { + warn!("No remote address in request!"); + return Err(AuthError::MissingCredentials); + }; + + let mut validation = Validation::new(jsonwebtoken::Algorithm::EdDSA); + validation.set_audience(&["ATM"]); + validation.set_required_spec_claims(&["exp", "sub", "aud", "session_id"]); + + let TypedHeader(Authorization(bearer)) = parts + .extract::>>() + .await + .map_err(|_| { + warn!( + "{}: No Authorization Bearer header in request!", + remote_addr + ); + AuthError::MissingCredentials + })?; + + let token_data: TokenData = if let Some(decoding_key) = + state.config.jwt_decoding_key.as_ref() + { + match jsonwebtoken::decode::(bearer.token(), decoding_key, &validation) { + Ok(token_data) => token_data, + Err(err) => { + event!( + Level::WARN, + "{}: decoding JWT failed {:?}", + remote_addr, + err + ); + return Err(AuthError::InvalidToken); + } + } + } else { + return Err(AuthError::MissingCredentials); + }; + + let session_id = token_data.claims.session_id.clone(); + let did = token_data.claims.sub.clone(); + let did_hash = digest(&did); + + info!( + "{}: Protected connection accepted from address({}) did_hash({})", + &session_id, &remote_addr, &did_hash + ); + + let session = Session { + session_id, + remote_addr, + authenticated: true, + challenge_sent: None, + did, + did_hash, + }; + + Ok(session) + } +} diff --git a/affinidi-messaging-mediator/src/common/mod.rs b/affinidi-messaging-mediator/src/common/mod.rs new file mode 100644 index 0000000..6e8e249 --- /dev/null +++ b/affinidi-messaging-mediator/src/common/mod.rs @@ -0,0 +1,3 @@ +pub mod config; +pub mod errors; +pub mod jwt_auth; diff --git a/affinidi-messaging-mediator/src/database/delete.rs b/affinidi-messaging-mediator/src/database/delete.rs new file mode 100644 index 0000000..8de14bc --- /dev/null +++ b/affinidi-messaging-mediator/src/database/delete.rs @@ -0,0 +1,59 @@ +use super::DatabaseHandler; +use crate::common::errors::MediatorError; +use tracing::{debug, event, span, Instrument, Level}; + +impl DatabaseHandler { + /// Deletes a message in the database + /// - session_id: authentication session ID + /// - did: DID of the delete requestor + /// - message_hash: sha257 hash of the message to delete + pub async fn delete_message( + &self, + session_id: &str, + did_hash: &str, + message_hash: &str, + ) -> Result<(), MediatorError> { + let _span = span!( + Level::DEBUG, + "delete_message", + message_hash = message_hash, + did_hash = did_hash + ); + async move { + let mut conn = self.get_async_connection().await?; + let response: String = deadpool_redis::redis::cmd("FCALL") + .arg("delete_message") + .arg(1) + .arg(message_hash) + .arg(did_hash) + .query_async(&mut conn) + .await + .map_err(|err| { + event!( + Level::ERROR, + "Couldn't delete message_id({}) from database for DID {}: {}", + message_hash, + did_hash, + err + ); + MediatorError::DatabaseError( + did_hash.into(), + format!( + "Couldn't delete message_id({}) from database for DID {}: {}", + message_hash, did_hash, err + ), + ) + })?; + + debug!("database response: ({})", response); + + if response != "OK" { + Err(MediatorError::DatabaseError(session_id.into(), response)) + } else { + Ok(()) + } + } + .instrument(_span) + .await + } +} diff --git a/affinidi-messaging-mediator/src/database/fetch.rs b/affinidi-messaging-mediator/src/database/fetch.rs new file mode 100644 index 0000000..ad548e2 --- /dev/null +++ b/affinidi-messaging-mediator/src/database/fetch.rs @@ -0,0 +1,100 @@ +use affinidi_messaging_sdk::messages::{ + fetch::FetchOptions, FetchDeletePolicy, GetMessagesResponse, MessageListElement, +}; +use itertools::Itertools; +use redis::{from_redis_value, Value}; +use tracing::{debug, event, span, warn, Instrument, Level}; + +use crate::common::errors::MediatorError; + +use super::DatabaseHandler; + +impl DatabaseHandler { + /// Fetch as many messages as possible from the database + /// - did_hash: DID we are checking + pub async fn fetch_messages( + &self, + session_id: &str, + did_hash: &str, + options: &FetchOptions, + ) -> Result { + let _span = span!(Level::DEBUG, "fetch_messages"); + async move { + let mut conn = self.get_async_connection().await?; + + let start_id = options.start_id.as_deref().unwrap_or("-"); + + let results: Vec = deadpool_redis::redis::cmd("FCALL") + .arg("fetch_messages") + .arg(1) + .arg(did_hash) + .arg(start_id) + .arg(options.limit) + .query_async(&mut conn) + .await + .map_err(|err| { + event!( + Level::ERROR, + "Couldn't fetch_messages() from database: {}", + err + ); + MediatorError::DatabaseError( + "NA".into(), + format!("Couldn't fetch_messages() from database: {}", err), + ) + })?; + + let mut messages = GetMessagesResponse::default(); + for item in &results { + let sub_item: Vec = match from_redis_value(item) { + Ok(v) => v, + Err(e) => { + warn!("Error parsing redis value: ({:?}). Reason: {:?}", item, e); + messages + .get_errors + .push((format!("{:?}", item), e.to_string())); + continue; + } + }; + let mut message = MessageListElement::default(); + for (k, v) in sub_item.iter().tuples() { + match k.as_str() { + "MSG_ID" => message.msg_id.clone_from(v), + "META_SEND_ID" => message.send_id = Some(v.clone()), + "META_RECEIVE_ID" => message.receive_id = Some(v.clone()), + "META_BYTES" => message.size = v.parse().unwrap_or(0), + "META_TIMESTAMP" => message.timestamp = v.parse().unwrap_or(0), + "META_TO" => message.to_address = Some(v.clone()), + "FROM_DID" => message.from_address = Some(v.clone()), + "MSG" => message.msg = Some(v.clone()), + _ => {} + } + } + debug!("Message id({}) fetched", &message.msg_id); + + // Delete message if requested + if let FetchDeletePolicy::Optimistic = options.delete_policy { + match self + .delete_message(session_id, did_hash, &message.msg_id) + .await + { + Ok(_) => { + debug!("Message deleted: ({})", message.msg_id); + } + Err(e) => { + warn!("Error deleting message: ({})", e); + messages + .delete_errors + .push((message.msg_id.clone(), e.to_string())); + } + } + } + messages.success.push(message); + } + + Ok(messages) + } + .instrument(_span) + .await + } +} diff --git a/affinidi-messaging-mediator/src/database/get.rs b/affinidi-messaging-mediator/src/database/get.rs new file mode 100644 index 0000000..02f8597 --- /dev/null +++ b/affinidi-messaging-mediator/src/database/get.rs @@ -0,0 +1,97 @@ +use affinidi_messaging_sdk::messages::MessageListElement; +use itertools::Itertools; +use redis::{from_redis_value, Value}; +use tracing::{debug, event, span, Instrument, Level}; + +use crate::common::errors::MediatorError; + +use super::DatabaseHandler; + +impl DatabaseHandler { + /// Get a message from the database + /// - msg_id: The unique identifier of the message + pub async fn get_message( + &self, + did_hash: &str, + msg_id: &str, + ) -> Result { + let _span = span!(Level::DEBUG, "get_message", msg_id = msg_id,); + async move { + let mut conn = self.get_async_connection().await?; + + let (didcomm_message, meta_data): (Value, Vec) = deadpool_redis::redis::pipe() + .atomic() + .cmd("GET") + .arg(["MSG:", msg_id].concat()) + .cmd("HGETALL") + .arg(["MSG:META:", msg_id].concat()) + .query_async(&mut conn) + .await + .map_err(|err| { + event!( + Level::ERROR, + "Couldn't get message_id({}) from database: {}", + msg_id, + err + ); + MediatorError::DatabaseError( + "NA".into(), + format!("Couldn't get message_id({}) from database: {}", msg_id, err), + ) + })?; + + let didcomm_message: String = match didcomm_message { + Value::Nil => { + return Err(MediatorError::DatabaseError( + did_hash.into(), + format!("Message not found for ID: {}", msg_id), + )); + } + v => from_redis_value(&v).map_err(|e| { + MediatorError::InternalError( + did_hash.into(), + format!("Couldn't convert didcomm_message to string: {}", e), + ) + })?, + }; + + debug!("didcomm_message: {:?}", didcomm_message); + debug!("metadata: {:?}", meta_data); + + let mut message = MessageListElement { + msg_id: msg_id.to_string(), + msg: Some(didcomm_message), + ..Default::default() + }; + + for (k, v) in meta_data.iter().tuples() { + match k.as_str() { + "MSG_ID" => message.msg_id.clone_from(v), + "BYTES" => message.size = v.parse().unwrap_or(0), + "FROM" => message.from_address = Some(v.clone()), + "TO" => message.to_address = Some(v.clone()), + "TIMESTAMP" => message.timestamp = v.parse().unwrap_or(0), + "SEND_ID" => message.send_id = Some(v.clone()), + "RECEIVE_ID" => message.receive_id = Some(v.clone()), + _ => {} + } + } + + // Update SEND metrics + + if did_hash == message.from_address.as_ref().unwrap_or(&"".to_string()) + || did_hash == message.to_address.as_ref().unwrap_or(&"".to_string()) + { + let _ = self.update_send_stats(message.size as i64).await; + Ok(message) + } else { + Err(MediatorError::DatabaseError( + did_hash.into(), + format!("Message not found for DID: {}", did_hash), + )) + } + } + .instrument(_span) + .await + } +} diff --git a/affinidi-messaging-mediator/src/database/handlers.rs b/affinidi-messaging-mediator/src/database/handlers.rs new file mode 100644 index 0000000..ad010f3 --- /dev/null +++ b/affinidi-messaging-mediator/src/database/handlers.rs @@ -0,0 +1,143 @@ +use std::{thread::sleep, time::Duration}; + +use deadpool_redis::Connection; +use redis::aio::PubSub; +use tracing::{event, Level}; + +use crate::common::{config::Config, errors::MediatorError}; + +use super::DatabaseHandler; + +impl DatabaseHandler { + pub async fn new(config: &Config) -> Result { + // Creates initial pool Configuration from the redis database URL + let pool = deadpool_redis::Config::from_url(&config.database_url) + .builder() + .map_err(|err| { + event!(Level::ERROR, "Database URL is invalid. Reason: {}", err); + MediatorError::DatabaseError( + "NA".into(), + format!("Database URL is invalid. Reason: {}", err), + ) + })?; + + // Now that we have a base config, we customise the redis pool config + // and create the async pool of redis connections + let pool = pool + .runtime(deadpool_redis::Runtime::Tokio1) + .max_size(config.database_pool_size) + .timeouts(deadpool_redis::Timeouts { + wait: Some(Duration::from_secs(config.database_timeout.into())), + create: Some(Duration::from_secs(config.database_timeout.into())), + recycle: Some(Duration::from_secs(config.database_timeout.into())), + }) + .build() + .map_err(|err| { + event!(Level::ERROR, "Database config is invalid. Reason: {}", err); + MediatorError::DatabaseError( + "NA".into(), + format!("Database config is invalid. Reason: {}", err), + ) + })?; + + let database = Self { + pool, + redis_url: config.database_url.clone(), + }; + loop { + let mut conn = match database.get_async_connection().await { + Ok(conn) => conn, + Err(err) => { + event!(Level::WARN, "Error getting connection to database: {}", err); + event!(Level::WARN, "Retrying database connection in 10 seconds"); + sleep(Duration::from_secs(10)); + continue; + } + }; + + let pong: Result = + deadpool_redis::redis::cmd("PING") + .query_async(&mut conn) + .await; + match pong { + Ok(pong) => { + event!( + Level::INFO, + "Database ping ok! Expected (PONG) received ({})", + pong + ); + break; + } + Err(err) => { + event!( + Level::WARN, + "Can't get connection to database. Reason: {}", + err + ); + event!(Level::WARN, "Retrying database connection in 10 seconds"); + sleep(Duration::from_secs(10)); + } + } + } + + // Check and load LUA scripts as required + { + let mut conn = database.get_async_connection().await?; + let function_load: Result = + deadpool_redis::redis::cmd("FUNCTION") + .arg("LOAD") + .arg(config.lua_scripts.clone()) + .query_async(&mut conn) + .await; + match function_load { + Ok(function_load) => { + event!( + Level::INFO, + "database response for FUNCTION LOAD: ({})", + function_load + ); + } + Err(err) => { + event!( + Level::WARN, + "database response for FUNCTION LOAD: ({})", + err + ); + } + } + } + + database.get_db_metadata().await?; + Ok(database) + } + + /// Returns a redis async database connector or returns an Error + /// This is the main method to get a connection to the database + pub async fn get_async_connection(&self) -> Result { + self.pool.get().await.map_err(|err| { + event!(Level::ERROR, "Couldn't get database connection: {}", err); + MediatorError::DatabaseError( + "NA".into(), + format!("Couldn't get database connection: {}", err), + ) + }) + } + + /// Returns a redis database connector or returns an Error + /// This should only be used for pubsub operations + pub async fn get_pubsub_connection(&self) -> Result { + let client = redis::Client::open(self.redis_url.clone()).map_err(|err| { + MediatorError::DatabaseError( + "NA".into(), + format!("Couldn't open redis pubsub connection. Reason: {}", err), + ) + })?; + + client.get_async_pubsub().await.map_err(|err| { + MediatorError::DatabaseError( + "NA".into(), + format!("Couldn't get redis pubsub connection. Reason: {}", err), + ) + }) + } +} diff --git a/affinidi-messaging-mediator/src/database/list.rs b/affinidi-messaging-mediator/src/database/list.rs new file mode 100644 index 0000000..105150c --- /dev/null +++ b/affinidi-messaging-mediator/src/database/list.rs @@ -0,0 +1,140 @@ +use affinidi_messaging_sdk::messages::{Folder, MessageList, MessageListElement}; +use itertools::Itertools; +use redis::{from_redis_value, Value}; +use tracing::{event, span, Instrument, Level}; + +use crate::common::errors::MediatorError; + +use super::DatabaseHandler; + +impl DatabaseHandler { + /// Retrieves list of messages for the specified DID and folder + /// The folder can be either Inbox or Outbox + /// - did_hash: The DID sha256 hash to retrieve messages for + /// - range: stream ID range to retrieve (defaults to '-' and '+' which gets all messages) + pub async fn list_messages( + &self, + did_hash: &str, + folder: Folder, + range: Option<(&str, &str)>, + ) -> Result { + let _span = span!( + Level::DEBUG, + "list_messages", + did_hash = did_hash, + folder = format!("{:?}", folder), + range = format!("{:?}", range) + ); + async move { + let mut conn = self.get_async_connection().await?; + + let key = match folder { + Folder::Inbox => format!("RECEIVE_Q:{}", did_hash), + Folder::Outbox => format!("SEND_Q:{}", did_hash), + }; + + let (start, end) = if let Some((start, end)) = range { + (start, end) + } else { + ("-", "+") + }; + + let db_response: Value = deadpool_redis::redis::cmd("XRANGE") + .arg(&key) + .arg(start) + .arg(end) + .query_async(&mut conn) + .await + .map_err(|err| { + event!( + Level::ERROR, + "Couldn't get message_list({}) from database for DID_hash {}: {}", + key, + did_hash, + err + ); + MediatorError::DatabaseError( + did_hash.into(), + format!( + "Couldn't get message_list({}) from database for DID_hash {}: {}", + key, did_hash, err + ), + ) + })?; + + // The following should really be a Impl FromRedisValue for MessageList + // But I don't want to poison the affinidi-messaging-sdk crate with Redis and internal details + // So I'm going to manually parse the response here + // We could have an internal/external SDK - but that's a lot of work + let mut messages: MessageList = Vec::new(); + + // Redis response is + // Bulk([bulk(string(id), bulk(string(field), string(field))]) + + fn _error(e: T, did: &str, key: &str) -> MediatorError + where + T: std::fmt::Display, + { + event!( + Level::ERROR, + "Couldn't parse message_list did({}) folder({}): {}", + did, + key, + e + ); + MediatorError::DatabaseError( + "NA".into(), + format!( + "Couldn't parse message_list did({}) folder({}): {}", + did, key, e + ), + ) + } + + let items: Vec = + from_redis_value(&db_response).map_err(|e| _error(e, did_hash, &key))?; + + for item in items { + // item = Bulk(string(id), Bulk(fields...)) + let item: Vec = from_redis_value(&item).unwrap(); + let mut msg_element = MessageListElement::default(); + + let stream_id: String = + from_redis_value(&item[0]).map_err(|e| _error(e, did_hash, &key))?; + match folder { + Folder::Inbox => { + msg_element.receive_id = Some(stream_id.clone()); + } + Folder::Outbox => { + msg_element.send_id = Some(stream_id.clone()); + } + } + + msg_element.timestamp = stream_id + .split('-') + .next() + .unwrap_or("") + .parse() + .unwrap_or(0); + + let fields: Vec = + from_redis_value(&item[1]).map_err(|e| _error(e, did_hash, &key))?; + + for (k, v) in fields.iter().tuples() { + match k.as_str() { + "MSG_ID" => msg_element.msg_id.clone_from(v), + "BYTES" => msg_element.size = v.parse().unwrap_or(0), + "FROM" => msg_element.from_address = Some(v.clone()), + "TO" => msg_element.to_address = Some(v.clone()), + _ => {} + } + } + messages.push(msg_element); + } + + Ok(messages) + } + .instrument(_span) + .await + } +} diff --git a/affinidi-messaging-mediator/src/database/mod.rs b/affinidi-messaging-mediator/src/database/mod.rs new file mode 100644 index 0000000..4e4fb75 --- /dev/null +++ b/affinidi-messaging-mediator/src/database/mod.rs @@ -0,0 +1,14 @@ +pub mod delete; +pub mod fetch; +pub mod get; +pub mod handlers; +pub mod list; +pub mod session; +pub mod stats; +pub mod store; +pub mod streaming; +#[derive(Clone)] +pub struct DatabaseHandler { + pub pool: deadpool_redis::Pool, + redis_url: String, +} diff --git a/affinidi-messaging-mediator/src/database/session.rs b/affinidi-messaging-mediator/src/database/session.rs new file mode 100644 index 0000000..4762a25 --- /dev/null +++ b/affinidi-messaging-mediator/src/database/session.rs @@ -0,0 +1,211 @@ +use std::{ + collections::HashMap, + fmt::{self, Display, Formatter}, +}; + +use serde::{Deserialize, Serialize}; +use tracing::{debug, warn}; + +use crate::common::errors::MediatorError; + +use super::DatabaseHandler; + +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct SessionClaims { + pub aud: String, //audience (atm) + pub sub: String, // subject (DID) + pub session_id: String, + pub exp: u64, +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub enum SessionState { + #[default] + Unknown, + ChallengeSent, + Authenticated, + Blocked, +} + +impl TryFrom<&String> for SessionState { + type Error = MediatorError; + + fn try_from(value: &String) -> Result { + match value.as_str() { + "ChallengeSent" => Ok(Self::ChallengeSent), + "Authenticated" => Ok(Self::Authenticated), + _ => { + warn!("Unknown SessionState: ({})", value); + Err(MediatorError::SessionError( + "NA".into(), + format!("Unknown SessionState: ({})", value), + )) + } + } + } +} + +impl Display for SessionState { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct Session { + #[serde(skip)] + pub session_id: String, + pub challenge: String, + pub remote_address: String, + pub state: SessionState, + pub did: String, +} + +impl TryFrom<(&str, HashMap)> for Session { + type Error = MediatorError; + + fn try_from(value: (&str, HashMap)) -> Result { + let mut session: Session = Session::default(); + let (sid, hash) = value; + session.session_id = sid.into(); + + if let Some(challenge) = hash.get("challenge") { + session.challenge.clone_from(challenge); + } else { + warn!( + "{}: No challenge found when retrieving session({})!", + sid, sid + ); + return Err(MediatorError::SessionError( + sid.into(), + "No challenge found when retrieving session!".into(), + )); + } + + if let Some(remote_address) = hash.get("remote_address") { + session.remote_address.clone_from(remote_address); + } else { + warn!( + "{}: No remote_address found when retrieving session({})!", + sid, sid + ); + return Err(MediatorError::SessionError( + sid.into(), + "No remote_address found when retrieving session!".into(), + )); + } + + if let Some(state) = hash.get("state") { + session.state = state.try_into()?; + } else { + warn!("{}: No state found when retrieving session({})!", sid, sid); + return Err(MediatorError::SessionError( + sid.into(), + "No state found when retrieving session!".into(), + )); + } + + if let Some(did) = hash.get("did") { + session.did = did.into(); + } else { + warn!("{}: No DID found when retrieving session({})!", sid, sid); + return Err(MediatorError::SessionError( + sid.into(), + "No DID found when retrieving session!".into(), + )); + } + + Ok(session) + } +} + +impl DatabaseHandler { + /// Creates a new session in the database + /// Typically called when sending the initial challenge to the client + pub async fn create_session(&self, session: &Session) -> Result<(), MediatorError> { + let mut con = self.get_async_connection().await?; + + let sid = format!("SESSION:{}", session.session_id); + + deadpool_redis::redis::pipe() + .atomic() + .cmd("HSET") + .arg(&sid) + .arg("challenge") + .arg(&session.challenge) + .arg("remote_address") + .arg(&session.remote_address) + .arg("state") + .arg(session.state.to_string()) + .arg("did") + .arg(&session.did) + .cmd("HINCRBY") + .arg("GLOBAL") + .arg("SESSIONS_CREATED") + .arg(1) + .expire(&sid, 900) + .query_async(&mut con) + .await + .map_err(|err| { + MediatorError::SessionError( + sid.clone(), + format!("tried to create new session ({})! Error: {}", sid, err), + ) + })?; + + debug!("Session created: {:?}", session); + + Ok(()) + } + + /// Retrieves a session from the database + pub async fn get_session(&self, session_id: &str) -> Result { + let mut con = self.get_async_connection().await?; + + let result: HashMap = deadpool_redis::redis::cmd("HGETALL") + .arg(format!("SESSION:{}", session_id)) + .query_async(&mut con) + .await + .map_err(|err| { + MediatorError::SessionError( + session_id.into(), + format!("tried to retrieve session({}). Error: {}", session_id, err), + ) + })?; + + (session_id, result).try_into() + } + + /// Updates a session in the database to become authenticated + /// Updates the state, and the expiry time + pub async fn update_session_authenticated( + &self, + session_id: &str, + ) -> Result<(), MediatorError> { + let mut con = self.get_async_connection().await?; + + let sid = format!("SESSION:{}", session_id); + + deadpool_redis::redis::pipe() + .atomic() + .cmd("HSET") + .arg(&sid) + .arg("state") + .arg(SessionState::Authenticated.to_string()) + .cmd("HINCRBY") + .arg("GLOBAL") + .arg("SESSIONS_SUCCESS") + .arg(1) + .expire(&sid, 86400) + .query_async(&mut con) + .await + .map_err(|err| { + MediatorError::SessionError( + session_id.into(), + format!("tried to retrieve session({}). Error: {}", session_id, err), + ) + })?; + + Ok(()) + } +} diff --git a/affinidi-messaging-mediator/src/database/stats.rs b/affinidi-messaging-mediator/src/database/stats.rs new file mode 100644 index 0000000..c17afd1 --- /dev/null +++ b/affinidi-messaging-mediator/src/database/stats.rs @@ -0,0 +1,188 @@ +use super::DatabaseHandler; +use crate::common::errors::MediatorError; +use itertools::Itertools; +use redis::{from_redis_value, Value}; +use std::fmt::{self, Display, Formatter}; +use tracing::{debug, event, Level}; + +/// Statistics for the mediator +#[derive(Default, Debug)] +pub struct MetadataStats { + pub received_bytes: i64, // Total number of bytes processed + pub sent_bytes: i64, // Total number of bytes sent + pub deleted_bytes: i64, // Total number of bytes deleted + pub received_count: i64, // Total number of messages received + pub sent_count: i64, // Total number of messages sent + pub deleted_count: i64, // Total number of messages deleted + pub websocket_open: i64, // Total number of websocket connections opened + pub websocket_close: i64, // Total number of websocket connections closed + pub sessions_created: i64, // Total number of sessions created + pub sessions_success: i64, // Total number of sessions successfully authenticated +} + +impl Display for MetadataStats { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!( + f, + r#" + Message counts: recv({}) sent({}) deleted({}) queued({}) + Storage: received({}), sent({}), deleted({}), current_queued({}) + Connections: ws_open({}) ws_close({}) ws_current({}) :: sessions_created({}), sessions_authenticated({}) + "#, + self.received_count, + self.sent_count, + self.deleted_count, + self.received_count - self.deleted_count, + self.received_bytes, + self.sent_bytes, + self.deleted_bytes, + self.received_bytes - self.deleted_bytes, + self.websocket_open, + self.websocket_close, + self.websocket_open - self.websocket_close, + self.sessions_created, + self.sessions_success + ) + } +} + +impl MetadataStats { + /// Calculate the delta between two MetadataStats + pub fn delta(&self, previous: &MetadataStats) -> MetadataStats { + MetadataStats { + received_bytes: self.received_bytes - previous.received_bytes, + sent_bytes: self.sent_bytes - previous.sent_bytes, + deleted_bytes: self.deleted_bytes - previous.deleted_bytes, + received_count: self.received_count - previous.received_count, + sent_count: self.sent_count - previous.sent_count, + deleted_count: self.deleted_count - previous.deleted_count, + websocket_open: self.websocket_open - previous.websocket_open, + websocket_close: self.websocket_close - previous.websocket_close, + sessions_created: self.sessions_created - previous.sessions_created, + sessions_success: self.sessions_success - previous.sessions_success, + } + } +} + +impl DatabaseHandler { + /// Retrieves metadata statistics that are global to the mediator database + /// This means it may include more than this mediator's messages + pub async fn get_db_metadata(&self) -> Result { + let mut conn = self.get_async_connection().await?; + + let mut stats = MetadataStats::default(); + + let result: Value = deadpool_redis::redis::cmd("HGETALL") + .arg("GLOBAL") + .query_async(&mut conn) + .await + .map_err(|err| { + event!( + Level::ERROR, + "Couldn't get shared METADATA from database: {}", + err + ); + MediatorError::DatabaseError( + "NA".into(), + format!("Couldn't get shared METADATA from database: {}", err), + ) + })?; + + let result: Vec = from_redis_value(&result).map_err(|e| { + MediatorError::DatabaseError( + "NA".into(), + format!("Couldn't parse GLOBAL metadata from database: {}", e), + ) + })?; + debug!("Stats: {:?}", result); + + for (k, v) in result.iter().tuples() { + match k.as_str() { + "RECEIVED_BYTES" => stats.received_bytes = v.parse().unwrap_or(0), + "SENT_BYTES" => stats.sent_bytes = v.parse().unwrap_or(0), + "DELETED_BYTES" => stats.deleted_bytes = v.parse().unwrap_or(0), + "RECEIVED_COUNT" => stats.received_count = v.parse().unwrap_or(0), + "SENT_COUNT" => stats.sent_count = v.parse().unwrap_or(0), + "DELETED_COUNT" => stats.deleted_count = v.parse().unwrap_or(0), + "WEBSOCKET_OPEN" => stats.websocket_open = v.parse().unwrap_or(0), + "WEBSOCKET_CLOSE" => stats.websocket_close = v.parse().unwrap_or(0), + "SESSIONS_CREATED" => stats.sessions_created = v.parse().unwrap_or(0), + "SESSIONS_SUCCESS" => stats.sessions_success = v.parse().unwrap_or(0), + _ => {} + } + } + + Ok(stats) + } + + /// Updates GLOBAL send metrics + pub async fn update_send_stats(&self, sent_bytes: i64) -> Result<(), MediatorError> { + let mut con = self.get_async_connection().await?; + + deadpool_redis::redis::pipe() + .atomic() + .cmd("HINCRBY") + .arg("GLOBAL") + .arg("SENT_BYTES") + .arg(sent_bytes) + .cmd("HINCRBY") + .arg("SENT_COUNT") + .arg(1) + .query_async(&mut con) + .await + .map_err(|err| { + MediatorError::DatabaseError( + "INTERNAL".into(), + format!("Couldn't update GLOBAL SEND stats. Reason: {}", err), + ) + })?; + + Ok(()) + } + + /// Increment WebSocket open count + pub async fn global_stats_increment_websocket_open(&self) -> Result<(), MediatorError> { + let mut con = self.get_async_connection().await?; + + deadpool_redis::redis::cmd("HINCRBY") + .arg("GLOBAL") + .arg("WEBSOCKET_OPEN") + .arg(1) + .query_async(&mut con) + .await + .map_err(|err| { + MediatorError::DatabaseError( + "INTERNAL".into(), + format!( + "Couldn't update GLOBAL(WEBSOCKET_OPEN) stats. Reason: {}", + err + ), + ) + })?; + + Ok(()) + } + + /// Increment WebSocket close count + pub async fn global_stats_increment_websocket_close(&self) -> Result<(), MediatorError> { + let mut con = self.get_async_connection().await?; + + deadpool_redis::redis::cmd("HINCRBY") + .arg("GLOBAL") + .arg("WEBSOCKET_CLOSE") + .arg(1) + .query_async(&mut con) + .await + .map_err(|err| { + MediatorError::DatabaseError( + "INTERNAL".into(), + format!( + "Couldn't update GLOBAL(WEBSOCKET_CLOSE) stats. Reason: {}", + err + ), + ) + })?; + + Ok(()) + } +} diff --git a/affinidi-messaging-mediator/src/database/store.rs b/affinidi-messaging-mediator/src/database/store.rs new file mode 100644 index 0000000..33b4502 --- /dev/null +++ b/affinidi-messaging-mediator/src/database/store.rs @@ -0,0 +1,126 @@ +use super::DatabaseHandler; +use crate::common::errors::MediatorError; +use serde::{Deserialize, Serialize}; +use sha256::digest; +use tracing::{debug, event, span, Instrument, Level}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct MessageMetaData { + pub bytes: usize, + pub to_did_hash: String, + pub from_did_hash: Option, + pub timestamp: u128, +} + +impl DatabaseHandler { + /// Stores a message in the database + /// Returns the message_id (hash of the message) + pub async fn store_message( + &self, + session_id: &str, + message: &str, + to_did: &str, + from_did: Option<&str>, + ) -> Result { + let _span = span!(Level::DEBUG, "store_message", session_id = session_id); + async move { + let message_hash = digest(message.as_bytes()); + let to_hash = digest(to_did.as_bytes()); + + let mut conn = self.get_async_connection().await?; + let mut tx = deadpool_redis::redis::cmd("FCALL"); + + tx.arg("store_message") + .arg(1) + .arg(&message_hash) + .arg(message) + .arg(message.len()) + .arg(to_did) + .arg(&to_hash); + + if let Some(from_did) = from_did { + let from_hash = digest(from_did.as_bytes()); + tx.arg(from_did).arg(from_hash); + } else { + tx.arg("ANONYMOUS"); + } + + debug!( + "trying to store msg_id({}), from({:?}) from_hash({:?}) to({}) to_hash({}), bytes({})", + message_hash, + from_did, + from_did.map(|h| digest(h.as_bytes())), + to_did, + &to_hash, + message.len() + ); + + let result: String = tx.query_async(&mut conn).await.map_err(|err| { + event!(Level::ERROR, "Couldn't store message in database: {}", err); + MediatorError::DatabaseError( + session_id.into(), + format!("Couldn't store message in database: {}", err), + ) + })?; + + debug!("result = {:?}", result); + + debug!("Message hash({}) stored in database", message_hash); + + Ok(message_hash) + } + .instrument(_span) + .await + } + + /// Retrieves the message MetaData for a given message hash + /// - session_id: The session_id for the request + /// - message_hash: The hash of the message to retrieve + pub async fn get_message_metadata( + &self, + session_id: &str, + message_hash: &str, + ) -> Result { + let _span = span!( + Level::DEBUG, + "get_message_metadata", + session_id = session_id, + message_hash = message_hash + ); + async move { + let mut conn = self.get_async_connection().await?; + let metadata: String = deadpool_redis::redis::cmd("HGET") + .arg("MESSAGE_STORE") + .arg(["METADATA:", message_hash].concat()) + .query_async(&mut conn) + .await + .map_err(|err| { + event!( + Level::ERROR, + "Couldn't get message metadata from database: {}", + err + ); + MediatorError::DatabaseError( + session_id.into(), + format!("Couldn't get message metadata from database: {}", err), + ) + })?; + + let metadata: MessageMetaData = serde_json::from_str(&metadata).map_err(|err| { + event!( + Level::ERROR, + "Couldn't parse message metadata from database: {}", + err + ); + MediatorError::DatabaseError( + session_id.into(), + format!("Couldn't parse message metadata from database: {}", err), + ) + })?; + + Ok(metadata) + } + .instrument(_span) + .await + } +} diff --git a/affinidi-messaging-mediator/src/database/streaming.rs b/affinidi-messaging-mediator/src/database/streaming.rs new file mode 100644 index 0000000..7b8138c --- /dev/null +++ b/affinidi-messaging-mediator/src/database/streaming.rs @@ -0,0 +1,367 @@ +use super::DatabaseHandler; +use crate::{common::errors::MediatorError, tasks::websocket_streaming::PubSubRecord}; +use redis::{from_redis_value, Value}; +use tracing::{debug, error, event, Level}; + +impl DatabaseHandler { + pub async fn streaming_clean_start(&self, uuid: &str) -> Result<(), MediatorError> { + let mut conn = self.get_async_connection().await?; + + let response: Vec = deadpool_redis::redis::pipe() + .atomic() + .cmd("FCALL") + .arg("clean_start_streaming") + .arg(1) + .arg(uuid) + .query_async(&mut conn) + .await + .map_err(|err| { + event!( + Level::ERROR, + "redis function clean_start_streaming() failed. Reason: {}", + err + ); + MediatorError::DatabaseError( + "NA".into(), + format!( + "redis function clean_start_streaming() failed. Reason: {}", + err + ), + ) + })?; + + if let Ok(count) = from_redis_value::(&response[0]) { + event!( + Level::INFO, + "clean_start_streaming() cleaned {} sessions", + count + ); + Ok(()) + } else { + event!( + Level::ERROR, + "clean_start_streaming() failed to parse response: {:?}", + response + ); + Err(MediatorError::DatabaseError( + "NA".into(), + format!( + "redis fn clean_start_streaming() failed. Response ({:?})", + response + ), + )) + } + } + + /// Checks if the given DID hash is live streaming + /// did_hash: The DID hash to check + /// force_delivery: If true, the message will be delivered even if the client is not live streaming + /// Returns the streaming service ID that the client is connecting to if the DID hash is live streaming + pub async fn streaming_is_client_live( + &self, + did_hash: &str, + force_delivery: bool, + ) -> Option { + let mut conn = if let Ok(conn) = self.get_async_connection().await { + conn + } else { + error!("is_live_streaming(): Failed to get connection to Redis"); + return None; + }; + + match deadpool_redis::redis::cmd("HGET") + .arg("GLOBAL_STREAMING") + .arg(did_hash) + .query_async::>(&mut conn) + .await + { + Ok(response) => { + if let Some(row) = response { + let parts: Vec<&str> = row.split(':').collect(); + + if parts.len() == 2 { + if parts[1] == "TRUE" { + // Client is live streaming + Some(parts[0].to_string()) + } else if force_delivery { + // Client is not live streaming, but we are forcing delivery + Some(parts[0].to_string()) + } else { + // Client is not live streaming + None + } + } else { + // Invalid Redis data + None + } + } else { + // No Redis data found. + None + } + } + Err(err) => { + event!( + Level::ERROR, + "check if live_streaming for did_hash({}) failed. Reason: {}", + did_hash, + err + ); + None + } + } + } + + /// Publishes a live message to the streaming service + /// This follows a call to is_live_streaming() to get the streaming service ID if valid + /// NOTE: There is a chance that a client could close live-streaming between the check and publishing + /// Not an issue, as the streaming component will handle this gracefully + /// NOTE: Why not combine is_live_streaming() and this function into an atomic action? + /// While it would be more efficient in some ways, it would also mean pushing the full message to Redis + /// on every delivery, as messages get larger this would become less efficient. + /// So best compromise is to have the client check if live-streaming is active, then send the message + pub async fn streaming_publish_message( + &self, + did_hash: &str, + stream_uuid: &str, + message: &str, + force_delivery: bool, + ) -> Result<(), MediatorError> { + let record = match serde_json::to_string(&PubSubRecord { + did_hash: did_hash.to_string(), + message: message.to_string(), + force_delivery, + }) { + Ok(record) => record, + Err(err) => { + event!( + Level::ERROR, + "publish_live_message() for did_hash({}) failed to serialize message. Reason: {}", + did_hash, + err + ); + return Err(MediatorError::DatabaseError( + "NA".into(), + format!( + "publish_live_message() for did_hash({}) failed to serialize message. Reason: {}", + did_hash, + err + ), + )); + } + }; + + let mut conn = self.get_async_connection().await?; + + match deadpool_redis::redis::cmd("PUBLISH") + .arg(["CHANNEL:", stream_uuid].concat()) + .arg(record) + .query_async::(&mut conn) + .await + { + Ok(_) => { + debug!( + "published message to channel(CHANNEL:{}) for did_hash({})", + stream_uuid, did_hash + ); + Ok(()) + } + Err(err) => { + event!( + Level::ERROR, + "publish_live_message() for did_hash({}) channel(CHANNEL:{}) failed. Reason: {}", + did_hash, + stream_uuid, + err + ); + Err(MediatorError::DatabaseError( + "NA".into(), + format!( + "publish_live_message() for did_hash({}) channel(CHANNEL:{}) failed. Reason: {}", + did_hash, + stream_uuid, + err + ), + )) + } + } + } + + /// Registers a client to the live streaming service + pub async fn streaming_register_client( + &self, + did_hash: &str, + stream_uuid: &str, + ) -> Result<(), MediatorError> { + let mut conn = self.get_async_connection().await?; + + match deadpool_redis::redis::pipe() + .atomic() + .cmd("SADD") + .arg(["STREAMING_SESSIONS:", stream_uuid].concat()) + .arg(did_hash) + .cmd("HSET") + .arg("GLOBAL_STREAMING") + .arg(did_hash) + .arg([stream_uuid, ":FALSE"].concat()) + .query_async::(&mut conn) + .await + { + Ok(_) => { + debug!("did_hash({}) registered to ({})", did_hash, stream_uuid); + + Ok(()) + } + Err(err) => { + event!( + Level::ERROR, + "streaming_register_client() for did_hash({}) stream_uuid(STREAMING_SESSIONS:{}) failed. Reason: {}", + did_hash, + stream_uuid, + err + ); + Err(MediatorError::DatabaseError( + "NA".into(), + format!( + "streaming_register_client() for did_hash({}) stream_uuid(STREAMING_SESSIONS:{}) failed. Reason: {}", + did_hash, + stream_uuid, + err + ), + )) + } + } + } + + /// Enables live streaming for the client + pub async fn streaming_start_live( + &self, + did_hash: &str, + stream_uuid: &str, + ) -> Result<(), MediatorError> { + let mut conn = self.get_async_connection().await?; + + match deadpool_redis::redis::cmd("HSET") + .arg("GLOBAL_STREAMING") + .arg(did_hash) + .arg([stream_uuid, ":", "TRUE"].concat()) + .query_async::(&mut conn) + .await + { + Ok(_) => { + debug!( + "did_hash({}) started live streaming from ({})", + did_hash, stream_uuid + ); + + Ok(()) + } + Err(err) => { + event!( + Level::ERROR, + "streaming_start_live() for did_hash({}) stream_uuid(STREAMING_SESSIONS:{}) failed. Reason: {}", + did_hash, + stream_uuid, + err + ); + Err(MediatorError::DatabaseError( + "NA".into(), + format!( + "streaming_start_live() for did_hash({}) stream_uuid(STREAMING_SESSIONS:{}) failed. Reason: {}", + did_hash, + stream_uuid, + err + ), + )) + } + } + } + + /// Disables live streaming for a client + pub async fn streaming_stop_live( + &self, + did_hash: &str, + stream_uuid: &str, + ) -> Result<(), MediatorError> { + let mut conn = self.get_async_connection().await?; + + match deadpool_redis::redis::cmd("HSET") + .arg("GLOBAL_STREAMING") + .arg(did_hash) + .arg([stream_uuid, ":", "FALSE"].concat()) + .query_async::(&mut conn) + .await + { + Ok(_) => { + debug!( + "did_hash({}) stopped live streaming from ({})", + did_hash, stream_uuid + ); + + Ok(()) + } + Err(err) => { + event!( + Level::ERROR, + "streaming_stop_live() for did_hash({}) stream_uuid(STREAMING_SESSIONS:{}) failed. Reason: {}", + did_hash, + stream_uuid, + err + ); + Err(MediatorError::DatabaseError( + "NA".into(), + format!( + "streaming_stop_live() for did_hash({}) stream_uuid(STREAMING_SESSIONS:{}) failed. Reason: {}", + did_hash, + stream_uuid, + err + ), + )) + } + } + } + + /// Removes client from live streaming service + pub async fn streaming_deregister_client( + &self, + did_hash: &str, + stream_uuid: &str, + ) -> Result<(), MediatorError> { + let mut conn = self.get_async_connection().await?; + + match deadpool_redis::redis::pipe() + .atomic() + .cmd("SREM") + .arg(["STREAMING_SESSIONS:", stream_uuid].concat()) + .arg(did_hash) + .cmd("HDEL") + .arg("GLOBAL_STREAMING") + .arg(did_hash) + .query_async::(&mut conn) + .await + { + Ok(_) => { + debug!("did_hash({}) deregistered from ({})", did_hash, stream_uuid); + + Ok(()) + } + Err(err) => { + event!( + Level::ERROR, + "streaming_deregister_client() for did_hash({}) stream_uuid(STREAMING_SESSIONS:{}) failed. Reason: {}", + did_hash, + stream_uuid, + err + ); + Err(MediatorError::DatabaseError( + "NA".into(), + format!( + "streaming_deregister_client() for did_hash({}) stream_uuid(STREAMING_SESSIONS:{}) failed. Reason: {}", + did_hash, + stream_uuid, + err + ), + )) + } + } + } +} diff --git a/affinidi-messaging-mediator/src/handlers/authenticate.rs b/affinidi-messaging-mediator/src/handlers/authenticate.rs new file mode 100644 index 0000000..9ca1817 --- /dev/null +++ b/affinidi-messaging-mediator/src/handlers/authenticate.rs @@ -0,0 +1,285 @@ +use std::{net::SocketAddr, time::SystemTime}; + +use affinidi_messaging_didcomm::{envelope::MetaEnvelope, Message, UnpackOptions}; +use affinidi_messaging_sdk::messages::GenericDataStruct; +use axum::{ + extract::{ConnectInfo, State}, + Json, +}; +use http::StatusCode; +use jsonwebtoken::{encode, Header}; +use rand::{distributions::Alphanumeric, Rng}; +use serde::{Deserialize, Serialize}; +use tracing::{debug, info, warn}; + +use crate::{ + common::errors::{AppError, MediatorError, SuccessResponse}, + database::session::{Session, SessionClaims, SessionState}, + messages::MessageType, + SharedData, +}; + +use super::message_inbound::InboundMessage; + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct AuthenticationChallenge { + pub challenge: String, + pub session_id: String, +} +impl GenericDataStruct for AuthenticationChallenge {} + +/// Request body for POST /authenticate/challenge +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct ChallengeBody { + pub did: String, +} + +// Authorization Process +// 1. Client gets a random challenge from the server +// 2. Client encrypts the random challenge in a message and sends it back to the server POST /authenticate +// 3. Server decrypts the message and verifies the challenge +// 4. If the challenge is correct, the server sends two JWT tokens to the client (access and refresh tokens) +// 5. Client uses the access token to access protected services +// 6. If the access token expires, the client uses the refresh token to get a new access token + +/// POST /authenticate/challenge +/// Request from client to get the challenge +/// This is the first step in the authentication process +/// Creates a new sessionID and a random challenge string to the client +pub async fn authentication_challenge( + ConnectInfo(connect_info): ConnectInfo, + State(state): State, + Json(body): Json, +) -> Result<(StatusCode, Json>), AppError> { + let session = Session { + session_id: create_random_string(12), + challenge: create_random_string(32), + remote_address: connect_info.to_string(), + state: SessionState::ChallengeSent, + did: body.did.clone(), + }; + + state.database.create_session(&session).await?; + + info!( + "{}:{}: Challenge sent to DID({})", + session.session_id, session.remote_address, session.did + ); + + Ok(( + StatusCode::OK, + Json(SuccessResponse { + sessionId: session.session_id.clone(), + httpCode: StatusCode::OK.as_u16(), + errorCode: 0, + errorCodeStr: "NA".to_string(), + message: "Success".to_string(), + data: Some(AuthenticationChallenge { + challenge: session.challenge, + session_id: session.session_id.clone(), + }), + }), + )) +} + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct AuthorizationResponse { + pub access_token: String, + pub refresh_token: String, +} +impl GenericDataStruct for AuthorizationResponse {} + +/// POST /authenticate +/// Response from client to the challenge +/// Unpack the message (only accepts Affinidi Authenticate Protocol) +/// Retrieve Session data from database +/// Check that the DID matches from the message to the session DID recorded +pub async fn authentication_response( + State(state): State, + Json(body): Json, +) -> Result<(StatusCode, Json>), AppError> { + let s = serde_json::to_string(&body).unwrap(); + + let mut envelope = + match MetaEnvelope::new(&s, &state.did_resolver, &state.config.mediator_secrets).await { + Ok(envelope) => envelope, + Err(e) => { + return Err(MediatorError::ParseError( + "UNKNOWN".to_string(), + "Raw inbound DIDComm message".into(), + e.to_string(), + ) + .into()); + } + }; + + // Unpack the message + let (msg, _) = match Message::unpack( + &mut envelope, + &state.did_resolver, + &state.config.mediator_secrets, + &UnpackOptions::default(), + ) + .await + { + Ok(ok) => ok, + Err(e) => { + return Err(MediatorError::MessageUnpackError( + "UNKNOWN".to_string(), + format!("Couldn't unpack incoming message. Reason: {}", e), + ) + .into()); + } + }; + + // Only accepts AffinidiAuthenticate messages + match msg.type_.as_str().parse::()? { + MessageType::AffinidiAuthenticate => (), + _ => { + return Err(MediatorError::SessionError( + "UNKNOWN".to_string(), + "Only accepts Affinidi Authentication protocol messages".to_string(), + ) + .into()) + } + } + + // Ensure the message hasn't expired + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + if let Some(expires) = msg.expires_time { + if expires <= now { + return Err(MediatorError::MessageExpired( + "-1".into(), + expires.to_string(), + now.to_string(), + ) + .into()); + } + } + + // Turn message body into Challenge + let challenge: AuthenticationChallenge = serde_json::from_value(msg.body).map_err(|err| { + warn!("Couldn't parse body into ChallengeBody. Reason: {}", err); + MediatorError::SessionError( + "UNKNOWN".into(), + format!("Couldn't parse body into ChallengeBody. Reason: {}", err), + ) + })?; + + // Retrieve the session info from the database + let session = state.database.get_session(&challenge.session_id).await?; + + // check that the DID matches from what was given for the initial challenge request to what was used for the message response + if let Some(from_did) = msg.from { + if from_did != session.did { + warn!( + "DID ({}) from authorization message does not match DID ({}) from session", + from_did, session.did + ); + return Err(MediatorError::SessionError( + challenge.session_id.clone(), + format!( + "DID ({}) from authorization message does not match DID from session", + from_did + ), + ) + .into()); + } + } + + // Check that this isn't a replay attack + if let SessionState::ChallengeSent = session.state { + debug!("Database session state is ChallengeSent - Good to go!"); + } else { + warn!( + "{}:{}: Session is in an invalid state for authentication", + session.session_id, session.remote_address + ); + return Err(MediatorError::SessionError( + session.session_id.clone(), + "Session is in an invalid state for authentication".into(), + ) + .into()); + } + + // Passed all the checks, now create the JWT tokens + let access_claims = SessionClaims { + aud: "ATM".to_string(), + sub: session.did.clone(), + session_id: session.session_id.clone(), + exp: (SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs() + + 900), + }; + // refresh token expires in 24 hours (86,400 seconds - 900 (15 minutes) = 85,500 seconds) + let mut refresh_claims = access_claims.clone(); + refresh_claims.exp += 85500; + + let response = if let Some(encoding_key) = state.config.jwt_encoding_key.as_ref() { + AuthorizationResponse { + access_token: encode( + &Header::new(jsonwebtoken::Algorithm::EdDSA), + &access_claims, + encoding_key, + ) + .map_err(|err| { + MediatorError::InternalError( + "UNKNOWN".into(), + format!("Couldn't encode access token. Reason: {}", err), + ) + })?, + refresh_token: encode( + &Header::new(jsonwebtoken::Algorithm::EdDSA), + &refresh_claims, + encoding_key, + ) + .map_err(|err| { + MediatorError::InternalError( + "UNKNOWN".into(), + format!("Couldn't encode refresh token. Reason: {}", err), + ) + })?, + } + } else { + return Err( + MediatorError::InternalError("NA".into(), "JWT Encoding Key not found".into()).into(), + ); + }; + + // Set the session state to Authorized + state + .database + .update_session_authenticated(&session.session_id) + .await?; + + info!( + "{}:{}: Authentication successful for DID({})", + session.session_id, session.remote_address, session.did + ); + + Ok(( + StatusCode::OK, + Json(SuccessResponse { + sessionId: session.session_id.clone(), + httpCode: StatusCode::OK.as_u16(), + errorCode: 0, + errorCodeStr: "NA".to_string(), + message: "Success".to_string(), + data: Some(response), + }), + )) +} + +/// creates a random string of up to length characters +fn create_random_string(length: usize) -> String { + rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(length) + .map(char::from) + .collect() +} diff --git a/affinidi-messaging-mediator/src/handlers/inbox_fetch.rs b/affinidi-messaging-mediator/src/handlers/inbox_fetch.rs new file mode 100644 index 0000000..9131224 --- /dev/null +++ b/affinidi-messaging-mediator/src/handlers/inbox_fetch.rs @@ -0,0 +1,64 @@ +use crate::{ + common::errors::{AppError, MediatorError, Session, SuccessResponse}, + SharedData, +}; +use affinidi_messaging_sdk::messages::{fetch::FetchOptions, GetMessagesResponse}; +use axum::{extract::State, Json}; +use http::StatusCode; +use regex::Regex; +use tracing::{span, Instrument, Level}; + +/// Fetches available messages from the inbox +/// +/// # Parameters +/// - `session`: Session information +/// - `folder`: Folder to retrieve messages from +/// - `did_hash`: sha256 hash of the DID we are checking +pub async fn inbox_fetch_handler( + session: Session, + State(state): State, + Json(body): Json, +) -> Result<(StatusCode, Json>), AppError> { + let _span = span!( + Level::DEBUG, + "inbox_fetch_handler", + session = session.session_id, + session_did = session.did, + fetch.limit = body.limit, + fetch.start_id = body.start_id, + fetch.delete_policy = body.delete_policy.to_string() + ); + async move { + + // Check options + if body.limit< 1 || body.limit > 100 { + return Err(MediatorError::ConfigError(session.session_id, format!("limit must be between 1 and 100 inclusive. Received limit({})", body.limit)).into()); + } + + // Check for valid start_id (unixtime in milliseconds including+1 digit so we are ok for another 3,114 years!) + // Supports up to 999 messages per millisecond + let re = Regex::new(r"\d{13,14}-\d{1,3}$").unwrap(); + if let Some(start_id) = &body.start_id { + if ! re.is_match(start_id) { + return Err(MediatorError::ConfigError(session.session_id, format!("start_id isn't valid. Should match UNIX_EPOCH in milliseconds + -(0..999). Received start_id({})", start_id)).into()); + } + } + + // Fetch messages if possible + let results = state.database.fetch_messages(&session.session_id, &session.did_hash, &body).await?; + + Ok(( + StatusCode::OK, + Json(SuccessResponse { + sessionId: session.session_id, + httpCode: StatusCode::OK.as_u16(), + errorCode: 0, + errorCodeStr: "NA".to_string(), + message: "Success".to_string(), + data: Some(results), + }), + )) + } + .instrument(_span) + .await +} diff --git a/affinidi-messaging-mediator/src/handlers/message_delete.rs b/affinidi-messaging-mediator/src/handlers/message_delete.rs new file mode 100644 index 0000000..1fef7db --- /dev/null +++ b/affinidi-messaging-mediator/src/handlers/message_delete.rs @@ -0,0 +1,69 @@ +use affinidi_messaging_didcomm::UnpackMetadata; +use affinidi_messaging_sdk::messages::{ + DeleteMessageRequest, DeleteMessageResponse, GenericDataStruct, +}; +use axum::{extract::State, Json}; +use http::StatusCode; +use serde::{Deserialize, Serialize}; +use tracing::{debug, span, warn, Instrument, Level}; + +use crate::{ + common::errors::{AppError, Session, SuccessResponse}, + SharedData, +}; + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct ResponseData { + pub body: String, + pub metadata: UnpackMetadata, +} +impl GenericDataStruct for ResponseData {} + +/// Deletes a specific message from ATM +/// Returns a list of messages that were deleted +pub async fn message_delete_handler( + session: Session, + State(state): State, + Json(body): Json, +) -> Result<(StatusCode, Json>), AppError> { + let _span = span!( + Level::DEBUG, + "message_delete_handler", + session = session.session_id, + did = session.did, + ); + async move { + debug!("Deleting ({}) messages", body.message_ids.len()); + let mut deleted: DeleteMessageResponse = DeleteMessageResponse::default(); + + for message in &body.message_ids { + debug!("Deleting message: message_id({})", message); + let result = state + .database + .delete_message(&session.session_id, &session.did_hash, message) + .await; + + match result { + Ok(_) => deleted.success.push(message.into()), + Err(err) => { + warn!("failed to delete msg({}). Reason: {}", message, err); + deleted.errors.push((message.into(), err.to_string())); + } + } + } + + Ok(( + StatusCode::OK, + Json(SuccessResponse { + sessionId: session.session_id, + httpCode: StatusCode::OK.as_u16(), + errorCode: 0, + errorCodeStr: "NA".to_string(), + message: "Success".to_string(), + data: Some(deleted), + }), + )) + } + .instrument(_span) + .await +} diff --git a/affinidi-messaging-mediator/src/handlers/message_inbound.rs b/affinidi-messaging-mediator/src/handlers/message_inbound.rs new file mode 100644 index 0000000..7ceb279 --- /dev/null +++ b/affinidi-messaging-mediator/src/handlers/message_inbound.rs @@ -0,0 +1,61 @@ +use affinidi_messaging_sdk::messages::sending::InboundMessageResponse; +use axum::{extract::State, Json}; +use http::StatusCode; +use serde::{Deserialize, Serialize}; +use tracing::{span, Instrument, Level}; + +use crate::{ + common::errors::{AppError, Session, SuccessResponse}, + messages::inbound::handle_inbound, + SharedData, +}; + +#[derive(Serialize, Deserialize, Debug)] +pub struct RecipientHeader { + pub kid: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Recipient { + pub header: RecipientHeader, + pub encrypted_key: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct InboundMessage { + pub protected: String, + pub recipients: Vec, + pub iv: String, + pub ciphertext: String, + pub tag: String, +} + +pub async fn message_inbound_handler( + session: Session, + State(state): State, + Json(body): Json, +) -> Result<(StatusCode, Json>), AppError> { + let _span = span!( + Level::DEBUG, + "message_inbound_handler", + session = session.session_id + ); + async move { + let s = serde_json::to_string(&body).unwrap(); + let response = handle_inbound(&state, &session, &s).await?; + + Ok(( + StatusCode::OK, + Json(SuccessResponse { + sessionId: session.session_id, + httpCode: StatusCode::OK.as_u16(), + errorCode: 0, + errorCodeStr: "NA".to_string(), + message: "Success".to_string(), + data: Some(response), + }), + )) + } + .instrument(_span) + .await +} diff --git a/affinidi-messaging-mediator/src/handlers/message_list.rs b/affinidi-messaging-mediator/src/handlers/message_list.rs new file mode 100644 index 0000000..084afd6 --- /dev/null +++ b/affinidi-messaging-mediator/src/handlers/message_list.rs @@ -0,0 +1,71 @@ +use crate::{ + common::errors::{AppError, MediatorError, Session, SuccessResponse}, + SharedData, +}; +use affinidi_messaging_didcomm::UnpackMetadata; +use affinidi_messaging_sdk::messages::{Folder, GenericDataStruct, MessageList}; +use axum::{ + extract::{Path, State}, + Json, +}; +use http::StatusCode; +use serde::{Deserialize, Serialize}; +use tracing::{debug, span, Instrument, Level}; + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct ResponseData { + pub body: String, + pub metadata: UnpackMetadata, +} +impl GenericDataStruct for ResponseData {} + +/// Retrieves lists of messages either from the send or receive queue +/// # Parameters +/// - `session`: Session information +/// - `folder`: Folder to retrieve messages from +/// - `did_hash`: sha256 hash of the DID we are checking +pub async fn message_list_handler( + session: Session, + Path((did_hash, folder)): Path<(String, Folder)>, + State(state): State, +) -> Result<(StatusCode, Json>), AppError> { + let _span = span!( + Level::DEBUG, + "message_list_handler", + session = session.session_id, + session_did = session.did, + did_hash = did_hash, + folder = folder.to_string() + ); + async move { + // Check that the DID hash matches the session DID + // TODO: In the future, add support for lists of DID's owned by the session owner + if session.did_hash != did_hash { + return Err(MediatorError::PermissionError( + session.session_id, + "You don't have permission to access this resource.".into(), + ) + .into()); + } + + let messages = state + .database + .list_messages(&did_hash, folder, None) + .await?; + + debug!("List contains ({}) messages", messages.len()); + Ok(( + StatusCode::OK, + Json(SuccessResponse { + sessionId: session.session_id, + httpCode: StatusCode::OK.as_u16(), + errorCode: 0, + errorCodeStr: "NA".to_string(), + message: "Success".to_string(), + data: Some(messages), + }), + )) + } + .instrument(_span) + .await +} diff --git a/affinidi-messaging-mediator/src/handlers/message_outbound.rs b/affinidi-messaging-mediator/src/handlers/message_outbound.rs new file mode 100644 index 0000000..ab3b84c --- /dev/null +++ b/affinidi-messaging-mediator/src/handlers/message_outbound.rs @@ -0,0 +1,77 @@ +use affinidi_messaging_sdk::messages::{GetMessagesRequest, GetMessagesResponse}; +use axum::{extract::State, Json}; +use http::StatusCode; +use tracing::{debug, span, Instrument, Level}; + +use crate::{ + common::errors::{AppError, Session, SuccessResponse}, + SharedData, +}; + +pub async fn message_outbound_handler( + session: Session, + State(state): State, + Json(body): Json, +) -> Result<(StatusCode, Json>), AppError> { + let _span = span!( + Level::DEBUG, + "message_outbound_handler", + session = session.session_id, + delete = body.delete, + ); + async move { + debug!( + "Client has asked to get ({}) messages", + body.message_ids.len() + ); + + let mut messages = GetMessagesResponse::default(); + + for msg_id in &body.message_ids { + debug!("getting message with id: {}", msg_id); + match state.database.get_message(&session.did_hash, msg_id).await { + Ok(msg) => { + debug!("Got message: {:?}", msg); + messages.success.push(msg); + + if body.delete { + debug!("Deleting message: {}", msg_id); + match state + .database + .delete_message(&session.session_id, &session.did_hash, msg_id) + .await + { + Ok(_) => { + debug!("Deleted message: {}", msg_id); + } + Err(err) => { + debug!("Error deleting message: {:?}", err); + messages + .delete_errors + .push((msg_id.clone(), err.to_string())); + } + } + } + } + Err(err) => { + debug!("Error getting message: {:?}", err); + messages.get_errors.push((msg_id.clone(), err.to_string())); + } + } + } + + Ok(( + StatusCode::OK, + Json(SuccessResponse { + sessionId: session.session_id, + httpCode: StatusCode::OK.as_u16(), + errorCode: 0, + errorCodeStr: "NA".to_string(), + message: "Success".to_string(), + data: Some(messages), + }), + )) + } + .instrument(_span) + .await +} diff --git a/affinidi-messaging-mediator/src/handlers/mod.rs b/affinidi-messaging-mediator/src/handlers/mod.rs new file mode 100644 index 0000000..a8de1d2 --- /dev/null +++ b/affinidi-messaging-mediator/src/handlers/mod.rs @@ -0,0 +1,66 @@ +use crate::SharedData; +use axum::{ + extract::State, + response::IntoResponse, + routing::{delete, get, post}, + Json, Router, +}; + +pub mod authenticate; +pub mod inbox_fetch; +pub mod message_delete; +pub mod message_inbound; +pub mod message_list; +pub mod message_outbound; +pub mod websocket; +pub mod well_known_did_fetch; + +pub fn application_routes(shared_data: &SharedData) -> Router { + let app = Router::new() + // Inbound message handling from ATM clients + .route("/inbound", post(message_inbound::message_inbound_handler)) + // Outbound message handling to ATM clients + .route( + "/outbound", + post(message_outbound::message_outbound_handler), + ) + .route("/fetch", post(inbox_fetch::inbox_fetch_handler)) + // Listing of messages for a DID + .route( + "/list/:did_hash/:folder", + get(message_list::message_list_handler), + ) + // Delete/remove messages stored in ATM + .route("/delete", delete(message_delete::message_delete_handler)) + // Authentication step 1/2 - Client requests challenge from server + .route( + "/authenticate/challenge", + post(authenticate::authentication_challenge), + ) + // Authentication step 2/2 - Client sends encrypted challenge to server + .route("/authenticate", post(authenticate::authentication_response)) + // Websocket endpoint for ATM clients + .route("/ws", get(websocket::websocket_handler)) + .route( + "/.well-known/did", + get(well_known_did_fetch::well_known_did_fetch_handler), + ); + + Router::new() + .nest("/atm/v1/", app) + .with_state(shared_data.to_owned()) +} + +pub async fn health_checker_handler(State(state): State) -> impl IntoResponse { + let message: String = format!( + "Affinidi Secure Messaging Mediator Service, Version: {}, Started: UTC {}", + env!("CARGO_PKG_VERSION"), + state.service_start_timestamp.format("%Y-%m-%d %H:%M:%S"), + ); + + let response_json = serde_json::json!({ + "status": "success".to_string(), + "message": message, + }); + Json(response_json) +} diff --git a/affinidi-messaging-mediator/src/handlers/websocket.rs b/affinidi-messaging-mediator/src/handlers/websocket.rs new file mode 100644 index 0000000..5dfb61e --- /dev/null +++ b/affinidi-messaging-mediator/src/handlers/websocket.rs @@ -0,0 +1,132 @@ +use axum::{ + extract::{ + ws::{Message, WebSocket}, + State, WebSocketUpgrade, + }, + response::IntoResponse, +}; +use tokio::{ + select, + sync::mpsc::{self, Receiver, Sender}, +}; +use tracing::{debug, info, span, warn, Instrument}; + +use crate::{ + common::errors::Session, + messages::inbound::handle_inbound, + tasks::websocket_streaming::{StreamingUpdate, StreamingUpdateState}, + SharedData, +}; + +// Handles the switching of the protocol to a websocket connection +pub async fn websocket_handler( + session: Session, + ws: WebSocketUpgrade, + State(state): State, +) -> impl IntoResponse { + let _span = span!( + tracing::Level::DEBUG, + "websocket_handler", + session = session.session_id + ); + async move { ws.on_upgrade(move |socket| handle_socket(socket, state, session)) } + .instrument(_span) + .await +} + +/// WebSocket state machine. This is spawned per connection. +async fn handle_socket(mut socket: WebSocket, state: SharedData, session: Session) { + let _span = span!( + tracing::Level::DEBUG, + "handle_socket", + session = session.session_id + ); + async move { + // Register the transmission channel between websocket_streaming task and this websocket. + let (tx, mut rx): (Sender, Receiver) = mpsc::channel(5); + if let Some(streaming) = &state.streaming_task { + + let start = StreamingUpdate { + did_hash: session.did_hash.clone(), + state: StreamingUpdateState::Register(tx), + }; + match streaming.channel.send(start).await { + Ok(_) => { + debug!("Sent start message to streaming task"); + } + Err(e) => { + warn!("Error sending start message to streaming task: {:?}", e); + return; + } + } + } + + let _ = state.database.global_stats_increment_websocket_open().await; + info!("Websocket connection established"); + + loop { + select! { + value = socket.recv() => { + if let Some(msg) = value { + info!("ws: Received message: {:?}", msg); + if let Ok(msg) = msg { + if let Message::Text(msg) = msg { + debug!("ws: Received text message: {:?}", msg); + if msg.len() > state.config.ws_size_limit as usize { + warn!("Error processing message, the size is too big. limit is {}, message size is {}", state.config.ws_size_limit, msg.len()); + break; + } + + // Process the message, which also takes care of any storing and live-streaming of the message + match handle_inbound(&state, &session, &msg).await { + Ok(response) => { + debug!("Successful handling of message - finished processing"); + response + } + Err(e) => { + warn!("Error processing message: {:?}", e); + continue; + } + }; + } else { + warn!("Received non-text message, ignoring"); + continue; + } + } + } else { + debug!("Received None, closing connection"); + break; + } + } + value = rx.recv() => { + if let Some(msg) = value { + debug!("ws: Received message from streaming task: {:?}", msg); + let _ = socket.send(Message::Text(msg)).await; + } else { + debug!("Received None from streaming task, closing connection"); + break; + } + } + } + } + + // Remove this websocket and associated info from the streaming task + if let Some(streaming) = &state.streaming_task { + let stop = StreamingUpdate { + did_hash: session.did_hash.clone(), + state: StreamingUpdateState::Deregister, + }; + let _ = streaming.channel.send(stop).await; + } + + // We're done, close the connection + let _ = state + .database + .global_stats_increment_websocket_close() + .await; + + info!("Websocket connection closed"); + } + .instrument(_span) + .await +} diff --git a/affinidi-messaging-mediator/src/handlers/well_known_did_fetch.rs b/affinidi-messaging-mediator/src/handlers/well_known_did_fetch.rs new file mode 100644 index 0000000..6158ca4 --- /dev/null +++ b/affinidi-messaging-mediator/src/handlers/well_known_did_fetch.rs @@ -0,0 +1,29 @@ +use affinidi_messaging_sdk::messages::SuccessResponse; +use axum::{extract::State, Json}; +use http::StatusCode; +use tracing::{span, Instrument, Level}; + +use crate::{common::errors::AppError, SharedData}; + +pub async fn well_known_did_fetch_handler( + State(state): State, +) -> Result<(StatusCode, Json>), AppError> { + let _span = span!(Level::DEBUG, "well_known_jwks_fetch_handler"); + async move { + let did = state.config.clone().mediator_did; + + Ok(( + StatusCode::OK, + Json(SuccessResponse { + sessionId: "".to_string(), + data: Some(did), + httpCode: StatusCode::OK.as_u16(), + errorCode: 0, + errorCodeStr: "NA".to_string(), + message: "Success".to_string(), + }), + )) + } + .instrument(_span) + .await +} diff --git a/affinidi-messaging-mediator/src/lib.rs b/affinidi-messaging-mediator/src/lib.rs new file mode 100644 index 0000000..e4e9a84 --- /dev/null +++ b/affinidi-messaging-mediator/src/lib.rs @@ -0,0 +1,99 @@ +use std::fmt::Debug; + +use affinidi_did_resolver_cache_sdk::DIDCacheClient; +use axum::{ + async_trait, + extract::{FromRef, FromRequestParts}, +}; +use chrono::{DateTime, Utc}; +use common::{ + config::{read_config_file, Config, ConfigRaw}, + errors::MediatorError, + jwt_auth::AuthError, +}; +use database::DatabaseHandler; +use http::request::Parts; +use tasks::websocket_streaming::StreamingTask; +use tracing::{event, level_filters::LevelFilter, Level}; +use tracing_subscriber::{reload::Handle, Registry}; + +pub mod common; +pub mod database; +pub mod handlers; +pub mod messages; +pub mod resolvers; +pub mod tasks; + +#[derive(Clone)] +pub struct SharedData { + pub config: Config, + pub service_start_timestamp: DateTime, + pub did_resolver: DIDCacheClient, + pub database: DatabaseHandler, + pub streaming_task: Option, +} + +impl Debug for SharedData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SharedData") + .field("config", &self.config) + .field("service_start_timestamp", &self.service_start_timestamp) + .finish() + } +} + +#[async_trait] +impl FromRequestParts for SharedData +where + Self: FromRef, + S: Send + Sync + Debug, +{ + type Rejection = AuthError; + + async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result { + Ok(Self::from_ref(state)) // <---- added this line + } +} + +pub async fn init( + reload_handle: Option>, +) -> Result { + // Read configuration file parameters + let config = read_config_file("conf/mediator.toml")?; + + // Setup logging + if reload_handle.is_some() { + let level: LevelFilter = match config.log_level.as_str() { + "trace" => LevelFilter::TRACE, + "debug" => LevelFilter::DEBUG, + "info" => LevelFilter::INFO, + "warn" => LevelFilter::WARN, + "error" => LevelFilter::ERROR, + _ => { + event!( + Level::WARN, + "log_level({}) is unknown in config file. Defaults to INFO", + config.log_level + ); + LevelFilter::INFO + } + }; + reload_handle + .unwrap() + .modify(|filter| *filter = level) + .map_err(|e| MediatorError::InternalError("NA".into(), e.to_string()))?; + event!(Level::INFO, "Log level set to ({})", config.log_level); + } + + match >::try_from(config).await { + Ok(config) => { + event!( + Level::INFO, + "Configuration settings parsed successfully.\n{:#?}", + config + ); + Ok(config) + } + Err(err) => Err(err), + } +} diff --git a/affinidi-messaging-mediator/src/main.rs b/affinidi-messaging-mediator/src/main.rs new file mode 100644 index 0000000..e1b9090 --- /dev/null +++ b/affinidi-messaging-mediator/src/main.rs @@ -0,0 +1,172 @@ +use affinidi_did_resolver_cache_sdk::DIDCacheClient; +use affinidi_messaging_mediator::{ + database::DatabaseHandler, + handlers::{application_routes, health_checker_handler}, + init, + tasks::websocket_streaming::StreamingTask, + SharedData, +}; +use axum::{routing::get, Router}; +use axum_server::tls_rustls::RustlsConfig; +use http::Method; +use std::{env, net::SocketAddr}; +use tower_http::limit::RequestBodyLimitLayer; +use tower_http::trace::{self, TraceLayer}; +use tracing::{event, Level}; +use tracing_subscriber::{filter, layer::SubscriberExt, reload, util::SubscriberInitExt}; + +#[tokio::main] +async fn main() { + // setup logging/tracing framework + let filter = filter::LevelFilter::INFO; // This can be changed in the config file! + let (filter, reload_handle) = reload::Layer::new(filter); + let ansi = env::var("LOCAL").is_ok(); + tracing_subscriber::registry() + .with(filter) + .with(tracing_subscriber::fmt::layer().with_ansi(ansi)) + .init(); + + if ansi { + event!( + Level::INFO, + r#" db ad88 ad88 88 88 88 88 88b d88 88 88"# + ); + event!( + Level::INFO, + r#" d88b d8" d8" "" "" 88 "" 888b d888 88 "" ,d"# + ); + event!( + Level::INFO, + r#" d8'`8b 88 88 88 88`8b d8'88 88 88"# + ); + event!( + Level::INFO, + r#" d8' `8b MM88MMM MM88MMM 88 8b,dPPYba, 88 ,adPPYb,88 88 88 `8b d8' 88 ,adPPYba, ,adPPYb,88 88 ,adPPYYba, MM88MMM ,adPPYba, 8b,dPPYba,"# + ); + event!( + Level::INFO, + r#" d8YaaaaY8b 88 88 88 88P' `"8a 88 a8" `Y88 88 88 `8b d8' 88 a8P_____88 a8" `Y88 88 "" `Y8 88 a8" "8a 88P' "Y8"# + ); + event!( + Level::INFO, + r#" d8""""""""8b 88 88 88 88 88 88 8b 88 88 88 `8b d8' 88 8PP""""""" 8b 88 88 ,adPPPPP88 88 8b d8 88"# + ); + event!( + Level::INFO, + r#" d8' `8b 88 88 88 88 88 88 "8a, ,d88 88 88 `888' 88 "8b, ,aa "8a, ,d88 88 88, ,88 88, "8a, ,a8" 88"# + ); + event!( + Level::INFO, + r#" d8' `8b 88 88 88 88 88 88 `"8bbdP"Y8 88 88 `8' 88 `"Ybbd8"' `"8bbdP"Y8 88 `"8bbdP"Y8 "Y888 `"YbbdP"' 88"# + ); + event!(Level::INFO, ""); + } + + event!( + Level::INFO, + "[Loading Affinidi Secure Messaging Mediator configuration]" + ); + + let config = init(Some(reload_handle)) + .await + .expect("Couldn't initialize mediator!"); + + // Start setting up the database durability and handling + let database = match DatabaseHandler::new(&config).await { + Ok(db) => db, + Err(err) => { + event!(Level::ERROR, "Error opening database: {}", err); + event!(Level::ERROR, "Exiting..."); + std::process::exit(1); + } + }; + + // Start the statistics thread + let _stats_database = database.clone(); // Clone the database handler for the statistics thread + tokio::spawn(async move { + affinidi_messaging_mediator::tasks::statistics::statistics(_stats_database) + .await + .expect("Error starting statistics thread"); + }); + + // Start the streaming thread if enabled + let (streaming_task, _) = if config.streaming_enabled { + let _database = database.clone(); // Clone the database handler for the subscriber thread + let uuid = config.streaming_uuid.clone(); + let (_task, _handle) = StreamingTask::new(_database.clone(), &uuid) + .await + .expect("Error starting streaming task"); + (Some(_task), Some(_handle)) + } else { + (None, None) + }; + + // Create the DID Resolver + let did_resolver = DIDCacheClient::new(config.did_resolver_config.clone()) + .await + .unwrap(); + + // Create the shared application State + let shared_state = SharedData { + config: config.clone(), + service_start_timestamp: chrono::Utc::now(), + did_resolver, + database, + streaming_task, + }; + + // build our application routes + let app: Router = application_routes(&shared_state); + + // Add middleware to all routes + let app = Router::new() + .merge(app) + .layer( + config + .cors_allow_origin + .allow_headers([http::header::CONTENT_TYPE]) + .allow_methods([ + Method::GET, + Method::POST, + Method::PUT, + Method::DELETE, + Method::PATCH, + ]), + ) + .layer( + TraceLayer::new_for_http() + .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO)) + .on_response(trace::DefaultOnResponse::new().level(Level::INFO)), + ) + .layer(RequestBodyLimitLayer::new(config.http_size_limit as usize)) + // Add the healthcheck route after the tracing so we don't fill up logs with healthchecks + .route( + "/atm/healthchecker", + get(health_checker_handler).with_state(shared_state), + ); + + if config.use_ssl { + event!( + Level::INFO, + "This mediator is using SSL/TLS for secure communication." + ); + // configure certificate and private key used by https + // TODO: Build a proper TLS Config + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + let ssl_config = + RustlsConfig::from_pem_file(config.ssl_certificate_file, config.ssl_key_file) + .await + .expect("bad certificate/key"); + + axum_server::bind_rustls(config.listen_address.parse().unwrap(), ssl_config) + .serve(app.into_make_service_with_connect_info::()) + .await + .unwrap(); + } else { + event!(Level::WARN, "**** WARNING: Running without SSL/TLS ****"); + axum_server::bind(config.listen_address.parse().unwrap()) + .serve(app.into_make_service_with_connect_info::()) + .await + .unwrap(); + } +} diff --git a/affinidi-messaging-mediator/src/messages/inbound.rs b/affinidi-messaging-mediator/src/messages/inbound.rs new file mode 100644 index 0000000..5dc1125 --- /dev/null +++ b/affinidi-messaging-mediator/src/messages/inbound.rs @@ -0,0 +1,206 @@ +use crate::{ + common::errors::{MediatorError, Session}, + database::DatabaseHandler, + messages::{MessageHandler, PackOptions}, + SharedData, +}; +use affinidi_messaging_didcomm::{envelope::MetaEnvelope, Message, UnpackOptions}; +use affinidi_messaging_sdk::messages::sending::{InboundMessageList, InboundMessageResponse}; +use tracing::{debug, error, span, trace, warn, Instrument}; + +pub(crate) async fn handle_inbound( + state: &SharedData, + session: &Session, + message: &str, +) -> Result { + let _span = span!(tracing::Level::DEBUG, "handle_inbound",); + + async move { + let mut envelope = + match MetaEnvelope::new(message, &state.did_resolver, &state.config.mediator_secrets) + .await + { + Ok(envelope) => envelope, + Err(e) => { + return Err(MediatorError::ParseError( + session.session_id.clone(), + "Raw inbound DIDComm message".into(), + e.to_string(), + )); + } + }; + debug!("message converted to MetaEnvelope"); + + // Unpack the message + let (msg, metadata) = match Message::unpack( + &mut envelope, + &state.did_resolver, + &state.config.mediator_secrets, + &UnpackOptions { + crypto_operations_limit_per_message: state + .config + .crypto_operations_per_message_limit, + ..UnpackOptions::default() + }, + ) + .await + { + Ok(ok) => ok, + Err(e) => { + return Err(MediatorError::MessageUnpackError( + session.session_id.clone(), + format!("Couldn't unpack incoming message. Reason: {}", e), + )); + } + }; + + debug!("message unpacked:\n{:#?}", msg); + + // Process the message + let message_response = msg.process(state, session).await?; + debug!("message processed:\n{:#?}", message_response); + + // Pack the message and store it if necessary + let packed_message = if message_response.store_message { + let mut stored_messages = InboundMessageList::default(); + if let Some(response) = &message_response.message { + // Pack the message for the next recipient(s) + let to_dids = if let Some(to_did) = &response.to { + to_did + } else { + return Err(MediatorError::MessagePackError( + session.session_id.clone(), + "No recipients found".into(), + )); + }; + debug!( + "response to_dids: count({}) vec({:?})", + to_dids.len(), + to_dids + ); + + if to_dids.len() > state.config.to_recipients_limit { + return Err(MediatorError::MessagePackError( + session.session_id.clone(), + format!("Recipient count({}) exceeds limit", to_dids.len()), + )); + } + + for recipient in to_dids { + let (packed, _msg_metadata) = response + .pack( + recipient, + &state.config.mediator_did, + &metadata, + &state.config.mediator_secrets, + &state.did_resolver, + &PackOptions { to_keys_per_recipient_limit: state.config.to_keys_per_recipient_limit }, + ) + .await?; + + // Live stream the message? + if let Some(stream_uuid) = state + .database + .streaming_is_client_live( + &session.did_hash, + message_response.force_live_delivery, + ) + .await + { + _live_stream( + &state.database, + &session.did_hash, + &stream_uuid, + &packed, + message_response.force_live_delivery, + ) + .await; + } + + match state + .database + .store_message( + &session.session_id, + &packed, + recipient, + Some(&state.config.mediator_did), + ) + .await + { + Ok(msg_id) => { + debug!( + "message id({}) stored successfully recipient({})", + msg_id, recipient + ); + stored_messages.messages.push((recipient.clone(), msg_id)); + } + Err(e) => { + warn!("error storing message recipient({}): {:?}", recipient, e); + stored_messages + .errors + .push((recipient.clone(), e.to_string())); + } + } + } + } + InboundMessageResponse::Stored(stored_messages) + } else if let Some(message) = message_response.message { + let (packed, meta) = message + .pack( + &session.did, + &state.config.mediator_did, + &metadata, + &state.config.mediator_secrets, + &state.did_resolver, + &PackOptions { to_keys_per_recipient_limit: state.config.to_keys_per_recipient_limit }, + ) + .await?; + trace!("Ephemeral message packed (meta):\n{:#?}", meta); + trace!("Ephemeral message (msg):\n{:#?}", packed); + // Live stream the message? + if let Some(stream_uuid) = state + .database + .streaming_is_client_live(&session.did_hash, message_response.force_live_delivery) + .await + { + _live_stream( + &state.database, + &session.did_hash, + &stream_uuid, + &packed, + message_response.force_live_delivery, + ) + .await; + } + InboundMessageResponse::Ephemeral(packed) + } else { + error!("No message to return"); + return Err(MediatorError::InternalError( + session.session_id.clone(), + "Expected a message to return, but got None".into(), + )); + }; + + Ok(packed_message) + } + .instrument(_span) + .await +} + +/// If live streaming is enabled, this function will send the message to the live stream +/// Ok to ignore errors here +async fn _live_stream( + database: &DatabaseHandler, + did_hash: &str, + stream_uuid: &str, + message: &str, + force_live_delivery: bool, +) { + if database + .streaming_publish_message(did_hash, stream_uuid, message, force_live_delivery) + .await + .is_ok() + { + debug!("Live streaming message to UUID: {}", stream_uuid); + } +} diff --git a/affinidi-messaging-mediator/src/messages/mod.rs b/affinidi-messaging-mediator/src/messages/mod.rs new file mode 100644 index 0000000..3451e1a --- /dev/null +++ b/affinidi-messaging-mediator/src/messages/mod.rs @@ -0,0 +1,196 @@ +use self::protocols::ping; +use crate::{ + common::errors::{MediatorError, Session}, + SharedData, +}; +use affinidi_did_resolver_cache_sdk::DIDCacheClient; +use affinidi_messaging_didcomm::{ + secrets::SecretsResolver, Message, PackEncryptedMetadata, PackEncryptedOptions, UnpackMetadata, +}; +use protocols::message_pickup; +use protocols::routing; +use std::{default, str::FromStr, time::SystemTime}; + +pub mod inbound; +pub mod protocols; + +pub enum MessageType { + AffinidiAuthenticate, // Affinidi Authentication Response + ForwardRequest, // DidComm Routing 2.0 Forward Request + MessagePickupStatusRequest, // Message Pickup 3.0 Status Request + MessagePickupDeliveryRequest, // Message Pickup 3.0 Delivery Request + MessagePickupMessagesReceived, // Message Pickup 3.0 Messages Received (ok to delete) + MessagePickupLiveDeliveryChange, // Message Pickup 3.0 Live-delivery-change (Streaming enabled) + TrustPing, // Trust Ping Protocol +} + +impl FromStr for MessageType { + type Err = MediatorError; + + fn from_str(s: &str) -> Result { + match s { + "https://didcomm.org/trust-ping/2.0/ping" => Ok(Self::TrustPing), + "https://affinidi.com/atm/1.0/authenticate" => Ok(Self::AffinidiAuthenticate), + "https://didcomm.org/messagepickup/3.0/status-request" => { + Ok(Self::MessagePickupStatusRequest) + } + "https://didcomm.org/messagepickup/3.0/live-delivery-change" => { + Ok(Self::MessagePickupLiveDeliveryChange) + } + "https://didcomm.org/messagepickup/3.0/delivery-request" => { + Ok(Self::MessagePickupDeliveryRequest) + } + "https://didcomm.org/routing/2.0/forward" => Ok(Self::ForwardRequest), + _ => Err(MediatorError::ParseError( + "-1".into(), + s.into(), + "Couldn't match on MessageType".into(), + )), + } + } +} + +impl MessageType { + pub(crate) async fn process( + &self, + message: &Message, + state: &SharedData, + session: &Session, + ) -> Result { + match self { + Self::TrustPing => ping::process(message, session), + Self::MessagePickupStatusRequest => { + message_pickup::status_request(message, state, session).await + } + Self::MessagePickupDeliveryRequest => { + message_pickup::delivery_request(message, state, session).await + } + Self::MessagePickupMessagesReceived => Err(MediatorError::NotImplemented( + session.session_id.clone(), + "NOT IMPLEMENTED".into(), + )), + Self::MessagePickupLiveDeliveryChange => { + message_pickup::toggle_live_delivery(message, state, session).await + } + Self::AffinidiAuthenticate => Err(MediatorError::NotImplemented( + session.session_id.clone(), + "Affinidi Authentication is only handled by the Authorization handler".into(), + )), + Self::ForwardRequest => routing::process(message, session), + } + } +} + +#[derive(Debug, Default)] +pub(crate) struct ProcessMessageResponse { + pub store_message: bool, + pub force_live_delivery: bool, // Will force a live delivery attempt. + pub message: Option, +} + +#[derive(Debug)] +pub struct PackOptions { + pub to_keys_per_recipient_limit: usize +} + + +impl Default for PackOptions { + fn default() -> Self { + PackOptions { + to_keys_per_recipient_limit: 100, + } + } +} + +pub(crate) trait MessageHandler { + /// Processes an incoming message, determines any additional actions to take + /// Returns a message to store and deliver if necessary + async fn process( + &self, + state: &SharedData, + session: &Session, + ) -> Result; + + /// Uses the incoming unpack metadata to determine best way to pack the message + async fn pack( + &self, + to_did: &str, + mediator_did: &str, + metadata: &UnpackMetadata, + secrets_resolver: &S, + did_resolver: &DIDCacheClient, + pack_options: &PackOptions, + ) -> Result<(String, PackEncryptedMetadata), MediatorError> + where + S: SecretsResolver; +} + +impl MessageHandler for Message { + async fn process( + &self, + state: &SharedData, + session: &Session, + ) -> Result { + let msg_type = self.type_.as_str().parse::()?; + + // Check if message expired + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + if let Some(expires) = self.expires_time { + if expires <= now { + return Err(MediatorError::MessageExpired( + "-1".into(), + expires.to_string(), + now.to_string(), + )); + } + } + + msg_type.process(self, state, session).await + } + + async fn pack( + &self, + to_did: &str, + mediator_did: &str, + metadata: &UnpackMetadata, + secrets_resolver: &S, + did_resolver: &DIDCacheClient, + pack_options: &PackOptions, + ) -> Result<(String, PackEncryptedMetadata), MediatorError> + where + S: SecretsResolver, + { + if metadata.encrypted { + // Respond with an encrypted message + let a = match self + .pack_encrypted( + to_did, + self.from.as_deref(), + Some(mediator_did), + did_resolver, + secrets_resolver, + &PackEncryptedOptions{ + to_kids_limit: pack_options.to_keys_per_recipient_limit, + ..PackEncryptedOptions::default() + }, + ) + .await + { + Ok(msg) => msg, + Err(e) => { + return Err(MediatorError::MessagePackError("-1".into(), e.to_string())); + } + }; + + Ok(a) + } else { + Err(MediatorError::MessagePackError( + "-1".into(), + "PACK METHOD NOT IMPLEMENTED".into(), + )) + } + } +} diff --git a/affinidi-messaging-mediator/src/messages/protocols/message_pickup.rs b/affinidi-messaging-mediator/src/messages/protocols/message_pickup.rs new file mode 100644 index 0000000..135e689 --- /dev/null +++ b/affinidi-messaging-mediator/src/messages/protocols/message_pickup.rs @@ -0,0 +1,586 @@ +use affinidi_messaging_didcomm::{Attachment, Message}; +use affinidi_messaging_sdk::{ + messages::fetch::FetchOptions, + protocols::message_pickup::{ + MessagePickupDeliveryRequest, MessagePickupLiveDelivery, MessagePickupStatusReply, + MessagePickupStatusRequest, + }, +}; +use base64::prelude::*; +use itertools::Itertools; +use redis::{from_redis_value, Value}; +use serde_json::json; +use sha256::digest; +use std::time::SystemTime; +use tracing::{debug, error, event, info, span, warn, Instrument, Level}; +use uuid::Uuid; + +use crate::{ + common::errors::{MediatorError, Session}, + messages::ProcessMessageResponse, + tasks::websocket_streaming::{StreamingUpdate, StreamingUpdateState}, + SharedData, +}; + +/// Process a Status Request message and generates a response +pub(crate) async fn status_request( + msg: &Message, + state: &SharedData, + session: &Session, +) -> Result { + let _span = span!(tracing::Level::DEBUG, "status_request",); + async move { + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + + if let Some(expires) = msg.expires_time { + if expires <= now { + debug!( + "Message expired at ({}) now({}) seconds_ago({})", + expires, + now, + now - expires + ); + return Err(MediatorError::MessageExpired( + session.session_id.clone(), + expires.to_string(), + now.to_string(), + )); + } + } + + // Ensure to: exists and is valid + let to = if let Some(to) = &msg.to { + if let Some(first) = to.first() { + first.to_owned() + } else { + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + "Message missing valid 'to' field, expect at least one address in array.".into(), + )); + } + } else { + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + "Message missing 'to' field".into(), + )); + }; + debug!("To: {}", to); + + // Must be addressed to ATM + if to != state.config.mediator_did { + debug!( + "to: ({}) doesn't match ATM DID ({})", + to, state.config.mediator_did + ); + return Err(MediatorError::RequestDataError(session.session_id.clone(), + format!("message to: ({}) didn't match ATM DID ({}). Status Request messages must be addressed directly to ATM!", + to, state.config.mediator_did))); + } + + // Message can not be anonymous + if msg.from.is_none() { + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + "Message Pickup 3.0 Status-Request can not be anonymous as it is needed from to validate permissions".into(), + )); + }; + + // Check for extra-header `return_route` + if let Some(header) = msg.extra_headers.get("return_route") { + if header.as_str() != Some("all") { + debug!( + "return_route: extra-header exists. Expected (all) but received ({})", + header + ); + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + format!( + "return_route: extra-header exists. Expected (all) but received ({})", + header + ), + )); + } + } else { + debug!("return_route: extra-header does not exist!"); + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + "return_route: extra-header does not exist! It should!".into(), + )); + } + + // Get or create the thread id for the response + let thid = if let Some(thid) = &msg.thid { + thid.to_owned() + } else { + msg.id.clone() + }; + debug!("thid = ({})", thid); + + // Pull recipient_did from message body + let recipient_did: String = if let Ok(body) = + serde_json::from_value::(msg.body.to_owned()) + { + if let Some(recipient_did) = body.recipient_did { + if recipient_did != session.did { + debug!( + "recipient_did: ({}) doesn't match session.did!", + recipient_did + ); + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + format!( + "recipient_did: ({}) doesn't match session.did!", + recipient_did + ), + )); + } else { + digest(recipient_did) + } + } else { + session.did_hash.clone() + } + } else { + session.did_hash.clone() + }; + debug!("Body: recipient_did: {}", recipient_did); + + info!( + "MessagePickup Status-Request received from: ({}) recipient_did_hash({:?})", + msg.from.clone().unwrap_or_else(|| "ANONYMOUS".to_string()), + recipient_did + ); + + generate_status_reply(state, session, &recipient_did, &thid, false, None).await +}.instrument(_span).await +} + +/// Creates the reply to a valid StatusRequest message +/// force_live_delivery: If true, will force the message to be live streamed even if live streaming is disabled. +/// Required due to the protocol specification to send a status update on live_streaming changes +/// override_live_delivery: If Some(bool), will override the live delivery status of the recipient +/// Because we are async handling streaming updates, we send the response before the streaming task has updated the status +async fn generate_status_reply( + state: &SharedData, + session: &Session, + did_hash: &str, + thid: &str, + force_live_delivery: bool, + override_live_delivery: Option, +) -> Result { + let _span = span!(tracing::Level::DEBUG, "generate_status_reply",); + + async move { + let mut conn = state.database.get_async_connection().await?; + + let response: Vec = deadpool_redis::redis::cmd("FCALL") + .arg("get_status_reply") + .arg(1) + .arg(did_hash) + .query_async(&mut conn) + .await + .map_err(|err| { + event!( + Level::ERROR, + "get_status_reply({}) failed. Reason: {}", + did_hash, + err + ); + MediatorError::DatabaseError( + session.session_id.clone(), + format!("get_status_reply({}) failed. Reason: {}", did_hash, err), + ) + })?; + + let mut status = MessagePickupStatusReply { + recipient_did: session.did.clone(), + ..Default::default() + }; + + for (k, v) in response.into_iter().tuples() { + match from_redis_value::(&k).unwrap_or("".into()).as_str() { + "newest_received" => { + if let Ok(v) = from_redis_value::(&v) { + let a: Vec<&str> = v.split('-').collect(); + if a.len() != 2 { + continue; + } + status.newest_received_time = if let Ok(t) = a[0].parse::() { + Some(t / 1000) + } else { + None + }; + } + } + "oldest_received" => { + if let Ok(v) = from_redis_value::(&v) { + let a: Vec<&str> = v.split('-').collect(); + if a.len() != 2 { + continue; + } + status.oldest_received_time = if let Ok(t) = a[0].parse::() { + Some(t / 1000) + } else { + None + }; + } + } + "message_count" => { + if let Ok(v) = from_redis_value::(&v) { + status.message_count = v; + } + } + "queue_count" => continue, + "live_delivery" => { + if let Ok(v) = from_redis_value::(&v) { + status.live_delivery = v; + } + } + "total_bytes" => { + if let Ok(v) = from_redis_value::(&v) { + status.total_bytes = v; + } + } + "recipient_did" => continue, + _ => { + warn!("Unknown key: ({:?}) with value: ({:?})", k, v); + } + } + } + + if let Some(live_delivery) = override_live_delivery { + status.live_delivery = live_delivery; + } + + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + + if let Some(t) = status.oldest_received_time { + status.longest_waited_seconds = Some(now - t); + } + + // Build the message + let status_msg = Message::build( + Uuid::new_v4().into(), + "https://didcomm.org/messagepickup/3.0/status".to_owned(), + json!(status), + ) + .thid(thid.to_owned()) + .to(session.did.clone()) + .from(state.config.mediator_did.clone()) + .created_time(now) + .expires_time(now + 300) + .finalize(); + + debug!("status message =\n{:?}", status_msg); + + Ok(ProcessMessageResponse { + store_message: false, + force_live_delivery, + message: Some(status_msg), + }) + } + .instrument(_span) + .await +} + +/// Allows for turning on and off live delivery within ATM for a client +pub(crate) async fn toggle_live_delivery( + msg: &Message, + state: &SharedData, + session: &Session, +) -> Result { + let _span = span!(tracing::Level::DEBUG, "toggle_live_delivery",); + async move { + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + + if let Some(expires) = msg.expires_time { + if expires <= now { + debug!( + "Message expired at ({}) now({}) seconds_ago({})", + expires, + now, + now - expires + ); + return Err(MediatorError::MessageExpired( + session.session_id.clone(), + expires.to_string(), + now.to_string(), + )); + } + } + + // Ensure to: exists and is valid + let to = if let Some(to) = &msg.to { + if let Some(first) = to.first() { + first.to_owned() + } else { + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + "Message missing valid 'to' field, expect at least one address in array.".into(), + )); + } + } else { + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + "Message missing 'to' field".into(), + )); + }; + debug!("To: {}", to); + + // Must be addressed to ATM + if to != state.config.mediator_did { + debug!( + "to: ({}) doesn't match ATM DID ({})", + to, state.config.mediator_did + ); + return Err(MediatorError::RequestDataError(session.session_id.clone(), + format!("message to: ({}) didn't match ATM DID ({}). Live Delivery messages must be addressed directly to ATM!", + to, state.config.mediator_did))); + } + + // Message can not be anonymous + if msg.from.is_none() { + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + "Message Pickup 3.0 Status-Request can not be anonymous as it is needed from to validate permissions".into(), + )); + }; + + // Check for extra-header `return_route` + if let Some(header) = msg.extra_headers.get("return_route") { + if header.as_str() != Some("all") { + debug!( + "return_route: extra-header exists. Expected (all) but received ({})", + header + ); + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + format!( + "return_route: extra-header exists. Expected (all) but received ({})", + header + ), + )); + } + } else { + debug!("return_route: extra-header does not exist!"); + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + "return_route: extra-header does not exist! It should!".into(), + )); + } + + // Get or create the thread id for the response + let thid = if let Some(thid) = &msg.thid { + thid.to_owned() + } else { + msg.id.clone() + }; + debug!("thid = ({})", thid); + + // Pull live_delivery from message body + let live_delivery: bool = if let Ok(body) = + serde_json::from_value::(msg.body.to_owned()) + { + body.live_delivery + } else { + error!("Failed to parse live_delivery from message body"); + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + "Failed to parse live_delivery from message body".into(), + )); + }; + debug!("Body: live_delivery: {}", live_delivery); + + info!( + "MessagePickup live_delivery received from: ({}) live_delivery?({})", + msg.from.clone().unwrap_or_else(|| "ANONYMOUS".to_string()), + live_delivery + ); + + // Take action to change live delivery status + if live_delivery { + // Enable Live delivery + if let Some(stream_task) = &state.streaming_task { + stream_task.channel.send(StreamingUpdate { did_hash: session.did_hash.clone(), state: StreamingUpdateState::Start}).await.map_err(|e| { + error!("Error sending start message to streaming task: {:?}", e); + MediatorError::InternalError(session.session_id.clone(), "Error sending start message to streaming task".into()) + })?; + } + + } else { + // Disable live delivery + if let Some(stream_task) = &state.streaming_task { + stream_task.channel.send(StreamingUpdate { did_hash: session.did_hash.clone(), state: StreamingUpdateState::Stop}).await.map_err(|e| { + error!("Error sending stop message to streaming task: {:?}", e); + MediatorError::InternalError(session.session_id.clone(), "Error sending stop message to streaming task".into()) + })?; + } + } + + generate_status_reply(state, session, &session.did_hash, &thid, true, Some(live_delivery)).await + }.instrument(_span).await +} + +/// Process a Delivery Request message and generates a response +pub(crate) async fn delivery_request( + msg: &Message, + state: &SharedData, + session: &Session, +) -> Result { + let _span = span!(tracing::Level::DEBUG, "delivery_request",); + async move { + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + + if let Some(expires) = msg.expires_time { + if expires <= now { + debug!( + "Message expired at ({}) now({}) seconds_ago({})", + expires, + now, + now - expires + ); + return Err(MediatorError::MessageExpired( + session.session_id.clone(), + expires.to_string(), + now.to_string(), + )); + } + } + + // Ensure to: exists and is valid + let to = if let Some(to) = &msg.to { + if let Some(first) = to.first() { + first.to_owned() + } else { + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + "Message missing valid 'to' field, expect at least one address in array.".into(), + )); + } + } else { + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + "Message missing 'to' field".into(), + )); + }; + debug!("To: {}", to); + + // Must be addressed to ATM + if to != state.config.mediator_did { + debug!( + "to: ({}) doesn't match ATM DID ({})", + to, state.config.mediator_did + ); + return Err(MediatorError::RequestDataError(session.session_id.clone(), + format!("message to: ({}) didn't match ATM DID ({}). Status Request messages must be addressed directly to ATM!", + to, state.config.mediator_did))); + } + + // Message can not be anonymous + if msg.from.is_none() { + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + "Message Pickup 3.0 Status-Request can not be anonymous as it is needed from to validate permissions".into(), + )); + }; + + // Check for extra-header `return_route` + if let Some(header) = msg.extra_headers.get("return_route") { + if header.as_str() != Some("all") { + debug!( + "return_route: extra-header exists. Expected (all) but received ({})", + header + ); + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + format!( + "return_route: extra-header exists. Expected (all) but received ({})", + header + ), + )); + } + } else { + debug!("return_route: extra-header does not exist!"); + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + "return_route: extra-header does not exist! It should!".into(), + )); + } + + // Get or create the thread id for the response + let thid = if let Some(thid) = &msg.thid { + thid.to_owned() + } else { + msg.id.clone() + }; + debug!("thid = ({})", thid); + + // Pull recipient_did and limit from message body + let (recipient_did, limit): (String, usize) = match + serde_json::from_value::(msg.body.to_owned()) + { + Ok(body) => + (body.recipient_did, body.limit), + Err(e) => + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + format!("delivery-request body isn't valid. Reason: {}", e), + )) + }; + + let recipient_did_hash = digest(recipient_did.clone()); + + debug!("Body: recipient_did: {}, limit: {}", recipient_did_hash, limit); + + info!( + "MessagePickup Delivery-Request received from: ({}) recipient_did({}) limit({})", + msg.from.clone().unwrap_or_else(|| "ANONYMOUS".to_string()), + recipient_did_hash, limit + ); + + // All the parsing is done, lets attempt to retrieve messages + let messages = state.database.fetch_messages(&session.session_id, &recipient_did_hash, &FetchOptions { limit, ..Default::default() }).await?; + debug!("msgs fetched: {}", messages.success.len()); + + if !messages.success.is_empty() { + let response_msg = Message::build(Uuid::new_v4().into(), "https://didcomm.org/messagepickup/3.0/delivery".to_string(), json!({"recipient_did": recipient_did})).thid(thid.clone()); + + let mut attachments: Vec = Vec::new(); + + for element in messages.success { + if let Some(msg) = element.msg { + let attachment = Attachment::base64(BASE64_URL_SAFE_NO_PAD.encode(msg)).id(element.msg_id); + + attachments.push(attachment.finalize()) + } + } + + let response_msg = response_msg.attachments(attachments).to(session.did.clone()) + .from(state.config.mediator_did.clone()) + .created_time(now) + .expires_time(now + 300) + .finalize(); + + debug!("delivery message =\n{:?}", response_msg); + + Ok(ProcessMessageResponse { + store_message: false, + force_live_delivery: false, + message: Some(response_msg), + }) + } else { + generate_status_reply(state, session, &recipient_did_hash, &thid, false, None).await + } +}.instrument(_span).await +} diff --git a/affinidi-messaging-mediator/src/messages/protocols/mod.rs b/affinidi-messaging-mediator/src/messages/protocols/mod.rs new file mode 100644 index 0000000..0d44487 --- /dev/null +++ b/affinidi-messaging-mediator/src/messages/protocols/mod.rs @@ -0,0 +1,3 @@ +pub mod message_pickup; +pub mod ping; +pub mod routing; diff --git a/affinidi-messaging-mediator/src/messages/protocols/ping.rs b/affinidi-messaging-mediator/src/messages/protocols/ping.rs new file mode 100644 index 0000000..edca2f7 --- /dev/null +++ b/affinidi-messaging-mediator/src/messages/protocols/ping.rs @@ -0,0 +1,124 @@ +use std::time::SystemTime; + +use affinidi_messaging_didcomm::Message; +use serde::Deserialize; +use serde_json::json; +use tracing::{debug, info, span}; +use uuid::Uuid; + +use crate::{ + common::errors::{MediatorError, Session}, + messages::ProcessMessageResponse, +}; + +// Reads the body of an incoming trust-ping and whether to generate a return ping message +#[derive(Deserialize)] +struct Ping { + response_requested: bool, // Defaults to true +} + +impl Default for Ping { + fn default() -> Self { + Self { + response_requested: true, + } + } +} + +/// Process a trust-ping message and generates a response if needed +pub(crate) fn process( + msg: &Message, + session: &Session, +) -> Result { + let _span = span!( + tracing::Level::DEBUG, + "trust_ping", + session_id = session.session_id.as_str() + ) + .entered(); + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + + if let Some(expires) = msg.expires_time { + if expires <= now { + debug!( + "Message expired at ({}) now({}) seconds_ago({})", + expires, + now, + now - expires + ); + return Err(MediatorError::MessageExpired( + session.session_id.clone(), + expires.to_string(), + now.to_string(), + )); + } + } + + let to = if let Some(to) = &msg.to { + if let Some(first) = to.first() { + first.to_owned() + } else { + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + "Message missing valid 'to' field, expect at least one address in array.".into(), + )); + } + } else { + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + "Message missing 'to' field".into(), + )); + }; + debug!("To: {}", to); + + let respond: bool = if let Ok(body) = serde_json::from_value::(msg.body.to_owned()) { + body.response_requested + } else { + true + }; + debug!("Response requested: {}", respond); + + info!( + "ping received from: ({}) Respond?({})", + msg.from.clone().unwrap_or_else(|| "ANONYMOUS".to_string()), + respond + ); + + if respond { + let from = if let Some(from) = &msg.from { + from.to_owned() + } else { + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + "Anonymous Trust-Ping is asking for a response, this is an invalid request!".into(), + )); + }; + + // Build the message (we swap from and to) + let response_msg = Message::build( + Uuid::new_v4().into(), + "https://didcomm.org/trust-ping/2.0/ping".to_owned(), + json!({}), + ) + .thid(msg.id.clone()) + .to(from) + .from(to) + .created_time(now) + .expires_time(now + 300) + .finalize(); + + debug!("response_msg: {:?}", response_msg); + + Ok(ProcessMessageResponse { + store_message: true, + force_live_delivery: false, + message: Some(response_msg), + }) + } else { + debug!("No response requested"); + Ok(ProcessMessageResponse::default()) + } +} diff --git a/affinidi-messaging-mediator/src/messages/protocols/routing.rs b/affinidi-messaging-mediator/src/messages/protocols/routing.rs new file mode 100644 index 0000000..61f3e1f --- /dev/null +++ b/affinidi-messaging-mediator/src/messages/protocols/routing.rs @@ -0,0 +1,132 @@ +use std::time::SystemTime; + +use affinidi_messaging_didcomm::Message; +use serde::Deserialize; +use serde_json::json; +use tracing::{debug, info, span}; +use uuid::Uuid; + +use crate::{ + common::errors::{MediatorError, Session}, + messages::ProcessMessageResponse, +}; + +const FORWARD_REQUEST_TTL_IN_SEC: u64 = 3_600; + +// Reads the body of an incoming trust-ping and whether to generate a return ping message +#[derive(Deserialize)] +struct ForwardRequest { + next: Option, // Defaults to true +} + +impl Default for ForwardRequest { + fn default() -> Self { + Self { next: None } + } +} + +/// Process a trust-ping message and generates a response if needed +pub(crate) fn process( + msg: &Message, + session: &Session, +) -> Result { + let _span = span!( + tracing::Level::DEBUG, + "routing", + session_id = session.session_id.as_str() + ) + .entered(); + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + + if let Some(expires) = msg.expires_time { + if expires <= now { + debug!( + "Message expired at ({}) now({}) seconds_ago({})", + expires, + now, + now - expires + ); + return Err(MediatorError::MessageExpired( + session.session_id.clone(), + expires.to_string(), + now.to_string(), + )); + } + } + + let to = if let Some(to) = &msg.to { + if let Some(first) = to.first() { + first.to_owned() + } else { + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + "Message missing valid 'to' field, expect at least one address in array.".into(), + )); + } + } else { + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + "Message missing 'to' field".into(), + )); + }; + + let next: String = + if let Ok(body) = serde_json::from_value::(msg.body.to_owned()) { + match body.next { + Some(next_str) => next_str, + None => { + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + "Message missing valid 'next' field".into(), + )) + } + } + } else { + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + "Message is not a valid ForwardRequest".into(), + )); + }; + debug!("Forward to: {}", next); + + info!( + "Forward request received from: ({}) Next is ({})", + msg.from.clone().unwrap_or_else(|| "ANONYMOUS".to_string()), + next + ); + + let attachments = if let Some(attachments) = &msg.attachments { + attachments.to_owned() + } else { + return Err(MediatorError::RequestDataError( + session.session_id.clone(), + "Nothing to forward, attachments are not defined!".into(), + )); + }; + + // let messages_to_forward: Vec = vec![]; + // for attachment in attachments { + // attachment.data + + // debug!("response_msg: {:?}", response_msg); + // } + // Build the message (we swap from and to) + + let response_msg = Message::build(Uuid::new_v4().into(), msg.type_.to_owned(), json!({})) + .thid(msg.id.clone()) // should we reuse msg.thid? + .to(next) // to next buddy in chain + .from(to) // from mediator + .created_time(now) + .expires_time(now + FORWARD_REQUEST_TTL_IN_SEC) + .attachments(attachments) // the value should be taken from config, how much we are ok to store the messages in the db + .finalize(); + + Ok(ProcessMessageResponse { + store_message: true, + force_live_delivery: false, + message: Some(response_msg), + }) +} diff --git a/affinidi-messaging-mediator/src/resolvers/affinidi_secrets.rs b/affinidi-messaging-mediator/src/resolvers/affinidi_secrets.rs new file mode 100644 index 0000000..d8c409e --- /dev/null +++ b/affinidi-messaging-mediator/src/resolvers/affinidi_secrets.rs @@ -0,0 +1,45 @@ +use async_trait::async_trait; + +use affinidi_messaging_didcomm::{ + error::Result, + secrets::{Secret, SecretsResolver}, +}; + +#[derive(Clone, Debug)] +pub struct AffinidiSecrets { + known_secrets: Vec, +} + +impl AffinidiSecrets { + pub fn new(known_secrets: Vec) -> Self { + AffinidiSecrets { known_secrets } + } + + pub fn len(&self) -> usize { + self.known_secrets.len() + } + + pub fn is_empty(&self) -> bool { + self.known_secrets.is_empty() + } +} + +#[cfg_attr(feature = "uniffi", async_trait)] +#[cfg_attr(not(feature = "uniffi"), async_trait)] +impl SecretsResolver for AffinidiSecrets { + async fn get_secret(&self, secret_id: &str) -> Result> { + Ok(self + .known_secrets + .iter() + .find(|s| s.id == secret_id) + .cloned()) + } + + async fn find_secrets(&self, secret_ids: &[String]) -> Result> { + Ok(secret_ids + .iter() + .filter(|sid| self.known_secrets.iter().any(|s| s.id == sid.to_string())) + .cloned() + .collect()) + } +} diff --git a/affinidi-messaging-mediator/src/resolvers/mod.rs b/affinidi-messaging-mediator/src/resolvers/mod.rs new file mode 100644 index 0000000..7e94b4c --- /dev/null +++ b/affinidi-messaging-mediator/src/resolvers/mod.rs @@ -0,0 +1 @@ +pub mod affinidi_secrets; diff --git a/affinidi-messaging-mediator/src/tasks/mod.rs b/affinidi-messaging-mediator/src/tasks/mod.rs new file mode 100644 index 0000000..c17d2ca --- /dev/null +++ b/affinidi-messaging-mediator/src/tasks/mod.rs @@ -0,0 +1,3 @@ +/// Any parallel task (thread) that needs to be spawned should be defined here. +pub mod statistics; +pub mod websocket_streaming; diff --git a/affinidi-messaging-mediator/src/tasks/statistics.rs b/affinidi-messaging-mediator/src/tasks/statistics.rs new file mode 100644 index 0000000..cf18d65 --- /dev/null +++ b/affinidi-messaging-mediator/src/tasks/statistics.rs @@ -0,0 +1,32 @@ +use std::time::Duration; + +use tracing::{debug, info, span, Instrument, Level}; + +use crate::{ + common::errors::MediatorError, + database::{stats::MetadataStats, DatabaseHandler}, +}; + +/// Periodically logs statistics about the database. +/// Is spawned as a task from main(). +pub async fn statistics(database: DatabaseHandler) -> Result<(), MediatorError> { + let _span = span!(Level::INFO, "statistics"); + + async move { + debug!("Starting statistics thread..."); + let mut interval = tokio::time::interval(Duration::from_secs(60)); + + let mut previous_stats = MetadataStats::default(); + + loop { + interval.tick().await; + let stats = database.get_db_metadata().await?; + info!("Statistics: {}", stats); + info!("Delta: {}", stats.delta(&previous_stats)); + + previous_stats = stats; + } + } + .instrument(_span) + .await +} diff --git a/affinidi-messaging-mediator/src/tasks/websocket_streaming.rs b/affinidi-messaging-mediator/src/tasks/websocket_streaming.rs new file mode 100644 index 0000000..99cddf8 --- /dev/null +++ b/affinidi-messaging-mediator/src/tasks/websocket_streaming.rs @@ -0,0 +1,235 @@ +use std::{collections::HashMap, time::Duration}; + +use redis::aio::PubSub; +use serde::{Deserialize, Serialize}; +use tokio::{select, sync::mpsc, task::JoinHandle, time::sleep}; +use tokio_stream::StreamExt; +use tracing::{debug, error, info, span, warn, Instrument, Level}; + +use crate::{common::errors::MediatorError, database::DatabaseHandler}; + +// Useful links on redis pub/sub in Rust: +// https://github.com/redis-rs/redis-rs/issues/509 + +/// Used when updating the streaming state. +/// Register: Creates the hash map entry for the DID hash and TX Channel +/// Start: Start streaming messages to clients. +/// Stop: Stop streaming messages to clients. +/// Deregister: Remove the hash map entry for the DID hash. +pub enum StreamingUpdateState { + Register(mpsc::Sender), + Start, + Stop, + Deregister, +} + +/// Used to update the streaming state. +/// did_hash: The DID hash to update the state for. +/// state: The state to update to. +pub struct StreamingUpdate { + pub did_hash: String, + pub state: StreamingUpdateState, +} + +#[derive(Clone)] +pub struct StreamingTask { + pub channel: mpsc::Sender, +} + +/// This is the format of the JSON message that is sent to the pub/sub channel. +/// did_hash : SHA256 hash of the DID +/// message : The message to send to the client +/// force_delivery : If true, the message will be sent to the client even if they are not active. +/// +/// NOTE: The force_delivery is required as when changing live_delivery status, standard says to send a status message +#[derive(Serialize, Deserialize, Debug)] +pub struct PubSubRecord { + pub did_hash: String, + pub message: String, + pub force_delivery: bool, +} + +impl StreamingTask { + /// Creates the streaming task handler + pub async fn new( + database: DatabaseHandler, + mediator_uuid: &str, + ) -> Result<(Self, JoinHandle<()>), MediatorError> { + let _span = span!(Level::INFO, "StreamingTask::new"); + + async move { + // Create the inter-task channel - allows up to 10 queued messages + let (tx, mut rx) = mpsc::channel(10); + let task = StreamingTask { + channel: tx.clone(), + }; + + // Start the streaming task + // With it's own clone of required data + let handle = { + let _mediator_uuid = mediator_uuid.to_string(); + let _task = task.clone(); + tokio::spawn(async move { + _task + .ws_streaming_task(database, &mut rx, _mediator_uuid) + .await + .expect("Error starting websocket_streaming thread"); + }) + }; + + Ok((task, handle)) + } + .instrument(_span) + .await + } + + /// Starts a pubsub connection to Redis and subscribes to a channel. + /// Useful way to restart a terminated connection from within a loop. + async fn _start_pubsub( + &self, + database: DatabaseHandler, + uuid: &str, + ) -> Result { + let _span = span!(Level::INFO, "_start_pubsub"); + + async move { + let mut pubsub = database.get_pubsub_connection().await?; + + let channel = format!("CHANNEL:{}", uuid); + pubsub.subscribe(channel.clone()).await.map_err(|err| { + error!("Error subscribing to channel: {}", err); + MediatorError::DatabaseError( + "NA".into(), + format!("Error subscribing to channel: {}", err), + ) + })?; + + info!("Subscribed to channel: {}", channel); + Ok(pubsub) + } + .instrument(_span) + .await + } + + /// Streams messages to subscribed clients over websocket. + /// Is spawned as a task + async fn ws_streaming_task( + &self, + database: DatabaseHandler, + channel: &mut mpsc::Receiver, + uuid: String, + ) -> Result<(), MediatorError> { + let _span = span!(Level::INFO, "ws_streaming_task", uuid = uuid); + + async move { + debug!("Starting..."); + + // Clean up any existing sessions left over from previous runs + database.streaming_clean_start(&uuid).await?; + + // Create a hashmap to store the clients and if they are active (true = yes) + let mut clients: HashMap, bool)> = HashMap::new(); + + // Start streaming messages to clients + let mut pubsub = self._start_pubsub(database.clone(), &uuid).await?; + loop { + let mut stream = pubsub.on_message(); + + // Listen for an update on either the redis pubsub stream, or the command channel + // stream: redis pubsub of incoming messages destined for a client + // channel: command channel to start/stop streaming for a client + select! { + value = stream.next() => { + if let Some(msg) = value { + if let Ok(payload) = msg.get_payload::() { + let payload: PubSubRecord = serde_json::from_str(&payload).unwrap(); + + // Find the MPSC transmit channel for the associated DID hash + if let Some((tx, active)) = clients.get(&payload.did_hash) { + if payload.force_delivery || *active { + // Send the message to the client + if let Err(err) = tx.send(payload.message.clone()).await { + error!("Error sending message to client ({}): {}", payload.did_hash, err); + } else { + info!("Sent message to client ({})", payload.did_hash); + } + } else { + warn!("pub/sub msg received for did_hash({}) but it is not active", payload.did_hash); + if let Err(err) = database.streaming_stop_live(&payload.did_hash, &uuid).await { + error!("Error stopping streaming for client ({}): {}", payload.did_hash, err); + } + } + } else { + warn!("pub/sub msg received for did_hash({}) but it doesn't exist in clients HashMap", payload.did_hash); + } + + } else { + error!("Error getting payload from message"); + continue; + }; + } else { + // Redis connection dropped, need to retry + error!("Redis connection dropped, retrying..."); + drop(stream); + + pubsub = loop { + sleep(Duration::from_secs(1)).await; + match self._start_pubsub(database.clone(), &uuid).await { + Ok(pubsub) => break pubsub, + Err(err) => { + error!("Error starting pubsub: {}", err); + continue; + } + } + } + } + } + value = channel.recv() => { + if let Some(value) = value { + match value.state { + StreamingUpdateState::Register(client_tx) => { + info!("Registered streaming for DID: ({}) registered_clients({})", value.did_hash, clients.len()+1); + clients.insert(value.did_hash.clone(), (client_tx, false)); + + if let Err(err) = database.streaming_register_client(&value.did_hash, &uuid).await { + error!("Error starting streaming to client ({}) streaming: {}",value.did_hash, err); + } + }, + StreamingUpdateState::Start => { + if let Some((_, active)) = clients.get_mut(&value.did_hash) { + info!("Starting streaming for DID: ({})", value.did_hash); + *active = true; + }; + + if let Err(err) = database.streaming_start_live(&value.did_hash, &uuid).await { + error!("Error starting streaming to client ({}) streaming: {}",value.did_hash, err); + } + }, + StreamingUpdateState::Stop => { + // Set active to false + if let Some((_, active)) = clients.get_mut(&value.did_hash) { + info!("Stopping streaming for DID: ({})", value.did_hash); + *active = false; + }; + + if let Err(err) = database.streaming_stop_live(&value.did_hash, &uuid).await { + error!("Error stopping streaming for client ({}): {}",value.did_hash, err); + } + }, + StreamingUpdateState::Deregister => { + info!("Deregistering streaming for DID: ({}) registered_clients({})", value.did_hash, clients.len()-1); + if let Err(err) = database.streaming_deregister_client(&value.did_hash, &uuid).await { + error!("Error stopping streaming for client ({}): {}",value.did_hash, err); + } + clients.remove(value.did_hash.as_str()); + } + } + } + } + } + } + } + .instrument(_span) + .await + } +} diff --git a/affinidi-messaging-processor/.gitignore b/affinidi-messaging-processor/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/affinidi-messaging-processor/.gitignore @@ -0,0 +1 @@ +/target diff --git a/affinidi-messaging-processor/CHANGELOG.md b/affinidi-messaging-processor/CHANGELOG.md new file mode 100644 index 0000000..99d5635 --- /dev/null +++ b/affinidi-messaging-processor/CHANGELOG.md @@ -0,0 +1,27 @@ +# Affinidi Trust Network - Affinidi Trusted Messaging - Processor Service + +## Changelog history + +### 24th June 2024 + +* Adding websocket clean_start_streaming() function. This removes any existing session details. + +### 20th June 2024 + +* Changed delete_message() so it updates GLOBAL metric DELETED_BYTES + +### 16th June 2024 + +* Fixed a bug in store_message where time format was truncating on millisecond values < 100 + +### 9th June 2024 + +* Fixed a bug in store_message redis Lua function where stream ID's were in seconds and not milliseconds. + +### 7th June 2024 + +* Added delete_message Lua function + +### 6th June 2024 + +* Added store_message Lua function diff --git a/affinidi-messaging-processor/Cargo.toml b/affinidi-messaging-processor/Cargo.toml new file mode 100644 index 0000000..242b9e7 --- /dev/null +++ b/affinidi-messaging-processor/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "affinidi-messaging-processing" +version = "0.1.1" +description = "Affinidi Trusted Messaging Processor" +edition.workspace = true +authors.workspace = true +readme.workspace = true +homepage.workspace = true +keywords.workspace = true +publish.workspace = true +license.workspace = true + +[dependencies] diff --git a/affinidi-messaging-processor/README.md b/affinidi-messaging-processor/README.md new file mode 100644 index 0000000..c728e80 --- /dev/null +++ b/affinidi-messaging-processor/README.md @@ -0,0 +1,4 @@ +# Affinidi Messaging Processor + +A service that handles messages addressed directly to the ATM itself, these are protocol level messages that can potentially +modify the state of the ATM (I.e. modify allow/deny lists, resource limits, discovery etc) diff --git a/affinidi-messaging-processor/redis-functions/atm-functions.lua b/affinidi-messaging-processor/redis-functions/atm-functions.lua new file mode 100644 index 0000000..41010cc --- /dev/null +++ b/affinidi-messaging-processor/redis-functions/atm-functions.lua @@ -0,0 +1,283 @@ +#!lua name=atm + +-- store_message +-- keys = message_hash +-- args = [1] message +-- [2] message length in bytes +-- [3] to_did +-- [4] to_did_hash +-- [5] from_did +-- [6] from_did_hash +local function store_message(keys, args) + -- Do we have the correct number of arguments? + -- from_did_hash can be optional!!! + if #args < 5 then + return redis.error_reply('store_message: not enough arguments') + elseif #args > 6 then + return redis.error_reply('store_message: too many arguments') + end + + -- set response type to Version 3 + redis.setresp(3) + + -- Get current time on server + local time = redis.call('TIME') + redis.log(redis.LOG_WARNING, 'TIME: '..time[1]..':'..time[2]) + local time = string.format("%d%03.0f", time[1], time[2]/1000) + redis.log(redis.LOG_WARNING, 'TIME-2: '..time) + local bytes = tonumber(args[2]) + if bytes == nil then + return redis.error_reply('store_message: invalid bytes') + end + + -- Store message + redis.call('SET', 'MSG:'..keys[1], args[1]) + + -- Set Global Metrics + redis.call('HINCRBY', 'GLOBAL', 'RECEIVED_BYTES', bytes) + redis.call('HINCRBY', 'GLOBAL', 'RECEIVED_COUNT', 1) + + -- Create Message Expiry Record + redis.call('RPUSH', 'MSG_EXPIRY', keys[1]..':'..time) + + -- Update the receiver records + redis.call('HINCRBY', 'DID:'..args[4], 'RECEIVE_QUEUE_BYTES', bytes) + redis.call('HINCRBY', 'DID:'..args[4], 'RECEIVE_QUEUE_COUNT', 1) + -- If changing the fields in the future, update the fetch_messages function + local RQ = redis.call('XADD', 'RECEIVE_Q:'..args[4], time..'-*', 'MSG_ID', keys[1], 'BYTES', bytes, 'FROM', args[5]) + + -- Update the sender records + local SQ = nil + if table.getn(args) == 6 then + -- Update the sender records + redis.call('HINCRBY', 'DID:'..args[6], 'SEND_QUEUE_BYTES', bytes) + redis.call('HINCRBY', 'DID:'..args[6], 'SEND_QUEUE_COUNT', 1) + SQ = redis.call('XADD', 'SEND_Q:'..args[6], time..'-*', 'MSG_ID', keys[1], 'BYTES', bytes, 'TO', args[3]) + end + + -- Update message MetaData + redis.call('HMSET', 'MSG:META:'..keys[1], 'BYTES', bytes, 'TO', args[4], 'TIMESTAMP', time, 'RECEIVE_ID', RQ) + if SQ ~= nil then + redis.call('HMSET', 'MSG:META:'..keys[1], 'FROM', args[6], 'SEND_ID', SQ) + end + + return redis.status_reply('OK') +end + +-- delete_message +-- keys = message_hash +-- args = [1] did_hash +local function delete_message(keys, args) + -- Correct number of keys? + if #keys ~= 1 then + return redis.error_reply('delete_message: only accepts one key') + end + + -- Correct number of args? + if #args ~= 1 then + return redis.error_reply('delete_message: Requires DID hash argument') + end + + -- set response type to Version 3 + redis.setresp(3) + + -- Retrieve message metadata + local meta = redis.call('HGETALL', 'MSG:META:'..keys[1]) + if meta.map == nil then + return redis.error_reply('Couldn\'t retrieve metadata') + end + + -- Check that the requesting DID has some form of ownership of this message + if meta.map.TO ~= args[1] and meta.map.FROM ~= args[1] then + return redis.error_reply('Requesting DID does not have ownership of this message') + end + + local bytes = meta.map.BYTES + if bytes == nil then + redis.log(redis.LOG_WARNING, 'message ('..keys[1]..') metadata did not contain BYTES field.') + return redis.error_reply('message ('..keys[1]..') metadata did not contain BYTES field.') + end + + -- Delete message + redis.call('DEL', 'MSG:'..keys[1]) + + -- Set Global Metrics + redis.call('HINCRBY', 'GLOBAL', 'DELETED_BYTES', bytes) + redis.call('HINCRBY', 'GLOBAL', 'DELETED_COUNT', 1) + + -- Remove the receiver records + redis.call('HINCRBY', 'DID:'..meta.map.TO, 'RECEIVE_QUEUE_BYTES', -bytes) + redis.call('HINCRBY', 'DID:'..meta.map.TO, 'RECEIVE_QUEUE_COUNT', -1) + redis.call('XDEL', 'RECEIVE_Q:'..meta.map.TO, meta.map.RECEIVE_ID) + + -- Remove the sender records + local SQ = nil + if meta.map.SEND_ID ~= nil then + -- Remove the sender records + redis.call('HINCRBY', 'DID:'..meta.map.FROM, 'SEND_QUEUE_BYTES', -bytes) + redis.call('HINCRBY', 'DID:'..meta.map.FROM, 'SEND_QUEUE_COUNT', -1) + SQ = redis.call('XDEL', 'SEND_Q:'..meta.map.FROM, meta.map.SEND_ID) + end + + -- Remove the message metadata + redis.call('DEL', 'MSG:META:'..keys[1]) + + return redis.status_reply('OK') +end + +-- fetch_messages +-- keys = did_hash +-- args = [1] start_id +-- [2] limit +local function fetch_messages(keys, args) + -- Do we have the correct number of arguments? + if #args ~= 2 then + return redis.error_reply('fetch_messages: wrong arguments') + end + + -- set response type to Version 3 + redis.setresp(3) + + -- Prepend an exclusive start_id if it exists + local start_id = '-' + if args[1] ~= "-" then + start_id = '('..args[1] + end + + -- Get list of messages from stream + local list = redis.call('XRANGE', 'RECEIVE_Q:'..keys[1], start_id, '+', 'COUNT', args[2]) + + local fetched_messages = {} + -- unpack the XRANGE list + for x, element in ipairs(list) do + -- element[1] = stream_id + -- element[2] = array of Stream Fields + for i, sub_element in ipairs(element) do + if i == 1 then + -- This is the stream ID + fetched_messages[x] = {'STREAM_ID', sub_element} + else + -- [1] = MSG_ID + -- [2] = message_id + -- [3] = BYTES + -- [4] = bytes + -- [5] = FROM + -- [6] = from_did + table.insert(fetched_messages[x], sub_element[1]) + table.insert(fetched_messages[x], sub_element[2]) + table.insert(fetched_messages[x], 'FROM_DID') + table.insert(fetched_messages[x], sub_element[6]) + + -- fetch the message + table.insert(fetched_messages[x], 'MSG') + local msg = redis.call('GET', 'MSG:'..sub_element[2]) + table.insert(fetched_messages[x], msg) + + -- fetch the message metadata + local meta = redis.call('HGETALL', 'MSG:META:'..sub_element[2]) + for k, v in pairs(meta.map) do + table.insert(fetched_messages[x], 'META_'..k) + table.insert(fetched_messages[x], v) + end + end + end + end -- end of XRANGE list + + return fetched_messages +end + +-- clean_start_streaming +-- keys = uuid +-- returns number of sessions cleaned up +local function clean_start_streaming(keys, args) + -- Correct number of keys? + if #keys ~= 1 then + return redis.error_reply('clean_start_streaming: only accepts one key') + end + + -- Correct number of args? + if #args ~= 0 then + return redis.error_reply('clean_start_streaming: No arguments required') + end + + -- set response type to Version 3 + redis.setresp(3) + + -- Prepend an exclusive start_id if it exists + local key = 'STREAMING_SESSIONS:'..keys[1] + + -- Clean up sessions + local counter = 0 + while (true) do + local response = redis.call('SPOP', key, 1) + + -- No more items in the set + if next(response.set) == nil then + break + end + + local session = nil + for k,v in pairs(response.set) do + session = k + counter = counter+1 + end + + + -- remove from global session list + redis.call('HDEL', 'GLOBAL_STREAMING', session) + end + + return counter +end + +-- get_status_reply +-- keys = did_hash that we are getting status for +-- returns Message Pickup 3.0 Status details +local function get_status_reply(keys, args) + -- Correct number of keys? + if #keys ~= 1 then + return redis.error_reply('get_status_reply: only accepts one key (recipient_did_hash)') + end + + -- Correct number of args? + if #args ~= 0 then + return redis.error_reply('get_status_reply: No arguments required') + end + + -- set response type to Version 3 + redis.setresp(3) + + local response = {} + response.map = {} + response.map.recipient_did = keys[1] + + -- Set the message count and total bytes + local r = redis.call('HMGET', 'DID:'..keys[1], 'RECEIVE_QUEUE_COUNT', 'RECEIVE_QUEUE_BYTES') + response.map.message_count = tonumber(r[1]) + response.map.total_bytes = tonumber(r[2]) + + -- Get the oldest and newest message information + local r = redis.pcall('XINFO', 'STREAM', 'RECEIVE_Q:'..keys[1]) + if r['err'] == nil then + response.map.oldest_received = r.map['first-entry'][1] + response.map.newest_received = r.map['last-entry'][1] + response.map.queue_count = r.map['length'] + end + + -- Get live streaming status + local r = redis.call("HEXISTS", "GLOBAL_STREAMING", keys[1]) + if r == 0 then + response.map.live_delivery = false + else + response.map.live_delivery = true + end + + return response +end + + +redis.register_function('store_message', store_message) +redis.register_function('delete_message', delete_message) +redis.register_function('fetch_messages', fetch_messages) +redis.register_function('clean_start_streaming', clean_start_streaming) +redis.register_function('get_status_reply', get_status_reply) \ No newline at end of file diff --git a/affinidi-messaging-processor/src/main.rs b/affinidi-messaging-processor/src/main.rs new file mode 100644 index 0000000..e7a11a9 --- /dev/null +++ b/affinidi-messaging-processor/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/affinidi-messaging-sdk/.gitignore b/affinidi-messaging-sdk/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/affinidi-messaging-sdk/.gitignore @@ -0,0 +1 @@ +/target diff --git a/affinidi-messaging-sdk/CHANGELOG.md b/affinidi-messaging-sdk/CHANGELOG.md new file mode 100644 index 0000000..9639933 --- /dev/null +++ b/affinidi-messaging-sdk/CHANGELOG.md @@ -0,0 +1,76 @@ +# Affinidi Trust Network - Affinidi Trusted Messaging - Software Development Kit + +## Changelog history + +### 3rd Sept 2024 (v0.1.3) + +* Added the ability create a affinidi-did-resolver-cache outside of the affinidi-messaging-sdk and share it the ATM SDK + * Allows for sharing of DID cache inside and outside of ATM for efficiency. + +### 2nd Sept 2024 (v0.1.2) + +* Added DIDComm pack_* methods so you can pack/unpack directly using the ATM-SDK +* pack_encrypted() - encrypted and optionally signed message +* pack_signed() - plaintext and signed message +* pack_plaintext() - plaintext! not encrypted, not signed +* unpack() - unpacks any format DIDComm message +* NOTE: You are responsible for sending this message to the mediator as a separate step. +* HTTP(S) : Call atm.send.didcomm_message(&message, true|false) +* WebSocket : Call atm.ws-send_didcomm_message(&message, &message_id) + +### 20th July 2024 + +* Message Pickup Message-Delivery and Messages Received implemented + +### 18th July 2024 + +* Message Pickup live-streaming implemented +* Ability to call next() and get() on websocket +* WebSocket cache implemented + +### 29th June 2024 + +* Added ability to pre-load secrets when creating a new ATM client +* This fixes an issue where WebSocket auth requires secrets to be loaded before trying to start +* Implemented Message-Pickup protocol - Status-Request + +### 15th June 2024 + +* get_websocket() function added to ATM +* ATMWebSocket is now a child struct that allows for websocket specific calls + +### 13th June 2024 + +* Adding Secured WebSockets support +* Refactored how SSL certificates are loaded by the SDK + * HTTP(S) requests use a different SSL/TLS Certificate model than WebSockets + * Internal change within SDK only. No change to client side code + +### 12th June 2024 + +* Allow option to disable SSL via config + +### 9th June 2024 + +* Minor refactoring of unpack() +* Refactored send_didcomm_message() so it now returns more relevant information such as recipients, message_ids and errors +* refactored send_ping() so it returns new structure +* Changed example ping to now just do a full trust-ping to ATM, includes timing data +* Added new example `demo` to show all API calls + +### 8th June 2024 + +* Refactored namespace for several SDK structs +* implemented get_messages() +* implemented unpack() + +### 7th June 2024 + +* improved delete_message return type to DeleteMessageResponse Struct, with returns for success and errors +* Changed send_ping() so that instead of anonymous field, it is now signed field. Easier to logically think of. +* Added README Documentation +* Modified DeleteMessageResponse from successful to success + +### 6th June 2024 + +* Modified list_messages() to send the requested did as part of the request. diff --git a/affinidi-messaging-sdk/Cargo.toml b/affinidi-messaging-sdk/Cargo.toml new file mode 100644 index 0000000..a3a69ab --- /dev/null +++ b/affinidi-messaging-sdk/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "affinidi-messaging-sdk" +version = "0.1.3" +description = "Affinidi Trusted Messaging SDK" +edition.workspace = true +authors.workspace = true +readme.workspace = true +homepage.workspace = true +keywords.workspace = true +publish.workspace = true +license.workspace = true + +[dependencies] +async-trait = "0.1" +affinidi-messaging-didcomm = { version = "0.5" } +affinidi-did-resolver-cache-sdk.workspace = true +base64 = "0.22" +futures-util = "0.3" +http = "1" +jsonwebtoken = "9.3" +reqwest = { version = "0.12", features = ["rustls-tls-manual-roots", "json"] } +rustls = { version = "0.23", default-features = false, features = [ + "aws_lc_rs", + "tls12", +] } +rustls-native-certs = "0.8" +rustls-pemfile = "2" +serde = { version = "1.0", features = ["derive", "rc"] } +serde_json = "1.0" +sha256 = "1.5" +ssi = "0.8" +thiserror = "1.0" +tokio = { version = "1.37.0", features = ["full"] } +tokio-stream = "0.1" +tokio-tungstenite = { version = "0.23.0", features = [ + "rustls-tls-native-roots", +] } +tracing = { version = "0.1", features = [ + "max_level_debug", + "release_max_level_info", +] } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +url = "2.5" +uuid = { version = "1.8.0", features = ["v4", "fast-rng"] } + +[dev-dependencies] +clap.workspace = true diff --git a/affinidi-messaging-sdk/README.md b/affinidi-messaging-sdk/README.md new file mode 100644 index 0000000..636c713 --- /dev/null +++ b/affinidi-messaging-sdk/README.md @@ -0,0 +1,269 @@ +# Affinidi Messaging SDK + +a Software Development Kit (SDK) to simplify the implementation of Affinidi Messaging into your application. + +## Debug logging + +To enable logging at DEBUG level just for atm_sdk crate, + +```bash +export RUST_LOG=none,affinidi_messaging_sdk=debug +``` + +## Examples + +Use `` from [affinidi-messaging-mediator - Running affinidi-messaging-mediator service](../affinidi-messaging-mediator#running-affinidi-messaging-mediator-service). + +```bash +# enable logging for examples, +export RUST_LOG=none,affinidi_messaging_sdk=debug,ping=debug,demo=debug + +# no "did://" prefix for examples +export MEDIATOR_DID= + +cargo run --example message_pickup -- --mediator-did $MEDIATOR_DID + +# send a trust ping +cargo run --example ping -- --mediator-did $MEDIATOR_DID + +# send message to another DID +cargo run --example send_message_to_me -- --mediator-did $MEDIATOR_DID + +cargo run --example send_message -- --mediator-did $MEDIATOR_DID +``` + +## WebSocket and HTTPS support + +By default, the ATM SDK will use both a WebSocket and HTTPS REST based API calls. Authentication in particular is handled via REST so that JWT access +tokens can be retrieved, these are then used when upgrading to a WebSocket Connection. + +WebSocket is used for the following: + +1. Sending DIDComm messages, a response containing the message_id is sent via the same websocket. +2. Receiving a stream of inbound messages to the DID used in this SDK + +To start using WebSockets, no action is required. A WebSocket is created when `ATM::new()` is called. + +You can disable WebSocket through the `ConfigBuilder::with_websocket_disabled()` function. + +A custom Websocket URL can be provided via `ConfigBuilder::with_atm_websocket_api()` + +**_NOTE:_** Default action is to take the `ConfigBuilder::with_atm_api()` and convert to a valid WebSocket address + + E.g. `https://atm.affinidi.com/atm/v1` would become `wss://atm.affinidi.com/atm/v1/ws` + +While you can disable the WebSocket, you can also start and close the WebSocket manually via: + +```rust + +let my_did = "did:example:alice"; +let bob_did = "did:example:bob"; +let atm_did = "did:example:atm"; + +let config = Config::builder() + .with_ssl_certificates(&mut vec![ + "../affinidi-messaging-mediator/conf/keys/client.chain".into() + ]) + .with_my_did(my_did) + .with_atm_did(atm_did) + .with_websocket_disabled() + .build()? +let mut atm = ATM::new(config, vec![Box::new(DIDPeer)]).await?; + +// Send a ping via REST +atm.send_ping(bob_did, true, true).await?; + +// Start the Websocket +atm.start_websocket().await?; + +// Send a ping via WebSocket +atm.send_ping(bob_did, true, true).await?; + +// Close the websocket (optional) +atm.close_websocket().await?; + +``` + +## WebSocket API Calls + +### Send DIDComm Message via WebSocket + +- Sends a DIDComm packed message to ATM +- Creating the DIDComm message is not handled by this call + +```rust +async fn ws_send_didcomm_message(msg: &str) -> Result, ATMError> +// Sends a DIDComm packed message to ATM +// Can specify the return type via + +// Example: + +let msg = atm.pack_encrypted(&msg, to, from, sign_by); +ws_send_didcomm_message(&msg).await?; +``` + +Response from `ws_send_didcomm_message()` is: + +- Success : Result->Ok `SendMessageResponse` struct +- Error : Result->Err with ATMError object describing the error + +## REST API Calls + +### DIDComm Trust-Ping + +- It can be useful to send a Trust-Ping to a target to test connectivity and routing. +- If you have control of the target, then a response may not be needed. Otherwise you can request a response. + +**_NOTE:_** If you a sending an anonymous message, then there is no ability to get a response. + +```rust +async fn send_ping(to_did: &str, signed: bool, response:bool) -> Result<(), ATMError> +// Sends a ping to an address via ATM +// - to_did : DID to ping +// - signed : if true, then message is signed with your DID, if false then sent anonymous +// - response : Request a PONG response from the target? If signed == false, then this will also be reset to false + +// Example: Send a signed trust-ping requesting a response from the target +send_ping("did:example:target#123", true, true).await?; +``` + +Response from `send_ping()` is: + +- Success : Result->Ok with `SendMessageResponse` struct +- Error : Result->Err with ATMError object describing the error + +### List Messages + +- ATM supports two message folders: + - Inbox (incoming messages sent to you) + - Outbox (outbound messages you have sent that have not been delivered to the next node) +- `enum Folder` holds the folder types + +```rust +aync fn list_messages(did: &str, folder: Folder) -> Result +// Retrieves a list of messages for the specified DID (you must own this DID) +// - did : DID that we want to retrieve messages from (must have authenticated as this DID) +// - folder : Folder enum of either Inbox or Outbox + +// Example: +list_messages("did:example:target#123", Folder::Inbox).await?; +``` + +Response from `list_messages()` is: + +- Success : Result->Ok `MessageList` struct containing a list of messages including Metadata +- Error : Result->Err with ATMError object describing the error + +### Delete Messages + +- Deletes one or more messages from ATM, you need to know the message_ids first! +- Receiver can delete any message that is waiting to be delivered to them +- Sender can delete any message that is waiting still to be delivered for them + +```rust +async fn delete_messages(messages: &DeleteMessageRequest) -> Result +// Deletes a set of messages contained in an array +// - messages : Vec of message hash ID's + +// Example: +delete_messages(&vec!["message_hash1", "message_hash2", ...]).await?; +``` + +Response from `delete_messages()` is: + +- Success : Result->Ok `DeleteMessageResponse` struct +- Error : Result->Err with ATMError object describing the error + +Working with `DeleteMessageResponse`: + +- success `Vec` : List of message_id's that were successfully deleted +- errors `Vec<(String, String)>` : List of message_id's and error information + +### Send DIDComm Message + +- Sends a DIDComm packed message to ATM +- Creating the DIDComm message is not handled by this call + +```rust +async fn send_didcomm_message(msg: &str) -> Result, ATMError> +// Sends a DIDComm packed message to ATM +// Can specify the return type via + +// Example: + +let msg = atm.pack_encrypted(&msg, to, from, sign_by); +send_didcomm_message(&msg).await?; +``` + +Response from `send_didcomm_message()` is: + +- Success : Result->Ok `SendMessageResponse` struct +- Error : Result->Err with ATMError object describing the error + +### Get DIDComm Message + +- Retrieves one or more messages from ATM +- You must know the message_id(s) in advance. See `list_messages()` +- You can specify if you want to delete along with the get_message() request +- Call `unpack(&message)` next to unpack the message you have received + +```rust +async fn get_messages(messages: &GetMessagesRequest) -> Result +// Gets one or more messages from ATM + +// Example: Get one message, and don't delete it +get_messages(&vec!["message_id".into()], false).await?; +``` + +Response from `get_messages()` is: + +- Success : Result->Ok `GetMessagesResponse` struct +- Error : Result->Err with ATMError object describing the error + +Working with `GetMessageResponse`: + +- success `Vec` : List of message_id's that were successfully deleted +- get_errors `Vec<(String, String)>` : List of failed gets on message_ids + error message +- delete_errors `vec<(String, String)>` : List of failed deletes on message_ids + error message + +### Pack a DIDComm message + +- There are three methods to pack (create) a DIDComm message + - pack_encrypted(message, from, sign_by) + - encrypts message, if `from` is None, then anonymous encrypt, if `sign_by` is specified then will cryptographically sign the message + - pack_signed(message, sign_by) + - signs a plaintext message using the `sign_by` key + - pack_plaintext(message) + - creates an unencrypted, no-signature plaintext DIDComm message + +```rust +// Example: Send an encrypted and signed message +let message = Message::build()...; +let (message, meta_data) = atm.pack_encrypted(&message, Some(from_did), Some(from_did)).await?; +``` + +Response from `pack_encrypted()` is: + +- Success : Result->Ok (`PackEncryptedMetadata | PackSignedMetadata | String`) +- Error : Result->Err with ATMError object describing the error + +### Unpack a DIDComm message + +- A wrapper for DIDComm Message::unpack() that simplifies the setup of resolvers etc already done for ATM +- Takes a plain `String` and returns a DIDComm `Message` and `UnpackMetadata` objects + +```rust +async fn unpack(message: &str) -> Result<(Message, UnpackMetadata), ATMError> +// Unpacks any DIDComm message into a Message and UnpackMetadata response + +// Example: +let didcomm_message = atm.get_messages(&vec!["msg_id".to_string()], true).await?; +let (message, meta_data) = atm.unpack(&didcomm_message).await?; + +println!("Message body = {}", message.body); +``` + +Response from `unpack()` is: + +- Success : Result->Ok (`Message`, `UnpackMetadata`) +- Error : Result->Err with ATMError object describing the error diff --git a/affinidi-messaging-sdk/certs/mediator-key.pem b/affinidi-messaging-sdk/certs/mediator-key.pem new file mode 100644 index 0000000..61ae256 --- /dev/null +++ b/affinidi-messaging-sdk/certs/mediator-key.pem @@ -0,0 +1,20 @@ +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- \ No newline at end of file diff --git a/affinidi-messaging-sdk/examples/message_pickup.rs b/affinidi-messaging-sdk/examples/message_pickup.rs new file mode 100644 index 0000000..e07c2a2 --- /dev/null +++ b/affinidi-messaging-sdk/examples/message_pickup.rs @@ -0,0 +1,153 @@ +use std::time::Duration; + +use affinidi_messaging_sdk::{ + config::Config, conversions::secret_from_str, errors::ATMError, protocols::Protocols, ATM, +}; +use clap::Parser; +use serde_json::json; +use tracing::{error, info}; +use tracing_subscriber::filter; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// network address if running in network mode (ws://127.0.0.1:8080/did/v1/ws) + #[arg(short, long)] + network_address: Option, + #[arg(short, long)] + mediator_did: String, +} + +#[tokio::main] +async fn main() -> Result<(), ATMError> { + let args = Args::parse(); + + // construct a subscriber that prints formatted traces to stdout + let subscriber = tracing_subscriber::fmt() + // Use a more compact, abbreviated log format + .with_env_filter(filter::EnvFilter::from_default_env()) + .finish(); + // use that subscriber to process traces emitted after this point + tracing::subscriber::set_global_default(subscriber).expect("Logging failed, exiting..."); + + let my_did = "did:peer:2.Vz6MkgWJfVmPELozq6aCycK3CpxHN8Upphn3WSuQkWY6iqsjF.EzQ3shfb7vwQaTJqFkt8nRfo7Nu98tmeYpdDfWgrqQitDaqXRz"; + // Signing and verification key + let v1 = json!({ + "crv": "Ed25519", + "d": "LLWCf83n8VsUYq31zlZRe0NNMCcn1N4Dh85dGpIqSFw", + "kty": "OKP", + "x": "Hn8T4ZjjT0oJ6rjhqox8AykwC3GDFsJF6KkaYZExwQo" + }); + + // Encryption key + let e1 = json!({ + "crv": "secp256k1", + "d": "oi-dXG4EqfNODFPjv2vkieoLdbQZH9k6dwPDV8HDoms", + "kty": "EC", + "x": "DhfaXbhwo0KkOiyA5V1K1RZx6Ikr86h_lX5GOwxjmjE", + "y": "PpYqybOwMsm64vftt-7gBCQPIUbglMmyy_6rloSSAPk" + }); + + let atm_did = &args.mediator_did; + + let mut config = Config::builder().with_my_did(my_did).with_atm_did(atm_did); + + if let Some(address) = &args.network_address { + println!("Running in network mode with address: {}", address); + config = config + .with_ssl_certificates(&mut vec!["./certs/mediator-key.pem".into()]) + .with_atm_api(address); + } else { + println!("Running in local mode."); + config = config.with_ssl_certificates(&mut vec![ + "../affinidi-messaging-mediator/conf/keys/client.chain".into(), + ]); + } + + // Create a new ATM Client + let mut atm = ATM::new(config.build()?).await?; + + // Add our secrets to ATM Client - stays local. + atm.add_secret(secret_from_str(&format!("{}#key-1", my_did), &v1)); + atm.add_secret(secret_from_str(&format!("{}#key-2", my_did), &e1)); + + let protocols = Protocols::new(); + + /*let message = r#" + {"protected":"eyJ0eXAiOiJhcHBsaWNhdGlvbi9kaWRjb21tLWVuY3J5cHRlZCtqc29uIiwiYWxnIjoiRUNESC0xUFUrQTI1NktXIiwiZW5jIjoiQTI1NkNCQy1IUzUxMiIsInNraWQiOiJkaWQ6cGVlcjoyLlZ6Nk1rZ1dKZlZtUEVMb3pxNmFDeWNLM0NweEhOOFVwcGhuM1dTdVFrV1k2aXFzakYuRXpRM3NoZmI3dndRYVRKcUZrdDhuUmZvN051OTh0bWVZcGREZldncnFRaXREYXFYUnoja2V5LTIiLCJhcHUiOiJaR2xrT25CbFpYSTZNaTVXZWpaTmEyZFhTbVpXYlZCRlRHOTZjVFpoUTNsalN6TkRjSGhJVGpoVmNIQm9iak5YVTNWUmExZFpObWx4YzJwR0xrVjZVVE56YUdaaU4zWjNVV0ZVU25GR2EzUTRibEptYnpkT2RUazRkRzFsV1hCa1JHWlhaM0p4VVdsMFJHRnhXRko2STJ0bGVTMHkiLCJhcHYiOiI1a05fc2kyd2toMFVQX0ZlNVNLejVLQkJPNkYzMVRneXJBNEZ5Z1hTeExZIiwiZXBrIjp7ImNydiI6InNlY3AyNTZrMSIsImt0eSI6IkVDIiwieCI6IlluMllkX1BVUUZTSVQtaWt1WlFSZ0kyVmgzdFFOVXVLdWtEdWZ1clpHMzQiLCJ5IjoidkpRV1Y3U09jUWNtMkNwUkI1S3ZDMU5YeUxrS1hmdndwdVdlQlZ2RkY0RSJ9fQ","recipients":[{"header":{"kid":"did:peer:2.Vz6MkiToqovww7vYtxm1xNM15u9JzqzUFZ1k7s7MazYJUyAxv.EzQ3shQLqRUza6AMJFbPuMdvFRFWm1wKviQRnQSC1fScovJN4s.SeyJ0IjoiRElEQ29tbU1lc3NhZ2luZyIsInMiOnsidXJpIjoiaHR0cHM6Ly8xMjcuMC4wLjE6NzAzNyIsImEiOlsiZGlkY29tbS92MiJdLCJyIjpbXX19#key-2"},"encrypted_key":"_tK6Tu4uzgmKqJATtvQetmPSYC1dGKnLfDUOc6XLr4LDJHT1ujrAPtm-p2pIUd1Pewrt35CR0hK3-rdK0ffGSLJrP1ntrJ1K"}],"iv":"HP4BfkLKN5X0WTbu-PMV5g","ciphertext":"hQ4TLBC-yjGFx6GZonfzSM5DB8W9rNYeIW0eAPf1vxAkj0zuXkwsLx9tEyTbS1Oc46DDaIuMzFYcbfAyrioUNOUp-43OHGxtVKXues4wTNvMFVbWEkOq27bm18APj59JohT5QAd8Dn-iZ1vPl82MZbwxpymPWS0MQqdGB_OoY1Clm6nE7lDM3plgxbvH0VnUWopCrHHHhKE00esX7Ax5fO8RJLmdSt8DMPrVII3qOASDJIBHc2f5af06o7PytErkoN_d8oOyl2QhoQm7HWe1gWFXZcxxeZaFRqdJDcVrUJ_h3X7S21qxn38TdZ7r5fgGu9as0GvzkHCZbn9OBCjpQ6cUQNdB6NRuUXhjgWFH1vXALAiRn3OLsgiUVJM__Nr3EWLOgOeBLDz76K6LuXfFZApYjErf6doLbjMQ_vlb93r9-HNKy5xNHKZJF0i7qPCkZJUOPxm7DUsYt_zmq3gNs9C6DAug8Dp5ayBPriNUxgWBXpwaJyFB4idyMDs3rNyvsswm_Fz1ImiHT4w0DvCDPvgBbGgEot0wqzKdT-cjNAHaXIF_O-t1pzanrjUFU16VVeuyZ0m0WWB0qRnZWpADpWy_9Vdpjvvh3kw8BO05QwvTzdR82ed1R4JX2HHTZR3GNjvHy-y_b4sXK0yKAFrrSULcwLfJY-8tTLrfIlSssT9LMmBLJKBlhdIiG0gGyw0_Zf_3PocQ8a-MRcqdMFguKPAX49PcMnaSRkQkAPPYTPWp0zv8p_HREY3h8WIYWYhrfD_wAeJSAlaISxH-7RJkdnweauKyZ5sIQ4HZ7oqfw3ARR5IaJxI9N1H7gKuphLe27oYhHqbPb5OJmqlCOFi2MwmFruW9hZMitG9nYkr1GNF4rxfoT7kOfsLoEAkxmNHrBWqSoGdsCOtYCAbjeWNf6mtPoCaTfELf0hgzkOD5UXfgNO6oKoB76Y6YsuUNPbULPkEu20TDz9b2MFGkfyXONHNgA46MErNaWFM24J4-yK2hJ7YaiQ7i4x1ZvUTayoF4Co-1a4IS3Tc4kx1HF11vPzWRLQGgXtAh0LQKaoRhspMPUA2MSwWBu9Pf8cKBrY99NuQ1qVZaz8JmzikIDXEuLh6wPmny1OlvyWC4BvqPVX4-IwQV5c7Y_UiXs4p9_OU0Ioq-eA2SaZQRt1IXAodIZJ0mOf3L5om_ngoazEU9cc3N2BdwlrQ9i0fGtRmxC6nk03HqeFV182ZM8x8NFOIzRx_5lYBuTZVgjzV6vVVYK2LI_1hmGFkm5pr77vLidbSxH_oi7INANDcV9PzLKY5NB-1rdXvSUSkw5kQ2W_50HTTzLtluDD91q4DtJZg9rPI_7YFJUxWhJHOX5-iuitZNu788Cng1iHvKV2ylqIJ8GHp3f_FrIgfe8KFeULiuqQWbDgjp71NWPnk_GyNKWcGrcGx03sRV2U1l3kptkZ_Nv8yjsZygfW2iYM9vm_X-9f2rufPlpL2BmnzrmXpOBGIkk2TSXCYY5TzO65igG3yAE7-hrg02vIwbi7msUCMtiHcY","tag":"Tr-FOMqIRytEJvm1KSLFhalhErUUKZbIOWvJgyAaV2M"} + "#; + let response = atm.unpack(message).await?; + info!("Unpacked: {:?}", response); + */ + // For this example, we are forcing REST API only by closing the websocket + // NOTE: We could have done this when we configured the ATM, but we are doing it here for demonstration purposes + //atm.close_websocket().await?; + + // Enable live streaming + protocols + .message_pickup + .toggle_live_delivery(&mut atm, true) + .await?; + + // Send a Message Pickup 3.0 Status Request + error!("Testing live_stream_next()!"); + let status = protocols + .message_pickup + .send_status_request(&mut atm, None, None, None) + .await?; + + info!("Status: {:?}", status); + + if let Some((message, _)) = protocols + .message_pickup + .live_stream_next(&mut atm, Duration::from_secs(2)) + .await? + { + info!("Message: {:?}", message); + } + + error!("Testing delivery-request()!"); + let response = protocols + .message_pickup + .send_delivery_request(&mut atm, None, None, None, None) + .await?; + + let mut delete_ids: Vec = Vec::new(); + + for (message, _) in response { + info!("Message: {}", message.id); + delete_ids.push(message.id.clone()); + } + + let response = protocols + .message_pickup + .send_messages_received(&mut atm, None, None, &delete_ids, None) + .await?; + + info!("Status: after send_messages_received() : {:?}", response); + + /* TODO: Need to complete this part of the protocol... + + tokio::time::sleep(Duration::from_secs(1)).await; + error!("Testing live_stream_get()!"); + + let response = protocols + .message_pickup + .send_status_request(&mut atm, None, None, None) + .await?; + + info!("Status: {:?}", response); + */ + + // Disable live streaming + protocols + .message_pickup + .toggle_live_delivery(&mut atm, false) + .await?; + + tokio::time::sleep(Duration::from_secs(1)).await; + + atm.abort_websocket_task().await?; + + Ok(()) +} diff --git a/affinidi-messaging-sdk/examples/ping.rs b/affinidi-messaging-sdk/examples/ping.rs new file mode 100644 index 0000000..a1467de --- /dev/null +++ b/affinidi-messaging-sdk/examples/ping.rs @@ -0,0 +1,180 @@ +use std::time::SystemTime; + +use affinidi_did_resolver_cache_sdk::{config::ClientConfigBuilder, DIDCacheClient}; +use affinidi_messaging_sdk::{ + config::Config, conversions::secret_from_str, errors::ATMError, messages::GetMessagesRequest, + protocols::Protocols, ATM, +}; +use clap::Parser; +use serde_json::json; +use tracing::info; +use tracing_subscriber::filter; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// network address if running in network mode (https://localhost:7037/atm/v1) + #[arg(short, long)] + network_address: Option, + #[arg(short, long)] + mediator_did: String, +} + +#[tokio::main] +async fn main() -> Result<(), ATMError> { + let args = Args::parse(); + // construct a subscriber that prints formatted traces to stdout + let subscriber = tracing_subscriber::fmt() + // Use a more compact, abbreviated log format + .with_env_filter(filter::EnvFilter::from_default_env()) + .finish(); + // use that subscriber to process traces emitted after this point + tracing::subscriber::set_global_default(subscriber).expect("Logging failed, exiting..."); + + let my_did = "did:peer:2.Vz6MkgWJfVmPELozq6aCycK3CpxHN8Upphn3WSuQkWY6iqsjF.EzQ3shfb7vwQaTJqFkt8nRfo7Nu98tmeYpdDfWgrqQitDaqXRz"; + // Signing and verification key + let v1 = json!({ + "crv": "Ed25519", + "d": "LLWCf83n8VsUYq31zlZRe0NNMCcn1N4Dh85dGpIqSFw", + "kty": "OKP", + "x": "Hn8T4ZjjT0oJ6rjhqox8AykwC3GDFsJF6KkaYZExwQo" + }); + + // Encryption key + let e1 = json!({ + "crv": "secp256k1", + "d": "oi-dXG4EqfNODFPjv2vkieoLdbQZH9k6dwPDV8HDoms", + "kty": "EC", + "x": "DhfaXbhwo0KkOiyA5V1K1RZx6Ikr86h_lX5GOwxjmjE", + "y": "PpYqybOwMsm64vftt-7gBCQPIUbglMmyy_6rloSSAPk" + }); + + let atm_did = &args.mediator_did; + + // ATM SDK supports an externally created DID Cache Resolver + let did_resolver = DIDCacheClient::new(ClientConfigBuilder::default().build()) + .await + .expect("Couldn't create DID Resolver!"); + + let mut config = Config::builder() + .with_my_did(my_did) + .with_atm_did(atm_did) + .with_websocket_disabled() + .with_external_did_resolver(&did_resolver); + + if let Some(address) = &args.network_address { + println!("Running in network mode with address: {}", address); + config = config + .with_ssl_certificates(&mut vec!["./certs/mediator-key.pem".into()]) + .with_atm_api(address); + } else { + config = config.with_ssl_certificates(&mut vec![ + "../affinidi-messaging-mediator/conf/keys/client.chain".into(), + ]); + println!("Running in local mode."); + } + + // Create a new ATM Client + let mut atm = ATM::new(config.build()?).await?; + let protocols = Protocols::new(); + + // Add our secrets to ATM Client - stays local. + atm.add_secret(secret_from_str(&format!("{}#key-1", my_did), &v1)); + atm.add_secret(secret_from_str(&format!("{}#key-2", my_did), &e1)); + + // Ready to send a trust-ping to ATM + let start = SystemTime::now(); + + let well_know_res = atm.well_known_did_json().await?; + println!("DID: {}", well_know_res); + + // You normally don't need to call authenticate() as it is called automatically + // We do this here so we can time the auth cycle + atm.authenticate().await?; + + let after_auth = SystemTime::now(); + + // Send a trust-ping message to ATM, will generate a PONG response + let response = protocols + .trust_ping + .send_ping(&mut atm, atm_did, true, true) + .await?; + let after_ping = SystemTime::now(); + + info!("PING sent: {}", response.message_hash); + + // Get the PONG message from ATM + let msgs = atm + .get_messages(&GetMessagesRequest { + delete: false, + message_ids: vec![response.response.unwrap()], + }) + .await?; + let after_get = SystemTime::now(); + + // Unpack the messages retrieved + for msg in msgs.success { + atm.unpack(&msg.msg.unwrap()).await?; + info!("PONG received: {}", msg.msg_id); + } + let after_unpack = SystemTime::now(); + + // Print out timing information + info!( + "Authenticating took {}ms :: total {}ms to complete", + after_auth.duration_since(start).unwrap().as_millis(), + after_auth.duration_since(start).unwrap().as_millis() + ); + info!( + "Sending Ping took {}ms :: total {}ms to complete", + after_ping.duration_since(after_auth).unwrap().as_millis(), + after_ping.duration_since(start).unwrap().as_millis() + ); + info!( + "Get response took {}ms :: total {}ms to complete", + after_get.duration_since(after_ping).unwrap().as_millis(), + after_get.duration_since(start).unwrap().as_millis() + ); + info!( + "Unpack took {}ms :: total {}ms to complete", + after_unpack.duration_since(after_get).unwrap().as_millis(), + after_unpack.duration_since(start).unwrap().as_millis() + ); + info!( + "Total trust-ping took {}ms to complete", + after_unpack.duration_since(start).unwrap().as_millis() + ); + + /* + // Send a WebSocket message + info!("Starting WebSocket test..."); + let start = SystemTime::now(); + atm.start_websocket_task().await?; + let after_websocket = SystemTime::now(); + + let response = protocols + .trust_ping + .send_ping(&mut atm, atm_did, true, true) + .await?; + let after_ping = SystemTime::now(); + + info!("PING sent: {}", response.message_hash); + + // Print out timing information + info!( + "Creating WebSocket took {}ms :: total {}ms to complete", + after_websocket.duration_since(start).unwrap().as_millis(), + after_websocket.duration_since(start).unwrap().as_millis() + ); + + info!( + "Sending Ping took {}ms :: total {}ms to complete", + after_ping + .duration_since(after_websocket) + .unwrap() + .as_millis(), + after_ping.duration_since(start).unwrap().as_millis() + );*/ + + Ok(()) +} diff --git a/affinidi-messaging-sdk/examples/send_message.rs b/affinidi-messaging-sdk/examples/send_message.rs new file mode 100644 index 0000000..dea2285 --- /dev/null +++ b/affinidi-messaging-sdk/examples/send_message.rs @@ -0,0 +1,140 @@ +use affinidi_messaging_didcomm::Message; +use affinidi_messaging_sdk::{ + config::Config, + conversions::secret_from_str, + errors::ATMError, + // messages::{fetch::FetchOptions, sending::InboundMessageResponse, FetchDeletePolicy}, + messages::sending::InboundMessageResponse, + ATM, +}; +use clap::Parser; +use serde_json::json; +use std::error::Error; +use std::time::SystemTime; +use tracing_subscriber::filter; +use uuid::Uuid; + +// use tracing::info; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// network address if running in network mode (https://localhost:7037/atm/v1) + #[arg(short, long)] + network_address: Option, + #[arg(short, long)] + mediator_did: String, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // ************************************************************** + // *** Initial setup + // ************************************************************** + let args = Args::parse(); + + // construct a subscriber that prints formatted traces to stdout + let subscriber = tracing_subscriber::fmt() + // Use a more compact, abbreviated log format + .with_env_filter(filter::EnvFilter::from_default_env()) + .finish(); + // use that subscriber to process traces emitted after this point + tracing::subscriber::set_global_default(subscriber).expect("Logging failed, exiting..."); + + let my_did = "did:peer:2.Vz6MkgWJfVmPELozq6aCycK3CpxHN8Upphn3WSuQkWY6iqsjF.EzQ3shfb7vwQaTJqFkt8nRfo7Nu98tmeYpdDfWgrqQitDaqXRz"; + // Signing and verification key + let v1 = json!({ + "crv": "Ed25519", + "d": "LLWCf83n8VsUYq31zlZRe0NNMCcn1N4Dh85dGpIqSFw", + "kty": "OKP", + "x": "Hn8T4ZjjT0oJ6rjhqox8AykwC3GDFsJF6KkaYZExwQo" + }); + + // Encryption key + let e1 = json!({ + "crv": "secp256k1", + "d": "oi-dXG4EqfNODFPjv2vkieoLdbQZH9k6dwPDV8HDoms", + "kty": "EC", + "x": "DhfaXbhwo0KkOiyA5V1K1RZx6Ikr86h_lX5GOwxjmjE", + "y": "PpYqybOwMsm64vftt-7gBCQPIUbglMmyy_6rloSSAPk" + }); + + let atm_did = &args.mediator_did; + let to_did = atm_did; + + // TODO: in the future we likely want to pull this from the DID itself + let mut config = Config::builder() + .with_my_did(my_did) + .with_atm_did(atm_did) + .with_websocket_disabled(); + + if let Some(address) = &args.network_address { + println!("Running in network mode with address: {}", address); + config = config + .with_ssl_certificates(&mut vec!["./certs/mediator-key.pem".into()]) + .with_atm_api(address); + } else { + println!("Running in local mode."); + config = config.with_ssl_certificates(&mut vec![ + "../affinidi-messaging-mediator/conf/keys/client.chain".into(), + ]); + } + + // Create a new ATM Client + let mut atm = ATM::new(config.build()?).await?; + + // Add our secrets to ATM Client - stays local. + atm.add_secret(secret_from_str(&format!("{}#key-1", my_did), &v1)); + atm.add_secret(secret_from_str(&format!("{}#key-2", my_did), &e1)); + + // You normally don't need to call authenticate() as it is called automatically + + atm.authenticate().await?; + + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + + let msg = Message::build( + Uuid::new_v4().into(), + "https://didcomm.org/trust-ping/2.0/ping".to_owned(), + json!("Hello World! Message sent via DIDComm"), + ) + .to(to_did.to_owned()) + .from(my_did.to_string()); + + let msg = msg.created_time(now).expires_time(now + 300).finalize(); + + println!("Message Build"); + + println!("Send message: {:?}", msg); + + // Pack the message + let (msg, _) = atm + .pack_encrypted(&msg, to_did, Some(my_did), Some(my_did)) + .await + .map_err(|e| ATMError::MsgSendError(format!("Error packing message: {}", e)))?; + + let response = atm + .send_didcomm_message::(&msg, true) + .await?; + + println!("Response message: {:?}", response); + + // // Get the PONG message from ATM + // let msgs = atm + // .fetch_messages(&FetchOptions { + // limit: 10, + // start_id: None, + // delete_policy: FetchDeletePolicy::DoNotDelete + // }) + // .await?; + + // for msg in msgs.success { + // atm.unpack(&msg.msg.unwrap()).await?; + // info!("Messages sent: {}", msg.msg_id); + // } + + Ok(()) +} diff --git a/affinidi-messaging-sdk/examples/send_message_to_me.rs b/affinidi-messaging-sdk/examples/send_message_to_me.rs new file mode 100644 index 0000000..5eb8a9e --- /dev/null +++ b/affinidi-messaging-sdk/examples/send_message_to_me.rs @@ -0,0 +1,154 @@ +use affinidi_messaging_didcomm::{Attachment, Message}; +use affinidi_messaging_sdk::{ + config::Config, + conversions::secret_from_str, + errors::ATMError, + // messages::{fetch::FetchOptions, sending::InboundMessageResponse, FetchDeletePolicy}, + messages::{fetch::FetchOptions, sending::InboundMessageResponse, FetchDeletePolicy}, + ATM, +}; +use clap::Parser; +use serde_json::json; +use std::error::Error; +use std::time::SystemTime; +use tracing_subscriber::filter; +use uuid::Uuid; + +// use tracing::info; + +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// network address if running in network mode (https://localhost:7037/atm/v1) + #[arg(short, long)] + network_address: Option, + #[arg(short, long)] + mediator_did: String, +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + // ************************************************************** + // *** Initial setup + // ************************************************************** + let args = Args::parse(); + + // construct a subscriber that prints formatted traces to stdout + let subscriber = tracing_subscriber::fmt() + // Use a more compact, abbreviated log format + .with_env_filter(filter::EnvFilter::from_default_env()) + .finish(); + // use that subscriber to process traces emitted after this point + tracing::subscriber::set_global_default(subscriber).expect("Logging failed, exiting..."); + + let my_did = "did:peer:2.Vz6MkgWJfVmPELozq6aCycK3CpxHN8Upphn3WSuQkWY6iqsjF.EzQ3shfb7vwQaTJqFkt8nRfo7Nu98tmeYpdDfWgrqQitDaqXRz"; + // Signing and verification key + let v1 = json!({ + "crv": "Ed25519", + "d": "LLWCf83n8VsUYq31zlZRe0NNMCcn1N4Dh85dGpIqSFw", + "kty": "OKP", + "x": "Hn8T4ZjjT0oJ6rjhqox8AykwC3GDFsJF6KkaYZExwQo" + }); + + // Encryption key + let e1 = json!({ + "crv": "secp256k1", + "d": "oi-dXG4EqfNODFPjv2vkieoLdbQZH9k6dwPDV8HDoms", + "kty": "EC", + "x": "DhfaXbhwo0KkOiyA5V1K1RZx6Ikr86h_lX5GOwxjmjE", + "y": "PpYqybOwMsm64vftt-7gBCQPIUbglMmyy_6rloSSAPk" + }); + + let atm_did = &args.mediator_did; + let to_did = my_did; + + // TODO: in the future we likely want to pull this from the DID itself + let mut config = Config::builder() + .with_my_did(my_did) + .with_atm_did(atm_did) + .with_websocket_disabled(); + + if let Some(address) = &args.network_address { + println!("Running in network mode with address: {}", address); + config = config + .with_ssl_certificates(&mut vec!["./certs/mediator-key.pem".into()]) + .with_atm_api(address); + } else { + println!("Running in local mode."); + config = config.with_ssl_certificates(&mut vec![ + "../affinidi-messaging-mediator/conf/keys/client.chain".into(), + ]); + } + + // Create a new ATM Client + let mut atm = ATM::new(config.build()?).await?; + + // Add our secrets to ATM Client - stays local. + atm.add_secret(secret_from_str(&format!("{}#key-1", my_did), &v1)); + atm.add_secret(secret_from_str(&format!("{}#key-2", my_did), &e1)); + + // You normally don't need to call authenticate() as it is called automatically + + atm.authenticate().await?; + + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + + let msg_to_bob = Message::build( + Uuid::new_v4().into(), + "https://didcomm.org/routing/2.0/forward".to_owned(), + json!({ "message": "Body of message, can be read only by mediator", "next": my_did }), + ) + .to(to_did.to_owned()) + .from(my_did.to_string()) + .attachment( + Attachment::json(json!({ "message": "plaintext attachment, mediator can read this" })) + .finalize(), + ) + .attachment( + Attachment::base64(String::from( + "ciphertext and iv which is encrypted by the recipient public key", + )) + .finalize(), + ); + + let msg_to_bob = msg_to_bob + .created_time(now) + .expires_time(now + 300) + .finalize(); + + println!("Message Build"); + + println!("Send message: {:?}", msg_to_bob); + + // Pack the message + let (msg, _) = atm + .pack_encrypted(&msg_to_bob, to_did, Some(my_did), Some(my_did)) + .await + .map_err(|e| ATMError::MsgSendError(format!("Error packing message: {}", e)))?; + + let response = atm + .send_didcomm_message::(&msg, true) + .await?; + + println!("---====---"); + println!("Response message: {:?}", response); + + // Get the messages from ATM + let msgs = atm + .fetch_messages(&FetchOptions { + limit: 10, + start_id: None, + delete_policy: FetchDeletePolicy::DoNotDelete, + }) + .await?; + + for msg in msgs.success { + let (received_msg_unpacked, _) = atm.unpack(&msg.msg.unwrap()).await?; + println!("Message received: {:?}", received_msg_unpacked); + } + + Ok(()) +} diff --git a/affinidi-messaging-sdk/src/authentication/mod.rs b/affinidi-messaging-sdk/src/authentication/mod.rs new file mode 100644 index 0000000..142154d --- /dev/null +++ b/affinidi-messaging-sdk/src/authentication/mod.rs @@ -0,0 +1,179 @@ +use std::time::SystemTime; + +use affinidi_messaging_didcomm::{Message, PackEncryptedOptions}; +use serde_json::json; +use tracing::{debug, span, Instrument, Level}; +use uuid::Uuid; + +use crate::{ + errors::ATMError, + messages::{AuthenticationChallenge, AuthorizationResponse, SuccessResponse}, + ATM, +}; + +impl<'c> ATM<'c> { + /// Authenticate the SDK against Affinidi Trusted Messaging + pub async fn authenticate(&mut self) -> Result { + if self.authenticated { + // Already authenticated + if let Some(tokens) = &self.jwt_tokens { + return Ok(tokens.clone()); + } else { + return Err(ATMError::AuthenticationError( + "Authenticated but no tokens found".to_owned(), + )); + } + } + + let _span = span!(Level::DEBUG, "authenticate",); + async move { + debug!("Retrieving authentication challenge..."); + // Step 1. Get the challenge + let res = self + .client + .post(format!("{}/authenticate/challenge", self.config.atm_api)) + .header("Content-Type", "application/json") + .body(format!("{{\"did\": \"{}\"}}", self.config.my_did).to_string()) + .send() + .await + .map_err(|e| { + ATMError::TransportError(format!( + "retrieving authentication challenge failed. Reason: {:?}", + e + )) + })?; + let status = res.status(); + debug!("Challenge response: status({})", status); + + let body = res + .text() + .await + .map_err(|e| ATMError::TransportError(format!("Couldn't get body: {:?}", e)))?; + + if !status.is_success() { + debug!("Failed to get authentication challenge. Body: {:?}", body); + return Err(ATMError::AuthenticationError( + "Failed to get authentication challenge".to_owned(), + )); + } + let body = serde_json::from_str::>(&body) + .ok() + .unwrap(); + + debug!("Challenge received:\n{:#?}", body); + + // Step 2. Sign the challenge + let challenge = if let Some(challenge) = &body.data { + challenge + } else { + return Err(ATMError::AuthenticationError( + "No challenge received from ATM".to_owned(), + )); + }; + + let auth_response = + self._create_auth_challenge_response(&self.config.atm_did, challenge); + debug!("Auth response message:\n{:#?}", auth_response); + + let (auth_msg, _) = auth_response + .pack_encrypted( + &self.config.atm_did, + Some(&self.config.my_did), + Some(&self.config.my_did), + &self.did_resolver, + &self.secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + .map_err(|e| { + ATMError::MsgSendError(format!( + "Couldn't pack authentication response message: {:?}", + e + )) + })?; + + debug!("Successfully packed auth message"); + + let res = self + .client + .post(format!("{}/authenticate", self.config.atm_api)) + .header("Content-Type", "application/json") + .body(auth_msg) + .send() + .await + .map_err(|e| { + ATMError::TransportError(format!( + "Could not post authentication response: {:?}", + e + )) + })?; + + let status = res.status(); + debug!("Authentication response: status({})", status); + + let body = res + .text() + .await + .map_err(|e| ATMError::TransportError(format!("Couldn't get body: {:?}", e)))?; + + if !status.is_success() { + debug!("Failed to get authentication response. Body: {:?}", body); + return Err(ATMError::AuthenticationError( + "Failed to get authentication response".to_owned(), + )); + } + let body = serde_json::from_str::>(&body) + .map_err(|e| { + ATMError::AuthenticationError(format!( + "Couldn't deserialize AuthorizationResponse: {}", + e + )) + })?; + + if let Some(tokens) = &body.data { + debug!("Tokens received:\n{:#?}", tokens); + self.jwt_tokens = Some(tokens.clone()); + debug!("Successfully authenticated"); + self.authenticated = true; + + Ok(tokens.clone()) + } else { + Err(ATMError::AuthenticationError( + "No tokens received from ATM".to_owned(), + )) + } + } + .instrument(_span) + .await + } + + /// Creates an Affinidi Trusted Messaging Authentication Challenge Response Message + /// # Arguments + /// * `atm_did` - The DID for ATM + /// * `challenge` - The challenge that was sent + /// # Returns + /// A DIDComm message to be sent + /// + /// Notes: + /// - This message will expire after 60 seconds + fn _create_auth_challenge_response( + &self, + atm_did: &str, + body: &AuthenticationChallenge, + ) -> Message { + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + Message::build( + Uuid::new_v4().into(), + "https://affinidi.com/atm/1.0/authenticate".to_owned(), + json!(body), + ) + .to(atm_did.to_owned()) + .from(self.config.my_did.to_owned()) + .created_time(now) + .expires_time(now + 60) + .finalize() + } +} diff --git a/affinidi-messaging-sdk/src/config.rs b/affinidi-messaging-sdk/src/config.rs new file mode 100644 index 0000000..d5ef04e --- /dev/null +++ b/affinidi-messaging-sdk/src/config.rs @@ -0,0 +1,250 @@ +use crate::errors::ATMError; +use affinidi_did_resolver_cache_sdk::DIDCacheClient; +use affinidi_messaging_didcomm::secrets::Secret; +use rustls::pki_types::CertificateDer; +use std::{fs::File, io::BufReader}; +use tracing::error; + +/// Configuration for the Affinidi Trusted Messaging (ATM) Service +/// You need to use the `builder()` method to create a new instance of `Config` +/// Example: +/// ``` +/// use affinidi_messaging_sdk::config::Config; +/// +/// let config = Config::builder().build(); +/// ``` +#[derive(Clone)] +pub struct Config<'a> { + pub(crate) my_did: String, + pub(crate) ssl_certificates: Vec>, + pub(crate) atm_api: String, + pub(crate) atm_api_ws: String, + pub(crate) atm_did: String, + pub(crate) ssl_only: bool, + pub(crate) ws_enabled: bool, + pub(crate) fetch_cache_limit_count: u32, + pub(crate) fetch_cache_limit_bytes: u64, + pub(crate) secrets: Vec, + pub(crate) did_resolver: Option, +} + +impl<'a> Config<'a> { + /// Returns a builder for `Config` + /// Example: + /// ``` + /// use affinidi_messaging_sdk::config::Config; + /// + /// let config = Config::builder().build(); + /// ``` + pub fn builder() -> ConfigBuilder { + ConfigBuilder::default() + } + + pub fn get_ssl_certificates(&self) -> &Vec { + &self.ssl_certificates + } +} + +/// Builder for `Config`. +/// Example: +/// ``` +/// use affinidi_messaging_sdk::config::Config; +/// +/// // Create a new `Config` with defaults +/// let config = Config::builder().build(); +/// ``` +pub struct ConfigBuilder { + ssl_certificates: Vec, + my_did: Option, + atm_api: Option, + atm_api_ws: Option, + atm_did: Option, + ssl_only: bool, + ws_enabled: bool, + fetch_cache_limit_count: u32, + fetch_cache_limit_bytes: u64, + secrets: Vec, + did_resolver: Option, +} + +impl Default for ConfigBuilder { + fn default() -> Self { + ConfigBuilder { + ssl_certificates: vec![], + my_did: None, + atm_api: None, + atm_api_ws: None, + atm_did: None, + ssl_only: true, + ws_enabled: true, + fetch_cache_limit_count: 100, + fetch_cache_limit_bytes: 1024 * 1024 * 10, // Defaults to 10MB Cache + secrets: Vec::new(), + did_resolver: None, + } + } +} + +impl ConfigBuilder { + /// Basic starting constructor for `ConfigBuilder` + pub fn new() -> ConfigBuilder { + ConfigBuilder::default() + } + + /// Add a list of SSL certificates to the configuration + /// Each certificate should be a file path to a PEM encoded certificate + pub fn with_ssl_certificates(mut self, ssl_certificates: &mut Vec) -> Self { + self.ssl_certificates.append(ssl_certificates); + self + } + + /// Add the DID for the client itself to ATM + pub fn with_my_did(mut self, my_did: &str) -> Self { + self.my_did = Some(my_did.to_owned()); + self + } + + /// Add the URL for the ATM API + pub fn with_atm_api(mut self, api_url: &str) -> Self { + self.atm_api = Some(api_url.to_owned()); + self + } + + /// Add the URL for the ATM API WebSocket + /// Defaults: ATM API URL with `/ws` appended + pub fn with_atm_websocket_api(mut self, ws_api_url: &str) -> Self { + self.atm_api_ws = Some(ws_api_url.to_owned()); + self + } + + /// Add the DID for the ATM service itself + pub fn with_atm_did(mut self, atm_did: &str) -> Self { + self.atm_did = Some(atm_did.to_owned()); + self + } + + /// Allow non-SSL connections to the ATM service + /// This is not recommended for production use + /// Default: `true` + pub fn with_non_ssl(mut self) -> Self { + self.ssl_only = false; + self + } + + /// Disables WebSocket connections to the ATM service + /// This is not recommended for production use + /// Default: `true` + pub fn with_websocket_disabled(mut self) -> Self { + self.ws_enabled = false; + self + } + + /// Add a secret to the SDK + /// This is required to auto-start the websocket connection + pub fn with_secret(mut self, secret: Secret) -> Self { + self.secrets.push(secret); + self + } + + /// Set the maximum number of messages to cache in the fetch task + /// Default: 100 + pub fn with_fetch_cache_limit_count(mut self, count: u32) -> Self { + self.fetch_cache_limit_count = count; + self + } + + /// Set the maximum total size of messages to cache in the fetch task in bytes + /// Default: 10MB (1024*1024*10) + pub fn with_fetch_cache_limit_bytes(mut self, count: u64) -> Self { + self.fetch_cache_limit_bytes = count; + self + } + + /// Use an external DID resolver for the SDK + /// Useful if you want to configure the DID resolver externally. + /// Default: ATM SDK will instantiate a local DID resolver + pub fn with_external_did_resolver(mut self, did_resolver: &DIDCacheClient) -> Self { + self.did_resolver = Some(did_resolver.clone()); + self + } + + pub fn build<'a>(self) -> Result, ATMError> { + // Process any custom SSL certificates + let mut certs = vec![]; + let mut failed_certs = false; + for cert in &self.ssl_certificates { + let file = File::open(cert).map_err(|e| { + ATMError::SSLError(format!( + "Couldn't open SSL certificate file ({})! Reason: {}", + cert, e + )) + })?; + let mut reader = BufReader::new(file); + + for cert in rustls_pemfile::certs(&mut reader) { + match cert { + Ok(cert) => certs.push(cert.into_owned()), + Err(e) => { + failed_certs = true; + error!("Couldn't parse SSL certificate! Reason: {}", e) + } + } + } + } + if failed_certs { + return Err(ATMError::SSLError( + "Couldn't parse all SSL certificates!".to_owned(), + )); + } + + let my_did = if let Some(my_did) = self.my_did { + my_did + } else { + return Err(ATMError::ConfigError( + "You must provide a DID for the SDK, used for authentication!".to_owned(), + )); + }; + + let atm_api = if let Some(atm_url) = self.atm_api { + atm_url + } else { + // TODO: Change this to the production URL + "https://localhost:7037/atm/v1".to_string() + }; + + // convert the ATM API URL to a WebSocket URL + let atm_api_ws = if let Some(atm_api_ws) = self.atm_api_ws { + atm_api_ws + } else if atm_api.starts_with("http://") { + format!("ws://{}/ws", atm_api.split_at(7).1) + } else if atm_api.starts_with("https://") { + format!("wss://{}/ws", atm_api.split_at(8).1) + } else { + return Err(ATMError::ConfigError( + "ATM API URL must start with http:// or https://".to_string(), + )); + }; + + let atm_did = if let Some(atm_did) = self.atm_did { + atm_did + } else { + return Err(ATMError::ConfigError( + "You must provide the DID for the ATM service!".to_owned(), + )); + }; + + Ok(Config { + ssl_certificates: certs, + my_did, + atm_api, + atm_api_ws, + atm_did, + ssl_only: self.ssl_only, + ws_enabled: self.ws_enabled, + fetch_cache_limit_count: self.fetch_cache_limit_count, + fetch_cache_limit_bytes: self.fetch_cache_limit_bytes, + secrets: self.secrets, + did_resolver: self.did_resolver, + }) + } +} diff --git a/affinidi-messaging-sdk/src/conversions/mod.rs b/affinidi-messaging-sdk/src/conversions/mod.rs new file mode 100644 index 0000000..2035a25 --- /dev/null +++ b/affinidi-messaging-sdk/src/conversions/mod.rs @@ -0,0 +1,36 @@ +use affinidi_messaging_didcomm::secrets::Secret; +use serde_json::Value; + +/// Helper functions for converting between different types. + +/// Create a new Secret from a JWK JSON string +/// Example: +/// ```ignore +/// use affinidi_messaging_didcomm::secrets::Secret; +/// use affinidi_messaging_sdk::ATM; +/// use affinidi_messaging_sdk::config::Config; +/// use affinidi_messaging_sdk::conversations::secret_from_str; +/// +/// let config = Config::builder().build()?; +/// let atm = ATM::new(config).await?; +/// +/// let key_id = "did:example:123#key-1"; +/// let key_str = r#"{ +/// "crv": "Ed25519", +/// "d": "LLWCf...dGpIqSFw", +/// "kty": "OKP", +/// "x": "Hn8T...ZExwQo" +/// }"#; +/// +/// let secret = secret_from_str(key_id, key_str)?; +/// atm.add_secret(secret); +/// ``` +pub fn secret_from_str(key_id: &str, jwk: &Value) -> Secret { + Secret { + id: key_id.to_string(), + type_: affinidi_messaging_didcomm::secrets::SecretType::JsonWebKey2020, + secret_material: affinidi_messaging_didcomm::secrets::SecretMaterial::JWK { + private_key_jwk: jwk.clone(), + }, + } +} diff --git a/affinidi-messaging-sdk/src/errors.rs b/affinidi-messaging-sdk/src/errors.rs new file mode 100644 index 0000000..2686bfe --- /dev/null +++ b/affinidi-messaging-sdk/src/errors.rs @@ -0,0 +1,24 @@ +use thiserror::Error; + +/// ATMError +#[derive(Error, Debug)] +pub enum ATMError { + #[error("DID error: {0}")] + DIDError(String), + #[error("Secrets error: {0}")] + SecretsError(String), + #[error("SSL error: {0}")] + SSLError(String), + #[error("Transport (HTTP(S)) error: {0}")] + TransportError(String), + #[error("Message sending error: {0}")] + MsgSendError(String), + #[error("Message receive error: {0}")] + MsgReceiveError(String), + #[error("Config error: {0}")] + ConfigError(String), + #[error("Authentication error: {0}")] + AuthenticationError(String), + #[error("DIDComm message error: {0}. Reason: {1}")] + DidcommError(String, String), +} diff --git a/affinidi-messaging-sdk/src/lib.rs b/affinidi-messaging-sdk/src/lib.rs new file mode 100644 index 0000000..4015213 --- /dev/null +++ b/affinidi-messaging-sdk/src/lib.rs @@ -0,0 +1,195 @@ +use affinidi_did_resolver_cache_sdk::DIDCacheClient; +use affinidi_messaging_didcomm::secrets::Secret; +use config::Config; +use errors::ATMError; +use messages::AuthorizationResponse; +use reqwest::{Certificate, Client}; +use resolvers::secrets_resolver::AffinidiSecrets; +use rustls::{ClientConfig, RootCertStore}; +use ssi::dids::Document; +use std::sync::Arc; +use tokio::sync::mpsc::Receiver; +use tokio::sync::mpsc::Sender; +use tokio::task::JoinHandle; +use tokio_tungstenite::Connector; +use tracing::{debug, span, warn}; +use websockets::ws_handler::WSCommand; + +mod authentication; +pub mod config; +pub mod conversions; +pub mod errors; +pub mod messages; +pub mod protocols; +mod resolvers; +pub mod transports; + +pub mod websockets { + #[doc(inline)] + pub use crate::transports::websockets::*; +} + +pub struct ATM<'c> { + pub(crate) config: Config<'c>, + did_resolver: DIDCacheClient, + secrets_resolver: AffinidiSecrets, + pub(crate) client: Client, + authenticated: bool, + jwt_tokens: Option, + ws_connector: Connector, + pub(crate) ws_enabled: bool, + ws_handler: Option>, + ws_send_stream: Option>, + ws_recv_stream: Option>, +} + +/// Affinidi Trusted Messaging SDK +/// This is the top level struct for the SSK +/// +/// Example: +/// ```ignore +/// use affinidi_messaging_sdk::ATM; +/// use affinidi_messaging_sdk::config::Config; +/// +/// let config = Config::builder().build(); +/// let mut atm = ATM::new(config); +/// +/// // Add the DID:Peer method +/// atm.add_did_method(Box::new(DIDPeer)); +/// +/// let response = atm.ping("did:example:123", true); +/// ``` +impl<'c> ATM<'c> { + /// Creates a new instance of the SDK with a given configuration + /// You need to add at least the DID Method for the SDK DID to work + pub async fn new(config: Config<'c>) -> Result, ATMError> { + // Set a process wide default crypto provider. + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + + // Set up the HTTP/HTTPS client + let mut client = reqwest::ClientBuilder::new() + .use_rustls_tls() + .https_only(config.ssl_only) + .user_agent("Affinidi Trusted Messaging"); + + for cert in config.get_ssl_certificates() { + client = client.add_root_certificate( + Certificate::from_der(cert.to_vec().as_slice()).map_err(|e| { + ATMError::SSLError(format!("Couldn't add certificate. Reason: {}", e)) + })?, + ); + } + + let client = match client.build() { + Ok(client) => client, + Err(e) => { + return Err(ATMError::TransportError(format!( + "Couldn't create HTTPS Client. Reason: {}", + e + ))) + } + }; + + // Set up the WebSocket Client + let mut root_store = RootCertStore::empty(); + if config.get_ssl_certificates().is_empty() { + debug!("Use native SSL Certs"); + + for cert in + rustls_native_certs::load_native_certs().expect("Could not load platform certs") + { + root_store.add(cert).map_err(|e| { + warn!("Couldn't add cert: {:?}", e); + ATMError::SSLError(format!("Couldn't add cert. Reason: {}", e)) + })?; + } + } else { + debug!("Use custom SSL Certs"); + for cert in config.get_ssl_certificates() { + root_store.add(cert.to_owned()).map_err(|e| { + warn!("Couldn't add cert: {:?}", e); + ATMError::SSLError(format!("Couldn't add cert. Reason: {}", e)) + })?; + } + } + + let ws_config = ClientConfig::builder() + .with_root_certificates(root_store) + .with_no_client_auth(); + + let ws_connector = Connector::Rustls(Arc::new(ws_config)); + + // Set up the DID Resolver + let did_resolver = if let Some(did_resolver) = &config.did_resolver { + did_resolver.clone() + } else { + match DIDCacheClient::new( + affinidi_did_resolver_cache_sdk::config::ClientConfigBuilder::default().build(), + ) + .await + { + Ok(config) => config, + Err(err) => { + return Err(ATMError::DIDError(format!( + "Couldn't create DID resolver! Reason: {}", + err + ))) + } + } + }; + + let mut atm = ATM { + config: config.clone(), + did_resolver, + secrets_resolver: AffinidiSecrets::new(vec![]), + client, + authenticated: false, + jwt_tokens: None, + ws_connector, + ws_enabled: config.ws_enabled, + ws_handler: None, + ws_send_stream: None, + ws_recv_stream: None, + }; + + // Add our own DID to the DID_RESOLVER + atm.add_did(&config.my_did).await?; + // Add our ATM DID to the DID_RESOLVER + atm.add_did(&config.atm_did).await?; + + // Add any pre-loaded secrets + for secret in config.secrets { + atm.add_secret(secret); + } + + // Start the websocket connection if enabled + if atm.ws_enabled { + atm.start_websocket_task().await?; + } + debug!("ATM SDK initialized"); + + Ok(atm) + } + + /// Adds a DID to the resolver + /// This resolves the DID to the DID Document, and adds it to the list of known DIDs + /// Returns the DIDDoc itself if successful, or an SDK Error + pub async fn add_did(&mut self, did: &str) -> Result { + let _span = span!(tracing::Level::DEBUG, "add_did", did = did).entered(); + debug!("Adding DID to resolver"); + + match self.did_resolver.resolve(did).await { + Ok(results) => Ok(results.doc), + Err(err) => Err(ATMError::DIDError(format!( + "Couldn't resolve did ({}). Reason: {}", + did, err + ))), + } + } + + /// Adds required secrets to the secrets resolver + /// You need to add the private keys of the DIDs you want to sign and encrypt messages with + pub fn add_secret(&mut self, secret: Secret) { + self.secrets_resolver.insert(secret); + } +} diff --git a/affinidi-messaging-sdk/src/messages/delete.rs b/affinidi-messaging-sdk/src/messages/delete.rs new file mode 100644 index 0000000..d0754f6 --- /dev/null +++ b/affinidi-messaging-sdk/src/messages/delete.rs @@ -0,0 +1,78 @@ +use tracing::{debug, span, Level}; + +use crate::{errors::ATMError, messages::SuccessResponse, ATM}; + +use super::{DeleteMessageRequest, DeleteMessageResponse}; + +impl<'c> ATM<'c> { + /// Delete messages from ATM + /// - messages: List of message_ids to delete + pub async fn delete_messages( + &mut self, + messages: &DeleteMessageRequest, + ) -> Result { + let _span = span!(Level::DEBUG, "delete_messages").entered(); + + // Check if authenticated + let tokens = self.authenticate().await?; + + let msg = serde_json::to_string(messages).map_err(|e| { + ATMError::TransportError(format!( + "Could not serialize delete message request: {:?}", + e + )) + })?; + + debug!("Sending delete_messages request: {:?}", msg); + + let res = self + .client + .delete(format!("{}/delete", self.config.atm_api)) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", tokens.access_token)) + .body(msg) + .send() + .await + .map_err(|e| { + ATMError::TransportError(format!("Could not send delete_messages request: {:?}", e)) + })?; + + let status = res.status(); + debug!("API response: status({})", status); + + let body = res + .text() + .await + .map_err(|e| ATMError::TransportError(format!("Couldn't get body: {:?}", e)))?; + + if !status.is_success() { + return Err(ATMError::TransportError(format!( + "Status not successful. status({}), response({})", + status, body + ))); + } + + let body = serde_json::from_str::>(&body) + .ok() + .unwrap(); + + let list = if let Some(list) = body.data { + list + } else { + return Err(ATMError::TransportError("No messages found".to_string())); + }; + + debug!( + "response: success({}) messages, failed({}) messages", + list.success.len(), + list.errors.len() + ); + if !list.errors.is_empty() { + for (msg, err) in &list.errors { + debug!("failed: msg({}) error({})", msg, err); + } + } + + Ok(list) + } +} diff --git a/affinidi-messaging-sdk/src/messages/fetch.rs b/affinidi-messaging-sdk/src/messages/fetch.rs new file mode 100644 index 0000000..2b96f8e --- /dev/null +++ b/affinidi-messaging-sdk/src/messages/fetch.rs @@ -0,0 +1,156 @@ +use serde::{Deserialize, Serialize}; +use tracing::{debug, span, Level}; + +use crate::{ + errors::ATMError, + messages::{DeleteMessageRequest, SuccessResponse}, + ATM, +}; + +use super::{FetchDeletePolicy, GetMessagesResponse}; + +/// fetch_messages() options +#[derive(Serialize, Deserialize, Debug)] +pub struct FetchOptions { + /// The maximum number of messages to fetch. Default: 10 + pub limit: usize, + /// The receive_id to start fetching from. Default: None. Starts with oldest message + pub start_id: Option, + /// Delete policy for messages after fetching. Default: DoNotDelete + pub delete_policy: FetchDeletePolicy, +} + +impl Default for FetchOptions { + fn default() -> Self { + FetchOptions { + limit: 10, + start_id: None, + delete_policy: FetchDeletePolicy::DoNotDelete, + } + } +} + +impl<'c> ATM<'c> { + /// Fetches any available messages from your ATM inbox + /// This differs from the `get_messages()` function in that you don't need to know the message_id in advance + /// + /// # Option Fields + /// + /// * `limit` - The maximum number of messages to fetch (default: 10, minimum: 1, maximum: 100) + /// * `start_id` - The message_id to start fetching from (default: Starts with oldest message) + /// * `delete_policy` - Delete policy for messages after fetching (default: DoNotDelete) + /// + /// Calling fetch with no start_id and default delete_policy will result in the same messages being retrieved again and again + /// + /// # Example + /// ```ignore + /// // Use default options + /// let messages = atm.fetch_messages(&FetchOptions::default()).await?; + /// + /// // Use custom options + /// let messages = atm.fetch_messages(&FetchOptions {start_id: Some("12345689-0".to_string()), ..FetchOptions::default()}).await?; + /// ``` + pub async fn fetch_messages( + &mut self, + options: &FetchOptions, + ) -> Result { + let _span = span!( + Level::DEBUG, + "fetch_messages", + limit = options.limit, + start_id = options.start_id, + delete_policy = options.delete_policy.to_string() + ) + .entered(); + + // Check if limit is within bounds + if options.limit < 1 || options.limit > 100 { + return Err(ATMError::ConfigError(format!( + "FetchOptions.limit must be between 1 and 100 inclusive. Got: {}", + options.limit + ))); + } + + // Check if authenticated + let tokens = self.authenticate().await?; + + let body = serde_json::to_string(options).map_err(|e| { + ATMError::TransportError(format!( + "Could not serialize fetch_message() options: {:?}", + e + )) + })?; + + let res = self + .client + .post(format!("{}/fetch", self.config.atm_api,)) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", tokens.access_token)) + .body(body) + .send() + .await + .map_err(|e| { + ATMError::TransportError(format!("Could not send list_messages request: {:?}", e)) + })?; + + let status = res.status(); + debug!("API response: status({})", status); + + let body = res + .text() + .await + .map_err(|e| ATMError::TransportError(format!("Couldn't get body: {:?}", e)))?; + + if !status.is_success() { + return Err(ATMError::TransportError(format!( + "Status not successful. status({}), response({})", + status, body + ))); + } + + let body = serde_json::from_str::>(&body) + .ok() + .unwrap(); + + let mut list = if let Some(list) = body.data { + list + } else { + return Err(ATMError::TransportError("No messages found".to_string())); + }; + + if let FetchDeletePolicy::OnReceive = options.delete_policy { + match self + .delete_messages(&DeleteMessageRequest { + message_ids: list.success.iter().map(|m| m.msg_id.clone()).collect(), + }) + .await + { + Ok(r) => { + debug!("Messages deleted: ({})", r.success.len()); + list.delete_errors.extend(r.errors); + } + Err(e) => { + debug!("Error deleting messages: ({})", e); + list.delete_errors = list + .success + .iter() + .map(|m| (m.msg_id.clone(), "ERROR".to_string())) + .collect(); + } + } + } + + debug!( + "response: success({}) messages, failed_deleted({}) messages", + list.success.len(), + list.delete_errors.len() + ); + if !list.delete_errors.is_empty() { + for (msg, err) in &list.delete_errors { + debug!("failed delete: msg({}) error({})", msg, err); + } + } + + Ok(list) + } +} diff --git a/affinidi-messaging-sdk/src/messages/get.rs b/affinidi-messaging-sdk/src/messages/get.rs new file mode 100644 index 0000000..bc5474a --- /dev/null +++ b/affinidi-messaging-sdk/src/messages/get.rs @@ -0,0 +1,85 @@ +use tracing::{debug, span, Level}; + +use crate::{ + errors::ATMError, + messages::{GetMessagesResponse, SuccessResponse}, + ATM, +}; + +use super::GetMessagesRequest; + +impl<'c> ATM<'c> { + /// Returns a list of messages that are stored in the ATM + /// - messages : List of message IDs to retrieve + pub async fn get_messages( + &mut self, + messages: &GetMessagesRequest, + ) -> Result { + let _span = span!(Level::DEBUG, "get_messages").entered(); + + // Check if authenticated + let tokens = self.authenticate().await?; + + let body = serde_json::to_string(messages).map_err(|e| { + ATMError::TransportError(format!("Could not serialize get message request: {:?}", e)) + })?; + + debug!("Sending get_messages request: {:?}", body); + + let res = self + .client + .post(format!("{}/outbound", self.config.atm_api)) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", tokens.access_token)) + .body(body) + .send() + .await + .map_err(|e| { + ATMError::TransportError(format!("Could not send get_messages request: {:?}", e)) + })?; + + let status = res.status(); + debug!("API response: status({})", status); + + let body = res + .text() + .await + .map_err(|e| ATMError::TransportError(format!("Couldn't get body: {:?}", e)))?; + + if !status.is_success() { + return Err(ATMError::TransportError(format!( + "Status not successful. status({}), response({})", + status, body + ))); + } + + let body = serde_json::from_str::>(&body) + .ok() + .unwrap(); + + let list = if let Some(list) = body.data { + list + } else { + return Err(ATMError::TransportError("No messages found".to_string())); + }; + + debug!( + "response: success({}) messages, failed({}) messages, failed_deleted({}) messages", + list.success.len(), + list.get_errors.len(), + list.delete_errors.len() + ); + if !list.get_errors.is_empty() { + for (msg, err) in &list.get_errors { + debug!("failed get: msg({}) error({})", msg, err); + } + } + if !list.delete_errors.is_empty() { + for (msg, err) in &list.delete_errors { + debug!("failed delete: msg({}) error({})", msg, err); + } + } + + Ok(list) + } +} diff --git a/affinidi-messaging-sdk/src/messages/list.rs b/affinidi-messaging-sdk/src/messages/list.rs new file mode 100644 index 0000000..5b58f64 --- /dev/null +++ b/affinidi-messaging-sdk/src/messages/list.rs @@ -0,0 +1,67 @@ +use super::{Folder, MessageList}; +use crate::{errors::ATMError, messages::SuccessResponse, ATM}; +use sha256::digest; +use tracing::{debug, span, Level}; + +impl<'c> ATM<'c> { + /// Returns a list of messages that are stored in the ATM + /// # Parameters + /// - `did`: The DID to list messages for + /// - `folder`: The folder to list messages from + pub async fn list_messages( + &mut self, + did: &str, + folder: Folder, + ) -> Result { + let _span = span!(Level::DEBUG, "list_messages", folder = folder.to_string()).entered(); + debug!("listing folder({}) for DID({})", did, folder); + + // Check if authenticated + let tokens = self.authenticate().await?; + + let res = self + .client + .get(format!( + "{}/list/{}/{}", + self.config.atm_api, + digest(did), + folder, + )) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", tokens.access_token)) + .send() + .await + .map_err(|e| { + ATMError::TransportError(format!("Could not send list_messages request: {:?}", e)) + })?; + + let status = res.status(); + debug!("API response: status({})", status); + + let body = res + .text() + .await + .map_err(|e| ATMError::TransportError(format!("Couldn't get body: {:?}", e)))?; + + if !status.is_success() { + return Err(ATMError::TransportError(format!( + "Status not successful. status({}), response({})", + status, body + ))); + } + + let body = serde_json::from_str::>(&body) + .ok() + .unwrap(); + + let list = if let Some(list) = body.data { + list + } else { + return Err(ATMError::TransportError("No messages found".to_string())); + }; + + debug!("List contains ({}) messages", list.len()); + + Ok(list) + } +} diff --git a/affinidi-messaging-sdk/src/messages/mod.rs b/affinidi-messaging-sdk/src/messages/mod.rs new file mode 100644 index 0000000..e150281 --- /dev/null +++ b/affinidi-messaging-sdk/src/messages/mod.rs @@ -0,0 +1,152 @@ +use std::fmt::Display; + +use serde::{de::DeserializeOwned, Deserialize, Serialize}; + +pub mod delete; +pub mod fetch; +pub mod get; +pub mod list; +pub mod pack; +pub mod sending; +pub mod unpack; +pub mod well_known_did; + +/// Generic response structure for all responses from the ATM API +#[derive(Serialize, Deserialize, Debug)] +#[allow(non_snake_case)] +pub struct SuccessResponse { + pub sessionId: String, + pub httpCode: u16, + pub errorCode: i32, + pub errorCodeStr: String, + pub message: String, + #[serde(bound(deserialize = ""))] + pub data: Option, +} + +/// Specific response structure for the authentication challenge response +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct AuthenticationChallenge { + pub challenge: String, + pub session_id: String, +} +impl GenericDataStruct for AuthenticationChallenge {} + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct AuthorizationResponse { + pub access_token: String, + pub refresh_token: String, +} +impl GenericDataStruct for AuthorizationResponse {} + +/// Response from message_delete +/// - successful: Contains list of message_id's that were deleted successfully +/// - errors: Contains a list of message_id's and error messages for failed deletions +#[derive(Default, Serialize, Deserialize)] +pub struct DeleteMessageResponse { + pub success: Vec, + pub errors: Vec<(String, String)>, +} +impl GenericDataStruct for DeleteMessageResponse {} +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct DeleteMessageRequest { + pub message_ids: Vec, +} +impl GenericDataStruct for DeleteMessageRequest {} + +/// A list of messages that are stored in the ATM for a given DID +/// - msg_id : The unique identifier of the message +/// - send_id : The unique identifier of the element in the senders stream +/// - receive_id : The unique identifier of the element in the senders stream +/// - size : The size of the message in bytes +/// - timestamp : The date the message was stored in the ATM (milliseconds since epoch) +/// - to_address : Address the message was sent to +/// - from_address : Address the message was sent from (if applicable) +/// - msg : The message itself +#[derive(Debug, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct MessageListElement { + pub msg_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub send_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub receive_id: Option, + pub size: u64, + pub timestamp: u64, + #[serde(skip_serializing_if = "Option::is_none")] + pub to_address: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub from_address: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub msg: Option, +} +impl GenericDataStruct for MessageListElement {} + +pub type MessageList = Vec; +impl GenericDataStruct for MessageList {} + +/// enum of ATM folder types +/// inbox = messages inbound to the caller +/// outbox = messages outbound to the caller +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(rename_all = "lowercase")] +pub enum Folder { + Inbox, + Outbox, +} + +impl Display for Folder { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Folder::Inbox => write!(f, "inbox"), + Folder::Outbox => write!(f, "outbox"), + } + } +} + +/// Get messages Request struct +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct GetMessagesRequest { + pub message_ids: Vec, + pub delete: bool, +} +impl GenericDataStruct for GetMessagesRequest {} + +/// Get messages Response struct +#[derive(Debug, Default, Serialize, Deserialize)] +pub struct GetMessagesResponse { + pub success: MessageList, + pub get_errors: Vec<(String, String)>, + pub delete_errors: Vec<(String, String)>, +} +impl GenericDataStruct for GetMessagesResponse {} + +/// Enum for the delete policy when retrieving messages +#[derive(Default, Serialize, Deserialize, Debug)] +pub enum FetchDeletePolicy { + /// Deletes messages as they are fetched, occurs automatically within ATM + Optimistic, + /// The SDK will delete messages after they are received by the SDK + OnReceive, + /// Messages are not deleted (Default behavior) + /// It is up to the caller as to when and how they want to delete messages + #[default] + DoNotDelete, +} + +impl Display for FetchDeletePolicy { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FetchDeletePolicy::Optimistic => write!(f, "optimistic"), + FetchDeletePolicy::OnReceive => write!(f, "on_receive"), + FetchDeletePolicy::DoNotDelete => write!(f, "do_not_delete"), + } + } +} + +/// Helps with deserializing the generic data field in the SuccessResponse struct +pub trait GenericDataStruct: DeserializeOwned + Serialize {} + +#[derive(Serialize, Deserialize)] +pub struct EmptyResponse; +impl GenericDataStruct for EmptyResponse {} diff --git a/affinidi-messaging-sdk/src/messages/pack.rs b/affinidi-messaging-sdk/src/messages/pack.rs new file mode 100644 index 0000000..bc5f746 --- /dev/null +++ b/affinidi-messaging-sdk/src/messages/pack.rs @@ -0,0 +1,81 @@ +use affinidi_messaging_didcomm::{ + Message, PackEncryptedMetadata, PackEncryptedOptions, PackSignedMetadata, +}; +use tracing::{span, Instrument, Level}; + +use crate::{errors::ATMError, ATM}; + +impl<'c> ATM<'c> { + /// Pack a message for sending to a recipient + /// from: if None, then will use anonymous encryption + /// sign_by: If None, then will not sign the message + pub async fn pack_encrypted( + &mut self, + message: &Message, + to: &str, + from: Option<&str>, + sign_by: Option<&str>, + ) -> Result<(String, PackEncryptedMetadata), ATMError> { + let _span = span!(Level::DEBUG, "pack_encrypted",); + + async move { + message + .pack_encrypted( + to, + from, + sign_by, + &self.did_resolver, + &self.secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + } + .instrument(_span) + .await + .map_err(|e| { + ATMError::DidcommError( + "SDK".to_string(), + format!("pack_encrypted() failed. Reason: {}", e), + ) + }) + } + + /// Rare case of creating an unencrypted message, but you want to prove who sent the message + /// Signs the unencrypted message with the sign_by key + pub async fn pack_signed( + &mut self, + message: &Message, + sign_by: &str, + ) -> Result<(String, PackSignedMetadata), ATMError> { + let _span = span!(Level::DEBUG, "pack_signed",); + + async move { + message + .pack_signed(sign_by, &self.did_resolver, &self.secrets_resolver) + .await + } + .instrument(_span) + .await + .map_err(|e| { + ATMError::DidcommError( + "SDK".to_string(), + format!("pack_signed() failed. Reason: {}", e), + ) + }) + } + + /// creates a plaintext (unencrypted and unsigned) message + pub async fn pack_plaintext(&mut self, message: &Message) -> Result { + let _span = span!(Level::DEBUG, "pack_plaintext",); + + async move { message.pack_plaintext(&self.did_resolver).await } + .instrument(_span) + .await + .map_err(|e| { + ATMError::DidcommError( + "SDK".to_string(), + format!("pack_plaintext() failed. Reason: {}", e), + ) + }) + } +} diff --git a/affinidi-messaging-sdk/src/messages/sending.rs b/affinidi-messaging-sdk/src/messages/sending.rs new file mode 100644 index 0000000..74927b1 --- /dev/null +++ b/affinidi-messaging-sdk/src/messages/sending.rs @@ -0,0 +1,32 @@ +use serde::{Deserialize, Serialize}; + +use crate::ATM; + +use super::GenericDataStruct; + +/// Response from the ATM API when sending messages (inbound messages) +/// Stored messages will have a list of messages that were stored +/// Ephemeral messages contain the actual message response (it is not stored anywhere) +/// Empty is used when there is no expected response +#[derive(Serialize, Deserialize, Debug)] +pub enum InboundMessageResponse { + Stored(InboundMessageList), + Ephemeral(String), + Empty, +} +impl GenericDataStruct for InboundMessageResponse {} + +/// Response from the ATM API when sending a message that is stored +/// Contains a list of messages that were stored +/// - messages : List of successful stored messages (recipient, message_ids) +/// - errors : List of errors that occurred while storing messages (recipient, error) +/// +/// NOTE: Sending a single message can result in multiple forward messages being stored! +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +pub struct InboundMessageList { + pub messages: Vec<(String, String)>, + pub errors: Vec<(String, String)>, +} +impl GenericDataStruct for InboundMessageList {} + +impl<'c> ATM<'c> {} diff --git a/affinidi-messaging-sdk/src/messages/unpack.rs b/affinidi-messaging-sdk/src/messages/unpack.rs new file mode 100644 index 0000000..4076299 --- /dev/null +++ b/affinidi-messaging-sdk/src/messages/unpack.rs @@ -0,0 +1,52 @@ +use affinidi_messaging_didcomm::{envelope::MetaEnvelope, Message, UnpackMetadata, UnpackOptions}; +use tracing::{debug, span, Instrument, Level}; + +use crate::{errors::ATMError, ATM}; + +impl<'c> ATM<'c> { + pub async fn unpack(&mut self, message: &str) -> Result<(Message, UnpackMetadata), ATMError> { + let _span = span!(Level::DEBUG, "unpack",); + + async move { + let mut envelope = match MetaEnvelope::new( + message, + &self.did_resolver, + &self.secrets_resolver, + ) + .await + { + Ok(envelope) => envelope, + Err(e) => { + return Err(ATMError::DidcommError( + "Cannot convert string to MetaEnvelope".into(), + e.to_string(), + )); + } + }; + debug!("message converted to MetaEnvelope"); + + // Unpack the message + let (msg, metadata) = match Message::unpack( + &mut envelope, + &self.did_resolver, + &self.secrets_resolver, + &UnpackOptions::default(), + ) + .await + { + Ok(ok) => ok, + Err(e) => { + return Err(ATMError::DidcommError( + "Couldn't unpack incoming message".into(), + e.to_string(), + )); + } + }; + + debug!("message unpacked:\n{:#?}", msg); + Ok((msg, metadata)) + } + .instrument(_span) + .await + } +} diff --git a/affinidi-messaging-sdk/src/messages/well_known_did.rs b/affinidi-messaging-sdk/src/messages/well_known_did.rs new file mode 100644 index 0000000..02b8c98 --- /dev/null +++ b/affinidi-messaging-sdk/src/messages/well_known_did.rs @@ -0,0 +1,49 @@ +use crate::{errors::ATMError, ATM}; +use tracing::{debug, span, Level}; + +impl<'c> ATM<'c> { + /// Returns a list of messages that are stored in the ATM + /// - messages : List of message IDs to retrieve + pub async fn well_known_did_json(&mut self) -> Result { + let _span = span!(Level::DEBUG, "well_known_did_json").entered(); + + debug!("Sending well_known_did request"); + + let well_known_did_atm_api = format!("{}/.well-known/did", self.config.clone().atm_api); + debug!( + "API well_known_did_api({})", + format!("{}/.well-known/did", well_known_did_atm_api) + ); + + let res = self + .client + .get(well_known_did_atm_api) + .header("Content-Type", "application/json") + .send() + .await + .map_err(|e| { + ATMError::TransportError(format!( + "Could not send get well-known did.json request: {:?}", + e + )) + })?; + + let status = res.status(); + debug!("API response: status({})", status); + let string_body = res + .text() + .await + .map_err(|e| ATMError::TransportError(format!("Couldn't get string body: {:?}", e)))?; + + if !status.is_success() { + return Err(ATMError::TransportError(format!( + "Status not successful. status({}), response({})", + status, string_body + ))); + } + + debug!("API response: body({})", string_body); + + Ok(string_body) + } +} diff --git a/affinidi-messaging-sdk/src/protocols/message_pickup.rs b/affinidi-messaging-sdk/src/protocols/message_pickup.rs new file mode 100644 index 0000000..aead021 --- /dev/null +++ b/affinidi-messaging-sdk/src/protocols/message_pickup.rs @@ -0,0 +1,643 @@ +use affinidi_messaging_didcomm::{AttachmentData, Message, PackEncryptedOptions, UnpackMetadata}; +use base64::prelude::*; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::time::{Duration, SystemTime}; +use tokio::select; +use tracing::{debug, span, warn, Instrument, Level}; +use uuid::Uuid; + +use crate::{ + errors::ATMError, + messages::{sending::InboundMessageResponse, EmptyResponse, GenericDataStruct}, + transports::SendMessageResponse, + websockets::ws_handler::WSCommand, + ATM, +}; + +#[derive(Default)] +pub struct MessagePickup {} + +// Reads the body of an incoming Message Pickup 3.0 Status Request Message +#[derive(Default, Deserialize)] +pub struct MessagePickupStatusRequest { + pub recipient_did: Option, +} + +// Reads the body of an incoming Message Pickup 3.0 Live Delivery Message +#[derive(Default, Deserialize)] +pub struct MessagePickupLiveDelivery { + pub live_delivery: bool, +} + +// Body of a StatusRequest reply +#[derive(Default, Debug, Serialize, Deserialize)] +pub struct MessagePickupStatusReply { + pub recipient_did: String, + pub message_count: u32, + #[serde(skip_serializing_if = "Option::is_none")] + pub longest_waited_seconds: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub newest_received_time: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub oldest_received_time: Option, + pub total_bytes: u64, + pub live_delivery: bool, +} +impl GenericDataStruct for MessagePickupStatusReply {} + +// Reads the body of an incoming Message Pickup 3.0 Delivery Request Message +#[derive(Default, Deserialize, Serialize)] +pub struct MessagePickupDeliveryRequest { + pub recipient_did: String, + pub limit: usize, +} + +/// Handles the return from a message delivery request +/// returns +/// - StatusReply : No messages available +/// - MessageDelivery : A DIDComm message containing messages +#[derive(Serialize, Deserialize)] +enum DeliveryRequestResponse { + StatusReply(MessagePickupStatusReply), + MessageDelivery(String), +} + +impl MessagePickup { + /// Sends a Message Pickup 3.0 `Status Request` message + /// recipient_did : Optional, allows you to ask for status for a specific DID. If none, will ask for default DID in ATM + /// mediator_did : Optional, allows you to ask a specific mediator. If none, will ask for default mediator in ATM + /// wait : Time Duration to wait for a response from websocket. Default (10 Seconds) + /// + /// Returns a StatusReply if successful + pub async fn send_status_request( + &self, + atm: &mut ATM<'_>, + recipient_did: Option, + mediator_did: Option, + wait: Option, + ) -> Result { + let _span = span!(Level::DEBUG, "send_status_request",).entered(); + debug!( + "Status Request to recipient_did: {:?}, mediator_did: {:?}, wait: {:?}", + recipient_did, mediator_did, wait + ); + + let mut msg = Message::build( + Uuid::new_v4().into(), + "https://didcomm.org/messagepickup/3.0/status-request".to_owned(), + json!({}), + ) + .header("return_route".into(), Value::String("all".into())); + + if let Some(recipient_did) = &recipient_did { + msg = msg.body(json!({"recipient_did": recipient_did})); + } + + let to_did = if let Some(mediator_did) = mediator_did { + mediator_did + } else { + atm.config.atm_did.clone() + }; + msg = msg.to(to_did.clone()); + + msg = msg.from(atm.config.my_did.clone()); + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + let msg = msg.created_time(now).expires_time(now + 300).finalize(); + let msg_id = msg.id.clone(); + + debug!("Status-Request message: {:?}", msg); + + // Pack the message + let (msg, _) = msg + .pack_encrypted( + &to_did, + Some(&atm.config.my_did), + Some(&atm.config.my_did), + &atm.did_resolver, + &atm.secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + .map_err(|e| ATMError::MsgSendError(format!("Error packing message: {}", e)))?; + + if atm.ws_send_stream.is_some() { + atm.ws_send_didcomm_message::(&msg, &msg_id) + .await?; + let response = self + .live_stream_get( + atm, + &msg_id, + wait.unwrap_or_else(|| Duration::from_secs(10)), + ) + .await?; + + if let Some((message, _)) = response { + self._parse_status_response(&message).await + } else { + Err(ATMError::MsgSendError("No response from API".into())) + } + } else { + type MessageString = String; + impl GenericDataStruct for MessageString {} + + let a = atm + .send_didcomm_message::(&msg, true) + .await?; + + debug!("Response: {:?}", a); + + // Unpack the response + if let SendMessageResponse::RestAPI(Some(InboundMessageResponse::Ephemeral(message))) = + a + { + let (message, _) = atm.unpack(&message).await?; + self._parse_status_response(&message).await + } else { + Err(ATMError::MsgSendError("No response from API".into())) + } + } + } + + pub(crate) async fn _parse_status_response( + &self, + message: &Message, + ) -> Result { + let status: MessagePickupStatusReply = serde_json::from_value(message.body.clone()) + .map_err(|err| { + ATMError::MsgReceiveError(format!("Error reading status response: {}", err)) + })?; + Ok(status) + } + + /// Sends a Message Pickup 3.0 `Live Delivery` message + pub async fn toggle_live_delivery( + &self, + atm: &mut ATM<'_>, + live_delivery: bool, + ) -> Result<(), ATMError> { + let _span = span!(Level::DEBUG, "toggle_live_delivery",).entered(); + debug!("Setting live_delivery to ({})", live_delivery); + + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + + let msg = Message::build( + Uuid::new_v4().into(), + "https://didcomm.org/messagepickup/3.0/live-delivery-change".to_owned(), + json!({"live_delivery": live_delivery}), + ) + .header("return_route".into(), Value::String("all".into())) + .created_time(now) + .expires_time(now + 300) + .from(atm.config.my_did.clone()) + .to(atm.config.atm_did.clone()) + .finalize(); + let msg_id = msg.id.clone(); + + // Pack the message + let (msg, _) = msg + .pack_encrypted( + &atm.config.atm_did, + Some(&atm.config.my_did), + Some(&atm.config.my_did), + &atm.did_resolver, + &atm.secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + .map_err(|e| ATMError::MsgSendError(format!("Error packing message: {}", e)))?; + + if atm.ws_send_stream.is_some() { + atm.ws_send_didcomm_message::(&msg, &msg_id) + .await?; + } else { + atm.send_didcomm_message::(&msg, true) + .await?; + } + Ok(()) + } + + /// Waits for the next message to be received via websocket live delivery + /// atm : The ATM SDK to use + /// wait : How long to wait (in milliseconds) for a message before returning None + /// If 0, will block forever until a message is received + /// Returns a tuple of the message and metadata, or None if no message was received + /// NOTE: You still need to delete the message from the server after receiving it + pub async fn live_stream_next( + &self, + atm: &mut ATM<'_>, + wait: Duration, + ) -> Result)>, ATMError> { + let _span = span!(Level::DEBUG, "live_stream_next"); + + async move { + let binding = atm.ws_recv_stream.as_mut(); + let stream = if let Some(stream) = binding { + stream + } else { + return Err(ATMError::TransportError("No websocket recv stream".into())); + }; + + // Send the next request to the ws_handler + if let Some(tx_stream) = &atm.ws_send_stream { + tx_stream.send(WSCommand::Next).await.map_err(|err| { + ATMError::TransportError(format!( + "Could not send message to ws_handler: {:?}", + err + )) + })?; + debug!("sent next request to ws_handler"); + } else { + return Err(ATMError::TransportError("No websocket send stream".into())); + } + + // Setup the timer for the wait, doesn't do anything till `await` is called in the select! macro + let sleep: tokio::time::Sleep = tokio::time::sleep(wait); + + select! { + _ = sleep, if wait.as_millis() > 0 => { + debug!("Timeout reached, no message received"); + Ok(None) + } + value = stream.recv() => { + if let Some(msg) = value { + match msg { + WSCommand::MessageReceived(message, meta) => { + Ok(Some((message, meta))) + } + _ => { + Err(ATMError::MsgReceiveError("Unexpected message type".into())) + } + } + } else { + Ok(None) + } + } + } + } + .instrument(_span) + .await + } + + /// Attempts to retrieve a specific message from the server via websocket live delivery + /// atm : The ATM SDK to use + /// msg_id : The ID of the message to retrieve (matches on either `id` or `pthid`) + /// wait : How long to wait (in milliseconds) for a message before returning None + /// If 0, will not block + /// Returns a tuple of the message and metadata, or None if no message was received + /// NOTE: You still need to delete the message from the server after receiving it + pub async fn live_stream_get( + &self, + atm: &mut ATM<'_>, + msg_id: &str, + wait: Duration, + ) -> Result)>, ATMError> { + let _span = span!(Level::DEBUG, "live_stream_get"); + + async move { + let binding = atm.ws_recv_stream.as_mut(); + let stream = if let Some(stream) = binding { + stream + } else { + return Err(ATMError::TransportError("No websocket stream".into())); + }; + + // Send the get request to the ws_handler + let tx_stream = if let Some(tx_stream) = &atm.ws_send_stream { + tx_stream + .send(WSCommand::Get(msg_id.to_string())) + .await + .map_err(|err| { + ATMError::TransportError(format!( + "Could not send get message to ws_handler: {:?}", + err + )) + })?; + debug!("sent get request to ws_handler"); + tx_stream + } else { + return Err(ATMError::TransportError("No websocket send stream".into())); + }; + + // Setup the timer for the wait, doesn't do anything till `await` is called in the select! macro + let sleep = tokio::time::sleep(wait); + tokio::pin!(sleep); + + loop { + select! { + _ = &mut sleep, if wait.as_millis() > 0 => { + debug!("Timeout reached, no message received"); + tx_stream.send(WSCommand::TimeOut(msg_id.to_string())).await.map_err(|err| { + ATMError::TransportError(format!("Could not send timeout message to ws_handler: {:?}", err)) + })?; + return Ok(None); + } + value = stream.recv() => { + if let Some(msg) = value { + match msg { + WSCommand::MessageReceived(message, meta) => { + return Ok(Some((message, meta))); + } + WSCommand::NotFound => { + // Do nothing, keep waiting + } + _ => { + return Err(ATMError::MsgReceiveError("Unexpected message type".into())); + } + } + } else { + return Ok(None); + } + } + } + } + } + .instrument(_span) + .await + } + + /// Sends a Message Pickup 3.0 `Delivery Request` message + /// atm : The ATM SDK to use + /// recipient_did : Optional, allows you to ask for status for a specific DID. If none, will ask for default DID in ATM + /// mediator_did : Optional, allows you to ask a specific mediator. If none, will ask for default mediator in ATM + /// limit : # of messages to retrieve, defaults to 10 if None + /// wait : Time Duration to wait for a response from websocket. Default (10 Seconds) + pub async fn send_delivery_request( + &self, + atm: &mut ATM<'_>, + recipient_did: Option, + mediator_did: Option, + limit: Option, + wait: Option, + ) -> Result, ATMError> { + let _span = span!(Level::DEBUG, "send_delivery_request",).entered(); + debug!( + "Delivery Request for recipient_did: {:?}, mediator_did: {:?} limit: {:?}", + recipient_did, mediator_did, limit + ); + + let body = MessagePickupDeliveryRequest { + recipient_did: if let Some(recipient) = recipient_did { + recipient + } else { + atm.config.my_did.clone() + }, + limit: if let Some(limit) = limit { limit } else { 10 }, + }; + + let mut msg = Message::build( + Uuid::new_v4().into(), + "https://didcomm.org/messagepickup/3.0/delivery-request".to_owned(), + serde_json::to_value(body).unwrap(), + ) + .header("return_route".into(), Value::String("all".into())); + + let to_did = if let Some(mediator_did) = mediator_did { + mediator_did + } else { + atm.config.atm_did.clone() + }; + msg = msg.to(to_did.clone()); + + msg = msg.from(atm.config.my_did.clone()); + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + let msg = msg.created_time(now).expires_time(now + 300).finalize(); + let msg_id = msg.id.clone(); + + debug!("Delivery-Request message: {:?}", msg); + + // Pack the message + let (msg, _) = msg + .pack_encrypted( + &to_did, + Some(&atm.config.my_did), + Some(&atm.config.my_did), + &atm.did_resolver, + &atm.secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + .map_err(|e| ATMError::MsgSendError(format!("Error packing message: {}", e)))?; + + if atm.ws_send_stream.is_some() { + let wait_duration = if let Some(wait) = wait { + wait + } else { + Duration::from_secs(10) + }; + + atm.ws_send_didcomm_message::(&msg, &msg_id) + .await?; + + if let Some((message, _)) = self.live_stream_get(atm, &msg_id, wait_duration).await? { + debug!("unpacked: {:#?}", message); + } + + // wait for the response from the server + let message = self + .live_stream_get( + atm, + &msg_id, + wait.unwrap_or_else(|| Duration::from_secs(10)), + ) + .await?; + + if let Some((message, _)) = message { + self._handle_delivery(atm, &message).await + } else { + Err(ATMError::MsgSendError("No response from API".into())) + } + } else { + let a = atm + .send_didcomm_message::(&msg, true) + .await?; + + debug!("Response: {:?}", a); + + // Unpack the response + if let SendMessageResponse::RestAPI(Some(InboundMessageResponse::Ephemeral(message))) = + a + { + let (unpacked, _) = atm.unpack(&message).await?; + + debug!("unpacked: {:#?}", unpacked); + + self._handle_delivery(atm, &unpacked).await + } else { + Err(ATMError::MsgSendError("No response from API".into())) + } + } + } + + /// Iterates through each attachment and unpacks each message into an array to return + pub(crate) async fn _handle_delivery( + &self, + atm: &mut ATM<'_>, + message: &Message, + ) -> Result, ATMError> { + let mut response: Vec<(Message, UnpackMetadata)> = Vec::new(); + + if let Some(attachments) = &message.attachments { + for attachment in attachments { + match &attachment.data { + AttachmentData::Base64 { value } => { + let decoded = match BASE64_URL_SAFE_NO_PAD.decode(value.base64.clone()) { + Ok(decoded) => match String::from_utf8(decoded) { + Ok(decoded) => decoded, + Err(e) => { + warn!( + "Error encoding vec[u8] to string: ({:?}). Attachment ID ({:?})", + e, attachment.id + ); + continue; + } + }, + Err(e) => { + warn!( + "Error decoding base64: ({:?}). Attachment ID ({:?})", + e, attachment.id + ); + continue; + } + }; + + match atm.unpack(&decoded).await { + Ok((m, u)) => response.push((m, u)), + Err(e) => { + warn!("Error unpacking message: ({:?})", e); + continue; + } + }; + } + _ => { + warn!("Attachment type not supported: {:?}", attachment.data); + continue; + } + }; + } + } + + Ok(response) + } + + /// Sends a Message Pickup 3.0 `Messages Received` message + /// This effectively deletes the messages from the server + /// atm : The ATM SDK to use + /// recipient_did : Optional, allows you to ask for status for a specific DID. If none, will ask for default DID in ATM + /// mediator_did : Optional, allows you to ask a specific mediator. If none, will ask for default mediator in ATM + /// list : List of messages to delete (message ID's) + /// wait : Time Duration to wait for a response from websocket. Default (10 Seconds) + /// + /// A status reply will be returned if successful + pub async fn send_messages_received( + &self, + atm: &mut ATM<'_>, + recipient_did: Option, + mediator_did: Option, + list: &Vec, + wait: Option, + ) -> Result { + let _span = span!(Level::DEBUG, "send_messages_received",).entered(); + debug!( + "Messages Received for recipient_did: {:?}, mediator_did: {:?}, # msgs to delete: {}", + recipient_did, + mediator_did, + list.len() + ); + + let mut msg = Message::build( + Uuid::new_v4().into(), + "https://didcomm.org/messagepickup/3.0/delivery-request".to_owned(), + json!({"message_id_list": list}), + ) + .header("return_route".into(), Value::String("all".into())); + + let to_did = if let Some(mediator_did) = mediator_did { + mediator_did + } else { + atm.config.atm_did.clone() + }; + msg = msg.to(to_did.clone()); + + msg = msg.from(atm.config.my_did.clone()); + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + let msg = msg.created_time(now).expires_time(now + 300).finalize(); + let msg_id = msg.id.clone(); + + debug!("Delivery-Request message: {:?}", msg); + + // Pack the message + let (msg, _) = msg + .pack_encrypted( + &to_did, + Some(&atm.config.my_did), + Some(&atm.config.my_did), + &atm.did_resolver, + &atm.secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + .map_err(|e| ATMError::MsgSendError(format!("Error packing message: {}", e)))?; + + if atm.ws_send_stream.is_some() { + let wait_duration = if let Some(wait) = wait { + wait + } else { + Duration::from_secs(10) + }; + + atm.ws_send_didcomm_message::(&msg, &msg_id) + .await?; + + if let Some((message, _)) = self.live_stream_get(atm, &msg_id, wait_duration).await? { + debug!("unpacked: {:#?}", message); + } + + // wait for the response from the server + let message = self + .live_stream_get( + atm, + &msg_id, + wait.unwrap_or_else(|| Duration::from_secs(10)), + ) + .await?; + + if let Some((message, _)) = message { + self._parse_status_response(&message).await + } else { + Err(ATMError::MsgSendError("No response from API".into())) + } + } else { + let a = atm + .send_didcomm_message::(&msg, true) + .await?; + + debug!("Response: {:?}", a); + + // Unpack the response + if let SendMessageResponse::RestAPI(Some(InboundMessageResponse::Ephemeral(message))) = + a + { + let (message, _) = atm.unpack(&message).await?; + + debug!("unpacked: {:#?}", message); + + self._parse_status_response(&message).await + } else { + Err(ATMError::MsgSendError("No response from API".into())) + } + } + } +} diff --git a/affinidi-messaging-sdk/src/protocols/mod.rs b/affinidi-messaging-sdk/src/protocols/mod.rs new file mode 100644 index 0000000..42ebb73 --- /dev/null +++ b/affinidi-messaging-sdk/src/protocols/mod.rs @@ -0,0 +1,17 @@ +#[derive(Default)] +pub struct Protocols { + pub message_pickup: message_pickup::MessagePickup, + pub trust_ping: trust_ping::TrustPing, +} + +pub mod message_pickup; +pub mod trust_ping; + +impl Protocols { + pub fn new() -> Protocols { + Protocols { + message_pickup: message_pickup::MessagePickup::default(), + trust_ping: trust_ping::TrustPing::default(), + } + } +} diff --git a/affinidi-messaging-sdk/src/protocols/trust_ping.rs b/affinidi-messaging-sdk/src/protocols/trust_ping.rs new file mode 100644 index 0000000..e6e6ffe --- /dev/null +++ b/affinidi-messaging-sdk/src/protocols/trust_ping.rs @@ -0,0 +1,126 @@ +use std::time::SystemTime; + +use affinidi_messaging_didcomm::{Message, PackEncryptedOptions}; +use serde_json::json; +use sha256::digest; +use tracing::{debug, span, Level}; +use uuid::Uuid; + +use crate::{ + errors::ATMError, + messages::{sending::InboundMessageResponse, EmptyResponse}, + transports::SendMessageResponse, + ATM, +}; + +#[derive(Default)] +pub struct TrustPing {} + +/// Used to construct the response for a TrustPing message +/// - `message_id` - The ID of the message sent +/// - `message_hash` - The sha256 hash of the message sent +/// - `bytes` - The number of bytes sent +pub struct TrustPingSent { + pub message_id: String, + pub message_hash: String, + pub bytes: u32, + pub response: Option, +} + +impl TrustPing { + /// Sends a DIDComm Trust-Ping message + /// - `to_did` - The DID to send the ping to + /// - `signed` - Whether the ping should signed or anonymous? + /// - `expect_response` - Whether a response is expected[^note] + /// + /// Returns: The message ID and sha256 hash of the ping message + /// [^note]: Anonymous pings cannot expect a response, the SDK will automatically set this to false if anonymous is true + pub async fn send_ping<'c>( + &self, + atm: &'c mut ATM<'_>, + to_did: &str, + signed: bool, + expect_response: bool, + ) -> Result { + let _span = span!(Level::DEBUG, "create_ping_message",).entered(); + debug!( + "Pinging {}, signed?({}) response_expected?({})", + to_did, signed, expect_response + ); + + // If an anonymous ping is being sent, we should ensure that expect_response is false + let expect_response = if !signed && expect_response { + debug!("Anonymous pings cannot expect a response, changing to false..."); + false + } else { + expect_response + }; + + let now = SystemTime::now() + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap() + .as_secs(); + + let mut msg = Message::build( + Uuid::new_v4().into(), + "https://didcomm.org/trust-ping/2.0/ping".to_owned(), + json!({"response_requested": expect_response}), + ) + .to(to_did.to_owned()); + + let from_did = if !signed { + // Can support anonymous pings + None + } else { + msg = msg.from(atm.config.my_did.clone()); + Some(atm.config.my_did.clone()) + }; + let msg = msg.created_time(now).expires_time(now + 300).finalize(); + let mut msg_info = TrustPingSent { + message_id: msg.id.clone(), + message_hash: "".to_string(), + bytes: 0, + response: None, + }; + + debug!("Ping message: {:?}", msg); + + // Pack the message + let (msg, _) = msg + .pack_encrypted( + to_did, + from_did.as_deref(), + from_did.as_deref(), + &atm.did_resolver, + &atm.secrets_resolver, + &PackEncryptedOptions::default(), + ) + .await + .map_err(|e| ATMError::MsgSendError(format!("Error packing message: {}", e)))?; + + msg_info.message_hash = digest(&msg).to_string(); + msg_info.bytes = msg.len() as u32; + + if atm.ws_send_stream.is_some() { + atm.ws_send_didcomm_message::(&msg, &msg_info.message_id) + .await?; + } else { + let response = atm + .send_didcomm_message::(&msg, true) + .await?; + msg_info.response = + if let SendMessageResponse::RestAPI(Some(InboundMessageResponse::Stored(m))) = + response + { + if let Some((_, msg_id)) = m.messages.first() { + Some(msg_id.to_owned()) + } else { + None + } + } else { + None + }; + } + Ok(msg_info) + } +} diff --git a/affinidi-messaging-sdk/src/resolvers/mod.rs b/affinidi-messaging-sdk/src/resolvers/mod.rs new file mode 100644 index 0000000..8e2198e --- /dev/null +++ b/affinidi-messaging-sdk/src/resolvers/mod.rs @@ -0,0 +1 @@ +pub(crate) mod secrets_resolver; diff --git a/affinidi-messaging-sdk/src/resolvers/secrets_resolver.rs b/affinidi-messaging-sdk/src/resolvers/secrets_resolver.rs new file mode 100644 index 0000000..dcc2ead --- /dev/null +++ b/affinidi-messaging-sdk/src/resolvers/secrets_resolver.rs @@ -0,0 +1,45 @@ +use affinidi_messaging_didcomm::{ + error::Result, + secrets::{Secret, SecretsResolver}, +}; +use async_trait::async_trait; +use tracing::debug; + +#[derive(Clone, Debug)] +pub struct AffinidiSecrets { + known_secrets: Vec, +} + +impl AffinidiSecrets { + pub fn new(known_secrets: Vec) -> Self { + AffinidiSecrets { known_secrets } + } + + pub fn insert(&mut self, secret: Secret) { + debug!("Adding secret ({})", secret.id); + self.known_secrets.push(secret); + } + + pub fn len(&self) -> usize { + self.known_secrets.len() + } +} + +#[async_trait] +impl SecretsResolver for AffinidiSecrets { + async fn get_secret(&self, secret_id: &str) -> Result> { + Ok(self + .known_secrets + .iter() + .find(|s| s.id == secret_id) + .cloned()) + } + + async fn find_secrets(&self, secret_ids: &[String]) -> Result> { + Ok(secret_ids + .iter() + .filter(|sid| self.known_secrets.iter().any(|s| s.id == sid.to_string())) + .cloned() + .collect()) + } +} diff --git a/affinidi-messaging-sdk/src/transports/http/mod.rs b/affinidi-messaging-sdk/src/transports/http/mod.rs new file mode 100644 index 0000000..9b99b8b --- /dev/null +++ b/affinidi-messaging-sdk/src/transports/http/mod.rs @@ -0,0 +1 @@ +pub mod sending; diff --git a/affinidi-messaging-sdk/src/transports/http/sending.rs b/affinidi-messaging-sdk/src/transports/http/sending.rs new file mode 100644 index 0000000..76eac6e --- /dev/null +++ b/affinidi-messaging-sdk/src/transports/http/sending.rs @@ -0,0 +1,62 @@ +use crate::{ + errors::ATMError, + messages::{GenericDataStruct, SuccessResponse}, + transports::SendMessageResponse, + ATM, +}; +use tracing::{debug, span, Level}; + +impl<'c> ATM<'c> { + /// send_didcomm_message + /// - msg: Packed DIDComm message that we want to send + /// - return_response: Whether to return the response from the API + pub async fn send_didcomm_message( + &mut self, + message: &str, + return_response: bool, + ) -> Result, ATMError> + where + T: GenericDataStruct, + { + let _span = span!(Level::DEBUG, "send_message",).entered(); + let tokens = self.authenticate().await?; + + let msg = message.to_owned(); + + let res = self + .client + .post(format!("{}/inbound", self.config.atm_api)) + .header("Content-Type", "application/json") + .header("Authorization", format!("Bearer {}", tokens.access_token)) + .body(msg) + .send() + .await + .map_err(|e| ATMError::TransportError(format!("Could not send message: {:?}", e)))?; + + let status = res.status(); + debug!("API response: status({})", status); + + let body = res + .text() + .await + .map_err(|e| ATMError::TransportError(format!("Couldn't get body: {:?}", e)))?; + + if !status.is_success() { + return Err(ATMError::TransportError(format!( + "API returned an error: status({}), body({})", + status, body + ))); + } + debug!("body =\n{}", body); + let http_response: Option = if return_response { + let r: SuccessResponse = serde_json::from_str(&body).map_err(|e| { + ATMError::TransportError(format!("Couldn't parse response: {:?}", e)) + })?; + r.data + } else { + None + }; + + Ok(SendMessageResponse::RestAPI(http_response)) + } +} diff --git a/affinidi-messaging-sdk/src/transports/mod.rs b/affinidi-messaging-sdk/src/transports/mod.rs new file mode 100644 index 0000000..4dc81ce --- /dev/null +++ b/affinidi-messaging-sdk/src/transports/mod.rs @@ -0,0 +1,31 @@ +pub mod http; +pub mod websockets; + +/// WebSocketSendResponse is the response from sending a message over a WebSocket connection +/// message_digest is sha256 digest of the message sent +/// bytes_sent is the number of bytes sent +/// message_id is the id of the message sent +#[derive(Debug)] +pub struct WebSocketSendResponse { + pub message_digest: String, + pub bytes_sent: u32, + pub message_id: String, +} + +/// SendMessageResponse is the response from sending a message +/// Allows for returning the response from the API or the WebSocket +#[derive(Debug)] +pub enum SendMessageResponse { + RestAPI(Option), + WebSocket(WebSocketSendResponse), +} + +impl SendMessageResponse { + pub fn get_http_response(&self) -> Option<&T> { + match self { + SendMessageResponse::RestAPI(Some(response)) => Some(response), + SendMessageResponse::WebSocket(_) => None, + _ => None, + } + } +} diff --git a/affinidi-messaging-sdk/src/transports/websockets/mod.rs b/affinidi-messaging-sdk/src/transports/websockets/mod.rs new file mode 100644 index 0000000..1ac844f --- /dev/null +++ b/affinidi-messaging-sdk/src/transports/websockets/mod.rs @@ -0,0 +1,95 @@ +use crate::{config::Config, errors::ATMError, ATM}; +use tokio::sync::mpsc; +use tracing::{debug, error, warn}; +use ws_handler::WSCommand; + +pub mod sending; +pub mod ws_handler; + +impl<'c> ATM<'c> { + /// Starts websocket connection to the ATM API + /// Example: + /// ```ignore + /// use affinidi_messaging_sdk::ATM; + /// use affinidi_messaging_sdk::config::Config; + /// + /// // Configure and create ATM instance + /// let config = Config::builder().build(); + /// let atm = ATM::new(config).await?; + /// + /// // Get a websocket connection (should be mutable as it will be used to send messages) + /// let mut ws = atm.get_websocket().await?; + /// ``` + pub async fn start_websocket_task(&mut self) -> Result<(), ATMError> { + // Some hackery to get around the Rust lifetimes by various items in the SDK + // Create a copy of ATM with owned values + let mut config = Config { + ssl_certificates: Vec::new(), + ..self.config.clone() + }; + + for cert in &self.config.ssl_certificates { + config.ssl_certificates.push(cert.clone().into_owned()) + } + + let mut atm = ATM { + config, + did_resolver: self.did_resolver.clone(), + secrets_resolver: self.secrets_resolver.clone(), + client: self.client.clone(), + authenticated: self.authenticated, + jwt_tokens: self.jwt_tokens.clone(), + ws_connector: self.ws_connector.clone(), + ws_enabled: self.ws_enabled, + ws_handler: None, + ws_send_stream: None, + ws_recv_stream: None, + }; + + error!("secrets: {}", atm.secrets_resolver.len()); + + // Create a new channel with a capacity of at most 32. This communicates from SDK to the websocket handler + let (tx, mut rx) = mpsc::channel::(32); + self.ws_send_stream = Some(tx); + + // Create a new channel with a capacity of at most 32. This communicates from websocket handler to SDK + let (tx2, rx2) = mpsc::channel::(32); + self.ws_recv_stream = Some(rx2); + + // Start the websocket connection + //let mut web_socket = self._create_socket(&mut atm).await?; + //self.ws_websocket = Some(self._create_socket(&mut atm).await?); + // self.ws_websocket = Some(web_socket); + + self.ws_handler = Some(tokio::spawn(async move { + let _ = ATM::ws_handler(&mut atm, &mut rx, &tx2).await; + })); + + if let Some(ws_recv) = self.ws_recv_stream.as_mut() { + // Wait for Started message + if let Some(msg) = ws_recv.recv().await { + match msg { + WSCommand::Started => { + debug!("Websocket connection started"); + } + _ => { + warn!("Unknown message from ws_handler: {:?}", msg); + } + } + } + } + + debug!("Websocket connection and handler started"); + + Ok(()) + } + + /// Close the WebSocket task gracefully + pub async fn abort_websocket_task(&mut self) -> Result<(), ATMError> { + if let Some(channel) = self.ws_send_stream.as_mut() { + let _ = channel.send(WSCommand::Exit).await; + } + + Ok(()) + } +} diff --git a/affinidi-messaging-sdk/src/transports/websockets/sending.rs b/affinidi-messaging-sdk/src/transports/websockets/sending.rs new file mode 100644 index 0000000..b041cc3 --- /dev/null +++ b/affinidi-messaging-sdk/src/transports/websockets/sending.rs @@ -0,0 +1,48 @@ +use crate::{ + errors::ATMError, + transports::{SendMessageResponse, WebSocketSendResponse}, + websockets::ws_handler::WSCommand, + ATM, +}; +use sha256::digest; +use tracing::{debug, span, Level}; + +impl<'c> ATM<'c> { + /// send_didcomm_message + /// - message: Packed DIDComm message that we want to send + /// - message_id: The message ID of the message + pub async fn ws_send_didcomm_message( + &mut self, + message: &str, + message_id: &str, + ) -> Result, ATMError> { + let _span = span!(Level::DEBUG, "send_didcomm_message",).entered(); + + let ws_stream = if let Some(ws_stream) = &self.ws_send_stream { + ws_stream.clone() + } else { + self.start_websocket_task().await?; + self.ws_send_stream.clone().ok_or_else(|| { + ATMError::TransportError("Could not get websocket stream".to_string()) + })? + }; + + let message_digest = digest(message); + + debug!("Sending message: {:?}", message); + + ws_stream + .send(WSCommand::Send(message.to_owned())) + .await + .map_err(|err| { + ATMError::TransportError(format!("Could not send websocket message: {:?}", err)) + })?; + + debug!("Message ({}) sent successfully", message_digest); + Ok(SendMessageResponse::WebSocket(WebSocketSendResponse { + message_digest, + bytes_sent: message.len() as u32, + message_id: message_id.into(), + })) + } +} diff --git a/affinidi-messaging-sdk/src/transports/websockets/ws_handler.rs b/affinidi-messaging-sdk/src/transports/websockets/ws_handler.rs new file mode 100644 index 0000000..ed4b4fc --- /dev/null +++ b/affinidi-messaging-sdk/src/transports/websockets/ws_handler.rs @@ -0,0 +1,336 @@ +use crate::{errors::ATMError, ATM}; +use affinidi_messaging_didcomm::{Message, UnpackMetadata}; +use futures_util::sink::SinkExt; +use http::header::AUTHORIZATION; +use std::{ + collections::{HashMap, HashSet}, + mem::size_of_val, +}; +use tokio::{ + net::TcpStream, + select, + sync::mpsc::{Receiver, Sender}, +}; +use tokio_stream::StreamExt; +use tokio_tungstenite::{ + connect_async_tls_with_config, tungstenite::client::IntoClientRequest, MaybeTlsStream, + WebSocketStream, +}; +use tracing::{debug, error, info, span, warn, Instrument, Level}; + +/// Message cache struct +/// Holds live-stream messages in a cache so we can get the first available or by a specific message ID +#[derive(Default)] +struct MessageCache { + messages: HashMap, // Cache of message data, key is the message ID + thid_lookup: HashMap, // Lookup table for thread ID to message ID + search_list: HashSet, // Search list of message IDs key = ID to look up (could be ID or THID) + ordered_list: Vec, // Ordered list of message IDs in order as they are received + total_count: u32, // Number of messages in cache + total_bytes: u64, // Total size of messages in cache (approx as based on object size) + cache_full: bool, // Flag to state that the cache is full + fetch_cache_limit_count: u32, // Cache limit on # of messages + fetch_cache_limit_bytes: u64, // Cache limit on total size of messages + next_flag: bool, // Used to state that next() was called on an empty cache +} + +impl MessageCache { + fn insert(&mut self, message: Message, meta: UnpackMetadata) { + self.messages + .insert(message.id.clone(), (message.clone(), meta)); + self.ordered_list.push(message.id.clone()); + self.total_count += 1; + self.total_bytes += size_of_val(&message) as u64; + if self.total_count > self.fetch_cache_limit_count + || self.total_bytes > self.fetch_cache_limit_bytes + { + self.cache_full = true; + } + if let Some(thid) = message.thid { + self.thid_lookup.insert(thid, message.id.clone()); + } + debug!( + "Message inserted into cache: id({}) cached_count({})", + message.id, self.total_count + ); + } + + /// Get the next message from the cache + fn next(&mut self) -> Option<(Message, UnpackMetadata)> { + if self.ordered_list.is_empty() { + self.next_flag = true; + return None; + } + + // Get the message ID of the first next message + let id = self.ordered_list.remove(0); + + self.remove(&id) + } + + /// Can we find a specific message in the cache? + /// If not, then we add it to the search list to look up later as messages come in (within the duration of the original get request) + fn get(&mut self, msg_id: &str) -> Option<(Message, UnpackMetadata)> { + let r = if let Some((message, meta)) = self.messages.get(msg_id) { + Some((message.clone(), meta.clone())) + } else if let Some(id) = self.thid_lookup.get(msg_id) { + if let Some((message, meta)) = self.messages.get(id) { + Some((message.clone(), meta.clone())) + } else { + warn!( + "thid_lookup found message ID ({}) but message id ({}) not found in cache", + msg_id, id + ); + None + } + } else { + debug!( + "Message ID ({}) not found in cache, adding to search list", + msg_id + ); + self.search_list.insert(msg_id.to_string()); + None + }; + + // Remove the message from cache if it was found + if let Some((message, _)) = &r { + self.remove(&message.id); + } + r + } + + fn remove(&mut self, msg_id: &str) -> Option<(Message, UnpackMetadata)> { + // remove the message from thh ordered list + if let Some(pos) = self.ordered_list.iter().position(|r| r == msg_id) { + self.ordered_list.remove(pos); + } + + // Remove from search list + self.search_list.remove(msg_id); + + // Get the message and metadata from the cache + let (message, meta) = if let Some((message, meta)) = self.messages.remove(msg_id) { + // Remove this from thid_lookup if it exists + if let Some(thid) = &message.thid { + self.thid_lookup.remove(thid); + } + + (message, meta) + } else { + return None; + }; + + self.total_count -= 1; + self.total_bytes -= size_of_val(&message) as u64; + + // reset cache_full flag + if self.cache_full + && (self.total_count <= self.fetch_cache_limit_count + && self.total_bytes <= self.fetch_cache_limit_bytes) + { + self.cache_full = false; + } + + Some((message, meta)) + } + + /// Is the cache full based on limits? + fn is_full(&self) -> bool { + self.cache_full + } +} + +#[derive(Debug)] +pub(crate) enum WSCommand { + Started, // Signals that the websocket handler has started + Exit, // Exits the websocket handler + Send(String), // Sends the message string to the websocket + Next, // Gets the next message from the cache + Get(String), // Gets the message with the specified ID from the cache + MessageReceived(Message, Box), // Message received from the websocket + NotFound, // Message not found in the cache + TimeOut(String), // SDK request timed out, contains msg_id we were looking for +} + +impl<'c> ATM<'c> { + pub(crate) async fn _create_socket( + &mut self, + //atm: &mut ATM<'_>, + ) -> Result>, ATMError> { + // Check if authenticated + let tokens = self.authenticate().await?; + + debug!("Creating websocket connection"); + // Create a custom websocket request, turn this into a client_request + // Allows adding custom headers later + let mut request = self + .config + .atm_api_ws + .clone() + .into_client_request() + .map_err(|e| { + ATMError::TransportError(format!( + "Could not create websocket request. Reason: {}", + e + )) + })?; + + // Add the Authorization header to the request + let headers = request.headers_mut(); + headers.insert( + AUTHORIZATION, + format!("Bearer {}", tokens.access_token) + .parse() + .map_err(|e| { + ATMError::TransportError(format!("Could not set Authorization header: {:?}", e)) + })?, + ); + + // Connect to the websocket + let (ws_stream, _) = + connect_async_tls_with_config(request, None, false, Some(self.ws_connector.clone())) + .await + .expect("Failed to connect"); + + debug!("Completed websocket connection"); + + Ok(ws_stream) + } + + /// WebSocket streaming handler + /// from_sdk is an MPSC channel used to receive messages from the main thread that will be sent to the websocket + /// to_sdk is an MPSC channel used to send messages to the main thread that were received from the websocket + /// web_socket is the websocket stream itself which can be used to send and receive messages + pub(crate) async fn ws_handler( + atm: &mut ATM<'_>, + from_sdk: &mut Receiver, + to_sdk: &Sender, + // web_socket: &mut WebSocketStream>, + ) -> Result<(), ATMError> { + let _span = span!(Level::INFO, "ws_handler"); + async move { + debug!("Starting websocket handler"); + + // Set up a message cache + let mut cache = MessageCache { + fetch_cache_limit_count: atm.config.fetch_cache_limit_count, + fetch_cache_limit_bytes: atm.config.fetch_cache_limit_bytes, + ..Default::default() + }; + + let mut web_socket = atm._create_socket().await?; + to_sdk.send(WSCommand::Started).await.map_err(|err| { + ATMError::TransportError(format!("Could not send message to SDK: {:?}", err)) + })?; + loop { + select! { + value = web_socket.next(), if !cache.is_full() => { + if let Some(msg) = value { + if let Ok(payload) = msg { + if payload.is_text() { + if let Ok(msg) = payload.to_text() { + debug!("Received text message ({})", msg); + let (message, meta) = match atm.unpack(msg).await { + Ok((msg, meta)) => (msg, meta), + Err(err) => { + error!("Error unpacking message: {:?}", err); + continue; + } + }; + // Check if we are searching for this message via a get request + if let Some(thid) = &message.thid { + if cache.search_list.contains(thid) { + cache.remove(thid); + to_sdk.send(WSCommand::MessageReceived(message.clone(), Box::new(meta.clone()))).await.map_err(|err| { + ATMError::TransportError(format!("Could not send message to SDK: {:?}", err)) + })?; + } + } else if cache.search_list.contains(&message.id) { + to_sdk.send(WSCommand::MessageReceived(message.clone(), Box::new(meta.clone()))).await.map_err(|err| { + ATMError::TransportError(format!("Could not send message to SDK: {:?}", err)) + })?; + cache.remove(&message.id); + } + + // Send the message to the SDK if next_flag is set + if cache.next_flag { + cache.next_flag = false; + to_sdk.send(WSCommand::MessageReceived(message, Box::new(meta))).await.map_err(|err| { + ATMError::TransportError(format!("Could not send message to SDK: {:?}", err)) + })?; + } else { + // Add to cache + cache.insert(message, meta); + } + } else { + error!("Error getting text from message") + } + } else { + error!("Received non-text message"); + } + } else { + error!("Error getting payload from message"); + continue; + } + } else { + + error!("Error getting message"); + break; + } + } + value = from_sdk.recv() => { + if let Some(cmd) = value { + match cmd { + WSCommand::Send(msg) => { + debug!("Sending message: {}", msg); + web_socket.send(msg.into()).await.map_err(|err| { + ATMError::TransportError(format!("Could not send websocket message: {:?}", err)) + })?; + } + WSCommand::Next => { + if let Some((message, meta)) = cache.next() { + to_sdk.send(WSCommand::MessageReceived(message, Box::new(meta))).await.map_err(|err| { + ATMError::TransportError(format!("Could not send message to SDK: {:?}", err)) + })?; + } + } + WSCommand::Get(id) => { + if let Some((message, meta)) = cache.get(&id) { + to_sdk.send(WSCommand::MessageReceived(message, Box::new(meta))).await.map_err(|err| { + ATMError::TransportError(format!("Could not send message to SDK: {:?}", err)) + })?; + } else { + to_sdk.send(WSCommand::NotFound).await.map_err(|err| { + ATMError::TransportError(format!("Could not send message to SDK: {:?}", err)) + })?; + } + } + WSCommand::Exit => { + debug!("Received EXIT message, closing channel"); + break; + } + WSCommand::TimeOut(id) => { + cache.search_list.remove(&id); + } + _ => { + debug!("Received unknown command"); + } + } + } else { + info!("Channel Closed"); + break; + } + } + } + } + + let _ = web_socket.close(None).await; + from_sdk.close(); + + debug!("Channel closed, stopping websocket handler"); + Ok(()) + } + .instrument(_span) + .await + } +} diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..5145c55 --- /dev/null +++ b/deny.toml @@ -0,0 +1,64 @@ +[advisories] +version = 2 +ignore = [ + { id = "RUSTSEC-2022-0092", reason = "askalono always provides valid utf-8 files from a cache, this is not relevant" }, +] + +[bans] +multiple-versions = "allow" + +[licenses] +# We want really high confidence when inferring licenses from text +confidence-threshold = 0.93 +allow = [ + "Apache-1.1", + "Apache-2.0", + "BSL-1.0", + "BSD-1-Clause", + "BSD-2-Clause", + "BSD-3-Clause", + "0BSD", + "CC0-1.0", + "WTFPL", + "Fair", + "Intel", + "ISC", + "MIT-0", + "MIT", + "MIT-Modern-Variant", + "MulanPSL-2.0", + "Multics", + "Naumen", + "PHP-3.01", + "PostgreSQL", + "Python-2.0", + "OFL-1.1", + "MirOS", + "Unlicense", + "Unicode-DFS-2016", + "UPL-1.0", + "NCSA", + "Zlib", + "AFL-2.1", + "OpenSSL", + "ISC", + "MPL-2.0", + "W3C-20150513", +] + +exceptions = [ + { allow = ["OpenSSL"], crate = "ring" }, +] + +# Sigh +[[licenses.clarify]] +crate = "ring" +# SPDX considers OpenSSL to encompass both the OpenSSL and SSLeay licenses +# https://spdx.org/licenses/OpenSSL.html +# ISC - Both BoringSSL and ring use this for their new files +# MIT - "Files in third_party/ have their own licenses, as described therein. The MIT +# license, for third_party/fiat, which, unlike other third_party directories, is +# compiled into non-test libraries, is included below." +# OpenSSL - Obviously +expression = "ISC AND MIT AND OpenSSL" +license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }]