/* * Copyright (C) 2022 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.settings.biometrics2.ui.viewmodel; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; import static com.android.settings.biometrics.BiometricEnrollBase.EXTRA_KEY_CHALLENGE; import static com.android.settings.biometrics2.ui.model.CredentialModel.INVALID_GK_PW_HANDLE; import static com.android.settings.password.ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE; import android.annotation.IntDef; import android.app.Activity; import android.app.Application; import android.content.Context; import android.content.Intent; import android.os.Bundle; import android.os.SystemClock; import android.util.Log; import androidx.activity.result.ActivityResult; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import androidx.lifecycle.AndroidViewModel; import androidx.lifecycle.LiveData; import androidx.lifecycle.MutableLiveData; import com.android.internal.widget.LockPatternUtils; import com.android.internal.widget.VerifyCredentialResponse; import com.android.settings.biometrics.BiometricUtils; import com.android.settings.biometrics.BiometricUtils.GatekeeperCredentialNotMatchException; import com.android.settings.biometrics2.data.repository.FingerprintRepository; import com.android.settings.biometrics2.ui.model.CredentialModel; import com.android.settings.password.ChooseLockGeneric; import com.android.settings.password.ChooseLockPattern; import com.android.settings.password.ChooseLockSettingsHelper; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; /** * AutoCredentialViewModel which uses CredentialModel to determine next actions for activity, like * start ChooseLockActivity, start ConfirmLockActivity, GenerateCredential, or do nothing. */ public class AutoCredentialViewModel extends AndroidViewModel { private static final String TAG = "AutoCredentialViewModel"; @VisibleForTesting static final String KEY_CREDENTIAL_MODEL = "credential_model"; @VisibleForTesting static final String KEY_IS_GENERATING_CHALLENGE_DURING_CHECKING_CREDENTIAL = "is_generating_challenge_during_checking_credential"; private static final boolean DEBUG = false; /** * Valid credential, activity does nothing. */ public static final int CREDENTIAL_VALID = 0; /** * This credential looks good, but still need to run generateChallenge(). */ public static final int CREDENTIAL_IS_GENERATING_CHALLENGE = 1; /** * Need activity to run choose lock */ public static final int CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK = 2; /** * Need activity to run confirm lock */ public static final int CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK = 3; @IntDef(prefix = { "CREDENTIAL_" }, value = { CREDENTIAL_VALID, CREDENTIAL_IS_GENERATING_CHALLENGE, CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK, CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK }) @Retention(RetentionPolicy.SOURCE) public @interface CredentialAction {} /** * Generic callback for FingerprintManager#generateChallenge or FaceManager#generateChallenge */ public interface GenerateChallengeCallback { /** * Generic generateChallenge method for FingerprintManager or FaceManager */ void onChallengeGenerated(int sensorId, int userId, long challenge); } /** * A generic interface class for calling different generateChallenge from FingerprintManager or * FaceManager */ public interface ChallengeGenerator { /** * Get callback that will be called later after challenge generated */ @Nullable GenerateChallengeCallback getCallback(); /** * Set callback that will be called later after challenge generated */ void setCallback(@Nullable GenerateChallengeCallback callback); /** * Method for generating challenge from FingerprintManager or FaceManager */ void generateChallenge(int userId); } /** * Used to generate challenge through FingerprintRepository */ public static class FingerprintChallengeGenerator implements ChallengeGenerator { private static final String TAG = "FingerprintChallengeGenerator"; @NonNull private final FingerprintRepository mFingerprintRepository; @Nullable private GenerateChallengeCallback mCallback = null; public FingerprintChallengeGenerator(@NonNull FingerprintRepository fingerprintRepository) { mFingerprintRepository = fingerprintRepository; } @Nullable @Override public GenerateChallengeCallback getCallback() { return mCallback; } @Override public void setCallback(@Nullable GenerateChallengeCallback callback) { mCallback = callback; } @Override public void generateChallenge(int userId) { final GenerateChallengeCallback callback = mCallback; if (callback == null) { Log.e(TAG, "generateChallenge, null callback"); return; } mFingerprintRepository.generateChallenge(userId, callback::onChallengeGenerated); } } @NonNull private final LockPatternUtils mLockPatternUtils; @NonNull private final ChallengeGenerator mChallengeGenerator; private CredentialModel mCredentialModel = null; @NonNull private final MutableLiveData mGenerateChallengeFailedLiveData = new MutableLiveData<>(); // flag if token is generating through checkCredential()'s generateChallenge() private boolean mIsGeneratingChallengeDuringCheckingCredential; public AutoCredentialViewModel( @NonNull Application application, @NonNull LockPatternUtils lockPatternUtils, @NonNull ChallengeGenerator challengeGenerator) { super(application); mLockPatternUtils = lockPatternUtils; mChallengeGenerator = challengeGenerator; } /** * Set CredentialModel, the source is coming from savedInstanceState or activity intent */ public void setCredentialModel(@Nullable Bundle savedInstanceState, @NonNull Intent intent) { final Bundle bundle; if (savedInstanceState != null) { bundle = savedInstanceState.getBundle(KEY_CREDENTIAL_MODEL); mIsGeneratingChallengeDuringCheckingCredential = savedInstanceState.getBoolean( KEY_IS_GENERATING_CHALLENGE_DURING_CHECKING_CREDENTIAL); } else { bundle = intent.getExtras(); } mCredentialModel = new CredentialModel(bundle, SystemClock.elapsedRealtimeClock()); if (DEBUG) { Log.d(TAG, "setCredentialModel " + mCredentialModel + ", savedInstanceState exist:" + (savedInstanceState != null)); } } /** * Handle onSaveInstanceState from activity */ public void onSaveInstanceState(@NonNull Bundle outState) { outState.putBoolean(KEY_IS_GENERATING_CHALLENGE_DURING_CHECKING_CREDENTIAL, mIsGeneratingChallengeDuringCheckingCredential); outState.putBundle(KEY_CREDENTIAL_MODEL, mCredentialModel.getBundle()); } @NonNull public LiveData getGenerateChallengeFailedLiveData() { return mGenerateChallengeFailedLiveData; } /** * Get bundle which passing back to FingerprintSettings for late generateChallenge() */ @Nullable public Bundle createGeneratingChallengeExtras() { if (!mIsGeneratingChallengeDuringCheckingCredential || !mCredentialModel.isValidToken() || !mCredentialModel.isValidChallenge()) { return null; } Bundle bundle = new Bundle(); bundle.putByteArray(ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, mCredentialModel.getToken()); bundle.putLong(EXTRA_KEY_CHALLENGE, mCredentialModel.getChallenge()); return bundle; } /** * Check credential status for biometric enrollment. */ @CredentialAction public int checkCredential() { if (isValidCredential()) { return CREDENTIAL_VALID; } if (isUnspecifiedPassword()) { return CREDENTIAL_FAIL_NEED_TO_CHOOSE_LOCK; } else if (mCredentialModel.isValidGkPwHandle()) { final long gkPwHandle = mCredentialModel.getGkPwHandle(); mCredentialModel.clearGkPwHandle(); // GkPwHandle is got through caller activity, we shall not revoke it after // generateChallenge(). Let caller activity to make decision. generateChallenge(gkPwHandle, false /* revokeGkPwHandle */); mIsGeneratingChallengeDuringCheckingCredential = true; return CREDENTIAL_IS_GENERATING_CHALLENGE; } else { return CREDENTIAL_FAIL_NEED_TO_CONFIRM_LOCK; } } private void generateChallenge(long gkPwHandle, boolean revokeGkPwHandle) { mChallengeGenerator.setCallback((sensorId, userId, challenge) -> { try { final byte[] newToken = requestGatekeeperHat(gkPwHandle, challenge, userId); mCredentialModel.setChallenge(challenge); mCredentialModel.setToken(newToken); } catch (IllegalStateException e) { Log.e(TAG, "generateChallenge, IllegalStateException", e); mGenerateChallengeFailedLiveData.postValue(true); return; } if (revokeGkPwHandle) { mLockPatternUtils.removeGatekeeperPasswordHandle(gkPwHandle); } if (DEBUG) { Log.d(TAG, "generateChallenge(), model:" + mCredentialModel + ", revokeGkPwHandle:" + revokeGkPwHandle); } // Check credential again if (!isValidCredential()) { Log.w(TAG, "generateChallenge, invalid Credential"); mGenerateChallengeFailedLiveData.postValue(true); } }); mChallengeGenerator.generateChallenge(getUserId()); } private boolean isValidCredential() { return !isUnspecifiedPassword() && mCredentialModel.isValidToken(); } private boolean isUnspecifiedPassword() { return mLockPatternUtils.getActivePasswordQuality(getUserId()) == PASSWORD_QUALITY_UNSPECIFIED; } /** * Handle activity result from ChooseLockGeneric, ConfirmLockPassword, or ConfirmLockPattern * @param isChooseLock true if result is coming from ChooseLockGeneric. False if result is * coming from ConfirmLockPassword or ConfirmLockPattern * @param result activity result * @return if it is a valid result */ public boolean checkNewCredentialFromActivityResult(boolean isChooseLock, @NonNull ActivityResult result) { if ((isChooseLock && result.getResultCode() == ChooseLockPattern.RESULT_FINISHED) || (!isChooseLock && result.getResultCode() == Activity.RESULT_OK)) { final Intent data = result.getData(); if (data != null) { final long gkPwHandle = result.getData().getLongExtra( EXTRA_KEY_GK_PW_HANDLE, INVALID_GK_PW_HANDLE); // Revoke self requested GkPwHandle because it shall only used once inside this // activity lifecycle. generateChallenge(gkPwHandle, true /* revokeGkPwHandle */); return true; } } return false; } /** * Get userId for this credential */ public int getUserId() { return mCredentialModel.getUserId(); } /** * Get userId for this credential */ @Nullable public byte[] getToken() { return mCredentialModel.getToken(); } @Nullable private byte[] requestGatekeeperHat(long gkPwHandle, long challenge, int userId) throws IllegalStateException { final VerifyCredentialResponse response = mLockPatternUtils .verifyGatekeeperPasswordHandle(gkPwHandle, challenge, userId); if (!response.isMatched()) { throw new GatekeeperCredentialNotMatchException("Unable to request Gatekeeper HAT"); } return response.getGatekeeperHAT(); } /** * Create Intent for choosing lock */ @NonNull public Intent createChooseLockIntent(@NonNull Context context, boolean isSuw, @NonNull Bundle suwExtras) { final Intent intent = BiometricUtils.getChooseLockIntent(context, isSuw, suwExtras); intent.putExtra(ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS, true); intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true); intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, true); if (mCredentialModel.isValidUserId()) { intent.putExtra(Intent.EXTRA_USER_ID, mCredentialModel.getUserId()); } return intent; } /** * Create ConfirmLockLauncher */ @NonNull public ChooseLockSettingsHelper createConfirmLockLauncher(@NonNull Activity activity, int requestCode, @NonNull String title) { final ChooseLockSettingsHelper.Builder builder = new ChooseLockSettingsHelper.Builder(activity); builder.setRequestCode(requestCode) .setTitle(title) .setRequestGatekeeperPasswordHandle(true) .setForegroundOnly(true) .setReturnCredentials(true); if (mCredentialModel.isValidUserId()) { builder.setUserId(mCredentialModel.getUserId()); } return builder.build(); } }