Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 4 additions & 2 deletions 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 Expand Up @@ -390,7 +390,9 @@ impl VpndClient {
let mut vpnd = self.vpnd().await?;

let response = vpnd
.store_account(lib::StoreAccountRequest::Vpn { mnemonic })
.store_account(lib::StoreAccountRequest::Vpn {
secret: lib::LoginSecret::Mnemonic(mnemonic),
})
.await
.map_err(VpndError::RpcClient)
.inspect_err(|e| {
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 private key in addition to mnemonic (https://github.com/nymtech/nym-vpn-client/pull/4117)

### Fixed

Expand Down
13 changes: 13 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.

4 changes: 3 additions & 1 deletion nym-vpn-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ members = [
"crates/nym-macos",
"crates/nym-offline-monitor",
"crates/nym-platform-metadata",
"crates/nym-utils",
"crates/nym-routing",
"crates/nym-setup",
"crates/nym-statistics",
Expand Down Expand Up @@ -189,6 +190,8 @@ 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-utils = { path = "crates/nym-utils" }
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 +205,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
23 changes: 23 additions & 0 deletions nym-vpn-core/crates/nym-utils/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "nym-utils"
version.workspace = true
authors.workspace = true
repository.workspace = true
homepage.workspace = true
documentation.workspace = true
edition.workspace = true
license.workspace = true

[lints]
workspace = true

[dependencies]
bip39.workspace = true
hex.workspace = true
sha2.workspace = true
thiserror.workspace = true

nym-vpn-lib-types.workspace = true

[dev-dependencies]
bip39 = { workspace = true, features = ["rand"] }
8 changes: 8 additions & 0 deletions nym-vpn-core/crates/nym-utils/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#[derive(thiserror::Error, Debug, Clone, PartialEq)]
pub enum UtilsError {
#[error(transparent)]
Hex(#[from] hex::FromHexError),

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

use bip39::Mnemonic;
use nym_vpn_lib_types::LoginSecret;
use sha2::{Digest, Sha256};

use crate::error::UtilsError;

pub mod error;

pub fn parse_secret(secret: &LoginSecret) -> Result<Mnemonic, UtilsError> {
let bytes_signature = match secret {
LoginSecret::Mnemonic(mnemonic) => return Ok(Mnemonic::parse(mnemonic)?),
LoginSecret::PrivyHexSignature(hex_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_mnemonic() {
let mnemonic = Mnemonic::generate(24).unwrap();
let parsed_mnemonic = parse_secret(&LoginSecret::Mnemonic(mnemonic.to_string())).unwrap();
assert_eq!(mnemonic, parsed_mnemonic);

assert!(parse_secret(&LoginSecret::Mnemonic(String::from("invalid mnemonic"))).is_err());
}

#[test]
fn parse_hex_signature() {
let hex_signature = String::from(
"a564a87ccbed5cb5be4929201e555f5b5e26cb01d300d621520d724e57c582c33fa374caf21fd0c5e3118d70d14894845a32acfee47da7f347a0b9a57cba07931c",
);

assert!(parse_secret(&LoginSecret::PrivyHexSignature(hex_signature)).is_ok());
assert!(parse_secret(&LoginSecret::PrivyHexSignature(String::from("invalidhex"))).is_err());
assert!(parse_secret(&LoginSecret::PrivyHexSignature(String::from("deadbeef"))).is_ok());
}
}
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: 1 addition & 1 deletion nym-vpn-core/crates/nym-vpn-lib-types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ pub use network::{
pub use network_stats::{NetworkStatisticsConfig, NetworkStatisticsIdentity};
pub use rpc_requests::{
AccountBalanceResponse, AccountCommandResponse, Coin, DecentralisedObtainTicketbooksRequest,
ListGatewaysOptions, StoreAccountRequest,
ListGatewaysOptions, LoginSecret, StoreAccountRequest,
};
pub use service::{TargetState, VpnServiceConfig, VpnServiceInfo};
pub use socks5::{HttpRpcSettings, Socks5Settings, Socks5State, Socks5Status};
Expand Down
19 changes: 17 additions & 2 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 @@ -16,11 +16,26 @@ pub struct ListGatewaysOptions {
pub user_agent: Option<UserAgent>,
}

#[derive(zeroize::Zeroize)]
#[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Enum))]
#[cfg_attr(
feature = "typescript-bindings",
derive(TS),
ts(export),
ts(export_to = "bindings.ts")
)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "typescript-bindings", serde(rename_all = "camelCase"))]
pub enum LoginSecret {
Mnemonic(String),
PrivyHexSignature(String),
}

#[derive(zeroize::Zeroize)]
#[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Enum))]
pub enum StoreAccountRequest {
Vpn { mnemonic: String },
Decentralised { mnemonic: String },
Vpn { secret: LoginSecret },
Decentralised { secret: LoginSecret },
}

impl std::fmt::Debug for StoreAccountRequest {
Expand Down
1 change: 1 addition & 0 deletions nym-vpn-core/crates/nym-vpn-lib-uniffi/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ nym-http-api-client.workspace = true
nym-common.workspace = true
nym-gateway-directory.workspace = true
nym-platform-metadata.workspace = true
nym-utils.workspace = true

[target.'cfg(target_os = "macos")'.dependencies]
nym-vpn-proto = { workspace = true, features = ["rpc_client"] }
Expand Down
22 changes: 12 additions & 10 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, LoginSecret, RegisterAccountResponse};
use nym_vpn_network_config::Network;
use nym_vpn_store::{
account::Mnemonic,
Expand Down Expand Up @@ -198,14 +198,10 @@ 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 {
pub(super) async fn login(secret: &LoginSecret) -> Result<(), VpnError> {
let mnemonic = nym_utils::parse_secret(secret).map_err(|err| VpnError::InvalidSecret {
details: err.to_string(),
})
}

pub(super) async fn login(mnemonic: &str) -> Result<(), VpnError> {
let mnemonic = parse_mnemonic(mnemonic).await?;
})?;
get_command_sender()
.await?
.store_account(mnemonic.into())
Expand Down Expand Up @@ -313,15 +309,21 @@ 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(secret: &LoginSecret, path: &str) -> Result<(), VpnError> {
let mnemonic = nym_utils::parse_secret(secret).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
10 changes: 5 additions & 5 deletions nym-vpn-core/crates/nym-vpn-lib-uniffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ use sentry::ClientInitGuard;
use tokio::{runtime::Runtime, sync::Mutex};

use nym_vpn_lib_types::{
AccountControllerState, EntryPoint, ExitPoint, Gateway, GatewayType, Network,
AccountControllerState, EntryPoint, ExitPoint, Gateway, GatewayType, LoginSecret, Network,
NetworkCompatibility, ParsedAccountLinks, RegisterAccountResponse, SystemMessage, TunnelEvent,
UserAgent,
};
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(secret: LoginSecret) -> Result<(), VpnError> {
RUNTIME.block_on(account::login(&secret))
}

/// 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(secret: LoginSecret, path: String) -> Result<(), VpnError> {
RUNTIME.block_on(account::raw::login_raw(&secret, &path))
}

/// Generate the account mnemonic locally and store it.
Expand Down
12 changes: 10 additions & 2 deletions nym-vpn-core/crates/nym-vpn-proto/proto/nym_vpn_service.proto
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,7 @@ message AccountCommandError {
bool no_device_stored = 6;
bool existing_account = 7;
bool offline = 8;
string invalid_secret = 9;
string invalid_mnemonic = 10;
string nyxd_connection_failure = 11;
string nyxd_query_failure = 12;
Expand Down Expand Up @@ -685,13 +686,20 @@ message StoreAccountRequest {
}
}

message LoginSecret {
oneof login_type {
string mnemonic = 1;
string private_key_hex = 2;
}
}

// VPN variant with mnemonic
message VpnAccountStoreRequest {
string mnemonic = 1;
LoginSecret secret = 1;
}

message DecentralisedAccountStoreRequest {
string mnemonic = 1;
LoginSecret secret = 1;
}

message DecentralisedObtainTicketbooksRequest {
Expand Down
8 changes: 8 additions & 0 deletions nym-vpn-core/crates/nym-vpn-proto/src/conversions/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ impl TryFrom<proto::AccountCommandError> for AccountCommandError {
proto::account_command_error::ErrorDetail::InvalidMnemonic(message) => {
Self::InvalidMnemonic(message)
}
proto::account_command_error::ErrorDetail::InvalidSecret(message) => {
Self::InvalidSecret(message)
}
proto::account_command_error::ErrorDetail::NyxdConnectionFailure(err) => {
Self::NyxdConnectionFailure(err)
}
Expand Down Expand Up @@ -132,6 +135,11 @@ impl From<AccountCommandError> for proto::AccountCommandError {
err,
)),
},
AccountCommandError::InvalidSecret(err) => proto::AccountCommandError {
error_detail: Some(proto::account_command_error::ErrorDetail::InvalidSecret(
err,
)),
},
AccountCommandError::NyxdConnectionFailure(err) => proto::AccountCommandError {
error_detail: Some(
proto::account_command_error::ErrorDetail::NyxdConnectionFailure(err),
Expand Down
Loading
Loading