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.