Standards · otpauth-migration
Is the Google Authenticator export QR safe to scan?
Only if you generated it yourself, on your own old phone, with your own new phone aimed at the screen. Anyone else who photographs that QR — for any reason — gains permanent access to every 2FA code for every account in the bundle.
Verify a bundle → All standards →
What it is
Google Authenticator and several compatible apps (Aegis, Raivo OTP, 2FAS) let you "export" your stored 2FA accounts so you can move them to a new device. The export takes the form of a QR code with a URI that starts with otpauth-migration://. Inside that URI is a single protocol-buffer bundle (Google's MigrationPayload schema) carrying every account at once — issuer, account name, secret seed, algorithm, digit count, type (HOTP / TOTP), and counter.
The format was originally undocumented, but it has been reverse-engineered and is now the de-facto interchange format for 2FA backups. Third-party authenticator apps recognize and import the same QR.
One QR can contain dozens of accounts. The QR is visually indistinguishable from a normal TOTP setup QR — there's no visual marker telling a casual observer it carries the entire bundle.
Why a single bundle QR is uniquely dangerous
A regular otpauth:// setup QR is dangerous to scan if you didn't request it — but the blast radius is one account. The migration QR's blast radius is every account in your authenticator. If an attacker photographs the screen showing this QR — over your shoulder in a coffee shop, via a CCTV camera you didn't notice, through a clear glass window — they walk away with the ability to bypass 2FA on every account it covered. Permanently. Until you rotate each secret manually, which most services don't make easy.
There is no expiration on the secrets in the bundle. Unlike a session token (which expires), unlike a passkey QR (which is one-shot), the seeds in a migration bundle are valid until you replace them service-by-service.
This is why our scanner's verdict on every otpauth-migration:// QR starts at likely_dangerous and stays there regardless of context. The format is fine; the threat model is the format's existence.
What's actually inside (anatomy)
The URI looks like:
otpauth-migration://offline?data=<urlsafe-base64-encoded-protobuf>
The base64 body, once decoded, is a MigrationPayload protocol-buffer with these top-level fields:
- otp_parameters — repeated. One entry per account.
- version — schema version.
- batch_size — total chunk count when an export is split across multiple QRs.
- batch_index — this chunk's position.
- batch_id — unique ID linking the chunks together.
Each otp_parameters entry has:
- secret — the raw seed bytes. This is the actual key material.
- name — the account label (e.g.
alice@acme.com). - issuer — the service name (e.g.
ACME, GitHub). - algorithm — SHA1 / SHA256 / SHA512 / MD5.
- digits — 6 or 8 digits per code.
- type — TOTP (time-based) or HOTP (counter-based).
- counter — current counter value for HOTP entries.
What our scanner surfaces, and what it deliberately doesn't
✓ Surfaced
For every entry in the bundle, the verdict shows the issuer (ACME, GitHub, AWS), the account name (alice@acme.com), the algorithm (SHA1), the digit count (6), and the type (TOTP). The verdict disclosure reads "Grants in this bundle: ACME / alice@acme.com; GitHub / bob@github; AWS / root; ..." so a user who IS mid-migration can audit the list before importing.
Schema version, batch index, batch size, and batch ID are also surfaced — useful when an export is chunked across multiple QRs.
✗ Never decoded
The raw secret bytes are never decoded into the verdict output. Our analyzer reads them only to verify each entry is well-formed (secret present + non-empty), then discards the value before findings is built.
This is asserted by the test suite — we feed the analyzer hand-crafted protobuf payloads containing canary strings (SECRET_SEED_1, SECRET_SEED_2) and assert those strings never appear in the serialized verdict output. A regression there fails CI.
When (and how) to use one
Legitimate use case: you're upgrading from one phone to another. The OLD phone shows the migration QR on its screen for ~30 seconds. The NEW phone's authenticator app aims at the screen and imports. Nobody else is in the room. You delete the export from the old phone after confirming everything imported.
The danger surfaces are:
- Screenshots of the QR. A screenshot saved to cloud-synced photos puts the QR on every device synced to that account.
- Sharing the QR over chat. Even briefly — anyone with access to the chat (now or later) can extract the bundle.
- Posting the QR in a help-forum thread. This happens more than you'd think; people ask for help and post a photo of their authenticator export by mistake.
- Public posters / signage misidentified as setup QRs. The format is visually indistinguishable from a single-account setup QR.
If you suspect a migration QR was exposed
Treat as a credential leak across every account in the bundle. The recovery steps, in order:
- Sign in to each affected service and rotate the 2FA secret. Most services let you do this under Account → Security → Two-factor authentication → Disable then re-enroll.
- For services that don't expose rotation (rare but exists), remove and re-add the account.
- Audit recent sign-in activity on every affected account.
- Delete the original export QR from every device + cloud-synced photo library that may have it.
An attacker with the bundle can produce valid 2FA codes for years until you rotate. Don't postpone.
Verify your bundle
Drop the QR image into our scanner, paste the otpauth-migration:// URI, or use the camera. Verdict shows every account in the bundle without ever decoding the secrets.
Open scanner →