Make face auth ineligible when camera access is disabled

Test: Disable camera access in Settings, Open biometric prompt test app
and try to authenticate; Fingerprint authentication should work normally
Bug: 277854521

Change-Id: I19556fa7f861f2f7655e792b1da9d74d3cf2259e
This commit is contained in:
Diya Bera
2023-06-06 10:47:52 -07:00
parent eb09020592
commit c35a712ab4
6 changed files with 233 additions and 26 deletions

View File

@@ -0,0 +1,25 @@
/*
* Copyright (C) 2023 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.android.server.biometrics;
/**
* Interface for biometric operations to get camera privacy state.
*/
public interface BiometricSensorPrivacy {
/* Returns true if privacy is enabled and camera access is disabled. */
boolean isCameraPrivacyEnabled();
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright (C) 2023 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.android.server.biometrics;
import static android.hardware.SensorPrivacyManager.Sensors.CAMERA;
import android.annotation.Nullable;
import android.hardware.SensorPrivacyManager;
public class BiometricSensorPrivacyImpl implements
BiometricSensorPrivacy {
private final SensorPrivacyManager mSensorPrivacyManager;
public BiometricSensorPrivacyImpl(@Nullable SensorPrivacyManager sensorPrivacyManager) {
mSensorPrivacyManager = sensorPrivacyManager;
}
@Override
public boolean isCameraPrivacyEnabled() {
return mSensorPrivacyManager != null && mSensorPrivacyManager
.isSensorPrivacyEnabled(SensorPrivacyManager.TOGGLE_TYPE_SOFTWARE, CAMERA);
}
}

View File

@@ -33,6 +33,7 @@ import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.UserInfo;
import android.database.ContentObserver;
import android.hardware.SensorPrivacyManager;
import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricPrompt;
@@ -124,6 +125,8 @@ public class BiometricService extends SystemService {
AuthSession mAuthSession;
private final Handler mHandler = new Handler(Looper.getMainLooper());
private final BiometricSensorPrivacy mBiometricSensorPrivacy;
/**
* Tracks authenticatorId invalidation. For more details, see
* {@link com.android.server.biometrics.sensors.InvalidationRequesterClient}.
@@ -933,7 +936,7 @@ public class BiometricService extends SystemService {
return PreAuthInfo.create(mTrustManager, mDevicePolicyManager, mSettingObserver, mSensors,
userId, promptInfo, opPackageName, false /* checkDevicePolicyManager */,
getContext());
getContext(), mBiometricSensorPrivacy);
}
/**
@@ -1026,6 +1029,11 @@ public class BiometricService extends SystemService {
public UserManager getUserManager(Context context) {
return context.getSystemService(UserManager.class);
}
public BiometricSensorPrivacy getBiometricSensorPrivacy(Context context) {
return new BiometricSensorPrivacyImpl(context.getSystemService(
SensorPrivacyManager.class));
}
}
/**
@@ -1054,6 +1062,7 @@ public class BiometricService extends SystemService {
mRequestCounter = mInjector.getRequestGenerator();
mBiometricContext = injector.getBiometricContext(context);
mUserManager = injector.getUserManager(context);
mBiometricSensorPrivacy = injector.getBiometricSensorPrivacy(context);
try {
injector.getActivityManagerService().registerUserSwitchObserver(
@@ -1290,7 +1299,7 @@ public class BiometricService extends SystemService {
final PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager,
mDevicePolicyManager, mSettingObserver, mSensors, userId, promptInfo,
opPackageName, promptInfo.isDisallowBiometricsIfPolicyExists(),
getContext());
getContext(), mBiometricSensorPrivacy);
final Pair<Integer, Integer> preAuthStatus = preAuthInfo.getPreAuthenticateStatus();
@@ -1300,9 +1309,7 @@ public class BiometricService extends SystemService {
+ promptInfo.isIgnoreEnrollmentState());
// BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED is added so that BiometricPrompt can
// be shown for this case.
if (preAuthStatus.second == BiometricConstants.BIOMETRIC_SUCCESS
|| preAuthStatus.second
== BiometricConstants.BIOMETRIC_ERROR_SENSOR_PRIVACY_ENABLED) {
if (preAuthStatus.second == BiometricConstants.BIOMETRIC_SUCCESS) {
// If BIOMETRIC_WEAK or BIOMETRIC_STRONG are allowed, but not enrolled, but
// CREDENTIAL is requested and available, set the bundle to only request
// CREDENTIAL.

View File

@@ -27,7 +27,6 @@ import android.annotation.NonNull;
import android.app.admin.DevicePolicyManager;
import android.app.trust.ITrustManager;
import android.content.Context;
import android.hardware.SensorPrivacyManager;
import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.PromptInfo;
@@ -73,13 +72,16 @@ class PreAuthInfo {
final Context context;
private final boolean mBiometricRequested;
private final int mBiometricStrengthRequested;
private final BiometricSensorPrivacy mBiometricSensorPrivacy;
private PreAuthInfo(boolean biometricRequested, int biometricStrengthRequested,
boolean credentialRequested, List<BiometricSensor> eligibleSensors,
List<Pair<BiometricSensor, Integer>> ineligibleSensors, boolean credentialAvailable,
boolean confirmationRequested, boolean ignoreEnrollmentState, int userId,
Context context) {
Context context, BiometricSensorPrivacy biometricSensorPrivacy) {
mBiometricRequested = biometricRequested;
mBiometricStrengthRequested = biometricStrengthRequested;
mBiometricSensorPrivacy = biometricSensorPrivacy;
this.credentialRequested = credentialRequested;
this.eligibleSensors = eligibleSensors;
@@ -96,7 +98,8 @@ class PreAuthInfo {
BiometricService.SettingObserver settingObserver,
List<BiometricSensor> sensors,
int userId, PromptInfo promptInfo, String opPackageName,
boolean checkDevicePolicyManager, Context context)
boolean checkDevicePolicyManager, Context context,
BiometricSensorPrivacy biometricSensorPrivacy)
throws RemoteException {
final boolean confirmationRequested = promptInfo.isConfirmationRequested();
@@ -124,7 +127,7 @@ class PreAuthInfo {
checkDevicePolicyManager, requestedStrength,
promptInfo.getAllowedSensorIds(),
promptInfo.isIgnoreEnrollmentState(),
context);
biometricSensorPrivacy);
Slog.d(TAG, "Package: " + opPackageName
+ " Sensor ID: " + sensor.id
@@ -138,7 +141,7 @@ class PreAuthInfo {
//
// Note: if only a certain sensor is required and the privacy is enabled,
// canAuthenticate() will return false.
if (status == AUTHENTICATOR_OK || status == BIOMETRIC_SENSOR_PRIVACY_ENABLED) {
if (status == AUTHENTICATOR_OK) {
eligibleSensors.add(sensor);
} else {
ineligibleSensors.add(new Pair<>(sensor, status));
@@ -148,7 +151,7 @@ class PreAuthInfo {
return new PreAuthInfo(biometricRequested, requestedStrength, credentialRequested,
eligibleSensors, ineligibleSensors, credentialAvailable, confirmationRequested,
promptInfo.isIgnoreEnrollmentState(), userId, context);
promptInfo.isIgnoreEnrollmentState(), userId, context, biometricSensorPrivacy);
}
/**
@@ -165,7 +168,7 @@ class PreAuthInfo {
BiometricSensor sensor, int userId, String opPackageName,
boolean checkDevicePolicyManager, int requestedStrength,
@NonNull List<Integer> requestedSensorIds,
boolean ignoreEnrollmentState, Context context) {
boolean ignoreEnrollmentState, BiometricSensorPrivacy biometricSensorPrivacy) {
if (!requestedSensorIds.isEmpty() && !requestedSensorIds.contains(sensor.id)) {
return BIOMETRIC_NO_HARDWARE;
@@ -191,12 +194,10 @@ class PreAuthInfo {
&& !ignoreEnrollmentState) {
return BIOMETRIC_NOT_ENROLLED;
}
final SensorPrivacyManager sensorPrivacyManager = context
.getSystemService(SensorPrivacyManager.class);
if (sensorPrivacyManager != null && sensor.modality == TYPE_FACE) {
if (sensorPrivacyManager
.isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA, userId)) {
if (biometricSensorPrivacy != null && sensor.modality == TYPE_FACE) {
if (biometricSensorPrivacy.isCameraPrivacyEnabled()) {
//Camera privacy is enabled as the access is disabled
return BIOMETRIC_SENSOR_PRIVACY_ENABLED;
}
}
@@ -292,13 +293,9 @@ class PreAuthInfo {
@AuthenticatorStatus final int status;
@BiometricAuthenticator.Modality int modality = TYPE_NONE;
final SensorPrivacyManager sensorPrivacyManager = context
.getSystemService(SensorPrivacyManager.class);
boolean cameraPrivacyEnabled = false;
if (sensorPrivacyManager != null) {
cameraPrivacyEnabled = sensorPrivacyManager
.isSensorPrivacyEnabled(SensorPrivacyManager.Sensors.CAMERA, userId);
if (mBiometricSensorPrivacy != null) {
cameraPrivacyEnabled = mBiometricSensorPrivacy.isCameraPrivacyEnabled();
}
if (mBiometricRequested && credentialRequested) {
@@ -315,7 +312,7 @@ class PreAuthInfo {
// and the face sensor privacy is enabled then return
// BIOMETRIC_SENSOR_PRIVACY_ENABLED.
//
// Note: This sensor will still be eligible for calls to authenticate.
// Note: This sensor will not be eligible for calls to authenticate.
status = BIOMETRIC_SENSOR_PRIVACY_ENABLED;
} else {
status = AUTHENTICATOR_OK;
@@ -340,7 +337,7 @@ class PreAuthInfo {
// If the only modality requested is face and the privacy is enabled
// then return BIOMETRIC_SENSOR_PRIVACY_ENABLED.
//
// Note: This sensor will still be eligible for calls to authenticate.
// Note: This sensor will not be eligible for calls to authenticate.
status = BIOMETRIC_SENSOR_PRIVACY_ENABLED;
} else {
status = AUTHENTICATOR_OK;

View File

@@ -104,6 +104,7 @@ public class AuthSessionTest {
@Mock private KeyStore mKeyStore;
@Mock private AuthSession.ClientDeathReceiver mClientDeathReceiver;
@Mock private BiometricFrameworkStatsLogger mBiometricFrameworkStatsLogger;
@Mock BiometricSensorPrivacy mBiometricSensorPrivacy;
private Random mRandom;
private IBinder mToken;
@@ -571,7 +572,8 @@ public class AuthSessionTest {
promptInfo,
TEST_PACKAGE,
checkDevicePolicyManager,
mContext);
mContext,
mBiometricSensorPrivacy);
}
private AuthSession createAuthSession(List<BiometricSensor> sensors,

View File

@@ -0,0 +1,139 @@
/*
* Copyright (C) 2023 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.android.server.biometrics;
import static android.app.admin.DevicePolicyManager.KEYGUARD_DISABLE_FEATURES_NONE;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
import static com.android.server.biometrics.sensors.LockoutTracker.LOCKOUT_NONE;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.when;
import android.app.admin.DevicePolicyManager;
import android.app.trust.ITrustManager;
import android.content.Context;
import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.IBiometricAuthenticator;
import android.hardware.biometrics.PromptInfo;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.util.List;
@Presubmit
@SmallTest
public class PreAuthInfoTest {
@Rule
public final MockitoRule mMockitoRule = MockitoJUnit.rule();
private static final int SENSOR_ID_FACE = 1;
private static final String TEST_PACKAGE_NAME = "PreAuthInfoTestPackage";
@Mock
IBiometricAuthenticator mFaceAuthenticator;
@Mock
Context mContext;
@Mock
ITrustManager mTrustManager;
@Mock
DevicePolicyManager mDevicePolicyManager;
@Mock
BiometricService.SettingObserver mSettingObserver;
@Mock
BiometricSensorPrivacy mBiometricSensorPrivacyUtil;
@Before
public void setup() throws RemoteException {
when(mTrustManager.isDeviceSecure(anyInt(), anyInt())).thenReturn(true);
when(mDevicePolicyManager.getKeyguardDisabledFeatures(any(), anyInt()))
.thenReturn(KEYGUARD_DISABLE_FEATURES_NONE);
when(mSettingObserver.getEnabledForApps(anyInt())).thenReturn(true);
when(mFaceAuthenticator.hasEnrolledTemplates(anyInt(), any())).thenReturn(true);
when(mFaceAuthenticator.isHardwareDetected(any())).thenReturn(true);
when(mFaceAuthenticator.getLockoutModeForUser(anyInt()))
.thenReturn(LOCKOUT_NONE);
}
@Test
public void testFaceAuthentication_whenCameraPrivacyIsEnabled() throws Exception {
when(mBiometricSensorPrivacyUtil.isCameraPrivacyEnabled()).thenReturn(true);
BiometricSensor sensor = new BiometricSensor(mContext, SENSOR_ID_FACE, TYPE_FACE,
BiometricManager.Authenticators.BIOMETRIC_STRONG, mFaceAuthenticator) {
@Override
boolean confirmationAlwaysRequired(int userId) {
return false;
}
@Override
boolean confirmationSupported() {
return false;
}
};
PromptInfo promptInfo = new PromptInfo();
promptInfo.setConfirmationRequested(false /* requireConfirmation */);
promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG);
promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */);
PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
mSettingObserver, List.of(sensor),
0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
false /* checkDevicePolicyManager */, mContext, mBiometricSensorPrivacyUtil);
assertThat(preAuthInfo.eligibleSensors).isEmpty();
}
@Test
public void testFaceAuthentication_whenCameraPrivacyIsDisabled() throws Exception {
when(mBiometricSensorPrivacyUtil.isCameraPrivacyEnabled()).thenReturn(false);
BiometricSensor sensor = new BiometricSensor(mContext, SENSOR_ID_FACE, TYPE_FACE,
BiometricManager.Authenticators.BIOMETRIC_STRONG, mFaceAuthenticator) {
@Override
boolean confirmationAlwaysRequired(int userId) {
return false;
}
@Override
boolean confirmationSupported() {
return false;
}
};
PromptInfo promptInfo = new PromptInfo();
promptInfo.setConfirmationRequested(false /* requireConfirmation */);
promptInfo.setAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG);
promptInfo.setDisallowBiometricsIfPolicyExists(false /* checkDevicePolicy */);
PreAuthInfo preAuthInfo = PreAuthInfo.create(mTrustManager, mDevicePolicyManager,
mSettingObserver, List.of(sensor),
0 /* userId */, promptInfo, TEST_PACKAGE_NAME,
false /* checkDevicePolicyManager */, mContext, mBiometricSensorPrivacyUtil);
assertThat(preAuthInfo.eligibleSensors).hasSize(1);
}
}