How Can I Use the Android KeyStore to securely store arbitrary strings? How Can I Use the Android KeyStore to securely store arbitrary strings? android android

How Can I Use the Android KeyStore to securely store arbitrary strings?


I started with the premise that I could use AndroidKeyStore to secure arbitrary blobs of data, and call them "keys". However, the deeper I delved into this, the clearer it became that the KeyStore API is deeply entangled with Security-related objects: Certificates, KeySpecs, Providers, etc. It's not designed to store arbitrary data, and I don't see a straightforward path to bending it to that purpose.

However, the AndroidKeyStore can be used to help me to secure my sensitive data. I can use it to manage the cryptographic keys which I will use to encrypt data local to the app. By using a combination of AndroidKeyStore, CipherOutputStream, and CipherInputStream, we can:

  • Generate, securely store, and retrieve encryption keys on the device
  • Encrypt arbitrary data and save it on the device (in the app's directory, where it will be further protected by the file system permissions)
  • Access and decrypt the data for subsequent use.

Here is some example code which demonstrates how this is achieved.

try {    KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");    keyStore.load(null);    String alias = "key3";    int nBefore = keyStore.size();    // Create the keys if necessary    if (!keyStore.containsAlias(alias)) {        Calendar notBefore = Calendar.getInstance();        Calendar notAfter = Calendar.getInstance();        notAfter.add(Calendar.YEAR, 1);        KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(this)            .setAlias(alias)            .setKeyType("RSA")            .setKeySize(2048)            .setSubject(new X500Principal("CN=test"))            .setSerialNumber(BigInteger.ONE)            .setStartDate(notBefore.getTime())            .setEndDate(notAfter.getTime())            .build();        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");        generator.initialize(spec);        KeyPair keyPair = generator.generateKeyPair();    }    int nAfter = keyStore.size();    Log.v(TAG, "Before = " + nBefore + " After = " + nAfter);    // Retrieve the keys    KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(alias, null);    RSAPrivateKey privateKey = (RSAPrivateKey) privateKeyEntry.getPrivateKey();    RSAPublicKey publicKey = (RSAPublicKey) privateKeyEntry.getCertificate().getPublicKey();    Log.v(TAG, "private key = " + privateKey.toString());    Log.v(TAG, "public key = " + publicKey.toString());    // Encrypt the text    String plainText = "This text is supposed to be a secret!";    String dataDirectory = getApplicationInfo().dataDir;    String filesDirectory = getFilesDir().getAbsolutePath();    String encryptedDataFilePath = filesDirectory + File.separator + "keep_yer_secrets_here";    Log.v(TAG, "plainText = " + plainText);    Log.v(TAG, "dataDirectory = " + dataDirectory);    Log.v(TAG, "filesDirectory = " + filesDirectory);    Log.v(TAG, "encryptedDataFilePath = " + encryptedDataFilePath);    Cipher inCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL");    inCipher.init(Cipher.ENCRYPT_MODE, publicKey);    Cipher outCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidOpenSSL");    outCipher.init(Cipher.DECRYPT_MODE, privateKey);    CipherOutputStream cipherOutputStream =         new CipherOutputStream(            new FileOutputStream(encryptedDataFilePath), inCipher);    cipherOutputStream.write(plainText.getBytes("UTF-8"));    cipherOutputStream.close();    CipherInputStream cipherInputStream =         new CipherInputStream(new FileInputStream(encryptedDataFilePath),            outCipher);    byte [] roundTrippedBytes = new byte[1000]; // TODO: dynamically resize as we get more data    int index = 0;    int nextByte;    while ((nextByte = cipherInputStream.read()) != -1) {        roundTrippedBytes[index] = (byte)nextByte;        index++;    }    String roundTrippedString = new String(roundTrippedBytes, 0, index, "UTF-8");    Log.v(TAG, "round tripped string = " + roundTrippedString);} catch (NoSuchAlgorithmException e) {    Log.e(TAG, Log.getStackTraceString(e));} catch (NoSuchProviderException e) {    Log.e(TAG, Log.getStackTraceString(e));} catch (InvalidAlgorithmParameterException e) {    Log.e(TAG, Log.getStackTraceString(e));} catch (KeyStoreException e) {    Log.e(TAG, Log.getStackTraceString(e));} catch (CertificateException e) {    Log.e(TAG, Log.getStackTraceString(e));} catch (IOException e) {    Log.e(TAG, Log.getStackTraceString(e));} catch (UnrecoverableEntryException e) {    Log.e(TAG, Log.getStackTraceString(e));} catch (NoSuchPaddingException e) {    Log.e(TAG, Log.getStackTraceString(e));} catch (InvalidKeyException e) {    Log.e(TAG, Log.getStackTraceString(e));} catch (BadPaddingException e) {    Log.e(TAG, Log.getStackTraceString(e));} catch (IllegalBlockSizeException e) {    Log.e(TAG, Log.getStackTraceString(e));} catch (UnsupportedOperationException e) {    Log.e(TAG, Log.getStackTraceString(e));}


You may have noticed that there are problems handling different API levels with the Android Keystore.

Scytale is an open source library that provides a convenient wrapper around the Android Keystore so that you don't have write boiler plate and can dive straight into enryption/decryption.

Sample code:

