Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f754ecb5 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Rename "delay" to "timeout" in the SoftwareRateLimiter" into main

parents 34d0b07f 3602f27b
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -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:
+30 −33
Original line number Diff line number Diff line
@@ -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,
@@ -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;

@@ -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;

@@ -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.
@@ -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.
@@ -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) {
@@ -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();

@@ -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) {
@@ -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) {
+5 −5
Original line number Diff line number Diff line
@@ -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() {
@@ -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() {
+1 −1
Original line number Diff line number Diff line
@@ -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)
+26 −25
Original line number Diff line number Diff line
@@ -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
@@ -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);
@@ -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.
@@ -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"));
    }
@@ -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.
@@ -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) {