diff options
Diffstat (limited to 'packages/BackupEncryption')
2 files changed, 415 insertions, 0 deletions
diff --git a/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyStore.java b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyStore.java new file mode 100644 index 000000000000..01444bf0cd00 --- /dev/null +++ b/packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyStore.java @@ -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; + } +} diff --git a/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyStoreTest.java b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyStoreTest.java new file mode 100644 index 000000000000..ccc5f32dad36 --- /dev/null +++ b/packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyStoreTest.java @@ -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()); + } +} |