/** * Created by Pasin Suriyentrakorn on 8/28/15. * <p/> * Copyright (c) 2015 Couchbase, Inc 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.couchbase.lite; import com.couchbase.lite.store.EncryptableStore; import com.couchbase.lite.store.Store; import com.couchbase.lite.support.security.SymmetricKey; import com.couchbase.lite.util.ArrayUtils; import com.couchbase.lite.util.Log; import com.couchbase.lite.util.TextUtils; import com.couchbase.lite.util.Utils; import junit.framework.Assert; import java.io.ByteArrayInputStream; import java.io.FileInputStream; import java.io.InputStream; import java.security.SecureRandom; import java.util.Arrays; import java.util.HashMap; import java.util.Map; public class DatabaseEncryptionTest extends LiteTestCaseWithDB { private static final String TEST_DIR = "encryption"; private static final String SEEKRIT_DB_NAME = "seekrit"; private static final String NULL_PASSWORD = null; private static final boolean USE_OPENDATABASE_API = true; private Manager cryptoManager; @Override protected void setUp() throws Exception { super.setUp(); cryptoManager = createManager(getTestContext(TEST_DIR, true)); } @Override protected void tearDown() throws Exception { super.tearDown(); if (cryptoManager != null) cryptoManager.close(); } private Database openSeekritDatabase(Object key) throws CouchbaseLiteException { if (USE_OPENDATABASE_API) { DatabaseOptions options = new DatabaseOptions(); options.setCreate(true); options.setEncryptionKey(key); return cryptoManager.openDatabase(SEEKRIT_DB_NAME, options); } else { cryptoManager.registerEncryptionKey(key, SEEKRIT_DB_NAME); return cryptoManager.getDatabase(SEEKRIT_DB_NAME); } } public void testSymmetricKey() throws Exception { if (!isEncryptionTestEnabled()) return; Database database = openSeekritDatabase(null); long start = System.currentTimeMillis(); SymmetricKey key = database.createSymmetricKey("letmein123456"); long end = System.currentTimeMillis(); Log.i(TAG, "Finished getting a symmetric key in " + (end - start) + " msec."); byte[] keyData = key.getKey(); Log.i(TAG, "Key = " + key); // Encrypt using the key: byte[] clearText = "This is the clear text.".getBytes(); byte[] ciphertext = key.encryptData(clearText); Log.i(TAG, "Encrypted = " + new String(ciphertext)); Assert.assertNotNull(ciphertext); // Decrypt using the key: byte[] decrypted = key.decryptData(ciphertext); Log.i(TAG, "Decrypted String = " + new String(decrypted)); Assert.assertTrue(Arrays.equals(clearText, decrypted)); // Incremental encryption: start = System.currentTimeMillis(); SymmetricKey.Encryptor encryptor = key.createEncryptor(); byte[] incrementalClearText = new byte[0]; byte[] incrementalCiphertext = new byte[0]; SecureRandom random = new SecureRandom(); for (int i = 0; i < 55; i++) { byte[] data = new byte[555]; random.nextBytes(data); byte[] cipherData = encryptor.encrypt(data); incrementalClearText = ArrayUtils.concat(incrementalClearText, data); incrementalCiphertext = ArrayUtils.concat(incrementalCiphertext, cipherData); } incrementalCiphertext = ArrayUtils.concat(incrementalCiphertext, encryptor.encrypt(null)); decrypted = key.decryptData(incrementalCiphertext); Assert.assertTrue(Arrays.equals(incrementalClearText, decrypted)); end = System.currentTimeMillis(); Log.i(TAG, "Finished incremental encryption test in " + (end - start) + " msec."); } public void testCreateRandomSymmetricKey() throws Exception { if (!isEncryptionTestEnabled()) return; long start = System.currentTimeMillis(); SymmetricKey key = new SymmetricKey(); long end = System.currentTimeMillis(); Log.i(TAG, "Finished creating a random symmetric key in " + (end - start) + " msec."); byte[] keyData = key.getKey(); Assert.assertNotNull(keyData); Assert.assertEquals(32, keyData.length); Log.i(TAG, "Key = " + key); } public void testKeyDerivation() throws Exception { if (!isEncryptionTestEnabled()) return; Store store = database.getStore(); if (store instanceof EncryptableStore) { EncryptableStore ens = (EncryptableStore) store; final byte[] salt = "Salty McNaCl".getBytes(); final int rounds = 64000; byte[] result = ens.derivePBKDF2SHA256Key("letmein", salt, rounds); Assert.assertNotNull(result); String hexData = Utils.bytesToHex(result); Assert.assertEquals("6a9bd780221f4fe8a594fc728a94ba633b882983fe5613db427bb61242bfef0f", hexData); } else Assert.fail("No encryptable store"); } public void testEncryptionFailsGracefully() throws Exception { if (isEncryptionTestEnabled()) return; Database seekrit = null; CouchbaseLiteException error = null; try { seekrit = openSeekritDatabase("123456"); } catch (CouchbaseLiteException e) { error = e; } Assert.assertNotNull(error); Assert.assertEquals(501, error.getCBLStatus().getCode()); Assert.assertNull(seekrit); } public void testUnEncryptedDB() throws Exception { if (!isEncryptionTestEnabled()) return; // Create unencrypted DB: Database seekrit = openSeekritDatabase(null); Map<String, Object> properties = new HashMap<String, Object>(); properties.put("answer", "42"); createDocumentWithProperties(seekrit, properties); // Close DB: Assert.assertTrue(seekrit.close()); // Try to reopen with a password which should be failed: CouchbaseLiteException error = null; try { seekrit = null; seekrit = openSeekritDatabase("letmein"); } catch (CouchbaseLiteException e) { error = e; } Assert.assertNotNull(error); Assert.assertEquals(401, error.getCBLStatus().getCode()); Assert.assertNull(seekrit); // Reopen with no password: seekrit = openSeekritDatabase(NULL_PASSWORD); Assert.assertNotNull(seekrit); } public void testEncryptedDB() throws Exception { if (!isEncryptionTestEnabled()) return; // Create encrypted DB: Database seekrit = openSeekritDatabase("123456"); Assert.assertNotNull(seekrit); Map<String, Object> properties = new HashMap<String, Object>(); properties.put("answer", "42"); createDocumentWithProperties(seekrit, properties); Assert.assertTrue(seekrit.close()); // Try to reopen without the password (fails): CouchbaseLiteException error = null; try { seekrit = null; seekrit = openSeekritDatabase(NULL_PASSWORD); } catch (CouchbaseLiteException e) { error = e; } Assert.assertNotNull(error); Assert.assertEquals(401, error.getCBLStatus().getCode()); Assert.assertNull(seekrit); // Reopen with correct password: seekrit = openSeekritDatabase("123456"); Assert.assertNotNull(seekrit); Assert.assertEquals(1, seekrit.getDocumentCount()); Assert.assertTrue(seekrit.close()); } public void testDeleteEcryptedDB() throws Exception { if (!isEncryptionTestEnabled()) return; // Create encrypted DB: Database seekrit = openSeekritDatabase("letmein"); Assert.assertNotNull(seekrit); Map<String, Object> properties = new HashMap<String, Object>(); properties.put("answer", "42"); createDocumentWithProperties(seekrit, properties); // Delete db; this also unregisters its password: seekrit.delete(); // Re-create database: seekrit = openSeekritDatabase(null); Assert.assertNotNull(seekrit); Assert.assertEquals(0, seekrit.getDocumentCount()); Assert.assertTrue(seekrit.close()); // Make sure it doesn't need a password now: seekrit = openSeekritDatabase(NULL_PASSWORD); Assert.assertNotNull(seekrit); Assert.assertEquals(0, seekrit.getDocumentCount()); Assert.assertTrue(seekrit.close()); // Make sure old password doesn't work: CouchbaseLiteException error = null; try { seekrit = null; seekrit = openSeekritDatabase("letmein"); } catch (CouchbaseLiteException e) { error = e; } Assert.assertNotNull(error); Assert.assertEquals(401, error.getCBLStatus().getCode()); Assert.assertNull(seekrit); } public void testCompactEncryptedDB() throws Exception { if (!isEncryptionTestEnabled()) return; // Create encrypted DB: Database seekrit = openSeekritDatabase("letmein"); Assert.assertNotNull(seekrit); // Create a doc and then update it: Map<String, Object> properties = new HashMap<String, Object>(); properties.put("answer", "42"); Document doc = seekrit.createDocument(); doc.putProperties(properties); doc.update(new Document.DocumentUpdater() { @Override public boolean update(UnsavedRevision rev) { Map<String, Object> properties = rev.getProperties(); properties.put("foo", "84"); rev.setProperties(properties); return true; } }); // Compact: seekrit.compact(); // Update the document: doc.update(new Document.DocumentUpdater() { @Override public boolean update(UnsavedRevision rev) { Map<String, Object> properties = rev.getProperties(); properties.put("foo", "85"); rev.setProperties(properties); return true; } }); // Close and then reopen the database: Assert.assertTrue(seekrit.close()); // Reopen the database: seekrit = null; seekrit = openSeekritDatabase("letmein"); Assert.assertNotNull(seekrit); Assert.assertEquals(1, seekrit.getDocumentCount()); } public void testEncryptedAttachments() throws Exception { if (!isEncryptionTestEnabled()) return; Database seekrit = openSeekritDatabase("letmein"); Assert.assertNotNull(seekrit); // Save a doc with an attachment: Document doc = seekrit.getDocument("att"); byte[] body = "This is a test attachment!".getBytes(); ByteArrayInputStream bis = new ByteArrayInputStream(body); UnsavedRevision rev = doc.createRevision(); rev.setAttachment("att.txt", "text/plain; charset=utf-8", bis); SavedRevision savedRev = rev.save(); Assert.assertNotNull(savedRev); // Read the raw attachment file and make sure it's not clear text: Map<String, Object> atts = (Map<String, Object>) savedRev.getProperties().get("_attachments"); Map<String, Object> att = (Map<String, Object>) atts.get("att.txt"); String digest = (String) att.get("digest"); Assert.assertNotNull(digest); BlobKey key = new BlobKey(digest); String path = seekrit.getAttachmentStore().getRawPathForKey(key); FileInputStream fis = null; fis = new FileInputStream(path); byte[] raw; try { raw = TextUtils.read(fis); } finally { fis.close(); } Assert.assertNotNull(raw); Assert.assertTrue(!Arrays.equals(raw, body)); } public void testRekey() throws Exception { if (!isEncryptionTestEnabled()) return; // First run the encrypted-attachments test to populate the db: testEncryptedAttachments(); Database seekrit = openSeekritDatabase("letmein"); seekrit.changeEncryptionKey("letmeout"); // Close & reopen seekrit: Assert.assertTrue(seekrit.close()); cryptoManager.registerEncryptionKey("letmeout", "seekrit"); Database seekrit2 = openSeekritDatabase("letmeout"); Assert.assertNotNull(seekrit2); seekrit = seekrit2; // Check the document and its attachment: SavedRevision savedRev = seekrit.getDocument("att").getCurrentRevision(); Assert.assertNotNull(savedRev); Attachment att = savedRev.getAttachment("att.txt"); Assert.assertNotNull(att); byte[] body = "This is a test attachment!".getBytes(); InputStream in = att.getContent(); byte[] rawAtt; try { rawAtt = TextUtils.read(in); } finally { in.close(); } Assert.assertTrue(Arrays.equals(rawAtt, body)); } }