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.authentication.domain.interactor
18 
19 import android.app.admin.flags.Flags
20 import android.os.UserHandle
21 import com.android.internal.widget.LockPatternUtils
22 import com.android.internal.widget.LockPatternView
23 import com.android.internal.widget.LockscreenCredential
24 import com.android.systemui.authentication.data.repository.AuthenticationRepository
25 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
26 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Password
27 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern
28 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin
29 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
30 import com.android.systemui.authentication.shared.model.AuthenticationWipeModel
31 import com.android.systemui.authentication.shared.model.AuthenticationWipeModel.WipeTarget
32 import com.android.systemui.dagger.SysUISingleton
33 import com.android.systemui.dagger.qualifiers.Application
34 import com.android.systemui.dagger.qualifiers.Background
35 import com.android.systemui.user.domain.interactor.SelectedUserInteractor
36 import com.android.systemui.util.time.SystemClock
37 import javax.inject.Inject
38 import kotlin.math.max
39 import kotlin.time.Duration
40 import kotlin.time.Duration.Companion.seconds
41 import kotlinx.coroutines.CoroutineDispatcher
42 import kotlinx.coroutines.CoroutineScope
43 import kotlinx.coroutines.delay
44 import kotlinx.coroutines.flow.Flow
45 import kotlinx.coroutines.flow.MutableSharedFlow
46 import kotlinx.coroutines.flow.SharedFlow
47 import kotlinx.coroutines.flow.SharingStarted
48 import kotlinx.coroutines.flow.StateFlow
49 import kotlinx.coroutines.flow.asSharedFlow
50 import kotlinx.coroutines.flow.combine
51 import kotlinx.coroutines.flow.map
52 import kotlinx.coroutines.flow.stateIn
53 import kotlinx.coroutines.launch
54 
55 /**
56  * Hosts application business logic related to user authentication.
57  *
58  * Note: there is a distinction between authentication (determining a user's identity) and device
59  * entry (dismissing the lockscreen). For logic that is specific to device entry, please use
60  * `DeviceEntryInteractor` instead.
61  */
62 @SysUISingleton
63 class AuthenticationInteractor
64 @Inject
65 constructor(
66     @Application private val applicationScope: CoroutineScope,
67     @Background private val backgroundDispatcher: CoroutineDispatcher,
68     private val repository: AuthenticationRepository,
69     private val selectedUserInteractor: SelectedUserInteractor,
70 ) {
71     /**
72      * The currently-configured authentication method. This determines how the authentication
73      * challenge needs to be completed in order to unlock an otherwise locked device.
74      *
75      * Note: there may be other ways to unlock the device that "bypass" the need for this
76      * authentication challenge (notably, biometrics like fingerprint or face unlock).
77      *
78      * Note: by design, this is a [Flow] and not a [StateFlow]; a consumer who wishes to get a
79      * snapshot of the current authentication method without establishing a collector of the flow
80      * can do so by invoking [getAuthenticationMethod].
81      *
82      * Note: this layer adds the synthetic authentication method of "swipe" which is special. When
83      * the current authentication method is "swipe", the user does not need to complete any
84      * authentication challenge to unlock the device; they just need to dismiss the lockscreen to
85      * get past it. This also means that the value of `DeviceEntryInteractor#isUnlocked` remains
86      * `true` even when the lockscreen is showing and still needs to be dismissed by the user to
87      * proceed.
88      */
89     val authenticationMethod: Flow<AuthenticationMethodModel> = repository.authenticationMethod
90 
91     /**
92      * Whether the auto confirm feature is enabled for the currently-selected user.
93      *
94      * Note that the length of the PIN is also important to take into consideration, please see
95      * [hintedPinLength].
96      */
97     val isAutoConfirmEnabled: StateFlow<Boolean> =
98         combine(repository.isAutoConfirmFeatureEnabled, repository.hasLockoutOccurred) {
99                 featureEnabled,
100                 hasLockoutOccurred ->
101                 // Disable auto-confirm if lockout occurred since the last successful
102                 // authentication attempt.
103                 featureEnabled && !hasLockoutOccurred
104             }
105             .stateIn(
106                 scope = applicationScope,
107                 started = SharingStarted.WhileSubscribed(),
108                 initialValue = false,
109             )
110 
111     /** The length of the hinted PIN, or `null` if pin length hint should not be shown. */
112     val hintedPinLength: StateFlow<Int?> =
113         isAutoConfirmEnabled
114             .map { isAutoConfirmEnabled ->
115                 repository.getPinLength().takeIf {
116                     isAutoConfirmEnabled && it == repository.hintedPinLength
117                 }
118             }
119             .stateIn(
120                 scope = applicationScope,
121                 // Make sure this is kept as WhileSubscribed or we can run into a bug where the
122                 // downstream continues to receive old/stale/cached values.
123                 started = SharingStarted.WhileSubscribed(),
124                 initialValue = null,
125             )
126 
127     /** Whether the pattern should be visible for the currently-selected user. */
128     val isPatternVisible: StateFlow<Boolean> = repository.isPatternVisible
129 
130     private val _onAuthenticationResult = MutableSharedFlow<Boolean>()
131     /**
132      * Emits the outcome (successful or unsuccessful) whenever a PIN/Pattern/Password security
133      * challenge is attempted by the user in order to unlock the device.
134      */
135     val onAuthenticationResult: SharedFlow<Boolean> = _onAuthenticationResult.asSharedFlow()
136 
137     /** Whether the "enhanced PIN privacy" setting is enabled for the current user. */
138     val isPinEnhancedPrivacyEnabled: StateFlow<Boolean> = repository.isPinEnhancedPrivacyEnabled
139 
140     /**
141      * The number of failed authentication attempts for the selected user since the last successful
142      * authentication.
143      */
144     val failedAuthenticationAttempts: StateFlow<Int> = repository.failedAuthenticationAttempts
145 
146     /**
147      * Timestamp for when the current lockout (aka "throttling") will end, allowing the user to
148      * attempt authentication again. Returns `null` if no lockout is active.
149      *
150      * To be notified whenever a lockout is started, the caller should subscribe to
151      * [onAuthenticationResult].
152      *
153      * Note that the value is in milliseconds and matches [SystemClock.elapsedRealtime].
154      *
155      * Also note that the value may change when the selected user is changed.
156      */
157     val lockoutEndTimestamp: Long?
158         get() = repository.lockoutEndTimestamp
159 
160     /**
161      * Models an imminent wipe risk to the user, profile, or device upon further unsuccessful
162      * authentication attempts.
163      *
164      * Returns `null` when there is no risk of wipe yet, or when there's no wipe policy set by the
165      * DevicePolicyManager.
166      */
167     val upcomingWipe: Flow<AuthenticationWipeModel?> =
168         repository.failedAuthenticationAttempts.map { failedAttempts ->
169             val failedAttemptsBeforeWipe = repository.getMaxFailedUnlockAttemptsForWipe()
170             if (failedAttemptsBeforeWipe == 0) {
171                 return@map null // There is no restriction.
172             }
173 
174             // The user has a DevicePolicyManager that requests a user/profile to be wiped after N
175             // attempts. Once the grace period is reached, show a dialog every time as a clear
176             // warning until the deletion fires.
177             val remainingAttemptsBeforeWipe = max(0, failedAttemptsBeforeWipe - failedAttempts)
178             if (remainingAttemptsBeforeWipe >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_WIPE_GRACE) {
179                 return@map null // There is no current risk of wiping the device.
180             }
181 
182             AuthenticationWipeModel(
183                 wipeTarget = getWipeTarget(),
184                 failedAttempts = failedAttempts,
185                 remainingAttempts = remainingAttemptsBeforeWipe,
186             )
187         }
188 
189     /**
190      * Returns the currently-configured authentication method. This determines how the
191      * authentication challenge needs to be completed in order to unlock an otherwise locked device.
192      *
193      * Note: there may be other ways to unlock the device that "bypass" the need for this
194      * authentication challenge (notably, biometrics like fingerprint or face unlock).
195      *
196      * Note: by design, this is offered as a convenience method alongside [authenticationMethod].
197      * The flow should be used for code that wishes to stay up-to-date its logic as the
198      * authentication changes over time and this method should be used for simple code that only
199      * needs to check the current value.
200      */
201     suspend fun getAuthenticationMethod() = repository.getAuthenticationMethod()
202 
203     /**
204      * Attempts to authenticate the user and unlock the device. May trigger lockout or wipe the
205      * user/profile/device data upon failure.
206      *
207      * If [tryAutoConfirm] is `true`, authentication is attempted if and only if the auth method
208      * supports auto-confirming, and the input's length is at least the required length. Otherwise,
209      * `AuthenticationResult.SKIPPED` is returned.
210      *
211      * @param input The input from the user to try to authenticate with. This can be a list of
212      *   different things, based on the current authentication method.
213      * @param tryAutoConfirm `true` if called while the user inputs the code, without an explicit
214      *   request to validate.
215      * @return The result of this authentication attempt.
216      */
217     suspend fun authenticate(
218         input: List<Any>,
219         tryAutoConfirm: Boolean = false
220     ): AuthenticationResult {
221         if (input.isEmpty()) {
222             throw IllegalArgumentException("Input was empty!")
223         }
224 
225         val authMethod = getAuthenticationMethod()
226         if (shouldSkipAuthenticationAttempt(authMethod, tryAutoConfirm, input.size)) {
227             return AuthenticationResult.SKIPPED
228         }
229 
230         // Attempt to authenticate:
231         val credential = authMethod.createCredential(input) ?: return AuthenticationResult.SKIPPED
232         val authenticationResult = repository.checkCredential(credential)
233         credential.zeroize()
234 
235         if (authenticationResult.isSuccessful) {
236             repository.reportAuthenticationAttempt(isSuccessful = true)
237             _onAuthenticationResult.emit(true)
238 
239             // Force a garbage collection in an attempt to erase any credentials left in memory.
240             // Do it after a 5-sec delay to avoid making the bouncer dismiss animation janky.
241             initiateGarbageCollection(delay = 5.seconds)
242 
243             return AuthenticationResult.SUCCEEDED
244         }
245 
246         // Authentication failed.
247         repository.reportAuthenticationAttempt(isSuccessful = false)
248 
249         if (authenticationResult.lockoutDurationMs > 0) {
250             // Lockout has been triggered.
251             repository.reportLockoutStarted(authenticationResult.lockoutDurationMs)
252         }
253 
254         _onAuthenticationResult.emit(false)
255         return AuthenticationResult.FAILED
256     }
257 
258     private suspend fun shouldSkipAuthenticationAttempt(
259         authenticationMethod: AuthenticationMethodModel,
260         isAutoConfirmAttempt: Boolean,
261         inputLength: Int,
262     ): Boolean {
263         return when {
264             // Lockout is active, the UI layer should not have called this; skip the attempt.
265             repository.lockoutEndTimestamp != null -> true
266             // Auto-confirm attempt when the feature is not enabled; skip the attempt.
267             isAutoConfirmAttempt && !isAutoConfirmEnabled.value -> true
268             // The pin is too short; skip only if this is an auto-confirm attempt.
269             authenticationMethod == Pin && authenticationMethod.isInputTooShort(inputLength) ->
270                 isAutoConfirmAttempt
271             // The input is too short.
272             authenticationMethod.isInputTooShort(inputLength) -> true
273             else -> false
274         }
275     }
276 
277     private suspend fun AuthenticationMethodModel.isInputTooShort(inputLength: Int): Boolean {
278         return when (this) {
279             Pattern -> inputLength < repository.minPatternLength
280             Password -> inputLength < repository.minPasswordLength
281             Pin -> inputLength < repository.getPinLength()
282             else -> false
283         }
284     }
285 
286     /**
287      * @return Whether the current user, managed profile or whole device is next at risk of wipe.
288      */
289     private suspend fun getWipeTarget(): WipeTarget {
290         // Check which profile has the strictest policy for failed authentication attempts.
291         val userToBeWiped = repository.getProfileWithMinFailedUnlockAttemptsForWipe()
292         val primaryUser =
293             if (Flags.headlessSingleUserFixes()) {
294                 selectedUserInteractor.getMainUserId() ?: UserHandle.USER_SYSTEM
295             } else {
296                 UserHandle.USER_SYSTEM
297             }
298         return when (userToBeWiped) {
299             selectedUserInteractor.getSelectedUserId() ->
300                 if (userToBeWiped == primaryUser) {
301                     WipeTarget.WholeDevice
302                 } else {
303                     WipeTarget.User
304                 }
305 
306             // Shouldn't happen at this stage; this is to maintain legacy behavior.
307             UserHandle.USER_NULL -> WipeTarget.WholeDevice
308             else -> WipeTarget.ManagedProfile
309         }
310     }
311 
312     private fun AuthenticationMethodModel.createCredential(
313         input: List<Any>
314     ): LockscreenCredential? {
315         return when (this) {
316             is Pin -> LockscreenCredential.createPin(input.joinToString(""))
317             is Password -> LockscreenCredential.createPassword(input.joinToString(""))
318             is Pattern ->
319                 LockscreenCredential.createPattern(
320                     input
321                         .map { it as AuthenticationPatternCoordinate }
322                         .map { LockPatternView.Cell.of(it.y, it.x) }
323                 )
324             else -> null
325         }
326     }
327 
328     private suspend fun initiateGarbageCollection(delay: Duration) {
329         applicationScope.launch(backgroundDispatcher) {
330             delay(delay)
331             System.gc()
332             System.runFinalization()
333             System.gc()
334         }
335     }
336 
337     companion object {
338         const val TAG = "AuthenticationInteractor"
339     }
340 }
341 
342 /** Result of a user authentication attempt. */
343 enum class AuthenticationResult {
344     /** Authentication succeeded. */
345     SUCCEEDED,
346     /** Authentication failed. */
347     FAILED,
348     /** Authentication was not performed, e.g. due to insufficient input. */
349     SKIPPED,
350 }
351