summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Kehrer <paul.l.kehrer@gmail.com>2016-06-29 09:27:21 -0500
committerDonald Stufft <donald@stufft.io>2016-06-29 10:27:21 -0400
commitc9a9ec1e7a39949b1d09d72746fad6a1d681a80b (patch)
tree6726e1973f4c55af714bc9c4c77d76d772d59a7c
parent81e8efd0cf48ecf3acfe4c205489c8301ca28045 (diff)
downloadpy-bcrypt-git-c9a9ec1e7a39949b1d09d72746fad6a1d681a80b.tar.gz
Add checkpw (#76)
-rw-r--r--README.rst14
-rw-r--r--src/bcrypt/__init__.py18
-rw-r--r--src/build_bcrypt.py1
-rw-r--r--tests/test_bcrypt.py55
4 files changed, 83 insertions, 5 deletions
diff --git a/README.rst b/README.rst
index 62f189f..0883286 100644
--- a/README.rst
+++ b/README.rst
@@ -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):