Use of Android KeyStore

During our App development process, some sensitive and secure data may need to be encrypted, such as the storage of login tokens. We often use some encryption algorithms to encrypt these sensitive data and then save them, and then decrypt them when they need to be taken out.

At this point, there will be a problem: how to store the Key used for encryption and decryption?

  • If the Key and the encrypted data are stored together, there is a certain security risk.
  • Encrypt the Key again, and it will fall into an endless loop.

In order to ensure security, Android provides the KeyStore system to store Keys. This article will briefly explore KeyStore and its usage.

1. What is KeyStore? How to ensure security?

1. What is KeyStore?

Let’s take a look at the official definition of him:

The Android Keystore system lets you store encryption keys in containers to make them harder to extract from the device. Once a key is in the keystore, it can be used in cryptographic operations, while the key material remains non-exportable. Also, it provides the ability to restrict when and how keys are used, such as requiring users to authenticate to use keys, or restricting keys to use only in certain encryption modes.

The definition is actually very simple, it is used to save the Key and certificate for encryption and decryption.

2. How to ensure security?

The security protection measures are written in the official document (Android Keystore System | Android Developers | Android Developers (google.cn)), I will summarize it according to my understanding:

  • The key is stored in the mobile phone system, and when the application process obtains the key, it is obtained into the memory through the system process. That is to say, the Key can only be obtained in this application process, and it is impossible to extract the Key.
  • KeyStore can also specify the authorized use method of the key (such as user security lock verification), which can ensure that the Key can only be obtained under the authorization.

Generally speaking, the user can only use the Key stored in KeyStore when the application is running, unless the attacker gets the source code to interrupt

Click Debug, otherwise you will not know what the Key is. This ensures the security of the stored Key.

2. Use of KeyStore

There are many encryption algorithms supported by KeyStore, some of which have API Level requirements, for details, please refer to Android Keystore System | Android Developers | Android Developers (google.cn)

This article takes the AES encryption algorithm as an example to briefly explain how to use KeyStore. Note that the code involved in this article requires minSdk>=23.

First simply implement the encryption and decryption of the AES algorithm

1. Data encryption

object AESUtil {

    private const val IV_BLOCK_SIZE = 16

    fun encryptAES(encryptBytes: ByteArray, encryptKey: SecretKey): ByteArray?{
        try {
            //Create a cipher
            val cipher = Cipher.getInstance("AES/CBC/PKCS7PADDING")
            //Initialize the Cipher object with the key
            cipher.init(Cipher.ENCRYPT_MODE, encryptKey)
            val final = cipher. doFinal(encryptBytes)
            // iv occupies the first 16 digits, and the encrypted data occupies the latter
            return cipher.iv + final
        } catch (e: NoSuchPaddingException) {
            e. printStackTrace()
        } catch (e: NoSuchAlgorithmException) {
            e. printStackTrace()
        } catch (e: InvalidAlgorithmParameterException) {
            e. printStackTrace()
        } catch (e: InvalidKeyException) {
            e. printStackTrace()
        } catch (e: BadPaddingException) {
            e. printStackTrace()
        } catch (e: IllegalBlockSizeException) {
            e. printStackTrace()
        }
        return null
    }

    fun decryptAES(decryptBytes: ByteArray, decryptKey: SecretKey): ByteArray? {
        try {
            // Take out the IV first
            val iv = decryptBytes. copyOfRange(0, IV_BLOCK_SIZE)
            // Get the encrypted data
            val decryptData = decryptBytes.copyOfRange(IV_BLOCK_SIZE, decryptBytes.size)
            val cipher = Cipher.getInstance("AES/CBC/PKCS7PADDING")
            cipher.init(Cipher.DECRYPT_MODE, decryptKey, IvParameterSpec(iv))
            return cipher. doFinal(decryptData)
        } catch (e: NoSuchPaddingException) {
            e. printStackTrace()
        } catch (e: NoSuchAlgorithmException) {
            e. printStackTrace()
        } catch (e: InvalidAlgorithmParameterException) {
            e. printStackTrace()
        } catch (e: InvalidKeyException) {
            e. printStackTrace()
        } catch (e: BadPaddingException) {
            e. printStackTrace()
        } catch (e: IllegalBlockSizeException) {
            e. printStackTrace()
        }
        return null
    }

}

