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 17 package com.android.systemui.biometrics.domain.interactor 18 19 import com.android.internal.widget.LockPatternView 20 import com.android.internal.widget.LockscreenCredential 21 import com.android.systemui.biometrics.data.repository.PromptRepository 22 import com.android.systemui.biometrics.domain.model.BiometricOperationInfo 23 import com.android.systemui.biometrics.domain.model.BiometricPromptRequest 24 import com.android.systemui.biometrics.shared.model.BiometricUserInfo 25 import com.android.systemui.biometrics.shared.model.PromptKind 26 import com.android.systemui.dagger.qualifiers.Background 27 import javax.inject.Inject 28 import kotlinx.coroutines.CoroutineDispatcher 29 import kotlinx.coroutines.flow.Flow 30 import kotlinx.coroutines.flow.MutableStateFlow 31 import kotlinx.coroutines.flow.asStateFlow 32 import kotlinx.coroutines.flow.combine 33 import kotlinx.coroutines.flow.distinctUntilChanged 34 import kotlinx.coroutines.flow.lastOrNull 35 import kotlinx.coroutines.flow.map 36 import kotlinx.coroutines.flow.onEach 37 import kotlinx.coroutines.withContext 38 39 /** 40 * Business logic for BiometricPrompt's CredentialViews, which primarily includes checking a users 41 * PIN, pattern, or password credential instead of a biometric. 42 * 43 * Views that use any biometric should use [PromptSelectorInteractor] instead. 44 */ 45 class PromptCredentialInteractor 46 @Inject 47 constructor( 48 @Background private val bgDispatcher: CoroutineDispatcher, 49 private val biometricPromptRepository: PromptRepository, 50 private val credentialInteractor: CredentialInteractor, 51 ) { 52 /** If the prompt is currently showing. */ 53 val isShowing: Flow<Boolean> = biometricPromptRepository.isShowing 54 55 /** 56 * If vertical list content view is shown, credential view should hide subtitle and content view 57 */ 58 val showTitleOnly: Flow<Boolean> = 59 biometricPromptRepository.promptInfo.map { promptInfo -> 60 promptInfo?.contentView != null && !promptInfo.isContentViewMoreOptionsButtonUsed 61 } 62 63 /** Metadata about the current credential prompt, including app-supplied preferences. */ 64 val prompt: Flow<BiometricPromptRequest.Credential?> = 65 combine( 66 biometricPromptRepository.promptInfo, 67 biometricPromptRepository.challenge, 68 biometricPromptRepository.userId, 69 biometricPromptRepository.promptKind 70 ) { promptInfo, challenge, userId, promptKind -> 71 if (promptInfo == null || userId == null || challenge == null) { 72 return@combine null 73 } 74 75 when (promptKind) { 76 PromptKind.Pin -> 77 BiometricPromptRequest.Credential.Pin( 78 info = promptInfo, 79 userInfo = 80 userInfo( 81 userId, 82 promptInfo.shouldUseParentProfileForDeviceCredential() 83 ), 84 operationInfo = operationInfo(challenge) 85 ) 86 PromptKind.Pattern -> 87 BiometricPromptRequest.Credential.Pattern( 88 info = promptInfo, 89 userInfo = 90 userInfo( 91 userId, 92 promptInfo.shouldUseParentProfileForDeviceCredential() 93 ), 94 operationInfo = operationInfo(challenge), 95 stealthMode = credentialInteractor.isStealthModeActive(userId) 96 ) 97 PromptKind.Password -> 98 BiometricPromptRequest.Credential.Password( 99 info = promptInfo, 100 userInfo = 101 userInfo( 102 userId, 103 promptInfo.shouldUseParentProfileForDeviceCredential() 104 ), 105 operationInfo = operationInfo(challenge) 106 ) 107 else -> null 108 } 109 } 110 .distinctUntilChanged() 111 112 private fun userInfo( 113 userId: Int, 114 useParentProfileForDeviceCredential: Boolean 115 ): BiometricUserInfo = 116 BiometricUserInfo( 117 userId = userId, 118 deviceCredentialOwnerId = credentialInteractor.getCredentialOwnerOrSelfId(userId), 119 userIdForPasswordEntry = 120 if (useParentProfileForDeviceCredential) 121 credentialInteractor.getParentProfileIdOrSelfId(userId) 122 else credentialInteractor.getCredentialOwnerOrSelfId(userId), 123 ) 124 125 private fun operationInfo(challenge: Long): BiometricOperationInfo = 126 BiometricOperationInfo(gatekeeperChallenge = challenge) 127 128 /** Most recent error due to [verifyCredential]. */ 129 private val _verificationError = MutableStateFlow<CredentialStatus.Fail?>(null) 130 val verificationError: Flow<CredentialStatus.Fail?> = _verificationError.asStateFlow() 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