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