diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 7a5a6fbda2f..aca3b3be989 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2171,6 +2171,7 @@ dependencies = [ "tw_hash", "tw_keypair", "tw_memory", + "tw_misc", "tw_number", "tw_proto", "tw_ton_sdk", diff --git a/rust/tw_hd_wallet/src/bip39/bip39_english.rs b/rust/tw_hd_wallet/src/bip39/bip39_english.rs index e84fd1fd1a8..ae0c461da23 100644 --- a/rust/tw_hd_wallet/src/bip39/bip39_english.rs +++ b/rust/tw_hd_wallet/src/bip39/bip39_english.rs @@ -18,7 +18,7 @@ lazy_static! { /// https://github.com/trustwallet/wallet-core/blob/43c92837db9f5d773f2545473f29c8a597d86de5/trezor-crypto/include/TrezorCrypto/bip39_english.h#L24-L367 /// https://github.com/dvc94ch/rust-bip39/blob/master/src/language/english.rs #[rustfmt::skip] -pub const BIP39_WORDS_LIST: [&'static str; 2048] = [ +pub const BIP39_WORDS_LIST: [&str; 2048] = [ "abandon", "ability", "able", diff --git a/rust/tw_hd_wallet/src/bip39/mod.rs b/rust/tw_hd_wallet/src/bip39/mod.rs index eee6bd1a05a..d0e1db66b2e 100644 --- a/rust/tw_hd_wallet/src/bip39/mod.rs +++ b/rust/tw_hd_wallet/src/bip39/mod.rs @@ -3,3 +3,7 @@ // Copyright © 2017 Trust Wallet. pub mod bip39_english; + +pub fn normalize_mnemonic(mnemonic: &str) -> String { + mnemonic.trim().to_string() +} diff --git a/rust/tw_hd_wallet/src/ton/mnemonic.rs b/rust/tw_hd_wallet/src/ton/mnemonic.rs index 011199141c8..2d0dbbf52a6 100644 --- a/rust/tw_hd_wallet/src/ton/mnemonic.rs +++ b/rust/tw_hd_wallet/src/ton/mnemonic.rs @@ -4,47 +4,37 @@ use crate::bip39::bip39_english::BIP39_WORDS_MAP; use crate::{WalletError, WalletResult}; -use zeroize::ZeroizeOnDrop; -#[derive(Debug, ZeroizeOnDrop)] -pub struct TonMnemonic(String); - -impl TonMnemonic { - pub const WORDS_LEN: usize = 24; - - /// Creates `TonMnemonic` from a 24-words string. - /// - /// Please note there can be a passphrase required to be used to derive a key-pair. - /// See [`TonWallet::new`]. - pub fn new(mnemonic: &str) -> WalletResult { - let normalized_mnemonic = mnemonic.trim().to_string(); - - let mut invalid = false; - let mut words_count = 0; - for word in normalized_mnemonic.split(" ") { - words_count += 1; - - // Although this operation is not security-critical, we aim for constant-time operation here as well - // (i.e., no early exit on match) - // - // We expect words in lowercase only. - // It's a responsibility of the WalletCore user to transform the mnemonic if needed. - if !BIP39_WORDS_MAP.contains_key(word) { - invalid = true; - } - } - - if invalid { - return Err(WalletError::InvalidMnemonicUnknownWord); - } - if words_count != TonMnemonic::WORDS_LEN { - return Err(WalletError::InvalidMnemonicWordCount); +pub const WORDS_LEN: usize = 24; + +/// Validates the given `mnemonic` string if it consists of 24 known words (see BIP39 words list). +/// +/// Please note there this function doesn't validate the mnemonic but it words only. +/// See [`TonWallet::new`]. +pub fn validate_mnemonic_words(mnemonic: &str) -> WalletResult<()> { + let normalized_mnemonic = mnemonic.trim().to_string(); + + let mut invalid = false; + let mut words_count = 0; + for word in normalized_mnemonic.split(" ") { + words_count += 1; + + // Although this operation is not security-critical, we aim for constant-time operation here as well + // (i.e., no early exit on match) + // + // We expect words in lowercase only. + // It's a responsibility of the WalletCore user to transform the mnemonic if needed. + if !BIP39_WORDS_MAP.contains_key(word) { + invalid = true; } - - Ok(TonMnemonic(normalized_mnemonic)) } - pub fn as_str(&self) -> &str { - &self.0 + if invalid { + return Err(WalletError::InvalidMnemonicUnknownWord); } + if words_count != WORDS_LEN { + return Err(WalletError::InvalidMnemonicWordCount); + } + + Ok(()) } diff --git a/rust/tw_hd_wallet/src/ton/mod.rs b/rust/tw_hd_wallet/src/ton/mod.rs index 9f580393c2f..22e9c5b7a44 100644 --- a/rust/tw_hd_wallet/src/ton/mod.rs +++ b/rust/tw_hd_wallet/src/ton/mod.rs @@ -2,7 +2,8 @@ // // Copyright © 2017 Trust Wallet. -use crate::ton::mnemonic::TonMnemonic; +use crate::bip39::normalize_mnemonic; +use crate::ton::mnemonic::validate_mnemonic_words; use crate::{WalletError, WalletResult}; use tw_hash::hmac::hmac_sha512; use tw_hash::pbkdf2::pbkdf2_hmac_sha512; @@ -22,11 +23,11 @@ const TON_BASIC_SEED_ROUNDS: u32 = 390; const TON_PASSPHRASE_SEED_SALT: &[u8] = b"TON fast seed version"; const TON_PASSPHRASE_SEED_ROUNDS: u32 = 1; -pub mod mnemonic; +mod mnemonic; -#[derive(ZeroizeOnDrop)] +#[derive(Debug, ZeroizeOnDrop)] pub struct TonWallet { - mnemonic: TonMnemonic, + mnemonic: String, passphrase: Option, entropy: H512, seed: H512, @@ -37,7 +38,10 @@ impl TonWallet { /// Creates `TonWallet` while validating if there should or shouldn't be a passphrase. /// https://github.com/toncenter/tonweb-mnemonic/blob/a338a00d4ca0ed833431e0e49e4cfad766ac713c/src/functions/validate-mnemonic.ts#L20-L28 - pub fn new(mnemonic: TonMnemonic, passphrase: Option) -> WalletResult { + pub fn new(mnemonic: &str, passphrase: Option) -> WalletResult { + let mnemonic = normalize_mnemonic(mnemonic); + validate_mnemonic_words(&mnemonic)?; + let entropy = Self::ton_mnemonic_to_entropy(&mnemonic, passphrase.as_deref()); match passphrase { @@ -72,16 +76,16 @@ impl TonWallet { /// Whether the mnemonic can be used with a passphrase only. /// https://github.com/toncenter/tonweb-mnemonic/blob/a338a00d4ca0ed833431e0e49e4cfad766ac713c/src/functions/is-password-needed.ts#L5-L11 - fn is_password_needed(mnemonic: &TonMnemonic) -> bool { + fn is_password_needed(mnemonic: &str) -> bool { // Password mnemonic (without password) should be password seed, but not basic seed. let entropy = Self::ton_mnemonic_to_entropy(mnemonic, None); is_password_seed(&entropy) && !is_basic_seed(&entropy) } /// https://github.com/toncenter/tonweb-mnemonic/blob/a338a00d4ca0ed833431e0e49e4cfad766ac713c/src/functions/common.ts#L20-L23 - fn ton_mnemonic_to_entropy(mnemonic: &TonMnemonic, passphrase: Option<&str>) -> H512 { + fn ton_mnemonic_to_entropy(mnemonic: &str, passphrase: Option<&str>) -> H512 { let passphrase_bytes = passphrase.map(str::as_bytes).unwrap_or(&[]); - let entropy = hmac_sha512(mnemonic.as_str().as_bytes(), passphrase_bytes); + let entropy = hmac_sha512(mnemonic.as_bytes(), passphrase_bytes); H512::try_from(entropy.as_slice()).expect("hmac_sha512 must return 64 bytes") } } diff --git a/rust/tw_hd_wallet/tests/ton_mnemonic.rs b/rust/tw_hd_wallet/tests/ton_mnemonic.rs index 02a5f777443..3125140ad90 100644 --- a/rust/tw_hd_wallet/tests/ton_mnemonic.rs +++ b/rust/tw_hd_wallet/tests/ton_mnemonic.rs @@ -3,7 +3,6 @@ // Copyright © 2017 Trust Wallet. use tw_encoding::hex::ToHex; -use tw_hd_wallet::ton::mnemonic::TonMnemonic; use tw_hd_wallet::ton::TonWallet; use tw_hd_wallet::WalletError; use tw_keypair::traits::KeyPairTrait; @@ -23,8 +22,7 @@ fn mnemonic_to_keypair_impl(input: MnemonicTest) { Some(input.passphrase.to_string()) }; - let mnemonic = TonMnemonic::new(input.mnemonic).unwrap(); - let wallet = TonWallet::new(mnemonic, passphrase).unwrap(); + let wallet = TonWallet::new(input.mnemonic, passphrase).unwrap(); let key_pair = wallet.to_key_pair(); assert_eq!( @@ -51,9 +49,8 @@ fn mnemonic_to_keypair_error(input: MnemonicErrorTest) { Some(input.passphrase.to_string()) }; - let mnemonic = TonMnemonic::new(input.mnemonic).unwrap(); assert!( - TonWallet::new(mnemonic, passphrase).is_err(), + TonWallet::new(input.mnemonic, passphrase).is_err(), "Expected an error" ); } @@ -147,13 +144,13 @@ fn test_mnemonic_to_keypair_error_expected_no_passphrase() { #[test] fn test_invalid_mnemonic() { // 24 words mnemonic is supported only. - let error = TonMnemonic::new("cost dash dress stove morning robust group affair stomach vacant route volume yellow salute laugh").unwrap_err(); + let error = TonWallet::new("cost dash dress stove morning robust group affair stomach vacant route volume yellow salute laugh", None).unwrap_err(); assert_eq!(error, WalletError::InvalidMnemonicWordCount); - let error = TonMnemonic::new("foo bar oooo edit wash faint patient cancel roof edit silly battle half engine reunion hotel joy fan unhappy oil alone sense empty mesh").unwrap_err(); + let error = TonWallet::new("foo bar oooo edit wash faint patient cancel roof edit silly battle half engine reunion hotel joy fan unhappy oil alone sense empty mesh", None).unwrap_err(); assert_eq!(error, WalletError::InvalidMnemonicUnknownWord); // Upper-case mnemonic is not allowed. - let error = TonMnemonic::new("TAIL SWING SUGGEST EDIT WASH FAINT PATIENT CANCEL ROOF EDIT SILLY BATTLE HALF ENGINE REUNION HOTEL JOY FAN UNHAPPY OIL ALONE SENSE EMPTY MESH").unwrap_err(); + let error = TonWallet::new("TAIL SWING SUGGEST EDIT WASH FAINT PATIENT CANCEL ROOF EDIT SILLY BATTLE HALF ENGINE REUNION HOTEL JOY FAN UNHAPPY OIL ALONE SENSE EMPTY MESH", None).unwrap_err(); assert_eq!(error, WalletError::InvalidMnemonicUnknownWord); } diff --git a/rust/wallet_core_rs/src/ffi/wallet/ton_wallet.rs b/rust/wallet_core_rs/src/ffi/wallet/ton_wallet.rs index 5b467a15dbc..d367824ed81 100644 --- a/rust/wallet_core_rs/src/ffi/wallet/ton_wallet.rs +++ b/rust/wallet_core_rs/src/ffi/wallet/ton_wallet.rs @@ -4,7 +4,6 @@ #![allow(clippy::missing_safety_doc)] -use tw_hd_wallet::ton::mnemonic::TonMnemonic; use tw_hd_wallet::ton::TonWallet; use tw_keypair::ffi::privkey::TWPrivateKey; use tw_keypair::ffi::pubkey::TWPublicKey; @@ -32,7 +31,6 @@ pub unsafe extern "C" fn tw_ton_wallet_is_valid_mnemonic( ) -> bool { let mnemonic = try_or_false!(TWString::from_ptr_as_ref(mnemonic)); let mnemonic = try_or_false!(mnemonic.as_str()); - let mnemonic = try_or_false!(TonMnemonic::new(mnemonic)); let passphrase = TWString::from_ptr_as_ref(passphrase) .and_then(TWString::as_str) @@ -55,7 +53,6 @@ pub unsafe extern "C" fn tw_ton_wallet_create_with_mnemonic( ) -> *mut TWTONWallet { let mnemonic = try_or_else!(TWString::from_ptr_as_ref(mnemonic), std::ptr::null_mut); let mnemonic = try_or_else!(mnemonic.as_str(), std::ptr::null_mut); - let mnemonic = try_or_else!(TonMnemonic::new(mnemonic), std::ptr::null_mut); let passphrase = TWString::from_ptr_as_ref(passphrase) .and_then(TWString::as_str)