Keys and the Keystore

The Java Cryptography Architecture (JCA) has been available in Java since Java 1.1, though it has expanded over the years. It gives us KeyStore objects that can manage cryptographic keys for a variety of symmetric and asymmetric ciphers. You can create keys, then use those keys to encrypt, decrypt, sign, and validate data. The actual algorithms that implement the cryptography come from service provider implementations (SPIs) that plug into the JCA. You do not need to know all of the details of the cryptography – you treat the algorithms mostly as a black box.

Android extends JCA by offering a KeyStore that is integrated into the Android architecture. In particular, on some Android 7.0+ devices, the keys can be tied to hardware, as part of the so-called Trusted Execution Environment (TEE, presumably named by a golfer). This dramatically reduces the likelihood of your keys somehow getting leaked to some malicious actor. The Android-supplied KeyStore also offers hooks to tie keys to device authentication, where you can require the user to authenticate the device before you can use the key to decrypt the data.

In this chapter, we will explore the basics of using this Android-specific KeyStore. The important word in the preceding sentence is “basics”, as this is not a complete treatment of how to use the JCA. The JCA is part of standard Java; other educational resources can show you how to implement effective cryptography using the JCA.

Also note that many of the Android-specific APIs shown in this chapter have a minSdkVersion of 23.

Prerequisites

To understand this chapter, please read the preceding chapter on device authentication first. Also, the examples in this chapter make extensive use of RxJava.

Terminology

First, let’s review some…ummmm… “key” terms that will be used in this chapter.

Keys

To encrypt, decrypt, sign, or validate some data, you need a key and a corresponding algorithm.

From a javax.crypto standpoint, the Java class that we use to represent keys is SecretKey. Whether this is the “real” key, or is merely an identifier to “key material” held elsewhere (e.g., in hardware), depends on the SPI and KeyStore implementation.

KeyStore

A KeyStore, as the name suggests, is a storage location for keys. There are several implementations of KeyStore, based on the storage location and format of the keys in the store.

In Android, we are particularly interested in one known as AndroidKeyStore. It offers two benefits:

  1. The “key material” — the actual bytes representing the key — never make it into our application process. Instead, the cryptography is performed by a system process. The APIs that we use make it appear that everything is local, but in reality IPC is occurring under the covers to pass our data and key identifier to this system process. The benefit here is that even if our process gets attacked, the attacker cannot get the “key material” to be able to decrypt encrypted data away from this device.
  2. On supported devices, and for supported key/algorithm combinations, the key material may “bound” to the device itself. This means that even the system process that normally does the cryptography does not have the bytes representing the key itself. Instead, the cryptography is performed by dedicated hardware, as part of the TEE. The key material held in the AndroidKeyStore needs to be blended with secure data held in hardware to get the real key to use with the cryptography. As a result, even if an attacker is able to hack core system processes, the attacker will not be able to retrieve a key that can be used outside of this device.

KeyChain

If you rummage around the Android SDK, you will also see a separate KeyChain class. This offers a stripped down system similar to the KeyStore. However, the KeyChain is for device-wide keys, and as such is usually not what developers of an individual app need.

Getting a KeyStore

The AndroidKeyStore is merely the name of a JCA KeyStore that you obtain using standard javax.crypto APIs:


ks=KeyStore.getInstance("AndroidKeyStore");
ks.load(null);

The static getInstance() method returns an instance of the KeyStore, and load() is used to populate it. With traditional JCA KeyStore implementations, load() is used to load the keys from a file. The AndroidKeyStore does not need this, and so we can pass null into the load() method.

Note that these methods throw a variety of checked exceptions, and so you will need to be in position to catch those and do something useful. In practice, nothing should go wrong, as those checked exceptions are mostly for cases where:

Creating a Key

Android has a dedicated KeyGenParameterSpec class, with a corresponding Builder, that is used to create secret keys for symmetric encryption, stored in the AndroidKeyStore:


