1 /*
2  * 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 com.android.internal.widget.LockPatternView
20 import com.android.internal.widget.LockscreenCredential
21 import com.android.systemui.authentication.data.model.AuthenticationMethodModel as DataLayerAuthenticationMethodModel
22 import com.android.systemui.authentication.data.repository.AuthenticationRepository
23 import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainLayerAuthenticationMethodModel
24 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
25 import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel
26 import com.android.systemui.dagger.SysUISingleton
27 import com.android.systemui.dagger.qualifiers.Application
28 import com.android.systemui.dagger.qualifiers.Background
29 import com.android.systemui.keyguard.data.repository.KeyguardRepository
30 import com.android.systemui.scene.domain.interactor.SceneInteractor
31 import com.android.systemui.scene.shared.model.SceneKey
32 import com.android.systemui.user.data.repository.UserRepository
33 import com.android.systemui.util.time.SystemClock
34 import javax.inject.Inject
35 import kotlin.math.max
36 import kotlin.time.Duration.Companion.seconds
37 import kotlinx.coroutines.CoroutineDispatcher
38 import kotlinx.coroutines.CoroutineScope
39 import kotlinx.coroutines.Job
40 import kotlinx.coroutines.async
41 import kotlinx.coroutines.delay
42 import kotlinx.coroutines.flow.Flow
43 import kotlinx.coroutines.flow.SharingStarted
44 import kotlinx.coroutines.flow.StateFlow
45 import kotlinx.coroutines.flow.combine
46 import kotlinx.coroutines.flow.distinctUntilChanged
47 import kotlinx.coroutines.flow.filter
48 import kotlinx.coroutines.flow.map
49 import kotlinx.coroutines.flow.stateIn
50 import kotlinx.coroutines.launch
51 import kotlinx.coroutines.withContext
52 
53 /** Hosts application business logic related to authentication. */
54 @SysUISingleton
55 class AuthenticationInteractor
56 @Inject
57 constructor(
58     @Application private val applicationScope: CoroutineScope,
59     private val repository: AuthenticationRepository,
60     @Background private val backgroundDispatcher: CoroutineDispatcher,
61     private val userRepository: UserRepository,
62     private val keyguardRepository: KeyguardRepository,
63     sceneInteractor: SceneInteractor,
64     private val clock: SystemClock,
65 ) {
66     /**
67      * The currently-configured authentication method. This determines how the authentication
68      * challenge needs to be completed in order to unlock an otherwise locked device.
69      *
70      * Note: there may be other ways to unlock the device that "bypass" the need for this
71      * authentication challenge (notably, biometrics like fingerprint or face unlock).
72      *
73      * Note: by design, this is a [Flow] and not a [StateFlow]; a consumer who wishes to get a
74      * snapshot of the current authentication method without establishing a collector of the flow
75      * can do so by invoking [getAuthenticationMethod].
76      *
77      * Note: this layer adds the synthetic authentication method of "swipe" which is special. When
78      * the current authentication method is "swipe", the user does not need to complete any
79      * authentication challenge to unlock the device; they just need to dismiss the lockscreen to
80      * get past it. This also means that the value of [isUnlocked] remains `false` even when the
81      * lockscreen is showing and still needs to be dismissed by the user to proceed.
82      */
83     val authenticationMethod: Flow<DomainLayerAuthenticationMethodModel> =
84         repository.authenticationMethod.map { rawModel -> rawModel.toDomainLayer() }
85 
86     /**
87      * Whether the device is unlocked.
88      *
89      * A device that is not yet unlocked requires unlocking by completing an authentication
90      * challenge according to the current authentication method, unless in cases when the current
91      * authentication method is not "secure" (for example, None and Swipe); in such cases, the value
92      * of this flow will always be `true`, even if the lockscreen is showing and still needs to be
93      * dismissed by the user to proceed.
94      */
95     val isUnlocked: StateFlow<Boolean> =
96         combine(
97                 repository.isUnlocked,
98                 authenticationMethod,
99             ) { isUnlocked, authenticationMethod ->
100                 !authenticationMethod.isSecure || isUnlocked
101             }
102             .stateIn(
103                 scope = applicationScope,
104                 started = SharingStarted.Eagerly,
105                 initialValue = false,
106             )
107 
108     /**
109      * Whether the lockscreen has been dismissed (by any method). This can be false even when the
110      * device is unlocked, e.g. when swipe to unlock is enabled.
111      *
112      * Note:
113      * - `false` doesn't mean the lockscreen is visible (it may be occluded or covered by other UI).
114      * - `true` doesn't mean the lockscreen is invisible (since this state changes before the
115      *   transition occurs).
116      */
117     val isLockscreenDismissed: StateFlow<Boolean> =
118         sceneInteractor.desiredScene
119             .map { it.key }
120             .filter { currentScene ->
121                 currentScene == SceneKey.Gone || currentScene == SceneKey.Lockscreen
122             }
123             .map { it == SceneKey.Gone }
124             .stateIn(
125                 scope = applicationScope,
126                 started = SharingStarted.WhileSubscribed(),
127                 initialValue = false,
128             )
129 
130     /**
131      * Whether it's currently possible to swipe up to dismiss the lockscreen without requiring
132      * authentication. This returns false whenever the lockscreen has been dismissed.
133      *
134      * Note: `true` doesn't mean the lockscreen is visible. It may be occluded or covered by other
135      * UI.
136      */
137     val canSwipeToDismiss =
138         combine(authenticationMethod, isLockscreenDismissed) {
139                 authenticationMethod,
140                 isLockscreenDismissed ->
141                 authenticationMethod is DomainLayerAuthenticationMethodModel.Swipe &&
142                     !isLockscreenDismissed
143             }
144             .stateIn(
145                 scope = applicationScope,
146                 started = SharingStarted.WhileSubscribed(),
147                 initialValue = false,
148             )
149 
150     /** The current authentication throttling state, only meaningful if [isThrottled] is `true`. */
151     val throttling: StateFlow<AuthenticationThrottlingModel> = repository.throttling
152 
153     /**
154      * Whether currently throttled and the user has to wait before being able to try another
155      * authentication attempt.
156      */
157     val isThrottled: StateFlow<Boolean> =
158         throttling
159             .map { it.remainingMs > 0 }
160             .stateIn(
161                 scope = applicationScope,
162                 started = SharingStarted.Eagerly,
163                 initialValue = throttling.value.remainingMs > 0,
164             )
165 
166     /** The length of the hinted PIN, or `null` if pin length hint should not be shown. */
167     val hintedPinLength: StateFlow<Int?> =
168         repository.isAutoConfirmEnabled
169             .map { isAutoConfirmEnabled ->
170                 repository.getPinLength().takeIf {
171                     isAutoConfirmEnabled && it == repository.hintedPinLength
172                 }
173             }
174             .stateIn(
175                 scope = applicationScope,
176                 // Make sure this is kept as WhileSubscribed or we can run into a bug where the
177                 // downstream continues to receive old/stale/cached values.
178                 started = SharingStarted.WhileSubscribed(),
179                 initialValue = null,
180             )
181 
182     /** Whether the auto confirm feature is enabled for the currently-selected user. */
183     val isAutoConfirmEnabled: StateFlow<Boolean> = repository.isAutoConfirmEnabled
184 
185     /** Whether the pattern should be visible for the currently-selected user. */
186     val isPatternVisible: StateFlow<Boolean> = repository.isPatternVisible
187 
188     private var throttlingCountdownJob: Job? = null
189 
190     init {
191         applicationScope.launch {
192             userRepository.selectedUserInfo
193                 .map { it.id }
194                 .distinctUntilChanged()
195                 .collect { onSelectedUserChanged() }
196         }
197     }
198 
199     /**
200      * Returns the currently-configured authentication method. This determines how the
201      * authentication challenge needs to be completed in order to unlock an otherwise locked device.
202      *
203      * Note: there may be other ways to unlock the device that "bypass" the need for this
204      * authentication challenge (notably, biometrics like fingerprint or face unlock).
205      *
206      * Note: by design, this is offered as a convenience method alongside [authenticationMethod].
207      * The flow should be used for code that wishes to stay up-to-date its logic as the
208      * authentication changes over time and this method should be used for simple code that only
209      * needs to check the current value.
210      *
211      * Note: this layer adds the synthetic authentication method of "swipe" which is special. When
212      * the current authentication method is "swipe", the user does not need to complete any
213      * authentication challenge to unlock the device; they just need to dismiss the lockscreen to
214      * get past it. This also means that the value of [isUnlocked] remains `false` even when the
215      * lockscreen is showing and still needs to be dismissed by the user to proceed.
216      */
217     suspend fun getAuthenticationMethod(): DomainLayerAuthenticationMethodModel {
218         return repository.getAuthenticationMethod().toDomainLayer()
219     }
220 
221     /**
222      * Returns `true` if the device currently requires authentication before content can be viewed;
223      * `false` if content can be displayed without unlocking first.
224      */
225     suspend fun isAuthenticationRequired(): Boolean {
226         return !isUnlocked.value && getAuthenticationMethod().isSecure
227     }
228 
229     /**
230      * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically
231      * dismisses once the authentication challenge is completed. For example, completing a biometric
232      * authentication challenge via face unlock or fingerprint sensor can automatically bypass the
233      * lock screen.
234      */
235     fun isBypassEnabled(): Boolean {
236         return keyguardRepository.isBypassEnabled()
237     }
238 
239     /**
240      * Attempts to authenticate the user and unlock the device.
241      *
242      * If [tryAutoConfirm] is `true`, authentication is attempted if and only if the auth method
243      * supports auto-confirming, and the input's length is at least the code's length. Otherwise,
244      * `null` is returned.
245      *
246      * @param input The input from the user to try to authenticate with. This can be a list of
247      *   different things, based on the current authentication method.
248      * @param tryAutoConfirm `true` if called while the user inputs the code, without an explicit
249      *   request to validate.
250      * @return `true` if the authentication succeeded and the device is now unlocked; `false` when
251      *   authentication failed, `null` if the check was not performed.
252      */
253     suspend fun authenticate(input: List<Any>, tryAutoConfirm: Boolean = false): Boolean? {
254         if (input.isEmpty()) {
255             throw IllegalArgumentException("Input was empty!")
256         }
257 
258         val skipCheck =
259             when {
260                 // We're being throttled, the UI layer should not have called this; skip the
261                 // attempt.
262                 isThrottled.value -> true
263                 // Auto-confirm attempt when the feature is not enabled; skip the attempt.
264                 tryAutoConfirm && !isAutoConfirmEnabled.value -> true
265                 // Auto-confirm should skip the attempt if the pin entered is too short.
266                 tryAutoConfirm && input.size < repository.getPinLength() -> true
267                 else -> false
268             }
269         if (skipCheck) {
270             return null
271         }
272 
273         // Attempt to authenticate:
274         val authMethod = getAuthenticationMethod()
275         val credential = authMethod.createCredential(input) ?: return null
276         val authenticationResult = repository.checkCredential(credential)
277         credential.zeroize()
278 
279         if (authenticationResult.isSuccessful || !tryAutoConfirm) {
280             repository.reportAuthenticationAttempt(
281                 isSuccessful = authenticationResult.isSuccessful,
282             )
283         }
284 
285         // Check if we need to throttle and, if so, kick off the throttle countdown:
286         if (!authenticationResult.isSuccessful && authenticationResult.throttleDurationMs > 0) {
287             repository.setThrottleDuration(
288                 durationMs = authenticationResult.throttleDurationMs,
289             )
290             startThrottlingCountdown()
291         }
292 
293         if (authenticationResult.isSuccessful) {
294             // Since authentication succeeded, we should refresh throttling to make sure that our
295             // state is completely reflecting the upstream source of truth.
296             refreshThrottling()
297         }
298 
299         return authenticationResult.isSuccessful
300     }
301 
302     /** Starts refreshing the throttling state every second. */
303     private suspend fun startThrottlingCountdown() {
304         cancelCountdown()
305         throttlingCountdownJob =
306             applicationScope.launch {
307                 while (refreshThrottling() > 0) {
308                     delay(1.seconds.inWholeMilliseconds)
309                 }
310             }
311     }
312 
313     /** Cancels any throttling state countdown started in [startThrottlingCountdown]. */
314     private fun cancelCountdown() {
315         throttlingCountdownJob?.cancel()
316         throttlingCountdownJob = null
317     }
318 
319     /** Notifies that the currently-selected user has changed. */
320     private suspend fun onSelectedUserChanged() {
321         cancelCountdown()
322         if (refreshThrottling() > 0) {
323             startThrottlingCountdown()
324         }
325     }
326 
327     /**
328      * Refreshes the throttling state, hydrating the repository with the latest state.
329      *
330      * @return The remaining time for the current throttling countdown, in milliseconds or `0` if
331      *   not being throttled.
332      */
333     private suspend fun refreshThrottling(): Long {
334         return withContext(backgroundDispatcher) {
335             val failedAttemptCount = async { repository.getFailedAuthenticationAttemptCount() }
336             val deadline = async { repository.getThrottlingEndTimestamp() }
337             val remainingMs = max(0, deadline.await() - clock.elapsedRealtime())
338             repository.setThrottling(
339                 AuthenticationThrottlingModel(
340                     failedAttemptCount = failedAttemptCount.await(),
341                     remainingMs = remainingMs.toInt(),
342                 ),
343             )
344             remainingMs
345         }
346     }
347 
348     private fun DomainLayerAuthenticationMethodModel.createCredential(
349         input: List<Any>
350     ): LockscreenCredential? {
351         return when (this) {
352             is DomainLayerAuthenticationMethodModel.Pin ->
353                 LockscreenCredential.createPin(input.joinToString(""))
354             is DomainLayerAuthenticationMethodModel.Password ->
355                 LockscreenCredential.createPassword(input.joinToString(""))
356             is DomainLayerAuthenticationMethodModel.Pattern ->
357                 LockscreenCredential.createPattern(
358                     input
359                         .map { it as AuthenticationPatternCoordinate }
360                         .map { LockPatternView.Cell.of(it.y, it.x) }
361                 )
362             else -> null
363         }
364     }
365 
366     private suspend fun DataLayerAuthenticationMethodModel.toDomainLayer():
367         DomainLayerAuthenticationMethodModel {
368         return when (this) {
369             is DataLayerAuthenticationMethodModel.None ->
370                 if (repository.isLockscreenEnabled()) {
371                     DomainLayerAuthenticationMethodModel.Swipe
372                 } else {
373                     DomainLayerAuthenticationMethodModel.None
374                 }
375             is DataLayerAuthenticationMethodModel.Pin -> DomainLayerAuthenticationMethodModel.Pin
376             is DataLayerAuthenticationMethodModel.Password ->
377                 DomainLayerAuthenticationMethodModel.Password
378             is DataLayerAuthenticationMethodModel.Pattern ->
379                 DomainLayerAuthenticationMethodModel.Pattern
380         }
381     }
382 }
383