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

Commit 7d54d6aa authored by Al Sutton's avatar Al Sutton
Browse files

Import ProtoStore

Bug: 111386661
Test: make RunBackupEncryptionRoboTests
Change-Id: I9cbaf2c1f1e933b08ac578e4243e8555e552ef1d
parent 295aaad6
Loading
Loading
Loading
Loading
+174 −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.chunking;

import static com.android.internal.util.Preconditions.checkNotNull;

import android.content.Context;
import android.text.TextUtils;
import android.util.AtomicFile;
import android.util.Slog;

import com.android.internal.annotations.VisibleForTesting;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;

import com.google.protobuf.nano.MessageNano;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Optional;

/**
 * Stores a nano proto for each package, persisting the proto to disk.
 *
 * <p>This is used to store {@link ChunksMetadataProto.ChunkListing}.
 *
 * @param <T> the type of nano proto to store.
 */
public class ProtoStore<T extends MessageNano> {
    private static final String CHUNK_LISTING_FOLDER = "backup_chunk_listings";
    private static final String KEY_VALUE_LISTING_FOLDER = "backup_kv_listings";

    private static final String TAG = "BupEncProtoStore";

    private final File mStoreFolder;
    private final Class<T> mClazz;

    /** Creates a new instance which stores chunk listings at the default location. */
    public static ProtoStore<ChunksMetadataProto.ChunkListing> createChunkListingStore(
            Context context) throws IOException {
        return new ProtoStore<>(
                ChunksMetadataProto.ChunkListing.class,
                new File(context.getFilesDir().getAbsoluteFile(), CHUNK_LISTING_FOLDER));
    }

    /** Creates a new instance which stores key value listings in the default location. */
    public static ProtoStore<KeyValueListingProto.KeyValueListing> createKeyValueListingStore(
            Context context) throws IOException {
        return new ProtoStore<>(
                KeyValueListingProto.KeyValueListing.class,
                new File(context.getFilesDir().getAbsoluteFile(), KEY_VALUE_LISTING_FOLDER));
    }

    /**
     * Creates a new instance which stores protos in the given folder.
     *
     * @param storeFolder The location where the serialized form is stored.
     */
    @VisibleForTesting
    ProtoStore(Class<T> clazz, File storeFolder) throws IOException {
        mClazz = checkNotNull(clazz);
        mStoreFolder = ensureDirectoryExistsOrThrow(storeFolder);
    }

    private static File ensureDirectoryExistsOrThrow(File directory) throws IOException {
        if (directory.exists() && !directory.isDirectory()) {
            throw new IOException("Store folder already exists, but isn't a directory.");
        }

        if (!directory.exists() && !directory.mkdir()) {
            throw new IOException("Unable to create store folder.");
        }

        return directory;
    }

    /**
     * Returns the chunk listing for the given package, or {@link Optional#empty()} if no listing
     * exists.
     */
    public Optional<T> loadProto(String packageName)
            throws IOException, IllegalAccessException, InstantiationException,
            NoSuchMethodException, InvocationTargetException {
        File file = getFileForPackage(packageName);

        if (!file.exists()) {
            Slog.d(
                    TAG,
                    "No chunk listing existed for " + packageName + ", returning empty listing.");
            return Optional.empty();
        }

        AtomicFile protoStore = new AtomicFile(file);
        byte[] data = protoStore.readFully();

        Constructor<T> constructor = mClazz.getDeclaredConstructor();
        T proto = constructor.newInstance();
        MessageNano.mergeFrom(proto, data);
        return Optional.of(proto);
    }

    /** Saves a proto to disk, associating it with the given package. */
    public void saveProto(String packageName, T proto) throws IOException {
        checkNotNull(proto);
        File file = getFileForPackage(packageName);

        try (FileOutputStream os = new FileOutputStream(file)) {
            os.write(MessageNano.toByteArray(proto));
        } catch (IOException e) {
            Slog.e(
                    TAG,
                    "Exception occurred when saving the listing for "
                            + packageName
                            + ", deleting saved listing.",
                    e);

            // If a problem occurred when writing the listing then it might be corrupt, so delete
            // it.
            file.delete();

            throw e;
        }
    }

    /** Deletes the proto for the given package, or does nothing if the package has no proto. */
    public void deleteProto(String packageName) {
        File file = getFileForPackage(packageName);
        file.delete();
    }

    /** Deletes every proto of this type, for all package names. */
    public void deleteAllProtos() {
        File[] files = mStoreFolder.listFiles();

        // We ensure that the storeFolder exists in the constructor, but check just in case it has
        // mysteriously disappeared.
        if (files == null) {
            return;
        }

        for (File file : files) {
            file.delete();
        }
    }

    private File getFileForPackage(String packageName) {
        checkPackageName(packageName);
        return new File(mStoreFolder, packageName);
    }

    private static void checkPackageName(String packageName) {
        if (TextUtils.isEmpty(packageName) || packageName.contains("/")) {
            throw new IllegalArgumentException(
                    "Package name must not contain '/' or be empty: " + packageName);
        }
    }
}
+264 −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.chunking;

