Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6e62e34ac8 | |||
| 18e72c90df | |||
| fd051540aa | |||
| 46268edf9c | |||
| f2091cc9d6 |
@@ -8,7 +8,7 @@ env:
|
||||
|
||||
jobs:
|
||||
build-container:
|
||||
runs-on: arc-ubuntu-22.04-dind
|
||||
runs-on: arc-linux-latest-dind
|
||||
steps:
|
||||
- name: Login to Harbor
|
||||
uses: docker/login-action@v3
|
||||
|
||||
@@ -63,3 +63,5 @@ nym-api/redocly/formatted-openapi.json
|
||||
|
||||
**/settings.sql
|
||||
**/enter_db.sh
|
||||
|
||||
*.profraw
|
||||
Generated
+467
-323
File diff suppressed because it is too large
Load Diff
+8
-2
@@ -87,7 +87,9 @@ members = [
|
||||
"common/nymsphinx/params",
|
||||
"common/nymsphinx/routing",
|
||||
"common/nymsphinx/types",
|
||||
"common/nyxd-scraper",
|
||||
"common/nyxd-scraper-sqlite",
|
||||
"common/nyxd-scraper-psql",
|
||||
"common/nyxd-scraper-shared",
|
||||
"common/pemstore",
|
||||
"common/registration",
|
||||
"common/serde-helpers",
|
||||
@@ -124,6 +126,7 @@ members = [
|
||||
"nym-credential-proxy/nym-credential-proxy",
|
||||
"nym-credential-proxy/nym-credential-proxy-requests",
|
||||
"nym-credential-proxy/vpn-api-lib-wasm",
|
||||
"nym-data-observatory",
|
||||
"nym-ip-packet-client",
|
||||
"nym-network-monitor",
|
||||
"nym-node",
|
||||
@@ -263,6 +266,7 @@ futures = "0.3.31"
|
||||
futures-util = "0.3"
|
||||
generic-array = "0.14.7"
|
||||
getrandom = "0.2.10"
|
||||
glob = "0.3"
|
||||
handlebars = "3.5.5"
|
||||
hex = "0.4.3"
|
||||
hickory-resolver = "0.25"
|
||||
@@ -398,7 +402,9 @@ cw-multi-test = "=2.3.2"
|
||||
bip32 = { version = "0.5.3", default-features = false }
|
||||
|
||||
|
||||
cosmrs = { version = "0.21.1" }
|
||||
cosmrs = { version = "0.22.0" }
|
||||
cosmos-sdk-proto = { version = "0.27.0" }
|
||||
ibc-proto = { version = "0.52.0" }
|
||||
tendermint = "0.40.4"
|
||||
tendermint-rpc = "0.40.4"
|
||||
prost = { version = "0.13", default-features = false }
|
||||
|
||||
+2
-2
@@ -6,14 +6,14 @@
|
||||
{
|
||||
"name": "exists",
|
||||
"ordinal": 0,
|
||||
"type_info": "Int"
|
||||
"type_info": "Integer"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "06e743d143fcc4be20ca2af5e99b19f15d22fff72490473587a14cdc046fda32"
|
||||
|
||||
-44
@@ -1,44 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT * FROM remote_gateway_details WHERE gateway_id_bs58 = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "gateway_id_bs58",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "gateway_owner_address",
|
||||
"ordinal": 1,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "gateway_listener",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "derived_aes128_ctr_blake3_hmac_keys_bs58",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "derived_aes256_gcm_siv_key",
|
||||
"ordinal": 4,
|
||||
"type_info": "Blob"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "0e85ec18da67cf4e3df04ad80136571f6e920eb2290f20b1b8c5b0ab4b489985"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n UPDATE remote_gateway_details\n SET\n derived_aes128_ctr_blake3_hmac_keys_bs58 = ?,\n derived_aes256_gcm_siv_key = ?\n WHERE gateway_id_bs58 = ?\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0f1dfb89f1eb39f4a58787af0f53a7a93afb7e4d2e54e2d38fd79d31c8575a54"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO custom_gateway_details(gateway_id_bs58, data) \n VALUES (?, ?)\n ",
|
||||
"query": "\n INSERT INTO custom_gateway_details(gateway_id_bs58, data)\n VALUES (?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -8,5 +8,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "b059bc3688b6b7f83f47048db9897720fd4e6f3211bf74030a9638f7bf6738e4"
|
||||
"hash": "2c113b37864f9fec7e64c0f8fdd38edcdf149acfd38c56a4db3bbf97bdb13210"
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "SELECT\n rgd.gateway_id_bs58,\n derived_aes256_gcm_siv_key,\n gateway_listener,\n fallback_listener\n FROM\n remote_gateway_details AS rgd\n INNER JOIN\n remote_gateway_shared_keys AS rgsk\n ON\n rgd.gateway_id_bs58 = rgsk.gateway_id_bs58\n WHERE\n rgd.gateway_id_bs58 = ?",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"name": "gateway_id_bs58",
|
||||
"ordinal": 0,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "derived_aes256_gcm_siv_key",
|
||||
"ordinal": 1,
|
||||
"type_info": "Blob"
|
||||
},
|
||||
{
|
||||
"name": "gateway_listener",
|
||||
"ordinal": 2,
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"name": "fallback_listener",
|
||||
"ordinal": 3,
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Right": 1
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "4b739e12ea8d917cb17580337caeabb05f0e3ddbec04fdfa111d0fc86ba75505"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO remote_gateway_shared_keys(gateway_id_bs58, derived_aes256_gcm_siv_key)\n VALUES (?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 2
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "700a75acbcd90c74baa7823c40739a8ff8a26400c1d2bd45a689970bf1ba0e66"
|
||||
}
|
||||
+2
-2
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO registered_gateway(gateway_id_bs58, registration_timestamp, gateway_type) \n VALUES (?, ?, ?)\n ",
|
||||
"query": "\n INSERT INTO registered_gateway(gateway_id_bs58, registration_timestamp, gateway_type)\n VALUES (?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -8,5 +8,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "8909fd329e7e5fb16c4989b15b3d3a12bba1569520e01f6f074178e23d6ee89e"
|
||||
"hash": "727598e516090da6d26e36d09062b60ccb76d6468f359891428c0bfb96ddd7ef"
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n INSERT INTO remote_gateway_details(gateway_id_bs58, gateway_listener, fallback_listener)\n VALUES (?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 3
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a64a557ba87d4b2c7457857afa7ebc7d4f895fc4991da18ec02c9e250bea0fe0"
|
||||
}
|
||||
-12
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"db_name": "SQLite",
|
||||
"query": "\n 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)\n VALUES (?, ?, ?, ?, ?)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Right": 5
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a6939bea03b10cde810a9a099bd597b4f51092e30a41c4085a8f8668f039f7c0"
|
||||
}
|
||||
@@ -9,7 +9,6 @@ rust-version.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
cosmrs.workspace = true
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
thiserror.workspace = true
|
||||
time.workspace = true
|
||||
@@ -20,6 +19,7 @@ zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
|
||||
nym-crypto = { path = "../../crypto", features = ["asymmetric"] }
|
||||
nym-gateway-requests = { path = "../../gateway-requests" }
|
||||
nym-gateway-client = { path = "../../client-libs/gateway-client" }
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
|
||||
workspace = true
|
||||
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
CREATE TABLE remote_gateway_details_temp
|
||||
(
|
||||
gateway_id_bs58 TEXT NOT NULL UNIQUE PRIMARY KEY REFERENCES registered_gateway (gateway_id_bs58),
|
||||
derived_aes256_gcm_siv_key BLOB NOT NULL,
|
||||
gateway_listener TEXT NOT NULL,
|
||||
fallback_listener TEXT,
|
||||
expiration_timestamp DATETIME NOT NULL
|
||||
);
|
||||
|
||||
-- keep only registrations with a non null aes256 key
|
||||
INSERT INTO remote_gateway_details_temp SELECT gateway_id_bs58, derived_aes256_gcm_siv_key, gateway_listener, NULL, datetime(0, 'unixepoch') FROM remote_gateway_details WHERE derived_aes256_gcm_siv_key IS NOT NULL;
|
||||
|
||||
DROP TABLE remote_gateway_details;
|
||||
ALTER TABLE remote_gateway_details_temp RENAME TO remote_gateway_details;
|
||||
|
||||
-- delete registrations with no key
|
||||
DELETE FROM registered_gateway WHERE gateway_id_bs58 NOT IN ( SELECT gateway_id_bs58 FROM remote_gateway_details);
|
||||
@@ -6,6 +6,7 @@ use crate::{
|
||||
types::{
|
||||
RawActiveGateway, RawCustomGatewayDetails, RawRegisteredGateway, RawRemoteGatewayDetails,
|
||||
},
|
||||
RawGatewayPublishedData,
|
||||
};
|
||||
use sqlx::{
|
||||
sqlite::{SqliteAutoVacuum, SqliteSynchronous},
|
||||
@@ -144,13 +145,11 @@ impl StorageManager {
|
||||
&self,
|
||||
gateway_id: &str,
|
||||
) -> Result<RawRemoteGatewayDetails, sqlx::Error> {
|
||||
sqlx::query_as!(
|
||||
RawRemoteGatewayDetails,
|
||||
"SELECT * FROM remote_gateway_details WHERE gateway_id_bs58 = ?",
|
||||
gateway_id
|
||||
)
|
||||
.fetch_one(&self.connection_pool)
|
||||
.await
|
||||
// query_as! macro doesn't use fromRow
|
||||
sqlx::query_as("SELECT * FROM remote_gateway_details WHERE gateway_id_bs58 = ?")
|
||||
.bind(gateway_id)
|
||||
.fetch_one(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn set_remote_gateway_details(
|
||||
@@ -159,41 +158,36 @@ impl StorageManager {
|
||||
) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
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)
|
||||
INSERT INTO remote_gateway_details(gateway_id_bs58, derived_aes256_gcm_siv_key, gateway_listener, fallback_listener, expiration_timestamp)
|
||||
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,
|
||||
remote.published_data.gateway_listener,
|
||||
remote.published_data.fallback_listener,
|
||||
remote.published_data.expiration_timestamp
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn update_remote_gateway_key(
|
||||
pub(crate) async fn update_remote_gateway_published_data(
|
||||
&self,
|
||||
gateway_id_bs58: &str,
|
||||
derived_aes128_ctr_blake3_hmac_keys_bs58: Option<&str>,
|
||||
derived_aes256_gcm_siv_key: Option<&[u8]>,
|
||||
published_data: &RawGatewayPublishedData,
|
||||
) -> 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 = ?
|
||||
UPDATE remote_gateway_details SET gateway_listener = ?, fallback_listener = ?, expiration_timestamp = ? WHERE gateway_id_bs58 = ?
|
||||
"#,
|
||||
derived_aes128_ctr_blake3_hmac_keys_bs58,
|
||||
derived_aes256_gcm_siv_key,
|
||||
published_data.gateway_listener,
|
||||
published_data.fallback_listener,
|
||||
published_data.expiration_timestamp,
|
||||
gateway_id_bs58
|
||||
)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -2,18 +2,16 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{
|
||||
ActiveGateway, BadGateway, GatewayDetails, GatewayRegistration, GatewayType,
|
||||
GatewaysDetailsStore, StorageError,
|
||||
ActiveGateway, BadGateway, GatewayDetails, GatewayPublishedData, GatewayRegistration,
|
||||
GatewayType, GatewaysDetailsStore, StorageError,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use manager::StorageManager;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_gateway_requests::SharedSymmetricKey;
|
||||
use std::path::Path;
|
||||
|
||||
pub mod error;
|
||||
mod manager;
|
||||
mod models;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct OnDiskGatewaysDetails {
|
||||
@@ -134,16 +132,15 @@ impl GatewaysDetailsStore for OnDiskGatewaysDetails {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn upgrade_stored_remote_gateway_key(
|
||||
async fn update_gateway_published_data(
|
||||
&self,
|
||||
gateway_id: ed25519::PublicKey,
|
||||
updated_key: &SharedSymmetricKey,
|
||||
gateway_id: &ed25519::PublicKey,
|
||||
published_data: &GatewayPublishedData,
|
||||
) -> Result<(), Self::StorageError> {
|
||||
self.manager
|
||||
.update_remote_gateway_key(
|
||||
.update_remote_gateway_published_data(
|
||||
&gateway_id.to_base58_string(),
|
||||
None,
|
||||
Some(updated_key.as_bytes()),
|
||||
&published_data.into(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
@@ -2,10 +2,9 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::types::{ActiveGateway, GatewayRegistration};
|
||||
use crate::{BadGateway, GatewayDetails, GatewaysDetailsStore};
|
||||
use crate::{BadGateway, GatewayDetails, GatewayPublishedData, GatewaysDetailsStore};
|
||||
use async_trait::async_trait;
|
||||
use nym_crypto::asymmetric::ed25519::PublicKey;
|
||||
use nym_gateway_requests::{SharedGatewayKey, SharedSymmetricKey};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
use thiserror::Error;
|
||||
@@ -96,26 +95,17 @@ impl GatewaysDetailsStore for InMemGatewaysDetails {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn upgrade_stored_remote_gateway_key(
|
||||
async fn update_gateway_published_data(
|
||||
&self,
|
||||
gateway_id: PublicKey,
|
||||
updated_key: &SharedSymmetricKey,
|
||||
gateway_id: &ed25519::PublicKey,
|
||||
published_data: &GatewayPublishedData,
|
||||
) -> 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(),
|
||||
))
|
||||
if let Some(gateway) = guard.gateways.get_mut(&gateway_id.to_base58_string()) {
|
||||
if let GatewayDetails::Remote(ref mut remote_details) = gateway.details {
|
||||
remote_details.published_data = published_data.clone();
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -18,16 +18,6 @@ pub enum BadGateway {
|
||||
source: Ed25519RecoveryError,
|
||||
},
|
||||
|
||||
#[error("the account owner of gateway {gateway_id} ({raw_owner}) is malformed: {source}")]
|
||||
MalformedGatewayOwnerAccountAddress {
|
||||
gateway_id: String,
|
||||
|
||||
raw_owner: String,
|
||||
|
||||
#[source]
|
||||
source: cosmrs::ErrorReport,
|
||||
},
|
||||
|
||||
#[error("the shared keys provided for gateway {gateway_id} are malformed: {source}")]
|
||||
MalformedSharedKeys {
|
||||
gateway_id: String,
|
||||
@@ -50,4 +40,12 @@ pub enum BadGateway {
|
||||
#[source]
|
||||
source: url::ParseError,
|
||||
},
|
||||
|
||||
#[error("the listening address ({raw_listener}) is malformed: {source}")]
|
||||
MalformedListenerNoId {
|
||||
raw_listener: String,
|
||||
|
||||
#[source]
|
||||
source: url::ParseError,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
use async_trait::async_trait;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_gateway_requests::SharedSymmetricKey;
|
||||
use std::error::Error;
|
||||
|
||||
pub mod backend;
|
||||
@@ -60,10 +59,11 @@ pub trait GatewaysDetailsStore {
|
||||
details: &GatewayRegistration,
|
||||
) -> Result<(), Self::StorageError>;
|
||||
|
||||
async fn upgrade_stored_remote_gateway_key(
|
||||
/// Update the gateway details
|
||||
async fn update_gateway_published_data(
|
||||
&self,
|
||||
gateway_id: ed25519::PublicKey,
|
||||
updated_key: &SharedSymmetricKey,
|
||||
gateway_id: &ed25519::PublicKey,
|
||||
published_data: &GatewayPublishedData,
|
||||
) -> Result<(), Self::StorageError>;
|
||||
|
||||
/// Remove given gateway details from the underlying store.
|
||||
|
||||
@@ -2,20 +2,21 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::BadGateway;
|
||||
use cosmrs::AccountId;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey};
|
||||
use nym_gateway_client::client::GatewayListeners;
|
||||
use nym_gateway_requests::shared_key::SharedSymmetricKey;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::{Display, Formatter};
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use time::Duration;
|
||||
use time::OffsetDateTime;
|
||||
use url::Url;
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop};
|
||||
|
||||
pub const REMOTE_GATEWAY_TYPE: &str = "remote";
|
||||
pub const CUSTOM_GATEWAY_TYPE: &str = "custom";
|
||||
const GATEWAY_DETAILS_TTL: Duration = Duration::days(7);
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ActiveGateway {
|
||||
@@ -65,15 +66,13 @@ impl From<GatewayDetails> for GatewayRegistration {
|
||||
impl GatewayDetails {
|
||||
pub fn new_remote(
|
||||
gateway_id: ed25519::PublicKey,
|
||||
shared_key: Arc<SharedGatewayKey>,
|
||||
gateway_owner_address: Option<AccountId>,
|
||||
gateway_listener: Url,
|
||||
shared_key: Arc<SharedSymmetricKey>,
|
||||
published_data: GatewayPublishedData,
|
||||
) -> Self {
|
||||
GatewayDetails::Remote(RemoteGatewayDetails {
|
||||
gateway_id,
|
||||
shared_key,
|
||||
gateway_owner_address,
|
||||
gateway_listener,
|
||||
published_data,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -88,13 +87,20 @@ impl GatewayDetails {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn shared_key(&self) -> Option<&SharedGatewayKey> {
|
||||
pub fn shared_key(&self) -> Option<&SharedSymmetricKey> {
|
||||
match self {
|
||||
GatewayDetails::Remote(details) => Some(&details.shared_key),
|
||||
GatewayDetails::Custom(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn details_exipration(&self) -> Option<OffsetDateTime> {
|
||||
match self {
|
||||
GatewayDetails::Remote(details) => Some(details.published_data.expiration_timestamp),
|
||||
GatewayDetails::Custom(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_custom(&self) -> bool {
|
||||
matches!(self, GatewayDetails::Custom(..))
|
||||
}
|
||||
@@ -164,14 +170,78 @@ pub struct RegisteredGateway {
|
||||
pub gateway_type: GatewayType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GatewayPublishedData {
|
||||
pub listeners: GatewayListeners,
|
||||
pub expiration_timestamp: OffsetDateTime,
|
||||
}
|
||||
|
||||
impl GatewayPublishedData {
|
||||
pub fn new(listeners: GatewayListeners) -> GatewayPublishedData {
|
||||
GatewayPublishedData {
|
||||
listeners,
|
||||
expiration_timestamp: OffsetDateTime::now_utc() + GATEWAY_DETAILS_TTL,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
|
||||
pub struct RawGatewayPublishedData {
|
||||
pub gateway_listener: String,
|
||||
pub fallback_listener: Option<String>,
|
||||
pub expiration_timestamp: OffsetDateTime,
|
||||
}
|
||||
|
||||
impl<'a> From<&'a GatewayPublishedData> for RawGatewayPublishedData {
|
||||
fn from(value: &'a GatewayPublishedData) -> Self {
|
||||
Self {
|
||||
gateway_listener: value.listeners.primary.to_string(),
|
||||
fallback_listener: value.listeners.fallback.as_ref().map(|uri| uri.to_string()),
|
||||
expiration_timestamp: value.expiration_timestamp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<RawGatewayPublishedData> for GatewayPublishedData {
|
||||
type Error = BadGateway;
|
||||
|
||||
fn try_from(value: RawGatewayPublishedData) -> Result<Self, Self::Error> {
|
||||
let gateway_listener: Url = Url::parse(&value.gateway_listener).map_err(|source| {
|
||||
BadGateway::MalformedListenerNoId {
|
||||
raw_listener: value.gateway_listener.clone(),
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
let fallback_listener = value
|
||||
.fallback_listener
|
||||
.as_ref()
|
||||
.map(|uri| {
|
||||
Url::parse(uri).map_err(|source| BadGateway::MalformedListenerNoId {
|
||||
raw_listener: uri.to_owned(),
|
||||
source,
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
Ok(GatewayPublishedData {
|
||||
listeners: GatewayListeners {
|
||||
primary: gateway_listener,
|
||||
fallback: fallback_listener,
|
||||
},
|
||||
expiration_timestamp: value.expiration_timestamp,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Zeroize, ZeroizeOnDrop, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "sqlx", derive(sqlx::FromRow))]
|
||||
pub struct RawRemoteGatewayDetails {
|
||||
pub gateway_id_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,
|
||||
pub derived_aes256_gcm_siv_key: Vec<u8>,
|
||||
#[zeroize(skip)]
|
||||
#[cfg_attr(feature = "sqlx", sqlx(flatten))]
|
||||
pub published_data: RawGatewayPublishedData,
|
||||
}
|
||||
|
||||
impl TryFrom<RawRemoteGatewayDetails> for RemoteGatewayDetails {
|
||||
@@ -186,81 +256,26 @@ impl TryFrom<RawRemoteGatewayDetails> for RemoteGatewayDetails {
|
||||
}
|
||||
})?;
|
||||
|
||||
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
|
||||
.as_ref()
|
||||
.map(|raw_owner| {
|
||||
AccountId::from_str(raw_owner).map_err(|source| {
|
||||
BadGateway::MalformedGatewayOwnerAccountAddress {
|
||||
gateway_id: value.gateway_id_bs58.clone(),
|
||||
raw_owner: raw_owner.clone(),
|
||||
source,
|
||||
}
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
let gateway_listener = Url::parse(&value.gateway_listener).map_err(|source| {
|
||||
BadGateway::MalformedListener {
|
||||
let shared_key = SharedSymmetricKey::try_from_bytes(&value.derived_aes256_gcm_siv_key)
|
||||
.map_err(|source| BadGateway::MalformedSharedKeys {
|
||||
gateway_id: value.gateway_id_bs58.clone(),
|
||||
raw_listener: value.gateway_listener.clone(),
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
})?;
|
||||
|
||||
Ok(RemoteGatewayDetails {
|
||||
gateway_id,
|
||||
shared_key: Arc::new(shared_key),
|
||||
gateway_owner_address,
|
||||
gateway_listener,
|
||||
published_data: value.published_data.clone().try_into()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
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(),
|
||||
derived_aes256_gcm_siv_key: value.shared_key.to_bytes(),
|
||||
published_data: (&value.published_data).into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -269,11 +284,9 @@ impl<'a> From<&'a RemoteGatewayDetails> for RawRemoteGatewayDetails {
|
||||
pub struct RemoteGatewayDetails {
|
||||
pub gateway_id: ed25519::PublicKey,
|
||||
|
||||
pub shared_key: Arc<SharedGatewayKey>,
|
||||
pub shared_key: Arc<SharedSymmetricKey>,
|
||||
|
||||
pub gateway_owner_address: Option<AccountId>,
|
||||
|
||||
pub gateway_listener: Url,
|
||||
pub published_data: GatewayPublishedData,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
@@ -87,6 +87,7 @@ where
|
||||
user_chosen_gateway_id.map(|id| id.to_base58_string()),
|
||||
Some(common_args.latency_based_selection),
|
||||
common_args.force_tls_gateway,
|
||||
false,
|
||||
);
|
||||
tracing::debug!("Gateway selection specification: {selection_spec:?}");
|
||||
|
||||
@@ -167,6 +168,7 @@ where
|
||||
identity: gateway_details.gateway_id,
|
||||
active: common_args.set_active,
|
||||
typ: gateway_registration.details.typ().to_string(),
|
||||
endpoint: Some(gateway_details.gateway_listener.clone()),
|
||||
endpoint: Some(gateway_details.published_data.listeners.primary.clone()),
|
||||
fallback_endpoint: gateway_details.published_data.listeners.fallback.clone(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -140,6 +140,7 @@ where
|
||||
user_chosen_gateway_id.map(|id| id.to_base58_string()),
|
||||
Some(common_args.latency_based_selection),
|
||||
common_args.force_tls_gateway,
|
||||
false,
|
||||
);
|
||||
tracing::debug!("Gateway selection specification: {selection_spec:?}");
|
||||
|
||||
|
||||
@@ -56,7 +56,8 @@ where
|
||||
identity: remote_details.gateway_id,
|
||||
active: active_gateway == Some(remote_details.gateway_id),
|
||||
typ: GatewayType::Remote.to_string(),
|
||||
endpoint: Some(remote_details.gateway_listener),
|
||||
endpoint: Some(remote_details.published_data.listeners.primary.clone()),
|
||||
fallback_endpoint: remote_details.published_data.listeners.fallback.clone(),
|
||||
}),
|
||||
GatewayDetails::Custom(_) => info.push(GatewayInfo {
|
||||
registration: gateway.registration_timestamp,
|
||||
@@ -64,6 +65,7 @@ where
|
||||
active: active_gateway == Some(gateway.details.gateway_id()),
|
||||
typ: gateway.details.typ().to_string(),
|
||||
endpoint: None,
|
||||
fallback_endpoint: None,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ pub struct GatewayInfo {
|
||||
|
||||
pub typ: String,
|
||||
pub endpoint: Option<Url>,
|
||||
pub fallback_endpoint: Option<Url>,
|
||||
}
|
||||
|
||||
impl Display for GatewayInfo {
|
||||
@@ -30,6 +31,9 @@ impl Display for GatewayInfo {
|
||||
if let Some(endpoint) = &self.endpoint {
|
||||
write!(f, " endpoint: {endpoint}")?;
|
||||
}
|
||||
if let Some(fallback_endpoint) = &self.fallback_endpoint {
|
||||
write!(f, " fallback: {fallback_endpoint}")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -529,7 +529,6 @@ where
|
||||
config: &Config,
|
||||
initialisation_result: InitialisationResult,
|
||||
bandwidth_controller: Option<BandwidthController<C, S::CredentialStore>>,
|
||||
details_store: &S::GatewaysDetailsStore,
|
||||
packet_router: PacketRouter,
|
||||
stats_reporter: ClientStatsSender,
|
||||
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
|
||||
@@ -555,14 +554,7 @@ where
|
||||
shutdown_tracker.clone_shutdown_token(),
|
||||
)
|
||||
} else {
|
||||
let cfg = GatewayConfig::new(
|
||||
details.gateway_id,
|
||||
details
|
||||
.gateway_owner_address
|
||||
.as_ref()
|
||||
.map(|o| o.to_string()),
|
||||
details.gateway_listener.to_string(),
|
||||
);
|
||||
let cfg = GatewayConfig::new(details.gateway_id, details.published_data.listeners);
|
||||
GatewayClient::new(
|
||||
GatewayClientConfig::new_default()
|
||||
.with_disabled_credentials_mode(config.client.disabled_credentials_mode)
|
||||
@@ -592,32 +584,13 @@ where
|
||||
// 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
|
||||
let _ = gateway_client
|
||||
.perform_initial_authentication()
|
||||
.await
|
||||
.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| {
|
||||
tracing::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
|
||||
@@ -636,7 +609,6 @@ where
|
||||
config: &Config,
|
||||
initialisation_result: InitialisationResult,
|
||||
bandwidth_controller: Option<BandwidthController<C, S::CredentialStore>>,
|
||||
details_store: &S::GatewaysDetailsStore,
|
||||
packet_router: PacketRouter,
|
||||
stats_reporter: ClientStatsSender,
|
||||
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
|
||||
@@ -667,7 +639,6 @@ where
|
||||
config,
|
||||
initialisation_result,
|
||||
bandwidth_controller,
|
||||
details_store,
|
||||
packet_router,
|
||||
stats_reporter,
|
||||
#[cfg(unix)]
|
||||
@@ -975,8 +946,7 @@ where
|
||||
)
|
||||
.await?;
|
||||
|
||||
let (reply_storage_backend, credential_store, details_store) =
|
||||
self.client_store.into_runtime_stores();
|
||||
let (reply_storage_backend, credential_store, _) = self.client_store.into_runtime_stores();
|
||||
|
||||
// channels for inter-component communication
|
||||
// TODO: make the channels be internally created by the relevant components
|
||||
@@ -1069,7 +1039,6 @@ where
|
||||
&self.config,
|
||||
init_res,
|
||||
bandwidth_controller,
|
||||
&details_store,
|
||||
gateway_packet_router,
|
||||
stats_reporter.clone(),
|
||||
#[cfg(unix)]
|
||||
|
||||
@@ -4,7 +4,9 @@
|
||||
use crate::client::key_manager::persistence::KeyStore;
|
||||
use crate::client::key_manager::ClientKeys;
|
||||
use crate::error::ClientCoreError;
|
||||
use nym_client_core_gateways_storage::{ActiveGateway, GatewayRegistration, GatewaysDetailsStore};
|
||||
use nym_client_core_gateways_storage::{
|
||||
ActiveGateway, GatewayPublishedData, GatewayRegistration, GatewaysDetailsStore,
|
||||
};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
|
||||
// helpers for error wrapping
|
||||
@@ -85,6 +87,23 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn update_stored_published_data_gateway<D>(
|
||||
details_store: &D,
|
||||
gateway_id: &ed25519::PublicKey,
|
||||
published_data: &GatewayPublishedData,
|
||||
) -> Result<(), ClientCoreError>
|
||||
where
|
||||
D: GatewaysDetailsStore,
|
||||
D::StorageError: Send + Sync + 'static,
|
||||
{
|
||||
details_store
|
||||
.update_gateway_published_data(gateway_id, published_data)
|
||||
.await
|
||||
.map_err(|source| ClientCoreError::GatewaysDetailsStoreError {
|
||||
source: Box::new(source),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn load_active_gateway_details<D>(
|
||||
details_store: &D,
|
||||
) -> Result<ActiveGateway, ClientCoreError>
|
||||
|
||||
@@ -2,210 +2,18 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod v1_1_33 {
|
||||
use crate::client::base_client::{
|
||||
non_wasm_helpers::setup_fs_gateways_storage,
|
||||
storage::helpers::{set_active_gateway, store_gateway_details},
|
||||
};
|
||||
use crate::config::disk_persistence::old_v1_1_33::CommonClientPathsV1_1_33;
|
||||
use crate::config::disk_persistence::CommonClientPaths;
|
||||
use crate::config::old_config_v1_1_33::OldGatewayEndpointConfigV1_1_33;
|
||||
use crate::error::ClientCoreError;
|
||||
use nym_client_core_gateways_storage::{
|
||||
CustomGatewayDetails, GatewayDetails, GatewayRegistration, RemoteGatewayDetails,
|
||||
};
|
||||
use nym_gateway_requests::shared_key::LegacySharedKeys;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{digest::Digest, Sha256};
|
||||
use std::ops::Deref;
|
||||
use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
mod base64 {
|
||||
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||
use serde::{Deserialize, Deserializer, Serializer};
|
||||
|
||||
pub fn serialize<S: Serializer>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
|
||||
serializer.serialize_str(&STANDARD.encode(bytes))
|
||||
}
|
||||
|
||||
pub fn deserialize<'de, D: Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<Vec<u8>, D::Error> {
|
||||
let s = <String>::deserialize(deserializer)?;
|
||||
STANDARD.decode(s).map_err(serde::de::Error::custom)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum PersistedGatewayDetails {
|
||||
/// Standard details of a remote gateway
|
||||
Default(PersistedGatewayConfig),
|
||||
|
||||
/// Custom gateway setup, such as for a client embedded inside gateway itself
|
||||
Custom(PersistedCustomGatewayDetails),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
struct PersistedGatewayConfig {
|
||||
/// The hash of the shared keys to ensure the correct ones are used with those gateway details.
|
||||
#[serde(with = "base64")]
|
||||
key_hash: Vec<u8>,
|
||||
|
||||
/// Actual gateway details being persisted.
|
||||
details: OldGatewayEndpointConfigV1_1_33,
|
||||
}
|
||||
|
||||
impl PersistedGatewayConfig {
|
||||
fn verify(&self, shared_key: &LegacySharedKeys) -> bool {
|
||||
let key_bytes = Zeroizing::new(shared_key.to_bytes());
|
||||
|
||||
let mut key_hasher = Sha256::new();
|
||||
key_hasher.update(&key_bytes);
|
||||
let key_hash = key_hasher.finalize();
|
||||
|
||||
self.key_hash == key_hash.deref()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct PersistedCustomGatewayDetails {
|
||||
gateway_id: String,
|
||||
}
|
||||
|
||||
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)?)
|
||||
}
|
||||
|
||||
fn gateway_details_from_raw(
|
||||
gateway_id: String,
|
||||
gateway_owner: String,
|
||||
gateway_listener: String,
|
||||
gateway_shared_key: LegacySharedKeys,
|
||||
) -> Result<GatewayDetails, ClientCoreError> {
|
||||
Ok(GatewayDetails::Remote(RemoteGatewayDetails {
|
||||
gateway_id: gateway_id
|
||||
.parse()
|
||||
.map_err(|err| ClientCoreError::UpgradeFailure {
|
||||
message: format!("the stored gateway id was malformed: {err}"),
|
||||
})?,
|
||||
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}"),
|
||||
}
|
||||
})?),
|
||||
gateway_listener: gateway_listener.parse().map_err(|err| {
|
||||
ClientCoreError::UpgradeFailure {
|
||||
message: format!("the stored gateway listener address was malformed: {err}"),
|
||||
}
|
||||
})?,
|
||||
}))
|
||||
}
|
||||
|
||||
// helper to extract shared key and gateway details into the new GatewayRegistration
|
||||
fn extract_gateway_registration(
|
||||
storage_paths: &CommonClientPathsV1_1_33,
|
||||
) -> Result<GatewayRegistration, ClientCoreError> {
|
||||
let details_file = std::fs::File::open(&storage_paths.gateway_details).map_err(|err| {
|
||||
ClientCoreError::UpgradeFailure {
|
||||
message: format!(
|
||||
"failed to open gateway details file at {}: {err}",
|
||||
storage_paths.gateway_details.display()
|
||||
),
|
||||
}
|
||||
})?;
|
||||
|
||||
// in v1.1.33 of the clients, the gateway details struct was saved as json
|
||||
let details: PersistedGatewayDetails =
|
||||
serde_json::from_reader(details_file).map_err(|err| {
|
||||
ClientCoreError::UpgradeFailure {
|
||||
message: format!(
|
||||
"failed to deserialize gateway details from {}: {err}",
|
||||
storage_paths.gateway_details.display()
|
||||
),
|
||||
}
|
||||
})?;
|
||||
|
||||
let details = match details {
|
||||
PersistedGatewayDetails::Default(config) => {
|
||||
let gateway_shared_key =
|
||||
load_shared_key(&storage_paths.keys.gateway_shared_key_file)?;
|
||||
if !config.verify(&gateway_shared_key) {
|
||||
return Err(ClientCoreError::UpgradeFailure {
|
||||
message: "failed to verify consistency of the existing gateway details"
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
gateway_details_from_raw(
|
||||
config.details.gateway_id,
|
||||
config.details.gateway_owner,
|
||||
config.details.gateway_listener,
|
||||
gateway_shared_key,
|
||||
)?
|
||||
}
|
||||
PersistedGatewayDetails::Custom(custom) => {
|
||||
GatewayDetails::Custom(CustomGatewayDetails {
|
||||
gateway_id: custom.gateway_id.parse().map_err(|err| {
|
||||
ClientCoreError::UpgradeFailure {
|
||||
message: format!("the stored gateway id was malformed: {err}"),
|
||||
}
|
||||
})?,
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
Ok(details.into())
|
||||
}
|
||||
|
||||
// it's responsibility of the caller to ensure this is called **after** new registration has already been saved
|
||||
fn remove_old_gateway_details(storage_paths: &CommonClientPathsV1_1_33) -> std::io::Result<()> {
|
||||
std::fs::remove_file(&storage_paths.gateway_details)?;
|
||||
|
||||
if storage_paths.keys.gateway_shared_key_file.exists() {
|
||||
std::fs::remove_file(&storage_paths.keys.gateway_shared_key_file)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn migrate_gateway_details(
|
||||
old_storage_paths: &CommonClientPathsV1_1_33,
|
||||
new_storage_paths: &CommonClientPaths,
|
||||
preloaded_config: Option<OldGatewayEndpointConfigV1_1_33>,
|
||||
_old_storage_paths: &CommonClientPathsV1_1_33,
|
||||
_new_storage_paths: &CommonClientPaths,
|
||||
_preloaded_config: Option<OldGatewayEndpointConfigV1_1_33>,
|
||||
) -> Result<(), ClientCoreError> {
|
||||
let gateway_registration = match preloaded_config {
|
||||
Some(config) => {
|
||||
let gateway_shared_key =
|
||||
load_shared_key(&old_storage_paths.keys.gateway_shared_key_file)?;
|
||||
gateway_details_from_raw(
|
||||
config.gateway_id,
|
||||
config.gateway_owner,
|
||||
config.gateway_listener,
|
||||
gateway_shared_key,
|
||||
)?
|
||||
.into()
|
||||
}
|
||||
None => extract_gateway_registration(old_storage_paths)?,
|
||||
};
|
||||
|
||||
// since we're migrating to a brand new store, the store should be empty
|
||||
// and thus set the 'new' gateway as the active one
|
||||
let details_store =
|
||||
setup_fs_gateways_storage(&new_storage_paths.gateway_registrations).await?;
|
||||
store_gateway_details(&details_store, &gateway_registration).await?;
|
||||
set_active_gateway(
|
||||
&details_store,
|
||||
&gateway_registration.details.gateway_id().to_base58_string(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
remove_old_gateway_details(old_storage_paths).map_err(|err| {
|
||||
ClientCoreError::UpgradeFailure {
|
||||
message: format!("failed to remove old data: {err}"),
|
||||
}
|
||||
})
|
||||
Err(ClientCoreError::UnsupportedMigration(
|
||||
"migration of legacy keys has been removed and is no longer supported".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use nym_crypto::{
|
||||
asymmetric::{ed25519, x25519},
|
||||
hkdf::{DerivationMaterial, InvalidLength},
|
||||
};
|
||||
use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey};
|
||||
use nym_gateway_requests::shared_key::SharedSymmetricKey;
|
||||
use nym_sphinx::acknowledgements::AckKey;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use std::sync::Arc;
|
||||
@@ -106,7 +106,5 @@ fn _assert_keys_zeroize_on_drop() {
|
||||
_assert_zeroize_on_drop::<ed25519::KeyPair>();
|
||||
_assert_zeroize_on_drop::<x25519::KeyPair>();
|
||||
_assert_zeroize_on_drop::<AckKey>();
|
||||
_assert_zeroize_on_drop::<LegacySharedKeys>();
|
||||
_assert_zeroize_on_drop::<SharedSymmetricKey>();
|
||||
_assert_zeroize_on_drop::<SharedGatewayKey>();
|
||||
}
|
||||
|
||||
@@ -16,6 +16,9 @@ use std::path::PathBuf;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ClientCoreError {
|
||||
#[error("could not perform the state migration: {0}")]
|
||||
UnsupportedMigration(String),
|
||||
|
||||
#[error("I/O error: {0}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
|
||||
@@ -43,6 +46,9 @@ pub enum ClientCoreError {
|
||||
#[error("Invalid URL: {0}")]
|
||||
InvalidUrl(String),
|
||||
|
||||
#[error("node doesn't advertise ip addresses : {0}")]
|
||||
MissingIpAddress(String),
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[error("resolution failed: {0}")]
|
||||
ResolutionFailed(#[from] nym_http_api_client::ResolveError),
|
||||
@@ -163,6 +169,9 @@ pub enum ClientCoreError {
|
||||
#[error("custom selection of gateway was expected")]
|
||||
CustomGatewaySelectionExpected,
|
||||
|
||||
#[error("custom selection of gateway was unexpected")]
|
||||
UnexpectedCustomGatewaySelection,
|
||||
|
||||
#[error("the persisted gateway details were set for a custom setup")]
|
||||
UnexpectedPersistedCustomGatewayDetails,
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::error::ClientCoreError;
|
||||
use crate::init::types::RegistrationResult;
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_gateway_client::client::GatewayListeners;
|
||||
use nym_gateway_client::GatewayClient;
|
||||
use nym_topology::node::RoutingNode;
|
||||
use nym_validator_client::client::{IdentityKeyRef, NymApiClientExt};
|
||||
@@ -379,12 +380,12 @@ pub(super) fn get_specified_gateway(
|
||||
|
||||
pub(super) async fn register_with_gateway(
|
||||
gateway_id: ed25519::PublicKey,
|
||||
gateway_listener: Url,
|
||||
gateway_listeners: GatewayListeners,
|
||||
our_identity: Arc<ed25519::KeyPair>,
|
||||
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
|
||||
) -> Result<RegistrationResult, ClientCoreError> {
|
||||
let mut gateway_client = GatewayClient::new_init(
|
||||
gateway_listener,
|
||||
gateway_listeners,
|
||||
gateway_id,
|
||||
our_identity.clone(),
|
||||
#[cfg(unix)]
|
||||
@@ -409,14 +410,6 @@ pub(super) async fn register_with_gateway(
|
||||
}
|
||||
})?;
|
||||
|
||||
// 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: auth_response.initial_shared_key,
|
||||
authenticated_ephemeral_client: gateway_client,
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
use crate::client::base_client::storage::helpers::{
|
||||
has_gateway_details, load_active_gateway_details, load_client_keys, load_gateway_details,
|
||||
store_gateway_details,
|
||||
store_gateway_details, update_stored_published_data_gateway,
|
||||
};
|
||||
use crate::client::key_manager::persistence::KeyStore;
|
||||
use crate::client::key_manager::ClientKeys;
|
||||
@@ -16,8 +16,8 @@ use crate::init::helpers::{
|
||||
use crate::init::types::{
|
||||
GatewaySelectionSpecification, GatewaySetup, InitialisationResult, SelectedGateway,
|
||||
};
|
||||
use nym_client_core_gateways_storage::GatewaysDetailsStore;
|
||||
use nym_client_core_gateways_storage::{GatewayDetails, GatewayRegistration};
|
||||
use nym_client_core_gateways_storage::{GatewayPublishedData, GatewaysDetailsStore};
|
||||
use nym_gateway_client::client::InitGatewayClient;
|
||||
use nym_topology::node::RoutingNode;
|
||||
use rand::rngs::OsRng;
|
||||
@@ -71,21 +71,28 @@ where
|
||||
let mut rng = OsRng;
|
||||
|
||||
let selected_gateway = match selection_specification {
|
||||
GatewaySelectionSpecification::UniformRemote { must_use_tls } => {
|
||||
GatewaySelectionSpecification::UniformRemote {
|
||||
must_use_tls,
|
||||
no_hostname,
|
||||
} => {
|
||||
let gateway = uniformly_random_gateway(&mut rng, &available_gateways, must_use_tls)?;
|
||||
SelectedGateway::from_topology_node(gateway, must_use_tls)?
|
||||
SelectedGateway::from_topology_node(gateway, must_use_tls, no_hostname)?
|
||||
}
|
||||
GatewaySelectionSpecification::RemoteByLatency { must_use_tls } => {
|
||||
GatewaySelectionSpecification::RemoteByLatency {
|
||||
must_use_tls,
|
||||
no_hostname,
|
||||
} => {
|
||||
let gateway =
|
||||
choose_gateway_by_latency(&mut rng, &available_gateways, must_use_tls).await?;
|
||||
SelectedGateway::from_topology_node(gateway, must_use_tls)?
|
||||
SelectedGateway::from_topology_node(gateway, must_use_tls, no_hostname)?
|
||||
}
|
||||
GatewaySelectionSpecification::Specified {
|
||||
must_use_tls,
|
||||
no_hostname,
|
||||
identity,
|
||||
} => {
|
||||
let gateway = get_specified_gateway(&identity, &available_gateways, must_use_tls)?;
|
||||
SelectedGateway::from_topology_node(gateway, must_use_tls)?
|
||||
SelectedGateway::from_topology_node(gateway, must_use_tls, no_hostname)?
|
||||
}
|
||||
GatewaySelectionSpecification::Custom {
|
||||
gateway_identity,
|
||||
@@ -105,15 +112,15 @@ where
|
||||
let (gateway_details, authenticated_ephemeral_client) = match selected_gateway {
|
||||
SelectedGateway::Remote {
|
||||
gateway_id,
|
||||
gateway_owner_address,
|
||||
gateway_listener,
|
||||
|
||||
gateway_listeners,
|
||||
} => {
|
||||
// if we're using a 'normal' gateway setup, do register
|
||||
let our_identity = client_keys.identity_keypair();
|
||||
|
||||
let registration = helpers::register_with_gateway(
|
||||
gateway_id,
|
||||
gateway_listener.clone(),
|
||||
gateway_listeners.clone(),
|
||||
our_identity,
|
||||
#[cfg(unix)]
|
||||
connection_fd_callback,
|
||||
@@ -123,8 +130,7 @@ where
|
||||
GatewayDetails::new_remote(
|
||||
gateway_id,
|
||||
registration.shared_keys,
|
||||
gateway_owner_address,
|
||||
gateway_listener,
|
||||
GatewayPublishedData::new(gateway_listeners),
|
||||
),
|
||||
Some(registration.authenticated_ephemeral_client),
|
||||
)
|
||||
@@ -150,6 +156,46 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn refresh_gateway_published_data<D>(
|
||||
details_store: &D,
|
||||
registration: GatewayRegistration,
|
||||
available_gateways: Vec<RoutingNode>,
|
||||
must_use_tls: bool,
|
||||
no_hostname: bool,
|
||||
) -> Result<(), ClientCoreError>
|
||||
where
|
||||
D: GatewaysDetailsStore,
|
||||
D::StorageError: Send + Sync + 'static,
|
||||
{
|
||||
let gateway_id = registration.gateway_id().to_base58_string();
|
||||
tracing::trace!("Updating gateway details : {gateway_id}");
|
||||
|
||||
let gateway = get_specified_gateway(&gateway_id, &available_gateways, must_use_tls)?;
|
||||
let selected_gateway = SelectedGateway::from_topology_node(gateway, must_use_tls, no_hostname)?;
|
||||
|
||||
let new_gateway_listeners = match selected_gateway {
|
||||
SelectedGateway::Remote {
|
||||
gateway_listeners, ..
|
||||
} => gateway_listeners,
|
||||
SelectedGateway::Custom { .. } => {
|
||||
// this should not happen, as `from_topology_node` returns a Remote
|
||||
Err(ClientCoreError::UnexpectedCustomGatewaySelection)?
|
||||
}
|
||||
};
|
||||
|
||||
let new_published_data = GatewayPublishedData::new(new_gateway_listeners);
|
||||
|
||||
// update gateway details
|
||||
update_stored_published_data_gateway(
|
||||
details_store,
|
||||
®istration.gateway_id(),
|
||||
&new_published_data,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn use_loaded_gateway_details<K, D>(
|
||||
key_store: &K,
|
||||
details_store: &D,
|
||||
|
||||
@@ -10,12 +10,11 @@ use nym_client_core_gateways_storage::{
|
||||
GatewayRegistration, GatewaysDetailsStore, RemoteGatewayDetails,
|
||||
};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_gateway_client::client::InitGatewayClient;
|
||||
use nym_gateway_requests::shared_key::SharedGatewayKey;
|
||||
use nym_gateway_client::client::{GatewayListeners, InitGatewayClient};
|
||||
use nym_gateway_client::SharedSymmetricKey;
|
||||
use nym_sphinx::addressing::clients::Recipient;
|
||||
use nym_topology::node::RoutingNode;
|
||||
use nym_validator_client::client::IdentityKey;
|
||||
use nym_validator_client::nyxd::AccountId;
|
||||
use serde::Serialize;
|
||||
use std::fmt::{Debug, Display};
|
||||
#[cfg(unix)]
|
||||
@@ -28,9 +27,7 @@ pub enum SelectedGateway {
|
||||
Remote {
|
||||
gateway_id: ed25519::PublicKey,
|
||||
|
||||
gateway_owner_address: Option<AccountId>,
|
||||
|
||||
gateway_listener: Url,
|
||||
gateway_listeners: GatewayListeners,
|
||||
},
|
||||
Custom {
|
||||
gateway_id: ed25519::PublicKey,
|
||||
@@ -42,24 +39,40 @@ impl SelectedGateway {
|
||||
pub fn from_topology_node(
|
||||
node: RoutingNode,
|
||||
must_use_tls: bool,
|
||||
no_hostname: bool,
|
||||
) -> Result<Self, ClientCoreError> {
|
||||
// for now, let's use 'old' behaviour, if you want to change it, you can pass it up the enum stack yourself : )
|
||||
let prefer_ipv6 = false;
|
||||
|
||||
let gateway_listener = if must_use_tls {
|
||||
node.ws_entry_address_tls()
|
||||
.ok_or(ClientCoreError::UnsupportedWssProtocol {
|
||||
gateway: node.identity_key.to_base58_string(),
|
||||
})?
|
||||
let (gateway_listener, fallback_listener) = if must_use_tls {
|
||||
// WSS main, no fallback
|
||||
let primary =
|
||||
node.ws_entry_address_tls()
|
||||
.ok_or(ClientCoreError::UnsupportedWssProtocol {
|
||||
gateway: node.identity_key.to_base58_string(),
|
||||
})?;
|
||||
(primary, None)
|
||||
} else {
|
||||
node.ws_entry_address(prefer_ipv6)
|
||||
.ok_or(ClientCoreError::UnsupportedEntry {
|
||||
let (maybe_primary, fallback) =
|
||||
node.ws_entry_address_with_fallback(prefer_ipv6, no_hostname);
|
||||
(
|
||||
maybe_primary.ok_or(ClientCoreError::UnsupportedEntry {
|
||||
id: node.node_id,
|
||||
identity: node.identity_key.to_base58_string(),
|
||||
})?
|
||||
})?,
|
||||
fallback,
|
||||
)
|
||||
};
|
||||
|
||||
let gateway_listener =
|
||||
let fallback_listener_url = fallback_listener.and_then(|address| {
|
||||
Url::parse(&address)
|
||||
.inspect_err(|err| {
|
||||
tracing::warn!("Malformed fallback listener, none will be used : {err}")
|
||||
})
|
||||
.ok()
|
||||
});
|
||||
|
||||
let gateway_listener_url =
|
||||
Url::parse(&gateway_listener).map_err(|source| ClientCoreError::MalformedListener {
|
||||
gateway_id: node.identity_key.to_base58_string(),
|
||||
raw_listener: gateway_listener,
|
||||
@@ -68,8 +81,10 @@ impl SelectedGateway {
|
||||
|
||||
Ok(SelectedGateway::Remote {
|
||||
gateway_id: node.identity_key,
|
||||
gateway_owner_address: None,
|
||||
gateway_listener,
|
||||
gateway_listeners: GatewayListeners {
|
||||
primary: gateway_listener_url,
|
||||
fallback: fallback_listener_url,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -98,7 +113,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<SharedGatewayKey>,
|
||||
pub shared_keys: Arc<SharedSymmetricKey>,
|
||||
pub authenticated_ephemeral_client: InitGatewayClient,
|
||||
}
|
||||
|
||||
@@ -145,20 +160,36 @@ impl InitialisationResult {
|
||||
pub fn gateway_id(&self) -> ed25519::PublicKey {
|
||||
self.gateway_registration.details.gateway_id()
|
||||
}
|
||||
|
||||
// indicates if the remote gateway details TTL has expired
|
||||
pub fn exipred_details(&self) -> bool {
|
||||
if let Some(expiration_timestamp) = self.gateway_registration.details.details_exipration() {
|
||||
OffsetDateTime::now_utc() > expiration_timestamp
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum GatewaySelectionSpecification {
|
||||
/// Uniformly choose a random remote gateway.
|
||||
UniformRemote { must_use_tls: bool },
|
||||
UniformRemote {
|
||||
must_use_tls: bool,
|
||||
no_hostname: bool,
|
||||
},
|
||||
|
||||
/// Should the new, remote, gateway be selected based on latency.
|
||||
RemoteByLatency { must_use_tls: bool },
|
||||
RemoteByLatency {
|
||||
must_use_tls: bool,
|
||||
no_hostname: bool,
|
||||
},
|
||||
|
||||
/// Gateway with this specific identity should be chosen.
|
||||
// JS: I don't really like the name of this enum variant but couldn't think of anything better at the time
|
||||
Specified {
|
||||
must_use_tls: bool,
|
||||
no_hostname: bool,
|
||||
identity: IdentityKey,
|
||||
},
|
||||
|
||||
@@ -174,6 +205,7 @@ impl Default for GatewaySelectionSpecification {
|
||||
fn default() -> Self {
|
||||
GatewaySelectionSpecification::UniformRemote {
|
||||
must_use_tls: false,
|
||||
no_hostname: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -183,16 +215,24 @@ impl GatewaySelectionSpecification {
|
||||
gateway_identity: Option<String>,
|
||||
latency_based_selection: Option<bool>,
|
||||
must_use_tls: bool,
|
||||
no_hostname: bool,
|
||||
) -> Self {
|
||||
if let Some(identity) = gateway_identity {
|
||||
GatewaySelectionSpecification::Specified {
|
||||
identity,
|
||||
must_use_tls,
|
||||
no_hostname,
|
||||
}
|
||||
} else if let Some(true) = latency_based_selection {
|
||||
GatewaySelectionSpecification::RemoteByLatency { must_use_tls }
|
||||
GatewaySelectionSpecification::RemoteByLatency {
|
||||
must_use_tls,
|
||||
no_hostname,
|
||||
}
|
||||
} else {
|
||||
GatewaySelectionSpecification::UniformRemote { must_use_tls }
|
||||
GatewaySelectionSpecification::UniformRemote {
|
||||
must_use_tls,
|
||||
no_hostname,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -315,6 +355,7 @@ pub struct InitResults {
|
||||
pub encryption_key: String,
|
||||
pub gateway_id: String,
|
||||
pub gateway_listener: String,
|
||||
pub fallback_listener: Option<String>,
|
||||
pub gateway_registration: OffsetDateTime,
|
||||
pub address: Recipient,
|
||||
}
|
||||
@@ -332,7 +373,13 @@ impl InitResults {
|
||||
identity_key: address.identity().to_base58_string(),
|
||||
encryption_key: address.encryption_key().to_base58_string(),
|
||||
gateway_id: gateway.gateway_id.to_base58_string(),
|
||||
gateway_listener: gateway.gateway_listener.to_string(),
|
||||
gateway_listener: gateway.published_data.listeners.primary.to_string(),
|
||||
fallback_listener: gateway
|
||||
.published_data
|
||||
.listeners
|
||||
.fallback
|
||||
.as_ref()
|
||||
.map(|uri| uri.to_string()),
|
||||
gateway_registration: registration,
|
||||
address,
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ use nym_crypto::asymmetric::ed25519;
|
||||
use nym_gateway_requests::registration::handshake::client_handshake;
|
||||
use nym_gateway_requests::{
|
||||
BandwidthResponse, BinaryRequest, ClientControlRequest, ClientRequest, GatewayProtocolVersion,
|
||||
GatewayProtocolVersionExt, GatewayRequestsError, SensitiveServerResponse, ServerResponse,
|
||||
SharedGatewayKey, SharedSymmetricKey, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION,
|
||||
GatewayProtocolVersionExt, GatewayRequestsError, ServerResponse, SharedSymmetricKey,
|
||||
CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, CURRENT_PROTOCOL_VERSION,
|
||||
};
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
use nym_statistics_common::clients::connection::ConnectionStatsEvent;
|
||||
@@ -47,43 +47,39 @@ 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;
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) mod websockets;
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use websockets::connect_async;
|
||||
use crate::client::websockets::connect_async_with_fallback;
|
||||
|
||||
pub struct GatewayConfig {
|
||||
pub gateway_identity: ed25519::PublicKey,
|
||||
|
||||
// currently a dead field
|
||||
pub gateway_owner: Option<String>,
|
||||
|
||||
pub gateway_listener: String,
|
||||
pub gateway_listeners: GatewayListeners,
|
||||
}
|
||||
|
||||
impl GatewayConfig {
|
||||
pub fn new(
|
||||
gateway_identity: ed25519::PublicKey,
|
||||
gateway_owner: Option<String>,
|
||||
gateway_listener: String,
|
||||
) -> Self {
|
||||
pub fn new(gateway_identity: ed25519::PublicKey, gateway_listeners: GatewayListeners) -> Self {
|
||||
GatewayConfig {
|
||||
gateway_identity,
|
||||
gateway_owner,
|
||||
gateway_listener,
|
||||
gateway_listeners,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GatewayListeners {
|
||||
pub primary: Url,
|
||||
pub fallback: Option<Url>,
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
#[derive(Debug)]
|
||||
pub struct AuthenticationResponse {
|
||||
pub initial_shared_key: Arc<SharedGatewayKey>,
|
||||
pub requires_key_upgrade: bool,
|
||||
pub initial_shared_key: Arc<SharedSymmetricKey>,
|
||||
}
|
||||
|
||||
// TODO: this should be refactored into a state machine that keeps track of its authentication state
|
||||
@@ -92,10 +88,10 @@ pub struct GatewayClient<C, St = EphemeralCredentialStorage> {
|
||||
|
||||
authenticated: bool,
|
||||
bandwidth: ClientBandwidth,
|
||||
gateway_address: String,
|
||||
gateway_addresses: GatewayListeners,
|
||||
gateway_identity: ed25519::PublicKey,
|
||||
local_identity: Arc<ed25519::KeyPair>,
|
||||
shared_key: Option<Arc<SharedGatewayKey>>,
|
||||
shared_key: Option<Arc<SharedSymmetricKey>>,
|
||||
connection: SocketState,
|
||||
packet_router: PacketRouter,
|
||||
bandwidth_controller: Option<BandwidthController<C, St>>,
|
||||
@@ -118,7 +114,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
gateway_config: GatewayConfig,
|
||||
local_identity: Arc<ed25519::KeyPair>,
|
||||
// TODO: make it mandatory. if you don't want to pass it, use `new_init`
|
||||
shared_key: Option<Arc<SharedGatewayKey>>,
|
||||
shared_key: Option<Arc<SharedSymmetricKey>>,
|
||||
packet_router: PacketRouter,
|
||||
bandwidth_controller: Option<BandwidthController<C, St>>,
|
||||
stats_reporter: ClientStatsSender,
|
||||
@@ -129,7 +125,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
cfg,
|
||||
authenticated: false,
|
||||
bandwidth: ClientBandwidth::new_empty(),
|
||||
gateway_address: gateway_config.gateway_listener,
|
||||
gateway_addresses: gateway_config.gateway_listeners,
|
||||
gateway_identity: gateway_config.gateway_identity,
|
||||
local_identity,
|
||||
shared_key,
|
||||
@@ -148,7 +144,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
self.gateway_identity
|
||||
}
|
||||
|
||||
pub fn shared_key(&self) -> Option<Arc<SharedGatewayKey>> {
|
||||
pub fn shared_key(&self) -> Option<Arc<SharedSymmetricKey>> {
|
||||
self.shared_key.clone()
|
||||
}
|
||||
|
||||
@@ -203,12 +199,19 @@ impl<C, St> GatewayClient<C, St> {
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> {
|
||||
debug!(
|
||||
"Attempting to establish connection to gateway at: {}",
|
||||
self.gateway_address
|
||||
);
|
||||
let (ws_stream, _) = connect_async(
|
||||
&self.gateway_address,
|
||||
if let Some(fallback_url) = &self.gateway_addresses.fallback {
|
||||
debug!(
|
||||
"Attempting to establish connection to gateway at: {}, with fallback at: {fallback_url}",
|
||||
self.gateway_addresses.primary
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
"Attempting to establish connection to gateway at: {}",
|
||||
self.gateway_addresses.primary
|
||||
);
|
||||
}
|
||||
let (ws_stream, _) = connect_async_with_fallback(
|
||||
&self.gateway_addresses,
|
||||
#[cfg(unix)]
|
||||
self.connection_fd_callback.clone(),
|
||||
)
|
||||
@@ -221,7 +224,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
pub async fn establish_connection(&mut self) -> Result<(), GatewayClientError> {
|
||||
let ws_stream = match JSWebsocket::new(&self.gateway_address) {
|
||||
let ws_stream = match JSWebsocket::new(self.gateway_addresses.primary.as_ref()) {
|
||||
Ok(ws_stream) => ws_stream,
|
||||
Err(e) => {
|
||||
return Err(GatewayClientError::NetworkErrorWasm(e));
|
||||
@@ -274,7 +277,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
message: ClientRequest,
|
||||
) -> Result<(), GatewayClientError> {
|
||||
if let Some(shared_key) = self.shared_key() {
|
||||
let encrypted = message.encrypt(&*shared_key)?;
|
||||
let encrypted = message.encrypt(&shared_key)?;
|
||||
Box::pin(self.send_websocket_message_without_response(encrypted)).await?;
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -463,19 +466,14 @@ impl<C, St> GatewayClient<C, St> {
|
||||
|
||||
async fn register(
|
||||
&mut self,
|
||||
supported_gateway_protocol: Option<GatewayProtocolVersion>,
|
||||
supported_gateway_protocol: GatewayProtocolVersion,
|
||||
) -> Result<(), GatewayClientError> {
|
||||
if !self.connection.is_established() {
|
||||
return Err(GatewayClientError::ConnectionNotEstablished);
|
||||
}
|
||||
|
||||
let derive_aes256_gcm_siv_key = supported_gateway_protocol.supports_aes256_gcm_siv();
|
||||
|
||||
debug_assert!(self.connection.is_available());
|
||||
log::debug!(
|
||||
"registering with gateway. using legacy key derivation: {}",
|
||||
!derive_aes256_gcm_siv_key
|
||||
);
|
||||
log::debug!("registering 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
|
||||
@@ -525,75 +523,6 @@ impl<C, St> GatewayClient<C, St> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn upgrade_key_authenticated(
|
||||
&mut self,
|
||||
) -> Result<Zeroizing<SharedSymmetricKey>, GatewayClientError> {
|
||||
info!("*** STARTING AES128CTR-HMAC KEY UPGRADE INTO AES256GCM-SIV***");
|
||||
|
||||
if !self.connection.is_established() {
|
||||
return Err(GatewayClientError::ConnectionNotEstablished);
|
||||
}
|
||||
|
||||
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_with_response(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 send_authenticate_request_and_handle_response(
|
||||
&mut self,
|
||||
msg: ClientControlRequest,
|
||||
@@ -606,17 +535,14 @@ impl<C, St> GatewayClient<C, St> {
|
||||
upgrade_mode,
|
||||
} => {
|
||||
if protocol_version.is_future_version() {
|
||||
// SAFETY: future version is always defined
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let version = protocol_version.unwrap();
|
||||
error!("the gateway insists on using v{version} protocol which is not supported by this client");
|
||||
error!("the gateway insists on using v{protocol_version} protocol which is not supported by this client");
|
||||
return Err(GatewayClientError::AuthenticationFailure);
|
||||
}
|
||||
self.authenticated = status;
|
||||
self.bandwidth
|
||||
.update_and_maybe_log(bandwidth_remaining, upgrade_mode);
|
||||
|
||||
self.negotiated_protocol = protocol_version;
|
||||
self.negotiated_protocol = Some(protocol_version);
|
||||
log::debug!("authenticated: {status}, bandwidth remaining: {bandwidth_remaining}");
|
||||
if upgrade_mode {
|
||||
warn!("the system is currently undergoing an upgrade. some of its functionalities might be unstable")
|
||||
@@ -629,27 +555,6 @@ impl<C, St> GatewayClient<C, St> {
|
||||
}
|
||||
}
|
||||
|
||||
async fn authenticate_v1(&mut self) -> Result<(), GatewayClientError> {
|
||||
debug!("using v1 authentication");
|
||||
|
||||
let Some(shared_key) = self.shared_key.as_ref() else {
|
||||
return Err(GatewayClientError::NoSharedKeyAvailable);
|
||||
};
|
||||
|
||||
let self_address = self
|
||||
.local_identity
|
||||
.public_key()
|
||||
.derive_destination_address();
|
||||
|
||||
let msg = ClientControlRequest::new_legacy_authenticate(
|
||||
self_address,
|
||||
shared_key,
|
||||
self.cfg.bandwidth.require_tickets,
|
||||
)?;
|
||||
self.send_authenticate_request_and_handle_response(msg)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn authenticate_v2(
|
||||
&mut self,
|
||||
requested_protocol_version: GatewayProtocolVersion,
|
||||
@@ -670,30 +575,22 @@ impl<C, St> GatewayClient<C, St> {
|
||||
|
||||
async fn authenticate(
|
||||
&mut self,
|
||||
supported_gateway_protocol: Option<GatewayProtocolVersion>,
|
||||
requested_protocol_version: GatewayProtocolVersion,
|
||||
) -> Result<(), GatewayClientError> {
|
||||
if !self.connection.is_established() {
|
||||
return Err(GatewayClientError::ConnectionNotEstablished);
|
||||
}
|
||||
debug!("authenticating with gateway");
|
||||
|
||||
if supported_gateway_protocol.supports_authenticate_v2() {
|
||||
// use the highest possible protocol version the gateway has announced support for
|
||||
|
||||
// SAFETY: if announced protocol supports auth v2, it means it's properly set
|
||||
#[allow(clippy::unwrap_used)]
|
||||
self.authenticate_v2(supported_gateway_protocol.unwrap())
|
||||
.await
|
||||
} else {
|
||||
self.authenticate_v1().await
|
||||
}
|
||||
// use the highest possible protocol version the gateway has announced support for
|
||||
self.authenticate_v2(requested_protocol_version).await
|
||||
}
|
||||
|
||||
/// 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
|
||||
gateway_address = %self.gateway_addresses.primary
|
||||
)
|
||||
)]
|
||||
pub async fn perform_initial_authentication(
|
||||
@@ -704,15 +601,9 @@ impl<C, St> GatewayClient<C, St> {
|
||||
}
|
||||
|
||||
// 1. check gateway's protocol version
|
||||
let gw_protocol = match self.get_gateway_protocol().await {
|
||||
Ok(protocol) => Some(protocol),
|
||||
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?;
|
||||
None
|
||||
}
|
||||
};
|
||||
// if we failed to get this request resolved, it means the gateway is on an old version
|
||||
// that definitely does not support auth v2 or aes256gcm, so we bail
|
||||
let gw_protocol = self.get_gateway_protocol().await?;
|
||||
|
||||
debug!("supported gateway protocol: {gw_protocol:?}");
|
||||
|
||||
@@ -727,6 +618,16 @@ impl<C, St> GatewayClient<C, St> {
|
||||
if !supports_auth_v2 {
|
||||
warn!("this gateway is on an old version that doesn't support authentication v2")
|
||||
}
|
||||
|
||||
// Dropping v1 support
|
||||
if !supports_auth_v2 || !supports_aes_gcm_siv {
|
||||
// we can't continue
|
||||
return Err(GatewayClientError::IncompatibleProtocol {
|
||||
gateway: gw_protocol,
|
||||
current: CURRENT_PROTOCOL_VERSION,
|
||||
});
|
||||
}
|
||||
|
||||
if !supports_key_rotation_info {
|
||||
warn!("this gateway is on an old version that doesn't support key rotation packets")
|
||||
}
|
||||
@@ -736,7 +637,7 @@ impl<C, St> GatewayClient<C, St> {
|
||||
|
||||
let gw_protocol = if gw_protocol.is_future_version() {
|
||||
warn!("we're running outdated software as gateway is announcing protocol {gw_protocol:?} whilst we're using {}. we're going to attempt to downgrade", GatewayProtocolVersion::CURRENT);
|
||||
Some(GatewayProtocolVersion::CURRENT)
|
||||
GatewayProtocolVersion::CURRENT
|
||||
} else {
|
||||
gw_protocol
|
||||
};
|
||||
@@ -746,7 +647,6 @@ impl<C, St> GatewayClient<C, St> {
|
||||
return if let Some(shared_key) = &self.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)
|
||||
@@ -761,11 +661,8 @@ impl<C, St> GatewayClient<C, St> {
|
||||
#[allow(clippy::unwrap_used)]
|
||||
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)
|
||||
@@ -781,7 +678,6 @@ impl<C, St> GatewayClient<C, St> {
|
||||
// so no upgrades are required
|
||||
Ok(AuthenticationResponse {
|
||||
initial_shared_key: Arc::clone(shared_key),
|
||||
requires_key_upgrade: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1143,7 +1039,12 @@ impl<C, St> GatewayClient<C, St> {
|
||||
}
|
||||
|
||||
// if we're reconnecting, because we lost connection, we need to re-authenticate the connection
|
||||
self.authenticate(self.negotiated_protocol).await?;
|
||||
if let Some(negotiated_protocol) = self.negotiated_protocol {
|
||||
self.authenticate(negotiated_protocol).await?;
|
||||
} else {
|
||||
// This should never happen, because it would mean we're not registered
|
||||
return Err(GatewayClientError::NotRegistered);
|
||||
}
|
||||
|
||||
// this call is NON-blocking
|
||||
self.start_listening_for_mixnet_messages()?;
|
||||
@@ -1188,7 +1089,7 @@ pub struct InitOnly;
|
||||
impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
|
||||
// for initialisation we do not need credential storage. Though it's still a bit weird we have to set the generic...
|
||||
pub fn new_init(
|
||||
gateway_listener: Url,
|
||||
gateway_listeners: GatewayListeners,
|
||||
gateway_identity: ed25519::PublicKey,
|
||||
local_identity: Arc<ed25519::KeyPair>,
|
||||
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
|
||||
@@ -1207,7 +1108,7 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
|
||||
cfg: GatewayClientConfig::default().with_disabled_credentials_mode(true),
|
||||
authenticated: false,
|
||||
bandwidth: ClientBandwidth::new_empty(),
|
||||
gateway_address: gateway_listener.to_string(),
|
||||
gateway_addresses: gateway_listeners,
|
||||
gateway_identity,
|
||||
local_identity,
|
||||
shared_key: None,
|
||||
@@ -1239,7 +1140,7 @@ impl GatewayClient<InitOnly, EphemeralCredentialStorage> {
|
||||
cfg: self.cfg,
|
||||
authenticated: self.authenticated,
|
||||
bandwidth: self.bandwidth,
|
||||
gateway_address: self.gateway_address,
|
||||
gateway_addresses: self.gateway_addresses,
|
||||
gateway_identity: self.gateway_identity,
|
||||
local_identity: self.local_identity,
|
||||
shared_key: self.shared_key,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
use crate::client::GatewayListeners;
|
||||
use crate::error::GatewayClientError;
|
||||
|
||||
use nym_http_api_client::HickoryDnsResolver;
|
||||
@@ -85,3 +87,35 @@ pub(crate) async fn connect_async(
|
||||
source: Box::new(error),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
pub(crate) async fn connect_async_with_fallback(
|
||||
endpoints: &GatewayListeners,
|
||||
#[cfg(unix)] connection_fd_callback: Option<Arc<dyn Fn(RawFd) + Send + Sync>>,
|
||||
) -> Result<(WebSocketStream<MaybeTlsStream<TcpStream>>, Response), GatewayClientError> {
|
||||
match connect_async(
|
||||
endpoints.primary.as_ref(),
|
||||
#[cfg(unix)]
|
||||
connection_fd_callback.clone(),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(inner) => Ok(inner),
|
||||
Err(e) => {
|
||||
if let Some(fallback) = &endpoints.fallback {
|
||||
tracing::warn!(
|
||||
"Main endpoint failed {} : {e}, trying fallback : {fallback}",
|
||||
endpoints.primary
|
||||
);
|
||||
connect_async(
|
||||
fallback.as_ref(),
|
||||
#[cfg(unix)]
|
||||
connection_fd_callback,
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,6 +83,9 @@ pub enum GatewayClientError {
|
||||
#[error("Client is not authenticated")]
|
||||
NotAuthenticated,
|
||||
|
||||
#[error("Client is not registered")]
|
||||
NotRegistered,
|
||||
|
||||
#[error("Client does not have enough bandwidth: estimated {0}, remaining: {1}")]
|
||||
NotEnoughBandwidth(i64, i64),
|
||||
|
||||
@@ -116,8 +119,8 @@ pub enum GatewayClientError {
|
||||
#[error("Failed to send mixnet message")]
|
||||
MixnetMsgSenderFailedToSend,
|
||||
|
||||
#[error("Attempted to negotiate connection with gateway using incompatible protocol version. Ours is {current} and the gateway reports {gateway:?}")]
|
||||
IncompatibleProtocol { gateway: Option<u8>, current: u8 },
|
||||
#[error("Attempted to negotiate connection with gateway using incompatible protocol version. Ours is {current} and the gateway reports {gateway}")]
|
||||
IncompatibleProtocol { gateway: u8, current: u8 },
|
||||
|
||||
#[error(
|
||||
"The packet router hasn't been set - are you sure you started up the client correctly?"
|
||||
|
||||
@@ -7,9 +7,7 @@ use tracing::{error, warn};
|
||||
use tungstenite::{protocol::Message, Error as WsError};
|
||||
|
||||
pub use client::{config::GatewayClientConfig, GatewayClient, GatewayConfig};
|
||||
pub use nym_gateway_requests::shared_key::{
|
||||
LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey,
|
||||
};
|
||||
pub use nym_gateway_requests::shared_key::SharedSymmetricKey;
|
||||
pub use packet_router::{
|
||||
AcknowledgementReceiver, AcknowledgementSender, MixnetMessageReceiver, MixnetMessageSender,
|
||||
PacketRouter,
|
||||
@@ -47,7 +45,7 @@ pub(crate) fn cleanup_socket_messages(
|
||||
|
||||
pub(crate) fn try_decrypt_binary_message(
|
||||
bin_msg: Vec<u8>,
|
||||
shared_keys: &SharedGatewayKey,
|
||||
shared_keys: &SharedSymmetricKey,
|
||||
) -> Option<Vec<u8>> {
|
||||
match BinaryResponse::try_from_encrypted_tagged_bytes(bin_msg, shared_keys) {
|
||||
Ok(bin_response) => match bin_response {
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{cleanup_socket_messages, try_decrypt_binary_message};
|
||||
use futures::channel::oneshot;
|
||||
use futures::stream::{SplitSink, SplitStream};
|
||||
use futures::{SinkExt, StreamExt};
|
||||
use nym_gateway_requests::shared_key::SharedGatewayKey;
|
||||
use nym_gateway_requests::shared_key::SharedSymmetricKey;
|
||||
use nym_gateway_requests::{
|
||||
SendResponse, SensitiveServerResponse, ServerResponse, SimpleGatewayRequestsError,
|
||||
};
|
||||
@@ -66,7 +66,7 @@ pub(crate) struct PartiallyDelegatedHandle {
|
||||
|
||||
struct PartiallyDelegatedRouter {
|
||||
packet_router: PacketRouter,
|
||||
shared_key: Arc<SharedGatewayKey>,
|
||||
shared_key: Arc<SharedSymmetricKey>,
|
||||
client_bandwidth: ClientBandwidth,
|
||||
|
||||
stream_return: SplitStreamSender,
|
||||
@@ -76,7 +76,7 @@ struct PartiallyDelegatedRouter {
|
||||
impl PartiallyDelegatedRouter {
|
||||
fn new(
|
||||
packet_router: PacketRouter,
|
||||
shared_key: Arc<SharedGatewayKey>,
|
||||
shared_key: Arc<SharedSymmetricKey>,
|
||||
client_bandwidth: ClientBandwidth,
|
||||
stream_return: SplitStreamSender,
|
||||
stream_return_requester: oneshot::Receiver<()>,
|
||||
@@ -214,11 +214,6 @@ impl PartiallyDelegatedRouter {
|
||||
SensitiveServerResponse::RememberMeAck {} => {
|
||||
info!("received remember me acknowledgement");
|
||||
}
|
||||
SensitiveServerResponse::KeyUpgradeAck {} => {
|
||||
warn!(
|
||||
"received illegal key upgrade acknowledgement in an authenticated client"
|
||||
);
|
||||
}
|
||||
_ => {
|
||||
warn!("received unknown SensitiveServerResponse");
|
||||
}
|
||||
@@ -294,7 +289,7 @@ impl PartiallyDelegatedHandle {
|
||||
pub(crate) fn split_and_listen_for_mixnet_messages(
|
||||
conn: WsConn,
|
||||
packet_router: PacketRouter,
|
||||
shared_key: Arc<SharedGatewayKey>,
|
||||
shared_key: Arc<SharedSymmetricKey>,
|
||||
client_bandwidth: ClientBandwidth,
|
||||
shutdown: ShutdownToken,
|
||||
) -> Self {
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
// Copyright 2020-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::shared_key::{SharedGatewayKey, SharedKeyUsageError};
|
||||
use nym_sphinx::DestinationAddressBytes;
|
||||
use thiserror::Error;
|
||||
|
||||
/// 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)]
|
||||
// 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>);
|
||||
|
||||
impl From<Vec<u8>> for EncryptedAddressBytes {
|
||||
fn from(encrypted_address: Vec<u8>) -> Self {
|
||||
EncryptedAddressBytes(encrypted_address)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum EncryptedAddressConversionError {
|
||||
#[error("Failed to decode the encrypted address - {0}")]
|
||||
DecodeError(#[from] bs58::decode::Error),
|
||||
}
|
||||
|
||||
impl EncryptedAddressBytes {
|
||||
pub fn new(
|
||||
address: &DestinationAddressBytes,
|
||||
key: &SharedGatewayKey,
|
||||
nonce: &[u8],
|
||||
) -> Result<Self, SharedKeyUsageError> {
|
||||
let ciphertext = key.encrypt_naive(address.as_bytes_ref(), Some(nonce))?;
|
||||
|
||||
Ok(EncryptedAddressBytes(ciphertext))
|
||||
}
|
||||
|
||||
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] {
|
||||
&self.0
|
||||
}
|
||||
|
||||
pub fn try_from_base58_string<S: Into<String>>(
|
||||
val: S,
|
||||
) -> Result<Self, EncryptedAddressConversionError> {
|
||||
let decoded = bs58::decode(val.into()).into_vec()?;
|
||||
Ok(EncryptedAddressBytes(decoded))
|
||||
}
|
||||
|
||||
pub fn to_base58_string(self) -> String {
|
||||
bs58::encode(self.0).into_string()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<EncryptedAddressBytes> for String {
|
||||
fn from(val: EncryptedAddressBytes) -> Self {
|
||||
val.to_base58_string()
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
// Copyright 2020 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod encrypted_address;
|
||||
@@ -7,17 +7,12 @@ use nym_sphinx::params::GatewayIntegrityHmacAlgorithm;
|
||||
|
||||
pub use types::*;
|
||||
|
||||
pub mod authentication;
|
||||
pub mod models;
|
||||
pub mod registration;
|
||||
pub mod shared_key;
|
||||
pub mod types;
|
||||
|
||||
pub use shared_key::helpers::SymmetricKey;
|
||||
pub use shared_key::legacy::{LegacySharedKeySize, LegacySharedKeys};
|
||||
pub use shared_key::{
|
||||
SharedGatewayKey, SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey,
|
||||
};
|
||||
pub use shared_key::{SharedKeyConversionError, SharedKeyUsageError, SharedSymmetricKey};
|
||||
|
||||
pub type GatewayProtocolVersion = u8;
|
||||
|
||||
@@ -29,7 +24,7 @@ pub const CURRENT_PROTOCOL_VERSION: GatewayProtocolVersion = UPGRADE_MODE_VERSIO
|
||||
// 1 - initial release
|
||||
// 2 - changes to client credentials structure
|
||||
// 3 - change to AES-GCM-SIV and non-zero IVs
|
||||
// 4 - introduction of v2 authentication protocol to prevent reply attacks
|
||||
// 4 - introduction of v2 authentication protocol to prevent replay attacks
|
||||
// 5 - add key rotation information to the serialised mix packet
|
||||
// 6 - support for 'upgrade mode'
|
||||
pub const INITIAL_PROTOCOL_VERSION: GatewayProtocolVersion = 1;
|
||||
|
||||
@@ -5,7 +5,7 @@ use crate::registration::handshake::messages::{Finalization, GatewayMaterialExch
|
||||
use crate::registration::handshake::state::State;
|
||||
use crate::registration::handshake::HandshakeResult;
|
||||
use crate::registration::handshake::{error::HandshakeError, WsItem};
|
||||
use crate::{GatewayProtocolVersionExt, INITIAL_PROTOCOL_VERSION};
|
||||
use crate::GatewayProtocolVersionExt;
|
||||
use futures::{Sink, Stream};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
use tracing::info;
|
||||
@@ -18,11 +18,11 @@ impl<S, R> State<'_, S, R> {
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
// 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();
|
||||
let hkdf_salt = self.generate_initiator_salt();
|
||||
|
||||
// 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());
|
||||
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY || SALT
|
||||
let init_message = self.init_message(hkdf_salt.clone());
|
||||
self.send_handshake_data(init_message).await?;
|
||||
|
||||
// 2. wait for response with remote x25519 pubkey as well as encrypted signature
|
||||
@@ -33,23 +33,20 @@ impl<S, R> State<'_, S, R> {
|
||||
|
||||
// NEGOTIATE PROTOCOL
|
||||
if gateway_protocol.is_future_version() {
|
||||
// SAFETY: future version means it's greater than CURRENT, which is always a `Some`
|
||||
#[allow(clippy::unwrap_used)]
|
||||
return Err(HandshakeError::UnsupportedProtocol {
|
||||
version: gateway_protocol.unwrap(),
|
||||
version: gateway_protocol,
|
||||
});
|
||||
}
|
||||
let gateway_protocol = gateway_protocol.unwrap_or(INITIAL_PROTOCOL_VERSION);
|
||||
|
||||
// that should never happen, but we're fine with that outcome
|
||||
if Some(gateway_protocol) != self.proposed_protocol_version() {
|
||||
if gateway_protocol != self.proposed_protocol_version() {
|
||||
info!("the gateway insists on protocol version different from the one we suggested. it wants {gateway_protocol} whilst we wanted {:?}, however, we can support it", self.proposed_protocol_version());
|
||||
self.set_protocol_version(gateway_protocol);
|
||||
}
|
||||
|
||||
// 3. derive shared keys locally
|
||||
// hkdf::<blake3>::(g^xy)
|
||||
self.derive_shared_key(&mid_res.ephemeral_dh, maybe_hkdf_salt.as_deref());
|
||||
self.derive_shared_key(&mid_res.ephemeral_dh, &hkdf_salt);
|
||||
|
||||
// 4. verify the received signature using the locally derived keys
|
||||
self.verify_remote_key_material(&mid_res.materials, &mid_res.ephemeral_dh)?;
|
||||
|
||||
@@ -56,10 +56,7 @@ impl<S, R> State<'_, S, R> {
|
||||
|
||||
// 2. derive shared keys locally
|
||||
// hkdf::<blake3>::(g^xy)
|
||||
self.derive_shared_key(
|
||||
&init_message.ephemeral_dh,
|
||||
init_message.initiator_salt.as_deref(),
|
||||
);
|
||||
self.derive_shared_key(&init_message.ephemeral_dh, &init_message.initiator_salt);
|
||||
|
||||
// 3. send ephemeral x25519 pubkey alongside the encrypted signature
|
||||
// g^y || AES(k, sig(gate_priv, (g^y || g^x))
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
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_crypto::symmetric::aead::{nonce_size, tag_size, Nonce};
|
||||
use nym_sphinx::params::GatewayEncryptionAlgorithm;
|
||||
|
||||
// it is vital nobody changes the serialisation implementation unless you have an EXTREMELY good reason,
|
||||
@@ -21,13 +21,13 @@ pub trait HandshakeMessage {
|
||||
pub struct Initialisation {
|
||||
pub identity: ed25519::PublicKey,
|
||||
pub ephemeral_dh: x25519::PublicKey,
|
||||
pub initiator_salt: Option<Vec<u8>>,
|
||||
pub initiator_salt: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct MaterialExchange {
|
||||
pub signature_ciphertext: Vec<u8>,
|
||||
pub nonce: Option<Vec<u8>>,
|
||||
pub nonce: Nonce<GatewayEncryptionAlgorithm>,
|
||||
}
|
||||
|
||||
impl MaterialExchange {
|
||||
@@ -61,21 +61,16 @@ impl Finalization {
|
||||
}
|
||||
|
||||
impl HandshakeMessage for Initialisation {
|
||||
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_SALT
|
||||
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY || 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
|
||||
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()
|
||||
}
|
||||
.chain(self.ephemeral_dh.to_bytes())
|
||||
.chain(self.initiator_salt)
|
||||
.collect()
|
||||
}
|
||||
|
||||
// this will need to be adjusted when REMOTE_ID_PUBKEY is removed
|
||||
@@ -83,9 +78,8 @@ impl HandshakeMessage for Initialisation {
|
||||
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 {
|
||||
let current_len = ed25519::PUBLIC_KEY_LENGTH + x25519::PUBLIC_KEY_SIZE + KDF_SALT_LENGTH;
|
||||
if bytes.len() != current_len {
|
||||
return Err(HandshakeError::MalformedRequest);
|
||||
}
|
||||
|
||||
@@ -95,14 +89,13 @@ impl HandshakeMessage for Initialisation {
|
||||
// SAFETY: this can only fail if the provided bytes have len different from encryption::PUBLIC_KEY_SIZE
|
||||
// which is impossible
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let ephemeral_dh =
|
||||
x25519::PublicKey::from_bytes(&bytes[ed25519::PUBLIC_KEY_LENGTH..legacy_len]).unwrap();
|
||||
let ephemeral_dh = x25519::PublicKey::from_bytes(
|
||||
&bytes
|
||||
[ed25519::PUBLIC_KEY_LENGTH..ed25519::PUBLIC_KEY_LENGTH + x25519::PUBLIC_KEY_SIZE],
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let initiator_salt = if bytes.len() == legacy_len {
|
||||
None
|
||||
} else {
|
||||
Some(bytes[legacy_len..].to_vec())
|
||||
};
|
||||
let initiator_salt = bytes[ed25519::PUBLIC_KEY_LENGTH + x25519::PUBLIC_KEY_SIZE..].to_vec();
|
||||
|
||||
Ok(Initialisation {
|
||||
identity,
|
||||
@@ -115,43 +108,31 @@ impl HandshakeMessage for Initialisation {
|
||||
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()
|
||||
}
|
||||
self.signature_ciphertext
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(self.nonce)
|
||||
.collect()
|
||||
}
|
||||
|
||||
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
|
||||
let current_len = ed25519::SIGNATURE_LENGTH
|
||||
+ tag_size::<GatewayEncryptionAlgorithm>()
|
||||
+ nonce_size::<GatewayEncryptionAlgorithm>();
|
||||
|
||||
if bytes.len() != legacy_len && bytes.len() != current_len {
|
||||
if 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)
|
||||
};
|
||||
let ciphertext_len = ed25519::SIGNATURE_LENGTH + tag_size::<GatewayEncryptionAlgorithm>();
|
||||
let signature_ciphertext = bytes[..ciphertext_len].to_vec();
|
||||
|
||||
// SAFETY: we know the bytes have correct length
|
||||
let nonce = Nonce::<GatewayEncryptionAlgorithm>::clone_from_slice(&bytes[ciphertext_len..]);
|
||||
|
||||
Ok(MaterialExchange {
|
||||
signature_ciphertext,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use self::error::HandshakeError;
|
||||
use crate::registration::handshake::state::State;
|
||||
use crate::{GatewayProtocolVersion, SharedGatewayKey};
|
||||
use crate::{GatewayProtocolVersion, SharedSymmetricKey};
|
||||
use futures::future::BoxFuture;
|
||||
use futures::{Sink, Stream};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
@@ -48,7 +48,7 @@ impl Future for GatewayHandshake<'_> {
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub struct HandshakeResult {
|
||||
pub negotiated_protocol: GatewayProtocolVersion,
|
||||
pub derived_key: SharedGatewayKey,
|
||||
pub derived_key: SharedSymmetricKey,
|
||||
}
|
||||
|
||||
pub fn client_handshake<'a, S, R>(
|
||||
@@ -56,7 +56,7 @@ pub fn client_handshake<'a, S, R>(
|
||||
ws_stream: &'a mut S,
|
||||
identity: &'a ed25519::KeyPair,
|
||||
gateway_pubkey: ed25519::PublicKey,
|
||||
gateway_protocol: Option<GatewayProtocolVersion>,
|
||||
gateway_protocol: GatewayProtocolVersion,
|
||||
#[cfg(not(target_arch = "wasm32"))] shutdown_token: ShutdownToken,
|
||||
) -> GatewayHandshake<'a>
|
||||
where
|
||||
@@ -84,7 +84,7 @@ pub fn gateway_handshake<'a, S, R>(
|
||||
ws_stream: &'a mut S,
|
||||
identity: &'a ed25519::KeyPair,
|
||||
received_init_payload: Vec<u8>,
|
||||
requested_client_protocol: Option<GatewayProtocolVersion>,
|
||||
requested_client_protocol: GatewayProtocolVersion,
|
||||
shutdown_token: ShutdownToken,
|
||||
) -> GatewayHandshake<'a>
|
||||
where
|
||||
@@ -125,7 +125,7 @@ DONE(status)
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{ClientControlRequest, CURRENT_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION};
|
||||
use crate::{ClientControlRequest, CURRENT_PROTOCOL_VERSION};
|
||||
use anyhow::{bail, Context};
|
||||
use futures::StreamExt;
|
||||
use nym_test_utils::helpers::u64_seeded_rng;
|
||||
@@ -221,7 +221,7 @@ mod tests {
|
||||
client.socket,
|
||||
client.keys,
|
||||
*gateway.keys.public_key(),
|
||||
Some(CURRENT_PROTOCOL_VERSION),
|
||||
CURRENT_PROTOCOL_VERSION,
|
||||
ShutdownToken::default(),
|
||||
);
|
||||
|
||||
@@ -235,7 +235,7 @@ mod tests {
|
||||
gateway.socket,
|
||||
gateway.keys,
|
||||
init_msg,
|
||||
Some(CURRENT_PROTOCOL_VERSION),
|
||||
CURRENT_PROTOCOL_VERSION,
|
||||
ShutdownToken::default(),
|
||||
);
|
||||
|
||||
@@ -261,7 +261,7 @@ mod tests {
|
||||
client.socket,
|
||||
client.keys,
|
||||
*gateway.keys.public_key(),
|
||||
Some(CURRENT_PROTOCOL_VERSION + 42),
|
||||
CURRENT_PROTOCOL_VERSION + 42,
|
||||
ShutdownToken::default(),
|
||||
);
|
||||
|
||||
@@ -274,7 +274,7 @@ mod tests {
|
||||
gateway.socket,
|
||||
gateway.keys,
|
||||
init_msg,
|
||||
Some(CURRENT_PROTOCOL_VERSION + 42),
|
||||
CURRENT_PROTOCOL_VERSION + 42,
|
||||
ShutdownToken::default(),
|
||||
);
|
||||
|
||||
@@ -292,46 +292,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn protocol_upgrade() -> anyhow::Result<()> {
|
||||
let (client, gateway) = setup();
|
||||
|
||||
let handshake_client = client_handshake(
|
||||
client.rng,
|
||||
client.socket,
|
||||
client.keys,
|
||||
*gateway.keys.public_key(),
|
||||
None,
|
||||
ShutdownToken::default(),
|
||||
);
|
||||
|
||||
let client_fut = handshake_client.spawn_timeboxed();
|
||||
|
||||
// we need to receive the first message so that it could be propagated to the gateway side of the handshake
|
||||
let init_msg = gateway.socket.get_handshake_init_data().await?;
|
||||
|
||||
let handshake_gateway = gateway_handshake(
|
||||
gateway.rng,
|
||||
gateway.socket,
|
||||
gateway.keys,
|
||||
init_msg,
|
||||
None,
|
||||
ShutdownToken::default(),
|
||||
);
|
||||
|
||||
let gateway_fut = handshake_gateway.spawn_timeboxed();
|
||||
let (client, gateway) = join!(client_fut, gateway_fut);
|
||||
|
||||
let client_res = client???;
|
||||
let gateway_res = gateway???;
|
||||
|
||||
// ensure the created keys are the same
|
||||
assert_eq!(client_res, gateway_res);
|
||||
|
||||
// and the protocol got upgraded to the first known version
|
||||
assert_eq!(client_res.negotiated_protocol, INITIAL_PROTOCOL_VERSION);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,12 +5,11 @@ use crate::registration::handshake::error::HandshakeError;
|
||||
use crate::registration::handshake::messages::{
|
||||
HandshakeMessage, Initialisation, MaterialExchange,
|
||||
};
|
||||
use crate::registration::handshake::{HandshakeResult, SharedGatewayKey, WsItem, KDF_SALT_LENGTH};
|
||||
use crate::shared_key::SharedKeySize;
|
||||
use crate::{
|
||||
types, GatewayProtocolVersion, GatewayProtocolVersionExt, LegacySharedKeySize,
|
||||
LegacySharedKeys, SharedSymmetricKey, INITIAL_PROTOCOL_VERSION,
|
||||
use crate::registration::handshake::{
|
||||
HandshakeResult, SharedSymmetricKey, WsItem, KDF_SALT_LENGTH,
|
||||
};
|
||||
use crate::shared_key::SharedKeySize;
|
||||
use crate::{types, GatewayProtocolVersion};
|
||||
use futures::{Sink, SinkExt, Stream, StreamExt};
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_crypto::symmetric::aead::random_nonce;
|
||||
@@ -48,17 +47,15 @@ pub(crate) struct State<'a, S, R> {
|
||||
ephemeral_keypair: x25519::KeyPair,
|
||||
|
||||
/// The derived shared key using the ephemeral keys of both parties.
|
||||
derived_shared_keys: Option<SharedGatewayKey>,
|
||||
derived_shared_keys: Option<SharedSymmetricKey>,
|
||||
|
||||
/// The known or received public identity key of the remote.
|
||||
/// Ideally it would always be known before the handshake was initiated.
|
||||
remote_pubkey: Option<ed25519::PublicKey>,
|
||||
|
||||
/// Version of the protocol to use during the handshake that also implicitly specifies
|
||||
/// additional features such as the type of derived shared keys, i.e.
|
||||
/// AES128Ctr + blake3 HMAC keys (legacy) or AES256-GCM-SIV (current)
|
||||
/// the above is decided by whether the specified protocol version supports the new variant or not.
|
||||
protocol_version: Option<GatewayProtocolVersion>,
|
||||
/// additional features
|
||||
protocol_version: GatewayProtocolVersion,
|
||||
|
||||
// channel to receive shutdown signal
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -71,7 +68,7 @@ impl<'a, S, R> State<'a, S, R> {
|
||||
ws_stream: &'a mut S,
|
||||
identity: &'a ed25519::KeyPair,
|
||||
remote_pubkey: Option<ed25519::PublicKey>,
|
||||
protocol_version: Option<GatewayProtocolVersion>,
|
||||
protocol_version: GatewayProtocolVersion,
|
||||
#[cfg(not(target_arch = "wasm32"))] shutdown_token: ShutdownToken,
|
||||
) -> Self
|
||||
where
|
||||
@@ -96,31 +93,27 @@ impl<'a, S, R> State<'a, S, R> {
|
||||
self.ephemeral_keypair.public_key()
|
||||
}
|
||||
|
||||
pub(crate) fn proposed_protocol_version(&self) -> Option<GatewayProtocolVersion> {
|
||||
pub(crate) fn proposed_protocol_version(&self) -> GatewayProtocolVersion {
|
||||
self.protocol_version
|
||||
}
|
||||
|
||||
pub(crate) fn set_protocol_version(&mut self, protocol_version: GatewayProtocolVersion) {
|
||||
self.protocol_version = Some(protocol_version);
|
||||
self.protocol_version = protocol_version;
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_generate_initiator_salt(&mut self) -> Option<Vec<u8>>
|
||||
pub(crate) fn generate_initiator_salt(&mut self) -> Vec<u8>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
if self.protocol_version.supports_aes256_gcm_siv() {
|
||||
let mut salt = vec![0u8; KDF_SALT_LENGTH];
|
||||
self.rng.fill_bytes(&mut salt);
|
||||
Some(salt)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let mut salt = vec![0u8; KDF_SALT_LENGTH];
|
||||
self.rng.fill_bytes(&mut salt);
|
||||
salt
|
||||
}
|
||||
|
||||
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY || MAYBE_SALT
|
||||
// LOCAL_ID_PUBKEY || EPHEMERAL_KEY || 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, initiator_salt: Option<Vec<u8>>) -> Initialisation {
|
||||
pub(crate) fn init_message(&self, initiator_salt: Vec<u8>) -> Initialisation {
|
||||
Initialisation {
|
||||
identity: *self.identity.public_key(),
|
||||
ephemeral_dh: *self.ephemeral_keypair.public_key(),
|
||||
@@ -138,23 +131,19 @@ impl<'a, S, R> State<'a, S, R> {
|
||||
pub(crate) fn derive_shared_key(
|
||||
&mut self,
|
||||
remote_ephemeral_key: &x25519::PublicKey,
|
||||
initiator_salt: Option<&[u8]>,
|
||||
initiator_salt: &[u8],
|
||||
) {
|
||||
let dh_result = self
|
||||
.ephemeral_keypair
|
||||
.private_key()
|
||||
.diffie_hellman(remote_ephemeral_key);
|
||||
|
||||
let key_size = if self.protocol_version.supports_aes256_gcm_siv() {
|
||||
SharedKeySize::to_usize()
|
||||
} else {
|
||||
LegacySharedKeySize::to_usize()
|
||||
};
|
||||
let key_size = SharedKeySize::to_usize();
|
||||
|
||||
// SAFETY: there is no reason for this to fail as our okm is expected to be only 16 bytes
|
||||
#[allow(clippy::expect_used)]
|
||||
let okm = hkdf::extract_then_expand::<GatewaySharedKeyHkdfAlgorithm>(
|
||||
initiator_salt,
|
||||
Some(initiator_salt),
|
||||
&dh_result,
|
||||
None,
|
||||
key_size,
|
||||
@@ -162,17 +151,10 @@ impl<'a, S, R> State<'a, S, R> {
|
||||
.expect("somehow too long okm was provided");
|
||||
|
||||
// SAFETY: the okm has been expanded to the length expected by the corresponding keys
|
||||
let shared_key = if self.protocol_version.supports_aes256_gcm_siv() {
|
||||
#[allow(clippy::expect_used)]
|
||||
let current_key = SharedSymmetricKey::try_from_bytes(&okm)
|
||||
.expect("okm was expanded to incorrect length!");
|
||||
SharedGatewayKey::Current(current_key)
|
||||
} else {
|
||||
#[allow(clippy::expect_used)]
|
||||
let legacy_key = LegacySharedKeys::try_from_bytes(&okm)
|
||||
.expect("okm was expanded to incorrect length!");
|
||||
SharedGatewayKey::Legacy(legacy_key)
|
||||
};
|
||||
#[allow(clippy::expect_used)]
|
||||
let shared_key = SharedSymmetricKey::try_from_bytes(&okm)
|
||||
.expect("okm was expanded to incorrect length!");
|
||||
|
||||
self.derived_shared_keys = Some(shared_key)
|
||||
}
|
||||
|
||||
@@ -191,12 +173,8 @@ impl<'a, S, R> State<'a, S, R> {
|
||||
.collect();
|
||||
let signature = self.identity.private_key().sign(plaintext);
|
||||
|
||||
let nonce = if self.protocol_version.supports_aes256_gcm_siv() {
|
||||
let mut rng = thread_rng();
|
||||
Some(random_nonce::<GatewayEncryptionAlgorithm, _>(&mut rng).to_vec())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let mut rng = thread_rng();
|
||||
let nonce = random_nonce::<GatewayEncryptionAlgorithm, _>(&mut rng);
|
||||
|
||||
// SAFETY: this function is only called after the local key has already been derived
|
||||
#[allow(clippy::expect_used)]
|
||||
@@ -204,7 +182,7 @@ impl<'a, S, R> State<'a, S, R> {
|
||||
.derived_shared_keys
|
||||
.as_ref()
|
||||
.expect("shared key was not derived!")
|
||||
.encrypt_naive(&signature.to_bytes(), nonce.as_deref())?;
|
||||
.encrypt(&signature.to_bytes(), &nonce)?;
|
||||
|
||||
Ok(MaterialExchange {
|
||||
signature_ciphertext,
|
||||
@@ -224,15 +202,10 @@ impl<'a, S, R> State<'a, S, R> {
|
||||
.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.protocol_version.supports_aes256_gcm_siv() && remote_response.nonce.is_none() {
|
||||
return Err(HandshakeError::MissingNonceForCurrentKey);
|
||||
}
|
||||
|
||||
// first decrypt received data
|
||||
let decrypted_signature = derived_shared_key.decrypt_naive(
|
||||
let decrypted_signature = derived_shared_key.decrypt(
|
||||
&remote_response.signature_ciphertext,
|
||||
remote_response.nonce.as_deref(),
|
||||
&remote_response.nonce,
|
||||
)?;
|
||||
|
||||
// now verify signature itself
|
||||
@@ -262,7 +235,7 @@ impl<'a, S, R> State<'a, S, R> {
|
||||
#[allow(clippy::complexity)]
|
||||
fn on_wg_msg(
|
||||
msg: Option<WsItem>,
|
||||
) -> Result<Option<(Vec<u8>, Option<GatewayProtocolVersion>)>, HandshakeError> {
|
||||
) -> Result<Option<(Vec<u8>, GatewayProtocolVersion)>, HandshakeError> {
|
||||
let Some(msg) = msg else {
|
||||
return Err(HandshakeError::ClosedStream);
|
||||
};
|
||||
@@ -303,7 +276,7 @@ impl<'a, S, R> State<'a, S, R> {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
async fn _receive_handshake_message_bytes(
|
||||
&mut self,
|
||||
) -> Result<(Vec<u8>, Option<GatewayProtocolVersion>), HandshakeError>
|
||||
) -> Result<(Vec<u8>, GatewayProtocolVersion), HandshakeError>
|
||||
where
|
||||
S: Stream<Item = WsItem> + Unpin,
|
||||
{
|
||||
@@ -324,7 +297,7 @@ impl<'a, S, R> State<'a, S, R> {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
async fn _receive_handshake_message_bytes(
|
||||
&mut self,
|
||||
) -> Result<(Vec<u8>, Option<GatewayProtocolVersion>), HandshakeError>
|
||||
) -> Result<(Vec<u8>, GatewayProtocolVersion), HandshakeError>
|
||||
where
|
||||
S: Stream<Item = WsItem> + Unpin,
|
||||
{
|
||||
@@ -339,7 +312,7 @@ impl<'a, S, R> State<'a, S, R> {
|
||||
|
||||
pub(crate) async fn receive_handshake_message<M>(
|
||||
&mut self,
|
||||
) -> Result<(M, Option<GatewayProtocolVersion>), HandshakeError>
|
||||
) -> Result<(M, GatewayProtocolVersion), HandshakeError>
|
||||
where
|
||||
S: Stream<Item = WsItem> + Unpin,
|
||||
M: HandshakeMessage,
|
||||
@@ -396,9 +369,7 @@ impl<'a, S, R> State<'a, S, R> {
|
||||
// SAFETY: handshake can't be finalised without deriving the shared keys
|
||||
#[allow(clippy::unwrap_used)]
|
||||
HandshakeResult {
|
||||
negotiated_protocol: self
|
||||
.proposed_protocol_version()
|
||||
.unwrap_or(INITIAL_PROTOCOL_VERSION),
|
||||
negotiated_protocol: self.proposed_protocol_version(),
|
||||
derived_key: self.derived_shared_keys.unwrap(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,141 @@
|
||||
// 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_pemstore::traits::PemStorableKey;
|
||||
use nym_sphinx::params::GatewayEncryptionAlgorithm;
|
||||
use rand::thread_rng;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use thiserror::Error;
|
||||
use zeroize::{Zeroize, ZeroizeOnDrop, Zeroizing};
|
||||
|
||||
pub type SharedKeySize = <GatewayEncryptionAlgorithm as KeySizeUser>::KeySize;
|
||||
|
||||
#[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),
|
||||
}
|
||||
|
||||
#[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 random_nonce(&self) -> Nonce<GatewayEncryptionAlgorithm> {
|
||||
let mut rng = thread_rng();
|
||||
random_nonce::<GatewayEncryptionAlgorithm, _>(&mut rng)
|
||||
}
|
||||
|
||||
pub fn nonce_size(&self) -> usize {
|
||||
nonce_size::<GatewayEncryptionAlgorithm>()
|
||||
}
|
||||
|
||||
pub fn validate_aead_nonce(
|
||||
raw: &[u8],
|
||||
) -> Result<Nonce<GatewayEncryptionAlgorithm>, SharedKeyUsageError> {
|
||||
if raw.len() != nonce_size::<GatewayEncryptionAlgorithm>() {
|
||||
return Err(SharedKeyUsageError::MalformedNonce);
|
||||
}
|
||||
Ok(Nonce::<GatewayEncryptionAlgorithm>::clone_from_slice(raw))
|
||||
}
|
||||
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,98 +0,0 @@
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
// 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());
|
||||
#[allow(clippy::expect_used)]
|
||||
let okm = hkdf::extract_then_expand::<GatewaySharedKeyHkdfAlgorithm>(
|
||||
Some(&salt),
|
||||
&legacy_bytes,
|
||||
None,
|
||||
SharedKeySize::to_usize(),
|
||||
)
|
||||
.expect("somehow too long okm was provided");
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
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());
|
||||
#[allow(clippy::expect_used)]
|
||||
let okm = hkdf::extract_then_expand::<GatewaySharedKeyHkdfAlgorithm>(
|
||||
Some(salt),
|
||||
&legacy_bytes,
|
||||
None,
|
||||
SharedKeySize::to_usize(),
|
||||
)
|
||||
.expect("somehow too long okm was provided");
|
||||
|
||||
#[allow(clippy::expect_used)]
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -1,306 +0,0 @@
|
||||
// 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,
|
||||
}
|
||||
}
|
||||
|
||||
// it is responsibility of the caller to ensure the correct variant is present
|
||||
#[allow(clippy::panic)]
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::types::helpers::BinaryData;
|
||||
use crate::{GatewayRequestsError, SharedGatewayKey};
|
||||
use crate::{GatewayRequestsError, SharedSymmetricKey};
|
||||
use nym_sphinx::forwarding::packet::MixPacket;
|
||||
use strum::FromRepr;
|
||||
use tungstenite::Message;
|
||||
@@ -57,14 +57,14 @@ impl BinaryRequest {
|
||||
|
||||
pub fn try_from_encrypted_tagged_bytes(
|
||||
bytes: Vec<u8>,
|
||||
shared_key: &SharedGatewayKey,
|
||||
shared_key: &SharedSymmetricKey,
|
||||
) -> Result<Self, GatewayRequestsError> {
|
||||
BinaryData::from_raw(&bytes, shared_key)?.into_request(shared_key)
|
||||
}
|
||||
|
||||
pub fn into_encrypted_tagged_bytes(
|
||||
self,
|
||||
shared_key: &SharedGatewayKey,
|
||||
shared_key: &SharedSymmetricKey,
|
||||
) -> Result<Vec<u8>, GatewayRequestsError> {
|
||||
let kind = self.kind();
|
||||
|
||||
@@ -78,7 +78,7 @@ impl BinaryRequest {
|
||||
|
||||
pub fn into_ws_message(
|
||||
self,
|
||||
shared_key: &SharedGatewayKey,
|
||||
shared_key: &SharedSymmetricKey,
|
||||
) -> Result<Message, GatewayRequestsError> {
|
||||
// all variants are currently encrypted
|
||||
let blob = match self {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::types::helpers::BinaryData;
|
||||
use crate::{GatewayRequestsError, SharedGatewayKey};
|
||||
use crate::{GatewayRequestsError, SharedSymmetricKey};
|
||||
use strum::FromRepr;
|
||||
use tungstenite::Message;
|
||||
|
||||
@@ -38,14 +38,14 @@ impl BinaryResponse {
|
||||
|
||||
pub fn try_from_encrypted_tagged_bytes(
|
||||
bytes: Vec<u8>,
|
||||
shared_key: &SharedGatewayKey,
|
||||
shared_key: &SharedSymmetricKey,
|
||||
) -> Result<Self, GatewayRequestsError> {
|
||||
BinaryData::from_raw(&bytes, shared_key)?.into_response(shared_key)
|
||||
}
|
||||
|
||||
pub fn into_encrypted_tagged_bytes(
|
||||
self,
|
||||
shared_key: &SharedGatewayKey,
|
||||
shared_key: &SharedSymmetricKey,
|
||||
) -> Result<Vec<u8>, GatewayRequestsError> {
|
||||
let kind = self.kind();
|
||||
|
||||
@@ -58,7 +58,7 @@ impl BinaryResponse {
|
||||
|
||||
pub fn into_ws_message(
|
||||
self,
|
||||
shared_key: &SharedGatewayKey,
|
||||
shared_key: &SharedSymmetricKey,
|
||||
) -> Result<Message, GatewayRequestsError> {
|
||||
// all variants are currently encrypted
|
||||
let blob = match self {
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
|
||||
use crate::{
|
||||
BinaryRequest, BinaryRequestKind, BinaryResponse, BinaryResponseKind, GatewayRequestsError,
|
||||
SharedGatewayKey,
|
||||
SharedSymmetricKey,
|
||||
};
|
||||
use std::iter::once;
|
||||
|
||||
// each binary message consists of the following structure (for non-legacy messages)
|
||||
// KIND || ENC_FLAG || MAYBE_NONCE || CIPHERTEXT/PLAINTEXT
|
||||
// KIND || ENC_FLAG || 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
|
||||
@@ -16,43 +16,25 @@ use std::iter::once;
|
||||
pub struct BinaryData<'a> {
|
||||
kind: u8,
|
||||
encrypted: bool,
|
||||
maybe_nonce: Option<&'a [u8]>,
|
||||
nonce: &'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()
|
||||
}
|
||||
pub fn into_raw(self) -> Vec<u8> {
|
||||
once(self.kind)
|
||||
.chain(once(if self.encrypted { 1 } else { 0 }))
|
||||
.chain(self.nonce.iter().copied())
|
||||
.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,
|
||||
available_key: &SharedSymmetricKey,
|
||||
) -> 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);
|
||||
}
|
||||
@@ -74,7 +56,7 @@ impl<'a> BinaryData<'a> {
|
||||
Ok(BinaryData {
|
||||
kind,
|
||||
encrypted,
|
||||
maybe_nonce: Some(&raw[2..2 + available_key.nonce_size()]),
|
||||
nonce: &raw[2..2 + available_key.nonce_size()],
|
||||
data: &raw[2 + available_key.nonce_size()..],
|
||||
})
|
||||
}
|
||||
@@ -83,30 +65,32 @@ impl<'a> BinaryData<'a> {
|
||||
pub fn make_encrypted_blob(
|
||||
kind: u8,
|
||||
plaintext: &[u8],
|
||||
key: &SharedGatewayKey,
|
||||
key: &SharedSymmetricKey,
|
||||
) -> Result<Vec<u8>, GatewayRequestsError> {
|
||||
let maybe_nonce = key.random_nonce_or_zero_iv();
|
||||
let nonce = key.random_nonce();
|
||||
|
||||
let ciphertext = key.encrypt(plaintext, maybe_nonce.as_deref())?;
|
||||
let ciphertext = key.encrypt(plaintext, &nonce)?;
|
||||
Ok(BinaryData {
|
||||
kind,
|
||||
encrypted: true,
|
||||
maybe_nonce: maybe_nonce.as_deref(),
|
||||
nonce: &nonce,
|
||||
data: &ciphertext,
|
||||
}
|
||||
.into_raw(key.is_legacy()))
|
||||
.into_raw())
|
||||
}
|
||||
|
||||
// attempts to parse previously recovered bytes into a [`BinaryRequest`]
|
||||
pub fn into_request(
|
||||
self,
|
||||
key: &SharedGatewayKey,
|
||||
key: &SharedSymmetricKey,
|
||||
) -> 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)?
|
||||
let nonce = SharedSymmetricKey::validate_aead_nonce(self.nonce)?;
|
||||
|
||||
&*key.decrypt(self.data, &nonce)?
|
||||
} else {
|
||||
self.data
|
||||
};
|
||||
@@ -117,13 +101,15 @@ impl<'a> BinaryData<'a> {
|
||||
// attempts to parse previously recovered bytes into a [`BinaryResponse`]
|
||||
pub fn into_response(
|
||||
self,
|
||||
key: &SharedGatewayKey,
|
||||
key: &SharedSymmetricKey,
|
||||
) -> 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)?
|
||||
let nonce = SharedSymmetricKey::validate_aead_nonce(self.nonce)?;
|
||||
|
||||
&*key.decrypt(self.data, &nonce)?
|
||||
} else {
|
||||
self.data
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::str::FromStr;
|
||||
pub enum RegistrationHandshake {
|
||||
HandshakePayload {
|
||||
#[serde(default)]
|
||||
protocol_version: Option<GatewayProtocolVersion>,
|
||||
protocol_version: GatewayProtocolVersion,
|
||||
data: Vec<u8>,
|
||||
},
|
||||
HandshakeError {
|
||||
@@ -19,7 +19,7 @@ pub enum RegistrationHandshake {
|
||||
}
|
||||
|
||||
impl RegistrationHandshake {
|
||||
pub fn new_payload(data: Vec<u8>, protocol_version: Option<GatewayProtocolVersion>) -> Self {
|
||||
pub fn new_payload(data: Vec<u8>, protocol_version: GatewayProtocolVersion) -> Self {
|
||||
RegistrationHandshake::HandshakePayload {
|
||||
protocol_version,
|
||||
data,
|
||||
@@ -66,7 +66,7 @@ mod tests {
|
||||
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),
|
||||
protocol_version: 42,
|
||||
data: handshake_data.clone(),
|
||||
};
|
||||
let serialized = serde_json::to_string(&handshake_payload_with_protocol).unwrap();
|
||||
@@ -77,25 +77,7 @@ mod tests {
|
||||
protocol_version,
|
||||
data,
|
||||
} => {
|
||||
assert_eq!(protocol_version, Some(42));
|
||||
assert_eq!(data, handshake_data)
|
||||
}
|
||||
_ => panic!("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!(protocol_version, 42);
|
||||
assert_eq!(data, handshake_data)
|
||||
}
|
||||
_ => panic!("this branch shouldn't have been reached!"),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
use crate::{
|
||||
AuthenticationFailure, GatewayProtocolVersion, GatewayRequestsError, SharedGatewayKey,
|
||||
AuthenticationFailure, GatewayProtocolVersion, GatewayRequestsError, SharedSymmetricKey,
|
||||
};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -23,7 +23,7 @@ pub struct AuthenticateRequest {
|
||||
impl AuthenticateRequest {
|
||||
pub fn new(
|
||||
protocol_version: GatewayProtocolVersion,
|
||||
shared_key: &SharedGatewayKey,
|
||||
shared_key: &SharedSymmetricKey,
|
||||
identity_keys: &ed25519::KeyPair,
|
||||
) -> Result<AuthenticateRequest, GatewayRequestsError> {
|
||||
let content = AuthenticateRequestContent::new(
|
||||
@@ -72,14 +72,14 @@ impl AuthenticateRequest {
|
||||
|
||||
pub fn verify_ciphertext(
|
||||
&self,
|
||||
shared_key: &SharedGatewayKey,
|
||||
shared_key: &SharedSymmetricKey,
|
||||
) -> Result<(), AuthenticationFailure> {
|
||||
let expected = shared_key.encrypt(
|
||||
self.content
|
||||
.client_identity
|
||||
.derive_destination_address()
|
||||
.as_bytes_ref(),
|
||||
Some(&self.content.nonce),
|
||||
&SharedSymmetricKey::validate_aead_nonce(&self.content.nonce)?,
|
||||
)?;
|
||||
|
||||
if !bool::from(expected.ct_eq(&self.content.address_ciphertext)) {
|
||||
@@ -117,20 +117,19 @@ pub struct AuthenticateRequestContent {
|
||||
impl AuthenticateRequestContent {
|
||||
fn new(
|
||||
protocol_version: u8,
|
||||
shared_key: &SharedGatewayKey,
|
||||
shared_key: &SharedSymmetricKey,
|
||||
client_identity: ed25519::PublicKey,
|
||||
) -> Result<AuthenticateRequestContent, GatewayRequestsError> {
|
||||
let nonce = shared_key.random_nonce_or_iv();
|
||||
let nonce = shared_key.random_nonce();
|
||||
let destination_address = client_identity.derive_destination_address();
|
||||
|
||||
let address_ciphertext =
|
||||
shared_key.encrypt(destination_address.as_bytes_ref(), Some(&nonce))?;
|
||||
let address_ciphertext = shared_key.encrypt(destination_address.as_bytes_ref(), &nonce)?;
|
||||
let now = OffsetDateTime::now_utc();
|
||||
Ok(AuthenticateRequestContent {
|
||||
protocol_version,
|
||||
client_identity,
|
||||
address_ciphertext,
|
||||
nonce,
|
||||
nonce: nonce.to_vec(),
|
||||
request_unix_timestamp: now.unix_timestamp() as u64, // SAFETY: we're running this in post 1970...
|
||||
})
|
||||
}
|
||||
|
||||
@@ -3,13 +3,9 @@
|
||||
|
||||
use crate::models::CredentialSpendingRequest;
|
||||
use crate::text_request::authenticate::AuthenticateRequest;
|
||||
use crate::{
|
||||
GatewayProtocolVersion, GatewayRequestsError, SharedGatewayKey, SymmetricKey,
|
||||
AES_GCM_SIV_PROTOCOL_VERSION, CREDENTIAL_UPDATE_V2_PROTOCOL_VERSION, INITIAL_PROTOCOL_VERSION,
|
||||
};
|
||||
use crate::{GatewayProtocolVersion, GatewayRequestsError, SharedSymmetricKey};
|
||||
use nym_credentials_interface::CredentialSpendingData;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_sphinx::DestinationAddressBytes;
|
||||
use nym_statistics_common::types::SessionType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr;
|
||||
@@ -21,23 +17,14 @@ pub mod authenticate;
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[non_exhaustive]
|
||||
pub enum ClientRequest {
|
||||
UpgradeKey {
|
||||
hkdf_salt: Vec<u8>,
|
||||
derived_key_digest: Vec<u8>,
|
||||
},
|
||||
ForgetMe {
|
||||
client: bool,
|
||||
stats: bool,
|
||||
},
|
||||
RememberMe {
|
||||
session_type: SessionType,
|
||||
},
|
||||
ForgetMe { client: bool, stats: bool },
|
||||
RememberMe { session_type: SessionType },
|
||||
}
|
||||
|
||||
impl ClientRequest {
|
||||
pub fn encrypt<S: SymmetricKey>(
|
||||
pub fn encrypt(
|
||||
&self,
|
||||
key: &S,
|
||||
key: &SharedSymmetricKey,
|
||||
) -> Result<ClientControlRequest, GatewayRequestsError> {
|
||||
// we're using json representation for few reasons:
|
||||
// - ease of re-implementation in other languages (compared to for example bincode)
|
||||
@@ -47,17 +34,21 @@ impl ClientRequest {
|
||||
// SAFETY: the trait has been derived correctly with no weird variants
|
||||
#[allow(clippy::unwrap_used)]
|
||||
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 })
|
||||
let nonce = key.random_nonce();
|
||||
let ciphertext = key.encrypt(&plaintext, &nonce)?;
|
||||
Ok(ClientControlRequest::EncryptedRequest {
|
||||
ciphertext,
|
||||
nonce: nonce.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decrypt<S: SymmetricKey>(
|
||||
pub fn decrypt(
|
||||
ciphertext: &[u8],
|
||||
nonce: &[u8],
|
||||
key: &S,
|
||||
key: &SharedSymmetricKey,
|
||||
) -> Result<Self, GatewayRequestsError> {
|
||||
let plaintext = key.decrypt(ciphertext, Some(nonce))?;
|
||||
let nonce = SharedSymmetricKey::validate_aead_nonce(nonce)?;
|
||||
let plaintext = key.decrypt(ciphertext, &nonce)?;
|
||||
serde_json::from_slice(&plaintext)
|
||||
.map_err(|source| GatewayRequestsError::MalformedRequest { source })
|
||||
}
|
||||
@@ -68,35 +59,20 @@ impl ClientRequest {
|
||||
#[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<GatewayProtocolVersion>,
|
||||
address: String,
|
||||
enc_address: String,
|
||||
iv: String,
|
||||
},
|
||||
|
||||
AuthenticateV2(Box<AuthenticateRequest>),
|
||||
|
||||
#[serde(alias = "handshakePayload")]
|
||||
RegisterHandshakeInitRequest {
|
||||
#[serde(default)]
|
||||
protocol_version: Option<GatewayProtocolVersion>,
|
||||
protocol_version: GatewayProtocolVersion,
|
||||
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>,
|
||||
// Old gateways only understand `iv` so rename the field, but have nonce as an alias for next version update, to then phase out `iv`
|
||||
#[serde(rename = "iv")]
|
||||
#[serde(alias = "nonce")]
|
||||
nonce: Vec<u8>,
|
||||
},
|
||||
UpgradeModeJWT {
|
||||
// no need to encrypt it as it's public anyway
|
||||
@@ -109,40 +85,29 @@ pub enum ClientControlRequest {
|
||||
},
|
||||
SupportedProtocol {},
|
||||
// if you're adding new variants here, consider putting them inside `ClientRequest` instead
|
||||
|
||||
// NO LONGER SUPPORTED
|
||||
Authenticate {
|
||||
#[serde(default)]
|
||||
protocol_version: Option<GatewayProtocolVersion>,
|
||||
address: String,
|
||||
enc_address: String,
|
||||
iv: String,
|
||||
},
|
||||
|
||||
BandwidthCredential {
|
||||
enc_credential: Vec<u8>,
|
||||
iv: Vec<u8>,
|
||||
},
|
||||
BandwidthCredentialV2 {
|
||||
enc_credential: Vec<u8>,
|
||||
iv: Vec<u8>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ClientControlRequest {
|
||||
pub fn new_legacy_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
|
||||
// since we are using legacy authentication, the gateway definitely doesn't understand the protocol downgrade,
|
||||
// so use the lowest possible version we can
|
||||
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 new_authenticate_v2(
|
||||
shared_key: &SharedGatewayKey,
|
||||
shared_key: &SharedSymmetricKey,
|
||||
identity_keys: &ed25519::KeyPair,
|
||||
protocol_version: GatewayProtocolVersion,
|
||||
) -> Result<Self, GatewayRequestsError> {
|
||||
@@ -174,26 +139,27 @@ impl ClientControlRequest {
|
||||
|
||||
pub fn new_enc_ecash_credential(
|
||||
credential: CredentialSpendingData,
|
||||
shared_key: &SharedGatewayKey,
|
||||
shared_key: &SharedSymmetricKey,
|
||||
) -> 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))?;
|
||||
let nonce = shared_key.random_nonce();
|
||||
let enc_credential = shared_key.encrypt(&serialized_credential, &nonce)?;
|
||||
|
||||
Ok(ClientControlRequest::EcashCredential {
|
||||
enc_credential,
|
||||
iv: nonce,
|
||||
nonce: nonce.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn try_from_enc_ecash_credential(
|
||||
enc_credential: Vec<u8>,
|
||||
shared_key: &SharedGatewayKey,
|
||||
iv: Vec<u8>,
|
||||
shared_key: &SharedSymmetricKey,
|
||||
nonce: Vec<u8>,
|
||||
) -> Result<CredentialSpendingRequest, GatewayRequestsError> {
|
||||
let credential_bytes = shared_key.decrypt(&enc_credential, Some(&iv))?;
|
||||
let nonce = SharedSymmetricKey::validate_aead_nonce(&nonce)?;
|
||||
let credential_bytes = shared_key.decrypt(&enc_credential, &nonce)?;
|
||||
CredentialSpendingRequest::try_from_bytes(credential_bytes.as_slice())
|
||||
.map_err(|_| GatewayRequestsError::MalformedEncryption)
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{
|
||||
GatewayProtocolVersion, GatewayRequestsError, SimpleGatewayRequestsError, SymmetricKey,
|
||||
GatewayProtocolVersion, GatewayRequestsError, SharedSymmetricKey, SimpleGatewayRequestsError,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tungstenite::Message;
|
||||
@@ -12,15 +12,14 @@ use tungstenite::Message;
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[non_exhaustive]
|
||||
pub enum SensitiveServerResponse {
|
||||
KeyUpgradeAck {},
|
||||
ForgetMeAck {},
|
||||
RememberMeAck {},
|
||||
}
|
||||
|
||||
impl SensitiveServerResponse {
|
||||
pub fn encrypt<S: SymmetricKey>(
|
||||
pub fn encrypt(
|
||||
&self,
|
||||
key: &S,
|
||||
key: &SharedSymmetricKey,
|
||||
) -> Result<ServerResponse, GatewayRequestsError> {
|
||||
// we're using json representation for few reasons:
|
||||
// - ease of re-implementation in other languages (compared to for example bincode)
|
||||
@@ -30,17 +29,21 @@ impl SensitiveServerResponse {
|
||||
// SAFETY: the trait has been derived correctly with no weird variants
|
||||
#[allow(clippy::unwrap_used)]
|
||||
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 })
|
||||
let nonce = key.random_nonce();
|
||||
let ciphertext = key.encrypt(&plaintext, &nonce)?;
|
||||
Ok(ServerResponse::EncryptedResponse {
|
||||
ciphertext,
|
||||
nonce: nonce.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn decrypt<S: SymmetricKey>(
|
||||
pub fn decrypt(
|
||||
ciphertext: &[u8],
|
||||
nonce: &[u8],
|
||||
key: &S,
|
||||
key: &SharedSymmetricKey,
|
||||
) -> Result<Self, GatewayRequestsError> {
|
||||
let plaintext = key.decrypt(ciphertext, Some(nonce))?;
|
||||
let nonce = SharedSymmetricKey::validate_aead_nonce(nonce)?;
|
||||
let plaintext = key.decrypt(ciphertext, &nonce)?;
|
||||
serde_json::from_slice(&plaintext)
|
||||
.map_err(|source| GatewayRequestsError::MalformedRequest { source })
|
||||
}
|
||||
@@ -72,7 +75,7 @@ pub struct SendResponse {
|
||||
pub enum ServerResponse {
|
||||
Authenticate {
|
||||
#[serde(default)]
|
||||
protocol_version: Option<GatewayProtocolVersion>,
|
||||
protocol_version: GatewayProtocolVersion,
|
||||
status: bool,
|
||||
bandwidth_remaining: i64,
|
||||
|
||||
@@ -83,7 +86,7 @@ pub enum ServerResponse {
|
||||
},
|
||||
Register {
|
||||
#[serde(default)]
|
||||
protocol_version: Option<GatewayProtocolVersion>,
|
||||
protocol_version: GatewayProtocolVersion,
|
||||
status: bool,
|
||||
|
||||
/// Flag indicating whether the gateway has detected the system is undergoing the upgrade
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
* SPDX-License-Identifier: GPL-3.0-only
|
||||
*/
|
||||
|
||||
|
||||
-- make aes256gcm column non-nullable and drop any clients that still use the legacy keys
|
||||
CREATE TABLE shared_keys_tmp
|
||||
(
|
||||
client_id INTEGER NOT NULL PRIMARY KEY REFERENCES clients (id),
|
||||
client_address_bs58 TEXT NOT NULL UNIQUE,
|
||||
derived_aes256_gcm_siv_key BLOB NOT NULL,
|
||||
last_used_authentication TIMESTAMP WITHOUT TIME ZONE
|
||||
);
|
||||
|
||||
INSERT INTO shared_keys_tmp (client_id, client_address_bs58, derived_aes256_gcm_siv_key, last_used_authentication)
|
||||
SELECT client_id, client_address_bs58, derived_aes256_gcm_siv_key, last_used_authentication
|
||||
FROM shared_keys
|
||||
WHERE derived_aes256_gcm_siv_key IS NOT NULL;
|
||||
|
||||
DROP TABLE shared_keys;
|
||||
ALTER TABLE shared_keys_tmp
|
||||
RENAME TO shared_keys;
|
||||
@@ -9,7 +9,7 @@ use models::{
|
||||
VerifiedTicket, WireguardPeer,
|
||||
};
|
||||
use nym_credentials_interface::ClientTicket;
|
||||
use nym_gateway_requests::shared_key::SharedGatewayKey;
|
||||
use nym_gateway_requests::shared_key::SharedSymmetricKey;
|
||||
use nym_sphinx::DestinationAddressBytes;
|
||||
use shared_keys::SharedKeysManager;
|
||||
use sqlx::{
|
||||
@@ -165,7 +165,7 @@ impl SharedKeyGatewayStorage for GatewayStorage {
|
||||
async fn insert_shared_keys(
|
||||
&self,
|
||||
client_address: DestinationAddressBytes,
|
||||
shared_keys: &SharedGatewayKey,
|
||||
shared_keys: &SharedSymmetricKey,
|
||||
) -> Result<i64, GatewayStorageError> {
|
||||
let client_address_bs58 = client_address.as_base58_string();
|
||||
let client_id = match self
|
||||
@@ -184,8 +184,7 @@ impl SharedKeyGatewayStorage for GatewayStorage {
|
||||
.insert_shared_keys(
|
||||
client_id,
|
||||
client_address_bs58,
|
||||
shared_keys.aes128_ctr_hmac_bs58().as_deref(),
|
||||
shared_keys.aes256_gcm_siv().as_deref(),
|
||||
shared_keys.to_bytes().as_ref(),
|
||||
)
|
||||
.await?;
|
||||
Ok(client_id)
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use crate::{error::GatewayStorageError, make_bincode_serializer};
|
||||
use nym_credentials_interface::{AvailableBandwidth, ClientTicket, CredentialSpendingData};
|
||||
use nym_gateway_requests::shared_key::{LegacySharedKeys, SharedGatewayKey, SharedSymmetricKey};
|
||||
use nym_gateway_requests::shared_key::SharedSymmetricKey;
|
||||
use sqlx::FromRow;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
@@ -18,33 +18,16 @@ pub struct PersistedSharedKeys {
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub client_address_bs58: String,
|
||||
pub derived_aes128_ctr_blake3_hmac_keys_bs58: Option<String>,
|
||||
pub derived_aes256_gcm_siv_key: Option<Vec<u8>>,
|
||||
pub derived_aes256_gcm_siv_key: Vec<u8>,
|
||||
pub last_used_authentication: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
impl TryFrom<PersistedSharedKeys> for SharedGatewayKey {
|
||||
impl TryFrom<PersistedSharedKeys> for SharedSymmetricKey {
|
||||
type Error = GatewayStorageError;
|
||||
|
||||
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(GatewayStorageError::MissingSharedKey {
|
||||
id: value.client_id,
|
||||
}),
|
||||
(Some(aes256gcm_siv), _) => {
|
||||
let current_key = SharedSymmetricKey::try_from_bytes(aes256gcm_siv)
|
||||
.map_err(|source| GatewayStorageError::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| GatewayStorageError::DataCorruption(source.to_string()))?;
|
||||
Ok(SharedGatewayKey::Legacy(legacy_key))
|
||||
}
|
||||
}
|
||||
SharedSymmetricKey::try_from_bytes(&value.derived_aes256_gcm_siv_key)
|
||||
.map_err(|source| GatewayStorageError::DataCorruption(source.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,26 +42,22 @@ impl SharedKeysManager {
|
||||
&self,
|
||||
client_id: i64,
|
||||
client_address_bs58: String,
|
||||
derived_aes128_ctr_blake3_hmac_keys_bs58: Option<&String>,
|
||||
derived_aes256_gcm_siv_key: Option<&Vec<u8>>,
|
||||
derived_aes256_gcm_siv_key: &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, derived_aes256_gcm_siv_key) VALUES (?, ?, ?, ?);
|
||||
INSERT OR IGNORE INTO shared_keys(client_id, client_address_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?;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
use async_trait::async_trait;
|
||||
use nym_credentials_interface::ClientTicket;
|
||||
use nym_gateway_requests::SharedGatewayKey;
|
||||
use nym_gateway_requests::SharedSymmetricKey;
|
||||
use nym_sphinx::DestinationAddressBytes;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
@@ -25,7 +25,7 @@ pub trait SharedKeyGatewayStorage {
|
||||
async fn insert_shared_keys(
|
||||
&self,
|
||||
client_address: DestinationAddressBytes,
|
||||
shared_keys: &SharedGatewayKey,
|
||||
shared_keys: &SharedSymmetricKey,
|
||||
) -> Result<i64, GatewayStorageError>;
|
||||
async fn get_shared_keys(
|
||||
&self,
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO transaction\n (hash, height, index, success, messages, memo, signatures, signer_infos, fee, gas_wanted, gas_used, raw_log, logs, events)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)\n ON CONFLICT (hash) DO UPDATE\n SET height = excluded.height,\n index = excluded.index,\n success = excluded.success,\n messages = excluded.messages,\n memo = excluded.memo,\n signatures = excluded.signatures,\n signer_infos = excluded.signer_infos,\n fee = excluded.fee,\n gas_wanted = excluded.gas_wanted,\n gas_used = excluded.gas_used,\n raw_log = excluded.raw_log,\n logs = excluded.logs,\n events = excluded.events\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int4",
|
||||
"Bool",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"TextArray",
|
||||
"Jsonb",
|
||||
"Jsonb",
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "08f4e54ac24fccd54f4208797b3749e457f8cd4ba3d7d906a7ab3bf5b4e7dc9c"
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO validator (consensus_address, consensus_pubkey)\n VALUES ($1, $2)\n ON CONFLICT DO NOTHING\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0d3709efacf763b06bf14803bb803b5ee5b27879b0026bb0480b3f2722318a75"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM pre_commit WHERE height < $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1c2fb0e9ffceca21ef8dbea19b116422b1f723d0a316314b50c43c8b29f8891d"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT height\n FROM block\n ORDER BY height ASC\n LIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "height",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "2561fb016951ea4cd29e43fb9a4a93e944b0d44ed1f7c1036f306e34372da11c"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE metadata SET last_processed_height = GREATEST(last_processed_height, $1)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2679cdf11fa66c7920678cde860c57402119ec7c3aae731b0da831327301466f"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE pruning SET last_pruned_height = $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "36ba5941aca6e7b604a10b8b0aba70635028f392fe794d6131827b083e1755e1"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT last_pruned_height FROM pruning\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "last_pruned_height",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "3bdf81a9db6075f6f77224c30553f419a849d4ec45af40b052a4cbf09b44f3ec"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM message WHERE height < $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "52c27143720ddfdfd0f5644b60f5b67fd9281ce1de0653efa53b9d9b93cf335d"
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO pre_commit (validator_address, height, timestamp, voting_power, proposer_priority)\n VALUES ($1, $2, $3, $4, $5)\n ON CONFLICT (validator_address, timestamp) DO NOTHING\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8",
|
||||
"Timestamp",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "62e14613f5ffe692346a79086857a22f0444fbc679db1c06b651fb8b5538b278"
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO block (height, hash, num_txs, total_gas, proposer_address, timestamp)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT DO NOTHING\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Text",
|
||||
"Int4",
|
||||
"Int8",
|
||||
"Text",
|
||||
"Timestamp"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "64a484fd46d8ec46797f944a4cced56b6e270ce186f0e49528865d1924343b78"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT height\n FROM block\n WHERE timestamp < $1\n ORDER BY timestamp DESC\n LIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "height",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamp"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "7e82426f5dbcadf1631ba1a806e19cc462d04222fb20ad76de2a40f3f4f8fe15"
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT height\n FROM block\n WHERE timestamp > $1\n ORDER BY timestamp\n LIMIT 1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "height",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Timestamp"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "9455331f9be5a3be28e2bd399a36b2e2d6a9ad4b225c4c883aafc4e9f0428008"
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT COUNT(*) as count FROM pre_commit\n WHERE\n validator_address = $1\n AND height >= $2\n AND height <= $3\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "count",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "bc7795e58ce71893c3f32a19db8e77b7bc0a1af315ffd42c3e68156d6e4ace70"
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT * FROM validator\n WHERE EXISTS (\n SELECT 1 FROM pre_commit\n WHERE height = $1\n AND pre_commit.validator_address = validator.consensus_address\n )\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "consensus_address",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "consensus_pubkey",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "be43d4873911deca784b7be0531ab7bd82ecd68041aa932a56c8ce09623251e4"
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT last_processed_height FROM metadata\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "last_processed_height",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": []
|
||||
},
|
||||
"nullable": [
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "c88d07fecc3f33deaa6e93db1469ce71582635df47f52dcf3fd1df4e7be6b96d"
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO message(transaction_hash, index, type, value, involved_accounts_addresses, height)\n VALUES ($1, $2, $3, $4, $5, $6)\n ON CONFLICT (transaction_hash, index) DO UPDATE\n SET height = excluded.height,\n type = excluded.type,\n value = excluded.value,\n involved_accounts_addresses = excluded.involved_accounts_addresses\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Int8",
|
||||
"Text",
|
||||
"Jsonb",
|
||||
"TextArray",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cc0ae74082d7d8a89f2d3364676890bbf6150ab394c72783114340d4def5f9ef"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM block WHERE height < $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "cdba9b267f143c8a8c6c3d6ed713cf00236490b86779559d84740ec18bcfa3a9"
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM transaction WHERE height < $1",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d89558c37c51e8e6b1e6a9d5a2b13d0598fd856aa019a0cbbae12d7cafb4672f"
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "nyxd-scraper-psql"
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
homepage.workspace = true
|
||||
documentation.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
readme.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-trait = { workspace = true }
|
||||
base64 = { workspace = true }
|
||||
itertools = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "postgres", "macros", "migrate", "time"] }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tracing.workspace = true
|
||||
|
||||
nyxd-scraper-shared = { path = "../nyxd-scraper-shared" }
|
||||
|
||||
# temp due to cosmrs redefinitions for serde
|
||||
cosmrs = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "postgres", "macros", "migrate"] }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,105 @@
|
||||
# Makefile for nyxd-scraper-psql database management
|
||||
|
||||
# --- Configuration ---
|
||||
TEST_DATABASE_URL := postgres://testuser:testpass@localhost:5433/nyxd_scraper_test
|
||||
|
||||
# Docker compose service names
|
||||
DB_SERVICE_NAME := postgres-test
|
||||
DB_CONTAINER_NAME := nyxd_scraper_psql_test
|
||||
|
||||
# Default target
|
||||
.PHONY: default
|
||||
default: help
|
||||
|
||||
# --- Main Targets ---
|
||||
.PHONY: prepare-pg
|
||||
prepare-pg: test-db-up test-db-wait test-db-migrate test-db-prepare test-db-down ## Setup PostgreSQL and prepare SQLx offline cache
|
||||
|
||||
.PHONY: test-db
|
||||
test-db: test-db-up test-db-wait test-db-migrate test-db-run test-db-down ## Run tests with PostgreSQL database
|
||||
|
||||
.PHONY: dev-db
|
||||
dev-db: test-db-up test-db-wait test-db-migrate ## Start PostgreSQL for development (keeps running)
|
||||
@echo "PostgreSQL is running on port 5433"
|
||||
@echo "Connection string: $(TEST_DATABASE_URL)"
|
||||
|
||||
.PHONY: dev-db-restart
|
||||
dev-db-restart: clean-db dev-db
|
||||
|
||||
# --- Docker Compose Targets ---
|
||||
.PHONY: test-db-up
|
||||
test-db-up: ## Start the PostgreSQL test database in the background
|
||||
@echo "Starting PostgreSQL test database..."
|
||||
docker compose up -d $(DB_SERVICE_NAME)
|
||||
|
||||
.PHONY: test-db-wait
|
||||
test-db-wait: ## Wait for the PostgreSQL database to be healthy
|
||||
@echo "Waiting for PostgreSQL database..."
|
||||
@while ! docker inspect --format='{{.State.Health.Status}}' $(DB_CONTAINER_NAME) 2>/dev/null | grep -q 'healthy'; do \
|
||||
echo -n "."; \
|
||||
sleep 1; \
|
||||
done; \
|
||||
echo " Database is healthy!"
|
||||
|
||||
.PHONY: test-db-down
|
||||
test-db-down: ## Stop and remove the test database
|
||||
@echo "Stopping PostgreSQL test database..."
|
||||
docker compose down
|
||||
|
||||
# --- SQLx Targets ---
|
||||
.PHONY: test-db-migrate
|
||||
test-db-migrate: ## Run database migrations against PostgreSQL
|
||||
@echo "Running PostgreSQL migrations..."
|
||||
DATABASE_URL="$(TEST_DATABASE_URL)" sqlx migrate run --source sql_migrations
|
||||
|
||||
.PHONY: test-db-prepare
|
||||
test-db-prepare: ## Run sqlx prepare for compile-time query verification
|
||||
@echo "Running sqlx prepare for PostgreSQL..."
|
||||
DATABASE_URL="$(TEST_DATABASE_URL)" cargo sqlx prepare
|
||||
|
||||
# --- Build and Test Targets ---
|
||||
.PHONY: test-db-run
|
||||
test-db-run: ## Run tests with PostgreSQL feature
|
||||
@echo "Running tests with PostgreSQL..."
|
||||
DATABASE_URL="$(TEST_DATABASE_URL)" cargo test --features pg --no-default-features
|
||||
|
||||
.PHONY: build-pg
|
||||
build-pg: ## Build with PostgreSQL feature
|
||||
@echo "Building with PostgreSQL feature..."
|
||||
cargo build
|
||||
|
||||
.PHONY: check-pg
|
||||
check-pg: ## Check code with PostgreSQL feature
|
||||
@echo "Checking code with PostgreSQL feature..."
|
||||
cargo check
|
||||
|
||||
.PHONY: clippy
|
||||
clippy: clippy-pg
|
||||
|
||||
.PHONY: clippy-pg
|
||||
clippy-pg: ## Run clippy with PostgreSQL feature
|
||||
@echo "Running clippy with PostgreSQL feature..."
|
||||
cargo clippy -- -D warnings
|
||||
|
||||
# --- Cleanup Targets ---
|
||||
.PHONY: clean
|
||||
clean: ## Clean build artifacts and SQLx cache
|
||||
cargo clean
|
||||
rm -rf .sqlx
|
||||
|
||||
.PHONY: clean-db
|
||||
clean-db: test-db-down ## Stop database and clean volumes
|
||||
docker volume rm -f nym-node-status-api_postgres_test_data 2>/dev/null || true
|
||||
|
||||
# --- Utility Targets ---
|
||||
.PHONY: sqlx-cli
|
||||
sqlx-cli: ## Install sqlx-cli if not already installed
|
||||
@command -v sqlx >/dev/null 2>&1 || cargo install sqlx-cli --features postgres
|
||||
|
||||
.PHONY: psql
|
||||
psql: ## Connect to the running PostgreSQL database with psql
|
||||
@docker exec -it $(DB_CONTAINER_NAME) psql -U testuser -d nyxd_scraper_test
|
||||
|
||||
.PHONY: help
|
||||
help: ## Show help for Makefile targets
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
|
||||
@@ -0,0 +1,80 @@
|
||||
## Quick Start with PostgreSQL
|
||||
|
||||
### 1. Install Prerequisites
|
||||
|
||||
```bash
|
||||
# Install sqlx-cli if not already installed
|
||||
make sqlx-cli
|
||||
```
|
||||
|
||||
### 2. Prepare PostgreSQL for Development
|
||||
|
||||
```bash
|
||||
# This will:
|
||||
# - Start PostgreSQL in Docker
|
||||
# - Run migrations
|
||||
# - Generate SQLx offline query cache
|
||||
# - Stop the database
|
||||
make prepare-pg
|
||||
```
|
||||
|
||||
### 3. Build with PostgreSQL
|
||||
|
||||
```bash
|
||||
# Build with PostgreSQL feature
|
||||
make build-pg
|
||||
|
||||
# Or manually:
|
||||
cargo build
|
||||
```
|
||||
|
||||
### 4. Run with PostgreSQL
|
||||
|
||||
```bash
|
||||
# Start PostgreSQL for development (keeps running)
|
||||
make dev-db
|
||||
|
||||
# In another terminal, run the application
|
||||
DATABASE_URL=postgres://testuser:testpass@localhost:5433/nym_node_status_api_test \
|
||||
cargo run
|
||||
```
|
||||
|
||||
## Makefile Targets
|
||||
|
||||
```bash
|
||||
make help # Show all available targets
|
||||
make prepare-pg # Setup PostgreSQL and prepare SQLx cache
|
||||
make dev-db # Start PostgreSQL for development
|
||||
make test-db # Run tests with PostgreSQL
|
||||
make build-pg # Build with PostgreSQL
|
||||
make psql # Connect to running PostgreSQL
|
||||
make clean # Clean build artifacts
|
||||
make clean-db # Stop database and clean volumes
|
||||
make dev-db-restart # Stop database, clean volumes, rebuild test database and restart
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
See `.env.example` for all configuration options. Key variable:
|
||||
|
||||
```bash
|
||||
# For PostgreSQL:
|
||||
DATABASE_URL=postgres://testuser:testpass@localhost:5433/nym_node_status_api_test
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### SQLx Offline Mode
|
||||
|
||||
If you see "no cached data for this query" errors:
|
||||
|
||||
1. Ensure PostgreSQL is running: `make dev-db`
|
||||
2. Run: `make test-db-prepare`
|
||||
|
||||
### Connection Refused
|
||||
|
||||
If you see "Connection refused" errors:
|
||||
|
||||
1. Check Docker is running: `docker ps`
|
||||
2. Check PostgreSQL container: `docker ps | grep nym_node_status_api_postgres_test`
|
||||
3. Restart database: `make test-db-down && make dev-db`
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
fn main() {
|
||||
if let Ok(database_url) = std::env::var("DATABASE_URL") {
|
||||
println!("cargo::rustc-env=DATABASE_URL={database_url}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
services:
|
||||
postgres-test:
|
||||
image: postgres:16-alpine
|
||||
container_name: nyxd_scraper_psql_test
|
||||
environment:
|
||||
POSTGRES_DB: nyxd_scraper_test
|
||||
POSTGRES_USER: testuser
|
||||
POSTGRES_PASSWORD: testpass
|
||||
ports:
|
||||
- '5433:5432' # Map to 5433 to avoid conflicts with default PostgreSQL
|
||||
healthcheck:
|
||||
test: [ 'CMD-SHELL', 'pg_isready -U testuser -d nyxd_scraper_test' ]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
# Optional: Add volume for persistent data during development
|
||||
# volumes:
|
||||
# - postgres_test_data:/var/lib/postgresql/data
|
||||
|
||||
# volumes:
|
||||
# postgres_test_data:
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
* Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
CREATE TABLE METADATA
|
||||
(
|
||||
id INTEGER PRIMARY KEY CHECK (id = 0),
|
||||
last_processed_height BIGINT NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,127 @@
|
||||
CREATE TABLE validator
|
||||
(
|
||||
consensus_address TEXT NOT NULL PRIMARY KEY, /* Validator consensus address */
|
||||
consensus_pubkey TEXT NOT NULL UNIQUE /* Validator consensus public key */
|
||||
);
|
||||
|
||||
CREATE TABLE pre_commit
|
||||
(
|
||||
validator_address TEXT NOT NULL REFERENCES validator (consensus_address),
|
||||
height BIGINT NOT NULL,
|
||||
timestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL,
|
||||
voting_power BIGINT NOT NULL,
|
||||
proposer_priority BIGINT NOT NULL,
|
||||
UNIQUE (validator_address, timestamp)
|
||||
);
|
||||
CREATE INDEX pre_commit_validator_address_index ON pre_commit (validator_address);
|
||||
CREATE INDEX pre_commit_height_index ON pre_commit (height);
|
||||
|
||||
CREATE TABLE block
|
||||
(
|
||||
height BIGINT UNIQUE PRIMARY KEY,
|
||||
hash TEXT NOT NULL UNIQUE,
|
||||
num_txs INTEGER DEFAULT 0,
|
||||
total_gas BIGINT DEFAULT 0,
|
||||
proposer_address TEXT REFERENCES validator (consensus_address),
|
||||
timestamp TIMESTAMP WITHOUT TIME ZONE NOT NULL
|
||||
);
|
||||
CREATE INDEX block_height_index ON block (height);
|
||||
CREATE INDEX block_hash_index ON block (hash);
|
||||
CREATE INDEX block_proposer_address_index ON block (proposer_address);
|
||||
ALTER TABLE block
|
||||
SET (
|
||||
autovacuum_vacuum_scale_factor = 0,
|
||||
autovacuum_analyze_scale_factor = 0,
|
||||
autovacuum_vacuum_threshold = 10000,
|
||||
autovacuum_analyze_threshold = 10000
|
||||
);
|
||||
|
||||
CREATE TABLE transaction
|
||||
(
|
||||
hash TEXT NOT NULL,
|
||||
height BIGINT NOT NULL REFERENCES block (height),
|
||||
"index" INTEGER NOT NULL, -- <<<=== not present in original bdjuno table, but it's quite useful
|
||||
success BOOLEAN NOT NULL,
|
||||
|
||||
/* Body */
|
||||
messages JSONB NOT NULL DEFAULT '[]'::JSONB,
|
||||
memo TEXT,
|
||||
signatures TEXT[] NOT NULL,
|
||||
|
||||
/* AuthInfo */
|
||||
signer_infos JSONB NOT NULL DEFAULT '[]'::JSONB,
|
||||
fee JSONB NOT NULL DEFAULT '{}'::JSONB,
|
||||
|
||||
/* Tx response */
|
||||
gas_wanted BIGINT DEFAULT 0,
|
||||
gas_used BIGINT DEFAULT 0,
|
||||
raw_log TEXT,
|
||||
logs JSONB,
|
||||
events JSONB,
|
||||
|
||||
CONSTRAINT unique_tx UNIQUE (hash)
|
||||
);
|
||||
CREATE INDEX transaction_hash_index ON transaction (hash);
|
||||
CREATE INDEX transaction_height_index ON transaction (height);
|
||||
|
||||
CREATE TYPE COIN AS
|
||||
(
|
||||
denom TEXT,
|
||||
amount TEXT
|
||||
);
|
||||
|
||||
CREATE TABLE message
|
||||
(
|
||||
transaction_hash TEXT NOT NULL,
|
||||
index BIGINT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
value JSONB NOT NULL,
|
||||
involved_accounts_addresses TEXT[] NOT NULL,
|
||||
height BIGINT NOT NULL,
|
||||
|
||||
funds COIN[] DEFAULT '{}',
|
||||
|
||||
FOREIGN KEY (transaction_hash) REFERENCES transaction (hash),
|
||||
CONSTRAINT unique_message_per_tx UNIQUE (transaction_hash, index)
|
||||
);
|
||||
CREATE INDEX message_transaction_hash_index ON message (transaction_hash);
|
||||
CREATE INDEX message_type_index ON message (type);
|
||||
CREATE INDEX message_involved_accounts_index ON message USING GIN (involved_accounts_addresses);
|
||||
|
||||
/**
|
||||
* This function is used to find all the utils that involve any of the given addresses and have
|
||||
* type that is one of the specified types.
|
||||
*/
|
||||
CREATE FUNCTION messages_by_address(
|
||||
addresses TEXT[],
|
||||
types TEXT[],
|
||||
"limit" BIGINT = 100,
|
||||
"offset" BIGINT = 0)
|
||||
RETURNS SETOF message AS
|
||||
$$
|
||||
SELECT *
|
||||
FROM message
|
||||
WHERE (cardinality(types) = 0 OR type = ANY (types))
|
||||
AND addresses && involved_accounts_addresses
|
||||
ORDER BY height DESC
|
||||
LIMIT "limit" OFFSET "offset"
|
||||
$$ LANGUAGE sql STABLE;
|
||||
|
||||
CREATE FUNCTION messages_by_type(
|
||||
types text[],
|
||||
"limit" bigint DEFAULT 100,
|
||||
"offset" bigint DEFAULT 0)
|
||||
RETURNS SETOF message AS
|
||||
$$
|
||||
SELECT *
|
||||
FROM message
|
||||
WHERE (cardinality(types) = 0 OR type = ANY (types))
|
||||
ORDER BY height DESC
|
||||
LIMIT "limit" OFFSET "offset"
|
||||
$$ LANGUAGE sql STABLE;
|
||||
|
||||
CREATE TABLE pruning
|
||||
(
|
||||
last_pruned_height BIGINT NOT NULL
|
||||
);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nyxd_scraper_shared::helpers::MalformedDataError;
|
||||
use nyxd_scraper_shared::storage::NyxdScraperStorageError;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PostgresScraperError {
|
||||
#[error("experienced internal database error: {0}")]
|
||||
InternalDatabaseError(#[from] sqlx::error::Error),
|
||||
|
||||
#[error("failed to perform startup SQL migration: {0}")]
|
||||
StartupMigrationFailure(#[from] sqlx::migrate::MigrateError),
|
||||
|
||||
#[error("failed to begin storage tx: {source}")]
|
||||
StorageTxBeginFailure {
|
||||
#[source]
|
||||
source: sqlx::error::Error,
|
||||
},
|
||||
|
||||
#[error("failed to commit storage tx: {source}")]
|
||||
StorageTxCommitFailure {
|
||||
#[source]
|
||||
source: sqlx::error::Error,
|
||||
},
|
||||
|
||||
#[error(transparent)]
|
||||
MalformedData(#[from] MalformedDataError),
|
||||
|
||||
// TOOD: add struct name
|
||||
#[error("json serialisation failure: {source}")]
|
||||
SerialisationFailure {
|
||||
#[from]
|
||||
source: serde_json::Error,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<PostgresScraperError> for NyxdScraperStorageError {
|
||||
fn from(err: PostgresScraperError) -> Self {
|
||||
NyxdScraperStorageError::new(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::storage::block_storage::PostgresScraperStorage;
|
||||
use nyxd_scraper_shared::NyxdScraper;
|
||||
|
||||
pub use nyxd_scraper_shared::constants;
|
||||
pub use nyxd_scraper_shared::error::ScraperError;
|
||||
pub use nyxd_scraper_shared::{
|
||||
BlockModule, MsgModule, NyxdScraperTransaction, ParsedTransactionResponse, PruningOptions,
|
||||
PruningStrategy, StartingBlockOpts, TxModule,
|
||||
};
|
||||
pub use storage::models;
|
||||
|
||||
pub mod error;
|
||||
pub mod storage;
|
||||
|
||||
pub type PostgresNyxdScraper = NyxdScraper<PostgresScraperStorage>;
|
||||
|
||||
// TODO: for now just use exactly the same config
|
||||
pub use nyxd_scraper_shared::Config;
|
||||
@@ -0,0 +1,243 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::PostgresScraperError;
|
||||
use crate::models::{CommitSignature, Validator};
|
||||
use crate::storage::manager::{
|
||||
StorageManager, prune_blocks, prune_messages, prune_pre_commits, prune_transactions,
|
||||
update_last_pruned,
|
||||
};
|
||||
use crate::storage::transaction::PostgresStorageTransaction;
|
||||
use async_trait::async_trait;
|
||||
use nyxd_scraper_shared::storage::helpers::log_db_operation_time;
|
||||
use nyxd_scraper_shared::storage::{NyxdScraperStorage, NyxdScraperStorageError};
|
||||
use sqlx::types::time::{OffsetDateTime, PrimitiveDateTime};
|
||||
use tokio::time::Instant;
|
||||
use tracing::{debug, error, info, instrument};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct PostgresScraperStorage {
|
||||
pub(crate) manager: StorageManager,
|
||||
}
|
||||
|
||||
impl PostgresScraperStorage {
|
||||
#[instrument]
|
||||
pub async fn init(
|
||||
connection_string: &str,
|
||||
run_migrations: &bool,
|
||||
) -> Result<Self, PostgresScraperError> {
|
||||
debug!("initialising scraper database with '{connection_string}'",);
|
||||
|
||||
let connection_pool = match sqlx::PgPool::connect(connection_string).await {
|
||||
Ok(db) => db,
|
||||
Err(err) => {
|
||||
error!("Failed to connect to SQLx database: {err}");
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
|
||||
if *run_migrations {
|
||||
if let Err(err) = sqlx::migrate!("./sql_migrations")
|
||||
.run(&connection_pool)
|
||||
.await
|
||||
{
|
||||
return Err(err.into());
|
||||
}
|
||||
}
|
||||
|
||||
info!("Database migration finished!");
|
||||
|
||||
let manager = StorageManager { connection_pool };
|
||||
manager.set_initial_metadata().await?;
|
||||
|
||||
let storage = PostgresScraperStorage { manager };
|
||||
|
||||
Ok(storage)
|
||||
}
|
||||
|
||||
#[instrument(skip(self))]
|
||||
pub async fn prune_storage(
|
||||
&self,
|
||||
oldest_to_keep: u32,
|
||||
current_height: u32,
|
||||
) -> Result<(), PostgresScraperError> {
|
||||
let start = Instant::now();
|
||||
|
||||
let mut tx = self.begin_processing_tx().await?;
|
||||
|
||||
prune_messages(oldest_to_keep.into(), &mut **tx).await?;
|
||||
prune_transactions(oldest_to_keep.into(), &mut **tx).await?;
|
||||
prune_pre_commits(oldest_to_keep.into(), &mut **tx).await?;
|
||||
prune_blocks(oldest_to_keep.into(), &mut **tx).await?;
|
||||
update_last_pruned(current_height.into(), &mut **tx).await?;
|
||||
|
||||
let commit_start = Instant::now();
|
||||
tx.inner
|
||||
.commit()
|
||||
.await
|
||||
.map_err(|source| PostgresScraperError::StorageTxCommitFailure { source })?;
|
||||
log_db_operation_time("committing pruning tx", commit_start);
|
||||
|
||||
log_db_operation_time("pruning storage", start);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub async fn begin_processing_tx(
|
||||
&self,
|
||||
) -> Result<PostgresStorageTransaction, PostgresScraperError> {
|
||||
debug!("starting storage tx");
|
||||
self.manager
|
||||
.connection_pool
|
||||
.begin()
|
||||
.await
|
||||
.map(|inner| PostgresStorageTransaction { inner })
|
||||
.map_err(|source| PostgresScraperError::StorageTxBeginFailure { source })
|
||||
}
|
||||
|
||||
pub async fn lowest_block_height(&self) -> Result<Option<i64>, PostgresScraperError> {
|
||||
Ok(self.manager.get_lowest_block().await?)
|
||||
}
|
||||
|
||||
pub async fn get_first_block_height_after(
|
||||
&self,
|
||||
time: OffsetDateTime,
|
||||
) -> Result<Option<i64>, PostgresScraperError> {
|
||||
let time = PrimitiveDateTime::new(time.date(), time.time());
|
||||
|
||||
Ok(self.manager.get_first_block_height_after(time).await?)
|
||||
}
|
||||
|
||||
pub async fn get_last_block_height_before(
|
||||
&self,
|
||||
time: OffsetDateTime,
|
||||
) -> Result<Option<i64>, PostgresScraperError> {
|
||||
let time = PrimitiveDateTime::new(time.date(), time.time());
|
||||
|
||||
Ok(self.manager.get_last_block_height_before(time).await?)
|
||||
}
|
||||
|
||||
pub async fn get_blocks_between(
|
||||
&self,
|
||||
start_time: OffsetDateTime,
|
||||
end_time: OffsetDateTime,
|
||||
) -> Result<i64, PostgresScraperError> {
|
||||
let Some(block_start) = self.get_first_block_height_after(start_time).await? else {
|
||||
return Ok(0);
|
||||
};
|
||||
let Some(block_end) = self.get_last_block_height_before(end_time).await? else {
|
||||
return Ok(0);
|
||||
};
|
||||
|
||||
Ok(block_end - block_start)
|
||||
}
|
||||
|
||||
pub async fn get_signed_between(
|
||||
&self,
|
||||
consensus_address: &str,
|
||||
start_height: i64,
|
||||
end_height: i64,
|
||||
) -> Result<i64, PostgresScraperError> {
|
||||
Ok(self
|
||||
.manager
|
||||
.get_signed_between(consensus_address, start_height, end_height)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn get_signed_between_times(
|
||||
&self,
|
||||
consensus_address: &str,
|
||||
start_time: OffsetDateTime,
|
||||
end_time: OffsetDateTime,
|
||||
) -> Result<i64, PostgresScraperError> {
|
||||
let Some(block_start) = self.get_first_block_height_after(start_time).await? else {
|
||||
return Ok(0);
|
||||
};
|
||||
let Some(block_end) = self.get_last_block_height_before(end_time).await? else {
|
||||
return Ok(0);
|
||||
};
|
||||
|
||||
self.get_signed_between(consensus_address, block_start, block_end)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_precommit(
|
||||
&self,
|
||||
consensus_address: &str,
|
||||
height: i64,
|
||||
) -> Result<Option<CommitSignature>, PostgresScraperError> {
|
||||
Ok(self
|
||||
.manager
|
||||
.get_precommit(consensus_address, height)
|
||||
.await?)
|
||||
}
|
||||
|
||||
pub async fn get_block_signers(
|
||||
&self,
|
||||
height: i64,
|
||||
) -> Result<Vec<Validator>, PostgresScraperError> {
|
||||
Ok(self.manager.get_block_validators(height).await?)
|
||||
}
|
||||
|
||||
pub async fn get_all_known_validators(&self) -> Result<Vec<Validator>, PostgresScraperError> {
|
||||
Ok(self.manager.get_validators().await?)
|
||||
}
|
||||
|
||||
pub async fn get_last_processed_height(&self) -> Result<i64, PostgresScraperError> {
|
||||
Ok(self.manager.get_last_processed_height().await?)
|
||||
}
|
||||
|
||||
pub async fn get_pruned_height(&self) -> Result<i64, PostgresScraperError> {
|
||||
Ok(self.manager.get_pruned_height().await?)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl NyxdScraperStorage for PostgresScraperStorage {
|
||||
type StorageTransaction = PostgresStorageTransaction;
|
||||
|
||||
async fn initialise(
|
||||
storage: &str,
|
||||
run_migrations: &bool,
|
||||
) -> Result<Self, NyxdScraperStorageError> {
|
||||
PostgresScraperStorage::init(storage, run_migrations)
|
||||
.await
|
||||
.map_err(NyxdScraperStorageError::from)
|
||||
}
|
||||
|
||||
async fn begin_processing_tx(
|
||||
&self,
|
||||
) -> Result<Self::StorageTransaction, NyxdScraperStorageError> {
|
||||
self.begin_processing_tx()
|
||||
.await
|
||||
.map_err(NyxdScraperStorageError::from)
|
||||
}
|
||||
|
||||
async fn get_last_processed_height(&self) -> Result<i64, NyxdScraperStorageError> {
|
||||
self.get_last_processed_height()
|
||||
.await
|
||||
.map_err(NyxdScraperStorageError::from)
|
||||
}
|
||||
|
||||
async fn get_pruned_height(&self) -> Result<i64, NyxdScraperStorageError> {
|
||||
self.get_pruned_height()
|
||||
.await
|
||||
.map_err(NyxdScraperStorageError::from)
|
||||
}
|
||||
|
||||
async fn lowest_block_height(&self) -> Result<Option<i64>, NyxdScraperStorageError> {
|
||||
self.lowest_block_height()
|
||||
.await
|
||||
.map_err(NyxdScraperStorageError::from)
|
||||
}
|
||||
|
||||
async fn prune_storage(
|
||||
&self,
|
||||
oldest_to_keep: u32,
|
||||
current_height: u32,
|
||||
) -> Result<(), NyxdScraperStorageError> {
|
||||
self.prune_storage(oldest_to_keep, current_height)
|
||||
.await
|
||||
.map_err(NyxdScraperStorageError::from)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use cosmrs::AccountId;
|
||||
use itertools::Itertools;
|
||||
use nyxd_scraper_shared::ParsedTransactionResponse;
|
||||
use std::str::FromStr;
|
||||
|
||||
// replicate behaviour of `CosmosMessageAddressesParser` from juno
|
||||
pub(crate) fn parse_addresses_from_events(tx: &ParsedTransactionResponse) -> Vec<String> {
|
||||
let mut addresses: Vec<String> = Vec::new();
|
||||
for event in &tx.tx_result.events {
|
||||
for attribute in &event.attributes {
|
||||
let Ok(value) = attribute.value_str() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
// Try parsing the address as an account address
|
||||
if let Ok(address) = AccountId::from_str(value) {
|
||||
addresses.push(address.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
addresses.into_iter().unique().collect()
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::storage::models::{CommitSignature, Validator};
|
||||
use nyxd_scraper_shared::storage::helpers::log_db_operation_time;
|
||||
use sqlx::types::JsonValue;
|
||||
use sqlx::types::time::PrimitiveDateTime;
|
||||
use sqlx::{Executor, Postgres};
|
||||
use tokio::time::Instant;
|
||||
use tracing::{instrument, trace};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct StorageManager {
|
||||
pub(crate) connection_pool: sqlx::Pool<Postgres>,
|
||||
}
|
||||
|
||||
impl StorageManager {
|
||||
pub(crate) async fn set_initial_metadata(&self) -> Result<(), sqlx::Error> {
|
||||
if sqlx::query("SELECT * from metadata")
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
sqlx::query("INSERT INTO metadata (id, last_processed_height) VALUES (0, 0)")
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn get_lowest_block(&self) -> Result<Option<i64>, sqlx::Error> {
|
||||
trace!("get_lowest_block");
|
||||
let start = Instant::now();
|
||||
|
||||
let maybe_record = sqlx::query!(
|
||||
r#"
|
||||
SELECT height
|
||||
FROM block
|
||||
ORDER BY height ASC
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await?;
|
||||
log_db_operation_time("get_lowest_block", start);
|
||||
|
||||
Ok(maybe_record.map(|x| x.height))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_first_block_height_after(
|
||||
&self,
|
||||
time: PrimitiveDateTime,
|
||||
) -> Result<Option<i64>, sqlx::Error> {
|
||||
trace!("get_first_block_height_after");
|
||||
let start = Instant::now();
|
||||
|
||||
let maybe_record = sqlx::query!(
|
||||
r#"
|
||||
SELECT height
|
||||
FROM block
|
||||
WHERE timestamp > $1
|
||||
ORDER BY timestamp
|
||||
LIMIT 1
|
||||
"#,
|
||||
time
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await?;
|
||||
log_db_operation_time("get_first_block_height_after", start);
|
||||
|
||||
Ok(maybe_record.map(|x| x.height))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_last_block_height_before(
|
||||
&self,
|
||||
time: PrimitiveDateTime,
|
||||
) -> Result<Option<i64>, sqlx::Error> {
|
||||
trace!("get_last_block_height_before");
|
||||
let start = Instant::now();
|
||||
|
||||
let maybe_record = sqlx::query!(
|
||||
r#"
|
||||
SELECT height
|
||||
FROM block
|
||||
WHERE timestamp < $1
|
||||
ORDER BY timestamp DESC
|
||||
LIMIT 1
|
||||
"#,
|
||||
time
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await?;
|
||||
log_db_operation_time("get_last_block_height_before", start);
|
||||
|
||||
Ok(maybe_record.map(|x| x.height))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_signed_between(
|
||||
&self,
|
||||
consensus_address: &str,
|
||||
start_height: i64,
|
||||
end_height: i64,
|
||||
) -> Result<i64, sqlx::Error> {
|
||||
trace!("get_signed_between");
|
||||
let start = Instant::now();
|
||||
|
||||
let count = sqlx::query!(
|
||||
r#"
|
||||
SELECT COUNT(*) as count FROM pre_commit
|
||||
WHERE
|
||||
validator_address = $1
|
||||
AND height >= $2
|
||||
AND height <= $3
|
||||
"#,
|
||||
consensus_address,
|
||||
start_height,
|
||||
end_height
|
||||
)
|
||||
.fetch_one(&self.connection_pool)
|
||||
.await?
|
||||
.count;
|
||||
log_db_operation_time("get_signed_between", start);
|
||||
|
||||
Ok(count.unwrap_or(0))
|
||||
}
|
||||
|
||||
pub(crate) async fn get_precommit(
|
||||
&self,
|
||||
consensus_address: &str,
|
||||
height: i64,
|
||||
) -> Result<Option<CommitSignature>, sqlx::Error> {
|
||||
trace!("get_precommit");
|
||||
let start = Instant::now();
|
||||
|
||||
let res = sqlx::query_as(
|
||||
r#"
|
||||
SELECT * FROM pre_commit
|
||||
WHERE validator_address = $1
|
||||
AND height = $2
|
||||
"#,
|
||||
)
|
||||
.bind(consensus_address)
|
||||
.bind(height)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await?;
|
||||
log_db_operation_time("get_precommit", start);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_block_validators(
|
||||
&self,
|
||||
height: i64,
|
||||
) -> Result<Vec<Validator>, sqlx::Error> {
|
||||
trace!("get_block_validators");
|
||||
let start = Instant::now();
|
||||
|
||||
let res = sqlx::query_as!(
|
||||
Validator,
|
||||
r#"
|
||||
SELECT * FROM validator
|
||||
WHERE EXISTS (
|
||||
SELECT 1 FROM pre_commit
|
||||
WHERE height = $1
|
||||
AND pre_commit.validator_address = validator.consensus_address
|
||||
)
|
||||
"#,
|
||||
height
|
||||
)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await?;
|
||||
log_db_operation_time("get_block_validators", start);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_validators(&self) -> Result<Vec<Validator>, sqlx::Error> {
|
||||
trace!("get_validators");
|
||||
let start = Instant::now();
|
||||
|
||||
let res = sqlx::query_as("SELECT * FROM validator")
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await?;
|
||||
log_db_operation_time("get_validators", start);
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub(crate) async fn get_last_processed_height(&self) -> Result<i64, sqlx::Error> {
|
||||
trace!("get_last_processed_height");
|
||||
let start = Instant::now();
|
||||
|
||||
let maybe_record = sqlx::query!(
|
||||
r#"
|
||||
SELECT last_processed_height FROM metadata
|
||||
"#
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await?;
|
||||
log_db_operation_time("get_last_processed_height", start);
|
||||
|
||||
if let Some(row) = maybe_record {
|
||||
#[allow(clippy::useless_conversion)]
|
||||
Ok(row.last_processed_height.into())
|
||||
} else {
|
||||
Ok(-1)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_pruned_height(&self) -> Result<i64, sqlx::Error> {
|
||||
trace!("get_pruned_height");
|
||||
let start = Instant::now();
|
||||
|
||||
let maybe_record = sqlx::query!(
|
||||
r#"
|
||||
SELECT last_pruned_height FROM pruning
|
||||
"#
|
||||
)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await?;
|
||||
|
||||
log_db_operation_time("get_pruned_height", start);
|
||||
|
||||
if let Some(row) = maybe_record {
|
||||
Ok(row.last_pruned_height)
|
||||
} else {
|
||||
Ok(-1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// make those generic over executor so that they could be performed over connection pool and a tx
|
||||
|
||||
#[instrument(skip(executor))]
|
||||
pub(crate) async fn insert_validator<'a, E>(
|
||||
consensus_address: String,
|
||||
consensus_pubkey: String,
|
||||
executor: E,
|
||||
) -> Result<(), sqlx::Error>
|
||||
where
|
||||
E: Executor<'a, Database = Postgres>,
|
||||
{
|
||||
trace!("insert_validator");
|
||||
let start = Instant::now();
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO validator (consensus_address, consensus_pubkey)
|
||||
VALUES ($1, $2)
|
||||
ON CONFLICT DO NOTHING
|
||||
"#,
|
||||
consensus_address,
|
||||
consensus_pubkey
|
||||
)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("insert_validator", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(executor))]
|
||||
pub(crate) async fn insert_block<'a, E>(
|
||||
height: i64,
|
||||
hash: String,
|
||||
num_txs: i32,
|
||||
total_gas: i64,
|
||||
proposer_address: String,
|
||||
timestamp: PrimitiveDateTime,
|
||||
executor: E,
|
||||
) -> Result<(), sqlx::Error>
|
||||
where
|
||||
E: Executor<'a, Database = Postgres>,
|
||||
{
|
||||
trace!("insert_block");
|
||||
let start = Instant::now();
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO block (height, hash, num_txs, total_gas, proposer_address, timestamp)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT DO NOTHING
|
||||
"#,
|
||||
height,
|
||||
hash,
|
||||
num_txs,
|
||||
total_gas,
|
||||
proposer_address,
|
||||
timestamp
|
||||
)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("insert_block", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(executor))]
|
||||
pub(crate) async fn insert_precommit<'a, E>(
|
||||
validator_address: String,
|
||||
height: i64,
|
||||
timestamp: PrimitiveDateTime,
|
||||
voting_power: i64,
|
||||
proposer_priority: i64,
|
||||
executor: E,
|
||||
) -> Result<(), sqlx::Error>
|
||||
where
|
||||
E: Executor<'a, Database = Postgres>,
|
||||
{
|
||||
trace!("insert_precommit");
|
||||
let start = Instant::now();
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO pre_commit (validator_address, height, timestamp, voting_power, proposer_priority)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (validator_address, timestamp) DO NOTHING
|
||||
"#,
|
||||
validator_address,
|
||||
height,
|
||||
timestamp,
|
||||
voting_power,
|
||||
proposer_priority
|
||||
)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("insert_precommit", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(executor))]
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) async fn insert_transaction<'a, E>(
|
||||
hash: String,
|
||||
height: i64,
|
||||
index: i32,
|
||||
success: bool,
|
||||
messages: JsonValue,
|
||||
memo: String,
|
||||
signatures: Vec<String>,
|
||||
signer_infos: JsonValue,
|
||||
fee: JsonValue,
|
||||
gas_wanted: i64,
|
||||
gas_used: i64,
|
||||
raw_log: String,
|
||||
logs: JsonValue,
|
||||
events: JsonValue,
|
||||
executor: E,
|
||||
) -> Result<(), sqlx::Error>
|
||||
where
|
||||
E: Executor<'a, Database = Postgres>,
|
||||
{
|
||||
trace!("insert_transaction");
|
||||
let start = Instant::now();
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO transaction
|
||||
(hash, height, index, success, messages, memo, signatures, signer_infos, fee, gas_wanted, gas_used, raw_log, logs, events)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
||||
ON CONFLICT (hash) DO UPDATE
|
||||
SET height = excluded.height,
|
||||
index = excluded.index,
|
||||
success = excluded.success,
|
||||
messages = excluded.messages,
|
||||
memo = excluded.memo,
|
||||
signatures = excluded.signatures,
|
||||
signer_infos = excluded.signer_infos,
|
||||
fee = excluded.fee,
|
||||
gas_wanted = excluded.gas_wanted,
|
||||
gas_used = excluded.gas_used,
|
||||
raw_log = excluded.raw_log,
|
||||
logs = excluded.logs,
|
||||
events = excluded.events
|
||||
"#,
|
||||
hash,
|
||||
height,
|
||||
index,
|
||||
success,
|
||||
messages,
|
||||
memo,
|
||||
&signatures,
|
||||
signer_infos,
|
||||
fee,
|
||||
gas_wanted,
|
||||
gas_used,
|
||||
raw_log,
|
||||
logs,
|
||||
events,
|
||||
)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("insert_transaction", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[instrument(skip(executor))]
|
||||
pub(crate) async fn insert_message<'a, E>(
|
||||
transaction_hash: String,
|
||||
index: i64,
|
||||
typ: String,
|
||||
value: JsonValue,
|
||||
involved_account_addresses: Vec<String>,
|
||||
height: i64,
|
||||
executor: E,
|
||||
) -> Result<(), sqlx::Error>
|
||||
where
|
||||
E: Executor<'a, Database = Postgres>,
|
||||
{
|
||||
trace!("insert_message");
|
||||
let start = Instant::now();
|
||||
|
||||
sqlx::query!(
|
||||
r#"
|
||||
INSERT INTO message(transaction_hash, index, type, value, involved_accounts_addresses, height)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
ON CONFLICT (transaction_hash, index) DO UPDATE
|
||||
SET height = excluded.height,
|
||||
type = excluded.type,
|
||||
value = excluded.value,
|
||||
involved_accounts_addresses = excluded.involved_accounts_addresses
|
||||
"#,
|
||||
transaction_hash,
|
||||
index,
|
||||
typ,
|
||||
value,
|
||||
&involved_account_addresses,
|
||||
height,
|
||||
)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("insert_message", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(executor))]
|
||||
pub(crate) async fn update_last_processed<'a, E>(
|
||||
height: i64,
|
||||
executor: E,
|
||||
) -> Result<(), sqlx::Error>
|
||||
where
|
||||
E: Executor<'a, Database = Postgres>,
|
||||
{
|
||||
trace!("update_last_processed");
|
||||
let start = Instant::now();
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE metadata SET last_processed_height = GREATEST(last_processed_height, $1)",
|
||||
height as i32
|
||||
)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("update_last_processed", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(executor))]
|
||||
pub(crate) async fn update_last_pruned<'a, E>(height: i64, executor: E) -> Result<(), sqlx::Error>
|
||||
where
|
||||
E: Executor<'a, Database = Postgres>,
|
||||
{
|
||||
trace!("update_last_pruned");
|
||||
let start = Instant::now();
|
||||
|
||||
sqlx::query!("UPDATE pruning SET last_pruned_height = $1", height)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("update_last_pruned", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn prune_blocks<'a, E>(oldest_to_keep: i64, executor: E) -> Result<(), sqlx::Error>
|
||||
where
|
||||
E: Executor<'a, Database = Postgres>,
|
||||
{
|
||||
trace!("prune_blocks");
|
||||
let start = Instant::now();
|
||||
|
||||
sqlx::query!("DELETE FROM block WHERE height < $1", oldest_to_keep)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("prune_blocks", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn prune_pre_commits<'a, E>(
|
||||
oldest_to_keep: i64,
|
||||
executor: E,
|
||||
) -> Result<(), sqlx::Error>
|
||||
where
|
||||
E: Executor<'a, Database = Postgres>,
|
||||
{
|
||||
trace!("prune_pre_commits");
|
||||
let start = Instant::now();
|
||||
|
||||
sqlx::query!("DELETE FROM pre_commit WHERE height < $1", oldest_to_keep)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("prune_pre_commits", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn prune_transactions<'a, E>(
|
||||
oldest_to_keep: i64,
|
||||
executor: E,
|
||||
) -> Result<(), sqlx::Error>
|
||||
where
|
||||
E: Executor<'a, Database = Postgres>,
|
||||
{
|
||||
trace!("prune_transactions");
|
||||
let start = Instant::now();
|
||||
sqlx::query!("DELETE FROM transaction WHERE height < $1", oldest_to_keep)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("prune_transactions", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn prune_messages<'a, E>(
|
||||
oldest_to_keep: i64,
|
||||
executor: E,
|
||||
) -> Result<(), sqlx::Error>
|
||||
where
|
||||
E: Executor<'a, Database = Postgres>,
|
||||
{
|
||||
trace!("prune_messages");
|
||||
let start = Instant::now();
|
||||
sqlx::query!("DELETE FROM message WHERE height < $1", oldest_to_keep)
|
||||
.execute(executor)
|
||||
.await?;
|
||||
log_db_operation_time("prune_messages", start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod block_storage;
|
||||
mod helpers;
|
||||
mod manager;
|
||||
pub mod models;
|
||||
pub mod transaction;
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright 2023 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::FromRow;
|
||||
use sqlx::types::time::OffsetDateTime;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Hash, FromRow)]
|
||||
pub struct Validator {
|
||||
pub consensus_address: String,
|
||||
pub consensus_pubkey: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct Block {
|
||||
pub height: i64,
|
||||
pub hash: String,
|
||||
pub num_txs: u32,
|
||||
pub total_gas: i64,
|
||||
pub proposer_address: String,
|
||||
pub timestamp: OffsetDateTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, FromRow)]
|
||||
pub struct CommitSignature {
|
||||
pub height: i64,
|
||||
pub validator_address: String,
|
||||
pub voting_power: i64,
|
||||
pub proposer_priority: i64,
|
||||
pub timestamp: OffsetDateTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, sqlx::Type)]
|
||||
#[sqlx(type_name = "coin")]
|
||||
pub struct DbCoin {
|
||||
pub amount: String,
|
||||
pub denom: String,
|
||||
}
|
||||
|
||||
impl From<cosmrs::proto::cosmos::base::v1beta1::Coin> for DbCoin {
|
||||
fn from(coin: cosmrs::proto::cosmos::base::v1beta1::Coin) -> Self {
|
||||
Self {
|
||||
amount: coin.amount,
|
||||
denom: coin.denom,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,299 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::PostgresScraperError;
|
||||
use crate::storage::helpers::parse_addresses_from_events;
|
||||
use crate::storage::manager::{
|
||||
insert_block, insert_message, insert_precommit, insert_transaction, insert_validator,
|
||||
update_last_processed,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use base64::Engine as _;
|
||||
use base64::engine::general_purpose;
|
||||
use cosmrs::proto;
|
||||
use nyxd_scraper_shared::ParsedTransactionResponse;
|
||||
use nyxd_scraper_shared::helpers::{
|
||||
validator_consensus_address, validator_info, validator_pubkey_to_bech32,
|
||||
};
|
||||
use nyxd_scraper_shared::storage::validators::Response;
|
||||
use nyxd_scraper_shared::storage::{
|
||||
Block, Commit, CommitSig, NyxdScraperStorageError, NyxdScraperTransaction, validators,
|
||||
};
|
||||
use serde_json::json;
|
||||
use sqlx::types::time::{OffsetDateTime, PrimitiveDateTime};
|
||||
use sqlx::{Postgres, Transaction};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use tracing::{debug, error, trace, warn};
|
||||
|
||||
pub struct PostgresStorageTransaction {
|
||||
pub(super) inner: Transaction<'static, Postgres>,
|
||||
}
|
||||
|
||||
impl Deref for PostgresStorageTransaction {
|
||||
type Target = Transaction<'static, Postgres>;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for PostgresStorageTransaction {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.inner
|
||||
}
|
||||
}
|
||||
|
||||
impl PostgresStorageTransaction {
|
||||
async fn persist_validators(
|
||||
&mut self,
|
||||
validators: &validators::Response,
|
||||
) -> Result<(), PostgresScraperError> {
|
||||
debug!("persisting {} validators", validators.total);
|
||||
for validator in &validators.validators {
|
||||
let consensus_address = validator_consensus_address(validator.address)?;
|
||||
let consensus_pubkey = validator_pubkey_to_bech32(validator.pub_key)?;
|
||||
|
||||
insert_validator(
|
||||
consensus_address.to_string(),
|
||||
consensus_pubkey.to_string(),
|
||||
self.inner.as_mut(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn persist_block_data(
|
||||
&mut self,
|
||||
block: &Block,
|
||||
total_gas: i64,
|
||||
) -> Result<(), PostgresScraperError> {
|
||||
let proposer_address =
|
||||
validator_consensus_address(block.header.proposer_address)?.to_string();
|
||||
|
||||
let offset_datetime: OffsetDateTime = block.header.time.into();
|
||||
let time = PrimitiveDateTime::new(offset_datetime.date(), offset_datetime.time());
|
||||
|
||||
insert_block(
|
||||
block.header.height.into(),
|
||||
block.header.hash().to_string(),
|
||||
block.data.len() as i32,
|
||||
total_gas,
|
||||
proposer_address,
|
||||
time,
|
||||
self.inner.as_mut(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn persist_commits(
|
||||
&mut self,
|
||||
commits: &Commit,
|
||||
validators: &validators::Response,
|
||||
) -> Result<(), PostgresScraperError> {
|
||||
debug!("persisting up to {} commits", commits.signatures.len());
|
||||
let height: i64 = commits.height.into();
|
||||
|
||||
for commit_sig in &commits.signatures {
|
||||
let (validator_id, timestamp, signature) = match commit_sig {
|
||||
CommitSig::BlockIdFlagAbsent => {
|
||||
trace!("absent signature");
|
||||
continue;
|
||||
}
|
||||
CommitSig::BlockIdFlagCommit {
|
||||
validator_address,
|
||||
timestamp,
|
||||
signature,
|
||||
} => (validator_address, timestamp, signature),
|
||||
CommitSig::BlockIdFlagNil {
|
||||
validator_address,
|
||||
timestamp,
|
||||
signature,
|
||||
} => (validator_address, timestamp, signature),
|
||||
};
|
||||
|
||||
let validator = validator_info(*validator_id, validators)?;
|
||||
let validator_address = validator_consensus_address(*validator_id)?;
|
||||
|
||||
if signature.is_none() {
|
||||
warn!("empty signature for {validator_address} at height {height}");
|
||||
continue;
|
||||
}
|
||||
|
||||
let offset_datetime: OffsetDateTime = (*timestamp).into();
|
||||
let time = PrimitiveDateTime::new(offset_datetime.date(), offset_datetime.time());
|
||||
|
||||
insert_precommit(
|
||||
validator_address.to_string(),
|
||||
height,
|
||||
time,
|
||||
validator.power.into(),
|
||||
validator.proposer_priority.value(),
|
||||
self.inner.as_mut(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn persist_txs(
|
||||
&mut self,
|
||||
txs: &[ParsedTransactionResponse],
|
||||
) -> Result<(), PostgresScraperError> {
|
||||
debug!("persisting {} txs", txs.len());
|
||||
|
||||
for chain_tx in txs {
|
||||
// bdjuno style, base64 encode them
|
||||
let signatures = chain_tx
|
||||
.tx
|
||||
.signatures
|
||||
.iter()
|
||||
.map(|sig| general_purpose::STANDARD.encode(sig))
|
||||
.collect();
|
||||
|
||||
let messages = chain_tx
|
||||
.parsed_messages
|
||||
.values()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let signer_infos = chain_tx
|
||||
.tx
|
||||
.auth_info
|
||||
.signer_infos
|
||||
.iter()
|
||||
.map(|info| proto::cosmos::tx::v1beta1::SignerInfo::from(info.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let hash = chain_tx.hash.to_string();
|
||||
let height = chain_tx.height.into();
|
||||
let index = chain_tx.index as i32;
|
||||
|
||||
let log = serde_json::to_value(chain_tx.tx_result.log.clone())
|
||||
.inspect_err(|e| error!(hash, height, index, "Failed to parse logs: {e}"))
|
||||
.unwrap_or_default();
|
||||
let events = &chain_tx.tx_result.events;
|
||||
|
||||
insert_transaction(
|
||||
hash,
|
||||
height,
|
||||
index,
|
||||
chain_tx.tx_result.code.is_ok(),
|
||||
serde_json::Value::Array(messages),
|
||||
chain_tx.tx.body.memo.clone(),
|
||||
signatures,
|
||||
serde_json::to_value(signer_infos)?,
|
||||
serde_json::to_value(&chain_tx.tx.auth_info.fee)?,
|
||||
chain_tx.tx_result.gas_wanted,
|
||||
chain_tx.tx_result.gas_used,
|
||||
chain_tx.tx_result.log.clone(),
|
||||
json!(log),
|
||||
json!(events),
|
||||
self.inner.as_mut(),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn persist_messages(
|
||||
&mut self,
|
||||
txs: &[ParsedTransactionResponse],
|
||||
) -> Result<(), PostgresScraperError> {
|
||||
debug!("persisting messages");
|
||||
|
||||
for chain_tx in txs {
|
||||
let involved_addresses = parse_addresses_from_events(chain_tx);
|
||||
for (index, msg) in chain_tx.tx.body.messages.iter().enumerate() {
|
||||
let parsed_message = chain_tx.parsed_messages.get(&index);
|
||||
let value = serde_json::to_value(parsed_message)?;
|
||||
|
||||
insert_message(
|
||||
chain_tx.hash.to_string(),
|
||||
index as i64,
|
||||
msg.type_url.clone(),
|
||||
value,
|
||||
involved_addresses.clone(),
|
||||
chain_tx.height.into(),
|
||||
self.inner.as_mut(),
|
||||
)
|
||||
.await?
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn update_last_processed(&mut self, height: i64) -> Result<(), PostgresScraperError> {
|
||||
debug!("update_last_processed");
|
||||
update_last_processed(height, self.inner.as_mut()).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl NyxdScraperTransaction for PostgresStorageTransaction {
|
||||
async fn commit(self) -> Result<(), NyxdScraperStorageError> {
|
||||
self.inner
|
||||
.commit()
|
||||
.await
|
||||
.map_err(PostgresScraperError::from)
|
||||
.map_err(NyxdScraperStorageError::from)
|
||||
}
|
||||
|
||||
async fn persist_validators(
|
||||
&mut self,
|
||||
validators: &Response,
|
||||
) -> Result<(), NyxdScraperStorageError> {
|
||||
self.persist_validators(validators)
|
||||
.await
|
||||
.map_err(NyxdScraperStorageError::from)
|
||||
}
|
||||
|
||||
async fn persist_block_data(
|
||||
&mut self,
|
||||
block: &Block,
|
||||
total_gas: i64,
|
||||
) -> Result<(), NyxdScraperStorageError> {
|
||||
self.persist_block_data(block, total_gas)
|
||||
.await
|
||||
.map_err(NyxdScraperStorageError::from)
|
||||
}
|
||||
|
||||
async fn persist_commits(
|
||||
&mut self,
|
||||
commits: &Commit,
|
||||
validators: &Response,
|
||||
) -> Result<(), NyxdScraperStorageError> {
|
||||
self.persist_commits(commits, validators)
|
||||
.await
|
||||
.map_err(NyxdScraperStorageError::from)
|
||||
}
|
||||
|
||||
async fn persist_txs(
|
||||
&mut self,
|
||||
txs: &[ParsedTransactionResponse],
|
||||
) -> Result<(), NyxdScraperStorageError> {
|
||||
self.persist_txs(txs)
|
||||
.await
|
||||
.map_err(NyxdScraperStorageError::from)
|
||||
}
|
||||
|
||||
async fn persist_messages(
|
||||
&mut self,
|
||||
txs: &[ParsedTransactionResponse],
|
||||
) -> Result<(), NyxdScraperStorageError> {
|
||||
self.persist_messages(txs)
|
||||
.await
|
||||
.map_err(NyxdScraperStorageError::from)
|
||||
}
|
||||
|
||||
async fn update_last_processed(&mut self, height: i64) -> Result<(), NyxdScraperStorageError> {
|
||||
self.update_last_processed(height)
|
||||
.await
|
||||
.map_err(NyxdScraperStorageError::from)
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
[package]
|
||||
name = "nyxd-scraper"
|
||||
name = "nyxd-scraper-shared"
|
||||
version = "0.1.0"
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
@@ -8,19 +8,22 @@ documentation.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
readme.workspace = true
|
||||
|
||||
[dependencies]
|
||||
async-trait.workspace = true
|
||||
base64.workspace = true
|
||||
const_format = { workspace = true }
|
||||
cosmrs.workspace = true
|
||||
cosmos-sdk-proto = { workspace = true, features = ["serde", "cosmwasm"] } # we need to explicitly include serde feature
|
||||
eyre = { workspace = true }
|
||||
futures.workspace = true
|
||||
humantime = { workspace = true }
|
||||
ibc-proto = { workspace = true, features = ["serde"] }
|
||||
prost = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate", "time"] }
|
||||
serde_json = { workspace = true }
|
||||
tendermint.workspace = true
|
||||
tendermint-rpc = { workspace = true, features = ["websocket-client", "http-client"] }
|
||||
thiserror.workspace = true
|
||||
@@ -32,11 +35,5 @@ tracing.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
|
||||
# TEMP
|
||||
#nym-bin-common = { path = "../bin-common", features = ["basic_tracing"]}
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
[lints]
|
||||
workspace = true
|
||||
+15
-10
@@ -8,7 +8,7 @@ use crate::block_requester::BlockRequest;
|
||||
use crate::error::ScraperError;
|
||||
use crate::modules::{BlockModule, MsgModule, TxModule};
|
||||
use crate::rpc_client::RpcClient;
|
||||
use crate::storage::{ScraperStorage, persist_block};
|
||||
use crate::storage::{NyxdScraperStorage, NyxdScraperTransaction, persist_block};
|
||||
use futures::StreamExt;
|
||||
use std::cmp::max;
|
||||
use std::collections::{BTreeMap, HashSet, VecDeque};
|
||||
@@ -77,7 +77,7 @@ impl BlockProcessorConfig {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct BlockProcessor {
|
||||
pub struct BlockProcessor<S> {
|
||||
config: BlockProcessorConfig,
|
||||
cancel: CancellationToken,
|
||||
synced: Arc<Notify>,
|
||||
@@ -90,7 +90,7 @@ pub struct BlockProcessor {
|
||||
rpc_client: RpcClient,
|
||||
incoming: UnboundedReceiverStream<BlockToProcess>,
|
||||
block_requester: Sender<BlockRequest>,
|
||||
storage: ScraperStorage,
|
||||
storage: S,
|
||||
|
||||
// future work: rather than sending each msg to every msg module,
|
||||
// let them subscribe based on `type_url` inside the message itself
|
||||
@@ -101,14 +101,17 @@ pub struct BlockProcessor {
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
impl BlockProcessor {
|
||||
impl<S> BlockProcessor<S>
|
||||
where
|
||||
S: NyxdScraperStorage,
|
||||
{
|
||||
pub async fn new(
|
||||
config: BlockProcessorConfig,
|
||||
cancel: CancellationToken,
|
||||
synced: Arc<Notify>,
|
||||
incoming: UnboundedReceiver<BlockToProcess>,
|
||||
block_requester: Sender<BlockRequest>,
|
||||
storage: ScraperStorage,
|
||||
storage: S,
|
||||
rpc_client: RpcClient,
|
||||
) -> Result<Self, ScraperError> {
|
||||
let last_processed = storage.get_last_processed_height().await?;
|
||||
@@ -164,7 +167,11 @@ impl BlockProcessor {
|
||||
|
||||
// process the entire block as a transaction so that if anything fails,
|
||||
// we won't end up with a corrupted storage.
|
||||
let mut tx = self.storage.begin_processing_tx().await?;
|
||||
let mut tx = self
|
||||
.storage
|
||||
.begin_processing_tx()
|
||||
.await
|
||||
.map_err(ScraperError::tx_begin_failure)?;
|
||||
|
||||
persist_block(&full_info, &mut tx, self.config.store_precommits).await?;
|
||||
|
||||
@@ -192,10 +199,8 @@ impl BlockProcessor {
|
||||
}
|
||||
|
||||
let commit_start = Instant::now();
|
||||
tx.commit()
|
||||
.await
|
||||
.map_err(|source| ScraperError::StorageTxCommitFailure { source })?;
|
||||
crate::storage::log_db_operation_time("committing processing tx", commit_start);
|
||||
tx.commit().await.map_err(ScraperError::tx_commit_failure)?;
|
||||
crate::storage::helpers::log_db_operation_time("committing processing tx", commit_start);
|
||||
|
||||
self.last_processed_height = full_info.block.header.height.value() as u32;
|
||||
self.last_processed_at = Instant::now();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user