EU Age Verification App Hacked in Under 2 Minutes
Why is this Security meme funny?
Level 1: A Lock With the Key Taped to the Door
Imagine a house with a fancy keypad lock on the front door. It looks secure — there's a PIN, there's even a little "fingerprint required" sticker. But the keypad keeps the correct code, the try-counter, and the "fingerprint on/off" switch all written on a sticky note taped to the outside of the door. Anyone walking up can read the note, flip the fingerprint switch to "off," erase the code, type their own, and walk in — and the house still insists they're the rightful owner. That's the joke: the security looked real, but every lock in it could be reset by the person it was supposed to keep out.
Level 2: What shared_prefs Actually Is
A few terms make the joke land. shared_prefs is Android's lightweight storage for app settings: small XML files under the app's private data directory. "Private" only means other apps can't read it on a non-rooted device — the app itself, the OS owner, anyone with a device backup, and any rooted/emulated environment can read and edit it freely. So putting a security secret there and assuming nobody can touch it is the client_side_security trap.
Encrypt vs. hash: encryption is a two-way door (you can lock and unlock with a key), while hashing is a one-way door (you can check a match but never recover the original). For verifying a PIN, you want the one-way door. The PinEnc/PinIV names (Enc = ciphertext, IV = initialization vector, the random starter value a cipher needs) reveal the app chose the two-way door.
Rate limiting normally means "after N wrong tries, slow down or lock out." That decision has to be enforced somewhere the attacker can't edit — a server, or at minimum tamper-resistant storage. Here it's a counter in the same editable file, so the attacker resets it to 0 and brute-forces forever. This is the kind of thing you only internalize after your first "wait, the client can just lie to us?" production incident.
Level 3: The Client Is Not Your Friend
The post is a screenshot of an X thread from Paul Moore - Security Consultant (@Paul_Reviews), opening with the line:
Hacking the #EU #AgeVerification app in under 2 minutes.
What follows is a checklist of the oldest mistake in application security: trusting the client. The app, by his account, takes the PIN you create during setup, encrypts it, and drops it into Android's shared_prefs directory — a per-app folder of XML key/value files that the app owner (and anyone with root, a backup extraction, or a rooted/emulated device) can read and rewrite at will. That single design choice is the whole vulnerability, and everything else is just consequences.
The first sin he names is conceptual:
- It shouldn't be encrypted at all - that's a really poor design.
He's right, and the distinction matters. A PIN you need to verify should never be recoverable — you store a salted hash (ideally via a slow KDF like scrypt, bcrypt, or Argon2), then hash the user's input and compare. Encryption is reversible by definition; it implies the system can get the plaintext PIN back, which is exactly what you don't want for an authenticator. Choosing encrypt over hash here is the hash_vs_encrypt confusion that haunts junior and government-contractor code alike.
The second sin is architectural:
- It's not cryptographically tied to the vault which contains the identity data.
This is the killer. The PIN and the identity vault are stored as independent blobs. Because nothing binds them — no key derived from the PIN that actually decrypts the vault — the PIN is decorative. The attack he describes is brutally simple: delete the PinEnc/PinIV values from shared_prefs, restart, set a brand-new PIN, and the app cheerfully re-attaches the old profile's verified credentials to your new PIN and presents them as valid. The proof of "I am a verified adult" is now portable to anyone holding the file.
Then come the Other issues:, which read like a satire of security_theater:
- Rate limiting is an incrementing number in the same config file. Just reset it to 0 and keep trying.
- "UseBiometricAuth" is a boolean, also in the same file. Set it to false and it just skips that step.
A rate limiter that lives in a file the attacker controls isn't a rate limiter; it's a polite suggestion. A UseBiometricAuth boolean in attacker-writable storage means the "biometric protection" is one false away from oblivion. The systemic pattern is client-side-only enforcement of trust decisions — the canonical anti-pattern. Any control that protects the server's trust in the client must be validated server-side, because the client device is, in threat-model terms, fully hostile. Government identity software gets a pass on this far too often: procurement rewards shipping over review, the people who write the spec rarely threat-model, and "it has encryption" satisfies a compliance checkbox without anyone asking what the encryption is bound to. The brutal irony is that this is an age verification app — a system whose entire reason to exist is gatekeeping — built so that the gate swings open if you delete two strings.
Description
A dark-mode X post by Paul Moore - Security Consultant (@Paul_Reviews, avatar of a hooded figure with binoculars) describing 'Hacking the #EU #AgeVerification app in under 2 minutes.' He explains that during setup the app creates a PIN, *encrypts* it, and stores it in the Android shared_prefs directory; he criticizes that (1) it shouldn't be a recoverable encrypted PIN at all and (2) it isn't cryptographically tied to the vault holding identity data. An attacker can delete the PinEnc/PinIV values from shared_prefs, restart the app, set a new PIN, and the app presents the old profile's credentials as valid. Other issues: rate limiting is an incrementing counter in the same config file (reset to 0 and keep trying), and 'UseBiometricAuth' is a boolean in the same file (set false and biometrics are skipped). A textbook case of client-side-only security controls in a government identity app
Comments
34Comment deleted
Storing the PIN, the rate limiter, and the 'UseBiometricAuth' flag in one client-side file isn't defense in depth - it's a settings menu for the attacker
*they actually name it to be just a demo and every member state must do their own but cmon Comment deleted
This was my first question when I read this, whether it was just a demo, because if so that makes complete sense to not lock test users into a model that might change before an actual deployment. I'm sure the final version will use a security key or something similar. Comment deleted
Doing it properly would require using Secure Element which would be quite hard and pointless for a demo Comment deleted
To be fair, its hard to do mass surveillance, without having some security vulnerabilities maybe some information just shouldn't be collected... Comment deleted
W age verification 🤯🤯 Comment deleted
So, an attacker can simply remove the PinEnc/PinIV values from the shared_prefs file and restart the app. You need root to access shared pref Comment deleted
there's an age verification app? Comment deleted
yes, but not enforced and only presented for demo purposes Comment deleted
not enforced yet Comment deleted
Nothing is secure if others can have your device physically... Comment deleted
If an attacker is that smart, he surely passes the age verification. Comment deleted
doesn't matter, attackers can make rogue clients or 3rd party software that will help bypass these checks Comment deleted
don't underestimate kids. back in my days hacking starforce was a part of daily life. Comment deleted
cmon, didn't lucky patcher teach us not to trust edge device side data? Comment deleted
can't use lucky patcher once sideloading is deemed illegal Comment deleted
also while we are at it, kids are very smart Comment deleted
even if it gets them +0.001 robux they will hack it Comment deleted
I'm actually excited for more ability to verify the age, especially from EU side it can't possibly be worse than for-profit american options like persona and being able to exclude kids from my vrchat lobbies is a big + not excited for it being mandated in basic communication apps tho... Comment deleted
VRChat uses persona and has doesn't appear to have plans to change vendors Comment deleted
yeah, but I have plans to change the platform to Resonite its better in almost every way for me, but when they support questies - its gonna be flooded with kids, just like vrc has been Comment deleted
and yeah, there is no age verification, but there are all the tools to implement your own for your own lobbies Comment deleted
What the fuck shared_preferences sure is easier to use than secure_storage, but it's not that hard Comment deleted
Is the CN still not up on that post? He's showing off the vulnerability with a rooted device, you cant "simply remove" something from shared_prefs. If you can do that - you've blown open most, if not all, of Android's security measures. Comment deleted
The main problem of the post is that the app is actually a demo. But I would expect the actual age verification app that stores my official documents and can be used to prove my age and identity to be zero trust Comment deleted
If the Baltics were able to develop a government level 2FA system (Smart ID) then I think the EU can do the same, especially if it's gonna be open source. Comment deleted
A post from Durov on the matter 🙂 https://t.me/durov/491 Comment deleted
mhm. sure Comment deleted
He's not wrong though, ID and Age verification apps should be in the same category as passwords manager. They should be zero knowledge based, otherwise that's asking for trouble Comment deleted
well duh. but his conclusion about it being on purpose is straight up a conspiracy theory Comment deleted
True, but what I don't understand is why even show this unfinished Swiss cheese security demo app Comment deleted
I'm guessing that it was less work for the contractor this way. get overpaid and then underdeliver, is the motto of any and all government contractors Comment deleted
Maybe Comment deleted
ok but you still need physical access to the device Comment deleted