@Secure-Legion Thank you for the detailed response. Unfortunately, several of your rebuttals contain technical inaccuracies that misrepresent the actual risk. Let me address each one directly.
Rebuttal 1: "Hex strings in EncryptedSharedPreferences are encrypted at rest"
Your claim: The hex encoding is just serialization; the ciphertext is what persists.
The reality you are ignoring: The problem is not whether the data is encrypted at rest. The problem is that every operational private key is materialized as a hex string in application memory and then placed into a software-backed storage container.
EncryptedSharedPreferences uses a MasterKey that is not setIsStrongBoxBacked(true). On the majority of Android devices in the wild, this MasterKey is derived inside the TEE but its key material is software-backed — meaning it can be extracted via known Keystore vulnerabilities (CVE-2022-20007, CVE-2023-21237, and others) or forensic tools like Frida/ objection.
Once that MasterKey is extracted, the attacker has every private key in one shot — Ed25519, X25519, Kyber-1024, Tor HS, voice service. That is not "an implementation detail." That is a single point of total failure.
Your comparison to "Google apps" is a red herring. Google apps do not store cryptocurrency private keys and post-quantum encryption keys in the same storage bucket.
Rebuttal 2: "MasterKey automatically uses hardware-backed storage"
Your claim: AES256_GCM automatically uses TEE/StrongBox.
Factually incorrect. MasterKey.Builder with .setKeyScheme(AES256_GCM) does not set setIsStrongBoxBacked(true). The Android documentation explicitly states:
"If StrongBox is not available, the key will be generated in TEE."
TEE is not equivalent to StrongBox. TEE keys have been extracted in the wild via:
- Qualcomm TEE vulnerabilities (CVE-2022-22071)
- Samsung Knox TEE bypasses
- Kernel-level attacks that compromise the TEE boundary
StrongBox (dedicated HSM) is the only Android Keystore security level that resists physical extraction. You chose not to enforce it. That is a security downgrade, not a compatibility feature.
Your claim that forcing StrongBox would "brick the app" is also incorrect. You can attempt StrongBox and gracefully fall back to TEE while still enforcing setUserAuthenticationRequired(true) — which you also omitted.
Rebuttal 3: "BIP39 wrapping is standard key hierarchy"
Your claim: This is identical to MetaMask and Trust Wallet.
Incorrect. MetaMask does not store the full BIP39 mnemonic in retrievable form. It stores an encrypted vault blob that requires user password + PBKDF2 to decrypt. Trust Wallet uses Android Keystore to generate keys directly — the mnemonic is shown once for backup and then discarded from persistent storage.
Your code:
fun getMainWalletSeedForZcash(): String? {
return encryptedPrefs.getString("...wallet_main_seed", null)
}
This returns the complete 12-word mnemonic as a String. Any code path — including JNI callbacks from your Rust core — can invoke this and obtain the root secret. That is not "standard key hierarchy." That is persisting the master secret in retrievable form.
Your argument that "any app that uses a seed must load it into memory" conflates transient memory usage with persistent retrievable storage. Yes, the seed must be in RAM to derive keys. No, it does not need to be stored in SharedPreferences so that getMainWalletSeedForZcash() can return it on demand.
Rebuttal 4: "System password is not a backdoor"
Your claim: It is encrypted under a dedicated Keystore key and protected by app sandbox.
You missed the point entirely. The issue is not whether the system password is encrypted. The issue is that it is retrievable by your own code and used for automatic authentication bypass:
val systemPassword = keyManager.getSystemPassword()
if (systemPassword != null && keyManager.unlockSeed(systemPassword)) {
SessionManager.setUnlocked(this)
// navigates to MainActivity
}
This means:
- User sets "no password" during onboarding
- App generates a random password
- App stores that password so it can read it back later
- On every cold start, app reads the password and unlocks everything without user interaction
That is the definition of a stored credential that bypasses authentication. Signal's "no password" mode does not work this way — it uses a Keystore key bound to the lock screen (setUserAuthenticationRequired(true)), which requires the user to unlock the device. Your implementation requires nothing — the app unlocks itself.
Your claim that "no other app can call KeyManager" ignores:
- Root access
- Memory dumps (
adb shell am dumpheap)
- Your own Rust core running in the same process with JNI access
Rebuttal 5: "Argon2id is strong, so offline cracking is a strawman"
Your claim: If the attacker has the MasterKey, they already have total compromise.
This is factually wrong and dangerous reasoning. These are two distinct attack paths:
Path A — Keystore extraction: Difficult. Requires specific vulnerabilities or physical access with forensic tools.
Path B — EncryptedSharedPreferences file copy: Trivial. On a rooted device, the attacker simply copies /data/data/your.package/shared_prefs/*.xml to their machine. The files are encrypted, yes — but the attacker now has the ciphertext and can perform unlimited offline brute-force against the Argon2id hash.
You are arguing that Path A is hard, therefore Path B does not matter. That is incorrect. Path B does not require extracting the MasterKey from Keystore. It only requires reading a file — which root access provides instantly.
Once the password is cracked offline, the attacker calls unwrapSeedBytes() and has the root seed. This is a real, practical attack that your architecture enables.
Rebuttal 6: "SHA-256 vs HKDF is a nitpick"
Your claim: The seed is high-entropy, so SHA-256 is functionally equivalent.
Cryptographically incorrect. HKDF (RFC 5869) is the standard for a reason:
- It provides extract-then-expand structure, ensuring uniform output even if input entropy is partially compromised
- It is domain-separated via the
info parameter, preventing cross-protocol key reuse
- It is provably secure under standard assumptions
SHA-256(seed || "x25519") is an ad-hoc construction with no security proof. While length-extension attacks may not be directly applicable here, the use of raw hash functions for key derivation is explicitly discouraged by NIST SP 800-108 and RFC 5869.
Labeling a deviation from cryptographic standards as "not a security fix" reveals a culture of cutting corners in security-critical code.
Rebuttal 7: "50/100 is arbitrary and unprofessional"
Your claim: The rubric is unexplained.
It was explicitly explained:
- Key Management (10/30): You store all private keys as retrievable hex strings instead of generating them in Keystore
- Authentication (12/25): You store a retrievable system password that auto-unlocks the app
- Data Erasure (13/20): Duress mode still misleads the user with "Incorrect password"
- Logging (9/15): Sensitive data still logged despite ProGuard fix
- Cryptographic Implementation (6/10): SHA-256 used instead of HKDF; keys not hardware-bound
Your defense — "Signal and WhatsApp do this too" — is whataboutism. The security of other apps has no bearing on your vulnerabilities. If Signal has the same flaw, that is a separate bug report.
Final Response to Your "Disputed" Status
You have not disputed any vulnerability with technical accuracy. You have:
- Misrepresented how
MasterKey.Builder works
- Conflated TEE with StrongBox
- Compared your implementation to apps that do not actually behave the same way
- Dismissed offline brute-force as a "strawman" despite it being a distinct, practical attack path
- Downgraded cryptographic standard deviations to "code quality"
The core architectural flaw remains unaddressed: Your app stores every private key and the master mnemonic in a form that is retrievable by your own code, protected by a single software-fallback MasterKey. That is not a "threat model choice." That is a design error for an app handling cryptocurrency and encrypted communications.
I maintain the original severity ratings. These vulnerabilities are real, reproducible in code review, and require architectural fixes — not README updates.
Recommended status: Confirmed, requires fix.
@Secure-Legion Thank you for the detailed response. Unfortunately, several of your rebuttals contain technical inaccuracies that misrepresent the actual risk. Let me address each one directly.
Rebuttal 1: "Hex strings in EncryptedSharedPreferences are encrypted at rest"
Your claim: The hex encoding is just serialization; the ciphertext is what persists.
The reality you are ignoring: The problem is not whether the data is encrypted at rest. The problem is that every operational private key is materialized as a hex string in application memory and then placed into a software-backed storage container.
EncryptedSharedPreferencesuses aMasterKeythat is notsetIsStrongBoxBacked(true). On the majority of Android devices in the wild, this MasterKey is derived inside the TEE but its key material is software-backed — meaning it can be extracted via known Keystore vulnerabilities (CVE-2022-20007, CVE-2023-21237, and others) or forensic tools like Frida/ objection.Once that MasterKey is extracted, the attacker has every private key in one shot — Ed25519, X25519, Kyber-1024, Tor HS, voice service. That is not "an implementation detail." That is a single point of total failure.
Your comparison to "Google apps" is a red herring. Google apps do not store cryptocurrency private keys and post-quantum encryption keys in the same storage bucket.
Rebuttal 2: "MasterKey automatically uses hardware-backed storage"
Your claim:
AES256_GCMautomatically uses TEE/StrongBox.Factually incorrect.
MasterKey.Builderwith.setKeyScheme(AES256_GCM)does not setsetIsStrongBoxBacked(true). The Android documentation explicitly states:TEE is not equivalent to StrongBox. TEE keys have been extracted in the wild via:
StrongBox (dedicated HSM) is the only Android Keystore security level that resists physical extraction. You chose not to enforce it. That is a security downgrade, not a compatibility feature.
Your claim that forcing StrongBox would "brick the app" is also incorrect. You can attempt StrongBox and gracefully fall back to TEE while still enforcing
setUserAuthenticationRequired(true)— which you also omitted.Rebuttal 3: "BIP39 wrapping is standard key hierarchy"
Your claim: This is identical to MetaMask and Trust Wallet.
Incorrect. MetaMask does not store the full BIP39 mnemonic in retrievable form. It stores an encrypted vault blob that requires user password + PBKDF2 to decrypt. Trust Wallet uses Android Keystore to generate keys directly — the mnemonic is shown once for backup and then discarded from persistent storage.
Your code:
This returns the complete 12-word mnemonic as a
String. Any code path — including JNI callbacks from your Rust core — can invoke this and obtain the root secret. That is not "standard key hierarchy." That is persisting the master secret in retrievable form.Your argument that "any app that uses a seed must load it into memory" conflates transient memory usage with persistent retrievable storage. Yes, the seed must be in RAM to derive keys. No, it does not need to be stored in
SharedPreferencesso thatgetMainWalletSeedForZcash()can return it on demand.Rebuttal 4: "System password is not a backdoor"
Your claim: It is encrypted under a dedicated Keystore key and protected by app sandbox.
You missed the point entirely. The issue is not whether the system password is encrypted. The issue is that it is retrievable by your own code and used for automatic authentication bypass:
This means:
That is the definition of a stored credential that bypasses authentication. Signal's "no password" mode does not work this way — it uses a Keystore key bound to the lock screen (
setUserAuthenticationRequired(true)), which requires the user to unlock the device. Your implementation requires nothing — the app unlocks itself.Your claim that "no other app can call KeyManager" ignores:
adb shell am dumpheap)Rebuttal 5: "Argon2id is strong, so offline cracking is a strawman"
Your claim: If the attacker has the MasterKey, they already have total compromise.
This is factually wrong and dangerous reasoning. These are two distinct attack paths:
Path A — Keystore extraction: Difficult. Requires specific vulnerabilities or physical access with forensic tools.
Path B — EncryptedSharedPreferences file copy: Trivial. On a rooted device, the attacker simply copies
/data/data/your.package/shared_prefs/*.xmlto their machine. The files are encrypted, yes — but the attacker now has the ciphertext and can perform unlimited offline brute-force against the Argon2id hash.You are arguing that Path A is hard, therefore Path B does not matter. That is incorrect. Path B does not require extracting the MasterKey from Keystore. It only requires reading a file — which root access provides instantly.
Once the password is cracked offline, the attacker calls
unwrapSeedBytes()and has the root seed. This is a real, practical attack that your architecture enables.Rebuttal 6: "SHA-256 vs HKDF is a nitpick"
Your claim: The seed is high-entropy, so SHA-256 is functionally equivalent.
Cryptographically incorrect. HKDF (RFC 5869) is the standard for a reason:
infoparameter, preventing cross-protocol key reuseSHA-256(seed || "x25519")is an ad-hoc construction with no security proof. While length-extension attacks may not be directly applicable here, the use of raw hash functions for key derivation is explicitly discouraged by NIST SP 800-108 and RFC 5869.Labeling a deviation from cryptographic standards as "not a security fix" reveals a culture of cutting corners in security-critical code.
Rebuttal 7: "50/100 is arbitrary and unprofessional"
Your claim: The rubric is unexplained.
It was explicitly explained:
Your defense — "Signal and WhatsApp do this too" — is whataboutism. The security of other apps has no bearing on your vulnerabilities. If Signal has the same flaw, that is a separate bug report.
Final Response to Your "Disputed" Status
You have not disputed any vulnerability with technical accuracy. You have:
MasterKey.BuilderworksThe core architectural flaw remains unaddressed: Your app stores every private key and the master mnemonic in a form that is retrievable by your own code, protected by a single software-fallback MasterKey. That is not a "threat model choice." That is a design error for an app handling cryptocurrency and encrypted communications.
I maintain the original severity ratings. These vulnerabilities are real, reproducible in code review, and require architectural fixes — not README updates.
Recommended status: Confirmed, requires fix.