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 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]. */
asBiometricPromptCredentialnull207 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