Extracting TOTP keys from a proprietary Android 2FA app
This is an analysis of an early 2010s proprietary Android-based two-factor authentication (2FA) application for a particular cloud service provider – à la Okta, or Microsoft Authenticator. This particular cloud service has been publicly criticised for not supporting industry standard 2FA algorithms such as time-based one-time password (TOTP).
Interestingly, many such proprietary 2FA applications internally use TOTP; for example, Okta. In such cases, extracting the TOTP shared secret key enables 2FA tokens to be generated from standard TOTP software. This 2FA application turned out to be no exception.
Teardown
This application was distributed a single APK file, and featured no appreciable obfuscation, so could easily be decompiled using a tool like jadx.
A full-text search for ‘OTP’ reveals the following relevant code:
public static String generateToken(Context context) {
String secretKey = EncryptionHelper.readSecretKeyValue(context);
// ...
long otp_state = mOtpProvider.getTotpCounter().getValueAtTime(currentTime());
byte[] keyBytes = Base32String.decode(secretKey);
Mac mac = Mac.getInstance("HMACSHA1");
mac.init(new SecretKeySpec(keyBytes, ""));
TokenGenerator tokgen = new TokenGenerator(mac, 6);
return tokgen.generateResponseCode(otp_state);
}
So it appears likely that the application is generating 2FA tokens with standard 6-digit TOTP, using the usual HMAC-SHA1 function. The secret key is read from the function EncryptionHelper.readSecretKeyValue. Inspecting this function, we find:
public static String readSecretKeyValue(Context context) {
// Read encrypted value from file
String ciphertextString = readFileToString("totp_secretkey", context);
byte[] ciphertext = Base64.decode(ciphertextString, Base64.NO_PADDING | Base64.NO_WRAP);
// Get encryption key from KeyStore
KeyStore.Entry entry = getKeyStoreEntry("totp_secretkey", context);
SecretKey key = ((KeyStore.SecretKeyEntry) entry).getSecretKey();
// Get encryption IV from shared preferences
SharedPreferences sp = context.getSharedPreferences(/* ... */);
String ivString = sp.getString("totp_secretkey_iv", "");
byte[] iv = Base64.decode(ivString, Base64.NO_PADDING | Base64.NO_WRAP);
// Decrypt using AES/CBC/PKCS5Padding
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
cipher.init(2, key, new IvParameterSpec(iv));
byte[] plaintext = cipher.doFinal(ciphertext);
return new String(plaintext);
}
This appears to describe a roundabout method of obtaining the secret key. First, an encrypted version of the secret key is read from a file, then it is decrypted using an encryption key sourced from a Java KeyStore. The encryption algorithm is, clearly, AES-CBC with PKCS#5 padding, and the initialisation vector for the encryption is read from the Android shared preferences.
We can locate the relevant files on the Android device at /data/data/com.example.packagename, which requires us to have root access to the Android device. There, we find files/totp_secretkey, files/keystore.ks and shared_prefs/com.example.packagename.prefs_file.xml as required. The encrypted secret key and initialisation vector are easily read. The keystore, containing the encryption key, poses a slight difficulty. It does not appear to be a standard Java KeyStore:
$ keytool -list -keystore keystore.ks
keytool error: java.security.KeyStoreException: Unrecognized keystore format. Please load it with a specified type
$ file keystore.ks
keystore.ks: data
Inspecting the contents of the keystore.ks file, we obtain:1
$ xxd keystore.ks
00000000: 0000 0002 0000 0014 e744 652a b81c 3ff5 .........De*..?.
00000010: dc0e 078a 88dd c797 f602 fe44 0000 05a1 ...........D....
00000020: 0400 0e74 6f74 705f 7365 6372 6574 6b65 ...totp_secretke
00000030: 7900 0001 8920 9edf e500 0000 0000 0000 y.... ..........
00000040: 4c00 0000 1452 1a55 4af8 6eb0 03f0 833b L....R.UJ.n....;
00000050: 5907 e3bb 0499 b449 2600 0006 b93b 9682 Y......I&....;..
00000060: b0d6 60ae 34b8 58f5 0b50 5ff3 80e2 ec86 ..`.4.X..P_.....
00000070: 9b3d 1a23 26c6 846a edb6 5eef 91c4 4a9e .=.#&..j..^...J.
00000080: 730b 65aa 34fd 86ba d030 af0f 3c00 4f33 s.e.4....0..<.O3
00000090: 6773 b464 dbcb c017 8f96 e007 504d 6694 gs.d........PMf.
000000a0: 1560
We know from the Android KeyStore documentation that one of formats supported by Android for a keystore file is the BKS format, used by the BouncyCastle library. We can identify that the BKS format begins with a 4-byte version number, and ends with a null byte followed by a SHA1 hash. That is what we have in our keystore.ks file, and so we can surmise that this is a BKS format keystore.
We can then simply use a tool which can read BKS keystores, such as KeyStore Explorer. This allows us to obtain the encryption key:1
It is now a simple task to decrypt the TOTP secret key; for example, using PyCryptodome:
from Crypto.Cipher import AES
import base64
TOTP_SECRET = ...
TOTP_SECRET_KEY = ...
TOTP_SECRET_IV = ...
def b64decode_nopadding(s):
return base64.b64decode(s + '=' * (-len(s) % 4))
cipher = AES.new(bytes.fromhex(TOTP_SECRET_KEY), AES.MODE_CBC, b64decode_nopadding(TOTP_SECRET_IV))
pt = cipher.decrypt(b64decode_nopadding(TOTP_SECRET))
print(pt)
This secret key could then be loaded into any standard TOTP application, to obviate the need to use the proprietary 2FA application.