Vault example documents provider.

Example provider that encrypts both metadata and contents of
documents stored inside.  It shows advanced usage of new storage
access APIs and hardware-backed key chain.

Change-Id: I2cdf4e949be8471c3d8b4f45ec0681c9248ea09c
This commit is contained in:
Jeff Sharkey
2013-10-31 11:25:31 -07:00
parent 4e5bae3461
commit 93de411538
12 changed files with 1588 additions and 0 deletions

View File

@@ -0,0 +1,251 @@
/*
* Copyright (C) 2013 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.example.android.vault;
import android.os.ParcelFileDescriptor;
import android.test.AndroidTestCase;
import android.test.MoreAsserts;
import android.test.suitebuilder.annotation.MediumTest;
import org.json.JSONObject;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.charset.StandardCharsets;
import java.security.DigestException;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
/**
* Tests for {@link EncryptedDocument}.
*/
@MediumTest
public class EncryptedDocumentTest extends AndroidTestCase {
private File mFile;
private SecretKey mDataKey = new SecretKeySpec(new byte[] {
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 }, "AES");
private SecretKey mMacKey = new SecretKeySpec(new byte[] {
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02 }, "AES");
@Override
protected void setUp() throws Exception {
super.setUp();
mFile = new File(getContext().getFilesDir(), "meow");
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
for (File f : getContext().getFilesDir().listFiles()) {
f.delete();
}
}
public void testEmptyFile() throws Exception {
mFile.createNewFile();
final EncryptedDocument doc = new EncryptedDocument(4, mFile, mDataKey, mMacKey);
try {
doc.readMetadata();
fail("expected metadata to throw");
} catch (IOException expected) {
}
try {
final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createReliablePipe();
doc.readContent(pipe[1]);
fail("expected content to throw");
} catch (IOException expected) {
}
}
public void testNormalMetadataAndContents() throws Exception {
final byte[] content = "KITTENS".getBytes(StandardCharsets.UTF_8);
testMetadataAndContents(content);
}
public void testGiantMetadataAndContents() throws Exception {
// try with content size of prime number >1MB
final byte[] content = new byte[1298047];
Arrays.fill(content, (byte) 0x42);
testMetadataAndContents(content);
}
private void testMetadataAndContents(byte[] content) throws Exception {
final EncryptedDocument doc = new EncryptedDocument(4, mFile, mDataKey, mMacKey);
final byte[] beforeContent = content;
final ParcelFileDescriptor[] beforePipe = ParcelFileDescriptor.createReliablePipe();
new Thread() {
@Override
public void run() {
final FileOutputStream os = new FileOutputStream(beforePipe[1].getFileDescriptor());
try {
os.write(beforeContent);
beforePipe[1].close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}.start();
// fully write metadata and content
final JSONObject before = new JSONObject();
before.put("meow", "cake");
doc.writeMetadataAndContent(before, beforePipe[0]);
// now go back and verify we can read
final JSONObject after = doc.readMetadata();
assertEquals("cake", after.getString("meow"));
final CountDownLatch latch = new CountDownLatch(1);
final ParcelFileDescriptor[] afterPipe = ParcelFileDescriptor.createReliablePipe();
final byte[] afterContent = new byte[beforeContent.length];
new Thread() {
@Override
public void run() {
final FileInputStream is = new FileInputStream(afterPipe[0].getFileDescriptor());
try {
int i = 0;
while (i < afterContent.length) {
int n = is.read(afterContent, i, afterContent.length - i);
i += n;
}
afterPipe[0].close();
latch.countDown();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}.start();
doc.readContent(afterPipe[1]);
latch.await(5, TimeUnit.SECONDS);
MoreAsserts.assertEquals(beforeContent, afterContent);
}
public void testNormalMetadataOnly() throws Exception {
final EncryptedDocument doc = new EncryptedDocument(4, mFile, mDataKey, mMacKey);
// write only metadata
final JSONObject before = new JSONObject();
before.put("lol", "wut");
doc.writeMetadataAndContent(before, null);
// verify we can read
final JSONObject after = doc.readMetadata();
assertEquals("wut", after.getString("lol"));
final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createReliablePipe();
try {
doc.readContent(pipe[1]);
fail("found document content");
} catch (IOException expected) {
}
}
public void testCopiedFile() throws Exception {
final EncryptedDocument doc1 = new EncryptedDocument(1, mFile, mDataKey, mMacKey);
final EncryptedDocument doc4 = new EncryptedDocument(4, mFile, mDataKey, mMacKey);
// write values for doc1 into file
final JSONObject meta1 = new JSONObject();
meta1.put("key1", "value1");
doc1.writeMetadataAndContent(meta1, null);
// now try reading as doc4, which should fail
try {
doc4.readMetadata();
fail("somehow read without checking docid");
} catch (DigestException expected) {
}
}
public void testBitTwiddle() throws Exception {
final EncryptedDocument doc = new EncryptedDocument(4, mFile, mDataKey, mMacKey);
// write some metadata
final JSONObject before = new JSONObject();
before.put("twiddle", "twiddle");
doc.writeMetadataAndContent(before, null);
final RandomAccessFile f = new RandomAccessFile(mFile, "rw");
f.seek(f.length() - 4);
f.write(0x00);
f.close();
try {
doc.readMetadata();
fail("somehow passed hmac");
} catch (DigestException expected) {
}
}
public void testErrorAbortsWrite() throws Exception {
final EncryptedDocument doc = new EncryptedDocument(4, mFile, mDataKey, mMacKey);
// write initial metadata
final JSONObject init = new JSONObject();
init.put("color", "red");
doc.writeMetadataAndContent(init, null);
// try writing with a pipe that reports failure
final byte[] content = "KITTENS".getBytes(StandardCharsets.UTF_8);
final ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createReliablePipe();
new Thread() {
@Override
public void run() {
final FileOutputStream os = new FileOutputStream(pipe[1].getFileDescriptor());
try {
os.write(content);
pipe[1].closeWithError("ZOMG");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}.start();
final JSONObject second = new JSONObject();
second.put("color", "blue");
try {
doc.writeMetadataAndContent(second, pipe[0]);
fail("somehow wrote without error");
} catch (IOException ignored) {
}
// verify that original metadata still in place
final JSONObject after = doc.readMetadata();
assertEquals("red", after.getString("color"));
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright (C) 2013 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.example.android.vault;
import static com.example.android.vault.VaultProvider.AUTHORITY;
import static com.example.android.vault.VaultProvider.DEFAULT_DOCUMENT_ID;
import android.content.ContentProviderClient;
import android.database.Cursor;
import android.provider.DocumentsContract.Document;
import android.test.AndroidTestCase;
import java.util.HashSet;
/**
* Tests for {@link VaultProvider}.
*/
public class VaultProviderTest extends AndroidTestCase {
private static final String MIME_TYPE_DEFAULT = "text/plain";
private ContentProviderClient mClient;
private VaultProvider mProvider;
@Override
protected void setUp() throws Exception {
super.setUp();
mClient = getContext().getContentResolver().acquireContentProviderClient(AUTHORITY);
mProvider = (VaultProvider) mClient.getLocalContentProvider();
mProvider.wipeAllContents();
}
@Override
protected void tearDown() throws Exception {
super.tearDown();
mClient.release();
}
public void testDeleteDirectory() throws Exception {
Cursor c;
final String file = mProvider.createDocument(
DEFAULT_DOCUMENT_ID, MIME_TYPE_DEFAULT, "file");
final String dir = mProvider.createDocument(
DEFAULT_DOCUMENT_ID, Document.MIME_TYPE_DIR, "dir");
final String dirfile = mProvider.createDocument(
dir, MIME_TYPE_DEFAULT, "dirfile");
final String dirdir = mProvider.createDocument(
dir, Document.MIME_TYPE_DIR, "dirdir");
final String dirdirfile = mProvider.createDocument(
dirdir, MIME_TYPE_DEFAULT, "dirdirfile");
// verify everything is in place
c = mProvider.queryChildDocuments(DEFAULT_DOCUMENT_ID, null, null);
assertContains(c, "file", "dir");
c = mProvider.queryChildDocuments(dir, null, null);
assertContains(c, "dirfile", "dirdir");
// should remove children and parent ref
mProvider.deleteDocument(dir);
c = mProvider.queryChildDocuments(DEFAULT_DOCUMENT_ID, null, null);
assertContains(c, "file");
mProvider.queryDocument(file, null);
try { mProvider.queryDocument(dir, null); } catch (Exception expected) { }
try { mProvider.queryDocument(dirfile, null); } catch (Exception expected) { }
try { mProvider.queryDocument(dirdir, null); } catch (Exception expected) { }
try { mProvider.queryDocument(dirdirfile, null); } catch (Exception expected) { }
}
private static void assertContains(Cursor c, String... docs) {
final HashSet<String> set = new HashSet<String>();
while (c.moveToNext()) {
set.add(c.getString(c.getColumnIndex(Document.COLUMN_DISPLAY_NAME)));
}
for (String doc : docs) {
assertTrue(doc, set.contains(doc));
}
}
}