Friday, March 29, 2024

How to use the Android Keystore to store passwords and other sensitive information

Share

DataEncryptionLockedA few months ago, Godfrey Nolan wrote an excellent article discussing how an Android app developer can store user passwords and sensitive/personal information. The Android keystore provides a secure system level credential storage. With the keystore, an app can create a new Private/Public key pair, and use this to encrypt application secrets before saving it in the private storage folders. In this article, we are going to show how to use the Android Keystore to create and delete keys, and how to use these keys to encrypt and decrypt user supplied text.

Preparation

Before we begin coding, it is helpful to understand a bit about the Android Keystore, and it’s capabilities. The Keystore is not used directly for storing application secrets such as password, however, it provides a secure container, which can be used by apps to store their private keys, in a way that’s pretty difficult for malicious (unauthorised) users and apps to retrieve.

As its name suggests, an app can store multiple keys in the Keystore, but an app can only view, and query, its own keys. Ideally, with the keystore, an app would generate/or receive a private/public key pair, which would be stored in the keystore. The public key can then be used to encrypt application secrets, before being stored in the app specific folders, with the private key used to decrypt the same information when needed.

Although the Android Keystore provider was introduced in API level 18 (Android 4.3), the Keystore itself has been available since API 1, restricted to use by VPN and WiFi systems.

The Keystore itself is encrypted using the user’s own lockscreen pin/password, hence, when the device screen is locked the Keystore is unavailable. Keep this in mind if you have a background service that could need to access your application secrets.

Layout

The main layout for our sample app is a ListView, with items made up of a list of all the keys (actually the key aliases/names) created by the app. This is saved as layout/activity_main.xml.

<ListView xmlns:android=”http://schemas.android.com/apk/res/android”
xmlns:tools=”http://schemas.android.com/tools”
android:id=”@+id/listView”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:fillViewport=”true”
tools:context=”com.sample.foo.simplekeystoreapp.MainActivity”>

</ListView>

Each item on the list contains a TextView representing the key alias, a button to delete the key, and buttons each to encrypt and decrypt text. This layout/list_item.xml in our project.
aa_keystore_list_item

<?xml version=”1.0″ encoding=”utf-8″?>
<RelativeLayout
xmlns:card_view=”http://schemas.android.com/apk/res-auto”
xmlns:android=”http://schemas.android.com/apk/res/android”
android:id=”@+id/cardBackground”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
card_view:cardCornerRadius=”4dp”
android:layout_margin=”5dp”>

<TextView
android:id=”@+id/keyAlias”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:gravity=”center_vertical”
android:textSize=”30dp”/>

<Button
android:id=”@+id/deleteButton”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_below=”@id/keyAlias”
android:layout_alignParentLeft=”true”
android:layout_centerHorizontal=”true”
android:text=”@string/delete”
style=”@style/Base.Widget.AppCompat.Button.Borderless” />

<Button
android:id=”@+id/encryptButton”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_below=”@+id/keyAlias”
android:layout_alignRight=”@+id/keyAlias”
android:text=”@string/encrypt”
style=”@style/Base.Widget.AppCompat.Button.Borderless”/>

<Button
android:id=”@+id/decryptButton”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_below=”@+id/keyAlias”
android:layout_toLeftOf=”@+id/encryptButton”
android:text=”@string/decrypt”
style=”@style/Base.Widget.AppCompat.Button.Borderless”/>

</RelativeLayout>

List Header

The List header is added to the ListView using the method

View listHeader = View.inflate(this, R.layout.activity_main_header, null);
listView.addHeaderView(listHeader);

aa_keystore_layout
In the image above, the ListView is currently empty, so only the List header is visible. The List Header is pretty straightforward, with an EditText at the top, which expects a string to be used as an alias when creating a key. A button to generate new keys lies immediately below this. The button is followed by three EditTexts, one expecting a string input to be encrypted, another displays the result of the encryption, and the third shows the decrypted string (for a successful decryption). This file is saved in layout/activity_main_header.xml.

<?xml version=”1.0″ encoding=”utf-8″?>
<RelativeLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:orientation=”vertical”
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:gravity=”center_horizontal”
android:paddingLeft=”@dimen/activity_horizontal_margin”
android:paddingRight=”@dimen/activity_horizontal_margin”
android:paddingTop=”@dimen/activity_vertical_margin”
android:paddingBottom=”@dimen/activity_vertical_margin”>

