From 1050ddf44f0713a587cd0ba239e23c95064a39bc Mon Sep 17 00:00:00 2001 From: Paul Kehrer Date: Mon, 27 Jan 2014 21:04:03 -0600 Subject: PBKDF2 support for OpenSSL backend --- cryptography/hazmat/backends/interfaces.py | 15 ++++++++ cryptography/hazmat/backends/openssl/backend.py | 46 +++++++++++++++++++++++-- cryptography/hazmat/primitives/kdf/__init__.py | 0 cryptography/hazmat/primitives/kdf/pbkdf2.py | 46 +++++++++++++++++++++++++ docs/hazmat/backends/interfaces.rst | 39 +++++++++++++++++++++ pytest.ini | 3 +- tests/conftest.py | 3 +- tests/hazmat/primitives/test_pbkdf2_vectors.py | 37 ++++++++++++++++++++ tests/hazmat/primitives/utils.py | 25 ++++++++++++++ tests/utils.py | 4 +++ 10 files changed, 214 insertions(+), 4 deletions(-) create mode 100644 cryptography/hazmat/primitives/kdf/__init__.py create mode 100644 cryptography/hazmat/primitives/kdf/pbkdf2.py create mode 100644 tests/hazmat/primitives/test_pbkdf2_vectors.py diff --git a/cryptography/hazmat/backends/interfaces.py b/cryptography/hazmat/backends/interfaces.py index 4fbb3488..936520eb 100644 --- a/cryptography/hazmat/backends/interfaces.py +++ b/cryptography/hazmat/backends/interfaces.py @@ -65,3 +65,18 @@ class HMACBackend(six.with_metaclass(abc.ABCMeta)): """ Create a HashContext for calculating a message authentication code. """ + + +class PBKDF2Backend(six.with_metaclass(abc.ABCMeta)): + @abc.abstractmethod + def pbkdf2_hash_supported(self, algorithm): + """ + Return True if the hash algorithm is supported for PBKDF2 by this + backend. + """ + + @abc.abstractmethod + def derive_pbkdf2(self, algorithm, length, salt, iterations, key_material): + """ + Return length bytes derived from provided PBKDF2 parameters. + """ diff --git a/cryptography/hazmat/backends/openssl/backend.py b/cryptography/hazmat/backends/openssl/backend.py index d8d4669a..ca7d1778 100644 --- a/cryptography/hazmat/backends/openssl/backend.py +++ b/cryptography/hazmat/backends/openssl/backend.py @@ -20,9 +20,9 @@ from cryptography.exceptions import ( UnsupportedAlgorithm, InvalidTag, InternalError ) from cryptography.hazmat.backends.interfaces import ( - CipherBackend, HashBackend, HMACBackend + CipherBackend, HashBackend, HMACBackend, PBKDF2Backend ) -from cryptography.hazmat.primitives import interfaces +from cryptography.hazmat.primitives import interfaces, hashes from cryptography.hazmat.primitives.ciphers.algorithms import ( AES, Blowfish, Camellia, TripleDES, ARC4, ) @@ -35,6 +35,7 @@ from cryptography.hazmat.bindings.openssl.binding import Binding @utils.register_interface(CipherBackend) @utils.register_interface(HashBackend) @utils.register_interface(HMACBackend) +@utils.register_interface(PBKDF2Backend) class Backend(object): """ OpenSSL API binding interfaces. @@ -133,6 +134,47 @@ class Backend(object): def create_symmetric_decryption_ctx(self, cipher, mode): return _CipherContext(self, cipher, mode, _CipherContext._DECRYPT) + def pbkdf2_hash_supported(self, algorithm): + if self._lib.Cryptography_HAS_PBKDF2_HMAC: + digest = self._lib.EVP_get_digestbyname( + algorithm.name.encode("ascii")) + return digest != self._ffi.NULL + else: + return isinstance(algorithm, hashes.SHA1) + + def derive_pbkdf2(self, algorithm, length, salt, iterations, key_material): + buf = self._ffi.new("char[]", length) + if self._lib.Cryptography_HAS_PBKDF2_HMAC: + evp_md = self._lib.EVP_get_digestbyname( + algorithm.name.encode("ascii")) + assert evp_md != self._ffi.NULL + res = self._lib.PKCS5_PBKDF2_HMAC( + key_material, + len(key_material), + salt, + len(salt), + iterations, + evp_md, + length, + buf + ) + assert res == 1 + else: + # OpenSSL < 1.0.0 + assert isinstance(algorithm, hashes.SHA1) + res = self._lib.PKCS5_PBKDF2_HMAC_SHA1( + key_material, + len(key_material), + salt, + len(salt), + iterations, + length, + buf + ) + assert res == 1 + + return self._ffi.buffer(buf)[:] + def _handle_error(self, mode): code = self._lib.ERR_get_error() if not code and isinstance(mode, GCM): diff --git a/cryptography/hazmat/primitives/kdf/__init__.py b/cryptography/hazmat/primitives/kdf/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/cryptography/hazmat/primitives/kdf/pbkdf2.py b/cryptography/hazmat/primitives/kdf/pbkdf2.py new file mode 100644 index 00000000..cc01246f --- /dev/null +++ b/cryptography/hazmat/primitives/kdf/pbkdf2.py @@ -0,0 +1,46 @@ +# 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 + +from cryptography.exceptions import InvalidKey, UnsupportedAlgorithm +from cryptography.hazmat.primitives import constant_time + + +class PBKDF2(object): + def __init__(self, algorithm, length, salt, iterations, backend): + if not backend.pbkdf2_hash_supported(algorithm): + raise UnsupportedAlgorithm( + "{0} is not supported by this backend".format(algorithm.name) + ) + self.algorithm = algorithm + if length > 2**31 - 1: + raise ValueError("Requested length too large.") + self._length = length + # TODO: handle salt + self._salt = salt + self.iterations = iterations + self._backend = backend + + def derive(self, key_material): + return self._backend.derive_pbkdf2( + self.algorithm, + self._length, + self._salt, + self.iterations, + key_material + ) + + def verify(self, key_material, expected_key): + if not constant_time.bytes_eq(key_material, expected_key): + raise InvalidKey("Signature did not match digest.") diff --git a/docs/hazmat/backends/interfaces.rst b/docs/hazmat/backends/interfaces.rst index 11e2f2a2..fa4f800c 100644 --- a/docs/hazmat/backends/interfaces.rst +++ b/docs/hazmat/backends/interfaces.rst @@ -131,3 +131,42 @@ A specific ``backend`` may provide one or more of these interfaces. :returns: :class:`~cryptography.hazmat.primitives.interfaces.HashContext` + + + +.. class:: PBKDF2Backend + + A backend with methods for using PBKDF2. + + .. method:: pbkdf2_hash_supported(algorithm) + + Check if the specified ``algorithm`` is supported by this backend. + + :param algorithm: An instance of a + :class:`~cryptography.hazmat.primitives.interfaces.HashAlgorithm` + provider. + + :returns: ``True`` if the specified ``algorithm`` is supported for + PBKDF2 by this backend, otherwise ``False``. + + .. method:: derive_pbkdf2(self, algorithm, length, salt, iterations, + key_material) + + :param algorithm: An instance of a + :class:`~cryptography.hazmat.primitives.interfaces.HashAlgorithm` + provider. + + :param int length: The desired length of the derived key. Maximum is + 2\ :sup:`31` - 1. + + :param bytes salt: A salt. `RFC 2898`_ recommends 64-bits or longer. + + :param int iterations: The number of iterations to perform of the hash + function. + + :param bytes key_material: The key material to use as a basis for + the derived key. This is typically a password. + + :return bytes: Derived key. + +.. _`RFC 2898`: https://www.ietf.org/rfc/rfc2898.txt diff --git a/pytest.ini b/pytest.ini index 36d4edc4..89fda539 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,8 @@ [pytest] addopts = -r s markers = - hmac: this test requires a backend providing HMACBackend cipher: this test requires a backend providing CipherBackend hash: this test requires a backend providing HashBackend + hmac: this test requires a backend providing HMACBackend + pbkdf2: this test requires a backend providing PBKDF2Backend supported: parametrized test requiring only_if and skip_message diff --git a/tests/conftest.py b/tests/conftest.py index a9acb54a..7370294f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,7 +2,7 @@ import pytest from cryptography.hazmat.backends import _ALL_BACKENDS from cryptography.hazmat.backends.interfaces import ( - HMACBackend, CipherBackend, HashBackend + HMACBackend, CipherBackend, HashBackend, PBKDF2Backend ) from .utils import check_for_iface, check_backend_support, select_backends @@ -21,6 +21,7 @@ def pytest_runtest_setup(item): check_for_iface("hmac", HMACBackend, item) check_for_iface("cipher", CipherBackend, item) check_for_iface("hash", HashBackend, item) + check_for_iface("pbkdf2", PBKDF2Backend, item) check_backend_support(item) diff --git a/tests/hazmat/primitives/test_pbkdf2_vectors.py b/tests/hazmat/primitives/test_pbkdf2_vectors.py new file mode 100644 index 00000000..e6e3935f --- /dev/null +++ b/tests/hazmat/primitives/test_pbkdf2_vectors.py @@ -0,0 +1,37 @@ +# 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 pytest + +from cryptography.hazmat.primitives import hashes + +from .utils import generate_pbkdf2_test +from ...utils import load_nist_vectors + + +@pytest.mark.supported( + only_if=lambda backend: backend.pbkdf2_hash_supported(hashes.SHA1()), + skip_message="Does not support SHA1 for PBKDF2", +) +@pytest.mark.pbkdf2 +class TestPBKDF2_SHA1(object): + test_pbkdf2_sha1 = generate_pbkdf2_test( + load_nist_vectors, + "KDF", + [ + "rfc-6070-PBKDF2-SHA1.txt", + ], + hashes.SHA1(), + ) diff --git a/tests/hazmat/primitives/utils.py b/tests/hazmat/primitives/utils.py index f27afe41..3a1d6d88 100644 --- a/tests/hazmat/primitives/utils.py +++ b/tests/hazmat/primitives/utils.py @@ -4,6 +4,7 @@ import os import pytest from cryptography.hazmat.primitives import hashes, hmac +from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2 from cryptography.hazmat.primitives.ciphers import Cipher from cryptography.exceptions import ( AlreadyFinalized, NotYetFinalized, AlreadyUpdated, InvalidTag, @@ -211,6 +212,30 @@ def hmac_test(backend, algorithm, params): assert h.finalize() == binascii.unhexlify(md.encode("ascii")) +def generate_pbkdf2_test(param_loader, path, file_names, algorithm): + all_params = _load_all_params(path, file_names, param_loader) + + @pytest.mark.parametrize("params", all_params) + def test_pbkdf2(self, backend, params): + pbkdf2_test(backend, algorithm, params) + return test_pbkdf2 + + +def pbkdf2_test(backend, algorithm, params): + # Password and salt can contain \0, which should be loaded as a null char. + # The NIST loader loads them as literal strings so we replace with the + # proper value. + kdf = PBKDF2( + algorithm, + int(params["length"]), + params["salt"], + int(params["iterations"]), + backend + ) + derived_key = kdf.derive(params["password"]) + assert binascii.hexlify(derived_key) == params["derived_key"] + + def generate_aead_exception_test(cipher_factory, mode_factory): def test_aead_exception(self, backend): aead_exception_test(backend, cipher_factory, mode_factory) diff --git a/tests/utils.py b/tests/utils.py index 507bc421..5c0e524f 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -89,6 +89,10 @@ def load_nist_vectors(vector_data): # Build our data using a simple Key = Value format name, value = [c.strip() for c in line.split("=")] + # Some tests (PBKDF2) contain \0, which should be interpreted as a + # null character rather than literal. + value = value.replace("\\0", "\0") + # COUNT is a special token that indicates a new block of data if name.upper() == "COUNT": test_data = {} -- cgit v1.2.3