Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions nym-vpn-app/src-tauri/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ pub enum ErrorKey {
MixnetNoBandwidth,
// Some specific account management errors
AccountInvalidMnemonic,
AccountInvalidSecret,
NoAccountStored,
NoDeviceStored,
ExistingAccount,
Expand Down
5 changes: 5 additions & 0 deletions nym-vpn-app/src-tauri/src/vpnd/account_error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ impl From<lib::AccountCommandError> for BackendError {
ErrorKey::AccountInvalidMnemonic,
format!("invalid mnemonic: {e}"),
),
lib::AccountCommandError::InvalidSecret(e) => BackendError::with_detail(
"invalid secret",
ErrorKey::AccountInvalidSecret,
format!("invalid secret: {e}"),
),
lib::AccountCommandError::NyxdConnectionFailure(e) => {
BackendError::internal_with_detail("failed to connect to nyxd", e)
}
Expand Down
2 changes: 1 addition & 1 deletion nym-vpn-app/src-tauri/src/vpnd/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use super::{

use anyhow::Result;
use lib::UserAgent;
use nym_vpn_lib_types as lib;
use nym_vpn_lib_types::{self as lib};
use nym_vpn_proto::rpc_client::RpcClient;
use once_cell::sync::Lazy;
use std::{
Expand Down
1 change: 1 addition & 0 deletions nym-vpn-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Add custom DNS setting for mobile platforms (https://github.com/nymtech/nym-vpn-client/pull/4106)
- Login with signature string in addition to mnemonic (https://github.com/nymtech/nym-vpn-client/pull/4117)

### Fixed

Expand Down
2 changes: 2 additions & 0 deletions nym-vpn-core/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion nym-vpn-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,7 @@ nym-gateway-directory = { path = "crates/nym-gateway-directory" }
nym-ipc = { path = "crates/nym-ipc" }
nym-macos = { path = "crates/nym-macos" }
nym-offline-monitor = { path = "crates/nym-offline-monitor" }
nym-platform-metadata = { path = "crates/nym-platform-metadata" }
nym-routing = { path = "crates/nym-routing" }
nym-statistics = { path = "crates/nym-statistics" }
nym-statistics-api-client = { path = "crates/nym-statistics-api-client" }
Expand All @@ -202,7 +203,6 @@ nym-vpn-store = { path = "crates/nym-vpn-store" }
nym-wg-go = { path = "crates/nym-wg-go" }
nym-wg-metadata-client = { path = "crates/nym-wg-metadata-client" }
nym-windows = { path = "crates/nym-windows" }
nym-platform-metadata = { path = "crates/nym-platform-metadata" }

# For normal development
nym-authenticator-client = { git = "https://github.com/nymtech/nym", branch = "develop" }
Expand Down
3 changes: 3 additions & 0 deletions nym-vpn-core/crates/nym-vpn-lib-types/src/account/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ pub enum AccountCommandError {

#[error("invalid mnemonic: {0}")]
InvalidMnemonic(String),

#[error("invalid secret: {0}")]
InvalidSecret(String),
}

impl AccountCommandError {
Expand Down
2 changes: 2 additions & 0 deletions nym-vpn-core/crates/nym-vpn-lib-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ mod gateway;
mod log_path;
mod network;
mod network_stats;
mod privy;
mod rpc_requests;
mod service;
mod socks5;
Expand Down Expand Up @@ -74,6 +75,7 @@ pub use network::{
SystemConfiguration, SystemMessage, ValidatorDetails,
};
pub use network_stats::{NetworkStatisticsConfig, NetworkStatisticsIdentity};
pub use privy::PrivyDerivationMessage;
pub use rpc_requests::{
AccountBalanceResponse, AccountCommandResponse, Coin, DecentralisedObtainTicketbooksRequest,
ListGatewaysOptions, StoreAccountRequest,
Expand Down
16 changes: 16 additions & 0 deletions nym-vpn-core/crates/nym-vpn-lib-types/src/privy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright 2025 - Nym Technologies SA <[email protected]>
// SPDX-License-Identifier: GPL-3.0-only

#[derive(Clone, Debug)]
#[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))]
#[cfg_attr(
feature = "typescript-bindings",
derive(TS),
ts(export),
ts(export_to = "bindings.ts")
)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "typescript-bindings", serde(rename_all = "camelCase"))]
pub struct PrivyDerivationMessage {
pub message: String,
}
7 changes: 7 additions & 0 deletions nym-vpn-core/crates/nym-vpn-lib-types/src/rpc_requests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,16 @@ pub struct ListGatewaysOptions {
#[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Enum))]
pub enum StoreAccountRequest {
Vpn { mnemonic: String },
Privy { hex_signature: String },
Decentralised { mnemonic: String },
}

impl StoreAccountRequest {
pub fn centralised(&self) -> bool {
!matches!(self, Self::Decentralised { .. })
}
}

impl std::fmt::Debug for StoreAccountRequest {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("StoreVpnAccountRequest")
Expand Down
31 changes: 20 additions & 11 deletions nym-vpn-core/crates/nym-vpn-lib-uniffi/src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ use nym_offline_monitor::ConnectivityHandle;
use nym_vpn_account_controller::{AccountCommandSender, AccountStateReceiver, NyxdClient};
use nym_vpn_api_client::types::{Platform, VpnAccount};
use nym_vpn_lib::{new_user_agent, storage::VpnClientOnDiskStorage};
use nym_vpn_lib_types::{AccountControllerState, RegisterAccountResponse};
use nym_vpn_lib_types::{AccountControllerState, RegisterAccountResponse, StoreAccountRequest};
use nym_vpn_network_config::Network;
use nym_vpn_store::{
account::Mnemonic,
Expand Down Expand Up @@ -198,14 +198,12 @@ pub(super) async fn update_account_state() -> Result<(), VpnError> {
.map_err(VpnError::from)
}

async fn parse_mnemonic(mnemonic: &str) -> Result<Mnemonic, VpnError> {
Mnemonic::parse(mnemonic).map_err(|err| VpnError::InvalidMnemonic {
details: err.to_string(),
})
}

pub(super) async fn login(mnemonic: &str) -> Result<(), VpnError> {
let mnemonic = parse_mnemonic(mnemonic).await?;
pub(super) async fn login(request: &StoreAccountRequest) -> Result<(), VpnError> {
let mnemonic = nym_vpn_lib::login::parse_account_request(request).map_err(|err| {
VpnError::InvalidSecret {
details: err.to_string(),
}
})?;
get_command_sender()
.await?
.store_account(mnemonic.into())
Expand Down Expand Up @@ -313,15 +311,26 @@ pub(crate) mod raw {
Ok(VpnClientOnDiskStorage::new(path))
}

pub(crate) async fn login_raw(mnemonic: &str, path: &str) -> Result<(), VpnError> {
let mnemonic = parse_mnemonic(mnemonic).await?;
async fn login_inner(mnemonic: Mnemonic, path: &str) -> Result<(), VpnError> {
get_account_by_mnemonic_raw(mnemonic.clone()).await?;
let storage = setup_account_storage(path).await?;
storage.store_account(mnemonic.into()).await?;
storage.init_keys(None).await?;
Ok(())
}

pub(crate) async fn login_raw(
request: &StoreAccountRequest,
path: &str,
) -> Result<(), VpnError> {
let mnemonic = nym_vpn_lib::login::parse_account_request(request).map_err(|err| {
VpnError::InvalidSecret {
details: err.to_string(),
}
})?;
login_inner(mnemonic, path).await
}

pub(crate) async fn create_account_raw(path: &str) -> Result<(), VpnError> {
let (_, mnemonic) = VpnAccount::generate_new().map_err(VpnError::internal)?;
let storage = setup_account_storage(path).await?;
Expand Down
4 changes: 4 additions & 0 deletions nym-vpn-core/crates/nym-vpn-lib-uniffi/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ pub enum VpnError {
#[error("failed to parse mnemonic with error: {details}")]
InvalidMnemonic { details: String },

#[error("failed to parse secret with error: {details}")]
InvalidSecret { details: String },

#[error("invalid account storage path: {details}")]
InvalidAccountStoragePath { details: String },

Expand Down Expand Up @@ -120,6 +123,7 @@ impl From<AccountCommandError> for VpnError {
},
AccountCommandError::ExistingAccount => Self::ExistingAccount,
AccountCommandError::InvalidMnemonic(details) => Self::InvalidMnemonic { details },
AccountCommandError::InvalidSecret(details) => Self::InvalidSecret { details },
AccountCommandError::NyxdConnectionFailure(details) => {
Self::NyxdConnectionFailure { details }
}
Expand Down
21 changes: 15 additions & 6 deletions nym-vpn-core/crates/nym-vpn-lib-uniffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ use tokio::{runtime::Runtime, sync::Mutex};

use nym_vpn_lib_types::{
AccountControllerState, EntryPoint, ExitPoint, Gateway, GatewayType, Network,
NetworkCompatibility, ParsedAccountLinks, RegisterAccountResponse, SystemMessage, TunnelEvent,
UserAgent,
NetworkCompatibility, ParsedAccountLinks, PrivyDerivationMessage, RegisterAccountResponse,
StoreAccountRequest, SystemMessage, TunnelEvent, UserAgent,
};

use account::AccountControllerHandle;
Expand Down Expand Up @@ -271,8 +271,8 @@ pub fn getAccountLinksRaw(
/// Import the account mnemonic
#[allow(non_snake_case)]
#[uniffi::export]
pub fn login(mnemonic: String) -> Result<(), VpnError> {
RUNTIME.block_on(account::login(&mnemonic))
pub fn login(request: StoreAccountRequest) -> Result<(), VpnError> {
RUNTIME.block_on(account::login(&request))
}

/// Generate the account mnemonic locally and store it.
Expand All @@ -293,8 +293,8 @@ pub fn registerAccount(args: AccountRegistrationArgs) -> Result<RegisterAccountR
/// This is a version that can be called when the account controller is not running.
#[allow(non_snake_case)]
#[uniffi::export]
pub fn loginRaw(mnemonic: String, path: String) -> Result<(), VpnError> {
RUNTIME.block_on(account::raw::login_raw(&mnemonic, &path))
pub fn loginRaw(request: StoreAccountRequest, path: String) -> Result<(), VpnError> {
RUNTIME.block_on(account::raw::login_raw(&request, &path))
}

/// Generate the account mnemonic locally and store it.
Expand Down Expand Up @@ -430,6 +430,15 @@ pub fn getGateways(gw_type: GatewayType) -> Result<Vec<Gateway>, VpnError> {
RUNTIME.block_on(get_gateways(gw_type))
}

/// Get the message to be signed using the Privy signing API.
#[allow(non_snake_case)]
#[uniffi::export]
pub fn getPrivyDerivationMessage() -> PrivyDerivationMessage {
PrivyDerivationMessage {
message: nym_vpn_lib::login::privy::message_to_sign(),
}
}

async fn get_gateways(gw_type: GatewayType) -> Result<Vec<Gateway>, VpnError> {
gateway_cache::get_gateway_cache_handle()
.await?
Expand Down
2 changes: 2 additions & 0 deletions nym-vpn-core/crates/nym-vpn-lib/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ workspace = true

async-trait.workspace = true
bincode.workspace = true
bip39.workspace = true
bytes.workspace = true
futures.workspace = true
hex.workspace = true
hickory-resolver = { workspace = true, features = [
"tokio",
"tls-ring",
Expand Down
1 change: 1 addition & 0 deletions nym-vpn-core/crates/nym-vpn-lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ pub mod storage;

mod bandwidth_controller;
mod error;
pub mod login;
mod mixnet;
#[cfg(any(target_os = "ios", target_os = "android"))]
pub mod tunnel_provider;
Expand Down
11 changes: 11 additions & 0 deletions nym-vpn-core/crates/nym-vpn-lib/src/login/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright 2025 - Nym Technologies SA <[email protected]>
// SPDX-License-Identifier: GPL-3.0-only

#[derive(thiserror::Error, Debug, Clone, PartialEq)]
pub enum LoginError {
#[error(transparent)]
Bip39(#[from] bip39::Error),

#[error(transparent)]
Hex(#[from] hex::FromHexError),
}
69 changes: 69 additions & 0 deletions nym-vpn-core/crates/nym-vpn-lib/src/login/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright 2025 - Nym Technologies SA <[email protected]>
// SPDX-License-Identifier: GPL-3.0-only

use error::LoginError;
use nym_vpn_lib_types::StoreAccountRequest;
use nym_vpn_store::account::Mnemonic;

pub mod error;
pub mod privy;

pub fn parse_account_request(request: &StoreAccountRequest) -> Result<Mnemonic, LoginError> {
let mnemonic = match request {
StoreAccountRequest::Vpn { mnemonic } | StoreAccountRequest::Decentralised { mnemonic } => {
Mnemonic::parse(mnemonic)?
}
StoreAccountRequest::Privy { hex_signature } => {
privy::hex_signature_to_mnemonic(hex_signature)?
}
};

Ok(mnemonic)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parse_vpn_mnemonic() {
let mnemonic = Mnemonic::generate(24).unwrap();
let parsed_mnemonic = parse_account_request(&StoreAccountRequest::Vpn {
mnemonic: mnemonic.to_string(),
})
.unwrap();
assert_eq!(mnemonic, parsed_mnemonic);

assert!(
parse_account_request(&StoreAccountRequest::Vpn {
mnemonic: String::from("invalid mnemonic")
})
.is_err()
);
}

#[test]
fn parse_decentralised_mnemonic() {
let mnemonic = Mnemonic::generate(24).unwrap();
let parsed_mnemonic = parse_account_request(&StoreAccountRequest::Decentralised {
mnemonic: mnemonic.to_string(),
})
.unwrap();
assert_eq!(mnemonic, parsed_mnemonic);

assert!(
parse_account_request(&StoreAccountRequest::Decentralised {
mnemonic: String::from("invalid mnemonic")
})
.is_err()
);
}

#[test]
fn parse_hex_signature() {
let hex_signature = String::from(
"a564a87ccbed5cb5be4929201e555f5b5e26cb01d300d621520d724e57c582c33fa374caf21fd0c5e3118d70d14894845a32acfee47da7f347a0b9a57cba07931c",
);
assert!(parse_account_request(&StoreAccountRequest::Privy { hex_signature }).is_ok());
}
}
37 changes: 37 additions & 0 deletions nym-vpn-core/crates/nym-vpn-lib/src/login/privy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright 2025 - Nym Technologies SA <[email protected]>
// SPDX-License-Identifier: GPL-3.0-only

use nym_crypto::Digest;
use nym_validator_client::nyxd::bip32::secp256k1::sha2::Sha256;
use nym_vpn_store::account::Mnemonic;

use super::error::LoginError;

pub const PRIVY_DERIVATION_MESSAGE: &str = "DeriveAccount:NymVPN";

pub fn message_to_sign() -> String {
hex::encode(PRIVY_DERIVATION_MESSAGE.as_bytes())
}

pub fn hex_signature_to_mnemonic(hex_signature: &str) -> Result<Mnemonic, LoginError> {
let bytes_signature = hex::decode(hex_signature)?;

let mut hasher = Sha256::new();
hasher.update(&bytes_signature);
let hashed_signature = hasher.finalize();

let mnemonic = Mnemonic::from_entropy(&hashed_signature)?;

Ok(mnemonic)
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn parse_hex_signature() {
assert!(hex_signature_to_mnemonic("deadbeef").is_ok());
assert!(hex_signature_to_mnemonic("invalidhex").is_err());
}
}
Loading
Loading