From a21d447de3ec9c37465d5dd9d8c570550a1f476f Mon Sep 17 00:00:00 2001 From: satoshiotomakan <127754187+satoshiotomakan@users.noreply.github.com> Date: Mon, 5 Aug 2024 21:24:10 +0700 Subject: [PATCH] feat(ton): Add support for `crypto_box` encryption used in TON Connect (#3964) * feat(ton): Add support for `crypto_box` key-pair generation and message encryption * Remove `test_nist256p1_sign_verify_ring` as no longer needed * feat(ton): Add `crypto_box` Rust FFI functions * feat(ton): Add C++ FFI interface * feat(ton): Fix C++ interface * Add Kotlin, Swift tests * [CI] Trigger CI * feat(ton): Fix rustfmt warnings * feat(ton): Fix clippy warnings * feat(ton): Fix C++ tests --- .../core/app/utils/TestCryptoBox.kt | 29 ++++ include/TrustWalletCore/TWCryptoBox.h | 40 +++++ .../TrustWalletCore/TWCryptoBoxPublicKey.h | 45 ++++++ .../TrustWalletCore/TWCryptoBoxSecretKey.h | 38 +++++ rust/Cargo.lock | 134 +++++++++++----- rust/tw_coin_entry/src/error/impl_from.rs | 4 +- rust/tw_hash/src/hash_array.rs | 1 + rust/tw_hash/src/lib.rs | 4 +- rust/tw_keypair/Cargo.toml | 12 +- rust/tw_keypair/src/ffi/crypto_box/mod.rs | 74 +++++++++ .../src/ffi/crypto_box/public_key.rs | 66 ++++++++ .../src/ffi/crypto_box/secret_key.rs | 48 ++++++ rust/tw_keypair/src/ffi/mod.rs | 1 + rust/tw_keypair/src/lib.rs | 4 + rust/tw_keypair/src/nacl_crypto_box/mod.rs | 150 ++++++++++++++++++ .../src/nacl_crypto_box/public_key.rs | 33 ++++ .../src/nacl_crypto_box/secret_key.rs | 40 +++++ rust/tw_keypair/src/rand.rs | 6 + rust/tw_keypair/src/test_utils/mod.rs | 1 + .../src/test_utils/tw_crypto_box_helpers.rs | 22 +++ rust/tw_keypair/tests/crypto_box_ffi_tests.rs | 88 ++++++++++ rust/tw_keypair/tests/nist256p1_tests.rs | 31 ---- src/CryptoBox.cpp | 53 +++++++ src/CryptoBox.h | 62 ++++++++ src/interface/TWCryptoBox.cpp | 23 +++ src/interface/TWCryptoBoxPublicKey.cpp | 31 ++++ src/interface/TWCryptoBoxSecretKey.cpp | 22 +++ swift/Tests/CryptoBoxTests.swift | 24 +++ tests/interface/TWCryptoBoxTests.cpp | 53 +++++++ 29 files changed, 1062 insertions(+), 77 deletions(-) create mode 100644 android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestCryptoBox.kt create mode 100644 include/TrustWalletCore/TWCryptoBox.h create mode 100644 include/TrustWalletCore/TWCryptoBoxPublicKey.h create mode 100644 include/TrustWalletCore/TWCryptoBoxSecretKey.h create mode 100644 rust/tw_keypair/src/ffi/crypto_box/mod.rs create mode 100644 rust/tw_keypair/src/ffi/crypto_box/public_key.rs create mode 100644 rust/tw_keypair/src/ffi/crypto_box/secret_key.rs create mode 100644 rust/tw_keypair/src/nacl_crypto_box/mod.rs create mode 100644 rust/tw_keypair/src/nacl_crypto_box/public_key.rs create mode 100644 rust/tw_keypair/src/nacl_crypto_box/secret_key.rs create mode 100644 rust/tw_keypair/src/rand.rs create mode 100644 rust/tw_keypair/src/test_utils/tw_crypto_box_helpers.rs create mode 100644 rust/tw_keypair/tests/crypto_box_ffi_tests.rs create mode 100644 src/CryptoBox.cpp create mode 100644 src/CryptoBox.h create mode 100644 src/interface/TWCryptoBox.cpp create mode 100644 src/interface/TWCryptoBoxPublicKey.cpp create mode 100644 src/interface/TWCryptoBoxSecretKey.cpp create mode 100644 swift/Tests/CryptoBoxTests.swift create mode 100644 tests/interface/TWCryptoBoxTests.cpp diff --git a/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestCryptoBox.kt b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestCryptoBox.kt new file mode 100644 index 00000000000..60433329350 --- /dev/null +++ b/android/app/src/androidTest/java/com/trustwallet/core/app/utils/TestCryptoBox.kt @@ -0,0 +1,29 @@ +package com.trustwallet.core.app.utils + +import com.trustwallet.core.app.utils.toHexBytes +import com.trustwallet.core.app.utils.toHex +import org.junit.Assert.* +import org.junit.Test +import wallet.core.jni.* + +class TestCryptoBox { + init { + System.loadLibrary("TrustWalletCore") + } + + @Test + fun testEncryptDecryptEasy() { + val mySecret = CryptoBoxSecretKey() + val myPubkey = mySecret.publicKey + + val otherSecret = CryptoBoxSecretKey() + val otherPubkey = otherSecret.publicKey + + val message = "Well done is better than well said. -Benjamin Franklin" + val encrypted = CryptoBox.encryptEasy(mySecret, otherPubkey, message.toByteArray()) + + // Step 2. Make sure the Box can be decrypted by the other side. + val decrypted = CryptoBox.decryptEasy(otherSecret, myPubkey, encrypted) + assertEquals(decrypted.toString(Charsets.UTF_8), message) + } +} \ No newline at end of file diff --git a/include/TrustWalletCore/TWCryptoBox.h b/include/TrustWalletCore/TWCryptoBox.h new file mode 100644 index 00000000000..abd27ca8191 --- /dev/null +++ b/include/TrustWalletCore/TWCryptoBox.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" +#include "TWCryptoBoxPublicKey.h" +#include "TWCryptoBoxSecretKey.h" +#include "TWData.h" +#include "TWString.h" + +TW_EXTERN_C_BEGIN + +/// `crypto_box` encryption algorithms. +TW_EXPORT_STRUCT +struct TWCryptoBox; + +/// Encrypts message using `my_secret` and `other_pubkey`. +/// The output will have a randomly generated nonce prepended to it. +/// The output will be Overhead + 24 bytes longer than the original. +/// +/// \param mySecret *non-null* pointer to my secret key. +/// \param otherPubkey *non-null* pointer to other's public key. +/// \param message *non-null* pointer to the message to be encrypted. +/// \return *nullable* pointer to the encrypted message with randomly generated nonce prepended to it. +TW_EXPORT_STATIC_METHOD +TWData* _Nonnull TWCryptoBoxEncryptEasy(struct TWCryptoBoxSecretKey* _Nonnull mySecret, struct TWCryptoBoxPublicKey* _Nonnull otherPubkey, TWData* _Nonnull message); + +/// Decrypts box produced by `TWCryptoBoxEncryptEasy`. +/// We assume a 24-byte nonce is prepended to the encrypted text in box. +/// +/// \param mySecret *non-null* pointer to my secret key. +/// \param otherPubkey *non-null* pointer to other's public key. +/// \param encrypted *non-null* pointer to the encrypted message with nonce prepended to it. +/// \return *nullable* pointer to the decrypted message. +TW_EXPORT_STATIC_METHOD +TWData* _Nullable TWCryptoBoxDecryptEasy(struct TWCryptoBoxSecretKey* _Nonnull mySecret, struct TWCryptoBoxPublicKey* _Nonnull otherPubkey, TWData* _Nonnull encrypted); + +TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWCryptoBoxPublicKey.h b/include/TrustWalletCore/TWCryptoBoxPublicKey.h new file mode 100644 index 00000000000..e46ea72feae --- /dev/null +++ b/include/TrustWalletCore/TWCryptoBoxPublicKey.h @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" +#include "TWData.h" +#include "TWString.h" + +TW_EXTERN_C_BEGIN + +/// Public key used in `crypto_box` cryptography. +TW_EXPORT_CLASS +struct TWCryptoBoxPublicKey; + +/// Determines if the given public key is valid or not. +/// +/// \param data *non-null* byte array. +/// \return true if the public key is valid, false otherwise. +TW_EXPORT_STATIC_METHOD +bool TWCryptoBoxPublicKeyIsValid(TWData* _Nonnull data); + +/// Create a `crypto_box` public key with the given block of data. +/// +/// \param data *non-null* byte array. Expected to have 32 bytes. +/// \note Should be deleted with \tw_crypto_box_public_key_delete. +/// \return Nullable pointer to Public Key. +TW_EXPORT_STATIC_METHOD +struct TWCryptoBoxPublicKey* _Nullable TWCryptoBoxPublicKeyCreateWithData(TWData* _Nonnull data); + +/// Delete the given public key. +/// +/// \param publicKey *non-null* pointer to public key. +TW_EXPORT_METHOD +void TWCryptoBoxPublicKeyDelete(struct TWCryptoBoxPublicKey* _Nonnull publicKey); + +/// Returns the raw data of the given public-key. +/// +/// \param publicKey *non-null* pointer to a public key. +/// \return C-compatible result with a C-compatible byte array. +TW_EXPORT_PROPERTY +TWData* _Nonnull TWCryptoBoxPublicKeyData(struct TWCryptoBoxPublicKey* _Nonnull publicKey); + +TW_EXTERN_C_END diff --git a/include/TrustWalletCore/TWCryptoBoxSecretKey.h b/include/TrustWalletCore/TWCryptoBoxSecretKey.h new file mode 100644 index 00000000000..f2d5d10d5b1 --- /dev/null +++ b/include/TrustWalletCore/TWCryptoBoxSecretKey.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "TWBase.h" +#include "TWCryptoBoxPublicKey.h" +#include "TWData.h" +#include "TWString.h" + +TW_EXTERN_C_BEGIN + +/// Secret key used in `crypto_box` cryptography. +TW_EXPORT_CLASS +struct TWCryptoBoxSecretKey; + +/// Create a random secret key. +/// +/// \note Should be deleted with \tw_crypto_box_secret_key_delete. +/// \return *non-null* pointer to Secret Key. +TW_EXPORT_STATIC_METHOD +struct TWCryptoBoxSecretKey* _Nonnull TWCryptoBoxSecretKeyCreate(); + +/// Delete the given secret `key`. +/// +/// \param key *non-null* pointer to secret key. +TW_EXPORT_METHOD +void TWCryptoBoxSecretKeyDelete(struct TWCryptoBoxSecretKey* _Nonnull key); + +/// Returns the public key associated with the given `key`. +/// +/// \param key *non-null* pointer to the private key. +/// \return *non-null* pointer to the corresponding public key. +TW_EXPORT_METHOD +struct TWCryptoBoxPublicKey* _Nonnull TWCryptoBoxSecretKeyGetPublicKey(struct TWCryptoBoxSecretKey* _Nonnull key); + +TW_EXTERN_C_END diff --git a/rust/Cargo.lock b/rust/Cargo.lock index d73705dbb8a..49f8393f329 100644 --- a/rust/Cargo.lock +++ b/rust/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + [[package]] name = "aho-corasick" version = "0.7.20" @@ -381,6 +391,17 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", + "zeroize", +] + [[package]] name = "clap" version = "2.34.0" @@ -451,9 +472,39 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "crypto_box" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16182b4f39a82ec8a6851155cc4c0cda3065bb1db33651726a29e1951de0f009" +dependencies = [ + "aead", + "crypto_secretbox", + "curve25519-dalek", + "salsa20", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto_secretbox" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d6cf87adf719ddf43a805e92c6870a531aedda35ff640442cbaf8674e141e1" +dependencies = [ + "aead", + "cipher", + "generic-array", + "poly1305", + "salsa20", + "subtle", + "zeroize", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -647,9 +698,9 @@ checksum = "fed34cd105917e91daa4da6b3728c47b068749d6a62c59811f06ed2ac71d9da7" [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -794,6 +845,15 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "itertools" version = "0.10.5" @@ -983,6 +1043,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "p256" version = "0.13.2" @@ -1049,6 +1115,17 @@ dependencies = [ "spki", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1234,21 +1311,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin", - "untrusted", - "web-sys", - "winapi", -] - [[package]] name = "ripemd" version = "0.1.3" @@ -1295,6 +1357,15 @@ version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "sec1" version = "0.7.1" @@ -1428,12 +1499,6 @@ dependencies = [ "rand_core", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "spki" version = "0.7.1" @@ -1929,6 +1994,7 @@ dependencies = [ "arbitrary 1.3.0", "bitcoin", "blake2", + "crypto_box", "curve25519-dalek", "der", "digest 0.10.6", @@ -1937,8 +2003,8 @@ dependencies = [ "lazy_static", "p256", "pkcs8", + "rand_core", "rfc6979", - "ring", "secp256k1", "serde", "serde_json", @@ -2156,10 +2222,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] -name = "untrusted" -version = "0.7.1" +name = "universal-hash" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] [[package]] name = "uuid" @@ -2274,16 +2344,6 @@ version = "0.2.84" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" -[[package]] -name = "web-sys" -version = "0.3.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33b99f4b23ba3eec1a53ac264e35a755f00e966e0065077d6027c0f575b0b97" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - [[package]] name = "winapi" version = "0.3.9" diff --git a/rust/tw_coin_entry/src/error/impl_from.rs b/rust/tw_coin_entry/src/error/impl_from.rs index 8af3f926749..12d95dddd82 100644 --- a/rust/tw_coin_entry/src/error/impl_from.rs +++ b/rust/tw_coin_entry/src/error/impl_from.rs @@ -44,10 +44,12 @@ impl From for SigningError { KeyPairError::InvalidPublicKey | KeyPairError::InvalidSignature | KeyPairError::InvalidSignMessage - | KeyPairError::SignatureVerifyError => { + | KeyPairError::SignatureVerifyError + | KeyPairError::InvalidEncryptedMessage => { TWError::new(SigningErrorType::Error_invalid_params) }, KeyPairError::SigningError => TWError::new(SigningErrorType::Error_signing), + KeyPairError::InternalError => TWError::new(SigningErrorType::Error_internal), } } } diff --git a/rust/tw_hash/src/hash_array.rs b/rust/tw_hash/src/hash_array.rs index e1676410954..6bbae8a85db 100644 --- a/rust/tw_hash/src/hash_array.rs +++ b/rust/tw_hash/src/hash_array.rs @@ -12,6 +12,7 @@ use zeroize::DefaultIsZeroes; pub type H32 = Hash<4>; pub type H160 = Hash<20>; +pub type H192 = Hash<24>; pub type H256 = Hash<32>; pub type H264 = Hash<33>; pub type H288 = Hash<36>; diff --git a/rust/tw_hash/src/lib.rs b/rust/tw_hash/src/lib.rs index 1fb102ddbb4..2371346ee7a 100644 --- a/rust/tw_hash/src/lib.rs +++ b/rust/tw_hash/src/lib.rs @@ -17,9 +17,7 @@ pub mod sha3; mod hash_array; mod hash_wrapper; -pub use hash_array::{ - as_byte_sequence, as_bytes, concat, Hash, H160, H256, H264, H288, H32, H512, H520, -}; +pub use hash_array::*; use tw_encoding::hex::FromHexError; diff --git a/rust/tw_keypair/Cargo.toml b/rust/tw_keypair/Cargo.toml index e018cbe95ab..d0e223475da 100644 --- a/rust/tw_keypair/Cargo.toml +++ b/rust/tw_keypair/Cargo.toml @@ -9,9 +9,8 @@ test-utils = [] [dependencies] arbitrary = { version = "1", features = ["derive"], optional = true } lazy_static = "1.4.0" +rand_core = "0.6.4" serde = { version = "1.0", features = ["derive"] } -starknet-crypto = "0.5.0" -starknet-ff = "0.3.2" tw_encoding = { path = "../tw_encoding" } tw_hash = { path = "../tw_hash" } tw_memory = { path = "../tw_memory" } @@ -32,10 +31,13 @@ sha2 = "0.10.6" # Bitcoin schnorr specific: bitcoin = { version = "0.30.0", features = ["rand-std"] } secp256k1 = { version = "0.27.0", features = ["global-context", "rand-std"] } +# TON Session key-exchange specific: +crypto_box = "0.9.1" +# Starknet specific: +starknet-crypto = "0.5.0" +starknet-ff = "0.3.2" [dev-dependencies] serde_json = "1.0" tw_keypair = { path = "./", features = ["test-utils"] } - -[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -ring = "0.16.20" +tw_memory = { path = "../tw_memory", features = ["test-utils"] } diff --git a/rust/tw_keypair/src/ffi/crypto_box/mod.rs b/rust/tw_keypair/src/ffi/crypto_box/mod.rs new file mode 100644 index 00000000000..dcf624c35af --- /dev/null +++ b/rust/tw_keypair/src/ffi/crypto_box/mod.rs @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#![allow(clippy::missing_safety_doc)] + +use crate::ffi::crypto_box::public_key::TWCryptoBoxPublicKey; +use crate::ffi::crypto_box::secret_key::TWCryptoBoxSecretKey; +use crate::nacl_crypto_box::CryptoBox; +use tw_memory::ffi::tw_data::TWData; +use tw_memory::ffi::RawPtrTrait; +use tw_misc::try_or_else; + +pub mod public_key; +pub mod secret_key; + +/// Encrypts message using `my_secret` and `other_pubkey`. +/// The output will have a randomly generated nonce prepended to it. +/// The output will be Overhead + 24 bytes longer than the original. +/// +/// \param my_secret *non-null* pointer to my secret key. +/// \param other_pubkey *non-null* pointer to other's public key. +/// \param message *non-null* pointer to the message to be encrypted. +/// \return *nullable* pointer to the encrypted message with randomly generated nonce prepended to it. +#[no_mangle] +pub unsafe extern "C" fn tw_crypto_box_encrypt_easy( + my_secret: *const TWCryptoBoxSecretKey, + other_pubkey: *const TWCryptoBoxPublicKey, + message: *const TWData, +) -> *mut TWData { + let my_secret = try_or_else!( + TWCryptoBoxSecretKey::from_ptr_as_ref(my_secret), + std::ptr::null_mut + ); + let other_pubkey = try_or_else!( + TWCryptoBoxPublicKey::from_ptr_as_ref(other_pubkey), + std::ptr::null_mut + ); + let message = try_or_else!(TWData::from_ptr_as_ref(message), std::ptr::null_mut); + + CryptoBox::encrypt_easy(&my_secret.0, &other_pubkey.0, message.as_slice()) + .map(TWData::from) + .map(TWData::into_ptr) + .unwrap_or_else(|_| std::ptr::null_mut()) +} + +/// Decrypts box produced by `TWCryptoBoxEncryptEasy`. +/// We assume a 24-byte nonce is prepended to the encrypted text in box. +/// +/// \param my_secret *non-null* pointer to my secret key. +/// \param other_pubkey *non-null* pointer to other's public key. +/// \param encrypted *non-null* pointer to the encrypted message with nonce prepended to it. +/// \return *nullable* pointer to the decrypted message. +#[no_mangle] +pub unsafe extern "C" fn tw_crypto_box_decrypt_easy( + my_secret: *const TWCryptoBoxSecretKey, + other_pubkey: *const TWCryptoBoxPublicKey, + encrypted: *const TWData, +) -> *mut TWData { + let my_secret = try_or_else!( + TWCryptoBoxSecretKey::from_ptr_as_ref(my_secret), + std::ptr::null_mut + ); + let other_pubkey = try_or_else!( + TWCryptoBoxPublicKey::from_ptr_as_ref(other_pubkey), + std::ptr::null_mut + ); + let encrypted = try_or_else!(TWData::from_ptr_as_ref(encrypted), std::ptr::null_mut); + + CryptoBox::decrypt_easy(&my_secret.0, &other_pubkey.0, encrypted.as_slice()) + .map(TWData::from) + .map(TWData::into_ptr) + .unwrap_or_else(|_| std::ptr::null_mut()) +} diff --git a/rust/tw_keypair/src/ffi/crypto_box/public_key.rs b/rust/tw_keypair/src/ffi/crypto_box/public_key.rs new file mode 100644 index 00000000000..44caab07d63 --- /dev/null +++ b/rust/tw_keypair/src/ffi/crypto_box/public_key.rs @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#![allow(clippy::missing_safety_doc)] + +use crate::nacl_crypto_box::public_key::PublicKey; +use tw_memory::ffi::tw_data::TWData; +use tw_memory::ffi::RawPtrTrait; +use tw_misc::{try_or_else, try_or_false}; + +/// Public key used in `crypto_box` cryptography. +pub struct TWCryptoBoxPublicKey(pub(crate) PublicKey); + +impl RawPtrTrait for TWCryptoBoxPublicKey {} + +/// Determines if the given public key is valid or not. +/// +/// \param data *non-null* byte array. +/// \return true if the public key is valid, false otherwise. +#[no_mangle] +pub unsafe extern "C" fn tw_crypto_box_public_key_is_valid(data: *const TWData) -> bool { + let bytes_ref = try_or_false!(TWData::from_ptr_as_ref(data)); + PublicKey::try_from(bytes_ref.as_slice()).is_ok() +} + +/// Create a `crypto_box` public key with the given block of data. +/// +/// \param data *non-null* byte array. Expected to have 32 bytes. +/// \note Should be deleted with \tw_crypto_box_public_key_delete. +/// \return Nullable pointer to Public Key. +#[no_mangle] +pub unsafe extern "C" fn tw_crypto_box_public_key_create_with_data( + data: *const TWData, +) -> *mut TWCryptoBoxPublicKey { + let bytes_ref = try_or_else!(TWData::from_ptr_as_ref(data), std::ptr::null_mut); + let pubkey = try_or_else!( + PublicKey::try_from(bytes_ref.as_slice()), + std::ptr::null_mut + ); + TWCryptoBoxPublicKey(pubkey).into_ptr() +} + +/// Delete the given public key. +/// +/// \param public_key *non-null* pointer to public key. +#[no_mangle] +pub unsafe extern "C" fn tw_crypto_box_public_key_delete(public_key: *mut TWCryptoBoxPublicKey) { + // Take the ownership back to rust and drop the owner. + let _ = TWCryptoBoxPublicKey::from_ptr(public_key); +} + +/// Returns the raw data of a given public-key. +/// +/// \param public_key *non-null* pointer to a public key. +/// \return C-compatible result with a C-compatible byte array. +#[no_mangle] +pub unsafe extern "C" fn tw_crypto_box_public_key_data( + public_key: *const TWCryptoBoxPublicKey, +) -> *mut TWData { + let pubkey_ref = try_or_else!( + TWCryptoBoxPublicKey::from_ptr_as_ref(public_key), + std::ptr::null_mut + ); + TWData::from(pubkey_ref.0.as_slice().to_vec()).into_ptr() +} diff --git a/rust/tw_keypair/src/ffi/crypto_box/secret_key.rs b/rust/tw_keypair/src/ffi/crypto_box/secret_key.rs new file mode 100644 index 00000000000..b0582a016f1 --- /dev/null +++ b/rust/tw_keypair/src/ffi/crypto_box/secret_key.rs @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#![allow(clippy::missing_safety_doc)] + +use crate::ffi::crypto_box::public_key::TWCryptoBoxPublicKey; +use crate::nacl_crypto_box::secret_key::SecretKey; +use tw_memory::ffi::RawPtrTrait; +use tw_misc::try_or_else; + +/// Secret key used in `crypto_box` cryptography. +pub struct TWCryptoBoxSecretKey(pub(crate) SecretKey); + +impl RawPtrTrait for TWCryptoBoxSecretKey {} + +/// Create a random secret key. +/// +/// \note Should be deleted with \tw_crypto_box_secret_key_delete. +/// \return Nullable pointer to Private Key. +#[no_mangle] +pub unsafe extern "C" fn tw_crypto_box_secret_key_create() -> *mut TWCryptoBoxSecretKey { + TWCryptoBoxSecretKey(SecretKey::random()).into_ptr() +} + +/// Delete the given secret `key`. +/// +/// \param key *non-null* pointer to secret key. +#[no_mangle] +pub unsafe extern "C" fn tw_crypto_box_secret_key_delete(key: *mut TWCryptoBoxSecretKey) { + // Take the ownership back to rust and drop the owner. + let _ = TWCryptoBoxSecretKey::from_ptr(key); +} + +/// Returns the public key associated with the given `key`. +/// +/// \param key *non-null* pointer to the private key. +/// \return *non-null* pointer to the corresponding public key. +#[no_mangle] +pub unsafe extern "C" fn tw_crypto_box_secret_key_get_public_key( + key: *mut TWCryptoBoxSecretKey, +) -> *mut TWCryptoBoxPublicKey { + let secret = try_or_else!( + TWCryptoBoxSecretKey::from_ptr_as_ref(key), + std::ptr::null_mut + ); + TWCryptoBoxPublicKey(secret.0.public_key()).into_ptr() +} diff --git a/rust/tw_keypair/src/ffi/mod.rs b/rust/tw_keypair/src/ffi/mod.rs index 7256fd84acf..33cdc647905 100644 --- a/rust/tw_keypair/src/ffi/mod.rs +++ b/rust/tw_keypair/src/ffi/mod.rs @@ -3,5 +3,6 @@ // Copyright © 2017 Trust Wallet. pub mod asn; +pub mod crypto_box; pub mod privkey; pub mod pubkey; diff --git a/rust/tw_keypair/src/lib.rs b/rust/tw_keypair/src/lib.rs index 4f75c103970..9a51090213d 100644 --- a/rust/tw_keypair/src/lib.rs +++ b/rust/tw_keypair/src/lib.rs @@ -45,6 +45,8 @@ pub mod ecdsa; pub mod ed25519; pub mod ffi; +pub mod nacl_crypto_box; +pub mod rand; pub mod schnorr; pub mod starkex; pub mod traits; @@ -61,6 +63,8 @@ pub enum KeyPairError { InvalidPublicKey, InvalidSignature, InvalidSignMessage, + InvalidEncryptedMessage, SignatureVerifyError, SigningError, + InternalError, } diff --git a/rust/tw_keypair/src/nacl_crypto_box/mod.rs b/rust/tw_keypair/src/nacl_crypto_box/mod.rs new file mode 100644 index 00000000000..8763fdf2841 --- /dev/null +++ b/rust/tw_keypair/src/nacl_crypto_box/mod.rs @@ -0,0 +1,150 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::{KeyPairError, KeyPairResult}; +use crypto_box::aead::Aead; +use tw_hash::H192; +use tw_memory::Data; + +pub mod public_key; +pub mod secret_key; + +use public_key::PublicKey; +use secret_key::SecretKey; + +const NONCE_LEN: usize = H192::LEN; + +pub struct CryptoBox; + +impl CryptoBox { + /// Encrypts message using `other_pubkey` and `my_secret`. + /// The output will have a randomly generated nonce prepended to it. + /// The output will be Overhead + 24 bytes longer than the original. + pub fn encrypt_easy( + my_secret: &SecretKey, + other_pubkey: &PublicKey, + message: &[u8], + ) -> KeyPairResult { + use crate::rand::OsRng; + use crypto_box::aead::AeadCore; + + let nonce = crypto_box::SalsaBox::generate_nonce(&mut OsRng); + let nonce = H192::try_from(nonce.as_slice()).map_err(|_| KeyPairError::InternalError)?; + let encrypted = Self::encrypt(my_secret, other_pubkey, message, nonce)?; + + let ecrypted_with_nonce: Data = nonce + .as_slice() + .iter() + .chain(encrypted.iter()) + .copied() + .collect(); + Ok(ecrypted_with_nonce) + } + + /// Encrypts message using `other_pubkey`, `my_secret` and explicit `nonce`. + pub fn encrypt( + my_secret: &SecretKey, + other_pubkey: &PublicKey, + message: &[u8], + nonce: H192, + ) -> KeyPairResult { + let nonce = crypto_box::Nonce::from(nonce.take()); + let salsa_box = crypto_box::SalsaBox::new(other_pubkey.inner(), my_secret.inner()); + salsa_box + .encrypt(&nonce, message) + .map_err(|_| KeyPairError::InternalError) + } + + /// Decrypts box produced by [`CryptoBox::encrypt_easy`]. + /// We assume a 24-byte nonce is prepended to the encrypted text in box. + pub fn decrypt_easy( + my_secret: &SecretKey, + other_pubkey: &PublicKey, + encrypted_with_nonce: &[u8], + ) -> KeyPairResult { + if encrypted_with_nonce.len() < NONCE_LEN { + return Err(KeyPairError::InvalidEncryptedMessage); + } + + let nonce = H192::try_from(&encrypted_with_nonce[..NONCE_LEN]) + .map_err(|_| KeyPairError::InternalError)?; + let encrypted = &encrypted_with_nonce[NONCE_LEN..]; + + Self::decrypt(my_secret, other_pubkey, encrypted, nonce) + } + + /// Decrypts a box produced by [`CryptoBox::encrypt`] by using the same `nonce`. + pub fn decrypt( + my_secret: &SecretKey, + other_pubkey: &PublicKey, + encrypted: &[u8], + nonce: H192, + ) -> KeyPairResult { + let nonce = crypto_box::Nonce::from(nonce.take()); + let salsa_box = crypto_box::SalsaBox::new(other_pubkey.inner(), my_secret.inner()); + salsa_box + .decrypt(&nonce, encrypted) + .map_err(|_| KeyPairError::InvalidEncryptedMessage) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tw_encoding::hex::ToHex; + use tw_hash::H256; + + /// This test uses values generated by using https://github.com/kevinburke/nacl library. + #[test] + fn test_encrypt_decrypt() { + let my_secret = + H256::from("d465226bebafda6bcd6e1783712e9f7fad6385ef2210630887b6564cb4f6e051"); + let my_secret = SecretKey::try_from(my_secret.as_slice()).unwrap(); + let my_public = my_secret.public_key(); + + let other_secret = + H256::from("dd87000d4805d6fbd89ae1352f5e4445648b79d5e901c92aebcb610e9be468e4"); + let other_secret = SecretKey::try_from(other_secret.as_slice()).unwrap(); + let other_public = other_secret.public_key(); + + // 7a7b9c8fee6e3c597512848c7d513e7131193cdfd410ff6611522fdeea99d7160873182019d7a18502f22c5e3644d26a2b669365 + let nonce = H192::from("7a7b9c8fee6e3c597512848c7d513e7131193cdfd410ff66"); + let message = b"Hello, world"; + + let encrypted = CryptoBox::encrypt(&my_secret, &other_public, message, nonce).unwrap(); + assert_eq!( + encrypted.to_hex(), + "11522fdeea99d7160873182019d7a18502f22c5e3644d26a2b669365" + ); + + // Step 2. Make sure the Box can be decrypted by the other side. + let decrypted = CryptoBox::decrypt(&other_secret, &my_public, &encrypted, nonce).unwrap(); + assert_eq!( + decrypted, message, + "Decrypted message differs from the original message" + ); + } + + #[test] + fn test_encrypt_decrypt_easy() { + let my_secret = SecretKey::random(); + let my_public = my_secret.public_key(); + + let other_secret = SecretKey::random(); + let other_public = other_secret.public_key(); + + let message = b"Test message to be encrypted"; + + let encrypted_with_nonce = + CryptoBox::encrypt_easy(&my_secret, &other_public, message).unwrap(); + + // Step 2. Make sure the Box can be decrypted by the other side. + let decrypted = + CryptoBox::decrypt_easy(&other_secret, &my_public, &encrypted_with_nonce).unwrap(); + assert_eq!( + decrypted, message, + "Decrypted message differs from the original message" + ); + } +} diff --git a/rust/tw_keypair/src/nacl_crypto_box/public_key.rs b/rust/tw_keypair/src/nacl_crypto_box/public_key.rs new file mode 100644 index 00000000000..01cb765624c --- /dev/null +++ b/rust/tw_keypair/src/nacl_crypto_box/public_key.rs @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::KeyPairError; + +pub struct PublicKey { + public: crypto_box::PublicKey, +} + +impl PublicKey { + pub(crate) fn with_inner(public: crypto_box::PublicKey) -> PublicKey { + PublicKey { public } + } + + pub(crate) fn inner(&self) -> &crypto_box::PublicKey { + &self.public + } + + pub fn as_slice(&self) -> &[u8] { + self.public.as_bytes() + } +} + +impl<'a> TryFrom<&'a [u8]> for PublicKey { + type Error = KeyPairError; + + fn try_from(value: &'a [u8]) -> Result { + let public = + crypto_box::PublicKey::from_slice(value).map_err(|_| KeyPairError::InvalidPublicKey)?; + Ok(PublicKey { public }) + } +} diff --git a/rust/tw_keypair/src/nacl_crypto_box/secret_key.rs b/rust/tw_keypair/src/nacl_crypto_box/secret_key.rs new file mode 100644 index 00000000000..7a8bb0054f0 --- /dev/null +++ b/rust/tw_keypair/src/nacl_crypto_box/secret_key.rs @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use crate::nacl_crypto_box::public_key::PublicKey; +use crate::rand::{CryptoRngCore, OsRng}; +use crate::KeyPairError; + +pub struct SecretKey { + secret: crypto_box::SecretKey, +} + +impl SecretKey { + pub fn random() -> SecretKey { + SecretKey::generate(&mut OsRng) + } + + pub fn generate(rand: &mut T) -> SecretKey { + let secret = crypto_box::SecretKey::generate(rand); + SecretKey { secret } + } + + pub fn public_key(&self) -> PublicKey { + PublicKey::with_inner(self.secret.public_key()) + } + + pub(crate) fn inner(&self) -> &crypto_box::SecretKey { + &self.secret + } +} + +impl<'a> TryFrom<&'a [u8]> for SecretKey { + type Error = KeyPairError; + + fn try_from(value: &'a [u8]) -> Result { + let secret = + crypto_box::SecretKey::from_slice(value).map_err(|_| KeyPairError::InvalidSecretKey)?; + Ok(SecretKey { secret }) + } +} diff --git a/rust/tw_keypair/src/rand.rs b/rust/tw_keypair/src/rand.rs new file mode 100644 index 00000000000..00484f30107 --- /dev/null +++ b/rust/tw_keypair/src/rand.rs @@ -0,0 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +pub use rand_core::CryptoRngCore; +pub use rand_core::OsRng; diff --git a/rust/tw_keypair/src/test_utils/mod.rs b/rust/tw_keypair/src/test_utils/mod.rs index a9570356b7e..65c4f86c7fa 100644 --- a/rust/tw_keypair/src/test_utils/mod.rs +++ b/rust/tw_keypair/src/test_utils/mod.rs @@ -2,5 +2,6 @@ // // Copyright © 2017 Trust Wallet. +pub mod tw_crypto_box_helpers; pub mod tw_private_key_helper; pub mod tw_public_key_helper; 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 new file mode 100644 index 00000000000..88b4a146003 --- /dev/null +++ b/rust/tw_keypair/src/test_utils/tw_crypto_box_helpers.rs @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +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}; + +pub type TWCryptoBoxSecretKeyHelper = TWWrapper; +pub type TWCryptoBoxPublicKeyHelper = TWWrapper; + +impl WithDestructor for TWCryptoBoxSecretKey { + fn destructor() -> unsafe extern "C" fn(*mut Self) { + tw_crypto_box_secret_key_delete + } +} + +impl WithDestructor for TWCryptoBoxPublicKey { + fn destructor() -> unsafe extern "C" fn(*mut Self) { + tw_crypto_box_public_key_delete + } +} diff --git a/rust/tw_keypair/tests/crypto_box_ffi_tests.rs b/rust/tw_keypair/tests/crypto_box_ffi_tests.rs new file mode 100644 index 00000000000..fd2a3308ae7 --- /dev/null +++ b/rust/tw_keypair/tests/crypto_box_ffi_tests.rs @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +use tw_encoding::hex::DecodeHex; +use tw_keypair::ffi::crypto_box::public_key::{ + tw_crypto_box_public_key_create_with_data, tw_crypto_box_public_key_data, + tw_crypto_box_public_key_is_valid, +}; +use tw_keypair::ffi::crypto_box::secret_key::{ + tw_crypto_box_secret_key_create, tw_crypto_box_secret_key_get_public_key, +}; +use tw_keypair::ffi::crypto_box::{tw_crypto_box_decrypt_easy, tw_crypto_box_encrypt_easy}; +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; + +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()) }); + (secret, pubkey) +} + +#[test] +fn test_encrypt_decrypt_easy() { + let message = b"Well done is better than well said. -Benjamin Franklin"; + + let (my_secret, my_pubkey) = random_key_pair(); + let (other_secret, other_pubkey) = random_key_pair(); + + let message_data = TWDataHelper::create(message.to_vec()); + + let encrypted = TWDataHelper::wrap(unsafe { + tw_crypto_box_encrypt_easy(my_secret.ptr(), other_pubkey.ptr(), message_data.ptr()) + }); + + // Step 2. Make sure the Box can be decrypted by the other side. + let decrypted = TWDataHelper::wrap(unsafe { + tw_crypto_box_decrypt_easy(other_secret.ptr(), my_pubkey.ptr(), encrypted.ptr()) + }); + + assert_eq!(decrypted.to_vec().unwrap(), message); +} + +#[test] +fn test_encrypt_decrypt_easy_error() { + let (my_secret, _my_pubkey) = random_key_pair(); + + let other_pubkey_data = TWDataHelper::create( + "afccabc5b28a8a1fd1cd880516f9c854ae2498d0d1b978b53a59f38e4ae55747" + .decode_hex() + .unwrap(), + ); + let other_pubkey = TWWrapper::wrap(unsafe { + tw_crypto_box_public_key_create_with_data(other_pubkey_data.ptr()) + }); + + // The given encrypted box cannot be decrypted by using `my_secret` and `other_pubkey`. + let invalid_encrypted = "7a7b9c8fee6e3c597512848c7d513e7131193cdfd410ff6611522fdeea99d7160873182019d7a18502f22c5e3644d26a2b669365".decode_hex().unwrap(); + let invalid_encrypted_data = TWDataHelper::create(invalid_encrypted); + + let decrypted = TWDataHelper::wrap(unsafe { + tw_crypto_box_decrypt_easy( + my_secret.ptr(), + other_pubkey.ptr(), + invalid_encrypted_data.ptr(), + ) + }); + + assert!(decrypted.is_null()); +} + +#[test] +fn test_public_key() { + let pubkey_bytes = "afccabc5b28a8a1fd1cd880516f9c854ae2498d0d1b978b53a59f38e4ae55747" + .decode_hex() + .unwrap(); + + 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 actual_data = TWDataHelper::wrap(unsafe { tw_crypto_box_public_key_data(pubkey.ptr()) }); + assert_eq!(actual_data.to_vec().unwrap(), pubkey_bytes); +} diff --git a/rust/tw_keypair/tests/nist256p1_tests.rs b/rust/tw_keypair/tests/nist256p1_tests.rs index a3a603f9bd3..570f98d4fe0 100644 --- a/rust/tw_keypair/tests/nist256p1_tests.rs +++ b/rust/tw_keypair/tests/nist256p1_tests.rs @@ -53,34 +53,3 @@ fn test_nist256p1_priv_to_pub() { assert_eq!(actual_public, test.public); } } - -#[test] -#[cfg(not(target_arch = "wasm32"))] -fn test_nist256p1_sign_verify_ring() { - use ring::rand::{generate, SystemRandom}; - use ring::signature::{UnparsedPublicKey, ECDSA_P256_SHA256_FIXED}; - use tw_keypair::ecdsa::nist256p1::KeyPair; - use tw_keypair::traits::{KeyPairTrait, SigningKeyTrait}; - - let rng = SystemRandom::new(); - - for _ in 0..1000 { - let secret: [u8; 32] = generate(&rng).unwrap().expose(); - let msg_to_sign: [u8; 64] = generate(&rng).unwrap().expose(); - - let hash_to_sign = tw_hash::sha2::sha256(&msg_to_sign); - let hash_to_sign = H256::try_from(hash_to_sign.as_slice()).unwrap(); - - let key_pair = KeyPair::try_from(secret.as_slice()).unwrap(); - let actual_sign = key_pair.sign(hash_to_sign).unwrap(); - - let public_bytes = key_pair.public().uncompressed(); - - let ring_public_key = - UnparsedPublicKey::new(&ECDSA_P256_SHA256_FIXED, public_bytes.as_slice()); - - ring_public_key - .verify(&msg_to_sign, &actual_sign.to_bytes()[0..64]) - .unwrap(); - } -} diff --git a/src/CryptoBox.cpp b/src/CryptoBox.cpp new file mode 100644 index 00000000000..89f77f72e68 --- /dev/null +++ b/src/CryptoBox.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "CryptoBox.h" + +namespace TW::CryptoBox { + +bool PublicKey::isValid(const Data& bytes) { + Rust::TWDataWrapper data = bytes; + return Rust::tw_crypto_box_public_key_is_valid(data.get()); +} + +std::optional PublicKey::fromBytes(const Data& bytes) { + Rust::TWDataWrapper data = bytes; + if (!Rust::tw_crypto_box_public_key_is_valid(data.get())) { + return std::nullopt; + } + auto* publicKey = Rust::tw_crypto_box_public_key_create_with_data(data.get()); + return PublicKey(PublicKeyPtr(publicKey, Rust::tw_crypto_box_public_key_delete)); +} + +Data PublicKey::getData() const { + Rust::TWDataWrapper data = Rust::tw_crypto_box_public_key_data(impl.get()); + return data.toDataOrDefault(); +} + +SecretKey::SecretKey() { + auto* secretKey = Rust::tw_crypto_box_secret_key_create(); + impl = SecretKeyPtr(secretKey, Rust::tw_crypto_box_secret_key_delete); +} + +PublicKey SecretKey::getPublicKey() const noexcept { + auto* publicKey = Rust::tw_crypto_box_secret_key_get_public_key(impl.get()); + return PublicKey(PublicKeyPtr(publicKey, Rust::tw_crypto_box_public_key_delete)); +} + +Data encryptEasy(const SecretKey& mySecret, const PublicKey& otherPubkey, const Data& message) { + Rust::TWDataWrapper messageData = message; + Rust::TWDataWrapper encrypted = Rust::tw_crypto_box_encrypt_easy(mySecret.impl.get(), otherPubkey.impl.get(), messageData.get()); + return encrypted.toDataOrDefault(); +} + +std::optional decryptEasy(const SecretKey& mySecret, const PublicKey& otherPubkey, const Data& encrypted) { + Rust::TWDataWrapper encryptedData = encrypted; + Rust::TWDataWrapper decryptedData = Rust::tw_crypto_box_decrypt_easy(mySecret.impl.get(), otherPubkey.impl.get(), encryptedData.get()); + if (!decryptedData.ptr) { + return std::nullopt; + } + return decryptedData.toDataOrDefault(); +} + +} // namespace TW::CryptoBox \ No newline at end of file diff --git a/src/CryptoBox.h b/src/CryptoBox.h new file mode 100644 index 00000000000..474179965bd --- /dev/null +++ b/src/CryptoBox.h @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#pragma once + +#include "rust/Wrapper.h" + +namespace TW::CryptoBox { + +using PublicKeyPtr = std::shared_ptr; +using SecretKeyPtr = std::shared_ptr; + +/// Public key used in `crypto_box` cryptography. +struct PublicKey { + explicit PublicKey(PublicKeyPtr ptr): impl(std::move(ptr)) { + } + + /// Determines if the given public key is valid or not. + static bool isValid(const Data& bytes); + + /// Create a `crypto_box` public key with the given block of data. + static std::optional fromBytes(const Data& bytes); + + /// Returns the raw data of the given public-key. + Data getData() const; + + PublicKeyPtr impl; +}; + +/// Secret key used in `crypto_box` cryptography. +class SecretKey { +public: + /// Create a random secret key. + SecretKey(); + + /// Returns the public key associated with the given `key`. + PublicKey getPublicKey() const noexcept; + + SecretKeyPtr impl; +}; + +/// Encrypts message using `my_secret` and `other_pubkey`. +/// The output will have a randomly generated nonce prepended to it. +/// The output will be Overhead + 24 bytes longer than the original. +Data encryptEasy(const SecretKey& mySecret, const PublicKey& otherPubkey, const Data& message); + +/// Decrypts box produced by `TWCryptoBoxEncryptEasy`. +/// We assume a 24-byte nonce is prepended to the encrypted text in box. +std::optional decryptEasy(const SecretKey& mySecret, const PublicKey& otherPubkey, const Data& encrypted); + +} // namespace TW::CryptoBox + +/// Wrapper for C interface. +struct TWCryptoBoxSecretKey { + TW::CryptoBox::SecretKey impl; +}; + +/// Wrapper for C interface. +struct TWCryptoBoxPublicKey { + TW::CryptoBox::PublicKey impl; +}; diff --git a/src/interface/TWCryptoBox.cpp b/src/interface/TWCryptoBox.cpp new file mode 100644 index 00000000000..35f83bceb8b --- /dev/null +++ b/src/interface/TWCryptoBox.cpp @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "TrustWalletCore/TWCryptoBox.h" +#include "CryptoBox.h" + +using namespace TW; + +TWData* _Nonnull TWCryptoBoxEncryptEasy(struct TWCryptoBoxSecretKey* _Nonnull mySecret, struct TWCryptoBoxPublicKey* _Nonnull otherPubkey, TWData* _Nonnull message) { + auto& messageBytes = *reinterpret_cast(message); + auto encrypted = CryptoBox::encryptEasy(mySecret->impl, otherPubkey->impl, messageBytes); + return TWDataCreateWithBytes(encrypted.data(), encrypted.size()); +} + +TWData* _Nullable TWCryptoBoxDecryptEasy(struct TWCryptoBoxSecretKey* _Nonnull mySecret, struct TWCryptoBoxPublicKey* _Nonnull otherPubkey, TWData* _Nonnull encrypted) { + auto& encryptedBytes = *reinterpret_cast(encrypted); + auto decrypted = CryptoBox::decryptEasy(mySecret->impl, otherPubkey->impl, encryptedBytes); + if (!decrypted) { + return nullptr; + } + return TWDataCreateWithBytes(decrypted->data(), decrypted->size()); +} diff --git a/src/interface/TWCryptoBoxPublicKey.cpp b/src/interface/TWCryptoBoxPublicKey.cpp new file mode 100644 index 00000000000..3dc0e10ec08 --- /dev/null +++ b/src/interface/TWCryptoBoxPublicKey.cpp @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "TrustWalletCore/TWCryptoBoxPublicKey.h" +#include "CryptoBox.h" + +using namespace TW; + +bool TWCryptoBoxPublicKeyIsValid(TWData* _Nonnull data) { + auto& bytes = *reinterpret_cast(data); + return CryptoBox::PublicKey::isValid(bytes); +} + +struct TWCryptoBoxPublicKey* _Nullable TWCryptoBoxPublicKeyCreateWithData(TWData* _Nonnull data) { + auto& bytes = *reinterpret_cast(data); + auto publicKey = CryptoBox::PublicKey::fromBytes(bytes); + if (!publicKey) { + return nullptr; + } + return new TWCryptoBoxPublicKey { publicKey.value() }; +} + +void TWCryptoBoxPublicKeyDelete(struct TWCryptoBoxPublicKey* _Nonnull publicKey) { + delete publicKey; +} + +TWData* _Nonnull TWCryptoBoxPublicKeyData(struct TWCryptoBoxPublicKey* _Nonnull publicKey) { + auto bytes = publicKey->impl.getData(); + return TWDataCreateWithBytes(bytes.data(), bytes.size()); +} diff --git a/src/interface/TWCryptoBoxSecretKey.cpp b/src/interface/TWCryptoBoxSecretKey.cpp new file mode 100644 index 00000000000..358445bd4a3 --- /dev/null +++ b/src/interface/TWCryptoBoxSecretKey.cpp @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "TrustWalletCore/TWCryptoBoxSecretKey.h" +#include "CryptoBox.h" + +using namespace TW; + +struct TWCryptoBoxSecretKey* _Nonnull TWCryptoBoxSecretKeyCreate() { + CryptoBox::SecretKey secretKey; + return new TWCryptoBoxSecretKey { secretKey }; +} + +void TWCryptoBoxSecretKeyDelete(struct TWCryptoBoxSecretKey* _Nonnull key) { + delete key; +} + +struct TWCryptoBoxPublicKey* TWCryptoBoxSecretKeyGetPublicKey(struct TWCryptoBoxSecretKey* _Nonnull key) { + auto publicKey = key->impl.getPublicKey(); + return new TWCryptoBoxPublicKey { publicKey }; +} diff --git a/swift/Tests/CryptoBoxTests.swift b/swift/Tests/CryptoBoxTests.swift new file mode 100644 index 00000000000..3e929c78109 --- /dev/null +++ b/swift/Tests/CryptoBoxTests.swift @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +import WalletCore +import XCTest + +class CryptoBoxTests: XCTestCase { + func testEncryptDecryptEasy() { + let mySecret = CryptoBoxSecretKey() + let myPubkey = mySecret.getPublicKey() + + let otherSecret = CryptoBoxSecretKey() + let otherPubkey = otherSecret.getPublicKey() + + let message = "Well done is better than well said. -Benjamin Franklin" + let encrypted = CryptoBox.encryptEasy(mySecret: mySecret, otherPubkey: otherPubkey, message: Data(message.utf8)) + + // Step 2. Make sure the Box can be decrypted by the other side. + let decrypted = CryptoBox.decryptEasy(mySecret: otherSecret, otherPubkey: myPubkey, encrypted: encrypted)! + let decryptedStr = String(bytes: decrypted, encoding: .utf8) + XCTAssertEqual(decryptedStr, message) + } +} diff --git a/tests/interface/TWCryptoBoxTests.cpp b/tests/interface/TWCryptoBoxTests.cpp new file mode 100644 index 00000000000..0ae626b13e6 --- /dev/null +++ b/tests/interface/TWCryptoBoxTests.cpp @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// Copyright © 2017 Trust Wallet. + +#include "TestUtilities.h" +#include "TrustWalletCore/TWCryptoBox.h" +#include "TrustWalletCore/TWCryptoBoxPublicKey.h" +#include "TrustWalletCore/TWCryptoBoxSecretKey.h" + +#include + +TEST(TWCryptoBox, EncryptDecryptEasy) { + const auto mySecret = WRAP(TWCryptoBoxSecretKey, TWCryptoBoxSecretKeyCreate()); + const auto myPubkey = WRAP(TWCryptoBoxPublicKey, TWCryptoBoxSecretKeyGetPublicKey(mySecret.get())); + + const auto otherSecret = WRAP(TWCryptoBoxSecretKey, TWCryptoBoxSecretKeyCreate()); + const auto otherPubkey = WRAP(TWCryptoBoxPublicKey, TWCryptoBoxSecretKeyGetPublicKey(otherSecret.get())); + + const auto message = "Well done is better than well said. -Benjamin Franklin"; + const auto messageData = WRAPD(TWDataCreateWithBytes(reinterpret_cast(message), strlen(message))); + + const auto encrypted = WRAPD(TWCryptoBoxEncryptEasy(mySecret.get(), otherPubkey.get(), messageData.get())); + + // Step 2. Make sure the Box can be decrypted by the other side. + const auto decrypted = WRAPD(TWCryptoBoxDecryptEasy(otherSecret.get(), myPubkey.get(), encrypted.get())); + const auto decryptedData = dataFromTWData(decrypted.get()); + std::string decryptedMessage(decryptedData->begin(), decryptedData->end()); + + EXPECT_EQ(decryptedMessage, message); +} + +TEST(TWCryptoBox, PublicKeyWithData) { + auto pubkeyBytesHex = "afccabc5b28a8a1fd1cd880516f9c854ae2498d0d1b978b53a59f38e4ae55747"; + auto pubkeyBytes = DATA(pubkeyBytesHex); + + ASSERT_TRUE(TWCryptoBoxPublicKeyIsValid(pubkeyBytes.get())); + const auto publicKey = WRAP(TWCryptoBoxPublicKey, TWCryptoBoxPublicKeyCreateWithData(pubkeyBytes.get())); + const auto actualBytes = WRAPD(TWCryptoBoxPublicKeyData(publicKey.get())); + assertHexEqual(actualBytes, pubkeyBytesHex); +} + +TEST(TWCryptoBox, DecryptEasyError) { + auto otherPubkeyBytes = DATA("afccabc5b28a8a1fd1cd880516f9c854ae2498d0d1b978b53a59f38e4ae55747"); + + const auto mySecret = WRAP(TWCryptoBoxSecretKey, TWCryptoBoxSecretKeyCreate()); + const auto otherPubkey = WRAP(TWCryptoBoxPublicKey, TWCryptoBoxPublicKeyCreateWithData(otherPubkeyBytes.get())); + + // The given encrypted box cannot be decrypted by using `mySecret` and `otherPubkey`. + const auto invalidEncrypted = DATA("7a7b9c8fee6e3c597512848c7d513e7131193cdfd410ff6611522fdeea99d7160873182019d7a18502f22c5e3644d26a2b669365"); + + const auto* decrypted = TWCryptoBoxDecryptEasy(mySecret.get(), otherPubkey.get(), invalidEncrypted.get()); + ASSERT_EQ(decrypted, nullptr); +}