Decrypting TOTP keys from FortiToken for Android

One of my frequently-used services requires me to use FortiToken Mobile for 2FA authentication. While I'm glad they required 2FA in order to access their service, I'm less glad that they required 2FA by means of a closed-source, proprietary app1. I already use Aegis for all my 2FA needs, why do I need to download a second app just to access one service?

Plus, the UX for the FortiToken app leaves a lot to be desired, especially compared to Aegis. So I wondered if it would be possible to transfer the proprietary FortiToken keys into a standard format accepted by Aegis.

The first thing I had to do was figure out whether FortiToken used an open standard like TOTP under the hood or if it was truly proprietary. I noticed that the app worked offline and also allowed you to generate TOTP-compliant codes from other services. This made me reasonably confident that, under the hood, it was just using TOTP itself. Then I found this description in their marketing materials which basically confirms it:

Evidence for TOTP support from FortiToken marketing materials

Score! Looks like they're using standard TOTP. Once I started digging around the app files, I was able to confirm that the particular key I was after used TOTP/SHA-1/60 seconds.

I believe the TOTP secrets are provisioned by a Fortinet server. The proprietary app is still needed to communicate with this server during the initial setup process. However, once the secret is provisioned, the app no longer needs a network connection and the TOTP secret can be extracted as described below.

I started looking through the app's files, and (unsurprisingly) found that TOTP keys appeared to be encrypted. It wasn't clear to me how the data was encrypted, or where the decryption key was stored. So I used jadx to decompile the app and started looking through the decompiled code.

Eventually I found this promising-looking method in TokenContentProvider.java:

private static String m366b(Cursor cursor) {
    String a = C0706cr.m130a(cursor, "type");
    String a2 = C0706cr.m130a(cursor, "seed");
    int b = C0706cr.m133b(cursor, "otp_period");
    int b2 = C0706cr.m133b(cursor, "digits");
    int b3 = C0706cr.m133b(cursor, "counter");
    String p = C0753ek.m244p(a2);
    if (C0749eg.TIME_BASED_TOKEN.f370gr.equalsIgnoreCase(a)) {
        return C0748ef.m188b(p, b, b2);
    }
    if (C0749eg.COUNTER_BASED_TOKEN.f370gr.equalsIgnoreCase(a)) {
        return C0748ef.m185a(p, b3, b2);
    }
    return null;
}

I started going through the call stack for C0748ef.m188b(), and eventually was able to decrypt the TOTP secret. Here's how.

This procedure works on FortiToken Mobile version 5.0.2.0025, which is the latest version at the time of publication. This procedure was carried out on Android 10 and requires root. I'm not sure if a similar procedure works on iOS.

Step 1: Extract the encrypted secret

FortiToken stores its encrypted TOTP keys in an sqlite database located at /data/data/com.fortinet.android.ftm/databases/FortiToken.db. To pull out the encrypted key data, use the following sqlite query:

SELECT name, seed, otp_period, digits FROM Account WHERE type="totp"

which should give you something like the following:

FortiToken Key|MNmAN7drtlNJxjFqo5bgSN/DZcdWVK9Qv1YyUP3OjuJkDXgV06siQYlQfO0678Lg|60|6

Pull out the second value2 - that's our encrypted, base64-encoded TOTP seed. In this case, MNmAN7drtlNJxjFqo5bgSN/DZcdWVK9Qv1YyUP3OjuJkDXgV06siQYlQfO0678Lg. (Yours will differ, of course.)

Step 2: Extract the encrypted UUID

FortiToken uses a concatenation of a UUIDv4 and a device-specific identifier as an encryption key for your TOTP secrets. We need to rebuild this key in order to decrypt the data, so the first thing we have to do is pull out the UUID portion.

The encrypted UUID is stored in the UUID key of the XML file stored at /data/data/com.fortinet.android.ftm/shared_prefs/FortiToken_SharedPrefs_NAME.xml.

That looks like this:

<string name="UUID">N7gAr30eX72sR2owbVR4WrFiw4e3ignGBO6IcgA4qJjvBYjZvIxZXIMTHOix8QDt</string>

So our encrypted UUID, in this case, is N7gAr30eX72sR2owbVR4WrFiw4e3ignGBO6IcgA4qJjvBYjZvIxZXIMTHOix8QDtB. Let's decrypt it!

Step 3: Extract your device ID and serial

The decryption key for the UUID is a combination of a serial value and a device-specific ID.3

The serial value is stored in the same XML file, at /data/data/com.fortinet.android.ftm/shared_prefs/FortiToken_SharedPrefs_NAME.xml, with the key SerialNumberPreAndroid9:

<string name="SerialNumberPreAndroid9">TOKENSERIALunknown</string>

