Protection of the sensitive date stored on the mobile devices is a hot topic. There are a huge number of apps that provide access to the corporate (confidential) data, banking and payment tools, social networks and many other web-services, where user authorization is required. That it why it is very important for mobile app developers to care about data protection and build solutions with access security and credential protection in mind. For these purposes, Keystone API 18 brought native support for Android encryption algorithms. It added AndroidKeyStore provider, which allows to:
- Generate new private cryptographic key or a pair of keys
- Work with Keystore entries โ receive the list of saved keys
- Sign/verify data
- Transfer responsibility for safety of Keystore access to operating system.
In this article, we will show how to use this technology in practice to build secure Android apps with user password encryption and also support earlier Android versions providing password protection by means of other technologies.
Contents
Introduction
For Keystore API 1 you need to manually create Keystore file and make sure that access to it is secure. Usually, for 90% of software, private directory is a safe enough place to store data. However, this is not the case for rooted devices, where said data can be easily accessed.
Additionally, Keystore can be protected with a password. However, while this solution does increase security, it is very inconvenient for the user, as it forces them to enter password each time they want to access Keystore. In any case, there is always the possibility that the password will be cracked and perpetrator will get your Keystore file.
As mentioned above, AndroidKeyStore provider was added in Keystore API 18. As a result, all the hard work on providing security now lies on the system itself. We no longer need to manually protect our storage with a password, the system will do it automatically based on the userโs LockScreen settings (whether itโs a PIN, graphical password, or a fingerprint). If device supports hardware key storage, than it will be used instead of a software one, eliminating the possibility that perpetrators will be able to obtain private keys. When using AndroidKeystore, each application only has access to the keys, used in the context of this application.
The Problem
We have a minSdkVersion 16 application that we need an authorization in order to run. We need to save credentials to automatically log in the next time an app has been launched. In order to do this in a secure way, we will encrypt the password with AndroidKeyStore before saving it in Shared Preferences.
However, since AndroidKeyStore provider was only added since API18, we need to make a separate implementation for password storage for different versions of Android OS.
In this example we will use asymmetric cryptographic algorithm called RSA, where we need to generate and use a pair of keys (a public and a private one). These keys work together: an open key to encrypt data and a private key to decrypt it.
RSA is well suited to encrypt small blocks of data, such as passwords and AES keys. However, when it comes to encrypting large amounts of data, this algorithm is a poor choice because of its performance, while something like AES, for example, is way faster.
Read also:
How to Receive and Handle SMS on Android
Implementation
First, we create a PassowrdStorageHelper class that will provide High level API for working with specific data. Letโs define a specific interface that will implement this class.
interface PasswordStorageInterface
private interface PasswordStorageInterface {
// Initialize all necessary objects for working with AndroidKeyStore
boolean init(Context context);
// Set data which we want to keep in secret
void setData(String key, byte[] data);
// Get stored secret data by key
byte[] getData(String key);
// Remove stored data
void remove(String key);
}
Init() โ generates a pair of keys if they donโt exist yet. Keys will be available for further use in KeyStore via an alias specified during generation.
setData() โ allows to encrypt data and save the results in SharedPreferences by using the public key from the KeyStore.
getData() โ allows to get encrypted password from SharedPreferences by using the private key to decrypt data
remove() โ removes decrypted password, saved in SharedPreferences.
In order for PasswordStorageHelper to distinguish between devices that support AndroidKeyStore and those that done, we need to create two classes that implement PasswordStorageInterface.
class PasswordStorageHelper_SDK16 implements PasswordStorageInterface
class PasswordStorageHelper_SDK18 implements PasswordStorageInterface
In the PasswordStorageHelper constructor, we need to create specific class object that implements algorithms for working with protected data depending on your current Android version:
public class PasswordStorageHelper {
private PasswordStorageInterface passwordStorage = null;
public PasswordStorageHelper(Context context) {
if (android.os.Build.VERSION.SDK_INT < 18) {
passwordStorage = new PasswordStorageHelper_SDK16();
} else {
passwordStorage = new PasswordStorageHelper_SDK18();
}
passwordStorage.init(context);
}
}
Caution: on certain API 18+ devices an exception can occur when initializing PasswordStorageHelper_SDK18().
In this case, we will use the second implementation of the helper:
boolean isInitialized = false;
try {
isInitialized = passwordStorage.init(context);
} catch (Exception ex) {
Log.e(LOG_TAG, "PasswordStorage initialization error:" + ex.getMessage(), ex);
}
if (!isInitialized && passwordStorage instanceof PasswordStorageHelper_SDK18) {
passwordStorage = new PasswordStorageHelper_SDK16();
passwordStorage.init(context);
}
Thus, by using the Facade pattern we painlessly split different implementations for working with encrypted data using different versions of Android OS. At the same time, we donโt need to worry about how to correctly initialize helper object โ everything happens automatically.
We also prepared an article that will help you learn how to receive SMS messages on Android.
Details of PasswordStorageHelper_SDK18_init() implementation
First, we need to check whether private/public keys are already exist in order to avoid generating them a second time.
try {
ks = KeyStore.getInstance(KEYSTORE_PROVIDER_ANDROID_KEYSTORE);
//Use null to load Keystore with default parameters.
ks.load(null);
// Check if Private and Public already keys exists. If so we don't need to generate them again
PrivateKey privateKey = (PrivateKey) ks.getKey(alias, null);
if (privateKey != null && ks.getCertificate(alias) != null) {
PublicKey publicKey = ks.getCertificate(alias).getPublicKey();
if (publicKey != null) {
// All keys are available.
return true;
}
}
} catch (Exception ex) {
return false;
}
We should prepare AlgorithmParameterSpec, this is necessary for further KeyPairGenerator initiation.
// Create a start and end time, for the validity range of the key pair that's about to be
// generated.
Calendar start = new GregorianCalendar();
Calendar end = new GregorianCalendar();
end.add(Calendar.YEAR, 10);
// Specify the parameters object which will be passed to KeyPairGenerator
AlgorithmParameterSpec spec;
if (android.os.Build.VERSION.SDK_INT < 23) {
spec = new android.security.KeyPairGeneratorSpec.Builder(context)
// Alias - is a key for your KeyPair, to obtain it from Keystore in future.
.setAlias(alias)
// The subject used for the self-signed certificate of the generated pair
.setSubject(new X500Principal("CN=" + alias))
// The serial number used for the self-signed certificate of the generated pair.
.setSerialNumber(BigInteger.valueOf(1337))
// Date range of validity for the generated pair.
.setStartDate(start.getTime()).setEndDate(end.getTime())
.build();
} else {
spec = new KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_DECRYPT)
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
.build();
}
Now we can start to actually generate keys:
KeyPairGenerator kpGenerator;
try {
// Initialize a KeyPair generator using the the intended algorithm (in this example, RSA
// and the KeyStore. This example uses the AndroidKeyStore.
kpGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM_RSA, KEYSTORE_PROVIDER_ANDROID_KEYSTORE);
kpGenerator.initialize(spec);
// Generate private/public keys
kpGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | NoSuchProviderException e) {
}
Along with the software implementation, some devices are also support the hardware key storages. In order to confirm whether a device supports such a storage, we can use the KeyChain.isBoundKeyAlgorithm(Algorithm);
However, Android 6.0 saw a much more extended API for Keystore, which is why this method for checking whether there is a hardware key storage doesnโt work anymore. Now, in order to conduct a check, we need keys to already be in the storage. As a result, we use the following method:
boolean isHardwareBackedKeystoreSupported;
if (android.os.Build.VERSION.SDK_INT < 23) {
isHardwareBackedKeystoreSupported = KeyChain.isBoundKeyAlgorithm(KeyProperties.KEY_ALGORITHM_RSA);
} else {
PrivateKey privateKey = (PrivateKey) ks.getKey(alias, null);
KeyChain.isBoundKeyAlgorithm(KeyProperties.KEY_ALGORITHM_RSA);
KeyFactory keyFactory = KeyFactory.getInstance(privateKey.getAlgorithm(), "AndroidKeyStore");
KeyInfo keyInfo = keyFactory.getKeySpec(privateKey, KeyInfo.class);
isHardwareBackedKeystoreSupported = keyInfo.isInsideSecureHardware();
}
Log.d(LOG_TAG, "Hardware-Backed Keystore Supported: " + isHardwareBackedKeystoreSupported);
PasswordStorageHelper_SDK18 – setData()
In this method, we need to encrypt passwords with the public key that is already located in the KeyStore, after which we need to save encrypted password in the SharedPreferences.
KeyStore ks = KeyStore.getInstance(KEYSTORE_PROVIDER_ANDROID_KEYSTORE);
ks.load(null); //Use null to load Keystore with default parameters.
if(ks.getCertificate(alias) == null) return;
PublicKey publicKey = ks.getCertificate(alias).getPublicKey();
if (publicKey == null) {
Log.e(LOG_TAG, "Error: Public key was not found in Keystore");
return;
}
Encrypting data with a public key.
Cipher cipher = Cipher.getInstance(RSA_ECB_PKCS1_PADDING);
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encrypted = cipher.doFinal(data);
String value = Base64.encodeToString(encrypted, Base64.DEFAULT);
Saving the result
Editor editor = preferences.edit();
editor.putString(key, value);
editor.commit();
PasswordStorageHelper_SDK18 – getData()
We do everything similarly to setData(), only in a reverse order โ get the encrypted password from SharedPreferences and decrypt it with a private key.
Keystore initialization
KeyStore ks = KeyStore.getInstance(KEYSTORE_PROVIDER_ANDROID_KEYSTORE);
ks.load(null); //Use null to load Keystore with default parameters.
PrivateKey privateKey = (PrivateKey) ks.getKey(alias, null);
Decrypting data with a private key
byte[] encryptedBuffer = Base64.decode(encryptedData, Base64.DEFAULT);
Cipher cipher = Cipher.getInstance(RSA_ECB_PKCS1_PADDING);
cipher.init(Cipher.DECRYPT_MODE, decryptionKey);
byte[] decrypted = cipher.doFinal(encryptedBuffer);
In this example of the PasswordStorageHelper_SDK16 class implementation, initial password is simply encrypted with the Base64 and saved in SharedPreferences. This approach poses certain risks. Any app with root access will be able to access your saved password and use it for its own purposes. This is why, when confidentiality of credentials is extremely important, you need to find other ways to implement PasswordStorageHelper for Android versions that doesnโt support AndroidKeyStore provider.
You can also take a look at the source code
And to learn more about testing, check out our article on Android app penetration testing.