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

Commit c1c6d82f authored by Kenny Root's avatar Kenny Root Committed by Android (Google) Code Review
Browse files

Merge "Resume-on-Reboot: add storage for armed status"

parents 42c9aadc 5e6a9596
Loading
Loading
Loading
Loading
+56 −4
Original line number Diff line number Diff line
@@ -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.
     * <p>
     * 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.
     * <p>
     * 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;
    }

+145 −4
Original line number Diff line number Diff line
@@ -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<byte[]> 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<Boolean> 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<byte[]> 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<Boolean> 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());
    }
}