1 /* 2 * Copyright (C) 2015 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.confirmcredential; 18 19 import android.app.Activity; 20 import android.app.KeyguardManager; 21 import android.content.Context; 22 import android.content.Intent; 23 import android.os.Bundle; 24 import android.security.keystore.KeyGenParameterSpec; 25 import android.security.keystore.KeyPermanentlyInvalidatedException; 26 import android.security.keystore.KeyProperties; 27 import android.security.keystore.UserNotAuthenticatedException; 28 import android.view.View; 29 import android.widget.Button; 30 import android.widget.TextView; 31 import android.widget.Toast; 32 33 import java.io.IOException; 34 import java.security.InvalidAlgorithmParameterException; 35 import java.security.InvalidKeyException; 36 import java.security.KeyStore; 37 import java.security.KeyStoreException; 38 import java.security.NoSuchAlgorithmException; 39 import java.security.NoSuchProviderException; 40 import java.security.UnrecoverableKeyException; 41 import java.security.cert.CertificateException; 42 43 import javax.crypto.BadPaddingException; 44 import javax.crypto.Cipher; 45 import javax.crypto.IllegalBlockSizeException; 46 import javax.crypto.KeyGenerator; 47 import javax.crypto.NoSuchPaddingException; 48 import javax.crypto.SecretKey; 49 50 /** 51 * Main entry point for the sample, showing a backpack and "Purchase" button. 52 */ 53 public class MainActivity extends Activity { 54 55 /** Alias for our key in the Android Key Store. */ 56 private static final String KEY_NAME = "my_key"; 57 private static final byte[] SECRET_BYTE_ARRAY = new byte[] {1, 2, 3, 4, 5, 6}; 58 59 private static final int REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS = 1; 60 61 /** 62 * If the user has unlocked the device Within the last this number of seconds, 63 * it can be considered as an authenticator. 64 */ 65 private static final int AUTHENTICATION_DURATION_SECONDS = 30; 66 67 private KeyguardManager mKeyguardManager; 68 69 @Override onCreate(Bundle savedInstanceState)70 protected void onCreate(Bundle savedInstanceState) { 71 super.onCreate(savedInstanceState); 72 setContentView(R.layout.activity_main); 73 mKeyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); 74 Button purchaseButton = (Button) findViewById(R.id.purchase_button); 75 if (!mKeyguardManager.isKeyguardSecure()) { 76 // Show a message that the user hasn't set up a lock screen. 77 Toast.makeText(this, 78 "Secure lock screen hasn't set up.\n" 79 + "Go to 'Settings -> Security -> Screenlock' to set up a lock screen", 80 Toast.LENGTH_LONG).show(); 81 purchaseButton.setEnabled(false); 82 return; 83 } 84 createKey(); 85 findViewById(R.id.purchase_button).setOnClickListener(new View.OnClickListener() { 86 @Override 87 public void onClick(View v) { 88 // Test to encrypt something. It might fail if the timeout expired (30s). 89 tryEncrypt(); 90 } 91 }); 92 } 93 94 /** 95 * Tries to encrypt some data with the generated key in {@link #createKey} which is 96 * only works if the user has just authenticated via device credentials. 97 */ tryEncrypt()98 private boolean tryEncrypt() { 99 try { 100 KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); 101 keyStore.load(null); 102 SecretKey secretKey = (SecretKey) keyStore.getKey(KEY_NAME, null); 103 Cipher cipher = Cipher.getInstance( 104 KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" 105 + KeyProperties.ENCRYPTION_PADDING_PKCS7); 106 107 // Try encrypting something, it will only work if the user authenticated within 108 // the last AUTHENTICATION_DURATION_SECONDS seconds. 109 cipher.init(Cipher.ENCRYPT_MODE, secretKey); 110 cipher.doFinal(SECRET_BYTE_ARRAY); 111 112 // If the user has recently authenticated, you will reach here. 113 showAlreadyAuthenticated(); 114 return true; 115 } catch (UserNotAuthenticatedException e) { 116 // User is not authenticated, let's authenticate with device credentials. 117 showAuthenticationScreen(); 118 return false; 119 } catch (KeyPermanentlyInvalidatedException e) { 120 // This happens if the lock screen has been disabled or reset after the key was 121 // generated after the key was generated. 122 Toast.makeText(this, "Keys are invalidated after created. Retry the purchase\n" 123 + e.getMessage(), 124 Toast.LENGTH_LONG).show(); 125 return false; 126 } catch (BadPaddingException | IllegalBlockSizeException | KeyStoreException | 127 CertificateException | UnrecoverableKeyException | IOException 128 | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException e) { 129 throw new RuntimeException(e); 130 } 131 } 132 133 /** 134 * Creates a symmetric key in the Android Key Store which can only be used after the user has 135 * authenticated with device credentials within the last X seconds. 136 */ createKey()137 private void createKey() { 138 // Generate a key to decrypt payment credentials, tokens, etc. 139 // This will most likely be a registration step for the user when they are setting up your app. 140 try { 141 KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); 142 keyStore.load(null); 143 KeyGenerator keyGenerator = KeyGenerator.getInstance( 144 KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); 145 146 // Set the alias of the entry in Android KeyStore where the key will appear 147 // and the constrains (purposes) in the constructor of the Builder 148 keyGenerator.init(new KeyGenParameterSpec.Builder(KEY_NAME, 149 KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) 150 .setBlockModes(KeyProperties.BLOCK_MODE_CBC) 151 .setUserAuthenticationRequired(true) 152 // Require that the user has unlocked in the last 30 seconds 153 .setUserAuthenticationValidityDurationSeconds(AUTHENTICATION_DURATION_SECONDS) 154 .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7) 155 .build()); 156 keyGenerator.generateKey(); 157 } catch (NoSuchAlgorithmException | NoSuchProviderException 158 | InvalidAlgorithmParameterException | KeyStoreException 159 | CertificateException | IOException e) { 160 throw new RuntimeException("Failed to create a symmetric key", e); 161 } 162 } 163 showAuthenticationScreen()164 private void showAuthenticationScreen() { 165 // Create the Confirm Credentials screen. You can customize the title and description. Or 166 // we will provide a generic one for you if you leave it null 167 Intent intent = mKeyguardManager.createConfirmDeviceCredentialIntent(null, null); 168 if (intent != null) { 169 startActivityForResult(intent, REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS); 170 } 171 } 172 173 @Override onActivityResult(int requestCode, int resultCode, Intent data)174 protected void onActivityResult(int requestCode, int resultCode, Intent data) { 175 if (requestCode == REQUEST_CODE_CONFIRM_DEVICE_CREDENTIALS) { 176 // Challenge completed, proceed with using cipher 177 if (resultCode == RESULT_OK) { 178 if (tryEncrypt()) { 179 showPurchaseConfirmation(); 180 } 181 } else { 182 // The user canceled or didn’t complete the lock screen 183 // operation. Go to error/cancellation flow. 184 } 185 } 186 } 187 showPurchaseConfirmation()188 private void showPurchaseConfirmation() { 189 findViewById(R.id.confirmation_message).setVisibility(View.VISIBLE); 190 findViewById(R.id.purchase_button).setEnabled(false); 191 } 192 showAlreadyAuthenticated()193 private void showAlreadyAuthenticated() { 194 TextView textView = (TextView) findViewById( 195 R.id.already_has_valid_device_credential_message); 196 textView.setVisibility(View.VISIBLE); 197 textView.setText(getString( 198 R.string.already_confirmed_device_credentials_within_last_x_seconds, 199 AUTHENTICATION_DURATION_SECONDS)); 200 findViewById(R.id.purchase_button).setEnabled(false); 201 } 202 203 } 204