Skip to content

Instantly share code, notes, and snippets.

@dgobaud
Last active August 4, 2025 16:57
Show Gist options
  • Save dgobaud/a0bb94db77cc5ad800b592c44c16fea1 to your computer and use it in GitHub Desktop.
Save dgobaud/a0bb94db77cc5ad800b592c44c16fea1 to your computer and use it in GitHub Desktop.
How to Export Your Authy Tokens
import json
import urllib.parse
# Load source content from file
with open("decrypted_tokens.json", "r") as f:
authy_data = json.load(f)
# Convert to Bitwarden format
bitwarden_data = {"items": []}
for account in authy_data["decrypted_authenticator_tokens"]:
# Ensure all fields are strings or provide defaults
name = str(account.get("name", ""))
issuer = account.get("issuer") # May be None
secret = str(account.get("decrypted_seed", ""))
digits = str(account.get("digits", "6"))
# Construct TOTP URI
totp_uri = f"otpauth://totp/{urllib.parse.quote(issuer or '')}:{urllib.parse.quote(name)}?"
totp_uri += f"secret={urllib.parse.quote(secret)}&digits={digits}"
if issuer: # Only add issuer parameter if it exists
totp_uri += f"&issuer={urllib.parse.quote(issuer)}"
# Add to Bitwarden items
bitwarden_data["items"].append({
"name": name,
"type": 1, # Type 1 is for login items
"login": {
"username": name,
"totp": totp_uri
}
})
# Save to JSON file
with open("vaultwarden_import.json", "w") as f:
json.dump(bitwarden_data, f, indent=4)
print("Vaultwarden import file created: vaultwarden_import.json")
import json
import base64
import binascii # For base16 decoding
from getpass import getpass # For hidden password input
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
def decrypt_token(kdf_rounds, encrypted_seed_b64, salt, passphrase):
try:
# Decode the base64-encoded encrypted seed
encrypted_seed = base64.b64decode(encrypted_seed_b64)
# Derive the encryption key using PBKDF2 with SHA-1
kdf = PBKDF2HMAC(
algorithm=hashes.SHA1(),
length=32, # AES-256 requires a 32-byte key
salt=salt.encode(),
iterations=kdf_rounds,
backend=default_backend()
)
key = kdf.derive(passphrase.encode())
# AES with CBC mode, zero IV
iv = bytes([0] * 16) # Zero IV (16 bytes for AES block size)
cipher = Cipher(algorithms.AES(key), modes.CBC(iv), backend=default_backend())
decryptor = cipher.decryptor()
# Decrypt the ciphertext
decrypted_data = decryptor.update(encrypted_seed) + decryptor.finalize()
# Remove PKCS7 padding
padding_len = decrypted_data[-1]
padding_start = len(decrypted_data) - padding_len
# Validate padding
if padding_len > 16 or padding_start < 0:
raise ValueError("Invalid padding length")
if not all(pad == padding_len for pad in decrypted_data[padding_start:]):
raise ValueError("Invalid padding bytes")
# Extract the decrypted seed, base16 decode, and interpret as UTF-8 string
decrypted_seed_hex = decrypted_data[:padding_start].hex()
return binascii.unhexlify(decrypted_seed_hex).decode('utf-8') # Decode base16 and interpret as UTF-8
except Exception as e:
return f"Decryption failed: {str(e)}"
def process_authenticator_data(input_file, output_file, backup_password):
with open(input_file, "r") as json_file:
data = json.load(json_file)
decrypted_tokens = []
for token in data['authenticator_tokens']:
decrypted_seed = decrypt_token(
kdf_rounds=token['key_derivation_iterations'],
encrypted_seed_b64=token['encrypted_seed'],
salt=token['salt'],
passphrase=backup_password
)
decrypted_token = {
"account_type": token["account_type"],
"name": token["name"],
"issuer": token["issuer"],
"decrypted_seed": decrypted_seed, # Store as UTF-8 string
"digits": token["digits"],
"logo": token["logo"],
"unique_id": token["unique_id"]
}
decrypted_tokens.append(decrypted_token)
output_data = {
"message": "success",
"decrypted_authenticator_tokens": decrypted_tokens,
"success": True
}
with open(output_file, "w") as output_json_file:
json.dump(output_data, output_json_file, indent=4)
print(f"Decryption completed. Decrypted data saved to '{output_file}'.")
# User configuration
input_file = "authenticator_tokens.json" # Replace with your input file
output_file = "decrypted_tokens.json" # Replace with your desired output file
# Prompt for the backup password at runtime (hidden input)
backup_password = getpass("Enter the backup password: ").strip()
# Process the file
process_authenticator_data(input_file, output_file, backup_password)
@marcus-at-localhost
Copy link

Hey, just wanted to let you know that I've got a decryption error (utf8 something something) with your version of decrypt.py. After using the one from the repo it worked: https://github.com/AlexTech01/Authy-iOS-MiTM/blob/main/decrypt.py
thanks for the guide.

@St0bsel
Copy link

St0bsel commented Aug 4, 2025

Hey, just wanted to let you know that I've got a decryption error (utf8 something something) with your version of decrypt.py. After using the one from the repo it worked: https://github.com/AlexTech01/Authy-iOS-MiTM/blob/main/decrypt.py thanks for the guide.

same here. Used the linked repo and it worked. Thanks for the work all of you did!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment