Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[local_auth] Fix getEnrolledBiometrics returning non-enrolled biometrics on Android. #5309

Merged
6 changes: 6 additions & 0 deletions packages/local_auth/local_auth_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 1.0.1

* Fixes `getEnrolledBiometrics` to match documented behaviour:
Present biometrics that are not enrolled are no longer returned.
* `getEnrolledBiometrics` now only returns `weak` and `strong` biometric types.

## 1.0.0

* Initial release from migration to federated architecture.
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
import android.app.KeyguardManager;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.hardware.fingerprint.FingerprintManager;
import android.os.Build;
import androidx.annotation.NonNull;
Expand Down Expand Up @@ -101,15 +100,18 @@ public void onMethodCall(MethodCall call, @NonNull final Result result) {
case "authenticate":
authenticate(call, result);
break;
case "getAvailableBiometrics":
getAvailableBiometrics(result);
case "getEnrolledBiometrics":
getEnrolledBiometrics(result);
break;
case "isDeviceSupported":
isDeviceSupported(result);
break;
case "stopAuthentication":
stopAuthentication(result);
break;
case "deviceSupportsBiometrics":
deviceSupportsBiometrics(result);
break;
default:
result.notImplemented();
break;
Expand Down Expand Up @@ -248,42 +250,44 @@ private void stopAuthentication(Result result) {
}
}

private void deviceSupportsBiometrics(final Result result) {
result.success(hasBiometricHardware());
}

/*
* Returns biometric types available on device
* Returns enrolled biometric types available on device.
*/
private void getAvailableBiometrics(final Result result) {
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 = getAvailableBiometrics();
ArrayList<String> biometrics = getEnrolledBiometrics();
result.success(biometrics);
} catch (Exception e) {
result.error("no_biometrics_available", e.getMessage(), null);
}
}

private ArrayList<String> getAvailableBiometrics() {
private ArrayList<String> getEnrolledBiometrics() {
ArrayList<String> biometrics = new ArrayList<>();
if (activity == null || activity.isFinishing()) {
return biometrics;
}
PackageManager packageManager = activity.getPackageManager();
if (Build.VERSION.SDK_INT >= 23) {
if (packageManager.hasSystemFeature(PackageManager.FEATURE_FINGERPRINT)) {
biometrics.add("fingerprint");
}
// If no hardware is present, or no biometrics are enrolled, an empty list is returned.
if (!canAuthenticateWithBiometrics()) {
return biometrics;
}
if (Build.VERSION.SDK_INT >= 29) {
if (packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) {
biometrics.add("face");
}
if (packageManager.hasSystemFeature(PackageManager.FEATURE_IRIS)) {
biometrics.add("iris");
}
// If there are biometrics enrolled, the available ones are returned.
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;
}

Expand Down Expand Up @@ -359,4 +363,9 @@ public void onDetachedFromActivity() {
final Activity getActivity() {
return activity;
}

@VisibleForTesting
void setBiometricManager(BiometricManager biometricManager) {
this.biometricManager = biometricManager;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import android.app.Activity;
import android.content.Context;
import androidx.biometric.BiometricManager;
import androidx.lifecycle.Lifecycle;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.embedding.engine.dart.DartExecutor;
Expand All @@ -20,6 +21,8 @@
import io.flutter.embedding.engine.plugins.lifecycle.HiddenLifecycleReference;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import java.util.ArrayList;
import java.util.Collections;
import org.junit.Test;

public class LocalAuthTest {
Expand Down Expand Up @@ -61,4 +64,122 @@ public void onDetachedFromActivity_ShouldReleaseActivity() {
plugin.onDetachedFromActivity();
assertNull(plugin.getActivity());
}

@Test
public void getEnrolledBiometrics_shouldReturnError_whenNoActivity() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);

plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
verify(mockResult)
.error("no_activity", "local_auth plugin requires a foreground activity", null);
}

@Test
public void getEnrolledBiometrics_shouldReturnError_whenFinishingActivity() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
final Activity mockActivity = buildMockActivity();
when(mockActivity.isFinishing()).thenReturn(true);
setPluginActivity(plugin, mockActivity);

plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
verify(mockResult)
.error("no_activity", "local_auth plugin requires a foreground activity", null);
}

@Test
public void getEnrolledBiometrics_shouldReturnEmptyList_withoutHardwarePresent() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
setPluginActivity(plugin, buildMockActivity());
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
when(mockBiometricManager.canAuthenticate())
.thenReturn(BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE);
plugin.setBiometricManager(mockBiometricManager);

plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
verify(mockResult).success(Collections.emptyList());
}

@Test
public void getEnrolledBiometrics_shouldReturnEmptyList_withNoMethodsEnrolled() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
setPluginActivity(plugin, buildMockActivity());
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
when(mockBiometricManager.canAuthenticate())
.thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED);
plugin.setBiometricManager(mockBiometricManager);

plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
verify(mockResult).success(Collections.emptyList());
}

