Affiliate links on Android Authority may earn us a commission. Learn more.
How to use the Android Keystore to store passwords and other sensitive information
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.
<?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);
In the image above (no longer available), 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
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
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
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
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));
}
}
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.