Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Android/wallet/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,43 @@
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
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,
Expand Down Expand Up @@ -91,4 +119,46 @@ class CryptoProviderKey(
override fun allKeys(): List<String> {
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
}
}
16 changes: 16 additions & 0 deletions Android/wallet/src/main/java/com/flow/wallet/keys/SeedPhraseKey.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<KeyData>(keyDataStr)
SeedPhraseKey(keyData.mnemonic, keyData.passphrase, keyData.path, storage, keyData.length)
} catch (e: Exception) { null }
}
}

private val hdWallet: HDWallet = try {
Expand Down
Loading