From a486e2d780b721deca0083a39f5ec368fec385c0 Mon Sep 17 00:00:00 2001 From: Kenny Root Date: Mon, 24 Feb 2020 08:58:46 -0800 Subject: [PATCH 1/2] LockSettingsService: move database storage The database storage commands are part of ILockSettings, but the actual storage takes place on LockSettingsStorage. Redirect the calls in LockSettingsService to LockSettingsStorage to make the ownership a bit more clear. (cherry picked from commit 366c310e4ada13d629bc0dce14f71cad122db9e8) Bug: 149833390 Test: atest LockSettingsStorageTests LockSettingsServiceTests Change-Id: Ibc9236a171cedd3b09b1afed70895bc91e895a1c Merged-In: Ibc9236a171cedd3b09b1afed70895bc91e895a1c --- .../locksettings/LockSettingsService.java | 83 +++------------- .../locksettings/LockSettingsStorage.java | 94 +++++++++++++++++++ 2 files changed, 108 insertions(+), 69 deletions(-) diff --git a/services/core/java/com/android/server/locksettings/LockSettingsService.java b/services/core/java/com/android/server/locksettings/LockSettingsService.java index 15dfab93ed279..6faf67486ff3e 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsService.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsService.java @@ -50,7 +50,6 @@ import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyManagerInternal; import android.app.admin.DeviceStateCache; import android.app.admin.PasswordMetrics; -import android.app.backup.BackupManager; import android.app.trust.IStrongAuthTracker; import android.app.trust.TrustManager; import android.content.BroadcastReceiver; @@ -112,10 +111,8 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.messages.nano.SystemMessageProto.SystemMessage; import com.android.internal.notification.SystemNotificationChannels; -import com.android.internal.util.ArrayUtils; import com.android.internal.util.DumpUtils; import com.android.internal.util.IndentingPrintWriter; -import com.android.internal.util.Preconditions; import com.android.internal.widget.ICheckCredentialProgressCallback; import com.android.internal.widget.ILockSettings; import com.android.internal.widget.LockPatternUtils; @@ -701,7 +698,7 @@ public class LockSettingsService extends ILockSettings.Stub { // Serial number is never reusued, so we can use it as a distinguisher for user Id reuse. int serialNumber = mUserManager.getUserSerialNumber(userId); - int storedSerialNumber = getIntUnchecked(USER_SERIAL_NUMBER_KEY, -1, userId); + int storedSerialNumber = mStorage.getInt(USER_SERIAL_NUMBER_KEY, -1, userId); if (storedSerialNumber != serialNumber) { // If LockSettingsStorage does not have a copy of the serial number, it could be either // this is a user created before the serial number recording logic is introduced, or @@ -710,7 +707,7 @@ public class LockSettingsService extends ILockSettings.Stub { if (storedSerialNumber != -1) { removeUser(userId, /* unknownUser */ true); } - setIntUnchecked(USER_SERIAL_NUMBER_KEY, serialNumber, userId); + mStorage.setInt(USER_SERIAL_NUMBER_KEY, serialNumber, userId); } } @@ -1069,7 +1066,7 @@ public class LockSettingsService extends ILockSettings.Stub { private boolean getSeparateProfileChallengeEnabledInternal(int userId) { synchronized (mSeparateChallengeLock) { - return getBooleanUnchecked(SEPARATE_PROFILE_CHALLENGE_KEY, false, userId); + return mStorage.getBoolean(SEPARATE_PROFILE_CHALLENGE_KEY, false, userId); } } @@ -1122,94 +1119,49 @@ public class LockSettingsService extends ILockSettings.Stub { @Override public void setBoolean(String key, boolean value, int userId) { checkWritePermission(userId); - setStringUnchecked(key, userId, value ? "1" : "0"); + mStorage.setBoolean(key, value, userId); } @Override public void setLong(String key, long value, int userId) { checkWritePermission(userId); - setLongUnchecked(key, value, userId); - } - - private void setLongUnchecked(String key, long value, int userId) { - setStringUnchecked(key, userId, Long.toString(value)); - } - - private void setIntUnchecked(String key, int value, int userId) { - setStringUnchecked(key, userId, Integer.toString(value)); + mStorage.setLong(key, value, userId); } @Override public void setString(String key, String value, int userId) { checkWritePermission(userId); - setStringUnchecked(key, userId, value); - } - - private void setStringUnchecked(String key, int userId, String value) { - Preconditions.checkArgument(userId != USER_FRP, "cannot store lock settings for FRP user"); - - mStorage.writeKeyValue(key, value, userId); - if (ArrayUtils.contains(SETTINGS_TO_BACKUP, key)) { - BackupManager.dataChanged("com.android.providers.settings"); - } + mStorage.setString(key, value, userId); } @Override public boolean getBoolean(String key, boolean defaultValue, int userId) { checkReadPermission(key, userId); - return getBooleanUnchecked(key, defaultValue, userId); - } - - private boolean getBooleanUnchecked(String key, boolean defaultValue, int userId) { - String value = getStringUnchecked(key, null, userId); - return TextUtils.isEmpty(value) ? - defaultValue : (value.equals("1") || value.equals("true")); + if (Settings.Secure.LOCK_PATTERN_ENABLED.equals(key)) { + return getCredentialTypeInternal(userId) == CREDENTIAL_TYPE_PATTERN; + } + return mStorage.getBoolean(key, defaultValue, userId); } @Override public long getLong(String key, long defaultValue, int userId) { checkReadPermission(key, userId); - return getLongUnchecked(key, defaultValue, userId); - } - - private long getLongUnchecked(String key, long defaultValue, int userId) { - String value = getStringUnchecked(key, null, userId); - return TextUtils.isEmpty(value) ? defaultValue : Long.parseLong(value); - } - - private int getIntUnchecked(String key, int defaultValue, int userId) { - String value = getStringUnchecked(key, null, userId); - return TextUtils.isEmpty(value) ? defaultValue : Integer.parseInt(value); + return mStorage.getLong(key, defaultValue, userId); } @Override public String getString(String key, String defaultValue, int userId) { checkReadPermission(key, userId); - return getStringUnchecked(key, defaultValue, userId); - } - - private String getStringUnchecked(String key, String defaultValue, int userId) { - if (Settings.Secure.LOCK_PATTERN_ENABLED.equals(key)) { - return getCredentialTypeInternal(userId) == CREDENTIAL_TYPE_PATTERN ? "1" : "0"; - } - if (userId == USER_FRP) { - return null; - } - - if (LockPatternUtils.LEGACY_LOCK_PATTERN_ENABLED.equals(key)) { - key = Settings.Secure.LOCK_PATTERN_ENABLED; - } - - return mStorage.readKeyValue(key, defaultValue, userId); + return mStorage.getString(key, defaultValue, userId); } private void setKeyguardStoredQuality(int quality, int userId) { if (DEBUG) Slog.d(TAG, "setKeyguardStoredQuality: user=" + userId + " quality=" + quality); - setLongUnchecked(LockPatternUtils.PASSWORD_TYPE_KEY, quality, userId); + mStorage.setLong(LockPatternUtils.PASSWORD_TYPE_KEY, quality, userId); } private int getKeyguardStoredQuality(int userId) { - return (int) getLongUnchecked(LockPatternUtils.PASSWORD_TYPE_KEY, + return (int) mStorage.getLong(LockPatternUtils.PASSWORD_TYPE_KEY, DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED, userId); } @@ -2493,13 +2445,6 @@ public class LockSettingsService extends ILockSettings.Stub { SEPARATE_PROFILE_CHALLENGE_KEY }; - private static final String[] SETTINGS_TO_BACKUP = new String[] { - Secure.LOCK_SCREEN_OWNER_INFO_ENABLED, - Secure.LOCK_SCREEN_OWNER_INFO, - Secure.LOCK_PATTERN_VISIBLE, - LockPatternUtils.LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS - }; - private class GateKeeperDiedRecipient implements IBinder.DeathRecipient { @Override public void binderDied() { diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java index fec0189377c82..81d07cc11527b 100644 --- a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java +++ b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java @@ -18,8 +18,12 @@ package com.android.server.locksettings; import static android.content.Context.USER_SERVICE; +import static com.android.internal.annotations.VisibleForTesting.Visibility.PACKAGE; +import static com.android.internal.widget.LockPatternUtils.USER_FRP; + import android.annotation.Nullable; import android.app.admin.DevicePolicyManager; +import android.app.backup.BackupManager; import android.content.ContentValues; import android.content.Context; import android.content.pm.UserInfo; @@ -29,6 +33,8 @@ import android.database.sqlite.SQLiteOpenHelper; import android.os.Environment; import android.os.UserHandle; import android.os.UserManager; +import android.provider.Settings; +import android.text.TextUtils; import android.util.ArrayMap; import android.util.Slog; @@ -87,6 +93,13 @@ class LockSettingsStorage { private static final Object DEFAULT = new Object(); + private static final String[] SETTINGS_TO_BACKUP = new String[] { + Settings.Secure.LOCK_SCREEN_OWNER_INFO_ENABLED, + Settings.Secure.LOCK_SCREEN_OWNER_INFO, + Settings.Secure.LOCK_PATTERN_VISIBLE, + LockPatternUtils.LOCKSCREEN_POWER_BUTTON_INSTANTLY_LOCKS + }; + private final DatabaseHelper mOpenHelper; private final Context mContext; private final Cache mCache = new Cache(); @@ -136,10 +149,12 @@ class LockSettingsStorage { mOpenHelper.setCallback(callback); } + @VisibleForTesting(visibility = PACKAGE) public void writeKeyValue(String key, String value, int userId) { writeKeyValue(mOpenHelper.getWritableDatabase(), key, value, userId); } + @VisibleForTesting public void writeKeyValue(SQLiteDatabase db, String key, String value, int userId) { ContentValues cv = new ContentValues(); cv.put(COLUMN_KEY, key); @@ -159,6 +174,7 @@ class LockSettingsStorage { } + @VisibleForTesting public String readKeyValue(String key, String defaultValue, int userId) { int version; synchronized (mCache) { @@ -184,6 +200,28 @@ class LockSettingsStorage { return result == DEFAULT ? defaultValue : (String) result; } + @VisibleForTesting + public void removeKey(String key, int userId) { + removeKey(mOpenHelper.getWritableDatabase(), key, userId); + } + + private void removeKey(SQLiteDatabase db, String key, int userId) { + ContentValues cv = new ContentValues(); + cv.put(COLUMN_KEY, key); + cv.put(COLUMN_USERID, userId); + + db.beginTransaction(); + try { + db.delete(TABLE, COLUMN_KEY + "=? AND " + COLUMN_USERID + "=?", + new String[] {key, Integer.toString(userId)}); + db.setTransactionSuccessful(); + mCache.removeKey(key, userId); + } finally { + db.endTransaction(); + } + + } + public void prefetchUser(int userId) { int version; synchronized (mCache) { @@ -537,6 +575,55 @@ class LockSettingsStorage { } } + public void setBoolean(String key, boolean value, int userId) { + setString(key, value ? "1" : "0", userId); + } + + public void setLong(String key, long value, int userId) { + setString(key, Long.toString(value), userId); + } + + public void setInt(String key, int value, int userId) { + setString(key, Integer.toString(value), userId); + } + + public void setString(String key, String value, int userId) { + Preconditions.checkArgument(userId != USER_FRP, "cannot store lock settings for FRP user"); + + writeKeyValue(key, value, userId); + if (ArrayUtils.contains(SETTINGS_TO_BACKUP, key)) { + BackupManager.dataChanged("com.android.providers.settings"); + } + } + + public boolean getBoolean(String key, boolean defaultValue, int userId) { + String value = getString(key, null, userId); + return TextUtils.isEmpty(value) + ? defaultValue : (value.equals("1") || value.equals("true")); + } + + public long getLong(String key, long defaultValue, int userId) { + String value = getString(key, null, userId); + return TextUtils.isEmpty(value) ? defaultValue : Long.parseLong(value); + } + + public int getInt(String key, int defaultValue, int userId) { + String value = getString(key, null, userId); + return TextUtils.isEmpty(value) ? defaultValue : Integer.parseInt(value); + } + + public String getString(String key, String defaultValue, int userId) { + if (userId == USER_FRP) { + return null; + } + + if (LockPatternUtils.LEGACY_LOCK_PATTERN_ENABLED.equals(key)) { + key = Settings.Secure.LOCK_PATTERN_ENABLED; + } + + return readKeyValue(key, defaultValue, userId); + } + @VisibleForTesting void closeDatabase() { mOpenHelper.close(); @@ -764,6 +851,10 @@ class LockSettingsStorage { putIfUnchanged(CacheKey.TYPE_KEY_VALUE, key, value, userId, version); } + void removeKey(String key, int userId) { + remove(CacheKey.TYPE_KEY_VALUE, key, userId); + } + byte[] peekFile(String fileName) { return copyOf((byte[]) peek(CacheKey.TYPE_FILE, fileName, -1 /* userId */)); } @@ -788,6 +879,9 @@ class LockSettingsStorage { return contains(CacheKey.TYPE_FETCHED, "", userId); } + private synchronized void remove(int type, String key, int userId) { + mCache.remove(mCacheKey.set(type, key, userId)); + } private synchronized void put(int type, String key, Object value, int userId) { // Create a new CachKey here because it may be saved in the map if the key is absent. From ea134edcd503b3e0e9b95c4467a1a0ad7e8958b0 Mon Sep 17 00:00:00 2001 From: Kenny Root Date: Thu, 20 Feb 2020 09:36:27 -0800 Subject: [PATCH 2/2] Resume-on-Reboot: add storage for armed status In order to avoid marking a manual reboot as a RoR failure, add another indicator that the resume-on-reboot was armed before the last reboot. (cherry picked from commit 5e6a959653cdad5e2c0c2eaabe4e3a0252bf43c4) Test: atest RebootEscrowManagerTests Bug: 149833390 Change-Id: Ief1cac1952302e43a2c57314bc1449fa7bf38ba7 Merged-In: Ief1cac1952302e43a2c57314bc1449fa7bf38ba7 --- .../locksettings/RebootEscrowManager.java | 60 ++++++- .../RebootEscrowManagerTests.java | 149 +++++++++++++++++- 2 files changed, 201 insertions(+), 8 deletions(-) diff --git a/services/core/java/com/android/server/locksettings/RebootEscrowManager.java b/services/core/java/com/android/server/locksettings/RebootEscrowManager.java index c9e356ec14703..351dd6ed3d3d5 100644 --- a/services/core/java/com/android/server/locksettings/RebootEscrowManager.java +++ b/services/core/java/com/android/server/locksettings/RebootEscrowManager.java @@ -16,6 +16,8 @@ package com.android.server.locksettings; +import static android.os.UserHandle.USER_SYSTEM; + import android.annotation.Nullable; import android.annotation.UserIdInt; import android.content.Context; @@ -24,6 +26,7 @@ import android.hardware.rebootescrow.IRebootEscrow; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserManager; +import android.provider.Settings; import android.util.Slog; import com.android.internal.annotations.GuardedBy; @@ -39,6 +42,26 @@ import java.util.NoSuchElementException; class RebootEscrowManager { private static final String TAG = "RebootEscrowManager"; + /** + * Used in the database storage to indicate the boot count at which the reboot escrow was + * previously armed. + */ + @VisibleForTesting + public static final String REBOOT_ESCROW_ARMED_KEY = "reboot_escrow_armed_count"; + + /** + * Number of boots until we consider the escrow data to be stale for the purposes of metrics. + *

+ * If the delta between the current boot number and the boot number stored when the mechanism + * was armed is under this number and the escrow mechanism fails, we report it as a failure of + * the mechanism. + *

+ * If the delta over this number and escrow fails, we will not report the metric as failed + * since there most likely was some other issue if the device rebooted several times before + * getting to the escrow restore code. + */ + private static final int BOOT_COUNT_TOLERANCE = 5; + /** * Used to track when the reboot escrow is wanted. Should stay true once escrow is requested * unless clearRebootEscrow is called. This will allow all the active users to be unlocked @@ -74,6 +97,7 @@ class RebootEscrowManager { interface Callbacks { boolean isUserSecure(int userId); + void onRebootEscrowRestored(byte spVersion, byte[] syntheticPassword, int userId); } @@ -92,7 +116,8 @@ class RebootEscrowManager { return (UserManager) mContext.getSystemService(Context.USER_SERVICE); } - public @Nullable IRebootEscrow getRebootEscrow() { + @Nullable + public IRebootEscrow getRebootEscrow() { try { return IRebootEscrow.Stub.asInterface(ServiceManager.getService( "android.hardware.rebootescrow.IRebootEscrow/default")); @@ -101,6 +126,15 @@ class RebootEscrowManager { } return null; } + + public int getBootCount() { + return Settings.Global.getInt(mContext.getContentResolver(), Settings.Global.BOOT_COUNT, + 0); + } + + public void reportMetric(boolean success) { + FrameworkStatsLog.write(FrameworkStatsLog.REBOOT_ESCROW_RECOVERY_REPORTED, success); + } } RebootEscrowManager(Context context, Callbacks callbacks, LockSettingsStorage storage) { @@ -135,7 +169,7 @@ class RebootEscrowManager { for (UserInfo user : users) { mStorage.removeRebootEscrow(user.id); } - FrameworkStatsLog.write(FrameworkStatsLog.REBOOT_ESCROW_RECOVERY_REPORTED, false); + onEscrowRestoreComplete(false); return; } @@ -143,8 +177,19 @@ class RebootEscrowManager { for (UserInfo user : rebootEscrowUsers) { allUsersUnlocked &= restoreRebootEscrowForUser(user.id, escrowKey); } - FrameworkStatsLog.write(FrameworkStatsLog.REBOOT_ESCROW_RECOVERY_REPORTED, - allUsersUnlocked); + onEscrowRestoreComplete(allUsersUnlocked); + } + + private void onEscrowRestoreComplete(boolean success) { + int previousBootCount = mStorage.getInt(REBOOT_ESCROW_ARMED_KEY, 0, USER_SYSTEM); + mStorage.removeKey(REBOOT_ESCROW_ARMED_KEY, USER_SYSTEM); + + int bootCountDelta = mInjector.getBootCount() - previousBootCount; + if (bootCountDelta > BOOT_COUNT_TOLERANCE) { + return; + } + + mInjector.reportMetric(success); } private RebootEscrowKey getAndClearRebootEscrowKey() { @@ -267,6 +312,8 @@ class RebootEscrowManager { return; } + mStorage.removeKey(REBOOT_ESCROW_ARMED_KEY, USER_SYSTEM); + try { rebootEscrow.storeKey(new byte[32]); } catch (RemoteException e) { @@ -308,6 +355,11 @@ class RebootEscrowManager { } catch (RemoteException e) { Slog.e(TAG, "Failed escrow secret to RebootEscrow HAL", e); } + + if (armedRebootEscrow) { + mStorage.setInt(REBOOT_ESCROW_ARMED_KEY, mInjector.getBootCount(), USER_SYSTEM); + } + return armedRebootEscrow; } diff --git a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java index 44d88d2b44d32..1cf8525eecdd3 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java @@ -19,12 +19,16 @@ package com.android.server.locksettings; import static android.content.pm.UserInfo.FLAG_FULL; import static android.content.pm.UserInfo.FLAG_PRIMARY; import static android.content.pm.UserInfo.FLAG_PROFILE; +import static android.os.UserHandle.USER_SYSTEM; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; @@ -32,8 +36,10 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import static org.testng.Assert.assertNull; import android.content.Context; +import android.content.ContextWrapper; import android.content.pm.UserInfo; import android.hardware.rebootescrow.IRebootEscrow; import android.os.RemoteException; @@ -49,6 +55,7 @@ import com.android.internal.widget.RebootEscrowListener; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import java.io.File; import java.util.ArrayList; @@ -76,16 +83,26 @@ public class RebootEscrowManagerTests { LockSettingsStorageTestable mStorage; + private MockableRebootEscrowInjected mInjected; private RebootEscrowManager mService; + public interface MockableRebootEscrowInjected { + int getBootCount(); + + void reportMetric(boolean success); + } + static class MockInjector extends RebootEscrowManager.Injector { private final IRebootEscrow mRebootEscrow; private final UserManager mUserManager; + private final MockableRebootEscrowInjected mInjected; - MockInjector(Context context, UserManager userManager, IRebootEscrow rebootEscrow) { + MockInjector(Context context, UserManager userManager, IRebootEscrow rebootEscrow, + MockableRebootEscrowInjected injected) { super(context); mRebootEscrow = rebootEscrow; mUserManager = userManager; + mInjected = injected; } @Override @@ -97,11 +114,21 @@ public class RebootEscrowManagerTests { public IRebootEscrow getRebootEscrow() { return mRebootEscrow; } + + @Override + public int getBootCount() { + return mInjected.getBootCount(); + } + + @Override + public void reportMetric(boolean success) { + mInjected.reportMetric(success); + } } @Before public void setUp_baseServices() throws Exception { - mContext = mock(Context.class); + mContext = new ContextWrapper(InstrumentationRegistry.getContext()); mUserManager = mock(UserManager.class); mCallbacks = mock(RebootEscrowManager.Callbacks.class); mRebootEscrow = mock(IRebootEscrow.class); @@ -119,8 +146,9 @@ public class RebootEscrowManagerTests { when(mCallbacks.isUserSecure(WORK_PROFILE_USER_ID)).thenReturn(true); when(mCallbacks.isUserSecure(NONSECURE_SECONDARY_USER_ID)).thenReturn(false); when(mCallbacks.isUserSecure(SECURE_SECONDARY_USER_ID)).thenReturn(true); - mService = new RebootEscrowManager(new MockInjector(mContext, mUserManager, mRebootEscrow), - mCallbacks, mStorage); + mInjected = mock(MockableRebootEscrowInjected.class); + mService = new RebootEscrowManager(new MockInjector(mContext, mUserManager, mRebootEscrow, + mInjected), mCallbacks, mStorage); } @Test @@ -160,7 +188,11 @@ public class RebootEscrowManagerTests { verify(mockListener).onPreparedForReboot(eq(true)); verify(mRebootEscrow, never()).storeKey(any()); + assertNull( + mStorage.getString(RebootEscrowManager.REBOOT_ESCROW_ARMED_KEY, null, USER_SYSTEM)); assertTrue(mService.armRebootEscrowIfNeeded()); + assertNotNull( + mStorage.getString(RebootEscrowManager.REBOOT_ESCROW_ARMED_KEY, null, USER_SYSTEM)); verify(mRebootEscrow).storeKey(any()); assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID)); @@ -180,7 +212,15 @@ public class RebootEscrowManagerTests { FAKE_AUTH_TOKEN); verify(mRebootEscrow, never()).storeKey(any()); + assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID)); + assertTrue(mStorage.hasRebootEscrow(SECURE_SECONDARY_USER_ID)); + assertFalse(mStorage.hasRebootEscrow(NONSECURE_SECONDARY_USER_ID)); + + assertNull( + mStorage.getString(RebootEscrowManager.REBOOT_ESCROW_ARMED_KEY, null, USER_SYSTEM)); assertTrue(mService.armRebootEscrowIfNeeded()); + assertNotNull( + mStorage.getString(RebootEscrowManager.REBOOT_ESCROW_ARMED_KEY, null, USER_SYSTEM)); verify(mRebootEscrow, times(1)).storeKey(any()); assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID)); @@ -200,4 +240,105 @@ public class RebootEscrowManagerTests { assertFalse(mService.armRebootEscrowIfNeeded()); verifyNoMoreInteractions(mRebootEscrow); } + + @Test + public void loadRebootEscrowDataIfAvailable_NothingAvailable_Success() throws Exception { + mService.loadRebootEscrowDataIfAvailable(); + } + + @Test + public void loadRebootEscrowDataIfAvailable_Success() throws Exception { + when(mInjected.getBootCount()).thenReturn(0); + + RebootEscrowListener mockListener = mock(RebootEscrowListener.class); + mService.setRebootEscrowListener(mockListener); + mService.prepareRebootEscrow(); + + clearInvocations(mRebootEscrow); + mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN); + verify(mockListener).onPreparedForReboot(eq(true)); + + verify(mRebootEscrow, never()).storeKey(any()); + + ArgumentCaptor keyByteCaptor = ArgumentCaptor.forClass(byte[].class); + assertTrue(mService.armRebootEscrowIfNeeded()); + verify(mRebootEscrow).storeKey(keyByteCaptor.capture()); + + assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID)); + assertFalse(mStorage.hasRebootEscrow(NONSECURE_SECONDARY_USER_ID)); + + // pretend reboot happens here + + when(mInjected.getBootCount()).thenReturn(1); + ArgumentCaptor metricsSuccessCaptor = ArgumentCaptor.forClass(Boolean.class); + doNothing().when(mInjected).reportMetric(metricsSuccessCaptor.capture()); + when(mRebootEscrow.retrieveKey()).thenAnswer(invocation -> keyByteCaptor.getValue()); + + mService.loadRebootEscrowDataIfAvailable(); + verify(mRebootEscrow).retrieveKey(); + assertTrue(metricsSuccessCaptor.getValue()); + } + + @Test + public void loadRebootEscrowDataIfAvailable_TooManyBootsInBetween_NoMetrics() throws Exception { + when(mInjected.getBootCount()).thenReturn(0); + + RebootEscrowListener mockListener = mock(RebootEscrowListener.class); + mService.setRebootEscrowListener(mockListener); + mService.prepareRebootEscrow(); + + clearInvocations(mRebootEscrow); + mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN); + verify(mockListener).onPreparedForReboot(eq(true)); + + verify(mRebootEscrow, never()).storeKey(any()); + + ArgumentCaptor keyByteCaptor = ArgumentCaptor.forClass(byte[].class); + assertTrue(mService.armRebootEscrowIfNeeded()); + verify(mRebootEscrow).storeKey(keyByteCaptor.capture()); + + assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID)); + assertFalse(mStorage.hasRebootEscrow(NONSECURE_SECONDARY_USER_ID)); + + // pretend reboot happens here + + when(mInjected.getBootCount()).thenReturn(10); + when(mRebootEscrow.retrieveKey()).thenAnswer(invocation -> keyByteCaptor.getValue()); + + mService.loadRebootEscrowDataIfAvailable(); + verify(mRebootEscrow).retrieveKey(); + verify(mInjected, never()).reportMetric(anyBoolean()); + } + + + @Test + public void loadRebootEscrowDataIfAvailable_RestoreUnsuccessful_Failure() throws Exception { + when(mInjected.getBootCount()).thenReturn(0); + + RebootEscrowListener mockListener = mock(RebootEscrowListener.class); + mService.setRebootEscrowListener(mockListener); + mService.prepareRebootEscrow(); + + clearInvocations(mRebootEscrow); + mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN); + verify(mockListener).onPreparedForReboot(eq(true)); + + verify(mRebootEscrow, never()).storeKey(any()); + + assertTrue(mService.armRebootEscrowIfNeeded()); + verify(mRebootEscrow).storeKey(any()); + + assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID)); + assertFalse(mStorage.hasRebootEscrow(NONSECURE_SECONDARY_USER_ID)); + + // pretend reboot happens here. + + when(mInjected.getBootCount()).thenReturn(1); + ArgumentCaptor metricsSuccessCaptor = ArgumentCaptor.forClass(Boolean.class); + doNothing().when(mInjected).reportMetric(metricsSuccessCaptor.capture()); + when(mRebootEscrow.retrieveKey()).thenAnswer(invocation -> new byte[32]); + mService.loadRebootEscrowDataIfAvailable(); + verify(mRebootEscrow).retrieveKey(); + assertFalse(metricsSuccessCaptor.getValue()); + } }