Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a5c760d8ff |
@@ -5,6 +5,7 @@ on:
|
||||
paths:
|
||||
- 'clients/**'
|
||||
- 'common/**'
|
||||
- 'explorer-api/**'
|
||||
- 'gateway/**'
|
||||
- 'integrations/**'
|
||||
- 'nym-api/**'
|
||||
@@ -38,7 +39,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ arc-ubuntu-22.04, custom-windows-11, custom-macos-15 ]
|
||||
os: [ arc-ubuntu-22.04, custom-windows-11, custom-runner-mac-m1 ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
|
||||
@@ -44,10 +44,8 @@ jobs:
|
||||
echo "Tag is empty"
|
||||
exit 1
|
||||
fi
|
||||
# first, list all tags for logging purposes
|
||||
curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq
|
||||
# check if there's a matching tag
|
||||
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' )
|
||||
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' )
|
||||
if [[ $exists = "true" ]]; then
|
||||
echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo"
|
||||
exit 1
|
||||
@@ -55,5 +53,5 @@ jobs:
|
||||
echo "Version '$TAG' doesn't exist on the remote"
|
||||
else
|
||||
echo "Unknown output '$exists'"
|
||||
exit 2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -44,10 +44,8 @@ jobs:
|
||||
echo "Tag is empty"
|
||||
exit 1
|
||||
fi
|
||||
# first, list all tags for logging purposes
|
||||
curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq
|
||||
# check if there's a matching tag
|
||||
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq -r --arg tag "$TAG" 'any(.tags[]; . == $tag)' )
|
||||
exists=$(curl -su ${{ secrets.HARBOR_ROBOT_USERNAME }}:${{ secrets.HARBOR_ROBOT_SECRET }} "$registry/v2/$repo_name/tags/list" | jq --arg tag $TAG '.tags | contains([$tag])' )
|
||||
if [[ $exists = "true" ]]; then
|
||||
echo "Version '$TAG' defined in Cargo.toml ALREADY EXISTS as tag in harbor repo"
|
||||
exit 1
|
||||
@@ -55,5 +53,5 @@ jobs:
|
||||
echo "Version '$TAG' doesn't exist on the remote"
|
||||
else
|
||||
echo "Unknown output '$exists'"
|
||||
exit 2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -66,6 +66,7 @@ jobs:
|
||||
with:
|
||||
name: my-artifact
|
||||
path: |
|
||||
target/release/explorer-api
|
||||
target/release/nym-client
|
||||
target/release/nym-socks5-client
|
||||
target/release/nym-api
|
||||
@@ -74,13 +75,14 @@ jobs:
|
||||
target/release/nymvisor
|
||||
target/release/nym-node
|
||||
retention-days: 30
|
||||
|
||||
|
||||
- id: create-release
|
||||
name: Upload to release based on tag name
|
||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
|
||||
uses: softprops/action-gh-release@v2
|
||||
if: github.event_name == 'release'
|
||||
with:
|
||||
files: |
|
||||
target/release/explorer-api
|
||||
target/release/nym-client
|
||||
target/release/nym-socks5-client
|
||||
target/release/nym-api
|
||||
|
||||
@@ -40,6 +40,7 @@ validator-config
|
||||
validator-api-config.toml
|
||||
dist
|
||||
storybook-static
|
||||
envs/qwerty.env
|
||||
.parcel-cache
|
||||
**/.DS_Store
|
||||
cpu-cycles/libcpucycles/build
|
||||
|
||||
@@ -4,30 +4,6 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https://
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2025.11-cheddar] (2025-06-10)
|
||||
|
||||
- No autoremoval of peers ([#5831])
|
||||
- Set cached storage counters to 0 ([#5812])
|
||||
- hack: temporarily use next.config.js instead of next.config.ts ([#5805])
|
||||
- chore: resolve 1.87 clippy warnings ([#5802])
|
||||
- Nym Statistics API ([#5800])
|
||||
- QoL: RequestPath trait for http-api-client ([#5788])
|
||||
- Fix contains ticketbook function that always returned true ([#5787])
|
||||
- swap a decode into a fromrow to please future postgres feature ([#5785])
|
||||
- Make address cache configurable ([#5784])
|
||||
- Track wireguard credential retries ([#5783])
|
||||
|
||||
[#5831]: https://github.com/nymtech/nym/pull/5831
|
||||
[#5812]: https://github.com/nymtech/nym/pull/5812
|
||||
[#5805]: https://github.com/nymtech/nym/pull/5805
|
||||
[#5802]: https://github.com/nymtech/nym/pull/5802
|
||||
[#5800]: https://github.com/nymtech/nym/pull/5800
|
||||
[#5788]: https://github.com/nymtech/nym/pull/5788
|
||||
[#5787]: https://github.com/nymtech/nym/pull/5787
|
||||
[#5785]: https://github.com/nymtech/nym/pull/5785
|
||||
[#5784]: https://github.com/nymtech/nym/pull/5784
|
||||
[#5783]: https://github.com/nymtech/nym/pull/5783
|
||||
|
||||
## [2025.10-brie] (2025-05-27)
|
||||
|
||||
- Backport PR 5779 ([#5801])
|
||||
|
||||
Generated
+265
-1018
File diff suppressed because it is too large
Load Diff
+1
-4
@@ -66,10 +66,8 @@ members = [
|
||||
"common/nym-id",
|
||||
"common/nym-metrics",
|
||||
"common/nym_offline_compact_ecash",
|
||||
"common/nymkkt",
|
||||
"common/nymnoise",
|
||||
"common/nymnoise/keys",
|
||||
"common/nympsq",
|
||||
"common/nymsphinx",
|
||||
"common/nymsphinx/acknowledgements",
|
||||
"common/nymsphinx/addressing",
|
||||
@@ -103,6 +101,7 @@ members = [
|
||||
"common/wireguard-types",
|
||||
"documentation/autodoc",
|
||||
"gateway",
|
||||
"integrations/bity",
|
||||
"nym-api",
|
||||
"nym-api/nym-api-requests",
|
||||
"nym-browser-extension/storage",
|
||||
@@ -128,7 +127,6 @@ members = [
|
||||
"service-providers/common",
|
||||
"service-providers/ip-packet-router",
|
||||
"service-providers/network-requester",
|
||||
"sqlx-pool-guard",
|
||||
"tools/echo-server",
|
||||
"tools/internal/contract-state-importer/importer-cli",
|
||||
"tools/internal/contract-state-importer/importer-contract",
|
||||
@@ -290,7 +288,6 @@ petgraph = "0.6.5"
|
||||
pin-project = "1.1"
|
||||
pin-project-lite = "0.2.16"
|
||||
publicsuffix = "2.3.0"
|
||||
proc_pidinfo = "0.1.3"
|
||||
quote = "1"
|
||||
rand = "0.8.5"
|
||||
rand_chacha = "0.3"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-client"
|
||||
version = "1.1.57"
|
||||
version = "1.1.56"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>", "Jędrzej Stuczyński <andrew@nymtech.net>"]
|
||||
description = "Implementation of the Nym Client"
|
||||
edition = "2021"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "nym-socks5-client"
|
||||
version = "1.1.57"
|
||||
version = "1.1.56"
|
||||
authors = ["Dave Hrycyszyn <futurechimp@users.noreply.github.com>"]
|
||||
description = "A SOCKS5 localhost proxy that converts incoming messages to Sphinx and sends them to a Nym address"
|
||||
edition = "2021"
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::BadGateway;
|
||||
use std::{io, path::PathBuf};
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -18,6 +19,7 @@ pub enum StorageError {
|
||||
|
||||
#[error("failed to perform sqlx migration: {source}")]
|
||||
MigrationError {
|
||||
#[source]
|
||||
#[from]
|
||||
source: sqlx::migrate::MigrateError,
|
||||
},
|
||||
@@ -30,6 +32,7 @@ pub enum StorageError {
|
||||
|
||||
#[error("failed to run the SQL query: {source}")]
|
||||
QueryError {
|
||||
#[source]
|
||||
#[from]
|
||||
source: sqlx::error::Error,
|
||||
},
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
// Copyright 2022-2024 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{
|
||||
client::replies::reply_storage::{fs_backend, CombinedReplyStorage, ReplyStorageBackend},
|
||||
config,
|
||||
config::Config,
|
||||
error::ClientCoreError,
|
||||
use crate::client::replies::reply_storage::{
|
||||
fs_backend, CombinedReplyStorage, ReplyStorageBackend,
|
||||
};
|
||||
use crate::config;
|
||||
use crate::config::Config;
|
||||
use crate::error::ClientCoreError;
|
||||
use log::{error, info, trace};
|
||||
use nym_bandwidth_controller::BandwidthController;
|
||||
use nym_client_core_gateways_storage::OnDiskGatewaysDetails;
|
||||
use nym_credential_storage::storage::Storage as CredentialStorage;
|
||||
use nym_validator_client::{nyxd, QueryHttpRpcNyxdClient};
|
||||
use std::{io, path::Path};
|
||||
use nym_validator_client::nyxd;
|
||||
use nym_validator_client::QueryHttpRpcNyxdClient;
|
||||
use std::path::Path;
|
||||
use std::{fs, io};
|
||||
use time::OffsetDateTime;
|
||||
use url::Url;
|
||||
|
||||
@@ -20,11 +22,11 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
|
||||
db_path: P,
|
||||
surb_config: &config::ReplySurbs,
|
||||
) -> Result<fs_backend::Backend, ClientCoreError> {
|
||||
info!("Creating fresh surb database");
|
||||
info!("creating fresh surb database");
|
||||
let mut storage_backend = match fs_backend::Backend::init(db_path).await {
|
||||
Ok(backend) => backend,
|
||||
Err(err) => {
|
||||
error!("setup_fresh_backend: Failed to setup persistent storage backend for our reply needs: {err}");
|
||||
error!("failed to setup persistent storage backend for our reply needs: {err}");
|
||||
return Err(ClientCoreError::SurbStorageError {
|
||||
source: Box::new(err),
|
||||
});
|
||||
@@ -38,15 +40,14 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
|
||||
surb_config.minimum_reply_surb_storage_threshold,
|
||||
surb_config.maximum_reply_surb_storage_threshold,
|
||||
);
|
||||
match storage_backend.init_fresh(&mem_store).await {
|
||||
Ok(()) => Ok(storage_backend),
|
||||
Err(err) => {
|
||||
storage_backend.shutdown().await;
|
||||
Err(ClientCoreError::SurbStorageError {
|
||||
source: Box::new(err),
|
||||
})
|
||||
}
|
||||
}
|
||||
storage_backend
|
||||
.init_fresh(&mem_store)
|
||||
.await
|
||||
.map_err(|err| ClientCoreError::SurbStorageError {
|
||||
source: Box::new(err),
|
||||
})?;
|
||||
|
||||
Ok(storage_backend)
|
||||
}
|
||||
|
||||
// fn setup_inactive_backend(surb_config: &config::ReplySurbs) -> fs_backend::Backend {
|
||||
@@ -57,11 +58,12 @@ async fn setup_fresh_backend<P: AsRef<Path>>(
|
||||
// )
|
||||
// }
|
||||
|
||||
async fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
|
||||
fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()> {
|
||||
let db_path = db_path.as_ref();
|
||||
debug_assert!(db_path.exists());
|
||||
|
||||
let now = OffsetDateTime::now_utc().unix_timestamp();
|
||||
|
||||
let suffix = format!("_{now}.corrupted");
|
||||
|
||||
let new_extension =
|
||||
@@ -70,15 +72,11 @@ async fn archive_corrupted_database<P: AsRef<Path>>(db_path: P) -> io::Result<()
|
||||
} else {
|
||||
suffix
|
||||
};
|
||||
let renamed = db_path.with_extension(new_extension);
|
||||
|
||||
tokio::fs::rename(db_path, &renamed).await.inspect_err(|_| {
|
||||
error!(
|
||||
"Failed to rename corrupt database file: {} to {}",
|
||||
db_path.display(),
|
||||
renamed.display()
|
||||
);
|
||||
})
|
||||
let mut renamed = db_path.to_owned();
|
||||
renamed.set_extension(new_extension);
|
||||
|
||||
fs::rename(db_path, renamed)
|
||||
}
|
||||
|
||||
pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
|
||||
@@ -89,12 +87,13 @@ pub async fn setup_fs_reply_surb_backend<P: AsRef<Path>>(
|
||||
// the existing one
|
||||
let db_path = db_path.as_ref();
|
||||
if db_path.exists() {
|
||||
info!("Loading existing surb database");
|
||||
info!("loading existing surb database");
|
||||
match fs_backend::Backend::try_load(db_path, surb_config.fresh_sender_tags).await {
|
||||
Ok(backend) => Ok(backend),
|
||||
Err(err) => {
|
||||
error!("setup_fs_reply_surb_backend: Failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
|
||||
archive_corrupted_database(db_path).await?;
|
||||
error!("failed to setup persistent storage backend for our reply needs: {err}. We're going to create a fresh database instead. This behaviour might change in the future");
|
||||
|
||||
archive_corrupted_database(db_path)?;
|
||||
setup_fresh_backend(db_path, surb_config).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,26 +17,15 @@ nym-crypto = { path = "../../crypto", optional = true, default-features = false
|
||||
nym-sphinx = { path = "../../nymsphinx" }
|
||||
nym-task = { path = "../../task" }
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.tokio]
|
||||
workspace = true
|
||||
features = ["fs"]
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
|
||||
workspace = true
|
||||
features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"]
|
||||
optional = true
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard]
|
||||
path = "../../../sqlx-pool-guard"
|
||||
|
||||
[build-dependencies]
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
sqlx = { workspace = true, features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
"macros",
|
||||
"migrate",
|
||||
] }
|
||||
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
|
||||
|
||||
[features]
|
||||
fs-surb-storage = ["sqlx", "nym-crypto", "nym-crypto/hashing"]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::{io, path::PathBuf};
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -29,6 +30,7 @@ pub enum StorageError {
|
||||
|
||||
#[error("failed to perform sqlx migration: {source}")]
|
||||
MigrationError {
|
||||
#[source]
|
||||
#[from]
|
||||
source: sqlx::migrate::MigrateError,
|
||||
},
|
||||
@@ -41,6 +43,7 @@ pub enum StorageError {
|
||||
|
||||
#[error("failed to run the SQL query: {source}")]
|
||||
QueryError {
|
||||
#[source]
|
||||
#[from]
|
||||
source: sqlx::error::Error,
|
||||
},
|
||||
|
||||
@@ -15,11 +15,9 @@ use sqlx::{
|
||||
};
|
||||
use std::path::Path;
|
||||
|
||||
use sqlx_pool_guard::SqlitePoolGuard;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StorageManager {
|
||||
connection_pool: SqlitePoolGuard,
|
||||
pub connection_pool: sqlx::SqlitePool,
|
||||
}
|
||||
|
||||
// all SQL goes here
|
||||
@@ -39,7 +37,7 @@ impl StorageManager {
|
||||
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
||||
.synchronous(SqliteSynchronous::Normal)
|
||||
.auto_vacuum(SqliteAutoVacuum::Incremental)
|
||||
.filename(&database_path)
|
||||
.filename(database_path)
|
||||
.create_if_missing(fresh)
|
||||
.disable_statement_logging();
|
||||
|
||||
@@ -51,15 +49,11 @@ impl StorageManager {
|
||||
}
|
||||
};
|
||||
|
||||
let connection_pool =
|
||||
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
|
||||
|
||||
if let Err(err) = sqlx::migrate!("./fs_surbs_migrations")
|
||||
.run(&*connection_pool)
|
||||
.run(&connection_pool)
|
||||
.await
|
||||
{
|
||||
error!("Failed to initialize SQLx database: {err}");
|
||||
connection_pool.close().await;
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
@@ -67,43 +61,38 @@ impl StorageManager {
|
||||
Ok(StorageManager { connection_pool })
|
||||
}
|
||||
|
||||
/// Close connection pool waiting for all connections to be closed.
|
||||
pub async fn close_pool(&self) {
|
||||
self.connection_pool.close().await;
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn status_table_exists(&self) -> Result<bool, sqlx::Error> {
|
||||
sqlx::query!("SELECT name FROM sqlite_master WHERE type='table' AND name='status'")
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await
|
||||
.map(|r| r.is_some())
|
||||
}
|
||||
|
||||
pub async fn create_status_table(&self) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("INSERT INTO status(flush_in_progress, previous_flush_timestamp, client_in_use) VALUES (0, 0, 1)")
|
||||
.execute(&*self.connection_pool)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_flush_status(&self) -> Result<bool, sqlx::Error> {
|
||||
sqlx::query!("SELECT flush_in_progress FROM status;")
|
||||
.fetch_one(&*self.connection_pool)
|
||||
.fetch_one(&self.connection_pool)
|
||||
.await
|
||||
.map(|r| r.flush_in_progress > 0)
|
||||
}
|
||||
|
||||
pub async fn set_previous_flush_timestamp(&self, timestamp: i64) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("UPDATE status SET previous_flush_timestamp = ?", timestamp)
|
||||
.execute(&*self.connection_pool)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_previous_flush_timestamp(&self) -> Result<i64, sqlx::Error> {
|
||||
sqlx::query!("SELECT previous_flush_timestamp FROM status;")
|
||||
.fetch_one(&*self.connection_pool)
|
||||
.fetch_one(&self.connection_pool)
|
||||
.await
|
||||
.map(|r| r.previous_flush_timestamp)
|
||||
}
|
||||
@@ -111,14 +100,14 @@ impl StorageManager {
|
||||
pub async fn set_flush_status(&self, in_progress: bool) -> Result<(), sqlx::Error> {
|
||||
let in_progress_int = i64::from(in_progress);
|
||||
sqlx::query!("UPDATE status SET flush_in_progress = ?", in_progress_int)
|
||||
.execute(&*self.connection_pool)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_client_in_use_status(&self) -> Result<bool, sqlx::Error> {
|
||||
sqlx::query!("SELECT client_in_use FROM status;")
|
||||
.fetch_one(&*self.connection_pool)
|
||||
.fetch_one(&self.connection_pool)
|
||||
.await
|
||||
.map(|r| r.client_in_use > 0)
|
||||
}
|
||||
@@ -126,21 +115,21 @@ impl StorageManager {
|
||||
pub async fn set_client_in_use_status(&self, in_use: bool) -> Result<(), sqlx::Error> {
|
||||
let in_use_int = i64::from(in_use);
|
||||
sqlx::query!("UPDATE status SET client_in_use = ?", in_use_int)
|
||||
.execute(&*self.connection_pool)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_all_tags(&self) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("DELETE FROM sender_tag;")
|
||||
.execute(&*self.connection_pool)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_tags(&self) -> Result<Vec<StoredSenderTag>, sqlx::Error> {
|
||||
sqlx::query_as!(StoredSenderTag, "SELECT * FROM sender_tag;",)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -152,21 +141,21 @@ impl StorageManager {
|
||||
stored_tag.recipient,
|
||||
stored_tag.tag
|
||||
)
|
||||
.execute(&*self.connection_pool)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn delete_all_reply_keys(&self) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("DELETE FROM reply_key;")
|
||||
.execute(&*self.connection_pool)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_reply_keys(&self) -> Result<Vec<StoredReplyKey>, sqlx::Error> {
|
||||
sqlx::query_as!(StoredReplyKey, "SELECT * FROM reply_key;",)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -182,14 +171,14 @@ impl StorageManager {
|
||||
stored_reply_key.reply_key,
|
||||
stored_reply_key.sent_at_timestamp
|
||||
)
|
||||
.execute(&*self.connection_pool)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_surb_senders(&self) -> Result<Vec<StoredSurbSender>, sqlx::Error> {
|
||||
sqlx::query_as!(StoredSurbSender, "SELECT * FROM reply_surb_sender;",)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -204,7 +193,7 @@ impl StorageManager {
|
||||
stored_surb_sender.tag,
|
||||
stored_surb_sender.last_sent_timestamp
|
||||
)
|
||||
.execute(&*self.connection_pool)
|
||||
.execute(&self.connection_pool)
|
||||
.await?
|
||||
.last_insert_rowid();
|
||||
Ok(id)
|
||||
@@ -222,17 +211,17 @@ impl StorageManager {
|
||||
"#,
|
||||
sender_id
|
||||
)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn delete_all_reply_surb_data(&self) -> Result<(), sqlx::Error> {
|
||||
sqlx::query!("DELETE FROM reply_surb;")
|
||||
.execute(&*self.connection_pool)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
|
||||
sqlx::query!("DELETE FROM reply_surb_sender;")
|
||||
.execute(&*self.connection_pool)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
@@ -250,7 +239,7 @@ impl StorageManager {
|
||||
stored_reply_surb.reply_surb,
|
||||
stored_reply_surb.encoded_key_rotation
|
||||
)
|
||||
.execute(&*self.connection_pool)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -264,7 +253,7 @@ impl StorageManager {
|
||||
SELECT min_reply_surb_threshold as "min_reply_surb_threshold: u32", max_reply_surb_threshold as "max_reply_surb_threshold: u32" FROM reply_surb_storage_metadata;
|
||||
"#,
|
||||
)
|
||||
.fetch_one(&*self.connection_pool)
|
||||
.fetch_one(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -278,7 +267,7 @@ impl StorageManager {
|
||||
"#,
|
||||
metadata.min_reply_surb_threshold,
|
||||
metadata.max_reply_surb_threshold,
|
||||
).execute(&*self.connection_pool).await?;
|
||||
).execute(&self.connection_pool).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::backend::fs_backend::manager::StorageManager;
|
||||
use crate::backend::fs_backend::models::{
|
||||
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag, StoredSurbSender,
|
||||
};
|
||||
use crate::surb_storage::ReceivedReplySurbs;
|
||||
use crate::{
|
||||
backend::fs_backend::{
|
||||
manager::StorageManager,
|
||||
models::{
|
||||
ReplySurbStorageMetadata, StoredReplyKey, StoredReplySurb, StoredSenderTag,
|
||||
StoredSurbSender,
|
||||
},
|
||||
},
|
||||
surb_storage::ReceivedReplySurbs,
|
||||
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys,
|
||||
UsedSenderTags,
|
||||
CombinedReplyStorage, ReceivedReplySurbsMap, ReplyStorageBackend, SentReplyKeys, UsedSenderTags,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use log::{debug, error, info, warn};
|
||||
use nym_sphinx::anonymous_replies::requests::AnonymousSenderTag;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
@@ -44,17 +41,15 @@ impl Backend {
|
||||
}
|
||||
|
||||
let manager = StorageManager::init(database_path, true).await?;
|
||||
match manager.create_status_table().await {
|
||||
Ok(()) => Ok(Backend {
|
||||
temporary_old_path: None,
|
||||
database_path: owned_path,
|
||||
manager,
|
||||
}),
|
||||
Err(err) => {
|
||||
manager.close_pool().await;
|
||||
Err(err.into())
|
||||
}
|
||||
}
|
||||
manager.create_status_table().await?;
|
||||
|
||||
let backend = Backend {
|
||||
temporary_old_path: None,
|
||||
database_path: owned_path,
|
||||
manager,
|
||||
};
|
||||
|
||||
Ok(backend)
|
||||
}
|
||||
|
||||
pub async fn try_load<P: AsRef<Path>>(
|
||||
@@ -69,28 +64,7 @@ impl Backend {
|
||||
}
|
||||
|
||||
let manager = StorageManager::init(database_path, false).await?;
|
||||
match Self::try_load_inner(&manager, fresh_sender_tags).await {
|
||||
Ok(()) => Ok(Backend {
|
||||
temporary_old_path: None,
|
||||
database_path: owned_path,
|
||||
manager,
|
||||
}),
|
||||
Err(e) => {
|
||||
manager.close_pool().await;
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gracefully close sqlite connection pool and drop backend.
|
||||
pub async fn shutdown(self) {
|
||||
self.manager.close_pool().await
|
||||
}
|
||||
|
||||
async fn try_load_inner(
|
||||
manager: &StorageManager,
|
||||
fresh_sender_tags: bool,
|
||||
) -> Result<(), StorageError> {
|
||||
// the database flush wasn't fully finished and thus the data is in inconsistent state
|
||||
// (we don't really know what's properly saved or what's not)
|
||||
if manager.get_flush_status().await? {
|
||||
@@ -152,11 +126,20 @@ impl Backend {
|
||||
manager.delete_all_tags().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(Backend {
|
||||
temporary_old_path: None,
|
||||
database_path: owned_path,
|
||||
// manager: StorageManagerState::Storage(manager),
|
||||
manager,
|
||||
})
|
||||
}
|
||||
|
||||
async fn close_pool(&mut self) {
|
||||
self.manager.connection_pool.close().await;
|
||||
}
|
||||
|
||||
async fn rotate(&mut self) -> Result<(), StorageError> {
|
||||
self.manager.close_pool().await;
|
||||
self.close_pool().await;
|
||||
|
||||
let new_extension = if let Some(existing_extension) =
|
||||
self.database_path.extension().and_then(|ext| ext.to_str())
|
||||
@@ -169,8 +152,7 @@ impl Backend {
|
||||
let mut temp_old = self.database_path.clone();
|
||||
temp_old.set_extension(new_extension);
|
||||
|
||||
tokio::fs::rename(&self.database_path, &temp_old)
|
||||
.await
|
||||
fs::rename(&self.database_path, &temp_old)
|
||||
.map_err(|err| StorageError::DatabaseRenameError { source: err })?;
|
||||
self.manager = StorageManager::init(&self.database_path, true).await?;
|
||||
self.manager.create_status_table().await?;
|
||||
@@ -179,10 +161,9 @@ impl Backend {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn remove_old(&mut self) -> Result<(), StorageError> {
|
||||
fn remove_old(&mut self) -> Result<(), StorageError> {
|
||||
if let Some(old_path) = self.temporary_old_path.take() {
|
||||
tokio::fs::remove_file(old_path)
|
||||
.await
|
||||
fs::remove_file(old_path)
|
||||
.map_err(|err| StorageError::DatabaseOldFileRemoveError { source: err })
|
||||
} else {
|
||||
warn!("the old database file doesn't seem to exist!");
|
||||
@@ -354,7 +335,7 @@ impl ReplyStorageBackend for Backend {
|
||||
self.dump_reply_surb_storage_metadata(surbs_ref).await?;
|
||||
self.dump_reply_surbs(surbs_ref).await?;
|
||||
|
||||
self.remove_old().await?;
|
||||
self.remove_old()?;
|
||||
self.end_storage_flush().await
|
||||
}
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ where
|
||||
self.backend.load_surb_storage().await
|
||||
}
|
||||
|
||||
// this will have to get enabled after merging develop
|
||||
pub async fn flush_on_shutdown(
|
||||
mut self,
|
||||
mem_state: CombinedReplyStorage,
|
||||
@@ -49,6 +50,7 @@ where
|
||||
shutdown.recv().await;
|
||||
|
||||
info!("PersistentReplyStorage is flushing all reply-related data to underlying storage");
|
||||
info!("you MUST NOT forcefully shutdown now or you risk data corruption!");
|
||||
if let Err(err) = self.backend.flush_surb_storage(&mem_state).await {
|
||||
error!("failed to flush our reply-related data to the persistent storage: {err}")
|
||||
} else {
|
||||
|
||||
@@ -133,7 +133,7 @@ impl ManagedConnection {
|
||||
debug!("Managed to establish connection to {}", self.address);
|
||||
|
||||
let noise_stream =
|
||||
match upgrade_noise_initiator(stream, &self.noise_config, None).await {
|
||||
match upgrade_noise_initiator(stream, &self.noise_config).await {
|
||||
Ok(noise_stream) => noise_stream,
|
||||
Err(err) => {
|
||||
error!("Failed to perform Noise handshake with {address} - {err}");
|
||||
|
||||
@@ -16,8 +16,8 @@ async fn main() {
|
||||
let prefix = "n";
|
||||
let denom: Denom = "unym".parse().unwrap();
|
||||
let signer_mnemonic: bip39::Mnemonic = "<MNEMONIC WITH FUNDS HERE>".parse().unwrap();
|
||||
let validator = "https://rpc.sandbox.nymtech.net";
|
||||
let to_address: AccountId = "n1pefc2utwpy5w78p2kqdsfmpjxfwmn9d39k5mqa".parse().unwrap();
|
||||
let validator = "https://qwerty-validator.qa.nymte.ch";
|
||||
let to_address: AccountId = "n19kdst4srf76xgwe55jg32mpcpcyf6aqgp6qrdk".parse().unwrap();
|
||||
|
||||
let signer = DirectSecp256k1HdWallet::from_mnemonic(prefix, signer_mnemonic);
|
||||
let signer_address = signer.try_derive_accounts().unwrap()[0].address().clone();
|
||||
|
||||
@@ -60,7 +60,7 @@ pub async fn send(args: Args, client: &SigningClient) {
|
||||
"Nodesguru: https://nym.explorers.guru/transaction/{}",
|
||||
&res.hash
|
||||
);
|
||||
println!("Mintscan: https://ping.pub/nyx/tx/{}", &res.hash);
|
||||
println!("Mintscan: https://www.mintscan.io/nyx/txs/{}", &res.hash);
|
||||
println!("Transaction result code: {}", &res.tx_result.code.value());
|
||||
println!("Transaction hash: {}", &res.hash);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ pub async fn send_multiple(args: Args, client: &SigningClient) {
|
||||
"Nodesguru: https://nym.explorers.guru/transaction/{}",
|
||||
&res.hash
|
||||
);
|
||||
println!("Mintscan: https://ping.pub/nyx/tx/{}", &res.hash);
|
||||
println!("Mintscan: https://www.mintscan.io/nyx/txs/{}", &res.hash);
|
||||
println!("Transaction result code: {}", &res.tx_result.code.value());
|
||||
println!("Transaction hash: {}", &res.hash);
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ pub async fn execute(args: Args, client: SigningClient) {
|
||||
&res.transaction_hash
|
||||
);
|
||||
println!(
|
||||
"Mintscan: https://ping.pub/nyx/tx/{}",
|
||||
"Mintscan: https://www.mintscan.io/nyx/txs/{}",
|
||||
&res.transaction_hash
|
||||
);
|
||||
println!("Transaction hash: {}", &res.transaction_hash);
|
||||
|
||||
@@ -20,8 +20,6 @@ nym-credentials = { path = "../credentials" }
|
||||
nym-compact-ecash = { path = "../nym_offline_compact_ecash" }
|
||||
nym-ecash-time = { path = "../ecash-time" }
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx-pool-guard]
|
||||
path = "../../sqlx-pool-guard"
|
||||
|
||||
[target."cfg(not(target_arch = \"wasm32\"))".dependencies.sqlx]
|
||||
workspace = true
|
||||
@@ -33,13 +31,8 @@ features = ["rt-multi-thread", "net", "signal", "fs"]
|
||||
|
||||
|
||||
[build-dependencies]
|
||||
sqlx = { workspace = true, features = [
|
||||
"runtime-tokio-rustls",
|
||||
"sqlite",
|
||||
"macros",
|
||||
"migrate",
|
||||
] }
|
||||
sqlx = { workspace = true, features = ["runtime-tokio-rustls", "sqlite", "macros", "migrate"] }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
||||
|
||||
[features]
|
||||
persistent-storage = ["bincode", "serde"]
|
||||
persistent-storage = ["bincode", "serde"]
|
||||
@@ -7,11 +7,10 @@ use crate::models::{
|
||||
};
|
||||
use nym_ecash_time::Date;
|
||||
use sqlx::{Executor, Sqlite, Transaction};
|
||||
use sqlx_pool_guard::SqlitePoolGuard;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SqliteEcashTicketbookManager {
|
||||
connection_pool: SqlitePoolGuard,
|
||||
connection_pool: sqlx::SqlitePool,
|
||||
}
|
||||
|
||||
impl SqliteEcashTicketbookManager {
|
||||
@@ -20,7 +19,7 @@ impl SqliteEcashTicketbookManager {
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `connection_pool`: database connection pool to use.
|
||||
pub fn new(connection_pool: SqlitePoolGuard) -> Self {
|
||||
pub fn new(connection_pool: sqlx::SqlitePool) -> Self {
|
||||
SqliteEcashTicketbookManager { connection_pool }
|
||||
}
|
||||
|
||||
@@ -34,7 +33,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"DELETE FROM ecash_ticketbook WHERE expiration_date <= ?",
|
||||
deadline
|
||||
)
|
||||
.execute(&*self.connection_pool)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -61,7 +60,7 @@ impl SqliteEcashTicketbookManager {
|
||||
data,
|
||||
expiration_date,
|
||||
)
|
||||
.execute(&*self.connection_pool)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
@@ -91,7 +90,7 @@ impl SqliteEcashTicketbookManager {
|
||||
epoch_id,
|
||||
total_tickets,
|
||||
used_tickets,
|
||||
).execute(&*self.connection_pool).await?;
|
||||
).execute(&self.connection_pool).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -106,7 +105,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"#,
|
||||
)
|
||||
.bind(data)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await?
|
||||
.is_some();
|
||||
|
||||
@@ -122,7 +121,7 @@ impl SqliteEcashTicketbookManager {
|
||||
FROM ecash_ticketbook
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -144,7 +143,7 @@ impl SqliteEcashTicketbookManager {
|
||||
ticketbook_id,
|
||||
expected_current_total_spent
|
||||
)
|
||||
.execute(&*self.connection_pool)
|
||||
.execute(&self.connection_pool)
|
||||
.await?
|
||||
.rows_affected();
|
||||
Ok(affected > 0)
|
||||
@@ -154,7 +153,7 @@ impl SqliteEcashTicketbookManager {
|
||||
&self,
|
||||
) -> Result<Vec<StoredPendingTicketbook>, sqlx::Error> {
|
||||
sqlx::query_as("SELECT * FROM pending_issuance")
|
||||
.fetch_all(&*self.connection_pool)
|
||||
.fetch_all(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -166,7 +165,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"DELETE FROM pending_issuance WHERE deposit_id = ?",
|
||||
pending_id
|
||||
)
|
||||
.execute(&*self.connection_pool)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -183,7 +182,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"#,
|
||||
epoch_id
|
||||
)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -209,7 +208,7 @@ impl SqliteEcashTicketbookManager {
|
||||
serialisation_revision,
|
||||
epoch_id
|
||||
)
|
||||
.execute(&*self.connection_pool)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -226,7 +225,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"#,
|
||||
epoch_id
|
||||
)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -252,7 +251,7 @@ impl SqliteEcashTicketbookManager {
|
||||
serialisation_revision,
|
||||
epoch_id,
|
||||
)
|
||||
.execute(&*self.connection_pool)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -270,7 +269,7 @@ impl SqliteEcashTicketbookManager {
|
||||
"#,
|
||||
expiration_date
|
||||
)
|
||||
.fetch_optional(&*self.connection_pool)
|
||||
.fetch_optional(&self.connection_pool)
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -299,7 +298,7 @@ impl SqliteEcashTicketbookManager {
|
||||
serialisation_revision,
|
||||
expiration_date
|
||||
)
|
||||
.execute(&*self.connection_pool)
|
||||
.execute(&self.connection_pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -37,7 +37,6 @@ use sqlx::{
|
||||
sqlite::{SqliteAutoVacuum, SqliteSynchronous},
|
||||
ConnectOptions,
|
||||
};
|
||||
use sqlx_pool_guard::SqlitePoolGuard;
|
||||
use std::path::Path;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
@@ -55,15 +54,15 @@ impl PersistentStorage {
|
||||
/// * `database_path`: path to the database.
|
||||
pub async fn init<P: AsRef<Path>>(database_path: P) -> Result<Self, StorageError> {
|
||||
debug!(
|
||||
"Attempting to connect to database {}",
|
||||
database_path.as_ref().display()
|
||||
"Attempting to connect to database {:?}",
|
||||
database_path.as_ref().as_os_str()
|
||||
);
|
||||
|
||||
let opts = sqlx::sqlite::SqliteConnectOptions::new()
|
||||
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
|
||||
.synchronous(SqliteSynchronous::Normal)
|
||||
.auto_vacuum(SqliteAutoVacuum::Incremental)
|
||||
.filename(&database_path)
|
||||
.filename(database_path)
|
||||
.create_if_missing(true)
|
||||
.disable_statement_logging();
|
||||
|
||||
@@ -75,17 +74,13 @@ impl PersistentStorage {
|
||||
}
|
||||
};
|
||||
|
||||
let connection_pool =
|
||||
SqlitePoolGuard::new(database_path.as_ref().to_path_buf(), connection_pool);
|
||||
|
||||
if let Err(err) = sqlx::migrate!("./migrations").run(&*connection_pool).await {
|
||||
if let Err(err) = sqlx::migrate!("./migrations").run(&connection_pool).await {
|
||||
error!("Failed to perform migration on the SQLx database: {err}");
|
||||
connection_pool.close().await;
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
Ok(PersistentStorage {
|
||||
storage_manager: SqliteEcashTicketbookManager::new(connection_pool),
|
||||
storage_manager: SqliteEcashTicketbookManager::new(connection_pool.clone()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,8 +33,8 @@ impl PersistentStatsStorage {
|
||||
/// * `database_path`: path to the database.
|
||||
pub async fn init<P: AsRef<Path> + Send>(database_path: P) -> Result<Self, StatsStorageError> {
|
||||
debug!(
|
||||
"Attempting to connect to database {}",
|
||||
database_path.as_ref().display()
|
||||
"Attempting to connect to database {:?}",
|
||||
database_path.as_ref().as_os_str()
|
||||
);
|
||||
|
||||
// TODO: we can inject here more stuff based on our gateway global config
|
||||
|
||||
@@ -82,8 +82,8 @@ impl GatewayStorage {
|
||||
message_retrieval_limit: i64,
|
||||
) -> Result<Self, GatewayStorageError> {
|
||||
debug!(
|
||||
"Attempting to connect to database {}",
|
||||
database_path.as_ref().display()
|
||||
"Attempting to connect to database {:?}",
|
||||
database_path.as_ref().as_os_str()
|
||||
);
|
||||
|
||||
// TODO: we can inject here more stuff based on our gateway global config
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#[cfg(feature = "network")]
|
||||
use crate::{ApiUrlConst, DenomDetails, ValidatorDetails};
|
||||
use crate::{DenomDetails, ValidatorDetails};
|
||||
|
||||
pub const NETWORK_NAME: &str = "mainnet";
|
||||
|
||||
@@ -29,36 +29,10 @@ pub const COCONUT_DKG_CONTRACT_ADDRESS: &str =
|
||||
pub const REWARDING_VALIDATOR_ADDRESS: &str = "n10yyd98e2tuwu0f7ypz9dy3hhjw7v772q6287gy";
|
||||
|
||||
pub const NYXD_URL: &str = "https://rpc.nymtech.net";
|
||||
pub const NYM_API: &str = "https://validator.nymtech.net/api/";
|
||||
pub const NYXD_WS: &str = "wss://rpc.nymtech.net/websocket";
|
||||
pub const EXPLORER_API: &str = "https://explorer.nymtech.net/api/";
|
||||
pub const NYM_API: &str = "https://validator.nymtech.net/api/";
|
||||
#[cfg(feature = "network")]
|
||||
pub const NYM_APIS: &[ApiUrlConst] = &[
|
||||
ApiUrlConst {
|
||||
url: NYM_API,
|
||||
front_hosts: None,
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nym-fronntdoor.vercel.app/api/",
|
||||
front_hosts: Some(&["vercel.app", "vercel.com"]),
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nym-frontdoor.global.ssl.fastly.net/api/",
|
||||
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
|
||||
},
|
||||
];
|
||||
pub const NYM_VPN_API: &str = "https://nymvpn.com/api/";
|
||||
#[cfg(feature = "network")]
|
||||
pub const NYM_VPN_APIS: &[ApiUrlConst] = &[
|
||||
ApiUrlConst {
|
||||
url: NYM_VPN_API,
|
||||
front_hosts: Some(&["vercel.app", "vercel.com"]),
|
||||
},
|
||||
ApiUrlConst {
|
||||
url: "https://nymvpn-frontdoor.global.ssl.fastly.net/api/",
|
||||
front_hosts: Some(&["yelp.global.ssl.fastly.net"]),
|
||||
},
|
||||
];
|
||||
|
||||
// I'm making clippy mad on purpose, because that url HAS TO be updated and deployed before merging
|
||||
pub const EXIT_POLICY_URL: &str =
|
||||
|
||||
@@ -37,37 +37,6 @@ pub struct NymNetworkDetails {
|
||||
pub contracts: NymContracts,
|
||||
pub explorer_api: Option<String>,
|
||||
pub nym_vpn_api_url: Option<String>,
|
||||
pub nym_api_urls: Option<Vec<ApiUrl>>,
|
||||
pub nym_vpn_api_urls: Option<Vec<ApiUrl>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, Hash, PartialEq, Serialize, JsonSchema)]
|
||||
#[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))]
|
||||
pub struct ApiUrl {
|
||||
/// Expects a string formatted Url
|
||||
///
|
||||
/// see https://docs.rs/url/latest/url/struct.Url.html
|
||||
pub url: String,
|
||||
/// Optional alternative equivalent hostnames. Each entry must parse as valid Host
|
||||
///
|
||||
/// see https://docs.rs/url/latest/url/enum.Host.html
|
||||
pub front_hosts: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
pub struct ApiUrlConst<'a> {
|
||||
pub url: &'a str,
|
||||
pub front_hosts: Option<&'a [&'a str]>,
|
||||
}
|
||||
|
||||
impl From<ApiUrlConst<'_>> for ApiUrl {
|
||||
fn from(value: ApiUrlConst) -> Self {
|
||||
ApiUrl {
|
||||
url: value.url.to_string(),
|
||||
front_hosts: value
|
||||
.front_hosts
|
||||
.map(|slice| slice.iter().map(|s| s.to_string()).collect()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// by default we assume the same defaults as mainnet, i.e. same prefixes and denoms
|
||||
@@ -98,8 +67,6 @@ impl NymNetworkDetails {
|
||||
contracts: Default::default(),
|
||||
explorer_api: Default::default(),
|
||||
nym_vpn_api_url: Default::default(),
|
||||
nym_api_urls: Default::default(),
|
||||
nym_vpn_api_urls: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,8 +154,6 @@ impl NymNetworkDetails {
|
||||
},
|
||||
explorer_api: parse_optional_str(mainnet::EXPLORER_API),
|
||||
nym_vpn_api_url: parse_optional_str(mainnet::NYM_VPN_API),
|
||||
nym_api_urls: None,
|
||||
nym_vpn_api_urls: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
[package]
|
||||
name = "nym-kkt"
|
||||
version = "0.1.0"
|
||||
authors = ["Georgio Nicolas <georgio@nymtech.net>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
arc-swap = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
futures = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
pin-project = { workspace = true }
|
||||
blake3 = { workspace = true }
|
||||
aead = { workspace = true }
|
||||
strum = { workspace = true, features = ["derive"] }
|
||||
thiserror = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
tokio-util = { workspace = true, features = ["codec"] }
|
||||
|
||||
|
||||
|
||||
# internal
|
||||
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde"]}
|
||||
|
||||
libcrux-traits = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = ["test-utils"] }
|
||||
libcrux-sha3 = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-ml-kem = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-ecdh = { git = "https://github.com/cryspen/libcrux", features = ["codec"]}
|
||||
|
||||
rand = "0.9.2"
|
||||
curve25519-dalek = {version = "4.1.3", features = ["rand_core", "serde"] }
|
||||
zeroize = { workspace = true, features = ["zeroize_derive"] }
|
||||
classic-mceliece-rust = { git = "https://github.com/georgio/classic-mceliece-rust", features = ["mceliece460896f","zeroize"]}
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = {workspace = true}
|
||||
|
||||
[[bench]]
|
||||
name = "benches"
|
||||
harness = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,518 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_kkt::{
|
||||
ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, SignatureScheme, KEM},
|
||||
context::KKTMode,
|
||||
frame::KKTFrame,
|
||||
key_utils::{generate_keypair_libcrux, generate_keypair_mceliece, hash_encapsulation_key},
|
||||
session::{
|
||||
anonymous_initiator_process, initiator_ingest_response, initiator_process,
|
||||
responder_ingest_message, responder_process,
|
||||
},
|
||||
};
|
||||
use rand::prelude::*;
|
||||
|
||||
pub fn gen_ed25519_keypair(c: &mut Criterion) {
|
||||
c.bench_function("Generate Ed25519 Keypair", |b| {
|
||||
b.iter(|| {
|
||||
let mut s: [u8; 32] = [0u8; 32];
|
||||
rand::rng().fill_bytes(&mut s);
|
||||
ed25519::KeyPair::from_secret(s, 0)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn gen_mlkem768_keypair(c: &mut Criterion) {
|
||||
c.bench_function("Generate MlKem768 Keypair", |b| {
|
||||
b.iter(|| {
|
||||
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand::rng()).unwrap()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn kkt_benchmark(c: &mut Criterion) {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// generate ed25519 keys
|
||||
let mut secret_initiator: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut secret_initiator);
|
||||
let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0);
|
||||
|
||||
let mut secret_responder: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut secret_responder);
|
||||
let responder_ed25519_keypair = ed25519::KeyPair::from_secret(secret_responder, 1);
|
||||
for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] {
|
||||
for hash_function in [
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::SHAKE128,
|
||||
HashFunction::SHAKE256,
|
||||
] {
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
kem,
|
||||
hash_function,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// generate kem public keys
|
||||
|
||||
let (responder_kem_public_key, initiator_kem_public_key) = match kem {
|
||||
KEM::MlKem768 => (
|
||||
EncapsulationKey::MlKem768(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
EncapsulationKey::MlKem768(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
),
|
||||
KEM::XWing => (
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
),
|
||||
KEM::X25519 => (
|
||||
EncapsulationKey::X25519(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
EncapsulationKey::X25519(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
),
|
||||
KEM::McEliece => (
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
),
|
||||
};
|
||||
|
||||
let i_kem_key_bytes = initiator_kem_public_key.encode();
|
||||
|
||||
let r_kem_key_bytes = responder_kem_public_key.encode();
|
||||
|
||||
let i_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&i_kem_key_bytes,
|
||||
);
|
||||
|
||||
let r_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&r_kem_key_bytes,
|
||||
);
|
||||
|
||||
// Anonymous Initiator, OneWay
|
||||
{
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{}, {} | Anonymous Initiator: Generate Request",
|
||||
kem, hash_function
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| anonymous_initiator_process(&mut rng, ciphersuite).unwrap());
|
||||
},
|
||||
);
|
||||
|
||||
let (mut i_context, i_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{}, {} | Anonymous Initiator: Encode Frame - Request",
|
||||
kem, hash_function
|
||||
),
|
||||
|b| b.iter(|| i_frame.to_bytes()),
|
||||
);
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{}, {} | Anonymous Initiator: Decode Frame - Request",
|
||||
kem, hash_function
|
||||
),
|
||||
|b| b.iter(|| KKTFrame::from_bytes(&i_frame_bytes).unwrap()),
|
||||
);
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{}, {} | Anonymous Initiator: Responder Ingest Frame",
|
||||
kem, hash_function
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let (mut r_context, _) =
|
||||
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{}, {} | Anonymous Initiator: Responder Generate Response",
|
||||
kem, hash_function
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id_ref(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id_ref(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{}, {} | Anonymous Initiator: Responder Encode Frame",
|
||||
kem, hash_function
|
||||
),
|
||||
|b| b.iter(|| r_frame.to_bytes()),
|
||||
);
|
||||
|
||||
let r_bytes = r_frame.to_bytes();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{}, {} | Anonymous Initiator: Initiator Ingest Response",
|
||||
kem, hash_function
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_ingest_response(
|
||||
&mut i_context,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
&r_bytes,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
&r_bytes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{}, {} | Initiator OneWay: Generate Request",
|
||||
kem, hash_function
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_process(
|
||||
&mut rng,
|
||||
KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{}, {} | Initiator OneWay: Encode Frame - Request",
|
||||
kem, hash_function
|
||||
),
|
||||
|b| b.iter(|| i_frame.to_bytes()),
|
||||
);
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{}, {} | Initiator OneWay: Decode Frame - Request",
|
||||
kem, hash_function
|
||||
),
|
||||
|b| b.iter(|| KKTFrame::from_bytes(&i_frame_bytes).unwrap()),
|
||||
);
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{}, {} | Initiator OneWay: Responder Ingest Frame",
|
||||
kem, hash_function
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(r_obtained_key.is_none());
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{}, {} | Initiator OneWay: Responder Generate Response",
|
||||
kem, hash_function
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id_ref(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id_ref(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{}, {} | Initiator OneWay: Responder Encode Frame",
|
||||
kem, hash_function
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| r_frame.to_bytes());
|
||||
},
|
||||
);
|
||||
|
||||
let r_bytes = r_frame.to_bytes();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{}, {} | Initiator OneWay: Initiator Ingest Response",
|
||||
kem, hash_function
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_ingest_response(
|
||||
&mut i_context,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
&r_bytes,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
&r_bytes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
|
||||
// Initiator, Mutual
|
||||
{
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{}, {} | Initiator Mutual: Generate Request",
|
||||
kem, hash_function
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_process(
|
||||
&mut rng,
|
||||
KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
Some(&initiator_kem_public_key),
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
Some(&initiator_kem_public_key),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{}, {} | Initiator Mutual: Encode Frame - Request",
|
||||
kem, hash_function
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| i_frame.to_bytes());
|
||||
},
|
||||
);
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{}, {} | Initiator Mutual: Decode Frame - Request",
|
||||
kem, hash_function
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| KKTFrame::from_bytes(&i_frame_bytes).unwrap());
|
||||
},
|
||||
);
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{}, {} | Initiator Mutual: Responder Ingest Frame",
|
||||
kem, hash_function
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{}, {} | Initiator Mutual: Responder Generate Response",
|
||||
kem, hash_function
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id_ref(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id_ref(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{}, {} | Initiator Mutual: Responder Encode Frame",
|
||||
kem, hash_function
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
r_frame.to_bytes();
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let r_bytes = r_frame.to_bytes();
|
||||
|
||||
c.bench_function(
|
||||
&format!(
|
||||
"{}, {} | Initiator Mutual: Initiator Ingest Response",
|
||||
kem, hash_function
|
||||
),
|
||||
|b| {
|
||||
b.iter(|| {
|
||||
initiator_ingest_response(
|
||||
&mut i_context,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
&r_bytes,
|
||||
)
|
||||
.unwrap()
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
let obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
&r_bytes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
gen_ed25519_keypair,
|
||||
gen_mlkem768_keypair,
|
||||
kkt_benchmark
|
||||
);
|
||||
criterion_main!(benches);
|
||||
@@ -1,311 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use libcrux_kem::{Algorithm, MlKem768PublicKey};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
|
||||
use crate::error::KKTError;
|
||||
|
||||
pub const HASH_LEN_256: u8 = 32;
|
||||
pub const CIPHERSUITE_ENCODING_LEN: usize = 4;
|
||||
|
||||
pub const CURVE25519_KEY_LEN: usize = 32;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum HashFunction {
|
||||
Blake3,
|
||||
SHAKE128,
|
||||
SHAKE256,
|
||||
SHA256,
|
||||
}
|
||||
impl Display for HashFunction {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
HashFunction::Blake3 => "Blake3",
|
||||
HashFunction::SHAKE128 => "SHAKE128",
|
||||
HashFunction::SHAKE256 => "SHAKE256",
|
||||
HashFunction::SHA256 => "SHA256",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub enum EncapsulationKey<'a> {
|
||||
MlKem768(libcrux_kem::PublicKey),
|
||||
XWing(libcrux_kem::PublicKey),
|
||||
X25519(libcrux_kem::PublicKey),
|
||||
McEliece(classic_mceliece_rust::PublicKey<'a>),
|
||||
}
|
||||
|
||||
pub enum DecapsulationKey<'a> {
|
||||
MlKem768(libcrux_kem::PrivateKey),
|
||||
XWing(libcrux_kem::PrivateKey),
|
||||
X25519(libcrux_kem::PrivateKey),
|
||||
McEliece(classic_mceliece_rust::SecretKey<'a>),
|
||||
}
|
||||
impl<'a> EncapsulationKey<'a> {
|
||||
pub(crate) fn decode(kem: KEM, bytes: &[u8]) -> Result<Self, KKTError> {
|
||||
match kem {
|
||||
KEM::McEliece => {
|
||||
if bytes.len() != classic_mceliece_rust::CRYPTO_PUBLICKEYBYTES {
|
||||
Err(KKTError::KEMError {
|
||||
info: "Received McEliece Encapsulation Key with Invalid Length",
|
||||
})
|
||||
} else {
|
||||
let mut public_key_bytes =
|
||||
Box::new([0u8; classic_mceliece_rust::CRYPTO_PUBLICKEYBYTES]);
|
||||
// Size must be correct due to KKTFrame::from_bytes(message_bytes)?
|
||||
public_key_bytes.clone_from_slice(bytes);
|
||||
Ok(EncapsulationKey::McEliece(
|
||||
classic_mceliece_rust::PublicKey::from(public_key_bytes),
|
||||
))
|
||||
}
|
||||
}
|
||||
KEM::X25519 => Ok(EncapsulationKey::X25519(libcrux_kem::PublicKey::decode(
|
||||
map_kem_to_libcrux_kem(kem),
|
||||
bytes,
|
||||
)?)),
|
||||
KEM::MlKem768 => Ok(EncapsulationKey::MlKem768(libcrux_kem::PublicKey::decode(
|
||||
map_kem_to_libcrux_kem(kem),
|
||||
bytes,
|
||||
)?)),
|
||||
KEM::XWing => Ok(EncapsulationKey::XWing(libcrux_kem::PublicKey::decode(
|
||||
map_kem_to_libcrux_kem(kem),
|
||||
bytes,
|
||||
)?)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn encode(&self) -> Vec<u8> {
|
||||
match self {
|
||||
EncapsulationKey::XWing(public_key)
|
||||
| EncapsulationKey::MlKem768(public_key)
|
||||
| EncapsulationKey::X25519(public_key) => public_key.encode(),
|
||||
EncapsulationKey::McEliece(public_key) => Vec::from(public_key.as_array()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum SignatureScheme {
|
||||
Ed25519,
|
||||
}
|
||||
impl Display for SignatureScheme {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
SignatureScheme::Ed25519 => "Ed25519",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum KEM {
|
||||
MlKem768,
|
||||
XWing,
|
||||
X25519,
|
||||
McEliece,
|
||||
}
|
||||
|
||||
impl Display for KEM {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
KEM::MlKem768 => "MlKem768",
|
||||
KEM::XWing => "XWing",
|
||||
KEM::X25519 => "x25519",
|
||||
KEM::McEliece => "McEliece",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Ciphersuite {
|
||||
hash_function: HashFunction,
|
||||
signature_scheme: SignatureScheme,
|
||||
kem: KEM,
|
||||
hash_length: u8,
|
||||
encapsulation_key_length: usize,
|
||||
signing_key_length: usize,
|
||||
verification_key_length: usize,
|
||||
signature_length: usize,
|
||||
}
|
||||
|
||||
impl Ciphersuite {
|
||||
pub fn kem_key_len(&self) -> usize {
|
||||
self.encapsulation_key_length
|
||||
}
|
||||
|
||||
pub fn signature_len(&self) -> usize {
|
||||
self.signature_length
|
||||
}
|
||||
pub fn signing_key_len(&self) -> usize {
|
||||
self.signing_key_length
|
||||
}
|
||||
pub fn verification_key_len(&self) -> usize {
|
||||
self.verification_key_length
|
||||
}
|
||||
pub fn hash_function(&self) -> HashFunction {
|
||||
self.hash_function
|
||||
}
|
||||
pub fn kem(&self) -> KEM {
|
||||
self.kem
|
||||
}
|
||||
pub fn signature_scheme(&self) -> SignatureScheme {
|
||||
self.signature_scheme
|
||||
}
|
||||
pub fn hash_len(&self) -> usize {
|
||||
self.hash_length as usize
|
||||
}
|
||||
|
||||
pub fn default() -> Self {
|
||||
Self::resolve_ciphersuite(
|
||||
KEM::XWing,
|
||||
HashFunction::Blake3,
|
||||
SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn resolve_ciphersuite(
|
||||
kem: KEM,
|
||||
hash_function: HashFunction,
|
||||
signature_scheme: SignatureScheme,
|
||||
// This should be None 99.9999% of the time
|
||||
custom_hash_length: Option<u8>,
|
||||
) -> Result<Self, KKTError> {
|
||||
let hash_len = match custom_hash_length {
|
||||
Some(l) => {
|
||||
if l < 16 {
|
||||
return Err(KKTError::InsecureHashLen);
|
||||
} else {
|
||||
l
|
||||
}
|
||||
}
|
||||
None => HASH_LEN_256,
|
||||
};
|
||||
Ok(Self {
|
||||
hash_function: hash_function,
|
||||
signature_scheme: signature_scheme,
|
||||
kem: kem,
|
||||
hash_length: hash_len,
|
||||
encapsulation_key_length: match kem {
|
||||
// 1184 bytes
|
||||
KEM::MlKem768 => MlKem768PublicKey::len(),
|
||||
// 1216 bytes = 1184 + 32
|
||||
KEM::XWing => MlKem768PublicKey::len() + CURVE25519_KEY_LEN,
|
||||
// 32 bytes
|
||||
KEM::X25519 => CURVE25519_KEY_LEN,
|
||||
// 524160 bytes
|
||||
KEM::McEliece => classic_mceliece_rust::CRYPTO_PUBLICKEYBYTES,
|
||||
},
|
||||
signing_key_length: match signature_scheme {
|
||||
// 32 bytes
|
||||
SignatureScheme::Ed25519 => ed25519::SECRET_KEY_LENGTH,
|
||||
},
|
||||
verification_key_length: match signature_scheme {
|
||||
// 32 bytes
|
||||
SignatureScheme::Ed25519 => ed25519::PUBLIC_KEY_LENGTH,
|
||||
},
|
||||
signature_length: match signature_scheme {
|
||||
// 64 bytes
|
||||
SignatureScheme::Ed25519 => ed25519::SIGNATURE_LENGTH,
|
||||
},
|
||||
})
|
||||
}
|
||||
pub fn encode(&self) -> [u8; 4] {
|
||||
// [kem, hash, hashlen, sig]
|
||||
[
|
||||
match self.kem {
|
||||
KEM::XWing => 0,
|
||||
KEM::MlKem768 => 1,
|
||||
KEM::McEliece => 2,
|
||||
KEM::X25519 => 255,
|
||||
},
|
||||
match self.hash_function {
|
||||
HashFunction::Blake3 => 0,
|
||||
HashFunction::SHAKE256 => 1,
|
||||
HashFunction::SHAKE128 => 2,
|
||||
HashFunction::SHA256 => 3,
|
||||
},
|
||||
match self.hash_length {
|
||||
HASH_LEN_256 => 0,
|
||||
_ => self.hash_length as u8,
|
||||
},
|
||||
match self.signature_scheme {
|
||||
SignatureScheme::Ed25519 => 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
pub fn decode(encoding: &[u8]) -> Result<Self, KKTError> {
|
||||
if encoding.len() == 4 {
|
||||
let kem = match encoding[0] {
|
||||
0 => KEM::XWing,
|
||||
1 => KEM::MlKem768,
|
||||
2 => KEM::McEliece,
|
||||
255 => KEM::X25519,
|
||||
_ => {
|
||||
return Err(KKTError::CiphersuiteDecodingError {
|
||||
info: format!("Undefined KEM: {}", encoding[0]),
|
||||
})
|
||||
}
|
||||
};
|
||||
let hash_function = match encoding[1] {
|
||||
0 => HashFunction::Blake3,
|
||||
1 => HashFunction::SHAKE256,
|
||||
2 => HashFunction::SHAKE128,
|
||||
3 => HashFunction::SHA256,
|
||||
_ => {
|
||||
return Err(KKTError::CiphersuiteDecodingError {
|
||||
info: format!("Undefined Hash Function: {}", encoding[1]),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let custom_hash_length = match encoding[2] {
|
||||
0 => None,
|
||||
_ => Some(encoding[2]),
|
||||
};
|
||||
|
||||
let signature_scheme = match encoding[3] {
|
||||
0 => SignatureScheme::Ed25519,
|
||||
_ => {
|
||||
return Err(KKTError::CiphersuiteDecodingError {
|
||||
info: format!("Undefined Signature Scheme: {}", encoding[3]),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
Self::resolve_ciphersuite(kem, hash_function, signature_scheme, custom_hash_length)
|
||||
} else {
|
||||
return Err(KKTError::CiphersuiteDecodingError {
|
||||
info: format!(
|
||||
"Incorrect Encoding Length: actual: {} != expected: {}",
|
||||
encoding.len(),
|
||||
CIPHERSUITE_ENCODING_LEN
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Ciphersuite {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(
|
||||
&format!(
|
||||
"{}_{}({})_{}",
|
||||
self.kem, self.hash_function, self.hash_length, self.signature_scheme
|
||||
)
|
||||
.to_ascii_lowercase(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn map_kem_to_libcrux_kem(kem: KEM) -> Algorithm {
|
||||
match kem {
|
||||
KEM::MlKem768 => Algorithm::MlKem768,
|
||||
KEM::XWing => Algorithm::XWingKemDraft06,
|
||||
KEM::X25519 => Algorithm::X25519,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
@@ -1,258 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::fmt::Display;
|
||||
|
||||
use crate::{ciphersuite::Ciphersuite, error::KKTError, frame::KKT_SESSION_ID_LEN, KKT_VERSION};
|
||||
|
||||
pub const KKT_CONTEXT_LEN: usize = 7;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub enum KKTStatus {
|
||||
Ok,
|
||||
InvalidRequestFormat,
|
||||
InvalidResponseFormat,
|
||||
InvalidSignature,
|
||||
UnsupportedCiphersuite,
|
||||
UnsupportedKKTVersion,
|
||||
InvalidKey,
|
||||
Timeout,
|
||||
}
|
||||
|
||||
impl Display for KKTStatus {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
KKTStatus::Ok => "Ok",
|
||||
KKTStatus::InvalidRequestFormat => "Invalid Request Format",
|
||||
KKTStatus::InvalidResponseFormat => "Invalid Response Format",
|
||||
KKTStatus::InvalidSignature => "Invalid Signature",
|
||||
KKTStatus::UnsupportedCiphersuite => "Unsupported Ciphersuite",
|
||||
KKTStatus::UnsupportedKKTVersion => "Unsupported KKT Version",
|
||||
KKTStatus::InvalidKey => "Invalid Key",
|
||||
KKTStatus::Timeout => "Timeout",
|
||||
})
|
||||
}
|
||||
}
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub enum KKTRole {
|
||||
Initiator,
|
||||
AnonymousInitiator,
|
||||
Responder,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub enum KKTMode {
|
||||
OneWay,
|
||||
Mutual,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct KKTContext {
|
||||
version: u8,
|
||||
message_sequence: u8,
|
||||
status: KKTStatus,
|
||||
mode: KKTMode,
|
||||
role: KKTRole,
|
||||
ciphersuite: Ciphersuite,
|
||||
}
|
||||
impl KKTContext {
|
||||
pub fn new(role: KKTRole, mode: KKTMode, ciphersuite: Ciphersuite) -> Result<Self, KKTError> {
|
||||
if role == KKTRole::AnonymousInitiator && mode != KKTMode::OneWay {
|
||||
return Err(KKTError::IncompatibilityError {
|
||||
info: "Anonymous Initiator can only use OneWay mode",
|
||||
});
|
||||
}
|
||||
Ok(Self {
|
||||
version: KKT_VERSION,
|
||||
message_sequence: 0,
|
||||
status: KKTStatus::Ok,
|
||||
mode: mode,
|
||||
role: role,
|
||||
ciphersuite: ciphersuite,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn derive_responder_header(&self) -> Result<Self, KKTError> {
|
||||
let mut responder_header = self.clone();
|
||||
|
||||
responder_header.increment_message_sequence_count()?;
|
||||
responder_header.role = KKTRole::Responder;
|
||||
|
||||
Ok(responder_header)
|
||||
}
|
||||
|
||||
pub fn increment_message_sequence_count(&mut self) -> Result<(), KKTError> {
|
||||
if self.message_sequence + 1 < (1 << 4) {
|
||||
self.message_sequence += 1;
|
||||
Ok(())
|
||||
} else {
|
||||
Err(KKTError::MessageCountLimitReached)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_status(&mut self, status: KKTStatus) {
|
||||
self.status = status;
|
||||
}
|
||||
pub fn version(&self) -> u8 {
|
||||
self.version
|
||||
}
|
||||
pub fn status(&self) -> KKTStatus {
|
||||
self.status
|
||||
}
|
||||
pub fn ciphersuite(&self) -> Ciphersuite {
|
||||
self.ciphersuite
|
||||
}
|
||||
pub fn role(&self) -> KKTRole {
|
||||
self.role
|
||||
}
|
||||
pub fn mode(&self) -> KKTMode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
pub fn body_len(&self) -> usize {
|
||||
if self.status != KKTStatus::Ok
|
||||
|| (self.mode == KKTMode::OneWay
|
||||
&& (self.role == KKTRole::Initiator || self.role == KKTRole::AnonymousInitiator))
|
||||
{
|
||||
0
|
||||
} else {
|
||||
self.ciphersuite.kem_key_len()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn signature_len(&self) -> usize {
|
||||
match self.role {
|
||||
KKTRole::Initiator | KKTRole::Responder => self.ciphersuite.signature_len(),
|
||||
KKTRole::AnonymousInitiator => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn header_len(&self) -> usize {
|
||||
KKT_CONTEXT_LEN
|
||||
}
|
||||
|
||||
pub fn session_id_len(&self) -> usize {
|
||||
// match self.role {
|
||||
// KKTRole::Initiator | KKTRole::Responder => SESSION_ID_LENGTH,
|
||||
// It doesn't make sense to send a session_id if we send messages in the clear
|
||||
// KKTRole::AnonymousInitiator => 0,
|
||||
// }
|
||||
KKT_SESSION_ID_LEN
|
||||
}
|
||||
|
||||
pub fn full_message_len(&self) -> usize {
|
||||
self.body_len() + self.signature_len() + self.header_len() + self.session_id_len()
|
||||
}
|
||||
|
||||
pub fn encode(&self) -> Result<Vec<u8>, KKTError> {
|
||||
let mut header_bytes: Vec<u8> = Vec::with_capacity(KKT_CONTEXT_LEN);
|
||||
if self.message_sequence >= 1 << 4 {
|
||||
return Err(KKTError::MessageCountLimitReached);
|
||||
}
|
||||
|
||||
header_bytes.push((KKT_VERSION << 4) + self.message_sequence);
|
||||
|
||||
header_bytes.push(
|
||||
match self.status {
|
||||
KKTStatus::Ok => 0,
|
||||
KKTStatus::InvalidRequestFormat => 0b001_000_00,
|
||||
KKTStatus::InvalidResponseFormat => 0b010_000_00,
|
||||
KKTStatus::InvalidSignature => 0b011_000_00,
|
||||
KKTStatus::UnsupportedCiphersuite => 0b100_000_00,
|
||||
KKTStatus::UnsupportedKKTVersion => 0b101_000_00,
|
||||
KKTStatus::InvalidKey => 0b110_000_00,
|
||||
KKTStatus::Timeout => 0b111_000_00,
|
||||
} + match self.mode {
|
||||
KKTMode::OneWay => 0,
|
||||
KKTMode::Mutual => 0b000_001_00,
|
||||
} + match self.role {
|
||||
KKTRole::Initiator => 0,
|
||||
KKTRole::Responder => 1,
|
||||
KKTRole::AnonymousInitiator => 2,
|
||||
},
|
||||
);
|
||||
|
||||
header_bytes.extend_from_slice(&self.ciphersuite.encode());
|
||||
header_bytes.push(0);
|
||||
Ok(header_bytes)
|
||||
}
|
||||
|
||||
pub fn try_decode(header_bytes: &[u8]) -> Result<Self, KKTError> {
|
||||
if header_bytes.len() == KKT_CONTEXT_LEN {
|
||||
let kkt_version = header_bytes[0] & 0b1111_0000;
|
||||
|
||||
let message_sequence_counter = header_bytes[0] & 0b0000_1111;
|
||||
|
||||
// We only check if stuff is valid here, not necessarily if it's compatible
|
||||
|
||||
if (kkt_version >> 4) > KKT_VERSION {
|
||||
return Err(KKTError::FrameDecodingError {
|
||||
info: format!("Header - Invalid KKT Version: {}", kkt_version >> 4),
|
||||
});
|
||||
}
|
||||
|
||||
let status = match header_bytes[1] & 0b111_000_00 {
|
||||
0 => KKTStatus::Ok,
|
||||
0b001_000_00 => KKTStatus::InvalidRequestFormat,
|
||||
0b010_000_00 => KKTStatus::InvalidResponseFormat,
|
||||
0b011_000_00 => KKTStatus::InvalidSignature,
|
||||
0b100_000_00 => KKTStatus::UnsupportedCiphersuite,
|
||||
0b101_000_00 => KKTStatus::UnsupportedKKTVersion,
|
||||
0b110_000_00 => KKTStatus::InvalidKey,
|
||||
0b111_000_00 => KKTStatus::Timeout,
|
||||
_ => {
|
||||
return Err(KKTError::FrameDecodingError {
|
||||
info: format!(
|
||||
"Header - Invalid KKT Status: {}",
|
||||
header_bytes[1] & 0b111_000_00
|
||||
),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let role = match header_bytes[1] & 0b000_000_11 {
|
||||
0 => KKTRole::Initiator,
|
||||
1 => KKTRole::Responder,
|
||||
2 => KKTRole::AnonymousInitiator,
|
||||
_ => {
|
||||
return Err(KKTError::FrameDecodingError {
|
||||
info: format!(
|
||||
"Header - Invalid KKT Role: {}",
|
||||
header_bytes[1] & 0b000_000_11
|
||||
),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
let mode = match (header_bytes[1] & 0b000_111_00) >> 2 {
|
||||
0 => KKTMode::OneWay,
|
||||
1 => KKTMode::Mutual,
|
||||
_ => {
|
||||
return Err(KKTError::FrameDecodingError {
|
||||
info: format!(
|
||||
"Header - Invalid KKT Mode: {}",
|
||||
(header_bytes[1] & 0b000_111_00) >> 2
|
||||
),
|
||||
})
|
||||
}
|
||||
};
|
||||
|
||||
Ok(KKTContext {
|
||||
version: kkt_version,
|
||||
status: status,
|
||||
mode: mode,
|
||||
role: role,
|
||||
ciphersuite: Ciphersuite::decode(&header_bytes[2..6])?,
|
||||
message_sequence: message_sequence_counter,
|
||||
})
|
||||
} else {
|
||||
Err(KKTError::FrameDecodingError {
|
||||
info: format!(
|
||||
"Header - Invalid Header Length: actual: {} != expected: {}",
|
||||
header_bytes.len(),
|
||||
KKT_CONTEXT_LEN
|
||||
),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
use core::hash;
|
||||
|
||||
use blake3::{Hash, Hasher};
|
||||
use curve25519_dalek::digest::DynDigest;
|
||||
use libcrux_psq::traits::Ciphertext;
|
||||
use nym_crypto::symmetric::aead::{AeadKey, Nonce};
|
||||
use nym_crypto::{
|
||||
aes::Aes256,
|
||||
asymmetric::x25519::{self, PrivateKey, PublicKey},
|
||||
generic_array::GenericArray,
|
||||
Aes256GcmSiv,
|
||||
};
|
||||
// use rand::{CryptoRng, RngCore};
|
||||
use zeroize::Zeroize;
|
||||
|
||||
use nym_crypto::aes::cipher::crypto_common::rand_core::{CryptoRng, RngCore};
|
||||
|
||||
use crate::error::KKTError;
|
||||
|
||||
fn generate_round_trip_symmetric_key<R>(
|
||||
rng: &mut R,
|
||||
remote_public_key: &PublicKey,
|
||||
) -> ([u8; 64], [u8; 32])
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
let mut s = x25519::PrivateKey::new(rng);
|
||||
let gs = s.public_key();
|
||||
|
||||
let mut gbs = s.diffie_hellman(remote_public_key);
|
||||
s.zeroize();
|
||||
|
||||
let mut message: [u8; 64] = [0u8; 64];
|
||||
message[0..32].clone_from_slice(gs.as_bytes());
|
||||
|
||||
let mut hasher = Hasher::new();
|
||||
|
||||
hasher.update(&gbs);
|
||||
gbs.zeroize();
|
||||
let key: [u8; 32] = hasher.finalize().as_bytes().to_owned();
|
||||
|
||||
hasher.update(remote_public_key.as_bytes());
|
||||
hasher.update(gs.as_bytes());
|
||||
|
||||
hasher.finalize_into_reset(&mut message[32..64]);
|
||||
|
||||
(message, key)
|
||||
}
|
||||
|
||||
fn extract_shared_secret(b: &PrivateKey, message: &[u8; 64]) -> Result<[u8; 32], KKTError> {
|
||||
let gs = PublicKey::from_bytes(&message[0..32])?;
|
||||
|
||||
let mut gsb = b.diffie_hellman(&gs);
|
||||
|
||||
let mut hasher = Hasher::new();
|
||||
hasher.update(&gsb);
|
||||
gsb.zeroize();
|
||||
let key: [u8; 32] = hasher.finalize().as_bytes().to_owned();
|
||||
|
||||
hasher.update(b.public_key().as_bytes());
|
||||
hasher.update(gs.as_bytes());
|
||||
|
||||
// This runs in constant time
|
||||
if hasher.finalize() == message[32..64] {
|
||||
Ok(key)
|
||||
} else {
|
||||
Err(KKTError::X25519Error {
|
||||
info: format!("Symmetric Key Hash Validation Error"),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn encrypt(mut key: [u8; 32], message: &[u8]) -> Result<Vec<u8>, KKTError> {
|
||||
// The empty nonce is fine since we use the key once.
|
||||
let nonce = Nonce::<Aes256GcmSiv>::from_slice(&[]);
|
||||
|
||||
let ciphertext =
|
||||
nym_crypto::symmetric::aead::encrypt::<Aes256GcmSiv>(&key.into(), nonce, message)?;
|
||||
|
||||
key.zeroize();
|
||||
|
||||
Ok(ciphertext)
|
||||
}
|
||||
|
||||
fn decrypt(key: [u8; 32], ciphertext: Vec<u8>) -> Vec<u8> {
|
||||
// The empty nonce is fine since we use the key once.
|
||||
let nonce = Nonce::<Aes256>::from_slice(&[]);
|
||||
|
||||
let ciphertext =
|
||||
nym_crypto::symmetric::aead::encrypt::<Aes256GcmSiv>(&key.into(), nonce, message)?;
|
||||
|
||||
key.zeroize();
|
||||
|
||||
Ok(ciphertext)
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::context::KKTStatus;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum KKTError {
|
||||
#[error("Signature constructor error")]
|
||||
SigConstructorError,
|
||||
#[error("Signature verification error")]
|
||||
SigVerifError,
|
||||
#[error("Ciphersuite Decoding Error: {}", info)]
|
||||
CiphersuiteDecodingError { info: String },
|
||||
#[error("Insecure Encapsulation Key Hash Length")]
|
||||
InsecureHashLen,
|
||||
|
||||
#[error("KKT Frame Decoding Error: {}", info)]
|
||||
FrameDecodingError { info: String },
|
||||
|
||||
#[error("KKT Frame Encoding Error: {}", info)]
|
||||
FrameEncodingError { info: String },
|
||||
|
||||
#[error("KKT Incompatibility Error: {}", info)]
|
||||
IncompatibilityError { info: &'static str },
|
||||
|
||||
#[error("KKT Responder Flagged Error: {}", status)]
|
||||
ResponderFlaggedError { status: KKTStatus },
|
||||
|
||||
#[error("KKT Message Count Limit Reached")]
|
||||
MessageCountLimitReached,
|
||||
|
||||
#[error("PSQ KEM Error: {}", info)]
|
||||
KEMError { info: &'static str },
|
||||
|
||||
#[error("Local Function Input Error: {}", info)]
|
||||
FunctionInputError { info: &'static str },
|
||||
|
||||
#[error("{}", info)]
|
||||
X25519Error { info: &'static str },
|
||||
// #[error("Protocol did not complete")]
|
||||
// ProtocolError,
|
||||
|
||||
// #[error("encountered an IO error: {0}")]
|
||||
// IoError(#[from] io::Error),s
|
||||
|
||||
// #[error("Handshake timeout")]
|
||||
// HandshakeTimeout(#[from] tokio::time::error::Elapsed),
|
||||
}
|
||||
|
||||
impl From<libcrux_kem::Error> for KKTError {
|
||||
fn from(err: libcrux_kem::Error) -> Self {
|
||||
match err {
|
||||
libcrux_kem::Error::EcDhError(_) => KKTError::KEMError { info: "ECDH Error" },
|
||||
libcrux_kem::Error::KeyGen => KKTError::KEMError {
|
||||
info: "Key Generation Error",
|
||||
},
|
||||
libcrux_kem::Error::Encapsulate => KKTError::KEMError {
|
||||
info: "Encapsulation Error",
|
||||
},
|
||||
libcrux_kem::Error::Decapsulate => KKTError::KEMError {
|
||||
info: "Decapsulation Error",
|
||||
},
|
||||
libcrux_kem::Error::UnsupportedAlgorithm => KKTError::KEMError {
|
||||
info: "libcrux Unsupported Algorithm",
|
||||
},
|
||||
libcrux_kem::Error::InvalidPrivateKey => KKTError::KEMError {
|
||||
info: "Invalid Private Key",
|
||||
},
|
||||
|
||||
libcrux_kem::Error::InvalidPublicKey => KKTError::KEMError {
|
||||
info: "Invalid Public Key",
|
||||
},
|
||||
libcrux_kem::Error::InvalidCiphertext => KKTError::KEMError {
|
||||
info: "Invalid Ciphertext",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
impl From<libcrux_ecdh::Error> for KKTError {
|
||||
fn from(err: libcrux_ecdh::Error) -> Self {
|
||||
match err {
|
||||
libcrux_ecdh::Error::InvalidPoint => KKTError::KEMError {
|
||||
info: "Invalid Remote Public Key",
|
||||
},
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// | 0 | 1 | 2, 3, 4, 5 | 6 | 7
|
||||
// [0] => KKT version (4 bits) + Message Sequence Count (4 bits)
|
||||
// [1] => Status (3 bits) + Mode (3 bits) + Role (2 bits)
|
||||
// [2..=5] => Ciphersuite
|
||||
// [6] => Reserved
|
||||
|
||||
use nym_crypto::asymmetric::x25519::PublicKey;
|
||||
|
||||
use crate::{
|
||||
context::{KKTContext, KKT_CONTEXT_LEN},
|
||||
error::KKTError,
|
||||
};
|
||||
|
||||
pub const KKT_SESSION_ID_LEN: usize = 16;
|
||||
|
||||
pub struct KKTFrame {
|
||||
context: Vec<u8>,
|
||||
session_id: Vec<u8>,
|
||||
body: Vec<u8>,
|
||||
signature: Vec<u8>,
|
||||
}
|
||||
|
||||
// if oneway and message coming from initiator => body is empty, signature contains signature of context + session id (64 bytes).
|
||||
// if message coming from anonymous initiator => body is empty, there is no signature.
|
||||
// if mutual and message coming from initiator => body has the initiator's kem public key and the signature is over the context + body + session_id.
|
||||
// if coming from responder => body has the responder's kem public key and the signature is over the context + body + session_id.
|
||||
|
||||
impl KKTFrame {
|
||||
pub fn new(context: &[u8], body: &[u8], session_id: &[u8], signature: &[u8]) -> Self {
|
||||
Self {
|
||||
context: Vec::from(context),
|
||||
body: Vec::from(body),
|
||||
session_id: Vec::from(session_id),
|
||||
signature: Vec::from(signature),
|
||||
}
|
||||
}
|
||||
pub fn context_ref<'a>(&'a self) -> &'a [u8] {
|
||||
&self.context
|
||||
}
|
||||
pub fn signature_ref<'a>(&'a self) -> &'a [u8] {
|
||||
&self.signature
|
||||
}
|
||||
pub fn body_ref<'a>(&'a self) -> &'a [u8] {
|
||||
&self.body
|
||||
}
|
||||
|
||||
pub fn session_id_ref<'a>(&'a self) -> &'a [u8] {
|
||||
&self.session_id
|
||||
}
|
||||
pub fn signature_mut<'a>(&'a mut self) -> &'a mut [u8] {
|
||||
&mut self.signature
|
||||
}
|
||||
pub fn body_mut<'a>(&'a mut self) -> &'a mut [u8] {
|
||||
&mut self.body
|
||||
}
|
||||
|
||||
pub fn session_id_mut<'a>(&'a mut self) -> &'a mut [u8] {
|
||||
&mut self.session_id
|
||||
}
|
||||
|
||||
pub fn frame_length(&self) -> usize {
|
||||
self.context.len() + self.session_id.len() + self.body.len() + self.signature.len()
|
||||
}
|
||||
|
||||
pub fn to_bytes(&self) -> Vec<u8> {
|
||||
let mut bytes = Vec::with_capacity(self.frame_length());
|
||||
bytes.extend_from_slice(&self.context);
|
||||
bytes.extend_from_slice(&self.body);
|
||||
bytes.extend_from_slice(&self.session_id);
|
||||
bytes.extend_from_slice(&self.signature);
|
||||
bytes
|
||||
}
|
||||
|
||||
pub fn from_bytes(bytes: &[u8]) -> Result<(Self, KKTContext), KKTError> {
|
||||
if bytes.len() < KKT_CONTEXT_LEN {
|
||||
return Err(KKTError::FrameDecodingError {
|
||||
info: format!(
|
||||
"Frame is shorter than expected context length: actual {} != expected {}",
|
||||
bytes.len(),
|
||||
KKT_CONTEXT_LEN
|
||||
),
|
||||
});
|
||||
} else {
|
||||
let context_bytes = Vec::from(&bytes[0..KKT_CONTEXT_LEN]);
|
||||
|
||||
let context = KKTContext::try_decode(&context_bytes)?;
|
||||
|
||||
let (mut session_id, mut body, mut signature): (Vec<u8>, Vec<u8>, Vec<u8>) =
|
||||
(vec![], vec![], vec![]);
|
||||
|
||||
if bytes.len() == context.full_message_len() {
|
||||
if context.body_len() > 0 {
|
||||
body.extend_from_slice(
|
||||
&bytes[KKT_CONTEXT_LEN..KKT_CONTEXT_LEN + context.body_len()],
|
||||
);
|
||||
}
|
||||
if context.session_id_len() > 0 {
|
||||
session_id.extend_from_slice(
|
||||
&bytes[KKT_CONTEXT_LEN + context.body_len()
|
||||
..KKT_CONTEXT_LEN + context.body_len() + context.session_id_len()],
|
||||
);
|
||||
}
|
||||
if context.signature_len() > 0 {
|
||||
signature.extend_from_slice(
|
||||
&bytes[KKT_CONTEXT_LEN + context.body_len() + context.session_id_len()
|
||||
..KKT_CONTEXT_LEN
|
||||
+ context.body_len()
|
||||
+ context.session_id_len()
|
||||
+ context.signature_len()],
|
||||
);
|
||||
}
|
||||
|
||||
Ok((
|
||||
KKTFrame::new(&context_bytes, &body, &session_id, &signature),
|
||||
context,
|
||||
))
|
||||
} else {
|
||||
return Err(KKTError::FrameDecodingError {
|
||||
info: format!(
|
||||
"Frame is shorter than expected: actual {} != expected {}",
|
||||
bytes.len(),
|
||||
context.full_message_len()
|
||||
),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EncryptedKKTRequest {
|
||||
ephermeral_key: PublicKey,
|
||||
body: Vec<u8>,
|
||||
}
|
||||
impl EncryptedKKTRequest {
|
||||
pub fn to_bytes(mut self) -> Vec<u8> {
|
||||
self.body.extend(self.ephermeral_key.to_bytes());
|
||||
self.body
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EncryptedKKTResponse {
|
||||
body: Vec<u8>,
|
||||
}
|
||||
@@ -1,107 +0,0 @@
|
||||
use crate::{
|
||||
ciphersuite::{HashFunction, KEM},
|
||||
error::KKTError,
|
||||
};
|
||||
|
||||
use classic_mceliece_rust::keypair_boxed;
|
||||
use libcrux_kem::{key_gen, Algorithm};
|
||||
|
||||
use libcrux_sha3;
|
||||
use rand::{CryptoRng, RngCore};
|
||||
|
||||
// (decapsulation_key, encapsulation_key)
|
||||
pub fn generate_keypair_libcrux<R>(
|
||||
rng: &mut R,
|
||||
kem: KEM,
|
||||
) -> Result<(libcrux_kem::PrivateKey, libcrux_kem::PublicKey), KKTError>
|
||||
where
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
match kem {
|
||||
KEM::MlKem768 => Ok(key_gen(Algorithm::MlKem768, rng)?),
|
||||
KEM::XWing => Ok(key_gen(Algorithm::XWingKemDraft06, rng)?),
|
||||
KEM::X25519 => Ok(key_gen(Algorithm::X25519, rng)?),
|
||||
_ => Err(KKTError::KEMError {
|
||||
info: "Key Generation Error: Unsupported Libcrux Algorithm",
|
||||
}),
|
||||
}
|
||||
}
|
||||
// (decapsulation_key, encapsulation_key)
|
||||
pub fn generate_keypair_mceliece<'a, R>(
|
||||
rng: &mut R,
|
||||
) -> (
|
||||
classic_mceliece_rust::SecretKey<'a>,
|
||||
classic_mceliece_rust::PublicKey<'a>,
|
||||
)
|
||||
where
|
||||
// this is annoying because mceliece lib uses rand 0.8.5...
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
let (encapsulation_key, decapsulation_key) = keypair_boxed(rng);
|
||||
(decapsulation_key, encapsulation_key)
|
||||
}
|
||||
|
||||
pub fn hash_key_bytes(
|
||||
hash_function: &HashFunction,
|
||||
hash_length: usize,
|
||||
key_bytes: &[u8],
|
||||
) -> Vec<u8> {
|
||||
let mut hashed_key: Vec<u8> = vec![0u8; hash_length];
|
||||
match hash_function {
|
||||
HashFunction::Blake3 => {
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
hasher.update(key_bytes);
|
||||
hasher.finalize_xof().fill(&mut hashed_key);
|
||||
hasher.reset();
|
||||
}
|
||||
HashFunction::SHAKE256 => {
|
||||
libcrux_sha3::shake256_ema(&mut hashed_key, &key_bytes);
|
||||
}
|
||||
HashFunction::SHAKE128 => {
|
||||
libcrux_sha3::shake128_ema(&mut hashed_key, &key_bytes);
|
||||
}
|
||||
HashFunction::SHA256 => {
|
||||
libcrux_sha3::sha256_ema(&mut hashed_key, &key_bytes);
|
||||
}
|
||||
}
|
||||
|
||||
hashed_key
|
||||
}
|
||||
|
||||
/// This does NOT run in constant time.
|
||||
// It's fine for KKT since we are comparing hashes.
|
||||
fn compare_hashes(a: &[u8], b: &[u8]) -> bool {
|
||||
a == b
|
||||
}
|
||||
|
||||
pub fn validate_encapsulation_key(
|
||||
hash_function: &HashFunction,
|
||||
hash_length: usize,
|
||||
encapsulation_key: &[u8],
|
||||
expected_hash_bytes: &[u8],
|
||||
) -> bool {
|
||||
compare_hashes(
|
||||
&hash_encapsulation_key(hash_function, hash_length, encapsulation_key),
|
||||
expected_hash_bytes,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn validate_key_bytes(
|
||||
hash_function: &HashFunction,
|
||||
hash_length: usize,
|
||||
key_bytes: &[u8],
|
||||
expected_hash_bytes: &[u8],
|
||||
) -> bool {
|
||||
compare_hashes(
|
||||
&hash_key_bytes(hash_function, hash_length, key_bytes),
|
||||
expected_hash_bytes,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn hash_encapsulation_key(
|
||||
hash_function: &HashFunction,
|
||||
hash_length: usize,
|
||||
encapsulation_key: &[u8],
|
||||
) -> Vec<u8> {
|
||||
hash_key_bytes(hash_function, hash_length, &encapsulation_key)
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod ciphersuite;
|
||||
pub mod context;
|
||||
// pub mod encryption;
|
||||
pub mod error;
|
||||
pub mod frame;
|
||||
pub mod key_utils;
|
||||
pub mod session;
|
||||
|
||||
// pub mod psq;
|
||||
|
||||
// This must be less than 4 bits
|
||||
pub const KKT_VERSION: u8 = 1;
|
||||
const _: () = assert!(KKT_VERSION < 1 << 4);
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use rand::prelude::*;
|
||||
|
||||
use crate::{
|
||||
ciphersuite::{Ciphersuite, EncapsulationKey, HashFunction, KEM},
|
||||
frame::KKTFrame,
|
||||
key_utils::{generate_keypair_libcrux, generate_keypair_mceliece, hash_encapsulation_key},
|
||||
session::{
|
||||
anonymous_initiator_process, initiator_ingest_response, initiator_process,
|
||||
responder_ingest_message, responder_process,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_kkt_psq_e2e_clear() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// generate ed25519 keys
|
||||
let mut secret_initiator: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut secret_initiator);
|
||||
let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0);
|
||||
|
||||
let mut secret_responder: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut secret_responder);
|
||||
let responder_ed25519_keypair = ed25519::KeyPair::from_secret(secret_responder, 1);
|
||||
for kem in [KEM::MlKem768, KEM::XWing, KEM::X25519, KEM::McEliece] {
|
||||
for hash_function in [
|
||||
HashFunction::Blake3,
|
||||
HashFunction::SHA256,
|
||||
HashFunction::SHAKE128,
|
||||
HashFunction::SHAKE256,
|
||||
] {
|
||||
let ciphersuite = Ciphersuite::resolve_ciphersuite(
|
||||
kem,
|
||||
hash_function,
|
||||
crate::ciphersuite::SignatureScheme::Ed25519,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// generate kem public keys
|
||||
|
||||
let (responder_kem_public_key, initiator_kem_public_key) = match kem {
|
||||
KEM::MlKem768 => (
|
||||
EncapsulationKey::MlKem768(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
EncapsulationKey::MlKem768(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
),
|
||||
KEM::XWing => (
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
EncapsulationKey::XWing(generate_keypair_libcrux(&mut rng, kem).unwrap().1),
|
||||
),
|
||||
KEM::X25519 => (
|
||||
EncapsulationKey::X25519(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
EncapsulationKey::X25519(
|
||||
generate_keypair_libcrux(&mut rng, kem).unwrap().1,
|
||||
),
|
||||
),
|
||||
KEM::McEliece => (
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
EncapsulationKey::McEliece(generate_keypair_mceliece(&mut rng).1),
|
||||
),
|
||||
};
|
||||
|
||||
let i_kem_key_bytes = initiator_kem_public_key.encode();
|
||||
|
||||
let r_kem_key_bytes = responder_kem_public_key.encode();
|
||||
|
||||
let i_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&i_kem_key_bytes,
|
||||
);
|
||||
|
||||
let r_dir_hash = hash_encapsulation_key(
|
||||
&ciphersuite.hash_function(),
|
||||
ciphersuite.hash_len(),
|
||||
&r_kem_key_bytes,
|
||||
);
|
||||
|
||||
// Anonymous Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) =
|
||||
anonymous_initiator_process(&mut rng, ciphersuite).unwrap();
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
let (mut r_context, _) =
|
||||
responder_ingest_message(&r_context, None, None, &i_frame_r).unwrap();
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id_ref(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let r_bytes = r_frame.to_bytes();
|
||||
|
||||
let obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
&r_bytes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
// Initiator, OneWay
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::OneWay,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
None,
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(r_obtained_key.is_none());
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id_ref(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let r_bytes = r_frame.to_bytes();
|
||||
|
||||
let i_obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
&r_bytes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(i_obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
|
||||
// Initiator, Mutual
|
||||
{
|
||||
let (mut i_context, i_frame) = initiator_process(
|
||||
&mut rng,
|
||||
crate::context::KKTMode::Mutual,
|
||||
ciphersuite,
|
||||
initiator_ed25519_keypair.private_key(),
|
||||
Some(&initiator_kem_public_key),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let i_frame_bytes = i_frame.to_bytes();
|
||||
|
||||
let (i_frame_r, r_context) = KKTFrame::from_bytes(&i_frame_bytes).unwrap();
|
||||
|
||||
let (mut r_context, r_obtained_key) = responder_ingest_message(
|
||||
&r_context,
|
||||
Some(initiator_ed25519_keypair.public_key()),
|
||||
Some(&i_dir_hash),
|
||||
&i_frame_r,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(r_obtained_key.unwrap().encode(), i_kem_key_bytes);
|
||||
|
||||
let r_frame = responder_process(
|
||||
&mut r_context,
|
||||
i_frame_r.session_id_ref(),
|
||||
responder_ed25519_keypair.private_key(),
|
||||
&responder_kem_public_key,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let r_bytes = r_frame.to_bytes();
|
||||
|
||||
let obtained_key = initiator_ingest_response(
|
||||
&mut i_context,
|
||||
responder_ed25519_keypair.public_key(),
|
||||
&r_dir_hash,
|
||||
&r_bytes,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(obtained_key.encode(), r_kem_key_bytes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,228 +0,0 @@
|
||||
use nym_crypto::asymmetric::ed25519::{self, Signature};
|
||||
use rand::{CryptoRng, RngCore};
|
||||
|
||||
use crate::{
|
||||
ciphersuite::{Ciphersuite, EncapsulationKey},
|
||||
context::{KKTContext, KKTMode, KKTRole, KKTStatus},
|
||||
error::KKTError,
|
||||
frame::{KKTFrame, KKT_SESSION_ID_LEN},
|
||||
key_utils::validate_encapsulation_key,
|
||||
};
|
||||
|
||||
pub fn initiator_process<'a, R>(
|
||||
rng: &mut R,
|
||||
mode: KKTMode,
|
||||
ciphersuite: Ciphersuite,
|
||||
signing_key: &ed25519::PrivateKey,
|
||||
own_encapsulation_key: Option<&EncapsulationKey<'a>>,
|
||||
) -> Result<(KKTContext, KKTFrame), KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
let context = KKTContext::new(KKTRole::Initiator, mode, ciphersuite)?;
|
||||
|
||||
let context_bytes = context.encode()?;
|
||||
|
||||
let mut session_id = [0; KKT_SESSION_ID_LEN];
|
||||
// Generate Session ID
|
||||
rng.fill_bytes(&mut session_id);
|
||||
|
||||
let body: &[u8] = match mode {
|
||||
KKTMode::OneWay => &[],
|
||||
KKTMode::Mutual => match own_encapsulation_key {
|
||||
Some(encaps_key) => &encaps_key.encode(),
|
||||
|
||||
// Missing key
|
||||
None => {
|
||||
return Err(KKTError::FunctionInputError {
|
||||
info: "KEM Key Not Provided",
|
||||
})
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
let mut bytes_to_sign =
|
||||
Vec::with_capacity(context.full_message_len() - context.signature_len());
|
||||
bytes_to_sign.extend_from_slice(&context_bytes);
|
||||
bytes_to_sign.extend_from_slice(&body);
|
||||
bytes_to_sign.extend_from_slice(&session_id);
|
||||
|
||||
let signature = signing_key.sign(bytes_to_sign).to_bytes();
|
||||
|
||||
Ok((
|
||||
context,
|
||||
KKTFrame::new(&context_bytes, &body, &session_id, &signature),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn anonymous_initiator_process<R>(
|
||||
rng: &mut R,
|
||||
ciphersuite: Ciphersuite,
|
||||
) -> Result<(KKTContext, KKTFrame), KKTError>
|
||||
where
|
||||
R: CryptoRng + RngCore,
|
||||
{
|
||||
let context = KKTContext::new(KKTRole::AnonymousInitiator, KKTMode::OneWay, ciphersuite)?;
|
||||
let context_bytes = context.encode()?;
|
||||
|
||||
let mut session_id = [0u8; KKT_SESSION_ID_LEN];
|
||||
rng.fill_bytes(&mut session_id);
|
||||
|
||||
Ok((
|
||||
context,
|
||||
KKTFrame::new(&context_bytes, &[], &session_id, &[]),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn initiator_ingest_response<'a>(
|
||||
own_context: &mut KKTContext,
|
||||
remote_verification_key: &ed25519::PublicKey,
|
||||
expected_hash: &[u8],
|
||||
message_bytes: &[u8],
|
||||
) -> Result<EncapsulationKey<'a>, KKTError> {
|
||||
// sizes have to be correct
|
||||
let (frame, remote_context) = KKTFrame::from_bytes(message_bytes)?;
|
||||
|
||||
check_compatibility(&own_context, &remote_context)?;
|
||||
match remote_context.status() {
|
||||
KKTStatus::Ok => {
|
||||
let mut bytes_to_verify: Vec<u8> = Vec::with_capacity(
|
||||
remote_context.full_message_len() - remote_context.signature_len(),
|
||||
);
|
||||
bytes_to_verify.extend_from_slice(&remote_context.encode()?);
|
||||
bytes_to_verify.extend_from_slice(frame.body_ref());
|
||||
bytes_to_verify.extend_from_slice(frame.session_id_ref());
|
||||
|
||||
match Signature::from_bytes(frame.signature_ref()) {
|
||||
Ok(sig) => match remote_verification_key.verify(bytes_to_verify, &sig) {
|
||||
Ok(()) => {
|
||||
let received_encapsulation_key = EncapsulationKey::decode(
|
||||
own_context.ciphersuite().kem(),
|
||||
frame.body_ref(),
|
||||
)?;
|
||||
|
||||
match validate_encapsulation_key(
|
||||
&own_context.ciphersuite().hash_function(),
|
||||
own_context.ciphersuite().hash_len(),
|
||||
&frame.body_ref(),
|
||||
expected_hash,
|
||||
) {
|
||||
true => Ok(received_encapsulation_key),
|
||||
|
||||
// The key does not match the hash obtained from the directory
|
||||
false => Err(KKTError::KEMError { info: "Hash of received encapsulation key does not match the value stored on the directory." }),
|
||||
}
|
||||
}
|
||||
Err(_) => Err(KKTError::SigVerifError),
|
||||
},
|
||||
Err(_) => Err(KKTError::SigConstructorError),
|
||||
}
|
||||
}
|
||||
_ => Err(KKTError::ResponderFlaggedError {
|
||||
status: remote_context.status(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
// todo: figure out how to handle errors using status codes
|
||||
|
||||
pub fn responder_ingest_message<'a>(
|
||||
remote_context: &KKTContext,
|
||||
remote_verification_key: Option<&ed25519::PublicKey>,
|
||||
expected_hash: Option<&[u8]>,
|
||||
remote_frame: &KKTFrame,
|
||||
) -> Result<(KKTContext, Option<EncapsulationKey<'a>>), KKTError> {
|
||||
let own_context = remote_context.derive_responder_header()?;
|
||||
|
||||
match remote_context.role() {
|
||||
KKTRole::AnonymousInitiator => Ok((own_context, None)),
|
||||
|
||||
KKTRole::Initiator => {
|
||||
match remote_verification_key {
|
||||
Some(remote_verif_key) => {
|
||||
let mut bytes_to_verify: Vec<u8> = Vec::with_capacity(
|
||||
own_context.full_message_len() - own_context.signature_len(),
|
||||
);
|
||||
bytes_to_verify.extend_from_slice(remote_frame.context_ref());
|
||||
bytes_to_verify.extend_from_slice(remote_frame.body_ref());
|
||||
bytes_to_verify.extend_from_slice(remote_frame.session_id_ref());
|
||||
|
||||
match Signature::from_bytes(remote_frame.signature_ref()) {
|
||||
Ok(sig) => match remote_verif_key.verify(bytes_to_verify, &sig) {
|
||||
Ok(()) => {
|
||||
// using own_context here because maybe for whatever reason we want to ignore the remote kem key
|
||||
match own_context.mode() {
|
||||
KKTMode::OneWay => return Ok((own_context, None)),
|
||||
KKTMode::Mutual => {
|
||||
match expected_hash {
|
||||
Some(expected_hash) =>{
|
||||
let received_encapsulation_key =
|
||||
EncapsulationKey::decode(own_context.ciphersuite().kem(), remote_frame.body_ref())?;
|
||||
if
|
||||
validate_encapsulation_key(
|
||||
&own_context.ciphersuite().hash_function(),
|
||||
own_context.ciphersuite().hash_len(),
|
||||
&remote_frame.body_ref(),
|
||||
expected_hash,
|
||||
){
|
||||
Ok((own_context, Some(received_encapsulation_key)))
|
||||
}
|
||||
// The key does not match the hash obtained from the directory
|
||||
else {
|
||||
Err(KKTError::KEMError { info: "Hash of received encapsulation key does not match the value stored on the directory." })
|
||||
}
|
||||
}
|
||||
None => Err(KKTError::FunctionInputError { info: "Expected hash of the remote encapsulation key is not provided." }),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(_) => Err(KKTError::SigVerifError),
|
||||
},
|
||||
Err(_) => Err(KKTError::SigConstructorError),
|
||||
}
|
||||
}
|
||||
None => Err(KKTError::FunctionInputError {
|
||||
info: "Remote Signature Verification Key Not Provided",
|
||||
}),
|
||||
}
|
||||
}
|
||||
KKTRole::Responder => Err(KKTError::IncompatibilityError {
|
||||
info: "Responder received a request from another responder.",
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn responder_process<'a>(
|
||||
own_context: &mut KKTContext,
|
||||
session_id: &[u8],
|
||||
signing_key: &ed25519::PrivateKey,
|
||||
encapsulation_key: &EncapsulationKey<'a>,
|
||||
) -> Result<KKTFrame, KKTError> {
|
||||
let body = encapsulation_key.encode();
|
||||
|
||||
let context_bytes = own_context.encode()?;
|
||||
|
||||
let mut bytes_to_sign =
|
||||
Vec::with_capacity(own_context.full_message_len() - own_context.signature_len());
|
||||
bytes_to_sign.extend_from_slice(&own_context.encode()?);
|
||||
bytes_to_sign.extend_from_slice(&body);
|
||||
bytes_to_sign.extend_from_slice(&session_id);
|
||||
|
||||
let signature = signing_key.sign(bytes_to_sign).to_bytes();
|
||||
|
||||
Ok(KKTFrame::new(
|
||||
&context_bytes,
|
||||
&body,
|
||||
&session_id,
|
||||
&signature,
|
||||
))
|
||||
}
|
||||
|
||||
fn check_compatibility(
|
||||
_own_context: &KKTContext,
|
||||
_remote_context: &KKTContext,
|
||||
) -> Result<(), KKTError> {
|
||||
// todo: check ciphersuite/context compatibility
|
||||
Ok(())
|
||||
}
|
||||
@@ -22,12 +22,6 @@ tokio-util = { workspace = true, features = ["codec"] }
|
||||
nym-crypto = { path = "../crypto" }
|
||||
nym-noise-keys = { path = "keys" }
|
||||
|
||||
nympsq = {path = "../nympsq"}
|
||||
libcrux-ed25519 = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = ["test-utils"] }
|
||||
libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
|
||||
rand = "0.9.2"
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
tokio = { workspace = true, features = ["full"] }
|
||||
|
||||
@@ -9,7 +9,6 @@ use serde::{Deserialize, Serialize};
|
||||
#[serde(from = "u8", into = "u8")]
|
||||
pub enum NoiseVersion {
|
||||
V1,
|
||||
V2,
|
||||
Unknown(u8), //Implies a newer version we don't know
|
||||
}
|
||||
|
||||
@@ -17,7 +16,6 @@ impl From<u8> for NoiseVersion {
|
||||
fn from(value: u8) -> Self {
|
||||
match value {
|
||||
1 => NoiseVersion::V1,
|
||||
2 => NoiseVersion::V2,
|
||||
other => NoiseVersion::Unknown(other),
|
||||
}
|
||||
}
|
||||
@@ -27,7 +25,6 @@ impl From<NoiseVersion> for u8 {
|
||||
fn from(version: NoiseVersion) -> Self {
|
||||
match version {
|
||||
NoiseVersion::V1 => 1,
|
||||
NoiseVersion::V2 => 2,
|
||||
NoiseVersion::Unknown(other) => other,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_noise_keys::NoiseVersion;
|
||||
use nympsq::error::PSQError;
|
||||
use snow::Error;
|
||||
use std::io;
|
||||
use thiserror::Error;
|
||||
@@ -71,12 +70,6 @@ pub enum NoiseError {
|
||||
|
||||
#[error("Handshake timeout")]
|
||||
HandshakeTimeout(#[from] tokio::time::error::Elapsed),
|
||||
|
||||
#[error("Missing Field")]
|
||||
MissingField { info: &'static str },
|
||||
|
||||
#[error("PSQ Error")]
|
||||
PSQError(PSQError),
|
||||
}
|
||||
|
||||
impl NoiseError {
|
||||
@@ -88,18 +81,6 @@ impl NoiseError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PSQError> for NoiseError {
|
||||
fn from(err: PSQError) -> Self {
|
||||
NoiseError::PSQError(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<libcrux_kem::Error> for NoiseError {
|
||||
fn from(err: libcrux_kem::Error) -> Self {
|
||||
NoiseError::PSQError(err.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for NoiseError {
|
||||
fn from(err: Error) -> Self {
|
||||
match err {
|
||||
|
||||
+28
-136
@@ -1,11 +1,9 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use nym_noise_keys::NoiseVersion;
|
||||
use nympsq::psq::CONTEXT_LEN;
|
||||
|
||||
use snow::{error::Prerequisite, Error};
|
||||
use snow::error::Prerequisite;
|
||||
use snow::Error;
|
||||
use tokio::net::TcpStream;
|
||||
use tracing::{error, warn};
|
||||
|
||||
@@ -14,138 +12,48 @@ pub mod connection;
|
||||
pub mod error;
|
||||
pub mod stream;
|
||||
|
||||
use crate::{
|
||||
config::NoiseConfig, connection::Connection, error::NoiseError, stream::NoiseStreamBuilder,
|
||||
};
|
||||
use crate::config::NoiseConfig;
|
||||
use crate::connection::Connection;
|
||||
use crate::error::NoiseError;
|
||||
use crate::stream::NoiseStreamBuilder;
|
||||
|
||||
const NOISE_PSK_PREFIX: &[u8] = b"NYMTECH_NOISE_dQw4w9WgXcQ";
|
||||
|
||||
pub(crate) const NOISE_PSQ_DEFAULT_CONTEXT: &[u8; CONTEXT_LEN] = b"Exsl88AD2ccS99kk";
|
||||
pub(crate) const NOISE_PSQ_DEFAULT_DURATION_SECS: u64 = 1000;
|
||||
|
||||
pub const LATEST_NOISE_VERSION: NoiseVersion = NoiseVersion::V2;
|
||||
pub const LATEST_NOISE_VERSION: NoiseVersion = NoiseVersion::V1;
|
||||
|
||||
// TODO: this should be behind some trait because presumably, depending on the version,
|
||||
// other arguments would be needed
|
||||
mod psk_gen {
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::{error::NoiseError, stream::Psk, NOISE_PSK_PREFIX};
|
||||
use libcrux_ed25519::VerificationKey;
|
||||
use libcrux_kem::{PrivateKey, PublicKey};
|
||||
use libcrux_psq::impls::{MlKem768, XWingKemDraft06, X25519};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
|
||||
use nympsq::{
|
||||
error::PSQError,
|
||||
psq::{PSQInitiator, PSQResponder, CONTEXT_LEN, PSK_HANDLE_LEN},
|
||||
};
|
||||
|
||||
use crate::error::NoiseError;
|
||||
use crate::stream::Psk;
|
||||
use crate::NOISE_PSK_PREFIX;
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_noise_keys::NoiseVersion;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
pub fn generate_psk_v1(responder_pub_key: &[u8]) -> [u8; 32] {
|
||||
pub(crate) fn generate_psk(
|
||||
responder_pub_key: x25519::PublicKey,
|
||||
version: NoiseVersion,
|
||||
) -> Result<Psk, NoiseError> {
|
||||
match version {
|
||||
NoiseVersion::V1 => Ok(generate_psk_v1(responder_pub_key)),
|
||||
NoiseVersion::Unknown(noise_version) => {
|
||||
Err(NoiseError::PskGenerationFailure { noise_version })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_psk_v1(responder_pub_key: x25519::PublicKey) -> [u8; 32] {
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(NOISE_PSK_PREFIX);
|
||||
hasher.update(responder_pub_key);
|
||||
hasher.update(responder_pub_key.to_bytes());
|
||||
hasher.finalize().into()
|
||||
}
|
||||
|
||||
pub(crate) fn psq_initiate_x25519(
|
||||
initiator: &mut PSQInitiator<X25519>,
|
||||
responder_pub_key: impl AsRef<[u8]>,
|
||||
context: &[u8; CONTEXT_LEN],
|
||||
psq_ttl: Duration,
|
||||
) -> Result<Vec<u8>, NoiseError> {
|
||||
let pub_key =
|
||||
PublicKey::decode(libcrux_kem::Algorithm::X25519, responder_pub_key.as_ref())?;
|
||||
let message =
|
||||
initiator.compute_initiator_message(&mut rand::rng(), &pub_key, context, psq_ttl)?;
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
pub(crate) fn psq_respond_x25519(
|
||||
responder_private_key: impl AsRef<[u8]>,
|
||||
responder_public_key: impl AsRef<[u8]>,
|
||||
initiator_verification_key: &ed25519::PublicKey,
|
||||
initiator_message: &mut [u8],
|
||||
context: &[u8; CONTEXT_LEN],
|
||||
psq_ttl: Duration,
|
||||
psk_handle: &[u8; PSK_HANDLE_LEN],
|
||||
) -> Result<(Psk, Vec<u8>), PSQError> {
|
||||
let kem_private_key = PrivateKey::decode(
|
||||
libcrux_kem::Algorithm::X25519,
|
||||
responder_private_key.as_ref(),
|
||||
)?;
|
||||
let kem_public_key = PublicKey::decode(
|
||||
libcrux_kem::Algorithm::X25519,
|
||||
responder_public_key.as_ref(),
|
||||
)?;
|
||||
|
||||
let responder: PSQResponder<X25519> = PSQResponder::init(&kem_private_key, &kem_public_key);
|
||||
|
||||
let res = responder.compute_responder_message(
|
||||
&VerificationKey::from_bytes(initiator_verification_key.to_bytes()),
|
||||
initiator_message,
|
||||
context,
|
||||
psq_ttl,
|
||||
psk_handle,
|
||||
)?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub(crate) fn psq_initiate_xwing(
|
||||
initiator: &mut PSQInitiator<XWingKemDraft06>,
|
||||
responder_pub_key: impl AsRef<[u8]>,
|
||||
context: &[u8; CONTEXT_LEN],
|
||||
psq_ttl: Duration,
|
||||
) -> Result<Vec<u8>, NoiseError> {
|
||||
let pub_key = PublicKey::decode(
|
||||
libcrux_kem::Algorithm::XWingKemDraft06,
|
||||
responder_pub_key.as_ref(),
|
||||
)?;
|
||||
let message =
|
||||
initiator.compute_initiator_message(&mut rand::rng(), &pub_key, context, psq_ttl)?;
|
||||
Ok(message)
|
||||
}
|
||||
|
||||
pub(crate) fn psq_respond_xwing(
|
||||
responder_private_key: impl AsRef<[u8]>,
|
||||
responder_public_key: impl AsRef<[u8]>,
|
||||
initiator_verification_key: &ed25519::PublicKey,
|
||||
initiator_message: &mut [u8],
|
||||
context: &[u8; CONTEXT_LEN],
|
||||
psq_ttl: Duration,
|
||||
psk_handle: &[u8; PSK_HANDLE_LEN],
|
||||
) -> Result<(Psk, Vec<u8>), PSQError> {
|
||||
let kem_private_key = PrivateKey::decode(
|
||||
libcrux_kem::Algorithm::XWingKemDraft06,
|
||||
responder_private_key.as_ref(),
|
||||
)?;
|
||||
let kem_public_key = PublicKey::decode(
|
||||
libcrux_kem::Algorithm::XWingKemDraft06,
|
||||
responder_public_key.as_ref(),
|
||||
)?;
|
||||
|
||||
let responder: PSQResponder<XWingKemDraft06> =
|
||||
PSQResponder::init(&kem_private_key, &kem_public_key);
|
||||
|
||||
let res = responder.compute_responder_message(
|
||||
&VerificationKey::from_bytes(initiator_verification_key.to_bytes()),
|
||||
initiator_message,
|
||||
context,
|
||||
psq_ttl,
|
||||
psk_handle,
|
||||
)?;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn upgrade_noise_initiator(
|
||||
conn: TcpStream,
|
||||
config: &NoiseConfig,
|
||||
initiator_identity_keypair: Option<ed25519::KeyPair>,
|
||||
) -> Result<Connection<TcpStream>, NoiseError> {
|
||||
if config.unsafe_disabled {
|
||||
warn!("Noise is disabled in the config. Not attempting any handshake");
|
||||
@@ -165,7 +73,6 @@ pub async fn upgrade_noise_initiator(
|
||||
|
||||
let handshake_version = match key.supported_version {
|
||||
NoiseVersion::V1 => NoiseVersion::V1,
|
||||
NoiseVersion::V2 => NoiseVersion::V2,
|
||||
|
||||
// We're talking to a more recent node, but we can't adapt. Let's try to do our best and if it fails, it fails.
|
||||
// If that node sees we're older, it will try to adapt too.
|
||||
@@ -175,29 +82,14 @@ pub async fn upgrade_noise_initiator(
|
||||
}
|
||||
};
|
||||
|
||||
let (signing_key, verification_key): (Option<[u8; 32]>, Option<[u8; 32]>) =
|
||||
match initiator_identity_keypair {
|
||||
Some(keypair) => (
|
||||
Some(keypair.private_key().to_bytes()),
|
||||
Some(keypair.public_key().to_bytes()),
|
||||
),
|
||||
None => (None, None),
|
||||
};
|
||||
NoiseStreamBuilder::new(conn)
|
||||
.perform_initiator_handshake(
|
||||
config,
|
||||
handshake_version,
|
||||
key.x25519_pubkey,
|
||||
signing_key,
|
||||
verification_key,
|
||||
)
|
||||
.perform_initiator_handshake(config, handshake_version, key.x25519_pubkey)
|
||||
.await
|
||||
.map(|stream| Connection::Noise(Box::new(stream)))
|
||||
}
|
||||
pub async fn upgrade_noise_responder(
|
||||
conn: TcpStream,
|
||||
config: &NoiseConfig,
|
||||
initiator_verification_key: Option<ed25519::PublicKey>,
|
||||
) -> Result<Connection<TcpStream>, NoiseError> {
|
||||
if config.unsafe_disabled {
|
||||
warn!("Noise is disabled in the config. Not attempting any handshake");
|
||||
@@ -220,7 +112,7 @@ pub async fn upgrade_noise_responder(
|
||||
};
|
||||
|
||||
NoiseStreamBuilder::new(conn)
|
||||
.perform_responder_handshake(config, initiator_verification_key)
|
||||
.perform_responder_handshake(config)
|
||||
.await
|
||||
.map(|stream| Connection::Noise(Box::new(stream)))
|
||||
}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{
|
||||
config::{NoiseConfig, NoisePattern},
|
||||
error::NoiseError,
|
||||
psk_gen::{generate_psk_v1, psq_initiate_x25519, psq_respond_x25519},
|
||||
stream::{codec::NymNoiseCodec, framing::NymNoiseFrame},
|
||||
NOISE_PSQ_DEFAULT_CONTEXT, NOISE_PSQ_DEFAULT_DURATION_SECS,
|
||||
};
|
||||
|
||||
use crate::config::{NoiseConfig, NoisePattern};
|
||||
use crate::error::NoiseError;
|
||||
use crate::psk_gen::generate_psk;
|
||||
use crate::stream::codec::NymNoiseCodec;
|
||||
use crate::stream::framing::NymNoiseFrame;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures::{Sink, SinkExt, Stream, StreamExt};
|
||||
use nym_crypto::asymmetric::{ed25519, x25519};
|
||||
use nym_crypto::asymmetric::x25519;
|
||||
use nym_noise_keys::NoiseVersion;
|
||||
use nympsq::psq::{PSQInitiator, PSK_HANDLE_LEN};
|
||||
use snow::{Builder, HandshakeState, TransportState};
|
||||
use std::{cmp::min, io, pin::Pin, task::ready, task::Poll, time::Duration};
|
||||
use std::io;
|
||||
use std::pin::Pin;
|
||||
use std::task::Poll;
|
||||
use std::{cmp::min, task::ready};
|
||||
use tokio::io::{AsyncRead, AsyncWrite, ReadBuf};
|
||||
use tokio_util::codec::Framed;
|
||||
|
||||
@@ -23,7 +22,6 @@ mod codec;
|
||||
mod framing;
|
||||
|
||||
const TAGLEN: usize = 16;
|
||||
// This needs to be increased when using psq
|
||||
const HANDSHAKE_MAX_LEN: usize = 1024; // using this constant to limit the handshake's buffer size
|
||||
|
||||
pub(crate) type Psk = [u8; 32];
|
||||
@@ -47,65 +45,19 @@ impl<C> NoiseStreamBuilder<C> {
|
||||
pattern: NoisePattern,
|
||||
local_private_key: impl AsRef<[u8]>,
|
||||
remote_pub_key: impl AsRef<[u8]>,
|
||||
local_signing_key: Option<impl AsRef<[u8]>>,
|
||||
local_verification_key: Option<impl AsRef<[u8]>>,
|
||||
psk: Psk,
|
||||
version: NoiseVersion,
|
||||
) -> Result<NoiseStream<C>, NoiseError>
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let mut handshake = Builder::new(pattern.as_noise_params())
|
||||
let handshake = Builder::new(pattern.as_noise_params())
|
||||
.local_private_key(local_private_key.as_ref())
|
||||
.remote_public_key(remote_pub_key.as_ref())
|
||||
.psk(pattern.psk_position(), &psk)
|
||||
.build_initiator()?;
|
||||
|
||||
let payload: Vec<u8> = match version {
|
||||
NoiseVersion::V1 => {
|
||||
handshake.set_psk(
|
||||
pattern.psk_position() as usize,
|
||||
&generate_psk_v1(remote_pub_key.as_ref()),
|
||||
)?;
|
||||
vec![]
|
||||
}
|
||||
NoiseVersion::V2 => match (local_signing_key, local_verification_key) {
|
||||
(Some(signing_key), Some(verification_key)) => {
|
||||
let mut psq_initiator = PSQInitiator::init(signing_key, verification_key);
|
||||
let payload = psq_initiate_x25519(
|
||||
&mut psq_initiator,
|
||||
&remote_pub_key,
|
||||
NOISE_PSQ_DEFAULT_CONTEXT,
|
||||
Duration::from_secs(NOISE_PSQ_DEFAULT_DURATION_SECS),
|
||||
)?;
|
||||
handshake.set_psk(
|
||||
pattern.psk_position() as usize,
|
||||
//
|
||||
&psq_initiator.get_psk().unwrap(),
|
||||
)?;
|
||||
payload
|
||||
}
|
||||
(None, None) => {
|
||||
return Err(NoiseError::MissingField {
|
||||
info: "Missing Local Signing Key and Local Verification Key",
|
||||
})
|
||||
}
|
||||
(None, _) => {
|
||||
return Err(NoiseError::MissingField {
|
||||
info: "Local Signing Key",
|
||||
})
|
||||
}
|
||||
(_, None) => {
|
||||
return Err(NoiseError::MissingField {
|
||||
info: "Local Verification Key",
|
||||
})
|
||||
}
|
||||
},
|
||||
NoiseVersion::Unknown(version) => {
|
||||
return Err(NoiseError::UnknownVersion { encoded: version })
|
||||
}
|
||||
};
|
||||
|
||||
self.perform_handshake(handshake, payload, version, pattern)
|
||||
.await
|
||||
self.perform_handshake(handshake, version, pattern).await
|
||||
}
|
||||
|
||||
pub(crate) async fn perform_initiator_handshake(
|
||||
@@ -113,12 +65,12 @@ impl<C> NoiseStreamBuilder<C> {
|
||||
config: &NoiseConfig,
|
||||
version: NoiseVersion,
|
||||
remote_pub_key: x25519::PublicKey,
|
||||
local_signing_key: Option<impl AsRef<[u8]>>,
|
||||
local_verification_key: Option<impl AsRef<[u8]>>,
|
||||
) -> Result<NoiseStream<C>, NoiseError>
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let psk = generate_psk(remote_pub_key, version)?;
|
||||
|
||||
let timeout = config.timeout;
|
||||
tokio::time::timeout(
|
||||
timeout,
|
||||
@@ -126,8 +78,7 @@ impl<C> NoiseStreamBuilder<C> {
|
||||
config.pattern,
|
||||
config.local_key.private_key(),
|
||||
remote_pub_key,
|
||||
local_signing_key,
|
||||
local_verification_key,
|
||||
psk,
|
||||
version,
|
||||
),
|
||||
)
|
||||
@@ -138,8 +89,7 @@ impl<C> NoiseStreamBuilder<C> {
|
||||
mut self,
|
||||
noise_pattern: NoisePattern,
|
||||
local_private_key: impl AsRef<[u8]>,
|
||||
local_public_key: impl AsRef<[u8]>,
|
||||
initiator_verification_key: Option<ed25519::PublicKey>,
|
||||
local_pub_key: x25519::PublicKey,
|
||||
) -> Result<NoiseStream<C>, NoiseError>
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin,
|
||||
@@ -167,58 +117,26 @@ impl<C> NoiseStreamBuilder<C> {
|
||||
});
|
||||
}
|
||||
|
||||
// 2. generate psk and handshake state
|
||||
let psk = generate_psk(local_pub_key, initial_frame.header.version)?;
|
||||
|
||||
let mut handshake = Builder::new(pattern.as_noise_params())
|
||||
.local_private_key(local_private_key.as_ref())
|
||||
.psk(pattern.psk_position(), &psk)
|
||||
.build_responder()?;
|
||||
|
||||
// update handshake state with initial frame
|
||||
let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN);
|
||||
let len = handshake.read_message(&initial_frame.data, &mut buf)?;
|
||||
buf.truncate(len);
|
||||
|
||||
let payload: Vec<u8> = match initial_frame.version() {
|
||||
NoiseVersion::V1 => {
|
||||
handshake.set_psk(
|
||||
pattern.psk_position() as usize,
|
||||
&generate_psk_v1(local_public_key.as_ref()),
|
||||
)?;
|
||||
vec![]
|
||||
}
|
||||
NoiseVersion::V2 => match initiator_verification_key {
|
||||
Some(verif_key) => {
|
||||
let (psk, message) = psq_respond_x25519(
|
||||
&local_private_key,
|
||||
&local_public_key,
|
||||
&verif_key,
|
||||
&mut buf,
|
||||
NOISE_PSQ_DEFAULT_CONTEXT,
|
||||
Duration::from_secs(NOISE_PSQ_DEFAULT_DURATION_SECS),
|
||||
&[1; PSK_HANDLE_LEN],
|
||||
)?;
|
||||
handshake.set_psk(pattern.psk_position() as usize, psk.as_slice())?;
|
||||
|
||||
message
|
||||
}
|
||||
None => {
|
||||
return Err(NoiseError::MissingField {
|
||||
info: "Initiator Verification Key",
|
||||
})
|
||||
}
|
||||
},
|
||||
NoiseVersion::Unknown(version) => {
|
||||
return Err(NoiseError::UnknownVersion { encoded: version })
|
||||
}
|
||||
};
|
||||
handshake.read_message(&initial_frame.data, &mut buf)?;
|
||||
|
||||
// 3. run handshake to completion
|
||||
self.perform_handshake(handshake, payload, initial_frame.version(), pattern)
|
||||
self.perform_handshake(handshake, initial_frame.version(), pattern)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(crate) async fn perform_responder_handshake(
|
||||
self,
|
||||
config: &NoiseConfig,
|
||||
initiator_verification_key: Option<ed25519::PublicKey>,
|
||||
) -> Result<NoiseStream<C>, NoiseError>
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin,
|
||||
@@ -229,8 +147,7 @@ impl<C> NoiseStreamBuilder<C> {
|
||||
self.perform_responder_handshake_inner(
|
||||
config.pattern,
|
||||
config.local_key.private_key(),
|
||||
config.local_key.public_key(),
|
||||
initiator_verification_key,
|
||||
*config.local_key.public_key(),
|
||||
),
|
||||
)
|
||||
.await?
|
||||
@@ -239,7 +156,6 @@ impl<C> NoiseStreamBuilder<C> {
|
||||
async fn send_handshake_msg(
|
||||
&mut self,
|
||||
handshake: &mut HandshakeState,
|
||||
payload: impl AsRef<[u8]>,
|
||||
version: NoiseVersion,
|
||||
pattern: NoisePattern,
|
||||
) -> Result<(), NoiseError>
|
||||
@@ -247,7 +163,7 @@ impl<C> NoiseStreamBuilder<C> {
|
||||
C: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN); // we're in the handshake, we can afford a smaller buffer
|
||||
let len = handshake.write_message(payload.as_ref(), &mut buf)?;
|
||||
let len = handshake.write_message(&[], &mut buf)?;
|
||||
buf.truncate(len);
|
||||
|
||||
let frame = NymNoiseFrame::new_handshake_frame(buf.freeze(), version, pattern)?;
|
||||
@@ -260,7 +176,7 @@ impl<C> NoiseStreamBuilder<C> {
|
||||
handshake: &mut HandshakeState,
|
||||
version: NoiseVersion,
|
||||
pattern: NoisePattern,
|
||||
) -> Result<Bytes, NoiseError>
|
||||
) -> Result<(), NoiseError>
|
||||
where
|
||||
C: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
@@ -283,12 +199,9 @@ impl<C> NoiseStreamBuilder<C> {
|
||||
});
|
||||
}
|
||||
|
||||
// The buffer below is the payload...
|
||||
// we're in the handshake, we can afford a smaller buffer
|
||||
|
||||
let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN);
|
||||
let mut buf = BytesMut::zeroed(HANDSHAKE_MAX_LEN); // we're in the handshake, we can afford a smaller buffer
|
||||
handshake.read_message(&frame.data, &mut buf)?;
|
||||
Ok(buf.into())
|
||||
Ok(())
|
||||
}
|
||||
Some(Err(err)) => Err(err),
|
||||
None => Err(NoiseError::HandshakeError),
|
||||
@@ -298,7 +211,6 @@ impl<C> NoiseStreamBuilder<C> {
|
||||
async fn perform_handshake(
|
||||
mut self,
|
||||
mut handshake_state: HandshakeState,
|
||||
payload: impl AsRef<[u8]>,
|
||||
version: NoiseVersion,
|
||||
pattern: NoisePattern,
|
||||
) -> Result<NoiseStream<C>, NoiseError>
|
||||
@@ -307,7 +219,7 @@ impl<C> NoiseStreamBuilder<C> {
|
||||
{
|
||||
while !handshake_state.is_handshake_finished() {
|
||||
if handshake_state.is_my_turn() {
|
||||
self.send_handshake_msg(&mut handshake_state, &payload, version, pattern)
|
||||
self.send_handshake_msg(&mut handshake_state, version, pattern)
|
||||
.await?;
|
||||
} else {
|
||||
self.recv_handshake_msg(&mut handshake_state, version, pattern)
|
||||
@@ -607,26 +519,24 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn noise_handshake_v1() -> anyhow::Result<()> {
|
||||
async fn noise_handshake() -> anyhow::Result<()> {
|
||||
let dummy_seed = [42u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
|
||||
let initiator_keys = Arc::new(x25519::KeyPair::new(&mut rng));
|
||||
let responder_keys = Arc::new(x25519::KeyPair::new(&mut rng));
|
||||
|
||||
let no_key: Option<[u8; 0]> = None;
|
||||
|
||||
let (initiator_stream, responder_stream) = mock_streams();
|
||||
|
||||
let psk = generate_psk(*responder_keys.public_key(), NoiseVersion::V1)?;
|
||||
let pattern = NoisePattern::default();
|
||||
|
||||
let stream_initiator = NoiseStreamBuilder::new(initiator_stream)
|
||||
.perform_initiator_handshake_inner(
|
||||
pattern,
|
||||
initiator_keys.private_key().to_bytes(),
|
||||
*responder_keys.public_key(),
|
||||
no_key,
|
||||
no_key,
|
||||
responder_keys.public_key().to_bytes(),
|
||||
psk,
|
||||
NoiseVersion::V1,
|
||||
);
|
||||
|
||||
@@ -635,75 +545,6 @@ mod tests {
|
||||
pattern,
|
||||
responder_keys.private_key().to_bytes(),
|
||||
*responder_keys.public_key(),
|
||||
None,
|
||||
);
|
||||
|
||||
let initiator_fut =
|
||||
tokio::spawn(
|
||||
async move { timeout(Duration::from_millis(200), stream_initiator).await },
|
||||
);
|
||||
let responder_fut =
|
||||
tokio::spawn(
|
||||
async move { timeout(Duration::from_millis(200), stream_responder).await },
|
||||
);
|
||||
|
||||
let (initiator, responder) = join!(initiator_fut, responder_fut);
|
||||
|
||||
let mut initiator = initiator???;
|
||||
let mut responder = responder???;
|
||||
|
||||
let msg = b"hello there";
|
||||
// if noise was successful we should be able to write a proper message across
|
||||
timeout(Duration::from_millis(200), initiator.write_all(msg)).await??;
|
||||
|
||||
initiator.inner_stream.flush().await?;
|
||||
|
||||
let inner_buf = initiator.inner_stream.get_ref().unchecked_tx_data();
|
||||
|
||||
let mut buf = [0u8; 11];
|
||||
timeout(Duration::from_millis(200), responder.read(&mut buf)).await??;
|
||||
|
||||
assert_eq!(&buf[..], msg);
|
||||
|
||||
// the inner content is different from the actual msg since it was encrypted
|
||||
assert_ne!(inner_buf, buf);
|
||||
assert_ne!(inner_buf.len(), msg.len());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
async fn noise_handshake_v2() -> anyhow::Result<()> {
|
||||
let dummy_seed = [42u8; 32];
|
||||
let mut rng = rand_chacha::ChaCha20Rng::from_seed(dummy_seed);
|
||||
|
||||
let initiator_keys = Arc::new(x25519::KeyPair::new(&mut rng));
|
||||
let initator_identity_keys = Arc::new(ed25519::KeyPair::new(&mut rng));
|
||||
let responder_keys = Arc::new(x25519::KeyPair::new(&mut rng));
|
||||
|
||||
let init_verif_key = initator_identity_keys.public_key().clone();
|
||||
|
||||
let (initiator_stream, responder_stream) = mock_streams();
|
||||
|
||||
let pattern = NoisePattern::default();
|
||||
|
||||
let stream_initiator = NoiseStreamBuilder::new(initiator_stream)
|
||||
.perform_initiator_handshake_inner(
|
||||
pattern,
|
||||
initiator_keys.private_key().to_bytes(),
|
||||
responder_keys.public_key().to_bytes(),
|
||||
Some(initator_identity_keys.private_key().to_bytes()),
|
||||
Some(initator_identity_keys.public_key().to_bytes()),
|
||||
NoiseVersion::V2,
|
||||
);
|
||||
|
||||
let stream_responder = NoiseStreamBuilder::new(responder_stream)
|
||||
.perform_responder_handshake_inner(
|
||||
pattern,
|
||||
responder_keys.private_key().to_bytes(),
|
||||
*responder_keys.public_key(),
|
||||
Some(init_verif_key),
|
||||
);
|
||||
|
||||
let initiator_fut =
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
[package]
|
||||
name = "nympsq"
|
||||
version = "0.1.0"
|
||||
authors = ["Georgio Nicolas <georgio@nymtech.net>"]
|
||||
edition = "2021"
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
thiserror = { workspace = true }
|
||||
|
||||
# internal
|
||||
nym-crypto = { path = "../crypto", features = ["asymmetric", "serde"]}
|
||||
|
||||
libcrux-traits = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-kem = { git = "https://github.com/cryspen/libcrux" }
|
||||
libcrux-psq = { git = "https://github.com/cryspen/libcrux", features = ["test-utils"] }
|
||||
libcrux-ed25519 = { git = "https://github.com/cryspen/libcrux" }
|
||||
|
||||
rand = "0.9.2"
|
||||
tls_codec = { version = "0.4.2", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = {workspace = true}
|
||||
|
||||
[[bench]]
|
||||
name = "benches"
|
||||
harness = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -1,47 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use rand::prelude::*;
|
||||
|
||||
pub fn gen_ed25519_keypair(c: &mut Criterion) {
|
||||
c.bench_function("Generate Ed25519 Keypair", |b| {
|
||||
b.iter(|| {
|
||||
let mut s: [u8; 32] = [0u8; 32];
|
||||
rand::rng().fill_bytes(&mut s);
|
||||
ed25519::KeyPair::from_secret(s, 0)
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn gen_mlkem768_keypair(c: &mut Criterion) {
|
||||
c.bench_function("Generate MlKem768 Keypair", |b| {
|
||||
b.iter(|| {
|
||||
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rand::rng()).unwrap()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn gen_x25519_keypair(c: &mut Criterion) {
|
||||
c.bench_function("Generate DHKem Keypair", |b| {
|
||||
b.iter(|| libcrux_kem::key_gen(libcrux_kem::Algorithm::X25519, &mut rand::rng()).unwrap());
|
||||
});
|
||||
}
|
||||
|
||||
pub fn gen_xwing_keypair(c: &mut Criterion) {
|
||||
c.bench_function("Generate XWingKem Keypair", |b| {
|
||||
b.iter(|| {
|
||||
libcrux_kem::key_gen(libcrux_kem::Algorithm::XWingKemDraft06, &mut rand::rng()).unwrap()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
gen_ed25519_keypair,
|
||||
gen_mlkem768_keypair,
|
||||
gen_x25519_keypair,
|
||||
gen_xwing_keypair
|
||||
);
|
||||
criterion_main!(benches);
|
||||
@@ -1,58 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::io;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum PSQError {
|
||||
#[error("encountered a decryption error")]
|
||||
DecryptionError,
|
||||
|
||||
#[error("encountered a KEM error")]
|
||||
KEMError,
|
||||
|
||||
#[error("encountered a PSQ error")]
|
||||
PSQError,
|
||||
|
||||
#[error("encountered a Serialization/Deserialization error")]
|
||||
SerializationError,
|
||||
|
||||
#[error("encountered an IO error: {0}")]
|
||||
IoError(#[from] io::Error),
|
||||
|
||||
#[error("Incorrect state")]
|
||||
IncorrectStateError,
|
||||
|
||||
#[error("Handshake did not complete")]
|
||||
HandshakeError,
|
||||
|
||||
#[error("Unknown PSQ version")]
|
||||
UnknownVersion,
|
||||
}
|
||||
|
||||
impl From<libcrux_kem::Error> for PSQError {
|
||||
fn from(err: libcrux_kem::Error) -> Self {
|
||||
match err {
|
||||
// Error::Decrypt => PSQError::DecryptionError,
|
||||
_err => PSQError::KEMError,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<libcrux_psq::Error> for PSQError {
|
||||
fn from(err: libcrux_psq::Error) -> Self {
|
||||
match err {
|
||||
// Error::Decrypt => PSQError::DecryptionError,
|
||||
_err => PSQError::PSQError,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<tls_codec::Error> for PSQError {
|
||||
fn from(err: tls_codec::Error) -> Self {
|
||||
match err {
|
||||
_err => PSQError::SerializationError,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
pub mod error;
|
||||
pub mod psq;
|
||||
@@ -1,270 +0,0 @@
|
||||
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use std::{io::Cursor, marker::PhantomData, time::Duration};
|
||||
|
||||
use libcrux_ed25519::VerificationKey;
|
||||
use libcrux_psq::{
|
||||
cred::Ed25519,
|
||||
psk_registration::{Initiator, InitiatorMsg, Responder, ResponderMsg},
|
||||
traits::PSQ,
|
||||
};
|
||||
|
||||
use tls_codec::{Deserialize, Serialize};
|
||||
|
||||
use libcrux_traits::kem::KEM;
|
||||
|
||||
use rand::{CryptoRng, RngCore};
|
||||
|
||||
use crate::error::PSQError;
|
||||
|
||||
pub const PSK_LENGTH: usize = 32;
|
||||
pub const CONTEXT_LEN: usize = 16;
|
||||
pub const PSK_HANDLE_LEN: usize = 16;
|
||||
|
||||
pub struct PSQInitiator<T: PSQ> {
|
||||
signing_key: [u8; 32],
|
||||
verification_key: VerificationKey,
|
||||
state: Option<Initiator>,
|
||||
_t: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<T: PSQ> PSQInitiator<T> {
|
||||
pub fn init(signing_key: impl AsRef<[u8]>, verification_key: impl AsRef<[u8]>) -> Self {
|
||||
let mut sig_key: [u8; 32] = [0u8; 32];
|
||||
sig_key.clone_from_slice(signing_key.as_ref());
|
||||
|
||||
let mut verif_key: [u8; 32] = [0u8; 32];
|
||||
verif_key.clone_from_slice(verification_key.as_ref());
|
||||
|
||||
Self {
|
||||
signing_key: sig_key,
|
||||
verification_key: VerificationKey::from_bytes(verif_key),
|
||||
state: None,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute_initiator_message<R>(
|
||||
&mut self,
|
||||
rng: &mut R,
|
||||
responder_kem_public_key: &<T::InnerKEM as KEM>::EncapsulationKey,
|
||||
context: &[u8; CONTEXT_LEN],
|
||||
psk_ttl: Duration,
|
||||
) -> Result<Vec<u8>, PSQError>
|
||||
where
|
||||
<T as PSQ>::InnerKEM: KEM,
|
||||
R: RngCore + CryptoRng,
|
||||
{
|
||||
let (state, message) = Initiator::send_initial_message::<Ed25519, T>(
|
||||
context,
|
||||
psk_ttl,
|
||||
responder_kem_public_key,
|
||||
&self.signing_key,
|
||||
&self.verification_key,
|
||||
rng,
|
||||
)?;
|
||||
self.state = Some(state);
|
||||
|
||||
Ok(message.tls_serialize_detached()?)
|
||||
}
|
||||
|
||||
// The initator generates the PSK themselves while computing the first message.
|
||||
// It's possible to get_psk() this after sending the first message.
|
||||
// The key will only be valid if we're sure that the responder has received it.
|
||||
//
|
||||
// If we use noise right after PSQ, we can just extract the PSK and expect the responder
|
||||
// to just send the first Noise message instead of responding to with a PSQ message.
|
||||
// In other words, instead of using the responder's PSQ message for key confirmation,
|
||||
// we could use a Noise handshake for key confirmation.
|
||||
|
||||
pub fn get_psk(&self) -> Option<[u8; 32]> {
|
||||
match &self.state {
|
||||
Some(initiator_state) => Some(*initiator_state.unregistered_psk()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn finalize(&self, responder_message: &[u8]) -> Result<[u8; PSK_LENGTH], PSQError> {
|
||||
match &self.state {
|
||||
Some(state) => match ResponderMsg::tls_deserialize_exact(responder_message) {
|
||||
Ok(deserialized_responder_message) => {
|
||||
match state.complete_handshake(&deserialized_responder_message) {
|
||||
Ok(psk) => Ok(psk.psk),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
Err(err) => Err(err.into()),
|
||||
},
|
||||
|
||||
None => Err(PSQError::IncorrectStateError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PSQResponder<'a, T: PSQ>
|
||||
where
|
||||
<T as PSQ>::InnerKEM: KEM,
|
||||
{
|
||||
kem_private_key: &'a <T::InnerKEM as KEM>::DecapsulationKey,
|
||||
kem_public_key: &'a <T::InnerKEM as KEM>::EncapsulationKey,
|
||||
_t: PhantomData<T>,
|
||||
}
|
||||
impl<'a, T: PSQ> PSQResponder<'a, T>
|
||||
where
|
||||
<T as PSQ>::InnerKEM: KEM,
|
||||
{
|
||||
pub fn init(
|
||||
kem_private_key: &'a <T::InnerKEM as KEM>::DecapsulationKey,
|
||||
kem_public_key: &'a <T::InnerKEM as KEM>::EncapsulationKey,
|
||||
) -> Self {
|
||||
Self {
|
||||
kem_private_key,
|
||||
kem_public_key,
|
||||
_t: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compute_responder_message(
|
||||
&self,
|
||||
initiator_verification_key: &VerificationKey,
|
||||
initiator_message: &mut [u8],
|
||||
context: &[u8; CONTEXT_LEN],
|
||||
psk_ttl: Duration,
|
||||
psk_handle: &[u8; PSK_HANDLE_LEN],
|
||||
) -> Result<([u8; PSK_LENGTH], Vec<u8>), PSQError> {
|
||||
let mut cursor = Cursor::new(initiator_message);
|
||||
let deserialized_initiator_message = InitiatorMsg::tls_deserialize(&mut cursor)?;
|
||||
|
||||
// the problem is here the deserialized message is the same on the initiator's side
|
||||
let (registered_psk, responder_msg) = Responder::send::<Ed25519, T>(
|
||||
psk_handle,
|
||||
psk_ttl,
|
||||
context,
|
||||
self.kem_public_key,
|
||||
self.kem_private_key,
|
||||
initiator_verification_key,
|
||||
&deserialized_initiator_message,
|
||||
)?;
|
||||
|
||||
let responder_bytes = responder_msg.tls_serialize_detached()?;
|
||||
Ok((registered_psk.psk, responder_bytes))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::time::Duration;
|
||||
|
||||
use libcrux_ed25519::VerificationKey;
|
||||
use libcrux_psq::impls::{MlKem768, XWingKemDraft06, X25519};
|
||||
use libcrux_psq::traits::PSQ;
|
||||
use libcrux_traits::kem::KEM;
|
||||
use nym_crypto::asymmetric::ed25519;
|
||||
use rand::prelude::*;
|
||||
|
||||
use crate::psq::{CONTEXT_LEN, PSK_HANDLE_LEN};
|
||||
|
||||
use super::{PSQInitiator, PSQResponder};
|
||||
|
||||
fn test_helper<T: PSQ, R>(
|
||||
rng: &mut R,
|
||||
responder_kem_private_key: <T::InnerKEM as KEM>::DecapsulationKey,
|
||||
responder_kem_public_key: <T::InnerKEM as KEM>::EncapsulationKey,
|
||||
) where
|
||||
R: CryptoRng + rand::RngCore,
|
||||
{
|
||||
// set PSQ TTL
|
||||
let psk_ttl: Duration = Duration::from_secs(3600);
|
||||
|
||||
// generate random context string
|
||||
let mut context: [u8; CONTEXT_LEN] = [0u8; CONTEXT_LEN];
|
||||
rng.fill_bytes(&mut context);
|
||||
|
||||
// generate random psk handle
|
||||
let mut psk_handle: [u8; PSK_HANDLE_LEN] = [0u8; PSK_HANDLE_LEN];
|
||||
rng.fill_bytes(&mut psk_handle);
|
||||
|
||||
// generate ed25519 keys
|
||||
let mut secret_initiator: [u8; 32] = [0u8; 32];
|
||||
rng.fill_bytes(&mut secret_initiator);
|
||||
let initiator_ed25519_keypair = ed25519::KeyPair::from_secret(secret_initiator, 0);
|
||||
|
||||
let mut initiator: PSQInitiator<T> = PSQInitiator::init(
|
||||
initiator_ed25519_keypair.private_key().to_bytes(),
|
||||
initiator_ed25519_keypair.public_key().to_bytes(),
|
||||
);
|
||||
let responder: PSQResponder<T> =
|
||||
PSQResponder::init(&responder_kem_private_key, &responder_kem_public_key);
|
||||
|
||||
let mut initiator_msg = initiator
|
||||
.compute_initiator_message(rng, &responder_kem_public_key, &context, psk_ttl)
|
||||
.unwrap();
|
||||
|
||||
let pre_response_psk = initiator.get_psk().unwrap();
|
||||
|
||||
let initiator_public_key =
|
||||
VerificationKey::from_bytes(initiator_ed25519_keypair.public_key().to_bytes());
|
||||
|
||||
let (responder_psk, responder_msg) = responder
|
||||
.compute_responder_message(
|
||||
&initiator_public_key,
|
||||
&mut initiator_msg,
|
||||
&context,
|
||||
psk_ttl,
|
||||
&psk_handle,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let initiator_psk = initiator.finalize(&responder_msg).unwrap();
|
||||
|
||||
assert_eq!(initiator_psk, pre_response_psk);
|
||||
assert_eq!(initiator_psk, responder_psk);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psq_e2e_mlkem() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// generate mlkem keypair
|
||||
let (responder_kem_private_key, responder_kem_public_key) =
|
||||
libcrux_kem::key_gen(libcrux_kem::Algorithm::MlKem768, &mut rng).unwrap();
|
||||
|
||||
test_helper::<MlKem768, _>(
|
||||
&mut rng,
|
||||
responder_kem_private_key,
|
||||
responder_kem_public_key,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psq_e2e_xwing() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// generate xwing keypair
|
||||
let (responder_kem_private_key, responder_kem_public_key) =
|
||||
libcrux_kem::key_gen(libcrux_kem::Algorithm::XWingKemDraft06, &mut rand::rng())
|
||||
.unwrap();
|
||||
|
||||
test_helper::<XWingKemDraft06, _>(
|
||||
&mut rng,
|
||||
responder_kem_private_key,
|
||||
responder_kem_public_key,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psq_e2e_dhkem() {
|
||||
let mut rng = rand::rng();
|
||||
|
||||
// generate dhkem keypair
|
||||
let (responder_kem_private_key, responder_kem_public_key) =
|
||||
libcrux_kem::key_gen(libcrux_kem::Algorithm::X25519, &mut rand::rng()).unwrap();
|
||||
|
||||
test_helper::<X25519, _>(
|
||||
&mut rng,
|
||||
responder_kem_private_key,
|
||||
responder_kem_public_key,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -13,10 +13,12 @@ use nym_gateway_storage::models::WireguardPeer;
|
||||
use nym_task::TaskClient;
|
||||
use nym_wireguard_types::DEFAULT_PEER_TIMEOUT_CHECK;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, SystemTime};
|
||||
use tokio::sync::{mpsc, RwLock};
|
||||
use tokio_stream::{wrappers::IntervalStream, StreamExt};
|
||||
|
||||
pub(crate) type SharedBandwidthStorageManager = Arc<RwLock<BandwidthStorageManager>>;
|
||||
const AUTO_REMOVE_AFTER: Duration = Duration::from_secs(60 * 60); // 1 hour
|
||||
|
||||
pub struct PeerHandle {
|
||||
public_key: Key,
|
||||
@@ -26,6 +28,7 @@ pub struct PeerHandle {
|
||||
request_tx: mpsc::Sender<PeerControlRequest>,
|
||||
timeout_check_interval: IntervalStream,
|
||||
task_client: TaskClient,
|
||||
startup_timestamp: SystemTime,
|
||||
}
|
||||
|
||||
impl PeerHandle {
|
||||
@@ -50,6 +53,7 @@ impl PeerHandle {
|
||||
request_tx,
|
||||
timeout_check_interval,
|
||||
task_client,
|
||||
startup_timestamp: SystemTime::now(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,49 +73,26 @@ impl PeerHandle {
|
||||
Ok(success)
|
||||
}
|
||||
|
||||
fn compute_spent_bandwidth(kernel_peer: &Peer, storage_peer: &WireguardPeer) -> Option<u64> {
|
||||
let storage_peer_rx_bytes = u64::try_from(storage_peer.rx_bytes)
|
||||
.inspect_err(|e| tracing::error!("Storage rx bytes could not be converted: {e}"))
|
||||
.ok()?;
|
||||
let storage_peer_tx_bytes = u64::try_from(storage_peer.tx_bytes)
|
||||
.inspect_err(|e| tracing::error!("Storage tx bytes could not be converted: {e}"))
|
||||
.ok()?;
|
||||
|
||||
let kernel_total = kernel_peer
|
||||
.rx_bytes
|
||||
.checked_add(kernel_peer.tx_bytes)
|
||||
.or_else(|| {
|
||||
tracing::error!(
|
||||
"Overflow on kernel adding bytes: {} + {}",
|
||||
kernel_peer.rx_bytes,
|
||||
kernel_peer.tx_bytes
|
||||
);
|
||||
None
|
||||
})?;
|
||||
let storage_total = storage_peer_rx_bytes
|
||||
.checked_add(storage_peer_tx_bytes)
|
||||
.or_else(|| {
|
||||
tracing::error!("Overflow on storage adding bytes: {storage_peer_rx_bytes} + {storage_peer_tx_bytes}");
|
||||
None
|
||||
})?;
|
||||
|
||||
kernel_total.checked_sub(storage_total).or_else(|| {
|
||||
tracing::error!("Overflow on spent bandwidth subtraction: kernel - storage = {kernel_total} - {storage_total}");
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
async fn active_peer(&mut self, kernel_peer: &Peer) -> Result<bool, Error> {
|
||||
let Some(storage_peer) = self.peer_storage_manager.get_peer() else {
|
||||
log::debug!(
|
||||
"Peer {:?} not in storage anymore, shutting down handle",
|
||||
self.public_key
|
||||
);
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
async fn active_peer(
|
||||
&mut self,
|
||||
storage_peer: &WireguardPeer,
|
||||
kernel_peer: &Peer,
|
||||
) -> Result<bool, Error> {
|
||||
if let Some(bandwidth_manager) = &self.bandwidth_storage_manager {
|
||||
let spent_bandwidth = Self::compute_spent_bandwidth(kernel_peer, &storage_peer)
|
||||
if kernel_peer.last_handshake.is_none()
|
||||
&& SystemTime::now().duration_since(self.startup_timestamp)? >= AUTO_REMOVE_AFTER
|
||||
{
|
||||
let success = self.remove_peer().await?;
|
||||
self.peer_storage_manager.remove_peer();
|
||||
tracing::debug!(
|
||||
"Peer {} has not been active for more then {} seconds, removing it",
|
||||
kernel_peer.public_key.to_string(),
|
||||
AUTO_REMOVE_AFTER.as_secs()
|
||||
);
|
||||
return Ok(!success);
|
||||
}
|
||||
let spent_bandwidth = (kernel_peer.rx_bytes + kernel_peer.tx_bytes)
|
||||
.checked_sub(storage_peer.rx_bytes as u64 + storage_peer.tx_bytes as u64)
|
||||
.unwrap_or_else(|| {
|
||||
// if gateway restarted, the kernel values restart from 0
|
||||
// and we should restart from 0 in storage as well
|
||||
@@ -119,10 +100,8 @@ impl PeerHandle {
|
||||
self.peer_storage_manager.peer_information.as_mut()
|
||||
{
|
||||
peer_information.force_sync = true;
|
||||
peer_information.peer.rx_bytes = kernel_peer.rx_bytes;
|
||||
peer_information.peer.tx_bytes = kernel_peer.tx_bytes;
|
||||
}
|
||||
0
|
||||
kernel_peer.rx_bytes + kernel_peer.tx_bytes
|
||||
})
|
||||
.try_into()
|
||||
.map_err(|_| Error::InconsistentConsumedBytes)?;
|
||||
@@ -145,6 +124,14 @@ impl PeerHandle {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if SystemTime::now().duration_since(self.startup_timestamp)? >= AUTO_REMOVE_AFTER {
|
||||
log::debug!(
|
||||
"Peer {} has been present for 30 days, removing it",
|
||||
self.public_key
|
||||
);
|
||||
let success = self.remove_peer().await?;
|
||||
return Ok(!success);
|
||||
}
|
||||
let spent_bandwidth = kernel_peer.rx_bytes + kernel_peer.tx_bytes;
|
||||
if spent_bandwidth >= BANDWIDTH_CAP_PER_DAY {
|
||||
log::debug!(
|
||||
@@ -171,7 +158,14 @@ impl PeerHandle {
|
||||
// the host information hasn't beed updated yet
|
||||
return Ok(true);
|
||||
};
|
||||
if !self.active_peer(&kernel_peer).await? {
|
||||
let Some(storage_peer) = self.peer_storage_manager.get_peer() else {
|
||||
log::debug!(
|
||||
"Peer {:?} not in storage anymore, shutting down handle",
|
||||
self.public_key
|
||||
);
|
||||
return Ok(false);
|
||||
};
|
||||
if !self.active_peer(&storage_peer, &kernel_peer).await? {
|
||||
log::debug!(
|
||||
"Peer {:?} is not active anymore, shutting down handle",
|
||||
self.public_key
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
version: "3.7"
|
||||
|
||||
x-network: &NETWORK
|
||||
BECH32_PREFIX: nymt
|
||||
DENOM: nymt
|
||||
STAKE_DENOM: nyxt
|
||||
WASMD_VERSION: v0.27.0
|
||||
|
||||
services:
|
||||
genesis_validator:
|
||||
build:
|
||||
context: docker/validator
|
||||
args: *NETWORK
|
||||
image: validator:latest
|
||||
ports:
|
||||
- "26657:26657"
|
||||
- "1317:1317"
|
||||
container_name: genesis_validator
|
||||
volumes:
|
||||
- "genesis_volume:/genesis_volume"
|
||||
- "genesis_nyxd:/root/.nyxd"
|
||||
environment: *NETWORK
|
||||
networks:
|
||||
localnet:
|
||||
ipv4_address: 172.168.10.2
|
||||
command: ["genesis"]
|
||||
secondary_validator:
|
||||
build:
|
||||
context: docker/validator
|
||||
args: *NETWORK
|
||||
image: validator:latest
|
||||
ports:
|
||||
- "36657:26657"
|
||||
- "2317:1317"
|
||||
volumes:
|
||||
- "genesis_volume:/genesis_volume"
|
||||
- "secondary_nyxd:/root/.nyxd"
|
||||
environment: *NETWORK
|
||||
networks:
|
||||
localnet:
|
||||
ipv4_address: 172.168.10.3
|
||||
depends_on:
|
||||
- "genesis_validator"
|
||||
command: ["secondary"]
|
||||
# mixnet_contract:
|
||||
# build: docker/mixnet_contract
|
||||
# image: contract:latest
|
||||
# volumes:
|
||||
# - ".:/nym"
|
||||
# vesting_contract:
|
||||
# build: docker/vesting_contract
|
||||
# image: vesting_contract:latest
|
||||
# volumes:
|
||||
# - ".:/nym"
|
||||
# contract_uploader:
|
||||
# build: docker/typescript_client
|
||||
# image: contract_uploader:typescript
|
||||
# volumes:
|
||||
# - "genesis_volume:/genesis_volume:ro"
|
||||
# - "contract_volume:/contract_volume"
|
||||
# - ".:/nym"
|
||||
# depends_on:
|
||||
# - "genesis_validator"
|
||||
# - "secondary_validator"
|
||||
# - "mixnet_contract"
|
||||
# environment:
|
||||
# BECH32_PREFIX: *BECH32_PREFIX
|
||||
mnemonic_echo:
|
||||
build: docker/mnemonic_echo
|
||||
image: mnemonic_echo:latest
|
||||
volumes:
|
||||
- "genesis_volume:/genesis_volume:ro"
|
||||
depends_on:
|
||||
- "genesis_validator"
|
||||
- "secondary_validator"
|
||||
|
||||
# mongo:
|
||||
# image: mongo:latest
|
||||
# command:
|
||||
# - --storageEngine=wiredTiger
|
||||
# volumes:
|
||||
# - mongo_data:/data/db
|
||||
# block_explorer:
|
||||
# build:
|
||||
# context: https://github.com/forbole/big-dipper.git#v0.41.x-7
|
||||
# image: block_explorer:v0.41.x-7
|
||||
# ports:
|
||||
# - "3080:3000"
|
||||
# depends_on:
|
||||
# - "mongo"
|
||||
# environment:
|
||||
# ROOT_URL: ${APP_ROOT_URL:-http://localhost}
|
||||
# MONGO_URL: mongodb://mongo:27017/meteor
|
||||
# PORT: 3000
|
||||
# METEOR_SETTINGS: ${METEOR_SETTINGS}
|
||||
# explorer:
|
||||
# build:
|
||||
# context: docker/explorer
|
||||
# image: explorer:latest
|
||||
# ports:
|
||||
# - "3040:3000"
|
||||
# depends_on:
|
||||
# - "genesis_validator"
|
||||
# - "block_explorer"
|
||||
|
||||
# service to update geoip binary database, for explorer-api
|
||||
geoipupdate:
|
||||
container_name: geoipupdate
|
||||
image: maxmindinc/geoipupdate
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
GEOIPUPDATE_ACCOUNT_ID: ${GEOIPUPDATE_ACCOUNT_ID}
|
||||
GEOIPUPDATE_LICENSE_KEY: ${GEOIPUPDATE_LICENSE_KEY}
|
||||
GEOIPUPDATE_EDITION_IDS: ${GEOIPUPDATE_EDITION_IDS}
|
||||
GEOIPUPDATE_FREQUENCY: ${GEOIPUPDATE_FREQUENCY}
|
||||
networks:
|
||||
- geoipupdate
|
||||
volumes:
|
||||
- ${GEOIP_DB_DIRECTORY}:/usr/share/GeoIP
|
||||
|
||||
volumes:
|
||||
genesis_volume:
|
||||
genesis_nyxd:
|
||||
secondary_nyxd:
|
||||
|
||||
# contract_volume:
|
||||
# mongo_data:
|
||||
|
||||
networks:
|
||||
geoipupdate:
|
||||
localnet:
|
||||
driver: bridge
|
||||
ipam:
|
||||
driver: default
|
||||
config:
|
||||
- subnet: 172.168.10.0/25
|
||||
@@ -1 +1 @@
|
||||
Thursday, June 12th 2025, 11:03:30 UTC
|
||||
Friday, June 6th 2025, 09:33:10 UTC
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
"chain-registry": "^1.19.0",
|
||||
"cosmjs-types": "^0.9.0",
|
||||
"lucide-react": "^0.438.0",
|
||||
"next": "^15.2.4",
|
||||
"next": "^14.2.15",
|
||||
"nextra": "2",
|
||||
"nextra-theme-docs": "2",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -47,53 +47,6 @@ This page displays a full list of all the changes during our release cycle from
|
||||
|
||||
<VarInfo />
|
||||
|
||||
## `v2025.11-cheddar`
|
||||
|
||||
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.11-cheddar)
|
||||
- [`nym-node`](nodes/nym-node.mdx) version `1.13.0`
|
||||
|
||||
```sh
|
||||
nym-node
|
||||
Binary Name: nym-node
|
||||
Build Timestamp: 2025-06-10T09:41:31.291089877Z
|
||||
Build Version: 1.13.0
|
||||
Commit SHA: ef220882d4bcf0a8a703ea62f9273e017c9ebb51
|
||||
Commit Date: 2025-06-10T11:39:20.000000000+02:00
|
||||
Commit Branch: HEAD
|
||||
rustc Version: 1.86.0
|
||||
rustc Channel: stable
|
||||
cargo Profile: release
|
||||
```
|
||||
### Operators Updates & Tools
|
||||
|
||||
- **Nym Improvement Proposals**: NIPs are being introduced, allowing us to propose changes to the Nym network and ecosystem, and decentralise governance
|
||||
- **NIP1** will outline the governance process: Its draft will be published soon for discussion
|
||||
- **NIP2** will be about reducing the stake saturation point variable, which would make reward distribution across the network more equitable
|
||||
- **Join the Operator AMA next Tuesday** for an in-depth overview of what this all means (feat Nym Chief Scientist Claudia Diaz)
|
||||
- Keep an eye on our channels for information about how to participate in the upcoming voting (no need for forum registration, we are building something new!)
|
||||
|
||||
### Features
|
||||
|
||||
- [Track wireguard credential retries](https://github.com/nymtech/nym/pull/5783)
|
||||
|
||||
- [Swap a decode into a `fromrow` to please future `postgres` feature](https://github.com/nymtech/nym/pull/5785): This PR change a sqlx derive from `Decode` to `FromRow` because a future PR will bring the `postgres` feature and it doesn't like that `Decode` there
|
||||
|
||||
- [Fix contains ticketbook function that always returned true](https://github.com/nymtech/nym/pull/5787)
|
||||
|
||||
- [QoL: `RequestPath` trait for `http-api-client`](https://github.com/nymtech/nym/pull/5788): Those `PathSegments` in `ApiClient` weren't very user-friendly. Instead we've defined a `RequestPath` trait that allows you to also pass a good old `&str` or `String` and internally it does exactly the same sanitization as before. `PathSegments` are also fully supported to not break any existing compatibility.
|
||||
|
||||
- [Nym Statistics API](https://github.com/nymtech/nym/pull/5800):
|
||||
- Types for `nym-vpn-client`: It introduces `VpnClientStatsReport` which will be used by nym vpn clients to report statistics. Those types are in the monorepo because they're used by the `num-statistics-API`
|
||||
- Nym Statistics API: Basically a `REST API` with a `postgres` backend. In this first iteration, it only accepts `VpnClientStatsReport` and stores them in its database. It optionally connects to the Nym API to confirm reports are coming from the network and attach country code to them if it's the case (exit node country code).
|
||||
|
||||
- [Resolve 1.87 clippy warnings](https://github.com/nymtech/nym/pull/5802)
|
||||
|
||||
- [Adjusted wallet storybook mocks to fix the build](https://github.com/nymtech/nym/pull/5804)
|
||||
|
||||
- [Set cached storage counters to 0](https://github.com/nymtech/nym/pull/5812)
|
||||
|
||||
- [No autoremoval of peers](https://github.com/nymtech/nym/pull/5831)
|
||||
|
||||
## `v2025.10-brie`
|
||||
|
||||
- [Release Binaries](https://github.com/nymtech/nym/releases/tag/nym-binaries-v2025.10-brie)
|
||||
|
||||
@@ -18,12 +18,12 @@ This documentation page provides a guide on how to set up and run a [NYM NODE](.
|
||||
## Current version
|
||||
|
||||
```sh
|
||||
ym-node
|
||||
nym-node
|
||||
Binary Name: nym-node
|
||||
Build Timestamp: 2025-06-10T09:41:31.291089877Z
|
||||
Build Version: 1.13.0
|
||||
Commit SHA: ef220882d4bcf0a8a703ea62f9273e017c9ebb51
|
||||
Commit Date: 2025-06-10T11:39:20.000000000+02:00
|
||||
Build Timestamp: 2025-05-27T10:13:18.511075453Z
|
||||
Build Version: 1.12.0
|
||||
Commit SHA: 1c6db86259d08d80e8bcfbc4fcc71ccb147fcfd0
|
||||
Commit Date: 2025-05-27T12:11:13.000000000+02:00
|
||||
Commit Branch: HEAD
|
||||
rustc Version: 1.86.0
|
||||
rustc Channel: stable
|
||||
|
||||
Generated
+500
-1631
File diff suppressed because it is too large
Load Diff
+25
@@ -0,0 +1,25 @@
|
||||
CONFIGURED=true
|
||||
|
||||
RUST_LOG=info
|
||||
RUST_BACKTRACE=1
|
||||
NETWORK_NAME=qa
|
||||
BECH32_PREFIX=n
|
||||
MIX_DENOM=unym
|
||||
MIX_DENOM_DISPLAY=nym
|
||||
STAKE_DENOM=unyx
|
||||
STAKE_DENOM_DISPLAY=nyx
|
||||
DENOMS_EXPONENT=6
|
||||
|
||||
MIXNET_CONTRACT_ADDRESS=n1hm4y6fzgxgu688jgf7ek66px6xkrtmn3gyk8fax3eawhp68c2d5qujz296
|
||||
ECASH_CONTRACT_ADDRESS=n13xspq62y9gq6nueqmywxcdv2yep4p6nzv98w2889k25v3nhdy2dq2rkrk7
|
||||
GROUP_CONTRACT_ADDRESS=n13l7rwuwktklrwskc7m6lv70zws07en85uma28j7dxwsz9y5hvvhspl7a2t
|
||||
MULTISIG_CONTRACT_ADDRESS=n138c9pyf7f3hyx0j3t6vmsz7ultnw2wj0lu6hzndep9z5grgq9haqlc25k0
|
||||
COCONUT_DKG_CONTRACT_ADDRESS=n1pk8jgr6y4c5k93gz7qf3xc0hvygmp7csk88c2tf8l39tkq6834wq2a6dtr
|
||||
VESTING_CONTRACT_ADDRESS=n1jlzdxnyces4hrhqz68dqk28mrw5jgwtcfq0c2funcwrmw0dx9l9s8nnnvj
|
||||
REWARDING_VALIDATOR_ADDRESS=n1rfvpsynktze6wvn6ldskj8xgwfzzk5v6pnff39
|
||||
|
||||
EXPLORER_API=https://qa-network-explorer.qa.nymte.ch/api/
|
||||
NYXD=https://rpc.qa-validator.qa.nymte.ch
|
||||
NYXD_WS=wss://rpc.qa-validator.qa.nymte.ch/websocket
|
||||
NYM_API=https://qa-nym-api.qa.nymte.ch/api/
|
||||
NYM_VPN_API=https://nym-vpn-api-git-deploy-qa-nyx-network-staging.vercel.app/api/
|
||||
@@ -0,0 +1,21 @@
|
||||
[package]
|
||||
name = "nym-bity-integration"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.56"
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
k256 = { workspace = true, features = ["ecdsa", "sha256"] }
|
||||
eyre = { workspace = true }
|
||||
|
||||
cosmrs = { workspace = true }
|
||||
|
||||
nym-cli-commands = { path = "../../common/commands" }
|
||||
nym-validator-client = { path = "../../common/client-libs/validator-client" }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = { workspace = true }
|
||||
@@ -0,0 +1,53 @@
|
||||
# Buy NYM with Bity
|
||||
|
||||
This crate allows Bity to verify orders for purchasing NYM tokens. The same crate is used by the wallet to sign orders for purchases.
|
||||
|
||||
## Signing
|
||||
|
||||
The Nym Wallet user will sign an order message provided by Bity to create a signed order with the following fields:
|
||||
|
||||
```
|
||||
account_id: n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf
|
||||
message: "This is the order message from Bity"
|
||||
order signature:
|
||||
{
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"public_key": {
|
||||
"@type": "/cosmos.crypto.secp256k1.PubKey",
|
||||
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
|
||||
},
|
||||
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
|
||||
}
|
||||
```
|
||||
|
||||
The `signature` field of the order contains a JSON representation of:
|
||||
|
||||
- the Cosmos address of the signer (`account_id`)
|
||||
- the Cosmos public key
|
||||
- a hex string digest of the Bity order message signed by the user
|
||||
|
||||
Note: the `signature_as_hex` is not in recoverable form (e.g. allows recovering the public key from the signature in `secp256k1`). This is why the public key is supplied along with the account id, as the prefix cannot be recovered.
|
||||
|
||||
## Verification
|
||||
|
||||
Verification has been wrapped up into taking a single struct that can be parsed from JSON:
|
||||
|
||||
```
|
||||
{
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"message": "This is the order message from Bity",
|
||||
"signature": << ORDER SIGNATURE JSON GOES HERE >>
|
||||
}
|
||||
```
|
||||
|
||||
The following will be checked:
|
||||
|
||||
- the `account_id` supplied matches:
|
||||
- the account id derived from the public key
|
||||
- the account id field in the order signature JSON
|
||||
- the account id is for Nym mainnet
|
||||
- the signature is for the message
|
||||
- all data structures parse correctly
|
||||
- nested structs
|
||||
- account ids
|
||||
- Cosmos public keys
|
||||
@@ -0,0 +1,220 @@
|
||||
pub mod order;
|
||||
pub mod sign;
|
||||
pub mod verify;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::order::{Order, OrderSignature};
|
||||
use crate::{sign, verify};
|
||||
use cosmrs::AccountId;
|
||||
use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet;
|
||||
use std::str::FromStr;
|
||||
|
||||
fn get_order(prefix: &str) -> anyhow::Result<(OrderSignature, String)> {
|
||||
let mnemonic = "crush minute paddle tobacco message debate cabin peace bar jacket execute twenty winner view sure mask popular couch penalty fragile demise fresh pizza stove";
|
||||
let wallet = DirectSecp256k1HdWallet::from_mnemonic(prefix, mnemonic.parse()?);
|
||||
|
||||
let accounts = wallet.try_derive_accounts()?;
|
||||
|
||||
let message = "This is the order message from Bity";
|
||||
let signature = sign::sign_order(&wallet, &accounts[0], message.to_string())?;
|
||||
|
||||
Ok((signature, message.to_string()))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_happy_path() -> anyhow::Result<()> {
|
||||
let (signature, message) = get_order("n")?;
|
||||
|
||||
let account_id = AccountId::from_str("n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf").unwrap();
|
||||
assert_eq!(account_id.to_string(), signature.account_id.to_string());
|
||||
|
||||
println!("Order signature:");
|
||||
println!("{}", ::serde_json::to_string_pretty(&signature)?);
|
||||
|
||||
Ok(verify::verify_order(Order {
|
||||
account_id,
|
||||
signature,
|
||||
message,
|
||||
})?)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_fails_on_non_mainnet_address() -> anyhow::Result<()> {
|
||||
let (signature, message) = get_order("nymt")?;
|
||||
|
||||
println!("Order signature:");
|
||||
println!("{}", ::serde_json::to_string_pretty(&signature)?);
|
||||
|
||||
let res = verify::verify_order(Order {
|
||||
account_id: signature.clone().account_id,
|
||||
signature,
|
||||
message,
|
||||
});
|
||||
|
||||
println!("Expecting error, got: {:?}", res);
|
||||
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_fails_on_non_mainnet_address_variant2() -> anyhow::Result<()> {
|
||||
let (signature, message) = get_order("atom")?;
|
||||
|
||||
println!("Order signature:");
|
||||
println!("{}", ::serde_json::to_string_pretty(&signature)?);
|
||||
|
||||
let res = verify::verify_order(Order {
|
||||
account_id: signature.clone().account_id,
|
||||
signature,
|
||||
message,
|
||||
});
|
||||
|
||||
println!("Expecting error, got: {:?}", res);
|
||||
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_change_account_id() -> anyhow::Result<()> {
|
||||
let (signature, message) = get_order("n")?;
|
||||
|
||||
let OrderSignature {
|
||||
signature_as_hex,
|
||||
public_key,
|
||||
account_id: _,
|
||||
} = signature;
|
||||
|
||||
// use a different account id to the one that signed the order
|
||||
let account_id = AccountId::from_str("n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es").unwrap();
|
||||
|
||||
let res = verify::verify_order(Order {
|
||||
account_id: account_id.clone(),
|
||||
signature: OrderSignature {
|
||||
account_id,
|
||||
signature_as_hex,
|
||||
public_key,
|
||||
},
|
||||
message,
|
||||
});
|
||||
|
||||
println!("Expecting error, got: {:?}", res);
|
||||
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_change_signature() -> anyhow::Result<()> {
|
||||
let (signature, message) = get_order("n")?;
|
||||
|
||||
let OrderSignature {
|
||||
signature_as_hex: _,
|
||||
public_key,
|
||||
account_id,
|
||||
} = signature;
|
||||
|
||||
let res = verify::verify_order(Order {
|
||||
account_id: account_id.clone(),
|
||||
signature: OrderSignature {
|
||||
account_id,
|
||||
signature_as_hex: "this is not the signature you were looking for".to_string(),
|
||||
public_key,
|
||||
},
|
||||
message,
|
||||
});
|
||||
|
||||
println!("Expecting error, got: {:?}", res);
|
||||
|
||||
assert!(res.is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_with_json_happy_path() -> anyhow::Result<()> {
|
||||
let json_order = r#"{
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"message": "This is the order message from Bity",
|
||||
"signature": {
|
||||
"public_key": {
|
||||
"@type": "/cosmos.crypto.secp256k1.PubKey",
|
||||
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
|
||||
},
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
|
||||
}
|
||||
}"#;
|
||||
let order: Order = serde_json::from_str(json_order)?;
|
||||
|
||||
Ok(verify::verify_order(order)?)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_with_json_bad_signature() -> anyhow::Result<()> {
|
||||
let json_order = r#"{
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"message": "A different message to the one signed",
|
||||
"signature": {
|
||||
"public_key": {
|
||||
"@type": "/cosmos.crypto.secp256k1.PubKey",
|
||||
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
|
||||
},
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
|
||||
}
|
||||
}"#;
|
||||
let order: Order = serde_json::from_str(json_order)?;
|
||||
let res = verify::verify_order(order);
|
||||
println!("Expecting error, got: {:?}", res);
|
||||
assert!(res.is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_with_json_bad_account_id() -> anyhow::Result<()> {
|
||||
let json_order = r#"{
|
||||
"account_id": "n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es",
|
||||
"message": "This is the order message from Bity",
|
||||
"signature": {
|
||||
"public_key": {
|
||||
"@type": "/cosmos.crypto.secp256k1.PubKey",
|
||||
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
|
||||
},
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
|
||||
}
|
||||
}"#;
|
||||
let order: Order = serde_json::from_str(json_order)?;
|
||||
let res = verify::verify_order(order);
|
||||
println!("Expecting error, got: {:?}", res);
|
||||
assert!(res.is_err());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integration_with_json_bad_account_id_variation_2() -> anyhow::Result<()> {
|
||||
let json_order = r#"{
|
||||
"account_id": "n1jw6mp7d5xqc7w6xm79lha27glmd0vdt3l9artf",
|
||||
"message": "This is the order message from Bity",
|
||||
"signature": {
|
||||
"public_key": {
|
||||
"@type": "/cosmos.crypto.secp256k1.PubKey",
|
||||
"key": "A/zqdyeyPhCEXB9pyVLdNb5er+eds5ayboCdEEHK3Uom"
|
||||
},
|
||||
"account_id": "n1h5hgn94nsq4kh99rjj794hr5h5q6yfm2lr52es",
|
||||
"signature_as_hex": "31C522B9B5C522A93CE14BE38E2D380CA166F69E952DF6F5D45B3B9CCDAAFE9115FBDF8539092986391C46885242E6E4CF806EEC1BB869A28D0E6D347C52121A"
|
||||
}
|
||||
}"#;
|
||||
let order: Order = serde_json::from_str(json_order)?;
|
||||
let res = verify::verify_order(order);
|
||||
println!("Expecting error, got: {:?}", res);
|
||||
assert!(res.is_err());
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
use cosmrs::crypto::PublicKey;
|
||||
use cosmrs::AccountId;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct OrderSignature {
|
||||
pub public_key: PublicKey,
|
||||
pub account_id: AccountId,
|
||||
pub signature_as_hex: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct Order {
|
||||
pub account_id: AccountId,
|
||||
pub message: String,
|
||||
pub signature: OrderSignature,
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use crate::order::OrderSignature;
|
||||
use nym_validator_client::nyxd::error::NyxdError;
|
||||
use nym_validator_client::signing::direct_wallet::DirectSecp256k1HdWallet;
|
||||
use nym_validator_client::signing::signer::OfflineSigner;
|
||||
use nym_validator_client::signing::AccountData;
|
||||
|
||||
/// Signs an order message to purchase Nym with Bity
|
||||
pub fn sign_order(
|
||||
wallet: &DirectSecp256k1HdWallet,
|
||||
signer: &AccountData,
|
||||
message: String,
|
||||
) -> Result<OrderSignature, NyxdError> {
|
||||
Ok(OrderSignature {
|
||||
account_id: signer.address().clone(),
|
||||
public_key: signer.public_key(),
|
||||
signature_as_hex: wallet
|
||||
.sign_raw_with_account(signer, message.into_bytes())?
|
||||
.to_string(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
use thiserror::Error;
|
||||
|
||||
use nym_cli_commands::validator::signature::helpers::secp256k1_verify_with_public_key;
|
||||
|
||||
use crate::order::Order;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum VerifyOrderError {
|
||||
#[error("{source}")]
|
||||
K256Error {
|
||||
#[from]
|
||||
source: k256::ecdsa::Error,
|
||||
},
|
||||
#[error("{source}")]
|
||||
ErrorReport {
|
||||
#[from]
|
||||
source: eyre::Report,
|
||||
},
|
||||
#[error("Account id does not match public key")]
|
||||
AccountIdDoesNotMatchPubKey,
|
||||
#[error("Unsupported key type. Only secp256k1 is currently supported.")]
|
||||
UnsupportedKeyType,
|
||||
#[error("Signature error - {0}")]
|
||||
SignatureError(k256::ecdsa::signature::Error),
|
||||
#[error("Account id is not a Nyx mainnet account")]
|
||||
AccountIdPrefixIncorrect,
|
||||
}
|
||||
|
||||
/// Verifies an order
|
||||
pub fn verify_order(order: Order) -> Result<(), VerifyOrderError> {
|
||||
let account_id = order.signature.public_key.account_id("n")?;
|
||||
|
||||
if order.signature.account_id.prefix() != "n" || order.account_id.prefix() != "n" {
|
||||
return Err(VerifyOrderError::AccountIdPrefixIncorrect);
|
||||
}
|
||||
|
||||
// the account id in the order must match the account id derived from the public key
|
||||
if account_id != order.signature.account_id {
|
||||
return Err(VerifyOrderError::AccountIdDoesNotMatchPubKey);
|
||||
}
|
||||
|
||||
// the user provided account id in the order must match the derived account id
|
||||
if account_id != order.account_id || account_id != order.signature.account_id {
|
||||
return Err(VerifyOrderError::AccountIdDoesNotMatchPubKey);
|
||||
}
|
||||
|
||||
if order.signature.public_key.type_url() != cosmrs::crypto::PublicKey::SECP256K1_TYPE_URL {
|
||||
return Err(VerifyOrderError::UnsupportedKeyType);
|
||||
}
|
||||
|
||||
match secp256k1_verify_with_public_key(
|
||||
&order.signature.public_key.to_bytes(),
|
||||
order.signature.signature_as_hex,
|
||||
order.message,
|
||||
) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(e) => Err(VerifyOrderError::SignatureError(e)),
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -4,7 +4,7 @@
|
||||
[package]
|
||||
name = "nym-api"
|
||||
license = "GPL-3.0"
|
||||
version = "1.1.60"
|
||||
version = "1.1.59"
|
||||
authors.workspace = true
|
||||
edition = "2021"
|
||||
rust-version.workspace = true
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": [
|
||||
"@nymproject/eslint-config-react-typescript"
|
||||
],
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.eslint.json"
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,5 @@
|
||||
dist/
|
||||
build/
|
||||
**/*.wasm
|
||||
**/*.js.map
|
||||
.env*
|
||||
!.env.example
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
# environment
|
||||
.env.dev
|
||||
|
||||
# error logs
|
||||
yarn-error.log
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
16
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"printWidth": 120,
|
||||
"tabWidth": 2
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
RPC_URL=
|
||||
VALIDATOR_URL=
|
||||
PREFIX=
|
||||
MIXNET_CONTRACT_ADDRESS=
|
||||
VESTING_CONTRACT_ADDRESS=
|
||||
DENOM=
|
||||
BLOCK_EXPLORER_URL=
|
||||
+23
-108
@@ -1,125 +1,40 @@
|
||||
# Nym Browser Extension Storage
|
||||
# Nym Browser Extension
|
||||
|
||||
A WebAssembly-based storage component for securely managing mnemonics in extensions. This component provides encrypted storage functionality using IndexedDB with password-based encryption.
|
||||
The Nym browser extension lets you access your Nym wallet via the browser.
|
||||
|
||||
## Overview
|
||||
## Getting started
|
||||
|
||||
This storage component is built in Rust and compiled to WebAssembly, providing:
|
||||
You will need:
|
||||
|
||||
- **Secure mnemonic storage**: Password-encrypted storage of BIP39 mnemonics
|
||||
- **IndexedDB integration**: Browser-native persistent storage
|
||||
- **Multiple account support**: Store and manage multiple mnemonic phrases with custom names
|
||||
- **Type-safe API**: Promise-based JavaScript API with proper error handling
|
||||
- NodeJS (use `nvm install` to automatically install the correct version)
|
||||
- `npm`
|
||||
- `yarn`
|
||||
|
||||
## Getting Started
|
||||
> **Note**: This project is part of a mono repo, so you will need to build the shared packages before starting. And any time they change, you'll need to rebuild them.
|
||||
|
||||
### Prerequisites
|
||||
From the [root of the repository](../README.md) run the following to build shared packages:
|
||||
|
||||
- Rust (latest stable)
|
||||
- `wasm-pack` tool for building WebAssembly
|
||||
- Node.js (for the demo server)
|
||||
|
||||
### Building
|
||||
|
||||
```bash
|
||||
cd storage
|
||||
make wasm-pack
|
||||
```
|
||||
yarn
|
||||
yarn build
|
||||
```
|
||||
|
||||
This will compile the Rust code to WebAssembly and generate the necessary JavaScript bindings.
|
||||
From the `nym-browser-extension` directory of the `nym` monorepo, run:
|
||||
|
||||
### Example Usage
|
||||
`yarn dev` to run the extension in dev mode.
|
||||
|
||||
See the [internal-dev example](./storage/internal-dev/index.js) for complete usage examples.
|
||||
You can then open a browser to http://localhost:9000 and start development.
|
||||
|
||||
Basic usage:
|
||||
OR
|
||||
|
||||
```javascript
|
||||
import init, { ExtensionStorage, set_panic_hook } from "@nymproject/extension-storage"
|
||||
`yarn build` to build the extension.
|
||||
|
||||
// Initialize the WASM module first
|
||||
await init();
|
||||
The extension will build to the `nym-browser-extension/dist` directory.
|
||||
|
||||
// Set up better error handling
|
||||
set_panic_hook();
|
||||
## Load extension
|
||||
|
||||
// Create storage instance with password
|
||||
const storage = await new ExtensionStorage("your-secure-password");
|
||||
To load the extension into a Chrome browser
|
||||
|
||||
// Store a mnemonic
|
||||
const mnemonic = "your twenty four word mnemonic phrase goes here...";
|
||||
await storage.store_mnemonic("my-wallet", mnemonic);
|
||||
|
||||
// Read a mnemonic
|
||||
const retrievedMnemonic = await storage.read_mnemonic("my-wallet");
|
||||
|
||||
// Check if a mnemonic exists
|
||||
const exists = await storage.has_mnemonic("my-wallet");
|
||||
|
||||
// Get all stored mnemonic keys
|
||||
const allKeys = await storage.get_all_mnemonic_keys();
|
||||
|
||||
// Remove a mnemonic
|
||||
await storage.remove_mnemonic("my-wallet");
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
To run the internal development example:
|
||||
|
||||
```bash
|
||||
cd storage
|
||||
|
||||
make demo
|
||||
|
||||
# Option 2: Manual server setup
|
||||
cd internal-dev && node serve.js
|
||||
|
||||
# Then open http://localhost:8000 in your browser
|
||||
```
|
||||
|
||||
**Note**: The demo requires a server that properly serves WASM files with `application/wasm` MIME type. The Node.js server is recommended as it handles MIME types more reliably.
|
||||
|
||||
## API Reference
|
||||
|
||||
### Initialization
|
||||
- `init()` - **Required**: Initialize the WASM module before using other functions
|
||||
|
||||
### Constructor
|
||||
- `new ExtensionStorage(password: string)` - Creates a new storage instance with the given password
|
||||
|
||||
### Static Methods
|
||||
- `ExtensionStorage.exists()` - Check if storage database exists
|
||||
|
||||
### Instance Methods
|
||||
- `store_mnemonic(name: string, mnemonic: string)` - Store a mnemonic with the given name
|
||||
- `read_mnemonic(name: string)` - Retrieve a mnemonic by name (returns null if not found)
|
||||
- `has_mnemonic(name: string)` - Check if a mnemonic with the given name exists
|
||||
- `get_all_mnemonic_keys()` - Get all stored mnemonic names
|
||||
- `remove_mnemonic(name: string)` - Remove a mnemonic by name
|
||||
|
||||
### Error Handling
|
||||
- `set_panic_hook()` - Set up better stack traces for Rust panics in development
|
||||
|
||||
## Security Features
|
||||
|
||||
- **Password-based encryption**: All data is encrypted using the provided password
|
||||
- **BIP39 validation**: Mnemonics are validated before storage
|
||||
- **Secure memory handling**: Sensitive data is zeroed from memory when no longer needed
|
||||
- **Browser sandbox**: Runs within the browser's security model
|
||||
|
||||
## Architecture
|
||||
|
||||
The storage component consists of:
|
||||
|
||||
- **Rust core** (`src/storage.rs`): Main storage implementation with encryption
|
||||
- **WASM bindings** (`src/lib.rs`): WebAssembly interface layer
|
||||
- **Error handling** (`src/error.rs`): Comprehensive error types
|
||||
- **Build configuration** (`Cargo.toml`, `Makefile`): Build and dependency management
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **WASM Initialization**: Always call `await init()` before using any other functions
|
||||
2. **MIME Types**: The demo requires a server that properly serves WASM files
|
||||
3. **Browser Compatibility**: Requires modern browsers with WebAssembly support
|
||||
4. **Module Loading**: Uses ES modules - ensure your build system supports them
|
||||
- Go to `settings > extensions > manage extensions`
|
||||
- Select `Load unpacked`
|
||||
- Select the `nym-browser-extension/dist`
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
"name": "@nym/browser-extension",
|
||||
"version": "0.1.0",
|
||||
"main": "index.js",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"dev": "yarn webpack serve --config webpack.dev.js",
|
||||
"build": "yarn preinstall && webpack build --progress --config webpack.prod.js",
|
||||
"lint": "eslint src",
|
||||
"lint:fix": "eslint src --fix",
|
||||
"lint:ts": "tsc --noEmit",
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"storybook:build": "build-storybook"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.7.0",
|
||||
"@emotion/styled": "^11.7.0",
|
||||
"@hookform/resolvers": "^3.1.0",
|
||||
"@mui/icons-material": "^5.11.11",
|
||||
"@mui/material": "^5.11.15",
|
||||
"@mui/system": "^5.11.15",
|
||||
"@nymproject/mui-theme": "^1.0.0",
|
||||
"@nymproject/nym-validator-client": ">=1.2.0-rc.0 || 1",
|
||||
"@nymproject/react": "^1.0.0",
|
||||
"@nymproject/types": "^1.0.0",
|
||||
"@nymproject/extension-storage": ">=1.2.0-rc.0 || 1",
|
||||
"@storybook/react": "^6.5.16",
|
||||
"big.js": "^6.2.1",
|
||||
"crypto-js": "^4.1.1",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-error-boundary": "^4.0.3",
|
||||
"react-hook-form": "^7.43.9",
|
||||
"react-router-dom": "^6.9.0",
|
||||
"zod": "^3.21.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nymproject/eslint-config-react-typescript": "^1.0.0",
|
||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
|
||||
"@svgr/webpack": "^6.1.1",
|
||||
"@testing-library/jest-dom": "^5.14.1",
|
||||
"@testing-library/react": "^14.0.0",
|
||||
"@types/big.js": "^6.1.6",
|
||||
"@types/crypto-js": "4.1.1",
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/node": "^18.16.1",
|
||||
"@types/react": "^18.0.26",
|
||||
"@types/react-dom": "^18.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "^5.13.0",
|
||||
"@typescript-eslint/parser": "^5.13.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"dotenv-webpack": "^8.0.1",
|
||||
"eslint": "^8.10.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-airbnb-typescript": "^16.1.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-import-resolver-root-import": "^1.0.4",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-jest": "^26.1.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.5.1",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"eslint-plugin-react": "^7.29.2",
|
||||
"eslint-plugin-react-hooks": "^4.3.0",
|
||||
"eslint-plugin-storybook": "^0.5.12",
|
||||
"favicons-webpack-plugin": "^5.0.2",
|
||||
"html-webpack-plugin": "^5.3.2",
|
||||
"jest": "^27.1.0",
|
||||
"mini-css-extract-plugin": "^2.2.2",
|
||||
"prettier": "^2.8.7",
|
||||
"react-refresh": "^0.14.0",
|
||||
"react-refresh-typescript": "^2.0.8",
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-jest": "^27.0.5",
|
||||
"ts-loader": "^9.4.2",
|
||||
"tsconfig-paths-webpack-plugin": "^3.5.2",
|
||||
"typescript": "^4.6.2",
|
||||
"url-loader": "^4.1.1",
|
||||
"util": "^0.12.5",
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^5.0.1",
|
||||
"webpack-dev-server": "^4.5.0",
|
||||
"webpack-favicons": "^1.3.8",
|
||||
"webpack-merge": "^5.8.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from 'react';
|
||||
import { NymBrowserExtThemeWithMode } from './theme/NymBrowserExtensionTheme';
|
||||
import { AppRoutes } from './routes';
|
||||
import { AppLayout } from './layouts/AppLayout';
|
||||
import { AppProvider } from './context';
|
||||
|
||||
export const App = () => (
|
||||
<NymBrowserExtThemeWithMode mode="light">
|
||||
<AppProvider>
|
||||
<AppLayout>
|
||||
<AppRoutes />
|
||||
</AppLayout>
|
||||
</AppProvider>
|
||||
</NymBrowserExtThemeWithMode>
|
||||
);
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { Avatar, ListItem, ListItemAvatar, ListItemButton, ListItemText } from '@mui/material';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useAppContext } from 'src/context';
|
||||
import { AccountActions } from './Actions';
|
||||
|
||||
const AccountItem = ({
|
||||
accountName,
|
||||
disabled,
|
||||
onSelect,
|
||||
}: {
|
||||
accountName: string;
|
||||
disabled: boolean;
|
||||
onSelect: () => void;
|
||||
}) => (
|
||||
<ListItem disableGutters disablePadding secondaryAction={<AccountActions accountName={accountName} />} divider>
|
||||
<ListItemButton onClick={onSelect} disabled={disabled}>
|
||||
<ListItemAvatar>
|
||||
<Avatar>{accountName[0]}</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={accountName} secondary={disabled && '(Selected)'} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
export const AccountList = () => {
|
||||
const navigate = useNavigate();
|
||||
const { accounts, selectAccount, selectedAccount } = useAppContext();
|
||||
|
||||
const handleSelectAccount = async (accountName: string) => {
|
||||
await selectAccount(accountName);
|
||||
navigate('/user/balance');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
{accounts.map((accountName) => (
|
||||
<AccountItem
|
||||
disabled={selectedAccount === accountName}
|
||||
accountName={accountName}
|
||||
key={accountName}
|
||||
onSelect={() => handleSelectAccount(accountName)}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { IconButton, ListItemIcon, ListItemText, Menu, MenuItem } from '@mui/material';
|
||||
import { MoreVert, VisibilityOutlined } from '@mui/icons-material';
|
||||
import { useAppContext } from 'src/context';
|
||||
|
||||
type ActionType = {
|
||||
title: string;
|
||||
Icon: React.ReactNode;
|
||||
onSelect: () => void;
|
||||
};
|
||||
|
||||
const ActionItem = ({ action }: { action: ActionType }) => (
|
||||
<MenuItem dense onClick={action.onSelect}>
|
||||
<ListItemIcon>{action.Icon}</ListItemIcon>
|
||||
<ListItemText>{action.title}</ListItemText>
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
export const AccountActions = ({ accountName }: { accountName: string }) => {
|
||||
const { setShowSeedForAccount } = useAppContext();
|
||||
|
||||
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
|
||||
|
||||
const open = Boolean(anchorEl);
|
||||
|
||||
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
const handleClose = () => {
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const actions: Array<ActionType> = [
|
||||
{
|
||||
title: 'View seed phrase',
|
||||
Icon: <VisibilityOutlined />,
|
||||
onSelect: () => {
|
||||
setShowSeedForAccount(accountName);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<IconButton onClick={handleClick}>
|
||||
<MoreVert />
|
||||
</IconButton>
|
||||
<Menu anchorEl={anchorEl} id="account-menu" open={open} onClose={handleClose} onClick={handleClose}>
|
||||
{actions.map((action) => (
|
||||
<ActionItem action={action} key={action.title} />
|
||||
))}
|
||||
</Menu>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Box, Card, CardContent, Typography } from '@mui/material';
|
||||
import { PasswordInput } from '@nymproject/react/textfields/Password';
|
||||
import { ExtensionStorage } from '@nymproject/extension-storage';
|
||||
import { Button, ConfirmationModal } from 'src/components/ui';
|
||||
|
||||
const ShowSeedButton = ({ handleShowSeedPhrase }: { handleShowSeedPhrase: () => void }) => (
|
||||
<Button fullWidth variant="contained" onClick={handleShowSeedPhrase}>
|
||||
Show seed phrase
|
||||
</Button>
|
||||
);
|
||||
|
||||
const DoneButton = ({ onDone }: { onDone: () => void }) => (
|
||||
<Button fullWidth variant="contained" onClick={onDone}>
|
||||
Done
|
||||
</Button>
|
||||
);
|
||||
|
||||
const Seed = ({ seed }: { seed: string }) => (
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Typography>{seed}</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
|
||||
export const ViewSeedPhrase = ({ accountName, onDone }: { accountName: string; onDone: () => void }) => {
|
||||
const [seed, setSeed] = useState<string>();
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState<string>();
|
||||
|
||||
const handleShowSeedPhrase = async () => {
|
||||
try {
|
||||
const storage = await new ExtensionStorage(password);
|
||||
const accountSeed = await storage.read_mnemonic(accountName);
|
||||
setSeed(accountSeed);
|
||||
} catch (e) {
|
||||
setError('Could not retrieve seed phrase. Please check your password');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfirmationModal
|
||||
open
|
||||
onClose={onDone}
|
||||
title={seed ? 'Account seed phrase' : 'Password'}
|
||||
subtitle={seed ? '' : 'Enter your account password'}
|
||||
ConfirmButton={
|
||||
seed ? <DoneButton onDone={onDone} /> : <ShowSeedButton handleShowSeedPhrase={handleShowSeedPhrase} />
|
||||
}
|
||||
>
|
||||
{seed ? (
|
||||
<Seed seed={seed} />
|
||||
) : (
|
||||
<Box sx={{ mt: 2 }}>
|
||||
<PasswordInput
|
||||
label="Password"
|
||||
error={error}
|
||||
password={password}
|
||||
onUpdatePassword={(pw: string) => {
|
||||
setPassword(pw);
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</ConfirmationModal>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './Accounts';
|
||||
export * from './Actions';
|
||||
export * from './ViewSeedPhrase';
|
||||
@@ -0,0 +1,16 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
import { Stack } from '@mui/system';
|
||||
import { ClientAddress } from '@nymproject/react/client-address/ClientAddress';
|
||||
import { useAppContext } from 'src/context';
|
||||
|
||||
export const Address = () => {
|
||||
const { client } = useAppContext();
|
||||
|
||||
return (
|
||||
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
||||
<Typography fontWeight={700}>Address</Typography>
|
||||
<ClientAddress withCopy address={client?.address || ''} />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,23 @@
|
||||
import React, { useEffect } from 'react';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { useAppContext } from 'src/context';
|
||||
|
||||
export const Balance = () => {
|
||||
const { balance, fiatBalance, currency, getBalance } = useAppContext();
|
||||
|
||||
useEffect(() => {
|
||||
getBalance();
|
||||
}, []);
|
||||
|
||||
const fiat = fiatBalance ? `~ ${Intl.NumberFormat().format(fiatBalance)} ${currency.toUpperCase()}` : '-';
|
||||
|
||||
return (
|
||||
<Stack alignItems="center" gap={1}>
|
||||
<Typography sx={{ color: 'grey.600' }}>Available</Typography>
|
||||
<Typography variant="h4" textAlign="center">
|
||||
{balance} NYM
|
||||
</Typography>
|
||||
<Typography sx={{ color: 'grey.600' }}>{fiat}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
export * from './accounts';
|
||||
export * from './address';
|
||||
export * from './balance';
|
||||
export * from './receive';
|
||||
export * from './send';
|
||||
export * from './ui';
|
||||
@@ -0,0 +1,35 @@
|
||||
import React from 'react';
|
||||
import { Card, CardContent, Dialog, DialogContent, DialogTitle, IconButton, Stack, Typography } from '@mui/material';
|
||||
import { QRCodeSVG } from 'qrcode.react';
|
||||
import { useAppContext } from 'src/context';
|
||||
import { ClientAddress } from '@nymproject/react/client-address/ClientAddress';
|
||||
import { Close } from '@mui/icons-material';
|
||||
|
||||
export const ReceiveModal = ({ open, onClose }: { open: boolean; onClose: () => void }) => {
|
||||
const { client } = useAppContext();
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="xl" fullWidth>
|
||||
<DialogTitle>
|
||||
<Stack direction="row" justifyContent="space-between">
|
||||
<Typography fontWeight={700}>Receive</Typography>
|
||||
<IconButton size="small" onClick={onClose} sx={{ padding: 0 }}>
|
||||
<Close fontSize="small" />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Stack gap={1} alignItems="center">
|
||||
<Card elevation={3} sx={{ my: 2, width: 200 }}>
|
||||
<CardContent sx={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||
<QRCodeSVG value={client?.address || ''} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Typography variant="body2" fontWeight={700}>
|
||||
Your Nym address
|
||||
</Typography>
|
||||
<ClientAddress address={client?.address || ''} withCopy smallIcons />
|
||||
</Stack>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export * from './ReceiveModal';
|
||||
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { Box, Typography } from '@mui/material';
|
||||
import { Link } from '@nymproject/react/link/Link';
|
||||
import { ConfirmationModal, Button } from 'src/components/ui';
|
||||
|
||||
export const SendConfirmationModal = ({
|
||||
amount,
|
||||
txUrl,
|
||||
onConfirm,
|
||||
}: {
|
||||
amount: string;
|
||||
txUrl: string;
|
||||
onConfirm: () => void;
|
||||
}) => (
|
||||
<ConfirmationModal
|
||||
open
|
||||
fullWidth
|
||||
title="You sent"
|
||||
ConfirmButton={
|
||||
<Button fullWidth variant="contained" size="large" onClick={onConfirm}>
|
||||
Done
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
<Typography variant="h6">{amount}</Typography>
|
||||
<Link href={txUrl} target="_blank" sx={{ ml: 1 }} text="View on blockchain" />
|
||||
</Box>
|
||||
</ConfirmationModal>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export * from './SendConfirmationModal';
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as React from 'react';
|
||||
import { AppBar as MUIAppBar } from '@mui/material/';
|
||||
import Box from '@mui/material/Box';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
|
||||
export const AppBar = ({ Action }: { Action: React.ReactNode }) => (
|
||||
<Box sx={{ flexGrow: 1 }}>
|
||||
<MUIAppBar position="static" elevation={0} sx={{ bgcolor: 'rgba(103, 80, 164, 0.14)' }}>
|
||||
<Toolbar variant="dense">{Action}</Toolbar>
|
||||
</MUIAppBar>
|
||||
</Box>
|
||||
);
|
||||
|
||||
export default AppBar;
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { ArrowBackIosRounded } from '@mui/icons-material';
|
||||
import { IconButton } from '@mui/material';
|
||||
|
||||
export const BackButton = ({ onBack }: { onBack?: () => void }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleClick = () => {
|
||||
if (onBack) {
|
||||
onBack();
|
||||
} else {
|
||||
navigate(-1);
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return (
|
||||
<IconButton size="small" onClick={handleClick}>
|
||||
<ArrowBackIosRounded fontSize="small" />
|
||||
</IconButton>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import { Button as MUIButton, ButtonProps } from '@mui/material';
|
||||
|
||||
export const Button = (props: ButtonProps) => (
|
||||
<MUIButton {...props} disableElevation sx={{ textTransform: 'initial' }} />
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import React from 'react';
|
||||
import { NymLogoBW } from '@nymproject/react/logo/NymLogoBW';
|
||||
|
||||
export const Logo = ({ small }: { small?: boolean }) => (
|
||||
<NymLogoBW width={small ? '37.5px' : '75px'} height={small ? '37.5px' : '75px'} />
|
||||
);
|
||||
@@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import { Stack, Typography } from '@mui/material';
|
||||
import { Logo } from '../Logo';
|
||||
import { Title } from '../Title';
|
||||
|
||||
export const LogoWithText = ({
|
||||
logoSmall,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
logoSmall?: boolean;
|
||||
title: string;
|
||||
description?: string;
|
||||
}) => (
|
||||
<Stack alignItems="center" justifyContent="center" gap={3}>
|
||||
<Logo small={logoSmall} />
|
||||
<Title>{title}</Title>
|
||||
<Typography sx={{ color: 'grey.700', textAlign: 'center' }}>{description}</Typography>
|
||||
</Stack>
|
||||
);
|
||||
@@ -0,0 +1,55 @@
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import ListItemButton from '@mui/material/ListItemButton';
|
||||
import ListItemIcon from '@mui/material/ListItemIcon';
|
||||
import ListItemText from '@mui/material/ListItemText';
|
||||
import { AccountBalanceWalletRounded, AccountCircleRounded, ArrowDownwardRounded } from '@mui/icons-material';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const menuSchema = [
|
||||
{
|
||||
title: 'Accounts',
|
||||
Icon: <AccountCircleRounded />,
|
||||
path: '/user/accounts',
|
||||
},
|
||||
{
|
||||
title: 'Balance',
|
||||
Icon: <AccountBalanceWalletRounded />,
|
||||
path: '/user/balance',
|
||||
},
|
||||
{
|
||||
title: 'Send',
|
||||
Icon: <ArrowDownwardRounded />,
|
||||
path: '/user/send',
|
||||
},
|
||||
];
|
||||
|
||||
export const MenuDrawer = ({ open, onClose }: { open: boolean; onClose: () => void }) => {
|
||||
const list = () => (
|
||||
<Box sx={{ width: 250 }} role="presentation" onClick={() => {}}>
|
||||
<List>
|
||||
{menuSchema.map(({ title, Icon, path }) => (
|
||||
<Link to={path} style={{ textDecoration: 'none', color: 'unset' }} key={title}>
|
||||
<ListItem disablePadding>
|
||||
<ListItemButton>
|
||||
<ListItemIcon>{Icon}</ListItemIcon>
|
||||
<ListItemText primary={title} />
|
||||
</ListItemButton>
|
||||
</ListItem>
|
||||
</Link>
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Drawer anchor="left" open={open} onClose={onClose}>
|
||||
{list()}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Breakpoint,
|
||||
Paper,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
SxProps,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { Button } from '../Button';
|
||||
|
||||
export interface ErrorModalProps {
|
||||
open: boolean;
|
||||
children?: React.ReactNode;
|
||||
title: React.ReactNode | string;
|
||||
subtitle?: React.ReactNode | string;
|
||||
sx?: SxProps;
|
||||
fullWidth?: boolean;
|
||||
maxWidth?: Breakpoint;
|
||||
backdropProps?: object;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const ErrorModal = ({
|
||||
open,
|
||||
onClose,
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
sx,
|
||||
fullWidth,
|
||||
maxWidth,
|
||||
backdropProps,
|
||||
}: ErrorModalProps) => {
|
||||
const Title = (
|
||||
<DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{title}
|
||||
</Typography>
|
||||
{subtitle &&
|
||||
(typeof subtitle === 'string' ? (
|
||||
<Typography fontWeight={400} variant="subtitle1" fontSize={12} sx={{ color: 'grey.400' }}>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
) : (
|
||||
subtitle
|
||||
))}
|
||||
</DialogTitle>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
aria-labelledby="responsive-dialog-title"
|
||||
maxWidth={maxWidth || 'sm'}
|
||||
sx={{ textAlign: 'center', ...sx }}
|
||||
fullWidth={fullWidth}
|
||||
BackdropProps={backdropProps}
|
||||
PaperComponent={Paper}
|
||||
PaperProps={{ elevation: 0 }}
|
||||
>
|
||||
{Title}
|
||||
<DialogContent>{children}</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 3 }}>
|
||||
<Button variant="contained" size="large" fullWidth onClick={onClose}>
|
||||
Close
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { Box, CircularProgress, Modal, Stack, Typography, SxProps } from '@mui/material';
|
||||
|
||||
const modalStyle: SxProps = {
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: 300,
|
||||
bgcolor: 'background.paper',
|
||||
boxShadow: 24,
|
||||
borderRadius: '16px',
|
||||
p: 4,
|
||||
};
|
||||
|
||||
export const LoadingModal = ({ sx, backdropProps }: { sx?: SxProps; backdropProps?: object }) => (
|
||||
<Modal open BackdropProps={backdropProps}>
|
||||
<Box sx={{ border: (t) => `1px solid ${t.palette.grey[500]}`, ...modalStyle, ...sx }} textAlign="center">
|
||||
<Stack spacing={4} direction="row" alignItems="center">
|
||||
<CircularProgress />
|
||||
<Typography sx={{ color: 'text.primary' }}>Please wait...</Typography>
|
||||
</Stack>
|
||||
</Box>
|
||||
</Modal>
|
||||
);
|
||||
@@ -0,0 +1,71 @@
|
||||
import React from 'react';
|
||||
import {
|
||||
Breakpoint,
|
||||
Paper,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
SxProps,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
|
||||
export interface ConfirmationModalProps {
|
||||
open: boolean;
|
||||
children?: React.ReactNode;
|
||||
title: React.ReactNode | string;
|
||||
subtitle?: React.ReactNode | string;
|
||||
ConfirmButton: React.ReactNode;
|
||||
sx?: SxProps;
|
||||
fullWidth?: boolean;
|
||||
maxWidth?: Breakpoint;
|
||||
backdropProps?: object;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const ConfirmationModal = ({
|
||||
open,
|
||||
onClose,
|
||||
children,
|
||||
title,
|
||||
subtitle,
|
||||
ConfirmButton,
|
||||
sx,
|
||||
fullWidth,
|
||||
maxWidth,
|
||||
backdropProps,
|
||||
}: ConfirmationModalProps) => {
|
||||
const Title = (
|
||||
<DialogTitle id="responsive-dialog-title" sx={{ pb: 2 }}>
|
||||
<Typography variant="body2" fontWeight={600}>
|
||||
{title}
|
||||
</Typography>
|
||||
{subtitle &&
|
||||
(typeof subtitle === 'string' ? (
|
||||
<Typography fontWeight={400} variant="subtitle1" fontSize={12} sx={{ color: 'grey.400' }}>
|
||||
{subtitle}
|
||||
</Typography>
|
||||
) : (
|
||||
subtitle
|
||||
))}
|
||||
</DialogTitle>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
aria-labelledby="responsive-dialog-title"
|
||||
maxWidth={maxWidth || 'sm'}
|
||||
sx={{ textAlign: 'center', ...sx }}
|
||||
fullWidth={fullWidth}
|
||||
BackdropProps={backdropProps}
|
||||
PaperComponent={Paper}
|
||||
PaperProps={{ elevation: 0 }}
|
||||
>
|
||||
{Title}
|
||||
<DialogContent>{children}</DialogContent>
|
||||
<DialogActions sx={{ px: 3, pb: 3 }}>{ConfirmButton}</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './Modal';
|
||||
export * from './LoadingModal';
|
||||
export * from './ErrorModal';
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { Typography } from '@mui/material';
|
||||
|
||||
const FONT_WEIGHT = 400;
|
||||
|
||||
export const Title = ({ children }: { children: string }) => (
|
||||
<Typography variant="h5" fontWeight={FONT_WEIGHT}>
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
@@ -0,0 +1,8 @@
|
||||
export * from './AppBar';
|
||||
export * from './Button';
|
||||
export * from './BackButton';
|
||||
export * from './Logo';
|
||||
export * from './LogoWithText';
|
||||
export * from './MenuDrawer';
|
||||
export * from './Modal';
|
||||
export * from './Title';
|
||||
@@ -0,0 +1,8 @@
|
||||
export const config = {
|
||||
rpcUrl: process.env.RPC_URL || '',
|
||||
validatorUrl: process.env.VALIDATOR_URL || '',
|
||||
prefix: process.env.PREFIX || '',
|
||||
mixnetContractAddress: process.env.MIXNET_CONTRACT_ADDRESS || '',
|
||||
vestingContractAddress: process.env.VESTING_CONTRACT_ADDRESS || '',
|
||||
denom: process.env.DENOM || '',
|
||||
};
|
||||
@@ -0,0 +1,109 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import ValidatorClient from '@nymproject/nym-validator-client';
|
||||
import { ExtensionStorage } from '@nymproject/extension-storage';
|
||||
import { connectToValidator } from 'src/validator-client';
|
||||
import { unymToNym } from 'src/utils/coin';
|
||||
import { Currency, getTokenPrice } from 'src/utils/price';
|
||||
|
||||
type TAppContext = {
|
||||
client?: ValidatorClient;
|
||||
accounts: string[];
|
||||
balance?: string;
|
||||
fiatBalance?: number;
|
||||
denom: 'NYM';
|
||||
minorDenom: 'unym';
|
||||
currency: Currency;
|
||||
showSeedForAccount?: string;
|
||||
selectedAccount: string;
|
||||
storage?: ExtensionStorage;
|
||||
selectAccount: (accountName: string) => Promise<void>;
|
||||
setAccounts: (accounts: string[]) => void;
|
||||
setShowSeedForAccount: (accountName?: string) => void;
|
||||
handleUnlockWallet: (password: string) => void;
|
||||
getBalance: () => void;
|
||||
};
|
||||
|
||||
type TBalanceInNYMs = string;
|
||||
|
||||
const DEFAULT_ACCOUNT_NAME = 'Default account';
|
||||
|
||||
const AppContext = React.createContext({} as TAppContext);
|
||||
|
||||
export const AppProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [client, setClient] = useState<ValidatorClient>();
|
||||
const [selectedAccount, setSelected] = useState<string>(DEFAULT_ACCOUNT_NAME);
|
||||
const [balance, setBalance] = useState<TBalanceInNYMs>();
|
||||
const [fiatBalance, setFiatBalance] = useState<number>();
|
||||
const [accounts, setAccounts] = useState<string[]>([]);
|
||||
const [showSeedForAccount, setShowSeedForAccount] = useState<string>();
|
||||
const [storage, setStorage] = useState<ExtensionStorage>();
|
||||
|
||||
const denom = 'NYM';
|
||||
const minorDenom = 'unym';
|
||||
const currency = 'gbp';
|
||||
|
||||
const handleUnlockWallet = async (password: string) => {
|
||||
const store = await new ExtensionStorage(password);
|
||||
const mnemonic = await store.read_mnemonic(DEFAULT_ACCOUNT_NAME);
|
||||
const userAccounts = await store.get_all_mnemonic_keys();
|
||||
const clientFromMnemonic = await connectToValidator(mnemonic);
|
||||
|
||||
setStorage(store);
|
||||
setAccounts(userAccounts);
|
||||
setClient(clientFromMnemonic);
|
||||
};
|
||||
|
||||
const selectAccount = async (accountName: string) => {
|
||||
const mnemonic = await storage!.read_mnemonic(accountName);
|
||||
const clientFromMnemonic = await connectToValidator(mnemonic);
|
||||
setSelected(accountName);
|
||||
setClient(clientFromMnemonic);
|
||||
};
|
||||
|
||||
const getFiatBalance = async (bal: number) => {
|
||||
const tokenPrice = await getTokenPrice('nym', currency);
|
||||
const fiatBal = tokenPrice.nym.gbp * bal;
|
||||
return fiatBal;
|
||||
};
|
||||
|
||||
const getBalance = async () => {
|
||||
const bal = await client?.getBalance(client.address);
|
||||
if (bal) {
|
||||
const nym = unymToNym(Number(bal.amount));
|
||||
const fiat = await getFiatBalance(nym);
|
||||
setFiatBalance(fiat);
|
||||
setBalance(nym.toString());
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (client) {
|
||||
getBalance();
|
||||
}
|
||||
}, [client]);
|
||||
|
||||
const value = useMemo<TAppContext>(
|
||||
() => ({
|
||||
client,
|
||||
accounts,
|
||||
balance,
|
||||
fiatBalance,
|
||||
currency,
|
||||
denom,
|
||||
minorDenom,
|
||||
selectedAccount,
|
||||
storage,
|
||||
handleUnlockWallet,
|
||||
getBalance,
|
||||
setShowSeedForAccount,
|
||||
showSeedForAccount,
|
||||
setAccounts,
|
||||
selectAccount,
|
||||
}),
|
||||
[client, accounts, balance, fiatBalance, denom, minorDenom, selectedAccount, showSeedForAccount, storage],
|
||||
);
|
||||
|
||||
return <AppContext.Provider value={value}>{children}</AppContext.Provider>;
|
||||
};
|
||||
|
||||
export const useAppContext = () => React.useContext(AppContext);
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './app';
|
||||
export * from './send';
|
||||
export * from './register';
|
||||
@@ -0,0 +1,71 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { ExtensionStorage } from '@nymproject/extension-storage';
|
||||
|
||||
const RegisterContext = React.createContext({} as TRegisterContext);
|
||||
|
||||
type TRegisterContext = {
|
||||
userPassword: string;
|
||||
userMnemonic: string;
|
||||
accountName: string;
|
||||
checkAccountName: () => Promise<boolean>;
|
||||
setUserPassword: (password: string) => void;
|
||||
setUserMnemonic: (mnemonic: string) => void;
|
||||
setAccountName: (name: string) => void;
|
||||
createAccount: (args: { mnemonic: string; password: string; accName: string }) => Promise<void>;
|
||||
importAccount: () => Promise<string[]>;
|
||||
resetState: () => void;
|
||||
};
|
||||
|
||||
export const RegisterContextProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const [userPassword, setUserPassword] = useState('');
|
||||
const [userMnemonic, setUserMnemonic] = useState('');
|
||||
const [accountName, setAccountName] = useState('');
|
||||
|
||||
const resetState = () => {
|
||||
setUserMnemonic('');
|
||||
setUserPassword('');
|
||||
setAccountName('');
|
||||
};
|
||||
|
||||
const createAccount = async ({
|
||||
mnemonic,
|
||||
password,
|
||||
accName,
|
||||
}: {
|
||||
mnemonic: string;
|
||||
password: string;
|
||||
accName: string;
|
||||
}) => {
|
||||
const storage = await new ExtensionStorage(password);
|
||||
await storage.store_mnemonic(accName, mnemonic);
|
||||
};
|
||||
|
||||
const importAccount = async () => {
|
||||
const storage = await new ExtensionStorage(userPassword);
|
||||
await storage.store_mnemonic(accountName, userMnemonic);
|
||||
const accounts = await storage.get_all_mnemonic_keys();
|
||||
return accounts;
|
||||
};
|
||||
|
||||
const checkAccountName = async () => true;
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
userPassword,
|
||||
setUserPassword,
|
||||
userMnemonic,
|
||||
accountName,
|
||||
setAccountName,
|
||||
setUserMnemonic,
|
||||
createAccount,
|
||||
checkAccountName,
|
||||
importAccount,
|
||||
resetState,
|
||||
}),
|
||||
[userPassword, userMnemonic, accountName],
|
||||
);
|
||||
|
||||
return <RegisterContext.Provider value={value}>{children}</RegisterContext.Provider>;
|
||||
};
|
||||
|
||||
export const useRegisterContext = () => React.useContext(RegisterContext);
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user