Compare commits
76 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dce0d161ea | |||
| 853a62bc5b | |||
| 1927614803 | |||
| 75a5192c6d | |||
| 25ad0920cf | |||
| a4c6f51fe0 | |||
| f76300669a | |||
| 333ace1f97 | |||
| 487bf6732e | |||
| 5d4a0fef55 | |||
| 1627146c0e | |||
| ae40a00b8f | |||
| 7f3c0470e0 | |||
| 1bc26ed79f | |||
| 60fa5cfeb8 | |||
| 3b7088aeea | |||
| 179d214e21 | |||
| 2a94ce6443 | |||
| 95ec91daa1 | |||
| 803850be74 | |||
| 2f267cf787 | |||
| 0d2418ef6a | |||
| 6f0c8dbe73 | |||
| 2198c1bd7b | |||
| be7f00fe52 | |||
| 35c94f5c4b | |||
| f5863b9668 | |||
| 963c54fea2 | |||
| db55a96f91 | |||
| 7c0235ab26 | |||
| 92af6f7024 | |||
| 7146c4c012 | |||
| 3dc62a9a60 | |||
| b3d7c26443 | |||
| 9efeef881a | |||
| 9d8369a5b2 | |||
| cc32eb3904 | |||
| 8cf4977021 | |||
| 2c2748832c | |||
| 114db3c1cf | |||
| a65df5a0ab | |||
| b6f07fbfce | |||
| c39d42b7dd | |||
| 21e9df488f | |||
| 94113206b2 | |||
| 71532484a9 | |||
| 8756763875 | |||
| 5753b79997 | |||
| 2a6aa13ecd | |||
| 9213e02b43 | |||
| ede4b23e8a | |||
| 2e95ea16f9 | |||
| d5c9e1d8cb | |||
| 0c955817fd | |||
| 87751894d9 | |||
| ec3c4fb1aa | |||
| 789221f144 | |||
| 5b925d8b68 | |||
| c8c3928575 | |||
| 2fa8da8117 | |||
| 4548ef4d05 | |||
| 7f147ee2b0 | |||
| 48bcd7e802 | |||
| 6598d677da | |||
| e736a01ecc | |||
| a708fa2d4a | |||
| a512217382 | |||
| 086611c7ac | |||
| 05d6652177 | |||
| 9c514fe3b7 | |||
| aad028be3f | |||
| 924160b3e7 | |||
| 23d14b60de | |||
| a4b47ef3a5 | |||
| 6db3b34bcb | |||
| f9383578da |
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+24
-10
@@ -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,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,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"
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
}
|
||||
@@ -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;
|
||||
+13
-22
@@ -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>,
|
||||
}
|
||||
@@ -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" }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
+13
@@ -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(),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -150,7 +150,7 @@ impl<S: Storage + Clone + 'static> CredentialVerifier<S> {
|
||||
Ok(self
|
||||
.bandwidth_storage_manager
|
||||
.client_bandwidth
|
||||
.bandwidth
|
||||
.bytes)
|
||||
.available()
|
||||
.await)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
///
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user