1 /* 2 * Copyright (C) 2013 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.example.android.vault; 18 19 import android.os.ParcelFileDescriptor; 20 import android.test.AndroidTestCase; 21 import android.test.MoreAsserts; 22 23 import androidx.test.filters.MediumTest; 24 25 import org.json.JSONObject; 26 27 import java.io.File; 28 import java.io.FileInputStream; 29 import java.io.FileOutputStream; 30 import java.io.IOException; 31 import java.io.RandomAccessFile; 32 import java.nio.charset.StandardCharsets; 33 import java.security.DigestException; 34 import java.util.Arrays; 35 import java.util.concurrent.CountDownLatch; 36 import java.util.concurrent.TimeUnit; 37 38 import javax.crypto.SecretKey; 39 import javax.crypto.spec.SecretKeySpec; 40 41 /** 42 * Tests for {@link EncryptedDocument}. 43 */ 44 @MediumTest 45 public class EncryptedDocumentTest extends AndroidTestCase { 46 47 private File mFile; 48 49 private SecretKey mDataKey = new SecretKeySpec(new byte[] { 50 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 51 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 }, "AES"); 52 53 private SecretKey mMacKey = new SecretKeySpec(new byte[] { 54 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 55 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 56 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 57 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02 }, "AES"); 58 59 @Override setUp()60 protected void setUp() throws Exception { 61 super.setUp(); 62 63 mFile = new File(getContext().getFilesDir(), "meow"); 64 } 65 66 @Override tearDown()67 protected void tearDown() throws Exception { 68 super.tearDown(); 69 70 for (File f : getContext().getFilesDir().listFiles()) { 71 f.delete(); 72 } 73 } 74 testEmptyFile()75 public void testEmptyFile() throws Exception { 76 mFile.createNewFile(); 77 final EncryptedDocument doc = new EncryptedDocument(4, mFile, mDataKey, mMacKey); 78 79 try { 80 doc.readMetadata(); 81 fail("expected metadata to throw"); 82 } catch (IOException expected) { 83 } 84 85 try { 86 final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createReliablePipe(); 87 doc.readContent(pipe[1]); 88 fail("expected content to throw"); 89 } catch (IOException expected) { 90 } 91 } 92 testNormalMetadataAndContents()93 public void testNormalMetadataAndContents() throws Exception { 94 final byte[] content = "KITTENS".getBytes(StandardCharsets.UTF_8); 95 testMetadataAndContents(content); 96 } 97 testGiantMetadataAndContents()98 public void testGiantMetadataAndContents() throws Exception { 99 // try with content size of prime number >1MB 100 final byte[] content = new byte[1298047]; 101 Arrays.fill(content, (byte) 0x42); 102 testMetadataAndContents(content); 103 } 104 testMetadataAndContents(byte[] content)105 private void testMetadataAndContents(byte[] content) throws Exception { 106 final EncryptedDocument doc = new EncryptedDocument(4, mFile, mDataKey, mMacKey); 107 final byte[] beforeContent = content; 108 109 final ParcelFileDescriptor[] beforePipe = ParcelFileDescriptor.createReliablePipe(); 110 new Thread() { 111 @Override 112 public void run() { 113 final FileOutputStream os = new FileOutputStream(beforePipe[1].getFileDescriptor()); 114 try { 115 os.write(beforeContent); 116 beforePipe[1].close(); 117 } catch (IOException e) { 118 throw new RuntimeException(e); 119 } 120 } 121 }.start(); 122 123 // fully write metadata and content 124 final JSONObject before = new JSONObject(); 125 before.put("meow", "cake"); 126 doc.writeMetadataAndContent(before, beforePipe[0]); 127 128 // now go back and verify we can read 129 final JSONObject after = doc.readMetadata(); 130 assertEquals("cake", after.getString("meow")); 131 132 final CountDownLatch latch = new CountDownLatch(1); 133 final ParcelFileDescriptor[] afterPipe = ParcelFileDescriptor.createReliablePipe(); 134 final byte[] afterContent = new byte[beforeContent.length]; 135 new Thread() { 136 @Override 137 public void run() { 138 final FileInputStream is = new FileInputStream(afterPipe[0].getFileDescriptor()); 139 try { 140 int i = 0; 141 while (i < afterContent.length) { 142 int n = is.read(afterContent, i, afterContent.length - i); 143 i += n; 144 } 145 afterPipe[0].close(); 146 latch.countDown(); 147 } catch (IOException e) { 148 throw new RuntimeException(e); 149 } 150 } 151 }.start(); 152 153 doc.readContent(afterPipe[1]); 154 latch.await(5, TimeUnit.SECONDS); 155 156 MoreAsserts.assertEquals(beforeContent, afterContent); 157 } 158 testNormalMetadataOnly()159 public void testNormalMetadataOnly() throws Exception { 160 final EncryptedDocument doc = new EncryptedDocument(4, mFile, mDataKey, mMacKey); 161 162 // write only metadata 163 final JSONObject before = new JSONObject(); 164 before.put("lol", "wut"); 165 doc.writeMetadataAndContent(before, null); 166 167 // verify we can read 168 final JSONObject after = doc.readMetadata(); 169 assertEquals("wut", after.getString("lol")); 170 171 final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createReliablePipe(); 172 try { 173 doc.readContent(pipe[1]); 174 fail("found document content"); 175 } catch (IOException expected) { 176 } 177 } 178 testCopiedFile()179 public void testCopiedFile() throws Exception { 180 final EncryptedDocument doc1 = new EncryptedDocument(1, mFile, mDataKey, mMacKey); 181 final EncryptedDocument doc4 = new EncryptedDocument(4, mFile, mDataKey, mMacKey); 182 183 // write values for doc1 into file 184 final JSONObject meta1 = new JSONObject(); 185 meta1.put("key1", "value1"); 186 doc1.writeMetadataAndContent(meta1, null); 187 188 // now try reading as doc4, which should fail 189 try { 190 doc4.readMetadata(); 191 fail("somehow read without checking docid"); 192 } catch (DigestException expected) { 193 } 194 } 195 testBitTwiddle()196 public void testBitTwiddle() throws Exception { 197 final EncryptedDocument doc = new EncryptedDocument(4, mFile, mDataKey, mMacKey); 198 199 // write some metadata 200 final JSONObject before = new JSONObject(); 201 before.put("twiddle", "twiddle"); 202 doc.writeMetadataAndContent(before, null); 203 204 final RandomAccessFile f = new RandomAccessFile(mFile, "rw"); 205 f.seek(f.length() - 4); 206 f.write(0x00); 207 f.close(); 208 209 try { 210 doc.readMetadata(); 211 fail("somehow passed hmac"); 212 } catch (DigestException expected) { 213 } 214 } 215 testErrorAbortsWrite()216 public void testErrorAbortsWrite() throws Exception { 217 final EncryptedDocument doc = new EncryptedDocument(4, mFile, mDataKey, mMacKey); 218 219 // write initial metadata 220 final JSONObject init = new JSONObject(); 221 init.put("color", "red"); 222 doc.writeMetadataAndContent(init, null); 223 224 // try writing with a pipe that reports failure 225 final byte[] content = "KITTENS".getBytes(StandardCharsets.UTF_8); 226 final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createReliablePipe(); 227 new Thread() { 228 @Override 229 public void run() { 230 final FileOutputStream os = new FileOutputStream(pipe[1].getFileDescriptor()); 231 try { 232 os.write(content); 233 pipe[1].closeWithError("ZOMG"); 234 } catch (IOException e) { 235 throw new RuntimeException(e); 236 } 237 } 238 }.start(); 239 240 final JSONObject second = new JSONObject(); 241 second.put("color", "blue"); 242 try { 243 doc.writeMetadataAndContent(second, pipe[0]); 244 fail("somehow wrote without error"); 245 } catch (IOException ignored) { 246 } 247 248 // verify that original metadata still in place 249 final JSONObject after = doc.readMetadata(); 250 assertEquals("red", after.getString("color")); 251 } 252 } 253