Repository: pyca/cryptography

Issue:
Add Support for CAST5 (CAST-128) CBC/CFB/OFB
OpenSSL supports these modes, but we have disabled them in our backend due to a lack of test vectors.

If we still can't find any we should consider generating some against OpenSSL's implementation and documenting their provenance. We can use them for validation as we expand our backend support.



Discussion:
There are NESSIE and RFC test vectors. I guess you want ones longer than a single block though?

For non-ECB? Obviously more than single block would be best but just demonstrating that the IV is xored properly in CBC/CFB/OFB modes would be good.


Resolving patch:
diff --git a/cryptography/hazmat/backends/commoncrypto/backend.py b/cryptography/hazmat/backends/commoncrypto/backend.py
--- a/cryptography/hazmat/backends/commoncrypto/backend.py
+++ b/cryptography/hazmat/backends/commoncrypto/backend.py
@@ -25,7 +25,7 @@
 from cryptography.hazmat.bindings.commoncrypto.binding import Binding
 from cryptography.hazmat.primitives import interfaces, constant_time
 from cryptography.hazmat.primitives.ciphers.algorithms import (
-    AES, Blowfish, TripleDES, ARC4
+    AES, Blowfish, TripleDES, ARC4, CAST5
 )
 from cryptography.hazmat.primitives.ciphers.modes import (
     CBC, CTR, ECB, OFB, CFB, GCM
@@ -198,6 +198,18 @@ def _register_default_ciphers(self):
                 mode_cls,
                 mode_const
             )
