Merge "Add PlatformKeyManager helper for RecoverableKeyStoreLoader"
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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;
|
||||
|
||||
/**
|
||||
* Error thrown initializing {@link PlatformKeyManager} if the user is not secure (i.e., has no
|
||||
* lock screen set).
|
||||
*/
|
||||
public class InsecureUserException extends Exception {
|
||||
|
||||
/**
|
||||
* A new instance with {@code message} error message.
|
||||
*/
|
||||
public InsecureUserException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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 java.security.Key;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
|
||||
/**
|
||||
* Proxies {@link java.security.KeyStore}. As all of its methods are final, it cannot otherwise be
|
||||
* mocked for tests.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public interface KeyStoreProxy {
|
||||
|
||||
/** @see KeyStore#containsAlias(String) */
|
||||
boolean containsAlias(String alias) throws KeyStoreException;
|
||||
|
||||
/** @see KeyStore#getKey(String, char[]) */
|
||||
Key getKey(String alias, char[] password)
|
||||
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException;
|
||||
|
||||
/** @see KeyStore#setEntry(String, KeyStore.Entry, KeyStore.ProtectionParameter) */
|
||||
void setEntry(String alias, KeyStore.Entry entry, KeyStore.ProtectionParameter protParam)
|
||||
throws KeyStoreException;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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 java.security.Key;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
|
||||
/**
|
||||
* Implementation of {@link KeyStoreProxy} that delegates all method calls to the {@link KeyStore}.
|
||||
*/
|
||||
public class KeyStoreProxyImpl implements KeyStoreProxy {
|
||||
|
||||
private final KeyStore mKeyStore;
|
||||
|
||||
/**
|
||||
* A new instance, delegating to {@code keyStore}.
|
||||
*/
|
||||
public KeyStoreProxyImpl(KeyStore keyStore) {
|
||||
mKeyStore = keyStore;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsAlias(String alias) throws KeyStoreException {
|
||||
return mKeyStore.containsAlias(alias);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Key getKey(String alias, char[] password)
|
||||
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
|
||||
return mKeyStore.getKey(alias, password);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setEntry(String alias, KeyStore.Entry entry, KeyStore.ProtectionParameter protParam)
|
||||
throws KeyStoreException {
|
||||
mKeyStore.setEntry(alias, entry, protParam);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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 android.app.KeyguardManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.os.Environment;
|
||||
import android.security.keystore.AndroidKeyStoreSecretKey;
|
||||
import android.security.keystore.KeyProperties;
|
||||
import android.security.keystore.KeyProtection;
|
||||
import android.util.Log;
|
||||
|
||||
import com.android.internal.annotations.VisibleForTesting;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.security.KeyStore;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.util.Locale;
|
||||
|
||||
import javax.crypto.KeyGenerator;
|
||||
import javax.crypto.SecretKey;
|
||||
import javax.security.auth.DestroyFailedException;
|
||||
|
||||
/**
|
||||
* Manages creating and checking the validity of the platform key.
|
||||
*
|
||||
* <p>The platform key is used to wrap the material of recoverable keys before persisting them to
|
||||
* disk. It is also used to decrypt the same keys on a screen unlock, before re-wrapping them with
|
||||
* a recovery key and syncing them with remote storage.
|
||||
*
|
||||
* <p>Each platform key has two entries in AndroidKeyStore:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Encrypt entry - this entry enables the root user to at any time encrypt.
|
||||
* <li>Decrypt entry - this entry enables the root user to decrypt only after recent user
|
||||
* authentication, i.e., within 15 seconds after a screen unlock.
|
||||
* </ul>
|
||||
*
|
||||
* <p>Both entries are enabled only for AES/GCM/NoPadding Cipher algorithm.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public class PlatformKeyManager {
|
||||
private static final String TAG = "PlatformKeyManager";
|
||||
|
||||
private static final String KEY_ALGORITHM = "AES";
|
||||
private static final int KEY_SIZE_BITS = 256;
|
||||
private static final String SHARED_PREFS_KEY_GENERATION_ID = "generationId";
|
||||
private static final String SHARED_PREFS_PATH = "/system/recoverablekeystore/platform_keys.xml";
|
||||
private static final String KEY_ALIAS_PREFIX =
|
||||
"com.android.server.locksettings.recoverablekeystore/platform/";
|
||||
private static final String ENCRYPT_KEY_ALIAS_SUFFIX = "encrypt";
|
||||
private static final String DECRYPT_KEY_ALIAS_SUFFIX = "decrypt";
|
||||
private static final int USER_AUTHENTICATION_VALIDITY_DURATION_SECONDS = 15;
|
||||
|
||||
private final Context mContext;
|
||||
private final KeyStoreProxy mKeyStore;
|
||||
private final SharedPreferences mSharedPreferences;
|
||||
private final int mUserId;
|
||||
|
||||
private static final String ANDROID_KEY_STORE_PROVIDER = "AndroidKeyStore";
|
||||
|
||||
/**
|
||||
* A new instance operating on behalf of {@code userId}, storing its prefs in the location
|
||||
* defined by {@code context}.
|
||||
*
|
||||
* @param context This should be the context of the RecoverableKeyStoreLoader service.
|
||||
* @param userId The ID of the user to whose lock screen the platform key must be bound.
|
||||
* @throws KeyStoreException if failed to initialize AndroidKeyStore.
|
||||
* @throws NoSuchAlgorithmException if AES is unavailable - should never happen.
|
||||
* @throws InsecureUserException if the user does not have a lock screen set.
|
||||
* @throws SecurityException if the caller does not have permission to write to /data/system.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public static PlatformKeyManager getInstance(Context context, int userId)
|
||||
throws KeyStoreException, NoSuchAlgorithmException, InsecureUserException {
|
||||
context = context.getApplicationContext();
|
||||
File sharedPreferencesFile = new File(
|
||||
Environment.getDataDirectory().getAbsoluteFile(), SHARED_PREFS_PATH);
|
||||
sharedPreferencesFile.mkdirs();
|
||||
PlatformKeyManager keyManager = new PlatformKeyManager(
|
||||
userId,
|
||||
context,
|
||||
new KeyStoreProxyImpl(getAndLoadAndroidKeyStore()),
|
||||
context.getSharedPreferences(sharedPreferencesFile, Context.MODE_PRIVATE));
|
||||
keyManager.init();
|
||||
return keyManager;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
PlatformKeyManager(
|
||||
int userId,
|
||||
Context context,
|
||||
KeyStoreProxy keyStore,
|
||||
SharedPreferences sharedPreferences) {
|
||||
mUserId = userId;
|
||||
mKeyStore = keyStore;
|
||||
mContext = context;
|
||||
mSharedPreferences = sharedPreferences;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current generation ID of the platform key. This increments whenever a platform
|
||||
* key has to be replaced. (e.g., because the user has removed and then re-added their lock
|
||||
* screen).
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public int getGenerationId() {
|
||||
return mSharedPreferences.getInt(getGenerationIdKey(), 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the platform key is available. A platform key won't be available if
|
||||
* the user has not set up a lock screen.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public boolean isAvailable() {
|
||||
return mContext.getSystemService(KeyguardManager.class).isDeviceSecure(mUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new key and increments the generation ID. Should be invoked if the platform key
|
||||
* is corrupted and needs to be rotated.
|
||||
*
|
||||
* @throws NoSuchAlgorithmException if AES is unavailable - should never happen.
|
||||
* @throws KeyStoreException if there is an error in AndroidKeyStore.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public void regenerate() throws NoSuchAlgorithmException, KeyStoreException {
|
||||
int generationId = getGenerationId();
|
||||
generateAndLoadKey(generationId + 1);
|
||||
setGenerationId(generationId + 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the platform key used for encryption.
|
||||
*
|
||||
* @throws KeyStoreException if there was an AndroidKeyStore error.
|
||||
* @throws UnrecoverableKeyException if the key could not be recovered.
|
||||
* @throws NoSuchAlgorithmException if AES is unavailable - should never occur.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public PlatformEncryptionKey getEncryptKey()
|
||||
throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException {
|
||||
int generationId = getGenerationId();
|
||||
AndroidKeyStoreSecretKey key = (AndroidKeyStoreSecretKey) mKeyStore.getKey(
|
||||
getEncryptAlias(generationId), /*password=*/ null);
|
||||
return new PlatformEncryptionKey(generationId, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the platform key used for decryption. Only works after a recent screen unlock.
|
||||
*
|
||||
* @throws KeyStoreException if there was an AndroidKeyStore error.
|
||||
* @throws UnrecoverableKeyException if the key could not be recovered.
|
||||
* @throws NoSuchAlgorithmException if AES is unavailable - should never occur.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public PlatformDecryptionKey getDecryptKey()
|
||||
throws KeyStoreException, UnrecoverableKeyException, NoSuchAlgorithmException {
|
||||
int generationId = getGenerationId();
|
||||
AndroidKeyStoreSecretKey key = (AndroidKeyStoreSecretKey) mKeyStore.getKey(
|
||||
getDecryptAlias(generationId), /*password=*/ null);
|
||||
return new PlatformDecryptionKey(generationId, key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the class. If there is no current platform key, and the user has a lock screen
|
||||
* set, will create the platform key and set the generation ID.
|
||||
*
|
||||
* @throws KeyStoreException if there was an error in AndroidKeyStore.
|
||||
* @throws NoSuchAlgorithmException if AES is unavailable - should never happen.
|
||||
*
|
||||
* @hide
|
||||
*/
|
||||
public void init() throws KeyStoreException, NoSuchAlgorithmException, InsecureUserException {
|
||||
if (!isAvailable()) {
|
||||
throw new InsecureUserException(String.format(
|
||||
Locale.US, "%d does not have a lock screen set.", mUserId));
|
||||
}
|
||||
|
||||
int generationId = getGenerationId();
|
||||
if (isKeyLoaded(generationId)) {
|
||||
Log.i(TAG, String.format(
|
||||
Locale.US, "Platform key generation %d exists already.", generationId));
|
||||
return;
|
||||
}
|
||||
if (generationId == 1) {
|
||||
Log.i(TAG, "Generating initial platform ID.");
|
||||
} else {
|
||||
Log.w(TAG, String.format(Locale.US, "Platform generation ID was %d but no "
|
||||
+ "entry was present in AndroidKeyStore. Generating fresh key.", generationId));
|
||||
}
|
||||
|
||||
generateAndLoadKey(generationId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the alias of the encryption key with the specific {@code generationId} in the
|
||||
* AndroidKeyStore.
|
||||
*
|
||||
* <p>These IDs look as follows:
|
||||
* {@code com.security.recoverablekeystore/platform/<user id>/<generation id>/encrypt}
|
||||
*
|
||||
* @param generationId The generation ID.
|
||||
* @return The alias.
|
||||
*/
|
||||
private String getEncryptAlias(int generationId) {
|
||||
return KEY_ALIAS_PREFIX + mUserId + "/" + generationId + "/" + ENCRYPT_KEY_ALIAS_SUFFIX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the alias of the decryption key with the specific {@code generationId} in the
|
||||
* AndroidKeyStore.
|
||||
*
|
||||
* <p>These IDs look as follows:
|
||||
* {@code com.security.recoverablekeystore/platform/<user id>/<generation id>/decrypt}
|
||||
*
|
||||
* @param generationId The generation ID.
|
||||
* @return The alias.
|
||||
*/
|
||||
private String getDecryptAlias(int generationId) {
|
||||
return KEY_ALIAS_PREFIX + mUserId + "/" + generationId + "/" + DECRYPT_KEY_ALIAS_SUFFIX;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the current generation ID to {@code generationId}.
|
||||
*/
|
||||
private void setGenerationId(int generationId) {
|
||||
mSharedPreferences.edit().putInt(getGenerationIdKey(), generationId).commit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current user's generation ID key in the shared preferences.
|
||||
*/
|
||||
private String getGenerationIdKey() {
|
||||
return SHARED_PREFS_KEY_GENERATION_ID + "/" + mUserId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if a key has been loaded with the given {@code generationId} into
|
||||
* AndroidKeyStore.
|
||||
*
|
||||
* @throws KeyStoreException if there was an error checking AndroidKeyStore.
|
||||
*/
|
||||
private boolean isKeyLoaded(int generationId) throws KeyStoreException {
|
||||
return mKeyStore.containsAlias(getEncryptAlias(generationId))
|
||||
&& mKeyStore.containsAlias(getDecryptAlias(generationId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new 256-bit AES key, and loads it into AndroidKeyStore with the given
|
||||
* {@code generationId} determining its aliases.
|
||||
*
|
||||
* @throws NoSuchAlgorithmException if AES is unavailable. This should never happen, as it is
|
||||
* available since API version 1.
|
||||
* @throws KeyStoreException if there was an issue loading the keys into AndroidKeyStore.
|
||||
*/
|
||||
private void generateAndLoadKey(int generationId)
|
||||
throws NoSuchAlgorithmException, KeyStoreException {
|
||||
String encryptAlias = getEncryptAlias(generationId);
|
||||
String decryptAlias = getDecryptAlias(generationId);
|
||||
SecretKey secretKey = generateAesKey();
|
||||
|
||||
mKeyStore.setEntry(
|
||||
encryptAlias,
|
||||
new KeyStore.SecretKeyEntry(secretKey),
|
||||
new KeyProtection.Builder(KeyProperties.PURPOSE_ENCRYPT)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
.build());
|
||||
mKeyStore.setEntry(
|
||||
decryptAlias,
|
||||
new KeyStore.SecretKeyEntry(secretKey),
|
||||
new KeyProtection.Builder(KeyProperties.PURPOSE_DECRYPT)
|
||||
.setUserAuthenticationRequired(true)
|
||||
.setUserAuthenticationValidityDurationSeconds(
|
||||
USER_AUTHENTICATION_VALIDITY_DURATION_SECONDS)
|
||||
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
|
||||
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
|
||||
.setBoundToSpecificSecureUserId(mUserId)
|
||||
.build());
|
||||
|
||||
try {
|
||||
secretKey.destroy();
|
||||
} catch (DestroyFailedException e) {
|
||||
Log.w(TAG, "Failed to destroy in-memory platform key.", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new 256-bit AES key, in software.
|
||||
*
|
||||
* @return The software-generated AES key.
|
||||
* @throws NoSuchAlgorithmException if AES key generation is not available. This should never
|
||||
* happen, as AES has been supported since API level 1.
|
||||
*/
|
||||
private static SecretKey generateAesKey() throws NoSuchAlgorithmException {
|
||||
KeyGenerator keyGenerator = KeyGenerator.getInstance(KEY_ALGORITHM);
|
||||
keyGenerator.init(KEY_SIZE_BITS);
|
||||
return keyGenerator.generateKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns AndroidKeyStore-provided {@link KeyStore}, having already invoked
|
||||
* {@link KeyStore#load(KeyStore.LoadStoreParameter)}.
|
||||
*
|
||||
* @throws KeyStoreException if there was a problem getting or initializing the key store.
|
||||
*/
|
||||
private static KeyStore getAndLoadAndroidKeyStore() throws KeyStoreException {
|
||||
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE_PROVIDER);
|
||||
try {
|
||||
keyStore.load(/*param=*/ null);
|
||||
} catch (CertificateException | IOException | NoSuchAlgorithmException e) {
|
||||
// Should never happen.
|
||||
throw new KeyStoreException("Unable to load keystore.", e);
|
||||
}
|
||||
return keyStore;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* Copyright (C) 2017 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 org.junit.Assert.assertArrayEquals;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyString;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import android.app.KeyguardManager;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.security.keystore.KeyProperties;
|
||||
import android.security.keystore.KeyProtection;
|
||||
import android.support.test.InstrumentationRegistry;
|
||||
import android.support.test.filters.SmallTest;
|
||||
import android.support.test.runner.AndroidJUnit4;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.Captor;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.MockitoAnnotations;
|
||||
|
||||
import java.security.KeyStore;
|
||||
import java.util.List;
|
||||
|
||||
@SmallTest
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class PlatformKeyManagerTest {
|
||||
|
||||
private static final int USER_AUTHENTICATION_VALIDITY_DURATION_SECONDS = 15;
|
||||
private static final int USER_ID_FIXTURE = 42;
|
||||
private static final String TEST_SHARED_PREFS_NAME = "PlatformKeyManagerTestPrefs";
|
||||
|
||||
@Mock private Context mContext;
|
||||
@Mock private KeyStoreProxy mKeyStoreProxy;
|
||||
@Mock private KeyguardManager mKeyguardManager;
|
||||
|
||||
@Captor private ArgumentCaptor<KeyStore.ProtectionParameter> mProtectionParameterCaptor;
|
||||
@Captor private ArgumentCaptor<KeyStore.Entry> mEntryArgumentCaptor;
|
||||
|
||||
private SharedPreferences mSharedPreferences;
|
||||
private PlatformKeyManager mPlatformKeyManager;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
MockitoAnnotations.initMocks(this);
|
||||
|
||||
Context testContext = InstrumentationRegistry.getTargetContext();
|
||||
mSharedPreferences = testContext.getSharedPreferences(
|
||||
TEST_SHARED_PREFS_NAME, Context.MODE_PRIVATE);
|
||||
mPlatformKeyManager = new PlatformKeyManager(
|
||||
USER_ID_FIXTURE, mContext, mKeyStoreProxy, mSharedPreferences);
|
||||
|
||||
when(mContext.getSystemService(anyString())).thenReturn(mKeyguardManager);
|
||||
when(mContext.getSystemServiceName(any())).thenReturn("test");
|
||||
when(mKeyguardManager.isDeviceSecure(USER_ID_FIXTURE)).thenReturn(true);
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
mSharedPreferences.edit().clear().commit();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void init_createsEncryptKeyWithCorrectAlias() throws Exception {
|
||||
mPlatformKeyManager.init();
|
||||
|
||||
verify(mKeyStoreProxy).setEntry(
|
||||
eq("com.android.server.locksettings.recoverablekeystore/platform/42/1/encrypt"),
|
||||
any(),
|
||||
any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void init_createsEncryptKeyWithCorrectPurposes() throws Exception {
|
||||
mPlatformKeyManager.init();
|
||||
|
||||
assertEquals(KeyProperties.PURPOSE_ENCRYPT, getEncryptKeyProtection().getPurposes());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void init_createsEncryptKeyWithCorrectPaddings() throws Exception {
|
||||
mPlatformKeyManager.init();
|
||||
|
||||
assertArrayEquals(
|
||||
new String[] { KeyProperties.ENCRYPTION_PADDING_NONE },
|
||||
getEncryptKeyProtection().getEncryptionPaddings());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void init_createsEncryptKeyWithCorrectBlockModes() throws Exception {
|
||||
mPlatformKeyManager.init();
|
||||
|
||||
assertArrayEquals(
|
||||
new String[] { KeyProperties.BLOCK_MODE_GCM },
|
||||
getEncryptKeyProtection().getBlockModes());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void init_createsEncryptKeyWithoutAuthenticationRequired() throws Exception {
|
||||
mPlatformKeyManager.init();
|
||||
|
||||
assertFalse(getEncryptKeyProtection().isUserAuthenticationRequired());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void init_createsDecryptKeyWithCorrectAlias() throws Exception {
|
||||
mPlatformKeyManager.init();
|
||||
|
||||
verify(mKeyStoreProxy).setEntry(
|
||||
eq("com.android.server.locksettings.recoverablekeystore/platform/42/1/decrypt"),
|
||||
any(),
|
||||
any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void init_createsDecryptKeyWithCorrectPurposes() throws Exception {
|
||||
mPlatformKeyManager.init();
|
||||
|
||||
assertEquals(KeyProperties.PURPOSE_DECRYPT, getDecryptKeyProtection().getPurposes());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void init_createsDecryptKeyWithCorrectPaddings() throws Exception {
|
||||
mPlatformKeyManager.init();
|
||||
|
||||
assertArrayEquals(
|
||||
new String[] { KeyProperties.ENCRYPTION_PADDING_NONE },
|
||||
getDecryptKeyProtection().getEncryptionPaddings());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void init_createsDecryptKeyWithCorrectBlockModes() throws Exception {
|
||||
mPlatformKeyManager.init();
|
||||
|
||||
assertArrayEquals(
|
||||
new String[] { KeyProperties.BLOCK_MODE_GCM },
|
||||
getDecryptKeyProtection().getBlockModes());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void init_createsDecryptKeyWithAuthenticationRequired() throws Exception {
|
||||
mPlatformKeyManager.init();
|
||||
|
||||
assertTrue(getDecryptKeyProtection().isUserAuthenticationRequired());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void init_createsDecryptKeyWithAuthenticationValidFor15Seconds() throws Exception {
|
||||
mPlatformKeyManager.init();
|
||||
|
||||
assertEquals(
|
||||
USER_AUTHENTICATION_VALIDITY_DURATION_SECONDS,
|
||||
getDecryptKeyProtection().getUserAuthenticationValidityDurationSeconds());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void init_createsDecryptKeyBoundToTheUsersAuthentication() throws Exception {
|
||||
mPlatformKeyManager.init();
|
||||
|
||||
assertEquals(
|
||||
USER_ID_FIXTURE,
|
||||
getDecryptKeyProtection().getBoundToSpecificSecureUserId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void init_createsBothKeysWithSameMaterial() throws Exception {
|
||||
mPlatformKeyManager.init();
|
||||
|
||||
verify(mKeyStoreProxy, times(2)).setEntry(any(), mEntryArgumentCaptor.capture(), any());
|
||||
List<KeyStore.Entry> entries = mEntryArgumentCaptor.getAllValues();
|
||||
assertArrayEquals(
|
||||
((KeyStore.SecretKeyEntry) entries.get(0)).getSecretKey().getEncoded(),
|
||||
((KeyStore.SecretKeyEntry) entries.get(1)).getSecretKey().getEncoded());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void init_setsGenerationIdTo1() throws Exception {
|
||||
mPlatformKeyManager.init();
|
||||
|
||||
assertEquals(1, mPlatformKeyManager.getGenerationId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getDecryptKey_getsDecryptKeyWithCorrectAlias() throws Exception {
|
||||
mPlatformKeyManager.getDecryptKey();
|
||||
|
||||
verify(mKeyStoreProxy).getKey(
|
||||
eq("com.android.server.locksettings.recoverablekeystore/platform/42/1/decrypt"),
|
||||
any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getEncryptKey_getsDecryptKeyWithCorrectAlias() throws Exception {
|
||||
mPlatformKeyManager.getEncryptKey();
|
||||
|
||||
verify(mKeyStoreProxy).getKey(
|
||||
eq("com.android.server.locksettings.recoverablekeystore/platform/42/1/encrypt"),
|
||||
any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void regenerate_incrementsTheGenerationId() throws Exception {
|
||||
mPlatformKeyManager.init();
|
||||
|
||||
mPlatformKeyManager.regenerate();
|
||||
|
||||
assertEquals(2, mPlatformKeyManager.getGenerationId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void regenerate_generatesANewEncryptKeyWithTheCorrectAlias() throws Exception {
|
||||
mPlatformKeyManager.init();
|
||||
|
||||
mPlatformKeyManager.regenerate();
|
||||
|
||||
verify(mKeyStoreProxy).setEntry(
|
||||
eq("com.android.server.locksettings.recoverablekeystore/platform/42/2/encrypt"),
|
||||
any(),
|
||||
any());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void regenerate_generatesANewDecryptKeyWithTheCorrectAlias() throws Exception {
|
||||
mPlatformKeyManager.init();
|
||||
|
||||
mPlatformKeyManager.regenerate();
|
||||
|
||||
verify(mKeyStoreProxy).setEntry(
|
||||
eq("com.android.server.locksettings.recoverablekeystore/platform/42/2/decrypt"),
|
||||
any(),
|
||||
any());
|
||||
}
|
||||
|
||||
private KeyProtection getEncryptKeyProtection() throws Exception {
|
||||
verify(mKeyStoreProxy).setEntry(
|
||||
eq("com.android.server.locksettings.recoverablekeystore/platform/42/1/encrypt"),
|
||||
any(),
|
||||
mProtectionParameterCaptor.capture());
|
||||
return (KeyProtection) mProtectionParameterCaptor.getValue();
|
||||
}
|
||||
|
||||
private KeyProtection getDecryptKeyProtection() throws Exception {
|
||||
verify(mKeyStoreProxy).setEntry(
|
||||
eq("com.android.server.locksettings.recoverablekeystore/platform/42/1/decrypt"),
|
||||
any(),
|
||||
mProtectionParameterCaptor.capture());
|
||||
return (KeyProtection) mProtectionParameterCaptor.getValue();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user