blob: ea2bfa7d17db4d820a133829b9dfa31729bb7ce4 [file] [log] [blame]
/*
* Copyright (C) 2020 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.biometrics.BiometricAuthenticator.TYPE_FACE;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_NONE;
import android.annotation.IntDef;
import android.annotation.Nullable;
import android.hardware.biometrics.BiometricAuthenticator;
import android.hardware.biometrics.BiometricConstants;
import android.hardware.biometrics.BiometricManager;
import android.hardware.biometrics.BiometricPrompt;
import android.hardware.biometrics.IBiometricSensorReceiver;
import android.hardware.biometrics.IBiometricServiceReceiver;
import android.hardware.biometrics.IBiometricSysuiReceiver;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.security.KeyStore;
import android.util.Slog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.IStatusBarService;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Random;
/**
* Class that defines the states of an authentication session invoked via
* {@link android.hardware.biometrics.BiometricPrompt}, as well as all of the necessary
* state information for such a session.
*/
final class AuthSession {
private static final String TAG = "BiometricService/AuthSession";
/**
* Authentication either just called and we have not transitioned to the CALLED state, or
* authentication terminated (success or error).
*/
static final int STATE_AUTH_IDLE = 0;
/**
* Authentication was called and we are waiting for the <Biometric>Services to return their
* cookies before starting the hardware and showing the BiometricPrompt.
*/
static final int STATE_AUTH_CALLED = 1;
/**
* Authentication started, BiometricPrompt is showing and the hardware is authenticating.
*/
static final int STATE_AUTH_STARTED = 2;
/**
* Authentication is paused, waiting for the user to press "try again" button. Only
* passive modalities such as Face or Iris should have this state. Note that for passive
* modalities, the HAL enters the idle state after onAuthenticated(false) which differs from
* fingerprint.
*/
static final int STATE_AUTH_PAUSED = 3;
/**
* Paused, but "try again" was pressed. Sensors have new cookies and we're now waiting for all
* cookies to be returned.
*/
static final int STATE_AUTH_PAUSED_RESUMING = 4;
/**
* Authentication is successful, but we're waiting for the user to press "confirm" button.
*/
static final int STATE_AUTH_PENDING_CONFIRM = 5;
/**
* Biometric authenticated, waiting for SysUI to finish animation
*/
static final int STATE_AUTHENTICATED_PENDING_SYSUI = 6;
/**
* Biometric error, waiting for SysUI to finish animation
*/
static final int STATE_ERROR_PENDING_SYSUI = 7;
/**
* Device credential in AuthController is showing
*/
static final int STATE_SHOWING_DEVICE_CREDENTIAL = 8;
@IntDef({
STATE_AUTH_IDLE,
STATE_AUTH_CALLED,
STATE_AUTH_STARTED,
STATE_AUTH_PAUSED,
STATE_AUTH_PAUSED_RESUMING,
STATE_AUTH_PENDING_CONFIRM,
STATE_AUTHENTICATED_PENDING_SYSUI,
STATE_ERROR_PENDING_SYSUI,
STATE_SHOWING_DEVICE_CREDENTIAL})
@Retention(RetentionPolicy.SOURCE)
@interface SessionState {}
private final IStatusBarService mStatusBarService;
private final IBiometricSysuiReceiver mSysuiReceiver;
private final KeyStore mKeyStore;
private final Random mRandom;
final PreAuthInfo mPreAuthInfo;
// The following variables are passed to authenticateInternal, which initiates the
// appropriate <Biometric>Services.
@VisibleForTesting final IBinder mToken;
// Info to be shown on BiometricDialog when all cookies are returned.
@VisibleForTesting final Bundle mBundle;
private final long mOperationId;
private final int mUserId;
private final IBiometricSensorReceiver mSensorReceiver;
// Original receiver from BiometricPrompt.
private final IBiometricServiceReceiver mClientReceiver;
private final String mOpPackageName;
private final int mCallingUid;
private final int mCallingPid;
private final int mCallingUserId;
// The current state, which can be either idle, called, or started
private @SessionState int mState = STATE_AUTH_IDLE;
// For explicit confirmation, do not send to keystore until the user has confirmed
// the authentication.
private byte[] mTokenEscrow;
// Waiting for SystemUI to complete animation
private int mErrorEscrow;
private int mVendorCodeEscrow;
// Timestamp when authentication started
long mStartTimeMs;
// Timestamp when hardware authentication occurred
long mAuthenticatedTimeMs;
AuthSession(IStatusBarService statusBarService, IBiometricSysuiReceiver sysuiReceiver,
KeyStore keystore, Random random, PreAuthInfo preAuthInfo,
IBinder token, long operationId, int userId, IBiometricSensorReceiver sensorReceiver,
IBiometricServiceReceiver clientReceiver, String opPackageName, Bundle bundle,
int callingUid, int callingPid, int callingUserId) {
mStatusBarService = statusBarService;
mSysuiReceiver = sysuiReceiver;
mKeyStore = keystore;
mRandom = random;
mPreAuthInfo = preAuthInfo;
mToken = token;
mOperationId = operationId;
mUserId = userId;
mSensorReceiver = sensorReceiver;
mClientReceiver = clientReceiver;
mOpPackageName = opPackageName;
mBundle = bundle;
mCallingUid = callingUid;
mCallingPid = callingPid;
mCallingUserId = callingUserId;
setSensorsToStateUnknown();
}
/**
* @return bitmask representing the modalities that are running or could be running for the
* current session.
*/
private @BiometricAuthenticator.Modality int getEligibleModalities() {
return mPreAuthInfo.getEligibleModalities();
}
private void setSensorsToStateUnknown() {
// Generate random cookies to pass to the services that should prepare to start
// authenticating. Store the cookie here and wait for all services to "ack"
// with the cookie. Once all cookies are received, we can show the prompt
// and let the services start authenticating. The cookie should be non-zero.
for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) {
sensor.goToStateUnknown();
}
}
private void setSensorsToStateWaitingForCookie() throws RemoteException {
for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) {
final int cookie = mRandom.nextInt(Integer.MAX_VALUE - 1) + 1;
final boolean requireConfirmation = sensor.confirmationSupported()
&& (sensor.confirmationAlwaysRequired(mUserId)
|| mPreAuthInfo.confirmationRequested);
sensor.goToStateWaitingForCookie(requireConfirmation, mToken, mOperationId,
mUserId, mSensorReceiver, mOpPackageName, cookie, mCallingUid, mCallingPid,
mCallingUserId);
}
}
void goToInitialState() throws RemoteException {
if (mPreAuthInfo.credentialAvailable && mPreAuthInfo.eligibleSensors.isEmpty()) {
// Only device credential should be shown. In this case, we don't need to wait,
// since LockSettingsService/Gatekeeper is always ready to check for credential.
// SystemUI invokes that path.
mState = AuthSession.STATE_SHOWING_DEVICE_CREDENTIAL;
mStatusBarService.showAuthenticationDialog(
mBundle,
mSysuiReceiver,
0 /* biometricModality */,
false /* requireConfirmation */,
mUserId,
mOpPackageName,
mOperationId);
} else if (!mPreAuthInfo.eligibleSensors.isEmpty()) {
// Some combination of biometric or biometric|credential is requested
setSensorsToStateWaitingForCookie();
mState = AuthSession.STATE_AUTH_CALLED;
} else {
// No authenticators requested. This should never happen - an exception should have
// been thrown earlier in the pipeline.
throw new IllegalStateException("No authenticators requested");
}
}
void onCookieReceived(int cookie, boolean requireConfirmation) {
for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) {
sensor.goToStateCookieReturnedIfCookieMatches(cookie);
}
if (allCookiesReceived()) {
mStartTimeMs = System.currentTimeMillis();
startAllPreparedSensors();
// No need to request the UI if we're coming from the paused state
if (mState != STATE_AUTH_PAUSED_RESUMING) {
try {
final @BiometricAuthenticator.Modality int modality =
getEligibleModalities();
mStatusBarService.showAuthenticationDialog(mBundle,
mSysuiReceiver,
modality,
requireConfirmation,
mUserId,
mOpPackageName,
mOperationId);
} catch (RemoteException e) {
Slog.e(TAG, "Remote exception", e);
}
}
mState = STATE_AUTH_STARTED;
}
}
private void startAllPreparedSensors() {
for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) {
try {
sensor.startSensor();
} catch (RemoteException e) {
Slog.e(TAG, "Unable to start prepared client, sensor ID: "
+ sensor.id, e);
}
}
}
private void cancelAllSensors() {
// TODO: For multiple modalities, send a single ERROR_CANCELED only when all
// drivers have canceled authentication. We'd probably have to add a state for
// STATE_CANCELING for when we're waiting for final ERROR_CANCELED before
// sending the final error callback to the application.
for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) {
try {
sensor.goToStateCancelling(mToken, mOpPackageName, mCallingUid, mCallingPid,
mCallingUserId);
} catch (RemoteException e) {
Slog.e(TAG, "Unable to cancel authentication");
}
}
}
/**
* @return true if this AuthSession is finished, e.g. should be set to null.
*/
boolean onErrorReceived(int cookie, @BiometricAuthenticator.Modality int modality,
int error, int vendorCode) throws RemoteException {
if (!containsCookie(cookie)) {
Slog.e(TAG, "Unknown/expired cookie: " + cookie);
return false;
}
// TODO: The sensor-specific state is not currently used, this would need to be updated if
// multiple authenticators are running.
for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) {
if (sensor.getSensorState() == BiometricSensor.STATE_AUTHENTICATING) {
sensor.goToStoppedStateIfCookieMatches(cookie, error);
}
}
mErrorEscrow = error;
mVendorCodeEscrow = vendorCode;
switch (mState) {
case STATE_AUTH_CALLED: {
// If any error is received while preparing the auth session (lockout, etc),
// and if device credential is allowed, just show the credential UI.
if (isAllowDeviceCredential()) {
@BiometricManager.Authenticators.Types int authenticators = mBundle.getInt(
BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED, 0);
// Disallow biometric and notify SystemUI to show the authentication prompt.
authenticators = Utils.removeBiometricBits(authenticators);
mBundle.putInt(BiometricPrompt.KEY_AUTHENTICATORS_ALLOWED,
authenticators);
mState = AuthSession.STATE_SHOWING_DEVICE_CREDENTIAL;
mStatusBarService.showAuthenticationDialog(
mBundle,
mSysuiReceiver,
0 /* biometricModality */,
false /* requireConfirmation */,
mUserId,
mOpPackageName,
mOperationId);
} else {
mClientReceiver.onError(modality, error, vendorCode);
return true;
}
break;
}
case STATE_AUTH_STARTED: {
final boolean errorLockout = error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT
|| error == BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT;
if (isAllowDeviceCredential() && errorLockout) {
// SystemUI handles transition from biometric to device credential.
mState = AuthSession.STATE_SHOWING_DEVICE_CREDENTIAL;
mStatusBarService.onBiometricError(modality, error, vendorCode);
} else if (error == BiometricConstants.BIOMETRIC_ERROR_CANCELED) {
mStatusBarService.hideAuthenticationDialog();
// TODO: If multiple authenticators are simultaneously running, this will
// need to be modified. Send the error to the client here, instead of doing
// a round trip to SystemUI.
mClientReceiver.onError(modality, error, vendorCode);
return true;
} else {
mState = AuthSession.STATE_ERROR_PENDING_SYSUI;
mStatusBarService.onBiometricError(modality, error, vendorCode);
}
break;
}
case STATE_AUTH_PAUSED: {
// In the "try again" state, we should forward canceled errors to
// the client and clean up. The only error we should get here is
// ERROR_CANCELED due to another client kicking us out.
mClientReceiver.onError(modality, error, vendorCode);
mStatusBarService.hideAuthenticationDialog();
return true;
}
case STATE_SHOWING_DEVICE_CREDENTIAL:
Slog.d(TAG, "Biometric canceled, ignoring from state: " + mState);
break;
default:
Slog.e(TAG, "Unhandled error state, mState: " + mState);
break;
}
return false;
}
void onAcquired(int acquiredInfo, String message) {
if (message == null) {
Slog.w(TAG, "Ignoring null message: " + acquiredInfo);
return;
}
try {
mStatusBarService.onBiometricHelp(message);
} catch (RemoteException e) {
Slog.e(TAG, "Remote exception", e);
}
}
void onSystemEvent(int event) {
final boolean shouldReceive = mBundle
.getBoolean(BiometricPrompt.KEY_RECEIVE_SYSTEM_EVENTS, false);
if (!shouldReceive) {
return;
}
try {
mClientReceiver.onSystemEvent(event);
} catch (RemoteException e) {
Slog.e(TAG, "RemoteException", e);
}
}
void onTryAgainPressed() {
if (mState != STATE_AUTH_PAUSED) {
Slog.w(TAG, "onTryAgainPressed, state: " + mState);
}
try {
setSensorsToStateWaitingForCookie();
mState = STATE_AUTH_PAUSED_RESUMING;
} catch (RemoteException e) {
Slog.e(TAG, "RemoteException: " + e);
}
}
void onAuthenticationSucceeded(int sensorId, boolean requireConfirmation, boolean strong,
byte[] token) {
if (strong) {
mTokenEscrow = token;
} else {
if (token != null) {
Slog.w(TAG, "Dropping authToken for non-strong biometric, id: " + sensorId);
}
}
try {
// Notify SysUI that the biometric has been authenticated. SysUI already knows
// the implicit/explicit state and will react accordingly.
mStatusBarService.onBiometricAuthenticated();
if (!requireConfirmation) {
mState = STATE_AUTHENTICATED_PENDING_SYSUI;
} else {
mAuthenticatedTimeMs = System.currentTimeMillis();
mState = STATE_AUTH_PENDING_CONFIRM;
}
} catch (RemoteException e) {
Slog.e(TAG, "RemoteException", e);
}
}
void onAuthenticationRejected() {
try {
mStatusBarService.onBiometricError(TYPE_NONE,
BiometricConstants.BIOMETRIC_PAUSED_REJECTED, 0 /* vendorCode */);
// TODO: This logic will need to be updated if BP is multi-modal
if (hasPausableBiometric()) {
// Pause authentication. onBiometricAuthenticated(false) causes the
// dialog to show a "try again" button for passive modalities.
mState = AuthSession.STATE_AUTH_PAUSED;
}
mClientReceiver.onAuthenticationFailed();
} catch (RemoteException e) {
Slog.e(TAG, "RemoteException", e);
}
}
void onAuthenticationTimedOut(@BiometricAuthenticator.Modality int modality, int error,
int vendorCode) {
try {
mStatusBarService.onBiometricError(modality, error, vendorCode);
mState = STATE_AUTH_PAUSED;
} catch (RemoteException e) {
Slog.e(TAG, "RemoteException", e);
}
}
void onDeviceCredentialPressed() {
// Cancel authentication. Skip the token/package check since we are cancelling
// from system server. The interface is permission protected so this is fine.
cancelBiometricOnly();
mState = STATE_SHOWING_DEVICE_CREDENTIAL;
}
void onDialogDismissed(@BiometricPrompt.DismissedReason int reason,
@Nullable byte[] credentialAttestation) {
try {
switch (reason) {
case BiometricPrompt.DISMISSED_REASON_CREDENTIAL_CONFIRMED:
mKeyStore.addAuthToken(credentialAttestation);
case BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRMED:
case BiometricPrompt.DISMISSED_REASON_BIOMETRIC_CONFIRM_NOT_REQUIRED:
if (mTokenEscrow != null) {
mKeyStore.addAuthToken(mTokenEscrow);
}
mClientReceiver.onAuthenticationSucceeded(
Utils.getAuthenticationTypeForResult(reason));
break;
case BiometricPrompt.DISMISSED_REASON_NEGATIVE:
mClientReceiver.onDialogDismissed(reason);
cancelBiometricOnly();
break;
case BiometricPrompt.DISMISSED_REASON_USER_CANCEL:
mClientReceiver.onError(
getEligibleModalities(),
BiometricConstants.BIOMETRIC_ERROR_USER_CANCELED,
0 /* vendorCode */
);
cancelBiometricOnly();
break;
case BiometricPrompt.DISMISSED_REASON_SERVER_REQUESTED:
case BiometricPrompt.DISMISSED_REASON_ERROR:
mClientReceiver.onError(
getEligibleModalities(),
mErrorEscrow,
mVendorCodeEscrow
);
break;
default:
Slog.w(TAG, "Unhandled reason: " + reason);
break;
}
} catch (RemoteException e) {
Slog.e(TAG, "Remote exception", e);
}
}
/**
* Cancels authentication for the entire authentication session. The caller will receive
* {@link BiometricPrompt#BIOMETRIC_ERROR_CANCELED} at some point.
*
* @param force if true, will immediately dismiss the dialog and send onError to the client
* @return true if this AuthSession is finished, e.g. should be set to null
*/
boolean onCancelAuthSession(boolean force) {
if (mState == STATE_AUTH_STARTED && !force) {
cancelAllSensors();
// Wait for ERROR_CANCELED to be returned from the sensors
return false;
} else {
// If we're in a state where biometric sensors are not running (e.g. pending confirm,
// showing device credential, etc), we need to dismiss the dialog and send our own
// ERROR_CANCELED to the client, since we won't be getting an onError from the driver.
try {
// Send error to client
mClientReceiver.onError(
getEligibleModalities(),
BiometricConstants.BIOMETRIC_ERROR_CANCELED,
0 /* vendorCode */
);
mStatusBarService.hideAuthenticationDialog();
return true;
} catch (RemoteException e) {
Slog.e(TAG, "Remote exception", e);
}
}
return false;
}
/**
* Cancels biometric authentication only. AuthSession may either be going into
* {@link #STATE_SHOWING_DEVICE_CREDENTIAL} or dismissed.
*/
private void cancelBiometricOnly() {
if (mState == STATE_AUTH_STARTED) {
cancelAllSensors();
}
}
boolean isCrypto() {
return mOperationId != 0;
}
private boolean containsCookie(int cookie) {
for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) {
if (sensor.getCookie() == cookie) {
return true;
}
}
return false;
}
private boolean isAllowDeviceCredential() {
return Utils.isCredentialRequested(mBundle);
}
boolean allCookiesReceived() {
final int remainingCookies = mPreAuthInfo.numSensorsWaitingForCookie();
Slog.d(TAG, "Remaining cookies: " + remainingCookies);
return remainingCookies == 0;
}
private boolean hasPausableBiometric() {
for (BiometricSensor sensor : mPreAuthInfo.eligibleSensors) {
if (sensor.modality == TYPE_FACE) {
return true;
}
}
return false;
}
@SessionState int getState() {
return mState;
}
int getUserId() {
return mUserId;
}
@Override
public String toString() {
return "State: " + mState
+ "\nisCrypto: " + isCrypto()
+ "\nPreAuthInfo: " + mPreAuthInfo;
}
}