diff options
author | Paul Kehrer <paul.l.kehrer@gmail.com> | 2016-06-29 09:27:21 -0500 |
---|---|---|
committer | Donald Stufft <donald@stufft.io> | 2016-06-29 10:27:21 -0400 |
commit | c9a9ec1e7a39949b1d09d72746fad6a1d681a80b (patch) | |
tree | 6726e1973f4c55af714bc9c4c77d76d772d59a7c | |
parent | 81e8efd0cf48ecf3acfe4c205489c8301ca28045 (diff) | |
download | py-bcrypt-git-c9a9ec1e7a39949b1d09d72746fad6a1d681a80b.tar.gz |
Add checkpw (#76)
-rw-r--r-- | README.rst | 14 | ||||
-rw-r--r-- | src/bcrypt/__init__.py | 18 | ||||
-rw-r--r-- | src/build_bcrypt.py | 1 | ||||
-rw-r--r-- | tests/test_bcrypt.py | 55 |
4 files changed, 83 insertions, 5 deletions
@@ -37,6 +37,10 @@ For Fedora and RHEL-derivatives, the following command will ensure that the requ Changelog ========= +3.1.0 +----- +* Added support for ``checkpw`` as another method of verifying a password. + 3.0.0 ----- * Switched the C backend to code obtained from the OpenBSD project rather than @@ -51,8 +55,8 @@ Changelog Usage ----- -Hashing -~~~~~~~ +Password Hashing +~~~~~~~~~~~~~~~~ Hashing and then later checking that a password matches the previous hashed password is very simple: @@ -63,9 +67,9 @@ password is very simple: >>> password = b"super secret password" >>> # Hash a password for the first time, with a randomly-generated salt >>> hashed = bcrypt.hashpw(password, bcrypt.gensalt()) - >>> # Check that a unhashed password matches one that has previously been - >>> # hashed - >>> if bcrypt.hashpw(password, hashed) == hashed: + >>> # Check that an unhashed password matches one that has previously been + >>> # hashed + >>> if bcrypt.checkpw(password, hashed): ... print("It Matches!") ... else: ... print("It Does not Match :(") diff --git a/src/bcrypt/__init__.py b/src/bcrypt/__init__.py index ad44e93..c2be96d 100644 --- a/src/bcrypt/__init__.py +++ b/src/bcrypt/__init__.py @@ -78,6 +78,24 @@ def hashpw(password, salt): return _bcrypt.ffi.string(hashed) +def checkpw(password, hashed_password): + if (isinstance(password, six.text_type) or + isinstance(hashed_password, six.text_type)): + raise TypeError("Unicode-objects must be encoded before checking") + + if b"\x00" in password or b"\x00" in hashed_password: + raise ValueError( + "password and hashed_password may not contain NUL bytes" + ) + + # If the user supplies a $2y$ prefix we normalize to $2b$ + hashed_password = _normalize_prefix(hashed_password) + + ret = hashpw(password, hashed_password) + + return _bcrypt.lib.timingsafe_bcmp(ret, hashed_password, len(ret)) == 0 + + 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") diff --git a/src/build_bcrypt.py b/src/build_bcrypt.py index e7aca4c..3eec35c 100644 --- a/src/build_bcrypt.py +++ b/src/build_bcrypt.py @@ -25,6 +25,7 @@ int bcrypt_hashpass(const char *, const char *, char *, size_t); int encode_base64(char *, const uint8_t *, size_t); int bcrypt_pbkdf(const char *, size_t, const uint8_t *, size_t, uint8_t *, size_t, unsigned int); +int timingsafe_bcmp(const void *, const void *, size_t); """) ffi.set_source( diff --git a/tests/test_bcrypt.py b/tests/test_bcrypt.py index b506a7a..ea5cee3 100644 --- a/tests/test_bcrypt.py +++ b/tests/test_bcrypt.py @@ -217,6 +217,11 @@ def test_hashpw_new(password, salt, hashed): @pytest.mark.parametrize(("password", "salt", "hashed"), _test_vectors) +def test_checkpw(password, salt, hashed): + assert bcrypt.checkpw(password, hashed) is True + + +@pytest.mark.parametrize(("password", "salt", "hashed"), _test_vectors) def test_hashpw_existing(password, salt, hashed): assert bcrypt.hashpw(password, hashed) == hashed @@ -226,11 +231,47 @@ def test_hashpw_2y_prefix(password, hashed, expected): assert bcrypt.hashpw(password, hashed) == expected +@pytest.mark.parametrize(("password", "hashed", "expected"), _2y_test_vectors) +def test_checkpw_2y_prefix(password, hashed, expected): + assert bcrypt.checkpw(password, hashed) is True + + def test_hashpw_invalid(): with pytest.raises(ValueError): bcrypt.hashpw(b"password", b"$2z$04$cVWp4XaNU8a4v1uMRum2SO") +def test_checkpw_wrong_password(): + assert bcrypt.checkpw( + b"badpass", + b"$2b$04$2Siw3Nv3Q/gTOIPetAyPr.GNj3aO0lb1E5E9UumYGKjP9BYqlNWJe" + ) is False + + +def test_checkpw_bad_salt(): + with pytest.raises(ValueError): + bcrypt.checkpw( + b"badpass", + b"$2b$04$?Siw3Nv3Q/gTOIPetAyPr.GNj3aO0lb1E5E9UumYGKjP9BYqlNWJe" + ) + + +def test_checkpw_str_password(): + with pytest.raises(TypeError): + bcrypt.checkpw( + six.text_type("password"), + b"$2b$04$cVWp4XaNU8a4v1uMRum2SO", + ) + + +def test_checkpw_str_salt(): + with pytest.raises(TypeError): + bcrypt.checkpw( + b"password", + six.text_type("$2b$04$cVWp4XaNU8a4v1uMRum2SO"), + ) + + def test_hashpw_str_password(): with pytest.raises(TypeError): bcrypt.hashpw( @@ -247,6 +288,20 @@ def test_hashpw_str_salt(): ) +def test_checkpw_nul_byte(): + with pytest.raises(ValueError): + bcrypt.checkpw( + b"abc\0def", + b"$2b$04$2Siw3Nv3Q/gTOIPetAyPr.GNj3aO0lb1E5E9UumYGKjP9BYqlNWJe" + ) + + with pytest.raises(ValueError): + bcrypt.checkpw( + b"abcdef", + b"$2b$04$2S\0w3Nv3Q/gTOIPetAyPr.GNj3aO0lb1E5E9UumYGKjP9BYqlNWJe" + ) + + def test_hashpw_nul_byte(): salt = bcrypt.gensalt(4) with pytest.raises(ValueError): |