|
| 1 | +import asyncio |
1 | 2 | import base64 |
2 | 3 | import hashlib |
3 | 4 | import hmac |
4 | | -from typing import NewType |
| 5 | +import logging |
5 | 6 |
|
6 | 7 | import bcrypt |
7 | 8 |
|
8 | 9 | from app.domain.ports.password_hasher import PasswordHasher |
9 | 10 | from app.domain.value_objects.raw_password import RawPassword |
| 11 | +from app.infrastructure.adapters.types import HasherThreadPoolExecutor |
10 | 12 |
|
11 | | -PasswordPepper = NewType("PasswordPepper", str) |
| 13 | +log = logging.getLogger(__name__) |
12 | 14 |
|
13 | 15 |
|
14 | 16 | class BcryptPasswordHasher(PasswordHasher): |
15 | | - def __init__(self, pepper: PasswordPepper): |
| 17 | + def __init__( |
| 18 | + self, |
| 19 | + pepper: str, |
| 20 | + work_factor: int, |
| 21 | + executor: HasherThreadPoolExecutor, |
| 22 | + ): |
16 | 23 | self._pepper = pepper |
| 24 | + self._work_factor = work_factor |
| 25 | + self._executor = executor |
17 | 26 |
|
18 | | - def hash(self, raw_password: RawPassword) -> bytes: |
| 27 | + async def hash(self, raw_password: RawPassword) -> bytes: |
| 28 | + loop = asyncio.get_running_loop() |
| 29 | + return await loop.run_in_executor(self._executor, self.hash_sync, raw_password) |
| 30 | + |
| 31 | + async def verify(self, raw_password: RawPassword, hashed_password: bytes) -> bool: |
| 32 | + loop = asyncio.get_running_loop() |
| 33 | + return await loop.run_in_executor( |
| 34 | + self._executor, |
| 35 | + self.verify_sync, |
| 36 | + raw_password, |
| 37 | + hashed_password, |
| 38 | + ) |
| 39 | + |
| 40 | + def hash_sync(self, raw_password: RawPassword) -> bytes: |
19 | 41 | """ |
20 | | - Bcrypt is limited to 72-character passwords. Adding a pepper may surpass this character count. |
21 | | - To keep the input within the 72-character limit, pre-hashing can be employed. |
22 | | - One option is using HMAC-SHA256, which produces a fixed-length digest of the peppered password. |
23 | | - However, pre-hashing may introduce null bytes, which `bcrypt` cannot process correctly. |
24 | | - This issue can be resolved by applying `base64` encoding to the digest. |
25 | | - The resulting `base64(hmac-sha256(password, pepper))` string is then ready for bcrypt hashing. |
26 | | - Salt is added to this string before passing it to `bcrypt` for the final hashing step. |
27 | | - Inspired by: https://blog.ircmaxell.com/2015/03/security-issue-combining-bcrypt-with.html |
| 42 | + Pre-hashing: |
| 43 | + https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pre-hashing-passwords-with-bcrypt |
| 44 | + Work factor: |
| 45 | + https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#introduction |
28 | 46 | """ |
29 | | - base64_hmac_password: bytes = self._add_pepper(raw_password, self._pepper) |
30 | | - salt: bytes = bcrypt.gensalt() |
31 | | - return bcrypt.hashpw(base64_hmac_password, salt) |
| 47 | + log.debug("hash") |
| 48 | + base64_hmac_peppered: bytes = self._add_pepper(raw_password, self._pepper) |
| 49 | + salt: bytes = bcrypt.gensalt(rounds=self._work_factor) |
| 50 | + return bcrypt.hashpw(base64_hmac_peppered, salt) |
| 51 | + |
| 52 | + def verify_sync(self, raw_password: RawPassword, hashed_password: bytes) -> bool: |
| 53 | + log.debug("verify") |
| 54 | + base64_hmac_peppered: bytes = self._add_pepper(raw_password, self._pepper) |
| 55 | + return bcrypt.checkpw(base64_hmac_peppered, hashed_password) |
32 | 56 |
|
33 | 57 | @staticmethod |
34 | | - def _add_pepper(raw_password: RawPassword, pepper: PasswordPepper) -> bytes: |
| 58 | + def _add_pepper(raw_password: RawPassword, pepper: str) -> bytes: |
35 | 59 | hmac_password: bytes = hmac.new( |
36 | 60 | key=pepper.encode(), |
37 | 61 | msg=raw_password.value.encode(), |
38 | | - digestmod=hashlib.sha256, |
| 62 | + digestmod=hashlib.sha384, |
39 | 63 | ).digest() |
40 | 64 | return base64.b64encode(hmac_password) |
41 | | - |
42 | | - def verify(self, *, raw_password: RawPassword, hashed_password: bytes) -> bool: |
43 | | - base64_hmac_password: bytes = self._add_pepper(raw_password, self._pepper) |
44 | | - return bcrypt.checkpw(base64_hmac_password, hashed_password) |
|
0 commit comments