All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.com.android.server.backup.encryption.tasks.EncryptedKvBackupTaskTest Maven / Gradle / Ivy

/*
 * 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.tasks;

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

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertThrows;

import android.app.Application;
import android.util.Pair;

import androidx.test.core.app.ApplicationProvider;

import com.android.server.backup.encryption.chunk.ChunkHash;
import com.android.server.backup.encryption.chunking.ProtoStore;
import com.android.server.backup.encryption.keys.RecoverableKeyStoreSecondaryKey;
import com.android.server.backup.encryption.keys.TertiaryKeyManager;
import com.android.server.backup.encryption.kv.KeyValueListingBuilder;
import com.android.server.backup.encryption.protos.nano.ChunksMetadataProto;
import com.android.server.backup.encryption.protos.nano.KeyValueListingProto;
import com.android.server.backup.encryption.protos.nano.WrappedKeyProto;
import com.android.server.backup.testing.CryptoTestUtils;

import java.io.IOException;
import java.util.Arrays;
import java.util.Map;
import java.util.Map.Entry;

import javax.crypto.SecretKey;

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.Captor;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.robolectric.RobolectricTestRunner;

@RunWith(RobolectricTestRunner.class)
public class EncryptedKvBackupTaskTest {
    private static final boolean INCREMENTAL = true;
    private static final boolean NON_INCREMENTAL = false;

    private static final String TEST_PACKAGE_1 = "com.example.app1";
    private static final String TEST_KEY_1 = "key_1";
    private static final String TEST_KEY_2 = "key_2";
    private static final ChunkHash TEST_HASH_1 =
            new ChunkHash(Arrays.copyOf(new byte[] {1}, ChunkHash.HASH_LENGTH_BYTES));
    private static final ChunkHash TEST_HASH_2 =
            new ChunkHash(Arrays.copyOf(new byte[] {2}, ChunkHash.HASH_LENGTH_BYTES));
    private static final int TEST_LENGTH_1 = 200;
    private static final int TEST_LENGTH_2 = 300;

    @Rule public TemporaryFolder mTemporaryFolder = new TemporaryFolder();

    @Captor private ArgumentCaptor mChunkListingCaptor;

    @Mock private TertiaryKeyManager mTertiaryKeyManager;
    @Mock private RecoverableKeyStoreSecondaryKey mSecondaryKey;
    @Mock private ProtoStore mKeyValueListingStore;
    @Mock private ProtoStore mChunkListingStore;
    @Mock private KvBackupEncrypter mKvBackupEncrypter;
    @Mock private EncryptedBackupTask mEncryptedBackupTask;
    @Mock private SecretKey mTertiaryKey;

    private WrappedKeyProto.WrappedKey mWrappedTertiaryKey;
    private KeyValueListingProto.KeyValueListing mNewKeyValueListing;
    private ChunksMetadataProto.ChunkListing mNewChunkListing;
    private EncryptedKvBackupTask mTask;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);

        Application application = ApplicationProvider.getApplicationContext();
        mKeyValueListingStore = ProtoStore.createKeyValueListingStore(application);
        mChunkListingStore = ProtoStore.createChunkListingStore(application);

        mWrappedTertiaryKey = new WrappedKeyProto.WrappedKey();

        when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(false);
        when(mTertiaryKeyManager.getKey()).thenReturn(mTertiaryKey);
        when(mTertiaryKeyManager.getWrappedKey()).thenReturn(mWrappedTertiaryKey);

        mNewKeyValueListing =
                createKeyValueListing(
                        CryptoTestUtils.mapOf(
                                new Pair<>(TEST_KEY_1, TEST_HASH_1),
                                new Pair<>(TEST_KEY_2, TEST_HASH_2)));
        mNewChunkListing =
                createChunkListing(
                        CryptoTestUtils.mapOf(
                                new Pair<>(TEST_HASH_1, TEST_LENGTH_1),
                                new Pair<>(TEST_HASH_2, TEST_LENGTH_2)));
        when(mKvBackupEncrypter.getNewKeyValueListing()).thenReturn(mNewKeyValueListing);
        when(mEncryptedBackupTask.performIncrementalBackup(
                        eq(mTertiaryKey), eq(mWrappedTertiaryKey), any()))
                .thenReturn(mNewChunkListing);
        when(mEncryptedBackupTask.performNonIncrementalBackup(
                        eq(mTertiaryKey), eq(mWrappedTertiaryKey), any()))
                .thenReturn(mNewChunkListing);

        mTask =
                new EncryptedKvBackupTask(
                        mTertiaryKeyManager,
                        mKeyValueListingStore,
                        mSecondaryKey,
                        mChunkListingStore,
                        mKvBackupEncrypter,
                        mEncryptedBackupTask,
                        TEST_PACKAGE_1);
    }

    @Test
    public void testPerformBackup_rotationRequired_deletesListings() throws Exception {
        mKeyValueListingStore.saveProto(
                TEST_PACKAGE_1,
                createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
        mChunkListingStore.saveProto(
                TEST_PACKAGE_1,
                createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));

        when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(true);
        // Throw an IOException so it aborts before saving the new listings.
        when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
                .thenThrow(IOException.class);

        assertThrows(IOException.class, () -> mTask.performBackup(NON_INCREMENTAL));

        assertFalse(mKeyValueListingStore.loadProto(TEST_PACKAGE_1).isPresent());
        assertFalse(mChunkListingStore.loadProto(TEST_PACKAGE_1).isPresent());
    }

    @Test
    public void testPerformBackup_rotationRequiredButIncremental_throws() throws Exception {
        mKeyValueListingStore.saveProto(
                TEST_PACKAGE_1,
                createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
        mChunkListingStore.saveProto(
                TEST_PACKAGE_1,
                createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));

        when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(true);

        assertThrows(NonIncrementalBackupRequiredException.class,
                () -> mTask.performBackup(INCREMENTAL));
    }

    @Test
    public void testPerformBackup_rotationRequiredAndNonIncremental_performsNonIncrementalBackup()
            throws Exception {
        mKeyValueListingStore.saveProto(
                TEST_PACKAGE_1,
                createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
        mChunkListingStore.saveProto(
                TEST_PACKAGE_1,
                createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));

        when(mTertiaryKeyManager.wasKeyRotated()).thenReturn(true);

        mTask.performBackup(NON_INCREMENTAL);

        verify(mEncryptedBackupTask)
                .performNonIncrementalBackup(eq(mTertiaryKey), eq(mWrappedTertiaryKey), any());
    }

    @Test
    public void testPerformBackup_existingStateButNonIncremental_deletesListings() throws Exception {
        mKeyValueListingStore.saveProto(
                TEST_PACKAGE_1,
                createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
        mChunkListingStore.saveProto(
                TEST_PACKAGE_1,
                createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));

        // Throw an IOException so it aborts before saving the new listings.
        when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
                .thenThrow(IOException.class);

        assertThrows(IOException.class, () -> mTask.performBackup(NON_INCREMENTAL));

        assertFalse(mKeyValueListingStore.loadProto(TEST_PACKAGE_1).isPresent());
        assertFalse(mChunkListingStore.loadProto(TEST_PACKAGE_1).isPresent());
    }

    @Test
    public void testPerformBackup_keyValueListingMissing_deletesChunkListingAndPerformsNonIncremental()
            throws Exception {
        mChunkListingStore.saveProto(
                TEST_PACKAGE_1,
                createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));

        // Throw an IOException so it aborts before saving the new listings.
        when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
                .thenThrow(IOException.class);

        assertThrows(IOException.class, () -> mTask.performBackup(NON_INCREMENTAL));

        verify(mEncryptedBackupTask).performNonIncrementalBackup(any(), any(), any());
        assertFalse(mKeyValueListingStore.loadProto(TEST_PACKAGE_1).isPresent());
        assertFalse(mChunkListingStore.loadProto(TEST_PACKAGE_1).isPresent());
    }

    @Test
    public void testPerformBackup_chunkListingMissing_deletesKeyValueListingAndPerformsNonIncremental()
            throws Exception {
        mKeyValueListingStore.saveProto(
                TEST_PACKAGE_1,
                createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));

        // Throw an IOException so it aborts before saving the new listings.
        when(mEncryptedBackupTask.performNonIncrementalBackup(any(), any(), any()))
                .thenThrow(IOException.class);

        assertThrows(IOException.class, () -> mTask.performBackup(NON_INCREMENTAL));

        verify(mEncryptedBackupTask).performNonIncrementalBackup(any(), any(), any());
        assertFalse(mKeyValueListingStore.loadProto(TEST_PACKAGE_1).isPresent());
        assertFalse(mChunkListingStore.loadProto(TEST_PACKAGE_1).isPresent());
    }

    @Test
    public void testPerformBackup_existingStateAndIncremental_performsIncrementalBackup()
            throws Exception {
        mKeyValueListingStore.saveProto(
                TEST_PACKAGE_1,
                createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
        ChunksMetadataProto.ChunkListing oldChunkListing =
                createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1)));
        mChunkListingStore.saveProto(TEST_PACKAGE_1, oldChunkListing);

        mTask.performBackup(INCREMENTAL);

        verify(mEncryptedBackupTask)
                .performIncrementalBackup(
                        eq(mTertiaryKey), eq(mWrappedTertiaryKey), mChunkListingCaptor.capture());
        assertChunkListingsEqual(mChunkListingCaptor.getValue(), oldChunkListing);
    }

    @Test
    public void testPerformBackup_noExistingStateAndNonIncremental_performsNonIncrementalBackup()
            throws Exception {
        mTask.performBackup(NON_INCREMENTAL);

        verify(mEncryptedBackupTask)
                .performNonIncrementalBackup(eq(mTertiaryKey), eq(mWrappedTertiaryKey), eq(null));
    }

    @Test
    public void testPerformBackup_incremental_savesNewListings() throws Exception {
        mKeyValueListingStore.saveProto(
                TEST_PACKAGE_1,
                createKeyValueListing(CryptoTestUtils.mapOf(new Pair<>(TEST_KEY_1, TEST_HASH_1))));
        mChunkListingStore.saveProto(
                TEST_PACKAGE_1,
                createChunkListing(CryptoTestUtils.mapOf(new Pair<>(TEST_HASH_1, TEST_LENGTH_1))));

        mTask.performBackup(INCREMENTAL);

        KeyValueListingProto.KeyValueListing actualKeyValueListing =
                mKeyValueListingStore.loadProto(TEST_PACKAGE_1).get();
        ChunksMetadataProto.ChunkListing actualChunkListing =
                mChunkListingStore.loadProto(TEST_PACKAGE_1).get();
        assertKeyValueListingsEqual(actualKeyValueListing, mNewKeyValueListing);
        assertChunkListingsEqual(actualChunkListing, mNewChunkListing);
    }

    @Test
    public void testPerformBackup_nonIncremental_savesNewListings() throws Exception {
        mTask.performBackup(NON_INCREMENTAL);

        KeyValueListingProto.KeyValueListing actualKeyValueListing =
                mKeyValueListingStore.loadProto(TEST_PACKAGE_1).get();
        ChunksMetadataProto.ChunkListing actualChunkListing =
                mChunkListingStore.loadProto(TEST_PACKAGE_1).get();
        assertKeyValueListingsEqual(actualKeyValueListing, mNewKeyValueListing);
        assertChunkListingsEqual(actualChunkListing, mNewChunkListing);
    }

    private static KeyValueListingProto.KeyValueListing createKeyValueListing(
            Map pairs) {
        return new KeyValueListingBuilder().addAll(pairs).build();
    }

    private static ChunksMetadataProto.ChunkListing createChunkListing(
            Map chunks) {
        ChunksMetadataProto.Chunk[] listingChunks = new ChunksMetadataProto.Chunk[chunks.size()];
        int chunksAdded = 0;
        for (Entry entry : chunks.entrySet()) {
            listingChunks[chunksAdded] = CryptoTestUtils.newChunk(entry.getKey(), entry.getValue());
            chunksAdded++;
        }
        return CryptoTestUtils.newChunkListingWithoutDocId(
                /* fingerprintSalt */ new byte[0],
                ChunksMetadataProto.AES_256_GCM,
                ChunksMetadataProto.CHUNK_ORDERING_TYPE_UNSPECIFIED,
                listingChunks);
    }

    private static void assertKeyValueListingsEqual(
            KeyValueListingProto.KeyValueListing actual,
            KeyValueListingProto.KeyValueListing expected) {
        KeyValueListingProto.KeyValueEntry[] actualEntries = actual.entries;
        KeyValueListingProto.KeyValueEntry[] expectedEntries = expected.entries;
        assertThat(actualEntries.length).isEqualTo(expectedEntries.length);
        for (int i = 0; i < actualEntries.length; i++) {
            assertWithMessage("entry " + i)
                    .that(actualEntries[i].key)
                    .isEqualTo(expectedEntries[i].key);
            assertWithMessage("entry " + i)
                    .that(actualEntries[i].hash)
                    .isEqualTo(expectedEntries[i].hash);
        }
    }

    private static void assertChunkListingsEqual(
            ChunksMetadataProto.ChunkListing actual, ChunksMetadataProto.ChunkListing expected) {
        ChunksMetadataProto.Chunk[] actualChunks = actual.chunks;
        ChunksMetadataProto.Chunk[] expectedChunks = expected.chunks;
        assertThat(actualChunks.length).isEqualTo(expectedChunks.length);
        for (int i = 0; i < actualChunks.length; i++) {
            assertWithMessage("chunk " + i)
                    .that(actualChunks[i].hash)
                    .isEqualTo(expectedChunks[i].hash);
            assertWithMessage("chunk " + i)
                    .that(actualChunks[i].length)
                    .isEqualTo(expectedChunks[i].length);
        }
        assertThat(actual.cipherType).isEqualTo(expected.cipherType);
        assertThat(actual.documentId)
                .isEqualTo(expected.documentId == null ? "" : expected.documentId);
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy