Skip to content

Instantly share code, notes, and snippets.

@mapster
Last active April 8, 2025 09:48
Show Gist options
  • Save mapster/4b8b9f8f6b92cc1ca58ae5506e0508f7 to your computer and use it in GitHub Desktop.
Save mapster/4b8b9f8f6b92cc1ca58ae5506e0508f7 to your computer and use it in GitHub Desktop.
Export Google Authenticator secret OTP-keys

Export Google Authenticator secret OTP-keys

I recently got myself a Yubikey and wanted to set up the Yubico Authenticator with all the OTPs I had in Google Authenticator. Unfortunately Yubico Authenticator doesn't support scanning the QR-code that the Google Authenticator generates when you export the OTP-keys, and it seemed like quite the daunting task to log in to every service to generate new OTP-keys. So I decided to have a look at the contents of the QR code, to see if I could import the keys into Yubico Authenticator in one go. Luckily I found a blog post by Alex Bakker that describes the data format.

Transfer QR-code to computer

Unfortunately, but likely for the best, the security policy of Google Authenticator won't allow you to take a screenshot of the generated export-all QR-code. Since my phone is also the only device I own with a decent camera, I had to resign to snap a picture of QR-code on the phone screen using the built-in webcam of my laptop. If you also use a low quality camera you might run into the same issue that I did, namely that the picture will have too much noice for QR-code readers to interpret the QR-code. The easiest way around it was split the export into multiple QR-codes, which for me meant two codes instead of twenty. I used the Linux desktop app Kamoso to snap the pictures.

Extract OTP-keys

To extract the OTP-keys from the Google Authenticator QR-code is a four-step procedure:

  1. Extract data-URL from the QR-code
  2. Base64 Decode the query parameter data
  3. Decode the protobuf message
  4. For each OTP-key; base32 decode the secret field

Requirements

  • nodejs
  • zbar-tools

The zbar-tools package includes a tool to extract URLs from QR-codes. I did try to use jimp and qrcode-reader in the javascript, but it didn't work straight out the box so I didn't bother spending more time to get it to work.

Usage

  1. Download the files package.json, index.js, migration-payload.proto and otp-codes.sh to an empty directory
  2. Make otp-codes.sh executable: chmod +x otp-codes.sh
  3. Extract codes ./otp-codes.sh <path to qr-code image>
const protobuf = require("protobufjs");
const fs = require('fs');
const base32 = require('hi-base32');
async function decodeMessage(buffer) {
const root = await protobuf.load("migration-payload.proto");
const payload = root.lookupType("MigrationPayload");
const err = payload.verify(buffer);
if (err) {
throw err;
}
const message = payload.decode(buffer);
const obj = payload.toObject(message);
return payload.toObject(message);
}
async function printOTPCodes(otpBuffer) {
const payload = await decodeMessage(otpBuffer);
const otpArray = payload.otpParameters;
for(let i = 0; i < otpArray.length; i++) {
const otp = otpArray[i];
console.log("Issuer: " + otp.issuer);
console.log("Name: " + otp.name);
console.log("Secret: " + base32.encode(otp.secret));
console.log("-----------------------------------");
}
}
const url = new URL(process.argv[2]);
const otpBuffer = Buffer.from(url.searchParams.get('data'), 'base64');
printOTPCodes(otpBuffer).catch(err => console.error(err));
syntax = "proto3";
message MigrationPayload {
enum Algorithm {
ALGO_INVALID = 0;
ALGO_SHA1 = 1;
}
enum OtpType {
OTP_INVALID = 0;
OTP_HOTP = 1;
OTP_TOTP = 2;
}
message OtpParameters {
bytes secret = 1;
string name = 2;
string issuer = 3;
Algorithm algorithm = 4;
int32 digits = 5;
OtpType type = 6;
int64 counter = 7;
}
repeated OtpParameters otp_parameters = 1;
int32 version = 2;
int32 batch_size = 3;
int32 batch_index = 4;
int32 batch_id = 5;
}
#!/bin/bash
qrcode="$(zbarimg $1 2>/dev/null)"
url="${qrcode/#QR-Code:}"
echo "Parsing: $url"
echo "-----------------------"
echo ""
node index.js "$url"
{
"name": "google-authenticator-otp-key-extractor",
"version": "1.0.0",
"description": "",
"main": "index.js",
"author": "Alexander Hoem Rosbach <[email protected]>",
"license": "MIT",
"dependencies": {
"hi-base32": "^0.5.0",
"protobufjs": "^6.10.1"
}
}
@scarlion1
Copy link

I'm trying to run through these steps manually, and when I decode the protobuf message, it looks like my secrets are still encoded or encrypted in different ways. For example I have some output that looks like:

otp_parameters {
secret: "\123\456\789\012"
}

while others look like:
otp_parameters {
secret: "ABC12E2F33GH4"
}

while still others look like:
otp_parameters {
secret: "RwX&pUe*QLn#9BPWGRQUmHP#T"
}

None of the secrets can be base32 decoded so I'm stuck. Any idea what's happened?

@msm-code
Copy link

Yes. You should base32 encode them, not decode. I.e. these are raw bytes, and you must base32 encode them to make them printable

@thomasmarwitz
Copy link

Thanks for your gist! I made this web version out of it: https://thomasmarwitz.github.io/tools/artifacts/extract-otp-secrets.html
(if you don't want to install tools)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment