The Second Factor in the Backup: Reading otpauth URIs Out of ASCII Decimal
The challenge
A handset came back at offboarding and the forensics queue pulled an unencrypted application backup off it. Inside, one of the authenticator app's preference files holds a value the developers stored as a run of numbers rather than as text, presumably because it looked less like a secret that way. It is not encrypted and it is not a hash. Three accounts are in there. Recover the shared secret that generates the payroll codes.
What you'll learn
- Recognise a run of numbers in the 32 to 126 range as ASCII code points
- Read the fields of an otpauth URI and identify the seed
- Explain why a leaked TOTP seed is permanent and device-independent
- Describe how application backups become an exfiltration path for secrets
Skills tested
Prerequisites
- What a one-time code app does
- That ASCII maps characters to numbers
How it works
A time-based one-time code is not a secret in itself. It is a function of the current time and a shared seed, computed identically on the phone and on the server. Scanning a setup QR code is how the seed gets onto the device, and the QR encodes an otpauth:// URI whose secret parameter is the seed in base32.
That makes the seed the whole of the second factor. It never changes, it is the same on every device that holds it, and using it leaves no trace distinguishable from the real user. An attacker who copies it does not need the phone again, and the account owner has no way to notice. Recovery means the issuer resetting the enrolment, which almost nobody does after a device is simply handed back.
Storage is where it goes wrong. Seeds belong in the platform keystore, flagged so that they are excluded from device backups. When an app keeps them in ordinary preferences, they travel wherever the backup goes, and writing each character as a decimal number is not protection of any kind: it is the same bytes, spelled differently, readable by anyone who notices the values never exceed 126.
Common mistakes
- Answering the Corvid Mail or Thornbury VPN seed. All three lines look alike. The label asks for payroll.
- Submitting the whole URI. The answer is the value of
secret, not the line it sits in. - Trying base64 or base32 on the input. Neither alphabet contains spaces, and the numbers give away what this is before you touch a button.
- Expecting a six-digit code. The code changes every thirty seconds. The seed does not, which is why the seed is what matters.
How to defend against it
Keep seeds out of anything that gets copied.
- Store them in the hardware-backed keystore, with the no-backup flag set. An authenticator that survives a restore to a new phone is one that put your seeds somewhere a backup can reach.
- Exclude the app's data directory from automatic backups, and test that exclusion rather than trusting the manifest.
- Re-enrol the second factor at offboarding as a matter of routine. Collecting the handset is not the same as revoking what was on it.
- Prefer phishing-resistant factors bound to a device, such as a passkey or a hardware key, where the private material cannot be exported at all.