Compare commits

..

45 Commits

Author SHA1 Message Date
Jon Häggblad c87410dbdd Fix variable name 2024-09-23 16:02:54 +02:00
Jon Häggblad ed8c700d00 Extendd UserAgent with system information collected at runtime 2024-09-23 11:08:49 +02:00
Drazen Urch 2f267cf787 Update network monitor entrypoint (#4893)
* Update entrypoint

* Update CI action

* Rollback ci changes
2024-09-20 10:58:50 +02:00
Jędrzej Stuczyński 0d2418ef6a Merge pull request #4885 from nymtech/feature/updated-gateway-registration
Feature/updated gateway registration
2024-09-20 09:09:28 +01:00
Bogdan-Ștefan Neacşu 6f0c8dbe73 Fix missing duplication of modified tables (#4904) 2024-09-19 18:25:21 +02:00
mx 2198c1bd7b added new instructions for building locally (#4902) 2024-09-19 15:48:51 +00:00
Jędrzej Stuczyński be7f00fe52 replaced an assertion with an error return instead 2024-09-19 15:59:04 +01:00
Jon Häggblad 35c94f5c4b Update cargo deny (#4901)
* Regenerate deny.toml

* Backport old settings to deny.toml

* Explicitly allow GPL-3 only on our own specific crates

* Update deny.toml for latest changes

* Fix cargo-deny warnings for duplicate crates

* Update cargo-deny-action to v2
2024-09-19 12:53:27 +02:00
Jędrzej Stuczyński f5863b9668 fixed client key upgrade due to extra Arc 2024-09-19 11:06:50 +01:00
dependabot[bot] 963c54fea2 build(deps): bump semver from 0.11.0 to 1.0.23 (#4881)
* build(deps): bump semver from 0.11.0 to 1.0.23

Bumps [semver](https://github.com/dtolnay/semver) from 0.11.0 to 1.0.23.
- [Release notes](https://github.com/dtolnay/semver/releases)
- [Commits](https://github.com/dtolnay/semver/compare/0.11.0...1.0.23)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Update for 1.0

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jon Häggblad <jon.haggblad@gmail.com>
2024-09-19 11:55:10 +02:00
dependabot[bot] db55a96f91 build(deps): bump toml from 0.5.11 to 0.8.14 (#4805)
* build(deps): bump toml from 0.5.11 to 0.8.14

Bumps [toml](https://github.com/toml-rs/toml) from 0.5.11 to 0.8.14.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.5.11...toml-v0.8.14)

---
updated-dependencies:
- dependency-name: toml
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* Use workspace dependency

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jon Häggblad <jon.haggblad@gmail.com>
2024-09-19 11:10:34 +02:00
Jędrzej Stuczyński 7c0235ab26 fixed wasm build and trait impl 2024-09-19 10:06:59 +01:00
dependabot[bot] 92af6f7024 build(deps): bump hyper from 1.3.1 to 1.4.1 (#4879)
Bumps [hyper](https://github.com/hyperium/hyper) from 1.3.1 to 1.4.1.
- [Release notes](https://github.com/hyperium/hyper/releases)
- [Changelog](https://github.com/hyperium/hyper/blob/master/CHANGELOG.md)
- [Commits](https://github.com/hyperium/hyper/compare/v1.3.1...v1.4.1)

---
updated-dependencies:
- dependency-name: hyper
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-19 11:06:49 +02:00
Sachin Kamath 7146c4c012 docs: add hostname instructions for wss (#4900) 2024-09-19 08:55:28 +00:00
Jędrzej Stuczyński b3d7c26443 added key upgrade mechanism 2024-09-18 17:43:47 +01:00
Jędrzej Stuczyński 9efeef881a split types.rs + added additional helpers 2024-09-18 17:43:44 +01:00
Jędrzej Stuczyński 9d8369a5b2 generate pseudorandom salt for deriving aes256gcm-siv key 2024-09-18 17:43:43 +01:00
Jędrzej Stuczyński cc32eb3904 fixed wasm build 2024-09-18 17:43:43 +01:00
Jędrzej Stuczyński 8cf4977021 assert new gateway keys zeroize on drop 2024-09-18 17:43:43 +01:00
Jędrzej Stuczyński 2c2748832c cargo fmt 2024-09-18 17:43:42 +01:00
Jędrzej Stuczyński 114db3c1cf post-rebasing fixes 2024-09-18 17:43:42 +01:00
Jędrzej Stuczyński a65df5a0ab clippy 2024-09-18 17:43:42 +01:00
Jędrzej Stuczyński b6f07fbfce warning for unimplemented upgrade 2024-09-18 17:43:42 +01:00
Jędrzej Stuczyński c39d42b7dd fixed deserialisation of updated gateway shared materials 2024-09-18 17:43:41 +01:00
Jędrzej Stuczyński 21e9df488f compatibility with legacy clients 2024-09-18 17:43:40 +01:00
Jędrzej Stuczyński 94113206b2 completing handshake using legacy keys 2024-09-18 17:43:07 +01:00
Jędrzej Stuczyński 71532484a9 updated client handshake to allow derivation of different key types 2024-09-18 17:43:07 +01:00
Jędrzej Stuczyński 8756763875 added support for aead in nym-crypto 2024-09-18 17:43:06 +01:00
Jon Häggblad 9213e02b43 Remove clippy annotation (#4896) 2024-09-18 11:47:41 +02:00
Jon Häggblad ede4b23e8a Fix clippy::too-long-first-doc-paragraph (#4897) 2024-09-18 10:25:49 +02:00
Jon Häggblad 2e95ea16f9 Update nym-vpn metapackage and replace nymvpn-x with nym-vpn-app (#4889)
* Update nym-vpn metapackage to 0.2.0 and replace nymvpn-x with nym-vpn-app

* Fix compression

* Update description
2024-09-18 09:16:17 +01:00
dependabot[bot] 0c955817fd build(deps): bump the patch-updates group across 1 directory with 9 updates (#4898)
Bumps the patch-updates group with 9 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.87` | `1.0.89` |
| [clap_complete](https://github.com/clap-rs/clap) | `4.5.5` | `4.5.28` |
| [clap_complete_fig](https://github.com/clap-rs/clap) | `4.5.1` | `4.5.2` |
| [curve25519-dalek](https://github.com/dalek-cryptography/curve25519-dalek) | `4.1.2` | `4.1.3` |
| [getset](https://github.com/jbaublitz/getset) | `0.1.2` | `0.1.3` |
| [log](https://github.com/rust-lang/log) | `0.4.21` | `0.4.22` |
| [quote](https://github.com/dtolnay/quote) | `1.0.36` | `1.0.37` |
| [safer-ffi](https://github.com/getditto/safer_ffi) | `0.1.12` | `0.1.13` |
| [url](https://github.com/servo/rust-url) | `2.5.1` | `2.5.2` |



Updates `anyhow` from 1.0.87 to 1.0.89
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.87...1.0.89)

Updates `clap_complete` from 4.5.5 to 4.5.28
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete-v4.5.5...clap_complete-v4.5.28)

Updates `clap_complete_fig` from 4.5.1 to 4.5.2
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/clap-rs/clap/compare/clap_complete_fig-v4.5.1...clap_complete_fig-v4.5.2)

Updates `curve25519-dalek` from 4.1.2 to 4.1.3
- [Release notes](https://github.com/dalek-cryptography/curve25519-dalek/releases)
- [Commits](https://github.com/dalek-cryptography/curve25519-dalek/compare/curve25519-4.1.2...curve25519-4.1.3)

Updates `getset` from 0.1.2 to 0.1.3
- [Release notes](https://github.com/jbaublitz/getset/releases)
- [Commits](https://github.com/jbaublitz/getset/compare/0.1.2...0.1.3)

Updates `log` from 0.4.21 to 0.4.22
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.21...0.4.22)

Updates `quote` from 1.0.36 to 1.0.37
- [Release notes](https://github.com/dtolnay/quote/releases)
- [Commits](https://github.com/dtolnay/quote/compare/1.0.36...1.0.37)

Updates `safer-ffi` from 0.1.12 to 0.1.13
- [Release notes](https://github.com/getditto/safer_ffi/releases)
- [Commits](https://github.com/getditto/safer_ffi/commits)

Updates `url` from 2.5.1 to 2.5.2
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/v2.5.1...v2.5.2)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: clap_complete
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: clap_complete_fig
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: curve25519-dalek
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: getset
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: log
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: quote
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: safer-ffi
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
- dependency-name: url
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: patch-updates
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-18 09:15:11 +02:00
dependabot[bot] ec3c4fb1aa build(deps): bump sysinfo from 0.30.12 to 0.30.13 (#4880)
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.30.12 to 0.30.13.
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/commits/v0.30.13)

---
updated-dependencies:
- dependency-name: sysinfo
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-18 00:33:44 +02:00
dependabot[bot] 789221f144 build(deps): bump comfy-table from 6.2.0 to 7.1.1 (#4882)
Bumps [comfy-table](https://github.com/nukesor/comfy-table) from 6.2.0 to 7.1.1.
- [Release notes](https://github.com/nukesor/comfy-table/releases)
- [Changelog](https://github.com/Nukesor/comfy-table/blob/main/CHANGELOG.md)
- [Commits](https://github.com/nukesor/comfy-table/compare/v6.2.0...v7.1.1)

---
updated-dependencies:
- dependency-name: comfy-table
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-18 00:30:46 +02:00
Jon Häggblad 5b925d8b68 Fix apt install in ci-build-upload-binaries.yml (#4894) 2024-09-17 23:29:36 +02:00
Drazen Urch 6598d677da Build and Push CI (#4887) 2024-09-17 10:26:45 +02:00
import this e736a01ecc [DOCs/operators]: Document changelog for patch/2024.10-caramello (#4886)
* changelog for patched release

* fix typo
2024-09-17 08:26:22 +00:00
Jędrzej Stuczyński a708fa2d4a Merge pull request #4873 from nymtech/feature/stateless-gateway-requests
allow clients to send stateless gateway requests without prior registration
2024-09-16 17:00:15 +01:00
Drazen Urch a512217382 Few fixes (#4883) 2024-09-16 17:15:40 +02:00
Jon Häggblad 086611c7ac Use serde from workspace (#4833)
* cargo autoinherit for serde

* cargo autoinherit for bs58 and vergen in cosmwasm-smart-contracts
2024-09-16 11:16:21 +02:00
import this 05d6652177 [DOCs/operators]: Post release docs updates (#4874)
* update proxy setup syntax

* update known errors and bugs

* docs: simplify wss

---------

Co-authored-by: Sachin Kamath <github@skamath.me>
2024-09-13 13:16:34 +00:00
Bogdan-Ștefan Neacşu 9c514fe3b7 Fix snake case serde (#4875) 2024-09-13 11:53:39 +02:00
Jędrzej Stuczyński 924160b3e7 removed unused import 2024-09-12 17:29:34 +01:00
Jędrzej Stuczyński 23d14b60de allow handling multiple of stateless requests on the same underlying connection 2024-09-12 16:54:56 +01:00
Jędrzej Stuczyński a4b47ef3a5 allow clients to send stateless gateway requests without prior registration 2024-09-12 15:57:38 +01:00
120 changed files with 3710 additions and 2184 deletions
@@ -56,7 +56,7 @@ jobs:
echo $OUTPUT_DIR
- name: Install Dependencies (Linux)
run: sudo apt update && sudo apt install libudev-dev
run: sudo apt-get update && sudo apt-get -y install libudev-dev
- name: Sets env vars for tokio if set in manual dispatch inputs
run: |
-8
View File
@@ -90,14 +90,6 @@ jobs:
command: test
args: --workspace -- --ignored
- name: Annotate with clippy checks
if: contains(matrix.os, 'ubuntu')
uses: actions-rs/clippy-check@v1
continue-on-error: true
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --workspace
- name: Clippy
uses: actions-rs/cargo@v1
with:
+1 -1
View File
@@ -18,7 +18,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: EmbarkStudios/cargo-deny-action@v1
- uses: EmbarkStudios/cargo-deny-action@v2
with:
log-level: warn
command: check ${{ matrix.checks }}
@@ -0,0 +1,55 @@
name: Build and upload Network monitor container to harbor.nymte.ch
on:
workflow_dispatch:
env:
WORKING_DIRECTORY: "."
CONTAINER_NAME: "network-monitor"
jobs:
build-container:
runs-on: arc-ubuntu-22.04-dind
steps:
- name: Login to Harbor
uses: docker/login-action@v3
with:
registry: harbor.nymte.ch
username: ${{ secrets.HARBOR_ROBOT_USERNAME }}
password: ${{ secrets.HARBOR_ROBOT_SECRET }}
- name: Checkout repo
uses: actions/checkout@v4
- name: Configure git identity
run: |
git config --global user.email "lawrence@nymtech.net"
git config --global user.name "Lawrence Stalder"
- name: Get version from package.json
uses: sergeysova/jq-action@v2
id: get_version
with:
cmd: jq -r '.version' ${{ env.WORKING_DIRECTORY }}/package.json
- name: Check if tag exists
run: |
if git rev-parse ${{ steps.get_version.outputs.value }} >/dev/null 2>&1; then
echo "Tag ${{ steps.get_version.outputs.value }} already exists"
fi
- name: Remove existing tag if exists
run: |
if git rev-parse ${{ steps.get_version.outputs.value }} >/dev/null 2>&1; then
git push --delete origin ${{ steps.get_version.outputs.value }}
git tag -d ${{ steps.get_version.outputs.value }}
fi
- name: Create tag
run: |
git tag -a ${{ steps.get_version.outputs.value }} -m "Version ${{ steps.get_version.outputs.value }}"
git push origin ${{ steps.get_version.outputs.value }}
- name: BuildAndPushImageOnHarbor
run: |
docker build -f nym-network-monitor.dockerfile ${{ env.WORKING_DIRECTORY }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:${{ steps.get_version.outputs.value }} -t harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }}:latest
docker push harbor.nymte.ch/nym/${{ env.CONTAINER_NAME }} --all-tags
Generated
+103 -405
View File
@@ -86,6 +86,21 @@ dependencies = [
"subtle 2.5.0",
]
[[package]]
name = "aes-gcm-siv"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d"
dependencies = [
"aead",
"aes",
"cipher",
"ctr",
"polyval",
"subtle 2.5.0",
"zeroize",
]
[[package]]
name = "ahash"
version = "0.7.8"
@@ -213,9 +228,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.87"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10f00e1f6e58a40e807377c75c6a7f97bf9044fab57816f2414e6f5f4499d7b8"
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
[[package]]
name = "arbitrary"
@@ -393,7 +408,7 @@ dependencies = [
"http 1.1.0",
"http-body 1.0.0",
"http-body-util",
"hyper 1.3.1",
"hyper 1.4.1",
"hyper-util",
"itoa",
"matchit",
@@ -952,18 +967,18 @@ dependencies = [
[[package]]
name = "clap_complete"
version = "4.5.5"
version = "4.5.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2020fa13af48afc65a9a87335bda648309ab3d154cd03c7ff95b378c7ed39c4"
checksum = "9b378c786d3bde9442d2c6dd7e6080b2a818db2b96e30d6e7f1b6d224eb617d3"
dependencies = [
"clap 4.5.17",
]
[[package]]
name = "clap_complete_fig"
version = "4.5.1"
version = "4.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb4bc503cddc1cd320736fb555d6598309ad07c2ddeaa23891a10ffb759ee612"
checksum = "d494102c8ff3951810c72baf96910b980fb065ca5d3101243e6a8dc19747c86b"
dependencies = [
"clap 4.5.17",
"clap_complete",
@@ -1022,18 +1037,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "comfy-table"
version = "6.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e959d788268e3bf9d35ace83e81b124190378e4c91c9067524675e33394b8ba"
dependencies = [
"crossterm 0.26.1",
"strum 0.24.1",
"strum_macros 0.24.3",
"unicode-width",
]
[[package]]
name = "comfy-table"
version = "7.1.1"
@@ -1471,22 +1474,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "crossterm"
version = "0.26.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13"
dependencies = [
"bitflags 1.3.2",
"crossterm_winapi",
"libc",
"mio 0.8.11",
"parking_lot 0.12.3",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm"
version = "0.27.0"
@@ -1633,16 +1620,15 @@ dependencies = [
[[package]]
name = "curve25519-dalek"
version = "4.1.2"
version = "4.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348"
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
dependencies = [
"cfg-if",
"cpufeatures",
"curve25519-dalek-derive",
"digest 0.10.7",
"fiat-crypto",
"platforms",
"rustc_version 0.4.0",
"serde",
"subtle 2.5.0",
@@ -2104,7 +2090,7 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
dependencies = [
"curve25519-dalek 4.1.2",
"curve25519-dalek 4.1.3",
"ed25519",
"rand_core 0.6.4",
"serde",
@@ -2303,6 +2289,12 @@ dependencies = [
"ext-trait",
]
[[package]]
name = "extern-c"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320bea982e85d42441eb25c49b41218e7eaa2657e8f90bc4eca7437376751e23"
[[package]]
name = "eyre"
version = "0.6.12"
@@ -2365,7 +2357,7 @@ dependencies = [
"atomic 0.6.0",
"pear",
"serde",
"toml 0.8.14",
"toml",
"uncased",
"version_check",
]
@@ -2626,14 +2618,14 @@ dependencies = [
[[package]]
name = "getset"
version = "0.1.2"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9"
checksum = "f636605b743120a8d32ed92fc27b6cde1a769f8f936c065151eb66f88ded513c"
dependencies = [
"proc-macro-error",
"proc-macro-error2",
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.66",
]
[[package]]
@@ -3052,9 +3044,9 @@ dependencies = [
[[package]]
name = "hyper"
version = "1.3.1"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05"
dependencies = [
"bytes",
"futures-channel",
@@ -3092,7 +3084,7 @@ checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c"
dependencies = [
"futures-util",
"http 1.1.0",
"hyper 1.3.1",
"hyper 1.4.1",
"hyper-util",
"rustls 0.22.4",
"rustls-pki-types",
@@ -3124,7 +3116,7 @@ dependencies = [
"futures-util",
"http 1.1.0",
"http-body 1.0.0",
"hyper 1.3.1",
"hyper 1.4.1",
"pin-project-lite",
"socket2",
"tokio",
@@ -3156,124 +3148,6 @@ dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
dependencies = [
"displaydoc",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_locid"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
dependencies = [
"displaydoc",
"litemap",
"tinystr",
"writeable",
"zerovec",
]
[[package]]
name = "icu_locid_transform"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
dependencies = [
"displaydoc",
"icu_locid",
"icu_locid_transform_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_locid_transform_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
[[package]]
name = "icu_normalizer"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
dependencies = [
"displaydoc",
"icu_collections",
"icu_normalizer_data",
"icu_properties",
"icu_provider",
"smallvec",
"utf16_iter",
"utf8_iter",
"write16",
"zerovec",
]
[[package]]
name = "icu_normalizer_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
[[package]]
name = "icu_properties"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036"
dependencies = [
"displaydoc",
"icu_collections",
"icu_locid_transform",
"icu_properties_data",
"icu_provider",
"tinystr",
"zerovec",
]
[[package]]
name = "icu_properties_data"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
[[package]]
name = "icu_provider"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
dependencies = [
"displaydoc",
"icu_locid",
"icu_provider_macros",
"stable_deref_trait",
"tinystr",
"writeable",
"yoke",
"zerofrom",
"zerovec",
]
[[package]]
name = "icu_provider_macros"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
]
[[package]]
name = "ident_case"
version = "1.0.1"
@@ -3292,14 +3166,12 @@ dependencies = [
[[package]]
name = "idna"
version = "1.0.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed"
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
dependencies = [
"icu_normalizer",
"icu_properties",
"smallvec",
"utf8_iter",
"unicode-bidi",
"unicode-normalization",
]
[[package]]
@@ -3724,12 +3596,6 @@ dependencies = [
"keystream",
]
[[package]]
name = "litemap"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
[[package]]
name = "lock_api"
version = "0.4.12"
@@ -3742,9 +3608,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.21"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "loom"
@@ -4426,7 +4292,7 @@ dependencies = [
"opentelemetry-jaeger",
"pretty_env_logger",
"schemars",
"semver 0.11.0",
"semver 1.0.23",
"serde",
"serde_json",
"tracing-opentelemetry",
@@ -4487,7 +4353,7 @@ dependencies = [
"cfg-if",
"clap 4.5.17",
"colored",
"comfy-table 6.2.0",
"comfy-table",
"cosmrs 0.17.0-pre",
"cosmwasm-std",
"csv",
@@ -4527,7 +4393,7 @@ dependencies = [
"thiserror",
"time",
"tokio",
"toml 0.8.14",
"toml",
"url",
"zeroize",
]
@@ -4578,12 +4444,12 @@ dependencies = [
"bs58",
"cfg-if",
"clap 4.5.17",
"comfy-table 7.1.1",
"comfy-table",
"futures",
"gloo-timers",
"http-body-util",
"humantime-serde",
"hyper 1.3.1",
"hyper 1.4.1",
"hyper-util",
"log",
"nym-bandwidth-controller",
@@ -4790,7 +4656,7 @@ dependencies = [
"log",
"nym-network-defaults",
"serde",
"toml 0.7.8",
"toml",
"url",
]
@@ -4919,7 +4785,9 @@ dependencies = [
name = "nym-crypto"
version = "0.4.0"
dependencies = [
"aead",
"aes",
"aes-gcm-siv",
"blake3",
"bs58",
"cipher",
@@ -5104,7 +4972,6 @@ dependencies = [
"futures",
"getrandom",
"gloo-utils 0.2.0",
"log",
"nym-bandwidth-controller",
"nym-credential-storage",
"nym-credentials",
@@ -5123,12 +4990,14 @@ dependencies = [
"tokio",
"tokio-stream",
"tokio-tungstenite",
"tracing",
"tungstenite 0.20.1",
"url",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasm-utils",
"wasmtimer",
"zeroize",
]
[[package]]
@@ -5148,6 +5017,7 @@ dependencies = [
"rand",
"serde",
"serde_json",
"strum 0.26.3",
"thiserror",
"tokio",
"tracing",
@@ -5407,7 +5277,7 @@ dependencies = [
"time",
"tokio",
"tokio-util",
"toml 0.5.11",
"toml",
"url",
]
@@ -5587,7 +5457,7 @@ dependencies = [
"sysinfo",
"thiserror",
"tokio",
"toml 0.8.14",
"toml",
"tracing",
"url",
"zeroize",
@@ -5605,7 +5475,7 @@ dependencies = [
"fastrand 2.1.1",
"headers",
"hmac",
"hyper 1.3.1",
"hyper 1.4.1",
"ipnetwork 0.20.0",
"nym-crypto",
"nym-http-api-common",
@@ -5733,7 +5603,7 @@ dependencies = [
"chacha20",
"chacha20poly1305",
"criterion 0.4.0",
"curve25519-dalek 4.1.2",
"curve25519-dalek 4.1.3",
"fastrand 2.1.1",
"getrandom",
"log",
@@ -5793,7 +5663,7 @@ dependencies = [
"tokio",
"tokio-stream",
"tokio-util",
"toml 0.5.11",
"toml",
"url",
"zeroize",
]
@@ -6144,7 +6014,7 @@ dependencies = [
"nym-sphinx-types",
"rand",
"reqwest 0.12.4",
"semver 0.11.0",
"semver 1.0.23",
"serde",
"serde_json",
"thiserror",
@@ -6838,12 +6708,6 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "platforms"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7"
[[package]]
name = "plotters"
version = "0.3.6"
@@ -6982,6 +6846,28 @@ dependencies = [
"version_check",
]
[[package]]
name = "proc-macro-error-attr2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
dependencies = [
"proc-macro2",
"quote",
]
[[package]]
name = "proc-macro-error2"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
dependencies = [
"proc-macro-error-attr2",
"proc-macro2",
"quote",
"syn 2.0.66",
]
[[package]]
name = "proc-macro2"
version = "1.0.85"
@@ -7128,9 +7014,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quote"
version = "1.0.36"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
@@ -7382,7 +7268,7 @@ dependencies = [
"http 1.1.0",
"http-body 1.0.0",
"http-body-util",
"hyper 1.3.1",
"hyper 1.4.1",
"hyper-rustls 0.26.0",
"hyper-util",
"ipnet",
@@ -7783,10 +7669,11 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "safer-ffi"
version = "0.1.12"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "614978f55c6e885794b44afb2e2378fe61b104ec4391f310d879edf3df9d874c"
checksum = "435fdd58b61a6f1d8545274c1dfa458e905ff68c166e65e294a0130ef5e675bd"
dependencies = [
"extern-c",
"inventory",
"libc",
"macro_rules_attribute",
@@ -7801,9 +7688,9 @@ dependencies = [
[[package]]
name = "safer_ffi-proc_macros"
version = "0.1.12"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cdcb365894c49e42b5413def59ac644ea085bbbdee535d7c80b2b008a345686"
checksum = "f0f25be5ba5f319542edb31925517e0380245ae37df50a9752cdbc05ef948156"
dependencies = [
"macro_rules_attribute",
"prettyplease",
@@ -7921,16 +7808,7 @@ version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
dependencies = [
"semver-parser 0.7.0",
]
[[package]]
name = "semver"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
dependencies = [
"semver-parser 0.10.2",
"semver-parser",
]
[[package]]
@@ -7948,15 +7826,6 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "semver-parser"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
dependencies = [
"pest",
]
[[package]]
name = "serde"
version = "1.0.210"
@@ -8335,7 +8204,7 @@ dependencies = [
"byteorder",
"chacha",
"ctr",
"curve25519-dalek 4.1.2",
"curve25519-dalek 4.1.3",
"digest 0.10.7",
"hkdf",
"hmac",
@@ -8523,12 +8392,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
[[package]]
name = "state"
version = "0.6.0"
@@ -8570,12 +8433,6 @@ dependencies = [
"strum_macros 0.23.1",
]
[[package]]
name = "strum"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
[[package]]
name = "strum"
version = "0.26.3"
@@ -8598,19 +8455,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "strum_macros"
version = "0.24.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
dependencies = [
"heck 0.4.1",
"proc-macro2",
"quote",
"rustversion",
"syn 1.0.109",
]
[[package]]
name = "strum_macros"
version = "0.26.4"
@@ -8685,22 +8529,11 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
[[package]]
name = "synstructure"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
]
[[package]]
name = "sysinfo"
version = "0.30.12"
version = "0.30.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "732ffa00f53e6b2af46208fba5718d9662a421049204e156328b66791ffa15ae"
checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3"
dependencies = [
"cfg-if",
"core-foundation-sys",
@@ -8833,7 +8666,7 @@ dependencies = [
"serde",
"serde_json",
"tendermint 0.37.0",
"toml 0.8.14",
"toml",
"url",
]
@@ -8947,7 +8780,7 @@ dependencies = [
"thiserror",
"time",
"tokio",
"toml 0.8.14",
"toml",
"tracing",
"url",
"zeroize",
@@ -9045,16 +8878,6 @@ dependencies = [
"time-core",
]
[[package]]
name = "tinystr"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
dependencies = [
"displaydoc",
"zerovec",
]
[[package]]
name = "tinytemplate"
version = "1.2.1"
@@ -9232,27 +9055,6 @@ dependencies = [
"tokio",
]
[[package]]
name = "toml"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
dependencies = [
"serde",
]
[[package]]
name = "toml"
version = "0.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit 0.19.15",
]
[[package]]
name = "toml"
version = "0.8.14"
@@ -9274,19 +9076,6 @@ dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.19.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
dependencies = [
"indexmap 2.2.6",
"serde",
"serde_spanned",
"toml_datetime",
"winnow 0.5.40",
]
[[package]]
name = "toml_edit"
version = "0.21.1"
@@ -9792,12 +9581,12 @@ checksum = "0976c77def3f1f75c4ef892a292c31c0bbe9e3d0702c63044d7c76db298171a3"
[[package]]
name = "url"
version = "2.5.1"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
dependencies = [
"form_urlencoded",
"idna 1.0.0",
"idna 0.5.0",
"percent-encoding",
"serde",
]
@@ -9814,18 +9603,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "utf16_iter"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
[[package]]
name = "utf8_iter"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
@@ -10541,18 +10318,6 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "write16"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
[[package]]
name = "writeable"
version = "0.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
[[package]]
name = "wyz"
version = "0.5.1"
@@ -10568,7 +10333,7 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
dependencies = [
"curve25519-dalek 4.1.2",
"curve25519-dalek 4.1.3",
"rand_core 0.6.4",
"serde",
"zeroize",
@@ -10594,30 +10359,6 @@ dependencies = [
"is-terminal",
]
[[package]]
name = "yoke"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5"
dependencies = [
"serde",
"stable_deref_trait",
"yoke-derive",
"zerofrom",
]
[[package]]
name = "yoke-derive"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
"synstructure",
]
[[package]]
name = "zerocopy"
version = "0.7.34"
@@ -10638,27 +10379,6 @@ dependencies = [
"syn 2.0.66",
]
[[package]]
name = "zerofrom"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55"
dependencies = [
"zerofrom-derive",
]
[[package]]
name = "zerofrom-derive"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.6.0"
@@ -10679,28 +10399,6 @@ dependencies = [
"syn 2.0.66",
]
[[package]]
name = "zerovec"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c"
dependencies = [
"yoke",
"zerofrom",
"zerovec-derive",
]
[[package]]
name = "zerovec-derive"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.66",
]
[[package]]
name = "zip"
version = "1.1.4"
+11 -8
View File
@@ -169,7 +169,9 @@ readme = "README.md"
addr = "0.15.6"
aes = "0.8.1"
aes-gcm = "0.10.1"
anyhow = "1.0.87"
aes-gcm-siv = "0.11.1"
aead = "0.5.2"
anyhow = "1.0.89"
argon2 = "0.5.0"
async-trait = "0.1.82"
axum = "0.7.5"
@@ -198,7 +200,7 @@ clap = "4.5.17"
clap_complete = "4.5"
clap_complete_fig = "4.5"
colored = "2.0"
comfy-table = "6.0.0"
comfy-table = "7.1.1"
console = "0.15.8"
console-subscriber = "0.1.1"
console_error_panic_hook = "0.1"
@@ -224,7 +226,7 @@ flate2 = "1.0.33"
futures = "0.3.28"
generic-array = "0.14.7"
getrandom = "0.2.10"
getset = "0.1.1"
getset = "0.1.3"
handlebars = "3.5.5"
headers = "0.4.0"
hex = "0.4.3"
@@ -232,10 +234,12 @@ hex-literal = "0.3.3"
hkdf = "0.12.3"
hmac = "0.12.1"
http = "1"
http-body-util = "0.1"
httpcodec = "0.2.3"
humantime = "2.1.0"
humantime-serde = "1.1.1"
hyper = "1.3.1"
hyper = "1.4.1"
hyper-util = "0.1"
indicatif = "0.17.8"
inquire = "0.6.2"
ip_network = "0.4.1"
@@ -274,7 +278,7 @@ reqwest = { version = "0.12.4", default-features = false }
rocket = "0.5.0"
rocket_cors = "0.6.0"
rocket_okapi = "0.8.0"
safer-ffi = "0.1.12"
safer-ffi = "0.1.13"
schemars = "0.8.21"
semver = "1.0.23"
serde = "1.0.210"
@@ -291,7 +295,7 @@ sqlx = "0.6.3"
strum = "0.26"
subtle-encoding = "0.5"
syn = "1"
sysinfo = "0.30.12"
sysinfo = "0.30.13"
tap = "1.0.1"
tar = "0.4.41"
tempfile = "3.5.0"
@@ -300,6 +304,7 @@ time = "0.3.30"
tokio = "1.39"
tokio-stream = "0.1.16"
tokio-test = "0.4.4"
tokio-tun = "0.11.5"
tokio-tungstenite = { version = "0.20.1" }
tokio-util = "0.7.12"
toml = "0.8.14"
@@ -313,7 +318,6 @@ ts-rs = "7.0.0"
tungstenite = { version = "0.20.1", default-features = false }
url = "2.5"
utoipa = "4.2"
utoipa-rapidoc = "4.0"
utoipa-swagger-ui = "7.1"
utoipauto = "0.1"
uuid = "*"
@@ -334,7 +338,6 @@ group = { version = "0.13.0", default-features = false }
ff = { version = "0.13.0", default-features = false }
# cosmwasm-related
cosmwasm-derive = "=1.4.3"
cosmwasm-schema = "=1.4.3"
cosmwasm-std = "=1.4.3"
# use 0.5.0 as that's the version used by cosmwasm-std 1.4.3
+1 -1
View File
@@ -18,7 +18,7 @@ nym-ecash-time = { path = "../ecash-time" }
nym-credential-storage = { path = "../credential-storage" }
nym-credentials = { path = "../credentials" }
nym-credentials-interface = { path = "../credentials-interface" }
nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "symmetric", "aes", "hashing"] }
nym-crypto = { path = "../crypto", features = ["rand", "asymmetric", "stream_cipher", "aes", "hashing"] }
nym-network-defaults = { path = "../network-defaults" }
nym-validator-client = { path = "../client-libs/validator-client", default-features = false }
nym-ecash-contract-common = { path = "../cosmwasm-smart-contracts/ecash-contract" }
+2 -2
View File
@@ -8,14 +8,14 @@ license = { workspace = true }
repository = { workspace = true }
[dependencies]
const-str = { workspace = true }
clap = { workspace = true, features = ["derive"], optional = true }
clap_complete = { workspace = true, optional = true }
clap_complete_fig = { workspace = true, optional = true }
const-str = { workspace = true }
log = { workspace = true }
pretty_env_logger = { workspace = true }
semver = "0.11"
schemars = { workspace = true, features = ["preserve_order"], optional = true }
semver.workspace = true
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, optional = true }
+3 -2
View File
@@ -1,9 +1,10 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use semver::SemVerError;
pub use semver::Version;
/// Checks if the version is minor version compatible.
///
/// Checks whether given `version` is compatible with a given semantic version requirement `req`
/// according to major-minor semver rules. The semantic version requirement can be passed as a full,
/// concrete version number, because that's what we'll have in our Cargo.toml files (e.g. 0.3.2).
@@ -22,7 +23,7 @@ pub fn is_minor_version_compatible(version: &str, req: &str) -> bool {
expected_version.major == req_version.major && expected_version.minor == req_version.minor
}
pub fn parse_version(raw_version: &str) -> Result<Version, SemVerError> {
pub fn parse_version(raw_version: &str) -> Result<Version, semver::Error> {
Version::parse(raw_version)
}
+6 -6
View File
@@ -14,7 +14,7 @@ base64 = { workspace = true }
bs58 = { workspace = true }
cfg-if = { workspace = true }
clap = { workspace = true, optional = true }
comfy-table = { version = "7.1.1", optional = true }
comfy-table = { workspace = true, optional = true }
futures = { workspace = true }
humantime-serde = { workspace = true }
log = { workspace = true }
@@ -59,19 +59,19 @@ nym-ecash-time = { path = "../ecash-time" }
### For serving prometheus metrics
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.hyper]
version = "1"
workspace = true
features = ["server", "http1"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.http-body-util]
version = "0.1"
workspace = true
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.hyper-util]
version = "0.1"
workspace = true
features = ["tokio"]
###
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-stream]
version = "0.1.16"
workspace = true
features = ["time"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
@@ -110,7 +110,7 @@ path = "../wasm/utils"
features = ["websocket"]
[target."cfg(target_arch = \"wasm32\")".dependencies.time]
version = "0.3.17"
workspace = true
features = ["wasm-bindgen"]
[dev-dependencies]
@@ -0,0 +1,13 @@
/*
* Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
-- make aes128 key column nullable and add aes256 column
ALTER TABLE remote_gateway_details RENAME COLUMN derived_aes128_ctr_blake3_hmac_keys_bs58 TO derived_aes128_ctr_blake3_hmac_keys_bs58_old;
ALTER TABLE remote_gateway_details ADD COLUMN derived_aes128_ctr_blake3_hmac_keys_bs58 TEXT;
ALTER TABLE remote_gateway_details ADD COLUMN derived_aes256_gcm_siv_key BLOB;
UPDATE remote_gateway_details SET derived_aes128_ctr_blake3_hmac_keys_bs58 = derived_aes128_ctr_blake3_hmac_keys_bs58_old;
ALTER TABLE remote_gateway_details DROP COLUMN derived_aes128_ctr_blake3_hmac_keys_bs58_old;
@@ -155,11 +155,12 @@ impl StorageManager {
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO remote_gateway_details(gateway_id_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58, gateway_owner_address, gateway_listener)
VALUES (?, ?, ?, ?)
INSERT INTO remote_gateway_details(gateway_id_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key, gateway_owner_address, gateway_listener)
VALUES (?, ?, ?, ?, ?)
"#,
remote.gateway_id_bs58,
remote.derived_aes128_ctr_blake3_hmac_keys_bs58,
remote.derived_aes256_gcm_siv_key,
remote.gateway_owner_address,
remote.gateway_listener,
)
@@ -168,6 +169,30 @@ impl StorageManager {
Ok(())
}
pub(crate) async fn update_remote_gateway_key(
&self,
gateway_id_bs58: &str,
derived_aes128_ctr_blake3_hmac_keys_bs58: Option<&str>,
derived_aes256_gcm_siv_key: Option<&[u8]>,
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
UPDATE remote_gateway_details
SET
derived_aes128_ctr_blake3_hmac_keys_bs58 = ?,
derived_aes256_gcm_siv_key = ?
WHERE gateway_id_bs58 = ?
"#,
derived_aes128_ctr_blake3_hmac_keys_bs58,
derived_aes256_gcm_siv_key,
gateway_id_bs58
)
.execute(&self.connection_pool)
.await?;
Ok(())
}
pub(crate) async fn remove_remote_gateway_details(
&self,
gateway_id: &str,
@@ -7,7 +7,8 @@ use crate::{
};
use async_trait::async_trait;
use manager::StorageManager;
use nym_crypto::asymmetric::identity::PublicKey;
use nym_crypto::asymmetric::ed25519;
use nym_gateway_requests::SharedSymmetricKey;
use std::path::Path;
pub mod error;
@@ -67,7 +68,7 @@ impl GatewaysDetailsStore for OnDiskGatewaysDetails {
Ok(registered)
}
async fn all_gateways_identities(&self) -> Result<Vec<PublicKey>, Self::StorageError> {
async fn all_gateways_identities(&self) -> Result<Vec<ed25519::PublicKey>, Self::StorageError> {
Ok(self
.manager
.registered_gateways()
@@ -132,6 +133,21 @@ impl GatewaysDetailsStore for OnDiskGatewaysDetails {
Ok(())
}
async fn upgrade_stored_remote_gateway_key(
&self,
gateway_id: ed25519::PublicKey,
updated_key: &SharedSymmetricKey,
) -> Result<(), Self::StorageError> {
self.manager
.update_remote_gateway_key(
&gateway_id.to_base58_string(),
None,
Some(updated_key.as_bytes()),
)
.await?;
Ok(())
}
// ideally all of those should be run under a storage tx to ensure storage consistency,
// but at that point it's fine
async fn remove_gateway_details(&self, gateway_id: &str) -> Result<(), Self::StorageError> {
@@ -2,8 +2,10 @@
// SPDX-License-Identifier: Apache-2.0
use crate::types::{ActiveGateway, GatewayRegistration};
use crate::{BadGateway, GatewaysDetailsStore};
use crate::{BadGateway, GatewayDetails, GatewaysDetailsStore};
use async_trait::async_trait;
use nym_crypto::asymmetric::ed25519::PublicKey;
use nym_gateway_requests::{SharedGatewayKey, SharedSymmetricKey};
use std::collections::HashMap;
use std::sync::Arc;
use thiserror::Error;
@@ -34,10 +36,6 @@ struct InMemStorageInner {
impl GatewaysDetailsStore for InMemGatewaysDetails {
type StorageError = InMemStorageError;
async fn has_gateway_details(&self, gateway_id: &str) -> Result<bool, Self::StorageError> {
Ok(self.inner.read().await.gateways.contains_key(gateway_id))
}
async fn active_gateway(&self) -> Result<ActiveGateway, Self::StorageError> {
let guard = self.inner.read().await;
@@ -68,6 +66,10 @@ impl GatewaysDetailsStore for InMemGatewaysDetails {
Ok(self.inner.read().await.gateways.values().cloned().collect())
}
async fn has_gateway_details(&self, gateway_id: &str) -> Result<bool, Self::StorageError> {
Ok(self.inner.read().await.gateways.contains_key(gateway_id))
}
async fn load_gateway_details(
&self,
gateway_id: &str,
@@ -94,6 +96,29 @@ impl GatewaysDetailsStore for InMemGatewaysDetails {
Ok(())
}
async fn upgrade_stored_remote_gateway_key(
&self,
gateway_id: PublicKey,
updated_key: &SharedSymmetricKey,
) -> Result<(), Self::StorageError> {
let mut guard = self.inner.write().await;
#[allow(clippy::unwrap_used)]
if let Some(target) = guard.gateways.get_mut(&gateway_id.to_string()) {
let GatewayDetails::Remote(details) = &mut target.details else {
return Ok(());
};
assert_eq!(Arc::strong_count(&details.shared_key), 1);
// eh. that's nasty, but it's only ever used for ephemeral clients so should be fine for now...
details.shared_key = Arc::new(SharedGatewayKey::Current(
SharedSymmetricKey::try_from_bytes(updated_key.as_bytes()).unwrap(),
))
}
Ok(())
}
async fn remove_gateway_details(&self, gateway_id: &str) -> Result<(), Self::StorageError> {
let mut guard = self.inner.write().await;
if let Some(active) = guard.active_gateway.as_ref() {
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use nym_crypto::asymmetric::identity::Ed25519RecoveryError;
use nym_gateway_requests::registration::handshake::shared_key::SharedKeyConversionError;
use nym_gateway_requests::shared_key::SharedKeyConversionError;
use thiserror::Error;
#[derive(Debug, Error)]
@@ -36,6 +36,9 @@ pub enum BadGateway {
source: SharedKeyConversionError,
},
#[error("could not find any valid shared keys for gateway {gateway_id}")]
MissingSharedKey { gateway_id: String },
#[error(
"the listening address of gateway {gateway_id} ({raw_listener}) is malformed: {source}"
)]
@@ -5,6 +5,8 @@
#![warn(clippy::unwrap_used)]
use async_trait::async_trait;
use nym_crypto::asymmetric::identity;
use nym_gateway_requests::SharedSymmetricKey;
use std::error::Error;
pub mod backend;
@@ -18,7 +20,6 @@ pub use error::BadGateway;
#[cfg(all(not(target_arch = "wasm32"), feature = "fs-gateways-storage"))]
pub use backend::fs_backend::{error::StorageError, OnDiskGatewaysDetails};
use nym_crypto::asymmetric::identity;
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
@@ -61,6 +62,12 @@ pub trait GatewaysDetailsStore {
details: &GatewayRegistration,
) -> Result<(), Self::StorageError>;
async fn upgrade_stored_remote_gateway_key(
&self,
gateway_id: identity::PublicKey,
updated_key: &SharedSymmetricKey,
) -> Result<(), Self::StorageError>;
/// Remove given gateway details from the underlying store.
async fn remove_gateway_details(&self, gateway_id: &str) -> Result<(), Self::StorageError>;
}
@@ -4,9 +4,10 @@
use crate::BadGateway;
use cosmrs::AccountId;
use nym_crypto::asymmetric::identity;
use nym_gateway_requests::registration::handshake::SharedKeys;
use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey};
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use std::ops::Deref;
use std::str::FromStr;
use std::sync::Arc;
use time::OffsetDateTime;
@@ -64,13 +65,13 @@ impl From<GatewayDetails> for GatewayRegistration {
impl GatewayDetails {
pub fn new_remote(
gateway_id: identity::PublicKey,
derived_aes128_ctr_blake3_hmac_keys: Arc<SharedKeys>,
shared_key: Arc<SharedGatewayKey>,
gateway_owner_address: Option<AccountId>,
gateway_listener: Url,
) -> Self {
GatewayDetails::Remote(RemoteGatewayDetails {
gateway_id,
derived_aes128_ctr_blake3_hmac_keys,
shared_key,
gateway_owner_address,
gateway_listener,
})
@@ -87,9 +88,9 @@ impl GatewayDetails {
}
}
pub fn shared_key(&self) -> Option<&SharedKeys> {
pub fn shared_key(&self) -> Option<&SharedGatewayKey> {
match self {
GatewayDetails::Remote(details) => Some(&details.derived_aes128_ctr_blake3_hmac_keys),
GatewayDetails::Remote(details) => Some(&details.shared_key),
GatewayDetails::Custom(_) => None,
}
}
@@ -167,7 +168,8 @@ pub struct RegisteredGateway {
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
pub struct RawRemoteGatewayDetails {
pub gateway_id_bs58: String,
pub derived_aes128_ctr_blake3_hmac_keys_bs58: String,
pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option<String>,
pub derived_aes256_gcm_siv_key: Option<Vec<u8>>,
pub gateway_owner_address: Option<String>,
pub gateway_listener: String,
}
@@ -184,13 +186,35 @@ impl TryFrom<RawRemoteGatewayDetails> for RemoteGatewayDetails {
}
})?;
let derived_aes128_ctr_blake3_hmac_keys = Arc::new(
SharedKeys::try_from_base58_string(&value.derived_aes128_ctr_blake3_hmac_keys_bs58)
.map_err(|source| BadGateway::MalformedSharedKeys {
gateway_id: value.gateway_id_bs58.clone(),
source,
})?,
);
let shared_key =
match (
&value.derived_aes256_gcm_siv_key,
&value.derived_aes128_ctr_blake3_hmac_keys_bs58,
) {
(None, None) => {
return Err(BadGateway::MissingSharedKey {
gateway_id: value.gateway_id_bs58.clone(),
})
}
(Some(aes256gcm_siv), _) => {
let current_key =
SharedSymmetricKey::try_from_bytes(aes256gcm_siv).map_err(|source| {
BadGateway::MalformedSharedKeys {
gateway_id: value.gateway_id_bs58.clone(),
source,
}
})?;
SharedGatewayKey::Current(current_key)
}
(None, Some(aes128ctr_hmac)) => {
let legacy_key = LegacySharedKeys::try_from_base58_string(aes128ctr_hmac)
.map_err(|source| BadGateway::MalformedSharedKeys {
gateway_id: value.gateway_id_bs58.clone(),
source,
})?;
SharedGatewayKey::Legacy(legacy_key)
}
};
let gateway_owner_address = value
.gateway_owner_address
@@ -216,7 +240,7 @@ impl TryFrom<RawRemoteGatewayDetails> for RemoteGatewayDetails {
Ok(RemoteGatewayDetails {
gateway_id,
derived_aes128_ctr_blake3_hmac_keys,
shared_key: Arc::new(shared_key),
gateway_owner_address,
gateway_listener,
})
@@ -225,11 +249,16 @@ impl TryFrom<RawRemoteGatewayDetails> for RemoteGatewayDetails {
impl<'a> From<&'a RemoteGatewayDetails> for RawRemoteGatewayDetails {
fn from(value: &'a RemoteGatewayDetails) -> Self {
let (derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key) =
match value.shared_key.deref() {
SharedGatewayKey::Current(key) => (None, Some(key.to_bytes())),
SharedGatewayKey::Legacy(key) => (Some(key.to_base58_string()), None),
};
RawRemoteGatewayDetails {
gateway_id_bs58: value.gateway_id.to_base58_string(),
derived_aes128_ctr_blake3_hmac_keys_bs58: value
.derived_aes128_ctr_blake3_hmac_keys
.to_base58_string(),
derived_aes128_ctr_blake3_hmac_keys_bs58,
derived_aes256_gcm_siv_key,
gateway_owner_address: value.gateway_owner_address.as_ref().map(|o| o.to_string()),
gateway_listener: value.gateway_listener.to_string(),
}
@@ -240,9 +269,7 @@ impl<'a> From<&'a RemoteGatewayDetails> for RawRemoteGatewayDetails {
pub struct RemoteGatewayDetails {
pub gateway_id: identity::PublicKey,
// note: `SharedKeys` implement ZeroizeOnDrop, meaning when `RemoteGatewayDetails` is dropped,
// the keys will be zeroized
pub derived_aes128_ctr_blake3_hmac_keys: Arc<SharedKeys>,
pub shared_key: Arc<SharedGatewayKey>,
pub gateway_owner_address: Option<AccountId>,
@@ -354,12 +354,14 @@ where
config: &Config,
initialisation_result: InitialisationResult,
bandwidth_controller: Option<BandwidthController<C, S::CredentialStore>>,
details_store: &S::GatewaysDetailsStore,
packet_router: PacketRouter,
shutdown: TaskClient,
) -> Result<GatewayClient<C, S::CredentialStore>, ClientCoreError>
where
<S::KeyStore as KeyStore>::StorageError: Send + Sync + 'static,
<S::CredentialStore as CredentialStorage>::StorageError: Send + Sync + 'static,
<S::GatewaysDetailsStore as GatewaysDetailsStore>::StorageError: Sync + Send,
{
let managed_keys = initialisation_result.client_keys;
let GatewayDetails::Remote(details) = initialisation_result.gateway_registration.details
@@ -387,23 +389,57 @@ where
),
cfg,
managed_keys.identity_keypair(),
Some(details.derived_aes128_ctr_blake3_hmac_keys),
Some(details.shared_key),
packet_router,
bandwidth_controller,
shutdown,
)
};
gateway_client
.authenticate_and_start()
let gateway_failure = |err| {
log::error!("Could not authenticate and start up the gateway connection - {err}");
ClientCoreError::GatewayClientError {
gateway_id: details.gateway_id.to_base58_string(),
source: err,
}
};
// the gateway client startup procedure is slightly more complicated now
// we need to:
// - perform handshake (reg or auth)
// - check for key upgrade
// - maybe perform another upgrade handshake
// - check for bandwidth
// - start background tasks
let auth_res = gateway_client
.perform_initial_authentication()
.await
.map_err(|err| {
log::error!("Could not authenticate and start up the gateway connection - {err}");
ClientCoreError::GatewayClientError {
gateway_id: details.gateway_id.to_base58_string(),
source: err,
}
})?;
.map_err(gateway_failure)?;
if auth_res.requires_key_upgrade {
// drop the shared_key arc because we don't need it and we can't hold it for the purposes of upgrade
drop(auth_res);
let updated_key = gateway_client
.upgrade_key_authenticated()
.await
.map_err(gateway_failure)?;
details_store
.upgrade_stored_remote_gateway_key(gateway_client.gateway_identity(), &updated_key)
.await.map_err(|err| {
error!("failed to store upgraded gateway key! this connection might be forever broken now: {err}");
ClientCoreError::GatewaysDetailsStoreError { source: Box::new(err) }
})?
}
gateway_client
.claim_initial_bandwidth()
.await
.map_err(gateway_failure)?;
gateway_client
.start_listening_for_mixnet_messages()
.map_err(gateway_failure)?;
Ok(gateway_client)
}
@@ -413,12 +449,14 @@ where
config: &Config,
initialisation_result: InitialisationResult,
bandwidth_controller: Option<BandwidthController<C, S::CredentialStore>>,
details_store: &S::GatewaysDetailsStore,
packet_router: PacketRouter,
mut shutdown: TaskClient,
) -> Result<Box<dyn GatewayTransceiver + Send>, ClientCoreError>
where
<S::KeyStore as KeyStore>::StorageError: Send + Sync + 'static,
<S::CredentialStore as CredentialStorage>::StorageError: Send + Sync + 'static,
<S::GatewaysDetailsStore as GatewaysDetailsStore>::StorageError: Sync + Send,
{
// if we have setup custom gateway sender and persisted details agree with it, return it
if let Some(mut custom_gateway_transceiver) = custom_gateway_transceiver {
@@ -429,7 +467,7 @@ where
{
Err(ClientCoreError::CustomGatewaySelectionExpected)
} else {
// and make sure to invalidate the task client so we wouldn't cause premature shutdown
// and make sure to invalidate the task client, so we wouldn't cause premature shutdown
shutdown.disarm();
custom_gateway_transceiver.set_packet_router(packet_router)?;
Ok(custom_gateway_transceiver)
@@ -441,6 +479,7 @@ where
config,
initialisation_result,
bandwidth_controller,
details_store,
packet_router,
shutdown,
)
@@ -630,7 +669,8 @@ where
)
.await?;
let (reply_storage_backend, credential_store) = self.client_store.into_runtime_stores();
let (reply_storage_backend, credential_store, details_store) =
self.client_store.into_runtime_stores();
// channels for inter-component communication
// TODO: make the channels be internally created by the relevant components
@@ -705,6 +745,7 @@ where
self.config,
init_res,
bandwidth_controller,
&details_store,
gateway_packet_router,
shutdown.fork("gateway_transceiver"),
)
@@ -13,7 +13,7 @@ pub mod v1_1_33 {
use nym_client_core_gateways_storage::{
CustomGatewayDetails, GatewayDetails, GatewayRegistration, RemoteGatewayDetails,
};
use nym_gateway_requests::registration::handshake::SharedKeys;
use nym_gateway_requests::shared_key::LegacySharedKeys;
use serde::{Deserialize, Serialize};
use sha2::{digest::Digest, Sha256};
use std::ops::Deref;
@@ -58,7 +58,7 @@ pub mod v1_1_33 {
}
impl PersistedGatewayConfig {
fn verify(&self, shared_key: &SharedKeys) -> bool {
fn verify(&self, shared_key: &LegacySharedKeys) -> bool {
let key_bytes = Zeroizing::new(shared_key.to_bytes());
let mut key_hasher = Sha256::new();
@@ -74,7 +74,7 @@ pub mod v1_1_33 {
gateway_id: String,
}
fn load_shared_key<P: AsRef<Path>>(path: P) -> Result<SharedKeys, ClientCoreError> {
fn load_shared_key<P: AsRef<Path>>(path: P) -> Result<LegacySharedKeys, ClientCoreError> {
// the shared key was a simple pem file
Ok(nym_pemstore::load_key(path)?)
}
@@ -83,7 +83,7 @@ pub mod v1_1_33 {
gateway_id: String,
gateway_owner: String,
gateway_listener: String,
gateway_shared_key: SharedKeys,
gateway_shared_key: LegacySharedKeys,
) -> Result<GatewayDetails, ClientCoreError> {
Ok(GatewayDetails::Remote(RemoteGatewayDetails {
gateway_id: gateway_id
@@ -91,7 +91,7 @@ pub mod v1_1_33 {
.map_err(|err| ClientCoreError::UpgradeFailure {
message: format!("the stored gateway id was malformed: {err}"),
})?,
derived_aes128_ctr_blake3_hmac_keys: Arc::new(gateway_shared_key),
shared_key: Arc::new(gateway_shared_key.into()),
gateway_owner_address: Some(gateway_owner.parse().map_err(|err| {
ClientCoreError::UpgradeFailure {
message: format!("the stored gateway owner address was malformed: {err}"),
@@ -49,7 +49,13 @@ pub trait MixnetClientStorage {
type CredentialStore: CredentialStorage;
type GatewaysDetailsStore: GatewaysDetailsStore;
fn into_runtime_stores(self) -> (Self::ReplyStore, Self::CredentialStore);
fn into_runtime_stores(
self,
) -> (
Self::ReplyStore,
Self::CredentialStore,
Self::GatewaysDetailsStore,
);
fn key_store(&self) -> &Self::KeyStore;
fn reply_store(&self) -> &Self::ReplyStore;
@@ -77,8 +83,18 @@ impl MixnetClientStorage for Ephemeral {
type CredentialStore = EphemeralCredentialStorage;
type GatewaysDetailsStore = InMemGatewaysDetails;
fn into_runtime_stores(self) -> (Self::ReplyStore, Self::CredentialStore) {
(self.reply_store, self.credential_store)
fn into_runtime_stores(
self,
) -> (
Self::ReplyStore,
Self::CredentialStore,
Self::GatewaysDetailsStore,
) {
(
self.reply_store,
self.credential_store,
self.gateway_details_store,
)
}
fn key_store(&self) -> &Self::KeyStore {
@@ -168,8 +184,18 @@ impl MixnetClientStorage for OnDiskPersistent {
type CredentialStore = PersistentCredentialStorage;
type GatewaysDetailsStore = OnDiskGatewaysDetails;
fn into_runtime_stores(self) -> (Self::ReplyStore, Self::CredentialStore) {
(self.reply_store, self.credential_store)
fn into_runtime_stores(
self,
) -> (
Self::ReplyStore,
Self::CredentialStore,
Self::GatewaysDetailsStore,
) {
(
self.reply_store,
self.credential_store,
self.gateway_details_store,
)
}
fn key_store(&self) -> &Self::KeyStore {
@@ -3,7 +3,7 @@
use crate::client::key_manager::persistence::KeyStore;
use nym_crypto::asymmetric::{encryption, identity};
use nym_gateway_requests::registration::handshake::SharedKeys;
use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey};
use nym_sphinx::acknowledgements::AckKey;
use rand::{CryptoRng, RngCore};
use std::sync::Arc;
@@ -84,5 +84,7 @@ fn _assert_keys_zeroize_on_drop() {
_assert_zeroize_on_drop::<identity::KeyPair>();
_assert_zeroize_on_drop::<encryption::KeyPair>();
_assert_zeroize_on_drop::<AckKey>();
_assert_zeroize_on_drop::<SharedKeys>();
_assert_zeroize_on_drop::<LegacySharedKeys>();
_assert_zeroize_on_drop::<SharedSymmetricKey>();
_assert_zeroize_on_drop::<SharedGatewayKey>();
}
@@ -102,6 +102,7 @@ impl TopologyRefresher {
.current_topology()
.await
.ok_or(NymTopologyError::EmptyNetworkTopology)?;
if !topology.gateway_exists(gateway) {
return Err(NymTopologyError::NonExistentGatewayError {
identity_key: gateway.to_base58_string(),
+5
View File
@@ -214,6 +214,11 @@ pub enum ClientCoreError {
#[error("this client has already registered with gateway {gateway_id}")]
AlreadyRegistered { gateway_id: String },
#[error(
"fresh registration with gateway {gateway_id} somehow requires an additional key upgrade!"
)]
UnexpectedKeyUpgrade { gateway_id: String },
}
/// Set of messages that the client can send to listeners via the task manager
+11 -2
View File
@@ -320,7 +320,7 @@ pub(super) async fn register_with_gateway(
source: err,
}
})?;
let shared_keys = gateway_client
let auth_response = gateway_client
.perform_initial_authentication()
.await
.map_err(|err| {
@@ -330,8 +330,17 @@ pub(super) async fn register_with_gateway(
source: err,
}
})?;
// this should NEVER happen, if it did, it means the function was misused,
// because for any fresh **registration**, the derived key is always up to date
if auth_response.requires_key_upgrade {
return Err(ClientCoreError::UnexpectedKeyUpgrade {
gateway_id: gateway_id.to_base58_string(),
});
}
Ok(RegistrationResult {
shared_keys,
shared_keys: auth_response.initial_shared_key,
authenticated_ephemeral_client: gateway_client,
})
}
+2 -2
View File
@@ -11,7 +11,7 @@ use nym_client_core_gateways_storage::{
};
use nym_crypto::asymmetric::identity;
use nym_gateway_client::client::InitGatewayClient;
use nym_gateway_requests::registration::handshake::SharedKeys;
use nym_gateway_requests::shared_key::SharedGatewayKey;
use nym_sphinx::addressing::clients::Recipient;
use nym_topology::gateway;
use nym_validator_client::client::IdentityKey;
@@ -104,7 +104,7 @@ impl SelectedGateway {
/// - shared keys derived between ourselves and the node
/// - an authenticated handle of an ephemeral handle created for the purposes of registration
pub struct RegistrationResult {
pub shared_keys: Arc<SharedKeys>,
pub shared_keys: Arc<SharedGatewayKey>,
pub authenticated_ephemeral_client: InitGatewayClient,
}
+3 -2
View File
@@ -11,13 +11,14 @@ license.workspace = true
# TODO: (for this and other crates), similarly to 'tokio', import only required "futures" modules rather than
# the entire crate
futures = { workspace = true }
log = { workspace = true }
tracing = { workspace = true }
thiserror = { workspace = true }
url = { workspace = true }
rand = { workspace = true }
tokio = { workspace = true, features = ["macros"] }
si-scale = { workspace = true }
time.workspace = true
zeroize.workspace = true
# internal
nym-bandwidth-controller = { path = "../../bandwidth-controller" }
@@ -43,7 +44,7 @@ workspace = true
features = ["macros", "rt", "net", "sync", "time"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-stream]
version = "0.1.16"
workspace = true
features = ["net", "sync", "time"]
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-tungstenite]
@@ -53,9 +53,9 @@ impl ClientBandwidth {
let remaining_bi2 = bibytes2(remaining as f64);
if remaining < 0 {
log::warn!("OUT OF BANDWIDTH. remaining: {remaining_bi2}");
tracing::warn!("OUT OF BANDWIDTH. remaining: {remaining_bi2}");
} else {
log::info!("remaining bandwidth: {remaining_bi2}");
tracing::info!("remaining bandwidth: {remaining_bi2}");
}
self.inner
@@ -1,5 +1,6 @@
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::bandwidth::ClientBandwidth;
use crate::client::config::GatewayClientConfig;
use crate::error::GatewayClientError;
@@ -11,24 +12,24 @@ use crate::socket_state::{ws_fd, PartiallyDelegatedHandle, SocketState};
use crate::traits::GatewayPacketRouter;
use crate::{cleanup_socket_message, try_decrypt_binary_message};
use futures::{SinkExt, StreamExt};
use log::*;
use nym_bandwidth_controller::{BandwidthController, BandwidthStatusMessage};
use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage;
use nym_credential_storage::storage::Storage as CredentialStorage;
use nym_credentials::CredentialSpendingData;
use nym_crypto::asymmetric::identity;
use nym_gateway_requests::authentication::encrypted_address::EncryptedAddressBytes;
use nym_gateway_requests::iv::IV;
use nym_gateway_requests::registration::handshake::{client_handshake, SharedKeys};
use nym_gateway_requests::registration::handshake::client_handshake;
use nym_gateway_requests::{
BinaryRequest, ClientControlRequest, ServerResponse, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION,
CURRENT_PROTOCOL_VERSION,
BinaryRequest, ClientControlRequest, ClientRequest, SensitiveServerResponse, ServerResponse,
SharedGatewayKey, SharedSymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION,
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION,
};
use nym_sphinx::forwarding::packet::MixPacket;
use nym_task::TaskClient;
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
use rand::rngs::OsRng;
use std::sync::Arc;
use tracing::instrument;
use tracing::*;
use tungstenite::protocol::Message;
use url::Url;
@@ -45,6 +46,7 @@ use std::os::raw::c_int as RawFd;
use wasm_utils::websocket::JSWebsocket;
#[cfg(target_arch = "wasm32")]
use wasmtimer::tokio::sleep;
use zeroize::Zeroizing;
pub mod config;
@@ -71,6 +73,13 @@ impl GatewayConfig {
}
}
#[must_use]
#[derive(Debug)]
pub struct AuthenticationResponse {
pub initial_shared_key: Arc<SharedGatewayKey>,
pub requires_key_upgrade: bool,
}
// TODO: this should be refactored into a state machine that keeps track of its authentication state
pub struct GatewayClient<C, St = EphemeralCredentialStorage> {
pub cfg: GatewayClientConfig,
@@ -80,7 +89,7 @@ pub struct GatewayClient<C, St = EphemeralCredentialStorage> {
gateway_address: String,
gateway_identity: identity::PublicKey,
local_identity: Arc<identity::KeyPair>,
shared_key: Option<Arc<SharedKeys>>,
shared_key: Option<Arc<SharedGatewayKey>>,
connection: SocketState,
packet_router: PacketRouter,
bandwidth_controller: Option<BandwidthController<C, St>>,
@@ -98,7 +107,7 @@ impl<C, St> GatewayClient<C, St> {
gateway_config: GatewayConfig,
local_identity: Arc<identity::KeyPair>,
// TODO: make it mandatory. if you don't want to pass it, use `new_init`
shared_key: Option<Arc<SharedKeys>>,
shared_key: Option<Arc<SharedGatewayKey>>,
packet_router: PacketRouter,
bandwidth_controller: Option<BandwidthController<C, St>>,
task_client: TaskClient,
@@ -293,7 +302,7 @@ impl<C, St> GatewayClient<C, St> {
// as we need to be able to write the request and read the subsequent response
async fn send_websocket_message(
&mut self,
msg: Message,
msg: impl Into<Message>,
) -> Result<ServerResponse, GatewayClientError> {
let should_restart_mixnet_listener = if self.connection.is_partially_delegated() {
self.recover_socket_connection().await?;
@@ -307,7 +316,7 @@ impl<C, St> GatewayClient<C, St> {
SocketState::NotConnected => return Err(GatewayClientError::ConnectionNotEstablished),
_ => return Err(GatewayClientError::ConnectionInInvalidState),
};
conn.send(msg).await?;
conn.send(msg.into()).await?;
let response = self.read_control_response().await;
if should_restart_mixnet_listener {
@@ -398,13 +407,19 @@ impl<C, St> GatewayClient<C, St> {
}
}
async fn register(&mut self) -> Result<(), GatewayClientError> {
async fn register(
&mut self,
derive_aes256_gcm_siv_key: bool,
) -> Result<(), GatewayClientError> {
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
debug_assert!(self.connection.is_available());
log::debug!("Registering gateway");
log::debug!(
"registering with gateway. using legacy key derivation: {}",
!derive_aes256_gcm_siv_key
);
// it's fine to instantiate it here as it's only used once (during authentication or registration)
// and putting it into the GatewayClient struct would be a hassle
@@ -417,13 +432,15 @@ impl<C, St> GatewayClient<C, St> {
self.local_identity.as_ref(),
self.gateway_identity,
self.cfg.bandwidth.require_tickets,
derive_aes256_gcm_siv_key,
#[cfg(not(target_arch = "wasm32"))]
self.task_client.clone(),
)
.await
.map_err(GatewayClientError::RegistrationFailure),
_ => unreachable!(),
_ => return Err(GatewayClientError::ConnectionInInvalidState),
}?;
let (authentication_status, gateway_protocol) = match self.read_control_response().await? {
ServerResponse::Register {
protocol_version,
@@ -432,7 +449,7 @@ impl<C, St> GatewayClient<C, St> {
ServerResponse::Error { message } => {
return Err(GatewayClientError::GatewayError(message))
}
_ => return Err(GatewayClientError::UnexpectedResponse),
other => return Err(GatewayClientError::UnexpectedResponse { name: other.name() }),
};
self.check_gateway_protocol(gateway_protocol)?;
@@ -448,41 +465,93 @@ impl<C, St> GatewayClient<C, St> {
Ok(())
}
async fn authenticate(
pub async fn upgrade_key_authenticated(
&mut self,
shared_key: Option<SharedKeys>,
) -> Result<(), GatewayClientError> {
if shared_key.is_none() && self.shared_key.is_none() {
return Err(GatewayClientError::NoSharedKeyAvailable);
}
) -> Result<Zeroizing<SharedSymmetricKey>, GatewayClientError> {
info!("*** STARTING AES128CTR-HMAC KEY UPGRADE INTO AES256GCM-SIV***");
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
log::debug!("Authenticating with gateway");
// it's fine to instantiate it here as it's only used once (during authentication or registration)
// and putting it into the GatewayClient struct would be a hassle
let mut rng = OsRng;
if !self.authenticated {
return Err(GatewayClientError::NotAuthenticated);
}
let Some(shared_key) = self.shared_key.as_ref() else {
return Err(GatewayClientError::NoSharedKeyAvailable);
};
if !shared_key.is_legacy() {
return Err(GatewayClientError::KeyAlreadyUpgraded);
}
// make sure we have the only reference, so we could safely swap it
if Arc::strong_count(shared_key) != 1 {
return Err(GatewayClientError::KeyAlreadyInUse);
}
assert!(shared_key.is_legacy());
let legacy_key = shared_key.unwrap_legacy();
let (updated_key, hkdf_salt) = legacy_key.upgrade();
let derived_key_digest = updated_key.digest();
let upgrade_request = ClientRequest::UpgradeKey {
hkdf_salt,
derived_key_digest,
}
.encrypt(legacy_key)?;
info!("sending upgrade request and awaiting the acknowledgement back");
let (ciphertext, nonce) = match self.send_websocket_message(upgrade_request).await? {
ServerResponse::EncryptedResponse { ciphertext, nonce } => (ciphertext, nonce),
ServerResponse::Error { message } => {
return Err(GatewayClientError::GatewayError(message))
}
other => return Err(GatewayClientError::UnexpectedResponse { name: other.name() }),
};
// attempt to decrypt it using NEW key
let Ok(response) = SensitiveServerResponse::decrypt(&ciphertext, &nonce, &updated_key)
else {
return Err(GatewayClientError::FatalKeyUpgradeFailure);
};
match response {
SensitiveServerResponse::KeyUpgradeAck { .. } => {
info!("received key upgrade acknowledgement")
}
_ => return Err(GatewayClientError::FatalKeyUpgradeFailure),
}
// perform in memory swap and make a copy for updating storage
let zeroizing_updated_key = updated_key.zeroizing_clone();
self.shared_key = Some(Arc::new(updated_key.into()));
Ok(zeroizing_updated_key)
}
async fn authenticate(&mut self) -> Result<(), GatewayClientError> {
let Some(shared_key) = self.shared_key.as_ref() else {
return Err(GatewayClientError::NoSharedKeyAvailable);
};
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
debug!("authenticating with gateway");
// because of the previous check one of the unwraps MUST succeed
let shared_key = shared_key
.as_ref()
.unwrap_or_else(|| self.shared_key.as_ref().unwrap());
let iv = IV::new_random(&mut rng);
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 = ClientControlRequest::new_authenticate(
self_address,
encrypted_address,
iv,
shared_key,
self.cfg.bandwidth.require_tickets,
)
.into();
)?;
match self.send_websocket_message(msg).await? {
ServerResponse::Authenticate {
@@ -496,39 +565,101 @@ impl<C, St> GatewayClient<C, St> {
self.negotiated_protocol = protocol_version;
log::debug!("authenticated: {status}, bandwidth remaining: {bandwidth_remaining}");
self.task_client.send_status_msg(Box::new(
BandwidthStatusMessage::RemainingBandwidth(bandwidth_remaining),
));
Ok(())
}
ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)),
_ => Err(GatewayClientError::UnexpectedResponse),
other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }),
}
}
/// Helper method to either call register or authenticate based on self.shared_key value
#[instrument(skip_all,
fields(
gateway = %self.gateway_identity,
gateway_address = %self.gateway_address
)
)]
pub async fn perform_initial_authentication(
&mut self,
) -> Result<Arc<SharedKeys>, GatewayClientError> {
) -> Result<AuthenticationResponse, GatewayClientError> {
if !self.connection.is_established() {
self.establish_connection().await?;
}
// 1. check gateway's protocol version
let supports_aes_gcm_siv = match self.get_gateway_protocol().await {
Ok(protocol) => protocol >= AES_GCM_SIV_PROTOCOL_VERSION,
Err(_) => {
// if we failed to send the request, it means the gateway is running the old binary,
// so it has reset our connection - we have to reconnect
self.establish_connection().await?;
false
}
};
if !supports_aes_gcm_siv {
warn!("this gateway is on an old version that doesn't support AES256-GCM-SIV");
}
if self.authenticated {
debug!("Already authenticated");
return if let Some(shared_key) = &self.shared_key {
Ok(Arc::clone(shared_key))
Ok(AuthenticationResponse {
initial_shared_key: Arc::clone(shared_key),
requires_key_upgrade: shared_key.is_legacy() && supports_aes_gcm_siv,
})
} else {
Err(GatewayClientError::AuthenticationFailureWithPreexistingSharedKey)
};
}
if self.shared_key.is_some() {
self.authenticate(None).await?;
self.authenticate().await?;
if self.authenticated {
// if we are authenticated it means we MUST have an associated shared_key
let shared_key = self.shared_key.as_ref().unwrap();
let requires_key_upgrade = shared_key.is_legacy() && supports_aes_gcm_siv;
Ok(AuthenticationResponse {
initial_shared_key: Arc::clone(shared_key),
requires_key_upgrade,
})
} else {
Err(GatewayClientError::AuthenticationFailure)
}
} else {
self.register().await?;
self.register(supports_aes_gcm_siv).await?;
// if registration didn't return an error, we MUST have an associated shared key
let shared_key = self.shared_key.as_ref().unwrap();
// we're always registering with the highest supported protocol,
// so no upgrades are required
Ok(AuthenticationResponse {
initial_shared_key: Arc::clone(shared_key),
requires_key_upgrade: false,
})
}
if self.authenticated {
// if we are authenticated it means we MUST have an associated shared_key
Ok(Arc::clone(self.shared_key.as_ref().unwrap()))
} else {
Err(GatewayClientError::AuthenticationFailure)
}
pub async fn get_gateway_protocol(&mut self) -> Result<u8, GatewayClientError> {
if !self.connection.is_established() {
return Err(GatewayClientError::ConnectionNotEstablished);
}
match self
.send_websocket_message(ClientControlRequest::SupportedProtocol {})
.await?
{
ServerResponse::SupportedProtocol { version } => Ok(version),
ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)),
other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }),
}
}
@@ -536,22 +667,17 @@ impl<C, St> GatewayClient<C, St> {
&mut self,
credential: CredentialSpendingData,
) -> Result<(), GatewayClientError> {
let mut rng = OsRng;
let iv = IV::new_random(&mut rng);
let msg = ClientControlRequest::new_enc_ecash_credential(
credential,
self.shared_key.as_ref().unwrap(),
iv,
)
.into();
)?;
let bandwidth_remaining = match self.send_websocket_message(msg).await? {
ServerResponse::Bandwidth { available_total } => Ok(available_total),
ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)),
ServerResponse::TypedError { error } => {
Err(GatewayClientError::TypedGatewayError(error))
}
_ => Err(GatewayClientError::UnexpectedResponse),
other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }),
}?;
// TODO: create tracing span
@@ -562,11 +688,11 @@ impl<C, St> GatewayClient<C, St> {
}
async fn try_claim_testnet_bandwidth(&mut self) -> Result<(), GatewayClientError> {
let msg = ClientControlRequest::ClaimFreeTestnetBandwidth.into();
let msg = ClientControlRequest::ClaimFreeTestnetBandwidth;
let bandwidth_remaining = match self.send_websocket_message(msg).await? {
ServerResponse::Bandwidth { available_total } => Ok(available_total),
ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)),
_ => Err(GatewayClientError::UnexpectedResponse),
other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }),
}?;
info!("managed to claim testnet bandwidth");
@@ -665,10 +791,10 @@ impl<C, St> GatewayClient<C, St> {
return Err(GatewayClientError::ConnectionNotEstablished);
}
let messages: Vec<_> = packets
let messages: Result<Vec<_>, _> = packets
.into_iter()
.map(|mix_packet| {
BinaryRequest::new_forward_request(mix_packet).into_ws_message(
BinaryRequest::ForwardSphinx { packet: mix_packet }.into_ws_message(
self.shared_key
.as_ref()
.expect("no shared key present even though we're authenticated!"),
@@ -677,7 +803,7 @@ impl<C, St> GatewayClient<C, St> {
.collect();
if let Err(err) = self
.batch_send_websocket_messages_without_response(messages)
.batch_send_websocket_messages_without_response(messages?)
.await
{
if err.is_closed_connection() && self.cfg.connection.should_reconnect_on_failure {
@@ -741,11 +867,11 @@ impl<C, St> GatewayClient<C, St> {
}
// note: into_ws_message encrypts the requests and adds a MAC on it. Perhaps it should
// be more explicit in the naming?
let msg = BinaryRequest::new_forward_request(mix_packet).into_ws_message(
let msg = BinaryRequest::ForwardSphinx { packet: mix_packet }.into_ws_message(
self.shared_key
.as_ref()
.expect("no shared key present even though we're authenticated!"),
);
)?;
self.send_with_reconnection_on_failure(msg).await
}
@@ -805,8 +931,8 @@ impl<C, St> GatewayClient<C, St> {
self.establish_connection().await?;
}
// TODO: the name of this method is very deceiving
self.perform_initial_authentication().await?;
// if we're reconnecting, because we lost connection, we need to re-authenticate the connection
self.authenticate().await?;
// this call is NON-blocking
self.start_listening_for_mixnet_messages()?;
@@ -820,16 +946,16 @@ impl<C, St> GatewayClient<C, St> {
Ok(())
}
pub async fn authenticate_and_start(&mut self) -> Result<Arc<SharedKeys>, GatewayClientError>
pub async fn claim_initial_bandwidth(&mut self) -> Result<(), GatewayClientError>
where
C: DkgQueryClient + Send + Sync,
St: CredentialStorage,
<St as CredentialStorage>::StorageError: Send + Sync + 'static,
{
if !self.connection.is_established() {
self.establish_connection().await?;
if !self.authenticated {
return Err(GatewayClientError::NotAuthenticated);
}
let shared_key = self.perform_initial_authentication().await?;
let bandwidth_remaining = self.bandwidth.remaining();
if bandwidth_remaining < self.cfg.bandwidth.remaining_bandwidth_threshold {
self.cfg
@@ -838,6 +964,20 @@ impl<C, St> GatewayClient<C, St> {
info!("Claiming more bandwidth with existing credentials. Stop the process now if you don't want that to happen.");
self.claim_bandwidth().await?;
}
Ok(())
}
#[deprecated(note = "this method does not deal with upgraded keys for legacy clients")]
pub async fn authenticate_and_start(
&mut self,
) -> Result<AuthenticationResponse, GatewayClientError>
where
C: DkgQueryClient + Send + Sync,
St: CredentialStorage,
<St as CredentialStorage>::StorageError: Send + Sync + 'static,
{
let shared_key = self.perform_initial_authentication().await?;
self.claim_initial_bandwidth().await?;
// this call is NON-blocking
self.start_listening_for_mixnet_messages()?;
+15 -3
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use nym_gateway_requests::registration::handshake::error::HandshakeError;
use nym_gateway_requests::SimpleGatewayRequestsError;
use nym_gateway_requests::{GatewayRequestsError, SimpleGatewayRequestsError};
use std::io;
use thiserror::Error;
use tungstenite::Error as WsError;
@@ -21,9 +21,21 @@ pub enum GatewayClientError {
#[error("gateway returned an error response: {0}")]
TypedGatewayError(SimpleGatewayRequestsError),
#[error("request error: {0}")]
RequestError(#[from] GatewayRequestsError),
#[error("There was a network error: {0}")]
NetworkError(#[from] WsError),
#[error("failed to upgrade our shared key - the gateway sent malformed response")]
FatalKeyUpgradeFailure,
#[error("the current key is already up to date! there's no need to upgrade it")]
KeyAlreadyUpgraded,
#[error("can't perform key upgrade as the key is already being used elsewhere")]
KeyAlreadyInUse,
#[cfg(target_arch = "wasm32")]
#[error("There was a network error: {0}")]
NetworkErrorWasm(#[from] JsError),
@@ -73,8 +85,8 @@ pub enum GatewayClientError {
cutoff_bi2: String,
},
#[error("Received an unexpected response")]
UnexpectedResponse,
#[error("received an unexpected response of type {name}")]
UnexpectedResponse { name: String },
#[error("Connection is in an invalid state - please send a bug report")]
ConnectionInInvalidState,
+10 -4
View File
@@ -2,12 +2,14 @@
// SPDX-License-Identifier: Apache-2.0
use crate::error::GatewayClientError;
use log::warn;
use nym_gateway_requests::BinaryResponse;
use tracing::{error, warn};
use tungstenite::{protocol::Message, Error as WsError};
pub use client::{config::GatewayClientConfig, GatewayClient, GatewayConfig};
pub use nym_gateway_requests::registration::handshake::SharedKeys;
pub use nym_gateway_requests::shared_key::{
LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey,
};
pub use packet_router::{
AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender,
PacketRouter,
@@ -45,11 +47,15 @@ pub(crate) fn cleanup_socket_messages(
pub(crate) fn try_decrypt_binary_message(
bin_msg: Vec<u8>,
shared_keys: &SharedKeys,
shared_keys: &SharedGatewayKey,
) -> Option<Vec<u8>> {
match BinaryResponse::try_from_encrypted_tagged_bytes(bin_msg, shared_keys) {
Ok(bin_response) => match bin_response {
BinaryResponse::PushedMixMessage(plaintext) => Some(plaintext),
BinaryResponse::PushedMixMessage { message } => Some(message),
_ => {
error!("received unhandled binary response");
None
}
},
Err(err) => {
warn!("message received from the gateway was malformed! - {err}",);
@@ -44,7 +44,7 @@ impl PacketRouter {
// having already been dropped
if self.shutdown.is_shutdown_poll() || self.shutdown.is_dummy() {
// This should ideally not happen, but it's ok
log::warn!("Failed to send mixnet messages due to receiver task shutdown");
tracing::warn!("Failed to send mixnet messages due to receiver task shutdown");
return Err(GatewayClientError::ShutdownInProgress);
}
// This should never happen during ordinary operation the way it's currently used.
@@ -60,7 +60,7 @@ impl PacketRouter {
// having already been dropped
if self.shutdown.is_shutdown_poll() || self.shutdown.is_dummy() {
// This should ideally not happen, but it's ok
log::warn!("Failed to send acks due to receiver task shutdown");
tracing::warn!("Failed to send acks due to receiver task shutdown");
return Err(GatewayClientError::ShutdownInProgress);
}
// This should never happen during ordinary operation the way it's currently used.
@@ -9,15 +9,15 @@ use crate::{cleanup_socket_messages, try_decrypt_binary_message};
use futures::channel::oneshot;
use futures::stream::{SplitSink, SplitStream};
use futures::{SinkExt, StreamExt};
use log::*;
use nym_gateway_requests::registration::handshake::SharedKeys;
use nym_gateway_requests::shared_key::SharedGatewayKey;
use nym_gateway_requests::{ServerResponse, SimpleGatewayRequestsError};
use nym_task::TaskClient;
use si_scale::helpers::bibytes2;
use std::os::raw::c_int as RawFd;
use std::sync::Arc;
use tracing::*;
use tungstenite::{protocol::Message, Error as WsError};
use si_scale::helpers::bibytes2;
#[cfg(unix)]
use std::os::fd::AsRawFd;
#[cfg(not(target_arch = "wasm32"))]
@@ -62,7 +62,7 @@ pub(crate) struct PartiallyDelegatedHandle {
struct PartiallyDelegatedRouter {
packet_router: PacketRouter,
shared_key: Arc<SharedKeys>,
shared_key: Arc<SharedGatewayKey>,
client_bandwidth: ClientBandwidth,
stream_return: SplitStreamSender,
@@ -72,7 +72,7 @@ struct PartiallyDelegatedRouter {
impl PartiallyDelegatedRouter {
fn new(
packet_router: PacketRouter,
shared_key: Arc<SharedKeys>,
shared_key: Arc<SharedGatewayKey>,
client_bandwidth: ClientBandwidth,
stream_return: SplitStreamSender,
stream_return_requester: oneshot::Receiver<()>,
@@ -247,7 +247,7 @@ impl PartiallyDelegatedHandle {
pub(crate) fn split_and_listen_for_mixnet_messages(
conn: WsConn,
packet_router: PacketRouter,
shared_key: Arc<SharedKeys>,
shared_key: Arc<SharedGatewayKey>,
client_bandwidth: ClientBandwidth,
shutdown: TaskClient,
) -> Self {
@@ -1,9 +1,9 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use log::{error, trace, warn};
use nym_sphinx::addressing::nodes::MAX_NODE_ADDRESS_UNPADDED_LEN;
use nym_sphinx::params::PacketSize;
use tracing::{error, trace, warn};
pub trait GatewayPacketRouter {
type Error: std::error::Error;
+2 -2
View File
@@ -8,11 +8,11 @@ license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
dirs = { version = "5.0.1", optional = true }
dirs = { workspace = true, optional = true }
handlebars = { workspace = true }
log = { workspace = true }
serde = { workspace = true, features = ["derive"] }
toml = "0.7.4"
toml = { workspace = true }
url = { workspace = true }
nym-network-defaults = { path = "../network-defaults", features = ["utoipa"] }
@@ -20,4 +20,4 @@ thiserror = { workspace = true }
serde_json = { workspace = true }
[build-dependencies]
vergen = { version = "=8.3.1", features = ["build", "git", "gitcl", "rustc", "cargo"] }
vergen = { workspace = true, features = ["build", "git", "gitcl", "rustc", "cargo"] }
@@ -4,11 +4,9 @@ version = "0.1.0"
edition = "2021"
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cosmwasm-schema = { workspace = true }
cw4 = { workspace = true }
cw-controllers = { workspace = true }
cw4 = { workspace = true }
schemars = { workspace = true }
serde = { version = "1.0.210", default-features = false, features = ["derive"] }
serde = { workspace = true, features = ["derive"] }
@@ -9,7 +9,7 @@ license = { workspace = true }
repository = { workspace = true }
[dependencies]
bs58 = "0.5.1"
bs58 = { workspace = true }
cosmwasm-std = { workspace = true }
cosmwasm-schema = { workspace = true }
cw-controllers = { workspace = true }
@@ -4,15 +4,13 @@ version = "0.1.0"
edition = "2021"
license.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cosmwasm-schema = { workspace = true }
cosmwasm-std = { workspace = true }
cw-storage-plus = { workspace = true }
cw-utils = { workspace = true }
cw3 = { workspace = true }
cw4 = { workspace = true }
cw-storage-plus = { workspace = true }
cosmwasm-schema = { workspace = true }
cosmwasm-std = { workspace = true }
schemars = { workspace = true }
serde = { version = "1.0.210", default-features = false, features = ["derive"] }
serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
+5 -2
View File
@@ -8,7 +8,9 @@ license = { workspace = true }
repository = { workspace = true }
[dependencies]
aes-gcm-siv = { workspace = true, optional = true }
aes = { workspace = true, optional = true }
aead = { workspace = true, optional = true }
bs58 = { workspace = true }
blake3 = { workspace = true, features = ["traits-preview"], optional = true }
ctr = { workspace = true, optional = true }
@@ -21,7 +23,7 @@ x25519-dalek = { workspace = true, features = ["static_secrets"], optional = tru
ed25519-dalek = { workspace = true, features = ["rand_core"], optional = true }
rand = { workspace = true, optional = true }
serde_bytes = { workspace = true, optional = true }
serde = { version = "1.0", optional = true, default-features = false, features = ["derive"] }
serde = { workspace = true, features = ["derive"], optional = true }
subtle-encoding = { workspace = true, features = ["bech32-preview"] }
thiserror = { workspace = true }
zeroize = { workspace = true, optional = true, features = ["zeroize_derive"] }
@@ -35,9 +37,10 @@ rand_chacha = { workspace = true }
[features]
default = ["sphinx"]
aead = ["dep:aead", "aead/std", "aes-gcm-siv", "generic-array"]
serde = ["dep:serde", "serde_bytes", "ed25519-dalek/serde", "x25519-dalek/serde"]
asymmetric = ["x25519-dalek", "ed25519-dalek", "zeroize"]
hashing = ["blake3", "digest", "hkdf", "hmac", "generic-array"]
symmetric = ["aes", "ctr", "cipher", "generic-array"]
stream_cipher = ["aes", "ctr", "cipher", "generic-array"]
sphinx = ["nym-sphinx-types/sphinx"]
outfox = ["nym-sphinx-types/outfox"]
+6 -5
View File
@@ -10,21 +10,22 @@ pub mod crypto_hash;
pub mod hkdf;
#[cfg(feature = "hashing")]
pub mod hmac;
#[cfg(all(feature = "asymmetric", feature = "hashing", feature = "symmetric"))]
#[cfg(all(feature = "asymmetric", feature = "hashing", feature = "stream_cipher"))]
pub mod shared_key;
#[cfg(feature = "symmetric")]
pub mod symmetric;
#[cfg(feature = "hashing")]
pub use digest::{Digest, OutputSizeUser};
#[cfg(any(feature = "hashing", feature = "symmetric"))]
#[cfg(any(feature = "hashing", feature = "stream_cipher", feature = "aead"))]
pub use generic_array;
// with the below my idea was to try to introduce having a single place of importing all hashing, encryption,
// etc. algorithms and import them elsewhere as needed via common/crypto
#[cfg(feature = "symmetric")]
#[cfg(feature = "stream_cipher")]
pub use aes;
#[cfg(feature = "aead")]
pub use aes_gcm_siv::{Aes128GcmSiv, Aes256GcmSiv};
#[cfg(feature = "hashing")]
pub use blake3;
#[cfg(feature = "symmetric")]
#[cfg(feature = "stream_cipher")]
pub use ctr;
+98
View File
@@ -0,0 +1,98 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use aead::{Aead, AeadCore, AeadInPlace, Buffer, KeyInit, Payload};
use generic_array::typenum::Unsigned;
#[cfg(feature = "rand")]
use rand::{CryptoRng, RngCore};
pub use aead::{Error as AeadError, Key as AeadKey, KeySizeUser, Nonce, Tag};
#[cfg(feature = "rand")]
pub fn generate_key<A, R>(rng: &mut R) -> AeadKey<A>
where
A: KeyInit,
R: RngCore + CryptoRng,
{
let mut key = AeadKey::<A>::default();
rng.fill_bytes(&mut key);
key
}
#[cfg(feature = "rand")]
pub fn random_nonce<A, R>(rng: &mut R) -> Nonce<A>
where
A: AeadCore,
R: RngCore + CryptoRng,
{
<A as AeadCore>::generate_nonce(rng)
}
pub fn nonce_size<A>() -> usize
where
A: AeadCore,
{
<<A as AeadCore>::NonceSize>::to_usize()
}
pub fn tag_size<A>() -> usize
where
A: AeadCore,
{
<<A as AeadCore>::TagSize>::to_usize()
}
#[inline]
pub fn encrypt<'msg, 'aad, A>(
key: &AeadKey<A>,
nonce: &Nonce<A>,
plaintext: impl Into<Payload<'msg, 'aad>>,
) -> Result<Vec<u8>, AeadError>
where
A: Aead + KeyInit,
{
let cipher = A::new(key);
cipher.encrypt(nonce, plaintext)
}
#[inline]
pub fn decrypt<'msg, 'aad, A>(
key: &AeadKey<A>,
nonce: &Nonce<A>,
ciphertext: impl Into<Payload<'msg, 'aad>>,
) -> Result<Vec<u8>, AeadError>
where
A: Aead + KeyInit,
{
let cipher = A::new(key);
cipher.decrypt(nonce, ciphertext)
}
#[inline]
pub fn encrypt_in_place<A>(
key: &AeadKey<A>,
nonce: &Nonce<A>,
associated_data: &[u8],
buffer: &mut dyn Buffer,
) -> Result<(), AeadError>
where
A: AeadInPlace + KeyInit,
{
let cipher = A::new(key);
cipher.encrypt_in_place(nonce, associated_data, buffer)
}
#[inline]
pub fn decrypt_in_place<A>(
key: &AeadKey<A>,
nonce: &Nonce<A>,
associated_data: &[u8],
buffer: &mut dyn Buffer,
) -> Result<(), AeadError>
where
A: AeadInPlace + KeyInit,
{
let cipher = A::new(key);
cipher.decrypt_in_place(nonce, associated_data, buffer)
}
+3
View File
@@ -1,4 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
#[cfg(feature = "aead")]
pub mod aead;
#[cfg(feature = "stream_cipher")]
pub mod stream_cipher;
+13 -4
View File
@@ -2,12 +2,14 @@
// SPDX-License-Identifier: Apache-2.0
use cipher::{Iv, StreamCipher};
pub use cipher::{IvSizeUser, KeyIvInit, KeySizeUser};
use generic_array::typenum::Unsigned;
#[cfg(feature = "rand")]
use rand::{CryptoRng, RngCore};
// re-export this for ease of use
pub use cipher::Key as CipherKey;
pub use cipher::{IvSizeUser, KeyIvInit, KeySizeUser};
// SECURITY:
// TODO: note that this is not the most secure approach here
@@ -36,7 +38,7 @@ where
#[cfg(feature = "rand")]
pub fn random_iv<C, R>(rng: &mut R) -> IV<C>
where
C: KeyIvInit,
C: IvSizeUser,
R: RngCore + CryptoRng,
{
let mut iv = IV::<C>::default();
@@ -44,16 +46,23 @@ where
iv
}
pub fn iv_size<C>() -> usize
where
C: IvSizeUser,
{
<<C as IvSizeUser>::IvSize>::to_usize()
}
pub fn zero_iv<C>() -> IV<C>
where
C: KeyIvInit,
C: IvSizeUser,
{
Iv::<C>::default()
}
pub fn iv_from_slice<C>(b: &[u8]) -> &IV<C>
where
C: KeyIvInit,
C: IvSizeUser,
{
if b.len() != C::iv_size() {
// `from_slice` would have caused a panic about this issue anyway.
+1
View File
@@ -166,6 +166,7 @@ impl Ciphertexts {
#[derive(Zeroize)]
#[zeroize(drop)]
/// Randomness generated during ciphertext generation that is required for proofs of knowledge.
///
/// It must be handled with extreme care as its misuse might help malicious parties to recover
/// the underlying plaintext.
pub struct HazmatRandomness {
+2
View File
@@ -320,6 +320,8 @@ impl<'a> TryFrom<&'a nym_contracts_common::dealings::ContractSafeBytes> for Deal
}
}
/// Try to recover the verification keys from the provided dealings.
///
/// Attempt to run the `VkCombine` algorithm to obtain the public master verification key, `VK`
/// alongside shares of the verification key, `shvk_{1}`, `shvk_{2}`, ... `svhk_{n}`, where n is the number of receivers.
///
+4
View File
@@ -64,6 +64,8 @@ fn generate_lagrangian_coefficients_at_x(
Ok(res)
}
/// Lagrange interpolation at x.
///
/// Performs a Lagrange interpolation at specified x for a polynomial defined by set of coordinates
/// (x, f(x)), where x is a `Scalar` and f(x) is a generic type that can be obtained by evaluating `f` at `x`.
/// It can be used for Scalars, G1 and G2 points.
@@ -85,6 +87,8 @@ where
.sum())
}
/// Lagrange interpolation at the origin.
///
/// Performs a Lagrange interpolation at the origin for a polynomial defined by set of coordinates
/// (x, f(x)), where x is a `Scalar` and f(x) is a generic type that can be obtained by evaluating `f` at `x`.
/// It can be used for Scalars, G1 and G2 points.
+2 -1
View File
@@ -17,11 +17,12 @@ generic-array = { workspace = true, features = ["serde"] }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
strum = { workspace = true }
thiserror = { workspace = true }
tracing = { workspace = true, features = ["log"] }
zeroize = { workspace = true }
nym-crypto = { path = "../crypto" }
nym-crypto = { path = "../crypto", features = ["aead", "hashing"] }
nym-pemstore = { path = "../pemstore" }
nym-sphinx = { path = "../nymsphinx" }
nym-task = { path = "../task" }
@@ -1,53 +1,47 @@
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2020-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::iv::IV;
use crate::registration::handshake::shared_key::SharedKeys;
use nym_crypto::symmetric::stream_cipher;
use nym_sphinx::params::GatewayEncryptionAlgorithm;
use nym_sphinx::{DestinationAddressBytes, DESTINATION_ADDRESS_LENGTH};
use crate::shared_key::{SharedGatewayKey, SharedKeyUsageError};
use nym_sphinx::DestinationAddressBytes;
use thiserror::Error;
pub const ENCRYPTED_ADDRESS_SIZE: usize = DESTINATION_ADDRESS_LENGTH;
/// Replacement for what used to be an `AuthToken`.
///
/// 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, PartialEq, Eq, Hash, Clone)]
// this is no longer constant size due to the differences in ciphertext between aes128ctr and aes256gcm-siv (inclusion of tag)
pub struct EncryptedAddressBytes(Vec<u8>);
#[derive(Debug, Error)]
pub enum EncryptedAddressConversionError {
#[error("Failed to decode the encrypted address - {0}")]
DecodeError(#[from] bs58::decode::Error),
#[error("The decoded address has invalid length")]
StringOfInvalidLengthError,
}
impl EncryptedAddressBytes {
pub fn new(address: &DestinationAddressBytes, key: &SharedKeys, iv: &IV) -> Self {
let ciphertext = stream_cipher::encrypt::<GatewayEncryptionAlgorithm>(
key.encryption_key(),
iv.inner(),
address.as_bytes_ref(),
);
pub fn new(
address: &DestinationAddressBytes,
key: &SharedGatewayKey,
nonce: &[u8],
) -> Result<Self, SharedKeyUsageError> {
let ciphertext = key.encrypt_naive(address.as_bytes_ref(), Some(nonce))?;
let mut enc_address = [0u8; ENCRYPTED_ADDRESS_SIZE];
enc_address.copy_from_slice(&ciphertext[..]);
EncryptedAddressBytes(enc_address)
Ok(EncryptedAddressBytes(ciphertext))
}
pub fn verify(&self, address: &DestinationAddressBytes, key: &SharedKeys, iv: &IV) -> 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 verify(
&self,
address: &DestinationAddressBytes,
key: &SharedGatewayKey,
nonce: &[u8],
) -> bool {
let Ok(reconstructed) = Self::new(address, key, nonce) else {
return false;
};
self == &reconstructed
}
pub fn as_bytes(&self) -> &[u8] {
@@ -58,14 +52,7 @@ impl EncryptedAddressBytes {
val: S,
) -> Result<Self, EncryptedAddressConversionError> {
let decoded = bs58::decode(val.into()).into_vec()?;
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))
Ok(EncryptedAddressBytes(decoded))
}
pub fn to_base58_string(self) -> String {
-76
View File
@@ -1,76 +0,0 @@
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_crypto::generic_array::{typenum::Unsigned, GenericArray};
use nym_crypto::symmetric::stream_cipher::{random_iv, IvSizeUser, IV as CryptoIV};
use nym_sphinx::params::GatewayEncryptionAlgorithm;
use rand::{CryptoRng, RngCore};
use thiserror::Error;
type NonceSize = <GatewayEncryptionAlgorithm as IvSizeUser>::IvSize;
// I think 'IV' looks better than 'Iv', feel free to change that.
#[allow(clippy::upper_case_acronyms)]
pub struct IV(CryptoIV<GatewayEncryptionAlgorithm>);
#[derive(Error, Debug)]
// I think 'IV' looks better than 'Iv', feel free to change that.
#[allow(clippy::upper_case_acronyms)]
pub enum IVConversionError {
#[error("Failed to decode the iv - {0}")]
DecodeError(#[from] bs58::decode::Error),
#[error("The decoded bytes iv has invalid length")]
BytesOfInvalidLengthError,
#[error("The decoded string iv has invalid length")]
StringOfInvalidLengthError,
}
impl IV {
pub fn new_random<R: RngCore + CryptoRng>(rng: &mut R) -> Self {
IV(random_iv::<GatewayEncryptionAlgorithm, _>(rng))
}
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, IVConversionError> {
if bytes.len() != NonceSize::to_usize() {
return Err(IVConversionError::BytesOfInvalidLengthError);
}
Ok(IV(GenericArray::clone_from_slice(bytes)))
}
pub fn to_bytes(&self) -> Vec<u8> {
self.0.to_vec()
}
pub fn as_bytes(&self) -> &[u8] {
self.0.as_ref()
}
pub fn inner(&self) -> &CryptoIV<GatewayEncryptionAlgorithm> {
&self.0
}
pub fn try_from_base58_string<S: Into<String>>(val: S) -> Result<Self, IVConversionError> {
let decoded = bs58::decode(val.into()).into_vec()?;
if decoded.len() != NonceSize::to_usize() {
return Err(IVConversionError::StringOfInvalidLengthError);
}
Ok(IV(
GenericArray::from_exact_iter(decoded).expect("Invalid vector length!")
))
}
pub fn to_base58_string(&self) -> String {
bs58::encode(self.to_bytes()).into_string()
}
}
impl From<IV> for String {
fn from(iv: IV) -> Self {
iv.to_base58_string()
}
}
+12 -6
View File
@@ -2,29 +2,35 @@
// SPDX-License-Identifier: Apache-2.0
pub use nym_crypto::generic_array;
use nym_crypto::hmac::HmacOutput;
use nym_crypto::OutputSizeUser;
use nym_sphinx::params::GatewayIntegrityHmacAlgorithm;
pub use types::*;
pub mod authentication;
pub mod iv;
pub mod models;
pub mod registration;
pub mod shared_key;
pub mod types;
pub const CURRENT_PROTOCOL_VERSION: u8 = CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION;
pub use shared_key::helpers::SymmetricKey;
pub use shared_key::legacy::{LegacySharedKeySize, LegacySharedKeys};
pub use shared_key::{
SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey,
};
pub const CURRENT_PROTOCOL_VERSION: u8 = AES_GCM_SIV_PROTOCOL_VERSION;
/// Defines the current version of the communication protocol between gateway and clients.
/// It has to be incremented for any breaking change.
// history:
// 1 - initial release
// 2 - changes to client credentials structure
// 3 - change to AES-GCM-SIV and non-zero IVs
pub const INITIAL_PROTOCOL_VERSION: u8 = 1;
pub const CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION: u8 = 2;
pub type GatewayMac = HmacOutput<GatewayIntegrityHmacAlgorithm>;
pub const AES_GCM_SIV_PROTOCOL_VERSION: u8 = 3;
// TODO: could using `Mac` trait here for OutputSize backfire?
// Should hmac itself be exposed, imported and used instead?
pub type GatewayMacSize = <GatewayIntegrityHmacAlgorithm as OutputSizeUser>::OutputSize;
pub type LegacyGatewayMacSize = <GatewayIntegrityHmacAlgorithm as OutputSizeUser>::OutputSize;
@@ -1,129 +1,62 @@
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::registration::handshake::shared_key::SharedKeys;
use crate::registration::handshake::messages::{Finalization, GatewayMaterialExchange};
use crate::registration::handshake::state::State;
use crate::registration::handshake::SharedGatewayKey;
use crate::registration::handshake::{error::HandshakeError, WsItem};
use futures::future::BoxFuture;
use futures::task::{Context, Poll};
use futures::{Future, Sink, Stream};
use nym_crypto::asymmetric::encryption::PUBLIC_KEY_SIZE;
use nym_crypto::asymmetric::identity::SIGNATURE_LENGTH;
use nym_crypto::asymmetric::{encryption, identity};
use futures::{Sink, Stream};
use rand::{CryptoRng, RngCore};
use std::pin::Pin;
use tungstenite::Message as WsMessage;
pub(crate) struct ClientHandshake<'a> {
handshake_future: BoxFuture<'a, Result<SharedKeys, HandshakeError>>,
}
impl<'a> ClientHandshake<'a> {
pub(crate) fn new<S>(
rng: &mut (impl RngCore + CryptoRng),
ws_stream: &'a mut S,
identity: &'a nym_crypto::asymmetric::identity::KeyPair,
gateway_pubkey: identity::PublicKey,
expects_credential_usage: bool,
#[cfg(not(target_arch = "wasm32"))] shutdown: nym_task::TaskClient,
) -> Self
impl<'a, S, R> State<'a, S, R> {
async fn client_handshake_inner(&mut self) -> Result<(), HandshakeError>
where
S: Stream<Item = WsItem> + Sink<WsMessage> + Unpin + Send + 'a,
S: Stream<Item = WsItem> + Sink<WsMessage> + Unpin,
R: CryptoRng + RngCore,
{
let mut state = State::new(
rng,
ws_stream,
identity,
Some(gateway_pubkey),
expects_credential_usage,
#[cfg(not(target_arch = "wasm32"))]
shutdown,
);
// 1. if we're using non-legacy, i.e. aes256gcm-siv derivation, generate initiator salt for kdf
let maybe_hkdf_salt = self.maybe_generate_initiator_salt();
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<T, S>(
result: Result<T, HandshakeError>,
state: &mut State<'_, S>,
) -> Result<T, HandshakeError>
where
S: Sink<WsMessage> + Unpin,
{
match result {
Ok(ok) => Ok(ok),
Err(err) => {
state.send_handshake_error(err.to_string()).await?;
Err(err)
}
}
}
// 1. send ed25519 pubkey alongside ephemeral x25519 pubkey and a hkdf salt if we're using non-legacy client
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_SALT
let init_message = self.init_message(maybe_hkdf_salt.clone());
self.send_handshake_data(init_message).await?;
let init_message = state.init_message();
state.send_handshake_data(init_message).await?;
// 2. wait for response with remote x25519 pubkey as well as encrypted signature
// <- g^y || AES(k, sig(gate_priv, (g^y || g^x)) || MAYBE_NONCE
let mid_res = self
.receive_handshake_message::<GatewayMaterialExchange>()
.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?;
// 3. derive shared keys locally
// hkdf::<blake3>::(g^xy)
self.derive_shared_key(&mid_res.ephemeral_dh, maybe_hkdf_salt.as_deref());
// hkdf::<blake3>::(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?;
// 4. verify the received signature using the locally derived keys
self.verify_remote_key_material(&mid_res.materials, &mid_res.ephemeral_dh)?;
// AES(k, sig(client_priv, (g^y || g^x))
let material = state.prepare_key_material_sig(&remote_ephemeral_key);
// 5. produce our own materials to get verified by the remote
// -> AES(k, sig(client_priv, g^x || g^y)) || MAYBE_NONCE
let materials = self.prepare_key_material_sig(&mid_res.ephemeral_dh)?;
self.send_handshake_data(materials).await?;
// -> 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())
}),
}
// 6. wait for remote confirmation of finalizing the handshake
let finalization = self.receive_handshake_message::<Finalization>().await?;
finalization.ensure_success()?;
Ok(())
}
// client should have received
// G^y || AES(k, SIG(PRIV_GATE, G^y || G^x))
fn parse_mid_response(
mut resp: Vec<u8>,
) -> Result<(encryption::PublicKey, Vec<u8>), 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<u8>) -> 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<SharedKeys, HandshakeError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.handshake_future).poll(cx)
pub(crate) async fn perform_client_handshake(
mut self,
) -> Result<SharedGatewayKey, HandshakeError>
where
S: Stream<Item = WsItem> + Sink<WsMessage> + Unpin,
R: CryptoRng + RngCore,
{
let handshake_res = self.client_handshake_inner().await;
self.check_for_handshake_processing_error(handshake_res)
.await?;
Ok(self.finalize_handshake())
}
}
@@ -1,16 +1,20 @@
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_crypto::asymmetric::identity;
use crate::shared_key::SharedKeyUsageError;
use thiserror::Error;
#[derive(Debug, Clone, Error)]
#[derive(Debug, Error)]
pub enum HandshakeError {
#[error(
"received key material of invalid length - {0}. Expected: {}",
identity::SIGNATURE_LENGTH
)]
KeyMaterialOfInvalidSize(usize),
#[error("received key material of invalid length: {received}. Expected: {expected}")]
KeyMaterialOfInvalidSize { received: usize, expected: usize },
#[error("no nonce has been provided for aes256-gcm-siv key derivation")]
MissingNonceForCurrentKey,
#[error(transparent)]
KeyUsageFailure(#[from] SharedKeyUsageError),
#[error("received invalid signature")]
InvalidSignature,
#[error("encountered network error")]
@@ -1,114 +1,66 @@
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::registration::handshake::shared_key::SharedKeys;
use crate::registration::handshake::messages::{
HandshakeMessage, Initialisation, MaterialExchange,
};
use crate::registration::handshake::state::State;
use crate::registration::handshake::SharedGatewayKey;
use crate::registration::handshake::{error::HandshakeError, WsItem};
use futures::future::BoxFuture;
use futures::task::{Context, Poll};
use futures::{Future, Sink, Stream};
use nym_crypto::asymmetric::encryption;
use nym_task::TaskClient;
use rand::{CryptoRng, RngCore};
use std::pin::Pin;
use futures::{Sink, Stream};
use tungstenite::Message as WsMessage;
pub(crate) struct GatewayHandshake<'a> {
handshake_future: BoxFuture<'a, Result<SharedKeys, HandshakeError>>,
}
impl<'a> GatewayHandshake<'a> {
pub(crate) fn new<S>(
rng: &mut (impl RngCore + CryptoRng),
ws_stream: &'a mut S,
identity: &'a nym_crypto::asymmetric::identity::KeyPair,
received_init_payload: Vec<u8>,
shutdown: TaskClient,
) -> Self
impl<'a, S, R> State<'a, S, R> {
async fn gateway_handshake_inner(
&mut self,
raw_init_message: Vec<u8>,
) -> Result<(), HandshakeError>
where
S: Stream<Item = WsItem> + Sink<WsMessage> + Unpin + Send + 'a,
S: Stream<Item = WsItem> + Sink<WsMessage> + Unpin,
{
let mut state = State::new(rng, ws_stream, identity, None, true, shutdown);
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<T, S>(
result: Result<T, HandshakeError>,
state: &mut State<'_, S>,
) -> Result<T, HandshakeError>
where
S: Sink<WsMessage> + Unpin,
{
match result {
Ok(ok) => Ok(ok),
Err(err) => {
state.send_handshake_error(err.to_string()).await?;
Err(err)
}
}
}
// 1. receive remote ed25519 pubkey alongside ephemeral x25519 pubkey and maybe a flag indicating non-legacy client
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_NON_LEGACY
let init_message = Initialisation::try_from_bytes(&raw_init_message)?;
self.update_remote_identity(init_message.identity);
self.set_aes256_gcm_siv_key_derivation(!init_message.is_legacy());
// init: <- pub_key || g^x
let (remote_identity, remote_ephemeral_key) = check_processing_error(
State::<S>::parse_init_message(received_init_payload),
&mut state,
)
.await?;
state.update_remote_identity(remote_identity);
// 2. derive shared keys locally
// hkdf::<blake3>::(g^xy)
self.derive_shared_key(
&init_message.ephemeral_dh,
init_message.initiator_salt.as_deref(),
);
// hkdf::<blake3>::(g^xy)
state.derive_shared_key(&remote_ephemeral_key);
// 3. send ephemeral x25519 pubkey alongside the encrypted signature
// g^y || AES(k, sig(gate_priv, (g^y || g^x))
let material = self
.prepare_key_material_sig(&init_message.ephemeral_dh)?
.attach_ephemeral_dh(*self.local_ephemeral_key());
self.send_handshake_data(material).await?;
// AES(k, sig(gate_priv, (g^y || g^x))
let material = state.prepare_key_material_sig(&remote_ephemeral_key);
// 4. wait for the remote response with their own encrypted signature
let materials = self.receive_handshake_message::<MaterialExchange>().await?;
// 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,
);
// 5. verify the received signature using the locally derived keys
self.verify_remote_key_material(&materials, &init_message.ephemeral_dh)?;
// -> g^y || AES(k, sig(gate_priv, (g^y || g^x))
state.send_handshake_data(handshake_payload).await?;
// 6. finally send the finalization message to conclude the exchange
let finalizer = self.finalization_message();
self.send_handshake_data(finalizer).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())
}),
}
Ok(())
}
// create g^y || AES(k, sig(gate_priv, (g^y || g^x))
fn combine_material_with_ephemeral_key(
ephemeral_key: &encryption::PublicKey,
material: Vec<u8>,
) -> Vec<u8> {
ephemeral_key
.to_bytes()
.iter()
.cloned()
.chain(material)
.collect()
}
fn prepare_finalization_response() -> Vec<u8> {
vec![1]
}
}
impl<'a> Future for GatewayHandshake<'a> {
type Output = Result<SharedKeys, HandshakeError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.handshake_future).poll(cx)
pub(crate) async fn perform_gateway_handshake(
mut self,
raw_init_message: Vec<u8>,
) -> Result<SharedGatewayKey, HandshakeError>
where
S: Stream<Item = WsItem> + Sink<WsMessage> + Unpin,
{
let handshake_res = self.gateway_handshake_inner(raw_init_message).await;
self.check_for_handshake_processing_error(handshake_res)
.await?;
Ok(self.finalize_handshake())
}
}
@@ -0,0 +1,228 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::registration::handshake::error::HandshakeError;
use crate::registration::handshake::KDF_SALT_LENGTH;
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_crypto::symmetric::aead::{nonce_size, tag_size};
use nym_sphinx::params::GatewayEncryptionAlgorithm;
// it is vital nobody changes the serialisation implementation unless you have an EXTREMELY good reason,
// as otherwise you have very high chance of breaking backwards compatibility
pub trait HandshakeMessage {
fn into_bytes(self) -> Vec<u8>;
fn try_from_bytes(bytes: &[u8]) -> Result<Self, HandshakeError>
where
Self: Sized;
}
#[derive(Debug)]
pub struct Initialisation {
pub identity: ed25519::PublicKey,
pub ephemeral_dh: x25519::PublicKey,
pub initiator_salt: Option<Vec<u8>>,
}
impl Initialisation {
#[cfg(not(target_arch = "wasm32"))]
pub fn is_legacy(&self) -> bool {
self.initiator_salt.is_none()
}
}
#[derive(Debug)]
pub struct MaterialExchange {
pub signature_ciphertext: Vec<u8>,
pub nonce: Option<Vec<u8>>,
}
impl MaterialExchange {
#[cfg(not(target_arch = "wasm32"))]
pub fn attach_ephemeral_dh(self, ephemeral_dh: x25519::PublicKey) -> GatewayMaterialExchange {
GatewayMaterialExchange {
ephemeral_dh,
materials: self,
}
}
}
#[derive(Debug)]
pub struct GatewayMaterialExchange {
pub ephemeral_dh: x25519::PublicKey,
pub materials: MaterialExchange,
}
#[derive(Debug)]
pub struct Finalization {
pub success: bool,
}
impl Finalization {
pub fn ensure_success(&self) -> Result<(), HandshakeError> {
if !self.success {
return Err(HandshakeError::HandshakeFailure);
}
Ok(())
}
}
impl HandshakeMessage for Initialisation {
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_SALT
// Eventually the ID_PUBKEY prefix will get removed and recipient will know
// initializer's identity from another source.
fn into_bytes(self) -> Vec<u8> {
let bytes = self
.identity
.to_bytes()
.into_iter()
.chain(self.ephemeral_dh.to_bytes());
if let Some(salt) = self.initiator_salt {
bytes.chain(salt).collect()
} else {
bytes.collect()
}
}
// this will need to be adjusted when REMOTE_ID_PUBKEY is removed
fn try_from_bytes(bytes: &[u8]) -> Result<Self, HandshakeError>
where
Self: Sized,
{
let legacy_len = ed25519::PUBLIC_KEY_LENGTH + x25519::PUBLIC_KEY_SIZE;
let current_len = legacy_len + KDF_SALT_LENGTH;
if bytes.len() != legacy_len && bytes.len() != current_len {
return Err(HandshakeError::MalformedRequest);
}
let identity = ed25519::PublicKey::from_bytes(&bytes[..ed25519::PUBLIC_KEY_LENGTH])
.map_err(|_| HandshakeError::MalformedRequest)?;
// this can only fail if the provided bytes have len different from encryption::PUBLIC_KEY_SIZE
// which is impossible
let ephemeral_dh =
x25519::PublicKey::from_bytes(&bytes[ed25519::PUBLIC_KEY_LENGTH..legacy_len]).unwrap();
let initiator_salt = if bytes.len() == legacy_len {
None
} else {
Some(bytes[legacy_len..].to_vec())
};
Ok(Initialisation {
identity,
ephemeral_dh,
initiator_salt,
})
}
}
impl HandshakeMessage for MaterialExchange {
// AES(k, SIG(PRIV_GATE, G^y || G^x))
fn into_bytes(self) -> Vec<u8> {
if let Some(nonce) = self.nonce {
self.signature_ciphertext
.iter()
.cloned()
.chain(nonce)
.collect()
} else {
self.signature_ciphertext.to_vec()
}
}
fn try_from_bytes(bytes: &[u8]) -> Result<Self, HandshakeError>
where
Self: Sized,
{
// we expect to receive either:
// LEGACY: ed25519 signature ciphertext (64 bytes)
// CURRENT: ed25519 signature ciphertext (+ tag) + AES256-GCM-SIV nonce (76 bytes)
let legacy_len = ed25519::SIGNATURE_LENGTH;
let current_len = legacy_len
+ tag_size::<GatewayEncryptionAlgorithm>()
+ nonce_size::<GatewayEncryptionAlgorithm>();
if bytes.len() != legacy_len && bytes.len() != current_len {
return Err(HandshakeError::MalformedResponse);
}
let (signature_ciphertext, nonce) = if bytes.len() == current_len {
let ciphertext_len =
ed25519::SIGNATURE_LENGTH + tag_size::<GatewayEncryptionAlgorithm>();
(
bytes[..ciphertext_len].to_vec(),
Some(bytes[ciphertext_len..].to_vec()),
)
} else {
(bytes.to_vec(), None)
};
Ok(MaterialExchange {
signature_ciphertext,
nonce,
})
}
}
impl HandshakeMessage for GatewayMaterialExchange {
// G^y || AES(k, SIG(PRIV_GATE, G^y || G^x))
fn into_bytes(self) -> Vec<u8> {
self.ephemeral_dh
.to_bytes()
.into_iter()
.chain(self.materials.into_bytes())
.collect()
}
fn try_from_bytes(bytes: &[u8]) -> Result<Self, HandshakeError>
where
Self: Sized,
{
// we expect to receive either:
// LEGACY: x25519 pubkey + ed25519 signature ciphertext (96 bytes)
// CURRENT: x25519 pubkey + ed25519 signature ciphertext (+ tag)+ AES256-GCM-SIV nonce (124 bytes)
let legacy_len = x25519::PUBLIC_KEY_SIZE + ed25519::SIGNATURE_LENGTH;
let current_len = legacy_len
+ nonce_size::<GatewayEncryptionAlgorithm>()
+ tag_size::<GatewayEncryptionAlgorithm>();
if bytes.len() != legacy_len && bytes.len() != current_len {
return Err(HandshakeError::MalformedResponse);
}
// this can only fail if the provided bytes have len different from PUBLIC_KEY_SIZE
// which is impossible
let ephemeral_dh =
x25519::PublicKey::from_bytes(&bytes[..x25519::PUBLIC_KEY_SIZE]).unwrap();
let materials = MaterialExchange::try_from_bytes(&bytes[x25519::PUBLIC_KEY_SIZE..])?;
Ok(GatewayMaterialExchange {
ephemeral_dh,
materials,
})
}
}
impl HandshakeMessage for Finalization {
fn into_bytes(self) -> Vec<u8> {
if self.success {
vec![1]
} else {
vec![0]
}
}
fn try_from_bytes(bytes: &[u8]) -> Result<Self, HandshakeError>
where
Self: Sized,
{
if bytes.len() != 1 {
return Err(HandshakeError::MalformedResponse);
}
let success = bytes[0] == 1;
Ok(Finalization { success })
}
}
@@ -1,17 +1,20 @@
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2020-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use self::client::ClientHandshake;
use self::error::HandshakeError;
#[cfg(not(target_arch = "wasm32"))]
use self::gateway::GatewayHandshake;
pub use self::shared_key::{SharedKeySize, SharedKeys};
use crate::registration::handshake::state::State;
use crate::SharedGatewayKey;
use futures::future::BoxFuture;
use futures::{Sink, Stream};
use nym_crypto::asymmetric::identity;
use rand::{CryptoRng, RngCore};
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use tungstenite::{Error as WsError, Message as WsMessage};
#[cfg(not(target_arch = "wasm32"))]
use nym_task::TaskClient;
use rand::{CryptoRng, RngCore};
use tungstenite::{Error as WsError, Message as WsMessage};
pub(crate) type WsItem = Result<WsMessage, WsError>;
@@ -19,49 +22,74 @@ mod client;
pub mod error;
#[cfg(not(target_arch = "wasm32"))]
mod gateway;
pub mod shared_key;
mod messages;
mod state;
// realistically even 32bit would have sufficed, so 128 is definitely enough
pub const KDF_SALT_LENGTH: usize = 16;
// 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<WsMessage> and Stream<Item = WsMessage> 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),
pub struct GatewayHandshake<'a> {
handshake_future: BoxFuture<'a, Result<SharedGatewayKey, HandshakeError>>,
}
impl<'a> Future for GatewayHandshake<'a> {
type Output = Result<SharedGatewayKey, HandshakeError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.handshake_future).poll(cx)
}
}
pub fn client_handshake<'a, S, R>(
rng: &'a mut R,
ws_stream: &'a mut S,
identity: &'a identity::KeyPair,
gateway_pubkey: identity::PublicKey,
expects_credential_usage: bool,
derive_aes256_gcm_siv_key: bool,
#[cfg(not(target_arch = "wasm32"))] shutdown: TaskClient,
) -> Result<SharedKeys, HandshakeError>
) -> GatewayHandshake<'a>
where
S: Stream<Item = WsItem> + Sink<WsMessage> + Unpin + Send + 'a,
R: CryptoRng + RngCore + Send,
{
ClientHandshake::new(
let state = State::new(
rng,
ws_stream,
identity,
gateway_pubkey,
expects_credential_usage,
Some(gateway_pubkey),
#[cfg(not(target_arch = "wasm32"))]
shutdown,
)
.await
.with_credential_usage(expects_credential_usage)
.with_aes256_gcm_siv_key(derive_aes256_gcm_siv_key);
GatewayHandshake {
handshake_future: Box::pin(state.perform_client_handshake()),
}
}
#[cfg(not(target_arch = "wasm32"))]
pub async fn gateway_handshake<'a, S>(
rng: &mut (impl RngCore + CryptoRng),
pub fn gateway_handshake<'a, S, R>(
rng: &'a mut R,
ws_stream: &'a mut S,
identity: &'a identity::KeyPair,
received_init_payload: Vec<u8>,
shutdown: TaskClient,
) -> Result<SharedKeys, HandshakeError>
) -> GatewayHandshake<'a>
where
S: Stream<Item = WsItem> + Sink<WsMessage> + Unpin + Send + 'a,
R: CryptoRng + RngCore + Send,
{
GatewayHandshake::new(rng, ws_stream, identity, received_init_payload, shutdown).await
let state = State::new(rng, ws_stream, identity, None, shutdown);
GatewayHandshake {
handshake_future: Box::pin(state.perform_gateway_handshake(received_init_payload)),
}
}
/*
@@ -1,178 +0,0 @@
// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{GatewayMacSize, GatewayRequestsError};
use nym_crypto::generic_array::{
typenum::{Sum, Unsigned, U16},
GenericArray,
};
use nym_crypto::hmac::{compute_keyed_hmac, recompute_keyed_hmac_and_verify_tag};
use nym_crypto::symmetric::stream_cipher::{self, CipherKey, KeySizeUser, IV};
use nym_pemstore::traits::PemStorableKey;
use nym_sphinx::params::{GatewayEncryptionAlgorithm, GatewayIntegrityHmacAlgorithm};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use zeroize::{Zeroize, ZeroizeOnDrop};
// shared key is as long as the encryption key and the MAC key combined.
pub type SharedKeySize = Sum<EncryptionKeySize, MacKeySize>;
// we're using 16 byte long key in sphinx, so let's use the same one here
type MacKeySize = U16;
type EncryptionKeySize = <GatewayEncryptionAlgorithm as KeySizeUser>::KeySize;
/// Shared key used when computing MAC for messages exchanged between client and its gateway.
pub type MacKey = GenericArray<u8, MacKeySize>;
#[derive(Debug, PartialEq, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
pub struct SharedKeys {
encryption_key: CipherKey<GatewayEncryptionAlgorithm>,
mac_key: MacKey,
}
#[derive(Debug, Clone, Copy, Error)]
pub enum SharedKeyConversionError {
#[error("the string representation of the shared keys was malformed - {0}")]
DecodeError(#[from] bs58::decode::Error),
#[error(
"the received shared keys had invalid size. Got: {received}, but expected: {expected}"
)]
InvalidSharedKeysSize { received: usize, expected: usize },
}
impl SharedKeys {
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, SharedKeyConversionError> {
if bytes.len() != SharedKeySize::to_usize() {
return Err(SharedKeyConversionError::InvalidSharedKeysSize {
received: bytes.len(),
expected: SharedKeySize::to_usize(),
});
}
let encryption_key =
GenericArray::clone_from_slice(&bytes[..EncryptionKeySize::to_usize()]);
let mac_key = GenericArray::clone_from_slice(&bytes[EncryptionKeySize::to_usize()..]);
Ok(SharedKeys {
encryption_key,
mac_key,
})
}
/// Encrypts the provided data using the optionally provided initialisation vector,
/// or a 0 value if nothing was given. Then it computes an integrity mac and concatenates it
/// with the previously produced ciphertext.
pub fn encrypt_and_tag(
&self,
data: &[u8],
iv: Option<&IV<GatewayEncryptionAlgorithm>>,
) -> Vec<u8> {
let encrypted_data = match iv {
Some(iv) => stream_cipher::encrypt::<GatewayEncryptionAlgorithm>(
self.encryption_key(),
iv,
data,
),
None => {
let zero_iv = stream_cipher::zero_iv::<GatewayEncryptionAlgorithm>();
stream_cipher::encrypt::<GatewayEncryptionAlgorithm>(
self.encryption_key(),
&zero_iv,
data,
)
}
};
let mac = compute_keyed_hmac::<GatewayIntegrityHmacAlgorithm>(
self.mac_key().as_slice(),
&encrypted_data,
);
mac.into_bytes().into_iter().chain(encrypted_data).collect()
}
pub fn decrypt_tagged(
&self,
enc_data: &[u8],
iv: Option<&IV<GatewayEncryptionAlgorithm>>,
) -> Result<Vec<u8>, GatewayRequestsError> {
let mac_size = GatewayMacSize::to_usize();
if enc_data.len() < mac_size {
return Err(GatewayRequestsError::TooShortRequest);
}
let mac_tag = &enc_data[..mac_size];
let message_bytes = &enc_data[mac_size..];
if !recompute_keyed_hmac_and_verify_tag::<GatewayIntegrityHmacAlgorithm>(
self.mac_key().as_slice(),
message_bytes,
mac_tag,
) {
return Err(GatewayRequestsError::InvalidMac);
}
// couldn't have made the first borrow mutable as you can't have an immutable borrow
// together with a mutable one
let message_bytes_mut = &mut enc_data.to_vec()[mac_size..];
let zero_iv = stream_cipher::zero_iv::<GatewayEncryptionAlgorithm>();
let iv = iv.unwrap_or(&zero_iv);
stream_cipher::decrypt_in_place::<GatewayEncryptionAlgorithm>(
self.encryption_key(),
iv,
message_bytes_mut,
);
Ok(message_bytes_mut.to_vec())
}
pub fn encryption_key(&self) -> &CipherKey<GatewayEncryptionAlgorithm> {
&self.encryption_key
}
pub fn mac_key(&self) -> &MacKey {
&self.mac_key
}
pub fn to_bytes(&self) -> Vec<u8> {
self.encryption_key
.iter()
.copied()
.chain(self.mac_key.iter().copied())
.collect()
}
pub fn try_from_base58_string<S: Into<String>>(
val: S,
) -> Result<Self, SharedKeyConversionError> {
let decoded = bs58::decode(val.into()).into_vec()?;
SharedKeys::try_from_bytes(&decoded)
}
pub fn to_base58_string(&self) -> String {
bs58::encode(self.to_bytes()).into_string()
}
}
impl From<SharedKeys> for String {
fn from(keys: SharedKeys) -> Self {
keys.to_base58_string()
}
}
impl PemStorableKey for SharedKeys {
type Error = SharedKeyConversionError;
fn pem_type() -> &'static str {
// TODO: If common\nymsphinx\params\src\lib::GatewayIntegrityHmacAlgorithm changes
// the pem type needs updating!
"AES-128-CTR + HMAC-BLAKE3 GATEWAY SHARED KEYS"
}
fn to_bytes(&self) -> Vec<u8> {
self.to_bytes()
}
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
Self::try_from_bytes(bytes)
}
}
@@ -2,26 +2,34 @@
// SPDX-License-Identifier: Apache-2.0
use crate::registration::handshake::error::HandshakeError;
use crate::registration::handshake::shared_key::{SharedKeySize, SharedKeys};
use crate::registration::handshake::WsItem;
use crate::types;
use crate::registration::handshake::messages::{
HandshakeMessage, Initialisation, MaterialExchange,
};
use crate::registration::handshake::{SharedGatewayKey, WsItem, KDF_SALT_LENGTH};
use crate::shared_key::SharedKeySize;
use crate::{
types, LegacySharedKeySize, LegacySharedKeys, SharedSymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION,
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION,
};
use futures::{Sink, SinkExt, Stream, StreamExt};
use nym_crypto::asymmetric::{ed25519, x25519};
use nym_crypto::symmetric::aead::random_nonce;
use nym_crypto::{
asymmetric::{encryption, identity},
generic_array::typenum::Unsigned,
hkdf,
symmetric::stream_cipher,
};
use nym_sphinx::params::{GatewayEncryptionAlgorithm, GatewaySharedKeyHkdfAlgorithm};
#[cfg(not(target_arch = "wasm32"))]
use nym_task::TaskClient;
use rand::{CryptoRng, RngCore};
use tracing::log::*;
use rand::{thread_rng, CryptoRng, RngCore};
use std::any::{type_name, Any};
use std::str::FromStr;
use std::time::Duration;
use tracing::log::*;
use tungstenite::Message as WsMessage;
#[cfg(not(target_arch = "wasm32"))]
use nym_task::TaskClient;
#[cfg(not(target_arch = "wasm32"))]
use tokio::time::timeout;
@@ -29,113 +37,152 @@ use tokio::time::timeout;
use wasmtimer::tokio::timeout;
/// Handshake state.
pub(crate) struct State<'a, S> {
pub(crate) struct State<'a, S, R> {
/// The underlying WebSocket stream.
ws_stream: &'a mut S,
/// Pseudorandom number generator used during the exchange
rng: &'a mut R,
/// Identity of the local "node" (client or gateway) which is used
/// during the handshake.
identity: &'a identity::KeyPair,
identity: &'a ed25519::KeyPair,
/// Local ephemeral Diffie-Hellman keypair generated as a part of the handshake.
ephemeral_keypair: encryption::KeyPair,
ephemeral_keypair: x25519::KeyPair,
/// The derived shared key using the ephemeral keys of both parties.
derived_shared_keys: Option<SharedKeys>,
derived_shared_keys: Option<SharedGatewayKey>,
/// The known or received public identity key of the remote.
/// Ideally it would always be known before the handshake was initiated.
remote_pubkey: Option<identity::PublicKey>,
remote_pubkey: Option<ed25519::PublicKey>,
// this field is really out of place here, however, we need to propagate this information somehow
// in order to establish correct protocol for backwards compatibility reasons
expects_credential_usage: bool,
/// Specifies whether the end product should be an AES128Ctr + blake3 HMAC keys (legacy) or AES256-GCM-SIV (current)
derive_aes256_gcm_siv_key: bool,
// channel to receive shutdown signal
#[cfg(not(target_arch = "wasm32"))]
shutdown: TaskClient,
}
impl<'a, S> State<'a, S> {
impl<'a, S, R> State<'a, S, R> {
pub(crate) fn new(
rng: &mut (impl RngCore + CryptoRng),
rng: &'a mut R,
ws_stream: &'a mut S,
identity: &'a identity::KeyPair,
remote_pubkey: Option<identity::PublicKey>,
expects_credential_usage: bool,
#[cfg(not(target_arch = "wasm32"))] shutdown: TaskClient,
) -> Self {
) -> Self
where
R: CryptoRng + RngCore,
{
let ephemeral_keypair = encryption::KeyPair::new(rng);
State {
ws_stream,
rng,
ephemeral_keypair,
identity,
remote_pubkey,
derived_shared_keys: None,
expects_credential_usage,
// later on this should become the default
expects_credential_usage: false,
derive_aes256_gcm_siv_key: false,
#[cfg(not(target_arch = "wasm32"))]
shutdown,
}
}
pub(crate) fn with_credential_usage(mut self, expects_credential_usage: bool) -> Self {
self.expects_credential_usage = expects_credential_usage;
self
}
pub(crate) fn with_aes256_gcm_siv_key(mut self, derive_aes256_gcm_siv_key: bool) -> Self {
self.derive_aes256_gcm_siv_key = derive_aes256_gcm_siv_key;
self
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn set_aes256_gcm_siv_key_derivation(&mut self, derive_aes256_gcm_siv_key: bool) {
self.derive_aes256_gcm_siv_key = derive_aes256_gcm_siv_key;
}
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn local_ephemeral_key(&self) -> &encryption::PublicKey {
self.ephemeral_keypair.public_key()
}
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY
pub(crate) fn maybe_generate_initiator_salt(&mut self) -> Option<Vec<u8>>
where
R: CryptoRng + RngCore,
{
if self.derive_aes256_gcm_siv_key {
let mut salt = vec![0u8; KDF_SALT_LENGTH];
self.rng.fill_bytes(&mut salt);
Some(salt)
} else {
None
}
}
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_SALT
// 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<u8> {
self.identity
.public_key()
.to_bytes()
.into_iter()
.chain(self.ephemeral_keypair.public_key().to_bytes())
.collect()
}
// this will need to be adjusted when REMOTE_ID_PUBKEY is removed
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn parse_init_message(
mut init_message: Vec<u8>,
) -> Result<(identity::PublicKey, encryption::PublicKey), HandshakeError> {
if init_message.len() != identity::PUBLIC_KEY_LENGTH + encryption::PUBLIC_KEY_SIZE {
return Err(HandshakeError::MalformedRequest);
pub(crate) fn init_message(&self, initiator_salt: Option<Vec<u8>>) -> Initialisation {
Initialisation {
identity: *self.identity.public_key(),
ephemeral_dh: *self.ephemeral_keypair.public_key(),
initiator_salt,
}
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) {
#[cfg(not(target_arch = "wasm32"))]
pub(crate) fn finalization_message(
&self,
) -> crate::registration::handshake::messages::Finalization {
crate::registration::handshake::messages::Finalization { success: true }
}
pub(crate) fn derive_shared_key(
&mut self,
remote_ephemeral_key: &encryption::PublicKey,
initiator_salt: Option<&[u8]>,
) {
let dh_result = self
.ephemeral_keypair
.private_key()
.diffie_hellman(remote_ephemeral_key);
let key_size = if self.derive_aes256_gcm_siv_key {
SharedKeySize::to_usize()
} else {
LegacySharedKeySize::to_usize()
};
// there is no reason for this to fail as our okm is expected to be only 16 bytes
let okm = hkdf::extract_then_expand::<GatewaySharedKeyHkdfAlgorithm>(
None,
initiator_salt,
&dh_result,
None,
SharedKeySize::to_usize(),
key_size,
)
.expect("somehow too long okm was provided");
let derived_shared_key =
SharedKeys::try_from_bytes(&okm).expect("okm was expanded to incorrect length!");
self.derived_shared_keys = Some(derived_shared_key)
let shared_key = if self.derive_aes256_gcm_siv_key {
let current_key = SharedSymmetricKey::try_from_bytes(&okm)
.expect("okm was expanded to incorrect length!");
SharedGatewayKey::Current(current_key)
} else {
let legacy_key = LegacySharedKeys::try_from_bytes(&okm)
.expect("okm was expanded to incorrect length!");
SharedGatewayKey::Legacy(legacy_key)
};
self.derived_shared_keys = Some(shared_key)
}
// produces AES(k, SIG(ID_PRIV, G^x || G^y),
@@ -143,47 +190,57 @@ impl<'a, S> State<'a, S> {
pub(crate) fn prepare_key_material_sig(
&self,
remote_ephemeral_key: &encryption::PublicKey,
) -> Vec<u8> {
let message: Vec<_> = self
) -> Result<MaterialExchange, HandshakeError> {
let plaintext: Vec<_> = self
.ephemeral_keypair
.public_key()
.to_bytes()
.into_iter()
.chain(remote_ephemeral_key.to_bytes())
.collect();
let signature = self.identity.private_key().sign(plaintext);
let signature = self.identity.private_key().sign(message);
let zero_iv = stream_cipher::zero_iv::<GatewayEncryptionAlgorithm>();
stream_cipher::encrypt::<GatewayEncryptionAlgorithm>(
self.derived_shared_keys.as_ref().unwrap().encryption_key(),
&zero_iv,
&signature.to_bytes(),
)
let nonce = if self.derive_aes256_gcm_siv_key {
let mut rng = thread_rng();
Some(random_nonce::<GatewayEncryptionAlgorithm, _>(&mut rng).to_vec())
} else {
None
};
// SAFETY: this function is only called after the local key has already been derived
let signature_ciphertext = self
.derived_shared_keys
.as_ref()
.expect("shared key was not derived!")
.encrypt_naive(&signature.to_bytes(), nonce.as_deref())?;
Ok(MaterialExchange {
signature_ciphertext,
nonce,
})
}
// 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,
remote_response: &MaterialExchange,
remote_ephemeral_key: &x25519::PublicKey,
) -> Result<(), HandshakeError> {
if remote_material.len() != identity::SIGNATURE_LENGTH {
return Err(HandshakeError::KeyMaterialOfInvalidSize(
remote_material.len(),
));
}
// SAFETY: this function is only called after the local key has already been derived
let derived_shared_key = self
.derived_shared_keys
.as_ref()
.expect("shared key was not derived!");
// if the [client] init message contained non-legacy flag, the associated nonce MUST be present
if self.derive_aes256_gcm_siv_key && remote_response.nonce.is_none() {
return Err(HandshakeError::MissingNonceForCurrentKey);
}
// first decrypt received data
let zero_iv = stream_cipher::zero_iv::<GatewayEncryptionAlgorithm>();
let decrypted_signature = stream_cipher::decrypt::<GatewayEncryptionAlgorithm>(
derived_shared_key.encryption_key(),
&zero_iv,
remote_material,
);
let decrypted_signature = derived_shared_key.decrypt_naive(
&remote_response.signature_ciphertext,
remote_response.nonce.as_deref(),
)?;
// now verify signature itself
let signature = identity::Signature::from_bytes(&decrypted_signature)
@@ -246,7 +303,7 @@ impl<'a, S> State<'a, S> {
}
#[cfg(not(target_arch = "wasm32"))]
async fn _receive_handshake_message(&mut self) -> Result<Vec<u8>, HandshakeError>
async fn _receive_handshake_message_bytes(&mut self) -> Result<Vec<u8>, HandshakeError>
where
S: Stream<Item = WsItem> + Unpin,
{
@@ -265,7 +322,7 @@ impl<'a, S> State<'a, S> {
}
#[cfg(target_arch = "wasm32")]
async fn _receive_handshake_message(&mut self) -> Result<Vec<u8>, HandshakeError>
async fn _receive_handshake_message_bytes(&mut self) -> Result<Vec<u8>, HandshakeError>
where
S: Stream<Item = WsItem> + Unpin,
{
@@ -278,14 +335,20 @@ impl<'a, S> State<'a, S> {
}
}
pub(crate) async fn receive_handshake_message(&mut self) -> Result<Vec<u8>, HandshakeError>
pub(crate) async fn receive_handshake_message<M>(&mut self) -> Result<M, HandshakeError>
where
S: Stream<Item = WsItem> + Unpin,
M: HandshakeMessage,
{
// TODO: make timeout duration configurable
timeout(Duration::from_secs(5), self._receive_handshake_message())
.await
.map_err(|_| HandshakeError::Timeout)?
let bytes = timeout(
Duration::from_secs(5),
self._receive_handshake_message_bytes(),
)
.await
.map_err(|_| HandshakeError::Timeout)??;
M::try_from_bytes(&bytes)
}
// upon receiving this, the receiver should terminate the handshake
@@ -303,15 +366,30 @@ impl<'a, S> State<'a, S> {
.map_err(|_| HandshakeError::ClosedStream)
}
pub(crate) async fn send_handshake_data(
fn request_protocol_version(&self) -> u8 {
if self.derive_aes256_gcm_siv_key {
AES_GCM_SIV_PROTOCOL_VERSION
} else if self.expects_credential_usage {
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION
} else {
INITIAL_PROTOCOL_VERSION
}
}
pub(crate) async fn send_handshake_data<M>(
&mut self,
payload: Vec<u8>,
inner_message: M,
) -> Result<(), HandshakeError>
where
S: Sink<WsMessage> + Unpin,
M: HandshakeMessage + Any,
{
let handshake_message =
types::RegistrationHandshake::new_payload(payload, self.expects_credential_usage);
trace!("sending handshake message: {}", type_name::<M>());
let handshake_message = types::RegistrationHandshake::new_payload(
inner_message.into_bytes(),
self.request_protocol_version(),
);
self.ws_stream
.send(WsMessage::Text(handshake_message.try_into().unwrap()))
.await
@@ -320,7 +398,26 @@ impl<'a, S> State<'a, S> {
/// Finish the handshake, yielding the derived shared key and implicitly dropping all borrowed
/// values.
pub(crate) fn finalize_handshake(self) -> SharedKeys {
pub(crate) fn finalize_handshake(self) -> SharedGatewayKey {
self.derived_shared_keys.unwrap()
}
// 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_for_handshake_processing_error<T>(
&mut self,
result: Result<T, HandshakeError>,
) -> Result<T, HandshakeError>
where
S: Sink<WsMessage> + Unpin,
{
match result {
Ok(ok) => Ok(ok),
Err(err) => {
self.send_handshake_error(err.to_string()).await?;
Err(err)
}
}
}
}
@@ -0,0 +1,98 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{LegacySharedKeys, SharedGatewayKey, SharedKeyUsageError, SharedSymmetricKey};
use nym_crypto::symmetric::aead::random_nonce;
use nym_crypto::symmetric::stream_cipher::random_iv;
use nym_sphinx::params::{GatewayEncryptionAlgorithm, LegacyGatewayEncryptionAlgorithm};
use rand::thread_rng;
pub trait SymmetricKey {
fn random_nonce_or_iv(&self) -> Vec<u8>;
fn encrypt(
&self,
plaintext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError>;
fn decrypt(
&self,
ciphertext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError>;
}
impl SymmetricKey for SharedGatewayKey {
fn random_nonce_or_iv(&self) -> Vec<u8> {
self.random_nonce_or_iv()
}
fn encrypt(
&self,
plaintext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
self.encrypt(plaintext, nonce)
}
fn decrypt(
&self,
ciphertext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
self.decrypt(ciphertext, nonce)
}
}
impl SymmetricKey for SharedSymmetricKey {
fn random_nonce_or_iv(&self) -> Vec<u8> {
let mut rng = thread_rng();
random_nonce::<GatewayEncryptionAlgorithm, _>(&mut rng).to_vec()
}
fn encrypt(
&self,
plaintext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
let nonce = SharedGatewayKey::validate_aead_nonce(nonce)?;
self.encrypt(plaintext, &nonce)
}
fn decrypt(
&self,
ciphertext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
let nonce = SharedGatewayKey::validate_aead_nonce(nonce)?;
self.decrypt(ciphertext, &nonce)
}
}
impl SymmetricKey for LegacySharedKeys {
fn random_nonce_or_iv(&self) -> Vec<u8> {
let mut rng = thread_rng();
random_iv::<LegacyGatewayEncryptionAlgorithm, _>(&mut rng).to_vec()
}
fn encrypt(
&self,
plaintext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
let iv = SharedGatewayKey::validate_cipher_iv(nonce)?;
Ok(self.encrypt_and_tag(plaintext, iv))
}
fn decrypt(
&self,
ciphertext: &[u8],
nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
let iv = SharedGatewayKey::validate_cipher_iv(nonce)?;
self.decrypt_tagged(ciphertext, iv)
}
}
@@ -0,0 +1,241 @@
// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::registration::handshake::KDF_SALT_LENGTH;
use crate::shared_key::SharedSymmetricKey;
use crate::shared_key::{SharedKeyConversionError, SharedKeySize, SharedKeyUsageError};
use crate::LegacyGatewayMacSize;
use nym_crypto::generic_array::{
typenum::{Sum, Unsigned, U16},
GenericArray,
};
use nym_crypto::hkdf;
use nym_crypto::hmac::{compute_keyed_hmac, recompute_keyed_hmac_and_verify_tag};
use nym_crypto::symmetric::stream_cipher::{self, CipherKey, KeySizeUser, IV};
use nym_pemstore::traits::PemStorableKey;
use nym_sphinx::params::{
GatewayIntegrityHmacAlgorithm, GatewaySharedKeyHkdfAlgorithm, LegacyGatewayEncryptionAlgorithm,
};
use rand::{thread_rng, RngCore};
use serde::{Deserialize, Serialize};
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
// shared key is as long as the encryption key and the MAC key combined.
pub type LegacySharedKeySize = Sum<EncryptionKeySize, MacKeySize>;
// we're using 16 byte long key in sphinx, so let's use the same one here
type MacKeySize = U16;
type EncryptionKeySize = <LegacyGatewayEncryptionAlgorithm as KeySizeUser>::KeySize;
/// Shared key used when computing MAC for messages exchanged between client and its gateway.
pub type MacKey = GenericArray<u8, MacKeySize>;
#[derive(Debug, PartialEq, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
pub struct LegacySharedKeys {
encryption_key: CipherKey<LegacyGatewayEncryptionAlgorithm>,
mac_key: MacKey,
}
impl LegacySharedKeys {
pub fn upgrade(&self) -> (SharedSymmetricKey, Vec<u8>) {
let mut rng = thread_rng();
let mut salt = vec![0u8; KDF_SALT_LENGTH];
rng.fill_bytes(&mut salt);
let legacy_bytes = Zeroizing::new(self.to_bytes());
let okm = hkdf::extract_then_expand::<GatewaySharedKeyHkdfAlgorithm>(
Some(&salt),
&legacy_bytes,
None,
SharedKeySize::to_usize(),
)
.expect("somehow too long okm was provided");
let key = SharedSymmetricKey::try_from_bytes(&okm)
.expect("okm was expanded to incorrect length!");
(key, salt)
}
pub fn upgrade_verify(
&self,
salt: &[u8],
expected_digest: &[u8],
) -> Option<SharedSymmetricKey> {
let legacy_bytes = Zeroizing::new(self.to_bytes());
let okm = hkdf::extract_then_expand::<GatewaySharedKeyHkdfAlgorithm>(
Some(salt),
&legacy_bytes,
None,
SharedKeySize::to_usize(),
)
.expect("somehow too long okm was provided");
let key = SharedSymmetricKey::try_from_bytes(&okm)
.expect("okm was expanded to incorrect length!");
if key.digest() != expected_digest {
// no need to zeroize that key since it's malformed and we won't be using it anyway
None
} else {
Some(key)
}
}
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, SharedKeyConversionError> {
if bytes.len() != LegacySharedKeySize::to_usize() {
return Err(SharedKeyConversionError::InvalidSharedKeysSize {
received: bytes.len(),
expected: LegacySharedKeySize::to_usize(),
});
}
let encryption_key =
GenericArray::clone_from_slice(&bytes[..EncryptionKeySize::to_usize()]);
let mac_key = GenericArray::clone_from_slice(&bytes[EncryptionKeySize::to_usize()..]);
Ok(LegacySharedKeys {
encryption_key,
mac_key,
})
}
/// Encrypts the provided data using the optionally provided initialisation vector,
/// or a 0 value if nothing was given.
/// It does **NOT** attach any integrity macs on the produced ciphertext
pub fn encrypt_without_tagging(
&self,
data: &[u8],
iv: Option<&IV<LegacyGatewayEncryptionAlgorithm>>,
) -> Vec<u8> {
match iv {
Some(iv) => stream_cipher::encrypt::<LegacyGatewayEncryptionAlgorithm>(
self.encryption_key(),
iv,
data,
),
None => {
let zero_iv = stream_cipher::zero_iv::<LegacyGatewayEncryptionAlgorithm>();
stream_cipher::encrypt::<LegacyGatewayEncryptionAlgorithm>(
self.encryption_key(),
&zero_iv,
data,
)
}
}
}
/// Encrypts the provided data using the optionally provided initialisation vector,
/// or a 0 value if nothing was given. Then it computes an integrity mac and concatenates it
/// with the previously produced ciphertext.
pub fn encrypt_and_tag(
&self,
data: &[u8],
iv: Option<&IV<LegacyGatewayEncryptionAlgorithm>>,
) -> Vec<u8> {
let ciphertext = self.encrypt_without_tagging(data, iv);
let mac = compute_keyed_hmac::<GatewayIntegrityHmacAlgorithm>(
self.mac_key().as_slice(),
&ciphertext,
);
mac.into_bytes().into_iter().chain(ciphertext).collect()
}
pub fn decrypt_without_tag(
&self,
ciphertext: &[u8],
iv: Option<&IV<LegacyGatewayEncryptionAlgorithm>>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
let zero_iv = stream_cipher::zero_iv::<LegacyGatewayEncryptionAlgorithm>();
let iv = iv.unwrap_or(&zero_iv);
Ok(stream_cipher::decrypt::<LegacyGatewayEncryptionAlgorithm>(
self.encryption_key(),
iv,
ciphertext,
))
}
pub fn decrypt_tagged(
&self,
enc_data: &[u8],
iv: Option<&IV<LegacyGatewayEncryptionAlgorithm>>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
let mac_size = LegacyGatewayMacSize::to_usize();
if enc_data.len() < mac_size {
return Err(SharedKeyUsageError::TooShortRequest);
}
let mac_tag = &enc_data[..mac_size];
let message_bytes = &enc_data[mac_size..];
if !recompute_keyed_hmac_and_verify_tag::<GatewayIntegrityHmacAlgorithm>(
self.mac_key().as_slice(),
message_bytes,
mac_tag,
) {
return Err(SharedKeyUsageError::InvalidMac);
}
// couldn't have made the first borrow mutable as you can't have an immutable borrow
// together with a mutable one
let mut message_bytes_mut = message_bytes.to_vec();
let zero_iv = stream_cipher::zero_iv::<LegacyGatewayEncryptionAlgorithm>();
let iv = iv.unwrap_or(&zero_iv);
stream_cipher::decrypt_in_place::<LegacyGatewayEncryptionAlgorithm>(
self.encryption_key(),
iv,
&mut message_bytes_mut,
);
Ok(message_bytes_mut)
}
pub fn encryption_key(&self) -> &CipherKey<LegacyGatewayEncryptionAlgorithm> {
&self.encryption_key
}
pub fn mac_key(&self) -> &MacKey {
&self.mac_key
}
pub fn to_bytes(&self) -> Vec<u8> {
self.encryption_key
.iter()
.copied()
.chain(self.mac_key.iter().copied())
.collect()
}
pub fn try_from_base58_string<S: Into<String>>(
val: S,
) -> Result<Self, SharedKeyConversionError> {
let decoded = bs58::decode(val.into()).into_vec()?;
LegacySharedKeys::try_from_bytes(&decoded)
}
pub fn to_base58_string(&self) -> String {
bs58::encode(self.to_bytes()).into_string()
}
}
impl From<LegacySharedKeys> for String {
fn from(keys: LegacySharedKeys) -> Self {
keys.to_base58_string()
}
}
impl PemStorableKey for LegacySharedKeys {
type Error = SharedKeyConversionError;
fn pem_type() -> &'static str {
// TODO: If common\nymsphinx\params\src\lib::GatewayIntegrityHmacAlgorithm changes
// the pem type needs updating!
"AES-128-CTR + HMAC-BLAKE3 GATEWAY SHARED KEYS"
}
fn to_bytes(&self) -> Vec<u8> {
self.to_bytes()
}
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
Self::try_from_bytes(bytes)
}
}
@@ -0,0 +1,304 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_crypto::blake3;
use nym_crypto::crypto_hash::compute_digest;
use nym_crypto::generic_array::{typenum::Unsigned, GenericArray};
use nym_crypto::symmetric::aead::{
self, nonce_size, random_nonce, AeadError, AeadKey, KeySizeUser, Nonce,
};
use nym_crypto::symmetric::stream_cipher::{iv_size, random_iv, IV};
use nym_pemstore::traits::PemStorableKey;
use nym_sphinx::params::{GatewayEncryptionAlgorithm, LegacyGatewayEncryptionAlgorithm};
use rand::thread_rng;
use serde::{Deserialize, Serialize};
use thiserror::Error;
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
pub use legacy::LegacySharedKeys;
pub mod helpers;
pub mod legacy;
pub type SharedKeySize = <GatewayEncryptionAlgorithm as KeySizeUser>::KeySize;
#[derive(Debug, PartialEq, Zeroize, ZeroizeOnDrop)]
pub enum SharedGatewayKey {
Current(SharedSymmetricKey),
Legacy(LegacySharedKeys),
}
impl SharedGatewayKey {
pub fn is_legacy(&self) -> bool {
matches!(self, SharedGatewayKey::Legacy(..))
}
pub fn aes128_ctr_hmac_bs58(&self) -> Option<Zeroizing<String>> {
match self {
SharedGatewayKey::Current(_) => None,
SharedGatewayKey::Legacy(key) => Some(Zeroizing::new(key.to_base58_string())),
}
}
pub fn aes256_gcm_siv(&self) -> Option<Zeroizing<Vec<u8>>> {
match self {
SharedGatewayKey::Current(key) => Some(Zeroizing::new(key.to_bytes())),
SharedGatewayKey::Legacy(_) => None,
}
}
pub fn unwrap_legacy(&self) -> &LegacySharedKeys {
match self {
SharedGatewayKey::Current(_) => panic!("expected legacy key"),
SharedGatewayKey::Legacy(key) => key,
}
}
pub fn random_nonce_or_iv(&self) -> Vec<u8> {
let mut rng = thread_rng();
if self.is_legacy() {
random_iv::<LegacyGatewayEncryptionAlgorithm, _>(&mut rng).to_vec()
} else {
random_nonce::<GatewayEncryptionAlgorithm, _>(&mut rng).to_vec()
}
}
pub fn random_nonce_or_zero_iv(&self) -> Option<Vec<u8>> {
if self.is_legacy() {
None
} else {
let mut rng = thread_rng();
Some(random_nonce::<GatewayEncryptionAlgorithm, _>(&mut rng).to_vec())
}
}
pub fn nonce_size(&self) -> usize {
match self {
SharedGatewayKey::Current(_) => nonce_size::<GatewayEncryptionAlgorithm>(),
SharedGatewayKey::Legacy(_) => iv_size::<LegacyGatewayEncryptionAlgorithm>(),
}
}
}
impl From<LegacySharedKeys> for SharedGatewayKey {
fn from(keys: LegacySharedKeys) -> Self {
SharedGatewayKey::Legacy(keys)
}
}
impl From<SharedSymmetricKey> for SharedGatewayKey {
fn from(keys: SharedSymmetricKey) -> Self {
SharedGatewayKey::Current(keys)
}
}
#[derive(Debug, Error)]
pub enum SharedKeyUsageError {
#[error("the request is too short")]
TooShortRequest,
#[error("provided MAC is invalid")]
InvalidMac,
#[error("the provided nonce (or legacy IV) did not have the expected length")]
MalformedNonce,
#[error("did not provide a valid nonce for aead encryption")]
MissingAeadNonce,
#[error("failed to either encrypt or decrypt provided message")]
AeadFailure(#[from] AeadError),
}
impl SharedGatewayKey {
fn validate_aead_nonce(
raw: Option<&[u8]>,
) -> Result<Nonce<GatewayEncryptionAlgorithm>, SharedKeyUsageError> {
let Some(raw) = raw else {
return Err(SharedKeyUsageError::MissingAeadNonce);
};
if raw.len() != nonce_size::<GatewayEncryptionAlgorithm>() {
return Err(SharedKeyUsageError::MalformedNonce);
}
Ok(Nonce::<GatewayEncryptionAlgorithm>::clone_from_slice(raw))
}
fn validate_cipher_iv(
raw: Option<&[u8]>,
) -> Result<Option<&IV<LegacyGatewayEncryptionAlgorithm>>, SharedKeyUsageError> {
let Some(raw) = raw else { return Ok(None) };
let iv = if raw.is_empty() {
None
} else {
if raw.len() != iv_size::<LegacyGatewayEncryptionAlgorithm>() {
return Err(SharedKeyUsageError::MalformedNonce);
}
Some(IV::<LegacyGatewayEncryptionAlgorithm>::from_slice(raw))
};
Ok(iv)
}
pub fn encrypt(
&self,
plaintext: &[u8],
// the best common denominator for converting into 'IV' and 'Nonce' types
raw_nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
match self {
SharedGatewayKey::Current(aes_gcm_siv) => {
let nonce = Self::validate_aead_nonce(raw_nonce)?;
aes_gcm_siv.encrypt(plaintext, &nonce)
}
SharedGatewayKey::Legacy(aes_ctr) => {
let iv = Self::validate_cipher_iv(raw_nonce)?;
Ok(aes_ctr.encrypt_and_tag(plaintext, iv))
}
}
}
pub fn decrypt(
&self,
ciphertext: &[u8],
// the best common denominator for converting into 'IV' and 'Nonce' types
raw_nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
match self {
SharedGatewayKey::Current(aes_gcm_siv) => {
let nonce = Self::validate_aead_nonce(raw_nonce)?;
aes_gcm_siv.decrypt(ciphertext, &nonce)
}
SharedGatewayKey::Legacy(aes_ctr) => {
let iv = Self::validate_cipher_iv(raw_nonce)?;
aes_ctr.decrypt_tagged(ciphertext, iv)
}
}
}
// for the legacy keys do not use integrity MAC
pub fn encrypt_naive(
&self,
plaintext: &[u8],
// the best common denominator for converting into 'IV' and 'Nonce' types
raw_nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
match self {
SharedGatewayKey::Current(aes_gcm_siv) => {
let nonce = Self::validate_aead_nonce(raw_nonce)?;
aes_gcm_siv.encrypt(plaintext, &nonce)
}
SharedGatewayKey::Legacy(aes_ctr) => {
let iv = Self::validate_cipher_iv(raw_nonce)?;
Ok(aes_ctr.encrypt_without_tagging(plaintext, iv))
}
}
}
// for the legacy keys do not use integrity MAC
pub fn decrypt_naive(
&self,
ciphertext: &[u8],
// the best common denominator for converting into 'IV' and 'Nonce' types
raw_nonce: Option<&[u8]>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
match self {
SharedGatewayKey::Current(aes_gcm_siv) => {
let nonce = Self::validate_aead_nonce(raw_nonce)?;
aes_gcm_siv.decrypt(ciphertext, &nonce)
}
SharedGatewayKey::Legacy(aes_ctr) => {
let iv = Self::validate_cipher_iv(raw_nonce)?;
aes_ctr.decrypt_without_tag(ciphertext, iv)
}
}
}
}
#[derive(Debug, PartialEq, Serialize, Deserialize, Zeroize, ZeroizeOnDrop)]
pub struct SharedSymmetricKey(AeadKey<GatewayEncryptionAlgorithm>);
type KeySize = <GatewayEncryptionAlgorithm as KeySizeUser>::KeySize;
#[derive(Debug, Clone, Copy, Error)]
pub enum SharedKeyConversionError {
#[error("the string representation of the shared key was malformed: {0}")]
DecodeError(#[from] bs58::decode::Error),
#[error(
"the received shared keys had invalid size. Got: {received}, but expected: {expected}"
)]
InvalidSharedKeysSize { received: usize, expected: usize },
}
impl SharedSymmetricKey {
pub fn try_from_bytes(bytes: &[u8]) -> Result<Self, SharedKeyConversionError> {
if bytes.len() != KeySize::to_usize() {
return Err(SharedKeyConversionError::InvalidSharedKeysSize {
received: bytes.len(),
expected: KeySize::to_usize(),
});
}
Ok(SharedSymmetricKey(GenericArray::clone_from_slice(bytes)))
}
pub fn zeroizing_clone(&self) -> Zeroizing<Self> {
Zeroizing::new(SharedSymmetricKey(self.0))
}
pub fn digest(&self) -> Vec<u8> {
compute_digest::<blake3::Hasher>(self.as_bytes()).to_vec()
}
pub fn as_bytes(&self) -> &[u8] {
self.0.as_slice()
}
pub fn to_bytes(&self) -> Vec<u8> {
self.0.iter().copied().collect()
}
pub fn try_from_base58_string<S: Into<String>>(
val: S,
) -> Result<Self, SharedKeyConversionError> {
let bs58_str = Zeroizing::new(val.into());
let decoded = Zeroizing::new(bs58::decode(bs58_str).into_vec()?);
Self::try_from_bytes(&decoded)
}
pub fn to_base58_string(&self) -> String {
let bytes = Zeroizing::new(self.to_bytes());
bs58::encode(bytes).into_string()
}
pub fn encrypt(
&self,
plaintext: &[u8],
nonce: &Nonce<GatewayEncryptionAlgorithm>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
aead::encrypt::<GatewayEncryptionAlgorithm>(&self.0, nonce, plaintext).map_err(Into::into)
}
pub fn decrypt(
&self,
ciphertext: &[u8],
nonce: &Nonce<GatewayEncryptionAlgorithm>,
) -> Result<Vec<u8>, SharedKeyUsageError> {
aead::decrypt::<GatewayEncryptionAlgorithm>(&self.0, nonce, ciphertext).map_err(Into::into)
}
}
impl PemStorableKey for SharedSymmetricKey {
type Error = SharedKeyConversionError;
fn pem_type() -> &'static str {
"AES-256-GCM-SIV GATEWAY SHARED KEY"
}
fn to_bytes(&self) -> Vec<u8> {
self.to_bytes()
}
fn from_bytes(bytes: &[u8]) -> Result<Self, Self::Error> {
Self::try_from_bytes(bytes)
}
}
-511
View File
@@ -1,511 +0,0 @@
// Copyright 2020-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::authentication::encrypted_address::EncryptedAddressBytes;
use crate::iv::{IVConversionError, IV};
use crate::models::CredentialSpendingRequest;
use crate::registration::handshake::SharedKeys;
use crate::{GatewayMacSize, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION};
use nym_credentials::ecash::bandwidth::CredentialSpendingData;
use nym_credentials_interface::CompactEcashError;
use nym_crypto::generic_array::typenum::Unsigned;
use nym_crypto::hmac::recompute_keyed_hmac_and_verify_tag;
use nym_crypto::symmetric::stream_cipher;
use nym_sphinx::addressing::nodes::NymNodeRoutingAddressError;
use nym_sphinx::forwarding::packet::{MixPacket, MixPacketFormattingError};
use nym_sphinx::params::packet_sizes::PacketSize;
use nym_sphinx::params::{GatewayEncryptionAlgorithm, GatewayIntegrityHmacAlgorithm};
use nym_sphinx::DestinationAddressBytes;
use serde::{Deserialize, Serialize};
use tracing::log::error;
use std::str::FromStr;
use std::string::FromUtf8Error;
use thiserror::Error;
use tungstenite::protocol::Message;
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum RegistrationHandshake {
HandshakePayload {
#[serde(default)]
protocol_version: Option<u8>,
data: Vec<u8>,
},
HandshakeError {
message: String,
},
}
impl RegistrationHandshake {
pub fn new_payload(data: Vec<u8>, will_use_credentials: bool) -> Self {
// if we're not going to be using credentials, advertise lower protocol version to allow connection
// to wider range of gateways
let protocol_version = if will_use_credentials {
Some(CURRENT_PROTOCOL_VERSION)
} else {
Some(INITIAL_PROTOCOL_VERSION)
};
RegistrationHandshake::HandshakePayload {
protocol_version,
data,
}
}
pub fn new_error<S: Into<String>>(message: S) -> Self {
RegistrationHandshake::HandshakeError {
message: message.into(),
}
}
}
impl FromStr for RegistrationHandshake {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}
impl TryFrom<String> for RegistrationHandshake {
type Error = serde_json::Error;
fn try_from(msg: String) -> Result<Self, serde_json::Error> {
msg.parse()
}
}
impl TryInto<String> for RegistrationHandshake {
type Error = serde_json::Error;
fn try_into(self) -> Result<String, serde_json::Error> {
serde_json::to_string(&self)
}
}
// specific errors (that should not be nested!!) for clients to match on
#[derive(Debug, Copy, Clone, Error, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SimpleGatewayRequestsError {
#[error("insufficient bandwidth available to process the request. required: {required}B, available: {available}B")]
OutOfBandwidth { required: i64, available: i64 },
#[error("the provided ticket has already been spent before at this gateway")]
TicketReplay,
}
impl SimpleGatewayRequestsError {
pub fn is_ticket_replay(&self) -> bool {
matches!(self, SimpleGatewayRequestsError::TicketReplay)
}
}
#[derive(Debug, Error)]
pub enum GatewayRequestsError {
#[error("the request is too short")]
TooShortRequest,
#[error("provided MAC is invalid")]
InvalidMac,
#[error("Provided bandwidth IV is malformed: {0}")]
MalformedIV(#[from] IVConversionError),
#[error("address field was incorrectly encoded: {source}")]
IncorrectlyEncodedAddress {
#[from]
source: NymNodeRoutingAddressError,
},
#[error("received request had invalid size. (actual: {0}, but expected one of: {} (ACK), {} (REGULAR), {}, {}, {} (EXTENDED))",
PacketSize::AckPacket.size(),
PacketSize::RegularPacket.size(),
PacketSize::ExtendedPacket8.size(),
PacketSize::ExtendedPacket16.size(),
PacketSize::ExtendedPacket32.size())
]
RequestOfInvalidSize(usize),
#[error("received sphinx packet was malformed")]
MalformedSphinxPacket,
#[error("the received encrypted data was malformed")]
MalformedEncryption,
#[error("provided packet mode is invalid")]
InvalidPacketMode,
#[error("provided mix packet was malformed: {source}")]
InvalidMixPacket {
#[from]
source: MixPacketFormattingError,
},
#[error("failed to deserialize provided credential: {0}")]
EcashCredentialDeserializationFailure(#[from] CompactEcashError),
#[error("failed to deserialize provided credential: EOF")]
CredentialDeserializationFailureEOF,
#[error("failed to deserialize provided credential: malformed string: {0}")]
CredentialDeserializationFailureMalformedString(#[from] FromUtf8Error),
#[error("the provided [v1] credential has invalid number of parameters - {0}")]
InvalidNumberOfEmbededParameters(u32),
// variant to catch legacy errors
#[error("{0}")]
Other(String),
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ClientControlRequest {
// TODO: should this also contain a MAC considering that at this point we already
// have the shared key derived?
Authenticate {
#[serde(default)]
protocol_version: Option<u8>,
address: String,
enc_address: String,
iv: String,
},
#[serde(alias = "handshakePayload")]
RegisterHandshakeInitRequest {
#[serde(default)]
protocol_version: Option<u8>,
data: Vec<u8>,
},
BandwidthCredential {
enc_credential: Vec<u8>,
iv: Vec<u8>,
},
BandwidthCredentialV2 {
enc_credential: Vec<u8>,
iv: Vec<u8>,
},
EcashCredential {
enc_credential: Vec<u8>,
iv: Vec<u8>,
},
ClaimFreeTestnetBandwidth,
}
impl ClientControlRequest {
pub fn new_authenticate(
address: DestinationAddressBytes,
enc_address: EncryptedAddressBytes,
iv: IV,
uses_credentials: bool,
) -> Self {
// if we're not going to be using credentials, advertise lower protocol version to allow connection
// to wider range of gateways
let protocol_version = if uses_credentials {
Some(CURRENT_PROTOCOL_VERSION)
} else {
Some(INITIAL_PROTOCOL_VERSION)
};
ClientControlRequest::Authenticate {
protocol_version,
address: address.as_base58_string(),
enc_address: enc_address.to_base58_string(),
iv: iv.to_base58_string(),
}
}
pub fn name(&self) -> String {
match self {
ClientControlRequest::Authenticate { .. } => "Authenticate".to_string(),
ClientControlRequest::RegisterHandshakeInitRequest { .. } => {
"RegisterHandshakeInitRequest".to_string()
}
ClientControlRequest::BandwidthCredential { .. } => "BandwidthCredential".to_string(),
ClientControlRequest::BandwidthCredentialV2 { .. } => {
"BandwidthCredentialV2".to_string()
}
ClientControlRequest::EcashCredential { .. } => "EcashCredential".to_string(),
ClientControlRequest::ClaimFreeTestnetBandwidth => {
"ClaimFreeTestnetBandwidth".to_string()
}
}
}
pub fn new_enc_ecash_credential(
credential: CredentialSpendingData,
shared_key: &SharedKeys,
iv: IV,
) -> Self {
let cred = CredentialSpendingRequest::new(credential);
let serialized_credential = cred.to_bytes();
let enc_credential = shared_key.encrypt_and_tag(&serialized_credential, Some(iv.inner()));
ClientControlRequest::EcashCredential {
enc_credential,
iv: iv.to_bytes(),
}
}
pub fn try_from_enc_ecash_credential(
enc_credential: Vec<u8>,
shared_key: &SharedKeys,
iv: Vec<u8>,
) -> Result<CredentialSpendingRequest, GatewayRequestsError> {
let iv = IV::try_from_bytes(&iv)?;
let credential_bytes = shared_key.decrypt_tagged(&enc_credential, Some(iv.inner()))?;
CredentialSpendingRequest::try_from_bytes(credential_bytes.as_slice())
.map_err(|_| GatewayRequestsError::MalformedEncryption)
}
}
impl From<ClientControlRequest> for Message {
fn from(req: ClientControlRequest) -> Self {
// it should be safe to call `unwrap` here as the message is generated by the server
// so if it fails (and consequently panics) it's a bug that should be resolved
let str_req = serde_json::to_string(&req).unwrap();
Message::Text(str_req)
}
}
impl TryFrom<String> for ClientControlRequest {
type Error = serde_json::Error;
fn try_from(msg: String) -> Result<Self, Self::Error> {
serde_json::from_str(&msg)
}
}
impl TryInto<String> for ClientControlRequest {
type Error = serde_json::Error;
fn try_into(self) -> Result<String, Self::Error> {
serde_json::to_string(&self)
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum ServerResponse {
Authenticate {
#[serde(default)]
protocol_version: Option<u8>,
status: bool,
bandwidth_remaining: i64,
},
Register {
#[serde(default)]
protocol_version: Option<u8>,
status: bool,
},
Bandwidth {
available_total: i64,
},
Send {
remaining_bandwidth: i64,
},
// Generic error
Error {
message: String,
},
// Specific typed errors
// so that clients could match on this variant without doing naive string matching
TypedError {
error: SimpleGatewayRequestsError,
},
}
impl ServerResponse {
pub fn name(&self) -> String {
match self {
ServerResponse::Authenticate { .. } => "Authenticate".to_string(),
ServerResponse::Register { .. } => "Register".to_string(),
ServerResponse::Bandwidth { .. } => "Bandwidth".to_string(),
ServerResponse::Send { .. } => "Send".to_string(),
ServerResponse::Error { .. } => "Error".to_string(),
ServerResponse::TypedError { .. } => "TypedError".to_string(),
}
}
pub fn new_error<S: Into<String>>(msg: S) -> Self {
ServerResponse::Error {
message: msg.into(),
}
}
pub fn is_error(&self) -> bool {
matches!(self, ServerResponse::Error { .. })
}
pub fn implies_successful_authentication(&self) -> bool {
match self {
ServerResponse::Authenticate { status, .. } => *status,
ServerResponse::Register { status, .. } => *status,
_ => false,
}
}
}
impl From<ServerResponse> for Message {
fn from(res: ServerResponse) -> Self {
// it should be safe to call `unwrap` here as the message is generated by the server
// so if it fails (and consequently panics) it's a bug that should be resolved
let str_res = serde_json::to_string(&res).unwrap();
Message::Text(str_res)
}
}
impl TryFrom<String> for ServerResponse {
type Error = serde_json::Error;
fn try_from(msg: String) -> Result<Self, serde_json::Error> {
serde_json::from_str(&msg)
}
}
pub enum BinaryRequest {
ForwardSphinx(MixPacket),
}
// 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_encrypted_tagged_bytes(
raw_req: Vec<u8>,
shared_keys: &SharedKeys,
) -> Result<Self, GatewayRequestsError> {
let message_bytes = &shared_keys.decrypt_tagged(&raw_req, None)?;
// 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 mix_packet = MixPacket::try_from_bytes(message_bytes)?;
Ok(BinaryRequest::ForwardSphinx(mix_packet))
}
pub fn into_encrypted_tagged_bytes(self, shared_key: &SharedKeys) -> Vec<u8> {
match self {
BinaryRequest::ForwardSphinx(mix_packet) => {
let forwarding_data = match mix_packet.into_bytes() {
Ok(mix_packet) => mix_packet,
Err(e) => {
error!("Could not convert packet to bytes: {e}");
return vec![];
}
};
// TODO: it could be theoretically slightly more efficient if the data wasn't taken
// by reference because then it makes a copy for encryption rather than do it in place
shared_key.encrypt_and_tag(&forwarding_data, None)
}
}
}
// TODO: this will be encrypted, etc.
pub fn new_forward_request(mix_packet: MixPacket) -> BinaryRequest {
BinaryRequest::ForwardSphinx(mix_packet)
}
pub fn into_ws_message(self, shared_key: &SharedKeys) -> Message {
Message::Binary(self.into_encrypted_tagged_bytes(shared_key))
}
}
// Introduced for consistency sake
pub enum BinaryResponse {
PushedMixMessage(Vec<u8>),
}
impl BinaryResponse {
pub fn try_from_encrypted_tagged_bytes(
raw_req: Vec<u8>,
shared_keys: &SharedKeys,
) -> Result<Self, GatewayRequestsError> {
let mac_size = GatewayMacSize::to_usize();
if raw_req.len() < mac_size {
return Err(GatewayRequestsError::TooShortRequest);
}
let mac_tag = &raw_req[..mac_size];
let message_bytes = &raw_req[mac_size..];
if !recompute_keyed_hmac_and_verify_tag::<GatewayIntegrityHmacAlgorithm>(
shared_keys.mac_key().as_slice(),
message_bytes,
mac_tag,
) {
return Err(GatewayRequestsError::InvalidMac);
}
let zero_iv = stream_cipher::zero_iv::<GatewayEncryptionAlgorithm>();
let plaintext = stream_cipher::decrypt::<GatewayEncryptionAlgorithm>(
shared_keys.encryption_key(),
&zero_iv,
message_bytes,
);
Ok(BinaryResponse::PushedMixMessage(plaintext))
}
pub fn into_encrypted_tagged_bytes(self, shared_key: &SharedKeys) -> Vec<u8> {
match self {
// TODO: it could be theoretically slightly more efficient if the data wasn't taken
// by reference because then it makes a copy for encryption rather than do it in place
BinaryResponse::PushedMixMessage(message) => shared_key.encrypt_and_tag(&message, None),
}
}
pub fn new_pushed_mix_message(msg: Vec<u8>) -> Self {
BinaryResponse::PushedMixMessage(msg)
}
pub fn into_ws_message(self, shared_key: &SharedKeys) -> Message {
Message::Binary(self.into_encrypted_tagged_bytes(shared_key))
}
}
#[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_with_protocol = RegistrationHandshake::HandshakePayload {
protocol_version: Some(42),
data: handshake_data.clone(),
};
let serialized = serde_json::to_string(&handshake_payload_with_protocol).unwrap();
let deserialized = ClientControlRequest::try_from(serialized).unwrap();
match deserialized {
ClientControlRequest::RegisterHandshakeInitRequest {
protocol_version,
data,
} => {
assert_eq!(protocol_version, Some(42));
assert_eq!(data, handshake_data)
}
_ => unreachable!("this branch shouldn't have been reached!"),
}
let handshake_payload_without_protocol = RegistrationHandshake::HandshakePayload {
protocol_version: None,
data: handshake_data.clone(),
};
let serialized = serde_json::to_string(&handshake_payload_without_protocol).unwrap();
let deserialized = ClientControlRequest::try_from(serialized).unwrap();
match deserialized {
ClientControlRequest::RegisterHandshakeInitRequest {
protocol_version,
data,
} => {
assert!(protocol_version.is_none());
assert_eq!(data, handshake_data)
}
_ => unreachable!("this branch shouldn't have been reached!"),
}
}
}
@@ -0,0 +1,78 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::types::helpers::BinaryData;
use crate::{GatewayRequestsError, SharedGatewayKey};
use nym_sphinx::forwarding::packet::MixPacket;
use strum::FromRepr;
use tungstenite::Message;
// in legacy mode requests use zero IV without
#[non_exhaustive]
pub enum BinaryRequest {
ForwardSphinx { packet: MixPacket },
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, FromRepr, PartialEq)]
#[non_exhaustive]
pub enum BinaryRequestKind {
ForwardSphinx = 1,
}
// 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 kind(&self) -> BinaryRequestKind {
match self {
BinaryRequest::ForwardSphinx { .. } => BinaryRequestKind::ForwardSphinx,
}
}
pub fn from_plaintext(
kind: BinaryRequestKind,
plaintext: &[u8],
) -> Result<Self, GatewayRequestsError> {
match kind {
BinaryRequestKind::ForwardSphinx => {
let packet = MixPacket::try_from_bytes(plaintext)?;
Ok(BinaryRequest::ForwardSphinx { packet })
}
}
}
pub fn try_from_encrypted_tagged_bytes(
bytes: Vec<u8>,
shared_key: &SharedGatewayKey,
) -> Result<Self, GatewayRequestsError> {
BinaryData::from_raw(&bytes, shared_key)?.into_request(shared_key)
}
pub fn into_encrypted_tagged_bytes(
self,
shared_key: &SharedGatewayKey,
) -> Result<Vec<u8>, GatewayRequestsError> {
let kind = self.kind();
let plaintext = match self {
BinaryRequest::ForwardSphinx { packet } => packet.into_bytes()?,
};
BinaryData::make_encrypted_blob(kind as u8, &plaintext, shared_key)
}
pub fn into_ws_message(
self,
shared_key: &SharedGatewayKey,
) -> Result<Message, GatewayRequestsError> {
// all variants are currently encrypted
let blob = match self {
BinaryRequest::ForwardSphinx { .. } => self.into_encrypted_tagged_bytes(shared_key)?,
};
Ok(Message::Binary(blob))
}
}
@@ -0,0 +1,72 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::types::helpers::BinaryData;
use crate::{GatewayRequestsError, SharedGatewayKey};
use strum::FromRepr;
use tungstenite::Message;
#[non_exhaustive]
pub enum BinaryResponse {
PushedMixMessage { message: Vec<u8> },
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, FromRepr, PartialEq)]
#[non_exhaustive]
pub enum BinaryResponseKind {
PushedMixMessage = 1,
}
impl BinaryResponse {
pub fn kind(&self) -> BinaryResponseKind {
match self {
BinaryResponse::PushedMixMessage { .. } => BinaryResponseKind::PushedMixMessage,
}
}
pub fn from_plaintext(
kind: BinaryResponseKind,
plaintext: &[u8],
) -> Result<Self, GatewayRequestsError> {
match kind {
BinaryResponseKind::PushedMixMessage => Ok(BinaryResponse::PushedMixMessage {
message: plaintext.to_vec(),
}),
}
}
pub fn try_from_encrypted_tagged_bytes(
bytes: Vec<u8>,
shared_key: &SharedGatewayKey,
) -> Result<Self, GatewayRequestsError> {
BinaryData::from_raw(&bytes, shared_key)?.into_response(shared_key)
}
pub fn into_encrypted_tagged_bytes(
self,
shared_key: &SharedGatewayKey,
) -> Result<Vec<u8>, GatewayRequestsError> {
let kind = self.kind();
let plaintext = match self {
BinaryResponse::PushedMixMessage { message } => message,
};
BinaryData::make_encrypted_blob(kind as u8, &plaintext, shared_key)
}
pub fn into_ws_message(
self,
shared_key: &SharedGatewayKey,
) -> Result<Message, GatewayRequestsError> {
// all variants are currently encrypted
let blob = match self {
BinaryResponse::PushedMixMessage { .. } => {
self.into_encrypted_tagged_bytes(shared_key)?
}
};
Ok(Message::Binary(blob))
}
}
@@ -0,0 +1,98 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::SharedKeyUsageError;
use nym_credentials_interface::CompactEcashError;
use nym_sphinx::addressing::nodes::NymNodeRoutingAddressError;
use nym_sphinx::forwarding::packet::MixPacketFormattingError;
use nym_sphinx::params::packet_sizes::PacketSize;
use serde::{Deserialize, Serialize};
use std::string::FromUtf8Error;
use thiserror::Error;
// specific errors (that should not be nested!!) for clients to match on
#[derive(Debug, Copy, Clone, Error, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SimpleGatewayRequestsError {
#[error("insufficient bandwidth available to process the request. required: {required}B, available: {available}B")]
OutOfBandwidth { required: i64, available: i64 },
#[error("the provided ticket has already been spent before at this gateway")]
TicketReplay,
}
impl SimpleGatewayRequestsError {
pub fn is_ticket_replay(&self) -> bool {
matches!(self, SimpleGatewayRequestsError::TicketReplay)
}
}
#[derive(Debug, Error)]
pub enum GatewayRequestsError {
#[error(transparent)]
KeyUsageFailure(#[from] SharedKeyUsageError),
#[error("the received request is malformed: {source}")]
MalformedRequest { source: serde_json::Error },
#[error("the received response is malformed: {source}")]
MalformedResponse { source: serde_json::Error },
#[error("received request with an unknown kind: {kind}")]
UnknownRequestKind { kind: u8 },
#[error("received response with an unknown kind: {kind}")]
UnknownResponseKind { kind: u8 },
#[error("the encryption flag had an unexpected value")]
InvalidEncryptionFlag,
#[error("the request is too short")]
TooShortRequest,
#[error("provided MAC is invalid")]
InvalidMac,
#[error("address field was incorrectly encoded: {source}")]
IncorrectlyEncodedAddress {
#[from]
source: NymNodeRoutingAddressError,
},
#[error("received request had invalid size. (actual: {0}, but expected one of: {} (ACK), {} (REGULAR), {}, {}, {} (EXTENDED))",
PacketSize::AckPacket.size(),
PacketSize::RegularPacket.size(),
PacketSize::ExtendedPacket8.size(),
PacketSize::ExtendedPacket16.size(),
PacketSize::ExtendedPacket32.size())
]
RequestOfInvalidSize(usize),
#[error("received sphinx packet was malformed")]
MalformedSphinxPacket,
#[error("failed to serialise created sphinx packet: {0}")]
SphinxSerialisationFailure(#[from] MixPacketFormattingError),
#[error("the received encrypted data was malformed")]
MalformedEncryption,
#[error("provided packet mode is invalid")]
InvalidPacketMode,
#[error("failed to deserialize provided credential: {0}")]
EcashCredentialDeserializationFailure(#[from] CompactEcashError),
#[error("failed to deserialize provided credential: EOF")]
CredentialDeserializationFailureEOF,
#[error("failed to deserialize provided credential: malformed string: {0}")]
CredentialDeserializationFailureMalformedString(#[from] FromUtf8Error),
#[error("the provided [v1] credential has invalid number of parameters - {0}")]
InvalidNumberOfEmbededParameters(u32),
// variant to catch legacy errors
#[error("{0}")]
Other(String),
}
@@ -0,0 +1,133 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{
BinaryRequest, BinaryRequestKind, BinaryResponse, BinaryResponseKind, GatewayRequestsError,
SharedGatewayKey,
};
use std::iter::once;
// each binary message consists of the following structure (for non-legacy messages)
// KIND || ENC_FLAG || MAYBE_NONCE || CIPHERTEXT/PLAINTEXT
// first byte is the kind of data to influence further serialisation/deseralisation
// second byte is a flag indicating whether the content is encrypted
// then it's followed by a pseudorandom nonce, assuming encryption is used
// finally, the rest of the message is the associated ciphertext or just plaintext (if message wasn't encrypted)
pub struct BinaryData<'a> {
kind: u8,
encrypted: bool,
maybe_nonce: Option<&'a [u8]>,
data: &'a [u8],
}
impl<'a> BinaryData<'a> {
// serialises possibly encrypted data into bytes to be put on the wire
pub fn into_raw(self, legacy: bool) -> Vec<u8> {
if legacy {
return self.data.to_vec();
}
let i = once(self.kind).chain(once(if self.encrypted { 1 } else { 0 }));
if let Some(nonce) = self.maybe_nonce {
i.chain(nonce.iter().copied())
.chain(self.data.iter().copied())
.collect()
} else {
i.chain(self.data.iter().copied()).collect()
}
}
// attempts to perform basic parsing on bytes received from the wire
pub fn from_raw(
raw: &'a [u8],
available_key: &SharedGatewayKey,
) -> Result<Self, GatewayRequestsError> {
// if we're using legacy key, it's quite simple:
// it's always encrypted with no nonce and the request/response kind is always 1
if available_key.is_legacy() {
return Ok(BinaryData {
kind: 1,
encrypted: true,
maybe_nonce: None,
data: raw,
});
}
if raw.len() < 2 {
return Err(GatewayRequestsError::TooShortRequest);
}
let kind = raw[0];
let encrypted = if raw[1] == 1 {
true
} else if raw[1] == 0 {
false
} else {
return Err(GatewayRequestsError::InvalidEncryptionFlag);
};
// if data is encrypted, there MUST be a nonce present for non-legacy keys
if encrypted && raw.len() < available_key.nonce_size() + 2 {
return Err(GatewayRequestsError::TooShortRequest);
}
Ok(BinaryData {
kind,
encrypted,
maybe_nonce: Some(&raw[2..2 + available_key.nonce_size()]),
data: &raw[2 + available_key.nonce_size()..],
})
}
// attempt to encrypt plaintext of provided response/request and serialise it into wire format
pub fn make_encrypted_blob(
kind: u8,
plaintext: &[u8],
key: &SharedGatewayKey,
) -> Result<Vec<u8>, GatewayRequestsError> {
let maybe_nonce = key.random_nonce_or_zero_iv();
let ciphertext = key.encrypt(plaintext, maybe_nonce.as_deref())?;
Ok(BinaryData {
kind,
encrypted: true,
maybe_nonce: maybe_nonce.as_deref(),
data: &ciphertext,
}
.into_raw(key.is_legacy()))
}
// attempts to parse previously recovered bytes into a [`BinaryRequest`]
pub fn into_request(
self,
key: &SharedGatewayKey,
) -> Result<BinaryRequest, GatewayRequestsError> {
let kind = BinaryRequestKind::from_repr(self.kind)
.ok_or(GatewayRequestsError::UnknownRequestKind { kind: self.kind })?;
let plaintext = if self.encrypted {
&*key.decrypt(self.data, self.maybe_nonce)?
} else {
self.data
};
BinaryRequest::from_plaintext(kind, plaintext)
}
// attempts to parse previously recovered bytes into a [`BinaryResponse`]
pub fn into_response(
self,
key: &SharedGatewayKey,
) -> Result<BinaryResponse, GatewayRequestsError> {
let kind = BinaryResponseKind::from_repr(self.kind)
.ok_or(GatewayRequestsError::UnknownResponseKind { kind: self.kind })?;
let plaintext = if self.encrypted {
&*key.decrypt(self.data, self.maybe_nonce)?
} else {
self.data
};
BinaryResponse::from_plaintext(kind, plaintext)
}
}
+18
View File
@@ -0,0 +1,18 @@
// Copyright 2020-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod binary_request;
pub mod binary_response;
pub mod error;
mod helpers;
pub mod registration_handshake_wrapper;
pub mod text_request;
pub mod text_response;
// just to preserve existing imports
pub use binary_request::*;
pub use binary_response::*;
pub use error::*;
pub use registration_handshake_wrapper::*;
pub use text_request::*;
pub use text_response::*;
@@ -0,0 +1,103 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum RegistrationHandshake {
HandshakePayload {
#[serde(default)]
protocol_version: Option<u8>,
data: Vec<u8>,
},
HandshakeError {
message: String,
},
}
impl RegistrationHandshake {
pub fn new_payload(data: Vec<u8>, protocol_version: u8) -> Self {
RegistrationHandshake::HandshakePayload {
protocol_version: Some(protocol_version),
data,
}
}
pub fn new_error<S: Into<String>>(message: S) -> Self {
RegistrationHandshake::HandshakeError {
message: message.into(),
}
}
}
impl FromStr for RegistrationHandshake {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}
impl TryFrom<String> for RegistrationHandshake {
type Error = serde_json::Error;
fn try_from(msg: String) -> Result<Self, serde_json::Error> {
msg.parse()
}
}
impl TryInto<String> for RegistrationHandshake {
type Error = serde_json::Error;
fn try_into(self) -> Result<String, serde_json::Error> {
serde_json::to_string(&self)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ClientControlRequest;
#[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_with_protocol = RegistrationHandshake::HandshakePayload {
protocol_version: Some(42),
data: handshake_data.clone(),
};
let serialized = serde_json::to_string(&handshake_payload_with_protocol).unwrap();
let deserialized = ClientControlRequest::try_from(serialized).unwrap();
match deserialized {
ClientControlRequest::RegisterHandshakeInitRequest {
protocol_version,
data,
} => {
assert_eq!(protocol_version, Some(42));
assert_eq!(data, handshake_data)
}
_ => unreachable!("this branch shouldn't have been reached!"),
}
let handshake_payload_without_protocol = RegistrationHandshake::HandshakePayload {
protocol_version: None,
data: handshake_data.clone(),
};
let serialized = serde_json::to_string(&handshake_payload_without_protocol).unwrap();
let deserialized = ClientControlRequest::try_from(serialized).unwrap();
match deserialized {
ClientControlRequest::RegisterHandshakeInitRequest {
protocol_version,
data,
} => {
assert!(protocol_version.is_none());
assert_eq!(data, handshake_data)
}
_ => unreachable!("this branch shouldn't have been reached!"),
}
}
}
@@ -0,0 +1,199 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::CredentialSpendingRequest;
use crate::{
GatewayRequestsError, SharedGatewayKey, SymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION,
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION,
};
use nym_credentials_interface::CredentialSpendingData;
use nym_sphinx::DestinationAddressBytes;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use tungstenite::Message;
// wrapper for all encrypted requests for ease of use
#[derive(Serialize, Deserialize, Debug)]
#[non_exhaustive]
pub enum ClientRequest {
UpgradeKey {
hkdf_salt: Vec<u8>,
derived_key_digest: Vec<u8>,
},
}
impl ClientRequest {
pub fn encrypt<S: SymmetricKey>(
&self,
key: &S,
) -> Result<ClientControlRequest, GatewayRequestsError> {
// we're using json representation for few reasons:
// - ease of re-implementation in other languages (compared to for example bincode)
// - we expect all requests to be relatively small - for anything bigger use BinaryRequest!
// - the schema is self-describing which simplifies deserialisation
// SAFETY: the trait has been derived correctly with no weird variants
let plaintext = serde_json::to_vec(self).unwrap();
let nonce = key.random_nonce_or_iv();
let ciphertext = key.encrypt(&plaintext, Some(&nonce))?;
Ok(ClientControlRequest::EncryptedRequest { ciphertext, nonce })
}
pub fn decrypt<S: SymmetricKey>(
ciphertext: &[u8],
nonce: &[u8],
key: &S,
) -> Result<Self, GatewayRequestsError> {
let plaintext = key.decrypt(ciphertext, Some(nonce))?;
serde_json::from_slice(&plaintext)
.map_err(|source| GatewayRequestsError::MalformedRequest { source })
}
}
// if you're adding new variants here, consider putting them inside `ClientRequest` instead
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type", rename_all = "camelCase")]
#[non_exhaustive]
pub enum ClientControlRequest {
// TODO: should this also contain a MAC considering that at this point we already
// have the shared key derived?
Authenticate {
#[serde(default)]
protocol_version: Option<u8>,
address: String,
enc_address: String,
iv: String,
},
#[serde(alias = "handshakePayload")]
RegisterHandshakeInitRequest {
#[serde(default)]
protocol_version: Option<u8>,
data: Vec<u8>,
},
BandwidthCredential {
enc_credential: Vec<u8>,
iv: Vec<u8>,
},
BandwidthCredentialV2 {
enc_credential: Vec<u8>,
iv: Vec<u8>,
},
EcashCredential {
enc_credential: Vec<u8>,
iv: Vec<u8>,
},
ClaimFreeTestnetBandwidth,
EncryptedRequest {
ciphertext: Vec<u8>,
nonce: Vec<u8>,
},
SupportedProtocol {},
// if you're adding new variants here, consider putting them inside `ClientRequest` instead
}
impl ClientControlRequest {
pub fn new_authenticate(
address: DestinationAddressBytes,
shared_key: &SharedGatewayKey,
uses_credentials: bool,
) -> Result<Self, GatewayRequestsError> {
// if we're encrypting with non-legacy key, the remote must support AES256-GCM-SIV
let protocol_version = if !shared_key.is_legacy() {
Some(AES_GCM_SIV_PROTOCOL_VERSION)
} else if uses_credentials {
Some(CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION)
} else {
// if we're not going to be using credentials, advertise lower protocol version to allow connection
// to wider range of gateways
Some(INITIAL_PROTOCOL_VERSION)
};
let nonce = shared_key.random_nonce_or_iv();
let ciphertext = shared_key.encrypt_naive(address.as_bytes_ref(), Some(&nonce))?;
Ok(ClientControlRequest::Authenticate {
protocol_version,
address: address.as_base58_string(),
enc_address: bs58::encode(&ciphertext).into_string(),
iv: bs58::encode(&nonce).into_string(),
})
}
pub fn name(&self) -> String {
match self {
ClientControlRequest::Authenticate { .. } => "Authenticate".to_string(),
ClientControlRequest::RegisterHandshakeInitRequest { .. } => {
"RegisterHandshakeInitRequest".to_string()
}
ClientControlRequest::BandwidthCredential { .. } => "BandwidthCredential".to_string(),
ClientControlRequest::BandwidthCredentialV2 { .. } => {
"BandwidthCredentialV2".to_string()
}
ClientControlRequest::EcashCredential { .. } => "EcashCredential".to_string(),
ClientControlRequest::ClaimFreeTestnetBandwidth => {
"ClaimFreeTestnetBandwidth".to_string()
}
ClientControlRequest::SupportedProtocol { .. } => "SupportedProtocol".to_string(),
ClientControlRequest::EncryptedRequest { .. } => "EncryptedRequest".to_string(),
}
}
pub fn new_enc_ecash_credential(
credential: CredentialSpendingData,
shared_key: &SharedGatewayKey,
) -> Result<Self, GatewayRequestsError> {
let cred = CredentialSpendingRequest::new(credential);
let serialized_credential = cred.to_bytes();
let nonce = shared_key.random_nonce_or_iv();
let enc_credential = shared_key.encrypt(&serialized_credential, Some(&nonce))?;
Ok(ClientControlRequest::EcashCredential {
enc_credential,
iv: nonce,
})
}
pub fn try_from_enc_ecash_credential(
enc_credential: Vec<u8>,
shared_key: &SharedGatewayKey,
iv: Vec<u8>,
) -> Result<CredentialSpendingRequest, GatewayRequestsError> {
let credential_bytes = shared_key.decrypt(&enc_credential, Some(&iv))?;
CredentialSpendingRequest::try_from_bytes(credential_bytes.as_slice())
.map_err(|_| GatewayRequestsError::MalformedEncryption)
}
}
impl From<ClientControlRequest> for Message {
fn from(req: ClientControlRequest) -> Self {
// it should be safe to call `unwrap` here as the message is generated by the server
// so if it fails (and consequently panics) it's a bug that should be resolved
let str_req = serde_json::to_string(&req).unwrap();
Message::Text(str_req)
}
}
impl TryFrom<String> for ClientControlRequest {
type Error = serde_json::Error;
fn try_from(msg: String) -> Result<Self, Self::Error> {
msg.parse()
}
}
impl FromStr for ClientControlRequest {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}
impl TryInto<String> for ClientControlRequest {
type Error = serde_json::Error;
fn try_into(self) -> Result<String, Self::Error> {
serde_json::to_string(&self)
}
}
@@ -0,0 +1,130 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::{GatewayRequestsError, SimpleGatewayRequestsError, SymmetricKey};
use serde::{Deserialize, Serialize};
use tungstenite::Message;
// naming things is difficult...
// the name implies that the content is encrypted before being sent on the wire
#[derive(Serialize, Deserialize, Debug)]
#[non_exhaustive]
pub enum SensitiveServerResponse {
KeyUpgradeAck {},
}
impl SensitiveServerResponse {
pub fn encrypt<S: SymmetricKey>(
&self,
key: &S,
) -> Result<ServerResponse, GatewayRequestsError> {
// we're using json representation for few reasons:
// - ease of re-implementation in other languages (compared to for example bincode)
// - we expect all requests to be relatively small - for anything bigger use BinaryRequest!
// - the schema is self-describing which simplifies deserialisation
// SAFETY: the trait has been derived correctly with no weird variants
let plaintext = serde_json::to_vec(self).unwrap();
let nonce = key.random_nonce_or_iv();
let ciphertext = key.encrypt(&plaintext, Some(&nonce))?;
Ok(ServerResponse::EncryptedResponse { ciphertext, nonce })
}
pub fn decrypt<S: SymmetricKey>(
ciphertext: &[u8],
nonce: &[u8],
key: &S,
) -> Result<Self, GatewayRequestsError> {
let plaintext = key.decrypt(ciphertext, Some(nonce))?;
serde_json::from_slice(&plaintext)
.map_err(|source| GatewayRequestsError::MalformedRequest { source })
}
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type", rename_all = "camelCase")]
#[non_exhaustive]
pub enum ServerResponse {
Authenticate {
#[serde(default)]
protocol_version: Option<u8>,
status: bool,
bandwidth_remaining: i64,
},
Register {
#[serde(default)]
protocol_version: Option<u8>,
status: bool,
},
EncryptedResponse {
ciphertext: Vec<u8>,
nonce: Vec<u8>,
},
Bandwidth {
available_total: i64,
},
Send {
remaining_bandwidth: i64,
},
SupportedProtocol {
version: u8,
},
// Generic error
Error {
message: String,
},
// Specific typed errors
// so that clients could match on this variant without doing naive string matching
TypedError {
error: SimpleGatewayRequestsError,
},
}
impl ServerResponse {
pub fn name(&self) -> String {
match self {
ServerResponse::Authenticate { .. } => "Authenticate".to_string(),
ServerResponse::Register { .. } => "Register".to_string(),
ServerResponse::Bandwidth { .. } => "Bandwidth".to_string(),
ServerResponse::Send { .. } => "Send".to_string(),
ServerResponse::Error { .. } => "Error".to_string(),
ServerResponse::TypedError { .. } => "TypedError".to_string(),
ServerResponse::SupportedProtocol { .. } => "SupportedProtocol".to_string(),
ServerResponse::EncryptedResponse { .. } => "EncryptedResponse".to_string(),
}
}
pub fn new_error<S: Into<String>>(msg: S) -> Self {
ServerResponse::Error {
message: msg.into(),
}
}
pub fn is_error(&self) -> bool {
matches!(self, ServerResponse::Error { .. })
}
pub fn implies_successful_authentication(&self) -> bool {
match self {
ServerResponse::Authenticate { status, .. } => *status,
ServerResponse::Register { status, .. } => *status,
_ => false,
}
}
}
impl From<ServerResponse> for Message {
fn from(res: ServerResponse) -> Self {
// it should be safe to call `unwrap` here as the message is generated by the server
// so if it fails (and consequently panics) it's a bug that should be resolved
let str_res = serde_json::to_string(&res).unwrap();
Message::Text(str_res)
}
}
impl TryFrom<String> for ServerResponse {
type Error = serde_json::Error;
fn try_from(msg: String) -> Result<Self, serde_json::Error> {
serde_json::from_str(&msg)
}
}
+1
View File
@@ -19,6 +19,7 @@ sqlx = { workspace = true, features = [
"macros",
"migrate",
"time",
"chrono"
] }
time = { workspace = true }
thiserror = { workspace = true }
@@ -73,6 +73,9 @@ CREATE TABLE ticket_verification_tmp (
PRIMARY KEY (ticket_id, signer_id)
);
INSERT INTO ticket_verification_tmp
SELECT * FROM ticket_verification;
DROP INDEX ticket_verification_index;
CREATE INDEX ticket_verification_index ON ticket_verification_tmp (ticket_id);
@@ -85,6 +88,9 @@ CREATE TABLE verified_tickets_tmp (
proposal_id INTEGER REFERENCES redemption_proposals(proposal_id)
);
INSERT INTO verified_tickets_tmp
SELECT * FROM verified_tickets;
DROP INDEX verified_tickets_index;
CREATE INDEX verified_tickets_index ON verified_tickets_tmp (proposal_id);
@@ -0,0 +1,13 @@
/*
* Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: GPL-3.0-only
*/
-- make aes128 key column nullable and add aes256 column
ALTER TABLE shared_keys RENAME COLUMN derived_aes128_ctr_blake3_hmac_keys_bs58 TO derived_aes128_ctr_blake3_hmac_keys_bs58_old;
ALTER TABLE shared_keys ADD COLUMN derived_aes128_ctr_blake3_hmac_keys_bs58 TEXT;
ALTER TABLE shared_keys ADD COLUMN derived_aes256_gcm_siv_key BLOB;
UPDATE shared_keys SET derived_aes128_ctr_blake3_hmac_keys_bs58 = derived_aes128_ctr_blake3_hmac_keys_bs58_old;
ALTER TABLE shared_keys DROP COLUMN derived_aes128_ctr_blake3_hmac_keys_bs58_old;
+1
View File
@@ -7,6 +7,7 @@ use crate::models::Client;
#[derive(Debug, PartialEq, sqlx::Type)]
#[sqlx(type_name = "TEXT")] // SQLite TEXT type
#[sqlx(rename_all = "snake_case")]
pub enum ClientType {
EntryMixnet,
ExitMixnet,
+3
View File
@@ -11,6 +11,9 @@ pub enum StorageError {
#[error("Failed to perform database migration: {0}")]
MigrationError(#[from] sqlx::migrate::MigrateError),
#[error("could not find any valid shared keys for under id {id}")]
MissingSharedKey { id: i64 },
#[error("Somehow stored data is incorrect: {0}")]
DataCorruption(String),
+8 -5
View File
@@ -11,7 +11,7 @@ use models::{
VerifiedTicket, WireguardPeer,
};
use nym_credentials_interface::ClientTicket;
use nym_gateway_requests::registration::handshake::SharedKeys;
use nym_gateway_requests::shared_key::SharedGatewayKey;
use nym_sphinx::DestinationAddressBytes;
use shared_keys::SharedKeysManager;
use sqlx::ConnectOptions;
@@ -42,11 +42,13 @@ pub trait Storage: Send + Sync {
/// # Arguments
///
/// * `client_address`: base58-encoded address of the client
/// * `shared_keys`: shared encryption (AES128CTR) and mac (hmac-blake3) derived shared keys to store.
/// * `shared_keys`:
/// - legacy: shared encryption (AES128CTR) and mac (hmac-blake3) derived shared keys to store.
/// - current: shared AES256-GCM-SIV keys
async fn insert_shared_keys(
&self,
client_address: DestinationAddressBytes,
shared_keys: &SharedKeys,
shared_keys: &SharedGatewayKey,
) -> Result<i64, StorageError>;
/// Tries to retrieve shared keys stored for the particular client.
@@ -330,7 +332,7 @@ impl Storage for PersistentStorage {
async fn insert_shared_keys(
&self,
client_address: DestinationAddressBytes,
shared_keys: &SharedKeys,
shared_keys: &SharedGatewayKey,
) -> Result<i64, StorageError> {
let client_id = self
.client_manager
@@ -340,7 +342,8 @@ impl Storage for PersistentStorage {
.insert_shared_keys(
client_id,
client_address.as_base58_string(),
shared_keys.to_base58_string(),
shared_keys.aes128_ctr_hmac_bs58().as_deref(),
shared_keys.aes256_gcm_siv().as_deref(),
)
.await?;
Ok(client_id)
+29 -1
View File
@@ -3,6 +3,7 @@
use crate::error::StorageError;
use nym_credentials_interface::{AvailableBandwidth, ClientTicket, CredentialSpendingData};
use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey};
use sqlx::FromRow;
use time::OffsetDateTime;
@@ -11,13 +12,40 @@ pub struct Client {
pub client_type: crate::clients::ClientType,
}
#[derive(FromRow)]
pub struct PersistedSharedKeys {
#[allow(dead_code)]
pub client_id: i64,
#[allow(dead_code)]
pub client_address_bs58: String,
pub derived_aes128_ctr_blake3_hmac_keys_bs58: String,
pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option<String>,
pub derived_aes256_gcm_siv_key: Option<Vec<u8>>,
}
impl TryFrom<PersistedSharedKeys> for SharedGatewayKey {
type Error = StorageError;
fn try_from(value: PersistedSharedKeys) -> Result<Self, Self::Error> {
match (
&value.derived_aes256_gcm_siv_key,
&value.derived_aes128_ctr_blake3_hmac_keys_bs58,
) {
(None, None) => Err(StorageError::MissingSharedKey {
id: value.client_id,
}),
(Some(aes256gcm_siv), _) => {
let current_key = SharedSymmetricKey::try_from_bytes(aes256gcm_siv)
.map_err(|source| StorageError::DataCorruption(source.to_string()))?;
Ok(SharedGatewayKey::Current(current_key))
}
(None, Some(aes128ctr_hmac)) => {
let legacy_key = LegacySharedKeys::try_from_base58_string(aes128ctr_hmac)
.map_err(|source| StorageError::DataCorruption(source.to_string()))?;
Ok(SharedGatewayKey::Legacy(legacy_key))
}
}
}
}
pub struct StoredMessage {
+11 -3
View File
@@ -41,19 +41,27 @@ impl SharedKeysManager {
&self,
client_id: i64,
client_address_bs58: String,
derived_aes128_ctr_blake3_hmac_keys_bs58: String,
derived_aes128_ctr_blake3_hmac_keys_bs58: Option<&String>,
derived_aes256_gcm_siv_key: Option<&Vec<u8>>,
) -> Result<(), sqlx::Error> {
// https://stackoverflow.com/a/20310838
// we don't want to be using `INSERT OR REPLACE INTO` due to the foreign key on `available_bandwidth` if the entry already exists
sqlx::query!(
r#"
INSERT OR IGNORE INTO shared_keys(client_id, client_address_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58) VALUES (?, ?, ?);
UPDATE shared_keys SET derived_aes128_ctr_blake3_hmac_keys_bs58 = ? WHERE client_address_bs58 = ?
INSERT OR IGNORE INTO shared_keys(client_id, client_address_bs58, derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key) VALUES (?, ?, ?, ?);
UPDATE shared_keys
SET
derived_aes128_ctr_blake3_hmac_keys_bs58 = ?,
derived_aes256_gcm_siv_key = ?
WHERE client_address_bs58 = ?
"#,
client_id,
client_address_bs58,
derived_aes128_ctr_blake3_hmac_keys_bs58,
derived_aes256_gcm_siv_key,
derived_aes128_ctr_blake3_hmac_keys_bs58,
derived_aes256_gcm_siv_key,
client_address_bs58,
).execute(&self.connection_pool).await?;
+6 -2
View File
@@ -10,6 +10,7 @@ use nym_bin_common::build_information::{BinaryBuildInformation, BinaryBuildInfor
pub struct UserAgent {
pub application: String,
pub version: String,
pub system: Option<String>,
pub platform: String,
pub git_commit: String,
}
@@ -17,10 +18,11 @@ pub struct UserAgent {
impl fmt::Display for UserAgent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let abbreviated_commit = self.git_commit.chars().take(7).collect::<String>();
let system = self.system.clone().unwrap_or_else(|| "unknown".to_string());
write!(
f,
"{}/{}/{}/{}",
self.application, self.version, self.platform, abbreviated_commit
"{}/{}/{}/{}/{}",
self.application, self.version, system, self.platform, abbreviated_commit
)
}
}
@@ -38,6 +40,7 @@ impl From<BinaryBuildInformation> for UserAgent {
UserAgent {
application: build_info.binary_name.to_string(),
version: build_info.build_version.to_string(),
system: None,
platform: build_info.cargo_triple.to_string(),
git_commit: build_info.commit_sha.to_string(),
}
@@ -49,6 +52,7 @@ impl From<BinaryBuildInformationOwned> for UserAgent {
UserAgent {
application: build_info.binary_name,
version: build_info.build_version,
system: None,
platform: build_info.cargo_triple,
git_commit: build_info.commit_sha,
}
+1 -1
View File
@@ -14,7 +14,7 @@ tokio-stream = { workspace = true } # this one seems to be a thing until `Stream
workspace = true
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-util]
version = "0.7.12"
workspace = true
features = ["time"]
[target."cfg(target_arch = \"wasm32\")".dependencies.wasmtimer]
@@ -616,6 +616,7 @@ fn compute_kappa(
}
/// Represents the unique payment information associated with the payment.
///
/// The bytes representing the payment information encode the public key of the
/// provider with whom you are spending the payment, timestamp and a unique random 32 bytes.
///
+3
View File
@@ -581,6 +581,7 @@ impl TryFrom<&[u8]> for KeyPair {
}
/// Generate a single Coconut keypair ((x, y0, y1...), (g2^x, g2^y0, ...)).
///
/// It is not suitable for threshold credentials as all subsequent calls to `keygen` generate keys
/// that are independent of each other.
pub fn keygen(params: &Parameters) -> KeyPair {
@@ -599,6 +600,8 @@ pub fn keygen(params: &Parameters) -> KeyPair {
}
}
/// Generate Coconut keypairs.
///
/// Generate a set of n Coconut keypairs [((x, y0, y1...), (g2^x, g2^y0, ...)), ...],
/// such that they support threshold aggregation by `threshold` number of parties.
/// It is expected that this procedure is executed by a Trusted Third Party.
+1 -1
View File
@@ -42,7 +42,7 @@ nym-crypto = { path = "../crypto", version = "0.4.0", features = [
path = "framing"
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
version = "1.39.3"
workspace = true
features = ["sync"]
[features]
+2 -2
View File
@@ -9,12 +9,12 @@ repository = { workspace = true }
[dependencies]
rand = { workspace = true }
serde = { version = "1.0", optional = true, default-features = false, features = ["derive"] }
serde = { workspace = true, features = ["derive"], optional = true }
generic-array = { workspace = true, optional = true, features = ["serde"] }
thiserror = { workspace = true }
zeroize = { workspace = true }
nym-crypto = { path = "../../crypto", features = ["symmetric", "rand"] }
nym-crypto = { path = "../../crypto", features = ["stream_cipher", "rand"] }
nym-pemstore = { path = "../../pemstore" }
nym-sphinx-addressing = { path = "../addressing" }
nym-sphinx-params = { path = "../params" }
+2
View File
@@ -12,6 +12,8 @@ use thiserror::Error;
pub type NodeIdentity = identity::PublicKey;
pub const NODE_IDENTITY_SIZE: usize = identity::PUBLIC_KEY_LENGTH;
/// Encodoing and decoding node routing information.
///
/// This module is responsible for encoding and decoding node routing information, so that
/// they could be later put into an appropriate field in a sphinx header.
/// Currently, that routing information is an IP address, but in principle it can be anything
@@ -13,7 +13,7 @@ bs58 = { workspace = true }
serde = { workspace = true }
thiserror = { workspace = true }
nym-crypto = { path = "../../crypto", features = ["symmetric", "rand"] }
nym-crypto = { path = "../../crypto", features = ["stream_cipher", "rand"] }
nym-sphinx-addressing = { path = "../addressing" }
nym-sphinx-params = { path = "../params" }
nym-sphinx-routing = { path = "../routing" }
@@ -22,6 +22,8 @@ use std::fmt::{self, Debug, Formatter};
// (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)
/// Length of the header of a `Fragment` when it's part of a single `FragmentSet`.
///
/// 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
@@ -29,6 +31,8 @@ use std::fmt::{self, Debug, Formatter};
/// and finally an extra byte to indicate the fragment has no links to other sets.
pub const UNLINKED_FRAGMENTED_HEADER_LEN: usize = 7;
/// Length of the header of a `Fragment` when it's linked.
///
/// 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.
@@ -111,6 +115,7 @@ impl FragmentIdentifier {
}
/// 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.
@@ -283,6 +288,8 @@ impl Fragment {
}
}
/// Header of a `Fragment` that is used to be able to reconstruct the original message.
///
/// 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.
+4
View File
@@ -14,6 +14,8 @@ pub const fn max_unlinked_set_payload_length(max_plaintext_size: usize) -> usize
u8::MAX as usize * unlinked_fragment_payload_max_len(max_plaintext_size)
}
/// Maximum payload length for a set that is being linked to another one, but is not the last one.
///
/// 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
/// bit extra data overhead: id of the other set.
@@ -25,6 +27,8 @@ pub const fn max_one_way_linked_set_payload_length(max_plaintext_size: usize) ->
- (LINKED_FRAGMENTED_HEADER_LEN - UNLINKED_FRAGMENTED_HEADER_LEN)
}
/// Set payload length for a set that is being linked to another one, but is not the last one.
///
/// 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.
+1 -1
View File
@@ -11,7 +11,7 @@ repository = { workspace = true }
thiserror = { workspace = true }
serde = { workspace = true, features = ["derive"] }
nym-crypto = { path = "../../crypto", features = ["hashing", "symmetric"] }
nym-crypto = { path = "../../crypto", features = ["hashing", "stream_cipher", "aead"] }
nym-sphinx-types = { path = "../types" }
[features]
+10 -4
View File
@@ -4,6 +4,7 @@
use nym_crypto::aes::Aes128;
use nym_crypto::blake3;
use nym_crypto::ctr;
use nym_crypto::Aes256GcmSiv;
type Aes128Ctr = ctr::Ctr64BE<Aes128>;
@@ -48,7 +49,7 @@ pub type GatewaySharedKeyHkdfAlgorithm = blake3::Hasher;
pub type ReplySurbKeyDigestAlgorithm = blake3::Hasher;
/// Hashing algorithm used when computing integrity (H)Mac for message exchanged between client and gateway.
// TODO: if updated, the pem type defined in gateway\gateway-requests\src\registration\handshake\shared_key
// TODO: if updated, the pem type defined in gateway\gateway-requests\src\registration\handshake\legacy_shared_key
// needs updating!
pub type GatewayIntegrityHmacAlgorithm = blake3::Hasher;
@@ -59,11 +60,16 @@ pub type GatewayIntegrityHmacAlgorithm = blake3::Hasher;
// - the pem type defined in nym\common\nymsphinx\acknowledgements\src\key needs updating!
pub type AckEncryptionAlgorithm = Aes128Ctr;
/// Legacy encryption algorithm used for end-to-end encryption of messages exchanged between clients
/// and their gateways.
// TODO: if updated, the pem type defined in gateway\gateway-requests\src\registration\handshake\legacy_shared_key
// needs updating!
pub type LegacyGatewayEncryptionAlgorithm = Aes128Ctr;
/// Encryption algorithm used for end-to-end encryption of messages exchanged between clients
/// and their gateways.
// TODO: if updated, the pem type defined in gateway\gateway-requests\src\registration\handshake\shared_key
// needs updating!
pub type GatewayEncryptionAlgorithm = Aes128Ctr;
// NOTE: if updated, the pem type defined in gateway\gateway-requests\src\registration\handshake\shared_key
pub type GatewayEncryptionAlgorithm = Aes256GcmSiv;
/// Encryption algorithm used for end-to-end encryption of messages exchanged between clients that are
/// encapsulated inside sphinx packets.
+2
View File
@@ -302,6 +302,8 @@ pub trait FragmentPreparer {
}
}
/// Prepares the message that is to be sent through the mix network.
///
/// Prepares the message that is to be sent through the mix network by attaching
/// an optional reply-SURB, padding it to appropriate length, encrypting its content,
/// and chunking into appropriate size [`Fragment`]s.
@@ -10,9 +10,11 @@ use nym_task::connections::{ConnectionCommand, ConnectionCommandSender};
use nym_task::TaskClient;
use std::collections::{HashMap, HashSet};
/// A generic message produced after reading from a socket/connection. It includes data that was
/// actually read alongside boolean indicating whether the connection got closed so that
/// remote could act accordingly.
/// A generic message produced after reading from a socket/connection.
///
/// A generic message produced after reading from a socket/connection that includes data that was
/// actually read alongside boolean indicating whether the connection got closed so that remote
/// could act accordingly.
#[derive(Debug)]
pub struct ConnectionMessage {
pub payload: Vec<u8>,
@@ -20,6 +22,7 @@ pub struct ConnectionMessage {
}
/// Channel responsible for sending data that was received from mix network into particular connection.
///
/// Data includes the actual payload that is to be written onto the connection
/// alongside boolean indicating whether the remote connection was closed after producing this message,
/// so that the local connection should also shut down.
+2 -2
View File
@@ -11,13 +11,13 @@ documentation = { workspace = true }
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
async-trait = { workspace = true, optional = true }
bs58 = { workspace = true }
log = { workspace = true }
rand = { workspace = true }
reqwest = { workspace = true, features = ["json"] }
semver = { workspace = true }
thiserror = { workspace = true }
async-trait = { workspace = true, optional = true }
semver = { version = "0.11" }
# 'serializable' feature
serde = { workspace = true, features = ["derive"], optional = true }
+1 -1
View File
@@ -18,4 +18,4 @@ log.workspace = true
nym-wireguard-types = { path = "../wireguard-types", optional = true }
[target.'cfg(target_os = "linux")'.dependencies]
tokio-tun = "0.11.5"
tokio-tun.workspace = true
@@ -15,6 +15,8 @@ use nym_client_core::client::key_manager::persistence::KeyStore;
use nym_client_core::client::key_manager::ClientKeys;
use nym_client_core::client::replies::reply_storage::browser_backend;
use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage;
use nym_crypto::asymmetric::ed25519::PublicKey;
use nym_gateway_client::SharedSymmetricKey;
use wasm_utils::console_log;
// temporary until other variants are properly implemented (probably it should get changed into `ClientStorage`
@@ -43,8 +45,18 @@ impl MixnetClientStorage for FullWasmClientStorage {
type GatewaysDetailsStore = ClientStorage;
fn into_runtime_stores(self) -> (Self::ReplyStore, Self::CredentialStore) {
(self.reply_storage, self.credential_storage)
fn into_runtime_stores(
self,
) -> (
Self::ReplyStore,
Self::CredentialStore,
Self::GatewaysDetailsStore,
) {
(
self.reply_storage,
self.credential_storage,
self.keys_and_gateway_store,
)
}
fn key_store(&self) -> &Self::KeyStore {
@@ -142,6 +154,19 @@ impl GatewaysDetailsStore for ClientStorage {
self.store_registered_gateway(&raw_registration).await
}
async fn upgrade_stored_remote_gateway_key(
&self,
gateway_id: PublicKey,
updated_key: &SharedSymmetricKey,
) -> Result<(), Self::StorageError> {
self.update_remote_gateway_key(
&gateway_id.to_base58_string(),
None,
Some(updated_key.as_bytes()),
)
.await
}
async fn remove_gateway_details(&self, gateway_id: &str) -> Result<(), Self::StorageError> {
self.remove_registered_gateway(gateway_id).await
}
+19 -6
View File
@@ -4,17 +4,24 @@
use nym_client_core::client::base_client::storage::gateways_storage::{
BadGateway, GatewayDetails, GatewayRegistration, RawRemoteGatewayDetails, RemoteGatewayDetails,
};
use nym_gateway_client::SharedGatewayKey;
use serde::{Deserialize, Serialize};
use std::ops::Deref;
use time::OffsetDateTime;
use zeroize::Zeroize;
// a more nested struct since we only have a single gateway type in wasm (no 'custom')
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, Zeroize)]
pub struct WasmRawRegisteredGateway {
pub gateway_id_bs58: String,
#[zeroize(skip)]
pub registration_timestamp: OffsetDateTime,
pub derived_aes128_ctr_blake3_hmac_keys_bs58: String,
pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option<String>,
#[serde(default)]
pub derived_aes256_gcm_siv_key: Option<Vec<u8>>,
pub gateway_owner_address: Option<String>,
@@ -30,6 +37,7 @@ impl TryFrom<WasmRawRegisteredGateway> for GatewayRegistration {
gateway_id_bs58: value.gateway_id_bs58,
derived_aes128_ctr_blake3_hmac_keys_bs58: value
.derived_aes128_ctr_blake3_hmac_keys_bs58,
derived_aes256_gcm_siv_key: value.derived_aes256_gcm_siv_key,
gateway_owner_address: value.gateway_owner_address,
gateway_listener: value.gateway_listener,
};
@@ -48,17 +56,22 @@ impl<'a> From<&'a GatewayRegistration> for WasmRawRegisteredGateway {
panic!("somehow obtained custom gateway registration in wasm!")
};
let (derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key) =
match remote_details.shared_key.deref() {
SharedGatewayKey::Current(key) => (None, Some(key.to_bytes())),
SharedGatewayKey::Legacy(key) => (Some(key.to_base58_string()), None),
};
WasmRawRegisteredGateway {
gateway_id_bs58: remote_details.gateway_id.to_string(),
registration_timestamp: value.registration_timestamp,
derived_aes128_ctr_blake3_hmac_keys_bs58: remote_details
.derived_aes128_ctr_blake3_hmac_keys
.to_base58_string(),
derived_aes128_ctr_blake3_hmac_keys_bs58,
derived_aes256_gcm_siv_key,
gateway_listener: remote_details.gateway_listener.to_string(),
gateway_owner_address: remote_details
.gateway_owner_address
.as_ref()
.map(|a| a.to_string()),
gateway_listener: remote_details.gateway_listener.to_string(),
}
}
}
@@ -10,6 +10,7 @@ use std::error::Error;
use thiserror::Error;
use wasm_bindgen::JsValue;
use wasm_storage::traits::BaseWasmStorage;
use zeroize::Zeroize;
// v1 tables
pub(crate) mod v1 {
@@ -228,6 +229,23 @@ pub trait WasmClientStorage: BaseWasmStorage {
.map_err(Into::into)
}
async fn update_remote_gateway_key(
&self,
gateway_id_bs58: &str,
derived_aes128_ctr_blake3_hmac_keys_bs58: Option<&str>,
derived_aes256_gcm_siv_key: Option<&[u8]>,
) -> Result<(), <Self as WasmClientStorage>::StorageError> {
if let Some(mut current) = self.maybe_get_registered_gateway(gateway_id_bs58).await? {
current.derived_aes128_ctr_blake3_hmac_keys_bs58 =
derived_aes128_ctr_blake3_hmac_keys_bs58.map(|k| k.to_string());
current.derived_aes256_gcm_siv_key = derived_aes256_gcm_siv_key.map(|k| k.to_vec());
self.store_registered_gateway(&current).await?;
current.zeroize();
}
Ok(())
}
async fn remove_registered_gateway(
&self,
gateway_id: &str,
+44 -66
View File
@@ -11,6 +11,9 @@
# Root options
# The graph table configures how the dependency graph is constructed and thus
# which crates the checks are performed against
[graph]
# If 1 or more target triples (and optionally, target_features) are specified,
# only the specified targets will be checked when running `cargo deny check`.
# This means, if a particular package is only ever used as a target specific
@@ -22,11 +25,13 @@
targets = [
# The triple can be any string, but only the target triples built in to
# rustc (as of 1.40) can be checked against actual config expressions
#{ triple = "x86_64-unknown-linux-musl" },
#"x86_64-unknown-linux-musl",
# You can also specify which target_features you promise are enabled for a
# particular target. target_features are currently not validated against
# the actual valid features supported by the target architecture.
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
"x86_64-unknown-linux-gnu",
"wasm32-unknown-unknown",
]
# When creating the dependency graph used as the source of truth when checks are
# executed, this field can be used to prune crates from the graph, removing them
@@ -46,6 +51,9 @@ no-default-features = false
# If set, these feature will be enabled when collecting metadata. If `--features`
# is specified on the cmd line they will take precedence over this option.
#features = []
# The output table provides options for how/if diagnostics are outputted
[output]
# When outputting inclusion graphs in diagnostics that include features, this
# option can be used to specify the depth at which feature edges will be added.
# This option is included since the graphs can be quite large and the addition
@@ -57,37 +65,18 @@ feature-depth = 1
# More documentation for the advisories section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
[advisories]
# The path where the advisory database is cloned/fetched into
db-path = "~/.cargo/advisory-db"
# The path where the advisory databases are cloned/fetched into
#db-path = "$CARGO_HOME/advisory-dbs"
# The url(s) of the advisory databases to use
db-urls = ["https://github.com/rustsec/advisory-db"]
# The lint level for security vulnerabilities
vulnerability = "deny"
# The lint level for unmaintained crates
unmaintained = "allow"
# The lint level for crates that have been yanked from their source registry
yanked = "warn"
# The lint level for crates with security notices. Note that as of
# 2019-12-17 there are no security notice advisories in
# https://github.com/rustsec/advisory-db
notice = "warn"
#db-urls = ["https://github.com/rustsec/advisory-db"]
# A list of advisory IDs to ignore. Note that ignored advisories will still
# output a note when they are encountered.
ignore = [
"RUSTSEC-2020-0159",
"RUSTSEC-2020-0071",
"RUSTSEC-2021-0145",
#"RUSTSEC-0000-0000",
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
]
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
# lower than the range specified will be ignored. Note that ignored advisories
# will still output a note when they are encountered.
# * None - CVSS Score 0.0
# * Low - CVSS Score 0.1 - 3.9
# * Medium - CVSS Score 4.0 - 6.9
# * High - CVSS Score 7.0 - 8.9
# * Critical - CVSS Score 9.0 - 10.0
#severity-threshold =
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
@@ -98,8 +87,6 @@ ignore = [
# More documentation for the licenses section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
[licenses]
# The lint level for crates which do not have a detectable license
unlicensed = "deny"
# List of explicitly allowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
@@ -115,28 +102,7 @@ allow = [
"Unicode-DFS-2016",
"OpenSSL",
"Zlib",
"Unicode-3.0",
]
# List of explicitly disallowed licenses
# See https://spdx.org/licenses/ for list of possible licenses
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
deny = [
#"Nokia",
]
# Lint level for licenses considered copyleft
copyleft = "warn"
# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
# * both - The license will be approved if it is both OSI-approved *AND* FSF
# * either - The license will be approved if it is either OSI-approved *OR* FSF
# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
# * neither - This predicate is ignored and the default lint level is used
allow-osi-fsf-free = "neither"
# Lint level used when no other predicates are matched
# 1. License isn't in the allow or deny lists
# 2. License isn't copyleft
# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
default = "deny"
# The confidence threshold for detecting a license from license text.
# The higher the value, the more closely the license text must be to the
# canonical license text of a valid SPDX license file.
@@ -147,17 +113,21 @@ confidence-threshold = 0.8
exceptions = [
# Each entry is the crate and version constraint, and its specific allow
# list
#{ allow = ["Zlib"], name = "adler32", version = "*" },
#{ allow = ["Zlib"], crate = "adler32" },
{ allow = ["GPL-3.0"], crate = "nym-api" },
{ allow = ["GPL-3.0"], crate = "nym-gateway" },
{ allow = ["GPL-3.0"], crate = "nym-mixnode" },
{ allow = ["GPL-3.0"], crate = "nym-network-requester" },
{ allow = ["GPL-3.0"], crate = "nym-node" },
{ allow = ["GPL-3.0"], crate = "nym-validator-rewarder" },
]
# Some crates don't have (easily) machine readable licensing information,
# adding a clarification entry for it allows you to manually specify the
# licensing information
[[licenses.clarify]]
# The name of the crate the clarification applies to
name = "ring"
# The optional version constraint for the crate
version = "*"
# The package spec the clarification applies to
crate = "ring"
# The SPDX expression for the license requirements of the crate
expression = "MIT AND ISC AND OpenSSL"
# One or more files in the crate's source used as the "source of truth" for
@@ -198,26 +168,25 @@ wildcards = "allow"
# * all - Both lowest-version and simplest-path are used
highlight = "all"
# The default lint level for `default` features for crates that are members of
# the workspace that is being checked. This can be overriden by allowing/denying
# the workspace that is being checked. This can be overridden by allowing/denying
# `default` on a crate-by-crate basis if desired.
workspace-default-features = "allow"
# The default lint level for `default` features for external crates that are not
# members of the workspace. This can be overriden by allowing/denying `default`
# members of the workspace. This can be overridden by allowing/denying `default`
# on a crate-by-crate basis if desired.
external-default-features = "allow"
# List of crates that are allowed. Use with care!
allow = [
#{ name = "ansi_term", version = "=0.11.0" },
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
]
# List of crates to deny
deny = [
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#{ name = "ansi_term", version = "=0.11.0" },
#
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
# Wrapper crates can optionally be specified to allow the crate when it
# is a direct dependency of the otherwise banned crate
#{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
{ name = "openssl" },
]
@@ -225,7 +194,7 @@ deny = [
# Each entry the name of a crate and a version range. If version is
# not specified, all versions will be matched.
#[[bans.features]]
#name = "reqwest"
#crate = "reqwest"
# Features to not allow
#deny = ["json"]
# Features to allow
@@ -246,16 +215,25 @@ deny = [
# Certain crates/versions that will be skipped when doing duplicate detection.
skip = [
#{ name = "ansi_term", version = "=0.11.0" },
#"ansi_term@0.11.0",
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
]
# Similarly to `skip` allows you to skip certain crates during duplicate
# detection. Unlike skip, it also includes the entire tree of transitive
# dependencies starting at the specified crate, up to a certain depth, which is
# by default infinite.
skip-tree = [
#{ name = "ansi_term", version = "=0.11.0", depth = 20 },
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
#{ crate = "ansi_term@0.11.0", depth = 20 },
]
# NOTE: re-enable once we remove the last duplicates (only digest and sha2 at
# the time of this writing).
#[bans.workspace-dependencies]
#duplicates = "deny"
#include-path-dependencies = false
#unused = "deny"
# This section is considered when running `cargo deny check sources`.
# More documentation about the 'sources' section can be found here:
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
+60 -35
View File
@@ -2,6 +2,31 @@
This page displays a full list of all the changes during our release cycle from [`v2024.3-eclipse`](https://github.com/nymtech/nym/blob/nym-binaries-v2024.3-eclipse/CHANGELOG.md) onwards. Operators can find here the newest updates together with links to relevant documentation. The list is sorted so that the newest changes appear first.
## `nym-node` patch from `release/2024.10-caramello`
- [Patch release binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2024.10-caramello-patch)
```sh
Binary Name: nym-node
Build Timestamp: 2024-09-16T15:00:41.019107021Z
Build Version: 1.1.7
Commit SHA: 65c8982cab0ff3a1154966e7d61956cb42a065fc
Commit Date: 2024-09-16T15:59:34.000000000+02:00
Commit Branch: HEAD
rustc Version: 1.81.0
rustc Channel: stable
cargo Profile: release
```
This patch fixes [`v202410-caramello`](#v202410-caramello) release [bug](#known-bugs--undone-features) where one of the used dependencies - [`DefGuard`](https://github.com/DefGuard/defguard/issues/619), was failing.
Updating to this patched version and running `nym-node --mode exit-gateway` with `--wireguard-enabled true` should result in a smooth node start without the `defguard_wireguard` error, occuring to some operators before:
```sh
/home/ubuntu/.cargo/registry/src/index.crates.io-6f17d22bba15001f/defguard_wireguard_rs-0.4.2/src/netlink.rs:155: Serialized netlink packet (23240 bytes) larger than maximum size 12288: NetlinkMessage.
```
This release is a patch only, there are no additional features, everything else stays the same like in the latest release [`v202410-caramello`](#v202410-caramello).
## `v2024.10-caramello`
- [Release binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2024.10-caramello)
@@ -118,12 +143,12 @@ The client was removed from the peer list after 3 days of inactivity. Upon re-co
- [Handle clients with different versions in IPR](https://github.com/nymtech/nym/pull/4723): Allow the IPR to handle clients connecting both using `v6` and `v7`, independently. The motivation is that we want to be able to roll out a API version change gradually for VPN clients without breaking backwards compatibility. The main feature on the new `v7` format that is not yet used, is that it adds signatures for connect/disconnect.
~~~admonish example collapsible=true title='Testing steps performed'
Run the same command (using same gateways deployed from this PR) on different versions of the `nym-vpn-cli`.
Run the same command (using same gateways deployed from this PR) on different versions of the `nym-vpn-cli`.
Example:
Example:
```sh
~/nym-vpn-core-v0.1.10_macos_universal sudo -E ./nym-vpn-cli -c ../qa.env run --entry-gateway-id $entry_gateway --exit-gateway-id $exit_gateway --enable-two-hop
~/nym-vpn-core-v0.1.11-dev_macos_universal sudo -E ./nym-vpn-cli -c ../qa.env run --entry-gateway-id $entry_gateway --exit-gateway-id $exit_gateway --enable-two-hop
```
~~~
@@ -164,11 +189,11 @@ Example:
- [Feature/vesting purge plus ranged cost params](https://github.com/nymtech/nym/pull/4716): Combines [\#4715](https://github.com/nymtech/nym/pull/4715) and [\#4711](https://github.com/nymtech/nym/pull/4711) into one.
- Disables all non-essential operations on the vesting contract => you can no longer bond mixnodes/make delegations/etc. (you can still, however, withdraw your vested tokens and so on)
- Introduces admin-controlled minimum (and maximum) profit margin and interval operating costs.
- Introduces admin-controlled minimum (and maximum) profit margin and interval operating costs.
- both contracts have to be migrated **at the same time**. ideally within the same transaction
- mixnet contract migration is not allowed (and will fail) if there are any pending actions involving vesting tokens, like delegating, increasing pledge, etc
- [Bump braces from `3.0.2` to `3.0.3` in `/nym-wallet/webdriver`](https://github.com/nymtech/nym/pull/4709): Bumps [braces](https://github.com/micromatch/braces) from `3.0.2` to `3.0.3`.
- [Bump braces from `3.0.2` to `3.0.3` in `/nym-wallet/webdriver`](https://github.com/nymtech/nym/pull/4709): Bumps [braces](https://github.com/micromatch/braces) from `3.0.2` to `3.0.3`.
### Bugfix
@@ -176,29 +201,29 @@ Example:
~~~admonish example collapsible=true title='Testing steps performed'
- Building all binaries is ok
- Running `cargo fmt` returns no issues
- Running `cargo fmt` returns no issues
~~~
- [Fix version 1 not having template correspondent initially](https://github.com/nymtech/nym/pull/4733)
~~~admonish example collapsible=true title='Testing steps performed'
Tested updating an old `nym-node` version and ensuring it did not throw any errors.
Tested updating an old `nym-node` version and ensuring it did not throw any errors.
~~~
- [Bugfix/client registration vol2](https://github.com/nymtech/nym/pull/4856)
- [Fix tokio error in `1.39`](https://github.com/nymtech/nym/pull/4730):
- Bump tokio to `1.39.2`, skipping the issue with `1.39.1`
- [Fix (some) feature unification build failures](https://github.com/nymtech/nym/pull/4681): Running a script in the root workspace that builds each crate independently
~~~admonish example collapsible=true title='The script'
```sh
#!/bin/bash
packages=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages[].name')
# Loop through each package and build
for package in $packages; do
echo "Building $package"
@@ -211,16 +236,16 @@ Tested updating an old `nym-node` version and ensuring it did not throw any erro
done
```
~~~
- [bugfix: make sure DKG parses data out of events if logs are empty](https://github.com/nymtech/nym/pull/4764): This will be the case on post `0.50` chains
- [Fix clippy on rustc beta toolchain](https://github.com/nymtech/nym/pull/4746): Fix clippy warnings for rust beta toolchain
- [Fix clippy for beta toolchain](https://github.com/nymtech/nym/pull/4742): Fix beta toolchain clippy by removing unused module
- Add `nym-` prefix to `serde-common` crate
- Remove ignored `default-features = false` attribute for workspace dependency
### Crypto
### Crypto
- [Feature Compact Ecash : The One PR](https://github.com/nymtech/nym/pull/4623)
@@ -249,7 +274,7 @@ Tested updating an old `nym-node` version and ensuring it did not throw any erro
- Wireguard registration and complete handshake test, to configure see [tasklist below](#operators-tasks)
- DNS resolution check, to configure see [tasklist below](#operators-tasks)
- Wireguard perfomance > 0.75, to configure see [tasklist below](#operators-tasks)
- New [Nym Wallet](https://github.com/nymtech/nym/releases/tag/nym-wallet-v1.2.14) is out!
- Vesting contract functionalities have been purged, users can only remove tokens from vesting
- Migrating from `mixnode` or `gateway` smart contracts to a new unifying `nym-node` smart contract will be available soon using Nym desktop wallet, just like you are used to for bonding and node settings. After this migration all `nym-nodes` will be able to receive delegation and rewards. We will share a step by step guide once this migration will be deployed. No action needed now.
@@ -353,7 +378,7 @@ THANK YOU!
- New `nym-nodes` without a performance 24h history above 50% don't show routing properly on `nym-gateway-probe`, on Nym Harbourmaster the page may appear blank - we are working on a fix.
- Wireguard works on IPv4 only for the time being, we are working on IPv6 implementation.
- Harbourmaster *Role* column shows `nym-node --mode exit-gateway` as `EntryGateway`, we are working to fix it.
- In rare occassions Harbourmaster shows only *"panda"* without the *"smiley"* badge even for nodes, which have T&C's accepted. We are working to fix it.
- In rare occassions Harbourmaster shows only *"panda"* without the *"smiley"* badge even for nodes, which have T&C's accepted. We are working to fix it.
- Sometimes `nym-node` running with `--wireguard-enabled true` gives this error on restart: `Serialized netlink packet .. larger than maximum size ..`
```sh
/home/ubuntu/.cargo/registry/src/index.crates.io-6f17d22bba15001f/defguard_wireguard_rs-0.4.2/src/netlink.rs:155: Serialized netlink packet (23240 bytes) larger than maximum size 12288: NetlinkMessage.
@@ -419,42 +444,42 @@ From what we found out it seems that one of our [dependencies - `DefGuard` - is
**Notes:**
Bandwidth reset to 1GB for all clients at midnight. Logs and reports correctly showed the reset.
~~~
~~~admonish example collapsible=true title='Testing steps performed - Scenario 3: Bandwidth Reset at a Different Time (e.g., Midday)'
1. Configure the system to reset bandwidth at midday.
2. Use the client and monitored bandwidth until midday.
3. Keep the client connected during the reset time.
4. Verify that bandwidth reset to 1GB live at midday.
**Notes:**
Bandwidth reset to 1GB at midday while the client was connected. Logs and reports correctly reflected the reset.
~~~
* [Handle clients with different versions in IPR](https://github.com/nymtech/nym/pull/4723): Allow the IPR to handle clients connecting both using `v6` and `v7`, independently. The motivation is that we want to be able to roll out an API version change gradually for NymVPN clients without breaking backwards compatibility. The main feature on the new `v7` format that is not yet used, is that it adds signatures for connect/disconnect.
~~~admonish example collapsible=true title='Testing steps performed'
Run the same command (using same gateways deployed from this PR) on different versions of the `nym-vpn-cli`.
Run the same command (using same gateways deployed from this PR) on different versions of the `nym-vpn-cli`.
Example:
Example:
```sh
sudo -E ./nym-vpn-cli -c ../qa.env run --entry-gateway-id $entry_gateway --exit-gateway-id $exit_gateway --enable-two-hop
sudo -E ./nym-vpn-cli -c ../qa.env run --entry-gateway-id $entry_gateway --exit-gateway-id $exit_gateway --enable-two-hop
```
~~~
### Bugfix
* [Feature/merge back](https://github.com/nymtech/nym/pull/4710): Merge back from the release branch the changes that fix the `nym-node` upgrades.
* [Feature/merge back](https://github.com/nymtech/nym/pull/4710): Merge back from the release branch the changes that fix the `nym-node` upgrades.
* [Fix version `1.x.x` not having template correspondent initially](https://github.com/nymtech/nym/pull/4733): This should fix the problem of config deserialisation when operators upgrade nodes and skip over multiple versions.
* [Fix version `1.x.x` not having template correspondent initially](https://github.com/nymtech/nym/pull/4733): This should fix the problem of config deserialisation when operators upgrade nodes and skip over multiple versions.
~~~admonish example collapsible=true title='Testing steps performed'
- Tested updating an old nym-node version and ensuring it did not throw any errors.
~~~
* [chore: fix 1.80 lint issues](https://github.com/nymtech/nym/pull/4731):
* [chore: fix 1.80 lint issues](https://github.com/nymtech/nym/pull/4731):
~~~admonish example collapsible=true title='Testing steps performed'
- Building all binaries is ok
- Running `cargo fmt` returns no issues
- Running `cargo fmt` returns no issues
~~~
### Operators Guide updates
@@ -496,7 +521,7 @@ ufw allow 1789/tcp # Nym specific
ufw allow 1790/tcp # Nym specific
ufw allow 8080/tcp # Nym specific - nym-node-api
ufw allow 9000/tcp # Nym Specific - clients port
ufw allow 9001/tcp # Nym specific - wss port
ufw allow 9001/tcp # Nym specific - wss port
ufw allow 51822/udp # WireGuard
```
@@ -592,7 +617,7 @@ For Entry Gateway:
```
~~~
* [Update Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt): Based on the survey, AMA and following discussions we added several ports to Nym exit policy. The ports voted upon in the [forum governance](https://forum.nymtech.net/t/poll-a-new-nym-exit-policy-for-exit-gateways-and-the-nym-mixnet-is-inbound/464) have not been added yet due to the concerns raised. These ports were unrestricted:
* [Update Nym exit policy](https://nymtech.net/.wellknown/network-requester/exit-policy.txt): Based on the survey, AMA and following discussions we added several ports to Nym exit policy. The ports voted upon in the [forum governance](https://forum.nymtech.net/t/poll-a-new-nym-exit-policy-for-exit-gateways-and-the-nym-mixnet-is-inbound/464) have not been added yet due to the concerns raised. These ports were unrestricted:
~~~admonish example collapsible=true title='Newly opened ports in Nym exit policy'
```
@@ -625,7 +650,7 @@ For Entry Gateway:
* [Create a NymConnect archive page](https://nymtech.net/developers/archive/nym-connect.html), PR [\#4750](https://github.com/nymtech/nym/commit/5096c1e60e203dcf8be934823946e24fda16a9a3): Archive deprecated NymConnect for backward compatibility, show PEApps examples for both NC and maintained `nym-socks5-client`.
* Fix broken URLs and correct redirection. PRs: [\#4745](https://github.com/nymtech/nym/commit/7e36595d8fa7706876880b42df1c998a4b8c1478), [\#4752](https://github.com/nymtech/nym/commit/1db61f800c6884e284c5ab21e7abce3bc6d91d99) [\#4755](https://github.com/nymtech/nym/commit/aaf3dca5b999ad7f19d2ff170078b43c9c4476c2), [\#4737](https://github.com/nymtech/nym/commit/6f669866e92e637772726ad05caa5c5501a830f3)
* Fix broken URLs and correct redirection. PRs: [\#4745](https://github.com/nymtech/nym/commit/7e36595d8fa7706876880b42df1c998a4b8c1478), [\#4752](https://github.com/nymtech/nym/commit/1db61f800c6884e284c5ab21e7abce3bc6d91d99) [\#4755](https://github.com/nymtech/nym/commit/aaf3dca5b999ad7f19d2ff170078b43c9c4476c2), [\#4737](https://github.com/nymtech/nym/commit/6f669866e92e637772726ad05caa5c5501a830f3)
~~~admonish example collapsible=true title='Testing steps performed'
- Use [deadlinkchecker.com](https://www.deadlinkchecker.com/website-dead-link-checker.asp) to go over `nymtech.net` and correct all docs URLs
- Go over search engines and old medium articles and check that all dead URLs re-directing correctly
@@ -706,8 +731,8 @@ bind_address = '0.0.0.0:9000'
3. Checked the implementation of the `default_node_role` function to ensure it returns `NodeRole::Inactive`.
4. Ran the updated code in the sandbox environment.
5. Monitored the sandbox environment for any issues or errors related to the changes.
**Notes (if any):**
The test was successful. No issues were flagged during the testing in the sandbox environment. The new default value for `NodeRole` ensures backward compatibility without causing disruptions.
~~~
@@ -755,7 +780,7 @@ bind_address = '0.0.0.0:9000'
6. Validated that no leftover code from the old statistics service provider caused any issues.
~~~
* [Remove `UserAgent` constructor since it's weakly typed](https://github.com/nymtech/nym/pull/4689):
* [Remove `UserAgent` constructor since it's weakly typed](https://github.com/nymtech/nym/pull/4689):
~~~admonish example collapsible=true title='Testing steps performed'
1. Reviewed the changes in `common/http-api-client/src/user_agent.rs` file.
2. Verified the removal of the `UserAgent` constructor and ensured that all instances of `UserAgent::new` are updated accordingly.
@@ -763,14 +788,14 @@ bind_address = '0.0.0.0:9000'
4. Deployed the updated code across different environments (QA, sandbox, and canary).
5. Ran tests to ensure that the `UserAgent` struct functions correctly without the constructor.
~~~
* [Add mixnodes to self describing api cache](https://github.com/nymtech/nym/pull/4684):
- Abstracts getting the self describing info a bit
- Adds mixnodes to the cache refresher as well
- Adds `role` field to the `NodeDescription` struct, to be able to distinguish between mixnodes and gateways
- Switched to using `NodeStatusCache` instead of `ContractCache`
~~~admonish example collapsible=true title='Testing steps performed'
Called the new `/mixnodes/described` endpoint as well as the existing `/gateways/described` endpoint and verified that the data returned for each was correct based on the settings that different nodes have when they are setup.
Called the new `/mixnodes/described` endpoint as well as the existing `/gateways/described` endpoint and verified that the data returned for each was correct based on the settings that different nodes have when they are setup.
For gateway endpoint, the “role” for now does not differentiate between entry and exit gateways, this will be implemented in the future.
~~~
@@ -806,7 +831,7 @@ For gateway endpoint, the “role” for now does not differentiate between entr
5. Ran all tests to ensure the removal did not affect the functionality of the project.
~~~
* [Remove code that refers to removed `nym-network-statistics`](https://github.com/nymtech/nym/pull/4679): Follow up to [\#4678](https://github.com/nymtech/nym/pull/4678) where all code interacting with it is removed.
~~~admonish example collapsible=true title='Testing steps performed'
1. Reviewed the project to identify all references to `nym-network-statistics`.
@@ -823,7 +848,7 @@ For gateway endpoint, the “role” for now does not differentiate between entr
- `nym-client`
- `nym-socks5-client`
- Standalone `ip-packet-router`
~~~admonish example collapsible=true title='Testing steps performed'
Used the nym-vpn-cli to test this, and we can visibly see the `UserAgent`, no issues with the comments mentioned above.
@@ -831,7 +856,7 @@ Example of the user agent sent:
`nym-client/1.1.36/x86_64-unknown-linux-gnu/e18bb70`
<img width="1435" alt="image" src="https://github.com/nymtech/nym/assets/60836166/5d4cc76f-84e6-45cb-9102-adc2b58a25d9">
Connected with no problems
~~~
@@ -420,13 +420,13 @@ Now your `nginx` should be configured, up and running. Test it by inserting your
## Web Secure Socket Setup
This section assumes that you have already configured a reverse proxy and have set it up to work over https. If not, head over to [the reverse proxy section](#reverse-proxy-configuration) to configure it.
We strongly recommend node operators to configure secure web sockets on their nodes. This will provide clients a more secure way to connect to your node.
You can read more about *Secure Socket Layer* (SSL) in [here](https://www.geeksforgeeks.org/secure-socket-layer-ssl/).
If you haven't configured reverse proxy before, start with [*Preliminary steps* chapter](#preliminary-steps) and only then move to WSS setup. If you have reverse proxy already running and your `nym-node` can be reached via https, you can skip *Preliminary steps* and begin to setup WSS directly.
Remember that there may be some unique variables and customization depending on the way your reverse proxy is setup which you may have to adjust when configuring WSS to ensure correct functionality
```admonish tip
@@ -496,7 +496,7 @@ server {
```
~~~
4. Activate the configuration by creating a simlink to `/etc/nginx/sites-enabled`:
4. Activate the configuration by creating a symlink to `/etc/nginx/sites-enabled`:
```sh
ln -s /etc/nginx/sites-available/wss-config-nym /etc/nginx/sites-enabled
```
@@ -512,7 +512,11 @@ systemctl restart nginx
```
7. Finally, configure your `nym-node` to announce the port you have setup. This is done by opening your node configuration file located at `~/.nym/nym-nodes/<ID>/config/config.toml` and changing the value of the line `announce_wss_port` in the `[entry_gateway]` section:
7. Finally, configure your `nym-node` to announce the port and hostname you have setup.
This is done by opening your node configuration file located at `~/.nym/nym-nodes/<ID>/config/config.toml` and changing the following values :
- `announce_wss_port` in the `[entry_gateway]` section:
```
announce_wss_port = <WSS_PORT>
@@ -521,6 +525,12 @@ announce_wss_port = <WSS_PORT>
# announce_wss_port = 9001
```
- `hostname` in the `[host]` section:
```
hostname = '<HOSTNAME>'
```
8. Restart your `nym-node` :
```sh
systemctl restart nym-node
@@ -20,7 +20,8 @@ use nym_credential_verification::{
};
use nym_gateway_requests::{
types::{BinaryRequest, ServerResponse},
ClientControlRequest, GatewayRequestsError, SimpleGatewayRequestsError,
ClientControlRequest, ClientRequest, GatewayRequestsError, SensitiveServerResponse,
SimpleGatewayRequestsError,
};
use nym_gateway_storage::{error::StorageError, Storage};
use nym_sphinx::forwarding::packet::MixPacket;
@@ -43,9 +44,21 @@ pub enum RequestHandlingError {
)]
MissingClientBandwidthEntry { client_address: String },
#[error("received a binary request of an unknown type")]
UnknownBinaryRequest,
#[error("received a text request of an unknown type")]
UnknownTextRequest,
#[error("received an encrypted text request of an unknown type")]
UnknownEncryptedTextRequest,
#[error("Provided binary request was malformed - {0}")]
InvalidBinaryRequest(#[from] GatewayRequestsError),
#[error("failed to decrypt provided text request")]
InvalidEncryptedTextRequest,
#[error("Provided binary request was malformed - {0}")]
InvalidTextRequest(<ClientControlRequest as TryFrom<String>>::Error),
@@ -115,6 +128,12 @@ impl IntoWSMessage for Result<ServerResponse, RequestHandlingError> {
}
}
impl IntoWSMessage for ServerResponse {
fn into_ws_message(self) -> Message {
self.into()
}
}
pub(crate) struct AuthenticatedHandler<R, S, St> {
inner: FreshHandler<R, S, St>,
bandwidth_storage_manager: BandwidthStorageManager<St>,
@@ -280,14 +299,64 @@ where
}
Ok(request) => match request {
// currently only a single type exists
BinaryRequest::ForwardSphinx(mix_packet) => self
.handle_forward_sphinx(mix_packet)
.await
.into_ws_message(),
BinaryRequest::ForwardSphinx { packet } => {
self.handle_forward_sphinx(packet).await.into_ws_message()
}
_ => RequestHandlingError::UnknownBinaryRequest.into_error_message(),
},
}
}
async fn handle_key_upgrade(
&mut self,
hkdf_salt: Vec<u8>,
client_key_digest: Vec<u8>,
) -> Result<ServerResponse, RequestHandlingError> {
if !self.client.shared_keys.is_legacy() {
return Ok(ServerResponse::new_error(
"the connection is already using an aes256-gcm-siv key",
));
}
let legacy_key = self.client.shared_keys.unwrap_legacy();
let Some(upgraded_key) = legacy_key.upgrade_verify(&hkdf_salt, &client_key_digest) else {
return Ok(ServerResponse::new_error(
"failed to derive matching aes256-gcm-siv key",
));
};
let updated_key = upgraded_key.into();
self.inner
.shared_state
.storage
.insert_shared_keys(self.client.address, &updated_key)
.await?;
// swap the in-memory key
self.client.shared_keys = updated_key;
Ok(SensitiveServerResponse::KeyUpgradeAck {}.encrypt(&self.client.shared_keys)?)
}
async fn handle_encrypted_text_request(
&mut self,
ciphertext: Vec<u8>,
nonce: Vec<u8>,
) -> Message {
let Ok(req) = ClientRequest::decrypt(&ciphertext, &nonce, &self.client.shared_keys) else {
return RequestHandlingError::InvalidEncryptedTextRequest.into_error_message();
};
match req {
ClientRequest::UpgradeKey {
hkdf_salt,
derived_key_digest,
} => self
.handle_key_upgrade(hkdf_salt, derived_key_digest)
.await
.into_ws_message(),
_ => RequestHandlingError::UnknownEncryptedTextRequest.into_error_message(),
}
}
/// Attempts to handle a text data frame websocket message.
///
/// Currently the bandwidth credential request is the only one we can receive after authentication.
@@ -300,6 +369,9 @@ where
match ClientControlRequest::try_from(raw_request) {
Err(e) => RequestHandlingError::InvalidTextRequest(e).into_error_message(),
Ok(request) => match request {
ClientControlRequest::EncryptedRequest { ciphertext, nonce } => {
self.handle_encrypted_text_request(ciphertext, nonce).await
}
ClientControlRequest::EcashCredential { enc_credential, iv } => self
.handle_ecash_bandwidth(enc_credential, iv)
.await
@@ -322,13 +394,29 @@ where
.await
.map_err(|e| e.into())
.into_ws_message(),
other => RequestHandlingError::IllegalRequest {
additional_context: format!(
"received illegal message of type {} in an authenticated client",
other.name()
),
ClientControlRequest::SupportedProtocol { .. } => self
.inner
.handle_supported_protocol_request()
.into_ws_message(),
other @ ClientControlRequest::Authenticate { .. } => {
RequestHandlingError::IllegalRequest {
additional_context: format!(
"received illegal message of type {} in an authenticated client",
other.name()
),
}
.into_error_message()
}
.into_error_message(),
other @ ClientControlRequest::RegisterHandshakeInitRequest { .. } => {
RequestHandlingError::IllegalRequest {
additional_context: format!(
"received illegal message of type {} in an authenticated client",
other.name()
),
}
.into_error_message()
}
_ => RequestHandlingError::UnknownTextRequest.into_error_message(),
},
}
}

Some files were not shown because too many files have changed in this diff Show More