69 lines
2.3 KiB
Python
69 lines
2.3 KiB
Python
from __future__ import annotations
|
|
|
|
import base64
|
|
import hashlib
|
|
import hmac
|
|
import secrets
|
|
from dataclasses import dataclass
|
|
|
|
|
|
class PasswordPolicyError(ValueError):
|
|
pass
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class PasswordHasher:
|
|
work_factor: int = 2**15
|
|
block_size: int = 8
|
|
parallelism: int = 1
|
|
salt_bytes: int = 16
|
|
key_bytes: int = 32
|
|
|
|
def validate(self, password: str) -> None:
|
|
if not 8 <= len(password) <= 128:
|
|
raise PasswordPolicyError("密码长度应为8至128位。")
|
|
categories = (
|
|
any(character.isalpha() for character in password),
|
|
any(character.isdigit() for character in password),
|
|
any(not character.isalnum() for character in password),
|
|
)
|
|
if sum(categories) < 2:
|
|
raise PasswordPolicyError("密码至少包含字母、数字、符号中的两类。")
|
|
|
|
def hash(self, password: str) -> str:
|
|
self.validate(password)
|
|
salt = secrets.token_bytes(self.salt_bytes)
|
|
digest = hashlib.scrypt(
|
|
password.encode("utf-8"),
|
|
salt=salt,
|
|
n=self.work_factor,
|
|
r=self.block_size,
|
|
p=self.parallelism,
|
|
dklen=self.key_bytes,
|
|
maxmem=64 * 1024 * 1024,
|
|
)
|
|
encoded_salt = base64.urlsafe_b64encode(salt).decode("ascii")
|
|
encoded_digest = base64.urlsafe_b64encode(digest).decode("ascii")
|
|
return (
|
|
f"scrypt${self.work_factor}${self.block_size}${self.parallelism}"
|
|
f"${encoded_salt}${encoded_digest}"
|
|
)
|
|
|
|
def verify(self, password: str, encoded: str) -> bool:
|
|
try:
|
|
scheme, work_factor, block_size, parallelism, salt, expected = encoded.split("$")
|
|
if scheme != "scrypt":
|
|
return False
|
|
digest = hashlib.scrypt(
|
|
password.encode("utf-8"),
|
|
salt=base64.urlsafe_b64decode(salt.encode("ascii")),
|
|
n=int(work_factor),
|
|
r=int(block_size),
|
|
p=int(parallelism),
|
|
dklen=self.key_bytes,
|
|
maxmem=64 * 1024 * 1024,
|
|
)
|
|
return hmac.compare_digest(base64.urlsafe_b64encode(digest).decode("ascii"), expected)
|
|
except (TypeError, ValueError):
|
|
return False
|