Skip to content

Commit

Permalink
Merge pull request #18 from Zozi96/feature/refactor-imports-and-add-ruff
Browse files Browse the repository at this point in the history
Refactor imports and update hashers module structure; remove unused constants and improve test cases
  • Loading branch information
Zozi96 authored Nov 8, 2024
2 parents 4643f50 + 2db0e7e commit 109696b
Show file tree
Hide file tree
Showing 25 changed files with 188 additions and 196 deletions.
31 changes: 0 additions & 31 deletions .github/workflows/mypy.yml

This file was deleted.

22 changes: 22 additions & 0 deletions .github/workflows/ruff.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Ruff
on:
pull_request:
branches:
- develop
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install uv
uv sync --extra dev
- name: Run Ruff
run: uv run ruff check --output-format=github .
10 changes: 9 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.uv]
dev-dependencies = ["mypy>=1.13.0", "pytest>=8.3.3"]
dev-dependencies = ["pytest>=8.3.3", "ruff>=0.7.3"]

[tool.mypy]
files = "src/hash_forge"
Expand All @@ -48,3 +48,11 @@ warn_return_any = true
bcrypt = ["bcrypt==4.2.0"]
argon2 = ["argon2-cffi==23.1.0"]
crypto = ["pycryptodome==3.21.0"]

[tool.ruff]
line-length = 120
exclude = [".venv"]
src = ["src", "tests"]

[tool.ruff.lint]
select = ["E", "F", "UP", "B", "SIM", "I"]
39 changes: 15 additions & 24 deletions src/hash_forge/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
from hash_forge.protocols import PHasher
from hash_forge.hashers.argon2_hasher import Argon2Hasher
from hash_forge.hashers.bcrypt_hasher import BCryptHasher, BCryptSha256Hasher
from hash_forge.hashers.pbkdf2_hasher import PBKDF2Sha256Hasher, PBKDF2Sha1Hasher
from hash_forge.hashers.scrypt_hasher import ScryptHasher
from hash_forge.hashers.blake2_hasher import Blake2Hasher
from hash_forge.hashers.ripemd160_hasher import Ripemd160Hasher
from hash_forge.hashers.whirlpool_hasher import WhirlpoolHasher


class HashManager:
Expand All @@ -24,8 +17,10 @@ def __init__(self, *hashers: PHasher) -> None:
preferred_hasher (PHasher): The first hasher provided, used as the preferred hasher.
"""
if not hashers:
raise ValueError('At least one hasher is required.')
self.hashers: set[tuple[str, PHasher]] = {(hasher.algorithm, hasher) for hasher in hashers}
raise ValueError("At least one hasher is required.")
self.hashers: set[tuple[str, PHasher]] = {
(hasher.algorithm, hasher) for hasher in hashers
}
self.preferred_hasher: PHasher = hashers[0]

def hash(self, string: str) -> str:
Expand Down Expand Up @@ -95,18 +90,14 @@ def _get_hasher_by_hash(self, hashed_string: str) -> PHasher | None:
PHasher | None: The hasher instance that matches the hashed string, or
None if no match is found.
"""
return next((hasher for algorithm, hasher in self.hashers if hashed_string.startswith(algorithm)), None)


__all__ = [
'Argon2Hasher',
'BCryptHasher',
'BCryptSha256Hasher',
'PBKDF2Sha256Hasher',
'PBKDF2Sha1Hasher',
'ScryptHasher',
'Blake2Hasher',
'Ripemd160Hasher',
'WhirlpoolHasher',
'HashManager',
]
return next(
(
hasher
for algorithm, hasher in self.hashers
if hashed_string.startswith(algorithm)
),
None,
)


__all__ = ["HashManager"]
1 change: 0 additions & 1 deletion src/hash_forge/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import string
from typing import Final


RANDOM_STRING_CHARS: Final[str] = string.ascii_letters + string.digits
19 changes: 19 additions & 0 deletions src/hash_forge/hashers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from .argon2_hasher import Argon2Hasher
from .bcrypt_hasher import BCryptHasher, BCryptSha256Hasher
from .blake2_hasher import Blake2Hasher
from .pbkdf2_hasher import PBKDF2Sha1Hasher, PBKDF2Sha256Hasher
from .ripemd160_hasher import Ripemd160Hasher
from .scrypt_hasher import ScryptHasher
from .whirlpool_hasher import WhirlpoolHasher

