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