|
@@ -0,0 +1,348 @@
|
|
|
+/**
|
|
|
+ * Nextcloud Android client application
|
|
|
+ *
|
|
|
+ * @author Florian Lentz
|
|
|
+ * Copyright (C) 2017 Florian Lentz
|
|
|
+ *
|
|
|
+ * This program is free software; you can redistribute it and/or
|
|
|
+ * modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
|
|
|
+ * License as published by the Free Software Foundation; either
|
|
|
+ * version 3 of the License, or any later version.
|
|
|
+ *
|
|
|
+ * This program is distributed in the hope that it will be useful,
|
|
|
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
+ * GNU AFFERO GENERAL PUBLIC LICENSE for more details.
|
|
|
+ *
|
|
|
+ * You should have received a copy of the GNU Affero General Public
|
|
|
+ * License along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
+ */
|
|
|
+package com.owncloud.android.ui.activity;
|
|
|
+
|
|
|
+import android.Manifest;
|
|
|
+import android.annotation.SuppressLint;
|
|
|
+import android.annotation.TargetApi;
|
|
|
+import android.app.KeyguardManager;
|
|
|
+import android.content.Context;
|
|
|
+import android.content.Intent;
|
|
|
+import android.content.pm.PackageManager;
|
|
|
+import android.content.res.ColorStateList;
|
|
|
+import android.graphics.Color;
|
|
|
+import android.graphics.drawable.Drawable;
|
|
|
+import android.hardware.fingerprint.FingerprintManager;
|
|
|
+import android.os.Build;
|
|
|
+import android.os.Bundle;
|
|
|
+import android.os.CancellationSignal;
|
|
|
+import android.security.keystore.KeyGenParameterSpec;
|
|
|
+import android.security.keystore.KeyPermanentlyInvalidatedException;
|
|
|
+import android.security.keystore.KeyProperties;
|
|
|
+import android.support.v4.app.ActivityCompat;
|
|
|
+import android.support.v4.graphics.drawable.DrawableCompat;
|
|
|
+import android.support.v7.app.AppCompatActivity;
|
|
|
+import android.view.KeyEvent;
|
|
|
+import android.widget.ImageView;
|
|
|
+import android.widget.TextView;
|
|
|
+import android.widget.Toast;
|
|
|
+
|
|
|
+import com.owncloud.android.MainApp;
|
|
|
+import com.owncloud.android.R;
|
|
|
+import com.owncloud.android.lib.common.utils.Log_OC;
|
|
|
+
|
|
|
+import java.io.IOException;
|
|
|
+import java.security.InvalidAlgorithmParameterException;
|
|
|
+import java.security.InvalidKeyException;
|
|
|
+import java.security.KeyStore;
|
|
|
+import java.security.KeyStoreException;
|
|
|
+import java.security.NoSuchAlgorithmException;
|
|
|
+import java.security.NoSuchProviderException;
|
|
|
+import java.security.UnrecoverableKeyException;
|
|
|
+import java.security.cert.CertificateException;
|
|
|
+
|
|
|
+import javax.crypto.Cipher;
|
|
|
+import javax.crypto.KeyGenerator;
|
|
|
+import javax.crypto.NoSuchPaddingException;
|
|
|
+import javax.crypto.SecretKey;
|
|
|
+
|
|
|
+/**
|
|
|
+ * Activity to handle access to the app based on the fingerprint.
|
|
|
+ */
|
|
|
+public class FingerprintActivity extends AppCompatActivity {
|
|
|
+
|
|
|
+ private static final String TAG = FingerprintActivity.class.getSimpleName();
|
|
|
+
|
|
|
+ public final static String KEY_CHECK_RESULT = "KEY_CHECK_RESULT";
|
|
|
+
|
|
|
+ public final static String PREFERENCE_USE_FINGERPRINT = "use_fingerprint";
|
|
|
+ public static final String ANDROID_KEY_STORE = "AndroidKeyStore";
|
|
|
+
|
|
|
+ private KeyStore keyStore;
|
|
|
+ // Variable used for storing the key in the Android Keystore container
|
|
|
+ private static final String KEY_NAME = "Nextcloud";
|
|
|
+ private Cipher cipher;
|
|
|
+
|
|
|
+ private FingerprintHandler helper;
|
|
|
+
|
|
|
+ private FingerprintManager.CryptoObject cryptoObject;
|
|
|
+
|
|
|
+ private CancellationSignal cancellationSignal;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Initializes the activity.
|
|
|
+ *
|
|
|
+ * @param savedInstanceState Previously saved state - irrelevant in this case
|
|
|
+ */
|
|
|
+ protected void onCreate(Bundle savedInstanceState) {
|
|
|
+ super.onCreate(savedInstanceState);
|
|
|
+ setContentView(R.layout.fingerprintlock);
|
|
|
+ }
|
|
|
+
|
|
|
+ private void startFingerprint() {
|
|
|
+ TextView fingerprintTextView = (TextView) findViewById(R.id.scanfingerprinttext);
|
|
|
+
|
|
|
+ FingerprintManager fingerprintManager =
|
|
|
+ (FingerprintManager) MainApp.getAppContext().getSystemService(Context.FINGERPRINT_SERVICE);
|
|
|
+
|
|
|
+ if (ActivityCompat.checkSelfPermission(this, Manifest.permission.USE_FINGERPRINT)
|
|
|
+ != PackageManager.PERMISSION_GRANTED) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ KeyguardManager keyguardManager = (KeyguardManager) getSystemService(KEYGUARD_SERVICE);
|
|
|
+
|
|
|
+ if (!keyguardManager.isKeyguardSecure()) {
|
|
|
+ return;
|
|
|
+ } else {
|
|
|
+ generateKey();
|
|
|
+
|
|
|
+ if (cipherInit()) {
|
|
|
+ cryptoObject = new FingerprintManager.CryptoObject(cipher);
|
|
|
+ FingerprintHandler.Callback callback = new FingerprintHandler.Callback() {
|
|
|
+ @Override
|
|
|
+ public void onAuthenticated() {
|
|
|
+ fingerprintResult(true);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onFailed(String error) {
|
|
|
+ Toast.makeText(MainApp.getAppContext(), error, Toast.LENGTH_LONG).show();
|
|
|
+ ImageView imageView = (ImageView) findViewById(R.id.fingerprinticon);
|
|
|
+ int[][] states = new int[][]{
|
|
|
+ new int[]{android.R.attr.state_activated},
|
|
|
+ new int[]{-android.R.attr.state_activated}
|
|
|
+ };
|
|
|
+ int[] colors = new int[]{Color.parseColor("#FF0000"), Color.RED};
|
|
|
+ ColorStateList csl = new ColorStateList(states, colors);
|
|
|
+ Drawable drawable = DrawableCompat.wrap(imageView.getDrawable());
|
|
|
+ DrawableCompat.setTintList(drawable, csl);
|
|
|
+ imageView.setImageDrawable(drawable);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ helper = new FingerprintHandler(fingerprintTextView, callback);
|
|
|
+ cancellationSignal = new CancellationSignal();
|
|
|
+ if (ActivityCompat.checkSelfPermission(MainApp.getAppContext(), Manifest.permission.USE_FINGERPRINT)
|
|
|
+ != PackageManager.PERMISSION_GRANTED) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ fingerprintManager.authenticate(cryptoObject, cancellationSignal, 0, helper, null);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onResume(){
|
|
|
+ super.onResume();
|
|
|
+ startFingerprint();
|
|
|
+ ImageView imageView = (ImageView)findViewById(R.id.fingerprinticon);
|
|
|
+ imageView.setImageDrawable(getDrawable(R.drawable.ic_fingerprint));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onStop(){
|
|
|
+ super.onStop();
|
|
|
+ cancellationSignal.cancel();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Overrides click on the BACK arrow to prevent fingerprint from being worked around.
|
|
|
+ *
|
|
|
+ * @param keyCode Key code of the key that triggered the down event.
|
|
|
+ * @param event Event triggered.
|
|
|
+ * @return 'True' when the key event was processed by this method.
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public boolean onKeyDown(int keyCode, KeyEvent event){
|
|
|
+ if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount()== 0){
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ return super.onKeyDown(keyCode, event);
|
|
|
+ }
|
|
|
+
|
|
|
+ @TargetApi(Build.VERSION_CODES.M)
|
|
|
+ protected void generateKey() {
|
|
|
+ try {
|
|
|
+ keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
|
|
|
+ } catch (Exception e) {
|
|
|
+ Log_OC.e(TAG, "Error getting KeyStore", e);
|
|
|
+ }
|
|
|
+
|
|
|
+ KeyGenerator keyGenerator;
|
|
|
+ try {
|
|
|
+ keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
|
|
|
+ } catch (NoSuchAlgorithmException | NoSuchProviderException e) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ keyStore.load(null);
|
|
|
+ keyGenerator.init(
|
|
|
+ new KeyGenParameterSpec.Builder(
|
|
|
+ KEY_NAME, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT
|
|
|
+ )
|
|
|
+ .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
|
|
|
+ .setUserAuthenticationRequired(true)
|
|
|
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
|
|
|
+ .build());
|
|
|
+ keyGenerator.generateKey();
|
|
|
+ } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | CertificateException | IOException e) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @TargetApi(Build.VERSION_CODES.M)
|
|
|
+ public boolean cipherInit() {
|
|
|
+ try {
|
|
|
+ cipher = Cipher.getInstance(
|
|
|
+ KeyProperties.KEY_ALGORITHM_AES + "/"
|
|
|
+ + KeyProperties.BLOCK_MODE_CBC + "/"
|
|
|
+ + KeyProperties.ENCRYPTION_PADDING_PKCS7);
|
|
|
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ keyStore.load(null);
|
|
|
+ SecretKey key = (SecretKey) keyStore.getKey(KEY_NAME, null);
|
|
|
+ cipher.init(Cipher.ENCRYPT_MODE, key);
|
|
|
+ return true;
|
|
|
+ } catch (KeyPermanentlyInvalidatedException e) {
|
|
|
+ return false;
|
|
|
+ } catch (KeyStoreException
|
|
|
+ | CertificateException
|
|
|
+ | UnrecoverableKeyException
|
|
|
+ | IOException
|
|
|
+ | NoSuchAlgorithmException
|
|
|
+ | InvalidKeyException e) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void fingerprintResult(boolean fingerok) {
|
|
|
+ if (fingerok) {
|
|
|
+ Intent resultIntent = new Intent();
|
|
|
+ resultIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
|
|
|
+ resultIntent.putExtra(KEY_CHECK_RESULT, true);
|
|
|
+ setResult(RESULT_OK, resultIntent);
|
|
|
+ finish();
|
|
|
+ } else {
|
|
|
+ showErrorAndRestart(R.string.fingerprint_unknown);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void showErrorAndRestart(int errorMessage) {
|
|
|
+ CharSequence errorSeq = getString(errorMessage);
|
|
|
+ Toast.makeText(this, errorSeq, Toast.LENGTH_LONG).show();
|
|
|
+ }
|
|
|
+
|
|
|
+ final static public boolean isFingerprintCapable(Context context) {
|
|
|
+ try {
|
|
|
+ FingerprintManager fingerprintManager =
|
|
|
+ (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
|
|
|
+
|
|
|
+ if (ActivityCompat.checkSelfPermission(context, Manifest.permission.USE_FINGERPRINT)
|
|
|
+ != PackageManager.PERMISSION_GRANTED) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
|
+ return fingerprintManager.isHardwareDetected();
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ final static public boolean isFingerprintReady(Context context) {
|
|
|
+ try {
|
|
|
+ FingerprintManager fingerprintManager =
|
|
|
+ (FingerprintManager) context.getSystemService(Context.FINGERPRINT_SERVICE);
|
|
|
+
|
|
|
+ if (ActivityCompat.checkSelfPermission(context, Manifest.permission.USE_FINGERPRINT)
|
|
|
+ != PackageManager.PERMISSION_GRANTED) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
|
+ return fingerprintManager.isHardwareDetected() && fingerprintManager.hasEnrolledFingerprints();
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+@SuppressLint("NewApi")
|
|
|
+class FingerprintHandler extends FingerprintManager.AuthenticationCallback {
|
|
|
+
|
|
|
+ private TextView text;
|
|
|
+ private Callback callback;
|
|
|
+
|
|
|
+ // Constructor
|
|
|
+ FingerprintHandler(TextView mtext, Callback mcallback) {
|
|
|
+ text = mtext;
|
|
|
+ callback = mcallback;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onAuthenticationError(int errMsgId, CharSequence errString) {
|
|
|
+ // this.update(String.valueOf(errString), false);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onAuthenticationHelp(int helpMsgId, CharSequence helpString) {
|
|
|
+ this.update(String.valueOf(helpString), false);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onAuthenticationFailed() {
|
|
|
+ this.update(MainApp.getAppContext().getString(R.string.fingerprint_unknown), false);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void onAuthenticationSucceeded(FingerprintManager.AuthenticationResult result) {
|
|
|
+ this.update("Fingerprint Authentication succeeded.", true);
|
|
|
+ }
|
|
|
+
|
|
|
+ public void update(final String e, Boolean success) {
|
|
|
+ if(success) {
|
|
|
+ text.postDelayed(new Runnable() {
|
|
|
+ @Override
|
|
|
+ public void run() {
|
|
|
+ callback.onAuthenticated();
|
|
|
+ }
|
|
|
+ }, 0);
|
|
|
+ } else {
|
|
|
+ text.postDelayed(new Runnable() {
|
|
|
+ @Override
|
|
|
+ public void run() {
|
|
|
+ callback.onFailed(e);
|
|
|
+ }
|
|
|
+ }, 0);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ interface Callback {
|
|
|
+ void onAuthenticated();
|
|
|
+ void onFailed(String error);
|
|
|
+ }
|
|
|
+}
|