Skip to content

feat(key-storage): add independent key storage decoupled from Account#2828

Closed
jaymengxy wants to merge 4 commits into
devfrom
fix_key_storage
Closed

feat(key-storage): add independent key storage decoupled from Account#2828
jaymengxy wants to merge 4 commits into
devfrom
fix_key_storage

Conversation

@jaymengxy
Copy link
Copy Markdown
Contributor

Summary

  • Add KeyStorageManager and KeyStorageMigration to store mnemonic / private key / Android Keystore prefix independently of the Account object
  • Update WalletCreationHelper, CryptoProviderManager, AuthBridgeHandler, NativeFRWBridge, KeyStoreRestoreViewModel to write to / read from new storage
  • Add buildLocalKeyAccounts() in AccountManager to surface recoverable accounts whose key survives but Account cache was lost
  • Bump flow-wallet-android 0.2.1 → 0.2.4 (picks up SeedPhraseKey.load() and CryptoProviderKey identifier storage helpers)
  • Fix: onboarding issues, account list COA display, switch current account set

Test Plan

  • New wallet creation stores key in frw_sp_storage/ and survives app-cache clear
  • Private key import stores key in frw_pk_storage/ and survives app-cache clear
  • Android Keystore prefix stored in frw_akp_storage/ and wallet recreates correctly
  • Migration runs once on first launch with existing accounts and is idempotent
  • Orphaned-key accounts appear in account switcher after clearing Account cache
  • Switch via LocalSwitchAccount completes login and re-adds account normally

🤖 Generated with Claude Code

@jaymengxy jaymengxy requested a review from a team as a code owner March 3, 2026 02:08
@jaymengxy
Copy link
Copy Markdown
Contributor Author

Key Independent Storage & Account Decoupling — Implementation Notes

Background & Goals

Problem: Key material (mnemonic, private key, Android Keystore prefix) was previously serialized inside the Account object. A corrupted or missing AccountCacheManager cache made keys inaccessible, forcing users to re-import their wallets.

Goal: Decouple all three key types from Account and store them independently with encryption. If the account cache is lost, keys survive and can be used to restore the account locally without re-entering a mnemonic.


Physical Storage Layout

filesDir/
├── frw_sp_storage/          ← SeedPhraseKey (HD wallet / mnemonic)
│   └── {uid}                  key = Firebase UID, value = ChaCha20-encrypted JSON
├── frw_pk_storage/          ← PrivateKey (raw key import)
│   └── {uid}                  key = Firebase UID, value = ChaCha20-encrypted private key
└── frw_akp_storage/         ← CryptoProvider identifier (Android Keystore prefix)
    └── {uid}                  key = Firebase UID, value = ChaCha20-encrypted prefix string

EncryptedSharedPreferences:
└── frw_key_master_password  ← Random 32-byte master password (protected by AES-256-GCM)

Each key type occupies its own dedicated directory — mirroring the original design of one SharedPreferences file per type. The encryption key is a random hex string generated once at startup and stored in EncryptedSharedPreferences.


Architecture

┌─────────────────────────────────────────────────────────────────────┐
│                        App Layer (FRW Android)                      │
│                                                                     │
│  ┌──────────────────────┐     ┌───────────────────────────────────┐ │
│  │   AccountManager     │     │  WalletCreationHelper /           │ │
│  │                      │     │  CryptoProviderManager            │ │
│  │  init()              │     │                                   │ │
│  │   └─ Migration       │     │  1. getSeedPhraseKey(uid)         │ │
│  │                      │     │  2. getPrivateKeyObject(uid)      │ │
│  │  getSwitchAccountList│     │  3. getAndroidKeystorePrefix(uid) │ │
│  │   └─ buildLocalKey   │     │                                   │ │
│  │      Accounts()      │     │  → WalletFactory.createKeyWallet  │ │
│  └──────────────────────┘     │    / createProxyWallet            │ │
│              │                └────────────────┬──────────────────┘ │
│              └────────────────────────────┐    │                    │
│                                           ▼    ▼                    │
│              ┌────────────────────────────────────────────────────┐ │
│              │              KeyStorageManager                      │ │
│              │                                                     │ │
│              │  spStorage  → FileSystemStorage(frw_sp_storage/)   │ │
│              │  pkStorage  → FileSystemStorage(frw_pk_storage/)   │ │
│              │  akpStorage → FileSystemStorage(frw_akp_storage/)  │ │
│              │  masterPwd  ← EncryptedSharedPreferences           │ │
│              └──────────┬──────────────┬──────────────┬───────────┘ │
└─────────────────────────┼──────────────┼──────────────┼─────────────┘
                          │              │              │
