Skip to content

Instantly share code, notes, and snippets.

@albfan albfan/.gitignore forked from kontez/
Last active Sep 26, 2019

What would you like to do?
A guide to back up and recover 2FA tokens from FreeOTP (Android)

Backing up and recovering 2FA tokens from FreeOTP

Backing up FreeOTP

Using adb, create a backup of the app using the following command:

adb backup -f freeotp-backup.ab -apk org.fedorahosted.freeotp

org.fedorahosted.freeotp is the app ID for FreeOTP.

This will ask, on the phone, for a password to encrypt the backup. Proceed with a password.

Manually extracting the backup

The backups are some form of encrypted tar file. Android Backup Extractor can decrypt them. It's available on the AUR as android-backup-extractor-git.

Use it like so (this command will ask you for the password you just set to decrypt it):

abe unpack freeotp-backup.ab freeotp-backup.tar

Then extract the generated tar file:

$ tar xvf freeotp-backup.tar

We don't care about the manifest file, so let's look at apps/org.fedorahosted.freeotp/sp/tokens.xml.

Reading tokens.xml

The tokens.xml file is the preference file of FreeOTP. Each <string>...</string> is a token (except the one with the name tokenOrder).

The token is a JSON blob. Let's take a look at an example token (which is no longer valid!):

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
	<!-- ... -->
	<string name="">{&quot;algo&quot;:&quot;SHA1&quot;,&quot;counter&quot;:0,&quot;digits&quot;:6,&quot;imageAlt&quot;:&quot;content://;,&quot;issuerExt&quot;:&quot;Discord&quot;,&quot;issuerInt&quot;:&quot;Discord&quot;,&quot;label&quot;:&quot;;,&quot;period&quot;:30,&quot;secret&quot;:[122,-15,11,51,-100,-109,21,89,-30,-35],&quot;type&quot;:&quot;TOTP&quot;}</string>

Let's open a python shell and get the inner text of the XML into a Python 3 shell. We'll need base64, json and html in a moment:

>>> import base64, json, html
>>> s = """{&quot;algo&quot;:&quot;SHA1&quot;,&quot;counter&quot;:0,&quot;digits&quot;:6,&quot;imageAlt&quot;:&quot;content://;,&quot;issuerExt&quot;:&quot;Discord&quot;,&quot;issuerInt&quot;:&quot;Discord&quot;,&quot;label&quot;:&quot;;,&quot;period&quot;:30,&quot;secret&quot;:[122,-15,11,51,-100,-109,21,89,-30,-35],&quot;type&quot;:&quot;TOTP&quot;}"""

We decode all those HTML entities from the XML encoding:

>>> s = html.unescape(s); print(s)

What we specifically need from this is the secret. It's a signed byte array from Java... Let's grab it:

>>> token = json.loads(s); print(token["secret"])
[122, -15, 11, 51, -100, -109, 21, 89, -30, -35]

Now we have to turn this into a Python bytestring. For that, these bytes need to be turned back into unsigned bytes. Let's go:

>>> secret = bytes((x + 256) & 255 for x in token["secret"]); print(secret)

Finally, the TOTP standard uses base32 strings for TOTP secrets, so we'll need to turn those bytes into a base32 string:

>>> code = base64.b32encode(secret); print(code.decode())

There we go. PLYQWM44SMKVTYW5 is our secret in a format we can manually input into FreeOTP or Keepass.

In a nutshell

Install abe (choose your preferred aur installer):

yaourt -S android-backup-extractor-git

Get your token:

adb backup -f freeotp-backup.ab -apk org.fedorahosted.freeotp
abe unpack freeotp-backup.ab freeotp-backup.tar
tar xvf freeotp-backup.tar
secret name: token name
token secret base64: PLYQWM44SMKVTYW5

There's a verbose flag on python script if you want all the details

#!/usr/bin/env python
import base64, json
import xml.etree.ElementTree as ET
verbose = False
root = ET.parse ('apps/org.fedorahosted.freeotp/sp/tokens.xml').getroot()
for secrets in root.findall ('string'):
name = secrets.get ('name')
if name == 'tokenOrder':
secret_json = secrets.text
print ("secret name: {}".format(name))
if verbose: print ("secret json: {}".format(secret_json))
token = json.loads(secret_json);
token_secret = token["secret"]
if verbose: print("token secret: {}".format(token_secret))
secret = bytes((x + 256) & 255 for x in token_secret)
if verbose: print("token secret bytes {}".format(secret))
code = base64.b32encode(secret)
print("token secret base64: {}".format(code.decode()))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.