summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorVictor Shyba <victor.shyba@gmail.com>2015-01-14 17:14:35 -0300
committerVictor Shyba <victor.shyba@gmail.com>2015-01-14 17:14:35 -0300
commitab2fb69e74464d5424d7e3429d4e787586cd00e8 (patch)
tree9669414250ac38fe5812219c691700d0920ff35b
parent25aa934117802fab7a0c20f029d797602ebd46a8 (diff)
for #227, MAC on encrypted storage
-rw-r--r--service/pixelated/adapter/soledad/soledad_search_key_masterkey_retrieval_mixin.py2
-rw-r--r--service/pixelated/support/encrypted_file_storage.py29
-rw-r--r--service/test/support/integration/app_test_client.py2
-rw-r--r--service/test/unit/support/__init__.py0
-rw-r--r--service/test/unit/support/encrypted_file_storage_test.py64
5 files changed, 89 insertions, 8 deletions
diff --git a/service/pixelated/adapter/soledad/soledad_search_key_masterkey_retrieval_mixin.py b/service/pixelated/adapter/soledad/soledad_search_key_masterkey_retrieval_mixin.py
index d2d6f416..05d32779 100644
--- a/service/pixelated/adapter/soledad/soledad_search_key_masterkey_retrieval_mixin.py
+++ b/service/pixelated/adapter/soledad/soledad_search_key_masterkey_retrieval_mixin.py
@@ -25,7 +25,7 @@ class SoledadSearchIndexMasterkeyRetrievalMixin(SoledadDbFacadeMixin, object):
index_key_doc = result[0] if result else None
if not index_key_doc:
- new_index_key = os.urandom(32)
+ new_index_key = os.urandom(64) # 32 for encryption, 32 for hmac
self.create_doc(dict(type='index_key', value=base64.encodestring(new_index_key)))
return new_index_key
return base64.decodestring(index_key_doc.content['value'])
diff --git a/service/pixelated/support/encrypted_file_storage.py b/service/pixelated/support/encrypted_file_storage.py
index 04f2e6e8..49a67627 100644
--- a/service/pixelated/support/encrypted_file_storage.py
+++ b/service/pixelated/support/encrypted_file_storage.py
@@ -15,9 +15,10 @@
# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
import io
-from hashlib import sha512
+from hashlib import sha256
import os
+import hmac
from whoosh.filedb.filestore import FileStorage
from whoosh.filedb.structfile import StructFile, BufferFile
from leap.soledad.client.crypto import encrypt_sym
@@ -28,7 +29,8 @@ from whoosh.util import random_name
class EncryptedFileStorage(FileStorage):
def __init__(self, path, masterkey=None):
- self.masterkey = masterkey
+ self.masterkey = masterkey[:32]
+ self.signkey = masterkey[32:]
self._tmp_storage = self.temp_storage
self.length_cache = {}
FileStorage.__init__(self, path, supports_mmap=False)
@@ -49,21 +51,36 @@ class EncryptedFileStorage(FileStorage):
def file_length(self, name):
return self.length_cache[name][0]
+ def gen_mac(self, ciphertext):
+ return hmac.new(self.signkey, ciphertext, sha256).digest()
+
+ def encrypt(self, content):
+ iv, ciphertext = encrypt_sym(content, self.masterkey, EncryptionMethods.XSALSA20)
+ mac = self.gen_mac(ciphertext)
+ return ''.join((mac, iv, ciphertext))
+
+ def decrypt(self, payload):
+ payload_mac, iv, ciphertext = payload[:32], payload[32:65], payload[65:]
+ generated_mac = self.gen_mac(ciphertext)
+ if sha256(payload_mac).digest() != sha256(generated_mac).digest():
+ raise Exception("EncryptedFileStorage - Error opening file. Wrong MAC")
+ return decrypt_sym(ciphertext, self.masterkey, EncryptionMethods.XSALSA20, iv=iv)
+
def _encrypt_index_on_close(self, name):
def wrapper(struct_file):
struct_file.seek(0)
content = struct_file.file.read()
- file_hash = sha512(content).digest()
+ file_hash = sha256(content).digest()
if name in self.length_cache and file_hash == self.length_cache[name][1]:
return
self.length_cache[name] = (len(content), file_hash)
- encrypted_content = ''.join(encrypt_sym(content, self.masterkey, EncryptionMethods.XSALSA20))
+ encrypted_content = self.encrypt(content)
with open(self._fpath(name), 'w+b') as f:
f.write(encrypted_content)
return wrapper
def _open_encrypted_file(self, name, onclose=lambda x: None):
file_content = open(self._fpath(name), "rb").read()
- decrypted = decrypt_sym(file_content[33:], self.masterkey, EncryptionMethods.XSALSA20, iv=file_content[:33])
- self.length_cache[name] = (len(decrypted), sha512(decrypted).digest())
+ decrypted = self.decrypt(file_content)
+ self.length_cache[name] = (len(decrypted), sha256(decrypted).digest())
return BufferFile(buffer(decrypted), name=name, onclose=onclose)
diff --git a/service/test/support/integration/app_test_client.py b/service/test/support/integration/app_test_client.py
index c577d881..893ec57f 100644
--- a/service/test/support/integration/app_test_client.py
+++ b/service/test/support/integration/app_test_client.py
@@ -51,7 +51,7 @@ class AppTestClient:
self.app = pixelated.runserver.app
self.soledad_querier = SoledadQuerier(self.soledad)
- self.soledad_querier.get_index_masterkey = lambda: 'h\xbcpC\xb1\xafc\x92\xf3\xa1v\x1fa\x9dlA\x1a\xf7\xcf\xf2\nG\xad4\xb8m\x01\xf5\xa0\xa9\xd8\xca'
+ self.soledad_querier.get_index_masterkey = lambda: '\xde3?\x87\xff\xd9\xd3\x14\xf0\xa7>\x1f%C{\x16.\\\xae\x8c\x13\xa7\xfb\x04\xd4]+\x8d_\xed\xd1\x8d\x0bI\x8a\x0e\xa4tm\xab\xbf\xb4\xa5\x99\x00d\xd5w\x9f\x18\xbc\x1d\xd4_W\xd2\xb6\xe8H\x83\x1b\xd8\x9d\xad'
self.account = SoledadBackedAccount('test', self.soledad, MagicMock())
self.mailboxes = Mailboxes(self.account, self.soledad_querier)
diff --git a/service/test/unit/support/__init__.py b/service/test/unit/support/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/service/test/unit/support/__init__.py
diff --git a/service/test/unit/support/encrypted_file_storage_test.py b/service/test/unit/support/encrypted_file_storage_test.py
new file mode 100644
index 00000000..2a6735c3
--- /dev/null
+++ b/service/test/unit/support/encrypted_file_storage_test.py
@@ -0,0 +1,64 @@
+#
+# Copyright (c) 2014 ThoughtWorks, Inc.
+#
+# Pixelated is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Pixelated is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with Pixelated. If not, see <http://www.gnu.org/licenses/>.
+import os
+import shutil
+import unittest
+from pixelated.support.encrypted_file_storage import EncryptedFileStorage
+
+
+class EncryptedFileStorageTest(unittest.TestCase):
+
+ def setUp(self):
+ self.key = '2\x06\xf87F:\xd2\xe2]w\xc9\x0c\xb8\x9b\x8e\xd3\x92\t\xabHu\xa6\xa3\x9a\x8d\xec\x0c\xab<8\xbb\x12\xfbP\xf2\x83"\xa1\xcf7\x92\xb0!\xfe\xebM\x80\x8a\x14\xe6\xf9xr\xf5#\x8f\x1bs\xb3#\x0e)a\xd8'
+ self.msg = 'this is a very, very secret binary message: \xbe\xba\xca\xfe'
+ self.path = os.path.join('tmp', 'search_test')
+ self.storage = EncryptedFileStorage(self.path, self.key)
+
+ def tearDown(self):
+ if os.path.exists(self.path):
+ shutil.rmtree(self.path)
+
+ def test_encrypt_decrypt(self):
+ storage, msg = self.storage, self.msg
+
+ ciphertext = storage.encrypt(msg)
+
+ self.assertNotEquals(msg, ciphertext)
+ self.assertEquals(msg, storage.decrypt(ciphertext))
+
+ def test_mac_against_appended_garbage(self):
+ storage, msg = self.storage, self.msg
+
+ ciphertext = storage.encrypt(msg)
+ corrupted_ciphertext = ciphertext + 'garbage'
+
+ try:
+ storage.decrypt(corrupted_ciphertext)
+ self.fail('MAC is not detecting appended garbage on ciphertext')
+ except:
+ pass
+
+ def test_mac_against_modified_file(self):
+ storage, msg = self.storage, self.msg
+
+ ciphertext = storage.encrypt(msg)
+ corrupted_ciphertext = ''.join([chr(ord(i) >> 1) for i in ciphertext])
+
+ try:
+ storage.decrypt(corrupted_ciphertext)
+ self.fail('MAC is not detecting corrupt ciphertext')
+ except:
+ pass