diff --git a/nym-vpn-app/src-tauri/src/error.rs b/nym-vpn-app/src-tauri/src/error.rs index d2c60662ea..0ddc725df9 100644 --- a/nym-vpn-app/src-tauri/src/error.rs +++ b/nym-vpn-app/src-tauri/src/error.rs @@ -129,6 +129,7 @@ pub enum ErrorKey { MixnetNoBandwidth, // Some specific account management errors AccountInvalidMnemonic, + AccountInvalidSecret, NoAccountStored, NoDeviceStored, ExistingAccount, diff --git a/nym-vpn-app/src-tauri/src/vpnd/account_error.rs b/nym-vpn-app/src-tauri/src/vpnd/account_error.rs index 9e3ad84caf..e89da02bd4 100644 --- a/nym-vpn-app/src-tauri/src/vpnd/account_error.rs +++ b/nym-vpn-app/src-tauri/src/vpnd/account_error.rs @@ -33,6 +33,11 @@ impl From 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) } diff --git a/nym-vpn-app/src-tauri/src/vpnd/client.rs b/nym-vpn-app/src-tauri/src/vpnd/client.rs index f5748de051..78075a583a 100644 --- a/nym-vpn-app/src-tauri/src/vpnd/client.rs +++ b/nym-vpn-app/src-tauri/src/vpnd/client.rs @@ -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::{ diff --git a/nym-vpn-core/CHANGELOG.md b/nym-vpn-core/CHANGELOG.md index 88e45a882b..42b2d4e642 100644 --- a/nym-vpn-core/CHANGELOG.md +++ b/nym-vpn-core/CHANGELOG.md @@ -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 diff --git a/nym-vpn-core/Cargo.lock b/nym-vpn-core/Cargo.lock index 7035bc4404..869eea4747 100644 --- a/nym-vpn-core/Cargo.lock +++ b/nym-vpn-core/Cargo.lock @@ -5478,12 +5478,14 @@ dependencies = [ "async-trait", "base64 0.22.1", "bincode", + "bip39", "bs58", "bytes", "debounced", "dispatch2", "ed25519-dalek", "futures", + "hex", "hickory-resolver", "hickory-server", "ipnetwork", diff --git a/nym-vpn-core/Cargo.toml b/nym-vpn-core/Cargo.toml index 1f76e0ef07..5b7f30b84d 100644 --- a/nym-vpn-core/Cargo.toml +++ b/nym-vpn-core/Cargo.toml @@ -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" } @@ -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" } diff --git a/nym-vpn-core/crates/nym-vpn-lib-types/src/account/mod.rs b/nym-vpn-core/crates/nym-vpn-lib-types/src/account/mod.rs index 23201a1cba..1c33c730a1 100644 --- a/nym-vpn-core/crates/nym-vpn-lib-types/src/account/mod.rs +++ b/nym-vpn-core/crates/nym-vpn-lib-types/src/account/mod.rs @@ -64,6 +64,9 @@ pub enum AccountCommandError { #[error("invalid mnemonic: {0}")] InvalidMnemonic(String), + + #[error("invalid secret: {0}")] + InvalidSecret(String), } impl AccountCommandError { diff --git a/nym-vpn-core/crates/nym-vpn-lib-types/src/lib.rs b/nym-vpn-core/crates/nym-vpn-lib-types/src/lib.rs index a4c4db51d3..588c1490f0 100644 --- a/nym-vpn-core/crates/nym-vpn-lib-types/src/lib.rs +++ b/nym-vpn-core/crates/nym-vpn-lib-types/src/lib.rs @@ -39,6 +39,7 @@ mod gateway; mod log_path; mod network; mod network_stats; +mod privy; mod rpc_requests; mod service; mod socks5; @@ -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, diff --git a/nym-vpn-core/crates/nym-vpn-lib-types/src/privy.rs b/nym-vpn-core/crates/nym-vpn-lib-types/src/privy.rs new file mode 100644 index 0000000000..3e1067dbe4 --- /dev/null +++ b/nym-vpn-core/crates/nym-vpn-lib-types/src/privy.rs @@ -0,0 +1,16 @@ +// Copyright 2025 - Nym Technologies SA +// 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, +} diff --git a/nym-vpn-core/crates/nym-vpn-lib-types/src/rpc_requests.rs b/nym-vpn-core/crates/nym-vpn-lib-types/src/rpc_requests.rs index 447a1eb210..88c2dccc21 100644 --- a/nym-vpn-core/crates/nym-vpn-lib-types/src/rpc_requests.rs +++ b/nym-vpn-core/crates/nym-vpn-lib-types/src/rpc_requests.rs @@ -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") diff --git a/nym-vpn-core/crates/nym-vpn-lib-uniffi/src/account.rs b/nym-vpn-core/crates/nym-vpn-lib-uniffi/src/account.rs index e420acf6b1..12bc2c8a5c 100644 --- a/nym-vpn-core/crates/nym-vpn-lib-uniffi/src/account.rs +++ b/nym-vpn-core/crates/nym-vpn-lib-uniffi/src/account.rs @@ -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, @@ -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::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()) @@ -313,8 +311,7 @@ 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?; @@ -322,6 +319,18 @@ pub(crate) mod raw { 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?; diff --git a/nym-vpn-core/crates/nym-vpn-lib-uniffi/src/error.rs b/nym-vpn-core/crates/nym-vpn-lib-uniffi/src/error.rs index 08d98dd758..2e3fd32149 100644 --- a/nym-vpn-core/crates/nym-vpn-lib-uniffi/src/error.rs +++ b/nym-vpn-core/crates/nym-vpn-lib-uniffi/src/error.rs @@ -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 }, @@ -120,6 +123,7 @@ impl From for VpnError { }, AccountCommandError::ExistingAccount => Self::ExistingAccount, AccountCommandError::InvalidMnemonic(details) => Self::InvalidMnemonic { details }, + AccountCommandError::InvalidSecret(details) => Self::InvalidSecret { details }, AccountCommandError::NyxdConnectionFailure(details) => { Self::NyxdConnectionFailure { details } } diff --git a/nym-vpn-core/crates/nym-vpn-lib-uniffi/src/lib.rs b/nym-vpn-core/crates/nym-vpn-lib-uniffi/src/lib.rs index 85a7ff421b..213bf66e98 100644 --- a/nym-vpn-core/crates/nym-vpn-lib-uniffi/src/lib.rs +++ b/nym-vpn-core/crates/nym-vpn-lib-uniffi/src/lib.rs @@ -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; @@ -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. @@ -293,8 +293,8 @@ pub fn registerAccount(args: AccountRegistrationArgs) -> Result 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. @@ -430,6 +430,15 @@ pub fn getGateways(gw_type: GatewayType) -> Result, 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, VpnError> { gateway_cache::get_gateway_cache_handle() .await? diff --git a/nym-vpn-core/crates/nym-vpn-lib/Cargo.toml b/nym-vpn-core/crates/nym-vpn-lib/Cargo.toml index 563f40c655..3a8abcfa80 100644 --- a/nym-vpn-core/crates/nym-vpn-lib/Cargo.toml +++ b/nym-vpn-core/crates/nym-vpn-lib/Cargo.toml @@ -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", diff --git a/nym-vpn-core/crates/nym-vpn-lib/src/lib.rs b/nym-vpn-core/crates/nym-vpn-lib/src/lib.rs index b4c2ad9284..f1a8a6868d 100644 --- a/nym-vpn-core/crates/nym-vpn-lib/src/lib.rs +++ b/nym-vpn-core/crates/nym-vpn-lib/src/lib.rs @@ -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; diff --git a/nym-vpn-core/crates/nym-vpn-lib/src/login/error.rs b/nym-vpn-core/crates/nym-vpn-lib/src/login/error.rs new file mode 100644 index 0000000000..578ff49bd7 --- /dev/null +++ b/nym-vpn-core/crates/nym-vpn-lib/src/login/error.rs @@ -0,0 +1,11 @@ +// Copyright 2025 - Nym Technologies SA +// 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), +} diff --git a/nym-vpn-core/crates/nym-vpn-lib/src/login/mod.rs b/nym-vpn-core/crates/nym-vpn-lib/src/login/mod.rs new file mode 100644 index 0000000000..d127a7ed68 --- /dev/null +++ b/nym-vpn-core/crates/nym-vpn-lib/src/login/mod.rs @@ -0,0 +1,69 @@ +// Copyright 2025 - Nym Technologies SA +// 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 { + 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()); + } +} diff --git a/nym-vpn-core/crates/nym-vpn-lib/src/login/privy.rs b/nym-vpn-core/crates/nym-vpn-lib/src/login/privy.rs new file mode 100644 index 0000000000..cda815c80f --- /dev/null +++ b/nym-vpn-core/crates/nym-vpn-lib/src/login/privy.rs @@ -0,0 +1,37 @@ +// Copyright 2025 - Nym Technologies SA +// 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 { + 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()); + } +} diff --git a/nym-vpn-core/crates/nym-vpn-proto/proto/nym_vpn_service.proto b/nym-vpn-core/crates/nym-vpn-proto/proto/nym_vpn_service.proto index d1c5c113c5..5cae9034cc 100644 --- a/nym-vpn-core/crates/nym-vpn-proto/proto/nym_vpn_service.proto +++ b/nym-vpn-core/crates/nym-vpn-proto/proto/nym_vpn_service.proto @@ -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; @@ -682,10 +683,10 @@ message StoreAccountRequest { oneof request { VpnAccountStoreRequest vpn_account_store = 1; DecentralisedAccountStoreRequest decentralised_account_store = 2; + PrivyAccountStoreRequest privy_account_store = 3; } } -// VPN variant with mnemonic message VpnAccountStoreRequest { string mnemonic = 1; } @@ -694,6 +695,10 @@ message DecentralisedAccountStoreRequest { string mnemonic = 1; } +message PrivyAccountStoreRequest { + string hex_signature = 1; +} + message DecentralisedObtainTicketbooksRequest { uint64 amount = 1; } @@ -843,6 +848,10 @@ message NetworkStatisticsIdentity { string id = 2; } +message PrivyDerivationMessage { + string message = 1; +} + service NymVpnService { // Get info regarding the nym-vpnd in general, like version etc. rpc Info (google.protobuf.Empty) returns (InfoResponse) {} @@ -983,4 +992,7 @@ service NymVpnService { // Get socks5 proxy status rpc GetSocks5Status (google.protobuf.Empty) returns (Socks5Status) {} + + // Get Privy derivation message to be signed + rpc GetPrivyDerivationMessage (google.protobuf.Empty) returns (PrivyDerivationMessage) {} } diff --git a/nym-vpn-core/crates/nym-vpn-proto/src/conversions/account.rs b/nym-vpn-core/crates/nym-vpn-proto/src/conversions/account.rs index 5effaced34..f55165c203 100644 --- a/nym-vpn-core/crates/nym-vpn-proto/src/conversions/account.rs +++ b/nym-vpn-core/crates/nym-vpn-proto/src/conversions/account.rs @@ -31,6 +31,9 @@ impl TryFrom 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) } @@ -132,6 +135,11 @@ impl From 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), diff --git a/nym-vpn-core/crates/nym-vpn-proto/src/conversions/network_stats.rs b/nym-vpn-core/crates/nym-vpn-proto/src/conversions/network_stats.rs index f8142b9446..ef8a31e86a 100644 --- a/nym-vpn-core/crates/nym-vpn-proto/src/conversions/network_stats.rs +++ b/nym-vpn-core/crates/nym-vpn-proto/src/conversions/network_stats.rs @@ -28,6 +28,7 @@ impl From for nym_vpn_lib_types::NetworkStatis } } } + impl From for proto::NetworkStatisticsIdentity { fn from(identity: nym_vpn_lib_types::NetworkStatisticsIdentity) -> Self { Self { @@ -36,3 +37,19 @@ impl From for proto::NetworkStatis } } } + +impl From for nym_vpn_lib_types::PrivyDerivationMessage { + fn from(message: proto::PrivyDerivationMessage) -> Self { + Self { + message: message.message, + } + } +} + +impl From for proto::PrivyDerivationMessage { + fn from(message: nym_vpn_lib_types::PrivyDerivationMessage) -> Self { + Self { + message: message.message, + } + } +} diff --git a/nym-vpn-core/crates/nym-vpn-proto/src/conversions/vpnd.rs b/nym-vpn-core/crates/nym-vpn-proto/src/conversions/vpnd.rs index 4b82c2e091..0f38d847a3 100644 --- a/nym-vpn-core/crates/nym-vpn-proto/src/conversions/vpnd.rs +++ b/nym-vpn-core/crates/nym-vpn-proto/src/conversions/vpnd.rs @@ -475,7 +475,7 @@ impl TryFrom for StoreAccountRequest { Ok(match request { proto::store_account_request::Request::VpnAccountStore(account) => { - StoreAccountRequest::Vpn { + nym_vpn_lib_types::StoreAccountRequest::Vpn { mnemonic: account.mnemonic, } } @@ -484,6 +484,11 @@ impl TryFrom for StoreAccountRequest { mnemonic: account.mnemonic, } } + proto::store_account_request::Request::PrivyAccountStore(account) => { + nym_vpn_lib_types::StoreAccountRequest::Privy { + hex_signature: account.hex_signature, + } + } }) } } @@ -496,11 +501,16 @@ impl From for proto::StoreAccountRequest { proto::VpnAccountStoreRequest { mnemonic }, ) } - nym_vpn_lib_types::StoreAccountRequest::Decentralised { mnemonic } => { + StoreAccountRequest::Decentralised { mnemonic } => { proto::store_account_request::Request::DecentralisedAccountStore( proto::DecentralisedAccountStoreRequest { mnemonic }, ) } + StoreAccountRequest::Privy { hex_signature } => { + proto::store_account_request::Request::PrivyAccountStore( + proto::PrivyAccountStoreRequest { hex_signature }, + ) + } }; proto::StoreAccountRequest { diff --git a/nym-vpn-core/crates/nym-vpn-proto/src/rpc_client.rs b/nym-vpn-core/crates/nym-vpn-proto/src/rpc_client.rs index 6cbe9a3101..beb4483806 100644 --- a/nym-vpn-core/crates/nym-vpn-proto/src/rpc_client.rs +++ b/nym-vpn-core/crates/nym-vpn-proto/src/rpc_client.rs @@ -5,8 +5,8 @@ use nym_vpn_lib_types::{ AccountBalanceResponse, AccountCommandResponse, AccountControllerState, AvailableTickets, EntryPoint, ExitPoint, FeatureFlags, Gateway, GatewayFilters, HttpRpcSettings, ListGatewaysOptions, LogPath, NetworkCompatibility, NetworkStatisticsIdentity, NymVpnDevice, - NymVpnUsage, ParsedAccountLinks, Socks5Settings, Socks5Status, StoreAccountRequest, - SystemMessage, TunnelEvent, TunnelState, VpnServiceConfig, VpnServiceInfo, + NymVpnUsage, ParsedAccountLinks, PrivyDerivationMessage, Socks5Settings, Socks5Status, + StoreAccountRequest, SystemMessage, TunnelEvent, TunnelState, VpnServiceConfig, VpnServiceInfo, }; use std::{net::IpAddr, path::PathBuf}; use tokio_stream::{Stream, StreamExt}; @@ -616,6 +616,17 @@ impl RpcClient { .map_err(Error::Rpc)?; Ok(NetworkStatisticsIdentity::from(response)) } + + pub async fn get_privy_derivation_message(&mut self) -> Result { + let response = self + .0 + .get_privy_derivation_message(()) + .await + .map(|v| v.into_inner()) + .map_err(Error::Rpc)?; + + Ok(PrivyDerivationMessage::from(response)) + } } pub fn get_rpc_socket_path() -> PathBuf { diff --git a/nym-vpn-core/crates/nym-vpn-rpc-uniffi/src/lib.rs b/nym-vpn-core/crates/nym-vpn-rpc-uniffi/src/lib.rs index d5857b7f9b..9cbff9eee2 100644 --- a/nym-vpn-core/crates/nym-vpn-rpc-uniffi/src/lib.rs +++ b/nym-vpn-core/crates/nym-vpn-rpc-uniffi/src/lib.rs @@ -14,8 +14,8 @@ use tokio_util::sync::CancellationToken; use nym_vpn_lib_types::{ AccountCommandError, AccountControllerState, EntryPoint, ExitPoint, FeatureFlags, Gateway, GatewayType, HttpRpcSettings, LogPath, NetworkCompatibility, NymVpnDevice, NymVpnUsage, - ParsedAccountLinks, Socks5Settings, Socks5Status, SystemMessage, TunnelEvent, TunnelState, - VpnServiceConfig, VpnServiceInfo, + ParsedAccountLinks, PrivyDerivationMessage, Socks5Settings, Socks5Status, StoreAccountRequest, + SystemMessage, TunnelEvent, TunnelState, VpnServiceConfig, VpnServiceInfo, }; uniffi::use_remote_type!(nym_vpn_lib_types::IpAddr); @@ -177,12 +177,8 @@ impl RpcClient { Ok(gateways) } - pub async fn store_account(&self, mnemonic: String) -> Result<()> { - let response = self - .inner - .clone() - .store_account(nym_vpn_lib_types::StoreAccountRequest::Vpn { mnemonic }) - .await?; + pub async fn store_account(&self, request: StoreAccountRequest) -> Result<()> { + let response = self.inner.clone().store_account(request).await?; if let Some(err) = response.error { Err(RpcError::new(InnerRpcError::AccountCommand(Arc::new(err)))) @@ -323,6 +319,11 @@ impl RpcClient { let status = self.inner.clone().get_socks5_status().await?; Ok(status) } + + pub async fn get_privy_derivation_message(&self) -> Result { + let message = self.inner.clone().get_privy_derivation_message().await?; + Ok(message) + } } #[derive(Debug)] diff --git a/nym-vpn-core/crates/nym-vpnc/src/commands/account.rs b/nym-vpn-core/crates/nym-vpnc/src/commands/account.rs index cd57fe220a..452f8a85f0 100644 --- a/nym-vpn-core/crates/nym-vpnc/src/commands/account.rs +++ b/nym-vpn-core/crates/nym-vpnc/src/commands/account.rs @@ -11,12 +11,12 @@ pub enum Command { Get, /// Login with mnemonic Set { - /// Mnemonic phrase + /// Mnemonic phrase (for Api or Decentralised modes) or private key hex encoded (for Privy mode) #[arg(index = 1)] - mnemonic: String, + secret: String, /// Account mode - #[clap(long, default_value_t = VpnAccountMode::Api)] - mode: VpnAccountMode, + #[clap(long, default_value_t = VpnAccount::Api)] + mode: VpnAccount, }, /// Forget account Forget, @@ -61,11 +61,14 @@ impl Command { println!("Account state: {account_state:?}"); Ok(()) } - Command::Set { mnemonic, mode } => { + Command::Set { secret, mode } => { let request = match mode { - VpnAccountMode::Api => StoreAccountRequest::Vpn { mnemonic }, - VpnAccountMode::Decentralised => { - StoreAccountRequest::Decentralised { mnemonic } + VpnAccount::Api => StoreAccountRequest::Vpn { mnemonic: secret }, + VpnAccount::Privy => StoreAccountRequest::Privy { + hex_signature: secret, + }, + VpnAccount::Decentralised => { + StoreAccountRequest::Decentralised { mnemonic: secret } } }; let response = rpc_client.store_account(request).await?; @@ -161,16 +164,18 @@ impl Command { } #[derive(Debug, Clone, clap::ValueEnum)] -pub enum VpnAccountMode { +pub enum VpnAccount { Api, + Privy, Decentralised, } -impl std::fmt::Display for VpnAccountMode { +impl std::fmt::Display for VpnAccount { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - VpnAccountMode::Api => write!(f, "api"), - VpnAccountMode::Decentralised => write!(f, "decentralised"), + VpnAccount::Api => write!(f, "api"), + VpnAccount::Privy => write!(f, "privy"), + VpnAccount::Decentralised => write!(f, "decentralised"), } } } diff --git a/nym-vpn-core/crates/nym-vpnd/src/command_interface.rs b/nym-vpn-core/crates/nym-vpnd/src/command_interface.rs index 4b35eacf49..d3b6a5f1a4 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/command_interface.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/command_interface.rs @@ -844,6 +844,15 @@ impl NymVpnService for CommandInterface { Ok(tonic::Response::new(proto_status)) } + + async fn get_privy_derivation_message( + &self, + _: tonic::Request<()>, + ) -> Result> { + Ok(tonic::Response::new(proto::PrivyDerivationMessage { + message: nym_vpn_lib::login::privy::message_to_sign(), + })) + } } pub async fn start_command_interface( diff --git a/nym-vpn-core/crates/nym-vpnd/src/service/vpn_service.rs b/nym-vpn-core/crates/nym-vpnd/src/service/vpn_service.rs index 2b72da209c..abaf825a68 100644 --- a/nym-vpn-core/crates/nym-vpnd/src/service/vpn_service.rs +++ b/nym-vpn-core/crates/nym-vpnd/src/service/vpn_service.rs @@ -1,7 +1,6 @@ // Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only -use bip39::Mnemonic; use futures::{FutureExt, StreamExt, future::Fuse, pin_mut}; use std::{net::IpAddr, path::PathBuf, pin::Pin, sync::Arc}; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; @@ -1284,24 +1283,19 @@ impl NymVpnService { &mut self, store_request: StoreAccountRequest, ) -> Result<(), AccountCommandError> { - match store_request { - StoreAccountRequest::Vpn { mnemonic } => { - let mnemonic = Mnemonic::parse::(mnemonic) - .map_err(|err| AccountCommandError::InvalidMnemonic(err.to_string()))?; - self.account_command_tx - .store_account(StorableAccount::new(mnemonic, StoredAccountMode::Api)) - .await - } - StoreAccountRequest::Decentralised { mnemonic } => { - let mnemonic = Mnemonic::parse::(mnemonic) - .map_err(|err| AccountCommandError::InvalidMnemonic(err.to_string()))?; - self.account_command_tx - .store_account(StorableAccount::new( - mnemonic, - StoredAccountMode::Decentralised, - )) - .await - } + let mnemonic = nym_vpn_lib::login::parse_account_request(&store_request) + .map_err(|err| AccountCommandError::InvalidSecret(err.to_string()))?; + if store_request.centralised() { + self.account_command_tx + .store_account(StorableAccount::new(mnemonic, StoredAccountMode::Api)) + .await + } else { + self.account_command_tx + .store_account(StorableAccount::new( + mnemonic, + StoredAccountMode::Decentralised, + )) + .await } }