KeyGenParameterSpec spec=
  new KeyGenParameterSpec.Builder("thisIsMyKey",
    KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
    .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
    .build();

KeyGenerator keygen=
  KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");

keygen.init(spec);
SecretKey secretKey=keygen.generateKey();

Grafting the Android-specific AndroidKeyStore onto the JCA base results in a few oddities. For example, we do not need to do anything to save this key in the AndroidKeyStore — saving it is a side effect of creating the key using a KeyGenerator backed by the AndroidKeyStore (via the getInstance() static method).

Each of your app’s keys has a name ("thisIsMyKey" in the above example). You will use this name to reference the key later on. This name needs to be unique for your app, though it does not need to be unique for the entire device.

Most of the methods on the KeyGenParameterSpec.Builder are tied to the JCA, such as the block modes, encryption paddings, and so forth. The above configuration – when coupled with KEY_ALGORITHM_AES when creating the KeyGenerator – is for AES/CBC/PKCS7Padding, which is a fairly typical symmetric key setup. Most of the details of setting up the Builder, therefore, are tied to the specific form of encrypting or digital signature that you wish to employ. The details of this are well out of scope for this book and are best left to educational resources dedicated to Java cryptography.

The JCA KeyStore is designed around symmetric keys (e.g., SecretKey) classes, though asymmetric encryption has been grafted onto the original KeyStore API, so RSA-style public-key encryption can also be performed using the AndroidKeyStore.

Tying the Key to Device Authentication

Part of the reason for an Android-specific KeyGenParameterSpec class is to be able to offer Android-specific features to extend the JCA. A prominent example of this is tying a generated key to device authentication, such that your app can only use the key if the user is currently authenticated.

The two primary Builder methods for this are:

The default validity period is, in effect, zero seconds. Every use of the key requires an immediately-preceding authentication. This may be excessive, and you will want to consider whether setting a longer validity period (e.g., one minute) is more appropriate for your situation.

So, in this sample, we generate a key with user authentication required, with a 60-second timeout:


KeyGenParameterSpec spec=
  new KeyGenParameterSpec.Builder("thisIsMyKey",
    KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
    .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
    .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
    .setUserAuthenticationRequired(true)
    .setUserAuthenticationValidityDurationSeconds(60)
    .build();

KeyGenerator keygen=
  KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore");

keygen.init(spec);
SecretKey secretKey=keygen.generateKey();

Later on, when we try to use the key for encryption, decryption, etc., we may get a UserNotAuthenticatedException. This means that the user has not authenticated against the device within the timeout period, and so we need to re-authenticate the user (e.g., createConfirmDeviceCredentialIntent()) before trying the cryptographic operation again. The preceding chapter outlines how to use createConfirmDeviceCredentialIntent() to authenticate the user.

Learning More About Your Key

For keys created in the AndroidKeyStore, you can use a somewhat clunky set of APIs to find out more details about the key, in particular whether the key is backed by any sort of hardware security (e.g., the TEE). This information is available through an Android-specific KeyInfo class. Unfortunately, you have to get one of those by means of a SecretKeyFactory and a cast, as shown below:


SecretKey key=(SecretKey)ks.getKey(keyName, null);
KeyInfo info=
  (KeyInfo)SecretKeyFactory.getInstance(key.getAlgorithm(), "AndroidKeyStore")
    .getKeySpec(key, KeyInfo.class);

if (info.isInsideSecureHardware()) {
  Toast.makeText(this, "Key is inside secure hardware", Toast.LENGTH_LONG).show();
}
else {
  Toast.makeText(this, "Key is only secured by software", Toast.LENGTH_LONG).show();
}

Given the SecretKey, you can get the associated SecretKeyFactory, based on the algorithm and keystore name. That SecretKeyFactory has a getKeySpec() method, which takes the SecretKey and the spec class (KeyInfo.class in this case). That needs to be cast to a KeyInfo, this JCA code probably pre-dates Java’s support for generics.

The sample code shown above checks one specific characteristic of the key: does the key reside inside of secure hardware. You get that from a call to isInsideSecureHardware().

Encrypting Data

So, with all of that as background, let’s look at actually encrypting some data using all of this stuff.

The DeviceAuth/SecureNote sample application has a single activity with a really big EditText widget, for you to type in a note. We will store that note in an encrypted form, using a key that requires device authentication.

And to do that, we will spend some time in a bodega.

Introducing RxKeyBodega

The RxKeyBodega class in this project implements a small RxJava-based API wrapped around the relevant javax.crypto and android.security.keystore classes. The idea is to isolate most of the “hard core” encryption logic in this class, but allow for composability, so that the calling app can control things like:

The only aspect that RxKeyBodega cannot handle itself is device authentication, since that involves a UI.

(this is far smaller than a key “store”; hence, a key “bodega”)

Encrypting the Note

First, let’s look at the encryption path. Our MainActivity not only has the really big EditText, but it has a “save” action bar item that, when clicked, will save the contents of the EditText in an encrypted form.

RxKeyBodega Client

That action bar item is tied to a save() method on MainActivity, which uses RxKeyBodega to set up an RxJava chain to process our encryption:

  private void save() {
    final Context app=getApplicationContext();
    byte[] toEncrypt=note.getText().toString().getBytes(UTF8);

    RxKeyBodega.encrypt(toEncrypt, KEY_NAME, TIMEOUT_SECONDS)
      .subscribeOn(Schedulers.io())
      .map(result -> {
        File f=new File(app.getFilesDir(), FILENAME);

        RxKeyBodega.save(f, result);

        return f;
      })
      .observeOn(AndroidSchedulers.mainThread())
      .subscribe(
        file -> Toast.makeText(MainActivity.this, R.string.saved, Toast.LENGTH_SHORT).show(),
        t -> {
          if (t instanceof UserNotAuthenticatedException) {
            requestAuth(REQUEST_SAVE);
          }
          else {
            Toast.makeText(MainActivity.this, t.getMessage(), Toast.LENGTH_LONG).show();
            Log.e(getString(R.string.app_name), "Exception saving encrypted file", t);
          }
        });
  }
(from DeviceAuth/SecureNote/app/src/main/java/com/commonsware/android/auth/note/MainActivity.java)

We will take a look at RxKeyBodega.encrypt() in the next section, but it returns an Observable of an EncryptionResult object, which itself is part of RxKeyBodega. We supply the data to be encrypted as a byte array, which we encode using UTF8 from the Editable returned by the EditText and its getText() method. We pass that, along with a unique name for the encryption key to use and how long the device authentication timeout should be. That encryption key will be lazy-created if it does not already exist.

After routing this work to the io() thread, we use map() to process the EncryptionResult. We create a File pointing to where we want the encrypted data to be stored on internal storage, then call RxKeyBodega.save() to save the EncryptionResult to that file. This File is then supplied downstream to our subscriber lambda, which:

requestAuth() takes a request code as a parameter and kicks off device authentication using createConfirmDeviceCredentialIntent():

  private void requestAuth(int requestCode) {
    Intent i=
      mgr.createConfirmDeviceCredentialIntent("title", "description");

    if (i==null) {
      Toast.makeText(this, "No authentication required?!?",
        Toast.LENGTH_SHORT).show();
    }
    else {
      startActivityForResult(i, requestCode);
    }
  }
(from DeviceAuth/SecureNote/app/src/main/java/com/commonsware/android/auth/note/MainActivity.java)

In onActivityResult(), if the user authenticated, we try save() again:

  @Override
  protected void onActivityResult(int requestCode, int resultCode,
                                  Intent data) {
    if (resultCode==RESULT_OK) {
      if (requestCode==REQUEST_SAVE) {
        save();
      }
      else if (requestCode==REQUEST_LOAD) {
        load();
      }
    }
    else {
      Toast.makeText(this, R.string.sorry, Toast.LENGTH_SHORT).show();
      finish();
    }
  }
(from DeviceAuth/SecureNote/app/src/main/java/com/commonsware/android/auth/note/MainActivity.java)

If the user declined to authenticate, we show a Toast and finish() the activity, since there is nothing useful that we can do.

RxKeyBodega Implementation

Overall, RxKeyBodega follows the implementation approach used by RxFingerprint, profiled in the previous chapter. So, for example, the encrypt() method on RxKeyBodega does not encrypt anything itself, but rather creates an Observable — named EncryptObservable — that will handle the encryption work itself:

  static Observable<EncryptionResult> encrypt(byte[] toEncrypt, String keyName,
                                              int timeout) {
    return Observable.create(new EncryptObservable(keyName, timeout, toEncrypt));
  }
(from DeviceAuth/SecureNote/app/src/main/java/com/commonsware/android/auth/note/RxKeyBodega.java)

This way, the consumer of RxKeyBodega can control the thread on which the encryption work is performed.

EncryptObservable extends a BodegaObservable class. It lazy-initializes a KeyStore, where KEYSTORE is "AndroidKeyStore", so we get access to the (potentially) hardware-backed keystore:

  private abstract static class BodegaObservable {
    KeyStore ks;
    Exception initException;

    BodegaObservable() {
      try {
        ks=KeyStore.getInstance(KEYSTORE);
        ks.load(null);
      }
      catch (Exception e) {
        initException=e;
      }
    }
  }
(from DeviceAuth/SecureNote/app/src/main/java/com/commonsware/android/auth/note/RxKeyBodega.java)

Nothing should go wrong in that initialization, but getInstance() and load() throw some checked exceptions. If one occurs, it is held onto in an initException field.

EncryptObservable implements the ObservableOnSubscribe interface, so the bulk of its logic goes into the subscribe() method, called when something eventually subscribes to this Observable:

    @Override
    public void subscribe(ObservableEmitter<EncryptionResult> emitter)
      throws Exception {
      if (initException==null) {
        createKey(keyName, timeout);

        SecretKey secretKey=(SecretKey)ks.getKey(keyName, null);
        Cipher cipher=Cipher.getInstance("AES/CBC/PKCS7Padding");
        SecureRandom rand=new SecureRandom();
        byte[] iv=new byte[BLOCK_SIZE];

        rand.nextBytes(iv);

        IvParameterSpec ivParams=new IvParameterSpec(iv);

        cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParams);
        emitter.onNext(new EncryptionResult(ivParams.getIV(), cipher.doFinal(toEncrypt)));
      }
      else {
        throw initException;
      }
    }
(from DeviceAuth/SecureNote/app/src/main/java/com/commonsware/android/auth/note/RxKeyBodega.java)

If initException is not null, we throw it here, so that the exception works its way through the RxJava chain and can be picked up by a Throwable handler in MainActivity.

If initException is null, though, we start of by lazy-creating our key, in a createKey() method:

    private void createKey(String keyName, int timeout) throws Exception {
      KeyStore.Entry entry=ks.getEntry(keyName, null);

      if (entry==null) {
        KeyGenParameterSpec spec=
          new KeyGenParameterSpec.Builder(keyName,
            KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
            .setUserAuthenticationRequired(true)
            .setUserAuthenticationValidityDurationSeconds(timeout)
            .setRandomizedEncryptionRequired(false)
            .build();

        KeyGenerator keygen=
          KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE);

        keygen.init(spec);
        keygen.generateKey();
      }
    }
  }
