From 4e8c18601d301659eb3324c53f618ece98006f28 Mon Sep 17 00:00:00 2001 From: Meng Date: Tue, 3 Mar 2026 08:40:08 +0800 Subject: [PATCH] feat(keys): add SeedPhraseKey.load() and CryptoProviderKey identifier storage - SeedPhraseKey: add static Companion.load(id, password, storage) factory method that decrypts and reconstructs a SeedPhraseKey without requiring an existing instance, enabling key recovery independent of Account cache - CryptoProviderKey: add companion object with three static helpers for persisting an opaque provider identifier (e.g. Android Keystore prefix) independently of the Account cache: saveProviderIdentifier(id, identifier, password, storage) getProviderIdentifier(id, password, storage): String? hasProviderIdentifier(id, storage): Boolean Identifier is encrypted with ChaCha20-Poly1305 via ChaChaPolyCipher. Removes AndroidKeystoreKey (superseded by CryptoProviderKey companion). - settings.gradle: bump Android Gradle Plugin to 8.8.2 Co-Authored-By: Claude Sonnet 4.6 --- Android/wallet/settings.gradle | 2 +- .../com/flow/wallet/keys/CryptoProviderKey.kt | 74 ++++++++++++++++++- .../com/flow/wallet/keys/SeedPhraseKey.kt | 16 ++++ 3 files changed, 89 insertions(+), 3 deletions(-) diff --git a/Android/wallet/settings.gradle b/Android/wallet/settings.gradle index 747727a..c2bd07c 100644 --- a/Android/wallet/settings.gradle +++ b/Android/wallet/settings.gradle @@ -5,7 +5,7 @@ pluginManagement { mavenCentral() } plugins { - id("com.android.library") version "8.6.1" + id("com.android.library") version "8.8.2" id("org.jetbrains.kotlin.android") version "2.1.20" id("org.jetbrains.kotlin.plugin.serialization") version "2.1.20" } diff --git a/Android/wallet/src/main/java/com/flow/wallet/keys/CryptoProviderKey.kt b/Android/wallet/src/main/java/com/flow/wallet/keys/CryptoProviderKey.kt index d7d9fe5..571beeb 100644 --- a/Android/wallet/src/main/java/com/flow/wallet/keys/CryptoProviderKey.kt +++ b/Android/wallet/src/main/java/com/flow/wallet/keys/CryptoProviderKey.kt @@ -1,6 +1,7 @@ package com.flow.wallet.keys import com.flow.wallet.CryptoProvider +import com.flow.wallet.crypto.ChaChaPolyCipher import com.flow.wallet.errors.WalletError import com.flow.wallet.storage.StorageProtocol import org.onflow.flow.models.HashingAlgorithm @@ -8,8 +9,35 @@ import org.onflow.flow.models.SigningAlgorithm import org.onflow.flow.models.hexToBytes /** - * Adapter class that wraps a CryptoProvider as a KeyProtocol. - * Useful for ProxyWallet to expose key information without managing the key itself. + * [KeyProtocol] adapter that wraps an externally-managed [CryptoProvider]. + * + * ### What is a "CryptoProvider-backed key"? + * Some signing backends (Android Keystore, Secure Enclave, HSM, custom signers…) are not + * natively representable as a raw key material object. They are instead accessed through a + * [CryptoProvider] interface that exposes publicKey / signData / hash & sign algorithm info. + * [CryptoProviderKey] lets such a provider participate in the [KeyProtocol] ecosystem by + * delegating all cryptographic operations to the injected provider. + * + * ### Provider-identifier storage (companion) + * In order to reconstruct a [CryptoProvider] after the app is killed or the account cache is + * lost, the caller needs to persist some *opaque identifier* (e.g. an Android Keystore alias, + * a key prefix string, a slot number …). The companion object offers three static helpers for + * this purpose: + * - [saveProviderIdentifier] – encrypt and store the identifier string + * - [getProviderIdentifier] – retrieve it later + * - [hasProviderIdentifier] – existence check without decryption + * + * ### Lifecycle (Android Keystore example) + * ``` + * // 1. Registration / import – persist the key identifier once: + * CryptoProviderKey.saveProviderIdentifier(uid, keystorePrefix, password, akpStorage) + * + * // 2. Wallet creation – reconstruct provider from the stored identifier and create wallet: + * val prefix = CryptoProviderKey.getProviderIdentifier(uid, password, akpStorage) ?: return + * val provider = AndroidKeystoreCryptoProvider(prefix) // app-layer concrete type + * val wallet = WalletFactory.createProxyWallet(provider, setOf(Mainnet, Testnet), storage) + * ``` + * The same pattern applies to any other [CryptoProvider] implementation. */ class CryptoProviderKey( private val cryptoProvider: CryptoProvider, @@ -91,4 +119,46 @@ class CryptoProviderKey( override fun allKeys(): List { return emptyList() } + + companion object { + /** + * Encrypts [identifier] with [password] and stores it under [id] in [storage]. + * + * [identifier] is an opaque string that identifies the backing [CryptoProvider] — + * e.g. an Android Keystore alias/prefix, a slot number, a remote key ID, etc. + * Storing it here allows the provider to be reconstructed after an account-cache loss. + */ + fun saveProviderIdentifier( + id: String, + identifier: String, + password: String, + storage: StorageProtocol + ) { + val cipher = ChaChaPolyCipher(password) + storage.set(id, cipher.encrypt(identifier.toByteArray(Charsets.UTF_8))) + } + + /** + * Retrieves a previously stored provider identifier. + * + * After obtaining the identifier, reconstruct the [CryptoProvider] at the app layer + * and create a wallet: + * ``` + * val prefix = getProviderIdentifier(uid, password, storage) ?: return + * val provider = AndroidKeystoreCryptoProvider(prefix) + * WalletFactory.createProxyWallet(provider, chains, storage) + * ``` + * Returns null if nothing is stored under [id] or if decryption fails. + */ + fun getProviderIdentifier(id: String, password: String, storage: StorageProtocol): String? { + val data = storage.get(id) ?: return null + return try { + String(ChaChaPolyCipher(password).decrypt(data), Charsets.UTF_8) + } catch (e: Exception) { null } + } + + /** Lightweight existence check – does NOT decrypt the stored data. */ + fun hasProviderIdentifier(id: String, storage: StorageProtocol): Boolean = + storage.get(id) != null + } } \ No newline at end of file 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..3a88782 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 @@ -59,6 +59,22 @@ class SeedPhraseKey( } .toIntArray() } + + /** + * Static factory that loads a [SeedPhraseKey] from [storage] without needing an + * existing instance. Mirrors the instance [get] method but is callable anywhere. + * + * Returns null instead of throwing so callers can treat a missing key as absent. + */ + fun load(id: String, password: String, storage: StorageProtocol): SeedPhraseKey? { + val encryptedData = storage.get(id) ?: return null + return try { + val cipher = ChaChaPolyCipher(password) + val keyDataStr = String(cipher.decrypt(encryptedData), Charsets.UTF_8) + val keyData = Json.decodeFromString(keyDataStr) + SeedPhraseKey(keyData.mnemonic, keyData.passphrase, keyData.path, storage, keyData.length) + } catch (e: Exception) { null } + } } private val hdWallet: HDWallet = try {