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