Merge changes I45054e68,I6f301314 am: 2befe56df0 am: 17cf7a6465 am: 15c3e45a8f

Original change: https://android-review.googlesource.com/c/platform/frameworks/base/+/1555542

MUST ONLY BE SUBMITTED BY AUTOMERGER

Change-Id: I5511b2fcdce3c2a58bab9b6c7a114d775e13883d
diff --git a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
index 7dd961a..b92a83f 100644
--- a/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
+++ b/services/core/java/com/android/server/locksettings/LockSettingsStorage.java
@@ -89,6 +89,7 @@
     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/";
 
@@ -318,6 +319,22 @@
         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));
     }
@@ -446,6 +463,12 @@
         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;
diff --git a/services/core/java/com/android/server/locksettings/RebootEscrowManager.java b/services/core/java/com/android/server/locksettings/RebootEscrowManager.java
index fbec915..06962d4 100644
--- a/services/core/java/com/android/server/locksettings/RebootEscrowManager.java
+++ b/services/core/java/com/android/server/locksettings/RebootEscrowManager.java
@@ -124,26 +124,28 @@
     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 @@
         }
 
         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 @@
     }
 
     RebootEscrowManager(Context context, Callbacks callbacks, LockSettingsStorage storage) {
-        this(new Injector(context), callbacks, storage);
+        this(new Injector(context, storage), callbacks, storage);
     }
 
     @VisibleForTesting
