summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.rst20
-rw-r--r--src/bcrypt/__init__.py29
-rw-r--r--tests/test_bcrypt.py124
3 files changed, 170 insertions, 3 deletions
diff --git a/README.rst b/README.rst
index 722e883..9519ff4 100644
--- a/README.rst
+++ b/README.rst
@@ -41,6 +41,7 @@ Changelog
-----
* Switched the C backend to code obtained from the OpenBSD project rather than
openwall.
+* Added support for `bcrypt_pbkdf` via the `kdf` function.
2.0.0
-----
@@ -50,8 +51,8 @@ Changelog
Usage
-----
-Basic
-~~~~~
+Hashing
+~~~~~~~
Hashing and then later checking that a password matches the previous hashed
password is very simple:
@@ -69,6 +70,21 @@ password is very simple:
... else:
... print("It Does not Match :(")
+KDF
+~~~
+
+As of 3.0.0 `bcrypt` now offers a `kdf` function which does `bcrypt_pbkdf`.
+This KDF is used in OpenSSH's newer encrypted private key format.
+
+.. code:: pycon
+
+ >>> import bcrypt
+ >>> key = bcrypt.kdf(
+ ... password=b'password',
+ ... salt=b'salt',
+ ... desired_key_bytes=32,
+ ... rounds=100)
+
Adjustable Work Factor
~~~~~~~~~~~~~~~~~~~~~~
diff --git a/src/bcrypt/__init__.py b/src/bcrypt/__init__.py
index 2c503da..ad44e93 100644
--- a/src/bcrypt/__init__.py
+++ b/src/bcrypt/__init__.py
@@ -32,7 +32,7 @@ from .__about__ import (
__all__ = [
"__title__", "__summary__", "__uri__", "__version__", "__author__",
"__email__", "__license__", "__copyright__",
- "gensalt", "hashpw",
+ "gensalt", "hashpw", "kdf",
]
@@ -76,3 +76,30 @@ def hashpw(password, salt):
raise ValueError("Invalid salt")
return _bcrypt.ffi.string(hashed)
+
+
+def kdf(password, salt, desired_key_bytes, rounds):
+ if isinstance(password, six.text_type) or isinstance(salt, six.text_type):
+ raise TypeError("Unicode-objects must be encoded before hashing")
+
+ if len(password) == 0 or len(salt) == 0:
+ raise ValueError("password and salt must not be empty")
+
+ if desired_key_bytes <= 0 or desired_key_bytes > 512:
+ raise ValueError("desired_key_bytes must be 1-512")
+
+ if rounds < 1:
+ raise ValueError("rounds must be 1 or more")
+
+ key = _bcrypt.ffi.new("uint8_t[]", desired_key_bytes)
+ res = _bcrypt.lib.bcrypt_pbkdf(
+ password, len(password), salt, len(salt), key, len(key), rounds
+ )
+ _bcrypt_assert(res == 0)
+
+ return _bcrypt.ffi.buffer(key, desired_key_bytes)[:]
+
+
+def _bcrypt_assert(ok):
+ if not ok:
+ raise SystemError("bcrypt assertion failed")
diff --git a/tests/test_bcrypt.py b/tests/test_bcrypt.py
index 90f1f66..428fe86 100644
--- a/tests/test_bcrypt.py
+++ b/tests/test_bcrypt.py
@@ -321,3 +321,127 @@ def test_nul_byte():
salt = bcrypt.gensalt(4)
with pytest.raises(ValueError):
bcrypt.hashpw(b"abc\0def", salt)
+
+
+@pytest.mark.parametrize(
+ ("rounds", "password", "salt", "expected"),
+ [[
+ 4, b"password", b"salt",
+ b"\x5b\xbf\x0c\xc2\x93\x58\x7f\x1c\x36\x35\x55\x5c\x27\x79\x65\x98"
+ b"\xd4\x7e\x57\x90\x71\xbf\x42\x7e\x9d\x8f\xbe\x84\x2a\xba\x34\xd9"
+ ], [
+ 4, b"password", b"\x00",
+ b"\xc1\x2b\x56\x62\x35\xee\xe0\x4c\x21\x25\x98\x97\x0a\x57\x9a\x67"
+ ], [
+ 4, b"\x00", b"salt",
+ b"\x60\x51\xbe\x18\xc2\xf4\xf8\x2c\xbf\x0e\xfe\xe5\x47\x1b\x4b\xb9"
+ ], [
+ # nul bytes in password and string
+ 4, b"password\x00", b"salt\x00",
+ b"\x74\x10\xe4\x4c\xf4\xfa\x07\xbf\xaa\xc8\xa9\x28\xb1\x72\x7f\xac"
+ b"\x00\x13\x75\xe7\xbf\x73\x84\x37\x0f\x48\xef\xd1\x21\x74\x30\x50"
+ ], [
+ 4, b"pass\x00wor", b"sa\0l",
+ b"\xc2\xbf\xfd\x9d\xb3\x8f\x65\x69\xef\xef\x43\x72\xf4\xde\x83\xc0"
+ ], [
+ 4, b"pass\x00word", b"sa\0lt",
+ b"\x4b\xa4\xac\x39\x25\xc0\xe8\xd7\xf0\xcd\xb6\xbb\x16\x84\xa5\x6f"
+ ], [
+ # bigger key
+ 8, b"password", b"salt",
+ b"\xe1\x36\x7e\xc5\x15\x1a\x33\xfa\xac\x4c\xc1\xc1\x44\xcd\x23\xfa"
+ b"\x15\xd5\x54\x84\x93\xec\xc9\x9b\x9b\x5d\x9c\x0d\x3b\x27\xbe\xc7"
+ b"\x62\x27\xea\x66\x08\x8b\x84\x9b\x20\xab\x7a\xa4\x78\x01\x02\x46"
+ b"\xe7\x4b\xba\x51\x72\x3f\xef\xa9\xf9\x47\x4d\x65\x08\x84\x5e\x8d"
+ ], [
+ # more rounds
+ 42, b"password", b"salt",
+ b"\x83\x3c\xf0\xdc\xf5\x6d\xb6\x56\x08\xe8\xf0\xdc\x0c\xe8\x82\xbd"
+ ], [
+ # longer password
+ 8,
+ b"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do "
+ b"eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut "
+ b"enim ad minim veniam, quis nostrud exercitation ullamco laboris "
+ b"nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor "
+ b"in reprehenderit in voluptate velit esse cillum dolore eu fugiat "
+ b"nulla pariatur. Excepteur sint occaecat cupidatat non proident, "
+ b"sunt in culpa qui officia deserunt mollit anim id est laborum.",
+ b"salis\x00",
+ b"\x10\x97\x8b\x07\x25\x3d\xf5\x7f\x71\xa1\x62\xeb\x0e\x8a\xd3\x0a"
+ ], [
+ # "unicode"
+ 8,
+ b"\x0d\xb3\xac\x94\xb3\xee\x53\x28\x4f\x4a\x22\x89\x3b\x3c\x24\xae",
+ b"\x3a\x62\xf0\xf0\xdb\xce\xf8\x23\xcf\xcc\x85\x48\x56\xea\x10\x28",
+ b"\x20\x44\x38\x17\x5e\xee\x7c\xe1\x36\xc9\x1b\x49\xa6\x79\x23\xff"
+ ], [
+ # very large key
+ 8,
+ b"\x0d\xb3\xac\x94\xb3\xee\x53\x28\x4f\x4a\x22\x89\x3b\x3c\x24\xae",
+ b"\x3a\x62\xf0\xf0\xdb\xce\xf8\x23\xcf\xcc\x85\x48\x56\xea\x10\x28",
+ b"\x20\x54\xb9\xff\xf3\x4e\x37\x21\x44\x03\x34\x74\x68\x28\xe9\xed"
+ b"\x38\xde\x4b\x72\xe0\xa6\x9a\xdc\x17\x0a\x13\xb5\xe8\xd6\x46\x38"
+ b"\x5e\xa4\x03\x4a\xe6\xd2\x66\x00\xee\x23\x32\xc5\xed\x40\xad\x55"
+ b"\x7c\x86\xe3\x40\x3f\xbb\x30\xe4\xe1\xdc\x1a\xe0\x6b\x99\xa0\x71"
+ b"\x36\x8f\x51\x8d\x2c\x42\x66\x51\xc9\xe7\xe4\x37\xfd\x6c\x91\x5b"
+ b"\x1b\xbf\xc3\xa4\xce\xa7\x14\x91\x49\x0e\xa7\xaf\xb7\xdd\x02\x90"
+ b"\xa6\x78\xa4\xf4\x41\x12\x8d\xb1\x79\x2e\xab\x27\x76\xb2\x1e\xb4"
+ b"\x23\x8e\x07\x15\xad\xd4\x12\x7d\xff\x44\xe4\xb3\xe4\xcc\x4c\x4f"
+ b"\x99\x70\x08\x3f\x3f\x74\xbd\x69\x88\x73\xfd\xf6\x48\x84\x4f\x75"
+ b"\xc9\xbf\x7f\x9e\x0c\x4d\x9e\x5d\x89\xa7\x78\x39\x97\x49\x29\x66"
+ b"\x61\x67\x07\x61\x1c\xb9\x01\xde\x31\xa1\x97\x26\xb6\xe0\x8c\x3a"
+ b"\x80\x01\x66\x1f\x2d\x5c\x9d\xcc\x33\xb4\xaa\x07\x2f\x90\xdd\x0b"
+ b"\x3f\x54\x8d\x5e\xeb\xa4\x21\x13\x97\xe2\xfb\x06\x2e\x52\x6e\x1d"
+ b"\x68\xf4\x6a\x4c\xe2\x56\x18\x5b\x4b\xad\xc2\x68\x5f\xbe\x78\xe1"
+ b"\xc7\x65\x7b\x59\xf8\x3a\xb9\xab\x80\xcf\x93\x18\xd6\xad\xd1\xf5"
+ b"\x93\x3f\x12\xd6\xf3\x61\x82\xc8\xe8\x11\x5f\x68\x03\x0a\x12\x44"
+ ], [
+ # UTF-8 Greek characters "odysseus" / "telemachos"
+ 8,
+ b"\xe1\xbd\x88\xce\xb4\xcf\x85\xcf\x83\xcf\x83\xce\xb5\xcf\x8d\xcf"
+ b"\x82",
+ b"\xce\xa4\xce\xb7\xce\xbb\xce\xad\xce\xbc\xce\xb1\xcf\x87\xce\xbf"
+ b"\xcf\x82",
+ b"\x43\x66\x6c\x9b\x09\xef\x33\xed\x8c\x27\xe8\xe8\xf3\xe2\xd8\xe6"
+ ]])
+def test_kdf(rounds, password, salt, expected):
+ derived = bcrypt.kdf(password, salt, len(expected), rounds)
+ assert derived == expected
+
+
+def test_kdf_str_password():
+ with pytest.raises(TypeError):
+ bcrypt.kdf(
+ six.text_type("password"), b"$2b$04$cVWp4XaNU8a4v1uMRum2SO", 10, 10
+ )
+
+
+def test_kdf_str_salt():
+ with pytest.raises(TypeError):
+ bcrypt.kdf(
+ b"password", six.text_type("salt"), 10, 10
+ )
+
+
+@pytest.mark.parametrize(
+ ("password", "salt", "desired_key_bytes", "rounds", "error"),
+ [
+ (u"pass", b"$2b$04$cVWp4XaNU8a4v1uMRum2SO", 10, 10, TypeError),
+ (b"password", u"salt", 10, 10, TypeError),
+ (b"", b"$2b$04$cVWp4XaNU8a4v1uMRum2SO", 10, 10, ValueError),
+ (b"password", b"", 10, 10, ValueError),
+ (b"password", b"$2b$04$cVWp4XaNU8a4v1uMRum2SO", 0, 10, ValueError),
+ (b"password", b"$2b$04$cVWp4XaNU8a4v1uMRum2SO", -3, 10, ValueError),
+ (b"password", b"$2b$04$cVWp4XaNU8a4v1uMRum2SO", 513, 10, ValueError),
+ (b"password", b"$2b$04$cVWp4XaNU8a4v1uMRum2SO", 20, 0, ValueError),
+ ]
+)
+def test_invalid_params(password, salt, desired_key_bytes, rounds, error):
+ with pytest.raises(error):
+ bcrypt.kdf(password, salt, desired_key_bytes, rounds)
+
+
+def test_bcrypt_assert():
+ with pytest.raises(SystemError):
+ bcrypt._bcrypt_assert(False)