/* * Copyright (C) 2016 Frederik Schweiger * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package link.fls.safe; import android.app.KeyguardManager; import android.content.Context; import android.content.Intent; import android.hardware.fingerprint.FingerprintManager; import android.os.Bundle; import android.security.keystore.KeyGenParameterSpec; import android.security.keystore.KeyInfo; import android.security.keystore.KeyPermanentlyInvalidatedException; import android.security.keystore.KeyProperties; import android.security.keystore.UserNotAuthenticatedException; import android.support.annotation.Nullable; import android.support.v7.app.AppCompatActivity; import android.support.v7.widget.CardView; import android.util.Base64; import android.view.View; import android.view.animation.OvershootInterpolator; import android.widget.EditText; import android.widget.RadioButton; import android.widget.TextView; import android.widget.Toast; import java.io.IOException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.NoSuchProviderException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.security.spec.InvalidKeySpecException; import javax.crypto.BadPaddingException; import javax.crypto.Cipher; import javax.crypto.IllegalBlockSizeException; import javax.crypto.KeyGenerator; import javax.crypto.NoSuchPaddingException; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; import javax.crypto.spec.IvParameterSpec; import butterknife.BindView; import butterknife.ButterKnife; import butterknife.OnClick; public class MainActivity extends AppCompatActivity implements FingerprintDialog.FingerprintDialogCallbacks { // This must have 'AndroidKeyStore' as value. Unfortunately there is no predefined constant. private static final String ANDROID_KEYSTORE_PROVIDER = "AndroidKeyStore"; // This is the default transformation used throughout this sample project. private static final String AES_DEFAULT_TRANSFORMATION = KeyProperties.KEY_ALGORITHM_AES + "/" + KeyProperties.BLOCK_MODE_CBC + "/" + KeyProperties.ENCRYPTION_PADDING_PKCS7; private static final String KEY_ALIAS_AES = "MyAesKeyAlias"; private static final String TAG_FINGERPRINT_DIALOG = "FingerprintDialog"; private static final String DELIMITER = "]"; private static final int REQUEST_CODE_CONFIRM_CREDENTIALS_ENCRYPT = 10; private static final int REQUEST_CODE_CONFIRM_CREDENTIALS_DECRYPT = 20; @BindView(R.id.textViewSuccess) protected TextView mTextCreateSuccess; @BindView(R.id.editTextInputOutput) protected EditText mEditTextInput; @BindView(R.id.radioAuthTimespan) protected RadioButton mRadioUserAuthentication; @BindView(R.id.radioAuthFingerprint) protected RadioButton mRadioUserFingerprint; @BindView(R.id.cardKeystore) protected CardView mKeystoreCard; private KeyguardManager mKeyguardManager; private FingerprintManager mFingerprintManager; private FingerprintDialog mFingerprintDialog; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mKeyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); mFingerprintManager = (FingerprintManager) getSystemService(Context.FINGERPRINT_SERVICE); mFingerprintDialog = FingerprintDialog.newInstance(); setContentView(R.layout.activity_main); ButterKnife.bind(this); } @OnClick(R.id.buttonCreateKey) void onCreateKeyButtonClick() { generateAesKey(); } @OnClick(R.id.buttonSymmetricEncrypt) void onEncryptButtonClick() { encryptWithAes(null); } @OnClick(R.id.buttonSymmetricDecrypt) void onDecryptButtonClick() { decryptWithAes(null); } /** * Generates a new AES key and stores it under the { @code KEY_ALIAS_AES } in the * Android Keystore. */ @SuppressWarnings("StatementWithEmptyBody") private void generateAesKey() { try { // The KeyGenerator is an engine class for creating symmetric keys utilizing the // algorithm it was initialized with. KeyGenerator keyGenerator = KeyGenerator.getInstance( KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE_PROVIDER); // Create a new instance of the KeyGenParameterSpec.Builder, hand over // the key alias and the different purposes for which you want to use the key. // Keep in mind that you can only use the key for the operations you have specified // here - once the key is created it can't be changed. KeyGenParameterSpec.Builder builder = new KeyGenParameterSpec.Builder( KEY_ALIAS_AES, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT); // Define the basic encryption parameters for the key. The set configuration // matches the AES_DEFAULT_TRANSFORMATION constant. builder.setBlockModes(KeyProperties.BLOCK_MODE_CBC) .setKeySize(256) .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7); if (mRadioUserAuthentication.isChecked()) { // Create a key which requires the user to be authenticated during // the last 30 seconds. Could also be 30 seconds or even 5 minutes - // choose whatever fits your security guidelines best. // Before continuing, check if the user has set up a secure lockscreen - // if not, prompt the user to set one up ;-) if (!hasSetupSecureLockscreen()) return; builder.setUserAuthenticationRequired(true) .setUserAuthenticationValidityDurationSeconds(15); } else if (mRadioUserFingerprint.isChecked()) { // Create a key which needs fingerprint authentication every time. // Before continuing, check if the device supports fingerprint // authentication and if the user has at least enrolled one fingerprint - // if not, prompt the user to enroll one ;-) if (!hasSetupFingerprint()) return; builder.setUserAuthenticationRequired(true); } else { // Create a key which does not need any user authentication. // Nothing more to add here! } // Initialize the KeyGenerator with the KeyGenParameterSpec which will be created by // the KeyGenParameterSpec.Builder . keyGenerator.init(builder.build()); // Finally, generate the key... keyGenerator.generateKey(); // ...and show a TextView with a confirmation text. showSuccessTextView(); } catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) { throw new RuntimeException("Failed to create a symmetric key", e); } } /** * Uses the key generated in { @code generateAesKey() } to encrypt the text * from an EditText view. * * @param cipher should be null. It's not null when called from { @code onFingerprintSuccess() } */ private void encryptWithAes(@Nullable Cipher cipher) { String plainText = mEditTextInput.getText().toString(); try { // Get a KeyStore instance with the Android Keystore provider. KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE_PROVIDER); // Relict of the old JCA API - you have to call load() even // if you do not have an input stream you want to load - otherwise it'll crash. keyStore.load(null); // Check if a generated key exists under the KEY_ALIAS_AES . if (!keyStore.containsAlias(KEY_ALIAS_AES)) { Toast.makeText(this, R.string.key_alias_not_found, Toast.LENGTH_LONG).show(); return; } // In normal cases, the cipher object should be null and we initialize a new one. // When the cipher is not null, this method was called from the onFingerprintSuccess() // method of the FingerprintDialogCallbacks. if (cipher == null) { // Get the SecretKey from the KeyStore and instantiate a Cipher with the // same params we used to create the key in generateAesKey() . SecretKey secretKey = (SecretKey) keyStore.getKey(KEY_ALIAS_AES, null); cipher = Cipher.getInstance(AES_DEFAULT_TRANSFORMATION); // Use the secretKey to initialize the Cipher in encryption mode. // If the key requires user authentication during a specific time span (specified // via setUserAuthenticationValidityDurationSeconds()), it may throw the // UserNotAuthenticatedException and we need to prompt the user // to authenticate again. cipher.init(Cipher.ENCRYPT_MODE, secretKey); // Retrieve information about the SecretKey from the KeyStore. SecretKeyFactory factory = SecretKeyFactory.getInstance( secretKey.getAlgorithm(), ANDROID_KEYSTORE_PROVIDER); KeyInfo info = (KeyInfo) factory.getKeySpec(secretKey, KeyInfo.class); // Check if the user needs to authenticate every time via fingerprint. if (info.isUserAuthenticationRequired() && info.getUserAuthenticationValidityDurationSeconds() == -1) { // It looks like the key needs authentication every time. // Show the fingerprint dialog and when the user authenticated successful, // the onFingerprintSuccess() method will be called. From there this // method gets called again with the initialized Cipher object. showFingerprintDialog(cipher, FingerprintDialog.PURPOSE_ENCRYPT); return; } } // The cipher is now fully initialized and ready to go - let's encrypt the // plainText bytes. byte[] encryptedBytes = cipher.doFinal(plainText.getBytes()); // Encode the initialization vector (IV) and encryptedBytes to Base64. String base64IV = Base64.encodeToString(cipher.getIV(), Base64.DEFAULT); String base64Cipher = Base64.encodeToString(encryptedBytes, Base64.DEFAULT); // Concatenate the IV and encryptedBytes strings divided by a delimiter // so the result can be stored in a single string. String result = base64IV + DELIMITER + base64Cipher; // Set the visibility of the mEditTextInput view to gone so it gets animated when // it reappears - thanks to animateLayoutChanges="true" . mEditTextInput.setVisibility(View.GONE); // Display the result inside the mEditTextInput view. mEditTextInput.setText(result); } catch (UserNotAuthenticatedException e) { // The user has not authenticated within the specified timeframe, let's authenticate // with device credentials and try again. showAuthenticationScreen(REQUEST_CODE_CONFIRM_CREDENTIALS_ENCRYPT); } catch (KeyPermanentlyInvalidatedException e) { // This happens if the generated key needs user authentication and the lock screen // has been disabled or reset. The same applies for enrolled fingerprints. Toast.makeText(this, R.string.key_invalidated_msg, Toast.LENGTH_LONG).show(); } catch (BadPaddingException | KeyStoreException | UnrecoverableKeyException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | NoSuchProviderException | InvalidKeySpecException | IllegalBlockSizeException | CertificateException | IOException e) { // When developing a real world app you should handle these exceptions correctly ;-) throw new RuntimeException(e); } finally { // Finally make the mEditTextInput view visible again. mEditTextInput.setVisibility(View.VISIBLE); } } /** * Uses the key generated in { @code generateAesKey() } to decrypt the text * from an EditText view. * * @param cipher should be null. It's not null when called from { @code onFingerprintSuccess() } */ private void decryptWithAes(@Nullable Cipher cipher) { String cipherText = mEditTextInput.getText().toString(); // Split the input string and check if it consists of two parts, the Base 64 encoded // IV and cipher text. String[] inputs = cipherText.split(DELIMITER); if (inputs.length < 2) { Toast.makeText(this, R.string.corrupt_data_msg, Toast.LENGTH_SHORT).show(); return; } try { // Decode the initialization vector (IV) and encryptedBytes. byte[] iv = Base64.decode(inputs[0], Base64.DEFAULT); byte[] cipherBytes = Base64.decode(inputs[1], Base64.DEFAULT); // Get a KeyStore instance with the Android Keystore provider. KeyStore keyStore = KeyStore.getInstance(ANDROID_KEYSTORE_PROVIDER); // Relict of the old JCA API - you have to call load() even // if you do not have an input stream you want to load - otherwise it'll crash. keyStore.load(null); // Check if a generated key exists under the KEY_ALIAS_AES . if (!keyStore.containsAlias(KEY_ALIAS_AES)) { Toast.makeText(this, R.string.key_alias_not_found, Toast.LENGTH_LONG).show(); return; } // In normal cases, the cipher object should be null and we initialize a new one. // When the cipher is not null, this method was called from the onFingerprintSuccess() // method of the FingerprintDialogCallbacks. if (cipher == null) { // Get the SecretKey from the KeyStore and instantiate a Cipher with the // same params we used to create the key in generateAesKey() . SecretKey secretKey = (SecretKey) keyStore.getKey(KEY_ALIAS_AES, null); cipher = Cipher.getInstance(AES_DEFAULT_TRANSFORMATION); // Use the secretKey to initialize the Cipher in decryption mode. // If the key requires user authentication during a specific time span (specified // via setUserAuthenticationValidityDurationSeconds()), it may throw the // UserNotAuthenticatedException and we need to prompt the user // to authenticate again. cipher.init(Cipher.DECRYPT_MODE, secretKey, new IvParameterSpec(iv)); // Retrieve information about the SecretKey from the KeyStore. SecretKeyFactory factory = SecretKeyFactory.getInstance( secretKey.getAlgorithm(), ANDROID_KEYSTORE_PROVIDER); KeyInfo info = (KeyInfo) factory.getKeySpec(secretKey, KeyInfo.class); // Check if the user needs to authenticate every time via fingerprint. if (info.isUserAuthenticationRequired() && info.getUserAuthenticationValidityDurationSeconds() == -1) { // It looks like the key needs authentication every time. // Show the fingerprint dialog and when the user authenticated successful, // the onFingerprintSuccess() method will be called. From there this // method gets called again with the initialized Cipher object. showFingerprintDialog(cipher, FingerprintDialog.PURPOSE_DECRYPT); return; } } // The cipher is now fully initialized and ready to go - let's decrypt the // cipherBytes. byte[] decryptedBytes = cipher.doFinal(cipherBytes); // Set the visibility of the mEditTextInput view to gone so it gets animated when // it reappears - thanks to animateLayoutChanges="true" . mEditTextInput.setVisibility(View.GONE); // Display the result inside the mEditTextInput view. mEditTextInput.setText(new String(decryptedBytes)); } catch (UserNotAuthenticatedException e) { // The user has not authenticated within the specified timeframe, let's authenticate // with device credentials and try again. showAuthenticationScreen(REQUEST_CODE_CONFIRM_CREDENTIALS_DECRYPT); } catch (KeyPermanentlyInvalidatedException e) { // This happens if the generated key needs user authentication and the lock screen // has been disabled or reset. The same applies for enrolled fingerprints. Toast.makeText(this, R.string.key_invalidated_msg, Toast.LENGTH_LONG).show(); } catch (IllegalBlockSizeException | BadPaddingException | IllegalArgumentException e) { // Catch invalid inputs Toast.makeText(this, R.string.corrupt_data_msg, Toast.LENGTH_SHORT).show(); } catch (KeyStoreException | UnrecoverableKeyException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | CertificateException | NoSuchProviderException | InvalidKeySpecException | IOException | InvalidAlgorithmParameterException e) { // When developing a real world app you should handle these exceptions correctly ;-) throw new RuntimeException(e); } finally { // Finally make the mEditTextInput view visible again. mEditTextInput.setVisibility(View.VISIBLE); } } /** * Shows a TextView which contains a confirmation that the key was successfully created * and uses an awesome animation to catch the users attention. Weeee! */ private void showSuccessTextView() { mTextCreateSuccess.setVisibility(View.VISIBLE); mTextCreateSuccess.setScaleX(0); mTextCreateSuccess.setScaleY(0); mTextCreateSuccess.animate() .scaleX(1) .scaleY(1) .setInterpolator(new OvershootInterpolator(1.4f)); } /** * Checks whether the user has set up a secure lock screen. * * @return true if the user has set up a secure lock screen */ private boolean hasSetupSecureLockscreen() { if (!mKeyguardManager.isKeyguardSecure()) { // Show a message that the user hasn't set up a lock screen. Toast.makeText(this, R.string.setup_lockscreen_msg, Toast.LENGTH_LONG).show(); return false; } return true; } /** * Checks whether the device supports fingerprint authentication and if the user has * enrolled at least one fingerprint. * * @return true if the user has a fingerprint capable device and has enrolled * one or more fingerprints */ private boolean hasSetupFingerprint() { try { if (!mFingerprintManager.isHardwareDetected()) { Toast.makeText(this, R.string.fingerprint_missing_hardware, Toast.LENGTH_LONG).show(); return false; } else if (!mFingerprintManager.hasEnrolledFingerprints()) { Toast.makeText(this, R.string.fingerprint_not_enrolled, Toast.LENGTH_LONG).show(); return false; } } catch (SecurityException e) { // Should never be thrown since we have declared the USE_FINGERPRINT permission // in the manifest file return false; } return true; } /** * Dispatches the confirm credential intent to authenticate the user. * * @param requestCode represents the action we want to do after a successful authentication */ private void showAuthenticationScreen(int requestCode) { // Create the Confirm Credentials screen. You can customize the title and description. // Or it will provide a generic one for you if you leave the arguments to null. Intent intent = mKeyguardManager.createConfirmDeviceCredentialIntent( getString(R.string.confirm_credential_title), getString(R.string.confirm_credential_msg)); if (intent != null) { startActivityForResult(intent, requestCode); } } /** * Shows a { @code FingerprintDialog } . * * @param cipher which needs user authentication * @param fingerprintPurpose represents the purpose e.g. ENCRYPT, DECRYPT */ private void showFingerprintDialog(Cipher cipher, int fingerprintPurpose) { if (getFragmentManager().findFragmentByTag(TAG_FINGERPRINT_DIALOG) == null) { mFingerprintDialog.init(fingerprintPurpose, new FingerprintManager.CryptoObject(cipher)); mFingerprintDialog.show(getFragmentManager(), TAG_FINGERPRINT_DIALOG); } } /** * Receives the results from the activity launched by the ConfirmDeviceCredentialIntent. */ @SuppressWarnings("StatementWithEmptyBody") @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { if (resultCode == RESULT_OK) { // The user authenticated successfully, so let's try to en-/decrypt again. if (requestCode == REQUEST_CODE_CONFIRM_CREDENTIALS_ENCRYPT) { encryptWithAes(null); } else if (requestCode == REQUEST_CODE_CONFIRM_CREDENTIALS_DECRYPT) { decryptWithAes(null); } } else { // The user canceled or didn’t complete the lock screen // operation. Go to error/cancellation flow. } } /** * Gets called by the { @code FingerprintDialog } when the user successfully * authenticated via fingerprint. * * @param purpose represents the purpose, e.g. PURPOSE_ENCRYPT or PURPOSE_DECRYPT * @param cryptoObject contains the cipher object */ @Override public void onFingerprintSuccess(int purpose, FingerprintManager.CryptoObject cryptoObject) { switch (purpose) { case FingerprintDialog.PURPOSE_ENCRYPT: // Let's try to encrypt again with the pre-initialized Cipher object encryptWithAes(cryptoObject.getCipher()); break; case FingerprintDialog.PURPOSE_DECRYPT: // Let's try to decrypt again with the pre-initialized Cipher object decryptWithAes(cryptoObject.getCipher()); break; } } /** * Gets called by the { @code FingerprintDialog } when the user clicked the cancel * button inside the dialog. */ @Override public void onFingerprintCancel() { // The user canceled or didn’t complete the fingerprint // operation. Go to error/cancellation flow. } }