(from DeviceAuth/SecureNote/app/src/main/java/com/commonsware/android/auth/note/RxKeyBodega.java)

This uses the approach outlined earlier in this chapter, creating an AES/CBC/PKCS7Padding key. In particular, it is tied to device authentication, so the user will have had to entered their PIN, passcode, fingerprint, etc. within the timeout period, or else the encryption cannot be performed.

Once we are sure that we have a key, subscribe() creates a Cipher for that key, uses doFinal() to pass along the data to be encrypted, and passes that doFinal() response, along with an “initialization vector”, to an EncryptionResult constructor. The EncryptionResult is what gets emitted by this Observable and is what is picked up by MainActivity in the first step of its RxJava chain.

The save() utility method on RxKeyBodega, used by MainActivity, writes the initialization vector data and the encrypted data to the supplied file:

  static void save(File f, EncryptionResult result)
    throws IOException {
    BufferedSink sink=Okio.buffer(Okio.sink(f));

    sink.write(result.iv);
    sink.write(result.encrypted);
    sink.close();
  }
(from DeviceAuth/SecureNote/app/src/main/java/com/commonsware/android/auth/note/RxKeyBodega.java)

Some Quick Initialization Vector Notes

Some encryption schemes use an initialization vector (IV). This is a series of random bytes that serves as the initial input into the encryption algorithm. The bytes do not have to be secret, which is why it is safe for us to save them as part of the encrypted data, as we did in the subscribe() method of EncryptObservable above.

However, the bytes do have to be random. This has been a problem with Android.

Early versions of Android had a hard-coded default IV value, rather than randomly generating a value. This is bad, and it forced developers to have to create their own IV using SecureRandom.

Google apparently has fixed the default IV behavior. In fact, to use your own IV, you have to call setRandomizedEncryptionRequired() on the KeyGenParameterSpec.Builder, as part of creating your key in the AndroidKeyStore. If you fail to do this, when you go to supply your own IV bytes for encryption, you crash, with an exception whose message reads: “Caller-provided IV not permitted when encrypting”.

So, as of API Level 23, Google wants you to use their random IV implementation, rather than supply your own IV bytes. But, on older Android devices, you cannot rely on Google’s implementation.

So, now what?

