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