aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPaul Kehrer <paul.l.kehrer@gmail.com>2014-01-27 21:04:03 -0600
committerPaul Kehrer <paul.l.kehrer@gmail.com>2014-01-28 11:18:26 -0600
commit1050ddf44f0713a587cd0ba239e23c95064a39bc (patch)
tree336c1329b82370fda3050b8c787ed8a85d32dc1b
parent1f8cd620cfbb854b0dfcdbf89c140160a8caba13 (diff)
downloadcryptography-1050ddf44f0713a587cd0ba239e23c95064a39bc.tar.gz
cryptography-1050ddf44f0713a587cd0ba239e23c95064a39bc.tar.bz2
cryptography-1050ddf44f0713a587cd0ba239e23c95064a39bc.zip
PBKDF2 support for OpenSSL backend
-rw-r--r--cryptography/hazmat/backends/interfaces.py15
-rw-r--r--cryptography/hazmat/backends/openssl/backend.py46
-rw-r--r--cryptography/hazmat/primitives/kdf/__init__.py0
-rw-r--r--cryptography/hazmat/primitives/kdf/pbkdf2.py46
-rw-r--r--docs/hazmat/backends/interfaces.rst39
-rw-r--r--pytest.ini3
-rw-r--r--tests/conftest.py3
-rw-r--r--tests/hazmat/primitives/test_pbkdf2_vectors.py37
-rw-r--r--tests/hazmat/primitives/utils.py25
-rw-r--r--tests/utils.py4
10 files changed, 214 insertions, 4 deletions
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
--- /dev/null
+++ b/cryptography/hazmat/primitives/kdf/__init__.py
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 = {}