com.cloudant.sync.datastore.encryption.EndToEndEncryptionTest.java Source code

Java tutorial

Introduction

Here is the source code for com.cloudant.sync.datastore.encryption.EndToEndEncryptionTest.java

Source

/**
 * Copyright (c) 2015 IBM Cloudant. All rights reserved.
 * <p/>
 * 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
 * <p/>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p/>
 * 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.cloudant.sync.datastore.encryption;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

import com.cloudant.sync.datastore.Attachment;
import com.cloudant.sync.datastore.ConflictException;
import com.cloudant.sync.datastore.Datastore;
import com.cloudant.sync.datastore.DatastoreManager;
import com.cloudant.sync.datastore.DatastoreNotCreatedException;
import com.cloudant.sync.datastore.DocumentBodyFactory;
import com.cloudant.sync.datastore.DocumentException;
import com.cloudant.sync.datastore.DocumentRevision;
import com.cloudant.sync.datastore.UnsavedFileAttachment;
import com.cloudant.sync.datastore.UnsavedStreamAttachment;
import com.cloudant.sync.query.IndexManager;
import com.cloudant.sync.query.QueryResult;
import com.cloudant.sync.util.TestUtils;

import net.sqlcipher.database.SQLiteDatabase;

import org.apache.commons.codec.DecoderException;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.io.IOUtils;
import org.hamcrest.core.IsEqual;
import org.hamcrest.core.IsNot;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * Test that when you open a database with an encryption key, the content on disk
 * is all encrypted.
 */
@RunWith(Parameterized.class)
public class EndToEndEncryptionTest {

    public static final byte[] KEY = new byte[] { -123, 53, -22, -15, -123, 53, -22, -15, 53, -22, -15, -123, -22,
            -15, 53, -22, -123, 53, -22, -15, -123, 53, -22, -15, 53, -22, -15, -123, -22, -15, 53, -22 };

    public static final byte[] WRONG_KEY = new byte[] { -22, -15, 53, -22, -123, 53, -22, -15, -123, 53, -22, -15,
            -123, 53, -22, -15, 53, -22, -15, -123, -123, 53, -22, -15, 53, -22, -15, -123, -22, -15, 53, -22 };

    static {
        // Load SQLCipher libraries
        SQLiteDatabase.loadLibs(ProviderTestUtil.getContext());
    }

    @Parameterized.Parameters(name = "{index}: encryption={0}")
    public static Collection<Object[]> data() {
        return Arrays.asList(new Object[][] { { false }, { true }, });
    }

    @Parameterized.Parameter
    public boolean dataShouldBeEncrypted;

    String datastoreManagerDir;
    DatastoreManager datastoreManager;
    Datastore datastore;

    // Magic bytes are "SQLite format 3" + null-terminator
    byte[] sqlCipherMagicBytes = hexStringToByteArray("53514c69746520666f726d6174203300");
    byte[] expectedFirstAttachmentByte = new byte[] { 1 };

    @Before
    public void setUp() throws DatastoreNotCreatedException {
        datastoreManagerDir = TestUtils.createTempTestingDir(this.getClass().getName());
        datastoreManager = new DatastoreManager(this.datastoreManagerDir);

        if (dataShouldBeEncrypted) {
            this.datastore = this.datastoreManager.openDatastore(getClass().getSimpleName(),
                    new SimpleKeyProvider(KEY));
        } else {
            this.datastore = this.datastoreManager.openDatastore(getClass().getSimpleName());
        }
    }

    @After
    public void tearDown() {
        TestUtils.deleteTempTestingDir(datastoreManagerDir);
    }

    @Test
    public void jsonDataEncrypted() throws IOException {
        File jsonDatabase = new File(
                datastoreManagerDir + File.separator + "EndToEndEncryptionTest" + File.separator + "db.sync");

        // Database creation happens in the background, so we need to call a blocking
        // database operation to ensure the database exists on disk before we look at
        // it.

        IndexManager im = new IndexManager(this.datastore);
        im.ensureIndexed(Arrays.<Object>asList("name", "age"));

        InputStream in = new FileInputStream(jsonDatabase);
        byte[] magicBytesBuffer = new byte[sqlCipherMagicBytes.length];
        int readLength = in.read(magicBytesBuffer);

        assertEquals("Didn't read full buffer", magicBytesBuffer.length, readLength);

        if (dataShouldBeEncrypted) {
            assertThat("SQLite magic bytes found in file that should be encrypted", sqlCipherMagicBytes,
                    IsNot.not(IsEqual.equalTo(magicBytesBuffer)));
        } else {
            assertThat("SQLite magic bytes not found in file that should not be encrypted", sqlCipherMagicBytes,
                    IsEqual.equalTo(magicBytesBuffer));
        }
    }