diff --git a/services/core/java/com/android/server/locksettings/RebootEscrowProviderServerBasedImpl.java b/services/core/java/com/android/server/locksettings/RebootEscrowProviderServerBasedImpl.java
new file mode 100644
index 0000000..ba1a680
--- /dev/null
+++ b/services/core/java/com/android/server/locksettings/RebootEscrowProviderServerBasedImpl.java
@@ -0,0 +1,202 @@
+/*
+ * 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.provider.DeviceConfig;
+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 DEFAULT_SERVICE_TIMEOUT_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 DEFAULT_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;
+        }
+
+        long getServiceTimeoutInSeconds() {
+            return DeviceConfig.getLong(DeviceConfig.NAMESPACE_OTA,
+                    "server_based_service_timeout_in_seconds",
+                    DEFAULT_SERVICE_TIMEOUT_IN_SECONDS);
+        }
+
+        long getServerBlobLifetimeInMillis() {
+            return DeviceConfig.getLong(DeviceConfig.NAMESPACE_OTA,
+                    "server_based_server_blob_lifetime_in_millis",
+                    DEFAULT_SERVER_BLOB_LIFETIME_IN_MILLIS);
+        }
+    }
+
+    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(mInjector.getServiceTimeoutInSeconds());
+        byte[] escrowKeyBytes = serviceConnection.unwrap(decryptedBlob,
+                mInjector.getServiceTimeoutInSeconds());
+        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(mInjector.getServiceTimeoutInSeconds());
+        // Ask the server connection service to encrypt the reboot escrow key.
+        byte[] serverEncryptedBlob = serviceConnection.wrapBlob(escrowKeyBytes,
+                mInjector.getServerBlobLifetimeInMillis(), mInjector.getServiceTimeoutInSeconds());
+        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;
+    }
+}
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 1581d9a..691d174 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTestable.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/LockSettingsStorageTestable.java
@@ -82,6 +82,11 @@
     }
 
     @Override
+    String getRebootEscrowServerBlob() {
+        return makeDirs(mStorageDir, super.getRebootEscrowServerBlob()).getAbsolutePath();
+    }
+
+    @Override
     protected File getSyntheticPasswordDirectoryForUser(int userId) {
         return makeDirs(mStorageDir, super.getSyntheticPasswordDirectoryForUser(
                 userId).getAbsolutePath());
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 f74e45b..a4ba4c8 100644
--- a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java
+++ b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowManagerTests.java
@@ -26,6 +26,7 @@
 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.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 @@
     private UserManager mUserManager;
     private RebootEscrowManager.Callbacks mCallbacks;
     private IRebootEscrow mRebootEscrow;
+    private ResumeOnRebootServiceConnection mServiceConnection;
     private RebootEscrowKeyStoreManager mKeyStoreManager;
 
     LockSettingsStorageTestable mStorage;
@@ -108,6 +111,7 @@
 
     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 @@
         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 @@
             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 @@
         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 @@
         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
@@ -202,6 +229,19 @@
     }
 
     @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);
         mService.setRebootEscrowListener(mockListener);
@@ -246,6 +286,28 @@
     }
 
     @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);
         mService.setRebootEscrowListener(mockListener);
@@ -346,6 +408,40 @@
     }
 
     @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);
 
diff --git a/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowProviderServerBasedImplTests.java b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowProviderServerBasedImplTests.java
new file mode 100644
index 0000000..bc1e025
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/locksettings/RebootEscrowProviderServerBasedImplTests.java
@@ -0,0 +1,145 @@
+/*
+ * 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 static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.platform.test.annotations.Presubmit;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.stubbing.Answer;
+
+import java.io.File;
+import java.io.IOException;
+
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+@SmallTest
+@Presubmit
+@RunWith(AndroidJUnit4.class)
+public class RebootEscrowProviderServerBasedImplTests {
+    private SecretKey mKeyStoreEncryptionKey;
+    private RebootEscrowKey mRebootEscrowKey;
+    private ResumeOnRebootServiceProvider.ResumeOnRebootServiceConnection mServiceConnection;
+    private LockSettingsStorageTestable mStorage;
+    private RebootEscrowProviderServerBasedImpl mRebootEscrowProvider;
+    private Answer<byte[]> mFakeEncryption;
+
+    private static final byte[] TEST_AES_KEY = new byte[] {
+            0x48, 0x19, 0x12, 0x54, 0x13, 0x13, 0x52, 0x31,
+            0x44, 0x74, 0x61, 0x54, 0x29, 0x74, 0x37, 0x61,
+            0x70, 0x70, 0x75, 0x25, 0x27, 0x31, 0x49, 0x09,
+            0x26, 0x52, 0x72, 0x63, 0x63, 0x61, 0x78, 0x23,
+    };
+
+    @Before
+    public void setUp() throws Exception {
+        mKeyStoreEncryptionKey = new SecretKeySpec(TEST_AES_KEY, "AES");
+        mRebootEscrowKey = RebootEscrowKey.generate();
+        mServiceConnection = mock(
+                ResumeOnRebootServiceProvider.ResumeOnRebootServiceConnection.class);
+
+        Context context = new ContextWrapper(InstrumentationRegistry.getContext());
+        mStorage = new LockSettingsStorageTestable(context,
+                new File(InstrumentationRegistry.getContext().getFilesDir(), "locksettings"));
+        mRebootEscrowProvider = new RebootEscrowProviderServerBasedImpl(mStorage,
+                new RebootEscrowProviderServerBasedImpl.Injector(mServiceConnection));
+
+        mFakeEncryption = invocation -> {
+            byte[] secret = invocation.getArgument(0);
+            for (int i = 0; i < secret.length; i++) {
+                secret[i] = (byte) (secret[i] ^ 0xf);
+            }
+            return secret;
+        };
+    }
+
+    @Test
+    public void getAndClearRebootEscrowKey_loopback_success() throws Exception {
+        when(mServiceConnection.wrapBlob(any(), anyLong(), anyLong())).thenAnswer(mFakeEncryption);
+        when(mServiceConnection.unwrap(any(), anyLong())).thenAnswer(mFakeEncryption);
+
+        assertTrue(mRebootEscrowProvider.hasRebootEscrowSupport());
+        mRebootEscrowProvider.storeRebootEscrowKey(mRebootEscrowKey, mKeyStoreEncryptionKey);
+        assertTrue(mStorage.hasRebootEscrowServerBlob());
+
+
+        RebootEscrowKey ks = mRebootEscrowProvider.getAndClearRebootEscrowKey(
+                mKeyStoreEncryptionKey);
+        assertThat(ks.getKeyBytes(), is(mRebootEscrowKey.getKeyBytes()));
+        assertFalse(mStorage.hasRebootEscrowServerBlob());
+    }
+
+    @Test
+    public void getAndClearRebootEscrowKey_WrongDecryptionMethod_failure() throws Exception {
+        when(mServiceConnection.wrapBlob(any(), anyLong(), anyLong())).thenAnswer(mFakeEncryption);
+        when(mServiceConnection.unwrap(any(), anyLong())).thenAnswer(
+                invocation -> {
+                    byte[] secret = invocation.getArgument(0);
+                    for (int i = 0; i < secret.length; i++) {
+                        secret[i] = (byte) (secret[i] ^ 0xe);
+                    }
+                    return secret;
+                }
+        );
+
+        assertTrue(mRebootEscrowProvider.hasRebootEscrowSupport());
+        mRebootEscrowProvider.storeRebootEscrowKey(mRebootEscrowKey, mKeyStoreEncryptionKey);
+        assertTrue(mStorage.hasRebootEscrowServerBlob());
+
+        // Expect to get wrong key bytes
+        RebootEscrowKey ks = mRebootEscrowProvider.getAndClearRebootEscrowKey(
+                mKeyStoreEncryptionKey);
+        assertNotEquals(ks.getKeyBytes(), mRebootEscrowKey.getKeyBytes());
+        assertFalse(mStorage.hasRebootEscrowServerBlob());
+    }
+
+    @Test
+    public void getAndClearRebootEscrowKey_ServiceConnectionException_failure() throws Exception {
+        when(mServiceConnection.wrapBlob(any(), anyLong(), anyLong())).thenAnswer(mFakeEncryption);
+        doThrow(IOException.class).when(mServiceConnection).unwrap(any(), anyLong());
+
+        assertTrue(mRebootEscrowProvider.hasRebootEscrowSupport());
+        mRebootEscrowProvider.storeRebootEscrowKey(mRebootEscrowKey, mKeyStoreEncryptionKey);
+        assertTrue(mStorage.hasRebootEscrowServerBlob());
+
+        // Expect to get null key bytes when the server service fails to unwrap the blob.
+        RebootEscrowKey ks = mRebootEscrowProvider.getAndClearRebootEscrowKey(
+                mKeyStoreEncryptionKey);
+        assertNull(ks);
+        assertFalse(mStorage.hasRebootEscrowServerBlob());
+    }
+}