Then we need to generate a Key for encryption, which is achieved through KeyGenerator, first generate a KeyGenerator

 private fun getKeyGenerator(alias: String): KeyGenerator {
        // The first parameter specifies the encryption algorithm, and the second parameter specifies the Provider
        val keyGenerator = KeyGenerator. getInstance("AES", "AndroidKeyStore")
        val parameterSpec = KeyGenParameterSpec. Builder(
            aliases,
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT //for encryption and decryption
        )
            .setBlockModes(KeyProperties.BLOCK_MODE_CBC) // AEC_CBC
            .setUserAuthenticationRequired(false) // Whether user authentication is required
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) //The PADDING of the AES algorithm is consistent with the previous AESUtil
            .build()
        keyGenerator.init(parameterSpec)
        return keyGenerator
    }

In this method, accepts an alias parameter, which is an alias for generating Key, which will be used later when fetching from KeyStore.

After generating KeyGenerator, you can generate the Key needed for encryption and decryption:

 val key: SecretKey = getKeyGenerator("myKey").generateKey()

Then we can encrypt and store the data that needs to be protected.

 val srcData = "hello world"
val encryptData = AESUtil.encryptAES(srcData.toByteArray(), key)
// Store encrypted data
...

2. Obtain Key decryption from KeyStore

We have encrypted and stored the data before, and the next step is to take out the data and decrypt it for use.

We take out our decrypted Key from KeyStore

 fun getKeyFromKeyStore(alias: String): SecretKey? {
        // The parameter is Provider
        val keyStore = KeyStore. getInstance("AndroidKeyStore")
        // Must be initialized first
        keyStore.load(null)
        // Get the aliases of all Keys in the KeyStore
        val aliases = keyStore. aliases()
        // There is no key in the KeyStore
        if (!aliases. hasMoreElements()) {
            return null
        }
        // Key's protection parameter, here is no password required
        val protParam: KeyStore. ProtectionParameter =
            KeyStore. PasswordProtection(null)
        // Get Key by alias
        val entry = keyStore.getEntry(alias, protParam) as KeyStore.SecretKeyEntry
        return entry.secretKey
    }

The comments of each step are written in the code. The alias parameter of the method here is the parameter generated when we generated the Key through KeyGenerator.

Then you can get the Key to decrypt.

 val decryptKey = KeyStoreUtil.getKeyFromKeyStore(KEY_ALIAS)
decryptKey?.let {
        // decrypt data
        val decryptAES = AESUtil.decryptAES(encryptData, decryptKey)
    }

At this point, the simple use of KeyStore is over.

3. Source code analysis

Careful readers may find the problem. In the previous use, there is no operation of storing the Key in the KeyStore. Why?

Can the noodles be taken out directly? If you want to figure out this problem, you have to solve it through the source code.

First draw up an idea for analyzing the problem:

  1. Where is the KeyStore obtained from?
  2. How does KeyGenerator store when generating Key?

1. Where is the KeyStore obtained from?

KeyStore The main way to get Key is getEntry, the source code of this method is very clear and simple

 public final Entry getEntry(String alias, ProtectionParameter protParam)
                throws NoSuchAlgorithmException, UnrecoverableEntryException,
                KeyStoreException {

        if (alias == null) {
            throw new NullPointerException("invalid null input");
        }
        if (!initialized) {
            throw new KeyStoreException("Uninitialized keystore");
        }
        return keyStoreSpi.engineGetEntry(alias, protParam);
    }

First of all, alias cannot be empty when fetching. This is an alias for taking Key. If it is empty, you will naturally not know which Key you want.

Secondly, it will judge whether KeyStore is initialized.

The core code is the last line.

The KeyStoreSpi here is an abstract class, which implements the engineGetEntry method.

public KeyStore.Entry engineGetEntry(String alias,
                    KeyStore.ProtectionParameter protParam)
            throws KeyStoreException, NoSuchAlgorithmException,
            UnrecoverableEntryException {

...
    if ((protParam == null) || protParam instanceof KeyStore.PasswordProtection) {
        if (engineIsCertificateEntry(alias)) {
            throw new UnsupportedOperationException
                ("trusted certificate entries are not password-protected");
        } else if (engineIsKeyEntry(alias)) {
            char[] password = null;
            if (protParam != null) {
                KeyStore.PasswordProtection pp =
                    (KeyStore.PasswordProtection)protParam;
                password = pp. getPassword();
            }
            Key key = engineGetKey(alias, password);
            ....
        }
    }

    ....
}

