Skip to content

Export from Twilio Authy using Android & root!

Published:

In 2020, I was introduced to Authy when I faced repeated blocked login attempts from somewhere in Russia on my Facebook account. Desperate to secure my account, I enabled two-factor authentication (2FA) and needed a way to store my TOTP (Time-based One-Time Password) codes. Authy caught my eye with its user-friendly interface and reliable backup and sync capabilities. Plus, it was backed by Twilio, a reputable company, so it seemed like a solid choice.

Fast forward to March 2024, and the unexpected happened: Twilio deprecated the Authy desktop app & old API endpoints. This move broke scripts that allowed users to export TOTP secrets from Authy using the desktop app or using the deprecated APIs, leaving us without an easy way to retrieve our codes or migrate to another authenticator.

Determined to find a solution, I turned to the Android app for answers. Here’s a step-by-step guide on how I managed to extract my TOTP codes from the Authy Android app.

Exploring the Android App

Knowing that the TOTP codes had to be stored somewhere within the /data directory, I realized that accessing this data would require root privileges on my Android device.

I had a spare phone that I had been using for testing custom ROMs, so I decided to log into the Authy app on that device and root the phone using Magisk. A quick disclaimer: rooting your device will void its warranty, and the steps to do so can vary depending on the device. Proceed at your own risk—I’m not responsible for any potential losses.

After successfully rooting the device, I navigated to the preferences directory of the Authy app located at:

/data/user/0/com.authy.authy/shared_prefs/

This directory is only accessible with superuser permissions. Using adb (Android Debug Bridge) from my laptop, I opened a superuser shell and navigated to the directory.

Exploring the Directory

Once inside the adb shell, I listed the files in the directory:

ls -1

FirebasePerfSharedPrefs.xml
SignUpRegistrationPreferences.xml
...
com.authy.storage.tokens.authenticator.xml
com.authy.storage.tokens.authy.xml
com.authy.storage.tokens_config_v2.xml
com.authy.storage.tokens_config_version.xml
com.authy.storage.tokens_grid_comparator.xml
...

Among the files, com.authy.storage.tokens.authenticator.xml caught my attention. Upon examining its contents, I found a JSON string encapsulated within the XML structure:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <int name="key_version" value="1046" />
    <string name="com.authy.storage.tokens.authenticator.key">
    ### JSON CONTENT REDACTED ###
    </string>
</map>

Extracting the TOTP Codes

The JSON data contained within the XML file had all the necessary TOTP information arranged in a array of objects:

// the inner object structure can be slightly different for some entries.
[
  {
    "accountType": "...",
    "decryptedSecret": "...",
    "digits": "...",
    "encryptedSecret": "...",
    "key_derivation_iterations": "...",
    "lastLogoVerificationTime": "...",
    "logo": "...",
    "timestamp": "...",
    "salt": "...",
    "upload_state": "...",
    "hidden": "...",
    "id": "...",
    "isNew": "...",
    "name": "...",
  },
  ...
]

To transfer this file to my system, I had to copy it to the SD card directory since adb pull won’t work directly in a privileged directory. Here’s how I did it:

# Inside adb shell
$ cp com.authy.storage.tokens.authenticator.xml /storage/emulated/0/delete-me-later.xml

Then, from my laptop:

$ adb pull /storage/emulated/0/delete-me-later.xml

Parsing the Data

With the file on my laptop, I wrote a Python script to parse the XML, extract the JSON data, and convert it into a format that can be imported into other authenticators, this one produces a new-line seperated list of totp uri’s, which is supported in ente.io’s authenticator. Here’s the script:

extract.py

import xml.etree.ElementTree as ET
import json
import urllib.parse

def extract_json_from_xml(file_path):
    tree = ET.parse(file_path)
    root = tree.getroot()

    json_element = root.find(".//string[@name='com.authy.storage.tokens.authenticator.key']")

    if json_element is not None:
        json_string = json_element.text
        try:
            return json.loads(json_string)
        except json.JSONDecodeError:
            print("Error: Unable to parse JSON content")
            return None
    else:
        print("Error: JSON element not found in the XML file")
        return None

def create_totp_uri(entry):
    name = urllib.parse.quote(entry['name'])
    secret = entry['decryptedSecret'] if entry['decryptedSecret'] else entry['encryptedSecret']

    uri = f"otpauth://totp/{name}?secret={secret}"

    params = {
        'issuer': entry.get('originalIssuer', ''),
        'digits': entry['digits']
    }

    if entry.get('accountType'):
        params['accountType'] = entry['accountType']

    if entry.get('originalName') and entry['originalName'] != entry['name']:
        params['originalName'] = entry['originalName']

    for key, value in params.items():
        if value:
            uri += f"&{key}={urllib.parse.quote(str(value))}"
    return uri

# Extract JSON from XML
xml_file_path = 'delete-me-later.xml'  # Replace with your actual XML file path
json_data = extract_json_from_xml(xml_file_path)

if json_data:
    # Create TOTP URIs
    totp_uris = [create_totp_uri(entry) for entry in json_data]

    # Write URIs to output file
    with open('totp_uris.txt', 'w') as file:
        file.write('\n'.join(totp_uris))

    print("TOTP URIs have been written to totp_uris.txt")
else:
    print("Failed to extract JSON data from XML file")

Wrapping Up

Finally, I ran the script, and it successfully generated a text file containing all my TOTP URIs:

$ python extract.py
TOTP URIs have been written to totp_uris.txt

Now, I can easily import these URIs into my password manager of choice.

Here is an alternative script which print’s the QR codes for the TOTP uri’s in the console, which can be scanned by almost every 2FA App!

this library is required

pip3 install qrcode

import xml.etree.ElementTree as ET
import json
import urllib.parse
import qrcode

def extract_json_from_xml(file_path):
    tree = ET.parse(file_path)
    root = tree.getroot()

    json_element = root.find(".//string[@name='com.authy.storage.tokens.authenticator.key']")

    if json_element is not None:
        json_string = json_element.text
        try:
            return json.loads(json_string)
        except json.JSONDecodeError:
            print("Error: Unable to parse JSON content")
            return None
    else:
        print("Error: JSON element not found in the XML file")
        return None

def create_totp_uri(entry):
    name = urllib.parse.quote(entry['name'])
    secret = entry['decryptedSecret'] if entry['decryptedSecret'] else entry['encryptedSecret']

    uri = f"otpauth://totp/{name}?secret={secret}"

    params = {
        'issuer': entry.get('originalIssuer', ''),
        'digits': entry['digits']
    }

    if entry.get('accountType'):
        params['accountType'] = entry['accountType']

    if entry.get('originalName') and entry['originalName'] != entry['name']:
        params['originalName'] = entry['originalName']

    for key, value in params.items():
        if value:
            uri += f"&{key}={urllib.parse.quote(str(value))}"
    return uri

def display_qr_code_console(uri, name):
    qr = qrcode.QRCode(
        version=1,
        error_correction=qrcode.constants.ERROR_CORRECT_L,
        box_size=5,
        border=2,
    )
    qr.add_data(uri)
    qr.make(fit=True)

    print(f"\nQR Code for {name}:\n")
    qr.print_ascii(invert=True)

xml_file_path = 'delete-me-later.xml'
json_data = extract_json_from_xml(xml_file_path)

if json_data:
    for entry in json_data:
        uri = create_totp_uri(entry)
        display_qr_code_console(uri, entry['name'])

    print("\nQR Codes have been displayed for each TOTP entry.")
else:
    print("Failed to extract JSON data from XML file")

Happy migrating!