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 @file:OptIn(ExperimentalCoroutinesApi::class)
18 
19 package com.android.systemui.authentication.data.repository
20 
21 import android.annotation.UserIdInt
22 import android.app.admin.DevicePolicyManager
23 import android.content.IntentFilter
24 import android.os.UserHandle
25 import com.android.internal.widget.LockPatternUtils
26 import com.android.internal.widget.LockscreenCredential
27 import com.android.keyguard.KeyguardSecurityModel
28 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
29 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.None
30 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Password
31 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern
32 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin
33 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Sim
34 import com.android.systemui.authentication.shared.model.AuthenticationResultModel
35 import com.android.systemui.broadcast.BroadcastDispatcher
36 import com.android.systemui.dagger.SysUISingleton
37 import com.android.systemui.dagger.qualifiers.Application
38 import com.android.systemui.dagger.qualifiers.Background
39 import com.android.systemui.scene.shared.flag.SceneContainerFlag
40 import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
41 import com.android.systemui.user.data.repository.UserRepository
42 import com.android.systemui.util.kotlin.onSubscriberAdded
43 import com.android.systemui.util.time.SystemClock
44 import dagger.Binds
45 import dagger.Module
46 import java.util.function.Function
47 import javax.inject.Inject
48 import kotlinx.coroutines.CoroutineDispatcher
49 import kotlinx.coroutines.CoroutineScope
50 import kotlinx.coroutines.ExperimentalCoroutinesApi
51 import kotlinx.coroutines.flow.Flow
52 import kotlinx.coroutines.flow.MutableStateFlow
53 import kotlinx.coroutines.flow.StateFlow
54 import kotlinx.coroutines.flow.asStateFlow
55 import kotlinx.coroutines.flow.combine
56 import kotlinx.coroutines.flow.distinctUntilChanged
57 import kotlinx.coroutines.flow.flatMapLatest
58 import kotlinx.coroutines.flow.map
59 import kotlinx.coroutines.flow.onStart
60 import kotlinx.coroutines.launch
61 import kotlinx.coroutines.withContext
62 
63 /** Defines interface for classes that can access authentication-related application state. */
64 interface AuthenticationRepository {
65     /**
66      * The exact length a PIN should be for us to enable PIN length hinting.
67      *
68      * A PIN that's shorter or longer than this is not eligible for the UI to render hints showing
69      * how many digits the current PIN is, even if [isAutoConfirmFeatureEnabled] is enabled.
70      *
71      * Note that PIN length hinting is only available if the PIN auto confirmation feature is
72      * available.
73      */
74     val hintedPinLength: Int
75 
76     /** Whether the pattern should be visible for the currently-selected user. */
77     val isPatternVisible: StateFlow<Boolean>
78 
79     /**
80      * Whether the auto confirm feature is enabled for the currently-selected user.
81      *
82      * Note that the length of the PIN is also important to take into consideration, please see
83      * [hintedPinLength].
84      */
85     val isAutoConfirmFeatureEnabled: StateFlow<Boolean>
86 
87     /**
88      * The number of failed authentication attempts for the selected user since their last
89      * successful authentication.
90      */
91     val failedAuthenticationAttempts: StateFlow<Int>
92 
93     /**
94      * Timestamp for when the current lockout (aka "throttling") will end, allowing the user to
95      * attempt authentication again. Returns `null` if no lockout is active.
96      *
97      * Note that the value is in milliseconds and matches [SystemClock.elapsedRealtime].
98      *
99      * Also note that the value may change when the selected user is changed.
100      */
101     val lockoutEndTimestamp: Long?
102 
103     /**
104      * Whether lockout has occurred at least once since the last successful authentication of any
105      * user.
106      */
107     val hasLockoutOccurred: StateFlow<Boolean>
108 
109     /**
110      * The currently-configured authentication method. This determines how the authentication
111      * challenge needs to be completed in order to unlock an otherwise locked device.
112      *
113      * Note: there may be other ways to unlock the device that "bypass" the need for this
114      * authentication challenge (notably, biometrics like fingerprint or face unlock).
115      *
116      * Note: by design, this is a [Flow] and not a [StateFlow]; a consumer who wishes to get a
117      * snapshot of the current authentication method without establishing a collector of the flow
118      * can do so by invoking [getAuthenticationMethod].
119      */
120     val authenticationMethod: Flow<AuthenticationMethodModel>
121 
122     /** The minimal length of a pattern. */
123     val minPatternLength: Int
124 
125     /** The minimal length of a password. */
126     val minPasswordLength: Int
127 
128     /** Whether the "enhanced PIN privacy" setting is enabled for the current user. */
129     val isPinEnhancedPrivacyEnabled: StateFlow<Boolean>
130 
131     /**
132      * Checks the given [LockscreenCredential] to see if it's correct, returning an
133      * [AuthenticationResultModel] representing what happened.
134      */
135     suspend fun checkCredential(credential: LockscreenCredential): AuthenticationResultModel
136 
137     /**
138      * Returns the currently-configured authentication method. This determines how the
139      * authentication challenge needs to be completed in order to unlock an otherwise locked device.
140      *
141      * Note: there may be other ways to unlock the device that "bypass" the need for this
142      * authentication challenge (notably, biometrics like fingerprint or face unlock).
143      *
144      * Note: by design, this is offered as a convenience method alongside [authenticationMethod].
145      * The flow should be used for code that wishes to stay up-to-date its logic as the
146      * authentication changes over time and this method should be used for simple code that only
147      * needs to check the current value.
148      */
149     suspend fun getAuthenticationMethod(): AuthenticationMethodModel
150 
151     /** Returns the length of the PIN or `0` if the current auth method is not PIN. */
152     suspend fun getPinLength(): Int
153 
154     /** Reports an authentication attempt. */
155     suspend fun reportAuthenticationAttempt(isSuccessful: Boolean)
156 
157     /** Reports that the user has entered a temporary device lockout (throttling). */
158     suspend fun reportLockoutStarted(durationMs: Int)
159 
160     /**
161      * Returns the current maximum number of login attempts that are allowed before the device or
162      * profile is wiped.
163      *
164      * If there is no wipe policy, returns `0`.
165      *
166      * @see [DevicePolicyManager.getMaximumFailedPasswordsForWipe]
167      */
168     suspend fun getMaxFailedUnlockAttemptsForWipe(): Int
169 
170     /**
171      * Returns the user that will be wiped first when too many failed attempts are made to unlock
172      * the device by the selected user. That user is either the same as the current user ID or
173      * belongs to the same profile group.
174      *
175      * When there is no such policy, returns [UserHandle.USER_NULL]. E.g. managed profile user may
176      * be wiped as a result of failed primary profile password attempts when using unified
177      * challenge. Primary user may be wiped as a result of failed password attempts on the managed
178      * profile of an organization-owned device.
179      */
180     @UserIdInt suspend fun getProfileWithMinFailedUnlockAttemptsForWipe(): Int
181 }
182 
183 @SysUISingleton
184 class AuthenticationRepositoryImpl
185 @Inject
186 constructor(
187     @Application private val applicationScope: CoroutineScope,
188     @Background private val backgroundDispatcher: CoroutineDispatcher,
189     private val clock: SystemClock,
190     private val getSecurityMode: Function<Int, KeyguardSecurityModel.SecurityMode>,
191     private val userRepository: UserRepository,
192     private val lockPatternUtils: LockPatternUtils,
193     private val devicePolicyManager: DevicePolicyManager,
194     broadcastDispatcher: BroadcastDispatcher,
195     mobileConnectionsRepository: MobileConnectionsRepository,
196 ) : AuthenticationRepository {
197 
198     override val hintedPinLength: Int = 6
199 
200     override val isPatternVisible: StateFlow<Boolean> =
201         refreshingFlow(
202             initialValue = true,
203             getFreshValue = lockPatternUtils::isVisiblePatternEnabled,
204         )
205 
206     override val isAutoConfirmFeatureEnabled: StateFlow<Boolean> =
207         refreshingFlow(
208             initialValue = false,
209             getFreshValue = lockPatternUtils::isAutoPinConfirmEnabled,
210         )
211 
212     override val authenticationMethod: Flow<AuthenticationMethodModel> =
213         combine(userRepository.selectedUserInfo, mobileConnectionsRepository.isAnySimSecure) {
selectedUserInfonull214                 selectedUserInfo,
215                 _ ->
216                 selectedUserInfo.id
217             }
218             .flatMapLatest { selectedUserId ->
219                 broadcastDispatcher
220                     .broadcastFlow(
221                         filter =
222                             IntentFilter(
223                                 DevicePolicyManager.ACTION_DEVICE_POLICY_MANAGER_STATE_CHANGED
224                             ),
225                         user = UserHandle.of(selectedUserId),
226                     )
227                     .onStart { emit(Unit) }
228                     .map { selectedUserId }
229             }
230             .map(::getAuthenticationMethod)
231             .distinctUntilChanged()
232 
233     override val minPatternLength: Int = LockPatternUtils.MIN_LOCK_PATTERN_SIZE
234 
235     override val minPasswordLength: Int = LockPatternUtils.MIN_LOCK_PASSWORD_SIZE
236 
237     override val isPinEnhancedPrivacyEnabled: StateFlow<Boolean> =
238         refreshingFlow(
239             initialValue = true,
userIdnull240             getFreshValue = { userId -> lockPatternUtils.isPinEnhancedPrivacyEnabled(userId) },
241         )
242 
243     private val _failedAuthenticationAttempts = MutableStateFlow(0)
244     override val failedAuthenticationAttempts: StateFlow<Int> =
245         _failedAuthenticationAttempts.asStateFlow()
246 
247     override val lockoutEndTimestamp: Long?
248         get() =
<lambda>null249             lockPatternUtils.getLockoutAttemptDeadline(selectedUserId).takeIf {
250                 clock.elapsedRealtime() < it
251             }
252 
253     private val _hasLockoutOccurred = MutableStateFlow(false)
254     override val hasLockoutOccurred: StateFlow<Boolean> = _hasLockoutOccurred.asStateFlow()
255 
256     init {
257         if (SceneContainerFlag.isEnabled) {
258             // Hydrate failedAuthenticationAttempts initially and whenever the selected user
259             // changes.
<lambda>null260             applicationScope.launch {
261                 userRepository.selectedUserInfo.collect {
262                     _failedAuthenticationAttempts.value = getFailedAuthenticationAttemptCount()
263                 }
264             }
265         }
266     }
267 
checkCredentialnull268     override suspend fun checkCredential(
269         credential: LockscreenCredential
270     ): AuthenticationResultModel {
271         return withContext(backgroundDispatcher) {
272             try {
273                 val matched = lockPatternUtils.checkCredential(credential, selectedUserId) {}
274                 AuthenticationResultModel(isSuccessful = matched, lockoutDurationMs = 0)
275             } catch (ex: LockPatternUtils.RequestThrottledException) {
276                 AuthenticationResultModel(isSuccessful = false, lockoutDurationMs = ex.timeoutMs)
277             }
278         }
279     }
280 
getAuthenticationMethodnull281     override suspend fun getAuthenticationMethod(): AuthenticationMethodModel =
282         getAuthenticationMethod(selectedUserId)
283 
284     override suspend fun getPinLength(): Int {
285         return withContext(backgroundDispatcher) { lockPatternUtils.getPinLength(selectedUserId) }
286     }
287 
reportAuthenticationAttemptnull288     override suspend fun reportAuthenticationAttempt(isSuccessful: Boolean) {
289         withContext(backgroundDispatcher) {
290             if (isSuccessful) {
291                 lockPatternUtils.reportSuccessfulPasswordAttempt(selectedUserId)
292                 _hasLockoutOccurred.value = false
293             } else {
294                 lockPatternUtils.reportFailedPasswordAttempt(selectedUserId)
295             }
296             _failedAuthenticationAttempts.value = getFailedAuthenticationAttemptCount()
297         }
298     }
299 
reportLockoutStartednull300     override suspend fun reportLockoutStarted(durationMs: Int) {
301         lockPatternUtils.setLockoutAttemptDeadline(selectedUserId, durationMs)
302         withContext(backgroundDispatcher) {
303             lockPatternUtils.reportPasswordLockout(durationMs, selectedUserId)
304         }
305         _hasLockoutOccurred.value = true
306     }
307 
getFailedAuthenticationAttemptCountnull308     private suspend fun getFailedAuthenticationAttemptCount(): Int {
309         return withContext(backgroundDispatcher) {
310             lockPatternUtils.getCurrentFailedPasswordAttempts(selectedUserId)
311         }
312     }
313 
getMaxFailedUnlockAttemptsForWipenull314     override suspend fun getMaxFailedUnlockAttemptsForWipe(): Int {
315         return withContext(backgroundDispatcher) {
316             lockPatternUtils.getMaximumFailedPasswordsForWipe(selectedUserId)
317         }
318     }
319 
getProfileWithMinFailedUnlockAttemptsForWipenull320     override suspend fun getProfileWithMinFailedUnlockAttemptsForWipe(): Int {
321         return withContext(backgroundDispatcher) {
322             devicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(selectedUserId)
323         }
324     }
325 
326     private val selectedUserId: Int
327         @UserIdInt get() = userRepository.getSelectedUserInfo().id
328 
329     /**
330      * Returns a [StateFlow] that's automatically kept fresh. The passed-in [getFreshValue] is
331      * invoked on a background thread every time the selected user is changed and every time a new
332      * downstream subscriber is added to the flow.
333      *
334      * Initially, the flow will emit [initialValue] while it refreshes itself in the background by
335      * invoking the [getFreshValue] function and emitting the fresh value when that's done.
336      *
337      * Every time the selected user is changed, the flow will re-invoke [getFreshValue] and emit the
338      * new value.
339      *
340      * Every time a new downstream subscriber is added to the flow it first receives the latest
341      * cached value that's either the [initialValue] or the latest previously fetched value. In
342      * addition, adding a new downstream subscriber also triggers another [getFreshValue] call and a
343      * subsequent emission of that newest value.
344      */
refreshingFlownull345     private fun <T> refreshingFlow(
346         initialValue: T,
347         getFreshValue: suspend (selectedUserId: Int) -> T,
348     ): StateFlow<T> {
349         val flow = MutableStateFlow(initialValue)
350         applicationScope.launch {
351             combine(
352                     // Emits a value initially and every time the selected user is changed.
353                     userRepository.selectedUserInfo.map { it.id }.distinctUntilChanged(),
354                     // Emits a value only when the number of downstream subscribers of this flow
355                     // increases.
356                     flow.onSubscriberAdded(),
357                 ) { selectedUserId, _ ->
358                     selectedUserId
359                 }
360                 .collect { selectedUserId ->
361                     flow.value = withContext(backgroundDispatcher) { getFreshValue(selectedUserId) }
362                 }
363         }
364 
365         return flow.asStateFlow()
366     }
367 
368     /** Returns the authentication method for the given user ID. */
getAuthenticationMethodnull369     private suspend fun getAuthenticationMethod(@UserIdInt userId: Int): AuthenticationMethodModel {
370         return withContext(backgroundDispatcher) {
371             when (getSecurityMode.apply(userId)) {
372                 KeyguardSecurityModel.SecurityMode.PIN -> Pin
373                 KeyguardSecurityModel.SecurityMode.SimPin,
374                 KeyguardSecurityModel.SecurityMode.SimPuk -> Sim
375                 KeyguardSecurityModel.SecurityMode.Password -> Password
376                 KeyguardSecurityModel.SecurityMode.Pattern -> Pattern
377                 KeyguardSecurityModel.SecurityMode.None -> None
378                 KeyguardSecurityModel.SecurityMode.Invalid -> error("Invalid security mode!")
379             }
380         }
381     }
382 }
383 
384 @Module
385 interface AuthenticationRepositoryModule {
repositorynull386     @Binds fun repository(impl: AuthenticationRepositoryImpl): AuthenticationRepository
387 }
388