content: /*
   * Copyright (C) 2018 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 android.content.Context;
  import android.hardware.biometrics.BiometricAuthenticator;
  import android.hardware.biometrics.BiometricConstants;
  import android.hardware.biometrics.BiometricsProtoEnums;
  import android.os.IBinder;
  import android.os.RemoteException;
  import android.security.KeyStore;
  import android.util.Slog;
  
  import java.util.ArrayList;
  
  /**
   * A class to keep track of the authentication state for a given client.
   */
  public abstract class AuthenticationClient extends ClientMonitor {
      private long mOpId;
  
      public abstract int handleFailedAttempt();
      public void resetFailedAttempts() {}
  
      public static final int LOCKOUT_NONE = 0;
      public static final int LOCKOUT_TIMED = 1;
      public static final int LOCKOUT_PERMANENT = 2;
  
      private final boolean mRequireConfirmation;
  
      // We need to track this state since it's possible for applications to request for
      // authentication while the device is already locked out. In that case, the client is created
      // but not started yet. The user shouldn't receive the error haptics in this case.
      private boolean mStarted;
      private long mStartTimeMs;
  
      /**
       * This method is called when authentication starts.
       */
      public abstract void onStart();
  
      /**
       * This method is called when a biometric is authenticated or authentication is stopped
       * (cancelled by the user, or an error such as lockout has occurred).
       */
      public abstract void onStop();
  
      /**
       * @return true if the framework should handle lockout.
       */
      public abstract boolean shouldFrameworkHandleLockout();
  
      public abstract boolean wasUserDetected();
  
      public AuthenticationClient(Context context, Constants constants,
              BiometricServiceBase.DaemonWrapper daemon, long halDeviceId, IBinder token,
              BiometricServiceBase.ServiceListener listener, int targetUserId, int groupId, long opId,
              boolean restricted, String owner, int cookie, boolean requireConfirmation) {
          super(context, constants, daemon, halDeviceId, token, listener, targetUserId, groupId,
                  restricted, owner, cookie);
          mOpId = opId;
          mRequireConfirmation = requireConfirmation;
      }
  
      protected long getStartTimeMs() {
          return mStartTimeMs;
      }
  
      @Override
      public void binderDied() {
          super.binderDied();
          // When the binder dies, we should stop the client. This probably belongs in
          // ClientMonitor's binderDied(), but testing all the cases would be tricky.
          // AuthenticationClient is the most user-visible case.
          stop(false /* initiatedByClient */);
      }
  
      @Override
      protected int statsAction() {
          return BiometricsProtoEnums.ACTION_AUTHENTICATE;
      }
  
      public boolean isBiometricPrompt() {
          return getCookie() != 0;
      }
  
      public boolean getRequireConfirmation() {
          return mRequireConfirmation;
      }
  
      @Override
      protected boolean isCryptoOperation() {
          return mOpId != 0;
      }
  
      @Override
      public boolean onError(long deviceId, int error, int vendorCode) {
          if (!shouldFrameworkHandleLockout()) {
              switch (error) {
                  case BiometricConstants.BIOMETRIC_ERROR_TIMEOUT:
                      if (!wasUserDetected() && !isBiometricPrompt()) {
                          // No vibration if user was not detected on keyguard
                          break;
                      }
                  case BiometricConstants.BIOMETRIC_ERROR_LOCKOUT:
                  case BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT:
                      if (mStarted) {
                          vibrateError();
                      }
                      break;
                  default:
                      break;
              }
          }
          return super.onError(deviceId, error, vendorCode);
      }
  
      @Override
      public boolean onAuthenticated(BiometricAuthenticator.Identifier identifier,
              boolean authenticated, ArrayList<Byte> token) {
          super.logOnAuthenticated(getContext(), authenticated, mRequireConfirmation,
                  getTargetUserId(), isBiometricPrompt());
  
          final BiometricServiceBase.ServiceListener listener = getListener();
  
          mMetricsLogger.action(mConstants.actionBiometricAuth(), authenticated);
          boolean result = false;
  
          try {
              if (DEBUG) Slog.v(getLogTag(), "onAuthenticated(" + authenticated + ")"
                      + ", ID:" + identifier.getBiometricId()
                      + ", Owner: " + getOwnerString()
                      + ", isBP: " + isBiometricPrompt()
                      + ", listener: " + listener
                      + ", requireConfirmation: " + mRequireConfirmation
                      + ", user: " + getTargetUserId());
  
              if (authenticated) {
                  mAlreadyDone = true;
  
                  if (listener != null) {
                      vibrateSuccess();
                  }
                  result = true;
                  if (shouldFrameworkHandleLockout()) {
                      resetFailedAttempts();
                  }
                  onStop();
  
                  final byte[] byteToken = new byte[token.size()];
                  for (int i = 0; i < token.size(); i++) {
                      byteToken[i] = token.get(i);
                  }
                  if (isBiometricPrompt() && listener != null) {
                      // BiometricService will add the token to keystore
                      listener.onAuthenticationSucceededInternal(mRequireConfirmation, byteToken);
                  } else if (!isBiometricPrompt() && listener != null) {
                      KeyStore.getInstance().addAuthToken(byteToken);
                      try {
                          // Explicitly have if/else here to make it super obvious in case the code is
                          // touched in the future.
                          if (!getIsRestricted()) {
                              listener.onAuthenticationSucceeded(
                                      getHalDeviceId(), identifier, getTargetUserId());
                          } else {
                              listener.onAuthenticationSucceeded(
                                      getHalDeviceId(), null, getTargetUserId());
                          }
                      } catch (RemoteException e) {
                          Slog.e(getLogTag(), "Remote exception", e);
                      }
                  } else {
                      // Client not listening
                      Slog.w(getLogTag(), "Client not listening");
                      result = true;
                  }
              } else {
                  if (listener != null) {
                      vibrateError();
                  }
  
                  // Allow system-defined limit of number of attempts before giving up
                  final int lockoutMode = handleFailedAttempt();
                  if (lockoutMode != LOCKOUT_NONE && shouldFrameworkHandleLockout()) {
                      Slog.w(getLogTag(), "Forcing lockout (driver code should do this!), mode("
                              + lockoutMode + ")");
                      stop(false);
                      final int errorCode = lockoutMode == LOCKOUT_TIMED
                              ? BiometricConstants.BIOMETRIC_ERROR_LOCKOUT
                              : BiometricConstants.BIOMETRIC_ERROR_LOCKOUT_PERMANENT;
                      onError(getHalDeviceId(), errorCode, 0 /* vendorCode */);
                  } else {
                      // Don't send onAuthenticationFailed if we're in lockout, it causes a
                      // janky UI on Keyguard/BiometricPrompt since "authentication failed"
                      // will show briefly and be replaced by "device locked out" message.
                      if (listener != null) {
                          if (isBiometricPrompt()) {
                              listener.onAuthenticationFailedInternal(getCookie(),
                                      getRequireConfirmation());
                          } else {
                              listener.onAuthenticationFailed(getHalDeviceId());
                          }
                      }
                  }
                  result = lockoutMode != LOCKOUT_NONE; // in a lockout mode
              }
          } catch (RemoteException e) {
              Slog.e(getLogTag(), "Remote exception", e);
              result = true;
          }
          return result;
      }
  
      /**
       * Start authentication
       */
      @Override
      public int start() {
          mStarted = true;
          onStart();
          try {
              mStartTimeMs = System.currentTimeMillis();
              final int result = getDaemonWrapper().authenticate(mOpId, getGroupId());
              if (result != 0) {
                  Slog.w(getLogTag(), "startAuthentication failed, result=" + result);
                  mMetricsLogger.histogram(mConstants.tagAuthStartError(), result);
                  onError(getHalDeviceId(), BiometricConstants.BIOMETRIC_ERROR_HW_UNAVAILABLE,
                          0 /* vendorCode */);
                  return result;
              }
              if (DEBUG) Slog.w(getLogTag(), "client " + getOwnerString() + " is authenticating...");
          } catch (RemoteException e) {
              Slog.e(getLogTag(), "startAuthentication failed", e);
              return ERROR_ESRCH;
          }
          return 0; // success
      }
  
      @Override
      public int stop(boolean initiatedByClient) {
          if (mAlreadyCancelled) {
              Slog.w(getLogTag(), "stopAuthentication: already cancelled!");
              return 0;
          }
  
          mStarted = false;
  
          onStop();
  
          try {
              final int result = getDaemonWrapper().cancel();
              if (result != 0) {
                  Slog.w(getLogTag(), "stopAuthentication failed, result=" + result);
                  return result;
              }
              if (DEBUG) Slog.w(getLogTag(), "client " + getOwnerString() +
                      " is no longer authenticating");
          } catch (RemoteException e) {
              Slog.e(getLogTag(), "stopAuthentication failed", e);
              return ERROR_ESRCH;
          }
  
          mAlreadyCancelled = true;
          return 0; // success
      }
  
      @Override
      public boolean onEnrollResult(BiometricAuthenticator.Identifier identifier,
              int remaining) {
          if (DEBUG) Slog.w(getLogTag(), "onEnrollResult() called for authenticate!");
          return true; // Invalid for Authenticate
      }
  
      @Override
      public boolean onRemoved(BiometricAuthenticator.Identifier identifier, int remaining) {
          if (DEBUG) Slog.w(getLogTag(), "onRemoved() called for authenticate!");
          return true; // Invalid for Authenticate
      }
  
      @Override
      public boolean onEnumerationResult(BiometricAuthenticator.Identifier identifier,
              int remaining) {
          if (DEBUG) Slog.w(getLogTag(), "onEnumerationResult() called for authenticate!");
          return true; // Invalid for Authenticate
      }
  }
  