Skip to content

Instantly share code, notes, and snippets.

@Kambaa
Last active July 17, 2026 16:26
Show Gist options
  • Select an option

  • Save Kambaa/887b275d7d714090e6c250df4ae0895f to your computer and use it in GitHub Desktop.

Select an option

Save Kambaa/887b275d7d714090e6c250df4ae0895f to your computer and use it in GitHub Desktop.
Authenticator Export Offline Decoder

Offline Google Authenticator Export Data Decoder

This python script decodes Authenticator's exported data starts with otpauth-migration://offline?data=******* to otpauth://totp/***** by account.

It does the decoding OFFLINE!

I needed this for manually importing my OTP's to my Custom Authenticator App

Run it with:

python decode_migration.py

Paste your entire otpauth-migration://offline?data=... URI when prompted, and it will output one otpauth://... URI per account.

If your Windows app only accepts a single TOTP account at a time, you can copy the corresponding otpauth://totp/... line directly into it.

Careful on spaces in names, my app did not like them and did not import.

import base64
import sys
from urllib.parse import urlparse, parse_qs, unquote
from google.protobuf import descriptor_pb2, descriptor_pool, message_factory
# Build the MigrationPayload protobuf dynamically
fd = descriptor_pb2.FileDescriptorProto()
fd.name = "migration.proto"
enum = fd.enum_type.add()
enum.name = "Algorithm"
for name, num in [("ALGORITHM_UNSPECIFIED",0),("SHA1",1),("SHA256",2),("SHA512",3),("MD5",4)]:
v = enum.value.add()
v.name = name
v.number = num
enum = fd.enum_type.add()
enum.name = "DigitCount"
for name, num in [("DIGIT_COUNT_UNSPECIFIED",0),("SIX",1),("EIGHT",2)]:
v = enum.value.add()
v.name = name
v.number = num
enum = fd.enum_type.add()
enum.name = "OtpType"
for name, num in [("OTP_TYPE_UNSPECIFIED",0),("HOTP",1),("TOTP",2)]:
v = enum.value.add()
v.name = name
v.number = num
otp = fd.message_type.add()
otp.name = "OtpParameters"
fields = [
("secret",1,12,None),
("name",2,9,None),
("issuer",3,9,None),
("algorithm",4,14,"Algorithm"),
("digits",5,14,"DigitCount"),
("type",6,14,"OtpType"),
("counter",7,4,None),
]
for name, num, typ, typname in fields:
f = otp.field.add()
f.name = name
f.number = num
f.label = 1
f.type = typ
if typname:
f.type_name = typname
payload = fd.message_type.add()
payload.name = "MigrationPayload"
f = payload.field.add()
f.name = "otp_parameters"
f.number = 1
f.label = 3
f.type = 11
f.type_name = "OtpParameters"
for name, num in [
("version",2),
("batch_size",3),
("batch_index",4),
("batch_id",5),
]:
f = payload.field.add()
f.name = name
f.number = num
f.label = 1
f.type = 13
pool = descriptor_pool.DescriptorPool()
pool.Add(fd)
MigrationPayload = message_factory.GetMessageClass(
pool.FindMessageTypeByName("MigrationPayload")
)
uri = input("Paste otpauth-migration URI:\n").strip()
qs = parse_qs(urlparse(uri).query)
if "data" not in qs:
print("No data= parameter found.")
sys.exit(1)
blob = qs["data"][0]
padding = "=" * (-len(blob) % 4)
raw = base64.urlsafe_b64decode(blob + padding)
msg = MigrationPayload()
msg.ParseFromString(raw)
ALG = {
1: "SHA1",
2: "SHA256",
3: "SHA512",
4: "MD5",
}
DIGITS = {
1: 6,
2: 8,
}
TYPE = {
1: "hotp",
2: "totp",
}
for i, otp in enumerate(msg.otp_parameters, 1):
secret = base64.b32encode(otp.secret).decode().rstrip("=")
issuer = otp.issuer
account = otp.name
label = account
if issuer:
label = f"{issuer}:{account}"
params = [
f"secret={secret}",
]
if issuer:
params.append(f"issuer={issuer}")
if otp.algorithm != 1:
params.append(f"algorithm={ALG.get(otp.algorithm,'SHA1')}")
if otp.digits == 2:
params.append("digits=8")
if otp.type == 1:
params.append(f"counter={otp.counter}")
uri = f"otpauth://{TYPE.get(otp.type,'totp')}/{unquote(label)}?" + "&".join(params)
print("=" * 70)
print(f"Account #{i}")
print(uri)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment