feat(#95): 安全模块 - RBAC角色权限+JWT认证+AES-256-GCM数据加密+安全审计哈希链+安全中间件(21个单元测试)

This commit is contained in:
bot_dev2
2026-08-11 07:02:47 +08:00
parent 0f382ce2d0
commit f07716c9fe
12 changed files with 619 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
"""数据加密与口令散列工具。
- DataEncryptor:AES-256-GCM(依赖 cryptography),密钥由主密钥经 HKDF 派生
- hash_password / verify_password:PBKDF2-HMAC-SHA256(stdlib)
"""
from __future__ import annotations
import base64
import hashlib
import hmac
import os
try:
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
_HAS_CRYPTO = True
except ImportError: # pragma: no cover
_HAS_CRYPTO = False
def hash_password(password: str, *, iterations: int = 120_000, salt: bytes | None = None) -> str:
"""PBKDF2-HMAC-SHA256 口令散列,输出 `pbkdf2$iterations$salt_b64$hash_b64`"""
salt = salt or os.urandom(16)
dk = hashlib.pbkdf2_hmac("sha256", password.encode(), salt, iterations)
return "pbkdf2${}${}${}".format(
iterations, base64.b64encode(salt).decode(), base64.b64encode(dk).decode())
def verify_password(password: str, stored: str) -> bool:
try:
scheme, iters, salt_b64, hash_b64 = stored.split("$")
if scheme != "pbkdf2":
return False
dk = hashlib.pbkdf2_hmac("sha256", password.encode(),
base64.b64decode(salt_b64), int(iters))
return hmac.compare_digest(dk, base64.b64decode(hash_b64))
except Exception:
return False
class DataEncryptor:
"""AES-256-GCM 字段级加密器(用于手机号/身份证等敏感字段落库加密)。"""
def __init__(self, master_key: bytes, *, info: bytes = b"wms-field-encryption"):
if not _HAS_CRYPTO:
raise RuntimeError("需要安装 cryptography 库以使用 AES-256-GCM 加密")
if len(master_key) < 16:
raise ValueError("master_key 长度至少 16 字节")
# HKDF-SHA256 派生 32 字节数据密钥
prk = hmac.new(b"wms-hkdf-salt", master_key, hashlib.sha256).digest()
self._key = hmac.new(prk, info + b"\x01", hashlib.sha256).digest()
self._aes = AESGCM(self._key)
def encrypt(self, plaintext: str) -> str:
nonce = os.urandom(12)
ct = self._aes.encrypt(nonce, plaintext.encode("utf-8"), None)
return base64.b64encode(nonce + ct).decode("ascii")
def decrypt(self, token: str) -> str:
raw = base64.b64decode(token)
nonce, ct = raw[:12], raw[12:]
return self._aes.decrypt(nonce, ct, None).decode("utf-8")