import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;

import static org.testng.Assert.assertThrows;

import android.content.Context;
import android.platform.test.annotations.Presubmit;

import androidx.test.core.app.ApplicationProvider;

import com.android.server.backup.encryption.chunk.ChunkHash;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;

import com.google.common.collect.ImmutableMap;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.robolectric.RobolectricTestRunner;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;

@RunWith(RobolectricTestRunner.class)
@Presubmit
public class ProtoStoreTest {
    private static final String TEST_KEY_1 = "test_key_1";
    private static final ChunkHash TEST_HASH_1 =
            new ChunkHash(Arrays.copyOf(new byte[] {1}, EncryptedChunk.KEY_LENGTH_BYTES));
    private static final ChunkHash TEST_HASH_2 =
            new ChunkHash(Arrays.copyOf(new byte[] {2}, EncryptedChunk.KEY_LENGTH_BYTES));
    private static final int TEST_LENGTH_1 = 10;
    private static final int TEST_LENGTH_2 = 18;

    private static final String TEST_PACKAGE_1 = "com.example.test1";
    private static final String TEST_PACKAGE_2 = "com.example.test2";

    @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();

    private File mStoreFolder;
    private ProtoStore<ChunksMetadataProto.ChunkListing> mProtoStore;

    @Before
    public void setUp() throws Exception {
        mStoreFolder = mTemporaryFolder.newFolder();
        mProtoStore = new ProtoStore<>(ChunksMetadataProto.ChunkListing.class, mStoreFolder);
    }

    @Test
    public void differentStoreTypes_operateSimultaneouslyWithoutInterfering() throws Exception {
        ChunksMetadataProto.ChunkListing chunkListing =
                createChunkListing(ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1));
        KeyValueListingProto.KeyValueListing keyValueListing =
                new KeyValueListingProto.KeyValueListing();
        keyValueListing.entries = new KeyValueListingProto.KeyValueEntry[1];
        keyValueListing.entries[0] = new KeyValueListingProto.KeyValueEntry();
        keyValueListing.entries[0].key = TEST_KEY_1;
        keyValueListing.entries[0].hash = TEST_HASH_1.getHash();

        Context application = ApplicationProvider.getApplicationContext();
        ProtoStore<ChunksMetadataProto.ChunkListing> chunkListingStore =
                ProtoStore.createChunkListingStore(application);
        ProtoStore<KeyValueListingProto.KeyValueListing> keyValueListingStore =
                ProtoStore.createKeyValueListingStore(application);

        chunkListingStore.saveProto(TEST_PACKAGE_1, chunkListing);
        keyValueListingStore.saveProto(TEST_PACKAGE_1, keyValueListing);