The safest course of action is to do what we are doing in the SecureNote sample: provide a random IV and tell Android that we intend to do this, via the setRandomizedEncryptionRequired(). This way, even if some device manufacturer screws something up and their device winds up with non-random default IV values, you are covered.

Escaping If Device Is Insecure

Lazy-creating that key — in fact, this entire app — only makes sense if the device has a secure keyguard. Otherwise, we cannot have the key be tied to device authentication.

So, part of what we do in onCreate() is get a KeyguardManager and see if the keyguard is secure, using the techniques outlined in the preceding chapter:

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mgr=(KeyguardManager)getSystemService(KEYGUARD_SERVICE);

    if (mgr.isKeyguardSecure()) {
      note=findViewById(R.id.note);
      load();
    }
    else {
      Toast.makeText(this, R.string.insecure, Toast.LENGTH_LONG).show();
      finish();
    }
  }
(from DeviceAuth/SecureNote/app/src/main/java/com/commonsware/android/auth/note/MainActivity.java)

If it is not secure, we show a Toast and finish() our activity to leave the app.

Starting Decryption in onCreate()

If we do have a secure keyguard, we call a load() method, which will kick off loading the encrypted data and decrypting it.

You might wonder why we do not take a similar approach for encrypting and saving the note. Rather than force the user to click a “save” action bar item, we could call save() from onDestroy(), or perhaps from onStop().

The problem is that we may need the user to re-authenticate, if they have not authenticated since before the timeout window. That requires us to start an activity and get a result. However, that is not safe to do once our activity is destroyed. So what happens is that the user presses BACK to exit the app, we determine that we need to authenticate the user, and then:

As a result, while we can automatically decrypt the note, we cannot automatically encrypt it.

Decrypting the Note

Now, let’s turn our attention to the load() method.

RxKeyBodega Client

load() implements another RxJava Observable chain, to take what we wrote to the file, decrypt it, and show the results in the EditText for possible changes by the user:

  private void load() {
    Observable.just(new File(getFilesDir(), FILENAME))
      .subscribeOn(Schedulers.io())
      .observeOn(AndroidSchedulers.mainThread())
      .filter(File::exists)
      .map(RxKeyBodega::load)
      .flatMap(toDecrypt -> RxKeyBodega.decrypt(toDecrypt, KEY_NAME))
      .subscribe(bytes -> note.setText(new String(bytes, UTF8)),
        t -> {
          if (t instanceof UserNotAuthenticatedException) {
            requestAuth(REQUEST_LOAD);
          }
          else {
            Toast.makeText(MainActivity.this, t.getMessage(), Toast.LENGTH_LONG).show();
            Log.e(getString(R.string.app_name), "Exception loading encrypted file", t);
          }
        });
  }
(from DeviceAuth/SecureNote/app/src/main/java/com/commonsware/android/auth/note/MainActivity.java)

The Observable starts out with a File pointing to where the note is saved on internal storage. We then use the filter() operator to only continue if the file exists — if it doesn’t, that means we have no notes to load, and so the empty EditText is a fine starting point.

We then use a static load() method on RxKeyBodega to do the file I/O to read in the file. We pass the result from load() to the RxKeyBodega.decrypt() method, supplying the same KEY_NAME that we will want to use for encryption.

At that point, there are three possibilities:

  1. Everything succeeds, in which case we get the decrypted bytes, so we can turn them into a String (using the UTF8 charset) and apply that to the EditText
  2. We get a UserNotAuthenticatedException, in which case we request that the user authenticate, and if that succeeds, onActivityResult() will try the load() again
  3. We get some other exception, in which case we just show a Toast and log the exception

RxKeyBodega Implementation

The load() method on RxKeyBodega simply reads in the data that we wrote out:

  static EncryptionResult load(File f) throws Exception {
    BufferedSource source=Okio.buffer(Okio.source(f));
    byte[] iv=source.readByteArray(BLOCK_SIZE);
    byte[] encrypted=source.readByteArray();

    source.close();

    return new EncryptionResult(iv, encrypted);
  }
(from DeviceAuth/SecureNote/app/src/main/java/com/commonsware/android/auth/note/RxKeyBodega.java)

To read in the initialization vector, we need to know how many bytes it should be. That is based on the choice of encryption cipher, but the value is a constant for any given cipher. So, we get that in a static initialization block, and pray to the high heavens that JCA does not thrown an unexpected exception:

  private static final int BLOCK_SIZE;

  static {
    int blockSize=-1;

    try {
      blockSize=Cipher.getInstance("AES/CBC/PKCS7Padding").getBlockSize();
    }
    catch (Exception e) {
      Log.e("RxKeyBodega", "Could not get AES/CBC/PKCS7Padding cipher", e);
    }

    BLOCK_SIZE=blockSize;
  }
(from DeviceAuth/SecureNote/app/src/main/java/com/commonsware/android/auth/note/RxKeyBodega.java)

(a production-grade app would do something with the exception besides logging it to LogCat)

load() then wraps the initialization vector and encrypted data in an EncryptionResult, which becomes input for the decrypt() method, which turns around and hands it to a DecryptObserverable:

  static Observable<byte[]> decrypt(EncryptionResult toDecrypt, String keyName) {
    return Observable.create(new DecryptObservable(keyName, toDecrypt));
  }
(from DeviceAuth/SecureNote/app/src/main/java/com/commonsware/android/auth/note/RxKeyBodega.java)

DecryptObservable then uses JCA to decrypt the encrypted data, using the chosen Cipher and initialization vector, emitting the decrypted data:

  private static class DecryptObservable extends BodegaObservable implements
    ObservableOnSubscribe<byte[]> {
    final private String keyName;
    final private EncryptionResult toDecrypt;

    private DecryptObservable(String keyName, EncryptionResult toDecrypt) {
      this.keyName=keyName;
      this.toDecrypt=toDecrypt;
    }

    @Override
    public void subscribe(ObservableEmitter<byte[]> emitter)
      throws Exception {
      if (initException==null) {
        SecretKey secretKey=(SecretKey)ks.getKey(keyName, null);
        Cipher cipher=Cipher.getInstance("AES/CBC/PKCS7Padding");

        cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(toDecrypt.iv));
        emitter.onNext(cipher.doFinal(toDecrypt.encrypted));
      }
      else {
        throw initException;
      }
    }
  }