// Create and save keyStore store = new Store(getApplicationContext());if (!store.hasKey("test")) {   SecretKey key = store.generateSymmetricKey("test", null);}...// Get keySecretKey key = store.getSymmetricKey("test", null);// Encrypt/Decrypt dataCrypto crypto = new Crypto(Options.TRANSFORMATION_SYMMETRIC);String text = "Sample text";String encryptedData = crypto.encrypt(text, key);Log.i("Scytale", "Encrypted data: " + encryptedData);String decryptedData = crypto.decrypt(encryptedData, key);Log.i("Scytale", "Decrypted data: " + decryptedData);


I have reworked the accepted answer by Patrick Brennan. on Android 9, it was yielding a NoSuchAlgorithmException. The deprecated KeyPairGeneratorSpec has been replaced with KeyPairGenerator. There was also some work required to address an exception regarding the padding.

The code is annotated with the changes made: "***"

@RequiresApi(api = Build.VERSION_CODES.M)public static void storeExistingKey(Context context) {    final String TAG = "KEY-UTIL";    try {        KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");        keyStore.load(null);        String alias = "key11";        int nBefore = keyStore.size();        // Create the keys if necessary        if (!keyStore.containsAlias(alias)) {            Calendar notBefore = Calendar.getInstance();            Calendar notAfter = Calendar.getInstance();            notAfter.add(Calendar.YEAR, 1);            // *** Replaced deprecated KeyPairGeneratorSpec with KeyPairGenerator            KeyPairGenerator spec = KeyPairGenerator.getInstance(                    // *** Specified algorithm here                    // *** Specified: Purpose of key here                    KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore");            spec.initialize(new KeyGenParameterSpec.Builder(                    alias, KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT)                     .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1) //  RSA/ECB/PKCS1Padding                    .setKeySize(2048)                    // *** Replaced: setStartDate                    .setKeyValidityStart(notBefore.getTime())                    // *** Replaced: setEndDate                    .setKeyValidityEnd(notAfter.getTime())                    // *** Replaced: setSubject                    .setCertificateSubject(new X500Principal("CN=test"))                    // *** Replaced: setSerialNumber                    .setCertificateSerialNumber(BigInteger.ONE)                    .build());            KeyPair keyPair = spec.generateKeyPair();            Log.i(TAG, keyPair.toString());        }        int nAfter = keyStore.size();        Log.v(TAG, "Before = " + nBefore + " After = " + nAfter);        // Retrieve the keys        KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry) keyStore.getEntry(alias, null);        PrivateKey privateKey = privateKeyEntry.getPrivateKey();        PublicKey publicKey = privateKeyEntry.getCertificate().getPublicKey();        Log.v(TAG, "private key = " + privateKey.toString());        Log.v(TAG, "public key = " + publicKey.toString());        // Encrypt the text        String plainText = "This text is supposed to be a secret!";        String dataDirectory = context.getApplicationInfo().dataDir;        String filesDirectory = context.getFilesDir().getAbsolutePath();        String encryptedDataFilePath = filesDirectory + File.separator + "keep_yer_secrets_here";        Log.v(TAG, "plainText = " + plainText);        Log.v(TAG, "dataDirectory = " + dataDirectory);        Log.v(TAG, "filesDirectory = " + filesDirectory);        Log.v(TAG, "encryptedDataFilePath = " + encryptedDataFilePath);        // *** Changed the padding type here and changed to AndroidKeyStoreBCWorkaround        Cipher inCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidKeyStoreBCWorkaround");        inCipher.init(Cipher.ENCRYPT_MODE, publicKey);        // *** Changed the padding type here and changed to AndroidKeyStoreBCWorkaround        Cipher outCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "AndroidKeyStoreBCWorkaround");        outCipher.init(Cipher.DECRYPT_MODE, privateKey);        CipherOutputStream cipherOutputStream =                new CipherOutputStream(                        new FileOutputStream(encryptedDataFilePath), inCipher);        // *** Replaced string literal with StandardCharsets.UTF_8        cipherOutputStream.write(plainText.getBytes(StandardCharsets.UTF_8));        cipherOutputStream.close();        CipherInputStream cipherInputStream =                new CipherInputStream(new FileInputStream(encryptedDataFilePath),                        outCipher);        byte[] roundTrippedBytes = new byte[1000]; // TODO: dynamically resize as we get more data        int index = 0;        int nextByte;        while ((nextByte = cipherInputStream.read()) != -1) {            roundTrippedBytes[index] = (byte) nextByte;            index++;        }        // *** Replaced string literal with StandardCharsets.UTF_8        String roundTrippedString = new String(roundTrippedBytes, 0, index, StandardCharsets.UTF_8);        Log.v(TAG, "round tripped string = " + roundTrippedString);    } catch (NoSuchAlgorithmException | UnsupportedOperationException | InvalidKeyException | NoSuchPaddingException | UnrecoverableEntryException | NoSuchProviderException | KeyStoreException | CertificateException | IOException e | InvalidAlgorithmParameterException e) {        e.printStackTrace();}

Note: “AndroidKeyStoreBCWorkaround” allows the code to work across different APIs.

I would be grateful if anyone can comment on any shortcomings in this updated solution. Else if anyone with more Crypto knowledge feels confident to update Patrick's answer then I will remove this one.