Skip to content

Commit

Permalink
[TON]: Add support for TON 24-words mnemonic (#3998)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
satoshiotomakan authored Oct 4, 2024
1 parent 5137601 commit 0b16771
Show file tree
Hide file tree
Showing 61 changed files with 4,041 additions and 202 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
}
38 changes: 38 additions & 0 deletions include/TrustWalletCore/TWStoredKey.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
36 changes: 35 additions & 1 deletion include/TrustWalletCore/TWTONWallet.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#pragma once

#include "TWBase.h"
#include "TWPrivateKey.h"
#include "TWPublicKey.h"
#include "TWString.h"

Expand All @@ -14,13 +15,46 @@ 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.
/// \param workchain TON workchain to which the wallet belongs. Usually, base chain is used (0).
/// \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
28 changes: 28 additions & 0 deletions rust/Cargo.lock

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

1 change: 1 addition & 0 deletions rust/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ members = [
"tw_encoding",
"tw_evm",
"tw_hash",
"tw_hd_wallet",
"tw_keypair",
"tw_memory",
"tw_misc",
Expand Down
1 change: 1 addition & 0 deletions rust/chains/tw_ton/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
4 changes: 2 additions & 2 deletions rust/tw_any_coin/src/test_utils/address_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<TWAnyAddress>;
pub type TWAnyAddressHelper = TWAutoWrapper<TWAnyAddress>;

impl WithDestructor for TWAnyAddress {
fn destructor() -> unsafe extern "C" fn(*mut Self) {
Expand Down
1 change: 1 addition & 0 deletions rust/tw_hash/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
2 changes: 2 additions & 0 deletions rust/tw_hash/src/ffi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@ pub enum CHashingCode {
Ok = 0,
InvalidHashLength = 1,
InvalidArgument = 2,
InvalidPassword = 3,
}

impl From<Error> for CHashingCode {
fn from(e: Error) -> Self {
match e {
Error::FromHexError(_) | Error::InvalidArgument => CHashingCode::InvalidArgument,
Error::InvalidHashLength => CHashingCode::InvalidHashLength,
Error::InvalidPassword => CHashingCode::InvalidPassword,
}
}
}
Expand Down
15 changes: 10 additions & 5 deletions rust/tw_hash/src/hmac.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,19 @@
// Copyright © 2017 Trust Wallet.

use hmac::{Hmac, Mac};
use sha2::Sha256;
use sha2::{Sha256, Sha512};

type HmacSha256 = Hmac<Sha256>;
type HmacSha512 = Hmac<Sha512>;

pub fn hmac_sha256(key: &[u8], input: &[u8]) -> Vec<u8> {
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<u8> {
let mut mac = HmacSha512::new_from_slice(key).expect("Hmac constructor should never fail");
mac.update(input);
mac.finalize().into_bytes().to_vec()
}
2 changes: 2 additions & 0 deletions rust/tw_hash/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -28,6 +29,7 @@ pub enum Error {
FromHexError(FromHexError),
InvalidHashLength,
InvalidArgument,
InvalidPassword,
}

impl From<FromHexError> for Error {
Expand Down
10 changes: 10 additions & 0 deletions rust/tw_hash/src/pbkdf2.rs
Original file line number Diff line number Diff line change
@@ -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::<Sha512, { H512::LEN }>(password, salt, rounds).into()
}
Loading

0 comments on commit 0b16771

Please sign in to comment.