Import TertiaryKeyStore

Bring the TertiaryKeyStore class and its test class into the main repo.

Bug: 111386661
Test: make RunBackupEncryptionRoboTests
Change-Id: I4718ddde737c19836e415af6ca1fd597e79a0a4a
This commit is contained in:
Al Sutton
2019-08-23 13:41:16 +01:00
parent d046fb26f4
commit bf257cbe23
2 changed files with 415 additions and 0 deletions

View File

@@ -0,0 +1,202 @@
/*
* Copyright (C) 2019 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.backup.encryption.keys;
import static com.android.internal.util.Preconditions.checkArgument;
import android.content.Context;
import android.util.ArrayMap;
import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
import com.android.server.backup.encryption.storage.BackupEncryptionDb;
import com.android.server.backup.encryption.storage.TertiaryKey;
import com.android.server.backup.encryption.storage.TertiaryKeysTable;
import com.google.protobuf.nano.CodedOutputByteBufferNano;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.Optional;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
/**
* Stores backup package keys. Each application package has its own {@link SecretKey}, which is used
* to encrypt the backup data. These keys are then wrapped by a master backup key, and stored in
* their wrapped form on disk and on the backup server.
*
* <p>For now this code only implements writing to disk. Once the backup server is ready, it will be
* extended to sync the keys there, also.
*/
public class TertiaryKeyStore {
private final RecoverableKeyStoreSecondaryKey mSecondaryKey;
private final BackupEncryptionDb mDatabase;
/**
* Creates an instance, using {@code secondaryKey} to wrap tertiary keys, and storing them in
* the database.
*/
public static TertiaryKeyStore newInstance(
Context context, RecoverableKeyStoreSecondaryKey secondaryKey) {
return new TertiaryKeyStore(secondaryKey, BackupEncryptionDb.newInstance(context));
}
private TertiaryKeyStore(
RecoverableKeyStoreSecondaryKey secondaryKey, BackupEncryptionDb database) {
mSecondaryKey = secondaryKey;
mDatabase = database;
}
/**
* Saves the given key.
*
* @param applicationName The package name of the application for which this key will be used to
* encrypt data. e.g., "com.example.app".
* @param key The key.
* @throws InvalidKeyException if the backup key is not capable of wrapping.
* @throws IOException if there is an issue writing to the database.
*/
public void save(String applicationName, SecretKey key)
throws IOException, InvalidKeyException, IllegalBlockSizeException,
NoSuchPaddingException, NoSuchAlgorithmException {
checkApplicationName(applicationName);
byte[] keyBytes = getEncodedKey(KeyWrapUtils.wrap(mSecondaryKey.getSecretKey(), key));
long pk;
try {
pk =
mDatabase
.getTertiaryKeysTable()
.addKey(
new TertiaryKey(
mSecondaryKey.getAlias(), applicationName, keyBytes));
} finally {
mDatabase.close();
}
if (pk == -1) {
throw new IOException("Failed to commit to db");
}
}
/**
* Tries to load a key for the given application.
*
* @param applicationName The package name of the application, e.g. "com.example.app".
* @return The key if it is exists, {@link Optional#empty()} ()} otherwise.
* @throws InvalidKeyException if the backup key is not good for unwrapping.
* @throws IOException if there is a problem loading the key from the database.
*/
public Optional<SecretKey> load(String applicationName)
throws IOException, InvalidKeyException, InvalidAlgorithmParameterException,
NoSuchAlgorithmException, NoSuchPaddingException {
checkApplicationName(applicationName);
Optional<TertiaryKey> keyFromDb;
try {
keyFromDb =
mDatabase
.getTertiaryKeysTable()
.getKey(mSecondaryKey.getAlias(), applicationName);
} finally {
mDatabase.close();
}
if (!keyFromDb.isPresent()) {
return Optional.empty();
}
WrappedKeyProto.WrappedKey wrappedKey =
WrappedKeyProto.WrappedKey.parseFrom(keyFromDb.get().getWrappedKeyBytes());
return Optional.of(KeyWrapUtils.unwrap(mSecondaryKey.getSecretKey(), wrappedKey));
}
/**
* Loads keys for all applications.
*
* @return All of the keys in a map keyed by package name.
* @throws IOException if there is an issue loading from the database.
* @throws InvalidKeyException if the backup key is not an appropriate key for unwrapping.
*/
public Map<String, SecretKey> getAll()
throws IOException, InvalidKeyException, InvalidAlgorithmParameterException,
NoSuchAlgorithmException, NoSuchPaddingException {
Map<String, TertiaryKey> tertiaries;
try {
tertiaries = mDatabase.getTertiaryKeysTable().getAllKeys(mSecondaryKey.getAlias());
} finally {
mDatabase.close();
}
Map<String, SecretKey> unwrappedKeys = new ArrayMap<>();
for (String applicationName : tertiaries.keySet()) {
WrappedKeyProto.WrappedKey wrappedKey =
WrappedKeyProto.WrappedKey.parseFrom(
tertiaries.get(applicationName).getWrappedKeyBytes());
unwrappedKeys.put(
applicationName, KeyWrapUtils.unwrap(mSecondaryKey.getSecretKey(), wrappedKey));
}
return unwrappedKeys;
}
/**
* Adds all wrapped keys to the database.
*
* @throws IOException if an error occurred adding a wrapped key.
*/
public void putAll(Map<String, WrappedKeyProto.WrappedKey> wrappedKeysByApplicationName)
throws IOException {
TertiaryKeysTable tertiaryKeysTable = mDatabase.getTertiaryKeysTable();
try {
for (String applicationName : wrappedKeysByApplicationName.keySet()) {
byte[] keyBytes = getEncodedKey(wrappedKeysByApplicationName.get(applicationName));
long primaryKey =
tertiaryKeysTable.addKey(
new TertiaryKey(
mSecondaryKey.getAlias(), applicationName, keyBytes));
if (primaryKey == -1) {
throw new IOException("Failed to commit to db");
}
}
} finally {
mDatabase.close();
}
}
private static void checkApplicationName(String applicationName) {
checkArgument(!applicationName.isEmpty(), "applicationName must not be empty string.");
checkArgument(!applicationName.contains("/"), "applicationName must not contain slash.");
}
private byte[] getEncodedKey(WrappedKeyProto.WrappedKey key) throws IOException {
byte[] buffer = new byte[key.getSerializedSize()];
CodedOutputByteBufferNano out = CodedOutputByteBufferNano.newInstance(buffer);
key.writeTo(out);
return buffer;
}
}

