Merge "Import TertiaryKeyStore"
This commit is contained in:
committed by
Android (Google) Code Review
commit
dddb368954
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user