1 /* 2 * 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 17 package com.android.systemui.biometrics.domain.interactor 18 19 import android.hardware.biometrics.PromptInfo 20 import com.android.internal.widget.LockPatternView 21 import com.android.internal.widget.LockscreenCredential 22 import com.android.systemui.biometrics.Utils 23 import com.android.systemui.biometrics.data.repository.PromptRepository 24 import com.android.systemui.biometrics.domain.model.BiometricOperationInfo 25 import com.android.systemui.biometrics.domain.model.BiometricPromptRequest 26 import com.android.systemui.biometrics.shared.model.BiometricUserInfo 27 import com.android.systemui.biometrics.shared.model.PromptKind 28 import com.android.systemui.dagger.qualifiers.Background 29 import javax.inject.Inject 30 import kotlinx.coroutines.CoroutineDispatcher 31 import kotlinx.coroutines.flow.Flow 32 import kotlinx.coroutines.flow.MutableStateFlow 33 import kotlinx.coroutines.flow.asStateFlow 34 import kotlinx.coroutines.flow.combine 35 import kotlinx.coroutines.flow.distinctUntilChanged 36 import kotlinx.coroutines.flow.lastOrNull 37 import kotlinx.coroutines.flow.onEach 38 import kotlinx.coroutines.withContext 39 40 /** 41 * Business logic for BiometricPrompt's CredentialViews, which primarily includes checking a users 42 * PIN, pattern, or password credential instead of a biometric. 43 * 44 * This is used to cache the calling app's options that were given to the underlying authenticate 45 * APIs and should be set before any UI is shown to the user. 46 * 47 * There can be at most one request active at a given time. Use [resetPrompt] when no request is 48 * active to clear the cache. 49 * 50 * Views that use any biometric should use [PromptSelectorInteractor] instead. 51 */ 52 class PromptCredentialInteractor 53 @Inject 54 constructor( 55 @Background private val bgDispatcher: CoroutineDispatcher, 56 private val biometricPromptRepository: PromptRepository, 57 private val credentialInteractor: CredentialInteractor, 58 ) { 59 /** If the prompt is currently showing. */ 60 val isShowing: Flow<Boolean> = biometricPromptRepository.isShowing 61 62 /** Metadata about the current credential prompt, including app-supplied preferences. */ 63 val prompt: Flow<BiometricPromptRequest.Credential?> = 64 combine( 65 biometricPromptRepository.promptInfo, 66 biometricPromptRepository.challenge, 67 biometricPromptRepository.userId, 68 biometricPromptRepository.kind 69 ) { promptInfo, challenge, userId, kind -> 70 if (promptInfo == null || userId == null || challenge == null) { 71 return@combine null 72 } 73 74 when (kind) { 75 PromptKind.Pin -> 76 BiometricPromptRequest.Credential.Pin( 77 info = promptInfo, 78 userInfo = userInfo(userId), 79 operationInfo = operationInfo(challenge) 80 ) 81 PromptKind.Pattern -> 82 BiometricPromptRequest.Credential.Pattern( 83 info = promptInfo, 84 userInfo = userInfo(userId), 85 operationInfo = operationInfo(challenge), 86 stealthMode = credentialInteractor.isStealthModeActive(userId) 87 ) 88 PromptKind.Password -> 89 BiometricPromptRequest.Credential.Password( 90 info = promptInfo, 91 userInfo = userInfo(userId), 92 operationInfo = operationInfo(challenge) 93 ) 94 else -> null 95 } 96 } 97 .distinctUntilChanged() 98 99 private fun userInfo(userId: Int): BiometricUserInfo = 100 BiometricUserInfo( 101 userId = userId, 102 deviceCredentialOwnerId = credentialInteractor.getCredentialOwnerOrSelfId(userId) 103 ) 104 105 private fun operationInfo(challenge: Long): BiometricOperationInfo = 106 BiometricOperationInfo(gatekeeperChallenge = challenge) 107 108 /** Most recent error due to [verifyCredential]. */ 109 private val _verificationError = MutableStateFlow<CredentialStatus.Fail?>(null) 110 val verificationError: Flow<CredentialStatus.Fail?> = _verificationError.asStateFlow() 111 112 /** Update the current request to use credential-based authentication instead of biometrics. */ 113 fun useCredentialsForAuthentication( 114 promptInfo: PromptInfo, 115 @Utils.CredentialType kind: Int, 116 userId: Int, 117 challenge: Long, 118 ) { 119 biometricPromptRepository.setPrompt( 120 promptInfo, 121 userId, 122 challenge, 123 kind.asBiometricPromptCredential() 124 ) 125 } 126 127 /** Unset the current authentication request. */ 128 fun resetPrompt() { 129 biometricPromptRepository.unsetPrompt() 130 } 131 132 /** 133 * Check a credential and return the attestation token (HAT) if successful. 134 * 135 * This method will not return if credential checks are being throttled until the throttling has 136 * expired and the user can try again. It will periodically update the [verificationError] until 137 * cancelled or the throttling has completed. If the request is not throttled, but unsuccessful, 138 * the [verificationError] will be set and an optional 139 * [CredentialStatus.Fail.Error.urgentMessage] message may be provided to indicate additional 140 * hints to the user (i.e. device will be wiped on next failure, etc.). 141 * 142 * The check happens on the background dispatcher given in the constructor. 143 */ 144 suspend fun checkCredential( 145 request: BiometricPromptRequest.Credential, 146 text: CharSequence? = null, 147 pattern: List<LockPatternView.Cell>? = null, 148 ): CredentialStatus = 149 withContext(bgDispatcher) { 150 val credential = 151 when (request) { 152 is BiometricPromptRequest.Credential.Pin -> 153 LockscreenCredential.createPinOrNone(text ?: "") 154 is BiometricPromptRequest.Credential.Password -> 155 LockscreenCredential.createPasswordOrNone(text ?: "") 156 is BiometricPromptRequest.Credential.Pattern -> 157 LockscreenCredential.createPattern(pattern ?: listOf()) 158 } 159 160 credential.use { c -> verifyCredential(request, c) } 161 } 162 163 private suspend fun verifyCredential( 164 request: BiometricPromptRequest.Credential, 165 credential: LockscreenCredential? 166 ): CredentialStatus { 167 if (credential == null || credential.isNone) { 168 return CredentialStatus.Fail.Error() 169 } 170 171 val finalStatus = 172 credentialInteractor 173 .verifyCredential(request, credential) 174 .onEach { status -> 175 when (status) { 176 is CredentialStatus.Success -> _verificationError.value = null 177 is CredentialStatus.Fail -> _verificationError.value = status 178 } 179 } 180 .lastOrNull() 181 182 return finalStatus ?: CredentialStatus.Fail.Error() 183 } 184 185 /** 186 * Report a user-visible error. 187 * 188 * Use this instead of calling [verifyCredential] when it is not necessary because the check 189 * will obviously fail (i.e. too short, empty, etc.) 190 */ 191 fun setVerificationError(error: CredentialStatus.Fail.Error?) { 192 if (error != null) { 193 _verificationError.value = error 194 } else { 195 resetVerificationError() 196 } 197 } 198 199 /** Clear the current error message, if any. */ 200 fun resetVerificationError() { 201 _verificationError.value = null 202 } 203 } 204 205 // TODO(b/251476085): remove along with Utils.CredentialType 206 /** Convert a [Utils.CredentialType] to the corresponding [PromptKind]. */ 207 private fun @receiver:Utils.CredentialType Int.asBiometricPromptCredential(): PromptKind = 208 when (this) { 209 Utils.CREDENTIAL_PIN -> PromptKind.Pin 210 Utils.CREDENTIAL_PASSWORD -> PromptKind.Password 211 Utils.CREDENTIAL_PATTERN -> PromptKind.Pattern 212 else -> PromptKind.Biometric() 213 } 214