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