<EditText
android:id=”@+id/aliasText”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_centerHorizontal=”true”
android:hint=”@string/key_alias”/>

<Button
android:id=”@+id/generateKeyPair”
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:layout_below=”@id/aliasText”
android:layout_centerHorizontal=”true”
android:layout_alignParentRight=”true”
android:text=”@string/generate”
android:onClick=”createNewKeys” />

<EditText
android:id=”@+id/startText”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_below=”@id/generateKeyPair”
android:layout_centerHorizontal=”true”
android:hint=”@string/initial_text”/>

<EditText
android:id=”@+id/encryptedText”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_below=”@id/startText”
android:layout_centerHorizontal=”true”
android:editable=”false”
android:textIsSelectable=”true”
android:hint=”@string/final_text”/>

<EditText
android:id=”@+id/decryptedText”
android:layout_width=”match_parent”
android:layout_height=”wrap_content”
android:layout_below=”@id/encryptedText”
android:layout_centerHorizontal=”true”
android:editable=”false”
android:textIsSelectable=”true”
android:hint=”@string/decrypt_result”/>
</RelativeLayout>

MainActivity

As with any Activity, we begin with the onCreate() method. The first thing we do is get a reference to the AndroidKeyStore, and then initialize it, using:

Keystore.getInstance(“AndroidKeyStore”);
keystore.load(null)

We then call our refreshKeys() method (discussed next) to list all the keys our app has stored in the Keystore. This ensures that any keys in the Keystore will be shown immediately when the ListView is initialized.

List all keys in the Keystore

aa_keystore_keys
To fetch an Enumeration of all keys available to an app in the keystore, simply call the aliases() method. In our refreshKeys() method below, we fetch the keystore aliases, and put the returned Strings into an ArrayList (used by our ListView’s Adapter).

private void refreshKeys() {
keyAliases = new ArrayList<>();
try {
Enumeration<String> aliases = keyStore.aliases();
while (aliases.hasMoreElements()) {
keyAliases.add(aliases.nextElement());
}
}
catch(Exception e) {}

if(listAdapter != null)
listAdapter.notifyDataSetChanged();
}

Add a new key to the Keystore

aa_keystore_new_key
Each key created by an app must have a unique alias, which can be any String. We use a KeyPairGeneratorSpec object to build the specifications of our required key. You can set the validity period of the key (setStartDate() and setEndDate()), set the alias and the subject (for self signed keys) among others. The subject must be an X500Principal object that resolves to a String of the form “CN=Common Name, O=Organization, C=Country”.

To generate a Public/Private keypair, we need a KeyPairGenerator object. We get an instance of KeyPairGenerator set to use the RSA algorithm with the “AndroidKeyStore”. Calling generateKeyPair() creates the new pair of keys (Private and corresponding Public key), and adds it to the Keystore.

public void createNewKeys(View view) {
String alias = aliasText.getText().toString();
try {
// Create new key if needed
if (!keyStore.containsAlias(alias)) {
Calendar start = Calendar.getInstance();
Calendar end = Calendar.getInstance();
end.add(Calendar.YEAR, 1);
KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(this)
.setAlias(alias)
.setSubject(new X500Principal(“CN=Sample Name, O=Android Authority”))
.setSerialNumber(BigInteger.ONE)
.setStartDate(start.getTime())
.setEndDate(end.getTime())
.build();
KeyPairGenerator generator = KeyPairGenerator.getInstance(“RSA”, “AndroidKeyStore”);
generator.initialize(spec);

KeyPair keyPair = generator.generateKeyPair();
}
} catch (Exception e) {
Toast.makeText(this, “Exception ” + e.getMessage() + ” occured”, Toast.LENGTH_LONG).show();
Log.e(TAG, Log.getStackTraceString(e));
}
refreshKeys();
}

Delete key from the Keystore

aa_keystore_delete
Deleting a key from the keystore is pretty straightforward. Armed with the key alias, call keystore.deleteEntry(keyAlias). There is no way to restore a deleted key, so you may want to be certain before using this.

public void deleteKey(final String alias) {
AlertDialog alertDialog =new AlertDialog.Builder(this)
.setTitle(“Delete Key”)
.setMessage(“Do you want to delete the key “” + alias + “” from the keystore?”)
.setPositiveButton(“Yes”, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
try {
keyStore.deleteEntry(alias);
refreshKeys();
} catch (KeyStoreException e) {
Toast.makeText(MainActivity.this,
“Exception ” + e.getMessage() + ” occured”,
Toast.LENGTH_LONG).show();
Log.e(TAG, Log.getStackTraceString(e));
}
dialog.dismiss();
}
})
.setNegativeButton(“No”, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
}
})
.create();
alertDialog.show();
}

