Michał
7 min readApr 3, 2019

Using fingerprint for encrypt text and decrypt cipher. Storing cipher on Android device for further use. Example coded in Kotlin.

I would like to show you my journey with fingerprint. I found interesting topic how we can encrypt and decrypt string only using fingerprint. After doing some research I decided to save it somewhere to have it for later. Because it could be useful for somebody I made it public on medium and github.

A candle loses nothing by lighting another candle.

When using Fingerprint I found only encryption examples with no decryption by fingerprint. This is important if you would like to create login form to your application by fingerprint and see how the process works and understand flow.

In this example we will use the message, code, PIN or any string you type in. Encrypt this string and save to shared preferences and then decrypt it by fingerprint even after application restart.

I will create list of subject I will go through to make it clean and easy to understand and follow. I will insert links for those of you who would like to understand topic more deeply.

  1. KeyStore
  2. KeyGenerator
  3. Cipher (Encryption / Decryption)
  4. Fingerprint Authentication

1. KeyStore

There is a good explanation what KeyStore from Android website linked here KeyStore:

“ The Android Keystore system lets you store cryptographic keys in a container to make it more difficult to extract from the device.”

To get KeyStore object we have to type:

private const val ANDROID_KEY_STORE = "AndroidKeyStore"val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE)
keyStore.load(null)

What is the “AndroidKeyStore” String value:

”Using the AndroidKeyStore provider takes place through all the standard KeyStore APIs.”

We will need this information as we will go through all aliases in KeyStore to find whenever we already generate key by KeyGenerator. Next chapter we will generate key to KeyStore or try to find one if it exists.

2. KeyGenerator

Next step is to generate symmetric key used for our encryption and decryption.

Symmetric-key algorithms are algorithms for cryptography that use the same cryptographic keys for both encryption of plaintext and decryption of ciphertext.”

Following the definition of KeyGenerator class:

“ This class provides the functionality of a secret (symmetric) key generator.”

We choose AES algorithm for our symmetric key setting parameters as:

val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
ANDROID_KEY_STORE
)

First parameter KeyProperties.KEY_ALGORITHM_AES defines algorithm and second parameter ANDROID_KEY_STORE means that the key will be automatically stored in KeyStore after we run init and generateKey method.

keyGenerator.init(builder.build())
keyGenerator.generateKey()

You should notice builder object inside init method of keyGenerator.

val builder = KeyGenParameterSpec.Builder(
keyName,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)

Before we generate the key we need to define what it is used for only encryption or decryption or both. First we need to give our key name, this name will be passed in keyName variable and stored in KeyStore. Next we have to define that key will be used for encrypt and decrypt.

builder.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setKeySize(256)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)

Next step we define block mode, key size and padding.

Please refresh your memory about block mode, initialize vector, if you get little confused, this link will help you a lot when you get confused or get some errors while coding — Block Cipher.

I suggest that keySize should be 256 despite that it is slower, we won’t encrypt often and big amount of text.

Encryption Padding is defined to PCKS#7

builder.setUserAuthenticationRequired(true)

Very important line in key specification is setUserAuthenticationRequired. We need it set to true then the key can be used only when user is authorized. Only then we can use android fingerprint authorization and keep our key secured.

We can find on stackoverflow answers to set it to false. PLEASE DO NOT DO THAT! It can be set to false only to debug. When you have encryption and decryption issues, for example when second time key generated will corrupt decryption this will work as work around but it won’t use fingerprint for encryption and decryption and won’t solve the real issue.

We ended creating our SecretKey and it has been stored automatically in the KeyStore.

SecretKey

Here is full code for key generator and receiving SecretKey from KeyStore It will be required when init the Cipher objects:

