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 {