← Back to list

Inside the Android Keystore

Pablo Ajo · 2026-04-14 07:58 · 1 claps · 6.4 min read
#android #cryptography #reverse-engineering #pentesting
Open on Medium ↗
Wiki topics: CRY · Crypto & Web3 🔒 · Cybersecurity

Inside the Android Keystore

When you’re reversing an Android app, hooking Cipher.doFinal() with Frida gives you plaintext in seconds. No key needed. So why would an attacker ever care about extracting the key itself?

Because plaintext is passive. A key is active.

With intercepted plaintext you can read what the app decrypted on this device, in this session. With the key you can encrypt and decrypt anything, anywhere, at any time, without the original device, without Frida running, and without the user being involved at all. Here are some examples of why keys are important.

Device binding. Some apps generate a key on first launch and register it with the server as “this is my device”. Every request carries a signature made with that key. If you extract the key, you can make requests from another device that the server treats as the original.

Anti-fraud tokens. Financial and payment apps sometimes use a key to prove that an operation came from a specific previously registered device. With the key, that mechanism is useless.

That’s why cryptographic key storage matters beyond the obvious use case of encrypting local data. Keys are identity. Whoever holds the key holds the device’s trusted position with the server.

What It Is

The Android Keystore is the system that lets apps store cryptographic keys in a way that makes them difficult or impossible to extract. It’s not a file or a database in the traditional sense. It’s a provider backed by a daemon (keystored) that forwards operations to the underlying hardware or software implementation.

From an app’s perspective, you ask the Keystore to generate a key, give it an alias, and then use that alias to perform cryptographic operations. The actual key material never passes through your process:

// Generate a key — the alias is the only handle you get
KeyGenerator keyGen = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");
keyGen.init(new KeyGenParameterSpec.Builder("my_key",
        KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT).build());
keyGen.generateKey();

// Use it later — you never touch the raw bytes
SecretKey key = (SecretKey) KeyStore.getInstance("AndroidKeyStore").getKey("my_key", null);
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] ciphertext = cipher.doFinal(plaintext);

The app calls cipher.doFinal(), the call goes to the Keystore daemon, the daemon sends it to the TEE (hardware-isolated secure processor) or software layer, and the result comes back. At no point does the app hold the raw key bytes.

Where Keys Actually Live

Not all Keystore keys are equal. A key can be backed by software or by hardware, and the difference is significant.

Software-backed keys are stored as encrypted blobs on disk, under /data/misc/keystore/. The encryption uses a device-specific master key. If you have root and can get to that master key, you can extract the key material. These keys can also be included in device backups.

TEE-backed keys are generated inside the Trusted Execution Environment and never leave it. The key material exists only in the TEE’s isolated memory. Root access on the Android side cannot reach it. Even a fully compromised kernel cannot extract a TEE-backed key.

StrongBox keys go one level further. Instead of the TEE (which runs on the same SoC, System on a Chip, as Android, isolated via ARM TrustZone), StrongBox keys live in a dedicated secure processor, physically separate from the main chip. Pixel phones use Google’s Titan M/M2 chip for this. StrongBox also enforces hardware rate limiting: five incorrect authentication attempts trigger a 30-second lockout, and after 139 attempts the lockout extends to 24 hours per attempt. Brute force is not a viable attack.

To check programmatically which level a key is at:

// Load the private key by alias from the Keystore
KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
ks.load(null);
PrivateKey privateKey = (PrivateKey) ks.getKey("my_key", null);

// EC = Elliptic Curve, the algorithm this key was generated with
KeyFactory factory = KeyFactory.getInstance("EC", "AndroidKeyStore");
KeyInfo info = (KeyInfo) factory.getKeySpec(privateKey, KeyInfo.class);

int level = info.getSecurityLevel();
// KeyProperties.SECURITY_LEVEL_SOFTWARE = 0
// KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT = 1
// KeyProperties.SECURITY_LEVEL_STRONGBOX = 2

Key Types

The Keystore supports three categories:

Symmetric keys (AES, HMAC): AES in 128 or 256 bits, HMAC with SHA-256/384/512. Used for encryption and message authentication. Generated with KeyGenerator.

Asymmetric keys (RSA, EC): RSA at 2048+ bits, EC on P-256. Used for signing and key agreement. Generated with KeyPairGenerator. Only the private key is protected inside the Keystore. The public key can be freely exported.

Imported keys (Android 9+): Keys generated outside the device can be imported via a wrapping mechanism. An asymmetric key with PURPOSE_WRAP_KEY decrypts the wrapped key material inside the TEE. Once imported, the key is treated the same as a generated one.

Key Constraints

Keys aren’t just blobs. They carry a policy that the Keystore enforces on every operation. The most relevant constraints:

setUserAuthenticationRequired(true) requires the user to authenticate (biometric or PIN) before the key can be used. Without this, any process that can reach the Keystore daemon can use the key.

setUserAuthenticationValidityDurationSeconds(N) sets a window after authentication during which the key is available. A value of 0 means re-authenticate before every single operation. Higher values trade security for convenience.

setUnlockedDeviceRequired(true) prevents the key from being used while the device is locked, regardless of authentication state.

setIsStrongBoxBacked(true) forces the key into the secure processor. If the device doesn't have StrongBox, key generation fails outright.

setKeyValidityEnd(date) makes the Keystore refuse operations after a given date. Useful for keys that should rotate.

Key Attestation

