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

Commit 5799bc7d authored by Eric Biggers's avatar Eric Biggers
Browse files

Don't allow more than 20 failed primary authentication attempts

The following has existed in section 9.11.1 [C-SR-5] of the CDD since
Android 14: "Device implementations are STRONGLY RECOMMENDED to
implement an upper bound of 20 failed primary authentication attempts".

To align with that strong recommendation, update the SoftwareRateLimiter
to explicitly forbid additional guesses after the failure counter
reaches 20, when it is operating in enforcing mode.

Note that this has no practical impact, since any guesses after the 19th
could be made no faster than 1 per 9.09 years anyway.  The 21st could
only be made at least 22.7 years from the start.

Bug: 430642788
Test: atest FrameworksServicesTests:com.android.server.locksettings
Flag: android.security.software_ratelimiter
Change-Id: I56f4147989513ae80c56927ca91b5cbe391cc450
parent 59b8756d
Loading
Loading
Loading
Loading
+5 −1
Original line number Diff line number Diff line
@@ -645,6 +645,10 @@ public class LockSettingsService extends ILockSettings.Stub {
        public boolean isHeadlessSystemUserMode() {
            return UserManager.isHeadlessSystemUserMode();
        }

        public Duration getTimeSinceBoot() {
            return Duration.ofMillis(SystemClock.elapsedRealtime());
        }
    }

    private class SoftwareRateLimiterInjector implements SoftwareRateLimiter.Injector {
@@ -661,7 +665,7 @@ public class LockSettingsService extends ILockSettings.Stub {

        @Override
        public Duration getTimeSinceBoot() {
            return Duration.ofMillis(SystemClock.elapsedRealtime());
            return mInjector.getTimeSinceBoot();
        }

        @Override
+20 −13
Original line number Diff line number Diff line
@@ -85,7 +85,7 @@ class SoftwareRateLimiter {

    /**
     * A table that maps the number of (real) failures to the delay that is enforced after that
     * number of (real) failures. Out-of-bounds indices default to the final delay.
     * number of (real) failures. Out-of-bounds indices default to not allowed.
     */
    private static final Duration[] DELAY_TABLE =
            new Duration[] {
@@ -172,16 +172,6 @@ class SoftwareRateLimiter {
        mEnforcing = enforcing;
    }

    private Duration getCurrentDelay(RateLimiterState state) {
        if (!mEnforcing) {
            return Duration.ZERO;
        } else if (state.numFailures >= 0 && state.numFailures < DELAY_TABLE.length) {
            return DELAY_TABLE[state.numFailures];
        } else {
            return DELAY_TABLE[DELAY_TABLE.length - 1];
        }
    }

    /**
     * Applies the software rate-limiter to the given LSKF guess.
     *
@@ -229,7 +219,16 @@ class SoftwareRateLimiter {
        // 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 delay;
        if (mEnforcing) {
            if (state.numFailures >= DELAY_TABLE.length || state.numFailures < 0) {
                Slogf.e(TAG, "No more guesses allowed; numFailures=%d", state.numFailures);
                return SoftwareRateLimiterResult.noMoreGuesses();
            }
            delay = DELAY_TABLE[state.numFailures];
        } else {
            delay = Duration.ZERO;
        }
        final Duration now = mInjector.getTimeSinceBoot();
        final Duration remainingDelay = state.timeSinceBootOfLastFailure.plus(delay).minus(now);
        if (remainingDelay.isPositive()) {
@@ -362,7 +361,15 @@ class SoftwareRateLimiter {
                    SAVED_WRONG_GUESS_TIMEOUT.toMillis());
        }

        return getCurrentDelay(state);
        if (!mEnforcing) {
            return Duration.ZERO;
        }
        if (state.numFailures >= DELAY_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];
        }
        return DELAY_TABLE[state.numFailures];
    }

    private static int getStatsCredentialType(LockscreenCredential firstGuess) {
+9 −3
Original line number Diff line number Diff line
@@ -27,14 +27,16 @@ import java.time.Duration;
/** The result from the {@link SoftwareRateLimiter} */
class SoftwareRateLimiterResult {
    public static final int CREDENTIAL_TOO_SHORT = 0;
    public static final int RATE_LIMITED = 1;
    public static final int DUPLICATE_WRONG_GUESS = 2;
    public static final int CONTINUE_TO_HARDWARE = 3;
    public static final int NO_MORE_GUESSES = 1;
    public static final int RATE_LIMITED = 2;
    public static final int DUPLICATE_WRONG_GUESS = 3;
    public static final int CONTINUE_TO_HARDWARE = 4;

    @Retention(RetentionPolicy.SOURCE)
    @IntDef(
            value = {
                CREDENTIAL_TOO_SHORT,
                NO_MORE_GUESSES,
                RATE_LIMITED,
                DUPLICATE_WRONG_GUESS,
                CONTINUE_TO_HARDWARE,
@@ -67,6 +69,10 @@ class SoftwareRateLimiterResult {
        return new SoftwareRateLimiterResult(CREDENTIAL_TOO_SHORT, null);
    }

    static SoftwareRateLimiterResult noMoreGuesses() {
        return new SoftwareRateLimiterResult(NO_MORE_GUESSES, null);
    }

    static SoftwareRateLimiterResult rateLimited(@NonNull Duration remainingDelay) {
        return new SoftwareRateLimiterResult(RATE_LIMITED, remainingDelay);
    }
+14 −0
Original line number Diff line number Diff line
@@ -41,6 +41,7 @@ import com.android.server.pm.UserManagerInternal;

import java.io.FileNotFoundException;
import java.security.KeyStore;
import java.time.Duration;

public class LockSettingsServiceTestable extends LockSettingsService {
    private Intent mSavedFrpNotificationIntent = null;
@@ -58,6 +59,7 @@ public class LockSettingsServiceTestable extends LockSettingsService {
        private RecoverableKeyStoreManager mRecoverableKeyStoreManager;
        private UserManagerInternal mUserManagerInternal;
        private DeviceStateCache mDeviceStateCache;
        private Duration mTimeSinceBoot;

        public boolean mIsHeadlessSystemUserMode = false;

@@ -148,6 +150,18 @@ public class LockSettingsServiceTestable extends LockSettingsService {
        public boolean isHeadlessSystemUserMode() {
            return mIsHeadlessSystemUserMode;
        }

        void setTimeSinceBoot(Duration time) {
            mTimeSinceBoot = time;
        }

        @Override
        public Duration getTimeSinceBoot() {
            if (mTimeSinceBoot != null) {
                return mTimeSinceBoot;
            }
            return super.getTimeSinceBoot();
        }
    }

    protected LockSettingsServiceTestable(
+59 −0
Original line number Diff line number Diff line
@@ -806,6 +806,65 @@ public class LockSettingsServiceTests extends BaseLockSettingsServiceTests {
        assertTrue(response.isMatched());
    }

    @Test
    @EnableFlags(android.security.Flags.FLAG_SOFTWARE_RATELIMITER)
    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);
        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);
        }
        response = mService.verifyCredential(credential, userId, /* flags= */ 0);
        assertTrue(response.isMatched());
    }

    @Test
    @EnableFlags(android.security.Flags.FLAG_SOFTWARE_RATELIMITER)
    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);
        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);
        }
        response = mService.verifyCredential(credential, userId, /* flags= */ 0);
        assertFalse(response.isMatched());
    }

    @Test
    @DisableFlags(android.security.Flags.FLAG_SOFTWARE_RATELIMITER)
    public void testMoreThan20UniqueGuessesAllowed_softwareRateLimiterFlagDisabled()
            throws Exception {
        final int userId = PRIMARY_USER_ID;
        final LockscreenCredential credential = newPassword("password");
        VerifyCredentialResponse response;

        setCredential(userId, credential);
        for (int i = 0; i < 20; i++) {
            response = mService.verifyCredential(newPassword("wrong" + i), userId, /* flags= */ 0);
            assertFalse(response.isMatched());
        }
        response = mService.verifyCredential(credential, userId, /* flags= */ 0);
        assertTrue(response.isMatched());
    }

    @Test
    public void testVerifyCredentialResponseTimeoutClamping() {
        testTimeoutClamping(Duration.ofMillis(Long.MIN_VALUE), Integer.MAX_VALUE);
Loading