(from DeviceAuth/SecureNote/app/src/main/java/com/commonsware/android/auth/note/RxKeyBodega.java)

Time-Limited Device Authentication

In the previous chapter, we saw the use of createConfirmDeviceCredentialIntent() to force the user to re-authenticate before proceeding with something in your app. As noted then, the problem is that createConfirmDeviceCredentialIntent() always forces re-authentication, even if the user authenticated just moments ago (e.g., just unlocked their device).

However, for working with AndroidKeyStore-backed keys, we can require device authentication… but with a timeout. That way, the user only needs to re-authenticate if they have not authenticated recently, for whatever value of “recently” we want when setting up the key.

So, if you want to use createConfirmDeviceCredentialIntent(), but you want a timeout, one solution is silly, but works: encrypt something with a timeout-enabled, authenticated key. What you encrypt does not matter — you can throw away the results of the encryption. You are simply leveraging the timeout facility to let you know whether authentication is really needed or not.

The DeviceAuth/SecureTimeout sample application demonstrates this. It is based on the SecureCheck sample from the preceding chapter, but now uses the encryption technique outlined above.

In onCreate(), we set up the AndroidKeyStore:

    try {
      ks=KeyStore.getInstance(KEYSTORE);
      ks.load(null);
    }
    catch (Exception e) {
      Toast.makeText(this, "Ummm... this shouldn't happen", Toast.LENGTH_LONG).show();
      Log.e(getClass().getSimpleName(), "Exception initializing keystore", e);
    }
(from DeviceAuth/SecureTimeout/app/src/main/java/com/commonsware/android/auth/check/MainActivity.java)

We only enable the authentication action bar item if we have a secure keyguard, as otherwise we cannot set up the key that we want to use:

  @Override
  public boolean onCreateOptionsMenu(Menu menu) {
    getMenuInflater().inflate(R.menu.actions, menu);
    menu.findItem(R.id.auth).setEnabled(mgr.isKeyguardSecure());

    return super.onCreateOptionsMenu(menu);
  }
(from DeviceAuth/SecureTimeout/app/src/main/java/com/commonsware/android/auth/check/MainActivity.java)

As before, tapping that action bar item invokes an authenticate() method. Before, that just blindly called createConfirmDeviceCredentialIntent() and forced authentication. Now… it’s a bit different:

  private void authenticate() {
    try {
      createKeyForTimeout();
    }
    catch (Exception e) {
      Toast.makeText(this, "Could not create the key", Toast.LENGTH_LONG).show();
      Log.e(getClass().getSimpleName(), "Exception creating key", e);
      return;
    }

    if (needsAuth(false)) {
      Intent i=
        mgr.createConfirmDeviceCredentialIntent("title", "description");

      if (i==null) {
        Toast.makeText(this, "No authentication required!",
          Toast.LENGTH_SHORT).show();
      }
      else {
        startActivityForResult(i, REQUEST_CODE);
      }
    }
  }
(from DeviceAuth/SecureTimeout/app/src/main/java/com/commonsware/android/auth/check/MainActivity.java)

We start by invoking the same sort of lazy-create-the-key logic that we used in SecureNote, this time in a createKeyForTimeout() method:

  private void createKeyForTimeout() throws Exception {
    KeyStore.Entry entry=ks.getEntry(KEY_NAME, null);

    if (entry==null) {
      KeyGenParameterSpec spec=
        new KeyGenParameterSpec.Builder(KEY_NAME,
          KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
          .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
          .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
          .setUserAuthenticationRequired(true)
          .setUserAuthenticationValidityDurationSeconds(TIMEOUT_SECONDS)
          .build();

      KeyGenerator keygen=
        KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE);

      keygen.init(spec);
      keygen.generateKey();
    }
  }
(from DeviceAuth/SecureTimeout/app/src/main/java/com/commonsware/android/auth/check/MainActivity.java)

If that works, we call a needsAuth() method to see if we need authentication or not:

  private boolean needsAuth(boolean isRecheck) {
    boolean result=false;

    try {
      SecretKey secretKey=(SecretKey)ks.getKey(KEY_NAME, null);
      Cipher cipher=Cipher.getInstance("AES/CBC/PKCS7Padding");

      cipher.init(Cipher.ENCRYPT_MODE, secretKey);
      cipher.doFinal(POINTLESS_DATA);

      if (!isRecheck) {
        Toast.makeText(this, "Already authenticated!", Toast.LENGTH_LONG).show();
      }
    }
    catch (UserNotAuthenticatedException e) {
      result=true;
    }
    catch (KeyPermanentlyInvalidatedException e) {
      Toast.makeText(this, "You reset the lock screen!",
        Toast.LENGTH_LONG).show();
    }
    catch (Exception e) {
      Toast.makeText(this, "Could not validate the key", Toast.LENGTH_LONG).show();
      Log.e(getClass().getSimpleName(), "Exception validating key", e);
    }

    return result;
  }
(from DeviceAuth/SecureTimeout/app/src/main/java/com/commonsware/android/auth/check/MainActivity.java)

This just encrypts a pointless bit of data:

  private static final byte[] POINTLESS_DATA=new byte[] {1, 2, 3};
(from DeviceAuth/SecureTimeout/app/src/main/java/com/commonsware/android/auth/check/MainActivity.java)

