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
.