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; + } +}