There are several possible outcomes of this. The biggest one is that we could catch a UserNotAuthenticatedException, meaning that the user has not authenticated within our timeout. In that case, needsAuth() returns true, and authenticate() uses createConfirmDeviceCredentialIntent() to trigger authentication. onActivityResult() then calls needsAuth() again, to confirm that we did successfully authenticate the user:

  protected void onActivityResult(int requestCode, int resultCode,
                                  Intent data) {
    if (requestCode==REQUEST_CODE) {
      if (resultCode==RESULT_OK) {
        if (needsAuth(true)) {
          Toast.makeText(this, "Good authentication... but still needs auth?",
            Toast.LENGTH_SHORT).show();
        }
        else {
          Toast.makeText(this, "Authenticated!", Toast.LENGTH_SHORT).show();
        }
      }
      else {
        Toast.makeText(this, "WE ARE UNDER ATTACK!", Toast.LENGTH_SHORT).show();
      }
    }
  }
(from DeviceAuth/SecureTimeout/app/src/main/java/com/commonsware/android/auth/check/MainActivity.java)

The boolean parameter to needsAuth() simply indicates whether we are checking on the first or second pass. If we can successfully encrypt the pointless data, we know that the user has authenticated within our timeout period. If it’s the first pass through needsAuth(), we show a Toast here, otherwise we show it in onActivityResult().

This sample specifically checks for KeyPermanentlyInvalidatedException. This will be thrown if we had a key from before, but the user reset their lock screen, and that key is now no longer valid. If there was data encrypted using that key… the user is now in big trouble. From a JCA standpoint, you should be able to use deleteEntry() on the KeyStore to remove the old key under your key name and create a new one, if desired.

Encrypting Passphrases

The AndroidKeyStore is good for some scenarios, such as your own encrypted files. However, other things will need passphrases and do not integrate with the JCA. A prominent example is SQLCipher for Android. SQLCipher is an extension to SQLite, and SQLite is not tied to Android. Hence, SQLCipher is not tied to Android, and the SQLCipher for Android distribution is focused more on providing a near-clone of the Android SQLite classes (e.g., SQLiteDatabase).

However, you can integrate anything that takes a passphrase with the AndroidKeyStore – and, by extension, with fingerprint-based device authentication — by generating the passphrase and encrypting it. This works akin to encrypting notes, as seen in the earlier example, in that you are encrypting and decrypting from files. However, rather than the files being the actual data, they are merely the keys with which to access that actual data, which has its own security solution.

The DeviceAuth/CipherNote sample application is a clone of the SecureNote sample. There, the note was encrypted and saved as a file. In this sample, the note is saved in SQLCipher for Android, with a generated passphrase being encrypted and saved as a file. In this case, this approach is overkill. However, there are plenty of scenarios where you need a SQLite-like solution for querying and such, but you also want to give the user the option of tying their security to device authentication, instead of having to type in a passphrase themselves.

From a UI standpoint, CipherNote works the same as SecureNote:

A NoteRepository

We start off with a simple SQLiteOpenHelper subclass named DatabaseHelper:

package com.commonsware.android.auth.note;

import android.content.Context;
import net.sqlcipher.database.SQLiteDatabase;
import net.sqlcipher.database.SQLiteOpenHelper;

public class DatabaseHelper extends SQLiteOpenHelper {
  private static final String DATABASE_NAME="note.db";
  private static final int SCHEMA=1;

  DatabaseHelper(Context context) {
    super(context, DATABASE_NAME, null, SCHEMA);

    SQLiteDatabase.loadLibs(context);
  }

  @Override
  public void onCreate(SQLiteDatabase db) {
    db.execSQL("CREATE TABLE note (_id INTEGER PRIMARY KEY AUTOINCREMENT, content TEXT);");
  }

  @Override
  public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    throw new RuntimeException("How did we get here?");
  }
}
(from DeviceAuth/CipherNote/app/src/main/java/com/commonsware/android/auth/note/DatabaseHelper.java)

This sets up a trivial table (note) with a primary key and the content. It also initializes SQLCipher for Android, by means of SQLiteDatabase.loadLibs(context).

That is then wrapped in a NoteRepository, which provides us with an RxJava-based API for loading and saving notes from the encrypted database, given a passphrase. As with many repository-style classes — and as with many things that work with SQLite or SQLCipher for Android — NoteRepository is a singleton. Its sole instance is lazy-created, as it needs a Context for use with DatabaseHelper:

  private static volatile NoteRepository INSTANCE;
  private SQLiteDatabase db;

  private synchronized static NoteRepository init(Context ctxt, char[] passphrase) {
    if (INSTANCE==null) {
      INSTANCE=new NoteRepository(ctxt.getApplicationContext(), passphrase);
    }

    return INSTANCE;
  }

  private synchronized static NoteRepository get() {
    return INSTANCE;
  }

  private NoteRepository(Context ctxt, char[] passphrase) {
    DatabaseHelper helper=new DatabaseHelper(ctxt);

    db=helper.getWritableDatabase(passphrase);
  }
(from DeviceAuth/CipherNote/app/src/main/java/com/commonsware/android/auth/note/NoteRepository.java)

The NoteRepository holds onto the SQLiteDatabase opened from the DatabaseHelper. More importantly, it does not hold onto the passphrase. Ideally, that passphrase should be cleared out of memory after the database is opened, as SQLCipher for Android no longer needs it, and the longer the passphrase is in memory, the more likely it is that somebody is going to find a way to extract it.

Our model object representing the note is named Note:

  static class Note {
    final long id;
    final String content;

    private Note(long id, String content) {
      this.id=id;
      this.content=content;
    }
  }
(from DeviceAuth/CipherNote/app/src/main/java/com/commonsware/android/auth/note/NoteRepository.java)

Loading the Note is reactive. We have a static load() method that returns an Observable based on a LoadObservable:

  static Observable<Note> load(Context ctxt, char[] passphrase) {
    return Observable.create(new LoadObservable(ctxt, passphrase));
  }
