Add bcrypt pbkdf support (#70)
* add bcrypt_pbkdf support * bytes! * some docs
This commit is contained in:
parent
0517d7a368
commit
e565dad5de
20
README.rst
20
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
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user