Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ class EthereumKeyTests {
mnemonic,
passphrase = "",
derivationPath = "m/44'/539'/0'/0/0",
keyPair = null,
storage = storage
)

Expand Down
Original file line number Diff line number Diff line change
@@ -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<WalletError> {
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<WalletError> {
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<WalletError> {
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)

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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": {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -208,7 +212,6 @@ class EthereumWalletTests {
mnemonic,
passphrase = "",
derivationPath = "m/44'/539'/0'/0/0",
keyPair = null,
storage = storage
)
val wallet = TestWallet(key, storage)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Comment on lines +54 to +55

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TODO implementation should not return a hardcoded ByteArray(1). This could cause runtime issues if this method is actually called during testing.

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
Expand Down
Loading
Loading