(from DeviceAuth/CipherNote/app/src/main/java/com/commonsware/android/auth/note/NoteRepository.java)

LoadObservable does as its name suggests: it loads the note from the database:

  private static class LoadObservable implements ObservableOnSubscribe<Note> {
    private final Context app;
    private final char[] passphrase;

    LoadObservable(Context ctxt, char[] passphrase) {
      this.app=ctxt.getApplicationContext();
      this.passphrase=passphrase;
    }

    @Override
    public void subscribe(ObservableEmitter<Note> e) throws Exception {
      Cursor c=NoteRepository.init(app, passphrase).db
        .rawQuery("SELECT _id, content FROM note", null);

      if (c.isAfterLast()) {
        e.onNext(EMPTY);
      }
      else {
        c.moveToFirst();
        e.onNext(new Note(c.getLong(0), c.getString(1)));
        Arrays.fill(passphrase, '\u0000');
      }

      c.close();
    }
  }
(from DeviceAuth/CipherNote/app/src/main/java/com/commonsware/android/auth/note/NoteRepository.java)

While LoadObservable holds onto the passphrase, it does not make its own copy. Instead, it uses Arrays.fill() to clear out that char array as part of loading the note.

If there are no rows in the database, that means this was the first run of the app (or the user cleared the app’s data). RxJava does not like an ObservableEmitter emitting a null value, so we have a magic EMPTY constant Note to use that signifies that we had no note:

  private static final Note EMPTY=new Note(-1, null);
(from DeviceAuth/CipherNote/app/src/main/java/com/commonsware/android/auth/note/NoteRepository.java)

Otherwise, this code is unremarkable: it queries the database, gets the values out of the Cursor to create a Note, and emits the Note.

There is a corresponding save() method. It is not reactive, but instead is designed to be added to some external RxJava chain. It takes the existing Note and the revised content from the EditText, either inserts or updates the database with the content, and returns a fresh Note instance:

  static Note save(Note note, String content) {
    ContentValues cv=new ContentValues(1);

    cv.put("content", content);

    if (note==EMPTY) {
      long id=NoteRepository.get().db.insert("note", null, cv);

      return new Note(id, content);
    }
    else {
      NoteRepository.get().db.update("note", cv, "_id=?",
        new String[]{String.valueOf(note.id)});

      return new Note(note.id, content);
    }
  }
(from DeviceAuth/CipherNote/app/src/main/java/com/commonsware/android/auth/note/NoteRepository.java)

This way, clients can blindly request a save(), and the repository can determine if that requires an insert or an update, based on whether we are starting with the EMPTY note or not.

RxPassphrase

This sample modifies RxKeyBodega from earlier into RxPassphrase. Here, we have a reactive API to get our passphrase for use with our SQLCipher for Android database… including lazy-creating (and encrypting) that passphrase if it does not already exist.

The public API is a simple static get() method. It takes the file to use for storing the passphrase, the key name for our key in the AndroidKeyStore, and our desired authentication timeout as parameters. It just creates a PassphraseObservable to do the real work:

  static Observable<char[]> get(File encryptedFile, String keyName, int timeout) {
    return Observable.create(new RxPassphrase.PassphraseObservable(encryptedFile, keyName, timeout));
  }
(from DeviceAuth/CipherNote/app/src/main/java/com/commonsware/android/auth/note/RxPassphrase.java)

subscribe() initializes our KeyStore, then sees if the encrypted passphrase file exists, branching to load() or create() methods accordingly:

    @Override
    public void subscribe(ObservableEmitter<char[]> emitter) throws Exception {
      KeyStore ks=KeyStore.getInstance(KEYSTORE);

      ks.load(null);

      if (encryptedFile.exists()) {
        load(ks, emitter);
      }
      else {
        create(ks, emitter);
      }
    }
(from DeviceAuth/CipherNote/app/src/main/java/com/commonsware/android/auth/note/RxPassphrase.java)

create() first creates a 128-character passphrase, using the same base36 algorithm used in an example from the preceding chapter:

    private void create(KeyStore ks, ObservableEmitter<char[]> emitter)
      throws Exception {
      SecureRandom rand=new SecureRandom();
      char[] passphrase=new char[128];

      for (int i=0; i<passphrase.length; i++) {
        passphrase[i]=BASE36_SYMBOLS.charAt(rand.nextInt(BASE36_SYMBOLS.length()));
      }

      createKey(ks, keyName, timeout);

      SecretKey secretKey=(SecretKey)ks.getKey(keyName, null);
      Cipher cipher=Cipher.getInstance("AES/CBC/PKCS7Padding");
      byte[] iv=new byte[BLOCK_SIZE];

      rand.nextBytes(iv);

      IvParameterSpec ivParams=new IvParameterSpec(iv);

      cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParams);

      byte[] toEncrypt=toBytes(passphrase);
      byte[] encrypted=cipher.doFinal(toEncrypt);

      BufferedSink sink=Okio.buffer(Okio.sink(encryptedFile));

      sink.write(iv);
      sink.write(encrypted);
      sink.close();

      emitter.onNext(passphrase);
    }
(from DeviceAuth/CipherNote/app/src/main/java/com/commonsware/android/auth/note/RxPassphrase.java)

create() then lazy-creates our key in our KeyStore, using the same createKey() method as was used previously:

    private void createKey(KeyStore ks, String keyName, int timeout)
      throws Exception {
      KeyStore.Entry entry=ks.getEntry(keyName, null);

      if (entry==null) {
        KeyGenParameterSpec spec=
          new KeyGenParameterSpec.Builder(keyName,
            KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
            .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
            .setUserAuthenticationRequired(true)
            .setUserAuthenticationValidityDurationSeconds(timeout)
            .setRandomizedEncryptionRequired(false)
            .build();

        KeyGenerator keygen=
          KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, KEYSTORE);

        keygen.init(spec);
        keygen.generateKey();
      }
    }