I believe this value will always be set to "TOKENSERIALunknown" unless the app was installed on Android versions < 9.

Next, we have to get our device ID. Since Android 10, Android reports a different device ID to each app, as a security measure to prevent cross-app tracking. So we need to pull this ID out. It is stored in /data/system/users/0/settings_ssaid.xml. Find the line with package="com.fortinet.android.ftm" and extract the ID:

<setting id="88" name="12188" value="eefd7d4837294e94" package="com.fortinet.android.ftm" defaultValue="eefd7d4837294e94" defaultSysSet="false" tag="null" />

In this case, our device ID is eefd7d4837294e94.

Step 4: Decrypt the UUID

Remove the first 11 characters from the serial, and prepend the device ID to generate the decryption key for the UUID.

uuid_key = DEVICE_ID + SERIAL[11:]

In our case, our UUID key is eefd7d4837294e94unknown.

It's encrypted using AES/CBC/PKCS5, let's decrypt it:

import base64
import hashlib
from Crypto.Cipher import AES

def unpad(s):
    return s[0:-ord(s[-1])]

def decrypt(cipher, key):
    sha256 = hashlib.sha256()
    sha256.update(bytes(key, 'utf-8'))
    digest = sha256.digest()
    iv = bytes([0] * 16)
    aes = AES.new(digest, AES.MODE_CBC, iv)
    decrypted = aes.decrypt(base64.b64decode(cipher))
    return unpad(str(decrypted, "utf-8"))

print(decrypt('N7gAr30eX72sR2owbVR4WrFiw4e3ignGBO6IcgA4qJjvBYjZvIxZXIMTHOix8QDt', 'eefd7d4837294e94unknown'))

This gives us our UUID, bbc350195b88433dbcc7365cdbd130e5.

Step 5: Decrypt the TOTP secret

Now that we have the decrypted UUID, we can decrypt our TOTP secret! Add the decrypted UUID to the end of the UUID key. In our case, this gives us eefd7d4837294e94unknownbbc350195b88433dbcc7365cdbd130e5. Use that as the key to decrypt the TOTP secret.

print(decrypt('MNmAN7drtlNJxjFqo5bgSN/DZcdWVK9Qv1YyUP3OjuJkDXgV06siQYlQfO0678Lg', 'eefd7d4837294e94unknownbbc350195b88433dbcc7365cdbd130e5'))

That gives us this value: 1900309085190030908519003090851900309085. This is the hexadecimal representation of the base32-decoded TOTP secret value. Let's convert it to hex bytes, then base32 encode it, to transform it into a string we can actually use in our standard TOTP app.

import base64

totp_secret = bytes.fromhex(decrypted_seed)

totp_secret_encoded = str(base64.b32encode(totp_secret), "utf-8")
print(totp_secret_encoded)

This gives us our proper base32-encoded TOTP secret: DEADBEEFDEADBEEFDEADBEEFDEADBEEF.

You can type this secret into any TOTP-compliant app, set the algorithm and time period, and you no longer need the FortiToken app! (Double check the output matches between both apps before deleting the FortiToken app.)

Putting it all together, here's a script you can run to do all the decryption/encoding for you (requires pycrypto):

Source Code

import hashlib
import base64
from Crypto.Cipher import AES

SEED = 'MNmAN7drtlNJxjFqo5bgSN/DZcdWVK9Qv1YyUP3OjuJkDXgV06siQYlQfO0678Lg'
UUID = 'N7gAr30eX72sR2owbVR4WrFiw4e3ignGBO6IcgA4qJjvBYjZvIxZXIMTHOix8QDt'
DEVICE_ID = 'eefd7d4837294e94'

SERIAL = 'TOKENSERIALunknown'

def unpad(s):
    return s[0:-ord(s[-1])]

def decrypt(cipher, key):
    sha256 = hashlib.sha256()
    sha256.update(bytes(key, 'utf-8'))
    digest = sha256.digest()
    iv = bytes([0] * 16)
    aes = AES.new(digest, AES.MODE_CBC, iv)
    decrypted = aes.decrypt(base64.b64decode(cipher))
    return unpad(str(decrypted, "utf-8"))

uuid_key = DEVICE_ID + SERIAL[11:]
print("UUID KEY: %s" % uuid_key)
decoded_uuid = decrypt(UUID, uuid_key)
print("UUID: %s" % decoded_uuid)

seed_decryption_key = uuid_key + decoded_uuid
print("SEED KEY: %s" % seed_decryption_key)
decrypted_seed = decrypt(SEED, seed_decryption_key)

totp_secret = bytes.fromhex(decrypted_seed)

totp_secret_encoded = str(base64.b32encode(totp_secret), "utf-8")
print("TOTP SECRET: %s" % totp_secret_encoded