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

Commit 1f34ce76 authored by Eric Biggers's avatar Eric Biggers Committed by Android (Google) Code Review
Browse files

Merge "Detect duplicates only of certainly-wrong guesses" into main

parents 2638d5a3 f9a21d0b
Loading
Loading
Loading
Loading
+6 −2
Original line number Diff line number Diff line
@@ -2549,8 +2549,12 @@ public class LockSettingsService extends ILockSettings.Stub {
            if (response.isMatched()) {
                mSoftwareRateLimiter.reportSuccess(lskfId);
            } else {
                // TODO(b/395976735): don't count transient failures
                Duration swTimeout = mSoftwareRateLimiter.reportWrongGuess(lskfId, credential);
                boolean isCertainlyWrongGuess =
                        response.getResponseCode()
                                == VerifyCredentialResponse.RESPONSE_CRED_INCORRECT;
                Duration swTimeout =
                        mSoftwareRateLimiter.reportFailure(
                                lskfId, credential, isCertainlyWrongGuess);

                // The software rate-limiter may use longer delays than the hardware one. While the
                // long-term solution is to update the hardware rate-limiter to match, for now this
+44 −31
Original line number Diff line number Diff line
@@ -224,10 +224,10 @@ class SoftwareRateLimiter {
                        });

        // Check for remaining delay. Note that the case of a positive remaining delay normally
        // won't be reached, since reportWrongGuess() 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.
        // 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 = getCurrentDelay(state);
        final Duration now = mInjector.getTimeSinceBoot();
        final Duration remainingDelay = state.timeSinceBootOfLastWrongGuess.plus(delay).minus(now);
@@ -298,27 +298,37 @@ class SoftwareRateLimiter {
    }

    /**
     * Reports a new wrong guess to the software rate-limiter.
     * Reports a failure to the software rate-limiter.
     *
     * <p>This must be called immediately after the hardware rate-limiter reported that the given
     * guess is incorrect, before the credential check failure is made visible in the UI. It is
     * assumed that {@link #apply(LskfIdentifier, LockscreenCredential)} was previously called with
     * the same parameters and returned a {@code CONTINUE_TO_HARDWARE} result.
     * <p>This must be called immediately after the hardware rate-limiter reported a failure, before
     * the credential check failure is made visible in the UI. It is assumed that {@link
     * #apply(LskfIdentifier, LockscreenCredential)} was previously called with the same parameters
     * and returned a {@code CONTINUE_TO_HARDWARE} result.
     *
     * @param id the ID of the protector or special credential
     * @param newWrongGuess a new wrong guess for the LSKF
     * @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
     */
    synchronized Duration reportWrongGuess(LskfIdentifier id, LockscreenCredential newWrongGuess) {
    synchronized Duration reportFailure(
            LskfIdentifier id, LockscreenCredential guess, boolean isCertainlyWrongGuess) {
        RateLimiterState state = getExistingState(id);

        // In non-enforcing mode, ignore duplicate wrong guesses here since they were already
        // counted by apply(), including having stats written for them. In enforcing mode, this
        // method isn't passed duplicate wrong guesses.
        if (!mEnforcing && ArrayUtils.contains(state.savedWrongGuesses, newWrongGuess)) {
        if (!mEnforcing && ArrayUtils.contains(state.savedWrongGuesses, guess)) {
            return Duration.ZERO;
        }

        // 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.
        state.numWrongGuesses++;
        state.timeSinceBootOfLastWrongGuess = mInjector.getTimeSinceBoot();

@@ -329,7 +339,9 @@ class SoftwareRateLimiter {

        writeStats(id, state, /* success= */ false);

        insertNewWrongGuess(state, newWrongGuess);
        // Save certainly wrong guesses so that duplicates of them can be detected.
        if (isCertainlyWrongGuess) {
            insertNewWrongGuess(state, guess);

            // Schedule the saved wrong guesses to be forgotten after a few minutes, extending the
            // existing timeout if one was already running.
@@ -347,6 +359,7 @@ class SoftwareRateLimiter {
                    },
                    /* token= */ state,
                    SAVED_WRONG_GUESS_TIMEOUT.toMillis());
        }

        return getCurrentDelay(state);
    }
@@ -384,7 +397,7 @@ class SoftwareRateLimiter {
    private RateLimiterState getExistingState(LskfIdentifier id) {
        RateLimiterState state = mState.get(id);
        if (state == null) {
            // This should never happen, since reportSuccess() and reportWrongGuess() are always
            // This should never happen, since reportSuccess() and reportFailure() are always
            // supposed to be paired with a call to apply() that created the state if it did not
            // exist. Nor is it supported to call clearLskfState() or clearUserState() in between;
            // higher-level locking in LockSettingsService guarantees that never happens.
+0 −3
Original line number Diff line number Diff line
@@ -723,8 +723,6 @@ class SyntheticPasswordManager {
                // the credential was incorrect and there is a timeout before the next attempt will
                // be allowed. INCORRECT_KEY is preferred in the latter case to avoid the ambiguity,
                // but we still have to support implementations that use THROTTLE for both cases.
                //
                // TODO(b/395976735): needs unit testing via MockWeaverService
                return responseFromTimeout(weaverResponse);
            case WeaverReadStatus.INCORRECT_KEY:
                if (weaverResponse.timeout != 0) {
@@ -734,7 +732,6 @@ class SyntheticPasswordManager {
                    //
                    // TODO(b/395976735): use RESPONSE_CRED_INCORRECT in this case, and update users
                    // of VerifyCredentialResponse to be compatible with that.
                    // TODO(b/395976735): needs unit testing via MockWeaverService
                    return responseFromTimeout(weaverResponse);
                }
                if (android.security.Flags.softwareRatelimiter()) {
+106 −0
Original line number Diff line number Diff line
@@ -39,6 +39,7 @@ import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.Intent;
import android.hardware.weaver.WeaverReadStatus;
import android.os.RemoteException;
import android.os.UserHandle;
import android.platform.test.annotations.DisableFlags;
@@ -57,10 +58,13 @@ import com.android.internal.widget.LockscreenCredential;
import com.android.internal.widget.VerifyCredentialResponse;

import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;

import java.time.Duration;

/**
 * atest FrameworksServicesTests:LockSettingsServiceTests
 */
@@ -719,6 +723,108 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests {
        assertEquals(VerifyCredentialResponse.RESPONSE_OTHER_ERROR, response.getResponseCode());
    }

    // Tests that if verifyCredential is passed a wrong guess and Weaver reports INCORRECT_KEY with
    // zero timeout (which indicates a certainly wrong guess), then LockSettingsService saves that
    // guess as a recent wrong guess and rejects a repeat of it as a duplicate.
    @Test
    @EnableFlags(android.security.Flags.FLAG_SOFTWARE_RATELIMITER)
    public void testRepeatOfWrongGuessRejectedAsDuplicate_afterWeaverIncorrectKeyWithoutTimeout()
            throws Exception {
        final int userId = PRIMARY_USER_ID;
        final LockscreenCredential credential = newPassword("password");
        final LockscreenCredential wrongGuess = newPassword("wrong");

        mSpManager.enableWeaver();
        setCredential(userId, credential);

        mSpManager.injectWeaverReadResponse(WeaverReadStatus.INCORRECT_KEY, Duration.ZERO);
        VerifyCredentialResponse response =
                mService.verifyCredential(wrongGuess, userId, /* flags= */ 0);
        assertEquals(VerifyCredentialResponse.RESPONSE_CRED_INCORRECT, response.getResponseCode());
        assertEquals(Duration.ZERO, response.getTimeoutAsDuration());

        response = mService.verifyCredential(wrongGuess, userId, /* flags= */ 0);
        assertEquals(
                VerifyCredentialResponse.RESPONSE_CRED_ALREADY_TRIED, response.getResponseCode());
        assertEquals(Duration.ZERO, response.getTimeoutAsDuration());
    }

    // Same as preceding test case, but uses a nonzero timeout.
    //
    // TODO(b/395976735): currently the behavior in this scenario is wrong, so currently this test
    // case is ignored. Fix the behavior and remove @Ignore.
    @Ignore
    @Test
    @EnableFlags(android.security.Flags.FLAG_SOFTWARE_RATELIMITER)
    public void testRepeatOfWrongGuessRejectedAsDuplicate_afterWeaverIncorrectKeyWithTimeout()
            throws Exception {
        final int userId = PRIMARY_USER_ID;
        final LockscreenCredential credential = newPassword("password");
        final LockscreenCredential wrongGuess = newPassword("wrong");
        final Duration timeout = Duration.ofSeconds(60);

        mSpManager.enableWeaver();
        setCredential(userId, credential);

        mSpManager.injectWeaverReadResponse(WeaverReadStatus.INCORRECT_KEY, timeout);
        VerifyCredentialResponse response =
                mService.verifyCredential(wrongGuess, userId, /* flags= */ 0);
        assertEquals(VerifyCredentialResponse.RESPONSE_RETRY, response.getResponseCode());
        assertEquals(timeout, response.getTimeoutAsDuration());

        response = mService.verifyCredential(wrongGuess, userId, /* flags= */ 0);
        assertEquals(
                VerifyCredentialResponse.RESPONSE_CRED_ALREADY_TRIED, response.getResponseCode());
        assertEquals(Duration.ZERO, response.getTimeoutAsDuration());
    }

    // 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
    // re-attempted and in particular does not reject it as a duplicate wrong guess.
    @Test
    @EnableFlags(android.security.Flags.FLAG_SOFTWARE_RATELIMITER)
    public void testRepeatOfCorrectGuessAllowed_afterWeaverThrottle() throws Exception {
        final int userId = PRIMARY_USER_ID;
        final LockscreenCredential credential = newPassword("password");
        final Duration timeout = Duration.ofSeconds(60);

        mSpManager.enableWeaver();
        setCredential(userId, credential);

        mSpManager.injectWeaverReadResponse(WeaverReadStatus.THROTTLE, timeout);
        VerifyCredentialResponse response =
                mService.verifyCredential(credential, userId, /* flags= */ 0);
        assertEquals(VerifyCredentialResponse.RESPONSE_RETRY, response.getResponseCode());
        assertEquals(timeout, response.getTimeoutAsDuration());

        response = mService.verifyCredential(credential, userId, /* flags= */ 0);
        assertTrue(response.isMatched());
    }

    // Tests that if verifyCredential is passed a correct guess but it fails due to Weaver reporting
    // a status of FAILED (which is the expected status when there is a transient error unrelated to
    // the guess), 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)
    public void testRepeatOfCorrectGuessAllowed_afterWeaverFailed() throws Exception {
        final int userId = PRIMARY_USER_ID;
        final LockscreenCredential credential = newPassword("password");

        mSpManager.enableWeaver();
        setCredential(userId, credential);

        mSpManager.injectWeaverReadResponse(WeaverReadStatus.FAILED, Duration.ZERO);
        VerifyCredentialResponse response =
                mService.verifyCredential(credential, userId, /* flags= */ 0);
        assertEquals(VerifyCredentialResponse.RESPONSE_OTHER_ERROR, response.getResponseCode());
        assertEquals(Duration.ZERO, response.getTimeoutAsDuration());

        response = mService.verifyCredential(credential, userId, /* flags= */ 0);
        assertTrue(response.isMatched());
    }

    private void checkRecordedFrpNotificationIntent() {
        if (android.security.Flags.frpEnforcement()) {
            Intent savedNotificationIntent = mService.getSavedFrpNotificationIntent();
+6 −0
Original line number Diff line number Diff line
@@ -28,6 +28,7 @@ import junit.framework.AssertionFailedError;
import java.nio.ByteBuffer;
import java.security.NoSuchAlgorithmException;
import java.security.spec.InvalidKeySpecException;
import java.time.Duration;
import java.util.Arrays;

import javax.crypto.SecretKeyFactory;
@@ -152,4 +153,9 @@ public class MockSyntheticPasswordManager extends SyntheticPasswordManager {
    public int getSumOfWeaverFailureCounters() {
        return mWeaverService.getSumOfFailureCounters();
    }

    /** Injects a response to be returned by the next read from Weaver. */
    public void injectWeaverReadResponse(int status, Duration timeout) {
        mWeaverService.injectReadResponse(status, timeout);
    }
}
Loading