blob: e545df01e7c0948feda76307f2e554add0633979 [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package io.flutter.plugins.localauth;
import static android.app.Activity.RESULT_OK;
import static android.content.Context.KEYGUARD_SERVICE;
import android.app.Activity;
import android.app.KeyguardManager;
import android.content.Context;
import android.content.Intent;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.biometric.BiometricManager;
import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.Lifecycle;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugin.common.PluginRegistry.Registrar;
import io.flutter.plugins.localauth.AuthenticationHelper.AuthCompletionHandler;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Flutter plugin providing access to local authentication.
*
* <p>Instantiate this in an add to app scenario to gracefully handle activity and context changes.
*/
@SuppressWarnings("deprecation")
public class LocalAuthPlugin implements MethodCallHandler, FlutterPlugin, ActivityAware {
private static final String CHANNEL_NAME = "plugins.flutter.io/local_auth_android";
private static final int LOCK_REQUEST_CODE = 221;
private Activity activity;
private AuthenticationHelper authHelper;
@VisibleForTesting final AtomicBoolean authInProgress = new AtomicBoolean(false);
// These are null when not using v2 embedding.
private MethodChannel channel;
private Lifecycle lifecycle;
private BiometricManager biometricManager;
private KeyguardManager keyguardManager;
private Result lockRequestResult;
private final PluginRegistry.ActivityResultListener resultListener =
new PluginRegistry.ActivityResultListener() {
@Override
public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == LOCK_REQUEST_CODE) {
if (resultCode == RESULT_OK && lockRequestResult != null) {
authenticateSuccess(lockRequestResult);
} else {
authenticateFail(lockRequestResult);
}
lockRequestResult = null;
}
return false;
}
};
/**
* Registers a plugin with the v1 embedding api {@code io.flutter.plugin.common}.
*
* <p>Calling this will register the plugin with the passed registrar. However, plugins
* initialized this way won't react to changes in activity or context.
*
* @param registrar attaches this plugin's {@link
* io.flutter.plugin.common.MethodChannel.MethodCallHandler} to the registrar's {@link
* io.flutter.plugin.common.BinaryMessenger}.
*/
@SuppressWarnings("deprecation")
public static void registerWith(Registrar registrar) {
final MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL_NAME);
final LocalAuthPlugin plugin = new LocalAuthPlugin();
plugin.activity = registrar.activity();
channel.setMethodCallHandler(plugin);
registrar.addActivityResultListener(plugin.resultListener);
}
/**
* Default constructor for LocalAuthPlugin.
*
* <p>Use this constructor when adding this plugin to an app with v2 embedding.
*/
public LocalAuthPlugin() {}
@Override
public void onMethodCall(MethodCall call, @NonNull final Result result) {
switch (call.method) {
case "authenticate":
authenticate(call, result);
break;
case "getEnrolledBiometrics":
getEnrolledBiometrics(result);
break;
case "isDeviceSupported":
isDeviceSupported(result);
break;
case "stopAuthentication":
stopAuthentication(result);
break;
case "deviceSupportsBiometrics":
deviceSupportsBiometrics(result);
break;
default:
result.notImplemented();
break;
}
}
/*
* Starts authentication process
*/
private void authenticate(MethodCall call, final Result result) {
if (authInProgress.get()) {
result.error("auth_in_progress", "Authentication in progress", null);
return;
}
if (activity == null || activity.isFinishing()) {
result.error("no_activity", "local_auth plugin requires a foreground activity", null);
return;
}
if (!(activity instanceof FragmentActivity)) {
result.error(
"no_fragment_activity",
"local_auth plugin requires activity to be a FragmentActivity.",
null);
return;
}
if (!isDeviceSupported()) {
authInProgress.set(false);
result.error("NotAvailable", "Required security features not enabled", null);
return;
}
authInProgress.set(true);
AuthCompletionHandler completionHandler = createAuthCompletionHandler(result);
boolean isBiometricOnly = call.argument("biometricOnly");
boolean allowCredentials = !isBiometricOnly && canAuthenticateWithDeviceCredential();
sendAuthenticationRequest(call, completionHandler, allowCredentials);
return;
}
@VisibleForTesting
public AuthCompletionHandler createAuthCompletionHandler(final Result result) {
return new AuthCompletionHandler() {
@Override
public void onSuccess() {
authenticateSuccess(result);
}
@Override
public void onFailure() {
authenticateFail(result);
}
@Override
public void onError(String code, String error) {
if (authInProgress.compareAndSet(true, false)) {
result.error(code, error, null);
}
}
};
}
@VisibleForTesting
public void sendAuthenticationRequest(
MethodCall call, AuthCompletionHandler completionHandler, boolean allowCredentials) {
authHelper =
new AuthenticationHelper(
lifecycle, (FragmentActivity) activity, call, completionHandler, allowCredentials);
authHelper.authenticate();
}
private void authenticateSuccess(Result result) {
if (authInProgress.compareAndSet(true, false)) {
result.success(true);
}
}
private void authenticateFail(Result result) {
if (authInProgress.compareAndSet(true, false)) {
result.success(false);
}
}
/*
* Stops the authentication if in progress.
*/
private void stopAuthentication(Result result) {
try {
if (authHelper != null && authInProgress.get()) {
authHelper.stopAuthentication();
authHelper = null;
}
authInProgress.set(false);
result.success(true);
} catch (Exception e) {
result.success(false);
}
}
private void deviceSupportsBiometrics(final Result result) {
result.success(hasBiometricHardware());
}
/*
* Returns enrolled biometric types available on device.
*/
private void getEnrolledBiometrics(final Result result) {
try {
if (activity == null || activity.isFinishing()) {
result.error("no_activity", "local_auth plugin requires a foreground activity", null);
return;
}
ArrayList<String> biometrics = getEnrolledBiometrics();
result.success(biometrics);
} catch (Exception e) {
result.error("no_biometrics_available", e.getMessage(), null);
}
}
@VisibleForTesting
public ArrayList<String> getEnrolledBiometrics() {
ArrayList<String> biometrics = new ArrayList<>();
if (activity == null || activity.isFinishing()) {
return biometrics;
}
if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
== BiometricManager.BIOMETRIC_SUCCESS) {
biometrics.add("weak");
}
if (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)
== BiometricManager.BIOMETRIC_SUCCESS) {
biometrics.add("strong");
}
return biometrics;
}
@VisibleForTesting
public boolean isDeviceSecure() {
if (keyguardManager == null) return false;
return (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && keyguardManager.isDeviceSecure());
}
@VisibleForTesting
public boolean isDeviceSupported() {
return isDeviceSecure() || canAuthenticateWithBiometrics();
}
private boolean canAuthenticateWithBiometrics() {
if (biometricManager == null) return false;
return biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
== BiometricManager.BIOMETRIC_SUCCESS;
}
private boolean hasBiometricHardware() {
if (biometricManager == null) return false;
return biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK)
!= BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE;
}
@VisibleForTesting
public boolean canAuthenticateWithDeviceCredential() {
if (Build.VERSION.SDK_INT < 30) {
// Checking for device credential only authentication via the BiometricManager
// is not allowed before API level 30, so we check for presence of PIN, pattern,
// or password instead.
return isDeviceSecure();
}
if (biometricManager == null) return false;
return biometricManager.canAuthenticate(BiometricManager.Authenticators.DEVICE_CREDENTIAL)
== BiometricManager.BIOMETRIC_SUCCESS;
}
private void isDeviceSupported(Result result) {
result.success(isDeviceSupported());
}
@Override
public void onAttachedToEngine(FlutterPluginBinding binding) {
channel = new MethodChannel(binding.getFlutterEngine().getDartExecutor(), CHANNEL_NAME);
channel.setMethodCallHandler(this);
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) {}
private void setServicesFromActivity(Activity activity) {
if (activity == null) return;
this.activity = activity;
Context context = activity.getBaseContext();
biometricManager = BiometricManager.from(activity);
keyguardManager = (KeyguardManager) context.getSystemService(KEYGUARD_SERVICE);
}
@Override
public void onAttachedToActivity(ActivityPluginBinding binding) {
binding.addActivityResultListener(resultListener);
setServicesFromActivity(binding.getActivity());
lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding);
channel.setMethodCallHandler(this);
}
@Override
public void onDetachedFromActivityForConfigChanges() {
lifecycle = null;
activity = null;
}
@Override
public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) {
binding.addActivityResultListener(resultListener);
setServicesFromActivity(binding.getActivity());
lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(binding);
}
@Override
public void onDetachedFromActivity() {
lifecycle = null;
channel.setMethodCallHandler(null);
activity = null;
}
@VisibleForTesting
final Activity getActivity() {
return activity;
}
@VisibleForTesting
void setBiometricManager(BiometricManager biometricManager) {
this.biometricManager = biometricManager;
}
@VisibleForTesting
void setKeyguardManager(KeyguardManager keyguardManager) {
this.keyguardManager = keyguardManager;
}
}