Skip to content

Instantly share code, notes, and snippets.

@motsmanish
Created March 21, 2026 07:30
Show Gist options
  • Select an option

  • Save motsmanish/dfb3f1981cb6fe54bb76fed64f879077 to your computer and use it in GitHub Desktop.

Select an option

Save motsmanish/dfb3f1981cb6fe54bb76fed64f879077 to your computer and use it in GitHub Desktop.
Google Authenticator Export Decoder - Zero dependencies. Pure Python stdlib only.
#!/usr/bin/env python3
"""
decode_otp.py — Google Authenticator Export Decoder
=====================================================
Zero dependencies. Pure Python stdlib only.
Usage:
1. Save your otpauth-migration:// URL to a file:
echo "otpauth-migration://offline?data=..." > /tmp/otp_url.txt
2. Run:
python3 decode_otp.py
Output:
Writes decoded secrets to /tmp/otp_secrets.txt
⚠️ DELETE /tmp/otp_secrets.txt immediately after copying secrets to your password manager.
Cleanup:
This script deletes itself and the input file automatically after running.
You must manually delete the output file after you are done:
rm /tmp/otp_secrets.txt
Author: https://gist.github.com/motsmanish
"""
import base64
import urllib.parse
import os
import sys
INPUT_FILE = "/tmp/otp_url.txt"
OUTPUT_FILE = "/tmp/otp_secrets.txt"
THIS_SCRIPT = os.path.abspath(__file__)
WARNING = """
====================================================
⚠️ SECURITY WARNING
====================================================
This file contains your 2FA secret keys.
Anyone with these secrets can generate your OTP codes.
DELETE THIS FILE immediately after copying
secrets to your password manager.
rm {}
====================================================
""".format(OUTPUT_FILE)
def parse_varint(data, pos):
result = 0
shift = 0
while pos < len(data):
b = data[pos]
pos += 1
result |= (b & 0x7F) << shift
if not (b & 0x80):
break
shift += 7
return result, pos
def parse_chunk(chunk):
fields = {}
i = 0
while i < len(chunk):
if i >= len(chunk):
break
tag = chunk[i]; i += 1
field_num = tag >> 3
wire_type = tag & 0x07
if wire_type == 2: # length-delimited
length = chunk[i]; i += 1
val = chunk[i:i + length]; i += length
fields[field_num] = val
elif wire_type == 0: # varint
val, i = parse_varint(chunk, i)
fields[field_num] = val
else:
break
return fields
def decode(url):
data = urllib.parse.parse_qs(urllib.parse.urlparse(url).query)['data'][0]
raw = base64.b64decode(data)
entries = []
i = 0
count = 1
while i < len(raw):
tag = raw[i]; i += 1
field_num = tag >> 3
wire_type = tag & 0x07
if wire_type == 2:
length = raw[i]; i += 1
chunk = raw[i:i + length]; i += length
if field_num == 1:
fields = parse_chunk(chunk)
secret = base64.b32encode(fields.get(1, b'')).decode() if isinstance(fields.get(1), bytes) else ''
name = fields.get(2, b'').decode('utf-8', errors='ignore') if isinstance(fields.get(2), bytes) else ''
issuer = fields.get(3, b'').decode('utf-8', errors='ignore') if isinstance(fields.get(3), bytes) else ''
entries.append({
'index': count,
'issuer': issuer or '(none)',
'name': name or '(none)',
'secret': secret or '(none)',
})
count += 1
else:
i += 1
return entries
def main():
# Check input file
if not os.path.exists(INPUT_FILE):
print("ERROR: Input file not found: {}".format(INPUT_FILE))
print("Create it with:")
print(' echo "otpauth-migration://offline?data=..." > {}'.format(INPUT_FILE))
sys.exit(1)
with open(INPUT_FILE, "r") as f:
url = f.read().strip()
if not url.startswith("otpauth-migration://"):
print("ERROR: File does not contain a valid otpauth-migration:// URL.")
sys.exit(1)
entries = decode(url)
if not entries:
print("ERROR: No OTP entries found in the URL.")
sys.exit(1)
# Write output
lines = []
lines.append(WARNING)
lines.append("Total entries found: {}\n".format(len(entries)))
lines.append("=" * 52 + "\n")
for e in entries:
lines.append("Entry #{}".format(e['index']))
lines.append(" Issuer : {}".format(e['issuer']))
lines.append(" Name : {}".format(e['name']))
lines.append(" Secret : {}".format(e['secret']))
lines.append("")
lines.append("=" * 52)
lines.append(WARNING)
output = "\n".join(lines)
with open(OUTPUT_FILE, "w") as f:
f.write(output)
print("✅ Done. Secrets written to: {}".format(OUTPUT_FILE))
print("⚠️ DELETE the output file after copying to your password manager:")
print(" rm {}".format(OUTPUT_FILE))
# Cleanup input file and this script
try:
os.remove(INPUT_FILE)
print("🧹 Deleted input file: {}".format(INPUT_FILE))
except Exception:
pass
try:
os.remove(THIS_SCRIPT)
print("🧹 Deleted script: {}".format(THIS_SCRIPT))
except Exception:
pass
print("\n⚠️ Clear your terminal history:")
print(" history -c && history -w")
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment