Google's "Transfer accounts -> Export accounts" produces an
otpauth-migration:// link wrapping a base64 protobuf that holds many accounts
at once, and splits large exports across several QR codes. v0.1 rejected these
outright; now they import.
The protobuf is decoded by hand rather than pulling prost: the schema is four
messages that have not changed since 2020, and the wire format needed is two
types (varint, length-delimited). Unknown fields are skipped properly, so a
future Google addition degrades rather than corrupting the parse.
Two details in this format silently produce plausible-but-wrong codes if taken
backwards, so both are pinned by tests: the `secret` field is RAW BYTES, not
base32, and the payload carries NO period field -- every Google export is
30-second TOTP.
- core/migration.rs: protobuf decode, lenient base64 (all four alphabet and
padding spellings), issuer reconciliation between the `name` prefix and the
dedicated `issuer` field, and batch metadata surfaced as "Part 1 of 2"
- vault: add_many() with duplicate detection, persisting once per batch rather
than once per account; duplicates match on secret AND naming, so keeping one
secret under two labels stays possible
- http-api: preview returns a list flagged with what is already stored; import
takes the link plus selected indices and re-parses server-side, so secrets
never travel out to the browser and back. /api/qr/decode now returns every
payload in an image, not just the first
- webui: multi-account import checklist with select all/none, per-entry
algorithm and digit chips, already-present entries pre-unticked, and skipped
entries named with their reason (HOTP, MD5) instead of silently dropped
Verified end to end against real QR images: upload -> decode -> preview ->
import, with the resulting codes cross-checked against an independent RFC 6238
implementation, including an 8-digit account.
124 tests (was 101).
Co-Authored-By: Claude Opus 4.8 <[email protected]>