summaryrefslogtreecommitdiff
path: root/packages/BackupEncryption
diff options
context:
space:
mode:
authorAl Sutton <alsutton@google.com>2019-08-23 13:41:16 +0100
committerAl Sutton <alsutton@google.com>2019-09-12 13:41:06 +0100
commitbf257cbe234e8d646db3f3aef79b5e9a3ce7a432 (patch)
treeeb7e111145fab229eb92b30800a9ea3266361c4a /packages/BackupEncryption
parentd046fb26f4874bdd4ddf9b1e44b79da03d030424 (diff)
Import TertiaryKeyStore
Bring the TertiaryKeyStore class and its test class into the main repo. Bug: 111386661 Test: make RunBackupEncryptionRoboTests Change-Id: I4718ddde737c19836e415af6ca1fd597e79a0a4a
Diffstat (limited to 'packages/BackupEncryption')
-rw-r--r--packages/BackupEncryption/src/com/android/server/backup/encryption/keys/TertiaryKeyStore.java202
-rw-r--r--packages/BackupEncryption/test/robolectric/src/com/android/server/backup/encryption/keys/TertiaryKeyStoreTest.java213
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());
+ }
+}