diff options
-rw-r--r-- | cryptography/bindings/openssl/api.py | 57 | ||||
-rw-r--r-- | cryptography/bindings/openssl/evp.py | 5 | ||||
-rw-r--r-- | cryptography/primitives/block/base.py | 60 | ||||
-rw-r--r-- | docs/primitives/symmetric-encryption.rst | 8 | ||||
-rw-r--r-- | tests/primitives/test_block.py | 55 | ||||
-rw-r--r-- | tests/primitives/utils.py | 9 |
6 files changed, 127 insertions, 67 deletions
diff --git a/cryptography/bindings/openssl/api.py b/cryptography/bindings/openssl/api.py index 7d189d62..67d73afb 100644 --- a/cryptography/bindings/openssl/api.py +++ b/cryptography/bindings/openssl/api.py @@ -136,7 +136,27 @@ class API(object): GetCipherByName("{cipher.name}-{cipher.key_size}-{mode.name}") ) - def create_block_cipher_context(self, cipher, mode): + def create_block_cipher_encrypt_context(self, cipher, mode): + ctx, evp, iv_nonce = self._create_block_cipher_context(cipher, mode) + res = self.lib.EVP_EncryptInit_ex(ctx, evp, api.ffi.NULL, cipher.key, + iv_nonce) + assert res != 0 + # We purposely disable padding here as it's handled higher up in the + # API. + self.lib.EVP_CIPHER_CTX_set_padding(ctx, 0) + return ctx + + def create_block_cipher_decrypt_context(self, cipher, mode): + ctx, evp, iv_nonce = self._create_block_cipher_context(cipher, mode) + res = self.lib.EVP_DecryptInit_ex(ctx, evp, api.ffi.NULL, cipher.key, + iv_nonce) + assert res != 0 + # We purposely disable padding here as it's handled higher up in the + # API. + self.lib.EVP_CIPHER_CTX_set_padding(ctx, 0) + return ctx + + def _create_block_cipher_context(self, cipher, mode): ctx = self.lib.EVP_CIPHER_CTX_new() ctx = self.ffi.gc(ctx, self.lib.EVP_CIPHER_CTX_free) evp_cipher = self._cipher_registry[type(cipher), type(mode)]( @@ -150,24 +170,21 @@ class API(object): else: iv_nonce = self.ffi.NULL - # TODO: Sometimes this needs to be a DecryptInit, when? - res = self.lib.EVP_EncryptInit_ex( - ctx, evp_cipher, self.ffi.NULL, cipher.key, iv_nonce - ) - assert res != 0 + return (ctx, evp_cipher, iv_nonce) - # We purposely disable padding here as it's handled higher up in the - # API. - self.lib.EVP_CIPHER_CTX_set_padding(ctx, 0) - return ctx + def update_encrypt_context(self, ctx, data): + block_size = self.lib.EVP_CIPHER_CTX_block_size(ctx) + buf = self.ffi.new("unsigned char[]", len(data) + block_size - 1) + outlen = self.ffi.new("int *") + res = self.lib.EVP_EncryptUpdate(ctx, buf, outlen, data, len(data)) + assert res != 0 + return self.ffi.buffer(buf)[:outlen[0]] - def update_encrypt_context(self, ctx, plaintext): + def update_decrypt_context(self, ctx, data): block_size = self.lib.EVP_CIPHER_CTX_block_size(ctx) - buf = self.ffi.new("unsigned char[]", len(plaintext) + block_size - 1) + buf = self.ffi.new("unsigned char[]", len(data) + block_size - 1) outlen = self.ffi.new("int *") - res = self.lib.EVP_EncryptUpdate( - ctx, buf, outlen, plaintext, len(plaintext) - ) + res = self.lib.EVP_DecryptUpdate(ctx, buf, outlen, data, len(data)) assert res != 0 return self.ffi.buffer(buf)[:outlen[0]] @@ -181,6 +198,16 @@ class API(object): assert res == 1 return self.ffi.buffer(buf)[:outlen[0]] + def finalize_decrypt_context(self, ctx): + block_size = self.lib.EVP_CIPHER_CTX_block_size(ctx) + buf = self.ffi.new("unsigned char[]", block_size) + outlen = self.ffi.new("int *") + res = self.lib.EVP_DecryptFinal_ex(ctx, buf, outlen) + assert res != 0 + res = self.lib.EVP_CIPHER_CTX_cleanup(ctx) + assert res == 1 + return self.ffi.buffer(buf)[:outlen[0]] + def supports_hash(self, hash_cls): return (self.ffi.NULL != self.lib.EVP_get_digestbyname(hash_cls.name.encode("ascii"))) diff --git a/cryptography/bindings/openssl/evp.py b/cryptography/bindings/openssl/evp.py index 2bb5b0f7..41df1056 100644 --- a/cryptography/bindings/openssl/evp.py +++ b/cryptography/bindings/openssl/evp.py @@ -41,6 +41,11 @@ int EVP_CIPHER_CTX_set_padding(EVP_CIPHER_CTX *, int); int EVP_EncryptUpdate(EVP_CIPHER_CTX *, unsigned char *, int *, const unsigned char *, int); int EVP_EncryptFinal_ex(EVP_CIPHER_CTX *, unsigned char *, int *); +int EVP_DecryptInit_ex(EVP_CIPHER_CTX *, const EVP_CIPHER *, ENGINE *, + const unsigned char *, const unsigned char *); +int EVP_DecryptUpdate(EVP_CIPHER_CTX *, unsigned char *, int *, + const unsigned char *, int); +int EVP_DecryptFinal_ex(EVP_CIPHER_CTX *, unsigned char *, int *); int EVP_CIPHER_CTX_cleanup(EVP_CIPHER_CTX *); const EVP_CIPHER *EVP_CIPHER_CTX_cipher(const EVP_CIPHER_CTX *); int EVP_CIPHER_block_size(const EVP_CIPHER *); diff --git a/cryptography/primitives/block/base.py b/cryptography/primitives/block/base.py index 42c1f799..e625dc7c 100644 --- a/cryptography/primitives/block/base.py +++ b/cryptography/primitives/block/base.py @@ -13,13 +13,6 @@ from __future__ import absolute_import, division, print_function -from enum import Enum - - -class _Operation(Enum): - encrypt = 0 - decrypt = 1 - class BlockCipher(object): def __init__(self, cipher, mode, api=None): @@ -31,30 +24,47 @@ class BlockCipher(object): self.cipher = cipher self.mode = mode self._api = api - self._ctx = api.create_block_cipher_context(cipher, mode) - self._operation = None - def encrypt(self, plaintext): - if self._ctx is None: - raise ValueError("BlockCipher was already finalized") + def encryptor(self): + return _BlockCipherEncryptionContext(self.cipher, self.mode, self._api) - if self._operation is None: - self._operation = _Operation.encrypt - elif self._operation is not _Operation.encrypt: - raise ValueError("BlockCipher cannot encrypt when the operation is" - " set to %s" % self._operation.name) + def decryptor(self): + return _BlockCipherDecryptionContext(self.cipher, self.mode, self._api) - return self._api.update_encrypt_context(self._ctx, plaintext) + +class _BlockCipherEncryptionContext(object): + def __init__(self, cipher, mode, api): + super(_BlockCipherEncryptionContext, self).__init__() + self._api = api + self._ctx = self._api.create_block_cipher_encrypt_context(cipher, mode) + + def update(self, data): + if self._ctx is None: + raise ValueError("Context was already finalized") + return self._api.update_encrypt_context(self._ctx, data) def finalize(self): if self._ctx is None: - raise ValueError("BlockCipher was already finalized") + raise ValueError("Context was already finalized") + data = self._api.finalize_encrypt_context(self._ctx) + self._ctx = None + return data + - if self._operation is _Operation.encrypt: - result = self._api.finalize_encrypt_context(self._ctx) - else: - raise ValueError("BlockCipher cannot finalize the unknown " - "operation %s" % self._operation.name) +class _BlockCipherDecryptionContext(object): + def __init__(self, cipher, mode, api): + super(_BlockCipherDecryptionContext, self).__init__() + self._api = api + self._ctx = self._api.create_block_cipher_decrypt_context(cipher, mode) + def update(self, data): + if self._ctx is None: + raise ValueError("Context was already finalized") + return self._api.update_decrypt_context(self._ctx, data) + + def finalize(self): + if self._ctx is None: + raise ValueError("Context was already finalized") + data = self._api.finalize_decrypt_context(self._ctx) self._ctx = None - return result + return data diff --git a/docs/primitives/symmetric-encryption.rst b/docs/primitives/symmetric-encryption.rst index 7899e67d..a8d9485d 100644 --- a/docs/primitives/symmetric-encryption.rst +++ b/docs/primitives/symmetric-encryption.rst @@ -15,13 +15,17 @@ where the encrypter and decrypter both use the same key. Block ciphers work by encrypting content in chunks, often 64- or 128-bits. They combine an underlying algorithm (such as AES), with a mode (such as - CBC, CTR, or GCM). A simple example of encrypting content with AES is: + CBC, CTR, or GCM). A simple example of encrypting (and then decrypting) + content with AES is: .. doctest:: >>> from cryptography.primitives.block import BlockCipher, ciphers, modes >>> cipher = BlockCipher(ciphers.AES(key), modes.CBC(iv)) - >>> cipher.encrypt(b"a secret message") + cipher.finalize() + >>> encrypt = cipher.encryptor() + >>> ct = encrypt.update(b"a secret message") + encrypt.finalize() + >>> decrypt = cipher.decryptor() + >>> decrypt.update(ct) + decrypt.finalize() '...' :param cipher: One of the ciphers described below. diff --git a/tests/primitives/test_block.py b/tests/primitives/test_block.py index 9f5905bf..8e429085 100644 --- a/tests/primitives/test_block.py +++ b/tests/primitives/test_block.py @@ -15,11 +15,9 @@ from __future__ import absolute_import, division, print_function import binascii -import pretend import pytest from cryptography.primitives.block import BlockCipher, ciphers, modes -from cryptography.primitives.block.base import _Operation class TestBlockCipher(object): @@ -29,40 +27,42 @@ class TestBlockCipher(object): modes.CBC(binascii.unhexlify(b"0" * 32)) ) - def test_use_after_finalize(self, api): + def test_creates_encryptor(self): cipher = BlockCipher( ciphers.AES(binascii.unhexlify(b"0" * 32)), - modes.CBC(binascii.unhexlify(b"0" * 32)), - api + modes.CBC(binascii.unhexlify(b"0" * 32)) ) - cipher.encrypt(b"a" * 16) - cipher.finalize() - with pytest.raises(ValueError): - cipher.encrypt(b"b" * 16) - with pytest.raises(ValueError): - cipher.finalize() + assert cipher.encryptor() is not None - def test_encrypt_with_invalid_operation(self, api): + def test_creates_decryptor(self): cipher = BlockCipher( ciphers.AES(binascii.unhexlify(b"0" * 32)), - modes.CBC(binascii.unhexlify(b"0" * 32)), - api + modes.CBC(binascii.unhexlify(b"0" * 32)) ) - cipher._operation = _Operation.decrypt + assert cipher.decryptor() is not None - with pytest.raises(ValueError): - cipher.encrypt(b"b" * 16) - def test_finalize_with_invalid_operation(self, api): +class TestBlockCipherContext(object): + def test_use_after_finalize(self, api): cipher = BlockCipher( ciphers.AES(binascii.unhexlify(b"0" * 32)), modes.CBC(binascii.unhexlify(b"0" * 32)), api ) - cipher._operation = pretend.stub(name="wat") - + encryptor = cipher.encryptor() + encryptor.update(b"a" * 16) + encryptor.finalize() + with pytest.raises(ValueError): + encryptor.update(b"b" * 16) + with pytest.raises(ValueError): + encryptor.finalize() + decryptor = cipher.decryptor() + decryptor.update(b"a" * 16) + decryptor.finalize() + with pytest.raises(ValueError): + decryptor.update(b"b" * 16) with pytest.raises(ValueError): - cipher.finalize() + decryptor.finalize() def test_unaligned_block_encryption(self, api): cipher = BlockCipher( @@ -70,7 +70,16 @@ class TestBlockCipher(object): modes.ECB(), api ) - ct = cipher.encrypt(b"a" * 15) + encryptor = cipher.encryptor() + ct = encryptor.update(b"a" * 15) assert ct == b"" - ct += cipher.encrypt(b"a" * 65) + ct += encryptor.update(b"a" * 65) assert len(ct) == 80 + ct += encryptor.finalize() + decryptor = cipher.decryptor() + pt = decryptor.update(ct[:3]) + assert pt == b"" + pt += decryptor.update(ct[3:]) + assert len(pt) == 80 + assert pt == b"a" * 80 + decryptor.finalize() diff --git a/tests/primitives/utils.py b/tests/primitives/utils.py index a3759b03..70ece52a 100644 --- a/tests/primitives/utils.py +++ b/tests/primitives/utils.py @@ -37,9 +37,14 @@ def encrypt_test(api, cipher_factory, mode_factory, params, only_if, mode_factory(**params), api ) - actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext)) - actual_ciphertext += cipher.finalize() + encryptor = cipher.encryptor() + actual_ciphertext = encryptor.update(binascii.unhexlify(plaintext)) + actual_ciphertext += encryptor.finalize() assert actual_ciphertext == binascii.unhexlify(ciphertext) + decryptor = cipher.decryptor() + actual_plaintext = decryptor.update(binascii.unhexlify(ciphertext)) + actual_plaintext += decryptor.finalize() + assert actual_plaintext == binascii.unhexlify(plaintext) def generate_hash_test(param_loader, path, file_names, hash_cls, |