Skip to content

Instantly share code, notes, and snippets.

Embed
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
apps/org.fedorahosted.freeotp/_manifest
apps/org.fedorahosted.freeotp/sp/tokens.xml

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' ?>
<map>
	<!-- ... -->
	<string name="Discord:me@example.org">{&quot;algo&quot;:&quot;SHA1&quot;,&quot;counter&quot;:0,&quot;digits&quot;:6,&quot;imageAlt&quot;:&quot;content://com.google.android.apps.photos.contentprovider/-1/1/content%3A%2F%2Fmedia%2Fexternal%2Ffile%2F3195/ORIGINAL/NONE/741876674&quot;,&quot;issuerExt&quot;:&quot;Discord&quot;,&quot;issuerInt&quot;:&quot;Discord&quot;,&quot;label&quot;:&quot;me@example.org&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>
</map>

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://com.google.android.apps.photos.contentprovider/-1/1/content%3A%2F%2Fmedia%2Fexternal%2Ffile%2F3195/ORIGINAL/NONE/741876674&quot;,&quot;issuerExt&quot;:&quot;Discord&quot;,&quot;issuerInt&quot;:&quot;Discord&quot;,&quot;label&quot;:&quot;me@example.org&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)
{"algo":"SHA1","counter":0,"digits":6,"imageAlt":"content://com.google.android.apps.photos.contentprovider/-1/1/content%3A%2F%2Fmedia%2Fexternal%2Ffile%2F3195/ORIGINAL/NONE/741876674","issuerExt":"Discord","issuerInt":"Discord","label":"me@example.org","period":30,"secret":[122,-15,11,51,-100,-109,21,89,-30,-35],"type":"TOTP"}

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)
b'z\xf1\x0b3\x9c\x93\x15Y\xe2\xdd'

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())
PLYQWM44SMKVTYW5

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

@KeronCyst

This comment has been minimized.

Copy link

@KeronCyst KeronCyst commented Sep 5, 2019

Hello, I'm trying to move from FreeOTP to FreeOTP+, and I'm not sure of whether this guide is the way to do it (as in, how exactly the process would involve that decoded base32 string at the end).

@kontez

This comment has been minimized.

Copy link
Owner Author

@kontez kontez commented Sep 5, 2019

Hi, this guide is quite old and I have since moved to iOS. But that string you get in the last step is the secret you can import in your authenticator app. You can use it as an alternative to the QR Code.

@albfan

This comment has been minimized.

Copy link

@albfan albfan commented Sep 17, 2019

Long story short:

Install abe (choose your preferred aur installer):

yaourt -S android-backup-extractor-git

Save this python script to get-token.py:

#!/usr/bin/env python

import base64, json
import xml.etree.ElementTree as ET

verbose = False

root = ET.parse ('org.fedorahosted.freeotp/sp/tokens.xml').getroot()
for secrets in root.findall ('string'):
    name = secrets.get ('name')
    if name == 'tokenOrder':
        continue

    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()))

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
cd apps/
../get-token.py
secret name: token name
token secret base64: PLYQWM44SMKVTYW5
@pranlawate

This comment has been minimized.

Copy link

@pranlawate pranlawate commented Oct 22, 2021

For those who can't get the abe working because you are on windows machine(unfortunately) like me. Here is what works fine with me.
Taken from link https://thevaliantway.com/2018/08/freeotp-migration/

*Note : you need to use git bash or any similar command line which allows running linux commands

Multiline:
dd if=freeotp-backup.ab bs=1 skip=24 > compressed-data
printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" | cat - compressed-data | gunzip -c > decompressed-data.tar
tar -xvf decompressed-data.tar

One liner:
(printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" && dd if=freeotp-backup.ab bs=1 skip=24) | gunzip -c | tar -xvO apps/org.fedorahosted.freeotp/sp/tokens.xml > tokens.xml


The method above works well until the time you try to add entry manually to FreeOTP of FreeOTP+(my case) .
Problem is >>> Issuer is a mandatory field in the app.
Add any random text like redhat and then edit it later to remove it if you want.

The alternative that was suggested on various sites be used(did not work for me) is QR code generator which doesn't need the issuer: https://freeotp.github.io/qrcode.html
The issue ^^ here is counter value can't be put manually and defaults to 4.

@sfan5

This comment has been minimized.

Copy link

@sfan5 sfan5 commented Nov 8, 2021

If anyone's switching to FreeOTP+ it's also possible to directly import all tokens using the JSON format.
script modified from @albfan's:

#!/usr/bin/env python3
import base64, json, sys
import xml.etree.ElementTree as ET

tokens = []
token_order = []

root = ET.parse("apps/org.fedorahosted.freeotp/sp/tokens.xml").getroot()
for secrets in root.findall("string"):
    name = secrets.get("name")
    if name == "tokenOrder":
        continue

    #print("secret name:", name)
    tokens.append(json.loads(secrets.text))
    token_order.append(name)

json.dump({"tokenOrder": token_order, "tokens": tokens}, sys.stdout)

Copy the output into a json file, transfer it to the device and import it inside FreeOTP+.

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