Files
MeshSate/app/security/crypto.py
zhaolei 16f134e328 feat: MeshSafe 文件摆渡安全卫士 v1.0 初始提交
核心功能:
- 6 层检测管线: L0 结构分析 → L1 ClamAV → L2 文档深度检测 → L3 关键词匹配 → L4 YARA → L5 PE/二进制
- FastAPI + PostgreSQL + Redis + Docker Compose 架构
- 客户端 HMAC-SHA256 签名鉴权 + 限流 + 幂等性
- 控制台 UI (登录/Dashboard/诊断中心/策略管理/任务列表/测试中心/产品介绍页)
- 三层诊断定时器 (error_scan 30s / db_aggregate 30s / container_check 5min)

并发与稳定性修复:
- 诊断定时器 Redis 时间槽位锁 (多 worker 同一槽位只有一个执行)
- JSONL 存储 fcntl 跨进程排他锁 + 容错解码 + 原子重写
- 移除 SQLite fallback, 统一 PostgreSQL
- API_CLIENTS 脱敏为环境变量, .env 已 .gitignore
2026-10-28 19:07:11 +08:00

107 lines
3.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""密码学基础工具:密钥派生、口令哈希、常数时间比较。"""
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"))