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