(from DeviceAuth/CipherNote/app/src/main/java/com/commonsware/android/auth/note/RxPassphrase.java)

We then create a random initialization vector, and use that to help create our Cipher for encryption. We encrypt the passphrase and IV bytes, writing the results to the designated file, before returning the cleartext passphrase. That passphrase can then be passed to the NoteRepository for the purposes of creating our encrypted database.

Note that encryption might throw a UserNotAuthenticatedException, which the client needs to catch and route through the createConfirmDeviceCredentialIntent()-based UI for authenticating the user.

If our encrypted passphrase file already exists, we can just open and decrypt it:

    private void load(KeyStore ks, ObservableEmitter<char[]> emitter)
      throws Exception {
      BufferedSource source=Okio.buffer(Okio.source(encryptedFile));
      byte[] iv=source.readByteArray(BLOCK_SIZE);
      byte[] encrypted=source.readByteArray();

      source.close();

      SecretKey secretKey=(SecretKey)ks.getKey(keyName, null);
      Cipher cipher=Cipher.getInstance("AES/CBC/PKCS7Padding");

      cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv));

      byte[] decrypted=cipher.doFinal(encrypted);
      char[] passphrase=toChars(decrypted);

      emitter.onNext(passphrase);
    }
  }
(from DeviceAuth/CipherNote/app/src/main/java/com/commonsware/android/auth/note/RxPassphrase.java)

We do not use createKey() here, as by definition our key should already exist, as we used it to encrypt the file. If for some reason our key is lost, then our data is lost, and we have to start over from scratch anyway.

Using the NoteRepository and RxPassphrase

The overall flow of MainActivity has not changed: we still call load() from onCreate() and still call save() from the action bar item click. Merely their implementations have changed, to blend NoteRepository and RxPassphrase.

load() starts an RxJava chain by asking RxPassphrase to get() the passphrase:

  private void load() {
    final Context app=getApplicationContext();
    File encryptedFile=new File(getFilesDir(), FILENAME);

    RxPassphrase.get(encryptedFile, KEY_NAME, TIMEOUT_SECONDS)
      .subscribeOn(Schedulers.io())
      .observeOn(AndroidSchedulers.mainThread())
      .flatMap(chars -> NoteRepository.load(app, chars))
      .subscribe(this::onNoteReady,
        t -> {
          if (t instanceof UserNotAuthenticatedException) {
            requestAuth(REQUEST_LOAD);
          }
          else {
            Toast.makeText(MainActivity.this, t.getMessage(), Toast.LENGTH_LONG).show();
            Log.e(getString(R.string.app_name), "Exception loading encrypted file", t);
          }
      });
  }
(from DeviceAuth/CipherNote/app/src/main/java/com/commonsware/android/auth/note/MainActivity.java)

That passphrase is then used with NoteRepository.load() to get the note from the encrypted database, using flatMap() to attach the Observable from NoteRepository.load() onto the existing chain. Then, if everything succeeds, we call onNoteReady() to hold onto the Note object and populate the EditText:

  private void onNoteReady(NoteRepository.Note note) {
    this.note=note;
    textarea.setText(note.content);
  }
(from DeviceAuth/CipherNote/app/src/main/java/com/commonsware/android/auth/note/MainActivity.java)

If we get a UserNotAuthenticatedException, we go through the same requestAuth() as before, triggering authentication. In onActivityResult(), we call load() again to try to get the Note, now that the user has authenticated.

The passphrase is stored on internal storage, in getFilesDir() (new File(getFilesDir(), FILENAME)). Hence, in principle, it will only get deleted if the database itself gets deleted, such as the user choosing “Clear Data” for our app in Settings. This is important, as our data will be lost if either the database or the encrypted passphrase file are lost.

save() does not need to use RxPassphrase, as NoteRepository should already have the open database. So, we can just use NoteRepository.save() to persist the note:

  private void save() {
    Observable.just(textarea.getText().toString())
      .map(content -> NoteRepository.save(note, content))
      .subscribeOn(Schedulers.io())
      .observeOn(AndroidSchedulers.mainThread())
      .subscribe(this::onNoteSaved,
        t -> {
          if (t instanceof UserNotAuthenticatedException) {
            requestAuth(REQUEST_LOAD);
          }
          else {
            Toast.makeText(MainActivity.this, t.getMessage(), Toast.LENGTH_LONG).show();
            Log.e(getString(R.string.app_name), "Exception loading encrypted file", t);
          }
        });
  }
(from DeviceAuth/CipherNote/app/src/main/java/com/commonsware/android/auth/note/MainActivity.java)

Here, onNoteSaved() just shows the “Saved!” Toast, plus calls onNoteReady() to ensure that we have the Note for any subsequent save() call:

  private void onNoteSaved(NoteRepository.Note note) {
    Toast.makeText(this, R.string.saved, Toast.LENGTH_LONG).show();
    onNoteReady(note);
  }
(from DeviceAuth/CipherNote/app/src/main/java/com/commonsware/android/auth/note/MainActivity.java)

A Key(Store) Limitation

The keys in the AndroidKeyStore are tied to a secure keyguard. That is why these samples check for a secure keyguard before proceeding.

But what if the user had a secure keyguard, then downgrades to merely swipe-to-unlock?

When they downgrade, they should get a system warning that they will lose some information. In particular, on fingerprint-enabled devices, they should get a note about losing stored fingerprints. However, the system warning may not be a sufficient deterrent, and the user may downgrade their keyguard security anyway.

If that happens, your keys in the AndroidKeyStore get wiped out. Even if the user turns right around and sets up a secure keyguard again, whatever had been in the AndroidKeyStore is gone, and anything encrypted with one of those keys will be unrecoverable.

Changing between different types of security — such as switching between a PIN and a passphrase — is fine and will not affect the AndroidKeyStore.