"""密码学基础工具:密钥派生、口令哈希、常数时间比较。""" from __future__ import annotations import base64 import hashlib import hmac import os from typing import Tuple # 口令哈希迭代次数。等保三级要求存储口令不可逆,PBKDF2-HMAC-SHA256 # 迭代 60 万次在现代 CPU 上约 0.3~0.6 秒,兼顾抗暴力破解与可用性。 PBKDF2_ITERATIONS = 600_000 PBKDF2_ALGORITHM = "sha256" SALT_BYTES = 16 def derive_key(secret: str, salt: str, info: str, length: int = 32) -> bytes: """使用 HKDF-SHA256 从调用方密钥派生用途隔离的子密钥。 不同用途使用不同 ``info`` 标签,使得请求签名密钥与响应签名密钥相互独立。 """ prk = hmac.new( salt.encode("utf-8"), secret.encode("utf-8"), hashlib.sha256 ).digest() okm = b"" block = b"" counter = 1 while len(okm) < length: block = hmac.new(prk, block + info.encode("utf-8") + bytes([counter]), hashlib.sha256).digest() okm += block counter += 1 return okm[:length] def derive_client_signing_key(secret: str, client_id: str) -> bytes: return derive_key(secret, client_id, "meshsafe/api/v1/request-signing") def derive_client_response_key(secret: str, client_id: str) -> bytes: return derive_key(secret, client_id, "meshsafe/api/v1/response-signing") def hash_password(password: str) -> str: """生成 ``pbkdf2_sha256$迭代次数$盐$哈希`` 格式的口令哈希。""" salt = os.urandom(SALT_BYTES) dk = hashlib.pbkdf2_hmac( PBKDF2_ALGORITHM, password.encode("utf-8"), salt, PBKDF2_ITERATIONS ) return "$".join( [ "pbkdf2_sha256", str(PBKDF2_ITERATIONS), base64.b64encode(salt).decode("ascii"), base64.b64encode(dk).decode("ascii"), ] ) def verify_password(password: str, stored: str) -> bool: """校验口令。格式非法时返回 False,不抛出异常。""" try: algo, iterations, salt_b64, hash_b64 = stored.split("$") if algo != "pbkdf2_sha256": return False salt = base64.b64decode(salt_b64) expected = base64.b64decode(hash_b64) actual = hashlib.pbkdf2_hmac( PBKDF2_ALGORITHM, password.encode("utf-8"), salt, int(iterations) ) return hmac.compare_digest(actual, expected) except Exception: # noqa: BLE001 return False def needs_rehash(stored: str) -> bool: """判断存量口令哈希是否需要按当前参数重新计算。""" try: algo, iterations, _, _ = stored.split("$") return algo != "pbkdf2_sha256" or int(iterations) < PBKDF2_ITERATIONS except Exception: # noqa: BLE001 return True def sha256_bytes(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def sha256_file(path: str, chunk_size: int = 1024 * 1024) -> Tuple[str, int]: """流式计算文件 SHA256 与 MD5,返回 (sha256, md5, size)。""" sha = hashlib.sha256() md5 = hashlib.md5() size = 0 with open(path, "rb") as fh: while True: chunk = fh.read(chunk_size) if not chunk: break sha.update(chunk) md5.update(chunk) size += len(chunk) return sha.hexdigest(), md5.hexdigest(), size def constant_time_equals(a: str, b: str) -> bool: return hmac.compare_digest(a.encode("utf-8"), b.encode("utf-8"))