With the recent changes to LastPass Free limiting the types of devices you can concurrently use with one account, I decided now was the time to migrate to a new service. I settled on a self-hosted instance of Bitwarden, specifically the bitwarden_rs implementation in Rust that allows me to run it within a single Docker container.

One new feature of Bitwarden I was particularly happy with was the integration of a TOTP generator. I have been using Authy for years, but it relies on your phone number, and with the rise in SIM swap attacks it seems like it might be time to move on to something else.

Unfortunately, Authy does not currently provide an easy way to export your OTP secrets. I have previously written about this and provided a method for extracting your secrets using mitmproxy, but with the changes to trusted CAs in Android Nougat, that method no longer works.

Fortunately, I found a new method in a Github Gist by gboudreau that uses the Electron remote debugging port on the desktop app to dump the decrypted secrets.

As with my last guide, follow this guide at your own risk. I am not responsible for any damages or lost data. You should always ensure your 2FA codes work before removing them from another 2FA app.

Method

First, download the Authy Desktop app and log in. This may require you to enable multi-device support in the settings of the mobile app. Once you have logged into your account, make sure to decrypt your accounts by entering your backup password.

After you have decrypted your accounts in the app, you can close the app and open a terminal window. I did this on macOS, but the Gist has commands for Windows and Linux as well.

open -a "Authy Desktop" --args --remote-debugging-port=5858

This will reopen the Authy Desktop app with debug server running on the specified port. You can now navigate to the debug page, http://localhost:5858/, and click on the only link, Twilio Authy.

You will see a debug view of the Authy Desktop app, and a DevTools window. In DevTools, on Application, then under Frames expand Top. Right-click on main.html and click Open in containing folder.

This should open a new console window. If you want to manually dump your data appManager.getModel() contains your accounts. Standard accounts will have a decryptedSeed field containing your TOTP secret. Authy specific accounts contain a secretSeed field in hexadecimal that needs to be converted to Base32 to be used in other 2FA apps.

Many of the scripts in the Gist used a large minified library, or an external service to generate QR codes to be scanned by your new 2FA app. I just wanted the TOTP URIs, so I wrote a modified version.

First I pasted a modified version of base32-encode by LinusU into the console window. This takes a hexadecimal string and converts it into a byte array, then reencodes it into Base32.

function hex_to_b32(hex) {
    let alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
    let bytes = [];

    for (let i = 0; i < hex.length; i += 2) {
        bytes.push(parseInt(hex.substr(i, 2), 16));
    }

    let bits = 0;
    let value = 0;
    let output = '';

    for (let i = 0; i < bytes.length; i++) {
        value = (value << 8) | bytes[i];
        bits += 8;

        while (bits >= 5) {
            output += alphabet[(value >>> (bits - 5)) & 31];
            bits -= 5;
        }
    }

    if (bits > 0) {
        output += alphabet[(value << (5 - bits)) & 31];
    }

    return output;
}

Then I pasted in my modified script to iterate over all my accounts, determine their type, and print a TOTP URI to the console.

appManager.getModel().forEach(function (i) {
    let secret = (i.markedForDeletion === false ? i.decryptedSeed : hex_to_b32(i.secretSeed));
    let period = (i.digits === 7 ? 10 : 30);
    let totp_uri = `otpauth://totp/${encodeURIComponent(i.name)}?secret=${secret}&digits=${i.digits}&period=${period}`;
    console.log(totp_uri);
});

This script will dump all your accounts in the following format.

otpauth://totp/Github?secret=REDACTED&digits=6&period=30

You can optionally add additional parameters, such as the issuer=SOME_ISSUER parameter. You can see the full URI format in the Google Authenticator wiki. If your new 2FA app does not allow for URI imports, you can also put the URI into a trusted QR code generator and scan it to import.