-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
4130988
commit 559d844
Showing
7 changed files
with
176 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import re | ||
import typing | ||
|
||
try: | ||
import bcrypt | ||
except ImportError as e: | ||
from ..exceptions import HasherNotAvailable | ||
|
||
raise HasherNotAvailable("bcrypt") from e | ||
|
||
from .base import HasherProtocol, ensure_bytes, ensure_str | ||
|
||
_IDENTIFY_REGEX = ( | ||
r"^\$(?P<prefix>2[abxy])\$(?P<rounds>\d{2})" | ||
r"\$(?P<salt>[A-Za-z0-9+/.]{22})(?P<hash>[A-Za-z0-9+/.]{31})$" | ||
) | ||
|
||
|
||
def _match_regex_hash( | ||
hash: typing.Union[str, bytes], | ||
) -> typing.Optional[typing.Match[str]]: | ||
return re.match(_IDENTIFY_REGEX, ensure_str(hash)) | ||
|
||
|
||
class BcryptHasher(HasherProtocol): | ||
def __init__( | ||
self, rounds: int = 12, prefix: typing.Literal["2a", "2b"] = "2b" | ||
) -> None: | ||
self.rounds = rounds | ||
self.prefix = prefix.encode("utf-8") | ||
|
||
@classmethod | ||
def identify(cls, hash: typing.Union[str, bytes]) -> bool: | ||
return _match_regex_hash(hash) is not None | ||
|
||
def hash( | ||
self, | ||
password: typing.Union[str, bytes], | ||
*, | ||
salt: typing.Union[bytes, None] = None, | ||
) -> str: | ||
if salt is None: | ||
salt = bcrypt.gensalt(self.rounds, self.prefix) | ||
return ensure_str(bcrypt.hashpw(ensure_bytes(password), salt)) | ||
|
||
def verify( | ||
self, hash: typing.Union[str, bytes], password: typing.Union[str, bytes] | ||
) -> bool: | ||
return bcrypt.checkpw(ensure_bytes(password), ensure_bytes(hash)) | ||
|
||
def check_needs_rehash(self, hash: typing.Union[str, bytes]) -> bool: | ||
_hash_match = _match_regex_hash(hash) | ||
if _hash_match is None: | ||
return True | ||
|
||
return int(_hash_match.group("rounds")) != self.rounds or _hash_match.group( | ||
"prefix" | ||
) != self.prefix.decode("utf-8") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
import typing | ||
|
||
import pytest | ||
|
||
from pwdlib.hashers.bcrypt import BcryptHasher | ||
|
||
_PASSWORD = "herminetincture" | ||
|
||
_HASHER = BcryptHasher() | ||
_HASH_STR = _HASHER.hash(_PASSWORD) | ||
_HASH_BYTES = _HASH_STR.encode("ascii") | ||
|
||
|
||
@pytest.fixture | ||
def bcrypt_hasher() -> BcryptHasher: | ||
return BcryptHasher() | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"hash,result", | ||
[ | ||
(_HASH_STR, True), | ||
(_HASH_BYTES, True), | ||
("INVALID_HASH", False), | ||
(b"INVALID_HASH", False), | ||
], | ||
) | ||
def test_identify(hash: typing.Union[str, bytes], result: bool) -> None: | ||
assert BcryptHasher.identify(hash) == result | ||
|
||
|
||
def test_hash(bcrypt_hasher: BcryptHasher) -> None: | ||
hash = bcrypt_hasher.hash("herminetincture") | ||
assert isinstance(hash, str) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"hash,password,result", | ||
[ | ||
(_HASH_STR, _PASSWORD, True), | ||
(_HASH_BYTES, _PASSWORD, True), | ||
(_HASH_STR, "INVALID_PASSWORD", False), | ||
(_HASH_BYTES, "INVALID_PASSWORD", False), | ||
], | ||
) | ||
def test_verify( | ||
hash: typing.Union[str, bytes], | ||
password: str, | ||
result: bool, | ||
bcrypt_hasher: BcryptHasher, | ||
) -> None: | ||
assert bcrypt_hasher.verify(hash, password) == result | ||
|
||
|
||
def test_check_needs_rehash(bcrypt_hasher: BcryptHasher) -> None: | ||
assert not bcrypt_hasher.check_needs_rehash(_HASH_STR) | ||
assert not bcrypt_hasher.check_needs_rehash(_HASH_BYTES) | ||
assert bcrypt_hasher.check_needs_rehash("INVALID_HASH") | ||
assert bcrypt_hasher.check_needs_rehash(b"INVALID_HASH") | ||
|
||
bcrypt_hasher_different_rounds = BcryptHasher(rounds=10) | ||
hash = bcrypt_hasher_different_rounds.hash("herminetincture") | ||
assert bcrypt_hasher.check_needs_rehash(hash) | ||
|
||
bcrypt_hasher_different_prefix = BcryptHasher(prefix="2a") | ||
hash = bcrypt_hasher_different_prefix.hash("herminetincture") | ||
assert bcrypt_hasher.check_needs_rehash(hash) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters