1 /*
<lambda>null2  * Copyright 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.data.repository
18 
19 import android.os.UserHandle
20 import com.android.internal.widget.LockPatternUtils
21 import com.android.internal.widget.LockPatternView
22 import com.android.internal.widget.LockscreenCredential
23 import com.android.keyguard.KeyguardSecurityModel.SecurityMode
24 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
25 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate
26 import com.android.systemui.authentication.shared.model.AuthenticationResultModel
27 import com.android.systemui.dagger.SysUISingleton
28 import dagger.Binds
29 import dagger.Module
30 import dagger.Provides
31 import kotlinx.coroutines.ExperimentalCoroutinesApi
32 import kotlinx.coroutines.flow.MutableStateFlow
33 import kotlinx.coroutines.flow.StateFlow
34 import kotlinx.coroutines.flow.asStateFlow
35 import kotlinx.coroutines.sync.Mutex
36 import kotlinx.coroutines.sync.withLock
37 import kotlinx.coroutines.test.TestScope
38 import kotlinx.coroutines.test.currentTime
39 
40 class FakeAuthenticationRepository(
41     private val currentTime: () -> Long,
42 ) : AuthenticationRepository {
43 
44     override val hintedPinLength: Int = HINTING_PIN_LENGTH
45 
46     private val _isPatternVisible = MutableStateFlow(true)
47     override val isPatternVisible: StateFlow<Boolean> = _isPatternVisible.asStateFlow()
48 
49     override val hasLockoutOccurred = MutableStateFlow(false)
50 
51     private val _isAutoConfirmFeatureEnabled = MutableStateFlow(false)
52     override val isAutoConfirmFeatureEnabled: StateFlow<Boolean> =
53         _isAutoConfirmFeatureEnabled.asStateFlow()
54 
55     private val _authenticationMethod =
56         MutableStateFlow<AuthenticationMethodModel>(DEFAULT_AUTHENTICATION_METHOD)
57     override val authenticationMethod: StateFlow<AuthenticationMethodModel> =
58         _authenticationMethod.asStateFlow()
59 
60     override val minPatternLength: Int = 4
61 
62     override val minPasswordLength: Int = 4
63 
64     private val _isPinEnhancedPrivacyEnabled = MutableStateFlow(false)
65     override val isPinEnhancedPrivacyEnabled: StateFlow<Boolean> =
66         _isPinEnhancedPrivacyEnabled.asStateFlow()
67 
68     private var credentialOverride: List<Any>? = null
69     private var securityMode: SecurityMode = DEFAULT_AUTHENTICATION_METHOD.toSecurityMode()
70 
71     var lockoutStartedReportCount = 0
72 
73     private val credentialCheckingMutex = Mutex(locked = false)
74 
75     override suspend fun getAuthenticationMethod(): AuthenticationMethodModel {
76         return authenticationMethod.value
77     }
78 
79     fun setAuthenticationMethod(authenticationMethod: AuthenticationMethodModel) {
80         _authenticationMethod.value = authenticationMethod
81         securityMode = authenticationMethod.toSecurityMode()
82     }
83 
84     fun overrideCredential(pin: List<Int>) {
85         credentialOverride = pin
86     }
87 
88     override suspend fun reportAuthenticationAttempt(isSuccessful: Boolean) {
89         if (isSuccessful) {
90             _failedAuthenticationAttempts.value = 0
91             _lockoutEndTimestamp = null
92             hasLockoutOccurred.value = false
93             lockoutStartedReportCount = 0
94         } else {
95             _failedAuthenticationAttempts.value++
96         }
97     }
98 
99     private var _failedAuthenticationAttempts = MutableStateFlow(0)
100     override val failedAuthenticationAttempts: StateFlow<Int> =
101         _failedAuthenticationAttempts.asStateFlow()
102 
103     private var _lockoutEndTimestamp: Long? = null
104     override val lockoutEndTimestamp: Long?
105         get() = if (currentTime() < (_lockoutEndTimestamp ?: 0)) _lockoutEndTimestamp else null
106 
107     override suspend fun reportLockoutStarted(durationMs: Int) {
108         _lockoutEndTimestamp = (currentTime() + durationMs).takeIf { durationMs > 0 }
109         hasLockoutOccurred.value = true
110         lockoutStartedReportCount++
111     }
112 
113     override suspend fun getMaxFailedUnlockAttemptsForWipe(): Int =
114         MAX_FAILED_AUTH_TRIES_BEFORE_WIPE
115 
116     var profileWithMinFailedUnlockAttemptsForWipe: Int = UserHandle.USER_SYSTEM
117     override suspend fun getProfileWithMinFailedUnlockAttemptsForWipe(): Int =
118         profileWithMinFailedUnlockAttemptsForWipe
119 
120     override suspend fun getPinLength(): Int {
121         return (credentialOverride ?: DEFAULT_PIN).size
122     }
123 
124     fun setAutoConfirmFeatureEnabled(isEnabled: Boolean) {
125         _isAutoConfirmFeatureEnabled.value = isEnabled
126     }
127 
128     override suspend fun checkCredential(
129         credential: LockscreenCredential
130     ): AuthenticationResultModel {
131         return credentialCheckingMutex.withLock {
132             val expectedCredential = credentialOverride ?: getExpectedCredential(securityMode)
133             val isSuccessful =
134                 when {
135                     credential.type != getCurrentCredentialType(securityMode) -> false
136                     credential.type == LockPatternUtils.CREDENTIAL_TYPE_PIN ->
137                         credential.isPin && credential.matches(expectedCredential)
138                     credential.type == LockPatternUtils.CREDENTIAL_TYPE_PASSWORD ->
139                         credential.isPassword && credential.matches(expectedCredential)
140                     credential.type == LockPatternUtils.CREDENTIAL_TYPE_PATTERN ->
141                         credential.isPattern && credential.matches(expectedCredential)
142                     else -> error("Unexpected credential type ${credential.type}!")
143                 }
144 
145             val failedAttempts = _failedAuthenticationAttempts.value
146             if (isSuccessful || failedAttempts < MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
147                 AuthenticationResultModel(
148                     isSuccessful = isSuccessful,
149                     lockoutDurationMs = 0,
150                 )
151             } else {
152                 AuthenticationResultModel(
153                     isSuccessful = false,
154                     lockoutDurationMs = LOCKOUT_DURATION_MS,
155                 )
156             }
157         }
158     }
159 
160     fun setPinEnhancedPrivacyEnabled(isEnabled: Boolean) {
161         _isPinEnhancedPrivacyEnabled.value = isEnabled
162     }
163 
164     /**
165      * Pauses any future credential checking. The test must call [unpauseCredentialChecking] to
166      * flush the accumulated credential checks.
167      */
168     suspend fun pauseCredentialChecking() {
169         credentialCheckingMutex.lock()
170     }
171 
172     /**
173      * Unpauses future credential checking, if it was paused using [pauseCredentialChecking]. This
174      * doesn't flush any pending coroutine jobs; the test code may still choose to do that using
175      * `runCurrent`.
176      */
177     fun unpauseCredentialChecking() {
178         credentialCheckingMutex.unlock()
179     }
180 
181     private fun getExpectedCredential(securityMode: SecurityMode): List<Any> {
182         return when (val credentialType = getCurrentCredentialType(securityMode)) {
183             LockPatternUtils.CREDENTIAL_TYPE_PIN -> credentialOverride ?: DEFAULT_PIN
184             LockPatternUtils.CREDENTIAL_TYPE_PASSWORD -> "password".toList()
185             LockPatternUtils.CREDENTIAL_TYPE_PATTERN -> PATTERN.toCells()
186             else -> error("Unsupported credential type $credentialType!")
187         }
188     }
189 
190     companion object {
191         val DEFAULT_AUTHENTICATION_METHOD = AuthenticationMethodModel.Pin
192         val PATTERN =
193             listOf(
194                 AuthenticationPatternCoordinate(2, 0),
195                 AuthenticationPatternCoordinate(2, 1),
196                 AuthenticationPatternCoordinate(2, 2),
197                 AuthenticationPatternCoordinate(1, 1),
198                 AuthenticationPatternCoordinate(0, 0),
199                 AuthenticationPatternCoordinate(0, 1),
200                 AuthenticationPatternCoordinate(0, 2),
201             )
202         const val MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT = 5
203         const val MAX_FAILED_AUTH_TRIES_BEFORE_WIPE =
204             MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT +
205                 LockPatternUtils.FAILED_ATTEMPTS_BEFORE_WIPE_GRACE
206         const val LOCKOUT_DURATION_SECONDS = 30
207         const val LOCKOUT_DURATION_MS = LOCKOUT_DURATION_SECONDS * 1000
208         const val HINTING_PIN_LENGTH = 6
209         val DEFAULT_PIN = buildList { repeat(HINTING_PIN_LENGTH) { add(it + 1) } }
210 
211         private fun AuthenticationMethodModel.toSecurityMode(): SecurityMode {
212             return when (this) {
213                 is AuthenticationMethodModel.Pin -> SecurityMode.PIN
214                 is AuthenticationMethodModel.Password -> SecurityMode.Password
215                 is AuthenticationMethodModel.Pattern -> SecurityMode.Pattern
216                 is AuthenticationMethodModel.None -> SecurityMode.None
217                 is AuthenticationMethodModel.Sim -> SecurityMode.SimPin
218             }
219         }
220 
221         @LockPatternUtils.CredentialType
222         private fun getCurrentCredentialType(
223             securityMode: SecurityMode,
224         ): Int {
225             return when (securityMode) {
226                 SecurityMode.PIN,
227                 SecurityMode.SimPin,
228                 SecurityMode.SimPuk -> LockPatternUtils.CREDENTIAL_TYPE_PIN
229                 SecurityMode.Password -> LockPatternUtils.CREDENTIAL_TYPE_PASSWORD
230                 SecurityMode.Pattern -> LockPatternUtils.CREDENTIAL_TYPE_PATTERN
231                 SecurityMode.None -> LockPatternUtils.CREDENTIAL_TYPE_NONE
232                 else -> error("Unsupported SecurityMode $securityMode!")
233             }
234         }
235 
236         private fun LockscreenCredential.matches(expectedCredential: List<Any>): Boolean {
237             @Suppress("UNCHECKED_CAST")
238             return when {
239                 isPin ->
240                     credential.map { byte -> byte.toInt().toChar() - '0' } == expectedCredential
241                 isPassword -> credential.map { byte -> byte.toInt().toChar() } == expectedCredential
242                 isPattern ->
243                     credential.contentEquals(
244                         LockPatternUtils.patternToByteArray(
245                             expectedCredential as List<LockPatternView.Cell>
246                         )
247                     )
248                 else -> error("Unsupported credential type $type!")
249             }
250         }
251 
252         private fun List<AuthenticationPatternCoordinate>.toCells(): List<LockPatternView.Cell> {
253             return map { coordinate -> LockPatternView.Cell.of(coordinate.y, coordinate.x) }
254         }
255     }
256 }
257 
258 @OptIn(ExperimentalCoroutinesApi::class)
259 @Module(includes = [FakeAuthenticationRepositoryModule.Bindings::class])
260 object FakeAuthenticationRepositoryModule {
261     @Provides
262     @SysUISingleton
provideFakenull263     fun provideFake(
264         scope: TestScope,
265     ) = FakeAuthenticationRepository(currentTime = { scope.currentTime })
266 
267     @Module
268     interface Bindings {
bindFakenull269         @Binds fun bindFake(fake: FakeAuthenticationRepository): AuthenticationRepository
270     }
271 }
272