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