com.tozny.e3db.android.DefaultKeyAuthenticator.java Source code

Java tutorial

Introduction

Here is the source code for com.tozny.e3db.android.DefaultKeyAuthenticator.java

Source

/*
 * TOZNY NON-COMMERCIAL LICENSE
 *
 * Tozny dual licenses this product. For commercial use, please contact
 * info@tozny.com. For non-commercial use, the contents of this file are
 * subject to the TOZNY NON-COMMERCIAL LICENSE (the "License") which
 * permits use of the software only by government agencies, schools,
 * universities, non-profit organizations or individuals on projects that
 * do not receive external funding other than government research grants
 * and contracts.  Any other use requires a commercial license. You may
 * not use this file except in compliance with the License. You may obtain
 * a copy of the License at https://tozny.com/legal/non-commercial-license.
 * Software distributed under the License is distributed on an "AS IS"
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
 * License for the specific language governing rights and limitations under
 * the License. Portions of the software are Copyright (c) TOZNY LLC, 2018.
 * All rights reserved.
 *
 */

package com.tozny.e3db.android;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.DialogFragment;
import android.app.Fragment;
import android.app.FragmentManager;
import android.app.FragmentTransaction;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.RequiresApi;
import android.support.v4.hardware.fingerprint.FingerprintManagerCompat;
import android.support.v4.os.CancellationSignal;
import android.text.InputType;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import com.tozny.e3db.R;

import java.security.GeneralSecurityException;
import java.security.UnrecoverableKeyException;

import static android.app.Activity.RESULT_OK;

/**
 * Performs fingerprint authentication to unlock protected keys.
 */
class DefaultKeyAuthenticator extends KeyAuthenticator {
    private final Activity activity;
    private final String title;

    /**
     * Create an instance that will display over the given activity.
     *
     * @param activity
     *         Activity that will host the dialog
     * @param title
     *         Title of the fingerprint dialog.
     */
    public DefaultKeyAuthenticator(Activity activity, String title) {
        if (activity == null)
            throw new IllegalArgumentException("activity");
        if (title == null)
            throw new IllegalArgumentException("title");

        this.activity = activity;
        this.title = title;
    }

