From 5cbf0f45337d29e17a58eb0a8166592d0e0fc388 Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Wed, 17 Dec 2025 18:41:26 +0900 Subject: [PATCH 1/3] fix: debugging key logic for secure enclave --- .../manager/key/KeyCompatibilityManager.kt | 37 ++- .../wallet/network/UserRegisterUtils.kt | 280 ++++++++++++++---- 2 files changed, 255 insertions(+), 62 deletions(-) diff --git a/app/src/main/java/com/flowfoundation/wallet/manager/key/KeyCompatibilityManager.kt b/app/src/main/java/com/flowfoundation/wallet/manager/key/KeyCompatibilityManager.kt index 69653ce43..76b2ff450 100644 --- a/app/src/main/java/com/flowfoundation/wallet/manager/key/KeyCompatibilityManager.kt +++ b/app/src/main/java/com/flowfoundation/wallet/manager/key/KeyCompatibilityManager.kt @@ -2,6 +2,7 @@ package com.flowfoundation.wallet.manager.key import com.flow.wallet.keys.PrivateKey import com.flow.wallet.storage.StorageProtocol +import com.flowfoundation.wallet.network.getKeystoreAliasForPrefix import com.flowfoundation.wallet.utils.logd import com.flowfoundation.wallet.utils.loge import java.security.KeyStore @@ -14,8 +15,9 @@ import com.flowfoundation.wallet.manager.account.HardwareBackedKeyException /** * Handles backward compatibility between old Android Keystore pattern and new Flow-Wallet-Kit storage. * - * Old pattern: user_keystore_{prefix} -> Android Keystore - * New pattern: prefix_key_{prefix} -> Flow-Wallet-Kit storage + * Old pattern: user_keystore_{prefix} -> Android Keystore (legacy) + * New pattern: prefix_key_{prefix} -> Flow-Wallet-Kit storage (mnemonic-based) + * Secure Enclave pattern: secure_enclave_{prefix} -> Android Keystore (hardware-backed, managed via getKeystoreAliasForPrefix) */ object KeyCompatibilityManager { private const val TAG = "KeyCompatibility" @@ -24,16 +26,37 @@ object KeyCompatibilityManager { /** * Attempts to get a private key with backward compatibility support. - * First tries the new storage pattern, then falls back to old Android Keystore pattern. + * First checks for Secure Enclave (hardware-backed) keys, then tries new storage, + * then falls back to old Android Keystore pattern. * * @param prefix The account prefix/password * @param storage The Flow-Wallet-Kit storage instance * @return PrivateKey instance or null if not found in either storage + * @throws HardwareBackedKeyException if the key is hardware-backed (Secure Enclave) */ fun getPrivateKeyWithFallback(prefix: String, storage: StorageProtocol): PrivateKey? { - logd(TAG, "Attempting to get private key") + logd(TAG, "Attempting to get private key for prefix: $prefix") - // First try the new storage pattern + // First check if this is a Secure Enclave (hardware-backed) key + val secureEnclaveAlias = getKeystoreAliasForPrefix(prefix) + if (secureEnclaveAlias != null) { + logd(TAG, "Found Secure Enclave alias mapping: $secureEnclaveAlias") + // Verify the key exists in Android Keystore + val keyStore = KeyStore.getInstance("AndroidKeyStore") + keyStore.load(null) + if (keyStore.containsAlias(secureEnclaveAlias)) { + logd(TAG, "Secure Enclave key exists in Android Keystore") + throw HardwareBackedKeyException( + secureEnclaveAlias, + "Secure Enclave hardware-backed key requires AndroidKeystoreCryptoProvider", + prefix + ) + } else { + loge(TAG, "Secure Enclave alias exists but key not found in Keystore!") + } + } + + // Try the new Flow-Wallet-Kit storage pattern (for mnemonic-based accounts) val newKeyId = "$NEW_STORAGE_KEY_PREFIX$prefix" val newStorageKey = tryGetFromNewStorage(newKeyId, prefix, storage) if (newStorageKey != null) { @@ -43,7 +66,7 @@ object KeyCompatibilityManager { logd(TAG, "Key not found in new storage, trying old Android Keystore pattern") - // Fallback to old Android Keystore pattern + // Fallback to old Android Keystore pattern (legacy accounts) val oldStorageKey = tryGetFromOldKeystore(prefix, storage) if (oldStorageKey != null) { logd(TAG, "Successfully retrieved key from old Android Keystore") @@ -51,7 +74,7 @@ object KeyCompatibilityManager { return oldStorageKey } - loge(TAG, "Private key not found in either new storage or old Android Keystore") + loge(TAG, "Private key not found in any storage: new storage, Secure Enclave, or old Android Keystore") return null } diff --git a/app/src/main/java/com/flowfoundation/wallet/network/UserRegisterUtils.kt b/app/src/main/java/com/flowfoundation/wallet/network/UserRegisterUtils.kt index 5539254d7..5877f89e3 100644 --- a/app/src/main/java/com/flowfoundation/wallet/network/UserRegisterUtils.kt +++ b/app/src/main/java/com/flowfoundation/wallet/network/UserRegisterUtils.kt @@ -1,9 +1,10 @@ package com.flowfoundation.wallet.network +// Removed: import com.flowfoundation.wallet.wallet.createWalletFromServer - was causing duplicate account creation +import android.security.keystore.KeyGenParameterSpec +import android.security.keystore.KeyProperties import android.webkit.WebStorage import android.widget.Toast -import com.flow.wallet.crypto.BIP39 -import com.flow.wallet.keys.PrivateKey import com.flow.wallet.storage.FileSystemStorage import com.flow.wallet.wallet.WalletFactory import com.flowfoundation.wallet.R @@ -45,14 +46,11 @@ import com.flowfoundation.wallet.utils.error.WalletError import com.flowfoundation.wallet.utils.ioScope import com.flowfoundation.wallet.utils.logd import com.flowfoundation.wallet.utils.loge -import com.flowfoundation.wallet.utils.readWalletPassword import com.flowfoundation.wallet.utils.setMeowDomainClaimed import com.flowfoundation.wallet.utils.setRegistered -import com.flowfoundation.wallet.utils.storeWalletPassword import com.flowfoundation.wallet.utils.toast import com.flowfoundation.wallet.utils.updateChainNetworkPreference import com.flowfoundation.wallet.wallet.Wallet -// Removed: import com.flowfoundation.wallet.wallet.createWalletFromServer - was causing duplicate account creation import com.google.firebase.auth.ktx.auth import com.google.firebase.ktx.Firebase import com.google.firebase.messaging.FirebaseMessaging @@ -60,9 +58,11 @@ import com.google.gson.Gson import com.google.gson.reflect.TypeToken import kotlinx.coroutines.delay import org.onflow.flow.ChainId -import org.onflow.flow.models.SigningAlgorithm import java.io.File +import java.security.KeyPair +import java.security.KeyPairGenerator import java.security.MessageDigest +import java.security.spec.ECGenParameterSpec import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine @@ -194,6 +194,11 @@ suspend fun initWalletWithTxId( } logd(TAG, "[InitWallet] Using prefix from pending registration: $prefix") + // Check if this is a hardware-backed key (Secure Enclave) + val keystoreAlias = getKeystoreAliasForPrefix(prefix) + val isHardwareBackedKey = keystoreAlias != null + logd(TAG, "[InitWallet] Is hardware-backed key: $isHardwareBackedKey, alias: $keystoreAlias") + // Fetch account by txId using Wallet SDK val chainId = when (chainNetWorkString()) { "mainnet" -> ChainId.Mainnet @@ -201,23 +206,35 @@ suspend fun initWalletWithTxId( else -> ChainId.Mainnet } - val storage = FileSystemStorage(File(Env.getApp().filesDir, "wallet")) - val keyForWalletSDK = KeyCompatibilityManager.getPrivateKeyWithFallback(prefix, storage) - if (keyForWalletSDK == null) { - loge(TAG, "[InitWallet] Failed to retrieve stored private key") - continuation.resume(Pair(false, null)) - return@ioScope - } + // For hardware-backed keys, we use a different approach since we can't extract the private key + // We need to fetch the account info directly from the blockchain using the txId + val createdAddress: String + if (isHardwareBackedKey) { + logd(TAG, "[InitWallet] Using hardware-backed key - fetching account via transaction result") + // For hardware-backed keys, we fetch the account address from the transaction result + // The account was already created on-chain, we just need to get the address + createdAddress = fetchAddressFromTransaction(txId, chainId) + logd(TAG, "[InitWallet] Fetched address from transaction: $createdAddress") + } else { + // For software keys, use the existing Flow-Wallet-Kit approach + val storage = FileSystemStorage(File(Env.getApp().filesDir, "wallet")) + val keyForWalletSDK = KeyCompatibilityManager.getPrivateKeyWithFallback(prefix, storage) + if (keyForWalletSDK == null) { + loge(TAG, "[InitWallet] Failed to retrieve stored private key") + continuation.resume(Pair(false, null)) + return@ioScope + } - val walletForSDK = WalletFactory.createKeyWallet( - keyForWalletSDK, - setOf(ChainId.Mainnet, ChainId.Testnet), - storage - ) + val walletForSDK = WalletFactory.createKeyWallet( + keyForWalletSDK, + setOf(ChainId.Mainnet, ChainId.Testnet), + storage + ) - logd(TAG, "[InitWallet] Fetching account by txId: $txId") - val fetchedAccount = walletForSDK.fetchAccountByCreationTxId(txId, chainId) - val createdAddress = fetchedAccount.address + logd(TAG, "[InitWallet] Fetching account by txId: $txId") + val fetchedAccount = walletForSDK.fetchAccountByCreationTxId(txId, chainId) + createdAddress = fetchedAccount.address + } logd(TAG, "[InitWallet] Account fetched successfully at address: $createdAddress") @@ -549,47 +566,26 @@ private fun registerFirebase(user: RegisterResponse, callback: (isSuccess: Boole } private suspend fun registerServer(username: String, prefix: String): RegisterResponse { - logd(TAG, "Starting server registration for username: $username") + logd(TAG, "Starting server registration for username: $username (Secure Enclave / Hardware-backed)") val deviceInfoRequest = DeviceInfoManager.getDeviceInfoRequest() val service = retrofit().create(ApiService::class.java) - val baseDir = File(Env.getApp().filesDir, "wallet") - val storage = FileSystemStorage(baseDir) try { - // Generate and store mnemonic globally for potential future EOA support - val mnemonic = BIP39.generate(BIP39.SeedPhraseLength.TWELVE) - logd(TAG, "Generated new 12-word mnemonic for backup support") - - val passwordMap = try { - val pref = readWalletPassword() - if (pref.isBlank()) { - HashMap() - } else { - Gson().fromJson(pref, object : TypeToken>() {}.type) - } - } catch (e: Exception) { - HashMap() - } + // For Secure Enclave / Hardware-backed accounts: + // - Generate key directly in Android Keystore (hardware-backed, non-extractable) + // - Do NOT generate a mnemonic (hardware keys cannot be derived from seed phrases) + // - The key stays in hardware and can only be used for signing, never exported - // Store mnemonic globally (available for future EOA enablement if user chooses) - storeWalletPassword(Gson().toJson(passwordMap.apply { put("global", mnemonic) })) - logd(TAG, "Stored mnemonic globally for backup support") + val keystoreAlias = "secure_enclave_$prefix" + logd(TAG, "Generating hardware-backed key in Android Keystore with alias: $keystoreAlias") - // Create a new private key - val privateKey = PrivateKey.create(storage) - logd(TAG, "Created new private key for registration") + // Generate EC key pair directly in Android Keystore + val keyPair = generateHardwareBackedKeyPair(keystoreAlias) + logd(TAG, "Successfully generated hardware-backed key pair") - // Store the private key with prefix as ID for later retrieval - val keyId = "prefix_key_$prefix" - privateKey.store(keyId, prefix) // Use prefix as password for simplicity - logd(TAG, "Stored private key with ID: $keyId") - - // Get the uncompressed public key using the fixed Flow-Wallet-Kit method - val publicKeyBytes = privateKey.publicKey(SigningAlgorithm.ECDSA_P256) - if (publicKeyBytes == null) { - logd(TAG, "Failed to get public key from private key") - throw IllegalStateException("Failed to get public key from private key") - } + // Extract public key from the generated key pair + val publicKey = keyPair.public + val publicKeyBytes = extractECPublicKeyBytes(publicKey) logd(TAG, "Public key size: ${publicKeyBytes.size} bytes") @@ -603,6 +599,10 @@ private suspend fun registerServer(username: String, prefix: String): RegisterRe } logd(TAG, "Formatted public key: $hexPublicKey (${hexPublicKey.length} chars)") + // Store the keystore alias in preferences so CryptoProviderManager can find it + storeKeystoreAlias(prefix, keystoreAlias) + logd(TAG, "Stored keystore alias mapping: prefix=$prefix -> alias=$keystoreAlias") + // Create registration request with correct algorithm parameters val request = RegisterRequest( username = username, @@ -636,6 +636,102 @@ private suspend fun registerServer(username: String, prefix: String): RegisterRe } } +/** + * Generate a hardware-backed EC key pair in Android Keystore. + * The private key never leaves the secure hardware (TEE/SE). + */ +private fun generateHardwareBackedKeyPair(keystoreAlias: String): KeyPair { + val keyPairGenerator = KeyPairGenerator.getInstance( + KeyProperties.KEY_ALGORITHM_EC, + "AndroidKeyStore" + ) + + val parameterSpec = KeyGenParameterSpec.Builder( + keystoreAlias, + KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY + ) + .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) // P-256 curve + .setDigests(KeyProperties.DIGEST_SHA256) + .setUserAuthenticationRequired(false) // Can be set to true for biometric protection + .build() + + keyPairGenerator.initialize(parameterSpec) + return keyPairGenerator.generateKeyPair() +} + +/** + * Extract the raw EC public key bytes from a Java PublicKey. + * Returns uncompressed format (04 || x || y) for 65 bytes total. + */ +private fun extractECPublicKeyBytes(publicKey: java.security.PublicKey): ByteArray { + val encoded = publicKey.encoded + + // The encoded form is X.509 SubjectPublicKeyInfo format + // For EC keys, we need to extract the actual point data + // The last 65 bytes contain: 04 (uncompressed indicator) + 32 bytes X + 32 bytes Y + + return if (encoded.size >= 65) { + // Look for the uncompressed point marker (0x04) + val startIndex = encoded.indexOfFirst { it == 0x04.toByte() } + if (startIndex != -1 && startIndex + 65 <= encoded.size) { + encoded.copyOfRange(startIndex, startIndex + 65) + } else { + // Fallback: take last 65 bytes + encoded.takeLast(65).toByteArray() + } + } else { + encoded + } +} + +/** + * Store the mapping from prefix to keystore alias. + * This allows CryptoProviderManager to find the hardware-backed key later. + */ +private fun storeKeystoreAlias(prefix: String, keystoreAlias: String) { + val aliasMap = try { + val pref = readKeystoreAliasPreference() + if (pref.isBlank()) { + HashMap() + } else { + Gson().fromJson(pref, object : TypeToken>() {}.type) + } + } catch (e: Exception) { + HashMap() + } + + aliasMap[prefix] = keystoreAlias + saveKeystoreAliasPreference(Gson().toJson(aliasMap)) +} + +/** + * Get the keystore alias for a given prefix. + * Returns null if not found (account may use software key instead). + */ +fun getKeystoreAliasForPrefix(prefix: String): String? { + return try { + val pref = readKeystoreAliasPreference() + if (pref.isBlank()) { + null + } else { + val aliasMap: HashMap = Gson().fromJson(pref, object : TypeToken>() {}.type) + aliasMap[prefix] + } + } catch (e: Exception) { + null + } +} + +private fun readKeystoreAliasPreference(): String { + val prefs = Env.getApp().getSharedPreferences("keystore_aliases", android.content.Context.MODE_PRIVATE) + return prefs.getString("alias_map", "") ?: "" +} + +private fun saveKeystoreAliasPreference(json: String) { + val prefs = Env.getApp().getSharedPreferences("keystore_aliases", android.content.Context.MODE_PRIVATE) + prefs.edit().putString("alias_map", json).apply() +} + fun generatePrefix(text: String): String { val timestamp = System.currentTimeMillis().toString() val combinedInput = "${text}_$timestamp" @@ -711,3 +807,77 @@ suspend fun clearUserCache() { fun clearWebViewCache() { WebStorage.getInstance().deleteAllData() } + +/** + * Fetch the created account address from a transaction result. + * This is used for hardware-backed keys where we can't use the Wallet SDK's fetchAccountByCreationTxId. + * + * For hardware-backed keys, we can't use the Wallet SDK approach because: + * 1. The Wallet SDK requires a PrivateKey to create a wallet instance + * 2. Hardware-backed keys cannot be extracted from Android Keystore + * + * Instead, we wait for the transaction to seal and then fetch the address from the backend. + */ +private suspend fun fetchAddressFromTransaction(txId: String, chainId: ChainId): String { + logd(TAG, "[fetchAddressFromTransaction] Fetching transaction result for txId: $txId on chainId: $chainId") + + try { + // Wait for transaction to seal using FlowCadenceApi + logd(TAG, "[fetchAddressFromTransaction] Waiting for transaction to seal...") + val txResult = com.flowfoundation.wallet.manager.flow.FlowCadenceApi.waitForSeal(txId) + logd(TAG, "[fetchAddressFromTransaction] Transaction sealed, status: ${txResult.status}") + + // Try to extract address from transaction events + txResult.events.forEach { event -> + logd(TAG, "[fetchAddressFromTransaction] Event type: ${event.type}") + if (event.type.contains("AccountCreated") || event.type.contains("flow.AccountCreated")) { + // Parse the address from the event payload + val eventPayload = event.payload.toString() + logd(TAG, "[fetchAddressFromTransaction] Found AccountCreated event: $eventPayload") + + // Extract address from the event (format varies, but typically contains the address) + val addressMatch = Regex("0x[a-fA-F0-9]{16}").find(eventPayload) + if (addressMatch != null) { + val address = addressMatch.value + logd(TAG, "[fetchAddressFromTransaction] Extracted address from event: $address") + return address + } + } + } + + logd(TAG, "[fetchAddressFromTransaction] Could not extract address from events, falling back to API") + } catch (e: Exception) { + loge(TAG, "[fetchAddressFromTransaction] Error waiting for transaction: ${e.message}") + } + + // Fallback: fetch from backend API + // The backend should have the wallet address by the time the transaction is sealed + logd(TAG, "[fetchAddressFromTransaction] Falling back to backend API to get address") + + // Poll backend a few times to allow for propagation delay + var attempts = 0 + val maxAttempts = 10 + while (attempts < maxAttempts) { + try { + val service = retrofit().create(ApiService::class.java) + val walletList = service.getWalletList().data + val address = walletList?.wallets?.firstOrNull()?.blockchain?.firstOrNull()?.address + + if (!address.isNullOrBlank()) { + val formattedAddress = if (address.startsWith("0x")) address else "0x$address" + logd(TAG, "[fetchAddressFromTransaction] Got address from backend: $formattedAddress") + return formattedAddress + } + + logd(TAG, "[fetchAddressFromTransaction] Backend returned empty address, retrying... (${attempts + 1}/$maxAttempts)") + attempts++ + delay(1000) + } catch (e: Exception) { + loge(TAG, "[fetchAddressFromTransaction] Backend API error: ${e.message}, retrying... (${attempts + 1}/$maxAttempts)") + attempts++ + delay(1000) + } + } + + throw IllegalStateException("Could not determine created account address after $maxAttempts attempts") +} From d13b0b47bb81a2bff47b7993a7f1e2348e0834b8 Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Wed, 17 Dec 2025 18:58:32 +0900 Subject: [PATCH 2/3] fix: debugging key logic for secure enclave --- .../com/flowfoundation/wallet/network/UserRegisterUtils.kt | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/flowfoundation/wallet/network/UserRegisterUtils.kt b/app/src/main/java/com/flowfoundation/wallet/network/UserRegisterUtils.kt index 5877f89e3..95c59a529 100644 --- a/app/src/main/java/com/flowfoundation/wallet/network/UserRegisterUtils.kt +++ b/app/src/main/java/com/flowfoundation/wallet/network/UserRegisterUtils.kt @@ -1,10 +1,11 @@ package com.flowfoundation.wallet.network -// Removed: import com.flowfoundation.wallet.wallet.createWalletFromServer - was causing duplicate account creation import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import android.webkit.WebStorage import android.widget.Toast +import com.flow.wallet.crypto.BIP39 +import com.flow.wallet.keys.PrivateKey import com.flow.wallet.storage.FileSystemStorage import com.flow.wallet.wallet.WalletFactory import com.flowfoundation.wallet.R @@ -46,8 +47,10 @@ import com.flowfoundation.wallet.utils.error.WalletError import com.flowfoundation.wallet.utils.ioScope import com.flowfoundation.wallet.utils.logd import com.flowfoundation.wallet.utils.loge +import com.flowfoundation.wallet.utils.readWalletPassword import com.flowfoundation.wallet.utils.setMeowDomainClaimed import com.flowfoundation.wallet.utils.setRegistered +import com.flowfoundation.wallet.utils.storeWalletPassword import com.flowfoundation.wallet.utils.toast import com.flowfoundation.wallet.utils.updateChainNetworkPreference import com.flowfoundation.wallet.wallet.Wallet @@ -58,9 +61,11 @@ import com.google.gson.Gson import com.google.gson.reflect.TypeToken import kotlinx.coroutines.delay import org.onflow.flow.ChainId +import org.onflow.flow.models.SigningAlgorithm import java.io.File import java.security.KeyPair import java.security.KeyPairGenerator +import java.security.KeyStore import java.security.MessageDigest import java.security.spec.ECGenParameterSpec import kotlin.coroutines.resume From 34a52822ca45452b99c5a10815f2219a94aa26a5 Mon Sep 17 00:00:00 2001 From: Lea Lobanov Date: Wed, 17 Dec 2025 18:59:10 +0900 Subject: [PATCH 3/3] fix: debugging key logic for secure enclave --- .../com/flowfoundation/wallet/network/UserRegisterUtils.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/src/main/java/com/flowfoundation/wallet/network/UserRegisterUtils.kt b/app/src/main/java/com/flowfoundation/wallet/network/UserRegisterUtils.kt index 95c59a529..405102dc5 100644 --- a/app/src/main/java/com/flowfoundation/wallet/network/UserRegisterUtils.kt +++ b/app/src/main/java/com/flowfoundation/wallet/network/UserRegisterUtils.kt @@ -4,8 +4,6 @@ import android.security.keystore.KeyGenParameterSpec import android.security.keystore.KeyProperties import android.webkit.WebStorage import android.widget.Toast -import com.flow.wallet.crypto.BIP39 -import com.flow.wallet.keys.PrivateKey import com.flow.wallet.storage.FileSystemStorage import com.flow.wallet.wallet.WalletFactory import com.flowfoundation.wallet.R @@ -47,10 +45,8 @@ import com.flowfoundation.wallet.utils.error.WalletError import com.flowfoundation.wallet.utils.ioScope import com.flowfoundation.wallet.utils.logd import com.flowfoundation.wallet.utils.loge -import com.flowfoundation.wallet.utils.readWalletPassword import com.flowfoundation.wallet.utils.setMeowDomainClaimed import com.flowfoundation.wallet.utils.setRegistered -import com.flowfoundation.wallet.utils.storeWalletPassword import com.flowfoundation.wallet.utils.toast import com.flowfoundation.wallet.utils.updateChainNetworkPreference import com.flowfoundation.wallet.wallet.Wallet @@ -61,11 +57,9 @@ import com.google.gson.Gson import com.google.gson.reflect.TypeToken import kotlinx.coroutines.delay import org.onflow.flow.ChainId -import org.onflow.flow.models.SigningAlgorithm import java.io.File import java.security.KeyPair import java.security.KeyPairGenerator -import java.security.KeyStore import java.security.MessageDigest import java.security.spec.ECGenParameterSpec import kotlin.coroutines.resume