When a key is generated in hardware, the Keystore can produce a certificate chain that proves it. A server that receives this chain can verify it against Google’s root and read the extension to confirm: this key was generated inside genuine Android secure hardware, with these exact constraints, on a device that was not running with an unlocked bootloader. It’s the foundation that Android’s device integrity checks (Play Integrity API) are built on, and it’s how payment processors and DRM systems remotely decide whether to trust a key on a device they don’t control. The full mechanics of attestation verification deserve their own article.

Finding Keys During RE

The first thing to do is enumerate what aliases the app is using. In smali, key aliases appear as string constants near KeyStore.getKey(), KeyGenerator.init(), or KeyGenParameterSpec.Builder calls:

grep -r "AndroidKeyStore" smali/
grep -r "getKey\|generateKey\|KeyGenParameterSpec" smali/
grep -r "\"key_\|\"aes_\|\"rsa_\|\"ec_" smali/

Common patterns: "key_payment", "biometric_key", "db_encryption_key", "signing_key". The alias tells you what the key is for before you look at anything else.

Dynamically, with Frida, you can list all aliases the app has registered:

Java.perform(() => {
    const KeyStore = Java.use("java.security.KeyStore");
    const ks = KeyStore.getInstance("AndroidKeyStore");
    ks.load(null);

    const aliases = ks.aliases();
    while (aliases.hasMoreElements()) {
        console.log("[+] Key alias: " + aliases.nextElement());
    }
});

To check whether a key is hardware-backed:

Java.perform(() => {
    const KeyFactory = Java.use("java.security.KeyFactory");
    const KeyInfo = Java.use("android.security.keystore.KeyInfo");

    const factory = KeyFactory.getInstance("EC", "AndroidKeyStore");
    const info = factory.getKeySpec(privateKey, KeyInfo.class);

    console.log("[+] Hardware backed: " + info.isInsideSecureHardware());
    console.log("[+] Security level: " + info.getSecurityLevel());
    // 0 = software, 1 = TEE, 2 = StrongBox
});

Can Keys Be Extracted?

Software-backed keys: Potentially yes. The encrypted blobs are on disk. With root access and the device-specific master key, key material can be decrypted. If the app stores key material in SharedPreferences or files (a common mistake), extraction is trivial even without root.

TEE-backed keys: No. Key material never leaves the TEE. With root on the Android side, you can intercept the operations that use the key, but you cannot get the key itself.

StrongBox keys: No. The separate secure processor and hardware rate limiting make both software and physical attacks impractical on modern devices.

What You Can Get by Hooking

When you can’t extract the key, the next layer is intercepting the operations that use it. Hooking Cipher.doFinal() gives you plaintext and ciphertext without ever touching the key:

Java.perform(() => {
    const Cipher = Java.use("javax.crypto.Cipher");

    Cipher.doFinal.overload("[B").implementation = function(input) {
        console.log("[+] Cipher.doFinal input: " + 
            Java.use("java.util.Arrays").toString(input));

        const result = this.doFinal(input);

        console.log("[+] Cipher.doFinal output: " + 
            Java.use("java.util.Arrays").toString(result));

        return result;
    };
});

The same applies to Signature.sign() to capture what's being signed and the resulting signature, and Mac.doFinal() for HMAC operations. You can reconstruct the full cryptographic flow of the app without knowing any key material.

Hooking Cipher.init() tells you which algorithm and mode are in use, and exposes the IV or nonce if one is passed:

Java.perform(() => {
    const Cipher = Java.use("javax.crypto.Cipher");

    Cipher.init.overload("int", "java.security.Key", 
        "java.security.spec.AlgorithmParameterSpec").implementation = 
        function(mode, key, params) {
            console.log("[+] Cipher.init: " + this.getAlgorithm() + 
                " mode=" + mode);
            return this.init(mode, key, params);
        };
});

Common Mistakes

No user authentication on sensitive keys. A key without setUserAuthenticationRequired(true) can be used by any code that reaches the Keystore daemon. If the app process is compromised, the key is effectively open.

Key material stored outside the Keystore. Encoding a key to bytes and saving it to SharedPreferences or a file bypasses all Keystore protections. Search for getEncoded() and Base64.encode() near key objects in smali — this pattern almost always indicates a key being moved outside the Keystore.

Large authentication validity windows. A setUserAuthenticationValidityDurationSeconds(3600) means one authentication unlocks the key for an hour. If you can observe or trigger one authentication event, you have a 60-minute window to use the key as many times as you want.

Not checking whether keys are hardware-backed. An app that uses setUserAuthenticationRequired(true) but never verifies isInsideSecureHardware() may be operating on software-backed keys without knowing it. On a rooted device, those keys can be extracted.

Allowing device backup. If android:allowBackup="true" in the manifest and the app stores sensitive data encrypted with Keystore keys but also backs up the ciphertext, an attacker with a backup can wait for a Keystore key extraction opportunity rather than attacking the live device.

This post is intended for security professionals and researchers. The techniques described here are shared for educational purposes in the context of security research and mobile application security testing. Do not use this information to access systems or services without explicit authorization.


메타데이터
post_id
293ff0f406b5
slug
inside-the-android-keystore-293ff0f406b5
url
https://medium.com/@cr0nos/inside-the-android-keystore-293ff0f406b5
canonical_url
https://medium.com/@cr0nos/inside-the-android-keystore-293ff0f406b5
author_url
https://medium.com/@cr0nos
status
ok
fetched_at
2026-06-22 12:55:45