Home | History | Annotate | Download | only in fingerprintdialog
      1 /*
      2  * Copyright (C) 2017 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.example.android.fingerprintdialog
     18 
     19 import android.app.KeyguardManager
     20 import android.content.Intent
     21 import android.content.SharedPreferences
     22 import android.hardware.fingerprint.FingerprintManager
     23 import android.os.Bundle
     24 import android.preference.PreferenceManager
     25 import android.security.keystore.KeyGenParameterSpec
     26 import android.security.keystore.KeyPermanentlyInvalidatedException
     27 import android.security.keystore.KeyProperties
     28 import android.security.keystore.KeyProperties.BLOCK_MODE_CBC
     29 import android.security.keystore.KeyProperties.ENCRYPTION_PADDING_PKCS7
     30 import android.security.keystore.KeyProperties.KEY_ALGORITHM_AES
     31 import android.support.v7.app.AppCompatActivity
     32 import android.util.Base64
     33 import android.util.Log
     34 import android.view.Menu
     35 import android.view.MenuItem
     36 import android.view.View
     37 import android.widget.Button
     38 import android.widget.TextView
     39 import android.widget.Toast
     40 import java.io.IOException
     41 import java.security.InvalidAlgorithmParameterException
     42 import java.security.InvalidKeyException
     43 import java.security.KeyStore
     44 import java.security.KeyStoreException
     45 import java.security.NoSuchAlgorithmException
     46 import java.security.NoSuchProviderException
     47 import java.security.UnrecoverableKeyException
     48 import java.security.cert.CertificateException
     49 import javax.crypto.BadPaddingException
     50 import javax.crypto.Cipher
     51 import javax.crypto.IllegalBlockSizeException
     52 import javax.crypto.KeyGenerator
     53 import javax.crypto.NoSuchPaddingException
     54 import javax.crypto.SecretKey
     55 
     56 /**
     57  * Main entry point for the sample, showing a backpack and "Purchase" button.
     58  */
     59 class MainActivity : AppCompatActivity(),
     60     FingerprintAuthenticationDialogFragment.Callback {
     61 
     62     private lateinit var keyStore: KeyStore
     63     private lateinit var keyGenerator: KeyGenerator
     64     private lateinit var sharedPreferences: SharedPreferences
     65 
     66     override fun onCreate(savedInstanceState: Bundle?) {
     67         super.onCreate(savedInstanceState)
     68         setContentView(R.layout.activity_main)
     69         setSupportActionBar(findViewById(R.id.toolbar))
     70         setupKeyStoreAndKeyGenerator()
     71 
     72         val (defaultCipher: Cipher, cipherNotInvalidated: Cipher) = setupCiphers()
     73         sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
     74         setUpPurchaseButtons(cipherNotInvalidated, defaultCipher)
     75     }
     76 
     77     /**
     78      * Enables or disables purchase buttons and sets the appropriate click listeners.
     79      *
     80      * @param cipherNotInvalidated cipher for the not invalidated purchase button
     81      * @param defaultCipher the default cipher, used for the purchase button
     82      */
     83     private fun setUpPurchaseButtons(cipherNotInvalidated: Cipher, defaultCipher: Cipher) {
     84         val purchaseButton = findViewById<Button>(R.id.purchase_button)
     85         val purchaseButtonNotInvalidated =
     86                 findViewById<Button>(R.id.purchase_button_not_invalidated)
     87 
     88         purchaseButtonNotInvalidated.run {
     89             isEnabled = true
     90             setOnClickListener(PurchaseButtonClickListener(
     91                     cipherNotInvalidated, KEY_NAME_NOT_INVALIDATED))
     92         }
     93 
     94         val keyguardManager = getSystemService(KeyguardManager::class.java)
     95         if (!keyguardManager.isKeyguardSecure) {
     96             // Show a message that the user hasn't set up a fingerprint or lock screen.
     97             showToast(getString(R.string.setup_lock_screen))
     98             purchaseButton.isEnabled = false
     99             purchaseButtonNotInvalidated.isEnabled = false
    100             return
    101         }
    102 
    103         val fingerprintManager = getSystemService(FingerprintManager::class.java)
    104         if (!fingerprintManager.hasEnrolledFingerprints()) {
    105             purchaseButton.isEnabled = false
    106             // This happens when no fingerprints are registered.
    107             showToast(getString(R.string.register_fingerprint))
    108             return
    109         }
    110 
    111         createKey(DEFAULT_KEY_NAME)
    112         createKey(KEY_NAME_NOT_INVALIDATED, false)
    113         purchaseButton.run {
    114             isEnabled = true
    115             setOnClickListener(PurchaseButtonClickListener(defaultCipher, DEFAULT_KEY_NAME))
    116         }
    117     }
    118 
    119     /**
    120      * Sets up KeyStore and KeyGenerator
    121      */
    122     private fun setupKeyStoreAndKeyGenerator() {
    123         try {
    124             keyStore = KeyStore.getInstance(ANDROID_KEY_STORE)
    125         } catch (e: KeyStoreException) {
    126             throw RuntimeException("Failed to get an instance of KeyStore", e)
    127         }
    128 
    129         try {
    130             keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM_AES, ANDROID_KEY_STORE)
    131         } catch (e: Exception) {
    132             when (e) {
    133                 is NoSuchAlgorithmException,
    134                 is NoSuchProviderException ->
    135                     throw RuntimeException("Failed to get an instance of KeyGenerator", e)
    136                 else -> throw e
    137             }
    138         }
    139     }
    140 
    141     /**
    142      * Sets up default cipher and a non-invalidated cipher
    143      */
    144     private fun setupCiphers(): Pair<Cipher, Cipher> {
    145         val defaultCipher: Cipher
    146         val cipherNotInvalidated: Cipher
    147         try {
    148             val cipherString = "$KEY_ALGORITHM_AES/$BLOCK_MODE_CBC/$ENCRYPTION_PADDING_PKCS7"
    149             defaultCipher = Cipher.getInstance(cipherString)
    150             cipherNotInvalidated = Cipher.getInstance(cipherString)
    151         } catch (e: Exception) {
    152             when (e) {
    153                 is NoSuchAlgorithmException,
    154                 is NoSuchPaddingException ->
    155                     throw RuntimeException("Failed to get an instance of Cipher", e)
    156                 else -> throw e
    157             }
    158         }
    159         return Pair(defaultCipher, cipherNotInvalidated)
    160     }
    161 
    162     /**
    163      * Initialize the [Cipher] instance with the created key in the [createKey] method.
    164      *
    165      * @param keyName the key name to init the cipher
    166      * @return `true` if initialization succeeded, `false` if the lock screen has been disabled or
    167      * reset after key generation, or if a fingerprint was enrolled after key generation.
    168      */
    169     private fun initCipher(cipher: Cipher, keyName: String): Boolean {
    170         try {
    171             keyStore.load(null)
    172             cipher.init(Cipher.ENCRYPT_MODE, keyStore.getKey(keyName, null) as SecretKey)
    173             return true
    174         } catch (e: Exception) {
    175             when (e) {
    176                 is KeyPermanentlyInvalidatedException -> return false
    177                 is KeyStoreException,
    178                 is CertificateException,
    179                 is UnrecoverableKeyException,
    180                 is IOException,
    181                 is NoSuchAlgorithmException,
    182                 is InvalidKeyException -> throw RuntimeException("Failed to init Cipher", e)
    183                 else -> throw e
    184             }
    185         }
    186     }
    187 
    188     /**
    189      * Proceed with the purchase operation
    190      *
    191      * @param withFingerprint `true` if the purchase was made by using a fingerprint
    192      * @param crypto the Crypto object
    193      */
    194     override fun onPurchased(withFingerprint: Boolean, crypto: FingerprintManager.CryptoObject?) {
    195         if (withFingerprint) {
    196             // If the user authenticated with fingerprint, verify using cryptography and then show
    197             // the confirmation message.
    198             if (crypto != null) {
    199                 tryEncrypt(crypto.cipher)
    200             }
    201         } else {
    202             // Authentication happened with backup password. Just show the confirmation message.
    203             showConfirmation()
    204         }
    205     }
    206 
    207     // Show confirmation message. Also show crypto information if fingerprint was used.
    208     private fun showConfirmation(encrypted: ByteArray? = null) {
    209         findViewById<View>(R.id.confirmation_message).visibility = View.VISIBLE
    210         if (encrypted != null) {
    211             findViewById<TextView>(R.id.encrypted_message).run {
    212                 visibility = View.VISIBLE
    213                 text = Base64.encodeToString(encrypted, 0 /* flags */)
    214             }
    215         }
    216     }
    217 
    218     /**
    219      * Tries to encrypt some data with the generated key from [createKey]. This only works if the
    220      * user just authenticated via fingerprint.
    221      */
    222     private fun tryEncrypt(cipher: Cipher) {
    223         try {
    224             showConfirmation(cipher.doFinal(SECRET_MESSAGE.toByteArray()))
    225         } catch (e: Exception) {
    226             when (e) {
    227                 is BadPaddingException,
    228                 is IllegalBlockSizeException -> {
    229                     Toast.makeText(this, "Failed to encrypt the data with the generated key. "
    230                             + "Retry the purchase", Toast.LENGTH_LONG).show()
    231                     Log.e(TAG, "Failed to encrypt the data with the generated key. ${e.message}")
    232                 }
    233                 else -> throw e
    234             }
    235         }
    236     }
    237 
    238     /**
    239      * Creates a symmetric key in the Android Key Store which can only be used after the user has
    240      * authenticated with a fingerprint.
    241      *
    242      * @param keyName the name of the key to be created
    243      * @param invalidatedByBiometricEnrollment if `false` is passed, the created key will not be
    244      * invalidated even if a new fingerprint is enrolled. The default value is `true` - the key will
    245      * be invalidated if a new fingerprint is enrolled.
    246      */
    247     override fun createKey(keyName: String, invalidatedByBiometricEnrollment: Boolean) {
    248         // The enrolling flow for fingerprint. This is where you ask the user to set up fingerprint
    249         // for your flow. Use of keys is necessary if you need to know if the set of enrolled
    250         // fingerprints has changed.
    251         try {
    252             keyStore.load(null)
    253 
    254             val keyProperties = KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
    255             val builder = KeyGenParameterSpec.Builder(keyName, keyProperties)
    256                     .setBlockModes(BLOCK_MODE_CBC)
    257                     .setUserAuthenticationRequired(true)
    258                     .setEncryptionPaddings(ENCRYPTION_PADDING_PKCS7)
    259                     .setInvalidatedByBiometricEnrollment(invalidatedByBiometricEnrollment)
    260 
    261             keyGenerator.run {
    262                 init(builder.build())
    263                 generateKey()
    264             }
    265         } catch (e: Exception) {
    266             when (e) {
    267                 is NoSuchAlgorithmException,
    268                 is InvalidAlgorithmParameterException,
    269                 is CertificateException,
    270                 is IOException -> throw RuntimeException(e)
    271                 else -> throw e
    272             }
    273         }
    274     }
    275 
    276     override fun onCreateOptionsMenu(menu: Menu): Boolean {
    277         menuInflater.inflate(R.menu.menu_main, menu)
    278         return true
    279     }
    280 
    281     override fun onOptionsItemSelected(item: MenuItem): Boolean {
    282         if (item.itemId == R.id.action_settings) {
    283             val intent = Intent(this, SettingsActivity::class.java)
    284             startActivity(intent)
    285             return true
    286         }
    287         return super.onOptionsItemSelected(item)
    288     }
    289 
    290     private inner class PurchaseButtonClickListener internal constructor(
    291             internal var cipher: Cipher,
    292             internal var keyName: String
    293     ) : View.OnClickListener {
    294 
    295         override fun onClick(view: View) {
    296             findViewById<View>(R.id.confirmation_message).visibility = View.GONE
    297             findViewById<View>(R.id.encrypted_message).visibility = View.GONE
    298 
    299             val fragment = FingerprintAuthenticationDialogFragment()
    300             fragment.setCryptoObject(FingerprintManager.CryptoObject(cipher))
    301             fragment.setCallback(this@MainActivity)
    302 
    303             // Set up the crypto object for later, which will be authenticated by fingerprint usage.
    304             if (initCipher(cipher, keyName)) {
    305 
    306                 // Show the fingerprint dialog. The user has the option to use the fingerprint with
    307                 // crypto, or can fall back to using a server-side verified password.
    308                 val useFingerprintPreference = sharedPreferences
    309                         .getBoolean(getString(R.string.use_fingerprint_to_authenticate_key), true)
    310                 if (useFingerprintPreference) {
    311                     fragment.setStage(Stage.FINGERPRINT)
    312                 } else {
    313                     fragment.setStage(Stage.PASSWORD)
    314                 }
    315             } else {
    316                 // This happens if the lock screen has been disabled or or a fingerprint was
    317                 // enrolled. Thus, show the dialog to authenticate with their password first and ask
    318                 // the user if they want to authenticate with a fingerprint in the future.
    319                 fragment.setStage(Stage.NEW_FINGERPRINT_ENROLLED)
    320             }
    321             fragment.show(fragmentManager, DIALOG_FRAGMENT_TAG)
    322         }
    323     }
    324 
    325     companion object {
    326         private val ANDROID_KEY_STORE = "AndroidKeyStore"
    327         private val DIALOG_FRAGMENT_TAG = "myFragment"
    328         private val KEY_NAME_NOT_INVALIDATED = "key_not_invalidated"
    329         private val SECRET_MESSAGE = "Very secret message"
    330         private val TAG = MainActivity::class.java.simpleName
    331     }
    332 }
    333