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 */
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 /**
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
UserManagernull162 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
Contextnull177 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
Contextnull189 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
Contextnull201 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
Contextnull216 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
Contextnull245 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
Contextnull260 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