security.py 2.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
  1. from __future__ import annotations
  2. import hashlib
  3. import hmac
  4. import secrets
  5. SCRYPT_N = 2**14
  6. SCRYPT_R = 8
  7. SCRYPT_P = 1
  8. SALT_BYTES = 16
  9. KEY_BYTES = 32
  10. def hash_password(password: str) -> str:
  11. if not password:
  12. raise ValueError("密码不能为空。")
  13. salt = secrets.token_bytes(SALT_BYTES)
  14. derived = hashlib.scrypt(
  15. password.encode("utf-8"),
  16. salt=salt,
  17. n=SCRYPT_N,
  18. r=SCRYPT_R,
  19. p=SCRYPT_P,
  20. dklen=KEY_BYTES,
  21. )
  22. return "$".join(
  23. [
  24. "scrypt",
  25. str(SCRYPT_N),
  26. str(SCRYPT_R),
  27. str(SCRYPT_P),
  28. salt.hex(),
  29. derived.hex(),
  30. ]
  31. )
  32. def verify_password(password: str, encoded: str) -> bool:
  33. try:
  34. algorithm, n_raw, r_raw, p_raw, salt_hex, expected_hex = encoded.split("$")
  35. if algorithm != "scrypt":
  36. return False
  37. derived = hashlib.scrypt(
  38. password.encode("utf-8"),
  39. salt=bytes.fromhex(salt_hex),
  40. n=int(n_raw),
  41. r=int(r_raw),
  42. p=int(p_raw),
  43. dklen=len(bytes.fromhex(expected_hex)),
  44. )
  45. except (TypeError, ValueError):
  46. return False
  47. return hmac.compare_digest(derived.hex(), expected_hex)
  48. def generate_session_token() -> str:
  49. return secrets.token_urlsafe(48)
  50. def generate_csrf_token() -> str:
  51. return secrets.token_urlsafe(32)
  52. def hash_token(token: str) -> str:
  53. return hashlib.sha256(token.encode("utf-8")).hexdigest()
  54. def validate_new_password(password: str, username: str = "admin") -> list[str]:
  55. errors: list[str] = []
  56. if len(password) < 10:
  57. errors.append("密码至少需要 10 个字符")
  58. if password.lower() == username.lower():
  59. errors.append("密码不能与用户名相同")
  60. if not any(character.isalpha() for character in password):
  61. errors.append("密码至少需要包含一个字母")
  62. if not any(character.isdigit() for character in password):
  63. errors.append("密码至少需要包含一个数字")
  64. return errors