diff options
| -rw-r--r-- | client/src/leap/soledad/client/_crypto.py | 228 | ||||
| -rw-r--r-- | testing/tests/client/test_crypto.py | 5 | 
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] | 
