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

Commit 58c9526e authored by Tianjie's avatar Tianjie
Browse files

Call server based implementation to persist reboot escrow key

Adds the implementation for RebootEscrowProviderServerBased. It will
bind to the resume on reboot service on device; and ask the server
to encrypt/decrypt the reboot escrow key.

Bug: 172780686
Test: atest FrameworksServicesTests:RebootEscrowDataTest \
            FrameworksServicesTests:LockSettingsServiceTests \
            FrameworksServicesTests:RebootEscrowManagerTests \
            FrameworksServicesTests:RebootEscrowProviderServerBasedImplTests;

Change-Id: I6f301314ba43eb106e67d849bfe14e80c607403f
parent 91d8ff41
Loading
Loading
Loading
Loading
+23 −0
Original line number Diff line number Diff line
@@ -88,6 +88,7 @@ class LockSettingsStorage {
    private static final String CHILD_PROFILE_LOCK_FILE = "gatekeeper.profile.key";

    private static final String REBOOT_ESCROW_FILE = "reboot.escrow.key";
    private static final String REBOOT_ESCROW_SERVER_BLOB = "reboot.escrow.server.blob.key";

    private static final String SYNTHETIC_PASSWORD_DIRECTORY = "spblob/";

@@ -317,6 +318,22 @@ class LockSettingsStorage {
        deleteFile(getRebootEscrowFile(userId));
    }

    public void writeRebootEscrowServerBlob(byte[] serverBlob) {
        writeFile(getRebootEscrowServerBlob(), serverBlob);
    }

    public byte[] readRebootEscrowServerBlob() {
        return readFile(getRebootEscrowServerBlob());
    }

    public boolean hasRebootEscrowServerBlob() {
        return hasFile(getRebootEscrowServerBlob());
    }

    public void removeRebootEscrowServerBlob() {
        deleteFile(getRebootEscrowServerBlob());
    }

    public boolean hasPassword(int userId) {
        return hasFile(getLockPasswordFilename(userId));
    }
@@ -445,6 +462,12 @@ class LockSettingsStorage {
        return getLockCredentialFilePathForUser(userId, REBOOT_ESCROW_FILE);
    }

    @VisibleForTesting
    String getRebootEscrowServerBlob() {
        // There is a single copy of server blob for all users.
        return getLockCredentialFilePathForUser(UserHandle.USER_SYSTEM, REBOOT_ESCROW_SERVER_BLOB);
    }

    private String getLockCredentialFilePathForUser(int userId, String basename) {
        String dataSystemDirectory = Environment.getDataDirectory().getAbsolutePath() +
                        SYSTEM_DIRECTORY;
+18 −10
Original line number Diff line number Diff line
@@ -124,26 +124,28 @@ class RebootEscrowManager {
    static class Injector {
        protected Context mContext;
        private final RebootEscrowKeyStoreManager mKeyStoreManager;
        private final RebootEscrowProviderInterface mRebootEscrowProvider;
        private final LockSettingsStorage mStorage;
        private RebootEscrowProviderInterface mRebootEscrowProvider;

        Injector(Context context) {
        Injector(Context context, LockSettingsStorage storage) {
            mContext = context;
            mStorage = storage;
            mKeyStoreManager = new RebootEscrowKeyStoreManager();
        }

            RebootEscrowProviderInterface rebootEscrowProvider = null;
            // TODO(xunchang) add implementation for server based ror.
        private RebootEscrowProviderInterface createRebootEscrowProvider() {
            RebootEscrowProviderInterface rebootEscrowProvider;
            if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_OTA,
                    "server_based_ror_enabled", false)) {
                Slog.e(TAG, "Server based ror isn't implemented yet.");
                rebootEscrowProvider = new RebootEscrowProviderServerBasedImpl(mContext, mStorage);
            } else {
                rebootEscrowProvider = new RebootEscrowProviderHalImpl();
            }

            if (rebootEscrowProvider != null && rebootEscrowProvider.hasRebootEscrowSupport()) {
                mRebootEscrowProvider = rebootEscrowProvider;
            } else {
                mRebootEscrowProvider = null;
            if (rebootEscrowProvider.hasRebootEscrowSupport()) {
                return rebootEscrowProvider;
            }
            return null;
        }

        public Context getContext() {
@@ -159,6 +161,12 @@ class RebootEscrowManager {
        }

        public RebootEscrowProviderInterface getRebootEscrowProvider() {
            // Initialize for the provider lazily. Because the device_config and service
            // implementation apps may change when system server is running.
            if (mRebootEscrowProvider == null) {
                mRebootEscrowProvider = createRebootEscrowProvider();
            }

            return mRebootEscrowProvider;
        }

@@ -177,7 +185,7 @@ class RebootEscrowManager {
    }

    RebootEscrowManager(Context context, Callbacks callbacks, LockSettingsStorage storage) {
        this(new Injector(context), callbacks, storage);
        this(new Injector(context, storage), callbacks, storage);
    }

    @VisibleForTesting
+189 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2021 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 android.annotation.Nullable;
import android.content.Context;
import android.os.RemoteException;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.locksettings.ResumeOnRebootServiceProvider.ResumeOnRebootServiceConnection;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

import javax.crypto.SecretKey;

/**
 * An implementation of the {@link RebootEscrowProviderInterface} by communicating with server to
 * encrypt & decrypt the blob.
 */
class RebootEscrowProviderServerBasedImpl implements RebootEscrowProviderInterface {
    private static final String TAG = "RebootEscrowProvider";

    // Timeout for service binding
    private static final long SERVICE_TIME_OUT_IN_SECONDS = 10;

    /**
     * Use the default lifetime of 10 minutes. The lifetime covers the following activities:
     * Server wrap secret -> device reboot -> server unwrap blob.
     */
    private static final long SERVER_BLOB_LIFETIME_IN_MILLIS = 600_1000;

    private final LockSettingsStorage mStorage;

    private final Injector mInjector;

    static class Injector {
        private ResumeOnRebootServiceConnection mServiceConnection = null;

        Injector(Context context) {
            mServiceConnection = new ResumeOnRebootServiceProvider(context).getServiceConnection();
            if (mServiceConnection == null) {
                Slog.e(TAG, "Failed to resolve resume on reboot server service.");
            }
        }

        Injector(ResumeOnRebootServiceConnection serviceConnection) {
            mServiceConnection = serviceConnection;
        }

        @Nullable
        private ResumeOnRebootServiceConnection getServiceConnection() {
            return mServiceConnection;
        }
    }

    RebootEscrowProviderServerBasedImpl(Context context, LockSettingsStorage storage) {
        this(storage, new Injector(context));
    }

    @VisibleForTesting
    RebootEscrowProviderServerBasedImpl(LockSettingsStorage storage, Injector injector) {
        mStorage = storage;
        mInjector = injector;
    }

    @Override
    public boolean hasRebootEscrowSupport() {
        return mInjector.getServiceConnection() != null;
    }

    private byte[] unwrapServerBlob(byte[] serverBlob, SecretKey decryptionKey) throws
            TimeoutException, RemoteException, IOException {
        ResumeOnRebootServiceConnection serviceConnection = mInjector.getServiceConnection();
        if (serviceConnection == null) {
            Slog.w(TAG, "Had reboot escrow data for users, but resume on reboot server"
                    + " service is unavailable");
            return null;
        }

        // Decrypt with k_k from the key store first.
        byte[] decryptedBlob = AesEncryptionUtil.decrypt(decryptionKey, serverBlob);
        if (decryptedBlob == null) {
            Slog.w(TAG, "Decrypted server blob should not be null");
            return null;
        }

        // Ask the server connection service to decrypt the inner layer, to get the reboot
        // escrow key (k_s).
        serviceConnection.bindToService(SERVICE_TIME_OUT_IN_SECONDS);
        byte[] escrowKeyBytes = serviceConnection.unwrap(decryptedBlob,
                SERVICE_TIME_OUT_IN_SECONDS);
        serviceConnection.unbindService();

        return escrowKeyBytes;
    }

    @Override
    public RebootEscrowKey getAndClearRebootEscrowKey(SecretKey decryptionKey) {
        byte[] serverBlob = mStorage.readRebootEscrowServerBlob();
        // Delete the server blob in storage.
        mStorage.removeRebootEscrowServerBlob();
        if (serverBlob == null) {
            Slog.w(TAG, "Failed to read reboot escrow server blob from storage");
            return null;
        }

        try {
            byte[] escrowKeyBytes = unwrapServerBlob(serverBlob, decryptionKey);
            if (escrowKeyBytes == null) {
                Slog.w(TAG, "Decrypted reboot escrow key bytes should not be null");
                return null;
            } else if (escrowKeyBytes.length != 32) {
                Slog.e(TAG, "Decrypted reboot escrow key has incorrect size "
                        + escrowKeyBytes.length);
                return null;
            }

            return RebootEscrowKey.fromKeyBytes(escrowKeyBytes);
        } catch (TimeoutException | RemoteException | IOException e) {
            Slog.w(TAG, "Failed to decrypt the server blob ", e);
            return null;
        }
    }

    @Override
    public void clearRebootEscrowKey() {
        mStorage.removeRebootEscrowServerBlob();
    }

    private byte[] wrapEscrowKey(byte[] escrowKeyBytes, SecretKey encryptionKey) throws
            TimeoutException, RemoteException, IOException {
        ResumeOnRebootServiceConnection serviceConnection = mInjector.getServiceConnection();
        if (serviceConnection == null) {
            Slog.w(TAG, "Failed to encrypt the reboot escrow key: resume on reboot server"
                    + " service is unavailable");
            return null;
        }

        serviceConnection.bindToService(SERVICE_TIME_OUT_IN_SECONDS);
        // Ask the server connection service to encrypt the reboot escrow key.
        byte[] serverEncryptedBlob = serviceConnection.wrapBlob(escrowKeyBytes,
                SERVER_BLOB_LIFETIME_IN_MILLIS, SERVICE_TIME_OUT_IN_SECONDS);
        serviceConnection.unbindService();

        if (serverEncryptedBlob == null) {
            Slog.w(TAG, "Server encrypted reboot escrow key cannot be null");
            return null;
        }

        // Additionally wrap the server blob with a local key.
        return AesEncryptionUtil.encrypt(encryptionKey, serverEncryptedBlob);
    }

    @Override
    public boolean storeRebootEscrowKey(RebootEscrowKey escrowKey, SecretKey encryptionKey) {
        mStorage.removeRebootEscrowServerBlob();
        try {
            byte[] wrappedBlob = wrapEscrowKey(escrowKey.getKeyBytes(), encryptionKey);
            if (wrappedBlob == null) {
                Slog.w(TAG, "Failed to encrypt the reboot escrow key");
                return false;
            }
            mStorage.writeRebootEscrowServerBlob(wrappedBlob);

            Slog.i(TAG, "Reboot escrow key encrypted and stored.");
            return true;
        } catch (TimeoutException | RemoteException | IOException e) {
            Slog.w(TAG, "Failed to encrypt the reboot escrow key ", e);
        }

        return false;
    }
}
+5 −0
Original line number Diff line number Diff line
@@ -81,6 +81,11 @@ public class LockSettingsStorageTestable extends LockSettingsStorage {
                super.getChildProfileLockFile(userId)).getAbsolutePath();
    }

    @Override
    String getRebootEscrowServerBlob() {
        return makeDirs(mStorageDir, super.getRebootEscrowServerBlob()).getAbsolutePath();
    }

    @Override
    protected File getSyntheticPasswordDirectoryForUser(int userId) {
        return makeDirs(mStorageDir, super.getSyntheticPasswordDirectoryForUser(
+99 −3
Original line number Diff line number Diff line
@@ -26,6 +26,7 @@ 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.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doNothing;
@@ -52,6 +53,7 @@ import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;

import com.android.internal.widget.RebootEscrowListener;
import com.android.server.locksettings.ResumeOnRebootServiceProvider.ResumeOnRebootServiceConnection;

import org.junit.Before;
import org.junit.Test;
@@ -92,6 +94,7 @@ public class RebootEscrowManagerTests {
    private UserManager mUserManager;
    private RebootEscrowManager.Callbacks mCallbacks;
    private IRebootEscrow mRebootEscrow;
    private ResumeOnRebootServiceConnection mServiceConnection;
    private RebootEscrowKeyStoreManager mKeyStoreManager;

    LockSettingsStorageTestable mStorage;
@@ -108,6 +111,7 @@ public class RebootEscrowManagerTests {

    static class MockInjector extends RebootEscrowManager.Injector {
        private final IRebootEscrow mRebootEscrow;
        private final ResumeOnRebootServiceConnection mServiceConnection;
        private final RebootEscrowProviderInterface mRebootEscrowProvider;
        private final UserManager mUserManager;
        private final MockableRebootEscrowInjected mInjected;
@@ -116,10 +120,11 @@ public class RebootEscrowManagerTests {
        MockInjector(Context context, UserManager userManager,
                IRebootEscrow rebootEscrow,
                RebootEscrowKeyStoreManager keyStoreManager,
                LockSettingsStorageTestable storage,
                MockableRebootEscrowInjected injected) {
            super(context);
            super(context, storage);
            mRebootEscrow = rebootEscrow;

            mServiceConnection = null;
            RebootEscrowProviderHalImpl.Injector halInjector =
                    new RebootEscrowProviderHalImpl.Injector() {
                        @Override
@@ -133,6 +138,22 @@ public class RebootEscrowManagerTests {
            mInjected = injected;
        }

        MockInjector(Context context, UserManager userManager,
                ResumeOnRebootServiceConnection serviceConnection,
                RebootEscrowKeyStoreManager keyStoreManager,
                LockSettingsStorageTestable storage,
                MockableRebootEscrowInjected injected) {
            super(context, storage);
            mServiceConnection = serviceConnection;
            mRebootEscrow = null;
            RebootEscrowProviderServerBasedImpl.Injector injector =
                    new RebootEscrowProviderServerBasedImpl.Injector(serviceConnection);
            mRebootEscrowProvider = new RebootEscrowProviderServerBasedImpl(storage, injector);
            mUserManager = userManager;
            mKeyStoreManager = keyStoreManager;
            mInjected = injected;
        }

        @Override
        public UserManager getUserManager() {
            return mUserManager;
@@ -165,6 +186,7 @@ public class RebootEscrowManagerTests {
        mUserManager = mock(UserManager.class);
        mCallbacks = mock(RebootEscrowManager.Callbacks.class);
        mRebootEscrow = mock(IRebootEscrow.class);
        mServiceConnection = mock(ResumeOnRebootServiceConnection.class);
        mKeyStoreManager = mock(RebootEscrowKeyStoreManager.class);
        mAesKey = new SecretKeySpec(TEST_AES_KEY, "AES");

@@ -186,7 +208,12 @@ public class RebootEscrowManagerTests {
        when(mCallbacks.isUserSecure(SECURE_SECONDARY_USER_ID)).thenReturn(true);
        mInjected = mock(MockableRebootEscrowInjected.class);
        mService = new RebootEscrowManager(new MockInjector(mContext, mUserManager, mRebootEscrow,
                mKeyStoreManager, mInjected), mCallbacks, mStorage);
                mKeyStoreManager, mStorage, mInjected), mCallbacks, mStorage);
    }

    private void setServerBasedRebootEscrowProvider() throws Exception {
        mService = new RebootEscrowManager(new MockInjector(mContext, mUserManager,
                mServiceConnection, mKeyStoreManager, mStorage, mInjected), mCallbacks, mStorage);
    }

    @Test
@@ -201,6 +228,19 @@ public class RebootEscrowManagerTests {
        verify(mRebootEscrow, never()).storeKey(any());
    }

    @Test
    public void prepareRebootEscrowServerBased_Success() throws Exception {
        setServerBasedRebootEscrowProvider();
        RebootEscrowListener mockListener = mock(RebootEscrowListener.class);
        mService.setRebootEscrowListener(mockListener);
        mService.prepareRebootEscrow();

        mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
        verify(mockListener).onPreparedForReboot(eq(true));
        verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());
        assertFalse(mStorage.hasRebootEscrowServerBlob());
    }

    @Test
    public void prepareRebootEscrow_ClearCredentials_Success() throws Exception {
        RebootEscrowListener mockListener = mock(RebootEscrowListener.class);
@@ -245,6 +285,28 @@ public class RebootEscrowManagerTests {
        assertFalse(mStorage.hasRebootEscrow(NONSECURE_SECONDARY_USER_ID));
    }

    @Test
    public void armServiceServerBased_Success() throws Exception {
        setServerBasedRebootEscrowProvider();
        RebootEscrowListener mockListener = mock(RebootEscrowListener.class);
        mService.setRebootEscrowListener(mockListener);
        mService.prepareRebootEscrow();

        clearInvocations(mServiceConnection);
        mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
        verify(mockListener).onPreparedForReboot(eq(true));
        verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());

        when(mServiceConnection.wrapBlob(any(), anyLong(), anyLong()))
                .thenAnswer(invocation -> invocation.getArgument(0));
        assertTrue(mService.armRebootEscrowIfNeeded());
        verify(mServiceConnection).wrapBlob(any(), anyLong(), anyLong());

        assertTrue(mStorage.hasRebootEscrow(PRIMARY_USER_ID));
        assertFalse(mStorage.hasRebootEscrow(NONSECURE_SECONDARY_USER_ID));
        assertTrue(mStorage.hasRebootEscrowServerBlob());
    }

    @Test
    public void armService_HalFailure_NonFatal() throws Exception {
        RebootEscrowListener mockListener = mock(RebootEscrowListener.class);
@@ -345,6 +407,40 @@ public class RebootEscrowManagerTests {
        verify(mKeyStoreManager).clearKeyStoreEncryptionKey();
    }

    @Test
    public void loadRebootEscrowDataIfAvailable_ServerBased_Success() throws Exception {
        setServerBasedRebootEscrowProvider();

        when(mInjected.getBootCount()).thenReturn(0);
        RebootEscrowListener mockListener = mock(RebootEscrowListener.class);
        mService.setRebootEscrowListener(mockListener);
        mService.prepareRebootEscrow();

        clearInvocations(mServiceConnection);
        mService.callToRebootEscrowIfNeeded(PRIMARY_USER_ID, FAKE_SP_VERSION, FAKE_AUTH_TOKEN);
        verify(mockListener).onPreparedForReboot(eq(true));
        verify(mServiceConnection, never()).wrapBlob(any(), anyLong(), anyLong());

        // Use x -> x for both wrap & unwrap functions.
        when(mServiceConnection.wrapBlob(any(), anyLong(), anyLong()))
                .thenAnswer(invocation -> invocation.getArgument(0));
        assertTrue(mService.armRebootEscrowIfNeeded());
        verify(mServiceConnection).wrapBlob(any(), anyLong(), anyLong());
        assertTrue(mStorage.hasRebootEscrowServerBlob());

        // pretend reboot happens here
        when(mInjected.getBootCount()).thenReturn(1);
        ArgumentCaptor<Boolean> metricsSuccessCaptor = ArgumentCaptor.forClass(Boolean.class);
        doNothing().when(mInjected).reportMetric(metricsSuccessCaptor.capture());

        when(mServiceConnection.unwrap(any(), anyLong()))
                .thenAnswer(invocation -> invocation.getArgument(0));
        mService.loadRebootEscrowDataIfAvailable();
        verify(mServiceConnection).unwrap(any(), anyLong());
        assertTrue(metricsSuccessCaptor.getValue());
        verify(mKeyStoreManager).clearKeyStoreEncryptionKey();
    }

    @Test
    public void loadRebootEscrowDataIfAvailable_TooManyBootsInBetween_NoMetrics() throws Exception {
        when(mInjected.getBootCount()).thenReturn(0);
Loading