Skip to content

Instantly share code, notes, and snippets.

@mapster
Last active December 30, 2023 06:29
Show Gist options
  • Star 16 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • 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 <alexander@rosbach.no>",
"license": "MIT",
"dependencies": {
"hi-base32": "^0.5.0",
"protobufjs": "^6.10.1"
}
}
@max-berman
Copy link

Works like a charm, Thank you!

@jcconnell
Copy link

Worked well for me also! Thank you.

Probably should add npm install as step 2 to the usage section.

@milansav
Copy link

milansav commented Aug 4, 2022

You are a legend! Thank you!

@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

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