private val key: SecretKey =
KeyStoreTools.getKeyFromKeyStore(KEY_NAME)
fun getKeyFromKeyStore(keyname: String): SecretKey {
val keyStore = createKeyStore()
if (!keyExists(keyname)) {
generateAesKey(keyname)
}
return keyStore.getKey(keyname, null) as SecretKey
}
private fun createKeyStore(): KeyStore {
val keyStore = KeyStore.getInstance(ANDROID_KEY_STORE)
keyStore.load(null)
return keyStore
try {
val keyGenerator = KeyGenerator.getInstance(
KeyProperties.KEY_ALGORITHM_AES,
ANDROID_KEY_STORE
)
val builder = KeyGenParameterSpec.Builder(
keyName,
KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
)
builder.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setKeySize(256)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
builder.setUserAuthenticationRequired(true)
keyGenerator.init(builder.build())
keyGenerator.generateKey()
} catch (e: NoSuchAlgorithmException) {
throw RuntimeException("Failed to create a symmetric key", e)
} catch (e: NoSuchProviderException) {
throw RuntimeException("Failed to create a symmetric key", e)
} catch (e: InvalidAlgorithmParameterException) {
throw RuntimeException("Failed to create a symmetric key", e)
}
}

3. Cipher

I will introduce you two ciphers one for encryption and second one for decryption. We can find a lot of articles about encryption the messages but still we can’t find decryption of ciphertext, moreover let think about decrypted message or PIN after application restart.

val cipherEnc: Cipher = createCipher()
val cipherDec: Cipher = createCipher()
private fun createCipher(): Cipher {
return Cipher.getInstance(
KeyProperties.KEY_ALGORITHM_AES + "/"
+ KeyProperties.BLOCK_MODE_CBC + "/"
+ KeyProperties.ENCRYPTION_PADDING_PKCS7
)
}

Please look at two objects of Cipher, you can see that we pass in the parameter for new instance all information that we set in our key generator. This is very important to have the same properties in key as in cipher object, you can face a lot of issue and errors if you won’t be careful.

Encryption

To prepare our cipher object for encryption we have to run init method with proper parameters:

cipherEnc.init(Cipher.ENCRYPT_MODE, key)

You can notice that the first one is defining if our cipher object encrypt the string. Second parameter is a SecretKey we generated in chapter 2.

To encrypt the message we can run doFinal and then we combine with IV from cipher. We return encoded ciphertext and IV with Base64 to secure string coding.

fun encrypt(cipher: Cipher, plainText: ByteArray, separator: String): String {
try {
val enc = cipher.doFinal(plainText)
return Base64.encodeToString(
enc,
Base64.DEFAULT
) + separator + Base64.encodeToString(
cipher.iv,
Base64.DEFAULT)
} catch (e: Exception) {
e.printStackTrace()
}
return ""
}

Now we have encrypted message with IV and we can store it in for example SharedPreferences.

Please remember that IV can’t be used more that once. To encrypt next message you will have to generate new IV or you will get this:

IV has already been used. Reusing IV in encryption mode violates security best practices.

Decryption

Initializing decryption cypher we need IV form encryption cipher. That is why we concat ciphertext and IV with separator. Now we can use this IV by firstly decoding from Base64 and inserting as parameter to IvParameterSpec and inserting as a third parameter to init method. Please look at second parameter key — it should be the same key as used in encryption — you can take it from KeyStore represented by KEY_NAME.

cipherDec.init(
Cipher.DECRYPT_MODE, key, IvParameterSpec(
Base64.decode(
IV.toByteArray(Charsets.UTF_8),
Base64.DEFAULT
)
)
)

to decode the cipher text we use following metho

fun decrypt(cipher: Cipher, encrypted: String): String {

try {
return cipher.doFinal(
Base64.decode(
encrypted,
Base64.DEFAULT
)
).toString(Charsets.UTF_8)

} catch (una: UserNotAuthenticatedException) {
una.printStackTrace()
} catch (kpi: KeyPermanentlyInvalidatedException) {
kpi.printStackTrace()
} catch (ibse: IllegalBlockSizeException) {
ibse.printStackTrace()
} catch (e: Exception) {
e.printStackTrace()
}
return ""
}

It will return our message.