@Test
public void getEnrolledBiometrics_shouldOnlyAddEnrolledBiometrics() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
setPluginActivity(plugin, buildMockActivity());
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK))
.thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG))
.thenReturn(BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED);
plugin.setBiometricManager(mockBiometricManager);

plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
verify(mockResult)
.success(
new ArrayList<String>() {
{
add("weak");
}
});
}

@Test
public void getEnrolledBiometrics_shouldAddStrongBiometrics() {
final LocalAuthPlugin plugin = new LocalAuthPlugin();
setPluginActivity(plugin, buildMockActivity());
final MethodChannel.Result mockResult = mock(MethodChannel.Result.class);
final BiometricManager mockBiometricManager = mock(BiometricManager.class);
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_WEAK))
.thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
when(mockBiometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG))
.thenReturn(BiometricManager.BIOMETRIC_SUCCESS);
plugin.setBiometricManager(mockBiometricManager);

plugin.onMethodCall(new MethodCall("getEnrolledBiometrics", null), mockResult);
verify(mockResult)
.success(
new ArrayList<String>() {
{
add("weak");
add("strong");
}
});
}

private Activity buildMockActivity() {
final Activity mockActivity = mock(Activity.class);
final Context mockContext = mock(Context.class);
when(mockActivity.getBaseContext()).thenReturn(mockContext);
when(mockActivity.getApplicationContext()).thenReturn(mockContext);
return mockActivity;
}

private void setPluginActivity(LocalAuthPlugin plugin, Activity activity) {
final HiddenLifecycleReference mockLifecycleReference = mock(HiddenLifecycleReference.class);
final FlutterPluginBinding mockPluginBinding = mock(FlutterPluginBinding.class);
final ActivityPluginBinding mockActivityBinding = mock(ActivityPluginBinding.class);
final FlutterEngine mockFlutterEngine = mock(FlutterEngine.class);
final DartExecutor mockDartExecutor = mock(DartExecutor.class);
when(mockPluginBinding.getFlutterEngine()).thenReturn(mockFlutterEngine);
when(mockFlutterEngine.getDartExecutor()).thenReturn(mockDartExecutor);
when(mockActivityBinding.getActivity()).thenReturn(activity);
when(mockActivityBinding.getLifecycle()).thenReturn(mockLifecycleReference);
plugin.onAttachedToEngine(mockPluginBinding);
plugin.onAttachedToActivity(mockActivityBinding);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,28 +49,24 @@ class LocalAuthAndroid extends LocalAuthPlatform {

@override
Future<bool> deviceSupportsBiometrics() async {
return (await getEnrolledBiometrics()).isNotEmpty;
return (await _channel.invokeMethod<bool>('deviceSupportsBiometrics')) ??
false;
}

@override
Future<List<BiometricType>> getEnrolledBiometrics() async {
final List<String> result = (await _channel.invokeListMethod<String>(
'getAvailableBiometrics',
'getEnrolledBiometrics',
)) ??
<String>[];
final List<BiometricType> biometrics = <BiometricType>[];
for (final String value in result) {
switch (value) {
case 'face':
biometrics.add(BiometricType.face);
case 'weak':
biometrics.add(BiometricType.weak);
break;
case 'fingerprint':
biometrics.add(BiometricType.fingerprint);
break;
case 'iris':
biometrics.add(BiometricType.iris);
break;
case 'undefined':
case 'strong':
biometrics.add(BiometricType.strong);
break;
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/local_auth/local_auth_android/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: local_auth_android
description: Android implementation of the local_auth plugin.
repository: https://github.com/flutter/plugins/tree/master/packages/local_auth/local_auth_android
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+local_auth%22
version: 1.0.0
version: 1.0.1

environment:
sdk: ">=2.14.0 <3.0.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,8 @@ void main() {
channel.setMockMethodCallHandler((MethodCall methodCall) {
log.add(methodCall);
switch (methodCall.method) {
case 'getAvailableBiometrics':
return Future<List<String>>.value(
<String>['face', 'fingerprint', 'iris', 'undefined']);
case 'getEnrolledBiometrics':
return Future<List<String>>.value(<String>['weak', 'strong']);
default:
return Future<dynamic>.value(true);
}
Expand All @@ -35,13 +34,13 @@ void main() {
log.clear();
});

test('deviceSupportsBiometrics calls getEnrolledBiometrics', () async {
test('deviceSupportsBiometrics calls platform', () async {
final bool result = await localAuthentication.deviceSupportsBiometrics();

expect(
log,
<Matcher>[
isMethodCall('getAvailableBiometrics', arguments: null),
isMethodCall('deviceSupportsBiometrics', arguments: null),
],
);
expect(result, true);
Expand All @@ -54,13 +53,12 @@ void main() {
expect(
log,
<Matcher>[
isMethodCall('getAvailableBiometrics', arguments: null),
isMethodCall('getEnrolledBiometrics', arguments: null),
],
);
expect(result, <BiometricType>[
BiometricType.face,
BiometricType.fingerprint,
BiometricType.iris
BiometricType.weak,
BiometricType.strong,
]);
});

Expand Down