From 9d88c8241e2c08690839c75f6e3e85a1aba49e60 Mon Sep 17 00:00:00 2001 From: Stefan Steinert Date: Tue, 9 Jun 2020 11:25:09 +0200 Subject: [PATCH 01/76] could not count to ten properly (#262) --- scripts/systemd/nym-mixnode.service | 44 ++++++++++++++--------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/scripts/systemd/nym-mixnode.service b/scripts/systemd/nym-mixnode.service index 49b054f407..0507624952 100644 --- a/scripts/systemd/nym-mixnode.service +++ b/scripts/systemd/nym-mixnode.service @@ -10,28 +10,28 @@ # However keep in mind that more often than not the defaults work very well. # # Directions of use (description and example command) -# 1. Create a "nym" user and group (example for Debian): -# sudo useradd -U -m -s /sbin/nologin nym -# 2. Start a new bash session with your nym user: -# sudo -u nym bash -# 3. Change to the users home directory: -# cd -# 4. Obtain the current mixnode binary from the nym release page and make it -# executable: -# curl -LO https://github.com/nymtech/nym/releases/download/v0.7.0/nym-mixnode_linux_x86_64 -# chmod 755 nym-mixnode_linux_x86_64 -# 6. Initialize the mixnode config (this assumes a simple IPv4 setup): -# ./nym-mixnode_linux_x86_64 init --id iamboss -layer 2 --host $(curl -sS v4.icanhazip.com) -# 7. Give it a try. The mixnode should run in the foreground until you -# terminate it with Ctrl-C: -# ./nym-mixnode_linux_x86_64 run --id iamboss -# 8. Exit from your nym shell: -# exit -# 9. Copy this file over to /etc/systemd/system/nym-mixnode.service. -# 4. Enable the service so that it will autostart at boot-time: -# sudo systemctl enable nym-mixnode -# 3. Start the nym service: -# sudo systemctl start nym-mixnode +# 1. Create a "nym" user and group (example for Debian): +# sudo useradd -U -m -s /sbin/nologin nym +# 2. Start a new bash session with your nym user: +# sudo -u nym bash +# 3. Change to the users home directory: +# cd +# 4. Obtain the current mixnode binary from the nym release page and make it +# executable: +# curl -LO https://github.com/nymtech/nym/releases/download/v0.7.0/nym-mixnode_linux_x86_64 +# chmod 755 nym-mixnode_linux_x86_64 +# 5. Initialize the mixnode config (this assumes a simple IPv4 setup): +# ./nym-mixnode_linux_x86_64 init --id iamboss -layer 2 --host $(curl -sS v4.icanhazip.com) +# 6. Give it a try. The mixnode should run in the foreground until you +# terminate it with Ctrl-C: +# ./nym-mixnode_linux_x86_64 run --id iamboss +# 7. Exit from your nym shell: +# exit +# 8. Copy this file over to /etc/systemd/system/nym-mixnode.service. +# 9. Enable the service so that it will autostart at boot-time: +# sudo systemctl enable nym-mixnode +# 10. Start the nym service: +# sudo systemctl start nym-mixnode [Unit] Description=nym mixnode service From 169c995e952a10ed6597f707284355a024474e13 Mon Sep 17 00:00:00 2001 From: Stefan Steinert Date: Mon, 15 Jun 2020 16:53:00 +0200 Subject: [PATCH 02/76] Removed misplaced WorkingDirectory parameter (#264) --- scripts/systemd/nym-mixnode.service | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/systemd/nym-mixnode.service b/scripts/systemd/nym-mixnode.service index 0507624952..2d76cb9e8e 100644 --- a/scripts/systemd/nym-mixnode.service +++ b/scripts/systemd/nym-mixnode.service @@ -41,7 +41,6 @@ After=network.target Type=simple User=nym Group=nym -WorkingDirectory=/home/nym ExecStart=/home/nym/nym-mixnode_linux_x86_64 run --id iamboss Restart=on-abort From db5540677c144203c4947e15d4a1fdc37d9abd0b Mon Sep 17 00:00:00 2001 From: hyperfekt Date: Mon, 15 Jun 2020 16:53:52 +0200 Subject: [PATCH 03/76] add disabling feature 'offline-test' for network-dependent tests (#260) --- clients/webassembly/Cargo.toml | 1 + clients/webassembly/src/lib.rs | 2 ++ clients/webassembly/src/models/mod.rs | 1 + common/client-libs/directory-client/Cargo.toml | 3 +++ common/client-libs/directory-client/src/presence/topology.rs | 1 + 5 files changed, 8 insertions(+) diff --git a/clients/webassembly/Cargo.toml b/clients/webassembly/Cargo.toml index cedd92610c..596ea4329c 100644 --- a/clients/webassembly/Cargo.toml +++ b/clients/webassembly/Cargo.toml @@ -12,6 +12,7 @@ crate-type = ["cdylib", "rlib"] [features] default = ["console_error_panic_hook"] +offline-test = [] [dependencies] log = "0.4" diff --git a/clients/webassembly/src/lib.rs b/clients/webassembly/src/lib.rs index 521e29f57b..c69045983e 100644 --- a/clients/webassembly/src/lib.rs +++ b/clients/webassembly/src/lib.rs @@ -149,6 +149,7 @@ mod test_constructing_a_sphinx_packet { // } #[test] + #[cfg_attr(feature = "offline-test", ignore)] fn starts_with_a_mix_address() { let mut payload = create_sphinx_packet( topology_fixture(), @@ -225,6 +226,7 @@ mod building_a_topology_from_json { } #[test] + #[cfg_attr(feature = "offline-test", ignore)] fn test_works_on_happy_json() { let route = sphinx_route_to( topology_fixture(), diff --git a/clients/webassembly/src/models/mod.rs b/clients/webassembly/src/models/mod.rs index 177d3a0e08..3f0c4e779b 100644 --- a/clients/webassembly/src/models/mod.rs +++ b/clients/webassembly/src/models/mod.rs @@ -168,6 +168,7 @@ mod converting_mixnode_presence_into_topology_mixnode { } #[test] + #[cfg_attr(feature = "offline-test", ignore)] fn it_returns_resolved_ip_on_resolvable_hostname() { let resolvable_hostname = "nymtech.net:1234"; diff --git a/common/client-libs/directory-client/Cargo.toml b/common/client-libs/directory-client/Cargo.toml index 73fbbb9a16..1b358c8a63 100644 --- a/common/client-libs/directory-client/Cargo.toml +++ b/common/client-libs/directory-client/Cargo.toml @@ -6,6 +6,9 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +offline-test = [] + [dependencies] log = "0.4" futures = "0.3" diff --git a/common/client-libs/directory-client/src/presence/topology.rs b/common/client-libs/directory-client/src/presence/topology.rs index a2bc5ca862..169415bc84 100644 --- a/common/client-libs/directory-client/src/presence/topology.rs +++ b/common/client-libs/directory-client/src/presence/topology.rs @@ -115,6 +115,7 @@ mod converting_mixnode_presence_into_topology_mixnode { } #[test] + #[cfg_attr(feature = "offline-test", ignore)] fn it_returns_resolved_ip_on_resolvable_hostname() { let resolvable_hostname = "nymtech.net:1234"; From b57a54835007b57de0bd88ebc6dd78e24f0fecf6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 16 Jun 2020 11:22:02 +0100 Subject: [PATCH 04/76] Feature/packet retransmission (#263) * Ability to send sphinx packets of different sizes + more efficient decoding * Closing connection on connection corruption * Missing semicolons * Missing license notices * Default for packetsize * Split nymsphinx * Replaced Mutex with RwLock for TopologyAccessor + impl Deref * Sphinx update + import cleanup * Moved packet_sizes file * Updated NymTopology API * sphinx version bump * Missing license notice * nymsphinx-params crate * Changes due to ibid. * Chunking rework to allow variable size fragments * Initial ack crate * Version bump to new dev build * Cargo lock changes * random_route_to_gateway by node address * exposing getting read permit * Very initial draft on ack control * Correctly dereferencing out of topology read permit * All pending changes + compilation todo!s * Restricted scope of deref on TopologyAccessorInner * Type path alias for generate_key * Derived traits for MessageChunker * Ack control starting to take shape! * Awaiting callbacks * Most of work done on acks. Now to wire it all together * Import cleanup * rng generalization * Connected real traffic together; only acks from gateway left * Removed redundant things from nymsphinx::utils * nymsphinx-cover crate * Ack-related fields in client config * Decreased packet store log level * Restored forward sphinx request * Slight adjustements to surb acks * Changed TopologyReadPermit from type alias into a struct * Changes due to ibid. * Sphinx version upgrade * Gateway being able to understand and handle acks * Special Cover FragmentIdentifier + removal of dead code * Initial packet router for gateway client * Kill client if it fails to send to gateway too many times * Cover messages with acks * Moved out gateway client errors * Ignoring cover traffic acks * Changes in ack control * Another sphinx version upgrade * websocket handler delegating message chunking * Using config defined ack wait additions * Other minor changes I should have been more dilligent with splitting * Import path fix * sphinx_receiver => mixnet_receiver * Missing renamed variable instance * Updated aes-ctr to 0.4.0 * Removed concept of 'unfragmented' single fragment * Replay fragments detection * Long method split * typo * Cleaner client init * Fixed race condition * Fixed similar issue for retransmission * Cargo fmt * Minor clenaup --- Cargo.lock | 187 ++- Cargo.toml | 7 + clients/native/Cargo.toml | 3 +- .../native/src/client/cover_traffic_stream.rs | 120 +- clients/native/src/client/mix_traffic.rs | 21 +- clients/native/src/client/mod.rs | 119 +- .../acknowledgement_listener.rs | 86 ++ .../input_message_listener.rs | 127 ++ .../acknowlegement_control/mod.rs | 240 ++++ .../retransmission_request_listener.rs | 128 ++ .../sent_notification_listener.rs | 99 ++ .../src/client/real_messages_control/mod.rs | 170 +++ .../real_traffic_stream.rs | 250 ++++ .../native/src/client/real_traffic_stream.rs | 189 --- clients/native/src/client/received_buffer.rs | 87 +- clients/native/src/client/topology_control.rs | 117 +- clients/native/src/commands/init.rs | 15 +- clients/native/src/config/mod.rs | 35 + clients/native/src/config/template.rs | 1 + clients/native/src/websocket/handler.rs | 19 +- clients/webassembly/src/lib.rs | 5 +- .../client-libs/gateway-client/src/error.rs | 75 ++ common/client-libs/gateway-client/src/lib.rs | 166 +-- .../gateway-client/src/packet_router.rs | 83 ++ common/healthcheck/src/path_check.rs | 1 - common/nymsphinx/Cargo.toml | 12 +- common/nymsphinx/acknowledgements/Cargo.toml | 19 + .../acknowledgements/src/identifier.rs | 103 ++ .../src/lib.rs} | 12 +- .../acknowledgements/src/surb_ack.rs | 119 ++ common/nymsphinx/addressing/Cargo.toml | 10 + .../addressing => addressing/src}/clients.rs | 6 +- .../mod.rs => addressing/src/lib.rs} | 0 .../addressing => addressing/src}/nodes.rs | 30 +- common/nymsphinx/chunking/Cargo.toml | 17 + common/nymsphinx/chunking/src/fragment.rs | 1100 ++++++++++++++++ common/nymsphinx/chunking/src/lib.rs | 240 ++++ .../src}/reconstruction.rs | 1047 +++++++++++---- .../{src/chunking => chunking/src}/set.rs | 539 ++++---- common/nymsphinx/cover/Cargo.toml | 17 + common/nymsphinx/cover/src/lib.rs | 116 ++ common/nymsphinx/framing/Cargo.toml | 14 + .../framing/mod.rs => framing/src/lib.rs} | 4 +- common/nymsphinx/params/Cargo.toml | 10 + common/nymsphinx/params/src/lib.rs | 15 + .../packets.rs => params/src/packet_sizes.rs} | 23 +- common/nymsphinx/src/chunking/fragment.rs | 1157 ----------------- common/nymsphinx/src/chunking/mod.rs | 116 -- common/nymsphinx/src/lib.rs | 34 +- common/nymsphinx/src/utils/encapsulation.rs | 76 -- common/nymsphinx/src/utils/mod.rs | 35 +- common/nymsphinx/types/Cargo.toml | 11 + common/nymsphinx/types/src/lib.rs | 27 + common/topology/Cargo.toml | 3 +- common/topology/src/gateway.rs | 6 +- common/topology/src/lib.rs | 27 +- common/topology/src/mix.rs | 6 +- common/topology/src/provider.rs | 6 +- gateway/Cargo.toml | 2 +- gateway/gateway-requests/src/types.rs | 23 +- .../receiver/connection_handler.rs | 1 + .../receiver/packet_processing.rs | 139 +- gateway/src/node/mod.rs | 9 +- gateway/src/node/storage/inboxes.rs | 5 +- mixnode/Cargo.toml | 2 +- validator/Cargo.toml | 2 +- 66 files changed, 5097 insertions(+), 2393 deletions(-) create mode 100644 clients/native/src/client/real_messages_control/acknowlegement_control/acknowledgement_listener.rs create mode 100644 clients/native/src/client/real_messages_control/acknowlegement_control/input_message_listener.rs create mode 100644 clients/native/src/client/real_messages_control/acknowlegement_control/mod.rs create mode 100644 clients/native/src/client/real_messages_control/acknowlegement_control/retransmission_request_listener.rs create mode 100644 clients/native/src/client/real_messages_control/acknowlegement_control/sent_notification_listener.rs create mode 100644 clients/native/src/client/real_messages_control/mod.rs create mode 100644 clients/native/src/client/real_messages_control/real_traffic_stream.rs delete mode 100644 clients/native/src/client/real_traffic_stream.rs create mode 100644 common/client-libs/gateway-client/src/error.rs create mode 100644 common/client-libs/gateway-client/src/packet_router.rs create mode 100644 common/nymsphinx/acknowledgements/Cargo.toml create mode 100644 common/nymsphinx/acknowledgements/src/identifier.rs rename common/nymsphinx/{src/utils/poisson.rs => acknowledgements/src/lib.rs} (55%) create mode 100644 common/nymsphinx/acknowledgements/src/surb_ack.rs create mode 100644 common/nymsphinx/addressing/Cargo.toml rename common/nymsphinx/{src/addressing => addressing/src}/clients.rs (95%) rename common/nymsphinx/{src/addressing/mod.rs => addressing/src/lib.rs} (100%) rename common/nymsphinx/{src/addressing => addressing/src}/nodes.rs (89%) create mode 100644 common/nymsphinx/chunking/Cargo.toml create mode 100644 common/nymsphinx/chunking/src/fragment.rs create mode 100644 common/nymsphinx/chunking/src/lib.rs rename common/nymsphinx/{src/chunking => chunking/src}/reconstruction.rs (55%) rename common/nymsphinx/{src/chunking => chunking/src}/set.rs (55%) create mode 100644 common/nymsphinx/cover/Cargo.toml create mode 100644 common/nymsphinx/cover/src/lib.rs create mode 100644 common/nymsphinx/framing/Cargo.toml rename common/nymsphinx/{src/framing/mod.rs => framing/src/lib.rs} (97%) create mode 100644 common/nymsphinx/params/Cargo.toml create mode 100644 common/nymsphinx/params/src/lib.rs rename common/nymsphinx/{src/packets.rs => params/src/packet_sizes.rs} (80%) delete mode 100644 common/nymsphinx/src/chunking/fragment.rs delete mode 100644 common/nymsphinx/src/chunking/mod.rs delete mode 100644 common/nymsphinx/src/utils/encapsulation.rs create mode 100644 common/nymsphinx/types/Cargo.toml create mode 100644 common/nymsphinx/types/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 669d9d7883..c004f43b3d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,10 +29,22 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2e5b0458ea3beae0d1d8c0f3946564f8e10f90646cf78c06b4351052058d1ee" dependencies = [ - "aes-soft", - "aesni", - "ctr", - "stream-cipher", + "aes-soft 0.3.3", + "aesni 0.6.0", + "ctr 0.3.2", + "stream-cipher 0.3.2", +] + +[[package]] +name = "aes-ctr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e60aeefd2a0243bd53a42e92444e039f67c3d7f0382c9813577696e7c10bf3" +dependencies = [ + "aes-soft 0.4.0", + "aesni 0.7.0", + "ctr 0.4.0", + "stream-cipher 0.4.1", ] [[package]] @@ -46,6 +58,17 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "aes-soft" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4925647ee64e5056cf231608957ce7c81e12d6d6e316b9ce1404778cc1d35fa7" +dependencies = [ + "block-cipher", + "byteorder", + "opaque-debug", +] + [[package]] name = "aesni" version = "0.6.0" @@ -54,7 +77,18 @@ checksum = "2f70a6b5f971e473091ab7cfb5ffac6cde81666c4556751d8d5620ead8abf100" dependencies = [ "block-cipher-trait", "opaque-debug", - "stream-cipher", + "stream-cipher 0.3.2", +] + +[[package]] +name = "aesni" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050d39b0b7688b3a3254394c3e30a9d66c41dcf9b05b0e2dbdc623f6505d264" +dependencies = [ + "block-cipher", + "opaque-debug", + "stream-cipher 0.4.1", ] [[package]] @@ -212,7 +246,16 @@ dependencies = [ "block-padding", "byte-tools", "byteorder", - "generic-array", + "generic-array 0.12.3", +] + +[[package]] +name = "block-cipher" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa136449e765dc7faa244561ccae839c394048667929af599b5d931ebe7b7f10" +dependencies = [ + "generic-array 0.14.1", ] [[package]] @@ -221,7 +264,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c924d49bd09e7c06003acda26cd9742e796e34282ec6c1189404dee0c1f4774" dependencies = [ - "generic-array", + "generic-array 0.12.3", ] [[package]] @@ -514,7 +557,7 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4434400df11d95d556bac068ddfedd482915eb18fe8bea89bc80b6e4b1c179e5" dependencies = [ - "generic-array", + "generic-array 0.12.3", "subtle 1.0.0", ] @@ -525,7 +568,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "022cd691704491df67d25d006fe8eca083098253c4d43516c2206479c58c6736" dependencies = [ "block-cipher-trait", - "stream-cipher", + "stream-cipher 0.3.2", +] + +[[package]] +name = "ctr" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3592740fd55aaf61dd72df96756bd0d11e6037b89dcf30ae2e1895b267692be" +dependencies = [ + "stream-cipher 0.4.1", ] [[package]] @@ -575,7 +627,7 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" dependencies = [ - "generic-array", + "generic-array 0.12.3", ] [[package]] @@ -929,6 +981,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "generic-array" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2664c2cf08049036f31015b04c6ac3671379a1d86f52ed2416893f16022deb" +dependencies = [ + "typenum", +] + [[package]] name = "getrandom" version = "0.1.14" @@ -1667,7 +1728,7 @@ dependencies = [ [[package]] name = "nym-client" -version = "0.7.0" +version = "0.8.0-dev" dependencies = [ "bs58", "built", @@ -1687,6 +1748,7 @@ dependencies = [ "pem", "pemstore", "pretty_env_logger", + "rand 0.7.3", "reqwest 0.9.24", "serde", "serde_json", @@ -1717,7 +1779,7 @@ dependencies = [ [[package]] name = "nym-gateway" -version = "0.7.0" +version = "0.8.0-dev" dependencies = [ "built", "clap", @@ -1747,7 +1809,7 @@ dependencies = [ [[package]] name = "nym-mixnode" -version = "0.7.0" +version = "0.8.0-dev" dependencies = [ "bs58", "built", @@ -1773,7 +1835,7 @@ dependencies = [ [[package]] name = "nym-validator" -version = "0.7.0" +version = "0.8.0-dev" dependencies = [ "abci", "bodyparser", @@ -1803,14 +1865,86 @@ dependencies = [ name = "nymsphinx" version = "0.1.0" dependencies = [ - "bytes 0.5.4", - "log 0.4.8", + "nymsphinx-acknowledgements", + "nymsphinx-addressing", + "nymsphinx-chunking", + "nymsphinx-cover", + "nymsphinx-framing", + "nymsphinx-params", + "nymsphinx-types", "rand 0.7.3", "rand_distr", - "sphinx", +] + +[[package]] +name = "nymsphinx-acknowledgements" +version = "0.1.0" +dependencies = [ + "aes-ctr 0.4.0", + "nymsphinx-addressing", + "nymsphinx-params", + "nymsphinx-types", + "rand 0.7.3", + "topology", +] + +[[package]] +name = "nymsphinx-addressing" +version = "0.1.0" +dependencies = [ + "nymsphinx-types", +] + +[[package]] +name = "nymsphinx-chunking" +version = "0.1.0" +dependencies = [ + "log 0.4.8", + "nymsphinx-acknowledgements", + "nymsphinx-addressing", + "nymsphinx-params", + "nymsphinx-types", + "rand 0.7.3", + "topology", +] + +[[package]] +name = "nymsphinx-cover" +version = "0.1.0" +dependencies = [ + "nymsphinx-acknowledgements", + "nymsphinx-addressing", + "nymsphinx-chunking", + "nymsphinx-params", + "nymsphinx-types", + "rand 0.7.3", + "topology", +] + +[[package]] +name = "nymsphinx-framing" +version = "0.1.0" +dependencies = [ + "bytes 0.5.4", + "nymsphinx-params", + "nymsphinx-types", "tokio-util", ] +[[package]] +name = "nymsphinx-params" +version = "0.1.0" +dependencies = [ + "nymsphinx-types", +] + +[[package]] +name = "nymsphinx-types" +version = "0.1.0" +dependencies = [ + "sphinx", +] + [[package]] name = "opaque-debug" version = "0.2.3" @@ -2755,9 +2889,9 @@ dependencies = [ [[package]] name = "sphinx" version = "0.1.0" -source = "git+https://github.com/nymtech/sphinx?rev=fcd17932acbd21a76c42a4bf2a42b9f8668f7e1f#fcd17932acbd21a76c42a4bf2a42b9f8668f7e1f" +source = "git+https://github.com/nymtech/sphinx?rev=cd9f09b85de33c60030ba6d7fae613c42cbe1faf#cd9f09b85de33c60030ba6d7fae613c42cbe1faf" dependencies = [ - "aes-ctr", + "aes-ctr 0.3.0", "arrayref", "blake2", "bs58", @@ -2780,7 +2914,17 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8131256a5896cabcf5eb04f4d6dacbe1aefda854b0d9896e09cb58829ec5638c" dependencies = [ - "generic-array", + "generic-array 0.12.3", +] + +[[package]] +name = "stream-cipher" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f8ed9974042b8c3672ff3030a69fcc03b74c47c3d1ecb7755e8a3626011e88" +dependencies = [ + "block-cipher", + "generic-array 0.14.1", ] [[package]] @@ -3130,7 +3274,8 @@ dependencies = [ "futures 0.3.4", "itertools", "log 0.4.8", - "nymsphinx", + "nymsphinx-addressing", + "nymsphinx-types", "pretty_env_logger", "rand 0.7.3", "serde", diff --git a/Cargo.toml b/Cargo.toml index cecc3be58f..3bc05a60bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,13 @@ members = [ "common/crypto", "common/healthcheck", "common/nymsphinx", + "common/nymsphinx/acknowledgements", + "common/nymsphinx/addressing", + "common/nymsphinx/chunking", + "common/nymsphinx/cover", + "common/nymsphinx/framing", + "common/nymsphinx/params", + "common/nymsphinx/types", "common/pemstore", "common/topology", "gateway", diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index 6176f343c2..ed7662676a 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -1,7 +1,7 @@ [package] build = "build.rs" name = "nym-client" -version = "0.7.0" +version = "0.8.0-dev" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] edition = "2018" @@ -21,6 +21,7 @@ futures = "0.3.1" log = "0.4" pem = "0.7.0" pretty_env_logger = "0.3" +rand = {version = "0.7.3", features = ["wasm-bindgen"]} reqwest = "0.9.22" serde = { version = "1.0.104", features = ["derive"] } serde_json = "1.0.44" diff --git a/clients/native/src/client/cover_traffic_stream.rs b/clients/native/src/client/cover_traffic_stream.rs index 5354e0c9aa..4c3fe3b9fb 100644 --- a/clients/native/src/client/cover_traffic_stream.rs +++ b/clients/native/src/client/cover_traffic_stream.rs @@ -17,25 +17,58 @@ use crate::client::topology_control::TopologyAccessor; use futures::task::{Context, Poll}; use futures::{Future, Stream, StreamExt}; use log::*; +use nymsphinx::acknowledgements::identifier::AckAes128Key; use nymsphinx::addressing::clients::Recipient; -use nymsphinx::utils::{encapsulation, poisson}; +use nymsphinx::cover::generate_loop_cover_packet; +use nymsphinx::utils::sample_poisson_duration; +use rand::{rngs::OsRng, CryptoRng, Rng}; use std::pin::Pin; -use std::time::Duration; +use std::sync::Arc; use tokio::runtime::Handle; use tokio::task::JoinHandle; use tokio::time; use topology::NymTopology; -pub(crate) struct LoopCoverTrafficStream { - average_packet_delay: Duration, - average_cover_message_sending_delay: Duration, +pub(crate) struct LoopCoverTrafficStream +where + R: CryptoRng + Rng, + T: NymTopology, +{ + /// Key used to encrypt and decrypt content of an ACK packet. + ack_key: Arc, + + /// Average delay an acknowledgement packet is going to get delay at a single mixnode. + average_ack_delay: time::Duration, + + /// Average delay a data packet is going to get delay at a single mixnode. + average_packet_delay: time::Duration, + + /// Average delay between sending subsequent cover packets. + average_cover_message_sending_delay: time::Duration, + + /// Internal state, determined by `average_message_sending_delay`, + /// used to keep track of when a next packet should be sent out. next_delay: time::Delay, + + /// Channel used for sending prepared sphinx packets to `MixTrafficController` that sends them + /// out to the network without any further delays. mix_tx: MixMessageSender, + + /// Represents full address of this client. our_full_destination: Recipient, + + /// Instance of a cryptographically secure random number generator. + rng: R, + + /// Accessor to the common instance of network topology. topology_access: TopologyAccessor, } -impl Stream for LoopCoverTrafficStream { +impl Stream for LoopCoverTrafficStream +where + R: CryptoRng + Rng + Unpin, + T: NymTopology, // this really confuses me, why T doesn't need to be Unpin? +{ // Item is only used to indicate we should create a new message rather than actual cover message // reason being to not introduce unnecessary complexity by having to keep state of topology // mutex when trying to acquire it. So right now the Stream trait serves as a glorified timer. @@ -50,8 +83,9 @@ impl Stream for LoopCoverTrafficStream { // we know it's time to send a message, so let's prepare delay for the next one // Get the `now` by looking at the current `delay` deadline + let avg_delay = self.average_cover_message_sending_delay; let now = self.next_delay.deadline(); - let next_poisson_delay = poisson::sample(self.average_cover_message_sending_delay); + let next_poisson_delay = sample_poisson_duration(&mut self.rng, avg_delay); // The next interval value is `next_poisson_delay` after the one that just // yielded. @@ -62,52 +96,58 @@ impl Stream for LoopCoverTrafficStream { } } -impl LoopCoverTrafficStream { +// obviously when we finally make shared rng that is on 'higher' level, this should become +// generic `R` +impl LoopCoverTrafficStream { pub(crate) fn new( + ack_key: Arc, + average_ack_delay: time::Duration, + average_packet_delay: time::Duration, + average_cover_message_sending_delay: time::Duration, mix_tx: MixMessageSender, our_full_destination: Recipient, topology_access: TopologyAccessor, - average_cover_message_sending_delay: time::Duration, - average_packet_delay: time::Duration, ) -> Self { + let rng = OsRng; + LoopCoverTrafficStream { + ack_key, + average_ack_delay, average_packet_delay, average_cover_message_sending_delay, next_delay: time::delay_for(Default::default()), mix_tx, our_full_destination, + rng, topology_access, } } async fn on_new_message(&mut self) { trace!("next cover message!"); - let route = match self - .topology_access - .random_route_to_gateway(&self.our_full_destination.gateway()) - .await - { - None => { - warn!("No valid topology detected - won't send any loop cover message this time"); - return; - } - Some(route) => route, - }; - let cover_message = match encapsulation::loop_cover_message_route( - self.our_full_destination.destination().clone(), - route, + // TODO for way down the line: in very rare cases (during topology update) we might have + // to wait a really tiny bit before actually obtaining the permit hence messing with our + // poisson delay, but is it really a problem? + let topology_permit = self.topology_access.get_read_permit().await; + // the ack is sent back to ourselves (and then ignored) + let topology_ref_option = topology_permit + .try_get_valid_topology_ref(&self.our_full_destination, &self.our_full_destination); + if topology_ref_option.is_none() { + warn!("No valid topology detected - won't send any loop cover message this time"); + return; + } + let topology_ref = topology_ref_option.unwrap(); + + let cover_message = generate_loop_cover_packet( + &mut self.rng, + topology_ref, + &*self.ack_key, + &self.our_full_destination, + self.average_ack_delay, self.average_packet_delay, - ) { - Ok(message) => message, - Err(err) => { - error!( - "Somehow we managed to create an invalid cover message - {:?}", - err - ); - return; - } - }; + ) + .expect("Somehow failed to generate a loop cover message with a valid topology"); // if this one fails, there's no retrying because it means that either: // - we run out of memory @@ -116,6 +156,12 @@ impl LoopCoverTrafficStream { self.mix_tx .unbounded_send(MixMessage::new(cover_message.0, cover_message.1)) .unwrap(); + + // TODO: I'm not entirely sure whether this is really required, because I'm not 100% + // sure how `yield_now()` works - whether it just notifies the scheduler or whether it + // properly blocks. So to play it on the safe side, just explicitly drop the read permit + drop(topology_permit); + // JS: due to identical logical structure to OutQueueControl::on_message(), this is also // presumably required to prevent bugs in the future. Exact reason is still unknown to me. tokio::task::yield_now().await; @@ -123,8 +169,10 @@ impl LoopCoverTrafficStream { async fn run(&mut self) { // we should set initial delay only when we actually start the stream - self.next_delay = - time::delay_for(poisson::sample(self.average_cover_message_sending_delay)); + self.next_delay = time::delay_for(sample_poisson_duration( + &mut self.rng, + self.average_cover_message_sending_delay, + )); while let Some(_) = self.next().await { self.on_new_message().await; diff --git a/clients/native/src/client/mix_traffic.rs b/clients/native/src/client/mix_traffic.rs index 3ccded4532..1e277a8371 100644 --- a/clients/native/src/client/mix_traffic.rs +++ b/clients/native/src/client/mix_traffic.rs @@ -31,11 +31,17 @@ impl MixMessage { } } +const MAX_FAILURE_COUNT: usize = 100; + pub(crate) struct MixTrafficController<'a> { // TODO: most likely to be replaced by some higher level construct as // later on gateway_client will need to be accessible by other entities gateway_client: GatewayClient<'a, url::Url>, mix_rx: MixMessageReceiver, + + // TODO: this is temporary work-around. + // in long run `gateway_client` will be moved away from `MixTrafficController` anyway. + consecutive_gateway_failure_count: usize, } impl<'a> MixTrafficController<'static> { @@ -46,6 +52,7 @@ impl<'a> MixTrafficController<'static> { MixTrafficController { gateway_client, mix_rx, + consecutive_gateway_failure_count: 0, } } @@ -56,8 +63,18 @@ impl<'a> MixTrafficController<'static> { .send_sphinx_packet(mix_message.0, mix_message.1) .await { - Err(e) => error!("Failed to send sphinx packet to the gateway! - {:?}", e), - Ok(_) => trace!("We *might* have managed to forward sphinx packet to the gateway!"), + Err(e) => { + error!("Failed to send sphinx packet to the gateway! - {:?}", e); + self.consecutive_gateway_failure_count += 1; + if self.consecutive_gateway_failure_count == MAX_FAILURE_COUNT { + // todo: in the future this should initiate a 'graceful' shutdown + panic!("failed to send sphinx packet to the gateway {} times in a row - assuming the gateway is dead. Can't do anything about it yet :(", MAX_FAILURE_COUNT) + } + } + Ok(_) => { + trace!("We *might* have managed to forward sphinx packet to the gateway!"); + self.consecutive_gateway_failure_count = 0; + } } } diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 6ad9efb433..42d400887a 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -15,6 +15,7 @@ use crate::client::cover_traffic_stream::LoopCoverTrafficStream; use crate::client::inbound_messages::{InputMessage, InputMessageReceiver, InputMessageSender}; use crate::client::mix_traffic::{MixMessageReceiver, MixMessageSender, MixTrafficController}; +use crate::client::real_messages_control::RealMessagesController; use crate::client::received_buffer::{ ReceivedBufferRequestReceiver, ReceivedBufferRequestSender, ReceivedMessagesBufferController, }; @@ -26,20 +27,24 @@ use crate::websocket; use crypto::identity::MixIdentityKeyPair; use directory_client::presence; use futures::channel::mpsc; -use gateway_client::{GatewayClient, SphinxPacketReceiver, SphinxPacketSender}; +use gateway_client::{ + AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver, + MixnetMessageSender, +}; use gateway_requests::auth_token::AuthToken; use log::*; +use nymsphinx::acknowledgements::identifier::AckAes128Key; use nymsphinx::addressing::clients::Recipient; -use nymsphinx::chunking::split_and_prepare_payloads; use nymsphinx::NodeAddressBytes; use received_buffer::{ReceivedBufferMessage, ReconstructedMessagesReceiver}; +use std::sync::Arc; use tokio::runtime::Runtime; use topology::NymTopology; mod cover_traffic_stream; pub(crate) mod inbound_messages; mod mix_traffic; -mod real_traffic_stream; +pub(crate) mod real_messages_control; pub(crate) mod received_buffer; pub(crate) mod topology_control; @@ -79,6 +84,7 @@ impl NymClient { // the pumped traffic goes to the MixTrafficController fn start_cover_traffic_stream( &self, + ack_key: Arc, topology_accessor: TopologyAccessor, mix_tx: MixMessageSender, ) { @@ -88,37 +94,54 @@ impl NymClient { self.runtime .enter(|| { LoopCoverTrafficStream::new( + ack_key, + self.config.get_average_ack_delay(), + self.config.get_average_packet_delay(), + self.config.get_loop_cover_traffic_average_delay(), mix_tx, self.as_mix_recipient(), topology_accessor, - self.config.get_loop_cover_traffic_average_delay(), - self.config.get_average_packet_delay(), ) }) .start(self.runtime.handle()); } - fn start_real_traffic_stream( + // TODO: I'm not a fan of this function signature, i.e. that it returns the ACK key, but then again + // I think the ACK key should be generated by 'acknowledgement_control' rather than by + // the client itself. However, it all might change once we have to implement key rotation. + fn start_real_traffic_controller( &self, topology_accessor: TopologyAccessor, - mix_tx: MixMessageSender, - input_rx: InputMessageReceiver, - ) { + ack_receiver: AcknowledgementReceiver, + input_receiver: InputMessageReceiver, + mix_sender: MixMessageSender, + ) -> Arc { + let controller_config = real_messages_control::Config::new( + self.config.get_ack_wait_multiplier(), + self.config.get_ack_wait_addition(), + self.config.get_average_ack_delay(), + self.config.get_message_sending_average_delay(), + self.config.get_average_packet_delay(), + self.as_mix_recipient(), + ); + info!("Starting real traffic stream..."); // we need to explicitly enter runtime due to "next_delay: time::delay_for(Default::default())" - // set in the constructor which HAS TO be called within context of a tokio runtime - self.runtime - .enter(|| { - real_traffic_stream::OutQueueControl::new( - mix_tx, - input_rx, - self.as_mix_recipient(), - topology_accessor, - self.config.get_average_packet_delay(), - self.config.get_message_sending_average_delay(), - ) - }) - .start(self.runtime.handle()); + // set in the constructor [of OutQueueControl] which HAS TO be called within context of a tokio runtime + // When refactoring this restriction should definitely be removed. + let real_messages_controller = self.runtime.enter(|| { + RealMessagesController::new( + controller_config, + ack_receiver, + input_receiver, + mix_sender, + topology_accessor, + ) + }); + let ack_key = real_messages_controller.ack_key(); + real_messages_controller.start(self.runtime.handle()); + + ack_key } // buffer controlling all messages fetched from provider @@ -126,16 +149,17 @@ impl NymClient { fn start_received_messages_buffer_controller( &self, query_receiver: ReceivedBufferRequestReceiver, - sphinx_receiver: SphinxPacketReceiver, + mixnet_receiver: MixnetMessageReceiver, ) { info!("Starting received messages buffer controller..."); - ReceivedMessagesBufferController::new(query_receiver, sphinx_receiver) + ReceivedMessagesBufferController::new(query_receiver, mixnet_receiver) .start(self.runtime.handle()) } fn start_gateway_client( &mut self, - sphinx_packet_sender: SphinxPacketSender, + mixnet_message_sender: MixnetMessageSender, + ack_sender: AcknowledgementSender, gateway_address: url::Url, ) -> GatewayClient<'static, url::Url> { let auth_token = self @@ -148,7 +172,8 @@ impl NymClient { gateway_address, self.as_mix_recipient().destination(), auth_token, - sphinx_packet_sender, + mixnet_message_sender, + ack_sender, self.config.get_gateway_response_timeout(), ); @@ -265,19 +290,13 @@ impl NymClient { /// It's untested and there are absolutely no guarantees about it (but seems to have worked /// well enough in local tests) pub fn send_message(&mut self, recipient: Recipient, message: Vec) { - let split_message = split_and_prepare_payloads(&message); - debug!( - "Splitting message into {:?} fragments!", - split_message.len() - ); - for message_fragment in split_message { - let input_msg = InputMessage::new(recipient.clone(), message_fragment); - self.input_tx - .as_ref() - .expect("start method was not called before!") - .unbounded_send(input_msg) - .unwrap() - } + let input_msg = InputMessage::new(recipient, message); + + self.input_tx + .as_ref() + .expect("start method was not called before!") + .unbounded_send(input_msg) + .unwrap(); } /// EXPERIMENTAL DIRECT RUST API @@ -323,9 +342,9 @@ impl NymClient { // sphinx_message_receiver is the receiver used by MixTrafficController that sends the actual traffic let (sphinx_message_sender, sphinx_message_receiver) = mpsc::unbounded(); - // unwrapped_sphinx_sender is the transmitter of [unwrapped] sphinx messages received from the gateway + // unwrapped_sphinx_sender is the transmitter of mixnet messages received from the gateway // unwrapped_sphinx_receiver is the receiver for said messages - used by ReceivedMessagesBuffer - let (unwrapped_sphinx_sender, unwrapped_sphinx_receiver) = mpsc::unbounded(); + let (mixnet_messages_sender, mixnet_messages_receiver) = mpsc::unbounded(); // used for announcing connection or disconnection of a channel for pushing re-assembled messages to let (received_buffer_request_sender, received_buffer_request_receiver) = mpsc::unbounded(); @@ -333,6 +352,9 @@ impl NymClient { // channels responsible for controlling real messages let (input_sender, input_receiver) = mpsc::unbounded::(); + // channels responsible for controlling ack messages + let (ack_sender, ack_receiver) = mpsc::unbounded(); + // TODO: when we switch to our graph topology, we need to remember to change 'presence::Topology' type let shared_topology_accessor = TopologyAccessor::::new(); // the components are started in very specific order. Unless you know what you are doing, @@ -340,24 +362,29 @@ impl NymClient { self.start_topology_refresher(shared_topology_accessor.clone()); self.start_received_messages_buffer_controller( received_buffer_request_receiver, - unwrapped_sphinx_receiver, + mixnet_messages_receiver, ); let gateway_url = self.runtime.block_on(Self::get_gateway_address( self.config.get_gateway_id(), shared_topology_accessor.clone(), )); - let gateway_client = self.start_gateway_client(unwrapped_sphinx_sender, gateway_url); + let gateway_client = + self.start_gateway_client(mixnet_messages_sender, ack_sender, gateway_url); self.start_mix_traffic_controller(sphinx_message_receiver, gateway_client); - self.start_cover_traffic_stream( + + let ack_key = self.start_real_traffic_controller( shared_topology_accessor.clone(), + ack_receiver, + input_receiver, sphinx_message_sender.clone(), ); - self.start_real_traffic_stream( + + self.start_cover_traffic_stream( + ack_key, shared_topology_accessor.clone(), sphinx_message_sender, - input_receiver, ); match self.config.get_socket_type() { diff --git a/clients/native/src/client/real_messages_control/acknowlegement_control/acknowledgement_listener.rs b/clients/native/src/client/real_messages_control/acknowlegement_control/acknowledgement_listener.rs new file mode 100644 index 0000000000..7b81fbe31a --- /dev/null +++ b/clients/native/src/client/real_messages_control/acknowlegement_control/acknowledgement_listener.rs @@ -0,0 +1,86 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use super::PendingAcksMap; +use futures::StreamExt; +use gateway_client::AcknowledgementReceiver; +use log::*; +use nymsphinx::{ + acknowledgements::{identifier::recover_identifier, AckAes128Key}, + chunking::fragment::{FragmentIdentifier, COVER_FRAG_ID}, +}; +use std::sync::Arc; + +// responsible for cancelling retransmission timers and removed entries from the map +pub(super) struct AcknowledgementListener { + ack_key: Arc, + ack_receiver: AcknowledgementReceiver, + pending_acks: PendingAcksMap, +} + +impl AcknowledgementListener { + pub(super) fn new( + ack_key: Arc, + ack_receiver: AcknowledgementReceiver, + pending_acks: PendingAcksMap, + ) -> Self { + AcknowledgementListener { + ack_key, + ack_receiver, + pending_acks, + } + } + + async fn on_ack(&mut self, ack_content: Vec) { + debug!("Received an ack"); + let frag_id = match recover_identifier(&self.ack_key, &ack_content) { + None => { + warn!("Received invalid ACK!"); // should we do anything else about that? + return; + } + Some(frag_id_bytes) => match FragmentIdentifier::try_from_bytes(&frag_id_bytes) { + Ok(frag_id) => frag_id, + Err(err) => { + warn!("Received invalid ACK! - {:?}", err); // should we do anything else about that? + return; + } + }, + }; + + if frag_id == COVER_FRAG_ID { + trace!("Received an ack for a cover message - no need to do anything"); + return; + } + + if let Some(pending_ack) = self.pending_acks.write().await.remove(&frag_id) { + // cancel the retransmission future + pending_ack.retransmission_cancel.notify(); + } else { + warn!("received ACK for packet we haven't stored! - {:?}", frag_id); + } + } + + pub(super) async fn run(&mut self) { + debug!("Started AcknowledgementListener"); + while let Some(acks) = self.ack_receiver.next().await { + // realistically we would only be getting one ack at the time, but if we managed to + // introduce batching in gateway client, this call should be improved to not re-acquire + // write permit on the map every loop iteration + for ack in acks { + self.on_ack(ack).await; + } + } + error!("TODO: error msg. Or maybe panic?") + } +} diff --git a/clients/native/src/client/real_messages_control/acknowlegement_control/input_message_listener.rs b/clients/native/src/client/real_messages_control/acknowlegement_control/input_message_listener.rs new file mode 100644 index 0000000000..f0b93eba82 --- /dev/null +++ b/clients/native/src/client/real_messages_control/acknowlegement_control/input_message_listener.rs @@ -0,0 +1,127 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use super::{PendingAcknowledgement, PendingAcksMap}; +use crate::client::{ + inbound_messages::{InputMessage, InputMessageReceiver}, + real_messages_control::real_traffic_stream::{RealMessage, RealMessageSender}, + topology_control::TopologyAccessor, +}; +use futures::StreamExt; +use log::*; +use nymsphinx::{ + acknowledgements::AckAes128Key, addressing::clients::Recipient, chunking::MessageChunker, +}; +use rand::{CryptoRng, Rng}; +use std::sync::Arc; +use topology::NymTopology; + +// responsible for splitting received message and initial sending attempt +pub(super) struct InputMessageListener +where + R: CryptoRng + Rng, + T: NymTopology, +{ + ack_key: Arc, + ack_recipient: Recipient, + input_receiver: InputMessageReceiver, + message_chunker: MessageChunker, + pending_acks: PendingAcksMap, + real_message_sender: RealMessageSender, + topology_access: TopologyAccessor, +} + +impl InputMessageListener +where + R: CryptoRng + Rng, + T: NymTopology, +{ + pub(super) fn new( + ack_key: Arc, + ack_recipient: Recipient, + input_receiver: InputMessageReceiver, + message_chunker: MessageChunker, + pending_acks: PendingAcksMap, + real_message_sender: RealMessageSender, + topology_access: TopologyAccessor, + ) -> Self { + InputMessageListener { + ack_key, + ack_recipient, + input_receiver, + message_chunker, + pending_acks, + real_message_sender, + topology_access, + } + } + + async fn on_input_message(&mut self, msg: InputMessage) { + let (recipient, content) = msg.destruct(); + let split_message = self.message_chunker.split_message(&content); + let topology_permit = self.topology_access.get_read_permit().await; + + let topology_ref_option = + topology_permit.try_get_valid_topology_ref(&self.ack_recipient, &recipient); + if topology_ref_option.is_none() { + warn!("Could not process the message - the network topology is invalid"); + return; + } + let topology_ref = topology_ref_option.unwrap(); + + let mut pending_acks = Vec::with_capacity(split_message.len()); + let mut real_messages = Vec::with_capacity(split_message.len()); + for message_chunk in split_message { + // since the paths can be constructed, this CAN'T fail, if it does, there's a bug somewhere + let frag_id = message_chunk.fragment_identifier(); + // we need to clone it because we need to keep it in memory in case we had to retransmit + // it. And then we'd need to recreate entire ACK again. + let chunk_clone = message_chunk.clone(); + let (total_delay, (first_hop, packet)) = self + .message_chunker + .prepare_chunk_for_sending(chunk_clone, topology_ref, &self.ack_key, &recipient) + .unwrap(); + + real_messages.push(RealMessage::new(first_hop, packet, frag_id)); + + let pending_ack = + PendingAcknowledgement::new(message_chunk, total_delay, recipient.clone()); + + pending_acks.push((frag_id, pending_ack)); + } + + // first insert pending_acks only then request fragments to be sent, otherwise you might get + // some very nasty (and time-consuming to figure out...) race condition. + let mut pending_acks_map_write_guard = self.pending_acks.write().await; + for (frag_id, pending_ack) in pending_acks.into_iter() { + if let Some(_) = pending_acks_map_write_guard.insert(frag_id, pending_ack) { + panic!("Tried to insert duplicate pending ack") + } + } + + for real_message in real_messages { + self.real_message_sender + .unbounded_send(real_message) + .unwrap(); + } + } + + pub(super) async fn run(&mut self) { + debug!("Started InputMessageListener"); + while let Some(input_msg) = self.input_receiver.next().await { + self.on_input_message(input_msg).await; + } + error!("TODO: error msg. Or maybe panic?") + } +} diff --git a/clients/native/src/client/real_messages_control/acknowlegement_control/mod.rs b/clients/native/src/client/real_messages_control/acknowlegement_control/mod.rs new file mode 100644 index 0000000000..52d3e5f86f --- /dev/null +++ b/clients/native/src/client/real_messages_control/acknowlegement_control/mod.rs @@ -0,0 +1,240 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use self::{ + acknowledgement_listener::AcknowledgementListener, + input_message_listener::InputMessageListener, + retransmission_request_listener::RetransmissionRequestListener, + sent_notification_listener::SentNotificationListener, +}; +use super::real_traffic_stream::RealMessageSender; +use crate::client::{inbound_messages::InputMessageReceiver, topology_control::TopologyAccessor}; +use futures::channel::mpsc; +use gateway_client::AcknowledgementReceiver; +use log::*; +use nymsphinx::{ + acknowledgements::{self, identifier::AckAes128Key}, + addressing::clients::Recipient, + chunking::{ + fragment::{Fragment, FragmentIdentifier}, + MessageChunker, + }, + Delay, +}; +use rand::{CryptoRng, Rng}; +use std::{collections::HashMap, sync::Arc, time::Duration}; +use tokio::{ + sync::{Notify, RwLock}, + task::JoinHandle, +}; +use topology::NymTopology; + +mod acknowledgement_listener; +mod input_message_listener; +mod retransmission_request_listener; +mod sent_notification_listener; + +type RetransmissionRequestSender = mpsc::UnboundedSender; +type RetransmissionRequestReceiver = mpsc::UnboundedReceiver; + +pub(super) type SentPacketNotificationSender = mpsc::UnboundedSender; +type SentPacketNotificationReceiver = mpsc::UnboundedReceiver; + +type PendingAcksMap = Arc>>; + +struct PendingAcknowledgement { + message_chunk: Fragment, + delay: Delay, + recipient: Recipient, + retransmission_cancel: Arc, +} + +impl PendingAcknowledgement { + fn new(message_chunk: Fragment, delay: Delay, recipient: Recipient) -> Self { + PendingAcknowledgement { + message_chunk, + delay, + retransmission_cancel: Arc::new(Notify::new()), + recipient, + } + } + + fn update_delay(&mut self, new_delay: Delay) { + self.delay = new_delay; + } +} + +pub(super) struct AcknowledgementControllerConnectors { + real_message_sender: RealMessageSender, + input_receiver: InputMessageReceiver, + sent_notifier: SentPacketNotificationReceiver, + ack_receiver: AcknowledgementReceiver, +} + +impl AcknowledgementControllerConnectors { + pub(super) fn new( + real_message_sender: RealMessageSender, + input_receiver: InputMessageReceiver, + sent_notifier: SentPacketNotificationReceiver, + ack_receiver: AcknowledgementReceiver, + ) -> Self { + AcknowledgementControllerConnectors { + real_message_sender, + input_receiver, + sent_notifier, + ack_receiver, + } + } +} + +pub(super) struct AcknowledgementController +where + R: CryptoRng + Rng, + T: NymTopology, +{ + ack_key: Arc, + acknowledgement_listener: Option, + input_message_listener: Option>, + retransmission_request_listener: Option>, + sent_notification_listener: Option, +} + +impl AcknowledgementController +where + R: 'static + CryptoRng + Rng + Clone + Send, + T: 'static + NymTopology, +{ + pub(super) fn new( + mut rng: R, + topology_access: TopologyAccessor, + ack_recipient: Recipient, + average_packet_delay_duration: Duration, + average_ack_delay_duration: Duration, + ack_wait_multiplier: f64, + ack_wait_addition: Duration, + connectors: AcknowledgementControllerConnectors, + ) -> Self { + // note for future-self: perhaps for key rotation we could replace it with Arc> ? + // actually same could be true for any keys we use + let ack_key = Arc::new(acknowledgements::generate_key(&mut rng)); + let pending_acks = Arc::new(RwLock::new(HashMap::new())); + let message_chunker = MessageChunker::new_with_rng( + rng, + ack_recipient.clone(), + average_packet_delay_duration, + average_ack_delay_duration, + ); + + let acknowledgement_listener = AcknowledgementListener::new( + Arc::clone(&ack_key), + connectors.ack_receiver, + Arc::clone(&pending_acks), + ); + + let input_message_listener = InputMessageListener::new( + Arc::clone(&ack_key), + ack_recipient.clone(), + connectors.input_receiver, + message_chunker.clone(), + Arc::clone(&pending_acks), + connectors.real_message_sender.clone(), + topology_access.clone(), + ); + + let (retransmission_tx, retransmission_rx) = mpsc::unbounded(); + + let retransmission_request_listener = RetransmissionRequestListener::new( + Arc::clone(&ack_key), + ack_recipient, + message_chunker, + Arc::clone(&pending_acks), + connectors.real_message_sender, + retransmission_rx, + topology_access, + ); + + let sent_notification_listener = SentNotificationListener::new( + ack_wait_multiplier, + ack_wait_addition, + connectors.sent_notifier, + pending_acks, + retransmission_tx, + ); + + AcknowledgementController { + ack_key, + acknowledgement_listener: Some(acknowledgement_listener), + input_message_listener: Some(input_message_listener), + retransmission_request_listener: Some(retransmission_request_listener), + sent_notification_listener: Some(sent_notification_listener), + } + } + + pub(super) fn ack_key(&self) -> Arc { + Arc::clone(&self.ack_key) + } + + pub(super) async fn run(&mut self) { + let mut acknowledgement_listener = self.acknowledgement_listener.take().unwrap(); + let mut input_message_listener = self.input_message_listener.take().unwrap(); + let mut retransmission_request_listener = + self.retransmission_request_listener.take().unwrap(); + let mut sent_notification_listener = self.sent_notification_listener.take().unwrap(); + + // TODO: perhaps an extra 'DEBUG' task that would periodically check for stale entries in + // pending acks map? + // It would only be 'DEBUG' as I don't expect any stale entries to exist there to begin with, + // but when can bugs be expected to begin with? + + // the below are log messages are errors as at the current stage we do not expect any of + // the task to ever finish. This will of course change once we introduce + // graceful shutdowns. + let ack_listener_fut = tokio::spawn(async move { + acknowledgement_listener.run().await; + error!("The acknowledgement listener has finished execution!"); + acknowledgement_listener + }); + let input_listener_fut = tokio::spawn(async move { + input_message_listener.run().await; + error!("The input listener has finished execution!"); + input_message_listener + }); + let retransmission_req_fut = tokio::spawn(async move { + retransmission_request_listener.run().await; + error!("The retransmission request listener has finished execution!"); + retransmission_request_listener + }); + let sent_notification_fut = tokio::spawn(async move { + sent_notification_listener.run().await; + error!("The sent notification listener has finished execution!"); + sent_notification_listener + }); + + // technically we don't have to bring `AcknowledgementController` back to a valid state + // but we can do it, so why not? Perhaps it might be useful if we wanted to allow + // for restarts of certain modules without killing the entire process. + self.acknowledgement_listener = Some(ack_listener_fut.await.unwrap()); + self.input_message_listener = Some(input_listener_fut.await.unwrap()); + self.retransmission_request_listener = Some(retransmission_req_fut.await.unwrap()); + self.sent_notification_listener = Some(sent_notification_fut.await.unwrap()); + } + + #[allow(dead_code)] + pub(super) fn start(mut self) -> JoinHandle { + tokio::spawn(async move { + self.run().await; + self + }) + } +} diff --git a/clients/native/src/client/real_messages_control/acknowlegement_control/retransmission_request_listener.rs b/clients/native/src/client/real_messages_control/acknowlegement_control/retransmission_request_listener.rs new file mode 100644 index 0000000000..940467f1ba --- /dev/null +++ b/clients/native/src/client/real_messages_control/acknowlegement_control/retransmission_request_listener.rs @@ -0,0 +1,128 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use super::{PendingAcksMap, RetransmissionRequestReceiver}; +use crate::client::{ + real_messages_control::real_traffic_stream::{RealMessage, RealMessageSender}, + topology_control::TopologyAccessor, +}; +use futures::StreamExt; +use log::*; +use nymsphinx::{ + acknowledgements::AckAes128Key, + addressing::clients::Recipient, + chunking::{fragment::FragmentIdentifier, MessageChunker}, +}; +use rand::{CryptoRng, Rng}; +use std::sync::Arc; +use topology::NymTopology; + +// responsible for packet retransmission upon fired timer +pub(super) struct RetransmissionRequestListener +where + R: CryptoRng + Rng, + T: NymTopology, +{ + ack_key: Arc, + ack_recipient: Recipient, + message_chunker: MessageChunker, + pending_acks: PendingAcksMap, + real_message_sender: RealMessageSender, + request_receiver: RetransmissionRequestReceiver, + topology_access: TopologyAccessor, +} + +impl RetransmissionRequestListener +where + R: CryptoRng + Rng, + T: NymTopology, +{ + pub(super) fn new( + ack_key: Arc, + ack_recipient: Recipient, + message_chunker: MessageChunker, + pending_acks: PendingAcksMap, + real_message_sender: RealMessageSender, + request_receiver: RetransmissionRequestReceiver, + topology_access: TopologyAccessor, + ) -> Self { + RetransmissionRequestListener { + ack_key, + ack_recipient, + message_chunker, + pending_acks, + real_message_sender, + request_receiver, + topology_access, + } + } + + async fn on_retransmission_request(&mut self, frag_id: FragmentIdentifier) { + let pending_acks_map_read_guard = self.pending_acks.read().await; + // if the unwrap failed here, we have some weird bug somewhere - honestly, I'm not sure + // if it's even possible for it to happen + let unreceived_ack_fragment = pending_acks_map_read_guard + .get(&frag_id) + .expect("wanted to retransmit ack'd fragment"); + + let packet_recipient = unreceived_ack_fragment.recipient.clone(); + let chunk_clone = unreceived_ack_fragment.message_chunk.clone(); + let frag_id = unreceived_ack_fragment.message_chunk.fragment_identifier(); + + // TODO: we need some proper benchmarking here to determine whether it could + // be more efficient to just get write lock and keep it while doing sphinx computation, + // but my gut feeling tells me we should re-acquire it. + drop(pending_acks_map_read_guard); + + let topology_permit = self.topology_access.get_read_permit().await; + let topology_ref_option = + topology_permit.try_get_valid_topology_ref(&self.ack_recipient, &packet_recipient); + if topology_ref_option.is_none() { + warn!("Could not retransmit the packet - the network topology is invalid"); + // TODO: perhaps put back into pending acks and reset the timer? + return; + } + let topology_ref = topology_ref_option.unwrap(); + + let (total_delay, (first_hop, packet)) = self + .message_chunker + .prepare_chunk_for_sending(chunk_clone, topology_ref, &self.ack_key, &packet_recipient) + .unwrap(); + + // minor optimization to not hold the permit while we no longer need it and might have to block + // waiting for the write lock on `pending_acks` + drop(topology_permit); + + self.pending_acks + .write() + .await + .get_mut(&frag_id) + .expect( + "on_retransmission_request: somehow we already received an ack for this packet?", + ) + .update_delay(total_delay); + + self.real_message_sender + .unbounded_send(RealMessage::new(first_hop, packet, frag_id)) + .unwrap(); + } + + pub(super) async fn run(&mut self) { + debug!("Started RetransmissionRequestListener"); + while let Some(frag_id) = self.request_receiver.next().await { + self.on_retransmission_request(frag_id).await; + } + error!("TODO: error msg. Or maybe panic?") + } +} diff --git a/clients/native/src/client/real_messages_control/acknowlegement_control/sent_notification_listener.rs b/clients/native/src/client/real_messages_control/acknowlegement_control/sent_notification_listener.rs new file mode 100644 index 0000000000..77c6279b72 --- /dev/null +++ b/clients/native/src/client/real_messages_control/acknowlegement_control/sent_notification_listener.rs @@ -0,0 +1,99 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use super::{PendingAcksMap, RetransmissionRequestSender, SentPacketNotificationReceiver}; +use futures::StreamExt; +use log::*; +use nymsphinx::chunking::fragment::FragmentIdentifier; +use std::sync::Arc; +use std::time::Duration; + +// responsible for starting and controlling retransmission timers +// it is required because when we send our packet to the `real traffic stream` controlled +// with poisson timer, there's no guarantee the message will be sent immediately, so we might +// accidentally fire retransmission way quicker than we would have wanted. +pub(super) struct SentNotificationListener { + ack_wait_multiplier: f64, + ack_wait_addition: Duration, + sent_notifier: SentPacketNotificationReceiver, + pending_acks: PendingAcksMap, + retransmission_sender: RetransmissionRequestSender, +} + +impl SentNotificationListener { + pub(super) fn new( + ack_wait_multiplier: f64, + ack_wait_addition: Duration, + sent_notifier: SentPacketNotificationReceiver, + pending_acks: PendingAcksMap, + retransmission_sender: RetransmissionRequestSender, + ) -> Self { + SentNotificationListener { + ack_wait_multiplier, + ack_wait_addition, + sent_notifier, + pending_acks, + retransmission_sender, + } + } + + async fn on_sent_message(&mut self, frag_id: FragmentIdentifier) { + let pending_acks_map_read_guard = self.pending_acks.read().await; + // if the unwrap failed here, we have some weird bug somewhere + // although when I think about it, it *theoretically* could happen under extremely heavy client + // load that `on_sent_message()` is not called (and we do not receive the read permit) + // until we already received and processed an ack for the packet + // but this seems extremely unrealistic, but perhaps we should guard against that? + let pending_ack_data = pending_acks_map_read_guard + .get(&frag_id) + .expect("on_sent_message: somehow we already received an ack for this packet?"); + + // if this assertion ever fails, we have some bug due to some unintended leak. + // the only reason I see it could happen if the `tokio::select` in the spawned + // task below somehow did not drop it + debug_assert_eq!( + Arc::strong_count(&pending_ack_data.retransmission_cancel), + 1 + ); + + // TODO: read more about Arc::downgrade. it could be useful here + let retransmission_cancel = Arc::clone(&pending_ack_data.retransmission_cancel); + + let retransmission_timeout = tokio::time::delay_for( + (pending_ack_data.delay.clone() * self.ack_wait_multiplier).to_duration() + + self.ack_wait_addition, + ); + + let retransmission_sender = self.retransmission_sender.clone(); + tokio::spawn(async move { + tokio::select! { + _ = retransmission_cancel.notified() => { + trace!("received ack for the fragment. Cancelling retransmission future"); + } + _ = retransmission_timeout => { + trace!("did not receive an ack - will retransmit the packet"); + retransmission_sender.unbounded_send(frag_id).unwrap(); + } + } + }); + } + + pub(super) async fn run(&mut self) { + debug!("Started SentNotificationListener"); + while let Some(frag_id) = self.sent_notifier.next().await { + self.on_sent_message(frag_id).await; + } + error!("TODO: error msg. Or maybe panic?") + } +} diff --git a/clients/native/src/client/real_messages_control/mod.rs b/clients/native/src/client/real_messages_control/mod.rs new file mode 100644 index 0000000000..83be7007a3 --- /dev/null +++ b/clients/native/src/client/real_messages_control/mod.rs @@ -0,0 +1,170 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +// INPUT: InputMessage from user +// INPUT2: Acks from mix +// OUTPUT: MixMessage to mix traffic + +use self::{ + acknowlegement_control::AcknowledgementController, real_traffic_stream::OutQueueControl, +}; +use crate::client::real_messages_control::acknowlegement_control::AcknowledgementControllerConnectors; +use crate::client::{ + inbound_messages::InputMessageReceiver, mix_traffic::MixMessageSender, + topology_control::TopologyAccessor, +}; +use futures::channel::mpsc; +use gateway_client::AcknowledgementReceiver; +use log::*; +use nymsphinx::acknowledgements::identifier::AckAes128Key; +use nymsphinx::addressing::clients::Recipient; +use rand::{rngs::OsRng, CryptoRng, Rng}; +use std::sync::Arc; +use std::time::Duration; +use tokio::runtime::Handle; +use tokio::task::JoinHandle; +use topology::NymTopology; + +mod acknowlegement_control; +mod real_traffic_stream; + +pub(crate) struct Config { + ack_wait_multiplier: f64, + ack_wait_addition: Duration, + self_recipient: Recipient, + average_packet_delay_duration: Duration, + average_ack_delay_duration: Duration, + average_message_sending_delay: Duration, +} + +impl Config { + pub(crate) fn new( + ack_wait_multiplier: f64, + ack_wait_addition: Duration, + average_ack_delay_duration: Duration, + average_message_sending_delay: Duration, + average_packet_delay_duration: Duration, + self_recipient: Recipient, + ) -> Self { + Config { + self_recipient, + average_packet_delay_duration, + average_ack_delay_duration, + average_message_sending_delay, + ack_wait_multiplier, + ack_wait_addition, + } + } +} + +pub(crate) struct RealMessagesController +where + R: CryptoRng + Rng, + T: NymTopology, +{ + out_queue_control: Option>, + ack_control: Option>, +} + +// obviously when we finally make shared rng that is on 'higher' level, this should become +// generic `R` +impl RealMessagesController { + pub(crate) fn new( + config: Config, + ack_receiver: AcknowledgementReceiver, + input_receiver: InputMessageReceiver, + mix_sender: MixMessageSender, + topology_access: TopologyAccessor, + ) -> Self { + let rng = OsRng; + + let (real_message_sender, real_message_receiver) = mpsc::unbounded(); + let (sent_notifier_tx, sent_notifier_rx) = mpsc::unbounded(); + + let ack_controller_connectors = AcknowledgementControllerConnectors::new( + real_message_sender, + input_receiver, + sent_notifier_rx, + ack_receiver, + ); + + let ack_control = AcknowledgementController::new( + rng, + topology_access.clone(), + config.self_recipient.clone(), + config.average_packet_delay_duration, + config.average_ack_delay_duration, + config.ack_wait_multiplier, + config.ack_wait_addition, + ack_controller_connectors, + ); + + let out_queue_control = OutQueueControl::new( + ack_control.ack_key(), + config.average_ack_delay_duration, + config.average_packet_delay_duration, + config.average_message_sending_delay, + sent_notifier_tx, + mix_sender, + real_message_receiver, + rng, + config.self_recipient, + topology_access, + ); + + RealMessagesController { + out_queue_control: Some(out_queue_control), + ack_control: Some(ack_control), + } + } + + // Note: this method can only be called before `run` is called + pub(super) fn ack_key(&self) -> Arc { + self.ack_control.as_ref().unwrap().ack_key() + } + + pub(super) async fn run(&mut self) { + let mut out_queue_control = self.out_queue_control.take().unwrap(); + let mut ack_control = self.ack_control.take().unwrap(); + + // the below are log messages are errors as at the current stage we do not expect any of + // the task to ever finish. This will of course change once we introduce + // graceful shutdowns. + let out_queue_control_fut = tokio::spawn(async move { + out_queue_control.run_out_queue_control().await; + error!("The out queue controller has finished execution!"); + out_queue_control + }); + let ack_control_fut = tokio::spawn(async move { + ack_control.run().await; + error!("The acknowledgement controller has finished execution!"); + ack_control + }); + + // technically we don't have to bring `RealMessagesController` back to a valid state + // but we can do it, so why not? Perhaps it might be useful if we wanted to allow + // for restarts of certain modules without killing the entire process. + self.out_queue_control = Some(out_queue_control_fut.await.unwrap()); + self.ack_control = Some(ack_control_fut.await.unwrap()); + } + + // &Handle is only passed for consistency sake with other client modules, but I think + // when we get to refactoring, we should apply gateway approach and make it implicit + pub(super) fn start(mut self, handle: &Handle) -> JoinHandle { + handle.spawn(async move { + self.run().await; + self + }) + } +} diff --git a/clients/native/src/client/real_messages_control/real_traffic_stream.rs b/clients/native/src/client/real_messages_control/real_traffic_stream.rs new file mode 100644 index 0000000000..90b9c05b47 --- /dev/null +++ b/clients/native/src/client/real_messages_control/real_traffic_stream.rs @@ -0,0 +1,250 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use crate::client::mix_traffic::{MixMessage, MixMessageSender}; +use crate::client::real_messages_control::acknowlegement_control::SentPacketNotificationSender; +use crate::client::topology_control::TopologyAccessor; +use futures::channel::mpsc; +use futures::task::{Context, Poll}; +use futures::{Future, Stream, StreamExt}; +use log::*; +use nymsphinx::acknowledgements::identifier::AckAes128Key; +use nymsphinx::addressing::clients::Recipient; +use nymsphinx::chunking::fragment::FragmentIdentifier; +use nymsphinx::cover::generate_loop_cover_packet; +use nymsphinx::utils::sample_poisson_duration; +use nymsphinx::SphinxPacket; +use rand::{CryptoRng, Rng}; +use std::net::SocketAddr; +use std::pin::Pin; +use std::sync::Arc; +use std::time::Duration; +use tokio::time; +use topology::NymTopology; + +pub(crate) struct OutQueueControl +where + R: CryptoRng + Rng, + T: NymTopology, +{ + /// Key used to encrypt and decrypt content of an ACK packet. + ack_key: Arc, + + /// Average delay an acknowledgement packet is going to get delay at a single mixnode. + average_ack_delay: Duration, + + /// Average delay a data packet is going to get delay at a single mixnode. + average_packet_delay: Duration, + + /// Average delay between sending subsequent packets. + average_message_sending_delay: Duration, + + /// Channel used for notifying of a real packet being sent out. Used to start up retransmission timer. + sent_notifier: SentPacketNotificationSender, + + /// Internal state, determined by `average_message_sending_delay`, + /// used to keep track of when a next packet should be sent out. + next_delay: time::Delay, + + /// Channel used for sending prepared sphinx packets to `MixTrafficController` that sends them + /// out to the network without any further delays. + mix_tx: MixMessageSender, + + /// Channel used for receiving real, prepared, messages that must be first sufficiently delayed + /// before being sent out into the network. + real_receiver: RealMessageReceiver, + + /// Represents full address of this client. + our_full_destination: Recipient, + + /// Instance of a cryptographically secure random number generator. + rng: R, + + /// Accessor to the common instance of network topology. + topology_access: TopologyAccessor, +} + +pub(crate) struct RealMessage { + first_hop_address: SocketAddr, + packet: SphinxPacket, + fragment_id: FragmentIdentifier, +} + +impl RealMessage { + pub(crate) fn new( + first_hop_address: SocketAddr, + packet: SphinxPacket, + fragment_id: FragmentIdentifier, + ) -> Self { + RealMessage { + first_hop_address, + packet, + fragment_id, + } + } +} + +// messages are already prepared, etc. the real point of it is to forward it to mix_traffic +// after sufficient delay +pub(crate) type RealMessageSender = mpsc::UnboundedSender; +type RealMessageReceiver = mpsc::UnboundedReceiver; + +pub(crate) enum StreamMessage { + Cover, + Real(RealMessage), +} + +impl Stream for OutQueueControl +where + R: CryptoRng + Rng + Unpin, + T: NymTopology, // this really confuses me, why T doesn't need to be Unpin? +{ + type Item = StreamMessage; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + // it is not yet time to return a message + if Pin::new(&mut self.next_delay).poll(cx).is_pending() { + return Poll::Pending; + }; + + // we know it's time to send a message, so let's prepare delay for the next one + // Get the `now` by looking at the current `delay` deadline + let avg_delay = self.average_message_sending_delay; + let now = self.next_delay.deadline(); + let next_poisson_delay = sample_poisson_duration(&mut self.rng, avg_delay); + + // The next interval value is `next_poisson_delay` after the one that just + // yielded. + let next = now + next_poisson_delay; + self.next_delay.reset(next); + + // decide what kind of message to send + match Pin::new(&mut self.real_receiver).poll_next(cx) { + // in the case our real message channel stream was closed, we should also indicate we are closed + // (and whoever is using the stream should panic) + Poll::Ready(None) => Poll::Ready(None), + + // if there's an actual message - return it + Poll::Ready(Some(real_message)) => Poll::Ready(Some(StreamMessage::Real(real_message))), + + // otherwise construct a dummy one + Poll::Pending => Poll::Ready(Some(StreamMessage::Cover)), + } + } +} + +impl OutQueueControl +where + R: CryptoRng + Rng + Unpin, + T: NymTopology, +{ + pub(crate) fn new( + ack_key: Arc, + average_ack_delay: Duration, + average_packet_delay: Duration, + average_message_sending_delay: Duration, + sent_notifier: SentPacketNotificationSender, + mix_tx: MixMessageSender, + real_receiver: RealMessageReceiver, + rng: R, + our_full_destination: Recipient, + topology_access: TopologyAccessor, + ) -> Self { + OutQueueControl { + ack_key, + average_ack_delay, + average_packet_delay, + average_message_sending_delay, + sent_notifier, + next_delay: time::delay_for(Default::default()), + mix_tx, + real_receiver, + our_full_destination, + rng, + topology_access, + } + } + + async fn on_message(&mut self, next_message: StreamMessage) { + trace!("created new message"); + + let next_message = match next_message { + StreamMessage::Cover => { + // TODO for way down the line: in very rare cases (during topology update) we might have + // to wait a really tiny bit before actually obtaining the permit hence messing with our + // poisson delay, but is it really a problem? + let topology_permit = self.topology_access.get_read_permit().await; + // the ack is sent back to ourselves (and then ignored) + let topology_ref_option = topology_permit.try_get_valid_topology_ref( + &self.our_full_destination, + &self.our_full_destination, + ); + if topology_ref_option.is_none() { + warn!( + "No valid topology detected - won't send any loop cover message this time" + ); + return; + } + let topology_ref = topology_ref_option.unwrap(); + + let cover_message = generate_loop_cover_packet( + &mut self.rng, + topology_ref, + &*self.ack_key, + &self.our_full_destination, + self.average_ack_delay, + self.average_packet_delay, + ) + .expect("Somehow failed to generate a loop cover message with a valid topology"); + + MixMessage::new(cover_message.0, cover_message.1) + } + StreamMessage::Real(real_message) => { + // well technically the message was not sent just yet, but now it's up to internal + // queues and client load rather than the required delay. So realistically we can treat + // whatever is about to happen as negligible additional delay. + self.sent_notifier + .unbounded_send(real_message.fragment_id) + .unwrap(); + MixMessage::new(real_message.first_hop_address, real_message.packet) + } + }; + + // if this one fails, there's no retrying because it means that either: + // - we run out of memory + // - the receiver channel is closed + // in either case there's no recovery and we can only panic + self.mix_tx.unbounded_send(next_message).unwrap(); + + // JS: Not entirely sure why or how it fixes stuff, but without the yield call, + // the UnboundedReceiver [of mix_rx] will not get a chance to read anything + // JS2: Basically it was the case that with high enough rate, the stream had already a next value + // ready and hence was immediately re-scheduled causing other tasks to be starved; + // yield makes it go back the scheduling queue regardless of its value availability + tokio::task::yield_now().await; + } + + pub(crate) async fn run_out_queue_control(&mut self) { + // we should set initial delay only when we actually start the stream + self.next_delay = time::delay_for(sample_poisson_duration( + &mut self.rng, + self.average_message_sending_delay, + )); + + info!("Starting out queue controller..."); + while let Some(next_message) = self.next().await { + self.on_message(next_message).await; + } + } +} diff --git a/clients/native/src/client/real_traffic_stream.rs b/clients/native/src/client/real_traffic_stream.rs deleted file mode 100644 index 9d03d7f817..0000000000 --- a/clients/native/src/client/real_traffic_stream.rs +++ /dev/null @@ -1,189 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// 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. - -use crate::client::inbound_messages::InputMessage; -use crate::client::mix_traffic::MixMessage; -use crate::client::topology_control::TopologyAccessor; -use futures::channel::mpsc; -use futures::task::{Context, Poll}; -use futures::{Future, Stream, StreamExt}; -use log::{error, info, trace, warn}; -use nymsphinx::addressing::clients::Recipient; -use nymsphinx::utils::{encapsulation, poisson}; -use std::pin::Pin; -use std::time::Duration; -use tokio::runtime::Handle; -use tokio::task::JoinHandle; -use tokio::time; -use topology::NymTopology; - -pub(crate) struct OutQueueControl { - average_packet_delay: Duration, - average_message_sending_delay: Duration, - next_delay: time::Delay, - mix_tx: mpsc::UnboundedSender, - input_rx: mpsc::UnboundedReceiver, - our_full_destination: Recipient, - topology_access: TopologyAccessor, -} - -pub(crate) enum StreamMessage { - Cover, - Real(InputMessage), -} - -impl Stream for OutQueueControl { - type Item = StreamMessage; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - // it is not yet time to return a message - if Pin::new(&mut self.next_delay).poll(cx).is_pending() { - return Poll::Pending; - }; - - // we know it's time to send a message, so let's prepare delay for the next one - // Get the `now` by looking at the current `delay` deadline - let now = self.next_delay.deadline(); - let next_poisson_delay = poisson::sample(self.average_message_sending_delay); - - // The next interval value is `next_poisson_delay` after the one that just - // yielded. - let next = now + next_poisson_delay; - self.next_delay.reset(next); - - // decide what kind of message to send - match Pin::new(&mut self.input_rx).poll_next(cx) { - // in the case our real message channel stream was closed, we should also indicate we are closed - // (and whoever is using the stream should panic) - Poll::Ready(None) => Poll::Ready(None), - - // if there's an actual message - return it - Poll::Ready(Some(real_message)) => Poll::Ready(Some(StreamMessage::Real(real_message))), - - // otherwise construct a dummy one - Poll::Pending => Poll::Ready(Some(StreamMessage::Cover)), - } - } -} - -impl OutQueueControl { - pub(crate) fn new( - mix_tx: mpsc::UnboundedSender, - input_rx: mpsc::UnboundedReceiver, - our_full_destination: Recipient, - topology_access: TopologyAccessor, - average_packet_delay: Duration, - average_message_sending_delay: Duration, - ) -> Self { - OutQueueControl { - average_packet_delay, - average_message_sending_delay, - next_delay: time::delay_for(Default::default()), - mix_tx, - input_rx, - our_full_destination, - topology_access, - } - } - - async fn get_route(&self, other_recipient: Option<&Recipient>) -> Option> { - match other_recipient { - // we are sending to ourselves - None => { - self.topology_access - .random_route_to_gateway(&self.our_full_destination.gateway()) - .await - } - Some(other_recipient) => { - self.topology_access - .random_route_to_gateway(&other_recipient.gateway()) - .await - } - } - } - - async fn on_message(&mut self, next_message: StreamMessage) { - trace!("created new message"); - - let next_packet = match next_message { - StreamMessage::Cover => { - let route = self.get_route(None).await; - if route.is_none() { - warn!("No valid topology detected - won't send any real or loop message this time"); - return; - } - let route = route.unwrap(); - encapsulation::loop_cover_message_route( - self.our_full_destination.destination().clone(), - route, - self.average_packet_delay, - ) - } - StreamMessage::Real(real_message) => { - let (recipient, data) = real_message.destruct(); - let route = self.get_route(Some(&recipient)).await; - if route.is_none() { - warn!("No valid topology detected - won't send any real or loop message this time"); - return; - } - let route = route.unwrap(); - encapsulation::encapsulate_message_route( - recipient.destination(), - data, - route, - self.average_packet_delay, - ) - } - }; - - let next_packet = match next_packet { - Ok(message) => message, - Err(err) => { - error!( - "Somehow we managed to create an invalid traffic message - {:?}", - err - ); - return; - } - }; - - // if this one fails, there's no retrying because it means that either: - // - we run out of memory - // - the receiver channel is closed - // in either case there's no recovery and we can only panic - self.mix_tx - .unbounded_send(MixMessage::new(next_packet.0, next_packet.1)) - .unwrap(); - // JS: Not entirely sure why or how it fixes stuff, but without the yield call, - // the UnboundedReceiver [of mix_rx] will not get a chance to read anything - // JS2: Basically it was the case that with high enough rate, the stream had already a next value - // ready and hence was immediately re-scheduled causing other tasks to be starved; - // yield makes it go back the scheduling queue regardless of its value availability - tokio::task::yield_now().await; - } - - pub(crate) async fn run_out_queue_control(mut self) { - // we should set initial delay only when we actually start the stream - self.next_delay = time::delay_for(poisson::sample(self.average_message_sending_delay)); - - info!("Starting out queue controller..."); - while let Some(next_message) = self.next().await { - self.on_message(next_message).await; - } - } - - pub(crate) fn start(self, handle: &Handle) -> JoinHandle<()> { - handle.spawn(async move { self.run_out_queue_control().await }) - } -} diff --git a/clients/native/src/client/received_buffer.rs b/clients/native/src/client/received_buffer.rs index 029d36d8d0..408df39640 100644 --- a/clients/native/src/client/received_buffer.rs +++ b/clients/native/src/client/received_buffer.rs @@ -13,14 +13,13 @@ // limitations under the License. use futures::channel::mpsc; -use futures::lock::Mutex; +use futures::lock::{Mutex, MutexGuard}; use futures::StreamExt; -use gateway_client::SphinxPacketReceiver; +use gateway_client::MixnetMessageReceiver; use log::*; -use nymsphinx::{ - chunking::reconstruction::MessageReconstructor, - utils::encapsulation::LOOP_COVER_MESSAGE_PAYLOAD, -}; +use nymsphinx::chunking::reconstruction::MessageReconstructor; +use nymsphinx::cover::LOOP_COVER_MESSAGE_PAYLOAD; +use std::collections::HashSet; use std::sync::Arc; use tokio::runtime::Handle; use tokio::task::JoinHandle; @@ -38,6 +37,11 @@ struct ReceivedMessagesBufferInner { messages: Vec>, message_reconstructor: MessageReconstructor, message_sender: Option, + + // TODO: this will get cleared upon re-running the client + // but perhaps it should be changed to include timestamps of when the message was reconstructed + // and every now and then remove ids older than X + recently_reconstructed: HashSet, } #[derive(Debug, Clone)] @@ -54,6 +58,7 @@ impl ReceivedMessagesBuffer { messages: Vec::new(), message_reconstructor: MessageReconstructor::new(), message_sender: None, + recently_reconstructed: HashSet::new(), })), } } @@ -102,6 +107,47 @@ impl ReceivedMessagesBuffer { self.inner.lock().await.messages.extend(msgs) } + fn process_received_fragment( + mutex_guard: &mut MutexGuard, + raw_fragment: Vec, + ) -> Option> { + if raw_fragment == LOOP_COVER_MESSAGE_PAYLOAD { + trace!("The message was a loop cover message! Skipping it"); + return None; + } + + let fragment = match mutex_guard + .message_reconstructor + .recover_fragment(raw_fragment) + { + Err(e) => { + warn!("failed to recover fragment data: {:?}. The whole underlying message might be corrupted and unrecoverable!", e); + return None; + } + Ok(frag) => frag, + }; + + if mutex_guard.recently_reconstructed.contains(&fragment.id()) { + debug!("Received a chunk of already re-assembled message ({:?})! It probably got here because the ack got lost", fragment.id()); + return None; + } + + if let Some(reconstructed_message) = mutex_guard + .message_reconstructor + .insert_new_fragment(fragment) + { + let (completed_message, message_sets) = reconstructed_message; + for set_id in message_sets { + if !mutex_guard.recently_reconstructed.insert(set_id) { + // or perhaps we should even panic at this point? + error!("Reconstructed another message containing already used set id!") + } + } + return Some(completed_message); + } + None + } + async fn add_new_message_fragments(&mut self, msgs: Vec>) { debug!( "Adding {:?} new message fragments to the buffer!", @@ -112,15 +158,10 @@ impl ReceivedMessagesBuffer { let mut completed_messages = Vec::new(); let mut inner_guard = self.inner.lock().await; for msg_fragment in msgs { - if msg_fragment == LOOP_COVER_MESSAGE_PAYLOAD { - trace!("The message was a loop cover message! Skipping it"); - continue; - } - - if let Some(reconstructed_message) = - inner_guard.message_reconstructor.new_fragment(msg_fragment) + if let Some(completed_message) = + Self::process_received_fragment(&mut inner_guard, msg_fragment) { - completed_messages.push(reconstructed_message); + completed_messages.push(completed_message) } } @@ -189,22 +230,22 @@ impl RequestReceiver { struct FragmentedMessageReceiver { received_buffer: ReceivedMessagesBuffer, - sphinx_packet_receiver: SphinxPacketReceiver, + mixnet_packet_receiver: MixnetMessageReceiver, } impl FragmentedMessageReceiver { fn new( received_buffer: ReceivedMessagesBuffer, - sphinx_packet_receiver: SphinxPacketReceiver, + mixnet_packet_receiver: MixnetMessageReceiver, ) -> Self { FragmentedMessageReceiver { received_buffer, - sphinx_packet_receiver, + mixnet_packet_receiver, } } fn start(mut self, handle: &Handle) -> JoinHandle<()> { handle.spawn(async move { - while let Some(new_messages) = self.sphinx_packet_receiver.next().await { + while let Some(new_messages) = self.mixnet_packet_receiver.next().await { self.received_buffer .add_new_message_fragments(new_messages) .await; @@ -214,21 +255,21 @@ impl FragmentedMessageReceiver { } pub(crate) struct ReceivedMessagesBufferController { - fragmented_messsage_receiver: FragmentedMessageReceiver, + fragmented_message_receiver: FragmentedMessageReceiver, request_receiver: RequestReceiver, } impl ReceivedMessagesBufferController { pub(crate) fn new( query_receiver: ReceivedBufferRequestReceiver, - sphinx_packet_receiver: SphinxPacketReceiver, + mixnet_packet_receiver: MixnetMessageReceiver, ) -> Self { let received_buffer = ReceivedMessagesBuffer::new(); ReceivedMessagesBufferController { - fragmented_messsage_receiver: FragmentedMessageReceiver::new( + fragmented_message_receiver: FragmentedMessageReceiver::new( received_buffer.clone(), - sphinx_packet_receiver, + mixnet_packet_receiver, ), request_receiver: RequestReceiver::new(received_buffer, query_receiver), } @@ -236,7 +277,7 @@ impl ReceivedMessagesBufferController { pub(crate) fn start(self, handle: &Handle) { // TODO: should we do anything with JoinHandle(s) returned by start methods? - self.fragmented_messsage_receiver.start(handle); + self.fragmented_message_receiver.start(handle); self.request_receiver.start(handle); } } diff --git a/clients/native/src/client/topology_control.rs b/clients/native/src/client/topology_control.rs index fb949f4e88..740ffe34ec 100644 --- a/clients/native/src/client/topology_control.rs +++ b/clients/native/src/client/topology_control.rs @@ -13,18 +13,29 @@ // limitations under the License. use crate::built_info; -use futures::lock::Mutex; use log::*; -use nymsphinx::NodeAddressBytes; +use nymsphinx::addressing::clients::Recipient; +use std::ops::Deref; use std::sync::Arc; use std::time; use std::time::Duration; use tokio::runtime::Handle; +use tokio::sync::{RwLock, RwLockReadGuard}; use tokio::task::JoinHandle; use topology::{provider, NymTopology}; +// I'm extremely curious why compiler NEVER complained about lack of Debug here before +#[derive(Debug)] struct TopologyAccessorInner(Option); +impl Deref for TopologyAccessorInner { + type Target = Option; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + impl TopologyAccessorInner { fn new() -> Self { TopologyAccessorInner(None) @@ -35,26 +46,78 @@ impl TopologyAccessorInner { } } +pub(super) struct TopologyReadPermit<'a, T: NymTopology> { + permit: RwLockReadGuard<'a, TopologyAccessorInner>, +} + +impl<'a, T: NymTopology> TopologyReadPermit<'a, T> { + /// Using provided topology read permit, tries to get an immutable reference to the underlying + /// topology. For obvious reasons the lifetime of the topology reference is bound to the permit. + pub(super) fn try_get_valid_topology_ref( + &'a self, + ack_recipient: &Recipient, + packet_recipient: &Recipient, + ) -> Option<&'a T> { + // first we need to deref out of RwLockReadGuard + // then we need to deref out of TopologyAccessorInner + // then we must take ref of option, i.e. Option<&T> + // and finally try to unwrap it to obtain &T + let topology_ref_option = (*self.permit.deref()).as_ref(); + + if topology_ref_option.is_none() { + return None; + } + + let topology_ref = topology_ref_option.unwrap(); + + // see if it's possible to route the packet to both gateways + if !topology_ref.can_construct_path_through() + || !topology_ref.gateway_exists(&packet_recipient.gateway()) + || !topology_ref.gateway_exists(&ack_recipient.gateway()) + { + None + } else { + Some(topology_ref) + } + } +} + +impl<'a, T: NymTopology> From>> + for TopologyReadPermit<'a, T> +{ + fn from(read_permit: RwLockReadGuard<'a, TopologyAccessorInner>) -> Self { + TopologyReadPermit { + permit: read_permit, + } + } +} + #[derive(Clone, Debug)] pub(crate) struct TopologyAccessor { - // TODO: this requires some actual benchmarking to determine if obtaining mutex is not going - // to cause some bottlenecking and whether perhaps RwLock would be better - inner: Arc>>, + // `RwLock` *seems to* be the better approach for this as write access is only requested every + // few seconds, while reads are needed every single packet generated. + // However, proper benchmarks will be needed to determine if `RwLock` is indeed a better + // approach than a `Mutex` + inner: Arc>>, } impl TopologyAccessor { pub(crate) fn new() -> Self { TopologyAccessor { - inner: Arc::new(Mutex::new(TopologyAccessorInner::new())), + inner: Arc::new(RwLock::new(TopologyAccessorInner::new())), } } + pub(super) async fn get_read_permit(&self) -> TopologyReadPermit<'_, T> { + self.inner.read().await.into() + } + async fn update_global_topology(&mut self, new_topology: Option) { - self.inner.lock().await.update(new_topology); + self.inner.write().await.update(new_topology); } pub(crate) async fn get_gateway_socket_url(&self, id: &str) -> Option { - match &self.inner.lock().await.0 { + match &self.inner.read().await.0 { None => None, Some(ref topology) => topology .gateways() @@ -67,7 +130,7 @@ impl TopologyAccessor { // only used by the client at startup to get a slightly more reasonable error message // (currently displays as unused because healthchecker is disabled due to required changes) pub(crate) async fn is_routable(&self) -> bool { - match &self.inner.lock().await.0 { + match &self.inner.read().await.0 { None => false, Some(ref topology) => topology.can_construct_path_through(), } @@ -75,7 +138,7 @@ impl TopologyAccessor { pub(crate) async fn get_all_clients(&self) -> Option> { // TODO: this will need to be modified to instead return pairs (provider, client) - match &self.inner.lock().await.0 { + match &self.inner.read().await.0 { None => None, Some(ref topology) => Some( topology @@ -87,40 +150,6 @@ impl TopologyAccessor { ), } } - - pub(crate) async fn random_route_to_gateway( - &self, - gateway: &NodeAddressBytes, - ) -> Option> { - let b58_address = gateway.to_base58_string(); - let guard = self.inner.lock().await; - let topology = guard.0.as_ref()?; - - let gateway = topology - .gateways() - .iter() - .cloned() - .find(|gateway| gateway.pub_key == b58_address.clone())?; - - topology.random_route_to_gateway(gateway.into()).ok() - } - - // // this is a rather temporary solution as each client will have an associated provider - // // currently that is not implemented yet and there only exists one provider in the network - // pub(crate) async fn random_route(&self) -> Option> { - // match &self.inner.lock().await.0 { - // None => None, - // Some(ref topology) => { - // let mut gateways = topology.gateways(); - // if gateways.is_empty() { - // return None; - // } - // // unwrap is fine here as we asserted there is at least single provider - // let provider = gateways.pop().unwrap().into(); - // topology.random_route_to_gateway(provider).ok() - // } - // } - // } } pub(crate) struct TopologyRefresherConfig { diff --git a/clients/native/src/commands/init.rs b/clients/native/src/commands/init.rs index 740479bf7c..28ecdd5255 100644 --- a/clients/native/src/commands/init.rs +++ b/clients/native/src/commands/init.rs @@ -19,7 +19,6 @@ use clap::{App, Arg, ArgMatches}; use config::NymConfig; use crypto::identity::MixIdentityKeyPair; use directory_client::presence::Topology; -use futures::channel::mpsc; use gateway_client::GatewayClient; use gateway_requests::AuthToken; use nymsphinx::DestinationAddressBytes; @@ -68,20 +67,18 @@ async fn try_gateway_registration( gateways: Vec, our_address: DestinationAddressBytes, ) -> Option<(String, AuthToken)> { - // TODO: having to do something like this suggests that perhaps GatewayClient's constructor - // could be improved - let (sphinx_tx, _) = mpsc::unbounded(); let timeout = Duration::from_millis(1500); for gateway in gateways { - let mut gateway_client = GatewayClient::new( + let mut gateway_client = GatewayClient::new_init( url::Url::parse(&gateway.client_listener).unwrap(), our_address.clone(), - None, - sphinx_tx.clone(), timeout, ); - if gateway_client.establish_connection().await.is_ok() { - if let Ok(token) = gateway_client.register().await { + if let Ok(token) = gateway_client.register_without_listening().await { + if let Err(err) = gateway_client.close_connection().await { + eprintln!("Error while closing connection to the gateway! - {:?}", err); + continue; + } else { return Some((gateway.pub_key, token)); } } diff --git a/clients/native/src/config/mod.rs b/clients/native/src/config/mod.rs index 8ab8826fd5..7711129b9c 100644 --- a/clients/native/src/config/mod.rs +++ b/clients/native/src/config/mod.rs @@ -26,6 +26,8 @@ const DEFAULT_LISTENING_PORT: u16 = 1977; const DEFAULT_DIRECTORY_SERVER: &str = "https://directory.nymtech.net"; // 'DEBUG' // where applicable, the below are defined in milliseconds +const DEFAULT_ACK_WAIT_MULTIPLIER: f64 = 1.5; +const DEFAULT_ACK_WAIT_ADDITION: u64 = 800; const DEFAULT_LOOP_COVER_STREAM_AVERAGE_DELAY: u64 = 1000; // 1s const DEFAULT_MESSAGE_STREAM_AVERAGE_DELAY: u64 = 500; // 0.5s const DEFAULT_AVERAGE_PACKET_DELAY: u64 = 200; // 0.2s @@ -189,6 +191,18 @@ impl Config { time::Duration::from_millis(self.debug.average_packet_delay) } + pub fn get_average_ack_delay(&self) -> time::Duration { + time::Duration::from_millis(self.debug.average_ack_delay) + } + + pub fn get_ack_wait_multiplier(&self) -> f64 { + self.debug.ack_wait_multiplier + } + + pub fn get_ack_wait_addition(&self) -> time::Duration { + time::Duration::from_millis(self.debug.ack_wait_addition) + } + pub fn get_loop_cover_traffic_average_delay(&self) -> time::Duration { time::Duration::from_millis(self.debug.loop_cover_traffic_average_delay) } @@ -312,6 +326,24 @@ pub struct Debug { /// The provided value is interpreted as milliseconds. average_packet_delay: u64, + /// The parameter of Poisson distribution determining how long, on average, + /// sent acknowledgement is going to be delayed at any given mix node. + /// So for an ack going through three mix nodes, on average, it will take three times this value + /// until the packet reaches its destination. + /// The provided value is interpreted as milliseconds. + average_ack_delay: u64, + + /// Value multiplied with the expected round trip time of an acknowledgement packet before + /// it is assumed it was lost and retransmission of the data packet happens. + /// In an ideal network with 0 latency, this value would have been 1. + ack_wait_multiplier: f64, + + /// Value added to the expected round trip time of an acknowledgement packet before + /// it is assumed it was lost and retransmission of the data packet happens. + /// In an ideal network with 0 latency, this value would have been 0. + /// The provided value is interpreted as milliseconds. + ack_wait_addition: u64, + /// The parameter of Poisson distribution determining how long, on average, /// it is going to take for another loop cover traffic message to be sent. /// The provided value is interpreted as milliseconds. @@ -345,6 +377,9 @@ impl Default for Debug { fn default() -> Self { Debug { average_packet_delay: DEFAULT_AVERAGE_PACKET_DELAY, + average_ack_delay: DEFAULT_AVERAGE_PACKET_DELAY, + ack_wait_multiplier: DEFAULT_ACK_WAIT_MULTIPLIER, + ack_wait_addition: DEFAULT_ACK_WAIT_ADDITION, loop_cover_traffic_average_delay: DEFAULT_LOOP_COVER_STREAM_AVERAGE_DELAY, message_sending_average_delay: DEFAULT_MESSAGE_STREAM_AVERAGE_DELAY, gateway_response_timeout: DEFAULT_GATEWAY_RESPONSE_TIMEOUT, diff --git a/clients/native/src/config/template.rs b/clients/native/src/config/template.rs index 0afa6c2b33..590e567584 100644 --- a/clients/native/src/config/template.rs +++ b/clients/native/src/config/template.rs @@ -77,6 +77,7 @@ listening_port = {{ socket.listening_port }} [debug] average_packet_delay = {{ debug.average_packet_delay }} +average_ack_delay = {{ debug.average_ack_delay }} loop_cover_traffic_average_delay = {{ debug.loop_cover_traffic_average_delay }} message_sending_average_delay = {{ debug.message_sending_average_delay }} diff --git a/clients/native/src/websocket/handler.rs b/clients/native/src/websocket/handler.rs index 8ca9d9b867..7ca5554c33 100644 --- a/clients/native/src/websocket/handler.rs +++ b/clients/native/src/websocket/handler.rs @@ -24,7 +24,6 @@ use futures::channel::mpsc; use futures::{SinkExt, StreamExt}; use log::*; use nymsphinx::addressing::clients::Recipient; -use nymsphinx::chunking::split_and_prepare_payloads; use std::convert::TryFrom; use tokio::net::TcpStream; use tokio_tungstenite::{ @@ -104,12 +103,9 @@ impl Handler { } }; - // in case the message is too long and needs to be split into multiple packets: - let split_message = split_and_prepare_payloads(&message_bytes); - for message_fragment in split_message { - let input_msg = InputMessage::new(recipient.clone(), message_fragment); - self.msg_input.unbounded_send(input_msg).unwrap(); - } + // the ack control is now responsible for chunking, etc. + let input_msg = InputMessage::new(recipient, message_bytes); + self.msg_input.unbounded_send(input_msg).unwrap(); self.received_response_type = ReceivedResponseType::Text; @@ -155,12 +151,9 @@ impl Handler { } async fn handle_binary_send(&mut self, recipient: Recipient, data: Vec) -> ServerResponse { - // in case the message is too long and needs to be split into multiple packets: - let split_message = split_and_prepare_payloads(&data); - for message_fragment in split_message { - let input_msg = InputMessage::new(recipient.clone(), message_fragment); - self.msg_input.unbounded_send(input_msg).unwrap(); - } + // the ack control is now responsible for chunking, etc. + let input_msg = InputMessage::new(recipient, data); + self.msg_input.unbounded_send(input_msg).unwrap(); self.received_response_type = ReceivedResponseType::Binary; ServerResponse::Send diff --git a/clients/webassembly/src/lib.rs b/clients/webassembly/src/lib.rs index c69045983e..f20973d2cc 100644 --- a/clients/webassembly/src/lib.rs +++ b/clients/webassembly/src/lib.rs @@ -55,6 +55,7 @@ pub struct NodeData { /// /// Message chunking is currently not implemented. If the message exceeds the /// capacity of a single Sphinx packet, the extra information will be discarded. +/// #[wasm_bindgen] pub fn create_sphinx_packet(topology_json: &str, msg: &str, recipient: &str) -> Vec { utils::set_panic_hook(); // nicer js errors. @@ -73,7 +74,7 @@ pub fn create_sphinx_packet(topology_json: &str, msg: &str, recipient: &str) -> let message = msg.as_bytes().to_vec(); let destination = Destination::new(recipient.destination(), Default::default()); - let sphinx_packet = SphinxPacket::new(message, &route, &destination, &delays, None).unwrap(); + let sphinx_packet = SphinxPacket::new(message, &route, &destination, &delays).unwrap(); payload(sphinx_packet, route) } @@ -123,7 +124,7 @@ impl TryFrom for SphinxNode { let src = MixIdentityPublicKey::from_base58_string(node_data.public_key).to_bytes(); let mut dest: [u8; 32] = [0; 32]; dest.copy_from_slice(&src); - nymsphinx::key::new(dest) + nymsphinx::public_key_from_bytes(dest) }; Ok(SphinxNode { address, pub_key }) diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs new file mode 100644 index 0000000000..82f4c1d8c1 --- /dev/null +++ b/common/client-libs/gateway-client/src/error.rs @@ -0,0 +1,75 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use gateway_requests::auth_token::AuthTokenConversionError; +use std::fmt::{self, Error, Formatter}; +use tokio_tungstenite::tungstenite::Error as WsError; + +#[derive(Debug)] +pub enum GatewayClientError { + ConnectionNotEstablished, + GatewayError(String), + NetworkError(WsError), + NoAuthTokenAvailable, + ConnectionAbruptlyClosed, + MalformedResponse, + NotAuthenticated, + ConnectionInInvalidState, + AuthenticationFailure, + Timeout, +} + +impl From for GatewayClientError { + fn from(err: WsError) -> Self { + GatewayClientError::NetworkError(err) + } +} + +impl From for GatewayClientError { + fn from(_err: AuthTokenConversionError) -> Self { + GatewayClientError::MalformedResponse + } +} + +// better human readable representation of the error, mostly so that GatewayClientError +// would implement std::error::Error +impl fmt::Display for GatewayClientError { + fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { + match self { + GatewayClientError::ConnectionNotEstablished => { + write!(f, "connection to the gateway is not established") + } + GatewayClientError::NoAuthTokenAvailable => { + write!(f, "no AuthToken was provided or obtained") + } + GatewayClientError::NotAuthenticated => write!(f, "client is not authenticated"), + GatewayClientError::NetworkError(err) => { + write!(f, "there was a network error - {}", err) + } + GatewayClientError::ConnectionAbruptlyClosed => { + write!(f, "connection was abruptly closed") + } + GatewayClientError::Timeout => write!(f, "timed out"), + GatewayClientError::MalformedResponse => write!(f, "received response was malformed"), + GatewayClientError::ConnectionInInvalidState => write!( + f, + "connection is in an invalid state - please send a bug report" + ), + GatewayClientError::AuthenticationFailure => write!(f, "authentication failure"), + GatewayClientError::GatewayError(err) => { + write!(f, "gateway returned an error response - {}", err) + } + } + } +} diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index a8923ebc77..2d806d24b4 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -12,14 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crate::error::GatewayClientError; +use crate::packet_router::PacketRouter; +pub use crate::packet_router::{ + AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender, +}; use futures::stream::{SplitSink, SplitStream}; -use futures::{channel::mpsc, future::BoxFuture, FutureExt, SinkExt, StreamExt}; -use gateway_requests::auth_token::{AuthToken, AuthTokenConversionError}; +use futures::{future::BoxFuture, FutureExt, SinkExt, StreamExt}; +use gateway_requests::auth_token::AuthToken; use gateway_requests::{BinaryRequest, ClientControlRequest, ServerResponse}; use log::*; use nymsphinx::{DestinationAddressBytes, SphinxPacket}; use std::convert::TryFrom; -use std::fmt::{self, Error, Formatter}; use std::net::SocketAddr; use std::sync::Arc; use std::time::Duration; @@ -27,10 +31,13 @@ use tokio::net::TcpStream; use tokio::sync::Notify; use tokio_tungstenite::{ connect_async, - tungstenite::{client::IntoClientRequest, protocol::Message, Error as WsError}, + tungstenite::{client::IntoClientRequest, protocol::Message}, WebSocketStream, }; +pub mod error; +pub mod packet_router; + // TODO: combine the duplicate reading procedure, i.e. /* msg read from conn.next().await: @@ -51,70 +58,10 @@ msg read from conn.next().await: */ // TODO: some batching mechanism to allow reading and sending more than a single packet through -pub type SphinxPacketSender = mpsc::UnboundedSender>>; -pub type SphinxPacketReceiver = mpsc::UnboundedReceiver>>; // type alias for not having to type the whole thing every single time type WsConn = WebSocketStream; -#[derive(Debug)] -pub enum GatewayClientError { - ConnectionNotEstablished, - GatewayError(String), - NetworkError(WsError), - NoAuthTokenAvailable, - ConnectionAbruptlyClosed, - MalformedResponse, - NotAuthenticated, - ConnectionInInvalidState, - AuthenticationFailure, - Timeout, -} - -impl From for GatewayClientError { - fn from(err: WsError) -> Self { - GatewayClientError::NetworkError(err) - } -} - -impl From for GatewayClientError { - fn from(_err: AuthTokenConversionError) -> Self { - GatewayClientError::MalformedResponse - } -} - -// better human readable representation of the error, mostly so that GatewayClientError -// would implement std::error::Error -impl fmt::Display for GatewayClientError { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { - match self { - GatewayClientError::ConnectionNotEstablished => { - write!(f, "connection to the gateway is not established") - } - GatewayClientError::NoAuthTokenAvailable => { - write!(f, "no AuthToken was provided or obtained") - } - GatewayClientError::NotAuthenticated => write!(f, "client is not authenticated"), - GatewayClientError::NetworkError(err) => { - write!(f, "there was a network error - {}", err) - } - GatewayClientError::ConnectionAbruptlyClosed => { - write!(f, "connection was abruptly closed") - } - GatewayClientError::Timeout => write!(f, "timed out"), - GatewayClientError::MalformedResponse => write!(f, "received response was malformed"), - GatewayClientError::ConnectionInInvalidState => write!( - f, - "connection is in an invalid state - please send a bug report" - ), - GatewayClientError::AuthenticationFailure => write!(f, "authentication failure"), - GatewayClientError::GatewayError(err) => { - write!(f, "gateway returned an error response - {}", err) - } - } - } -} - // We have ownership over sink half of the connection, but the stream is owned // by some other task, however, we can notify it to get the stream back. struct PartiallyDelegated<'a> { @@ -129,9 +76,9 @@ impl<'a> PartiallyDelegated<'a> { // TODO: this can be potentially bad as we have no direct restrictions of ensuring it's called // within tokio runtime. Perhaps we should use the "old" way of passing explicit // runtime handle to the constructor and using that instead? - fn split_and_listen_for_sphinx_packets( + fn split_and_listen_for_mixnet_messages( conn: WsConn, - sphinx_packet_sender: SphinxPacketSender, + packet_router: PacketRouter, ) -> Result { // when called for, it NEEDS TO yield back the stream so that we could merge it and // read control request responses. @@ -140,7 +87,7 @@ impl<'a> PartiallyDelegated<'a> { let (sink, mut stream) = conn.split(); - let sphinx_receiver_future = async move { + let mixnet_receiver_future = async move { let mut should_return = false; while !should_return { tokio::select! { @@ -161,7 +108,7 @@ impl<'a> PartiallyDelegated<'a> { Message::Binary(bin_msg) => { // TODO: some batching mechanism to allow reading and sending more than // one packet at the time, because the receiver can easily handle it - sphinx_packet_sender.unbounded_send(vec![bin_msg]).unwrap() + packet_router.route_received(vec![bin_msg]) }, // I think that in the future we should perhaps have some sequence number system, i.e. // so each request/reponse pair can be easily identified, so that if messages are @@ -176,7 +123,7 @@ impl<'a> PartiallyDelegated<'a> { Ok(stream) }; - let spawned_boxed_task = tokio::spawn(sphinx_receiver_future) + let spawned_boxed_task = tokio::spawn(mixnet_receiver_future) .map(|join_handle| { join_handle.expect("task must have not failed to finish its execution!") }) @@ -244,7 +191,7 @@ pub struct GatewayClient<'a, R: IntoClientRequest + Unpin + Clone> { our_address: DestinationAddressBytes, auth_token: Option, connection: SocketState<'a>, - sphinx_packet_sender: SphinxPacketSender, + packet_router: PacketRouter, response_timeout_duration: Duration, } @@ -263,7 +210,8 @@ where gateway_address: R, our_address: DestinationAddressBytes, auth_token: Option, - sphinx_packet_sender: SphinxPacketSender, + mixnet_message_sender: MixnetMessageSender, + ack_sender: AcknowledgementSender, response_timeout_duration: Duration, ) -> Self { GatewayClient { @@ -272,11 +220,65 @@ where our_address, auth_token, connection: SocketState::NotConnected, - sphinx_packet_sender, + packet_router: PacketRouter::new(ack_sender, mixnet_message_sender), response_timeout_duration, } } + pub fn new_init( + gateway_address: R, + our_address: DestinationAddressBytes, + response_timeout_duration: Duration, + ) -> Self { + use futures::channel::mpsc; + + // note: this packet_router is completely invalid in normal circumstances, but "works" + // perfectly fine here, because it's not meant to be used + let (ack_tx, _) = mpsc::unbounded(); + let (mix_tx, _) = mpsc::unbounded(); + let packet_router = PacketRouter::new(ack_tx, mix_tx); + + GatewayClient { + authenticated: false, + gateway_address, + our_address, + auth_token: None, + connection: SocketState::NotConnected, + packet_router, + response_timeout_duration, + } + } + + pub async fn register_without_listening(&mut self) -> Result { + if !self.connection.is_established() { + self.establish_connection().await?; + } + let msg = ClientControlRequest::new_register(self.our_address.clone()).into(); + let token = match self.send_websocket_message(msg).await? { + ServerResponse::Register { token } => { + self.authenticated = true; + Ok(AuthToken::try_from_base58_string(token)?) + } + ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), + _ => unreachable!(), + }?; + Ok(token) + } + + pub async fn close_connection(&mut self) -> Result<(), GatewayClientError> { + if self.connection.is_partially_delegated() { + self.recover_socket_connection().await?; + } + + match std::mem::replace(&mut self.connection, SocketState::NotConnected) { + SocketState::Available(mut socket) => Ok(socket.close(None).await?), + SocketState::PartiallyDelegated(_) => { + unreachable!("this branch should have never been reached!") + } + _ => Ok(()), // no need to do anything in those cases + } + } + pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> { let ws_stream = match connect_async(self.gateway_address.clone()).await { Ok((ws_stream, _)) => ws_stream, @@ -320,7 +322,7 @@ where }; match msg { Message::Binary(bin_msg) => { - self.sphinx_packet_sender.unbounded_send(vec![bin_msg]).unwrap() + self.packet_router.route_received(vec![bin_msg]); } Message::Text(txt_msg) => { res = Some(ServerResponse::try_from(txt_msg).map_err(|_| GatewayClientError::MalformedResponse)); @@ -340,10 +342,10 @@ where &mut self, msg: Message, ) -> Result { - let mut should_restart_sphinx_listener = false; + let mut should_restart_mixnet_listener = false; if self.connection.is_partially_delegated() { self.recover_socket_connection().await?; - should_restart_sphinx_listener = true; + should_restart_mixnet_listener = true; } let conn = match self.connection { @@ -354,8 +356,8 @@ where conn.send(msg).await?; let response = self.read_control_response().await; - if should_restart_sphinx_listener { - self.start_listening_for_sphinx_packets()?; + if should_restart_mixnet_listener { + self.start_listening_for_mixnet_messages()?; } response } @@ -387,7 +389,7 @@ where ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), _ => unreachable!(), }?; - self.start_listening_for_sphinx_packets()?; + self.start_listening_for_mixnet_messages()?; Ok(token) } @@ -414,7 +416,7 @@ where ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), _ => unreachable!(), }?; - self.start_listening_for_sphinx_packets()?; + self.start_listening_for_mixnet_messages()?; Ok(authenticated) } @@ -468,7 +470,7 @@ where Ok(()) } - fn start_listening_for_sphinx_packets(&mut self) -> Result<(), GatewayClientError> { + fn start_listening_for_mixnet_messages(&mut self) -> Result<(), GatewayClientError> { if self.connection.is_partially_delegated() { return Ok(()); } @@ -479,9 +481,9 @@ where let partially_delegated = match std::mem::replace(&mut self.connection, SocketState::Invalid) { SocketState::Available(conn) => { - PartiallyDelegated::split_and_listen_for_sphinx_packets( + PartiallyDelegated::split_and_listen_for_mixnet_messages( conn, - self.sphinx_packet_sender.clone(), + self.packet_router.clone(), )? } _ => unreachable!(), diff --git a/common/client-libs/gateway-client/src/packet_router.rs b/common/client-libs/gateway-client/src/packet_router.rs new file mode 100644 index 0000000000..fcd4f6224c --- /dev/null +++ b/common/client-libs/gateway-client/src/packet_router.rs @@ -0,0 +1,83 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +// JS: I personally don't like this name very much, but could not think of anything better. +// I will gladly take any suggestions on how to rename this. + +use futures::channel::mpsc; +use log::*; +use nymsphinx::params::packet_sizes::PacketSize; + +pub type MixnetMessageSender = mpsc::UnboundedSender>>; +pub type MixnetMessageReceiver = mpsc::UnboundedReceiver>>; + +pub type AcknowledgementSender = mpsc::UnboundedSender>>; +pub type AcknowledgementReceiver = mpsc::UnboundedReceiver>>; + +#[derive(Clone, Debug)] +pub(super) struct PacketRouter { + ack_sender: AcknowledgementSender, + mixnet_message_sender: MixnetMessageSender, +} + +impl PacketRouter { + pub(super) fn new( + ack_sender: AcknowledgementSender, + mixnet_message_sender: MixnetMessageSender, + ) -> Self { + PacketRouter { + ack_sender, + mixnet_message_sender, + } + } + + pub(super) fn route_received(&self, unwrapped_packets: Vec>) { + let mut received_messages = Vec::new(); + let mut received_acks = Vec::new(); + + for received_packet in unwrapped_packets { + // TODO: currently this is not true because gateways are removing padding from the packets + // but will be fixed soon enough by all other changes in the pipeline + // the question is, however, what exactly will gateways be returning instead. payloads? + // 'plaintext'?. To be determined later on. + // if received_packet.len() == PacketSize::ACKPacket.payload_size() { + + // this is an extremely ugly if statement, but will be improved once things are actually + // constant length everywhere + if received_packet.len() == 21 { + received_acks.push(received_packet); + } else { + // well, technically all 21 bytes packets will be considered acks which is not + // entirely true, but for time being let's stick with it until other changes are + // introduced + received_messages.push(received_packet); + } + } + + // due to how we are currently using it, those unwraps can't fail, but if we ever + // wanted to make `gateway-client` into some more generic library, we would probably need + // to catch that error or something. + if !received_messages.is_empty() { + trace!("routing 'real'"); + self.mixnet_message_sender + .unbounded_send(received_messages) + .unwrap(); + } + + if !received_acks.is_empty() { + trace!("routing acks"); + self.ack_sender.unbounded_send(received_acks).unwrap(); + } + } +} diff --git a/common/healthcheck/src/path_check.rs b/common/healthcheck/src/path_check.rs index b61f921df8..f7fc2a0e6d 100644 --- a/common/healthcheck/src/path_check.rs +++ b/common/healthcheck/src/path_check.rs @@ -285,7 +285,6 @@ impl PathChecker { &path[..], &self.our_destination, &delays, - None, ) .unwrap(); diff --git a/common/nymsphinx/Cargo.toml b/common/nymsphinx/Cargo.toml index df40720199..b300b5fad0 100644 --- a/common/nymsphinx/Cargo.toml +++ b/common/nymsphinx/Cargo.toml @@ -7,12 +7,14 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bytes = "0.5" -log = "0.4.8" rand = {version = "0.7.3", features = ["wasm-bindgen"]} rand_distr = "0.2.2" -tokio-util = { version = "0.3.1", features = ["codec"] } -## will be moved to proper dependencies once released -sphinx = { git = "https://github.com/nymtech/sphinx", rev="fcd17932acbd21a76c42a4bf2a42b9f8668f7e1f" } +nymsphinx-acknowledgements = { path = "acknowledgements" } +nymsphinx-addressing = { path = "addressing" } +nymsphinx-chunking = { path = "chunking" } +nymsphinx-cover = { path = "cover" } +nymsphinx-framing = { path = "framing" } +nymsphinx-params = { path = "params" } +nymsphinx-types = { path = "types" } diff --git a/common/nymsphinx/acknowledgements/Cargo.toml b/common/nymsphinx/acknowledgements/Cargo.toml new file mode 100644 index 0000000000..c697fccd87 --- /dev/null +++ b/common/nymsphinx/acknowledgements/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "nymsphinx-acknowledgements" +version = "0.1.0" +authors = ["Jedrzej Stuczynski "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +aes-ctr = "0.4.0" +rand = {version = "0.7.3", features = ["wasm-bindgen"]} + + + +nymsphinx-addressing = { path = "../addressing" } +nymsphinx-params = { path = "../params" } +nymsphinx-types = { path = "../types" } + +topology = { path = "../../topology" } \ No newline at end of file diff --git a/common/nymsphinx/acknowledgements/src/identifier.rs b/common/nymsphinx/acknowledgements/src/identifier.rs new file mode 100644 index 0000000000..6ff2406ce3 --- /dev/null +++ b/common/nymsphinx/acknowledgements/src/identifier.rs @@ -0,0 +1,103 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use aes_ctr::{ + stream_cipher::{ + generic_array::{ + typenum::{marker_traits::Unsigned, U16}, + GenericArray, + }, + NewStreamCipher, SyncStreamCipher, + }, + Aes128Ctr, +}; +use nymsphinx_params::packet_sizes::PacketSize; +use rand::{CryptoRng, RngCore}; + +// the 'U16' type is taken directly from the `Ctr128` for consistency sake +pub type Aes128KeySize = U16; +pub type Aes128NonceSize = U16; + +pub type AckAes128Key = GenericArray; +type AckAes128IV = GenericArray; + +pub fn generate_key(rng: &mut R) -> AckAes128Key { + let mut ack_key = GenericArray::default(); + rng.fill_bytes(&mut ack_key); + ack_key +} + +fn random_iv(rng: &mut R) -> AckAes128IV { + let mut iv = GenericArray::default(); + rng.fill_bytes(&mut iv); + iv +} + +pub fn prepare_identifier( + rng: &mut R, + key: &AckAes128Key, + marshaled_id: &[u8], +) -> Vec { + // TODO: should we have some length checks on the id? + + let iv = random_iv(rng); + let mut cipher = Aes128Ctr::new(key, &iv); + let mut output = marshaled_id.to_vec(); + + cipher.apply_keystream(&mut output); + + iv.into_iter().chain(output.into_iter()).collect() +} + +pub fn recover_identifier(key: &AckAes128Key, iv_ciphertext: &[u8]) -> Option> { + // first few bytes are expected to be the concatenated IV. It must be followed by at least 1 more + // byte that we wish to recover, but it can be no longer from what we can physically store inside + // an ack + if iv_ciphertext.len() <= Aes128NonceSize::to_usize() + || iv_ciphertext.len() > PacketSize::ACKPacket.plaintext_size() + { + return None; + } + + let iv = GenericArray::from_slice(&iv_ciphertext[..Aes128NonceSize::to_usize()]); + let mut cipher = Aes128Ctr::new(key, &iv); + let mut output = iv_ciphertext[Aes128NonceSize::to_usize()..].to_vec(); + cipher.apply_keystream(&mut output); + + Some(output) +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::rngs::OsRng; + + #[test] + fn id_is_recoverable() { + let mut rng = OsRng; + let key = generate_key(&mut rng); + + let id1 = vec![42]; // single byte case + let id2 = vec![1, 2, 3, 4, 5]; // 5byte we expect to use + let id3 = vec![42; 8]; // some reasonable upper bound id size we could use later on + + let iv_ciphertext1 = prepare_identifier(&mut rng, &key, &id1); + let iv_ciphertext2 = prepare_identifier(&mut rng, &key, &id2); + let iv_ciphertext3 = prepare_identifier(&mut rng, &key, &id3); + + assert_eq!(id1, recover_identifier(&key, &iv_ciphertext1).unwrap()); + assert_eq!(id2, recover_identifier(&key, &iv_ciphertext2).unwrap()); + assert_eq!(id3, recover_identifier(&key, &iv_ciphertext3).unwrap()); + } +} diff --git a/common/nymsphinx/src/utils/poisson.rs b/common/nymsphinx/acknowledgements/src/lib.rs similarity index 55% rename from common/nymsphinx/src/utils/poisson.rs rename to common/nymsphinx/acknowledgements/src/lib.rs index 22414cb402..b091185843 100644 --- a/common/nymsphinx/src/utils/poisson.rs +++ b/common/nymsphinx/acknowledgements/src/lib.rs @@ -12,13 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use rand_distr::{Distribution, Exp}; -use std::time; +pub mod identifier; +pub mod surb_ack; -pub fn sample(average_duration: time::Duration) -> time::Duration { - // this is our internal code used by our traffic streams - // the error is only thrown if average delay is less than 0, which will never happen - // so call to unwrap is perfectly safe here - let exp = Exp::new(1.0 / average_duration.as_nanos() as f64).unwrap(); - time::Duration::from_nanos(exp.sample(&mut rand::thread_rng()).round() as u64) -} +pub use identifier::{generate_key, AckAes128Key}; diff --git a/common/nymsphinx/acknowledgements/src/surb_ack.rs b/common/nymsphinx/acknowledgements/src/surb_ack.rs new file mode 100644 index 0000000000..953ab9da1e --- /dev/null +++ b/common/nymsphinx/acknowledgements/src/surb_ack.rs @@ -0,0 +1,119 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use crate::identifier::{prepare_identifier, AckAes128Key}; +use nymsphinx_addressing::clients::Recipient; +use nymsphinx_addressing::nodes::{NymNodeRoutingAddress, MAX_NODE_ADDRESS_UNPADDED_LEN}; +use nymsphinx_params::packet_sizes::PacketSize; +use nymsphinx_types::builder::SphinxPacketBuilder; +use nymsphinx_types::{ + delays::{self, Delay}, + Destination, SphinxPacket, +}; +use rand::{CryptoRng, RngCore}; +use std::convert::TryFrom; +use std::time; +use topology::{NymTopology, NymTopologyError}; + +#[allow(non_snake_case)] +pub struct SURBAck { + surb_ack_packet: SphinxPacket, + first_hop_address: NymNodeRoutingAddress, + expected_total_delay: Delay, +} + +#[derive(Debug)] +pub enum SURBAckRecoveryError { + InvalidPacketSize, + InvalidAddress, + InvalidSphinxPacket, +} + +impl SURBAck { + pub fn construct( + rng: &mut R, + recipient: &Recipient, + ack_key: &AckAes128Key, + marshaled_fragment_id: &[u8], + average_delay: time::Duration, + topology: &T, + ) -> Result + where + R: RngCore + CryptoRng, + T: NymTopology, + { + let route = topology.random_route_to_gateway(&recipient.gateway())?; + let delays = delays::generate_from_average_duration(route.len(), average_delay); + let destination = Destination::new(recipient.destination(), Default::default()); + + let surb_ack_payload = prepare_identifier(rng, ack_key, marshaled_fragment_id); + + // once merged, that's an easy rng injection point for sphinx packets : ) + let surb_ack_packet = SphinxPacketBuilder::new() + .with_payload_size(PacketSize::ACKPacket.payload_size()) + .build_packet(surb_ack_payload, &route, &destination, &delays) + .unwrap(); + + let expected_total_delay = delays.iter().sum(); + let first_hop_address = + NymNodeRoutingAddress::try_from(route.first().unwrap().address.clone()).unwrap(); + + Ok(SURBAck { + surb_ack_packet, + first_hop_address, + expected_total_delay, + }) + } + + pub fn len() -> usize { + // TODO: this will be variable once/if we decide to introduce optimization described + // in common/nymsphinx/chunking/src/lib.rs:available_plaintext_size() + PacketSize::ACKPacket.size() + MAX_NODE_ADDRESS_UNPADDED_LEN + } + + pub fn prepare_for_sending(self) -> (Delay, Vec) { + // SURB_FIRST_HOP || SURB_ACK + let surb_bytes: Vec<_> = self + .first_hop_address + .as_zero_padded_bytes(MAX_NODE_ADDRESS_UNPADDED_LEN) + .into_iter() + .chain(self.surb_ack_packet.to_bytes().into_iter()) + .collect(); + (self.expected_total_delay, surb_bytes) + } + + // partial reciprocal of `prepare_for_sending` performed by the gateway + pub fn try_recover_first_hop_packet( + b: &[u8], + ) -> Result<(NymNodeRoutingAddress, SphinxPacket), SURBAckRecoveryError> { + if b.len() != Self::len() { + Err(SURBAckRecoveryError::InvalidPacketSize) + } else { + let address = match NymNodeRoutingAddress::try_from_bytes(&b) { + Ok(address) => address, + Err(_) => return Err(SURBAckRecoveryError::InvalidAddress), + }; + + // TODO: this will be variable once/if we decide to introduce optimization described + // in common/nymsphinx/chunking/src/lib.rs:available_plaintext_size() + let address_offset = MAX_NODE_ADDRESS_UNPADDED_LEN; + let packet = match SphinxPacket::from_bytes(&b[address_offset..]) { + Ok(packet) => packet, + Err(_) => return Err(SURBAckRecoveryError::InvalidSphinxPacket), + }; + + Ok((address, packet)) + } + } +} diff --git a/common/nymsphinx/addressing/Cargo.toml b/common/nymsphinx/addressing/Cargo.toml new file mode 100644 index 0000000000..f4cee942bd --- /dev/null +++ b/common/nymsphinx/addressing/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "nymsphinx-addressing" +version = "0.1.0" +authors = ["Jedrzej Stuczynski "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +nymsphinx-types = { path = "../types" } \ No newline at end of file diff --git a/common/nymsphinx/src/addressing/clients.rs b/common/nymsphinx/addressing/src/clients.rs similarity index 95% rename from common/nymsphinx/src/addressing/clients.rs rename to common/nymsphinx/addressing/src/clients.rs index b64e4a3a47..676368c13b 100644 --- a/common/nymsphinx/src/addressing/clients.rs +++ b/common/nymsphinx/addressing/src/clients.rs @@ -4,15 +4,15 @@ // of a helper/utils structure, because before it reaches the gateway // it's already destructed). -use crate::{ +use nymsphinx_types::{ DestinationAddressBytes, NodeAddressBytes, DESTINATION_ADDRESS_LENGTH, NODE_ADDRESS_LENGTH, }; #[derive(Debug)] pub struct RecipientFormattingError; -impl From for RecipientFormattingError { - fn from(_: crate::Error) -> Self { +impl From for RecipientFormattingError { + fn from(_: nymsphinx_types::Error) -> Self { Self } } diff --git a/common/nymsphinx/src/addressing/mod.rs b/common/nymsphinx/addressing/src/lib.rs similarity index 100% rename from common/nymsphinx/src/addressing/mod.rs rename to common/nymsphinx/addressing/src/lib.rs diff --git a/common/nymsphinx/src/addressing/nodes.rs b/common/nymsphinx/addressing/src/nodes.rs similarity index 89% rename from common/nymsphinx/src/addressing/nodes.rs rename to common/nymsphinx/addressing/src/nodes.rs index 8acd798f68..ebcbd0b789 100644 --- a/common/nymsphinx/src/addressing/nodes.rs +++ b/common/nymsphinx/addressing/src/nodes.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::{NodeAddressBytes, NODE_ADDRESS_LENGTH}; +use nymsphinx_types::{NodeAddressBytes, NODE_ADDRESS_LENGTH}; use std::convert::{TryFrom, TryInto}; use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; @@ -21,6 +21,10 @@ use std::net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}; /// Currently, that routing information is an IP address, but in principle it can be anything /// for as long as it's going to fit in the field. +/// MAX_UNPADDED_LEN represents maximum length an unpadded address could have. +/// In this case it's an ipv6 socket address (with version prefix) +pub const MAX_NODE_ADDRESS_UNPADDED_LEN: usize = 19; + #[derive(Debug)] pub enum NymNodeRoutingAddressError { InsufficientNumberOfBytesAvailableError, @@ -59,6 +63,23 @@ impl NymNodeRoutingAddress { .collect() } + /// Converts self into a vector of bytes optionally padded with zeroes to the `expected_len`. + /// Note this does not necessarily represent a NodeAddressBytes, unless + /// `expected_len` == NODE_ADDRESS_LENGTH + pub fn as_zero_padded_bytes(&self, expected_len: usize) -> Vec { + let self_bytes = self.as_bytes(); + if self_bytes.len() >= expected_len { + // can't add padding + self_bytes + } else { + self_bytes + .into_iter() + .chain(std::iter::repeat(0)) + .take(expected_len) + .collect() + } + } + /// Tries to recover `Self` from a bytes slice. /// Does not care if it's zero-padded or not. pub fn try_from_bytes(b: &[u8]) -> Result { @@ -129,12 +150,7 @@ impl TryInto for NymNodeRoutingAddress { return Err(NymNodeRoutingAddressError::InsufficientNumberOfBytesAvailableError); } - let unpadded_address = self.as_bytes(); - let padded_address: Vec<_> = unpadded_address - .into_iter() - .chain(std::iter::repeat(0)) - .take(NODE_ADDRESS_LENGTH) - .collect(); + let padded_address = self.as_zero_padded_bytes(NODE_ADDRESS_LENGTH); let mut node_address_bytes = [0u8; 32]; node_address_bytes.copy_from_slice(&padded_address); diff --git a/common/nymsphinx/chunking/Cargo.toml b/common/nymsphinx/chunking/Cargo.toml new file mode 100644 index 0000000000..88f53ce322 --- /dev/null +++ b/common/nymsphinx/chunking/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "nymsphinx-chunking" +version = "0.1.0" +authors = ["Jedrzej Stuczynski "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +log = "0.4.8" +rand = { version = "0.7.3", features = ["wasm-bindgen"] } + +nymsphinx-acknowledgements = { path = "../acknowledgements" } +nymsphinx-addressing = { path = "../addressing" } +nymsphinx-params = { path = "../params" } +nymsphinx-types = { path = "../types" } +topology = { path = "../../topology" } \ No newline at end of file diff --git a/common/nymsphinx/chunking/src/fragment.rs b/common/nymsphinx/chunking/src/fragment.rs new file mode 100644 index 0000000000..76d9c432f0 --- /dev/null +++ b/common/nymsphinx/chunking/src/fragment.rs @@ -0,0 +1,1100 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use crate::ChunkingError; +use std::convert::TryInto; + +// Personal reflection: In hindsight I've spent too much time on relatively too little +// gain here, as even though I might have saved couple of bytes per packet, the gain +// is negligible in the context of having to include SURB-ACKs and reply-SURBs in the packets. +// +// However, if we really cared about those tiny optimisations, `UNLINKED_FRAGMENTED_HEADER` +// could be further compressed: if current_fragment != 1 && current_fragment != 255, you don't +// need to use the tail byte to indicate lack of linking as it can be implied from the fragment +// position. + +// TODO for later: with the removal of 'unfragmented' fragments, the first bit of each header +// is completely useless, we should then think how to make the set_id become u32 instead of i32. +// (the current limitation for making the seemingly trivial change is "linked id" which +// has to have same amount of space available and right now it only has 31 bits available) + +/// When the underlying message has to be split into multiple Fragments, but still manages to fit +/// into a single `FragmentSet`, each `FragmentHeader` needs to hold additional information to allow +/// for correct message reconstruction: 4 bytes for set id, 1 byte to represent total number +/// of fragments in the set, 1 byte to represent position of the current fragment in the set +/// and finally an extra byte to indicate the fragment has no links to other sets. +pub const UNLINKED_FRAGMENTED_HEADER_LEN: usize = 7; + +/// Logically almost identical to `UNLINKED_FRAGMENTED_HEADER_LEN`, however, the extra three +/// bytes are due to changing the final byte that used to indicate the `Fragment` is not linked +/// into 4 byte id of either previous or the next set. +/// Note that the linked headers can potentially be used only for very first and very last +/// `Fragment` in a `FragmentSet`. +pub const LINKED_FRAGMENTED_HEADER_LEN: usize = 10; + +/// Maximum size of payload of each fragment is always the maximum amount of plaintext data +/// we can put into a sphinx packet minus length of respective fragment header. +pub const fn unlinked_fragment_payload_max_len(max_plaintext_size: usize) -> usize { + max_plaintext_size - UNLINKED_FRAGMENTED_HEADER_LEN +} + +/// Maximum size of payload of each fragment is always the maximum amount of plaintext data +/// we can put into a sphinx packet minus length of respective fragment header. +pub const fn linked_fragment_payload_max_len(max_plaintext_size: usize) -> usize { + max_plaintext_size - LINKED_FRAGMENTED_HEADER_LEN +} + +// TODO: should this be defined in this module or in `cover`? I can see arguments for both options... +/// A special `FragmentIdentifier` that is not valid in all cases unless if it's used in a loop +/// cover message. +pub const COVER_FRAG_ID: FragmentIdentifier = FragmentIdentifier { + set_id: 0, + fragment_position: 0, +}; + +/// Identifier to uniquely identify a fragment. It represents 31bit ID of given `FragmentSet` +/// and u8 position of the `Fragment` in the set. +#[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] +pub struct FragmentIdentifier { + set_id: i32, + fragment_position: u8, +} + +impl FragmentIdentifier { + pub fn to_bytes(&self) -> Vec { + self.set_id + .to_be_bytes() + .iter() + .cloned() + .chain(std::iter::once(self.fragment_position)) + .collect() + } + + pub fn try_from_bytes(b: &[u8]) -> Result { + if b.len() != 5 { + return Err(ChunkingError::MalformedFragmentIdentifier); + } + + let set_id = i32::from_be_bytes([b[0], b[1], b[2], b[3]]); + // set_id == 0 is valid for, and only for, COVER_FRAG_ID + if set_id < 0 { + return Err(ChunkingError::MalformedFragmentIdentifier); + } + + Ok(FragmentIdentifier { + set_id, + fragment_position: b[4], + }) + } +} + +/// The basic unit of division of underlying bytes message sent through the mix network. +/// Each `Fragment` after being marshaled is guaranteed to fit into a single sphinx packet. +/// The `Fragment` itself consists of part, or whole of, message to be sent as well as additional +/// header used to reconstruct the message after being received. +#[derive(PartialEq, Clone, Debug)] +pub struct Fragment { + header: FragmentHeader, + payload: Vec, +} + +impl Fragment { + /// Tries to encapsulate provided payload slice and metadata into a `Fragment`. + /// It can fail if payload would not fully fit in a single `Fragment` or some of the metadata + /// is malformed or self-contradictory, for example if current_fragment > total_fragments. + pub(crate) fn try_new( + payload: &[u8], + id: i32, + total_fragments: u8, + current_fragment: u8, + previous_fragments_set_id: Option, + next_fragments_set_id: Option, + max_plaintext_size: usize, + ) -> Result { + let header = FragmentHeader::try_new( + id, + total_fragments, + current_fragment, + previous_fragments_set_id, + next_fragments_set_id, + )?; + + // check for whether payload has expected length, which depend on whether fragment is linked + // and if it's the only one or the last one in the set (then lower bound is removed) + if previous_fragments_set_id.is_some() { + if total_fragments > 1 { + if payload.len() != linked_fragment_payload_max_len(max_plaintext_size) { + return Err(ChunkingError::InvalidPayloadLengthError); + } + } else { + if payload.len() > linked_fragment_payload_max_len(max_plaintext_size) { + return Err(ChunkingError::InvalidPayloadLengthError); + } + } + } else if next_fragments_set_id.is_some() { + if payload.len() != linked_fragment_payload_max_len(max_plaintext_size) { + return Err(ChunkingError::InvalidPayloadLengthError); + } + } else { + if total_fragments != current_fragment { + if payload.len() != unlinked_fragment_payload_max_len(max_plaintext_size) { + return Err(ChunkingError::InvalidPayloadLengthError); + } + } else { + if payload.len() > unlinked_fragment_payload_max_len(max_plaintext_size) { + return Err(ChunkingError::InvalidPayloadLengthError); + } + } + } + + Ok(Fragment { + header, + payload: payload.to_vec(), + }) + } + + /// Convert this `Fragment` into vector of bytes which can be put into a sphinx packet. + pub(crate) fn into_bytes(self) -> Vec { + self.header + .to_bytes() + .into_iter() + .chain(self.payload.into_iter()) + .collect() + } + + /// Derive identifier unique for this particular fragment + pub fn fragment_identifier(&self) -> FragmentIdentifier { + FragmentIdentifier { + set_id: self.header.id, + fragment_position: self.header.current_fragment, + } + } + + /// Extracts id of this `Fragment`. + pub fn id(&self) -> i32 { + self.header.id + } + + /// Extracts total number of fragments associated with this particular `Fragment` (belonging to + /// the same `FragmentSet`). + pub fn total_fragments(&self) -> u8 { + self.header.total_fragments + } + + /// Extracts position of this `Fragment` in a `FragmentSet`. + pub fn current_fragment(&self) -> u8 { + self.header.current_fragment + } + + /// Extracts information regarding id of pre-linked `FragmentSet` + pub fn previous_fragments_set_id(&self) -> Option { + self.header.previous_fragments_set_id + } + + /// Extracts information regarding id of post-linked `FragmentSet` + pub fn next_fragments_set_id(&self) -> Option { + self.header.next_fragments_set_id + } + + /// Consumes `self` to obtain payload (i.e. part of original message) associated with this + /// `Fragment`. + pub(crate) fn extract_payload(self) -> Vec { + self.payload + } + + /// Tries to recover `Fragment` from slice of bytes extracted from received sphinx packet. + /// It can fail if payload would not fully fit in a single `Fragment` or some of the metadata + /// is malformed or self-contradictory, for example if current_fragment > total_fragments. + pub(crate) fn try_from_bytes(b: &[u8]) -> Result { + let (header, n) = FragmentHeader::try_from_bytes(b)?; + + // there's no sane way to decide if payload has correct range anymore as + // it's no longer fixed + + Ok(Fragment { + header, + payload: b[n..].to_vec(), + }) + } +} + +/// In order to be able to re-assemble fragmented message sent through a mix-network, some +/// metadata is attached alongside the actual message payload. The idea is to include as little +/// of that data as possible due to computationally very costly process of sphinx encapsulation. +/// +/// The generic `FragmentHeader` is represented as follows: +/// IF flag || 31 bit ID || TotalFragments || CurrentFragment || LID flag || 31 bit Linked ID +/// note that LID is a valid flag only for first and +/// last fragment (if TotalFragments == CurrentFragment == 255) in given set. +/// +/// further note if LID is not set, +/// then the Linked ID bytes in the header are used as payload. +/// +/// Hence after marshaling `FragmentHeader` into bytes, +/// the following three alternatives are possible: +/// +/// 7 byte long sequence representing that this `Fragment` is one of multiple ones in the set. +/// However, the set is not linked to any other sets: +/// '1'bit || 31-bit ID || 1-byte TF || 1 byte CF || '0'byte +/// +/// 10 byte sequence representing first (or last) fragment in the set, +/// where the set is linked to either preceding data (TF == 1) or proceeding data (TF == CF == 255) +/// '1'bit || 31-bit ID || 1-byte TF || 1 byte CF || '1'bit || 31-bit LID +/// +/// And hence for messages larger than `max_plaintext_size` but small enough +/// to avoid set division (which happens if message has to be fragmented into more than 255 fragments) +/// there is 7 bytes of overhead inside each sphinx packet sent +/// and for the longest messages, without upper bound, there is usually also only 7 bytes +/// of overhead apart from first and last fragments in each set that instead have 10 bytes of overhead. +#[derive(PartialEq, Clone, Debug)] +pub(crate) struct FragmentHeader { + /// ID associated with `FragmentSet` to which this particular `Fragment` belongs. + /// Its value is restricted to (0, i32::max_value()]. + /// Note that it *excludes* 0, but *includes* i32::max_value(). + /// This allows the field to be represented using 31 bits. + id: i32, + + /// Total number of `Fragment`s in `FragmentSet` used to be able to determine if entire + /// set was fully received as well as to perform bound checks. + total_fragments: u8, + + /// Since message is always fragmented into payloads of constant lengths + /// (apart from possibly the last one), there's no need to use offsets like ipv4/ipv6 + /// and we can just simply enumerate the fragments to later reconstruct the message. + current_fragment: u8, + + /// Optional ID of previous `FragmentSet` into which the original message was split. + /// Note, this option is only valid of `current_fragment == 1` + previous_fragments_set_id: Option, + + /// Optional ID of next `FragmentSet` into which the original message was split. + /// Note, this option is only valid of `current_fragment == total_fragments == u8::max_value()` + next_fragments_set_id: Option, +} + +impl FragmentHeader { + /// Tries to create a new `FragmentHeader` using provided metadata. Bunch of logical + /// checks are performed to see if the data is not self-contradictory, + /// for example if current_fragment > total_fragments. + fn try_new( + id: i32, + total_fragments: u8, + current_fragment: u8, + previous_fragments_set_id: Option, + next_fragments_set_id: Option, + ) -> Result { + if id <= 0 { + return Err(ChunkingError::MalformedHeaderError); + } + if total_fragments < current_fragment { + return Err(ChunkingError::MalformedHeaderError); + } + if total_fragments == 0 { + return Err(ChunkingError::MalformedHeaderError); + } + if current_fragment == 0 { + return Err(ChunkingError::MalformedHeaderError); + } + if let Some(pfid) = previous_fragments_set_id { + if pfid <= 0 || current_fragment != 1 || pfid == id { + return Err(ChunkingError::MalformedHeaderError); + } + } + if let Some(nfid) = next_fragments_set_id { + if nfid <= 0 || current_fragment != total_fragments || nfid == id { + return Err(ChunkingError::MalformedHeaderError); + } + } + + Ok(FragmentHeader { + id, + total_fragments, + current_fragment, + previous_fragments_set_id, + next_fragments_set_id, + }) + } + + /// Tries to recover `FragmentHeader` from slice of bytes extracted from received sphinx packet. + /// If successful, returns `Self` and number of bytes used, as those can differ based on the + /// type of header (unlinked or linked). + fn try_from_bytes(b: &[u8]) -> Result<(Self, usize), ChunkingError> { + // header needs to be at least 7 bytes long + if b.len() < UNLINKED_FRAGMENTED_HEADER_LEN { + return Err(ChunkingError::TooShortFragmentData); + } + let frag_id = i32::from_be_bytes(b[0..4].try_into().unwrap()); + // sanity check for the fragmentation flag + if ((frag_id >> 31) & 1) == 0 { + return Err(ChunkingError::MalformedHeaderError); + } + + let id = frag_id & !(1 << 31); // make sure to clear the flag bit to parse id correctly + let total_fragments = b[4]; + let current_fragment = b[5]; + + if total_fragments == 0 || current_fragment == 0 || current_fragment > total_fragments { + return Err(ChunkingError::MalformedHeaderError); + } + + let mut previous_fragments_set_id = None; + let mut next_fragments_set_id = None; + + // check if the linking id flag might be set + let read_bytes = if b[6] != 0 { + // there's linking ID supposedly attached, make sure we have enough bytes to parse + if b.len() < LINKED_FRAGMENTED_HEADER_LEN { + return Err(ChunkingError::TooShortFragmentData); + } + let flagged_linked_id = i32::from_be_bytes(b[6..10].try_into().unwrap()); + + // sanity check for the linked flag + if ((flagged_linked_id >> 31) & 1) == 0 { + return Err(ChunkingError::MalformedHeaderError); + } + + let linked_id = flagged_linked_id & !(1 << 31); // make sure to clear the flag bit to parse id correctly + + if current_fragment == 1 { + previous_fragments_set_id = Some(linked_id); + } else if total_fragments == current_fragment && current_fragment == u8::max_value() { + next_fragments_set_id = Some(linked_id); + } else { + return Err(ChunkingError::MalformedHeaderError); + } + + 10 + } else { + 7 + }; + + Ok(( + Self::try_new( + id, + total_fragments, + current_fragment, + previous_fragments_set_id, + next_fragments_set_id, + )?, + read_bytes, + )) + } + + /// Marshal this `FragmentHeader` into vector of bytes which can be put into a sphinx packet. + fn to_bytes(&self) -> Vec { + let frag_id = self.id | (1 << 31); + let frag_id_bytes = frag_id.to_be_bytes(); + let bytes_prefix_iter = frag_id_bytes + .iter() + .cloned() + .chain(std::iter::once(self.total_fragments)) + .chain(std::iter::once(self.current_fragment)); + + let is_linked = + self.previous_fragments_set_id.is_some() || self.next_fragments_set_id.is_some(); + if is_linked { + let linked_id = self + .previous_fragments_set_id + .unwrap_or_else(|| self.next_fragments_set_id.unwrap()); + let linked_id_entry = linked_id | (1 << 31); + let linked_id_bytes = linked_id_entry.to_be_bytes(); + bytes_prefix_iter + .chain(linked_id_bytes.iter().cloned()) + .collect() + } else { + bytes_prefix_iter.chain(std::iter::once(0)).collect() + } + } +} + +// everything below are tests + +#[cfg(test)] +mod fragment { + use super::*; + use nymsphinx_params::packet_sizes::PacketSize; + use rand::{thread_rng, RngCore}; + + fn max_plaintext_size() -> usize { + PacketSize::default().plaintext_size() - PacketSize::ACKPacket.size() + } + + #[test] + fn can_be_converted_to_and_from_bytes_for_unfragmented_payload() { + let mut rng = thread_rng(); + + let mlen = 40; + let mut valid_message = vec![0u8; mlen]; + rng.fill_bytes(&mut valid_message); + + let valid_unfragmented_packet = Fragment { + header: FragmentHeader::try_new(12345, 1, 1, None, None).unwrap(), + payload: valid_message, + }; + let packet_bytes = valid_unfragmented_packet.clone().into_bytes(); + assert_eq!( + valid_unfragmented_packet, + Fragment::try_from_bytes(&packet_bytes).unwrap() + ); + + let empty_unfragmented_packet = Fragment { + header: FragmentHeader::try_new(12345, 1, 1, None, None).unwrap(), + payload: Vec::new(), + }; + let packet_bytes = empty_unfragmented_packet.clone().into_bytes(); + assert_eq!( + empty_unfragmented_packet, + Fragment::try_from_bytes(&packet_bytes).unwrap() + ); + } + + #[test] + fn can_be_converted_to_and_from_bytes_for_unlinked_fragmented_payload() { + let mut rng = thread_rng(); + + let mut msg = vec![0u8; unlinked_fragment_payload_max_len(max_plaintext_size())]; + rng.fill_bytes(&mut msg); + + let non_last_packet = Fragment { + header: FragmentHeader::try_new(12345, 10, 5, None, None).unwrap(), + payload: msg, + }; + let packet_bytes = non_last_packet.clone().into_bytes(); + assert_eq!( + non_last_packet, + Fragment::try_from_bytes(&packet_bytes).unwrap() + ); + + let mut msg = vec![0u8; unlinked_fragment_payload_max_len(max_plaintext_size())]; + rng.fill_bytes(&mut msg); + + let last_full_packet = Fragment { + header: FragmentHeader::try_new(12345, 10, 10, None, None).unwrap(), + payload: msg, + }; + let packet_bytes = last_full_packet.clone().into_bytes(); + assert_eq!( + last_full_packet, + Fragment::try_from_bytes(&packet_bytes).unwrap() + ); + + let mut msg = vec![0u8; unlinked_fragment_payload_max_len(max_plaintext_size()) - 20]; + rng.fill_bytes(&mut msg); + + let last_non_full_packet = Fragment { + header: FragmentHeader::try_new(12345, 10, 10, None, None).unwrap(), + payload: msg, + }; + let packet_bytes = last_non_full_packet.clone().into_bytes(); + + assert_eq!( + last_non_full_packet, + Fragment::try_from_bytes(&packet_bytes).unwrap() + ); + } + + #[test] + fn can_be_converted_to_and_from_bytes_for_pre_linked_fragmented_payload() { + let mut rng = thread_rng(); + + let mut msg = vec![0u8; linked_fragment_payload_max_len(max_plaintext_size())]; + rng.fill_bytes(&mut msg); + + let fragment = Fragment { + header: FragmentHeader::try_new(12345, 10, 1, Some(1234), None).unwrap(), + payload: msg, + }; + let packet_bytes = fragment.clone().into_bytes(); + assert_eq!(fragment, Fragment::try_from_bytes(&packet_bytes).unwrap()); + + let mut msg = vec![0u8; linked_fragment_payload_max_len(max_plaintext_size()) - 20]; + rng.fill_bytes(&mut msg); + + let fragment = Fragment { + header: FragmentHeader::try_new(12345, 1, 1, Some(1234), None).unwrap(), + payload: msg, + }; + let packet_bytes = fragment.clone().into_bytes(); + // TODO: + // TODO: + // packet_bytes len assertion + assert_eq!(fragment, Fragment::try_from_bytes(&packet_bytes).unwrap()); + } + + #[test] + fn can_be_converted_to_and_from_bytes_for_post_linked_fragmented_payload() { + let mut rng = thread_rng(); + + let mut msg = vec![0u8; linked_fragment_payload_max_len(max_plaintext_size())]; + rng.fill_bytes(&mut msg); + + let fragment = Fragment { + header: FragmentHeader::try_new( + 12345, + u8::max_value(), + u8::max_value(), + None, + Some(1234), + ) + .unwrap(), + payload: msg, + }; + let packet_bytes = fragment.clone().into_bytes(); + assert_eq!(fragment, Fragment::try_from_bytes(&packet_bytes).unwrap()); + + let mut msg = vec![0u8; linked_fragment_payload_max_len(max_plaintext_size()) - 20]; + rng.fill_bytes(&mut msg); + + let fragment = Fragment { + header: FragmentHeader::try_new( + 12345, + u8::max_value(), + u8::max_value(), + None, + Some(1234), + ) + .unwrap(), + payload: msg, + }; + let packet_bytes = fragment.clone().into_bytes(); + // TODO: + // TODO: + // packet_bytes len assertion + + assert_eq!(fragment, Fragment::try_from_bytes(&packet_bytes).unwrap()); + } + + #[test] + fn unlinked_fragment_can_be_created_with_payload_of_valid_length() { + let id = 12345; + let full_payload = vec![1u8; unlinked_fragment_payload_max_len(max_plaintext_size())]; + let non_full_payload = + vec![1u8; unlinked_fragment_payload_max_len(max_plaintext_size()) - 1]; + let non_full_payload2 = + vec![1u8; unlinked_fragment_payload_max_len(max_plaintext_size()) - 60]; + + assert!( + Fragment::try_new(&full_payload, id, 10, 1, None, None, max_plaintext_size()).is_ok() + ); + assert!( + Fragment::try_new(&full_payload, id, 10, 5, None, None, max_plaintext_size()).is_ok() + ); + assert!( + Fragment::try_new(&full_payload, id, 10, 10, None, None, max_plaintext_size()).is_ok() + ); + assert!( + Fragment::try_new(&full_payload, id, 1, 1, None, None, max_plaintext_size()).is_ok() + ); + + assert!(Fragment::try_new( + &non_full_payload, + id, + 10, + 10, + None, + None, + max_plaintext_size() + ) + .is_ok()); + assert!(Fragment::try_new( + &non_full_payload, + id, + 1, + 1, + None, + None, + max_plaintext_size() + ) + .is_ok()); + + assert!(Fragment::try_new( + &non_full_payload2, + id, + 10, + 10, + None, + None, + max_plaintext_size() + ) + .is_ok()); + assert!(Fragment::try_new( + &non_full_payload2, + id, + 1, + 1, + None, + None, + max_plaintext_size() + ) + .is_ok()); + } + + #[test] + fn unlinked_fragment_returns_error_when_created_with_payload_of_invalid_length() { + let id = 12345; + let non_full_payload = + vec![1u8; unlinked_fragment_payload_max_len(max_plaintext_size()) - 1]; + let non_full_payload2 = + vec![1u8; unlinked_fragment_payload_max_len(max_plaintext_size()) - 20]; + let too_much_payload = + vec![1u8; unlinked_fragment_payload_max_len(max_plaintext_size()) + 1]; + + assert!(Fragment::try_new( + &non_full_payload, + id, + 10, + 1, + None, + None, + max_plaintext_size() + ) + .is_err()); + assert!(Fragment::try_new( + &non_full_payload, + id, + 10, + 5, + None, + None, + max_plaintext_size() + ) + .is_err()); + + assert!(Fragment::try_new( + &too_much_payload, + id, + 10, + 1, + None, + None, + max_plaintext_size() + ) + .is_err()); + assert!(Fragment::try_new( + &too_much_payload, + id, + 10, + 5, + None, + None, + max_plaintext_size() + ) + .is_err()); + assert!(Fragment::try_new( + &too_much_payload, + id, + 1, + 1, + None, + None, + max_plaintext_size() + ) + .is_err()); + + assert!(Fragment::try_new( + &non_full_payload2, + id, + 10, + 1, + None, + None, + max_plaintext_size() + ) + .is_err()); + assert!(Fragment::try_new( + &non_full_payload2, + id, + 10, + 5, + None, + None, + max_plaintext_size() + ) + .is_err()); + } + + #[test] + fn linked_fragment_can_be_created_with_payload_of_valid_length() { + let id = 12345; + let link_id = 1234; + let full_payload = vec![1u8; linked_fragment_payload_max_len(max_plaintext_size())]; + let non_full_payload = vec![1u8; linked_fragment_payload_max_len(max_plaintext_size()) - 1]; + let non_full_payload2 = + vec![1u8; linked_fragment_payload_max_len(max_plaintext_size()) - 20]; + + assert!(Fragment::try_new( + &full_payload, + id, + 10, + 1, + Some(link_id), + None, + max_plaintext_size() + ) + .is_ok()); + assert!(Fragment::try_new( + &full_payload, + id, + 1, + 1, + Some(link_id), + None, + max_plaintext_size() + ) + .is_ok()); + assert!(Fragment::try_new( + &non_full_payload, + id, + 1, + 1, + Some(link_id), + None, + max_plaintext_size() + ) + .is_ok()); + assert!(Fragment::try_new( + &non_full_payload2, + id, + 1, + 1, + Some(link_id), + None, + max_plaintext_size() + ) + .is_ok()); + + assert!(Fragment::try_new( + &full_payload, + id, + u8::max_value(), + u8::max_value(), + None, + Some(link_id), + max_plaintext_size() + ) + .is_ok()); + } + + #[test] + fn linked_fragment_returns_error_when_created_with_payload_of_invalid_length() { + let id = 12345; + let link_id = 1234; + let non_full_payload = vec![1u8; linked_fragment_payload_max_len(max_plaintext_size()) - 1]; + let non_full_payload2 = + vec![1u8; linked_fragment_payload_max_len(max_plaintext_size()) - 20]; + let too_much_payload = vec![1u8; linked_fragment_payload_max_len(max_plaintext_size()) + 1]; + + assert!(Fragment::try_new( + &non_full_payload, + id, + 10, + 1, + Some(link_id), + None, + max_plaintext_size() + ) + .is_err()); + assert!(Fragment::try_new( + &non_full_payload2, + id, + 10, + 1, + Some(link_id), + None, + max_plaintext_size() + ) + .is_err()); + assert!(Fragment::try_new( + &too_much_payload, + id, + 10, + 1, + Some(link_id), + None, + max_plaintext_size() + ) + .is_err()); + assert!(Fragment::try_new( + &too_much_payload, + id, + 1, + 1, + Some(link_id), + None, + max_plaintext_size() + ) + .is_err()); + + assert!(Fragment::try_new( + &non_full_payload, + id, + u8::max_value(), + u8::max_value(), + None, + Some(link_id), + max_plaintext_size() + ) + .is_err()); + assert!(Fragment::try_new( + &non_full_payload2, + id, + u8::max_value(), + u8::max_value(), + None, + Some(link_id), + max_plaintext_size() + ) + .is_err()); + + assert!(Fragment::try_new( + &too_much_payload, + id, + u8::max_value(), + u8::max_value(), + None, + Some(link_id), + max_plaintext_size() + ) + .is_err()); + } +} + +#[cfg(test)] +mod fragment_header { + use super::*; + + #[cfg(test)] + mod unlinked_fragmented_payload { + use super::*; + + #[test] + fn can_be_converted_to_and_from_bytes_for_exact_number_of_bytes_provided() { + let fragmented_header = FragmentHeader::try_new(12345, 10, 5, None, None).unwrap(); + + let header_bytes = fragmented_header.to_bytes(); + let (recovered_header, bytes_used) = + FragmentHeader::try_from_bytes(&header_bytes).unwrap(); + assert_eq!(fragmented_header, recovered_header); + assert_eq!(UNLINKED_FRAGMENTED_HEADER_LEN, bytes_used); + } + + #[test] + fn can_be_converted_to_and_from_bytes_for_more_than_required_number_of_bytes() { + let fragmented_header = FragmentHeader::try_new(12345, 10, 5, None, None).unwrap(); + + let mut header_bytes = fragmented_header.to_bytes(); + header_bytes.append(vec![1, 2, 3, 4, 5].as_mut()); + + let (recovered_header, bytes_used) = + FragmentHeader::try_from_bytes(&header_bytes).unwrap(); + assert_eq!(fragmented_header, recovered_header); + assert_eq!(UNLINKED_FRAGMENTED_HEADER_LEN, bytes_used); + } + + #[test] + fn retrieval_from_bytes_fail_for_insufficient_number_of_bytes_provided() { + let fragmented_header = FragmentHeader::try_new(12345, 10, 5, None, None).unwrap(); + + let header_bytes = fragmented_header.to_bytes(); + let header_bytes = &header_bytes[..header_bytes.len() - 1]; + assert!(FragmentHeader::try_from_bytes(&header_bytes).is_err()) + } + + #[test] + fn retrieval_from_bytes_fail_for_invalid_fragmentation_flag() { + let fragmented_header = FragmentHeader::try_new(10, 10, 5, None, None).unwrap(); + + let mut header_bytes_low = fragmented_header.to_bytes(); + + // clear the fragmentation flag + header_bytes_low[0] &= !(1 << 7); + + let mut header_bytes_high = header_bytes_low.clone(); + // make sure first byte of id is non-empty (apart from the fragmentation flag) + // note for anyone reading this test in the future: choice of '3' here is arbitrary. + header_bytes_high[0] |= 1 << 3; + + // This will have cause an error as there will be a value in the first byte + assert!(FragmentHeader::try_from_bytes(&header_bytes_high).is_err()); + } + + #[test] + fn retrieval_from_bytes_fail_for_invalid_link_flag() { + let fragmented_header = FragmentHeader::try_new(12345, 10, 5, None, None).unwrap(); + + let mut header_bytes = fragmented_header.to_bytes(); + // set linked flag + header_bytes[6] |= 1 << 7; + assert!(FragmentHeader::try_from_bytes(&header_bytes).is_err()); + } + + #[test] + fn creation_of_header_fails_if_current_fragment_is_higher_than_total() { + assert!(FragmentHeader::try_new(12345, 10, 11, None, None).is_err()); + } + + #[test] + fn creation_of_header_fails_if_current_fragment_is_zero() { + assert!(FragmentHeader::try_new(12345, 10, 0, None, None).is_err()); + } + + #[test] + fn creation_of_header_fails_if_total_fragments_is_zero() { + assert!(FragmentHeader::try_new(12345, 0, 0, None, None).is_err()); + } + + #[test] + fn creation_of_header_fails_if_id_is_negative() { + assert!(FragmentHeader::try_new(-10, 10, 5, None, None).is_err()); + } + + #[test] + fn fragmented_header_cannot_be_created_with_zero_id() { + assert!(FragmentHeader::try_new(0, 10, 5, None, None).is_err()); + assert!(FragmentHeader::try_new(12345, 10, 5, Some(0), None).is_err()); + assert!(FragmentHeader::try_new( + 12345, + u8::max_value(), + u8::max_value(), + None, + Some(0) + ) + .is_err()); + } + + #[test] + fn retrieval_from_bytes_fail_if_current_fragment_is_higher_than_total() { + // manually create header to overwrite any constructor checks + let header = FragmentHeader { + id: 1234, + total_fragments: 10, + current_fragment: 11, + previous_fragments_set_id: None, + next_fragments_set_id: None, + }; + let header_bytes = header.to_bytes(); + assert!(FragmentHeader::try_from_bytes(&header_bytes).is_err()); + } + + #[test] + fn retrieval_from_bytes_fail_if_current_or_total_fragment_is_zero() { + // manually create header to overwrite any constructor checks + let header = FragmentHeader { + id: 1234, + total_fragments: 0, + current_fragment: 0, + previous_fragments_set_id: None, + next_fragments_set_id: None, + }; + let header_bytes = header.to_bytes(); + assert!(FragmentHeader::try_from_bytes(&header_bytes).is_err()); + } + } + + #[cfg(test)] + mod linked_fragmented_payload { + use super::*; + + #[test] + fn cannot_be_linked_to_itself() { + assert!(FragmentHeader::try_new(12345, 10, 1, Some(12345), None).is_err()); + assert!(FragmentHeader::try_new(12345, 10, 10, None, Some(12345)).is_err()); + } + + #[test] + fn can_only_be_pre_linked_for_first_fragment() { + assert!(FragmentHeader::try_new(12345, 10, 1, Some(1234), None).is_ok()); + assert!(FragmentHeader::try_new(12345, 10, 2, Some(1234), None).is_err()); + } + + #[test] + fn can_only_be_post_linked_for_last_fragment() { + assert!(FragmentHeader::try_new(12345, 10, 10, None, Some(1234)).is_ok()); + assert!(FragmentHeader::try_new( + 12345, + u8::max_value(), + u8::max_value(), + None, + Some(1234), + ) + .is_ok()); + assert!(FragmentHeader::try_new(12345, 10, 2, Some(1234), None).is_err()); + } + + #[test] + fn pre_linked_can_be_converted_to_and_from_bytes_for_exact_number_of_bytes_provided() { + let fragmented_header = + FragmentHeader::try_new(12345, 10, 1, Some(1234), None).unwrap(); + + let header_bytes = fragmented_header.to_bytes(); + let (recovered_header, bytes_used) = + FragmentHeader::try_from_bytes(&header_bytes).unwrap(); + assert_eq!(fragmented_header, recovered_header); + assert_eq!(LINKED_FRAGMENTED_HEADER_LEN, bytes_used); + } + + #[test] + fn pre_linked_can_be_converted_to_and_from_bytes_for_more_than_required_number_of_bytes() { + let fragmented_header = + FragmentHeader::try_new(12345, 10, 1, Some(1234), None).unwrap(); + + let mut header_bytes = fragmented_header.to_bytes(); + header_bytes.append(vec![1, 2, 3, 4, 5].as_mut()); + + let (recovered_header, bytes_used) = + FragmentHeader::try_from_bytes(&header_bytes).unwrap(); + assert_eq!(fragmented_header, recovered_header); + assert_eq!(LINKED_FRAGMENTED_HEADER_LEN, bytes_used); + } + + #[test] + fn pre_linked_is_successfully_recovered_if_its_both_first_and_final_fragment() { + let fragmented_header = FragmentHeader::try_new(12345, 1, 1, Some(1234), None).unwrap(); + + let header_bytes = fragmented_header.to_bytes(); + let (recovered_header, bytes_used) = + FragmentHeader::try_from_bytes(&header_bytes).unwrap(); + assert_eq!(fragmented_header, recovered_header); + assert_eq!(LINKED_FRAGMENTED_HEADER_LEN, bytes_used); + } + + #[test] + fn post_linked_can_be_converted_to_and_from_bytes_for_exact_number_of_bytes_provided() { + let fragmented_header = + FragmentHeader::try_new(12345, u8::max_value(), u8::max_value(), None, Some(1234)) + .unwrap(); + + let header_bytes = fragmented_header.to_bytes(); + let (recovered_header, bytes_used) = + FragmentHeader::try_from_bytes(&header_bytes).unwrap(); + assert_eq!(fragmented_header, recovered_header); + assert_eq!(LINKED_FRAGMENTED_HEADER_LEN, bytes_used); + } + + #[test] + fn post_linked_can_be_converted_to_and_from_bytes_for_more_than_required_number_of_bytes() { + let fragmented_header = + FragmentHeader::try_new(12345, u8::max_value(), u8::max_value(), None, Some(1234)) + .unwrap(); + + let mut header_bytes = fragmented_header.to_bytes(); + header_bytes.append(vec![1, 2, 3, 4, 5].as_mut()); + + let (recovered_header, bytes_used) = + FragmentHeader::try_from_bytes(&header_bytes).unwrap(); + assert_eq!(fragmented_header, recovered_header); + assert_eq!(LINKED_FRAGMENTED_HEADER_LEN, bytes_used); + } + } +} diff --git a/common/nymsphinx/chunking/src/lib.rs b/common/nymsphinx/chunking/src/lib.rs new file mode 100644 index 0000000000..df9e968b04 --- /dev/null +++ b/common/nymsphinx/chunking/src/lib.rs @@ -0,0 +1,240 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use crate::fragment::{Fragment, FragmentIdentifier}; +use crate::set::split_into_sets; +use nymsphinx_acknowledgements::identifier::AckAes128Key; +use nymsphinx_acknowledgements::surb_ack::SURBAck; +use nymsphinx_addressing::clients::Recipient; +use nymsphinx_addressing::nodes::{NymNodeRoutingAddress, MAX_NODE_ADDRESS_UNPADDED_LEN}; +use nymsphinx_params::packet_sizes::PacketSize; +use nymsphinx_types::builder::SphinxPacketBuilder; +use nymsphinx_types::{delays, Delay, Destination, SphinxPacket}; +use rand::{rngs::OsRng, CryptoRng, Rng}; +use std::convert::TryFrom; +use std::net::SocketAddr; +use std::time::Duration; +use topology::{NymTopology, NymTopologyError}; + +// Future consideration: currently in a lot of places, the payloads have randomised content +// which is not a perfect testing strategy as it might not detect some edge cases I never would +// have assumed could be possible. A better approach would be to research some Fuzz testing +// library like: https://github.com/rust-fuzz/afl.rs and use that instead for the inputs. + +// perhaps it might be useful down the line for interaction testing between client,mixes,etc? + +pub mod fragment; +pub mod reconstruction; +pub mod set; + +type DefaultRng = OsRng; +const DEFAULT_RNG: DefaultRng = OsRng; + +/// The idea behind the process of chunking is to incur as little data overhead as possible due +/// to very computationally costly sphinx encapsulation procedure. +/// +/// To achieve this, the underlying message is split into so-called "sets", which are further +/// subdivided into the base unit of "fragment" that is directly encapsulated by a Sphinx packet. +/// This allows to encapsulate messages of arbitrary length. +/// +/// Each message, regardless of its size, consists of at least a single `Set` that has at least +/// a single `Fragment`. +/// +/// Each `Fragment` can have variable, yet fully deterministic, length, +/// that depends on its position in the set as well as total number of sets. This is further +/// explained in `fragment.rs` file. +/// +/// Similarly, each `Set` can have a variable number of `Fragment`s inside. However, that +/// value is more restrictive: if it's the last set into which the message was split +/// (or implicitly the only one), it has no lower bound on the number of `Fragment`s. +/// (Apart from the restriction of containing at least a single one). If the set is located +/// somewhere in the middle, *it must be* full. Finally, regardless of its position, it must also be +/// true that it contains no more than `u8::max_value()`, i.e. 255 `Fragment`s. +/// Again, the reasoning for this is further explained in `set.rs` file. However, you might +/// also want to look at `fragment.rs` to understand the full context behind that design choice. +/// +/// Both of those concepts as well as their structures, i.e. `Set` and `Fragment` +/// are further explained in the respective files. + +#[derive(PartialEq, Debug)] +pub enum ChunkingError { + InvalidPayloadLengthError, + TooBigMessageToSplit, + MalformedHeaderError, + NoValidProvidersError, + NoValidRoutesAvailableError, + InvalidTopologyError, + TooShortFragmentData, + MalformedFragmentData, + UnexpectedFragmentCount, + MalformedFragmentIdentifier, +} + +// Note: `Rng` implies `RngCore` +#[derive(Debug, Clone)] +pub struct MessageChunker { + rng: R, + ack_recipient: Recipient, + packet_size: PacketSize, + reply_surbs: bool, + average_packet_delay_duration: Duration, + average_ack_delay_duration: Duration, +} + +impl MessageChunker { + pub fn new( + ack_recipient: Recipient, + average_packet_delay_duration: Duration, + average_ack_delay_duration: Duration, + ) -> Self { + Self::new_with_rng( + DEFAULT_RNG, + ack_recipient, + average_packet_delay_duration, + average_ack_delay_duration, + ) + } + + #[cfg(test)] + pub(crate) fn test_fixture() -> Self { + use nymsphinx_types::{DestinationAddressBytes, NodeAddressBytes}; + + let empty_address = [0u8; 32]; + let empty_recipient = Recipient::new( + DestinationAddressBytes::from_bytes(empty_address), + NodeAddressBytes::from_bytes(empty_address), + ); + Self::new(empty_recipient, Default::default(), Default::default()) + } +} + +impl MessageChunker { + pub fn new_with_rng( + rng: R, + ack_recipient: Recipient, + average_packet_delay_duration: Duration, + average_ack_delay_duration: Duration, + ) -> Self { + MessageChunker { + rng, + ack_recipient, + packet_size: Default::default(), + reply_surbs: false, + average_packet_delay_duration, + average_ack_delay_duration, + } + } + + pub fn available_plaintext_size(&self) -> usize { + // we need to put first hop's destination alongside the actual ack + // TODO: a possible optimization way down the line: currently we're always assuming that + // the addresses will have `MAX_NODE_ADDRESS_UNPADDED_LEN`, i.e. be ipv6. In most cases + // they're actually going to be ipv4 hence wasting few bytes every packet. + // To fully utilise all available space, I guess first we'd need to generate routes for ACKs + // and only then perform the chunking with `available_plaintext_size` being called per chunk. + // However this will probably introduce bunch of complexity + // for relatively not a lot of gain, so it shouldn't be done just yet. + let available_size = self.packet_size.plaintext_size() + - PacketSize::ACKPacket.size() + - MAX_NODE_ADDRESS_UNPADDED_LEN; + if self.reply_surbs { + // TODO + unimplemented!(); + } + available_size + } + + pub fn with_reply_surbs(mut self, reply_surbs: bool) -> Self { + self.reply_surbs = reply_surbs; + self + } + + pub fn with_packet_size(mut self, packet_size: PacketSize) -> Self { + self.packet_size = packet_size; + self + } + + /// Tries to convert this `Fragment` into a `SphinxPacket` that can be sent through the Nym mix-network, + /// such that it contains required SURB-ACK. + /// This method can fail if the provided network topology is invalid. + /// It returns total expected delay as well as the `SphinxPacket` to be sent through the network. + pub fn prepare_chunk_for_sending( + &mut self, + fragment: Fragment, + topology: &T, + ack_key: &AckAes128Key, + packet_recipient: &Recipient, + ) -> Result<(Delay, (SocketAddr, SphinxPacket)), NymTopologyError> { + let (ack_delay, surb_bytes) = self + .generate_surb_ack(&fragment.fragment_identifier(), topology, ack_key)? + .prepare_for_sending(); + + // SURB_FIRST_HOP || SURB_ACK || CHUNK_DATA + let packet_payload: Vec<_> = surb_bytes + .into_iter() + .chain(fragment.into_bytes().into_iter()) + .collect(); + + let route = topology.random_route_to_gateway(&packet_recipient.gateway())?; + let delays = + delays::generate_from_average_duration(route.len(), self.average_packet_delay_duration); + let destination = Destination::new(packet_recipient.destination(), Default::default()); + + // once merged, that's an easy rng injection point for sphinx packets : ) + let packet = SphinxPacketBuilder::new() + .with_payload_size(self.packet_size.payload_size()) + .build_packet(packet_payload, &route, &destination, &delays) + .unwrap(); + + let first_hop_address = + NymNodeRoutingAddress::try_from(route.first().unwrap().address.clone()).unwrap(); + + Ok(( + delays.iter().sum::() + ack_delay, + (first_hop_address.into(), packet), + )) + } + + fn generate_surb_ack( + &mut self, + fragment_id: &FragmentIdentifier, + topology: &T, + ack_key: &AckAes128Key, + ) -> Result + where + T: NymTopology, + { + SURBAck::construct( + &mut self.rng, + &self.ack_recipient, + ack_key, + &fragment_id.to_bytes(), + self.average_ack_delay_duration, + topology, + ) + } + + /// Takes the entire message and splits it into bytes chunks that will fit into sphinx packets + /// after attaching SURB-ACK. + /// After receiving they can be combined using `reconstruction::MessageReconstructor` + /// to obtain the original message back. + pub fn split_message(&mut self, message: &[u8]) -> Vec { + let available_plaintext_per_fragment = self.available_plaintext_size(); + + split_into_sets(&mut self.rng, message, available_plaintext_per_fragment) + .into_iter() + .flat_map(|fragment_set| fragment_set.into_iter()) + .collect() + } +} diff --git a/common/nymsphinx/src/chunking/reconstruction.rs b/common/nymsphinx/chunking/src/reconstruction.rs similarity index 55% rename from common/nymsphinx/src/chunking/reconstruction.rs rename to common/nymsphinx/chunking/src/reconstruction.rs index bcfac94389..e25ac3df03 100644 --- a/common/nymsphinx/src/chunking/reconstruction.rs +++ b/common/nymsphinx/chunking/src/reconstruction.rs @@ -12,7 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::chunking::fragment::Fragment; +use crate::fragment::Fragment; +use crate::ChunkingError; use log::*; use std::collections::HashMap; @@ -44,6 +45,10 @@ struct ReconstructionBuffer { fragments: Vec>, } +/// Type alias representing fully reconstructed message - its original data and list of all +/// set ids used for the reconstructions processed so that they could be used for replay prevention. +pub type ReconstructedMessage = (Vec, Vec); + impl ReconstructionBuffer { /// Initialises new instance of a `ReconstructionBuffer` with given size, i.e. /// number of expected `Fragment`s in the set. @@ -114,6 +119,8 @@ impl ReconstructionBuffer { let fragment_index = fragment.current_fragment() as usize - 1; if self.fragments[fragment_index].is_some() { // TODO: what to do in that case? give up on the message? overwrite it? panic? + // it *might* be due to lock ack-packet, but let's keep the `warn` level in case + // it could be somehow exploited warn!( "duplicate fragment received! - frag - {} (set id: {})", fragment.current_fragment(), @@ -253,31 +260,26 @@ impl MessageReconstructor { /// Given id of *any* one of the sets into which message was divided, /// reconstruct the entire original message. /// Note, before you call this method, you *must* ensure all sets were fully received - fn reconstruct_message(&mut self, set_id: i32) -> Vec { + fn reconstruct_message(&mut self, set_id: i32) -> ReconstructedMessage { debug_assert!(self.is_message_fully_received(set_id)); let starting_id = self.find_starting_set_id(set_id).unwrap(); let set_id_sequence: Vec<_> = std::iter::successors(Some(starting_id), |&id| self.next_linked_set_id(id)).collect(); - set_id_sequence + let message_content = set_id_sequence .iter() .map(|&id| self.extract_set_payload(id)) .flat_map(|payload| payload.into_iter()) - .collect() + .collect(); + + (message_content, set_id_sequence) } - /// Given raw `Fragment` data, tries to decode it and add into an appropriate `ReconstructionBuffer`. + /// Given recovered `Fragment`, tries to insert it into an appropriate `ReconstructionBuffer`. /// If a buffer does not exist, a new instance is created. /// If it was last remaining `Fragment` for the original message, the message is reconstructed - /// and returned. - pub fn new_fragment(&mut self, fragment_data: Vec) -> Option> { - let fragment_res = Fragment::try_from_bytes(&fragment_data); - if let Err(e) = fragment_res { - warn!("failed to recover fragment data: {:?}. The whole underlying message might be corrupted and unrecoverable!", e); - return None; - } - let fragment = fragment_res.unwrap(); - + /// and returned alongside all (if applicable) set ids used in the message. + pub fn insert_new_fragment(&mut self, fragment: Fragment) -> Option { let set_id = fragment.id(); let set_len = fragment.total_fragments(); @@ -293,14 +295,19 @@ impl MessageReconstructor { None } } + + /// Given raw `Fragment` data, tries to decode and return it. + pub fn recover_fragment(&self, fragment_data: Vec) -> Result { + Fragment::try_from_bytes(&fragment_data) + } } #[cfg(test)] mod reconstruction_buffer { use super::*; - use crate::chunking::fragment::UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN; - use crate::chunking::set::MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH; - use crate::chunking::split_and_prepare_payloads; + use crate::fragment::unlinked_fragment_payload_max_len; + use crate::set::max_one_way_linked_set_payload_length; + use crate::MessageChunker; #[test] fn creating_new_instance_correctly_initialised_fragments_buffer() { @@ -331,35 +338,72 @@ mod reconstruction_buffer { #[test] fn reconstructing_set_data_works_for_buffers_of_different_sizes() { + let mut message_chunker = MessageChunker::test_fixture(); + let mut buf = ReconstructionBuffer::new(1); - let message = [42u8; 42]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = vec![42u8; 42]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); + + // acks are ignored as they will be stripped by gateways before getting to the reconstruction + buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[0]).unwrap()); assert_eq!(message.to_vec(), buf.reconstruct_set_data()); let mut buf = ReconstructionBuffer::new(3); - let message = [42u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) * 3 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); + assert_eq!(raw_fragments.len(), 3); + buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[0]).unwrap()); buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[1]).unwrap()); buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[2]).unwrap()); assert_eq!(message.to_vec(), buf.reconstruct_set_data()); - let mut buf = ReconstructionBuffer::new(u8::max_value()); - let message = [42u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * u8::max_value() as usize]; - let raw_fragments = split_and_prepare_payloads(&message); - for raw_fragment in raw_fragments { - buf.insert_fragment(Fragment::try_from_bytes(&raw_fragment).unwrap()) - } - assert_eq!(message.to_vec(), buf.reconstruct_set_data()); + // let mut buf = ReconstructionBuffer::new(u8::max_value()); + // let message = vec![ + // 42u8; + // unlinked_fragmented_payload_max_len(message_chunker.available_plaintext_size()) + // * u8::max_value() as usize + // ]; + // let raw_fragments: Vec<_> = message_chunker + // .split_message(&message) + // .into_iter() + // .map(|x| x.into_bytes()) + // .collect(); + // for raw_fragment in raw_fragments { + // buf.insert_fragment(Fragment::try_from_bytes(&raw_fragment).unwrap()) + // } + // assert_eq!(message.to_vec(), buf.reconstruct_set_data()); } #[test] #[should_panic] fn reconstructing_set_data_is_not_allowed_for_incomplete_sets() { + let mut message_chunker = MessageChunker::test_fixture(); + let mut buf = ReconstructionBuffer::new(3); - let raw_fragments = - split_and_prepare_payloads(&[42u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]); + let raw_fragments: Vec<_> = message_chunker + .split_message(&vec![ + 42u8; + unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) * 3 + ]) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[0]).unwrap()); buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[1]).unwrap()); @@ -368,9 +412,19 @@ mod reconstruction_buffer { #[test] fn inserting_new_fragment_puts_it_in_correct_location_based_on_its_ordering() { + let mut message_chunker = MessageChunker::test_fixture(); + let mut buf = ReconstructionBuffer::new(3); - let raw_fragments = - split_and_prepare_payloads(&[42u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]); + let raw_fragments: Vec<_> = message_chunker + .split_message(&vec![ + 42u8; + unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) * 3 + ]) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[1]).unwrap()); assert!(buf.fragments[0].is_none()); @@ -380,9 +434,19 @@ mod reconstruction_buffer { #[test] fn inserting_final_fragment_correctly_sets_auxiliary_flags() { + let mut message_chunker = MessageChunker::test_fixture(); + let mut buf = ReconstructionBuffer::new(3); - let message = [42u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) * 3 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[0]).unwrap()); buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[2]).unwrap()); @@ -395,8 +459,17 @@ mod reconstruction_buffer { assert!(buf.next_fragments_set_id.is_none()); let mut buf = ReconstructionBuffer::new(255); - let message = [42u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 123]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = vec![ + 42u8; + max_one_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + 123 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); for i in 0..254 { buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[i]).unwrap()); } @@ -423,13 +496,31 @@ mod reconstruction_buffer { #[test] #[should_panic] fn does_not_allow_for_inserting_new_fragments_with_different_ids() { + let mut message_chunker = MessageChunker::test_fixture(); + let mut buf = ReconstructionBuffer::new(3); // they will have different IDs - let raw_fragments1 = - split_and_prepare_payloads(&[42u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]); - let raw_fragments2 = - split_and_prepare_payloads(&[42u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]); + let raw_fragments1: Vec<_> = message_chunker + .split_message(&vec![ + 42u8; + unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) * 3 + ]) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); + let raw_fragments2: Vec<_> = message_chunker + .split_message(&vec![ + 42u8; + unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) * 3 + ]) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments1[0]).unwrap()); buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments2[0]).unwrap()); @@ -439,32 +530,46 @@ mod reconstruction_buffer { #[cfg(test)] mod message_reconstructor { use super::*; - use crate::chunking::fragment::{ - UNFRAGMENTED_PAYLOAD_MAX_LEN, UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN, - }; - use crate::chunking::set::{ - MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH, TWO_WAY_LINKED_SET_PAYLOAD_LENGTH, - }; - use crate::chunking::split_and_prepare_payloads; + use crate::fragment::unlinked_fragment_payload_max_len; + use crate::set::{max_one_way_linked_set_payload_length, two_way_linked_set_payload_length}; + use crate::MessageChunker; use rand::{thread_rng, RngCore}; #[test] #[should_panic] fn checking_front_chain_is_not_allowed_for_incomplete_sets() { + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; - MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 123]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + max_one_way_linked_set_payload_length(message_chunker.available_plaintext_size()) + + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) + + 123 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); // first set is fully inserted for i in 0..255 { assert!(reconstructor - .new_fragment(raw_fragments[i].clone()) - .is_none()); + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[i].clone()) + .unwrap() + ) + .is_none()) } assert!(reconstructor - .new_fragment(raw_fragments[255].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[255].clone()) + .unwrap() + ) .is_none()); let second_set_id = Fragment::try_from_bytes(&raw_fragments[255]).unwrap().id(); @@ -474,21 +579,43 @@ mod message_reconstructor { #[test] #[should_panic] fn checking_back_chain_is_not_allowed_for_incomplete_sets() { + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 123]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = vec![ + 42u8; + max_one_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + 123 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); for i in 0..254 { assert!(reconstructor - .new_fragment(raw_fragments[i].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[i].clone()) + .unwrap() + ) .is_none()); } // finish next set for good measure assert!(reconstructor - .new_fragment(raw_fragments[255].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[255].clone()) + .unwrap() + ) .is_none()); assert!(reconstructor - .new_fragment(raw_fragments[256].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[256].clone()) + .unwrap() + ) .is_none()); let first_set_id = Fragment::try_from_bytes(&raw_fragments[0]).unwrap().id(); @@ -497,24 +624,46 @@ mod message_reconstructor { #[test] fn checking_front_chain_returns_false_for_complete_set_but_incomplete_message() { + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; - MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 123]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + max_one_way_linked_set_payload_length(message_chunker.available_plaintext_size()) + + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) + + 123 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); // note that first set is not fully inserted for i in 0..254 { assert!(reconstructor - .new_fragment(raw_fragments[i].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[i].clone()) + .unwrap() + ) .is_none()); } assert!(reconstructor - .new_fragment(raw_fragments[255].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[255].clone()) + .unwrap() + ) .is_none()); assert!(reconstructor - .new_fragment(raw_fragments[256].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[256].clone()) + .unwrap() + ) .is_none()); let second_set_id = Fragment::try_from_bytes(&raw_fragments[255]).unwrap().id(); @@ -523,20 +672,38 @@ mod message_reconstructor { #[test] fn checking_back_chain_returns_false_for_complete_set_but_incomplete_message() { + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; - MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 123]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + max_one_way_linked_set_payload_length(message_chunker.available_plaintext_size()) + + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) + + 123 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); for i in 0..255 { assert!(reconstructor - .new_fragment(raw_fragments[i].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[i].clone()) + .unwrap() + ) .is_none()); } // notice that entirety of second set is not inserted assert!(reconstructor - .new_fragment(raw_fragments[255].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[255].clone()) + .unwrap() + ) .is_none()); let first_set_id = Fragment::try_from_bytes(&raw_fragments[0]).unwrap().id(); @@ -547,20 +714,38 @@ mod message_reconstructor { #[test] fn checking_front_chain_returns_true_for_if_there_are_no_more_front_sets() { // case of 2 sets: [id1 -- id2], where id1 is completed and being checked + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; - MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 123]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + max_one_way_linked_set_payload_length(message_chunker.available_plaintext_size()) + + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) + + 123 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); for i in 0..255 { assert!(reconstructor - .new_fragment(raw_fragments[i].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[i].clone()) + .unwrap() + ) .is_none()); } // notice that entirety of second set is not inserted assert!(reconstructor - .new_fragment(raw_fragments[255].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[255].clone()) + .unwrap() + ) .is_none()); let first_set_id = Fragment::try_from_bytes(&raw_fragments[0]).unwrap().id(); @@ -571,20 +756,38 @@ mod message_reconstructor { #[test] fn checking_back_chain_returns_true_for_if_there_are_no_more_back_sets() { // case of 2 sets: [id1 -- id2], where id2 is completed and being checked + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 123]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = vec![ + 42u8; + max_one_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + 123 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); // note that first set is not fully inserted for i in 0..254 { assert!(reconstructor - .new_fragment(raw_fragments[i].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[i].clone()) + .unwrap() + ) .is_none()); } assert!(reconstructor - .new_fragment(raw_fragments[255].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[255].clone()) + .unwrap() + ) .is_none()); let second_set_id = Fragment::try_from_bytes(&raw_fragments[255]).unwrap().id(); @@ -594,23 +797,39 @@ mod message_reconstructor { #[test] fn checking_front_chain_returns_true_for_complete_front_chain() { // case of 3 sets: [id1 -- id2 -- id3], where id1 and id2 are completed and id2 is being checked + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; - MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - + TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - + 123]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + max_one_way_linked_set_payload_length(message_chunker.available_plaintext_size()) + + two_way_linked_set_payload_length(message_chunker.available_plaintext_size()) + + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) + + 123 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); for i in 0..(u8::max_value() as usize) * 2 { assert!(reconstructor - .new_fragment(raw_fragments[i].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[i].clone()) + .unwrap() + ) .is_none()); } // notice that entirety of third set is not inserted assert!(reconstructor - .new_fragment(raw_fragments[(u8::max_value() as usize) * 2].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[(u8::max_value() as usize) * 2].clone()) + .unwrap() + ) .is_none()); let second_set_id = Fragment::try_from_bytes(&raw_fragments[300]).unwrap().id(); @@ -621,20 +840,38 @@ mod message_reconstructor { #[test] fn checking_back_chain_returns_true_for_complete_back_chain() { // case of 3 sets: [id1 -- id2 -- id3], where id2 and id3 are completed and id2 is being checked + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); let message = - [42u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + TWO_WAY_LINKED_SET_PAYLOAD_LENGTH + 123]; - let raw_fragments = split_and_prepare_payloads(&message); + vec![ + 42u8; + max_one_way_linked_set_payload_length(message_chunker.available_plaintext_size()) + + two_way_linked_set_payload_length(message_chunker.available_plaintext_size()) + + 123 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); // note that first set is not fully inserted for i in 1..(u8::max_value() as usize) * 2 { assert!(reconstructor - .new_fragment(raw_fragments[i].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[i].clone()) + .unwrap() + ) .is_none()); } assert!(reconstructor - .new_fragment(raw_fragments[(u8::max_value() as usize) * 2].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[(u8::max_value() as usize) * 2].clone()) + .unwrap() + ) .is_none()); let second_set_id = Fragment::try_from_bytes(&raw_fragments[300]).unwrap().id(); @@ -677,38 +914,73 @@ mod message_reconstructor { #[test] fn finding_starting_set_id_returns_none_if_message_was_not_fully_received() { + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message1 = [42u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 123]; - let raw_fragments1 = split_and_prepare_payloads(&message1); + let message1 = vec![ + 42u8; + max_one_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + 123 + ]; + let raw_fragments1: Vec<_> = message_chunker + .split_message(&message1) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); // note that first set is not fully inserted for i in 0..254 { assert!(reconstructor - .new_fragment(raw_fragments1[i].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments1[i].clone()) + .unwrap() + ) .is_none()); } assert!(reconstructor - .new_fragment(raw_fragments1[255].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments1[255].clone()) + .unwrap() + ) .is_none()); let second_set_id = Fragment::try_from_bytes(&raw_fragments1[255]).unwrap().id(); assert!(reconstructor.find_starting_set_id(second_set_id).is_none()); - let message2 = [43u8; - MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 123]; - let raw_fragments2 = split_and_prepare_payloads(&message2); + let message2 = + vec![ + 43u8; + max_one_way_linked_set_payload_length(message_chunker.available_plaintext_size()) + + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) + + 123 + ]; + let raw_fragments2: Vec<_> = message_chunker + .split_message(&message2) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); for i in 0..255 { assert!(reconstructor - .new_fragment(raw_fragments2[i].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments2[i].clone()) + .unwrap() + ) .is_none()); } // notice that entirety of second set is not inserted assert!(reconstructor - .new_fragment(raw_fragments2[255].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments2[255].clone()) + .unwrap() + ) .is_none()); let second_set_id = Fragment::try_from_bytes(&raw_fragments2[255]).unwrap().id(); @@ -717,20 +989,38 @@ mod message_reconstructor { #[test] fn finding_starting_set_id_returns_expected_starting_id() { + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; - MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 123]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + max_one_way_linked_set_payload_length(message_chunker.available_plaintext_size()) + + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) + + 123 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); for i in 0..255 { assert!(reconstructor - .new_fragment(raw_fragments[i].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[i].clone()) + .unwrap() + ) .is_none()); } // notice that entirety of second set is not inserted assert!(reconstructor - .new_fragment(raw_fragments[255].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[255].clone()) + .unwrap() + ) .is_none()); let first_set_id = Fragment::try_from_bytes(&raw_fragments[0]).unwrap().id(); @@ -785,15 +1075,32 @@ mod message_reconstructor { #[test] #[should_panic] fn getting_previous_linked_set_id_is_not_allowed_for_incomplete_sets() { + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) * 3 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); assert!(reconstructor - .new_fragment(raw_fragments[0].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[0].clone()) + .unwrap() + ) .is_none()); assert!(reconstructor - .new_fragment(raw_fragments[1].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[1].clone()) + .unwrap() + ) .is_none()); let id = Fragment::try_from_bytes(&raw_fragments[0]).unwrap().id(); @@ -828,15 +1135,32 @@ mod message_reconstructor { #[test] #[should_panic] fn getting_next_linked_set_id_is_not_allowed_for_incomplete_sets() { + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) * 3 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); assert!(reconstructor - .new_fragment(raw_fragments[0].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[0].clone()) + .unwrap() + ) .is_none()); assert!(reconstructor - .new_fragment(raw_fragments[1].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[1].clone()) + .unwrap() + ) .is_none()); let id = Fragment::try_from_bytes(&raw_fragments[0]).unwrap().id(); @@ -871,15 +1195,32 @@ mod message_reconstructor { #[test] #[should_panic] fn extracting_set_payload_is_not_allowed_for_incomplete_sets() { + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); - let message = [42u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]; - let raw_fragments = split_and_prepare_payloads(&message); + let message = + vec![ + 42u8; + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) * 3 + ]; + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); assert!(reconstructor - .new_fragment(raw_fragments[0].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[0].clone()) + .unwrap() + ) .is_none()); assert!(reconstructor - .new_fragment(raw_fragments[1].clone()) + .insert_new_fragment( + reconstructor + .recover_fragment(raw_fragments[1].clone()) + .unwrap() + ) .is_none()); let id = Fragment::try_from_bytes(&raw_fragments[0]).unwrap().id(); @@ -888,14 +1229,23 @@ mod message_reconstructor { #[test] fn extracting_set_payload_is_returns_entire_set_data() { + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); let mut set_buf = ReconstructionBuffer::new(3); let mut rng = thread_rng(); - let mut message = [0u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]; + let mut message = + vec![ + 0u8; + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) * 3 + ]; rng.fill_bytes(&mut message); - let raw_fragments = split_and_prepare_payloads(&message); + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); set_buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[0]).unwrap()); set_buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[1]).unwrap()); set_buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[2]).unwrap()); @@ -914,14 +1264,23 @@ mod message_reconstructor { #[test] fn reconstructing_message_for_single_set_is_equivalent_to_extracting_set_payload() { // we're inserting this via the buffer approach as not to trigger immediate re-assembly + let mut message_chunker = MessageChunker::test_fixture(); let mut reconstructor = MessageReconstructor::new(); let mut set_buf = ReconstructionBuffer::new(3); let mut rng = thread_rng(); - let mut message = [0u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN * 3]; + let mut message = + vec![ + 0u8; + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) * 3 + ]; rng.fill_bytes(&mut message); - let raw_fragments = split_and_prepare_payloads(&message); + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); set_buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[0]).unwrap()); set_buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[1]).unwrap()); set_buf.insert_fragment(Fragment::try_from_bytes(&raw_fragments[2]).unwrap()); @@ -930,25 +1289,39 @@ mod message_reconstructor { reconstructor.reconstructed_sets.insert(set_id, set_buf); let mut reconstructor_clone = reconstructor.clone(); + let reconstructed_message = reconstructor_clone.reconstruct_message(set_id); assert_eq!( reconstructor.extract_set_payload(set_id), - reconstructor_clone.reconstruct_message(set_id) + reconstructed_message.0 ); + assert_eq!(reconstructed_message.1.len(), 1); + assert_eq!(reconstructed_message.1[0], set_id); } #[test] fn reconstructing_message_for_two_sets_is_equivalent_to_combining_results_of_extracting_set_payload( ) { + let mut message_chunker = MessageChunker::test_fixture(); + // we're inserting this via the buffer approach as not to trigger immediate re-assembly let mut reconstructor = MessageReconstructor::new(); let mut set_buf1 = ReconstructionBuffer::new(u8::max_value()); let mut set_buf2 = ReconstructionBuffer::new(1); let mut rng = thread_rng(); - let mut message = [42u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 123]; + let mut message = vec![ + 42u8; + max_one_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + 123 + ]; rng.fill_bytes(&mut message); - let raw_fragments = split_and_prepare_payloads(&message); + let raw_fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); for i in 0..255 { set_buf1.insert_fragment(Fragment::try_from_bytes(&raw_fragments[i]).unwrap()); } @@ -968,34 +1341,50 @@ mod message_reconstructor { let manually_combined_message = [extracted_set1, extracted_set2].concat(); + let reconstructed_message1 = reconstructor_clone.reconstruct_message(set_id1); + let reconstructed_message2 = reconstructor_clone2.reconstruct_message(set_id2); + + assert_eq!(reconstructed_message1.1.len(), 2); + assert_eq!(reconstructed_message1.1, vec![set_id1, set_id2]); + + assert_eq!(reconstructed_message2.1.len(), 2); + assert_eq!(reconstructed_message2.1, vec![set_id1, set_id2]); + // make sure we can use any id that is part of the message - assert_eq!( - reconstructor_clone.reconstruct_message(set_id1), - manually_combined_message - ); - assert_eq!( - reconstructor_clone2.reconstruct_message(set_id2), - manually_combined_message - ); + assert_eq!(reconstructed_message1.0, manually_combined_message); + assert_eq!(reconstructed_message2.0, manually_combined_message); } #[test] fn adding_invalid_fragment_does_not_change_reconstructor_state() { - let mut empty_reconstructor = MessageReconstructor::new(); + let mut message_chunker = MessageChunker::test_fixture(); + let empty_reconstructor = MessageReconstructor::new(); assert!(empty_reconstructor - .new_fragment([24u8; 43].to_vec()) - .is_none()); + .recover_fragment([24u8; 43].to_vec()) + .is_err()); assert_eq!(empty_reconstructor, MessageReconstructor::new()); let mut reconstructor_with_data = MessageReconstructor::new(); - let dummy_message = [24u8; UNFRAGMENTED_PAYLOAD_MAX_LEN + 30]; - let mut fragments = split_and_prepare_payloads(&dummy_message); - reconstructor_with_data.new_fragment(fragments.pop().unwrap()); + let dummy_message = + vec![ + 24u8; + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) + 30 + ]; + let mut fragments: Vec<_> = message_chunker + .split_message(&dummy_message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); + reconstructor_with_data.insert_new_fragment( + reconstructor_with_data + .recover_fragment(fragments.pop().unwrap()) + .unwrap(), + ); let reconstructor_clone = reconstructor_with_data.clone(); assert!(empty_reconstructor - .new_fragment([24u8; 43].to_vec()) - .is_none()); + .recover_fragment([24u8; 43].to_vec()) + .is_err()); assert_eq!(reconstructor_with_data, reconstructor_clone); } } @@ -1003,133 +1392,224 @@ mod message_reconstructor { #[cfg(test)] mod message_reconstruction { use super::*; - use crate::chunking::split_and_prepare_payloads; + use crate::MessageChunker; use rand::seq::SliceRandom; use rand::{thread_rng, RngCore}; #[cfg(test)] mod single_set_split { use super::*; - use crate::chunking::fragment::{ - UNFRAGMENTED_PAYLOAD_MAX_LEN, UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN, - }; - use crate::chunking::set::MAX_UNLINKED_SET_PAYLOAD_LENGTH; + use crate::fragment::unlinked_fragment_payload_max_len; + use crate::set::max_unlinked_set_payload_length; #[test] fn it_reconstructs_unfragmented_message() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); - let mut message = vec![0u8; UNFRAGMENTED_PAYLOAD_MAX_LEN - 20]; + let mut message = vec![ + 0u8; + unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) - 20 + ]; rng.fill_bytes(&mut message); - let fragment = split_and_prepare_payloads(&message); + let fragment: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); assert_eq!(fragment.len(), 1); let mut message_reconstructor = MessageReconstructor::new(); - assert_eq!( - message_reconstructor - .new_fragment(fragment[0].clone()) - .unwrap(), - message - ) + let reconstructed_message = message_reconstructor + .insert_new_fragment( + message_reconstructor + .recover_fragment(fragment[0].clone()) + .unwrap(), + ) + .unwrap(); + + assert_eq!(reconstructed_message.0, message); + assert_eq!(reconstructed_message.1.len(), 1); } #[test] fn it_reconstructs_unfragmented_message_of_max_length() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); - let mut message = vec![0u8; UNFRAGMENTED_PAYLOAD_MAX_LEN]; + let mut message = + vec![ + 0u8; + unlinked_fragment_payload_max_len(message_chunker.available_plaintext_size()) + ]; rng.fill_bytes(&mut message); - let fragment = split_and_prepare_payloads(&message); + let fragment: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); assert_eq!(fragment.len(), 1); let mut message_reconstructor = MessageReconstructor::new(); - assert_eq!( - message_reconstructor - .new_fragment(fragment[0].clone()) - .unwrap(), - message - ) + let reconstructed_message = message_reconstructor + .insert_new_fragment( + message_reconstructor + .recover_fragment(fragment[0].clone()) + .unwrap(), + ) + .unwrap(); + + assert_eq!(reconstructed_message.0, message); + assert_eq!(reconstructed_message.1.len(), 1); } #[test] fn it_reconstructs_fragmented_message_in_order_of_2_max_lenghts() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); - let mut message = vec![0u8; 2 * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; + let mut message = vec![ + 0u8; + 2 * unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) + ]; rng.fill_bytes(&mut message); - let fragments = split_and_prepare_payloads(&message); + let fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); assert_eq!(fragments.len(), 2); let mut message_reconstructor = MessageReconstructor::new(); assert!(message_reconstructor - .new_fragment(fragments[0].clone()) + .insert_new_fragment( + message_reconstructor + .recover_fragment(fragments[0].clone()) + .unwrap() + ) .is_none()); - assert_eq!( - message_reconstructor - .new_fragment(fragments[1].clone()) - .unwrap(), - message - ) + + let reconstructed_message = message_reconstructor + .insert_new_fragment( + message_reconstructor + .recover_fragment(fragments[1].clone()) + .unwrap(), + ) + .unwrap(); + + assert_eq!(reconstructed_message.0, message); + assert_eq!(reconstructed_message.1.len(), 1); } #[test] fn it_reconstructs_fragmented_message_in_order_of_with_non_max_tail() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); - let mut message = vec![0u8; 2 * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 42]; + let mut message = vec![ + 0u8; + 2 * unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) - 42 + ]; rng.fill_bytes(&mut message); - let fragments = split_and_prepare_payloads(&message); + let fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); assert_eq!(fragments.len(), 2); let mut message_reconstructor = MessageReconstructor::new(); assert!(message_reconstructor - .new_fragment(fragments[0].clone()) + .insert_new_fragment( + message_reconstructor + .recover_fragment(fragments[0].clone()) + .unwrap() + ) .is_none()); - assert_eq!( - message_reconstructor - .new_fragment(fragments[1].clone()) - .unwrap(), - message - ) + + let reconstructed_message = message_reconstructor + .insert_new_fragment( + message_reconstructor + .recover_fragment(fragments[1].clone()) + .unwrap(), + ) + .unwrap(); + + assert_eq!(reconstructed_message.0, message); + assert_eq!(reconstructed_message.1.len(), 1); } #[test] fn it_reconstructs_fragmented_message_in_order_of_30_fragments() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); - let mut message = vec![0u8; 30 * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; + let mut message = vec![ + 0u8; + 30 * unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) + ]; rng.fill_bytes(&mut message); - let fragments = split_and_prepare_payloads(&message); + let fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); assert_eq!(fragments.len(), 30); let mut message_reconstructor = MessageReconstructor::new(); for i in 0..29 { assert!(message_reconstructor - .new_fragment(fragments[i].clone()) + .insert_new_fragment( + message_reconstructor + .recover_fragment(fragments[i].clone()) + .unwrap() + ) .is_none()); } - assert_eq!( - message_reconstructor - .new_fragment(fragments[29].clone()) - .unwrap(), - message - ) + let reconstructed_message = message_reconstructor + .insert_new_fragment( + message_reconstructor + .recover_fragment(fragments[29].clone()) + .unwrap(), + ) + .unwrap(); + + assert_eq!(reconstructed_message.0, message); + assert_eq!(reconstructed_message.1.len(), 1); } #[test] fn it_reconstructs_fragmented_message_not_in_order_of_30_fragments() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); - let mut message = vec![0u8; 30 * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; + let mut message = vec![ + 0u8; + 30 * unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) + ]; rng.fill_bytes(&mut message); - let mut fragments = split_and_prepare_payloads(&message); + let mut fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); assert_eq!(fragments.len(), 30); // shuffle the fragments @@ -1138,33 +1618,52 @@ mod message_reconstruction { let mut message_reconstructor = MessageReconstructor::new(); for i in 0..29 { assert!(message_reconstructor - .new_fragment(fragments[i].clone()) + .insert_new_fragment( + message_reconstructor + .recover_fragment(fragments[i].clone()) + .unwrap() + ) .is_none()); } - assert_eq!( - message_reconstructor - .new_fragment(fragments[29].clone()) - .unwrap(), - message - ) + let reconstructed_message = message_reconstructor + .insert_new_fragment( + message_reconstructor + .recover_fragment(fragments[29].clone()) + .unwrap(), + ) + .unwrap(); + + assert_eq!(reconstructed_message.0, message); + assert_eq!(reconstructed_message.1.len(), 1); } #[test] fn it_reconstructs_two_different_fragmented_messages_not_in_order_of_30_fragments_each() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); - let mut message1 = vec![0u8; 30 * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; + let mut message1 = vec![ + 0u8; + 30 * unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) + ]; rng.fill_bytes(&mut message1); - let mut message2 = vec![0u8; 30 * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; + let mut message2 = vec![ + 0u8; + 30 * unlinked_fragment_payload_max_len( + message_chunker.available_plaintext_size() + ) + ]; rng.fill_bytes(&mut message2); // introduce dummy way to identify the messages message1[0] = 1; message2[0] = 2; - let mut fragments1 = split_and_prepare_payloads(&message1); + let mut fragments1 = message_chunker.split_message(&message1); assert_eq!(fragments1.len(), 30); - let mut fragments2 = split_and_prepare_payloads(&message2); + let mut fragments2 = message_chunker.split_message(&message2); assert_eq!(fragments2.len(), 30); // combine and shuffle fragments @@ -1175,10 +1674,15 @@ mod message_reconstruction { let mut message_reconstructor = MessageReconstructor::new(); for fragment in fragments { - if let Some(msg) = message_reconstructor.new_fragment(fragment) { - match msg[0] { - 1 => assert_eq!(msg, message1), - 2 => assert_eq!(msg, message2), + if let Some(reconstructed_msg) = message_reconstructor.insert_new_fragment( + message_reconstructor + .recover_fragment(fragment.into_bytes()) + .unwrap(), + ) { + assert_eq!(reconstructed_msg.1.len(), 1); + match reconstructed_msg.0[0] { + 1 => assert_eq!(reconstructed_msg.0, message1), + 2 => assert_eq!(reconstructed_msg.0, message2), _ => panic!("Unknown message!"), } } @@ -1187,19 +1691,28 @@ mod message_reconstruction { #[test] fn it_reconstructs_two_different_messages_not_in_order_of_maximum_single_set_size_each() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); - let mut message1 = vec![0u8; MAX_UNLINKED_SET_PAYLOAD_LENGTH]; + let mut message1 = + vec![ + 0u8; + max_unlinked_set_payload_length(message_chunker.available_plaintext_size()) + ]; rng.fill_bytes(&mut message1); - let mut message2 = vec![0u8; MAX_UNLINKED_SET_PAYLOAD_LENGTH]; + let mut message2 = + vec![ + 0u8; + max_unlinked_set_payload_length(message_chunker.available_plaintext_size()) + ]; rng.fill_bytes(&mut message2); // introduce dummy way to identify the messages message1[0] = 1; message2[0] = 2; - let mut fragments1 = split_and_prepare_payloads(&message1); + let mut fragments1 = message_chunker.split_message(&message1); assert_eq!(fragments1.len(), u8::max_value() as usize); - let mut fragments2 = split_and_prepare_payloads(&message2); + let mut fragments2 = message_chunker.split_message(&message2); assert_eq!(fragments2.len(), u8::max_value() as usize); // combine and shuffle fragments @@ -1210,10 +1723,15 @@ mod message_reconstruction { let mut message_reconstructor = MessageReconstructor::new(); for fragment in fragments.into_iter() { - if let Some(msg) = message_reconstructor.new_fragment(fragment) { - match msg[0] { - 1 => assert_eq!(msg, message1), - 2 => assert_eq!(msg, message2), + if let Some(reconstructed_msg) = message_reconstructor.insert_new_fragment( + message_reconstructor + .recover_fragment(fragment.into_bytes()) + .unwrap(), + ) { + assert_eq!(reconstructed_msg.1.len(), 1); + match reconstructed_msg.0[0] { + 1 => assert_eq!(reconstructed_msg.0, message1), + 2 => assert_eq!(reconstructed_msg.0, message2), _ => panic!("Unknown message!"), } } @@ -1224,18 +1742,28 @@ mod message_reconstruction { #[cfg(test)] mod multiple_sets_split { use super::*; - use crate::chunking::set::{ - MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH, TWO_WAY_LINKED_SET_PAYLOAD_LENGTH, + use crate::set::{ + max_one_way_linked_set_payload_length, two_way_linked_set_payload_length, }; #[test] fn it_reconstructs_fragmented_message_not_in_order_split_into_two_sets() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); - let mut message = vec![0u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 12345]; + let mut message = vec![ + 0u8; + max_one_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + 12345 + ]; rng.fill_bytes(&mut message); - let mut fragments = split_and_prepare_payloads(&message); + let mut fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); // shuffle the fragments fragments.shuffle(&mut rng); @@ -1247,8 +1775,11 @@ mod message_reconstruction { "Shouldn't have gone into another iteration if message was reconstructed!" ) } - if let Some(msg) = message_reconstructor.new_fragment(fragment) { - assert_eq!(msg, message); + if let Some(msg) = message_reconstructor + .insert_new_fragment(message_reconstructor.recover_fragment(fragment).unwrap()) + { + assert_eq!(msg.0, message); + assert_eq!(msg.1.len(), 2); finished_reconstruction = true; } } @@ -1256,17 +1787,24 @@ mod message_reconstruction { #[test] fn it_reconstructs_fragmented_message_not_in_order_split_into_four_sets() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); let mut message = vec![ 0u8; - 2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - + 12345 + 2 * two_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + max_one_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + 12345 ]; rng.fill_bytes(&mut message); - let mut fragments = split_and_prepare_payloads(&message); + let mut fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); // shuffle the fragments fragments.shuffle(&mut rng); @@ -1278,8 +1816,11 @@ mod message_reconstruction { "Shouldn't have gone into another iteration if message was reconstructed!" ) } - if let Some(msg) = message_reconstructor.new_fragment(fragment) { - assert_eq!(msg, message); + if let Some(msg) = message_reconstructor + .insert_new_fragment(message_reconstructor.recover_fragment(fragment).unwrap()) + { + assert_eq!(msg.0, message); + assert_eq!(msg.1.len(), 4); finished_reconstruction = true; } } @@ -1287,16 +1828,24 @@ mod message_reconstruction { #[test] fn it_reconstructs_fragmented_message_not_in_order_split_into_four_full_sets() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); let mut message = vec![ 0u8; - 2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 2 * two_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + 2 * max_one_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) ]; rng.fill_bytes(&mut message); - let mut fragments = split_and_prepare_payloads(&message); + let mut fragments: Vec<_> = message_chunker + .split_message(&message) + .into_iter() + .map(|x| x.into_bytes()) + .collect(); assert_eq!(fragments.len(), 4 * (u8::max_value() as usize)); // shuffle the fragments fragments.shuffle(&mut rng); @@ -1309,8 +1858,11 @@ mod message_reconstruction { "Shouldn't have gone into another iteration if message was reconstructed!" ) } - if let Some(msg) = message_reconstructor.new_fragment(fragment) { - assert_eq!(msg, message); + if let Some(msg) = message_reconstructor + .insert_new_fragment(message_reconstructor.recover_fragment(fragment).unwrap()) + { + assert_eq!(msg.0, message); + assert_eq!(msg.1.len(), 4); finished_reconstruction = true; } } @@ -1318,27 +1870,34 @@ mod message_reconstruction { #[test] fn it_reconstructs_two_fragmented_messages_not_in_order_split_into_four_sets() { + let mut message_chunker = MessageChunker::test_fixture(); let mut rng = thread_rng(); let mut message1 = vec![ 0u8; - 2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 2 * two_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + 2 * max_one_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) ]; rng.fill_bytes(&mut message1); let mut message2 = vec![ 0u8; - 2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 2 * two_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) + 2 * max_one_way_linked_set_payload_length( + message_chunker.available_plaintext_size() + ) ]; rng.fill_bytes(&mut message2); // introduce dummy way to identify the messages message1[0] = 1; message2[0] = 2; - let mut fragments1 = split_and_prepare_payloads(&message1); + let mut fragments1 = message_chunker.split_message(&message1); assert_eq!(fragments1.len(), 4 * (u8::max_value() as usize)); - let mut fragments2 = split_and_prepare_payloads(&message2); + let mut fragments2 = message_chunker.split_message(&message2); assert_eq!(fragments2.len(), 4 * (u8::max_value() as usize)); // combine and shuffle fragments @@ -1349,10 +1908,20 @@ mod message_reconstruction { let mut message_reconstructor = MessageReconstructor::new(); for fragment in fragments.into_iter() { - if let Some(msg) = message_reconstructor.new_fragment(fragment) { - match msg[0] { - 1 => assert_eq!(msg, message1), - 2 => assert_eq!(msg, message2), + if let Some(msg) = message_reconstructor.insert_new_fragment( + message_reconstructor + .recover_fragment(fragment.into_bytes()) + .unwrap(), + ) { + match msg.0[0] { + 1 => { + assert_eq!(msg.0, message1); + assert_eq!(msg.1.len(), 4); + } + 2 => { + assert_eq!(msg.0, message2); + assert_eq!(msg.1.len(), 4); + } _ => panic!("Unknown message!"), } } diff --git a/common/nymsphinx/src/chunking/set.rs b/common/nymsphinx/chunking/src/set.rs similarity index 55% rename from common/nymsphinx/src/chunking/set.rs rename to common/nymsphinx/chunking/src/set.rs index 68f5d20d36..9cbbe82e33 100644 --- a/common/nymsphinx/src/chunking/set.rs +++ b/common/nymsphinx/chunking/src/set.rs @@ -12,18 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::chunking::fragment::{ - Fragment, LINKED_FRAGMENTED_HEADER_LEN, LINKED_FRAGMENTED_PAYLOAD_MAX_LEN, - UNFRAGMENTED_PAYLOAD_MAX_LEN, UNLINKED_FRAGMENTED_HEADER_LEN, - UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN, +use crate::fragment::{ + linked_fragment_payload_max_len, unlinked_fragment_payload_max_len, Fragment, + LINKED_FRAGMENTED_HEADER_LEN, UNLINKED_FRAGMENTED_HEADER_LEN, }; use rand::Rng; /// In the simplest case of message being divided into a single set, the set has the upper bound /// on its payload length of the maximum number of `Fragment`s multiplied by their maximum, /// fragmented, length. -pub const MAX_UNLINKED_SET_PAYLOAD_LENGTH: usize = - u8::max_value() as usize * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN; +pub const fn max_unlinked_set_payload_length(max_plaintext_size: usize) -> usize { + u8::max_value() as usize * unlinked_fragment_payload_max_len(max_plaintext_size) +} /// If the set is being linked to another one, by either being the very first set, or the very last, /// one of its `Fragment`s must be changed from "unlinked" into "linked" to compensate for a tiny @@ -31,16 +31,20 @@ pub const MAX_UNLINKED_SET_PAYLOAD_LENGTH: usize = /// Note that the "MAX" prefix only applies to if the set is the last one as it does not have /// a lower bound on its length. If the set is one way linked and a first one, it *must have* /// this exact payload length instead. -pub const MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH: usize = MAX_UNLINKED_SET_PAYLOAD_LENGTH - - (LINKED_FRAGMENTED_HEADER_LEN - UNLINKED_FRAGMENTED_HEADER_LEN); +pub const fn max_one_way_linked_set_payload_length(max_plaintext_size: usize) -> usize { + max_unlinked_set_payload_length(max_plaintext_size) + - (LINKED_FRAGMENTED_HEADER_LEN - UNLINKED_FRAGMENTED_HEADER_LEN) +} /// If the set is being linked two others sets by being stuck in the middle of divided message, /// two of its `Fragment`s (first and final one) must be changed from /// "unlinked" into "linked" to compensate for data overhead. /// Note that this constant no longer has a "MAX" prefix, this is because each set being stuck /// between different sets, *must* have this exact payload length. -pub const TWO_WAY_LINKED_SET_PAYLOAD_LENGTH: usize = MAX_UNLINKED_SET_PAYLOAD_LENGTH - - 2 * (LINKED_FRAGMENTED_HEADER_LEN - UNLINKED_FRAGMENTED_HEADER_LEN); +pub const fn two_way_linked_set_payload_length(max_plaintext_size: usize) -> usize { + max_unlinked_set_payload_length(max_plaintext_size) + - 2 * (LINKED_FRAGMENTED_HEADER_LEN - UNLINKED_FRAGMENTED_HEADER_LEN) +} /// `FragmentSet` is an ordered collection of 1 to 255 `Fragment`s, each with the same ID /// that can be used to produce original message, assuming no linking took place. @@ -69,7 +73,7 @@ pub(crate) type FragmentSet = Vec; /// This approach saves whole byte per `Fragment`, which while may seem insignificant and /// introduces extra complexity, quickly adds up when faced with sphinx packet encapsulation for longer /// messages. -/// Finally, the reason 0 id is not allowed is to explicitly distinguish it from unfragmented +/// Finally, the reason 0 id is not allowed is to explicitly distinguish it from `COVER_FRAG_ID` /// `Fragment`s thus allowing for some additional optimizations by letting it skip /// certain procedures when reconstructing. fn generate_set_id(rng: &mut R) -> i32 { @@ -83,18 +87,16 @@ fn generate_set_id(rng: &mut R) -> i32 { } } -/// The simplest case of splitting underlying message - when it fits into a single -/// `Fragment` thus requiring no linking or even a set id. -/// For obvious reasons the most efficient approach. -fn prepare_unfragmented_set(message: &[u8]) -> FragmentSet { - vec![Fragment::try_new_unfragmented(&message).unwrap()] -} - /// Splits underlying message into multiple `Fragment`s while all of them fit in a single /// `Set` (number of `Fragment`s <= 255) -fn prepare_unlinked_fragmented_set(message: &[u8], id: i32) -> FragmentSet { - let pre_casted_frags = - (message.len() as f64 / UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN as f64).ceil() as usize; +fn prepare_unlinked_fragmented_set( + message: &[u8], + id: i32, + max_plaintext_size: usize, +) -> FragmentSet { + let pre_casted_frags = (message.len() as f64 + / unlinked_fragment_payload_max_len(max_plaintext_size) as f64) + .ceil() as usize; debug_assert!(pre_casted_frags <= u8::max_value() as usize); let num_fragments = pre_casted_frags as u8; @@ -103,14 +105,22 @@ fn prepare_unlinked_fragmented_set(message: &[u8], id: i32) -> FragmentSet { for i in 1..(pre_casted_frags + 1) { // we can't use u8 directly here as upper (NON-INCLUSIVE, so it would always fit) bound could be u8::max_value() + 1 - let lb = (i as usize - 1) * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN; + let lb = (i as usize - 1) * unlinked_fragment_payload_max_len(max_plaintext_size); let ub = usize::min( message.len(), - i as usize * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN, + i as usize * unlinked_fragment_payload_max_len(max_plaintext_size), ); fragments.push( - Fragment::try_new_fragmented(&message[lb..ub], id, num_fragments, i as u8, None, None) - .unwrap(), + Fragment::try_new( + &message[lb..ub], + id, + num_fragments, + i as u8, + None, + None, + max_plaintext_size, + ) + .unwrap(), ) } @@ -126,19 +136,21 @@ fn prepare_linked_fragment_set( id: i32, previous_link_id: Option, next_link_id: Option, + max_plaintext_size: usize, ) -> FragmentSet { // determine number of fragments in the set: let num_frags_usize = if next_link_id.is_some() { u8::max_value() as usize } else { // we know this set is linked, if it's not post-linked then it MUST BE pre-linked - let tail_len = if message.len() >= LINKED_FRAGMENTED_PAYLOAD_MAX_LEN { - message.len() - LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + let tail_len = if message.len() >= linked_fragment_payload_max_len(max_plaintext_size) { + message.len() - linked_fragment_payload_max_len(max_plaintext_size) } else { 0 }; - let pre_casted_frags = - 1 + (tail_len as f64 / UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN as f64).ceil() as usize; + let pre_casted_frags = 1 + + (tail_len as f64 / unlinked_fragment_payload_max_len(max_plaintext_size) as f64) + .ceil() as usize; if pre_casted_frags > u8::max_value() as usize { panic!("message would produce too many fragments!") }; @@ -148,16 +160,19 @@ fn prepare_linked_fragment_set( // determine bounds for the first fragment which depends on whether set is pre-linked let mut lb = 0; let mut ub = if previous_link_id.is_some() { - usize::min(message.len(), LINKED_FRAGMENTED_PAYLOAD_MAX_LEN) + usize::min( + message.len(), + linked_fragment_payload_max_len(max_plaintext_size), + ) } else { // the set might be linked, but fragment itself is not (i.e. the set is linked at the tail) - UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + unlinked_fragment_payload_max_len(max_plaintext_size) }; let mut fragments = Vec::with_capacity(num_frags_usize); for i in 1..(num_frags_usize + 1) { // we can't use u8 directly here as upper (NON-INCLUSIVE, so i would always fit) bound could be u8::max_value() + 1 - let fragment = Fragment::try_new_fragmented( + let fragment = Fragment::try_new( &message[lb..ub], id, num_frags_usize as u8, @@ -168,30 +183,37 @@ fn prepare_linked_fragment_set( } else { None }, + max_plaintext_size, ) .unwrap(); fragments.push(fragment); // update bounds for the next fragment lb = ub; - ub = usize::min(message.len(), ub + UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN); + ub = usize::min( + message.len(), + ub + unlinked_fragment_payload_max_len(max_plaintext_size), + ); } fragments } /// Based on total message length, determines the number of sets into which it is going to be split. -fn total_number_of_sets(message_len: usize) -> usize { - if message_len <= MAX_UNLINKED_SET_PAYLOAD_LENGTH { +fn total_number_of_sets(message_len: usize, max_plaintext_size: usize) -> usize { + if message_len <= max_unlinked_set_payload_length(max_plaintext_size) { 1 - } else if message_len > MAX_UNLINKED_SET_PAYLOAD_LENGTH - && message_len <= 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + } else if message_len > max_unlinked_set_payload_length(max_plaintext_size) + && message_len <= 2 * max_one_way_linked_set_payload_length(max_plaintext_size) { 2 } else { - let len_without_edges = message_len - 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH; + let len_without_edges = + message_len - 2 * max_one_way_linked_set_payload_length(max_plaintext_size); // every set in between edges must be two way linked - (len_without_edges as f64 / TWO_WAY_LINKED_SET_PAYLOAD_LENGTH as f64).ceil() as usize + 2 + (len_without_edges as f64 / two_way_linked_set_payload_length(max_plaintext_size) as f64) + .ceil() as usize + + 2 } } @@ -202,38 +224,50 @@ fn prepare_fragment_set( id: i32, previous_link_id: Option, next_link_id: Option, + max_plaintext_size: usize, ) -> FragmentSet { if previous_link_id.is_some() || next_link_id.is_some() { - prepare_linked_fragment_set(message, id, previous_link_id, next_link_id) - } else if message.len() > UNFRAGMENTED_PAYLOAD_MAX_LEN { + prepare_linked_fragment_set( + message, + id, + previous_link_id, + next_link_id, + max_plaintext_size, + ) + } else { // the bounds on whether the message fits in an unlinked set should have been done by the callee // when determining ids of other sets - prepare_unlinked_fragmented_set(message, id) - } else { - prepare_unfragmented_set(message) + prepare_unlinked_fragmented_set(message, id, max_plaintext_size) } } /// Entry point for splitting whole message into possibly multiple `Set`s. -pub(crate) fn split_into_sets(message: &[u8]) -> Vec { - use rand::thread_rng; - - let mut rng = thread_rng(); - let num_of_sets = total_number_of_sets(message.len()); +pub(crate) fn split_into_sets( + rng: &mut R, + message: &[u8], + max_plaintext_size: usize, +) -> Vec { + let num_of_sets = total_number_of_sets(message.len(), max_plaintext_size); if num_of_sets == 1 { - let set_id = generate_set_id(&mut rng); - vec![prepare_fragment_set(message, set_id, None, None)] + let set_id = generate_set_id(rng); + vec![prepare_fragment_set( + message, + set_id, + None, + None, + max_plaintext_size, + )] } else { let mut sets = Vec::with_capacity(num_of_sets); // pre-generate all ids for the sets let set_ids: Vec<_> = std::iter::repeat(()) - .map(|_| generate_set_id(&mut rng)) + .map(|_| generate_set_id(rng)) .take(num_of_sets) .collect(); // initial bounds for the set payloads let mut lb = 0; - let mut ub = MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH; + let mut ub = max_one_way_linked_set_payload_length(max_plaintext_size); for i in 0..num_of_sets { let fragment_set = prepare_fragment_set( @@ -245,6 +279,7 @@ pub(crate) fn split_into_sets(message: &[u8]) -> Vec { } else { Some(set_ids[i + 1]) }, + max_plaintext_size, ); sets.push(fragment_set); @@ -252,9 +287,15 @@ pub(crate) fn split_into_sets(message: &[u8]) -> Vec { lb = ub; ub = if i == num_of_sets - 2 { // we're going to go into the last iteration now, hence the last set will be one-way linked - usize::min(message.len(), ub + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH) + usize::min( + message.len(), + ub + max_one_way_linked_set_payload_length(max_plaintext_size), + ) } else { - usize::min(message.len(), ub + TWO_WAY_LINKED_SET_PAYLOAD_LENGTH) + usize::min( + message.len(), + ub + two_way_linked_set_payload_length(max_plaintext_size), + ) } } @@ -266,13 +307,21 @@ pub(crate) fn split_into_sets(message: &[u8]) -> Vec { #[cfg(test)] mod tests { use super::*; + use nymsphinx_params::packet_sizes::PacketSize; + + fn max_plaintext_size() -> usize { + PacketSize::default().plaintext_size() - PacketSize::ACKPacket.size() + } fn verify_unlinked_set_payload(mut set: FragmentSet, payload: &[u8]) { for i in (0..set.len()).rev() { assert_eq!( set.pop().unwrap().extract_payload(), - payload[i * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - ..usize::min(payload.len(), (i + 1) * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN)] + payload[i * unlinked_fragment_payload_max_len(max_plaintext_size()) + ..usize::min( + payload.len(), + (i + 1) * unlinked_fragment_payload_max_len(max_plaintext_size()) + )] .to_vec() ) } @@ -283,11 +332,13 @@ mod tests { let lb = if i == 0 { 0 } else { - (i - 1) * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + (i - 1) * unlinked_fragment_payload_max_len(max_plaintext_size()) + + linked_fragment_payload_max_len(max_plaintext_size()) }; let ub = usize::min( payload.len(), - i * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + LINKED_FRAGMENTED_PAYLOAD_MAX_LEN, + i * unlinked_fragment_payload_max_len(max_plaintext_size()) + + linked_fragment_payload_max_len(max_plaintext_size()), ); assert_eq!( @@ -299,11 +350,12 @@ mod tests { fn verify_post_linked_set_payload(mut set: FragmentSet, payload: &[u8]) { for i in (0..set.len()).rev() { - let lb = i * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN; + let lb = i * unlinked_fragment_payload_max_len(max_plaintext_size()); let ub = if i == (u8::max_value() as usize - 1) { - i * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + i * unlinked_fragment_payload_max_len(max_plaintext_size()) + + linked_fragment_payload_max_len(max_plaintext_size()) } else { - (i + 1) * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + (i + 1) * unlinked_fragment_payload_max_len(max_plaintext_size()) }; assert_eq!( @@ -318,13 +370,15 @@ mod tests { let lb = if i == 0 { 0 } else { - (i - 1) * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + (i - 1) * unlinked_fragment_payload_max_len(max_plaintext_size()) + + linked_fragment_payload_max_len(max_plaintext_size()) }; let ub = if i == (u8::max_value() as usize - 1) { - (i - 1) * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - + 2 * LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + (i - 1) * unlinked_fragment_payload_max_len(max_plaintext_size()) + + 2 * linked_fragment_payload_max_len(max_plaintext_size()) } else { - i * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + i * unlinked_fragment_payload_max_len(max_plaintext_size()) + + linked_fragment_payload_max_len(max_plaintext_size()) }; assert_eq!( @@ -334,7 +388,7 @@ mod tests { } } - fn verfiy_correct_link(left: &FragmentSet, right: &FragmentSet) { + fn verify_correct_link(left: &FragmentSet, right: &FragmentSet) { let first_id = left[0].id(); let post_id = left[254].next_fragments_set_id().unwrap(); @@ -345,31 +399,6 @@ mod tests { assert_eq!(second_id, post_id); } - #[cfg(test)] - mod preparing_unfragmented_set { - use super::*; - - #[test] - fn makes_set_with_single_unfragmented_element_for_valid_message_lengths() { - let mut set = prepare_unfragmented_set(&[1]); - assert_eq!(1, set.len()); - assert_eq!(set.pop().unwrap().extract_payload(), [1].to_vec()); - - let mut set = prepare_unfragmented_set(&[1u8; UNFRAGMENTED_PAYLOAD_MAX_LEN]); - assert_eq!(1, set.len()); - assert_eq!( - set.pop().unwrap().extract_payload(), - [1u8; UNFRAGMENTED_PAYLOAD_MAX_LEN].to_vec() - ); - } - - #[test] - #[should_panic] - fn panics_for_too_long_payload() { - prepare_unfragmented_set(&[1u8; UNFRAGMENTED_PAYLOAD_MAX_LEN + 1]); - } - } - #[cfg(test)] mod preparing_unlinked_set { // remember this this is only called for a sole set with <= 255 fragments @@ -381,30 +410,46 @@ mod tests { let id = 12345; let mut rng = thread_rng(); - let mut two_element_set_payload = [0u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 1]; + let mut two_element_set_payload = + vec![0u8; unlinked_fragment_payload_max_len(max_plaintext_size()) + 1]; rng.fill_bytes(&mut two_element_set_payload); - let two_element_set = prepare_unlinked_fragmented_set(&two_element_set_payload, id); + let two_element_set = + prepare_unlinked_fragmented_set(&two_element_set_payload, id, max_plaintext_size()); assert_eq!(2, two_element_set.len()); verify_unlinked_set_payload(two_element_set, &two_element_set_payload); let mut forty_two_element_set_payload = - [0u8; 41 * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 42]; + vec![0u8; 41 * unlinked_fragment_payload_max_len(max_plaintext_size()) + 42]; rng.fill_bytes(&mut forty_two_element_set_payload); - let forty_two_element_set = - prepare_unlinked_fragmented_set(&forty_two_element_set_payload, id); + let forty_two_element_set = prepare_unlinked_fragmented_set( + &forty_two_element_set_payload, + id, + max_plaintext_size(), + ); assert_eq!(42, forty_two_element_set.len()); verify_unlinked_set_payload(forty_two_element_set, &forty_two_element_set_payload); let mut max_fragments_set_payload = - [0u8; MAX_UNLINKED_SET_PAYLOAD_LENGTH - UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 1]; // last fragment should have a single byte of data + vec![ + 0u8; + max_unlinked_set_payload_length(max_plaintext_size()) + - unlinked_fragment_payload_max_len(max_plaintext_size()) + + 1 + ]; // last fragment should have a single byte of data rng.fill_bytes(&mut max_fragments_set_payload); - let max_fragment_set = prepare_unlinked_fragmented_set(&max_fragments_set_payload, id); + let max_fragment_set = prepare_unlinked_fragmented_set( + &max_fragments_set_payload, + id, + max_plaintext_size(), + ); assert_eq!(u8::max_value() as usize, max_fragment_set.len()); verify_unlinked_set_payload(max_fragment_set, &max_fragments_set_payload); - let mut full_set_payload = [0u8; MAX_UNLINKED_SET_PAYLOAD_LENGTH]; + let mut full_set_payload = + vec![0u8; max_unlinked_set_payload_length(max_plaintext_size())]; rng.fill_bytes(&mut full_set_payload); - let full_fragment_set = prepare_unlinked_fragmented_set(&full_set_payload, id); + let full_fragment_set = + prepare_unlinked_fragmented_set(&full_set_payload, id, max_plaintext_size()); assert_eq!(u8::max_value() as usize, full_fragment_set.len()); verify_unlinked_set_payload(full_fragment_set, &full_set_payload); } @@ -412,7 +457,11 @@ mod tests { #[test] #[should_panic] fn panics_for_too_long_payload() { - prepare_unlinked_fragmented_set(&[0u8; MAX_UNLINKED_SET_PAYLOAD_LENGTH + 1], 12345); + prepare_unlinked_fragmented_set( + &vec![0u8; max_unlinked_set_payload_length(max_plaintext_size()) + 1], + 12345, + max_plaintext_size(), + ); } } @@ -427,39 +476,66 @@ mod tests { let link_id = 1234; let mut rng = thread_rng(); - let mut two_element_set_payload = [0u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 1]; + let mut two_element_set_payload = + vec![0u8; linked_fragment_payload_max_len(max_plaintext_size()) + 1]; rng.fill_bytes(&mut two_element_set_payload); - let two_element_set = - prepare_linked_fragment_set(&two_element_set_payload, id, Some(link_id), None); + let two_element_set = prepare_linked_fragment_set( + &two_element_set_payload, + id, + Some(link_id), + None, + max_plaintext_size(), + ); assert_eq!(2, two_element_set.len()); verify_pre_linked_set_payload(two_element_set, &two_element_set_payload); - let mut forty_two_element_set_payload = [0u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN - + 40 * UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - + 42]; + let mut forty_two_element_set_payload = + vec![ + 0u8; + linked_fragment_payload_max_len(max_plaintext_size()) + + 40 * unlinked_fragment_payload_max_len(max_plaintext_size()) + + 42 + ]; rng.fill_bytes(&mut forty_two_element_set_payload); let forty_two_element_set = prepare_linked_fragment_set( &forty_two_element_set_payload, id, Some(link_id), None, + max_plaintext_size(), ); assert_eq!(42, forty_two_element_set.len()); verify_pre_linked_set_payload(forty_two_element_set, &forty_two_element_set_payload); let mut max_fragments_set_payload = - [0u8; MAX_UNLINKED_SET_PAYLOAD_LENGTH - LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 1]; // last fragment should have a single byte of data + vec![ + 0u8; + max_unlinked_set_payload_length(max_plaintext_size()) + - linked_fragment_payload_max_len(max_plaintext_size()) + + 1 + ]; // last fragment should have a single byte of data rng.fill_bytes(&mut max_fragments_set_payload); - let max_fragment_set = - prepare_linked_fragment_set(&max_fragments_set_payload, id, Some(link_id), None); + let max_fragment_set = prepare_linked_fragment_set( + &max_fragments_set_payload, + id, + Some(link_id), + None, + max_plaintext_size(), + ); assert_eq!(u8::max_value() as usize, max_fragment_set.len()); verify_pre_linked_set_payload(max_fragment_set, &max_fragments_set_payload); - let mut full_set_payload = [0u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH]; + let mut full_set_payload = + vec![0u8; max_one_way_linked_set_payload_length(max_plaintext_size())]; rng.fill_bytes(&mut full_set_payload); - let full_fragment_set = - prepare_linked_fragment_set(&full_set_payload, id, Some(link_id), None); + let full_fragment_set = prepare_linked_fragment_set( + &full_set_payload, + id, + Some(link_id), + None, + max_plaintext_size(), + ); assert_eq!(u8::max_value() as usize, full_fragment_set.len()); verify_pre_linked_set_payload(full_fragment_set, &full_set_payload); } @@ -468,10 +544,11 @@ mod tests { #[should_panic] fn panics_for_too_long_payload_for_pre_linked_set() { prepare_linked_fragment_set( - &[0u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 1], + &vec![0u8; max_one_way_linked_set_payload_length(max_plaintext_size()) + 1], 12345, Some(1234), None, + max_plaintext_size(), ); } @@ -482,10 +559,16 @@ mod tests { let mut rng = thread_rng(); // if set is post-linked, there is only a single valid case - full length payload - let mut full_set_payload = [0u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH]; + let mut full_set_payload = + vec![0u8; max_one_way_linked_set_payload_length(max_plaintext_size())]; rng.fill_bytes(&mut full_set_payload); - let full_fragment_set = - prepare_linked_fragment_set(&full_set_payload, id, None, Some(link_id)); + let full_fragment_set = prepare_linked_fragment_set( + &full_set_payload, + id, + None, + Some(link_id), + max_plaintext_size(), + ); assert_eq!(u8::max_value() as usize, full_fragment_set.len()); verify_post_linked_set_payload(full_fragment_set, &full_set_payload); } @@ -494,10 +577,11 @@ mod tests { #[should_panic] fn panics_for_too_long_payload_for_post_linked_set() { prepare_linked_fragment_set( - &[0u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 1], + &vec![0u8; max_one_way_linked_set_payload_length(max_plaintext_size()) + 1], 12345, None, Some(1234), + max_plaintext_size(), ); } @@ -505,10 +589,11 @@ mod tests { #[should_panic] fn panics_for_too_short_payload_for_post_linked_set() { prepare_linked_fragment_set( - &[0u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - 1], + &vec![0u8; max_one_way_linked_set_payload_length(max_plaintext_size()) - 1], 12345, None, Some(1234), + max_plaintext_size(), ); } @@ -521,13 +606,15 @@ mod tests { let post_link_id = 123456; let mut rng = thread_rng(); - let mut full_set_payload = [0u8; TWO_WAY_LINKED_SET_PAYLOAD_LENGTH]; + let mut full_set_payload = + vec![0u8; two_way_linked_set_payload_length(max_plaintext_size())]; rng.fill_bytes(&mut full_set_payload); let full_fragment_set = prepare_linked_fragment_set( &full_set_payload, id, Some(pre_link_id), Some(post_link_id), + max_plaintext_size(), ); assert_eq!(u8::max_value() as usize, full_fragment_set.len()); verify_two_way_linked_set_payload(full_fragment_set, &full_set_payload); @@ -537,10 +624,11 @@ mod tests { #[should_panic] fn panics_for_too_long_payload_for_two_way_linked_set() { prepare_linked_fragment_set( - &[0u8; TWO_WAY_LINKED_SET_PAYLOAD_LENGTH + 1], + &vec![0u8; two_way_linked_set_payload_length(max_plaintext_size()) + 1], 12345, Some(123456), Some(1234), + max_plaintext_size(), ); } @@ -548,10 +636,11 @@ mod tests { #[should_panic] fn panics_for_too_short_payload_for_two_way_linked_set() { prepare_linked_fragment_set( - &[0u8; TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - 1], + &vec![0u8; two_way_linked_set_payload_length(max_plaintext_size()) - 1], 12345, Some(123456), Some(1234), + max_plaintext_size(), ); } } @@ -561,37 +650,14 @@ mod tests { use super::*; use rand::{thread_rng, RngCore}; - #[test] - fn correctly_creates_set_with_single_unfragmented_element_when_expected() { - let mut rng = thread_rng(); - let tiny_message = [1, 2, 3, 4, 5]; - let mut max_unfragmented_message = [0u8; UNFRAGMENTED_PAYLOAD_MAX_LEN]; - rng.fill_bytes(&mut max_unfragmented_message); - - let mut sets = split_into_sets(&tiny_message); - assert_eq!(1, sets.len()); - assert_eq!(1, sets[0].len()); - assert_eq!( - tiny_message.to_vec(), - sets.pop().unwrap().pop().unwrap().extract_payload() - ); - - let mut sets = split_into_sets(&max_unfragmented_message); - assert_eq!(1, sets.len()); - assert_eq!(1, sets[0].len()); - assert_eq!( - max_unfragmented_message.to_vec(), - sets.pop().unwrap().pop().unwrap().extract_payload() - ); - } - #[test] fn correctly_creates_single_fragmented_set_when_expected() { let mut rng = thread_rng(); - let mut message = [0u8; MAX_UNLINKED_SET_PAYLOAD_LENGTH - 2345]; + let mut message = + vec![0u8; max_unlinked_set_payload_length(max_plaintext_size()) - 2345]; rng.fill_bytes(&mut message); - let mut sets = split_into_sets(&message); + let mut sets = split_into_sets(&mut rng, &message, max_plaintext_size()); assert_eq!(1, sets.len()); verify_unlinked_set_payload(sets.pop().unwrap(), &message); } @@ -602,127 +668,141 @@ mod tests { fn correctly_creates_two_singly_linked_sets_with_second_set_containing_data_fitting_in_unfragmented_payload( ) { let mut rng = thread_rng(); - let mut message = [0u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 123]; + let mut message = + vec![0u8; max_one_way_linked_set_payload_length(max_plaintext_size()) + 123]; rng.fill_bytes(&mut message); - let mut sets = split_into_sets(&message); + let mut sets = split_into_sets(&mut rng, &message, max_plaintext_size()); assert_eq!(2, sets.len()); - verfiy_correct_link(&sets[0], &sets[1]); + verify_correct_link(&sets[0], &sets[1]); verify_pre_linked_set_payload( sets.pop().unwrap(), - &message[MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH..], + &message[max_one_way_linked_set_payload_length(max_plaintext_size())..], ); verify_post_linked_set_payload( sets.pop().unwrap(), - &message[..MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH], + &message[..max_one_way_linked_set_payload_length(max_plaintext_size())], ); } #[test] fn correctly_creates_two_singly_linked_sets_when_expected() { let mut rng = thread_rng(); - let mut message = [0u8; MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 2345]; + let mut message = + vec![0u8; max_one_way_linked_set_payload_length(max_plaintext_size()) + 2345]; rng.fill_bytes(&mut message); - let mut sets = split_into_sets(&message); + let mut sets = split_into_sets(&mut rng, &message, max_plaintext_size()); assert_eq!(2, sets.len()); - verfiy_correct_link(&sets[0], &sets[1]); + verify_correct_link(&sets[0], &sets[1]); verify_pre_linked_set_payload( sets.pop().unwrap(), - &message[MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH..], + &message[max_one_way_linked_set_payload_length(max_plaintext_size())..], ); verify_post_linked_set_payload( sets.pop().unwrap(), - &message[..MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH], + &message[..max_one_way_linked_set_payload_length(max_plaintext_size())], ); - let mut message = [0u8; 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH]; + let mut message = + vec![0u8; 2 * max_one_way_linked_set_payload_length(max_plaintext_size())]; rng.fill_bytes(&mut message); - let mut sets = split_into_sets(&message); + let mut sets = split_into_sets(&mut rng, &message, max_plaintext_size()); assert_eq!(2, sets.len()); assert_eq!(sets[0].len(), u8::max_value() as usize); assert_eq!(sets[1].len(), u8::max_value() as usize); - verfiy_correct_link(&sets[0], &sets[1]); + verify_correct_link(&sets[0], &sets[1]); verify_pre_linked_set_payload( sets.pop().unwrap(), - &message[MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH..], + &message[max_one_way_linked_set_payload_length(max_plaintext_size())..], ); verify_post_linked_set_payload( sets.pop().unwrap(), - &message[..MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH], + &message[..max_one_way_linked_set_payload_length(max_plaintext_size())], ); } #[test] fn correctly_creates_four_correctly_formed_sets_when_expected() { let mut rng = thread_rng(); - let mut message = [0u8; 2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - + 2345]; + let mut message = vec![ + 0u8; + 2 * two_way_linked_set_payload_length(max_plaintext_size()) + + max_one_way_linked_set_payload_length(max_plaintext_size()) + + 2345 + ]; rng.fill_bytes(&mut message); - let mut sets = split_into_sets(&message); + let mut sets = split_into_sets(&mut rng, &message, max_plaintext_size()); assert_eq!(4, sets.len()); assert_eq!(sets[0].len(), u8::max_value() as usize); assert_eq!(sets[1].len(), u8::max_value() as usize); assert_eq!(sets[2].len(), u8::max_value() as usize); - verfiy_correct_link(&sets[0], &sets[1]); - verfiy_correct_link(&sets[1], &sets[2]); - verfiy_correct_link(&sets[2], &sets[3]); + verify_correct_link(&sets[0], &sets[1]); + verify_correct_link(&sets[1], &sets[2]); + verify_correct_link(&sets[2], &sets[3]); verify_pre_linked_set_payload( sets.pop().unwrap(), - &message[2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH..], + &message[2 * two_way_linked_set_payload_length(max_plaintext_size()) + + max_one_way_linked_set_payload_length(max_plaintext_size())..], ); verify_two_way_linked_set_payload( sets.pop().unwrap(), - &message[TWO_WAY_LINKED_SET_PAYLOAD_LENGTH + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - ..2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH], + &message[two_way_linked_set_payload_length(max_plaintext_size()) + + max_one_way_linked_set_payload_length(max_plaintext_size()) + ..2 * two_way_linked_set_payload_length(max_plaintext_size()) + + max_one_way_linked_set_payload_length(max_plaintext_size())], ); verify_two_way_linked_set_payload( sets.pop().unwrap(), - &message[MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - ..TWO_WAY_LINKED_SET_PAYLOAD_LENGTH + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH], + &message[max_one_way_linked_set_payload_length(max_plaintext_size()) + ..two_way_linked_set_payload_length(max_plaintext_size()) + + max_one_way_linked_set_payload_length(max_plaintext_size())], ); verify_post_linked_set_payload( sets.pop().unwrap(), - &message[..MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH], + &message[..max_one_way_linked_set_payload_length(max_plaintext_size())], ); - let mut message = [0u8; 2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH]; + let mut message = + vec![ + 0u8; + 2 * two_way_linked_set_payload_length(max_plaintext_size()) + + 2 * max_one_way_linked_set_payload_length(max_plaintext_size()) + ]; rng.fill_bytes(&mut message); - let mut sets = split_into_sets(&message); + let mut sets = split_into_sets(&mut rng, &message, max_plaintext_size()); assert_eq!(4, sets.len()); assert_eq!(sets[0].len(), u8::max_value() as usize); assert_eq!(sets[1].len(), u8::max_value() as usize); assert_eq!(sets[2].len(), u8::max_value() as usize); assert_eq!(sets[3].len(), u8::max_value() as usize); - verfiy_correct_link(&sets[0], &sets[1]); - verfiy_correct_link(&sets[1], &sets[2]); - verfiy_correct_link(&sets[2], &sets[3]); + verify_correct_link(&sets[0], &sets[1]); + verify_correct_link(&sets[1], &sets[2]); + verify_correct_link(&sets[2], &sets[3]); verify_pre_linked_set_payload( sets.pop().unwrap(), - &message[2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH..], + &message[2 * two_way_linked_set_payload_length(max_plaintext_size()) + + max_one_way_linked_set_payload_length(max_plaintext_size())..], ); verify_two_way_linked_set_payload( sets.pop().unwrap(), - &message[TWO_WAY_LINKED_SET_PAYLOAD_LENGTH + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - ..2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH], + &message[two_way_linked_set_payload_length(max_plaintext_size()) + + max_one_way_linked_set_payload_length(max_plaintext_size()) + ..2 * two_way_linked_set_payload_length(max_plaintext_size()) + + max_one_way_linked_set_payload_length(max_plaintext_size())], ); verify_two_way_linked_set_payload( sets.pop().unwrap(), - &message[MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - ..TWO_WAY_LINKED_SET_PAYLOAD_LENGTH + MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH], + &message[max_one_way_linked_set_payload_length(max_plaintext_size()) + ..two_way_linked_set_payload_length(max_plaintext_size()) + + max_one_way_linked_set_payload_length(max_plaintext_size())], ); verify_post_linked_set_payload( sets.pop().unwrap(), - &message[..MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH], + &message[..max_one_way_linked_set_payload_length(max_plaintext_size())], ); } } @@ -735,66 +815,89 @@ mod tests { fn total_number_of_sets() { assert_eq!( 1, - super::total_number_of_sets(MAX_UNLINKED_SET_PAYLOAD_LENGTH - 1) + super::total_number_of_sets( + max_unlinked_set_payload_length(max_plaintext_size()) - 1, + max_plaintext_size() + ) ); assert_eq!( 1, - super::total_number_of_sets(MAX_UNLINKED_SET_PAYLOAD_LENGTH) + super::total_number_of_sets( + max_unlinked_set_payload_length(max_plaintext_size()), + max_plaintext_size() + ) ); assert_eq!( 2, - super::total_number_of_sets(MAX_UNLINKED_SET_PAYLOAD_LENGTH + 1) + super::total_number_of_sets( + max_unlinked_set_payload_length(max_plaintext_size()) + 1, + max_plaintext_size() + ) ); assert_eq!( 2, - super::total_number_of_sets(2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH) - ); - assert_eq!( - 3, - super::total_number_of_sets(2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + 1) - ); - assert_eq!( - 3, super::total_number_of_sets( - 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - - 1 + 2 * max_one_way_linked_set_payload_length(max_plaintext_size()), + max_plaintext_size() ) ); assert_eq!( 3, super::total_number_of_sets( - 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH + TWO_WAY_LINKED_SET_PAYLOAD_LENGTH + 2 * max_one_way_linked_set_payload_length(max_plaintext_size()) + 1, + max_plaintext_size() + ) + ); + assert_eq!( + 3, + super::total_number_of_sets( + 2 * max_one_way_linked_set_payload_length(max_plaintext_size()) + + two_way_linked_set_payload_length(max_plaintext_size()) + - 1, + max_plaintext_size() + ) + ); + assert_eq!( + 3, + super::total_number_of_sets( + 2 * max_one_way_linked_set_payload_length(max_plaintext_size()) + + two_way_linked_set_payload_length(max_plaintext_size()), + max_plaintext_size() ) ); assert_eq!( 4, super::total_number_of_sets( - 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - + TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + 1 + 2 * max_one_way_linked_set_payload_length(max_plaintext_size()) + + two_way_linked_set_payload_length(max_plaintext_size()) + + 1, + max_plaintext_size() ) ); assert_eq!( 4, super::total_number_of_sets( - 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - + 2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - - 1 + 2 * max_one_way_linked_set_payload_length(max_plaintext_size()) + + 2 * two_way_linked_set_payload_length(max_plaintext_size()) + - 1, + max_plaintext_size() ) ); assert_eq!( 4, super::total_number_of_sets( - 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - + 2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH + 2 * max_one_way_linked_set_payload_length(max_plaintext_size()) + + 2 * two_way_linked_set_payload_length(max_plaintext_size()), + max_plaintext_size() ) ); assert_eq!( 5, super::total_number_of_sets( - 2 * MAX_ONE_WAY_LINKED_SET_PAYLOAD_LENGTH - + 2 * TWO_WAY_LINKED_SET_PAYLOAD_LENGTH - + 1 + 2 * max_one_way_linked_set_payload_length(max_plaintext_size()) + + 2 * two_way_linked_set_payload_length(max_plaintext_size()) + + 1, + max_plaintext_size() ) ); } diff --git a/common/nymsphinx/cover/Cargo.toml b/common/nymsphinx/cover/Cargo.toml new file mode 100644 index 0000000000..ccf8eb6005 --- /dev/null +++ b/common/nymsphinx/cover/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "nymsphinx-cover" +version = "0.1.0" +authors = ["Jedrzej Stuczynski "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +rand = { version = "0.7.3", features = ["wasm-bindgen"] } + +nymsphinx-acknowledgements = { path = "../acknowledgements" } +nymsphinx-addressing = { path = "../addressing" } +nymsphinx-chunking = { path = "../chunking" } +nymsphinx-params = { path = "../params" } +nymsphinx-types = { path = "../types" } +topology = { path = "../../topology" } \ No newline at end of file diff --git a/common/nymsphinx/cover/src/lib.rs b/common/nymsphinx/cover/src/lib.rs new file mode 100644 index 0000000000..fe50b44956 --- /dev/null +++ b/common/nymsphinx/cover/src/lib.rs @@ -0,0 +1,116 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use nymsphinx_acknowledgements::surb_ack::SURBAck; +use nymsphinx_acknowledgements::AckAes128Key; +use nymsphinx_addressing::clients::Recipient; +use nymsphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError}; +use nymsphinx_chunking::fragment::COVER_FRAG_ID; +use nymsphinx_params::packet_sizes::PacketSize; +use nymsphinx_types::builder::SphinxPacketBuilder; +use nymsphinx_types::{delays, Destination, Error as SphinxError, SphinxPacket}; +use rand::{CryptoRng, RngCore}; +use std::convert::TryFrom; +use std::net::SocketAddr; +use std::time; +use topology::{NymTopology, NymTopologyError}; + +pub const LOOP_COVER_MESSAGE_PAYLOAD: &[u8] = b"The cake is a lie!"; + +#[derive(Debug)] +pub enum CoverMessageError { + NoValidProvidersError, + InvalidTopologyError, + SphinxError(SphinxError), + InvalidFirstMixAddress, +} + +impl From for CoverMessageError { + fn from(err: SphinxError) -> Self { + CoverMessageError::SphinxError(err) + } +} + +impl From for CoverMessageError { + fn from(_: NymNodeRoutingAddressError) -> Self { + use CoverMessageError::*; + InvalidFirstMixAddress + } +} + +impl From for CoverMessageError { + fn from(_: NymTopologyError) -> Self { + CoverMessageError::InvalidTopologyError + } +} + +pub fn generate_loop_cover_surb_ack( + rng: &mut R, + topology: &T, + ack_key: &AckAes128Key, + full_address: &Recipient, + average_ack_delay: time::Duration, +) -> Result +where + R: RngCore + CryptoRng, + T: NymTopology, +{ + Ok(SURBAck::construct( + rng, + full_address, + ack_key, + &COVER_FRAG_ID.to_bytes(), + average_ack_delay, + topology, + )?) +} + +pub fn generate_loop_cover_packet( + rng: &mut R, + topology: &T, + ack_key: &AckAes128Key, + full_address: &Recipient, + average_ack_delay: time::Duration, + average_packet_delay: time::Duration, +) -> Result<(SocketAddr, SphinxPacket), CoverMessageError> +where + R: RngCore + CryptoRng, + T: NymTopology, +{ + // we don't care about total ack delay - we will not be retransmitting it anyway + let (_, ack_bytes) = + generate_loop_cover_surb_ack(rng, topology, ack_key, full_address, average_ack_delay)? + .prepare_for_sending(); + + let cover_payload: Vec<_> = ack_bytes + .into_iter() + .chain(LOOP_COVER_MESSAGE_PAYLOAD.into_iter().cloned()) + .collect(); + + let route = topology.random_route_to_gateway(&full_address.gateway())?; + let delays = delays::generate_from_average_duration(route.len(), average_packet_delay); + // in our design we don't care about SURB_ID + let destination = Destination::new(full_address.destination(), Default::default()); + + // once merged, that's an easy rng injection point for sphinx packets : ) + let packet = SphinxPacketBuilder::new() + .with_payload_size(PacketSize::default().payload_size()) + .build_packet(cover_payload, &route, &destination, &delays) + .unwrap(); + + let first_hop_address = + NymNodeRoutingAddress::try_from(route.first().unwrap().address.clone()).unwrap(); + + Ok((first_hop_address.into(), packet)) +} diff --git a/common/nymsphinx/framing/Cargo.toml b/common/nymsphinx/framing/Cargo.toml new file mode 100644 index 0000000000..6e645bd246 --- /dev/null +++ b/common/nymsphinx/framing/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "nymsphinx-framing" +version = "0.1.0" +authors = ["Jedrzej Stuczynski "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bytes = "0.5" +tokio-util = { version = "0.3.1", features = ["codec"] } + +nymsphinx-types = { path = "../types" } +nymsphinx-params = { path = "../params" } diff --git a/common/nymsphinx/src/framing/mod.rs b/common/nymsphinx/framing/src/lib.rs similarity index 97% rename from common/nymsphinx/src/framing/mod.rs rename to common/nymsphinx/framing/src/lib.rs index 4add544ffd..d86eaf918a 100644 --- a/common/nymsphinx/src/framing/mod.rs +++ b/common/nymsphinx/framing/src/lib.rs @@ -12,9 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::packets::{InvalidPacketSize, PacketSize}; use bytes::{Buf, BufMut, BytesMut}; -use sphinx::SphinxPacket; +use nymsphinx_params::packet_sizes::{InvalidPacketSize, PacketSize}; +use nymsphinx_types::SphinxPacket; use std::convert::TryFrom; use std::io; use tokio_util::codec::{Decoder, Encoder}; diff --git a/common/nymsphinx/params/Cargo.toml b/common/nymsphinx/params/Cargo.toml new file mode 100644 index 0000000000..b00828e908 --- /dev/null +++ b/common/nymsphinx/params/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "nymsphinx-params" +version = "0.1.0" +authors = ["Jedrzej Stuczynski "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +nymsphinx-types = { path = "../types" } \ No newline at end of file diff --git a/common/nymsphinx/params/src/lib.rs b/common/nymsphinx/params/src/lib.rs new file mode 100644 index 0000000000..17543c20d3 --- /dev/null +++ b/common/nymsphinx/params/src/lib.rs @@ -0,0 +1,15 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +pub mod packet_sizes; diff --git a/common/nymsphinx/src/packets.rs b/common/nymsphinx/params/src/packet_sizes.rs similarity index 80% rename from common/nymsphinx/src/packets.rs rename to common/nymsphinx/params/src/packet_sizes.rs index 34b22071a7..5c8969797c 100644 --- a/common/nymsphinx/src/packets.rs +++ b/common/nymsphinx/params/src/packet_sizes.rs @@ -12,22 +12,23 @@ // See the License for the specific language governing permissions and // limitations under the License. +use nymsphinx_types::header::HEADER_SIZE; +use nymsphinx_types::PAYLOAD_OVERHEAD_SIZE; use std::convert::TryFrom; // it's up to the smart people to figure those values out : ) -const REGULAR_PACKET_SIZE: usize = 2 * 1024; -const ACK_PACKET_SIZE: usize = 512; -const EXTENDED_PACKET_SIZE: usize = 32 * 1024; +const REGULAR_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 2 * 1024; +const ACK_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 24; +const EXTENDED_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 32 * 1024; pub struct InvalidPacketSize; #[repr(u8)] +#[derive(Clone, Copy, Debug)] pub enum PacketSize { RegularPacket = 1, // for example instant messaging use case ACKPacket = 2, // for sending SURB-ACKs ExtendedPacket = 3, // for example for streaming fast and furious in uncompressed 10bit 4K HDR quality - - PreSURBChanges = 0, } impl TryFrom for PacketSize { @@ -38,7 +39,6 @@ impl TryFrom for PacketSize { _ if value == (PacketSize::RegularPacket as u8) => Ok(Self::RegularPacket), _ if value == (PacketSize::ACKPacket as u8) => Ok(Self::ACKPacket), _ if value == (PacketSize::ExtendedPacket as u8) => Ok(Self::ExtendedPacket), - _ if value == (PacketSize::PreSURBChanges as u8) => Ok(Self::PreSURBChanges), _ => Err(InvalidPacketSize), } } @@ -50,10 +50,17 @@ impl PacketSize { PacketSize::RegularPacket => REGULAR_PACKET_SIZE, PacketSize::ACKPacket => ACK_PACKET_SIZE, PacketSize::ExtendedPacket => EXTENDED_PACKET_SIZE, - PacketSize::PreSURBChanges => crate::PACKET_SIZE, } } + pub fn plaintext_size(&self) -> usize { + self.size() - HEADER_SIZE - PAYLOAD_OVERHEAD_SIZE + } + + pub fn payload_size(&self) -> usize { + self.size() - HEADER_SIZE + } + pub fn get_type(size: usize) -> std::result::Result { if PacketSize::RegularPacket.size() == size { Ok(PacketSize::RegularPacket) @@ -61,8 +68,6 @@ impl PacketSize { Ok(PacketSize::ACKPacket) } else if PacketSize::ExtendedPacket.size() == size { Ok(PacketSize::ExtendedPacket) - } else if PacketSize::PreSURBChanges.size() == size { - Ok(PacketSize::PreSURBChanges) } else { Err(InvalidPacketSize) } diff --git a/common/nymsphinx/src/chunking/fragment.rs b/common/nymsphinx/src/chunking/fragment.rs deleted file mode 100644 index ddb315f479..0000000000 --- a/common/nymsphinx/src/chunking/fragment.rs +++ /dev/null @@ -1,1157 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// 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. - -use crate::chunking::ChunkingError; -use std::convert::TryInto; - -/// The entire marshaled `Fragment` can never be longer than the maximum length of the plaintext -/// data we can put into a Sphinx packet. -pub const MAXIMUM_FRAGMENT_LENGTH: usize = sphinx::constants::MAXIMUM_PLAINTEXT_LENGTH; - -/// The minimum data overhead required for message fitting into a single `Fragment`. The single byte -/// used to literally indicate "this message is not fragmented". -pub const UNFRAGMENTED_HEADER_LEN: usize = 1; - -/// When the underlying message has to be split into multiple Fragments, but still manages to fit -/// into a single `FragmentSet`, each `FragmentHeader` needs to hold additional information to allow -/// for correct message reconstruction: 4 bytes for set id, 1 byte to represent total number -/// of fragments in the set, 1 byte to represent position of the current fragment in the set -/// and finally an extra byte to indicate the fragment has no links to other sets. -pub const UNLINKED_FRAGMENTED_HEADER_LEN: usize = 7; - -/// Logically almost identical to `UNLINKED_FRAGMENTED_HEADER_LEN`, however, the extra three -/// bytes are due to changing the final byte that used to indicate the `Fragment` is not linked -/// into 4 byte id of either previous or the next set. -/// Note that the linked headers can potentially be used only for very first and very last -/// `Fragment` in a `FragmentSet`. -pub const LINKED_FRAGMENTED_HEADER_LEN: usize = 10; - -/// Maximum size of payload of each fragment is always the maximum amount of plaintext data -/// we can put into a sphinx packet minus length of respective fragment header. -pub const UNFRAGMENTED_PAYLOAD_MAX_LEN: usize = MAXIMUM_FRAGMENT_LENGTH - UNFRAGMENTED_HEADER_LEN; - -/// Maximum size of payload of each fragment is always the maximum amount of plaintext data -/// we can put into a sphinx packet minus length of respective fragment header. -pub const UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN: usize = - MAXIMUM_FRAGMENT_LENGTH - UNLINKED_FRAGMENTED_HEADER_LEN; - -/// Maximum size of payload of each fragment is always the maximum amount of plaintext data -/// we can put into a sphinx packet minus length of respective fragment header. -pub const LINKED_FRAGMENTED_PAYLOAD_MAX_LEN: usize = - MAXIMUM_FRAGMENT_LENGTH - LINKED_FRAGMENTED_HEADER_LEN; - -/// The basic unit of division of underlying bytes message sent through the mix network. -/// Each `Fragment` after being marshaled is guaranteed to fit into a single sphinx packet. -/// The `Fragment` itself consists of part, or whole of, message to be sent as well as additional -/// header used to reconstruct the message after being received. -#[derive(PartialEq, Clone, Debug)] -pub(crate) struct Fragment { - header: FragmentHeader, - payload: Vec, -} - -impl Fragment { - /// Tries to encapsulate provided payload slice and metadata into a `Fragment`. - /// It can fail if payload would not fully fit in a single `Fragment` or some of the metadata - /// is malformed or self-contradictory, for example if current_fragment > total_fragments. - pub(crate) fn try_new_fragmented( - payload: &[u8], - id: i32, - total_fragments: u8, - current_fragment: u8, - previous_fragments_set_id: Option, - next_fragments_set_id: Option, - ) -> Result { - let header = FragmentHeader::try_new_fragmented( - id, - total_fragments, - current_fragment, - previous_fragments_set_id, - next_fragments_set_id, - )?; - - // check for whether payload has expected length, which depend on whether fragment is linked - // and if it's the only one or the last one in the set (then lower bound is removed) - if previous_fragments_set_id.is_some() { - if total_fragments > 1 { - if payload.len() != LINKED_FRAGMENTED_PAYLOAD_MAX_LEN { - return Err(ChunkingError::InvalidPayloadLengthError); - } - } else { - if payload.len() > LINKED_FRAGMENTED_PAYLOAD_MAX_LEN { - return Err(ChunkingError::InvalidPayloadLengthError); - } - } - } else if next_fragments_set_id.is_some() { - if payload.len() != LINKED_FRAGMENTED_PAYLOAD_MAX_LEN { - return Err(ChunkingError::InvalidPayloadLengthError); - } - } else { - if total_fragments != current_fragment { - if payload.len() != UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN { - return Err(ChunkingError::InvalidPayloadLengthError); - } - } else { - if payload.len() > UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN { - return Err(ChunkingError::InvalidPayloadLengthError); - } - } - } - - Ok(Fragment { - header, - payload: payload.to_vec(), - }) - } - - /// The most efficient way of representing underlying message incurring the least data overhead - /// for as long as the message can fit into a single, unfragmented, `Fragment`. - pub(crate) fn try_new_unfragmented(payload: &[u8]) -> Result { - if payload.len() > UNFRAGMENTED_PAYLOAD_MAX_LEN { - Err(ChunkingError::InvalidPayloadLengthError) - } else { - Ok(Fragment { - header: FragmentHeader::new_unfragmented(), - payload: payload.to_vec(), - }) - } - } - - /// Convert this `Fragment` into vector of bytes which can be put into a sphinx packet. - pub(crate) fn into_bytes(self) -> Vec { - self.header - .to_bytes() - .into_iter() - .chain(self.payload.into_iter()) - .collect() - } - - /// Extracts id of this `Fragment`. - pub(crate) fn id(&self) -> i32 { - self.header.id - } - - /// Extracts total number of fragments associated with this particular `Fragment` (belonging to - /// the same `FragmentSet`). - pub(crate) fn total_fragments(&self) -> u8 { - self.header.total_fragments - } - - /// Extracts position of this `Fragment` in a `FragmentSet`. - pub(crate) fn current_fragment(&self) -> u8 { - self.header.current_fragment - } - - /// Extracts information regarding id of pre-linked `FragmentSet` - pub(crate) fn previous_fragments_set_id(&self) -> Option { - self.header.previous_fragments_set_id - } - - /// Extracts information regarding id of post-linked `FragmentSet` - pub(crate) fn next_fragments_set_id(&self) -> Option { - self.header.next_fragments_set_id - } - - /// Consumes `Self` to obtain payload (i.e. part of original message) associated with this - /// `Fragment`. - pub(crate) fn extract_payload(self) -> Vec { - self.payload - } - - /// Tries to recover `Fragment` from slice of bytes extracted from received sphinx packet. - /// It can fail if payload would not fully fit in a single `Fragment` or some of the metadata - /// is malformed or self-contradictory, for example if current_fragment > total_fragments. - pub(crate) fn try_from_bytes(b: &[u8]) -> Result { - let (header, n) = FragmentHeader::try_from_bytes(b)?; - - // determine what's our expected payload size bound and whether the message fits in this - let is_payload_in_range = if header.is_fragmented { - if header.is_linked() { - if header.is_final() { - (b.len() - n) <= LINKED_FRAGMENTED_PAYLOAD_MAX_LEN - } else { - (b.len() - n) == LINKED_FRAGMENTED_PAYLOAD_MAX_LEN - } - } else { - if header.is_final() { - (b.len() - n) <= UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - } else { - (b.len() - n) == UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - } - } - } else { - (b.len() - n) <= UNFRAGMENTED_PAYLOAD_MAX_LEN - }; - - if !is_payload_in_range { - return Err(ChunkingError::MalformedFragmentData); - } - - Ok(Fragment { - header, - payload: b[n..].to_vec(), - }) - } -} - -/// In order to be able to re-assemble fragmented message sent through a mix-network, some -/// metadata is attached alongside the actual message payload. The idea is to include as little -/// of that data as possible due to computationally very costly process of sphinx encapsulation. -/// -/// The generic `FragmentHeader` is represented as follows: -/// IF flag || 31 bit ID || TotalFragments || CurrentFragment || LID flag || 31 bit Linked ID -/// note that LID is a valid flag only for first and -/// last fragment (if TotalFragments == CurrentFragment == 255) in given set. -/// -/// further note that if IF (isFragmented) is not set, -/// then the remaining bytes of the header are used as payload and also note that if LID is not set, -/// then the Linked ID bytes in the header are used as payload. -/// -/// Hence after marshaling `FragmentHeader` into bytes, -/// the following three alternatives are possible: -/// -/// Single byte representing that the `Fragment` is the only one into which the message was split. -/// '0'byte -/// -/// 7 byte long sequence representing that this `Fragment` is one of multiple ones in the set. -/// However, the set is not linked to any other sets: -/// '1'bit || 31-bit ID || 1-byte TF || 1 byte CF || '0'byte -/// -/// 10 byte sequence representing first (or last) fragment in the set, -/// where the set is linked to either preceding data (TF == 1) or proceeding data (TF == CF == 255) -/// '1'bit || 31-bit ID || 1-byte TF || 1 byte CF || '1'bit || 31-bit LID -/// -/// And hence for messages smaller than sphinx::constants::MAXIMUM_PLAINTEXT_LENGTH, -/// there is only a single byte of overhead; -/// for messages larger than sphinx::constants::MAXIMUM_PLAINTEXT_LENGTH but small enough -/// to avoid set division (which happens if message has to be fragmented into more than 255 fragments) -/// there is 7 bytes of overhead inside each sphinx packet sent -/// and finally for the longest messages, without upper bound, there is usually also only 7 bytes -/// of overhead apart from first and last fragments in each set that instead have 10 bytes of overhead. -#[derive(PartialEq, Clone, Debug)] -pub(crate) struct FragmentHeader { - /// Flag used to indicate whether this `Fragment` is the only one into which original - /// message was split. - /// If so, rest of the fields are meaningless as they are set to their default values - /// the whole time and serve no purpose when marshaling or unmarshaling a `FragmentHeader` - is_fragmented: bool, - - /// ID associated with `FragmentSet` to which this particular `Fragment` belongs. - /// Its value is restricted to (0, i32::max_value()]. - /// Note that it *excludes* 0, but *includes* i32::max_value(). - /// This allows the field to be represented using 31 bits. - id: i32, - - /// Total number of `Fragment`s in `FragmentSet` used to be able to determine if entire - /// set was fully received as well as to perform bound checks. - total_fragments: u8, - - /// Since message is always fragmented into payloads of constant lengths - /// (apart from possibly the last one), there's no need to use offsets like ipv4/ipv6 - /// and we can just simply enumerate the fragments to later reconstruct the message. - current_fragment: u8, - - /// Optional ID of previous `FragmentSet` into which the original message was split. - /// Note, this option is only valid of `current_fragment == 1` - previous_fragments_set_id: Option, - - /// Optional ID of next `FragmentSet` into which the original message was split. - /// Note, this option is only valid of `current_fragment == total_fragments == u8::max_value()` - next_fragments_set_id: Option, -} - -impl FragmentHeader { - /// Tries to create a new `FragmentHeader` using provided metadata. Bunch of logical - /// checks are performed to see if the data is not self-contradictory, - /// for example if current_fragment > total_fragments. - fn try_new_fragmented( - id: i32, - total_fragments: u8, - current_fragment: u8, - previous_fragments_set_id: Option, - next_fragments_set_id: Option, - ) -> Result { - if id <= 0 { - return Err(ChunkingError::MalformedHeaderError); - } - if total_fragments < current_fragment { - return Err(ChunkingError::MalformedHeaderError); - } - if total_fragments == 0 { - return Err(ChunkingError::MalformedHeaderError); - } - if current_fragment == 0 { - return Err(ChunkingError::MalformedHeaderError); - } - if let Some(pfid) = previous_fragments_set_id { - if pfid <= 0 || current_fragment != 1 || pfid == id { - return Err(ChunkingError::MalformedHeaderError); - } - } - if let Some(nfid) = next_fragments_set_id { - if nfid <= 0 || current_fragment != total_fragments || nfid == id { - return Err(ChunkingError::MalformedHeaderError); - } - } - - Ok(FragmentHeader { - is_fragmented: true, - id, - total_fragments, - current_fragment, - previous_fragments_set_id, - next_fragments_set_id, - }) - } - - /// When unfragmented header is created, no checks need to happen as there is no variability - /// in the values of its fields. All of them always have the same, default, values. - fn new_unfragmented() -> Self { - FragmentHeader { - is_fragmented: false, - id: 0, - total_fragments: 1, - current_fragment: 1, - previous_fragments_set_id: None, - next_fragments_set_id: None, - } - } - - /// Tries to recover `FragmentHeader` from slice of bytes extracted from received sphinx packet. - /// If successful, returns `Self` and number of bytes used, as those can differ based on the - /// type of header (unfragmented, unlinked, linked). - fn try_from_bytes(b: &[u8]) -> Result<(Self, usize), ChunkingError> { - if b.is_empty() { - return Err(ChunkingError::TooShortFragmentData); - } - // check if it's fragmented - if it's not - the whole first byte is set to 0 - // otherwise first bit is set to 1 - if b[0] == 0 { - Ok((Self::new_unfragmented(), 1)) - } else { - // if it's fragmented, it needs to be at least 7 bytes long - if b.len() < UNLINKED_FRAGMENTED_HEADER_LEN { - return Err(ChunkingError::TooShortFragmentData); - } - let frag_id = i32::from_be_bytes(b[0..4].try_into().unwrap()); - // sanity check for the fragmentation flag - if ((frag_id >> 31) & 1) == 0 { - return Err(ChunkingError::MalformedHeaderError); - } - - let id = frag_id & !(1 << 31); // make sure to clear the flag bit to parse id correctly - let total_fragments = b[4]; - let current_fragment = b[5]; - - if total_fragments == 0 || current_fragment == 0 || current_fragment > total_fragments { - return Err(ChunkingError::MalformedHeaderError); - } - - let mut previous_fragments_set_id = None; - let mut next_fragments_set_id = None; - - // check if the linking id flag might be set - let read_bytes = if b[6] != 0 { - // there's linking ID supposedly attached, make sure we have enough bytes to parse - if b.len() < LINKED_FRAGMENTED_HEADER_LEN { - return Err(ChunkingError::TooShortFragmentData); - } - let flagged_linked_id = i32::from_be_bytes(b[6..10].try_into().unwrap()); - - // sanity check for the linked flag - if ((flagged_linked_id >> 31) & 1) == 0 { - return Err(ChunkingError::MalformedHeaderError); - } - - let linked_id = flagged_linked_id & !(1 << 31); // make sure to clear the flag bit to parse id correctly - - if current_fragment == 1 { - previous_fragments_set_id = Some(linked_id); - } else if total_fragments == current_fragment && current_fragment == u8::max_value() - { - next_fragments_set_id = Some(linked_id); - } else { - return Err(ChunkingError::MalformedHeaderError); - } - - 10 - } else { - 7 - }; - - Ok(( - Self::try_new_fragmented( - id, - total_fragments, - current_fragment, - previous_fragments_set_id, - next_fragments_set_id, - )?, - read_bytes, - )) - } - } - - /// Helper method to determine if this `FragmentHeader` is used to represent a linked `Fragment`. - fn is_linked(&self) -> bool { - self.previous_fragments_set_id.is_some() || self.next_fragments_set_id.is_some() - } - - /// Helper method to determine if this `FragmentHeader` is used to represent a `Fragment` that - /// is a final one in some `FragmentSet`. - fn is_final(&self) -> bool { - self.total_fragments == self.current_fragment - } - - /// Marshal this `FragmentHeader` into vector of bytes which can be put into a sphinx packet. - fn to_bytes(&self) -> Vec { - if self.is_fragmented { - let frag_id = self.id | (1 << 31); - let frag_id_bytes = frag_id.to_be_bytes(); - let bytes_prefix_iter = frag_id_bytes - .iter() - .cloned() - .chain(std::iter::once(self.total_fragments)) - .chain(std::iter::once(self.current_fragment)); - - let is_linked = - self.previous_fragments_set_id.is_some() || self.next_fragments_set_id.is_some(); - if is_linked { - let linked_id = self - .previous_fragments_set_id - .unwrap_or_else(|| self.next_fragments_set_id.unwrap()); - let linked_id_entry = linked_id | (1 << 31); - let linked_id_bytes = linked_id_entry.to_be_bytes(); - bytes_prefix_iter - .chain(linked_id_bytes.iter().cloned()) - .collect() - } else { - bytes_prefix_iter.chain(std::iter::once(0)).collect() - } - } else { - [0].to_vec() - } - } -} - -// everything below are tests - -#[cfg(test)] -mod fragment { - use rand::{thread_rng, RngCore}; - - use super::*; - - #[test] - fn can_be_converted_to_and_from_bytes_for_unfragmented_payload() { - let mut rng = thread_rng(); - - let mlen = UNFRAGMENTED_PAYLOAD_MAX_LEN - 20; - let mut valid_message = vec![0u8; mlen]; - rng.fill_bytes(&mut valid_message); - - let valid_unfragmented_packet = Fragment { - header: FragmentHeader::new_unfragmented(), - payload: valid_message, - }; - let packet_bytes = valid_unfragmented_packet.clone().into_bytes(); - assert_eq!( - valid_unfragmented_packet, - Fragment::try_from_bytes(&packet_bytes).unwrap() - ); - - let empty_unfragmented_packet = Fragment { - header: FragmentHeader::new_unfragmented(), - payload: Vec::new(), - }; - let packet_bytes = empty_unfragmented_packet.clone().into_bytes(); - assert_eq!( - empty_unfragmented_packet, - Fragment::try_from_bytes(&packet_bytes).unwrap() - ); - - let mut full_message = vec![0u8; UNFRAGMENTED_PAYLOAD_MAX_LEN]; - rng.fill_bytes(&mut full_message); - - let full_unfragmented_packet = Fragment { - header: FragmentHeader::new_unfragmented(), - payload: full_message, - }; - let packet_bytes = full_unfragmented_packet.clone().into_bytes(); - assert_eq!( - full_unfragmented_packet, - Fragment::try_from_bytes(&packet_bytes).unwrap() - ); - } - - #[test] - fn conversion_from_bytes_fails_for_too_long_unfragmented_payload() { - let mut rng = thread_rng(); - - let mlen = UNFRAGMENTED_PAYLOAD_MAX_LEN + 1; - let mut message = vec![0u8; mlen]; - rng.fill_bytes(&mut message); - - let packet = Fragment { - header: FragmentHeader::new_unfragmented(), - payload: message, - }; - - let packet_bytes = packet.into_bytes(); - assert_eq!( - Fragment::try_from_bytes(&packet_bytes), - Err(ChunkingError::MalformedFragmentData) - ); - } - - #[test] - fn can_be_converted_to_and_from_bytes_for_unlinked_fragmented_payload() { - let mut rng = thread_rng(); - - let mut msg = vec![0u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; - rng.fill_bytes(&mut msg); - - let non_last_packet = Fragment { - header: FragmentHeader::try_new_fragmented(12345, 10, 5, None, None).unwrap(), - payload: msg, - }; - let packet_bytes = non_last_packet.clone().into_bytes(); - assert_eq!( - non_last_packet, - Fragment::try_from_bytes(&packet_bytes).unwrap() - ); - - let mut msg = vec![0u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; - rng.fill_bytes(&mut msg); - - let last_full_packet = Fragment { - header: FragmentHeader::try_new_fragmented(12345, 10, 10, None, None).unwrap(), - payload: msg, - }; - let packet_bytes = last_full_packet.clone().into_bytes(); - assert_eq!( - last_full_packet, - Fragment::try_from_bytes(&packet_bytes).unwrap() - ); - - let mut msg = vec![0u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 20]; - rng.fill_bytes(&mut msg); - - let last_non_full_packet = Fragment { - header: FragmentHeader::try_new_fragmented(12345, 10, 10, None, None).unwrap(), - payload: msg, - }; - let packet_bytes = last_non_full_packet.clone().into_bytes(); - assert_eq!( - last_non_full_packet, - Fragment::try_from_bytes(&packet_bytes).unwrap() - ); - } - - #[test] - fn conversion_from_bytes_fails_for_too_long_unlinked_fragmented_payload() { - let mut rng = thread_rng(); - - let mlen = UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 1; - let mut message = vec![0u8; mlen]; - rng.fill_bytes(&mut message); - - let packet = Fragment { - header: FragmentHeader::try_new_fragmented(12345, 10, 5, None, None).unwrap(), - payload: message, - }; - - let packet_bytes = packet.into_bytes(); - assert_eq!( - Fragment::try_from_bytes(&packet_bytes), - Err(ChunkingError::MalformedFragmentData) - ); - } - - #[test] - fn conversion_from_bytes_fails_for_too_short_fragmented_payload_if_not_last() { - let mut rng = thread_rng(); - - let mlen = UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 1; - let mut message = vec![0u8; mlen]; - rng.fill_bytes(&mut message); - - let packet = Fragment { - header: FragmentHeader::try_new_fragmented(12345, 10, 5, None, None).unwrap(), - payload: message, - }; - - let packet_bytes = packet.into_bytes(); - assert_eq!( - Fragment::try_from_bytes(&packet_bytes), - Err(ChunkingError::MalformedFragmentData) - ); - } - - #[test] - fn can_be_converted_to_and_from_bytes_for_pre_linked_fragmented_payload() { - let mut rng = thread_rng(); - - let mut msg = vec![0u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; - rng.fill_bytes(&mut msg); - - let fragment = Fragment { - header: FragmentHeader::try_new_fragmented(12345, 10, 1, Some(1234), None).unwrap(), - payload: msg, - }; - let packet_bytes = fragment.clone().into_bytes(); - assert_eq!(fragment, Fragment::try_from_bytes(&packet_bytes).unwrap()); - - let mut msg = vec![0u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 20]; - rng.fill_bytes(&mut msg); - - let fragment = Fragment { - header: FragmentHeader::try_new_fragmented(12345, 1, 1, Some(1234), None).unwrap(), - payload: msg, - }; - let packet_bytes = fragment.clone().into_bytes(); - assert_eq!(fragment, Fragment::try_from_bytes(&packet_bytes).unwrap()); - } - - #[test] - fn conversion_from_bytes_fails_for_too_long_pre_linked_fragmented_payload() { - let mut rng = thread_rng(); - - let mlen = LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 1; - let mut message = vec![0u8; mlen]; - rng.fill_bytes(&mut message); - - let packet = Fragment { - header: FragmentHeader::try_new_fragmented(12345, 10, 1, Some(1234), None).unwrap(), - payload: message, - }; - - let packet_bytes = packet.into_bytes(); - assert_eq!( - Fragment::try_from_bytes(&packet_bytes), - Err(ChunkingError::MalformedFragmentData) - ); - } - - #[test] - fn can_be_converted_to_and_from_bytes_for_post_linked_fragmented_payload() { - let mut rng = thread_rng(); - - let mut msg = vec![0u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; - rng.fill_bytes(&mut msg); - - let fragment = Fragment { - header: FragmentHeader::try_new_fragmented( - 12345, - u8::max_value(), - u8::max_value(), - None, - Some(1234), - ) - .unwrap(), - payload: msg, - }; - let packet_bytes = fragment.clone().into_bytes(); - assert_eq!(fragment, Fragment::try_from_bytes(&packet_bytes).unwrap()); - - let mut msg = vec![0u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 20]; - rng.fill_bytes(&mut msg); - - let fragment = Fragment { - header: FragmentHeader::try_new_fragmented( - 12345, - u8::max_value(), - u8::max_value(), - None, - Some(1234), - ) - .unwrap(), - payload: msg, - }; - let packet_bytes = fragment.clone().into_bytes(); - assert_eq!(fragment, Fragment::try_from_bytes(&packet_bytes).unwrap()); - } - - #[test] - fn conversion_from_bytes_fails_for_too_long_post_linked_fragmented_payload() { - let mut rng = thread_rng(); - - let mlen = LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 1; - let mut message = vec![0u8; mlen]; - rng.fill_bytes(&mut message); - - let packet = Fragment { - header: FragmentHeader::try_new_fragmented( - 12345, - u8::max_value(), - u8::max_value(), - None, - Some(1234), - ) - .unwrap(), - payload: message, - }; - - let packet_bytes = packet.into_bytes(); - assert_eq!( - Fragment::try_from_bytes(&packet_bytes), - Err(ChunkingError::MalformedFragmentData) - ); - } - - #[test] - fn unfragmented_fragment_can_be_created_with_payload_of_valid_length() { - let payload = [1u8; UNFRAGMENTED_PAYLOAD_MAX_LEN]; - assert!(Fragment::try_new_unfragmented(&payload).is_ok()); - - let payload = [1u8; UNFRAGMENTED_PAYLOAD_MAX_LEN - 1]; - assert!(Fragment::try_new_unfragmented(&payload).is_ok()); - - let payload = [1u8; UNFRAGMENTED_PAYLOAD_MAX_LEN - 20]; - assert!(Fragment::try_new_unfragmented(&payload).is_ok()); - } - - #[test] - fn unfragmented_fragment_returns_error_when_created_with_payload_of_invalid_length() { - let payload = [1u8; UNFRAGMENTED_PAYLOAD_MAX_LEN + 1]; - assert!(Fragment::try_new_unfragmented(&payload).is_err()); - - let payload = [1u8; UNFRAGMENTED_PAYLOAD_MAX_LEN + 20]; - assert!(Fragment::try_new_unfragmented(&payload).is_err()); - } - - #[test] - fn unlinked_fragment_can_be_created_with_payload_of_valid_length() { - let id = 12345; - let full_payload = [1u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; - let non_full_payload = [1u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 1]; - let non_full_payload2 = [1u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 20]; - - assert!(Fragment::try_new_fragmented(&full_payload, id, 10, 1, None, None).is_ok()); - assert!(Fragment::try_new_fragmented(&full_payload, id, 10, 5, None, None).is_ok()); - assert!(Fragment::try_new_fragmented(&full_payload, id, 10, 10, None, None).is_ok()); - assert!(Fragment::try_new_fragmented(&full_payload, id, 1, 1, None, None).is_ok()); - - assert!(Fragment::try_new_fragmented(&non_full_payload, id, 10, 10, None, None).is_ok()); - assert!(Fragment::try_new_fragmented(&non_full_payload, id, 1, 1, None, None).is_ok()); - - assert!(Fragment::try_new_fragmented(&non_full_payload2, id, 10, 10, None, None).is_ok()); - assert!(Fragment::try_new_fragmented(&non_full_payload2, id, 1, 1, None, None).is_ok()); - } - - #[test] - fn unlinked_fragment_returns_error_when_created_with_payload_of_invalid_length() { - let id = 12345; - let non_full_payload = [1u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 1]; - let non_full_payload2 = [1u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 20]; - let too_much_payload = [1u8; UNLINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 1]; - - assert!(Fragment::try_new_fragmented(&non_full_payload, id, 10, 1, None, None).is_err()); - assert!(Fragment::try_new_fragmented(&non_full_payload, id, 10, 5, None, None).is_err()); - - assert!(Fragment::try_new_fragmented(&too_much_payload, id, 10, 1, None, None).is_err()); - assert!(Fragment::try_new_fragmented(&too_much_payload, id, 10, 5, None, None).is_err()); - assert!(Fragment::try_new_fragmented(&too_much_payload, id, 1, 1, None, None).is_err()); - - assert!(Fragment::try_new_fragmented(&non_full_payload2, id, 10, 1, None, None).is_err()); - assert!(Fragment::try_new_fragmented(&non_full_payload2, id, 10, 5, None, None).is_err()); - } - - #[test] - fn linked_fragment_can_be_created_with_payload_of_valid_length() { - let id = 12345; - let link_id = 1234; - let full_payload = [1u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN]; - let non_full_payload = [1u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 1]; - let non_full_payload2 = [1u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 20]; - - assert!( - Fragment::try_new_fragmented(&full_payload, id, 10, 1, Some(link_id), None).is_ok() - ); - assert!(Fragment::try_new_fragmented(&full_payload, id, 1, 1, Some(link_id), None).is_ok()); - assert!( - Fragment::try_new_fragmented(&non_full_payload, id, 1, 1, Some(link_id), None).is_ok() - ); - assert!( - Fragment::try_new_fragmented(&non_full_payload2, id, 1, 1, Some(link_id), None).is_ok() - ); - - assert!(Fragment::try_new_fragmented( - &full_payload, - id, - u8::max_value(), - u8::max_value(), - None, - Some(link_id) - ) - .is_ok()); - } - - #[test] - fn linked_fragment_returns_error_when_created_with_payload_of_invalid_length() { - let id = 12345; - let link_id = 1234; - let non_full_payload = [1u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 1]; - let non_full_payload2 = [1u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN - 20]; - let too_much_payload = [1u8; LINKED_FRAGMENTED_PAYLOAD_MAX_LEN + 1]; - - assert!( - Fragment::try_new_fragmented(&non_full_payload, id, 10, 1, Some(link_id), None) - .is_err() - ); - assert!( - Fragment::try_new_fragmented(&non_full_payload2, id, 10, 1, Some(link_id), None) - .is_err() - ); - assert!( - Fragment::try_new_fragmented(&too_much_payload, id, 10, 1, Some(link_id), None) - .is_err() - ); - assert!( - Fragment::try_new_fragmented(&too_much_payload, id, 1, 1, Some(link_id), None).is_err() - ); - - assert!(Fragment::try_new_fragmented( - &non_full_payload, - id, - u8::max_value(), - u8::max_value(), - None, - Some(link_id) - ) - .is_err()); - assert!(Fragment::try_new_fragmented( - &non_full_payload2, - id, - u8::max_value(), - u8::max_value(), - None, - Some(link_id) - ) - .is_err()); - - assert!(Fragment::try_new_fragmented( - &too_much_payload, - id, - u8::max_value(), - u8::max_value(), - None, - Some(link_id) - ) - .is_err()); - } -} - -#[cfg(test)] -mod fragment_header { - use super::*; - - #[cfg(test)] - mod unfragmented_payload { - use super::*; - - #[test] - fn can_be_converted_to_and_from_bytes_for_exact_number_of_bytes_provided() { - let unfragmented_header = FragmentHeader::new_unfragmented(); - - let header_bytes = unfragmented_header.to_bytes(); - let (recovered_header, bytes_used) = - FragmentHeader::try_from_bytes(&header_bytes).unwrap(); - assert_eq!(unfragmented_header, recovered_header); - assert_eq!(UNFRAGMENTED_HEADER_LEN, bytes_used); - } - - #[test] - fn can_be_converted_to_and_from_bytes_for_more_than_required_number_of_bytes() { - let unfragmented_header = FragmentHeader::new_unfragmented(); - - let mut header_bytes = unfragmented_header.to_bytes(); - header_bytes.append(vec![1, 2, 3, 4, 5].as_mut()); - - let (recovered_header, bytes_used) = - FragmentHeader::try_from_bytes(&header_bytes).unwrap(); - assert_eq!(unfragmented_header, recovered_header); - assert_eq!(UNFRAGMENTED_HEADER_LEN, bytes_used); - } - - #[test] - fn retrieval_from_bytes_fail_for_empty_slice() { - let empty_vec = Vec::new(); - - assert!(FragmentHeader::try_from_bytes(&empty_vec).is_err()) - } - - #[test] - fn retrieval_from_bytes_fail_for_invalid_fragmentation_flag() { - let unfragmented_header = FragmentHeader::new_unfragmented(); - - let mut header_bytes = unfragmented_header.to_bytes(); - header_bytes.append(vec![1, 2, 3, 4, 5].as_mut()); - - // set the fragmentation flag - header_bytes[0] |= 1 << 7; - - assert!(FragmentHeader::try_from_bytes(&header_bytes).is_err()); - } - } - - #[cfg(test)] - mod unlinked_fragmented_payload { - use super::*; - - #[test] - fn can_be_converted_to_and_from_bytes_for_exact_number_of_bytes_provided() { - let fragmented_header = - FragmentHeader::try_new_fragmented(12345, 10, 5, None, None).unwrap(); - - let header_bytes = fragmented_header.to_bytes(); - let (recovered_header, bytes_used) = - FragmentHeader::try_from_bytes(&header_bytes).unwrap(); - assert_eq!(fragmented_header, recovered_header); - assert_eq!(UNLINKED_FRAGMENTED_HEADER_LEN, bytes_used); - } - - #[test] - fn can_be_converted_to_and_from_bytes_for_more_than_required_number_of_bytes() { - let fragmented_header = - FragmentHeader::try_new_fragmented(12345, 10, 5, None, None).unwrap(); - - let mut header_bytes = fragmented_header.to_bytes(); - header_bytes.append(vec![1, 2, 3, 4, 5].as_mut()); - - let (recovered_header, bytes_used) = - FragmentHeader::try_from_bytes(&header_bytes).unwrap(); - assert_eq!(fragmented_header, recovered_header); - assert_eq!(UNLINKED_FRAGMENTED_HEADER_LEN, bytes_used); - } - - #[test] - fn retrieval_from_bytes_fail_for_insufficient_number_of_bytes_provided() { - let fragmented_header = - FragmentHeader::try_new_fragmented(12345, 10, 5, None, None).unwrap(); - - let header_bytes = fragmented_header.to_bytes(); - let header_bytes = &header_bytes[..header_bytes.len() - 1]; - assert!(FragmentHeader::try_from_bytes(&header_bytes).is_err()) - } - - #[test] - fn retrieval_from_bytes_fail_for_invalid_fragmentation_flag() { - let fragmented_header = - FragmentHeader::try_new_fragmented(10, 10, 5, None, None).unwrap(); - - let mut header_bytes_low = fragmented_header.to_bytes(); - - // clear the fragmentation flag - header_bytes_low[0] &= !(1 << 7); - - let mut header_bytes_high = header_bytes_low.clone(); - // make sure first byte of id is non-empty (apart from the fragmentation flag) - // note for anyone reading this test in the future: choice of '3' here is arbitrary. - header_bytes_high[0] |= 1 << 3; - - // this will cause it to be parsed as 'unfragmented' header due to whole byte being set to 0 - // there isn't a really a good way of preventing that apart from adding even data overhead - assert_eq!( - FragmentHeader::new_unfragmented(), - FragmentHeader::try_from_bytes(&header_bytes_low).unwrap().0 - ); - - // however, this will have cause an error as there will be a value in the first byte - assert!(FragmentHeader::try_from_bytes(&header_bytes_high).is_err()); - } - - #[test] - fn retrieval_from_bytes_fail_for_invalid_link_flag() { - let fragmented_header = - FragmentHeader::try_new_fragmented(12345, 10, 5, None, None).unwrap(); - - let mut header_bytes = fragmented_header.to_bytes(); - // set linked flag - header_bytes[6] |= 1 << 7; - assert!(FragmentHeader::try_from_bytes(&header_bytes).is_err()); - } - - #[test] - fn creation_of_header_fails_if_current_fragment_is_higher_than_total() { - assert!(FragmentHeader::try_new_fragmented(12345, 10, 11, None, None).is_err()); - } - - #[test] - fn creation_of_header_fails_if_current_fragment_is_zero() { - assert!(FragmentHeader::try_new_fragmented(12345, 10, 0, None, None).is_err()); - } - - #[test] - fn creation_of_header_fails_if_total_fragments_is_zero() { - assert!(FragmentHeader::try_new_fragmented(12345, 0, 0, None, None).is_err()); - } - - #[test] - fn creation_of_header_fails_if_id_is_negative() { - assert!(FragmentHeader::try_new_fragmented(-10, 10, 5, None, None).is_err()); - } - - #[test] - fn fragmented_header_cannot_be_created_with_zero_id() { - assert!(FragmentHeader::try_new_fragmented(0, 10, 5, None, None).is_err()); - assert!(FragmentHeader::try_new_fragmented(12345, 10, 5, Some(0), None).is_err()); - assert!(FragmentHeader::try_new_fragmented( - 12345, - u8::max_value(), - u8::max_value(), - None, - Some(0) - ) - .is_err()); - } - - #[test] - fn retrieval_from_bytes_fail_if_current_fragment_is_higher_than_total() { - // manually create header to overwrite any constructor checks - let header = FragmentHeader { - is_fragmented: true, - id: 1234, - total_fragments: 10, - current_fragment: 11, - previous_fragments_set_id: None, - next_fragments_set_id: None, - }; - let header_bytes = header.to_bytes(); - assert!(FragmentHeader::try_from_bytes(&header_bytes).is_err()); - } - - #[test] - fn retrieval_from_bytes_fail_if_current_or_total_fragment_is_zero() { - // manually create header to overwrite any constructor checks - let header = FragmentHeader { - is_fragmented: true, - id: 1234, - total_fragments: 0, - current_fragment: 0, - previous_fragments_set_id: None, - next_fragments_set_id: None, - }; - let header_bytes = header.to_bytes(); - assert!(FragmentHeader::try_from_bytes(&header_bytes).is_err()); - } - } - - #[cfg(test)] - mod linked_fragmented_payload { - use super::*; - - #[test] - fn cannot_be_linked_to_itself() { - assert!(FragmentHeader::try_new_fragmented(12345, 10, 1, Some(12345), None).is_err()); - assert!(FragmentHeader::try_new_fragmented(12345, 10, 10, None, Some(12345)).is_err()); - } - - #[test] - fn can_only_be_pre_linked_for_first_fragment() { - assert!(FragmentHeader::try_new_fragmented(12345, 10, 1, Some(1234), None).is_ok()); - assert!(FragmentHeader::try_new_fragmented(12345, 10, 2, Some(1234), None).is_err()); - } - - #[test] - fn can_only_be_post_linked_for_last_fragment() { - assert!(FragmentHeader::try_new_fragmented(12345, 10, 10, None, Some(1234)).is_ok()); - assert!(FragmentHeader::try_new_fragmented( - 12345, - u8::max_value(), - u8::max_value(), - None, - Some(1234), - ) - .is_ok()); - assert!(FragmentHeader::try_new_fragmented(12345, 10, 2, Some(1234), None).is_err()); - } - - #[test] - fn pre_linked_can_be_converted_to_and_from_bytes_for_exact_number_of_bytes_provided() { - let fragmented_header = - FragmentHeader::try_new_fragmented(12345, 10, 1, Some(1234), None).unwrap(); - - let header_bytes = fragmented_header.to_bytes(); - let (recovered_header, bytes_used) = - FragmentHeader::try_from_bytes(&header_bytes).unwrap(); - assert_eq!(fragmented_header, recovered_header); - assert_eq!(LINKED_FRAGMENTED_HEADER_LEN, bytes_used); - } - - #[test] - fn pre_linked_can_be_converted_to_and_from_bytes_for_more_than_required_number_of_bytes() { - let fragmented_header = - FragmentHeader::try_new_fragmented(12345, 10, 1, Some(1234), None).unwrap(); - - let mut header_bytes = fragmented_header.to_bytes(); - header_bytes.append(vec![1, 2, 3, 4, 5].as_mut()); - - let (recovered_header, bytes_used) = - FragmentHeader::try_from_bytes(&header_bytes).unwrap(); - assert_eq!(fragmented_header, recovered_header); - assert_eq!(LINKED_FRAGMENTED_HEADER_LEN, bytes_used); - } - - #[test] - fn pre_linked_is_successfully_recovered_if_its_both_first_and_final_fragment() { - let fragmented_header = - FragmentHeader::try_new_fragmented(12345, 1, 1, Some(1234), None).unwrap(); - - let header_bytes = fragmented_header.to_bytes(); - let (recovered_header, bytes_used) = - FragmentHeader::try_from_bytes(&header_bytes).unwrap(); - assert_eq!(fragmented_header, recovered_header); - assert_eq!(LINKED_FRAGMENTED_HEADER_LEN, bytes_used); - } - - #[test] - fn post_linked_can_be_converted_to_and_from_bytes_for_exact_number_of_bytes_provided() { - let fragmented_header = FragmentHeader::try_new_fragmented( - 12345, - u8::max_value(), - u8::max_value(), - None, - Some(1234), - ) - .unwrap(); - - let header_bytes = fragmented_header.to_bytes(); - let (recovered_header, bytes_used) = - FragmentHeader::try_from_bytes(&header_bytes).unwrap(); - assert_eq!(fragmented_header, recovered_header); - assert_eq!(LINKED_FRAGMENTED_HEADER_LEN, bytes_used); - } - - #[test] - fn post_linked_can_be_converted_to_and_from_bytes_for_more_than_required_number_of_bytes() { - let fragmented_header = FragmentHeader::try_new_fragmented( - 12345, - u8::max_value(), - u8::max_value(), - None, - Some(1234), - ) - .unwrap(); - - let mut header_bytes = fragmented_header.to_bytes(); - header_bytes.append(vec![1, 2, 3, 4, 5].as_mut()); - - let (recovered_header, bytes_used) = - FragmentHeader::try_from_bytes(&header_bytes).unwrap(); - assert_eq!(fragmented_header, recovered_header); - assert_eq!(LINKED_FRAGMENTED_HEADER_LEN, bytes_used); - } - } -} diff --git a/common/nymsphinx/src/chunking/mod.rs b/common/nymsphinx/src/chunking/mod.rs deleted file mode 100644 index 5f0238bace..0000000000 --- a/common/nymsphinx/src/chunking/mod.rs +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// 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. - -use crate::chunking::set::split_into_sets; -use crate::packets::PacketSize; - -pub mod fragment; -pub mod reconstruction; -pub mod set; - -/// The idea behind the process of chunking is to incur as little data overhead as possible due -/// to very computationally costly sphinx encapsulation procedure. -/// -/// To achieve this, the underlying message is split into so-called "sets", which are further -/// subdivided into the base unit of "fragment" that is directly encapsulated by a Sphinx packet. -/// This allows to encapsulate messages of arbitrary length. -/// -/// Each message, regardless of its size, consists of at least a single `Set` that has at least -/// a single `Fragment`. -/// -/// Each `Fragment` can have variable, yet fully deterministic, length, -/// that depends on its position in the set as well as total number of sets. This is further -/// explained in `fragment.rs` file. -/// -/// Similarly, each `Set` can have a variable number of `Fragment`s inside. However, that -/// value is more restrictive: if it's the last set into which the message was split -/// (or implicitly the only one), it has no lower bound on the number of `Fragment`s. -/// (Apart from the restriction of containing at least a single one). If the set is located -/// somewhere in the middle, *it must be* full. Finally, regardless of its position, it must also be -/// true that it contains no more than `u8::max_value()`, i.e. 255 `Fragment`s. -/// Again, the reasoning for this is further explained in `set.rs` file. However, you might -/// also want to look at `fragment.rs` to understand the full context behind that design choice. -/// -/// Both of those concepts as well as their structures, i.e. `Set` and `Fragment` -/// are further explained in the respective files. - -#[derive(PartialEq, Debug)] -pub enum ChunkingError { - InvalidPayloadLengthError, - TooBigMessageToSplit, - MalformedHeaderError, - NoValidProvidersError, - NoValidRoutesAvailableError, - InvalidTopologyError, - TooShortFragmentData, - MalformedFragmentData, - UnexpectedFragmentCount, -} - -pub struct MessageChunker { - packet_size: PacketSize, - reply_surbs: bool, - surb_acks: bool, -} - -impl MessageChunker { - pub fn new() -> Self { - Default::default() - } - - pub fn with_reply_surbs(mut self, reply_surbs: bool) -> Self { - self.reply_surbs = reply_surbs; - self - } - - pub fn with_surb_acks(mut self, surb_acks: bool) -> Self { - self.surb_acks = surb_acks; - self - } - - pub fn with_packet_size(mut self, packet_size: PacketSize) -> Self { - self.packet_size = packet_size; - self - } - - pub fn finalize() -> Vec> { - todo!() - } - - pub fn attach_surb_acks() {} - - pub fn attach_reply_surbs() {} -} - -impl Default for MessageChunker { - fn default() -> Self { - MessageChunker { - packet_size: Default::default(), - reply_surbs: false, - surb_acks: false, - } - } -} - -/// Takes the entire message and splits it into bytes chunks that will fit into sphinx packets -/// directly. After receiving they can be combined using `reconstruction::MessageReconstructor` -/// to obtain the original message back. -pub fn split_and_prepare_payloads(message: &[u8]) -> Vec> { - let fragmented_messages = split_into_sets(message); - fragmented_messages - .into_iter() - .flat_map(|fragment_set| fragment_set.into_iter()) - .map(|fragment| fragment.into_bytes()) - .collect() -} diff --git a/common/nymsphinx/src/lib.rs b/common/nymsphinx/src/lib.rs index e3ec920bb9..b660507af9 100644 --- a/common/nymsphinx/src/lib.rs +++ b/common/nymsphinx/src/lib.rs @@ -12,31 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod addressing; -pub mod chunking; -pub mod framing; -pub mod packets; pub mod utils; -// Future consideration: currently in a lot of places, the payloads have randomised content -// which is not a perfect testing strategy as it might not detect some edge cases I never would -// have assumed could be possible. A better approach would be to research some Fuzz testing -// library like: https://github.com/rust-fuzz/afl.rs and use that instead for the inputs. - -// perhaps it might be useful down the line for interaction testing between client,mixes,etc? - -// re-exporting types and constants available in sphinx -pub use sphinx::{ - constants::{ - DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, MAX_PATH_LENGTH, NODE_ADDRESS_LENGTH, - }, - header::{delays, delays::Delay, ProcessedHeader, SphinxHeader}, - payload::Payload, - route::{Destination, DestinationAddressBytes, Node, NodeAddressBytes, SURBIdentifier}, - Error, ProcessedPacket, Result, SphinxPacket, PACKET_SIZE, -}; - -// re-exporting this separately to remember to put special attention to below -// modules/types/constants when refactoring sphinx crate itself -// TODO: replace with sphinx::PublicKey once merged -pub use sphinx::key; +// re-export sub-crates +pub use nymsphinx_acknowledgements as acknowledgements; +pub use nymsphinx_addressing as addressing; +pub use nymsphinx_chunking as chunking; +pub use nymsphinx_cover as cover; +pub use nymsphinx_framing as framing; +pub use nymsphinx_params as params; +pub use nymsphinx_types::*; diff --git a/common/nymsphinx/src/utils/encapsulation.rs b/common/nymsphinx/src/utils/encapsulation.rs deleted file mode 100644 index 7b229a7e7e..0000000000 --- a/common/nymsphinx/src/utils/encapsulation.rs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// 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. - -use crate::addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError}; -use crate::{delays, Destination, DestinationAddressBytes, SphinxPacket}; -use crate::{Error as SphinxError, Node as SphinxNode}; -use std::convert::TryFrom; -use std::net::SocketAddr; -use std::time; - -pub const LOOP_COVER_MESSAGE_PAYLOAD: &[u8] = b"The cake is a lie!"; - -#[derive(Debug)] -pub enum SphinxPacketEncapsulationError { - NoValidProvidersError, - InvalidTopologyError, - SphinxError(SphinxError), - InvalidFirstMixAddress, -} - -impl From for SphinxPacketEncapsulationError { - fn from(err: SphinxError) -> Self { - SphinxPacketEncapsulationError::SphinxError(err) - } -} - -impl From for SphinxPacketEncapsulationError { - fn from(_: NymNodeRoutingAddressError) -> Self { - use SphinxPacketEncapsulationError::*; - InvalidFirstMixAddress - } -} - -pub fn loop_cover_message_route( - our_address: DestinationAddressBytes, - route: Vec, - average_delay: time::Duration, -) -> Result<(SocketAddr, SphinxPacket), SphinxPacketEncapsulationError> { - encapsulate_message_route( - our_address, - LOOP_COVER_MESSAGE_PAYLOAD.to_vec(), - route, - average_delay, - ) -} - -pub fn encapsulate_message_route( - destination: DestinationAddressBytes, - message: Vec, - route: Vec, - average_delay: time::Duration, -) -> Result<(SocketAddr, SphinxPacket), SphinxPacketEncapsulationError> { - // in our design we don't care about SURB_ID - let destination = Destination::new(destination, Default::default()); - - let delays = delays::generate_from_average_duration(route.len(), average_delay); - - // build the packet - let packet = SphinxPacket::new(message, &route[..], &destination, &delays, None)?; - - let first_node_address = - NymNodeRoutingAddress::try_from(route.first().unwrap().address.clone())?; - - Ok((first_node_address.into(), packet)) -} diff --git a/common/nymsphinx/src/utils/mod.rs b/common/nymsphinx/src/utils/mod.rs index 3235d6d540..b3baf8710b 100644 --- a/common/nymsphinx/src/utils/mod.rs +++ b/common/nymsphinx/src/utils/mod.rs @@ -1,2 +1,33 @@ -pub mod encapsulation; -pub mod poisson; +// Copyright 2020 Nym Technologies SA +// +// 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. + +use rand::Rng; +use rand_distr::{Distribution, Exp}; +use std::time; + +// TODO: ask @AP why we are actually using Distribution::Exp(1/L) rather than just +// Distribution::Poisson(L) directly? + +// TODO: should we put an extra trait bound on this to require `CryptoRng`? Could there be any attacks +// because of weak rng used? +pub fn sample_poisson_duration( + rng: &mut R, + average_duration: time::Duration, +) -> time::Duration { + // this is our internal code used by our traffic streams + // the error is only thrown if average delay is less than 0, which will never happen + // so call to unwrap is perfectly safe here + let exp = Exp::new(1.0 / average_duration.as_nanos() as f64).unwrap(); + time::Duration::from_nanos(exp.sample(rng).round() as u64) +} diff --git a/common/nymsphinx/types/Cargo.toml b/common/nymsphinx/types/Cargo.toml new file mode 100644 index 0000000000..4ac06239d3 --- /dev/null +++ b/common/nymsphinx/types/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "nymsphinx-types" +version = "0.1.0" +authors = ["Jedrzej Stuczynski "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +sphinx = { git = "https://github.com/nymtech/sphinx", rev="cd9f09b85de33c60030ba6d7fae613c42cbe1faf" } +#sphinx = { path = "../../../../sphinx"} \ No newline at end of file diff --git a/common/nymsphinx/types/src/lib.rs b/common/nymsphinx/types/src/lib.rs new file mode 100644 index 0000000000..8d16ba194c --- /dev/null +++ b/common/nymsphinx/types/src/lib.rs @@ -0,0 +1,27 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +// re-exporting types and constants available in sphinx +pub use sphinx::{ + constants::{ + self, DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, MAX_PATH_LENGTH, NODE_ADDRESS_LENGTH, + }, + crypto::{public_key_from_bytes, PublicKey, SecretKey}, + header::{self, delays, delays::Delay, ProcessedHeader, SphinxHeader, HEADER_SIZE}, + packet::builder::{self, DEFAULT_PAYLOAD_SIZE}, + payload::{Payload, PAYLOAD_OVERHEAD_SIZE}, + route::{Destination, DestinationAddressBytes, Node, NodeAddressBytes, SURBIdentifier}, + surb::{SURBMaterial, SURB}, + Error, ProcessedPacket, Result, SphinxPacket, +}; diff --git a/common/topology/Cargo.toml b/common/topology/Cargo.toml index 46186e1714..17a568b979 100644 --- a/common/topology/Cargo.toml +++ b/common/topology/Cargo.toml @@ -16,5 +16,6 @@ rand = "0.7.2" serde = { version = "1.0.104", features = ["derive"] } ## internal -nymsphinx = {path = "../nymsphinx"} +nymsphinx-addressing = {path = "../nymsphinx/addressing"} +nymsphinx-types = {path = "../nymsphinx/types"} version-checker = {path = "../version-checker" } diff --git a/common/topology/src/gateway.rs b/common/topology/src/gateway.rs index c67b95ffe1..d514b7ae62 100644 --- a/common/topology/src/gateway.rs +++ b/common/topology/src/gateway.rs @@ -13,8 +13,8 @@ // limitations under the License. use crate::filter; -use nymsphinx::addressing::nodes::NymNodeRoutingAddress; -use nymsphinx::Node as SphinxNode; +use nymsphinx_addressing::nodes::NymNodeRoutingAddress; +use nymsphinx_types::Node as SphinxNode; use std::convert::TryInto; use std::net::SocketAddr; @@ -61,7 +61,7 @@ impl Into for Node { .try_into() .unwrap(); let key_bytes = self.get_pub_key_bytes(); - let key = nymsphinx::key::new(key_bytes); + let key = nymsphinx_types::public_key_from_bytes(key_bytes); SphinxNode::new(node_address_bytes, key) } diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index e9231c1496..c519572b1f 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -15,7 +15,7 @@ use crate::filter::VersionFilterable; use futures::future::BoxFuture; use itertools::Itertools; -use nymsphinx::Node as SphinxNode; +use nymsphinx_types::{Node as SphinxNode, NodeAddressBytes}; use rand::seq::IteratorRandom; use std::cmp::max; use std::collections::HashMap; @@ -48,7 +48,7 @@ pub trait NymTopology: Sized + std::fmt::Debug + Send + Sync + Clone { let mut highest_layer = 0; for mix in self.mix_nodes() { // we need to have extra space for provider - if mix.layer > nymsphinx::MAX_PATH_LENGTH as u64 { + if mix.layer > nymsphinx_types::MAX_PATH_LENGTH as u64 { return Err(NymTopologyError::InvalidMixLayerError); } highest_layer = max(highest_layer, mix.layer); @@ -90,15 +90,31 @@ pub trait NymTopology: Sized + std::fmt::Debug + Send + Sync + Clone { Ok(route) } - // Sets up a route to a specific gateway + fn gateway_exists(&self, gateway_address: &NodeAddressBytes) -> bool { + let b58_address = gateway_address.to_base58_string(); + self.gateways() + .iter() + .find(|&gateway| gateway.pub_key == b58_address) + .is_some() + } + fn random_route_to_gateway( &self, - gateway_node: SphinxNode, + gateway_address: &NodeAddressBytes, ) -> Result, NymTopologyError> { + let b58_address = gateway_address.to_base58_string(); + + let gateway = self + .gateways() + .iter() + .find(|&gateway| gateway.pub_key == b58_address) + .ok_or_else(|| NymTopologyError::NonExistentGatewayError)? + .clone(); + Ok(self .random_mix_route()? .into_iter() - .chain(std::iter::once(gateway_node)) + .chain(std::iter::once(gateway.into())) .collect()) } @@ -159,4 +175,5 @@ pub trait NymTopology: Sized + std::fmt::Debug + Send + Sync + Clone { pub enum NymTopologyError { InvalidMixLayerError, MissingLayerError(Vec), + NonExistentGatewayError, } diff --git a/common/topology/src/mix.rs b/common/topology/src/mix.rs index 68241ee1a5..588938c69b 100644 --- a/common/topology/src/mix.rs +++ b/common/topology/src/mix.rs @@ -13,8 +13,8 @@ // limitations under the License. use crate::filter; -use nymsphinx::addressing::nodes::NymNodeRoutingAddress; -use nymsphinx::Node as SphinxNode; +use nymsphinx_addressing::nodes::NymNodeRoutingAddress; +use nymsphinx_types::Node as SphinxNode; use std::convert::TryInto; use std::net::SocketAddr; @@ -46,7 +46,7 @@ impl Into for Node { fn into(self) -> SphinxNode { let node_address_bytes = NymNodeRoutingAddress::from(self.host).try_into().unwrap(); let key_bytes = self.get_pub_key_bytes(); - let key = nymsphinx::key::new(key_bytes); + let key = nymsphinx_types::public_key_from_bytes(key_bytes); SphinxNode::new(node_address_bytes, key) } diff --git a/common/topology/src/provider.rs b/common/topology/src/provider.rs index c869a88637..de1a722a97 100644 --- a/common/topology/src/provider.rs +++ b/common/topology/src/provider.rs @@ -13,8 +13,8 @@ // limitations under the License. use crate::filter; -use nymsphinx::addressing::nodes::NymNodeRoutingAddress; -use nymsphinx::Node as SphinxNode; +use nymsphinx_addressing::nodes::NymNodeRoutingAddress; +use nymsphinx_types::Node as SphinxNode; use std::convert::TryInto; use std::net::SocketAddr; @@ -54,7 +54,7 @@ impl Into for Node { .try_into() .unwrap(); let key_bytes = self.get_pub_key_bytes(); - let key = nymsphinx::key::new(key_bytes); + let key = nymsphinx_types::public_key_from_bytes(key_bytes); SphinxNode::new(node_address_bytes, key) } diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index a5c93d8198..8454722fd5 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -1,7 +1,7 @@ [package] build = "build.rs" name = "nym-gateway" -version = "0.7.0" +version = "0.8.0-dev" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] edition = "2018" diff --git a/gateway/gateway-requests/src/types.rs b/gateway/gateway-requests/src/types.rs index de7672aff2..1a5a221743 100644 --- a/gateway/gateway-requests/src/types.rs +++ b/gateway/gateway-requests/src/types.rs @@ -13,8 +13,8 @@ // limitations under the License. use crate::auth_token::AuthToken; -use crate::types::BinaryRequest::ForwardSphinx; use nymsphinx::addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError}; +use nymsphinx::params::packet_sizes::PacketSize; use nymsphinx::{DestinationAddressBytes, SphinxPacket}; use serde::{Deserialize, Serialize}; use std::{ @@ -27,7 +27,7 @@ use tokio_tungstenite::tungstenite::protocol::Message; #[derive(Debug)] pub enum GatewayRequestsError { IncorrectlyEncodedAddress, - RequestOfInvalidSize(usize, usize), + RequestOfInvalidSize(usize), MalformedSphinxPacket, } @@ -38,10 +38,10 @@ impl fmt::Display for GatewayRequestsError { use GatewayRequestsError::*; match self { IncorrectlyEncodedAddress => write!(f, "address field was incorrectly encoded"), - RequestOfInvalidSize(actual, expected) => write!( + RequestOfInvalidSize(actual) => write!( f, - "received request had invalid size. (actual: {}, expected: {})", - actual, expected + "received request had invalid size. (actual: {}, but expected one of: {} (ACK), {} (REGULAR), {} (EXTENDED))", + actual, PacketSize::ACKPacket.size(), PacketSize::RegularPacket.size(), PacketSize::ExtendedPacket.size() ), MalformedSphinxPacket => write!(f, "received sphinx packet was malformed"), } @@ -164,18 +164,18 @@ impl BinaryRequest { let address = NymNodeRoutingAddress::try_from_bytes(&raw_req)?; let addr_offset = address.bytes_min_len(); - if raw_req[addr_offset..].len() != nymsphinx::PACKET_SIZE { - Err(GatewayRequestsError::RequestOfInvalidSize( - raw_req[addr_offset..].len(), - nymsphinx::PACKET_SIZE, - )) + let packet_size = raw_req[addr_offset..].len(); + if let Err(_) = PacketSize::get_type(packet_size) { + // TODO: should this allow AckPacket sizes? + + Err(GatewayRequestsError::RequestOfInvalidSize(packet_size)) } else { let sphinx_packet = match SphinxPacket::from_bytes(&raw_req[addr_offset..]) { Ok(packet) => packet, Err(_) => return Err(GatewayRequestsError::MalformedSphinxPacket), }; - Ok(ForwardSphinx { + Ok(BinaryRequest::ForwardSphinx { address: address.into(), sphinx_packet, }) @@ -201,6 +201,7 @@ impl BinaryRequest { } } + // TODO: this will be encrypted, etc. pub fn new_forward_request(address: SocketAddr, sphinx_packet: SphinxPacket) -> BinaryRequest { BinaryRequest::ForwardSphinx { address, diff --git a/gateway/src/node/mixnet_handling/receiver/connection_handler.rs b/gateway/src/node/mixnet_handling/receiver/connection_handler.rs index 51f23d93db..3389e774ad 100644 --- a/gateway/src/node/mixnet_handling/receiver/connection_handler.rs +++ b/gateway/src/node/mixnet_handling/receiver/connection_handler.rs @@ -61,6 +61,7 @@ where while let Some(sphinx_packet) = self.framed_connection.next().await { match sphinx_packet { Ok(sphinx_packet) => { + // rather important TODO: // we *really* need a worker pool here, because if we receive too many packets, // we will spawn too many tasks and starve CPU due to context switching. // (because presumably tokio has some concept of context switching in its diff --git a/gateway/src/node/mixnet_handling/receiver/packet_processing.rs b/gateway/src/node/mixnet_handling/receiver/packet_processing.rs index 8142724aa5..a6345b5a99 100644 --- a/gateway/src/node/mixnet_handling/receiver/packet_processing.rs +++ b/gateway/src/node/mixnet_handling/receiver/packet_processing.rs @@ -16,15 +16,17 @@ use crate::node::client_handling::clients_handler::{ ClientsHandlerRequest, ClientsHandlerRequestSender, ClientsHandlerResponse, }; use crate::node::client_handling::websocket::message_receiver::MixMessageSender; +use crate::node::mixnet_handling::sender::OutboundMixMessageSender; use crate::node::storage::inboxes::{ClientStorage, StoreData}; use crypto::encryption; use futures::channel::oneshot; use futures::lock::Mutex; use log::*; -use nymsphinx::{ - utils::encapsulation::LOOP_COVER_MESSAGE_PAYLOAD, DestinationAddressBytes, - Error as SphinxError, ProcessedPacket, SphinxPacket, -}; +use nymsphinx::acknowledgements::surb_ack::{SURBAck, SURBAckRecoveryError}; +use nymsphinx::cover::LOOP_COVER_MESSAGE_PAYLOAD; +use nymsphinx::params::packet_sizes::PacketSize; +use nymsphinx::{DestinationAddressBytes, Error as SphinxError, ProcessedPacket, SphinxPacket}; + use std::collections::HashMap; use std::io; use std::ops::Deref; @@ -34,9 +36,10 @@ use std::sync::Arc; pub enum MixProcessingError { ReceivedForwardHopError, NonMatchingRecipient, - InvalidPayload, + UnsupportedSphinxPacketSize(usize), SphinxProcessingError(SphinxError), - IOError(String), + IncorrectlyFormattedSURBAck(SURBAckRecoveryError), + IOError(io::Error), } impl From for MixProcessingError { @@ -52,7 +55,15 @@ impl From for MixProcessingError { fn from(e: io::Error) -> Self { use MixProcessingError::*; - IOError(e.to_string()) + IOError(e) + } +} + +impl From for MixProcessingError { + fn from(err: SURBAckRecoveryError) -> Self { + use MixProcessingError::*; + + IncorrectlyFormattedSURBAck(err) } } @@ -65,6 +76,7 @@ pub struct PacketProcessor { available_socket_senders_cache: Arc>>, client_store: ClientStorage, clients_handler_sender: ClientsHandlerRequestSender, + ack_sender: OutboundMixMessageSender, } impl PacketProcessor { @@ -72,12 +84,14 @@ impl PacketProcessor { secret_key: Arc, clients_handler_sender: ClientsHandlerRequestSender, client_store: ClientStorage, + ack_sender: OutboundMixMessageSender, ) -> Self { PacketProcessor { available_socket_senders_cache: Arc::new(Mutex::new(HashMap::new())), clients_handler_sender, client_store, secret_key, + ack_sender, } } @@ -85,10 +99,16 @@ impl PacketProcessor { &self, sender_channel: Option, message: Vec, - ) -> bool { + ) -> Result<(), Vec> { match sender_channel { - None => false, - Some(sender_channel) => sender_channel.unbounded_send(vec![message]).is_ok(), + None => Err(message), + Some(sender_channel) => { + sender_channel + .unbounded_send(vec![message]) + // right now it's a "simpler" case here as we're only ever sending 1 message + // at the time, but the channel itself could accept arbitrary many messages at once + .map_err(|try_send_err| try_send_err.into_inner().pop().unwrap()) + } } } @@ -140,7 +160,7 @@ impl PacketProcessor { client_address: DestinationAddressBytes, message: Vec, ) -> io::Result<()> { - trace!( + debug!( "Storing received packet for {:?} on the disk...", client_address.to_base58_string() ); @@ -148,6 +168,9 @@ impl PacketProcessor { // not cause our sfw-provider to run out of disk space too quickly. // Eventually this is going to get removed and be replaced by a quota system described in: // https://github.com/nymtech/nym/issues/137 + + // JS: I think this would never get called anyway, because if loop cover messages are sent + // it means client is online and hence all his messages should be pushed directly to him? if message == LOOP_COVER_MESSAGE_PAYLOAD { debug!("Received a loop cover message - not going to store it"); return Ok(()); @@ -163,15 +186,15 @@ impl PacketProcessor { ) -> Result<(DestinationAddressBytes, Vec), MixProcessingError> { match packet.process(self.secret_key.deref().inner()) { Ok(ProcessedPacket::ProcessedPacketForwardHop(_, _, _)) => { - warn!("Received a forward hop message - those are not implemented for providers"); + warn!("Received a forward hop message - those are not implemented for gateways"); Err(MixProcessingError::ReceivedForwardHopError) } Ok(ProcessedPacket::ProcessedPacketFinalHop(client_address, _surb_id, payload)) => { // in our current design, we do not care about the 'surb_id' in the header // as it will always be empty anyway - let (payload_destination, message) = payload - .try_recover_destination_and_plaintext() - .ok_or_else(|| MixProcessingError::InvalidPayload)?; + let (payload_destination, message) = + payload.try_recover_destination_and_plaintext()?; + // TODO: @AP, does that check still make sense? if client_address != payload_destination { return Err(MixProcessingError::NonMatchingRecipient); } @@ -184,28 +207,100 @@ impl PacketProcessor { } } + fn split_plaintext_into_ack_and_message( + &self, + mut extracted_plaintext: Vec, + ) -> (Vec, Vec) { + if extracted_plaintext.len() < SURBAck::len() { + // TODO: + // TODO: + // this is mostly for dev purposes to see if we receive something we did not mean to send + // but in an actual system, what should we do? abandon the whole packet? + // store client's data regardless? + // I'm going to leave this question open for until I've implemented reply SURBs + // as they will change the communication between client and gateway so this + // if statement might no longer make any sense + panic!("received packet without an ack"); + } + + let plaintext = extracted_plaintext.split_off(SURBAck::len()); + let ack_data = extracted_plaintext; + (ack_data, plaintext) + } + pub(crate) async fn process_sphinx_packet( &mut self, sphinx_packet: SphinxPacket, ) -> Result<(), MixProcessingError> { + // see if what we got now is an ack or normal packet + let packet_len = sphinx_packet.len(); + // TODO: micro-optimisations: + // 1. don't even try to unwrap the packet if it's not one of `PacketSize` variants + // 2. if client_address doesn't exist at this gateway, don't do any other work here + // (as stupid as this sounds, there's currently no easy way of directly checking if the + // client exists here) let (client_address, plaintext) = self.unwrap_sphinx_packet(sphinx_packet)?; + let (routable_ack, plaintext) = match packet_len { + n if n == PacketSize::ACKPacket.size() => { + trace!("received an ack packet!"); + (None, plaintext) + } + n if n == PacketSize::RegularPacket.size() + || n == PacketSize::ExtendedPacket.size() => + { + trace!("received a normal packet!"); + let (ack_data, plaintext) = self.split_plaintext_into_ack_and_message(plaintext); + let (ack_first_hop, ack_packet) = SURBAck::try_recover_first_hop_packet(&ack_data)?; + (Some((ack_first_hop, ack_packet)), plaintext) + } + n => return Err(MixProcessingError::UnsupportedSphinxPacketSize(n)), + }; let client_sender = self .try_to_obtain_client_ws_message_sender(client_address.clone()) .await; - // TODO: think of a way to prevent having to clone the plaintext here, perhaps make channels use references? - // this will, again, take slightly more time, so it's an issue for later - if !self.try_push_message_to_client(client_sender, plaintext.clone()) { - Ok(self - .store_processed_packet_payload(client_address, plaintext) - .await?) + if let Err(unsent_plaintext) = self.try_push_message_to_client(client_sender, plaintext) { + // means we failed to push message directly to the client (it might be offline) + // but we don't want to store an ack message for him - he won't be able to decode + // it anyway. + // TODO: after keybase discussion we *might* want to store them after all + if routable_ack.is_none() { + trace!("Received an ack for offline client - won't try storing it"); + return Ok(()); + } + + if let Err(io_err) = self + .store_processed_packet_payload(client_address.clone(), unsent_plaintext) + .await + { + return Err(io_err)?; + } else { + trace!( + "Managed to store packet for {:?} on the disk", + client_address.to_base58_string() + ); + } } else { trace!( "Managed to push received packet for {:?} to websocket connection!", client_address.to_base58_string() ); - Ok(()) } + + // if we managed to either push message directly to the [online] client or store it at + // it's inbox, it means that it must exist at this gateway, hence we can send the + // received ack back into the network + if let Some((ack_first_hop, ack_packet)) = routable_ack { + trace!( + "Sending an ack back into the network. The first hop is {:?}", + ack_first_hop + ); + self.ack_sender + .unbounded_send((ack_first_hop.into(), ack_packet)) + .unwrap(); + } + + Ok(()) } } diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index 87e289a90d..fc1bfe8c85 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -66,13 +66,18 @@ impl Gateway { } } - fn start_mix_socket_listener(&self, clients_handler_sender: ClientsHandlerRequestSender) { + fn start_mix_socket_listener( + &self, + clients_handler_sender: ClientsHandlerRequestSender, + ack_sender: OutboundMixMessageSender, + ) { info!("Starting mix socket listener..."); let packet_processor = mixnet_handling::PacketProcessor::new( Arc::new(self.sphinx_keypair.private_key().clone()), clients_handler_sender, self.client_inbox_storage.clone(), + ack_sender, ); mixnet_handling::Listener::new(self.config.get_mix_listening_address()) @@ -180,7 +185,7 @@ impl Gateway { let mix_forwarding_channel = self.start_packet_forwarder(); let clients_handler_sender = self.start_clients_handler(); - self.start_mix_socket_listener(clients_handler_sender.clone()); + self.start_mix_socket_listener(clients_handler_sender.clone(), mix_forwarding_channel.clone()); self.start_client_websocket_listener(mix_forwarding_channel, clients_handler_sender); self.start_presence_notifier(); diff --git a/gateway/src/node/storage/inboxes.rs b/gateway/src/node/storage/inboxes.rs index bd165e5a81..9c835c133b 100644 --- a/gateway/src/node/storage/inboxes.rs +++ b/gateway/src/node/storage/inboxes.rs @@ -118,9 +118,10 @@ impl ClientStorage { let full_store_path = full_store_dir.join(Self::generate_random_file_name( inner_data.filename_length as usize, )); - debug!( + trace!( "going to store: {:?} in file: {:?}", - store_data.message, full_store_path + store_data.message, + full_store_path ); let mut file = File::create(full_store_path).await?; diff --git a/mixnode/Cargo.toml b/mixnode/Cargo.toml index cec01d1bfb..28b811eecc 100644 --- a/mixnode/Cargo.toml +++ b/mixnode/Cargo.toml @@ -1,7 +1,7 @@ [package] build = "build.rs" name = "nym-mixnode" -version = "0.7.0" +version = "0.8.0-dev" authors = ["Dave Hrycyszyn ", "Jędrzej Stuczyński "] edition = "2018" diff --git a/validator/Cargo.toml b/validator/Cargo.toml index 359d21c809..0472a694e9 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -1,7 +1,7 @@ [package] build = "build.rs" name = "nym-validator" -version = "0.7.0" +version = "0.8.0-dev" authors = ["Dave Hrycyszyn ", "Jedrzej Stuczynski "] edition = "2018" From fcfe463efa4a220fb63262a700f3b565302de855 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 16 Jun 2020 12:12:19 +0100 Subject: [PATCH 05/76] Feature/wasm topology duplication (#265) * Removed constructor from NymTopology trait + split directory_client * Removed bunch of duplicate wasm topology code * Post-merge changes --- Cargo.lock | 12 +- Cargo.toml | 1 + clients/native/src/client/mod.rs | 10 +- clients/native/src/client/topology_control.rs | 31 ++-- clients/native/src/commands/init.rs | 8 +- clients/webassembly/Cargo.toml | 1 + clients/webassembly/src/lib.rs | 28 ++- clients/webassembly/src/models/keys.rs | 14 ++ clients/webassembly/src/models/mod.rs | 173 +----------------- clients/webassembly/src/models/topology.rs | 90 +++++++++ .../client-libs/directory-client/Cargo.toml | 4 +- .../directory-client/models/Cargo.toml | 12 ++ .../directory-client/models/src/lib.rs | 16 ++ .../{ => models}/src/metrics.rs | 0 .../models/src/presence}/coconodes.rs | 0 .../models/src/presence}/gateways.rs | 0 .../models/src/presence}/mixnodes.rs | 0 .../{ => models}/src/presence/mod.rs | 0 .../models/src/presence}/providers.rs | 0 .../{ => models}/src/presence/topology.rs | 22 +-- .../client-libs/directory-client/src/lib.rs | 14 +- .../src/presence/coconodes.rs | 50 ----- .../directory-client/src/presence/gateways.rs | 86 --------- .../directory-client/src/presence/mixnodes.rs | 66 ------- .../src/presence/providers.rs | 86 --------- common/topology/Cargo.toml | 1 - common/topology/src/lib.rs | 5 - mixnode/src/node/mod.rs | 10 +- 28 files changed, 212 insertions(+), 528 deletions(-) create mode 100644 clients/webassembly/src/models/topology.rs create mode 100644 common/client-libs/directory-client/models/Cargo.toml create mode 100644 common/client-libs/directory-client/models/src/lib.rs rename common/client-libs/directory-client/{ => models}/src/metrics.rs (100%) rename {clients/webassembly/src/models => common/client-libs/directory-client/models/src/presence}/coconodes.rs (100%) rename {clients/webassembly/src/models => common/client-libs/directory-client/models/src/presence}/gateways.rs (100%) rename {clients/webassembly/src/models => common/client-libs/directory-client/models/src/presence}/mixnodes.rs (100%) rename common/client-libs/directory-client/{ => models}/src/presence/mod.rs (100%) rename {clients/webassembly/src/models => common/client-libs/directory-client/models/src/presence}/providers.rs (100%) rename common/client-libs/directory-client/{ => models}/src/presence/topology.rs (83%) delete mode 100644 common/client-libs/directory-client/src/presence/coconodes.rs delete mode 100644 common/client-libs/directory-client/src/presence/gateways.rs delete mode 100644 common/client-libs/directory-client/src/presence/mixnodes.rs delete mode 100644 common/client-libs/directory-client/src/presence/providers.rs diff --git a/Cargo.lock b/Cargo.lock index c004f43b3d..4675662729 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -634,6 +634,7 @@ dependencies = [ name = "directory-client" version = "0.1.0" dependencies = [ + "directory-client-models", "futures 0.3.4", "log 0.4.8", "mockito", @@ -641,6 +642,13 @@ dependencies = [ "reqwest 0.10.4", "serde", "tokio 0.2.16", +] + +[[package]] +name = "directory-client-models" +version = "0.1.0" +dependencies = [ + "serde", "topology", ] @@ -1761,10 +1769,11 @@ dependencies = [ [[package]] name = "nym-client-wasm" -version = "0.7.4" +version = "0.7.5" dependencies = [ "console_error_panic_hook", "crypto", + "directory-client-models", "log 0.4.8", "nymsphinx", "rand 0.7.3", @@ -3271,7 +3280,6 @@ name = "topology" version = "0.1.0" dependencies = [ "bs58", - "futures 0.3.4", "itertools", "log 0.4.8", "nymsphinx-addressing", diff --git a/Cargo.toml b/Cargo.toml index 3bc05a60bc..9821863fe0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "clients/native", "clients/webassembly", "common/client-libs/directory-client", + "common/client-libs/directory-client/models", "common/client-libs/gateway-client", "common/client-libs/mixnet-client", "common/client-libs/validator-client", diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 42d400887a..27344223fa 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -25,7 +25,6 @@ use crate::client::topology_control::{ use crate::config::{Config, SocketType}; use crate::websocket; use crypto::identity::MixIdentityKeyPair; -use directory_client::presence; use futures::channel::mpsc; use gateway_client::{ AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver, @@ -221,16 +220,16 @@ impl NymClient { // future responsible for periodically polling directory server and updating // the current global view of topology - fn start_topology_refresher( + fn start_topology_refresher( &mut self, - topology_accessor: TopologyAccessor, + topology_accessor: TopologyAccessor, ) { let topology_refresher_config = TopologyRefresherConfig::new( self.config.get_directory_server(), self.config.get_topology_refresh_rate(), ); let mut topology_refresher = - TopologyRefresher::new(topology_refresher_config, topology_accessor); + TopologyRefresher::new_directory_client(topology_refresher_config, topology_accessor); // before returning, block entire runtime to refresh the current network view so that any // components depending on topology would see a non-empty view info!( @@ -354,9 +353,8 @@ impl NymClient { // channels responsible for controlling ack messages let (ack_sender, ack_receiver) = mpsc::unbounded(); + let shared_topology_accessor = TopologyAccessor::::new(); - // TODO: when we switch to our graph topology, we need to remember to change 'presence::Topology' type - let shared_topology_accessor = TopologyAccessor::::new(); // the components are started in very specific order. Unless you know what you are doing, // do not change that. self.start_topology_refresher(shared_topology_accessor.clone()); diff --git a/clients/native/src/client/topology_control.rs b/clients/native/src/client/topology_control.rs index 740ffe34ec..126a2bdea0 100644 --- a/clients/native/src/client/topology_control.rs +++ b/clients/native/src/client/topology_control.rs @@ -13,6 +13,7 @@ // limitations under the License. use crate::built_info; +use directory_client::DirectoryClient; use log::*; use nymsphinx::addressing::clients::Recipient; use std::ops::Deref; @@ -167,34 +168,40 @@ impl TopologyRefresherConfig { } pub(crate) struct TopologyRefresher { - directory_server: String, + directory_client: directory_client::Client, topology_accessor: TopologyAccessor, refresh_rate: Duration, } -impl TopologyRefresher { - pub(crate) fn new( +// TODO: consider (or maybe not) restoring generic TopologyRefresher +impl TopologyRefresher { + pub(crate) fn new_directory_client( cfg: TopologyRefresherConfig, - topology_accessor: TopologyAccessor, + topology_accessor: TopologyAccessor, ) -> Self { + let directory_client_config = directory_client::Config::new(cfg.directory_server); + let directory_client = directory_client::Client::new(directory_client_config); + TopologyRefresher { - directory_server: cfg.directory_server, + directory_client, topology_accessor, refresh_rate: cfg.refresh_rate, } } - async fn get_current_compatible_topology(&self) -> T { - // note: this call makes it necessary that `T::new()`does *not* have 'static lifetime - let full_topology = T::new(self.directory_server.clone()).await; - // just filter by version and assume the validators will remove all bad behaving - // nodes with the staking - full_topology.filter_system_version(built_info::PKG_VERSION) + async fn get_current_compatible_topology(&self) -> Option { + match self.directory_client.get_topology().await { + Err(err) => { + error!("failed to get network topology! - {:?}", err); + None + } + Ok(topology) => Some(topology.filter_system_version(built_info::PKG_VERSION)), + } } pub(crate) async fn refresh(&mut self) { trace!("Refreshing the topology"); - let new_topology = Some(self.get_current_compatible_topology().await); + let new_topology = self.get_current_compatible_topology().await; self.topology_accessor .update_global_topology(new_topology) diff --git a/clients/native/src/commands/init.rs b/clients/native/src/commands/init.rs index 28ecdd5255..006cefb397 100644 --- a/clients/native/src/commands/init.rs +++ b/clients/native/src/commands/init.rs @@ -18,7 +18,7 @@ use crate::config::persistence::pathfinder::ClientPathfinder; use clap::{App, Arg, ArgMatches}; use config::NymConfig; use crypto::identity::MixIdentityKeyPair; -use directory_client::presence::Topology; +use directory_client::DirectoryClient; use gateway_client::GatewayClient; use gateway_requests::AuthToken; use nymsphinx::DestinationAddressBytes; @@ -90,8 +90,10 @@ async fn choose_gateway( directory_server: String, our_address: DestinationAddressBytes, ) -> (String, AuthToken) { - // TODO: once we change to graph topology this here will need to be updated! - let topology = Topology::new(directory_server.clone()).await; + let directory_client_config = directory_client::Config::new(directory_server.clone()); + let directory_client = directory_client::Client::new(directory_client_config); + let topology = directory_client.get_topology().await.unwrap(); + let version_filtered_topology = topology.filter_system_version(built_info::PKG_VERSION); // don't care about health of the networks as mixes can go up and down any time, // but DO care about gateways diff --git a/clients/webassembly/Cargo.toml b/clients/webassembly/Cargo.toml index 596ea4329c..a630dbe1e6 100644 --- a/clients/webassembly/Cargo.toml +++ b/clients/webassembly/Cargo.toml @@ -26,6 +26,7 @@ rand = "0.7.2" crypto = { path = "../../common/crypto" } nymsphinx = { path = "../../common/nymsphinx" } topology = { path = "../../common/topology" } +directory-client-models = { path = "../../common/client-libs/directory-client/models" } # The `console_error_panic_hook` crate provides better debugging of panics by # logging them with `console.error`. This is great for development, but requires diff --git a/clients/webassembly/src/lib.rs b/clients/webassembly/src/lib.rs index f20973d2cc..9466912cf9 100644 --- a/clients/webassembly/src/lib.rs +++ b/clients/webassembly/src/lib.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. use crypto::identity::MixIdentityPublicKey; -use models::Topology; +use models::topology::Topology; use nymsphinx::addressing::nodes::NymNodeRoutingAddress; use nymsphinx::Node as SphinxNode; use nymsphinx::{delays, Destination, NodeAddressBytes, SphinxPacket}; @@ -28,6 +28,7 @@ mod utils; pub use models::keys::keygen; use nymsphinx::addressing::clients::Recipient; +use topology::NymTopology; // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global // allocator. @@ -198,8 +199,8 @@ mod building_a_topology_from_json { #[test] #[should_panic] fn panics_when_there_are_no_mixnodes() { - let mut topology: Topology = serde_json::from_str(topology_fixture()).unwrap(); - topology.mix_nodes = vec![]; + let mut topology = Topology::new(topology_fixture()); + topology.set_mixnodes(vec![]); let json = serde_json::to_string(&topology).unwrap(); sphinx_route_to( &json, @@ -213,9 +214,9 @@ mod building_a_topology_from_json { #[test] #[should_panic] fn panics_when_there_are_not_enough_mixnodes() { - let mut topology: Topology = serde_json::from_str(topology_fixture()).unwrap(); - let node = topology.mix_nodes.first().unwrap().clone(); - topology.mix_nodes = vec![node]; // 1 mixnode isn't enough. Panic! + let mut topology = Topology::new(topology_fixture()); + let node = topology.get_current_raw_mixnodes().first().unwrap().clone(); + topology.set_mixnodes(vec![node]); // 1 mixnode isn't enough. Panic! let json = serde_json::to_string(&topology).unwrap(); sphinx_route_to( &json, @@ -226,6 +227,7 @@ mod building_a_topology_from_json { ); } + // JS: why is this an "offline-test" feature? It makes no network requests? #[test] #[cfg_attr(feature = "offline-test", ignore)] fn test_works_on_happy_json() { @@ -238,6 +240,20 @@ mod building_a_topology_from_json { ); assert_eq!(4, route.len()); } + + #[test] + fn test_works_on_happy_json_when_serialized() { + let topology = Topology::new(topology_fixture()); + let json = serde_json::to_string(&topology).unwrap(); + let route = sphinx_route_to( + &json, + &NodeAddressBytes::try_from_base58_string( + "7vhgER4Gz789QHNTSu4apMpTcpTuUaRiLxJnbz1g2HFh", + ) + .unwrap(), + ); + assert_eq!(4, route.len()); + } } #[cfg(test)] diff --git a/clients/webassembly/src/models/keys.rs b/clients/webassembly/src/models/keys.rs index d06696cb88..e559b7a10f 100644 --- a/clients/webassembly/src/models/keys.rs +++ b/clients/webassembly/src/models/keys.rs @@ -1,3 +1,17 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + use crypto::identity::MixIdentityKeyPair; use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; diff --git a/clients/webassembly/src/models/mod.rs b/clients/webassembly/src/models/mod.rs index 3f0c4e779b..b156775803 100644 --- a/clients/webassembly/src/models/mod.rs +++ b/clients/webassembly/src/models/mod.rs @@ -12,176 +12,5 @@ // See the License for the specific language governing permissions and // limitations under the License. -use nymsphinx::Node as SphinxNode; -use nymsphinx::NodeAddressBytes; -use rand::seq::IteratorRandom; -use serde::{Deserialize, Serialize}; -use std::cmp::max; -use std::collections::HashMap; -use std::convert::TryInto; -use topology::{gateway, mix, provider}; - -pub mod coconodes; -pub mod gateways; pub mod keys; -pub mod mixnodes; -pub mod providers; - -// JS: can we just get rid of this? It's mostly just copied (and un-updated) code from NymTopology trait -// Topology shows us the current state of the overall Nym network -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct Topology { - pub coco_nodes: Vec, - pub mix_nodes: Vec, - pub mix_provider_nodes: Vec, - pub gateway_nodes: Vec, -} - -impl Topology { - pub fn new(json: &str) -> Self { - if json.is_empty() { - panic!("empty json passed"); - } - serde_json::from_str(json).unwrap() - } - - fn make_layered_topology(&self) -> Result>, NymTopologyError> { - let mut layered_topology: HashMap> = HashMap::new(); - let mut highest_layer = 0; - for mix in self.mix_nodes() { - // we need to have extra space for provider - if mix.layer > nymsphinx::MAX_PATH_LENGTH as u64 { - return Err(NymTopologyError::InvalidMixLayerError); - } - highest_layer = max(highest_layer, mix.layer); - - let layer_nodes = layered_topology.entry(mix.layer).or_insert_with(Vec::new); - layer_nodes.push(mix); - } - - // verify the topology - make sure there are no gaps and there is at least one node per layer - let mut missing_layers = Vec::new(); - for layer in 1..=highest_layer { - if !layered_topology.contains_key(&layer) { - missing_layers.push(layer); - } - if layered_topology[&layer].is_empty() { - missing_layers.push(layer); - } - } - - if !missing_layers.is_empty() { - return Err(NymTopologyError::MissingLayerError(missing_layers)); - } - - Ok(layered_topology) - } - - // Tries to get a route through the mix network - fn random_mix_route(&self) -> Result, NymTopologyError> { - let mut layered_topology = self.make_layered_topology()?; - let num_layers = layered_topology.len(); - let route = (1..=num_layers as u64) - // unwrap is safe for 'remove' as it it failed, it implied the entry never existed - // in the map in the first place which would contradict what we've just done - .map(|layer| layered_topology.remove(&layer).unwrap()) // for each layer - .map(|nodes| nodes.into_iter().choose(&mut rand::thread_rng()).unwrap()) // choose random node - .map(|random_node| random_node.into()) // and convert it into sphinx specific node format - .collect(); - - Ok(route) - } - - // Sets up a route to a specific gateway - pub fn random_route_to_gateway( - &self, - gateway_address: &NodeAddressBytes, - ) -> Result, NymTopologyError> { - let b58_address = gateway_address.to_base58_string(); - let gateway_node = self - .gateways() - .iter() - .cloned() - .find(|gateway| gateway.pub_key == b58_address.clone()) - .ok_or_else(|| NymTopologyError::InvalidMixLayerError)?; - - Ok(self - .random_mix_route()? - .into_iter() - .chain(std::iter::once(gateway_node.into())) - .collect()) - } - - pub fn mix_nodes(&self) -> Vec { - self.mix_nodes - .iter() - .filter_map(|x| x.clone().try_into().ok()) - .collect() - } - - // it's intesting how compiler knows this function is unused even though it's public - // but it will be removed once our topology is refactored and the concept of "provider" - // goes away. - #[allow(dead_code)] - pub fn providers(&self) -> Vec { - self.mix_provider_nodes - .iter() - .map(|x| x.clone().into()) - .collect() - } - - pub fn gateways(&self) -> Vec { - self.gateway_nodes - .iter() - .map(|x| x.clone().into()) - .collect() - } -} - -#[derive(Debug)] -pub enum NymTopologyError { - InvalidMixLayerError, - MissingLayerError(Vec), -} - -#[cfg(test)] -mod converting_mixnode_presence_into_topology_mixnode { - use super::*; - - #[test] - fn it_returns_error_on_unresolvable_hostname() { - let unresolvable_hostname = "foomp.foomp.foomp:1234"; - - let mix_presence = mixnodes::MixNodePresence { - location: "".to_string(), - host: unresolvable_hostname.to_string(), - pub_key: "".to_string(), - layer: 0, - last_seen: 0, - version: "".to_string(), - }; - - let _result: Result = mix_presence.try_into(); - // assert!(result.is_err()) // This fails only for me. Why? - // ¯\_(ツ)_/¯ - works on my machine (and travis) - } - - #[test] - #[cfg_attr(feature = "offline-test", ignore)] - fn it_returns_resolved_ip_on_resolvable_hostname() { - let resolvable_hostname = "nymtech.net:1234"; - - let mix_presence = mixnodes::MixNodePresence { - location: "".to_string(), - host: resolvable_hostname.to_string(), - pub_key: "".to_string(), - layer: 0, - last_seen: 0, - version: "".to_string(), - }; - - let result: Result = mix_presence.try_into(); - assert!(result.is_ok()) - } -} +pub mod topology; diff --git a/clients/webassembly/src/models/topology.rs b/clients/webassembly/src/models/topology.rs new file mode 100644 index 0000000000..ee9f263370 --- /dev/null +++ b/clients/webassembly/src/models/topology.rs @@ -0,0 +1,90 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use serde::Serializer; +use topology::{coco, gateway, mix, provider, NymTopology}; + +#[derive(Clone, Debug)] +pub struct Topology { + inner: directory_client_models::presence::Topology, +} + +impl serde::Serialize for Topology { + fn serialize(&self, serializer: S) -> Result<::Ok, ::Error> + where + S: Serializer, + { + self.inner.serialize(serializer) + } +} + +impl Topology { + pub fn new(json: &str) -> Self { + if json.is_empty() { + panic!("empty json passed"); + } + + Topology { + inner: serde_json::from_str(json).unwrap(), + } + } + + #[cfg(test)] + pub(crate) fn set_mixnodes( + &mut self, + mix_nodes: Vec, + ) { + self.inner.mix_nodes = mix_nodes + } + + #[cfg(test)] + pub(crate) fn get_current_raw_mixnodes( + &self, + ) -> Vec { + self.inner.mix_nodes.clone() + } +} + +impl NymTopology for Topology { + fn new_from_nodes( + mix_nodes: Vec, + mix_provider_nodes: Vec, + coco_nodes: Vec, + gateway_nodes: Vec, + ) -> Self { + Topology { + inner: directory_client_models::presence::Topology::new_from_nodes( + mix_nodes, + mix_provider_nodes, + coco_nodes, + gateway_nodes, + ), + } + } + fn mix_nodes(&self) -> Vec { + self.inner.mix_nodes() + } + + fn providers(&self) -> Vec { + self.inner.providers() + } + + fn gateways(&self) -> Vec { + self.inner.gateways() + } + + fn coco_nodes(&self) -> Vec { + self.inner.coco_nodes() + } +} diff --git a/common/client-libs/directory-client/Cargo.toml b/common/client-libs/directory-client/Cargo.toml index 1b358c8a63..d9a96a2b47 100644 --- a/common/client-libs/directory-client/Cargo.toml +++ b/common/client-libs/directory-client/Cargo.toml @@ -13,11 +13,11 @@ offline-test = [] log = "0.4" futures = "0.3" pretty_env_logger = "0.3" -reqwest = { version = "0.10", features = ["json"] } serde = { version = "1.0.104", features = ["derive"] } +reqwest = { version = "0.10", features = ["json"] } ## internal -topology = {path = "../../topology"} +directory-client-models = { path = "models" } [dev-dependencies] mockito = "0.23.0" diff --git a/common/client-libs/directory-client/models/Cargo.toml b/common/client-libs/directory-client/models/Cargo.toml new file mode 100644 index 0000000000..533e3b8577 --- /dev/null +++ b/common/client-libs/directory-client/models/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "directory-client-models" +version = "0.1.0" +authors = ["Jędrzej Stuczyński "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +serde = { version = "1.0.104", features = ["derive"] } + +topology = { path = "../../../topology" } diff --git a/common/client-libs/directory-client/models/src/lib.rs b/common/client-libs/directory-client/models/src/lib.rs new file mode 100644 index 0000000000..c577e00bea --- /dev/null +++ b/common/client-libs/directory-client/models/src/lib.rs @@ -0,0 +1,16 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +pub mod metrics; +pub mod presence; diff --git a/common/client-libs/directory-client/src/metrics.rs b/common/client-libs/directory-client/models/src/metrics.rs similarity index 100% rename from common/client-libs/directory-client/src/metrics.rs rename to common/client-libs/directory-client/models/src/metrics.rs diff --git a/clients/webassembly/src/models/coconodes.rs b/common/client-libs/directory-client/models/src/presence/coconodes.rs similarity index 100% rename from clients/webassembly/src/models/coconodes.rs rename to common/client-libs/directory-client/models/src/presence/coconodes.rs diff --git a/clients/webassembly/src/models/gateways.rs b/common/client-libs/directory-client/models/src/presence/gateways.rs similarity index 100% rename from clients/webassembly/src/models/gateways.rs rename to common/client-libs/directory-client/models/src/presence/gateways.rs diff --git a/clients/webassembly/src/models/mixnodes.rs b/common/client-libs/directory-client/models/src/presence/mixnodes.rs similarity index 100% rename from clients/webassembly/src/models/mixnodes.rs rename to common/client-libs/directory-client/models/src/presence/mixnodes.rs diff --git a/common/client-libs/directory-client/src/presence/mod.rs b/common/client-libs/directory-client/models/src/presence/mod.rs similarity index 100% rename from common/client-libs/directory-client/src/presence/mod.rs rename to common/client-libs/directory-client/models/src/presence/mod.rs diff --git a/clients/webassembly/src/models/providers.rs b/common/client-libs/directory-client/models/src/presence/providers.rs similarity index 100% rename from clients/webassembly/src/models/providers.rs rename to common/client-libs/directory-client/models/src/presence/providers.rs diff --git a/common/client-libs/directory-client/src/presence/topology.rs b/common/client-libs/directory-client/models/src/presence/topology.rs similarity index 83% rename from common/client-libs/directory-client/src/presence/topology.rs rename to common/client-libs/directory-client/models/src/presence/topology.rs index 169415bc84..994da2e50f 100644 --- a/common/client-libs/directory-client/src/presence/topology.rs +++ b/common/client-libs/directory-client/models/src/presence/topology.rs @@ -13,12 +13,10 @@ // limitations under the License. use super::{coconodes, gateways, mixnodes, providers}; -use crate::{Client, Config, DirectoryClient}; -use futures::future::BoxFuture; -use log::*; use serde::{Deserialize, Serialize}; use std::convert::TryInto; use topology::{coco, gateway, mix, provider, NymTopology}; + // Topology shows us the current state of the overall Nym network #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -30,24 +28,6 @@ pub struct Topology { } impl NymTopology for Topology { - // TODO: this will need some changes to not imply having to make an HTTP request in constructor - // TODO2: and also be reworked/removed with the topology re-work - fn new<'a>(directory_server: String) -> BoxFuture<'a, Self> { - debug!("Using directory server: {:?}", directory_server); - let directory_config = Config { - base_url: directory_server, - }; - let directory = Client::new(directory_config); - Box::pin({ - async move { - directory - .get_topology() - .await - .expect("Failed to retrieve network topology.") - } - }) - } - fn new_from_nodes( mix_nodes: Vec, mix_provider_nodes: Vec, diff --git a/common/client-libs/directory-client/src/lib.rs b/common/client-libs/directory-client/src/lib.rs index 80277174be..2540157870 100644 --- a/common/client-libs/directory-client/src/lib.rs +++ b/common/client-libs/directory-client/src/lib.rs @@ -20,16 +20,18 @@ use crate::requests::presence_gateways_post::Request as PresenceGatewayPost; use crate::requests::presence_mixnodes_post::Request as PresenceMixNodesPost; use crate::requests::presence_providers_post::Request as PresenceProvidersPost; use crate::requests::presence_topology_get::Request as PresenceTopologyRequest; - -use metrics::{MixMetric, PersistedMixMetric}; -use presence::{ +use directory_client_models::metrics::{MixMetric, PersistedMixMetric}; +use directory_client_models::presence::{ coconodes::CocoPresence, gateways::GatewayPresence, mixnodes::MixNodePresence, - providers::MixProviderPresence, Topology, + providers::MixProviderPresence, }; use requests::{health_check_get::HealthCheckResponse, DirectoryGetRequest, DirectoryPostRequest}; -pub mod metrics; -pub mod presence; +pub use directory_client_models::{ + metrics, + presence::{self, Topology}, +}; + pub mod requests; pub struct Config { diff --git a/common/client-libs/directory-client/src/presence/coconodes.rs b/common/client-libs/directory-client/src/presence/coconodes.rs deleted file mode 100644 index bff8800630..0000000000 --- a/common/client-libs/directory-client/src/presence/coconodes.rs +++ /dev/null @@ -1,50 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// 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. - -use serde::{Deserialize, Serialize}; -use topology::coco; - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct CocoPresence { - pub location: String, - pub host: String, - pub pub_key: String, - pub last_seen: u64, - pub version: String, -} - -impl Into for CocoPresence { - fn into(self) -> topology::coco::Node { - topology::coco::Node { - location: self.location, - host: self.host, - pub_key: self.pub_key, - last_seen: self.last_seen, - version: self.version, - } - } -} - -impl From for CocoPresence { - fn from(cn: coco::Node) -> Self { - CocoPresence { - location: cn.location, - host: cn.host, - pub_key: cn.pub_key, - last_seen: cn.last_seen, - version: cn.version, - } - } -} diff --git a/common/client-libs/directory-client/src/presence/gateways.rs b/common/client-libs/directory-client/src/presence/gateways.rs deleted file mode 100644 index 78ffa78f44..0000000000 --- a/common/client-libs/directory-client/src/presence/gateways.rs +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// 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. - -use serde::{Deserialize, Serialize}; -use topology::gateway; - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GatewayPresence { - pub location: String, - pub client_listener: String, - pub mixnet_listener: String, - pub pub_key: String, - pub registered_clients: Vec, - pub last_seen: u64, - pub version: String, -} - -impl Into for GatewayPresence { - fn into(self) -> topology::gateway::Node { - topology::gateway::Node { - location: self.location, - client_listener: self.client_listener.parse().unwrap(), - mixnet_listener: self.mixnet_listener.parse().unwrap(), - pub_key: self.pub_key, - registered_clients: self - .registered_clients - .into_iter() - .map(|c| c.into()) - .collect(), - last_seen: self.last_seen, - version: self.version, - } - } -} - -impl From for GatewayPresence { - fn from(mpn: gateway::Node) -> Self { - GatewayPresence { - location: mpn.location, - client_listener: mpn.client_listener.to_string(), - mixnet_listener: mpn.mixnet_listener.to_string(), - pub_key: mpn.pub_key, - registered_clients: mpn - .registered_clients - .into_iter() - .map(|c| c.into()) - .collect(), - last_seen: mpn.last_seen, - version: mpn.version, - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct GatewayClient { - pub pub_key: String, -} - -impl Into for GatewayClient { - fn into(self) -> topology::gateway::Client { - topology::gateway::Client { - pub_key: self.pub_key, - } - } -} - -impl From for GatewayClient { - fn from(mpc: topology::gateway::Client) -> Self { - GatewayClient { - pub_key: mpc.pub_key, - } - } -} diff --git a/common/client-libs/directory-client/src/presence/mixnodes.rs b/common/client-libs/directory-client/src/presence/mixnodes.rs deleted file mode 100644 index b1f6e7f837..0000000000 --- a/common/client-libs/directory-client/src/presence/mixnodes.rs +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// 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. - -use serde::{Deserialize, Serialize}; -use std::convert::TryInto; -use std::io; -use std::net::ToSocketAddrs; -use topology::mix; - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct MixNodePresence { - pub location: String, - pub host: String, - pub pub_key: String, - pub layer: u64, - pub last_seen: u64, - pub version: String, -} - -impl TryInto for MixNodePresence { - type Error = io::Error; - - fn try_into(self) -> Result { - let resolved_hostname = self.host.to_socket_addrs()?.next(); - if resolved_hostname.is_none() { - return Err(io::Error::new( - io::ErrorKind::Other, - "no valid socket address", - )); - } - - Ok(topology::mix::Node { - location: self.location, - host: resolved_hostname.unwrap(), - pub_key: self.pub_key, - layer: self.layer, - last_seen: self.last_seen, - version: self.version, - }) - } -} - -impl From for MixNodePresence { - fn from(mn: mix::Node) -> Self { - MixNodePresence { - location: mn.location, - host: mn.host.to_string(), - pub_key: mn.pub_key, - layer: mn.layer, - last_seen: mn.last_seen, - version: mn.version, - } - } -} diff --git a/common/client-libs/directory-client/src/presence/providers.rs b/common/client-libs/directory-client/src/presence/providers.rs deleted file mode 100644 index 7f01f6d6b7..0000000000 --- a/common/client-libs/directory-client/src/presence/providers.rs +++ /dev/null @@ -1,86 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// 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. - -use serde::{Deserialize, Serialize}; -use topology::provider; - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct MixProviderPresence { - pub location: String, - pub client_listener: String, - pub mixnet_listener: String, - pub pub_key: String, - pub registered_clients: Vec, - pub last_seen: u64, - pub version: String, -} - -impl Into for MixProviderPresence { - fn into(self) -> topology::provider::Node { - topology::provider::Node { - location: self.location, - client_listener: self.client_listener.parse().unwrap(), - mixnet_listener: self.mixnet_listener.parse().unwrap(), - pub_key: self.pub_key, - registered_clients: self - .registered_clients - .into_iter() - .map(|c| c.into()) - .collect(), - last_seen: self.last_seen, - version: self.version, - } - } -} - -impl From for MixProviderPresence { - fn from(mpn: provider::Node) -> Self { - MixProviderPresence { - location: mpn.location, - client_listener: mpn.client_listener.to_string(), - mixnet_listener: mpn.mixnet_listener.to_string(), - pub_key: mpn.pub_key, - registered_clients: mpn - .registered_clients - .into_iter() - .map(|c| c.into()) - .collect(), - last_seen: mpn.last_seen, - version: mpn.version, - } - } -} - -#[derive(Clone, Debug, Deserialize, Serialize)] -#[serde(rename_all = "camelCase")] -pub struct MixProviderClient { - pub pub_key: String, -} - -impl Into for MixProviderClient { - fn into(self) -> topology::provider::Client { - topology::provider::Client { - pub_key: self.pub_key, - } - } -} - -impl From for MixProviderClient { - fn from(mpc: topology::provider::Client) -> Self { - MixProviderClient { - pub_key: mpc.pub_key, - } - } -} diff --git a/common/topology/Cargo.toml b/common/topology/Cargo.toml index 17a568b979..7d15a481a5 100644 --- a/common/topology/Cargo.toml +++ b/common/topology/Cargo.toml @@ -9,7 +9,6 @@ edition = "2018" [dependencies] bs58 = "0.3.0" itertools = "0.8.2" -futures = "0.3" log = "0.4" pretty_env_logger = "0.3" rand = "0.7.2" diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index c519572b1f..712c0eef21 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -13,7 +13,6 @@ // limitations under the License. use crate::filter::VersionFilterable; -use futures::future::BoxFuture; use itertools::Itertools; use nymsphinx_types::{Node as SphinxNode, NodeAddressBytes}; use rand::seq::IteratorRandom; @@ -29,10 +28,6 @@ pub mod provider; // TODO: Figure out why 'Clone' was required to have 'TopologyAccessor' working // even though it only contains an Arc pub trait NymTopology: Sized + std::fmt::Debug + Send + Sync + Clone { - // this is just a temporary work-around to make current code work without having to - // do major topology rewrites now. - // This will be removed once topology is re-worked - fn new<'a>(directory_server: String) -> BoxFuture<'a, Self>; fn new_from_nodes( mix_nodes: Vec, mix_provider_nodes: Vec, diff --git a/mixnode/src/node/mod.rs b/mixnode/src/node/mod.rs index 34089d3474..0338602719 100644 --- a/mixnode/src/node/mod.rs +++ b/mixnode/src/node/mod.rs @@ -15,13 +15,12 @@ use crate::config::Config; use crate::node::packet_processing::PacketProcessor; use crypto::encryption; -use directory_client::presence::Topology; +use directory_client::DirectoryClient; use futures::channel::mpsc; use log::*; use nymsphinx::SphinxPacket; use std::net::SocketAddr; use tokio::runtime::Runtime; -use topology::NymTopology; mod listener; mod metrics; @@ -102,10 +101,13 @@ impl MixNode { } fn check_if_same_ip_node_exists(&mut self) -> Option { - // TODO: once we change to graph topology this here will need to be updated! + let directory_client_config = + directory_client::Config::new(self.config.get_presence_directory_server()); + let directory_client = directory_client::Client::new(directory_client_config); let topology = self .runtime - .block_on(Topology::new(self.config.get_presence_directory_server())); + .block_on(directory_client.get_topology()) + .ok()?; let existing_mixes_presence = topology.mix_nodes; existing_mixes_presence .iter() From 68f7b1a042fad8046e8b6eb689e3de3668d10090 Mon Sep 17 00:00:00 2001 From: jstuczyn Date: Tue, 16 Jun 2020 13:03:35 +0100 Subject: [PATCH 06/76] Added local scripts to .gitignore --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 8262ad0500..b30f994948 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,8 @@ target /.vscode/settings.json validator/.vscode sample-configs/validator-config.toml -.vscode \ No newline at end of file +.vscode +scripts/deploy_qa.sh +scripts/run_gate.sh +scripts/run_mix.sh +scripts/start_local_tmux_network.sh From 3c7a01e1e679e1d7e5a36690eb77c99609a81dd7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Thu, 18 Jun 2020 10:50:31 +0100 Subject: [PATCH 07/76] Feature/constant length packet payloads (#268) * Reducing ACK packet size to exactly what we need * Made fragmentidentifier into an array * Padding all sent messages so they'd be split into constant length packets --- .../acknowledgement_listener.rs | 2 +- .../acknowlegement_control/mod.rs | 1 + clients/native/src/client/received_buffer.rs | 4 +- .../gateway-client/src/packet_router.rs | 37 ++- .../acknowledgements/src/identifier.rs | 31 +- .../acknowledgements/src/surb_ack.rs | 2 +- common/nymsphinx/chunking/src/fragment.rs | 22 +- common/nymsphinx/chunking/src/lib.rs | 270 +++++++++++++++++- .../nymsphinx/chunking/src/reconstruction.rs | 95 +++--- common/nymsphinx/chunking/src/set.rs | 2 +- common/nymsphinx/cover/src/lib.rs | 70 ++++- common/nymsphinx/params/src/packet_sizes.rs | 2 +- .../receiver/packet_processing.rs | 2 +- 13 files changed, 442 insertions(+), 98 deletions(-) diff --git a/clients/native/src/client/real_messages_control/acknowlegement_control/acknowledgement_listener.rs b/clients/native/src/client/real_messages_control/acknowlegement_control/acknowledgement_listener.rs index 7b81fbe31a..0a337fce5d 100644 --- a/clients/native/src/client/real_messages_control/acknowlegement_control/acknowledgement_listener.rs +++ b/clients/native/src/client/real_messages_control/acknowlegement_control/acknowledgement_listener.rs @@ -49,7 +49,7 @@ impl AcknowledgementListener { warn!("Received invalid ACK!"); // should we do anything else about that? return; } - Some(frag_id_bytes) => match FragmentIdentifier::try_from_bytes(&frag_id_bytes) { + Some(frag_id_bytes) => match FragmentIdentifier::try_from_bytes(frag_id_bytes) { Ok(frag_id) => frag_id, Err(err) => { warn!("Received invalid ACK! - {:?}", err); // should we do anything else about that? diff --git a/clients/native/src/client/real_messages_control/acknowlegement_control/mod.rs b/clients/native/src/client/real_messages_control/acknowlegement_control/mod.rs index 52d3e5f86f..1af48d9792 100644 --- a/clients/native/src/client/real_messages_control/acknowlegement_control/mod.rs +++ b/clients/native/src/client/real_messages_control/acknowlegement_control/mod.rs @@ -132,6 +132,7 @@ where let message_chunker = MessageChunker::new_with_rng( rng, ack_recipient.clone(), + true, average_packet_delay_duration, average_ack_delay_duration, ); diff --git a/clients/native/src/client/received_buffer.rs b/clients/native/src/client/received_buffer.rs index 408df39640..6db09190aa 100644 --- a/clients/native/src/client/received_buffer.rs +++ b/clients/native/src/client/received_buffer.rs @@ -56,7 +56,7 @@ impl ReceivedMessagesBuffer { ReceivedMessagesBuffer { inner: Arc::new(Mutex::new(ReceivedMessagesBufferInner { messages: Vec::new(), - message_reconstructor: MessageReconstructor::new(), + message_reconstructor: MessageReconstructor::new(true), message_sender: None, recently_reconstructed: HashSet::new(), })), @@ -111,7 +111,7 @@ impl ReceivedMessagesBuffer { mutex_guard: &mut MutexGuard, raw_fragment: Vec, ) -> Option> { - if raw_fragment == LOOP_COVER_MESSAGE_PAYLOAD { + if nymsphinx::cover::is_cover(&raw_fragment) { trace!("The message was a loop cover message! Skipping it"); return None; } diff --git a/common/client-libs/gateway-client/src/packet_router.rs b/common/client-libs/gateway-client/src/packet_router.rs index fcd4f6224c..5785fe102d 100644 --- a/common/client-libs/gateway-client/src/packet_router.rs +++ b/common/client-libs/gateway-client/src/packet_router.rs @@ -17,6 +17,7 @@ use futures::channel::mpsc; use log::*; +use nymsphinx::addressing::nodes::MAX_NODE_ADDRESS_UNPADDED_LEN; use nymsphinx::params::packet_sizes::PacketSize; pub type MixnetMessageSender = mpsc::UnboundedSender>>; @@ -46,21 +47,33 @@ impl PacketRouter { let mut received_messages = Vec::new(); let mut received_acks = Vec::new(); - for received_packet in unwrapped_packets { - // TODO: currently this is not true because gateways are removing padding from the packets - // but will be fixed soon enough by all other changes in the pipeline - // the question is, however, what exactly will gateways be returning instead. payloads? - // 'plaintext'?. To be determined later on. - // if received_packet.len() == PacketSize::ACKPacket.payload_size() { + // remember: gateway removes final layer of sphinx encryption and from the unwrapped + // data he takes the SURB-ACK and first hop address. + // currently SURB-ACKs are attached in EVERY packet, even cover, so this is always true + let ack_overhead = PacketSize::ACKPacket.size() + MAX_NODE_ADDRESS_UNPADDED_LEN; - // this is an extremely ugly if statement, but will be improved once things are actually - // constant length everywhere - if received_packet.len() == 21 { + for received_packet in unwrapped_packets { + // NOTE TO FUTURE-SELF: + // Right now we're kinda cheating to achieve constant length packets + // by basically including padding in the message itself + // this will eventually be removed in favour of proper encryption. + // and I guess some changes in gateways to maybe not remove padding from sphinx packets + // themselves? to be determined. + + if received_packet.len() == PacketSize::ACKPacket.plaintext_size() { received_acks.push(received_packet); + } else if received_packet.len() + == PacketSize::RegularPacket.plaintext_size() - ack_overhead + { + received_messages.push(received_packet); + } else if received_packet.len() + == PacketSize::ExtendedPacket.plaintext_size() - ack_overhead + { + warn!("received extended packet? Did not expect this..."); + received_messages.push(received_packet); } else { - // well, technically all 21 bytes packets will be considered acks which is not - // entirely true, but for time being let's stick with it until other changes are - // introduced + // this can happen if other clients are not padding their messages + warn!("Received message of unexpected size. Probably from an outdated client... len: {}", received_packet.len()); received_messages.push(received_packet); } } diff --git a/common/nymsphinx/acknowledgements/src/identifier.rs b/common/nymsphinx/acknowledgements/src/identifier.rs index 6ff2406ce3..03ed6deedb 100644 --- a/common/nymsphinx/acknowledgements/src/identifier.rs +++ b/common/nymsphinx/acknowledgements/src/identifier.rs @@ -47,10 +47,8 @@ fn random_iv(rng: &mut R) -> AckAes128IV { pub fn prepare_identifier( rng: &mut R, key: &AckAes128Key, - marshaled_id: &[u8], + marshaled_id: [u8; 5], ) -> Vec { - // TODO: should we have some length checks on the id? - let iv = random_iv(rng); let mut cipher = Aes128Ctr::new(key, &iv); let mut output = marshaled_id.to_vec(); @@ -60,13 +58,11 @@ pub fn prepare_identifier( iv.into_iter().chain(output.into_iter()).collect() } -pub fn recover_identifier(key: &AckAes128Key, iv_ciphertext: &[u8]) -> Option> { +pub fn recover_identifier(key: &AckAes128Key, iv_ciphertext: &[u8]) -> Option<[u8; 5]> { // first few bytes are expected to be the concatenated IV. It must be followed by at least 1 more // byte that we wish to recover, but it can be no longer from what we can physically store inside // an ack - if iv_ciphertext.len() <= Aes128NonceSize::to_usize() - || iv_ciphertext.len() > PacketSize::ACKPacket.plaintext_size() - { + if iv_ciphertext.len() != PacketSize::ACKPacket.plaintext_size() { return None; } @@ -75,7 +71,9 @@ pub fn recover_identifier(key: &AckAes128Key, iv_ciphertext: &[u8]) -> Option Result diff --git a/common/nymsphinx/chunking/src/fragment.rs b/common/nymsphinx/chunking/src/fragment.rs index 76d9c432f0..91c530e1b1 100644 --- a/common/nymsphinx/chunking/src/fragment.rs +++ b/common/nymsphinx/chunking/src/fragment.rs @@ -72,20 +72,18 @@ pub struct FragmentIdentifier { } impl FragmentIdentifier { - pub fn to_bytes(&self) -> Vec { - self.set_id - .to_be_bytes() - .iter() - .cloned() - .chain(std::iter::once(self.fragment_position)) - .collect() + pub fn to_bytes(&self) -> [u8; 5] { + let set_id_bytes = self.set_id.to_be_bytes(); + [ + set_id_bytes[0], + set_id_bytes[1], + set_id_bytes[2], + set_id_bytes[3], + self.fragment_position, + ] } - pub fn try_from_bytes(b: &[u8]) -> Result { - if b.len() != 5 { - return Err(ChunkingError::MalformedFragmentIdentifier); - } - + pub fn try_from_bytes(b: [u8; 5]) -> Result { let set_id = i32::from_be_bytes([b[0], b[1], b[2], b[3]]); // set_id == 0 is valid for, and only for, COVER_FRAG_ID if set_id < 0 { diff --git a/common/nymsphinx/chunking/src/lib.rs b/common/nymsphinx/chunking/src/lib.rs index df9e968b04..a006f313c5 100644 --- a/common/nymsphinx/chunking/src/lib.rs +++ b/common/nymsphinx/chunking/src/lib.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::fragment::{Fragment, FragmentIdentifier}; +use crate::fragment::{ + linked_fragment_payload_max_len, unlinked_fragment_payload_max_len, Fragment, + FragmentIdentifier, +}; use crate::set::split_into_sets; use nymsphinx_acknowledgements::identifier::AckAes128Key; use nymsphinx_acknowledgements::surb_ack::SURBAck; @@ -34,6 +37,10 @@ use topology::{NymTopology, NymTopologyError}; // perhaps it might be useful down the line for interaction testing between client,mixes,etc? +// TODO: this module has evolved significantly since the tests were first written +// they should definitely be revisited. +// For instance there are not tests for the cases when we are padding the message + pub mod fragment; pub mod reconstruction; pub mod set; @@ -88,6 +95,7 @@ pub struct MessageChunker { ack_recipient: Recipient, packet_size: PacketSize, reply_surbs: bool, + should_pad: bool, average_packet_delay_duration: Duration, average_ack_delay_duration: Duration, } @@ -95,12 +103,14 @@ pub struct MessageChunker { impl MessageChunker { pub fn new( ack_recipient: Recipient, + should_pad: bool, average_packet_delay_duration: Duration, average_ack_delay_duration: Duration, ) -> Self { Self::new_with_rng( DEFAULT_RNG, ack_recipient, + should_pad, average_packet_delay_duration, average_ack_delay_duration, ) @@ -115,7 +125,12 @@ impl MessageChunker { DestinationAddressBytes::from_bytes(empty_address), NodeAddressBytes::from_bytes(empty_address), ); - Self::new(empty_recipient, Default::default(), Default::default()) + Self::new( + empty_recipient, + false, + Default::default(), + Default::default(), + ) } } @@ -123,12 +138,14 @@ impl MessageChunker { pub fn new_with_rng( rng: R, ack_recipient: Recipient, + should_pad: bool, average_packet_delay_duration: Duration, average_ack_delay_duration: Duration, ) -> Self { MessageChunker { rng, ack_recipient, + should_pad, packet_size: Default::default(), reply_surbs: false, average_packet_delay_duration, @@ -219,22 +236,259 @@ impl MessageChunker { &mut self.rng, &self.ack_recipient, ack_key, - &fragment_id.to_bytes(), + fragment_id.to_bytes(), self.average_ack_delay_duration, topology, ) } + /// Returns number of fragments the message will be split to as well as number of available + /// bytes in the final fragment + pub fn number_of_required_fragments( + message_len: usize, + plaintext_per_fragment: usize, + ) -> (usize, usize) { + let max_unlinked = unlinked_fragment_payload_max_len(plaintext_per_fragment); + let max_linked = linked_fragment_payload_max_len(plaintext_per_fragment); + + match set::total_number_of_sets(message_len, plaintext_per_fragment) { + n if n == 1 => { + // is if it's a single fragment message + if message_len < max_unlinked { + return (1, max_unlinked - message_len); + } + + // all fragments will be 'unlinked' + let quot = message_len / max_unlinked; + let rem = message_len % max_unlinked; + + if rem == 0 { + (quot, 0) + } else { + (quot + 1, max_unlinked - rem) + } + } + + n => { + // in first and last set there will be one 'linked' fragment + // and two 'linked' fragment in every other set, meaning + // there will be 2 * (n - 2) + 2 = 2n - 2 'linked' fragments total + // rest will be 'unlinked' + + // we know for sure that all fragments in all but last set are definitely full + // (last one has single 'linked' fragment) + let without_last = (n - 1) * (u8::max_value() as usize); + let linked_fragments_without_last = (2 * n - 2) - 1; + let unlinked_fragments_without_last = without_last - linked_fragments_without_last; + + let final_set_message_len = message_len + - linked_fragments_without_last * max_linked + - unlinked_fragments_without_last * max_unlinked; + + // we must be careful with the last set as it might be the case that it only + // consists of a single, linked, non-full fragment + if final_set_message_len < max_linked { + return (without_last + 1, max_linked - final_set_message_len); + } else if final_set_message_len == max_linked { + return (without_last + 1, 0); + } + + let remaining_len = final_set_message_len - max_linked; + + let quot = remaining_len / max_unlinked; + let rem = remaining_len % max_unlinked; + + if rem == 0 { + (without_last + quot + 1, 0) + } else { + (without_last + quot + 2, max_unlinked - rem) + } + } + } + } + + /// Takes the entire message and splits it into bytes chunks that will fit into sphinx packets + /// after attaching SURB-ACK, such that the payload of the sphinx packet will be fully + /// used up. + /// After receiving they can be combined using `reconstruction::MessageReconstructor` + /// to obtain the original message back. + pub fn split_message_to_constant_length_chunks(&mut self, message: &[u8]) -> Vec { + let available_plaintext_per_fragment = self.available_plaintext_size(); + + // 1 is added as there will always have to be at least a single byte of padding (1) added + // to be able to later remove the padding + let (_, space_left) = + Self::number_of_required_fragments(message.len() + 1, available_plaintext_per_fragment); + + // TODO: this makes copy of all data and so will a fragment chunker, + // so a tiny optimization would be to make all + // methods using this value, i.e. take Vec rather than &[u8] + let message: Vec<_> = message + .iter() + .cloned() + .chain(std::iter::once(1u8)) + .chain(std::iter::repeat(0u8).take(space_left)) + .collect(); + + split_into_sets(&mut self.rng, &message, available_plaintext_per_fragment) + .into_iter() + .flat_map(|fragment_set| fragment_set.into_iter()) + .collect() + } + /// Takes the entire message and splits it into bytes chunks that will fit into sphinx packets /// after attaching SURB-ACK. /// After receiving they can be combined using `reconstruction::MessageReconstructor` /// to obtain the original message back. pub fn split_message(&mut self, message: &[u8]) -> Vec { - let available_plaintext_per_fragment = self.available_plaintext_size(); + if self.should_pad { + self.split_message_to_constant_length_chunks(message) + } else { + let available_plaintext_per_fragment = self.available_plaintext_size(); - split_into_sets(&mut self.rng, message, available_plaintext_per_fragment) - .into_iter() - .flat_map(|fragment_set| fragment_set.into_iter()) - .collect() + split_into_sets(&mut self.rng, &message, available_plaintext_per_fragment) + .into_iter() + .flat_map(|fragment_set| fragment_set.into_iter()) + .collect() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::set::{max_one_way_linked_set_payload_length, two_way_linked_set_payload_length}; + + #[test] + fn calculating_number_of_required_fragments() { + // plaintext len should not affect this at all, but let's test it with something tiny + // and reasonable + let used_plaintext_len = PacketSize::default().plaintext_size() + - PacketSize::ACKPacket.size() + - MAX_NODE_ADDRESS_UNPADDED_LEN; + + let plaintext_lens = vec![17, used_plaintext_len, 20, 42, 10000]; + const SET_LEN: usize = u8::max_value() as usize; + + for plaintext_len in plaintext_lens { + let unlinked_len = unlinked_fragment_payload_max_len(plaintext_len); + let linked_len = linked_fragment_payload_max_len(plaintext_len); + let full_edge_set = max_one_way_linked_set_payload_length(plaintext_len); + let full_middle_set = two_way_linked_set_payload_length(plaintext_len); + + let single_non_full_frag_message_len = unlinked_len - 5; + let (frags, space_left) = MessageChunker::::number_of_required_fragments( + single_non_full_frag_message_len, + plaintext_len, + ); + assert_eq!(frags, 1); + assert_eq!(space_left, unlinked_len - single_non_full_frag_message_len); + + let single_full_frag_message_len = unlinked_len; + let (frags, space_left) = MessageChunker::::number_of_required_fragments( + single_full_frag_message_len, + plaintext_len, + ); + assert_eq!(frags, 1); + assert_eq!(space_left, 0); + + let two_non_full_frags_len = unlinked_len + 1; + let (frags, space_left) = MessageChunker::::number_of_required_fragments( + two_non_full_frags_len, + plaintext_len, + ); + assert_eq!(frags, 2); + assert_eq!(space_left, unlinked_len - 1); + + let two_full_frags_len = 2 * unlinked_len; + let (frags, space_left) = MessageChunker::::number_of_required_fragments( + two_full_frags_len, + plaintext_len, + ); + assert_eq!(frags, 2); + assert_eq!(space_left, 0); + + let multi_single_set_frags_non_full = unlinked_len * 42 - 5; + let (frags, space_left) = MessageChunker::::number_of_required_fragments( + multi_single_set_frags_non_full, + plaintext_len, + ); + assert_eq!(frags, 42); + assert_eq!(space_left, 5); + + let multi_single_set_frags_full = unlinked_len * 42; + let (frags, space_left) = MessageChunker::::number_of_required_fragments( + multi_single_set_frags_full, + plaintext_len, + ); + assert_eq!(frags, 42); + assert_eq!(space_left, 0); + + let two_set_one_non_full_frag = full_edge_set + linked_len - 1; + let (frags, space_left) = MessageChunker::::number_of_required_fragments( + two_set_one_non_full_frag, + plaintext_len, + ); + assert_eq!(frags, SET_LEN + 1); + assert_eq!(space_left, 1); + + let two_set_one_full_frag = full_edge_set + linked_len; + let (frags, space_left) = MessageChunker::::number_of_required_fragments( + two_set_one_full_frag, + plaintext_len, + ); + assert_eq!(frags, SET_LEN + 1); + assert_eq!(space_left, 0); + + let two_set_multi_frags_non_full = full_edge_set + linked_len + unlinked_len * 41 - 5; + let (frags, space_left) = MessageChunker::::number_of_required_fragments( + two_set_multi_frags_non_full, + plaintext_len, + ); + assert_eq!(frags, SET_LEN + 42); + assert_eq!(space_left, 5); + + let two_set_multi_frags_full = full_edge_set + linked_len + unlinked_len * 41; + let (frags, space_left) = MessageChunker::::number_of_required_fragments( + two_set_multi_frags_full, + plaintext_len, + ); + assert_eq!(frags, SET_LEN + 42); + assert_eq!(space_left, 0); + + let ten_set_one_non_full_frag = full_edge_set + 8 * full_middle_set + linked_len - 1; + let (frags, space_left) = MessageChunker::::number_of_required_fragments( + ten_set_one_non_full_frag, + plaintext_len, + ); + assert_eq!(frags, 9 * SET_LEN + 1); + assert_eq!(space_left, 1); + + let ten_set_one_full_frag = full_edge_set + 8 * full_middle_set + linked_len; + let (frags, space_left) = MessageChunker::::number_of_required_fragments( + ten_set_one_full_frag, + plaintext_len, + ); + assert_eq!(frags, 9 * SET_LEN + 1); + assert_eq!(space_left, 0); + + let ten_set_multi_frags_non_full = + full_edge_set + 8 * full_middle_set + linked_len + 41 * unlinked_len - 5; + let (frags, space_left) = MessageChunker::::number_of_required_fragments( + ten_set_multi_frags_non_full, + plaintext_len, + ); + assert_eq!(frags, 9 * SET_LEN + 42); + assert_eq!(space_left, 5); + + let ten_set_multi_frags_full = + full_edge_set + 8 * full_middle_set + linked_len + 41 * unlinked_len; + let (frags, space_left) = MessageChunker::::number_of_required_fragments( + ten_set_multi_frags_full, + plaintext_len, + ); + assert_eq!(frags, 9 * SET_LEN + 42); + assert_eq!(space_left, 0); + } } } diff --git a/common/nymsphinx/chunking/src/reconstruction.rs b/common/nymsphinx/chunking/src/reconstruction.rs index e25ac3df03..051e52e34c 100644 --- a/common/nymsphinx/chunking/src/reconstruction.rs +++ b/common/nymsphinx/chunking/src/reconstruction.rs @@ -150,6 +150,7 @@ impl ReconstructionBuffer { /// returning original messages that they encapsulate. #[derive(Default, PartialEq, Debug, Clone)] pub struct MessageReconstructor { + expects_padding: bool, // TODO: some cleaner thread/routine that if message is incomplete and // we haven't received any fragments in X time, we assume they // were lost and message can't be restored. @@ -163,8 +164,11 @@ pub struct MessageReconstructor { impl MessageReconstructor { /// Creates an empty `MessageReconstructor`. - pub fn new() -> Self { - Default::default() + pub fn new(expects_padding: bool) -> Self { + MessageReconstructor { + expects_padding, + reconstructed_sets: HashMap::new(), + } } /// Given fully received set of given `id`, if it has any post-linked sets, recursively @@ -266,13 +270,26 @@ impl MessageReconstructor { let set_id_sequence: Vec<_> = std::iter::successors(Some(starting_id), |&id| self.next_linked_set_id(id)).collect(); - let message_content = set_id_sequence + let message_content: Vec<_> = set_id_sequence .iter() .map(|&id| self.extract_set_payload(id)) .flat_map(|payload| payload.into_iter()) .collect(); - (message_content, set_id_sequence) + if self.expects_padding { + // remove padding + // we are looking for first occurrence of 1 in the tail and we get its index + if let Some(i) = message_content.iter().rposition(|b| *b == 1) { + // and now we only take bytes until that point (but not including it) + let unpadded_content = message_content.into_iter().take(i).collect(); + (unpadded_content, set_id_sequence) + } else { + error!("received unpadded message!"); + (message_content, set_id_sequence) + } + } else { + (message_content, set_id_sequence) + } } /// Given recovered `Fragment`, tries to insert it into an appropriate `ReconstructionBuffer`. @@ -539,7 +556,7 @@ mod message_reconstructor { #[should_panic] fn checking_front_chain_is_not_allowed_for_incomplete_sets() { let mut message_chunker = MessageChunker::test_fixture(); - let mut reconstructor = MessageReconstructor::new(); + let mut reconstructor = MessageReconstructor::default(); let message = vec![ @@ -580,7 +597,7 @@ mod message_reconstructor { #[should_panic] fn checking_back_chain_is_not_allowed_for_incomplete_sets() { let mut message_chunker = MessageChunker::test_fixture(); - let mut reconstructor = MessageReconstructor::new(); + let mut reconstructor = MessageReconstructor::default(); let message = vec![ 42u8; @@ -625,7 +642,7 @@ mod message_reconstructor { #[test] fn checking_front_chain_returns_false_for_complete_set_but_incomplete_message() { let mut message_chunker = MessageChunker::test_fixture(); - let mut reconstructor = MessageReconstructor::new(); + let mut reconstructor = MessageReconstructor::default(); let message = vec![ @@ -673,7 +690,7 @@ mod message_reconstructor { #[test] fn checking_back_chain_returns_false_for_complete_set_but_incomplete_message() { let mut message_chunker = MessageChunker::test_fixture(); - let mut reconstructor = MessageReconstructor::new(); + let mut reconstructor = MessageReconstructor::default(); let message = vec![ @@ -715,7 +732,7 @@ mod message_reconstructor { fn checking_front_chain_returns_true_for_if_there_are_no_more_front_sets() { // case of 2 sets: [id1 -- id2], where id1 is completed and being checked let mut message_chunker = MessageChunker::test_fixture(); - let mut reconstructor = MessageReconstructor::new(); + let mut reconstructor = MessageReconstructor::default(); let message = vec![ @@ -757,7 +774,7 @@ mod message_reconstructor { fn checking_back_chain_returns_true_for_if_there_are_no_more_back_sets() { // case of 2 sets: [id1 -- id2], where id2 is completed and being checked let mut message_chunker = MessageChunker::test_fixture(); - let mut reconstructor = MessageReconstructor::new(); + let mut reconstructor = MessageReconstructor::default(); let message = vec![ 42u8; @@ -798,7 +815,7 @@ mod message_reconstructor { fn checking_front_chain_returns_true_for_complete_front_chain() { // case of 3 sets: [id1 -- id2 -- id3], where id1 and id2 are completed and id2 is being checked let mut message_chunker = MessageChunker::test_fixture(); - let mut reconstructor = MessageReconstructor::new(); + let mut reconstructor = MessageReconstructor::default(); let message = vec![ @@ -841,7 +858,7 @@ mod message_reconstructor { fn checking_back_chain_returns_true_for_complete_back_chain() { // case of 3 sets: [id1 -- id2 -- id3], where id2 and id3 are completed and id2 is being checked let mut message_chunker = MessageChunker::test_fixture(); - let mut reconstructor = MessageReconstructor::new(); + let mut reconstructor = MessageReconstructor::default(); let message = vec![ @@ -881,13 +898,13 @@ mod message_reconstructor { #[test] fn checking_if_set_is_fully_received_returns_false_if_no_fragments_were_ever_received() { - let reconstructor = MessageReconstructor::new(); + let reconstructor = MessageReconstructor::default(); assert!(!reconstructor.is_set_fully_received(12345)); } #[test] fn checking_if_set_is_fully_received_if_exists_returns_whatever_is_complete_flag_is_set_to() { - let mut reconstructor = MessageReconstructor::new(); + let mut reconstructor = MessageReconstructor::default(); reconstructor.reconstructed_sets.insert( 12345, ReconstructionBuffer { @@ -915,7 +932,7 @@ mod message_reconstructor { #[test] fn finding_starting_set_id_returns_none_if_message_was_not_fully_received() { let mut message_chunker = MessageChunker::test_fixture(); - let mut reconstructor = MessageReconstructor::new(); + let mut reconstructor = MessageReconstructor::default(); let message1 = vec![ 42u8; @@ -990,7 +1007,7 @@ mod message_reconstructor { #[test] fn finding_starting_set_id_returns_expected_starting_id() { let mut message_chunker = MessageChunker::test_fixture(); - let mut reconstructor = MessageReconstructor::new(); + let mut reconstructor = MessageReconstructor::default(); let message = vec![ @@ -1076,7 +1093,7 @@ mod message_reconstructor { #[should_panic] fn getting_previous_linked_set_id_is_not_allowed_for_incomplete_sets() { let mut message_chunker = MessageChunker::test_fixture(); - let mut reconstructor = MessageReconstructor::new(); + let mut reconstructor = MessageReconstructor::default(); let message = vec![ @@ -1109,7 +1126,7 @@ mod message_reconstructor { #[test] fn getting_previous_linked_set_id_returns_id_of_previous_set() { - let mut reconstructor = MessageReconstructor::new(); + let mut reconstructor = MessageReconstructor::default(); reconstructor.reconstructed_sets.insert( 12345, ReconstructionBuffer { @@ -1136,7 +1153,7 @@ mod message_reconstructor { #[should_panic] fn getting_next_linked_set_id_is_not_allowed_for_incomplete_sets() { let mut message_chunker = MessageChunker::test_fixture(); - let mut reconstructor = MessageReconstructor::new(); + let mut reconstructor = MessageReconstructor::default(); let message = vec![ @@ -1169,7 +1186,7 @@ mod message_reconstructor { #[test] fn getting_next_linked_set_id_returns_id_of_next_set() { - let mut reconstructor = MessageReconstructor::new(); + let mut reconstructor = MessageReconstructor::default(); reconstructor.reconstructed_sets.insert( 12345, ReconstructionBuffer { @@ -1196,7 +1213,7 @@ mod message_reconstructor { #[should_panic] fn extracting_set_payload_is_not_allowed_for_incomplete_sets() { let mut message_chunker = MessageChunker::test_fixture(); - let mut reconstructor = MessageReconstructor::new(); + let mut reconstructor = MessageReconstructor::default(); let message = vec![ @@ -1230,7 +1247,7 @@ mod message_reconstructor { #[test] fn extracting_set_payload_is_returns_entire_set_data() { let mut message_chunker = MessageChunker::test_fixture(); - let mut reconstructor = MessageReconstructor::new(); + let mut reconstructor = MessageReconstructor::default(); let mut set_buf = ReconstructionBuffer::new(3); let mut rng = thread_rng(); @@ -1265,7 +1282,7 @@ mod message_reconstructor { fn reconstructing_message_for_single_set_is_equivalent_to_extracting_set_payload() { // we're inserting this via the buffer approach as not to trigger immediate re-assembly let mut message_chunker = MessageChunker::test_fixture(); - let mut reconstructor = MessageReconstructor::new(); + let mut reconstructor = MessageReconstructor::default(); let mut set_buf = ReconstructionBuffer::new(3); let mut rng = thread_rng(); @@ -1304,7 +1321,7 @@ mod message_reconstructor { let mut message_chunker = MessageChunker::test_fixture(); // we're inserting this via the buffer approach as not to trigger immediate re-assembly - let mut reconstructor = MessageReconstructor::new(); + let mut reconstructor = MessageReconstructor::default(); let mut set_buf1 = ReconstructionBuffer::new(u8::max_value()); let mut set_buf2 = ReconstructionBuffer::new(1); @@ -1358,13 +1375,13 @@ mod message_reconstructor { #[test] fn adding_invalid_fragment_does_not_change_reconstructor_state() { let mut message_chunker = MessageChunker::test_fixture(); - let empty_reconstructor = MessageReconstructor::new(); + let empty_reconstructor = MessageReconstructor::default(); assert!(empty_reconstructor .recover_fragment([24u8; 43].to_vec()) .is_err()); - assert_eq!(empty_reconstructor, MessageReconstructor::new()); + assert_eq!(empty_reconstructor, MessageReconstructor::default()); - let mut reconstructor_with_data = MessageReconstructor::new(); + let mut reconstructor_with_data = MessageReconstructor::default(); let dummy_message = vec![ 24u8; @@ -1422,7 +1439,7 @@ mod message_reconstruction { .collect(); assert_eq!(fragment.len(), 1); - let mut message_reconstructor = MessageReconstructor::new(); + let mut message_reconstructor = MessageReconstructor::default(); let reconstructed_message = message_reconstructor .insert_new_fragment( message_reconstructor @@ -1454,7 +1471,7 @@ mod message_reconstruction { .collect(); assert_eq!(fragment.len(), 1); - let mut message_reconstructor = MessageReconstructor::new(); + let mut message_reconstructor = MessageReconstructor::default(); let reconstructed_message = message_reconstructor .insert_new_fragment( message_reconstructor @@ -1487,7 +1504,7 @@ mod message_reconstruction { .collect(); assert_eq!(fragments.len(), 2); - let mut message_reconstructor = MessageReconstructor::new(); + let mut message_reconstructor = MessageReconstructor::default(); assert!(message_reconstructor .insert_new_fragment( message_reconstructor @@ -1528,7 +1545,7 @@ mod message_reconstruction { .collect(); assert_eq!(fragments.len(), 2); - let mut message_reconstructor = MessageReconstructor::new(); + let mut message_reconstructor = MessageReconstructor::default(); assert!(message_reconstructor .insert_new_fragment( message_reconstructor @@ -1569,7 +1586,7 @@ mod message_reconstruction { .collect(); assert_eq!(fragments.len(), 30); - let mut message_reconstructor = MessageReconstructor::new(); + let mut message_reconstructor = MessageReconstructor::default(); for i in 0..29 { assert!(message_reconstructor .insert_new_fragment( @@ -1615,7 +1632,7 @@ mod message_reconstruction { // shuffle the fragments fragments.shuffle(&mut rng); - let mut message_reconstructor = MessageReconstructor::new(); + let mut message_reconstructor = MessageReconstructor::default(); for i in 0..29 { assert!(message_reconstructor .insert_new_fragment( @@ -1672,7 +1689,7 @@ mod message_reconstruction { let fragments = fragments1; assert_eq!(fragments.len(), 60); - let mut message_reconstructor = MessageReconstructor::new(); + let mut message_reconstructor = MessageReconstructor::default(); for fragment in fragments { if let Some(reconstructed_msg) = message_reconstructor.insert_new_fragment( message_reconstructor @@ -1721,7 +1738,7 @@ mod message_reconstruction { let fragments = fragments1; assert_eq!(fragments.len(), (u8::max_value() as usize) * 2); - let mut message_reconstructor = MessageReconstructor::new(); + let mut message_reconstructor = MessageReconstructor::default(); for fragment in fragments.into_iter() { if let Some(reconstructed_msg) = message_reconstructor.insert_new_fragment( message_reconstructor @@ -1767,7 +1784,7 @@ mod message_reconstruction { // shuffle the fragments fragments.shuffle(&mut rng); - let mut message_reconstructor = MessageReconstructor::new(); + let mut message_reconstructor = MessageReconstructor::default(); let mut finished_reconstruction = false; for fragment in fragments.into_iter() { if finished_reconstruction { @@ -1808,7 +1825,7 @@ mod message_reconstruction { // shuffle the fragments fragments.shuffle(&mut rng); - let mut message_reconstructor = MessageReconstructor::new(); + let mut message_reconstructor = MessageReconstructor::default(); let mut finished_reconstruction = false; for fragment in fragments.into_iter() { if finished_reconstruction { @@ -1850,7 +1867,7 @@ mod message_reconstruction { // shuffle the fragments fragments.shuffle(&mut rng); - let mut message_reconstructor = MessageReconstructor::new(); + let mut message_reconstructor = MessageReconstructor::default(); let mut finished_reconstruction = false; for fragment in fragments.into_iter() { if finished_reconstruction { @@ -1906,7 +1923,7 @@ mod message_reconstruction { let fragments = fragments1; assert_eq!(fragments.len(), (u8::max_value() as usize) * 8); - let mut message_reconstructor = MessageReconstructor::new(); + let mut message_reconstructor = MessageReconstructor::default(); for fragment in fragments.into_iter() { if let Some(msg) = message_reconstructor.insert_new_fragment( message_reconstructor diff --git a/common/nymsphinx/chunking/src/set.rs b/common/nymsphinx/chunking/src/set.rs index 9cbbe82e33..25e6edf6a9 100644 --- a/common/nymsphinx/chunking/src/set.rs +++ b/common/nymsphinx/chunking/src/set.rs @@ -200,7 +200,7 @@ fn prepare_linked_fragment_set( } /// Based on total message length, determines the number of sets into which it is going to be split. -fn total_number_of_sets(message_len: usize, max_plaintext_size: usize) -> usize { +pub(crate) fn total_number_of_sets(message_len: usize, max_plaintext_size: usize) -> usize { if message_len <= max_unlinked_set_payload_length(max_plaintext_size) { 1 } else if message_len > max_unlinked_set_payload_length(max_plaintext_size) diff --git a/common/nymsphinx/cover/src/lib.rs b/common/nymsphinx/cover/src/lib.rs index fe50b44956..3c5b5d22be 100644 --- a/common/nymsphinx/cover/src/lib.rs +++ b/common/nymsphinx/cover/src/lib.rs @@ -70,7 +70,7 @@ where rng, full_address, ack_key, - &COVER_FRAG_ID.to_bytes(), + COVER_FRAG_ID.to_bytes(), average_ack_delay, topology, )?) @@ -93,9 +93,16 @@ where generate_loop_cover_surb_ack(rng, topology, ack_key, full_address, average_ack_delay)? .prepare_for_sending(); + let plaintext_size = PacketSize::default().plaintext_size(); + let cover_payload: Vec<_> = ack_bytes .into_iter() .chain(LOOP_COVER_MESSAGE_PAYLOAD.into_iter().cloned()) + // let's be lazy about it (temporarily! because cover messages will need to be encrypted) + // TODO: to remember: encrypt cover messages + .chain(std::iter::once(1)) + .chain(std::iter::repeat(0)) + .take(plaintext_size) .collect(); let route = topology.random_route_to_gateway(&full_address.gateway())?; @@ -114,3 +121,64 @@ where Ok((first_hop_address.into(), packet)) } + +/// Helper function used to determine if given message represents a loop cover message. +// It kinda seems like there must exist "prefix" or "starts_with" method for bytes +// or something, but I couldn't find anything +pub fn is_cover(data: &[u8]) -> bool { + if data.len() < LOOP_COVER_MESSAGE_PAYLOAD.len() { + return false; + } + + for i in 0..LOOP_COVER_MESSAGE_PAYLOAD.len() { + if data[i] != LOOP_COVER_MESSAGE_PAYLOAD[i] { + return false; + } + } + + true +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn is_cover_works_for_identical_input() { + assert!(is_cover(&LOOP_COVER_MESSAGE_PAYLOAD)) + } + + #[test] + fn is_cover_works_for_longer_input() { + let input: Vec<_> = LOOP_COVER_MESSAGE_PAYLOAD + .iter() + .cloned() + .chain(std::iter::repeat(42).take(100)) + .collect(); + assert!(is_cover(&input)) + } + + #[test] + fn is_cover_returns_false_for_unrelated_input() { + // make sure the length checks out + let input: Vec<_> = LOOP_COVER_MESSAGE_PAYLOAD.iter().map(|_| 42).collect(); + assert!(!is_cover(&input)) + } + + #[test] + fn is_cover_returns_false_for_part_of_correct_input() { + let input: Vec<_> = LOOP_COVER_MESSAGE_PAYLOAD + .iter() + .cloned() + .take(LOOP_COVER_MESSAGE_PAYLOAD.len() - 1) + .chain(std::iter::once(42)) + .collect(); + assert!(!is_cover(&input)) + } + + #[test] + fn is_cover_returns_false_for_empty_input() { + let empty = Vec::new(); + assert!(!is_cover(&empty)) + } +} diff --git a/common/nymsphinx/params/src/packet_sizes.rs b/common/nymsphinx/params/src/packet_sizes.rs index 5c8969797c..862bf264bc 100644 --- a/common/nymsphinx/params/src/packet_sizes.rs +++ b/common/nymsphinx/params/src/packet_sizes.rs @@ -18,7 +18,7 @@ use std::convert::TryFrom; // it's up to the smart people to figure those values out : ) const REGULAR_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 2 * 1024; -const ACK_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 24; +const ACK_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 21; // 16B IV + 5B ID const EXTENDED_PACKET_SIZE: usize = HEADER_SIZE + PAYLOAD_OVERHEAD_SIZE + 32 * 1024; pub struct InvalidPacketSize; diff --git a/gateway/src/node/mixnet_handling/receiver/packet_processing.rs b/gateway/src/node/mixnet_handling/receiver/packet_processing.rs index a6345b5a99..642c4bc975 100644 --- a/gateway/src/node/mixnet_handling/receiver/packet_processing.rs +++ b/gateway/src/node/mixnet_handling/receiver/packet_processing.rs @@ -171,7 +171,7 @@ impl PacketProcessor { // JS: I think this would never get called anyway, because if loop cover messages are sent // it means client is online and hence all his messages should be pushed directly to him? - if message == LOOP_COVER_MESSAGE_PAYLOAD { + if nymsphinx::cover::is_cover(&message) { debug!("Received a loop cover message - not going to store it"); return Ok(()); } From a47ce33dcf22480b1bcd2cb099c88c039deb73cc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 18 Jun 2020 10:59:53 +0100 Subject: [PATCH 08/76] build(deps): bump websocket-extensions (#261) Bumps [websocket-extensions](https://github.com/faye/websocket-extensions-node) from 0.1.3 to 0.1.4. - [Release notes](https://github.com/faye/websocket-extensions-node/releases) - [Changelog](https://github.com/faye/websocket-extensions-node/blob/master/CHANGELOG.md) - [Commits](https://github.com/faye/websocket-extensions-node/compare/0.1.3...0.1.4) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .../webassembly/js-example/package-lock.json | 22 ++++++++++++++----- clients/webassembly/js-example/yarn.lock | 21 +++++++++++++----- 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/clients/webassembly/js-example/package-lock.json b/clients/webassembly/js-example/package-lock.json index 6c51a77baf..e12e1a7cd5 100644 --- a/clients/webassembly/js-example/package-lock.json +++ b/clients/webassembly/js-example/package-lock.json @@ -5,9 +5,19 @@ "requires": true, "dependencies": { "@nymproject/nym-client-wasm": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@nymproject/nym-client-wasm/-/nym-client-wasm-0.7.3.tgz", - "integrity": "sha512-sCLX4BGK+BjWh1lP+URexWsJoZQtyXSy2AmCaDUgCJm2rVFWhgeS2odkg2S+tdTeTeYO9Fu2YPvD8P3CFIelLg==" + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@nymproject/nym-client-wasm/-/nym-client-wasm-0.7.5.tgz", + "integrity": "sha512-r5RW4aSqyzWUvnO01EwqWdA0UGcULGrNSTe9eTDo+EoprCp1ab3vFpJs4pcg64DhxkbNbTgFVbi6TZVxObKhPQ==", + "requires": { + "semver": "^7.3.2" + }, + "dependencies": { + "semver": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.2.tgz", + "integrity": "sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ==" + } + } }, "@types/events": { "version": "3.0.0", @@ -5304,9 +5314,9 @@ } }, "websocket-extensions": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", - "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", "dev": true }, "which": { diff --git a/clients/webassembly/js-example/yarn.lock b/clients/webassembly/js-example/yarn.lock index ff3b6962f0..87ce0bf36d 100644 --- a/clients/webassembly/js-example/yarn.lock +++ b/clients/webassembly/js-example/yarn.lock @@ -2,6 +2,13 @@ # yarn lockfile v1 +"@nymproject/nym-client-wasm@^0.7.4": + version "0.7.5" + resolved "https://registry.yarnpkg.com/@nymproject/nym-client-wasm/-/nym-client-wasm-0.7.5.tgz#75bc6c426a93d12915f5e28862daafff34ff1f7e" + integrity sha512-r5RW4aSqyzWUvnO01EwqWdA0UGcULGrNSTe9eTDo+EoprCp1ab3vFpJs4pcg64DhxkbNbTgFVbi6TZVxObKhPQ== + dependencies: + semver "^7.3.2" + "@types/events@*": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/events/-/events-3.0.0.tgz#2862f3f58a9a7f7c3e78d79f130dd4d71c25c2a7" @@ -2448,9 +2455,6 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" -"nym-client-wasm@file:../pkg": - version "0.7.0-pre" - object-assign@^4.0.1, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -3064,6 +3068,11 @@ semver@^6.3.0: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== +semver@^7.3.2: + version "7.3.2" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" + integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== + send@0.17.1: version "0.17.1" resolved "https://registry.yarnpkg.com/send/-/send-0.17.1.tgz#c1d8b059f7900f7466dd4938bdc44e11ddb376c8" @@ -3804,9 +3813,9 @@ websocket-driver@>=0.5.1: websocket-extensions ">=0.1.1" websocket-extensions@>=0.1.1: - version "0.1.3" - resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.3.tgz#5d2ff22977003ec687a4b87073dfbbac146ccf29" - integrity sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg== + version "0.1.4" + resolved "https://registry.yarnpkg.com/websocket-extensions/-/websocket-extensions-0.1.4.tgz#7f8473bc839dfd87608adb95d7eb075211578a42" + integrity sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg== which-module@^2.0.0: version "2.0.0" From e0cf35a9c3e92422ea267fbf7366a8b2de152e4f Mon Sep 17 00:00:00 2001 From: Dave Hrycyszyn Date: Fri, 19 Jun 2020 16:01:48 +0100 Subject: [PATCH 09/76] Running npm audit on js examples (#269) --- .../js-examples/websocket/package-lock.json | 2176 ++++++++--------- .../js-examples/websocket/package.json | 2 +- 2 files changed, 1081 insertions(+), 1097 deletions(-) diff --git a/clients/native/examples/js-examples/websocket/package-lock.json b/clients/native/examples/js-examples/websocket/package-lock.json index 9708b35068..ca08d53be1 100644 --- a/clients/native/examples/js-examples/websocket/package-lock.json +++ b/clients/native/examples/js-examples/websocket/package-lock.json @@ -21,9 +21,9 @@ "integrity": "sha512-1Bh06cbWJUHMC97acuD6UMG29nMt0Aqz1vF3guLfG+kHHJhy3AyohZFFxYk2f7Q1SQIrNwvncxAE0N/9s70F2w==", "dev": true, "requires": { - "@types/events": "3.0.0", - "@types/minimatch": "3.0.3", - "@types/node": "13.11.1" + "@types/events": "*", + "@types/minimatch": "*", + "@types/node": "*" } }, "@types/html-minifier-terser": { @@ -57,7 +57,7 @@ "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.9.0.tgz", "integrity": "sha512-3ZcoyPYHVOCcLpnfZwD47KFLr8W/mpUcgjpf1M4Q78TMJIw7KMAHSjiCLJp1z3ZrBR9pTLbe191O0TldFK5zcw==", "requires": { - "source-map": "0.6.1" + "source-map": "^0.6.1" } }, "@types/webpack": { @@ -65,12 +65,12 @@ "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.12.tgz", "integrity": "sha512-BpCtM4NnBen6W+KEhrL9jKuZCXVtiH6+0b6cxdvNt2EwU949Al334PjQSl2BeAyvAX9mgoNNG21wvjP3xZJJ5w==", "requires": { - "@types/anymatch": "1.3.1", - "@types/node": "13.11.1", - "@types/tapable": "1.0.5", - "@types/uglify-js": "3.9.0", - "@types/webpack-sources": "0.1.7", - "source-map": "0.6.1" + "@types/anymatch": "*", + "@types/node": "*", + "@types/tapable": "*", + "@types/uglify-js": "*", + "@types/webpack-sources": "*", + "source-map": "^0.6.0" } }, "@types/webpack-sources": { @@ -78,9 +78,9 @@ "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-0.1.7.tgz", "integrity": "sha512-XyaHrJILjK1VHVC4aVlKsdNN5KBTwufMb43cQs+flGxtPAf/1Qwl8+Q0tp5BwEGaI8D6XT1L+9bSWXckgkjTLw==", "requires": { - "@types/node": "13.11.1", - "@types/source-list-map": "0.1.2", - "source-map": "0.6.1" + "@types/node": "*", + "@types/source-list-map": "*", + "source-map": "^0.6.1" } }, "@webassemblyjs/ast": { @@ -160,7 +160,7 @@ "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", "dev": true, "requires": { - "@xtuc/ieee754": "1.2.0" + "@xtuc/ieee754": "^1.2.0" } }, "@webassemblyjs/leb128": { @@ -276,7 +276,7 @@ "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", "dev": true, "requires": { - "mime-types": "2.1.26", + "mime-types": "~2.1.24", "negotiator": "0.6.2" } }, @@ -292,10 +292,10 @@ "integrity": "sha512-D6gFiFA0RRLyUbvijN74DWAjXSFxWKaWP7mldxkVhyhAV3+SWA9HEJPHQ2c9soIeTFJqcSdFDGFgdqs1iUU2Hw==", "dev": true, "requires": { - "fast-deep-equal": "3.1.1", - "fast-json-stable-stringify": "2.1.0", - "json-schema-traverse": "0.4.1", - "uri-js": "4.2.2" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" } }, "ajv-errors": { @@ -333,7 +333,7 @@ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "requires": { - "color-convert": "1.9.3" + "color-convert": "^1.9.0" } }, "anymatch": { @@ -342,8 +342,8 @@ "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", "dev": true, "requires": { - "micromatch": "3.1.10", - "normalize-path": "2.1.1" + "micromatch": "^3.1.4", + "normalize-path": "^2.1.1" }, "dependencies": { "normalize-path": { @@ -352,7 +352,7 @@ "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", "dev": true, "requires": { - "remove-trailing-separator": "1.1.0" + "remove-trailing-separator": "^1.0.1" } } } @@ -393,7 +393,7 @@ "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", "dev": true, "requires": { - "array-uniq": "1.0.3" + "array-uniq": "^1.0.1" } }, "array-uniq": { @@ -414,9 +414,9 @@ "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", "dev": true, "requires": { - "bn.js": "4.11.8", - "inherits": "2.0.4", - "minimalistic-assert": "1.0.1" + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" } }, "assert": { @@ -425,7 +425,7 @@ "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", "dev": true, "requires": { - "object-assign": "4.1.1", + "object-assign": "^4.1.1", "util": "0.10.3" }, "dependencies": { @@ -458,7 +458,7 @@ "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", "dev": true, "requires": { - "lodash": "4.17.15" + "lodash": "^4.17.14" } }, "async-each": { @@ -479,6 +479,21 @@ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true }, + "autoprefixer": { + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.0.tgz", + "integrity": "sha512-D96ZiIHXbDmU02dBaemyAg53ez+6F5yZmapmgKcjm35yEe1uVDYI8hGW3VYoGRaG290ZFf91YxHrR518vC0u/A==", + "dev": true, + "requires": { + "browserslist": "^4.12.0", + "caniuse-lite": "^1.0.30001061", + "chalk": "^2.4.2", + "normalize-range": "^0.1.2", + "num2fraction": "^1.2.2", + "postcss": "^7.0.30", + "postcss-value-parser": "^4.1.0" + } + }, "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", @@ -491,13 +506,13 @@ "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", "dev": true, "requires": { - "cache-base": "1.0.1", - "class-utils": "0.3.6", - "component-emitter": "1.3.0", - "define-property": "1.0.0", - "isobject": "3.0.1", - "mixin-deep": "1.3.2", - "pascalcase": "0.1.1" + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" }, "dependencies": { "define-property": { @@ -506,7 +521,7 @@ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dev": true, "requires": { - "is-descriptor": "1.0.2" + "is-descriptor": "^1.0.0" } }, "is-accessor-descriptor": { @@ -515,7 +530,7 @@ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dev": true, "requires": { - "kind-of": "6.0.3" + "kind-of": "^6.0.0" } }, "is-data-descriptor": { @@ -524,7 +539,7 @@ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dev": true, "requires": { - "kind-of": "6.0.3" + "kind-of": "^6.0.0" } }, "is-descriptor": { @@ -533,9 +548,9 @@ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dev": true, "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.3" + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" } } } @@ -592,15 +607,15 @@ "dev": true, "requires": { "bytes": "3.1.0", - "content-type": "1.0.4", + "content-type": "~1.0.4", "debug": "2.6.9", - "depd": "1.1.2", + "depd": "~1.1.2", "http-errors": "1.7.2", "iconv-lite": "0.4.24", - "on-finished": "2.3.0", + "on-finished": "~2.3.0", "qs": "6.7.0", "raw-body": "2.4.0", - "type-is": "1.6.18" + "type-is": "~1.6.17" }, "dependencies": { "bytes": { @@ -617,12 +632,12 @@ "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", "dev": true, "requires": { - "array-flatten": "2.1.2", - "deep-equal": "1.1.1", - "dns-equal": "1.0.0", - "dns-txt": "2.0.2", - "multicast-dns": "6.2.3", - "multicast-dns-service-types": "1.1.0" + "array-flatten": "^2.1.0", + "deep-equal": "^1.0.1", + "dns-equal": "^1.0.0", + "dns-txt": "^2.0.2", + "multicast-dns": "^6.0.1", + "multicast-dns-service-types": "^1.1.0" } }, "boolbase": { @@ -636,7 +651,7 @@ "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "requires": { - "balanced-match": "1.0.0", + "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, @@ -646,16 +661,16 @@ "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", "dev": true, "requires": { - "arr-flatten": "1.1.0", - "array-unique": "0.3.2", - "extend-shallow": "2.0.1", - "fill-range": "4.0.0", - "isobject": "3.0.1", - "repeat-element": "1.1.3", - "snapdragon": "0.8.2", - "snapdragon-node": "2.1.1", - "split-string": "3.1.0", - "to-regex": "3.0.2" + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" }, "dependencies": { "extend-shallow": { @@ -664,7 +679,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } } } @@ -681,12 +696,12 @@ "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", "dev": true, "requires": { - "buffer-xor": "1.0.3", - "cipher-base": "1.0.4", - "create-hash": "1.2.0", - "evp_bytestokey": "1.0.3", - "inherits": "2.0.4", - "safe-buffer": "5.1.2" + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" } }, "browserify-cipher": { @@ -695,9 +710,9 @@ "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", "dev": true, "requires": { - "browserify-aes": "1.2.0", - "browserify-des": "1.0.2", - "evp_bytestokey": "1.0.3" + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" } }, "browserify-des": { @@ -706,10 +721,10 @@ "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", "dev": true, "requires": { - "cipher-base": "1.0.4", - "des.js": "1.0.1", - "inherits": "2.0.4", - "safe-buffer": "5.1.2" + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" } }, "browserify-rsa": { @@ -718,8 +733,8 @@ "integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=", "dev": true, "requires": { - "bn.js": "4.11.8", - "randombytes": "2.1.0" + "bn.js": "^4.1.0", + "randombytes": "^2.0.1" } }, "browserify-sign": { @@ -728,13 +743,13 @@ "integrity": "sha1-qk62jl17ZYuqa/alfmMMvXqT0pg=", "dev": true, "requires": { - "bn.js": "4.11.8", - "browserify-rsa": "4.0.1", - "create-hash": "1.2.0", - "create-hmac": "1.1.7", - "elliptic": "6.5.2", - "inherits": "2.0.4", - "parse-asn1": "5.1.5" + "bn.js": "^4.1.1", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.2", + "elliptic": "^6.0.0", + "inherits": "^2.0.1", + "parse-asn1": "^5.0.0" } }, "browserify-zlib": { @@ -743,7 +758,19 @@ "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", "dev": true, "requires": { - "pako": "1.0.11" + "pako": "~1.0.5" + } + }, + "browserslist": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.12.0.tgz", + "integrity": "sha512-UH2GkcEDSI0k/lRkuDSzFl9ZZ87skSy9w2XAn1MsZnL+4c4rqbBd3e82UWHbYDpztABrPBhZsTEeuxVfHppqDg==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001043", + "electron-to-chromium": "^1.3.413", + "node-releases": "^1.1.53", + "pkg-up": "^2.0.0" } }, "buffer": { @@ -752,9 +779,9 @@ "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", "dev": true, "requires": { - "base64-js": "1.3.1", - "ieee754": "1.1.13", - "isarray": "1.0.0" + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" } }, "buffer-from": { @@ -792,21 +819,21 @@ "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", "dev": true, "requires": { - "bluebird": "3.7.2", - "chownr": "1.1.4", - "figgy-pudding": "3.5.2", - "glob": "7.1.6", - "graceful-fs": "4.2.3", - "infer-owner": "1.0.4", - "lru-cache": "5.1.1", - "mississippi": "3.0.0", - "mkdirp": "0.5.5", - "move-concurrently": "1.0.1", - "promise-inflight": "1.0.1", - "rimraf": "2.7.1", - "ssri": "6.0.1", - "unique-filename": "1.1.1", - "y18n": "4.0.0" + "bluebird": "^3.5.5", + "chownr": "^1.1.1", + "figgy-pudding": "^3.5.1", + "glob": "^7.1.4", + "graceful-fs": "^4.1.15", + "infer-owner": "^1.0.3", + "lru-cache": "^5.1.1", + "mississippi": "^3.0.0", + "mkdirp": "^0.5.1", + "move-concurrently": "^1.0.1", + "promise-inflight": "^1.0.1", + "rimraf": "^2.6.3", + "ssri": "^6.0.1", + "unique-filename": "^1.1.1", + "y18n": "^4.0.0" } }, "cache-base": { @@ -815,15 +842,15 @@ "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", "dev": true, "requires": { - "collection-visit": "1.0.0", - "component-emitter": "1.3.0", - "get-value": "2.0.6", - "has-value": "1.0.0", - "isobject": "3.0.1", - "set-value": "2.0.1", - "to-object-path": "0.3.0", - "union-value": "1.0.1", - "unset-value": "1.0.0" + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" } }, "camel-case": { @@ -831,8 +858,8 @@ "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.1.tgz", "integrity": "sha512-7fa2WcG4fYFkclIvEmxBbTvmibwF2/agfEBc6q3lOpVu0A13ltLsA+Hr/8Hp6kp5f+G7hKi6t8lys6XxP+1K6Q==", "requires": { - "pascal-case": "3.1.1", - "tslib": "1.11.1" + "pascal-case": "^3.1.1", + "tslib": "^1.10.0" } }, "camelcase": { @@ -841,15 +868,21 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, + "caniuse-lite": { + "version": "1.0.30001084", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001084.tgz", + "integrity": "sha512-ftdc5oGmhEbLUuMZ/Qp3mOpzfZLCxPYKcvGv6v2dJJ+8EdqcvZRbAGOiLmkM/PV1QGta/uwBs8/nCl6sokDW6w==", + "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.5.0" + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" }, "dependencies": { "supports-color": { @@ -858,7 +891,7 @@ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "requires": { - "has-flag": "3.0.0" + "has-flag": "^3.0.0" } } } @@ -869,18 +902,18 @@ "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", "dev": true, "requires": { - "anymatch": "2.0.0", - "async-each": "1.0.3", - "braces": "2.3.2", - "fsevents": "1.2.12", - "glob-parent": "3.1.0", - "inherits": "2.0.4", - "is-binary-path": "1.0.1", - "is-glob": "4.0.1", - "normalize-path": "3.0.0", - "path-is-absolute": "1.0.1", - "readdirp": "2.2.1", - "upath": "1.2.0" + "anymatch": "^2.0.0", + "async-each": "^1.0.1", + "braces": "^2.3.2", + "fsevents": "^1.2.7", + "glob-parent": "^3.1.0", + "inherits": "^2.0.3", + "is-binary-path": "^1.0.0", + "is-glob": "^4.0.0", + "normalize-path": "^3.0.0", + "path-is-absolute": "^1.0.0", + "readdirp": "^2.2.1", + "upath": "^1.1.1" } }, "chownr": { @@ -895,7 +928,7 @@ "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", "dev": true, "requires": { - "tslib": "1.11.1" + "tslib": "^1.9.0" } }, "cipher-base": { @@ -904,8 +937,8 @@ "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", "dev": true, "requires": { - "inherits": "2.0.4", - "safe-buffer": "5.1.2" + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" } }, "class-utils": { @@ -914,10 +947,10 @@ "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", "dev": true, "requires": { - "arr-union": "3.1.0", - "define-property": "0.2.5", - "isobject": "3.0.1", - "static-extend": "0.1.2" + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" }, "dependencies": { "define-property": { @@ -926,7 +959,7 @@ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "0.1.6" + "is-descriptor": "^0.1.0" } } } @@ -936,7 +969,7 @@ "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.3.tgz", "integrity": "sha512-VcMWDN54ZN/DS+g58HYL5/n4Zrqe8vHJpGA8KdgUXFU4fuP/aHNw8eld9SyEIyabIMJX/0RaY/fplOo5hYLSFA==", "requires": { - "source-map": "0.6.1" + "source-map": "~0.6.0" } }, "clean-webpack-plugin": { @@ -945,8 +978,8 @@ "integrity": "sha512-MciirUH5r+cYLGCOL5JX/ZLzOZbVr1ot3Fw+KcvbhUb6PM+yycqd9ZhIlcigQ5gl+XhppNmw3bEFuaaMNyLj3A==", "dev": true, "requires": { - "@types/webpack": "4.41.12", - "del": "4.1.1" + "@types/webpack": "^4.4.31", + "del": "^4.1.1" } }, "cliui": { @@ -955,9 +988,9 @@ "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "dev": true, "requires": { - "string-width": "3.1.0", - "strip-ansi": "5.2.0", - "wrap-ansi": "5.1.0" + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" }, "dependencies": { "ansi-regex": { @@ -972,25 +1005,19 @@ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, "requires": { - "ansi-regex": "4.1.0" + "ansi-regex": "^4.1.0" } } } }, - "code-point-at": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", - "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true - }, "collection-visit": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", "dev": true, "requires": { - "map-visit": "1.0.0", - "object-visit": "1.0.1" + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" } }, "color-convert": { @@ -1031,7 +1058,7 @@ "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "dev": true, "requires": { - "mime-db": "1.43.0" + "mime-db": ">= 1.43.0 < 2" } }, "compression": { @@ -1040,13 +1067,13 @@ "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", "dev": true, "requires": { - "accepts": "1.3.7", + "accepts": "~1.3.5", "bytes": "3.0.0", - "compressible": "2.0.18", + "compressible": "~2.0.16", "debug": "2.6.9", - "on-headers": "1.0.2", + "on-headers": "~1.0.2", "safe-buffer": "5.1.2", - "vary": "1.1.2" + "vary": "~1.1.2" } }, "concat-map": { @@ -1061,10 +1088,10 @@ "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", "dev": true, "requires": { - "buffer-from": "1.1.1", - "inherits": "2.0.4", - "readable-stream": "2.3.7", - "typedarray": "0.0.6" + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" } }, "connect-history-api-fallback": { @@ -1118,12 +1145,12 @@ "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", "dev": true, "requires": { - "aproba": "1.2.0", - "fs-write-stream-atomic": "1.0.10", - "iferr": "0.1.5", - "mkdirp": "0.5.5", - "rimraf": "2.7.1", - "run-queue": "1.0.3" + "aproba": "^1.1.1", + "fs-write-stream-atomic": "^1.0.8", + "iferr": "^0.1.5", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.0" } }, "copy-descriptor": { @@ -1149,8 +1176,8 @@ "integrity": "sha512-GbEHQPMOswGpKXM9kCWVrremUcBmjteUaQ01T9rkKCPDXfUHX0IoP9LpHYo2NPFampa4e+/pFDc3jQdxrxQLaw==", "dev": true, "requires": { - "bn.js": "4.11.8", - "elliptic": "6.5.2" + "bn.js": "^4.1.0", + "elliptic": "^6.0.0" } }, "create-hash": { @@ -1159,11 +1186,11 @@ "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", "dev": true, "requires": { - "cipher-base": "1.0.4", - "inherits": "2.0.4", - "md5.js": "1.3.5", - "ripemd160": "2.0.2", - "sha.js": "2.4.11" + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" } }, "create-hmac": { @@ -1172,12 +1199,12 @@ "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", "dev": true, "requires": { - "cipher-base": "1.0.4", - "create-hash": "1.2.0", - "inherits": "2.0.4", - "ripemd160": "2.0.2", - "safe-buffer": "5.1.2", - "sha.js": "2.4.11" + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" } }, "cross-spawn": { @@ -1186,11 +1213,11 @@ "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", "dev": true, "requires": { - "nice-try": "1.0.5", - "path-key": "2.0.1", - "semver": "5.7.1", - "shebang-command": "1.2.0", - "which": "1.3.1" + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" }, "dependencies": { "semver": { @@ -1207,17 +1234,17 @@ "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", "dev": true, "requires": { - "browserify-cipher": "1.0.1", - "browserify-sign": "4.0.4", - "create-ecdh": "4.0.3", - "create-hash": "1.2.0", - "create-hmac": "1.1.7", - "diffie-hellman": "5.0.3", - "inherits": "2.0.4", - "pbkdf2": "3.0.17", - "public-encrypt": "4.0.3", - "randombytes": "2.1.0", - "randomfill": "1.0.4" + "browserify-cipher": "^1.0.0", + "browserify-sign": "^4.0.0", + "create-ecdh": "^4.0.0", + "create-hash": "^1.1.0", + "create-hmac": "^1.1.0", + "diffie-hellman": "^5.0.0", + "inherits": "^2.0.1", + "pbkdf2": "^3.0.3", + "public-encrypt": "^4.0.0", + "randombytes": "^2.0.0", + "randomfill": "^1.0.3" } }, "css-select": { @@ -1225,10 +1252,10 @@ "resolved": "https://registry.npmjs.org/css-select/-/css-select-1.2.0.tgz", "integrity": "sha1-KzoRBTnFNV8c2NMUYj6HCxIeyFg=", "requires": { - "boolbase": "1.0.0", - "css-what": "2.1.3", + "boolbase": "~1.0.0", + "css-what": "2.1", "domutils": "1.5.1", - "nth-check": "1.0.2" + "nth-check": "~1.0.1" } }, "css-what": { @@ -1269,12 +1296,12 @@ "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", "dev": true, "requires": { - "is-arguments": "1.0.4", - "is-date-object": "1.0.2", - "is-regex": "1.0.5", - "object-is": "1.1.2", - "object-keys": "1.1.1", - "regexp.prototype.flags": "1.3.0" + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" } }, "default-gateway": { @@ -1283,8 +1310,8 @@ "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", "dev": true, "requires": { - "execa": "1.0.0", - "ip-regex": "2.1.0" + "execa": "^1.0.0", + "ip-regex": "^2.1.0" } }, "define-properties": { @@ -1292,7 +1319,7 @@ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", "requires": { - "object-keys": "1.1.1" + "object-keys": "^1.0.12" } }, "define-property": { @@ -1301,8 +1328,8 @@ "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", "dev": true, "requires": { - "is-descriptor": "1.0.2", - "isobject": "3.0.1" + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" }, "dependencies": { "is-accessor-descriptor": { @@ -1311,7 +1338,7 @@ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dev": true, "requires": { - "kind-of": "6.0.3" + "kind-of": "^6.0.0" } }, "is-data-descriptor": { @@ -1320,7 +1347,7 @@ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dev": true, "requires": { - "kind-of": "6.0.3" + "kind-of": "^6.0.0" } }, "is-descriptor": { @@ -1329,9 +1356,9 @@ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dev": true, "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.3" + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" } } } @@ -1342,13 +1369,13 @@ "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", "dev": true, "requires": { - "@types/glob": "7.1.1", - "globby": "6.1.0", - "is-path-cwd": "2.2.0", - "is-path-in-cwd": "2.1.0", - "p-map": "2.1.0", - "pify": "4.0.1", - "rimraf": "2.7.1" + "@types/glob": "^7.1.1", + "globby": "^6.1.0", + "is-path-cwd": "^2.0.0", + "is-path-in-cwd": "^2.0.0", + "p-map": "^2.0.0", + "pify": "^4.0.1", + "rimraf": "^2.6.3" } }, "depd": { @@ -1363,8 +1390,8 @@ "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", "dev": true, "requires": { - "inherits": "2.0.4", - "minimalistic-assert": "1.0.1" + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" } }, "destroy": { @@ -1391,9 +1418,9 @@ "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", "dev": true, "requires": { - "bn.js": "4.11.8", - "miller-rabin": "4.0.1", - "randombytes": "2.1.0" + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" } }, "dns-equal": { @@ -1408,8 +1435,8 @@ "integrity": "sha512-0UxfQkMhYAUaZI+xrNZOz/as5KgDU0M/fQ9b6SpkyLbk3GEswDi6PADJVaYJradtRVsRIlF1zLyOodbcTCDzUg==", "dev": true, "requires": { - "ip": "1.1.5", - "safe-buffer": "5.1.2" + "ip": "^1.1.0", + "safe-buffer": "^5.0.1" } }, "dns-txt": { @@ -1418,7 +1445,7 @@ "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", "dev": true, "requires": { - "buffer-indexof": "1.1.1" + "buffer-indexof": "^1.0.0" } }, "dom-converter": { @@ -1426,7 +1453,7 @@ "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", "requires": { - "utila": "0.4.0" + "utila": "~0.4" } }, "dom-serializer": { @@ -1434,8 +1461,8 @@ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", "requires": { - "domelementtype": "2.0.1", - "entities": "2.0.0" + "domelementtype": "^2.0.1", + "entities": "^2.0.0" }, "dependencies": { "domelementtype": { @@ -1461,7 +1488,7 @@ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", "requires": { - "domelementtype": "1.3.1" + "domelementtype": "1" } }, "domutils": { @@ -1469,8 +1496,8 @@ "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", "requires": { - "dom-serializer": "0.2.2", - "domelementtype": "1.3.1" + "dom-serializer": "0", + "domelementtype": "1" } }, "dot-case": { @@ -1478,8 +1505,8 @@ "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.3.tgz", "integrity": "sha512-7hwEmg6RiSQfm/GwPL4AAWXKy3YNNZA3oFv2Pdiey0mwkRCPZ9x6SZbkLcn8Ma5PYeVokzoD4Twv2n7LKp5WeA==", "requires": { - "no-case": "3.0.3", - "tslib": "1.11.1" + "no-case": "^3.0.3", + "tslib": "^1.10.0" } }, "duplexify": { @@ -1488,10 +1515,10 @@ "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", "dev": true, "requires": { - "end-of-stream": "1.4.4", - "inherits": "2.0.4", - "readable-stream": "2.3.7", - "stream-shift": "1.0.1" + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" } }, "ee-first": { @@ -1500,19 +1527,25 @@ "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=", "dev": true }, + "electron-to-chromium": { + "version": "1.3.477", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.477.tgz", + "integrity": "sha512-81p6DZ/XmHDD7O0ITJMa7ESo9bSCfE+v3Fny3MIYR0y77xmhoriu2ShNOLXcPS4eowF6dkxw6d2QqxTkS3DjBg==", + "dev": true + }, "elliptic": { "version": "6.5.2", "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", "dev": true, "requires": { - "bn.js": "4.11.8", - "brorand": "1.1.0", - "hash.js": "1.1.7", - "hmac-drbg": "1.0.1", - "inherits": "2.0.4", - "minimalistic-assert": "1.0.1", - "minimalistic-crypto-utils": "1.0.1" + "bn.js": "^4.4.0", + "brorand": "^1.0.1", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.0" } }, "emoji-regex": { @@ -1538,7 +1571,7 @@ "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", "dev": true, "requires": { - "once": "1.4.0" + "once": "^1.4.0" } }, "enhanced-resolve": { @@ -1547,9 +1580,9 @@ "integrity": "sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA==", "dev": true, "requires": { - "graceful-fs": "4.2.3", - "memory-fs": "0.5.0", - "tapable": "1.1.3" + "graceful-fs": "^4.1.2", + "memory-fs": "^0.5.0", + "tapable": "^1.0.0" }, "dependencies": { "memory-fs": { @@ -1558,8 +1591,8 @@ "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", "dev": true, "requires": { - "errno": "0.1.7", - "readable-stream": "2.3.7" + "errno": "^0.1.3", + "readable-stream": "^2.0.1" } } } @@ -1575,7 +1608,7 @@ "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", "dev": true, "requires": { - "prr": "1.0.1" + "prr": "~1.0.1" } }, "es-abstract": { @@ -1583,17 +1616,17 @@ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.5.tgz", "integrity": "sha512-BR9auzDbySxOcfog0tLECW8l28eRGpDpU3Dm3Hp4q/N+VtLTmyj4EUN088XZWQDW/hzj6sYRDXeOFsaAODKvpg==", "requires": { - "es-to-primitive": "1.2.1", - "function-bind": "1.1.1", - "has": "1.0.3", - "has-symbols": "1.0.1", - "is-callable": "1.1.5", - "is-regex": "1.0.5", - "object-inspect": "1.7.0", - "object-keys": "1.1.1", - "object.assign": "4.1.0", - "string.prototype.trimleft": "2.1.2", - "string.prototype.trimright": "2.1.2" + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.1.5", + "is-regex": "^1.0.5", + "object-inspect": "^1.7.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.0", + "string.prototype.trimleft": "^2.1.1", + "string.prototype.trimright": "^2.1.1" } }, "es-to-primitive": { @@ -1601,9 +1634,9 @@ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", "requires": { - "is-callable": "1.1.5", - "is-date-object": "1.0.2", - "is-symbol": "1.0.3" + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" } }, "escape-html": { @@ -1624,8 +1657,8 @@ "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", "dev": true, "requires": { - "esrecurse": "4.2.1", - "estraverse": "4.3.0" + "esrecurse": "^4.1.0", + "estraverse": "^4.1.1" } }, "esrecurse": { @@ -1634,7 +1667,7 @@ "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", "dev": true, "requires": { - "estraverse": "4.3.0" + "estraverse": "^4.1.0" } }, "estraverse": { @@ -1650,9 +1683,9 @@ "dev": true }, "eventemitter3": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz", - "integrity": "sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.4.tgz", + "integrity": "sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ==", "dev": true }, "events": { @@ -1667,7 +1700,7 @@ "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", "dev": true, "requires": { - "original": "1.0.2" + "original": "^1.0.0" } }, "evp_bytestokey": { @@ -1676,8 +1709,8 @@ "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", "dev": true, "requires": { - "md5.js": "1.3.5", - "safe-buffer": "5.1.2" + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" } }, "execa": { @@ -1686,13 +1719,13 @@ "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", "dev": true, "requires": { - "cross-spawn": "6.0.5", - "get-stream": "4.1.0", - "is-stream": "1.1.0", - "npm-run-path": "2.0.2", - "p-finally": "1.0.0", - "signal-exit": "3.0.3", - "strip-eof": "1.0.0" + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" } }, "expand-brackets": { @@ -1701,13 +1734,13 @@ "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", "dev": true, "requires": { - "debug": "2.6.9", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "posix-character-classes": "0.1.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" }, "dependencies": { "define-property": { @@ -1716,7 +1749,7 @@ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "0.1.6" + "is-descriptor": "^0.1.0" } }, "extend-shallow": { @@ -1725,7 +1758,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } } } @@ -1736,7 +1769,7 @@ "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", "dev": true, "requires": { - "homedir-polyfill": "1.0.3" + "homedir-polyfill": "^1.0.1" } }, "express": { @@ -1745,36 +1778,36 @@ "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", "dev": true, "requires": { - "accepts": "1.3.7", + "accepts": "~1.3.7", "array-flatten": "1.1.1", "body-parser": "1.19.0", "content-disposition": "0.5.3", - "content-type": "1.0.4", + "content-type": "~1.0.4", "cookie": "0.4.0", "cookie-signature": "1.0.6", "debug": "2.6.9", - "depd": "1.1.2", - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "etag": "1.8.1", - "finalhandler": "1.1.2", + "depd": "~1.1.2", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.1.2", "fresh": "0.5.2", "merge-descriptors": "1.0.1", - "methods": "1.1.2", - "on-finished": "2.3.0", - "parseurl": "1.3.3", + "methods": "~1.1.2", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", "path-to-regexp": "0.1.7", - "proxy-addr": "2.0.6", + "proxy-addr": "~2.0.5", "qs": "6.7.0", - "range-parser": "1.2.1", + "range-parser": "~1.2.1", "safe-buffer": "5.1.2", "send": "0.17.1", "serve-static": "1.14.1", "setprototypeof": "1.1.1", - "statuses": "1.5.0", - "type-is": "1.6.18", + "statuses": "~1.5.0", + "type-is": "~1.6.18", "utils-merge": "1.0.1", - "vary": "1.1.2" + "vary": "~1.1.2" }, "dependencies": { "array-flatten": { @@ -1791,8 +1824,8 @@ "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", "dev": true, "requires": { - "assign-symbols": "1.0.0", - "is-extendable": "1.0.1" + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" }, "dependencies": { "is-extendable": { @@ -1801,7 +1834,7 @@ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dev": true, "requires": { - "is-plain-object": "2.0.4" + "is-plain-object": "^2.0.4" } } } @@ -1812,14 +1845,14 @@ "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", "dev": true, "requires": { - "array-unique": "0.3.2", - "define-property": "1.0.0", - "expand-brackets": "2.1.4", - "extend-shallow": "2.0.1", - "fragment-cache": "0.2.1", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" }, "dependencies": { "define-property": { @@ -1828,7 +1861,7 @@ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dev": true, "requires": { - "is-descriptor": "1.0.2" + "is-descriptor": "^1.0.0" } }, "extend-shallow": { @@ -1837,7 +1870,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } }, "is-accessor-descriptor": { @@ -1846,7 +1879,7 @@ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dev": true, "requires": { - "kind-of": "6.0.3" + "kind-of": "^6.0.0" } }, "is-data-descriptor": { @@ -1855,7 +1888,7 @@ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dev": true, "requires": { - "kind-of": "6.0.3" + "kind-of": "^6.0.0" } }, "is-descriptor": { @@ -1864,9 +1897,9 @@ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dev": true, "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.3" + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" } } } @@ -1889,7 +1922,7 @@ "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", "dev": true, "requires": { - "websocket-driver": "0.7.3" + "websocket-driver": ">=0.5.1" } }, "figgy-pudding": { @@ -1911,10 +1944,10 @@ "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", "dev": true, "requires": { - "extend-shallow": "2.0.1", - "is-number": "3.0.0", - "repeat-string": "1.6.1", - "to-regex-range": "2.1.1" + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" }, "dependencies": { "extend-shallow": { @@ -1923,7 +1956,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } } } @@ -1935,12 +1968,12 @@ "dev": true, "requires": { "debug": "2.6.9", - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "on-finished": "2.3.0", - "parseurl": "1.3.3", - "statuses": "1.5.0", - "unpipe": "1.0.0" + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" } }, "find-cache-dir": { @@ -1949,9 +1982,9 @@ "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", "dev": true, "requires": { - "commondir": "1.0.1", - "make-dir": "2.1.0", - "pkg-dir": "3.0.0" + "commondir": "^1.0.1", + "make-dir": "^2.0.0", + "pkg-dir": "^3.0.0" } }, "find-up": { @@ -1960,7 +1993,7 @@ "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", "dev": true, "requires": { - "locate-path": "3.0.0" + "locate-path": "^3.0.0" } }, "findup-sync": { @@ -1969,10 +2002,10 @@ "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", "dev": true, "requires": { - "detect-file": "1.0.0", - "is-glob": "4.0.1", - "micromatch": "3.1.10", - "resolve-dir": "1.0.1" + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" } }, "flush-write-stream": { @@ -1981,35 +2014,15 @@ "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", "dev": true, "requires": { - "inherits": "2.0.4", - "readable-stream": "2.3.7" + "inherits": "^2.0.3", + "readable-stream": "^2.3.6" } }, "follow-redirects": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.11.0.tgz", - "integrity": "sha512-KZm0V+ll8PfBrKwMzdo5D13b1bur9Iq9Zd/RMmAoQQcl2PxxFml8cxXPaaPYVbV0RjNjq1CU7zIzAOqtUPudmA==", - "dev": true, - "requires": { - "debug": "3.2.6" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } - } + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.12.0.tgz", + "integrity": "sha512-JgawlbfBQKjbKegPn8vUsvJqplE7KHJuhGO4yPcb+ZOIYKSr+xobMVlfRBToZwZUUxy7lFiKBdFNloz9ui368Q==", + "dev": true }, "for-in": { "version": "1.0.2", @@ -2029,7 +2042,7 @@ "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", "dev": true, "requires": { - "map-cache": "0.2.2" + "map-cache": "^0.2.2" } }, "fresh": { @@ -2044,8 +2057,8 @@ "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", "dev": true, "requires": { - "inherits": "2.0.4", - "readable-stream": "2.3.7" + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" } }, "fs-write-stream-atomic": { @@ -2054,10 +2067,10 @@ "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", "dev": true, "requires": { - "graceful-fs": "4.2.3", - "iferr": "0.1.5", - "imurmurhash": "0.1.4", - "readable-stream": "2.3.7" + "graceful-fs": "^4.1.2", + "iferr": "^0.1.5", + "imurmurhash": "^0.1.4", + "readable-stream": "1 || 2" } }, "fs.realpath": { @@ -2073,9 +2086,9 @@ "dev": true, "optional": true, "requires": { - "bindings": "1.5.0", - "nan": "2.14.0", - "node-pre-gyp": "0.14.0" + "bindings": "^1.5.0", + "nan": "^2.12.1", + "node-pre-gyp": "*" }, "dependencies": { "abbrev": { @@ -2634,7 +2647,7 @@ "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", "dev": true, "requires": { - "pump": "3.0.0" + "pump": "^3.0.0" } }, "get-value": { @@ -2649,12 +2662,12 @@ "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", "dev": true, "requires": { - "fs.realpath": "1.0.0", - "inflight": "1.0.6", - "inherits": "2.0.4", - "minimatch": "3.0.4", - "once": "1.4.0", - "path-is-absolute": "1.0.1" + "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" } }, "glob-parent": { @@ -2663,8 +2676,8 @@ "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", "dev": true, "requires": { - "is-glob": "3.1.0", - "path-dirname": "1.0.2" + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" }, "dependencies": { "is-glob": { @@ -2673,7 +2686,7 @@ "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", "dev": true, "requires": { - "is-extglob": "2.1.1" + "is-extglob": "^2.1.0" } } } @@ -2684,7 +2697,7 @@ "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", "dev": true, "requires": { - "global-prefix": "3.0.0" + "global-prefix": "^3.0.0" }, "dependencies": { "global-prefix": { @@ -2693,9 +2706,9 @@ "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", "dev": true, "requires": { - "ini": "1.3.5", - "kind-of": "6.0.3", - "which": "1.3.1" + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" } } } @@ -2706,11 +2719,11 @@ "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", "dev": true, "requires": { - "expand-tilde": "2.0.2", - "homedir-polyfill": "1.0.3", - "ini": "1.3.5", - "is-windows": "1.0.2", - "which": "1.3.1" + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" } }, "globby": { @@ -2719,11 +2732,11 @@ "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", "dev": true, "requires": { - "array-union": "1.0.2", - "glob": "7.1.6", - "object-assign": "4.1.1", - "pify": "2.3.0", - "pinkie-promise": "2.0.1" + "array-union": "^1.0.1", + "glob": "^7.0.3", + "object-assign": "^4.0.1", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" }, "dependencies": { "pify": { @@ -2751,7 +2764,7 @@ "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "requires": { - "function-bind": "1.1.1" + "function-bind": "^1.1.1" } }, "has-flag": { @@ -2771,9 +2784,9 @@ "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", "dev": true, "requires": { - "get-value": "2.0.6", - "has-values": "1.0.0", - "isobject": "3.0.1" + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" } }, "has-values": { @@ -2782,8 +2795,8 @@ "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", "dev": true, "requires": { - "is-number": "3.0.0", - "kind-of": "4.0.0" + "is-number": "^3.0.0", + "kind-of": "^4.0.0" }, "dependencies": { "kind-of": { @@ -2792,7 +2805,7 @@ "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -2803,8 +2816,8 @@ "integrity": "sha1-X8hoaEfs1zSZQDMZprCj8/auSRg=", "dev": true, "requires": { - "inherits": "2.0.4", - "safe-buffer": "5.1.2" + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" } }, "hash.js": { @@ -2813,8 +2826,8 @@ "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", "dev": true, "requires": { - "inherits": "2.0.4", - "minimalistic-assert": "1.0.1" + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" } }, "he": { @@ -2828,9 +2841,9 @@ "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", "dev": true, "requires": { - "hash.js": "1.1.7", - "minimalistic-assert": "1.0.1", - "minimalistic-crypto-utils": "1.0.1" + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" } }, "homedir-polyfill": { @@ -2839,7 +2852,7 @@ "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", "dev": true, "requires": { - "parse-passwd": "1.0.0" + "parse-passwd": "^1.0.0" } }, "hpack.js": { @@ -2848,10 +2861,10 @@ "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", "dev": true, "requires": { - "inherits": "2.0.4", - "obuf": "1.1.2", - "readable-stream": "2.3.7", - "wbuf": "1.7.3" + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" } }, "html-entities": { @@ -2865,13 +2878,13 @@ "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-5.0.5.tgz", "integrity": "sha512-cBSFFghQh/uHcfSiL42KxxIRMF7A144+3E44xdlctIjxEmkEfCvouxNyFH2wysXk1fCGBPwtcr3hDWlGTfkDew==", "requires": { - "camel-case": "4.1.1", - "clean-css": "4.2.3", - "commander": "4.1.1", - "he": "1.2.0", - "param-case": "3.0.3", - "relateurl": "0.2.7", - "terser": "4.6.11" + "camel-case": "^4.1.1", + "clean-css": "^4.2.3", + "commander": "^4.1.1", + "he": "^1.2.0", + "param-case": "^3.0.3", + "relateurl": "^0.2.7", + "terser": "^4.6.3" }, "dependencies": { "commander": { @@ -2886,14 +2899,14 @@ "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-4.2.0.tgz", "integrity": "sha512-zL7LYTuq/fcJX6vV6tmmvFR508Bd9e6kvVGbS76YAjZ2CPVRzsjkvDYs/SshPevpolSdTWgaDV39D6k6oQoVFw==", "requires": { - "@types/html-minifier-terser": "5.0.0", - "@types/tapable": "1.0.5", - "@types/webpack": "4.41.12", - "html-minifier-terser": "5.0.5", - "loader-utils": "1.4.0", - "lodash": "4.17.15", - "pretty-error": "2.1.1", - "tapable": "1.1.3", + "@types/html-minifier-terser": "^5.0.0", + "@types/tapable": "^1.0.5", + "@types/webpack": "^4.41.8", + "html-minifier-terser": "^5.0.1", + "loader-utils": "^1.2.3", + "lodash": "^4.17.15", + "pretty-error": "^2.1.1", + "tapable": "^1.1.3", "util.promisify": "1.0.0" } }, @@ -2902,12 +2915,12 @@ "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", "requires": { - "domelementtype": "1.3.1", - "domhandler": "2.4.2", - "domutils": "1.5.1", - "entities": "1.1.2", - "inherits": "2.0.4", - "readable-stream": "3.6.0" + "domelementtype": "^1.3.1", + "domhandler": "^2.3.0", + "domutils": "^1.5.1", + "entities": "^1.1.1", + "inherits": "^2.0.1", + "readable-stream": "^3.1.1" }, "dependencies": { "entities": { @@ -2920,9 +2933,9 @@ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "requires": { - "inherits": "2.0.4", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" } } } @@ -2939,10 +2952,10 @@ "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", "dev": true, "requires": { - "depd": "1.1.2", + "depd": "~1.1.2", "inherits": "2.0.3", "setprototypeof": "1.1.1", - "statuses": "1.5.0", + "statuses": ">= 1.5.0 < 2", "toidentifier": "1.0.0" }, "dependencies": { @@ -2954,21 +2967,15 @@ } } }, - "http-parser-js": { - "version": "0.4.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz", - "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=", - "dev": true - }, "http-proxy": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz", - "integrity": "sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "dev": true, "requires": { - "eventemitter3": "4.0.0", - "follow-redirects": "1.11.0", - "requires-port": "1.0.0" + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" } }, "http-proxy-middleware": { @@ -2977,10 +2984,10 @@ "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", "dev": true, "requires": { - "http-proxy": "1.18.0", - "is-glob": "4.0.1", - "lodash": "4.17.15", - "micromatch": "3.1.10" + "http-proxy": "^1.17.0", + "is-glob": "^4.0.0", + "lodash": "^4.17.11", + "micromatch": "^3.1.10" } }, "https-browserify": { @@ -2995,7 +3002,7 @@ "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, "requires": { - "safer-buffer": "2.1.2" + "safer-buffer": ">= 2.1.2 < 3" } }, "ieee754": { @@ -3016,8 +3023,8 @@ "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", "dev": true, "requires": { - "pkg-dir": "3.0.0", - "resolve-cwd": "2.0.0" + "pkg-dir": "^3.0.0", + "resolve-cwd": "^2.0.0" } }, "imurmurhash": { @@ -3038,8 +3045,8 @@ "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", "dev": true, "requires": { - "once": "1.4.0", - "wrappy": "1.0.2" + "once": "^1.3.0", + "wrappy": "1" } }, "inherits": { @@ -3059,8 +3066,8 @@ "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", "dev": true, "requires": { - "default-gateway": "4.2.0", - "ipaddr.js": "1.9.1" + "default-gateway": "^4.2.0", + "ipaddr.js": "^1.9.0" } }, "interpret": { @@ -3105,7 +3112,7 @@ "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { @@ -3114,7 +3121,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -3131,7 +3138,7 @@ "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", "dev": true, "requires": { - "binary-extensions": "1.13.1" + "binary-extensions": "^1.0.0" } }, "is-buffer": { @@ -3151,7 +3158,7 @@ "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { @@ -3160,7 +3167,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -3176,9 +3183,9 @@ "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", "dev": true, "requires": { - "is-accessor-descriptor": "0.1.6", - "is-data-descriptor": "0.1.4", - "kind-of": "5.1.0" + "is-accessor-descriptor": "^0.1.6", + "is-data-descriptor": "^0.1.4", + "kind-of": "^5.0.0" }, "dependencies": { "kind-of": { @@ -3213,7 +3220,7 @@ "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", "dev": true, "requires": { - "is-extglob": "2.1.1" + "is-extglob": "^2.1.1" } }, "is-number": { @@ -3222,7 +3229,7 @@ "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { @@ -3231,7 +3238,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -3248,7 +3255,7 @@ "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", "dev": true, "requires": { - "is-path-inside": "2.1.0" + "is-path-inside": "^2.1.0" } }, "is-path-inside": { @@ -3257,7 +3264,7 @@ "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", "dev": true, "requires": { - "path-is-inside": "1.0.2" + "path-is-inside": "^1.0.2" } }, "is-plain-object": { @@ -3266,7 +3273,7 @@ "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, "requires": { - "isobject": "3.0.1" + "isobject": "^3.0.1" } }, "is-regex": { @@ -3274,7 +3281,7 @@ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", "requires": { - "has": "1.0.3" + "has": "^1.0.3" } }, "is-stream": { @@ -3288,7 +3295,7 @@ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", "requires": { - "has-symbols": "1.0.1" + "has-symbols": "^1.0.1" } }, "is-windows": { @@ -3344,7 +3351,7 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", "requires": { - "minimist": "1.2.5" + "minimist": "^1.2.0" } }, "killable": { @@ -3365,7 +3372,7 @@ "integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==", "dev": true, "requires": { - "invert-kv": "2.0.0" + "invert-kv": "^2.0.0" } }, "loader-runner": { @@ -3379,9 +3386,9 @@ "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", "requires": { - "big.js": "5.2.2", - "emojis-list": "3.0.0", - "json5": "1.0.1" + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^1.0.1" } }, "locate-path": { @@ -3390,8 +3397,8 @@ "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", "dev": true, "requires": { - "p-locate": "3.0.0", - "path-exists": "3.0.0" + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" } }, "lodash": { @@ -3410,7 +3417,7 @@ "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.1.tgz", "integrity": "sha512-LiWgfDLLb1dwbFQZsSglpRj+1ctGnayXz3Uv0/WO8n558JycT5fg6zkNcnW0G68Nn0aEldTFeEfmjCfmqry/rQ==", "requires": { - "tslib": "1.11.1" + "tslib": "^1.10.0" } }, "lru-cache": { @@ -3419,7 +3426,7 @@ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, "requires": { - "yallist": "3.1.1" + "yallist": "^3.0.2" } }, "make-dir": { @@ -3428,8 +3435,8 @@ "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", "dev": true, "requires": { - "pify": "4.0.1", - "semver": "5.7.1" + "pify": "^4.0.1", + "semver": "^5.6.0" }, "dependencies": { "semver": { @@ -3446,7 +3453,7 @@ "integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==", "dev": true, "requires": { - "p-defer": "1.0.0" + "p-defer": "^1.0.0" } }, "map-cache": { @@ -3461,7 +3468,7 @@ "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", "dev": true, "requires": { - "object-visit": "1.0.1" + "object-visit": "^1.0.0" } }, "md5.js": { @@ -3470,9 +3477,9 @@ "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", "dev": true, "requires": { - "hash-base": "3.0.4", - "inherits": "2.0.4", - "safe-buffer": "5.1.2" + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" } }, "media-typer": { @@ -3487,9 +3494,9 @@ "integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==", "dev": true, "requires": { - "map-age-cleaner": "0.1.3", - "mimic-fn": "2.1.0", - "p-is-promise": "2.1.0" + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" } }, "memory-fs": { @@ -3498,8 +3505,8 @@ "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", "dev": true, "requires": { - "errno": "0.1.7", - "readable-stream": "2.3.7" + "errno": "^0.1.3", + "readable-stream": "^2.0.1" } }, "merge-descriptors": { @@ -3520,19 +3527,19 @@ "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", "dev": true, "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "braces": "2.3.2", - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "extglob": "2.0.4", - "fragment-cache": "0.2.1", - "kind-of": "6.0.3", - "nanomatch": "1.2.13", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" } }, "miller-rabin": { @@ -3541,8 +3548,8 @@ "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", "dev": true, "requires": { - "bn.js": "4.11.8", - "brorand": "1.1.0" + "bn.js": "^4.0.0", + "brorand": "^1.0.1" } }, "mime": { @@ -3552,18 +3559,18 @@ "dev": true }, "mime-db": { - "version": "1.43.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.43.0.tgz", - "integrity": "sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ==", + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz", + "integrity": "sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg==", "dev": true }, "mime-types": { - "version": "2.1.26", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.26.tgz", - "integrity": "sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ==", + "version": "2.1.27", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz", + "integrity": "sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w==", "dev": true, "requires": { - "mime-db": "1.43.0" + "mime-db": "1.44.0" } }, "mimic-fn": { @@ -3590,7 +3597,7 @@ "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, "requires": { - "brace-expansion": "1.1.11" + "brace-expansion": "^1.1.7" } }, "minimist": { @@ -3604,16 +3611,16 @@ "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", "dev": true, "requires": { - "concat-stream": "1.6.2", - "duplexify": "3.7.1", - "end-of-stream": "1.4.4", - "flush-write-stream": "1.1.1", - "from2": "2.3.0", - "parallel-transform": "1.2.0", - "pump": "3.0.0", - "pumpify": "1.5.1", - "stream-each": "1.2.3", - "through2": "2.0.5" + "concat-stream": "^1.5.0", + "duplexify": "^3.4.2", + "end-of-stream": "^1.1.0", + "flush-write-stream": "^1.0.0", + "from2": "^2.1.0", + "parallel-transform": "^1.1.0", + "pump": "^3.0.0", + "pumpify": "^1.3.3", + "stream-each": "^1.1.0", + "through2": "^2.0.0" } }, "mixin-deep": { @@ -3622,8 +3629,8 @@ "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", "dev": true, "requires": { - "for-in": "1.0.2", - "is-extendable": "1.0.1" + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" }, "dependencies": { "is-extendable": { @@ -3632,7 +3639,7 @@ "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", "dev": true, "requires": { - "is-plain-object": "2.0.4" + "is-plain-object": "^2.0.4" } } } @@ -3643,7 +3650,7 @@ "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "dev": true, "requires": { - "minimist": "1.2.5" + "minimist": "^1.2.5" } }, "move-concurrently": { @@ -3652,12 +3659,12 @@ "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", "dev": true, "requires": { - "aproba": "1.2.0", - "copy-concurrently": "1.0.5", - "fs-write-stream-atomic": "1.0.10", - "mkdirp": "0.5.5", - "rimraf": "2.7.1", - "run-queue": "1.0.3" + "aproba": "^1.1.1", + "copy-concurrently": "^1.0.0", + "fs-write-stream-atomic": "^1.0.8", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.4", + "run-queue": "^1.0.3" } }, "ms": { @@ -3672,8 +3679,8 @@ "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", "dev": true, "requires": { - "dns-packet": "1.3.1", - "thunky": "1.1.0" + "dns-packet": "^1.3.1", + "thunky": "^1.0.2" } }, "multicast-dns-service-types": { @@ -3695,17 +3702,17 @@ "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", "dev": true, "requires": { - "arr-diff": "4.0.0", - "array-unique": "0.3.2", - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "fragment-cache": "0.2.1", - "is-windows": "1.0.2", - "kind-of": "6.0.3", - "object.pick": "1.3.0", - "regex-not": "1.0.2", - "snapdragon": "0.8.2", - "to-regex": "3.0.2" + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" } }, "negotiator": { @@ -3731,8 +3738,8 @@ "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.3.tgz", "integrity": "sha512-ehY/mVQCf9BL0gKfsJBvFJen+1V//U+0HQMPrWct40ixE4jnv0bfvxDbWtAHL9EcaPEOJHVVYKoQn1TlZUB8Tw==", "requires": { - "lower-case": "2.0.1", - "tslib": "1.11.1" + "lower-case": "^2.0.1", + "tslib": "^1.10.0" } }, "node-forge": { @@ -3747,29 +3754,29 @@ "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", "dev": true, "requires": { - "assert": "1.5.0", - "browserify-zlib": "0.2.0", - "buffer": "4.9.2", - "console-browserify": "1.2.0", - "constants-browserify": "1.0.0", - "crypto-browserify": "3.12.0", - "domain-browser": "1.2.0", - "events": "3.1.0", - "https-browserify": "1.0.0", - "os-browserify": "0.3.0", + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", "path-browserify": "0.0.1", - "process": "0.11.10", - "punycode": "1.4.1", - "querystring-es3": "0.2.1", - "readable-stream": "2.3.7", - "stream-browserify": "2.0.2", - "stream-http": "2.8.3", - "string_decoder": "1.1.1", - "timers-browserify": "2.0.11", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", "tty-browserify": "0.0.0", - "url": "0.11.0", - "util": "0.11.1", - "vm-browserify": "1.1.2" + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" }, "dependencies": { "punycode": { @@ -3780,19 +3787,31 @@ } } }, + "node-releases": { + "version": "1.1.58", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.58.tgz", + "integrity": "sha512-NxBudgVKiRh/2aPWMgPR7bPTX0VPmGx5QBwCtdHitnqFE5/O8DeBXuIMH1nwNnw/aMo6AjOrpsHzfY3UbUJ7yg==", + "dev": true + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", + "dev": true + }, "npm-run-path": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "dev": true, "requires": { - "path-key": "2.0.1" + "path-key": "^2.0.0" } }, "nth-check": { @@ -3800,13 +3819,13 @@ "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", "requires": { - "boolbase": "1.0.0" + "boolbase": "~1.0.0" } }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", + "num2fraction": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", + "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", "dev": true }, "object-assign": { @@ -3821,9 +3840,9 @@ "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", "dev": true, "requires": { - "copy-descriptor": "0.1.1", - "define-property": "0.2.5", - "kind-of": "3.2.2" + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" }, "dependencies": { "define-property": { @@ -3832,7 +3851,7 @@ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "0.1.6" + "is-descriptor": "^0.1.0" } }, "kind-of": { @@ -3841,7 +3860,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -3857,8 +3876,8 @@ "integrity": "sha512-5lHCz+0uufF6wZ7CRFWJN3hp8Jqblpgve06U5CMQ3f//6iDjPr2PEo9MWCjEssDsa+UZEL4PkFpr+BMop6aKzQ==", "dev": true, "requires": { - "define-properties": "1.1.3", - "es-abstract": "1.17.5" + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" } }, "object-keys": { @@ -3872,7 +3891,7 @@ "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", "dev": true, "requires": { - "isobject": "3.0.1" + "isobject": "^3.0.0" } }, "object.assign": { @@ -3880,10 +3899,10 @@ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", "requires": { - "define-properties": "1.1.3", - "function-bind": "1.1.1", - "has-symbols": "1.0.1", - "object-keys": "1.1.1" + "define-properties": "^1.1.2", + "function-bind": "^1.1.1", + "has-symbols": "^1.0.0", + "object-keys": "^1.0.11" } }, "object.getownpropertydescriptors": { @@ -3891,8 +3910,8 @@ "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.0.tgz", "integrity": "sha512-Z53Oah9A3TdLoblT7VKJaTDdXdT+lQO+cNpKVnya5JDe9uLvzu1YyY1yFDFrcxrlRgWrEFH0jJtD/IbuwjcEVg==", "requires": { - "define-properties": "1.1.3", - "es-abstract": "1.17.5" + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" } }, "object.pick": { @@ -3901,7 +3920,7 @@ "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", "dev": true, "requires": { - "isobject": "3.0.1" + "isobject": "^3.0.1" } }, "obuf": { @@ -3931,7 +3950,7 @@ "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, "requires": { - "wrappy": "1.0.2" + "wrappy": "1" } }, "opn": { @@ -3940,7 +3959,7 @@ "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", "dev": true, "requires": { - "is-wsl": "1.1.0" + "is-wsl": "^1.1.0" } }, "original": { @@ -3949,7 +3968,7 @@ "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", "dev": true, "requires": { - "url-parse": "1.4.7" + "url-parse": "^1.4.3" } }, "os-browserify": { @@ -3964,9 +3983,9 @@ "integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==", "dev": true, "requires": { - "execa": "1.0.0", - "lcid": "2.0.0", - "mem": "4.3.0" + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" } }, "p-defer": { @@ -3993,7 +4012,7 @@ "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, "requires": { - "p-try": "2.2.0" + "p-try": "^2.0.0" } }, "p-locate": { @@ -4002,7 +4021,7 @@ "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", "dev": true, "requires": { - "p-limit": "2.3.0" + "p-limit": "^2.0.0" } }, "p-map": { @@ -4017,7 +4036,7 @@ "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", "dev": true, "requires": { - "retry": "0.12.0" + "retry": "^0.12.0" } }, "p-try": { @@ -4038,9 +4057,9 @@ "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", "dev": true, "requires": { - "cyclist": "1.0.1", - "inherits": "2.0.4", - "readable-stream": "2.3.7" + "cyclist": "^1.0.1", + "inherits": "^2.0.3", + "readable-stream": "^2.1.5" } }, "param-case": { @@ -4048,8 +4067,8 @@ "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.3.tgz", "integrity": "sha512-VWBVyimc1+QrzappRs7waeN2YmoZFCGXWASRYX1/rGHtXqEcrGEIDm+jqIwFa2fRXNgQEwrxaYuIrX0WcAguTA==", "requires": { - "dot-case": "3.0.3", - "tslib": "1.11.1" + "dot-case": "^3.0.3", + "tslib": "^1.10.0" } }, "parse-asn1": { @@ -4058,12 +4077,12 @@ "integrity": "sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ==", "dev": true, "requires": { - "asn1.js": "4.10.1", - "browserify-aes": "1.2.0", - "create-hash": "1.2.0", - "evp_bytestokey": "1.0.3", - "pbkdf2": "3.0.17", - "safe-buffer": "5.1.2" + "asn1.js": "^4.0.0", + "browserify-aes": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.0", + "pbkdf2": "^3.0.3", + "safe-buffer": "^5.1.1" } }, "parse-passwd": { @@ -4083,8 +4102,8 @@ "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.1.tgz", "integrity": "sha512-XIeHKqIrsquVTQL2crjq3NfJUxmdLasn3TYOU0VBM+UX2a6ztAWBlJQBePLGY7VHW8+2dRadeIPK5+KImwTxQA==", "requires": { - "no-case": "3.0.3", - "tslib": "1.11.1" + "no-case": "^3.0.3", + "tslib": "^1.10.0" } }, "pascalcase": { @@ -4141,11 +4160,11 @@ "integrity": "sha512-U/il5MsrZp7mGg3mSQfn742na2T+1/vHDCG5/iTI3X9MKUuYUZVLQhyRsg06mCgDBTd57TxzgZt7P+fYfjRLtA==", "dev": true, "requires": { - "create-hash": "1.2.0", - "create-hmac": "1.1.7", - "ripemd160": "2.0.2", - "safe-buffer": "5.1.2", - "sha.js": "2.4.11" + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" } }, "pify": { @@ -4166,7 +4185,7 @@ "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", "dev": true, "requires": { - "pinkie": "2.0.4" + "pinkie": "^2.0.0" } }, "pkg-dir": { @@ -4175,18 +4194,72 @@ "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", "dev": true, "requires": { - "find-up": "3.0.0" + "find-up": "^3.0.0" + } + }, + "pkg-up": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz", + "integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=", + "dev": true, + "requires": { + "find-up": "^2.1.0" + }, + "dependencies": { + "find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", + "dev": true, + "requires": { + "locate-path": "^2.0.0" + } + }, + "locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", + "dev": true, + "requires": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "requires": { + "p-try": "^1.0.0" + } + }, + "p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", + "dev": true, + "requires": { + "p-limit": "^1.1.0" + } + }, + "p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", + "dev": true + } } }, "portfinder": { - "version": "1.0.25", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.25.tgz", - "integrity": "sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg==", + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz", + "integrity": "sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ==", "dev": true, "requires": { - "async": "2.6.3", - "debug": "3.2.6", - "mkdirp": "0.5.5" + "async": "^2.6.2", + "debug": "^3.1.1", + "mkdirp": "^0.5.1" }, "dependencies": { "debug": { @@ -4195,7 +4268,7 @@ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "dev": true, "requires": { - "ms": "2.1.2" + "ms": "^2.1.1" } }, "ms": { @@ -4212,13 +4285,30 @@ "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", "dev": true }, + "postcss": { + "version": "7.0.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.32.tgz", + "integrity": "sha512-03eXong5NLnNCD05xscnGKGDZ98CyzoqPSMjOe6SuoQY7Z2hIj0Ld1g/O/UQRuOle2aRtiIRDg9tDcTGAkLfKw==", + "dev": true, + "requires": { + "chalk": "^2.4.2", + "source-map": "^0.6.1", + "supports-color": "^6.1.0" + } + }, + "postcss-value-parser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", + "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", + "dev": true + }, "pretty-error": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.1.tgz", "integrity": "sha1-X0+HyPkeWuPzuoerTPXgOxoX8aM=", "requires": { - "renderkid": "2.0.3", - "utila": "0.4.0" + "renderkid": "^2.0.1", + "utila": "~0.4" } }, "process": { @@ -4245,7 +4335,7 @@ "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", "dev": true, "requires": { - "forwarded": "0.1.2", + "forwarded": "~0.1.2", "ipaddr.js": "1.9.1" } }, @@ -4261,12 +4351,12 @@ "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", "dev": true, "requires": { - "bn.js": "4.11.8", - "browserify-rsa": "4.0.1", - "create-hash": "1.2.0", - "parse-asn1": "5.1.5", - "randombytes": "2.1.0", - "safe-buffer": "5.1.2" + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" } }, "pump": { @@ -4275,8 +4365,8 @@ "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, "requires": { - "end-of-stream": "1.4.4", - "once": "1.4.0" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, "pumpify": { @@ -4285,9 +4375,9 @@ "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", "dev": true, "requires": { - "duplexify": "3.7.1", - "inherits": "2.0.4", - "pump": "2.0.1" + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" }, "dependencies": { "pump": { @@ -4296,8 +4386,8 @@ "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", "dev": true, "requires": { - "end-of-stream": "1.4.4", - "once": "1.4.0" + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } } } @@ -4338,7 +4428,7 @@ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, "requires": { - "safe-buffer": "5.1.2" + "safe-buffer": "^5.1.0" } }, "randomfill": { @@ -4347,8 +4437,8 @@ "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", "dev": true, "requires": { - "randombytes": "2.1.0", - "safe-buffer": "5.1.2" + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" } }, "range-parser": { @@ -4383,13 +4473,13 @@ "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", "dev": true, "requires": { - "core-util-is": "1.0.2", - "inherits": "2.0.4", - "isarray": "1.0.0", - "process-nextick-args": "2.0.1", - "safe-buffer": "5.1.2", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, "readdirp": { @@ -4398,9 +4488,9 @@ "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", "dev": true, "requires": { - "graceful-fs": "4.2.3", - "micromatch": "3.1.10", - "readable-stream": "2.3.7" + "graceful-fs": "^4.1.11", + "micromatch": "^3.1.10", + "readable-stream": "^2.0.2" } }, "regex-not": { @@ -4409,8 +4499,8 @@ "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", "dev": true, "requires": { - "extend-shallow": "3.0.2", - "safe-regex": "1.1.0" + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" } }, "regexp.prototype.flags": { @@ -4419,8 +4509,8 @@ "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", "dev": true, "requires": { - "define-properties": "1.1.3", - "es-abstract": "1.17.5" + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" } }, "relateurl": { @@ -4439,11 +4529,11 @@ "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.3.tgz", "integrity": "sha512-z8CLQp7EZBPCwCnncgf9C4XAi3WR0dv+uWu/PjIyhhAb5d6IJ/QZqlHFprHeKT+59//V6BNUsLbvN8+2LarxGA==", "requires": { - "css-select": "1.2.0", - "dom-converter": "0.2.0", - "htmlparser2": "3.10.1", - "strip-ansi": "3.0.1", - "utila": "0.4.0" + "css-select": "^1.1.0", + "dom-converter": "^0.2", + "htmlparser2": "^3.3.0", + "strip-ansi": "^3.0.0", + "utila": "^0.4.0" } }, "repeat-element": { @@ -4482,7 +4572,7 @@ "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", "dev": true, "requires": { - "resolve-from": "3.0.0" + "resolve-from": "^3.0.0" } }, "resolve-dir": { @@ -4491,8 +4581,8 @@ "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", "dev": true, "requires": { - "expand-tilde": "2.0.2", - "global-modules": "1.0.0" + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" }, "dependencies": { "global-modules": { @@ -4501,9 +4591,9 @@ "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", "dev": true, "requires": { - "global-prefix": "1.0.2", - "is-windows": "1.0.2", - "resolve-dir": "1.0.1" + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" } } } @@ -4538,7 +4628,7 @@ "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "dev": true, "requires": { - "glob": "7.1.6" + "glob": "^7.1.3" } }, "ripemd160": { @@ -4547,8 +4637,8 @@ "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", "dev": true, "requires": { - "hash-base": "3.0.4", - "inherits": "2.0.4" + "hash-base": "^3.0.0", + "inherits": "^2.0.1" } }, "run-queue": { @@ -4557,7 +4647,7 @@ "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", "dev": true, "requires": { - "aproba": "1.2.0" + "aproba": "^1.1.1" } }, "safe-buffer": { @@ -4571,7 +4661,7 @@ "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", "dev": true, "requires": { - "ret": "0.1.15" + "ret": "~0.1.10" } }, "safer-buffer": { @@ -4580,6 +4670,17 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "schema-utils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", + "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "dev": true, + "requires": { + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" + } + }, "select-hose": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", @@ -4608,18 +4709,18 @@ "dev": true, "requires": { "debug": "2.6.9", - "depd": "1.1.2", - "destroy": "1.0.4", - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "etag": "1.8.1", + "depd": "~1.1.2", + "destroy": "~1.0.4", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", "fresh": "0.5.2", - "http-errors": "1.7.2", + "http-errors": "~1.7.2", "mime": "1.6.0", "ms": "2.1.1", - "on-finished": "2.3.0", - "range-parser": "1.2.1", - "statuses": "1.5.0" + "on-finished": "~2.3.0", + "range-parser": "~1.2.1", + "statuses": "~1.5.0" }, "dependencies": { "ms": { @@ -4642,13 +4743,13 @@ "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", "dev": true, "requires": { - "accepts": "1.3.7", + "accepts": "~1.3.4", "batch": "0.6.1", "debug": "2.6.9", - "escape-html": "1.0.3", - "http-errors": "1.6.3", - "mime-types": "2.1.26", - "parseurl": "1.3.3" + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" }, "dependencies": { "http-errors": { @@ -4657,10 +4758,10 @@ "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", "dev": true, "requires": { - "depd": "1.1.2", + "depd": "~1.1.2", "inherits": "2.0.3", "setprototypeof": "1.1.0", - "statuses": "1.5.0" + "statuses": ">= 1.4.0 < 2" } }, "inherits": { @@ -4683,9 +4784,9 @@ "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", "dev": true, "requires": { - "encodeurl": "1.0.2", - "escape-html": "1.0.3", - "parseurl": "1.3.3", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", "send": "0.17.1" } }, @@ -4701,10 +4802,10 @@ "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", "dev": true, "requires": { - "extend-shallow": "2.0.1", - "is-extendable": "0.1.1", - "is-plain-object": "2.0.4", - "split-string": "3.1.0" + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" }, "dependencies": { "extend-shallow": { @@ -4713,7 +4814,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } } } @@ -4736,8 +4837,8 @@ "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", "dev": true, "requires": { - "inherits": "2.0.4", - "safe-buffer": "5.1.2" + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" } }, "shebang-command": { @@ -4746,7 +4847,7 @@ "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", "dev": true, "requires": { - "shebang-regex": "1.0.0" + "shebang-regex": "^1.0.0" } }, "shebang-regex": { @@ -4767,14 +4868,14 @@ "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", "dev": true, "requires": { - "base": "0.11.2", - "debug": "2.6.9", - "define-property": "0.2.5", - "extend-shallow": "2.0.1", - "map-cache": "0.2.2", - "source-map": "0.5.7", - "source-map-resolve": "0.5.3", - "use": "3.1.1" + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" }, "dependencies": { "define-property": { @@ -4783,7 +4884,7 @@ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "0.1.6" + "is-descriptor": "^0.1.0" } }, "extend-shallow": { @@ -4792,7 +4893,7 @@ "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", "dev": true, "requires": { - "is-extendable": "0.1.1" + "is-extendable": "^0.1.0" } }, "source-map": { @@ -4809,9 +4910,9 @@ "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", "dev": true, "requires": { - "define-property": "1.0.0", - "isobject": "3.0.1", - "snapdragon-util": "3.0.1" + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" }, "dependencies": { "define-property": { @@ -4820,7 +4921,7 @@ "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", "dev": true, "requires": { - "is-descriptor": "1.0.2" + "is-descriptor": "^1.0.0" } }, "is-accessor-descriptor": { @@ -4829,7 +4930,7 @@ "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", "dev": true, "requires": { - "kind-of": "6.0.3" + "kind-of": "^6.0.0" } }, "is-data-descriptor": { @@ -4838,7 +4939,7 @@ "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", "dev": true, "requires": { - "kind-of": "6.0.3" + "kind-of": "^6.0.0" } }, "is-descriptor": { @@ -4847,9 +4948,9 @@ "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", "dev": true, "requires": { - "is-accessor-descriptor": "1.0.0", - "is-data-descriptor": "1.0.0", - "kind-of": "6.0.3" + "is-accessor-descriptor": "^1.0.0", + "is-data-descriptor": "^1.0.0", + "kind-of": "^6.0.2" } } } @@ -4860,7 +4961,7 @@ "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.2.0" }, "dependencies": { "kind-of": { @@ -4869,19 +4970,20 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } }, "sockjs": { - "version": "0.3.19", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz", - "integrity": "sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw==", + "version": "0.3.20", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz", + "integrity": "sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA==", "dev": true, "requires": { - "faye-websocket": "0.10.0", - "uuid": "3.4.0" + "faye-websocket": "^0.10.0", + "uuid": "^3.4.0", + "websocket-driver": "0.6.5" } }, "sockjs-client": { @@ -4890,12 +4992,12 @@ "integrity": "sha512-5zaLyO8/nri5cua0VtOrFXBPK1jbL4+1cebT/mmKA1E1ZXOvJrII75bPu0l0k843G/+iAbhEqzyKr0w/eCCj7g==", "dev": true, "requires": { - "debug": "3.2.6", - "eventsource": "1.0.7", - "faye-websocket": "0.11.3", - "inherits": "2.0.4", - "json3": "3.3.3", - "url-parse": "1.4.7" + "debug": "^3.2.5", + "eventsource": "^1.0.7", + "faye-websocket": "~0.11.1", + "inherits": "^2.0.3", + "json3": "^3.3.2", + "url-parse": "^1.4.3" }, "dependencies": { "debug": { @@ -4904,7 +5006,7 @@ "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", "dev": true, "requires": { - "ms": "2.1.2" + "ms": "^2.1.1" } }, "faye-websocket": { @@ -4913,7 +5015,7 @@ "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", "dev": true, "requires": { - "websocket-driver": "0.7.3" + "websocket-driver": ">=0.5.1" } }, "ms": { @@ -4941,11 +5043,11 @@ "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", "dev": true, "requires": { - "atob": "2.1.2", - "decode-uri-component": "0.2.0", - "resolve-url": "0.2.1", - "source-map-url": "0.4.0", - "urix": "0.1.0" + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" } }, "source-map-url": { @@ -4960,11 +5062,11 @@ "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", "dev": true, "requires": { - "debug": "4.1.1", - "handle-thing": "2.0.1", - "http-deceiver": "1.2.7", - "select-hose": "2.0.0", - "spdy-transport": "3.0.0" + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" }, "dependencies": { "debug": { @@ -4973,7 +5075,7 @@ "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { - "ms": "2.1.2" + "ms": "^2.1.1" } }, "ms": { @@ -4990,12 +5092,12 @@ "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", "dev": true, "requires": { - "debug": "4.1.1", - "detect-node": "2.0.4", - "hpack.js": "2.1.6", - "obuf": "1.1.2", - "readable-stream": "3.6.0", - "wbuf": "1.7.3" + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" }, "dependencies": { "debug": { @@ -5004,7 +5106,7 @@ "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { - "ms": "2.1.2" + "ms": "^2.1.1" } }, "ms": { @@ -5019,9 +5121,9 @@ "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", "dev": true, "requires": { - "inherits": "2.0.4", - "string_decoder": "1.1.1", - "util-deprecate": "1.0.2" + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" } } } @@ -5032,7 +5134,7 @@ "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", "dev": true, "requires": { - "extend-shallow": "3.0.2" + "extend-shallow": "^3.0.0" } }, "ssri": { @@ -5041,7 +5143,7 @@ "integrity": "sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA==", "dev": true, "requires": { - "figgy-pudding": "3.5.2" + "figgy-pudding": "^3.5.1" } }, "static-extend": { @@ -5050,8 +5152,8 @@ "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", "dev": true, "requires": { - "define-property": "0.2.5", - "object-copy": "0.1.0" + "define-property": "^0.2.5", + "object-copy": "^0.1.0" }, "dependencies": { "define-property": { @@ -5060,7 +5162,7 @@ "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", "dev": true, "requires": { - "is-descriptor": "0.1.6" + "is-descriptor": "^0.1.0" } } } @@ -5077,8 +5179,8 @@ "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", "dev": true, "requires": { - "inherits": "2.0.4", - "readable-stream": "2.3.7" + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" } }, "stream-each": { @@ -5087,8 +5189,8 @@ "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", "dev": true, "requires": { - "end-of-stream": "1.4.4", - "stream-shift": "1.0.1" + "end-of-stream": "^1.1.0", + "stream-shift": "^1.0.0" } }, "stream-http": { @@ -5097,11 +5199,11 @@ "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", "dev": true, "requires": { - "builtin-status-codes": "3.0.0", - "inherits": "2.0.4", - "readable-stream": "2.3.7", - "to-arraybuffer": "1.0.1", - "xtend": "4.0.2" + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" } }, "stream-shift": { @@ -5116,9 +5218,9 @@ "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", "dev": true, "requires": { - "emoji-regex": "7.0.3", - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "5.2.0" + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" }, "dependencies": { "ansi-regex": { @@ -5133,7 +5235,7 @@ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, "requires": { - "ansi-regex": "4.1.0" + "ansi-regex": "^4.1.0" } } } @@ -5143,8 +5245,8 @@ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", "requires": { - "define-properties": "1.1.3", - "es-abstract": "1.17.5" + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" } }, "string.prototype.trimleft": { @@ -5152,9 +5254,9 @@ "resolved": "https://registry.npmjs.org/string.prototype.trimleft/-/string.prototype.trimleft-2.1.2.tgz", "integrity": "sha512-gCA0tza1JBvqr3bfAIFJGqfdRTyPae82+KTnm3coDXkZN9wnuW3HjGgN386D7hfv5CHQYCI022/rJPVlqXyHSw==", "requires": { - "define-properties": "1.1.3", - "es-abstract": "1.17.5", - "string.prototype.trimstart": "1.0.1" + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimstart": "^1.0.0" } }, "string.prototype.trimright": { @@ -5162,9 +5264,9 @@ "resolved": "https://registry.npmjs.org/string.prototype.trimright/-/string.prototype.trimright-2.1.2.tgz", "integrity": "sha512-ZNRQ7sY3KroTaYjRS6EbNiiHrOkjihL9aQE/8gfQ4DtAC/aEBRHFJa44OmoWxGGqXuJlfKkZW4WcXErGr+9ZFg==", "requires": { - "define-properties": "1.1.3", - "es-abstract": "1.17.5", - "string.prototype.trimend": "1.0.1" + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5", + "string.prototype.trimend": "^1.0.0" } }, "string.prototype.trimstart": { @@ -5172,8 +5274,8 @@ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", "requires": { - "define-properties": "1.1.3", - "es-abstract": "1.17.5" + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" } }, "string_decoder": { @@ -5181,7 +5283,7 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "requires": { - "safe-buffer": "5.1.2" + "safe-buffer": "~5.1.0" } }, "strip-ansi": { @@ -5189,7 +5291,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "requires": { - "ansi-regex": "2.1.1" + "ansi-regex": "^2.0.0" } }, "strip-eof": { @@ -5204,7 +5306,7 @@ "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", "dev": true, "requires": { - "has-flag": "3.0.0" + "has-flag": "^3.0.0" } }, "tapable": { @@ -5217,9 +5319,9 @@ "resolved": "https://registry.npmjs.org/terser/-/terser-4.6.11.tgz", "integrity": "sha512-76Ynm7OXUG5xhOpblhytE7X58oeNSmC8xnNhjWVo8CksHit0U0kO4hfNbPrrYwowLWFgM2n9L176VNx2QaHmtA==", "requires": { - "commander": "2.20.3", - "source-map": "0.6.1", - "source-map-support": "0.5.16" + "commander": "^2.20.0", + "source-map": "~0.6.1", + "source-map-support": "~0.5.12" }, "dependencies": { "source-map-support": { @@ -5227,8 +5329,8 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.16.tgz", "integrity": "sha512-efyLRJDr68D9hBBNIPWFjhpFzURh+KJykQwvMyW5UiZzYwoF6l4YMMDIJJEyFWxWCqfyxLzz6tSfUFR+kXXsVQ==", "requires": { - "buffer-from": "1.1.1", - "source-map": "0.6.1" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } } } @@ -5239,15 +5341,15 @@ "integrity": "sha512-QMxecFz/gHQwteWwSo5nTc6UaICqN1bMedC5sMtUc7y3Ha3Q8y6ZO0iCR8pq4RJC8Hjf0FEPEHZqcMB/+DFCrA==", "dev": true, "requires": { - "cacache": "12.0.4", - "find-cache-dir": "2.1.0", - "is-wsl": "1.1.0", - "schema-utils": "1.0.0", - "serialize-javascript": "2.1.2", - "source-map": "0.6.1", - "terser": "4.6.11", - "webpack-sources": "1.4.3", - "worker-farm": "1.7.0" + "cacache": "^12.0.2", + "find-cache-dir": "^2.1.0", + "is-wsl": "^1.1.0", + "schema-utils": "^1.0.0", + "serialize-javascript": "^2.1.2", + "source-map": "^0.6.1", + "terser": "^4.1.2", + "webpack-sources": "^1.4.0", + "worker-farm": "^1.7.0" }, "dependencies": { "schema-utils": { @@ -5256,9 +5358,9 @@ "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", "dev": true, "requires": { - "ajv": "6.12.0", - "ajv-errors": "1.0.1", - "ajv-keywords": "3.4.1" + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" } } } @@ -5269,8 +5371,8 @@ "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", "dev": true, "requires": { - "readable-stream": "2.3.7", - "xtend": "4.0.2" + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" } }, "thunky": { @@ -5285,7 +5387,7 @@ "integrity": "sha512-60aV6sgJ5YEbzUdn9c8kYGIqOubPoUdqQCul3SBAsRCZ40s6Y5cMcrW4dt3/k/EsbLVJNl9n6Vz3fTc+k2GeKQ==", "dev": true, "requires": { - "setimmediate": "1.0.5" + "setimmediate": "^1.0.4" } }, "to-arraybuffer": { @@ -5300,7 +5402,7 @@ "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", "dev": true, "requires": { - "kind-of": "3.2.2" + "kind-of": "^3.0.2" }, "dependencies": { "kind-of": { @@ -5309,7 +5411,7 @@ "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", "dev": true, "requires": { - "is-buffer": "1.1.6" + "is-buffer": "^1.1.5" } } } @@ -5320,10 +5422,10 @@ "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", "dev": true, "requires": { - "define-property": "2.0.2", - "extend-shallow": "3.0.2", - "regex-not": "1.0.2", - "safe-regex": "1.1.0" + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "regex-not": "^1.0.2", + "safe-regex": "^1.1.0" } }, "to-regex-range": { @@ -5332,8 +5434,8 @@ "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", "dev": true, "requires": { - "is-number": "3.0.0", - "repeat-string": "1.6.1" + "is-number": "^3.0.0", + "repeat-string": "^1.6.1" } }, "toidentifier": { @@ -5360,7 +5462,7 @@ "dev": true, "requires": { "media-typer": "0.3.0", - "mime-types": "2.1.26" + "mime-types": "~2.1.24" } }, "typedarray": { @@ -5375,10 +5477,10 @@ "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", "dev": true, "requires": { - "arr-union": "3.1.0", - "get-value": "2.0.6", - "is-extendable": "0.1.1", - "set-value": "2.0.1" + "arr-union": "^3.1.0", + "get-value": "^2.0.6", + "is-extendable": "^0.1.1", + "set-value": "^2.0.1" } }, "unique-filename": { @@ -5387,7 +5489,7 @@ "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", "dev": true, "requires": { - "unique-slug": "2.0.2" + "unique-slug": "^2.0.0" } }, "unique-slug": { @@ -5396,7 +5498,7 @@ "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", "dev": true, "requires": { - "imurmurhash": "0.1.4" + "imurmurhash": "^0.1.4" } }, "unpipe": { @@ -5411,8 +5513,8 @@ "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", "dev": true, "requires": { - "has-value": "0.3.1", - "isobject": "3.0.1" + "has-value": "^0.3.1", + "isobject": "^3.0.0" }, "dependencies": { "has-value": { @@ -5421,9 +5523,9 @@ "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", "dev": true, "requires": { - "get-value": "2.0.6", - "has-values": "0.1.4", - "isobject": "2.1.0" + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" }, "dependencies": { "isobject": { @@ -5457,7 +5559,7 @@ "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", "dev": true, "requires": { - "punycode": "2.1.1" + "punycode": "^2.1.0" } }, "urix": { @@ -5490,8 +5592,8 @@ "integrity": "sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==", "dev": true, "requires": { - "querystringify": "2.1.1", - "requires-port": "1.0.0" + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" } }, "use": { @@ -5527,8 +5629,8 @@ "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", "requires": { - "define-properties": "1.1.3", - "object.getownpropertydescriptors": "2.1.0" + "define-properties": "^1.1.2", + "object.getownpropertydescriptors": "^2.0.3" } }, "utila": { @@ -5572,9 +5674,9 @@ "integrity": "sha512-+IF9hfUFOrYOOaKyfaI7h7dquUIOgyEMoQMLA7OP5FxegKA2+XdXThAZ9TU2kucfhDH7rfMHs1oPYziVGWRnZA==", "dev": true, "requires": { - "chokidar": "2.1.8", - "graceful-fs": "4.2.3", - "neo-async": "2.6.1" + "chokidar": "^2.1.8", + "graceful-fs": "^4.1.2", + "neo-async": "^2.5.0" } }, "wbuf": { @@ -5583,7 +5685,7 @@ "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", "dev": true, "requires": { - "minimalistic-assert": "1.0.1" + "minimalistic-assert": "^1.0.0" } }, "webpack": { @@ -5596,25 +5698,25 @@ "@webassemblyjs/helper-module-context": "1.9.0", "@webassemblyjs/wasm-edit": "1.9.0", "@webassemblyjs/wasm-parser": "1.9.0", - "acorn": "6.4.1", - "ajv": "6.12.0", - "ajv-keywords": "3.4.1", - "chrome-trace-event": "1.0.2", - "enhanced-resolve": "4.1.1", - "eslint-scope": "4.0.3", - "json-parse-better-errors": "1.0.2", - "loader-runner": "2.4.0", - "loader-utils": "1.4.0", - "memory-fs": "0.4.1", - "micromatch": "3.1.10", - "mkdirp": "0.5.5", - "neo-async": "2.6.1", - "node-libs-browser": "2.2.1", - "schema-utils": "1.0.0", - "tapable": "1.1.3", - "terser-webpack-plugin": "1.4.3", - "watchpack": "1.6.1", - "webpack-sources": "1.4.3" + "acorn": "^6.2.1", + "ajv": "^6.10.2", + "ajv-keywords": "^3.4.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^4.1.0", + "eslint-scope": "^4.0.3", + "json-parse-better-errors": "^1.0.2", + "loader-runner": "^2.4.0", + "loader-utils": "^1.2.3", + "memory-fs": "^0.4.1", + "micromatch": "^3.1.10", + "mkdirp": "^0.5.3", + "neo-async": "^2.6.1", + "node-libs-browser": "^2.2.1", + "schema-utils": "^1.0.0", + "tapable": "^1.1.3", + "terser-webpack-plugin": "^1.4.3", + "watchpack": "^1.6.0", + "webpack-sources": "^1.4.1" }, "dependencies": { "schema-utils": { @@ -5623,9 +5725,9 @@ "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", "dev": true, "requires": { - "ajv": "6.12.0", - "ajv-errors": "1.0.1", - "ajv-keywords": "3.4.1" + "ajv": "^6.1.0", + "ajv-errors": "^1.0.0", + "ajv-keywords": "^3.1.0" } } } @@ -5661,9 +5763,9 @@ "integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", "dev": true, "requires": { - "graceful-fs": "4.2.3", - "memory-fs": "0.4.1", - "tapable": "1.1.3" + "graceful-fs": "^4.1.2", + "memory-fs": "^0.4.0", + "tapable": "^1.0.0" } }, "loader-utils": { @@ -5672,9 +5774,9 @@ "integrity": "sha512-fkpz8ejdnEMG3s37wGL07iSBDg99O9D5yflE9RGNH3hRdx9SOwYfnGYdZOUIZitN8E+E2vkq3MUMYMvPYl5ZZA==", "dev": true, "requires": { - "big.js": "5.2.2", - "emojis-list": "2.1.0", - "json5": "1.0.1" + "big.js": "^5.2.2", + "emojis-list": "^2.0.0", + "json5": "^1.0.1" } } } @@ -5685,209 +5787,93 @@ "integrity": "sha512-1xC42LxbYoqLNAhV6YzTYacicgMZQTqRd27Sim9wn5hJrX3I5nxYy1SxSd4+gjUFsz1dQFj+yEe6zEVmSkeJjw==", "dev": true, "requires": { - "memory-fs": "0.4.1", - "mime": "2.4.4", - "mkdirp": "0.5.5", - "range-parser": "1.2.1", - "webpack-log": "2.0.0" + "memory-fs": "^0.4.1", + "mime": "^2.4.4", + "mkdirp": "^0.5.1", + "range-parser": "^1.2.1", + "webpack-log": "^2.0.0" }, "dependencies": { "mime": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.4.tgz", - "integrity": "sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA==", + "version": "2.4.6", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.6.tgz", + "integrity": "sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA==", "dev": true } } }, "webpack-dev-server": { - "version": "3.10.3", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.10.3.tgz", - "integrity": "sha512-e4nWev8YzEVNdOMcNzNeCN947sWJNd43E5XvsJzbAL08kGc2frm1tQ32hTJslRS+H65LCb/AaUCYU7fjHCpDeQ==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz", + "integrity": "sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg==", "dev": true, "requires": { "ansi-html": "0.0.7", - "bonjour": "3.5.0", - "chokidar": "2.1.8", - "compression": "1.7.4", - "connect-history-api-fallback": "1.6.0", - "debug": "4.1.1", - "del": "4.1.1", - "express": "4.17.1", - "html-entities": "1.3.1", + "bonjour": "^3.5.0", + "chokidar": "^2.1.8", + "compression": "^1.7.4", + "connect-history-api-fallback": "^1.6.0", + "debug": "^4.1.1", + "del": "^4.1.1", + "express": "^4.17.1", + "html-entities": "^1.3.1", "http-proxy-middleware": "0.19.1", - "import-local": "2.0.0", - "internal-ip": "4.3.0", - "ip": "1.1.5", - "is-absolute-url": "3.0.3", - "killable": "1.0.1", - "loglevel": "1.6.8", - "opn": "5.5.0", - "p-retry": "3.0.1", - "portfinder": "1.0.25", - "schema-utils": "1.0.0", - "selfsigned": "1.10.7", - "semver": "6.3.0", - "serve-index": "1.9.1", - "sockjs": "0.3.19", + "import-local": "^2.0.0", + "internal-ip": "^4.3.0", + "ip": "^1.1.5", + "is-absolute-url": "^3.0.3", + "killable": "^1.0.1", + "loglevel": "^1.6.8", + "opn": "^5.5.0", + "p-retry": "^3.0.1", + "portfinder": "^1.0.26", + "schema-utils": "^1.0.0", + "selfsigned": "^1.10.7", + "semver": "^6.3.0", + "serve-index": "^1.9.1", + "sockjs": "0.3.20", "sockjs-client": "1.4.0", - "spdy": "4.0.2", - "strip-ansi": "3.0.1", - "supports-color": "6.1.0", - "url": "0.11.0", - "webpack-dev-middleware": "3.7.2", - "webpack-log": "2.0.0", - "ws": "6.2.1", - "yargs": "12.0.5" + "spdy": "^4.0.2", + "strip-ansi": "^3.0.1", + "supports-color": "^6.1.0", + "url": "^0.11.0", + "webpack-dev-middleware": "^3.7.2", + "webpack-log": "^2.0.0", + "ws": "^6.2.1", + "yargs": "^13.3.2" }, "dependencies": { - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "cliui": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-4.1.0.tgz", - "integrity": "sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ==", - "dev": true, - "requires": { - "string-width": "2.1.1", - "strip-ansi": "4.0.0", - "wrap-ansi": "2.1.0" - }, - "dependencies": { - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "3.0.0" - } - } - } - }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", "dev": true, "requires": { - "ms": "2.1.2" + "ms": "^2.1.1" } }, - "get-caller-file": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", - "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==", - "dev": true - }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "require-main-filename": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", - "integrity": "sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE=", - "dev": true - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "6.12.0", - "ajv-errors": "1.0.1", - "ajv-keywords": "3.4.1" - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "2.0.0", - "strip-ansi": "4.0.0" - }, - "dependencies": { - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "3.0.0" - } - } - } - }, - "wrap-ansi": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", - "integrity": "sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU=", - "dev": true, - "requires": { - "string-width": "1.0.2", - "strip-ansi": "3.0.1" - }, - "dependencies": { - "is-fullwidth-code-point": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", - "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", - "dev": true, - "requires": { - "number-is-nan": "1.0.1" - } - }, - "string-width": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", - "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", - "dev": true, - "requires": { - "code-point-at": "1.1.0", - "is-fullwidth-code-point": "1.0.0", - "strip-ansi": "3.0.1" - } - } - } - }, "yargs": { - "version": "12.0.5", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz", - "integrity": "sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw==", + "version": "13.3.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", + "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", "dev": true, "requires": { - "cliui": "4.1.0", - "decamelize": "1.2.0", - "find-up": "3.0.0", - "get-caller-file": "1.0.3", - "os-locale": "3.1.0", - "require-directory": "2.1.1", - "require-main-filename": "1.0.1", - "set-blocking": "2.0.0", - "string-width": "2.1.1", - "which-module": "2.0.0", - "y18n": "4.0.0", - "yargs-parser": "11.1.1" - } - }, - "yargs-parser": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz", - "integrity": "sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ==", - "dev": true, - "requires": { - "camelcase": "5.3.1", - "decamelize": "1.2.0" + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.2" } } } @@ -5898,8 +5884,8 @@ "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", "dev": true, "requires": { - "ansi-colors": "3.2.4", - "uuid": "3.4.0" + "ansi-colors": "^3.0.0", + "uuid": "^3.3.2" } }, "webpack-sources": { @@ -5908,25 +5894,23 @@ "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", "dev": true, "requires": { - "source-list-map": "2.0.1", - "source-map": "0.6.1" + "source-list-map": "^2.0.0", + "source-map": "~0.6.1" } }, "websocket-driver": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz", - "integrity": "sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz", + "integrity": "sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY=", "dev": true, "requires": { - "http-parser-js": "0.4.10", - "safe-buffer": "5.1.2", - "websocket-extensions": "0.1.3" + "websocket-extensions": ">=0.1.1" } }, "websocket-extensions": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.3.tgz", - "integrity": "sha512-nqHUnMXmBzT0w570r2JpJxfiSD1IzoI+HGVdd3aZ0yNi3ngvQ4jv1dtHt5VGxfI2yj5yqImPhOK4vmIh2xMbGg==", + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", "dev": true }, "which": { @@ -5935,7 +5919,7 @@ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, "requires": { - "isexe": "2.0.0" + "isexe": "^2.0.0" } }, "which-module": { @@ -5950,7 +5934,7 @@ "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", "dev": true, "requires": { - "errno": "0.1.7" + "errno": "~0.1.7" } }, "wrap-ansi": { @@ -5959,9 +5943,9 @@ "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", "dev": true, "requires": { - "ansi-styles": "3.2.1", - "string-width": "3.1.0", - "strip-ansi": "5.2.0" + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" }, "dependencies": { "ansi-regex": { @@ -5976,7 +5960,7 @@ "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", "dev": true, "requires": { - "ansi-regex": "4.1.0" + "ansi-regex": "^4.1.0" } } } @@ -5993,7 +5977,7 @@ "integrity": "sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==", "dev": true, "requires": { - "async-limiter": "1.0.1" + "async-limiter": "~1.0.0" } }, "xtend": { @@ -6020,17 +6004,17 @@ "integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==", "dev": true, "requires": { - "cliui": "5.0.0", - "find-up": "3.0.0", - "get-caller-file": "2.0.5", - "os-locale": "3.1.0", - "require-directory": "2.1.1", - "require-main-filename": "2.0.0", - "set-blocking": "2.0.0", - "string-width": "3.1.0", - "which-module": "2.0.0", - "y18n": "4.0.0", - "yargs-parser": "13.1.2" + "cliui": "^5.0.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", + "os-locale": "^3.1.0", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^3.0.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^13.1.0" } }, "yargs-parser": { @@ -6039,8 +6023,8 @@ "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", "dev": true, "requires": { - "camelcase": "5.3.1", - "decamelize": "1.2.0" + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" } } } diff --git a/clients/native/examples/js-examples/websocket/package.json b/clients/native/examples/js-examples/websocket/package.json index 3496bc3f4c..db79ea39d6 100644 --- a/clients/native/examples/js-examples/websocket/package.json +++ b/clients/native/examples/js-examples/websocket/package.json @@ -22,7 +22,7 @@ "clean-webpack-plugin": "^3.0.0", "webpack": "^4.42.1", "webpack-cli": "^3.3.11", - "webpack-dev-server": "^3.10.3" + "webpack-dev-server": "^3.11.0" }, "dependencies": { "core-js": "^3.6.5", From 26ee30e252949fedfa63e242b59ea67e011f736b Mon Sep 17 00:00:00 2001 From: Dave Hrycyszyn Date: Fri, 19 Jun 2020 16:01:59 +0100 Subject: [PATCH 10/76] Adding description field to wasm client to kill warning (#270) --- clients/webassembly/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/clients/webassembly/Cargo.toml b/clients/webassembly/Cargo.toml index a630dbe1e6..7408b983e0 100644 --- a/clients/webassembly/Cargo.toml +++ b/clients/webassembly/Cargo.toml @@ -6,6 +6,7 @@ edition = "2018" keywords = ["nym", "sphinx", "wasm", "webassembly", "privacy", "client"] license = "Apache-2.0" repository = "https://github.com/nymtech/nym" +description = "A webassembly client which can be used to interact with the the Nym privacy platform. Wasm is used for Sphinx packet generation." [lib] crate-type = ["cdylib", "rlib"] From cf2c20011600a8896a6f496f5f9037981827faae Mon Sep 17 00:00:00 2001 From: Dave Hrycyszyn Date: Fri, 19 Jun 2020 16:02:12 +0100 Subject: [PATCH 11/76] Removed the healthcheck module, it's no longer in use. (#271) --- Cargo.lock | 21 -- Cargo.toml | 1 - common/healthcheck/Cargo.toml | 27 --- common/healthcheck/src/config.rs | 29 --- common/healthcheck/src/lib.rs | 92 -------- common/healthcheck/src/path_check.rs | 319 --------------------------- common/healthcheck/src/result.rs | 201 ----------------- common/healthcheck/src/score.rs | 181 --------------- validator/Cargo.toml | 1 - 9 files changed, 872 deletions(-) delete mode 100644 common/healthcheck/Cargo.toml delete mode 100644 common/healthcheck/src/config.rs delete mode 100644 common/healthcheck/src/lib.rs delete mode 100644 common/healthcheck/src/path_check.rs delete mode 100644 common/healthcheck/src/result.rs delete mode 100644 common/healthcheck/src/score.rs diff --git a/Cargo.lock b/Cargo.lock index 4675662729..e02f9e8d59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1074,26 +1074,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "healthcheck" -version = "0.1.0" -dependencies = [ - "crypto", - "directory-client", - "futures 0.3.4", - "itertools", - "log 0.4.8", - "mixnet-client", - "nymsphinx", - "pretty_env_logger", - "rand 0.7.3", - "rand_os", - "serde", - "serde_derive", - "tokio 0.2.16", - "topology", -] - [[package]] name = "hermit-abi" version = "0.1.10" @@ -1858,7 +1838,6 @@ dependencies = [ "dirs", "dotenv", "futures 0.3.4", - "healthcheck", "iron", "log 0.4.8", "pretty_env_logger", diff --git a/Cargo.toml b/Cargo.toml index 9821863fe0..ec4b4a44d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,6 @@ members = [ "common/client-libs/validator-client", "common/config", "common/crypto", - "common/healthcheck", "common/nymsphinx", "common/nymsphinx/acknowledgements", "common/nymsphinx/addressing", diff --git a/common/healthcheck/Cargo.toml b/common/healthcheck/Cargo.toml deleted file mode 100644 index 90e82c59c6..0000000000 --- a/common/healthcheck/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "healthcheck" -version = "0.1.0" -authors = ["Jedrzej Stuczynski "] -edition = "2018" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - -[dependencies] -futures = "0.3.1" -itertools = "0.8.2" -log = "0.4.8" -pretty_env_logger = "0.3" -rand_os = "0.1" -rand = "0.7.2" -serde = "1.0.104" -serde_derive = "1.0.104" -tokio = { version = "0.2", features = ["full"] } - -## internal -crypto = { path = "../crypto" } -directory-client = { path = "../client-libs/directory-client" } -mixnet-client = { path = "../client-libs/mixnet-client" } -nymsphinx = {path = "../nymsphinx" } -topology = {path = "../topology" } - -[dev-dependencies] diff --git a/common/healthcheck/src/config.rs b/common/healthcheck/src/config.rs deleted file mode 100644 index cb0596e9ea..0000000000 --- a/common/healthcheck/src/config.rs +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// 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. - -use serde_derive::Deserialize; - -#[derive(Deserialize, Debug)] -pub struct HealthCheck { - #[serde(rename(deserialize = "directory-server"))] - pub directory_server: String, - - pub interval: f64, // in seconds - - #[serde(rename(deserialize = "resolution-timeout"))] - pub resolution_timeout: f64, // in seconds - - #[serde(rename(deserialize = "test-packets-per-node"))] - pub num_test_packets: usize, -} diff --git a/common/healthcheck/src/lib.rs b/common/healthcheck/src/lib.rs deleted file mode 100644 index 8ba406a9ec..0000000000 --- a/common/healthcheck/src/lib.rs +++ /dev/null @@ -1,92 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// 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. - -use crate::result::HealthCheckResult; -use crypto::identity::MixIdentityKeyPair; -use log::trace; -use std::fmt::{Error, Formatter}; -use std::time::Duration; -use topology::{NymTopology, NymTopologyError}; - -// basically no limit -pub(crate) const MAX_PROVIDER_RESPONSE_SIZE: usize = 1024 * 1024; - -pub mod config; -mod path_check; -mod result; -mod score; - -#[derive(Debug)] -pub enum HealthCheckerError { - FailedToObtainTopologyError, - InvalidTopologyError, -} - -// required by std::error::Error -impl std::fmt::Display for HealthCheckerError { - fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { - // just have implementation equivalent to derived debug - write!(f, "{:?}", self) - } -} - -impl std::error::Error for HealthCheckerError {} - -impl From for HealthCheckerError { - fn from(_: NymTopologyError) -> Self { - use HealthCheckerError::*; - InvalidTopologyError - } -} - -pub struct HealthChecker { - num_test_packets: usize, - resolution_timeout: Duration, - connection_timeout: Duration, - identity_keypair: MixIdentityKeyPair, -} - -impl HealthChecker { - pub fn new( - resolution_timeout: Duration, - connection_timeout: Duration, - num_test_packets: usize, - identity_keypair: MixIdentityKeyPair, - ) -> Self { - HealthChecker { - resolution_timeout, - connection_timeout, - num_test_packets, - identity_keypair, - } - } - - pub async fn do_check( - &self, - current_topology: &T, - ) -> Result { - trace!("going to perform a healthcheck!"); - - let mut healthcheck_result = HealthCheckResult::calculate( - current_topology, - self.num_test_packets, - self.resolution_timeout, - self.connection_timeout, - &self.identity_keypair, - ) - .await; - healthcheck_result.sort_scores(); - Ok(healthcheck_result) - } -} diff --git a/common/healthcheck/src/path_check.rs b/common/healthcheck/src/path_check.rs deleted file mode 100644 index f7fc2a0e6d..0000000000 --- a/common/healthcheck/src/path_check.rs +++ /dev/null @@ -1,319 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// 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. - -use crate::MAX_PROVIDER_RESPONSE_SIZE; -use crypto::identity::MixIdentityKeyPair; -use itertools::Itertools; -use log::{debug, error, info, trace, warn}; -use nymsphinx::addressing::nodes::NymNodeRoutingAddress; -use nymsphinx::{Delay, Destination, DestinationAddressBytes, Node as SphinxNode}; -// use provider_client::{ProviderClient, ProviderClientError}; -use std::collections::HashMap; -use std::convert::TryFrom; -use std::net::SocketAddr; -use std::time::Duration; -use topology::provider; - -pub(crate) type CheckId = [u8; 16]; - -// JUST TO MAKE IT COMPILE, THIS WILL NEED TO BE REPLACED WITH GATEWAY -struct ProviderClient {} - -struct AuthToken {} - -impl ProviderClient { - fn new( - _provider_network_address: SocketAddr, - _our_address: DestinationAddressBytes, - _auth_token: Option, - _max_response_size: usize, - ) -> Self { - unimplemented!() - } - fn update_token(&mut self, _auth_token: AuthToken) { - unimplemented!() - } - async fn register(&mut self) -> Result { - unimplemented!() - } - async fn retrieve_messages(&mut self) -> Result>, ProviderClientError> { - unimplemented!() - } -} - -#[derive(Debug)] -enum ProviderClientError { - #[allow(dead_code)] - ClientAlreadyRegisteredError, -} - -const DUMMY_MESSAGE_CONTENT: &[u8] = b"THIS NEEDS TO BE REDONE AS IT DOESNT EXIST ANYMORE"; - -#[derive(Debug, PartialEq, Clone)] -pub enum PathStatus { - Healthy, - Unhealthy, - Pending, -} - -pub(crate) struct PathChecker { - provider_clients: HashMap<[u8; 32], Option>, - mixnet_client: mixnet_client::Client, - paths_status: HashMap, PathStatus>, - our_destination: Destination, - check_id: CheckId, -} - -impl PathChecker { - pub(crate) async fn new( - providers: Vec, - identity_keys: &MixIdentityKeyPair, - connection_timeout: Duration, - check_id: CheckId, - ) -> Self { - let mut provider_clients = HashMap::new(); - - let address = identity_keys.public_key().derive_address(); - - for provider in providers { - let mut provider_client = ProviderClient::new( - provider.client_listener, - address.clone(), - None, - MAX_PROVIDER_RESPONSE_SIZE, - ); - // TODO: we might be sending unnecessary register requests since after first healthcheck, - // we are registered for any subsequent ones (since our address did not change) - - let insertion_result = match provider_client.register().await { - Ok(token) => { - debug!("[Healthcheck] registered at provider {}", provider.pub_key); - provider_client.update_token(token); - provider_clients.insert(provider.get_pub_key_bytes(), Some(provider_client)) - } - Err(ProviderClientError::ClientAlreadyRegisteredError) => { - info!("[Healthcheck] We were already registered"); - provider_clients.insert(provider.get_pub_key_bytes(), Some(provider_client)) - } - }; - - if insertion_result.is_some() { - error!("provider {} already existed!", provider.pub_key); - } - } - - // there's no reconnection allowed - if it fails, then it fails. - let mixnet_client_config = mixnet_client::Config::new( - Duration::from_secs(1_000_000_000), - Duration::from_secs(1_000_000_000), - connection_timeout, - ); - - PathChecker { - provider_clients, - mixnet_client: mixnet_client::Client::new(mixnet_client_config), - our_destination: Destination::new(address, Default::default()), - paths_status: HashMap::new(), - check_id, - } - } - - // iteration is used to distinguish packets sent through the same path (as the healthcheck - // may try to send say 10 packets through given path) - fn unique_path_key(path: &[SphinxNode], check_id: CheckId, iteration: u8) -> Vec { - check_id - .iter() - .cloned() - .chain(std::iter::once(iteration)) - .chain( - path.iter() - .map(|node| node.pub_key.to_bytes().to_vec()) - .flatten(), - ) - .collect() - } - - pub(crate) fn path_key_to_node_keys(path_key: Vec) -> Vec<[u8; 32]> { - assert_eq!(path_key.len() % 32, 17); - path_key - .into_iter() - .skip(16 + 1) // remove 16 + 1 bytes as it represents check_id and the iteration number which we do not care about now - .chunks(32) - .into_iter() - .map(|key_chunk| { - let key_chunk_vec: Vec<_> = key_chunk.collect(); - let mut key = [0u8; 32]; - key.copy_from_slice(&key_chunk_vec); - key - }) - .collect() - } - - fn update_path_statuses(&mut self, messages: Vec>) { - for msg in messages.into_iter() { - // mark path as healthy - let previous_status = self.paths_status.insert(msg, PathStatus::Healthy); - match previous_status { - None => warn!("we received information about unknown path! - perhaps somebody is messing with healthchecker?"), - Some(status) => { - if status != PathStatus::Pending { - warn!("we received information about path that WASN'T in PENDING state! (it was in {:?}", status); - } - } - } - } - } - - // consume path_checker and return all path statuses - pub(crate) fn get_all_statuses(self) -> HashMap, PathStatus> { - self.paths_status - } - - // pull messages from given provider until there are no more 'real' messages - async fn resolve_pending_provider_checks( - provider_client: &mut ProviderClient, - check_id: CheckId, - ) -> Vec> { - // keep getting messages until we encounter the dummy message - let mut provider_messages = Vec::new(); - loop { - match provider_client.retrieve_messages().await { - Err(err) => { - error!("failed to fetch provider messages! - {:?}", err); - break; - } - Ok(messages) => { - let mut should_stop = false; - for msg in messages.into_iter() { - trace!("received provider response: {:?}", msg); - if msg == DUMMY_MESSAGE_CONTENT { - // finish iterating the loop as the messages might not be ordered - should_stop = true; - } else if msg[..16] != check_id { - warn!("received response from previous healthcheck") - } else { - provider_messages.push(msg); - } - } - if should_stop { - break; - } - } - } - } - provider_messages - } - - pub(crate) async fn resolve_pending_checks(&mut self) { - // not sure how to nicely put it into an iterator due to it being async calls - let mut provider_messages = Vec::new(); - for provider_client in self.provider_clients.values_mut() { - // if it was none all associated paths were already marked as unhealthy - let pc = match provider_client { - Some(pc) => pc, - None => continue, - }; - - provider_messages - .extend(Self::resolve_pending_provider_checks(pc, self.check_id).await); - } - - self.update_path_statuses(provider_messages); - } - - pub(crate) async fn send_test_packet(&mut self, path: &[SphinxNode], iteration: u8) { - if path.is_empty() { - warn!("trying to send test packet through an empty path!"); - return; - } - - trace!("Checking path: {:?} ({})", path, iteration); - let path_identifier = PathChecker::unique_path_key(path, self.check_id, iteration); - - // check if there is even any point in sending the packet - - // does provider exist? - let provider_client = self - .provider_clients - .get( - &path - .last() - .expect("We checked the path to contain at least one entry") - .pub_key - .to_bytes(), - ) - .unwrap(); - - if provider_client.is_none() { - debug!("we can ignore this path as provider itself is inaccessible"); - if self - .paths_status - .insert(path_identifier, PathStatus::Unhealthy) - .is_some() - { - panic!("Overwriting path checks!") - } - return; - } - - let layer_one_mix = path - .first() - .expect("We checked the path to contain at least one entry"); - - // we generated the bytes data so unwrap is fine - let first_node_address: SocketAddr = - NymNodeRoutingAddress::try_from(layer_one_mix.address.clone()) - .unwrap() - .into(); - - let delays: Vec<_> = path.iter().map(|_| Delay::new_from_nanos(0)).collect(); - - // all of the data used to create the packet was created by us - let packet = nymsphinx::SphinxPacket::new( - path_identifier.clone(), - &path[..], - &self.our_destination, - &delays, - ) - .unwrap(); - - debug!("sending test packet to {}", first_node_address); - - match self - .mixnet_client - .send(first_node_address, packet, true) - .await - { - Err(err) => { - debug!("failed to send packet to {} - {}", first_node_address, err); - if self - .paths_status - .insert(path_identifier, PathStatus::Unhealthy) - .is_some() - { - panic!("Overwriting path checks!") - } - } - Ok(_) => { - if self - .paths_status - .insert(path_identifier, PathStatus::Pending) - .is_some() - { - panic!("Overwriting path checks!") - } - } - } - } -} diff --git a/common/healthcheck/src/result.rs b/common/healthcheck/src/result.rs deleted file mode 100644 index 536228cb17..0000000000 --- a/common/healthcheck/src/result.rs +++ /dev/null @@ -1,201 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// 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. - -use crate::path_check::{PathChecker, PathStatus}; -use crate::score::NodeScore; -use crypto::identity::MixIdentityKeyPair; -use log::{debug, error, warn}; -use nymsphinx::NodeAddressBytes; -use rand_os::rand_core::RngCore; -use std::collections::HashMap; -use std::fmt::{Error, Formatter}; -use std::time::Duration; -use topology::NymTopology; - -#[derive(Debug)] -pub struct HealthCheckResult(Vec); - -impl std::fmt::Display for HealthCheckResult { - fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { - write!(f, "NETWORK HEALTH\n==============\n")?; - for score in self.0.iter() { - writeln!(f, "{}", score)? - } - Ok(()) - } -} - -impl HealthCheckResult { - pub fn sort_scores(&mut self) { - self.0.sort(); - } - - fn zero_score(topology: &T) -> Self { - warn!("The network is unhealthy, could not send any packets - returning zero score!"); - let mixes = topology.mix_nodes(); - let providers = topology.providers(); - - let health = mixes - .into_iter() - .map(NodeScore::from_mixnode) - .chain(providers.into_iter().map(NodeScore::from_provider)) - .collect(); - - HealthCheckResult(health) - } - - // TODO: that is O(n) so maybe not the most efficient considering it will be called n times... - fn node_score(&self, node_key: NodeAddressBytes) -> Option { - self.0 - .iter() - .find(|&node_score| node_score.pub_key() == node_key) - .map(|node| node.score()) - } - - pub fn filter_topology_by_score( - &self, - topology: &T, - score_threshold: f64, - ) -> T { - let filtered_mix_nodes = topology - .mix_nodes() - .into_iter() - .filter(|node| { - match self.node_score( - NodeAddressBytes::try_from_base58_string(node.pub_key.clone()).unwrap(), - ) { - None => { - error!("Unknown node in topology - {:?}", node); - false - } - Some(score) => score > score_threshold, - } - }) - .collect(); - - let filtered_provider_nodes = topology - .providers() - .into_iter() - .filter(|node| { - match self.node_score( - NodeAddressBytes::try_from_base58_string(node.pub_key.clone()).unwrap(), - ) { - None => { - error!("Unknown node in topology - {:?}", node); - false - } - Some(score) => score > score_threshold, - } - }) - .collect(); - - let filtered_gateway_nodes = topology - .gateways() - .into_iter() - .filter(|node| { - match self.node_score( - NodeAddressBytes::try_from_base58_string(node.pub_key.clone()).unwrap(), - ) { - None => { - error!("Unknown node in topology - {:?}", node); - false - } - Some(score) => score > score_threshold, - } - }) - .collect(); - - // coco nodes remain unchanged as no healthcheck is being run on them or time being - let filtered_coco_nodes = topology.coco_nodes(); - - T::new_from_nodes( - filtered_mix_nodes, - filtered_provider_nodes, - filtered_coco_nodes, - filtered_gateway_nodes, - ) - } - - fn generate_check_id() -> [u8; 16] { - let mut id = [0u8; 16]; - let mut rng = rand_os::OsRng::new().unwrap(); - rng.fill_bytes(&mut id); - id - } - - pub async fn calculate( - topology: &T, - iterations: usize, - resolution_timeout: Duration, - connection_timeout: Duration, - identity_keys: &MixIdentityKeyPair, - ) -> Self { - // currently healthchecker supports only up to 255 iterations - if we somehow - // find we need more, it's relatively easy change - assert!(iterations <= 255); - - let check_id = Self::generate_check_id(); - - let all_paths = match topology.all_paths() { - Ok(paths) => paths, - Err(_) => return Self::zero_score(topology), - }; - - // create entries for all nodes - let mut score_map = HashMap::new(); - topology.mix_nodes().into_iter().for_each(|node| { - score_map.insert(node.get_pub_key_bytes(), NodeScore::from_mixnode(node)); - }); - - topology.providers().into_iter().for_each(|node| { - score_map.insert(node.get_pub_key_bytes(), NodeScore::from_provider(node)); - }); - - let providers = topology.providers(); - - let mut path_checker = - PathChecker::new(providers, identity_keys, connection_timeout, check_id).await; - for i in 0..iterations { - debug!("running healthcheck iteration {} / {}", i + 1, iterations); - for path in &all_paths { - path_checker.send_test_packet(&path, i as u8).await; - // increase sent count for each node - for node in path { - let current_node_score = score_map.get_mut(&node.pub_key.0).unwrap(); - current_node_score.increase_sent_packet_count(); - } - } - } - - debug!( - "waiting {:?} for pending requests to resolve", - resolution_timeout - ); - tokio::time::delay_for(resolution_timeout).await; - path_checker.resolve_pending_checks().await; - - let all_statuses = path_checker.get_all_statuses(); - for (path_key, status) in all_statuses.into_iter() { - let node_keys = PathChecker::path_key_to_node_keys(path_key); - for node in node_keys { - if status == PathStatus::Healthy { - let current_node_score = score_map.get_mut(&node).unwrap(); - current_node_score.increase_received_packet_count(); - } - } - } - - HealthCheckResult(score_map.into_iter().map(|(_, v)| v).collect()) - } -} diff --git a/common/healthcheck/src/score.rs b/common/healthcheck/src/score.rs deleted file mode 100644 index fbdd1f985c..0000000000 --- a/common/healthcheck/src/score.rs +++ /dev/null @@ -1,181 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// 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. - -use log::error; -use nymsphinx::NodeAddressBytes; -use std::cmp::Ordering; -use std::fmt::Error; -use std::fmt::Formatter; -use std::net::SocketAddr; -use topology::mix; -use topology::provider; - -// TODO: should 'nodetype' really be part of healthcheck::score - -#[derive(Debug, Eq, PartialEq, PartialOrd, Ord, Clone, Copy)] -pub(crate) enum NodeType { - Mix, - MixProvider, -} - -impl std::fmt::Display for NodeType { - fn fmt(&self, f: &mut Formatter) -> Result<(), Error> { - match self { - NodeType::Mix => write!(f, "Mix"), - NodeType::MixProvider => write!(f, "MixProvider"), - } - } -} - -#[derive(Debug, Eq)] -pub(crate) struct NodeScore { - typ: NodeType, - pub_key: NodeAddressBytes, - addresses: Vec, - version: String, - layer: String, - packets_sent: u64, - packets_received: u64, -} - -impl Ord for NodeScore { - // order by: version, layer, sent, received, pubkey; ignore addresses - #[allow(clippy::comparison_chain)] - fn cmp(&self, other: &Self) -> Ordering { - if self.typ > other.typ { - return Ordering::Greater; - } else if self.typ < other.typ { - return Ordering::Less; - } - if self.version > other.version { - return Ordering::Greater; - } else if self.version < other.version { - return Ordering::Less; - } - if self.layer > other.layer { - return Ordering::Greater; - } else if self.layer < other.layer { - return Ordering::Less; - } - if self.packets_sent > other.packets_sent { - return Ordering::Greater; - } else if self.packets_sent < other.packets_sent { - return Ordering::Less; - } - if self.packets_received > other.packets_received { - return Ordering::Greater; - } else if self.packets_received < other.packets_received { - return Ordering::Less; - } - if self.pub_key > other.pub_key { - return Ordering::Greater; - } else if self.pub_key < other.pub_key { - return Ordering::Less; - } - - Ordering::Equal - } -} - -impl PartialOrd for NodeScore { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl PartialEq for NodeScore { - fn eq(&self, other: &Self) -> bool { - self.typ == other.typ - && self.pub_key == other.pub_key - && self.addresses == other.addresses - && self.version == other.version - && self.layer == other.layer - && self.packets_sent == other.packets_sent - && self.packets_received == other.packets_received - } -} - -impl NodeScore { - pub(crate) fn from_mixnode(node: mix::Node) -> Self { - NodeScore { - typ: NodeType::Mix, - pub_key: NodeAddressBytes::try_from_base58_string(node.pub_key).unwrap(), - addresses: vec![node.host], - version: node.version, - layer: format!("layer {}", node.layer), - packets_sent: 0, - packets_received: 0, - } - } - - pub(crate) fn from_provider(node: provider::Node) -> Self { - NodeScore { - typ: NodeType::MixProvider, - pub_key: NodeAddressBytes::try_from_base58_string(node.pub_key).unwrap(), // just to make it compile - I think we should just retire the whole crate... - addresses: vec![node.mixnet_listener, node.client_listener], - version: node.version, - layer: "provider".to_string(), - packets_sent: 0, - packets_received: 0, - } - } - - pub(crate) fn increase_sent_packet_count(&mut self) { - self.packets_sent += 1; - } - - pub(crate) fn increase_received_packet_count(&mut self) { - self.packets_received += 1; - } - - pub(crate) fn score(&self) -> f64 { - match self.packets_sent { - 0 => 0.0, - _ => (self.packets_received as f64 / self.packets_sent as f64) * 100.0, - } - } - - pub(crate) fn pub_key(&self) -> NodeAddressBytes { - self.pub_key.clone() - } -} - -impl std::fmt::Display for NodeScore { - fn fmt(&self, f: &mut Formatter) -> Result<(), std::fmt::Error> { - let fmtd_addresses = match self.addresses.len() { - 1 => format!("{}", self.addresses[0]), - 2 => format!("{}, {}", self.addresses[0], self.addresses[1]), - n => { - error!( - "could not format score - node has {} addresses while only 1 or 2 are allowed!", - n - ); - return Err(std::fmt::Error); - } - }; - let stringified_key = self.pub_key.to_base58_string(); - write!( - f, - "({})\t\t{}/{}\t({:.2}%) \t|| {}\tv{} <{}> - {}", - self.typ, - self.packets_received, - self.packets_sent, - self.score(), - self.layer, - self.version, - fmtd_addresses, - stringified_key, - ) - } -} diff --git a/validator/Cargo.toml b/validator/Cargo.toml index 0472a694e9..411e204c92 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -29,7 +29,6 @@ tokio = { version = "0.2", features = ["full"] } crypto = {path = "../common/crypto"} config = {path = "../common/config"} directory-client = { path = "../common/client-libs/directory-client" } -healthcheck = {path = "../common/healthcheck" } topology = {path = "../common/topology"} [build-dependencies] From e849e45b126b23d214a93ac0b6dac0a665bcd543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 30 Jun 2020 12:01:45 +0100 Subject: [PATCH 12/76] Feature/gateway shared key generation (#272) * Changed identity keypair to use ed25519 * Encryption key is now x25519 based + compatibiltiy with sphinx * Pathing and import fixes * Moved all asymmetric keys to sub-module in crypto * Extracted aes to separate module * kdf module in crypto * Ability to perform diffie hellman on encryption keys * ecdsa on identity keys * Extremely rough and incomplete registration handshake * Authentication primitives * Creating new random authenticationIV * Wrapper type for the derived shared key * Removed AuthToken in favour of using SharedKey for authentication * Gateway identity keys * Registration handshake without error mapping * Gateway address in client config * Added extra key for gateway presence * Updated pemstore to work on borrows instead * Gateway client trying to perform the handshake * Gateway changes to allow for handshake and shared key * Debug trait on sharedkey * native client using updated gateway client * Slightly updated gateway API * Minor cleanup * Fixed pemstore to correctly save multiple keypairs * Gateway actually deriving shared key during handshake * Gateway sending correct mid-handshake message * Missing quotation mark in client config template * Fixed template for correct shared key serialization * Fixed gateway authentication * Fixed tests * Using correct gateway key when converting to sphinx node * "get_all_clients" takes them from gateways as opposed to providers now * cargo fmt * Renamed pemstore methods * Unused import * Encryption of forward requests between client and gateway * Updated sphinx dependency to use public revision * Sending 'error' on handshake processing error * Removed some dead code --- Cargo.lock | 144 +++++++-- clients/native/Cargo.toml | 1 - clients/native/src/client/mod.rs | 87 +++--- clients/native/src/client/received_buffer.rs | 1 - clients/native/src/client/topology_control.rs | 28 +- clients/native/src/commands/init.rs | 99 ++++-- clients/native/src/commands/run.rs | 8 +- clients/native/src/config/mod.rs | 27 +- clients/native/src/config/template.rs | 7 +- clients/webassembly/src/lib.rs | 34 +-- clients/webassembly/src/models/keys.rs | 4 +- .../models/src/presence/gateways.rs | 9 +- .../src/requests/presence_gateways_post.rs | 3 +- .../src/requests/presence_topology_get.rs | 9 +- common/client-libs/gateway-client/Cargo.toml | 1 + .../client-libs/gateway-client/src/error.rs | 20 +- common/client-libs/gateway-client/src/lib.rs | 208 +++++++------ common/crypto/Cargo.toml | 17 +- .../crypto/src/asymmetric/encryption/mod.rs | 273 +++++++++++++++++ common/crypto/src/asymmetric/identity/mod.rs | 186 ++++++++++++ common/crypto/src/asymmetric/mod.rs | 16 + common/crypto/src/encryption/mod.rs | 153 ---------- common/crypto/src/identity/mod.rs | 160 ---------- common/crypto/src/kdf/blake3_hkdf.rs | 39 +++ common/crypto/src/kdf/mod.rs | 15 + common/crypto/src/lib.rs | 10 +- common/crypto/src/symmetric/aes_ctr.rs | 138 +++++++++ common/crypto/src/symmetric/mod.rs | 15 + common/nymsphinx/acknowledgements/Cargo.toml | 7 +- .../acknowledgements/src/identifier.rs | 56 ++-- common/nymsphinx/types/Cargo.toml | 2 +- common/nymsphinx/types/src/lib.rs | 2 +- common/pemstore/src/pemstore.rs | 112 ++++--- common/topology/src/gateway.rs | 12 +- common/topology/src/lib.rs | 4 +- common/topology/src/mix.rs | 2 +- common/topology/src/provider.rs | 2 +- gateway/Cargo.toml | 2 - gateway/gateway-requests/Cargo.toml | 8 +- gateway/gateway-requests/src/auth_token.rs | 125 -------- .../src/authentication/encrypted_address.rs | 91 ++++++ .../gateway-requests/src/authentication/iv.rs | 86 ++++++ .../src/authentication/mod.rs | 16 + gateway/gateway-requests/src/lib.rs | 6 +- .../src/registration/handshake/client.rs | 130 ++++++++ .../src/registration/handshake/error.rs | 52 ++++ .../src/registration/handshake/gateway.rs | 123 ++++++++ .../src/registration/handshake/mod.rs | 81 +++++ .../src/registration/handshake/shared_key.rs | 86 ++++++ .../src/registration/handshake/state.rs | 256 ++++++++++++++++ .../gateway-requests/src/registration/mod.rs | 20 ++ gateway/gateway-requests/src/types.rs | 136 +++++++-- gateway/src/commands/init.rs | 11 +- gateway/src/commands/run.rs | 28 +- gateway/src/config/mod.rs | 37 +++ gateway/src/config/persistence/pathfinder.rs | 10 +- gateway/src/config/template.rs | 6 + .../node/client_handling/clients_handler.rs | 107 ++++--- .../websocket/connection_handler.rs | 282 +++++++++++++----- .../client_handling/websocket/listener.rs | 13 +- .../receiver/packet_processing.rs | 12 +- gateway/src/node/mod.rs | 43 ++- gateway/src/node/presence/mod.rs | 18 +- gateway/src/node/storage/ledger.rs | 69 +++-- mixnode/src/commands/init.rs | 4 +- mixnode/src/commands/run.rs | 4 +- mixnode/src/node/mod.rs | 2 +- mixnode/src/node/packet_processing.rs | 6 +- 68 files changed, 2736 insertions(+), 1045 deletions(-) create mode 100644 common/crypto/src/asymmetric/encryption/mod.rs create mode 100644 common/crypto/src/asymmetric/identity/mod.rs create mode 100644 common/crypto/src/asymmetric/mod.rs delete mode 100644 common/crypto/src/encryption/mod.rs delete mode 100644 common/crypto/src/identity/mod.rs create mode 100644 common/crypto/src/kdf/blake3_hkdf.rs create mode 100644 common/crypto/src/kdf/mod.rs create mode 100644 common/crypto/src/symmetric/aes_ctr.rs create mode 100644 common/crypto/src/symmetric/mod.rs delete mode 100644 gateway/gateway-requests/src/auth_token.rs create mode 100644 gateway/gateway-requests/src/authentication/encrypted_address.rs create mode 100644 gateway/gateway-requests/src/authentication/iv.rs create mode 100644 gateway/gateway-requests/src/authentication/mod.rs create mode 100644 gateway/gateway-requests/src/registration/handshake/client.rs create mode 100644 gateway/gateway-requests/src/registration/handshake/error.rs create mode 100644 gateway/gateway-requests/src/registration/handshake/gateway.rs create mode 100644 gateway/gateway-requests/src/registration/handshake/mod.rs create mode 100644 gateway/gateway-requests/src/registration/handshake/shared_key.rs create mode 100644 gateway/gateway-requests/src/registration/handshake/state.rs create mode 100644 gateway/gateway-requests/src/registration/mod.rs diff --git a/Cargo.lock b/Cargo.lock index e02f9e8d59..9a9cb2f1c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -221,8 +221,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330" dependencies = [ "byte-tools", - "crypto-mac", - "digest", + "crypto-mac 0.7.0", + "digest 0.8.1", "opaque-debug", ] @@ -237,6 +237,20 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "blake3" +version = "0.3.4" +source = "git+https://github.com/BLAKE3-team/BLAKE3?rev=4c41a893a00a3ebe7b24529531ccf96d8593a57c#4c41a893a00a3ebe7b24529531ccf96d8593a57c" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "crypto-mac 0.8.0", + "digest 0.9.0", +] + [[package]] name = "block-buffer" version = "0.7.3" @@ -393,6 +407,15 @@ dependencies = [ "vec_map", ] +[[package]] +name = "clear_on_drop" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97276801e127ffb46b66ce23f35cc96bd454fa311294bced4bbace7baa8b1d17" +dependencies = [ + "cc", +] + [[package]] name = "cloudabi" version = "0.0.3" @@ -542,13 +565,16 @@ dependencies = [ name = "crypto" version = "0.1.0" dependencies = [ + "aes-ctr 0.4.0", + "blake3", "bs58", - "curve25519-dalek", + "ed25519-dalek", + "hkdf 0.9.0", "log 0.4.8", - "nymsphinx", + "nymsphinx-types", "pretty_env_logger", "rand 0.7.3", - "rand_core 0.5.1", + "x25519-dalek", ] [[package]] @@ -561,6 +587,16 @@ dependencies = [ "subtle 1.0.0", ] +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array 0.14.1", + "subtle 2.2.2", +] + [[package]] name = "ctr" version = "0.3.2" @@ -587,7 +623,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26778518a7f6cffa1d25a44b602b62b979bd88adb9e99ffec546998cf3404839" dependencies = [ "byteorder", - "digest", + "digest 0.8.1", "rand_core 0.5.1", "subtle 2.2.2", "zeroize", @@ -630,6 +666,15 @@ dependencies = [ "generic-array 0.12.3", ] +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.1", +] + [[package]] name = "directory-client" version = "0.1.0" @@ -686,6 +731,18 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3" +[[package]] +name = "ed25519-dalek" +version = "1.0.0-pre.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978710b352437433c97b2bff193f2fb1dfd58a093f863dd95e225a19baa599a2" +dependencies = [ + "clear_on_drop", + "curve25519-dalek", + "rand 0.7.3", + "sha2", +] + [[package]] name = "either" version = "1.5.3" @@ -961,6 +1018,7 @@ dependencies = [ name = "gateway-client" version = "0.1.0" dependencies = [ + "crypto", "futures 0.3.4", "gateway-requests", "log 0.4.8", @@ -974,7 +1032,11 @@ name = "gateway-requests" version = "0.1.0" dependencies = [ "bs58", + "crypto", + "futures 0.3.4", + "log 0.4.8", "nymsphinx", + "rand 0.7.3", "serde", "serde_json", "tokio-tungstenite", @@ -1089,8 +1151,18 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fa08a006102488bd9cd5b8013aabe84955cf5ae22e304c2caf655b633aefae3" dependencies = [ - "digest", - "hmac", + "digest 0.8.1", + "hmac 0.7.1", +] + +[[package]] +name = "hkdf" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe1149865383e4526a43aee8495f9a325f0b806c63ce6427d06336a590abbbc9" +dependencies = [ + "digest 0.9.0", + "hmac 0.8.0", ] [[package]] @@ -1099,8 +1171,18 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5dcb5e64cda4c23119ab41ba960d1e170a774c8e4b9d9e6a9bc18aabf5e59695" dependencies = [ - "crypto-mac", - "digest", + "crypto-mac 0.7.0", + "digest 0.8.1", +] + +[[package]] +name = "hmac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b87b580bd66811cc2324a27f3587de707cacf7525b96dca8122f7493e6cce0da" +dependencies = [ + "crypto-mac 0.8.0", + "digest 0.9.0", ] [[package]] @@ -1723,7 +1805,6 @@ dependencies = [ "clap", "config", "crypto", - "curve25519-dalek", "directory-client", "dirs", "dotenv", @@ -1779,7 +1860,6 @@ dependencies = [ "dotenv", "futures 0.3.4", "gateway-requests", - "hmac", "log 0.4.8", "mixnet-client", "nymsphinx", @@ -1787,7 +1867,6 @@ dependencies = [ "pretty_env_logger", "rand 0.7.3", "serde", - "sha2", "sled", "tempfile", "tokio 0.2.16", @@ -1868,7 +1947,7 @@ dependencies = [ name = "nymsphinx-acknowledgements" version = "0.1.0" dependencies = [ - "aes-ctr 0.4.0", + "crypto", "nymsphinx-addressing", "nymsphinx-params", "nymsphinx-types", @@ -2786,7 +2865,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" dependencies = [ "block-buffer", - "digest", + "digest 0.8.1", "fake-simd", "opaque-debug", ] @@ -2798,7 +2877,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27044adfd2e1f077f649f59deb9490d3941d674002f7d062870a60ebe9bd47a0" dependencies = [ "block-buffer", - "digest", + "digest 0.8.1", "fake-simd", "opaque-debug", ] @@ -2877,7 +2956,7 @@ dependencies = [ [[package]] name = "sphinx" version = "0.1.0" -source = "git+https://github.com/nymtech/sphinx?rev=cd9f09b85de33c60030ba6d7fae613c42cbe1faf#cd9f09b85de33c60030ba6d7fae613c42cbe1faf" +source = "git+https://github.com/nymtech/sphinx?rev=f31efdbf667952440bd9af5eb0c140135fd1bc4c#f31efdbf667952440bd9af5eb0c140135fd1bc4c" dependencies = [ "aes-ctr 0.3.0", "arrayref", @@ -2886,12 +2965,11 @@ dependencies = [ "byteorder", "chacha", "curve25519-dalek", - "hkdf", - "hmac", + "hkdf 0.8.0", + "hmac 0.7.1", "lioness", "log 0.4.8", "rand 0.7.3", - "rand_core 0.5.1", "rand_distr", "sha2", ] @@ -3698,8 +3776,34 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "x25519-dalek" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637ff90c9540fa3073bb577e65033069e4bae7c79d49d74aa3ffdf5342a53217" +dependencies = [ + "curve25519-dalek", + "rand_core 0.5.1", + "zeroize", +] + [[package]] name = "zeroize" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cbac2ed2ba24cc90f5e06485ac8c7c1e5449fe8911aef4d8877218af021a5b8" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de251eec69fc7c1bc3923403d18ececb929380e016afe103da75f396704f8ca2" +dependencies = [ + "proc-macro2 1.0.10", + "quote 1.0.3", + "syn", + "synstructure", +] diff --git a/clients/native/Cargo.toml b/clients/native/Cargo.toml index ed7662676a..d1df1f7071 100644 --- a/clients/native/Cargo.toml +++ b/clients/native/Cargo.toml @@ -14,7 +14,6 @@ path = "src/lib.rs" [dependencies] bs58 = "0.3.0" clap = "2.33.0" -curve25519-dalek = "2.0.0" dirs = "2.0.2" dotenv = "0.15.0" futures = "0.3.1" diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 27344223fa..6e73884bbd 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -24,13 +24,13 @@ use crate::client::topology_control::{ }; use crate::config::{Config, SocketType}; use crate::websocket; -use crypto::identity::MixIdentityKeyPair; +use crypto::asymmetric::identity; use futures::channel::mpsc; use gateway_client::{ AcknowledgementReceiver, AcknowledgementSender, GatewayClient, MixnetMessageReceiver, MixnetMessageSender, }; -use gateway_requests::auth_token::AuthToken; +use gateway_requests::registration::handshake::SharedKey; use log::*; use nymsphinx::acknowledgements::identifier::AckAes128Key; use nymsphinx::addressing::clients::Recipient; @@ -50,7 +50,7 @@ pub(crate) mod topology_control; pub struct NymClient { config: Config, runtime: Runtime, - identity_keypair: MixIdentityKeyPair, + identity_keypair: Arc, // to be used by "send" function or socket, etc input_tx: Option, @@ -60,11 +60,11 @@ pub struct NymClient { } impl NymClient { - pub fn new(config: Config, identity_keypair: MixIdentityKeyPair) -> Self { + pub fn new(config: Config, identity_keypair: identity::KeyPair) -> Self { NymClient { runtime: Runtime::new().unwrap(), config, - identity_keypair, + identity_keypair: Arc::new(identity_keypair), input_tx: None, receive_tx: None, } @@ -72,7 +72,7 @@ impl NymClient { pub fn as_mix_recipient(&self) -> Recipient { Recipient::new( - self.identity_keypair.public_key.derive_address(), + self.identity_keypair.public_key().derive_address(), // TODO: below only works under assumption that gateway address == gateway id // (which currently is true) NodeAddressBytes::try_from_base58_string(self.config.get_gateway_id()).unwrap(), @@ -159,65 +159,55 @@ impl NymClient { &mut self, mixnet_message_sender: MixnetMessageSender, ack_sender: AcknowledgementSender, - gateway_address: url::Url, ) -> GatewayClient<'static, url::Url> { - let auth_token = self - .config - .get_gateway_auth_token() - .map(|str_token| AuthToken::try_from_base58_string(str_token).ok()) - .unwrap_or(None); + let gateway_id = self.config.get_gateway_id(); + if gateway_id.is_empty() { + panic!("The identity of the gateway is unknown - did you run `nym-client` init?") + } + let gateway_address_str = self.config.get_gateway_listener(); + if gateway_address_str.is_empty() { + panic!("The address of the gateway is unknown - did you run `nym-client` init?") + } + + let gateway_identity = identity::PublicKey::from_base58_string(gateway_id) + .expect("provided gateway id is invalid!"); + + // TODO: since we presumably now get something valid-ish from the `init`, can we just + // ditch url::Url and operate on `String`? + let gateway_address = + url::Url::parse(&gateway_address_str).expect("provided gateway address is invalid!"); + + let shared_key = self.config.get_gateway_shared_key().map(|str_shared_key| { + SharedKey::try_from_base58_string(str_shared_key) + .expect("The stored shared key is invalid!") + }); let mut gateway_client = GatewayClient::new( gateway_address, - self.as_mix_recipient().destination(), - auth_token, + Arc::clone(&self.identity_keypair), + gateway_identity, + shared_key, mixnet_message_sender, ack_sender, self.config.get_gateway_response_timeout(), ); - let auth_token = self.runtime.block_on(async { + let shared_key = self.runtime.block_on(async { gateway_client - .establish_connection() + .authenticate_and_start() .await - .expect("could not establish initial connection with the gateway"); - gateway_client - .perform_initial_authentication() - .await - .expect("could not perform initial authentication with the gateway") + .expect("could not authenticate and start up the gateway connection") }); - // TODO: if we didn't have an auth_token initially, save it to config or something? + // TODO: if we didn't have a shared_key initially, save it to config or something? info!( "Performed initial authentication. Auth token is {:?}", - auth_token.to_base58_string() + shared_key.to_base58_string() ); gateway_client } - // TODO: this information should be just put in the config on init... - async fn get_gateway_address( - gateway_id: String, - topology_accessor: TopologyAccessor, - ) -> url::Url { - // we already have our gateway written in the config - let gateway_address = topology_accessor - .get_gateway_socket_url(&gateway_id) - .await - .unwrap_or_else(|| { - panic!( - "Could not find gateway with id {:?}.\ - It does not seem to be present in the current network topology.\ - Are you sure it is still online?\ - Perhaps try to run `nym-client init` again to obtain a new gateway", - gateway_id - ) - }); - - url::Url::parse(&gateway_address).expect("provided gateway address is invalid!") - } - // future responsible for periodically polling directory server and updating // the current global view of topology fn start_topology_refresher( @@ -363,12 +353,7 @@ impl NymClient { mixnet_messages_receiver, ); - let gateway_url = self.runtime.block_on(Self::get_gateway_address( - self.config.get_gateway_id(), - shared_topology_accessor.clone(), - )); - let gateway_client = - self.start_gateway_client(mixnet_messages_sender, ack_sender, gateway_url); + let gateway_client = self.start_gateway_client(mixnet_messages_sender, ack_sender); self.start_mix_traffic_controller(sphinx_message_receiver, gateway_client); diff --git a/clients/native/src/client/received_buffer.rs b/clients/native/src/client/received_buffer.rs index 6db09190aa..3ffad96e80 100644 --- a/clients/native/src/client/received_buffer.rs +++ b/clients/native/src/client/received_buffer.rs @@ -18,7 +18,6 @@ use futures::StreamExt; use gateway_client::MixnetMessageReceiver; use log::*; use nymsphinx::chunking::reconstruction::MessageReconstructor; -use nymsphinx::cover::LOOP_COVER_MESSAGE_PAYLOAD; use std::collections::HashSet; use std::sync::Arc; use tokio::runtime::Handle; diff --git a/clients/native/src/client/topology_control.rs b/clients/native/src/client/topology_control.rs index 126a2bdea0..0a5b6fb8eb 100644 --- a/clients/native/src/client/topology_control.rs +++ b/clients/native/src/client/topology_control.rs @@ -23,7 +23,7 @@ use std::time::Duration; use tokio::runtime::Handle; use tokio::sync::{RwLock, RwLockReadGuard}; use tokio::task::JoinHandle; -use topology::{provider, NymTopology}; +use topology::{gateway, NymTopology}; // I'm extremely curious why compiler NEVER complained about lack of Debug here before #[derive(Debug)] @@ -117,16 +117,16 @@ impl TopologyAccessor { self.inner.write().await.update(new_topology); } - pub(crate) async fn get_gateway_socket_url(&self, id: &str) -> Option { - match &self.inner.read().await.0 { - None => None, - Some(ref topology) => topology - .gateways() - .iter() - .find(|gateway| gateway.pub_key == id) - .map(|gateway| gateway.client_listener.clone()), - } - } + // pub(crate) async fn get_gateway_socket_url(&self, id: &str) -> Option { + // match &self.inner.read().await.0 { + // None => None, + // Some(ref topology) => topology + // .gateways() + // .iter() + // .find(|gateway| gateway.identity_key == id) + // .map(|gateway| gateway.client_listener.clone()), + // } + // } // only used by the client at startup to get a slightly more reasonable error message // (currently displays as unused because healthchecker is disabled due to required changes) @@ -137,15 +137,15 @@ impl TopologyAccessor { } } - pub(crate) async fn get_all_clients(&self) -> Option> { + pub(crate) async fn get_all_clients(&self) -> Option> { // TODO: this will need to be modified to instead return pairs (provider, client) match &self.inner.read().await.0 { None => None, Some(ref topology) => Some( topology - .providers() + .gateways() .iter() - .flat_map(|provider| provider.registered_clients.iter()) + .flat_map(|gateway| gateway.registered_clients.iter()) .cloned() .collect::>(), ), diff --git a/clients/native/src/commands/init.rs b/clients/native/src/commands/init.rs index 006cefb397..81c6982c49 100644 --- a/clients/native/src/commands/init.rs +++ b/clients/native/src/commands/init.rs @@ -17,12 +17,12 @@ use crate::commands::override_config; use crate::config::persistence::pathfinder::ClientPathfinder; use clap::{App, Arg, ArgMatches}; use config::NymConfig; -use crypto::identity::MixIdentityKeyPair; +use crypto::asymmetric::identity; use directory_client::DirectoryClient; use gateway_client::GatewayClient; -use gateway_requests::AuthToken; -use nymsphinx::DestinationAddressBytes; +use gateway_requests::registration::handshake::SharedKey; use pemstore::pemstore::PemStore; +use std::sync::Arc; use std::time::Duration; use topology::gateway::Node; use topology::NymTopology; @@ -65,21 +65,36 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { async fn try_gateway_registration( gateways: Vec, - our_address: DestinationAddressBytes, -) -> Option<(String, AuthToken)> { + our_identity: Arc, +) -> Option<(String, String, SharedKey)> { let timeout = Duration::from_millis(1500); for gateway in gateways { + let gateway_identity = + match identity::PublicKey::from_base58_string(gateway.identity_key.clone()) { + Ok(id) => id, + Err(_) => { + eprintln!( + "gateway {} announces invalid identity!", + gateway.identity_key + ); + continue; + } + }; + let mut gateway_client = GatewayClient::new_init( url::Url::parse(&gateway.client_listener).unwrap(), - our_address.clone(), + gateway_identity, + our_identity.clone(), timeout, ); - if let Ok(token) = gateway_client.register_without_listening().await { - if let Err(err) = gateway_client.close_connection().await { - eprintln!("Error while closing connection to the gateway! - {:?}", err); - continue; - } else { - return Some((gateway.pub_key, token)); + if let Ok(_) = gateway_client.establish_connection().await { + if let Ok(shared_key) = gateway_client.register().await { + if let Err(err) = gateway_client.close_connection().await { + eprintln!("Error while closing connection to the gateway! - {:?}", err); + continue; + } else { + return Some((gateway.identity_key, gateway.client_listener, shared_key)); + } } } } @@ -88,8 +103,8 @@ async fn try_gateway_registration( async fn choose_gateway( directory_server: String, - our_address: DestinationAddressBytes, -) -> (String, AuthToken) { + our_identity: Arc, +) -> (String, String, SharedKey) { let directory_client_config = directory_client::Config::new(directory_server.clone()); let directory_client = directory_client::Client::new(directory_client_config); let topology = directory_client.get_topology().await.unwrap(); @@ -101,7 +116,7 @@ async fn choose_gateway( // try to perform registration so that we wouldn't need to do it at startup // + at the same time we'll know if we can actually talk with that gateway - let registration_result = try_gateway_registration(gateways, our_address).await; + let registration_result = try_gateway_registration(gateways, our_identity).await; match registration_result { None => { // while technically there's no issue client-side, it will be impossible to execute @@ -113,10 +128,26 @@ async fn choose_gateway( directory_server ) } - Some((gateway_id, auth_token)) => (gateway_id, auth_token), + Some((gateway_id, gateway_listener, shared_key)) => { + (gateway_id, gateway_listener, shared_key) + } } } +async fn get_gateway_listener(directory_server: String, gateway_identity: &str) -> Option { + let directory_client_config = directory_client::Config::new(directory_server); + let directory_client = directory_client::Client::new(directory_client_config); + let topology = directory_client.get_topology().await.unwrap(); + let gateways = topology.gateways(); + + for gateway in gateways { + if gateway.identity_key == gateway_identity { + return Some(gateway.client_listener); + } + } + None +} + pub fn execute(matches: &ArgMatches) { println!("Initialising client..."); @@ -128,26 +159,41 @@ pub fn execute(matches: &ArgMatches) { config = config.set_high_default_traffic_volume(); } - let mix_identity_keys = MixIdentityKeyPair::new(); + let mix_identity_keys = Arc::new(identity::KeyPair::new()); // if there is no gateway chosen, get a random-ish one from the topology if config.get_gateway_id().is_empty() { - let our_address = mix_identity_keys.public_key().derive_address(); // TODO: is there perhaps a way to make it work without having to spawn entire runtime? let mut rt = tokio::runtime::Runtime::new().unwrap(); - let (gateway_id, auth_token) = - rt.block_on(choose_gateway(config.get_directory_server(), our_address)); + let (gateway_id, gateway_listener, shared_key) = rt.block_on(choose_gateway( + config.get_directory_server(), + mix_identity_keys.clone(), + )); - // TODO: this isn't really a gateway, but gateway, yet another change to make config = config .with_gateway_id(gateway_id) - .with_gateway_auth_token(auth_token); + .with_gateway_listener(gateway_listener) + .with_gateway_shared_key(shared_key.to_base58_string()); + } + + // we specified our gateway but don't know its physical address + if config.get_gateway_listener().is_empty() { + // TODO: is there perhaps a way to make it work without having to spawn entire runtime? + let mut rt = tokio::runtime::Runtime::new().unwrap(); + let gateway_listener = rt + .block_on(get_gateway_listener( + config.get_directory_server(), + &config.get_gateway_id(), + )) + .expect("No gateway with provided id exists!"); + + config = config.with_gateway_listener(gateway_listener); } let pathfinder = ClientPathfinder::new_from_config(&config); let pem_store = PemStore::new(pathfinder); pem_store - .write_identity(mix_identity_keys) + .write_identity_keypair(mix_identity_keys.as_ref()) .expect("Failed to save identity keys"); println!("Saved mixnet identity keypair"); @@ -161,11 +207,8 @@ pub fn execute(matches: &ArgMatches) { "Unless overridden in all `nym-client run` we will be talking to the following gateway: {}...", config.get_gateway_id(), ); - if config.get_gateway_auth_token().is_some() { - println!( - "using optional AuthToken: {:?}", - config.get_gateway_auth_token().unwrap() - ) + if let Some(shared_key) = config.get_gateway_shared_key() { + println!("using optional SharedKey: {:?}", shared_key) } println!("Client configuration completed.\n\n\n") } diff --git a/clients/native/src/commands/run.rs b/clients/native/src/commands/run.rs index 964cc3116b..776312a844 100644 --- a/clients/native/src/commands/run.rs +++ b/clients/native/src/commands/run.rs @@ -17,7 +17,7 @@ use crate::commands::override_config; use crate::config::{persistence::pathfinder::ClientPathfinder, Config}; use clap::{App, Arg, ArgMatches}; use config::NymConfig; -use crypto::identity::MixIdentityKeyPair; +use crypto::asymmetric::identity; use pemstore::pemstore::PemStore; pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { @@ -57,13 +57,13 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { ) } -fn load_identity_keys(config_file: &Config) -> MixIdentityKeyPair { +fn load_identity_keys(config_file: &Config) -> identity::KeyPair { let identity_keypair = PemStore::new(ClientPathfinder::new_from_config(&config_file)) - .read_identity() + .read_identity_keypair() .expect("Failed to read stored identity key files"); println!( "Public identity key: {}\n", - identity_keypair.public_key.to_base58_string() + identity_keypair.public_key().to_base58_string() ); identity_keypair } diff --git a/clients/native/src/config/mod.rs b/clients/native/src/config/mod.rs index 7711129b9c..e5d2b75ac8 100644 --- a/clients/native/src/config/mod.rs +++ b/clients/native/src/config/mod.rs @@ -126,8 +126,13 @@ impl Config { self } - pub fn with_gateway_auth_token>(mut self, token: S) -> Self { - self.client.gateway_authtoken = Some(token.into()); + pub fn with_gateway_listener>(mut self, gateway_listener: S) -> Self { + self.client.gateway_listener = gateway_listener.into(); + self + } + + pub fn with_gateway_shared_key>(mut self, shared_key: S) -> Self { + self.client.gateway_shared_key = Some(shared_key.into()); self } @@ -174,8 +179,12 @@ impl Config { self.client.gateway_id.clone() } - pub fn get_gateway_auth_token(&self) -> Option { - self.client.gateway_authtoken.clone() + pub fn get_gateway_listener(&self) -> String { + self.client.gateway_listener.clone() + } + + pub fn get_gateway_shared_key(&self) -> Option { + self.client.gateway_shared_key.clone() } pub fn get_socket_type(&self) -> SocketType { @@ -255,10 +264,13 @@ pub struct Client { /// If initially omitted, a random gateway will be chosen from the available topology. gateway_id: String, - /// A gateway specific, optional, base58 stringified authentication token used for + /// Address of the gateway listener to which all client requests should be sent. + gateway_listener: String, + + /// A gateway specific, optional, base58 stringified shared key used for /// communication with particular gateway. #[serde(deserialize_with = "de_option_string")] - gateway_authtoken: Option, + gateway_shared_key: Option, /// nym_home_directory specifies absolute path to the home nym Clients directory. /// It is expected to use default value and hence .toml file should not redefine this field. @@ -274,7 +286,8 @@ impl Default for Client { private_identity_key_file: Default::default(), public_identity_key_file: Default::default(), gateway_id: "".to_string(), - gateway_authtoken: None, + gateway_listener: "".to_string(), + gateway_shared_key: None, nym_root_directory: Config::default_root_directory(), } } diff --git a/clients/native/src/config/template.rs b/clients/native/src/config/template.rs index 590e567584..543ff65394 100644 --- a/clients/native/src/config/template.rs +++ b/clients/native/src/config/template.rs @@ -41,9 +41,12 @@ public_identity_key_file = '{{ client.public_identity_key_file }}' # ID of the gateway from which the client should be fetching messages. gateway_id = '{{ client.gateway_id }}' -# A gateway specific, optional, base58 stringified authentication token used for +# Address of the gateway listener to which all client requests should be sent. +gateway_listener = '{{ client.gateway_listener }}' + +# A gateway specific, optional, base58 stringified shared key used for # communication with particular gateway. -gateway_authtoken = '{{ client.gateway_authtoken }}' +gateway_shared_key = '{{ client.gateway_shared_key }}' ##### advanced configuration options ##### diff --git a/clients/webassembly/src/lib.rs b/clients/webassembly/src/lib.rs index 9466912cf9..06967d94c6 100644 --- a/clients/webassembly/src/lib.rs +++ b/clients/webassembly/src/lib.rs @@ -11,7 +11,6 @@ // 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. -use crypto::identity::MixIdentityPublicKey; use models::topology::Topology; use nymsphinx::addressing::nodes::NymNodeRoutingAddress; use nymsphinx::Node as SphinxNode; @@ -26,6 +25,7 @@ use wasm_bindgen::prelude::*; mod models; mod utils; +use crypto::asymmetric::encryption; pub use models::keys::keygen; use nymsphinx::addressing::clients::Recipient; use topology::NymTopology; @@ -114,19 +114,15 @@ fn sphinx_route_to(topology_json: &str, gateway_address: &NodeAddressBytes) -> V } impl TryFrom for SphinxNode { + // We really should start actually using errors rather than unwrapping on everything type Error = (); fn try_from(node_data: NodeData) -> Result { let addr: SocketAddr = node_data.address.parse().unwrap(); let address: NodeAddressBytes = NymNodeRoutingAddress::from(addr).try_into().unwrap(); - - // this has to be temporarily moved out of separate function as we can't return private types - let pub_key = { - let src = MixIdentityPublicKey::from_base58_string(node_data.public_key).to_bytes(); - let mut dest: [u8; 32] = [0; 32]; - dest.copy_from_slice(&src); - nymsphinx::public_key_from_bytes(dest) - }; + let pub_key = encryption::PublicKey::from_base58_string(node_data.public_key) + .unwrap() + .into(); Ok(SphinxNode { address, pub_key }) } @@ -156,7 +152,7 @@ mod test_constructing_a_sphinx_packet { let mut payload = create_sphinx_packet( topology_fixture(), "foomp", - "5pgrc4gPHP2tBQgfezcdJ2ZAjipoAsy6evrqHdxBbVXq@7vhgER4Gz789QHNTSu4apMpTcpTuUaRiLxJnbz1g2HFh", + "5pgrc4gPHP2tBQgfezcdJ2ZAjipoAsy6evrqHdxBbVXq@CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", ); // you don't really need 32 bytes here, but giving too much won't make it fail let mut address_buffer = [0; 32]; @@ -178,7 +174,7 @@ mod building_a_topology_from_json { sphinx_route_to( "", &NodeAddressBytes::try_from_base58_string( - "7vhgER4Gz789QHNTSu4apMpTcpTuUaRiLxJnbz1g2HFh", + "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", ) .unwrap(), ); @@ -190,7 +186,7 @@ mod building_a_topology_from_json { sphinx_route_to( "bad bad bad not json", &NodeAddressBytes::try_from_base58_string( - "7vhgER4Gz789QHNTSu4apMpTcpTuUaRiLxJnbz1g2HFh", + "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", ) .unwrap(), ); @@ -205,7 +201,7 @@ mod building_a_topology_from_json { sphinx_route_to( &json, &NodeAddressBytes::try_from_base58_string( - "7vhgER4Gz789QHNTSu4apMpTcpTuUaRiLxJnbz1g2HFh", + "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", ) .unwrap(), ); @@ -221,7 +217,7 @@ mod building_a_topology_from_json { sphinx_route_to( &json, &NodeAddressBytes::try_from_base58_string( - "7vhgER4Gz789QHNTSu4apMpTcpTuUaRiLxJnbz1g2HFh", + "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", ) .unwrap(), ); @@ -234,7 +230,7 @@ mod building_a_topology_from_json { let route = sphinx_route_to( topology_fixture(), &NodeAddressBytes::try_from_base58_string( - "7vhgER4Gz789QHNTSu4apMpTcpTuUaRiLxJnbz1g2HFh", + "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", ) .unwrap(), ); @@ -248,7 +244,7 @@ mod building_a_topology_from_json { let route = sphinx_route_to( &json, &NodeAddressBytes::try_from_base58_string( - "7vhgER4Gz789QHNTSu4apMpTcpTuUaRiLxJnbz1g2HFh", + "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", ) .unwrap(), ); @@ -316,7 +312,8 @@ fn topology_fixture() -> &'static str { { "clientListener": "139.162.246.48:9000", "mixnetListener": "139.162.246.48:1789", - "pubKey": "7vhgER4Gz789QHNTSu4apMpTcpTuUaRiLxJnbz1g2HFh", + "identityKey": "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", + "sphinxKey": "BnLYqQjb8K6TmW5oFdNZrUTocGxa3rgzBvapQrf8XUbF", "version": "0.6.0", "location": "London, UK", "registeredClients": [ @@ -329,7 +326,8 @@ fn topology_fixture() -> &'static str { { "clientListener": "127.0.0.1:9000", "mixnetListener": "127.0.0.1:1789", - "pubKey": "2XK8RDcUTRcJLUWoDfoXc2uP4YViscMLEM5NSzhSi87M", + "identityKey": "B9xz9V6jpp1fEbDkeyR5f8miorw9bzXGKoMbKnaxkD41", + "sphinxKey": "3KCpz1HCD8DqnQjemT1uuBZipmHFXM4V5btxLXwvM1gG", "version": "0.6.0", "location": "unknown", "registeredClients": [], diff --git a/clients/webassembly/src/models/keys.rs b/clients/webassembly/src/models/keys.rs index e559b7a10f..a75b759b78 100644 --- a/clients/webassembly/src/models/keys.rs +++ b/clients/webassembly/src/models/keys.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crypto::identity::MixIdentityKeyPair; +use crypto::asymmetric::identity; use serde::{Deserialize, Serialize}; use std::convert::{TryFrom, TryInto}; use wasm_bindgen::prelude::*; @@ -42,7 +42,7 @@ impl TryInto for GatewayIdentity { #[wasm_bindgen] pub fn keygen() -> String { - let keypair = MixIdentityKeyPair::new(); + let keypair = identity::KeyPair::new(); let address = keypair.public_key().derive_address(); GatewayIdentity { diff --git a/common/client-libs/directory-client/models/src/presence/gateways.rs b/common/client-libs/directory-client/models/src/presence/gateways.rs index 78ffa78f44..5822dc6a6a 100644 --- a/common/client-libs/directory-client/models/src/presence/gateways.rs +++ b/common/client-libs/directory-client/models/src/presence/gateways.rs @@ -21,7 +21,8 @@ pub struct GatewayPresence { pub location: String, pub client_listener: String, pub mixnet_listener: String, - pub pub_key: String, + pub identity_key: String, + pub sphinx_key: String, pub registered_clients: Vec, pub last_seen: u64, pub version: String, @@ -33,7 +34,8 @@ impl Into for GatewayPresence { location: self.location, client_listener: self.client_listener.parse().unwrap(), mixnet_listener: self.mixnet_listener.parse().unwrap(), - pub_key: self.pub_key, + identity_key: self.identity_key, + sphinx_key: self.sphinx_key, registered_clients: self .registered_clients .into_iter() @@ -51,7 +53,8 @@ impl From for GatewayPresence { location: mpn.location, client_listener: mpn.client_listener.to_string(), mixnet_listener: mpn.mixnet_listener.to_string(), - pub_key: mpn.pub_key, + identity_key: mpn.identity_key, + sphinx_key: mpn.sphinx_key, registered_clients: mpn .registered_clients .into_iter() diff --git a/common/client-libs/directory-client/src/requests/presence_gateways_post.rs b/common/client-libs/directory-client/src/requests/presence_gateways_post.rs index 1ed7adc5e9..80ef05c4c1 100644 --- a/common/client-libs/directory-client/src/requests/presence_gateways_post.rs +++ b/common/client-libs/directory-client/src/requests/presence_gateways_post.rs @@ -90,7 +90,8 @@ mod presence_gateways_post_request { location: "foomp".to_string(), client_listener: "foo.com".to_string(), mixnet_listener: "foo.com".to_string(), - pub_key: "abc".to_string(), + identity_key: "def".to_string(), + sphinx_key: "abc".to_string(), registered_clients: vec![], last_seen: 0, version: "0.1.0".to_string(), diff --git a/common/client-libs/directory-client/src/requests/presence_topology_get.rs b/common/client-libs/directory-client/src/requests/presence_topology_get.rs index d13a0d1f8e..ff44fb9bfb 100644 --- a/common/client-libs/directory-client/src/requests/presence_topology_get.rs +++ b/common/client-libs/directory-client/src/requests/presence_topology_get.rs @@ -48,6 +48,7 @@ mod topology_requests { #[cfg(test)] mod on_a_400_status { use super::*; + #[tokio::test] async fn it_returns_an_error() { let _m = mock("GET", PATH) @@ -60,9 +61,11 @@ mod topology_requests { _m.assert(); } } + #[cfg(test)] mod on_a_200 { use super::*; + #[tokio::test] async fn it_returns_a_response_with_200_status_and_a_correct_topology() { let json = fixtures::topology_response_json(); @@ -208,7 +211,8 @@ mod topology_requests { "location": "unknown", "clientListener": "3.8.176.11:8888", "mixnetListener": "3.8.176.11:9999", - "pubKey": "54U6krAr-j9nQXFlsHk3io04_p0tctuqH71t7w_usgI=", + "identityKey": "B9xz9V6jpp1fEbDkeyR5f8miorw9bzXGKoMbKnaxkD41", + "sphinxKey": "3KCpz1HCD8DqnQjemT1uuBZipmHFXM4V5btxLXwvM1gG", "registeredClients": [ { "pubKey": "zOqdJFH49HcgGSCRnmbXGzovnwRLEPN0YGN1SCafTyo=" @@ -224,7 +228,8 @@ mod topology_requests { "location": "unknown", "clientListener": "3.8.176.12:8888", "mixnetListener": "3.8.176.12:9999", - "pubKey": "sA-sxi038pEbGy4lgZWG-RdHHDkA6kZzu44G0LUxFSc=", + "identityKey": "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", + "sphinxKey": "BnLYqQjb8K6TmW5oFdNZrUTocGxa3rgzBvapQrf8XUbF", "registeredClients": [ { "pubKey": "UE-7r6-bpw0b4T3GxOBVxlg02psx23DF2p5Tuf-OBSE=" diff --git a/common/client-libs/gateway-client/Cargo.toml b/common/client-libs/gateway-client/Cargo.toml index cca78d7e14..88f7dc68e4 100644 --- a/common/client-libs/gateway-client/Cargo.toml +++ b/common/client-libs/gateway-client/Cargo.toml @@ -15,6 +15,7 @@ tokio = { version = "0.2", features = ["macros", "rt-core", "stream", "sync", "t tokio-tungstenite = "0.10" # internal +crypto = { path = "../../crypto" } gateway-requests = { path = "../../../gateway/gateway-requests" } nymsphinx = { path = "../../nymsphinx" } diff --git a/common/client-libs/gateway-client/src/error.rs b/common/client-libs/gateway-client/src/error.rs index 82f4c1d8c1..6112f1bfa3 100644 --- a/common/client-libs/gateway-client/src/error.rs +++ b/common/client-libs/gateway-client/src/error.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use gateway_requests::auth_token::AuthTokenConversionError; +use gateway_requests::registration::handshake::error::HandshakeError; use std::fmt::{self, Error, Formatter}; use tokio_tungstenite::tungstenite::Error as WsError; @@ -21,11 +21,12 @@ pub enum GatewayClientError { ConnectionNotEstablished, GatewayError(String), NetworkError(WsError), - NoAuthTokenAvailable, + NoSharedKeyAvailable, ConnectionAbruptlyClosed, MalformedResponse, NotAuthenticated, ConnectionInInvalidState, + RegistrationFailure(HandshakeError), AuthenticationFailure, Timeout, } @@ -36,12 +37,6 @@ impl From for GatewayClientError { } } -impl From for GatewayClientError { - fn from(_err: AuthTokenConversionError) -> Self { - GatewayClientError::MalformedResponse - } -} - // better human readable representation of the error, mostly so that GatewayClientError // would implement std::error::Error impl fmt::Display for GatewayClientError { @@ -50,8 +45,8 @@ impl fmt::Display for GatewayClientError { GatewayClientError::ConnectionNotEstablished => { write!(f, "connection to the gateway is not established") } - GatewayClientError::NoAuthTokenAvailable => { - write!(f, "no AuthToken was provided or obtained") + GatewayClientError::NoSharedKeyAvailable => { + write!(f, "no shared key was provided or obtained") } GatewayClientError::NotAuthenticated => write!(f, "client is not authenticated"), GatewayClientError::NetworkError(err) => { @@ -66,6 +61,11 @@ impl fmt::Display for GatewayClientError { f, "connection is in an invalid state - please send a bug report" ), + GatewayClientError::RegistrationFailure(handshake_err) => write!( + f, + "failed to finish registration handshake - {}", + handshake_err + ), GatewayClientError::AuthenticationFailure => write!(f, "authentication failure"), GatewayClientError::GatewayError(err) => { write!(f, "gateway returned an error response - {}", err) diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index 2d806d24b4..eb7d1f2821 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -17,12 +17,15 @@ use crate::packet_router::PacketRouter; pub use crate::packet_router::{ AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender, }; +use crypto::asymmetric::identity; use futures::stream::{SplitSink, SplitStream}; -use futures::{future::BoxFuture, FutureExt, SinkExt, StreamExt}; -use gateway_requests::auth_token::AuthToken; +use futures::{future::BoxFuture, FutureExt, SinkExt, Stream, StreamExt}; +use gateway_requests::authentication::encrypted_address::EncryptedAddressBytes; +use gateway_requests::authentication::iv::AuthenticationIV; +use gateway_requests::registration::handshake::{client_handshake, SharedKey, DEFAULT_RNG}; use gateway_requests::{BinaryRequest, ClientControlRequest, ServerResponse}; use log::*; -use nymsphinx::{DestinationAddressBytes, SphinxPacket}; +use nymsphinx::SphinxPacket; use std::convert::TryFrom; use std::net::SocketAddr; use std::sync::Arc; @@ -31,31 +34,26 @@ use tokio::net::TcpStream; use tokio::sync::Notify; use tokio_tungstenite::{ connect_async, - tungstenite::{client::IntoClientRequest, protocol::Message}, + tungstenite::{client::IntoClientRequest, protocol::Message, Error as WsError}, WebSocketStream, }; pub mod error; pub mod packet_router; -// TODO: combine the duplicate reading procedure, i.e. -/* -msg read from conn.next().await: - if msg.is_none() { - res = Some(Err(GatewayClientError::ConnectionAbruptlyClosed)); - break; +/// A helper method to read an underlying message from the stream or return an error. +async fn read_ws_stream_message(conn: &mut S) -> Result +where + S: Stream> + Unpin, +{ + match conn.next().await { + Some(msg) => match msg { + Ok(msg) => Ok(msg), + Err(err) => Err(GatewayClientError::NetworkError(err)), + }, + None => Err(GatewayClientError::ConnectionAbruptlyClosed), } - let msg = match msg.unwrap() { - Ok(msg) => msg, - Err(err) => { - res = Some(Err(GatewayClientError::NetworkError(err))); - break; - } - }; - match msg { - // specific handling - } -*/ +} // TODO: some batching mechanism to allow reading and sending more than a single packet through @@ -94,24 +92,15 @@ impl<'a> PartiallyDelegated<'a> { _ = notify_clone.notified() => { should_return = true; } - msg = stream.next() => { - if msg.is_none() { - return Err(GatewayClientError::ConnectionAbruptlyClosed); - } - let msg = match msg.unwrap() { - Ok(msg) => msg, - Err(err) => { - return Err(GatewayClientError::NetworkError(err)); - } - }; - match msg { + msg = read_ws_stream_message(&mut stream) => { + match msg? { Message::Binary(bin_msg) => { // TODO: some batching mechanism to allow reading and sending more than // one packet at the time, because the receiver can easily handle it packet_router.route_received(vec![bin_msg]) }, // I think that in the future we should perhaps have some sequence number system, i.e. - // so each request/reponse pair can be easily identified, so that if messages are + // so each request/response pair can be easily identified, so that if messages are // not ordered (for some peculiar reason) we wouldn't lose anything. // This would also require NOT discarding any text responses here. Message::Text(_) => debug!("received a text message - probably a response to some previous query!"), @@ -184,32 +173,25 @@ impl<'a> SocketState<'a> { } } -pub struct GatewayClient<'a, R: IntoClientRequest + Unpin + Clone> { +pub struct GatewayClient<'a, R> { authenticated: bool, // can be String, string slices, `url::Url`, `http::Uri`, etc. gateway_address: R, - our_address: DestinationAddressBytes, - auth_token: Option, + gateway_identity: identity::PublicKey, + local_identity: Arc, + shared_key: Option, connection: SocketState<'a>, packet_router: PacketRouter, response_timeout_duration: Duration, } -impl<'a, R: IntoClientRequest + Unpin + Clone> Drop for GatewayClient<'a, R> { - fn drop(&mut self) { - // TODO to fix forcibly closing connection (although now that I think about it, - // I'm not sure this would do it, as to fix the said issue we'd need graceful shutdowns) - } -} - -impl<'a, R> GatewayClient<'static, R> -where - R: IntoClientRequest + Unpin + Clone, -{ +impl<'a, R> GatewayClient<'static, R> { + // TODO: put it all in a Config struct pub fn new( gateway_address: R, - our_address: DestinationAddressBytes, - auth_token: Option, + local_identity: Arc, + gateway_identity: identity::PublicKey, + shared_key: Option, mixnet_message_sender: MixnetMessageSender, ack_sender: AcknowledgementSender, response_timeout_duration: Duration, @@ -217,8 +199,9 @@ where GatewayClient { authenticated: false, gateway_address, - our_address, - auth_token, + gateway_identity, + local_identity, + shared_key, connection: SocketState::NotConnected, packet_router: PacketRouter::new(ack_sender, mixnet_message_sender), response_timeout_duration, @@ -227,7 +210,8 @@ where pub fn new_init( gateway_address: R, - our_address: DestinationAddressBytes, + gateway_identity: identity::PublicKey, + local_identity: Arc, response_timeout_duration: Duration, ) -> Self { use futures::channel::mpsc; @@ -241,30 +225,15 @@ where GatewayClient { authenticated: false, gateway_address, - our_address, - auth_token: None, + gateway_identity, + local_identity, + shared_key: None, connection: SocketState::NotConnected, packet_router, response_timeout_duration, } } - pub async fn register_without_listening(&mut self) -> Result { - if !self.connection.is_established() { - self.establish_connection().await?; - } - let msg = ClientControlRequest::new_register(self.our_address.clone()).into(); - let token = match self.send_websocket_message(msg).await? { - ServerResponse::Register { token } => { - self.authenticated = true; - Ok(AuthToken::try_from_base58_string(token)?) - } - ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), - _ => unreachable!(), - }?; - Ok(token) - } - pub async fn close_connection(&mut self) -> Result<(), GatewayClientError> { if self.connection.is_partially_delegated() { self.recover_socket_connection().await?; @@ -279,7 +248,10 @@ where } } - pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> { + pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> + where + R: IntoClientRequest + Unpin + Clone, + { let ws_stream = match connect_async(self.gateway_address.clone()).await { Ok((ws_stream, _)) => ws_stream, Err(e) => return Err(GatewayClientError::NetworkError(e)), @@ -308,19 +280,12 @@ where } // just keep getting through socket buffer until we get to what we want... // (or we time out) - msg = conn.next() => { - if msg.is_none() { - res = Some(Err(GatewayClientError::ConnectionAbruptlyClosed)); + msg = read_ws_stream_message(conn) => { + if let Err(err) = msg { + res = Some(Err(err)); break; } - let msg = match msg.unwrap() { - Ok(msg) => msg, - Err(err) => { - res = Some(Err(GatewayClientError::NetworkError(err))); - break; - } - }; - match msg { + match msg.unwrap() { Message::Binary(bin_msg) => { self.packet_router.route_received(vec![bin_msg]); } @@ -376,38 +341,47 @@ where } } - pub async fn register(&mut self) -> Result { + pub async fn register(&mut self) -> Result { if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } - let msg = ClientControlRequest::new_register(self.our_address.clone()).into(); - let token = match self.send_websocket_message(msg).await? { - ServerResponse::Register { token } => { - self.authenticated = true; - Ok(AuthToken::try_from_base58_string(token)?) - } - ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), + + debug_assert!(self.connection.is_available()); + + match &mut self.connection { + SocketState::Available(ws_stream) => client_handshake( + &mut DEFAULT_RNG, + ws_stream, + self.local_identity.as_ref(), + self.gateway_identity.clone(), + ) + .await + .map_err(|handshake_err| GatewayClientError::RegistrationFailure(handshake_err)), _ => unreachable!(), - }?; - self.start_listening_for_mixnet_messages()?; - Ok(token) + } } pub async fn authenticate( &mut self, - auth_token: Option, + shared_key: Option, ) -> Result { - if auth_token.is_none() && self.auth_token.is_none() { - return Err(GatewayClientError::NoAuthTokenAvailable); + if shared_key.is_none() && self.shared_key.is_none() { + return Err(GatewayClientError::NoSharedKeyAvailable); } if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } // because of the previous check one of the unwraps MUST succeed - let auth_token = auth_token.unwrap_or_else(|| self.auth_token.unwrap()); + let shared_key = shared_key + .as_ref() + .unwrap_or_else(|| self.shared_key.as_ref().unwrap()); + let iv = AuthenticationIV::new_random(&mut DEFAULT_RNG); + let self_address = self.local_identity.as_ref().public_key().derive_address(); + let encrypted_address = EncryptedAddressBytes::new(&self_address, shared_key, &iv); let msg = - ClientControlRequest::new_authenticate(self.our_address.clone(), auth_token).into(); + ClientControlRequest::new_authenticate(self_address, encrypted_address, iv).into(); + let authenticated = match self.send_websocket_message(msg).await? { ServerResponse::Authenticate { status } => { self.authenticated = status; @@ -416,22 +390,21 @@ where ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)), _ => unreachable!(), }?; - self.start_listening_for_mixnet_messages()?; Ok(authenticated) } - // just a helper method to either call register or authenticate based on self.auth_token value + /// Helper method to either call register or authenticate based on self.shared_key value pub async fn perform_initial_authentication( &mut self, - ) -> Result { - if self.auth_token.is_some() { + ) -> Result { + if self.shared_key.is_some() { self.authenticate(None).await?; } else { self.register().await?; } if self.authenticated { - // if we are authenticated it means we MUST have an associated auth_token - Ok(self.auth_token.clone().unwrap()) + // if we are authenticated it means we MUST have an associated shared_key + Ok(self.shared_key.clone().unwrap()) } else { Err(GatewayClientError::AuthenticationFailure) } @@ -449,7 +422,11 @@ where if !self.connection.is_established() { return Err(GatewayClientError::ConnectionNotEstablished); } - let msg = BinaryRequest::new_forward_request(address, packet).into(); + let msg = BinaryRequest::new_forward_request(address, packet).into_ws_message( + self.shared_key + .as_ref() + .expect("no shared key present even though we're authenticated!"), + ); self.send_websocket_message_without_response(msg).await } @@ -470,7 +447,11 @@ where Ok(()) } - fn start_listening_for_mixnet_messages(&mut self) -> Result<(), GatewayClientError> { + // Note: this requires prior authentication + pub fn start_listening_for_mixnet_messages(&mut self) -> Result<(), GatewayClientError> { + if !self.authenticated { + return Err(GatewayClientError::NotAuthenticated); + } if self.connection.is_partially_delegated() { return Ok(()); } @@ -492,4 +473,19 @@ where self.connection = SocketState::PartiallyDelegated(partially_delegated); Ok(()) } + + pub async fn authenticate_and_start(&mut self) -> Result + where + R: IntoClientRequest + Unpin + Clone, + { + if !self.connection.is_established() { + self.establish_connection().await?; + } + let shared_key = self.perform_initial_authentication().await?; + + // that call is NON-blocking + self.start_listening_for_mixnet_messages()?; + + Ok(shared_key) + } } diff --git a/common/crypto/Cargo.toml b/common/crypto/Cargo.toml index 6b6be07de0..cb4e3df78b 100644 --- a/common/crypto/Cargo.toml +++ b/common/crypto/Cargo.toml @@ -7,12 +7,21 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +aes-ctr = "0.4.0" bs58 = "0.3.0" -curve25519-dalek = "2.0.0" +# can't use proper release just yet (unless we use outdated hkdf) +# as hkdf depends on digest 0.9.0 and most recent release of blake3 still uses 0.8.1 +# they've pushed an update to their repo on 12.06, so I presume another release is imminent. +blake3 = { git = "https://github.com/BLAKE3-team/BLAKE3", rev="4c41a893a00a3ebe7b24529531ccf96d8593a57c" } +#blake3 = "0.3.4" +hkdf = "0.9" + +x25519-dalek = "0.6" +# TODO: do we need serde feature? +ed25519-dalek = "1.0.0-pre.3" log = "0.4" pretty_env_logger = "0.3" -rand = "0.7.2" -rand_core = "0.5" +rand = {version = "0.7.3", features = ["wasm-bindgen"]} # internal -nymsphinx = { path = "../nymsphinx" } +nymsphinx-types = { path = "../nymsphinx/types" } diff --git a/common/crypto/src/asymmetric/encryption/mod.rs b/common/crypto/src/asymmetric/encryption/mod.rs new file mode 100644 index 0000000000..39b1fe61b5 --- /dev/null +++ b/common/crypto/src/asymmetric/encryption/mod.rs @@ -0,0 +1,273 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use crate::{PemStorableKey, PemStorableKeyPair}; +use rand::{rngs::OsRng, CryptoRng, RngCore}; +use std::fmt::{self, Display, Formatter}; + +/// Size of a X25519 private key +pub const PRIVATE_KEY_SIZE: usize = 32; + +/// Size of a X25519 public key +pub const PUBLIC_KEY_SIZE: usize = 32; + +/// Size of a X25519 shared secret +pub const SHARED_SECRET_SIZE: usize = 32; + +#[derive(Debug)] +pub enum EncryptionKeyError { + InvalidPublicKey, + InvalidPrivateKey, +} + +// required for std::error::Error +impl Display for EncryptionKeyError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + EncryptionKeyError::InvalidPrivateKey => write!(f, "Invalid private key"), + EncryptionKeyError::InvalidPublicKey => write!(f, "Invalid public key"), + } + } +} + +impl std::error::Error for EncryptionKeyError {} + +pub struct KeyPair { + pub(crate) private_key: PrivateKey, + pub(crate) public_key: PublicKey, +} + +impl KeyPair { + pub fn new() -> Self { + let mut rng = OsRng; + Self::new_with_rng(&mut rng) + } + + pub fn new_with_rng(rng: &mut R) -> Self { + let private_key = x25519_dalek::StaticSecret::new(rng); + let public_key = (&private_key).into(); + + KeyPair { + private_key: PrivateKey(private_key), + public_key: PublicKey(public_key), + } + } + + pub fn private_key(&self) -> &PrivateKey { + &self.private_key + } + + pub fn public_key(&self) -> &PublicKey { + &self.public_key + } + + pub fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Result { + Ok(KeyPair { + private_key: PrivateKey::from_bytes(priv_bytes)?, + public_key: PublicKey::from_bytes(pub_bytes)?, + }) + } +} + +impl Default for KeyPair { + fn default() -> Self { + KeyPair::new() + } +} + +impl PemStorableKeyPair for KeyPair { + type PrivatePemKey = PrivateKey; + type PublicPemKey = PublicKey; + type Error = EncryptionKeyError; + + fn private_key(&self) -> &Self::PrivatePemKey { + self.private_key() + } + + fn public_key(&self) -> &Self::PublicPemKey { + self.public_key() + } + + fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Result { + Self::from_bytes(priv_bytes, pub_bytes) + } +} + +#[derive(Debug, Clone)] +pub struct PublicKey(x25519_dalek::PublicKey); + +impl PublicKey { + pub fn to_bytes(&self) -> [u8; PUBLIC_KEY_SIZE] { + *self.0.as_bytes() + } + + pub fn from_bytes(b: &[u8]) -> Result { + if b.len() != PUBLIC_KEY_SIZE { + return Err(EncryptionKeyError::InvalidPublicKey); + } + let mut bytes = [0; PUBLIC_KEY_SIZE]; + bytes.copy_from_slice(&b[..PUBLIC_KEY_SIZE]); + Ok(Self(x25519_dalek::PublicKey::from(bytes))) + } + + pub fn to_base58_string(&self) -> String { + bs58::encode(&self.to_bytes()).into_string() + } + + pub fn from_base58_string>(val: S) -> Result { + let bytes = bs58::decode(val.into()) + .into_vec() + .expect("TODO: deal with this failure case"); + Self::from_bytes(&bytes) + } +} + +impl PemStorableKey for PublicKey { + fn pem_type(&self) -> String { + String::from("X25519 PUBLIC KEY") + } + + fn to_bytes(&self) -> Vec { + self.to_bytes().to_vec() + } +} + +#[derive(Clone)] +pub struct PrivateKey(x25519_dalek::StaticSecret); + +impl<'a> From<&'a PrivateKey> for PublicKey { + fn from(pk: &'a PrivateKey) -> Self { + PublicKey((&pk.0).into()) + } +} + +impl PrivateKey { + pub fn to_bytes(&self) -> [u8; PRIVATE_KEY_SIZE] { + self.0.to_bytes() + } + + pub fn from_bytes(b: &[u8]) -> Result { + if b.len() != PRIVATE_KEY_SIZE { + return Err(EncryptionKeyError::InvalidPrivateKey); + } + let mut bytes = [0; 32]; + bytes.copy_from_slice(&b[..PRIVATE_KEY_SIZE]); + Ok(Self(x25519_dalek::StaticSecret::from(bytes))) + } + + pub fn to_base58_string(&self) -> String { + bs58::encode(&self.to_bytes()).into_string() + } + + pub fn from_base58_string>(val: S) -> Result { + let bytes = bs58::decode(val.into()) + .into_vec() + .expect("TODO: deal with this failure case"); + Self::from_bytes(&bytes) + } + + /// Perform a key exchange with another public key + pub fn diffie_hellman(&self, remote_public: &PublicKey) -> [u8; SHARED_SECRET_SIZE] { + *self.0.diffie_hellman(&remote_public.0).as_bytes() + } +} + +impl PemStorableKey for PrivateKey { + fn pem_type(&self) -> String { + String::from("X25519 PRIVATE KEY") + } + + fn to_bytes(&self) -> Vec { + self.to_bytes().to_vec() + } +} + +// compatibility with sphinx keys: + +impl Into for PublicKey { + fn into(self) -> nymsphinx_types::PublicKey { + nymsphinx_types::PublicKey::from(self.to_bytes()) + } +} + +impl From for PublicKey { + fn from(pub_key: nymsphinx_types::PublicKey) -> Self { + Self(x25519_dalek::PublicKey::from(*pub_key.as_bytes())) + } +} + +impl Into for PrivateKey { + fn into(self) -> nymsphinx_types::PrivateKey { + nymsphinx_types::PrivateKey::from(self.to_bytes()) + } +} + +impl<'a> Into for &'a PrivateKey { + fn into(self) -> nymsphinx_types::PrivateKey { + nymsphinx_types::PrivateKey::from(self.to_bytes()) + } +} + +impl From for PrivateKey { + fn from(private_key: nymsphinx_types::PrivateKey) -> Self { + let private_key_bytes = private_key.to_bytes(); + assert_eq!(private_key_bytes.len(), PRIVATE_KEY_SIZE); + Self::from_bytes(&private_key_bytes).unwrap() + } +} + +#[cfg(test)] +mod sphinx_key_conversion { + use super::*; + + const NUM_ITERATIONS: usize = 100; + + #[test] + fn works_for_forward_conversion() { + for _ in 0..NUM_ITERATIONS { + let keys = KeyPair::new(); + let private = keys.private_key; + let public = keys.public_key; + + let private_bytes = private.to_bytes(); + let public_bytes = public.to_bytes(); + + let sphinx_private: nymsphinx_types::PrivateKey = private.into(); + let recovered_private = PrivateKey::from(sphinx_private); + + let sphinx_public: nymsphinx_types::PublicKey = public.into(); + let recovered_public = PublicKey::from(sphinx_public); + assert_eq!(private_bytes, recovered_private.to_bytes()); + assert_eq!(public_bytes, recovered_public.to_bytes()); + } + } + + #[test] + fn works_for_backward_conversion() { + for _ in 0..NUM_ITERATIONS { + let (sphinx_private, sphinx_public) = nymsphinx_types::crypto::keygen(); + + let private_bytes = sphinx_private.to_bytes(); + let public_bytes = sphinx_public.as_bytes(); + + let private: PrivateKey = sphinx_private.into(); + let recovered_sphinx_private: nymsphinx_types::PrivateKey = private.into(); + + let public: PublicKey = sphinx_public.into(); + let recovered_sphinx_public: nymsphinx_types::PublicKey = public.into(); + assert_eq!(private_bytes, recovered_sphinx_private.to_bytes()); + assert_eq!(public_bytes, recovered_sphinx_public.as_bytes()); + } + } +} diff --git a/common/crypto/src/asymmetric/identity/mod.rs b/common/crypto/src/asymmetric/identity/mod.rs new file mode 100644 index 0000000000..a92c2fc825 --- /dev/null +++ b/common/crypto/src/asymmetric/identity/mod.rs @@ -0,0 +1,186 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use crate::{PemStorableKey, PemStorableKeyPair}; +use bs58; +use ed25519_dalek::SignatureError; +pub use ed25519_dalek::{PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH}; +use nymsphinx_types::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH}; +use rand::{rngs::OsRng, CryptoRng, RngCore}; + +/// Keypair for usage in ed25519 EdDSA. +pub struct KeyPair { + private_key: PrivateKey, + public_key: PublicKey, +} + +impl KeyPair { + pub fn new() -> Self { + let mut rng = OsRng; + Self::new_with_rng(&mut rng) + } + + pub fn new_with_rng(rng: &mut R) -> Self { + let ed25519_keypair = ed25519_dalek::Keypair::generate(rng); + + KeyPair { + private_key: PrivateKey(ed25519_keypair.secret), + public_key: PublicKey(ed25519_keypair.public), + } + } + + pub fn private_key(&self) -> &PrivateKey { + &self.private_key + } + + pub fn public_key(&self) -> &PublicKey { + &self.public_key + } + + pub fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Result { + Ok(KeyPair { + private_key: PrivateKey::from_bytes(priv_bytes)?, + public_key: PublicKey::from_bytes(pub_bytes)?, + }) + } +} + +impl PemStorableKeyPair for KeyPair { + type PrivatePemKey = PrivateKey; + type PublicPemKey = PublicKey; + type Error = SignatureError; + + fn private_key(&self) -> &Self::PrivatePemKey { + self.private_key() + } + + fn public_key(&self) -> &Self::PublicPemKey { + self.public_key() + } + + fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Result { + Self::from_bytes(priv_bytes, pub_bytes) + } +} + +/// ed25519 EdDSA Public Key +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +pub struct PublicKey(ed25519_dalek::PublicKey); + +impl PublicKey { + pub fn derive_address(&self) -> DestinationAddressBytes { + let mut temporary_address = [0u8; DESTINATION_ADDRESS_LENGTH]; + let public_key_bytes = self.to_bytes(); + + assert_eq!(DESTINATION_ADDRESS_LENGTH, PUBLIC_KEY_LENGTH); + + temporary_address.copy_from_slice(&public_key_bytes[..]); + DestinationAddressBytes::from_bytes(temporary_address) + } + + /// Convert this public key to a byte array. + pub fn to_bytes(&self) -> [u8; PUBLIC_KEY_LENGTH] { + self.0.to_bytes() + } + + pub fn from_bytes(b: &[u8]) -> Result { + Ok(PublicKey(ed25519_dalek::PublicKey::from_bytes(b)?)) + } + + pub fn to_base58_string(&self) -> String { + bs58::encode(&self.to_bytes()).into_string() + } + + pub fn from_base58_string>(val: S) -> Result { + let bytes = bs58::decode(val.into()) + .into_vec() + .expect("TODO: deal with this failure case"); + Self::from_bytes(&bytes) + } + + pub fn verify(&self, message: &[u8], signature: &Signature) -> Result<(), SignatureError> { + self.0.verify(message, &signature.0) + } +} + +impl PemStorableKey for PublicKey { + fn pem_type(&self) -> String { + String::from("ED25519 PUBLIC KEY") + } + + fn to_bytes(&self) -> Vec { + self.to_bytes().to_vec() + } +} + +/// ed25519 EdDSA Private Key +#[derive(Debug)] +pub struct PrivateKey(ed25519_dalek::SecretKey); + +impl<'a> From<&'a PrivateKey> for PublicKey { + fn from(pk: &'a PrivateKey) -> Self { + PublicKey((&pk.0).into()) + } +} + +impl PrivateKey { + pub fn to_bytes(&self) -> [u8; SECRET_KEY_LENGTH] { + self.0.to_bytes() + } + + pub fn from_bytes(b: &[u8]) -> Result { + Ok(PrivateKey(ed25519_dalek::SecretKey::from_bytes(b)?)) + } + + pub fn to_base58_string(&self) -> String { + bs58::encode(&self.to_bytes()).into_string() + } + + pub fn from_base58_string>(val: S) -> Result { + let bytes = bs58::decode(val.into()) + .into_vec() + .expect("TODO: deal with this failure case"); + Self::from_bytes(&bytes) + } + + pub fn sign(&self, message: &[u8]) -> Signature { + let expanded_secret_key = ed25519_dalek::ExpandedSecretKey::from(&self.0); + let public_key: PublicKey = self.into(); + let sig = expanded_secret_key.sign(message, &public_key.0); + Signature(sig) + } +} + +impl PemStorableKey for PrivateKey { + fn pem_type(&self) -> String { + String::from("ED25519 PRIVATE KEY") + } + + fn to_bytes(&self) -> Vec { + self.to_bytes().to_vec() + } +} + +#[derive(Debug)] +pub struct Signature(ed25519_dalek::Signature); + +impl Signature { + pub fn to_bytes(&self) -> [u8; SIGNATURE_LENGTH] { + self.0.to_bytes() + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + Ok(Signature(ed25519_dalek::Signature::from_bytes(bytes)?)) + } +} diff --git a/common/crypto/src/asymmetric/mod.rs b/common/crypto/src/asymmetric/mod.rs new file mode 100644 index 0000000000..1623a8212b --- /dev/null +++ b/common/crypto/src/asymmetric/mod.rs @@ -0,0 +1,16 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +pub mod encryption; +pub mod identity; diff --git a/common/crypto/src/encryption/mod.rs b/common/crypto/src/encryption/mod.rs deleted file mode 100644 index da7e548aa8..0000000000 --- a/common/crypto/src/encryption/mod.rs +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// 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. - -use crate::{PemStorableKey, PemStorableKeyPair}; -use curve25519_dalek::montgomery::MontgomeryPoint; -use curve25519_dalek::scalar::Scalar; -use rand_core::OsRng; - -// TODO: ensure this is a proper name for this considering we are not implementing entire DH here - -const CURVE_GENERATOR: MontgomeryPoint = curve25519_dalek::constants::X25519_BASEPOINT; - -pub struct KeyPair { - pub(crate) private_key: PrivateKey, - pub(crate) public_key: PublicKey, -} - -impl KeyPair { - pub fn new() -> Self { - let mut rng = OsRng; - let private_key_value = Scalar::random(&mut rng); - let public_key_value = CURVE_GENERATOR * private_key_value; - - KeyPair { - private_key: PrivateKey(private_key_value), - public_key: PublicKey(public_key_value), - } - } - - pub fn private_key(&self) -> &PrivateKey { - &self.private_key - } - - pub fn public_key(&self) -> &PublicKey { - &self.public_key - } - - pub fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Self { - KeyPair { - private_key: PrivateKey::from_bytes(priv_bytes), - public_key: PublicKey::from_bytes(pub_bytes), - } - } -} - -impl Default for KeyPair { - fn default() -> Self { - KeyPair::new() - } -} - -impl PemStorableKeyPair for KeyPair { - type PrivatePemKey = PrivateKey; - type PublicPemKey = PublicKey; - - fn private_key(&self) -> &Self::PrivatePemKey { - self.private_key() - } - - fn public_key(&self) -> &Self::PublicPemKey { - self.public_key() - } - - fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Self { - Self::from_bytes(priv_bytes, pub_bytes) - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct PrivateKey(pub Scalar); - -impl PrivateKey { - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes().to_vec() - } - - pub fn from_bytes(b: &[u8]) -> Self { - let mut bytes = [0; 32]; - bytes.copy_from_slice(&b[..]); - // due to trait restriction we have no choice but to panic if this fails - let key = Scalar::from_canonical_bytes(bytes).unwrap(); - Self(key) - } - - pub fn inner(&self) -> Scalar { - self.0 - } -} - -impl PemStorableKey for PrivateKey { - fn pem_type(&self) -> String { - String::from("X25519 PRIVATE KEY") - } - - fn to_bytes(&self) -> Vec { - self.to_bytes() - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct PublicKey(pub MontgomeryPoint); - -impl<'a> From<&'a PrivateKey> for PublicKey { - fn from(pk: &'a PrivateKey) -> Self { - PublicKey(CURVE_GENERATOR * pk.0) - } -} - -impl PublicKey { - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes().to_vec() - } - - pub fn from_bytes(b: &[u8]) -> Self { - let mut bytes = [0; 32]; - bytes.copy_from_slice(&b[..]); - let key = MontgomeryPoint(bytes); - Self(key) - } - - pub fn inner(&self) -> MontgomeryPoint { - self.0 - } - - pub fn to_base58_string(&self) -> String { - bs58::encode(&self.to_bytes()).into_string() - } - - pub fn from_base58_string(val: String) -> Self { - Self::from_bytes(&bs58::decode(&val).into_vec().unwrap()) - } -} - -impl PemStorableKey for PublicKey { - fn pem_type(&self) -> String { - String::from("X25519 PUBLIC KEY") - } - - fn to_bytes(&self) -> Vec { - self.to_bytes() - } -} diff --git a/common/crypto/src/identity/mod.rs b/common/crypto/src/identity/mod.rs deleted file mode 100644 index 6a0b9d3ffe..0000000000 --- a/common/crypto/src/identity/mod.rs +++ /dev/null @@ -1,160 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// 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. - -use crate::{encryption, PemStorableKey, PemStorableKeyPair}; -use bs58; -use curve25519_dalek::scalar::Scalar; -use nymsphinx::DestinationAddressBytes; - -// for time being define a dummy identity using x25519 encryption keys (as we've done so far) -// and replace it with proper keys, like ed25519 later on -#[derive(Clone)] -pub struct MixIdentityKeyPair { - pub private_key: MixIdentityPrivateKey, - pub public_key: MixIdentityPublicKey, -} - -impl MixIdentityKeyPair { - pub fn new() -> Self { - let keypair = encryption::KeyPair::new(); - MixIdentityKeyPair { - private_key: MixIdentityPrivateKey(keypair.private_key), - public_key: MixIdentityPublicKey(keypair.public_key), - } - } - - pub fn private_key(&self) -> &MixIdentityPrivateKey { - &self.private_key - } - - pub fn public_key(&self) -> &MixIdentityPublicKey { - &self.public_key - } - - pub fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Self { - MixIdentityKeyPair { - private_key: MixIdentityPrivateKey::from_bytes(priv_bytes), - public_key: MixIdentityPublicKey::from_bytes(pub_bytes), - } - } -} - -impl Default for MixIdentityKeyPair { - fn default() -> Self { - MixIdentityKeyPair::new() - } -} - -impl PemStorableKeyPair for MixIdentityKeyPair { - type PrivatePemKey = MixIdentityPrivateKey; - type PublicPemKey = MixIdentityPublicKey; - - fn private_key(&self) -> &Self::PrivatePemKey { - self.private_key() - } - - fn public_key(&self) -> &Self::PublicPemKey { - self.public_key() - } - - fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Self { - Self::from_bytes(priv_bytes, pub_bytes) - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct MixIdentityPublicKey(encryption::PublicKey); - -impl MixIdentityPublicKey { - pub fn derive_address(&self) -> DestinationAddressBytes { - let mut temporary_address = [0u8; 32]; - let public_key_bytes = self.to_bytes(); - temporary_address.copy_from_slice(&public_key_bytes[..]); - - DestinationAddressBytes::from_bytes(temporary_address) - } - - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - pub fn from_bytes(b: &[u8]) -> Self { - Self(encryption::PublicKey::from_bytes(b)) - } - - pub fn to_base58_string(&self) -> String { - bs58::encode(&self.to_bytes()).into_string() - } - - pub fn from_base58_string(val: String) -> Self { - Self::from_bytes(&bs58::decode(&val).into_vec().unwrap()) - } -} - -impl PemStorableKey for MixIdentityPublicKey { - fn pem_type(&self) -> String { - format!("DUMMY KEY BASED ON {}", self.0.pem_type()) - } - - fn to_bytes(&self) -> Vec { - self.to_bytes() - } -} - -#[derive(Debug, Clone, Eq, PartialEq)] -pub struct MixIdentityPrivateKey(pub encryption::PrivateKey); - -impl<'a> From<&'a MixIdentityPrivateKey> for MixIdentityPublicKey { - fn from(pk: &'a MixIdentityPrivateKey) -> Self { - let private_ref = &pk.0; - let public: encryption::PublicKey = private_ref.into(); - MixIdentityPublicKey(public) - } -} - -impl MixIdentityPrivateKey { - pub fn to_bytes(&self) -> Vec { - self.0.to_bytes() - } - - pub fn from_bytes(b: &[u8]) -> Self { - Self(encryption::PrivateKey::from_bytes(b)) - } - - pub fn to_base58_string(&self) -> String { - bs58::encode(&self.to_bytes()).into_string() - } - - pub fn from_base58_string(val: String) -> Self { - Self::from_bytes(&bs58::decode(&val).into_vec().unwrap()) - } -} - -// TODO: this will be implemented differently by using the proper trait -impl MixIdentityPrivateKey { - pub fn as_scalar(&self) -> Scalar { - let encryption_key = &self.0; - encryption_key.0 - } -} - -impl PemStorableKey for MixIdentityPrivateKey { - fn pem_type(&self) -> String { - format!("DUMMY KEY BASED ON {}", self.0.pem_type()) - } - - fn to_bytes(&self) -> Vec { - self.to_bytes() - } -} diff --git a/common/crypto/src/kdf/blake3_hkdf.rs b/common/crypto/src/kdf/blake3_hkdf.rs new file mode 100644 index 0000000000..56aa58fdc6 --- /dev/null +++ b/common/crypto/src/kdf/blake3_hkdf.rs @@ -0,0 +1,39 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use hkdf::Hkdf; + +#[derive(Debug)] +pub enum HkdfError { + InvalidOkmLength, +} + +/// Perform HKDF `extract` then `expand` as a single step. +pub fn extract_then_expand( + salt: Option<&[u8]>, + ikm: &[u8], + info: Option<&[u8]>, + okm_length: usize, +) -> Result, HkdfError> { + // TODO: this would need to change if we ever needed the generated pseudorandom key, but + // realistically I don't see any reasons why we might need it + + let hkdf = Hkdf::::new(salt, ikm); + let mut okm = vec![0u8; okm_length]; + if let Err(_) = hkdf.expand(info.unwrap_or_else(|| &[]), &mut okm) { + return Err(HkdfError::InvalidOkmLength); + } + + Ok(okm) +} diff --git a/common/crypto/src/kdf/mod.rs b/common/crypto/src/kdf/mod.rs new file mode 100644 index 0000000000..e456e03c97 --- /dev/null +++ b/common/crypto/src/kdf/mod.rs @@ -0,0 +1,15 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +pub mod blake3_hkdf; diff --git a/common/crypto/src/lib.rs b/common/crypto/src/lib.rs index 52c8fa7b21..3eaa677b8c 100644 --- a/common/crypto/src/lib.rs +++ b/common/crypto/src/lib.rs @@ -12,8 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod encryption; -pub mod identity; +pub mod asymmetric; +pub mod kdf; +pub mod symmetric; // TODO: ideally those trait should be moved to 'pemstore' crate, however, that would cause // circular dependency. The best solution would be to remove dependency on 'crypto' from @@ -25,12 +26,13 @@ pub trait PemStorableKey { fn to_bytes(&self) -> Vec; } -pub trait PemStorableKeyPair { +pub trait PemStorableKeyPair: Sized { type PrivatePemKey: PemStorableKey; type PublicPemKey: PemStorableKey; + type Error: std::error::Error; fn private_key(&self) -> &Self::PrivatePemKey; fn public_key(&self) -> &Self::PublicPemKey; - fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Self; + fn from_bytes(priv_bytes: &[u8], pub_bytes: &[u8]) -> Result; } diff --git a/common/crypto/src/symmetric/aes_ctr.rs b/common/crypto/src/symmetric/aes_ctr.rs new file mode 100644 index 0000000000..6be403b63e --- /dev/null +++ b/common/crypto/src/symmetric/aes_ctr.rs @@ -0,0 +1,138 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +pub use aes_ctr::stream_cipher::generic_array; +use aes_ctr::{ + stream_cipher::{ + generic_array::{ + typenum::{Unsigned, U16}, + GenericArray, + }, + NewStreamCipher, SyncStreamCipher, + }, + Aes128Ctr, +}; +use rand::{CryptoRng, RngCore}; + +// the 'U16' type is taken directly from the `Ctr128` for consistency sake +pub type Aes128KeySize = U16; +pub type Aes128NonceSize = U16; + +pub type Aes128Key = GenericArray; +pub type Aes128IV = GenericArray; + +pub fn generate_key(rng: &mut R) -> Aes128Key { + let mut ack_key = GenericArray::default(); + rng.fill_bytes(&mut ack_key); + ack_key +} + +pub fn random_iv(rng: &mut R) -> Aes128IV { + let mut iv = GenericArray::default(); + rng.fill_bytes(&mut iv); + iv +} + +pub fn zero_iv() -> Aes128IV { + GenericArray::default() +} + +pub fn iv_from_slice(b: &[u8]) -> &Aes128IV { + if b.len() != Aes128NonceSize::to_usize() { + // `from_slice` would have caused a panic about this issue anyway. + // Now we at least have slightly more information + panic!( + "Tried to convert {} bytes to IV. Expected {}", + b.len(), + Aes128NonceSize::to_usize() + ) + } + GenericArray::from_slice(b) +} + +fn apply_aes_ctr(key: &Aes128Key, iv: &Aes128IV, mut data: &mut [u8]) { + let mut cipher = Aes128Ctr::new(key, iv); + cipher.apply_keystream(&mut data) +} + +pub fn encrypt(key: &Aes128Key, iv: &Aes128IV, data: &[u8]) -> Vec { + let mut ciphertext = data.to_vec(); + apply_aes_ctr(key, iv, &mut ciphertext); + ciphertext +} + +pub fn encrypt_in_place(key: &Aes128Key, iv: &Aes128IV, data: &mut [u8]) { + apply_aes_ctr(key, iv, data) +} + +pub fn decrypt(key: &Aes128Key, iv: &Aes128IV, ciphertext: &[u8]) -> Vec { + let mut data = ciphertext.to_vec(); + apply_aes_ctr(key, iv, &mut data); + data +} + +pub fn decrypt_in_place(key: &Aes128Key, iv: &Aes128IV, ciphertext: &mut [u8]) { + apply_aes_ctr(key, iv, ciphertext) +} + +#[cfg(test)] +mod tests { + use super::*; + use rand::rngs::OsRng; + + #[test] + fn zero_iv_is_actually_zero() { + let iv = zero_iv(); + for b in iv { + assert_eq!(b, 0); + } + } + + #[test] + fn decryption_is_reciprocal_to_encryption() { + let mut rng = OsRng; + + let arr_input = [42; 200]; + let vec_input = vec![123, 200]; + + let key1 = generate_key(&mut rng); + let key2 = generate_key(&mut rng); + + let iv1 = random_iv(&mut rng); + let iv2 = random_iv(&mut rng); + + let ciphertext1 = encrypt(&key1, &iv1, &arr_input); + let ciphertext2 = encrypt(&key2, &iv2, &vec_input); + + assert_eq!(arr_input.to_vec(), decrypt(&key1, &iv1, &ciphertext1)); + assert_eq!(vec_input, decrypt(&key2, &iv2, &ciphertext2)); + } + + #[test] + fn in_place_variants_work_same_way() { + let mut rng = OsRng; + + let mut data = [42; 200]; + let original_data = data.clone(); + + let key = generate_key(&mut rng); + let iv = random_iv(&mut rng); + + encrypt_in_place(&key, &iv, &mut data); + assert_eq!(data.to_vec(), encrypt(&key, &iv, &original_data)); + + decrypt_in_place(&key, &iv, &mut data); + assert_eq!(data.to_vec(), original_data.to_vec()); + } +} diff --git a/common/crypto/src/symmetric/mod.rs b/common/crypto/src/symmetric/mod.rs new file mode 100644 index 0000000000..a45cecf0d9 --- /dev/null +++ b/common/crypto/src/symmetric/mod.rs @@ -0,0 +1,15 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +pub mod aes_ctr; diff --git a/common/nymsphinx/acknowledgements/Cargo.toml b/common/nymsphinx/acknowledgements/Cargo.toml index c697fccd87..f98993d0a5 100644 --- a/common/nymsphinx/acknowledgements/Cargo.toml +++ b/common/nymsphinx/acknowledgements/Cargo.toml @@ -7,13 +7,12 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -aes-ctr = "0.4.0" rand = {version = "0.7.3", features = ["wasm-bindgen"]} - - +crypto = { path = "../../crypto" } nymsphinx-addressing = { path = "../addressing" } nymsphinx-params = { path = "../params" } nymsphinx-types = { path = "../types" } +topology = { path = "../../topology" } + -topology = { path = "../../topology" } \ No newline at end of file diff --git a/common/nymsphinx/acknowledgements/src/identifier.rs b/common/nymsphinx/acknowledgements/src/identifier.rs index 03ed6deedb..6d3cc940c6 100644 --- a/common/nymsphinx/acknowledgements/src/identifier.rs +++ b/common/nymsphinx/acknowledgements/src/identifier.rs @@ -12,36 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -use aes_ctr::{ - stream_cipher::{ - generic_array::{ - typenum::{marker_traits::Unsigned, U16}, - GenericArray, - }, - NewStreamCipher, SyncStreamCipher, - }, - Aes128Ctr, -}; +use crypto::symmetric::aes_ctr::generic_array::typenum::Unsigned; +use crypto::symmetric::aes_ctr::{iv_from_slice, Aes128IV, Aes128Key, Aes128NonceSize}; use nymsphinx_params::packet_sizes::PacketSize; use rand::{CryptoRng, RngCore}; -// the 'U16' type is taken directly from the `Ctr128` for consistency sake -pub type Aes128KeySize = U16; -pub type Aes128NonceSize = U16; - -pub type AckAes128Key = GenericArray; -type AckAes128IV = GenericArray; +pub type AckAes128Key = Aes128Key; +type AckAes128IV = Aes128IV; pub fn generate_key(rng: &mut R) -> AckAes128Key { - let mut ack_key = GenericArray::default(); - rng.fill_bytes(&mut ack_key); - ack_key + crypto::symmetric::aes_ctr::generate_key(rng) } fn random_iv(rng: &mut R) -> AckAes128IV { - let mut iv = GenericArray::default(); - rng.fill_bytes(&mut iv); - iv + crypto::symmetric::aes_ctr::random_iv(rng) } pub fn prepare_identifier( @@ -50,30 +34,30 @@ pub fn prepare_identifier( marshaled_id: [u8; 5], ) -> Vec { let iv = random_iv(rng); - let mut cipher = Aes128Ctr::new(key, &iv); - let mut output = marshaled_id.to_vec(); + let id_ciphertext = crypto::symmetric::aes_ctr::encrypt(key, &iv, &marshaled_id); - cipher.apply_keystream(&mut output); - - iv.into_iter().chain(output.into_iter()).collect() + // IV || ID_CIPHERTEXT + iv.into_iter().chain(id_ciphertext.into_iter()).collect() } -pub fn recover_identifier(key: &AckAes128Key, iv_ciphertext: &[u8]) -> Option<[u8; 5]> { +pub fn recover_identifier(key: &AckAes128Key, iv_id_ciphertext: &[u8]) -> Option<[u8; 5]> { // first few bytes are expected to be the concatenated IV. It must be followed by at least 1 more // byte that we wish to recover, but it can be no longer from what we can physically store inside // an ack - if iv_ciphertext.len() != PacketSize::ACKPacket.plaintext_size() { + if iv_id_ciphertext.len() != PacketSize::ACKPacket.plaintext_size() { return None; } - let iv = GenericArray::from_slice(&iv_ciphertext[..Aes128NonceSize::to_usize()]); - let mut cipher = Aes128Ctr::new(key, &iv); - let mut output = iv_ciphertext[Aes128NonceSize::to_usize()..].to_vec(); - cipher.apply_keystream(&mut output); + let iv = iv_from_slice(&iv_id_ciphertext[..Aes128NonceSize::to_usize()]); + let id = crypto::symmetric::aes_ctr::decrypt( + key, + iv, + &iv_id_ciphertext[Aes128NonceSize::to_usize()..], + ); - let mut output_arr = [0u8; 5]; - output_arr.copy_from_slice(&output); - Some(output_arr) + let mut id_arr = [0u8; 5]; + id_arr.copy_from_slice(&id); + Some(id_arr) } #[cfg(test)] diff --git a/common/nymsphinx/types/Cargo.toml b/common/nymsphinx/types/Cargo.toml index 4ac06239d3..cd7c78cadb 100644 --- a/common/nymsphinx/types/Cargo.toml +++ b/common/nymsphinx/types/Cargo.toml @@ -7,5 +7,5 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -sphinx = { git = "https://github.com/nymtech/sphinx", rev="cd9f09b85de33c60030ba6d7fae613c42cbe1faf" } +sphinx = { git = "https://github.com/nymtech/sphinx", rev="f31efdbf667952440bd9af5eb0c140135fd1bc4c" } #sphinx = { path = "../../../../sphinx"} \ No newline at end of file diff --git a/common/nymsphinx/types/src/lib.rs b/common/nymsphinx/types/src/lib.rs index 8d16ba194c..e9877d6229 100644 --- a/common/nymsphinx/types/src/lib.rs +++ b/common/nymsphinx/types/src/lib.rs @@ -17,7 +17,7 @@ pub use sphinx::{ constants::{ self, DESTINATION_ADDRESS_LENGTH, IDENTIFIER_LENGTH, MAX_PATH_LENGTH, NODE_ADDRESS_LENGTH, }, - crypto::{public_key_from_bytes, PublicKey, SecretKey}, + crypto::{self, EphemeralSecret, PrivateKey, PublicKey, SharedSecret}, header::{self, delays, delays::Delay, ProcessedHeader, SphinxHeader, HEADER_SIZE}, packet::builder::{self, DEFAULT_PAYLOAD_SIZE}, payload::{Payload, PAYLOAD_OVERHEAD_SIZE}, diff --git a/common/pemstore/src/pemstore.rs b/common/pemstore/src/pemstore.rs index 1e91ad7a00..2bc33c7097 100644 --- a/common/pemstore/src/pemstore.rs +++ b/common/pemstore/src/pemstore.rs @@ -13,9 +13,8 @@ // limitations under the License. use crate::pathfinder::PathFinder; -use crypto::identity::MixIdentityKeyPair; -use crypto::PemStorableKey; -use crypto::{encryption, PemStorableKeyPair}; +use crypto::asymmetric::{encryption, identity}; +use crypto::{PemStorableKey, PemStorableKeyPair}; use log::info; use pem::{encode, parse, Pem}; use std::fs::File; @@ -23,27 +22,54 @@ use std::io; use std::io::prelude::*; use std::path::PathBuf; -pub struct PemStore { - #[allow(dead_code)] - config_dir: PathBuf, - private_mix_key_file: PathBuf, - public_mix_key_file: PathBuf, +pub struct PemStore

{ + pathfinder: P, } -impl PemStore { - pub fn new(pathfinder: P) -> PemStore { - PemStore { - config_dir: pathfinder.config_dir(), - private_mix_key_file: pathfinder.private_identity_key(), - public_mix_key_file: pathfinder.public_identity_key(), +enum KeyType { + Identity, + Encryption, +} + +impl

PemStore

{ + pub fn new(pathfinder: P) -> Self { + PemStore { pathfinder } + } + + fn get_keypair_paths(&self, key_type: KeyType) -> (PathBuf, PathBuf) + where + P: PathFinder, + { + match key_type { + KeyType::Identity => ( + self.pathfinder.private_identity_key(), + self.pathfinder.public_identity_key(), + ), + KeyType::Encryption => ( + self.pathfinder + .private_encryption_key() + .expect("tried to write encryption keypair while no path was specified"), + self.pathfinder + .public_encryption_key() + .expect("tried to write encryption keypair while no path was specified"), + ), } } - pub fn read_keypair(&self) -> io::Result { - let private_pem = self.read_pem_file(self.private_mix_key_file.clone())?; - let public_pem = self.read_pem_file(self.public_mix_key_file.clone())?; + fn read_keypair(&self, key_type: KeyType) -> io::Result + where + P: PathFinder, + T: PemStorableKeyPair, + { + let (private_key_path, public_key_path) = self.get_keypair_paths(key_type); - let key_pair = T::from_bytes(&private_pem.contents, &public_pem.contents); + let private_pem = self.read_pem_file(private_key_path)?; + let public_pem = self.read_pem_file(public_key_path)?; + + let key_pair = match T::from_bytes(&private_pem.contents, &public_pem.contents) { + Ok(key_pair) => key_pair, + Err(err) => return Err(io::Error::new(io::ErrorKind::InvalidData, err.to_string())), + }; if key_pair.private_key().pem_type() != private_pem.tag { return Err(io::Error::new( @@ -62,12 +88,18 @@ impl PemStore { Ok(key_pair) } - pub fn read_encryption(&self) -> io::Result { - self.read_keypair() + pub fn read_encryption_keypair(&self) -> io::Result + where + P: PathFinder, + { + self.read_keypair(KeyType::Encryption) } - pub fn read_identity(&self) -> io::Result { - self.read_keypair() + pub fn read_identity_keypair(&self) -> io::Result + where + P: PathFinder, + { + self.read_keypair(KeyType::Identity) } fn read_pem_file(&self, filepath: PathBuf) -> io::Result { @@ -77,40 +109,46 @@ impl PemStore { parse(&buf).map_err(|e| io::Error::new(io::ErrorKind::Other, e)) } - fn write_keypair(&self, key_pair: impl PemStorableKeyPair) -> io::Result<()> { + fn write_keypair(&self, key_pair: &T, key_type: KeyType) -> io::Result<()> + where + P: PathFinder, + T: PemStorableKeyPair, + { let private_key = key_pair.private_key(); let public_key = key_pair.public_key(); + let (private_key_path, public_key_path) = self.get_keypair_paths(key_type); + self.write_pem_file( - self.private_mix_key_file.clone(), + private_key_path.clone(), private_key.to_bytes(), private_key.pem_type(), )?; - info!( - "Written private key to {:?}", - self.private_mix_key_file.clone() - ); + info!("Written private key to {:?}", private_key_path); self.write_pem_file( - self.public_mix_key_file.clone(), + public_key_path.clone(), public_key.to_bytes(), public_key.pem_type(), )?; - info!( - "Written public key to {:?}", - self.public_mix_key_file.clone() - ); + info!("Written public key to {:?}", public_key_path); Ok(()) } // This should be refactored and made more generic for when we have other kinds of // KeyPairs that we want to persist (e.g. validator keypairs, or keys for // signing vs encryption). However, for the moment, it does the job. - pub fn write_identity(&self, key_pair: MixIdentityKeyPair) -> io::Result<()> { - self.write_keypair(key_pair) + pub fn write_identity_keypair(&self, key_pair: &identity::KeyPair) -> io::Result<()> + where + P: PathFinder, + { + self.write_keypair(key_pair, KeyType::Identity) } - pub fn write_encryption_keys(&self, key_pair: encryption::KeyPair) -> io::Result<()> { - self.write_keypair(key_pair) + pub fn write_encryption_keypair(&self, key_pair: &encryption::KeyPair) -> io::Result<()> + where + P: PathFinder, + { + self.write_keypair(key_pair, KeyType::Encryption) } fn write_pem_file(&self, filepath: PathBuf, data: Vec, tag: String) -> io::Result<()> { diff --git a/common/topology/src/gateway.rs b/common/topology/src/gateway.rs index d514b7ae62..26d4def744 100644 --- a/common/topology/src/gateway.rs +++ b/common/topology/src/gateway.rs @@ -28,16 +28,18 @@ pub struct Node { pub location: String, pub client_listener: String, pub mixnet_listener: SocketAddr, - pub pub_key: String, + // TODO: should we just import common/crypto and use 'proper' types for those directly? + pub identity_key: String, + pub sphinx_key: String, pub registered_clients: Vec, pub last_seen: u64, pub version: String, } impl Node { - pub fn get_pub_key_bytes(&self) -> [u8; 32] { + fn get_sphinx_key_bytes(&self) -> [u8; 32] { let mut key_bytes = [0; 32]; - bs58::decode(&self.pub_key).into(&mut key_bytes).unwrap(); + bs58::decode(&self.sphinx_key).into(&mut key_bytes).unwrap(); key_bytes } @@ -60,8 +62,8 @@ impl Into for Node { let node_address_bytes = NymNodeRoutingAddress::from(self.mixnet_listener) .try_into() .unwrap(); - let key_bytes = self.get_pub_key_bytes(); - let key = nymsphinx_types::public_key_from_bytes(key_bytes); + let key_bytes = self.get_sphinx_key_bytes(); + let key = nymsphinx_types::PublicKey::from(key_bytes); SphinxNode::new(node_address_bytes, key) } diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 712c0eef21..56b2f24c82 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -89,7 +89,7 @@ pub trait NymTopology: Sized + std::fmt::Debug + Send + Sync + Clone { let b58_address = gateway_address.to_base58_string(); self.gateways() .iter() - .find(|&gateway| gateway.pub_key == b58_address) + .find(|&gateway| gateway.identity_key == b58_address) .is_some() } @@ -102,7 +102,7 @@ pub trait NymTopology: Sized + std::fmt::Debug + Send + Sync + Clone { let gateway = self .gateways() .iter() - .find(|&gateway| gateway.pub_key == b58_address) + .find(|&gateway| gateway.identity_key == b58_address) .ok_or_else(|| NymTopologyError::NonExistentGatewayError)? .clone(); diff --git a/common/topology/src/mix.rs b/common/topology/src/mix.rs index 588938c69b..3bb5e0caf6 100644 --- a/common/topology/src/mix.rs +++ b/common/topology/src/mix.rs @@ -46,7 +46,7 @@ impl Into for Node { fn into(self) -> SphinxNode { let node_address_bytes = NymNodeRoutingAddress::from(self.host).try_into().unwrap(); let key_bytes = self.get_pub_key_bytes(); - let key = nymsphinx_types::public_key_from_bytes(key_bytes); + let key = nymsphinx_types::PublicKey::from(key_bytes); SphinxNode::new(node_address_bytes, key) } diff --git a/common/topology/src/provider.rs b/common/topology/src/provider.rs index de1a722a97..93630b0058 100644 --- a/common/topology/src/provider.rs +++ b/common/topology/src/provider.rs @@ -54,7 +54,7 @@ impl Into for Node { .try_into() .unwrap(); let key_bytes = self.get_pub_key_bytes(); - let key = nymsphinx_types::public_key_from_bytes(key_bytes); + let key = nymsphinx_types::PublicKey::from(key_bytes); SphinxNode::new(node_address_bytes, key) } diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index 8454722fd5..9c14c11f8b 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -12,12 +12,10 @@ clap = "2.33.0" dirs = "2.0.2" dotenv = "0.15.0" futures = "0.3" -hmac = "0.7.1" log = "0.4" pretty_env_logger = "0.3" rand = "0.7.2" serde = { version = "1.0.104", features = ["derive"] } -sha2 = "0.8.1" sled = "0.31" tokio = { version = "0.2", features = ["full"] } tokio-util = { version = "0.3.1", features = ["codec"] } diff --git a/gateway/gateway-requests/Cargo.toml b/gateway/gateway-requests/Cargo.toml index 9948964040..79dcaf3ffb 100644 --- a/gateway/gateway-requests/Cargo.toml +++ b/gateway/gateway-requests/Cargo.toml @@ -8,7 +8,13 @@ edition = "2018" [dependencies] bs58 = "0.3" -nymsphinx = { path = "../../common/nymsphinx" } serde = { version = "1.0.104", features = ["derive"] } serde_json = "1.0.44" tokio-tungstenite = "0.10.1" + +nymsphinx = { path = "../../common/nymsphinx" } + +futures = "0.3" +rand = {version = "0.7.3", features = ["wasm-bindgen"]} +crypto = { path = "../../common/crypto" } +log = "0.4" diff --git a/gateway/gateway-requests/src/auth_token.rs b/gateway/gateway-requests/src/auth_token.rs deleted file mode 100644 index 92768c1dbe..0000000000 --- a/gateway/gateway-requests/src/auth_token.rs +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// 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. - -pub const AUTH_TOKEN_SIZE: usize = 32; - -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] -pub struct AuthToken([u8; AUTH_TOKEN_SIZE]); - -#[derive(Debug)] -pub enum AuthTokenConversionError { - InvalidStringError, - StringOfInvalidLengthError, -} - -impl AuthToken { - pub fn from_bytes(bytes: [u8; AUTH_TOKEN_SIZE]) -> Self { - AuthToken(bytes) - } - - pub fn to_bytes(&self) -> [u8; AUTH_TOKEN_SIZE] { - self.0 - } - - pub fn as_bytes(&self) -> &[u8] { - &self.0 - } - - pub fn try_from_base58_string>( - val: S, - ) -> Result { - let decoded = match bs58::decode(val.into()).into_vec() { - Ok(decoded) => decoded, - Err(_) => return Err(AuthTokenConversionError::InvalidStringError), - }; - - if decoded.len() != AUTH_TOKEN_SIZE { - return Err(AuthTokenConversionError::StringOfInvalidLengthError); - } - - let mut token = [0u8; AUTH_TOKEN_SIZE]; - token.copy_from_slice(&decoded[..]); - Ok(AuthToken(token)) - } - - pub fn to_base58_string(&self) -> String { - bs58::encode(self.0).into_string() - } -} - -impl Into for AuthToken { - fn into(self) -> String { - self.to_base58_string() - } -} - -#[cfg(test)] -mod auth_token_conversion { - use super::*; - - #[test] - fn it_is_possible_to_recover_it_from_valid_b58_string() { - let auth_token = AuthToken([42; AUTH_TOKEN_SIZE]); - let auth_token_string = auth_token.to_base58_string(); - assert_eq!( - auth_token, - AuthToken::try_from_base58_string(auth_token_string).unwrap() - ) - } - - #[test] - fn it_is_possible_to_recover_it_from_valid_b58_str_ref() { - let auth_token = AuthToken([42; AUTH_TOKEN_SIZE]); - let auth_token_string = auth_token.to_base58_string(); - let auth_token_str_ref: &str = &auth_token_string; - assert_eq!( - auth_token, - AuthToken::try_from_base58_string(auth_token_str_ref).unwrap() - ) - } - - #[test] - fn it_returns_error_on_b58_with_invalid_characters() { - let auth_token = AuthToken([42; AUTH_TOKEN_SIZE]); - let auth_token_string = auth_token.to_base58_string(); - - let mut chars = auth_token_string.chars(); - let _consumed_first_char = chars.next().unwrap(); - - let invalid_chars_token = "=".to_string() + chars.as_str(); - assert!(AuthToken::try_from_base58_string(invalid_chars_token).is_err()) - } - - #[test] - fn it_returns_error_on_too_long_b58_string() { - let auth_token = AuthToken([42; AUTH_TOKEN_SIZE]); - let mut auth_token_string = auth_token.to_base58_string(); - auth_token_string.push('f'); - - assert!(AuthToken::try_from_base58_string(auth_token_string).is_err()) - } - - #[test] - fn it_returns_error_on_too_short_b58_string() { - let auth_token = AuthToken([42; AUTH_TOKEN_SIZE]); - let auth_token_string = auth_token.to_base58_string(); - - let mut chars = auth_token_string.chars(); - let _consumed_first_char = chars.next().unwrap(); - let _consumed_second_char = chars.next().unwrap(); - let invalid_chars_token = chars.as_str(); - - assert!(AuthToken::try_from_base58_string(invalid_chars_token).is_err()) - } -} diff --git a/gateway/gateway-requests/src/authentication/encrypted_address.rs b/gateway/gateway-requests/src/authentication/encrypted_address.rs new file mode 100644 index 0000000000..358f056701 --- /dev/null +++ b/gateway/gateway-requests/src/authentication/encrypted_address.rs @@ -0,0 +1,91 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use crate::authentication::iv::AuthenticationIV; +use crate::registration::handshake::shared_key::SharedKey; +use crypto::symmetric::aes_ctr; +use nymsphinx::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH}; + +pub const ENCRYPTED_ADDRESS_SIZE: usize = DESTINATION_ADDRESS_LENGTH; + +/// Replacement for what used to be an 'AuthToken'. We used to be generating an 'AuthToken' based on +/// local secret and remote address in order to allow for authentication. Due to changes in registration +/// and the fact we are deriving a shared key, we are encrypting remote's address with the previously +/// derived shared key. If the value is as expected, then authentication is successful. +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub struct EncryptedAddressBytes([u8; ENCRYPTED_ADDRESS_SIZE]); + +#[derive(Debug)] +pub enum EncryptedAddressConversionError { + DecodeError(bs58::decode::Error), + StringOfInvalidLengthError, +} + +impl EncryptedAddressBytes { + pub fn new(address: &DestinationAddressBytes, key: &SharedKey, iv: &AuthenticationIV) -> Self { + let ciphertext = aes_ctr::encrypt(key, iv, address.as_bytes()); + + let mut enc_address = [0u8; ENCRYPTED_ADDRESS_SIZE]; + enc_address.copy_from_slice(&ciphertext[..]); + EncryptedAddressBytes(enc_address) + } + + pub fn verify( + &self, + address: &DestinationAddressBytes, + key: &SharedKey, + iv: &AuthenticationIV, + ) -> bool { + self == &Self::new(address, key, iv) + } + + pub fn from_bytes(bytes: [u8; ENCRYPTED_ADDRESS_SIZE]) -> Self { + EncryptedAddressBytes(bytes) + } + + pub fn to_bytes(&self) -> [u8; ENCRYPTED_ADDRESS_SIZE] { + self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + &self.0 + } + + pub fn try_from_base58_string>( + val: S, + ) -> Result { + let decoded = match bs58::decode(val.into()).into_vec() { + Ok(decoded) => decoded, + Err(err) => return Err(EncryptedAddressConversionError::DecodeError(err)), + }; + + if decoded.len() != ENCRYPTED_ADDRESS_SIZE { + return Err(EncryptedAddressConversionError::StringOfInvalidLengthError); + } + + let mut enc_address = [0u8; ENCRYPTED_ADDRESS_SIZE]; + enc_address.copy_from_slice(&decoded[..]); + Ok(EncryptedAddressBytes(enc_address)) + } + + pub fn to_base58_string(&self) -> String { + bs58::encode(self.0).into_string() + } +} + +impl Into for EncryptedAddressBytes { + fn into(self) -> String { + self.to_base58_string() + } +} diff --git a/gateway/gateway-requests/src/authentication/iv.rs b/gateway/gateway-requests/src/authentication/iv.rs new file mode 100644 index 0000000000..2ae4b85ac3 --- /dev/null +++ b/gateway/gateway-requests/src/authentication/iv.rs @@ -0,0 +1,86 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use crypto::symmetric::aes_ctr::{ + generic_array::{typenum::Unsigned, GenericArray}, + random_iv, Aes128IV, Aes128NonceSize, +}; +use rand::{CryptoRng, RngCore}; +use std::ops::Deref; + +pub struct AuthenticationIV(Aes128IV); + +#[derive(Debug)] +pub enum IVConversionError { + DecodeError(bs58::decode::Error), + BytesOfInvalidLengthError, + StringOfInvalidLengthError, +} + +impl AuthenticationIV { + pub fn new_random(rng: &mut R) -> Self { + AuthenticationIV(random_iv(rng)) + } + + pub fn try_from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != Aes128NonceSize::to_usize() { + return Err(IVConversionError::BytesOfInvalidLengthError); + } + + Ok(AuthenticationIV(GenericArray::clone_from_slice(bytes))) + } + + pub fn to_bytes(&self) -> Vec { + self.0.to_vec() + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_ref() + } + + pub fn try_from_base58_string>(val: S) -> Result { + let decoded = match bs58::decode(val.into()).into_vec() { + Ok(decoded) => decoded, + Err(err) => return Err(IVConversionError::DecodeError(err)), + }; + + if decoded.len() != Aes128NonceSize::to_usize() { + return Err(IVConversionError::StringOfInvalidLengthError); + } + + Ok(AuthenticationIV( + GenericArray::from_exact_iter(decoded).expect("Invalid vector length!"), + )) + } + + pub fn to_base58_string(&self) -> String { + bs58::encode(self.to_bytes()).into_string() + } +} + +impl Into for AuthenticationIV { + fn into(self) -> String { + self.to_base58_string() + } +} + +impl Deref for AuthenticationIV { + type Target = Aes128IV; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +// I don't see any cases in which DerefMut would be useful. So did not implement it. diff --git a/gateway/gateway-requests/src/authentication/mod.rs b/gateway/gateway-requests/src/authentication/mod.rs new file mode 100644 index 0000000000..e1e06430d4 --- /dev/null +++ b/gateway/gateway-requests/src/authentication/mod.rs @@ -0,0 +1,16 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +pub mod encrypted_address; +pub mod iv; diff --git a/gateway/gateway-requests/src/lib.rs b/gateway/gateway-requests/src/lib.rs index f437b47ddf..42cc0062bd 100644 --- a/gateway/gateway-requests/src/lib.rs +++ b/gateway/gateway-requests/src/lib.rs @@ -12,11 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. -pub mod auth_token; +pub mod authentication; +pub mod registration; pub mod types; pub const DUMMY_MESSAGE_CONTENT: &[u8] = b"[DUMMY MESSAGE] Wanting something does not give you the right to have it."; -pub use auth_token::AuthToken; +pub use crypto::symmetric::aes_ctr::generic_array; + pub use types::*; diff --git a/gateway/gateway-requests/src/registration/handshake/client.rs b/gateway/gateway-requests/src/registration/handshake/client.rs new file mode 100644 index 0000000000..7c11b0b85c --- /dev/null +++ b/gateway/gateway-requests/src/registration/handshake/client.rs @@ -0,0 +1,130 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use crate::registration::handshake::shared_key::SharedKey; +use crate::registration::handshake::state::State; +use crate::registration::handshake::{error::HandshakeError, WsItem}; +use crypto::asymmetric::encryption::PUBLIC_KEY_SIZE; +use crypto::asymmetric::identity::SIGNATURE_LENGTH; +use crypto::asymmetric::{encryption, identity}; +use futures::future::BoxFuture; +use futures::task::{Context, Poll}; +use futures::{Future, Sink, Stream}; +use rand::{CryptoRng, RngCore}; +use std::pin::Pin; +use tokio_tungstenite::tungstenite::Message as WsMessage; + +pub(crate) struct ClientHandshake<'a> { + handshake_future: BoxFuture<'a, Result>, +} + +impl<'a> ClientHandshake<'a> { + pub(crate) fn new( + rng: &mut (impl RngCore + CryptoRng), + ws_stream: &'a mut S, + identity: &'a crypto::asymmetric::identity::KeyPair, + gateway_pubkey: identity::PublicKey, + ) -> Self + where + S: Stream + Sink + Unpin + Send + 'a, + { + let mut state = State::new(rng, ws_stream, identity, Some(gateway_pubkey)); + + ClientHandshake { + handshake_future: Box::pin(async move { + // If any step along the way failed (that are non-network related), + // try to send 'error' message to the remote + // party to indicate handshake should be terminated + pub(crate) async fn check_processing_error( + result: Result, + state: &mut State<'_, S>, + ) -> Result + where + S: Sink + Unpin, + { + match result { + Ok(ok) => Ok(ok), + Err(err) => { + state.send_handshake_error(err.to_string()).await?; + Err(err) + } + } + } + + let init_message = state.init_message(); + state.send_handshake_data(init_message).await?; + + // <- g^y || AES(k, sig(gate_priv, (g^y || g^x)) + let mid_res = state.receive_handshake_message().await?; + let (remote_ephemeral_key, remote_key_material) = + check_processing_error(Self::parse_mid_response(mid_res), &mut state).await?; + + // hkdf::::(g^xy) + state.derive_shared_key(&remote_ephemeral_key); + let verification_res = + state.verify_remote_key_material(&remote_key_material, &remote_ephemeral_key); + check_processing_error(verification_res, &mut state).await?; + + // AES(k, sig(client_priv, (g^y || g^x)) + let material = state.prepare_key_material_sig(&remote_ephemeral_key); + + // -> AES(k, sig(client_priv, g^x || g^y)) + state.send_handshake_data(material).await?; + + // <- Ok + let finalization = state.receive_handshake_message().await?; + check_processing_error(Self::parse_finalization_response(finalization), &mut state) + .await?; + Ok(state.finalize_handshake()) + }), + } + } + + // client should have received + // G^y || AES(k, SIG(PRIV_GATE, G^y || G^x)) + fn parse_mid_response( + mut resp: Vec, + ) -> Result<(encryption::PublicKey, Vec), HandshakeError> { + if resp.len() != PUBLIC_KEY_SIZE + SIGNATURE_LENGTH { + return Err(HandshakeError::MalformedResponse); + } + + let remote_key_material = resp.split_off(PUBLIC_KEY_SIZE); + // this can only fail if the provided bytes have len different from PUBLIC_KEY_SIZE + // which is impossible + let remote_ephemeral_key = encryption::PublicKey::from_bytes(&resp).unwrap(); + Ok((remote_ephemeral_key, remote_key_material)) + } + + fn parse_finalization_response(resp: Vec) -> Result<(), HandshakeError> { + if resp.len() != 1 { + return Err(HandshakeError::MalformedResponse); + } + if resp[0] == 1 { + Ok(()) + } else if resp[0] == 0 { + Err(HandshakeError::HandshakeFailure) + } else { + Err(HandshakeError::MalformedResponse) + } + } +} + +impl<'a> Future for ClientHandshake<'a> { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Pin::new(&mut self.handshake_future).poll(cx) + } +} diff --git a/gateway/gateway-requests/src/registration/handshake/error.rs b/gateway/gateway-requests/src/registration/handshake/error.rs new file mode 100644 index 0000000000..d4be0f3be8 --- /dev/null +++ b/gateway/gateway-requests/src/registration/handshake/error.rs @@ -0,0 +1,52 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use crypto::asymmetric::identity; +use std::fmt::{self, Display, Formatter}; + +#[derive(Debug)] +pub enum HandshakeError { + KeyMaterialOfInvalidSize(usize), + InvalidSignature, + NetworkError, + ClosedStream, + RemoteError(String), + MalformedResponse, + MalformedRequest, + HandshakeFailure, +} + +impl Display for HandshakeError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + HandshakeError::KeyMaterialOfInvalidSize(received) => write!( + f, + "received key material of invalid length - {}. Expected: {}", + received, + identity::SIGNATURE_LENGTH + ), + HandshakeError::InvalidSignature => write!(f, "received invalid signature"), + HandshakeError::NetworkError => write!(f, "encountered network error"), + HandshakeError::ClosedStream => { + write!(f, "the stream was closed before completing handshake") + } + HandshakeError::RemoteError(err) => write!(f, "error on the remote: {}", err), + HandshakeError::MalformedResponse => write!(f, "received response was malformed:"), + HandshakeError::MalformedRequest => write!(f, "sent request was malformed"), + HandshakeError::HandshakeFailure => write!(f, "unknown handshake failure"), + } + } +} + +impl std::error::Error for HandshakeError {} diff --git a/gateway/gateway-requests/src/registration/handshake/gateway.rs b/gateway/gateway-requests/src/registration/handshake/gateway.rs new file mode 100644 index 0000000000..5ebec6a4fd --- /dev/null +++ b/gateway/gateway-requests/src/registration/handshake/gateway.rs @@ -0,0 +1,123 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use crate::registration::handshake::shared_key::SharedKey; +use crate::registration::handshake::state::State; +use crate::registration::handshake::{error::HandshakeError, WsItem}; +use crypto::asymmetric::encryption; +use futures::future::BoxFuture; +use futures::task::{Context, Poll}; +use futures::{Future, Sink, Stream}; +use rand::{CryptoRng, RngCore}; +use std::pin::Pin; +use tokio_tungstenite::tungstenite::Message as WsMessage; + +pub(crate) struct GatewayHandshake<'a> { + handshake_future: BoxFuture<'a, Result>, +} + +impl<'a> GatewayHandshake<'a> { + pub(crate) fn new( + rng: &mut (impl RngCore + CryptoRng), + ws_stream: &'a mut S, + identity: &'a crypto::asymmetric::identity::KeyPair, + received_init_payload: Vec, + ) -> Self + where + S: Stream + Sink + Unpin + Send + 'a, + { + let mut state = State::new(rng, ws_stream, identity, None); + GatewayHandshake { + handshake_future: Box::pin(async move { + // If any step along the way failed (that are non-network related), + // try to send 'error' message to the remote + // party to indicate handshake should be terminated + pub(crate) async fn check_processing_error( + result: Result, + state: &mut State<'_, S>, + ) -> Result + where + S: Sink + Unpin, + { + match result { + Ok(ok) => Ok(ok), + Err(err) => { + state.send_handshake_error(err.to_string()).await?; + Err(err) + } + } + } + + // init: <- pub_key || g^x + let (remote_identity, remote_ephemeral_key) = check_processing_error( + State::::parse_init_message(received_init_payload), + &mut state, + ) + .await?; + state.update_remote_identity(remote_identity); + + // hkdf::::(g^xy) + state.derive_shared_key(&remote_ephemeral_key); + + // AES(k, sig(gate_priv, (g^y || g^x)) + let material = state.prepare_key_material_sig(&remote_ephemeral_key); + + // g^y || AES(k, sig(gate_priv, (g^y || g^x)) + let handshake_payload = Self::combine_material_with_ephemeral_key( + state.local_ephemeral_key(), + material, + ); + + // -> g^y || AES(k, sig(gate_priv, (g^y || g^x)) + state.send_handshake_data(handshake_payload).await?; + + // <- AES(k, sig(client_priv, g^x || g^y)) + let remote_key_material = state.receive_handshake_message().await?; + let verification_res = + state.verify_remote_key_material(&remote_key_material, &remote_ephemeral_key); + check_processing_error(verification_res, &mut state).await?; + let finalizer = Self::prepare_finalization_response(); + + // -> Ok + state.send_handshake_data(finalizer).await?; + Ok(state.finalize_handshake()) + }), + } + } + + // create g^y || AES(k, sig(gate_priv, (g^y || g^x)) + fn combine_material_with_ephemeral_key( + ephemeral_key: &encryption::PublicKey, + material: Vec, + ) -> Vec { + ephemeral_key + .to_bytes() + .iter() + .cloned() + .chain(material.into_iter()) + .collect() + } + + fn prepare_finalization_response() -> Vec { + vec![1] + } +} + +impl<'a> Future for GatewayHandshake<'a> { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Pin::new(&mut self.handshake_future).poll(cx) + } +} diff --git a/gateway/gateway-requests/src/registration/handshake/mod.rs b/gateway/gateway-requests/src/registration/handshake/mod.rs new file mode 100644 index 0000000000..a317dd87bb --- /dev/null +++ b/gateway/gateway-requests/src/registration/handshake/mod.rs @@ -0,0 +1,81 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use self::client::ClientHandshake; +use self::error::HandshakeError; +use self::gateway::GatewayHandshake; +pub use self::shared_key::{SharedKey, SharedKeySize}; +use crypto::asymmetric::identity; +use futures::{Sink, Stream}; +use rand::rngs::OsRng; +use rand::{CryptoRng, RngCore}; +use tokio_tungstenite::tungstenite::{Error as WsError, Message as WsMessage}; + +// for ease of use +pub const DEFAULT_RNG: OsRng = OsRng; + +pub(crate) type WsItem = Result; + +mod client; +pub mod error; +mod gateway; +pub mod shared_key; +mod state; + +// Note: the handshake is built on top of WebSocket, but in principle it shouldn't be too difficult +// to remove that restriction, by just changing Sink and Stream into +// AsyncWrite and AsyncRead and slightly adjusting the implementation. But right now +// we do not need to worry about that. + +pub async fn client_handshake<'a, S>( + rng: &mut (impl RngCore + CryptoRng), + ws_stream: &'a mut S, + identity: &'a identity::KeyPair, + gateway_pubkey: identity::PublicKey, +) -> Result +where + S: Stream + Sink + Unpin + Send + 'a, +{ + ClientHandshake::new(rng, ws_stream, identity, gateway_pubkey).await +} + +pub async fn gateway_handshake<'a, S>( + rng: &mut (impl RngCore + CryptoRng), + ws_stream: &'a mut S, + identity: &'a identity::KeyPair, + received_init_payload: Vec, +) -> Result +where + S: Stream + Sink + Unpin + Send + 'a, +{ + GatewayHandshake::new(rng, ws_stream, identity, received_init_payload).await +} + +/* + +Messages exchanged: + +CLIENT -> GATEWAY: +CLIENT_ID_KEY || G^x + +GATEWAY -> CLIENT +G^y || AES(k, SIG(PRIV_G, G^y || G^x)) + +CLIENT -> GATEWAY +AES(k, SIG(PRIV_C, G^x || G^y)) + +GATEWAY -> CLIENT +DONE(status) + +*/ diff --git a/gateway/gateway-requests/src/registration/handshake/shared_key.rs b/gateway/gateway-requests/src/registration/handshake/shared_key.rs new file mode 100644 index 0000000000..808f01a7fa --- /dev/null +++ b/gateway/gateway-requests/src/registration/handshake/shared_key.rs @@ -0,0 +1,86 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use crypto::symmetric::aes_ctr::{ + generic_array::{typenum::Unsigned, GenericArray}, + Aes128Key, Aes128KeySize, +}; +use std::ops::Deref; + +pub type SharedKeySize = Aes128KeySize; + +#[derive(Clone, Debug)] +pub struct SharedKey(Aes128Key); + +#[derive(Debug)] +pub enum SharedKeyConversionError { + DecodeError(bs58::decode::Error), + BytesOfInvalidLengthError, + StringOfInvalidLengthError, +} + +impl SharedKey { + pub fn try_from_bytes(bytes: &[u8]) -> Result { + if bytes.len() != SharedKeySize::to_usize() { + return Err(SharedKeyConversionError::BytesOfInvalidLengthError); + } + + Ok(SharedKey(GenericArray::clone_from_slice(bytes))) + } + + pub fn to_bytes(&self) -> Vec { + self.0.to_vec() + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_ref() + } + + pub fn try_from_base58_string>( + val: S, + ) -> Result { + let decoded = match bs58::decode(val.into()).into_vec() { + Ok(decoded) => decoded, + Err(err) => return Err(SharedKeyConversionError::DecodeError(err)), + }; + + if decoded.len() != SharedKeySize::to_usize() { + return Err(SharedKeyConversionError::StringOfInvalidLengthError); + } + + Ok(SharedKey( + GenericArray::from_exact_iter(decoded).expect("Invalid vector length!"), + )) + } + + pub fn to_base58_string(&self) -> String { + bs58::encode(self.to_bytes()).into_string() + } +} + +impl Into for SharedKey { + fn into(self) -> String { + self.to_base58_string() + } +} + +impl Deref for SharedKey { + type Target = Aes128Key; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +// I don't see any cases in which DerefMut would be useful. So did not implement it. diff --git a/gateway/gateway-requests/src/registration/handshake/state.rs b/gateway/gateway-requests/src/registration/handshake/state.rs new file mode 100644 index 0000000000..fde4f9a9f5 --- /dev/null +++ b/gateway/gateway-requests/src/registration/handshake/state.rs @@ -0,0 +1,256 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +use crate::registration::handshake::error::HandshakeError; +use crate::registration::handshake::shared_key::{SharedKey, SharedKeySize}; +use crate::registration::handshake::WsItem; +use crate::types; +use crypto::{ + asymmetric::{encryption, identity}, + kdf::blake3_hkdf, + symmetric::aes_ctr::{self, generic_array::typenum::Unsigned}, +}; +use futures::{Sink, SinkExt, Stream, StreamExt}; +use log::*; +use rand::{CryptoRng, RngCore}; +use std::convert::{TryFrom, TryInto}; +use tokio_tungstenite::tungstenite::Message as WsMessage; + +/// Handshake state. +pub(crate) struct State<'a, S> { + /// The underlying WebSocket stream. + ws_stream: &'a mut S, + /// Identity of the local "node" (client or gateway) which is used + /// during the handshake. + identity: &'a identity::KeyPair, + /// Local ephemeral Diffie-Hellman keypair generated as a part of the handshake. + ephemeral_keypair: encryption::KeyPair, + /// The derived shared key using the ephemeral keys of both parties. + derived_shared_key: Option, + /// The known or received public identity key of the remote. + /// Ideally it would always be known before the handshake was initiated. + remote_pubkey: Option, +} + +impl<'a, S> State<'a, S> { + pub(crate) fn new( + rng: &mut (impl RngCore + CryptoRng), + ws_stream: &'a mut S, + identity: &'a identity::KeyPair, + remote_pubkey: Option, + ) -> Self { + let ephemeral_keypair = encryption::KeyPair::new_with_rng(rng); + State { + ws_stream, + ephemeral_keypair, + identity, + remote_pubkey, + derived_shared_key: None, + } + } + + pub(crate) fn local_ephemeral_key(&self) -> &encryption::PublicKey { + self.ephemeral_keypair.public_key() + } + + // LOCAL_ID_PUBKEY || EPHEMERAL_KEY + // Eventually the ID_PUBKEY prefix will get removed and recipient will know + // initializer's identity from another source. + pub(crate) fn init_message(&self) -> Vec { + self.identity + .public_key() + .to_bytes() + .iter() + .cloned() + .chain( + self.ephemeral_keypair + .public_key() + .to_bytes() + .iter() + .cloned(), + ) + .collect() + } + + // this will need to be adjusted when REMOTE_ID_PUBKEY is removed + pub(crate) fn parse_init_message( + mut init_message: Vec, + ) -> Result<(identity::PublicKey, encryption::PublicKey), HandshakeError> { + if init_message.len() != identity::PUBLIC_KEY_LENGTH + encryption::PUBLIC_KEY_SIZE { + return Err(HandshakeError::MalformedRequest); + } + + let remote_ephemeral_key_bytes = init_message.split_off(identity::PUBLIC_KEY_LENGTH); + // this can only fail if the provided bytes have len different from encryption::PUBLIC_KEY_SIZE + // which is impossible + let remote_ephemeral_key = + encryption::PublicKey::from_bytes(&remote_ephemeral_key_bytes).unwrap(); + + // this could actually fail if the curve point fails to get decompressed + let remote_identity = identity::PublicKey::from_bytes(&init_message) + .map_err(|_| HandshakeError::MalformedRequest)?; + + Ok((remote_identity, remote_ephemeral_key)) + } + + pub(crate) fn derive_shared_key(&mut self, remote_ephemeral_key: &encryption::PublicKey) { + let dh_result = self + .ephemeral_keypair + .private_key() + .diffie_hellman(remote_ephemeral_key); + + // there is no reason for this to fail as our okm is expected to be only 16 bytes + let okm = + blake3_hkdf::extract_then_expand(None, &dh_result, None, SharedKeySize::to_usize()) + .expect("somehow too long okm was provided"); + + let derived_shared_key = + SharedKey::try_from_bytes(&okm).expect("okm was expanded to incorrect length!"); + + self.derived_shared_key = Some(derived_shared_key) + } + + // produces AES(k, SIG(ID_PRIV, G^x || G^y), + // assuming x is local and y is remote + pub(crate) fn prepare_key_material_sig( + &self, + remote_ephemeral_key: &encryption::PublicKey, + ) -> Vec { + let message: Vec<_> = self + .ephemeral_keypair + .public_key() + .to_bytes() + .iter() + .cloned() + .chain(remote_ephemeral_key.to_bytes().iter().cloned()) + .collect(); + + let signature = self.identity.private_key().sign(&message); + aes_ctr::encrypt( + self.derived_shared_key.as_ref().unwrap(), + &aes_ctr::zero_iv(), + &signature.to_bytes(), + ) + } + + // must be called after shared key was derived locally and remote's identity is known + pub(crate) fn verify_remote_key_material( + &self, + remote_material: &[u8], + remote_ephemeral_key: &encryption::PublicKey, + ) -> Result<(), HandshakeError> { + if remote_material.len() != identity::SIGNATURE_LENGTH { + return Err(HandshakeError::KeyMaterialOfInvalidSize( + remote_material.len(), + )); + } + let derived_shared_key = self + .derived_shared_key + .as_ref() + .expect("shared key was not derived!"); + + // first decrypt received data + let decrypted_signature = + aes_ctr::decrypt(derived_shared_key, &aes_ctr::zero_iv(), remote_material); + + // now verify signature itself + let signature = identity::Signature::from_bytes(&decrypted_signature) + .map_err(|_| HandshakeError::InvalidSignature)?; + + // g^y || g^x, if y is remote and x is local + let signed_payload: Vec<_> = remote_ephemeral_key + .to_bytes() + .iter() + .cloned() + .chain( + self.ephemeral_keypair + .public_key() + .to_bytes() + .iter() + .cloned(), + ) + .collect(); + + self.remote_pubkey + .as_ref() + .unwrap() + .verify(&signed_payload, &signature) + .map_err(|_| HandshakeError::InvalidSignature) + } + + pub(crate) fn update_remote_identity(&mut self, remote_pubkey: identity::PublicKey) { + self.remote_pubkey = Some(remote_pubkey) + } + + pub(crate) async fn receive_handshake_message(&mut self) -> Result, HandshakeError> + where + S: Stream + Unpin, + { + loop { + if let Some(msg) = self.ws_stream.next().await { + if let Ok(msg) = msg { + match msg { + WsMessage::Text(ws_msg) => match types::RegistrationHandshake::try_from(ws_msg) { + Ok(reg_handshake_msg) => return match reg_handshake_msg { + types::RegistrationHandshake::HandshakePayload { data } => Ok(data), + types::RegistrationHandshake::HandshakeError { message } => Err(HandshakeError::RemoteError(message)), + }, + Err(_) => error!("Received a non-handshake message during the registration handshake! It's getting dropped."), + }, + _ => error!("Received non-text message during registration handshake"), + } + } else { + return Err(HandshakeError::NetworkError); + } + } else { + return Err(HandshakeError::ClosedStream); + } + } + } + + // upon receiving this, the receiver should terminate the handshake + pub(crate) async fn send_handshake_error>( + &mut self, + message: M, + ) -> Result<(), HandshakeError> + where + S: Sink + Unpin, + { + let handshake_message = types::RegistrationHandshake::new_error(message); + self.ws_stream + .send(WsMessage::Text(handshake_message.try_into().unwrap())) + .await + .map_err(|_| HandshakeError::ClosedStream) + } + + pub(crate) async fn send_handshake_data( + &mut self, + payload: Vec, + ) -> Result<(), HandshakeError> + where + S: Sink + Unpin, + { + let handshake_message = types::RegistrationHandshake::new_payload(payload); + self.ws_stream + .send(WsMessage::Text(handshake_message.try_into().unwrap())) + .await + .map_err(|_| HandshakeError::ClosedStream) + } + + /// Finish the handshake, yielding the derived shared key and implicitly dropping all borrowed + /// values. + pub(crate) fn finalize_handshake(self) -> SharedKey { + self.derived_shared_key.unwrap() + } +} diff --git a/gateway/gateway-requests/src/registration/mod.rs b/gateway/gateway-requests/src/registration/mod.rs new file mode 100644 index 0000000000..a4a154f8b9 --- /dev/null +++ b/gateway/gateway-requests/src/registration/mod.rs @@ -0,0 +1,20 @@ +// Copyright 2020 Nym Technologies SA +// +// 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. + +pub mod handshake; + +// TODO: is it perhaps possible to replace the 'custom' handshake with an existing +// implementation like with one of the variants on the Noise framework? + +// Right now it's based on the STS (Station-to-Station) Protocol. diff --git a/gateway/gateway-requests/src/types.rs b/gateway/gateway-requests/src/types.rs index 1a5a221743..bce58010c9 100644 --- a/gateway/gateway-requests/src/types.rs +++ b/gateway/gateway-requests/src/types.rs @@ -12,7 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::auth_token::AuthToken; +use crate::authentication::encrypted_address::EncryptedAddressBytes; +use crate::authentication::iv::AuthenticationIV; +use crate::registration::handshake::SharedKey; +use crypto::symmetric::aes_ctr; use nymsphinx::addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError}; use nymsphinx::params::packet_sizes::PacketSize; use nymsphinx::{DestinationAddressBytes, SphinxPacket}; @@ -24,11 +27,47 @@ use std::{ }; use tokio_tungstenite::tungstenite::protocol::Message; +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum RegistrationHandshake { + HandshakePayload { data: Vec }, + HandshakeError { message: String }, +} + +impl RegistrationHandshake { + pub fn new_payload(data: Vec) -> Self { + RegistrationHandshake::HandshakePayload { data } + } + + pub fn new_error>(message: S) -> Self { + RegistrationHandshake::HandshakeError { + message: message.into(), + } + } +} + +impl TryFrom for RegistrationHandshake { + type Error = serde_json::Error; + + fn try_from(msg: String) -> Result { + serde_json::from_str(&msg) + } +} + +impl TryInto for RegistrationHandshake { + type Error = serde_json::Error; + + fn try_into(self) -> Result { + serde_json::to_string(&self) + } +} + #[derive(Debug)] pub enum GatewayRequestsError { IncorrectlyEncodedAddress, RequestOfInvalidSize(usize), MalformedSphinxPacket, + MalformedEncryption, } // to use it as `std::error::Error`, and we don't want to just derive is because we want @@ -44,6 +83,7 @@ impl fmt::Display for GatewayRequestsError { actual, PacketSize::ACKPacket.size(), PacketSize::RegularPacket.size(), PacketSize::ExtendedPacket.size() ), MalformedSphinxPacket => write!(f, "received sphinx packet was malformed"), + MalformedEncryption => write!(f, "the received encrypted data was malformed"), } } } @@ -57,21 +97,25 @@ impl From for GatewayRequestsError { #[derive(Serialize, Deserialize, Debug)] #[serde(tag = "type", rename_all = "camelCase")] pub enum ClientControlRequest { - Authenticate { address: String, token: String }, - Register { address: String }, + Authenticate { + address: String, + enc_address: String, + iv: String, + }, + #[serde(alias = "handshakePayload")] + RegisterHandshakeInitRequest { data: Vec }, } impl ClientControlRequest { - pub fn new_authenticate(address: DestinationAddressBytes, token: AuthToken) -> Self { + pub fn new_authenticate( + address: DestinationAddressBytes, + enc_address: EncryptedAddressBytes, + iv: AuthenticationIV, + ) -> Self { ClientControlRequest::Authenticate { address: address.to_base58_string(), - token: token.to_base58_string(), - } - } - - pub fn new_register(address: DestinationAddressBytes) -> Self { - ClientControlRequest::Register { - address: address.to_base58_string(), + enc_address: enc_address.to_base58_string(), + iv: iv.to_base58_string(), } } } @@ -105,7 +149,7 @@ impl TryInto for ClientControlRequest { #[serde(tag = "type", rename_all = "camelCase")] pub enum ServerResponse { Authenticate { status: bool }, - Register { token: String }, + Register { status: bool }, Send { status: bool }, Error { message: String }, } @@ -127,7 +171,7 @@ impl ServerResponse { pub fn implies_successful_authentication(&self) -> bool { match self { ServerResponse::Authenticate { status, .. } => *status, - ServerResponse::Register { .. } => true, + ServerResponse::Register { status, .. } => *status, _ => false, } } @@ -157,20 +201,38 @@ pub enum BinaryRequest { }, } +// TODO: ask @AP if that's sufficient +const PADDING_LEN: usize = 16; + +// Right now the only valid `BinaryRequest` is a request to forward a sphinx packet. +// It is encrypted using the derived shared key between client and the gateway. Thanks to +// randomness inside the sphinx packet themselves (even via the same route), the 0s IV can be used here. +// HOWEVER, NOTE: If we introduced another 'BinaryRequest', we must carefully examine if a 0s IV +// would work there. impl BinaryRequest { - pub fn try_from_bytes(raw_req: &[u8]) -> Result { + pub fn try_from_encrypted_bytes( + mut raw_req: Vec, + shared_key: &SharedKey, + ) -> Result { + aes_ctr::decrypt_in_place(shared_key, &aes_ctr::zero_iv(), &mut raw_req); + // see if the padding is retained + if !raw_req.iter().rev().take(PADDING_LEN).all(|&x| x == 0) { + return Err(GatewayRequestsError::MalformedEncryption); + } + // right now there's only a single option possible which significantly simplifies the logic // if we decided to allow for more 'binary' messages, the API wouldn't need to change let address = NymNodeRoutingAddress::try_from_bytes(&raw_req)?; let addr_offset = address.bytes_min_len(); - let packet_size = raw_req[addr_offset..].len(); + let sphinx_packet_data = &raw_req[addr_offset..raw_req.len() - PADDING_LEN]; + let packet_size = sphinx_packet_data.len(); if let Err(_) = PacketSize::get_type(packet_size) { // TODO: should this allow AckPacket sizes? Err(GatewayRequestsError::RequestOfInvalidSize(packet_size)) } else { - let sphinx_packet = match SphinxPacket::from_bytes(&raw_req[addr_offset..]) { + let sphinx_packet = match SphinxPacket::from_bytes(sphinx_packet_data) { Ok(packet) => packet, Err(_) => return Err(GatewayRequestsError::MalformedSphinxPacket), }; @@ -182,21 +244,27 @@ impl BinaryRequest { } } - pub fn into_bytes(self) -> Vec { + pub fn into_encrypted_bytes(self, shared_key: &SharedKey) -> Vec { match self { BinaryRequest::ForwardSphinx { address, sphinx_packet, } => { // TODO: using intermediate `NymNodeRoutingAddress` here is just temporary, because - // it happens to do exactly what we needed, but we don't really want to be + // it happens to do exactly what we needed, but we really don't want to be // dependant on what it does let wrapped_address = NymNodeRoutingAddress::from(address); - wrapped_address + + // add 16 bytes of padding so that the gateway could catch incorrect encryption + let mut gateway_data: Vec<_> = wrapped_address .as_bytes() .into_iter() .chain(sphinx_packet.to_bytes().into_iter()) - .collect() + .chain(std::iter::repeat(0).take(PADDING_LEN)) + .collect(); + + aes_ctr::encrypt_in_place(shared_key, &aes_ctr::zero_iv(), &mut gateway_data); + gateway_data } } } @@ -208,12 +276,30 @@ impl BinaryRequest { sphinx_packet, } } -} -impl Into for BinaryRequest { - fn into(self) -> Message { - Message::Binary(self.into_bytes()) + pub fn into_ws_message(self, shared_key: &SharedKey) -> Message { + Message::Binary(self.into_encrypted_bytes(shared_key)) } } -// TODO: tests... +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn handshake_payload_can_be_deserialized_into_register_handshake_init_request() { + let handshake_data = vec![1, 2, 3, 4, 5, 6]; + let handshake_payload = RegistrationHandshake::HandshakePayload { + data: handshake_data.clone(), + }; + let serialized = serde_json::to_string(&handshake_payload).unwrap(); + let deserialized = ClientControlRequest::try_from(serialized).unwrap(); + + match deserialized { + ClientControlRequest::RegisterHandshakeInitRequest { data } => { + assert_eq!(data, handshake_data) + } + _ => unreachable!("this branch shouldn't have been reached!"), + } + } +} diff --git a/gateway/src/commands/init.rs b/gateway/src/commands/init.rs index 54752d5f63..0a55ecaf6c 100644 --- a/gateway/src/commands/init.rs +++ b/gateway/src/commands/init.rs @@ -16,7 +16,7 @@ use crate::commands::override_config; use crate::config::persistence::pathfinder::GatewayPathfinder; use clap::{App, Arg, ArgMatches}; use config::NymConfig; -use crypto::encryption; +use crypto::asymmetric::{encryption, identity}; use pemstore::pemstore::PemStore; pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { @@ -113,13 +113,18 @@ pub fn execute(matches: &ArgMatches) { config = override_config(config, matches); + let identity_keys = identity::KeyPair::new(); let sphinx_keys = encryption::KeyPair::new(); let pathfinder = GatewayPathfinder::new_from_config(&config); let pem_store = PemStore::new(pathfinder); pem_store - .write_encryption_keys(sphinx_keys) + .write_encryption_keypair(&sphinx_keys) .expect("Failed to save sphinx keys"); - println!("Saved mixnet sphinx keypair"); + pem_store + .write_identity_keypair(&identity_keys) + .expect("Failed to save identity keys"); + + println!("Saved identity and mixnet sphinx keypairs"); let config_save_location = config.get_config_file_save_location(); config diff --git a/gateway/src/commands/run.rs b/gateway/src/commands/run.rs index 0a0d780c4f..ad87ac87ef 100644 --- a/gateway/src/commands/run.rs +++ b/gateway/src/commands/run.rs @@ -18,7 +18,8 @@ use crate::config::Config; use crate::node::Gateway; use clap::{App, Arg, ArgMatches}; use config::NymConfig; -use crypto::encryption; +use crypto::asymmetric::{encryption, identity}; +use pemstore::pathfinder::PathFinder; use pemstore::pemstore::PemStore; pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { @@ -127,17 +128,28 @@ fn special_addresses() -> Vec<&'static str> { vec!["localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]"] } -fn load_sphinx_keys(config_file: &Config) -> encryption::KeyPair { - let sphinx_keypair = PemStore::new(GatewayPathfinder::new_from_config(&config_file)) - .read_encryption() +fn load_sphinx_keys(pemstore: &PemStore

) -> encryption::KeyPair { + let sphinx_keypair = pemstore + .read_encryption_keypair() .expect("Failed to read stored sphinx key files"); println!( - "Public key: {}\n", + "Public sphinx key: {}\n", sphinx_keypair.public_key().to_base58_string() ); sphinx_keypair } +fn load_identity_keys(pemstore: &PemStore

) -> identity::KeyPair { + let identity_keypair = pemstore + .read_identity_keypair() + .expect("Failed to read stored identity key files"); + println!( + "Public identity key: {}\n", + identity_keypair.public_key().to_base58_string() + ); + identity_keypair +} + pub fn execute(matches: &ArgMatches) { let id = matches.value_of("id").unwrap(); @@ -149,7 +161,9 @@ pub fn execute(matches: &ArgMatches) { config = override_config(config, matches); - let sphinx_keypair = load_sphinx_keys(&config); + let pemstore = PemStore::new(GatewayPathfinder::new_from_config(&config)); + let sphinx_keypair = load_sphinx_keys(&pemstore); + let identity = load_identity_keys(&pemstore); let mix_listening_ip_string = config.get_mix_listening_address().ip().to_string(); if special_addresses().contains(&mix_listening_ip_string.as_ref()) { @@ -194,5 +208,5 @@ pub fn execute(matches: &ArgMatches) { config.get_clients_ledger_path() ); - Gateway::new(config, sphinx_keypair).run(); + Gateway::new(config, sphinx_keypair, identity).run(); } diff --git a/gateway/src/config/mod.rs b/gateway/src/config/mod.rs index a985499fc3..bb53898960 100644 --- a/gateway/src/config/mod.rs +++ b/gateway/src/config/mod.rs @@ -105,6 +105,19 @@ impl Config { self.gateway.public_sphinx_key_file = self::Gateway::default_public_sphinx_key_file(&id); } + if self + .gateway + .private_identity_key_file + .as_os_str() + .is_empty() + { + self.gateway.private_identity_key_file = + self::Gateway::default_private_identity_key_file(&id); + } + if self.gateway.public_identity_key_file.as_os_str().is_empty() { + self.gateway.public_identity_key_file = + self::Gateway::default_public_identity_key_file(&id); + } if self .clients_endpoint .inboxes_directory @@ -317,6 +330,14 @@ impl Config { self.gateway.location.clone() } + pub fn get_private_identity_key_file(&self) -> PathBuf { + self.gateway.private_identity_key_file.clone() + } + + pub fn get_public_identity_key_file(&self) -> PathBuf { + self.gateway.public_identity_key_file.clone() + } + pub fn get_private_sphinx_key_file(&self) -> PathBuf { self.gateway.private_sphinx_key_file.clone() } @@ -390,6 +411,12 @@ pub struct Gateway { /// this field with as much accuracy as you wish to share. location: String, + /// Path to file containing private identity key. + private_identity_key_file: PathBuf, + + /// Path to file containing public identity key. + public_identity_key_file: PathBuf, + /// Path to file containing private sphinx key. private_sphinx_key_file: PathBuf, @@ -413,6 +440,14 @@ impl Gateway { Config::default_data_directory(Some(id)).join("public_sphinx.pem") } + fn default_private_identity_key_file(id: &str) -> PathBuf { + Config::default_data_directory(Some(id)).join("private_identity.pem") + } + + fn default_public_identity_key_file(id: &str) -> PathBuf { + Config::default_data_directory(Some(id)).join("public_identity.pem") + } + fn default_location() -> String { "unknown".into() } @@ -423,6 +458,8 @@ impl Default for Gateway { Gateway { id: "".to_string(), location: Self::default_location(), + private_identity_key_file: Default::default(), + public_identity_key_file: Default::default(), private_sphinx_key_file: Default::default(), public_sphinx_key_file: Default::default(), presence_directory_server: DEFAULT_DIRECTORY_SERVER.to_string(), diff --git a/gateway/src/config/persistence/pathfinder.rs b/gateway/src/config/persistence/pathfinder.rs index 81d3d2d69b..1a004ace27 100644 --- a/gateway/src/config/persistence/pathfinder.rs +++ b/gateway/src/config/persistence/pathfinder.rs @@ -21,6 +21,8 @@ pub struct GatewayPathfinder { pub config_dir: PathBuf, pub private_sphinx_key: PathBuf, pub public_sphinx_key: PathBuf, + pub private_identity_key: PathBuf, + pub public_identity_key: PathBuf, } impl GatewayPathfinder { @@ -29,6 +31,8 @@ impl GatewayPathfinder { config_dir: config.get_config_file_save_location(), private_sphinx_key: config.get_private_sphinx_key_file(), public_sphinx_key: config.get_public_sphinx_key_file(), + private_identity_key: config.get_private_identity_key_file(), + public_identity_key: config.get_public_identity_key_file(), } } } @@ -39,13 +43,11 @@ impl PathFinder for GatewayPathfinder { } fn private_identity_key(&self) -> PathBuf { - // TEMPORARILY USE SAME KEYS AS ENCRYPTION - self.private_sphinx_key.clone() + self.private_identity_key.clone() } fn public_identity_key(&self) -> PathBuf { - // TEMPORARILY USE SAME KEYS AS ENCRYPTION - self.public_sphinx_key.clone() + self.public_identity_key.clone() } fn private_encryption_key(&self) -> Option { diff --git a/gateway/src/config/template.rs b/gateway/src/config/template.rs index 70eac5b249..55e2c230a0 100644 --- a/gateway/src/config/template.rs +++ b/gateway/src/config/template.rs @@ -33,6 +33,12 @@ id = '{{ gateway.id }}' # this field with as much accuracy as you wish to share. location = '{{ gateway.location }}' +# Path to file containing private identity key. +private_identity_key_file = '{{ gateway.private_identity_key_file }}' + +# Path to file containing public identity key. +public_identity_key_file = '{{ gateway.public_identity_key_file }}' + # Path to file containing private sphinx key. private_sphinx_key_file = '{{ gateway.private_sphinx_key_file }}' diff --git a/gateway/src/node/client_handling/clients_handler.rs b/gateway/src/node/client_handling/clients_handler.rs index 273d6b70b2..163e56b890 100644 --- a/gateway/src/node/client_handling/clients_handler.rs +++ b/gateway/src/node/client_handling/clients_handler.rs @@ -16,18 +16,16 @@ use crate::node::{ client_handling::websocket::message_receiver::MixMessageSender, storage::{inboxes::ClientStorage, ClientLedger}, }; -use crypto::encryption; use futures::{ channel::{mpsc, oneshot}, StreamExt, }; -use gateway_requests::auth_token::AuthToken; -use hmac::{Hmac, Mac}; +use gateway_requests::authentication::encrypted_address::EncryptedAddressBytes; +use gateway_requests::authentication::iv::AuthenticationIV; +use gateway_requests::registration::handshake::SharedKey; use log::*; use nymsphinx::DestinationAddressBytes; -use sha2::Sha256; use std::collections::HashMap; -use std::sync::Arc; use tokio::task::JoinHandle; pub(crate) type ClientsHandlerRequestSender = mpsc::UnboundedSender; @@ -35,17 +33,19 @@ pub(crate) type ClientsHandlerRequestReceiver = mpsc::UnboundedReceiver; -#[derive(Debug)] +// #[derive(Debug)] pub(crate) enum ClientsHandlerRequest { // client Register( DestinationAddressBytes, + SharedKey, MixMessageSender, ClientsHandlerResponseSender, ), Authenticate( DestinationAddressBytes, - AuthToken, + EncryptedAddressBytes, + AuthenticationIV, MixMessageSender, ClientsHandlerResponseSender, ), @@ -57,27 +57,21 @@ pub(crate) enum ClientsHandlerRequest { #[derive(Debug)] pub(crate) enum ClientsHandlerResponse { - Register(AuthToken), - Authenticate(bool), + Register(bool), + Authenticate(Option), IsOnline(Option), Error(Box), } pub(crate) struct ClientsHandler { - secret_key: Arc, open_connections: HashMap, clients_ledger: ClientLedger, clients_inbox_storage: ClientStorage, } impl ClientsHandler { - pub(crate) fn new( - secret_key: Arc, - clients_ledger: ClientLedger, - clients_inbox_storage: ClientStorage, - ) -> Self { + pub(crate) fn new(clients_ledger: ClientLedger, clients_inbox_storage: ClientStorage) -> Self { ClientsHandler { - secret_key, open_connections: HashMap::new(), clients_ledger, clients_inbox_storage, @@ -101,18 +95,6 @@ impl ClientsHandler { } } - fn generate_new_auth_token(&self, client_address: DestinationAddressBytes) -> AuthToken { - type HmacSha256 = Hmac; - - // note that `new_varkey` doesn't even have an execution branch returning an error - // (true as of hmac 0.7.1) - let mut auth_token_raw = HmacSha256::new_varkey(&self.secret_key.to_bytes()).unwrap(); - auth_token_raw.input(client_address.as_bytes()); - let mut auth_token = [0u8; 32]; - auth_token.copy_from_slice(auth_token_raw.result().code().as_slice()); - AuthToken::from_bytes(auth_token) - } - async fn push_stored_messages_to_client_and_save_channel( &mut self, client_address: DestinationAddressBytes, @@ -172,6 +154,7 @@ impl ClientsHandler { async fn handle_register_request( &mut self, address: DestinationAddressBytes, + derived_shared_key: SharedKey, comm_channel: MixMessageSender, res_channel: ClientsHandlerResponseSender, ) { @@ -188,12 +171,9 @@ impl ClientsHandler { return; } - // I presume some additional checks will go here: - // ... - let auth_token = self.generate_new_auth_token(address.clone()); if self .clients_ledger - .insert_token(auth_token.clone(), address.clone()) + .insert_shared_key(derived_shared_key, address.clone()) .unwrap() .is_some() { @@ -206,17 +186,17 @@ impl ClientsHandler { .create_storage_dir(address.clone()) .await { - error!("We failed to create inbox directory for the client -{:?}\nReverting issued token...", e); + error!("We failed to create inbox directory for the client -{:?}\nReverting stored shared key...", e); // we must revert our changes if this operation failed - self.clients_ledger.remove_token(&address).unwrap(); - self.send_error_response("failed to issue an auth token", res_channel); + self.clients_ledger.remove_shared_key(&address).unwrap(); + self.send_error_response("failed to complete issuing shared key", res_channel); return; } self.push_stored_messages_to_client_and_save_channel(address, comm_channel) .await; - if let Err(_) = res_channel.send(ClientsHandlerResponse::Register(auth_token)) { + if let Err(_) = res_channel.send(ClientsHandlerResponse::Register(true)) { error!("Somehow we failed to send response back to websocket handler - there seem to be a weird bug present!"); } } @@ -224,7 +204,8 @@ impl ClientsHandler { async fn handle_authenticate_request( &mut self, address: DestinationAddressBytes, - token: AuthToken, + encrypted_address: EncryptedAddressBytes, + iv: AuthenticationIV, comm_channel: MixMessageSender, res_channel: ClientsHandlerResponseSender, ) { @@ -239,14 +220,26 @@ impl ClientsHandler { return; } - if self.clients_ledger.verify_token(&token, &address).unwrap() { + if self + .clients_ledger + .verify_shared_key(&address, &encrypted_address, &iv) + .unwrap() + { + // The first unwrap is due to possible db read errors, but I'm not entirely sure when could + // the second one happen. + let shared_key = self + .clients_ledger + .get_shared_key(&address) + .unwrap() + .unwrap(); self.push_stored_messages_to_client_and_save_channel(address, comm_channel) .await; - if let Err(_) = res_channel.send(ClientsHandlerResponse::Authenticate(true)) { + if let Err(_) = res_channel.send(ClientsHandlerResponse::Authenticate(Some(shared_key))) + { error!("Somehow we failed to send response back to websocket handler - there seem to be a weird bug present!"); } } else { - if let Err(_) = res_channel.send(ClientsHandlerResponse::Authenticate(false)) { + if let Err(_) = res_channel.send(ClientsHandlerResponse::Authenticate(None)) { error!("Somehow we failed to send response back to websocket handler - there seem to be a weird bug present!"); } } @@ -286,13 +279,35 @@ impl ClientsHandler { ) { while let Some(request) = request_receiver_channel.next().await { match request { - ClientsHandlerRequest::Register(address, comm_channel, res_channel) => { - self.handle_register_request(address, comm_channel, res_channel) - .await + ClientsHandlerRequest::Register( + address, + derived_shared_key, + comm_channel, + res_channel, + ) => { + self.handle_register_request( + address, + derived_shared_key, + comm_channel, + res_channel, + ) + .await } - ClientsHandlerRequest::Authenticate(address, token, comm_channel, res_channel) => { - self.handle_authenticate_request(address, token, comm_channel, res_channel) - .await + ClientsHandlerRequest::Authenticate( + address, + encrypted_address, + iv, + comm_channel, + res_channel, + ) => { + self.handle_authenticate_request( + address, + encrypted_address, + iv, + comm_channel, + res_channel, + ) + .await } ClientsHandlerRequest::Disconnect(address) => self.handle_disconnect(address), ClientsHandlerRequest::IsOnline(address, res_channel) => { diff --git a/gateway/src/node/client_handling/websocket/connection_handler.rs b/gateway/src/node/client_handling/websocket/connection_handler.rs index 92780d2f20..ddf0cef7c5 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler.rs @@ -12,22 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -use futures::{ - channel::{mpsc, oneshot}, - SinkExt, -}; -use log::*; -use std::convert::TryFrom; -use tokio::{prelude::*, stream::StreamExt}; -use tokio_tungstenite::{ - tungstenite::{protocol::Message, Error as WsError}, - WebSocketStream, -}; - -use gateway_requests::auth_token::AuthToken; -use gateway_requests::types::{BinaryRequest, ClientControlRequest, ServerResponse}; -use nymsphinx::DestinationAddressBytes; - use crate::node::client_handling::clients_handler::{ ClientsHandlerRequest, ClientsHandlerRequestSender, ClientsHandlerResponse, }; @@ -35,6 +19,25 @@ use crate::node::client_handling::websocket::message_receiver::{ MixMessageReceiver, MixMessageSender, }; use crate::node::mixnet_handling::sender::OutboundMixMessageSender; +use crypto::asymmetric::identity; +use futures::{ + channel::{mpsc, oneshot}, + SinkExt, +}; +use gateway_requests::authentication::encrypted_address::EncryptedAddressBytes; +use gateway_requests::authentication::iv::AuthenticationIV; +use gateway_requests::registration::handshake::error::HandshakeError; +use gateway_requests::registration::handshake::{gateway_handshake, SharedKey, DEFAULT_RNG}; +use gateway_requests::types::{BinaryRequest, ClientControlRequest, ServerResponse}; +use log::*; +use nymsphinx::DestinationAddressBytes; +use std::convert::TryFrom; +use std::sync::Arc; +use tokio::{prelude::*, stream::StreamExt}; +use tokio_tungstenite::{ + tungstenite::{protocol::Message, Error as WsError}, + WebSocketStream, +}; //// TODO: note for my future self to consider the following idea: //// split the socket connection into sink and stream @@ -42,39 +45,54 @@ use crate::node::mixnet_handling::sender::OutboundMixMessageSender; //// and sink for pumping responses AND mix traffic //// but as byproduct this might (or might not) break the clean "SocketStream" enum here -enum SocketStream { +enum SocketStream { RawTCP(S), UpgradedWebSocket(WebSocketStream), Invalid, } -pub(crate) struct Handle { - address: Option, +impl SocketStream { + fn is_websocket(&self) -> bool { + match self { + SocketStream::UpgradedWebSocket(_) => true, + _ => false, + } + } +} + +pub(crate) struct Handle { + remote_address: Option, + shared_key: Option, clients_handler_sender: ClientsHandlerRequestSender, outbound_mix_sender: OutboundMixMessageSender, socket_connection: SocketStream, + + local_identity: Arc, } -impl Handle -where - S: AsyncRead + AsyncWrite + Unpin, -{ +impl Handle { // for time being we assume handle is always constructed from raw socket. // if we decide we want to change it, that's not too difficult pub(crate) fn new( conn: S, clients_handler_sender: ClientsHandlerRequestSender, outbound_mix_sender: OutboundMixMessageSender, + local_identity: Arc, ) -> Self { Handle { - address: None, + remote_address: None, + shared_key: None, clients_handler_sender, outbound_mix_sender, socket_connection: SocketStream::RawTCP(conn), + local_identity, } } - async fn perform_websocket_handshake(&mut self) -> Result<(), WsError> { + async fn perform_websocket_handshake(&mut self) -> Result<(), WsError> + where + S: AsyncRead + AsyncWrite + Unpin, + { self.socket_connection = match std::mem::replace(&mut self.socket_connection, SocketStream::Invalid) { SocketStream::RawTCP(conn) => { @@ -94,14 +112,42 @@ where Ok(()) } - async fn next_websocket_request(&mut self) -> Option> { + async fn perform_registration_handshake( + &mut self, + init_msg: Vec, + ) -> Result + where + S: AsyncRead + AsyncWrite + Unpin + Send, + { + debug_assert!(self.socket_connection.is_websocket()); + match &mut self.socket_connection { + SocketStream::UpgradedWebSocket(ws_stream) => { + gateway_handshake( + &mut DEFAULT_RNG, + ws_stream, + self.local_identity.as_ref(), + init_msg, + ) + .await + } + _ => unreachable!(), + } + } + + async fn next_websocket_request(&mut self) -> Option> + where + S: AsyncRead + AsyncWrite + Unpin, + { match self.socket_connection { SocketStream::UpgradedWebSocket(ref mut ws_stream) => ws_stream.next().await, _ => panic!("impossible state - websocket handshake was somehow reverted"), } } - async fn send_websocket_response(&mut self, msg: Message) -> Result<(), WsError> { + async fn send_websocket_response(&mut self, msg: Message) -> Result<(), WsError> + where + S: AsyncRead + AsyncWrite + Unpin, + { match self.socket_connection { // TODO: more closely investigate difference between `Sink::send` and `Sink::send_all` // it got something to do with batching and flushing - it might be important if it @@ -111,10 +157,10 @@ where } } - async fn send_websocket_sphinx_packets( - &mut self, - packets: Vec>, - ) -> Result<(), WsError> { + async fn send_websocket_sphinx_packets(&mut self, packets: Vec>) -> Result<(), WsError> + where + S: AsyncRead + AsyncWrite + Unpin, + { let messages: Vec> = packets .into_iter() .map(|packet| Ok(Message::Binary(packet))) @@ -131,7 +177,7 @@ where fn disconnect(&self) { // if we never established what is the address of the client, its connection was never // announced hence we do not need to send 'disconnect' message - self.address.as_ref().map(|addr| { + self.remote_address.as_ref().map(|addr| { self.clients_handler_sender .unbounded_send(ClientsHandlerRequest::Disconnect(addr.clone())) .unwrap(); @@ -141,7 +187,12 @@ where async fn handle_binary(&self, bin_msg: Vec) -> Message { trace!("Handling binary message (presumably sphinx packet)"); - match BinaryRequest::try_from_bytes(&bin_msg) { + match BinaryRequest::try_from_encrypted_bytes( + bin_msg, + self.shared_key + .as_ref() + .expect("no shared key present even though we authenticated the client!"), + ) { Err(e) => ServerResponse::new_error(e.to_string()), Ok(request) => match request { // currently only a single type exists @@ -163,7 +214,8 @@ where async fn handle_authenticate( &mut self, address: String, - token: String, + enc_address: String, + iv: String, mix_sender: MixMessageSender, ) -> ServerResponse { let address = match DestinationAddressBytes::try_from_base58_string(address) { @@ -173,28 +225,43 @@ where return ServerResponse::new_error("malformed destination address").into(); } }; - let token = match AuthToken::try_from_base58_string(token) { - Ok(token) => token, + + let encrypted_address = match EncryptedAddressBytes::try_from_base58_string(enc_address) { + Ok(address) => address, Err(e) => { - trace!("failed to parse received AuthToken: {:?}", e); - return ServerResponse::new_error("malformed authentication token").into(); + trace!("failed to parse received encrypted address: {:?}", e); + return ServerResponse::new_error("malformed encrypted address").into(); + } + }; + + let iv = match AuthenticationIV::try_from_base58_string(iv) { + Ok(iv) => iv, + Err(e) => { + trace!("failed to parse received IV {:?}", e); + return ServerResponse::new_error("malformed iv").into(); } }; let (res_sender, res_receiver) = oneshot::channel(); - let clients_handler_request = - ClientsHandlerRequest::Authenticate(address.clone(), token, mix_sender, res_sender); + let clients_handler_request = ClientsHandlerRequest::Authenticate( + address.clone(), + encrypted_address, + iv, + mix_sender, + res_sender, + ); self.clients_handler_sender .unbounded_send(clients_handler_request) .unwrap(); // the receiver MUST BE alive match res_receiver.await.unwrap() { - ClientsHandlerResponse::Authenticate(authenticated) => { - if authenticated { - self.address = Some(address); - } - ServerResponse::Authenticate { - status: authenticated, + ClientsHandlerResponse::Authenticate(shared_key) => { + if shared_key.is_some() { + self.remote_address = Some(address); + self.shared_key = shared_key; + ServerResponse::Authenticate { status: true } + } else { + ServerResponse::Authenticate { status: false } } } ClientsHandlerResponse::Error(e) => { @@ -207,37 +274,61 @@ where } } + fn extract_remote_identity_from_register_init(init_data: &[u8]) -> Option { + if init_data.len() < identity::PUBLIC_KEY_LENGTH { + None + } else { + identity::PublicKey::from_bytes(&init_data[..identity::PUBLIC_KEY_LENGTH]).ok() + } + } + async fn handle_register( &mut self, - address: String, + init_data: Vec, mix_sender: MixMessageSender, - ) -> ServerResponse { - let address = match DestinationAddressBytes::try_from_base58_string(address) { - Ok(address) => address, - Err(e) => { - trace!("failed to parse received DestinationAddress: {:?}", e); - return ServerResponse::new_error("malformed destination address").into(); + ) -> ServerResponse + where + S: AsyncRead + AsyncWrite + Unpin + Send, + { + // not entirely sure how to it more "nicely"... + // hopefully, eventually this will go away once client's identity is known beforehand + let remote_identity = match Self::extract_remote_identity_from_register_init(&init_data) { + Some(address) => address, + None => return ServerResponse::new_error("malformed request"), + }; + let remote_address = remote_identity.derive_address(); + + let derived_shared_key = match self.perform_registration_handshake(init_data).await { + Ok(shared_key) => shared_key, + Err(err) => { + return ServerResponse::new_error(format!( + "failed to perform the handshake - {}", + err + )) } }; let (res_sender, res_receiver) = oneshot::channel(); - let clients_handler_request = - ClientsHandlerRequest::Register(address.clone(), mix_sender, res_sender); + let clients_handler_request = ClientsHandlerRequest::Register( + remote_address.clone(), + derived_shared_key, + mix_sender, + res_sender, + ); + self.clients_handler_sender .unbounded_send(clients_handler_request) .unwrap(); // the receiver MUST BE alive match res_receiver.await.unwrap() { // currently register can't fail (as in if all machines are working correctly and you - // send valid address, you will receive a valid token) - ClientsHandlerResponse::Register(token) => { - self.address = Some(address); - ServerResponse::Register { - token: token.to_base58_string(), - } + // managed to complete registration handshake) + ClientsHandlerResponse::Register(status) => { + self.remote_address = Some(remote_address); + ServerResponse::Register { status } } ClientsHandlerResponse::Error(e) => { - error!("Authentication unexpectedly failed - {}", e); + error!("Post-handshake registration unexpectedly failed - {}", e); ServerResponse::Error { message: "unexpected failure".into(), } @@ -265,7 +356,44 @@ where } } - async fn wait_for_initial_authentication(&mut self) -> Option { + /// Handles data that resembles request to either start registration handshake or perform + /// authentication. + async fn handle_initial_authentication_request( + &mut self, + mix_sender: MixMessageSender, + raw_request: String, + ) -> ServerResponse + where + S: AsyncRead + AsyncWrite + Unpin + Send, + { + if let Ok(request) = ClientControlRequest::try_from(raw_request) { + match request { + ClientControlRequest::Authenticate { + address, + enc_address, + iv, + } => { + self.handle_authenticate(address, enc_address, iv, mix_sender) + .await + } + ClientControlRequest::RegisterHandshakeInitRequest { data } => { + self.handle_register(data, mix_sender).await + } + } + } else { + // TODO: is this a malformed request or rather a network error and + // connection should be terminated? + ServerResponse::new_error("malformed request") + } + } + + /// Listens for only a subset of possible client requests, i.e. for those that can either + /// result in client getting registered or authenticated. All other requests, such as forwarding + /// sphinx packets are ignored. + async fn wait_for_initial_authentication(&mut self) -> Option + where + S: AsyncRead + AsyncWrite + Unpin + Send, + { trace!("Started waiting for authenticate/register request..."); while let Some(msg) = self.next_websocket_request().await { @@ -287,21 +415,12 @@ where let response = match msg { Message::Close(_) => break, Message::Text(text_msg) => { - if let Ok(request) = ClientControlRequest::try_from(text_msg) { - match request { - ClientControlRequest::Authenticate { address, token } => { - self.handle_authenticate(address, token, mix_sender).await - } - ClientControlRequest::Register { address } => { - self.handle_register(address, mix_sender).await - } - } - } else { - ServerResponse::new_error("malformed request") - } + self.handle_initial_authentication_request(mix_sender, text_msg) + .await } Message::Binary(_) => { // perhaps logging level should be reduced here, let's leave it for now and see what happens + // if client is working correctly, this should have never happened warn!("possibly received a sphinx packet without prior authentication. Request is going to be ignored"); ServerResponse::new_error("binary request without prior authentication") } @@ -328,7 +447,13 @@ where None } - async fn listen_for_requests(&mut self, mut mix_receiver: MixMessageReceiver) { + /// Simultaneously listens for incoming client requests, which realistically should only be + /// binary requests to forward sphinx packets, and for sphinx packets received from the mix + /// network that should be sent back to the client. + async fn listen_for_requests(&mut self, mut mix_receiver: MixMessageReceiver) + where + S: AsyncRead + AsyncWrite + Unpin, + { trace!("Started listening for ALL incoming requests..."); loop { @@ -373,7 +498,10 @@ where trace!("The stream was closed!"); } - pub(crate) async fn start_handling(&mut self) { + pub(crate) async fn start_handling(&mut self) + where + S: AsyncRead + AsyncWrite + Unpin + Send, + { if let Err(e) = self.perform_websocket_handshake().await { warn!( "Failed to complete WebSocket handshake - {:?}. Stopping the handler", diff --git a/gateway/src/node/client_handling/websocket/listener.rs b/gateway/src/node/client_handling/websocket/listener.rs index 970333ca87..e3acdbd9b4 100644 --- a/gateway/src/node/client_handling/websocket/listener.rs +++ b/gateway/src/node/client_handling/websocket/listener.rs @@ -15,17 +15,23 @@ use crate::node::client_handling::clients_handler::ClientsHandlerRequestSender; use crate::node::client_handling::websocket::connection_handler::Handle; use crate::node::mixnet_handling::sender::OutboundMixMessageSender; +use crypto::asymmetric::identity; use log::*; use std::net::SocketAddr; +use std::sync::Arc; use tokio::task::JoinHandle; pub(crate) struct Listener { address: SocketAddr, + local_identity: Arc, } impl Listener { - pub(crate) fn new(address: SocketAddr) -> Self { - Listener { address } + pub(crate) fn new(address: SocketAddr, local_identity: Arc) -> Self { + Listener { + address, + local_identity, + } } pub(crate) async fn run( @@ -42,12 +48,13 @@ impl Listener { match tcp_listener.accept().await { Ok((socket, remote_addr)) => { trace!("received a socket connection from {}", remote_addr); - // TODO: I think we need a mechanism for having a maximum number of connected + // TODO: I think we *REALLY* need a mechanism for having a maximum number of connected // clients or spawned tokio tasks -> perhaps a worker system? let mut handle = Handle::new( socket, clients_handler_sender.clone(), outbound_mix_sender.clone(), + Arc::clone(&self.local_identity), ); tokio::spawn(async move { handle.start_handling().await }); } diff --git a/gateway/src/node/mixnet_handling/receiver/packet_processing.rs b/gateway/src/node/mixnet_handling/receiver/packet_processing.rs index 642c4bc975..59ca2c5563 100644 --- a/gateway/src/node/mixnet_handling/receiver/packet_processing.rs +++ b/gateway/src/node/mixnet_handling/receiver/packet_processing.rs @@ -18,18 +18,16 @@ use crate::node::client_handling::clients_handler::{ use crate::node::client_handling::websocket::message_receiver::MixMessageSender; use crate::node::mixnet_handling::sender::OutboundMixMessageSender; use crate::node::storage::inboxes::{ClientStorage, StoreData}; -use crypto::encryption; +use crypto::asymmetric::encryption; use futures::channel::oneshot; use futures::lock::Mutex; use log::*; use nymsphinx::acknowledgements::surb_ack::{SURBAck, SURBAckRecoveryError}; -use nymsphinx::cover::LOOP_COVER_MESSAGE_PAYLOAD; use nymsphinx::params::packet_sizes::PacketSize; use nymsphinx::{DestinationAddressBytes, Error as SphinxError, ProcessedPacket, SphinxPacket}; use std::collections::HashMap; use std::io; -use std::ops::Deref; use std::sync::Arc; #[derive(Debug)] @@ -70,7 +68,7 @@ impl From for MixProcessingError { // PacketProcessor contains all data required to correctly unwrap and store sphinx packets #[derive(Clone)] pub struct PacketProcessor { - secret_key: Arc, + encryption_keys: Arc, // TODO: later investigate some concurrent hashmap solutions or perhaps RWLocks. // Right now Mutex is the simplest and fastest to implement approach available_socket_senders_cache: Arc>>, @@ -81,7 +79,7 @@ pub struct PacketProcessor { impl PacketProcessor { pub(crate) fn new( - secret_key: Arc, + encryption_keys: Arc, clients_handler_sender: ClientsHandlerRequestSender, client_store: ClientStorage, ack_sender: OutboundMixMessageSender, @@ -90,7 +88,7 @@ impl PacketProcessor { available_socket_senders_cache: Arc::new(Mutex::new(HashMap::new())), clients_handler_sender, client_store, - secret_key, + encryption_keys, ack_sender, } } @@ -184,7 +182,7 @@ impl PacketProcessor { &self, packet: SphinxPacket, ) -> Result<(DestinationAddressBytes, Vec), MixProcessingError> { - match packet.process(self.secret_key.deref().inner()) { + match packet.process(&self.encryption_keys.as_ref().private_key().into()) { Ok(ProcessedPacket::ProcessedPacketForwardHop(_, _, _)) => { warn!("Received a forward hop message - those are not implemented for gateways"); Err(MixProcessingError::ReceivedForwardHopError) diff --git a/gateway/src/node/mod.rs b/gateway/src/node/mod.rs index fc1bfe8c85..69b0269245 100644 --- a/gateway/src/node/mod.rs +++ b/gateway/src/node/mod.rs @@ -17,7 +17,7 @@ use crate::node::client_handling::clients_handler::{ClientsHandler, ClientsHandl use crate::node::client_handling::websocket; use crate::node::mixnet_handling::sender::{OutboundMixMessageSender, PacketForwarder}; use crate::node::storage::{inboxes, ClientLedger}; -use crypto::encryption; +use crypto::asymmetric::{encryption, identity}; use directory_client::DirectoryClient; use log::*; use std::sync::Arc; @@ -28,27 +28,22 @@ pub(crate) mod mixnet_handling; mod presence; pub(crate) mod storage; -// current issues in this file: -// - two calls to `Arc::new(self.sphinx_keypair.private_key().clone()),` - basically 2 separate -// Arcs to the same underlying data (well, after a clone), so what it ends up resulting in is -// private key being in 3 different places in memory rather than in a single location. -// Does it affect performance? No, not really. Is it *SUPER* insecure? Also, not as much, because -// if somebody could read memory of the machine, they probably got better attack vectors. -// Should it get fixed? Probably. But it's very low priority for time being. - pub struct Gateway { config: Config, - sphinx_keypair: encryption::KeyPair, + /// ed25519 keypair used to assert one's identity. + identity: Arc, + /// x25519 keypair used for Diffie-Hellman. Currently only used for sphinx key derivation. + encryption_keys: Arc, registered_clients_ledger: ClientLedger, client_inbox_storage: inboxes::ClientStorage, } impl Gateway { - // the constructor differs from mixnodes and providers in that it takes keys directly - // as opposed to having `Self::load_sphinx_keys(cfg: &Config)` method. Let's see - // how it works out, because I'm not sure which one would be "better", but when I think about it, - // I kinda prefer to delegate having to load the keys to outside the gateway - pub fn new(config: Config, sphinx_keypair: encryption::KeyPair) -> Self { + pub fn new( + config: Config, + encryption_keys: encryption::KeyPair, + identity: identity::KeyPair, + ) -> Self { let registered_clients_ledger = match ClientLedger::load(config.get_clients_ledger_path()) { Err(e) => panic!(format!("Failed to load the ledger - {:?}", e)), Ok(ledger) => ledger, @@ -60,7 +55,8 @@ impl Gateway { ); Gateway { config, - sphinx_keypair, + identity: Arc::new(identity), + encryption_keys: Arc::new(encryption_keys), client_inbox_storage, registered_clients_ledger, } @@ -74,7 +70,7 @@ impl Gateway { info!("Starting mix socket listener..."); let packet_processor = mixnet_handling::PacketProcessor::new( - Arc::new(self.sphinx_keypair.private_key().clone()), + Arc::clone(&self.encryption_keys), clients_handler_sender, self.client_inbox_storage.clone(), ack_sender, @@ -91,8 +87,11 @@ impl Gateway { ) { info!("Starting client [web]socket listener..."); - websocket::Listener::new(self.config.get_clients_listening_address()) - .start(clients_handler_sender, forwarding_channel); + websocket::Listener::new( + self.config.get_clients_listening_address(), + Arc::clone(&self.identity), + ) + .start(clients_handler_sender, forwarding_channel); } fn start_packet_forwarder(&self) -> OutboundMixMessageSender { @@ -110,7 +109,6 @@ impl Gateway { fn start_clients_handler(&self) -> ClientsHandlerRequestSender { info!("Starting clients handler"); let (_, clients_handler_sender) = ClientsHandler::new( - Arc::new(self.sphinx_keypair.private_key().clone()), self.registered_clients_ledger.clone(), self.client_inbox_storage.clone(), ) @@ -125,7 +123,8 @@ impl Gateway { self.config.get_presence_directory_server(), self.config.get_mix_announce_address(), self.config.get_clients_announce_address(), - self.sphinx_keypair.public_key().to_base58_string(), + self.identity.public_key().to_base58_string(), + self.encryption_keys.public_key().to_base58_string(), self.config.get_presence_sending_delay(), ); presence::Notifier::new(notifier_config, self.registered_clients_ledger.clone()).start(); @@ -160,7 +159,7 @@ impl Gateway { node.mixnet_listener == announced_mix_host || node.client_listener == announced_clients_host }) - .map(|node| node.pub_key.clone()) + .map(|node| node.identity_key.clone()) } // Rather than starting all futures with explicit `&Handle` argument, let's see how it works diff --git a/gateway/src/node/presence/mod.rs b/gateway/src/node/presence/mod.rs index ca0eeeb125..1173f393c7 100644 --- a/gateway/src/node/presence/mod.rs +++ b/gateway/src/node/presence/mod.rs @@ -25,7 +25,8 @@ pub(crate) struct NotifierConfig { directory_server: String, mix_announce_host: String, clients_announce_host: String, - pub_key_string: String, + identity_string: String, + sphinx_key_string: String, sending_delay: Duration, } @@ -35,7 +36,8 @@ impl NotifierConfig { directory_server: String, mix_announce_host: String, clients_announce_host: String, - pub_key_string: String, + identity_string: String, + sphinx_key_string: String, sending_delay: Duration, ) -> Self { NotifierConfig { @@ -43,7 +45,8 @@ impl NotifierConfig { directory_server, mix_announce_host, clients_announce_host, - pub_key_string, + identity_string, + sphinx_key_string, sending_delay, } } @@ -56,7 +59,8 @@ pub(crate) struct Notifier { sending_delay: Duration, client_listener: String, mixnet_listener: String, - pub_key_string: String, + identity: String, + sphinx_key: String, } impl Notifier { @@ -72,7 +76,8 @@ impl Notifier { location: config.location, client_listener: config.clients_announce_host, mixnet_listener: config.mix_announce_host, - pub_key_string: config.pub_key_string, + identity: config.identity_string, + sphinx_key: config.sphinx_key_string, sending_delay: config.sending_delay, } } @@ -90,7 +95,8 @@ impl Notifier { location: self.location.clone(), client_listener: self.client_listener.clone(), mixnet_listener: self.mixnet_listener.clone(), - pub_key: self.pub_key_string.clone(), + identity_key: self.identity.clone(), + sphinx_key: self.sphinx_key.clone(), registered_clients, last_seen: 0, version: built_info::PKG_VERSION.to_string(), diff --git a/gateway/src/node/storage/ledger.rs b/gateway/src/node/storage/ledger.rs index 1aa9e4ab30..9da2d5beff 100644 --- a/gateway/src/node/storage/ledger.rs +++ b/gateway/src/node/storage/ledger.rs @@ -18,12 +18,13 @@ //use sfw_provider_requests::auth_token::{AuthToken, AUTH_TOKEN_SIZE}; //use std::path::PathBuf; -use std::path::PathBuf; - +use gateway_requests::authentication::encrypted_address::EncryptedAddressBytes; +use gateway_requests::authentication::iv::AuthenticationIV; +use gateway_requests::generic_array::typenum::Unsigned; +use gateway_requests::registration::handshake::{SharedKey, SharedKeySize}; use log::*; - -use gateway_requests::auth_token::{AuthToken, AUTH_TOKEN_SIZE}; use nymsphinx::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH}; +use std::path::PathBuf; #[derive(Debug)] pub(crate) enum ClientLedgerError { @@ -60,18 +61,18 @@ impl ClientLedger { Ok(ledger) } - fn read_auth_token(&self, raw_token: sled::IVec) -> AuthToken { - let token_bytes_ref = raw_token.as_ref(); + fn read_shared_key(&self, raw_key: sled::IVec) -> SharedKey { + let key_bytes_ref = raw_key.as_ref(); // if this fails it means we have some database corruption and we // absolutely can't continue - if token_bytes_ref.len() != AUTH_TOKEN_SIZE { - error!("CLIENT LEDGER DATA CORRUPTION - TOKEN HAS INVALID LENGTH"); - panic!("CLIENT LEDGER DATA CORRUPTION - TOKEN HAS INVALID LENGTH"); + + if key_bytes_ref.len() != SharedKeySize::to_usize() { + error!("CLIENT LEDGER DATA CORRUPTION - SHARED KEY HAS INVALID LENGTH"); + panic!("CLIENT LEDGER DATA CORRUPTION - SHARED KEY HAS INVALID LENGTH"); } - let mut token_bytes = [0u8; AUTH_TOKEN_SIZE]; - token_bytes.copy_from_slice(token_bytes_ref); - AuthToken::from_bytes(token_bytes) + // this can only fail if the bytes have invalid length but we already asserted it + SharedKey::try_from_bytes(key_bytes_ref).unwrap() } fn read_destination_address_bytes( @@ -91,32 +92,46 @@ impl ClientLedger { DestinationAddressBytes::from_bytes(destination_bytes) } - pub(crate) fn verify_token( + pub(crate) fn verify_shared_key( &self, - auth_token: &AuthToken, client_address: &DestinationAddressBytes, + encrypted_address: &EncryptedAddressBytes, + iv: &AuthenticationIV, ) -> Result { match self.db.get(&client_address.to_bytes()) { Err(e) => Err(ClientLedgerError::DbReadError(e)), - Ok(token) => match token { - Some(token_ivec) => Ok(&self.read_auth_token(token_ivec) == auth_token), + Ok(existing_key) => match existing_key { + Some(existing_key_ivec) => { + let shared_key = &self.read_shared_key(existing_key_ivec); + Ok(encrypted_address.verify(client_address, shared_key, iv)) + } None => Ok(false), }, } } - pub(crate) fn insert_token( + pub(crate) fn get_shared_key( + &self, + client_address: &DestinationAddressBytes, + ) -> Result, ClientLedgerError> { + match self.db.get(&client_address.to_bytes()) { + Err(e) => Err(ClientLedgerError::DbReadError(e)), + Ok(existing_key) => Ok(existing_key.map(|key_ivec| self.read_shared_key(key_ivec))), + } + } + + pub(crate) fn insert_shared_key( &mut self, - auth_token: AuthToken, + shared_key: SharedKey, client_address: DestinationAddressBytes, - ) -> Result, ClientLedgerError> { + ) -> Result, ClientLedgerError> { let insertion_result = match self .db - .insert(&client_address.to_bytes(), &auth_token.to_bytes()) + .insert(&client_address.to_bytes(), shared_key.to_bytes()) { Err(e) => Err(ClientLedgerError::DbWriteError(e)), - Ok(existing_token) => { - Ok(existing_token.map(|existing_token| self.read_auth_token(existing_token))) + Ok(existing_key) => { + Ok(existing_key.map(|existing_key| self.read_shared_key(existing_key))) } }; @@ -125,18 +140,18 @@ impl ClientLedger { insertion_result } - pub(crate) fn remove_token( + pub(crate) fn remove_shared_key( &mut self, client_address: &DestinationAddressBytes, - ) -> Result, ClientLedgerError> { + ) -> Result, ClientLedgerError> { let removal_result = match self.db.remove(&client_address.to_bytes()) { Err(e) => Err(ClientLedgerError::DbWriteError(e)), - Ok(existing_token) => { - Ok(existing_token.map(|existing_token| self.read_auth_token(existing_token))) + Ok(existing_key) => { + Ok(existing_key.map(|existing_key| self.read_shared_key(existing_key))) } }; - // removing of tokens happens extremely rarely, so flush is also fine here + // removal of keys happens extremely rarely, so flush is also fine here self.db.flush().unwrap(); removal_result } diff --git a/mixnode/src/commands/init.rs b/mixnode/src/commands/init.rs index 02531e5472..9a760fb4fd 100644 --- a/mixnode/src/commands/init.rs +++ b/mixnode/src/commands/init.rs @@ -16,7 +16,7 @@ use crate::commands::override_config; use crate::config::persistence::pathfinder::MixNodePathfinder; use clap::{App, Arg, ArgMatches}; use config::NymConfig; -use crypto::encryption; +use crypto::asymmetric::encryption; use pemstore::pemstore::PemStore; pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { @@ -88,7 +88,7 @@ pub fn execute(matches: &ArgMatches) { let pathfinder = MixNodePathfinder::new_from_config(&config); let pem_store = PemStore::new(pathfinder); pem_store - .write_encryption_keys(sphinx_keys) + .write_encryption_keypair(&sphinx_keys) .expect("Failed to save sphinx keys"); println!("Saved mixnet sphinx keypair"); diff --git a/mixnode/src/commands/run.rs b/mixnode/src/commands/run.rs index 14f2e58532..029556f15a 100644 --- a/mixnode/src/commands/run.rs +++ b/mixnode/src/commands/run.rs @@ -17,7 +17,7 @@ use crate::config::{persistence::pathfinder::MixNodePathfinder, Config}; use crate::node::MixNode; use clap::{App, Arg, ArgMatches}; use config::NymConfig; -use crypto::encryption; +use crypto::asymmetric::encryption; use pemstore::pemstore::PemStore; pub fn command_args<'a, 'b>() -> App<'a, 'b> { @@ -98,7 +98,7 @@ fn special_addresses() -> Vec<&'static str> { fn load_sphinx_keys(config_file: &Config) -> encryption::KeyPair { let sphinx_keypair = PemStore::new(MixNodePathfinder::new_from_config(&config_file)) - .read_encryption() + .read_encryption_keypair() .expect("Failed to read stored sphinx key files"); println!( "Public key: {}\n", diff --git a/mixnode/src/node/mod.rs b/mixnode/src/node/mod.rs index 0338602719..a9010460d5 100644 --- a/mixnode/src/node/mod.rs +++ b/mixnode/src/node/mod.rs @@ -14,7 +14,7 @@ use crate::config::Config; use crate::node::packet_processing::PacketProcessor; -use crypto::encryption; +use crypto::asymmetric::encryption; use directory_client::DirectoryClient; use futures::channel::mpsc; use log::*; diff --git a/mixnode/src/node/packet_processing.rs b/mixnode/src/node/packet_processing.rs index e859f4001f..610781c77c 100644 --- a/mixnode/src/node/packet_processing.rs +++ b/mixnode/src/node/packet_processing.rs @@ -13,7 +13,7 @@ // limitations under the License. use crate::node::metrics; -use crypto::encryption; +use crypto::asymmetric::encryption; use log::*; use nymsphinx::addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError}; use nymsphinx::{ @@ -21,7 +21,6 @@ use nymsphinx::{ }; use std::convert::TryFrom; use std::net::SocketAddr; -use std::ops::Deref; use std::sync::Arc; #[derive(Debug)] @@ -96,8 +95,7 @@ impl PacketProcessor { ) -> Result { // we received something resembling a sphinx packet, report it! self.metrics_reporter.report_received(); - - match packet.process(self.secret_key.deref().inner()) { + match packet.process(&self.secret_key.as_ref().into()) { Ok(ProcessedPacket::ProcessedPacketForwardHop(packet, address, delay)) => { self.process_forward_hop(packet, address, delay).await } From a53d0a4aacc1513cbcaaa095711cd909ed2327c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Tue, 7 Jul 2020 10:56:50 +0100 Subject: [PATCH 13/76] Feature/topology refactor (#274) * VersionFilterable for HashMap * Removed NymTopology trait in favour of concrete type * Removed providers from NymTopology * Made gateway conversion use reference, similarly to mixes * Using more concrete types in topology rather than b58 strings * Allowing gateways to have DNS-resolvable mix listener address * Error propagation for gateway key conversion --- Cargo.lock | 12 +- .../native/src/client/cover_traffic_stream.rs | 13 +- clients/native/src/client/mod.rs | 24 +- .../input_message_listener.rs | 11 +- .../acknowlegement_control/mod.rs | 13 +- .../retransmission_request_listener.rs | 11 +- .../src/client/real_messages_control/mod.rs | 12 +- .../real_traffic_stream.rs | 14 +- clients/native/src/client/topology_control.rs | 99 +++--- clients/native/src/commands/init.rs | 38 +-- clients/native/src/websocket/handler.rs | 13 +- clients/native/src/websocket/listener.rs | 9 +- clients/webassembly/Cargo.toml | 2 +- clients/webassembly/src/lib.rs | 88 +++--- clients/webassembly/src/models/keys.rs | 2 +- clients/webassembly/src/models/topology.rs | 36 +-- .../directory-client/models/Cargo.toml | 1 + .../models/src/presence/coconodes.rs | 38 +-- .../models/src/presence/gateways.rs | 82 ++--- .../models/src/presence/mixnodes.rs | 39 ++- .../models/src/presence/mod.rs | 2 +- .../models/src/presence/providers.rs | 53 ---- .../models/src/presence/topology.rs | 98 +++--- common/client-libs/gateway-client/src/lib.rs | 6 +- .../crypto/src/asymmetric/encryption/mod.rs | 6 + common/crypto/src/asymmetric/identity/mod.rs | 18 +- .../acknowledgements/src/surb_ack.rs | 9 +- common/nymsphinx/chunking/src/lib.rs | 25 +- common/nymsphinx/cover/src/lib.rs | 14 +- common/nymsphinx/params/src/lib.rs | 4 + common/topology/Cargo.toml | 8 +- common/topology/src/coco.rs | 3 +- common/topology/src/filter.rs | 21 +- common/topology/src/gateway.rs | 18 +- common/topology/src/lib.rs | 293 +++++++++--------- common/topology/src/mix.rs | 17 +- common/topology/src/provider.rs | 61 ---- .../websocket/connection_handler.rs | 2 +- 38 files changed, 558 insertions(+), 657 deletions(-) delete mode 100644 common/topology/src/provider.rs diff --git a/Cargo.lock b/Cargo.lock index 9a9cb2f1c2..a7b7fba1fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -693,6 +693,7 @@ dependencies = [ name = "directory-client-models" version = "0.1.0" dependencies = [ + "crypto", "serde", "topology", ] @@ -1414,15 +1415,6 @@ dependencies = [ "url 1.7.2", ] -[[package]] -name = "itertools" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "0.4.5" @@ -3337,7 +3329,7 @@ name = "topology" version = "0.1.0" dependencies = [ "bs58", - "itertools", + "crypto", "log 0.4.8", "nymsphinx-addressing", "nymsphinx-types", diff --git a/clients/native/src/client/cover_traffic_stream.rs b/clients/native/src/client/cover_traffic_stream.rs index 4c3fe3b9fb..2cd75b6376 100644 --- a/clients/native/src/client/cover_traffic_stream.rs +++ b/clients/native/src/client/cover_traffic_stream.rs @@ -27,12 +27,10 @@ use std::sync::Arc; use tokio::runtime::Handle; use tokio::task::JoinHandle; use tokio::time; -use topology::NymTopology; -pub(crate) struct LoopCoverTrafficStream +pub(crate) struct LoopCoverTrafficStream where R: CryptoRng + Rng, - T: NymTopology, { /// Key used to encrypt and decrypt content of an ACK packet. ack_key: Arc, @@ -61,13 +59,12 @@ where rng: R, /// Accessor to the common instance of network topology. - topology_access: TopologyAccessor, + topology_access: TopologyAccessor, } -impl Stream for LoopCoverTrafficStream +impl Stream for LoopCoverTrafficStream where R: CryptoRng + Rng + Unpin, - T: NymTopology, // this really confuses me, why T doesn't need to be Unpin? { // Item is only used to indicate we should create a new message rather than actual cover message // reason being to not introduce unnecessary complexity by having to keep state of topology @@ -98,7 +95,7 @@ where // obviously when we finally make shared rng that is on 'higher' level, this should become // generic `R` -impl LoopCoverTrafficStream { +impl LoopCoverTrafficStream { pub(crate) fn new( ack_key: Arc, average_ack_delay: time::Duration, @@ -106,7 +103,7 @@ impl LoopCoverTrafficStream { average_cover_message_sending_delay: time::Duration, mix_tx: MixMessageSender, our_full_destination: Recipient, - topology_access: TopologyAccessor, + topology_access: TopologyAccessor, ) -> Self { let rng = OsRng; diff --git a/clients/native/src/client/mod.rs b/clients/native/src/client/mod.rs index 6e73884bbd..743bc59b07 100644 --- a/clients/native/src/client/mod.rs +++ b/clients/native/src/client/mod.rs @@ -38,7 +38,6 @@ use nymsphinx::NodeAddressBytes; use received_buffer::{ReceivedBufferMessage, ReconstructedMessagesReceiver}; use std::sync::Arc; use tokio::runtime::Runtime; -use topology::NymTopology; mod cover_traffic_stream; pub(crate) mod inbound_messages; @@ -72,7 +71,9 @@ impl NymClient { pub fn as_mix_recipient(&self) -> Recipient { Recipient::new( - self.identity_keypair.public_key().derive_address(), + self.identity_keypair + .public_key() + .derive_destination_address(), // TODO: below only works under assumption that gateway address == gateway id // (which currently is true) NodeAddressBytes::try_from_base58_string(self.config.get_gateway_id()).unwrap(), @@ -81,10 +82,10 @@ impl NymClient { // future constantly pumping loop cover traffic at some specified average rate // the pumped traffic goes to the MixTrafficController - fn start_cover_traffic_stream( + fn start_cover_traffic_stream( &self, ack_key: Arc, - topology_accessor: TopologyAccessor, + topology_accessor: TopologyAccessor, mix_tx: MixMessageSender, ) { info!("Starting loop cover traffic stream..."); @@ -108,9 +109,9 @@ impl NymClient { // TODO: I'm not a fan of this function signature, i.e. that it returns the ACK key, but then again // I think the ACK key should be generated by 'acknowledgement_control' rather than by // the client itself. However, it all might change once we have to implement key rotation. - fn start_real_traffic_controller( + fn start_real_traffic_controller( &self, - topology_accessor: TopologyAccessor, + topology_accessor: TopologyAccessor, ack_receiver: AcknowledgementReceiver, input_receiver: InputMessageReceiver, mix_sender: MixMessageSender, @@ -210,10 +211,7 @@ impl NymClient { // future responsible for periodically polling directory server and updating // the current global view of topology - fn start_topology_refresher( - &mut self, - topology_accessor: TopologyAccessor, - ) { + fn start_topology_refresher(&mut self, topology_accessor: TopologyAccessor) { let topology_refresher_config = TopologyRefresherConfig::new( self.config.get_directory_server(), self.config.get_topology_refresh_rate(), @@ -256,9 +254,9 @@ impl NymClient { MixTrafficController::new(mix_rx, gateway_client).start(self.runtime.handle()); } - fn start_websocket_listener( + fn start_websocket_listener( &self, - topology_accessor: TopologyAccessor, + topology_accessor: TopologyAccessor, buffer_requester: ReceivedBufferRequestSender, msg_input: InputMessageSender, ) { @@ -343,7 +341,7 @@ impl NymClient { // channels responsible for controlling ack messages let (ack_sender, ack_receiver) = mpsc::unbounded(); - let shared_topology_accessor = TopologyAccessor::::new(); + let shared_topology_accessor = TopologyAccessor::new(); // the components are started in very specific order. Unless you know what you are doing, // do not change that. diff --git a/clients/native/src/client/real_messages_control/acknowlegement_control/input_message_listener.rs b/clients/native/src/client/real_messages_control/acknowlegement_control/input_message_listener.rs index f0b93eba82..71b577bea8 100644 --- a/clients/native/src/client/real_messages_control/acknowlegement_control/input_message_listener.rs +++ b/clients/native/src/client/real_messages_control/acknowlegement_control/input_message_listener.rs @@ -25,13 +25,11 @@ use nymsphinx::{ }; use rand::{CryptoRng, Rng}; use std::sync::Arc; -use topology::NymTopology; // responsible for splitting received message and initial sending attempt -pub(super) struct InputMessageListener +pub(super) struct InputMessageListener where R: CryptoRng + Rng, - T: NymTopology, { ack_key: Arc, ack_recipient: Recipient, @@ -39,13 +37,12 @@ where message_chunker: MessageChunker, pending_acks: PendingAcksMap, real_message_sender: RealMessageSender, - topology_access: TopologyAccessor, + topology_access: TopologyAccessor, } -impl InputMessageListener +impl InputMessageListener where R: CryptoRng + Rng, - T: NymTopology, { pub(super) fn new( ack_key: Arc, @@ -54,7 +51,7 @@ where message_chunker: MessageChunker, pending_acks: PendingAcksMap, real_message_sender: RealMessageSender, - topology_access: TopologyAccessor, + topology_access: TopologyAccessor, ) -> Self { InputMessageListener { ack_key, diff --git a/clients/native/src/client/real_messages_control/acknowlegement_control/mod.rs b/clients/native/src/client/real_messages_control/acknowlegement_control/mod.rs index 1af48d9792..6c34d114b3 100644 --- a/clients/native/src/client/real_messages_control/acknowlegement_control/mod.rs +++ b/clients/native/src/client/real_messages_control/acknowlegement_control/mod.rs @@ -38,7 +38,6 @@ use tokio::{ sync::{Notify, RwLock}, task::JoinHandle, }; -use topology::NymTopology; mod acknowledgement_listener; mod input_message_listener; @@ -98,26 +97,24 @@ impl AcknowledgementControllerConnectors { } } -pub(super) struct AcknowledgementController +pub(super) struct AcknowledgementController where R: CryptoRng + Rng, - T: NymTopology, { ack_key: Arc, acknowledgement_listener: Option, - input_message_listener: Option>, - retransmission_request_listener: Option>, + input_message_listener: Option>, + retransmission_request_listener: Option>, sent_notification_listener: Option, } -impl AcknowledgementController +impl AcknowledgementController where R: 'static + CryptoRng + Rng + Clone + Send, - T: 'static + NymTopology, { pub(super) fn new( mut rng: R, - topology_access: TopologyAccessor, + topology_access: TopologyAccessor, ack_recipient: Recipient, average_packet_delay_duration: Duration, average_ack_delay_duration: Duration, diff --git a/clients/native/src/client/real_messages_control/acknowlegement_control/retransmission_request_listener.rs b/clients/native/src/client/real_messages_control/acknowlegement_control/retransmission_request_listener.rs index 940467f1ba..c2cff8839f 100644 --- a/clients/native/src/client/real_messages_control/acknowlegement_control/retransmission_request_listener.rs +++ b/clients/native/src/client/real_messages_control/acknowlegement_control/retransmission_request_listener.rs @@ -26,13 +26,11 @@ use nymsphinx::{ }; use rand::{CryptoRng, Rng}; use std::sync::Arc; -use topology::NymTopology; // responsible for packet retransmission upon fired timer -pub(super) struct RetransmissionRequestListener +pub(super) struct RetransmissionRequestListener where R: CryptoRng + Rng, - T: NymTopology, { ack_key: Arc, ack_recipient: Recipient, @@ -40,13 +38,12 @@ where pending_acks: PendingAcksMap, real_message_sender: RealMessageSender, request_receiver: RetransmissionRequestReceiver, - topology_access: TopologyAccessor, + topology_access: TopologyAccessor, } -impl RetransmissionRequestListener +impl RetransmissionRequestListener where R: CryptoRng + Rng, - T: NymTopology, { pub(super) fn new( ack_key: Arc, @@ -55,7 +52,7 @@ where pending_acks: PendingAcksMap, real_message_sender: RealMessageSender, request_receiver: RetransmissionRequestReceiver, - topology_access: TopologyAccessor, + topology_access: TopologyAccessor, ) -> Self { RetransmissionRequestListener { ack_key, diff --git a/clients/native/src/client/real_messages_control/mod.rs b/clients/native/src/client/real_messages_control/mod.rs index 83be7007a3..f53f6d4908 100644 --- a/clients/native/src/client/real_messages_control/mod.rs +++ b/clients/native/src/client/real_messages_control/mod.rs @@ -34,7 +34,6 @@ use std::sync::Arc; use std::time::Duration; use tokio::runtime::Handle; use tokio::task::JoinHandle; -use topology::NymTopology; mod acknowlegement_control; mod real_traffic_stream; @@ -68,24 +67,23 @@ impl Config { } } -pub(crate) struct RealMessagesController +pub(crate) struct RealMessagesController where R: CryptoRng + Rng, - T: NymTopology, { - out_queue_control: Option>, - ack_control: Option>, + out_queue_control: Option>, + ack_control: Option>, } // obviously when we finally make shared rng that is on 'higher' level, this should become // generic `R` -impl RealMessagesController { +impl RealMessagesController { pub(crate) fn new( config: Config, ack_receiver: AcknowledgementReceiver, input_receiver: InputMessageReceiver, mix_sender: MixMessageSender, - topology_access: TopologyAccessor, + topology_access: TopologyAccessor, ) -> Self { let rng = OsRng; diff --git a/clients/native/src/client/real_messages_control/real_traffic_stream.rs b/clients/native/src/client/real_messages_control/real_traffic_stream.rs index 90b9c05b47..57429e10e5 100644 --- a/clients/native/src/client/real_messages_control/real_traffic_stream.rs +++ b/clients/native/src/client/real_messages_control/real_traffic_stream.rs @@ -31,12 +31,10 @@ use std::pin::Pin; use std::sync::Arc; use std::time::Duration; use tokio::time; -use topology::NymTopology; -pub(crate) struct OutQueueControl +pub(crate) struct OutQueueControl where R: CryptoRng + Rng, - T: NymTopology, { /// Key used to encrypt and decrypt content of an ACK packet. ack_key: Arc, @@ -72,7 +70,7 @@ where rng: R, /// Accessor to the common instance of network topology. - topology_access: TopologyAccessor, + topology_access: TopologyAccessor, } pub(crate) struct RealMessage { @@ -105,10 +103,9 @@ pub(crate) enum StreamMessage { Real(RealMessage), } -impl Stream for OutQueueControl +impl Stream for OutQueueControl where R: CryptoRng + Rng + Unpin, - T: NymTopology, // this really confuses me, why T doesn't need to be Unpin? { type Item = StreamMessage; @@ -144,10 +141,9 @@ where } } -impl OutQueueControl +impl OutQueueControl where R: CryptoRng + Rng + Unpin, - T: NymTopology, { pub(crate) fn new( ack_key: Arc, @@ -159,7 +155,7 @@ where real_receiver: RealMessageReceiver, rng: R, our_full_destination: Recipient, - topology_access: TopologyAccessor, + topology_access: TopologyAccessor, ) -> Self { OutQueueControl { ack_key, diff --git a/clients/native/src/client/topology_control.rs b/clients/native/src/client/topology_control.rs index 0a5b6fb8eb..f3160fae8e 100644 --- a/clients/native/src/client/topology_control.rs +++ b/clients/native/src/client/topology_control.rs @@ -16,6 +16,8 @@ use crate::built_info; use directory_client::DirectoryClient; use log::*; use nymsphinx::addressing::clients::Recipient; +use nymsphinx::params::DEFAULT_NUM_MIX_HOPS; +use std::convert::TryInto; use std::ops::Deref; use std::sync::Arc; use std::time; @@ -27,66 +29,65 @@ use topology::{gateway, NymTopology}; // I'm extremely curious why compiler NEVER complained about lack of Debug here before #[derive(Debug)] -struct TopologyAccessorInner(Option); +pub(super) struct TopologyAccessorInner(Option); -impl Deref for TopologyAccessorInner { - type Target = Option; - - fn deref(&self) -> &Self::Target { +impl AsRef> for TopologyAccessorInner { + fn as_ref(&self) -> &Option { &self.0 } } -impl TopologyAccessorInner { +impl TopologyAccessorInner { fn new() -> Self { TopologyAccessorInner(None) } - fn update(&mut self, new: Option) { + fn update(&mut self, new: Option) { self.0 = new; } } -pub(super) struct TopologyReadPermit<'a, T: NymTopology> { - permit: RwLockReadGuard<'a, TopologyAccessorInner>, +pub(super) struct TopologyReadPermit<'a> { + permit: RwLockReadGuard<'a, TopologyAccessorInner>, } -impl<'a, T: NymTopology> TopologyReadPermit<'a, T> { +impl<'a> Deref for TopologyReadPermit<'a> { + type Target = TopologyAccessorInner; + + fn deref(&self) -> &Self::Target { + &self.permit + } +} + +impl<'a> TopologyReadPermit<'a> { /// Using provided topology read permit, tries to get an immutable reference to the underlying /// topology. For obvious reasons the lifetime of the topology reference is bound to the permit. pub(super) fn try_get_valid_topology_ref( &'a self, ack_recipient: &Recipient, packet_recipient: &Recipient, - ) -> Option<&'a T> { - // first we need to deref out of RwLockReadGuard - // then we need to deref out of TopologyAccessorInner - // then we must take ref of option, i.e. Option<&T> - // and finally try to unwrap it to obtain &T - let topology_ref_option = (*self.permit.deref()).as_ref(); - - if topology_ref_option.is_none() { - return None; - } - - let topology_ref = topology_ref_option.unwrap(); - - // see if it's possible to route the packet to both gateways - if !topology_ref.can_construct_path_through() - || !topology_ref.gateway_exists(&packet_recipient.gateway()) - || !topology_ref.gateway_exists(&ack_recipient.gateway()) - { - None - } else { - Some(topology_ref) + ) -> Option<&'a NymTopology> { + // Note: implicit deref with Deref for TopologyReadPermit is happening here + let topology_ref_option = self.permit.as_ref(); + match topology_ref_option { + None => None, + Some(topology_ref) => { + // see if it's possible to route the packet to both gateways + if !topology_ref.can_construct_path_through(DEFAULT_NUM_MIX_HOPS) + || !topology_ref.gateway_exists(&packet_recipient.gateway()) + || !topology_ref.gateway_exists(&ack_recipient.gateway()) + { + None + } else { + Some(topology_ref) + } + } } } } -impl<'a, T: NymTopology> From>> - for TopologyReadPermit<'a, T> -{ - fn from(read_permit: RwLockReadGuard<'a, TopologyAccessorInner>) -> Self { +impl<'a> From> for TopologyReadPermit<'a> { + fn from(read_permit: RwLockReadGuard<'a, TopologyAccessorInner>) -> Self { TopologyReadPermit { permit: read_permit, } @@ -94,26 +95,26 @@ impl<'a, T: NymTopology> From>> } #[derive(Clone, Debug)] -pub(crate) struct TopologyAccessor { +pub(crate) struct TopologyAccessor { // `RwLock` *seems to* be the better approach for this as write access is only requested every // few seconds, while reads are needed every single packet generated. // However, proper benchmarks will be needed to determine if `RwLock` is indeed a better // approach than a `Mutex` - inner: Arc>>, + inner: Arc>, } -impl TopologyAccessor { +impl TopologyAccessor { pub(crate) fn new() -> Self { TopologyAccessor { inner: Arc::new(RwLock::new(TopologyAccessorInner::new())), } } - pub(super) async fn get_read_permit(&self) -> TopologyReadPermit<'_, T> { + pub(super) async fn get_read_permit(&self) -> TopologyReadPermit<'_> { self.inner.read().await.into() } - async fn update_global_topology(&mut self, new_topology: Option) { + async fn update_global_topology(&mut self, new_topology: Option) { self.inner.write().await.update(new_topology); } @@ -133,7 +134,7 @@ impl TopologyAccessor { pub(crate) async fn is_routable(&self) -> bool { match &self.inner.read().await.0 { None => false, - Some(ref topology) => topology.can_construct_path_through(), + Some(ref topology) => topology.can_construct_path_through(DEFAULT_NUM_MIX_HOPS), } } @@ -167,17 +168,16 @@ impl TopologyRefresherConfig { } } -pub(crate) struct TopologyRefresher { +pub(crate) struct TopologyRefresher { directory_client: directory_client::Client, - topology_accessor: TopologyAccessor, + topology_accessor: TopologyAccessor, refresh_rate: Duration, } -// TODO: consider (or maybe not) restoring generic TopologyRefresher -impl TopologyRefresher { +impl TopologyRefresher { pub(crate) fn new_directory_client( cfg: TopologyRefresherConfig, - topology_accessor: TopologyAccessor, + topology_accessor: TopologyAccessor, ) -> Self { let directory_client_config = directory_client::Config::new(cfg.directory_server); let directory_client = directory_client::Client::new(directory_client_config); @@ -189,13 +189,16 @@ impl TopologyRefresher { } } - async fn get_current_compatible_topology(&self) -> Option { + async fn get_current_compatible_topology(&self) -> Option { match self.directory_client.get_topology().await { Err(err) => { error!("failed to get network topology! - {:?}", err); None } - Ok(topology) => Some(topology.filter_system_version(built_info::PKG_VERSION)), + Ok(topology) => { + let nym_topology: NymTopology = topology.try_into().ok()?; + Some(nym_topology.filter_system_version(built_info::PKG_VERSION)) + } } } diff --git a/clients/native/src/commands/init.rs b/clients/native/src/commands/init.rs index 81c6982c49..0ee1712d84 100644 --- a/clients/native/src/commands/init.rs +++ b/clients/native/src/commands/init.rs @@ -22,10 +22,10 @@ use directory_client::DirectoryClient; use gateway_client::GatewayClient; use gateway_requests::registration::handshake::SharedKey; use pemstore::pemstore::PemStore; +use std::convert::TryInto; use std::sync::Arc; use std::time::Duration; -use topology::gateway::Node; -use topology::NymTopology; +use topology::{gateway, NymTopology}; pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { App::new("init") @@ -64,26 +64,14 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> { } async fn try_gateway_registration( - gateways: Vec, + gateways: &Vec, our_identity: Arc, ) -> Option<(String, String, SharedKey)> { let timeout = Duration::from_millis(1500); for gateway in gateways { - let gateway_identity = - match identity::PublicKey::from_base58_string(gateway.identity_key.clone()) { - Ok(id) => id, - Err(_) => { - eprintln!( - "gateway {} announces invalid identity!", - gateway.identity_key - ); - continue; - } - }; - let mut gateway_client = GatewayClient::new_init( url::Url::parse(&gateway.client_listener).unwrap(), - gateway_identity, + gateway.identity_key.clone(), our_identity.clone(), timeout, ); @@ -93,7 +81,11 @@ async fn try_gateway_registration( eprintln!("Error while closing connection to the gateway! - {:?}", err); continue; } else { - return Some((gateway.identity_key, gateway.client_listener, shared_key)); + return Some(( + gateway.identity_key.to_base58_string(), + gateway.client_listener.clone(), + shared_key, + )); } } } @@ -108,8 +100,9 @@ async fn choose_gateway( let directory_client_config = directory_client::Config::new(directory_server.clone()); let directory_client = directory_client::Client::new(directory_client_config); let topology = directory_client.get_topology().await.unwrap(); + let nym_topology: NymTopology = topology.try_into().expect("Invalid topology data!"); - let version_filtered_topology = topology.filter_system_version(built_info::PKG_VERSION); + let version_filtered_topology = nym_topology.filter_system_version(built_info::PKG_VERSION); // don't care about health of the networks as mixes can go up and down any time, // but DO care about gateways let gateways = version_filtered_topology.gateways(); @@ -138,11 +131,14 @@ async fn get_gateway_listener(directory_server: String, gateway_identity: &str) let directory_client_config = directory_client::Config::new(directory_server); let directory_client = directory_client::Client::new(directory_client_config); let topology = directory_client.get_topology().await.unwrap(); - let gateways = topology.gateways(); + + // technically we don't need to do conversion here, but let's be consistent + let nym_topology: NymTopology = topology.try_into().ok()?; + let gateways = nym_topology.gateways(); for gateway in gateways { - if gateway.identity_key == gateway_identity { - return Some(gateway.client_listener); + if gateway.identity_key.to_base58_string() == gateway_identity { + return Some(gateway.client_listener.clone()); } } None diff --git a/clients/native/src/websocket/handler.rs b/clients/native/src/websocket/handler.rs index 7ca5554c33..9696c52fdd 100644 --- a/clients/native/src/websocket/handler.rs +++ b/clients/native/src/websocket/handler.rs @@ -31,7 +31,6 @@ use tokio_tungstenite::{ tungstenite::{protocol::Message, Error as WsError}, WebSocketStream, }; -use topology::NymTopology; enum ReceivedResponseType { Binary, @@ -44,17 +43,17 @@ impl Default for ReceivedResponseType { } } -pub(crate) struct Handler { +pub(crate) struct Handler { msg_input: InputMessageSender, buffer_requester: ReceivedBufferRequestSender, self_full_address: Recipient, - topology_accessor: TopologyAccessor, + topology_accessor: TopologyAccessor, socket: Option>, received_response_type: ReceivedResponseType, } // clone is used to use handler on a new connection, which initially is `None` -impl Clone for Handler { +impl Clone for Handler { fn clone(&self) -> Self { Handler { msg_input: self.msg_input.clone(), @@ -67,7 +66,7 @@ impl Clone for Handler { } } -impl Drop for Handler { +impl Drop for Handler { fn drop(&mut self) { self.buffer_requester .unbounded_send(ReceivedBufferMessage::ReceiverDisconnect) @@ -75,12 +74,12 @@ impl Drop for Handler { } } -impl Handler { +impl Handler { pub(crate) fn new( msg_input: InputMessageSender, buffer_requester: ReceivedBufferRequestSender, self_full_address: Recipient, - topology_accessor: TopologyAccessor, + topology_accessor: TopologyAccessor, ) -> Self { Handler { msg_input, diff --git a/clients/native/src/websocket/listener.rs b/clients/native/src/websocket/listener.rs index 05c5e8fd15..170fbaa52c 100644 --- a/clients/native/src/websocket/listener.rs +++ b/clients/native/src/websocket/listener.rs @@ -20,7 +20,6 @@ use std::{ }; use tokio::runtime; use tokio::{sync::Notify, task::JoinHandle}; -use topology::NymTopology; enum State { Connected, @@ -50,7 +49,7 @@ impl Listener { } } - pub(crate) async fn run(&mut self, handler: Handler) { + pub(crate) async fn run(&mut self, handler: Handler) { let mut tcp_listener = tokio::net::TcpListener::bind(self.address) .await .expect("Failed to start websocket listener"); @@ -100,11 +99,7 @@ impl Listener { } } - pub(crate) fn start( - mut self, - rt_handle: &runtime::Handle, - handler: Handler, - ) -> JoinHandle<()> { + pub(crate) fn start(mut self, rt_handle: &runtime::Handle, handler: Handler) -> JoinHandle<()> { info!("Running websocket on {:?}", self.address.to_string()); rt_handle.spawn(async move { self.run(handler).await }) diff --git a/clients/webassembly/Cargo.toml b/clients/webassembly/Cargo.toml index 7408b983e0..3e38ac3c55 100644 --- a/clients/webassembly/Cargo.toml +++ b/clients/webassembly/Cargo.toml @@ -21,7 +21,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" slice_as_array = "1.1.0" wasm-bindgen = "0.2" -rand = "0.7.2" +rand = {version = "0.7.3", features = ["wasm-bindgen"]} # internal crypto = { path = "../../common/crypto" } diff --git a/clients/webassembly/src/lib.rs b/clients/webassembly/src/lib.rs index 06967d94c6..3347738268 100644 --- a/clients/webassembly/src/lib.rs +++ b/clients/webassembly/src/lib.rs @@ -11,24 +11,27 @@ // 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. +use crypto::asymmetric::encryption; +pub use models::keys::keygen; use models::topology::Topology; +use nymsphinx::addressing::clients::Recipient; use nymsphinx::addressing::nodes::NymNodeRoutingAddress; +use nymsphinx::params::DEFAULT_NUM_MIX_HOPS; use nymsphinx::Node as SphinxNode; use nymsphinx::{delays, Destination, NodeAddressBytes, SphinxPacket}; +use rand::rngs::OsRng; use serde::{Deserialize, Serialize}; use std::convert::TryFrom; use std::convert::TryInto; use std::net::SocketAddr; use std::time::Duration; +use topology::NymTopology; use wasm_bindgen::prelude::*; mod models; mod utils; -use crypto::asymmetric::encryption; -pub use models::keys::keygen; -use nymsphinx::addressing::clients::Recipient; -use topology::NymTopology; +const DEFAULT_RNG: OsRng = OsRng; // When the `wee_alloc` feature is enabled, use `wee_alloc` as the global // allocator. @@ -63,7 +66,8 @@ pub fn create_sphinx_packet(topology_json: &str, msg: &str, recipient: &str) -> let recipient = Recipient::try_from_string(recipient).unwrap(); - let route = sphinx_route_to(topology_json, &recipient.gateway()); + let route = + sphinx_route_to(topology_json, &recipient.gateway()).expect("todo: error handling..."); let average_delay = Duration::from_secs_f64(0.1); let delays = delays::generate_from_average_duration(route.len(), average_delay); @@ -104,13 +108,17 @@ fn payload(sphinx_packet: SphinxPacket, route: Vec) -> Vec { /// /// This function panics if the supplied `raw_route` json string can't be /// extracted to a `JsonRoute`. -fn sphinx_route_to(topology_json: &str, gateway_address: &NodeAddressBytes) -> Vec { +fn sphinx_route_to( + topology_json: &str, + gateway_address: &NodeAddressBytes, +) -> Option> { let topology = Topology::new(topology_json); - let route = topology - .random_route_to_gateway(gateway_address) + let nym_topology: NymTopology = topology.try_into().ok()?; + let route = nym_topology + .random_route_to_gateway(&mut DEFAULT_RNG, DEFAULT_NUM_MIX_HOPS, gateway_address) .expect("invalid route produced"); assert_eq!(4, route.len()); - route + Some(route) } impl TryFrom for SphinxNode { @@ -145,23 +153,6 @@ mod test_constructing_a_sphinx_packet { // ); // assert_eq!(1404, packet.len()); // } - - #[test] - #[cfg_attr(feature = "offline-test", ignore)] - fn starts_with_a_mix_address() { - let mut payload = create_sphinx_packet( - topology_fixture(), - "foomp", - "5pgrc4gPHP2tBQgfezcdJ2ZAjipoAsy6evrqHdxBbVXq@CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", - ); - // you don't really need 32 bytes here, but giving too much won't make it fail - let mut address_buffer = [0; 32]; - let _ = payload.split_off(32); - address_buffer.copy_from_slice(payload.as_slice()); - let address = NymNodeRoutingAddress::try_from_bytes(&address_buffer); - - assert!(address.is_ok()); - } } #[cfg(test)] @@ -174,10 +165,11 @@ mod building_a_topology_from_json { sphinx_route_to( "", &NodeAddressBytes::try_from_base58_string( - "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", + "FE7zC2sJZrhXgQWvzXXVH8GHi2xXRynX8UWK8rD8ikf3", ) .unwrap(), - ); + ) + .unwrap(); } #[test] @@ -186,10 +178,11 @@ mod building_a_topology_from_json { sphinx_route_to( "bad bad bad not json", &NodeAddressBytes::try_from_base58_string( - "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", + "FE7zC2sJZrhXgQWvzXXVH8GHi2xXRynX8UWK8rD8ikf3", ) .unwrap(), - ); + ) + .unwrap(); } #[test] @@ -201,10 +194,11 @@ mod building_a_topology_from_json { sphinx_route_to( &json, &NodeAddressBytes::try_from_base58_string( - "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", + "FE7zC2sJZrhXgQWvzXXVH8GHi2xXRynX8UWK8rD8ikf3", ) .unwrap(), - ); + ) + .unwrap(); } #[test] @@ -217,23 +211,24 @@ mod building_a_topology_from_json { sphinx_route_to( &json, &NodeAddressBytes::try_from_base58_string( - "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", + "FE7zC2sJZrhXgQWvzXXVH8GHi2xXRynX8UWK8rD8ikf3", ) .unwrap(), - ); + ) + .unwrap(); } - // JS: why is this an "offline-test" feature? It makes no network requests? #[test] #[cfg_attr(feature = "offline-test", ignore)] fn test_works_on_happy_json() { let route = sphinx_route_to( topology_fixture(), &NodeAddressBytes::try_from_base58_string( - "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", + "FE7zC2sJZrhXgQWvzXXVH8GHi2xXRynX8UWK8rD8ikf3", ) .unwrap(), - ); + ) + .unwrap(); assert_eq!(4, route.len()); } @@ -244,14 +239,25 @@ mod building_a_topology_from_json { let route = sphinx_route_to( &json, &NodeAddressBytes::try_from_base58_string( - "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", + "FE7zC2sJZrhXgQWvzXXVH8GHi2xXRynX8UWK8rD8ikf3", ) .unwrap(), - ); + ) + .unwrap(); assert_eq!(4, route.len()); } } +#[cfg(test)] +mod topology_fixture { + use super::*; + + #[test] + fn is_valid() { + let _nym_topology: NymTopology = Topology::new(topology_fixture()).try_into().unwrap(); + } +} + #[cfg(test)] fn topology_fixture() -> &'static str { r#" @@ -312,7 +318,7 @@ fn topology_fixture() -> &'static str { { "clientListener": "139.162.246.48:9000", "mixnetListener": "139.162.246.48:1789", - "identityKey": "CdqJCedY5d1geJNDjUqnEx8zF7mKjb6PCZ6k3T6xhxD", + "identityKey": "FE7zC2sJZrhXgQWvzXXVH8GHi2xXRynX8UWK8rD8ikf3", "sphinxKey": "BnLYqQjb8K6TmW5oFdNZrUTocGxa3rgzBvapQrf8XUbF", "version": "0.6.0", "location": "London, UK", @@ -326,7 +332,7 @@ fn topology_fixture() -> &'static str { { "clientListener": "127.0.0.1:9000", "mixnetListener": "127.0.0.1:1789", - "identityKey": "B9xz9V6jpp1fEbDkeyR5f8miorw9bzXGKoMbKnaxkD41", + "identityKey": "7hU4RNHtGC1FreLYLoBXXTH8AdaqU913NbqCv5Fu4z1r", "sphinxKey": "3KCpz1HCD8DqnQjemT1uuBZipmHFXM4V5btxLXwvM1gG", "version": "0.6.0", "location": "unknown", diff --git a/clients/webassembly/src/models/keys.rs b/clients/webassembly/src/models/keys.rs index a75b759b78..1f7713bac3 100644 --- a/clients/webassembly/src/models/keys.rs +++ b/clients/webassembly/src/models/keys.rs @@ -43,7 +43,7 @@ impl TryInto for GatewayIdentity { #[wasm_bindgen] pub fn keygen() -> String { let keypair = identity::KeyPair::new(); - let address = keypair.public_key().derive_address(); + let address = keypair.public_key().derive_destination_address(); GatewayIdentity { private_key: keypair.private_key().to_base58_string(), diff --git a/clients/webassembly/src/models/topology.rs b/clients/webassembly/src/models/topology.rs index ee9f263370..5c9c5cf9f7 100644 --- a/clients/webassembly/src/models/topology.rs +++ b/clients/webassembly/src/models/topology.rs @@ -13,7 +13,8 @@ // limitations under the License. use serde::Serializer; -use topology::{coco, gateway, mix, provider, NymTopology}; +use std::convert::TryInto; +use topology::NymTopology; #[derive(Clone, Debug)] pub struct Topology { @@ -56,35 +57,10 @@ impl Topology { } } -impl NymTopology for Topology { - fn new_from_nodes( - mix_nodes: Vec, - mix_provider_nodes: Vec, - coco_nodes: Vec, - gateway_nodes: Vec, - ) -> Self { - Topology { - inner: directory_client_models::presence::Topology::new_from_nodes( - mix_nodes, - mix_provider_nodes, - coco_nodes, - gateway_nodes, - ), - } - } - fn mix_nodes(&self) -> Vec { - self.inner.mix_nodes() - } +impl TryInto for Topology { + type Error = directory_client_models::presence::TopologyConversionError; - fn providers(&self) -> Vec { - self.inner.providers() - } - - fn gateways(&self) -> Vec { - self.inner.gateways() - } - - fn coco_nodes(&self) -> Vec { - self.inner.coco_nodes() + fn try_into(self) -> Result { + self.inner.try_into() } } diff --git a/common/client-libs/directory-client/models/Cargo.toml b/common/client-libs/directory-client/models/Cargo.toml index 533e3b8577..257fde64dc 100644 --- a/common/client-libs/directory-client/models/Cargo.toml +++ b/common/client-libs/directory-client/models/Cargo.toml @@ -9,4 +9,5 @@ edition = "2018" [dependencies] serde = { version = "1.0.104", features = ["derive"] } +crypto = { path = "../../../crypto" } topology = { path = "../../../topology" } diff --git a/common/client-libs/directory-client/models/src/presence/coconodes.rs b/common/client-libs/directory-client/models/src/presence/coconodes.rs index bff8800630..7d1d74a4c6 100644 --- a/common/client-libs/directory-client/models/src/presence/coconodes.rs +++ b/common/client-libs/directory-client/models/src/presence/coconodes.rs @@ -12,8 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crypto::asymmetric::identity; use serde::{Deserialize, Serialize}; -use topology::coco; +use std::convert::TryInto; + +#[derive(Debug)] +pub enum ConversionError { + InvalidKeyError, +} + +impl From for ConversionError { + fn from(_: identity::SignatureError) -> Self { + ConversionError::InvalidKeyError + } +} #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -25,26 +37,16 @@ pub struct CocoPresence { pub version: String, } -impl Into for CocoPresence { - fn into(self) -> topology::coco::Node { - topology::coco::Node { +impl TryInto for CocoPresence { + type Error = ConversionError; + + fn try_into(self) -> Result { + Ok(topology::coco::Node { location: self.location, host: self.host, - pub_key: self.pub_key, + pub_key: identity::PublicKey::from_base58_string(self.pub_key)?, last_seen: self.last_seen, version: self.version, - } - } -} - -impl From for CocoPresence { - fn from(cn: coco::Node) -> Self { - CocoPresence { - location: cn.location, - host: cn.host, - pub_key: cn.pub_key, - last_seen: cn.last_seen, - version: cn.version, - } + }) } } diff --git a/common/client-libs/directory-client/models/src/presence/gateways.rs b/common/client-libs/directory-client/models/src/presence/gateways.rs index 5822dc6a6a..9d1839e00d 100644 --- a/common/client-libs/directory-client/models/src/presence/gateways.rs +++ b/common/client-libs/directory-client/models/src/presence/gateways.rs @@ -12,8 +12,35 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crypto::asymmetric::{encryption, identity}; use serde::{Deserialize, Serialize}; -use topology::gateway; +use std::convert::TryInto; +use std::io; +use std::net::ToSocketAddrs; + +#[derive(Debug)] +pub enum ConversionError { + InvalidKeyError, + InvalidAddress(io::Error), +} + +impl From for ConversionError { + fn from(_: identity::SignatureError) -> Self { + ConversionError::InvalidKeyError + } +} + +impl From for ConversionError { + fn from(_: encryption::EncryptionKeyError) -> Self { + ConversionError::InvalidKeyError + } +} + +impl From for ConversionError { + fn from(err: io::Error) -> Self { + ConversionError::InvalidAddress(err) + } +} #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -28,14 +55,24 @@ pub struct GatewayPresence { pub version: String, } -impl Into for GatewayPresence { - fn into(self) -> topology::gateway::Node { - topology::gateway::Node { +impl TryInto for GatewayPresence { + type Error = ConversionError; + + fn try_into(self) -> Result { + let resolved_mix_hostname = self.mixnet_listener.to_socket_addrs()?.next(); + if resolved_mix_hostname.is_none() { + return Err(io::Error::new( + io::ErrorKind::Other, + "no valid socket address", + ))?; + } + + Ok(topology::gateway::Node { location: self.location, - client_listener: self.client_listener.parse().unwrap(), - mixnet_listener: self.mixnet_listener.parse().unwrap(), - identity_key: self.identity_key, - sphinx_key: self.sphinx_key, + client_listener: self.client_listener, + mixnet_listener: resolved_mix_hostname.unwrap(), + identity_key: identity::PublicKey::from_base58_string(self.identity_key)?, + sphinx_key: encryption::PublicKey::from_base58_string(self.sphinx_key)?, registered_clients: self .registered_clients .into_iter() @@ -43,26 +80,7 @@ impl Into for GatewayPresence { .collect(), last_seen: self.last_seen, version: self.version, - } - } -} - -impl From for GatewayPresence { - fn from(mpn: gateway::Node) -> Self { - GatewayPresence { - location: mpn.location, - client_listener: mpn.client_listener.to_string(), - mixnet_listener: mpn.mixnet_listener.to_string(), - identity_key: mpn.identity_key, - sphinx_key: mpn.sphinx_key, - registered_clients: mpn - .registered_clients - .into_iter() - .map(|c| c.into()) - .collect(), - last_seen: mpn.last_seen, - version: mpn.version, - } + }) } } @@ -79,11 +97,3 @@ impl Into for GatewayClient { } } } - -impl From for GatewayClient { - fn from(mpc: topology::gateway::Client) -> Self { - GatewayClient { - pub_key: mpc.pub_key, - } - } -} diff --git a/common/client-libs/directory-client/models/src/presence/mixnodes.rs b/common/client-libs/directory-client/models/src/presence/mixnodes.rs index b1f6e7f837..aa7b051f79 100644 --- a/common/client-libs/directory-client/models/src/presence/mixnodes.rs +++ b/common/client-libs/directory-client/models/src/presence/mixnodes.rs @@ -12,11 +12,29 @@ // See the License for the specific language governing permissions and // limitations under the License. +use crypto::asymmetric::encryption; use serde::{Deserialize, Serialize}; use std::convert::TryInto; use std::io; use std::net::ToSocketAddrs; -use topology::mix; + +#[derive(Debug)] +pub enum ConversionError { + InvalidKeyError, + InvalidAddress(io::Error), +} + +impl From for ConversionError { + fn from(_: encryption::EncryptionKeyError) -> Self { + ConversionError::InvalidKeyError + } +} + +impl From for ConversionError { + fn from(err: io::Error) -> Self { + ConversionError::InvalidAddress(err) + } +} #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -30,7 +48,7 @@ pub struct MixNodePresence { } impl TryInto for MixNodePresence { - type Error = io::Error; + type Error = ConversionError; fn try_into(self) -> Result { let resolved_hostname = self.host.to_socket_addrs()?.next(); @@ -38,29 +56,16 @@ impl TryInto for MixNodePresence { return Err(io::Error::new( io::ErrorKind::Other, "no valid socket address", - )); + ))?; } Ok(topology::mix::Node { location: self.location, host: resolved_hostname.unwrap(), - pub_key: self.pub_key, + pub_key: encryption::PublicKey::from_base58_string(self.pub_key)?, layer: self.layer, last_seen: self.last_seen, version: self.version, }) } } - -impl From for MixNodePresence { - fn from(mn: mix::Node) -> Self { - MixNodePresence { - location: mn.location, - host: mn.host.to_string(), - pub_key: mn.pub_key, - layer: mn.layer, - last_seen: mn.last_seen, - version: mn.version, - } - } -} diff --git a/common/client-libs/directory-client/models/src/presence/mod.rs b/common/client-libs/directory-client/models/src/presence/mod.rs index 10bc4a3927..1787da37a8 100644 --- a/common/client-libs/directory-client/models/src/presence/mod.rs +++ b/common/client-libs/directory-client/models/src/presence/mod.rs @@ -18,4 +18,4 @@ pub mod mixnodes; pub mod providers; pub mod topology; -pub use self::topology::Topology; +pub use self::topology::{Topology, TopologyConversionError}; diff --git a/common/client-libs/directory-client/models/src/presence/providers.rs b/common/client-libs/directory-client/models/src/presence/providers.rs index 7f01f6d6b7..a4875a048a 100644 --- a/common/client-libs/directory-client/models/src/presence/providers.rs +++ b/common/client-libs/directory-client/models/src/presence/providers.rs @@ -13,7 +13,6 @@ // limitations under the License. use serde::{Deserialize, Serialize}; -use topology::provider; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] @@ -27,60 +26,8 @@ pub struct MixProviderPresence { pub version: String, } -impl Into for MixProviderPresence { - fn into(self) -> topology::provider::Node { - topology::provider::Node { - location: self.location, - client_listener: self.client_listener.parse().unwrap(), - mixnet_listener: self.mixnet_listener.parse().unwrap(), - pub_key: self.pub_key, - registered_clients: self - .registered_clients - .into_iter() - .map(|c| c.into()) - .collect(), - last_seen: self.last_seen, - version: self.version, - } - } -} - -impl From for MixProviderPresence { - fn from(mpn: provider::Node) -> Self { - MixProviderPresence { - location: mpn.location, - client_listener: mpn.client_listener.to_string(), - mixnet_listener: mpn.mixnet_listener.to_string(), - pub_key: mpn.pub_key, - registered_clients: mpn - .registered_clients - .into_iter() - .map(|c| c.into()) - .collect(), - last_seen: mpn.last_seen, - version: mpn.version, - } - } -} - #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct MixProviderClient { pub pub_key: String, } - -impl Into for MixProviderClient { - fn into(self) -> topology::provider::Client { - topology::provider::Client { - pub_key: self.pub_key, - } - } -} - -impl From for MixProviderClient { - fn from(mpc: topology::provider::Client) -> Self { - MixProviderClient { - pub_key: mpc.pub_key, - } - } -} diff --git a/common/client-libs/directory-client/models/src/presence/topology.rs b/common/client-libs/directory-client/models/src/presence/topology.rs index 994da2e50f..e79e76af77 100644 --- a/common/client-libs/directory-client/models/src/presence/topology.rs +++ b/common/client-libs/directory-client/models/src/presence/topology.rs @@ -15,7 +15,32 @@ use super::{coconodes, gateways, mixnodes, providers}; use serde::{Deserialize, Serialize}; use std::convert::TryInto; -use topology::{coco, gateway, mix, provider, NymTopology}; +use topology::{MixLayer, NymTopology}; + +#[derive(Debug)] +pub enum TopologyConversionError { + CocoError(self::coconodes::ConversionError), + GatewayError(self::gateways::ConversionError), + MixError(self::mixnodes::ConversionError), +} + +impl From for TopologyConversionError { + fn from(err: self::coconodes::ConversionError) -> Self { + TopologyConversionError::CocoError(err) + } +} + +impl From for TopologyConversionError { + fn from(err: self::gateways::ConversionError) -> Self { + TopologyConversionError::GatewayError(err) + } +} + +impl From for TopologyConversionError { + fn from(err: self::mixnodes::ConversionError) -> Self { + TopologyConversionError::MixError(err) + } +} // Topology shows us the current state of the overall Nym network #[derive(Clone, Debug, Deserialize, Serialize)] @@ -27,47 +52,30 @@ pub struct Topology { pub gateway_nodes: Vec, } -impl NymTopology for Topology { - fn new_from_nodes( - mix_nodes: Vec, - mix_provider_nodes: Vec, - coco_nodes: Vec, - gateway_nodes: Vec, - ) -> Self { - Topology { - coco_nodes: coco_nodes.into_iter().map(|node| node.into()).collect(), - mix_nodes: mix_nodes.into_iter().map(|node| node.into()).collect(), - mix_provider_nodes: mix_provider_nodes - .into_iter() - .map(|node| node.into()) - .collect(), - gateway_nodes: gateway_nodes.into_iter().map(|node| node.into()).collect(), +impl TryInto for Topology { + type Error = TopologyConversionError; + + fn try_into(self) -> Result { + use std::collections::HashMap; + + let mut coco_nodes = Vec::with_capacity(self.coco_nodes.len()); + for coco in self.coco_nodes.into_iter() { + coco_nodes.push(coco.try_into()?) } - } - fn mix_nodes(&self) -> Vec { - self.mix_nodes - .iter() - .filter_map(|x| x.clone().try_into().ok()) - .collect() - } + let mut mixes = HashMap::new(); + for mix in self.mix_nodes.into_iter() { + let layer = mix.layer as MixLayer; + let layer_entry = mixes.entry(layer).or_insert(Vec::new()); + layer_entry.push(mix.try_into()?) + } - fn providers(&self) -> Vec { - self.mix_provider_nodes - .iter() - .map(|x| x.clone().into()) - .collect() - } + let mut gateways = Vec::with_capacity(self.gateway_nodes.len()); + for gate in self.gateway_nodes.into_iter() { + gateways.push(gate.try_into()?) + } - fn gateways(&self) -> Vec { - self.gateway_nodes - .iter() - .map(|x| x.clone().into()) - .collect() - } - - fn coco_nodes(&self) -> Vec { - self.coco_nodes.iter().map(|x| x.clone().into()).collect() + Ok(NymTopology::new(coco_nodes, mixes, gateways)) } } @@ -77,18 +85,20 @@ mod converting_mixnode_presence_into_topology_mixnode { #[test] fn it_returns_error_on_unresolvable_hostname() { + use topology::mix; + let unresolvable_hostname = "foomp.foomp.foomp:1234"; let mix_presence = mixnodes::MixNodePresence { location: "".to_string(), host: unresolvable_hostname.to_string(), - pub_key: "".to_string(), + pub_key: "BnLYqQjb8K6TmW5oFdNZrUTocGxa3rgzBvapQrf8XUbF".to_string(), layer: 0, last_seen: 0, version: "".to_string(), }; - let result: Result = mix_presence.try_into(); + let result: Result = mix_presence.try_into(); assert!(result.is_err()) // This fails only for me. Why? // ¯\_(ツ)_/¯ - works on my machine (and travis) // Is it still broken? @@ -102,13 +112,15 @@ mod converting_mixnode_presence_into_topology_mixnode { let mix_presence = mixnodes::MixNodePresence { location: "".to_string(), host: resolvable_hostname.to_string(), - pub_key: "".to_string(), + pub_key: "BnLYqQjb8K6TmW5oFdNZrUTocGxa3rgzBvapQrf8XUbF".to_string(), layer: 0, last_seen: 0, version: "".to_string(), }; - let result: Result = mix_presence.try_into(); - assert!(result.is_ok()) + let result: Result = + mix_presence.try_into(); + result.unwrap(); + // assert!(result.is_ok()) } } diff --git a/common/client-libs/gateway-client/src/lib.rs b/common/client-libs/gateway-client/src/lib.rs index eb7d1f2821..7dd7ae6bd2 100644 --- a/common/client-libs/gateway-client/src/lib.rs +++ b/common/client-libs/gateway-client/src/lib.rs @@ -376,7 +376,11 @@ impl<'a, R> GatewayClient<'static, R> { .as_ref() .unwrap_or_else(|| self.shared_key.as_ref().unwrap()); let iv = AuthenticationIV::new_random(&mut DEFAULT_RNG); - let self_address = self.local_identity.as_ref().public_key().derive_address(); + let self_address = self + .local_identity + .as_ref() + .public_key() + .derive_destination_address(); let encrypted_address = EncryptedAddressBytes::new(&self_address, shared_key, &iv); let msg = diff --git a/common/crypto/src/asymmetric/encryption/mod.rs b/common/crypto/src/asymmetric/encryption/mod.rs index 39b1fe61b5..5e7d3e4c85 100644 --- a/common/crypto/src/asymmetric/encryption/mod.rs +++ b/common/crypto/src/asymmetric/encryption/mod.rs @@ -201,6 +201,12 @@ impl Into for PublicKey { } } +impl<'a> Into for &'a PublicKey { + fn into(self) -> nymsphinx_types::PublicKey { + nymsphinx_types::PublicKey::from(self.to_bytes()) + } +} + impl From for PublicKey { fn from(pub_key: nymsphinx_types::PublicKey) -> Self { Self(x25519_dalek::PublicKey::from(*pub_key.as_bytes())) diff --git a/common/crypto/src/asymmetric/identity/mod.rs b/common/crypto/src/asymmetric/identity/mod.rs index a92c2fc825..e9d4884c08 100644 --- a/common/crypto/src/asymmetric/identity/mod.rs +++ b/common/crypto/src/asymmetric/identity/mod.rs @@ -14,9 +14,11 @@ use crate::{PemStorableKey, PemStorableKeyPair}; use bs58; -use ed25519_dalek::SignatureError; +pub use ed25519_dalek::SignatureError; pub use ed25519_dalek::{PUBLIC_KEY_LENGTH, SECRET_KEY_LENGTH, SIGNATURE_LENGTH}; -use nymsphinx_types::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH}; +use nymsphinx_types::{ + DestinationAddressBytes, NodeAddressBytes, DESTINATION_ADDRESS_LENGTH, NODE_ADDRESS_LENGTH, +}; use rand::{rngs::OsRng, CryptoRng, RngCore}; /// Keypair for usage in ed25519 EdDSA. @@ -79,7 +81,7 @@ impl PemStorableKeyPair for KeyPair { pub struct PublicKey(ed25519_dalek::PublicKey); impl PublicKey { - pub fn derive_address(&self) -> DestinationAddressBytes { + pub fn derive_destination_address(&self) -> DestinationAddressBytes { let mut temporary_address = [0u8; DESTINATION_ADDRESS_LENGTH]; let public_key_bytes = self.to_bytes(); @@ -89,6 +91,16 @@ impl PublicKey { DestinationAddressBytes::from_bytes(temporary_address) } + pub fn derive_node_address(&self) -> NodeAddressBytes { + let mut temporary_address = [0u8; NODE_ADDRESS_LENGTH]; + let public_key_bytes = self.to_bytes(); + + assert_eq!(NODE_ADDRESS_LENGTH, PUBLIC_KEY_LENGTH); + + temporary_address.copy_from_slice(&public_key_bytes[..]); + NodeAddressBytes::from_bytes(temporary_address) + } + /// Convert this public key to a byte array. pub fn to_bytes(&self) -> [u8; PUBLIC_KEY_LENGTH] { self.0.to_bytes() diff --git a/common/nymsphinx/acknowledgements/src/surb_ack.rs b/common/nymsphinx/acknowledgements/src/surb_ack.rs index 47600a66a7..b278b76cc7 100644 --- a/common/nymsphinx/acknowledgements/src/surb_ack.rs +++ b/common/nymsphinx/acknowledgements/src/surb_ack.rs @@ -16,6 +16,7 @@ use crate::identifier::{prepare_identifier, AckAes128Key}; use nymsphinx_addressing::clients::Recipient; use nymsphinx_addressing::nodes::{NymNodeRoutingAddress, MAX_NODE_ADDRESS_UNPADDED_LEN}; use nymsphinx_params::packet_sizes::PacketSize; +use nymsphinx_params::DEFAULT_NUM_MIX_HOPS; use nymsphinx_types::builder::SphinxPacketBuilder; use nymsphinx_types::{ delays::{self, Delay}, @@ -41,19 +42,19 @@ pub enum SURBAckRecoveryError { } impl SURBAck { - pub fn construct( + pub fn construct( rng: &mut R, recipient: &Recipient, ack_key: &AckAes128Key, marshaled_fragment_id: [u8; 5], average_delay: time::Duration, - topology: &T, + topology: &NymTopology, ) -> Result where R: RngCore + CryptoRng, - T: NymTopology, { - let route = topology.random_route_to_gateway(&recipient.gateway())?; + let route = + topology.random_route_to_gateway(rng, DEFAULT_NUM_MIX_HOPS, &recipient.gateway())?; let delays = delays::generate_from_average_duration(route.len(), average_delay); let destination = Destination::new(recipient.destination(), Default::default()); diff --git a/common/nymsphinx/chunking/src/lib.rs b/common/nymsphinx/chunking/src/lib.rs index a006f313c5..138b18ee51 100644 --- a/common/nymsphinx/chunking/src/lib.rs +++ b/common/nymsphinx/chunking/src/lib.rs @@ -22,6 +22,7 @@ use nymsphinx_acknowledgements::surb_ack::SURBAck; use nymsphinx_addressing::clients::Recipient; use nymsphinx_addressing::nodes::{NymNodeRoutingAddress, MAX_NODE_ADDRESS_UNPADDED_LEN}; use nymsphinx_params::packet_sizes::PacketSize; +use nymsphinx_params::DEFAULT_NUM_MIX_HOPS; use nymsphinx_types::builder::SphinxPacketBuilder; use nymsphinx_types::{delays, Delay, Destination, SphinxPacket}; use rand::{rngs::OsRng, CryptoRng, Rng}; @@ -134,7 +135,10 @@ impl MessageChunker { } } -impl MessageChunker { +impl MessageChunker +where + R: CryptoRng + Rng, +{ pub fn new_with_rng( rng: R, ack_recipient: Recipient, @@ -186,10 +190,10 @@ impl MessageChunker { /// such that it contains required SURB-ACK. /// This method can fail if the provided network topology is invalid. /// It returns total expected delay as well as the `SphinxPacket` to be sent through the network. - pub fn prepare_chunk_for_sending( + pub fn prepare_chunk_for_sending( &mut self, fragment: Fragment, - topology: &T, + topology: &NymTopology, ack_key: &AckAes128Key, packet_recipient: &Recipient, ) -> Result<(Delay, (SocketAddr, SphinxPacket)), NymTopologyError> { @@ -203,7 +207,11 @@ impl MessageChunker { .chain(fragment.into_bytes().into_iter()) .collect(); - let route = topology.random_route_to_gateway(&packet_recipient.gateway())?; + let route = topology.random_route_to_gateway( + &mut self.rng, + DEFAULT_NUM_MIX_HOPS, + &packet_recipient.gateway(), + )?; let delays = delays::generate_from_average_duration(route.len(), self.average_packet_delay_duration); let destination = Destination::new(packet_recipient.destination(), Default::default()); @@ -223,15 +231,12 @@ impl MessageChunker { )) } - fn generate_surb_ack( + fn generate_surb_ack( &mut self, fragment_id: &FragmentIdentifier, - topology: &T, + topology: &NymTopology, ack_key: &AckAes128Key, - ) -> Result - where - T: NymTopology, - { + ) -> Result { SURBAck::construct( &mut self.rng, &self.ack_recipient, diff --git a/common/nymsphinx/cover/src/lib.rs b/common/nymsphinx/cover/src/lib.rs index 3c5b5d22be..981d8508e6 100644 --- a/common/nymsphinx/cover/src/lib.rs +++ b/common/nymsphinx/cover/src/lib.rs @@ -18,6 +18,7 @@ use nymsphinx_addressing::clients::Recipient; use nymsphinx_addressing::nodes::{NymNodeRoutingAddress, NymNodeRoutingAddressError}; use nymsphinx_chunking::fragment::COVER_FRAG_ID; use nymsphinx_params::packet_sizes::PacketSize; +use nymsphinx_params::DEFAULT_NUM_MIX_HOPS; use nymsphinx_types::builder::SphinxPacketBuilder; use nymsphinx_types::{delays, Destination, Error as SphinxError, SphinxPacket}; use rand::{CryptoRng, RngCore}; @@ -55,16 +56,15 @@ impl From for CoverMessageError { } } -pub fn generate_loop_cover_surb_ack( +pub fn generate_loop_cover_surb_ack( rng: &mut R, - topology: &T, + topology: &NymTopology, ack_key: &AckAes128Key, full_address: &Recipient, average_ack_delay: time::Duration, ) -> Result where R: RngCore + CryptoRng, - T: NymTopology, { Ok(SURBAck::construct( rng, @@ -76,9 +76,9 @@ where )?) } -pub fn generate_loop_cover_packet( +pub fn generate_loop_cover_packet( rng: &mut R, - topology: &T, + topology: &NymTopology, ack_key: &AckAes128Key, full_address: &Recipient, average_ack_delay: time::Duration, @@ -86,7 +86,6 @@ pub fn generate_loop_cover_packet( ) -> Result<(SocketAddr, SphinxPacket), CoverMessageError> where R: RngCore + CryptoRng, - T: NymTopology, { // we don't care about total ack delay - we will not be retransmitting it anyway let (_, ack_bytes) = @@ -105,7 +104,8 @@ where .take(plaintext_size) .collect(); - let route = topology.random_route_to_gateway(&full_address.gateway())?; + let route = + topology.random_route_to_gateway(rng, DEFAULT_NUM_MIX_HOPS, &full_address.gateway())?; let delays = delays::generate_from_average_duration(route.len(), average_packet_delay); // in our design we don't care about SURB_ID let destination = Destination::new(full_address.destination(), Default::default()); diff --git a/common/nymsphinx/params/src/lib.rs b/common/nymsphinx/params/src/lib.rs index 17543c20d3..17a1ea8942 100644 --- a/common/nymsphinx/params/src/lib.rs +++ b/common/nymsphinx/params/src/lib.rs @@ -13,3 +13,7 @@ // limitations under the License. pub mod packet_sizes; + +// If somebody can provide an argument why it might be reasonable to have more than 255 mix hops, +// I will change this to [`usize`] +pub const DEFAULT_NUM_MIX_HOPS: u8 = 3; diff --git a/common/topology/Cargo.toml b/common/topology/Cargo.toml index 7d15a481a5..aaa6201963 100644 --- a/common/topology/Cargo.toml +++ b/common/topology/Cargo.toml @@ -8,13 +8,13 @@ edition = "2018" [dependencies] bs58 = "0.3.0" -itertools = "0.8.2" log = "0.4" pretty_env_logger = "0.3" rand = "0.7.2" serde = { version = "1.0.104", features = ["derive"] } ## internal -nymsphinx-addressing = {path = "../nymsphinx/addressing"} -nymsphinx-types = {path = "../nymsphinx/types"} -version-checker = {path = "../version-checker" } +crypto = { path = "../crypto" } +nymsphinx-addressing = { path = "../nymsphinx/addressing" } +nymsphinx-types = { path = "../nymsphinx/types" } +version-checker = { path = "../version-checker" } diff --git a/common/topology/src/coco.rs b/common/topology/src/coco.rs index ee7c494168..ebf214d30e 100644 --- a/common/topology/src/coco.rs +++ b/common/topology/src/coco.rs @@ -13,12 +13,13 @@ // limitations under the License. use crate::filter; +use crypto::asymmetric::identity; #[derive(Debug, Clone)] pub struct Node { pub location: String, pub host: String, - pub pub_key: String, + pub pub_key: identity::PublicKey, pub last_seen: u64, pub version: String, } diff --git a/common/topology/src/filter.rs b/common/topology/src/filter.rs index 37f901579e..d54b80e99f 100644 --- a/common/topology/src/filter.rs +++ b/common/topology/src/filter.rs @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +use std::collections::HashMap; +use std::hash::Hash; + pub trait Versioned: Clone { fn version(&self) -> String; } @@ -20,7 +23,10 @@ pub trait VersionFilterable { fn filter_by_version(&self, expected_version: &str) -> Self; } -impl VersionFilterable for Vec { +impl VersionFilterable for Vec +where + T: Versioned, +{ fn filter_by_version(&self, expected_version: &str) -> Self { self.iter() .filter(|node| { @@ -30,3 +36,16 @@ impl VersionFilterable for Vec { .collect() } } + +impl VersionFilterable for HashMap +where + K: Eq + Hash + Clone, + V: VersionFilterable, + T: Versioned, +{ + fn filter_by_version(&self, expected_version: &str) -> Self { + self.iter() + .map(|(k, v)| (k.clone(), v.filter_by_version(expected_version))) + .collect() + } +} diff --git a/common/topology/src/gateway.rs b/common/topology/src/gateway.rs index 26d4def744..c7ea3abfc1 100644 --- a/common/topology/src/gateway.rs +++ b/common/topology/src/gateway.rs @@ -13,6 +13,7 @@ // limitations under the License. use crate::filter; +use crypto::asymmetric::{encryption, identity}; use nymsphinx_addressing::nodes::NymNodeRoutingAddress; use nymsphinx_types::Node as SphinxNode; use std::convert::TryInto; @@ -28,21 +29,14 @@ pub struct Node { pub location: String, pub client_listener: String, pub mixnet_listener: SocketAddr, - // TODO: should we just import common/crypto and use 'proper' types for those directly? - pub identity_key: String, - pub sphinx_key: String, + pub identity_key: identity::PublicKey, + pub sphinx_key: encryption::PublicKey, // TODO: or nymsphinx::PublicKey? both are x25519 pub registered_clients: Vec, pub last_seen: u64, pub version: String, } impl Node { - fn get_sphinx_key_bytes(&self) -> [u8; 32] { - let mut key_bytes = [0; 32]; - bs58::decode(&self.sphinx_key).into(&mut key_bytes).unwrap(); - key_bytes - } - pub fn has_client(&self, client_pub_key: String) -> bool { self.registered_clients .iter() @@ -57,14 +51,12 @@ impl filter::Versioned for Node { } } -impl Into for Node { +impl<'a> Into for &'a Node { fn into(self) -> SphinxNode { let node_address_bytes = NymNodeRoutingAddress::from(self.mixnet_listener) .try_into() .unwrap(); - let key_bytes = self.get_sphinx_key_bytes(); - let key = nymsphinx_types::PublicKey::from(key_bytes); - SphinxNode::new(node_address_bytes, key) + SphinxNode::new(node_address_bytes, (&self.sphinx_key).into()) } } diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 56b2f24c82..81c9a9ba0f 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -13,162 +13,165 @@ // limitations under the License. use crate::filter::VersionFilterable; -use itertools::Itertools; use nymsphinx_types::{Node as SphinxNode, NodeAddressBytes}; -use rand::seq::IteratorRandom; -use std::cmp::max; +use rand::Rng; use std::collections::HashMap; pub mod coco; mod filter; pub mod gateway; pub mod mix; -pub mod provider; - -// TODO: Figure out why 'Clone' was required to have 'TopologyAccessor' working -// even though it only contains an Arc -pub trait NymTopology: Sized + std::fmt::Debug + Send + Sync + Clone { - fn new_from_nodes( - mix_nodes: Vec, - mix_provider_nodes: Vec, - coco_nodes: Vec, - gateway_nodes: Vec, - ) -> Self; - fn mix_nodes(&self) -> Vec; - fn providers(&self) -> Vec; - fn gateways(&self) -> Vec; - fn coco_nodes(&self) -> Vec; - fn make_layered_topology(&self) -> Result>, NymTopologyError> { - let mut layered_topology: HashMap> = HashMap::new(); - let mut highest_layer = 0; - for mix in self.mix_nodes() { - // we need to have extra space for provider - if mix.layer > nymsphinx_types::MAX_PATH_LENGTH as u64 { - return Err(NymTopologyError::InvalidMixLayerError); - } - highest_layer = max(highest_layer, mix.layer); - - let layer_nodes = layered_topology.entry(mix.layer).or_insert_with(Vec::new); - layer_nodes.push(mix); - } - - // verify the topology - make sure there are no gaps and there is at least one node per layer - let mut missing_layers = Vec::new(); - for layer in 1..=highest_layer { - if !layered_topology.contains_key(&layer) { - missing_layers.push(layer); - } - if layered_topology[&layer].is_empty() { - missing_layers.push(layer); - } - } - - if !missing_layers.is_empty() { - return Err(NymTopologyError::MissingLayerError(missing_layers)); - } - - Ok(layered_topology) - } - - // Tries to get a route through the mix network - fn random_mix_route(&self) -> Result, NymTopologyError> { - let mut layered_topology = self.make_layered_topology()?; - let num_layers = layered_topology.len(); - let route = (1..=num_layers as u64) - // unwrap is safe for 'remove' as it it failed, it implied the entry never existed - // in the map in the first place which would contradict what we've just done - .map(|layer| layered_topology.remove(&layer).unwrap()) // for each layer - .map(|nodes| nodes.into_iter().choose(&mut rand::thread_rng()).unwrap()) // choose random node - .map(|random_node| random_node.into()) // and convert it into sphinx specific node format - .collect(); - - Ok(route) - } - - fn gateway_exists(&self, gateway_address: &NodeAddressBytes) -> bool { - let b58_address = gateway_address.to_base58_string(); - self.gateways() - .iter() - .find(|&gateway| gateway.identity_key == b58_address) - .is_some() - } - - fn random_route_to_gateway( - &self, - gateway_address: &NodeAddressBytes, - ) -> Result, NymTopologyError> { - let b58_address = gateway_address.to_base58_string(); - - let gateway = self - .gateways() - .iter() - .find(|&gateway| gateway.identity_key == b58_address) - .ok_or_else(|| NymTopologyError::NonExistentGatewayError)? - .clone(); - - Ok(self - .random_mix_route()? - .into_iter() - .chain(std::iter::once(gateway.into())) - .collect()) - } - - fn all_paths(&self) -> Result>, NymTopologyError> { - let mut layered_topology = self.make_layered_topology()?; - let gateways = self.gateways(); - - let sorted_layers: Vec> = (1..=layered_topology.len() as u64) - .map(|layer| layered_topology.remove(&layer).unwrap()) // get all nodes per layer - .map(|layer_nodes| layer_nodes.into_iter().map(|node| node.into()).collect()) // convert them into 'proper' sphinx nodes - .chain(std::iter::once( - gateways.into_iter().map(|node| node.into()).collect(), - )) // append all gateways to the end - .collect(); - - let all_paths = sorted_layers - .into_iter() - .multi_cartesian_product() // create all possible paths through that - .collect(); - - Ok(all_paths) - } - - fn filter_system_version(&self, expected_version: &str) -> Self { - self.filter_node_versions( - expected_version, - expected_version, - expected_version, - expected_version, - ) - } - - fn filter_node_versions( - &self, - expected_mix_version: &str, - expected_provider_version: &str, - expected_gateway_version: &str, - expected_coco_version: &str, - ) -> Self { - let mixes = self.mix_nodes().filter_by_version(expected_mix_version); - let providers = self - .providers() - .filter_by_version(expected_provider_version); - let gateways = self.gateways().filter_by_version(expected_gateway_version); - let cocos = self.coco_nodes().filter_by_version(expected_coco_version); - - Self::new_from_nodes(mixes, providers, cocos, gateways) - } - - fn can_construct_path_through(&self) -> bool { - !self.mix_nodes().is_empty() - && !self.gateways().is_empty() - && self.make_layered_topology().is_ok() - } -} #[derive(Debug)] pub enum NymTopologyError { InvalidMixLayerError, MissingLayerError(Vec), NonExistentGatewayError, + + InvalidNumberOfHopsError, + NoMixesOnLayerAvailable(MixLayer), +} + +pub type MixLayer = u8; + +#[derive(Debug)] +pub struct NymTopology { + coco_nodes: Vec, + mixes: HashMap>, + gateways: Vec, +} + +impl NymTopology { + pub fn new( + coco_nodes: Vec, + mixes: HashMap>, + gateways: Vec, + ) -> Self { + NymTopology { + coco_nodes, + mixes, + gateways, + } + } + + pub fn coco_nodes(&self) -> &Vec { + &self.coco_nodes + } + + pub fn mixes(&self) -> &HashMap> { + &self.mixes + } + + pub fn gateways(&self) -> &Vec { + &self.gateways + } + + fn get_gateway(&self, gateway_address: &NodeAddressBytes) -> Option<&gateway::Node> { + self.gateways + .iter() + .find(|gateway| &gateway.identity_key.derive_node_address() == gateway_address) + } + + pub fn gateway_exists(&self, gateway_address: &NodeAddressBytes) -> bool { + self.get_gateway(gateway_address).is_some() + } + + pub fn random_mix_route( + &self, + rng: &mut R, + num_mix_hops: u8, + ) -> Result, NymTopologyError> + where + // I don't think there's a need for this RNG to be crypto-secure + R: Rng + ?Sized, + { + use rand::seq::SliceRandom; + + if self.mixes.len() < num_mix_hops as usize { + return Err(NymTopologyError::InvalidNumberOfHopsError); + } + let mut route = Vec::with_capacity(num_mix_hops as usize); + + // there is no "layer 0" + for layer in 1..=num_mix_hops { + // get all mixes on particular layer + let layer_mixes = match self.mixes.get(&layer) { + Some(mixes) => mixes, + None => return Err(NymTopologyError::NoMixesOnLayerAvailable(layer)), + }; + + // choose a random mix from the above list + // this can return a 'None' only if slice is empty + let random_mix = match layer_mixes.choose(rng) { + Some(random_mix) => random_mix, + None => return Err(NymTopologyError::NoMixesOnLayerAvailable(layer)), + }; + route.push(random_mix.into()); + } + + Ok(route) + } + + pub fn random_route_to_gateway( + &self, + rng: &mut R, + num_mix_hops: u8, + gateway_address: &NodeAddressBytes, + ) -> Result, NymTopologyError> + where + // I don't think there's a need for this RNG to be crypto-secure + R: Rng + ?Sized, + { + let gateway = self + .get_gateway(gateway_address) + .ok_or_else(|| NymTopologyError::NonExistentGatewayError)?; + + Ok(self + .random_mix_route(rng, num_mix_hops)? + .into_iter() + .chain(std::iter::once(gateway.into())) + .collect()) + } + + pub fn can_construct_path_through(&self, num_mix_hops: u8) -> bool { + // if there are no gateways present, we can't do anything + if self.gateways.is_empty() { + return false; + } + + // early termination + if self.mixes.is_empty() { + return false; + } + + // make sure there's at least one mix per layer + for i in 1..=num_mix_hops { + match self.mixes.get(&i) { + None => return false, + Some(layer_entry) => { + if layer_entry.is_empty() { + return false; + } + } + } + } + true + } + + pub fn filter_system_version(&self, expected_version: &str) -> Self { + self.filter_node_versions(expected_version, expected_version, expected_version) + } + + pub fn filter_node_versions( + &self, + expected_mix_version: &str, + expected_gateway_version: &str, + expected_coco_version: &str, + ) -> Self { + NymTopology { + mixes: self.mixes.filter_by_version(expected_mix_version), + gateways: self.gateways.filter_by_version(expected_gateway_version), + coco_nodes: self.coco_nodes.filter_by_version(expected_coco_version), + } + } } diff --git a/common/topology/src/mix.rs b/common/topology/src/mix.rs index 3bb5e0caf6..b25d8e995f 100644 --- a/common/topology/src/mix.rs +++ b/common/topology/src/mix.rs @@ -13,6 +13,7 @@ // limitations under the License. use crate::filter; +use crypto::asymmetric::encryption; use nymsphinx_addressing::nodes::NymNodeRoutingAddress; use nymsphinx_types::Node as SphinxNode; use std::convert::TryInto; @@ -22,32 +23,22 @@ use std::net::SocketAddr; pub struct Node { pub location: String, pub host: SocketAddr, - pub pub_key: String, + pub pub_key: encryption::PublicKey, // TODO: or nymsphinx::PublicKey? both are x25519 pub layer: u64, pub last_seen: u64, pub version: String, } -impl Node { - pub fn get_pub_key_bytes(&self) -> [u8; 32] { - let mut key_bytes = [0; 32]; - bs58::decode(&self.pub_key).into(&mut key_bytes).unwrap(); - key_bytes - } -} - impl filter::Versioned for Node { fn version(&self) -> String { self.version.clone() } } -impl Into for Node { +impl<'a> Into for &'a Node { fn into(self) -> SphinxNode { let node_address_bytes = NymNodeRoutingAddress::from(self.host).try_into().unwrap(); - let key_bytes = self.get_pub_key_bytes(); - let key = nymsphinx_types::PublicKey::from(key_bytes); - SphinxNode::new(node_address_bytes, key) + SphinxNode::new(node_address_bytes, (&self.pub_key).into()) } } diff --git a/common/topology/src/provider.rs b/common/topology/src/provider.rs deleted file mode 100644 index 93630b0058..0000000000 --- a/common/topology/src/provider.rs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2020 Nym Technologies SA -// -// 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. - -use crate::filter; -use nymsphinx_addressing::nodes::NymNodeRoutingAddress; -use nymsphinx_types::Node as SphinxNode; -use std::convert::TryInto; -use std::net::SocketAddr; - -#[derive(Debug, Clone)] -pub struct Client { - pub pub_key: String, -} - -#[derive(Debug, Clone)] -pub struct Node { - pub location: String, - pub client_listener: SocketAddr, - pub mixnet_listener: SocketAddr, - pub pub_key: String, - pub registered_clients: Vec, - pub last_seen: u64, - pub version: String, -} - -impl Node { - pub fn get_pub_key_bytes(&self) -> [u8; 32] { - let mut key_bytes = [0; 32]; - bs58::decode(&self.pub_key).into(&mut key_bytes).unwrap(); - key_bytes - } -} - -impl filter::Versioned for Node { - fn version(&self) -> String { - self.version.clone() - } -} - -impl Into for Node { - fn into(self) -> SphinxNode { - let node_address_bytes = NymNodeRoutingAddress::from(self.mixnet_listener) - .try_into() - .unwrap(); - let key_bytes = self.get_pub_key_bytes(); - let key = nymsphinx_types::PublicKey::from(key_bytes); - - SphinxNode::new(node_address_bytes, key) - } -} diff --git a/gateway/src/node/client_handling/websocket/connection_handler.rs b/gateway/src/node/client_handling/websocket/connection_handler.rs index ddf0cef7c5..cc5b7c7e1f 100644 --- a/gateway/src/node/client_handling/websocket/connection_handler.rs +++ b/gateway/src/node/client_handling/websocket/connection_handler.rs @@ -296,7 +296,7 @@ impl Handle { Some(address) => address, None => return ServerResponse::new_error("malformed request"), }; - let remote_address = remote_identity.derive_address(); + let remote_address = remote_identity.derive_destination_address(); let derived_shared_key = match self.perform_registration_handshake(init_data).await { Ok(shared_key) => shared_key, From 5ef96aa24133b4c518ddaca68de225b31070174a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 13 Jul 2020 12:04:25 +0100 Subject: [PATCH 14/76] Updated blake3 dependency to 0.3.5 (#281) --- Cargo.lock | 5 +++-- common/crypto/Cargo.toml | 6 +----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a7b7fba1fc..5313b1b60c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -239,8 +239,9 @@ dependencies = [ [[package]] name = "blake3" -version = "0.3.4" -source = "git+https://github.com/BLAKE3-team/BLAKE3?rev=4c41a893a00a3ebe7b24529531ccf96d8593a57c#4c41a893a00a3ebe7b24529531ccf96d8593a57c" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59f88a20f7dc23e3896bcbd85add056543c87215de721468b90e0c85d5a9f365" dependencies = [ "arrayref", "arrayvec", diff --git a/common/crypto/Cargo.toml b/common/crypto/Cargo.toml index cb4e3df78b..9f6fcbdf61 100644 --- a/common/crypto/Cargo.toml +++ b/common/crypto/Cargo.toml @@ -9,11 +9,7 @@ edition = "2018" [dependencies] aes-ctr = "0.4.0" bs58 = "0.3.0" -# can't use proper release just yet (unless we use outdated hkdf) -# as hkdf depends on digest 0.9.0 and most recent release of blake3 still uses 0.8.1 -# they've pushed an update to their repo on 12.06, so I presume another release is imminent. -blake3 = { git = "https://github.com/BLAKE3-team/BLAKE3", rev="4c41a893a00a3ebe7b24529531ccf96d8593a57c" } -#blake3 = "0.3.4" +blake3 = "0.3.5" hkdf = "0.9" x25519-dalek = "0.6" From 6cc2bc6f91b9ebe752a7ca9d7db2c27ac27aac82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C4=99drzej=20Stuczy=C5=84ski?= Date: Mon, 13 Jul 2020 12:07:06 +0100 Subject: [PATCH 15/76] Feature/ws send confirmation removal (#280) * Removed send confirmation * Updated websocket examples --- .../go-examples/websocket/filesend.go | 8 --- .../go-examples/websocket/textsend.go | 9 --- .../python-examples/websocket/filesend.py | 2 - .../python-examples/websocket/textsend.py | 2 - clients/native/src/websocket/handler.rs | 57 +++++++++++-------- 5 files changed, 32 insertions(+), 46 deletions(-) diff --git a/clients/native/examples/go-examples/websocket/filesend.go b/clients/native/examples/go-examples/websocket/filesend.go index 5fe4859c8e..e143ef7546 100644 --- a/clients/native/examples/go-examples/websocket/filesend.go +++ b/clients/native/examples/go-examples/websocket/filesend.go @@ -56,14 +56,6 @@ func main() { if err = conn.WriteMessage(websocket.BinaryMessage, payload); err != nil { panic(err) } - sendConfirmationJSON := make(map[string]interface{}) - err = conn.ReadJSON(&sendConfirmationJSON) - if err != nil { - panic(err) - } - if sendConfirmationJSON["type"].(string) != "send" { - panic("invalid send confirmation") - } fmt.Printf("waiting to receive a message from the mix network...\n") _, receivedMessage, err := conn.ReadMessage() diff --git a/clients/native/examples/go-examples/websocket/textsend.go b/clients/native/examples/go-examples/websocket/textsend.go index 2f46490016..0396891d75 100644 --- a/clients/native/examples/go-examples/websocket/textsend.go +++ b/clients/native/examples/go-examples/websocket/textsend.go @@ -49,15 +49,6 @@ func main() { panic(err) } - sendConfirmationJSON := make(map[string]interface{}) - err = conn.ReadJSON(&sendConfirmationJSON) - if err != nil { - panic(err) - } - if sendConfirmationJSON["type"].(string) != "send" { - panic("invalid send confirmation") - } - fmt.Printf("waiting to receive a message from the mix network...\n") _, receivedMessage, err := conn.ReadMessage() if err != nil { diff --git a/clients/native/examples/python-examples/websocket/filesend.py b/clients/native/examples/python-examples/websocket/filesend.py index 7eba9a52a4..561be863c8 100644 --- a/clients/native/examples/python-examples/websocket/filesend.py +++ b/clients/native/examples/python-examples/websocket/filesend.py @@ -26,8 +26,6 @@ async def send_file(): print("sending content of 'dummy_file' over the mix network...") await websocket.send(bin_payload) - msg_send_confirmation = json.loads(await websocket.recv()) - assert msg_send_confirmation["type"], "send" print("waiting to receive the 'dummy_file' from the mix network...") received_data = await websocket.recv() diff --git a/clients/native/examples/python-examples/websocket/textsend.py b/clients/native/examples/python-examples/websocket/textsend.py index b92a1e1838..ea9cf770a5 100644 --- a/clients/native/examples/python-examples/websocket/textsend.py +++ b/clients/native/examples/python-examples/websocket/textsend.py @@ -24,8 +24,6 @@ async def send_text(): print("sending '{}' over the mix network...".format(message)) await websocket.send(text_send) - msg_send_confirmation = json.loads(await websocket.recv()) - assert msg_send_confirmation["type"], "send" print("waiting to receive a message from the mix network...") received_message = await websocket.recv() diff --git a/clients/native/src/websocket/handler.rs b/clients/native/src/websocket/handler.rs index 9696c52fdd..346d85c824 100644 --- a/clients/native/src/websocket/handler.rs +++ b/clients/native/src/websocket/handler.rs @@ -91,14 +91,18 @@ impl Handler { } } - fn handle_text_send(&mut self, msg: String, full_recipient_address: String) -> ServerResponse { + fn handle_text_send( + &mut self, + msg: String, + full_recipient_address: String, + ) -> Option { let message_bytes = msg.into_bytes(); let recipient = match Recipient::try_from_string(full_recipient_address) { Ok(address) => address, Err(e) => { trace!("failed to parse received Recipient: {:?}", e); - return ServerResponse::new_error("malformed recipient address"); + return Some(ServerResponse::new_error("malformed recipient address")); } }; @@ -107,8 +111,7 @@ impl Handler { self.msg_input.unbounded_send(input_msg).unwrap(); self.received_response_type = ReceivedResponseType::Text; - - ServerResponse::Send + None } async fn handle_text_get_clients(&mut self) -> ServerResponse { @@ -129,52 +132,58 @@ impl Handler { } } - async fn handle_text_message(&mut self, msg: String) -> Message { + async fn handle_text_message(&mut self, msg: String) -> Option { debug!("Handling text message request"); trace!("Content: {:?}", msg.clone()); match ClientRequest::try_from(msg) { - Err(e) => ServerResponse::Error { - message: format!("received invalid request. err: {:?}", e), - } - .into(), - Ok(req) => match req { - ClientRequest::Send { message, recipient } => { - self.handle_text_send(message, recipient) + Err(e) => Some( + ServerResponse::Error { + message: format!("received invalid request. err: {:?}", e), } - ClientRequest::GetClients => self.handle_text_get_clients().await, - ClientRequest::SelfAddress => self.handle_text_self_address(), - } - .into(), + .into(), + ), + Ok(req) => match req { + ClientRequest::Send { message, recipient } => self + .handle_text_send(message, recipient) + .map(|resp| resp.into()), + ClientRequest::GetClients => Some(self.handle_text_get_clients().await.into()), + ClientRequest::SelfAddress => Some(self.handle_text_self_address().into()), + }, } } - async fn handle_binary_send(&mut self, recipient: Recipient, data: Vec) -> ServerResponse { + async fn handle_binary_send( + &mut self, + recipient: Recipient, + data: Vec, + ) -> Option { // the ack control is now responsible for chunking, etc. let input_msg = InputMessage::new(recipient, data); self.msg_input.unbounded_send(input_msg).unwrap(); self.received_response_type = ReceivedResponseType::Binary; - ServerResponse::Send + + None } // if it's binary we assume it's a sphinx packet formatted the same way as we'd have sent // it to the gateway - async fn handle_binary_message(&mut self, msg: Vec) -> Message { + async fn handle_binary_message(&mut self, msg: Vec) -> Option { debug!("Handling binary message request"); self.received_response_type = ReceivedResponseType::Binary; // make sure it is correctly formatted let binary_request = BinaryClientRequest::try_from_bytes(&msg); if binary_request.is_none() { - return ServerResponse::new_error("invalid binary request").into(); + return Some(ServerResponse::new_error("invalid binary request").into()); } match binary_request.unwrap() { BinaryClientRequest::Send { recipient, data } => { self.handle_binary_send(recipient, data).await } } - .into() + .map(|resp| resp.into()) } async fn handle_request(&mut self, raw_request: Message) -> Option { @@ -182,10 +191,8 @@ impl Handler { // them and let's test that claim. If that's not the case, just copy code from // old version of this file. match raw_request { - Message::Text(text_message) => Some(self.handle_text_message(text_message).await), - Message::Binary(binary_message) => { - Some(self.handle_binary_message(binary_message).await) - } + Message::Text(text_message) => self.handle_text_message(text_message).await, + Message::Binary(binary_message) => self.handle_binary_message(binary_message).await, _ => None, } } From 2d6812afa820670537729f1d51525eb67069c1e9 Mon Sep 17 00:00:00 2001 From: Kevin Foesenek Date: Wed, 15 Jul 2020 17:11:52 +0200 Subject: [PATCH 16/76] Added react example (#275) Added react example in addition to the basic js example. --- .DS_Store | Bin 0 -> 6148 bytes clients/.DS_Store | Bin 0 -> 6148 bytes clients/webassembly/.DS_Store | Bin 0 -> 6148 bytes clients/webassembly/react-example/.gitignore | 23 + clients/webassembly/react-example/README.md | 20 + .../react-example/config-overrides.js | 25 + .../webassembly/react-example/package.json | 47 + .../react-example/public/favicon.ico | Bin 0 -> 3150 bytes .../react-example/public/index.html | 43 + .../react-example/public/logo192.png | Bin 0 -> 5347 bytes .../react-example/public/logo512.png | Bin 0 -> 9664 bytes .../react-example/public/manifest.json | 25 + .../react-example/public/robots.txt | 3 + clients/webassembly/react-example/src/App.js | 197 ++++ .../webassembly/react-example/src/App.test.js | 9 + .../webassembly/react-example/src/index.js | 19 + .../webassembly/react-example/src/logo.svg | 7 + .../react-example/src/scss/example-react.scss | 21 + .../implementation/bootstrap/_buttons.scss | 143 +++ .../scss/implementation/bootstrap/_card.scss | 301 +++++ .../scss/implementation/bootstrap/_forms.scss | 333 ++++++ .../implementation/bootstrap/_functions.scss | 86 ++ .../implementation/bootstrap/_mixins.scss | 41 + .../implementation/bootstrap/_reboot.scss | 483 ++++++++ .../implementation/bootstrap/_tables.scss | 187 +++ .../implementation/bootstrap/_utilities.scss | 15 + .../implementation/bootstrap/_variables.scss | 952 ++++++++++++++++ .../bootstrap/mixins/_alert.scss | 13 + .../bootstrap/mixins/_background-variant.scss | 21 + .../bootstrap/mixins/_badge.scss | 12 + .../bootstrap/mixins/_border-radius.scss | 35 + .../bootstrap/mixins/_box-shadow.scss | 5 + .../bootstrap/mixins/_breakpoints.scss | 123 ++ .../bootstrap/mixins/_buttons.scss | 109 ++ .../bootstrap/mixins/_caret.scss | 66 ++ .../bootstrap/mixins/_clearfix.scss | 7 + .../bootstrap/mixins/_float.scss | 11 + .../bootstrap/mixins/_forms.scss | 147 +++ .../bootstrap/mixins/_gradients.scss | 45 + .../bootstrap/mixins/_grid-framework.scss | 67 ++ .../bootstrap/mixins/_grid.scss | 52 + .../bootstrap/mixins/_hover.scss | 37 + .../bootstrap/mixins/_image.scss | 36 + .../bootstrap/mixins/_list-group.scss | 21 + .../bootstrap/mixins/_lists.scss | 7 + .../bootstrap/mixins/_nav-divider.scss | 10 + .../bootstrap/mixins/_pagination.scss | 22 + .../bootstrap/mixins/_reset-text.scss | 17 + .../bootstrap/mixins/_resize.scss | 6 + .../bootstrap/mixins/_screen-reader.scss | 33 + .../bootstrap/mixins/_size.scss | 6 + .../bootstrap/mixins/_table-row.scss | 30 + .../bootstrap/mixins/_text-emphasis.scss | 14 + .../bootstrap/mixins/_text-hide.scss | 13 + .../bootstrap/mixins/_text-truncate.scss | 8 + .../bootstrap/mixins/_transition.scss | 13 + .../bootstrap/mixins/_visibility.scss | 7 + .../bootstrap/utilities/_align.scss | 8 + .../bootstrap/utilities/_background.scss | 19 + .../bootstrap/utilities/_borders.scss | 59 + .../bootstrap/utilities/_clearfix.scss | 3 + .../bootstrap/utilities/_display.scss | 38 + .../bootstrap/utilities/_embed.scss | 52 + .../bootstrap/utilities/_flex.scss | 51 + .../bootstrap/utilities/_float.scss | 9 + .../bootstrap/utilities/_position.scss | 37 + .../bootstrap/utilities/_screenreaders.scss | 11 + .../bootstrap/utilities/_shadows.scss | 6 + .../bootstrap/utilities/_sizing.scss | 12 + .../bootstrap/utilities/_spacing.scss | 51 + .../bootstrap/utilities/_text.scss | 58 + .../bootstrap/utilities/_visibility.scss | 11 + .../scss/implementation/custom/_alert.scss | 6 + .../scss/implementation/custom/_avatar.scss | 6 + .../scss/implementation/custom/_badge.scss | 7 + .../scss/implementation/custom/_buttons.scss | 7 + .../src/scss/implementation/custom/_card.scss | 8 + .../scss/implementation/custom/_chart.scss | 5 + .../scss/implementation/custom/_close.scss | 5 + .../implementation/custom/_components.scss | 31 + .../scss/implementation/custom/_content.scss | 47 + .../implementation/custom/_custom-forms.scss | 9 + .../scss/implementation/custom/_dropdown.scss | 5 + .../scss/implementation/custom/_footer.scss | 5 + .../scss/implementation/custom/_forms.scss | 7 + .../implementation/custom/_functions.scss | 33 + .../scss/implementation/custom/_header.scss | 5 + .../scss/implementation/custom/_icons.scss | 6 + .../implementation/custom/_input-group.scss | 5 + .../implementation/custom/_list-group.scss | 5 + .../src/scss/implementation/custom/_map.scss | 5 + .../src/scss/implementation/custom/_mask.scss | 5 + .../scss/implementation/custom/_mixins.scss | 19 + .../scss/implementation/custom/_modal.scss | 5 + .../src/scss/implementation/custom/_nav.scss | 7 + .../scss/implementation/custom/_navbar.scss | 9 + .../implementation/custom/_pagination.scss | 5 + .../scss/implementation/custom/_popover.scss | 6 + .../scss/implementation/custom/_progress.scss | 5 + .../scss/implementation/custom/_reboot.scss | 13 + .../scss/implementation/custom/_section.scss | 139 +++ .../implementation/custom/_separator.scss | 5 + .../scss/implementation/custom/_tables.scss | 5 + .../src/scss/implementation/custom/_type.scss | 8 + .../implementation/custom/_utilities.scss | 16 + .../implementation/custom/_variables.scss | 1009 +++++++++++++++++ .../scss/implementation/custom/_vendors.scss | 9 + .../custom/alerts/_alert-dismissible.scss | 39 + .../implementation/custom/alerts/_alert.scss | 46 + .../custom/avatars/_avatar-group.scss | 22 + .../custom/avatars/_avatar.scss | 42 + .../custom/badges/_badge-circle.scss | 17 + .../custom/badges/_badge-dot.scss | 42 + .../implementation/custom/badges/_badge.scss | 55 + .../custom/buttons/_button-brand.scss | 12 + .../custom/buttons/_button-icon.scss | 92 ++ .../custom/buttons/_button.scss | 91 ++ .../custom/cards/_card-animations.scss | 10 + .../custom/cards/_card-blockquote.scss | 17 + .../custom/cards/_card-profile.scss | 49 + .../custom/cards/_card-stats.scss | 16 + .../implementation/custom/cards/_card.scss | 8 + .../implementation/custom/charts/_chart.scss | 69 ++ .../implementation/custom/close/_close.scss | 34 + .../custom/custom-forms/_custom-checkbox.scss | 37 + .../custom/custom-forms/_custom-control.scss | 51 + .../custom/custom-forms/_custom-form.scss | 37 + .../custom/custom-forms/_custom-radio.scss | 37 + .../custom/custom-forms/_custom-toggle.scss | 66 ++ .../custom/dropdowns/_dropdown.scss | 79 ++ .../custom/footers/_footer.scss | 98 ++ .../custom/forms/_form-validation.scss | 71 ++ .../implementation/custom/forms/_form.scss | 78 ++ .../custom/forms/_input-group.scss | 70 ++ .../custom/headers/_header.scss | 7 + .../custom/icons/_icon-shape.scss | 42 + .../implementation/custom/icons/_icon.scss | 65 ++ .../custom/list-groups/_list-group.scss | 46 + .../scss/implementation/custom/maps/_map.scss | 10 + .../implementation/custom/masks/_mask.scss | 12 + .../implementation/custom/mixins/_alert.scss | 13 + .../custom/mixins/_background-variant.scss | 30 + .../implementation/custom/mixins/_badge.scss | 12 + .../custom/mixins/_buttons.scss | 105 ++ .../implementation/custom/mixins/_forms.scss | 128 +++ .../implementation/custom/mixins/_icon.scss | 4 + .../implementation/custom/mixins/_modals.scss | 23 + .../custom/mixins/_popover.scss | 36 + .../implementation/custom/modals/_modal.scss | 25 + .../custom/navbars/_navbar-collapse.scss | 125 ++ .../custom/navbars/_navbar-dropdown.scss | 101 ++ .../custom/navbars/_navbar-search.scss | 69 ++ .../custom/navbars/_navbar-vertical.scss | 286 +++++ .../custom/navbars/_navbar.scss | 153 +++ .../custom/navs/_nav-pills.scss | 66 ++ .../scss/implementation/custom/navs/_nav.scss | 42 + .../custom/paginations/_pagination.scss | 48 + .../custom/popovers/_popover.scss | 21 + .../custom/progresses/_progress.scss | 79 ++ .../custom/separators/_separator.scss | 54 + .../implementation/custom/tables/_table.scss | 118 ++ .../implementation/custom/type/_article.scss | 22 + .../implementation/custom/type/_display.scss | 14 + .../implementation/custom/type/_heading.scss | 52 + .../implementation/custom/type/_type.scss | 27 + .../custom/utilities/_backgrounds.scss | 51 + .../custom/utilities/_blurable.scss | 37 + .../custom/utilities/_floating.scss | 64 ++ .../custom/utilities/_helper.scss | 18 + .../custom/utilities/_image.scss | 9 + .../custom/utilities/_opacity.scss | 35 + .../custom/utilities/_overflow.scss | 11 + .../custom/utilities/_position.scss | 25 + .../custom/utilities/_shadows.scss | 28 + .../custom/utilities/_sizing.scss | 7 + .../custom/utilities/_spacing.scss | 107 ++ .../custom/utilities/_text.scss | 49 + .../custom/utilities/_transform.scss | 13 + .../react-example/src/serviceWorker.js | 141 +++ .../react-example/src/setupTests.js | 5 + 180 files changed, 9590 insertions(+) create mode 100644 .DS_Store create mode 100644 clients/.DS_Store create mode 100644 clients/webassembly/.DS_Store create mode 100644 clients/webassembly/react-example/.gitignore create mode 100644 clients/webassembly/react-example/README.md create mode 100644 clients/webassembly/react-example/config-overrides.js create mode 100644 clients/webassembly/react-example/package.json create mode 100644 clients/webassembly/react-example/public/favicon.ico create mode 100644 clients/webassembly/react-example/public/index.html create mode 100644 clients/webassembly/react-example/public/logo192.png create mode 100644 clients/webassembly/react-example/public/logo512.png create mode 100644 clients/webassembly/react-example/public/manifest.json create mode 100644 clients/webassembly/react-example/public/robots.txt create mode 100644 clients/webassembly/react-example/src/App.js create mode 100644 clients/webassembly/react-example/src/App.test.js create mode 100644 clients/webassembly/react-example/src/index.js create mode 100644 clients/webassembly/react-example/src/logo.svg create mode 100644 clients/webassembly/react-example/src/scss/example-react.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/_buttons.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/_card.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/_forms.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/_functions.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/_mixins.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/_reboot.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/_tables.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/_utilities.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/_variables.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_alert.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_background-variant.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_badge.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_border-radius.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_box-shadow.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_breakpoints.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_buttons.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_caret.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_clearfix.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_float.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_forms.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_gradients.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_grid-framework.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_grid.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_hover.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_image.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_list-group.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_lists.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_nav-divider.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_pagination.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_reset-text.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_resize.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_screen-reader.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_size.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_table-row.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_text-emphasis.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_text-hide.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_text-truncate.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_transition.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/mixins/_visibility.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/utilities/_align.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/utilities/_background.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/utilities/_borders.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/utilities/_clearfix.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/utilities/_display.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/utilities/_embed.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/utilities/_flex.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/utilities/_float.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/utilities/_position.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/utilities/_screenreaders.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/utilities/_shadows.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/utilities/_sizing.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/utilities/_spacing.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/utilities/_text.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/bootstrap/utilities/_visibility.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_alert.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_avatar.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_badge.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_buttons.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_card.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_chart.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_close.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_components.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_content.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_custom-forms.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_dropdown.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_footer.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_forms.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_functions.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_header.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_icons.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_input-group.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_list-group.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_map.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_mask.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_mixins.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_modal.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_nav.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_navbar.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_pagination.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_popover.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_progress.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_reboot.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_section.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_separator.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_tables.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_type.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_utilities.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_variables.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/_vendors.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/alerts/_alert-dismissible.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/alerts/_alert.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/avatars/_avatar-group.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/avatars/_avatar.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/badges/_badge-circle.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/badges/_badge-dot.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/badges/_badge.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/buttons/_button-brand.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/buttons/_button-icon.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/buttons/_button.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/cards/_card-animations.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/cards/_card-blockquote.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/cards/_card-profile.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/cards/_card-stats.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/cards/_card.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/charts/_chart.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/close/_close.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/custom-forms/_custom-checkbox.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/custom-forms/_custom-control.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/custom-forms/_custom-form.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/custom-forms/_custom-radio.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/custom-forms/_custom-toggle.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/dropdowns/_dropdown.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/footers/_footer.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/forms/_form-validation.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/forms/_form.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/forms/_input-group.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/headers/_header.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/icons/_icon-shape.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/icons/_icon.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/list-groups/_list-group.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/maps/_map.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/masks/_mask.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/mixins/_alert.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/mixins/_background-variant.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/mixins/_badge.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/mixins/_buttons.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/mixins/_forms.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/mixins/_icon.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/mixins/_modals.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/mixins/_popover.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/modals/_modal.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/navbars/_navbar-collapse.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/navbars/_navbar-dropdown.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/navbars/_navbar-search.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/navbars/_navbar-vertical.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/navbars/_navbar.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/navs/_nav-pills.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/navs/_nav.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/paginations/_pagination.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/popovers/_popover.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/progresses/_progress.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/separators/_separator.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/tables/_table.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/type/_article.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/type/_display.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/type/_heading.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/type/_type.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/utilities/_backgrounds.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/utilities/_blurable.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/utilities/_floating.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/utilities/_helper.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/utilities/_image.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/utilities/_opacity.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/utilities/_overflow.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/utilities/_position.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/utilities/_shadows.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/utilities/_sizing.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/utilities/_spacing.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/utilities/_text.scss create mode 100644 clients/webassembly/react-example/src/scss/implementation/custom/utilities/_transform.scss create mode 100644 clients/webassembly/react-example/src/serviceWorker.js create mode 100644 clients/webassembly/react-example/src/setupTests.js diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..ab5b4a2c952ff26b4902615572c617933fba52f4 GIT binary patch literal 6148 zcmeHK-EPw`6h7X{nhYj-fwW!b28oMmYP*SDOh{eV3M%m@g5UzsmMqa0k;YY$u8dOD z8{PqU10IDZ;6Zo*`0SV|NedTTAXNEC_UG8=vwOai*fkNc&d6&L)riPI5m;GAwZO!< zD08-=dNzPe&cSJ$LgI8nujjmVhgLu<@V_Y_zTI^?B!^-;#g}{e{q{qC;)dMCxY!)_ zQEvT^hE|!Af;phD#Q9R~q~pXd#M5IZJ#$t2Dd*v3 z+i{--{`jbNm7r^7y#JN0FDvdF*UJE_77enoM?Et-950?M&;| zWN)ugw{~}KPp77_d9!(^e>}K&J9#&K{|O<%7&cXD*A@PP&uElHIk)2|YbHuXy0BIQNz0TTBHiR0vv zU-XuV&U>5m1oIwHmmUBYy&59fc}A5IL>gm_;euB~M4IAc}=}u#?|EwQAef~&zq&u;8hPu`YXa$N2tY(ij zasKyyfB!F%^pjRVE3i}wuu9kNb`X*}TjzokXRU>Dj3P|<%`%h}RO&dE4RI82p-97+ ZB^toC!dZssf$2X2QU+aV1^%f5KLJqi;miO4 literal 0 HcmV?d00001 diff --git a/clients/.DS_Store b/clients/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..dfb4022efb6b76c28403c71751e982e8240dcd0e GIT binary patch literal 6148 zcmeHK!A{#i5S@iI*r2L8RD$DPkb1}=38);ZN>-o;Zb+`mp{>9s7FaS~6gyZnr1EdH zf54~k1$<6BGdrzt(_8_eooM#W&d%(5pR_w!08qops1MKq01uV0wTjJugvLqdWJx@2 zM8$JlLb9KklQ=W+T(omI1{?!VjR88lZO9>l5>jZMU%C|Dp}zUW!k^xVjjQ59&iZY9 zk)=i1>)olP<<^V#is$>D|I+^&o|!5vqjFLVqw#0z9Yp0>EcQoZvcGl|e@c_;sPlT) z2>w?*4x?4 z_trQ2@Ar?-uf9#MXWxJDHbCKp#qvt<5Bx-40ms18WPt7mjY=2@))x8d zz=odyh%I!ppiT9Ua9))s9#D0zG+Sy{;oMe*u5 zzvggIfkoFk1{?!T29|BNL-qf|;`e_u$@Lrqj)7;zfNJf9dqdok)YiGpQLWXe$EYL} nS6k#1Y@{5sL8W*Dl?BH%8Hj;kZ4o^v?vDV|;2OukgEH_NdUUlc literal 0 HcmV?d00001 diff --git a/clients/webassembly/.DS_Store b/clients/webassembly/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..02c9dfaa8fb67b6570857c1b3e3cc6deb971a5e6 GIT binary patch literal 6148 zcmeHK%Sr<=6g_Dxv{j_8bU8~mEx2|weSrlPu^*s)QPj30wSvpp_z(Vq-{%MDxk)H9 zsdXtL_rlFdGWX;rC&Od{z_gynCqNBAnJ!qUu=vL0x%h?^e2?&q9V0;lDwh^rz ze^CM1yDHikV2LSrZoPlKSuz^Vk|CeV%W+CS{fLLO;HP+>K zQ8Ug6~j0>`y(G0drTcVI*h-37@yhr8;Y^nIeuirVPc0`ssgG& zUV)-(*5v%}ZomKMN!n5cRDpk`fGKtQofenm&(@8_$ypoGZ|PzZmpWWiSg}VjW92A5 bqFZBsBo$)pF?Glun*In_8MIIZepP`l)BlI3 literal 0 HcmV?d00001 diff --git a/clients/webassembly/react-example/.gitignore b/clients/webassembly/react-example/.gitignore new file mode 100644 index 0000000000..4d29575de8 --- /dev/null +++ b/clients/webassembly/react-example/.gitignore @@ -0,0 +1,23 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* diff --git a/clients/webassembly/react-example/README.md b/clients/webassembly/react-example/README.md new file mode 100644 index 0000000000..2bfb105a01 --- /dev/null +++ b/clients/webassembly/react-example/README.md @@ -0,0 +1,20 @@ +This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). + +# Nym Sphinx Wasm Demo + +This example application demonstrates how to use WebAssembly to create Sphinx packets, in the browser, and forward them to a Nym gateway. + +## 🚴 Usage + +``` +npm install # set up dependencies +npm run start # starts a web server at http://localhost:3000 +``` + +Check your dev console for output. + +### Rebuild after Rust source changes + +Install `wasm-pack`. Instruction are at the [Rust WASM tutorial](https://rustwasm.github.io/docs/book/game-of-life/hello-world.html). + +`wasm-pack build` in the `clients/webassembly` directory (one up) will rebuild the wasm package if you make changes to the Rust source. That will be automatically picked up (and reloaded, if need be) by the npm dev server. \ No newline at end of file diff --git a/clients/webassembly/react-example/config-overrides.js b/clients/webassembly/react-example/config-overrides.js new file mode 100644 index 0000000000..5959acfa58 --- /dev/null +++ b/clients/webassembly/react-example/config-overrides.js @@ -0,0 +1,25 @@ +const path = require('path'); + +module.exports = function override(config, env) { + const wasmExtensionRegExp = /\.wasm$/; + + config.resolve.extensions.push('.wasm'); + + config.module.rules.forEach(rule => { + (rule.oneOf || []).forEach(oneOf => { + if (oneOf.loader && oneOf.loader.indexOf('file-loader') >= 0) { + // make file-loader ignore WASM files + oneOf.exclude.push(wasmExtensionRegExp); + } + }); + }); + + // add a dedicated loader for WASM + config.module.rules.push({ + test: wasmExtensionRegExp, + include: path.resolve(__dirname, 'src'), + use: [{ loader: require.resolve('wasm-loader'), options: {} }] + }); + + return config; +}; \ No newline at end of file diff --git a/clients/webassembly/react-example/package.json b/clients/webassembly/react-example/package.json new file mode 100644 index 0000000000..2f77db0d4f --- /dev/null +++ b/clients/webassembly/react-example/package.json @@ -0,0 +1,47 @@ +{ + "name": "react-example", + "version": "0.1.0", + "private": true, + "dependencies": { + "@nymproject/nym-client-wasm": "^0.7.5", + "@testing-library/jest-dom": "^4.2.4", + "@testing-library/react": "^9.5.0", + "@testing-library/user-event": "^7.2.1", + "bootstrap": "^4.5.0", + "node-sass": "^4.14.1", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-scripts": "3.4.1", + "reactstrap": "^8.5.1", + "request": "^2.79.0" + }, + "scripts": { + "start": "react-app-rewired start", + "build": "react-app-rewired build", + "test": "react-app-rewired test", + "eject": "react-app-rewired eject" + }, + "eslintConfig": { + "extends": "react-app" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "copy-webpack-plugin": "^5.0.0", + "hello-wasm-pack": "^0.1.0", + "react-app-rewired": "^2.1.6", + "wasm-loader": "^1.3.0", + "webpack": "^4.29.3", + "webpack-cli": "^3.1.0" + } +} diff --git a/clients/webassembly/react-example/public/favicon.ico b/clients/webassembly/react-example/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..bcd5dfd67cd0361b78123e95c2dd96031f27f743 GIT binary patch literal 3150 zcmaKtc{Ei0AIGn;MZ^<@lHD*OV;K7~W1q3jSjJcqNywTkMOhP*k~Oj?GO|6{m(*C2 zC7JA+hN%%Bp7T4;J@?%2_x=5zbI<2~->=X60stMr0B~{wzpi9D0MG|# zyuANt7z6;uz%?PEfAnimLl^)6h5ARwGXemG2>?hqQv-I^Gpyh$JH}Ag92}3{$a#z& zd`il2Sb#$U&e&4#^4R|GTgk!Qs+x*PCL{2+`uB5mqtnqLaaw`*H2oqJ?XF(zUACc2 zSibBrdQzcidqv*TK}rpEv1ie&;Famq2IK5%4c}1Jt2b1x_{y1C!?EU)@`_F)yN*NK z)(u03@%g%uDawwXGAMm%EnP9FgoucUedioDwL~{6RVO@A-Q$+pwVRR%WYR>{K3E&Q zzqzT!EEZ$_NHGYM6&PK#CGUV$pTWsiI5#~m>htoJ!vbc0=gm3H8sz8KzIiVN5xdCT z%;}`UH2Pc8))1VS-unh?v4*H*NIy5On{MRKw7BTmOO9oE2UApwkCl9Z?^dod9M^#w z51tEZhf+#dpTo#GDDy#kuzoIjMjZ?%v*h$ z*vwUMOjGc?R0(FjLWkMD)kca4z6~H45FIzQ!Zzu&-yWyMdCBsDr2`l}Q{8fH$H@O< z$&snNzbqLk?(GIe?!PVh?F~2qk4z^rMcp$P^hw^rUPjyCyoNTRw%;hNOwrCoN?G0E z!wT^=4Loa9@O{t;Wk(Nj=?ms1Z?UN_;21m%sUm?uib=pg&x|u)8pP#l--$;B9l47n zUUnMV0sXLe*@Gvy>XWjRoqc2tOzgYn%?g@Lb8C&WsxV1Kjssh^ZBs*Ysr+E6%tsC_ zCo-)hkYY=Bn?wMB4sqm?WS>{kh<6*DO)vXnQpQ9`-_qF6!#b;3Nf@;#B>e2j$yokl6F|9p1<($2 z=WSr%)Z?^|r6njhgbuMrIN>8JE05u0x5t@_dEfbGn9r0hK4c2vp>(*$GXsjeLL_uz zWpyfUgdv!~-2N;llVzik#s2*XB*%7u8(^sJv&T3pzaR&<9({17Zs~UY>#ugZZkHBs zD+>0_an$?}utGp$dcXtyFHnTQZJ}SF=oZ}X07dz~K>^o(vjTzw8ZQc!Fw1W=&Z?9% zv63|~l}70sJbY?H8ON8j)w5=6OpXuaZ}YT03`2%u8{;B0Vafo_iY7&BiQTbRkdJBYL}?%ATfmc zLG$uXt$@3j#OIjALdT&Ut$=9F8cgV{w_f5eS)PjoVi z&oemp-SKJ~UuGuCP1|iY?J^S&P z)-IG?O-*=z6kfZrX5H*G=aQ{ZaqnOqP@&+_;nq@mA>EcjgxrYX8EK|Iq4&E&rxR?R z8N$QOdRwY zr{P`O)=87>YLHtFfGXW z6P)ucrhj~It_9w<^v5>T6N1U}+BkS))=WX*2JY=}^b2czGhH<`?`(}}qMcpPx_%>M zM|fs(+I1m&_h(zqp-HgP>re$2O^o$q)xu#fl0ivOJE({duU)a*OD(eYgSi^cdTn}pqcPM(;S)2%1By^Wh%-CaC%>d9hi`7J zaxL7@;nhA>PE%s99&;z{8>VFgf{u!(-B-x7Of6ueme+ScryL`h(^qKE)DtieWY>-7 zgB)VJESQS4*1LU(2&@pgLvSt{(((C?K_V(rQk``i&5}ZPG;G^FiPlZ$7|-vEmMWlU z5lQ%iK2nu=h2wd_7>gK@vX=*AG+u~rQP$NwPC`ZA?4nh{3tui1x@bT6-;Rk3yDQ>d z?3qRD#+PeV7#FAa>s`Xwxsx_oRFcN$StW2=CW`=qObsT?SD^#^jM1Yk}PSPxJ zG@-_mnNU_)vM|iLRSI>UMp|hatyS}17R{10IuL0TLlupt>9dRs_SPQbv7BLYyC#qv16E-y@XZ= z-!p7I%#r-BVi$nQq3&ssRc_IC%R6$tA&^s_l46880~Wst3@>(|EO<}T4~ci~#!=e; zD)B>o%1+$ksURD1p7I-<3ehlFyVkqrySf&gg>Bp0Z9?JaG|gyTZ{Cb8SdvAWVmFX7v2ohs!OCc!Udk zUITUpmZ33rKLI#(&lDj}cKA#dpL4Fil=$5pu_wi1XJR!llw` zSItPBDEdMHk2>c7#%lBxZHHvtVUOZ$}v?=?AT~9!Jcqa@IJGuMg(s^7r>pcTrd)pS`{5Cu8WPey` z9)!!OUUY@L%9Q+bZa*S5`3f_|lFCPN6kdp_M2>{le8;cn^XUsPa+TUk47qd6)IBR% zk*&Ip?!Ge_gmmdj)BX}P_5o@VI2*wbZ^>UhFju}0gQZh!pP%4XT9{@w;G#b3XK8sN zF(7i$Jv(IM$8Akys9dhP^^~H2(7BfJp}yDW1#@!CL-!mGcSCnJ599WK9MV@yo_u$v MDeX2GIKR{Qf5okjU;qFB literal 0 HcmV?d00001 diff --git a/clients/webassembly/react-example/public/index.html b/clients/webassembly/react-example/public/index.html new file mode 100644 index 0000000000..50813f5e54 --- /dev/null +++ b/clients/webassembly/react-example/public/index.html @@ -0,0 +1,43 @@ + + + + + + + + + + + + + Nym WebAssembly Demo + + + +

+ + + diff --git a/clients/webassembly/react-example/public/logo192.png b/clients/webassembly/react-example/public/logo192.png new file mode 100644 index 0000000000000000000000000000000000000000..fc44b0a3796c0e0a64c3d858ca038bd4570465d9 GIT binary patch literal 5347 zcmZWtbyO6NvR-oO24RV%BvuJ&=?+<7=`LvyB&A_#M7mSDYw1v6DJkiYl9XjT!%$dLEBTQ8R9|wd3008in6lFF3GV-6mLi?MoP_y~}QUnaDCHI#t z7w^m$@6DI)|C8_jrT?q=f8D?0AM?L)Z}xAo^e^W>t$*Y0KlT5=@bBjT9kxb%-KNdk zeOS1tKO#ChhG7%{ApNBzE2ZVNcxbrin#E1TiAw#BlUhXllzhN$qWez5l;h+t^q#Eav8PhR2|T}y5kkflaK`ba-eoE+Z2q@o6P$)=&` z+(8}+-McnNO>e#$Rr{32ngsZIAX>GH??tqgwUuUz6kjns|LjsB37zUEWd|(&O!)DY zQLrq%Y>)Y8G`yYbYCx&aVHi@-vZ3|ebG!f$sTQqMgi0hWRJ^Wc+Ibv!udh_r%2|U) zPi|E^PK?UE!>_4`f`1k4hqqj_$+d!EB_#IYt;f9)fBOumGNyglU(ofY`yHq4Y?B%- zp&G!MRY<~ajTgIHErMe(Z8JG*;D-PJhd@RX@QatggM7+G(Lz8eZ;73)72Hfx5KDOE zkT(m}i2;@X2AT5fW?qVp?@WgN$aT+f_6eo?IsLh;jscNRp|8H}Z9p_UBO^SJXpZew zEK8fz|0Th%(Wr|KZBGTM4yxkA5CFdAj8=QSrT$fKW#tweUFqr0TZ9D~a5lF{)%-tTGMK^2tz(y2v$i%V8XAxIywrZCp=)83p(zIk6@S5AWl|Oa2hF`~~^W zI;KeOSkw1O#TiQ8;U7OPXjZM|KrnN}9arP)m0v$c|L)lF`j_rpG(zW1Qjv$=^|p*f z>)Na{D&>n`jOWMwB^TM}slgTEcjxTlUby89j1)|6ydRfWERn3|7Zd2&e7?!K&5G$x z`5U3uFtn4~SZq|LjFVrz$3iln-+ucY4q$BC{CSm7Xe5c1J<=%Oagztj{ifpaZk_bQ z9Sb-LaQMKp-qJA*bP6DzgE3`}*i1o3GKmo2pn@dj0;He}F=BgINo};6gQF8!n0ULZ zL>kC0nPSFzlcB7p41doao2F7%6IUTi_+!L`MM4o*#Y#0v~WiO8uSeAUNp=vA2KaR&=jNR2iVwG>7t%sG2x_~yXzY)7K& zk3p+O0AFZ1eu^T3s};B%6TpJ6h-Y%B^*zT&SN7C=N;g|#dGIVMSOru3iv^SvO>h4M=t-N1GSLLDqVTcgurco6)3&XpU!FP6Hlrmj}f$ zp95;b)>M~`kxuZF3r~a!rMf4|&1=uMG$;h^g=Kl;H&Np-(pFT9FF@++MMEx3RBsK?AU0fPk-#mdR)Wdkj)`>ZMl#^<80kM87VvsI3r_c@_vX=fdQ`_9-d(xiI z4K;1y1TiPj_RPh*SpDI7U~^QQ?%0&!$Sh#?x_@;ag)P}ZkAik{_WPB4rHyW#%>|Gs zdbhyt=qQPA7`?h2_8T;-E6HI#im9K>au*(j4;kzwMSLgo6u*}-K`$_Gzgu&XE)udQ zmQ72^eZd|vzI)~!20JV-v-T|<4@7ruqrj|o4=JJPlybwMg;M$Ud7>h6g()CT@wXm` zbq=A(t;RJ^{Xxi*Ff~!|3!-l_PS{AyNAU~t{h;(N(PXMEf^R(B+ZVX3 z8y0;0A8hJYp@g+c*`>eTA|3Tgv9U8#BDTO9@a@gVMDxr(fVaEqL1tl?md{v^j8aUv zm&%PX4^|rX|?E4^CkplWWNv*OKM>DxPa z!RJ)U^0-WJMi)Ksc!^ixOtw^egoAZZ2Cg;X7(5xZG7yL_;UJ#yp*ZD-;I^Z9qkP`} zwCTs0*%rIVF1sgLervtnUo&brwz?6?PXRuOCS*JI-WL6GKy7-~yi0giTEMmDs_-UX zo=+nFrW_EfTg>oY72_4Z0*uG>MnXP=c0VpT&*|rvv1iStW;*^={rP1y?Hv+6R6bxFMkxpWkJ>m7Ba{>zc_q zEefC3jsXdyS5??Mz7IET$Kft|EMNJIv7Ny8ZOcKnzf`K5Cd)&`-fTY#W&jnV0l2vt z?Gqhic}l}mCv1yUEy$%DP}4AN;36$=7aNI^*AzV(eYGeJ(Px-j<^gSDp5dBAv2#?; zcMXv#aj>%;MiG^q^$0MSg-(uTl!xm49dH!{X0){Ew7ThWV~Gtj7h%ZD zVN-R-^7Cf0VH!8O)uUHPL2mO2tmE*cecwQv_5CzWeh)ykX8r5Hi`ehYo)d{Jnh&3p z9ndXT$OW51#H5cFKa76c<%nNkP~FU93b5h-|Cb}ScHs@4Q#|}byWg;KDMJ#|l zE=MKD*F@HDBcX@~QJH%56eh~jfPO-uKm}~t7VkHxHT;)4sd+?Wc4* z>CyR*{w@4(gnYRdFq=^(#-ytb^5ESD?x<0Skhb%Pt?npNW1m+Nv`tr9+qN<3H1f<% zZvNEqyK5FgPsQ`QIu9P0x_}wJR~^CotL|n zk?dn;tLRw9jJTur4uWoX6iMm914f0AJfB@C74a;_qRrAP4E7l890P&{v<}>_&GLrW z)klculcg`?zJO~4;BBAa=POU%aN|pmZJn2{hA!d!*lwO%YSIzv8bTJ}=nhC^n}g(ld^rn#kq9Z3)z`k9lvV>y#!F4e{5c$tnr9M{V)0m(Z< z#88vX6-AW7T2UUwW`g<;8I$Jb!R%z@rCcGT)-2k7&x9kZZT66}Ztid~6t0jKb&9mm zpa}LCb`bz`{MzpZR#E*QuBiZXI#<`5qxx=&LMr-UUf~@dRk}YI2hbMsAMWOmDzYtm zjof16D=mc`^B$+_bCG$$@R0t;e?~UkF?7<(vkb70*EQB1rfUWXh$j)R2)+dNAH5%R zEBs^?N;UMdy}V};59Gu#0$q53$}|+q7CIGg_w_WlvE}AdqoS<7DY1LWS9?TrfmcvT zaypmplwn=P4;a8-%l^e?f`OpGb}%(_mFsL&GywhyN(-VROj`4~V~9bGv%UhcA|YW% zs{;nh@aDX11y^HOFXB$a7#Sr3cEtNd4eLm@Y#fc&j)TGvbbMwze zXtekX_wJqxe4NhuW$r}cNy|L{V=t#$%SuWEW)YZTH|!iT79k#?632OFse{+BT_gau zJwQcbH{b}dzKO?^dV&3nTILYlGw{27UJ72ZN){BILd_HV_s$WfI2DC<9LIHFmtyw? zQ;?MuK7g%Ym+4e^W#5}WDLpko%jPOC=aN)3!=8)s#Rnercak&b3ESRX3z{xfKBF8L z5%CGkFmGO@x?_mPGlpEej!3!AMddChabyf~nJNZxx!D&{@xEb!TDyvqSj%Y5@A{}9 zRzoBn0?x}=krh{ok3Nn%e)#~uh;6jpezhA)ySb^b#E>73e*frBFu6IZ^D7Ii&rsiU z%jzygxT-n*joJpY4o&8UXr2s%j^Q{?e-voloX`4DQyEK+DmrZh8A$)iWL#NO9+Y@!sO2f@rI!@jN@>HOA< z?q2l{^%mY*PNx2FoX+A7X3N}(RV$B`g&N=e0uvAvEN1W^{*W?zT1i#fxuw10%~))J zjx#gxoVlXREWZf4hRkgdHx5V_S*;p-y%JtGgQ4}lnA~MBz-AFdxUxU1RIT$`sal|X zPB6sEVRjGbXIP0U+?rT|y5+ev&OMX*5C$n2SBPZr`jqzrmpVrNciR0e*Wm?fK6DY& zl(XQZ60yWXV-|Ps!A{EF;=_z(YAF=T(-MkJXUoX zI{UMQDAV2}Ya?EisdEW;@pE6dt;j0fg5oT2dxCi{wqWJ<)|SR6fxX~5CzblPGr8cb zUBVJ2CQd~3L?7yfTpLNbt)He1D>*KXI^GK%<`bq^cUq$Q@uJifG>p3LU(!H=C)aEL zenk7pVg}0{dKU}&l)Y2Y2eFMdS(JS0}oZUuVaf2+K*YFNGHB`^YGcIpnBlMhO7d4@vV zv(@N}(k#REdul8~fP+^F@ky*wt@~&|(&&meNO>rKDEnB{ykAZ}k>e@lad7to>Ao$B zz<1(L=#J*u4_LB=8w+*{KFK^u00NAmeNN7pr+Pf+N*Zl^dO{LM-hMHyP6N!~`24jd zXYP|Ze;dRXKdF2iJG$U{k=S86l@pytLx}$JFFs8e)*Vi?aVBtGJ3JZUj!~c{(rw5>vuRF$`^p!P8w1B=O!skwkO5yd4_XuG^QVF z`-r5K7(IPSiKQ2|U9+`@Js!g6sfJwAHVd|s?|mnC*q zp|B|z)(8+mxXyxQ{8Pg3F4|tdpgZZSoU4P&9I8)nHo1@)9_9u&NcT^FI)6|hsAZFk zZ+arl&@*>RXBf-OZxhZerOr&dN5LW9@gV=oGFbK*J+m#R-|e6(Loz(;g@T^*oO)0R zN`N=X46b{7yk5FZGr#5&n1!-@j@g02g|X>MOpF3#IjZ_4wg{dX+G9eqS+Es9@6nC7 zD9$NuVJI}6ZlwtUm5cCAiYv0(Yi{%eH+}t)!E^>^KxB5^L~a`4%1~5q6h>d;paC9c zTj0wTCKrhWf+F#5>EgX`sl%POl?oyCq0(w0xoL?L%)|Q7d|Hl92rUYAU#lc**I&^6p=4lNQPa0 znQ|A~i0ip@`B=FW-Q;zh?-wF;Wl5!+q3GXDu-x&}$gUO)NoO7^$BeEIrd~1Dh{Tr` z8s<(Bn@gZ(mkIGnmYh_ehXnq78QL$pNDi)|QcT*|GtS%nz1uKE+E{7jdEBp%h0}%r zD2|KmYGiPa4;md-t_m5YDz#c*oV_FqXd85d@eub?9N61QuYcb3CnVWpM(D-^|CmkL z(F}L&N7qhL2PCq)fRh}XO@U`Yn<?TNGR4L(mF7#4u29{i~@k;pLsgl({YW5`Mo+p=zZn3L*4{JU;++dG9 X@eDJUQo;Ye2mwlRs?y0|+_a0zY+Zo%Dkae}+MySoIppb75o?vUW_?)>@g{U2`ERQIXV zeY$JrWnMZ$QC<=ii4X|@0H8`si75jB(ElJb00HAB%>SlLR{!zO|C9P3zxw_U8?1d8uRZ=({Ga4shyN}3 zAK}WA(ds|``G4jA)9}Bt2Hy0+f3rV1E6b|@?hpGA=PI&r8)ah|)I2s(P5Ic*Ndhn^ z*T&j@gbCTv7+8rpYbR^Ty}1AY)YH;p!m948r#%7x^Z@_-w{pDl|1S4`EM3n_PaXvK z1JF)E3qy$qTj5Xs{jU9k=y%SQ0>8E$;x?p9ayU0bZZeo{5Z@&FKX>}s!0+^>C^D#z z>xsCPvxD3Z=dP}TTOSJhNTPyVt14VCQ9MQFN`rn!c&_p?&4<5_PGm4a;WS&1(!qKE z_H$;dDdiPQ!F_gsN`2>`X}$I=B;={R8%L~`>RyKcS$72ai$!2>d(YkciA^J0@X%G4 z4cu!%Ps~2JuJ8ex`&;Fa0NQOq_nDZ&X;^A=oc1&f#3P1(!5il>6?uK4QpEG8z0Rhu zvBJ+A9RV?z%v?!$=(vcH?*;vRs*+PPbOQ3cdPr5=tOcLqmfx@#hOqX0iN)wTTO21jH<>jpmwRIAGw7`a|sl?9y9zRBh>(_%| zF?h|P7}~RKj?HR+q|4U`CjRmV-$mLW>MScKnNXiv{vD3&2@*u)-6P@h0A`eeZ7}71 zK(w%@R<4lLt`O7fs1E)$5iGb~fPfJ?WxhY7c3Q>T-w#wT&zW522pH-B%r5v#5y^CF zcC30Se|`D2mY$hAlIULL%-PNXgbbpRHgn<&X3N9W!@BUk@9g*P5mz-YnZBb*-$zMM z7Qq}ic0mR8n{^L|=+diODdV}Q!gwr?y+2m=3HWwMq4z)DqYVg0J~^}-%7rMR@S1;9 z7GFj6K}i32X;3*$SmzB&HW{PJ55kT+EI#SsZf}bD7nW^Haf}_gXciYKX{QBxIPSx2Ma? zHQqgzZq!_{&zg{yxqv3xq8YV+`S}F6A>Gtl39_m;K4dA{pP$BW0oIXJ>jEQ!2V3A2 zdpoTxG&V=(?^q?ZTj2ZUpDUdMb)T?E$}CI>r@}PFPWD9@*%V6;4Ag>D#h>!s)=$0R zRXvdkZ%|c}ubej`jl?cS$onl9Tw52rBKT)kgyw~Xy%z62Lr%V6Y=f?2)J|bZJ5(Wx zmji`O;_B+*X@qe-#~`HFP<{8$w@z4@&`q^Q-Zk8JG3>WalhnW1cvnoVw>*R@c&|o8 zZ%w!{Z+MHeZ*OE4v*otkZqz11*s!#s^Gq>+o`8Z5 z^i-qzJLJh9!W-;SmFkR8HEZJWiXk$40i6)7 zZpr=k2lp}SasbM*Nbn3j$sn0;rUI;%EDbi7T1ZI4qL6PNNM2Y%6{LMIKW+FY_yF3) zSKQ2QSujzNMSL2r&bYs`|i2Dnn z=>}c0>a}>|uT!IiMOA~pVT~R@bGlm}Edf}Kq0?*Af6#mW9f9!}RjW7om0c9Qlp;yK z)=XQs(|6GCadQbWIhYF=rf{Y)sj%^Id-ARO0=O^Ad;Ph+ z0?$eE1xhH?{T$QI>0JP75`r)U_$#%K1^BQ8z#uciKf(C701&RyLQWBUp*Q7eyn76} z6JHpC9}R$J#(R0cDCkXoFSp;j6{x{b&0yE@P7{;pCEpKjS(+1RQy38`=&Yxo%F=3y zCPeefABp34U-s?WmU#JJw23dcC{sPPFc2#J$ZgEN%zod}J~8dLm*fx9f6SpO zn^Ww3bt9-r0XaT2a@Wpw;C23XM}7_14#%QpubrIw5aZtP+CqIFmsG4`Cm6rfxl9n5 z7=r2C-+lM2AB9X0T_`?EW&Byv&K?HS4QLoylJ|OAF z`8atBNTzJ&AQ!>sOo$?^0xj~D(;kS$`9zbEGd>f6r`NC3X`tX)sWgWUUOQ7w=$TO&*j;=u%25ay-%>3@81tGe^_z*C7pb9y*Ed^H3t$BIKH2o+olp#$q;)_ zfpjCb_^VFg5fU~K)nf*d*r@BCC>UZ!0&b?AGk_jTPXaSnCuW110wjHPPe^9R^;jo3 zwvzTl)C`Zl5}O2}3lec=hZ*$JnkW#7enKKc)(pM${_$9Hc=Sr_A9Biwe*Y=T?~1CK z6eZ9uPICjy-sMGbZl$yQmpB&`ouS8v{58__t0$JP%i3R&%QR3ianbZqDs<2#5FdN@n5bCn^ZtH992~5k(eA|8|@G9u`wdn7bnpg|@{m z^d6Y`*$Zf2Xr&|g%sai#5}Syvv(>Jnx&EM7-|Jr7!M~zdAyjt*xl;OLhvW-a%H1m0 z*x5*nb=R5u><7lyVpNAR?q@1U59 zO+)QWwL8t zyip?u_nI+K$uh{y)~}qj?(w0&=SE^8`_WMM zTybjG=999h38Yes7}-4*LJ7H)UE8{mE(6;8voE+TYY%33A>S6`G_95^5QHNTo_;Ao ztIQIZ_}49%{8|=O;isBZ?=7kfdF8_@azfoTd+hEJKWE!)$)N%HIe2cplaK`ry#=pV z0q{9w-`i0h@!R8K3GC{ivt{70IWG`EP|(1g7i_Q<>aEAT{5(yD z=!O?kq61VegV+st@XCw475j6vS)_z@efuqQgHQR1T4;|-#OLZNQJPV4k$AX1Uk8Lm z{N*b*ia=I+MB}kWpupJ~>!C@xEN#Wa7V+7{m4j8c?)ChV=D?o~sjT?0C_AQ7B-vxqX30s0I_`2$in86#`mAsT-w?j{&AL@B3$;P z31G4(lV|b}uSDCIrjk+M1R!X7s4Aabn<)zpgT}#gE|mIvV38^ODy@<&yflpCwS#fRf9ZX3lPV_?8@C5)A;T zqmouFLFk;qIs4rA=hh=GL~sCFsXHsqO6_y~*AFt939UYVBSx1s(=Kb&5;j7cSowdE;7()CC2|-i9Zz+_BIw8#ll~-tyH?F3{%`QCsYa*b#s*9iCc`1P1oC26?`g<9))EJ3%xz+O!B3 zZ7$j~To)C@PquR>a1+Dh>-a%IvH_Y7^ys|4o?E%3`I&ADXfC8++hAdZfzIT#%C+Jz z1lU~K_vAm0m8Qk}K$F>|>RPK%<1SI0(G+8q~H zAsjezyP+u!Se4q3GW)`h`NPSRlMoBjCzNPesWJwVTY!o@G8=(6I%4XHGaSiS3MEBK zhgGFv6Jc>L$4jVE!I?TQuwvz_%CyO!bLh94nqK11C2W$*aa2ueGopG8DnBICVUORP zgytv#)49fVXDaR$SukloYC3u7#5H)}1K21=?DKj^U)8G;MS)&Op)g^zR2($<>C*zW z;X7`hLxiIO#J`ANdyAOJle4V%ppa*(+0i3w;8i*BA_;u8gOO6)MY`ueq7stBMJTB; z-a0R>hT*}>z|Gg}@^zDL1MrH+2hsR8 zHc}*9IvuQC^Ju)^#Y{fOr(96rQNPNhxc;mH@W*m206>Lo<*SaaH?~8zg&f&%YiOEG zGiz?*CP>Bci}!WiS=zj#K5I}>DtpregpP_tfZtPa(N<%vo^#WCQ5BTv0vr%Z{)0q+ z)RbfHktUm|lg&U3YM%lMUM(fu}i#kjX9h>GYctkx9Mt_8{@s%!K_EI zScgwy6%_fR?CGJQtmgNAj^h9B#zmaMDWgH55pGuY1Gv7D z;8Psm(vEPiwn#MgJYu4Ty9D|h!?Rj0ddE|&L3S{IP%H4^N!m`60ZwZw^;eg4sk6K{ ziA^`Sbl_4~f&Oo%n;8Ye(tiAdlZKI!Z=|j$5hS|D$bDJ}p{gh$KN&JZYLUjv4h{NY zBJ>X9z!xfDGY z+oh_Z&_e#Q(-}>ssZfm=j$D&4W4FNy&-kAO1~#3Im;F)Nwe{(*75(p=P^VI?X0GFakfh+X-px4a%Uw@fSbmp9hM1_~R>?Z8+ ziy|e9>8V*`OP}4x5JjdWp}7eX;lVxp5qS}0YZek;SNmm7tEeSF*-dI)6U-A%m6YvCgM(}_=k#a6o^%-K4{`B1+}O4x zztDT%hVb;v#?j`lTvlFQ3aV#zkX=7;YFLS$uIzb0E3lozs5`Xy zi~vF+%{z9uLjKvKPhP%x5f~7-Gj+%5N`%^=yk*Qn{`> z;xj&ROY6g`iy2a@{O)V(jk&8#hHACVDXey5a+KDod_Z&}kHM}xt7}Md@pil{2x7E~ zL$k^d2@Ec2XskjrN+IILw;#7((abu;OJii&v3?60x>d_Ma(onIPtcVnX@ELF0aL?T zSmWiL3(dOFkt!x=1O!_0n(cAzZW+3nHJ{2S>tgSK?~cFha^y(l@-Mr2W$%MN{#af8J;V*>hdq!gx=d0h$T7l}>91Wh07)9CTX zh2_ZdQCyFOQ)l(}gft0UZG`Sh2`x-w`5vC2UD}lZs*5 zG76$akzn}Xi))L3oGJ75#pcN=cX3!=57$Ha=hQ2^lwdyU#a}4JJOz6ddR%zae%#4& za)bFj)z=YQela(F#Y|Q#dp}PJghITwXouVaMq$BM?K%cXn9^Y@g43$=O)F&ZlOUom zJiad#dea;-eywBA@e&D6Pdso1?2^(pXiN91?jvcaUyYoKUmvl5G9e$W!okWe*@a<^ z8cQQ6cNSf+UPDx%?_G4aIiybZHHagF{;IcD(dPO!#=u zWfqLcPc^+7Uu#l(Bpxft{*4lv#*u7X9AOzDO z1D9?^jIo}?%iz(_dwLa{ex#T}76ZfN_Z-hwpus9y+4xaUu9cX}&P{XrZVWE{1^0yw zO;YhLEW!pJcbCt3L8~a7>jsaN{V3>tz6_7`&pi%GxZ=V3?3K^U+*ryLSb)8^IblJ0 zSRLNDvIxt)S}g30?s_3NX>F?NKIGrG_zB9@Z>uSW3k2es_H2kU;Rnn%j5qP)!XHKE zPB2mHP~tLCg4K_vH$xv`HbRsJwbZMUV(t=ez;Ec(vyHH)FbfLg`c61I$W_uBB>i^r z&{_P;369-&>23R%qNIULe=1~T$(DA`ev*EWZ6j(B$(te}x1WvmIll21zvygkS%vwG zzkR6Z#RKA2!z!C%M!O>!=Gr0(J0FP=-MN=5t-Ir)of50y10W}j`GtRCsXBakrKtG& zazmITDJMA0C51&BnLY)SY9r)NVTMs);1<=oosS9g31l{4ztjD3#+2H7u_|66b|_*O z;Qk6nalpqdHOjx|K&vUS_6ITgGll;TdaN*ta=M_YtyC)I9Tmr~VaPrH2qb6sd~=AcIxV+%z{E&0@y=DPArw zdV7z(G1hBx7hd{>(cr43^WF%4Y@PXZ?wPpj{OQ#tvc$pABJbvPGvdR`cAtHn)cSEV zrpu}1tJwQ3y!mSmH*uz*x0o|CS<^w%&KJzsj~DU0cLQUxk5B!hWE>aBkjJle8z~;s z-!A=($+}Jq_BTK5^B!`R>!MulZN)F=iXXeUd0w5lUsE5VP*H*oCy(;?S$p*TVvTxwAeWFB$jHyb0593)$zqalVlDX=GcCN1gU0 zlgU)I$LcXZ8Oyc2TZYTPu@-;7<4YYB-``Qa;IDcvydIA$%kHhJKV^m*-zxcvU4viy&Kr5GVM{IT>WRywKQ9;>SEiQD*NqplK-KK4YR`p0@JW)n_{TU3bt0 zim%;(m1=#v2}zTps=?fU5w^(*y)xT%1vtQH&}50ZF!9YxW=&7*W($2kgKyz1mUgfs zfV<*XVVIFnohW=|j+@Kfo!#liQR^x>2yQdrG;2o8WZR+XzU_nG=Ed2rK?ntA;K5B{ z>M8+*A4!Jm^Bg}aW?R?6;@QG@uQ8&oJ{hFixcfEnJ4QH?A4>P=q29oDGW;L;= z9-a0;g%c`C+Ai!UmK$NC*4#;Jp<1=TioL=t^YM)<<%u#hnnfSS`nq63QKGO1L8RzX z@MFDqs1z ztYmxDl@LU)5acvHk)~Z`RW7=aJ_nGD!mOSYD>5Odjn@TK#LY{jf?+piB5AM-CAoT_ z?S-*q7}wyLJzK>N%eMPuFgN)Q_otKP;aqy=D5f!7<=n(lNkYRXVpkB{TAYLYg{|(jtRqYmg$xH zjmq?B(RE4 zQx^~Pt}gxC2~l=K$$-sYy_r$CO(d=+b3H1MB*y_5g6WLaWTXn+TKQ|hNY^>Mp6k*$ zwkovomhu776vQATqT4blf~g;TY(MWCrf^^yfWJvSAB$p5l;jm@o#=!lqw+Lqfq>X= z$6~kxfm7`3q4zUEB;u4qa#BdJxO!;xGm)wwuisj{0y2x{R(IGMrsIzDY9LW>m!Y`= z04sx3IjnYvL<4JqxQ8f7qYd0s2Ig%`ytYPEMKI)s(LD}D@EY>x`VFtqvnADNBdeao zC96X+MxnwKmjpg{U&gP3HE}1=s!lv&D{6(g_lzyF3A`7Jn*&d_kL<;dAFx!UZ>hB8 z5A*%LsAn;VLp>3${0>M?PSQ)9s3}|h2e?TG4_F{}{Cs>#3Q*t$(CUc}M)I}8cPF6% z=+h(Kh^8)}gj(0}#e7O^FQ6`~fd1#8#!}LMuo3A0bN`o}PYsm!Y}sdOz$+Tegc=qT z8x`PH$7lvnhJp{kHWb22l;@7B7|4yL4UOOVM0MP_>P%S1Lnid)+k9{+3D+JFa#Pyf zhVc#&df87APl4W9X)F3pGS>@etfl=_E5tBcVoOfrD4hmVeTY-cj((pkn%n@EgN{0f zwb_^Rk0I#iZuHK!l*lN`ceJn(sI{$Fq6nN& zE<-=0_2WN}m+*ivmIOxB@#~Q-cZ>l136w{#TIJe478`KE7@=a{>SzPHsKLzYAyBQO zAtuuF$-JSDy_S@6GW0MOE~R)b;+0f%_NMrW(+V#c_d&U8Z9+ec4=HmOHw?gdjF(Lu zzra83M_BoO-1b3;9`%&DHfuUY)6YDV21P$C!Rc?mv&{lx#f8oc6?0?x zK08{WP65?#>(vPfA-c=MCY|%*1_<3D4NX zeVTi-JGl2uP_2@0F{G({pxQOXt_d{g_CV6b?jNpfUG9;8yle-^4KHRvZs-_2siata zt+d_T@U$&t*xaD22(fH(W1r$Mo?3dc%Tncm=C6{V9y{v&VT#^1L04vDrLM9qBoZ4@ z6DBN#m57hX7$C(=#$Y5$bJmwA$T8jKD8+6A!-IJwA{WOfs%s}yxUw^?MRZjF$n_KN z6`_bGXcmE#5e4Ym)aQJ)xg3Pg0@k`iGuHe?f(5LtuzSq=nS^5z>vqU0EuZ&75V%Z{ zYyhRLN^)$c6Ds{f7*FBpE;n5iglx5PkHfWrj3`x^j^t z7ntuV`g!9Xg#^3!x)l*}IW=(Tz3>Y5l4uGaB&lz{GDjm2D5S$CExLT`I1#n^lBH7Y zDgpMag@`iETKAI=p<5E#LTkwzVR@=yY|uBVI1HG|8h+d;G-qfuj}-ZR6fN>EfCCW z9~wRQoAPEa#aO?3h?x{YvV*d+NtPkf&4V0k4|L=uj!U{L+oLa(z#&iuhJr3-PjO3R z5s?=nn_5^*^Rawr>>Nr@K(jwkB#JK-=+HqwfdO<+P5byeim)wvqGlP-P|~Nse8=XF zz`?RYB|D6SwS}C+YQv+;}k6$-%D(@+t14BL@vM z2q%q?f6D-A5s$_WY3{^G0F131bbh|g!}#BKw=HQ7mx;Dzg4Z*bTLQSfo{ed{4}NZW zfrRm^Ca$rlE{Ue~uYv>R9{3smwATcdM_6+yWIO z*ZRH~uXE@#p$XTbCt5j7j2=86e{9>HIB6xDzV+vAo&B?KUiMP|ttOElepnl%|DPqL b{|{}U^kRn2wo}j7|0ATu<;8xA7zX}7|B6mN literal 0 HcmV?d00001 diff --git a/clients/webassembly/react-example/public/manifest.json b/clients/webassembly/react-example/public/manifest.json new file mode 100644 index 0000000000..080d6c77ac --- /dev/null +++ b/clients/webassembly/react-example/public/manifest.json @@ -0,0 +1,25 @@ +{ + "short_name": "React App", + "name": "Create React App Sample", + "icons": [ + { + "src": "favicon.ico", + "sizes": "64x64 32x32 24x24 16x16", + "type": "image/x-icon" + }, + { + "src": "logo192.png", + "type": "image/png", + "sizes": "192x192" + }, + { + "src": "logo512.png", + "type": "image/png", + "sizes": "512x512" + } + ], + "start_url": ".", + "display": "standalone", + "theme_color": "#000000", + "background_color": "#ffffff" +} diff --git a/clients/webassembly/react-example/public/robots.txt b/clients/webassembly/react-example/public/robots.txt new file mode 100644 index 0000000000..e9e57dc4d4 --- /dev/null +++ b/clients/webassembly/react-example/public/robots.txt @@ -0,0 +1,3 @@ +# https://www.robotstxt.org/robotstxt.html +User-agent: * +Disallow: diff --git a/clients/webassembly/react-example/src/App.js b/clients/webassembly/react-example/src/App.js new file mode 100644 index 0000000000..40a96890fb --- /dev/null +++ b/clients/webassembly/react-example/src/App.js @@ -0,0 +1,197 @@ +import React from 'react'; +import { + Button, + Card, + CardHeader, + CardBody, + FormGroup, + Form, + Input, + InputGroup, + Row, + Col, + Container, + Table +} from 'reactstrap'; + +class App extends React.Component { + constructor(props) { + super(props); + this.handleChangeReceiver = this.handleChangeReceiver.bind(this); + this.handleChangeMessage = this.handleChangeMessage.bind(this); + this.receivedMessage = this.receivedMessage.bind(this); + this.state = { + nymClient: "", + sender: "", + receiver: "", + message: "Hello mixnet!", + transfers: [], + } + } + componentDidMount() { + this.loadWasm(); + } + handleChangeReceiver(e) { + this.setState({receiver: e.target.value}); + } + handleChangeMessage(e) { + this.setState({message: e.target.value}); + } + loadWasm = async () => { + try { + const wasm = await import('@nymproject/nym-client-wasm/client'); + this.setState({wasm}); + // Set up identity and client + let directory = "https://directory.nymtech.net"; + let identity = new wasm.Identity(); + let _nymClient = new wasm.Client(directory, identity, null); + _nymClient.onText = this.receivedMessage; + this.setState({nymClient: _nymClient}); + // Start the Nym client. Connects to a Nym gateway via websocket. + await _nymClient.start(); + let _sender = _nymClient.formatAsRecipient(); + this.setState({sender: _sender}); + } catch(err) { + console.error(`Unexpected error in loadWasm. [Message: ${err.message}]`); + } + } + sendmessage(message, receiver) { + this.state.nymClient.sendMessage(message, receiver); + let timestamp = new Date().toISOString().substr(11, 12); + this.setState({ + transfers: this.state.transfers.concat({time: timestamp, direction: "sent", message: message}) + }) + } + receivedMessage (message) { + let timestamp = new Date().toISOString().substr(11, 12); + this.setState({ + transfers: this.state.transfers.concat({time: timestamp, direction: "received", message: message}) + }) + } + renderTableData() { + return this.state.transfers.map((transfers, index) => { + const { time, direction, message } = transfers //destructuring + + return ( + + {direction === "sent" ? + <> + {time} + {direction} + {message} + + : + <> + {time} + {direction} + {message} + + } + + ) + }) + } + render() { + return ( + <> +
+
+ +
+ + +

NYM

+

+ Example react peaps implementation +

+ +
+
+
+
+ {/* Page content */} + + + + + +
+ Test NYM by sending a private message +
+
+ +
+ + + + + + + + + + + + + + + +
+ +
+
+
+
+ + + + +
+ Message history +
+
+ + + + + + + + + + + {this.renderTableData()} + +
TimeIn/OutMessage +
+
+
+ +
+
+
+ + ); + } +} + +export default App; diff --git a/clients/webassembly/react-example/src/App.test.js b/clients/webassembly/react-example/src/App.test.js new file mode 100644 index 0000000000..4db7ebc25c --- /dev/null +++ b/clients/webassembly/react-example/src/App.test.js @@ -0,0 +1,9 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import App from './App'; + +test('renders learn react link', () => { + const { getByText } = render(); + const linkElement = getByText(/learn react/i); + expect(linkElement).toBeInTheDocument(); +}); diff --git a/clients/webassembly/react-example/src/index.js b/clients/webassembly/react-example/src/index.js new file mode 100644 index 0000000000..bca5680c38 --- /dev/null +++ b/clients/webassembly/react-example/src/index.js @@ -0,0 +1,19 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import 'bootstrap/dist/css/bootstrap.css'; +import "./scss/example-react.scss"; + +import App from './App'; +import * as serviceWorker from './serviceWorker'; + +ReactDOM.render( + + + , + document.getElementById('root') +); + +// If you want your app to work offline and load faster, you can change +// unregister() to register() below. Note this comes with some pitfalls. +// Learn more about service workers: https://bit.ly/CRA-PWA +serviceWorker.unregister(); diff --git a/clients/webassembly/react-example/src/logo.svg b/clients/webassembly/react-example/src/logo.svg new file mode 100644 index 0000000000..6b60c1042f --- /dev/null +++ b/clients/webassembly/react-example/src/logo.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/clients/webassembly/react-example/src/scss/example-react.scss b/clients/webassembly/react-example/src/scss/example-react.scss new file mode 100644 index 0000000000..e3c1707b3c --- /dev/null +++ b/clients/webassembly/react-example/src/scss/example-react.scss @@ -0,0 +1,21 @@ +// Core +@import "implementation/custom/functions"; +@import "implementation/custom/variables"; +@import "implementation/custom/mixins"; + +// bootstrap (4.3.1) components +@import "implementation/bootstrap/reboot"; +@import "implementation/bootstrap/tables"; +@import "implementation/bootstrap/forms"; +@import "implementation/bootstrap/buttons"; +@import "implementation/bootstrap/card"; +@import "implementation/bootstrap/utilities"; + + +// Utilities and components +@import "implementation/custom/utilities"; +@import "implementation/custom/components"; + + + + diff --git a/clients/webassembly/react-example/src/scss/implementation/bootstrap/_buttons.scss b/clients/webassembly/react-example/src/scss/implementation/bootstrap/_buttons.scss new file mode 100644 index 0000000000..0a8eaa9b64 --- /dev/null +++ b/clients/webassembly/react-example/src/scss/implementation/bootstrap/_buttons.scss @@ -0,0 +1,143 @@ +// stylelint-disable selector-no-qualifying-type + +// +// Base styles +// + +.btn { + display: inline-block; + font-weight: $btn-font-weight; + text-align: center; + white-space: nowrap; + vertical-align: middle; + user-select: none; + border: $btn-border-width solid transparent; + @include button-size($btn-padding-y, $btn-padding-x, $font-size-base, $btn-line-height, $btn-border-radius); + @include transition($btn-transition); + + // Share hover and focus styles + @include hover-focus { + text-decoration: none; + } + + &:focus, + &.focus { + outline: 0; + box-shadow: $btn-focus-box-shadow; + } + + // Disabled comes first so active can properly restyle + &.disabled, + &:disabled { + opacity: $btn-disabled-opacity; + @include box-shadow(none); + } + + // Opinionated: add "hand" cursor to non-disabled .btn elements + &:not(:disabled):not(.disabled) { + cursor: pointer; + } + + &:not(:disabled):not(.disabled):active, + &:not(:disabled):not(.disabled).active { + @include box-shadow($btn-active-box-shadow); + + &:focus { + @include box-shadow($btn-focus-box-shadow, $btn-active-box-shadow); + } + } +} + +// Future-proof disabling of clicks on `` elements +a.btn.disabled, +fieldset:disabled a.btn { + pointer-events: none; +} + + +// +// Alternate buttons +// + +@each $color, $value in $theme-colors { + .btn-#{$color} { + @include button-variant($value, $value); + } +} + +@each $color, $value in $theme-colors { + .btn-outline-#{$color} { + @include button-outline-variant($value); + } +} + + +// +// Link buttons +// + +// Make a button look and behave like a link +.btn-link { + font-weight: $font-weight-normal; + color: $link-color; + background-color: transparent; + + @include hover { + color: $link-hover-color; + text-decoration: $link-hover-decoration; + background-color: transparent; + border-color: transparent; + } + + &:focus, + &.focus { + text-decoration: $link-hover-decoration; + border-color: transparent; + box-shadow: none; + } + + &:disabled, + &.disabled { + color: $btn-link-disabled-color; + pointer-events: none; + } + + // No need for an active state here +} + + +// +// Button Sizes +// + +.btn-lg { + @include button-size($btn-padding-y-lg, $btn-padding-x-lg, $font-size-lg, $btn-line-height-lg, $btn-border-radius-lg); +} + +.btn-sm { + @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $font-size-sm, $btn-line-height-sm, $btn-border-radius-sm); +} + + +// +// Block button +// + +.btn-block { + display: block; + width: 100%; + + // Vertically space out multiple block buttons + + .btn-block { + margin-top: $btn-block-spacing-y; + } +} + +// Specificity overrides +input[type="submit"], +input[type="reset"], +input[type="button"] { + &.btn-block { + width: 100%; + } +} diff --git a/clients/webassembly/react-example/src/scss/implementation/bootstrap/_card.scss b/clients/webassembly/react-example/src/scss/implementation/bootstrap/_card.scss new file mode 100644 index 0000000000..28d7e62447 --- /dev/null +++ b/clients/webassembly/react-example/src/scss/implementation/bootstrap/_card.scss @@ -0,0 +1,301 @@ +// +// Base styles +// + +.card { + position: relative; + display: flex; + flex-direction: column; + min-width: 0; + word-wrap: break-word; + background-color: $card-bg; + background-clip: border-box; + border: $card-border-width solid $card-border-color; + @include border-radius($card-border-radius); + + > hr { + margin-right: 0; + margin-left: 0; + } + + > .list-group:first-child { + .list-group-item:first-child { + @include border-top-radius($card-border-radius); + } + } + + > .list-group:last-child { + .list-group-item:last-child { + @include border-bottom-radius($card-border-radius); + } + } +} + +.card-body { + // Enable `flex-grow: 1` for decks and groups so that card blocks take up + // as much space as possible, ensuring footers are aligned to the bottom. + flex: 1 1 auto; + padding: $card-spacer-x; +} + +.card-title { + margin-bottom: $card-spacer-y; +} + +.card-subtitle { + margin-top: -($card-spacer-y / 2); + margin-bottom: 0; +} + +.card-text:last-child { + margin-bottom: 0; +} + +.card-link { + @include hover { + text-decoration: none; + } + + + .card-link { + margin-left: $card-spacer-x; + } +} + +// +// Optional textual caps +// + +.card-header { + padding: $card-spacer-y $card-spacer-x; + margin-bottom: 0; // Removes the default margin-bottom of + background-color: $card-cap-bg; + border-bottom: $card-border-width solid $card-border-color; + + &:first-child { + @include border-radius($card-inner-border-radius $card-inner-border-radius 0 0); + } + + + .list-group { + .list-group-item:first-child { + border-top: 0; + } + } +} + +.card-footer { + padding: $card-spacer-y $card-spacer-x; + background-color: $card-cap-bg; + border-top: $card-border-width solid $card-border-color; + + &:last-child { + @include border-radius(0 0 $card-inner-border-radius $card-inner-border-radius); + } +} + + +// +// Header navs +// + +.card-header-tabs { + margin-right: -($card-spacer-x / 2); + margin-bottom: -$card-spacer-y; + margin-left: -($card-spacer-x / 2); + border-bottom: 0; +} + +.card-header-pills { + margin-right: -($card-spacer-x / 2); + margin-left: -($card-spacer-x / 2); +} + +// Card image +.card-img-overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + padding: $card-img-overlay-padding; +} + +.card-img { + width: 100%; // Required because we use flexbox and this inherently applies align-self: stretch + @include border-radius($card-inner-border-radius); +} + +// Card image caps +.card-img-top { + width: 100%; // Required because we use flexbox and this inherently applies align-self: stretch + @include border-top-radius($card-inner-border-radius); +} + +.card-img-bottom { + width: 100%; // Required because we use flexbox and this inherently applies align-self: stretch + @include border-bottom-radius($card-inner-border-radius); +} + + +// Card deck + +.card-deck { + display: flex; + flex-direction: column; + + .card { + margin-bottom: $card-deck-margin; + } + + @include media-breakpoint-up(sm) { + flex-flow: row wrap; + margin-right: -$card-deck-margin; + margin-left: -$card-deck-margin; + + .card { + display: flex; + // Flexbugs #4: https://github.com/philipwalton/flexbugs#flexbug-4 + flex: 1 0 0%; + flex-direction: column; + margin-right: $card-deck-margin; + margin-bottom: 0; // Override the default + margin-left: $card-deck-margin; + } + } +} + + +// +// Card groups +// + +.card-group { + display: flex; + flex-direction: column; + + // The child selector allows nested `.card` within `.card-group` + // to display properly. + > .card { + margin-bottom: $card-group-margin; + } + + @include media-breakpoint-up(sm) { + flex-flow: row wrap; + // The child selector allows nested `.card` within `.card-group` + // to display properly. + > .card { + // Flexbugs #4: https://github.com/philipwalton/flexbugs#flexbug-4 + flex: 1 0 0%; + margin-bottom: 0; + + + .card { + margin-left: 0; + border-left: 0; + } + + // Handle rounded corners + @if $enable-rounded { + &:first-child { + @include border-right-radius(0); + + .card-img-top, + .card-header { + border-top-right-radius: 0; + } + .card-img-bottom, + .card-footer { + border-bottom-right-radius: 0; + } + } + + &:last-child { + @include border-left-radius(0); + + .card-img-top, + .card-header { + border-top-left-radius: 0; + } + .card-img-bottom, + .card-footer { + border-bottom-left-radius: 0; + } + } + + &:only-child { + @include border-radius($card-border-radius); + + .card-img-top, + .card-header { + @include border-top-radius($card-border-radius); + } + .card-img-bottom, + .card-footer { + @include border-bottom-radius($card-border-radius); + } + } + + &:not(:first-child):not(:last-child):not(:only-child) { + @include border-radius(0); + + .card-img-top, + .card-img-bottom, + .card-header, + .card-footer { + @include border-radius(0); + } + } + } + } + } +} + + +// +// Columns +// + +.card-columns { + .card { + margin-bottom: $card-columns-margin; + } + + @include media-breakpoint-up(sm) { + column-count: $card-columns-count; + column-gap: $card-columns-gap; + orphans: 1; + widows: 1; + + .card { + display: inline-block; // Don't let them vertically span multiple columns + width: 100%; // Don't let their width change + } + } +} + + +// +// Accordion +// + +.accordion { + .card:not(:first-of-type):not(:last-of-type) { + border-bottom: 0; + border-radius: 0; + } + + .card:not(:first-of-type) { + .card-header:first-child { + border-radius: 0; + } + } + + .card:first-of-type { + border-bottom: 0; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; + } + + .card:last-of-type { + border-top-left-radius: 0; + border-top-right-radius: 0; + } +} diff --git a/clients/webassembly/react-example/src/scss/implementation/bootstrap/_forms.scss b/clients/webassembly/react-example/src/scss/implementation/bootstrap/_forms.scss new file mode 100644 index 0000000000..5530630ed6 --- /dev/null +++ b/clients/webassembly/react-example/src/scss/implementation/bootstrap/_forms.scss @@ -0,0 +1,333 @@ +// stylelint-disable selector-no-qualifying-type + +// +// Textual form controls +// + +.form-control { + display: block; + width: 100%; + height: $input-height; + padding: $input-padding-y $input-padding-x; + font-size: $font-size-base; + line-height: $input-line-height; + color: $input-color; + background-color: $input-bg; + background-clip: padding-box; + border: $input-border-width solid $input-border-color; + + // Note: This has no effect on `s in CSS. + @if $enable-rounded { + // Manually use the if/else instead of the mixin to account for iOS override + border-radius: $input-border-radius; + } @else { + // Otherwise undo the iOS default + border-radius: 0; + } + + @include box-shadow($input-box-shadow); + @include transition($input-transition); + + // Unstyle the caret on ` receives focus + // in IE and (under certain conditions) Edge, as it looks bad and cannot be made to + // match the appearance of the native widget. + // See https://github.com/twbs/bootstrap/issues/19398. + color: $input-color; + background-color: $input-bg; + } +} + +// Make file inputs better match text inputs by forcing them to new lines. +.form-control-file, +.form-control-range { + display: block; + width: 100%; +} + + +// +// Labels +// + +// For use with horizontal and inline forms, when you need the label (or legend) +// text to align with the form controls. +.col-form-label { + padding-top: calc(#{$input-padding-y} + #{$input-border-width}); + padding-bottom: calc(#{$input-padding-y} + #{$input-border-width}); + margin-bottom: 0; // Override the `