diff options
-rw-r--r-- | cryptography/bindings/openssl/api.py | 90 | ||||
-rw-r--r-- | cryptography/primitives/__init__.py | 0 | ||||
-rw-r--r-- | cryptography/primitives/block/__init__.py | 6 | ||||
-rw-r--r-- | cryptography/primitives/block/base.py | 62 | ||||
-rw-r--r-- | cryptography/primitives/block/ciphers.py | 24 | ||||
-rw-r--r-- | cryptography/primitives/block/modes.py | 20 | ||||
-rw-r--r-- | docs/primitives/symmetric-encryption.rst | 8 | ||||
-rw-r--r-- | setup.py | 9 | ||||
-rw-r--r-- | tests/primitives/__init__.py | 0 | ||||
-rw-r--r-- | tests/primitives/test_block.py | 61 | ||||
-rw-r--r-- | tests/primitives/test_ciphers.py | 29 | ||||
-rw-r--r-- | tests/primitives/test_nist.py | 196 | ||||
-rw-r--r-- | tox.ini | 4 |
13 files changed, 502 insertions, 7 deletions
diff --git a/cryptography/bindings/openssl/api.py b/cryptography/bindings/openssl/api.py index 3cc6a0e9..720a6ad4 100644 --- a/cryptography/bindings/openssl/api.py +++ b/cryptography/bindings/openssl/api.py @@ -11,7 +11,18 @@ # See the License for the specific language governing permissions and # limitations under the License. -from __future__ import absolute_import, division, print_function +import cffi + + +class OpenSSLError(Exception): + def __init__(self, api): + e = api._lib.ERR_get_error() + if e == 0: + raise SystemError("Tried to create an OpenSSLError when there was " + "None") + msg = api._ffi.new("char[]", 120) + api._lib.ERR_error_string(e, msg) + super(OpenSSLError, self).__init__(api._ffi.string(msg)) class API(object): @@ -19,5 +30,82 @@ class API(object): OpenSSL API wrapper. """ + def __init__(self): + ffi = cffi.FFI() + self._populate_ffi(ffi) + self._ffi = ffi + self._lib = ffi.verify(""" + #include <openssl/evp.h> + """) + self._lib.OpenSSL_add_all_algorithms() + self._lib.ERR_load_crypto_strings() + + def _populate_ffi(self, ffi): + ffi.cdef(""" + typedef struct { + ...; + } EVP_CIPHER_CTX; + typedef ... EVP_CIPHER; + typedef ... ENGINE; + + void OpenSSL_add_all_algorithms(); + void ERR_load_crypto_strings(); + + const EVP_CIPHER *EVP_get_cipherbyname(const char *); + int EVP_EncryptInit_ex(EVP_CIPHER_CTX *, const EVP_CIPHER *, + ENGINE *, unsigned char *, unsigned char *); + int EVP_CIPHER_CTX_set_padding(EVP_CIPHER_CTX *, int); + int EVP_EncryptUpdate(EVP_CIPHER_CTX *, unsigned char *, int *, + unsigned char *, int); + int EVP_EncryptFinal_ex(EVP_CIPHER_CTX *, unsigned char *, int *); + int EVP_CIPHER_CTX_cleanup(EVP_CIPHER_CTX *); + + unsigned long ERR_get_error(); + """) + + def create_block_cipher_context(self, cipher, mode): + ctx = self._ffi.new("EVP_CIPHER_CTX *") + ctx = self._ffi.gc(ctx, self._lib.EVP_CIPHER_CTX_cleanup) + # TODO: compute name using a better algorithm + ciphername = "{0}-{1}-{2}".format( + cipher.name, cipher.key_size, mode.name + ) + evp_cipher = self._lib.EVP_get_cipherbyname(ciphername.encode("ascii")) + if evp_cipher == self._ffi.NULL: + raise OpenSSLError(self) + # TODO: only use the key and initialization_vector as needed. Sometimes + # this needs to be a DecryptInit, when? + res = self._lib.EVP_EncryptInit_ex( + ctx, evp_cipher, self._ffi.NULL, cipher.key, + mode.initialization_vector + ) + if res == 0: + raise OpenSSLError(self) + # TODO: this should depend on mode.padding + self._lib.EVP_CIPHER_CTX_set_padding(ctx, 0) + return ctx + + def update_encrypt_context(self, ctx, plaintext): + buf = self._ffi.new("unsigned char[]", len(plaintext)) + outlen = self._ffi.new("int *") + res = self._lib.EVP_EncryptUpdate( + ctx, buf, outlen, plaintext, len(plaintext) + ) + if res == 0: + raise OpenSSLError(self) + return self._ffi.buffer(buf)[:outlen[0]] + + def finalize_encrypt_context(self, ctx): + # TODO: use real block size + buf = self._ffi.new("unsigned char[]", 16) + outlen = self._ffi.new("int *") + res = self._lib.EVP_EncryptFinal_ex(ctx, buf, outlen) + if res == 0: + raise OpenSSLError(self) + res = self._lib.EVP_CIPHER_CTX_cleanup(ctx) + if res == 0: + raise OpenSSLError(self) + return self._ffi.buffer(buf)[:outlen[0]] + api = API() diff --git a/cryptography/primitives/__init__.py b/cryptography/primitives/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/cryptography/primitives/__init__.py diff --git a/cryptography/primitives/block/__init__.py b/cryptography/primitives/block/__init__.py new file mode 100644 index 00000000..e49fcf38 --- /dev/null +++ b/cryptography/primitives/block/__init__.py @@ -0,0 +1,6 @@ +from cryptography.primitives.block.base import BlockCipher + + +__all__ = [ + "BlockCipher", +] diff --git a/cryptography/primitives/block/base.py b/cryptography/primitives/block/base.py new file mode 100644 index 00000000..2018138c --- /dev/null +++ b/cryptography/primitives/block/base.py @@ -0,0 +1,62 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# TODO: which binding is used should be an option somewhere +from enum import Enum + +from cryptography.bindings.openssl import api + + +class _Operation(Enum): + encrypt = "encrypt" + decrypt = "decrypt" + + +class BlockCipher(object): + def __init__(self, cipher, mode): + super(BlockCipher, self).__init__() + self.cipher = cipher + self.mode = mode + self._ctx = api.create_block_cipher_context(cipher, mode) + self._operation = None + + @property + def name(self): + return "{0}-{1}-{2}".format( + self.cipher.name, self.cipher.key_size, self.mode.name, + ) + + def encrypt(self, plaintext): + if self._ctx is None: + raise ValueError("BlockCipher was already finalized") + + 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) + + return api.update_encrypt_context(self._ctx, plaintext) + + def finalize(self): + if self._ctx is None: + raise ValueError("BlockCipher was already finalized") + + if self._operation is _Operation.encrypt: + result = api.finalize_encrypt_context(self._ctx) + else: + raise ValueError("BlockCipher cannot finalize the unknown " + "operation %s" % self._operation.name) + + self._ctx = None + return result diff --git a/cryptography/primitives/block/ciphers.py b/cryptography/primitives/block/ciphers.py new file mode 100644 index 00000000..f40fc2a1 --- /dev/null +++ b/cryptography/primitives/block/ciphers.py @@ -0,0 +1,24 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class AES(object): + name = "AES" + + def __init__(self, key): + super(AES, self).__init__() + self.key = key + + @property + def key_size(self): + return len(self.key) * 8 diff --git a/cryptography/primitives/block/modes.py b/cryptography/primitives/block/modes.py new file mode 100644 index 00000000..02f0219c --- /dev/null +++ b/cryptography/primitives/block/modes.py @@ -0,0 +1,20 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +class CBC(object): + name = "CBC" + + def __init__(self, initialization_vector): + super(CBC, self).__init__() + self.initialization_vector = initialization_vector diff --git a/docs/primitives/symmetric-encryption.rst b/docs/primitives/symmetric-encryption.rst index 821e8e0f..05aea0fa 100644 --- a/docs/primitives/symmetric-encryption.rst +++ b/docs/primitives/symmetric-encryption.rst @@ -12,8 +12,8 @@ where the encrypter and decrypter both use the same key. .. code-block:: pycon - >>> from cryptography.primitives.block import BlockCipher, cipher, mode, padding - >>> cipher = BlockCipher(cipher.AES(key), mode.CBC(iv, padding.PKCS7())) + >>> from cryptography.primitives.block import BlockCipher, ciphers, modes + >>> cipher = BlockCipher(cipher.AES(key), mode.CBC(iv)) >>> cipher.encrypt("my secret message") + cipher.finalize() # The ciphertext [...] @@ -36,7 +36,7 @@ where the encrypter and decrypter both use the same key. Ciphers ~~~~~~~ -.. class:: cryptography.primitives.block.cipher.AES(key) +.. class:: cryptography.primitives.block.ciphers.AES(key) AES (Advanced Encryption Standard) is a block cipher standardized by NIST. AES is both fast, and cryptographically strong. It is a good default @@ -49,7 +49,7 @@ Ciphers Modes ~~~~~ -.. class:: cryptography.primitives.block.mode.CBC(initialization_vector, padding) +.. class:: cryptography.primitives.block.modes.CBC(initialization_vector) CBC (Cipher block chaining) is a mode of operation for block ciphers. It is considered cryptographically strong. @@ -10,12 +10,19 @@ # implied. # See the License for the specific language governing permissions and # limitations under the License. +import sys from setuptools import setup +install_requires = [ + "cffi>=0.6", +] + +if sys.version_info[:2] < (3, 4): + install_requires += ["enum34"] setup( name="cryptography", license="Apache License, Version 2.0", - install_requires=["cffi>=0.6"], + install_requires=install_requires, ) diff --git a/tests/primitives/__init__.py b/tests/primitives/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/tests/primitives/__init__.py diff --git a/tests/primitives/test_block.py b/tests/primitives/test_block.py new file mode 100644 index 00000000..9059a886 --- /dev/null +++ b/tests/primitives/test_block.py @@ -0,0 +1,61 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import binascii + +import pretend +import pytest + +from cryptography.primitives.block import BlockCipher, ciphers, modes +from cryptography.primitives.block.base import _Operation + + +class TestBlockCipher(object): + def test_cipher_name(self): + cipher = BlockCipher( + ciphers.AES(binascii.unhexlify(b"0" * 32)), + modes.CBC(binascii.unhexlify(b"0" * 32)) + ) + assert cipher.name == "AES-128-CBC" + + def test_use_after_finalize(self): + cipher = BlockCipher( + ciphers.AES(binascii.unhexlify(b"0" * 32)), + 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() + + def test_encrypt_with_invalid_operation(self): + cipher = BlockCipher( + ciphers.AES(binascii.unhexlify(b"0" * 32)), + modes.CBC(binascii.unhexlify(b"0" * 32)) + ) + cipher._operation = _Operation.decrypt + + with pytest.raises(ValueError): + cipher.encrypt(b"b" * 16) + + def test_finalize_with_invalid_operation(self): + cipher = BlockCipher( + ciphers.AES(binascii.unhexlify(b"0" * 32)), + modes.CBC(binascii.unhexlify(b"0" * 32)) + ) + cipher._operation = pretend.stub(name="wat") + + with pytest.raises(ValueError): + cipher.encrypt(b"b" * 16) diff --git a/tests/primitives/test_ciphers.py b/tests/primitives/test_ciphers.py new file mode 100644 index 00000000..31b4275e --- /dev/null +++ b/tests/primitives/test_ciphers.py @@ -0,0 +1,29 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import binascii + +import pytest + +from cryptography.primitives.block.ciphers import AES + + +class TestAES(object): + @pytest.mark.parametrize(("key", "keysize"), [ + (b"0" * 32, 128), + (b"0" * 48, 192), + (b"0" * 64, 256), + ]) + def test_key_size(self, key, keysize): + cipher = AES(binascii.unhexlify(key)) + assert cipher.key_size == keysize diff --git a/tests/primitives/test_nist.py b/tests/primitives/test_nist.py new file mode 100644 index 00000000..dc265435 --- /dev/null +++ b/tests/primitives/test_nist.py @@ -0,0 +1,196 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +Test using the NIST Test Vectors +""" +import binascii +import os + +import pytest + +from cryptography.primitives.block import BlockCipher, ciphers, modes + +from ..utils import load_nist_vectors_from_file + + +def parameterize_kat_encrypt(fname): + return pytest.mark.parametrize(("key", "iv", "plaintext", "ciphertext"), + load_nist_vectors_from_file( + os.path.join("AES/KAT/", fname), + "ENCRYPT", + ["key", "iv", "plaintext", "ciphertext"], + ), + ) + + +def paramterize_mmt_encrypt(fname): + return pytest.mark.parametrize(("key", "iv", "plaintext", "ciphertext"), + load_nist_vectors_from_file( + os.path.join("AES/MMT/", fname), + "ENCRYPT", + ["key", "iv", "plaintext", "ciphertext"], + ) + ) + + +class TestAES_CBC(object): + @parameterize_kat_encrypt("CBCGFSbox128.rsp") + def test_KAT_GFSbox_128_encrypt(self, key, iv, plaintext, ciphertext): + cipher = BlockCipher( + ciphers.AES(binascii.unhexlify(key)), + modes.CBC(binascii.unhexlify(iv)), + ) + actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext)) + actual_ciphertext += cipher.finalize() + assert binascii.hexlify(actual_ciphertext) == ciphertext + + @parameterize_kat_encrypt("CBCGFSbox192.rsp") + def test_KAT_GFSbox_192_encrypt(self, key, iv, plaintext, ciphertext): + cipher = BlockCipher( + ciphers.AES(binascii.unhexlify(key)), + modes.CBC(binascii.unhexlify(iv)), + ) + actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext)) + actual_ciphertext += cipher.finalize() + assert binascii.hexlify(actual_ciphertext) == ciphertext + + @parameterize_kat_encrypt("CBCGFSbox256.rsp") + def test_KAT_GFSbox_256_encrypt(self, key, iv, plaintext, ciphertext): + cipher = BlockCipher( + ciphers.AES(binascii.unhexlify(key)), + modes.CBC(binascii.unhexlify(iv)), + ) + actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext)) + actual_ciphertext += cipher.finalize() + assert binascii.hexlify(actual_ciphertext) == ciphertext + + @parameterize_kat_encrypt("CBCKeySbox128.rsp") + def test_KAT_KeySbox_128_encrypt(self, key, iv, plaintext, ciphertext): + cipher = BlockCipher( + ciphers.AES(binascii.unhexlify(key)), + modes.CBC(binascii.unhexlify(iv)), + ) + actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext)) + actual_ciphertext += cipher.finalize() + assert binascii.hexlify(actual_ciphertext) == ciphertext + + @parameterize_kat_encrypt("CBCKeySbox192.rsp") + def test_KAT_KeySbox_192_encrypt(self, key, iv, plaintext, ciphertext): + cipher = BlockCipher( + ciphers.AES(binascii.unhexlify(key)), + modes.CBC(binascii.unhexlify(iv)), + ) + actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext)) + actual_ciphertext += cipher.finalize() + assert binascii.hexlify(actual_ciphertext) == ciphertext + + @parameterize_kat_encrypt("CBCKeySbox256.rsp") + def test_KAT_KeySbox_256_encrypt(self, key, iv, plaintext, ciphertext): + cipher = BlockCipher( + ciphers.AES(binascii.unhexlify(key)), + modes.CBC(binascii.unhexlify(iv)), + ) + actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext)) + actual_ciphertext += cipher.finalize() + assert binascii.hexlify(actual_ciphertext) == ciphertext + + @parameterize_kat_encrypt("CBCVarKey128.rsp") + def test_KAT_VarKey_128_encrypt(self, key, iv, plaintext, ciphertext): + cipher = BlockCipher( + ciphers.AES(binascii.unhexlify(key)), + modes.CBC(binascii.unhexlify(iv)), + ) + actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext)) + actual_ciphertext += cipher.finalize() + assert binascii.hexlify(actual_ciphertext) == ciphertext + + @parameterize_kat_encrypt("CBCVarKey192.rsp") + def test_KAT_VarKey_192_encrypt(self, key, iv, plaintext, ciphertext): + cipher = BlockCipher( + ciphers.AES(binascii.unhexlify(key)), + modes.CBC(binascii.unhexlify(iv)), + ) + actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext)) + actual_ciphertext += cipher.finalize() + assert binascii.hexlify(actual_ciphertext) == ciphertext + + @parameterize_kat_encrypt("CBCVarKey256.rsp") + def test_KAT_VarKey_256_encrypt(self, key, iv, plaintext, ciphertext): + cipher = BlockCipher( + ciphers.AES(binascii.unhexlify(key)), + modes.CBC(binascii.unhexlify(iv)), + ) + actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext)) + actual_ciphertext += cipher.finalize() + assert binascii.hexlify(actual_ciphertext) == ciphertext + + @parameterize_kat_encrypt("CBCVarTxt128.rsp") + def test_KAT_VarTxt_128_encrypt(self, key, iv, plaintext, ciphertext): + cipher = BlockCipher( + ciphers.AES(binascii.unhexlify(key)), + modes.CBC(binascii.unhexlify(iv)), + ) + actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext)) + actual_ciphertext += cipher.finalize() + assert binascii.hexlify(actual_ciphertext) == ciphertext + + @parameterize_kat_encrypt("CBCVarTxt192.rsp") + def test_KAT_VarTxt_192_encrypt(self, key, iv, plaintext, ciphertext): + cipher = BlockCipher( + ciphers.AES(binascii.unhexlify(key)), + modes.CBC(binascii.unhexlify(iv)), + ) + actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext)) + actual_ciphertext += cipher.finalize() + assert binascii.hexlify(actual_ciphertext) == ciphertext + + @parameterize_kat_encrypt("CBCVarTxt256.rsp") + def test_KAT_VarTxt_256_encrypt(self, key, iv, plaintext, ciphertext): + cipher = BlockCipher( + ciphers.AES(binascii.unhexlify(key)), + modes.CBC(binascii.unhexlify(iv)), + ) + actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext)) + actual_ciphertext += cipher.finalize() + assert binascii.hexlify(actual_ciphertext) == ciphertext + + @paramterize_mmt_encrypt("CBCMMT128.rsp") + def test_MMT_128_encrypt(self, key, iv, plaintext, ciphertext): + cipher = BlockCipher( + ciphers.AES(binascii.unhexlify(key)), + modes.CBC(binascii.unhexlify(iv)), + ) + actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext)) + actual_ciphertext += cipher.finalize() + assert binascii.hexlify(actual_ciphertext) == ciphertext + + @paramterize_mmt_encrypt("CBCMMT192.rsp") + def test_MMT_192_encrypt(self, key, iv, plaintext, ciphertext): + cipher = BlockCipher( + ciphers.AES(binascii.unhexlify(key)), + modes.CBC(binascii.unhexlify(iv)), + ) + actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext)) + actual_ciphertext += cipher.finalize() + assert binascii.hexlify(actual_ciphertext) == ciphertext + + @paramterize_mmt_encrypt("CBCMMT256.rsp") + def test_MMT_256_encrypt(self, key, iv, plaintext, ciphertext): + cipher = BlockCipher( + ciphers.AES(binascii.unhexlify(key)), + modes.CBC(binascii.unhexlify(iv)), + ) + actual_ciphertext = cipher.encrypt(binascii.unhexlify(plaintext)) + actual_ciphertext += cipher.finalize() + assert binascii.hexlify(actual_ciphertext) == ciphertext @@ -2,7 +2,9 @@ envlist = py26,py27,pypy,py32,py33,docs,pep8 [testenv] -deps = pytest-cov +deps = + pytest-cov + pretend commands = py.test --cov=cryptography/ --cov=tests/ [testenv:docs] |