After finding out the vendor lock in of this tool, I wanted to get rid of it to move my TOTP generation to Bitwarden. But for that I would either need to setup TOTP again for 20 separate services, or somehow extract the seed from the current generator.

The backup from this tool is a .encrypt file, which encrypts with the password totpauthenticator by default, You can set it under Settings->Encryption Key without knowing the previous password, if you’d like to change it.

Then it’s off to decrypting it! I won’t explain the .apk decompilation and Java scouting done to find this method, but I will give you the tools to decrypt it yourself.

The process is quite simple, and explained in the code snippet below. To run it, you need to pip install pycryptodome.

import hashlib
import base64
import json

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad

# Place the file as `backup.encrypt` next to this script.
# It's a base64 encoded binary.
with open("backup.encrypt", "rb") as f:
    base64_binary = f.read()
    binary = base64.b64decode(base64_binary)

password = "Passw0rd"

# We need a SHA256 hash of the password.
password_hash = hashlib.sha256(password.encode('utf-8')).digest()

# The IV is set in an imported library, which didn't bother with it.
iv = b'\x00'*16

# Now we can decrypt the AES/CBC/PKCS7 encrypted binary.
def decrypt_with_AES(cipher_text, secret_key):
    cipher = AES.new(password_hash, AES.MODE_CBC, iv)
    plain_bytes = unpad(cipher.decrypt(cipher_text), AES.block_size)
    return plain_bytes.decode()

decrypted = decrypt_with_AES(binary, password_hash)

# The content is a key value pair, of which the key contains a
# list of key-value blocks with our data.
entries = json.loads(list(json.loads(decrypted))[0])

# Finally, write the resulting json to a file.
with open("backup.json", "w") as f:
    json.dump(entries, f)

# We find out the seeds have been encoded, so we will decode 
# these as well. Quite simple luckily: hex encoded byte arrays from
# which we retrieve the base32 seed.
readable = ""
    
for entry in entries:
    encoded = entry["key"]
    decoded = bytes.fromhex(encoded)
    recoded = base64.b32encode(decoded).decode()
    readable += f"{entry['issuer']} / {entry['name']}: {recoded}\n"

# Don't use any padding characters (=) when setting the seed
with open("backup.txt", "w") as f:
    f.write(readable)

With many thanks to my good friend Eddy.