1 /* 2 * 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 package com.android.systemui.authentication.domain.interactor 18 19 import com.android.internal.widget.LockPatternView 20 import com.android.internal.widget.LockscreenCredential 21 import com.android.systemui.authentication.data.model.AuthenticationMethodModel as DataLayerAuthenticationMethodModel 22 import com.android.systemui.authentication.data.repository.AuthenticationRepository 23 import com.android.systemui.authentication.domain.model.AuthenticationMethodModel as DomainLayerAuthenticationMethodModel 24 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate 25 import com.android.systemui.authentication.shared.model.AuthenticationThrottlingModel 26 import com.android.systemui.dagger.SysUISingleton 27 import com.android.systemui.dagger.qualifiers.Application 28 import com.android.systemui.dagger.qualifiers.Background 29 import com.android.systemui.keyguard.data.repository.KeyguardRepository 30 import com.android.systemui.scene.domain.interactor.SceneInteractor 31 import com.android.systemui.scene.shared.model.SceneKey 32 import com.android.systemui.user.data.repository.UserRepository 33 import com.android.systemui.util.time.SystemClock 34 import javax.inject.Inject 35 import kotlin.math.max 36 import kotlin.time.Duration.Companion.seconds 37 import kotlinx.coroutines.CoroutineDispatcher 38 import kotlinx.coroutines.CoroutineScope 39 import kotlinx.coroutines.Job 40 import kotlinx.coroutines.async 41 import kotlinx.coroutines.delay 42 import kotlinx.coroutines.flow.Flow 43 import kotlinx.coroutines.flow.SharingStarted 44 import kotlinx.coroutines.flow.StateFlow 45 import kotlinx.coroutines.flow.combine 46 import kotlinx.coroutines.flow.distinctUntilChanged 47 import kotlinx.coroutines.flow.filter 48 import kotlinx.coroutines.flow.map 49 import kotlinx.coroutines.flow.stateIn 50 import kotlinx.coroutines.launch 51 import kotlinx.coroutines.withContext 52 53 /** Hosts application business logic related to authentication. */ 54 @SysUISingleton 55 class AuthenticationInteractor 56 @Inject 57 constructor( 58 @Application private val applicationScope: CoroutineScope, 59 private val repository: AuthenticationRepository, 60 @Background private val backgroundDispatcher: CoroutineDispatcher, 61 private val userRepository: UserRepository, 62 private val keyguardRepository: KeyguardRepository, 63 sceneInteractor: SceneInteractor, 64 private val clock: SystemClock, 65 ) { 66 /** 67 * The currently-configured authentication method. This determines how the authentication 68 * challenge needs to be completed in order to unlock an otherwise locked device. 69 * 70 * Note: there may be other ways to unlock the device that "bypass" the need for this 71 * authentication challenge (notably, biometrics like fingerprint or face unlock). 72 * 73 * Note: by design, this is a [Flow] and not a [StateFlow]; a consumer who wishes to get a 74 * snapshot of the current authentication method without establishing a collector of the flow 75 * can do so by invoking [getAuthenticationMethod]. 76 * 77 * Note: this layer adds the synthetic authentication method of "swipe" which is special. When 78 * the current authentication method is "swipe", the user does not need to complete any 79 * authentication challenge to unlock the device; they just need to dismiss the lockscreen to 80 * get past it. This also means that the value of [isUnlocked] remains `false` even when the 81 * lockscreen is showing and still needs to be dismissed by the user to proceed. 82 */ 83 val authenticationMethod: Flow<DomainLayerAuthenticationMethodModel> = 84 repository.authenticationMethod.map { rawModel -> rawModel.toDomainLayer() } 85 86 /** 87 * Whether the device is unlocked. 88 * 89 * A device that is not yet unlocked requires unlocking by completing an authentication 90 * challenge according to the current authentication method, unless in cases when the current 91 * authentication method is not "secure" (for example, None and Swipe); in such cases, the value 92 * of this flow will always be `true`, even if the lockscreen is showing and still needs to be 93 * dismissed by the user to proceed. 94 */ 95 val isUnlocked: StateFlow<Boolean> = 96 combine( 97 repository.isUnlocked, 98 authenticationMethod, 99 ) { isUnlocked, authenticationMethod -> 100 !authenticationMethod.isSecure || isUnlocked 101 } 102 .stateIn( 103 scope = applicationScope, 104 started = SharingStarted.Eagerly, 105 initialValue = false, 106 ) 107 108 /** 109 * Whether the lockscreen has been dismissed (by any method). This can be false even when the 110 * device is unlocked, e.g. when swipe to unlock is enabled. 111 * 112 * Note: 113 * - `false` doesn't mean the lockscreen is visible (it may be occluded or covered by other UI). 114 * - `true` doesn't mean the lockscreen is invisible (since this state changes before the 115 * transition occurs). 116 */ 117 val isLockscreenDismissed: StateFlow<Boolean> = 118 sceneInteractor.desiredScene 119 .map { it.key } 120 .filter { currentScene -> 121 currentScene == SceneKey.Gone || currentScene == SceneKey.Lockscreen 122 } 123 .map { it == SceneKey.Gone } 124 .stateIn( 125 scope = applicationScope, 126 started = SharingStarted.WhileSubscribed(), 127 initialValue = false, 128 ) 129 130 /** 131 * Whether it's currently possible to swipe up to dismiss the lockscreen without requiring 132 * authentication. This returns false whenever the lockscreen has been dismissed. 133 * 134 * Note: `true` doesn't mean the lockscreen is visible. It may be occluded or covered by other 135 * UI. 136 */ 137 val canSwipeToDismiss = 138 combine(authenticationMethod, isLockscreenDismissed) { 139 authenticationMethod, 140 isLockscreenDismissed -> 141 authenticationMethod is DomainLayerAuthenticationMethodModel.Swipe && 142 !isLockscreenDismissed 143 } 144 .stateIn( 145 scope = applicationScope, 146 started = SharingStarted.WhileSubscribed(), 147 initialValue = false, 148 ) 149 150 /** The current authentication throttling state, only meaningful if [isThrottled] is `true`. */ 151 val throttling: StateFlow<AuthenticationThrottlingModel> = repository.throttling 152 153 /** 154 * Whether currently throttled and the user has to wait before being able to try another 155 * authentication attempt. 156 */ 157 val isThrottled: StateFlow<Boolean> = 158 throttling 159 .map { it.remainingMs > 0 } 160 .stateIn( 161 scope = applicationScope, 162 started = SharingStarted.Eagerly, 163 initialValue = throttling.value.remainingMs > 0, 164 ) 165 166 /** The length of the hinted PIN, or `null` if pin length hint should not be shown. */ 167 val hintedPinLength: StateFlow<Int?> = 168 repository.isAutoConfirmEnabled 169 .map { isAutoConfirmEnabled -> 170 repository.getPinLength().takeIf { 171 isAutoConfirmEnabled && it == repository.hintedPinLength 172 } 173 } 174 .stateIn( 175 scope = applicationScope, 176 // Make sure this is kept as WhileSubscribed or we can run into a bug where the 177 // downstream continues to receive old/stale/cached values. 178 started = SharingStarted.WhileSubscribed(), 179 initialValue = null, 180 ) 181 182 /** Whether the auto confirm feature is enabled for the currently-selected user. */ 183 val isAutoConfirmEnabled: StateFlow<Boolean> = repository.isAutoConfirmEnabled 184 185 /** Whether the pattern should be visible for the currently-selected user. */ 186 val isPatternVisible: StateFlow<Boolean> = repository.isPatternVisible 187 188 private var throttlingCountdownJob: Job? = null 189 190 init { 191 applicationScope.launch { 192 userRepository.selectedUserInfo 193 .map { it.id } 194 .distinctUntilChanged() 195 .collect { onSelectedUserChanged() } 196 } 197 } 198 199 /** 200 * Returns the currently-configured authentication method. This determines how the 201 * authentication challenge needs to be completed in order to unlock an otherwise locked device. 202 * 203 * Note: there may be other ways to unlock the device that "bypass" the need for this 204 * authentication challenge (notably, biometrics like fingerprint or face unlock). 205 * 206 * Note: by design, this is offered as a convenience method alongside [authenticationMethod]. 207 * The flow should be used for code that wishes to stay up-to-date its logic as the 208 * authentication changes over time and this method should be used for simple code that only 209 * needs to check the current value. 210 * 211 * Note: this layer adds the synthetic authentication method of "swipe" which is special. When 212 * the current authentication method is "swipe", the user does not need to complete any 213 * authentication challenge to unlock the device; they just need to dismiss the lockscreen to 214 * get past it. This also means that the value of [isUnlocked] remains `false` even when the 215 * lockscreen is showing and still needs to be dismissed by the user to proceed. 216 */ 217 suspend fun getAuthenticationMethod(): DomainLayerAuthenticationMethodModel { 218 return repository.getAuthenticationMethod().toDomainLayer() 219 } 220 221 /** 222 * Returns `true` if the device currently requires authentication before content can be viewed; 223 * `false` if content can be displayed without unlocking first. 224 */ 225 suspend fun isAuthenticationRequired(): Boolean { 226 return !isUnlocked.value && getAuthenticationMethod().isSecure 227 } 228 229 /** 230 * Whether lock screen bypass is enabled. When enabled, the lock screen will be automatically 231 * dismisses once the authentication challenge is completed. For example, completing a biometric 232 * authentication challenge via face unlock or fingerprint sensor can automatically bypass the 233 * lock screen. 234 */ 235 fun isBypassEnabled(): Boolean { 236 return keyguardRepository.isBypassEnabled() 237 } 238 239 /** 240 * Attempts to authenticate the user and unlock the device. 241 * 242 * If [tryAutoConfirm] is `true`, authentication is attempted if and only if the auth method 243 * supports auto-confirming, and the input's length is at least the code's length. Otherwise, 244 * `null` is returned. 245 * 246 * @param input The input from the user to try to authenticate with. This can be a list of 247 * different things, based on the current authentication method. 248 * @param tryAutoConfirm `true` if called while the user inputs the code, without an explicit 249 * request to validate. 250 * @return `true` if the authentication succeeded and the device is now unlocked; `false` when 251 * authentication failed, `null` if the check was not performed. 252 */ 253 suspend fun authenticate(input: List<Any>, tryAutoConfirm: Boolean = false): Boolean? { 254 if (input.isEmpty()) { 255 throw IllegalArgumentException("Input was empty!") 256 } 257 258 val skipCheck = 259 when { 260 // We're being throttled, the UI layer should not have called this; skip the 261 // attempt. 262 isThrottled.value -> true 263 // Auto-confirm attempt when the feature is not enabled; skip the attempt. 264 tryAutoConfirm && !isAutoConfirmEnabled.value -> true 265 // Auto-confirm should skip the attempt if the pin entered is too short. 266 tryAutoConfirm && input.size < repository.getPinLength() -> true 267 else -> false 268 } 269 if (skipCheck) { 270 return null 271 } 272 273 // Attempt to authenticate: 274 val authMethod = getAuthenticationMethod() 275 val credential = authMethod.createCredential(input) ?: return null 276 val authenticationResult = repository.checkCredential(credential) 277 credential.zeroize() 278 279 if (authenticationResult.isSuccessful || !tryAutoConfirm) { 280 repository.reportAuthenticationAttempt( 281 isSuccessful = authenticationResult.isSuccessful, 282 ) 283 } 284 285 // Check if we need to throttle and, if so, kick off the throttle countdown: 286 if (!authenticationResult.isSuccessful && authenticationResult.throttleDurationMs > 0) { 287 repository.setThrottleDuration( 288 durationMs = authenticationResult.throttleDurationMs, 289 ) 290 startThrottlingCountdown() 291 } 292 293 if (authenticationResult.isSuccessful) { 294 // Since authentication succeeded, we should refresh throttling to make sure that our 295 // state is completely reflecting the upstream source of truth. 296 refreshThrottling() 297 } 298 299 return authenticationResult.isSuccessful 300 } 301 302 /** Starts refreshing the throttling state every second. */ 303 private suspend fun startThrottlingCountdown() { 304 cancelCountdown() 305 throttlingCountdownJob = 306 applicationScope.launch { 307 while (refreshThrottling() > 0) { 308 delay(1.seconds.inWholeMilliseconds) 309 } 310 } 311 } 312 313 /** Cancels any throttling state countdown started in [startThrottlingCountdown]. */ 314 private fun cancelCountdown() { 315 throttlingCountdownJob?.cancel() 316 throttlingCountdownJob = null 317 } 318 319 /** Notifies that the currently-selected user has changed. */ 320 private suspend fun onSelectedUserChanged() { 321 cancelCountdown() 322 if (refreshThrottling() > 0) { 323 startThrottlingCountdown() 324 } 325 } 326 327 /** 328 * Refreshes the throttling state, hydrating the repository with the latest state. 329 * 330 * @return The remaining time for the current throttling countdown, in milliseconds or `0` if 331 * not being throttled. 332 */ 333 private suspend fun refreshThrottling(): Long { 334 return withContext(backgroundDispatcher) { 335 val failedAttemptCount = async { repository.getFailedAuthenticationAttemptCount() } 336 val deadline = async { repository.getThrottlingEndTimestamp() } 337 val remainingMs = max(0, deadline.await() - clock.elapsedRealtime()) 338 repository.setThrottling( 339 AuthenticationThrottlingModel( 340 failedAttemptCount = failedAttemptCount.await(), 341 remainingMs = remainingMs.toInt(), 342 ), 343 ) 344 remainingMs 345 } 346 } 347 348 private fun DomainLayerAuthenticationMethodModel.createCredential( 349 input: List<Any> 350 ): LockscreenCredential? { 351 return when (this) { 352 is DomainLayerAuthenticationMethodModel.Pin -> 353 LockscreenCredential.createPin(input.joinToString("")) 354 is DomainLayerAuthenticationMethodModel.Password -> 355 LockscreenCredential.createPassword(input.joinToString("")) 356 is DomainLayerAuthenticationMethodModel.Pattern -> 357 LockscreenCredential.createPattern( 358 input 359 .map { it as AuthenticationPatternCoordinate } 360 .map { LockPatternView.Cell.of(it.y, it.x) } 361 ) 362 else -> null 363 } 364 } 365 366 private suspend fun DataLayerAuthenticationMethodModel.toDomainLayer(): 367 DomainLayerAuthenticationMethodModel { 368 return when (this) { 369 is DataLayerAuthenticationMethodModel.None -> 370 if (repository.isLockscreenEnabled()) { 371 DomainLayerAuthenticationMethodModel.Swipe 372 } else { 373 DomainLayerAuthenticationMethodModel.None 374 } 375 is DataLayerAuthenticationMethodModel.Pin -> DomainLayerAuthenticationMethodModel.Pin 376 is DataLayerAuthenticationMethodModel.Password -> 377 DomainLayerAuthenticationMethodModel.Password 378 is DataLayerAuthenticationMethodModel.Pattern -> 379 DomainLayerAuthenticationMethodModel.Pattern 380 } 381 } 382 } 383