From 0b167710b325d299398ca82ef57ee8e630e54633 Mon Sep 17 00:00:00 2001 From: satoshiotomakan <127754187+satoshiotomakan@users.noreply.github.com> Date: Fri, 4 Oct 2024 15:49:54 +0700 Subject: [PATCH] [TON]: Add support for TON 24-words mnemonic (#3998) * feat(ton): Add support for TON 24-words mnemonic in Rust * feat(ton): Add tw_ton_wallet FFIs * feat(ton): Add TWTONWallet FFI in C++ * feat(ton): Add tonMnemonic StoredKey type * feat(ton): Add StoredKey TON tests * feat(ton): Add TWStoredKey TON tests * feat(ton): Add TONWallet support in Swift * TODO add iOS tests * feat(ton): Add `KeyStore` iOS tests * feat(ton): Add TONWallet support in JavaScript * Add `KeyStore` TypeScript tests * feat(ton): Remove `TonMnemonic` structure, replace with a `validate_mnemonic_words` function * [CI] Trigger CI * feat(ton): Fix rustfmt * feat(ton): Fix C++ build * feat(ton): Fix C++ build * feat(ton): Fix C++ build * feat(ton): Fix C++ address analyzer * feat(ton): Fix C++ tests * feat(ton): Add Android tests * feat(ton): Bump `actions/upload-artifact` to v4 * Bump `dawidd6/action-download-artifact` to v6 * feat(eth): Fix PR comments --- .../TestTheOpenNetworkWallet.kt | 17 + .../core/app/utils/TestKeyStore.kt | 48 + include/TrustWalletCore/TWStoredKey.h | 38 + include/TrustWalletCore/TWTONWallet.h | 36 +- rust/Cargo.lock | 28 + rust/Cargo.toml | 1 + rust/chains/tw_ton/Cargo.toml | 1 + .../src/test_utils/address_utils.rs | 4 +- rust/tw_hash/Cargo.toml | 1 + rust/tw_hash/src/ffi.rs | 2 + rust/tw_hash/src/hmac.rs | 15 +- rust/tw_hash/src/lib.rs | 2 + rust/tw_hash/src/pbkdf2.rs | 10 + rust/tw_hd_wallet/Cargo.toml | 14 + rust/tw_hd_wallet/src/bip39/bip39_english.rs | 2070 +++++++++++++++++ rust/tw_hd_wallet/src/bip39/mod.rs | 9 + rust/tw_hd_wallet/src/lib.rs | 18 + rust/tw_hd_wallet/src/ton/mnemonic.rs | 38 + rust/tw_hd_wallet/src/ton/mod.rs | 169 ++ rust/tw_hd_wallet/tests/ton_mnemonic.rs | 156 ++ rust/tw_keypair/src/ed25519/keypair.rs | 9 + rust/tw_keypair/src/ed25519/private.rs | 27 +- rust/tw_keypair/src/ffi/privkey.rs | 18 +- rust/tw_keypair/src/ffi/pubkey.rs | 2 +- .../src/test_utils/tw_crypto_box_helpers.rs | 6 +- .../src/test_utils/tw_private_key_helper.rs | 17 +- rust/tw_keypair/src/tw/private.rs | 13 + rust/tw_keypair/tests/crypto_box_ffi_tests.rs | 19 +- rust/tw_memory/Cargo.toml | 1 + rust/tw_memory/src/ffi/tw_data.rs | 13 + rust/tw_memory/src/test_utils/tw_wrapper.rs | 38 +- rust/tw_tests/tests/chains/ton/ton_wallet.rs | 32 +- rust/wallet_core_rs/Cargo.toml | 3 + rust/wallet_core_rs/src/ffi/mod.rs | 2 + rust/wallet_core_rs/src/ffi/ton/mod.rs | 1 - rust/wallet_core_rs/src/ffi/ton/wallet.rs | 55 - rust/wallet_core_rs/src/ffi/wallet/mod.rs | 5 + .../src/ffi/wallet/ton_wallet.rs | 128 + src/DerivationPath.cpp | 4 + src/Keystore/StoredKey.cpp | 112 +- src/Keystore/StoredKey.h | 44 +- src/TheOpenNetwork/TONWallet.cpp | 54 + src/TheOpenNetwork/TONWallet.h | 47 + src/interface/TWStoredKey.cpp | 26 + src/interface/TWTONWallet.cpp | 35 + src/rust/Wrapper.h | 8 +- swift/Sources/KeyStore.swift | 157 +- swift/Sources/Wallet.swift | 6 +- swift/Tests/Keystore/Data/ton_wallet.json | 30 + swift/Tests/Keystore/KeyStoreTests.swift | 111 +- swift/Tests/Keystore/WalletTests.swift | 50 + .../TheOpenNetwork/TWTONWalletTests.cpp | 19 + tests/common/Keystore/Data/ton-wallet.json | 30 + tests/common/Keystore/StoredKeyConstants.h | 19 + tests/common/Keystore/StoredKeyTONTests.cpp | 172 ++ tests/common/Keystore/StoredKeyTests.cpp | 22 +- tests/interface/TWStoredKeyTests.cpp | 126 +- wasm/src/keystore/default-impl.ts | 46 +- wasm/src/keystore/types.ts | 12 +- wasm/tests/KeyStore+extension.test.ts | 45 + wasm/tests/setup.test.ts | 2 + 61 files changed, 4041 insertions(+), 202 deletions(-) create mode 100644 rust/tw_hash/src/pbkdf2.rs create mode 100644 rust/tw_hd_wallet/Cargo.toml create mode 100644 rust/tw_hd_wallet/src/bip39/bip39_english.rs create mode 100644 rust/tw_hd_wallet/src/bip39/mod.rs create mode 100644 rust/tw_hd_wallet/src/lib.rs create mode 100644 rust/tw_hd_wallet/src/ton/mnemonic.rs create mode 100644 rust/tw_hd_wallet/src/ton/mod.rs create mode 100644 rust/tw_hd_wallet/tests/ton_mnemonic.rs delete mode 100644 rust/wallet_core_rs/src/ffi/ton/wallet.rs create mode 100644 rust/wallet_core_rs/src/ffi/wallet/mod.rs create mode 100644 rust/wallet_core_rs/src/ffi/wallet/ton_wallet.rs create mode 100644 src/TheOpenNetwork/TONWallet.cpp create mode 100644 src/TheOpenNetwork/TONWallet.h create mode 100644 swift/Tests/Keystore/Data/ton_wallet.json create mode 100644 tests/common/Keystore/Data/ton-wallet.json create mode 100644 tests/common/Keystore/StoredKeyConstants.h create mode 100644 tests/common/Keystore/StoredKeyTONTests.cpp diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkWallet.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkWallet.kt index 9305072bb75..b8f4b7d1b56 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkWallet.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkWallet.kt @@ -4,6 +4,7 @@ package com.trustwallet.core.app.blockchains.theopennetwork +import com.trustwallet.core.app.utils.toHex import com.trustwallet.core.app.utils.toHexByteArray import org.junit.Assert.assertEquals import org.junit.Test @@ -26,4 +27,20 @@ class TestTheOpenNetworkWallet { val expected = "te6cckECFgEAAwQAAgE0AQIBFP8A9KQT9LzyyAsDAFEAAAAAKamjF/IpqTcfp8IQiz2Q6iLJvnBf9dDP6u6cu5Nm/wFxV5NXQAIBIAQFAgFIBgcE+PKDCNcYINMf0x/THwL4I7vyZO1E0NMf0x/T//QE0VFDuvKhUVG68qIF+QFUEGT5EPKj+AAkpMjLH1JAyx9SMMv/UhD0AMntVPgPAdMHIcAAn2xRkyDXSpbTB9QC+wDoMOAhwAHjACHAAuMAAcADkTDjDQOkyMsfEssfy/8ICQoLAubQAdDTAyFxsJJfBOAi10nBIJJfBOAC0x8hghBwbHVnvSKCEGRzdHK9sJJfBeAD+kAwIPpEAcjKB8v/ydDtRNCBAUDXIfQEMFyBAQj0Cm+hMbOSXwfgBdM/yCWCEHBsdWe6kjgw4w0DghBkc3RyupJfBuMNDA0CASAODwBu0gf6ANTUIvkABcjKBxXL/8nQd3SAGMjLBcsCIs8WUAX6AhTLaxLMzMlz+wDIQBSBAQj0UfKnAgBwgQEI1xj6ANM/yFQgR4EBCPRR8qeCEG5vdGVwdIAYyMsFywJQBs8WUAT6AhTLahLLH8s/yXP7AAIAbIEBCNcY+gDTPzBSJIEBCPRZ8qeCEGRzdHJwdIAYyMsFywJQBc8WUAP6AhPLassfEss/yXP7AAAK9ADJ7VQAeAH6APQEMPgnbyIwUAqhIb7y4FCCEHBsdWeDHrFwgBhQBMsFJs8WWPoCGfQAy2kXyx9SYMs/IMmAQPsABgCKUASBAQj0WTDtRNCBAUDXIMgBzxb0AMntVAFysI4jghBkc3Rygx6xcIAYUAXLBVADzxYj+gITy2rLH8s/yYBA+wCSXwPiAgEgEBEAWb0kK29qJoQICga5D6AhhHDUCAhHpJN9KZEM5pA+n/mDeBKAG3gQFImHFZ8xhAIBWBITABG4yX7UTQ1wsfgAPbKd+1E0IEBQNch9AQwAsjKB8v/ydABgQEI9ApvoTGACASAUFQAZrc52omhAIGuQ64X/wAAZrx32omhAEGuQ64WPwEXtMkg=" assertEquals(stateInit, expected) } + + @Test + fun TheOpenNetworkWalletIsValidMnemonic() { + val validMnemonic = "sight shed side garbage illness clean health wet all win bench wide exist find galaxy drift task suggest portion fresh valve crime radar combine" + val noPassphrase = "" + val invalidPassphrase = "Expected empty passphrase" + assert(TONWallet.isValidMnemonic(validMnemonic, noPassphrase)) + assert(!TONWallet.isValidMnemonic(validMnemonic, invalidPassphrase)) + } + + @Test + fun TheOpenNetworkWalletGetKey() { + val tonMnemonic = "sight shed side garbage illness clean health wet all win bench wide exist find galaxy drift task suggest portion fresh valve crime radar combine" + val wallet = TONWallet(tonMnemonic, "") + assertEquals(wallet.key.data().toHex(), "0xb471884e691a9f5bb641b14f33bb9e555f759c24e368c4c0d997db3a60704220") + } } \ No newline at end of file diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestKeyStore.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestKeyStore.kt index 599d3369216..1843ea86257 100644 --- a/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestKeyStore.kt +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestKeyStore.kt @@ -4,6 +4,7 @@ import org.junit.Assert.* import org.junit.Test import wallet.core.jni.StoredKey import wallet.core.jni.CoinType +import wallet.core.jni.Derivation import wallet.core.jni.StoredKeyEncryption class TestKeyStore { @@ -17,9 +18,12 @@ class TestKeyStore { val keyStore = StoredKey("Test Wallet", "password".toByteArray()) val result = keyStore.decryptMnemonic("wrong".toByteArray()) val result2 = keyStore.decryptMnemonic("password".toByteArray()) + val result3 = keyStore.decryptTONMnemonic("password".toByteArray()) assertNull(result) assertNotNull(result2) + // StoredKey is an HD by default, so `decryptTONMnemonic` should return null. + assertNull(result3) } @Test @@ -91,4 +95,48 @@ class TestKeyStore { val privateKey = newKeyStore.decryptPrivateKey("".toByteArray()) assertNull(privateKey) } + + @Test + fun testImportTONWallet() { + val tonMnemonic = "laundry myself fitness beyond prize piano match acid vacuum already abandon dance occur pause grocery company inject excuse weasel carpet fog grunt trick spike" + val password = "password".toByteArray() + + val keyStore = StoredKey.importTONWallet(tonMnemonic, "Test Wallet", password, CoinType.TON) + + val decrypted1 = keyStore.decryptTONMnemonic("wrong".toByteArray()) + val decrypted2 = keyStore.decryptTONMnemonic("password".toByteArray()) + assertNull(decrypted1) + assertNotNull(decrypted2) + + assertEquals(keyStore.accountCount(), 1) + + // `StoredKey.account(index)` is only allowed. + // `StoredKey.accountForCoin(coin, wallet)` is not supported. + val tonAccount = keyStore.account(0) + assertEquals(tonAccount.address(), "UQDdB2lMwYM9Gxc-ln--Tu8cz-TYksQxYuUsMs2Pd4cHerYz") + assertEquals(tonAccount.coin(), CoinType.TON) + assertEquals(tonAccount.publicKey(), "c9af50596bd5c1c5a15fb32bef8d4f1ee5244b287aea1f49f6023a79f9b2f055") + assertEquals(tonAccount.extendedPublicKey(), "") + assertEquals(tonAccount.derivation(), Derivation.DEFAULT) + assertEquals(tonAccount.derivationPath(), "") + + val privateKey = keyStore.privateKey(CoinType.TON, password) + assertEquals(privateKey.data().toHex(), "0x859cd74ab605afb7ce9f5316a1f6d59217a130b75b494efd249913be874c9d46") + + // HD wallet is not supported for TON wallet + val hdWallet = keyStore.wallet(password) + assertNull(hdWallet) + } + + @Test + fun testExportTONWallet() { + val tonMnemonic = "laundry myself fitness beyond prize piano match acid vacuum already abandon dance occur pause grocery company inject excuse weasel carpet fog grunt trick spike" + val password = "password".toByteArray() + + val keyStore = StoredKey.importTONWallet(tonMnemonic, "Test Wallet", password, CoinType.TON) + val json = keyStore.exportJSON() + + val newKeyStore = StoredKey.importJSON(json) + assertEquals(newKeyStore.decryptTONMnemonic(password), tonMnemonic) + } } diff --git a/include/TrustWalletCore/TWStoredKey.h b/include/TrustWalletCore/TWStoredKey.h index 58a07e521c0..6df17e7a220 100644 --- a/include/TrustWalletCore/TWStoredKey.h +++ b/include/TrustWalletCore/TWStoredKey.h @@ -74,6 +74,29 @@ struct TWStoredKey* _Nullable TWStoredKeyImportHDWallet(TWString* _Nonnull mnemo TW_EXPORT_STATIC_METHOD struct TWStoredKey* _Nullable TWStoredKeyImportHDWalletWithEncryption(TWString* _Nonnull mnemonic, TWString* _Nonnull name, TWData* _Nonnull password, enum TWCoinType coin, enum TWStoredKeyEncryption encryption); +/// Imports a TON-specific wallet with a 24-words mnemonic. +/// +/// \param tonMnemonic Non-null TON mnemonic +/// \param name The name of the stored key to import as a non-null string +/// \param password Non-null block of data, password of the stored key +/// \param coin the coin type +/// \note Returned object needs to be deleted with \TWStoredKeyDelete +/// \return Nullptr if the key can't be imported, the stored key otherwise +TW_EXPORT_STATIC_METHOD +struct TWStoredKey* _Nullable TWStoredKeyImportTONWallet(TWString* _Nonnull tonMnemonic, TWString* _Nonnull name, TWData* _Nonnull password, enum TWCoinType coin); + +/// Imports a TON-specific wallet with a 24-words mnemonic. +/// +/// \param tonMnemonic Non-null TON mnemonic +/// \param name The name of the stored key to import as a non-null string +/// \param password Non-null block of data, password of the stored key +/// \param coin the coin type +/// \param encryption cipher encryption mode +/// \note Returned object needs to be deleted with \TWStoredKeyDelete +/// \return Nullptr if the key can't be imported, the stored key otherwise +TW_EXPORT_STATIC_METHOD +struct TWStoredKey* _Nullable TWStoredKeyImportTONWalletWithEncryption(TWString* _Nonnull tonMnemonic, TWString* _Nonnull name, TWData* _Nonnull password, enum TWCoinType coin, enum TWStoredKeyEncryption encryption); + /// Imports a key from JSON. /// /// \param json Json stored key import format as a non-null block of data @@ -152,6 +175,13 @@ TWString* _Nonnull TWStoredKeyName(struct TWStoredKey* _Nonnull key); TW_EXPORT_PROPERTY bool TWStoredKeyIsMnemonic(struct TWStoredKey* _Nonnull key); +/// Whether this key is a TON mnemonic phrase. +/// +/// \param key Non-null pointer to a stored key +/// \return true if the given stored key is a TON mnemonic, false otherwise +TW_EXPORT_PROPERTY +bool TWStoredKeyIsTONMnemonic(struct TWStoredKey* _Nonnull key); + /// The number of accounts. /// /// \param key Non-null pointer to a stored key @@ -261,6 +291,14 @@ TWData* _Nullable TWStoredKeyDecryptPrivateKey(struct TWStoredKey* _Nonnull key, TW_EXPORT_METHOD TWString* _Nullable TWStoredKeyDecryptMnemonic(struct TWStoredKey* _Nonnull key, TWData* _Nonnull password); +/// Decrypts the TON mnemonic phrase. +/// +/// \param key Non-null pointer to a stored key +/// \param password Non-null block of data, password of the stored key +/// \return TON decrypted mnemonic if success, null pointer otherwise +TW_EXPORT_METHOD +TWString* _Nullable TWStoredKeyDecryptTONMnemonic(struct TWStoredKey* _Nonnull key, TWData* _Nonnull password); + /// Returns the private key for a specific coin. Returned object needs to be deleted. /// /// \param key Non-null pointer to a stored key diff --git a/include/TrustWalletCore/TWTONWallet.h b/include/TrustWalletCore/TWTONWallet.h index 098702faa6a..53d230482bc 100644 --- a/include/TrustWalletCore/TWTONWallet.h +++ b/include/TrustWalletCore/TWTONWallet.h @@ -5,6 +5,7 @@ #pragma once #include "TWBase.h" +#include "TWPrivateKey.h" #include "TWPublicKey.h" #include "TWString.h" @@ -14,6 +15,39 @@ TW_EXTERN_C_BEGIN TW_EXPORT_CLASS struct TWTONWallet; +/// Determines whether the English mnemonic and passphrase are valid. +/// +/// \param mnemonic Non-null english mnemonic +/// \param passphrase Nullable optional passphrase +/// \note passphrase can be null or empty string if no passphrase required +/// \return whether the mnemonic and passphrase are valid (valid checksum) +TW_EXPORT_STATIC_METHOD +bool TWTONWalletIsValidMnemonic(TWString* _Nonnull mnemonic, TWString* _Nullable passphrase); + +/// Creates a \TONWallet from a valid TON mnemonic and passphrase. +/// +/// \param mnemonic Non-null english mnemonic +/// \param passphrase Nullable optional passphrase +/// \note Null is returned on invalid mnemonic and passphrase +/// \note passphrase can be null or empty string if no passphrase required +/// \return Nullable TWTONWallet +TW_EXPORT_STATIC_METHOD +struct TWTONWallet* _Nullable TWTONWalletCreateWithMnemonic(TWString* _Nonnull mnemonic, TWString* _Nullable passphrase); + +/// Delete the given \TONWallet +/// +/// \param wallet Non-null pointer to private key +TW_EXPORT_METHOD +void TWTONWalletDelete(struct TWTONWallet* _Nonnull wallet); + +/// Generates Ed25519 private key associated with the wallet. +/// +/// \param wallet non-null TWTONWallet +/// \note Returned object needs to be deleted with \TWPrivateKeyDelete +/// \return The Ed25519 private key +TW_EXPORT_METHOD +struct TWPrivateKey* _Nonnull TWTONWalletGetKey(struct TWTONWallet* _Nonnull wallet); + /// Constructs a TON Wallet V4R2 stateInit encoded as BoC (BagOfCells) for the given `public_key`. /// /// \param publicKey wallet's public key. @@ -21,6 +55,6 @@ struct TWTONWallet; /// \param walletId wallet's ID allows to create multiple wallets for the same private key. /// \return Pointer to a base64 encoded Bag Of Cells (BoC) StateInit. Null if invalid public key provided. TW_EXPORT_STATIC_METHOD -TWString *_Nullable TWTONWalletBuildV4R2StateInit(struct TWPublicKey *_Nonnull publicKey, int32_t workchain, int32_t walletId); +TWString *_Nullable TWTONWalletBuildV4R2StateInit(struct TWPublicKey* _Nonnull publicKey, int32_t workchain, int32_t walletId); TW_EXTERN_C_END diff --git a/rust/Cargo.lock b/rust/Cargo.lock index 108bb84e1e0..0a4e30effd7 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -1105,6 +1105,16 @@ dependencies = [ "nom", ] +[[package]] +name = "pbkdf2" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0ca0b5a68607598bf3bad68f32227a8164f6254833f84eafaac409cd6746c31" +dependencies = [ + "digest 0.10.6", + "hmac", +] + [[package]] name = "pkcs8" version = "0.10.2" @@ -1954,6 +1964,7 @@ dependencies = [ "digest 0.10.6", "groestl", "hmac", + "pbkdf2", "ripemd", "serde", "serde_json", @@ -1965,6 +1976,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "tw_hd_wallet" +version = "0.1.0" +dependencies = [ + "lazy_static", + "tw_encoding", + "tw_hash", + "tw_keypair", + "tw_misc", + "zeroize", +] + [[package]] name = "tw_internet_computer" version = "0.1.0" @@ -2015,6 +2038,9 @@ dependencies = [ [[package]] name = "tw_memory" version = "0.1.0" +dependencies = [ + "zeroize", +] [[package]] name = "tw_misc" @@ -2166,6 +2192,7 @@ dependencies = [ "tw_number", "tw_proto", "tw_ton_sdk", + "zeroize", ] [[package]] @@ -2281,6 +2308,7 @@ dependencies = [ "tw_encoding", "tw_ethereum", "tw_hash", + "tw_hd_wallet", "tw_keypair", "tw_memory", "tw_misc", diff --git a/rust/Cargo.toml b/rust/Cargo.toml index ca41fb6099d..dce2a6f9154 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -25,6 +25,7 @@ members = [ "tw_encoding", "tw_evm", "tw_hash", + "tw_hd_wallet", "tw_keypair", "tw_memory", "tw_misc", diff --git a/rust/chains/tw_ton/Cargo.toml b/rust/chains/tw_ton/Cargo.toml index 076a7d84f61..145508522a6 100644 --- a/rust/chains/tw_ton/Cargo.toml +++ b/rust/chains/tw_ton/Cargo.toml @@ -14,3 +14,4 @@ tw_number = { path = "../../tw_number" } tw_misc = { path = "../../tw_misc" } tw_proto = { path = "../../tw_proto" } tw_ton_sdk = { path = "../../frameworks/tw_ton_sdk" } +zeroize = "1.8.1" diff --git a/rust/tw_any_coin/src/test_utils/address_utils.rs b/rust/tw_any_coin/src/test_utils/address_utils.rs index 528dce6092a..17fc481e95b 100644 --- a/rust/tw_any_coin/src/test_utils/address_utils.rs +++ b/rust/tw_any_coin/src/test_utils/address_utils.rs @@ -19,9 +19,9 @@ use tw_keypair::test_utils::tw_public_key_helper::TWPublicKeyHelper; use tw_keypair::tw::PublicKeyType; use tw_memory::test_utils::tw_data_helper::TWDataHelper; use tw_memory::test_utils::tw_string_helper::TWStringHelper; -use tw_memory::test_utils::tw_wrapper::{TWWrapper, WithDestructor}; +use tw_memory::test_utils::tw_wrapper::{TWAutoWrapper, WithDestructor}; -pub type TWAnyAddressHelper = TWWrapper; +pub type TWAnyAddressHelper = TWAutoWrapper; impl WithDestructor for TWAnyAddress { fn destructor() -> unsafe extern "C" fn(*mut Self) { diff --git a/rust/tw_hash/Cargo.toml b/rust/tw_hash/Cargo.toml index c9b8ab18c42..3373eda15c9 100644 --- a/rust/tw_hash/Cargo.toml +++ b/rust/tw_hash/Cargo.toml @@ -13,6 +13,7 @@ blake2b-ref = "0.3.1" digest = "0.10.6" groestl = "0.10.1" hmac = "0.12.1" +pbkdf2 = "0.12.1" ripemd = "0.1.3" serde = { version = "1.0", features = ["derive"], optional = true } sha1 = "0.10.5" diff --git a/rust/tw_hash/src/ffi.rs b/rust/tw_hash/src/ffi.rs index 08c4faf44b9..2867ffa4a8d 100644 --- a/rust/tw_hash/src/ffi.rs +++ b/rust/tw_hash/src/ffi.rs @@ -14,6 +14,7 @@ pub enum CHashingCode { Ok = 0, InvalidHashLength = 1, InvalidArgument = 2, + InvalidPassword = 3, } impl From for CHashingCode { @@ -21,6 +22,7 @@ impl From for CHashingCode { match e { Error::FromHexError(_) | Error::InvalidArgument => CHashingCode::InvalidArgument, Error::InvalidHashLength => CHashingCode::InvalidHashLength, + Error::InvalidPassword => CHashingCode::InvalidPassword, } } } diff --git a/rust/tw_hash/src/hmac.rs b/rust/tw_hash/src/hmac.rs index 475c5bbdb7f..538d0409b15 100644 --- a/rust/tw_hash/src/hmac.rs +++ b/rust/tw_hash/src/hmac.rs @@ -3,14 +3,19 @@ // Copyright © 2017 Trust Wallet. use hmac::{Hmac, Mac}; -use sha2::Sha256; +use sha2::{Sha256, Sha512}; type HmacSha256 = Hmac; +type HmacSha512 = Hmac; pub fn hmac_sha256(key: &[u8], input: &[u8]) -> Vec { - let mut mac = HmacSha256::new_from_slice(key).unwrap(); + let mut mac = HmacSha256::new_from_slice(key).expect("Hmac constructor should never fail"); mac.update(input); - let res = mac.finalize(); - let code_bytes = res.into_bytes(); - code_bytes.to_vec() + mac.finalize().into_bytes().to_vec() +} + +pub fn hmac_sha512(key: &[u8], input: &[u8]) -> Vec { + let mut mac = HmacSha512::new_from_slice(key).expect("Hmac constructor should never fail"); + mac.update(input); + mac.finalize().into_bytes().to_vec() } diff --git a/rust/tw_hash/src/lib.rs b/rust/tw_hash/src/lib.rs index 2371346ee7a..6d1817edea6 100644 --- a/rust/tw_hash/src/lib.rs +++ b/rust/tw_hash/src/lib.rs @@ -9,6 +9,7 @@ pub mod ffi; pub mod groestl; pub mod hasher; pub mod hmac; +pub mod pbkdf2; pub mod ripemd; pub mod sha1; pub mod sha2; @@ -28,6 +29,7 @@ pub enum Error { FromHexError(FromHexError), InvalidHashLength, InvalidArgument, + InvalidPassword, } impl From for Error { diff --git a/rust/tw_hash/src/pbkdf2.rs b/rust/tw_hash/src/pbkdf2.rs new file mode 100644 index 00000000000..30135d2dc17 --- /dev/null +++ b/rust/tw_hash/src/pbkdf2.rs @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::H512; +use sha2::Sha512; + +pub fn pbkdf2_hmac_sha512(password: &[u8], salt: &[u8], rounds: u32) -> H512 { + pbkdf2::pbkdf2_hmac_array::(password, salt, rounds).into() +} diff --git a/rust/tw_hd_wallet/Cargo.toml b/rust/tw_hd_wallet/Cargo.toml new file mode 100644 index 00000000000..72acc5d0022 --- /dev/null +++ b/rust/tw_hd_wallet/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "tw_hd_wallet" +version = "0.1.0" +edition = "2021" + +[dependencies] +lazy_static = "1.4.0" +tw_hash = { path = "../tw_hash" } +tw_keypair = { path = "../tw_keypair" } +zeroize = "1.8.1" + +[dev-dependencies] +tw_encoding = { path = "../tw_encoding" } +tw_misc = { path = "../tw_misc" } diff --git a/rust/tw_hd_wallet/src/bip39/bip39_english.rs b/rust/tw_hd_wallet/src/bip39/bip39_english.rs new file mode 100644 index 00000000000..ae0c461da23 --- /dev/null +++ b/rust/tw_hd_wallet/src/bip39/bip39_english.rs @@ -0,0 +1,2070 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use lazy_static::lazy_static; +use std::collections::HashMap; + +lazy_static! { + pub static ref BIP39_WORDS_MAP: HashMap<&'static str, usize> = { + BIP39_WORDS_LIST + .iter() + .enumerate() + .map(|(idx, word)| (*word, idx)) + .collect() + }; +} + +/// 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: [&str; 2048] = [ + "abandon", + "ability", + "able", + "about", + "above", + "absent", + "absorb", + "abstract", + "absurd", + "abuse", + "access", + "accident", + "account", + "accuse", + "achieve", + "acid", + "acoustic", + "acquire", + "across", + "act", + "action", + "actor", + "actress", + "actual", + "adapt", + "add", + "addict", + "address", + "adjust", + "admit", + "adult", + "advance", + "advice", + "aerobic", + "affair", + "afford", + "afraid", + "again", + "age", + "agent", + "agree", + "ahead", + "aim", + "air", + "airport", + "aisle", + "alarm", + "album", + "alcohol", + "alert", + "alien", + "all", + "alley", + "allow", + "almost", + "alone", + "alpha", + "already", + "also", + "alter", + "always", + "amateur", + "amazing", + "among", + "amount", + "amused", + "analyst", + "anchor", + "ancient", + "anger", + "angle", + "angry", + "animal", + "ankle", + "announce", + "annual", + "another", + "answer", + "antenna", + "antique", + "anxiety", + "any", + "apart", + "apology", + "appear", + "apple", + "approve", + "april", + "arch", + "arctic", + "area", + "arena", + "argue", + "arm", + "armed", + "armor", + "army", + "around", + "arrange", + "arrest", + "arrive", + "arrow", + "art", + "artefact", + "artist", + "artwork", + "ask", + "aspect", + "assault", + "asset", + "assist", + "assume", + "asthma", + "athlete", + "atom", + "attack", + "attend", + "attitude", + "attract", + "auction", + "audit", + "august", + "aunt", + "author", + "auto", + "autumn", + "average", + "avocado", + "avoid", + "awake", + "aware", + "away", + "awesome", + "awful", + "awkward", + "axis", + "baby", + "bachelor", + "bacon", + "badge", + "bag", + "balance", + "balcony", + "ball", + "bamboo", + "banana", + "banner", + "bar", + "barely", + "bargain", + "barrel", + "base", + "basic", + "basket", + "battle", + "beach", + "bean", + "beauty", + "because", + "become", + "beef", + "before", + "begin", + "behave", + "behind", + "believe", + "below", + "belt", + "bench", + "benefit", + "best", + "betray", + "better", + "between", + "beyond", + "bicycle", + "bid", + "bike", + "bind", + "biology", + "bird", + "birth", + "bitter", + "black", + "blade", + "blame", + "blanket", + "blast", + "bleak", + "bless", + "blind", + "blood", + "blossom", + "blouse", + "blue", + "blur", + "blush", + "board", + "boat", + "body", + "boil", + "bomb", + "bone", + "bonus", + "book", + "boost", + "border", + "boring", + "borrow", + "boss", + "bottom", + "bounce", + "box", + "boy", + "bracket", + "brain", + "brand", + "brass", + "brave", + "bread", + "breeze", + "brick", + "bridge", + "brief", + "bright", + "bring", + "brisk", + "broccoli", + "broken", + "bronze", + "broom", + "brother", + "brown", + "brush", + "bubble", + "buddy", + "budget", + "buffalo", + "build", + "bulb", + "bulk", + "bullet", + "bundle", + "bunker", + "burden", + "burger", + "burst", + "bus", + "business", + "busy", + "butter", + "buyer", + "buzz", + "cabbage", + "cabin", + "cable", + "cactus", + "cage", + "cake", + "call", + "calm", + "camera", + "camp", + "can", + "canal", + "cancel", + "candy", + "cannon", + "canoe", + "canvas", + "canyon", + "capable", + "capital", + "captain", + "car", + "carbon", + "card", + "cargo", + "carpet", + "carry", + "cart", + "case", + "cash", + "casino", + "castle", + "casual", + "cat", + "catalog", + "catch", + "category", + "cattle", + "caught", + "cause", + "caution", + "cave", + "ceiling", + "celery", + "cement", + "census", + "century", + "cereal", + "certain", + "chair", + "chalk", + "champion", + "change", + "chaos", + "chapter", + "charge", + "chase", + "chat", + "cheap", + "check", + "cheese", + "chef", + "cherry", + "chest", + "chicken", + "chief", + "child", + "chimney", + "choice", + "choose", + "chronic", + "chuckle", + "chunk", + "churn", + "cigar", + "cinnamon", + "circle", + "citizen", + "city", + "civil", + "claim", + "clap", + "clarify", + "claw", + "clay", + "clean", + "clerk", + "clever", + "click", + "client", + "cliff", + "climb", + "clinic", + "clip", + "clock", + "clog", + "close", + "cloth", + "cloud", + "clown", + "club", + "clump", + "cluster", + "clutch", + "coach", + "coast", + "coconut", + "code", + "coffee", + "coil", + "coin", + "collect", + "color", + "column", + "combine", + "come", + "comfort", + "comic", + "common", + "company", + "concert", + "conduct", + "confirm", + "congress", + "connect", + "consider", + "control", + "convince", + "cook", + "cool", + "copper", + "copy", + "coral", + "core", + "corn", + "correct", + "cost", + "cotton", + "couch", + "country", + "couple", + "course", + "cousin", + "cover", + "coyote", + "crack", + "cradle", + "craft", + "cram", + "crane", + "crash", + "crater", + "crawl", + "crazy", + "cream", + "credit", + "creek", + "crew", + "cricket", + "crime", + "crisp", + "critic", + "crop", + "cross", + "crouch", + "crowd", + "crucial", + "cruel", + "cruise", + "crumble", + "crunch", + "crush", + "cry", + "crystal", + "cube", + "culture", + "cup", + "cupboard", + "curious", + "current", + "curtain", + "curve", + "cushion", + "custom", + "cute", + "cycle", + "dad", + "damage", + "damp", + "dance", + "danger", + "daring", + "dash", + "daughter", + "dawn", + "day", + "deal", + "debate", + "debris", + "decade", + "december", + "decide", + "decline", + "decorate", + "decrease", + "deer", + "defense", + "define", + "defy", + "degree", + "delay", + "deliver", + "demand", + "demise", + "denial", + "dentist", + "deny", + "depart", + "depend", + "deposit", + "depth", + "deputy", + "derive", + "describe", + "desert", + "design", + "desk", + "despair", + "destroy", + "detail", + "detect", + "develop", + "device", + "devote", + "diagram", + "dial", + "diamond", + "diary", + "dice", + "diesel", + "diet", + "differ", + "digital", + "dignity", + "dilemma", + "dinner", + "dinosaur", + "direct", + "dirt", + "disagree", + "discover", + "disease", + "dish", + "dismiss", + "disorder", + "display", + "distance", + "divert", + "divide", + "divorce", + "dizzy", + "doctor", + "document", + "dog", + "doll", + "dolphin", + "domain", + "donate", + "donkey", + "donor", + "door", + "dose", + "double", + "dove", + "draft", + "dragon", + "drama", + "drastic", + "draw", + "dream", + "dress", + "drift", + "drill", + "drink", + "drip", + "drive", + "drop", + "drum", + "dry", + "duck", + "dumb", + "dune", + "during", + "dust", + "dutch", + "duty", + "dwarf", + "dynamic", + "eager", + "eagle", + "early", + "earn", + "earth", + "easily", + "east", + "easy", + "echo", + "ecology", + "economy", + "edge", + "edit", + "educate", + "effort", + "egg", + "eight", + "either", + "elbow", + "elder", + "electric", + "elegant", + "element", + "elephant", + "elevator", + "elite", + "else", + "embark", + "embody", + "embrace", + "emerge", + "emotion", + "employ", + "empower", + "empty", + "enable", + "enact", + "end", + "endless", + "endorse", + "enemy", + "energy", + "enforce", + "engage", + "engine", + "enhance", + "enjoy", + "enlist", + "enough", + "enrich", + "enroll", + "ensure", + "enter", + "entire", + "entry", + "envelope", + "episode", + "equal", + "equip", + "era", + "erase", + "erode", + "erosion", + "error", + "erupt", + "escape", + "essay", + "essence", + "estate", + "eternal", + "ethics", + "evidence", + "evil", + "evoke", + "evolve", + "exact", + "example", + "excess", + "exchange", + "excite", + "exclude", + "excuse", + "execute", + "exercise", + "exhaust", + "exhibit", + "exile", + "exist", + "exit", + "exotic", + "expand", + "expect", + "expire", + "explain", + "expose", + "express", + "extend", + "extra", + "eye", + "eyebrow", + "fabric", + "face", + "faculty", + "fade", + "faint", + "faith", + "fall", + "false", + "fame", + "family", + "famous", + "fan", + "fancy", + "fantasy", + "farm", + "fashion", + "fat", + "fatal", + "father", + "fatigue", + "fault", + "favorite", + "feature", + "february", + "federal", + "fee", + "feed", + "feel", + "female", + "fence", + "festival", + "fetch", + "fever", + "few", + "fiber", + "fiction", + "field", + "figure", + "file", + "film", + "filter", + "final", + "find", + "fine", + "finger", + "finish", + "fire", + "firm", + "first", + "fiscal", + "fish", + "fit", + "fitness", + "fix", + "flag", + "flame", + "flash", + "flat", + "flavor", + "flee", + "flight", + "flip", + "float", + "flock", + "floor", + "flower", + "fluid", + "flush", + "fly", + "foam", + "focus", + "fog", + "foil", + "fold", + "follow", + "food", + "foot", + "force", + "forest", + "forget", + "fork", + "fortune", + "forum", + "forward", + "fossil", + "foster", + "found", + "fox", + "fragile", + "frame", + "frequent", + "fresh", + "friend", + "fringe", + "frog", + "front", + "frost", + "frown", + "frozen", + "fruit", + "fuel", + "fun", + "funny", + "furnace", + "fury", + "future", + "gadget", + "gain", + "galaxy", + "gallery", + "game", + "gap", + "garage", + "garbage", + "garden", + "garlic", + "garment", + "gas", + "gasp", + "gate", + "gather", + "gauge", + "gaze", + "general", + "genius", + "genre", + "gentle", + "genuine", + "gesture", + "ghost", + "giant", + "gift", + "giggle", + "ginger", + "giraffe", + "girl", + "give", + "glad", + "glance", + "glare", + "glass", + "glide", + "glimpse", + "globe", + "gloom", + "glory", + "glove", + "glow", + "glue", + "goat", + "goddess", + "gold", + "good", + "goose", + "gorilla", + "gospel", + "gossip", + "govern", + "gown", + "grab", + "grace", + "grain", + "grant", + "grape", + "grass", + "gravity", + "great", + "green", + "grid", + "grief", + "grit", + "grocery", + "group", + "grow", + "grunt", + "guard", + "guess", + "guide", + "guilt", + "guitar", + "gun", + "gym", + "habit", + "hair", + "half", + "hammer", + "hamster", + "hand", + "happy", + "harbor", + "hard", + "harsh", + "harvest", + "hat", + "have", + "hawk", + "hazard", + "head", + "health", + "heart", + "heavy", + "hedgehog", + "height", + "hello", + "helmet", + "help", + "hen", + "hero", + "hidden", + "high", + "hill", + "hint", + "hip", + "hire", + "history", + "hobby", + "hockey", + "hold", + "hole", + "holiday", + "hollow", + "home", + "honey", + "hood", + "hope", + "horn", + "horror", + "horse", + "hospital", + "host", + "hotel", + "hour", + "hover", + "hub", + "huge", + "human", + "humble", + "humor", + "hundred", + "hungry", + "hunt", + "hurdle", + "hurry", + "hurt", + "husband", + "hybrid", + "ice", + "icon", + "idea", + "identify", + "idle", + "ignore", + "ill", + "illegal", + "illness", + "image", + "imitate", + "immense", + "immune", + "impact", + "impose", + "improve", + "impulse", + "inch", + "include", + "income", + "increase", + "index", + "indicate", + "indoor", + "industry", + "infant", + "inflict", + "inform", + "inhale", + "inherit", + "initial", + "inject", + "injury", + "inmate", + "inner", + "innocent", + "input", + "inquiry", + "insane", + "insect", + "inside", + "inspire", + "install", + "intact", + "interest", + "into", + "invest", + "invite", + "involve", + "iron", + "island", + "isolate", + "issue", + "item", + "ivory", + "jacket", + "jaguar", + "jar", + "jazz", + "jealous", + "jeans", + "jelly", + "jewel", + "job", + "join", + "joke", + "journey", + "joy", + "judge", + "juice", + "jump", + "jungle", + "junior", + "junk", + "just", + "kangaroo", + "keen", + "keep", + "ketchup", + "key", + "kick", + "kid", + "kidney", + "kind", + "kingdom", + "kiss", + "kit", + "kitchen", + "kite", + "kitten", + "kiwi", + "knee", + "knife", + "knock", + "know", + "lab", + "label", + "labor", + "ladder", + "lady", + "lake", + "lamp", + "language", + "laptop", + "large", + "later", + "latin", + "laugh", + "laundry", + "lava", + "law", + "lawn", + "lawsuit", + "layer", + "lazy", + "leader", + "leaf", + "learn", + "leave", + "lecture", + "left", + "leg", + "legal", + "legend", + "leisure", + "lemon", + "lend", + "length", + "lens", + "leopard", + "lesson", + "letter", + "level", + "liar", + "liberty", + "library", + "license", + "life", + "lift", + "light", + "like", + "limb", + "limit", + "link", + "lion", + "liquid", + "list", + "little", + "live", + "lizard", + "load", + "loan", + "lobster", + "local", + "lock", + "logic", + "lonely", + "long", + "loop", + "lottery", + "loud", + "lounge", + "love", + "loyal", + "lucky", + "luggage", + "lumber", + "lunar", + "lunch", + "luxury", + "lyrics", + "machine", + "mad", + "magic", + "magnet", + "maid", + "mail", + "main", + "major", + "make", + "mammal", + "man", + "manage", + "mandate", + "mango", + "mansion", + "manual", + "maple", + "marble", + "march", + "margin", + "marine", + "market", + "marriage", + "mask", + "mass", + "master", + "match", + "material", + "math", + "matrix", + "matter", + "maximum", + "maze", + "meadow", + "mean", + "measure", + "meat", + "mechanic", + "medal", + "media", + "melody", + "melt", + "member", + "memory", + "mention", + "menu", + "mercy", + "merge", + "merit", + "merry", + "mesh", + "message", + "metal", + "method", + "middle", + "midnight", + "milk", + "million", + "mimic", + "mind", + "minimum", + "minor", + "minute", + "miracle", + "mirror", + "misery", + "miss", + "mistake", + "mix", + "mixed", + "mixture", + "mobile", + "model", + "modify", + "mom", + "moment", + "monitor", + "monkey", + "monster", + "month", + "moon", + "moral", + "more", + "morning", + "mosquito", + "mother", + "motion", + "motor", + "mountain", + "mouse", + "move", + "movie", + "much", + "muffin", + "mule", + "multiply", + "muscle", + "museum", + "mushroom", + "music", + "must", + "mutual", + "myself", + "mystery", + "myth", + "naive", + "name", + "napkin", + "narrow", + "nasty", + "nation", + "nature", + "near", + "neck", + "need", + "negative", + "neglect", + "neither", + "nephew", + "nerve", + "nest", + "net", + "network", + "neutral", + "never", + "news", + "next", + "nice", + "night", + "noble", + "noise", + "nominee", + "noodle", + "normal", + "north", + "nose", + "notable", + "note", + "nothing", + "notice", + "novel", + "now", + "nuclear", + "number", + "nurse", + "nut", + "oak", + "obey", + "object", + "oblige", + "obscure", + "observe", + "obtain", + "obvious", + "occur", + "ocean", + "october", + "odor", + "off", + "offer", + "office", + "often", + "oil", + "okay", + "old", + "olive", + "olympic", + "omit", + "once", + "one", + "onion", + "online", + "only", + "open", + "opera", + "opinion", + "oppose", + "option", + "orange", + "orbit", + "orchard", + "order", + "ordinary", + "organ", + "orient", + "original", + "orphan", + "ostrich", + "other", + "outdoor", + "outer", + "output", + "outside", + "oval", + "oven", + "over", + "own", + "owner", + "oxygen", + "oyster", + "ozone", + "pact", + "paddle", + "page", + "pair", + "palace", + "palm", + "panda", + "panel", + "panic", + "panther", + "paper", + "parade", + "parent", + "park", + "parrot", + "party", + "pass", + "patch", + "path", + "patient", + "patrol", + "pattern", + "pause", + "pave", + "payment", + "peace", + "peanut", + "pear", + "peasant", + "pelican", + "pen", + "penalty", + "pencil", + "people", + "pepper", + "perfect", + "permit", + "person", + "pet", + "phone", + "photo", + "phrase", + "physical", + "piano", + "picnic", + "picture", + "piece", + "pig", + "pigeon", + "pill", + "pilot", + "pink", + "pioneer", + "pipe", + "pistol", + "pitch", + "pizza", + "place", + "planet", + "plastic", + "plate", + "play", + "please", + "pledge", + "pluck", + "plug", + "plunge", + "poem", + "poet", + "point", + "polar", + "pole", + "police", + "pond", + "pony", + "pool", + "popular", + "portion", + "position", + "possible", + "post", + "potato", + "pottery", + "poverty", + "powder", + "power", + "practice", + "praise", + "predict", + "prefer", + "prepare", + "present", + "pretty", + "prevent", + "price", + "pride", + "primary", + "print", + "priority", + "prison", + "private", + "prize", + "problem", + "process", + "produce", + "profit", + "program", + "project", + "promote", + "proof", + "property", + "prosper", + "protect", + "proud", + "provide", + "public", + "pudding", + "pull", + "pulp", + "pulse", + "pumpkin", + "punch", + "pupil", + "puppy", + "purchase", + "purity", + "purpose", + "purse", + "push", + "put", + "puzzle", + "pyramid", + "quality", + "quantum", + "quarter", + "question", + "quick", + "quit", + "quiz", + "quote", + "rabbit", + "raccoon", + "race", + "rack", + "radar", + "radio", + "rail", + "rain", + "raise", + "rally", + "ramp", + "ranch", + "random", + "range", + "rapid", + "rare", + "rate", + "rather", + "raven", + "raw", + "razor", + "ready", + "real", + "reason", + "rebel", + "rebuild", + "recall", + "receive", + "recipe", + "record", + "recycle", + "reduce", + "reflect", + "reform", + "refuse", + "region", + "regret", + "regular", + "reject", + "relax", + "release", + "relief", + "rely", + "remain", + "remember", + "remind", + "remove", + "render", + "renew", + "rent", + "reopen", + "repair", + "repeat", + "replace", + "report", + "require", + "rescue", + "resemble", + "resist", + "resource", + "response", + "result", + "retire", + "retreat", + "return", + "reunion", + "reveal", + "review", + "reward", + "rhythm", + "rib", + "ribbon", + "rice", + "rich", + "ride", + "ridge", + "rifle", + "right", + "rigid", + "ring", + "riot", + "ripple", + "risk", + "ritual", + "rival", + "river", + "road", + "roast", + "robot", + "robust", + "rocket", + "romance", + "roof", + "rookie", + "room", + "rose", + "rotate", + "rough", + "round", + "route", + "royal", + "rubber", + "rude", + "rug", + "rule", + "run", + "runway", + "rural", + "sad", + "saddle", + "sadness", + "safe", + "sail", + "salad", + "salmon", + "salon", + "salt", + "salute", + "same", + "sample", + "sand", + "satisfy", + "satoshi", + "sauce", + "sausage", + "save", + "say", + "scale", + "scan", + "scare", + "scatter", + "scene", + "scheme", + "school", + "science", + "scissors", + "scorpion", + "scout", + "scrap", + "screen", + "script", + "scrub", + "sea", + "search", + "season", + "seat", + "second", + "secret", + "section", + "security", + "seed", + "seek", + "segment", + "select", + "sell", + "seminar", + "senior", + "sense", + "sentence", + "series", + "service", + "session", + "settle", + "setup", + "seven", + "shadow", + "shaft", + "shallow", + "share", + "shed", + "shell", + "sheriff", + "shield", + "shift", + "shine", + "ship", + "shiver", + "shock", + "shoe", + "shoot", + "shop", + "short", + "shoulder", + "shove", + "shrimp", + "shrug", + "shuffle", + "shy", + "sibling", + "sick", + "side", + "siege", + "sight", + "sign", + "silent", + "silk", + "silly", + "silver", + "similar", + "simple", + "since", + "sing", + "siren", + "sister", + "situate", + "six", + "size", + "skate", + "sketch", + "ski", + "skill", + "skin", + "skirt", + "skull", + "slab", + "slam", + "sleep", + "slender", + "slice", + "slide", + "slight", + "slim", + "slogan", + "slot", + "slow", + "slush", + "small", + "smart", + "smile", + "smoke", + "smooth", + "snack", + "snake", + "snap", + "sniff", + "snow", + "soap", + "soccer", + "social", + "sock", + "soda", + "soft", + "solar", + "soldier", + "solid", + "solution", + "solve", + "someone", + "song", + "soon", + "sorry", + "sort", + "soul", + "sound", + "soup", + "source", + "south", + "space", + "spare", + "spatial", + "spawn", + "speak", + "special", + "speed", + "spell", + "spend", + "sphere", + "spice", + "spider", + "spike", + "spin", + "spirit", + "split", + "spoil", + "sponsor", + "spoon", + "sport", + "spot", + "spray", + "spread", + "spring", + "spy", + "square", + "squeeze", + "squirrel", + "stable", + "stadium", + "staff", + "stage", + "stairs", + "stamp", + "stand", + "start", + "state", + "stay", + "steak", + "steel", + "stem", + "step", + "stereo", + "stick", + "still", + "sting", + "stock", + "stomach", + "stone", + "stool", + "story", + "stove", + "strategy", + "street", + "strike", + "strong", + "struggle", + "student", + "stuff", + "stumble", + "style", + "subject", + "submit", + "subway", + "success", + "such", + "sudden", + "suffer", + "sugar", + "suggest", + "suit", + "summer", + "sun", + "sunny", + "sunset", + "super", + "supply", + "supreme", + "sure", + "surface", + "surge", + "surprise", + "surround", + "survey", + "suspect", + "sustain", + "swallow", + "swamp", + "swap", + "swarm", + "swear", + "sweet", + "swift", + "swim", + "swing", + "switch", + "sword", + "symbol", + "symptom", + "syrup", + "system", + "table", + "tackle", + "tag", + "tail", + "talent", + "talk", + "tank", + "tape", + "target", + "task", + "taste", + "tattoo", + "taxi", + "teach", + "team", + "tell", + "ten", + "tenant", + "tennis", + "tent", + "term", + "test", + "text", + "thank", + "that", + "theme", + "then", + "theory", + "there", + "they", + "thing", + "this", + "thought", + "three", + "thrive", + "throw", + "thumb", + "thunder", + "ticket", + "tide", + "tiger", + "tilt", + "timber", + "time", + "tiny", + "tip", + "tired", + "tissue", + "title", + "toast", + "tobacco", + "today", + "toddler", + "toe", + "together", + "toilet", + "token", + "tomato", + "tomorrow", + "tone", + "tongue", + "tonight", + "tool", + "tooth", + "top", + "topic", + "topple", + "torch", + "tornado", + "tortoise", + "toss", + "total", + "tourist", + "toward", + "tower", + "town", + "toy", + "track", + "trade", + "traffic", + "tragic", + "train", + "transfer", + "trap", + "trash", + "travel", + "tray", + "treat", + "tree", + "trend", + "trial", + "tribe", + "trick", + "trigger", + "trim", + "trip", + "trophy", + "trouble", + "truck", + "true", + "truly", + "trumpet", + "trust", + "truth", + "try", + "tube", + "tuition", + "tumble", + "tuna", + "tunnel", + "turkey", + "turn", + "turtle", + "twelve", + "twenty", + "twice", + "twin", + "twist", + "two", + "type", + "typical", + "ugly", + "umbrella", + "unable", + "unaware", + "uncle", + "uncover", + "under", + "undo", + "unfair", + "unfold", + "unhappy", + "uniform", + "unique", + "unit", + "universe", + "unknown", + "unlock", + "until", + "unusual", + "unveil", + "update", + "upgrade", + "uphold", + "upon", + "upper", + "upset", + "urban", + "urge", + "usage", + "use", + "used", + "useful", + "useless", + "usual", + "utility", + "vacant", + "vacuum", + "vague", + "valid", + "valley", + "valve", + "van", + "vanish", + "vapor", + "various", + "vast", + "vault", + "vehicle", + "velvet", + "vendor", + "venture", + "venue", + "verb", + "verify", + "version", + "very", + "vessel", + "veteran", + "viable", + "vibrant", + "vicious", + "victory", + "video", + "view", + "village", + "vintage", + "violin", + "virtual", + "virus", + "visa", + "visit", + "visual", + "vital", + "vivid", + "vocal", + "voice", + "void", + "volcano", + "volume", + "vote", + "voyage", + "wage", + "wagon", + "wait", + "walk", + "wall", + "walnut", + "want", + "warfare", + "warm", + "warrior", + "wash", + "wasp", + "waste", + "water", + "wave", + "way", + "wealth", + "weapon", + "wear", + "weasel", + "weather", + "web", + "wedding", + "weekend", + "weird", + "welcome", + "west", + "wet", + "whale", + "what", + "wheat", + "wheel", + "when", + "where", + "whip", + "whisper", + "wide", + "width", + "wife", + "wild", + "will", + "win", + "window", + "wine", + "wing", + "wink", + "winner", + "winter", + "wire", + "wisdom", + "wise", + "wish", + "witness", + "wolf", + "woman", + "wonder", + "wood", + "wool", + "word", + "work", + "world", + "worry", + "worth", + "wrap", + "wreck", + "wrestle", + "wrist", + "write", + "wrong", + "yard", + "year", + "yellow", + "you", + "young", + "youth", + "zebra", + "zero", + "zone", + "zoo", +]; diff --git a/rust/tw_hd_wallet/src/bip39/mod.rs b/rust/tw_hd_wallet/src/bip39/mod.rs new file mode 100644 index 00000000000..d0e1db66b2e --- /dev/null +++ b/rust/tw_hd_wallet/src/bip39/mod.rs @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// 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/lib.rs b/rust/tw_hd_wallet/src/lib.rs new file mode 100644 index 00000000000..cfa3a09fa16 --- /dev/null +++ b/rust/tw_hd_wallet/src/lib.rs @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +extern crate core; + +pub mod bip39; +pub mod ton; + +pub type WalletResult = Result; + +#[derive(Debug, Eq, PartialEq)] +pub enum WalletError { + InvalidMnemonicWordCount, + InvalidMnemonicUnknownWord, + InvalidMnemonicEntropy, + InvalidChecksum, +} diff --git a/rust/tw_hd_wallet/src/ton/mnemonic.rs b/rust/tw_hd_wallet/src/ton/mnemonic.rs new file mode 100644 index 00000000000..27013cdc283 --- /dev/null +++ b/rust/tw_hd_wallet/src/ton/mnemonic.rs @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::bip39::bip39_english::BIP39_WORDS_MAP; +use crate::{WalletError, WalletResult}; + +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 mut invalid = false; + let mut words_count = 0; + for word in 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 != 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 new file mode 100644 index 00000000000..2971881654a --- /dev/null +++ b/rust/tw_hd_wallet/src/ton/mod.rs @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +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; +use tw_hash::{H256, H512}; +use tw_keypair::ed25519::sha512::KeyPair; +use zeroize::ZeroizeOnDrop; + +pub const TON_WALLET_SALT: &[u8] = b"TON default seed"; +pub const TON_WALLET_PBKDF_ITERATIONS: u32 = 100_000; + +/// Used to verify a pair of mnemonic and passphrase only. +const TON_BASIC_SEED_SALT: &[u8] = b"TON seed version"; +/// Equals to `(TON_WALLET_PBKDF_ITERATIONS as f64 / 256.0).floor() as u32`. +const TON_BASIC_SEED_ROUNDS: u32 = 390; + +/// Used to verify a pair of mnemonic and passphrase only. +const TON_PASSPHRASE_SEED_SALT: &[u8] = b"TON fast seed version"; +const TON_PASSPHRASE_SEED_ROUNDS: u32 = 1; + +mod mnemonic; + +#[derive(Debug, ZeroizeOnDrop)] +pub struct TonWallet { + mnemonic: String, + passphrase: Option, + entropy: H512, + seed: H512, +} + +impl TonWallet { + pub const MNEMONIC_WORDS: usize = 24; + + /// 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: &str, passphrase: Option) -> WalletResult { + let mnemonic = normalize_mnemonic(mnemonic); + validate_mnemonic_words(&mnemonic)?; + + match passphrase { + Some(ref passphrase) if !passphrase.is_empty() => { + // Check whether the passphrase is really needed. + if !Self::is_password_needed(&mnemonic) { + return Err(WalletError::InvalidMnemonicEntropy); + } + }, + _ => (), + }; + + let entropy = Self::ton_mnemonic_to_entropy(&mnemonic, passphrase.as_deref()); + + // The pair of `[mnemonic, Option]` should give a `basic` seed. + // Otherwise, `passphrase` is either not set or invalid. + if !is_basic_seed(&entropy) { + return Err(WalletError::InvalidMnemonicEntropy); + } + + let seed = ton_seed(&entropy); + Ok(TonWallet { + mnemonic, + passphrase, + entropy, + seed, + }) + } + + pub fn to_key_pair(&self) -> KeyPair { + let (secret, _): (H256, H256) = self.seed.split(); + KeyPair::from(secret) + } + + /// 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: &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: &str, passphrase: Option<&str>) -> H512 { + let passphrase_bytes = passphrase.map(str::as_bytes).unwrap_or(&[]); + let entropy = hmac_sha512(mnemonic.as_bytes(), passphrase_bytes); + H512::try_from(entropy.as_slice()).expect("hmac_sha512 must return 64 bytes") + } +} + +/// https://github.com/toncenter/tonweb-mnemonic/blob/a338a00d4ca0ed833431e0e49e4cfad766ac713c/src/functions/common.ts#L8-L11 +fn is_basic_seed(entropy: &H512) -> bool { + basic_seed(entropy)[0] == 0 +} + +/// https://github.com/toncenter/tonweb-mnemonic/blob/a338a00d4ca0ed833431e0e49e4cfad766ac713c/src/functions/mnemonic-to-seed.ts#L5-L17 +fn ton_seed(entropy: &H512) -> H512 { + pbkdf2_hmac_sha512( + entropy.as_slice(), + TON_WALLET_SALT, + TON_WALLET_PBKDF_ITERATIONS, + ) +} + +/// https://github.com/toncenter/tonweb-mnemonic/blob/a338a00d4ca0ed833431e0e49e4cfad766ac713c/src/functions/common.ts#L9 +fn basic_seed(entropy: &H512) -> H512 { + pbkdf2_hmac_sha512( + entropy.as_slice(), + TON_BASIC_SEED_SALT, + TON_BASIC_SEED_ROUNDS, + ) +} + +/// https://github.com/toncenter/tonweb-mnemonic/blob/a338a00d4ca0ed833431e0e49e4cfad766ac713c/src/functions/common.ts#L15 +fn password_seed(entropy: &H512) -> H512 { + pbkdf2_hmac_sha512( + entropy.as_slice(), + TON_PASSPHRASE_SEED_SALT, + TON_PASSPHRASE_SEED_ROUNDS, + ) +} + +/// https://github.com/toncenter/tonweb-mnemonic/blob/a338a00d4ca0ed833431e0e49e4cfad766ac713c/src/functions/common.ts#L14-L17 +fn is_password_seed(entropy: &H512) -> bool { + password_seed(entropy)[0] == 1 +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_is_basic_seed() { + let entropy = H512::from("db315e4b9a05d29d4921f3f99c754b52c1035bd20a6c38ae4c39068a844d2d4dccd1b16ab639494155bd635737c9120eab0b0382c6f27d941993ad2a98ee037b"); + let actual = basic_seed(&entropy); + let expected = H512::from("008303d550147ad20420875810a81496510e32e2ec7b1c4129c8fe55c0886ad2b6c8b6ad88427d3614a27997ec3760e4a6aaae45c8a1c70684aad5206e8559ce"); + assert_eq!(actual, expected); + assert!(is_basic_seed(&entropy)); + } + + #[test] + fn test_is_not_basic_seed() { + let entropy = H512::from("5ad5da67282f932eb9e0b66246af357e1e99f73b066ba1095fedf324629f67048c82e7c6d4cf09f204e2b2fcd002ab9bae25da67f99ecb918861d11ec6553a78"); + let actual = basic_seed(&entropy); + let expected = H512::from("fd1ab5001f7fec237ad7b90dbd3a8a6d716409688d23517cc80314b79f36f93a21aac798c39778684688b4763bf0294874c067ef3d28d854101c4e5616839dfd"); + assert_eq!(actual, expected); + assert!(!is_basic_seed(&entropy)); + } + + #[test] + fn test_is_password_seed() { + let entropy = H512::from("f0fc0e9610e3a215db47df1fc8dc2142ec4e8559ee1ec384fc3e51234d63c34087f4a6bdb2ad2c5a46a4504e30c9ab4ef7d92dc1836c2854ecef1f7988e60100"); + let actual = password_seed(&entropy); + let expected = H512::from("014944aac60ad889acab074b850df15b745b2c6ca5367c3ba02f4ae22e5a8953ac33413c0cd7fdb9108935bd6ed82acc73d4ac94202d83933a5480642c371eed"); + assert_eq!(actual, expected); + assert!(is_password_seed(&entropy)); + } + + #[test] + fn test_is_not_password_seed() { + let entropy = H512::from("85092586b7d675688d52b5870966546c7fb0144a2d94badd7c3960d6e9de3094016cea3aa155f5a9b3ce61d5b4ad8393984ed153dc0866304c911ba2edd2ea9e"); + let actual = password_seed(&entropy); + let expected = H512::from("31f4dd02b333be5635dc46245d67792b85e9b74d788a749caf3d45bfb1cd094a039626ba1a8bff77fa2436a6a228568d29884c2b274e09c0fe722f07980aab8e"); + assert_eq!(actual, expected); + assert!(!is_password_seed(&entropy)); + } +} diff --git a/rust/tw_hd_wallet/tests/ton_mnemonic.rs b/rust/tw_hd_wallet/tests/ton_mnemonic.rs new file mode 100644 index 00000000000..3125140ad90 --- /dev/null +++ b/rust/tw_hd_wallet/tests/ton_mnemonic.rs @@ -0,0 +1,156 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_encoding::hex::ToHex; +use tw_hd_wallet::ton::TonWallet; +use tw_hd_wallet::WalletError; +use tw_keypair::traits::KeyPairTrait; +use tw_misc::traits::ToBytesZeroizing; + +struct MnemonicTest { + mnemonic: &'static str, + passphrase: &'static str, + expected_private: &'static str, + expected_public: &'static str, +} + +fn mnemonic_to_keypair_impl(input: MnemonicTest) { + let passphrase = if input.passphrase.is_empty() { + None + } else { + Some(input.passphrase.to_string()) + }; + + let wallet = TonWallet::new(input.mnemonic, passphrase).unwrap(); + let key_pair = wallet.to_key_pair(); + + assert_eq!( + key_pair.private().to_zeroizing_vec().to_hex(), + input.expected_private, + "Invalid private key" + ); + assert_eq!( + key_pair.public().to_bytes().to_hex(), + input.expected_public, + "Invalid public key" + ); +} + +struct MnemonicErrorTest { + mnemonic: &'static str, + passphrase: &'static str, +} + +fn mnemonic_to_keypair_error(input: MnemonicErrorTest) { + let passphrase = if input.passphrase.is_empty() { + None + } else { + Some(input.passphrase.to_string()) + }; + + assert!( + TonWallet::new(input.mnemonic, passphrase).is_err(), + "Expected an error" + ); +} + +/// All tests generated by using `https://github.com/toncenter/tonweb-mnemonic/`. +#[test] +fn test_mnemonic_to_keypair_no_passphrase() { + mnemonic_to_keypair_impl(MnemonicTest { + mnemonic: "document shield addict crime broom point story depend suit satisfy test chicken valid tail speak fortune sound drill seek cube cheap body music recipe", + passphrase: "", + expected_private: "112d4e2e700a468f1eae699329202f1ee671d6b665caa2d92dea038cf3868c18", + expected_public: "4d656c35c830bf78d239d3225727dd1da051be0ec521c98e3012beafbb06f306", + }); + mnemonic_to_keypair_impl(MnemonicTest { + mnemonic: "slogan train glide measure mercy dizzy when satoshi vote change length pluck token walnut actress hollow guard soup solve rival summer vicious anxiety device", + passphrase: "", + expected_private: "ee11da8e64d17a8416c88a6a24a1e16569cc85a077b7b209528975c32a44a0c8", + expected_public: "3bab20a5f77e277e39443fc16c64e0479b4a9db542bf9e11c638598384c853f1", + }); + mnemonic_to_keypair_impl(MnemonicTest { + mnemonic: "laundry myself fitness beyond prize piano match acid vacuum already abandon dance occur pause grocery company inject excuse weasel carpet fog grunt trick spike", + passphrase: "", + expected_private: "859cd74ab605afb7ce9f5316a1f6d59217a130b75b494efd249913be874c9d46", + expected_public: "c9af50596bd5c1c5a15fb32bef8d4f1ee5244b287aea1f49f6023a79f9b2f055", + }); + mnemonic_to_keypair_impl(MnemonicTest { + mnemonic: "slim holiday tiny pizza donor egg round three verify post chat social offer mix rack soft loud code option learn this pipe mouse mango", + passphrase: "", + expected_private: "cdfd1e2a1f947701bddba2636c26a6d6d13efacba5e6fdc624254be9bf8cbc3b", + expected_public: "c1266dc4e8040e462af34fa7da9130950caf8c8a1bab78c9b938d2eb6e3d9a69", + }); +} + +/// All tests generated by using `https://github.com/toncenter/tonweb-mnemonic/`. +#[test] +fn test_mnemonic_to_keypair_with_passphrase() { + mnemonic_to_keypair_impl(MnemonicTest { + mnemonic: "afford skate husband stamp style affair jeans episode afraid mom pupil canal borrow artwork fetch excite shiver conduct acoustic rail crisp consider pave people", + passphrase: ".", + expected_private: "ddaed03c283c9e60883b6c7cda86af40a1a820a8181276900094db0d23b55144", + expected_public: "46aacb0e9e1faba24e0e87d4bf7ee5e54beaa4142fa1bd608324d7c67d78070e", + }); + mnemonic_to_keypair_impl(MnemonicTest { + mnemonic: "mimic close sibling chair shuffle goat fashion chunk increase tennis scene ceiling divert cross treat happy soccer sample umbrella oyster advance quality perfect call", + passphrase: "My passphrase", + expected_private: "78a6d95981847d6b7fb6b85be43071fbd83f4b0cebc1fd0c75147fde3c88d9e2", + expected_public: "452a6031290df95e972a269f3c042f5b18497ab27b8fe9915e5b5c94037382a6", + }); + mnemonic_to_keypair_impl(MnemonicTest { + mnemonic: "kind loan rifle gadget forward tortoise switch tuition orchard ball monkey glow gallery diary nature dynamic survey flush correct employ autumn wife disease coin", + passphrase: "189r012 9jr90fj--901hr8921'0r912j90", + expected_private: "ced0feac4f8cc46909a5a172d390f126afe46540c07c5163c194429269e6eb08", + expected_public: "482c9619307639a4b1699e83d771d656e5cf7be3ef877d849940cfebd718783e", + }); + mnemonic_to_keypair_impl(MnemonicTest { + mnemonic: " predict pelican worry swallow brother real truck fiber trophy melody joy kitten luggage lake woman clutch frost crop about stumble frozen kitchen mutual food ", + passphrase: "Foo Bar Zar", + expected_private: "f93ed43c379cb8210754016ffa669880b259854160446c1a12c5858258c32601", + expected_public: "f44d7f29bc8801f882097611544fcafe84cca05408006fa5b76b63f55464e4d0", + }); +} + +#[test] +fn test_mnemonic_to_keypair_error_expected_passphrase() { + // This mnemonic can only be used along with the "My passphrase" passphrase. + let mnemonic = "mimic close sibling chair shuffle goat fashion chunk increase tennis scene ceiling divert cross treat happy soccer sample umbrella oyster advance quality perfect call"; + mnemonic_to_keypair_error(MnemonicErrorTest { + mnemonic, + passphrase: "", + }); + mnemonic_to_keypair_error(MnemonicErrorTest { + mnemonic, + passphrase: "Unexpected passphrase", + }); +} + +#[test] +fn test_mnemonic_to_keypair_error_expected_no_passphrase() { + // This mnemonic can only be used without passphrase. + let mnemonic = "slogan train glide measure mercy dizzy when satoshi vote change length pluck token walnut actress hollow guard soup solve rival summer vicious anxiety device"; + mnemonic_to_keypair_error(MnemonicErrorTest { + mnemonic, + passphrase: "Hello world", + }); + mnemonic_to_keypair_error(MnemonicErrorTest { + mnemonic, + passphrase: "...", + }); +} + +#[test] +fn test_invalid_mnemonic() { + // 24 words mnemonic is supported only. + 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 = 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 = 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/tw_keypair/src/ed25519/keypair.rs b/rust/tw_keypair/src/ed25519/keypair.rs index 3ac2ac1e1fa..eed4a8ca6d0 100644 --- a/rust/tw_keypair/src/ed25519/keypair.rs +++ b/rust/tw_keypair/src/ed25519/keypair.rs @@ -6,6 +6,7 @@ use crate::ed25519::{private::PrivateKey, public::PublicKey, signature::Signatur use crate::traits::{KeyPairTrait, SigningKeyTrait, VerifyingKeyTrait}; use crate::{KeyPairError, KeyPairResult}; use tw_encoding::hex; +use tw_hash::H256; use zeroize::Zeroizing; /// Represents a pair of `ed25519` private and public keys. @@ -46,6 +47,14 @@ impl VerifyingKeyTrait for KeyPair { } } +impl From for KeyPair { + fn from(secret: H256) -> Self { + let private = PrivateKey::from(secret); + let public = private.public(); + KeyPair { private, public } + } +} + impl<'a, H: Hasher512> TryFrom<&'a [u8]> for KeyPair { type Error = KeyPairError; diff --git a/rust/tw_keypair/src/ed25519/private.rs b/rust/tw_keypair/src/ed25519/private.rs index 984392393a6..dc5ab54a68e 100644 --- a/rust/tw_keypair/src/ed25519/private.rs +++ b/rust/tw_keypair/src/ed25519/private.rs @@ -11,6 +11,7 @@ use crate::{KeyPairError, KeyPairResult}; use std::fmt; use tw_encoding::hex; use tw_hash::H256; +use tw_memory::Data; use tw_misc::traits::ToBytesZeroizing; use zeroize::{ZeroizeOnDrop, Zeroizing}; @@ -37,6 +38,12 @@ impl PrivateKey { PublicKey::with_expanded_secret(&self.expanded_key) } + /// Returns a reference to the private key bytes. + /// Please note if clone bytes, it must be zeroized with [`zeroize::Zeroize::zeroize`]. + pub fn bytes(&self) -> &H256 { + &self.secret + } + /// `ed25519` signing uses a public key associated with the private key. pub(crate) fn sign_with_public_key( &self, @@ -49,7 +56,7 @@ impl PrivateKey { } impl SigningKeyTrait for PrivateKey { - type SigningMessage = Vec; + type SigningMessage = Data; type Signature = Signature; fn sign(&self, message: Self::SigningMessage) -> KeyPairResult { @@ -57,16 +64,22 @@ impl SigningKeyTrait for PrivateKey { } } +impl From for PrivateKey { + fn from(secret: H256) -> Self { + let expanded_key = ExpandedSecretKey::::with_secret(secret); + PrivateKey { + secret, + expanded_key, + } + } +} + impl TryFrom<&[u8]> for PrivateKey { type Error = KeyPairError; fn try_from(data: &[u8]) -> Result { let secret = H256::try_from(data).map_err(|_| KeyPairError::InvalidSecretKey)?; - let expanded_key = ExpandedSecretKey::::with_secret(secret); - Ok(PrivateKey { - secret, - expanded_key, - }) + Ok(PrivateKey::from(secret)) } } @@ -80,7 +93,7 @@ impl<'a, H: Hasher512> TryFrom<&'a str> for PrivateKey { } impl ToBytesZeroizing for PrivateKey { - fn to_zeroizing_vec(&self) -> Zeroizing> { + fn to_zeroizing_vec(&self) -> Zeroizing { Zeroizing::new(self.secret.to_vec()) } } diff --git a/rust/tw_keypair/src/ffi/privkey.rs b/rust/tw_keypair/src/ffi/privkey.rs index 9d4efa7f8f4..c1a1b978569 100644 --- a/rust/tw_keypair/src/ffi/privkey.rs +++ b/rust/tw_keypair/src/ffi/privkey.rs @@ -8,10 +8,11 @@ use crate::ffi::pubkey::TWPublicKey; use crate::tw::{Curve, PrivateKey, PublicKeyType}; use tw_memory::ffi::c_byte_array::CByteArray; use tw_memory::ffi::c_byte_array_ref::CByteArrayRef; +use tw_memory::ffi::tw_data::TWData; use tw_memory::ffi::RawPtrTrait; use tw_misc::{try_or_else, try_or_false}; -pub struct TWPrivateKey(pub(crate) PrivateKey); +pub struct TWPrivateKey(pub PrivateKey); impl RawPtrTrait for TWPrivateKey {} @@ -67,6 +68,17 @@ pub unsafe extern "C" fn tw_private_key_is_valid( PrivateKey::is_valid(priv_key_slice, curve) } +/// Convert the given private key to raw-bytes block of data. +/// +/// \param key Non-null pointer to the private key +/// \note The returned block should be deleted with \tw_data_delete_zeroizing +/// \return Non-null block of data (raw bytes) of the given private key +#[no_mangle] +pub unsafe extern "C" fn tw_private_key_data(key: *const TWPrivateKey) -> *mut TWData { + let key = try_or_else!(TWPrivateKey::from_ptr_as_ref(key), std::ptr::null_mut); + TWData::from(key.0.bytes().to_vec()).into_ptr() +} + /// Signs a digest using ECDSA and given curve. /// /// \param key *non-null* pointer to a Private key @@ -76,7 +88,7 @@ pub unsafe extern "C" fn tw_private_key_is_valid( /// \return Signature as a C-compatible result with a C-compatible byte array. #[no_mangle] pub unsafe extern "C" fn tw_private_key_sign( - key: *mut TWPrivateKey, + key: *const TWPrivateKey, message: *const u8, message_len: usize, curve: u32, @@ -100,7 +112,7 @@ pub unsafe extern "C" fn tw_private_key_sign( /// \return *non-null* pointer to the corresponding public key. #[no_mangle] pub unsafe extern "C" fn tw_private_key_get_public_key_by_type( - key: *mut TWPrivateKey, + key: *const TWPrivateKey, pubkey_type: u32, ) -> *mut TWPublicKey { let ty = try_or_else!(PublicKeyType::from_raw(pubkey_type), std::ptr::null_mut); diff --git a/rust/tw_keypair/src/ffi/pubkey.rs b/rust/tw_keypair/src/ffi/pubkey.rs index 7f05f5f0fe9..690f1c3e645 100644 --- a/rust/tw_keypair/src/ffi/pubkey.rs +++ b/rust/tw_keypair/src/ffi/pubkey.rs @@ -10,7 +10,7 @@ use tw_memory::ffi::c_byte_array_ref::CByteArrayRef; use tw_memory::ffi::RawPtrTrait; use tw_misc::{try_or_else, try_or_false}; -pub struct TWPublicKey(pub(crate) PublicKey); +pub struct TWPublicKey(pub PublicKey); impl AsRef for TWPublicKey { fn as_ref(&self) -> &PublicKey { diff --git a/rust/tw_keypair/src/test_utils/tw_crypto_box_helpers.rs b/rust/tw_keypair/src/test_utils/tw_crypto_box_helpers.rs index 88b4a146003..34c2a0f965a 100644 --- a/rust/tw_keypair/src/test_utils/tw_crypto_box_helpers.rs +++ b/rust/tw_keypair/src/test_utils/tw_crypto_box_helpers.rs @@ -4,10 +4,10 @@ use crate::ffi::crypto_box::public_key::{tw_crypto_box_public_key_delete, TWCryptoBoxPublicKey}; use crate::ffi::crypto_box::secret_key::{tw_crypto_box_secret_key_delete, TWCryptoBoxSecretKey}; -use tw_memory::test_utils::tw_wrapper::{TWWrapper, WithDestructor}; +use tw_memory::test_utils::tw_wrapper::{TWAutoWrapper, WithDestructor}; -pub type TWCryptoBoxSecretKeyHelper = TWWrapper; -pub type TWCryptoBoxPublicKeyHelper = TWWrapper; +pub type TWCryptoBoxSecretKeyHelper = TWAutoWrapper; +pub type TWCryptoBoxPublicKeyHelper = TWAutoWrapper; impl WithDestructor for TWCryptoBoxSecretKey { fn destructor() -> unsafe extern "C" fn(*mut Self) { diff --git a/rust/tw_keypair/src/test_utils/tw_private_key_helper.rs b/rust/tw_keypair/src/test_utils/tw_private_key_helper.rs index 6c3c3bfc629..d745fb2a1e3 100644 --- a/rust/tw_keypair/src/test_utils/tw_private_key_helper.rs +++ b/rust/tw_keypair/src/test_utils/tw_private_key_helper.rs @@ -2,9 +2,12 @@ // // Copyright © 2017 Trust Wallet. -use crate::ffi::privkey::{tw_private_key_create_with_data, tw_private_key_delete, TWPrivateKey}; +use crate::ffi::privkey::{ + tw_private_key_create_with_data, tw_private_key_data, tw_private_key_delete, TWPrivateKey, +}; use tw_encoding::hex; use tw_memory::ffi::c_byte_array::CByteArray; +use tw_memory::test_utils::tw_data_helper::TWDataHelper; use tw_memory::Data; pub struct TWPrivateKeyHelper { @@ -12,6 +15,18 @@ pub struct TWPrivateKeyHelper { } impl TWPrivateKeyHelper { + pub fn wrap(ptr: *mut TWPrivateKey) -> TWPrivateKeyHelper { + TWPrivateKeyHelper { ptr } + } + + pub fn bytes(&self) -> Option { + if self.ptr.is_null() { + return None; + } + let data = TWDataHelper::wrap(unsafe { tw_private_key_data(self.ptr) }); + data.to_vec() + } + pub fn with_bytes>(bytes: T) -> TWPrivateKeyHelper { let priv_key_raw = CByteArray::from(bytes.into()); let ptr = diff --git a/rust/tw_keypair/src/tw/private.rs b/rust/tw_keypair/src/tw/private.rs index 0516f451798..b325b3ca834 100644 --- a/rust/tw_keypair/src/tw/private.rs +++ b/rust/tw_keypair/src/tw/private.rs @@ -55,6 +55,11 @@ impl PrivateKey { Ok(&self.bytes[Self::EXTENDED_CARDANO_RANGE]) } + /// Returns bytes (32 or 192 bytes). + pub fn bytes(&self) -> &[u8] { + &self.bytes + } + /// Checks if the given `bytes` secret is valid in general (without a concrete curve). pub fn is_valid_general(bytes: &[u8]) -> bool { if bytes.len() != Self::SIZE && bytes.len() != Self::CARDANO_SIZE { @@ -203,3 +208,11 @@ impl PrivateKey { schnorr::PrivateKey::try_from(self.key().as_slice()) } } + +impl From for PrivateKey { + fn from(key: ed25519::sha512::PrivateKey) -> Self { + PrivateKey { + bytes: key.bytes().to_vec(), + } + } +} diff --git a/rust/tw_keypair/tests/crypto_box_ffi_tests.rs b/rust/tw_keypair/tests/crypto_box_ffi_tests.rs index bf98b67b07e..4dbe6bb67e5 100644 --- a/rust/tw_keypair/tests/crypto_box_ffi_tests.rs +++ b/rust/tw_keypair/tests/crypto_box_ffi_tests.rs @@ -17,11 +17,12 @@ use tw_keypair::test_utils::tw_crypto_box_helpers::{ TWCryptoBoxPublicKeyHelper, TWCryptoBoxSecretKeyHelper, }; use tw_memory::test_utils::tw_data_helper::TWDataHelper; -use tw_memory::test_utils::tw_wrapper::TWWrapper; +use tw_memory::test_utils::tw_wrapper::TWAutoWrapper; fn random_key_pair() -> (TWCryptoBoxSecretKeyHelper, TWCryptoBoxPublicKeyHelper) { - let secret = TWWrapper::wrap(unsafe { tw_crypto_box_secret_key_create() }); - let pubkey = TWWrapper::wrap(unsafe { tw_crypto_box_secret_key_get_public_key(secret.ptr()) }); + let secret = TWAutoWrapper::wrap(unsafe { tw_crypto_box_secret_key_create() }); + let pubkey = + TWAutoWrapper::wrap(unsafe { tw_crypto_box_secret_key_get_public_key(secret.ptr()) }); (secret, pubkey) } @@ -55,7 +56,7 @@ fn test_encrypt_decrypt_easy_error() { .decode_hex() .unwrap(), ); - let other_pubkey = TWWrapper::wrap(unsafe { + let other_pubkey = TWAutoWrapper::wrap(unsafe { tw_crypto_box_public_key_create_with_data(other_pubkey_data.ptr()) }); @@ -83,8 +84,9 @@ fn test_public_key() { let pubkey_data = TWDataHelper::create(pubkey_bytes.clone()); assert!(unsafe { tw_crypto_box_public_key_is_valid(pubkey_data.ptr()) }); - let pubkey = - TWWrapper::wrap(unsafe { tw_crypto_box_public_key_create_with_data(pubkey_data.ptr()) }); + let pubkey = TWAutoWrapper::wrap(unsafe { + tw_crypto_box_public_key_create_with_data(pubkey_data.ptr()) + }); let actual_data = TWDataHelper::wrap(unsafe { tw_crypto_box_public_key_data(pubkey.ptr()) }); assert_eq!(actual_data.to_vec().unwrap(), pubkey_bytes); } @@ -98,8 +100,9 @@ fn test_secret_key() { let secret_data = TWDataHelper::create(secret_bytes.clone()); assert!(unsafe { tw_crypto_box_secret_key_is_valid(secret_data.ptr()) }); - let pubkey = - TWWrapper::wrap(unsafe { tw_crypto_box_secret_key_create_with_data(secret_data.ptr()) }); + let pubkey = TWAutoWrapper::wrap(unsafe { + tw_crypto_box_secret_key_create_with_data(secret_data.ptr()) + }); let actual_data = TWDataHelper::wrap(unsafe { tw_crypto_box_secret_key_data(pubkey.ptr()) }); assert_eq!(actual_data.to_vec().unwrap(), secret_bytes); } diff --git a/rust/tw_memory/Cargo.toml b/rust/tw_memory/Cargo.toml index 2c7b443f83f..642b3494812 100644 --- a/rust/tw_memory/Cargo.toml +++ b/rust/tw_memory/Cargo.toml @@ -4,6 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] +zeroize = "1.8.1" [features] test-utils = [] diff --git a/rust/tw_memory/src/ffi/tw_data.rs b/rust/tw_memory/src/ffi/tw_data.rs index 7a573d8fe4b..675d0d11e08 100644 --- a/rust/tw_memory/src/ffi/tw_data.rs +++ b/rust/tw_memory/src/ffi/tw_data.rs @@ -5,6 +5,7 @@ use crate::ffi::c_byte_array_ref::CByteArrayRef; use crate::ffi::RawPtrTrait; use crate::Data; +use zeroize::Zeroize; /// Defines a resizable block of data. /// @@ -79,6 +80,18 @@ pub unsafe extern "C" fn tw_data_delete(data: *mut TWData) { let _ = TWData::from_ptr(data); } +/// Deletes a block of data zeroizing the memory. +/// +/// \param data A non-null valid block of data +#[no_mangle] +pub unsafe extern "C" fn tw_data_delete_zeroizing(data: *mut TWData) { + // Take the ownership back to rust and drop the owner. + let Some(mut data) = TWData::from_ptr(data) else { + return; + }; + data.0.zeroize(); +} + /// Returns the raw pointer to the contents of data. /// /// \param data A non-null valid block of data diff --git a/rust/tw_memory/src/test_utils/tw_wrapper.rs b/rust/tw_memory/src/test_utils/tw_wrapper.rs index 448b16097e9..397ee144d6c 100644 --- a/rust/tw_memory/src/test_utils/tw_wrapper.rs +++ b/rust/tw_memory/src/test_utils/tw_wrapper.rs @@ -2,17 +2,22 @@ // // Copyright © 2017 Trust Wallet. +pub type Destructor = unsafe extern "C" fn(*mut T); + pub trait WithDestructor: Sized { fn destructor() -> unsafe extern "C" fn(*mut Self); } -pub struct TWWrapper { +/// Structure pointer wrapper. +/// Use this wrapper when `T` can implement the `WithDestructor` trait. +/// Otherwise, consider using `TWWrapper`. +pub struct TWAutoWrapper { ptr: *mut T, } -impl TWWrapper { +impl TWAutoWrapper { pub fn wrap(ptr: *mut T) -> Self { - TWWrapper { ptr } + TWAutoWrapper { ptr } } pub fn ptr(&self) -> *mut T { @@ -20,7 +25,7 @@ impl TWWrapper { } } -impl Drop for TWWrapper { +impl Drop for TWAutoWrapper { fn drop(&mut self) { if self.ptr.is_null() { return; @@ -28,3 +33,28 @@ impl Drop for TWWrapper { unsafe { (T::destructor())(self.ptr) } } } + +/// Structure pointer wrapper with a manual destructor. +pub struct TWWrapper { + ptr: *mut T, + destructor: Destructor, +} + +impl TWWrapper { + pub fn wrap(ptr: *mut T, destructor: Destructor) -> Self { + TWWrapper { ptr, destructor } + } + + pub fn ptr(&self) -> *mut T { + self.ptr + } +} + +impl Drop for TWWrapper { + fn drop(&mut self) { + if self.ptr.is_null() { + return; + } + unsafe { (self.destructor)(self.ptr) } + } +} diff --git a/rust/tw_tests/tests/chains/ton/ton_wallet.rs b/rust/tw_tests/tests/chains/ton/ton_wallet.rs index e7cdf8f35f9..a2187e038ed 100644 --- a/rust/tw_tests/tests/chains/ton/ton_wallet.rs +++ b/rust/tw_tests/tests/chains/ton/ton_wallet.rs @@ -2,14 +2,44 @@ // // Copyright © 2017 Trust Wallet. +use tw_encoding::hex::ToHex; +use tw_keypair::test_utils::tw_private_key_helper::TWPrivateKeyHelper; use tw_keypair::test_utils::tw_public_key_helper::TWPublicKeyHelper; use tw_keypair::tw::PublicKeyType; use tw_memory::test_utils::tw_string_helper::TWStringHelper; +use tw_memory::test_utils::tw_wrapper::TWWrapper; use tw_ton::resources::WALLET_ID_V5R1_TON_MAINNET; -use wallet_core_rs::ffi::ton::wallet::{ +use wallet_core_rs::ffi::wallet::ton_wallet::{ tw_ton_wallet_build_v4_r2_state_init, tw_ton_wallet_build_v5_r1_state_init, + tw_ton_wallet_create_with_mnemonic, tw_ton_wallet_delete, tw_ton_wallet_get_key, + tw_ton_wallet_is_valid_mnemonic, }; +#[test] +fn test_ton_wallet_is_valid_mnemonic() { + let mnemonic = TWStringHelper::create("protect drill sugar gallery note admit input wrist chicken swarm scheme hedgehog orbit ritual glove ski buddy slogan fragile sun delay toy lucky require"); + let passphrase = TWStringHelper::create(""); + let invalid_passphrase = TWStringHelper::create("Expected empty passphrase"); + assert!(unsafe { tw_ton_wallet_is_valid_mnemonic(mnemonic.ptr(), passphrase.ptr()) }); + assert!(!unsafe { tw_ton_wallet_is_valid_mnemonic(mnemonic.ptr(), invalid_passphrase.ptr()) }); +} + +#[test] +fn test_ton_wallet_get_key() { + let mnemonic = TWStringHelper::create("protect drill sugar gallery note admit input wrist chicken swarm scheme hedgehog orbit ritual glove ski buddy slogan fragile sun delay toy lucky require"); + let passphrase = std::ptr::null(); + let wallet = TWWrapper::wrap( + unsafe { tw_ton_wallet_create_with_mnemonic(mnemonic.ptr(), passphrase) }, + tw_ton_wallet_delete, + ); + let key = TWPrivateKeyHelper::wrap(unsafe { tw_ton_wallet_get_key(wallet.ptr()) }); + let key_data = key.bytes().unwrap(); + assert_eq!( + key_data.to_hex(), + "cdcea50b87d3f1ca859e7b2bdf9a5339b7b6804b5c70ac85198829f9607dc43b" + ); +} + #[test] fn test_ton_wallet_v4_r2_create_state_init() { let public_key = TWPublicKeyHelper::with_hex( diff --git a/rust/wallet_core_rs/Cargo.toml b/rust/wallet_core_rs/Cargo.toml index 1b8f34f853a..3ce587ab341 100644 --- a/rust/wallet_core_rs/Cargo.toml +++ b/rust/wallet_core_rs/Cargo.toml @@ -12,6 +12,7 @@ default = [ "any-coin", "bitcoin", "ethereum", + "hd_wallet", "keypair", "solana", "ton", @@ -20,6 +21,7 @@ default = [ any-coin = ["tw_any_coin"] bitcoin = ["tw_bitcoin", "tw_coin_registry"] ethereum = ["tw_ethereum", "tw_coin_registry"] +hd_wallet = ["tw_hd_wallet"] keypair = ["tw_keypair"] solana = ["tw_solana"] ton = ["tw_ton"] @@ -47,4 +49,5 @@ tw_misc = { path = "../tw_misc" } tw_proto = { path = "../tw_proto", optional = true } tw_solana = { path = "../chains/tw_solana", optional = true } tw_ton = { path = "../chains/tw_ton", optional = true } +tw_hd_wallet = { path = "../tw_hd_wallet", optional = true } uuid = { version = "1.7", features = ["v4"], optional = true } diff --git a/rust/wallet_core_rs/src/ffi/mod.rs b/rust/wallet_core_rs/src/ffi/mod.rs index 7b77f1ed28a..513396b4a06 100644 --- a/rust/wallet_core_rs/src/ffi/mod.rs +++ b/rust/wallet_core_rs/src/ffi/mod.rs @@ -12,3 +12,5 @@ pub mod solana; pub mod ton; #[cfg(feature = "utils")] pub mod utils; +#[cfg(feature = "hd_wallet")] +pub mod wallet; diff --git a/rust/wallet_core_rs/src/ffi/ton/mod.rs b/rust/wallet_core_rs/src/ffi/ton/mod.rs index 02721354dfc..fe4f7cf25f6 100644 --- a/rust/wallet_core_rs/src/ffi/ton/mod.rs +++ b/rust/wallet_core_rs/src/ffi/ton/mod.rs @@ -4,4 +4,3 @@ pub mod address_converter; pub mod message_signer; -pub mod wallet; diff --git a/rust/wallet_core_rs/src/ffi/ton/wallet.rs b/rust/wallet_core_rs/src/ffi/ton/wallet.rs deleted file mode 100644 index 0e3c81a1971..00000000000 --- a/rust/wallet_core_rs/src/ffi/ton/wallet.rs +++ /dev/null @@ -1,55 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -// -// Copyright © 2017 Trust Wallet. - -#![allow(clippy::missing_safety_doc)] - -use tw_keypair::ffi::pubkey::TWPublicKey; -use tw_memory::ffi::tw_string::TWString; -use tw_memory::ffi::RawPtrTrait; -use tw_misc::try_or_else; -use tw_ton::modules::wallet_provider::WalletProvider; - -/// Constructs a TON Wallet V4R2 stateInit encoded as BoC (BagOfCells) for the given `public_key`. -/// -/// \param public_key wallet's public key. -/// \param workchain TON workchain to which the wallet belongs. Usually, base chain is used (0). -/// \param wallet_id wallet's ID allows to create multiple wallets for the same private key. -/// \return Pointer to a base64 encoded Bag Of Cells (BoC) StateInit. Null if invalid public key provided. -#[no_mangle] -pub unsafe extern "C" fn tw_ton_wallet_build_v4_r2_state_init( - public_key: *const TWPublicKey, - workchain: i32, - wallet_id: i32, -) -> *mut TWString { - let public_key = try_or_else!(TWPublicKey::from_ptr_as_ref(public_key), std::ptr::null_mut); - let ed_pubkey = try_or_else!(public_key.as_ref().to_ed25519(), std::ptr::null_mut).clone(); - - let state_init = try_or_else!( - WalletProvider::v4r2_state_init(ed_pubkey, workchain, wallet_id), - std::ptr::null_mut - ); - TWString::from(state_init).into_ptr() -} - -// Constructs a TON Wallet V5R1 stateInit encoded as BoC (BagOfCells) for the given `public_key`. -/// -/// \param public_key wallet's public key. -/// \param workchain TON workchain to which the wallet belongs. Usually, base chain is used (0). -/// \param wallet_id wallet's ID allows to create multiple wallets for the same private key. -/// \return Pointer to a base64 encoded Bag Of Cells (BoC) StateInit. Null if invalid public key provided. -#[no_mangle] -pub unsafe extern "C" fn tw_ton_wallet_build_v5_r1_state_init( - public_key: *const TWPublicKey, - workchain: i32, - wallet_id: i32, -) -> *mut TWString { - let public_key = try_or_else!(TWPublicKey::from_ptr_as_ref(public_key), std::ptr::null_mut); - let ed_pubkey = try_or_else!(public_key.as_ref().to_ed25519(), std::ptr::null_mut).clone(); - - let state_init = try_or_else!( - WalletProvider::v5r1_state_init(ed_pubkey, workchain, wallet_id), - std::ptr::null_mut - ); - TWString::from(state_init).into_ptr() -} diff --git a/rust/wallet_core_rs/src/ffi/wallet/mod.rs b/rust/wallet_core_rs/src/ffi/wallet/mod.rs new file mode 100644 index 00000000000..3a0788a1e2c --- /dev/null +++ b/rust/wallet_core_rs/src/ffi/wallet/mod.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub mod ton_wallet; diff --git a/rust/wallet_core_rs/src/ffi/wallet/ton_wallet.rs b/rust/wallet_core_rs/src/ffi/wallet/ton_wallet.rs new file mode 100644 index 00000000000..d367824ed81 --- /dev/null +++ b/rust/wallet_core_rs/src/ffi/wallet/ton_wallet.rs @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#![allow(clippy::missing_safety_doc)] + +use tw_hd_wallet::ton::TonWallet; +use tw_keypair::ffi::privkey::TWPrivateKey; +use tw_keypair::ffi::pubkey::TWPublicKey; +use tw_keypair::traits::KeyPairTrait; +use tw_keypair::tw; +use tw_memory::ffi::tw_string::TWString; +use tw_memory::ffi::RawPtrTrait; +use tw_misc::{try_or_else, try_or_false}; +use tw_ton::modules::wallet_provider::WalletProvider; + +pub struct TWTONWallet(TonWallet); + +impl RawPtrTrait for TWTONWallet {} + +/// Determines whether the English mnemonic and passphrase are valid. +/// +/// \param mnemonic Non-null english mnemonic +/// \param passphrase Nullable optional passphrase +/// \note passphrase can be null or empty string if no passphrase required +/// \return whether the mnemonic and passphrase are valid (valid checksum) +#[no_mangle] +pub unsafe extern "C" fn tw_ton_wallet_is_valid_mnemonic( + mnemonic: *const TWString, + passphrase: *const TWString, +) -> bool { + let mnemonic = try_or_false!(TWString::from_ptr_as_ref(mnemonic)); + let mnemonic = try_or_false!(mnemonic.as_str()); + + let passphrase = TWString::from_ptr_as_ref(passphrase) + .and_then(TWString::as_str) + .map(|pass| pass.to_string()); + + TonWallet::new(mnemonic, passphrase).is_ok() +} + +/// Creates a `TONWallet` from a valid TON mnemonic and passphrase. +/// +/// \param mnemonic Non-null english mnemonic +/// \param passphrase Nullable optional passphrase +/// \note Null is returned on invalid mnemonic and passphrase +/// \note passphrase can be null or empty string if no passphrase required +/// \return Nullable TWTONWallet +#[no_mangle] +pub unsafe extern "C" fn tw_ton_wallet_create_with_mnemonic( + mnemonic: *const TWString, + passphrase: *const TWString, +) -> *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 passphrase = TWString::from_ptr_as_ref(passphrase) + .and_then(TWString::as_str) + .map(|pass| pass.to_string()); + + let wallet = try_or_else!(TonWallet::new(mnemonic, passphrase), std::ptr::null_mut); + TWTONWallet(wallet).into_ptr() +} + +/// Delete the given TON mnemonic. +/// +/// \param wallet *non-null* pointer to TWTONMnemonic. +#[no_mangle] +pub unsafe extern "C" fn tw_ton_wallet_delete(wallet: *mut TWTONWallet) { + // Take the ownership back to rust and drop the owner. + let _ = TWTONWallet::from_ptr(wallet); +} + +/// Generates Ed25519 private key associated with the wallet. +/// +/// \param wallet non-null TWTONWallet +/// \note Returned object needs to be deleted with \TWPrivateKeyDelete +/// \return The Ed25519 private key +#[no_mangle] +pub unsafe extern "C" fn tw_ton_wallet_get_key(wallet: *const TWTONWallet) -> *mut TWPrivateKey { + let wallet = try_or_else!(TWTONWallet::from_ptr_as_ref(wallet), std::ptr::null_mut); + let key = wallet.0.to_key_pair().private().clone(); + TWPrivateKey(tw::PrivateKey::from(key)).into_ptr() +} + +/// Constructs a TON Wallet V4R2 stateInit encoded as BoC (BagOfCells) for the given `public_key`. +/// +/// \param public_key wallet's public key. +/// \param workchain TON workchain to which the wallet belongs. Usually, base chain is used (0). +/// \param wallet_id wallet's ID allows to create multiple wallets for the same private key. +/// \return Pointer to a base64 encoded Bag Of Cells (BoC) StateInit. Null if invalid public key provided. +#[no_mangle] +pub unsafe extern "C" fn tw_ton_wallet_build_v4_r2_state_init( + public_key: *const TWPublicKey, + workchain: i32, + wallet_id: i32, +) -> *mut TWString { + let public_key = try_or_else!(TWPublicKey::from_ptr_as_ref(public_key), std::ptr::null_mut); + let ed_pubkey = try_or_else!(public_key.as_ref().to_ed25519(), std::ptr::null_mut).clone(); + + let state_init = try_or_else!( + WalletProvider::v4r2_state_init(ed_pubkey, workchain, wallet_id), + std::ptr::null_mut + ); + TWString::from(state_init).into_ptr() +} + +// Constructs a TON Wallet V5R1 stateInit encoded as BoC (BagOfCells) for the given `public_key`. +/// +/// \param public_key wallet's public key. +/// \param workchain TON workchain to which the wallet belongs. Usually, base chain is used (0). +/// \param wallet_id wallet's ID allows to create multiple wallets for the same private key. +/// \return Pointer to a base64 encoded Bag Of Cells (BoC) StateInit. Null if invalid public key provided. +#[no_mangle] +pub unsafe extern "C" fn tw_ton_wallet_build_v5_r1_state_init( + public_key: *const TWPublicKey, + workchain: i32, + wallet_id: i32, +) -> *mut TWString { + let public_key = try_or_else!(TWPublicKey::from_ptr_as_ref(public_key), std::ptr::null_mut); + let ed_pubkey = try_or_else!(public_key.as_ref().to_ed25519(), std::ptr::null_mut).clone(); + + let state_init = try_or_else!( + WalletProvider::v5r1_state_init(ed_pubkey, workchain, wallet_id), + std::ptr::null_mut + ); + TWString::from(state_init).into_ptr() +} diff --git a/src/DerivationPath.cpp b/src/DerivationPath.cpp index dd54e28efbc..c2d369ee413 100644 --- a/src/DerivationPath.cpp +++ b/src/DerivationPath.cpp @@ -46,6 +46,10 @@ DerivationPath::DerivationPath(const std::string& string) { } std::string DerivationPath::string() const noexcept { + if (indices.empty()) { + return {}; + } + std::string result = "m/"; for (auto& index : indices) { result += index.string(); diff --git a/src/Keystore/StoredKey.cpp b/src/Keystore/StoredKey.cpp index b216461fe31..732e0953e37 100644 --- a/src/Keystore/StoredKey.cpp +++ b/src/Keystore/StoredKey.cpp @@ -8,6 +8,7 @@ #include "HexCoding.h" #include "Mnemonic.h" #include "PrivateKey.h" +#include "TheOpenNetwork/TONWallet.h" #include #include @@ -67,6 +68,24 @@ StoredKey StoredKey::createWithPrivateKeyAddDefaultAddress(const std::string& na return key; } +StoredKey StoredKey::createWithTonMnemonic(const std::string& name, const Data& password, const std::string& tonMnemonic, TWStoredKeyEncryption encryption) { + if (!TheOpenNetwork::TONWallet::isValidMnemonic(tonMnemonic, std::nullopt)) { + throw std::invalid_argument("Invalid TON mnemonic"); + } + + Data mnemonicData = TW::Data(tonMnemonic.begin(), tonMnemonic.end()); + StoredKey key(StoredKeyType::tonMnemonicPhrase, name, password, mnemonicData, TWStoredKeyEncryptionLevelDefault, encryption); + memzero(mnemonicData.data(), mnemonicData.size()); + return key; +} + +StoredKey StoredKey::createWithTonMnemonicAddDefaultAddress(const std::string& name, const Data& password, TWCoinType coin, const std::string& tonMnemonic, TWStoredKeyEncryption encryption) { + StoredKey key = createWithTonMnemonic(name, password, tonMnemonic, encryption); + const auto tonWallet = key.tonWallet(password); + key.account(coin, TWDerivationDefault, tonWallet); + return key; +} + StoredKey::StoredKey(StoredKeyType type, std::string name, const Data& password, const Data& data, TWStoredKeyEncryptionLevel encryptionLevel, TWStoredKeyEncryption encryption) : type(type), id(), name(std::move(name)), accounts() { const auto encryptionParams = EncryptionParameters::getPreset(encryptionLevel, encryption); @@ -86,6 +105,20 @@ const HDWallet<> StoredKey::wallet(const Data& password) const { return HDWallet<>(mnemonic, ""); } +TheOpenNetwork::TONWallet StoredKey::tonWallet(const Data& password) const { + if (type != StoredKeyType::tonMnemonicPhrase) { + throw std::invalid_argument("Invalid account requested."); + } + const auto data = payload.decrypt(password); + const auto tonMnemonic = std::string(reinterpret_cast(data.data()), data.size()); + + auto maybeTonWallet = TheOpenNetwork::TONWallet::createWithMnemonic(tonMnemonic, std::nullopt); + if (!maybeTonWallet.has_value()) { + throw std::invalid_argument("Invalid TON mnemonic phrase."); + } + return std::move(*maybeTonWallet); +} + std::vector StoredKey::getAccounts(TWCoinType coin) const { std::vector result; for (auto& account : accounts) { @@ -143,6 +176,12 @@ std::optional StoredKey::getAccount(TWCoinType coin, TWDerivation deriv return getAccount(coin, address); } +std::optional StoredKey::getAccount(TWCoinType coin, TWDerivation derivation, const TheOpenNetwork::TONWallet& wallet) const { + // obtain address + const auto address = wallet.deriveAddress(coin, derivation); + return getAccount(coin, address); +} + Account StoredKey::fillAddressIfMissing(Account& account, const HDWallet<>* wallet) const { if (account.address.empty() && wallet != nullptr) { account.address = wallet->deriveAddress(account.coin, account.derivation); @@ -201,6 +240,28 @@ Account StoredKey::account(TWCoinType coin, TWDerivation derivation, const HDWal return accounts.back(); } +Account StoredKey::account(TWCoinType coin, TWDerivation derivation, const TheOpenNetwork::TONWallet& tonWallet) { + const auto coinAccount = getAccount(coin, derivation, tonWallet); + if (coinAccount.has_value()) { + // No need to use `fillAddressIfMissing` + // because `getAccount` searches for an account with both `coin` and `address` equal to expected. + // So `address` is always set. + return coinAccount.value(); + } + // Not found, add it. + + // No derivation path for TON wallet. Use default + const DerivationPath derivationPath {}; + const auto address = tonWallet.deriveAddress(coin, derivation); + // No extended public key for TON wallet. Use default + const std::string extendedPublicKey {}; + const auto pubKeyType = TW::publicKeyType(coin); + const auto pubKey = tonWallet.getKey(coin, derivation).getPublicKey(pubKeyType); + + addAccount(address, coin, derivation, derivationPath, hex(pubKey.bytes), extendedPublicKey); + return accounts.back(); +} + std::optional StoredKey::account(TWCoinType coin) const { return getDefaultAccountOrAny(coin, nullptr); } @@ -254,14 +315,23 @@ const PrivateKey StoredKey::privateKey(TWCoinType coin, const Data& password) { return privateKey(coin, TWDerivationDefault, password); } -const PrivateKey StoredKey::privateKey(TWCoinType coin, [[maybe_unused]] TWDerivation derivation, const Data& password) { - if (type == StoredKeyType::mnemonicPhrase) { - const auto wallet = this->wallet(password); - const Account& account = this->account(coin, derivation, wallet); - return wallet.getKey(coin, account.derivationPath); +const PrivateKey StoredKey::privateKey(TWCoinType coin, TWDerivation derivation, const Data& password) { + switch (type) { + case StoredKeyType::mnemonicPhrase: { + const auto wallet = this->wallet(password); + const Account account = this->account(coin, derivation, wallet); + return wallet.getKey(coin, account.derivationPath); + } + case StoredKeyType::privateKey: { + return PrivateKey(payload.decrypt(password)); + } + case StoredKeyType::tonMnemonicPhrase: { + const auto tonWallet = this->tonWallet(password); + return tonWallet.getKey(coin, derivation); + } + default: + throw std::invalid_argument("Unexpected StoredKey type"); } - // type == StoredKeyType::privateKey - return PrivateKey(payload.decrypt(password)); } void StoredKey::fixAddresses(const Data& password) { @@ -289,6 +359,21 @@ void StoredKey::fixAddresses(const Data& password) { updateAddressForAccount(key, account); } } break; + + case StoredKeyType::tonMnemonicPhrase: { + const auto tonWallet = this->tonWallet(password); + for (auto& account : accounts) { + if (!account.address.empty() && !account.publicKey.empty() && + TW::validateAddress(account.coin, account.address)) { + continue; + } + const auto key = tonWallet.getKey(account.coin, account.derivation); + updateAddressForAccount(key, account); + } + } break; + + default: + throw std::invalid_argument("Unexpected StoredKey type"); } } @@ -340,12 +425,18 @@ static const auto crypto = "Crypto"; namespace TypeString { static const auto privateKey = "private-key"; static const auto mnemonic = "mnemonic"; +static const auto tonMnemonic = "ton-mnemonic"; } // namespace TypeString void StoredKey::loadJson(const nlohmann::json& json) { - if (json.count(CodingKeys::SK::type) != 0 && - json[CodingKeys::SK::type].get() == TypeString::mnemonic) { + const auto isSKType = [](const nlohmann::json& json, const std::string& expected) -> bool { + return json.count(CodingKeys::SK::type) != 0 && json[CodingKeys::SK::type].get() == expected; + }; + + if (isSKType(json, TypeString::mnemonic)) { type = StoredKeyType::mnemonicPhrase; + } else if (isSKType(json, TypeString::tonMnemonic)) { + type = StoredKeyType::tonMnemonicPhrase; } else { type = StoredKeyType::privateKey; } @@ -396,6 +487,9 @@ nlohmann::json StoredKey::json() const { case StoredKeyType::mnemonicPhrase: j[CodingKeys::SK::type] = TypeString::mnemonic; break; + case StoredKeyType::tonMnemonicPhrase: + j[CodingKeys::SK::type] = TypeString::tonMnemonic; + break; } if (id) { diff --git a/src/Keystore/StoredKey.h b/src/Keystore/StoredKey.h index 12eaf5038f4..a98aace31df 100644 --- a/src/Keystore/StoredKey.h +++ b/src/Keystore/StoredKey.h @@ -7,7 +7,8 @@ #include "Account.h" #include "EncryptionParameters.h" #include "Data.h" -#include "../HDWallet.h" +#include "HDWallet.h" +#include "TheOpenNetwork/TONWallet.h" #include #include @@ -20,9 +21,13 @@ namespace TW::Keystore { -/// An stored key can be either a private key or a mnemonic phrase for a HD -/// wallet. -enum class StoredKeyType { privateKey, mnemonicPhrase }; +/// An stored key can be either a private key, or a mnemonic phrase for a HD +/// wallet, or a TON-specific mnemonic phrase. +enum class StoredKeyType { + privateKey, + mnemonicPhrase, + tonMnemonicPhrase +}; /// Represents a key stored as an encrypted file. class StoredKey { @@ -62,6 +67,16 @@ class StoredKey { /// @throws std::invalid_argument if privateKeyData is not a valid private key static StoredKey createWithPrivateKeyAddDefaultAddress(const std::string& name, const Data& password, TWCoinType coin, const Data& privateKeyData, TWStoredKeyEncryption encryption = TWStoredKeyEncryptionAes128Ctr); + /// Create a new StoredKey, with the given name and TON specific mnemonic. + /// @throws std::invalid_argument if menmonic is invalid + /// @note mnemonic created with a passphrase is not supported yet + static StoredKey createWithTonMnemonic(const std::string& name, const Data& password, const std::string& tonMnemonic, TWStoredKeyEncryption encryption = TWStoredKeyEncryptionAes128Ctr); + + /// Create a new StoredKey, with the given name and TON specific mnemonic, and also add the default address for the given coin. + /// @throws std::invalid_argument if menmonic is invalid + /// @note mnemonic created with a passphrase is not supported yet + static StoredKey createWithTonMnemonicAddDefaultAddress(const std::string& name, const Data& password, TWCoinType coin, const std::string& tonMnemonic, TWStoredKeyEncryption encryption = TWStoredKeyEncryptionAes128Ctr); + /// Create a StoredKey from a JSON object. static StoredKey createWithJson(const nlohmann::json& json); @@ -70,19 +85,31 @@ class StoredKey { /// @throws std::invalid_argument if this key is of a type other than `mnemonicPhrase`. const HDWallet<> wallet(const Data& password) const; + /// Returns the TONWallet for this key. + /// + /// @throws std::invalid_argument if this key is of a type other than `tonMnemonicPhrase`. + TheOpenNetwork::TONWallet tonWallet(const Data& password) const; + /// Returns all the accounts for a specific coin: 0, 1, or more. std::vector getAccounts(TWCoinType coin) const; - /// If found, returns the account for a specific coin. In case of muliple accounts, the default derivation is returned, or the first one is returned. + /// If found, returns the account for a specific coin. + /// In case of multiple accounts, the default derivation is returned, or the first one is returned. /// If none exists, and wallet is not null, an account is created (with default derivation). std::optional account(TWCoinType coin, const HDWallet<>* wallet); - /// If found, returns the account for a specific coin and derivation. In case of muliple accounts, the first one is returned. + /// If found, returns the account for a specific coin and derivation. + /// In case of multiple accounts, the first one is returned. /// If none exists, an account is created. Account account(TWCoinType coin, TWDerivation derivation, const HDWallet<>& wallet); + /// If found, returns the account for a specific coin. + /// In case of multiple accounts, the first one is returned. + /// If none exists, an account is created. + Account account(TWCoinType coin, TWDerivation derivation, const TheOpenNetwork::TONWallet& tonWallet); + /// Returns the account for a specific coin if it exists. - /// In case of muliple accounts, the default derivation is returned, or the first one is returned. + /// In case of multiple accounts, the default derivation is returned, or the first one is returned. std::optional account(TWCoinType coin) const; /// Returns the account for a specific coin and derivation, if it exists. @@ -173,6 +200,9 @@ class StoredKey { /// Find account by coin+derivation (should be one, if multiple, first is returned) std::optional getAccount(TWCoinType coin, TWDerivation derivation, const HDWallet<>& wallet) const; + /// Find account by coin+derivation (should be one, if multiple, first is returned) + std::optional getAccount(TWCoinType coin, TWDerivation derivation, const TheOpenNetwork::TONWallet& wallet) const; + /// Re-derive account address if missing Account fillAddressIfMissing(Account& account, const HDWallet<>* wallet) const; diff --git a/src/TheOpenNetwork/TONWallet.cpp b/src/TheOpenNetwork/TONWallet.cpp new file mode 100644 index 00000000000..a4b4adc6c28 --- /dev/null +++ b/src/TheOpenNetwork/TONWallet.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "TONWallet.h" +#include "Coin.h" + +namespace TW::TheOpenNetwork { + +bool TONWallet::isValidMnemonic(const std::string& mnemonic, const MaybePassphrase& passphrase) { + const Rust::TWStringWrapper mnemonicRust = mnemonic; + + if (passphrase.has_value()) { + const Rust::TWStringWrapper passphraseRust = passphrase.value(); + return Rust::tw_ton_wallet_is_valid_mnemonic(mnemonicRust.get(), passphraseRust.get()); + } else { + return Rust::tw_ton_wallet_is_valid_mnemonic(mnemonicRust.get(), nullptr); + } +} + +MaybeTONWallet TONWallet::createWithMnemonic(const std::string& mnemonic, const MaybePassphrase& passphrase) { + const Rust::TWStringWrapper mnemonicRust = mnemonic; + + Rust::TWTONWallet* walletPtr; + if (passphrase.has_value()) { + const Rust::TWStringWrapper passphraseRust = passphrase.value(); + walletPtr = Rust::tw_ton_wallet_create_with_mnemonic(mnemonicRust.get(), passphraseRust.get()); + } else { + walletPtr = Rust::tw_ton_wallet_create_with_mnemonic(mnemonicRust.get(), nullptr); + } + + if (!walletPtr) { + return std::nullopt; + } + + return TONWallet(TONWalletPtr(walletPtr, Rust::tw_ton_wallet_delete)); +} + +PrivateKey TONWallet::getKey(TWCoinType coin, TWDerivation derivation) const { + if (coin != TWCoinTypeTON || derivation != TWDerivationDefault) { + throw std::invalid_argument("'TONWallet' supports TON coin and Default derivation only"); + } + + const auto privateKeyRust = wrapTWPrivateKey(Rust::tw_ton_wallet_get_key(impl.get())); + const Rust::TWDataWrapper privateKeyBytes = Rust::tw_private_key_data(privateKeyRust.get()); + return PrivateKey(privateKeyBytes.toDataOrDefault()); +} + +std::string TONWallet::deriveAddress(TWCoinType coin, TWDerivation derivation) const { + const auto key = getKey(coin, derivation); + return TW::deriveAddress(coin, key, derivation); +} + +} // namespace TW::TheOpenNetwork diff --git a/src/TheOpenNetwork/TONWallet.h b/src/TheOpenNetwork/TONWallet.h new file mode 100644 index 00000000000..b6d80e1f0e2 --- /dev/null +++ b/src/TheOpenNetwork/TONWallet.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "PrivateKey.h" +#include "rust/Wrapper.h" + +#include "TrustWalletCore/TWCoinType.h" +#include "TrustWalletCore/TWDerivation.h" + +namespace TW::TheOpenNetwork { + +class TONWallet; + +using TONWalletPtr = std::shared_ptr; +using MaybePassphrase = std::optional; +using MaybeTONWallet = std::optional; + +class TONWallet { +public: + static bool isValidMnemonic(const std::string& mnemonic, const MaybePassphrase& passphrase); + + static MaybeTONWallet createWithMnemonic(const std::string& mnemonic, const MaybePassphrase& passphrase); + + /// Returns the private key with the given coin and derivation. + /// \throws std exception if `coin` or `derivation` aren't `TWCoinTypeTON` and `TWDerivationDefault` correspondingly. + PrivateKey getKey(TWCoinType coin = TWCoinTypeTON, TWDerivation derivation = TWDerivationDefault) const; + + /// Derives the address for the given coin and derivation. + /// \throws std exception if `coin` or `derivation` aren't `TWCoinTypeTON` and `TWDerivationDefault` correspondingly. + std::string deriveAddress(TWCoinType coin = TWCoinTypeTON, TWDerivation derivation = TWDerivationDefault) const; + +private: + explicit TONWallet(TONWalletPtr ptr): impl(std::move(ptr)) { + } + + TONWalletPtr impl; +}; + +} // namespace TW::TheOpenNetwork + +/// Wrapper for C interface. +struct TWTONWallet { + TW::TheOpenNetwork::TONWallet impl; +}; diff --git a/src/interface/TWStoredKey.cpp b/src/interface/TWStoredKey.cpp index f756ce5e6b3..90c78c94e13 100644 --- a/src/interface/TWStoredKey.cpp +++ b/src/interface/TWStoredKey.cpp @@ -72,6 +72,21 @@ struct TWStoredKey* _Nullable TWStoredKeyImportHDWalletWithEncryption(TWString* } } +struct TWStoredKey* _Nullable TWStoredKeyImportTONWallet(TWString* _Nonnull tonMnemonic, TWString* _Nonnull name, TWData* _Nonnull password, enum TWCoinType coin) { + return TWStoredKeyImportTONWalletWithEncryption(tonMnemonic, name, password, coin, TWStoredKeyEncryptionAes128Ctr); +} + +struct TWStoredKey* _Nullable TWStoredKeyImportTONWalletWithEncryption(TWString* _Nonnull tonMnemonic, TWString* _Nonnull name, TWData* _Nonnull password, enum TWCoinType coin, enum TWStoredKeyEncryption encryption) { + try { + const auto& tonMnemonicString = *reinterpret_cast(tonMnemonic); + const auto& nameString = *reinterpret_cast(name); + const auto passwordData = TW::data(TWDataBytes(password), TWDataSize(password)); + return new TWStoredKey{ KeyStore::StoredKey::createWithTonMnemonicAddDefaultAddress(nameString, passwordData, coin, tonMnemonicString, encryption) }; + } catch (...) { + return nullptr; + } +} + struct TWStoredKey* _Nullable TWStoredKeyImportJSON(TWData* _Nonnull json) { try { const auto& d = *reinterpret_cast(json); @@ -101,6 +116,10 @@ bool TWStoredKeyIsMnemonic(struct TWStoredKey* _Nonnull key) { return key->impl.type == KeyStore::StoredKeyType::mnemonicPhrase; } +bool TWStoredKeyIsTONMnemonic(struct TWStoredKey* _Nonnull key) { + return key->impl.type == KeyStore::StoredKeyType::tonMnemonicPhrase; +} + size_t TWStoredKeyAccountCount(struct TWStoredKey* _Nonnull key) { return key->impl.accounts.size(); } @@ -191,6 +210,13 @@ TWString* _Nullable TWStoredKeyDecryptMnemonic(struct TWStoredKey* _Nonnull key, } } +TWString* _Nullable TWStoredKeyDecryptTONMnemonic(struct TWStoredKey* _Nonnull key, TWData* _Nonnull password) { + if (!TWStoredKeyIsTONMnemonic(key)) { + return nullptr; + } + return TWStoredKeyDecryptMnemonic(key, password); +} + struct TWPrivateKey* _Nullable TWStoredKeyPrivateKey(struct TWStoredKey* _Nonnull key, enum TWCoinType coin, TWData* _Nonnull password) { try { const auto passwordData = TW::data(TWDataBytes(password), TWDataSize(password)); diff --git a/src/interface/TWTONWallet.cpp b/src/interface/TWTONWallet.cpp index 7e6495491ab..137a672f2c1 100644 --- a/src/interface/TWTONWallet.cpp +++ b/src/interface/TWTONWallet.cpp @@ -3,11 +3,46 @@ // Copyright © 2017 Trust Wallet. #include "TrustWalletCore/TWTONWallet.h" +#include "TheOpenNetwork/TONWallet.h" #include "rust/Wrapper.h" #include "PublicKey.h" using namespace TW; +bool TWTONWalletIsValidMnemonic(TWString* _Nonnull mnemonic, TWString* _Nullable passphrase) { + const auto& mnemonicRef = *reinterpret_cast(mnemonic); + std::string passphraseObj; + if (passphrase) { + passphraseObj = std::string(TWStringUTF8Bytes(passphrase), TWStringSize(passphrase)); + } + + return TheOpenNetwork::TONWallet::isValidMnemonic(mnemonicRef, passphraseObj); +} + +struct TWTONWallet* _Nullable TWTONWalletCreateWithMnemonic(TWString* _Nonnull mnemonic, TWString* _Nullable passphrase) { + const auto& mnemonicRef = *reinterpret_cast(mnemonic); + std::string passphraseObj; + if (passphrase) { + passphraseObj = std::string(TWStringUTF8Bytes(passphrase), TWStringSize(passphrase)); + } + + const auto maybeWallet = TheOpenNetwork::TONWallet::createWithMnemonic(mnemonicRef, passphraseObj); + if (!maybeWallet.has_value()) { + return nullptr; + } + + return new TWTONWallet { .impl = *maybeWallet }; +} + +void TWTONWalletDelete(struct TWTONWallet* _Nonnull wallet) { + delete wallet; +} + +struct TWPrivateKey* _Nonnull TWTONWalletGetKey(struct TWTONWallet* _Nonnull wallet) { + const auto privateKey = wallet->impl.getKey(); + return new TWPrivateKey { .impl = privateKey }; +} + TWString *_Nullable TWTONWalletBuildV4R2StateInit(struct TWPublicKey *_Nonnull publicKey, int32_t workchain, int32_t walletId) { auto keyType = static_cast(TWPublicKeyKeyType(publicKey)); auto* publicKeyRustRaw = Rust::tw_public_key_create_with_data(publicKey->impl.bytes.data(), publicKey->impl.bytes.size(), keyType); diff --git a/src/rust/Wrapper.h b/src/rust/Wrapper.h index 04d97a9104c..1ac64605951 100644 --- a/src/rust/Wrapper.h +++ b/src/rust/Wrapper.h @@ -13,11 +13,15 @@ namespace TW::Rust { inline std::shared_ptr wrapTWAnyAddress(TWAnyAddress* anyAddress) { - return std::shared_ptr(anyAddress, tw_any_address_delete); + return { anyAddress, tw_any_address_delete }; +} + +inline std::shared_ptr wrapTWPrivateKey(TWPrivateKey* privateKey) { + return { privateKey, tw_private_key_delete }; } inline std::shared_ptr wrapTWPublicKey(TWPublicKey* publicKey) { - return std::shared_ptr(publicKey, tw_public_key_delete); + return { publicKey, tw_public_key_delete }; } struct TWDataVectorWrapper { diff --git a/swift/Sources/KeyStore.swift b/swift/Sources/KeyStore.swift index 0b6c3368d77..e1a6b7bfa8f 100644 --- a/swift/Sources/KeyStore.swift +++ b/swift/Sources/KeyStore.swift @@ -6,6 +6,12 @@ import Foundation +private enum StoredKeyType { + case privateKey(PrivateKey) + case mnemonic(String) + case tonMnemonic(String) +} + /// Manages directories of key and wallet files and presents them as accounts. public final class KeyStore { static let watchesFileName = "watches.json" @@ -130,25 +136,49 @@ public final class KeyStore { guard let key = StoredKey.importJSON(json: json) else { throw Error.invalidJSON } - guard let data = key.decryptPrivateKey(password: Data(password.utf8)) else { + + switch try decryptSecret(key: key, password: Data(password.utf8)) { + case .privateKey(let privateKey): + return try self.import(privateKey: privateKey, name: name, password: newPassword, coin: coins.first ?? .ethereum) + case .mnemonic(let mnemonic): + return try self.import(mnemonic: mnemonic, name: name, encryptPassword: newPassword, coins: coins) + case .tonMnemonic(let tonMnemonic): + return try self.importTON(tonMnemonic: tonMnemonic, name: name, encryptPassword: newPassword, coin: coins.first ?? .ton) + } + } + + /// Decrypts an inner secret as a `privateKey`, `mnemonic` or another type. + /// - Returns: a `StoredKeyType` enum. + /// - Note: `StoredKey::type` is not always set, and mnemonic or private key can be stored encrypted without any tag. + private func decryptSecret(key: StoredKey, password: Data) throws -> StoredKeyType { + guard var secretData = key.decryptPrivateKey(password: password) else { throw Error.invalidPassword } + defer { + secretData.resetBytes(in: 0 ..< secretData.count) + } - if let mnemonic = checkMnemonic(data) { - return try self.import(mnemonic: mnemonic, name: name, encryptPassword: newPassword, coins: coins) + // First, check whether the key is init with a TON mnemonic. + // That's because TON Wallet is a new feature, and `StoredKey::type` is always set for these kind of keys. + if key.isTONMnemonic { + guard let tonMnemonic = String(data: secretData, encoding: .ascii), TONWallet.isValidMnemonic(mnemonic: tonMnemonic, passphrase: nil) else { + throw Error.invalidMnemonic + } + return StoredKeyType.tonMnemonic(tonMnemonic) } - guard let privateKey = PrivateKey(data: data) else { - throw Error.invalidKey + // The next, we should try to convert the secret into a string and check whether it's a valid BIP39 mnemonic phrase. + if let mnemonic = String(data: secretData, encoding: .ascii), Mnemonic.isValid(mnemonic: mnemonic) { + return StoredKeyType.mnemonic(mnemonic) } - return try self.import(privateKey: privateKey, name: name, password: newPassword, coin: coins.first ?? .ethereum) - } - private func checkMnemonic(_ data: Data) -> String? { - guard let mnemonic = String(data: data, encoding: .ascii), Mnemonic.isValid(mnemonic: mnemonic) else { - return nil + // Otherwise, we consider the secret as a private key. + + if let privateKey = PrivateKey(data: secretData) { + return StoredKeyType.privateKey(privateKey) } - return mnemonic + + throw Error.invalidKey } /// Imports a private key. @@ -193,6 +223,31 @@ public final class KeyStore { return wallet } + + /// Imports a TON wallet. + /// + /// - Parameters: + /// - tonMnemonic: TON wallet's mnemonic phrase + /// - encryptPassword: password to use for encrypting + /// - coin: coins to use for this wallet + /// - Returns: new account + public func `importTON`(tonMnemonic: String, name: String, encryptPassword: String, coin: CoinType, encryption: StoredKeyEncryption = .aes128Ctr) throws -> Wallet { + guard let newKey = StoredKey.importTONWalletWithEncryption(tonMnemonic: tonMnemonic, name: name, password: Data(encryptPassword.utf8), coin: coin, encryption: encryption) else { + throw Error.invalidKey + } + + let url = makeAccountURL() + let wallet = Wallet(keyURL: url, key: newKey) + // `StoredKey.importTONWalletWithEncryption` should create exactly one account only. + if wallet.accounts.count != 1 { + throw Error.invalidKey + } + wallets.append(wallet) + + try save(wallet: wallet) + + return wallet + } /// Exports a wallet as JSON data. /// @@ -202,28 +257,29 @@ public final class KeyStore { /// - newPassword: password to use for exported key /// - Returns: encrypted JSON key public func export(wallet: Wallet, password: String, newPassword: String, encryption: StoredKeyEncryption = .aes128Ctr) throws -> Data { - var privateKeyData = try exportPrivateKey(wallet: wallet, password: password) - defer { - privateKeyData.resetBytes(in: 0 ..< privateKeyData.count) - } - + // TODO why `importHDWalletWithEncryption` is called with a single coin? + // I guess that's because we don't want other wallets to know all user accounts. guard let coin = wallet.key.account(index: 0)?.coin else { throw Error.accountNotFound } - if let mnemonic = checkMnemonic(privateKeyData), let newKey = StoredKey.importHDWalletWithEncryption(mnemonic: mnemonic, name: "", password: Data(newPassword.utf8), coin: coin, encryption: encryption) { - guard let json = newKey.exportJSON() else { - throw Error.invalidKey - } - return json - } else if let newKey = StoredKey.importPrivateKeyWithEncryption(privateKey: privateKeyData, name: "", password: Data(newPassword.utf8), coin: coin, encryption: encryption) { - guard let json = newKey.exportJSON() else { - throw Error.invalidKey - } - return json + let secretType = try decryptSecret(key: wallet.key, password: Data(password.utf8)) + + let maybeNewKey: StoredKey? = switch secretType { + case .privateKey(let privateKey): + StoredKey.importPrivateKeyWithEncryption(privateKey: privateKey.data, name: "", password: Data(newPassword.utf8), coin: coin, encryption: encryption) + + case .mnemonic(let mnemonic): + StoredKey.importHDWalletWithEncryption(mnemonic: mnemonic, name: "", password: Data(newPassword.utf8), coin: coin, encryption: encryption) + + case .tonMnemonic(let tonMnemonic): + StoredKey.importTONWalletWithEncryption(tonMnemonic: tonMnemonic, name: "", password: Data(newPassword.utf8), coin: coin, encryption: encryption) } - throw Error.invalidKey + guard let newKey = maybeNewKey, let json = newKey.exportJSON() else { + throw Error.invalidKey + } + return json } /// Exports a wallet as private key data. @@ -232,6 +288,7 @@ public final class KeyStore { /// - wallet: wallet to export /// - password: account password /// - Returns: private key data for encrypted keys or mnemonic phrase for HD wallets + /// - Throws: `EncryptError.invalidPassword` if the password is incorrect. public func exportPrivateKey(wallet: Wallet, password: String) throws -> Data { guard let key = wallet.key.decryptPrivateKey(password: Data(password.utf8)) else { throw Error.invalidPassword @@ -245,13 +302,27 @@ public final class KeyStore { /// - wallet: wallet to export /// - password: account password /// - Returns: mnemonic phrase - /// - Throws: `EncryptError.invalidMnemonic` if the account is not an HD wallet. + /// - Throws: `EncryptError.invalidPassword` if the password is incorrect. public func exportMnemonic(wallet: Wallet, password: String) throws -> String { guard let mnemonic = wallet.key.decryptMnemonic(password: Data(password.utf8)) else { throw Error.invalidPassword } return mnemonic } + + /// Exports a wallet as a TON mnemonic phrase. + /// + /// - Parameters: + /// - wallet: wallet to export + /// - password: account password + /// - Returns: TON mnemonic phrase + /// - Throws: `EncryptError.invalidPassword` if the password is incorrect. + public func exportTONMnemonic(wallet: Wallet, password: String) throws -> String { + guard let tonMnemonic = wallet.key.decryptTONMnemonic(password: Data(password.utf8)) else { + throw Error.invalidPassword + } + return tonMnemonic + } /// Updates the password of an existing account. /// @@ -278,28 +349,30 @@ public final class KeyStore { fatalError("Missing wallet") } - guard var privateKeyData = wallet.key.decryptPrivateKey(password: Data(password.utf8)) else { - throw Error.invalidPassword - } - defer { - privateKeyData.resetBytes(in: 0 ..< privateKeyData.count) - } - let coins = wallet.accounts.map({ $0.coin }) guard !coins.isEmpty else { throw Error.accountNotFound } - if let mnemonic = checkMnemonic(privateKeyData), - let key = StoredKey.importHDWalletWithEncryption(mnemonic: mnemonic, name: newName, password: Data(newPassword.utf8), coin: coins[0], encryption: encryption) { - wallets[index].key = key - } else if let key = StoredKey.importPrivateKeyWithEncryption( - privateKey: privateKeyData, name: newName, password: Data(newPassword.utf8), coin: coins[0], encryption: encryption) { - wallets[index].key = key - } else { + let secretType = try decryptSecret(key: wallet.key, password: Data(password.utf8)) + + let maybeNewKey: StoredKey? = switch secretType { + case .privateKey(let privateKey): + StoredKey.importPrivateKeyWithEncryption( + privateKey: privateKey.data, name: newName, password: Data(newPassword.utf8), coin: coins[0], encryption: encryption) + + case .mnemonic(let mnemonic): + StoredKey.importHDWalletWithEncryption(mnemonic: mnemonic, name: newName, password: Data(newPassword.utf8), coin: coins[0], encryption: encryption) + + case .tonMnemonic(let tonMnemonic): + StoredKey.importTONWalletWithEncryption(tonMnemonic: tonMnemonic, name: newName, password: Data(newPassword.utf8), coin: coins[0], encryption: encryption) + } + + guard let newKey = maybeNewKey else { throw Error.invalidKey } + wallets[index].key = newKey _ = try wallets[index].getAccounts(password: newPassword, coins: coins) try save(wallet: wallets[index]) } diff --git a/swift/Sources/Wallet.swift b/swift/Sources/Wallet.swift index 04749b5d564..0dc2ec1c6d3 100644 --- a/swift/Sources/Wallet.swift +++ b/swift/Sources/Wallet.swift @@ -59,7 +59,7 @@ public final class Wallet: Hashable, Equatable { return account } - /// Returns the accounts for a specific coins. + /// Returns the accounts for specific coins. /// /// - Parameters: /// - password: wallet encryption password @@ -67,6 +67,10 @@ public final class Wallet: Hashable, Equatable { /// - Returns: the added accounts /// - Throws: `KeyStore.Error.invalidPassword` if the password is incorrect. public func getAccounts(password: String, coins: [CoinType]) throws -> [Account] { + if !key.isMnemonic { + return coins.compactMap({ key.accountForCoin(coin: $0, wallet: nil) }) + } + guard let wallet = key.wallet(password: Data(password.utf8)) else { throw KeyStore.Error.invalidPassword } diff --git a/swift/Tests/Keystore/Data/ton_wallet.json b/swift/Tests/Keystore/Data/ton_wallet.json new file mode 100644 index 00000000000..3b744a4b01e --- /dev/null +++ b/swift/Tests/Keystore/Data/ton_wallet.json @@ -0,0 +1,30 @@ +{ + "activeAccounts": [ + { + "address": "UQBlm676c6vy6Q9Js732pvf3ivfmIkVc0MVDQy-F6NAFJd4k", + "coin": 607, + "derivationPath": "", + "publicKey": "9016f03f9cfa4e183707761f25407e0e1975194a33a56b3e8d2c26f2438fa3d1" + } + ], + "crypto": { + "cipher": "aes-128-ctr", + "cipherparams": { + "iv": "459bbf197fe8e1cdfb949fc516257238" + }, + "ciphertext": "efbda7237b41d2340a4b4c7f0ab6a103e31f7a2b8148137abc815a0d346c3ae851d842ad068ad0218ef9f81647f41e52f7b056d568a314433a0b7980601f96e8974a071b5ddedd50079eebdb9281a8d84a0b9d4649ef3125f6605e303a78c0a4ed67556e3cd4e88b78b120267544eb44a912c92516562acb9782e0ea989cb50bce5948e2dfa26107053caf838af096ce47357061a2b49654", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "n": 16384, + "p": 4, + "r": 8, + "salt": "" + }, + "mac": "be9e9a26076334683799c2dbe22bfb2d9f2ff92ad130aa61b4687b392e7b98e6" + }, + "id": "f7a2172e-fb7a-427a-8526-99779fc47c0a", + "name": "name", + "type": "ton-mnemonic", + "version": 3 +} diff --git a/swift/Tests/Keystore/KeyStoreTests.swift b/swift/Tests/Keystore/KeyStoreTests.swift index 3017d10c304..087dcd5912c 100755 --- a/swift/Tests/Keystore/KeyStoreTests.swift +++ b/swift/Tests/Keystore/KeyStoreTests.swift @@ -25,12 +25,17 @@ extension KeyStore { var bnbWallet: Wallet { return wallets.first(where: { $0.identifier == "bnb_wallet.json"})! } + + var tonWallet: Wallet? { + return wallets.first(where: { $0.identifier == "ton_wallet.json"}) + } } class KeyStoreTests: XCTestCase { let keyAddress = AnyAddress(string: "0x008AeEda4D805471dF9b2A5B0f38A0C3bCBA786b", coin: .ethereum)! let walletAddress = AnyAddress(string: "0x32dd55E0BCF509a35A3F5eEb8593fbEb244796b1", coin: .ethereum)! let mnemonic = "often tobacco bread scare imitate song kind common bar forest yard wisdom" + let tonMnemonic = "laundry myself fitness beyond prize piano match acid vacuum already abandon dance occur pause grocery company inject excuse weasel carpet fog grunt trick spike" let fileManager = FileManager.default var keyDirectory: URL! @@ -73,11 +78,17 @@ class KeyStoreTests: XCTestCase { try? fileManager.removeItem(at: bnbWalletDestination) try? fileManager.copyItem(at: bnbWalletURL, to: bnbWalletDestination) + + let tonWalletURL = Bundle(for: type(of: self)).url(forResource: "ton_wallet", withExtension: "json")! + let tonWalletDestination = keyDirectory.appendingPathComponent("ton_wallet.json") + + try? fileManager.removeItem(at: tonWalletDestination) + try? fileManager.copyItem(at: tonWalletURL, to: tonWalletDestination) } func testLoadKeyStore() { let keyStore = try! KeyStore(keyDirectory: keyDirectory) - XCTAssertEqual(keyStore.wallets.count, 4) + XCTAssertEqual(keyStore.wallets.count, 5) XCTAssertEqual(keyStore.watches.count, 1) } @@ -87,13 +98,13 @@ class KeyStoreTests: XCTestCase { let newWallet = try keyStore.createWallet(name: "name", password: "password", coins: coins) XCTAssertEqual(newWallet.accounts.count, 3) - XCTAssertEqual(keyStore.wallets.count, 5) + XCTAssertEqual(keyStore.wallets.count, 6) XCTAssertNoThrow(try newWallet.getAccount(password: "password", coin: .ethereum)) XCTAssertNoThrow(try newWallet.getAccount(password: "password", coin: .binance)) XCTAssertNoThrow(try newWallet.getAccount(password: "password", coin: .smartChain)) } - func testUpdateKey() throws { + func testUpdatePassword() throws { let keyStore = try KeyStore(keyDirectory: keyDirectory) let coins = [CoinType.ethereum, .callisto, .poanetwork] let wallet = try keyStore.createWallet(name: "name", password: "password", coins: coins) @@ -113,6 +124,21 @@ class KeyStoreTests: XCTestCase { XCTAssertEqual(savedWallet.key.name, "name") } + func testUpdatePasswordTON() throws { + let keyStore = try KeyStore(keyDirectory: keyDirectory) + + try keyStore.update(wallet: keyStore.tonWallet!, password: "password", newPassword: "testpassword") + + let savedKeyStore = try KeyStore(keyDirectory: keyDirectory) + let savedWallet = savedKeyStore.tonWallet! + + let tonMnemonic = savedWallet.key.decryptTONMnemonic(password: Data("testpassword".utf8))! + + XCTAssertEqual(savedWallet.accounts.count, 1) + XCTAssert(TONWallet.isValidMnemonic(mnemonic: tonMnemonic, passphrase: nil)) + XCTAssertEqual(savedWallet.key.name, "name") + } + func testUpdateName() throws { let keyStore = try KeyStore(keyDirectory: keyDirectory) let coins = [CoinType.ethereum, .callisto, .poanetwork] @@ -155,6 +181,17 @@ class KeyStoreTests: XCTestCase { XCTAssertEqual(savedWallet.accounts.count, 1) XCTAssertEqual(savedWallet.accounts[0].coin, coins.last) } + + func testRemoveTONAccounts() throws { + let keyStore = try KeyStore(keyDirectory: keyDirectory) + + _ = try keyStore.removeAccounts(wallet: keyStore.tonWallet!, coins: [.ton], password: "password") + + let savedKeyStore = try KeyStore(keyDirectory: keyDirectory) + let savedWallet = savedKeyStore.tonWallet! + // The only account should have been removed. + XCTAssertEqual(savedWallet.accounts.count, 0) + } func testDeleteKey() throws { let keyStore = try KeyStore(keyDirectory: keyDirectory) @@ -169,6 +206,13 @@ class KeyStoreTests: XCTestCase { try keyStore.delete(wallet: wallet, password: "password") XCTAssertNil(keyStore.hdWallet) } + + func testDeleteTONWallet() throws { + let keyStore = try KeyStore(keyDirectory: keyDirectory) + let wallet = keyStore.tonWallet! + try keyStore.delete(wallet: wallet, password: "password") + XCTAssertNil(keyStore.tonWallet) + } func testImportKey() throws { let keyStore = try KeyStore(keyDirectory: keyDirectory) @@ -232,7 +276,60 @@ class KeyStoreTests: XCTestCase { XCTAssertEqual(wallet.accounts.count, 1) XCTAssertNotNil(keyStore.hdWallet) } + + func testImportTONWallet() throws { + let keyStore = try KeyStore(keyDirectory: keyDirectory) + let wallet = try keyStore.importTON(tonMnemonic: tonMnemonic, name: "name", encryptPassword: "newPassword", coin: .ton, encryption: .aes128Ctr) + let storedData = wallet.key.decryptMnemonic(password: Data("newPassword".utf8)) + XCTAssertNotNil(storedData) + XCTAssertEqual(wallet.accounts.count, 1) + } + + func testImportTONWalletJSON() throws { + let json = """ + { + "activeAccounts": [ + { + "address": "UQByxuJBNpeC4QjGdgnfeO8oM4G9srUG1FyIGmqX3YnVQ4p1", + "coin": 607, + "derivationPath": "", + "publicKey": "3bab20a5f77e277e39443fc16c64e0479b4a9db542bf9e11c638598384c853f1" + } + ], + "crypto": { + "cipher": "aes-128-ctr", + "cipherparams": { + "iv": "d7fdc3fa7a09e1163094d14a557ed1b6" + }, + "ciphertext": "58017dfbee52c6f22fa1eed323cda21e3413de8da48083e09be9a826bc4f9c184f14e7a47ffcc5f539dc94435d1742dfdc0785e612d039c4858777da9dcd92960580cb9c755434832d94f88b8f562a23ad16f7b6165bbd709a701b3ec46efbe5f6aa858000ce19641abcb7d20475fa1e9cfed5f2f5dae7c76d6496d54bd6db593050617c85c0f6bc3cf8fac89b671d53924202037e1c0e1ecd521492e5", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "n": 16384, + "p": 4, + "r": 8, + "salt": "" + }, + "mac": "ddc49eecdec579021cd18526982ec9519a82f8c39ff20aa7d9aa7f02d5ebd36e" + }, + "id": "e11e5404-73d5-416c-b957-a65164fb0171", + "name": "name", + "type": "ton-mnemonic", + "version": 3 + } + """ + + let password = "password" + let keyStore = try KeyStore(keyDirectory: keyDirectory) + + let wallet = try keyStore.import(json: Data(json.utf8), name: "newName", password: "password", newPassword: "newPassword", coins: [.ton]) + XCTAssertEqual(wallet.accounts.first!.address, "UQByxuJBNpeC4QjGdgnfeO8oM4G9srUG1FyIGmqX3YnVQ4p1") + let actualMnemonic = try keyStore.exportTONMnemonic(wallet: wallet, password: "newPassword") + let expectedMnemonic = "slogan train glide measure mercy dizzy when satoshi vote change length pluck token walnut actress hollow guard soup solve rival summer vicious anxiety device" + XCTAssertEqual(actualMnemonic, expectedMnemonic) + } + func testImportJSON() throws { let expected = """ { @@ -332,6 +429,14 @@ class KeyStoreTests: XCTestCase { XCTAssertEqual(mnemonic, exported) } + + func testExportTONMnemonic() throws { + let keyStore = try KeyStore(keyDirectory: keyDirectory) + let wallet = try keyStore.importTON(tonMnemonic: tonMnemonic, name: "name", encryptPassword: "newPassword", coin: .ton) + let exported = try keyStore.exportTONMnemonic(wallet: wallet, password: "newPassword") + + XCTAssertEqual(tonMnemonic, exported) + } func testFileName() { let keyStore = try! KeyStore(keyDirectory: keyDirectory) diff --git a/swift/Tests/Keystore/WalletTests.swift b/swift/Tests/Keystore/WalletTests.swift index 00815ed0271..48cf71436c5 100755 --- a/swift/Tests/Keystore/WalletTests.swift +++ b/swift/Tests/Keystore/WalletTests.swift @@ -25,4 +25,54 @@ class WalletTests: XCTestCase { let wallet = Wallet(keyURL: url, key: key) XCTAssertEqual(wallet.identifier, "UTC--2018-07-23T15-42-07.380692005-42000--6E199F01-FA96-4ADF-9A4B-36EE4B1E08C7") } + + func testPrivateKeyGetAccount() throws { + let url = Bundle(for: type(of: self)).url(forResource: "key", withExtension: "json")! + let key = StoredKey.load(path: url.path)! + let wallet = Wallet(keyURL: url, key: key) + + // The wallet already contains `Ethereum` account. No exception expected. + let ethAccount = try wallet.getAccount(password: "testpassword", coin: .ethereum) + XCTAssertEqual(ethAccount.address, "0x008AeEda4D805471dF9b2A5B0f38A0C3bCBA786b") + + let accounts1 = try wallet.getAccounts(password: "testpassword", coins: [.ethereum]) + XCTAssertEqual(accounts1.count, 1) + + // Should fail because `getAccount` currently doesn't support creating a new account from PrivateKey. + XCTAssertThrowsError(try wallet.getAccount(password: "testpassword", coin: .bitcoin)) + + // Should return only `Ethereum` account. + let accounts2 = try wallet.getAccounts(password: "testpassword", coins: [.bitcoin, .ethereum]) + XCTAssertEqual(accounts2.count, 1) + } + + func testTONWalletGetAccount() throws { + let url = Bundle(for: type(of: self)).url(forResource: "ton_wallet", withExtension: "json")! + let key = StoredKey.load(path: url.path)! + let wallet = Wallet(keyURL: url, key: key) + + // The wallet already contains `TON` account. No exception expected. + let tonAccount = try wallet.getAccount(password: "password", coin: .ton) + XCTAssertEqual(tonAccount.address, "UQBlm676c6vy6Q9Js732pvf3ivfmIkVc0MVDQy-F6NAFJd4k") + + let accounts1 = try wallet.getAccounts(password: "password", coins: [.ton]) + XCTAssertEqual(accounts1.count, 1) + + // Should fail because `getAccount` currently doesn't support creating a new account from PrivateKey. + XCTAssertThrowsError(try wallet.getAccount(password: "password", coin: .ethereum)) + + // Should return only `TON` account. + let accounts2 = try wallet.getAccounts(password: "password", coins: [.bitcoin, .ton]) + XCTAssertEqual(accounts2.count, 1) + } + + func testTONWalletGetPrivateKey() throws { + let url = Bundle(for: type(of: self)).url(forResource: "ton_wallet", withExtension: "json")! + let key = StoredKey.load(path: url.path)! + let wallet = Wallet(keyURL: url, key: key) + + // The wallet already contains `TON` account. No exception expected. + let privateKey = try wallet.privateKey(password: "password", coin: .ton) + XCTAssertEqual(privateKey.data.hexString, "cdcea50b87d3f1ca859e7b2bdf9a5339b7b6804b5c70ac85198829f9607dc43b") + } } diff --git a/tests/chains/TheOpenNetwork/TWTONWalletTests.cpp b/tests/chains/TheOpenNetwork/TWTONWalletTests.cpp index b346ee5568b..b77b47773be 100644 --- a/tests/chains/TheOpenNetwork/TWTONWalletTests.cpp +++ b/tests/chains/TheOpenNetwork/TWTONWalletTests.cpp @@ -9,6 +9,25 @@ namespace TW::TheOpenNetwork::tests { +TEST(TWTONWallet, IsValidMnemonic) { + const auto mnemonic = STRING("sight shed side garbage illness clean health wet all win bench wide exist find galaxy drift task suggest portion fresh valve crime radar combine"); + const auto emptyPassphrase = STRING(""); + const auto invalidPassphrase = STRING("Expected empty passphrase"); + EXPECT_TRUE(TWTONWalletIsValidMnemonic(mnemonic.get(), nullptr)); + EXPECT_TRUE(TWTONWalletIsValidMnemonic(mnemonic.get(), emptyPassphrase.get())); + EXPECT_FALSE(TWTONWalletIsValidMnemonic(mnemonic.get(), invalidPassphrase.get())); +} + +TEST(TWTONWallet, MnemonicToPrivateKey) { + const auto mnemonic = STRING("sight shed side garbage illness clean health wet all win bench wide exist find galaxy drift task suggest portion fresh valve crime radar combine"); + const auto wallet = WRAP(TWTONWallet, TWTONWalletCreateWithMnemonic(mnemonic.get(), nullptr)); + EXPECT_TRUE(wallet); + const auto key = WRAP(TWPrivateKey, TWTONWalletGetKey(wallet.get())); + EXPECT_TRUE(key); + const auto keyBytes = WRAPD(TWPrivateKeyData(key.get())); + assertHexEqual(keyBytes, "b471884e691a9f5bb641b14f33bb9e555f759c24e368c4c0d997db3a60704220"); +} + TEST(TWTONWallet, BuildV4R2StateInit) { auto publicKeyBytes = DATA("f229a9371fa7c2108b3d90ea22c9be705ff5d0cfeaee9cbb9366ff0171579357"); auto publicKey = WRAP(TWPublicKey, TWPublicKeyCreateWithData(publicKeyBytes.get(), TWPublicKeyTypeED25519)); diff --git a/tests/common/Keystore/Data/ton-wallet.json b/tests/common/Keystore/Data/ton-wallet.json new file mode 100644 index 00000000000..3ed1c08b087 --- /dev/null +++ b/tests/common/Keystore/Data/ton-wallet.json @@ -0,0 +1,30 @@ +{ + "activeAccounts": [ + { + "address": "UQBlm676c6vy6Q9Js732pvf3ivfmIkVc0MVDQy-F6NAFJd4k", + "coin": 607, + "derivationPath": "", + "publicKey": "9016f03f9cfa4e183707761f25407e0e1975194a33a56b3e8d2c26f2438fa3d1" + } + ], + "crypto": { + "cipher": "aes-128-ctr", + "cipherparams": { + "iv": "459bbf197fe8e1cdfb949fc516257238" + }, + "ciphertext": "efbda7237b41d2340a4b4c7f0ab6a103e31f7a2b8148137abc815a0d346c3ae851d842ad068ad0218ef9f81647f41e52f7b056d568a314433a0b7980601f96e8974a071b5ddedd50079eebdb9281a8d84a0b9d4649ef3125f6605e303a78c0a4ed67556e3cd4e88b78b120267544eb44a912c92516562acb9782e0ea989cb50bce5948e2dfa26107053caf838af096ce47357061a2b49654", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "n": 16384, + "p": 4, + "r": 8, + "salt": "" + }, + "mac": "be9e9a26076334683799c2dbe22bfb2d9f2ff92ad130aa61b4687b392e7b98e6" + }, + "id": "f7a2172e-fb7a-427a-8526-99779fc47c0a", + "name": "Test TON Account", + "type": "ton-mnemonic", + "version": 3 +} diff --git a/tests/common/Keystore/StoredKeyConstants.h b/tests/common/Keystore/StoredKeyConstants.h new file mode 100644 index 00000000000..93ce7404702 --- /dev/null +++ b/tests/common/Keystore/StoredKeyConstants.h @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +extern std::string TESTS_ROOT; + +namespace TW::Keystore::tests { + +const auto gName = "name"; +const auto gPasswordString = "password"; +const auto gPassword = TW::data(std::string(gPasswordString)); + +inline std::string testDataPath(const char *subpath) { + return TESTS_ROOT + "/common/Keystore/Data/" + subpath; +} + +} // namespace TW::Keystore::tests diff --git a/tests/common/Keystore/StoredKeyTONTests.cpp b/tests/common/Keystore/StoredKeyTONTests.cpp new file mode 100644 index 00000000000..6be77a508d3 --- /dev/null +++ b/tests/common/Keystore/StoredKeyTONTests.cpp @@ -0,0 +1,172 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "Keystore/StoredKey.h" + +#include "Coin.h" +#include "Data.h" +#include "HexCoding.h" +#include "StoredKeyConstants.h" + +#include +#include + +namespace TW::Keystore::tests { + +using namespace std; + +const auto gTONMnemonic = "protect drill sugar gallery note admit input wrist chicken swarm scheme hedgehog orbit ritual glove ski buddy slogan fragile sun delay toy lucky require"; +// The following TON mnemonic requires a passphrase to be used that we don't support right now. +const auto gTONInvalidMnemonic = "mimic close sibling chair shuffle goat fashion chunk increase tennis scene ceiling divert cross treat happy soccer sample umbrella oyster advance quality perfect call"; +const auto gTONPrivateKey = "cdcea50b87d3f1ca859e7b2bdf9a5339b7b6804b5c70ac85198829f9607dc43b"; +const auto gTONPublicKey = "9016f03f9cfa4e183707761f25407e0e1975194a33a56b3e8d2c26f2438fa3d1"; +const auto gBounceableAddress = "EQBlm676c6vy6Q9Js732pvf3ivfmIkVc0MVDQy-F6NAFJYPh"; +const auto gNonBounceableAddress = "UQBlm676c6vy6Q9Js732pvf3ivfmIkVc0MVDQy-F6NAFJd4k"; + +TEST(StoredKeyTON, CreateWithTonMnemonicAddDefault) { + auto key = StoredKey::createWithTonMnemonicAddDefaultAddress(gName, gPassword, TWCoinTypeTON, gTONMnemonic); + EXPECT_EQ(key.type, StoredKeyType::tonMnemonicPhrase); + const Data& mnemo2Data = key.payload.decrypt(gPassword); + EXPECT_EQ(string(mnemo2Data.begin(), mnemo2Data.end()), string(gTONMnemonic)); + EXPECT_EQ(key.accounts.size(), 1ul); + EXPECT_EQ(key.accounts[0].coin, TWCoinTypeTON); + EXPECT_EQ(key.accounts[0].address, "UQBlm676c6vy6Q9Js732pvf3ivfmIkVc0MVDQy-F6NAFJd4k"); + EXPECT_EQ(key.accounts[0].publicKey, gTONPublicKey); + EXPECT_EQ(key.accounts[0].extendedPublicKey, ""); + EXPECT_EQ(key.accounts[0].derivationPath.string(), ""); + EXPECT_EQ(key.accounts[0].derivation, TWDerivationDefault); + EXPECT_EQ(hex(key.privateKey(TWCoinTypeTON, gPassword).bytes), gTONPrivateKey); + EXPECT_EQ(key.payload.params.cipher(), "aes-128-ctr"); + + const auto json = key.json(); + EXPECT_EQ(json["name"], gName); + EXPECT_EQ(json["type"], "ton-mnemonic"); + EXPECT_EQ(json["version"], 3); +} + +TEST(StoredKeyTON, CreateWithTonMnemonicInvalid) { + EXPECT_THROW( + StoredKey::createWithTonMnemonicAddDefaultAddress(gName, gPassword, TWCoinTypeTON, gTONInvalidMnemonic), + std::invalid_argument + ); +} + +TEST(StoredKeyTON, CreateWithTonMnemonicInvalidCoinType) { + EXPECT_THROW( + StoredKey::createWithTonMnemonicAddDefaultAddress(gName, gPassword, TWCoinTypeBitcoin, gTONMnemonic), + std::invalid_argument + ); +} + +TEST(StoredKeyTON, CreateWithMnemonicAddDefaultAddressAes256) { + auto key = StoredKey::createWithTonMnemonicAddDefaultAddress(gName, gPassword, TWCoinTypeTON, gTONMnemonic, TWStoredKeyEncryptionAes256Ctr); + EXPECT_EQ(key.type, StoredKeyType::tonMnemonicPhrase); + const Data& mnemo2Data = key.payload.decrypt(gPassword); + EXPECT_EQ(string(mnemo2Data.begin(), mnemo2Data.end()), string(gTONMnemonic)); + EXPECT_EQ(key.accounts.size(), 1ul); + EXPECT_EQ(key.accounts[0].coin, TWCoinTypeTON); + EXPECT_EQ(key.accounts[0].address, "UQBlm676c6vy6Q9Js732pvf3ivfmIkVc0MVDQy-F6NAFJd4k"); + EXPECT_EQ(key.payload.params.cipher(), "aes-256-ctr"); +} + +TEST(StoredKeyTON, HDWalletNotSupported) { + auto key = StoredKey::createWithTonMnemonicAddDefaultAddress(gName, gPassword, TWCoinTypeTON, gTONMnemonic); + EXPECT_THROW(key.wallet(gPassword), std::invalid_argument); +} + +TEST(StoredKeyTON, AddRemoveAccount) { + auto key = StoredKey::createWithTonMnemonicAddDefaultAddress(gName, gPassword, TWCoinTypeTON, gTONMnemonic); + EXPECT_EQ(key.accounts.size(), 1ul); + + // Add another dummy (doesn't belong to the mnemonic) TON account. + { + const DerivationPath derivationPath {}; + const auto publicKey = "b191d35f81aa8b144aa91c90a6b887e0b165ad9c2933b1c5266eb5c4e8bea241"; + const auto extendedPublicKey = ""; + key.addAccount("UQDSRYDMMez8BdcOuPEiaR6aJZpO6EjlIwmOBFn14mMbnRah", TWCoinTypeTON, TWDerivationDefault, derivationPath, publicKey, extendedPublicKey); + EXPECT_EQ(key.accounts.size(), 2ul); + } + + key.removeAccount(TWCoinTypeTON, TWDerivationDefault); + EXPECT_EQ(key.accounts.size(), 0ul); +} + +TEST(StoredKeyTON, FixAddressHasNoEffect) { + // `StoredKey::createWithTonMnemonicAddDefaultAddress` derives the correct address. + auto key = StoredKey::createWithTonMnemonicAddDefaultAddress(gName, gPassword, TWCoinTypeTON, gTONMnemonic); + EXPECT_EQ(key.accounts.size(), 1ul); + + key.fixAddresses(gPassword); + EXPECT_EQ(key.accounts[0].address, gNonBounceableAddress); +} + +TEST(StoredKeyTON, FixAddress) { + auto key = StoredKey::createWithTonMnemonic(gName, gPassword, gTONMnemonic); + EXPECT_EQ(key.accounts.size(), 0ul); + + // Add an account with an invalid address manually. + { + const DerivationPath derivationPath {}; + const auto publicKey = gTONPublicKey; + const auto extendedPublicKey = ""; + const auto invalidAddress = "INVALID_ADDRESS"; + key.addAccount(invalidAddress, TWCoinTypeTON, TWDerivationDefault, derivationPath, publicKey, extendedPublicKey); + EXPECT_EQ(key.accounts.size(), 1ul); + } + + key.fixAddresses(gPassword); + EXPECT_EQ(key.accounts.size(), 1ul); + EXPECT_EQ(key.accounts[0].coin, TWCoinTypeTON); + // Address should be fixed to a valid non-bounceable address. + EXPECT_EQ(key.accounts[0].address, gNonBounceableAddress); +} + +TEST(StoredKeyTON, UpdateAddress) { + auto key = StoredKey::createWithTonMnemonic(gName, gPassword, gTONMnemonic); + EXPECT_EQ(key.accounts.size(), 0ul); + + // Add an account with a bounceable (EQ) address. + { + const DerivationPath derivationPath {}; + const auto publicKey = gTONPublicKey; + const auto extendedPublicKey = ""; + const auto invalidAddress = gBounceableAddress; + key.addAccount(invalidAddress, TWCoinTypeTON, TWDerivationDefault, derivationPath, publicKey, extendedPublicKey); + EXPECT_EQ(key.accounts.size(), 1ul); + } + + key.updateAddress(TWCoinTypeTON); + EXPECT_EQ(key.accounts.size(), 1ul); + EXPECT_EQ(key.accounts[0].coin, TWCoinTypeTON); + // Address should be fixed to a valid non-bounceable address. + EXPECT_EQ(key.accounts[0].address, gNonBounceableAddress); +} + +TEST(StoredKeyTON, LoadNonexistent) { + ASSERT_THROW(StoredKey::load(testDataPath("nonexistent.json")), invalid_argument); +} + +TEST(StoredKeyTON, LoadTonMnemonic) { + const auto key = StoredKey::load(testDataPath("ton-wallet.json")); + EXPECT_EQ(key.type, StoredKeyType::tonMnemonicPhrase); + EXPECT_EQ(key.id, "f7a2172e-fb7a-427a-8526-99779fc47c0a"); + EXPECT_EQ(key.name, "Test TON Account"); + + const auto data = key.payload.decrypt(gPassword); + const auto mnemonic = string(reinterpret_cast(data.data()), data.size()); + EXPECT_EQ(mnemonic, gTONMnemonic); + + EXPECT_EQ(key.accounts[0].coin, TWCoinTypeTON); + EXPECT_EQ(key.accounts[0].derivationPath.string(), ""); + EXPECT_EQ(key.accounts[0].address, gNonBounceableAddress); + EXPECT_EQ(key.accounts[0].publicKey, gTONPublicKey); +} + +TEST(StoredKeyTON, InvalidPassword) { + const auto key = StoredKey::load(testDataPath("ton-wallet.json")); + + ASSERT_THROW(key.payload.decrypt(TW::data("INVALID PASSWORD")), DecryptionError); +} + +} // namespace TW::Keystore diff --git a/tests/common/Keystore/StoredKeyTests.cpp b/tests/common/Keystore/StoredKeyTests.cpp index ba189cfe5a1..f6170c8e2d7 100644 --- a/tests/common/Keystore/StoredKeyTests.cpp +++ b/tests/common/Keystore/StoredKeyTests.cpp @@ -10,28 +10,21 @@ #include "HexCoding.h" #include "Mnemonic.h" #include "PrivateKey.h" +#include "StoredKeyConstants.h" #include #include -extern std::string TESTS_ROOT; - namespace TW::Keystore::tests { using namespace std; -const auto passwordString = "password"; -const auto gPassword = TW::data(string(passwordString)); -const auto gMnemonic = "team engine square letter hero song dizzy scrub tornado fabric divert saddle"; -const TWCoinType coinTypeBc = TWCoinTypeBitcoin; -const TWCoinType coinTypeBnb = TWCoinTypeBinance; -const TWCoinType coinTypeBsc = TWCoinTypeSmartChain; -const TWCoinType coinTypeEth = TWCoinTypeEthereum; -const TWCoinType coinTypeBscLegacy = TWCoinTypeSmartChainLegacy; - -const std::string testDataPath(const char* subpath) { - return TESTS_ROOT + "/common/Keystore/Data/" + subpath; -} +static const auto gMnemonic = "team engine square letter hero song dizzy scrub tornado fabric divert saddle"; +static const TWCoinType coinTypeBc = TWCoinTypeBitcoin; +static const TWCoinType coinTypeBnb = TWCoinTypeBinance; +static const TWCoinType coinTypeBsc = TWCoinTypeSmartChain; +static const TWCoinType coinTypeEth = TWCoinTypeEthereum; +static const TWCoinType coinTypeBscLegacy = TWCoinTypeSmartChainLegacy; TEST(StoredKey, CreateWithMnemonic) { auto key = StoredKey::createWithMnemonic("name", gPassword, gMnemonic, TWStoredKeyEncryptionLevelDefault); @@ -355,6 +348,7 @@ TEST(StoredKey, LoadLegacyMnemonic) { EXPECT_EQ(key.id, "629aad29-0b22-488e-a0e7-b4219d4f311c"); const auto data = key.payload.decrypt(gPassword); + // In this case, the encrypted mnemonic contains `\0` value at the end. const auto mnemonic = string(reinterpret_cast(data.data())); EXPECT_EQ(mnemonic, "ripple scissors kick mammal hire column oak again sun offer wealth tomorrow wagon turn back"); diff --git a/tests/interface/TWStoredKeyTests.cpp b/tests/interface/TWStoredKeyTests.cpp index ac9bd0cc366..ebb52d6b676 100644 --- a/tests/interface/TWStoredKeyTests.cpp +++ b/tests/interface/TWStoredKeyTests.cpp @@ -38,6 +38,32 @@ struct std::shared_ptr createDefaultStoredKey(TWStoredKeyEncryption return createAStoredKey(TWCoinTypeBitcoin, password.get(), encryption); } +/// Return a StoredKey instance that can be used for further tests. Needs to be deleted at the end. +struct std::shared_ptr createTONStoredKey(TWData* password, TWStoredKeyEncryption encryption = TWStoredKeyEncryptionAes128Ctr) { + const auto mnemonic = WRAPS(TWStringCreateWithUTF8Bytes("slim holiday tiny pizza donor egg round three verify post chat social offer mix rack soft loud code option learn this pipe mouse mango")); + const auto name = WRAPS(TWStringCreateWithUTF8Bytes("name")); + const auto coin = TWCoinTypeTON; + + return WRAP(TWStoredKey, TWStoredKeyImportTONWalletWithEncryption(mnemonic.get(), name.get(), password, coin, encryption)); +} + +Data readFileData(const std::string& path) { + // read contents of file + ifstream ifs(path); + // get length of file: + ifs.seekg (0, ifs.end); + auto length = ifs.tellg(); + ifs.seekg (0, ifs.beg); + EXPECT_TRUE(length > 20); + + Data data(length); + size_t idx = 0; + // read the slow way, ifs.read gave some false warnings with codacy + while (!ifs.eof() && idx < static_cast(length)) { char c = ifs.get(); data[idx++] = (uint8_t)c; } + + return data; +} + TEST(TWStoredKey, loadPBKDF2Key) { const auto filename = WRAPS(TWStringCreateWithUTF8Bytes((TESTS_ROOT + "/common/Keystore/Data/pbkdf2.json").c_str())); const auto key = WRAP(TWStoredKey, TWStoredKeyLoad(filename.get())); @@ -127,6 +153,36 @@ TEST(TWStoredKey, importHDWalletAES256) { EXPECT_EQ(nokey.get(), nullptr); } +TEST(TWStoredKey, importTONWallet) { + const auto mnemonicStr = "slim holiday tiny pizza donor egg round three verify post chat social offer mix rack soft loud code option learn this pipe mouse mango"; + const auto mnemonic = WRAPS(TWStringCreateWithUTF8Bytes(mnemonicStr)); + const auto name = WRAPS(TWStringCreateWithUTF8Bytes("name")); + const auto passwordString = WRAPS(TWStringCreateWithUTF8Bytes("password")); + const auto password = WRAPD(TWDataCreateWithBytes(reinterpret_cast(TWStringUTF8Bytes(passwordString.get())), TWStringSize(passwordString.get()))); + const auto coin = TWCoinTypeTON; + const auto key = WRAP(TWStoredKey, TWStoredKeyImportTONWallet(mnemonic.get(), name.get(), password.get(), coin)); + EXPECT_FALSE(TWStoredKeyIsMnemonic(key.get())); + EXPECT_TRUE(TWStoredKeyIsTONMnemonic(key.get())); + + const auto actualMnemonic = WRAPS(TWStoredKeyDecryptTONMnemonic(key.get(), password.get())); + assertStringsEqual(actualMnemonic, mnemonicStr); + + // invalid mnemonic + const auto mnemonicInvalid = WRAPS(TWStringCreateWithUTF8Bytes("_THIS_IS_AN_INVALID_MNEMONIC_")); + const auto nokey = WRAP(TWStoredKey, TWStoredKeyImportTONWallet(mnemonicInvalid.get(), name.get(), password.get(), coin)); + EXPECT_EQ(nokey.get(), nullptr); +} + +TEST(TWStoredKey, importTONWalletAES256) { + const auto mnemonic = WRAPS(TWStringCreateWithUTF8Bytes("slim holiday tiny pizza donor egg round three verify post chat social offer mix rack soft loud code option learn this pipe mouse mango")); + const auto name = WRAPS(TWStringCreateWithUTF8Bytes("name")); + const auto passwordString = WRAPS(TWStringCreateWithUTF8Bytes("password")); + const auto password = WRAPD(TWDataCreateWithBytes(reinterpret_cast(TWStringUTF8Bytes(passwordString.get())), TWStringSize(passwordString.get()))); + const auto coin = TWCoinTypeTON; + const auto key = WRAP(TWStoredKey, TWStoredKeyImportTONWalletWithEncryption(mnemonic.get(), name.get(), password.get(), coin, TWStoredKeyEncryptionAes256Ctr)); + EXPECT_TRUE(TWStoredKeyIsTONMnemonic(key.get())); +} + TEST(TWStoredKey, addressAddRemove) { const auto passwordString = WRAPS(TWStringCreateWithUTF8Bytes("password")); const auto password = WRAPD(TWDataCreateWithBytes(reinterpret_cast(TWStringUTF8Bytes(passwordString.get())), TWStringSize(passwordString.get()))); @@ -162,6 +218,15 @@ TEST(TWStoredKey, addressAddRemove) { EXPECT_EQ(TWStoredKeyAccount(key.get(), 1001), nullptr); } +/// HDWallet cannot be created from a TON mnemonic. +TEST(TWStoredKey, TONWalletGetWalletNotSupported) { + const auto passwordString = WRAPS(TWStringCreateWithUTF8Bytes("password")); + const auto password = WRAPD(TWDataCreateWithBytes(reinterpret_cast(TWStringUTF8Bytes(passwordString.get())), TWStringSize(passwordString.get()))); + + const auto key = createTONStoredKey(password.get()); + EXPECT_EQ(TWStoredKeyWallet(key.get(), password.get()), nullptr); +} + TEST(TWStoredKey, addressAddRemoveDerivationPath) { const auto passwordString = WRAPS(TWStringCreateWithUTF8Bytes("password")); const auto password = WRAPD(TWDataCreateWithBytes(reinterpret_cast(TWStringUTF8Bytes(passwordString.get())), TWStringSize(passwordString.get()))); @@ -214,25 +279,26 @@ TEST(TWStoredKey, exportJSON) { EXPECT_EQ(TWDataGet(json.get(), 0), '{'); } +TEST(TWStoredKey, TONWalletExportJSON) { + const auto passwordString = WRAPS(TWStringCreateWithUTF8Bytes("password")); + const auto password = WRAPD(TWDataCreateWithBytes(reinterpret_cast(TWStringUTF8Bytes(passwordString.get())), TWStringSize(passwordString.get()))); + + const auto key = createTONStoredKey(password.get()); + + const auto jsonData = WRAPD(TWStoredKeyExportJSON(key.get())); + const auto jsonStr = WRAPS(TWStringCreateWithRawBytes(TWDataBytes(jsonData.get()), TWDataSize(jsonData.get()))); + const auto json = nlohmann::json::parse(string(TWStringUTF8Bytes(jsonStr.get()))); + EXPECT_EQ(json["type"], "ton-mnemonic"); + EXPECT_EQ(json["activeAccounts"].size(), 1ul); +} + TEST(TWStoredKey, storeAndImportJSONAES256) { const auto key = createDefaultStoredKey(TWStoredKeyEncryptionAes256Ctr); const auto outFileName = string(getTestTempDir() + "/TWStoredKey_store.json"); const auto outFileNameStr = WRAPS(TWStringCreateWithUTF8Bytes(outFileName.c_str())); EXPECT_TRUE(TWStoredKeyStore(key.get(), outFileNameStr.get())); - // read contents of file - ifstream ifs(outFileName); - // get length of file: - ifs.seekg (0, ifs.end); - auto length = ifs.tellg(); - ifs.seekg (0, ifs.beg); - EXPECT_TRUE(length > 20); - - Data json(length); - size_t idx = 0; - // read the slow way, ifs.read gave some false warnings with codacy - while (!ifs.eof() && idx < static_cast(length)) { char c = ifs.get(); json[idx++] = (uint8_t)c; } - + const auto json = readFileData(outFileName); const auto key2 = WRAP(TWStoredKey, TWStoredKeyImportJSON(WRAPD(TWDataCreateWithData(&json)).get())); const auto name2 = WRAPS(TWStoredKeyName(key2.get())); EXPECT_EQ(string(TWStringUTF8Bytes(name2.get())), "name"); @@ -243,24 +309,28 @@ TEST(TWStoredKey, storeAndImportJSON) { const auto outFileName = string(getTestTempDir() + "/TWStoredKey_store.json"); const auto outFileNameStr = WRAPS(TWStringCreateWithUTF8Bytes(outFileName.c_str())); EXPECT_TRUE(TWStoredKeyStore(key.get(), outFileNameStr.get())); - //EXPECT_TRUE(filesystem::exists(outFileName)); // some linker issues with filesystem - - // read contents of file - ifstream ifs(outFileName); - // get length of file: - ifs.seekg (0, ifs.end); - auto length = ifs.tellg(); - ifs.seekg (0, ifs.beg); - EXPECT_TRUE(length > 20); - Data json(length); - size_t idx = 0; - // read the slow way, ifs.read gave some false warnings with codacy - while (!ifs.eof() && idx < static_cast(length)) { char c = ifs.get(); json[idx++] = (uint8_t)c; } + const auto json = readFileData(outFileName); + const auto key2 = WRAP(TWStoredKey, TWStoredKeyImportJSON(WRAPD(TWDataCreateWithData(&json)).get())); + const auto name2 = WRAPS(TWStoredKeyName(key2.get())); + EXPECT_EQ(string(TWStringUTF8Bytes(name2.get())), "name"); + EXPECT_TRUE(TWStoredKeyIsMnemonic(key2.get())); +} + +TEST(TWStoredKey, TONWalletStoreAndImport) { + const auto passwordString = WRAPS(TWStringCreateWithUTF8Bytes("password")); + const auto password = WRAPD(TWDataCreateWithBytes(reinterpret_cast(TWStringUTF8Bytes(passwordString.get())), TWStringSize(passwordString.get()))); + + const auto key = createTONStoredKey(password.get()); + const auto outFileName = string(getTestTempDir() + "/TWStoredKey_storeTON.json"); + const auto outFileNameStr = WRAPS(TWStringCreateWithUTF8Bytes(outFileName.c_str())); + EXPECT_TRUE(TWStoredKeyStore(key.get(), outFileNameStr.get())); + const auto json = readFileData(outFileName); const auto key2 = WRAP(TWStoredKey, TWStoredKeyImportJSON(WRAPD(TWDataCreateWithData(&json)).get())); const auto name2 = WRAPS(TWStoredKeyName(key2.get())); EXPECT_EQ(string(TWStringUTF8Bytes(name2.get())), "name"); + EXPECT_TRUE(TWStoredKeyIsTONMnemonic(key2.get())); } TEST(TWStoredKey, importJsonInvalid) { @@ -392,10 +462,10 @@ TEST(TWStoredKey, getWalletPasswordInvalid) { const auto name = WRAPS(TWStringCreateWithUTF8Bytes("name")); const auto passwordString = WRAPS(TWStringCreateWithUTF8Bytes("password")); const auto password = WRAPD(TWDataCreateWithBytes(reinterpret_cast(TWStringUTF8Bytes(passwordString.get())), TWStringSize(passwordString.get()))); - + const auto invalidString = WRAPS(TWStringCreateWithUTF8Bytes("_THIS_IS_INVALID_PASSWORD_")); const auto passwordInvalid = WRAPD(TWDataCreateWithBytes(reinterpret_cast(TWStringUTF8Bytes(invalidString.get())), TWStringSize(invalidString.get()))); - + auto key = WRAP(TWStoredKey, TWStoredKeyCreate(name.get(), password.get())); ASSERT_NE(WRAP(TWHDWallet, TWStoredKeyWallet(key.get(), password.get())).get(), nullptr); ASSERT_EQ(WRAP(TWHDWallet, TWStoredKeyWallet(key.get(), passwordInvalid.get())).get(), nullptr); diff --git a/wasm/src/keystore/default-impl.ts b/wasm/src/keystore/default-impl.ts index 6da4ae6c57a..04ecb4bae87 100644 --- a/wasm/src/keystore/default-impl.ts +++ b/wasm/src/keystore/default-impl.ts @@ -104,6 +104,31 @@ export class Default implements Types.IKeyStore { }); } + importTON( + tonMnemonic: string, + name: string, + password: string, + coin: CoinType, + encryption: StoredKeyEncryption + ): Promise { + return new Promise((resolve, reject) => { + const { StoredKey, TONWallet } = this.core; + + const passphrase = ""; + if (!TONWallet.isValidMnemonic(tonMnemonic, passphrase)) { + throw Types.Error.InvalidMnemonic; + } + + let pass = Buffer.from(password); + let storedKey = StoredKey.importTONWalletWithEncryption(tonMnemonic, name, pass, coin, encryption); + let wallet = this.mapWallet(storedKey); + storedKey.delete(); + this.importWallet(wallet) + .then(() => resolve(wallet)) + .catch((error) => reject(error)); + }); + } + addAccounts( id: string, password: string, @@ -129,11 +154,23 @@ export class Default implements Types.IKeyStore { ): Promise { return this.load(id).then((wallet) => { let storedKey = this.mapStoredKey(wallet); - let hdWallet = storedKey.wallet(Buffer.from(password)); let coin = (this.core.CoinType as any).values["" + account.coin]; - let privateKey = hdWallet.getKey(coin, account.derivationPath); + + let privateKey: PrivateKey; + switch (wallet.type) { + // In case of BIP39 mnemonic, we should use the custom derivation path. + case Types.WalletType.Mnemonic: + let hdWallet = storedKey.wallet(Buffer.from(password)); + privateKey = hdWallet.getKey(coin, account.derivationPath); + hdWallet.delete(); + break; + // Otherwise, use the default implementation. + default: + privateKey = storedKey.privateKey(coin, Buffer.from(password)); + break; + } + storedKey.delete(); - hdWallet.delete(); return privateKey; }); } @@ -149,6 +186,9 @@ export class Default implements Types.IKeyStore { case Types.WalletType.PrivateKey: value = storedKey.decryptPrivateKey(Buffer.from(password)); break; + case Types.WalletType.TonMnemonic: + value = storedKey.decryptTONMnemonic(Buffer.from(password)); + break; default: throw Types.Error.InvalidJSON; } diff --git a/wasm/src/keystore/types.ts b/wasm/src/keystore/types.ts index bccb05ef58a..ecdd3280db1 100644 --- a/wasm/src/keystore/types.ts +++ b/wasm/src/keystore/types.ts @@ -9,6 +9,7 @@ export enum WalletType { PrivateKey = "privateKey", WatchOnly = "watchOnly", Hardware = "hardware", + TonMnemonic = "ton-mnemonic" } export enum Error { @@ -68,6 +69,15 @@ export interface IKeyStore { // Import a Wallet object directly importWallet(wallet: Wallet): Promise; + // Import a TON wallet by 24-words mnemonic, name, password and initial active account (from coinType) + importTON( + tonMnemonic: string, + name: string, + password: string, + coin: CoinType, + encryption: StoredKeyEncryption + ): Promise; + // Add active accounts to a wallet by wallet id, password, coin addAccounts(id: string, password: string, coins: CoinType[]): Promise; @@ -78,7 +88,7 @@ export interface IKeyStore { account: ActiveAccount ): Promise; - // Delete a wallet by wallet id and password.aq1aq + // Delete a wallet by wallet id and password delete(id: string, password: string): Promise; // Export a wallet by wallet id and password, returns mnemonic or private key diff --git a/wasm/tests/KeyStore+extension.test.ts b/wasm/tests/KeyStore+extension.test.ts index 9cc1dcf5c22..a44b7cb3504 100644 --- a/wasm/tests/KeyStore+extension.test.ts +++ b/wasm/tests/KeyStore+extension.test.ts @@ -64,6 +64,51 @@ describe("KeyStore", async () => { }); }).timeout(10000); + it("test ExtensionStorage TONWallet", async () => { + const { CoinType, HexCoding, StoredKeyEncryption } = globalThis.core; + const tonMnemonic = globalThis.tonMnemonic as string; + const password = globalThis.password as string; + + const walletIdsKey = "all-wallet-ids"; + const storage = new KeyStore.ExtensionStorage( + walletIdsKey, + new ChromeStorageMock() + ); + const keystore = new KeyStore.Default(globalThis.core, storage); + + const wallet = await keystore.importTON(tonMnemonic, "Coolton", password, CoinType.ton, StoredKeyEncryption.aes128Ctr); + + assert.equal(wallet.name, "Coolton"); + assert.equal(wallet.type, "ton-mnemonic"); + assert.equal(wallet.version, 3); + + const account = wallet.activeAccounts[0]; + const key = await keystore.getKey(wallet.id, password, account); + + assert.equal( + HexCoding.encode(key.data()), + "0x859cd74ab605afb7ce9f5316a1f6d59217a130b75b494efd249913be874c9d46" + ); + assert.equal(account.address, "UQDdB2lMwYM9Gxc-ln--Tu8cz-TYksQxYuUsMs2Pd4cHerYz"); + assert.isUndefined(account.extendedPublicKey); + assert.equal( + account.publicKey, + "c9af50596bd5c1c5a15fb32bef8d4f1ee5244b287aea1f49f6023a79f9b2f055" + ); + + assert.isTrue(await keystore.hasWallet(wallet.id)); + assert.isFalse(await keystore.hasWallet("invalid-id")); + + const exported = await keystore.export(wallet.id, password); + assert.equal(exported, tonMnemonic); + + const wallets = await keystore.loadAll(); + + await wallets.forEach((w) => { + keystore.delete(w.id, password); + }); + }).timeout(10000); + it("test ExtensionStorage AES256", async () => { const { CoinType, HexCoding, StoredKeyEncryption } = globalThis.core; const mnemonic = globalThis.mnemonic as string; diff --git a/wasm/tests/setup.test.ts b/wasm/tests/setup.test.ts index 54d6664beee..98ede91486e 100644 --- a/wasm/tests/setup.test.ts +++ b/wasm/tests/setup.test.ts @@ -4,6 +4,8 @@ import { initWasm } from "../dist"; before(async () => { globalThis.mnemonic = "team engine square letter hero song dizzy scrub tornado fabric divert saddle"; + globalThis.tonMnemonic = + "laundry myself fitness beyond prize piano match acid vacuum already abandon dance occur pause grocery company inject excuse weasel carpet fog grunt trick spike"; globalThis.password = "password"; globalThis.core = await initWasm(); });