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.
@sanodin
Looks like they changed the format quite a bit.
Looking at this, you have three entries: a [uuid], a [uuid]-token, and a masterKey. It seems similar to what was there before, except that now the secret is encrypted.
In order:
HmacSHA1
TOTP secret, but it's been encrypted withAES/GCM/NoPadding
. The ciphertext bytes are[46, -7, 16, -72, 110, -85, 15, -24, 23, 29, 61, 102, -38, 127, 60, -101, 0, -86, -83, -63, -93, 101, 23, 85, -81, 74, -62, -111, 109, 64, -55, 122, -94, 80, 123, 104]
and parameter bytes[48, 17, 4, 12, 88, 92, 127, -56, 98, 50, 24, -50, -36, -50, -32, -47, 2, 1, 26]
.{'algo': 'SHA1', 'digits': 6, 'issuerExt': 'Forti VPN', 'issuerInt': 'Forti VPN', 'label': '[email protected]', 'period': 30, 'type': 'TOTP'}
This is the master key data from your xml:
I would try to recreate the key from that, but if I'm reading this correctly, the key itself is encrypted -- I would wager, by Android's own encryption utilities; if that's the case, you'd need to really mess around your phone to decrypt that.
Of course, if it is the case, I'm kinda wondering why they're doing this round-about "encrypt the secret, but store the key next to it, but encrypt that key" thing. Maybe I'm missing something.
Good luck