summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--client/src/leap/soledad/client/_crypto.py228
-rw-r--r--testing/tests/client/test_crypto.py5
2 files changed, 91 insertions, 142 deletions
diff --git a/client/src/leap/soledad/client/_crypto.py b/client/src/leap/soledad/client/_crypto.py
index 163c9e4e..22335f9d 100644
--- a/client/src/leap/soledad/client/_crypto.py
+++ b/client/src/leap/soledad/client/_crypto.py
@@ -36,7 +36,6 @@ import six
from twisted.internet import defer
from twisted.internet import interfaces
-from twisted.logger import Logger
from twisted.web.client import FileBodyProducer
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
@@ -47,21 +46,16 @@ from cryptography.hazmat.backends.openssl.backend \
from zope.interface import implements
-log = Logger()
-
MAC_KEY_LENGTH = 64
-crypto_backend = MultiBackend([OpenSSLBackend()])
+CRYPTO_BACKEND = MultiBackend([OpenSSLBackend()])
PACMAN = struct.Struct('cQbb16s255p255p')
-class ENC_SCHEME:
- symkey = 1
-
-
-class ENC_METHOD:
- aes_256_ctr = 1
+ENC_SCHEME = namedtuple('SCHEME', 'symkey')(1)
+ENC_METHOD = namedtuple('METHOD', 'aes_256_ctr')(1)
+DocInfo = namedtuple('DocInfo', 'doc_id rev')
class EncryptionDecryptionError(Exception):
@@ -72,9 +66,6 @@ class InvalidBlob(Exception):
pass
-docinfo = namedtuple('docinfo', 'doc_id rev')
-
-
class SoledadCrypto(object):
"""
This class provides convenient methods for document encryption and
@@ -107,7 +98,7 @@ class SoledadCrypto(object):
content = BytesIO()
content.write(str(doc.get_json()))
- info = docinfo(doc.doc_id, doc.rev)
+ info = DocInfo(doc.doc_id, doc.rev)
del doc
encryptor = BlobEncryptor(info, content, secret=self.secret)
d = encryptor.encrypt()
@@ -124,7 +115,7 @@ class SoledadCrypto(object):
:return: The decrypted cleartext content of the document.
:rtype: str
"""
- info = docinfo(doc.doc_id, doc.rev)
+ info = DocInfo(doc.doc_id, doc.rev)
ciphertext = BytesIO()
payload = doc.content['raw']
del doc
@@ -146,10 +137,10 @@ def encrypt_sym(data, key):
encoded as base64.
:rtype: (str, str)
"""
- encryptor = AESEncryptor(key)
+ encryptor = AESConsumer(key)
encryptor.write(data)
encryptor.end()
- ciphertext = encryptor.fd.getvalue()
+ ciphertext = encryptor.buffer.getvalue()
return base64.b64encode(encryptor.iv), ciphertext
@@ -169,10 +160,10 @@ def decrypt_sym(data, key, iv):
:rtype: str
"""
_iv = base64.b64decode(str(iv))
- decryptor = AESDecryptor(key, _iv)
+ decryptor = AESConsumer(key, _iv, operation=AESConsumer.decrypt)
decryptor.write(data)
decryptor.end()
- plaintext = decryptor.fd.getvalue()
+ plaintext = decryptor.buffer.getvalue()
return plaintext
@@ -205,15 +196,16 @@ class BlobEncryptor(object):
mac_key = _get_mac_key_for_doc(doc_info.doc_id, secret)
self._aes_fd = BytesIO()
- self._aes = AESEncryptor(sym_key, self._aes_fd)
- self._hmac = HMACWriter(mac_key)
+ _aes = AESConsumer(sym_key, _buffer=self._aes_fd)
+ self.__iv = _aes.iv
+ self._hmac_writer = HMACWriter(mac_key)
self._write_preamble()
- self._crypter = VerifiedEncrypter(self._aes, self._hmac)
+ self._crypter = VerifiedEncrypter(_aes, self._hmac_writer)
@property
def iv(self):
- return self._aes.iv
+ return self.__iv
def encrypt(self):
"""
@@ -224,26 +216,14 @@ class BlobEncryptor(object):
:rtype: twisted.internet.defer.Deferred
"""
d = self._producer.startProducing(self._crypter)
- d.addCallback(self._end_crypto_stream)
+ d.addCallback(lambda _: self._end_crypto_stream())
return d
- def encrypt_whole(self):
- """
- Encrypts the input data at once and returns the resulting ciphertext
- wrapped into a JSON string under the "raw" key.
-
- :return: The resulting ciphertext JSON string.
- :rtype: str
- """
- self._crypter.write(self._content_fd.getvalue())
- self._end_crypto_stream(None)
- return '{"raw":"' + self.result.getvalue() + '"}'
-
def _write_preamble(self):
def write(data):
self._preamble.write(data)
- self._hmac.write(data)
+ self._hmac_writer.write(data)
current_time = int(time.time())
@@ -256,23 +236,16 @@ class BlobEncryptor(object):
str(self.doc_id),
str(self.rev)))
- def _end_crypto_stream(self, ignored):
- self._aes.end()
- self._hmac.end()
- self._content_fd.close()
+ def _end_crypto_stream(self):
+ encrypted, content_hmac = self._crypter.end()
preamble = self._preamble.getvalue()
- encrypted = self._aes_fd.getvalue()
- hmac = self._hmac.result.getvalue()
self.result.write(
base64.urlsafe_b64encode(preamble))
self.result.write(' ')
self.result.write(
- base64.urlsafe_b64encode(encrypted + hmac))
- self._preamble.close()
- self._aes_fd.close()
- self._hmac.result.close()
+ base64.urlsafe_b64encode(encrypted + content_hmac))
self.result.seek(0)
return defer.succeed(self.result)
@@ -289,62 +262,65 @@ class BlobDecryptor(object):
secret=None):
if not secret:
raise EncryptionDecryptionError('no secret given')
- ciphertext_fd.seek(0)
self.doc_id = doc_info.doc_id
self.rev = doc_info.rev
- self.sym_key = _get_sym_key_for_doc(doc_info.doc_id, secret)
- self.mac_key = _get_mac_key_for_doc(doc_info.doc_id, secret)
-
- self._read_preamble(ciphertext_fd)
-
- self._producer = FileBodyProducer(self.ciphertext, readSize=2**16)
- self._content_fd = self.ciphertext
+ ciphertext_fd, preamble, iv = self._consume_preamble(ciphertext_fd)
+ mac_key = _get_mac_key_for_doc(doc_info.doc_id, secret)
+ self._current_hmac = BytesIO()
+ _hmac_writer = HMACWriter(mac_key, self._current_hmac)
+ _hmac_writer.write(preamble)
self.result = result or BytesIO()
+ sym_key = _get_sym_key_for_doc(doc_info.doc_id, secret)
+ _aes = AESConsumer(sym_key, iv, self.result,
+ operation=AESConsumer.decrypt)
+ self._decrypter = VerifiedDecrypter(_aes, _hmac_writer)
- self._aes_fd = BytesIO()
- self._aes = AESDecryptor(self.sym_key, self.iv, self.result)
- self._hmac = HMACWriter(self.mac_key)
- self._hmac.write(self.preamble)
-
- self._decrypter = VerifiedDecrypter(self._aes, self._hmac)
+ self._producer = FileBodyProducer(ciphertext_fd, readSize=2**16)
- def _read_preamble(self, ciphertext):
+ def _consume_preamble(self, ciphertext_fd):
+ ciphertext_fd.seek(0)
try:
- self.preamble, ciphertext = _split(ciphertext.getvalue())
- self.doc_hmac, self.ciphertext = ciphertext[-64:], ciphertext[:-64]
+ preamble, ciphertext = _split(ciphertext_fd.getvalue())
+ self.doc_hmac, ciphertext = ciphertext[-64:], ciphertext[:-64]
except (TypeError, binascii.Error):
raise InvalidBlob
- self.ciphertext = BytesIO(self.ciphertext)
+ ciphertext_fd.close()
- if len(self.preamble) != PACMAN.size:
+ if len(preamble) != PACMAN.size:
raise InvalidBlob
try:
- unpacked_data = PACMAN.unpack(self.preamble)
+ unpacked_data = PACMAN.unpack(preamble)
pad, ts, sch, meth, iv, doc_id, rev = unpacked_data
- self.iv = iv
except struct.error:
raise InvalidBlob
+
if pad != '\x80':
raise InvalidBlob
-
# TODO check timestamp
if sch != ENC_SCHEME.symkey:
raise InvalidBlob('invalid scheme')
# TODO should adapt the assymetric-gpg too, rigth?
if meth != ENC_METHOD.aes_256_ctr:
raise InvalidBlob('invalid encryption scheme')
-
if rev != self.rev:
raise InvalidBlob('invalid revision')
+ if doc_id != self.doc_id:
+ raise InvalidBlob('invalid revision')
+ return BytesIO(ciphertext), preamble, iv
def _check_hmac(self):
- if self._hmac._hmac.digest() != self.doc_hmac:
+ if self._current_hmac.getvalue() != self.doc_hmac:
raise InvalidBlob('HMAC could not be verifed')
+ def _end_stream(self):
+ self._decrypter.end()
+ self._check_hmac()
+ return self.result.getvalue()
+
def decrypt(self):
"""
Starts producing encrypted data from the cleartext data.
@@ -354,50 +330,9 @@ class BlobDecryptor(object):
:rtype: twisted.internet.defer.Deferred
"""
d = self._producer.startProducing(self._decrypter)
- d.addCallback(lambda _: self._check_hmac())
- d.addCallback(lambda _: self.result.getvalue())
+ d.addCallback(lambda _: self._end_stream())
return d
- def decrypt_whole(self):
- ciphertext = self.ciphertext.getvalue()
- self.hmac_obj.update(ciphertext)
- self._check_hmac()
- decryptor = _get_aes_ctr_cipher(self.sym_key, self.iv).decryptor()
-
- self.result.write(decryptor.update(ciphertext))
- self.result.write(decryptor.finalize())
- return self.result
-
-
-class AESEncryptor(object):
- """
- A Twisted's Consumer implementation that takes an input file descriptor and
- applies AES-256 cipher in CTR mode.
- """
- implements(interfaces.IConsumer)
-
- def __init__(self, key, fd=None):
- if len(key) != 32:
- raise EncryptionDecryptionError('key is not 256 bits')
- self.iv = os.urandom(16)
-
- cipher = _get_aes_ctr_cipher(key, self.iv)
- self.encryptor = cipher.encryptor()
-
- self.fd = fd or BytesIO()
-
- self.done = False
-
- def write(self, data):
- encrypted = self.encryptor.update(data)
- self.fd.write(encrypted)
- return encrypted
-
- def end(self):
- if not self.done:
- self.fd.write(self.encryptor.finalize())
- self.done = True
-
class HMACWriter(object):
"""
@@ -407,15 +342,16 @@ class HMACWriter(object):
implements(interfaces.IConsumer)
hashtype = 'sha512'
- def __init__(self, key):
+ def __init__(self, key, result=None):
self._hmac = hmac.new(key, '', getattr(hashlib, self.hashtype))
- self.result = BytesIO('')
+ self.result = result or BytesIO('')
def write(self, data):
self._hmac.update(data)
def end(self):
self.result.write(self._hmac.digest())
+ return self.result.getvalue()
class VerifiedEncrypter(object):
@@ -425,13 +361,18 @@ class VerifiedEncrypter(object):
"""
implements(interfaces.IConsumer)
- def __init__(self, crypter, hmac):
+ def __init__(self, crypter, hmac_writer):
self.crypter = crypter
- self.hmac = hmac
+ self.hmac_writer = hmac_writer
def write(self, data):
enc_chunk = self.crypter.write(data)
- self.hmac.write(enc_chunk)
+ self.hmac_writer.write(enc_chunk)
+
+ def end(self):
+ ciphertext = self.crypter.end()
+ content_hmac = self.hmac_writer.end()
+ return ciphertext, content_hmac
class VerifiedDecrypter(object):
@@ -442,46 +383,53 @@ class VerifiedDecrypter(object):
"""
implements(interfaces.IConsumer)
- def __init__(self, decrypter, hmac):
+ def __init__(self, decrypter, hmac_writer):
self.decrypter = decrypter
- self.hmac = hmac
+ self.hmac_writer = hmac_writer
def write(self, enc_chunk):
- self.hmac.write(enc_chunk)
+ self.hmac_writer.write(enc_chunk)
self.decrypter.write(enc_chunk)
+ def end(self):
+ self.decrypter.end()
+ self.hmac_writer.end()
+
-class AESDecryptor(object):
+class AESConsumer(object):
"""
- A Twisted's Consumer implementation that consumes data encrypted with
- AES-256 in CTR mode from a file descriptor and generates decrypted data.
+ A Twisted's Consumer implementation that takes an input file descriptor and
+ applies AES-256 cipher in CTR mode.
"""
implements(interfaces.IConsumer)
+ encrypt = 1
+ decrypt = 2
- def __init__(self, key, iv, fd=None):
- iv = iv or os.urandom(16)
+ def __init__(self, key, iv=None, _buffer=None, operation=encrypt):
if len(key) != 32:
raise EncryptionDecryptionError('key is not 256 bits')
- if len(iv) != 16:
- raise EncryptionDecryptionError('iv is not 128 bits')
-
- cipher = _get_aes_ctr_cipher(key, iv)
- self.decryptor = cipher.decryptor()
-
- self.fd = fd or BytesIO()
- self.done = False
+ self.iv = iv or os.urandom(16)
+ self.buffer = _buffer or BytesIO()
self.deferred = defer.Deferred()
+ self.done = False
+
+ cipher = _get_aes_ctr_cipher(key, self.iv)
+ if operation == self.encrypt:
+ self.operator = cipher.encryptor()
+ else:
+ self.operator = cipher.decryptor()
def write(self, data):
- decrypted = self.decryptor.update(data)
- self.fd.write(decrypted)
- return decrypted
+ consumed = self.operator.update(data)
+ self.buffer.write(consumed)
+ return consumed
def end(self):
if not self.done:
- self.decryptor.finalize()
- self.deferred.callback(self.fd)
+ self.buffer.write(self.operator.finalize())
+ self.deferred.callback(self.buffer)
self.done = True
+ return self.buffer.getvalue()
def is_symmetrically_encrypted(doc):
@@ -525,7 +473,7 @@ def _get_sym_key_for_doc(doc_id, secret):
def _get_aes_ctr_cipher(key, iv):
- return Cipher(algorithms.AES(key), modes.CTR(iv), backend=crypto_backend)
+ return Cipher(algorithms.AES(key), modes.CTR(iv), backend=CRYPTO_BACKEND)
def _split(base64_raw_payload):
diff --git a/testing/tests/client/test_crypto.py b/testing/tests/client/test_crypto.py
index 863873f7..7643f75d 100644
--- a/testing/tests/client/test_crypto.py
+++ b/testing/tests/client/test_crypto.py
@@ -52,7 +52,7 @@ class AESTest(unittest.TestCase):
key = 'A' * 32
fd = BytesIO()
- aes = _crypto.AESEncryptor(key, fd)
+ aes = _crypto.AESConsumer(key, _buffer=fd)
iv = aes.iv
data = snowden1
@@ -78,7 +78,8 @@ class AESTest(unittest.TestCase):
ciphertext = _aes_encrypt(key, iv, data)
fd = BytesIO()
- aes = _crypto.AESDecryptor(key, iv, fd)
+ operation = _crypto.AESConsumer.decrypt
+ aes = _crypto.AESConsumer(key, iv, fd, operation)
for i in range(len(ciphertext) / block):
chunk = ciphertext[i * block:(i + 1) * block]