1 /* <lambda>null2 * Copyright (C) 2023 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.settings.biometrics2.ui.viewmodel 17 18 import android.app.Activity 19 import android.app.Application 20 import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED 21 import android.content.Context 22 import android.content.Intent 23 import android.os.Bundle 24 import android.util.Log 25 import androidx.activity.result.ActivityResult 26 import androidx.lifecycle.AndroidViewModel 27 import com.android.internal.widget.LockPatternUtils 28 import com.android.settings.biometrics.BiometricEnrollBase 29 import com.android.settings.biometrics.BiometricUtils 30 import com.android.settings.biometrics.BiometricUtils.GatekeeperCredentialNotMatchException 31 import com.android.settings.biometrics2.data.repository.FingerprintRepository 32 import com.android.settings.biometrics2.ui.model.CredentialModel 33 import com.android.settings.password.ChooseLockGeneric 34 import com.android.settings.password.ChooseLockPattern 35 import com.android.settings.password.ChooseLockSettingsHelper 36 import kotlinx.coroutines.CoroutineScope 37 import kotlinx.coroutines.flow.MutableSharedFlow 38 import kotlinx.coroutines.flow.SharedFlow 39 import kotlinx.coroutines.flow.asSharedFlow 40 import kotlinx.coroutines.launch 41 42 /** 43 * AutoCredentialViewModel which uses CredentialModel to determine next actions for activity, like 44 * start ChooseLockActivity, start ConfirmLockActivity, GenerateCredential, or do nothing. 45 */ 46 class AutoCredentialViewModel( 47 application: Application, 48 private val lockPatternUtils: LockPatternUtils, 49 private val challengeGenerator: ChallengeGenerator, 50 private val credentialModel: CredentialModel 51 ) : AndroidViewModel(application) { 52 53 /** 54 * Generic callback for FingerprintManager#generateChallenge or FaceManager#generateChallenge 55 */ 56 interface GenerateChallengeCallback { 57 /** Generic generateChallenge method for FingerprintManager or FaceManager */ 58 fun onChallengeGenerated(sensorId: Int, userId: Int, challenge: Long) 59 } 60 61 /** 62 * A generic interface class for calling different generateChallenge from FingerprintManager or 63 * FaceManager 64 */ 65 interface ChallengeGenerator { 66 67 /** Callback that will be called later after challenge generated */ 68 var callback: GenerateChallengeCallback? 69 70 /** Method for generating challenge from FingerprintManager or FaceManager */ 71 fun generateChallenge(userId: Int) 72 } 73 74 /** Used to generate challenge through FingerprintRepository */ 75 class FingerprintChallengeGenerator( 76 private val fingerprintRepository: FingerprintRepository 77 ) : ChallengeGenerator { 78 79 override var callback: GenerateChallengeCallback? = null 80 81 override fun generateChallenge(userId: Int) { 82 callback?.let { 83 fingerprintRepository.generateChallenge(userId) { 84 sensorId: Int, uid: Int, challenge: Long -> 85 it.onChallengeGenerated(sensorId, uid, challenge) 86 } 87 } ?:run { 88 Log.e(TAG, "generateChallenge, null callback") 89 } 90 } 91 92 companion object { 93 private const val TAG = "FingerprintChallengeGenerator" 94 } 95 } 96 97 private val _generateChallengeFailedFlow = MutableSharedFlow<Boolean>() 98 val generateChallengeFailedFlow: SharedFlow<Boolean> 99 get() = _generateChallengeFailedFlow.asSharedFlow() 100 101 102 // flag if token is generating through checkCredential()'s generateChallenge() 103 private var isGeneratingChallengeDuringCheckingCredential = false 104 105 /** Get bundle which passing back to FingerprintSettings for late generateChallenge() */ 106 fun createGeneratingChallengeExtras(): Bundle? { 107 if (!isGeneratingChallengeDuringCheckingCredential 108 || !credentialModel.isValidToken 109 || !credentialModel.isValidChallenge 110 ) { 111 return null 112 } 113 val bundle = Bundle() 114 bundle.putByteArray( 115 ChooseLockSettingsHelper.EXTRA_KEY_CHALLENGE_TOKEN, 116 credentialModel.token 117 ) 118 bundle.putLong(BiometricEnrollBase.EXTRA_KEY_CHALLENGE, credentialModel.challenge) 119 return bundle 120 } 121 122 /** Check credential status for biometric enrollment. */ 123 fun checkCredential(scope: CoroutineScope): CredentialAction { 124 return if (isValidCredential) { 125 CredentialAction.CREDENTIAL_VALID 126 } else if (isUnspecifiedPassword) { 127 CredentialAction.FAIL_NEED_TO_CHOOSE_LOCK 128 } else if (credentialModel.isValidGkPwHandle) { 129 val gkPwHandle = credentialModel.gkPwHandle 130 credentialModel.clearGkPwHandle() 131 // GkPwHandle is got through caller activity, we shall not revoke it after 132 // generateChallenge(). Let caller activity to make decision. 133 generateChallenge(gkPwHandle, false, scope) 134 isGeneratingChallengeDuringCheckingCredential = true 135 CredentialAction.IS_GENERATING_CHALLENGE 136 } else { 137 CredentialAction.FAIL_NEED_TO_CONFIRM_LOCK 138 } 139 } 140 141 private fun generateChallenge( 142 gkPwHandle: Long, 143 revokeGkPwHandle: Boolean, 144 scope: CoroutineScope 145 ) { 146 challengeGenerator.callback = object : GenerateChallengeCallback { 147 override fun onChallengeGenerated(sensorId: Int, userId: Int, challenge: Long) { 148 var illegalStateExceptionCaught = false 149 try { 150 val newToken = requestGatekeeperHat(gkPwHandle, challenge, userId) 151 credentialModel.challenge = challenge 152 credentialModel.token = newToken 153 } catch (e: IllegalStateException) { 154 Log.e(TAG, "generateChallenge, IllegalStateException", e) 155 illegalStateExceptionCaught = true 156 } finally { 157 if (revokeGkPwHandle) { 158 lockPatternUtils.removeGatekeeperPasswordHandle(gkPwHandle) 159 } 160 Log.d( 161 TAG, 162 "generateChallenge(), model:$credentialModel" 163 + ", revokeGkPwHandle:$revokeGkPwHandle" 164 ) 165 // Check credential again 166 if (!isValidCredential || illegalStateExceptionCaught) { 167 Log.w(TAG, "generateChallenge, invalid Credential or IllegalStateException") 168 scope.launch { 169 _generateChallengeFailedFlow.emit(true) 170 } 171 } 172 } 173 } 174 } 175 challengeGenerator.generateChallenge(userId) 176 } 177 178 private val isValidCredential: Boolean 179 get() = !isUnspecifiedPassword && credentialModel.isValidToken 180 181 private val isUnspecifiedPassword: Boolean 182 get() = lockPatternUtils.getActivePasswordQuality(userId) == PASSWORD_QUALITY_UNSPECIFIED 183 184 /** 185 * Handle activity result from ChooseLockGeneric, ConfirmLockPassword, or ConfirmLockPattern 186 * @param isChooseLock true if result is coming from ChooseLockGeneric. False if result is 187 * coming from ConfirmLockPassword or ConfirmLockPattern 188 * @param result activity result 189 * @return if it is a valid result and viewModel is generating challenge 190 */ 191 fun generateChallengeAsCredentialActivityResult( 192 isChooseLock: Boolean, 193 result: ActivityResult, 194 scope: CoroutineScope 195 ): Boolean { 196 if ((isChooseLock && result.resultCode == ChooseLockPattern.RESULT_FINISHED) || 197 (!isChooseLock && result.resultCode == Activity.RESULT_OK)) { 198 result.data?.let { 199 val gkPwHandle = it.getLongExtra( 200 ChooseLockSettingsHelper.EXTRA_KEY_GK_PW_HANDLE, 201 CredentialModel.INVALID_GK_PW_HANDLE 202 ) 203 // Revoke self requested GkPwHandle because it shall only used once inside this 204 // activity lifecycle. 205 generateChallenge(gkPwHandle, true, scope) 206 return true 207 } 208 } 209 return false 210 } 211 212 val userId: Int 213 get() = credentialModel.userId 214 215 val token: ByteArray? 216 get() = credentialModel.token 217 218 @Throws(IllegalStateException::class) 219 private fun requestGatekeeperHat(gkPwHandle: Long, challenge: Long, userId: Int): ByteArray? { 220 val response = lockPatternUtils 221 .verifyGatekeeperPasswordHandle(gkPwHandle, challenge, userId) 222 if (!response.isMatched) { 223 throw GatekeeperCredentialNotMatchException("Unable to request Gatekeeper HAT") 224 } 225 return response.gatekeeperHAT 226 } 227 228 /** Create Intent for choosing lock */ 229 fun createChooseLockIntent( 230 context: Context, isSuw: Boolean, 231 suwExtras: Bundle 232 ): Intent { 233 val intent = BiometricUtils.getChooseLockIntent( 234 context, isSuw, 235 suwExtras 236 ) 237 intent.putExtra( 238 ChooseLockGeneric.ChooseLockGenericFragment.HIDE_INSECURE_OPTIONS, 239 true 240 ) 241 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_REQUEST_GK_PW_HANDLE, true) 242 intent.putExtra(ChooseLockSettingsHelper.EXTRA_KEY_FOR_FINGERPRINT, true) 243 if (credentialModel.isValidUserId) { 244 intent.putExtra(Intent.EXTRA_USER_ID, credentialModel.userId) 245 } 246 return intent 247 } 248 249 /** Create ConfirmLockLauncher */ 250 fun createConfirmLockLauncher( 251 activity: Activity, 252 requestCode: Int, title: String 253 ): ChooseLockSettingsHelper { 254 val builder = ChooseLockSettingsHelper.Builder(activity) 255 builder.setRequestCode(requestCode) 256 .setTitle(title) 257 .setRequestGatekeeperPasswordHandle(true) 258 .setForegroundOnly(true) 259 .setReturnCredentials(true) 260 if (credentialModel.isValidUserId) { 261 builder.setUserId(credentialModel.userId) 262 } 263 return builder.build() 264 } 265 266 companion object { 267 private const val TAG = "AutoCredentialViewModel" 268 } 269 } 270 271 enum class CredentialAction { 272 273 CREDENTIAL_VALID, 274 275 /** Valid credential, activity does nothing. */ 276 IS_GENERATING_CHALLENGE, 277 278 /** This credential looks good, but still need to run generateChallenge(). */ 279 FAIL_NEED_TO_CHOOSE_LOCK, 280 281 /** Need activity to run confirm lock */ 282 FAIL_NEED_TO_CONFIRM_LOCK 283 } 284