codekingpro/portable-devtools
114k
1"""2A standalone, minimal htpasswd parser.3 4This implementation currently supports bcrypt and SHA1 passwords. SHA1 is insecure.5"""6 7from __future__ import annotations8 9import base6410import hashlib11from pathlib import Path12 13import bcrypt14 15 16class HtpasswdFile:17 def __init__(self, content: str):18 """19 Create a HtpasswdFile from a string.20 """21 self.users: dict[str, str] = {}22 for line in content.splitlines():23 line = line.strip()24 if not line or line.startswith("#"):25 continue26 if ":" not in line:27 raise ValueError(f"Malformed htpasswd line: {line!r}")28 user, pwhash = line.split(":", 1)29 if not user:30 raise ValueError(f"Malformed htpasswd line: {line!r}")31 32 is_sha = pwhash.startswith("{SHA}")33 is_bcrypt = pwhash.startswith(("$2y$", "$2b$", "$2a$"))34 if not is_sha and not is_bcrypt:35 raise ValueError(f"Unsupported htpasswd format for user {user!r}")36 37 self.users[user] = pwhash38 39 @classmethod40 def from_file(cls, path: Path) -> HtpasswdFile:41 """42 Initializes and loads an htpasswd file.43 44 Args:45 path: The path to the htpasswd file.46 47 Raises:48 OSError: If the file cannot be read.49 ValueError: If the file is malformed.50 """51 try:52 content = path.read_text("utf-8")53 except FileNotFoundError:54 raise OSError(f"Htpasswd file not found: {path}") from None55 return cls(content)56 57 def check_password(self, username: str, password: str) -> bool:58 """59 Checks if a username and password combination is valid.60 61 Args:62 username: The username to check.63 password: The password to check.64 65 Returns:66 True if the password is valid, False otherwise.67 """68 pwhash = self.users.get(username)69 if pwhash is None:70 return False71 72 pwhash = pwhash.split(":", 1)[0]73 74 if pwhash.startswith("{SHA}"):75 # Apache's {SHA} is base64-encoded SHA-1.76 # https://httpd.apache.org/docs/2.4/misc/password_encryptions.html77 digest = hashlib.sha1(password.encode("utf-8")).digest()78 expected = base64.b64encode(digest).decode("ascii")79 return pwhash[5:] == expected80 else: # pwhash.startswith(("$2y$", "$2b$", "$2a$")):81 return bcrypt.checkpw(password.encode("utf-8"), pwhash.encode("utf-8"))82 