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

Commit 1416bd02 authored by Andrew Scull's avatar Andrew Scull
Browse files

Ensure a user's SP does't change.

Changes of the SP are caused by untrusted credential reset which can be
triggered by certain admin modes. When such an admin is active, the SP
needs to be cached. Untrusted reset will be removed in a future release
at which point this caching can also be removed.

Bug: 71527305
Test: runtest frameworks-services -p com.android.server.locksettings
Test: cts-tradefed run cts-dev -m CtsDevicePolicyManagerTestCases -t com.android.cts.devicepolicy
Change-Id: I54f3b299b79ce019ba679b5550d37fd090b679fb
parent 23374d63
Loading
Loading
Loading
Loading
+9 −0
Original line number Diff line number Diff line
@@ -123,4 +123,13 @@ public abstract class DevicePolicyManagerInternal {
     * @param userId User ID of the profile.
     */
    public abstract void reportSeparateProfileChallengeChanged(@UserIdInt int userId);

    /**
     * Check whether the user could have their password reset in an untrusted manor due to there
     * being an admin which can call {@link #resetPassword} to reset the password without knowledge
     * of the previous password.
     *
     * @param userId The user in question
     */
    public abstract boolean canUserHaveUntrustedCredentialReset(@UserIdInt int userId);
}
+95 −9
Original line number Diff line number Diff line
@@ -90,6 +90,7 @@ import android.util.ArrayMap;
import android.util.EventLog;
import android.util.Log;
import android.util.Slog;
import android.util.SparseArray;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
@@ -1874,6 +1875,7 @@ public class LockSettingsService extends ILockSettings.Stub {
        mSpManager.removeUser(userId);
        mStorage.removeUser(userId);
        mStrongAuth.removeUser(userId);
        cleanSpCache();

        final KeyStore ks = KeyStore.getInstance();
        ks.onUserRemoved(userId);
@@ -2111,6 +2113,63 @@ public class LockSettingsService extends ILockSettings.Stub {
        return null;
    }

    /**
     * A user's synthetic password does not change so it must be cached in certain circumstances to
     * enable untrusted credential reset.
     *
     * Untrusted credential reset will be removed in a future version (b/68036371) at which point
     * this cache is no longer needed as the SP will always be known when changing the user's
     * credential.
     */
    @GuardedBy("mSpManager")
    private SparseArray<AuthenticationToken> mSpCache = new SparseArray();

    private void onAuthTokenKnownForUser(@UserIdInt int userId, AuthenticationToken auth) {
        // Update the SP cache, removing the entry when allowed
        synchronized (mSpManager) {
            if (shouldCacheSpForUser(userId)) {
                Slog.i(TAG, "Caching SP for user " + userId);
                mSpCache.put(userId, auth);
            } else {
                Slog.i(TAG, "Not caching SP for user " + userId);
                mSpCache.delete(userId);
            }
        }
    }

    /** Clean up the SP cache by removing unneeded entries. */
    private void cleanSpCache() {
        synchronized (mSpManager) {
            // Preserve indicies after removal by iterating backwards
            for (int i = mSpCache.size() - 1; i >= 0; --i) {
                final int userId = mSpCache.keyAt(i);
                if (!shouldCacheSpForUser(userId)) {
                    Slog.i(TAG, "Uncaching SP for user " + userId);
                    mSpCache.removeAt(i);
                }
            }
        }
    }

    private boolean shouldCacheSpForUser(@UserIdInt int userId) {
        // Before the user setup has completed, an admin could be installed that requires the SP to
        // be cached (see below).
        if (Settings.Secure.getIntForUser(mContext.getContentResolver(),
                    Settings.Secure.USER_SETUP_COMPLETE, 0, userId) == 0) {
            return true;
        }

        // If the user has an admin which can perform an untrusted credential reset, the SP needs to
        // be cached. If there isn't a DevicePolicyManager then there can't be an admin in the first
        // place so caching is not necessary.
        final DevicePolicyManagerInternal dpmi = LocalServices.getService(
                DevicePolicyManagerInternal.class);
        if (dpmi == null) {
            return false;
        }
        return dpmi.canUserHaveUntrustedCredentialReset(userId);
    }

    /**
     * Precondition: vold and keystore unlocked.
     *
@@ -2126,9 +2185,7 @@ public class LockSettingsService extends ILockSettings.Stub {
     * 3. Once a user is migrated to have synthetic password, its value will never change, no matter
     *     whether the user changes his lockscreen PIN or clear/reset it. When the user clears its
     *     lockscreen PIN, we still maintain the existing synthetic password in a password blob
     *     protected by a default PIN. The only exception is when the DPC performs an untrusted
     *     credential change, in which case we have no way to derive the existing synthetic password
     *     and has to create a new one.
     *     protected by a default PIN.
     * 4. The user SID is linked with synthetic password, but its cleared/re-created when the user
     *     clears/re-creates his lockscreen PIN.
     *
@@ -2148,13 +2205,23 @@ public class LockSettingsService extends ILockSettings.Stub {
     *     This is the untrusted credential reset, OR the user sets a new lockscreen password
     *     FOR THE FIRST TIME on a SP-enabled device. New credential and new SID will be created
     */
    @GuardedBy("mSpManager")
    @VisibleForTesting
    protected AuthenticationToken initializeSyntheticPasswordLocked(byte[] credentialHash,
            String credential, int credentialType, int requestedQuality,
            int userId) throws RemoteException {
        Slog.i(TAG, "Initialize SyntheticPassword for user: " + userId);
        AuthenticationToken auth = mSpManager.newSyntheticPasswordAndSid(getGateKeeperService(),
        // Load from the cache or a make a new one
        AuthenticationToken auth = mSpCache.get(userId);
        if (auth != null) {
            // If the synthetic password has been cached, we can only be in case 3., described
            // above, for an untrusted credential reset so a new SID is still needed.
            mSpManager.newSidForUser(getGateKeeperService(), auth, userId);
        } else {
            auth = mSpManager.newSyntheticPasswordAndSid(getGateKeeperService(),
                      credentialHash, credential, userId);
        }
        onAuthTokenKnownForUser(userId, auth);
        if (auth == null) {
            Slog.wtf(TAG, "initializeSyntheticPasswordLocked returns null auth token");
            return null;
@@ -2269,6 +2336,8 @@ public class LockSettingsService extends ILockSettings.Stub {
                trustManager.setDeviceLockedForUser(userId, false);
            }
            mStrongAuth.reportSuccessfulStrongAuthUnlock(userId);

            onAuthTokenKnownForUser(userId, authResult.authToken);
        } else if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_RETRY) {
            if (response.getTimeout() > 0) {
                requireStrongAuth(STRONG_AUTH_REQUIRED_AFTER_LOCKOUT, userId);
@@ -2287,6 +2356,7 @@ public class LockSettingsService extends ILockSettings.Stub {
     * SID is gone. We also clear password from (software-based) keystore and vold, which will be
     * added back when new password is set in future.
     */
    @GuardedBy("mSpManager")
    private long setLockCredentialWithAuthTokenLocked(String credential, int credentialType,
            AuthenticationToken auth, int requestedQuality, int userId) throws RemoteException {
        if (DEBUG) Slog.d(TAG, "setLockCredentialWithAuthTokenLocked: user=" + userId);
@@ -2334,6 +2404,7 @@ public class LockSettingsService extends ILockSettings.Stub {
        return newHandle;
    }

    @GuardedBy("mSpManager")
    private void spBasedSetLockCredentialInternalLocked(String credential, int credentialType,
            String savedCredential, int requestedQuality, int userId) throws RemoteException {
        if (DEBUG) Slog.d(TAG, "spBasedSetLockCredentialInternalLocked: user=" + userId);
@@ -2369,13 +2440,19 @@ public class LockSettingsService extends ILockSettings.Stub {
            setLockCredentialWithAuthTokenLocked(credential, credentialType, auth, requestedQuality,
                    userId);
            mSpManager.destroyPasswordBasedSyntheticPassword(handle, userId);
            onAuthTokenKnownForUser(userId, auth);
        } else if (response != null
                && response.getResponseCode() == VerifyCredentialResponse.RESPONSE_ERROR) {
            // We are performing an untrusted credential change i.e. by DevicePolicyManager.
            // So provision a new SP and SID. This would invalidate existing escrow tokens.
            // Still support this for now but this flow will be removed in the next release.

            Slog.w(TAG, "Untrusted credential change invoked");

            if (mSpCache.get(userId) == null) {
                throw new IllegalStateException(
                        "Untrusted credential reset not possible without cached SP");
            }

            initializeSyntheticPasswordLocked(null, credential, credentialType, requestedQuality,
                    userId);
            synchronizeUnifiedWorkChallengeForProfiles(userId, null);
@@ -2486,8 +2563,9 @@ public class LockSettingsService extends ILockSettings.Stub {

    private boolean setLockCredentialWithTokenInternal(String credential, int type,
            long tokenHandle, byte[] token, int requestedQuality, int userId) throws RemoteException {
        final AuthenticationResult result;
        synchronized (mSpManager) {
            AuthenticationResult result = mSpManager.unwrapTokenBasedSyntheticPassword(
            result = mSpManager.unwrapTokenBasedSyntheticPassword(
                    getGateKeeperService(), tokenHandle, token, userId);
            if (result.authToken == null) {
                Slog.w(TAG, "Invalid escrow token supplied");
@@ -2508,8 +2586,9 @@ public class LockSettingsService extends ILockSettings.Stub {
            setLockCredentialWithAuthTokenLocked(credential, type, result.authToken,
                    requestedQuality, userId);
            mSpManager.destroyPasswordBasedSyntheticPassword(oldHandle, userId);
            return true;
        }
        onAuthTokenKnownForUser(userId, result.authToken);
        return true;
    }

    @Override
@@ -2529,6 +2608,7 @@ public class LockSettingsService extends ILockSettings.Stub {
            }
        }
        unlockUser(userId, null, authResult.authToken.deriveDiskEncryptionKey());
        onAuthTokenKnownForUser(userId, authResult.authToken);
    }

    @Override
@@ -2610,6 +2690,8 @@ public class LockSettingsService extends ILockSettings.Stub {
    private class DeviceProvisionedObserver extends ContentObserver {
        private final Uri mDeviceProvisionedUri = Settings.Global.getUriFor(
                Settings.Global.DEVICE_PROVISIONED);
        private final Uri mUserSetupCompleteUri = Settings.Secure.getUriFor(
                Settings.Secure.USER_SETUP_COMPLETE);

        private boolean mRegistered;

@@ -2627,6 +2709,8 @@ public class LockSettingsService extends ILockSettings.Stub {
                    reportDeviceSetupComplete();
                    clearFrpCredentialIfOwnerNotSecure();
                }
            } else if (mUserSetupCompleteUri.equals(uri)) {
                cleanSpCache();
            }
        }

@@ -2678,6 +2762,8 @@ public class LockSettingsService extends ILockSettings.Stub {
            if (register) {
                mContext.getContentResolver().registerContentObserver(mDeviceProvisionedUri,
                        false, this);
                mContext.getContentResolver().registerContentObserver(mUserSetupCompleteUri,
                        false, this, UserHandle.USER_ALL);
            } else {
                mContext.getContentResolver().unregisterContentObserver(this);
            }
+30 −3
Original line number Diff line number Diff line
@@ -4503,6 +4503,28 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
        }
    }
    private boolean canPOorDOCallResetPassword(ActiveAdmin admin, @UserIdInt int userId) {
        // Only if the admins targets a pre-O SDK
        return getTargetSdk(admin.info.getPackageName(), userId) < Build.VERSION_CODES.O;
    }
    /* PO or DO could do an untrusted reset in certain conditions. */
    private boolean canUserHaveUntrustedCredentialReset(@UserIdInt int userId) {
        synchronized (this) {
            // An active DO or PO might be able to fo an untrusted credential reset
            for (final ActiveAdmin admin : getUserData(userId).mAdminList) {
                if (!isActiveAdminWithPolicyForUserLocked(admin,
                          DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, userId)) {
                    continue;
                }
                if (canPOorDOCallResetPassword(admin, userId)) {
                    return true;
                }
            }
            return false;
        }
    }
    @Override
    public boolean resetPassword(String passwordOrNull, int flags) throws RemoteException {
        final int callingUid = mInjector.binderGetCallingUid();
@@ -4521,12 +4543,12 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
                    null, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER, callingUid);
            final boolean preN;
            if (admin != null) {
                final int targetSdk = getTargetSdk(admin.info.getPackageName(), userHandle);
                if (targetSdk >= Build.VERSION_CODES.O) {
                if (!canPOorDOCallResetPassword(admin, userHandle)) {
                    throw new SecurityException("resetPassword() is deprecated for DPC targeting O"
                            + " or later");
                }
                preN = targetSdk <= android.os.Build.VERSION_CODES.M;
                preN = getTargetSdk(admin.info.getPackageName(),
                        userHandle) <= android.os.Build.VERSION_CODES.M;
            } else {
                // Otherwise, make sure the caller has any active admin with the right policy.
                admin = getActiveAdminForCallerLocked(null,
@@ -10096,6 +10118,11 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager {
                updateMaximumTimeToLockLocked(userId);
            }
        }
        @Override
        public boolean canUserHaveUntrustedCredentialReset(@UserIdInt int userId) {
            return DevicePolicyManagerService.this.canUserHaveUntrustedCredentialReset(userId);
        }
    }
    private Intent createShowAdminSupportIntent(ComponentName admin, int userId) {
+7 −0
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import static org.mockito.Mockito.when;
import android.app.IActivityManager;
import android.app.NotificationManager;
import android.app.admin.DevicePolicyManager;
import android.app.admin.DevicePolicyManagerInternal;
import android.app.trust.TrustManager;
import android.content.ComponentName;
import android.content.pm.UserInfo;
@@ -41,6 +42,7 @@ import android.test.AndroidTestCase;

import com.android.internal.widget.ILockSettings;
import com.android.internal.widget.LockPatternUtils;
import com.android.server.LocalServices;

import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
@@ -75,6 +77,7 @@ public class BaseLockSettingsServiceTests extends AndroidTestCase {
    FakeStorageManager mStorageManager;
    IActivityManager mActivityManager;
    DevicePolicyManager mDevicePolicyManager;
    DevicePolicyManagerInternal mDevicePolicyManagerInternal;
    KeyStore mKeyStore;
    MockSyntheticPasswordManager mSpManager;

@@ -88,6 +91,10 @@ public class BaseLockSettingsServiceTests extends AndroidTestCase {
        mStorageManager = new FakeStorageManager();
        mActivityManager = mock(IActivityManager.class);
        mDevicePolicyManager = mock(DevicePolicyManager.class);
        mDevicePolicyManagerInternal = mock(DevicePolicyManagerInternal.class);

        LocalServices.removeServiceForTest(DevicePolicyManagerInternal.class);
        LocalServices.addService(DevicePolicyManagerInternal.class, mDevicePolicyManagerInternal);

        mContext = new MockLockSettingsContext(getContext(), mUserManager, mNotificationManager,
                mDevicePolicyManager, mock(StorageManager.class), mock(TrustManager.class));
+116 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2018 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.android.server.locksettings;

import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC;
import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;

import static com.android.internal.widget.LockPatternUtils.CREDENTIAL_TYPE_PASSWORD;
import static com.android.server.testutils.TestUtils.assertExpectException;

import static org.mockito.Mockito.anyInt;
import static org.mockito.Mockito.when;
import static org.mockito.Mockito.verify;

import android.os.RemoteException;

import com.android.internal.widget.LockPatternUtils;
import com.android.internal.widget.VerifyCredentialResponse;
import com.android.server.locksettings.SyntheticPasswordManager.AuthenticationResult;

/**
 * Run the synthetic password tests with caching enabled.
 *
 * By default, those tests run without caching. Untrusted credential reset depends on caching so
 * this class included those tests.
 */
public class CachedSyntheticPasswordTests extends SyntheticPasswordTests {

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        enableSpCaching(true);
    }

    private void enableSpCaching(boolean enable) {
        when(mDevicePolicyManagerInternal
                .canUserHaveUntrustedCredentialReset(anyInt())).thenReturn(enable);
    }

    public void testSyntheticPasswordClearCredentialUntrusted() throws RemoteException {
        final String PASSWORD = "testSyntheticPasswordClearCredential-password";
        final String NEWPASSWORD = "testSyntheticPasswordClearCredential-newpassword";

        initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
        long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
        // clear password
        mService.setLockCredential(null, LockPatternUtils.CREDENTIAL_TYPE_NONE, null,
                PASSWORD_QUALITY_UNSPECIFIED, PRIMARY_USER_ID);
        assertEquals(0, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));

        // set a new password
        mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null,
                PASSWORD_QUALITY_ALPHABETIC, PRIMARY_USER_ID);
        assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
                NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID)
                    .getResponseCode());
        assertNotEquals(sid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
    }

    public void testSyntheticPasswordChangeCredentialUntrusted() throws RemoteException {
        final String PASSWORD = "testSyntheticPasswordClearCredential-password";
        final String NEWPASSWORD = "testSyntheticPasswordClearCredential-newpassword";

        initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
        long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
        // Untrusted change password
        mService.setLockCredential(NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, null,
                PASSWORD_QUALITY_ALPHABETIC, PRIMARY_USER_ID);
        assertNotEquals(0, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));
        assertNotEquals(sid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));

        // Verify the password
        assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
                NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID)
                    .getResponseCode());
    }

    public void testUntrustedCredentialChangeBlockedIfSpNotCached() throws RemoteException {
        final String PASSWORD = "testUntrustedCredentialChangeBlockedIfSpNotCached-password";
        final String NEWPASSWORD = "testUntrustedCredentialChangeBlockedIfSpNotCached-newpassword";

        // Disable caching for this test
        enableSpCaching(false);

        initializeCredentialUnderSP(PASSWORD, PRIMARY_USER_ID);
        long sid = mGateKeeperService.getSecureUserId(PRIMARY_USER_ID);
        // Untrusted change password
        assertExpectException(IllegalStateException.class, /* messageRegex= */ null,
                () -> mService.setLockCredential(
                        NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD,
                        null, PASSWORD_QUALITY_ALPHABETIC, PRIMARY_USER_ID));
        assertEquals(sid, mGateKeeperService.getSecureUserId(PRIMARY_USER_ID));

        // Verify the new password doesn't work but the old one still does
        assertEquals(VerifyCredentialResponse.RESPONSE_ERROR, mService.verifyCredential(
                NEWPASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID)
                        .getResponseCode());
        assertEquals(VerifyCredentialResponse.RESPONSE_OK, mService.verifyCredential(
                PASSWORD, LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, 0, PRIMARY_USER_ID)
                        .getResponseCode());
    }

}
Loading