Encrypt block of text

aa_keystore_encrypt
Encrypting a block of text is performed with the Public key of the Key Pair. We retrieve the Public Key, request for a Cipher using our preferred encryption/decryption transformation (“RSA/ECB/PKCS1Padding”), then initialize the cipher to perform encryption (Cipher.ENCRYPT_MODE) using the retrieved Public Key. Cipher operates on (and returns) a byte []. We wrap the Cipher in a CipherOutputStream, along with a ByteArrayOutputStream to handle the encryption complexities. The result of the encryption process is then converted to a Base64 string for display.

public void encryptString(String alias) {
try {
KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(alias, null);
RSAPublicKey publicKey = (RSAPublicKey) privateKeyEntry.getCertificate().getPublicKey();

// Encrypt the text
String initialText = startText.getText().toString();
if(initialText.isEmpty()) {
Toast.makeText(this, “Enter text in the ‘Initial Text’ widget”, Toast.LENGTH_LONG).show();
return;
}

Cipher input = Cipher.getInstance(“RSA/ECB/PKCS1Padding”, “AndroidOpenSSL”);
input.init(Cipher.ENCRYPT_MODE, publicKey);

ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
CipherOutputStream cipherOutputStream = new CipherOutputStream(
outputStream, input);
cipherOutputStream.write(initialText.getBytes(“UTF-8”));
cipherOutputStream.close();

byte [] vals = outputStream.toByteArray();
encryptedText.setText(Base64.encodeToString(vals, Base64.DEFAULT));
} catch (Exception e) {
Toast.makeText(this, “Exception ” + e.getMessage() + ” occured”, Toast.LENGTH_LONG).show();
Log.e(TAG, Log.getStackTraceString(e));
}
}

Decryption

Decryption is basically the Encryption process in reverse. Decryption is done using the Private Key of the key pair. We then initialize a Cipher with the same transformation algorithm used for encryption, but set to Cipher.DECRYPT_MODE. The Base64 string is decoded to a byte[], which is then placed in a ByteArrayInputStream. We then use a CipherInputStream to decrypt the data into a byte[]. This is then displayed as a String.

public void decryptString(String alias) {
try {
KeyStore.PrivateKeyEntry privateKeyEntry = (KeyStore.PrivateKeyEntry)keyStore.getEntry(alias, null);
RSAPrivateKey privateKey = (RSAPrivateKey) privateKeyEntry.getPrivateKey();

Cipher output = Cipher.getInstance(“RSA/ECB/PKCS1Padding”, “AndroidOpenSSL”);
output.init(Cipher.DECRYPT_MODE, privateKey);

String cipherText = encryptedText.getText().toString();
CipherInputStream cipherInputStream = new CipherInputStream(
new ByteArrayInputStream(Base64.decode(cipherText, Base64.DEFAULT)), output);
ArrayList<Byte> values = new ArrayList<>();
int nextByte;
while ((nextByte = cipherInputStream.read()) != -1) {
values.add((byte)nextByte);
}

byte[] bytes = new byte[values.size()];
for(int i = 0; i < bytes.length; i++) {
bytes[i] = values.get(i).byteValue();
}

String finalText = new String(bytes, 0, bytes.length, “UTF-8”);
decryptedText.setText(finalText);

} catch (Exception e) {
Toast.makeText(this, “Exception ” + e.getMessage() + ” occured”, Toast.LENGTH_LONG).show();
Log.e(TAG, Log.getStackTraceString(e));
}
}

Android Developer Newsletter

Do you want to know more? Subscribe to our Android Developer Newsletter. Just type in your email address below to get all the top developer news, tips & links once a week in your inbox:Email:
PS. No spam, ever. Your email address will only ever be used for Android Dev Weekly.

Round up

The Android Keystore makes creating and managing app keys a breeze, and provides a safe and relatively secure vault for applications to store encryption keys. Of course the Public key can also be sent to your server, and the server public key sent to your apps, to ensure secure communications between your application and your server. As usual, the complete source code is available on github for use as you please. For additions, corrections and/or discussions, leave a comment below, we are eager to hear from you.

Read more

More News