Loading services/core/java/com/android/server/locksettings/SoftwareRateLimiter.java +108 −39 Original line number Diff line number Diff line Loading @@ -141,8 +141,12 @@ class SoftwareRateLimiter { */ public int numDuplicateWrongGuesses; /** The type of the LSKF, as a value of the stats CredentialType enum */ public final int statsCredentialType; /** * The type of the LSKF, as a value of the stats CredentialType enum. Updates after the * first guess. */ public int statsCredentialType = FrameworkStatsLog.LSKF_AUTHENTICATION_ATTEMPTED__CREDENTIAL_TYPE__UNKNOWN_TYPE; /** * The time since boot at which the failure counter was last incremented, or zero if the Loading @@ -150,6 +154,13 @@ class SoftwareRateLimiter { */ public Duration timeSinceBootOfLastFailure = Duration.ZERO; /** * The time since boot at which the lockout ends and another guess can be made, or {@link * Duration#ZERO} if there is not currently a lockout. Lockouts can be imposed by software * or externally, e.g. Weaver. */ public Duration lockoutEndTime = Duration.ZERO; /** * The list of wrong guesses that were recently tried already in the current boot, ordered * from newest to oldest. The used portion is followed by nulls in any unused space. Loading @@ -157,9 +168,8 @@ class SoftwareRateLimiter { public final LockscreenCredential[] savedWrongGuesses = new LockscreenCredential[MAX_SAVED_WRONG_GUESSES]; RateLimiterState(int numFailures, LockscreenCredential firstGuess) { RateLimiterState(int numFailures) { this.numFailures = numFailures; this.statsCredentialType = getStatsCredentialType(firstGuess); } } Loading Loading @@ -196,41 +206,23 @@ class SoftwareRateLimiter { return SoftwareRateLimiterResult.credentialTooShort(); } final RateLimiterState state = mState.computeIfAbsent( id, key -> { // The state isn't cached yet. Create it. // // For LSKF-based synthetic password protectors the only persistent // software rate-limiter state is the failure counter. // timeSinceBootOfLastFailure is just set to zero, so effectively the // timeout resets to its original value (for the current failure count) // upon reboot. That matches what typical hardware rate-limiter // implementations do; they typically do not have access to a trusted // real-time clock that runs without the device being powered on. // // Likewise, rebooting causes any saved wrong guesses to be forgotten. return new RateLimiterState(readFailureCounter(id), guess); }); final Duration now = mInjector.getTimeSinceBoot(); final RateLimiterState state = getOrComputeState(id, now); if (state.statsCredentialType == FrameworkStatsLog.LSKF_AUTHENTICATION_ATTEMPTED__CREDENTIAL_TYPE__UNKNOWN_TYPE) { state.statsCredentialType = getStatsCredentialType(guess); } // Check for remaining timeout. Note that the case of a positive remaining timeout normally // won't be reached, since reportFailure() will have returned the timeout when the last // guess was made, causing the lock screen to block inputs for that amount of time. But // checking for it is still needed to cover any cases where a guess gets made anyway, for // example following a reboot which causes the lock screen to "forget" the timeout. final Duration originalTimeout; if (mEnforcing) { if (state.numFailures >= TIMEOUT_TABLE.length || state.numFailures < 0) { if (mEnforcing && (state.numFailures >= TIMEOUT_TABLE.length || state.numFailures < 0)) { Slogf.e(TAG, "No more guesses allowed; numFailures=%d", state.numFailures); return SoftwareRateLimiterResult.noMoreGuesses(); } originalTimeout = TIMEOUT_TABLE[state.numFailures]; } else { originalTimeout = Duration.ZERO; } final Duration now = mInjector.getTimeSinceBoot(); final Duration timeout = state.timeSinceBootOfLastFailure.plus(originalTimeout).minus(now); final Duration timeout = computeRemainingTimeout(state, now); if (timeout.isPositive()) { Slogf.e(TAG, "Rate-limited; numFailures=%d, timeout=%s", state.numFailures, timeout); return SoftwareRateLimiterResult.rateLimited(timeout); Loading Loading @@ -259,6 +251,68 @@ class SoftwareRateLimiter { return SoftwareRateLimiterResult.continueToHardware(); } @GuardedBy("this") private RateLimiterState getOrComputeState(LskfIdentifier id, Duration now) { return mState.computeIfAbsent( id, key -> { // The state isn't cached yet. Create it. // // For LSKF-based synthetic password protectors the only persistent // software rate-limiter state is the failure counter. // timeSinceBootOfLastFailure is just set to zero, so effectively the // timeout resets to its original value (for the current failure count) // upon reboot. That matches what typical hardware rate-limiter // implementations do; they typically do not have access to a trusted // real-time clock that runs without the device being powered on. // // Likewise, rebooting causes any saved wrong guesses to be forgotten. RateLimiterState state = new RateLimiterState(readFailureCounter(id)); evaluateSoftwareRateLimit(state, now); return state; }); } @GuardedBy("this") private Duration computeRemainingTimeout(RateLimiterState state, Duration now) { final Duration remainingTimeout = state.lockoutEndTime.minus(now); return remainingTimeout.isPositive() ? remainingTimeout : Duration.ZERO; } /** Computes the software enforced lockout and updates the stored lockout end time. */ @GuardedBy("this") private void evaluateSoftwareRateLimit(RateLimiterState state, Duration now) { final Duration originalTimeout = getOriginalTimeout(state.numFailures); final Duration softwareLockoutEndTime = state.timeSinceBootOfLastFailure.plus(originalTimeout); updateLockoutEndTime(state, now, softwareLockoutEndTime); } /** * Updates state.lockoutEndTime to be the later of lockoutEndTime and state.lockoutEndTime, or * zero if that time has already been reached. */ @GuardedBy("this") private void updateLockoutEndTime( RateLimiterState state, Duration now, Duration lockoutEndTime) { if (state.lockoutEndTime.compareTo(lockoutEndTime) > 0) { lockoutEndTime = state.lockoutEndTime; // state.lockoutEndTime is later } if (now.compareTo(lockoutEndTime) >= 0) { lockoutEndTime = Duration.ZERO; // end time has already been reached } if (!lockoutEndTime.equals(state.lockoutEndTime)) { state.lockoutEndTime = lockoutEndTime; } } @GuardedBy("this") private void clearLockoutEndTime(RateLimiterState state) { if (!state.lockoutEndTime.isZero()) { state.lockoutEndTime = Duration.ZERO; } } /** * Reports a successful guess to the software rate-limiter. This causes the failure counter and * saved wrong guesses to be cleared. Loading @@ -274,6 +328,7 @@ class SoftwareRateLimiter { state.numDuplicateWrongGuesses = 0; writeFailureCounter(id, state); forgetSavedWrongGuesses(state); clearLockoutEndTime(state); } } Loading Loading @@ -318,6 +373,8 @@ class SoftwareRateLimiter { return Duration.ZERO; } final Duration now = mInjector.getTimeSinceBoot(); // Increment the failure counter regardless of whether the failure is a certainly wrong // guess or not. A generic failure might still be caused by a wrong guess. Gatekeeper only // ever returns generic failures, and some Weaver implementations prefer THROTTLE to Loading @@ -327,7 +384,9 @@ class SoftwareRateLimiter { // rate-limiter kicks in gradually anyway, so there will be a chance for the user to try // again. state.numFailures++; state.timeSinceBootOfLastFailure = mInjector.getTimeSinceBoot(); state.timeSinceBootOfLastFailure = now; evaluateSoftwareRateLimit(state, now); // Update the counter on-disk. It is important that this be done before the failure is // reported to the UI, and that it be done synchronously e.g. by fsync()-ing the file and Loading Loading @@ -358,15 +417,19 @@ class SoftwareRateLimiter { SAVED_WRONG_GUESS_TIMEOUT.toMillis()); } return computeRemainingTimeout(state, now); } private Duration getOriginalTimeout(int numFailures) { if (!mEnforcing) { return Duration.ZERO; } if (state.numFailures >= TIMEOUT_TABLE.length || state.numFailures < 0) { if (numFailures >= TIMEOUT_TABLE.length || numFailures < 0) { // In this case actually no more guesses are allowed, but currently there is no way to // convey that information. For now just report the final timeout again. return TIMEOUT_TABLE[TIMEOUT_TABLE.length - 1]; } return TIMEOUT_TABLE[state.numFailures]; return TIMEOUT_TABLE[numFailures]; } private static int getStatsCredentialType(LockscreenCredential firstGuess) { Loading Loading @@ -435,8 +498,7 @@ class SoftwareRateLimiter { synchronized void clearLskfState(LskfIdentifier id) { int index = mState.indexOfKey(id); if (index >= 0) { forgetSavedWrongGuesses(mState.valueAt(index)); mState.removeAt(index); clearLskfStateAtIndex(index); } } Loading @@ -449,12 +511,18 @@ class SoftwareRateLimiter { for (int index = mState.size() - 1; index >= 0; index--) { LskfIdentifier id = mState.keyAt(index); if (id.userId == userId) { forgetSavedWrongGuesses(mState.valueAt(index)); mState.removeAt(index); clearLskfStateAtIndex(index); } } } @GuardedBy("this") private void clearLskfStateAtIndex(int index) { final RateLimiterState state = mState.valueAt(index); forgetSavedWrongGuesses(state); mState.removeAt(index); } private int readFailureCounter(LskfIdentifier id) { if (id.isSpecialCredential()) { // Special credentials (e.g. FRP credential and repair mode exit credential) do not yet Loading Loading @@ -492,6 +560,7 @@ class SoftwareRateLimiter { pw.println("numDuplicateWrongGuesses=" + state.numDuplicateWrongGuesses); pw.println("statsCredentialType=" + state.statsCredentialType); pw.println("timeSinceBootOfLastFailure=" + state.timeSinceBootOfLastFailure); pw.println("lockoutEndTime=" + state.lockoutEndTime); pw.println( "numSavedWrongGuesses=" + Arrays.stream(state.savedWrongGuesses) Loading services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java +20 −22 Original line number Diff line number Diff line Loading @@ -74,6 +74,8 @@ import java.time.Duration; @Presubmit @RunWith(AndroidJUnit4.class) public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { private static final Duration TEN_YEARS = Duration.ofDays(10 * 365); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); Loading Loading @@ -840,18 +842,11 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { public void test20UniqueGuessesAllowed() throws Exception { final int userId = PRIMARY_USER_ID; final LockscreenCredential credential = newPassword("password"); final Duration tenYears = Duration.ofDays(10 * 365); Duration now = Duration.ZERO; VerifyCredentialResponse response; mInjector.setTimeSinceBoot(now); mInjector.setTimeSinceBoot(Duration.ZERO); setCredential(userId, credential); for (int i = 0; i < 19; i++) { response = mService.verifyCredential(newPassword("wrong" + i), userId, /* flags= */ 0); assertFalse(response.isMatched()); now = now.plus(tenYears); // Advance 10 years to get past rate-limiting mInjector.setTimeSinceBoot(now); } guessWrongCredential(userId, 19, TEN_YEARS); response = mService.verifyCredential(credential, userId, /* flags= */ 0); assertTrue(response.isMatched()); } Loading @@ -861,18 +856,11 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { public void testMoreThan20UniqueGuessesNotAllowed() throws Exception { final int userId = PRIMARY_USER_ID; final LockscreenCredential credential = newPassword("password"); final Duration tenYears = Duration.ofDays(10 * 365); Duration now = Duration.ZERO; VerifyCredentialResponse response; mInjector.setTimeSinceBoot(now); mInjector.setTimeSinceBoot(Duration.ZERO); setCredential(userId, credential); for (int i = 0; i < 20; i++) { response = mService.verifyCredential(newPassword("wrong" + i), userId, /* flags= */ 0); assertFalse(response.isMatched()); now = now.plus(tenYears); // Advance 10 years to get past rate-limiting mInjector.setTimeSinceBoot(now); } guessWrongCredential(userId, 20, TEN_YEARS); response = mService.verifyCredential(credential, userId, /* flags= */ 0); assertFalse(response.isMatched()); } Loading @@ -886,10 +874,7 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { VerifyCredentialResponse response; setCredential(userId, credential); for (int i = 0; i < 20; i++) { response = mService.verifyCredential(newPassword("wrong" + i), userId, /* flags= */ 0); assertFalse(response.isMatched()); } guessWrongCredential(userId, /* times= */ 20); response = mService.verifyCredential(credential, userId, /* flags= */ 0); assertTrue(response.isMatched()); } Loading @@ -911,6 +896,19 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { testTimeoutClamping(Duration.ofMillis(Long.MAX_VALUE), Integer.MAX_VALUE); } private void guessWrongCredential(int userId, int times) { guessWrongCredential(userId, times, Duration.ZERO); } private void guessWrongCredential(int userId, int times, Duration timeBetweenGuesses) { for (int i = 0; i < times; i++) { VerifyCredentialResponse response = mService.verifyCredential(newPassword("wrong" + i), userId, /* flags= */ 0); assertFalse(response.isMatched()); mInjector.setTimeSinceBoot(mInjector.getTimeSinceBoot().plus(timeBetweenGuesses)); } } private void testTimeoutClamping(Duration originalTimeout, int expectedClampedTimeout) { VerifyCredentialResponse response = VerifyCredentialResponse.fromTimeout(originalTimeout); assertEquals(expectedClampedTimeout, response.getTimeout()); Loading Loading
services/core/java/com/android/server/locksettings/SoftwareRateLimiter.java +108 −39 Original line number Diff line number Diff line Loading @@ -141,8 +141,12 @@ class SoftwareRateLimiter { */ public int numDuplicateWrongGuesses; /** The type of the LSKF, as a value of the stats CredentialType enum */ public final int statsCredentialType; /** * The type of the LSKF, as a value of the stats CredentialType enum. Updates after the * first guess. */ public int statsCredentialType = FrameworkStatsLog.LSKF_AUTHENTICATION_ATTEMPTED__CREDENTIAL_TYPE__UNKNOWN_TYPE; /** * The time since boot at which the failure counter was last incremented, or zero if the Loading @@ -150,6 +154,13 @@ class SoftwareRateLimiter { */ public Duration timeSinceBootOfLastFailure = Duration.ZERO; /** * The time since boot at which the lockout ends and another guess can be made, or {@link * Duration#ZERO} if there is not currently a lockout. Lockouts can be imposed by software * or externally, e.g. Weaver. */ public Duration lockoutEndTime = Duration.ZERO; /** * The list of wrong guesses that were recently tried already in the current boot, ordered * from newest to oldest. The used portion is followed by nulls in any unused space. Loading @@ -157,9 +168,8 @@ class SoftwareRateLimiter { public final LockscreenCredential[] savedWrongGuesses = new LockscreenCredential[MAX_SAVED_WRONG_GUESSES]; RateLimiterState(int numFailures, LockscreenCredential firstGuess) { RateLimiterState(int numFailures) { this.numFailures = numFailures; this.statsCredentialType = getStatsCredentialType(firstGuess); } } Loading Loading @@ -196,41 +206,23 @@ class SoftwareRateLimiter { return SoftwareRateLimiterResult.credentialTooShort(); } final RateLimiterState state = mState.computeIfAbsent( id, key -> { // The state isn't cached yet. Create it. // // For LSKF-based synthetic password protectors the only persistent // software rate-limiter state is the failure counter. // timeSinceBootOfLastFailure is just set to zero, so effectively the // timeout resets to its original value (for the current failure count) // upon reboot. That matches what typical hardware rate-limiter // implementations do; they typically do not have access to a trusted // real-time clock that runs without the device being powered on. // // Likewise, rebooting causes any saved wrong guesses to be forgotten. return new RateLimiterState(readFailureCounter(id), guess); }); final Duration now = mInjector.getTimeSinceBoot(); final RateLimiterState state = getOrComputeState(id, now); if (state.statsCredentialType == FrameworkStatsLog.LSKF_AUTHENTICATION_ATTEMPTED__CREDENTIAL_TYPE__UNKNOWN_TYPE) { state.statsCredentialType = getStatsCredentialType(guess); } // Check for remaining timeout. Note that the case of a positive remaining timeout normally // won't be reached, since reportFailure() will have returned the timeout when the last // guess was made, causing the lock screen to block inputs for that amount of time. But // checking for it is still needed to cover any cases where a guess gets made anyway, for // example following a reboot which causes the lock screen to "forget" the timeout. final Duration originalTimeout; if (mEnforcing) { if (state.numFailures >= TIMEOUT_TABLE.length || state.numFailures < 0) { if (mEnforcing && (state.numFailures >= TIMEOUT_TABLE.length || state.numFailures < 0)) { Slogf.e(TAG, "No more guesses allowed; numFailures=%d", state.numFailures); return SoftwareRateLimiterResult.noMoreGuesses(); } originalTimeout = TIMEOUT_TABLE[state.numFailures]; } else { originalTimeout = Duration.ZERO; } final Duration now = mInjector.getTimeSinceBoot(); final Duration timeout = state.timeSinceBootOfLastFailure.plus(originalTimeout).minus(now); final Duration timeout = computeRemainingTimeout(state, now); if (timeout.isPositive()) { Slogf.e(TAG, "Rate-limited; numFailures=%d, timeout=%s", state.numFailures, timeout); return SoftwareRateLimiterResult.rateLimited(timeout); Loading Loading @@ -259,6 +251,68 @@ class SoftwareRateLimiter { return SoftwareRateLimiterResult.continueToHardware(); } @GuardedBy("this") private RateLimiterState getOrComputeState(LskfIdentifier id, Duration now) { return mState.computeIfAbsent( id, key -> { // The state isn't cached yet. Create it. // // For LSKF-based synthetic password protectors the only persistent // software rate-limiter state is the failure counter. // timeSinceBootOfLastFailure is just set to zero, so effectively the // timeout resets to its original value (for the current failure count) // upon reboot. That matches what typical hardware rate-limiter // implementations do; they typically do not have access to a trusted // real-time clock that runs without the device being powered on. // // Likewise, rebooting causes any saved wrong guesses to be forgotten. RateLimiterState state = new RateLimiterState(readFailureCounter(id)); evaluateSoftwareRateLimit(state, now); return state; }); } @GuardedBy("this") private Duration computeRemainingTimeout(RateLimiterState state, Duration now) { final Duration remainingTimeout = state.lockoutEndTime.minus(now); return remainingTimeout.isPositive() ? remainingTimeout : Duration.ZERO; } /** Computes the software enforced lockout and updates the stored lockout end time. */ @GuardedBy("this") private void evaluateSoftwareRateLimit(RateLimiterState state, Duration now) { final Duration originalTimeout = getOriginalTimeout(state.numFailures); final Duration softwareLockoutEndTime = state.timeSinceBootOfLastFailure.plus(originalTimeout); updateLockoutEndTime(state, now, softwareLockoutEndTime); } /** * Updates state.lockoutEndTime to be the later of lockoutEndTime and state.lockoutEndTime, or * zero if that time has already been reached. */ @GuardedBy("this") private void updateLockoutEndTime( RateLimiterState state, Duration now, Duration lockoutEndTime) { if (state.lockoutEndTime.compareTo(lockoutEndTime) > 0) { lockoutEndTime = state.lockoutEndTime; // state.lockoutEndTime is later } if (now.compareTo(lockoutEndTime) >= 0) { lockoutEndTime = Duration.ZERO; // end time has already been reached } if (!lockoutEndTime.equals(state.lockoutEndTime)) { state.lockoutEndTime = lockoutEndTime; } } @GuardedBy("this") private void clearLockoutEndTime(RateLimiterState state) { if (!state.lockoutEndTime.isZero()) { state.lockoutEndTime = Duration.ZERO; } } /** * Reports a successful guess to the software rate-limiter. This causes the failure counter and * saved wrong guesses to be cleared. Loading @@ -274,6 +328,7 @@ class SoftwareRateLimiter { state.numDuplicateWrongGuesses = 0; writeFailureCounter(id, state); forgetSavedWrongGuesses(state); clearLockoutEndTime(state); } } Loading Loading @@ -318,6 +373,8 @@ class SoftwareRateLimiter { return Duration.ZERO; } final Duration now = mInjector.getTimeSinceBoot(); // Increment the failure counter regardless of whether the failure is a certainly wrong // guess or not. A generic failure might still be caused by a wrong guess. Gatekeeper only // ever returns generic failures, and some Weaver implementations prefer THROTTLE to Loading @@ -327,7 +384,9 @@ class SoftwareRateLimiter { // rate-limiter kicks in gradually anyway, so there will be a chance for the user to try // again. state.numFailures++; state.timeSinceBootOfLastFailure = mInjector.getTimeSinceBoot(); state.timeSinceBootOfLastFailure = now; evaluateSoftwareRateLimit(state, now); // Update the counter on-disk. It is important that this be done before the failure is // reported to the UI, and that it be done synchronously e.g. by fsync()-ing the file and Loading Loading @@ -358,15 +417,19 @@ class SoftwareRateLimiter { SAVED_WRONG_GUESS_TIMEOUT.toMillis()); } return computeRemainingTimeout(state, now); } private Duration getOriginalTimeout(int numFailures) { if (!mEnforcing) { return Duration.ZERO; } if (state.numFailures >= TIMEOUT_TABLE.length || state.numFailures < 0) { if (numFailures >= TIMEOUT_TABLE.length || numFailures < 0) { // In this case actually no more guesses are allowed, but currently there is no way to // convey that information. For now just report the final timeout again. return TIMEOUT_TABLE[TIMEOUT_TABLE.length - 1]; } return TIMEOUT_TABLE[state.numFailures]; return TIMEOUT_TABLE[numFailures]; } private static int getStatsCredentialType(LockscreenCredential firstGuess) { Loading Loading @@ -435,8 +498,7 @@ class SoftwareRateLimiter { synchronized void clearLskfState(LskfIdentifier id) { int index = mState.indexOfKey(id); if (index >= 0) { forgetSavedWrongGuesses(mState.valueAt(index)); mState.removeAt(index); clearLskfStateAtIndex(index); } } Loading @@ -449,12 +511,18 @@ class SoftwareRateLimiter { for (int index = mState.size() - 1; index >= 0; index--) { LskfIdentifier id = mState.keyAt(index); if (id.userId == userId) { forgetSavedWrongGuesses(mState.valueAt(index)); mState.removeAt(index); clearLskfStateAtIndex(index); } } } @GuardedBy("this") private void clearLskfStateAtIndex(int index) { final RateLimiterState state = mState.valueAt(index); forgetSavedWrongGuesses(state); mState.removeAt(index); } private int readFailureCounter(LskfIdentifier id) { if (id.isSpecialCredential()) { // Special credentials (e.g. FRP credential and repair mode exit credential) do not yet Loading Loading @@ -492,6 +560,7 @@ class SoftwareRateLimiter { pw.println("numDuplicateWrongGuesses=" + state.numDuplicateWrongGuesses); pw.println("statsCredentialType=" + state.statsCredentialType); pw.println("timeSinceBootOfLastFailure=" + state.timeSinceBootOfLastFailure); pw.println("lockoutEndTime=" + state.lockoutEndTime); pw.println( "numSavedWrongGuesses=" + Arrays.stream(state.savedWrongGuesses) Loading
services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java +20 −22 Original line number Diff line number Diff line Loading @@ -74,6 +74,8 @@ import java.time.Duration; @Presubmit @RunWith(AndroidJUnit4.class) public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { private static final Duration TEN_YEARS = Duration.ofDays(10 * 365); @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Rule public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); Loading Loading @@ -840,18 +842,11 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { public void test20UniqueGuessesAllowed() throws Exception { final int userId = PRIMARY_USER_ID; final LockscreenCredential credential = newPassword("password"); final Duration tenYears = Duration.ofDays(10 * 365); Duration now = Duration.ZERO; VerifyCredentialResponse response; mInjector.setTimeSinceBoot(now); mInjector.setTimeSinceBoot(Duration.ZERO); setCredential(userId, credential); for (int i = 0; i < 19; i++) { response = mService.verifyCredential(newPassword("wrong" + i), userId, /* flags= */ 0); assertFalse(response.isMatched()); now = now.plus(tenYears); // Advance 10 years to get past rate-limiting mInjector.setTimeSinceBoot(now); } guessWrongCredential(userId, 19, TEN_YEARS); response = mService.verifyCredential(credential, userId, /* flags= */ 0); assertTrue(response.isMatched()); } Loading @@ -861,18 +856,11 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { public void testMoreThan20UniqueGuessesNotAllowed() throws Exception { final int userId = PRIMARY_USER_ID; final LockscreenCredential credential = newPassword("password"); final Duration tenYears = Duration.ofDays(10 * 365); Duration now = Duration.ZERO; VerifyCredentialResponse response; mInjector.setTimeSinceBoot(now); mInjector.setTimeSinceBoot(Duration.ZERO); setCredential(userId, credential); for (int i = 0; i < 20; i++) { response = mService.verifyCredential(newPassword("wrong" + i), userId, /* flags= */ 0); assertFalse(response.isMatched()); now = now.plus(tenYears); // Advance 10 years to get past rate-limiting mInjector.setTimeSinceBoot(now); } guessWrongCredential(userId, 20, TEN_YEARS); response = mService.verifyCredential(credential, userId, /* flags= */ 0); assertFalse(response.isMatched()); } Loading @@ -886,10 +874,7 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { VerifyCredentialResponse response; setCredential(userId, credential); for (int i = 0; i < 20; i++) { response = mService.verifyCredential(newPassword("wrong" + i), userId, /* flags= */ 0); assertFalse(response.isMatched()); } guessWrongCredential(userId, /* times= */ 20); response = mService.verifyCredential(credential, userId, /* flags= */ 0); assertTrue(response.isMatched()); } Loading @@ -911,6 +896,19 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { testTimeoutClamping(Duration.ofMillis(Long.MAX_VALUE), Integer.MAX_VALUE); } private void guessWrongCredential(int userId, int times) { guessWrongCredential(userId, times, Duration.ZERO); } private void guessWrongCredential(int userId, int times, Duration timeBetweenGuesses) { for (int i = 0; i < times; i++) { VerifyCredentialResponse response = mService.verifyCredential(newPassword("wrong" + i), userId, /* flags= */ 0); assertFalse(response.isMatched()); mInjector.setTimeSinceBoot(mInjector.getTimeSinceBoot().plus(timeBetweenGuesses)); } } private void testTimeoutClamping(Duration originalTimeout, int expectedClampedTimeout) { VerifyCredentialResponse response = VerifyCredentialResponse.fromTimeout(originalTimeout); assertEquals(expectedClampedTimeout, response.getTimeout()); Loading