View File

@@ -0,0 +1,213 @@
/*
* Copyright (C) 2019 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.backup.encryption.keys;
import static com.android.server.backup.testing.CryptoTestUtils.generateAesKey;
import static com.google.common.truth.Truth.assertThat;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertThrows;
import static org.testng.Assert.assertTrue;
import android.content.Context;
import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import java.security.InvalidKeyException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import javax.crypto.SecretKey;
/** Tests for the tertiary key store */
@RunWith(RobolectricTestRunner.class)
public class TertiaryKeyStoreTest {
private static final String SECONDARY_KEY_ALIAS = "Robbo/Ranx";
private Context mApplication;
private TertiaryKeyStore mTertiaryKeyStore;
private SecretKey mSecretKey;
/** Initialise the keystore for testing */
@Before
public void setUp() throws Exception {
mApplication = RuntimeEnvironment.application;
mSecretKey = generateAesKey();
mTertiaryKeyStore =
TertiaryKeyStore.newInstance(
mApplication,
new RecoverableKeyStoreSecondaryKey(SECONDARY_KEY_ALIAS, mSecretKey));
}
/** Test a reound trip for a key */
@Test
public void load_loadsAKeyThatWasSaved() throws Exception {
String packageName = "com.android.example";
SecretKey packageKey = generateAesKey();
mTertiaryKeyStore.save(packageName, packageKey);
Optional<SecretKey> maybeLoadedKey = mTertiaryKeyStore.load(packageName);
assertTrue(maybeLoadedKey.isPresent());
assertEquals(packageKey, maybeLoadedKey.get());
}
/** Test isolation between packages */
@Test
public void load_doesNotLoadAKeyForAnotherSecondary() throws Exception {
String packageName = "com.android.example";
SecretKey packageKey = generateAesKey();
mTertiaryKeyStore.save(packageName, packageKey);
TertiaryKeyStore managerWithOtherSecondaryKey =
TertiaryKeyStore.newInstance(
mApplication,
new RecoverableKeyStoreSecondaryKey(
"myNewSecondaryKeyAlias", generateAesKey()));
assertFalse(managerWithOtherSecondaryKey.load(packageName).isPresent());
}
/** Test non-existent key handling */
@Test
public void load_returnsAbsentForANonExistentKey() throws Exception {
assertFalse(mTertiaryKeyStore.load("mystery.package").isPresent());
}
/** Test handling incorrect keys */
@Test
public void load_throwsIfHasWrongBackupKey() throws Exception {
String packageName = "com.android.example";
SecretKey packageKey = generateAesKey();
mTertiaryKeyStore.save(packageName, packageKey);
TertiaryKeyStore managerWithBadKey =
TertiaryKeyStore.newInstance(
mApplication,
new RecoverableKeyStoreSecondaryKey(SECONDARY_KEY_ALIAS, generateAesKey()));
assertThrows(InvalidKeyException.class, () -> managerWithBadKey.load(packageName));
}
/** Test handling of empty app name */
@Test
public void load_throwsForEmptyApplicationName() throws Exception {
assertThrows(IllegalArgumentException.class, () -> mTertiaryKeyStore.load(""));
}
/** Test handling of an invalid app name */
@Test
public void load_throwsForBadApplicationName() throws Exception {
assertThrows(
IllegalArgumentException.class,
() -> mTertiaryKeyStore.load("com/android/example"));
}
/** Test key replacement */
@Test
public void save_overwritesPreviousKey() throws Exception {
String packageName = "com.android.example";
SecretKey oldKey = generateAesKey();
mTertiaryKeyStore.save(packageName, oldKey);
SecretKey newKey = generateAesKey();
mTertiaryKeyStore.save(packageName, newKey);
Optional<SecretKey> maybeLoadedKey = mTertiaryKeyStore.load(packageName);
assertTrue(maybeLoadedKey.isPresent());
SecretKey loadedKey = maybeLoadedKey.get();
assertThat(loadedKey).isNotEqualTo(oldKey);
assertThat(loadedKey).isEqualTo(newKey);
}
/** Test saving with an empty application name fails */
@Test
public void save_throwsForEmptyApplicationName() throws Exception {
assertThrows(
IllegalArgumentException.class, () -> mTertiaryKeyStore.save("", generateAesKey()));
}
/** Test saving an invalid application name fails */
@Test
public void save_throwsForBadApplicationName() throws Exception {
assertThrows(
IllegalArgumentException.class,
() -> mTertiaryKeyStore.save("com/android/example", generateAesKey()));
}
/** Test handling an empty database */
@Test
public void getAll_returnsEmptyMapForEmptyDb() throws Exception {
assertThat(mTertiaryKeyStore.getAll()).isEmpty();
}
/** Test loading all available keys works as expected */
@Test
public void getAll_returnsAllKeysSaved() throws Exception {
String package1 = "com.android.example";
SecretKey key1 = generateAesKey();
String package2 = "com.anndroid.example1";
SecretKey key2 = generateAesKey();
String package3 = "com.android.example2";
SecretKey key3 = generateAesKey();
mTertiaryKeyStore.save(package1, key1);
mTertiaryKeyStore.save(package2, key2);
mTertiaryKeyStore.save(package3, key3);
Map<String, SecretKey> keys = mTertiaryKeyStore.getAll();
assertThat(keys).containsExactly(package1, key1, package2, key2, package3, key3);
}
/** Test cross-secondary isolation */
@Test
public void getAll_doesNotReturnKeysForOtherSecondary() throws Exception {
String packageName = "com.android.example";
TertiaryKeyStore managerWithOtherSecondaryKey =
TertiaryKeyStore.newInstance(
mApplication,
new RecoverableKeyStoreSecondaryKey(
"myNewSecondaryKeyAlias", generateAesKey()));
managerWithOtherSecondaryKey.save(packageName, generateAesKey());
assertThat(mTertiaryKeyStore.getAll()).isEmpty();
}
/** Test mass put into the keystore */
@Test
public void putAll_putsAllWrappedKeysInTheStore() throws Exception {
String packageName = "com.android.example";
SecretKey key = generateAesKey();
WrappedKeyProto.WrappedKey wrappedKey = KeyWrapUtils.wrap(mSecretKey, key);
Map<String, WrappedKeyProto.WrappedKey> testElements = new HashMap<>();
testElements.put(packageName, wrappedKey);
mTertiaryKeyStore.putAll(testElements);
assertThat(mTertiaryKeyStore.getAll()).containsKey(packageName);
assertThat(mTertiaryKeyStore.getAll().get(packageName).getEncoded())
.isEqualTo(key.getEncoded());
}
}