Skip to content

Instantly share code, notes, and snippets.

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.

Copy link

konfou commented Mar 17, 2019

All combined:

import html, json, base64
def decode(s):
    s = html.unescape(s)
    token = json.loads(s)
    secret = bytes((x + 256) & 255 for x in token["secret"])
    code = base64.b32encode(secret)

And for every token in the tokens.xml file:

import xml.etree.ElementTree, json, base64
tree = xml.etree.ElementTree.parse('tokens.xml')
tokens = tree.getroot().findall('string')
for t in tokens:
    n = t.get('name') 
    if n == "tokenOrder":
    token = json.loads(t.text)
    secret = bytes((x + 256) & 255 for x in token["secret"])
    code = base64.b32encode(secret)

Copy link

johndescs commented Mar 28, 2019

Also if you don't want to go through backup/unpack, you can just cat data/data/org.fedorahosted.freeotp/shared_prefs/tokens.xml from the adb shell. At least, works on my phone… path or way to get a shell may vary I suppose.

Copy link

AnatomicJC commented Dec 26, 2021

Thanks Man ! You helped me a lot to transfer an older account from FreeOTP to another TOTP app. This fucking app has no easy transfer option.
Thanks again ! thanks thanks thanks thanks 😽

Copy link

jleclanche commented Dec 26, 2021

@AnatomicJC glad it was helpful :)

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