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