1 /* 2 * Copyright (C) 2019 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.server.backup.encryption.kv; 18 19 import static com.google.common.truth.Truth.assertThat; 20 21 import static org.mockito.ArgumentMatchers.any; 22 import static org.mockito.Mockito.when; 23 import static org.testng.Assert.assertThrows; 24 25 import android.os.Debug; 26 import android.platform.test.annotations.Presubmit; 27 28 import com.android.server.backup.encryption.chunk.ChunkHash; 29 import com.android.server.backup.encryption.chunking.ChunkHasher; 30 import com.android.server.backup.encryption.protos.nano.KeyValuePairProto; 31 32 import org.junit.Before; 33 import org.junit.Test; 34 import org.junit.runner.RunWith; 35 import org.mockito.Mock; 36 import org.mockito.MockitoAnnotations; 37 import org.robolectric.RobolectricTestRunner; 38 39 import java.security.MessageDigest; 40 import java.util.ArrayList; 41 import java.util.Arrays; 42 import java.util.Collections; 43 import java.util.Comparator; 44 import java.util.List; 45 import java.util.stream.Stream; 46 47 @RunWith(RobolectricTestRunner.class) 48 @Presubmit 49 public class DecryptedChunkKvOutputTest { 50 private static final String TEST_KEY_1 = "key_1"; 51 private static final String TEST_KEY_2 = "key_2"; 52 private static final byte[] TEST_VALUE_1 = {1, 2, 3}; 53 private static final byte[] TEST_VALUE_2 = {10, 11, 12, 13}; 54 private static final byte[] TEST_PAIR_1 = toByteArray(createPair(TEST_KEY_1, TEST_VALUE_1)); 55 private static final byte[] TEST_PAIR_2 = toByteArray(createPair(TEST_KEY_2, TEST_VALUE_2)); 56 private static final int TEST_BUFFER_SIZE = Math.max(TEST_PAIR_1.length, TEST_PAIR_2.length); 57 58 @Mock private ChunkHasher mChunkHasher; 59 private DecryptedChunkKvOutput mOutput; 60 61 @Before setUp()62 public void setUp() throws Exception { 63 MockitoAnnotations.initMocks(this); 64 65 when(mChunkHasher.computeHash(any())) 66 .thenAnswer(invocation -> fakeHash(invocation.getArgument(0))); 67 mOutput = new DecryptedChunkKvOutput(mChunkHasher); 68 } 69 70 @Test open_returnsInstance()71 public void open_returnsInstance() throws Exception { 72 assertThat(mOutput.open()).isEqualTo(mOutput); 73 } 74 75 @Test processChunk_alreadyClosed_throws()76 public void processChunk_alreadyClosed_throws() throws Exception { 77 mOutput.open(); 78 mOutput.close(); 79 80 assertThrows( 81 IllegalStateException.class, 82 () -> mOutput.processChunk(TEST_PAIR_1, TEST_PAIR_1.length)); 83 } 84 85 @Test getDigest_beforeClose_throws()86 public void getDigest_beforeClose_throws() throws Exception { 87 // TODO: b/141356823 We should add a test which calls .open() here 88 assertThrows(IllegalStateException.class, () -> mOutput.getDigest()); 89 } 90 91 @Test getDigest_returnsDigestOfSortedHashes()92 public void getDigest_returnsDigestOfSortedHashes() throws Exception { 93 mOutput.open(); 94 Debug.waitForDebugger(); 95 mOutput.processChunk(Arrays.copyOf(TEST_PAIR_1, TEST_BUFFER_SIZE), TEST_PAIR_1.length); 96 mOutput.processChunk(Arrays.copyOf(TEST_PAIR_2, TEST_BUFFER_SIZE), TEST_PAIR_2.length); 97 mOutput.close(); 98 99 byte[] actualDigest = mOutput.getDigest(); 100 101 MessageDigest digest = MessageDigest.getInstance(DecryptedChunkKvOutput.DIGEST_ALGORITHM); 102 Stream.of(TEST_PAIR_1, TEST_PAIR_2) 103 .map(DecryptedChunkKvOutputTest::fakeHash) 104 .sorted(Comparator.naturalOrder()) 105 .forEachOrdered(hash -> digest.update(hash.getHash())); 106 assertThat(actualDigest).isEqualTo(digest.digest()); 107 } 108 109 @Test getPairs_beforeClose_throws()110 public void getPairs_beforeClose_throws() throws Exception { 111 // TODO: b/141356823 We should add a test which calls .open() here 112 assertThrows(IllegalStateException.class, () -> mOutput.getPairs()); 113 } 114 115 @Test getPairs_returnsPairsSortedByKey()116 public void getPairs_returnsPairsSortedByKey() throws Exception { 117 mOutput.open(); 118 // Write out of order to check that it sorts the chunks. 119 mOutput.processChunk(Arrays.copyOf(TEST_PAIR_2, TEST_BUFFER_SIZE), TEST_PAIR_2.length); 120 mOutput.processChunk(Arrays.copyOf(TEST_PAIR_1, TEST_BUFFER_SIZE), TEST_PAIR_1.length); 121 mOutput.close(); 122 123 List<KeyValuePairProto.KeyValuePair> pairs = mOutput.getPairs(); 124 125 assertThat( 126 isInOrder( 127 pairs, 128 Comparator.comparing( 129 (KeyValuePairProto.KeyValuePair pair) -> pair.key))) 130 .isTrue(); 131 assertThat(pairs).hasSize(2); 132 assertThat(pairs.get(0).key).isEqualTo(TEST_KEY_1); 133 assertThat(pairs.get(0).value).isEqualTo(TEST_VALUE_1); 134 assertThat(pairs.get(1).key).isEqualTo(TEST_KEY_2); 135 assertThat(pairs.get(1).value).isEqualTo(TEST_VALUE_2); 136 } 137 createPair(String key, byte[] value)138 private static KeyValuePairProto.KeyValuePair createPair(String key, byte[] value) { 139 KeyValuePairProto.KeyValuePair pair = new KeyValuePairProto.KeyValuePair(); 140 pair.key = key; 141 pair.value = value; 142 return pair; 143 } 144 isInOrder( List<KeyValuePairProto.KeyValuePair> list, Comparator<KeyValuePairProto.KeyValuePair> comparator)145 private boolean isInOrder( 146 List<KeyValuePairProto.KeyValuePair> list, 147 Comparator<KeyValuePairProto.KeyValuePair> comparator) { 148 if (list.size() < 2) { 149 return true; 150 } 151 152 List<KeyValuePairProto.KeyValuePair> sortedList = new ArrayList<>(list); 153 Collections.sort(sortedList, comparator); 154 return list.equals(sortedList); 155 } 156 toByteArray(KeyValuePairProto.KeyValuePair nano)157 private static byte[] toByteArray(KeyValuePairProto.KeyValuePair nano) { 158 return KeyValuePairProto.KeyValuePair.toByteArray(nano); 159 } 160 fakeHash(byte[] data)161 private static ChunkHash fakeHash(byte[] data) { 162 return new ChunkHash(Arrays.copyOf(data, ChunkHash.HASH_LENGTH_BYTES)); 163 } 164 } 165