Compare commits

..

1 Commits

Author SHA1 Message Date
Yana a5c760d8ff Remove Buy button and route to page 2025-06-10 17:58:39 +03:00
272 changed files with 11090 additions and 9222 deletions
+2 -1
View File
@@ -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
+4 -2
View File
@@ -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
+1
View File
@@ -40,6 +40,7 @@ validator-config
validator-api-config.toml
dist
storybook-static
envs/qwerty.env
.parcel-cache
**/.DS_Store
cpu-cycles/libcpucycles/build
-24
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+1 -4
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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
}
}
+1 -12
View File
@@ -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);
+2 -9
View File
@@ -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()),
})
}
}
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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 -28
View File
@@ -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 =
-35
View File
@@ -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,
}
}
-47
View File
@@ -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
-518
View File
@@ -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);
-311
View File
@@ -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!(),
}
}
-258
View File
@@ -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
),
})
}
}
}
-95
View File
@@ -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)
}
-90
View File
@@ -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!(),
}
}
}
-146
View File
@@ -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>,
}
-107
View File
@@ -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)
}
-231
View File
@@ -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)
}
}
}
}
}
-228
View File
@@ -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(())
}
-6
View File
@@ -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"] }
-3
View File
@@ -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,
}
}
-19
View File
@@ -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
View File
@@ -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)))
}
+34 -193
View File
@@ -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 =
-30
View File
@@ -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
-47
View File
@@ -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);
-58
View File
@@ -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,
}
}
}
-5
View File
@@ -1,5 +0,0 @@
// Copyright 2025 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub mod error;
pub mod psq;
-270
View File
@@ -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,
);
}
}
+40 -46
View File
@@ -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
+136
View File
@@ -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
+1 -1
View File
@@ -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
+500 -1631
View File
File diff suppressed because it is too large Load Diff
+25
View File
@@ -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/
+21
View File
@@ -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 }
+53
View File
@@ -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
+220
View File
@@ -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(())
}
}
+17
View File
@@ -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,
}
+20
View File
@@ -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(),
})
}
+59
View File
@@ -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
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
{
"extends": [
"@nymproject/eslint-config-react-typescript"
],
"parserOptions": {
"project": "./tsconfig.eslint.json"
}
}
+5 -14
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
16
+6
View File
@@ -0,0 +1,6 @@
{
"trailingComma": "all",
"singleQuote": true,
"printWidth": 120,
"tabWidth": 2
}
+7
View File
@@ -0,0 +1,7 @@
RPC_URL=
VALIDATOR_URL=
PREFIX=
MIXNET_CONTRACT_ADDRESS=
VESTING_CONTRACT_ADDRESS=
DENOM=
BLOCK_EXPLORER_URL=
View File
+23 -108
View File
@@ -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`
+86
View File
@@ -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"
}
}
+15
View File
@@ -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';
+8
View File
@@ -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 || '',
};
+109
View File
@@ -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