    @Test
    public void indexDataEncrypted() throws IOException {

        IndexManager im = new IndexManager(this.datastore);
        im.ensureIndexed(Arrays.<Object>asList("name", "age"));

        File jsonDatabase = new File(
                datastoreManagerDir + File.separator + "EndToEndEncryptionTest" + File.separator + "extensions"
                        + File.separator + "com.cloudant.sync.query" + File.separator + "indexes.sqlite");

        InputStream in = new FileInputStream(jsonDatabase);
        byte[] magicBytesBuffer = new byte[sqlCipherMagicBytes.length];
        int readLength = in.read(magicBytesBuffer);

        assertEquals("Didn't read full buffer", magicBytesBuffer.length, readLength);

        if (dataShouldBeEncrypted) {
            assertThat("SQLite magic bytes found in file that should be encrypted", sqlCipherMagicBytes,
                    IsNot.not(IsEqual.equalTo(magicBytesBuffer)));
        } else {
            assertThat("SQLite magic bytes not found in file that should not be encrypted", sqlCipherMagicBytes,
                    IsEqual.equalTo(magicBytesBuffer));
        }
    }

    @Test
    public void attachmentsDataEncrypted() throws IOException, DocumentException, InvalidKeyException {

        DocumentRevision rev = new DocumentRevision();
        rev.setBody(DocumentBodyFactory.create(new HashMap<String, String>()));

        File expectedPlainText = TestUtils.loadFixture("fixture/EncryptedAttachmentTest_plainText");
        assertNotNull(expectedPlainText);

        UnsavedFileAttachment attachment = new UnsavedFileAttachment(expectedPlainText, "text/plain");
        rev.getAttachments().put("EncryptedAttachmentTest_plainText", attachment);

        datastore.createDocumentFromRevision(rev);

        File attachmentsFolder = new File(datastoreManagerDir + File.separator + "EndToEndEncryptionTest"
                + File.separator + "extensions" + File.separator + "com.cloudant.attachments");

        File[] contents = attachmentsFolder.listFiles();
        assertNotNull("Didn't find expected attachments folder", contents);
        assertThat("Didn't find expected file in attachments", contents.length, IsEqual.equalTo(1));
        InputStream in = new FileInputStream(contents[0]);

        if (dataShouldBeEncrypted) {

            byte[] actualContent = new byte[expectedFirstAttachmentByte.length];
            int readLength = in.read(actualContent);
            assertEquals("Didn't read full buffer", actualContent.length, readLength);
            assertArrayEquals("First byte not version byte", expectedFirstAttachmentByte, actualContent);

            assertTrue("Encrypted attachment did not decrypt correctly",
                    IOUtils.contentEquals(new EncryptedAttachmentInputStream(new FileInputStream(contents[0]), KEY),
                            new FileInputStream(expectedPlainText)));

        } else {
            assertTrue("Unencrypted attachment did not read correctly",
                    IOUtils.contentEquals(new FileInputStream(expectedPlainText), in));
        }
    }

    /**
     * Opening a datastore should fail in both the encrypted and unencrypted case, as
     * supplying the wrong key will result in attempting to decrypt using that key,
     * which should fail in both cases.
     */
    @Test(expected = DatastoreNotCreatedException.class)
    public void testCannotOpenDatabaseWithWrongKey() throws DatastoreNotCreatedException {

        // First close the datastore, as otherwise DatastoreManager's uniquing just
        // gives us back the existing instance which has the correct key.
        datastore.close();

        this.datastoreManager.openDatastore(getClass().getSimpleName(), new SimpleKeyProvider(WRONG_KEY));
    }

