diff options
author | Yuichi Araki <yaraki@google.com> | 2015-09-15 11:13:03 +0900 |
---|---|---|
committer | Yuichi Araki <yaraki@google.com> | 2015-09-29 17:40:46 +0900 |
commit | 424704d90af155363b09861648c48811c7a7a7ad (patch) | |
tree | 121a59dfba33f9bf332c0505b6d4fb1f4d67ee0b | |
parent | 1d1948e9152433a491780f085daded25f0b4ec3c (diff) | |
download | android-424704d90af155363b09861648c48811c7a7a7ad.tar.gz |
AsymmetricFingerprintDialog: Add a new sample
This is a version of FingerprintDialog with asymmetric keys.
Bug: 23089151
Change-Id: I765430d9f79abcd57c9be8e4bf5f33bda1296f9b
48 files changed, 2208 insertions, 0 deletions
diff --git a/security/AsymmetricFingerprintDialog/Application/.gitignore b/security/AsymmetricFingerprintDialog/Application/.gitignore new file mode 100644 index 00000000..6eb878d4 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/.gitignore @@ -0,0 +1,16 @@ +# Copyright 2013 The Android Open Source Project +# +# 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. +src/template/ +src/common/ +build.gradle diff --git a/security/AsymmetricFingerprintDialog/Application/proguard-project.txt b/security/AsymmetricFingerprintDialog/Application/proguard-project.txt new file mode 100644 index 00000000..f2fe1559 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/proguard-project.txt @@ -0,0 +1,20 @@ +# To enable ProGuard in your project, edit project.properties +# to define the proguard.config property as described in that file. +# +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in ${sdk.dir}/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the ProGuard +# include property in project.properties. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/AndroidManifest.xml b/security/AsymmetricFingerprintDialog/Application/src/main/AndroidManifest.xml new file mode 100644 index 00000000..d1cf9f87 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/AndroidManifest.xml @@ -0,0 +1,43 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2015 The Android Open Source Project + + 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. +--> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.example.android.asymmetricfingerprintdialog" + android:versionCode="1" + android:versionName="1.0"> + + <uses-permission android:name="android.permission.USE_FINGERPRINT"/> + + <application + android:name=".InjectedApplication" + android:allowBackup="true" + android:label="@string/app_name" + android:icon="@mipmap/ic_launcher" + android:theme="@style/AppTheme"> + + <activity android:name=".MainActivity" + android:label="@string/app_name"> + <intent-filter> + <action android:name="android.intent.action.MAIN"/> + <category android:name="android.intent.category.LAUNCHER"/> + </intent-filter> + </activity> + + <activity + android:name=".SettingsActivity" + android:label="@string/action_settings" /> + </application> +</manifest> diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/FingerprintAuthenticationDialogFragment.java b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/FingerprintAuthenticationDialogFragment.java new file mode 100644 index 00000000..d40661c6 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/FingerprintAuthenticationDialogFragment.java @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.example.android.asymmetricfingerprintdialog; + +import com.example.android.asymmetricfingerprintdialog.server.StoreBackend; +import com.example.android.asymmetricfingerprintdialog.server.Transaction; + +import android.app.Activity; +import android.app.DialogFragment; +import android.content.SharedPreferences; +import android.hardware.fingerprint.FingerprintManager; +import android.os.Bundle; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; + +import java.io.IOException; +import java.security.KeyFactory; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.security.cert.CertificateException; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; + +import javax.inject.Inject; + +/** + * A dialog which uses fingerprint APIs to authenticate the user, and falls back to password + * authentication if fingerprint is not available. + */ +public class FingerprintAuthenticationDialogFragment extends DialogFragment + implements TextView.OnEditorActionListener, FingerprintUiHelper.Callback { + + private Button mCancelButton; + private Button mSecondDialogButton; + private View mFingerprintContent; + private View mBackupContent; + private EditText mPassword; + private CheckBox mUseFingerprintFutureCheckBox; + private TextView mPasswordDescriptionTextView; + private TextView mNewFingerprintEnrolledTextView; + + private Stage mStage = Stage.FINGERPRINT; + + private FingerprintManager.CryptoObject mCryptoObject; + private FingerprintUiHelper mFingerprintUiHelper; + private MainActivity mActivity; + + @Inject FingerprintUiHelper.FingerprintUiHelperBuilder mFingerprintUiHelperBuilder; + @Inject InputMethodManager mInputMethodManager; + @Inject SharedPreferences mSharedPreferences; + @Inject StoreBackend mStoreBackend; + + @Inject + public FingerprintAuthenticationDialogFragment() {} + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Do not create a new Fragment when the Activity is re-created such as orientation changes. + setRetainInstance(true); + setStyle(DialogFragment.STYLE_NORMAL, android.R.style.Theme_Material_Light_Dialog); + + // We register a new user account here. Real apps should do this with proper UIs. + enroll(); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + getDialog().setTitle(getString(R.string.sign_in)); + View v = inflater.inflate(R.layout.fingerprint_dialog_container, container, false); + mCancelButton = (Button) v.findViewById(R.id.cancel_button); + mCancelButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + dismiss(); + } + }); + + mSecondDialogButton = (Button) v.findViewById(R.id.second_dialog_button); + mSecondDialogButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + if (mStage == Stage.FINGERPRINT) { + goToBackup(); + } else { + verifyPassword(); + } + } + }); + mFingerprintContent = v.findViewById(R.id.fingerprint_container); + mBackupContent = v.findViewById(R.id.backup_container); + mPassword = (EditText) v.findViewById(R.id.password); + mPassword.setOnEditorActionListener(this); + mPasswordDescriptionTextView = (TextView) v.findViewById(R.id.password_description); + mUseFingerprintFutureCheckBox = (CheckBox) + v.findViewById(R.id.use_fingerprint_in_future_check); + mNewFingerprintEnrolledTextView = (TextView) + v.findViewById(R.id.new_fingerprint_enrolled_description); + mFingerprintUiHelper = mFingerprintUiHelperBuilder.build( + (ImageView) v.findViewById(R.id.fingerprint_icon), + (TextView) v.findViewById(R.id.fingerprint_status), this); + updateStage(); + + // If fingerprint authentication is not available, switch immediately to the backup + // (password) screen. + if (!mFingerprintUiHelper.isFingerprintAuthAvailable()) { + goToBackup(); + } + return v; + } + + @Override + public void onResume() { + super.onResume(); + if (mStage == Stage.FINGERPRINT) { + mFingerprintUiHelper.startListening(mCryptoObject); + } + } + + public void setStage(Stage stage) { + mStage = stage; + } + + @Override + public void onPause() { + super.onPause(); + mFingerprintUiHelper.stopListening(); + } + + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + mActivity = (MainActivity) activity; + } + + /** + * Sets the crypto object to be passed in when authenticating with fingerprint. + */ + public void setCryptoObject(FingerprintManager.CryptoObject cryptoObject) { + mCryptoObject = cryptoObject; + } + + /** + * Switches to backup (password) screen. This either can happen when fingerprint is not + * available or the user chooses to use the password authentication method by pressing the + * button. This can also happen when the user had too many fingerprint attempts. + */ + private void goToBackup() { + mStage = Stage.PASSWORD; + updateStage(); + mPassword.requestFocus(); + + // Show the keyboard. + mPassword.postDelayed(mShowKeyboardRunnable, 500); + + // Fingerprint is not used anymore. Stop listening for it. + mFingerprintUiHelper.stopListening(); + } + + /** + * Enrolls a user to the fake backend. + */ + private void enroll() { + try { + KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore"); + keyStore.load(null); + PublicKey publicKey = keyStore.getCertificate(MainActivity.KEY_NAME).getPublicKey(); + // Provide the public key to the backend. In most cases, the key needs to be transmitted + // to the backend over the network, for which Key.getEncoded provides a suitable wire + // format (X.509 DER-encoded). The backend can then create a PublicKey instance from the + // X.509 encoded form using KeyFactory.generatePublic. This conversion is also currently + // needed on API Level 23 (Android M) due to a platform bug which prevents the use of + // Android Keystore public keys when their private keys require user authentication. + // This conversion creates a new public key which is not backed by Android Keystore and + // thus is not affected by the bug. + KeyFactory factory = KeyFactory.getInstance(publicKey.getAlgorithm()); + X509EncodedKeySpec spec = new X509EncodedKeySpec(publicKey.getEncoded()); + PublicKey verificationKey = factory.generatePublic(spec); + mStoreBackend.enroll("user", "password", verificationKey); + } catch (KeyStoreException | CertificateException | NoSuchAlgorithmException | + IOException | InvalidKeySpecException e) { + e.printStackTrace(); + } + } + + /** + * Checks whether the current entered password is correct, and dismisses the the dialog and lets + * the activity know about the result. + */ + private void verifyPassword() { + Transaction transaction = new Transaction("user", 1); + if (!mStoreBackend.verify(transaction, mPassword.getText().toString())) { + return; + } + if (mStage == Stage.NEW_FINGERPRINT_ENROLLED) { + SharedPreferences.Editor editor = mSharedPreferences.edit(); + editor.putBoolean(getString(R.string.use_fingerprint_to_authenticate_key), + mUseFingerprintFutureCheckBox.isChecked()); + editor.apply(); + + if (mUseFingerprintFutureCheckBox.isChecked()) { + // Re-create the key so that fingerprints including new ones are validated. + mActivity.createKeyPair(); + mStage = Stage.FINGERPRINT; + } + } + mPassword.setText(""); + mActivity.onPurchased(null); + dismiss(); + } + + private final Runnable mShowKeyboardRunnable = new Runnable() { + @Override + public void run() { + mInputMethodManager.showSoftInput(mPassword, 0); + } + }; + + private void updateStage() { + switch (mStage) { + case FINGERPRINT: + mCancelButton.setText(R.string.cancel); + mSecondDialogButton.setText(R.string.use_password); + mFingerprintContent.setVisibility(View.VISIBLE); + mBackupContent.setVisibility(View.GONE); + break; + case NEW_FINGERPRINT_ENROLLED: + // Intentional fall through + case PASSWORD: + mCancelButton.setText(R.string.cancel); + mSecondDialogButton.setText(R.string.ok); + mFingerprintContent.setVisibility(View.GONE); + mBackupContent.setVisibility(View.VISIBLE); + if (mStage == Stage.NEW_FINGERPRINT_ENROLLED) { + mPasswordDescriptionTextView.setVisibility(View.GONE); + mNewFingerprintEnrolledTextView.setVisibility(View.VISIBLE); + mUseFingerprintFutureCheckBox.setVisibility(View.VISIBLE); + } + break; + } + } + + @Override + public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { + if (actionId == EditorInfo.IME_ACTION_GO) { + verifyPassword(); + return true; + } + return false; + } + + @Override + public void onAuthenticated() { + // Callback from FingerprintUiHelper. Let the activity know that authentication was + // successful. + mPassword.setText(""); + Signature signature = mCryptoObject.getSignature(); + Transaction transaction = new Transaction("user", 1); + try { + signature.update(transaction.toByteArray()); + byte[] sigBytes = signature.sign(); + if (mStoreBackend.verify(transaction, sigBytes)) { + mActivity.onPurchased(sigBytes); + dismiss(); + } else { + mActivity.onPurchaseFailed(); + dismiss(); + } + } catch (SignatureException e) { + throw new RuntimeException(e); + } + } + + @Override + public void onError() { + goToBackup(); + } + + /** + * Enumeration to indicate which authentication method the user is trying to authenticate with. + */ + public enum Stage { + FINGERPRINT, + NEW_FINGERPRINT_ENROLLED, + PASSWORD + } +} diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/FingerprintModule.java b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/FingerprintModule.java new file mode 100644 index 00000000..ae3acf80 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/FingerprintModule.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.example.android.asymmetricfingerprintdialog; + +import com.example.android.asymmetricfingerprintdialog.server.StoreBackend; +import com.example.android.asymmetricfingerprintdialog.server.StoreBackendImpl; + +import android.app.KeyguardManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.hardware.fingerprint.FingerprintManager; +import android.preference.PreferenceManager; +import android.security.keystore.KeyProperties; +import android.view.inputmethod.InputMethodManager; + +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Signature; + +import dagger.Module; +import dagger.Provides; + +/** + * Dagger module for Fingerprint APIs. + */ +@Module( + library = true, + injects = {MainActivity.class} +) +public class FingerprintModule { + + private final Context mContext; + + public FingerprintModule(Context context) { + mContext = context; + } + + @Provides + public Context providesContext() { + return mContext; + } + + @Provides + public FingerprintManager providesFingerprintManager(Context context) { + return context.getSystemService(FingerprintManager.class); + } + + @Provides + public KeyguardManager providesKeyguardManager(Context context) { + return context.getSystemService(KeyguardManager.class); + } + + @Provides + public KeyStore providesKeystore() { + try { + return KeyStore.getInstance("AndroidKeyStore"); + } catch (KeyStoreException e) { + throw new RuntimeException("Failed to get an instance of KeyStore", e); + } + } + + @Provides + public KeyPairGenerator providesKeyPairGenerator() { + try { + return KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, "AndroidKeyStore"); + } catch (NoSuchAlgorithmException | NoSuchProviderException e) { + throw new RuntimeException("Failed to get an instance of KeyPairGenerator", e); + } + } + + @Provides + public Signature providesSignature(KeyStore keyStore) { + try { + return Signature.getInstance("SHA256withECDSA"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Failed to get an instance of Signature", e); + } + } + + @Provides + public InputMethodManager providesInputMethodManager(Context context) { + return (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + } + + @Provides + public SharedPreferences providesSharedPreferences(Context context) { + return PreferenceManager.getDefaultSharedPreferences(context); + } + + @Provides + public StoreBackend providesStoreBackend() { + return new StoreBackendImpl(); + } +} diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/FingerprintUiHelper.java b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/FingerprintUiHelper.java new file mode 100644 index 00000000..f6548116 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/FingerprintUiHelper.java @@ -0,0 +1,165 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.example.android.asymmetricfingerprintdialog; + +import com.google.common.annotations.VisibleForTesting; + +import android.hardware.fingerprint.FingerprintManager; +import android.os.CancellationSignal; +import android.widget.ImageView; +import android.widget.TextView; + +import javax.inject.Inject; + +/** + * Small helper class to manage text/icon around fingerprint authentication UI. + */ +public class FingerprintUiHelper extends FingerprintManager.AuthenticationCallback { + + @VisibleForTesting static final long ERROR_TIMEOUT_MILLIS = 1600; + @VisibleForTesting static final long SUCCESS_DELAY_MILLIS = 1300; + + private final FingerprintManager mFingerprintManager; + private final ImageView mIcon; + private final TextView mErrorTextView; + private final Callback mCallback; + private CancellationSignal mCancellationSignal; + + @VisibleForTesting boolean mSelfCancelled; + + /** + * Builder class for {@link FingerprintUiHelper} in which injected fields from Dagger + * holds its fields and takes other arguments in the {@link #build} method. + */ + public static class FingerprintUiHelperBuilder { + private final FingerprintManager mFingerPrintManager; + + @Inject + public FingerprintUiHelperBuilder(FingerprintManager fingerprintManager) { + mFingerPrintManager = fingerprintManager; + } + + public FingerprintUiHelper build(ImageView icon, TextView errorTextView, Callback callback) { + return new FingerprintUiHelper(mFingerPrintManager, icon, errorTextView, + callback); + } + } + + /** + * Constructor for {@link FingerprintUiHelper}. This method is expected to be called from + * only the {@link FingerprintUiHelperBuilder} class. + */ + private FingerprintUiHelper(FingerprintManager fingerprintManager, + ImageView icon, TextView errorTextView, Callback callback) { + mFingerprintManager = fingerprintManager; + mIcon = icon; + mErrorTextView = errorTextView; + mCallback = callback; + } + + public boolean isFingerprintAuthAvailable() { + return mFingerprintManager.isHardwareDetected() + && mFingerprintManager.hasEnrolledFingerprints(); + } + + public void startListening(FingerprintManager.CryptoObject cryptoObject) { + if (!isFingerprintAuthAvailable()) { + return; + } + mCancellationSignal = new CancellationSignal(); + mSelfCancelled = false; + mFingerprintManager + .authenticate(cryptoObject, mCancellationSignal, 0 /* flags */, this, null); + mIcon.setImageResource(R.drawable.ic_fp_40px); + } + + public void stopListening() { + if (mCancellationSignal != null) { + mSelfCancelled = true; + mCancellationSignal.cancel(); + mCancellationSignal = null; + } + } + + @Override + public void onAuthenticationError(int errMsgId, CharSequence errString) { + if (!mSelfCancelled) { + showError(errString); + mIcon.postDelayed(new Runnable() { + @Override + public void run() { + mCallback.onError(); + } + }, ERROR_TIMEOUT_MILLIS); + } + } + + @Override + public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) { + showError(helpString); + } + + @Override + public void onAuthenticationFailed() { + showError(mIcon.getResources().getString( + R.string.fingerprint_not_recognized)); + } + + @Override + public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) { + mErrorTextView.removeCallbacks(mResetErrorTextRunnable); + mIcon.setImageResource(R.drawable.ic_fingerprint_success); + mErrorTextView.setTextColor( + mErrorTextView.getResources().getColor(R.color.success_color, null)); + mErrorTextView.setText( + mErrorTextView.getResources().getString(R.string.fingerprint_success)); + mIcon.postDelayed(new Runnable() { + @Override + public void run() { + mCallback.onAuthenticated(); + } + }, SUCCESS_DELAY_MILLIS); + } + + private void showError(CharSequence error) { + mIcon.setImageResource(R.drawable.ic_fingerprint_error); + mErrorTextView.setText(error); + mErrorTextView.setTextColor( + mErrorTextView.getResources().getColor(R.color.warning_color, null)); + mErrorTextView.removeCallbacks(mResetErrorTextRunnable); + mErrorTextView.postDelayed(mResetErrorTextRunnable, ERROR_TIMEOUT_MILLIS); + } + + @VisibleForTesting + Runnable mResetErrorTextRunnable = new Runnable() { + @Override + public void run() { + mErrorTextView.setTextColor( + mErrorTextView.getResources().getColor(R.color.hint_color, null)); + mErrorTextView.setText( + mErrorTextView.getResources().getString(R.string.fingerprint_hint)); + mIcon.setImageResource(R.drawable.ic_fp_40px); + } + }; + + public interface Callback { + + void onAuthenticated(); + + void onError(); + } +} diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/InjectedApplication.java b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/InjectedApplication.java new file mode 100644 index 00000000..1c3ed7e2 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/InjectedApplication.java @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.example.android.asymmetricfingerprintdialog; + +import android.app.Application; +import android.util.Log; + +import dagger.ObjectGraph; + +/** + * The Application class of the sample which holds the ObjectGraph in Dagger and enables + * dependency injection. + */ +public class InjectedApplication extends Application { + + private static final String TAG = InjectedApplication.class.getSimpleName(); + + private ObjectGraph mObjectGraph; + + @Override + public void onCreate() { + super.onCreate(); + + initObjectGraph(new FingerprintModule(this)); + } + + /** + * Initialize the Dagger module. Passing null or mock modules can be used for testing. + * + * @param module for Dagger + */ + public void initObjectGraph(Object module) { + mObjectGraph = module != null ? ObjectGraph.create(module) : null; + } + + public void inject(Object object) { + if (mObjectGraph == null) { + // This usually happens during tests. + Log.i(TAG, "Object graph is not initialized."); + return; + } + mObjectGraph.inject(object); + } + +} diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/MainActivity.java b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/MainActivity.java new file mode 100644 index 00000000..e6031c52 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/MainActivity.java @@ -0,0 +1,230 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.example.android.asymmetricfingerprintdialog; + +import android.Manifest; +import android.app.Activity; +import android.app.KeyguardManager; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.hardware.fingerprint.FingerprintManager; +import android.os.Bundle; +import android.security.keystore.KeyGenParameterSpec; +import android.security.keystore.KeyPermanentlyInvalidatedException; +import android.security.keystore.KeyProperties; +import android.util.Base64; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import java.io.IOException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.KeyPairGenerator; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.UnrecoverableKeyException; +import java.security.cert.CertificateException; +import java.security.spec.ECGenParameterSpec; + +import javax.inject.Inject; + +/** + * Main entry point for the sample, showing a backpack and "Purchase" button. + */ +public class MainActivity extends Activity { + + private static final String TAG = MainActivity.class.getSimpleName(); + + private static final String DIALOG_FRAGMENT_TAG = "myFragment"; + private static final String SECRET_MESSAGE = "Very secret message"; + /** Alias for our key in the Android Key Store */ + public static final String KEY_NAME = "my_key"; + + private static final int FINGERPRINT_PERMISSION_REQUEST_CODE = 0; + + @Inject KeyguardManager mKeyguardManager; + @Inject FingerprintManager mFingerprintManager; + @Inject FingerprintAuthenticationDialogFragment mFragment; + @Inject KeyStore mKeyStore; + @Inject KeyPairGenerator mKeyPairGenerator; + @Inject Signature mSignature; + @Inject SharedPreferences mSharedPreferences; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + ((InjectedApplication) getApplication()).inject(this); + + requestPermissions(new String[]{Manifest.permission.USE_FINGERPRINT}, + FINGERPRINT_PERMISSION_REQUEST_CODE); + } + + @Override + public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] state) { + if (requestCode == FINGERPRINT_PERMISSION_REQUEST_CODE + && state[0] == PackageManager.PERMISSION_GRANTED) { + setContentView(R.layout.activity_main); + Button purchaseButton = (Button) findViewById(R.id.purchase_button); + if (!mKeyguardManager.isKeyguardSecure()) { + // Show a message that the user hasn't set up a fingerprint or lock screen. + Toast.makeText(this, + "Secure lock screen hasn't set up.\n" + + "Go to 'Settings -> Security -> Fingerprint' to set up a fingerprint", + Toast.LENGTH_LONG).show(); + purchaseButton.setEnabled(false); + return; + } + if (!mFingerprintManager.hasEnrolledFingerprints()) { + purchaseButton.setEnabled(false); + // This happens when no fingerprints are registered. + Toast.makeText(this, + "Go to 'Settings -> Security -> Fingerprint' and register at least one fingerprint", + Toast.LENGTH_LONG).show(); + return; + } + createKeyPair(); + purchaseButton.setEnabled(true); + purchaseButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + findViewById(R.id.confirmation_message).setVisibility(View.GONE); + findViewById(R.id.encrypted_message).setVisibility(View.GONE); + + // Set up the crypto object for later. The object will be authenticated by use + // of the fingerprint. + if (initSignature()) { + + // Show the fingerprint dialog. The user has the option to use the fingerprint with + // crypto, or you can fall back to using a server-side verified password. + mFragment.setCryptoObject(new FingerprintManager.CryptoObject(mSignature)); + boolean useFingerprintPreference = mSharedPreferences + .getBoolean(getString(R.string.use_fingerprint_to_authenticate_key), + true); + if (useFingerprintPreference) { + mFragment.setStage( + FingerprintAuthenticationDialogFragment.Stage.FINGERPRINT); + } else { + mFragment.setStage( + FingerprintAuthenticationDialogFragment.Stage.PASSWORD); + } + mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG); + } else { + // This happens if the lock screen has been disabled or or a fingerprint got + // enrolled. Thus show the dialog to authenticate with their password first + // and ask the user if they want to authenticate with fingerprints in the + // future + mFragment.setStage( + FingerprintAuthenticationDialogFragment.Stage.NEW_FINGERPRINT_ENROLLED); + mFragment.show(getFragmentManager(), DIALOG_FRAGMENT_TAG); + } + } + }); + } + } + + /** + * Initialize the {@link Signature} instance with the created key in the + * {@link #createKeyPair()} method. + * + * @return {@code true} if initialization is successful, {@code false} if the lock screen has + * been disabled or reset after the key was generated, or if a fingerprint got enrolled after + * the key was generated. + */ + private boolean initSignature() { + try { + mKeyStore.load(null); + PrivateKey key = (PrivateKey) mKeyStore.getKey(KEY_NAME, null); + mSignature.initSign(key); + return true; + } catch (KeyPermanentlyInvalidatedException e) { + return false; + } catch (KeyStoreException | CertificateException | UnrecoverableKeyException | IOException + | NoSuchAlgorithmException | InvalidKeyException e) { + throw new RuntimeException("Failed to init Cipher", e); + } + } + + public void onPurchased(byte[] signature) { + showConfirmation(signature); + } + + public void onPurchaseFailed() { + Toast.makeText(this, R.string.purchase_fail, Toast.LENGTH_SHORT).show(); + } + + // Show confirmation, if fingerprint was used show crypto information. + private void showConfirmation(byte[] encrypted) { + findViewById(R.id.confirmation_message).setVisibility(View.VISIBLE); + if (encrypted != null) { + TextView v = (TextView) findViewById(R.id.encrypted_message); + v.setVisibility(View.VISIBLE); + v.setText(Base64.encodeToString(encrypted, 0 /* flags */)); + } + } + + /** + * Generates an asymmetric key pair in the Android Keystore. Every use of the private key must + * be authorized by the user authenticating with fingerprint. Public key use is unrestricted. + */ + public void createKeyPair() { + // The enrolling flow for fingerprint. This is where you ask the user to set up fingerprint + // for your flow. Use of keys is necessary if you need to know if the set of + // enrolled fingerprints has changed. + try { + // Set the alias of the entry in Android KeyStore where the key will appear + // and the constrains (purposes) in the constructor of the Builder + mKeyPairGenerator.initialize( + new KeyGenParameterSpec.Builder(KEY_NAME, + KeyProperties.PURPOSE_SIGN) + .setDigests(KeyProperties.DIGEST_SHA256) + .setAlgorithmParameterSpec(new ECGenParameterSpec("secp256r1")) + // Require the user to authenticate with a fingerprint to authorize + // every use of the private key + .setUserAuthenticationRequired(true) + .build()); + mKeyPairGenerator.generateKeyPair(); + } catch (InvalidAlgorithmParameterException e) { + throw new RuntimeException(e); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_main, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + + if (id == R.id.action_settings) { + Intent intent = new Intent(this, SettingsActivity.class); + startActivity(intent); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/SettingsActivity.java b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/SettingsActivity.java new file mode 100644 index 00000000..acc5e378 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/SettingsActivity.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.example.android.asymmetricfingerprintdialog; + +import android.app.Activity; +import android.os.Bundle; +import android.preference.PreferenceFragment; + +public class SettingsActivity extends Activity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Display the fragment as the main content. + getFragmentManager().beginTransaction().replace(android.R.id.content, + new SettingsFragment()).commit(); + } + + /** + * Fragment for settings. + */ + public static class SettingsFragment extends PreferenceFragment { + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.preferences); + } + } +} + + diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/server/StoreBackend.java b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/server/StoreBackend.java new file mode 100644 index 00000000..dde81519 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/server/StoreBackend.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.example.android.asymmetricfingerprintdialog.server; + +import java.security.PublicKey; + +/** + * An interface that defines the methods required for the store backend. + */ +public interface StoreBackend { + + /** + * Verifies the authenticity of the provided transaction by confirming that it was signed with + * the private key enrolled for the userId. + * + * @param transaction the contents of the purchase transaction, its contents are + * signed + * by the + * private key in the client side. + * @param transactionSignature the signature of the transaction's contents. + * @return true if the signedSignature was verified, false otherwise. If this method returns + * true, the server can consider the transaction is successful. + */ + boolean verify(Transaction transaction, byte[] transactionSignature); + + /** + * Verifies the authenticity of the provided transaction by password. + * + * @param transaction the contents of the purchase transaction, its contents are signed by the + * private key in the client side. + * @param password the password for the user associated with the {@code transaction}. + * @return true if the password is verified. + */ + boolean verify(Transaction transaction, String password); + + /** + * Enrolls a public key associated with the userId + * + * @param userId the unique ID of the user within the app including server side + * implementation + * @param password the password for the user for the server side + * @param publicKey the public key object to verify the signature from the user + * @return true if the enrollment was successful, false otherwise + */ + boolean enroll(String userId, String password, PublicKey publicKey); + +} diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/server/StoreBackendImpl.java b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/server/StoreBackendImpl.java new file mode 100644 index 00000000..8bf48d89 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/server/StoreBackendImpl.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.example.android.asymmetricfingerprintdialog.server; + + +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.Signature; +import java.security.SignatureException; +import java.util.HashMap; +import java.util.Map; + +/** + * A fake backend implementation of {@link StoreBackend}. + */ +public class StoreBackendImpl implements StoreBackend { + + private final Map<String, PublicKey> mPublicKeys = new HashMap<>(); + + @Override + public boolean verify(Transaction transaction, byte[] transactionSignature) { + try { + PublicKey publicKey = mPublicKeys.get(transaction.getUserId()); + Signature verificationFunction = Signature.getInstance("SHA256withECDSA"); + verificationFunction.initVerify(publicKey); + verificationFunction.update(transaction.toByteArray()); + if (verificationFunction.verify(transactionSignature)) { + // Transaction is verified with the public key associated with the user + // Do some post purchase processing in the server + return true; + } + } catch (NoSuchAlgorithmException | InvalidKeyException | SignatureException e) { + // In a real world, better to send some error message to the user + } + return false; + } + + @Override + public boolean verify(Transaction transaction, String password) { + // As this is just a sample, we always assume that the password is right. + return true; + } + + @Override + public boolean enroll(String userId, String password, PublicKey publicKey) { + if (publicKey != null) { + mPublicKeys.put(userId, publicKey); + } + // We just ignore the provided password here, but in real life, it is registered to the + // backend. + return true; + } + +} diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/server/Transaction.java b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/server/Transaction.java new file mode 100644 index 00000000..33b65ef8 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/java/com/example/android/asymmetricfingerprintdialog/server/Transaction.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * 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 com.example.android.asymmetricfingerprintdialog.server; + +import java.io.ByteArrayOutputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** + * An entity that represents a single transaction (purchase) of an item. + */ +public class Transaction { + + /** The unique ID of the item of the purchase */ + private final Long mItemId; + + /** The unique user ID who made the transaction */ + private final String mUserId; + + public Transaction(String userId, long itemId) { + mItemId = itemId; + mUserId = userId; + } + + public String getUserId() { + return mUserId; + } + + public byte[] toByteArray() { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + DataOutputStream dataOutputStream = null; + try { + dataOutputStream = new DataOutputStream(byteArrayOutputStream); + dataOutputStream.writeLong(mItemId); + dataOutputStream.writeUTF(mUserId); + return byteArrayOutputStream.toByteArray(); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + try { + if (dataOutputStream != null) { + dataOutputStream.close(); + } + } catch (IOException ignore) { + } + try { + byteArrayOutputStream.close(); + } catch (IOException ignore) { + } + } + } +} diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-hdpi/ic_fp_40px.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-hdpi/ic_fp_40px.png Binary files differnew file mode 100644 index 00000000..48ebd8ad --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-hdpi/ic_fp_40px.png diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-mdpi/ic_fp_40px.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-mdpi/ic_fp_40px.png Binary files differnew file mode 100644 index 00000000..122f4425 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-mdpi/ic_fp_40px.png diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-nodpi/android_robot.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-nodpi/android_robot.png Binary files differnew file mode 100644 index 00000000..40bf934b --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-nodpi/android_robot.png diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-xhdpi/ic_fp_40px.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-xhdpi/ic_fp_40px.png Binary files differnew file mode 100644 index 00000000..e1c9590b --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-xhdpi/ic_fp_40px.png diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-xxhdpi/ic_fp_40px.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-xxhdpi/ic_fp_40px.png Binary files differnew file mode 100644 index 00000000..f7e87240 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-xxhdpi/ic_fp_40px.png diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-xxxhdpi/ic_fp_40px.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-xxxhdpi/ic_fp_40px.png Binary files differnew file mode 100644 index 00000000..0fb85452 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable-xxxhdpi/ic_fp_40px.png diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable/card.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable/card.xml new file mode 100644 index 00000000..691a4c55 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable/card.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2015 The Android Open Source Project + ~ + ~ 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 + --> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + + <solid + android:color="#fefefe"/> + + <corners + android:radius="2dp" /> +</shape>
\ No newline at end of file diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable/ic_fingerprint_error.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable/ic_fingerprint_error.xml new file mode 100644 index 00000000..be46116d --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable/ic_fingerprint_error.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2015 The Android Open Source Project + ~ + ~ 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 + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="40.0dp" + android:height="40.0dp" + android:viewportWidth="40.0" + android:viewportHeight="40.0"> + <path + android:pathData="M20.0,0.0C8.96,0.0 0.0,8.95 0.0,20.0s8.96,20.0 20.0,20.0c11.04,0.0 20.0,-8.95 20.0,-20.0S31.04,0.0 20.0,0.0z" + android:fillColor="#F4511E"/> + <path + android:pathData="M21.33,29.33l-2.67,0.0l0.0,-2.67l2.67,0.0L21.33,29.33zM21.33,22.67l-2.67,0.0l0.0,-12.0l2.67,0.0L21.33,22.67z" + android:fillColor="#FFFFFF"/> +</vector> diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable/ic_fingerprint_success.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable/ic_fingerprint_success.xml new file mode 100644 index 00000000..261f3e7f --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/drawable/ic_fingerprint_success.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2015 The Android Open Source Project + ~ + ~ 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 + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="40.0dp" + android:height="40.0dp" + android:viewportWidth="40.0" + android:viewportHeight="40.0"> + <path + android:pathData="M20.0,20.0m-20.0,0.0a20.0,20.0 0.0,1.0 1.0,40.0 0.0a20.0,20.0 0.0,1.0 1.0,-40.0 0.0" + android:fillColor="#009688"/> + <path + android:pathData="M11.2,21.41l1.63,-1.619999 4.17,4.169998 10.59,-10.589999 1.619999,1.63 -12.209999,12.209999z" + android:fillColor="#FFFFFF"/> +</vector> diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/activity_main.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..8f30557b --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/activity_main.xml @@ -0,0 +1,107 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2015 The Android Open Source Project + ~ + ~ 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 + --> +<ScrollView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <ImageView + android:layout_width="150dp" + android:layout_height="150dp" + android:layout_marginTop="32dp" + android:layout_marginBottom="32dp" + android:layout_gravity="center_horizontal" + android:scaleType="fitCenter" + android:src="@drawable/android_robot"/> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginStart="8dp" + android:layout_marginEnd="8dp" + android:orientation="vertical" + android:background="@drawable/card" + android:elevation="4dp" + android:paddingTop="16dp" + android:paddingBottom="16dp" + android:paddingStart="16dp" + android:paddingEnd="16dp"> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@android:style/TextAppearance.Material.Headline" + android:text="@string/item_title"/> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@android:style/TextAppearance.Material.Body2" + android:textColor="?android:attr/colorAccent" + android:text="@string/item_price"/> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:textAppearance="@android:style/TextAppearance.Material.Body1" + android:textColor="?android:attr/textColorSecondary" + android:text="@string/item_description"/> + + </LinearLayout> + <Button style="@android:style/Widget.Material.Button.Colored" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="8dp" + android:layout_marginEnd="4dp" + android:layout_gravity="end" + android:textColor="?android:attr/textColorPrimaryInverse" + android:text="@string/purchase" + android:id="@+id/purchase_button" + android:layout_alignParentEnd="true"/> + + <TextView + android:id="@+id/confirmation_message" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:textAppearance="@android:style/TextAppearance.Material.Body2" + android:textColor="?android:attr/colorAccent" + android:text="@string/purchase_done" + android:visibility="gone"/> + + <TextView + android:id="@+id/encrypted_message" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="16dp" + android:paddingStart="16dp" + android:paddingEnd="16dp" + android:textAppearance="@android:style/TextAppearance.Material.Body2" + android:textColor="?android:attr/colorAccent" + android:text="@string/purchase_done" + android:visibility="gone"/> + </LinearLayout> +</ScrollView>
\ No newline at end of file diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/fingerprint_dialog_backup.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/fingerprint_dialog_backup.xml new file mode 100644 index 00000000..2be05b11 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/fingerprint_dialog_backup.xml @@ -0,0 +1,78 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2015 The Android Open Source Project + ~ + ~ 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 + --> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/backup_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingTop="16dp" + android:paddingBottom="8dp"> + + <FrameLayout + android:id="@+id/description" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:layout_alignParentStart="true" + android:layout_marginStart="24dp" + android:layout_marginEnd="24dp" + > + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@android:style/TextAppearance.Material.Subhead" + android:text="@string/password_description" + android:id="@+id/password_description" + android:textColor="?android:attr/textColorSecondary" /> + + <TextView + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textAppearance="@android:style/TextAppearance.Material.Subhead" + android:text="@string/new_fingerprint_enrolled_description" + android:id="@+id/new_fingerprint_enrolled_description" + android:visibility="gone" + android:textColor="?android:attr/textColorSecondary" /> + </FrameLayout> + + <EditText + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:inputType="textPassword" + android:ems="10" + android:hint="@string/password" + android:imeOptions="actionGo" + android:id="@+id/password" + android:layout_below="@+id/description" + android:layout_marginTop="16dp" + android:layout_marginStart="20dp" + android:layout_marginEnd="20dp" + android:layout_alignParentStart="true" /> + + <CheckBox + android:id="@+id/use_fingerprint_in_future_check" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@+id/password" + android:layout_alignParentStart="true" + android:layout_marginTop="16dp" + android:layout_marginStart="20dp" + android:layout_marginEnd="20dp" + android:checked="true" + android:visibility="gone" + android:text="@string/use_fingerprint_in_future" /> + +</RelativeLayout>
\ No newline at end of file diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/fingerprint_dialog_container.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/fingerprint_dialog_container.xml new file mode 100644 index 00000000..08bb1bbb --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/fingerprint_dialog_container.xml @@ -0,0 +1,65 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2015 The Android Open Source Project + ~ + ~ 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 + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <FrameLayout + android:layout_width="wrap_content" + android:layout_height="wrap_content"> + + <include layout="@layout/fingerprint_dialog_content" /> + + <include + layout="@layout/fingerprint_dialog_backup" + android:visibility="gone" /> + + </FrameLayout> + + <LinearLayout + android:id="@+id/buttonPanel" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:paddingStart="12dp" + android:paddingEnd="12dp" + android:paddingTop="4dp" + android:paddingBottom="4dp" + android:gravity="bottom" + style="?android:attr/buttonBarStyle"> + + <Space + android:id="@+id/spacer" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_weight="1" + android:visibility="invisible" /> + <Button + android:id="@+id/cancel_button" + style="?android:attr/buttonBarNegativeButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + + <Button + android:id="@+id/second_dialog_button" + style="?android:attr/buttonBarPositiveButtonStyle" + android:layout_width="wrap_content" + android:layout_height="wrap_content"/> + </LinearLayout> + +</LinearLayout>
\ No newline at end of file diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/fingerprint_dialog_content.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/fingerprint_dialog_content.xml new file mode 100644 index 00000000..3929ebae --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/layout/fingerprint_dialog_content.xml @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2015 The Android Open Source Project + ~ + ~ 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 + --> +<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/fingerprint_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:paddingBottom="8dp" + android:paddingStart="24dp" + android:paddingEnd="24dp" + android:paddingTop="16dp"> + + <TextView + android:id="@+id/fingerprint_description" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:text="@string/fingerprint_description" + android:textAppearance="@android:style/TextAppearance.Material.Subhead" + android:textColor="?android:attr/textColorSecondary"/> + + + <ImageView + android:id="@+id/fingerprint_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignParentStart="true" + android:layout_below="@+id/fingerprint_description" + android:layout_marginTop="20dp" + android:src="@drawable/ic_fp_40px" /> + + <TextView + android:id="@+id/fingerprint_status" + style="@android:style/TextAppearance.Material.Body1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_alignBottom="@+id/fingerprint_icon" + android:layout_alignTop="@+id/fingerprint_icon" + android:layout_marginStart="16dp" + android:layout_toEndOf="@+id/fingerprint_icon" + android:gravity="center_vertical" + android:text="@string/fingerprint_hint" + android:textColor="@color/hint_color" /> +</RelativeLayout>
\ No newline at end of file diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/menu/menu_main.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/menu/menu_main.xml new file mode 100644 index 00000000..73f5e89a --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/menu/menu_main.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2015 The Android Open Source Project + ~ + ~ 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 + --> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" tools:context=".MainActivity"> + <item android:id="@+id/action_settings" android:title="@string/action_settings" + android:orderInCategory="100" android:showAsAction="never" /> +</menu> diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-hdpi/ic_launcher.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..b82d7af3 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-mdpi/ic_launcher.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..b0d26bae --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-xhdpi/ic_launcher.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..135858d1 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-xxhdpi/ic_launcher.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..02960da8 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-xxxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 00000000..d4254591 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/values/colors.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/values/colors.xml new file mode 100644 index 00000000..a24f3c8f --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/values/colors.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2015 The Android Open Source Project + ~ + ~ 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 + --> +<resources> + <color name="warning_color">#f4511e</color> + <color name="hint_color">#42000000</color> + <color name="success_color">#009688</color> +</resources> diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/values/strings.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/values/strings.xml new file mode 100644 index 00000000..f44c06d6 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/values/strings.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2015 The Android Open Source Project + ~ + ~ 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 + --> +<resources> + <string name="action_settings">Settings</string> + <string name="cancel">Cancel</string> + <string name="use_password">Use password</string> + <string name="sign_in">Sign in</string> + <string name="ok">Ok</string> + <string name="password">Password</string> + <string name="fingerprint_description">Confirm fingerprint to continue</string> + <string name="fingerprint_hint">Touch sensor</string> + <string name="password_description">Enter your store password to continue</string> + <string name="purchase">Purchase</string> + <string name="fingerprint_not_recognized">Fingerprint not recognized. Try again</string> + <string name="fingerprint_success">Fingerprint recognized</string> + <string name="item_title">White Mesh Pluto Backpack</string> + <string name="item_price">$62.68</string> + <string name="item_description">Mesh backpack in white. Black textile trim throughout.</string> + <string name="purchase_done">Purchase successful</string> + <string name="purchase_fail">Purchase failed</string> + <string name="new_fingerprint_enrolled_description">A new fingerprint was added to this device, so your password is required.</string> + <string name="use_fingerprint_in_future">Use fingerprint in the future</string> + <string name="use_fingerprint_to_authenticate_title">Use fingerprint to authenticate</string> + <string name="use_fingerprint_to_authenticate_key" >use_fingerprint_to_authenticate_key</string> +</resources> diff --git a/security/AsymmetricFingerprintDialog/Application/src/main/res/xml/preferences.xml b/security/AsymmetricFingerprintDialog/Application/src/main/res/xml/preferences.xml new file mode 100644 index 00000000..761391d5 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/main/res/xml/preferences.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2015 The Android Open Source Project + ~ + ~ 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 + --> +<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"> + <CheckBoxPreference + android:key="@string/use_fingerprint_to_authenticate_key" + android:title="@string/use_fingerprint_to_authenticate_title" + android:persistent="true" + android:defaultValue="true" /> +</PreferenceScreen>
\ No newline at end of file diff --git a/security/AsymmetricFingerprintDialog/Application/src/test/java/com/example/android/fingerprintdialog/FingerprintUiHelperTest.java b/security/AsymmetricFingerprintDialog/Application/src/test/java/com/example/android/fingerprintdialog/FingerprintUiHelperTest.java new file mode 100644 index 00000000..a5a91085 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/Application/src/test/java/com/example/android/fingerprintdialog/FingerprintUiHelperTest.java @@ -0,0 +1,111 @@ +package com.example.android.asymmetricfingerprintdialog; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; + +import android.content.res.Resources; +import android.hardware.fingerprint.FingerprintManager; +import android.os.CancellationSignal; +import android.os.Handler; +import android.widget.ImageView; +import android.widget.TextView; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.isA; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit tests for {@link FingerprintUiHelper}. + */ +@RunWith(MockitoJUnitRunner.class) +public class FingerprintUiHelperTest { + + private static final int ERROR_MSG_ID = 1; + private static final CharSequence ERR_STRING = "ERROR_STRING"; + private static final int HINT_COLOR = 10; + + @Mock private FingerprintManager mockFingerprintManager; + @Mock private ImageView mockIcon; + @Mock private TextView mockTextView; + @Mock private FingerprintUiHelper.Callback mockCallback; + @Mock private FingerprintManager.CryptoObject mockCryptoObject; + @Mock private Resources mockResources; + + @Captor private ArgumentCaptor<Runnable> mRunnableArgumentCaptor; + + @InjectMocks private FingerprintUiHelper.FingerprintUiHelperBuilder mockBuilder; + + private FingerprintUiHelper mFingerprintUiHelper; + + @Before + public void setUp() { + mFingerprintUiHelper = mockBuilder.build(mockIcon, mockTextView, mockCallback); + + when(mockFingerprintManager.isHardwareDetected()).thenReturn(true); + when(mockFingerprintManager.hasEnrolledFingerprints()).thenReturn(true); + when(mockTextView.getResources()).thenReturn(mockResources); + when(mockResources.getColor(R.color.hint_color, null)).thenReturn(HINT_COLOR); + } + + @Test + public void testStartListening_fingerprintAuthAvailable() { + mFingerprintUiHelper.startListening(mockCryptoObject); + + verify(mockFingerprintManager).authenticate(eq(mockCryptoObject), + isA(CancellationSignal.class), eq(0), eq(mFingerprintUiHelper), + any(Handler.class)); + verify(mockIcon).setImageResource(R.drawable.ic_fp_40px); + } + + @Test + public void testStartListening_fingerprintAuthNotAvailable() { + when(mockFingerprintManager.isHardwareDetected()).thenReturn(false); + + mFingerprintUiHelper.startListening(mockCryptoObject); + + verify(mockFingerprintManager, never()).authenticate( + any(FingerprintManager.CryptoObject.class), any(CancellationSignal.class), eq(0), + any(FingerprintUiHelper.class), any(Handler.class)); + } + + @Test + public void testOnAuthenticationError() { + mFingerprintUiHelper.mSelfCancelled = false; + + mFingerprintUiHelper.onAuthenticationError(ERROR_MSG_ID, ERR_STRING); + + verify(mockIcon).setImageResource(R.drawable.ic_fingerprint_error); + verify(mockTextView).removeCallbacks(mFingerprintUiHelper.mResetErrorTextRunnable); + verify(mockTextView).postDelayed(mFingerprintUiHelper.mResetErrorTextRunnable, + FingerprintUiHelper.ERROR_TIMEOUT_MILLIS); + verify(mockIcon).postDelayed(mRunnableArgumentCaptor.capture(), + eq(FingerprintUiHelper.ERROR_TIMEOUT_MILLIS)); + + mRunnableArgumentCaptor.getValue().run(); + + verify(mockCallback).onError(); + } + + @Test + public void testOnAuthenticationSucceeded() { + mFingerprintUiHelper.onAuthenticationSucceeded(null); + + verify(mockIcon).setImageResource(R.drawable.ic_fingerprint_success); + verify(mockTextView).removeCallbacks(mFingerprintUiHelper.mResetErrorTextRunnable); + verify(mockIcon).postDelayed(mRunnableArgumentCaptor.capture(), + eq(FingerprintUiHelper.SUCCESS_DELAY_MILLIS)); + + mRunnableArgumentCaptor.getValue().run(); + + verify(mockCallback).onAuthenticated(); + } +} diff --git a/security/AsymmetricFingerprintDialog/build.gradle b/security/AsymmetricFingerprintDialog/build.gradle new file mode 100644 index 00000000..2b8d1ef1 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/build.gradle @@ -0,0 +1,11 @@ + +// BEGIN_EXCLUDE +import com.example.android.samples.build.SampleGenPlugin +apply plugin: SampleGenPlugin + +samplegen { + pathToBuild "../../../../build" + pathToSamplesCommon "../../common" +} +apply from: "../../../../build/build.gradle" +// END_EXCLUDE diff --git a/security/AsymmetricFingerprintDialog/buildSrc/build.gradle b/security/AsymmetricFingerprintDialog/buildSrc/build.gradle new file mode 100644 index 00000000..8c294c23 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/buildSrc/build.gradle @@ -0,0 +1,15 @@ +repositories { + mavenCentral() +} +dependencies { + compile 'org.freemarker:freemarker:2.3.20' +} + +sourceSets { + main { + groovy { + srcDir new File(rootDir, "../../../../../build/buildSrc/src/main/groovy") + } + } +} + diff --git a/security/AsymmetricFingerprintDialog/gradle/wrapper/gradle-wrapper.jar b/security/AsymmetricFingerprintDialog/gradle/wrapper/gradle-wrapper.jar Binary files differnew file mode 100644 index 00000000..8c0fb64a --- /dev/null +++ b/security/AsymmetricFingerprintDialog/gradle/wrapper/gradle-wrapper.jar diff --git a/security/AsymmetricFingerprintDialog/gradle/wrapper/gradle-wrapper.properties b/security/AsymmetricFingerprintDialog/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..f943b1e7 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Mon Apr 27 11:28:32 JST 2015 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=http\://services.gradle.org/distributions/gradle-2.2.1-all.zip diff --git a/security/AsymmetricFingerprintDialog/gradlew b/security/AsymmetricFingerprintDialog/gradlew new file mode 100755 index 00000000..91a7e269 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/gradlew @@ -0,0 +1,164 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# For Cygwin, ensure paths are in UNIX format before anything is touched. +if $cygwin ; then + [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` +fi + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/security/AsymmetricFingerprintDialog/gradlew.bat b/security/AsymmetricFingerprintDialog/gradlew.bat new file mode 100644 index 00000000..aec99730 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/security/AsymmetricFingerprintDialog/screenshots/1-purchase-screen.png b/security/AsymmetricFingerprintDialog/screenshots/1-purchase-screen.png Binary files differnew file mode 100644 index 00000000..0bf03bd0 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/screenshots/1-purchase-screen.png diff --git a/security/AsymmetricFingerprintDialog/screenshots/2-fingerprint-dialog.png b/security/AsymmetricFingerprintDialog/screenshots/2-fingerprint-dialog.png Binary files differnew file mode 100644 index 00000000..5e681f96 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/screenshots/2-fingerprint-dialog.png diff --git a/security/AsymmetricFingerprintDialog/screenshots/3-fingerprint-authenticated.png b/security/AsymmetricFingerprintDialog/screenshots/3-fingerprint-authenticated.png Binary files differnew file mode 100644 index 00000000..d485b1dd --- /dev/null +++ b/security/AsymmetricFingerprintDialog/screenshots/3-fingerprint-authenticated.png diff --git a/security/AsymmetricFingerprintDialog/screenshots/4-new-fingerprint-enrolled.png b/security/AsymmetricFingerprintDialog/screenshots/4-new-fingerprint-enrolled.png Binary files differnew file mode 100644 index 00000000..7dcf0800 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/screenshots/4-new-fingerprint-enrolled.png diff --git a/security/AsymmetricFingerprintDialog/screenshots/big-icon.png b/security/AsymmetricFingerprintDialog/screenshots/big-icon.png Binary files differnew file mode 100644 index 00000000..1063d0b6 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/screenshots/big-icon.png diff --git a/security/AsymmetricFingerprintDialog/settings.gradle b/security/AsymmetricFingerprintDialog/settings.gradle new file mode 100644 index 00000000..9464a359 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/settings.gradle @@ -0,0 +1 @@ +include 'Application' diff --git a/security/AsymmetricFingerprintDialog/template-params.xml b/security/AsymmetricFingerprintDialog/template-params.xml new file mode 100644 index 00000000..205f5e66 --- /dev/null +++ b/security/AsymmetricFingerprintDialog/template-params.xml @@ -0,0 +1,115 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Copyright 2015 The Android Open Source Project + + 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. +--> +<!-- TODO(thagikura) Add tests for Activity and Fragment once InstrumentationTests can be run + on an emulator or a device. + At this moment, due to the different API between the image and the SDK, they can't be launched. + E.g. Skipping device 'Nexus 5 - MNC', due to different API preview 'MNC' and 'android-MNC' + --> +<sample> + <name>Asymmetric Fingerprint Dialog Sample</name> + <group>security</group> + <package>com.example.android.asymmetricfingerprintdialog</package> + + <minSdk>23</minSdk> + <targetSdkVersion>23</targetSdkVersion> + <compileSdkVersion>23</compileSdkVersion> + + <dependency>com.squareup.dagger:dagger:1.2.2</dependency> + <dependency>com.squareup.dagger:dagger-compiler:1.2.2</dependency> + + <!-- TODO(thagikura) These dependencies should be created as testCompile instead of compile but + the template system doesn't allow androidTestCompile dependencies. Change it once fixed. + --> + <dependency>junit:junit:4.12</dependency> + <dependency>org.mockito:mockito-core:1.10.19</dependency> + + <strings> + <intro> + <![CDATA[ +This sample demonstrates how you can use registered fingerprints to authenticate the user +before proceeding some actions such as purchasing an item. This version uses asymmetric keys. + ]]> + </intro> + </strings> + + <template src="base" /> + + <metadata> + <!-- Values: {DRAFT | PUBLISHED | INTERNAL | DEPRECATED | SUPERCEDED} --> + <status>DRAFT</status> + <categories>security</categories> + <technologies>Android</technologies> + <languages>Java</languages> + <solutions>Mobile</solutions> + <level>INTERMEDIATE</level> + <icon>screenshots/big-icon.png</icon> + <screenshots> + <img>screenshots/1-purchase-screen.png</img> + <img>screenshots/2-fingerprint-dialog.png</img> + <img>screenshots/3-fingerprint-authenticated.png</img> + <img>screenshots/4-new-fingerprint-enrolled.png</img> + </screenshots> + + <api_refs> + <android>android.hardware.fingerprint.FingerprintManager</android> + <android>android.hardware.fingerprint.FingerprintManager.AuthenticationCallback</android> + <android>android.hardware.fingerprint.FingerprintManager.CryptoObject</android> + <android>android.security.KeyGenParameterSpec</android> + <android>java.security.KeyStore</android> + <android>java.security.Signature</android> + <android>java.security.KeyPairGenerator</android> + </api_refs> + + <description> +<![CDATA[ +A sample that demonstrates to use registered fingerprints to authenticate the user in your app +]]> + </description> + + <intro> +<![CDATA[ +This sample demonstrates how you can use registered fingerprints in your app to authenticate the +user before proceeding some actions such as purchasing an item. + +First you need to create an asymmetric key pair in the Android Key Store using [KeyPairGenerator][1] +in the way that its private key can only be used after the user has authenticated with fingerprint +and transmit the public key to your backend with the user verified password (In a real world, the +app should show proper UIs). + +By setting [KeyGeneratorSpec.Builder.setUserAuthenticationRequired][2] to true, you can permit the +use of the key only after the user authenticate it including when authenticated with the user's +fingerprint. + +Then start listening to a fingerprint on the fingerprint sensor by calling +[FingerprintManager.authenticate][3] with a [Signature][4] initialized with the asymmetric key pair +created. Or alternatively you can fall back to server-side verified password as an authenticator. + +Once the fingerprint (or password) is verified, the +[FingerprintManager.AuthenticationCallback#onAuthenticationSucceeded()][5] callback is called. + +Then you can verify the purchase transaction on server side with the public key passed from the +client, by verifying the piece of data signed by the Signature. + +[1]: https://developer.android.com/reference/java/security/KeyPairGenerator.html +[2]: https://developer.android.com/reference/android/security/KeyGenParameterSpec.Builder#setUserAuthenticationRequired().html +[3]: https://developer.android.com/reference/android/hardware/FingerprintManager#authenticate().html +[4]: https://developer.android.com/reference/java/security/Signature.html +[5]: https://developer.android.com/reference/android/hardware/FingerprintManager.AuthenticationCallback#onAuthenticationSucceeded().html +]]> + </intro> + </metadata> +</sample> |