1 /* <lambda>null2 * 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 android.os.UserHandle 20 import com.android.app.tracing.coroutines.launchTraced as launch 21 import com.android.internal.widget.LockPatternUtils 22 import com.android.internal.widget.LockPatternView 23 import com.android.internal.widget.LockscreenCredential 24 import com.android.systemui.authentication.data.repository.AuthenticationRepository 25 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel 26 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Password 27 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern 28 import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin 29 import com.android.systemui.authentication.shared.model.AuthenticationPatternCoordinate 30 import com.android.systemui.authentication.shared.model.AuthenticationWipeModel 31 import com.android.systemui.authentication.shared.model.AuthenticationWipeModel.WipeTarget 32 import com.android.systemui.dagger.SysUISingleton 33 import com.android.systemui.dagger.qualifiers.Application 34 import com.android.systemui.dagger.qualifiers.Background 35 import com.android.systemui.log.table.TableLogBuffer 36 import com.android.systemui.log.table.logDiffsForTable 37 import com.android.systemui.scene.domain.SceneFrameworkTableLog 38 import com.android.systemui.user.domain.interactor.SelectedUserInteractor 39 import com.android.systemui.util.time.SystemClock 40 import javax.inject.Inject 41 import kotlin.math.max 42 import kotlin.time.Duration 43 import kotlin.time.Duration.Companion.seconds 44 import kotlinx.coroutines.CoroutineDispatcher 45 import kotlinx.coroutines.CoroutineScope 46 import kotlinx.coroutines.delay 47 import kotlinx.coroutines.flow.Flow 48 import kotlinx.coroutines.flow.MutableSharedFlow 49 import kotlinx.coroutines.flow.SharedFlow 50 import kotlinx.coroutines.flow.SharingStarted 51 import kotlinx.coroutines.flow.StateFlow 52 import kotlinx.coroutines.flow.asSharedFlow 53 import kotlinx.coroutines.flow.combine 54 import kotlinx.coroutines.flow.map 55 import kotlinx.coroutines.flow.stateIn 56 57 /** 58 * Hosts application business logic related to user authentication. 59 * 60 * Note: there is a distinction between authentication (determining a user's identity) and device 61 * entry (dismissing the lockscreen). For logic that is specific to device entry, please use 62 * `DeviceEntryInteractor` instead. 63 */ 64 @SysUISingleton 65 class AuthenticationInteractor 66 @Inject 67 constructor( 68 @Application private val applicationScope: CoroutineScope, 69 @Background private val backgroundDispatcher: CoroutineDispatcher, 70 private val repository: AuthenticationRepository, 71 private val selectedUserInteractor: SelectedUserInteractor, 72 @SceneFrameworkTableLog private val tableLogBuffer: TableLogBuffer, 73 ) { 74 /** 75 * The currently-configured authentication method. This determines how the authentication 76 * challenge needs to be completed in order to unlock an otherwise locked device. 77 * 78 * Note: there may be other ways to unlock the device that "bypass" the need for this 79 * authentication challenge (notably, biometrics like fingerprint or face unlock). 80 * 81 * Note: by design, this is a [Flow] and not a [StateFlow]; a consumer who wishes to get a 82 * snapshot of the current authentication method without establishing a collector of the flow 83 * can do so by invoking [getAuthenticationMethod]. 84 * 85 * Note: this layer adds the synthetic authentication method of "swipe" which is special. When 86 * the current authentication method is "swipe", the user does not need to complete any 87 * authentication challenge to unlock the device; they just need to dismiss the lockscreen to 88 * get past it. This also means that the value of `DeviceEntryInteractor#isUnlocked` remains 89 * `true` even when the lockscreen is showing and still needs to be dismissed by the user to 90 * proceed. 91 */ 92 val authenticationMethod: Flow<AuthenticationMethodModel> = 93 repository.authenticationMethod.logDiffsForTable( 94 tableLogBuffer = tableLogBuffer, 95 initialValue = AuthenticationMethodModel.None, 96 ) 97 98 /** 99 * Whether the auto confirm feature is enabled for the currently-selected user. 100 * 101 * Note that the length of the PIN is also important to take into consideration, please see 102 * [hintedPinLength]. 103 */ 104 val isAutoConfirmEnabled: StateFlow<Boolean> = 105 combine(repository.isAutoConfirmFeatureEnabled, repository.hasLockoutOccurred) { 106 featureEnabled, 107 hasLockoutOccurred -> 108 // Disable auto-confirm if lockout occurred since the last successful 109 // authentication attempt. 110 featureEnabled && !hasLockoutOccurred 111 } 112 .stateIn( 113 scope = applicationScope, 114 started = SharingStarted.WhileSubscribed(), 115 initialValue = false, 116 ) 117 118 /** The length of the hinted PIN, or `null` if pin length hint should not be shown. */ 119 val hintedPinLength: StateFlow<Int?> = 120 isAutoConfirmEnabled 121 .map { isAutoConfirmEnabled -> 122 repository.getPinLength().takeIf { 123 isAutoConfirmEnabled && it == repository.hintedPinLength 124 } 125 } 126 .stateIn( 127 scope = applicationScope, 128 // Make sure this is kept as WhileSubscribed or we can run into a bug where the 129 // downstream continues to receive old/stale/cached values. 130 started = SharingStarted.WhileSubscribed(), 131 initialValue = null, 132 ) 133 134 /** Whether the pattern should be visible for the currently-selected user. */ 135 val isPatternVisible: StateFlow<Boolean> = repository.isPatternVisible 136 137 private val _onAuthenticationResult = MutableSharedFlow<Boolean>() 138 /** 139 * Emits the outcome (successful or unsuccessful) whenever a PIN/Pattern/Password security 140 * challenge is attempted by the user in order to unlock the device. 141 */ 142 val onAuthenticationResult: SharedFlow<Boolean> = _onAuthenticationResult.asSharedFlow() 143 144 /** Whether the "enhanced PIN privacy" setting is enabled for the current user. */ 145 val isPinEnhancedPrivacyEnabled: StateFlow<Boolean> = repository.isPinEnhancedPrivacyEnabled 146 147 /** 148 * The number of failed authentication attempts for the selected user since the last successful 149 * authentication. 150 */ 151 val failedAuthenticationAttempts: StateFlow<Int> = repository.failedAuthenticationAttempts 152 153 /** 154 * Timestamp for when the current lockout (aka "throttling") will end, allowing the user to 155 * attempt authentication again. Returns `null` if no lockout is active. 156 * 157 * To be notified whenever a lockout is started, the caller should subscribe to 158 * [onAuthenticationResult]. 159 * 160 * Note that the value is in milliseconds and matches [SystemClock.elapsedRealtime]. 161 * 162 * Also note that the value may change when the selected user is changed. 163 */ 164 val lockoutEndTimestamp: Long? 165 get() = repository.lockoutEndTimestamp 166 167 /** 168 * Models an imminent wipe risk to the user, profile, or device upon further unsuccessful 169 * authentication attempts. 170 * 171 * Returns `null` when there is no risk of wipe yet, or when there's no wipe policy set by the 172 * DevicePolicyManager. 173 */ 174 val upcomingWipe: Flow<AuthenticationWipeModel?> = 175 repository.failedAuthenticationAttempts.map { failedAttempts -> 176 val failedAttemptsBeforeWipe = repository.getMaxFailedUnlockAttemptsForWipe() 177 if (failedAttemptsBeforeWipe == 0) { 178 return@map null // There is no restriction. 179 } 180 181 // The user has a DevicePolicyManager that requests a user/profile to be wiped after N 182 // attempts. Once the grace period is reached, show a dialog every time as a clear 183 // warning until the deletion fires. 184 val remainingAttemptsBeforeWipe = max(0, failedAttemptsBeforeWipe - failedAttempts) 185 if (remainingAttemptsBeforeWipe >= LockPatternUtils.FAILED_ATTEMPTS_BEFORE_WIPE_GRACE) { 186 return@map null // There is no current risk of wiping the device. 187 } 188 189 AuthenticationWipeModel( 190 wipeTarget = getWipeTarget(), 191 failedAttempts = failedAttempts, 192 remainingAttempts = remainingAttemptsBeforeWipe, 193 ) 194 } 195 196 /** 197 * Returns the currently-configured authentication method. This determines how the 198 * authentication challenge needs to be completed in order to unlock an otherwise locked device. 199 * 200 * Note: there may be other ways to unlock the device that "bypass" the need for this 201 * authentication challenge (notably, biometrics like fingerprint or face unlock). 202 * 203 * Note: by design, this is offered as a convenience method alongside [authenticationMethod]. 204 * The flow should be used for code that wishes to stay up-to-date its logic as the 205 * authentication changes over time and this method should be used for simple code that only 206 * needs to check the current value. 207 */ 208 suspend fun getAuthenticationMethod() = repository.getAuthenticationMethod() 209 210 /** 211 * Attempts to authenticate the user and unlock the device. May trigger lockout or wipe the 212 * user/profile/device data upon failure. 213 * 214 * If [tryAutoConfirm] is `true`, authentication is attempted if and only if the auth method 215 * supports auto-confirming, and the input's length is at least the required length. Otherwise, 216 * `AuthenticationResult.SKIPPED` is returned. 217 * 218 * @param input The input from the user to try to authenticate with. This can be a list of 219 * different things, based on the current authentication method. 220 * @param tryAutoConfirm `true` if called while the user inputs the code, without an explicit 221 * request to validate. 222 * @return The result of this authentication attempt. 223 */ 224 suspend fun authenticate( 225 input: List<Any>, 226 tryAutoConfirm: Boolean = false, 227 ): AuthenticationResult { 228 if (input.isEmpty()) { 229 throw IllegalArgumentException("Input was empty!") 230 } 231 232 val authMethod = getAuthenticationMethod() 233 if (shouldSkipAuthenticationAttempt(authMethod, tryAutoConfirm, input.size)) { 234 return AuthenticationResult.SKIPPED 235 } 236 237 // Attempt to authenticate: 238 val credential = authMethod.createCredential(input) ?: return AuthenticationResult.SKIPPED 239 val authenticationResult = repository.checkCredential(credential) 240 credential.zeroize() 241 242 if (authenticationResult.isSuccessful) { 243 repository.reportAuthenticationAttempt(isSuccessful = true) 244 _onAuthenticationResult.emit(true) 245 246 // Force a garbage collection in an attempt to erase any credentials left in memory. 247 // Do it after a 5-sec delay to avoid making the bouncer dismiss animation janky. 248 initiateGarbageCollection(delay = 5.seconds) 249 250 return AuthenticationResult.SUCCEEDED 251 } 252 253 // Authentication failed. 254 repository.reportAuthenticationAttempt(isSuccessful = false) 255 256 if (authenticationResult.lockoutDurationMs > 0) { 257 // Lockout has been triggered. 258 repository.reportLockoutStarted(authenticationResult.lockoutDurationMs) 259 } 260 261 _onAuthenticationResult.emit(false) 262 return AuthenticationResult.FAILED 263 } 264 265 /** 266 * Returns the device policy enforced maximum time to lock the device, in milliseconds. When the 267 * device goes to sleep, this is the maximum time the device policy allows to wait before 268 * locking the device, despite what the user setting might be set to. 269 */ 270 suspend fun getMaximumTimeToLock(): Long { 271 return repository.getMaximumTimeToLock() 272 } 273 274 /** Returns `true` if the power button should instantly lock the device, `false` otherwise. */ 275 suspend fun getPowerButtonInstantlyLocks(): Boolean { 276 return !getAuthenticationMethod().isSecure || repository.getPowerButtonInstantlyLocks() 277 } 278 279 private suspend fun shouldSkipAuthenticationAttempt( 280 authenticationMethod: AuthenticationMethodModel, 281 isAutoConfirmAttempt: Boolean, 282 inputLength: Int, 283 ): Boolean { 284 return when { 285 // Lockout is active, the UI layer should not have called this; skip the attempt. 286 repository.lockoutEndTimestamp != null -> true 287 // Auto-confirm attempt when the feature is not enabled; skip the attempt. 288 isAutoConfirmAttempt && !isAutoConfirmEnabled.value -> true 289 // The pin is too short; skip only if this is an auto-confirm attempt. 290 authenticationMethod == Pin && authenticationMethod.isInputTooShort(inputLength) -> 291 isAutoConfirmAttempt 292 // The input is too short. 293 authenticationMethod.isInputTooShort(inputLength) -> true 294 else -> false 295 } 296 } 297 298 private suspend fun AuthenticationMethodModel.isInputTooShort(inputLength: Int): Boolean { 299 return when (this) { 300 Pattern -> inputLength < repository.minPatternLength 301 Password -> inputLength < repository.minPasswordLength 302 Pin -> inputLength < repository.getPinLength() 303 else -> false 304 } 305 } 306 307 /** 308 * @return Whether the current user, managed profile or whole device is next at risk of wipe. 309 */ 310 private suspend fun getWipeTarget(): WipeTarget { 311 // Check which profile has the strictest policy for failed authentication attempts. 312 val userToBeWiped = repository.getProfileWithMinFailedUnlockAttemptsForWipe() 313 val primaryUser = selectedUserInteractor.getMainUserId() ?: UserHandle.USER_SYSTEM 314 return when (userToBeWiped) { 315 selectedUserInteractor.getSelectedUserId() -> 316 if (userToBeWiped == primaryUser) { 317 WipeTarget.WholeDevice 318 } else { 319 WipeTarget.User 320 } 321 322 // Shouldn't happen at this stage; this is to maintain legacy behavior. 323 UserHandle.USER_NULL -> WipeTarget.WholeDevice 324 else -> WipeTarget.ManagedProfile 325 } 326 } 327 328 private fun AuthenticationMethodModel.createCredential( 329 input: List<Any> 330 ): LockscreenCredential? { 331 return when (this) { 332 is Pin -> LockscreenCredential.createPin(input.joinToString("")) 333 is Password -> LockscreenCredential.createPassword(input.joinToString("")) 334 is Pattern -> 335 LockscreenCredential.createPattern( 336 input 337 .map { it as AuthenticationPatternCoordinate } 338 .map { LockPatternView.Cell.of(it.y, it.x) } 339 ) 340 else -> null 341 } 342 } 343 344 private suspend fun initiateGarbageCollection(delay: Duration) { 345 applicationScope.launch(context = backgroundDispatcher) { 346 delay(delay) 347 System.gc() 348 System.runFinalization() 349 System.gc() 350 } 351 } 352 353 companion object { 354 const val TAG = "AuthenticationInteractor" 355 } 356 } 357 358 /** Result of a user authentication attempt. */ 359 enum class AuthenticationResult { 360 /** Authentication succeeded. */ 361 SUCCEEDED, 362 /** Authentication failed. */ 363 FAILED, 364 /** Authentication was not performed, e.g. due to insufficient input. */ 365 SKIPPED, 366 } 367