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