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