+        for mode_cls, mode_const in [
+            (CBC, self._lib.kCCModeCBC),
+            (ECB, self._lib.kCCModeECB),
+            (CFB, self._lib.kCCModeCFB),
+            (OFB, self._lib.kCCModeOFB)
+        ]:
+            self._register_cipher_adapter(
+                CAST5,
+                self._lib.kCCAlgorithmCAST,
+                mode_cls,
+                mode_const
+            )
         self._register_cipher_adapter(
             ARC4,
             self._lib.kCCAlgorithmRC4,
diff --git a/cryptography/hazmat/backends/openssl/backend.py b/cryptography/hazmat/backends/openssl/backend.py
--- a/cryptography/hazmat/backends/openssl/backend.py
+++ b/cryptography/hazmat/backends/openssl/backend.py
@@ -26,7 +26,7 @@
 from cryptography.hazmat.primitives import interfaces, hashes
 from cryptography.hazmat.primitives.asymmetric import rsa
 from cryptography.hazmat.primitives.ciphers.algorithms import (
-    AES, Blowfish, Camellia, TripleDES, ARC4,
+    AES, Blowfish, Camellia, TripleDES, ARC4, CAST5
 )
 from cryptography.hazmat.primitives.ciphers.modes import (
     CBC, CTR, ECB, OFB, CFB, GCM,
@@ -153,6 +153,12 @@ def _register_default_ciphers(self):
                 mode_cls,
                 GetCipherByName("bf-{mode.name}")
             )
+        for mode_cls in [CBC, CFB, OFB, ECB]:
+            self.register_cipher_adapter(
+                CAST5,
+                mode_cls,
+                GetCipherByName("cast5-{mode.name}")
+            )
         self.register_cipher_adapter(
             ARC4,
             type(None),
diff --git a/cryptography/hazmat/primitives/ciphers/algorithms.py b/cryptography/hazmat/primitives/ciphers/algorithms.py
--- a/cryptography/hazmat/primitives/ciphers/algorithms.py
+++ b/cryptography/hazmat/primitives/ciphers/algorithms.py
@@ -90,6 +90,21 @@ def key_size(self):
         return len(self.key) * 8
 
 
+@utils.register_interface(interfaces.BlockCipherAlgorithm)
+@utils.register_interface(interfaces.CipherAlgorithm)
+class CAST5(object):
+    name = "CAST5"
+    block_size = 64
+    key_sizes = frozenset(range(40, 129, 8))
+
+    def __init__(self, key):
+        self.key = _verify_key_size(self, key)
+
+    @property
+    def key_size(self):
+        return len(self.key) * 8
+
+
 @utils.register_interface(interfaces.CipherAlgorithm)
 class ARC4(object):
     name = "RC4"


Test patch:
diff --git a/tests/hazmat/primitives/test_cast5.py b/tests/hazmat/primitives/test_cast5.py
new file mode 100644
--- /dev/null
+++ b/tests/hazmat/primitives/test_cast5.py
@@ -0,0 +1,92 @@
+# 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.
+
+from __future__ import absolute_import, division, print_function
+
+import binascii
+import os
+
+import pytest
+
+from cryptography.hazmat.primitives.ciphers import algorithms, modes
+
+from .utils import generate_encrypt_test
+from ...utils import load_nist_vectors
+
+
+@pytest.mark.supported(
+    only_if=lambda backend: backend.cipher_supported(
+        algorithms.CAST5("\x00" * 16), modes.ECB()
+    ),
+    skip_message="Does not support CAST5 ECB",
+)
+@pytest.mark.cipher
+class TestCAST5_ECB(object):
+    test_ECB = generate_encrypt_test(
+        load_nist_vectors,
+        os.path.join("ciphers", "CAST5"),
+        ["cast5-ecb.txt"],
+        lambda key, **kwargs: algorithms.CAST5(binascii.unhexlify((key))),
+        lambda **kwargs: modes.ECB(),
+    )
+
+
+@pytest.mark.supported(
+    only_if=lambda backend: backend.cipher_supported(
+        algorithms.CAST5("\x00" * 16), modes.CBC("\x00" * 8)
+    ),
+    skip_message="Does not support CAST5 CBC",
+)
+@pytest.mark.cipher
+class TestCAST5_CBC(object):
+    test_CBC = generate_encrypt_test(
+        load_nist_vectors,
+        os.path.join("ciphers", "CAST5"),
+        ["cast5-cbc.txt"],
+        lambda key, **kwargs: algorithms.CAST5(binascii.unhexlify((key))),
+        lambda iv, **kwargs: modes.CBC(binascii.unhexlify(iv))
+    )
+
+
+@pytest.mark.supported(
+    only_if=lambda backend: backend.cipher_supported(
+        algorithms.CAST5("\x00" * 16), modes.OFB("\x00" * 8)
+    ),
+    skip_message="Does not support CAST5 OFB",
+)
+@pytest.mark.cipher
+class TestCAST5_OFB(object):
+    test_OFB = generate_encrypt_test(
+        load_nist_vectors,
+        os.path.join("ciphers", "CAST5"),
+        ["cast5-ofb.txt"],
+        lambda key, **kwargs: algorithms.CAST5(binascii.unhexlify((key))),
+        lambda iv, **kwargs: modes.OFB(binascii.unhexlify(iv))
+    )
+
+
+@pytest.mark.supported(
+    only_if=lambda backend: backend.cipher_supported(
+        algorithms.CAST5("\x00" * 16), modes.CFB("\x00" * 8)
+    ),
+    skip_message="Does not support CAST5 CFB",
+)
+@pytest.mark.cipher
+class TestCAST5_CFB(object):
+    test_CFB = generate_encrypt_test(
+        load_nist_vectors,
+        os.path.join("ciphers", "CAST5"),
+        ["cast5-cfb.txt"],
+        lambda key, **kwargs: algorithms.CAST5(binascii.unhexlify((key))),
+        lambda iv, **kwargs: modes.CFB(binascii.unhexlify(iv))
+    )
diff --git a/tests/hazmat/primitives/test_ciphers.py b/tests/hazmat/primitives/test_ciphers.py
--- a/tests/hazmat/primitives/test_ciphers.py
+++ b/tests/hazmat/primitives/test_ciphers.py
@@ -18,7 +18,7 @@
 import pytest
 
 from cryptography.hazmat.primitives.ciphers.algorithms import (
-    AES, Camellia, TripleDES, Blowfish, ARC4
+    AES, Camellia, TripleDES, Blowfish, ARC4, CAST5
 )
 
 
@@ -80,6 +80,19 @@ def test_invalid_key_size(self):
             Blowfish(binascii.unhexlify(b"0" * 6))
 
 
+class TestCAST5(object):
+    @pytest.mark.parametrize(("key", "keysize"), [
+        (b"0" * (keysize // 4), keysize) for keysize in range(40, 129, 8)
+    ])
+    def test_key_size(self, key, keysize):
+        cipher = CAST5(binascii.unhexlify(key))
+        assert cipher.key_size == keysize
+
+    def test_invalid_key_size(self):
+        with pytest.raises(ValueError):
+            CAST5(binascii.unhexlify(b"0" * 34))
+
+
 class TestARC4(object):
     @pytest.mark.parametrize(("key", "keysize"), [
         (b"0" * 10, 40),