    /**
     * A dialog which uses fingerprint APIs to authenticate the user. Not for public consumption,
     * but required to be so by fragments API.
     */
    @TargetApi(23)
    public static class FingerprintAuthDialogFragment extends DialogFragment
            implements FingerprintUiHelper.Callback {

        private Activity mActivity;
        private Callback mCallback;
        private FingerprintManagerCompat.CryptoObject mCryptoObject;
        private FingerprintUiHelper mFingerprintUiHelper;
        private String mTitle;

        public interface Callback {
            void onFingerprintAuthenticated(FingerprintManagerCompat.CryptoObject cryptoObject);

            void onFingerprintCancel();
        }

        @Override
        public void onAttach(Context context) {
            super.onAttach(context);
            mActivity = getActivity();
        }

        @Override
        public void onAuthenticated(FingerprintManagerCompat.CryptoObject cryptoObject) {
            // Callback from FingerprintUiHelper. Let the activity know that authentication was
            // successful.
            if (mCallback != null) {
                mCallback.onFingerprintAuthenticated(cryptoObject);
            }
            dismiss();
        }

        @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);
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            if (mTitle != null) {
                getDialog().setTitle(mTitle);
            } else {
                getDialog().setTitle(getString(R.string.sign_in));
            }
            View v = inflater.inflate(R.layout.fingerprint_dialog_container, container, false);
            Button mCancelButton = v.findViewById(R.id.cancel_button);
            mCancelButton.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View view) {
                    if (mCallback != null) {
                        mCallback.onFingerprintCancel();
                    }
                    dismiss();
                }
            });

            mFingerprintUiHelper = new FingerprintUiHelper(FingerprintManagerCompat.from(this.getActivity()),
                    (ImageView) v.findViewById(R.id.fingerprint_icon),
                    (TextView) v.findViewById(R.id.fingerprint_status), this);

            return v;
        }

        @Override
        public void onError(String errString) {
        }

        @Override
        public void onPause() {
            super.onPause();
            mFingerprintUiHelper.stopListening();
        }

        @Override
        public void onResume() {
            super.onResume();
            mFingerprintUiHelper.startListening(mCryptoObject);
        }

        public void setCallback(Callback callback) {
            mCallback = callback;
        }

        /**
         * Sets the crypto object to be passed in when authenticating with fingerprint.
         */
        public void setCryptoObject(FingerprintManagerCompat.CryptoObject cryptoObject) {
            mCryptoObject = cryptoObject;
        }

        public void setTitle(String title) {
            mTitle = title;
        }

        public void setTitle(int titleId) {
            mTitle = getString(titleId);
        }
    }

    /**
     * Small helper class to manage text/icon around fingerprint authentication UI.
     */
    @RequiresApi(api = Build.VERSION_CODES.M)
    private static class FingerprintUiHelper extends FingerprintManagerCompat.AuthenticationCallback {

        private static final long ERROR_TIMEOUT_MILLIS = 1600;
        private static final long SUCCESS_DELAY_MILLIS = 1300;
        private final Callback mCallback;
        private final TextView mErrorTextView;
        private final FingerprintManagerCompat mFingerprintManager;
        private final ImageView mIcon;
        private CancellationSignal mCancellationSignal;
        private 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);
            }
        };
        private boolean mSelfCancelled;

        /**
         * Constructor for {@link FingerprintUiHelper}.
         */
        FingerprintUiHelper(FingerprintManagerCompat fingerprintManager, ImageView icon, TextView errorTextView,
                Callback callback) {
            mFingerprintManager = fingerprintManager;
            mIcon = icon;
            mErrorTextView = errorTextView;
            mCallback = callback;
        }

        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);
        }

        public interface Callback {
            void onAuthenticated(FingerprintManagerCompat.CryptoObject cryptoObject);

            void onError(String errString);
        }

        @SuppressLint("MissingPermission")
        public boolean isFingerprintAuthAvailable() {
            return mFingerprintManager.isHardwareDetected() && mFingerprintManager.hasEnrolledFingerprints();
        }

        @Override
        public void onAuthenticationError(int errMsgId, final CharSequence errString) {
            if (!mSelfCancelled) {
                showError(errString);
                mIcon.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        mCallback.onError(errString.toString());
                    }
                }, ERROR_TIMEOUT_MILLIS);
            }
        }

        @Override
        public void onAuthenticationFailed() {
            showError(mIcon.getResources().getString(R.string.fingerprint_not_recognized));
        }

        @Override
        public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
            showError(helpString);
        }

        @Override
        public void onAuthenticationSucceeded(final FingerprintManagerCompat.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(result.getCryptoObject());
                }
            }, SUCCESS_DELAY_MILLIS);
        }

        @SuppressLint("MissingPermission")
        public void startListening(FingerprintManagerCompat.CryptoObject cryptoObject) {
            if (!isFingerprintAuthAvailable()) {
                return;
            }
            mCancellationSignal = new CancellationSignal();
            mSelfCancelled = false;
            mFingerprintManager.authenticate(cryptoObject, 0 /* flags */, mCancellationSignal, this, null);
            mIcon.setImageResource(R.drawable.ic_fp_40px);
        }

        public void stopListening() {
            if (mCancellationSignal != null) {
                mSelfCancelled = true;
                mCancellationSignal.cancel();
                mCancellationSignal = null;
            }
        }
    }

    /**
     * Public for technical reasons but shouldn't be.
     */
    @RequiresApi(Build.VERSION_CODES.M)
    public static final class DeviceCredentialsFragment extends Fragment {
        private final AuthenticateHandler cont;
        private final String title;
        private final KeyguardManager mgr;

        public DeviceCredentialsFragment() {
            this.cont = null;
            this.title = null;
            this.mgr = null;
        }

        @SuppressLint("ValidFragment")
        // Only used internally
        DeviceCredentialsFragment(AuthenticateHandler cont, String title, KeyguardManager mgr) {
            if (mgr == null)
                throw new IllegalArgumentException("mgr");
            if (cont == null)
                throw new IllegalArgumentException("cont");
            if (title == null)
                throw new IllegalArgumentException("title");

            this.title = title;
            this.cont = cont;
            this.mgr = mgr;
        }

        @Override
        public void onAttach(Context context) {
            super.onAttach(context);
            if (mgr != null && cont != null && title != null) {
                Intent confirmDeviceCredentialIntent = mgr.createConfirmDeviceCredentialIntent(title, "");
                if (confirmDeviceCredentialIntent != null)
                    startActivityForResult(confirmDeviceCredentialIntent, 1);
                else
                    cont.handleError(new GeneralSecurityException("Device credentials not set up."));
            }
        }

        @Override
        public void onActivityResult(int requestCode, int resultCode, Intent data) {
            if (this.cont != null) {
                if (requestCode == 1 && resultCode == RESULT_OK) {
                    this.cont.handleAuthenticated();
                } else if (requestCode == 1) {
                    this.cont.handleCancel();
                }
            }
        }
    }

    @RequiresApi(api = Build.VERSION_CODES.M)
    @Override
    public void authenticateWithLockScreen(AuthenticateHandler cont) {
        DeviceCredentialsFragment f = new DeviceCredentialsFragment(cont, title,
                (KeyguardManager) activity.getSystemService(Context.KEYGUARD_SERVICE));
        FragmentManager fragmentManager = activity.getFragmentManager();
        FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
        fragmentTransaction.add(f, "device_credentials_fragment");
        fragmentTransaction.commit();
    }

    final int[] wrongPasswordCount = { 0 };

    @Override
    public void getPassword(final PasswordHandler handler) {
        this.activity.runOnUiThread(new Runnable() {
            @Override
            public void run() {
                Context ctx = DefaultKeyAuthenticator.this.activity;

                final EditText input = new EditText(ctx);
                input.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD);

                new AlertDialog.Builder(DefaultKeyAuthenticator.this.activity)
                        .setMessage(ctx.getString(R.string.key_provider_please_enter_pin))
                        .setPositiveButton(ctx.getString(R.string.key_provider_ok),
                                new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialogInterface, int i) {
                                        try {
                                            handler.handlePassword(input.getText().toString());

                                        } catch (UnrecoverableKeyException e) {
                                            wrongPasswordCount[0]++;

                                            if (wrongPasswordCount[0] >= 3) {
                                                handler.handleError(
                                                        new RuntimeException("Too many password tries."));
                                            } else {
                                                Toast.makeText(DefaultKeyAuthenticator.this.activity,
                                                        e.getMessage(), Toast.LENGTH_SHORT).show();
                                                getPassword(handler);
                                            }
                                        }
                                    }
                                })
                        .setNegativeButton(ctx.getString(R.string.key_provider_cancel),
                                new DialogInterface.OnClickListener() {
                                    @Override
                                    public void onClick(DialogInterface dialogInterface, int i) {
                                        handler.handleCancel();
                                    }
                                })
                        .setView(input).show();

                input.setOnFocusChangeListener(new View.OnFocusChangeListener() {
                    @Override
                    public void onFocusChange(View view, boolean b) {
                        if (input.isEnabled() && input.isFocusable()) {
                            input.post(new Runnable() {
                                @Override
                                public void run() {
                                    final InputMethodManager imm = (InputMethodManager) DefaultKeyAuthenticator.this.activity
                                            .getSystemService(Context.INPUT_METHOD_SERVICE);
                                    imm.showSoftInput(input, InputMethodManager.SHOW_IMPLICIT);
                                }
                            });
                        }
                    }
                });
            }
        });
    }

    @Override
    public void authenticateWithFingerprint(FingerprintManagerCompat.CryptoObject cryptoObject,
            final AuthenticateHandler handler) {
        try {
            FingerprintAuthDialogFragment fragment = new FingerprintAuthDialogFragment();
            fragment.setCryptoObject(cryptoObject);
            fragment.setTitle(title);

            fragment.setCallback(new FingerprintAuthDialogFragment.Callback() {
                public void onFingerprintAuthenticated(FingerprintManagerCompat.CryptoObject cryptoObject) {
                    handler.handleAuthenticated();
                }

                public void onFingerprintCancel() {
                    handler.handleCancel();
                }
            });

            fragment.show(activity.getFragmentManager(), "fingerprintUI");
        } catch (Throwable e) {
            handler.handleError(e);
        }
    }
}