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 9b00f5adbfa..74bbd3c2da3 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 @@ -23,7 +23,17 @@ class TestKeyStore { } @Test - fun testDecryptMnemonicAes256() { + fun testDecryptMnemonicAes192Ctr() { + val keyStore = StoredKey("Test Wallet", "password".toByteArray(), StoredKeyEncryption.AES192CTR) + val result = keyStore.decryptMnemonic("wrong".toByteArray()) + val result2 = keyStore.decryptMnemonic("password".toByteArray()) + + assertNull(result) + assertNotNull(result2) + } + + @Test + fun testDecryptMnemonicAes256Ctr() { val keyStore = StoredKey("Test Wallet", "password".toByteArray(), StoredKeyEncryption.AES256CTR) val result = keyStore.decryptMnemonic("wrong".toByteArray()) val result2 = keyStore.decryptMnemonic("password".toByteArray()) diff --git a/include/TrustWalletCore/TWStoredKeyEncryption.h b/include/TrustWalletCore/TWStoredKeyEncryption.h index ccb0d7cfcac..3385262d655 100644 --- a/include/TrustWalletCore/TWStoredKeyEncryption.h +++ b/include/TrustWalletCore/TWStoredKeyEncryption.h @@ -12,7 +12,6 @@ TW_EXTERN_C_BEGIN TW_EXPORT_ENUM(uint32_t) enum TWStoredKeyEncryption { TWStoredKeyEncryptionAes128Ctr = 0, - TWStoredKeyEncryptionAes128Cbc = 1, TWStoredKeyEncryptionAes192Ctr = 2, TWStoredKeyEncryptionAes256Ctr = 3, }; diff --git a/src/Data.cpp b/src/Data.cpp index 909bc36dceb..5503f600efd 100644 --- a/src/Data.cpp +++ b/src/Data.cpp @@ -24,4 +24,19 @@ Data subData(const Data& data, size_t startIndex) { return TW::data(data.data() + startIndex, subLength); } +bool isEqualConstantTime(const Data& in_a, const Data& in_b) { + if (in_a.size() != in_b.size()) { + return false; + } + + const volatile unsigned char *a = in_a.data(); + const volatile unsigned char *b = in_b.data(); + unsigned char result = 0; + + for (size_t i = 0; i < in_a.size(); i++) { + result |= a[i] ^ b[i]; + } + return result == 0; +} + } // namespace TW diff --git a/src/Data.h b/src/Data.h index 7fe4a5112db..8d24e2429cf 100644 --- a/src/Data.h +++ b/src/Data.h @@ -70,6 +70,11 @@ inline bool has_prefix(const Data& data, T& prefix) { return std::equal(prefix.begin(), prefix.end(), data.begin(), data.begin() + std::min(data.size(), prefix.size())); } +/// Constant-time comparison to prevent timing attacks. +/// Note: This function assumes that `a` and `b` are of the same size. If they are not, it will return false immediately. +/// https://github.com/openssl/openssl/blob/94c36852d254a626739667874587b5364ddf087e/crypto/cpuid.c#L198 +bool isEqualConstantTime(const Data& in_a, const Data& in_b); + // Custom hash function for `Data` type. struct DataHash { std::size_t operator()(const Data& data) const { diff --git a/src/HDWallet.cpp b/src/HDWallet.cpp index be2d32dd390..e56e3a024d5 100644 --- a/src/HDWallet.cpp +++ b/src/HDWallet.cpp @@ -174,9 +174,9 @@ PrivateKey HDWallet::getKeyByCurve(TWCurve curve, const DerivationPath auto node = getNode(*this, curve, derivationPath); switch (privateKeyType) { case TWPrivateKeyTypeCardano: { - if (derivationPath.indices.size() < 4 || derivationPath.indices[3].value > 1) { - // invalid derivation path - return PrivateKey(Data(PrivateKey::cardanoKeySize), curve); + if (derivationPath.indices.size() < 5 || derivationPath.indices[3].value > 1) { + TW::memzero(&node); + throw std::invalid_argument("Invalid derivation path"); } const DerivationPath stakingPath = cardanoStakingDerivationPath(derivationPath); diff --git a/src/Keystore/AESParameters.cpp b/src/Keystore/AESParameters.cpp index f2156144a6c..a70746c4b8d 100644 --- a/src/Keystore/AESParameters.cpp +++ b/src/Keystore/AESParameters.cpp @@ -6,6 +6,7 @@ #include "../HexCoding.h" +#include #include using namespace TW; @@ -21,17 +22,21 @@ Data generateIv(std::size_t blockSize = TW::Keystore::gBlockSize) { static TWStoredKeyEncryption getCipher(const std::string& cipher) { if (cipher == Keystore::gAes128Ctr) { return TWStoredKeyEncryption::TWStoredKeyEncryptionAes128Ctr; - } else if (cipher == Keystore::gAes192Ctr) { + } + if (cipher == Keystore::gAes192Ctr) { return TWStoredKeyEncryption::TWStoredKeyEncryptionAes192Ctr; - } else if (cipher == Keystore::gAes256Ctr) { + } + if (cipher == Keystore::gAes256Ctr) { return TWStoredKeyEncryption::TWStoredKeyEncryptionAes256Ctr; } - return TWStoredKeyEncryptionAes128Ctr; + + std::stringstream ss; + ss << "Unsupported cipher: " << cipher; + throw std::invalid_argument(ss.str()); } const std::unordered_map gEncryptionRegistry{ {TWStoredKeyEncryptionAes128Ctr, Keystore::AESParameters{.mKeyLength = Keystore::A128, .mCipher = Keystore::gAes128Ctr, .mCipherEncryption = TWStoredKeyEncryptionAes128Ctr, .iv{}}}, - {TWStoredKeyEncryptionAes128Cbc, Keystore::AESParameters{.mKeyLength = Keystore::A128, .mCipher = Keystore::gAes128Cbc, .mCipherEncryption = TWStoredKeyEncryptionAes128Cbc, .iv{}}}, {TWStoredKeyEncryptionAes192Ctr, Keystore::AESParameters{.mKeyLength = Keystore::A192, .mCipher = Keystore::gAes192Ctr, .mCipherEncryption = TWStoredKeyEncryptionAes192Ctr, .iv{}}}, {TWStoredKeyEncryptionAes256Ctr, Keystore::AESParameters{.mKeyLength = Keystore::A256, .mCipher = Keystore::gAes256Ctr, .mCipherEncryption = TWStoredKeyEncryptionAes256Ctr, .iv{}}} }; @@ -43,6 +48,15 @@ namespace CodingKeys { static const auto iv = "iv"; } // namespace CodingKeys +std::string toString(AESValidationError error) { + switch (error) { + case AESValidationError::InvalidIV: + return "IV must be 16 bytes long"; + default: + return "Unknown error"; + } +} + /// Initializes `AESParameters` with a JSON object. AESParameters AESParameters::AESParametersFromJson(const nlohmann::json& json, const std::string& cipher) { auto parameters = AESParameters::AESParametersFromEncryption(getCipher(cipher)); @@ -64,4 +78,12 @@ AESParameters AESParameters::AESParametersFromEncryption(TWStoredKeyEncryption e return parameters; } +std::optional AESParameters::validate() const noexcept { + if (iv.size() != static_cast(mBlockSize)) { + return AESValidationError::InvalidIV; + } + + return {}; +} + } // namespace TW::Keystore diff --git a/src/Keystore/AESParameters.h b/src/Keystore/AESParameters.h index f22cb52974e..1480c32b97a 100644 --- a/src/Keystore/AESParameters.h +++ b/src/Keystore/AESParameters.h @@ -20,10 +20,15 @@ enum AESKeySize : std::int32_t { inline constexpr std::size_t gBlockSize{16}; inline constexpr const char* gAes128Ctr{"aes-128-ctr"}; -inline constexpr const char* gAes128Cbc{"aes-128-cbc"}; inline constexpr const char* gAes192Ctr{"aes-192-ctr"}; inline constexpr const char* gAes256Ctr{"aes-256-ctr"}; +enum class AESValidationError { + InvalidIV, +}; + +std::string toString(AESValidationError error); + // AES128/192/256 parameters. struct AESParameters { // For AES, your block length is always going to be 128 bits/16 bytes @@ -43,9 +48,7 @@ struct AESParameters { nlohmann::json json() const; /// Validates AES parameters. - [[nodiscard]] bool isValid() const { - return iv.size() == static_cast(mBlockSize); - } + [[nodiscard]] std::optional validate() const noexcept; }; } // namespace TW::Keystore diff --git a/src/Keystore/EncryptionParameters.cpp b/src/Keystore/EncryptionParameters.cpp index 7a60672f8d9..82bcda12262 100644 --- a/src/Keystore/EncryptionParameters.cpp +++ b/src/Keystore/EncryptionParameters.cpp @@ -4,6 +4,7 @@ #include "EncryptionParameters.h" +#include "memory/memzero_wrapper.h" #include "../Hash.h" #include @@ -40,8 +41,10 @@ static const auto mac = "mac"; EncryptionParameters::EncryptionParameters(const nlohmann::json& json) { auto cipher = json[CodingKeys::cipher].get(); cipherParams = AESParameters::AESParametersFromJson(json[CodingKeys::cipherParams], cipher); - if (!cipherParams.isValid()) { - throw std::invalid_argument("Invalid cipher params"); + if (const auto error = cipherParams.validate(); error.has_value()) { + std::stringstream ss; + ss << "Invalid cipher params: " << toString(*error); + throw std::invalid_argument(ss.str()); } auto kdf = json[CodingKeys::kdf].get(); @@ -68,13 +71,13 @@ nlohmann::json EncryptionParameters::json() const { return j; } -EncryptedPayload::EncryptedPayload(const Data& password, const Data& data, const EncryptionParameters& params) - : params(std::move(params)), _mac() { - if (!this->params.cipherParams.isValid()) { - throw std::invalid_argument("Invalid cipher params"); +EncryptedPayload::EncryptedPayload(const Data& password, const Data& data, const AESParameters& cipherParams, const ScryptParameters& scryptParams) { + if (const auto error = cipherParams.validate(); error.has_value()) { + std::stringstream ss; + ss << "Invalid cipher params: " << toString(*error); + throw std::invalid_argument(ss.str()); } - auto scryptParams = std::get(this->params.kdfParams); auto derivedKey = Data(scryptParams.desiredKeyLength); scrypt(reinterpret_cast(password.data()), password.size(), scryptParams.salt.data(), scryptParams.salt.size(), scryptParams.n, scryptParams.r, scryptParams.p, derivedKey.data(), @@ -82,9 +85,8 @@ EncryptedPayload::EncryptedPayload(const Data& password, const Data& data, const aes_encrypt_ctx ctx; auto result = 0; - switch(this->params.cipherParams.mCipherEncryption) { + switch(cipherParams.mCipherEncryption) { case TWStoredKeyEncryptionAes128Ctr: - case TWStoredKeyEncryptionAes128Cbc: result = aes_encrypt_key128(derivedKey.data(), &ctx); break; case TWStoredKeyEncryptionAes192Ctr: @@ -96,19 +98,23 @@ EncryptedPayload::EncryptedPayload(const Data& password, const Data& data, const } assert(result == EXIT_SUCCESS); if (result == EXIT_SUCCESS) { - Data iv = this->params.cipherParams.iv; + Data iv = cipherParams.iv; // iv size should have been validated in `AESParameters::isValid()`. assert(iv.size() == gBlockSize); + params = { cipherParams, scryptParams }; encrypted = Data(data.size()); aes_ctr_encrypt(data.data(), encrypted.data(), static_cast(data.size()), iv.data(), aes_ctr_cbuf_inc, &ctx); _mac = computeMAC(derivedKey.end() - params.getKeyBytesSize(), derivedKey.end(), encrypted); } + + memzero(&ctx, sizeof(ctx)); + memzero(derivedKey.data(), derivedKey.size()); } EncryptedPayload::~EncryptedPayload() { - std::fill(encrypted.begin(), encrypted.end(), 0); - std::fill(_mac.begin(), _mac.end(), 0); + memzero(encrypted.data(), encrypted.size()); + memzero(_mac.data(), _mac.size()); } Data EncryptedPayload::decrypt(const Data& password) const { @@ -131,13 +137,14 @@ Data EncryptedPayload::decrypt(const Data& password) const { throw DecryptionError::unsupportedKDF; } - if (mac != _mac) { + if (!isEqualConstantTime(mac, _mac)) { + memzero(derivedKey.data(), derivedKey.size()); throw DecryptionError::invalidPassword; } // Even though the cipher params should have been validated in `EncryptedPayload` constructor, // double check them here. - if (!params.cipherParams.isValid()) { + if (params.cipherParams.validate().has_value()) { throw DecryptionError::invalidCipher; } assert(params.cipherParams.iv.size() == gBlockSize); @@ -145,22 +152,19 @@ Data EncryptedPayload::decrypt(const Data& password) const { Data decrypted(encrypted.size()); Data iv = params.cipherParams.iv; const auto encryption = params.cipherParams.mCipherEncryption; - if (encryption == TWStoredKeyEncryptionAes128Ctr || encryption == TWStoredKeyEncryptionAes256Ctr) { + if (encryption == TWStoredKeyEncryptionAes128Ctr + || encryption == TWStoredKeyEncryptionAes192Ctr + || encryption == TWStoredKeyEncryptionAes256Ctr) { aes_encrypt_ctx ctx; [[maybe_unused]] auto result = aes_encrypt_key(derivedKey.data(), params.getKeyBytesSize(), &ctx); assert(result != EXIT_FAILURE); aes_ctr_decrypt(encrypted.data(), decrypted.data(), static_cast(encrypted.size()), iv.data(), aes_ctr_cbuf_inc, &ctx); - } else if (encryption == TWStoredKeyEncryptionAes128Cbc) { - aes_decrypt_ctx ctx; - [[maybe_unused]] auto result = aes_decrypt_key(derivedKey.data(), params.getKeyBytesSize(), &ctx); - assert(result != EXIT_FAILURE); - - for (auto i = 0ul; i < encrypted.size(); i += params.getKeyBytesSize()) { - aes_cbc_decrypt(encrypted.data() + i, decrypted.data() + i, params.getKeyBytesSize(), iv.data(), &ctx); - } + memzero(&ctx, sizeof(ctx)); + memzero(derivedKey.data(), derivedKey.size()); } else { + memzero(derivedKey.data(), derivedKey.size()); throw DecryptionError::unsupportedCipher; } diff --git a/src/Keystore/EncryptionParameters.h b/src/Keystore/EncryptionParameters.h index d12de1b90a2..9fe7a91f823 100644 --- a/src/Keystore/EncryptionParameters.h +++ b/src/Keystore/EncryptionParameters.h @@ -19,19 +19,6 @@ namespace TW::Keystore { /// Set of parameters used when encoding struct EncryptionParameters { - static EncryptionParameters getPreset(enum TWStoredKeyEncryptionLevel preset, enum TWStoredKeyEncryption encryption = TWStoredKeyEncryptionAes128Ctr) { - switch (preset) { - case TWStoredKeyEncryptionLevelMinimal: - return { AESParameters::AESParametersFromEncryption(encryption), ScryptParameters::minimal() }; - case TWStoredKeyEncryptionLevelWeak: - case TWStoredKeyEncryptionLevelDefault: - default: - return { AESParameters::AESParametersFromEncryption(encryption), ScryptParameters::weak() }; - case TWStoredKeyEncryptionLevelStandard: - return { AESParameters::AESParametersFromEncryption(encryption), ScryptParameters::standard() }; - } - } - std::int32_t getKeyBytesSize() const noexcept { return cipherParams.mKeyLength; } @@ -96,9 +83,9 @@ struct EncryptedPayload { , encrypted(std::move(encrypted)) , _mac(std::move(mac)) {} - /// Initializes by encrypting data with a password - /// using standard values. - EncryptedPayload(const Data& password, const Data& data, const EncryptionParameters& params); + /// Initializes by encrypting data with a password using standard values. + /// Note that we enforce to use Scrypt as KDF for new wallets encryption. + EncryptedPayload(const Data& password, const Data& data, const AESParameters& cipherParams, const ScryptParameters& scryptParams); /// Initializes with a JSON object. explicit EncryptedPayload(const nlohmann::json& json); diff --git a/src/Keystore/ScryptParameters.cpp b/src/Keystore/ScryptParameters.cpp index 465898f59a3..294b4559336 100644 --- a/src/Keystore/ScryptParameters.cpp +++ b/src/Keystore/ScryptParameters.cpp @@ -39,6 +39,19 @@ std::string toString(const ScryptValidationError error) { } } +ScryptParameters ScryptParameters::getPreset(TWStoredKeyEncryptionLevel preset) { + switch (preset) { + case TWStoredKeyEncryptionLevelMinimal: + return minimal(); + case TWStoredKeyEncryptionLevelStandard: + return standard(); + case TWStoredKeyEncryptionLevelWeak: + case TWStoredKeyEncryptionLevelDefault: + default: + return weak(); + } +} + ScryptParameters ScryptParameters::minimal() { return { internal::randomSalt(), minimalN, defaultR, minimalP, defaultDesiredKeyLength }; } diff --git a/src/Keystore/ScryptParameters.h b/src/Keystore/ScryptParameters.h index 8fa051c45d0..379be4ebe69 100644 --- a/src/Keystore/ScryptParameters.h +++ b/src/Keystore/ScryptParameters.h @@ -5,6 +5,7 @@ #pragma once #include "Data.h" +#include "TrustWalletCore/TWStoredKeyEncryptionLevel.h" #include "../HexCoding.h" #include @@ -63,6 +64,9 @@ struct ScryptParameters { /// Block size factor. uint32_t r = defaultR; + /// Returns a preset of Scrypt encryption parameters for the given encryption level. + static ScryptParameters getPreset(TWStoredKeyEncryptionLevel preset); + /// Generates Scrypt encryption parameters with the minimal sufficient level (4096), and with a random salt. static ScryptParameters minimal(); /// Generates Scrypt encryption parameters with the weak sufficient level (16k), and with a random salt. diff --git a/src/Keystore/StoredKey.cpp b/src/Keystore/StoredKey.cpp index 83dfdc1aafb..747d34bd2c7 100644 --- a/src/Keystore/StoredKey.cpp +++ b/src/Keystore/StoredKey.cpp @@ -95,25 +95,37 @@ StoredKey StoredKey::createWithEncodedPrivateKeyAddDefaultAddress( StoredKey::StoredKey(StoredKeyType type, std::string name, const Data& password, const Data& data, TWStoredKeyEncryptionLevel encryptionLevel, TWStoredKeyEncryption encryption, const std::optional& encodedStr) : type(type), id(), name(std::move(name)), accounts() { - const auto encryptionParams = EncryptionParameters::getPreset(encryptionLevel, encryption); - payload = EncryptedPayload(password, data, encryptionParams); + const auto cipherParams = AESParameters::AESParametersFromEncryption(encryption); + const auto scryptParams = ScryptParameters::getPreset(encryptionLevel); + payload = EncryptedPayload(password, data, cipherParams, scryptParams); + if (encodedStr) { const auto bytes = reinterpret_cast(encodedStr->c_str()); const auto encodedData = Data(bytes, bytes + encodedStr->size()); - encodedPayload = EncryptedPayload(password, encodedData, encryptionParams); + + // Generate new parameters with random iv and salt for the encoded private key. + const auto cipherParamsEncoded = AESParameters::AESParametersFromEncryption(encryption); + const auto scryptParamsEncoded = ScryptParameters::getPreset(encryptionLevel); + + encodedPayload = EncryptedPayload(password, encodedData, cipherParamsEncoded, scryptParamsEncoded); } const char* uuid_ptr = Rust::tw_uuid_random(); id = std::make_optional(uuid_ptr); Rust::free_string(uuid_ptr); } -const HDWallet<> StoredKey::wallet(const Data& password) const { +HDWallet<> StoredKey::wallet(const Data& password) const { if (type != StoredKeyType::mnemonicPhrase) { throw std::invalid_argument("Invalid account requested."); } - const auto data = payload.decrypt(password); - const auto mnemonic = std::string(reinterpret_cast(data.data()), data.size()); - return HDWallet<>(mnemonic, ""); + auto data = payload.decrypt(password); + auto mnemonic = std::string(reinterpret_cast(data.data()), data.size()); + const HDWallet<> wallet = {mnemonic, ""}; + + // clear decrypted data from memory + memzero(data.data(), data.size()); + memzero(mnemonic.data(), mnemonic.size()); + return wallet; } std::vector StoredKey::getAccounts(TWCoinType coin) const { @@ -340,14 +352,18 @@ bool StoredKey::updateAddress(TWCoinType coin) { return addressUpdated; } -const std::string StoredKey::decryptPrivateKeyEncoded(const Data& password) const { +std::string StoredKey::decryptPrivateKeyEncoded(const Data& password) const { if (encodedPayload) { auto data = encodedPayload->decrypt(password); - return std::string(reinterpret_cast(data.data()), data.size()); - } else { - auto data = payload.decrypt(password); - return TW::hex(data); + const auto dataString = std::string(reinterpret_cast(data.data()), data.size()); + memzero(data.data(), data.size()); + return dataString; } + + auto data = payload.decrypt(password); + const auto dataHex = TW::hex(data); + memzero(data.data(), data.size()); + return dataHex; } // ----------------- diff --git a/src/Keystore/StoredKey.h b/src/Keystore/StoredKey.h index 6d9ec35cc8d..0f7092263a1 100644 --- a/src/Keystore/StoredKey.h +++ b/src/Keystore/StoredKey.h @@ -89,10 +89,10 @@ class StoredKey { /// Returns the HDWallet for this key. /// /// @throws std::invalid_argument if this key is of a type other than `mnemonicPhrase`. - const HDWallet<> wallet(const Data& password) const; + [[nodiscard]] HDWallet<> wallet(const Data& password) const; /// Returns all the accounts for a specific coin: 0, 1, or more. - std::vector getAccounts(TWCoinType coin) const; + [[nodiscard]] 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 none exists, and wallet is not null, an account is created (with default derivation). @@ -104,10 +104,10 @@ class StoredKey { /// 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. - std::optional account(TWCoinType coin) const; + [[nodiscard]] std::optional account(TWCoinType coin) const; /// Returns the account for a specific coin and derivation, if it exists. - std::optional account(TWCoinType coin, TWDerivation derivation, const HDWallet<>& wallet) const; + [[nodiscard]] std::optional account(TWCoinType coin, TWDerivation derivation, const HDWallet<>& wallet) const; /// Add an account with aribitrary address/derivation path. Discouraged, use account() versions. /// Address must be unique (for a coin). @@ -157,7 +157,7 @@ class StoredKey { void loadJson(const nlohmann::json& json); /// Saves `this` as a JSON object. - nlohmann::json json() const; + [[nodiscard]] nlohmann::json json() const; /// Fills in all empty or invalid addresses and public keys. /// @@ -175,7 +175,7 @@ class StoredKey { /// /// \returns the decoded private key. /// \throws DecryptionError - const std::string decryptPrivateKeyEncoded(const Data& password) const; + [[nodiscard]] std::string decryptPrivateKeyEncoded(const Data& password) const; private: /// Default constructor, private @@ -203,10 +203,10 @@ class StoredKey { std::optional getDefaultAccountOrAny(TWCoinType coin, const HDWallet<>* wallet) const; /// Find account by coin+address (should be one, if multiple, first is returned) - std::optional getAccount(TWCoinType coin, const std::string& address) const; + [[nodiscard]] std::optional getAccount(TWCoinType coin, const std::string& address) const; /// Find account by coin+derivation (should be one, if multiple, first is returned) - std::optional getAccount(TWCoinType coin, TWDerivation derivation, const HDWallet<>& wallet) const; + [[nodiscard]] std::optional getAccount(TWCoinType coin, TWDerivation derivation, const HDWallet<>& wallet) const; /// Re-derive account address if missing Account fillAddressIfMissing(Account& account, const HDWallet<>* wallet) const; diff --git a/swift/Tests/Keystore/KeyStoreTests.swift b/swift/Tests/Keystore/KeyStoreTests.swift index 23db9ab0a71..3dd8fc40457 100755 --- a/swift/Tests/Keystore/KeyStoreTests.swift +++ b/swift/Tests/Keystore/KeyStoreTests.swift @@ -185,7 +185,21 @@ class KeyStoreTests: XCTestCase { XCTAssertNotNil(PrivateKey(data: storedData!)) } - func testImportPrivateKeyAES256() throws { + func testImportPrivateKeyAES192Ctr() throws { + let keyStore = try KeyStore(keyDirectory: keyDirectory) + let privateKeyData = Data(hexString: "9cdb5cab19aec3bd0fcd614c5f185e7a1d97634d4225730eba22497dc89a716c")! + let key = StoredKey.importPrivateKeyWithEncryption(privateKey: privateKeyData, name: "name", password: Data("password".utf8), coin: .ethereum, encryption: StoredKeyEncryption.aes192Ctr)! + let json = key.exportJSON()! + + let wallet = try keyStore.import(json: json, name: "name", password: "password", newPassword: "newPassword", coins: [.ethereum]) + let storedData = wallet.key.decryptPrivateKey(password: Data("newPassword".utf8)) + + XCTAssertNotNil(keyStore.keyWallet) + XCTAssertNotNil(storedData) + XCTAssertNotNil(PrivateKey(data: storedData!)) + } + + func testImportPrivateKeyAES256Ctr() throws { let keyStore = try KeyStore(keyDirectory: keyDirectory) let privateKeyData = Data(hexString: "9cdb5cab19aec3bd0fcd614c5f185e7a1d97634d4225730eba22497dc89a716c")! let key = StoredKey.importPrivateKeyWithEncryption(privateKey: privateKeyData, name: "name", password: Data("password".utf8), coin: .ethereum, encryption: StoredKeyEncryption.aes256Ctr)! @@ -275,7 +289,17 @@ class KeyStoreTests: XCTestCase { XCTAssertNotNil(keyStore.hdWallet) } - func testImportWalletAES256() throws { + func testImportWalletAES192Ctr() throws { + let keyStore = try KeyStore(keyDirectory: keyDirectory) + let wallet = try keyStore.import(mnemonic: mnemonic, name: "name", encryptPassword: "newPassword", coins: [.ethereum], encryption: .aes192Ctr) + let storedData = wallet.key.decryptMnemonic(password: Data("newPassword".utf8)) + + XCTAssertNotNil(storedData) + XCTAssertEqual(wallet.accounts.count, 1) + XCTAssertNotNil(keyStore.hdWallet) + } + + func testImportWalletAES256Ctr() throws { let keyStore = try KeyStore(keyDirectory: keyDirectory) let wallet = try keyStore.import(mnemonic: mnemonic, name: "name", encryptPassword: "newPassword", coins: [.ethereum], encryption: .aes256Ctr) let storedData = wallet.key.decryptMnemonic(password: Data("newPassword".utf8)) diff --git a/tests/common/Keystore/Data/cbc-encrypted.json b/tests/common/Keystore/Data/cbc-encrypted.json new file mode 100644 index 00000000000..8eb2b987511 --- /dev/null +++ b/tests/common/Keystore/Data/cbc-encrypted.json @@ -0,0 +1,19 @@ +{ + "version": 3, + "crypto": { + "cipher": "aes-128-cbc", + "cipherparams": { + "iv": "a4976ad73057007ad788d1f792d8851d" + }, + "ciphertext": "fd630455c685b412ac8ab7d5a2ee5df0320d8870ef599e49ed9ff68e88fe0034", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "n": 4096, + "r": 8, + "p": 1, + "salt": "0102030405060708090a0b0c0d0e0f10" + }, + "mac": "e465d8b0bd7946388c8415623ab28c0a001efcc1257f9eb96609ebe0f62c6e7a" + } +} diff --git a/tests/common/Keystore/StoredKeyTests.cpp b/tests/common/Keystore/StoredKeyTests.cpp index 82a14fbff16..69dd01ab9df 100644 --- a/tests/common/Keystore/StoredKeyTests.cpp +++ b/tests/common/Keystore/StoredKeyTests.cpp @@ -84,7 +84,23 @@ TEST(StoredKey, CreateWithMnemonicAddDefaultAddress) { EXPECT_EQ(hex(key.privateKey(coinTypeBc, gPassword).bytes), "d2568511baea8dc347f14c4e0479eb8ebe29eb5f664ed796e755896250ffd11f"); } -TEST(StoredKey, CreateWithMnemonicAddDefaultAddressAes256) { +TEST(StoredKey, CreateWithMnemonicAddDefaultAddressAes192Ctr) { + auto key = StoredKey::createWithMnemonicAddDefaultAddress("name", gPassword, gMnemonic, coinTypeBc, TWStoredKeyEncryptionAes192Ctr); + EXPECT_EQ(key.type, StoredKeyType::mnemonicPhrase); + auto header = key.payload; + EXPECT_EQ(header.params.cipher(), "aes-192-ctr"); + const Data& mnemo2Data = key.payload.decrypt(gPassword); + + EXPECT_EQ(string(mnemo2Data.begin(), mnemo2Data.end()), string(gMnemonic)); + EXPECT_EQ(key.accounts.size(), 1ul); + EXPECT_EQ(key.accounts[0].coin, coinTypeBc); + EXPECT_EQ(key.accounts[0].address, "bc1qturc268v0f2srjh4r2zu4t6zk4gdutqd5a6zny"); + EXPECT_EQ(key.accounts[0].publicKey, "02df6fc590ab3101bbe0bb5765cbaeab9b5dcfe09ac9315d707047cbd13bc7e006"); + EXPECT_EQ(key.accounts[0].extendedPublicKey, "zpub6qbsWdbcKW9sC6shTKK4VEhfWvDCoWpfLnnVfYKHLHt31wKYUwH3aFDz4WLjZvjHZ5W4qVEyk37cRwzTbfrrT1Gnu8SgXawASnkdQ994atn"); + EXPECT_EQ(hex(key.privateKey(coinTypeBc, gPassword).bytes), "d2568511baea8dc347f14c4e0479eb8ebe29eb5f664ed796e755896250ffd11f"); +} + +TEST(StoredKey, CreateWithMnemonicAddDefaultAddressAes256Ctr) { auto key = StoredKey::createWithMnemonicAddDefaultAddress("name", gPassword, gMnemonic, coinTypeBc, TWStoredKeyEncryptionAes256Ctr); EXPECT_EQ(key.type, StoredKeyType::mnemonicPhrase); auto header = key.payload; @@ -115,7 +131,24 @@ TEST(StoredKey, CreateWithPrivateKeyAddDefaultAddress) { EXPECT_EQ(json["version"], 3); } -TEST(StoredKey, CreateWithPrivateKeyAddDefaultAddressAes256) { +TEST(StoredKey, CreateWithPrivateKeyAddDefaultAddressAes192Ctr) { + const auto privateKey = parse_hex("3a1076bf45ab87712ad64ccb3b10217737f7faacbf2872e88fdd9a537d8fe266"); + auto key = StoredKey::createWithPrivateKeyAddDefaultAddress("name", gPassword, coinTypeBc, privateKey, TWStoredKeyEncryptionAes192Ctr); + auto header = key.payload; + EXPECT_EQ(header.params.cipher(), "aes-192-ctr"); + EXPECT_EQ(key.type, StoredKeyType::privateKey); + EXPECT_EQ(key.accounts.size(), 1ul); + EXPECT_EQ(key.accounts[0].coin, coinTypeBc); + EXPECT_EQ(key.accounts[0].address, "bc1q375sq4kl2nv0mlmup3vm8znn4eqwu7mt6hkwhr"); + EXPECT_EQ(hex(key.privateKey(coinTypeBc, gPassword).bytes), hex(privateKey)); + + const auto json = key.json(); + EXPECT_EQ(json["name"], "name"); + EXPECT_EQ(json["type"], "private-key"); + EXPECT_EQ(json["version"], 3); +} + +TEST(StoredKey, CreateWithPrivateKeyAddDefaultAddressAes256Ctr) { const auto privateKey = parse_hex("3a1076bf45ab87712ad64ccb3b10217737f7faacbf2872e88fdd9a537d8fe266"); auto key = StoredKey::createWithPrivateKeyAddDefaultAddress("name", gPassword, coinTypeBc, privateKey, TWStoredKeyEncryptionAes256Ctr); auto header = key.payload; @@ -414,6 +447,10 @@ TEST(StoredKey, InvalidIv) { ASSERT_THROW(StoredKey::load(testDataPath("invalid-iv.json")), std::invalid_argument); } +TEST(StoredKey, LoadCbcEncrypted) { + ASSERT_THROW(StoredKey::load(testDataPath("cbc-encrypted.json")), std::invalid_argument); +} + TEST(StoredKey, EmptyAccounts) { const auto key = StoredKey::load(testDataPath("empty-accounts.json")); @@ -727,6 +764,50 @@ TEST(StoredKey, CreateMultiAccounts) { // Multiple accounts from the same wallet } } +TEST(StoredKey, CreateWithEncodedPrivateKeyAddDefaultAddress) { + // Use a hex-encoded private key + const auto privateKeyHex = "3a1076bf45ab87712ad64ccb3b10217737f7faacbf2872e88fdd9a537d8fe266"; + auto key = StoredKey::createWithEncodedPrivateKeyAddDefaultAddress("name", gPassword, coinTypeBc, privateKeyHex); + + EXPECT_EQ(key.type, StoredKeyType::privateKey); + EXPECT_EQ(key.accounts.size(), 1ul); + EXPECT_EQ(key.accounts[0].coin, coinTypeBc); + EXPECT_EQ(key.accounts[0].address, "bc1q375sq4kl2nv0mlmup3vm8znn4eqwu7mt6hkwhr"); + + // Verify that the raw private key bytes can be decrypted from `payload` + const Data& decryptedKey = key.payload.decrypt(gPassword); + EXPECT_EQ(hex(decryptedKey), privateKeyHex); + + // Verify that the encoded private key string can be decrypted from `encodedPayload` + ASSERT_TRUE(key.encodedPayload.has_value()); + const Data& decryptedEncoded = key.encodedPayload->decrypt(gPassword); + EXPECT_EQ(string(decryptedEncoded.begin(), decryptedEncoded.end()), privateKeyHex); + + // Validate the JSON structure + const auto json = key.json(); + EXPECT_EQ(json["name"], "name"); + EXPECT_EQ(json["type"], "private-key"); + EXPECT_EQ(json["version"], 3); + EXPECT_TRUE(json.count("encodedCrypto") != 0); + + // Retrieve iv and salt from `crypto` (payload) and `encodedCrypto` (encodedPayload) + const auto payloadIv = json["crypto"]["cipherparams"]["iv"].get(); + const auto payloadSalt = json["crypto"]["kdfparams"]["salt"].get(); + + const auto encodedIv = json["encodedCrypto"]["cipherparams"]["iv"].get(); + const auto encodedSalt = json["encodedCrypto"]["kdfparams"]["salt"].get(); + + // iv and salt must be non-empty hex strings + EXPECT_EQ(payloadIv.size(), 32ul); // 16 bytes as hex + EXPECT_EQ(payloadSalt.size(), 64ul); // 32 bytes as hex + EXPECT_EQ(encodedIv.size(), 32ul); + EXPECT_EQ(encodedSalt.size(), 64ul); + + // iv and salt must differ between `payload` and `encodedPayload` + EXPECT_NE(payloadIv, encodedIv); + EXPECT_NE(payloadSalt, encodedSalt); +} + TEST(StoredKey, CreateWithMnemonicAlternativeDerivation) { const auto coin = TWCoinTypeSolana; auto key = StoredKey::createWithMnemonicAddDefaultAddress("name", gPassword, gMnemonic, coin); diff --git a/tests/interface/TWStoredKeyTests.cpp b/tests/interface/TWStoredKeyTests.cpp index 9eaf965a872..132ca66a28b 100644 --- a/tests/interface/TWStoredKeyTests.cpp +++ b/tests/interface/TWStoredKeyTests.cpp @@ -80,7 +80,24 @@ TEST(TWStoredKey, importPrivateKey) { TWPrivateKeyDelete(privateKey3); } -TEST(TWStoredKey, importPrivateKeyAes256) { +TEST(TWStoredKey, importPrivateKeyAes192Ctr) { + const auto privateKeyHex = "3a1076bf45ab87712ad64ccb3b10217737f7faacbf2872e88fdd9a537d8fe266"; + const auto privateKey = WRAPD(TWDataCreateWithHexString(WRAPS(TWStringCreateWithUTF8Bytes(privateKeyHex)).get())); + 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 = TWCoinTypeBitcoin; + const auto key = WRAP(TWStoredKey, TWStoredKeyImportPrivateKeyWithEncryption(privateKey.get(), name.get(), password.get(), coin, TWStoredKeyEncryptionAes192Ctr)); + const auto privateKey2 = WRAPD(TWStoredKeyDecryptPrivateKey(key.get(), password.get())); + EXPECT_EQ(hex(data(TWDataBytes(privateKey2.get()), TWDataSize(privateKey2.get()))), privateKeyHex); + + const auto privateKey3 = TWStoredKeyPrivateKey(key.get(), coin, password.get()); + const auto pkData3 = WRAPD(TWPrivateKeyData(privateKey3)); + EXPECT_EQ(hex(data(TWDataBytes(pkData3.get()), TWDataSize(pkData3.get()))), privateKeyHex); + TWPrivateKeyDelete(privateKey3); +} + +TEST(TWStoredKey, importPrivateKeyAes256Ctr) { const auto privateKeyHex = "3a1076bf45ab87712ad64ccb3b10217737f7faacbf2872e88fdd9a537d8fe266"; const auto privateKey = WRAPD(TWDataCreateWithHexString(WRAPS(TWStringCreateWithUTF8Bytes(privateKeyHex)).get())); const auto name = WRAPS(TWStringCreateWithUTF8Bytes("name")); @@ -294,7 +311,22 @@ TEST(TWStoredKey, importHDWallet) { EXPECT_EQ(nokey.get(), nullptr); } -TEST(TWStoredKey, importHDWalletAES256) { +TEST(TWStoredKey, importHDWalletAES192Ctr) { + const auto mnemonic = WRAPS(TWStringCreateWithUTF8Bytes("team engine square letter hero song dizzy scrub tornado fabric divert saddle")); + 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 = TWCoinTypeBitcoin; + const auto key = WRAP(TWStoredKey, TWStoredKeyImportHDWalletWithEncryption(mnemonic.get(), name.get(), password.get(), coin, TWStoredKeyEncryptionAes192Ctr)); + EXPECT_TRUE(TWStoredKeyIsMnemonic(key.get())); + + // invalid mnemonic + const auto mnemonicInvalid = WRAPS(TWStringCreateWithUTF8Bytes("_THIS_IS_AN_INVALID_MNEMONIC_")); + const auto nokey = WRAP(TWStoredKey, TWStoredKeyImportHDWalletWithEncryption(mnemonicInvalid.get(), name.get(), password.get(), coin, TWStoredKeyEncryptionAes192Ctr)); + EXPECT_EQ(nokey.get(), nullptr); +} + +TEST(TWStoredKey, importHDWalletAES256Ctr) { const auto mnemonic = WRAPS(TWStringCreateWithUTF8Bytes("team engine square letter hero song dizzy scrub tornado fabric divert saddle")); const auto name = WRAPS(TWStringCreateWithUTF8Bytes("name")); const auto passwordString = WRAPS(TWStringCreateWithUTF8Bytes("password")); @@ -401,7 +433,31 @@ TEST(TWStoredKey, exportJSON) { EXPECT_EQ(jsonBytes[0], '{'); } -TEST(TWStoredKey, storeAndImportJSONAES256) { +TEST(TWStoredKey, storeAndImportJSONAES192Ctr) { + const auto key = createDefaultStoredKey(TWStoredKeyEncryptionAes192Ctr); + 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 key2 = WRAP(TWStoredKey, TWStoredKeyImportJSON(WRAPD(TWDataCreateWithData(&json)).get())); + const auto name2 = WRAPS(TWStoredKeyName(key2.get())); + EXPECT_EQ(string(TWStringUTF8Bytes(name2.get())), "name"); +} + +TEST(TWStoredKey, storeAndImportJSONAES256Ctr) { const auto key = createDefaultStoredKey(TWStoredKeyEncryptionAes256Ctr); const auto outFileName = string(getTestTempDir() + "/TWStoredKey_store.json"); const auto outFileNameStr = WRAPS(TWStringCreateWithUTF8Bytes(outFileName.c_str())); diff --git a/wasm/tests/KeyStore+extension.test.ts b/wasm/tests/KeyStore+extension.test.ts index 69895fccf88..c10472d6770 100644 --- a/wasm/tests/KeyStore+extension.test.ts +++ b/wasm/tests/KeyStore+extension.test.ts @@ -64,7 +64,71 @@ describe("KeyStore", async () => { }); }).timeout(10000); - it("test ExtensionStorage AES256", async () => { + it("test ExtensionStorage AES192Ctr", async () => { + const { CoinType, Derivation, HexCoding, StoredKeyEncryption } = globalThis.core; + const mnemonic = globalThis.mnemonic 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); + + var wallet = await keystore.import(mnemonic, "Coolw", password, [ + CoinType.bitcoin, + ], StoredKeyEncryption.aes192Ctr); + + assert.equal(wallet.name, "Coolw"); + assert.equal(wallet.type, "mnemonic"); + assert.equal(wallet.version, 3); + + var account = wallet.activeAccounts[0]; + const key = await keystore.getKey(wallet.id, password, account); + + assert.equal( + HexCoding.encode(key.data()), + "0xd2568511baea8dc347f14c4e0479eb8ebe29eb5f664ed796e755896250ffd11f" + ); + assert.equal(account.address, "bc1qturc268v0f2srjh4r2zu4t6zk4gdutqd5a6zny"); + assert.equal( + account.extendedPublicKey, + "zpub6qbsWdbcKW9sC6shTKK4VEhfWvDCoWpfLnnVfYKHLHt31wKYUwH3aFDz4WLjZvjHZ5W4qVEyk37cRwzTbfrrT1Gnu8SgXawASnkdQ994atn" + ); + assert.equal( + account.publicKey, + "02df6fc590ab3101bbe0bb5765cbaeab9b5dcfe09ac9315d707047cbd13bc7e006" + ); + + wallet = await keystore.addAccounts(wallet.id, password, [ + CoinType.ethereum, + CoinType.binance, + ]); + + assert.equal(wallet.activeAccounts.length, 3); + assert.isTrue(await keystore.hasWallet(wallet.id)); + assert.isFalse(await keystore.hasWallet("invalid-id")); + + wallet = await keystore.addAccountsWithDerivations(wallet.id, password, [{ + coin: CoinType.solana, + derivation: Derivation.solanaSolana, + }]); + assert.equal(wallet.activeAccounts.length, 4); + account = wallet.activeAccounts[3]; + assert.equal(account.address, "CgWJeEWkiYqosy1ba7a3wn9HAQuHyK48xs3LM4SSDc1C"); + + const exported = await keystore.export(wallet.id, password); + assert.equal(exported, mnemonic); + + const wallets = await keystore.loadAll(); + + await wallets.forEach((w) => { + keystore.delete(w.id, password); + }); + }).timeout(10000); + + it("test ExtensionStorage AES256Ctr", async () => { const { CoinType, Derivation, HexCoding, StoredKeyEncryption } = globalThis.core; const mnemonic = globalThis.mnemonic as string; const password = globalThis.password as string;