Skip to content

Instantly share code, notes, and snippets.

@kontez
Forked from jleclanche/freeotp_backup.md
Created January 4, 2018 14:27
Show Gist options
  • Save kontez/05923f2fc208c6bbe3de81f28de571db to your computer and use it in GitHub Desktop.
Save kontez/05923f2fc208c6bbe3de81f28de571db to your computer and use it in GitHub Desktop.
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.

@manosnoam
Copy link

manosnoam commented Sep 2, 2022

To extract freeotp-backup.ab without "Android Backup Extractor", just run this:

( printf "\x1f\x8b\x08\x00\x00\x00\x00\x00" ; tail -c +25 freeotp-backup.ab ) |  tar xfvz -

You can ignore the gzip warring: unexpected end of file, it will still extract the file.

Credits to: https://stackoverflow.com/questions/18533567/how-to-extract-or-unpack-an-ab-file-android-backup-file

And thanks @albfan and @sfan5, your Json parser works great for FreeOTP+
https://gist.github.com/kontez/05923f2fc208c6bbe3de81f28de571db?permalink_comment_id=3955026#gistcomment-3955026

@Rockettsword
Copy link

Rockettsword commented Nov 13, 2022

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+.

Hi Sfan5,

kannst du mir helfen ich bin total am verzweifeln. Ich will ein OTP auf ein neues Handy übertragen, ich wollte das jetzt so machen wie du, indem ich es in OTP+ direkt übertrage, und dort kann ich ja vermutlich ein Backup machen davon weil OTP+ das ja zulässt.

Ich verstehe aber das alles hier nicht so, ich hab von der Materie auch nicht so viel Ahnung. Wie und wo gebe ich den von dir angegeben Code ein? Ich würde mich sehr freuen, wenn du mir das erklären könntest. Danke!!

Edit: Kann es sein das ich für das alle hier ein Linux Systen brauche? Ich versuch das grad alles in Windows Powershell

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