Skip to content

Instantly share code, notes, and snippets.

@ichernev
Created November 27, 2025 00:27
Show Gist options
  • Select an option

  • Save ichernev/65ff033c03b57ef3fdf4ed9864f46276 to your computer and use it in GitHub Desktop.

Select an option

Save ichernev/65ff033c03b57ef3fdf4ed9864f46276 to your computer and use it in GitHub Desktop.
AndOTP encrypted file reader
#!/usr/bin/python3
# NOTE: Install pycryptodome, there is no AES in stdlib...
import json
import hashlib
import sys
import argparse
import io
from urllib.parse import quote
from Crypto.Cipher import AES
def parse(args):
parser = argparse.ArgumentParser("read encrypted OTPAnd backup")
parser.add_argument("--password", type=str, required=True, help="the encryption password")
parser.add_argument("--otpauth", action="store_true", help="convert to otpauth:// format")
parser.add_argument("file", nargs=1, type=str, help="the encrypted file")
return parser.parse_args(args)
def main(args):
opts = parse(args)
with open(opts.file[0], 'rb') as f:
iter_b = f.read(4)
iter = int.from_bytes(iter_b, signed=True)
salt_b = f.read(12)
key = hashlib.pbkdf2_hmac('sha1', opts.password.encode(), salt_b, iter, 32)
enc_iv = f.read(12)
crnt_pos = f.tell()
end_pos = f.seek(0, io.SEEK_END)
f.seek(crnt_pos, io.SEEK_SET)
enc_b = f.read(end_pos - crnt_pos)
cipher = AES.new(key, AES.MODE_GCM, nonce=enc_iv)
plain = cipher.decrypt(enc_b)
decoder = json.JSONDecoder()
otps, _ = decoder.raw_decode(plain.decode(errors='ignore'))
if opts.otpauth:
for otp in otps:
print(f"otpauth://{otp['type'].lower()}/{quote(otp['issuer'])}:{quote(otp['label'])}?secret={quote(otp['secret'])}&period={otp['period']}&digits={otp['digits']}&issuer={quote(otp['issuer'])}")
else:
print(json.dumps(opts, indent=2))
if __name__ == '__main__':
main(sys.argv[1:])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment