From 59b4bcdd6c44a76aa2fe7ecfd0d4afc3b44137b9 Mon Sep 17 00:00:00 2001 From: lmcmz Date: Thu, 11 Dec 2025 23:10:11 +1100 Subject: [PATCH] Refactor Ethereum txId logic and update tests Standardizes Ethereum transaction hash calculation to use keccak256 of the signed/encoded transaction, removing previous logic that returned preHash. Updates related Android and iOS tests and wallet code to match this behavior, clarifies distinction between signing hash and transaction hash, and improves cache deserialization robustness. --- .../com/flow/wallet/keys/EthereumKeyTests.kt | 1 - .../wallet/keys/SeedPhraseKeyProviderTest.kt | 256 ++++++++++++++++++ .../flow/wallet/wallet/EthereumWalletTests.kt | 33 ++- .../wallet/wallet/WalletInstrumentedTest.kt | 14 +- .../com/flow/wallet/keys/SeedPhraseKey.kt | 40 +-- .../java/com/flow/wallet/storage/Cacheable.kt | 9 +- .../wallet/EthereumSigningOutputExtensions.kt | 10 +- .../wallet/keys/SeedPhraseKeyProviderTest.kt | 192 ------------- .../Wallet/EthereumSigningOutput+TxID.swift | 4 - .../Sources/Wallet/Wallet+EOA.swift | 5 +- .../Tests/FlowWalletKitTests/EOATests.swift | 14 +- 11 files changed, 324 insertions(+), 254 deletions(-) create mode 100644 Android/wallet/src/androidTest/java/com/flow/wallet/keys/SeedPhraseKeyProviderTest.kt delete mode 100644 Android/wallet/src/test/java/com/flow/wallet/keys/SeedPhraseKeyProviderTest.kt diff --git a/Android/wallet/src/androidTest/java/com/flow/wallet/keys/EthereumKeyTests.kt b/Android/wallet/src/androidTest/java/com/flow/wallet/keys/EthereumKeyTests.kt index 18839b7..0632c54 100644 --- a/Android/wallet/src/androidTest/java/com/flow/wallet/keys/EthereumKeyTests.kt +++ b/Android/wallet/src/androidTest/java/com/flow/wallet/keys/EthereumKeyTests.kt @@ -30,7 +30,6 @@ class EthereumKeyTests { mnemonic, passphrase = "", derivationPath = "m/44'/539'/0'/0/0", - keyPair = null, storage = storage ) diff --git a/Android/wallet/src/androidTest/java/com/flow/wallet/keys/SeedPhraseKeyProviderTest.kt b/Android/wallet/src/androidTest/java/com/flow/wallet/keys/SeedPhraseKeyProviderTest.kt new file mode 100644 index 0000000..920039e --- /dev/null +++ b/Android/wallet/src/androidTest/java/com/flow/wallet/keys/SeedPhraseKeyProviderTest.kt @@ -0,0 +1,256 @@ +package com.flow.wallet.keys + +import com.flow.wallet.errors.WalletError +import com.flow.wallet.crypto.ChaChaPolyCipher +import com.flow.wallet.storage.StorageProtocol +import com.flow.wallet.storage.InMemoryStorage +import junit.framework.TestCase.assertEquals +import junit.framework.TestCase.assertNotNull +import junit.framework.TestCase.assertTrue +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations +import org.mockito.junit.MockitoJUnitRunner +import org.mockito.kotlin.any +import org.onflow.flow.models.HashingAlgorithm +import org.onflow.flow.models.SigningAlgorithm +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse + +@RunWith(MockitoJUnitRunner::class) +class SeedPhraseKeyProviderTest { + + @Mock + private lateinit var mockStorage: StorageProtocol + + private lateinit var seedPhraseKeyProvider: SeedPhraseKeyProvider + private val validSeedPhrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" + + @Before + fun setup() { + MockitoAnnotations.openMocks(this) + seedPhraseKeyProvider = SeedPhraseKey(validSeedPhrase, "", "m/44'/539'/0'/0/0", mockStorage) + } + + @Test + fun `test key derivation`() { + val derivedKey = seedPhraseKeyProvider.deriveKey(0) + assertNotNull(derivedKey) + assertTrue(derivedKey is PrivateKey) + } + + @Test + fun `test key derivation with different indices`() { + val key1 = seedPhraseKeyProvider.deriveKey(0) + val key2 = seedPhraseKeyProvider.deriveKey(1) + + assertNotNull(key1) + assertNotNull(key2) + assertTrue(key1 is PrivateKey) + assertTrue(key2 is PrivateKey) + + // Different indices should produce different keys + assertTrue(!key1.secret.contentEquals(key2.secret)) + } + + @Test + fun `test key creation with default options`() { + runBlocking { + val key = seedPhraseKeyProvider.create(mockStorage) + assertNotNull(key) + assertEquals(KeyType.SEED_PHRASE, key.keyType) + } + } + + @Test + fun `test key creation with advanced options`() { + runBlocking { + val key = seedPhraseKeyProvider.create(Unit, mockStorage) + assertNotNull(key) + assertEquals(KeyType.SEED_PHRASE, key.keyType) + } + } + + @Test + fun `test key storage and retrieval`() { + runBlocking { + val testId = "test_key" + val testPassword = "test_password" + val encryptedData = "encrypted_data".toByteArray() + + `when`(mockStorage.get(testId)).thenReturn(encryptedData) + + val key = seedPhraseKeyProvider.createAndStore(testId, testPassword, mockStorage) + assertNotNull(key) + assertEquals(KeyType.SEED_PHRASE, key.keyType) + verify(mockStorage).set(testId, any()) + } + } + + @Test + fun `test storage failure scenarios`() { + runBlocking { + val testId = "test_key" + val testPassword = "test_password" + + `when`(mockStorage.set(any(), any())).thenThrow(RuntimeException("Storage error")) + + assertFailsWith { + seedPhraseKeyProvider.createAndStore(testId, testPassword, mockStorage) + } + } + } + + @Test + fun `test key retrieval with invalid password`() { + runBlocking { + val testId = "test_key" + val testPassword = "invalid_password" + val encryptedData = "encrypted_data".toByteArray() + + `when`(mockStorage.get(testId)).thenReturn(encryptedData) + + assertFailsWith { + seedPhraseKeyProvider.get(testId, testPassword, mockStorage) + } + } + } + + @Test + fun `test key restoration`() { + runBlocking { + val secret = "test_secret".toByteArray() + val restoredKey = seedPhraseKeyProvider.restore(secret, mockStorage) + assertNotNull(restoredKey) + assertEquals(KeyType.SEED_PHRASE, restoredKey.keyType) + } + } + + @Test + fun `test key restoration with invalid data`() { + runBlocking { + val invalidSecret = ByteArray(32) { it.toByte() } + assertFailsWith { + seedPhraseKeyProvider.restore(invalidSecret, mockStorage) + } + } + } + + @Test + fun `test public key retrieval for different algorithms`() { + val p256Key = seedPhraseKeyProvider.publicKey(SigningAlgorithm.ECDSA_P256) + val secp256k1Key = seedPhraseKeyProvider.publicKey(SigningAlgorithm.ECDSA_secp256k1) + + assertNotNull(p256Key) + assertNotNull(secp256k1Key) + if (p256Key != null) { + assertTrue(p256Key.isNotEmpty()) + } + if (secp256k1Key != null) { + assertTrue(secp256k1Key.isNotEmpty()) + } + } + + @Test + fun `test signing and verification`() { + runBlocking { + val message = "test message".toByteArray() + val signature = seedPhraseKeyProvider.sign(message, SigningAlgorithm.ECDSA_P256, HashingAlgorithm.SHA2_256) + + assertTrue(signature.isNotEmpty()) + assertTrue(seedPhraseKeyProvider.isValidSignature(signature, message, SigningAlgorithm.ECDSA_P256, HashingAlgorithm.SHA2_256)) + } + } + + @Test + fun `test signing with different hashing algorithms`() { + runBlocking { + val message = "test message".toByteArray() + + val sha2_256 = seedPhraseKeyProvider.sign(message, SigningAlgorithm.ECDSA_P256, HashingAlgorithm.SHA2_256) + val sha3_256 = seedPhraseKeyProvider.sign(message, SigningAlgorithm.ECDSA_P256, HashingAlgorithm.SHA3_256) + + assertTrue(sha2_256.isNotEmpty()) + assertTrue(sha3_256.isNotEmpty()) + } + } + + @Test + fun `test invalid signature verification`() { + val message = "test message".toByteArray() + val invalidSignature = "invalid signature".toByteArray() + + assertFalse(seedPhraseKeyProvider.isValidSignature(invalidSignature, message, SigningAlgorithm.ECDSA_P256, HashingAlgorithm.SHA2_256)) + } + + @Test + fun `test key removal`() { + runBlocking { + val testId = "test_key" + seedPhraseKeyProvider.remove(testId) + verify(mockStorage).remove(testId) + } + } + + @Test + fun `test getting all keys`() { + val testKeys = listOf("key1", "key2") + `when`(mockStorage.allKeys).thenReturn(testKeys) + + val allKeys = seedPhraseKeyProvider.allKeys() + assertEquals(testKeys, allKeys) + verify(mockStorage).allKeys + } + + @Test + fun `test hardware backed property`() { + assertFalse(seedPhraseKeyProvider.isHardwareBacked) + } + + @Test + fun `legacy keydata with length field is still readable`() { + runBlocking { + val storage = InMemoryStorage() + val password = "test_password" + val testId = "legacy_seed" + + // Simulate old app writing a JSON blob that includes an extra "length" field. + // Cover both numeric and string enum representations to be safe. + val legacyJsonNumeric = """ + { + "mnemonic": "$validSeedPhrase", + "passphrase": "", + "path": "m/44'/539'/0'/0/0", + "length": 12 + } + """.trimIndent().toByteArray() + val legacyJsonStringEnum = """ + { + "mnemonic": "$validSeedPhrase", + "passphrase": "", + "path": "m/44'/539'/0'/0/0", + "length": "TWELVE" + } + """.trimIndent().toByteArray() + + val cipher = ChaChaPolyCipher(password) + val encryptedNumeric = cipher.encrypt(legacyJsonNumeric) + storage.set(testId, encryptedNumeric) + val restoredNumeric = seedPhraseKeyProvider.get(testId, password, storage) as SeedPhraseKey + assertEquals(validSeedPhrase, restoredNumeric.mnemonic.joinToString(" ")) + assertEquals("m/44'/539'/0'/0/0", restoredNumeric.derivationPath) + + val encryptedEnum = cipher.encrypt(legacyJsonStringEnum) + storage.set(testId, encryptedEnum) + val restoredEnum = seedPhraseKeyProvider.get(testId, password, storage) as SeedPhraseKey + assertEquals(validSeedPhrase, restoredEnum.mnemonic.joinToString(" ")) + assertEquals("m/44'/539'/0'/0/0", restoredEnum.derivationPath) + + } + } +} diff --git a/Android/wallet/src/androidTest/java/com/flow/wallet/wallet/EthereumWalletTests.kt b/Android/wallet/src/androidTest/java/com/flow/wallet/wallet/EthereumWalletTests.kt index a0c4b03..6d49b8f 100644 --- a/Android/wallet/src/androidTest/java/com/flow/wallet/wallet/EthereumWalletTests.kt +++ b/Android/wallet/src/androidTest/java/com/flow/wallet/wallet/EthereumWalletTests.kt @@ -24,11 +24,21 @@ class EthereumWalletTests { private val mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" private val privateKeyHex = "1ab42cc412b618bdea3a599e3c9bae199ebf030895b039e9db1e30dafb12b727" - private val expectedAddress = "0x90f8bf6a479f320ead074411a4b0e7944ea8c9c1" + private val expectedAddress = "0x9858EfFD232B4033E47d90003D41EC34EcaEda94" + private val storage = InMemoryStorage() + private lateinit var wallet: TestWallet + private lateinit var key: SeedPhraseKey @Before fun setup() { Assert.assertTrue(NativeLibraryManager.ensureLibraryLoaded()) + key = SeedPhraseKey(mnemonic, storage = storage) + wallet = TestWallet(key, storage) + } + + @Test + fun eoaAddress() = runBlocking { + assertEquals(expectedAddress, wallet.ethAddress()) } @Test @@ -76,16 +86,6 @@ class EthereumWalletTests { @Test fun walletTypedDataSigningMatchesDirectSignature() = runBlocking { - val storage = InMemoryStorage() - val key = SeedPhraseKey( - mnemonic, - passphrase = "", - derivationPath = "m/44'/539'/0'/0/0", - keyPair = null, - storage = storage - ) - val wallet = TestWallet(key, storage) - val typedData = """ { "types": { @@ -168,9 +168,13 @@ class EthereumWalletTests { "f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83", output.encoded.toByteArray().toHexString() ) - val expectedHash = HasherImpl.keccak256(output.encoded.toByteArray()).toHexString() - assertEquals(expectedHash, output.preHash.toByteArray().toHexString()) - assertEquals(expectedHash, output.txId().toHexString()) + val expectedSigningHash = "daf5a779ae972f972197303d7b574746c7ef83eadac0f2791ad23db92e4c8e53" + val expectedTxHash = HasherImpl.keccak256(output.encoded.toByteArray()).toHexString() + + // WalletCore's preHash is the signing hash (hash of the unsigned payload) + assertEquals(expectedSigningHash, output.preHash.toByteArray().toHexString()) + // txId should be keccak of the signed transaction + assertEquals(expectedTxHash, output.txId().toHexString()) } @Test @@ -208,7 +212,6 @@ class EthereumWalletTests { mnemonic, passphrase = "", derivationPath = "m/44'/539'/0'/0/0", - keyPair = null, storage = storage ) val wallet = TestWallet(key, storage) diff --git a/Android/wallet/src/androidTest/java/com/flow/wallet/wallet/WalletInstrumentedTest.kt b/Android/wallet/src/androidTest/java/com/flow/wallet/wallet/WalletInstrumentedTest.kt index 76f9f2b..1eb324a 100644 --- a/Android/wallet/src/androidTest/java/com/flow/wallet/wallet/WalletInstrumentedTest.kt +++ b/Android/wallet/src/androidTest/java/com/flow/wallet/wallet/WalletInstrumentedTest.kt @@ -17,6 +17,7 @@ import org.junit.runner.RunWith import org.onflow.flow.ChainId import org.onflow.flow.models.HashingAlgorithm import org.onflow.flow.models.Signer +import org.onflow.flow.models.Transaction import kotlin.test.assertEquals import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -42,11 +43,18 @@ class WalletInstrumentedTest { override fun getPublicKey(): String = "test_public_key" override suspend fun getUserSignature(jwt: String): String = "test_signature" override suspend fun signData(data: ByteArray): String = "test_signed_data" - override fun getSigner(hashingAlgorithm: HashingAlgorithm): Signer = object : org.onflow.flow.models.Signer { + override fun getSigner(hashingAlgorithm: HashingAlgorithm): Signer = object : org.onflow.flow.models.Signer { override var address: String = testAddress override var keyIndex: Int = 0 - override suspend fun sign(transaction: org.onflow.flow.models.Transaction?, bytes: ByteArray): ByteArray = "test_signature".toByteArray() - override suspend fun sign(bytes: ByteArray): ByteArray = "test_signature".toByteArray() + override suspend fun sign( + bytes: ByteArray, + transaction: Transaction? + ): ByteArray { + TODO("Not yet implemented") + return ByteArray(1) + } + suspend fun sign(transaction: org.onflow.flow.models.Transaction?, bytes: ByteArray): ByteArray = "test_signature".toByteArray() + suspend fun sign(bytes: ByteArray): ByteArray = "test_signature".toByteArray() } override fun getHashAlgorithm(): org.onflow.flow.models.HashingAlgorithm = org.onflow.flow.models.HashingAlgorithm.SHA2_256 override fun getSignatureAlgorithm(): org.onflow.flow.models.SigningAlgorithm = org.onflow.flow.models.SigningAlgorithm.ECDSA_P256 diff --git a/Android/wallet/src/main/java/com/flow/wallet/keys/SeedPhraseKey.kt b/Android/wallet/src/main/java/com/flow/wallet/keys/SeedPhraseKey.kt index d676f4f..6dfcba7 100644 --- a/Android/wallet/src/main/java/com/flow/wallet/keys/SeedPhraseKey.kt +++ b/Android/wallet/src/main/java/com/flow/wallet/keys/SeedPhraseKey.kt @@ -8,8 +8,8 @@ import com.flow.wallet.crypto.ChaChaPolyCipher import com.flow.wallet.crypto.HasherImpl import com.flow.wallet.errors.WalletError import com.flow.wallet.storage.StorageProtocol -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.SerialName import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import org.onflow.flow.models.HashingAlgorithm @@ -27,9 +27,7 @@ private data class KeyData( @SerialName("passphrase") val passphrase: String, @SerialName("path") - val path: String, - @SerialName("length") - val length: BIP39.SeedPhraseLength + val path: String ) /** @@ -38,13 +36,13 @@ private data class KeyData( */ class SeedPhraseKey( private val mnemonicString: String, - private val passphrase: String, - override val derivationPath: String, - override var storage: StorageProtocol, - private val seedPhraseLength: BIP39.SeedPhraseLength = BIP39.SeedPhraseLength.TWELVE + private val passphrase: String = DEFAULT_PASSPHRASE, + override val derivationPath: String = DEFAULT_DERIVATION_PATH, + override var storage: StorageProtocol ) : SeedPhraseKeyProvider, EthereumKeyProtocol { companion object { private const val TAG = "SeedPhraseKey" + private const val DEFAULT_PASSPHRASE = "" public const val DEFAULT_DERIVATION_PATH = "m/44'/539'/0'/0/0" public const val ETH_DERIVATION_PREFIX = "m/44'/60'/0'/0" @@ -132,8 +130,8 @@ class SeedPhraseKey( val encryptedData = storage.get(id) ?: throw WalletError.EmptyKeychain val cipher = ChaChaPolyCipher(password) val keyDataStr = String(cipher.decrypt(encryptedData), Charsets.UTF_8) - val keyData = Json.decodeFromString(keyDataStr) - return SeedPhraseKey(keyData.mnemonic, keyData.passphrase, keyData.path, storage, keyData.length) + val keyData = Json { ignoreUnknownKeys = true }.decodeFromString(keyDataStr) + return SeedPhraseKey(keyData.mnemonic, keyData.passphrase, keyData.path, storage) } override suspend fun restore(secret: ByteArray, storage: StorageProtocol): KeyProtocol { @@ -269,16 +267,26 @@ class SeedPhraseKey( return storage.allKeys } - private fun createKeyData(): ByteArray { + private fun createKeyDataOld(): ByteArray { val data = mapOf( "mnemonic" to mnemonicString, "passphrase" to passphrase, "path" to derivationPath, - "length" to seedPhraseLength + "length" to "" ) return Json.encodeToString(data).toByteArray() } + + private fun createKeyData(): ByteArray { + val data = KeyData( + mnemonic = mnemonicString, + passphrase = passphrase, + path = derivationPath + ) + return Json { ignoreUnknownKeys = true }.encodeToString(data).toByteArray() + } + private fun encryptData(data: ByteArray, password: String): ByteArray { val cipher = ChaChaPolyCipher(password) return cipher.encrypt(data) @@ -329,12 +337,4 @@ class SeedPhraseKey( } } -// Helper class for returning four values -private data class Quadruple( - val first: A, - val second: B, - val third: C, - val fourth: D -) - private fun ByteArray.toHexString(): String = BaseEncoding.base16().lowerCase().encode(this) diff --git a/Android/wallet/src/main/java/com/flow/wallet/storage/Cacheable.kt b/Android/wallet/src/main/java/com/flow/wallet/storage/Cacheable.kt index fe7270f..12a072b 100644 --- a/Android/wallet/src/main/java/com/flow/wallet/storage/Cacheable.kt +++ b/Android/wallet/src/main/java/com/flow/wallet/storage/Cacheable.kt @@ -5,6 +5,9 @@ import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json import java.util.concurrent.TimeUnit +// Use a lenient JSON instance so added fields won't break cache deserialization +private val cacheJson = Json { ignoreUnknownKeys = true } + /** * Protocol defining caching behavior for wallet components * @param T The type of data being cached (must be serializable) @@ -41,7 +44,7 @@ interface Cacheable where T : @Serializable Any { data = data, expiresIn = expiresIn ?: cacheExpiration ) - val json = Json.encodeToString(wrapper) + val json = cacheJson.encodeToString(wrapper) storage.set(cacheId, json.toByteArray()) } @@ -53,7 +56,7 @@ interface Cacheable where T : @Serializable Any { fun loadCache(ignoreExpiration: Boolean = false): T? { val data = storage.get(cacheId) ?: return null val json = String(data) - val wrapper = Json.decodeFromString>(json) + val wrapper = cacheJson.decodeFromString>(json) if (!ignoreExpiration && wrapper.isExpired) { deleteCache() @@ -77,4 +80,4 @@ interface Cacheable where T : @Serializable Any { fun Cacheable<*>.expiresInDays(days: Long) = TimeUnit.DAYS.toMillis(days) fun Cacheable<*>.expiresInHours(hours: Long) = TimeUnit.HOURS.toMillis(hours) fun Cacheable<*>.expiresInMinutes(minutes: Long) = TimeUnit.MINUTES.toMillis(minutes) -fun Cacheable<*>.expiresInSeconds(seconds: Long) = TimeUnit.SECONDS.toMillis(seconds) \ No newline at end of file +fun Cacheable<*>.expiresInSeconds(seconds: Long) = TimeUnit.SECONDS.toMillis(seconds) diff --git a/Android/wallet/src/main/java/com/flow/wallet/wallet/EthereumSigningOutputExtensions.kt b/Android/wallet/src/main/java/com/flow/wallet/wallet/EthereumSigningOutputExtensions.kt index c1b9ea9..ae21a85 100644 --- a/Android/wallet/src/main/java/com/flow/wallet/wallet/EthereumSigningOutputExtensions.kt +++ b/Android/wallet/src/main/java/com/flow/wallet/wallet/EthereumSigningOutputExtensions.kt @@ -8,10 +8,6 @@ import wallet.core.jni.proto.Ethereum * Utilities for extracting transaction hash information from Ethereum signing outputs. */ fun Ethereum.SigningOutput.txId(): ByteArray { - val existing = preHash.toByteArray() - if (existing.isNotEmpty()) { - return existing - } - val computed = HasherImpl.keccak256(encoded.toByteArray()) - return computed -} \ No newline at end of file + // Transaction hash should be keccak256 of the signed/encoded transaction. + return HasherImpl.keccak256(encoded.toByteArray()) +} diff --git a/Android/wallet/src/test/java/com/flow/wallet/keys/SeedPhraseKeyProviderTest.kt b/Android/wallet/src/test/java/com/flow/wallet/keys/SeedPhraseKeyProviderTest.kt deleted file mode 100644 index ea64ce3..0000000 --- a/Android/wallet/src/test/java/com/flow/wallet/keys/SeedPhraseKeyProviderTest.kt +++ /dev/null @@ -1,192 +0,0 @@ -package com.flow.wallet.keys - -import com.flow.wallet.errors.WalletError -import com.flow.wallet.storage.StorageProtocol -import junit.framework.TestCase.assertEquals -import junit.framework.TestCase.assertNotNull -import junit.framework.TestCase.assertTrue -import kotlinx.coroutines.runBlocking -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mock -import org.mockito.Mockito.`when` -import org.mockito.Mockito.verify -import org.mockito.MockitoAnnotations -import org.mockito.junit.MockitoJUnitRunner -import org.mockito.kotlin.any -import org.onflow.flow.models.HashingAlgorithm -import org.onflow.flow.models.SigningAlgorithm -import kotlin.test.assertFailsWith -import kotlin.test.assertFalse - -@RunWith(MockitoJUnitRunner::class) -class SeedPhraseKeyProviderTest { - - @Mock - private lateinit var mockStorage: StorageProtocol - - private lateinit var seedPhraseKeyProvider: SeedPhraseKeyProvider - private val validSeedPhrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about" - - @Before - fun setup() { - MockitoAnnotations.openMocks(this) - seedPhraseKeyProvider = SeedPhraseKey(validSeedPhrase, "", "m/44'/539'/0'/0/0", mockStorage) - } - - @Test - fun `test key derivation`() { - val derivedKey = seedPhraseKeyProvider.deriveKey(0) - assertNotNull(derivedKey) - assertTrue(derivedKey is PrivateKey) - } - - @Test - fun `test key derivation with different indices`() { - val key1 = seedPhraseKeyProvider.deriveKey(0) - val key2 = seedPhraseKeyProvider.deriveKey(1) - - assertNotNull(key1) - assertNotNull(key2) - assertTrue(key1 is PrivateKey) - assertTrue(key2 is PrivateKey) - - // Different indices should produce different keys - assertTrue(!key1.secret.contentEquals(key2.secret)) - } - - @Test - fun `test key creation with default options`() = runBlocking { - val key = seedPhraseKeyProvider.create(mockStorage) - assertNotNull(key) - assertEquals(KeyType.SEED_PHRASE, key.keyType) - } - - @Test - fun `test key creation with advanced options`() = runBlocking { - val key = seedPhraseKeyProvider.create(Unit, mockStorage) - assertNotNull(key) - assertEquals(KeyType.SEED_PHRASE, key.keyType) - } - - @Test - fun `test key storage and retrieval`() = runBlocking { - val testId = "test_key" - val testPassword = "test_password" - val encryptedData = "encrypted_data".toByteArray() - - `when`(mockStorage.get(testId)).thenReturn(encryptedData) - - val key = seedPhraseKeyProvider.createAndStore(testId, testPassword, mockStorage) - assertNotNull(key) - assertEquals(KeyType.SEED_PHRASE, key.keyType) - verify(mockStorage).set(testId, any()) - } - - @Test - fun `test storage failure scenarios`() = runBlocking { - val testId = "test_key" - val testPassword = "test_password" - - `when`(mockStorage.set(any(), any())).thenThrow(RuntimeException("Storage error")) - - assertFailsWith { - seedPhraseKeyProvider.createAndStore(testId, testPassword, mockStorage) - } - } - - @Test - fun `test key retrieval with invalid password`() = runBlocking { - val testId = "test_key" - val testPassword = "invalid_password" - val encryptedData = "encrypted_data".toByteArray() - - `when`(mockStorage.get(testId)).thenReturn(encryptedData) - - assertFailsWith { - seedPhraseKeyProvider.get(testId, testPassword, mockStorage) - } - } - - @Test - fun `test key restoration`() = runBlocking { - val secret = "test_secret".toByteArray() - val restoredKey = seedPhraseKeyProvider.restore(secret, mockStorage) - assertNotNull(restoredKey) - assertEquals(KeyType.SEED_PHRASE, restoredKey.keyType) - } - - @Test - fun `test key restoration with invalid data`() = runBlocking { - val invalidSecret = ByteArray(32) { it.toByte() } - assertFailsWith { - seedPhraseKeyProvider.restore(invalidSecret, mockStorage) - } - } - - @Test - fun `test public key retrieval for different algorithms`() { - val p256Key = seedPhraseKeyProvider.publicKey(SigningAlgorithm.ECDSA_P256) - val secp256k1Key = seedPhraseKeyProvider.publicKey(SigningAlgorithm.ECDSA_secp256k1) - - assertNotNull(p256Key) - assertNotNull(secp256k1Key) - if (p256Key != null) { - assertTrue(p256Key.isNotEmpty()) - } - if (secp256k1Key != null) { - assertTrue(secp256k1Key.isNotEmpty()) - } - } - - @Test - fun `test signing and verification`() = runBlocking { - val message = "test message".toByteArray() - val signature = seedPhraseKeyProvider.sign(message, SigningAlgorithm.ECDSA_P256, HashingAlgorithm.SHA2_256) - - assertTrue(signature.isNotEmpty()) - assertTrue(seedPhraseKeyProvider.isValidSignature(signature, message, SigningAlgorithm.ECDSA_P256, HashingAlgorithm.SHA2_256)) - } - - @Test - fun `test signing with different hashing algorithms`() = runBlocking { - val message = "test message".toByteArray() - - val sha2_256 = seedPhraseKeyProvider.sign(message, SigningAlgorithm.ECDSA_P256, HashingAlgorithm.SHA2_256) - val sha3_256 = seedPhraseKeyProvider.sign(message, SigningAlgorithm.ECDSA_P256, HashingAlgorithm.SHA3_256) - - assertTrue(sha2_256.isNotEmpty()) - assertTrue(sha3_256.isNotEmpty()) - } - - @Test - fun `test invalid signature verification`() { - val message = "test message".toByteArray() - val invalidSignature = "invalid signature".toByteArray() - - assertFalse(seedPhraseKeyProvider.isValidSignature(invalidSignature, message, SigningAlgorithm.ECDSA_P256, HashingAlgorithm.SHA2_256)) - } - - @Test - fun `test key removal`() = runBlocking { - val testId = "test_key" - seedPhraseKeyProvider.remove(testId) - verify(mockStorage).remove(testId) - } - - @Test - fun `test getting all keys`() { - val testKeys = listOf("key1", "key2") - `when`(mockStorage.allKeys).thenReturn(testKeys) - - val allKeys = seedPhraseKeyProvider.allKeys() - assertEquals(testKeys, allKeys) - verify(mockStorage).allKeys - } - - @Test - fun `test hardware backed property`() { - assertFalse(seedPhraseKeyProvider.isHardwareBacked) - } -} diff --git a/iOS/FlowWalletKit/Sources/Wallet/EthereumSigningOutput+TxID.swift b/iOS/FlowWalletKit/Sources/Wallet/EthereumSigningOutput+TxID.swift index 97cac61..4f153b3 100644 --- a/iOS/FlowWalletKit/Sources/Wallet/EthereumSigningOutput+TxID.swift +++ b/iOS/FlowWalletKit/Sources/Wallet/EthereumSigningOutput+TxID.swift @@ -9,9 +9,6 @@ import WalletCore public extension EthereumSigningOutput { /// Returns the transaction hash (txid) for the signed payload. func txId() -> Data { - if !preHash.isEmpty { - return preHash - } return Hash.keccak256(data: encoded) } @@ -22,4 +19,3 @@ public extension EthereumSigningOutput { return "0x" + hash.hexString } } - diff --git a/iOS/FlowWalletKit/Sources/Wallet/Wallet+EOA.swift b/iOS/FlowWalletKit/Sources/Wallet/Wallet+EOA.swift index 8122ec6..c1a7b07 100644 --- a/iOS/FlowWalletKit/Sources/Wallet/Wallet+EOA.swift +++ b/iOS/FlowWalletKit/Sources/Wallet/Wallet+EOA.swift @@ -83,10 +83,7 @@ extension Wallet { var signingInput = input signingInput.privateKey = try key.ethPrivateKey(index: index) defer { signingInput.privateKey = Data() } - var output: EthereumSigningOutput = AnySigner.sign(input: signingInput, coin: .ethereum) - let transactionHash = Hash.keccak256(data: output.encoded) - output.preHash = transactionHash - return output + return AnySigner.sign(input: signingInput, coin: .ethereum) } public func refreshEOAAddresses() { diff --git a/iOS/FlowWalletKit/Tests/FlowWalletKitTests/EOATests.swift b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/EOATests.swift index 565fda1..003dff1 100644 --- a/iOS/FlowWalletKit/Tests/FlowWalletKitTests/EOATests.swift +++ b/iOS/FlowWalletKit/Tests/FlowWalletKitTests/EOATests.swift @@ -162,7 +162,7 @@ final class EOATests: XCTestCase { func testWalletTransactionSigningMatchesWalletCoreExample() throws { let storage = makeEphemeralStorage() guard - let pkData = Data(hexString: samplePrivateKeyHex), + let pkData = Data(hexString: "0x4646464646464646464646464646464646464646464646464646464646464646"), let corePrivateKey = WalletCore.PrivateKey(data: pkData) else { XCTFail("Failed to create WalletCore.PrivateKey from sample data") @@ -185,10 +185,14 @@ final class EOATests: XCTestCase { let output = try wallet.ethSignTransaction(input) XCTAssertEqual(output.encoded.hexString, "f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83") - let expectedHash = Hash.keccak256(data: output.encoded) - XCTAssertEqual(output.preHash.hexString, expectedHash.hexString) - XCTAssertEqual(output.txId().hexString, expectedHash.hexString) - XCTAssertEqual(output.txIdHex(), "0x" + expectedHash.hexString) + let expectedSigningHash = "daf5a779ae972f972197303d7b574746c7ef83eadac0f2791ad23db92e4c8e53" + let expectedTxHash = Hash.keccak256(data: output.encoded).hexString + + // WalletCore preHash is the signing hash (pre-signing payload hash) + XCTAssertEqual(output.preHash.hexString, expectedSigningHash) + // txId uses keccak of the signed/encoded transaction + XCTAssertEqual(output.txId().hexString, expectedTxHash) + XCTAssertEqual(output.txIdHex(), "0x" + expectedTxHash) } func testEcRecoverReturnsExpectedAddress() throws {