Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit bf257cbe authored by Al Sutton's avatar Al Sutton
Browse files

Import TertiaryKeyStore

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

Bug: 111386661
Test: make RunBackupEncryptionRoboTests
Change-Id: I4718ddde737c19836e415af6ca1fd597e79a0a4a
parent d046fb26
Loading
Loading
Loading
Loading
+202 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+213 −0
Original line number Diff line number Diff line
/*
 * 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());
    }
}