Compare commits
35 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c87410dbdd | |||
| ed8c700d00 | |||
| 2f267cf787 | |||
| 0d2418ef6a | |||
| 6f0c8dbe73 | |||
| 2198c1bd7b | |||
| be7f00fe52 | |||
| 35c94f5c4b | |||
| f5863b9668 | |||
| 963c54fea2 | |||
| db55a96f91 | |||
| 7c0235ab26 | |||
| 92af6f7024 | |||
| 7146c4c012 | |||
| b3d7c26443 | |||
| 9efeef881a | |||
| 9d8369a5b2 | |||
| cc32eb3904 | |||
| 8cf4977021 | |||
| 2c2748832c | |||
| 114db3c1cf | |||
| a65df5a0ab | |||
| b6f07fbfce | |||
| c39d42b7dd | |||
| 21e9df488f | |||
| 94113206b2 | |||
| 71532484a9 | |||
| 8756763875 | |||
| 9213e02b43 | |||
| ede4b23e8a | |||
| 2e95ea16f9 | |||
| 0c955817fd | |||
| ec3c4fb1aa | |||
| 789221f144 | |||
| 5b925d8b68 |
@@ -56,7 +56,7 @@ jobs:
|
||||
echo $OUTPUT_DIR
|
||||
|
||||
- name: Install Dependencies (Linux)
|
||||
run: sudo apt update && sudo apt install libudev-dev
|
||||
run: sudo apt-get update && sudo apt-get -y install libudev-dev
|
||||
|
||||
- name: Sets env vars for tokio if set in manual dispatch inputs
|
||||
run: |
|
||||
|
||||
@@ -90,14 +90,6 @@ jobs:
|
||||
command: test
|
||||
args: --workspace -- --ignored
|
||||
|
||||
- name: Annotate with clippy checks
|
||||
if: contains(matrix.os, 'ubuntu')
|
||||
uses: actions-rs/clippy-check@v1
|
||||
continue-on-error: true
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --workspace
|
||||
|
||||
- name: Clippy
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
|
||||
@@ -18,7 +18,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
||||
with:
|
||||
log-level: warn
|
||||
command: check ${{ matrix.checks }}
|
||||
|
||||
Generated
+103
-405
@@ -86,6 +86,21 @@ dependencies = [
|
||||
"subtle 2.5.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aes-gcm-siv"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae0784134ba9375416d469ec31e7c5f9fa94405049cf08c5ce5b4698be673e0d"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"aes",
|
||||
"cipher",
|
||||
"ctr",
|
||||
"polyval",
|
||||
"subtle 2.5.0",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.7.8"
|
||||
@@ -213,9 +228,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.87"
|
||||
version = "1.0.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10f00e1f6e58a40e807377c75c6a7f97bf9044fab57816f2414e6f5f4499d7b8"
|
||||
checksum = "86fdf8605db99b54d3cd748a44c6d04df638eb5dafb219b135d0149bd0db01f6"
|
||||
|
||||
[[package]]
|
||||
name = "arbitrary"
|
||||
@@ -393,7 +408,7 @@ dependencies = [
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.0",
|
||||
"http-body-util",
|
||||
"hyper 1.3.1",
|
||||
"hyper 1.4.1",
|
||||
"hyper-util",
|
||||
"itoa",
|
||||
"matchit",
|
||||
@@ -952,18 +967,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete"
|
||||
version = "4.5.5"
|
||||
version = "4.5.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2020fa13af48afc65a9a87335bda648309ab3d154cd03c7ff95b378c7ed39c4"
|
||||
checksum = "9b378c786d3bde9442d2c6dd7e6080b2a818db2b96e30d6e7f1b6d224eb617d3"
|
||||
dependencies = [
|
||||
"clap 4.5.17",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_complete_fig"
|
||||
version = "4.5.1"
|
||||
version = "4.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb4bc503cddc1cd320736fb555d6598309ad07c2ddeaa23891a10ffb759ee612"
|
||||
checksum = "d494102c8ff3951810c72baf96910b980fb065ca5d3101243e6a8dc19747c86b"
|
||||
dependencies = [
|
||||
"clap 4.5.17",
|
||||
"clap_complete",
|
||||
@@ -1022,18 +1037,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "comfy-table"
|
||||
version = "6.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e959d788268e3bf9d35ace83e81b124190378e4c91c9067524675e33394b8ba"
|
||||
dependencies = [
|
||||
"crossterm 0.26.1",
|
||||
"strum 0.24.1",
|
||||
"strum_macros 0.24.3",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "comfy-table"
|
||||
version = "7.1.1"
|
||||
@@ -1471,22 +1474,6 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.26.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crossterm_winapi",
|
||||
"libc",
|
||||
"mio 0.8.11",
|
||||
"parking_lot 0.12.3",
|
||||
"signal-hook",
|
||||
"signal-hook-mio",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.27.0"
|
||||
@@ -1633,16 +1620,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "curve25519-dalek"
|
||||
version = "4.1.2"
|
||||
version = "4.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348"
|
||||
checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"curve25519-dalek-derive",
|
||||
"digest 0.10.7",
|
||||
"fiat-crypto",
|
||||
"platforms",
|
||||
"rustc_version 0.4.0",
|
||||
"serde",
|
||||
"subtle 2.5.0",
|
||||
@@ -2104,7 +2090,7 @@ version = "2.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a3daa8e81a3963a60642bcc1f90a670680bd4a77535faa384e9d1c79d620871"
|
||||
dependencies = [
|
||||
"curve25519-dalek 4.1.2",
|
||||
"curve25519-dalek 4.1.3",
|
||||
"ed25519",
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
@@ -2303,6 +2289,12 @@ dependencies = [
|
||||
"ext-trait",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "extern-c"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320bea982e85d42441eb25c49b41218e7eaa2657e8f90bc4eca7437376751e23"
|
||||
|
||||
[[package]]
|
||||
name = "eyre"
|
||||
version = "0.6.12"
|
||||
@@ -2365,7 +2357,7 @@ dependencies = [
|
||||
"atomic 0.6.0",
|
||||
"pear",
|
||||
"serde",
|
||||
"toml 0.8.14",
|
||||
"toml",
|
||||
"uncased",
|
||||
"version_check",
|
||||
]
|
||||
@@ -2626,14 +2618,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "getset"
|
||||
version = "0.1.2"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e45727250e75cc04ff2846a66397da8ef2b3db8e40e0cef4df67950a07621eb9"
|
||||
checksum = "f636605b743120a8d32ed92fc27b6cde1a769f8f936c065151eb66f88ded513c"
|
||||
dependencies = [
|
||||
"proc-macro-error",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.109",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3052,9 +3044,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "1.3.1"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe575dd17d0862a9a33781c8c4696a55c320909004a67a00fb286ba8b1bc496d"
|
||||
checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-channel",
|
||||
@@ -3092,7 +3084,7 @@ checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"http 1.1.0",
|
||||
"hyper 1.3.1",
|
||||
"hyper 1.4.1",
|
||||
"hyper-util",
|
||||
"rustls 0.22.4",
|
||||
"rustls-pki-types",
|
||||
@@ -3124,7 +3116,7 @@ dependencies = [
|
||||
"futures-util",
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.0",
|
||||
"hyper 1.3.1",
|
||||
"hyper 1.4.1",
|
||||
"pin-project-lite",
|
||||
"socket2",
|
||||
"tokio",
|
||||
@@ -3156,124 +3148,6 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locid"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locid_transform"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locid",
|
||||
"icu_locid_transform_data",
|
||||
"icu_provider",
|
||||
"tinystr",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locid_transform_data"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"utf16_iter",
|
||||
"utf8_iter",
|
||||
"write16",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
"icu_locid_transform",
|
||||
"icu_properties_data",
|
||||
"icu_provider",
|
||||
"tinystr",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locid",
|
||||
"icu_provider_macros",
|
||||
"stable_deref_trait",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider_macros"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
@@ -3292,14 +3166,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "1.0.0"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed"
|
||||
checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6"
|
||||
dependencies = [
|
||||
"icu_normalizer",
|
||||
"icu_properties",
|
||||
"smallvec",
|
||||
"utf8_iter",
|
||||
"unicode-bidi",
|
||||
"unicode-normalization",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3724,12 +3596,6 @@ dependencies = [
|
||||
"keystream",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
@@ -3742,9 +3608,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.21"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "loom"
|
||||
@@ -4426,7 +4292,7 @@ dependencies = [
|
||||
"opentelemetry-jaeger",
|
||||
"pretty_env_logger",
|
||||
"schemars",
|
||||
"semver 0.11.0",
|
||||
"semver 1.0.23",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tracing-opentelemetry",
|
||||
@@ -4487,7 +4353,7 @@ dependencies = [
|
||||
"cfg-if",
|
||||
"clap 4.5.17",
|
||||
"colored",
|
||||
"comfy-table 6.2.0",
|
||||
"comfy-table",
|
||||
"cosmrs 0.17.0-pre",
|
||||
"cosmwasm-std",
|
||||
"csv",
|
||||
@@ -4527,7 +4393,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
"toml 0.8.14",
|
||||
"toml",
|
||||
"url",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -4578,12 +4444,12 @@ dependencies = [
|
||||
"bs58",
|
||||
"cfg-if",
|
||||
"clap 4.5.17",
|
||||
"comfy-table 7.1.1",
|
||||
"comfy-table",
|
||||
"futures",
|
||||
"gloo-timers",
|
||||
"http-body-util",
|
||||
"humantime-serde",
|
||||
"hyper 1.3.1",
|
||||
"hyper 1.4.1",
|
||||
"hyper-util",
|
||||
"log",
|
||||
"nym-bandwidth-controller",
|
||||
@@ -4790,7 +4656,7 @@ dependencies = [
|
||||
"log",
|
||||
"nym-network-defaults",
|
||||
"serde",
|
||||
"toml 0.7.8",
|
||||
"toml",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -4919,7 +4785,9 @@ dependencies = [
|
||||
name = "nym-crypto"
|
||||
version = "0.4.0"
|
||||
dependencies = [
|
||||
"aead",
|
||||
"aes",
|
||||
"aes-gcm-siv",
|
||||
"blake3",
|
||||
"bs58",
|
||||
"cipher",
|
||||
@@ -5104,7 +4972,6 @@ dependencies = [
|
||||
"futures",
|
||||
"getrandom",
|
||||
"gloo-utils 0.2.0",
|
||||
"log",
|
||||
"nym-bandwidth-controller",
|
||||
"nym-credential-storage",
|
||||
"nym-credentials",
|
||||
@@ -5123,12 +4990,14 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-tungstenite",
|
||||
"tracing",
|
||||
"tungstenite 0.20.1",
|
||||
"url",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"wasm-utils",
|
||||
"wasmtimer",
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5148,6 +5017,7 @@ dependencies = [
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"strum 0.26.3",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@@ -5407,7 +5277,7 @@ dependencies = [
|
||||
"time",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"toml 0.5.11",
|
||||
"toml",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -5587,7 +5457,7 @@ dependencies = [
|
||||
"sysinfo",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"toml 0.8.14",
|
||||
"toml",
|
||||
"tracing",
|
||||
"url",
|
||||
"zeroize",
|
||||
@@ -5605,7 +5475,7 @@ dependencies = [
|
||||
"fastrand 2.1.1",
|
||||
"headers",
|
||||
"hmac",
|
||||
"hyper 1.3.1",
|
||||
"hyper 1.4.1",
|
||||
"ipnetwork 0.20.0",
|
||||
"nym-crypto",
|
||||
"nym-http-api-common",
|
||||
@@ -5733,7 +5603,7 @@ dependencies = [
|
||||
"chacha20",
|
||||
"chacha20poly1305",
|
||||
"criterion 0.4.0",
|
||||
"curve25519-dalek 4.1.2",
|
||||
"curve25519-dalek 4.1.3",
|
||||
"fastrand 2.1.1",
|
||||
"getrandom",
|
||||
"log",
|
||||
@@ -5793,7 +5663,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
"tokio-util",
|
||||
"toml 0.5.11",
|
||||
"toml",
|
||||
"url",
|
||||
"zeroize",
|
||||
]
|
||||
@@ -6144,7 +6014,7 @@ dependencies = [
|
||||
"nym-sphinx-types",
|
||||
"rand",
|
||||
"reqwest 0.12.4",
|
||||
"semver 0.11.0",
|
||||
"semver 1.0.23",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror",
|
||||
@@ -6838,12 +6708,6 @@ version = "0.3.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
|
||||
|
||||
[[package]]
|
||||
name = "platforms"
|
||||
version = "3.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7"
|
||||
|
||||
[[package]]
|
||||
name = "plotters"
|
||||
version = "0.3.6"
|
||||
@@ -6982,6 +6846,28 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error-attr2"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.85"
|
||||
@@ -7128,9 +7014,9 @@ checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
version = "1.0.37"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -7382,7 +7268,7 @@ dependencies = [
|
||||
"http 1.1.0",
|
||||
"http-body 1.0.0",
|
||||
"http-body-util",
|
||||
"hyper 1.3.1",
|
||||
"hyper 1.4.1",
|
||||
"hyper-rustls 0.26.0",
|
||||
"hyper-util",
|
||||
"ipnet",
|
||||
@@ -7783,10 +7669,11 @@ checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
|
||||
|
||||
[[package]]
|
||||
name = "safer-ffi"
|
||||
version = "0.1.12"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "614978f55c6e885794b44afb2e2378fe61b104ec4391f310d879edf3df9d874c"
|
||||
checksum = "435fdd58b61a6f1d8545274c1dfa458e905ff68c166e65e294a0130ef5e675bd"
|
||||
dependencies = [
|
||||
"extern-c",
|
||||
"inventory",
|
||||
"libc",
|
||||
"macro_rules_attribute",
|
||||
@@ -7801,9 +7688,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "safer_ffi-proc_macros"
|
||||
version = "0.1.12"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cdcb365894c49e42b5413def59ac644ea085bbbdee535d7c80b2b008a345686"
|
||||
checksum = "f0f25be5ba5f319542edb31925517e0380245ae37df50a9752cdbc05ef948156"
|
||||
dependencies = [
|
||||
"macro_rules_attribute",
|
||||
"prettyplease",
|
||||
@@ -7921,16 +7808,7 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403"
|
||||
dependencies = [
|
||||
"semver-parser 0.7.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6"
|
||||
dependencies = [
|
||||
"semver-parser 0.10.2",
|
||||
"semver-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7948,15 +7826,6 @@ version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
|
||||
|
||||
[[package]]
|
||||
name = "semver-parser"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7"
|
||||
dependencies = [
|
||||
"pest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.210"
|
||||
@@ -8335,7 +8204,7 @@ dependencies = [
|
||||
"byteorder",
|
||||
"chacha",
|
||||
"ctr",
|
||||
"curve25519-dalek 4.1.2",
|
||||
"curve25519-dalek 4.1.3",
|
||||
"digest 0.10.7",
|
||||
"hkdf",
|
||||
"hmac",
|
||||
@@ -8523,12 +8392,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "stable_deref_trait"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
|
||||
|
||||
[[package]]
|
||||
name = "state"
|
||||
version = "0.6.0"
|
||||
@@ -8570,12 +8433,6 @@ dependencies = [
|
||||
"strum_macros 0.23.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.26.3"
|
||||
@@ -8598,19 +8455,6 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.24.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59"
|
||||
dependencies = [
|
||||
"heck 0.4.1",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustversion",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.26.4"
|
||||
@@ -8685,22 +8529,11 @@ version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
|
||||
|
||||
[[package]]
|
||||
name = "synstructure"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.30.12"
|
||||
version = "0.30.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "732ffa00f53e6b2af46208fba5718d9662a421049204e156328b66791ffa15ae"
|
||||
checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"core-foundation-sys",
|
||||
@@ -8833,7 +8666,7 @@ dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tendermint 0.37.0",
|
||||
"toml 0.8.14",
|
||||
"toml",
|
||||
"url",
|
||||
]
|
||||
|
||||
@@ -8947,7 +8780,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
"toml 0.8.14",
|
||||
"toml",
|
||||
"tracing",
|
||||
"url",
|
||||
"zeroize",
|
||||
@@ -9045,16 +8878,6 @@ dependencies = [
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinystr"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tinytemplate"
|
||||
version = "1.2.1"
|
||||
@@ -9232,27 +9055,6 @@ dependencies = [
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"toml_edit 0.19.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.8.14"
|
||||
@@ -9274,19 +9076,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.19.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
|
||||
dependencies = [
|
||||
"indexmap 2.2.6",
|
||||
"serde",
|
||||
"serde_spanned",
|
||||
"toml_datetime",
|
||||
"winnow 0.5.40",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_edit"
|
||||
version = "0.21.1"
|
||||
@@ -9792,12 +9581,12 @@ checksum = "0976c77def3f1f75c4ef892a292c31c0bbe9e3d0702c63044d7c76db298171a3"
|
||||
|
||||
[[package]]
|
||||
name = "url"
|
||||
version = "2.5.1"
|
||||
version = "2.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56"
|
||||
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
|
||||
dependencies = [
|
||||
"form_urlencoded",
|
||||
"idna 1.0.0",
|
||||
"idna 0.5.0",
|
||||
"percent-encoding",
|
||||
"serde",
|
||||
]
|
||||
@@ -9814,18 +9603,6 @@ version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
|
||||
|
||||
[[package]]
|
||||
name = "utf16_iter"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
|
||||
|
||||
[[package]]
|
||||
name = "utf8_iter"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
@@ -10541,18 +10318,6 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "write16"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
|
||||
|
||||
[[package]]
|
||||
name = "writeable"
|
||||
version = "0.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
|
||||
|
||||
[[package]]
|
||||
name = "wyz"
|
||||
version = "0.5.1"
|
||||
@@ -10568,7 +10333,7 @@ version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
|
||||
dependencies = [
|
||||
"curve25519-dalek 4.1.2",
|
||||
"curve25519-dalek 4.1.3",
|
||||
"rand_core 0.6.4",
|
||||
"serde",
|
||||
"zeroize",
|
||||
@@ -10594,30 +10359,6 @@ dependencies = [
|
||||
"is-terminal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"stable_deref_trait",
|
||||
"yoke-derive",
|
||||
"zerofrom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yoke-derive"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.34"
|
||||
@@ -10638,27 +10379,6 @@ dependencies = [
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55"
|
||||
dependencies = [
|
||||
"zerofrom-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerofrom-derive"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeroize"
|
||||
version = "1.6.0"
|
||||
@@ -10679,28 +10399,6 @@ dependencies = [
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerovec-derive"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.66",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zip"
|
||||
version = "1.1.4"
|
||||
|
||||
+11
-8
@@ -169,7 +169,9 @@ readme = "README.md"
|
||||
addr = "0.15.6"
|
||||
aes = "0.8.1"
|
||||
aes-gcm = "0.10.1"
|
||||
anyhow = "1.0.87"
|
||||
aes-gcm-siv = "0.11.1"
|
||||
aead = "0.5.2"
|
||||
anyhow = "1.0.89"
|
||||
argon2 = "0.5.0"
|
||||
async-trait = "0.1.82"
|
||||
axum = "0.7.5"
|
||||
@@ -198,7 +200,7 @@ clap = "4.5.17"
|
||||
clap_complete = "4.5"
|
||||
clap_complete_fig = "4.5"
|
||||
colored = "2.0"
|
||||
comfy-table = "6.0.0"
|
||||
comfy-table = "7.1.1"
|
||||
console = "0.15.8"
|
||||
console-subscriber = "0.1.1"
|
||||
console_error_panic_hook = "0.1"
|
||||
@@ -224,7 +226,7 @@ flate2 = "1.0.33"
|
||||
futures = "0.3.28"
|
||||
generic-array = "0.14.7"
|
||||
getrandom = "0.2.10"
|
||||
getset = "0.1.1"
|
||||
getset = "0.1.3"
|
||||
handlebars = "3.5.5"
|
||||
headers = "0.4.0"
|
||||
hex = "0.4.3"
|
||||
@@ -232,10 +234,12 @@ hex-literal = "0.3.3"
|
||||
hkdf = "0.12.3"
|
||||
hmac = "0.12.1"
|
||||
http = "1"
|
||||
http-body-util = "0.1"
|
||||
httpcodec = "0.2.3"
|
||||
humantime = "2.1.0"
|
||||
humantime-serde = "1.1.1"
|
||||
hyper = "1.3.1"
|
||||
hyper = "1.4.1"
|
||||
hyper-util = "0.1"
|
||||
indicatif = "0.17.8"
|
||||
inquire = "0.6.2"
|
||||
ip_network = "0.4.1"
|
||||
@@ -274,7 +278,7 @@ reqwest = { version = "0.12.4", default-features = false }
|
||||
rocket = "0.5.0"
|
||||
rocket_cors = "0.6.0"
|
||||
rocket_okapi = "0.8.0"
|
||||
safer-ffi = "0.1.12"
|
||||
safer-ffi = "0.1.13"
|
||||
schemars = "0.8.21"
|
||||
semver = "1.0.23"
|
||||
serde = "1.0.210"
|
||||
@@ -291,7 +295,7 @@ sqlx = "0.6.3"
|
||||
strum = "0.26"
|
||||
subtle-encoding = "0.5"
|
||||
syn = "1"
|
||||
sysinfo = "0.30.12"
|
||||
sysinfo = "0.30.13"
|
||||
tap = "1.0.1"
|
||||
tar = "0.4.41"
|
||||
tempfile = "3.5.0"
|
||||
@@ -300,6 +304,7 @@ time = "0.3.30"
|
||||
tokio = "1.39"
|
||||
tokio-stream = "0.1.16"
|
||||
tokio-test = "0.4.4"
|
||||
tokio-tun = "0.11.5"
|
||||
tokio-tungstenite = { version = "0.20.1" }
|
||||
tokio-util = "0.7.12"
|
||||
toml = "0.8.14"
|
||||
@@ -313,7 +318,6 @@ ts-rs = "7.0.0"
|
||||
tungstenite = { version = "0.20.1", default-features = false }
|
||||
url = "2.5"
|
||||
utoipa = "4.2"
|
||||
utoipa-rapidoc = "4.0"
|
||||
utoipa-swagger-ui = "7.1"
|
||||
utoipauto = "0.1"
|
||||
uuid = "*"
|
||||
@@ -334,7 +338,6 @@ group = { version = "0.13.0", default-features = false }
|
||||
ff = { version = "0.13.0", default-features = false }
|
||||
|
||||
# cosmwasm-related
|
||||
cosmwasm-derive = "=1.4.3"
|
||||
cosmwasm-schema = "=1.4.3"
|
||||
cosmwasm-std = "=1.4.3"
|
||||
# use 0.5.0 as that's the version used by cosmwasm-std 1.4.3
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -53,9 +53,9 @@ impl ClientBandwidth {
|
||||
let remaining_bi2 = bibytes2(remaining as f64);
|
||||
|
||||
if remaining < 0 {
|
||||
log::warn!("OUT OF BANDWIDTH. remaining: {remaining_bi2}");
|
||||
tracing::warn!("OUT OF BANDWIDTH. remaining: {remaining_bi2}");
|
||||
} else {
|
||||
log::info!("remaining bandwidth: {remaining_bi2}");
|
||||
tracing::info!("remaining bandwidth: {remaining_bi2}");
|
||||
}
|
||||
|
||||
self.inner
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Copyright 2021-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::bandwidth::ClientBandwidth;
|
||||
use crate::client::config::GatewayClientConfig;
|
||||
use crate::error::GatewayClientError;
|
||||
@@ -11,24 +12,24 @@ use crate::socket_state::{ws_fd, PartiallyDelegatedHandle, SocketState};
|
||||
use crate::traits::GatewayPacketRouter;
|
||||
use crate::{cleanup_socket_message, try_decrypt_binary_message};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use log::*;
|
||||
use nym_bandwidth_controller::{BandwidthController, BandwidthStatusMessage};
|
||||
use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage;
|
||||
use nym_credential_storage::storage::Storage as CredentialStorage;
|
||||
use nym_credentials::CredentialSpendingData;
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_gateway_requests::authentication::encrypted_address::EncryptedAddressBytes;
|
||||
use nym_gateway_requests::iv::IV;
|
||||
use nym_gateway_requests::registration::handshake::{client_handshake, SharedKeys};
|
||||
use nym_gateway_requests::registration::handshake::client_handshake;
|
||||
use nym_gateway_requests::{
|
||||
BinaryRequest, ClientControlRequest, ServerResponse, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION,
|
||||
CURRENT_PROTOCOL_VERSION,
|
||||
BinaryRequest, ClientControlRequest, ClientRequest, SensitiveServerResponse, ServerResponse,
|
||||
SharedGatewayKey, SharedSymmetricKey, AES_GCM_SIV_PROTOCOL_VERSION,
|
||||
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION,
|
||||
};
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
use nym_task::TaskClient;
|
||||
use nym_validator_client::nyxd::contract_traits::DkgQueryClient;
|
||||
use rand::rngs::OsRng;
|
||||
use std::sync::Arc;
|
||||
use tracing::instrument;
|
||||
use tracing::*;
|
||||
use tungstenite::protocol::Message;
|
||||
use url::Url;
|
||||
|
||||
@@ -45,6 +46,7 @@ use std::os::raw::c_int as RawFd;
|
||||
use wasm_utils::websocket::JSWebsocket;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
use wasmtimer::tokio::sleep;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
pub mod config;
|
||||
|
||||
@@ -71,6 +73,13 @@ impl GatewayConfig {
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[derive(Debug)]
|
||||
pub struct AuthenticationResponse {
|
||||
pub initial_shared_key: Arc<SharedGatewayKey>,
|
||||
pub requires_key_upgrade: bool,
|
||||
}
|
||||
|
||||
// TODO: this should be refactored into a state machine that keeps track of its authentication state
|
||||
pub struct GatewayClient<C, St = EphemeralCredentialStorage> {
|
||||
pub cfg: GatewayClientConfig,
|
||||
@@ -80,7 +89,7 @@ pub struct GatewayClient<C, St = EphemeralCredentialStorage> {
|
||||
gateway_address: String,
|
||||
gateway_identity: identity::PublicKey,
|
||||
local_identity: Arc<identity::KeyPair>,
|
||||
shared_key: Option<Arc<SharedKeys>>,
|
||||
shared_key: Option<Arc<SharedGatewayKey>>,
|
||||
connection: SocketState,
|
||||
packet_router: PacketRouter,
|
||||
bandwidth_controller: Option<BandwidthController<C, St>>,
|
||||
@@ -98,7 +107,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
gateway_config: GatewayConfig,
|
||||
local_identity: Arc<identity::KeyPair>,
|
||||
// TODO: make it mandatory. if you don't want to pass it, use `new_init`
|
||||
shared_key: Option<Arc<SharedKeys>>,
|
||||
shared_key: Option<Arc<SharedGatewayKey>>,
|
||||
packet_router: PacketRouter,
|
||||
bandwidth_controller: Option<BandwidthController<C, St>>,
|
||||
task_client: TaskClient,
|
||||
@@ -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,40 +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,
|
||||
);
|
||||
)?;
|
||||
|
||||
match self.send_websocket_message(msg).await? {
|
||||
ServerResponse::Authenticate {
|
||||
@@ -495,39 +565,86 @@ 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?;
|
||||
}
|
||||
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)
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -542,7 +659,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
{
|
||||
ServerResponse::SupportedProtocol { version } => Ok(version),
|
||||
ServerResponse::Error { message } => Err(GatewayClientError::GatewayError(message)),
|
||||
_ => Err(GatewayClientError::UnexpectedResponse),
|
||||
other => Err(GatewayClientError::UnexpectedResponse { name: other.name() }),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,21 +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,
|
||||
);
|
||||
)?;
|
||||
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
|
||||
@@ -579,7 +692,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
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");
|
||||
@@ -678,10 +791,10 @@ impl<C, St> GatewayClient<C, St> {
|
||||
return Err(GatewayClientError::ConnectionNotEstablished);
|
||||
}
|
||||
|
||||
let messages: Vec<_> = packets
|
||||
let messages: Result<Vec<_>, _> = packets
|
||||
.into_iter()
|
||||
.map(|mix_packet| {
|
||||
BinaryRequest::new_forward_request(mix_packet).into_ws_message(
|
||||
BinaryRequest::ForwardSphinx { packet: mix_packet }.into_ws_message(
|
||||
self.shared_key
|
||||
.as_ref()
|
||||
.expect("no shared key present even though we're authenticated!"),
|
||||
@@ -690,7 +803,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
.collect();
|
||||
|
||||
if let Err(err) = self
|
||||
.batch_send_websocket_messages_without_response(messages)
|
||||
.batch_send_websocket_messages_without_response(messages?)
|
||||
.await
|
||||
{
|
||||
if err.is_closed_connection() && self.cfg.connection.should_reconnect_on_failure {
|
||||
@@ -754,11 +867,11 @@ impl<C, St> GatewayClient<C, St> {
|
||||
}
|
||||
// note: into_ws_message encrypts the requests and adds a MAC on it. Perhaps it should
|
||||
// be more explicit in the naming?
|
||||
let msg = BinaryRequest::new_forward_request(mix_packet).into_ws_message(
|
||||
let msg = BinaryRequest::ForwardSphinx { packet: mix_packet }.into_ws_message(
|
||||
self.shared_key
|
||||
.as_ref()
|
||||
.expect("no shared key present even though we're authenticated!"),
|
||||
);
|
||||
)?;
|
||||
self.send_with_reconnection_on_failure(msg).await
|
||||
}
|
||||
|
||||
@@ -818,8 +931,8 @@ impl<C, St> GatewayClient<C, St> {
|
||||
self.establish_connection().await?;
|
||||
}
|
||||
|
||||
// TODO: the name of this method is very deceiving
|
||||
self.perform_initial_authentication().await?;
|
||||
// if we're reconnecting, because we lost connection, we need to re-authenticate the connection
|
||||
self.authenticate().await?;
|
||||
|
||||
// this call is NON-blocking
|
||||
self.start_listening_for_mixnet_messages()?;
|
||||
@@ -833,16 +946,16 @@ impl<C, St> GatewayClient<C, St> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn authenticate_and_start(&mut self) -> Result<Arc<SharedKeys>, GatewayClientError>
|
||||
pub async fn claim_initial_bandwidth(&mut self) -> Result<(), GatewayClientError>
|
||||
where
|
||||
C: DkgQueryClient + Send + Sync,
|
||||
St: CredentialStorage,
|
||||
<St as CredentialStorage>::StorageError: Send + Sync + 'static,
|
||||
{
|
||||
if !self.connection.is_established() {
|
||||
self.establish_connection().await?;
|
||||
if !self.authenticated {
|
||||
return Err(GatewayClientError::NotAuthenticated);
|
||||
}
|
||||
let shared_key = self.perform_initial_authentication().await?;
|
||||
|
||||
let bandwidth_remaining = self.bandwidth.remaining();
|
||||
if bandwidth_remaining < self.cfg.bandwidth.remaining_bandwidth_threshold {
|
||||
self.cfg
|
||||
@@ -851,6 +964,20 @@ impl<C, St> GatewayClient<C, St> {
|
||||
info!("Claiming more bandwidth with existing credentials. Stop the process now if you don't want that to happen.");
|
||||
self.claim_bandwidth().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[deprecated(note = "this method does not deal with upgraded keys for legacy clients")]
|
||||
pub async fn authenticate_and_start(
|
||||
&mut self,
|
||||
) -> Result<AuthenticationResponse, GatewayClientError>
|
||||
where
|
||||
C: DkgQueryClient + Send + Sync,
|
||||
St: CredentialStorage,
|
||||
<St as CredentialStorage>::StorageError: Send + Sync + 'static,
|
||||
{
|
||||
let shared_key = self.perform_initial_authentication().await?;
|
||||
self.claim_initial_bandwidth().await?;
|
||||
|
||||
// this call is NON-blocking
|
||||
self.start_listening_for_mixnet_messages()?;
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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 }
|
||||
@@ -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,525 +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,
|
||||
SupportedProtocol {},
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
ClientControlRequest::SupportedProtocol { .. } => "SupportedProtocol".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> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
#[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,
|
||||
},
|
||||
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(),
|
||||
}
|
||||
}
|
||||
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;
|
||||
@@ -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.
|
||||
@@ -330,7 +332,7 @@ impl Storage for PersistentStorage {
|
||||
async fn insert_shared_keys(
|
||||
&self,
|
||||
client_address: DestinationAddressBytes,
|
||||
shared_keys: &SharedKeys,
|
||||
shared_keys: &SharedGatewayKey,
|
||||
) -> Result<i64, StorageError> {
|
||||
let client_id = self
|
||||
.client_manager
|
||||
@@ -340,7 +342,8 @@ impl Storage for PersistentStorage {
|
||||
.insert_shared_keys(
|
||||
client_id,
|
||||
client_address.as_base58_string(),
|
||||
shared_keys.to_base58_string(),
|
||||
shared_keys.aes128_ctr_hmac_bs58().as_deref(),
|
||||
shared_keys.aes256_gcm_siv().as_deref(),
|
||||
)
|
||||
.await?;
|
||||
Ok(client_id)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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?;
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use nym_bin_common::build_information::{BinaryBuildInformation, BinaryBuildInfor
|
||||
pub struct UserAgent {
|
||||
pub application: String,
|
||||
pub version: String,
|
||||
pub system: Option<String>,
|
||||
pub platform: String,
|
||||
pub git_commit: String,
|
||||
}
|
||||
@@ -17,10 +18,11 @@ pub struct UserAgent {
|
||||
impl fmt::Display for UserAgent {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let abbreviated_commit = self.git_commit.chars().take(7).collect::<String>();
|
||||
let system = self.system.clone().unwrap_or_else(|| "unknown".to_string());
|
||||
write!(
|
||||
f,
|
||||
"{}/{}/{}/{}",
|
||||
self.application, self.version, self.platform, abbreviated_commit
|
||||
"{}/{}/{}/{}/{}",
|
||||
self.application, self.version, system, self.platform, abbreviated_commit
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -38,6 +40,7 @@ impl From<BinaryBuildInformation> for UserAgent {
|
||||
UserAgent {
|
||||
application: build_info.binary_name.to_string(),
|
||||
version: build_info.build_version.to_string(),
|
||||
system: None,
|
||||
platform: build_info.cargo_triple.to_string(),
|
||||
git_commit: build_info.commit_sha.to_string(),
|
||||
}
|
||||
@@ -49,6 +52,7 @@ impl From<BinaryBuildInformationOwned> for UserAgent {
|
||||
UserAgent {
|
||||
application: build_info.binary_name,
|
||||
version: build_info.build_version,
|
||||
system: None,
|
||||
platform: build_info.cargo_triple,
|
||||
git_commit: build_info.commit_sha,
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ tokio-stream = { workspace = true } # this one seems to be a thing until `Stream
|
||||
workspace = true
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio-util]
|
||||
version = "0.7.12"
|
||||
workspace = true
|
||||
features = ["time"]
|
||||
|
||||
[target."cfg(target_arch = \"wasm32\")".dependencies.wasmtimer]
|
||||
|
||||
@@ -616,6 +616,7 @@ fn compute_kappa(
|
||||
}
|
||||
|
||||
/// Represents the unique payment information associated with the payment.
|
||||
///
|
||||
/// The bytes representing the payment information encode the public key of the
|
||||
/// provider with whom you are spending the payment, timestamp and a unique random 32 bytes.
|
||||
///
|
||||
|
||||
@@ -581,6 +581,7 @@ impl TryFrom<&[u8]> for KeyPair {
|
||||
}
|
||||
|
||||
/// Generate a single Coconut keypair ((x, y0, y1...), (g2^x, g2^y0, ...)).
|
||||
///
|
||||
/// It is not suitable for threshold credentials as all subsequent calls to `keygen` generate keys
|
||||
/// that are independent of each other.
|
||||
pub fn keygen(params: &Parameters) -> KeyPair {
|
||||
@@ -599,6 +600,8 @@ pub fn keygen(params: &Parameters) -> KeyPair {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate Coconut keypairs.
|
||||
///
|
||||
/// Generate a set of n Coconut keypairs [((x, y0, y1...), (g2^x, g2^y0, ...)), ...],
|
||||
/// such that they support threshold aggregation by `threshold` number of parties.
|
||||
/// It is expected that this procedure is executed by a Trusted Third Party.
|
||||
|
||||
@@ -42,7 +42,7 @@ nym-crypto = { path = "../crypto", version = "0.4.0", features = [
|
||||
path = "framing"
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
|
||||
version = "1.39.3"
|
||||
workspace = true
|
||||
features = ["sync"]
|
||||
|
||||
[features]
|
||||
|
||||
@@ -14,7 +14,7 @@ generic-array = { workspace = true, optional = true, features = ["serde"] }
|
||||
thiserror = { workspace = true }
|
||||
zeroize = { workspace = true }
|
||||
|
||||
nym-crypto = { path = "../../crypto", features = ["symmetric", "rand"] }
|
||||
nym-crypto = { path = "../../crypto", features = ["stream_cipher", "rand"] }
|
||||
nym-pemstore = { path = "../../pemstore" }
|
||||
nym-sphinx-addressing = { path = "../addressing" }
|
||||
nym-sphinx-params = { path = "../params" }
|
||||
|
||||
@@ -12,6 +12,8 @@ use thiserror::Error;
|
||||
pub type NodeIdentity = identity::PublicKey;
|
||||
pub const NODE_IDENTITY_SIZE: usize = identity::PUBLIC_KEY_LENGTH;
|
||||
|
||||
/// Encodoing and decoding node routing information.
|
||||
///
|
||||
/// This module is responsible for encoding and decoding node routing information, so that
|
||||
/// they could be later put into an appropriate field in a sphinx header.
|
||||
/// Currently, that routing information is an IP address, but in principle it can be anything
|
||||
|
||||
@@ -13,7 +13,7 @@ bs58 = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
nym-crypto = { path = "../../crypto", features = ["symmetric", "rand"] }
|
||||
nym-crypto = { path = "../../crypto", features = ["stream_cipher", "rand"] }
|
||||
nym-sphinx-addressing = { path = "../addressing" }
|
||||
nym-sphinx-params = { path = "../params" }
|
||||
nym-sphinx-routing = { path = "../routing" }
|
||||
|
||||
@@ -22,6 +22,8 @@ use std::fmt::{self, Debug, Formatter};
|
||||
// (the current limitation for making the seemingly trivial change is "linked id" which
|
||||
// has to have same amount of space available and right now it only has 31 bits available)
|
||||
|
||||
/// Length of the header of a `Fragment` when it's part of a single `FragmentSet`.
|
||||
///
|
||||
/// When the underlying message has to be split into multiple Fragments, but still manages to fit
|
||||
/// into a single `FragmentSet`, each `FragmentHeader` needs to hold additional information to allow
|
||||
/// for correct message reconstruction: 4 bytes for set id, 1 byte to represent total number
|
||||
@@ -29,6 +31,8 @@ use std::fmt::{self, Debug, Formatter};
|
||||
/// and finally an extra byte to indicate the fragment has no links to other sets.
|
||||
pub const UNLINKED_FRAGMENTED_HEADER_LEN: usize = 7;
|
||||
|
||||
/// Length of the header of a `Fragment` when it's linked.
|
||||
///
|
||||
/// Logically almost identical to `UNLINKED_FRAGMENTED_HEADER_LEN`, however, the extra three
|
||||
/// bytes are due to changing the final byte that used to indicate the `Fragment` is not linked
|
||||
/// into 4 byte id of either previous or the next set.
|
||||
@@ -111,6 +115,7 @@ impl FragmentIdentifier {
|
||||
}
|
||||
|
||||
/// The basic unit of division of underlying bytes message sent through the mix network.
|
||||
///
|
||||
/// Each `Fragment` after being marshaled is guaranteed to fit into a single sphinx packet.
|
||||
/// The `Fragment` itself consists of part, or whole of, message to be sent as well as additional
|
||||
/// header used to reconstruct the message after being received.
|
||||
@@ -283,6 +288,8 @@ impl Fragment {
|
||||
}
|
||||
}
|
||||
|
||||
/// Header of a `Fragment` that is used to be able to reconstruct the original message.
|
||||
///
|
||||
/// In order to be able to re-assemble fragmented message sent through a mix-network, some
|
||||
/// metadata is attached alongside the actual message payload. The idea is to include as little
|
||||
/// of that data as possible due to computationally very costly process of sphinx encapsulation.
|
||||
|
||||
@@ -14,6 +14,8 @@ pub const fn max_unlinked_set_payload_length(max_plaintext_size: usize) -> usize
|
||||
u8::MAX as usize * unlinked_fragment_payload_max_len(max_plaintext_size)
|
||||
}
|
||||
|
||||
/// Maximum payload length for a set that is being linked to another one, but is not the last one.
|
||||
///
|
||||
/// If the set is being linked to another one, by either being the very first set, or the very last,
|
||||
/// one of its `Fragment`s must be changed from "unlinked" into "linked" to compensate for a tiny
|
||||
/// bit extra data overhead: id of the other set.
|
||||
@@ -25,6 +27,8 @@ pub const fn max_one_way_linked_set_payload_length(max_plaintext_size: usize) ->
|
||||
- (LINKED_FRAGMENTED_HEADER_LEN - UNLINKED_FRAGMENTED_HEADER_LEN)
|
||||
}
|
||||
|
||||
/// Set payload length for a set that is being linked to another one, but is not the last one.
|
||||
///
|
||||
/// If the set is being linked two others sets by being stuck in the middle of divided message,
|
||||
/// two of its `Fragment`s (first and final one) must be changed from
|
||||
/// "unlinked" into "linked" to compensate for data overhead.
|
||||
|
||||
@@ -11,7 +11,7 @@ repository = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
|
||||
nym-crypto = { path = "../../crypto", features = ["hashing", "symmetric"] }
|
||||
nym-crypto = { path = "../../crypto", features = ["hashing", "stream_cipher", "aead"] }
|
||||
nym-sphinx-types = { path = "../types" }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use nym_crypto::aes::Aes128;
|
||||
use nym_crypto::blake3;
|
||||
use nym_crypto::ctr;
|
||||
use nym_crypto::Aes256GcmSiv;
|
||||
|
||||
type Aes128Ctr = ctr::Ctr64BE<Aes128>;
|
||||
|
||||
@@ -48,7 +49,7 @@ pub type GatewaySharedKeyHkdfAlgorithm = blake3::Hasher;
|
||||
pub type ReplySurbKeyDigestAlgorithm = blake3::Hasher;
|
||||
|
||||
/// Hashing algorithm used when computing integrity (H)Mac for message exchanged between client and gateway.
|
||||
// TODO: if updated, the pem type defined in gateway\gateway-requests\src\registration\handshake\shared_key
|
||||
// TODO: if updated, the pem type defined in gateway\gateway-requests\src\registration\handshake\legacy_shared_key
|
||||
// needs updating!
|
||||
pub type GatewayIntegrityHmacAlgorithm = blake3::Hasher;
|
||||
|
||||
@@ -59,11 +60,16 @@ pub type GatewayIntegrityHmacAlgorithm = blake3::Hasher;
|
||||
// - the pem type defined in nym\common\nymsphinx\acknowledgements\src\key needs updating!
|
||||
pub type AckEncryptionAlgorithm = Aes128Ctr;
|
||||
|
||||
/// Legacy encryption algorithm used for end-to-end encryption of messages exchanged between clients
|
||||
/// and their gateways.
|
||||
// TODO: if updated, the pem type defined in gateway\gateway-requests\src\registration\handshake\legacy_shared_key
|
||||
// needs updating!
|
||||
pub type LegacyGatewayEncryptionAlgorithm = Aes128Ctr;
|
||||
|
||||
/// Encryption algorithm used for end-to-end encryption of messages exchanged between clients
|
||||
/// and their gateways.
|
||||
// TODO: if updated, the pem type defined in gateway\gateway-requests\src\registration\handshake\shared_key
|
||||
// needs updating!
|
||||
pub type GatewayEncryptionAlgorithm = Aes128Ctr;
|
||||
// NOTE: if updated, the pem type defined in gateway\gateway-requests\src\registration\handshake\shared_key
|
||||
pub type GatewayEncryptionAlgorithm = Aes256GcmSiv;
|
||||
|
||||
/// Encryption algorithm used for end-to-end encryption of messages exchanged between clients that are
|
||||
/// encapsulated inside sphinx packets.
|
||||
|
||||
@@ -302,6 +302,8 @@ pub trait FragmentPreparer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepares the message that is to be sent through the mix network.
|
||||
///
|
||||
/// Prepares the message that is to be sent through the mix network by attaching
|
||||
/// an optional reply-SURB, padding it to appropriate length, encrypting its content,
|
||||
/// and chunking into appropriate size [`Fragment`]s.
|
||||
|
||||
@@ -10,9 +10,11 @@ use nym_task::connections::{ConnectionCommand, ConnectionCommandSender};
|
||||
use nym_task::TaskClient;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
/// A generic message produced after reading from a socket/connection. It includes data that was
|
||||
/// actually read alongside boolean indicating whether the connection got closed so that
|
||||
/// remote could act accordingly.
|
||||
/// A generic message produced after reading from a socket/connection.
|
||||
///
|
||||
/// A generic message produced after reading from a socket/connection that includes data that was
|
||||
/// actually read alongside boolean indicating whether the connection got closed so that remote
|
||||
/// could act accordingly.
|
||||
#[derive(Debug)]
|
||||
pub struct ConnectionMessage {
|
||||
pub payload: Vec<u8>,
|
||||
@@ -20,6 +22,7 @@ pub struct ConnectionMessage {
|
||||
}
|
||||
|
||||
/// Channel responsible for sending data that was received from mix network into particular connection.
|
||||
///
|
||||
/// Data includes the actual payload that is to be written onto the connection
|
||||
/// alongside boolean indicating whether the remote connection was closed after producing this message,
|
||||
/// so that the local connection should also shut down.
|
||||
|
||||
@@ -11,13 +11,13 @@ documentation = { workspace = true }
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true, optional = true }
|
||||
bs58 = { workspace = true }
|
||||
log = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["json"] }
|
||||
semver = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
async-trait = { workspace = true, optional = true }
|
||||
semver = { version = "0.11" }
|
||||
|
||||
# 'serializable' feature
|
||||
serde = { workspace = true, features = ["derive"], optional = true }
|
||||
|
||||
@@ -18,4 +18,4 @@ log.workspace = true
|
||||
nym-wireguard-types = { path = "../wireguard-types", optional = true }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
tokio-tun = "0.11.5"
|
||||
tokio-tun.workspace = true
|
||||
|
||||
@@ -15,6 +15,8 @@ use nym_client_core::client::key_manager::persistence::KeyStore;
|
||||
use nym_client_core::client::key_manager::ClientKeys;
|
||||
use nym_client_core::client::replies::reply_storage::browser_backend;
|
||||
use nym_credential_storage::ephemeral_storage::EphemeralStorage as EphemeralCredentialStorage;
|
||||
use nym_crypto::asymmetric::ed25519::PublicKey;
|
||||
use nym_gateway_client::SharedSymmetricKey;
|
||||
use wasm_utils::console_log;
|
||||
|
||||
// temporary until other variants are properly implemented (probably it should get changed into `ClientStorage`
|
||||
@@ -43,8 +45,18 @@ impl MixnetClientStorage for FullWasmClientStorage {
|
||||
|
||||
type GatewaysDetailsStore = ClientStorage;
|
||||
|
||||
fn into_runtime_stores(self) -> (Self::ReplyStore, Self::CredentialStore) {
|
||||
(self.reply_storage, self.credential_storage)
|
||||
fn into_runtime_stores(
|
||||
self,
|
||||
) -> (
|
||||
Self::ReplyStore,
|
||||
Self::CredentialStore,
|
||||
Self::GatewaysDetailsStore,
|
||||
) {
|
||||
(
|
||||
self.reply_storage,
|
||||
self.credential_storage,
|
||||
self.keys_and_gateway_store,
|
||||
)
|
||||
}
|
||||
|
||||
fn key_store(&self) -> &Self::KeyStore {
|
||||
@@ -142,6 +154,19 @@ impl GatewaysDetailsStore for ClientStorage {
|
||||
self.store_registered_gateway(&raw_registration).await
|
||||
}
|
||||
|
||||
async fn upgrade_stored_remote_gateway_key(
|
||||
&self,
|
||||
gateway_id: PublicKey,
|
||||
updated_key: &SharedSymmetricKey,
|
||||
) -> Result<(), Self::StorageError> {
|
||||
self.update_remote_gateway_key(
|
||||
&gateway_id.to_base58_string(),
|
||||
None,
|
||||
Some(updated_key.as_bytes()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn remove_gateway_details(&self, gateway_id: &str) -> Result<(), Self::StorageError> {
|
||||
self.remove_registered_gateway(gateway_id).await
|
||||
}
|
||||
|
||||
@@ -4,17 +4,24 @@
|
||||
use nym_client_core::client::base_client::storage::gateways_storage::{
|
||||
BadGateway, GatewayDetails, GatewayRegistration, RawRemoteGatewayDetails, RemoteGatewayDetails,
|
||||
};
|
||||
use nym_gateway_client::SharedGatewayKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ops::Deref;
|
||||
use time::OffsetDateTime;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
// a more nested struct since we only have a single gateway type in wasm (no 'custom')
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Zeroize)]
|
||||
pub struct WasmRawRegisteredGateway {
|
||||
pub gateway_id_bs58: String,
|
||||
|
||||
#[zeroize(skip)]
|
||||
pub registration_timestamp: OffsetDateTime,
|
||||
|
||||
pub derived_aes128_ctr_blake3_hmac_keys_bs58: String,
|
||||
pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub derived_aes256_gcm_siv_key: Option<Vec<u8>>,
|
||||
|
||||
pub gateway_owner_address: Option<String>,
|
||||
|
||||
@@ -30,6 +37,7 @@ impl TryFrom<WasmRawRegisteredGateway> for GatewayRegistration {
|
||||
gateway_id_bs58: value.gateway_id_bs58,
|
||||
derived_aes128_ctr_blake3_hmac_keys_bs58: value
|
||||
.derived_aes128_ctr_blake3_hmac_keys_bs58,
|
||||
derived_aes256_gcm_siv_key: value.derived_aes256_gcm_siv_key,
|
||||
gateway_owner_address: value.gateway_owner_address,
|
||||
gateway_listener: value.gateway_listener,
|
||||
};
|
||||
@@ -48,17 +56,22 @@ impl<'a> From<&'a GatewayRegistration> for WasmRawRegisteredGateway {
|
||||
panic!("somehow obtained custom gateway registration in wasm!")
|
||||
};
|
||||
|
||||
let (derived_aes128_ctr_blake3_hmac_keys_bs58, derived_aes256_gcm_siv_key) =
|
||||
match remote_details.shared_key.deref() {
|
||||
SharedGatewayKey::Current(key) => (None, Some(key.to_bytes())),
|
||||
SharedGatewayKey::Legacy(key) => (Some(key.to_base58_string()), None),
|
||||
};
|
||||
|
||||
WasmRawRegisteredGateway {
|
||||
gateway_id_bs58: remote_details.gateway_id.to_string(),
|
||||
registration_timestamp: value.registration_timestamp,
|
||||
derived_aes128_ctr_blake3_hmac_keys_bs58: remote_details
|
||||
.derived_aes128_ctr_blake3_hmac_keys
|
||||
.to_base58_string(),
|
||||
derived_aes128_ctr_blake3_hmac_keys_bs58,
|
||||
derived_aes256_gcm_siv_key,
|
||||
gateway_listener: remote_details.gateway_listener.to_string(),
|
||||
gateway_owner_address: remote_details
|
||||
.gateway_owner_address
|
||||
.as_ref()
|
||||
.map(|a| a.to_string()),
|
||||
gateway_listener: remote_details.gateway_listener.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use std::error::Error;
|
||||
use thiserror::Error;
|
||||
use wasm_bindgen::JsValue;
|
||||
use wasm_storage::traits::BaseWasmStorage;
|
||||
use zeroize::Zeroize;
|
||||
|
||||
// v1 tables
|
||||
pub(crate) mod v1 {
|
||||
@@ -228,6 +229,23 @@ pub trait WasmClientStorage: BaseWasmStorage {
|
||||
.map_err(Into::into)
|
||||
}
|
||||
|
||||
async fn update_remote_gateway_key(
|
||||
&self,
|
||||
gateway_id_bs58: &str,
|
||||
derived_aes128_ctr_blake3_hmac_keys_bs58: Option<&str>,
|
||||
derived_aes256_gcm_siv_key: Option<&[u8]>,
|
||||
) -> Result<(), <Self as WasmClientStorage>::StorageError> {
|
||||
if let Some(mut current) = self.maybe_get_registered_gateway(gateway_id_bs58).await? {
|
||||
current.derived_aes128_ctr_blake3_hmac_keys_bs58 =
|
||||
derived_aes128_ctr_blake3_hmac_keys_bs58.map(|k| k.to_string());
|
||||
current.derived_aes256_gcm_siv_key = derived_aes256_gcm_siv_key.map(|k| k.to_vec());
|
||||
self.store_registered_gateway(¤t).await?;
|
||||
current.zeroize();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_registered_gateway(
|
||||
&self,
|
||||
gateway_id: &str,
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
|
||||
# Root options
|
||||
|
||||
# The graph table configures how the dependency graph is constructed and thus
|
||||
# which crates the checks are performed against
|
||||
[graph]
|
||||
# If 1 or more target triples (and optionally, target_features) are specified,
|
||||
# only the specified targets will be checked when running `cargo deny check`.
|
||||
# This means, if a particular package is only ever used as a target specific
|
||||
@@ -22,11 +25,13 @@
|
||||
targets = [
|
||||
# The triple can be any string, but only the target triples built in to
|
||||
# rustc (as of 1.40) can be checked against actual config expressions
|
||||
#{ triple = "x86_64-unknown-linux-musl" },
|
||||
#"x86_64-unknown-linux-musl",
|
||||
# You can also specify which target_features you promise are enabled for a
|
||||
# particular target. target_features are currently not validated against
|
||||
# the actual valid features supported by the target architecture.
|
||||
#{ triple = "wasm32-unknown-unknown", features = ["atomics"] },
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"wasm32-unknown-unknown",
|
||||
]
|
||||
# When creating the dependency graph used as the source of truth when checks are
|
||||
# executed, this field can be used to prune crates from the graph, removing them
|
||||
@@ -46,6 +51,9 @@ no-default-features = false
|
||||
# If set, these feature will be enabled when collecting metadata. If `--features`
|
||||
# is specified on the cmd line they will take precedence over this option.
|
||||
#features = []
|
||||
|
||||
# The output table provides options for how/if diagnostics are outputted
|
||||
[output]
|
||||
# When outputting inclusion graphs in diagnostics that include features, this
|
||||
# option can be used to specify the depth at which feature edges will be added.
|
||||
# This option is included since the graphs can be quite large and the addition
|
||||
@@ -57,37 +65,18 @@ feature-depth = 1
|
||||
# More documentation for the advisories section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html
|
||||
[advisories]
|
||||
# The path where the advisory database is cloned/fetched into
|
||||
db-path = "~/.cargo/advisory-db"
|
||||
# The path where the advisory databases are cloned/fetched into
|
||||
#db-path = "$CARGO_HOME/advisory-dbs"
|
||||
# The url(s) of the advisory databases to use
|
||||
db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
# The lint level for security vulnerabilities
|
||||
vulnerability = "deny"
|
||||
# The lint level for unmaintained crates
|
||||
unmaintained = "allow"
|
||||
# The lint level for crates that have been yanked from their source registry
|
||||
yanked = "warn"
|
||||
# The lint level for crates with security notices. Note that as of
|
||||
# 2019-12-17 there are no security notice advisories in
|
||||
# https://github.com/rustsec/advisory-db
|
||||
notice = "warn"
|
||||
#db-urls = ["https://github.com/rustsec/advisory-db"]
|
||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||
# output a note when they are encountered.
|
||||
ignore = [
|
||||
"RUSTSEC-2020-0159",
|
||||
"RUSTSEC-2020-0071",
|
||||
"RUSTSEC-2021-0145",
|
||||
#"RUSTSEC-0000-0000",
|
||||
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
|
||||
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
|
||||
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
|
||||
]
|
||||
# Threshold for security vulnerabilities, any vulnerability with a CVSS score
|
||||
# lower than the range specified will be ignored. Note that ignored advisories
|
||||
# will still output a note when they are encountered.
|
||||
# * None - CVSS Score 0.0
|
||||
# * Low - CVSS Score 0.1 - 3.9
|
||||
# * Medium - CVSS Score 4.0 - 6.9
|
||||
# * High - CVSS Score 7.0 - 8.9
|
||||
# * Critical - CVSS Score 9.0 - 10.0
|
||||
#severity-threshold =
|
||||
|
||||
# If this is true, then cargo deny will use the git executable to fetch advisory database.
|
||||
# If this is false, then it uses a built-in git library.
|
||||
# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support.
|
||||
@@ -98,8 +87,6 @@ ignore = [
|
||||
# More documentation for the licenses section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html
|
||||
[licenses]
|
||||
# The lint level for crates which do not have a detectable license
|
||||
unlicensed = "deny"
|
||||
# List of explicitly allowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
@@ -115,28 +102,7 @@ allow = [
|
||||
"Unicode-DFS-2016",
|
||||
"OpenSSL",
|
||||
"Zlib",
|
||||
"Unicode-3.0",
|
||||
]
|
||||
# List of explicitly disallowed licenses
|
||||
# See https://spdx.org/licenses/ for list of possible licenses
|
||||
# [possible values: any SPDX 3.11 short identifier (+ optional exception)].
|
||||
deny = [
|
||||
#"Nokia",
|
||||
]
|
||||
# Lint level for licenses considered copyleft
|
||||
copyleft = "warn"
|
||||
# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses
|
||||
# * both - The license will be approved if it is both OSI-approved *AND* FSF
|
||||
# * either - The license will be approved if it is either OSI-approved *OR* FSF
|
||||
# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF
|
||||
# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved
|
||||
# * neither - This predicate is ignored and the default lint level is used
|
||||
allow-osi-fsf-free = "neither"
|
||||
# Lint level used when no other predicates are matched
|
||||
# 1. License isn't in the allow or deny lists
|
||||
# 2. License isn't copyleft
|
||||
# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither"
|
||||
default = "deny"
|
||||
# The confidence threshold for detecting a license from license text.
|
||||
# The higher the value, the more closely the license text must be to the
|
||||
# canonical license text of a valid SPDX license file.
|
||||
@@ -147,17 +113,21 @@ confidence-threshold = 0.8
|
||||
exceptions = [
|
||||
# Each entry is the crate and version constraint, and its specific allow
|
||||
# list
|
||||
#{ allow = ["Zlib"], name = "adler32", version = "*" },
|
||||
#{ allow = ["Zlib"], crate = "adler32" },
|
||||
{ allow = ["GPL-3.0"], crate = "nym-api" },
|
||||
{ allow = ["GPL-3.0"], crate = "nym-gateway" },
|
||||
{ allow = ["GPL-3.0"], crate = "nym-mixnode" },
|
||||
{ allow = ["GPL-3.0"], crate = "nym-network-requester" },
|
||||
{ allow = ["GPL-3.0"], crate = "nym-node" },
|
||||
{ allow = ["GPL-3.0"], crate = "nym-validator-rewarder" },
|
||||
]
|
||||
|
||||
# Some crates don't have (easily) machine readable licensing information,
|
||||
# adding a clarification entry for it allows you to manually specify the
|
||||
# licensing information
|
||||
[[licenses.clarify]]
|
||||
# The name of the crate the clarification applies to
|
||||
name = "ring"
|
||||
# The optional version constraint for the crate
|
||||
version = "*"
|
||||
# The package spec the clarification applies to
|
||||
crate = "ring"
|
||||
# The SPDX expression for the license requirements of the crate
|
||||
expression = "MIT AND ISC AND OpenSSL"
|
||||
# One or more files in the crate's source used as the "source of truth" for
|
||||
@@ -198,26 +168,25 @@ wildcards = "allow"
|
||||
# * all - Both lowest-version and simplest-path are used
|
||||
highlight = "all"
|
||||
# The default lint level for `default` features for crates that are members of
|
||||
# the workspace that is being checked. This can be overriden by allowing/denying
|
||||
# the workspace that is being checked. This can be overridden by allowing/denying
|
||||
# `default` on a crate-by-crate basis if desired.
|
||||
workspace-default-features = "allow"
|
||||
# The default lint level for `default` features for external crates that are not
|
||||
# members of the workspace. This can be overriden by allowing/denying `default`
|
||||
# members of the workspace. This can be overridden by allowing/denying `default`
|
||||
# on a crate-by-crate basis if desired.
|
||||
external-default-features = "allow"
|
||||
# List of crates that are allowed. Use with care!
|
||||
allow = [
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is allowed" },
|
||||
]
|
||||
# List of crates to deny
|
||||
deny = [
|
||||
# Each entry the name of a crate and a version range. If version is
|
||||
# not specified, all versions will be matched.
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
#
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason it is banned" },
|
||||
# Wrapper crates can optionally be specified to allow the crate when it
|
||||
# is a direct dependency of the otherwise banned crate
|
||||
#{ name = "ansi_term", version = "=0.11.0", wrappers = [] },
|
||||
#{ crate = "ansi_term@0.11.0", wrappers = ["this-crate-directly-depends-on-ansi_term"] },
|
||||
{ name = "openssl" },
|
||||
]
|
||||
|
||||
@@ -225,7 +194,7 @@ deny = [
|
||||
# Each entry the name of a crate and a version range. If version is
|
||||
# not specified, all versions will be matched.
|
||||
#[[bans.features]]
|
||||
#name = "reqwest"
|
||||
#crate = "reqwest"
|
||||
# Features to not allow
|
||||
#deny = ["json"]
|
||||
# Features to allow
|
||||
@@ -246,16 +215,25 @@ deny = [
|
||||
|
||||
# Certain crates/versions that will be skipped when doing duplicate detection.
|
||||
skip = [
|
||||
#{ name = "ansi_term", version = "=0.11.0" },
|
||||
#"ansi_term@0.11.0",
|
||||
#{ crate = "ansi_term@0.11.0", reason = "you can specify a reason why it can't be updated/removed" },
|
||||
]
|
||||
# Similarly to `skip` allows you to skip certain crates during duplicate
|
||||
# detection. Unlike skip, it also includes the entire tree of transitive
|
||||
# dependencies starting at the specified crate, up to a certain depth, which is
|
||||
# by default infinite.
|
||||
skip-tree = [
|
||||
#{ name = "ansi_term", version = "=0.11.0", depth = 20 },
|
||||
#"ansi_term@0.11.0", # will be skipped along with _all_ of its direct and transitive dependencies
|
||||
#{ crate = "ansi_term@0.11.0", depth = 20 },
|
||||
]
|
||||
|
||||
# NOTE: re-enable once we remove the last duplicates (only digest and sha2 at
|
||||
# the time of this writing).
|
||||
#[bans.workspace-dependencies]
|
||||
#duplicates = "deny"
|
||||
#include-path-dependencies = false
|
||||
#unused = "deny"
|
||||
|
||||
# This section is considered when running `cargo deny check sources`.
|
||||
# More documentation about the 'sources' section can be found here:
|
||||
# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html
|
||||
|
||||
@@ -496,7 +496,7 @@ server {
|
||||
```
|
||||
~~~
|
||||
|
||||
4. Activate the configuration by creating a simlink to `/etc/nginx/sites-enabled`:
|
||||
4. Activate the configuration by creating a symlink to `/etc/nginx/sites-enabled`:
|
||||
```sh
|
||||
ln -s /etc/nginx/sites-available/wss-config-nym /etc/nginx/sites-enabled
|
||||
```
|
||||
@@ -512,7 +512,11 @@ systemctl restart nginx
|
||||
|
||||
```
|
||||
|
||||
7. Finally, configure your `nym-node` to announce the port you have setup. This is done by opening your node configuration file located at `~/.nym/nym-nodes/<ID>/config/config.toml` and changing the value of the line `announce_wss_port` in the `[entry_gateway]` section:
|
||||
7. Finally, configure your `nym-node` to announce the port and hostname you have setup.
|
||||
|
||||
This is done by opening your node configuration file located at `~/.nym/nym-nodes/<ID>/config/config.toml` and changing the following values :
|
||||
|
||||
- `announce_wss_port` in the `[entry_gateway]` section:
|
||||
|
||||
```
|
||||
announce_wss_port = <WSS_PORT>
|
||||
@@ -521,6 +525,12 @@ announce_wss_port = <WSS_PORT>
|
||||
# announce_wss_port = 9001
|
||||
```
|
||||
|
||||
- `hostname` in the `[host]` section:
|
||||
|
||||
```
|
||||
hostname = '<HOSTNAME>'
|
||||
```
|
||||
|
||||
8. Restart your `nym-node` :
|
||||
```sh
|
||||
systemctl restart nym-node
|
||||
|
||||
@@ -20,7 +20,8 @@ use nym_credential_verification::{
|
||||
};
|
||||
use nym_gateway_requests::{
|
||||
types::{BinaryRequest, ServerResponse},
|
||||
ClientControlRequest, GatewayRequestsError, SimpleGatewayRequestsError,
|
||||
ClientControlRequest, ClientRequest, GatewayRequestsError, SensitiveServerResponse,
|
||||
SimpleGatewayRequestsError,
|
||||
};
|
||||
use nym_gateway_storage::{error::StorageError, Storage};
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
@@ -43,9 +44,21 @@ pub enum RequestHandlingError {
|
||||
)]
|
||||
MissingClientBandwidthEntry { client_address: String },
|
||||
|
||||
#[error("received a binary request of an unknown type")]
|
||||
UnknownBinaryRequest,
|
||||
|
||||
#[error("received a text request of an unknown type")]
|
||||
UnknownTextRequest,
|
||||
|
||||
#[error("received an encrypted text request of an unknown type")]
|
||||
UnknownEncryptedTextRequest,
|
||||
|
||||
#[error("Provided binary request was malformed - {0}")]
|
||||
InvalidBinaryRequest(#[from] GatewayRequestsError),
|
||||
|
||||
#[error("failed to decrypt provided text request")]
|
||||
InvalidEncryptedTextRequest,
|
||||
|
||||
#[error("Provided binary request was malformed - {0}")]
|
||||
InvalidTextRequest(<ClientControlRequest as TryFrom<String>>::Error),
|
||||
|
||||
@@ -286,14 +299,64 @@ where
|
||||
}
|
||||
Ok(request) => match request {
|
||||
// currently only a single type exists
|
||||
BinaryRequest::ForwardSphinx(mix_packet) => self
|
||||
.handle_forward_sphinx(mix_packet)
|
||||
.await
|
||||
.into_ws_message(),
|
||||
BinaryRequest::ForwardSphinx { packet } => {
|
||||
self.handle_forward_sphinx(packet).await.into_ws_message()
|
||||
}
|
||||
_ => RequestHandlingError::UnknownBinaryRequest.into_error_message(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async fn handle_key_upgrade(
|
||||
&mut self,
|
||||
hkdf_salt: Vec<u8>,
|
||||
client_key_digest: Vec<u8>,
|
||||
) -> Result<ServerResponse, RequestHandlingError> {
|
||||
if !self.client.shared_keys.is_legacy() {
|
||||
return Ok(ServerResponse::new_error(
|
||||
"the connection is already using an aes256-gcm-siv key",
|
||||
));
|
||||
}
|
||||
let legacy_key = self.client.shared_keys.unwrap_legacy();
|
||||
let Some(upgraded_key) = legacy_key.upgrade_verify(&hkdf_salt, &client_key_digest) else {
|
||||
return Ok(ServerResponse::new_error(
|
||||
"failed to derive matching aes256-gcm-siv key",
|
||||
));
|
||||
};
|
||||
|
||||
let updated_key = upgraded_key.into();
|
||||
self.inner
|
||||
.shared_state
|
||||
.storage
|
||||
.insert_shared_keys(self.client.address, &updated_key)
|
||||
.await?;
|
||||
|
||||
// swap the in-memory key
|
||||
self.client.shared_keys = updated_key;
|
||||
Ok(SensitiveServerResponse::KeyUpgradeAck {}.encrypt(&self.client.shared_keys)?)
|
||||
}
|
||||
|
||||
async fn handle_encrypted_text_request(
|
||||
&mut self,
|
||||
ciphertext: Vec<u8>,
|
||||
nonce: Vec<u8>,
|
||||
) -> Message {
|
||||
let Ok(req) = ClientRequest::decrypt(&ciphertext, &nonce, &self.client.shared_keys) else {
|
||||
return RequestHandlingError::InvalidEncryptedTextRequest.into_error_message();
|
||||
};
|
||||
|
||||
match req {
|
||||
ClientRequest::UpgradeKey {
|
||||
hkdf_salt,
|
||||
derived_key_digest,
|
||||
} => self
|
||||
.handle_key_upgrade(hkdf_salt, derived_key_digest)
|
||||
.await
|
||||
.into_ws_message(),
|
||||
_ => RequestHandlingError::UnknownEncryptedTextRequest.into_error_message(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to handle a text data frame websocket message.
|
||||
///
|
||||
/// Currently the bandwidth credential request is the only one we can receive after authentication.
|
||||
@@ -306,6 +369,9 @@ where
|
||||
match ClientControlRequest::try_from(raw_request) {
|
||||
Err(e) => RequestHandlingError::InvalidTextRequest(e).into_error_message(),
|
||||
Ok(request) => match request {
|
||||
ClientControlRequest::EncryptedRequest { ciphertext, nonce } => {
|
||||
self.handle_encrypted_text_request(ciphertext, nonce).await
|
||||
}
|
||||
ClientControlRequest::EcashCredential { enc_credential, iv } => self
|
||||
.handle_ecash_bandwidth(enc_credential, iv)
|
||||
.await
|
||||
@@ -350,6 +416,7 @@ where
|
||||
}
|
||||
.into_error_message()
|
||||
}
|
||||
_ => RequestHandlingError::UnknownTextRequest.into_error_message(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,16 +17,15 @@ use futures::{
|
||||
SinkExt, StreamExt,
|
||||
};
|
||||
use nym_credentials_interface::AvailableBandwidth;
|
||||
use nym_crypto::aes::cipher::crypto_common::rand_core::RngCore;
|
||||
use nym_crypto::asymmetric::identity;
|
||||
use nym_gateway_requests::authentication::encrypted_address::{
|
||||
EncryptedAddressBytes, EncryptedAddressConversionError,
|
||||
};
|
||||
use nym_gateway_requests::registration::handshake::shared_key::SharedKeyConversionError;
|
||||
use nym_gateway_requests::{
|
||||
iv::{IVConversionError, IV},
|
||||
registration::handshake::{error::HandshakeError, gateway_handshake, SharedKeys},
|
||||
registration::handshake::{error::HandshakeError, gateway_handshake},
|
||||
types::{ClientControlRequest, ServerResponse},
|
||||
BinaryResponse, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION,
|
||||
BinaryResponse, SharedGatewayKey, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION,
|
||||
};
|
||||
use nym_gateway_storage::{error::StorageError, Storage};
|
||||
use nym_mixnet_client::forwarder::MixForwardingSender;
|
||||
@@ -52,7 +51,7 @@ pub(crate) enum InitialAuthenticationError {
|
||||
MalformedStoredSharedKey {
|
||||
client_id: String,
|
||||
#[source]
|
||||
source: SharedKeyConversionError,
|
||||
source: StorageError,
|
||||
},
|
||||
|
||||
#[error("Failed to perform registration handshake: {0}")]
|
||||
@@ -68,8 +67,8 @@ pub(crate) enum InitialAuthenticationError {
|
||||
#[error("There is already an open connection to this client")]
|
||||
DuplicateConnection,
|
||||
|
||||
#[error("Provided authentication IV is malformed: {0}")]
|
||||
MalformedIV(#[from] IVConversionError),
|
||||
#[error("provided authentication IV is malformed: {0}")]
|
||||
MalformedIV(bs58::decode::Error),
|
||||
|
||||
#[error("Only 'Register' or 'Authenticate' requests are allowed")]
|
||||
InvalidRequest,
|
||||
@@ -177,9 +176,10 @@ where
|
||||
async fn perform_registration_handshake(
|
||||
&mut self,
|
||||
init_msg: Vec<u8>,
|
||||
) -> Result<SharedKeys, HandshakeError>
|
||||
) -> Result<SharedGatewayKey, HandshakeError>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin + Send,
|
||||
R: CryptoRng + RngCore + Send,
|
||||
{
|
||||
debug_assert!(self.socket_connection.is_websocket());
|
||||
match &mut self.socket_connection {
|
||||
@@ -258,7 +258,7 @@ where
|
||||
/// * `packets`: unwrapped packets that are to be pushed back to the client.
|
||||
pub(crate) async fn push_packets_to_client(
|
||||
&mut self,
|
||||
shared_keys: &SharedKeys,
|
||||
shared_keys: &SharedGatewayKey,
|
||||
packets: Vec<Vec<u8>>,
|
||||
) -> Result<(), WsError>
|
||||
where
|
||||
@@ -268,10 +268,13 @@ where
|
||||
// be more explicit in the naming?
|
||||
let messages: Vec<Result<Message, WsError>> = packets
|
||||
.into_iter()
|
||||
.map(|received_message| {
|
||||
Ok(BinaryResponse::new_pushed_mix_message(received_message)
|
||||
.into_ws_message(shared_keys))
|
||||
.filter_map(|message| {
|
||||
BinaryResponse::PushedMixMessage { message }
|
||||
.into_ws_message(shared_keys)
|
||||
.inspect_err(|err| error!("failed to encrypt client message: {err}"))
|
||||
.ok()
|
||||
})
|
||||
.map(Ok)
|
||||
.collect();
|
||||
let mut send_stream = futures::stream::iter(messages);
|
||||
match self.socket_connection {
|
||||
@@ -313,7 +316,7 @@ where
|
||||
async fn push_stored_messages_to_client(
|
||||
&mut self,
|
||||
client_address: DestinationAddressBytes,
|
||||
shared_keys: &SharedKeys,
|
||||
shared_keys: &SharedGatewayKey,
|
||||
) -> Result<(), InitialAuthenticationError>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin,
|
||||
@@ -361,44 +364,33 @@ where
|
||||
///
|
||||
/// * `client_address`: address of the client.
|
||||
/// * `encrypted_address`: encrypted address of the client, presumably encrypted using the shared keys.
|
||||
/// * `iv`: iv created for this particular encryption.
|
||||
/// * `iv`: nonce/iv created for this particular encryption.
|
||||
async fn verify_stored_shared_key(
|
||||
&self,
|
||||
client_address: DestinationAddressBytes,
|
||||
encrypted_address: EncryptedAddressBytes,
|
||||
iv: IV,
|
||||
) -> Result<Option<SharedKeys>, InitialAuthenticationError> {
|
||||
nonce: &[u8],
|
||||
) -> Result<Option<SharedGatewayKey>, InitialAuthenticationError> {
|
||||
let shared_keys = self
|
||||
.shared_state
|
||||
.storage
|
||||
.get_shared_keys(client_address)
|
||||
.await?;
|
||||
|
||||
if let Some(shared_keys) = shared_keys {
|
||||
// this should never fail as we only ever construct persisted shared keys ourselves when inserting
|
||||
// data to the storage. The only way it could fail is if we somehow changed implementation without
|
||||
// performing proper migration
|
||||
let keys = SharedKeys::try_from_base58_string(
|
||||
shared_keys.derived_aes128_ctr_blake3_hmac_keys_bs58,
|
||||
)
|
||||
.map_err(|source| {
|
||||
InitialAuthenticationError::MalformedStoredSharedKey {
|
||||
client_id: client_address.as_base58_string(),
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
let Some(stored_shared_keys) = shared_keys else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// TODO: SECURITY:
|
||||
// this is actually what we have been doing in the past, however,
|
||||
// after looking deeper into implementation it seems that only checks the encryption
|
||||
// key part of the shared keys. the MAC key might still be wrong
|
||||
// (though I don't see how could this happen unless client messed with himself
|
||||
// and I don't think it could lead to any attacks, but somebody smarter should take a look)
|
||||
if encrypted_address.verify(&client_address, &keys, &iv) {
|
||||
Ok(Some(keys))
|
||||
} else {
|
||||
Ok(None)
|
||||
let keys = SharedGatewayKey::try_from(stored_shared_keys).map_err(|source| {
|
||||
InitialAuthenticationError::MalformedStoredSharedKey {
|
||||
client_id: client_address.as_base58_string(),
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
|
||||
// LEGACY ISSUE: we're not verifying HMAC key
|
||||
if encrypted_address.verify(&client_address, &keys, nonce) {
|
||||
Ok(Some(keys))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
@@ -420,6 +412,11 @@ where
|
||||
return Ok(1);
|
||||
}
|
||||
|
||||
// a v3 gateway will understand v2 requests (legacy keys)
|
||||
if client_protocol_version == 2 {
|
||||
return Ok(2);
|
||||
}
|
||||
|
||||
// we can't handle clients with higher protocol than ours
|
||||
// (perhaps we could try to negotiate downgrade on our end? sounds like a nice future improvement)
|
||||
if client_protocol_version <= CURRENT_PROTOCOL_VERSION {
|
||||
@@ -445,13 +442,13 @@ where
|
||||
///
|
||||
/// * `client_address`: address of the client wishing to authenticate.
|
||||
/// * `encrypted_address`: ciphertext of the address of the client wishing to authenticate.
|
||||
/// * `iv`: fresh IV received with the request.
|
||||
/// * `iv`: fresh nonce/IV received with the request.
|
||||
async fn authenticate_client(
|
||||
&mut self,
|
||||
client_address: DestinationAddressBytes,
|
||||
encrypted_address: EncryptedAddressBytes,
|
||||
iv: IV,
|
||||
) -> Result<Option<SharedKeys>, InitialAuthenticationError>
|
||||
nonce: &[u8],
|
||||
) -> Result<Option<SharedGatewayKey>, InitialAuthenticationError>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
@@ -461,7 +458,7 @@ where
|
||||
);
|
||||
|
||||
let shared_keys = self
|
||||
.verify_stored_shared_key(client_address, encrypted_address, iv)
|
||||
.verify_stored_shared_key(client_address, encrypted_address, nonce)
|
||||
.await?;
|
||||
|
||||
if let Some(shared_keys) = shared_keys {
|
||||
@@ -543,7 +540,7 @@ where
|
||||
client_protocol_version: Option<u8>,
|
||||
address: String,
|
||||
enc_address: String,
|
||||
iv: String,
|
||||
raw_nonce: String,
|
||||
) -> Result<InitialAuthResult, InitialAuthenticationError>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin,
|
||||
@@ -557,7 +554,9 @@ where
|
||||
let address = DestinationAddressBytes::try_from_base58_string(address)
|
||||
.map_err(|err| InitialAuthenticationError::MalformedClientAddress(err.to_string()))?;
|
||||
let encrypted_address = EncryptedAddressBytes::try_from_base58_string(enc_address)?;
|
||||
let iv = IV::try_from_base58_string(iv)?;
|
||||
let nonce = bs58::decode(&raw_nonce)
|
||||
.into_vec()
|
||||
.map_err(InitialAuthenticationError::MalformedIV)?;
|
||||
|
||||
// Check for duplicate clients
|
||||
if let Some(client_tx) = self.active_clients_store.get_remote_client(address) {
|
||||
@@ -567,7 +566,7 @@ where
|
||||
}
|
||||
|
||||
let Some(shared_keys) = self
|
||||
.authenticate_client(address, encrypted_address, iv)
|
||||
.authenticate_client(address, encrypted_address, &nonce)
|
||||
.await?
|
||||
else {
|
||||
// it feels weird to be returning an 'Ok' here, but I didn't want to change the existing behaviour
|
||||
@@ -616,7 +615,7 @@ where
|
||||
async fn register_client(
|
||||
&mut self,
|
||||
client_address: DestinationAddressBytes,
|
||||
client_shared_keys: &SharedKeys,
|
||||
client_shared_keys: &SharedGatewayKey,
|
||||
) -> Result<i64, InitialAuthenticationError>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin,
|
||||
@@ -665,6 +664,7 @@ where
|
||||
) -> Result<InitialAuthResult, InitialAuthenticationError>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin + Send,
|
||||
R: CryptoRng + RngCore + Send,
|
||||
{
|
||||
let negotiated_protocol = self.negotiate_client_protocol(client_protocol_version)?;
|
||||
// populate the negotiated protocol for future uses
|
||||
@@ -720,6 +720,7 @@ where
|
||||
) -> Result<Option<ClientDetails>, InitialAuthenticationError>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin + Send,
|
||||
R: CryptoRng + RngCore + Send,
|
||||
{
|
||||
// we can handle stateless client requests without prior authentication, like `ClientControlRequest::SupportedProtocol`
|
||||
let auth_result = match request {
|
||||
@@ -785,6 +786,7 @@ where
|
||||
) -> Option<AuthenticatedHandler<R, S, St>>
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin + Send,
|
||||
R: CryptoRng + RngCore + Send,
|
||||
{
|
||||
while !shutdown.is_shutdown() {
|
||||
let req = tokio::select! {
|
||||
@@ -871,6 +873,7 @@ where
|
||||
pub(crate) async fn start_handling(self)
|
||||
where
|
||||
S: AsyncRead + AsyncWrite + Unpin + Send,
|
||||
R: CryptoRng + RngCore + Send,
|
||||
{
|
||||
super::handle_connection(self).await
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use crate::config::Config;
|
||||
use nym_credential_verification::BandwidthFlushingBehaviourConfig;
|
||||
use nym_gateway_requests::registration::handshake::SharedKeys;
|
||||
use nym_gateway_requests::shared_key::SharedGatewayKey;
|
||||
use nym_gateway_requests::ServerResponse;
|
||||
use nym_gateway_storage::Storage;
|
||||
use nym_sphinx::DestinationAddressBytes;
|
||||
@@ -46,11 +46,15 @@ pub(crate) struct ClientDetails {
|
||||
#[zeroize(skip)]
|
||||
pub(crate) address: DestinationAddressBytes,
|
||||
pub(crate) id: i64,
|
||||
pub(crate) shared_keys: SharedKeys,
|
||||
pub(crate) shared_keys: SharedGatewayKey,
|
||||
}
|
||||
|
||||
impl ClientDetails {
|
||||
pub(crate) fn new(id: i64, address: DestinationAddressBytes, shared_keys: SharedKeys) -> Self {
|
||||
pub(crate) fn new(
|
||||
id: i64,
|
||||
address: DestinationAddressBytes,
|
||||
shared_keys: SharedGatewayKey,
|
||||
) -> Self {
|
||||
ClientDetails {
|
||||
address,
|
||||
id,
|
||||
@@ -88,7 +92,7 @@ impl InitialAuthResult {
|
||||
#[instrument(level = "debug", skip_all, fields(peer = %handle.peer_address))]
|
||||
pub(crate) async fn handle_connection<R, S, St>(mut handle: FreshHandler<R, S, St>)
|
||||
where
|
||||
R: Rng + CryptoRng,
|
||||
R: Rng + CryptoRng + Send,
|
||||
S: AsyncRead + AsyncWrite + Unpin + Send,
|
||||
St: Storage + Clone + 'static,
|
||||
{
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ sysinfo = { workspace = true }
|
||||
time.workspace = true
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "net", "signal"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
toml = "0.5.8"
|
||||
toml = { workspace = true }
|
||||
url = { workspace = true, features = ["serde"] }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
|
||||
@@ -11,17 +11,16 @@ bs58 = { workspace = true }
|
||||
cosmrs = { workspace = true }
|
||||
cosmwasm-std = { workspace = true }
|
||||
getset = { workspace = true }
|
||||
rocket = { workspace = true, optional = true }
|
||||
schemars = { workspace = true, features = ["preserve_order"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
ts-rs = { workspace = true, optional = true }
|
||||
sha2.workspace = true
|
||||
tendermint = { workspace = true }
|
||||
time = { workspace = true, features = ["serde", "parsing", "formatting"] }
|
||||
thiserror.workspace = true
|
||||
rocket = { workspace = true, optional = true }
|
||||
time = { workspace = true, features = ["serde", "parsing", "formatting"] }
|
||||
ts-rs = { workspace = true, optional = true }
|
||||
utoipa.workspace = true
|
||||
|
||||
sha2 = "0.10.8"
|
||||
|
||||
# for serde on secp256k1 signatures
|
||||
ecdsa = { workspace = true, features = ["serde"] }
|
||||
|
||||
|
||||
@@ -301,6 +301,10 @@ impl PacketSender {
|
||||
// and it wasn't shared with anyone, therefore we're the only one holding reference to it
|
||||
// and hence it's impossible to fail to obtain the permit.
|
||||
let mut unlocked_client = new_client.lock_client_unchecked();
|
||||
|
||||
// SAFETY: it's fine to use the deprecated method here as we're creating brand new clients each time,
|
||||
// and there's no need to deal with any key upgrades
|
||||
#[allow(deprecated)]
|
||||
match tokio::time::timeout(
|
||||
gateway_connection_timeout,
|
||||
unlocked_client.get_mut_unchecked().authenticate_and_start(),
|
||||
|
||||
@@ -4,17 +4,18 @@
|
||||
|
||||
set -ex
|
||||
|
||||
private_key=${1}
|
||||
network=${2:-mainnet}
|
||||
timeout=${3:-600}
|
||||
users=${4:-10}
|
||||
_private_key=${PRIVATE_KEY}
|
||||
network=${NYM_NETWORK:-mainnet}
|
||||
timeout=${LOCUST_TIMEOUT:-600}
|
||||
users=${LOCUST_USERS:-10}
|
||||
processes=${LOCUST_PROCESSES:-4}
|
||||
|
||||
RUST_LOG=info nym-network-monitor --env envs/"${network}".env --host 127.0.0.1 --port 8080 --private-key "${private_key}" &
|
||||
RUST_LOG=info nym-network-monitor --env envs/"${network}".env --private-key "${_private_key}" &
|
||||
nnm_pid=$!
|
||||
|
||||
sleep 10
|
||||
|
||||
python -m locust -H http://127.0.0.1:8080 --processes 4 --autostart --autoquit 60 -u "${users}" -t "${timeout}"s &
|
||||
python -m locust -H http://127.0.0.1:8080 --processes "${processes}" --autostart --autoquit 60 -u "${users}" -t "${timeout}"s &
|
||||
locust_pid=$!
|
||||
|
||||
wait $locust_pid
|
||||
|
||||
@@ -55,9 +55,10 @@ pub fn lion_transform_decrypt(message: &mut [u8], key: &[u8]) -> Result<(), Outf
|
||||
lion_transform(message, key, [3, 2, 1])
|
||||
}
|
||||
|
||||
/// The core of the lion transform function, that takes a message and a key,
|
||||
/// and applies the all-or-nothing transform. The key schedule represents the
|
||||
/// values of the 3 subkeys used by the 3 phases of the transform.
|
||||
/// The core of the lion transform function.
|
||||
///
|
||||
/// Takes a message and a key, and applies the all-or-nothing transform. The key schedule
|
||||
/// represents the values of the 3 subkeys used by the 3 phases of the transform.
|
||||
///
|
||||
/// The `key` must be 32 bytes, and the `message` >= 48.
|
||||
///
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user