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:
@@ -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"));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user