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