Extracting secrets from Google Authenticator export QR codes
Recently, I found myself in a tricky situation: I wanted to migrate my Google Authenticator keys to another app, but I had lost all my seed phrases. You know, those backup codes you're supposed to write down but never do? Yeah, those...
Luckily, Google Authenticator has an export feature that generates QR codes containing all your accounts. But here's the thing: these QR codes aren't your standard otpauth:// URLs that you can easily scan with any authenticator app. Instead, Google uses a custom migration format based on Protocol Buffers.
So I did what any reasonable developer would do: I "reverse-engineered" (read: "googled") the format and built a tool to extract the secrets. Let me walk you through how it works.
The export format: not your average QR code
When you use Google Authenticator's export feature, it generates QR codes that look like this:
otpauth-migration://offline?data=CjMKD6NIo47vL9P5plq8l5KEhRIQcmFwaGFlbEBiYWRpYS5jYxoIQ29pbkxpc3QgASgBMAIQAhgBIAA%3D
Notice the protocol: otpauth-migration:// instead of the usual otpauth://. The data parameter contains a base64-encoded Protocol Buffer (protobuf) that holds all your account information.
Step 1: Scanning and parsing the QR code
The first step is straightforward: scan the QR code and extract the URL. In my implementation, I used react-zxing to handle the camera scanning. Once we have the URL, we need to parse it:
function parseQrCodeUrl(value: string) {
const url = new URL(value);
if (url.protocol === 'otpauth-migration:') {
const dataParam = url.searchParams.get('data');
return dataParam; // This is our base64-encoded protobuf
}
}The data parameter is URL-encoded, so we need to decode it first. But wait, there's a catch: it's also base64-encoded. So we have base64 data inside a URL parameter. Fun!
Step 2: Understanding Protocol Buffers
Protocol Buffers (protobuf) are Google's language-neutral, platform-neutral mechanism for serializing structured data. Think of them as a more efficient alternative to JSON or XML. Google uses protobuf to encode the migration data because it's compact and supports complex nested structures.
The tricky part is that we need to know the schema (the .proto file) to decode it. Fortunately, Google's protobuf format for Authenticator exports is relatively straightforward. The structure looks something like this:
{
"MigrationPayload": {
"otpParameters": [
{
"secret": "bytes",
"name": "string",
"issuer": "string",
"algorithm": "ALGORITHM_SHA1",
"digits": "DIGIT_COUNT_SIX",
"type": "OTP_TYPE_TOTP"
}
]
}
}To decode this in JavaScript, I used the protobufjs library along with a JSON representation of the schema. The decoding process looks like this:
import protobufjs from 'protobufjs';
import protocol from './google_auth.proto.json';
function decodeGoogleAuthProtocolBuffer(dataString: string) {
const root = protobufjs.Root.fromJSON(protocol);
const message = root.lookupType('MigrationPayload');
const buffer = Buffer.from(dataString, 'base64');
const uint8Array = new Uint8Array(buffer);
const decoded = message.decode(uint8Array);
return decoded;
}This gives us a JavaScript object with all the account information, including the secrets. But there's a problem: the secrets are stored as raw bytes (Uint8Array), not as the base32 strings that TOTP algorithms expect.
{
"secret": Uint8Array[115, 163, 78, 163, 142, 239, 47, 211, 249, 166, 90, 188, 151, 146, 132, 133],
"name": "myemail@badia.cc",
"issuer": "CoinList",
"algorithm": "ALGORITHM_SHA1",
"digits": "DIGIT_COUNT_SIX",
"type": "OTP_TYPE_TOTP"
}Step 3: Converting to Base32
Here's where things get interesting. The secrets in the Protocol Buffer are stored as raw binary data (Uint8Array). But TOTP algorithms (the standard used by Google Authenticator) expect secrets to be encoded in Base32 format.
The conversion is straightforward:
import * as base32 from 'hi-base32';
function encodeToBase32(secretBytes: Uint8Array): string {
const buffer = Buffer.from(secretBytes);
return base32.encode(buffer);
}Base32 is a binary-to-text encoding scheme that uses 32 characters (A-Z and 2-7) to represent binary data. It's case-insensitive and doesn't include characters that could be confusing (like 0, O, I, and 1). This makes it perfect for manual entry, which is why TOTP uses it.
Once you have the secret in Base32 format, this is exactly what you can use in your new authenticator app. Most authenticator apps (like Authy, 1Password, or even another instance of Google Authenticator) allow you to manually enter a secret key. Just copy the Base32-encoded secret and paste it into your new app when setting up 2FA for that account.
Step 4: Generating TOTP codes
Once we have our secrets in Base32 format, we can generate Time-based One-Time Passwords (TOTP codes). TOTP is an algorithm that generates a 6-digit code that changes every 30 seconds, based on:
- The shared secret (in Base32)
- The current time (rounded to 30-second intervals)
- A cryptographic hash function (SHA-1 by default)
The algorithm works by:
- Taking the current Unix timestamp
- Dividing it by the time step (30 seconds) and rounding down
- Converting this to a big-endian 8-byte array
- Computing HMAC-SHA1 of this value using the secret key
- Extracting 4 bytes from the HMAC using dynamic truncation
- Taking the last 31 bits (to avoid signed integers) and computing modulo 10^6 to get a 6-digit code
Thankfully, we don't need to implement this ourselves! The otpauth library handles all the complexity:
import { TOTP } from 'otpauth';
const totp = new TOTP({
algorithm: 'SHA1',
digits: 6,
period: 30,
secret: secretBase32, // The Base32-encoded secret we just extracted
});
const passcode = totp.generate();The library automatically handles the time synchronization and code generation. All we need to do is call generate() and it returns the current 6-digit code.
Why this matters
You might be wondering: why go through all this trouble? Can't you just scan the QR code with another authenticator app?
The answer is: not really. While some apps support Google's migration format, many don't. More importantly, understanding how the export format works gives you:
- Control: You can extract the raw secrets and use them anywhere
- Backup: You can store the secrets in a password manager or other secure location
- Transparency: You know exactly what data is being exported and how it's encoded
Security considerations
Before you rush off to extract all your secrets, let's talk about security. The export QR codes contain your actual secrets in an encoded format. While they're not plaintext, anyone who can decode the Protocol Buffer can extract your secrets.
Important security notes:
- Never share these QR codes publicly
- Treat them with the same security as your passwords
- The export is meant for migration, not for backup
- Always set up new 2FA after migration if possible
The complete flow
To summarize the entire process:
- Scan QR code → Get
otpauth-migration://offline?data=...URL - Extract data parameter → Base64-encoded Protocol Buffer
- Decode Protocol Buffer → JavaScript object with account data
- Extract secrets → Raw bytes (
Uint8Array) - Convert to Base32 → TOTP-compatible format
- Generate TOTP codes → 6-digit codes that refresh every 30 seconds
Building the tool
I built a React application that runs entirely in the browser. This was important to me because I wanted to ensure that the secrets never leave my device. The tool walks through each step visually, showing:
- The scanned QR code data
- The decoded Protocol Buffer structure
- The extracted secrets in Base32 format
- Live TOTP codes that updates live
You can try it yourself here. The implementation uses React hooks for state management and setInterval to update the TOTP codes as they refresh. Each step is broken down into separate components, making the code easier to understand and maintain.
Conclusion
Reverse-engineering Google Authenticator's export format was an interesting exercise in understanding how modern authentication systems work. Protocol Buffers, Base32 encoding, and TOTP algorithms are all pieces of the puzzle that make 2FA work securely.
If you find yourself in a similar situation (lost seed phrases / 2FA secrets, need to migrate), you now know how to extract your secrets. But remember: this is a migration tool, not a backup solution. Always keep your initial secrets safe!
The code is available on my website if you want to try it yourself. Even thought it only runs in the browser, if you’re paranoid (which you should be when dealing with authentication secrets), read the code, recode it yourself, and trust nothing blindly.