4. Fingerprint Authentication

To include fingerprint authentication to our cipher we need to take care of couple things firstly.

I We need to add permissions to AndroidManifest

<uses-permission android:name="android.permission.USE_BIOMETRIC"/>
<uses-permission android:name="android.permission.USE_FINGERPRINT"/>

II We have to set Screen lock & Fingerprint. Go to Settings, Security & Location and set Screen lock and Fingerprint.

Encoding by Fingerprint

Now we can start adding fingerprint to our cipher.

private lateinit var fingerprintManager: FingerprintManager
private lateinit var keyguardManager: KeyguardManager
private lateinit var cryptoObjectEncrypt: FingerprintManager.CryptoObject
private lateinit var cryptoObjectDecrypt: FingerprintManager.CryptoObject
cryptoObjectEncrypt = FingerprintManager.CryptoObject(encryptionObject.cipherForEncryption())
val fingerprintHandler = FingerprintHandler(this)
fingerprintHandler.startAuth(fingerprintManager, cryptoObjectEncrypt)

We create class FingerprintHandler implementing FingerprintManager.AuthenticationCallback() to handle fingerprint callbacks. In arguments in startAuth method we give FingerprintManager to make authentication by authenticate method to receive all callbacks and encryption cipher packed inside CryptoObject.

fun startAuth(fingerprintManager: FingerprintManager, cryptoObject: FingerprintManager.CryptoObject){
cancellationSignal = CancellationSignal()

if(ActivityCompat.checkSelfPermission(context, Manifest.permission.USE_FINGERPRINT) != PackageManager.PERMISSION_GRANTED){
return
}
fingerprintManager.authenticate(cryptoObject, cancellationSignal, 0, this, null)

Toast.makeText(context,
"authentificate in startAuth",
Toast.LENGTH_LONG).show()
}

There are more overridden functions from AuthenticationCallback that will be visible on my github.

Now after cipher is authenticated by fingerprint on device we can use it to encrypt the message.

var pref = this.getSharedPreferences(
"com.yourdomain.secure.pref",
Context.MODE_PRIVATE
)

var editor = pref.edit()
encryptedMessage = encryptionObject.encrypt(
encryptionObject.cipherEnc,
pinEditText.text.toString().toByteArray(Charsets.UTF_8),
separator
)
editor.putString(SECURE_KEY, encryptedMessage)
editor.apply()

You can notice that we used SharedPreferences to store encrypted message and IV.

Decoding by Fingerprint

First notice that we take IV from SharedPreferences splitting by separator from encrypted message.

pref.getString(SECURE_KEY, null).split(separator)[1].replace("\n", "")

Method encryptionObject.cipherForDecryption returns decrypt object which we send to startAuth by pack it into CryptObject.

cryptoObjectDecrypt = FingerprintManager.CryptoObject(
encryptionObject.cipherForDecryption(
pref.getString(SECURE_KEY, null).split(separator)[1].replace("\n", "")
)
)
val fingerprintHandler = FingerprintHandler(this)
fingerprintHandler.startAuth(fingerprintManager, cryptoObjectDecrypt)

We send decryption cipher to authenticate by fingerprint and receive proper cipher for decryption. Last step we take encrypted message from SharedPreferences and cipher to decrypt the message.

val mess = pref.getString(SECURE_KEY, null).split(separator)[0]
val decryptedData = encryptionObject.decrypt(
encryptionObject.cipherDec,
mess
)

Result

Encrypt text:

  1. First we type text to cipher.
  2. We click listener to listen for our finger swipe for encryption.
  3. We click encrypt button to cipher the message.

Decrypt cipher:

  1. Cipher is taken from Shared Preferences
  2. We click listener to listen for our finger swipe for description.
  3. We click decrypt button to decrypt cipher and view text.

Code

Please follow this project on GitHub. You can find complete working project that was described in this paper. This example was written in Kotlin.

About me

If you find this article useful and you would like to stay in touch, feel free to invite me on LinkedIn.