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.
To understand this chapter, please read the preceding chapter on device authentication first. Also, the examples in this chapter make extensive use of RxJava.
First, let’s review some…ummmm… “key” terms that will be used in this chapter.
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.
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:
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.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.
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:
KeyStore
type name (e.g., KeyStore.getInstance("this does not exist"))
)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
.
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:
setUserAuthenticationRequired(true)
, to indicate that the key being generated
requires user authenticationsetUserAuthenticationValidityDurationSeconds()
, to indicate how long we have,
after user authentication, to be able to use the key, before the user needs
to re-authenticateThe 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.
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()
.
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.
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”)
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.
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);
}
});
}
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:
Toast
if everything seems OKrequestAuth()
if we got a UserNotAuthenticatedException
, indicating
that we need the user to re-authenticate with the device before we can encryptToast
if something else went wrongrequestAuth()
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);
}
}
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();
}
}
If the user declined to authenticate, we show a Toast
and finish()
the
activity, since there is nothing useful that we can do.
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));
}
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;
}
}
}
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;
}
}
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();
}
}
}
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();
}
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.
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();
}
}
If it is not secure, we show a Toast
and finish()
our activity to leave the
app.
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.
Now, let’s turn our attention to the load()
method.
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);
}
});
}
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:
String
(using the UTF8
charset) and apply that to the EditText
UserNotAuthenticatedException
, in which case we request that the
user authenticate, and if that succeeds, onActivityResult()
will try the load()
againToast
and log
the exceptionThe 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);
}
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;
}
(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));
}
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;
}
}
}
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);
}
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);
}
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);
}
}
}
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();
}
}
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;
}
This just encrypts a pointless bit of data:
private static final byte[] POINTLESS_DATA=new byte[] {1, 2, 3};
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();
}
}
}
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.
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
:
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?");
}
}
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);
}
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;
}
}
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));
}
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();
}
}
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);
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);
}
}
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.
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));
}
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);
}
}
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);
}
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();
}
}
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);
}
}
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.
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);
}
});
}
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);
}
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);
}
});
}
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);
}
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
.