63 lines
2.3 KiB
Python
63 lines
2.3 KiB
Python
"""数据加密与口令散列工具。
|
||
|
||
- 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")
|