        ChunksMetadataProto.ChunkListing actualChunkListing =
                chunkListingStore.loadProto(TEST_PACKAGE_1).get();
        KeyValueListingProto.KeyValueListing actualKeyValueListing =
                keyValueListingStore.loadProto(TEST_PACKAGE_1).get();
        assertListingsEqual(actualChunkListing, chunkListing);
        assertThat(actualKeyValueListing.entries.length).isEqualTo(1);
        assertThat(actualKeyValueListing.entries[0].key).isEqualTo(TEST_KEY_1);
        assertThat(actualKeyValueListing.entries[0].hash).isEqualTo(TEST_HASH_1.getHash());
    }

    @Test
    public void construct_storeLocationIsFile_throws() throws Exception {
        assertThrows(
                IOException.class,
                () ->
                        new ProtoStore<>(
                                ChunksMetadataProto.ChunkListing.class,
                                mTemporaryFolder.newFile()));
    }

    @Test
    public void loadChunkListing_noListingExists_returnsEmptyListing() throws Exception {
        Optional<ChunksMetadataProto.ChunkListing> chunkListing =
                mProtoStore.loadProto(TEST_PACKAGE_1);
        assertThat(chunkListing.isPresent()).isFalse();
    }

    @Test
    public void loadChunkListing_listingExists_returnsExistingListing() throws Exception {
        ChunksMetadataProto.ChunkListing expected =
                createChunkListing(
                        ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1, TEST_HASH_2, TEST_LENGTH_2));
        mProtoStore.saveProto(TEST_PACKAGE_1, expected);

        ChunksMetadataProto.ChunkListing result = mProtoStore.loadProto(TEST_PACKAGE_1).get();

        assertListingsEqual(result, expected);
    }

    @Test
    public void loadProto_emptyPackageName_throwsException() throws Exception {
        assertThrows(IllegalArgumentException.class, () -> mProtoStore.loadProto(""));
    }

    @Test
    public void loadProto_nullPackageName_throwsException() throws Exception {
        assertThrows(IllegalArgumentException.class, () -> mProtoStore.loadProto(null));
    }

    @Test
    public void loadProto_packageNameContainsSlash_throwsException() throws Exception {
        assertThrows(
                IllegalArgumentException.class, () -> mProtoStore.loadProto(TEST_PACKAGE_1 + "/"));
    }

    @Test
    public void saveProto_persistsToNewInstance() throws Exception {
        ChunksMetadataProto.ChunkListing expected =
                createChunkListing(
                        ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1, TEST_HASH_2, TEST_LENGTH_2));
        mProtoStore.saveProto(TEST_PACKAGE_1, expected);
        mProtoStore = new ProtoStore<>(ChunksMetadataProto.ChunkListing.class, mStoreFolder);

        ChunksMetadataProto.ChunkListing result = mProtoStore.loadProto(TEST_PACKAGE_1).get();

        assertListingsEqual(result, expected);
    }

    @Test
    public void saveProto_emptyPackageName_throwsException() throws Exception {
        assertThrows(
                IllegalArgumentException.class,
                () -> mProtoStore.saveProto("", new ChunksMetadataProto.ChunkListing()));
    }

    @Test
    public void saveProto_nullPackageName_throwsException() throws Exception {
        assertThrows(
                IllegalArgumentException.class,
                () -> mProtoStore.saveProto(null, new ChunksMetadataProto.ChunkListing()));
    }

    @Test
    public void saveProto_packageNameContainsSlash_throwsException() throws Exception {
        assertThrows(
                IllegalArgumentException.class,
                () ->
                        mProtoStore.saveProto(
                                TEST_PACKAGE_1 + "/", new ChunksMetadataProto.ChunkListing()));
    }

    @Test
    public void saveProto_nullListing_throwsException() throws Exception {
        assertThrows(NullPointerException.class, () -> mProtoStore.saveProto(TEST_PACKAGE_1, null));
    }

    @Test
    public void deleteProto_noListingExists_doesNothing() throws Exception {
        ChunksMetadataProto.ChunkListing listing =
                createChunkListing(ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1));
        mProtoStore.saveProto(TEST_PACKAGE_1, listing);

        mProtoStore.deleteProto(TEST_PACKAGE_2);

        assertThat(mProtoStore.loadProto(TEST_PACKAGE_1).get().chunks.length).isEqualTo(1);
    }

    @Test
    public void deleteProto_listingExists_deletesListing() throws Exception {
        ChunksMetadataProto.ChunkListing listing =
                createChunkListing(ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1));
        mProtoStore.saveProto(TEST_PACKAGE_1, listing);

        mProtoStore.deleteProto(TEST_PACKAGE_1);

        assertThat(mProtoStore.loadProto(TEST_PACKAGE_1).isPresent()).isFalse();
    }

    @Test
    public void deleteAllProtos_deletesAllProtos() throws Exception {
        ChunksMetadataProto.ChunkListing listing1 =
                createChunkListing(ImmutableMap.of(TEST_HASH_1, TEST_LENGTH_1));
        ChunksMetadataProto.ChunkListing listing2 =
                createChunkListing(ImmutableMap.of(TEST_HASH_2, TEST_LENGTH_2));
        mProtoStore.saveProto(TEST_PACKAGE_1, listing1);
        mProtoStore.saveProto(TEST_PACKAGE_2, listing2);

        mProtoStore.deleteAllProtos();

        assertThat(mProtoStore.loadProto(TEST_PACKAGE_1).isPresent()).isFalse();
        assertThat(mProtoStore.loadProto(TEST_PACKAGE_2).isPresent()).isFalse();
    }

    @Test
    public void deleteAllProtos_folderDeleted_doesNotCrash() throws Exception {
        mStoreFolder.delete();

        mProtoStore.deleteAllProtos();
    }

    private static ChunksMetadataProto.ChunkListing createChunkListing(
            ImmutableMap<ChunkHash, Integer> chunks) {
        ChunksMetadataProto.ChunkListing listing = new ChunksMetadataProto.ChunkListing();
        listing.cipherType = ChunksMetadataProto.AES_256_GCM;
        listing.chunkOrderingType = ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED;

        List<ChunksMetadataProto.Chunk> chunkProtos = new ArrayList<>();
        for (Entry<ChunkHash, Integer> entry : chunks.entrySet()) {
            ChunksMetadataProto.Chunk chunk = new ChunksMetadataProto.Chunk();
            chunk.hash = entry.getKey().getHash();
            chunk.length = entry.getValue();
            chunkProtos.add(chunk);
        }
        listing.chunks = chunkProtos.toArray(new ChunksMetadataProto.Chunk[0]);
        return listing;
    }

    private void assertListingsEqual(
            ChunksMetadataProto.ChunkListing result, ChunksMetadataProto.ChunkListing expected) {
        assertThat(result.chunks.length).isEqualTo(expected.chunks.length);
        for (int i = 0; i < result.chunks.length; i++) {
            assertWithMessage("Chunk " + i)
                    .that(result.chunks[i].length)
                    .isEqualTo(expected.chunks[i].length);
            assertWithMessage("Chunk " + i)
                    .that(result.chunks[i].hash)
                    .isEqualTo(expected.chunks[i].hash);
        }
    }
}