    /**
     * A basic check things round trip successfully.
     */
    @Test
    public void readAndWriteDocument() throws DocumentException, IOException {

        String documentId = "a-test-document";
        final String nonAsciiText = ";:xx\uD83D\uDC79?\uD83D\uDC7D";

        HashMap<String, String> documentBody = new HashMap<String, String>();
        documentBody.put("name", "mike");
        documentBody.put("pet", "cat");
        documentBody.put("non-ascii", nonAsciiText);

        // Create
        DocumentRevision rev = new DocumentRevision(documentId);
        rev.setBody(DocumentBodyFactory.create(documentBody));
        DocumentRevision saved = datastore.createDocumentFromRevision(rev);
        assertNotNull(saved);

        // Read
        DocumentRevision retrieved = datastore.getDocument(documentId);
        assertNotNull(retrieved);
        Map<String, Object> retrievedBody = retrieved.getBody().asMap();
        assertEquals("mike", retrievedBody.get("name"));
        assertEquals("cat", retrievedBody.get("pet"));
        assertEquals(nonAsciiText, retrievedBody.get("non-ascii"));
        assertEquals(3, retrievedBody.size());

        // Update
        DocumentRevision update = retrieved;
        Map<String, Object> updateBody = retrieved.getBody().asMap();
        updateBody.put("name", "fred");
        update.setBody(DocumentBodyFactory.create(updateBody));
        DocumentRevision updated = datastore.updateDocumentFromRevision(update);
        assertNotNull(updated);
        Map<String, Object> updatedBody = updated.getBody().asMap();
        assertEquals("fred", updatedBody.get("name"));
        assertEquals("cat", updatedBody.get("pet"));
        assertEquals(nonAsciiText, updatedBody.get("non-ascii"));
        assertEquals(3, updatedBody.size());

        // Update with attachments, one from file, one a non-ascii string test
        final String attachmentName = "EncryptedAttachmentTest_plainText";
        File expectedPlainText = TestUtils.loadFixture("fixture/EncryptedAttachmentTest_plainText");
        assertNotNull(expectedPlainText);
        DocumentRevision attachmentRevision = updated;
        final Map<String, Attachment> atts = attachmentRevision.getAttachments();
        atts.put(attachmentName, new UnsavedFileAttachment(expectedPlainText, "text/plain"));
        atts.put("non-ascii", new UnsavedStreamAttachment(new ByteArrayInputStream(nonAsciiText.getBytes()),
                "non-ascii", "text/plain"));
        DocumentRevision updatedWithAttachment = datastore.updateDocumentFromRevision(attachmentRevision);
        InputStream in = updatedWithAttachment.getAttachments().get(attachmentName).getInputStream();
        assertTrue("Saved attachment did not read correctly",
                IOUtils.contentEquals(new FileInputStream(expectedPlainText), in));
        in = updatedWithAttachment.getAttachments().get("non-ascii").getInputStream();
        assertTrue("Saved attachment did not read correctly",
                IOUtils.contentEquals(new ByteArrayInputStream(nonAsciiText.getBytes()), in));

        // perform a query to ensure we can use special chars
        IndexManager indexManager = new IndexManager(datastore);
        assertNotNull(indexManager.ensureIndexed(Arrays.<Object>asList("name", "pet"), "my index"));
        // query for the name fred and check that docs are returned.
        Map<String, Object> selector = new HashMap<String, Object>();
        selector.put("name", "fred");
        QueryResult queryResult = indexManager.find(selector);
        assertNotNull(queryResult);
        // Delete
        try {
            datastore.deleteDocumentFromRevision(saved);
            fail("Deleting document from old revision succeeded");
        } catch (ConflictException ex) {
            // Expected exception
        }
        DocumentRevision deleted = datastore.deleteDocumentFromRevision(updatedWithAttachment);
        assertNotNull(deleted);
        assertEquals(true, deleted.isDeleted());
    }

    public static byte[] hexStringToByteArray(String s) {
        try {
            return Hex.decodeHex(s.toCharArray());
        } catch (DecoderException ex) {
            // Crash the tests at this point, we've input bad data in our hard-coded values
            throw new RuntimeException("Error decoding hex data: " + s);
        }
    }

}