diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkMessageSigner.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkMessageSigner.kt new file mode 100644 index 00000000000..64c4627cb97 --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/blockchains/theopennetwork/TestTheOpenNetworkMessageSigner.kt @@ -0,0 +1,30 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +package com.trustwallet.core.app.blockchains.theopennetwork + +import com.trustwallet.core.app.utils.toHexByteArray +import org.junit.Assert.assertEquals +import org.junit.Test +import wallet.core.jni.PrivateKey +import wallet.core.jni.TONMessageSigner +import wallet.core.jni.TONWallet + +class TestTheOpenNetworkMessageSigner { + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun TheOpenNetworkMessageSignerSignMessage() { + // The private key has been derived by using [ton-mnemonic](https://www.npmjs.com/package/tonweb-mnemonic/v/0.0.2) + // from the following 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 + val privateKey = PrivateKey("112d4e2e700a468f1eae699329202f1ee671d6b665caa2d92dea038cf3868c18".toHexByteArray()) + val message = "Hello world" + val signature = TONMessageSigner.signMessage(privateKey, message) + // The following signature has been computed by calling `window.ton.send("ton_personalSign", { data: "Hello world" });`. + assertEquals(signature, "2490fbaa72aec0b77b19162bbbe0b0e3f7afd42cc9ef469f0494cd4a366a4bf76643300cd5991f66bce6006336742b8d1d435d541d244dcc013d428472e89504") + } +} diff --git a/include/TrustWalletCore/TWTONMessageSigner.h b/include/TrustWalletCore/TWTONMessageSigner.h new file mode 100644 index 00000000000..c9c73876ee3 --- /dev/null +++ b/include/TrustWalletCore/TWTONMessageSigner.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" +#include "TWPrivateKey.h" +#include "TWString.h" + +TW_EXTERN_C_BEGIN + +/// TON message signing. +TW_EXPORT_CLASS +struct TWTONMessageSigner; + +/// Signs an arbitrary message to prove ownership of an address for off-chain services. +/// https://github.com/ton-foundation/specs/blob/main/specs/wtf-0002.md +/// +/// \param privateKey: the private key used for signing +/// \param message: A custom message which is input to the signing. +/// \returns the signature, Hex-encoded. On invalid input null is returned. Returned object needs to be deleted after use. +TW_EXPORT_STATIC_METHOD +TWString *_Nullable TWTONMessageSignerSignMessage(struct TWPrivateKey *_Nonnull privateKey, TWString* _Nonnull message); + +TW_EXTERN_C_END diff --git a/rust/chains/tw_ton/src/modules/mod.rs b/rust/chains/tw_ton/src/modules/mod.rs index aafb323eedb..d27e79c19d1 100644 --- a/rust/chains/tw_ton/src/modules/mod.rs +++ b/rust/chains/tw_ton/src/modules/mod.rs @@ -3,4 +3,5 @@ // Copyright © 2017 Trust Wallet. pub mod address_converter; +pub mod personal_message_signer; pub mod wallet_provider; diff --git a/rust/chains/tw_ton/src/modules/personal_message_signer.rs b/rust/chains/tw_ton/src/modules/personal_message_signer.rs new file mode 100644 index 00000000000..fad7a7a023d --- /dev/null +++ b/rust/chains/tw_ton/src/modules/personal_message_signer.rs @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_hash::sha2::sha512; +use tw_keypair::ed25519::sha512::PrivateKey; +use tw_keypair::ed25519::Signature; +use tw_keypair::traits::SigningKeyTrait; +use tw_keypair::KeyPairResult; + +pub const TON_PERSONAL_MESSAGE_PREFIX: &str = "ton-safe-sign-magic"; + +pub struct PersonalMessageSigner; + +impl PersonalMessageSigner { + /// Signs an arbitrary message. + /// https://www.openmask.app/docs/api-reference/rpc-api#ton_personalsign + /// https://github.com/OpenProduct/openmask-extension/blob/7566ceb2772fed7a3a27d2a67bd34bf89e862557/src/view/screen/notifications/sign/api.ts#L21-L48 + pub fn sign(private_key: &PrivateKey, msg: &str) -> KeyPairResult { + let msg_hash = sha512(msg.as_bytes()); + + let mut msg_to_sign = vec![0xff_u8, 0xff]; + msg_to_sign.extend_from_slice(TON_PERSONAL_MESSAGE_PREFIX.as_bytes()); + msg_to_sign.extend_from_slice(msg_hash.as_slice()); + + private_key.sign(msg_to_sign) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tw_hash::H512; + + #[test] + fn test_sign_personal_message() { + // The private key has been derived by using [ton-mnemonic](https://www.npmjs.com/package/tonweb-mnemonic/v/0.0.2) + // from the following 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 + let private_key = PrivateKey::try_from( + "112d4e2e700a468f1eae699329202f1ee671d6b665caa2d92dea038cf3868c18", + ) + .unwrap(); + + let signature = PersonalMessageSigner::sign(&private_key, "Hello world").unwrap(); + // The following signature has been computed by calling `window.ton.send("ton_personalSign", { data: "Hello world" });`. + let expected_sig = "2490fbaa72aec0b77b19162bbbe0b0e3f7afd42cc9ef469f0494cd4a366a4bf76643300cd5991f66bce6006336742b8d1d435d541d244dcc013d428472e89504"; + assert_eq!(signature.to_bytes(), H512::from(expected_sig)); + } +} diff --git a/rust/tw_keypair/src/ffi/privkey.rs b/rust/tw_keypair/src/ffi/privkey.rs index 28850435a90..9d4efa7f8f4 100644 --- a/rust/tw_keypair/src/ffi/privkey.rs +++ b/rust/tw_keypair/src/ffi/privkey.rs @@ -15,6 +15,12 @@ pub struct TWPrivateKey(pub(crate) PrivateKey); impl RawPtrTrait for TWPrivateKey {} +impl AsRef for TWPrivateKey { + fn as_ref(&self) -> &PrivateKey { + &self.0 + } +} + /// Create a private key with the given block of data. /// /// \param input *non-null* byte array. diff --git a/rust/wallet_core_rs/src/ffi/ton/message_signer.rs b/rust/wallet_core_rs/src/ffi/ton/message_signer.rs new file mode 100644 index 00000000000..8a20d5f110e --- /dev/null +++ b/rust/wallet_core_rs/src/ffi/ton/message_signer.rs @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#![allow(clippy::missing_safety_doc)] + +use tw_encoding::hex::ToHex; +use tw_keypair::ed25519; +use tw_keypair::ffi::privkey::TWPrivateKey; +use tw_memory::ffi::tw_string::TWString; +use tw_memory::ffi::RawPtrTrait; +use tw_misc::try_or_else; +use tw_ton::modules::personal_message_signer::PersonalMessageSigner; + +/// Signs an arbitrary message to prove ownership of an address for off-chain services. +/// https://github.com/ton-foundation/specs/blob/main/specs/wtf-0002.md +/// +/// \param private_key: the private key used for signing +/// \param message: A custom message which is input to the signing. +/// \returns the signature, Hex-encoded. On invalid input null is returned. Returned object needs to be deleted after use. +#[no_mangle] +pub unsafe extern "C" fn tw_ton_message_signer_sign_message( + private_key: *const TWPrivateKey, + message: *const TWString, +) -> *mut TWString { + let private_key = try_or_else!( + TWPrivateKey::from_ptr_as_ref(private_key), + std::ptr::null_mut + ); + let private_key_bytes = private_key.as_ref().key(); + let ed25519_private_key = try_or_else!( + ed25519::sha512::PrivateKey::try_from(private_key_bytes.as_slice()), + std::ptr::null_mut + ); + + let message = try_or_else!(TWString::from_ptr_as_ref(message), std::ptr::null_mut); + let message_str = try_or_else!(message.as_str(), std::ptr::null_mut); + + let signature = try_or_else!( + PersonalMessageSigner::sign(&ed25519_private_key, message_str), + std::ptr::null_mut + ); + TWString::from(signature.to_bytes().to_hex()).into_ptr() +} diff --git a/rust/wallet_core_rs/src/ffi/ton/mod.rs b/rust/wallet_core_rs/src/ffi/ton/mod.rs index 64062dc056b..02721354dfc 100644 --- a/rust/wallet_core_rs/src/ffi/ton/mod.rs +++ b/rust/wallet_core_rs/src/ffi/ton/mod.rs @@ -3,4 +3,5 @@ // Copyright © 2017 Trust Wallet. pub mod address_converter; +pub mod message_signer; pub mod wallet; diff --git a/rust/wallet_core_rs/tests/ethereum.rs b/rust/wallet_core_rs/tests/ethereum.rs new file mode 100644 index 00000000000..6ca70fab1de --- /dev/null +++ b/rust/wallet_core_rs/tests/ethereum.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#[path = "ethereum/ethereum_abi.rs"] +mod ethereum_abi; +#[path = "ethereum/ethereum_rlp.rs"] +mod ethereum_rlp; diff --git a/rust/wallet_core_rs/tests/ethereum_abi.rs b/rust/wallet_core_rs/tests/ethereum/ethereum_abi.rs similarity index 97% rename from rust/wallet_core_rs/tests/ethereum_abi.rs rename to rust/wallet_core_rs/tests/ethereum/ethereum_abi.rs index 33802575420..1a9707eb54a 100644 --- a/rust/wallet_core_rs/tests/ethereum_abi.rs +++ b/rust/wallet_core_rs/tests/ethereum/ethereum_abi.rs @@ -43,8 +43,8 @@ fn number_n(value: u64) -> Proto::NumberNParam<'static> { #[test] fn test_ethereum_abi_decode_contract_call() { - const CUSTOM_ABI_JSON: &str = include_str!("data/custom.json"); - const CUSTOM_DECODED_JSON: &str = include_str!("data/custom_decoded.json"); + const CUSTOM_ABI_JSON: &str = include_str!("../data/custom.json"); + const CUSTOM_DECODED_JSON: &str = include_str!("../data/custom_decoded.json"); let encoded = "ec37a4a000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006400000000000000000000000000000000000000000000000000000000000000067472757374790000000000000000000000000000000000000000000000000000".decode_hex().unwrap(); diff --git a/rust/wallet_core_rs/tests/ethereum_rlp.rs b/rust/wallet_core_rs/tests/ethereum/ethereum_rlp.rs similarity index 100% rename from rust/wallet_core_rs/tests/ethereum_rlp.rs rename to rust/wallet_core_rs/tests/ethereum/ethereum_rlp.rs diff --git a/rust/wallet_core_rs/tests/solana.rs b/rust/wallet_core_rs/tests/solana.rs new file mode 100644 index 00000000000..878cbde2404 --- /dev/null +++ b/rust/wallet_core_rs/tests/solana.rs @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#[path = "solana/solana_address.rs"] +mod solana_address; +#[path = "solana/solana_transaction.rs"] +mod solana_transaction; diff --git a/rust/wallet_core_rs/tests/solana_address.rs b/rust/wallet_core_rs/tests/solana/solana_address.rs similarity index 100% rename from rust/wallet_core_rs/tests/solana_address.rs rename to rust/wallet_core_rs/tests/solana/solana_address.rs diff --git a/rust/wallet_core_rs/tests/solana_transaction.rs b/rust/wallet_core_rs/tests/solana/solana_transaction.rs similarity index 100% rename from rust/wallet_core_rs/tests/solana_transaction.rs rename to rust/wallet_core_rs/tests/solana/solana_transaction.rs diff --git a/rust/wallet_core_rs/tests/ton.rs b/rust/wallet_core_rs/tests/ton.rs new file mode 100644 index 00000000000..12a14637b1a --- /dev/null +++ b/rust/wallet_core_rs/tests/ton.rs @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#[path = "ton/ton_address_converter.rs"] +mod ton_address_converter; +#[path = "ton/ton_message_signer.rs"] +mod ton_message_signer; +#[path = "ton/ton_wallet.rs"] +mod ton_wallet; diff --git a/rust/wallet_core_rs/tests/ton_address_converter.rs b/rust/wallet_core_rs/tests/ton/ton_address_converter.rs similarity index 100% rename from rust/wallet_core_rs/tests/ton_address_converter.rs rename to rust/wallet_core_rs/tests/ton/ton_address_converter.rs diff --git a/rust/wallet_core_rs/tests/ton/ton_message_signer.rs b/rust/wallet_core_rs/tests/ton/ton_message_signer.rs new file mode 100644 index 00000000000..523188947fa --- /dev/null +++ b/rust/wallet_core_rs/tests/ton/ton_message_signer.rs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_keypair::test_utils::tw_private_key_helper::TWPrivateKeyHelper; +use tw_memory::test_utils::tw_string_helper::TWStringHelper; +use wallet_core_rs::ffi::ton::message_signer::tw_ton_message_signer_sign_message; + +#[test] +fn test_ton_wallet_create_state_init() { + let private_key = TWPrivateKeyHelper::with_hex( + "112d4e2e700a468f1eae699329202f1ee671d6b665caa2d92dea038cf3868c18", + ); + assert!(!private_key.is_null()); + let message = TWStringHelper::create("Hello world"); + + let signature = TWStringHelper::wrap(unsafe { + tw_ton_message_signer_sign_message(private_key.ptr(), message.ptr()) + }); + assert!(!signature.ptr().is_null()); + assert_eq!(signature.to_string().unwrap(), "2490fbaa72aec0b77b19162bbbe0b0e3f7afd42cc9ef469f0494cd4a366a4bf76643300cd5991f66bce6006336742b8d1d435d541d244dcc013d428472e89504"); +} diff --git a/rust/wallet_core_rs/tests/ton_wallet.rs b/rust/wallet_core_rs/tests/ton/ton_wallet.rs similarity index 100% rename from rust/wallet_core_rs/tests/ton_wallet.rs rename to rust/wallet_core_rs/tests/ton/ton_wallet.rs diff --git a/src/interface/TWTONMessageSigner.cpp b/src/interface/TWTONMessageSigner.cpp new file mode 100644 index 00000000000..3795470fbbe --- /dev/null +++ b/src/interface/TWTONMessageSigner.cpp @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "TrustWalletCore/TWTONMessageSigner.h" +#include "rust/Wrapper.h" +#include "PrivateKey.h" + +using namespace TW; + +TWString *_Nullable TWTONMessageSignerSignMessage(struct TWPrivateKey *_Nonnull privateKey, TWString* _Nonnull message) { + auto* privateKeyRustRaw = Rust::tw_private_key_create_with_data(privateKey->impl.bytes.data(), privateKey->impl.bytes.size()); + const auto privateKeyRust = std::shared_ptr(privateKeyRustRaw, Rust::tw_private_key_delete); + + Rust::TWStringWrapper messageRust = TWStringUTF8Bytes(message); + Rust::TWStringWrapper signature = Rust::tw_ton_message_signer_sign_message(privateKeyRust.get(), messageRust.get()); + + if (!signature) { + return nullptr; + } + return TWStringCreateWithUTF8Bytes(signature.c_str()); +} diff --git a/swift/Tests/Blockchains/TheOpenNetworkTests.swift b/swift/Tests/Blockchains/TheOpenNetworkTests.swift index 605b58d33e2..716d5fd8b9c 100644 --- a/swift/Tests/Blockchains/TheOpenNetworkTests.swift +++ b/swift/Tests/Blockchains/TheOpenNetworkTests.swift @@ -65,6 +65,18 @@ class TheOpenNetworkTests: XCTestCase { let stateInit = TONWallet.buildV4R2StateInit(publicKey: publicKey, workchain: 0, walletId: 0x29a9a317)! XCTAssertEqual(stateInit, "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=") } + + func testMessageSigner() { + // The private key has been derived by using [ton-mnemonic](https://www.npmjs.com/package/tonweb-mnemonic/v/0.0.2) + // from the following 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 + let privateKeyData = Data(hexString: "112d4e2e700a468f1eae699329202f1ee671d6b665caa2d92dea038cf3868c18")! + let privateKey = PrivateKey(data: privateKeyData)! + let message = "Hello world" + let signature = TONMessageSigner.signMessage(privateKey: privateKey, message: message)! + // The following signature has been computed by calling `window.ton.send("ton_personalSign", { data: "Hello world" });`. + XCTAssertEqual(signature, "2490fbaa72aec0b77b19162bbbe0b0e3f7afd42cc9ef469f0494cd4a366a4bf76643300cd5991f66bce6006336742b8d1d435d541d244dcc013d428472e89504") + } func testSign() { let privateKeyData = Data(hexString: "c38f49de2fb13223a9e7d37d5d0ffbdd89a5eb7c8b0ee4d1c299f2cefe7dc4a0")! diff --git a/tests/chains/TheOpenNetwork/TWTONMessageSignerTests.cpp b/tests/chains/TheOpenNetwork/TWTONMessageSignerTests.cpp new file mode 100644 index 00000000000..fd9367dffd7 --- /dev/null +++ b/tests/chains/TheOpenNetwork/TWTONMessageSignerTests.cpp @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "TestUtilities.h" + +#include "HexCoding.h" +#include "TrustWalletCore/TWTONMessageSigner.h" + +namespace TW::TheOpenNetwork::tests { + +TEST(TWTONMessageSigner, SignMessage) { + const auto privateKeyBytes = DATA("112d4e2e700a468f1eae699329202f1ee671d6b665caa2d92dea038cf3868c18"); + const auto privateKey = WRAP(TWPrivateKey, TWPrivateKeyCreateWithData(privateKeyBytes.get())); + const auto message = STRING("Hello world"); + + const auto signature = WRAPS(TWTONMessageSignerSignMessage(privateKey.get(), message.get())); + assertStringsEqual(signature, "2490fbaa72aec0b77b19162bbbe0b0e3f7afd42cc9ef469f0494cd4a366a4bf76643300cd5991f66bce6006336742b8d1d435d541d244dcc013d428472e89504"); +} + +} // namespace TW::TheOpenNetwork::tests