Compare commits

..

76 Commits

Author SHA1 Message Date
Jędrzej Stuczyński dce0d161ea replace logger 2024-10-15 17:11:21 +01:00
Jędrzej Stuczyński 853a62bc5b added debug symbols to release builds 2024-10-14 18:53:09 +01:00
Jędrzej Stuczyński 1927614803 added temporary and experimental memory profiling 2024-10-11 14:34:28 +01:00
Jędrzej Stuczyński 75a5192c6d Merge pull request #4958 from nymtech/bugfix/websocket-message-handling
bugfix: replace unreachable macro with an error return
2024-10-09 17:24:17 +01:00
Jędrzej Stuczyński 25ad0920cf bugfix: replace unreachable macro with an error return 2024-10-09 17:15:41 +01:00
Bogdan-Ștefan Neacşu a4c6f51fe0 Don't kill gateway on handle drop (#4934) 2024-09-27 11:02:39 +02:00
benedetta davico f76300669a Merge pull request #4931 from nymtech/feature/wedel-merge-conflicts
Wedel release to develop
2024-09-26 13:46:24 +02:00
Jędrzej Stuczyński 333ace1f97 Merge branch 'release/2024.11-wedel' into feature/wedel-merge-conflicts 2024-09-26 08:56:11 +01:00
Dinko Zdravac 487bf6732e Assume offline mode in sqlx (#4926)
* Assume offline mode

* PR feedback
2024-09-25 13:28:36 +02:00
Jędrzej Stuczyński 5d4a0fef55 Merge pull request #4871 from nymtech/chore/remove-another-mixnet-migration
chore: remove queued migration for adding explicit admin
2024-09-25 09:37:49 +01:00
Jon Häggblad 1627146c0e Make ip-packet-request VERSION pub (#4925) 2024-09-25 09:56:32 +02:00
Dinko Zdravac ae40a00b8f Data Observatory stub (#4905)
* Data Observatory stub

* Fix sqlx in CI

* Add troubleshooting tips for sqlx

* Update CI paths to trigger for this package

* Add this to CI upload binary build
2024-09-24 16:48:15 +02:00
Jon Häggblad 7f3c0470e0 Fix argument to cargo-deny action (#4922) 2024-09-24 13:17:35 +02:00
Bogdan-Ștefan Neacşu 1bc26ed79f Expose error type (#4924) 2024-09-24 11:48:54 +02:00
mx 60fa5cfeb8 Max/rust sdk stream abstraction (#4743)
* add TcpProxyClient and TcpProxyServer abstractions to SDK 
* add single connection example
* add multi-connection example 
* add simple echo server to `tools/`: used for multi-connection example 
* update FFI toml files: switched to local imports 
* add proxy bindings to `ffi/shared`
* add proxy bindings and example to `ffi/go` 
* add note to `ffi/cpp` about lack of Proxy bindings for the moment
2024-09-24 09:29:46 +00:00
Jon Häggblad 3b7088aeea Fix nymvpn.com url in mainnet defaults (#4920) 2024-09-24 10:25:27 +02:00
Bogdan-Ștefan Neacşu 179d214e21 Check both version and type in message header (#4918)
* Move client type to the client code

* Check both version and type in header
2024-09-23 17:57:03 +02:00
Jon Häggblad 2a94ce6443 Bump http-api-client default timeout to 30 sec (#4917) 2024-09-23 15:45:47 +02:00
Bogdan-Ștefan Neacşu 95ec91daa1 Entry wireguard tickets (#4888)
* Create credential verifier in authenticator

* Add new version of peer storage with client id

* Fix v1 to what it was before

* Compact storage into ecash verifier

* Fix non-linux build

* Less overlapping conditions

* Remove moved code

* Use handler thread for each peer

* Re-spawn stored handles at startup

* Keep new function without async & Result

* Put query peer in function too

* Query bandwidth

* Fix clippy

* Replace tap with inspect_err

* Fix copyright year

* Handle version 2 on the reqeust deser

* Add protocol type in req/resp messages
2024-09-23 14:49:18 +02:00
benedettadavico 803850be74 bump versions & update changelog 2024-09-23 10:00:20 +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 3dc62a9a60 Merge pull request #4892 from nymtech/bugfix/ticketbook-false-double-spending
Bugfix/ticketbook false double spending
2024-09-19 09:44:43 +01: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
Jędrzej Stuczyński 5753b79997 slightly refactored bandwidth tracking 2024-09-18 11:27:35 +01:00
Jędrzej Stuczyński 2a6aa13ecd fixed client bandwidth being not correctly deducted 2024-09-18 11:12:24 +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
benedetta davico d5c9e1d8cb Merge pull request #4899 from nymtech/jon/cherry-pick-4894-into-wedel
Backport #4894 to fix ci
2024-09-18 09:28:15 +02: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
Jon Häggblad 87751894d9 Fix apt install in ci-build-upload-binaries.yml (#4894) 2024-09-18 09:07:45 +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
Jędrzej Stuczyński c8c3928575 put client bandwidth (gateway-side) behind shared pointer 2024-09-17 18:40:24 +01:00
Jędrzej Stuczyński 2fa8da8117 making sure there can be only a single client task claiming more bandwidth 2024-09-17 18:39:49 +01:00
Jędrzej Stuczyński 4548ef4d05 adding extra logs 2024-09-17 18:39:01 +01:00
Jędrzej Stuczyński 7f147ee2b0 Merge pull request #4891 from nymtech/bugfix/ticketbook-aux-imports
fix: allow updating globally stored signatures
2024-09-17 15:33:34 +01:00
Jędrzej Stuczyński 48bcd7e802 fix: allow updating globally stored signatures 2024-09-17 14:21:42 +01: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
benedettadavico aad028be3f update qa env 2024-09-13 11:48:49 +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
Bogdan-Ștefan Neacşu 6db3b34bcb Bump defguard to github latest version (#4872)
* Bump defguard to github latest version

* Fix comment location
2024-09-12 13:49:33 +02:00
Jędrzej Stuczyński f9383578da chore: remove queued migration for adding explicit admin 2024-09-12 11:03:51 +01:00
233 changed files with 9483 additions and 3005 deletions
@@ -26,6 +26,7 @@ on:
- "nym-api/**"
- "nym-node/**"
- "nym-outfox/**"
- 'nym-data-observatory/**'
- "nym-validator-rewarder/**"
- "sdk/rust/nym-sdk/**"
- "service-providers/**"
@@ -56,7 +57,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: |
@@ -96,6 +97,7 @@ jobs:
target/release/nym-socks5-client
target/release/nym-api
target/release/nym-network-requester
target/release/nym-data-observatory
target/release/nym-cli
target/release/nymvisor
target/release/nym-node
@@ -113,6 +115,7 @@ jobs:
cp target/release/nym-socks5-client $OUTPUT_DIR
cp target/release/nym-api $OUTPUT_DIR
cp target/release/nym-network-requester $OUTPUT_DIR
cp target/release/nym-data-observatory $OUTPUT_DIR
cp target/release/nymvisor $OUTPUT_DIR
cp target/release/nym-node $OUTPUT_DIR
cp target/release/nym-cli $OUTPUT_DIR
+1 -8
View File
@@ -16,6 +16,7 @@ on:
- 'nym-api/**'
- 'nym-node/**'
- 'nym-outfox/**'
- 'nym-data-observatory/**'
- 'nym-validator-rewarder/**'
- 'tools/**'
- 'wasm/**'
@@ -90,14 +91,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:
+2 -2
View File
@@ -18,8 +18,8 @@ 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 }}
argument: --all-features
arguments: --all-features
@@ -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
+76
View File
@@ -4,6 +4,82 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
## [Unreleased]
## [2024.11-wedel] (2024-09-23)
- Backport #4894 to fix ci ([#4899])
- Bugfix/ticketbook false double spending ([#4892])
- fix: allow updating globally stored signatures ([#4891])
- [DOCs/operators]: Document changelog for patch/2024.10-caramello ([#4886])
- [DOCs/operators]: Post release docs updates ([#4874])
- Bump defguard to github latest version ([#4872])
- chore: removed completed queued mixnet migration ([#4865])
- Disable push trigger and add missing paths in ci-build ([#4864])
- Fix linux conditional in ci-build.yml ([#4863])
- Remove golang workaround in ci-sdk-wasm ([#4858])
- Revert runner for ci-docs ([#4855])
- Move credential verification into common crate ([#4853])
- Fix test failure in ipr request size ([#4844])
- Start switching over jobs to arc-ubuntu-20.04 ([#4843])
- Use ecash credential type for bandwidth value ([#4840])
- Create nym-repo-setup debian package and nym-vpn meta package ([#4837])
- Remove serde_crate named import ([#4832])
- Run cargo autoinherit following last weeks dependabot updates ([#4831])
- revamped ticketbook serialisation and exposed additional cli methods ([#4827])
- Expose wireguard details on self described endpoint ([#4825])
- Remove unused wireguard flag from SDK ([#4823])
- Add `axum` server to `nym-api` ([#4803])
- Run cargo-autoinherit for a few new crates ([#4801])
- Update dependabot ([#4796])
- Fix clippy for unwrap_or_default ([#4783])
- Enable dependabot version upgrades for root rust workspace ([#4778])
- Persist used wireguard private IPs ([#4771])
- Avoid race on ip and registration structures ([#4766])
- docs/hotfix ([#4765])
- chore: remove repetitive words ([#4763])
- Make gateway latency check generic ([#4759])
- Remove duplicate stat count for retransmissions ([#4756])
- Update peer refresh value ([#4754])
- Remove deprecated mark_as_success and use new disarm ([#4751])
- Add get_mixnodes_described to validator_client ([#4725])
- New Network Monitor ([#4610])
[#4899]: https://github.com/nymtech/nym/pull/4899
[#4892]: https://github.com/nymtech/nym/pull/4892
[#4891]: https://github.com/nymtech/nym/pull/4891
[#4886]: https://github.com/nymtech/nym/pull/4886
[#4874]: https://github.com/nymtech/nym/pull/4874
[#4872]: https://github.com/nymtech/nym/pull/4872
[#4865]: https://github.com/nymtech/nym/pull/4865
[#4864]: https://github.com/nymtech/nym/pull/4864
[#4863]: https://github.com/nymtech/nym/pull/4863
[#4858]: https://github.com/nymtech/nym/pull/4858
[#4855]: https://github.com/nymtech/nym/pull/4855
[#4853]: https://github.com/nymtech/nym/pull/4853
[#4844]: https://github.com/nymtech/nym/pull/4844
[#4843]: https://github.com/nymtech/nym/pull/4843
[#4840]: https://github.com/nymtech/nym/pull/4840
[#4837]: https://github.com/nymtech/nym/pull/4837
[#4832]: https://github.com/nymtech/nym/pull/4832
[#4831]: https://github.com/nymtech/nym/pull/4831
[#4827]: https://github.com/nymtech/nym/pull/4827
[#4825]: https://github.com/nymtech/nym/pull/4825
[#4823]: https://github.com/nymtech/nym/pull/4823
[#4803]: https://github.com/nymtech/nym/pull/4803
[#4801]: https://github.com/nymtech/nym/pull/4801
[#4796]: https://github.com/nymtech/nym/pull/4796
[#4783]: https://github.com/nymtech/nym/pull/4783
[#4778]: https://github.com/nymtech/nym/pull/4778
[#4771]: https://github.com/nymtech/nym/pull/4771
[#4766]: https://github.com/nymtech/nym/pull/4766
[#4765]: https://github.com/nymtech/nym/pull/4765
[#4763]: https://github.com/nymtech/nym/pull/4763
[#4759]: https://github.com/nymtech/nym/pull/4759
[#4756]: https://github.com/nymtech/nym/pull/4756
[#4754]: https://github.com/nymtech/nym/pull/4754
[#4751]: https://github.com/nymtech/nym/pull/4751
[#4725]: https://github.com/nymtech/nym/pull/4725
[#4610]: https://github.com/nymtech/nym/pull/4610
## [2024.10-caramello] (2024-09-10)
- Backport 4844 and 4845 ([#4857])
Generated
+610 -423
View File
File diff suppressed because it is too large Load Diff
+24 -10
View File
@@ -5,6 +5,7 @@
panic = "abort"
opt-level = "s"
overflow-checks = true
debug = true
[profile.dev]
panic = "abort"
@@ -81,6 +82,7 @@ members = [
"common/nyxd-scraper",
"common/pemstore",
"common/serde-helpers",
"common/service-provider-requests-common",
"common/socks5-client-core",
"common/socks5/proxy-helpers",
"common/socks5/requests",
@@ -102,6 +104,9 @@ members = [
"mixnode",
"sdk/lib/socks5-listener",
"sdk/rust/nym-sdk",
"sdk/ffi/shared",
"sdk/ffi/go",
"sdk/ffi/cpp",
"service-providers/authenticator",
"service-providers/common",
"service-providers/ip-packet-router",
@@ -110,11 +115,13 @@ members = [
"nym-api",
"nym-browser-extension/storage",
"nym-api/nym-api-requests",
"nym-data-observatory",
"nym-node",
"nym-node/nym-node-http-api",
"nym-node/nym-node-requests",
"nym-outfox",
"nym-validator-rewarder",
"tools/echo-server",
"tools/internal/ssl-inject",
# "tools/internal/sdk-version-bump",
"tools/internal/testnet-manager",
@@ -129,6 +136,9 @@ members = [
"wasm/mix-fetch",
"wasm/node-tester",
"wasm/zknym-lib",
"tools/internal/testnet-manager",
"tools/internal/testnet-manager/dkg-bypass-contract",
"tools/echo-server",
]
default-members = [
@@ -138,6 +148,7 @@ default-members = [
"gateway",
"mixnode",
"nym-api",
"nym-data-observatory",
"nym-node",
"nym-validator-rewarder",
"service-providers/authenticator",
@@ -152,7 +163,6 @@ exclude = [
"nym-wallet",
"nym-vpn/ui/src-tauri",
"cpu-cycles",
"sdk/ffi/cpp",
]
[workspace.package]
@@ -169,7 +179,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 +210,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"
@@ -210,7 +222,8 @@ ctr = "0.9.1"
cupid = "0.6.1"
curve25519-dalek = "4.1"
dashmap = "5.5.3"
defguard_wireguard_rs = "0.4.2"
# We want https://github.com/DefGuard/wireguard-rs/pull/64 , but there's no crates.io release being pushed out anymore
defguard_wireguard_rs = { git = "https://github.com/DefGuard/wireguard-rs.git", rev = "v0.4.7" }
digest = "0.10.7"
dirs = "5.0"
doc-comment = "0.3"
@@ -224,7 +237,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 +245,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 +289,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 +306,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 +315,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 +329,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 +349,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
@@ -1,6 +1,6 @@
[package]
name = "nym-client"
version = "1.1.40"
version = "1.1.41"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
description = "Implementation of the Nym Client"
edition = "2021"
+1 -1
View File
@@ -1,6 +1,6 @@
[package]
name = "nym-socks5-client"
version = "1.1.40"
version = "1.1.41"
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
edition = "2021"
+15
View File
@@ -9,9 +9,24 @@ edition.workspace = true
license.workspace = true
[dependencies]
base64 = { workspace = true }
bincode = { workspace = true }
rand = { workspace = true }
serde = { workspace = true, features = ["derive"] }
thiserror = { workspace = true }
nym-credentials-interface = { path = "../credentials-interface" }
nym-crypto = { path = "../crypto", features = ["asymmetric"] }
nym-service-provider-requests-common = { path = "../service-provider-requests-common" }
nym-sphinx = { path = "../nymsphinx" }
nym-wireguard-types = { path = "../wireguard-types" }
## verify:
hmac = { workspace = true, optional = true }
sha2 = { workspace = true, optional = true }
x25519-dalek = { workspace = true, features = ["static_secrets"] }
[features]
default = ["verify"]
# this is moved to a separate feature as we really need clients to import it (especially, *cough*, wasm)
verify = ["hmac", "sha2"]
@@ -0,0 +1,22 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use thiserror::Error;
#[derive(Debug, Error)]
pub enum Error {
#[error("the provided base64-encoded client MAC ('{mac}') was malformed: {source}")]
MalformedClientMac {
mac: String,
#[source]
source: base64::DecodeError,
},
#[cfg(feature = "verify")]
#[error("failed to verify mac provided by '{client}': {source}")]
FailedClientMacVerification {
client: String,
#[source]
source: hmac::digest::MacError,
},
}
+7 -1
View File
@@ -2,8 +2,14 @@
// SPDX-License-Identifier: Apache-2.0
pub mod v1;
pub mod v2;
pub const CURRENT_VERSION: u8 = 1;
mod error;
pub use error::Error;
pub use v2 as latest;
pub const CURRENT_VERSION: u8 = 2;
fn make_bincode_serializer() -> impl bincode::Options {
use bincode::Options;
@@ -1,7 +1,13 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod registration;
pub mod request;
pub mod response;
pub use registration::{ClientMac, GatewayClient, InitMessage, Nonce};
#[cfg(feature = "verify")]
pub use registration::HmacSha256;
const VERSION: u8 = 1;
@@ -0,0 +1,218 @@
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::Error;
use base64::{engine::general_purpose, Engine};
use nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::IpAddr;
use std::time::SystemTime;
use std::{fmt, ops::Deref, str::FromStr};
#[cfg(feature = "verify")]
use hmac::{Hmac, Mac};
#[cfg(feature = "verify")]
use nym_crypto::asymmetric::encryption::PrivateKey;
#[cfg(feature = "verify")]
use sha2::Sha256;
pub type PendingRegistrations = HashMap<PeerPublicKey, RegistrationData>;
pub type PrivateIPs = HashMap<IpAddr, Taken>;
#[cfg(feature = "verify")]
pub type HmacSha256 = Hmac<Sha256>;
pub type Nonce = u64;
pub type Taken = Option<SystemTime>;
pub const BANDWIDTH_CAP_PER_DAY: i64 = 1024 * 1024 * 1024; // 1 GB
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct InitMessage {
/// Base64 encoded x25519 public key
pub pub_key: PeerPublicKey,
}
impl InitMessage {
pub fn new(pub_key: PeerPublicKey) -> Self {
InitMessage { pub_key }
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RegistrationData {
pub nonce: u64,
pub gateway_data: GatewayClient,
pub wg_port: u16,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RegistredData {
pub pub_key: PeerPublicKey,
pub private_ip: IpAddr,
pub wg_port: u16,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RemainingBandwidthData {
pub available_bandwidth: u64,
pub suspended: bool,
}
/// Client that wants to register sends its PublicKey bytes mac digest encrypted with a DH shared secret.
/// Gateway/Nym node can then verify pub_key payload using the same process
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct GatewayClient {
/// Base64 encoded x25519 public key
pub pub_key: PeerPublicKey,
/// Assigned private IP
pub private_ip: IpAddr,
/// Sha256 hmac on the data (alongside the prior nonce)
pub mac: ClientMac,
}
impl GatewayClient {
#[cfg(feature = "verify")]
pub fn new(
local_secret: &PrivateKey,
remote_public: x25519_dalek::PublicKey,
private_ip: IpAddr,
nonce: u64,
) -> Self {
// convert from 1.0 x25519-dalek private key into 2.0 x25519-dalek
#[allow(clippy::expect_used)]
let static_secret = x25519_dalek::StaticSecret::from(local_secret.to_bytes());
let local_public: x25519_dalek::PublicKey = (&static_secret).into();
let dh = static_secret.diffie_hellman(&remote_public);
// TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)]
let mut mac = HmacSha256::new_from_slice(dh.as_bytes())
.expect("x25519 shared secret is always 32 bytes long");
mac.update(local_public.as_bytes());
mac.update(private_ip.to_string().as_bytes());
mac.update(&nonce.to_le_bytes());
GatewayClient {
pub_key: PeerPublicKey::new(local_public),
private_ip,
mac: ClientMac(mac.finalize().into_bytes().to_vec()),
}
}
// Reusable secret should be gateways Wireguard PK
// Client should perform this step when generating its payload, using its own WG PK
#[cfg(feature = "verify")]
pub fn verify(&self, gateway_key: &PrivateKey, nonce: u64) -> Result<(), Error> {
// convert from 1.0 x25519-dalek private key into 2.0 x25519-dalek
#[allow(clippy::expect_used)]
let static_secret = x25519_dalek::StaticSecret::from(gateway_key.to_bytes());
let dh = static_secret.diffie_hellman(&self.pub_key);
// TODO: change that to use our nym_crypto::hmac module instead
#[allow(clippy::expect_used)]
let mut mac = HmacSha256::new_from_slice(dh.as_bytes())
.expect("x25519 shared secret is always 32 bytes long");
mac.update(self.pub_key.as_bytes());
mac.update(self.private_ip.to_string().as_bytes());
mac.update(&nonce.to_le_bytes());
mac.verify_slice(&self.mac)
.map_err(|source| Error::FailedClientMacVerification {
client: self.pub_key.to_string(),
source,
})
}
pub fn pub_key(&self) -> PeerPublicKey {
self.pub_key
}
}
// TODO: change the inner type into generic array of size HmacSha256::OutputSize
// TODO2: rely on our internal crypto/hmac
#[derive(Debug, Clone)]
pub struct ClientMac(Vec<u8>);
impl fmt::Display for ClientMac {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", general_purpose::STANDARD.encode(&self.0))
}
}
impl ClientMac {
#[allow(dead_code)]
pub fn new(mac: Vec<u8>) -> Self {
ClientMac(mac)
}
}
impl Deref for ClientMac {
type Target = Vec<u8>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl FromStr for ClientMac {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mac_bytes: Vec<u8> =
general_purpose::STANDARD
.decode(s)
.map_err(|source| Error::MalformedClientMac {
mac: s.to_string(),
source,
})?;
Ok(ClientMac(mac_bytes))
}
}
impl Serialize for ClientMac {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let encoded_key = general_purpose::STANDARD.encode(self.0.clone());
serializer.serialize_str(&encoded_key)
}
}
impl<'de> Deserialize<'de> for ClientMac {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let encoded_key = String::deserialize(deserializer)?;
ClientMac::from_str(&encoded_key).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
use nym_crypto::asymmetric::encryption;
#[test]
#[cfg(feature = "verify")]
fn client_request_roundtrip() {
let mut rng = rand::thread_rng();
let gateway_key_pair = encryption::KeyPair::new(&mut rng);
let client_key_pair = encryption::KeyPair::new(&mut rng);
let nonce = 1234567890;
let client = GatewayClient::new(
client_key_pair.private_key(),
x25519_dalek::PublicKey::from(gateway_key_pair.public_key().to_bytes()),
"10.0.0.42".parse().unwrap(),
nonce,
);
assert!(client.verify(gateway_key_pair.private_key(), nonce).is_ok())
}
}
@@ -1,8 +1,9 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::registration::{GatewayClient, InitMessage};
use nym_sphinx::addressing::Recipient;
use nym_wireguard_types::{GatewayClient, InitMessage, PeerPublicKey};
use nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize};
use crate::make_bincode_serializer;
@@ -82,3 +83,24 @@ pub enum AuthenticatorRequestData {
Final(GatewayClient),
QueryBandwidth(PeerPublicKey),
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn check_first_byte_version() {
let version = 2;
let data = AuthenticatorRequest {
version,
data: AuthenticatorRequestData::Initial(InitMessage::new(
PeerPublicKey::from_str("yvNUDpT5l7W/xDhiu6HkqTHDQwbs/B3J5UrLmORl1EQ=").unwrap(),
)),
reply_to: Recipient::try_from_base58_string("D1rrpsysCGCYXy9saP8y3kmNpGtJZUXN9SvFoUcqAsM9.9Ssso1ea5NfkbMASdiseDSjTN1fSWda5SgEVjdSN4CvV@GJqd3ZxpXWSNxTfx7B1pPtswpetH4LnJdFeLeuY5KUuN").unwrap(),
request_id: 1,
};
let bytes = data.to_bytes().unwrap();
assert_eq!(*bytes.first().unwrap(), version);
}
}
@@ -1,8 +1,8 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData};
use nym_sphinx::addressing::Recipient;
use nym_wireguard_types::registration::{RegistrationData, RegistredData, RemainingBandwidthData};
use serde::{Deserialize, Serialize};
use crate::make_bincode_serializer;
@@ -0,0 +1,69 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use crate::{v1, v2};
impl From<v1::request::AuthenticatorRequest> for v2::request::AuthenticatorRequest {
fn from(authenticator_request: v1::request::AuthenticatorRequest) -> Self {
Self {
protocol: Protocol {
version: 2,
service_provider_type: ServiceProviderType::Authenticator,
},
data: authenticator_request.data.into(),
reply_to: authenticator_request.reply_to,
request_id: authenticator_request.request_id,
}
}
}
impl From<v1::request::AuthenticatorRequestData> for v2::request::AuthenticatorRequestData {
fn from(authenticator_request_data: v1::request::AuthenticatorRequestData) -> Self {
match authenticator_request_data {
v1::request::AuthenticatorRequestData::Initial(init_msg) => {
v2::request::AuthenticatorRequestData::Initial(init_msg.into())
}
v1::request::AuthenticatorRequestData::Final(gw_client) => {
v2::request::AuthenticatorRequestData::Final(gw_client.into())
}
v1::request::AuthenticatorRequestData::QueryBandwidth(pub_key) => {
v2::request::AuthenticatorRequestData::QueryBandwidth(pub_key)
}
}
}
}
impl From<v1::registration::InitMessage> for v2::registration::InitMessage {
fn from(init_msg: v1::registration::InitMessage) -> Self {
Self {
pub_key: init_msg.pub_key,
}
}
}
impl From<v1::registration::GatewayClient> for Box<v2::registration::FinalMessage> {
fn from(gw_client: v1::registration::GatewayClient) -> Self {
Box::new(v2::registration::FinalMessage {
gateway_client: gw_client.into(),
credential: None,
})
}
}
impl From<v1::registration::GatewayClient> for v2::registration::GatewayClient {
fn from(gw_client: v1::registration::GatewayClient) -> Self {
Self {
pub_key: gw_client.pub_key,
private_ip: gw_client.private_ip,
mac: gw_client.mac.into(),
}
}
}
impl From<v1::registration::ClientMac> for v2::registration::ClientMac {
fn from(mac: v1::registration::ClientMac) -> Self {
Self::new(mac.to_vec())
}
}
@@ -0,0 +1,9 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod conversion;
pub mod registration;
pub mod request;
pub mod response;
const VERSION: u8 = 2;
@@ -1,9 +1,10 @@
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
// Copyright 2023-2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::Error;
use crate::PeerPublicKey;
use base64::{engine::general_purpose, Engine};
use nym_credentials_interface::CredentialSpendingData;
use nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::IpAddr;
@@ -29,32 +30,26 @@ pub type Taken = Option<SystemTime>;
pub const BANDWIDTH_CAP_PER_DAY: u64 = 1024 * 1024 * 1024; // 1 GB
#[derive(Serialize, Deserialize, Debug, Clone)]
#[serde(tag = "type", rename_all = "camelCase")]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub enum ClientMessage {
Initial(InitMessage),
Final(GatewayClient),
Query(PeerPublicKey),
}
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct InitMessage {
/// Base64 encoded x25519 public key
#[cfg_attr(feature = "openapi", schema(value_type = String, format = Byte))]
pub pub_key: PeerPublicKey,
}
impl InitMessage {
pub fn pub_key(&self) -> PeerPublicKey {
self.pub_key
}
pub fn new(pub_key: PeerPublicKey) -> Self {
InitMessage { pub_key }
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct FinalMessage {
/// Gateway client data
pub gateway_client: GatewayClient,
/// Ecash credential
pub credential: Option<CredentialSpendingData>,
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RegistrationData {
pub nonce: u64,
@@ -71,24 +66,20 @@ pub struct RegistredData {
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct RemainingBandwidthData {
pub available_bandwidth: u64,
pub suspended: bool,
pub available_bandwidth: i64,
}
/// Client that wants to register sends its PublicKey bytes mac digest encrypted with a DH shared secret.
/// Gateway/Nym node can then verify pub_key payload using the same process
#[derive(Serialize, Deserialize, Debug, Clone)]
#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
pub struct GatewayClient {
/// Base64 encoded x25519 public key
#[cfg_attr(feature = "openapi", schema(value_type = String, format = Byte))]
pub pub_key: PeerPublicKey,
/// Assigned private IP
pub private_ip: IpAddr,
/// Sha256 hmac on the data (alongside the prior nonce)
#[cfg_attr(feature = "openapi", schema(value_type = String, format = Byte))]
pub mac: ClientMac,
}
@@ -0,0 +1,116 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::registration::{FinalMessage, InitMessage};
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use nym_sphinx::addressing::Recipient;
use nym_wireguard_types::PeerPublicKey;
use serde::{Deserialize, Serialize};
use crate::make_bincode_serializer;
use super::VERSION;
fn generate_random() -> u64 {
use rand::RngCore;
let mut rng = rand::rngs::OsRng;
rng.next_u64()
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AuthenticatorRequest {
pub protocol: Protocol,
pub data: AuthenticatorRequestData,
pub reply_to: Recipient,
pub request_id: u64,
}
impl AuthenticatorRequest {
pub fn from_reconstructed_message(
message: &nym_sphinx::receiver::ReconstructedMessage,
) -> Result<Self, bincode::Error> {
use bincode::Options;
make_bincode_serializer().deserialize(&message.message)
}
pub fn new_initial_request(init_message: InitMessage, reply_to: Recipient) -> (Self, u64) {
let request_id = generate_random();
(
Self {
protocol: Protocol {
service_provider_type: ServiceProviderType::Authenticator,
version: VERSION,
},
data: AuthenticatorRequestData::Initial(init_message),
reply_to,
request_id,
},
request_id,
)
}
pub fn new_final_request(final_message: FinalMessage, reply_to: Recipient) -> (Self, u64) {
let request_id = generate_random();
(
Self {
protocol: Protocol {
service_provider_type: ServiceProviderType::Authenticator,
version: VERSION,
},
data: AuthenticatorRequestData::Final(Box::new(final_message)),
reply_to,
request_id,
},
request_id,
)
}
pub fn new_query_request(peer_public_key: PeerPublicKey, reply_to: Recipient) -> (Self, u64) {
let request_id = generate_random();
(
Self {
protocol: Protocol {
service_provider_type: ServiceProviderType::Authenticator,
version: VERSION,
},
data: AuthenticatorRequestData::QueryBandwidth(peer_public_key),
reply_to,
request_id,
},
request_id,
)
}
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
make_bincode_serializer().serialize(self)
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum AuthenticatorRequestData {
Initial(InitMessage),
Final(Box<FinalMessage>),
QueryBandwidth(PeerPublicKey),
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn check_first_bytes_protocol() {
let version = 2;
let data = AuthenticatorRequest {
protocol: Protocol { version, service_provider_type: ServiceProviderType::Authenticator },
data: AuthenticatorRequestData::Initial(InitMessage::new(
PeerPublicKey::from_str("yvNUDpT5l7W/xDhiu6HkqTHDQwbs/B3J5UrLmORl1EQ=").unwrap(),
)),
reply_to: Recipient::try_from_base58_string("D1rrpsysCGCYXy9saP8y3kmNpGtJZUXN9SvFoUcqAsM9.9Ssso1ea5NfkbMASdiseDSjTN1fSWda5SgEVjdSN4CvV@GJqd3ZxpXWSNxTfx7B1pPtswpetH4LnJdFeLeuY5KUuN").unwrap(),
request_id: 1,
};
let bytes = *data.to_bytes().unwrap().first_chunk::<2>().unwrap();
assert_eq!(bytes, [version, ServiceProviderType::Authenticator as u8]);
}
}
@@ -0,0 +1,129 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::registration::{RegistrationData, RegistredData, RemainingBandwidthData};
use nym_service_provider_requests_common::{Protocol, ServiceProviderType};
use nym_sphinx::addressing::Recipient;
use serde::{Deserialize, Serialize};
use crate::make_bincode_serializer;
use super::VERSION;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AuthenticatorResponse {
pub protocol: Protocol,
pub data: AuthenticatorResponseData,
pub reply_to: Recipient,
}
impl AuthenticatorResponse {
pub fn new_pending_registration_success(
registration_data: RegistrationData,
request_id: u64,
reply_to: Recipient,
) -> Self {
Self {
protocol: Protocol {
service_provider_type: ServiceProviderType::Authenticator,
version: VERSION,
},
data: AuthenticatorResponseData::PendingRegistration(PendingRegistrationResponse {
reply: registration_data,
reply_to,
request_id,
}),
reply_to,
}
}
pub fn new_registered(
registred_data: RegistredData,
reply_to: Recipient,
request_id: u64,
) -> Self {
Self {
protocol: Protocol {
service_provider_type: ServiceProviderType::Authenticator,
version: VERSION,
},
data: AuthenticatorResponseData::Registered(RegisteredResponse {
reply: registred_data,
reply_to,
request_id,
}),
reply_to,
}
}
pub fn new_remaining_bandwidth(
remaining_bandwidth_data: Option<RemainingBandwidthData>,
reply_to: Recipient,
request_id: u64,
) -> Self {
Self {
protocol: Protocol {
service_provider_type: ServiceProviderType::Authenticator,
version: VERSION,
},
data: AuthenticatorResponseData::RemainingBandwidth(RemainingBandwidthResponse {
reply: remaining_bandwidth_data,
reply_to,
request_id,
}),
reply_to,
}
}
pub fn recipient(&self) -> Recipient {
self.reply_to
}
pub fn to_bytes(&self) -> Result<Vec<u8>, bincode::Error> {
use bincode::Options;
make_bincode_serializer().serialize(self)
}
pub fn from_reconstructed_message(
message: &nym_sphinx::receiver::ReconstructedMessage,
) -> Result<Self, bincode::Error> {
use bincode::Options;
make_bincode_serializer().deserialize(&message.message)
}
pub fn id(&self) -> Option<u64> {
match &self.data {
AuthenticatorResponseData::PendingRegistration(response) => Some(response.request_id),
AuthenticatorResponseData::Registered(response) => Some(response.request_id),
AuthenticatorResponseData::RemainingBandwidth(response) => Some(response.request_id),
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum AuthenticatorResponseData {
PendingRegistration(PendingRegistrationResponse),
Registered(RegisteredResponse),
RemainingBandwidth(RemainingBandwidthResponse),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PendingRegistrationResponse {
pub request_id: u64,
pub reply_to: Recipient,
pub reply: RegistrationData,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RegisteredResponse {
pub request_id: u64,
pub reply_to: Recipient,
pub reply: RegistredData,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RemainingBandwidthResponse {
pub request_id: u64,
pub reply_to: Recipient,
pub reply: Option<RemainingBandwidthData>,
}
+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]
@@ -2,21 +2,37 @@
// SPDX-License-Identifier: Apache-2.0
use si_scale::helpers::bibytes2;
use std::sync::atomic::{AtomicI64, Ordering};
use std::sync::atomic::{AtomicBool, AtomicI64, Ordering};
use std::sync::Arc;
use std::time::Duration;
use time::OffsetDateTime;
#[derive(Clone, Default)]
pub(crate) struct BandwidthClaimGuard {
inner: Arc<ClientBandwidthInner>,
}
impl Drop for BandwidthClaimGuard {
fn drop(&mut self) {
let old = self.inner.claiming_more.swap(false, Ordering::SeqCst);
assert!(
old,
"critical failure: there were multiple BandwidthClaimGuard existing"
)
}
}
#[derive(Clone)]
pub struct ClientBandwidth {
inner: Arc<ClientBandwidthInner>,
}
#[derive(Default)]
struct ClientBandwidthInner {
/// the actual bandwidth amount (in bytes) available
available: AtomicI64,
/// flag to indicate whether this client is currently in the process of claiming additional bandwidth
claiming_more: AtomicBool,
/// defines the timestamp when the bandwidth information has been logged to the logs stream
last_logged_ts: AtomicI64,
@@ -29,11 +45,28 @@ impl ClientBandwidth {
ClientBandwidth {
inner: Arc::new(ClientBandwidthInner {
available: AtomicI64::new(0),
claiming_more: AtomicBool::new(false),
last_logged_ts: AtomicI64::new(0),
last_updated_ts: AtomicI64::new(0),
}),
}
}
pub(crate) fn begin_bandwidth_claim(&self) -> Option<BandwidthClaimGuard> {
if self
.inner
.claiming_more
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
Some(BandwidthClaimGuard {
inner: self.inner.clone(),
})
} else {
None
}
}
pub(crate) fn remaining(&self) -> i64 {
self.inner.available.load(Ordering::Acquire)
}
@@ -53,9 +86,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");
@@ -598,6 +724,11 @@ impl<C, St> GatewayClient<C, St> {
return Err(GatewayClientError::NoBandwidthControllerAvailable);
}
let Some(_claim_guard) = self.bandwidth.begin_bandwidth_claim() else {
debug!("there's already an existing bandwidth claim ongoing");
return Ok(());
};
warn!("Not enough bandwidth. Trying to get more bandwidth, this might take a while");
if !self.cfg.bandwidth.require_tickets {
info!("The client is running in disabled credentials mode - attempting to claim bandwidth without a credential");
@@ -665,10 +796,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 +808,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 +872,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 +936,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 +951,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 +969,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 }
@@ -171,10 +171,20 @@ impl SqliteEcashTicketbookManager {
data: &[u8],
) -> Result<(), sqlx::Error> {
sqlx::query!(
"INSERT INTO master_verification_key(epoch_id, serialised_key, serialization_revision) VALUES (?, ?, ?)",
r#"
INSERT OR IGNORE INTO master_verification_key(epoch_id, serialised_key, serialization_revision) VALUES (?, ?, ?);
UPDATE master_verification_key
SET
serialised_key = ?,
serialization_revision = ?
WHERE epoch_id = ?
"#,
epoch_id,
data,
serialisation_revision
serialisation_revision,
data,
serialisation_revision,
epoch_id
)
.execute(&self.connection_pool)
.await?;
@@ -204,10 +214,20 @@ impl SqliteEcashTicketbookManager {
data: &[u8],
) -> Result<(), sqlx::Error> {
sqlx::query!(
"INSERT INTO coin_indices_signatures(epoch_id, serialised_signatures, serialization_revision) VALUES (?, ?, ?)",
r#"
INSERT OR IGNORE INTO coin_indices_signatures(epoch_id, serialised_signatures, serialization_revision) VALUES (?, ?, ?);
UPDATE coin_indices_signatures
SET
serialised_signatures = ?,
serialization_revision = ?
WHERE epoch_id = ?
"#,
epoch_id,
data,
serialisation_revision
serialisation_revision,
data,
serialisation_revision,
epoch_id,
)
.execute(&self.connection_pool)
.await?;
@@ -240,13 +260,21 @@ impl SqliteEcashTicketbookManager {
) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT INTO expiration_date_signatures(expiration_date, epoch_id, serialised_signatures, serialization_revision)
VALUES (?, ?, ?, ?)
INSERT OR IGNORE INTO expiration_date_signatures(expiration_date, epoch_id, serialised_signatures, serialization_revision)
VALUES (?, ?, ?, ?);
UPDATE expiration_date_signatures
SET
serialised_signatures = ?,
serialization_revision = ?
WHERE expiration_date = ?
"#,
expiration_date,
epoch_id,
data,
serialisation_revision
serialisation_revision,
data,
serialisation_revision,
expiration_date
)
.execute(&self.connection_pool)
.await?;
@@ -1,6 +1,9 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::*;
use crate::BandwidthFlushingBehaviourConfig;
use crate::ClientBandwidth;
use nym_credentials::ecash::utils::ecash_today;
use nym_credentials_interface::Bandwidth;
use nym_gateway_requests::ServerResponse;
@@ -9,10 +12,6 @@ use si_scale::helpers::bibytes2;
use time::OffsetDateTime;
use tracing::*;
use crate::error::*;
use crate::BandwidthFlushingBehaviourConfig;
use crate::ClientBandwidth;
const FREE_TESTNET_BANDWIDTH_VALUE: Bandwidth = Bandwidth::new_unchecked(64 * 1024 * 1024 * 1024); // 64GB
#[derive(Clone)]
@@ -41,9 +40,13 @@ impl<S: Storage + Clone + 'static> BandwidthStorageManager<S> {
}
}
pub async fn available_bandwidth(&self) -> i64 {
self.client_bandwidth.available().await
}
async fn sync_expiration(&mut self) -> Result<()> {
self.storage
.set_expiration(self.client_id, self.client_bandwidth.bandwidth.expiration)
.set_expiration(self.client_id, self.client_bandwidth.expiration().await)
.await?;
Ok(())
}
@@ -57,17 +60,17 @@ impl<S: Storage + Clone + 'static> BandwidthStorageManager<S> {
self.increase_bandwidth(FREE_TESTNET_BANDWIDTH_VALUE, ecash_today())
.await?;
let available_total = self.client_bandwidth.bandwidth.bytes;
let available_total = self.client_bandwidth.available().await;
Ok(ServerResponse::Bandwidth { available_total })
}
#[instrument(skip_all)]
pub async fn try_use_bandwidth(&mut self, required_bandwidth: i64) -> Result<i64> {
if self.client_bandwidth.bandwidth.expired() {
if self.client_bandwidth.expired().await {
self.expire_bandwidth().await?;
}
let available_bandwidth = self.client_bandwidth.bandwidth.bytes;
let available_bandwidth = self.client_bandwidth.available().await;
if available_bandwidth < required_bandwidth {
return Err(Error::OutOfBandwidth {
@@ -86,8 +89,7 @@ impl<S: Storage + Clone + 'static> BandwidthStorageManager<S> {
async fn expire_bandwidth(&mut self) -> Result<()> {
self.storage.reset_bandwidth(self.client_id).await?;
self.client_bandwidth.bandwidth = Default::default();
self.client_bandwidth.update_sync_data();
self.client_bandwidth.expire_bandwidth().await;
Ok(())
}
@@ -97,31 +99,31 @@ impl<S: Storage + Clone + 'static> BandwidthStorageManager<S> {
///
/// * `amount`: amount to decrease the available bandwidth by.
async fn consume_bandwidth(&mut self, amount: i64) -> Result<()> {
self.client_bandwidth.bandwidth.bytes -= amount;
self.client_bandwidth.bytes_delta_since_sync -= amount;
self.client_bandwidth.decrease_bandwidth(amount).await;
// since we're going to be operating on a fair use policy anyway, even if we crash and let extra few packets
// through, that's completely fine
if self.client_bandwidth.should_sync(self.bandwidth_cfg) {
self.sync_bandwidth().await?;
if self.client_bandwidth.should_sync(self.bandwidth_cfg).await {
self.sync_storage_bandwidth().await?;
}
Ok(())
}
#[instrument(level = "trace", skip_all)]
async fn sync_bandwidth(&mut self) -> Result<()> {
async fn sync_storage_bandwidth(&mut self) -> Result<()> {
trace!("syncing client bandwidth with the underlying storage");
let updated = self
.storage
.increase_bandwidth(self.client_id, self.client_bandwidth.bytes_delta_since_sync)
.increase_bandwidth(
self.client_id,
self.client_bandwidth.delta_since_sync().await,
)
.await?;
trace!(updated);
self.client_bandwidth.bandwidth.bytes = updated;
self.client_bandwidth.update_sync_data();
self.client_bandwidth
.resync_bandwidth_with_storage(updated)
.await;
Ok(())
}
@@ -136,13 +138,14 @@ impl<S: Storage + Clone + 'static> BandwidthStorageManager<S> {
bandwidth: Bandwidth,
expiration: OffsetDateTime,
) -> Result<()> {
self.client_bandwidth.bandwidth.bytes += bandwidth.value() as i64;
self.client_bandwidth.bytes_delta_since_sync += bandwidth.value() as i64;
self.client_bandwidth.bandwidth.expiration = expiration;
self.client_bandwidth
.increase_bandwidth(bandwidth.value() as i64, expiration)
.await;
// any increases to bandwidth should get flushed immediately
// (we don't want to accidentally miss somebody claiming a gigabyte voucher)
self.sync_expiration().await?;
self.sync_bandwidth().await
self.sync_storage_bandwidth().await?;
Ok(())
}
}
@@ -1,10 +1,15 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use std::time::Duration;
use nym_credentials::ecash::utils::ecash_today;
use nym_credentials_interface::AvailableBandwidth;
use std::sync::Arc;
use std::time::Duration;
use time::OffsetDateTime;
use tokio::sync::RwLock;
const DEFAULT_CLIENT_BANDWIDTH_MAX_FLUSHING_RATE: Duration = Duration::from_millis(5);
const DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT: i64 = 512 * 1024; // 512kB
#[derive(Debug, Clone, Copy)]
pub struct BandwidthFlushingBehaviourConfig {
@@ -15,10 +20,25 @@ pub struct BandwidthFlushingBehaviourConfig {
pub client_bandwidth_max_delta_flushing_amount: i64,
}
#[derive(Debug, Clone, Copy)]
impl Default for BandwidthFlushingBehaviourConfig {
fn default() -> Self {
Self {
client_bandwidth_max_flushing_rate: DEFAULT_CLIENT_BANDWIDTH_MAX_FLUSHING_RATE,
client_bandwidth_max_delta_flushing_amount:
DEFAULT_CLIENT_BANDWIDTH_MAX_DELTA_FLUSHING_AMOUNT,
}
}
}
#[derive(Debug, Clone)]
pub struct ClientBandwidth {
inner: Arc<RwLock<ClientBandwidthInner>>,
}
#[derive(Debug)]
struct ClientBandwidthInner {
pub(crate) bandwidth: AvailableBandwidth,
pub(crate) last_flushed: OffsetDateTime,
pub(crate) last_synced: OffsetDateTime,
/// the number of bytes the client had during the last sync.
/// it is used to determine whether the current value should be synced with the storage
@@ -30,28 +50,74 @@ pub struct ClientBandwidth {
impl ClientBandwidth {
pub fn new(bandwidth: AvailableBandwidth) -> ClientBandwidth {
ClientBandwidth {
bandwidth,
last_flushed: OffsetDateTime::now_utc(),
bytes_at_last_sync: bandwidth.bytes,
bytes_delta_since_sync: 0,
inner: Arc::new(RwLock::new(ClientBandwidthInner {
bandwidth,
last_synced: OffsetDateTime::now_utc(),
bytes_at_last_sync: bandwidth.bytes,
bytes_delta_since_sync: 0,
})),
}
}
pub(crate) fn should_sync(&self, cfg: BandwidthFlushingBehaviourConfig) -> bool {
if self.bytes_delta_since_sync.abs() >= cfg.client_bandwidth_max_delta_flushing_amount {
pub(crate) async fn should_sync(&self, cfg: BandwidthFlushingBehaviourConfig) -> bool {
let guard = self.inner.read().await;
if guard.bytes_delta_since_sync.abs() >= cfg.client_bandwidth_max_delta_flushing_amount {
return true;
}
if self.last_flushed + cfg.client_bandwidth_max_flushing_rate < OffsetDateTime::now_utc() {
if guard.last_synced + cfg.client_bandwidth_max_flushing_rate < OffsetDateTime::now_utc() {
return true;
}
false
}
pub(crate) fn update_sync_data(&mut self) {
self.last_flushed = OffsetDateTime::now_utc();
self.bytes_at_last_sync = self.bandwidth.bytes;
self.bytes_delta_since_sync = 0;
pub(crate) async fn available(&self) -> i64 {
self.inner.read().await.bandwidth.bytes
}
pub(crate) async fn delta_since_sync(&self) -> i64 {
self.inner.read().await.bytes_delta_since_sync
}
pub(crate) async fn expiration(&self) -> OffsetDateTime {
self.inner.read().await.bandwidth.expiration
}
pub(crate) async fn expired(&self) -> bool {
self.expiration().await < ecash_today()
}
pub(crate) async fn decrease_bandwidth(&self, decrease: i64) {
let mut guard = self.inner.write().await;
guard.bandwidth.bytes -= decrease;
guard.bytes_delta_since_sync -= decrease;
}
pub(crate) async fn increase_bandwidth(&self, increase: i64, new_expiration: OffsetDateTime) {
let mut guard = self.inner.write().await;
guard.bandwidth.bytes += increase;
guard.bandwidth.expiration = new_expiration;
guard.bytes_delta_since_sync += increase;
}
pub(crate) async fn expire_bandwidth(&self) {
let mut guard = self.inner.write().await;
guard.bandwidth = AvailableBandwidth::default();
guard.last_synced = OffsetDateTime::now_utc();
guard.bytes_at_last_sync = 0;
guard.bytes_delta_since_sync = 0;
}
pub(crate) async fn resync_bandwidth_with_storage(&self, stored: i64) {
let mut guard = self.inner.write().await;
guard.bandwidth.bytes = stored;
guard.bytes_at_last_sync = stored;
guard.bytes_delta_since_sync = 0;
guard.last_synced = OffsetDateTime::now_utc();
}
}
@@ -73,6 +73,10 @@ where
self.shared_state.verification_key(epoch_id).await
}
pub fn storage(&self) -> &S {
&self.shared_state.storage
}
//Check for duplicate pay_info, then check the payment, then insert pay_info if everything succeeded
pub async fn check_payment(
&self,
+2 -2
View File
@@ -150,7 +150,7 @@ impl<S: Storage + Clone + 'static> CredentialVerifier<S> {
Ok(self
.bandwidth_storage_manager
.client_bandwidth
.bandwidth
.bytes)
.available()
.await)
}
}
+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;
@@ -0,0 +1,10 @@
/*
* Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
* SPDX-License-Identifier: Apache-2.0
*/
ALTER TABLE wireguard_peer
ADD COLUMN client_id INTEGER REFERENCES clients(id) DEFAULT NULL;
ALTER TABLE wireguard_peer
DROP COLUMN suspended;
+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),
+49 -17
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.
@@ -225,12 +227,14 @@ pub trait Storage: Send + Sync {
/// # Arguments
///
/// * `peer`: wireguard peer data to be stored
/// * `suspended`: if peer exists, but it's currently suspended
/// * `with_client_id`: if the peer should have a corresponding client_id
/// (created with entry wireguard ticket) or live without one (or with an
/// exiting one), for temporary backwards compatibility.
async fn insert_wireguard_peer(
&self,
peer: &defguard_wireguard_rs::host::Peer,
suspended: bool,
) -> Result<(), StorageError>;
with_client_id: bool,
) -> Result<Option<i64>, StorageError>;
/// Tries to retrieve available bandwidth for the particular peer.
///
@@ -330,17 +334,27 @@ 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
.insert_client(ClientType::EntryMixnet)
.await?;
let client_address_bs58 = client_address.as_base58_string();
let client_id = match self
.shared_key_manager
.client_id(&client_address_bs58)
.await
{
Ok(client_id) => client_id,
_ => {
self.client_manager
.insert_client(ClientType::EntryMixnet)
.await?
}
};
self.shared_key_manager
.insert_shared_keys(
client_id,
client_address.as_base58_string(),
shared_keys.to_base58_string(),
client_address_bs58,
shared_keys.aes128_ctr_hmac_bs58().as_deref(),
shared_keys.aes256_gcm_siv().as_deref(),
)
.await?;
Ok(client_id)
@@ -637,12 +651,30 @@ impl Storage for PersistentStorage {
async fn insert_wireguard_peer(
&self,
peer: &defguard_wireguard_rs::host::Peer,
suspended: bool,
) -> Result<(), StorageError> {
with_client_id: bool,
) -> Result<Option<i64>, StorageError> {
let client_id = match self
.wireguard_peer_manager
.retrieve_peer(&peer.public_key.to_string())
.await?
{
Some(peer) => peer.client_id,
_ => {
if with_client_id {
Some(
self.client_manager
.insert_client(ClientType::EntryWireguard)
.await?,
)
} else {
None
}
}
};
let mut peer = WireguardPeer::from(peer.clone());
peer.suspended = suspended;
peer.client_id = client_id;
self.wireguard_peer_manager.insert_peer(&peer).await?;
Ok(())
Ok(client_id)
}
async fn get_wireguard_peer(
+31 -3
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 {
@@ -88,7 +116,7 @@ pub struct WireguardPeer {
pub rx_bytes: i64,
pub persistent_keepalive_interval: Option<i64>,
pub allowed_ips: Vec<u8>,
pub suspended: bool,
pub client_id: Option<i64>,
}
impl From<defguard_wireguard_rs::host::Peer> for WireguardPeer {
@@ -118,7 +146,7 @@ impl From<defguard_wireguard_rs::host::Peer> for WireguardPeer {
&value.allowed_ips,
)
.unwrap_or_default(),
suspended: false,
client_id: None,
}
}
}
+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?;
@@ -27,16 +27,16 @@ impl WgPeerManager {
pub(crate) async fn insert_peer(&self, peer: &WireguardPeer) -> Result<(), sqlx::Error> {
sqlx::query!(
r#"
INSERT OR IGNORE INTO wireguard_peer(public_key, preshared_key, protocol_version, endpoint, last_handshake, tx_bytes, rx_bytes, persistent_keepalive_interval, allowed_ips, suspended)
INSERT OR IGNORE INTO wireguard_peer(public_key, preshared_key, protocol_version, endpoint, last_handshake, tx_bytes, rx_bytes, persistent_keepalive_interval, allowed_ips, client_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
UPDATE wireguard_peer
SET preshared_key = ?, protocol_version = ?, endpoint = ?, last_handshake = ?, tx_bytes = ?, rx_bytes = ?, persistent_keepalive_interval = ?, allowed_ips = ?, suspended = ?
SET preshared_key = ?, protocol_version = ?, endpoint = ?, last_handshake = ?, tx_bytes = ?, rx_bytes = ?, persistent_keepalive_interval = ?, allowed_ips = ?, client_id = ?
WHERE public_key = ?
"#,
peer.public_key, peer.preshared_key, peer.protocol_version, peer.endpoint, peer.last_handshake, peer.tx_bytes, peer.rx_bytes, peer.persistent_keepalive_interval, peer.allowed_ips, peer.suspended,
peer.preshared_key, peer.protocol_version, peer.endpoint, peer.last_handshake, peer.tx_bytes, peer.rx_bytes, peer.persistent_keepalive_interval, peer.allowed_ips, peer.suspended,peer.public_key,
peer.public_key, peer.preshared_key, peer.protocol_version, peer.endpoint, peer.last_handshake, peer.tx_bytes, peer.rx_bytes, peer.persistent_keepalive_interval, peer.allowed_ips, peer.client_id,
peer.preshared_key, peer.protocol_version, peer.endpoint, peer.last_handshake, peer.tx_bytes, peer.rx_bytes, peer.persistent_keepalive_interval, peer.allowed_ips, peer.client_id,
peer.public_key,
)
.execute(&self.connection_pool)
.await?;
@@ -78,7 +78,7 @@ impl WgPeerManager {
.await
}
/// Retrieve the wireguard peer with the provided public key from the storage.
/// Remove the wireguard peer with the provided public key from the storage.
///
/// # Arguments
///
+3 -1
View File
@@ -18,7 +18,9 @@ pub use user_agent::UserAgent;
mod user_agent;
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
// The timeout is relatively high as we are often making requests over the mixnet, where latency is
// high and chatty protocols take a while to complete.
pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
pub type PathSegments<'a> = &'a [&'a str];
pub type Params<'a, K, V> = &'a [(K, V)];

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