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));
}

Leave a Comment