┌─────────────────────────┼──────────────┼──────────────┼─────────────┐
│   FlowWalletKit          │              │              │             │
│              ┌───────────▼────┐ ┌───────▼────┐ ┌──────▼──────────┐ │
│              │ SeedPhraseKey  │ │ PrivateKey │ │CryptoProviderKey│ │
│              │                │ │            │ │                 │ │
│              │ store(id, pwd) │ │ store()    │ │ companion:      │ │
│              │ Companion      │ │ get()      │ │  saveProvider   │ │
│              │  .load()       │ │ restore()  │ │  Identifier()   │ │
│              │                │ │            │ │  getProvider    │ │
│              │ → createKey    │ │ → createKey│ │  Identifier()   │ │
│              │   Wallet       │ │   Wallet   │ │  hasProvider    │ │
│              └────────────────┘ └────────────┘ │  Identifier()   │ │
│                                                │                 │ │
│                                                │ → AndroidKeystore│ │
│                                                │   CryptoProvider│ │
│                                                │ → createProxy   │ │
│                                                │   Wallet        │ │
│                                                └─────────────────┘ │
└───────────────────────────────────────────────────────────────────┘

Key Write Points

Trigger Location Key Type Written
New HD wallet (mnemonic) AuthBridgeHandler.saveMnemonic SeedPhrase
Keystore import (mnemonic) KeyStoreRestoreViewModel SeedPhrase
Keystore import (private key) KeyStoreRestoreViewModel PrivateKey
New account (Android Keystore) AccountManager.add() AK Prefix
RN Bridge new key NativeFRWBridge.saveNewKey SeedPhrase / PrivateKey
Startup migration (existing data) KeyStorageMigration.runMigrationIfNeeded() All three types

Key Read & Wallet Creation Flow

WalletCreationHelper.createWalletFromAccount(account)
│
├─ 1. KeyStorageManager.getSeedPhraseKey(uid)        → SeedPhraseKey
│       └─ WalletFactory.createKeyWallet(key, chains, storage)
│
├─ 2. KeyStorageManager.getPrivateKeyObject(uid)     → PrivateKey
│       └─ WalletFactory.createKeyWallet(key, chains, storage)
│
├─ 3. KeyStorageManager.getAndroidKeystorePrefix(uid) → prefix: String
│       └─ AndroidKeystoreCryptoProvider(prefix)
│           └─ WalletFactory.createProxyWallet(provider, chains, storage)
│
└─ 4. Fallback: Account.keyStoreInfo / Account.prefix / mnemonic (legacy path)

Orphaned Key Recovery

When the account cache is lost but keys survive, buildLocalKeyAccounts() auto-detects recoverable accounts:

knownUids  = accounts.mapNotNull { it.wallet?.id }
allKeyUids = getAllSeedPhraseUids ∪ getAllPrivateKeyUids ∪ getAllAndroidKeystoreUids
orphanUids = allKeyUids − knownUids

→ List<LocalSwitchAccount>(username=uid, userId=uid, prefix=akPrefix?)

These entries appear in getSwitchAccountList() alongside normal accounts. Selecting one triggers switch(LocalSwitchAccount) which reconstructs the CryptoProvider, calls the backend login API, and restores the full Account.


Migration Strategy

KeyStorageMigration.runMigrationIfNeeded()
  Trigger: inside AccountManager.init() ioScope, after accounts list is loaded

  For each Account in AccountManager.list():
    has prefix            → saveAndroidKeystorePrefix(uid, prefix)      [idempotent]
    keyStoreInfo+mnemonic → decrypt → saveSeedPhrase(uid, mnemonic)     [idempotent]
    keyStoreInfo+privkey  → savePrivateKey(uid, privateKeyHex)          [idempotent]
    no above fields       → getHDWalletMnemonicByUID → saveSeedPhrase   [idempotent]

  All operations skip if already stored — safe to run multiple times.

FWK Dependency Changes (flow-wallet-android 0.2.1 → 0.2.4)

Addition Description
SeedPhraseKey.Companion.load() Static factory — decrypts and reconstructs a SeedPhraseKey from storage without needing an existing instance
CryptoProviderKey.saveProviderIdentifier() Encrypt and persist an opaque provider identifier
CryptoProviderKey.getProviderIdentifier() Retrieve and decrypt a stored provider identifier
CryptoProviderKey.hasProviderIdentifier() Lightweight existence check (no decryption)
AndroidKeystoreKey Removed — superseded by CryptoProviderKey.companion

Related FWK PR: onflow/Flow-Wallet-Kit#83

@jaymengxy jaymengxy closed this Mar 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant