{"action":"created","comment":{"url":"https://api.github.com/repos/cloudant/sync-android/pulls/comments/31610898","id":31610898,"diff_hunk":"@@ -0,0 +1,183 @@\n+/**\n+ * Copyright (c) 2015 IBM Cloudant, Inc. All rights reserved.\n+ * <p/>\n+ * Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this file\n+ * except in compliance with the License. You may obtain a copy of the License at\n+ * <p/>\n+ * http://www.apache.org/licenses/LICENSE-2.0\n+ * <p/>\n+ * Unless required by applicable law or agreed to in writing, software distributed under the\n+ * License is distributed on an \"AS IS\" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,\n+ * either express or implied. See the License for the specific language governing permissions\n+ * and limitations under the License.\n+ */\n+\n+package com.cloudant.sync.datastore;\n+\n+import com.cloudant.sync.datastore.encryption.EncryptedAttachmentInputStream;\n+import com.cloudant.sync.datastore.encryption.EncryptedAttachmentOutputStream;\n+import com.cloudant.sync.datastore.encryption.EncryptionKey;\n+import com.cloudant.sync.datastore.encryption.KeyProvider;\n+\n+import org.apache.commons.io.FileUtils;\n+\n+import java.io.File;\n+import java.io.FileInputStream;\n+import java.io.FileNotFoundException;\n+import java.io.FileOutputStream;\n+import java.io.IOException;\n+import java.io.InputStream;\n+import java.io.OutputStream;\n+import java.security.InvalidAlgorithmParameterException;\n+import java.security.InvalidKeyException;\n+import java.security.SecureRandom;\n+import java.util.zip.GZIPInputStream;\n+import java.util.zip.GZIPOutputStream;\n+\n+import javax.crypto.CipherInputStream;\n+import javax.crypto.CipherOutputStream;\n+\n+/**\n+ * Class to manage returning appropriate streams to access on disk attachments.\n+ *\n+ * These may be encrypted and/or gzip encoded. This class handles returning an\n+ * appropriate input/output stream chain to handle this.\n+ *\n+ * The input stream returned will allow reading unencrypted, unzipped data from\n+ * disk.\n+ *\n+ * The output stream returned should have unencrypted, unzipped data written to\n+ * its write method.\n+ *\n+ */\n+class AttachmentStreamFactory {\n+\n+    /**\n+     * Byte array if there is a valid AES key, null if attachments\n+     * should not be encrypted.\n+     */\n+    private final byte[] key;\n+\n+    /**\n+     * Creates a factory Attachment objects can use to read/write attachments.\n+     *\n+     * @param keyProvider Datastore's key provider object. This object can cope with key providers\n+     *                    that return null keys.\n+     */\n+    public AttachmentStreamFactory(KeyProvider keyProvider) {\n+        if (keyProvider.getEncryptionKey() != null) {\n+            // Key guaranteed to be 256-bits by EncryptionKey class\n+            this.key = keyProvider.getEncryptionKey().getKey();\n+        } else {\n+            this.key = null;\n+        }\n+    }\n+\n+    /**\n+     * Return a stream to be used to read from the file on disk.\n+     *\n+     * Stream's bytes will be unzipped, unencrypted attachment content.\n+     *\n+     * @param file File object to read from.\n+     * @param encoding Encoding of attachment.\n+     * @return Stream for reading attachment data.\n+     * @throws IOException if there's a problem reading from disk, including a issues with\n+     *      encryption (bad key length and other key issues).\n+     */\n+    public InputStream getInputStream(File file, Attachment.Encoding encoding) throws IOException {\n+\n+        // First, open a stream to the raw bytes on disk.\n+        // Then, if we have a key assume the file is encrypted, so add a stream\n+        // to the chain which decrypts the data as we read from disk.\n+        // Finally, decode (unzip) the data if the attachment is encoded before\n+        // returning the data to the user.\n+        //\n+        //  Read from disk [-> Decryption Stream] [-> Decoding Stream] -> user reads\n+\n+        InputStream is = new FileInputStream(file);\n+\n+        if (key != null) {\n+            try {\n+                is = new EncryptedAttachmentInputStream(is, key);\n+            } catch (InvalidKeyException ex) {\n+                // Replace with an IOException as we validate the key when opening\n+                // the databases and the key should be the same -- it's therefore\n+                // not worth forcing the developer to catch something they can't\n+                // fix during file read; generic IOException works better.\n+                throw new IOException(\"Bad key used to open file; check encryption key.\", ex);\n+            }\n+        }\n+\n+        switch (encoding) {\n+            case Plain:\n+                break;  // nothing to do\n+            case Gzip:\n+                is = new GZIPInputStream(is);\n+        }\n+\n+        return is;\n+    }\n+\n+    /**\n+     * Get stream for writing attachment data to disk.\n+     *\n+     * Opens the output stream using {@see FileUtils#openOutputStream(File)}.\n+     *\n+     * Data should be written to the stream un-encoded, un-encrypted.","path":"sync-core/src/main/java/com/cloudant/sync/datastore/AttachmentStreamFactory.java","position":126,"original_position":126,"commit_id":"641d16e83180268a59d7650e7a7ddd069ac65457","original_commit_id":"3cdec95011d96cffedd31381169db317a7222f68","user":{"login":"mikerhodes","id":453210,"avatar_url":"https://avatars.githubusercontent.com/u/453210?v=3","gravatar_id":"","url":"https://api.github.com/users/mikerhodes","html_url":"https://github.com/mikerhodes","followers_url":"https://api.github.com/users/mikerhodes/followers","following_url":"https://api.github.com/users/mikerhodes/following{/other_user}","gists_url":"https://api.github.com/users/mikerhodes/gists{/gist_id}","starred_url":"https://api.github.com/users/mikerhodes/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mikerhodes/subscriptions","organizations_url":"https://api.github.com/users/mikerhodes/orgs","repos_url":"https://api.github.com/users/mikerhodes/repos","events_url":"https://api.github.com/users/mikerhodes/events{/privacy}","received_events_url":"https://api.github.com/users/mikerhodes/received_events","type":"User","site_admin":false},"body":"Fixed in 641d16e83180268a59d7650e7a7ddd069ac65457.","created_at":"2015-06-03T10:32:37Z","updated_at":"2015-06-03T10:32:37Z","html_url":"https://github.com/cloudant/sync-android/pull/150#discussion_r31610898","pull_request_url":"https://api.github.com/repos/cloudant/sync-android/pulls/150","_links":{"self":{"href":"https://api.github.com/repos/cloudant/sync-android/pulls/comments/31610898"},"html":{"href":"https://github.com/cloudant/sync-android/pull/150#discussion_r31610898"},"pull_request":{"href":"https://api.github.com/repos/cloudant/sync-android/pulls/150"}}},"pull_request":{"url":"https://api.github.com/repos/cloudant/sync-android/pulls/150","id":36562935,"html_url":"https://github.com/cloudant/sync-android/pull/150","diff_url":"https://github.com/cloudant/sync-android/pull/150.diff","patch_url":"https://github.com/cloudant/sync-android/pull/150.patch","issue_url":"https://api.github.com/repos/cloudant/sync-android/issues/150","number":150,"state":"open","locked":false,"title":"47457 attachment stream factory","user":{"login":"mikerhodes","id":453210,"avatar_url":"https://avatars.githubusercontent.com/u/453210?v=3","gravatar_id":"","url":"https://api.github.com/users/mikerhodes","html_url":"https://github.com/mikerhodes","followers_url":"https://api.github.com/users/mikerhodes/followers","following_url":"https://api.github.com/users/mikerhodes/following{/other_user}","gists_url":"https://api.github.com/users/mikerhodes/gists{/gist_id}","starred_url":"https://api.github.com/users/mikerhodes/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/mikerhodes/subscriptions","organizations_url":"https://api.github.com/users/mikerhodes/orgs","repos_url":"https://api.github.com/users/mikerhodes/repos","events_url":"https://api.github.com/users/mikerhodes/events{/privacy}","received_events_url":"https://api.github.com/users/mikerhodes/received_events","type":"User","site_admin":false},"body":"_What:_\r\nThis PR adds two things:\r\n1. AttachmentStreamFactory which encapsulates the process of getting a stream to read/write from attachment on-disk representations (6081d54).\r\n2. Adds use of encryption input/output streams to AttachmentStreamFactory which means that PreparedAttachment and SavedAttachment transparently write and read encrypted files respectively (4625b4b).\r\n\r\nThe temporary file written by prepared attachment is also encrypted, meaning data should not hit disk in an unencrypted form.\r\n\r\n_Why:_\r\nTransparently allows users of the datastore to read and write encrypted attachments to and from disk, using the same key as for encrypting the SQLite databases with SQLCipher. This is needed so all data on disk is encrypted at all times.\r\n\r\n_How_:\r\nSee \"What\".\r\n\r\n_Testing_:\r\nTests are added which check that AttachmentStreamFactory's streams make SavedAttachment and PreparedAttachment use encryption when there is a key, and that SavedAttachment can read blobs written by PreparedAttachment when the same key is provided to both.\r\n\r\nWe also check that SavedAttachment can read from pre-prepared encrypted blobs when using AttachmentStreamFactory.\r\n\r\nreviewer @tomblench \r\nreviewer @brynh ","created_at":"2015-05-30T09:23:29Z","updated_at":"2015-06-03T10:32:37Z","closed_at":null,"merged_at":null,"merge_commit_sha":"01ba3dae619baed82664274d9219d74ec8fe7db2","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/cloudant/sync-android/pulls/150/commits","review_comments_url":"https://api.github.com/repos/cloudant/sync-android/pulls/150/comments","review_comment_url":"https://api.github.com/repos/cloudant/sync-android/pulls/comments{/number}","comments_url":"https://api.github.com/repos/cloudant/sync-android/issues/150/comments","statuses_url":"https://api.github.com/repos/cloudant/sync-android/statuses/641d16e83180268a59d7650e7a7ddd069ac65457","head":{"label":"cloudant:47457-attachment-stream-factory","ref":"47457-attachment-stream-factory","sha":"641d16e83180268a59d7650e7a7ddd069ac65457","user":{"login":"cloudant","id":44642,"avatar_url":"https://avatars.githubusercontent.com/u/44642?v=3","gravatar_id":"","url":"https://api.github.com/users/cloudant","html_url":"https://github.com/cloudant","followers_url":"https://api.github.com/users/cloudant/followers","following_url":"https://api.github.com/users/cloudant/following{/other_user}","gists_url":"https://api.github.com/users/cloudant/gists{/gist_id}","starred_url":"https://api.github.com/users/cloudant/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cloudant/subscriptions","organizations_url":"https://api.github.com/users/cloudant/orgs","repos_url":"https://api.github.com/users/cloudant/repos","events_url":"https://api.github.com/users/cloudant/events{/privacy}","received_events_url":"https://api.github.com/users/cloudant/received_events","type":"Organization","site_admin":false},"repo":{"id":16811502,"name":"sync-android","full_name":"cloudant/sync-android","owner":{"login":"cloudant","id":44642,"avatar_url":"https://avatars.githubusercontent.com/u/44642?v=3","gravatar_id":"","url":"https://api.github.com/users/cloudant","html_url":"https://github.com/cloudant","followers_url":"https://api.github.com/users/cloudant/followers","following_url":"https://api.github.com/users/cloudant/following{/other_user}","gists_url":"https://api.github.com/users/cloudant/gists{/gist_id}","starred_url":"https://api.github.com/users/cloudant/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cloudant/subscriptions","organizations_url":"https://api.github.com/users/cloudant/orgs","repos_url":"https://api.github.com/users/cloudant/repos","events_url":"https://api.github.com/users/cloudant/events{/privacy}","received_events_url":"https://api.github.com/users/cloudant/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/cloudant/sync-android","description":"A JSON-based document datastore for Android applications","fork":false,"url":"https://api.github.com/repos/cloudant/sync-android","forks_url":"https://api.github.com/repos/cloudant/sync-android/forks","keys_url":"https://api.github.com/repos/cloudant/sync-android/keys{/key_id}","collaborators_url":"https://api.github.com/repos/cloudant/sync-android/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/cloudant/sync-android/teams","hooks_url":"https://api.github.com/repos/cloudant/sync-android/hooks","issue_events_url":"https://api.github.com/repos/cloudant/sync-android/issues/events{/number}","events_url":"https://api.github.com/repos/cloudant/sync-android/events","assignees_url":"https://api.github.com/repos/cloudant/sync-android/assignees{/user}","branches_url":"https://api.github.com/repos/cloudant/sync-android/branches{/branch}","tags_url":"https://api.github.com/repos/cloudant/sync-android/tags","blobs_url":"https://api.github.com/repos/cloudant/sync-android/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/cloudant/sync-android/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/cloudant/sync-android/git/refs{/sha}","trees_url":"https://api.github.com/repos/cloudant/sync-android/git/trees{/sha}","statuses_url":"https://api.github.com/repos/cloudant/sync-android/statuses/{sha}","languages_url":"https://api.github.com/repos/cloudant/sync-android/languages","stargazers_url":"https://api.github.com/repos/cloudant/sync-android/stargazers","contributors_url":"https://api.github.com/repos/cloudant/sync-android/contributors","subscribers_url":"https://api.github.com/repos/cloudant/sync-android/subscribers","subscription_url":"https://api.github.com/repos/cloudant/sync-android/subscription","commits_url":"https://api.github.com/repos/cloudant/sync-android/commits{/sha}","git_commits_url":"https://api.github.com/repos/cloudant/sync-android/git/commits{/sha}","comments_url":"https://api.github.com/repos/cloudant/sync-android/comments{/number}","issue_comment_url":"https://api.github.com/repos/cloudant/sync-android/issues/comments{/number}","contents_url":"https://api.github.com/repos/cloudant/sync-android/contents/{+path}","compare_url":"https://api.github.com/repos/cloudant/sync-android/compare/{base}...{head}","merges_url":"https://api.github.com/repos/cloudant/sync-android/merges","archive_url":"https://api.github.com/repos/cloudant/sync-android/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/cloudant/sync-android/downloads","issues_url":"https://api.github.com/repos/cloudant/sync-android/issues{/number}","pulls_url":"https://api.github.com/repos/cloudant/sync-android/pulls{/number}","milestones_url":"https://api.github.com/repos/cloudant/sync-android/milestones{/number}","notifications_url":"https://api.github.com/repos/cloudant/sync-android/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/cloudant/sync-android/labels{/name}","releases_url":"https://api.github.com/repos/cloudant/sync-android/releases{/id}","created_at":"2014-02-13T17:52:09Z","updated_at":"2015-06-01T09:59:35Z","pushed_at":"2015-06-03T10:32:27Z","git_url":"git://github.com/cloudant/sync-android.git","ssh_url":"git@github.com:cloudant/sync-android.git","clone_url":"https://github.com/cloudant/sync-android.git","svn_url":"https://github.com/cloudant/sync-android","homepage":"","size":28713,"stargazers_count":79,"watchers_count":79,"language":"Java","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":26,"mirror_url":null,"open_issues_count":9,"forks":26,"open_issues":9,"watchers":79,"default_branch":"master"}},"base":{"label":"cloudant:feature-encryption","ref":"feature-encryption","sha":"dc8f73ffa3ad8fb50df46300dc6c085d92bb500c","user":{"login":"cloudant","id":44642,"avatar_url":"https://avatars.githubusercontent.com/u/44642?v=3","gravatar_id":"","url":"https://api.github.com/users/cloudant","html_url":"https://github.com/cloudant","followers_url":"https://api.github.com/users/cloudant/followers","following_url":"https://api.github.com/users/cloudant/following{/other_user}","gists_url":"https://api.github.com/users/cloudant/gists{/gist_id}","starred_url":"https://api.github.com/users/cloudant/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cloudant/subscriptions","organizations_url":"https://api.github.com/users/cloudant/orgs","repos_url":"https://api.github.com/users/cloudant/repos","events_url":"https://api.github.com/users/cloudant/events{/privacy}","received_events_url":"https://api.github.com/users/cloudant/received_events","type":"Organization","site_admin":false},"repo":{"id":16811502,"name":"sync-android","full_name":"cloudant/sync-android","owner":{"login":"cloudant","id":44642,"avatar_url":"https://avatars.githubusercontent.com/u/44642?v=3","gravatar_id":"","url":"https://api.github.com/users/cloudant","html_url":"https://github.com/cloudant","followers_url":"https://api.github.com/users/cloudant/followers","following_url":"https://api.github.com/users/cloudant/following{/other_user}","gists_url":"https://api.github.com/users/cloudant/gists{/gist_id}","starred_url":"https://api.github.com/users/cloudant/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/cloudant/subscriptions","organizations_url":"https://api.github.com/users/cloudant/orgs","repos_url":"https://api.github.com/users/cloudant/repos","events_url":"https://api.github.com/users/cloudant/events{/privacy}","received_events_url":"https://api.github.com/users/cloudant/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/cloudant/sync-android","description":"A JSON-based document datastore for Android applications","fork":false,"url":"https://api.github.com/repos/cloudant/sync-android","forks_url":"https://api.github.com/repos/cloudant/sync-android/forks","keys_url":"https://api.github.com/repos/cloudant/sync-android/keys{/key_id}","collaborators_url":"https://api.github.com/repos/cloudant/sync-android/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/cloudant/sync-android/teams","hooks_url":"https://api.github.com/repos/cloudant/sync-android/hooks","issue_events_url":"https://api.github.com/repos/cloudant/sync-android/issues/events{/number}","events_url":"https://api.github.com/repos/cloudant/sync-android/events","assignees_url":"https://api.github.com/repos/cloudant/sync-android/assignees{/user}","branches_url":"https://api.github.com/repos/cloudant/sync-android/branches{/branch}","tags_url":"https://api.github.com/repos/cloudant/sync-android/tags","blobs_url":"https://api.github.com/repos/cloudant/sync-android/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/cloudant/sync-android/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/cloudant/sync-android/git/refs{/sha}","trees_url":"https://api.github.com/repos/cloudant/sync-android/git/trees{/sha}","statuses_url":"https://api.github.com/repos/cloudant/sync-android/statuses/{sha}","languages_url":"https://api.github.com/repos/cloudant/sync-android/languages","stargazers_url":"https://api.github.com/repos/cloudant/sync-android/stargazers","contributors_url":"https://api.github.com/repos/cloudant/sync-android/contributors","subscribers_url":"https://api.github.com/repos/cloudant/sync-android/subscribers","subscription_url":"https://api.github.com/repos/cloudant/sync-android/subscription","commits_url":"https://api.github.com/repos/cloudant/sync-android/commits{/sha}","git_commits_url":"https://api.github.com/repos/cloudant/sync-android/git/commits{/sha}","comments_url":"https://api.github.com/repos/cloudant/sync-android/comments{/number}","issue_comment_url":"https://api.github.com/repos/cloudant/sync-android/issues/comments{/number}","contents_url":"https://api.github.com/repos/cloudant/sync-android/contents/{+path}","compare_url":"https://api.github.com/repos/cloudant/sync-android/compare/{base}...{head}","merges_url":"https://api.github.com/repos/cloudant/sync-android/merges","archive_url":"https://api.github.com/repos/cloudant/sync-android/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/cloudant/sync-android/downloads","issues_url":"https://api.github.com/repos/cloudant/sync-android/issues{/number}","pulls_url":"https://api.github.com/repos/cloudant/sync-android/pulls{/number}","milestones_url":"https://api.github.com/repos/cloudant/sync-android/milestones{/number}","notifications_url":"https://api.github.com/repos/cloudant/sync-android/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/cloudant/sync-android/labels{/name}","releases_url":"https://api.github.com/repos/cloudant/sync-android/releases{/id}","created_at":"2014-02-13T17:52:09Z","updated_at":"2015-06-01T09:59:35Z","pushed_at":"2015-06-03T10:32:27Z","git_url":"git://github.com/cloudant/sync-android.git","ssh_url":"git@github.com:cloudant/sync-android.git","clone_url":"https://github.com/cloudant/sync-android.git","svn_url":"https://github.com/cloudant/sync-android","homepage":"","size":28713,"stargazers_count":79,"watchers_count":79,"language":"Java","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":26,"mirror_url":null,"open_issues_count":9,"forks":26,"open_issues":9,"watchers":79,"default_branch":"master"}},"_links":{"self":{"href":"https://api.github.com/repos/cloudant/sync-android/pulls/150"},"html":{"href":"https://github.com/cloudant/sync-android/pull/150"},"issue":{"href":"https://api.github.com/repos/cloudant/sync-android/issues/150"},"comments":{"href":"https://api.github.com/repos/cloudant/sync-android/issues/150/comments"},"review_comments":{"href":"https://api.github.com/repos/cloudant/sync-android/pulls/150/comments"},"review_comment":{"href":"https://api.github.com/repos/cloudant/sync-android/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/cloudant/sync-android/pulls/150/commits"},"statuses":{"href":"https://api.github.com/repos/cloudant/sync-android/statuses/641d16e83180268a59d7650e7a7ddd069ac65457"}}}}