diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt index cf692c69d8788..dd9dcbedd6fc8 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.authentication.domain.interactor +import android.app.admin.DevicePolicyManager import com.android.systemui.authentication.data.repository.AuthenticationRepository import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.dagger.SysUISingleton @@ -177,15 +178,21 @@ constructor( /** * Returns a PIN code from the given list. It's assumed the given list elements are all - * [Int]. + * [Int] in the range [0-9]. */ - private fun List.asCode(): Int? { - if (isEmpty()) { + private fun List.asCode(): Long? { + if (isEmpty() || size > DevicePolicyManager.MAX_PASSWORD_LENGTH) { return null } - var code = 0 - map { it as Int }.forEach { integer -> code = code * 10 + integer } + var code = 0L + map { + require(it is Int && it in 0..9) { + "Pin is required to be Int in range [0..9], but got $it" + } + it + } + .forEach { integer -> code = code * 10 + integer } return code } diff --git a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt index edaf73bea9eac..e4fbf9af35eaf 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/shared/model/AuthenticationMethodModel.kt @@ -32,7 +32,13 @@ sealed class AuthenticationMethodModel( /** The most basic authentication method. The lock screen can be swiped away when displayed. */ object Swipe : AuthenticationMethodModel(isSecure = false) - data class Pin(val code: Int) : AuthenticationMethodModel(isSecure = true) + /** + * Authentication method using a PIN. + * + * In practice, a pin is restricted to 16 decimal digits , see + * [android.app.admin.DevicePolicyManager.MAX_PASSWORD_LENGTH] + */ + data class Pin(val code: Long) : AuthenticationMethodModel(isSecure = true) data class Password(val password: String) : AuthenticationMethodModel(isSecure = true) diff --git a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt index 7ba3d9bdc56a9..3a93e7744d00a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.authentication.domain.interactor +import android.app.admin.DevicePolicyManager import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.authentication.data.repository.AuthenticationRepository @@ -168,6 +169,51 @@ class AuthenticationInteractorTest : SysuiTestCase() { assertThat(failedAttemptCount).isEqualTo(1) } + @Test + fun authenticate_withEmptyPin_returnsFalseAndDoesNotUnlockDevice() = + testScope.runTest { + val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts) + val isUnlocked by collectLastValue(underTest.isUnlocked) + underTest.setAuthenticationMethod(AuthenticationMethodModel.Pin(1234)) + assertThat(isUnlocked).isFalse() + + assertThat(underTest.authenticate(listOf())).isFalse() + assertThat(isUnlocked).isFalse() + assertThat(failedAttemptCount).isEqualTo(1) + } + + @Test + fun authenticate_withCorrectMaxLengthPin_returnsTrueAndUnlocksDevice() = + testScope.runTest { + val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts) + val isUnlocked by collectLastValue(underTest.isUnlocked) + underTest.setAuthenticationMethod(AuthenticationMethodModel.Pin(9999999999999999)) + assertThat(isUnlocked).isFalse() + + assertThat(underTest.authenticate(List(16) { 9 })).isTrue() + assertThat(isUnlocked).isTrue() + assertThat(failedAttemptCount).isEqualTo(0) + } + + @Test + fun authenticate_withCorrectTooLongPin_returnsFalseAndDoesNotUnlockDevice() = + testScope.runTest { + // Max pin length is 16 digits. To avoid issues with overflows, this test ensures + // that all pins > 16 decimal digits are rejected. + + // If the policy changes, there is work to do in SysUI. + assertThat(DevicePolicyManager.MAX_PASSWORD_LENGTH).isLessThan(17) + + val failedAttemptCount by collectLastValue(underTest.failedAuthenticationAttempts) + val isUnlocked by collectLastValue(underTest.isUnlocked) + underTest.setAuthenticationMethod(AuthenticationMethodModel.Pin(99999999999999999)) + assertThat(isUnlocked).isFalse() + + assertThat(underTest.authenticate(List(17) { 9 })).isFalse() + assertThat(isUnlocked).isFalse() + assertThat(failedAttemptCount).isEqualTo(1) + } + @Test fun authenticate_withCorrectPassword_returnsTrueAndUnlocksDevice() = testScope.runTest {