-
-
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
cb36916
commit 4130988
Showing
11 changed files
with
277 additions
and
32 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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import typing | ||
|
||
from . import exceptions | ||
from .hashers import HasherProtocol | ||
|
||
|
||
class PasswordHash: | ||
def __init__(self, hashers: typing.Sequence[HasherProtocol]) -> None: | ||
assert len(hashers) > 0, "You must specify at least one hasher." | ||
self.hashers = hashers | ||
self.current_hasher = hashers[0] | ||
|
||
def hash( | ||
self, | ||
password: typing.Union[str, bytes], | ||
*, | ||
salt: typing.Union[bytes, None] = None, | ||
) -> str: | ||
return self.current_hasher.hash(password, salt=salt) | ||
|
||
def verify( | ||
self, hash: typing.Union[str, bytes], password: typing.Union[str, bytes] | ||
) -> bool: | ||
for hasher in self.hashers: | ||
if hasher.identify(hash): | ||
return hasher.verify(hash, password) | ||
raise exceptions.UnknownHashError(hash) | ||
|
||
def verify_and_update( | ||
self, hash: typing.Union[str, bytes], password: typing.Union[str, bytes] | ||
) -> typing.Tuple[bool, typing.Union[str, None]]: | ||
for hasher in self.hashers: | ||
if hasher.identify(hash): | ||
if not hasher.verify(hash, password): | ||
return False, None | ||
else: | ||
updated_hash: typing.Union[str, None] = None | ||
if hasher != self.current_hasher or hasher.check_needs_rehash(hash): | ||
updated_hash = hasher.hash(password) | ||
return True, updated_hash | ||
raise exceptions.UnknownHashError(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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import typing | ||
|
||
|
||
class PwdlibError(Exception): | ||
""" | ||
Base pwdlib error. | ||
""" | ||
|
||
def __init__(self, message: str) -> None: | ||
""" | ||
Args: | ||
message: | ||
The error message. | ||
""" | ||
self.message = message | ||
super().__init__(message) | ||
|
||
|
||
class HasherNotAvailable(PwdlibError): | ||
""" | ||
Error raised when an unavailable hash algorithm was installed. | ||
""" | ||
|
||
def __init__(self, hasher: str) -> None: | ||
""" | ||
Args: | ||
hasher: | ||
The unavailable hash algorithm. | ||
""" | ||
self.hasher = hasher | ||
message = ( | ||
f"The {hasher} hash algorithm is not available. " | ||
"Are you sure it's installed? " | ||
f"Try to run `pip install pdwlib[{hasher}]`." | ||
) | ||
super().__init__(message) | ||
|
||
|
||
class UnknownHashError(PwdlibError): | ||
""" | ||
Error raised when the hash can't be identified from the list of provided hashers. | ||
""" | ||
|
||
def __init__(self, hash: typing.Union[str, bytes]) -> None: | ||
""" | ||
Args: | ||
hash: | ||
The hash we failed to identify. | ||
""" | ||
self.hash = hash | ||
message = ( | ||
"This hash can't be identified. " | ||
"Make sure it's valid and that its corresponding hasher is enabled." | ||
) | ||
super().__init__(message) |
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,3 @@ | ||
from .base import HasherProtocol | ||
|
||
__all__ = ["HasherProtocol"] |
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,42 @@ | ||
import typing | ||
|
||
try: | ||
import argon2.exceptions | ||
from argon2 import PasswordHasher | ||
except ImportError as e: | ||
from ..exceptions import HasherNotAvailable | ||
|
||
raise HasherNotAvailable("argon2") from e | ||
|
||
from .base import HasherProtocol, ensure_str_hash | ||
|
||
|
||
class Argon2Hasher(HasherProtocol): | ||
def __init__(self) -> None: | ||
self._hasher = PasswordHasher() # TODO: handle parameters | ||
|
||
@classmethod | ||
def identify(cls, hash: typing.Union[str, bytes]) -> bool: | ||
return ensure_str_hash(hash).startswith("$argon2id$") | ||
|
||
def hash( | ||
self, | ||
password: typing.Union[str, bytes], | ||
*, | ||
salt: typing.Union[bytes, None] = None, | ||
) -> str: | ||
return self._hasher.hash(password, salt=salt) | ||
|
||
def verify( | ||
self, hash: typing.Union[str, bytes], password: typing.Union[str, bytes] | ||
) -> bool: | ||
try: | ||
return self._hasher.verify(hash, password) | ||
except ( | ||
argon2.exceptions.VerificationError, | ||
argon2.exceptions.InvalidHashError, | ||
): | ||
return False | ||
|
||
def check_needs_rehash(self, hash: typing.Union[str, bytes]) -> bool: | ||
return self._hasher.check_needs_rehash(ensure_str_hash(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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import typing | ||
|
||
|
||
def ensure_str_hash(hash: typing.Union[str, bytes]) -> str: | ||
return hash.decode("ascii") if isinstance(hash, bytes) else typing.cast(str, hash) | ||
|
||
|
||
class HasherProtocol(typing.Protocol): | ||
@classmethod | ||
def identify(cls, hash: typing.Union[str, bytes]) -> bool: | ||
... | ||
|
||
def hash( | ||
self, | ||
password: typing.Union[str, bytes], | ||
*, | ||
salt: typing.Union[bytes, None] = None, | ||
) -> str: | ||
... | ||
|
||
def verify( | ||
self, hash: typing.Union[str, bytes], password: typing.Union[str, bytes] | ||
) -> bool: | ||
... | ||
|
||
def check_needs_rehash(self, hash: typing.Union[str, bytes]) -> bool: | ||
... | ||
|
||
|
||
__all__ = ["HasherProtocol", "ensure_str_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
Empty file.
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,52 @@ | ||
import typing | ||
|
||
import pytest | ||
|
||
from pwdlib.hashers.argon2 import Argon2Hasher | ||
|
||
_PASSWORD = "herminetincture" | ||
|
||
_HASHER = Argon2Hasher() | ||
_HASH_STR = _HASHER.hash(_PASSWORD) | ||
_HASH_BYTES = _HASH_STR.encode("ascii") | ||
|
||
|
||
@pytest.fixture | ||
def argon2_hasher() -> Argon2Hasher: | ||
return Argon2Hasher() | ||
|
||
|
||
@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 Argon2Hasher.identify(hash) == result | ||
|
||
|
||
def test_hash(argon2_hasher: Argon2Hasher) -> None: | ||
hash = argon2_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, | ||
argon2_hasher: Argon2Hasher, | ||
) -> None: | ||
assert argon2_hasher.verify(hash, password) == result |
This file was deleted.
Oops, something went wrong.
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,43 @@ | ||
import typing | ||
|
||
import pytest | ||
|
||
from pwdlib import PasswordHash, exceptions | ||
from pwdlib.hashers.argon2 import Argon2Hasher | ||
|
||
_PASSWORD = "herminetincture" | ||
|
||
_ARGON2_HASHER = Argon2Hasher() | ||
_ARGON2_HASH_STR = _ARGON2_HASHER.hash(_PASSWORD) | ||
|
||
|
||
@pytest.fixture | ||
def password_hash() -> PasswordHash: | ||
return PasswordHash((Argon2Hasher(),)) | ||
|
||
|
||
def test_hash(password_hash: PasswordHash) -> None: | ||
hash = password_hash.hash("herminetincture") | ||
assert isinstance(hash, str) | ||
assert password_hash.current_hasher.identify(hash) | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"hash,password,result", | ||
[ | ||
(_ARGON2_HASH_STR, _PASSWORD, True), | ||
(_ARGON2_HASH_STR, "INVALID_PASSWORD", False), | ||
], | ||
) | ||
def test_verify( | ||
hash: typing.Union[str, bytes], | ||
password: str, | ||
result: bool, | ||
password_hash: PasswordHash, | ||
) -> None: | ||
assert password_hash.verify(hash, password) == result | ||
|
||
|
||
def test_verify_unknown_hash(password_hash: PasswordHash) -> None: | ||
with pytest.raises(exceptions.UnknownHashError): | ||
assert password_hash.verify("INVALID_HASH", _PASSWORD) |