1 package com.android.systemui.biometrics.domain.interactor
2 
3 import android.app.admin.DevicePolicyManager
4 import android.app.admin.DevicePolicyResources
5 import android.content.Context
6 import android.os.UserManager
7 import com.android.internal.widget.LockPatternUtils
8 import com.android.internal.widget.LockscreenCredential
9 import com.android.internal.widget.VerifyCredentialResponse
10 import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
11 import com.android.systemui.dagger.qualifiers.Application
12 import com.android.systemui.res.R
13 import com.android.systemui.util.time.SystemClock
14 import javax.inject.Inject
15 import kotlinx.coroutines.delay
16 import kotlinx.coroutines.flow.Flow
17 import kotlinx.coroutines.flow.flow
18 
19 /**
20  * A wrapper for [LockPatternUtils] to verify PIN, pattern, or password credentials.
21  *
22  * This class also uses the [DevicePolicyManager] to generate appropriate error messages when policy
23  * exceptions are raised (i.e. wipe device due to excessive failed attempts, etc.).
24  */
25 interface CredentialInteractor {
26     /** If the user's pattern credential should be hidden */
isStealthModeActivenull27     fun isStealthModeActive(userId: Int): Boolean
28 
29     /** Get the effective user id (profile owner, if one exists) */
30     fun getCredentialOwnerOrSelfId(userId: Int): Int
31 
32     /** Get parent user profile (if exists) */
33     fun getParentProfileIdOrSelfId(userId: Int): Int
34 
35     /**
36      * Verifies a credential and returns a stream of results.
37      *
38      * The final emitted value will either be a [CredentialStatus.Fail.Error] or a
39      * [CredentialStatus.Success.Verified].
40      */
41     fun verifyCredential(
42         request: BiometricPromptRequest.Credential,
43         credential: LockscreenCredential,
44     ): Flow<CredentialStatus>
45 }
46 
47 /** Standard implementation of [CredentialInteractor]. */
48 class CredentialInteractorImpl
49 @Inject
50 constructor(
51     @Application private val applicationContext: Context,
52     private val lockPatternUtils: LockPatternUtils,
53     private val userManager: UserManager,
54     private val devicePolicyManager: DevicePolicyManager,
55     private val systemClock: SystemClock,
56 ) : CredentialInteractor {
57 
58     override fun isStealthModeActive(userId: Int): Boolean =
59         !lockPatternUtils.isVisiblePatternEnabled(userId)
60 
61     override fun getCredentialOwnerOrSelfId(userId: Int): Int =
62         userManager.getCredentialOwnerProfile(userId)
63 
64     override fun getParentProfileIdOrSelfId(userId: Int): Int =
65         userManager.getProfileParent(userId)?.id ?: userManager.getCredentialOwnerProfile(userId)
66 
67     override fun verifyCredential(
68         request: BiometricPromptRequest.Credential,
69         credential: LockscreenCredential,
70     ): Flow<CredentialStatus> = flow {
71         // Request LockSettingsService to return the Gatekeeper Password in the
72         // VerifyCredentialResponse so that we can request a Gatekeeper HAT with the
73         // Gatekeeper Password and operationId.
74         val effectiveUserId = request.userInfo.deviceCredentialOwnerId
75         val response =
76             lockPatternUtils.verifyCredential(
77                 credential,
78                 effectiveUserId,
79                 LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE
80             )
81 
82         if (response.isMatched) {
83             lockPatternUtils.userPresent(effectiveUserId)
84 
85             // The response passed into this method contains the Gatekeeper
86             // Password. We still have to request Gatekeeper to create a
87             // Hardware Auth Token with the Gatekeeper Password and Challenge
88             // (keystore operationId in this case)
89             val pwHandle = response.gatekeeperPasswordHandle
90             val gkResponse: VerifyCredentialResponse =
91                 lockPatternUtils.verifyGatekeeperPasswordHandle(
92                     pwHandle,
93                     request.operationInfo.gatekeeperChallenge,
94                     effectiveUserId
95                 )
96             val hat = gkResponse.gatekeeperHAT
97             lockPatternUtils.removeGatekeeperPasswordHandle(pwHandle)
98             emit(CredentialStatus.Success.Verified(checkNotNull(hat)))
99         } else if (response.timeout > 0) {
100             // if requests are being throttled, update the error message every
101             // second until the temporary lock has expired
102             val deadline: Long =
103                 lockPatternUtils.setLockoutAttemptDeadline(effectiveUserId, response.timeout)
104             val interval = LockPatternUtils.FAILED_ATTEMPT_COUNTDOWN_INTERVAL_MS
105             var remaining = deadline - systemClock.elapsedRealtime()
106             while (remaining > 0) {
107                 emit(
108                     CredentialStatus.Fail.Throttled(
109                         applicationContext.getString(
110                             R.string.biometric_dialog_credential_too_many_attempts,
111                             remaining / 1000
112                         )
113                     )
114                 )
115                 delay(interval)
116                 remaining -= interval
117             }
118             emit(CredentialStatus.Fail.Error(""))
119         } else { // bad request, but not throttled
120             val numAttempts = lockPatternUtils.getCurrentFailedPasswordAttempts(effectiveUserId) + 1
121             val maxAttempts = lockPatternUtils.getMaximumFailedPasswordsForWipe(effectiveUserId)
122             if (maxAttempts <= 0 || numAttempts <= 0) {
123                 // use a generic message if there's no maximum number of attempts
124                 emit(CredentialStatus.Fail.Error())
125             } else {
126                 val remainingAttempts = (maxAttempts - numAttempts).coerceAtLeast(0)
127                 emit(
128                     CredentialStatus.Fail.Error(
129                         applicationContext.getString(
130                             R.string.biometric_dialog_credential_attempts_before_wipe,
131                             numAttempts,
132                             maxAttempts
133                         ),
134                         remainingAttempts,
135                         fetchFinalAttemptMessageOrNull(request, remainingAttempts)
136                     )
137                 )
138             }
139             lockPatternUtils.reportFailedPasswordAttempt(effectiveUserId)
140         }
141     }
142 
143     private fun fetchFinalAttemptMessageOrNull(
144         request: BiometricPromptRequest.Credential,
145         remainingAttempts: Int?,
146     ): String? =
147         if (remainingAttempts != null && remainingAttempts <= 1) {
148             applicationContext.getFinalAttemptMessageOrBlank(
149                 request,
150                 devicePolicyManager,
151                 userManager.getUserTypeForWipe(
152                     devicePolicyManager,
153                     request.userInfo.deviceCredentialOwnerId
154                 ),
155                 remainingAttempts
156             )
157         } else {
158             null
159         }
160 }
161 
162 private enum class UserType {
163     PRIMARY,
164     MANAGED_PROFILE,
165     SECONDARY,
166 }
167 
UserManagernull168 private fun UserManager.getUserTypeForWipe(
169     devicePolicyManager: DevicePolicyManager,
170     effectiveUserId: Int,
171 ): UserType {
172     val userToBeWiped =
173         getUserInfo(
174             devicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(effectiveUserId)
175         )
176     return when {
177         userToBeWiped == null || userToBeWiped.isPrimary -> UserType.PRIMARY
178         userToBeWiped.isManagedProfile -> UserType.MANAGED_PROFILE
179         else -> UserType.SECONDARY
180     }
181 }
182 
Contextnull183 private fun Context.getFinalAttemptMessageOrBlank(
184     request: BiometricPromptRequest.Credential,
185     devicePolicyManager: DevicePolicyManager,
186     userType: UserType,
187     remaining: Int,
188 ): String =
189     when {
190         remaining == 1 -> getLastAttemptBeforeWipeMessage(request, devicePolicyManager, userType)
191         remaining <= 0 -> getNowWipingMessage(devicePolicyManager, userType)
192         else -> ""
193     }
194 
Contextnull195 private fun Context.getLastAttemptBeforeWipeMessage(
196     request: BiometricPromptRequest.Credential,
197     devicePolicyManager: DevicePolicyManager,
198     userType: UserType,
199 ): String =
200     when (userType) {
201         UserType.PRIMARY -> getLastAttemptBeforeWipeDeviceMessage(request)
202         UserType.MANAGED_PROFILE ->
203             getLastAttemptBeforeWipeProfileMessage(request, devicePolicyManager)
204         UserType.SECONDARY -> getLastAttemptBeforeWipeUserMessage(request)
205     }
206 
Contextnull207 private fun Context.getLastAttemptBeforeWipeDeviceMessage(
208     request: BiometricPromptRequest.Credential,
209 ): String {
210     val id =
211         when (request) {
212             is BiometricPromptRequest.Credential.Pin ->
213                 R.string.biometric_dialog_last_pin_attempt_before_wipe_device
214             is BiometricPromptRequest.Credential.Pattern ->
215                 R.string.biometric_dialog_last_pattern_attempt_before_wipe_device
216             is BiometricPromptRequest.Credential.Password ->
217                 R.string.biometric_dialog_last_password_attempt_before_wipe_device
218         }
219     return getString(id)
220 }
221 
Contextnull222 private fun Context.getLastAttemptBeforeWipeProfileMessage(
223     request: BiometricPromptRequest.Credential,
224     devicePolicyManager: DevicePolicyManager,
225 ): String {
226     val id =
227         when (request) {
228             is BiometricPromptRequest.Credential.Pin ->
229                 DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PIN_LAST_ATTEMPT
230             is BiometricPromptRequest.Credential.Pattern ->
231                 DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PATTERN_LAST_ATTEMPT
232             is BiometricPromptRequest.Credential.Password ->
233                 DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_PASSWORD_LAST_ATTEMPT
234         }
235     val getFallbackString = {
236         val defaultId =
237             when (request) {
238                 is BiometricPromptRequest.Credential.Pin ->
239                     R.string.biometric_dialog_last_pin_attempt_before_wipe_profile
240                 is BiometricPromptRequest.Credential.Pattern ->
241                     R.string.biometric_dialog_last_pattern_attempt_before_wipe_profile
242                 is BiometricPromptRequest.Credential.Password ->
243                     R.string.biometric_dialog_last_password_attempt_before_wipe_profile
244             }
245         getString(defaultId)
246     }
247 
248     return devicePolicyManager.resources?.getString(id, getFallbackString) ?: getFallbackString()
249 }
250 
Contextnull251 private fun Context.getLastAttemptBeforeWipeUserMessage(
252     request: BiometricPromptRequest.Credential,
253 ): String {
254     val resId =
255         when (request) {
256             is BiometricPromptRequest.Credential.Pin ->
257                 R.string.biometric_dialog_last_pin_attempt_before_wipe_user
258             is BiometricPromptRequest.Credential.Pattern ->
259                 R.string.biometric_dialog_last_pattern_attempt_before_wipe_user
260             is BiometricPromptRequest.Credential.Password ->
261                 R.string.biometric_dialog_last_password_attempt_before_wipe_user
262         }
263     return getString(resId)
264 }
265 
Contextnull266 private fun Context.getNowWipingMessage(
267     devicePolicyManager: DevicePolicyManager,
268     userType: UserType,
269 ): String {
270     val id =
271         when (userType) {
272             UserType.MANAGED_PROFILE ->
273                 DevicePolicyResources.Strings.SystemUi.BIOMETRIC_DIALOG_WORK_LOCK_FAILED_ATTEMPTS
274             else -> DevicePolicyResources.UNDEFINED
275         }
276 
277     val getFallbackString = {
278         val defaultId =
279             when (userType) {
280                 UserType.PRIMARY ->
281                     com.android.settingslib.R.string.failed_attempts_now_wiping_device
282                 UserType.MANAGED_PROFILE ->
283                     com.android.settingslib.R.string.failed_attempts_now_wiping_profile
284                 UserType.SECONDARY ->
285                     com.android.settingslib.R.string.failed_attempts_now_wiping_user
286             }
287         getString(defaultId)
288     }
289 
290     return devicePolicyManager.resources?.getString(id, getFallbackString) ?: getFallbackString()
291 }
292