__all__ = [
"Argon2Hasher",
"BCryptHasher",
"BCryptSha256Hasher",
"PBKDF2Sha256Hasher",
"PBKDF2Sha1Hasher",
"ScryptHasher",
"Blake2Hasher",
"Ripemd160Hasher",
"WhirlpoolHasher",
]
31 changes: 16 additions & 15 deletions src/hash_forge/hashers/argon2_hasher.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
from functools import partial
from contextlib import suppress
from typing import ClassVar, cast, Any
from functools import partial
from typing import Any, ClassVar, cast

from hash_forge.protocols import PHasher


class Argon2Hasher(PHasher):
algorithm: ClassVar[str] = 'argon2'
library_module: ClassVar[str] = 'argon2'
algorithm: ClassVar[str] = "argon2"
library_module: ClassVar[str] = "argon2"

def __init__(
self,
Expand Down Expand Up @@ -35,7 +35,7 @@ def __init__(
self.salt_len = salt_len
self.ph = self._get_hasher()

__slots__ = ('argon2', 'time_cost', 'memory_cost', 'parallelism', 'hash_len', 'salt_len')
__slots__ = ("argon2", "time_cost", "memory_cost", "parallelism", "hash_len", "salt_len")

def hash(self, _string: str, /) -> str:
"""
Expand All @@ -45,7 +45,8 @@ def hash(self, _string: str, /) -> str:
_string (str): The string to be hashed.
Returns:
str: The formatted hash string containing the algorithm, time cost, memory cost, parallelism, salt, and hashed value.
str: The formatted hash string containing the algorithm, time cost, memory cost, parallelism, salt, and
hashed value.
"""

return self.algorithm + cast(str, self.ph.hash(_string))
Expand All @@ -62,8 +63,8 @@ def verify(self, _string: str, _hashed_string: str, /) -> bool:
bool: True if the string matches the hashed string, False otherwise.
"""
with suppress(self.argon2.exceptions.VerifyMismatchError, self.argon2.exceptions.InvalidHash, Exception):
_, _hashed_string = _hashed_string.split('$', 1)
return cast(bool, self.ph.verify('$' + _hashed_string, _string))
_, _hashed_string = _hashed_string.split("$", 1)
return cast(bool, self.ph.verify("$" + _hashed_string, _string))
return False

def needs_rehash(self, _hashed_string: str, /) -> bool:
Expand All @@ -77,8 +78,8 @@ def needs_rehash(self, _hashed_string: str, /) -> bool:
bool: True if the hashed string needs to be rehashed, False otherwise.
"""
with suppress(self.argon2.exceptions.InvalidHash, Exception):
_, _hashed_string = _hashed_string.split('$', 1)
return cast(bool, self.ph.check_needs_rehash('$' + _hashed_string))
_, _hashed_string = _hashed_string.split("$", 1)
return cast(bool, self.ph.check_needs_rehash("$" + _hashed_string))
return False

def _get_hasher(self) -> Any:
Expand All @@ -98,13 +99,13 @@ def _get_hasher(self) -> Any:
"""
hasher_partial = partial(self.argon2.PasswordHasher)
if self.time_cost:
hasher_partial.keywords['time_cost'] = self.time_cost
hasher_partial.keywords["time_cost"] = self.time_cost
if self.memory_cost:
hasher_partial.keywords['memory_cost'] = self.memory_cost
hasher_partial.keywords["memory_cost"] = self.memory_cost
if self.parallelism:
hasher_partial.keywords['parallelism'] = self.parallelism
hasher_partial.keywords["parallelism"] = self.parallelism
if self.hash_len:
hasher_partial.keywords['hash_len'] = self.hash_len
hasher_partial.keywords["hash_len"] = self.hash_len
if self.salt_len:
hasher_partial.keywords['salt_len'] = self.salt_len
hasher_partial.keywords["salt_len"] = self.salt_len
return hasher_partial()
4 changes: 2 additions & 2 deletions src/hash_forge/hashers/bcrypt_hasher.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import binascii
import hashlib

from collections.abc import Callable
from contextlib import suppress
from typing import Any, Callable, ClassVar, Optional, cast
from typing import Any, ClassVar, cast

from hash_forge.protocols import PHasher

Expand Down
1 change: 0 additions & 1 deletion src/hash_forge/hashers/blake2_hasher.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import binascii
import hashlib
import hmac

from typing import ClassVar

from hash_forge.protocols import PHasher
Expand Down
8 changes: 4 additions & 4 deletions src/hash_forge/hashers/pbkdf2_hasher.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import hashlib
import os
import binascii
import hashlib
import hmac

from typing import Any, Callable, ClassVar
import os
from collections.abc import Callable
from typing import Any, ClassVar

from hash_forge.protocols import PHasher

Expand Down
10 changes: 5 additions & 5 deletions src/hash_forge/hashers/ripemd160_hasher.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@


class Ripemd160Hasher(PHasher):
algorithm: ClassVar[str] = 'RIPEMD-160'
library_module: ClassVar[str] = 'Crypto.Hash.RIPEMD160'
algorithm: ClassVar[str] = "RIPEMD-160"
library_module: ClassVar[str] = "Crypto.Hash.RIPEMD160"

def __init__(self) -> None:
"""
Expand All @@ -29,7 +29,7 @@ def hash(self, _string: str, /) -> str:
"""
hashed = self.ripemd160.new()
hashed.update(_string.encode())
return '%s$%s' % (self.algorithm, hashed.hexdigest())
return f"{self.algorithm}${hashed.hexdigest()}"

def verify(self, _string: str, _hashed: str, /) -> bool:
"""
Expand All @@ -42,7 +42,7 @@ def verify(self, _string: str, _hashed: str, /) -> bool:
Returns:
bool: True if the string matches the hashed value, False otherwise.
"""
algorithm, hash_value = _hashed.split('$', 1)
algorithm, hash_value = _hashed.split("$", 1)
if algorithm != self.algorithm:
return False
hashed = self.ripemd160.new()
Expand All @@ -60,5 +60,5 @@ def needs_rehash(self, _hashed_string: str, /) -> bool:
bool: True if the algorithm used in the hashed string does not match
the current algorithm, indicating that a rehash is needed.
"""
algorithm, _ = _hashed_string.split('$', 1)
algorithm, _ = _hashed_string.split("$", 1)
return algorithm != self.algorithm
43 changes: 22 additions & 21 deletions src/hash_forge/hashers/scrypt_hasher.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,29 @@
import hashlib
import base64
import secrets
import hashlib
import hmac

import secrets
from functools import lru_cache
from typing import ClassVar

from hash_forge.protocols import PHasher


@lru_cache
def _generate_salt(salt: int) -> str:
"""
Generates a base64-encoded salt string.
Args:
salt (int): The number of bytes to generate for the salt.
Returns:
str: A base64-encoded string representation of the generated salt.
"""
return base64.b64encode(secrets.token_bytes(salt)).decode("ascii")


class ScryptHasher(PHasher):
algorithm: ClassVar[str] = 'scrypt'
algorithm: ClassVar[str] = "scrypt"

def __init__(
self,
Expand Down Expand Up @@ -39,7 +52,7 @@ def __init__(
self.dklen = dklen
self.salt_length = salt_length

__slots__ = ('work_factor', 'block_size', 'parallelism', 'maxmem', 'dklen', 'salt_length')
__slots__ = ("work_factor", "block_size", "parallelism", "maxmem", "dklen", "salt_length")

def hash(self, _string: str) -> str:
"""
Expand All @@ -61,15 +74,8 @@ def hash(self, _string: str) -> str:
maxmem=self.maxmem,
dklen=self.dklen,
)
hashed_string = base64.b64encode(hashed).decode('ascii').strip()
return '%s$%s$%s$%s$%s$%s' % (
self.algorithm,
self.work_factor,
salt,
self.block_size,
self.parallelism,
hashed_string,
)
hashed_string = base64.b64encode(hashed).decode("ascii").strip()
return f"{self.algorithm}${self.work_factor}${salt}${self.block_size}${self.parallelism}${hashed_string}"

def verify(self, _string: str, _hashed_string: str) -> bool:
"""
Expand Down Expand Up @@ -100,16 +106,11 @@ def needs_rehash(self, _hashed_string: str) -> bool:
_, n, _, r, p, _ = _hashed_string.split("$", 5)
return int(n) != self.work_factor or int(r) != self.block_size or int(p) != self.parallelism

@lru_cache
def generate_salt(self) -> str:
"""
Generates a cryptographic salt.
This method generates a random salt using the `secrets` module to ensure
cryptographic security. The generated salt is then encoded in base64 and
returned as an ASCII string.
Returns:
str: A base64 encoded ASCII string representing the generated salt.
str: A string representing the generated salt.
"""
return base64.b64encode(secrets.token_bytes(self.salt_length)).decode('ascii')
return _generate_salt(self.salt_length)
2 changes: 1 addition & 1 deletion src/hash_forge/hashers/whirlpool_hasher.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def hash(self, _string: str, /) -> str:
"""
hashed = self.sha512.new()
hashed.update(_string.encode())
return '%s$%s' % (self.algorithm, hashed.hexdigest())
return f'{self.algorithm}${hashed.hexdigest()}'

def verify(self, _string: str, _hashed_string: str, /) -> bool:
"""
Expand Down
Loading

0 comments on commit 109696b

Please sign in to comment.