Merge changes I45054e68,I6f301314
* changes: Make the service timeout and blob lifetime configurable Call server based implementation to persist reboot escrow key
This commit is contained in:
@@ -88,6 +88,7 @@ class LockSettingsStorage {
|
|||||||
private static final String CHILD_PROFILE_LOCK_FILE = "gatekeeper.profile.key";
|
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_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/";
|
private static final String SYNTHETIC_PASSWORD_DIRECTORY = "spblob/";
|
||||||
|
|
||||||
@@ -317,6 +318,22 @@ class LockSettingsStorage {
|
|||||||
deleteFile(getRebootEscrowFile(userId));
|
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) {
|
public boolean hasPassword(int userId) {
|
||||||
return hasFile(getLockPasswordFilename(userId));
|
return hasFile(getLockPasswordFilename(userId));
|
||||||
}
|
}
|
||||||
@@ -445,6 +462,12 @@ class LockSettingsStorage {
|
|||||||
return getLockCredentialFilePathForUser(userId, REBOOT_ESCROW_FILE);
|
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) {
|
private String getLockCredentialFilePathForUser(int userId, String basename) {
|
||||||
String dataSystemDirectory = Environment.getDataDirectory().getAbsolutePath() +
|
String dataSystemDirectory = Environment.getDataDirectory().getAbsolutePath() +
|
||||||
SYSTEM_DIRECTORY;
|
SYSTEM_DIRECTORY;
|
||||||
|
|||||||
@@ -124,26 +124,28 @@ class RebootEscrowManager {
|
|||||||
static class Injector {
|
static class Injector {
|
||||||
protected Context mContext;
|
protected Context mContext;
|
||||||
private final RebootEscrowKeyStoreManager mKeyStoreManager;
|
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;
|
mContext = context;
|
||||||
|
mStorage = storage;
|
||||||
mKeyStoreManager = new RebootEscrowKeyStoreManager();
|
mKeyStoreManager = new RebootEscrowKeyStoreManager();
|
||||||
|
}
|
||||||
|
|
||||||
RebootEscrowProviderInterface rebootEscrowProvider = null;
|
private RebootEscrowProviderInterface createRebootEscrowProvider() {
|
||||||
// TODO(xunchang) add implementation for server based ror.
|
RebootEscrowProviderInterface rebootEscrowProvider;
|
||||||
if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_OTA,
|
if (DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_OTA,
|
||||||
"server_based_ror_enabled", false)) {
|
"server_based_ror_enabled", false)) {
|
||||||
Slog.e(TAG, "Server based ror isn't implemented yet.");
|
rebootEscrowProvider = new RebootEscrowProviderServerBasedImpl(mContext, mStorage);
|
||||||
} else {
|
} else {
|
||||||
rebootEscrowProvider = new RebootEscrowProviderHalImpl();
|
rebootEscrowProvider = new RebootEscrowProviderHalImpl();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rebootEscrowProvider != null && rebootEscrowProvider.hasRebootEscrowSupport()) {
|
if (rebootEscrowProvider.hasRebootEscrowSupport()) {
|
||||||
mRebootEscrowProvider = rebootEscrowProvider;
|
return rebootEscrowProvider;
|
||||||
} else {
|
|
||||||
mRebootEscrowProvider = null;
|
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Context getContext() {
|
public Context getContext() {
|
||||||
@@ -159,6 +161,12 @@ class RebootEscrowManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public RebootEscrowProviderInterface getRebootEscrowProvider() {
|
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;
|
return mRebootEscrowProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +185,7 @@ class RebootEscrowManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
RebootEscrowManager(Context context, Callbacks callbacks, LockSettingsStorage storage) {
|
RebootEscrowManager(Context context, Callbacks callbacks, LockSettingsStorage storage) {
|
||||||
this(new Injector(context), callbacks, storage);
|
this(new Injector(context, storage), callbacks, storage);
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@VisibleForTesting
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,6 +81,11 @@ public class LockSettingsStorageTestable extends LockSettingsStorage {
|
|||||||
super.getChildProfileLockFile(userId)).getAbsolutePath();
|
super.getChildProfileLockFile(userId)).getAbsolutePath();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
String getRebootEscrowServerBlob() {
|
||||||
|
return makeDirs(mStorageDir, super.getRebootEscrowServerBlob()).getAbsolutePath();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected File getSyntheticPasswordDirectoryForUser(int userId) {
|
protected File getSyntheticPasswordDirectoryForUser(int userId) {
|
||||||
return makeDirs(mStorageDir, super.getSyntheticPasswordDirectoryForUser(
|
return makeDirs(mStorageDir, super.getSyntheticPasswordDirectoryForUser(
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import static org.junit.Assert.assertNotNull;
|
|||||||
import static org.junit.Assert.assertTrue;
|
import static org.junit.Assert.assertTrue;
|
||||||
import static org.mockito.ArgumentMatchers.any;
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
import static org.mockito.ArgumentMatchers.eq;
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
import static org.mockito.Mockito.clearInvocations;
|
import static org.mockito.Mockito.clearInvocations;
|
||||||
import static org.mockito.Mockito.doNothing;
|
import static org.mockito.Mockito.doNothing;
|
||||||
@@ -52,6 +53,7 @@ import androidx.test.filters.SmallTest;
|
|||||||
import androidx.test.runner.AndroidJUnit4;
|
import androidx.test.runner.AndroidJUnit4;
|
||||||
|
|
||||||
import com.android.internal.widget.RebootEscrowListener;
|
import com.android.internal.widget.RebootEscrowListener;
|
||||||
|
import com.android.server.locksettings.ResumeOnRebootServiceProvider.ResumeOnRebootServiceConnection;
|
||||||
|
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
@@ -92,6 +94,7 @@ public class RebootEscrowManagerTests {
|
|||||||
private UserManager mUserManager;
|
private UserManager mUserManager;
|
||||||
private RebootEscrowManager.Callbacks mCallbacks;
|
private RebootEscrowManager.Callbacks mCallbacks;
|
||||||
private IRebootEscrow mRebootEscrow;
|
private IRebootEscrow mRebootEscrow;
|
||||||
|
private ResumeOnRebootServiceConnection mServiceConnection;
|
||||||
private RebootEscrowKeyStoreManager mKeyStoreManager;
|
private RebootEscrowKeyStoreManager mKeyStoreManager;
|
||||||
|
|
||||||
LockSettingsStorageTestable mStorage;
|
LockSettingsStorageTestable mStorage;
|
||||||
@@ -108,6 +111,7 @@ public class RebootEscrowManagerTests {
|
|||||||
|
|
||||||
static class MockInjector extends RebootEscrowManager.Injector {
|
static class MockInjector extends RebootEscrowManager.Injector {
|
||||||
private final IRebootEscrow mRebootEscrow;
|
private final IRebootEscrow mRebootEscrow;
|
||||||
|
private final ResumeOnRebootServiceConnection mServiceConnection;
|
||||||
private final RebootEscrowProviderInterface mRebootEscrowProvider;
|
private final RebootEscrowProviderInterface mRebootEscrowProvider;
|
||||||
private final UserManager mUserManager;
|
private final UserManager mUserManager;
|
||||||
private final MockableRebootEscrowInjected mInjected;
|
private final MockableRebootEscrowInjected mInjected;
|
||||||
@@ -116,10 +120,11 @@ public class RebootEscrowManagerTests {
|
|||||||
MockInjector(Context context, UserManager userManager,
|
MockInjector(Context context, UserManager userManager,
|
||||||
IRebootEscrow rebootEscrow,
|
IRebootEscrow rebootEscrow,
|
||||||
RebootEscrowKeyStoreManager keyStoreManager,
|
RebootEscrowKeyStoreManager keyStoreManager,
|
||||||
|
LockSettingsStorageTestable storage,
|
||||||
MockableRebootEscrowInjected injected) {
|
MockableRebootEscrowInjected injected) {
|
||||||
super(context);
|
super(context, storage);
|
||||||
mRebootEscrow = rebootEscrow;
|
mRebootEscrow = rebootEscrow;
|
||||||
|
mServiceConnection = null;
|
||||||
RebootEscrowProviderHalImpl.Injector halInjector =
|
RebootEscrowProviderHalImpl.Injector halInjector =
|
||||||
new RebootEscrowProviderHalImpl.Injector() {
|
new RebootEscrowProviderHalImpl.Injector() {
|
||||||
@Override
|
@Override
|
||||||
@@ -133,6 +138,22 @@ public class RebootEscrowManagerTests {
|
|||||||
mInjected = injected;
|
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
|
@Override
|
||||||
public UserManager getUserManager() {
|
public UserManager getUserManager() {
|
||||||
return mUserManager;
|
return mUserManager;
|
||||||
@@ -165,6 +186,7 @@ public class RebootEscrowManagerTests {
|
|||||||
mUserManager = mock(UserManager.class);
|
mUserManager = mock(UserManager.class);
|
||||||
mCallbacks = mock(RebootEscrowManager.Callbacks.class);
|
mCallbacks = mock(RebootEscrowManager.Callbacks.class);
|
||||||
mRebootEscrow = mock(IRebootEscrow.class);
|
mRebootEscrow = mock(IRebootEscrow.class);
|
||||||
|
mServiceConnection = mock(ResumeOnRebootServiceConnection.class);
|
||||||
mKeyStoreManager = mock(RebootEscrowKeyStoreManager.class);
|
mKeyStoreManager = mock(RebootEscrowKeyStoreManager.class);
|
||||||
mAesKey = new SecretKeySpec(TEST_AES_KEY, "AES");
|
mAesKey = new SecretKeySpec(TEST_AES_KEY, "AES");
|
||||||
|
|
||||||
@@ -186,7 +208,12 @@ public class RebootEscrowManagerTests {
|
|||||||
when(mCallbacks.isUserSecure(SECURE_SECONDARY_USER_ID)).thenReturn(true);
|
when(mCallbacks.isUserSecure(SECURE_SECONDARY_USER_ID)).thenReturn(true);
|
||||||
mInjected = mock(MockableRebootEscrowInjected.class);
|
mInjected = mock(MockableRebootEscrowInjected.class);
|
||||||
mService = new RebootEscrowManager(new MockInjector(mContext, mUserManager, mRebootEscrow,
|
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
|
@Test
|
||||||
@@ -201,6 +228,19 @@ public class RebootEscrowManagerTests {
|
|||||||
verify(mRebootEscrow, never()).storeKey(any());
|
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
|
@Test
|
||||||
public void prepareRebootEscrow_ClearCredentials_Success() throws Exception {
|
public void prepareRebootEscrow_ClearCredentials_Success() throws Exception {
|
||||||
RebootEscrowListener mockListener = mock(RebootEscrowListener.class);
|
RebootEscrowListener mockListener = mock(RebootEscrowListener.class);
|
||||||
@@ -245,6 +285,28 @@ public class RebootEscrowManagerTests {
|
|||||||
assertFalse(mStorage.hasRebootEscrow(NONSECURE_SECONDARY_USER_ID));
|
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
|
@Test
|
||||||
public void armService_HalFailure_NonFatal() throws Exception {
|
public void armService_HalFailure_NonFatal() throws Exception {
|
||||||
RebootEscrowListener mockListener = mock(RebootEscrowListener.class);
|
RebootEscrowListener mockListener = mock(RebootEscrowListener.class);
|
||||||
@@ -345,6 +407,40 @@ public class RebootEscrowManagerTests {
|
|||||||
verify(mKeyStoreManager).clearKeyStoreEncryptionKey();
|
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
|
@Test
|
||||||
public void loadRebootEscrowDataIfAvailable_TooManyBootsInBetween_NoMetrics() throws Exception {
|
public void loadRebootEscrowDataIfAvailable_TooManyBootsInBetween_NoMetrics() throws Exception {
|
||||||
when(mInjected.getBootCount()).thenReturn(0);
|
when(mInjected.getBootCount()).thenReturn(0);
|
||||||
|
|||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user