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 
onCreatenull66     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      */
setUpPurchaseButtonsnull83     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      */
setupKeyStoreAndKeyGeneratornull122     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      */
setupCiphersnull144     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      */
initCiphernull169     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      */
onPurchasednull194     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.
showConfirmationnull208     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      */
tryEncryptnull222     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      */
createKeynull247     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 
onCreateOptionsMenunull276     override fun onCreateOptionsMenu(menu: Menu): Boolean {
277         menuInflater.inflate(R.menu.menu_main, menu)
278         return true
279     }
280 
onOptionsItemSelectednull281     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 
onClicknull295         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