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.
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
.
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:[email protected]">{"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":"[email protected]","period":30,"secret":[122,-15,11,51,-100,-109,21,89,-30,-35],"type":"TOTP"}</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 = """{"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":"[email protected]","period":30,"secret":[122,-15,11,51,-100,-109,21,89,-30,-35],"type":"TOTP"}"""
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":"[email protected]","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.
FreeOTP got "Export to json" option now, info for new comers.