If you follow the source code, you will find that the key is obtained through the engineGetKey() method, and this method is a

The abstract method is to find the specific implementation class.

The Provider we use is AndroidKeyStore, and the corresponding implementation class is AndroidKeyStoreSpi. then go inside

Take a look at the implementation of engineGetKey(),

 public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException,UnrecoverableKeyException {
        try {
            return AndroidKeyStoreProvider.loadAndroidKeyStoreKeyFromKeystore(mKeyStore,alias,mNamespace);
        }
        ....
    }

The core code inside is only one sentence, continue to dig into AndroidKeyStoreProvider.

 public static AndroidKeyStoreKey loadAndroidKeyStoreKeyFromKeystore(
            @NonNull KeyStore2 keyStore, @NonNull String alias, int namespace)
            throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException {
        ....
        final AndroidKeyStoreKey key = loadAndroidKeyStoreKeyFromKeystore(keyStore, descriptor);
        if (key instanceof AndroidKeyStorePublicKey) {
            return ((AndroidKeyStorePublicKey) key).getPrivateKey();
        } else {
            return key;
        }
    }

The core code is the loadAndroidKeyStoreKeyFromKeystore method

 private static AndroidKeyStoreKey loadAndroidKeyStoreKeyFromKeystore(
            @NonNull KeyStore2 keyStore, @NonNull KeyDescriptor descriptor)
            throws UnrecoverableKeyException, KeyPermanentlyInvalidatedException {
        KeyEntryResponse response = null;
        try {
            // core code
            response = keyStore. getKeyEntry(descriptor);
        } catch (android. security. KeyStoreException e) {
           ....
            }
        }
...
    }

Finally, I can see the place where the key is finally taken, but the keyStore here should pay attention to the following, which is the KeyStore2 class under Android.

 /**
     * Retrieves a key entry from the keystore backend.
     * @see IKeystoreService#getKeyEntry(KeyDescriptor) for more details.
     * @param descriptor
     * @return
     * @throws KeyStoreException
     * @hide
     */
    public KeyEntryResponse getKeyEntry(@NonNull KeyDescriptor descriptor)
            throws KeyStoreException {
        return handleRemoteExceptionWithRetry((service) -> service. getKeyEntry(descriptor));
    }

As can be seen from the comments, the KeyStore obtains the Key through the IKeystoreService service, that is, through the system process. **Here we mainly check where to get it from. For more details on how to get it, readers can look at IKeystoreService.

2. How to save?

Earlier we figured out where it was taken from, and then we have to take a look at how it was stored.

The method of storing Key in KeyStore is setEntry(), let’s start from here.

 public final void setEntry(String alias, Entry entry,
                        ProtectionParameter protParam)
                throws KeyStoreException {
        if (alias == null || entry == null) {
            throw new NullPointerException("invalid null input");
        }
        if (!initialized) {
            throw new KeyStoreException("Uninitialized keystore");
        }
        keyStoreSpi.engineSetEntry(alias, entry, protParam);
    }

It can be seen that the KeyStore is still handed over to KeyStoreSpi when saving. The core method of KeyStoreSpi is engineSetKeyEntry(), we go directly to AndroidKeyStoreSpi to see the specific implementation.

 @Override
    public void engineSetKeyEntry(String alias, Key key, char[] password, Certificate[] chain)
            throws KeyStoreException {
        if ((password != null) & amp; & amp; (password. length > 0)) {
            throw new KeyStoreException("entries cannot be protected with passwords");
        }

        if (key instanceof PrivateKey) {
            setPrivateKeyEntry(alias, (PrivateKey) key, chain, null);
        } else if (key instanceof SecretKey) {
            setSecretKeyEntry(alias, (SecretKey) key, null);
        } else {
            throw new KeyStoreException("Only PrivateKey and SecretKey are supported");
        }
    }

Here is a brief introduction:

  • PrivateKey is usually the private key of an asymmetric encryption algorithm, the public key is used for encryption, and the private key is used for decryption, such as the RSA algorithm.
  • SecretKey is usually the key of a symmetric encryption algorithm, which is used for encryption and decryption, such as the AES algorithm.

