Files
xiaobaifupan/next/backend/security/passwords.py
T

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