Using adb, create a backup of the app using the following command:
adb backup -f freeotp-backup.ab -apk org.fedorahosted.freeotporg.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.tarThen extract the generated tar file:
$ tar xvf freeotp-backup.tar
apps/org.fedorahosted.freeotp/_manifest
apps/org.fedorahosted.freeotp/sp/tokens.xmlWe 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())
PLYQWM44SMKVTYW5There 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.
{"key": "{\"mCipher\":\"AES/GCM/NoPadding\",\"mCipherText\":[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],\"mParameters\":[48,17,4,12,88,92,127,-56,98,50,24,-50,-36,-50,-32,-47,2,1,26],\"mToken\":\"HmacSHA1\"}"} {"algo": "SHA1", "digits": 6, "issuerExt": "Forti VPN", "issuerInt": "Forti VPN", "label": "[email protected]", "period": 30, "type": "TOTP"} {"mAlgorithm": "PBKDF2withHmacSHA512", "mEncryptedKey": {"mCipher": "AES/GCM/NoPadding", "mCipherText": [-81, 26, 67, 112, 48, -66, 68, 22, 70, 117, 26, -14, -18, -76, 45, -55, -89, 93, -112, 23, 110, 82, -91, -47, -83, 79, -63, -122, -25, 18, -72, 87, -15, 106, -103, -9, 76, -106, 71, -35, -37, 47, -31, 116, 125, 124, -72, 13], "mParameters": [48, 17, 4, 12, 49, 105, -120, -75, 38, -47, 1, -48, 45, -16, 46, 107, 2, 1, 16], "mToken": "AES"}, "mIterations": 100000, "mSalt": [119, -4, -69, -28, 84, -38, 74, -17, 17, 14, 120, 37, -2, -67, -68, 49, 22, 8, -102, 12, 55, 91, -33, -111, -123, 111, 44, -92, 77, 121, 87, 5]}In order:
HmacSHA1TOTP 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:
{'mAlgorithm': 'PBKDF2withHmacSHA512', 'mEncryptedKey': {'mCipher': 'AES/GCM/NoPadding', 'mCipherText': [-81, 26, 67, 112, 48, -66, 68, 22, 70, 117, 26, -14, -18, -76, 45, -55, -89, 93, -112, 23, 110, 82, -91, -47, -83, 79, -63, -122, -25, 18, -72, 87, -15, 106, -103, -9, 76, -106, 71, -35, -37, 47, -31, 116, 125, 124, -72, 13], 'mParameters': [48, 17, 4, 12, 49, 105, -120, -75, 38, -47, 1, -48, 45, -16, 46, 107, 2, 1, 16], 'mToken': 'AES'}, 'mIterations': 100000, 'mSalt': [119, -4, -69, -28, 84, -38, 74, -17, 17, 14, 120, 37, -2, -67, -68, 49, 22, 8, -102, 12, 55, 91, -33, -111, -123, 111, 44, -92, 77, 121, 87, 5]}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