From bf44988f67cde401089f957cdf510d4c9e6f82df Mon Sep 17 00:00:00 2001 From: Rhed Jao Date: Wed, 17 May 2023 10:44:37 +0000 Subject: [PATCH] Create a verify flag to support verification for entering repair mode Add a flag provided to verifyCredential API to support verification for entering repair mode. If the flag is set, the API writes the password data to the repair mode file after the user credential is verified successfully. The file is deleted once the device boots into normal mode. Bug: 277561275 Test: atest com.android.server.locksettings Test: adb shell dumpsys lock_settings Test: am start -a android.app.action.PREPARE_REPAIR_MODE_DEVICE_CREDENTIAL The file is created in /metadata/repair-mode after the credential is verified. Change-Id: I8e22c1c9045c1c67de72fae6e9ed581bb4f04b96 Merged-In: I8e22c1c9045c1c67de72fae6e9ed581bb4f04b96 --- core/java/android/app/KeyguardManager.java | 11 ++ .../internal/widget/LockPatternUtils.java | 44 +++++- .../locksettings/LockSettingsService.java | 22 ++- .../locksettings/LockSettingsStorage.java | 50 +++++++ .../SyntheticPasswordManager.java | 51 +++++++ .../BaseLockSettingsServiceTests.java | 2 + .../LockSettingsStorageTestable.java | 5 + .../LockSettingsStorageTests.java | 25 ++++ .../LockscreenRepairModeTest.java | 129 ++++++++++++++++++ 9 files changed, 335 insertions(+), 4 deletions(-) create mode 100644 services/tests/servicestests/src/com/android/server/locksettings/LockscreenRepairModeTest.java diff --git a/core/java/android/app/KeyguardManager.java b/core/java/android/app/KeyguardManager.java index 09450f59ed3da..b71cb4a388ab8 100644 --- a/core/java/android/app/KeyguardManager.java +++ b/core/java/android/app/KeyguardManager.java @@ -116,6 +116,17 @@ public class KeyguardManager { public static final String ACTION_CONFIRM_REMOTE_DEVICE_CREDENTIAL = "android.app.action.CONFIRM_REMOTE_DEVICE_CREDENTIAL"; + /** + * Intent used to prompt user for device credential for entering repair + * mode. If the credential is verified successfully, then the information + * needed to verify the credential again will be written to a location that + * is available to repair mode. This makes it possible for repair mode to + * require that the same credential be provided to exit repair mode. + * @hide + */ + public static final String ACTION_PREPARE_REPAIR_MODE_DEVICE_CREDENTIAL = + "android.app.action.PREPARE_REPAIR_MODE_DEVICE_CREDENTIAL"; + /** * A CharSequence dialog title to show to the user when used with a * {@link #ACTION_CONFIRM_DEVICE_CREDENTIAL}. diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 92cfa670045e7..66b69c53a6847 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -47,6 +47,7 @@ import android.os.Message; import android.os.RemoteException; import android.os.ServiceManager; import android.os.SystemClock; +import android.os.SystemProperties; import android.os.UserHandle; import android.os.UserManager; import android.os.storage.StorageManager; @@ -160,9 +161,17 @@ public class LockPatternUtils { */ public static final int VERIFY_FLAG_REQUEST_GK_PW_HANDLE = 1 << 0; + /** + * Flag provided to {@link #verifyCredential(LockscreenCredential, int, int)} . If set, the + * method writes the password data to the repair mode file after the credential is verified + * successfully. + */ + public static final int VERIFY_FLAG_WRITE_REPAIR_MODE_PW = 1 << 1; + @Retention(RetentionPolicy.SOURCE) @IntDef(flag = true, value = { - VERIFY_FLAG_REQUEST_GK_PW_HANDLE + VERIFY_FLAG_REQUEST_GK_PW_HANDLE, + VERIFY_FLAG_WRITE_REPAIR_MODE_PW }) public @interface VerifyFlag {} @@ -201,6 +210,8 @@ public class LockPatternUtils { public static final String CURRENT_LSKF_BASED_PROTECTOR_ID_KEY = "sp-handle"; public static final String PASSWORD_HISTORY_DELIMITER = ","; + private static final String GSI_RUNNING_PROP = "ro.gsid.image_running"; + /** * drives the pin auto confirmation feature availability in code logic. */ @@ -1846,6 +1857,37 @@ public class LockPatternUtils { com.android.internal.R.bool.config_enableCredentialFactoryResetProtection); } + /** + * Return {@code true} if repair mode is supported by the device. + */ + public static boolean isRepairModeSupported(Context context) { + return context.getResources().getBoolean( + com.android.internal.R.bool.config_repairModeSupported); + } + + /** + * Return {@code true} if repair mode is active on the device. + */ + public static boolean isRepairModeActive(Context context) { + return Settings.Global.getInt(context.getContentResolver(), + Settings.Global.REPAIR_MODE_ACTIVE, /* def= */ 0) > 0; + } + + /** + * Return {@code true} if repair mode is supported by the device and the user has been granted + * admin privileges. + */ + public static boolean canUserEnterRepairMode(Context context, UserInfo info) { + return info != null && info.isAdmin() && isRepairModeSupported(context); + } + + /** + * Return {@code true} if GSI is running on the device. + */ + public static boolean isGsiRunning() { + return SystemProperties.getInt(GSI_RUNNING_PROP, 0) > 0; + } + /** * Attempt to rederive the unified work challenge for the specified profile user and unlock the * user. If successful, this would allow the user to leave quiet mode automatically without diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index 2e62ef4f85666..38f153b222a35 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -41,6 +41,7 @@ import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STR import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE; import static com.android.internal.widget.LockPatternUtils.USER_FRP; import static com.android.internal.widget.LockPatternUtils.VERIFY_FLAG_REQUEST_GK_PW_HANDLE; +import static com.android.internal.widget.LockPatternUtils.VERIFY_FLAG_WRITE_REPAIR_MODE_PW; import static com.android.internal.widget.LockPatternUtils.frpCredentialEnabled; import static com.android.internal.widget.LockPatternUtils.userOwnsFrpCredential; import static com.android.server.locksettings.SyntheticPasswordManager.TOKEN_TYPE_STRONG; @@ -278,8 +279,6 @@ public class LockSettingsService extends ILockSettings.Stub { protected IGateKeeperService mGateKeeperService; protected IAuthSecret mAuthSecretService; - private static final String GSI_RUNNING_PROP = "ro.gsid.image_running"; - /** * The UIDs that are used for system credential storage in keystore. */ @@ -311,6 +310,7 @@ public class LockSettingsService extends ILockSettings.Stub { if (phase == PHASE_ACTIVITY_MANAGER_READY) { mLockSettingsService.migrateOldDataAfterSystemReady(); mLockSettingsService.loadEscrowData(); + mLockSettingsService.deleteRepairModePersistentDataIfNeeded(); } } @@ -541,7 +541,7 @@ public class LockSettingsService extends ILockSettings.Stub { } public boolean isGsiRunning() { - return SystemProperties.getInt(GSI_RUNNING_PROP, 0) > 0; + return LockPatternUtils.isGsiRunning(); } public FingerprintManager getFingerprintManager() { @@ -949,6 +949,16 @@ public class LockSettingsService extends ILockSettings.Stub { return success; } + @VisibleForTesting + void deleteRepairModePersistentDataIfNeeded() { + if (!LockPatternUtils.isRepairModeSupported(mContext) + || LockPatternUtils.isRepairModeActive(mContext) + || mInjector.isGsiRunning()) { + return; + } + mStorage.deleteRepairModePersistentData(); + } + // This is called when Weaver is guaranteed to be available (if the device supports Weaver). // It does any synthetic password related work that was delayed from earlier in the boot. private void onThirdPartyAppsStarted() { @@ -2202,6 +2212,12 @@ public class LockSettingsService extends ILockSettings.Stub { response = authResult.gkResponse; if (response.getResponseCode() == VerifyCredentialResponse.RESPONSE_OK) { + if ((flags & VERIFY_FLAG_WRITE_REPAIR_MODE_PW) != 0) { + if (!mSpManager.writeRepairModeCredentialLocked(protectorId, userId)) { + Slog.e(TAG, "Failed to write repair mode credential"); + return VerifyCredentialResponse.ERROR; + } + } // credential has matched mBiometricDeferredQueue.addPendingLockoutResetForUser(userId, authResult.syntheticPassword.deriveGkPassword()); diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java index 2fa637e030f19..1ace6e5452ad4 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java @@ -90,6 +90,9 @@ class LockSettingsStorage { private static final String SYNTHETIC_PASSWORD_DIRECTORY = "spblob/"; + private static final String REPAIR_MODE_DIRECTORY = "repair-mode/"; + private static final String REPAIR_MODE_PERSISTENT_FILE = "pst"; + private static final Object DEFAULT = new Object(); private static final String[] SETTINGS_TO_BACKUP = new String[] { @@ -390,6 +393,29 @@ class LockSettingsStorage { } } + @VisibleForTesting + File getRepairModePersistentDataFile() { + final File directory = new File(Environment.getMetadataDirectory(), REPAIR_MODE_DIRECTORY); + return new File(directory, REPAIR_MODE_PERSISTENT_FILE); + } + + public PersistentData readRepairModePersistentData() { + final byte[] data = readFile(getRepairModePersistentDataFile()); + if (data == null) { + return PersistentData.NONE; + } + return PersistentData.fromBytes(data); + } + + public void writeRepairModePersistentData(int persistentType, int userId, byte[] payload) { + writeFile(getRepairModePersistentDataFile(), + PersistentData.toBytes(persistentType, userId, /* qualityForUi= */0, payload)); + } + + public void deleteRepairModePersistentData() { + deleteFile(getRepairModePersistentDataFile()); + } + /** * Writes the synthetic password state file for the given user ID, protector ID, and state name. * If the file already exists, then it is atomically replaced. @@ -583,6 +609,17 @@ class LockSettingsStorage { } } + /** + * Provides a concrete data structure to represent the minimal information from + * a user's LSKF-based SP protector that is needed to verify the user's LSKF, + * in combination with the corresponding Gatekeeper enrollment or Weaver slot. + * It can be stored in {@link com.android.server.PersistentDataBlockService} for + * FRP to live across factory resets not initiated via the Settings UI. + * Written to {@link #REPAIR_MODE_PERSISTENT_FILE} to support verification for + * exiting repair mode, since the device runs with an empty data partition in + * repair mode and the same credential be provided to exit repair mode is + * required. + */ public static class PersistentData { static final byte VERSION_1 = 1; static final int VERSION_1_HEADER_SIZE = 1 + 1 + 4 + 4; @@ -685,6 +722,19 @@ class LockSettingsStorage { } pw.decreaseIndent(); } + // Dump repair mode file states + final File repairModeFile = getRepairModePersistentDataFile(); + if (repairModeFile.exists()) { + pw.println(TextUtils.formatSimple("Repair Mode [%s]:", repairModeFile.getParent())); + pw.increaseIndent(); + pw.println(TextUtils.formatSimple("%6d %s %s", repairModeFile.length(), + LockSettingsService.timestampToString(repairModeFile.lastModified()), + repairModeFile.getName())); + final PersistentData data = readRepairModePersistentData(); + pw.println(TextUtils.formatSimple("type: %d, user id: %d, payload size: %d", + data.type, data.userId, data.payload != null ? data.payload.length : 0)); + pw.decreaseIndent(); + } } static class DatabaseHelper extends SQLiteOpenHelper { diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java index 66f862ac92057..d74d304be730c 100644 --- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java +++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java @@ -1114,6 +1114,57 @@ class SyntheticPasswordManager { } } + /** + * Writes the user's synthetic password data to the repair mode file. + * + * @param protectorId current LSKF based protectorId + * @param userId user id of the user + */ + public boolean writeRepairModeCredentialLocked(long protectorId, int userId) { + if (!shouldWriteRepairModeCredential(userId)) { + return false; + } + final byte[] data = loadState(PASSWORD_DATA_NAME, protectorId, userId); + if (data == null) { + Slogf.w(TAG, "Password data not found for user %d", userId); + return false; + } + final PasswordData pwd = PasswordData.fromBytes(data); + if (isNoneCredential(pwd)) { + Slogf.w(TAG, "User %d has NONE credential", userId); + return false; + } + Slogf.d(TAG, "Writing repair mode credential tied to user %d", userId); + final int weaverSlot = loadWeaverSlot(protectorId, userId); + if (weaverSlot != INVALID_WEAVER_SLOT) { + // write weaver password + mStorage.writeRepairModePersistentData( + PersistentData.TYPE_SP_WEAVER, weaverSlot, pwd.toBytes()); + } else { + // write gatekeeper password + mStorage.writeRepairModePersistentData( + PersistentData.TYPE_SP_GATEKEEPER, userId, pwd.toBytes()); + } + return true; + } + + private boolean shouldWriteRepairModeCredential(int userId) { + final UserInfo userInfo = mUserManager.getUserInfo(userId); + if (!LockPatternUtils.canUserEnterRepairMode(mContext, userInfo)) { + Slogf.w(TAG, "User %d can't enter repair mode", userId); + return false; + } + if (LockPatternUtils.isRepairModeActive(mContext)) { + Slog.w(TAG, "Can't write repair mode credential while repair mode is already active"); + return false; + } + if (LockPatternUtils.isGsiRunning()) { + Slog.w(TAG, "Can't write repair mode credential while GSI is running"); + return false; + } + return true; + } + private ArrayMap> tokenMap = new ArrayMap<>(); /** diff --git a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java index e960e995f6ce6..fe2ac176949d9 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/BaseLockSettingsServiceTests.java @@ -254,6 +254,8 @@ public abstract class BaseLockSettingsServiceTests { .thenReturn(true); when(res.getBoolean(eq(com.android.internal.R.bool.config_strongAuthRequiredOnBoot))) .thenReturn(true); + when(res.getBoolean(eq(com.android.internal.R.bool.config_repairModeSupported))) + .thenReturn(true); return res; } diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTestable.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTestable.java index 36dc6c5f9f956..a029db9223728 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTestable.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTestable.java @@ -83,6 +83,11 @@ public class LockSettingsStorageTestable extends LockSettingsStorage { return remapToStorageDir(super.getSyntheticPasswordDirectoryForUser(userId)); } + @Override + File getRepairModePersistentDataFile() { + return remapToStorageDir(super.getRepairModePersistentDataFile()); + } + @Override PersistentDataBlockManagerInternal getPersistentDataBlockManager() { return mPersistentDataBlockManager; diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java index 10ed882f343ff..23f14f8468bbc 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTests.java @@ -457,6 +457,31 @@ public class LockSettingsStorageTests { assertEquals(2, PersistentData.TYPE_SP_WEAVER); } + @Test + public void testRepairMode_emptyPersistentData() { + assertSame(PersistentData.NONE, mStorage.readPersistentDataBlock()); + } + + @Test + public void testRepairMode_writeGatekeeperPersistentData() { + mStorage.writeRepairModePersistentData( + PersistentData.TYPE_SP_GATEKEEPER, SOME_USER_ID, PAYLOAD); + + final PersistentData data = mStorage.readRepairModePersistentData(); + assertEquals(PersistentData.TYPE_SP_GATEKEEPER, data.type); + assertArrayEquals(PAYLOAD, data.payload); + } + + @Test + public void testRepairMode_writeWeaverPersistentData() { + mStorage.writeRepairModePersistentData( + PersistentData.TYPE_SP_WEAVER, SOME_USER_ID, PAYLOAD); + + final PersistentData data = mStorage.readRepairModePersistentData(); + assertEquals(PersistentData.TYPE_SP_WEAVER, data.type); + assertArrayEquals(PAYLOAD, data.payload); + } + private static void assertArrayEquals(byte[] expected, byte[] actual) { if (!Arrays.equals(expected, actual)) { fail("expected:<" + Arrays.toString(expected) + diff --git a/services/tests/servicestests/src/com/android/server/locksettings/LockscreenRepairModeTest.java b/services/tests/servicestests/src/com/android/server/locksettings/LockscreenRepairModeTest.java new file mode 100644 index 0000000000000..dc47b87cad2d1 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/locksettings/LockscreenRepairModeTest.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2023 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 com.android.internal.widget.LockPatternUtils.VERIFY_FLAG_WRITE_REPAIR_MODE_PW; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertSame; + +import android.app.PropertyInvalidatedCache; +import android.platform.test.annotations.Presubmit; +import android.provider.Settings; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.internal.widget.LockPatternUtils; +import com.android.internal.widget.VerifyCredentialResponse; +import com.android.server.locksettings.LockSettingsStorage.PersistentData; +import com.android.server.locksettings.SyntheticPasswordManager.PasswordData; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@Presubmit +@RunWith(AndroidJUnit4.class) +public class LockscreenRepairModeTest extends BaseLockSettingsServiceTests { + + @Before + public void setUp() throws Exception { + PropertyInvalidatedCache.disableForTestMode(); + mService.initializeSyntheticPassword(PRIMARY_USER_ID); + } + + @Test + public void verifyPin_writeRepairModePW() { + mService.setLockCredential(newPin("1234"), nonePassword(), PRIMARY_USER_ID); + assertSame(PersistentData.NONE, mStorage.readRepairModePersistentData()); + + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential( + newPin("1234"), PRIMARY_USER_ID, VERIFY_FLAG_WRITE_REPAIR_MODE_PW) + .getResponseCode()); + assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PIN, + getCredentialType(mStorage.readRepairModePersistentData())); + } + + @Test + public void verifyPattern_writeRepairModePW() { + mService.setLockCredential(newPattern("4321"), nonePassword(), PRIMARY_USER_ID); + assertSame(PersistentData.NONE, mStorage.readRepairModePersistentData()); + + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential( + newPattern("4321"), PRIMARY_USER_ID, VERIFY_FLAG_WRITE_REPAIR_MODE_PW) + .getResponseCode()); + assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PATTERN, + getCredentialType(mStorage.readRepairModePersistentData())); + } + + @Test + public void verifyPassword_writeRepairModePW() { + mService.setLockCredential(newPassword("4321"), nonePassword(), PRIMARY_USER_ID); + assertSame(PersistentData.NONE, mStorage.readRepairModePersistentData()); + + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential( + newPassword("4321"), PRIMARY_USER_ID, VERIFY_FLAG_WRITE_REPAIR_MODE_PW) + .getResponseCode()); + assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, + getCredentialType(mStorage.readRepairModePersistentData())); + } + + @Test + public void verifyCredential_writeRepairModePW_repairModeActive() { + mService.setLockCredential(newPin("1234"), nonePassword(), PRIMARY_USER_ID); + assertSame(PersistentData.NONE, mStorage.readRepairModePersistentData()); + + setRepairModeActive(true); + assertEquals(VerifyCredentialResponse.RESPONSE_ERROR, + mService.verifyCredential( + newPin("1234"), PRIMARY_USER_ID, VERIFY_FLAG_WRITE_REPAIR_MODE_PW) + .getResponseCode()); + assertSame(PersistentData.NONE, mStorage.readRepairModePersistentData()); + } + + @Test + public void deleteRepairModePersistentData() { + mService.setLockCredential(newPin("1234"), nonePassword(), PRIMARY_USER_ID); + assertSame(PersistentData.NONE, mStorage.readRepairModePersistentData()); + assertEquals(VerifyCredentialResponse.RESPONSE_OK, + mService.verifyCredential( + newPin("1234"), PRIMARY_USER_ID, VERIFY_FLAG_WRITE_REPAIR_MODE_PW) + .getResponseCode()); + assertEquals(LockPatternUtils.CREDENTIAL_TYPE_PIN, + getCredentialType(mStorage.readRepairModePersistentData())); + + mService.deleteRepairModePersistentDataIfNeeded(); + assertSame(PersistentData.NONE, mStorage.readRepairModePersistentData()); + } + + private void setRepairModeActive(boolean active) { + Settings.Global.putInt(mContext.getContentResolver(), + Settings.Global.REPAIR_MODE_ACTIVE, active ? 1 : 0); + } + + private static int getCredentialType(PersistentData persistentData) { + if (persistentData == null || persistentData.payload == null) { + return LockPatternUtils.CREDENTIAL_TYPE_NONE; + } + return PasswordData.fromBytes(persistentData.payload).credentialType; + } +}