Files
xiaozhi-esp32-server/main/manager-api-fastapi/app/core/crypto.py
T

64 lines
2.3 KiB
Python

from __future__ import annotations
import hashlib
import secrets
import uuid
import bcrypt
from gmssl import func, sm2 # type: ignore[import-untyped]
def generate_database_token(value: str | None = None) -> str:
source = value if value is not None else str(uuid.uuid4())
return hashlib.md5(source.encode("utf-8"), usedforsecurity=False).hexdigest()
def bcrypt_hash(password: str, rounds: int = 10) -> str:
encoded = bcrypt.hashpw(password.encode("utf-8"), bcrypt.gensalt(rounds=rounds))
# The bundled Java BCryptPasswordEncoder only accepts $2a$ hashes.
return encoded.decode("ascii").replace("$2b$", "$2a$", 1)
def bcrypt_matches(password: str, encoded: str | None) -> bool:
if not encoded or not encoded.startswith(("$2a$", "$2$")):
return False
normalized = encoded.replace("$2$", "$2a$", 1)
try:
return bcrypt.checkpw(password.encode("utf-8"), normalized.encode("ascii"))
except (ValueError, UnicodeEncodeError):
return False
def sm2_generate_keypair() -> tuple[str, str]:
private_key = func.random_hex(64)
helper = sm2.CryptSM2(private_key=private_key, public_key="", mode=1)
public_point = str(helper._kg(int(private_key, 16), sm2.default_ecc_table["g"])) # noqa: SLF001
public_key = "04" + public_point
return public_key, private_key
def sm2_encrypt_c1c3c2(public_key: str, plaintext: str) -> str:
helper = sm2.CryptSM2(private_key="", public_key=public_key, mode=1)
encrypted = helper.encrypt(plaintext.encode("utf-8"))
if encrypted is None:
raise ValueError("SM2 KDF returned an all-zero key")
# BouncyCastle's SM2Engine emits the uncompressed-point marker.
return "04" + bytes(encrypted).hex()
def sm2_decrypt_c1c3c2(private_key: str, ciphertext: str) -> str:
normalized = ciphertext.strip().lower()
if normalized.startswith("04"):
normalized = normalized[2:]
if len(normalized) < 128 + 64 or len(normalized) % 2:
raise ValueError("invalid SM2 C1C3C2 ciphertext")
helper = sm2.CryptSM2(private_key=private_key, public_key="", mode=1)
decrypted = helper.decrypt(bytes.fromhex(normalized))
if decrypted is None:
raise ValueError("SM2 decryption failed")
return bytes(decrypted).decode("utf-8")
def random_hex(length: int) -> str:
return secrets.token_hex((length + 1) // 2)[:length]