Merge "Add support for testing mode root certificate." into pi-dev

This commit is contained in:
Dmitry Dementyev
2018-03-29 23:25:14 +00:00
committed by Android (Google) Code Review
8 changed files with 567 additions and 62 deletions

View File

@@ -37,6 +37,40 @@ public final class TrustedRootCertificates {
public static final String GOOGLE_CLOUD_KEY_VAULT_SERVICE_V1_ALIAS =
"GoogleCloudKeyVaultServiceV1";
/**
* Certificate used for client-side end-to-end encryption tests.
* When recovery controller is initialized with the certificate, recovery snapshots will only
* contain application keys started with {@link INSECURE_KEY_ALIAS}.
* Recovery snapshot will only be created if device is unlocked with password started with
* {@link #INSECURE_PASSWORD_PREFIX}.
*
* @hide
*/
public static final String TEST_ONLY_INSECURE_CERTIFICATE_ALIAS =
"TEST_ONLY_INSECURE_CERTIFICATE_ALIAS";
/**
* TODO: Add insecure certificate to TestApi.
* @hide
*/
public static @NonNull X509Certificate getTestOnlyInsecureCertificate() {
return parseBase64Certificate(TEST_ONLY_INSECURE_CERTIFICATE_BASE64);
}
/**
* Keys, which alias starts with the prefix are not protected if
* recovery agent uses {@link #TEST_ONLY_INSECURE_CERTIFICATE_ALIAS} root certificate.
* @hide
*/
public static final String INSECURE_KEY_ALIAS_PREFIX =
"INSECURE_KEY_ALIAS_KEY_MATERIAL_IS_NOT_PROTECTED_";
/**
* Prefix for insecure passwords with length 14.
* Passwords started with the prefix are not protected if recovery agent uses
* {@link #TEST_ONLY_INSECURE_CERTIFICATE_ALIAS} root certificate.
* @hide
*/
public static final String INSECURE_PASSWORD_PREFIX =
"INSECURE_PSWD_";
private static final String GOOGLE_CLOUD_KEY_VAULT_SERVICE_V1_BASE64 = ""
+ "MIIFJjCCAw6gAwIBAgIJAIobXsJlzhNdMA0GCSqGSIb3DQEBDQUAMCAxHjAcBgNV"
@@ -68,13 +102,43 @@ public final class TrustedRootCertificates {
+ "/oM58v0orUWINtIc2hBlka36PhATYQiLf+AiWKnwhCaaHExoYKfQlMtXBodNvOK8"
+ "xqx69x05q/qbHKEcTHrsss630vxrp1niXvA=";
private static final String TEST_ONLY_INSECURE_CERTIFICATE_BASE64 = ""
+ "MIIFMDCCAxigAwIBAgIJAIZ9/G8KQie9MA0GCSqGSIb3DQEBDQUAMCUxIzAhBgNV"
+ "BAMMGlRlc3QgT25seSBVbnNlY3VyZSBSb290IENBMB4XDTE4MDMyODAwMzIyM1oX"
+ "DTM4MDMyMzAwMzIyM1owJTEjMCEGA1UEAwwaVGVzdCBPbmx5IFVuc2VjdXJlIFJv"
+ "b3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDGxFNzAEyzSPmw"
+ "E5gfuBXdXq++bl9Ep62V7Xn1UiejvmS+pRHT39pf/M7sl4Zr9ezanJTrFvf9+B85"
+ "VGehdsD32TgfEjThcqaoQCI6pKkHYsUo7FZ5n+G3eE8oabWRZJMVo3QDjnnFYp7z"
+ "20vnpjDofI2oQyxHcb/1yep+ca1+4lIvbUp/ybhNFqhRXAMcDXo7pyH38eUQ1JdK"
+ "Q/QlBbShpFEqx1Y6KilKfTDf7Wenqr67LkaEim//yLZjlHzn/BpuRTrpo+XmJZx1"
+ "P9CX9LGOXTtmsaCcYgD4yijOvV8aEsIJaf1kCIO558oH0oQc+0JG5aXeLN7BDlyZ"
+ "vH0RdSx5nQLS9kj2I6nthOw/q00/L+S6A0m5jyNZOAl1SY78p+wO0d9eHbqQzJwf"
+ "EsSq3qGAqlgQyyjp6oxHBqT9hZtN4rxw+iq0K1S4kmTLNF1FvmIB1BE+lNvvoGdY"
+ "5G0b6Pe4R5JFn9LV3C3PEmSYnae7iG0IQlKmRADIuvfJ7apWAVanJPJAAWh2Akfp"
+ "8Uxr02cHoY6o7vsEhJJOeMkipaBHThESm/XeFVubQzNfZ9gjQnB9ZX2v+lyj+WYZ"
+ "SAz3RuXx6TlLrmWccMpQDR1ibcgyyjLUtX3kwZl2OxmJXitjuD7xlxvAXYob15N+"
+ "K4xKHgxUDrbt2zU/tY0vgepAUg/xbwIDAQABo2MwYTAdBgNVHQ4EFgQUwyeNpYgs"
+ "XXYvh9z0/lFrja7sV+swHwYDVR0jBBgwFoAUwyeNpYgsXXYvh9z0/lFrja7sV+sw"
+ "DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQENBQAD"
+ "ggIBAGuOsvMN5SD3RIQnMJtBpcHNrxun+QFjPZFlYCLfIPrUkHpn5O1iIIq8tVLd"
+ "2V+12VKnToUEANsYBD3MP8XjP+6GZ7ZQ2rwLGvUABKSX4YXvmjEEXZUZp0y3tIV4"
+ "kUDlbACzguPneZDp5Qo7YWH4orgqzHkn0sD/ikO5XrAqmzc245ewJlrf+V11mjcu"
+ "ELfDrEejpPhi7Hk/ZNR0ftP737Hs/dNoCLCIaVNgYzBZhgo4kd220TeJu2ttW0XZ"
+ "ldyShtpcOmyWKBgVseixR6L/3sspPHyAPXkSuRo0Eh1xvzDKCg9ttb0qoacTlXMF"
+ "GkBpNzmVq67NWFGGa9UElift1mv6RfktPCAGZ+Ai8xUiKAUB0Eookpt/8gX9Senq"
+ "yP/jMxkxXmHWxUu8+KnLvj6WLrfftuuD7u3cfc7j5kkrheDz3O4h4477GnqL5wdo"
+ "9DuEsNc4FxJVz8Iy8RS6cJuW4pihYpM1Tyn7uopLnImpYzEY+R5aQqqr+q/A1diq"
+ "ogbEKPH6oUiqJUwq3nD70gPBUKJmIzS4vLwLouqUHEm1k/MgHV/BkEU0uVHszPFa"
+ "XUMMCHb0iT9P8LuZ7Ajer3SR/0TRVApCrk/6OV68e+6k/OFpM5kcZnNMD5ANyBri"
+ "Tsz3NrDwSw4i4+Dsfh6A9dB/cEghw4skLaBxnQLQIgVeqCzK";
/**
* The X509 certificate of the trusted root CA cert for the recoverable key store service.
*
* TODO: Change it to the production certificate root CA before the final launch.
*/
private static final X509Certificate GOOGLE_CLOUD_KEY_VAULT_SERVICE_V1_CERTIFICATE =
parseGoogleCloudKeyVaultServiceV1Certificate();
parseBase64Certificate(GOOGLE_CLOUD_KEY_VAULT_SERVICE_V1_BASE64);
private static final int NUMBER_OF_ROOT_CERTIFICATES = 1;
@@ -107,9 +171,9 @@ public final class TrustedRootCertificates {
return certificates;
}
private static X509Certificate parseGoogleCloudKeyVaultServiceV1Certificate() {
private static X509Certificate parseBase64Certificate(String base64Certificate) {
try {
return decodeBase64Cert(GOOGLE_CLOUD_KEY_VAULT_SERVICE_V1_BASE64);
return decodeBase64Cert(base64Certificate);
} catch (CertificateException e) {
// Should not happen
throw new RuntimeException(e);

View File

@@ -79,6 +79,7 @@ public class KeySyncTask implements Runnable {
private final PlatformKeyManager mPlatformKeyManager;
private final RecoverySnapshotStorage mRecoverySnapshotStorage;
private final RecoverySnapshotListenersStorage mSnapshotListenersStorage;
private final TestOnlyInsecureCertificateHelper mTestOnlyInsecureCertificateHelper;
public static KeySyncTask newInstance(
Context context,
@@ -98,7 +99,8 @@ public class KeySyncTask implements Runnable {
credentialType,
credential,
credentialUpdated,
PlatformKeyManager.getInstance(context, recoverableKeyStoreDb));
PlatformKeyManager.getInstance(context, recoverableKeyStoreDb),
new TestOnlyInsecureCertificateHelper());
}
/**
@@ -110,6 +112,7 @@ public class KeySyncTask implements Runnable {
* @param credential The credential, encoded as a {@link String}.
* @param credentialUpdated signals weather credentials were updated.
* @param platformKeyManager platform key manager
* @param TestOnlyInsecureCertificateHelper utility class used for end-to-end tests
*/
@VisibleForTesting
KeySyncTask(
@@ -120,7 +123,8 @@ public class KeySyncTask implements Runnable {
int credentialType,
String credential,
boolean credentialUpdated,
PlatformKeyManager platformKeyManager) {
PlatformKeyManager platformKeyManager,
TestOnlyInsecureCertificateHelper TestOnlyInsecureCertificateHelper) {
mSnapshotListenersStorage = recoverySnapshotListenersStorage;
mRecoverableKeyStoreDb = recoverableKeyStoreDb;
mUserId = userId;
@@ -129,6 +133,7 @@ public class KeySyncTask implements Runnable {
mCredentialUpdated = credentialUpdated;
mPlatformKeyManager = platformKeyManager;
mRecoverySnapshotStorage = snapshotStorage;
mTestOnlyInsecureCertificateHelper = TestOnlyInsecureCertificateHelper;
}
@Override
@@ -189,8 +194,9 @@ public class KeySyncTask implements Runnable {
PublicKey publicKey;
String rootCertAlias =
mRecoverableKeyStoreDb.getActiveRootOfTrust(mUserId, recoveryAgentUid);
rootCertAlias = mTestOnlyInsecureCertificateHelper
.getDefaultCertificateAliasIfEmpty(rootCertAlias);
rootCertAlias = replaceEmptyValueWithSecureDefault(rootCertAlias);
CertPath certPath = mRecoverableKeyStoreDb.getRecoveryServiceCertPath(mUserId,
recoveryAgentUid, rootCertAlias);
if (certPath != null) {
@@ -212,12 +218,18 @@ public class KeySyncTask implements Runnable {
return;
}
// The only place in this class which uses credential value
if (!TrustedRootCertificates.GOOGLE_CLOUD_KEY_VAULT_SERVICE_V1_ALIAS.equals(
rootCertAlias)) {
// TODO: allow only whitelisted LSKF usage
Log.w(TAG, "Untrusted root certificate is used by recovery agent "
if (mTestOnlyInsecureCertificateHelper.isTestOnlyCertificate(rootCertAlias)) {
Log.w(TAG, "Insecure root certificate is used by recovery agent "
+ recoveryAgentUid);
if (mTestOnlyInsecureCertificateHelper.doesCredentailSupportInsecureMode(
mCredentialType, mCredential)) {
Log.w(TAG, "Whitelisted credential is used to generate snapshot by "
+ "recovery agent "+ recoveryAgentUid);
} else {
Log.w(TAG, "Non whitelisted credential is used to generate recovery snapshot by "
+ recoveryAgentUid + " - ignore attempt.");
return; // User secret will not be used.
}
}
byte[] salt = generateSalt();
@@ -239,8 +251,10 @@ public class KeySyncTask implements Runnable {
return;
}
// TODO: filter raw keys based on the root of trust.
// It is the only place in the class where raw key material is used.
// Only include insecure key material for test
if (mTestOnlyInsecureCertificateHelper.isTestOnlyCertificate(rootCertAlias)) {
rawKeys = mTestOnlyInsecureCertificateHelper.keepOnlyWhitelistedInsecureKeys(rawKeys);
}
SecretKey recoveryKey;
try {
recoveryKey = generateRecoveryKey();
@@ -467,14 +481,4 @@ public class KeySyncTask implements Runnable {
}
return keyEntries;
}
private @NonNull String replaceEmptyValueWithSecureDefault(
@Nullable String rootCertificateAlias) {
if (rootCertificateAlias == null || rootCertificateAlias.isEmpty()) {
Log.e(TAG, "rootCertificateAlias is null or empty");
// Use the default Google Key Vault Service CA certificate if the alias is not provided
rootCertificateAlias = TrustedRootCertificates.GOOGLE_CLOUD_KEY_VAULT_SERVICE_V1_ALIAS;
}
return rootCertificateAlias;
}
}

View File

@@ -38,7 +38,6 @@ import android.security.keystore.recovery.KeyChainProtectionParams;
import android.security.keystore.recovery.KeyChainSnapshot;
import android.security.keystore.recovery.RecoveryCertPath;
import android.security.keystore.recovery.RecoveryController;
import android.security.keystore.recovery.TrustedRootCertificates;
import android.security.keystore.recovery.WrappedApplicationKey;
import android.security.KeyStore;
import android.util.ArrayMap;
@@ -100,6 +99,7 @@ public class RecoverableKeyStoreManager {
private final RecoverySnapshotStorage mSnapshotStorage;
private final PlatformKeyManager mPlatformKeyManager;
private final ApplicationKeyStorage mApplicationKeyStorage;
private final TestOnlyInsecureCertificateHelper mTestCertHelper;
/**
* Returns a new or existing instance.
@@ -130,7 +130,8 @@ public class RecoverableKeyStoreManager {
RecoverySnapshotStorage.newInstance(),
new RecoverySnapshotListenersStorage(),
platformKeyManager,
applicationKeyStorage);
applicationKeyStorage,
new TestOnlyInsecureCertificateHelper());
}
return mInstance;
}
@@ -144,7 +145,8 @@ public class RecoverableKeyStoreManager {
RecoverySnapshotStorage snapshotStorage,
RecoverySnapshotListenersStorage listenersStorage,
PlatformKeyManager platformKeyManager,
ApplicationKeyStorage applicationKeyStorage) {
ApplicationKeyStorage applicationKeyStorage,
TestOnlyInsecureCertificateHelper TestOnlyInsecureCertificateHelper) {
mContext = context;
mDatabase = recoverableKeyStoreDb;
mRecoverySessionStorage = recoverySessionStorage;
@@ -153,6 +155,7 @@ public class RecoverableKeyStoreManager {
mSnapshotStorage = snapshotStorage;
mPlatformKeyManager = platformKeyManager;
mApplicationKeyStorage = applicationKeyStorage;
mTestCertHelper = TestOnlyInsecureCertificateHelper;
try {
mRecoverableKeyGenerator = RecoverableKeyGenerator.newInstance(mDatabase);
@@ -171,7 +174,8 @@ public class RecoverableKeyStoreManager {
checkRecoverKeyStorePermission();
int userId = UserHandle.getCallingUserId();
int uid = Binder.getCallingUid();
rootCertificateAlias = replaceEmptyValueWithSecureDefault(rootCertificateAlias);
rootCertificateAlias
= mTestCertHelper.getDefaultCertificateAliasIfEmpty(rootCertificateAlias);
// Always set active alias to the argument of the last call to initRecoveryService method,
// even if cert file is incorrect.
@@ -216,7 +220,8 @@ public class RecoverableKeyStoreManager {
// Randomly choose and validate an endpoint certificate from the list
CertPath certPath;
X509Certificate rootCert = getRootCertificate(rootCertificateAlias);
X509Certificate rootCert =
mTestCertHelper.getRootCertificate(rootCertificateAlias);
try {
Log.d(TAG, "Getting and validating a random endpoint certificate");
certPath = certXml.getRandomEndpointCert(rootCert);
@@ -265,7 +270,8 @@ public class RecoverableKeyStoreManager {
@NonNull byte[] recoveryServiceSigFile)
throws RemoteException {
checkRecoverKeyStorePermission();
rootCertificateAlias = replaceEmptyValueWithSecureDefault(rootCertificateAlias);
rootCertificateAlias =
mTestCertHelper.getDefaultCertificateAliasIfEmpty(rootCertificateAlias);
Preconditions.checkNotNull(recoveryServiceCertFile, "recoveryServiceCertFile is null");
Preconditions.checkNotNull(recoveryServiceSigFile, "recoveryServiceSigFile is null");
@@ -279,7 +285,8 @@ public class RecoverableKeyStoreManager {
ERROR_BAD_CERTIFICATE_FORMAT, "Failed to parse the sig file.");
}
X509Certificate rootCert = getRootCertificate(rootCertificateAlias);
X509Certificate rootCert =
mTestCertHelper.getRootCertificate(rootCertificateAlias);
try {
sigXml.verifyFileSignature(rootCert, recoveryServiceCertFile);
} catch (CertValidationException e) {
@@ -519,7 +526,8 @@ public class RecoverableKeyStoreManager {
@NonNull List<KeyChainProtectionParams> secrets)
throws RemoteException {
checkRecoverKeyStorePermission();
rootCertificateAlias = replaceEmptyValueWithSecureDefault(rootCertificateAlias);
rootCertificateAlias =
mTestCertHelper.getDefaultCertificateAliasIfEmpty(rootCertificateAlias);
Preconditions.checkNotNull(sessionId, "invalid session");
Preconditions.checkNotNull(verifierCertPath, "verifierCertPath is null");
Preconditions.checkNotNull(vaultParams, "vaultParams is null");
@@ -534,7 +542,8 @@ public class RecoverableKeyStoreManager {
}
try {
CertUtils.validateCertPath(getRootCertificate(rootCertificateAlias), certPath);
CertUtils.validateCertPath(
mTestCertHelper.getRootCertificate(rootCertificateAlias), certPath);
} catch (CertValidationException e) {
Log.e(TAG, "Failed to validate the given cert path", e);
throw new ServiceSpecificException(ERROR_INVALID_CERTIFICATE, e.getMessage());
@@ -960,27 +969,6 @@ public class RecoverableKeyStoreManager {
}
}
private X509Certificate getRootCertificate(String rootCertificateAlias) throws RemoteException {
rootCertificateAlias = replaceEmptyValueWithSecureDefault(rootCertificateAlias);
X509Certificate rootCertificate =
TrustedRootCertificates.getRootCertificate(rootCertificateAlias);
if (rootCertificate == null) {
throw new ServiceSpecificException(
ERROR_INVALID_CERTIFICATE, "The provided root certificate alias is invalid");
}
return rootCertificate;
}
private @NonNull String replaceEmptyValueWithSecureDefault(
@Nullable String rootCertificateAlias) {
if (rootCertificateAlias == null || rootCertificateAlias.isEmpty()) {
Log.e(TAG, "rootCertificateAlias is null or empty");
// Use the default Google Key Vault Service CA certificate if the alias is not provided
rootCertificateAlias = TrustedRootCertificates.GOOGLE_CLOUD_KEY_VAULT_SERVICE_V1_ALIAS;
}
return rootCertificateAlias;
}
private void checkRecoverKeyStorePermission() {
mContext.enforceCallingOrSelfPermission(
Manifest.permission.RECOVER_KEYSTORE,

View File

@@ -0,0 +1,103 @@
/*
* Copyright (C) 2018 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.recoverablekeystore;
import static android.security.keystore.recovery.RecoveryController.ERROR_INVALID_CERTIFICATE;
import com.android.internal.widget.LockPatternUtils;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.security.keystore.recovery.TrustedRootCertificates;
import android.util.Log;
import java.util.HashMap;
import java.security.cert.X509Certificate;
import java.util.Map;
import javax.crypto.SecretKey;
/**
* The class provides helper methods to support end-to-end test with insecure certificate.
*/
public class TestOnlyInsecureCertificateHelper {
private static final String TAG = "TestCertHelper";
/**
* Constructor for the helper class.
*/
public TestOnlyInsecureCertificateHelper() {
}
/**
* Returns a root certificate installed in the system for given alias.
* Returns default secure certificate if alias is empty or null.
* Can return insecure certificate for its alias.
*/
public @NonNull X509Certificate
getRootCertificate(String rootCertificateAlias) throws RemoteException {
rootCertificateAlias = getDefaultCertificateAliasIfEmpty(rootCertificateAlias);
if (isTestOnlyCertificate(rootCertificateAlias)) {
return TrustedRootCertificates.getTestOnlyInsecureCertificate();
}
X509Certificate rootCertificate =
TrustedRootCertificates.getRootCertificate(rootCertificateAlias);
if (rootCertificate == null) {
throw new ServiceSpecificException(
ERROR_INVALID_CERTIFICATE, "The provided root certificate alias is invalid");
}
return rootCertificate;
}
public @NonNull String getDefaultCertificateAliasIfEmpty(
@Nullable String rootCertificateAlias) {
if (rootCertificateAlias == null || rootCertificateAlias.isEmpty()) {
Log.e(TAG, "rootCertificateAlias is null or empty - use secure default value");
// Use the default Google Key Vault Service CA certificate if the alias is not provided
rootCertificateAlias = TrustedRootCertificates.GOOGLE_CLOUD_KEY_VAULT_SERVICE_V1_ALIAS;
}
return rootCertificateAlias;
}
public boolean isTestOnlyCertificate(String rootCertificateAlias) {
return TrustedRootCertificates.TEST_ONLY_INSECURE_CERTIFICATE_ALIAS
.equals(rootCertificateAlias);
}
public boolean doesCredentailSupportInsecureMode(int credentialType, String credential) {
return (credentialType == LockPatternUtils.CREDENTIAL_TYPE_PASSWORD)
&& (credential != null)
&& credential.startsWith(TrustedRootCertificates.INSECURE_PASSWORD_PREFIX);
}
public Map<String, SecretKey> keepOnlyWhitelistedInsecureKeys(Map<String, SecretKey> rawKeys) {
if (rawKeys == null) {
return null;
}
Map<String, SecretKey> filteredKeys = new HashMap<>();
for (Map.Entry<String, SecretKey> entry : rawKeys.entrySet()) {
String alias = entry.getKey();
if (alias != null
&& alias.startsWith(TrustedRootCertificates.INSECURE_KEY_ALIAS_PREFIX)) {
filteredKeys.put(entry.getKey(), entry.getValue());
Log.d(TAG, "adding key with insecure alias " + alias + " to the recovery snapshot");
}
}
return filteredKeys;
}
}

View File

@@ -33,6 +33,11 @@ import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -42,8 +47,8 @@ import android.os.FileUtils;
import android.security.keystore.AndroidKeyStoreSecretKey;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.security.keystore.recovery.KeyDerivationParams;
import android.security.keystore.recovery.KeyChainSnapshot;
import android.security.keystore.recovery.KeyDerivationParams;
import android.security.keystore.recovery.RecoveryController;
import android.security.keystore.recovery.WrappedApplicationKey;
import android.support.test.InstrumentationRegistry;
@@ -59,6 +64,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import java.io.File;
import java.nio.charset.StandardCharsets;
@@ -94,6 +100,7 @@ public class KeySyncTaskTest {
@Mock private PlatformKeyManager mPlatformKeyManager;
@Mock private RecoverySnapshotListenersStorage mSnapshotListenersStorage;
@Spy private TestOnlyInsecureCertificateHelper mTestOnlyInsecureCertificateHelper;
private RecoverySnapshotStorage mRecoverySnapshotStorage;
private RecoverableKeyStoreDb mRecoverableKeyStoreDb;
@@ -130,7 +137,8 @@ public class KeySyncTaskTest {
TEST_CREDENTIAL_TYPE,
TEST_CREDENTIAL,
/*credentialUpdated=*/ false,
mPlatformKeyManager);
mPlatformKeyManager,
mTestOnlyInsecureCertificateHelper);
mWrappingKey = generateAndroidKeyStoreKey();
mEncryptKey = new PlatformEncryptionKey(TEST_GENERATION_ID, mWrappingKey);
@@ -283,6 +291,100 @@ public class KeySyncTaskTest {
assertNotNull(mRecoverySnapshotStorage.get(TEST_RECOVERY_AGENT_UID));
}
@Test
public void run_InTestModeWithWhitelistedCredentials() throws Exception {
mRecoverableKeyStoreDb.setServerParams(
TEST_USER_ID, TEST_RECOVERY_AGENT_UID, TEST_VAULT_HANDLE);
mRecoverableKeyStoreDb.setPlatformKeyGenerationId(TEST_USER_ID, TEST_GENERATION_ID);
addApplicationKey(TEST_USER_ID, TEST_RECOVERY_AGENT_UID, TEST_APP_KEY_ALIAS);
mRecoverableKeyStoreDb.setRecoveryServiceCertPath(
TEST_USER_ID, TEST_RECOVERY_AGENT_UID, TEST_ROOT_CERT_ALIAS, TestData.CERT_PATH_1);
// Enter test mode with whitelisted credentials
when(mTestOnlyInsecureCertificateHelper.isTestOnlyCertificate(any())).thenReturn(true);
when(mTestOnlyInsecureCertificateHelper.doesCredentailSupportInsecureMode(anyInt(), any()))
.thenReturn(true);
mKeySyncTask.run();
verify(mTestOnlyInsecureCertificateHelper)
.getDefaultCertificateAliasIfEmpty(eq(TEST_ROOT_CERT_ALIAS));
// run whitelist checks
verify(mTestOnlyInsecureCertificateHelper)
.doesCredentailSupportInsecureMode(anyInt(), any());
verify(mTestOnlyInsecureCertificateHelper)
.keepOnlyWhitelistedInsecureKeys(any());
KeyChainSnapshot keyChainSnapshot = mRecoverySnapshotStorage.get(TEST_RECOVERY_AGENT_UID);
assertNotNull(keyChainSnapshot); // created snapshot
List<WrappedApplicationKey> applicationKeys = keyChainSnapshot.getWrappedApplicationKeys();
assertThat(applicationKeys).hasSize(0); // non whitelisted key is not included
}
@Test
public void run_InTestModeWithNonWhitelistedCredentials() throws Exception {
mRecoverableKeyStoreDb.setServerParams(
TEST_USER_ID, TEST_RECOVERY_AGENT_UID, TEST_VAULT_HANDLE);
mRecoverableKeyStoreDb.setPlatformKeyGenerationId(TEST_USER_ID, TEST_GENERATION_ID);
addApplicationKey(TEST_USER_ID, TEST_RECOVERY_AGENT_UID, TEST_APP_KEY_ALIAS);
mRecoverableKeyStoreDb.setRecoveryServiceCertPath(
TEST_USER_ID, TEST_RECOVERY_AGENT_UID, TEST_ROOT_CERT_ALIAS, TestData.CERT_PATH_1);
// Enter test mode with non whitelisted credentials
when(mTestOnlyInsecureCertificateHelper.isTestOnlyCertificate(any())).thenReturn(true);
when(mTestOnlyInsecureCertificateHelper.doesCredentailSupportInsecureMode(anyInt(), any()))
.thenReturn(false);
mKeySyncTask.run();
assertNull(mRecoverySnapshotStorage.get(TEST_RECOVERY_AGENT_UID)); // not created
verify(mTestOnlyInsecureCertificateHelper)
.getDefaultCertificateAliasIfEmpty(eq(TEST_ROOT_CERT_ALIAS));
verify(mTestOnlyInsecureCertificateHelper)
.doesCredentailSupportInsecureMode(anyInt(), any());
}
@Test
public void run_doesNotFilterCredentialsAndAliasesInProd() throws Exception {
mRecoverableKeyStoreDb.setServerParams(
TEST_USER_ID, TEST_RECOVERY_AGENT_UID, TEST_VAULT_HANDLE);
mRecoverableKeyStoreDb.setPlatformKeyGenerationId(TEST_USER_ID, TEST_GENERATION_ID);
addApplicationKey(TEST_USER_ID, TEST_RECOVERY_AGENT_UID, TEST_APP_KEY_ALIAS);
mRecoverableKeyStoreDb.setRecoveryServiceCertPath(
TEST_USER_ID, TEST_RECOVERY_AGENT_UID, TEST_ROOT_CERT_ALIAS, TestData.CERT_PATH_1);
mKeySyncTask.run();
assertNotNull(mRecoverySnapshotStorage.get(TEST_RECOVERY_AGENT_UID));
verify(mTestOnlyInsecureCertificateHelper)
.getDefaultCertificateAliasIfEmpty(eq(TEST_ROOT_CERT_ALIAS));
verify(mTestOnlyInsecureCertificateHelper, atLeast(1))
.isTestOnlyCertificate(eq(TEST_ROOT_CERT_ALIAS));
// no whitelists check
verify(mTestOnlyInsecureCertificateHelper, never())
.doesCredentailSupportInsecureMode(anyInt(), any());
verify(mTestOnlyInsecureCertificateHelper, never())
.keepOnlyWhitelistedInsecureKeys(any());
}
@Test
public void run_replacesNullActiveRootAliasWithDefaultValue() throws Exception {
mRecoverableKeyStoreDb.setServerParams(
TEST_USER_ID, TEST_RECOVERY_AGENT_UID, TEST_VAULT_HANDLE);
mRecoverableKeyStoreDb.setPlatformKeyGenerationId(TEST_USER_ID, TEST_GENERATION_ID);
addApplicationKey(TEST_USER_ID, TEST_RECOVERY_AGENT_UID, TEST_APP_KEY_ALIAS);
mRecoverableKeyStoreDb.setRecoveryServiceCertPath(
TEST_USER_ID, TEST_RECOVERY_AGENT_UID, TEST_ROOT_CERT_ALIAS, TestData.CERT_PATH_1);
mRecoverableKeyStoreDb.setActiveRootOfTrust(TEST_USER_ID, TEST_RECOVERY_AGENT_UID,
/*alias=*/ null);
when(mTestOnlyInsecureCertificateHelper.getDefaultCertificateAliasIfEmpty(null))
.thenReturn(TEST_ROOT_CERT_ALIAS); // override default.
mKeySyncTask.run();
verify(mTestOnlyInsecureCertificateHelper).getDefaultCertificateAliasIfEmpty(null);
}
@Test
public void run_sendsEncryptedKeysIfAvailableToSync_withRawPublicKey() throws Exception {
mRecoverableKeyStoreDb.setRecoveryServiceCertPath(
@@ -398,7 +500,8 @@ public class KeySyncTaskTest {
CREDENTIAL_TYPE_PASSWORD,
"password",
/*credentialUpdated=*/ false,
mPlatformKeyManager);
mPlatformKeyManager,
mTestOnlyInsecureCertificateHelper);
mRecoverableKeyStoreDb.setRecoveryServiceCertPath(
TEST_USER_ID, TEST_RECOVERY_AGENT_UID, TEST_ROOT_CERT_ALIAS, TestData.CERT_PATH_1);
@@ -424,7 +527,8 @@ public class KeySyncTaskTest {
CREDENTIAL_TYPE_PASSWORD,
/*credential=*/ "1234",
/*credentialUpdated=*/ false,
mPlatformKeyManager);
mPlatformKeyManager,
mTestOnlyInsecureCertificateHelper);
mRecoverableKeyStoreDb.setRecoveryServiceCertPath(
TEST_USER_ID, TEST_RECOVERY_AGENT_UID, TEST_ROOT_CERT_ALIAS, TestData.CERT_PATH_1);
@@ -451,7 +555,8 @@ public class KeySyncTaskTest {
CREDENTIAL_TYPE_PATTERN,
"12345",
/*credentialUpdated=*/ false,
mPlatformKeyManager);
mPlatformKeyManager,
mTestOnlyInsecureCertificateHelper);
mRecoverableKeyStoreDb.setRecoveryServiceCertPath(
TEST_USER_ID, TEST_RECOVERY_AGENT_UID, TEST_ROOT_CERT_ALIAS, TestData.CERT_PATH_1);
@@ -532,7 +637,8 @@ public class KeySyncTaskTest {
/*credentialType=*/ 3,
"12345",
/*credentialUpdated=*/ false,
mPlatformKeyManager);
mPlatformKeyManager,
mTestOnlyInsecureCertificateHelper);
addApplicationKey(TEST_USER_ID, TEST_RECOVERY_AGENT_UID, TEST_APP_KEY_ALIAS);

View File

@@ -27,6 +27,7 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -67,6 +68,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.Spy;
import java.io.File;
import java.nio.charset.StandardCharsets;
@@ -93,6 +95,8 @@ public class RecoverableKeyStoreManagerTest {
private static final String ROOT_CERTIFICATE_ALIAS = "";
private static final String DEFAULT_ROOT_CERT_ALIAS =
TrustedRootCertificates.GOOGLE_CLOUD_KEY_VAULT_SERVICE_V1_ALIAS;
private static final String INSECURE_CERTIFICATE_ALIAS =
TrustedRootCertificates.TEST_ONLY_INSECURE_CERTIFICATE_ALIAS;
private static final String TEST_SESSION_ID = "karlin";
private static final byte[] TEST_PUBLIC_KEY = new byte[] {
(byte) 0x30, (byte) 0x59, (byte) 0x30, (byte) 0x13, (byte) 0x06, (byte) 0x07, (byte) 0x2a,
@@ -160,6 +164,7 @@ public class RecoverableKeyStoreManagerTest {
@Mock private KeyguardManager mKeyguardManager;
@Mock private PlatformKeyManager mPlatformKeyManager;
@Mock private ApplicationKeyStorage mApplicationKeyStorage;
@Spy private TestOnlyInsecureCertificateHelper mTestOnlyInsecureCertificateHelper;
private RecoverableKeyStoreDb mRecoverableKeyStoreDb;
private File mDatabaseFile;
@@ -195,7 +200,8 @@ public class RecoverableKeyStoreManagerTest {
mRecoverySnapshotStorage,
mMockListenersStorage,
mPlatformKeyManager,
mApplicationKeyStorage);
mApplicationKeyStorage,
mTestOnlyInsecureCertificateHelper);
}
@After
@@ -300,6 +306,9 @@ public class RecoverableKeyStoreManagerTest {
mRecoverableKeyStoreManager.initRecoveryService(ROOT_CERTIFICATE_ALIAS,
TestData.getCertXmlWithSerial(certSerial));
verify(mTestOnlyInsecureCertificateHelper, atLeast(1))
.getDefaultCertificateAliasIfEmpty(ROOT_CERTIFICATE_ALIAS);
assertThat(mRecoverableKeyStoreDb.getShouldCreateSnapshot(userId, uid)).isFalse();
assertThat(mRecoverableKeyStoreDb.getRecoveryServiceCertPath(userId, uid,
DEFAULT_ROOT_CERT_ALIAS)).isEqualTo(TestData.CERT_PATH_1);
@@ -308,6 +317,67 @@ public class RecoverableKeyStoreManagerTest {
assertThat(mRecoverableKeyStoreDb.getRecoveryServicePublicKey(userId, uid)).isNull();
}
@Test
public void initRecoveryService_triesToFilterRootAlias() throws Exception {
int uid = Binder.getCallingUid();
int userId = UserHandle.getCallingUserId();
long certSerial = 1000L;
mRecoverableKeyStoreDb.setShouldCreateSnapshot(userId, uid, false);
mRecoverableKeyStoreManager.initRecoveryService(ROOT_CERTIFICATE_ALIAS,
TestData.getCertXmlWithSerial(certSerial));
verify(mTestOnlyInsecureCertificateHelper, atLeast(1))
.getDefaultCertificateAliasIfEmpty(eq(ROOT_CERTIFICATE_ALIAS));
verify(mTestOnlyInsecureCertificateHelper, atLeast(1))
.getRootCertificate(eq(DEFAULT_ROOT_CERT_ALIAS));
String activeRootAlias = mRecoverableKeyStoreDb.getActiveRootOfTrust(userId, uid);
assertThat(activeRootAlias).isEqualTo(DEFAULT_ROOT_CERT_ALIAS);
}
@Test
public void initRecoveryService_usesProdCertificateForEmptyRootAlias() throws Exception {
int uid = Binder.getCallingUid();
int userId = UserHandle.getCallingUserId();
long certSerial = 1000L;
mRecoverableKeyStoreDb.setShouldCreateSnapshot(userId, uid, false);
mRecoverableKeyStoreManager.initRecoveryService(/*rootCertificateAlias=*/ "",
TestData.getCertXmlWithSerial(certSerial));
verify(mTestOnlyInsecureCertificateHelper, atLeast(1))
.getDefaultCertificateAliasIfEmpty(eq(""));
verify(mTestOnlyInsecureCertificateHelper, atLeast(1))
.getRootCertificate(eq(DEFAULT_ROOT_CERT_ALIAS));
String activeRootAlias = mRecoverableKeyStoreDb.getActiveRootOfTrust(userId, uid);
assertThat(activeRootAlias).isEqualTo(DEFAULT_ROOT_CERT_ALIAS);
}
@Test
public void initRecoveryService_usesProdCertificateForNullRootAlias() throws Exception {
int uid = Binder.getCallingUid();
int userId = UserHandle.getCallingUserId();
long certSerial = 1000L;
mRecoverableKeyStoreDb.setShouldCreateSnapshot(userId, uid, false);
mRecoverableKeyStoreManager.initRecoveryService(/*rootCertificateAlias=*/ null,
TestData.getCertXmlWithSerial(certSerial));
verify(mTestOnlyInsecureCertificateHelper, atLeast(1))
.getDefaultCertificateAliasIfEmpty(null);
verify(mTestOnlyInsecureCertificateHelper, atLeast(1))
.getRootCertificate(eq(DEFAULT_ROOT_CERT_ALIAS));
String activeRootAlias = mRecoverableKeyStoreDb.getActiveRootOfTrust(userId, uid);
assertThat(activeRootAlias).isEqualTo(DEFAULT_ROOT_CERT_ALIAS);
}
@Test
public void initRecoveryService_regeneratesCounterId() throws Exception {
int uid = Binder.getCallingUid();
@@ -416,6 +486,24 @@ public class RecoverableKeyStoreManagerTest {
assertThat(mRecoverableKeyStoreDb.getRecoveryServicePublicKey(userId, uid)).isNull();
}
@Test
public void initRecoveryServiceWithSigFile_usesProdCertificateForNullRootAlias()
throws Exception {
int uid = Binder.getCallingUid();
int userId = UserHandle.getCallingUserId();
long certSerial = 1000L;
mRecoverableKeyStoreDb.setShouldCreateSnapshot(userId, uid, false);
mRecoverableKeyStoreManager.initRecoveryServiceWithSigFile(
/*rootCertificateAlias=*/null, TestData.getCertXml(), TestData.getSigXml());
verify(mTestOnlyInsecureCertificateHelper, atLeast(1))
.getDefaultCertificateAliasIfEmpty(null);
verify(mTestOnlyInsecureCertificateHelper, atLeast(1))
.getRootCertificate(eq(DEFAULT_ROOT_CERT_ALIAS));
}
@Test
public void initRecoveryServiceWithSigFile_throwsIfNullCertFile() throws Exception {
try {
@@ -452,6 +540,18 @@ public class RecoverableKeyStoreManagerTest {
}
}
@Test
public void initRecoveryServiceWithSigFile_throwsIfTestAliasUsedWithProdCert()
throws Exception {
try {
mRecoverableKeyStoreManager.initRecoveryServiceWithSigFile(
INSECURE_CERTIFICATE_ALIAS, TestData.getCertXml(), TestData.getSigXml());
fail("should have thrown");
} catch (ServiceSpecificException e) {
assertThat(e.getMessage()).contains("signature over the cert file is invalid");
}
}
@Test
public void initRecoveryServiceWithSigFile_throwsIfInvalidFileSignature() throws Exception {
byte[] modifiedCertXml = TestData.getCertXml();
@@ -712,7 +812,8 @@ public class RecoverableKeyStoreManagerTest {
}
@Test
public void recoverKeyChainSnapshot_throwsIfFailedToDecryptAllApplicationKeys() throws Exception {
public void recoverKeyChainSnapshot_throwsIfFailedToDecryptAllApplicationKeys()
throws Exception {
mRecoverableKeyStoreManager.startRecoverySession(
TEST_SESSION_ID,
TEST_PUBLIC_KEY,
@@ -792,7 +893,8 @@ public class RecoverableKeyStoreManagerTest {
}
@Test
public void recoverKeyChainSnapshot_worksOnOtherApplicationKeysIfOneDecryptionFails() throws Exception {
public void recoverKeyChainSnapshot_worksOnOtherApplicationKeysIfOneDecryptionFails()
throws Exception {
mRecoverableKeyStoreManager.startRecoverySession(
TEST_SESSION_ID,
TEST_PUBLIC_KEY,

View File

@@ -14,8 +14,12 @@ import java.security.cert.CertificateFactory;
import java.security.cert.CertPath;
import java.security.spec.ECPrivateKeySpec;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
public final class TestData {
private static final String KEY_ALGORITHM = "AES";
private static final long DEFAULT_SERIAL = 1000;
private static final String CERT_PATH_ENCODING = "PkiPath";
@@ -308,4 +312,10 @@ public final class TestData {
KeyFactory keyFactory = KeyFactory.getInstance("EC");
return keyFactory.generatePrivate(new ECPrivateKeySpec(priv, SecureBox.EC_PARAM_SPEC));
}
public static SecretKey generateKey() throws Exception {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
keyGenerator.init(/*keySize=*/ 256);
return keyGenerator.generateKey();
}
}

View File

@@ -0,0 +1,128 @@
package com.android.server.locksettings.recoverablekeystore;
import static com.google.common.truth.Truth.assertThat;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.security.keystore.recovery.TrustedRootCertificates;
import android.support.test.filters.SmallTest;
import android.support.test.runner.AndroidJUnit4;
import android.util.Log;
import com.android.internal.widget.LockPatternUtils;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.HashMap;
import java.security.cert.X509Certificate;
import java.util.Map;
import javax.crypto.SecretKey;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class TestOnlyInsecureCertificateHelperTest {
private final TestOnlyInsecureCertificateHelper mHelper
= new TestOnlyInsecureCertificateHelper();
@Test
public void testDoesCredentailSupportInsecureMode_forNonWhitelistedPassword() throws Exception {
assertThat(mHelper.doesCredentailSupportInsecureMode(
LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, "secret12345")).isFalse();
assertThat(mHelper.doesCredentailSupportInsecureMode(
LockPatternUtils.CREDENTIAL_TYPE_PASSWORD, "1234")).isFalse();
}
@Test
public void testDoesCredentailSupportInsecureMode_forWhitelistedPassword() throws Exception {
assertThat(mHelper.doesCredentailSupportInsecureMode(
LockPatternUtils.CREDENTIAL_TYPE_PASSWORD,
TrustedRootCertificates.INSECURE_PASSWORD_PREFIX)).isTrue();
assertThat(mHelper.doesCredentailSupportInsecureMode(
LockPatternUtils.CREDENTIAL_TYPE_PASSWORD,
TrustedRootCertificates.INSECURE_PASSWORD_PREFIX + "12")).isTrue();
}
@Test
public void testDoesCredentailSupportInsecureMode_Pattern() throws Exception {
assertThat(mHelper.doesCredentailSupportInsecureMode(
LockPatternUtils.CREDENTIAL_TYPE_PATTERN,
TrustedRootCertificates.INSECURE_PASSWORD_PREFIX)).isFalse();
assertThat(mHelper.doesCredentailSupportInsecureMode(
LockPatternUtils.CREDENTIAL_TYPE_NONE,
TrustedRootCertificates.INSECURE_PASSWORD_PREFIX)).isFalse();
}
@Test
public void testIsTestOnlyCertificate() throws Exception {
assertThat(mHelper.isTestOnlyCertificate(
TrustedRootCertificates.GOOGLE_CLOUD_KEY_VAULT_SERVICE_V1_ALIAS)).isFalse();
assertThat(mHelper.isTestOnlyCertificate(
TrustedRootCertificates.TEST_ONLY_INSECURE_CERTIFICATE_ALIAS)).isTrue();
assertThat(mHelper.isTestOnlyCertificate(
"UNKNOWN_ALIAS")).isFalse();
}
@Test
public void testKeepOnlyWhitelistedInsecureKeys_emptyKeysList() throws Exception {
Map<String, SecretKey> rawKeys = new HashMap<>();
Map<String, SecretKey> expectedResult = new HashMap<>();
Map<String, SecretKey> filteredKeys =
mHelper.keepOnlyWhitelistedInsecureKeys(rawKeys);
assertThat(filteredKeys.entrySet()).containsExactlyElementsIn(expectedResult.entrySet());
assertThat(filteredKeys.entrySet()).containsAllIn(rawKeys.entrySet());
}
@Test
public void testKeepOnlyWhitelistedInsecureKeys_singleNonWhitelistedKey() throws Exception {
Map<String, SecretKey> rawKeys = new HashMap<>();
Map<String, SecretKey> expectedResult = new HashMap<>();
String alias = "secureAlias";
rawKeys.put(alias, TestData.generateKey());
Map<String, SecretKey> filteredKeys =
mHelper.keepOnlyWhitelistedInsecureKeys(rawKeys);
assertThat(filteredKeys.entrySet()).containsExactlyElementsIn(expectedResult.entrySet());
assertThat(rawKeys.entrySet()).containsAllIn(filteredKeys.entrySet());
}
@Test
public void testKeepOnlyWhitelistedInsecureKeys_singleWhitelistedKey() throws Exception {
Map<String, SecretKey> rawKeys = new HashMap<>();
Map<String, SecretKey> expectedResult = new HashMap<>();
String alias = TrustedRootCertificates.INSECURE_KEY_ALIAS_PREFIX;
rawKeys.put(alias, TestData.generateKey());
expectedResult.put(alias, rawKeys.get(alias));
Map<String, SecretKey> filteredKeys =
mHelper.keepOnlyWhitelistedInsecureKeys(rawKeys);
assertThat(filteredKeys.entrySet()).containsExactlyElementsIn(expectedResult.entrySet());
assertThat(rawKeys.entrySet()).containsAllIn(filteredKeys.entrySet());
}
@Test
public void testKeepOnlyWhitelistedInsecureKeys() throws Exception {
Map<String, SecretKey> rawKeys = new HashMap<>();
Map<String, SecretKey> expectedResult = new HashMap<>();
String alias = "SECURE_ALIAS" + TrustedRootCertificates.INSECURE_KEY_ALIAS_PREFIX;
rawKeys.put(alias, TestData.generateKey());
alias = TrustedRootCertificates.INSECURE_KEY_ALIAS_PREFIX + "1";
rawKeys.put(alias, TestData.generateKey());
expectedResult.put(alias, rawKeys.get(alias));
alias = TrustedRootCertificates.INSECURE_KEY_ALIAS_PREFIX + "2";
rawKeys.put(alias, TestData.generateKey());
expectedResult.put(alias, rawKeys.get(alias));
Map<String, SecretKey> filteredKeys =
mHelper.keepOnlyWhitelistedInsecureKeys(rawKeys);
assertThat(filteredKeys.entrySet()).containsExactlyElementsIn(expectedResult.entrySet());
assertThat(rawKeys.entrySet()).containsAllIn(filteredKeys.entrySet());
}
}