Then look at the setSecretKeyEntry() method

private void setSecretKeyEntry(String alias, SecretKey key,
            java.security.KeyStore.ProtectionParameter param)
            throws KeyStoreException {
        ...

        try {
            KeyStoreSecurityLevel securityLevelInterface = mKeyStore.getSecurityLevel(
                    securityLevel);

            KeyDescriptor descriptor = makeKeyDescriptor(alias);

            securityLevelInterface.importKey(descriptor, null /* TODO attestationKey */,
                    importArgs, flags, keyMaterial);
        } catch (android. security. KeyStoreException e) {
            throw new KeyStoreException("Failed to import secret key.", e);
        }
    }

The method is very long, but the final deposit method is here at the end.

It can be seen that the KeyStoreSecurityLevel class is finally saved through the importKey() method.

 /**
     * Imports a key into Keystore.
     * @see IKeystoreSecurityLevel#importKey(KeyDescriptor, KeyDescriptor, KeyParameter[], int,
     */
    public KeyMetadata importKey(KeyDescriptor descriptor, KeyDescriptor attestationKey,
            Collection<KeyParameter> args, int flags, byte[] keyData)
            throws KeyStoreException {
        return handleExceptions(() -> mSecurityLevel.importKey(descriptor, attestationKey,
                args.toArray(new KeyParameter[args.size()]), flags, keyData));
    }

You can understand it from the comments, import Key into KeyStore

3. Has KeyGenerator saved it for us?

After figuring out how to save it, you can go to the source code of KeyGenerator to see if he really saved it for us directly.

 public final SecretKey generateKey() {
        if (serviceIterator == null) {
            return spi.engineGenerateKey();
        }
        ....
   }

KeyGeneratorSpi is also an abstract class, and our specific implementation class here is AndroidKeyStoreKeyGeneratorSpi

@Override
    protected SecretKey engineGenerateKey() {
        ....
        KeyStoreSecurityLevel iSecurityLevel = null;
        try {
            iSecurityLevel = mKeyStore. getSecurityLevel(securityLevel);
            metadata = iSecurityLevel.generateKey(
                    descriptor,
                    null, /* Attestation key not applicable to symmetric keys. */
                    params,
                    flags,
                    additionalEntropy);
        } catch (android. security. KeyStoreException e) {
            switch (e. getErrorCode()) {
                // TODO replace with ErrorCode.HARDWARE_TYPE_UNAVAILABLE when KeyMint spec
                // becomes available.
                case KeymasterDefs.KM_ERROR_HARDWARE_TYPE_UNAVAILABLE:
                    throw new StrongBoxUnavailableException("Failed to generate key");
                default:
                    throw new ProviderException("Keystore key generation failed", e);
            }
        }
       ....
        SecretKey result = new AndroidKeyStoreSecretKey(descriptor, metadata, keyAlgorithmJCA,
                iSecurityLevel);
        return result;
    }

This method is also very long, but at the end you can see an old friend: KeyStoreSecurityLevel. It turns out that the final method to generate Key is to call his generateKey() method.

 /**
     * Generates a new key in Keystore.
     * @see IKeystoreSecurityLevel#generateKey(KeyDescriptor, KeyDescriptor, KeyParameter[], int,
     */
    public KeyMetadata generateKey(@NonNull KeyDescriptor descriptor, KeyDescriptor attestationKey,
            Collection<KeyParameter> args, int flags, byte[] entropy)
            throws KeyStoreException {
        return handleExceptions(() -> mSecurityLevel.generateKey(
                descriptor, attestationKey, args.toArray(new KeyParameter[args.size()]),
                flags, entropy));
    }

Generate a new Key in KeyStore, which is obvious here.

When the KeyGenerator finally generates the Key, it will be directly generated in the KeyStore, so we can get it directly.

Four. Summary
This article briefly introduces what is KeyStore, if you use KeyGenerator and KeyStore, and analyzes the source code of KeyStore’s access methods.
—————
Copyright statement: This article is an original article of CSDN blogger “Dianguang Ferrari”, which follows the CC 4.0 BY-SA copyright agreement. For reprinting, please attach the original source link and this statement.
Original link: https://blog.csdn.net/qq_43478882/article/details/127392947