Loading services/core/java/com/android/server/locksettings/LockSettingsService.java +1 −1 Original line number Diff line number Diff line Loading @@ -2387,7 +2387,7 @@ public class LockSettingsService extends ILockSettings.Stub { case SoftwareRateLimiterResult.CONTINUE_TO_HARDWARE: break; case SoftwareRateLimiterResult.RATE_LIMITED: return VerifyCredentialResponse.fromTimeout(res.remainingDelay); return VerifyCredentialResponse.fromTimeout(res.timeout); case SoftwareRateLimiterResult.CREDENTIAL_TOO_SHORT: return VerifyCredentialResponse.credTooShort(); case SoftwareRateLimiterResult.DUPLICATE_WRONG_GUESS: Loading services/core/java/com/android/server/locksettings/SoftwareRateLimiter.java +30 −33 Original line number Diff line number Diff line Loading @@ -84,10 +84,10 @@ class SoftwareRateLimiter { @VisibleForTesting static final Duration SAVED_WRONG_GUESS_TIMEOUT = Duration.ofMinutes(5); /** * A table that maps the number of (real) failures to the delay that is enforced after that * A table that maps the number of (real) failures to the timeout that is enforced after that * number of (real) failures. Out-of-bounds indices default to not allowed. */ private static final Duration[] DELAY_TABLE = private static final Duration[] TIMEOUT_TABLE = new Duration[] { /* 0 */ Duration.ZERO, /* 1 */ Duration.ZERO, Loading Loading @@ -115,7 +115,7 @@ class SoftwareRateLimiter { /** * Whether the software rate-limiter is actually in enforcing mode. In non-enforcing mode all * delays are considered to be zero, and "duplicate wrong guess" results are not returned. * timeouts are considered to be zero, and "duplicate wrong guess" results are not returned. */ private final boolean mEnforcing; Loading @@ -129,7 +129,7 @@ class SoftwareRateLimiter { /** * The number of failed attempts since the last successful attempt, not counting attempts * that never reached the real credential check for a reason such as detection of a * duplicate wrong guess, credential too short, delay still remaining, etc. * duplicate wrong guess, credential too short, timeout still remaining, etc. */ public int numFailures; Loading Loading @@ -205,7 +205,7 @@ class SoftwareRateLimiter { // 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 // delay resets to its original value (for the current failure count) // 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. Loading @@ -214,30 +214,26 @@ class SoftwareRateLimiter { return new RateLimiterState(readFailureCounter(id), guess); }); // Check for remaining delay. Note that the case of a positive remaining delay normally // won't be reached, since reportFailure() will have returned the delay 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 delay. final Duration delay; // 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 >= DELAY_TABLE.length || state.numFailures < 0) { if (state.numFailures >= TIMEOUT_TABLE.length || state.numFailures < 0) { Slogf.e(TAG, "No more guesses allowed; numFailures=%d", state.numFailures); return SoftwareRateLimiterResult.noMoreGuesses(); } delay = DELAY_TABLE[state.numFailures]; originalTimeout = TIMEOUT_TABLE[state.numFailures]; } else { delay = Duration.ZERO; originalTimeout = Duration.ZERO; } final Duration now = mInjector.getTimeSinceBoot(); final Duration remainingDelay = state.timeSinceBootOfLastFailure.plus(delay).minus(now); if (remainingDelay.isPositive()) { Slogf.e( TAG, "Rate-limited; numFailures=%d, remainingDelay=%s", state.numFailures, remainingDelay); return SoftwareRateLimiterResult.rateLimited(remainingDelay); final Duration timeout = state.timeSinceBootOfLastFailure.plus(originalTimeout).minus(now); if (timeout.isPositive()) { Slogf.e(TAG, "Rate-limited; numFailures=%d, timeout=%s", state.numFailures, timeout); return SoftwareRateLimiterResult.rateLimited(timeout); } // Check for duplicate wrong guess. Loading Loading @@ -309,7 +305,7 @@ class SoftwareRateLimiter { * @param guess the LSKF that was attempted * @param isCertainlyWrongGuess true if it's certain that the failure was caused by the guess * being wrong, as opposed to e.g. a transient hardware glitch * @return the delay until when the next guess will be allowed * @return the remaining timeout until when the next guess will be allowed */ synchronized Duration reportFailure( LskfIdentifier id, LockscreenCredential guess, boolean isCertainlyWrongGuess) { Loading @@ -325,10 +321,11 @@ class SoftwareRateLimiter { // 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 // INCORRECT_KEY once the delay becomes nonzero. Instead of making the software rate-limiter // ineffective on all such devices, still apply it. This does mean that correct guesses that // encountered an error will be rate-limited. However, by design the rate-limiter kicks in // gradually anyway, so there will be a chance for the user to try again. // INCORRECT_KEY once the timeout becomes nonzero. Instead of making the software // rate-limiter ineffective on all such devices, still apply it. This does mean that correct // guesses that encountered an error will be rate-limited. However, by design the // rate-limiter kicks in gradually anyway, so there will be a chance for the user to try // again. state.numFailures++; state.timeSinceBootOfLastFailure = mInjector.getTimeSinceBoot(); Loading Loading @@ -364,12 +361,12 @@ class SoftwareRateLimiter { if (!mEnforcing) { return Duration.ZERO; } if (state.numFailures >= DELAY_TABLE.length || state.numFailures < 0) { if (state.numFailures >= TIMEOUT_TABLE.length || state.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 delay again. return DELAY_TABLE[DELAY_TABLE.length - 1]; // convey that information. For now just report the final timeout again. return TIMEOUT_TABLE[TIMEOUT_TABLE.length - 1]; } return DELAY_TABLE[state.numFailures]; return TIMEOUT_TABLE[state.numFailures]; } private static int getStatsCredentialType(LockscreenCredential firstGuess) { Loading Loading @@ -478,8 +475,8 @@ class SoftwareRateLimiter { // Only for unit tests. @VisibleForTesting Duration[] getDelayTable() { return DELAY_TABLE; Duration[] getTimeoutTable() { return TIMEOUT_TABLE; } synchronized void dump(IndentingPrintWriter pw) { Loading services/core/java/com/android/server/locksettings/SoftwareRateLimiterResult.java +5 −5 Original line number Diff line number Diff line Loading @@ -54,15 +54,15 @@ class SoftwareRateLimiterResult { * preliminary validation done before <em>before</em> the rate-limit check does not have access * to the timeout yet, so none is reported for {@link #CREDENTIAL_TOO_SHORT} either. */ @Nullable public final Duration remainingDelay; @Nullable public final Duration timeout; // Pre-allocate a CONTINUE_TO_HARDWARE result since it is the most common case. private static final SoftwareRateLimiterResult CONTINUE_TO_HARDWARE_RESULT = new SoftwareRateLimiterResult(CONTINUE_TO_HARDWARE, null); private SoftwareRateLimiterResult(@Code int resultCode, Duration remainingDelay) { private SoftwareRateLimiterResult(@Code int resultCode, Duration timeout) { this.code = resultCode; this.remainingDelay = remainingDelay; this.timeout = timeout; } static SoftwareRateLimiterResult credentialTooShort() { Loading @@ -73,8 +73,8 @@ class SoftwareRateLimiterResult { return new SoftwareRateLimiterResult(NO_MORE_GUESSES, null); } static SoftwareRateLimiterResult rateLimited(@NonNull Duration remainingDelay) { return new SoftwareRateLimiterResult(RATE_LIMITED, remainingDelay); static SoftwareRateLimiterResult rateLimited(@NonNull Duration timeout) { return new SoftwareRateLimiterResult(RATE_LIMITED, timeout); } static SoftwareRateLimiterResult duplicateWrongGuess() { Loading services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java +1 −1 Original line number Diff line number Diff line Loading @@ -761,7 +761,7 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { // Tests that if verifyCredential is passed a correct guess but it fails due to Weaver reporting // a status of THROTTLE (which is the expected status when there is a remaining rate-limiting // delay in Weaver), then LockSettingsService does not block the same guess from being // timeout in Weaver), then LockSettingsService does not block the same guess from being // re-attempted and in particular does not reject it as a duplicate wrong guess. @Test @EnableFlags(android.security.Flags.FLAG_SOFTWARE_RATELIMITER) Loading services/tests/servicestests/src/com/android/server/locksettings/SoftwareRateLimiterTest.java +26 −25 Original line number Diff line number Diff line Loading @@ -64,43 +64,44 @@ public class SoftwareRateLimiterTest { public void testRateLimitSchedule() { mRateLimiter = new SoftwareRateLimiter(mInjector); final LskfIdentifier id = new LskfIdentifier(10, 1000); final Duration[] delayTable = mRateLimiter.getDelayTable(); final Duration[] timeoutTable = mRateLimiter.getTimeoutTable(); for (int i = 0; i < delayTable.length; i++) { for (int i = 0; i < timeoutTable.length; i++) { final LockscreenCredential guess = newPassword("password" + i); Duration expectedDelay = delayTable[i]; if (!expectedDelay.isZero()) { verifyRateLimited(id, guess, expectedDelay); Duration expectedTimeout = timeoutTable[i]; if (!expectedTimeout.isZero()) { verifyRateLimited(id, guess, expectedTimeout); mInjector.advanceTime(Duration.ofSeconds(1)); verifyRateLimited(id, guess, expectedDelay.minus(Duration.ofSeconds(1))); mInjector.advanceTime(expectedDelay.minus(Duration.ofMillis(1001))); verifyRateLimited(id, guess, expectedTimeout.minus(Duration.ofSeconds(1))); mInjector.advanceTime(expectedTimeout.minus(Duration.ofMillis(1001))); verifyRateLimited(id, guess, Duration.ofMillis(1)); mInjector.advanceTime(Duration.ofMillis(1)); } verifyUniqueWrongGuess(id, guess, delayTable[Math.min(i + 1, delayTable.length - 1)]); verifyUniqueWrongGuess( id, guess, timeoutTable[Math.min(i + 1, timeoutTable.length - 1)]); verifyFailureCounter(id, i + 1); } verifyNoMoreGuesses(id, newPassword("password")); } // This test re-instantiates the SoftwareRateLimiter, like what happens after a reboot, after // there have already been a certain number of wrong guesses. It verifies that the delay for the // current number of wrong guesses is reset to its original value and starts counting down from // the new instantiation time. // there have already been a certain number of wrong guesses. It verifies that the timeout for // the current number of wrong guesses is reset to its original value and starts counting down // from the new instantiation time. @Test public void testExistingDelayResetsOnReinstantiation() { public void testExistingTimeoutResetsOnReinstantiation() { final LskfIdentifier id = new LskfIdentifier(10, 1000); final LockscreenCredential guess = newPassword("password"); final Duration[] delayTable = mRateLimiter.getDelayTable(); final int numWrongGuesses = delayTable.length - 1; final Duration expectedDelay = delayTable[numWrongGuesses]; final Duration[] timeoutTable = mRateLimiter.getTimeoutTable(); final int numWrongGuesses = timeoutTable.length - 1; final Duration expectedTimeout = timeoutTable[numWrongGuesses]; mInjector.setTime(Duration.ofSeconds(10)); mInjector.writeFailureCounter(id, numWrongGuesses); mRateLimiter = new SoftwareRateLimiter(mInjector); mInjector.setTime(Duration.ofSeconds(1)); verifyRateLimited(id, guess, expectedDelay.minus(Duration.ofSeconds(1))); verifyRateLimited(id, guess, expectedTimeout.minus(Duration.ofSeconds(1))); } @Test Loading Loading @@ -194,7 +195,7 @@ public class SoftwareRateLimiterTest { for (int i = 0; i < n; i++) { verifyUniqueWrongGuess(id, newPassword("password" + i)); mInjector.advanceTime(Duration.ofDays(1)); // Advance past any delay mInjector.advanceTime(Duration.ofDays(1)); // Advance past any timeout } SoftwareRateLimiterResult result = mRateLimiter.apply(id, newPassword("password0")); assertThat(result.code).isEqualTo(expectedResultCode); Loading @@ -206,7 +207,7 @@ public class SoftwareRateLimiterTest { for (int i = 0; i < SoftwareRateLimiter.MAX_SAVED_WRONG_GUESSES; i++) { verifyUniqueWrongGuess(id, newPassword("password" + i)); mInjector.advanceTime(Duration.ofDays(1)); // Advance past any delay mInjector.advanceTime(Duration.ofDays(1)); // Advance past any timeout } // The list of saved guesses should now be full. Try the oldest one, which should cause it // to be moved to the front of the list. Loading @@ -214,7 +215,7 @@ public class SoftwareRateLimiterTest { // Try a new guess. Then verify that it caused the eviction of password1, not password0. verifyUniqueWrongGuess(id, newPassword("passwordN")); mInjector.advanceTime(Duration.ofDays(1)); // Advance past any delay mInjector.advanceTime(Duration.ofDays(1)); // Advance past any timeout verifyDuplicateWrongGuess(id, newPassword("password0")); verifyUniqueWrongGuess(id, newPassword("password1")); } Loading Loading @@ -496,10 +497,10 @@ public class SoftwareRateLimiterTest { } private void verifyRateLimited( LskfIdentifier id, LockscreenCredential guess, Duration expectedRemainingDelay) { LskfIdentifier id, LockscreenCredential guess, Duration expectedTimeout) { SoftwareRateLimiterResult result = mRateLimiter.apply(id, guess); assertThat(result.code).isEqualTo(RATE_LIMITED); assertThat(result.remainingDelay).isEqualTo(expectedRemainingDelay); assertThat(result.timeout).isEqualTo(expectedTimeout); } // Verifies that the rate-limiter returns a status of DUPLICATE_WRONG_GUESS for the given guess. Loading @@ -516,14 +517,14 @@ public class SoftwareRateLimiterTest { mRateLimiter.reportFailure(id, guess, /* isCertainlyWrongGuess= */ true); } // Same as above but also verifies the next delay reported by reportFailure(). // Same as above but also verifies the next timeout reported by reportFailure(). private void verifyUniqueWrongGuess( LskfIdentifier id, LockscreenCredential guess, Duration expectedNextDelay) { LskfIdentifier id, LockscreenCredential guess, Duration expectedNextTimeout) { SoftwareRateLimiterResult result = mRateLimiter.apply(id, guess); assertThat(result.code).isEqualTo(CONTINUE_TO_HARDWARE); Duration nextDelay = Duration nextTimeout = mRateLimiter.reportFailure(id, guess, /* isCertainlyWrongGuess= */ true); assertThat(nextDelay).isEqualTo(expectedNextDelay); assertThat(nextTimeout).isEqualTo(expectedNextTimeout); } private void verifyFailureCounter(LskfIdentifier id, int expectedValue) { Loading Loading
services/core/java/com/android/server/locksettings/LockSettingsService.java +1 −1 Original line number Diff line number Diff line Loading @@ -2387,7 +2387,7 @@ public class LockSettingsService extends ILockSettings.Stub { case SoftwareRateLimiterResult.CONTINUE_TO_HARDWARE: break; case SoftwareRateLimiterResult.RATE_LIMITED: return VerifyCredentialResponse.fromTimeout(res.remainingDelay); return VerifyCredentialResponse.fromTimeout(res.timeout); case SoftwareRateLimiterResult.CREDENTIAL_TOO_SHORT: return VerifyCredentialResponse.credTooShort(); case SoftwareRateLimiterResult.DUPLICATE_WRONG_GUESS: Loading
services/core/java/com/android/server/locksettings/SoftwareRateLimiter.java +30 −33 Original line number Diff line number Diff line Loading @@ -84,10 +84,10 @@ class SoftwareRateLimiter { @VisibleForTesting static final Duration SAVED_WRONG_GUESS_TIMEOUT = Duration.ofMinutes(5); /** * A table that maps the number of (real) failures to the delay that is enforced after that * A table that maps the number of (real) failures to the timeout that is enforced after that * number of (real) failures. Out-of-bounds indices default to not allowed. */ private static final Duration[] DELAY_TABLE = private static final Duration[] TIMEOUT_TABLE = new Duration[] { /* 0 */ Duration.ZERO, /* 1 */ Duration.ZERO, Loading Loading @@ -115,7 +115,7 @@ class SoftwareRateLimiter { /** * Whether the software rate-limiter is actually in enforcing mode. In non-enforcing mode all * delays are considered to be zero, and "duplicate wrong guess" results are not returned. * timeouts are considered to be zero, and "duplicate wrong guess" results are not returned. */ private final boolean mEnforcing; Loading @@ -129,7 +129,7 @@ class SoftwareRateLimiter { /** * The number of failed attempts since the last successful attempt, not counting attempts * that never reached the real credential check for a reason such as detection of a * duplicate wrong guess, credential too short, delay still remaining, etc. * duplicate wrong guess, credential too short, timeout still remaining, etc. */ public int numFailures; Loading Loading @@ -205,7 +205,7 @@ class SoftwareRateLimiter { // 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 // delay resets to its original value (for the current failure count) // 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. Loading @@ -214,30 +214,26 @@ class SoftwareRateLimiter { return new RateLimiterState(readFailureCounter(id), guess); }); // Check for remaining delay. Note that the case of a positive remaining delay normally // won't be reached, since reportFailure() will have returned the delay 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 delay. final Duration delay; // 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 >= DELAY_TABLE.length || state.numFailures < 0) { if (state.numFailures >= TIMEOUT_TABLE.length || state.numFailures < 0) { Slogf.e(TAG, "No more guesses allowed; numFailures=%d", state.numFailures); return SoftwareRateLimiterResult.noMoreGuesses(); } delay = DELAY_TABLE[state.numFailures]; originalTimeout = TIMEOUT_TABLE[state.numFailures]; } else { delay = Duration.ZERO; originalTimeout = Duration.ZERO; } final Duration now = mInjector.getTimeSinceBoot(); final Duration remainingDelay = state.timeSinceBootOfLastFailure.plus(delay).minus(now); if (remainingDelay.isPositive()) { Slogf.e( TAG, "Rate-limited; numFailures=%d, remainingDelay=%s", state.numFailures, remainingDelay); return SoftwareRateLimiterResult.rateLimited(remainingDelay); final Duration timeout = state.timeSinceBootOfLastFailure.plus(originalTimeout).minus(now); if (timeout.isPositive()) { Slogf.e(TAG, "Rate-limited; numFailures=%d, timeout=%s", state.numFailures, timeout); return SoftwareRateLimiterResult.rateLimited(timeout); } // Check for duplicate wrong guess. Loading Loading @@ -309,7 +305,7 @@ class SoftwareRateLimiter { * @param guess the LSKF that was attempted * @param isCertainlyWrongGuess true if it's certain that the failure was caused by the guess * being wrong, as opposed to e.g. a transient hardware glitch * @return the delay until when the next guess will be allowed * @return the remaining timeout until when the next guess will be allowed */ synchronized Duration reportFailure( LskfIdentifier id, LockscreenCredential guess, boolean isCertainlyWrongGuess) { Loading @@ -325,10 +321,11 @@ class SoftwareRateLimiter { // 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 // INCORRECT_KEY once the delay becomes nonzero. Instead of making the software rate-limiter // ineffective on all such devices, still apply it. This does mean that correct guesses that // encountered an error will be rate-limited. However, by design the rate-limiter kicks in // gradually anyway, so there will be a chance for the user to try again. // INCORRECT_KEY once the timeout becomes nonzero. Instead of making the software // rate-limiter ineffective on all such devices, still apply it. This does mean that correct // guesses that encountered an error will be rate-limited. However, by design the // rate-limiter kicks in gradually anyway, so there will be a chance for the user to try // again. state.numFailures++; state.timeSinceBootOfLastFailure = mInjector.getTimeSinceBoot(); Loading Loading @@ -364,12 +361,12 @@ class SoftwareRateLimiter { if (!mEnforcing) { return Duration.ZERO; } if (state.numFailures >= DELAY_TABLE.length || state.numFailures < 0) { if (state.numFailures >= TIMEOUT_TABLE.length || state.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 delay again. return DELAY_TABLE[DELAY_TABLE.length - 1]; // convey that information. For now just report the final timeout again. return TIMEOUT_TABLE[TIMEOUT_TABLE.length - 1]; } return DELAY_TABLE[state.numFailures]; return TIMEOUT_TABLE[state.numFailures]; } private static int getStatsCredentialType(LockscreenCredential firstGuess) { Loading Loading @@ -478,8 +475,8 @@ class SoftwareRateLimiter { // Only for unit tests. @VisibleForTesting Duration[] getDelayTable() { return DELAY_TABLE; Duration[] getTimeoutTable() { return TIMEOUT_TABLE; } synchronized void dump(IndentingPrintWriter pw) { Loading
services/core/java/com/android/server/locksettings/SoftwareRateLimiterResult.java +5 −5 Original line number Diff line number Diff line Loading @@ -54,15 +54,15 @@ class SoftwareRateLimiterResult { * preliminary validation done before <em>before</em> the rate-limit check does not have access * to the timeout yet, so none is reported for {@link #CREDENTIAL_TOO_SHORT} either. */ @Nullable public final Duration remainingDelay; @Nullable public final Duration timeout; // Pre-allocate a CONTINUE_TO_HARDWARE result since it is the most common case. private static final SoftwareRateLimiterResult CONTINUE_TO_HARDWARE_RESULT = new SoftwareRateLimiterResult(CONTINUE_TO_HARDWARE, null); private SoftwareRateLimiterResult(@Code int resultCode, Duration remainingDelay) { private SoftwareRateLimiterResult(@Code int resultCode, Duration timeout) { this.code = resultCode; this.remainingDelay = remainingDelay; this.timeout = timeout; } static SoftwareRateLimiterResult credentialTooShort() { Loading @@ -73,8 +73,8 @@ class SoftwareRateLimiterResult { return new SoftwareRateLimiterResult(NO_MORE_GUESSES, null); } static SoftwareRateLimiterResult rateLimited(@NonNull Duration remainingDelay) { return new SoftwareRateLimiterResult(RATE_LIMITED, remainingDelay); static SoftwareRateLimiterResult rateLimited(@NonNull Duration timeout) { return new SoftwareRateLimiterResult(RATE_LIMITED, timeout); } static SoftwareRateLimiterResult duplicateWrongGuess() { Loading
services/tests/servicestests/src/com/android/server/locksettings/LockSettingsServiceTests.java +1 −1 Original line number Diff line number Diff line Loading @@ -761,7 +761,7 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests { // Tests that if verifyCredential is passed a correct guess but it fails due to Weaver reporting // a status of THROTTLE (which is the expected status when there is a remaining rate-limiting // delay in Weaver), then LockSettingsService does not block the same guess from being // timeout in Weaver), then LockSettingsService does not block the same guess from being // re-attempted and in particular does not reject it as a duplicate wrong guess. @Test @EnableFlags(android.security.Flags.FLAG_SOFTWARE_RATELIMITER) Loading
services/tests/servicestests/src/com/android/server/locksettings/SoftwareRateLimiterTest.java +26 −25 Original line number Diff line number Diff line Loading @@ -64,43 +64,44 @@ public class SoftwareRateLimiterTest { public void testRateLimitSchedule() { mRateLimiter = new SoftwareRateLimiter(mInjector); final LskfIdentifier id = new LskfIdentifier(10, 1000); final Duration[] delayTable = mRateLimiter.getDelayTable(); final Duration[] timeoutTable = mRateLimiter.getTimeoutTable(); for (int i = 0; i < delayTable.length; i++) { for (int i = 0; i < timeoutTable.length; i++) { final LockscreenCredential guess = newPassword("password" + i); Duration expectedDelay = delayTable[i]; if (!expectedDelay.isZero()) { verifyRateLimited(id, guess, expectedDelay); Duration expectedTimeout = timeoutTable[i]; if (!expectedTimeout.isZero()) { verifyRateLimited(id, guess, expectedTimeout); mInjector.advanceTime(Duration.ofSeconds(1)); verifyRateLimited(id, guess, expectedDelay.minus(Duration.ofSeconds(1))); mInjector.advanceTime(expectedDelay.minus(Duration.ofMillis(1001))); verifyRateLimited(id, guess, expectedTimeout.minus(Duration.ofSeconds(1))); mInjector.advanceTime(expectedTimeout.minus(Duration.ofMillis(1001))); verifyRateLimited(id, guess, Duration.ofMillis(1)); mInjector.advanceTime(Duration.ofMillis(1)); } verifyUniqueWrongGuess(id, guess, delayTable[Math.min(i + 1, delayTable.length - 1)]); verifyUniqueWrongGuess( id, guess, timeoutTable[Math.min(i + 1, timeoutTable.length - 1)]); verifyFailureCounter(id, i + 1); } verifyNoMoreGuesses(id, newPassword("password")); } // This test re-instantiates the SoftwareRateLimiter, like what happens after a reboot, after // there have already been a certain number of wrong guesses. It verifies that the delay for the // current number of wrong guesses is reset to its original value and starts counting down from // the new instantiation time. // there have already been a certain number of wrong guesses. It verifies that the timeout for // the current number of wrong guesses is reset to its original value and starts counting down // from the new instantiation time. @Test public void testExistingDelayResetsOnReinstantiation() { public void testExistingTimeoutResetsOnReinstantiation() { final LskfIdentifier id = new LskfIdentifier(10, 1000); final LockscreenCredential guess = newPassword("password"); final Duration[] delayTable = mRateLimiter.getDelayTable(); final int numWrongGuesses = delayTable.length - 1; final Duration expectedDelay = delayTable[numWrongGuesses]; final Duration[] timeoutTable = mRateLimiter.getTimeoutTable(); final int numWrongGuesses = timeoutTable.length - 1; final Duration expectedTimeout = timeoutTable[numWrongGuesses]; mInjector.setTime(Duration.ofSeconds(10)); mInjector.writeFailureCounter(id, numWrongGuesses); mRateLimiter = new SoftwareRateLimiter(mInjector); mInjector.setTime(Duration.ofSeconds(1)); verifyRateLimited(id, guess, expectedDelay.minus(Duration.ofSeconds(1))); verifyRateLimited(id, guess, expectedTimeout.minus(Duration.ofSeconds(1))); } @Test Loading Loading @@ -194,7 +195,7 @@ public class SoftwareRateLimiterTest { for (int i = 0; i < n; i++) { verifyUniqueWrongGuess(id, newPassword("password" + i)); mInjector.advanceTime(Duration.ofDays(1)); // Advance past any delay mInjector.advanceTime(Duration.ofDays(1)); // Advance past any timeout } SoftwareRateLimiterResult result = mRateLimiter.apply(id, newPassword("password0")); assertThat(result.code).isEqualTo(expectedResultCode); Loading @@ -206,7 +207,7 @@ public class SoftwareRateLimiterTest { for (int i = 0; i < SoftwareRateLimiter.MAX_SAVED_WRONG_GUESSES; i++) { verifyUniqueWrongGuess(id, newPassword("password" + i)); mInjector.advanceTime(Duration.ofDays(1)); // Advance past any delay mInjector.advanceTime(Duration.ofDays(1)); // Advance past any timeout } // The list of saved guesses should now be full. Try the oldest one, which should cause it // to be moved to the front of the list. Loading @@ -214,7 +215,7 @@ public class SoftwareRateLimiterTest { // Try a new guess. Then verify that it caused the eviction of password1, not password0. verifyUniqueWrongGuess(id, newPassword("passwordN")); mInjector.advanceTime(Duration.ofDays(1)); // Advance past any delay mInjector.advanceTime(Duration.ofDays(1)); // Advance past any timeout verifyDuplicateWrongGuess(id, newPassword("password0")); verifyUniqueWrongGuess(id, newPassword("password1")); } Loading Loading @@ -496,10 +497,10 @@ public class SoftwareRateLimiterTest { } private void verifyRateLimited( LskfIdentifier id, LockscreenCredential guess, Duration expectedRemainingDelay) { LskfIdentifier id, LockscreenCredential guess, Duration expectedTimeout) { SoftwareRateLimiterResult result = mRateLimiter.apply(id, guess); assertThat(result.code).isEqualTo(RATE_LIMITED); assertThat(result.remainingDelay).isEqualTo(expectedRemainingDelay); assertThat(result.timeout).isEqualTo(expectedTimeout); } // Verifies that the rate-limiter returns a status of DUPLICATE_WRONG_GUESS for the given guess. Loading @@ -516,14 +517,14 @@ public class SoftwareRateLimiterTest { mRateLimiter.reportFailure(id, guess, /* isCertainlyWrongGuess= */ true); } // Same as above but also verifies the next delay reported by reportFailure(). // Same as above but also verifies the next timeout reported by reportFailure(). private void verifyUniqueWrongGuess( LskfIdentifier id, LockscreenCredential guess, Duration expectedNextDelay) { LskfIdentifier id, LockscreenCredential guess, Duration expectedNextTimeout) { SoftwareRateLimiterResult result = mRateLimiter.apply(id, guess); assertThat(result.code).isEqualTo(CONTINUE_TO_HARDWARE); Duration nextDelay = Duration nextTimeout = mRateLimiter.reportFailure(id, guess, /* isCertainlyWrongGuess= */ true); assertThat(nextDelay).isEqualTo(expectedNextDelay); assertThat(nextTimeout).isEqualTo(expectedNextTimeout); } private void verifyFailureCounter(LskfIdentifier id, int expectedValue) { Loading