ShiErFeng/shierfeng-fastapi-backend/cli/runtime/crypto/support.py
2026-07-24 09:43:10 +08:00

235 lines
8.1 KiB
Python
Raw Permalink 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.

import json
from collections.abc import Callable
from typing import Any
from cli.exit_codes import ARGUMENT_ERROR, RUNTIME_ERROR
from .gateway import CryptoInfrastructureGateway
MIN_RSA_KEY_SIZE = 2048
RSA_KEY_SIZE_STEP = 256
class CryptoDomainSupport:
"""
传输加密领域支持对象。
该对象负责 RSA 密钥规则校验、密钥对生成和历史密钥规整,
避免主运行时服务继续承载局部安全规则和数据拼装细节。
:param infrastructure_gateway: 传输加密基础设施网关
"""
def __init__(self, infrastructure_gateway: CryptoInfrastructureGateway) -> None:
"""
初始化传输加密领域支持对象。
:param infrastructure_gateway: 传输加密基础设施网关
:return: None
"""
self.infrastructure_gateway = infrastructure_gateway
@staticmethod
def validate_rsa_key_size(key_size: int) -> None:
"""
校验 RSA 密钥长度是否合法。
:param key_size: RSA 密钥长度
:return: None
:raises ValueError: 密钥长度不满足约束时抛出异常
"""
if key_size < MIN_RSA_KEY_SIZE or key_size % RSA_KEY_SIZE_STEP != 0:
raise ValueError('RSA 密钥长度必须大于等于 2048且为 256 的整数倍')
def generate_rsa_key_pair(self, key_size: int) -> tuple[str, str]:
"""
生成 PEM 格式的 RSA 密钥对。
:param key_size: RSA 密钥长度
:return: 私钥与公钥 PEM 字符串
"""
rsa_module = self.infrastructure_gateway.get_rsa_module()
serialization_module = self.infrastructure_gateway.get_serialization_module()
private_key = rsa_module.generate_private_key(public_exponent=65537, key_size=key_size)
private_key_pem = private_key.private_bytes(
encoding=serialization_module.Encoding.PEM,
format=serialization_module.PrivateFormat.PKCS8,
encryption_algorithm=serialization_module.NoEncryption(),
).decode('utf-8')
public_key_pem = (
private_key.public_key()
.public_bytes(
encoding=serialization_module.Encoding.PEM,
format=serialization_module.PublicFormat.SubjectPublicKeyInfo,
)
.decode('utf-8')
)
return private_key_pem, public_key_pem
def load_existing_legacy_key_pairs(self) -> list[dict[str, str]]:
"""
解析当前配置中的历史密钥列表。
:return: 历史密钥列表
:raises ValueError: 历史密钥配置非法时抛出异常
"""
transport_crypto_config = self.infrastructure_gateway.get_transport_crypto_config()
raw_legacy_key_pairs = transport_crypto_config.transport_crypto_legacy_key_pairs or '[]'
parsed_legacy_key_pairs = json.loads(raw_legacy_key_pairs)
if not isinstance(parsed_legacy_key_pairs, list):
raise ValueError('TRANSPORT_CRYPTO_LEGACY_KEY_PAIRS 必须为 JSON 数组')
return [item for item in parsed_legacy_key_pairs if isinstance(item, dict)]
@staticmethod
def merge_rotation_legacy_key_pairs(
legacy_key_pairs: list[dict[str, str]],
current_key_pair: Any,
next_kid: str,
) -> list[dict[str, str]]:
"""
合并轮换后的历史密钥列表。
:param legacy_key_pairs: 当前历史密钥列表
:param current_key_pair: 当前生效密钥对对象
:param next_kid: 即将切换的新密钥版本
:return: 合并后的历史密钥列表
"""
normalized_legacy_key_pairs: dict[str, dict[str, str]] = {
str(item.get('kid')): item for item in legacy_key_pairs if item.get('kid')
}
normalized_legacy_key_pairs[current_key_pair.kid] = {
'kid': current_key_pair.kid,
'privateKey': current_key_pair.private_key_pem,
'publicKey': current_key_pair.public_key_pem,
}
normalized_legacy_key_pairs.pop(next_kid, None)
return list(normalized_legacy_key_pairs.values())
@staticmethod
def build_key_pair_env_patch(kid: str, private_key_pem: str, public_key_pem: str) -> dict[str, str]:
"""
构建密钥对写回环境变量所需的补丁数据。
:param kid: 密钥版本标识
:param private_key_pem: 私钥 PEM 文本
:param public_key_pem: 公钥 PEM 文本
:return: 环境变量补丁数据
"""
return {
'TRANSPORT_CRYPTO_KID': kid,
'TRANSPORT_CRYPTO_PRIVATE_KEY': private_key_pem,
'TRANSPORT_CRYPTO_PUBLIC_KEY': public_key_pem,
}
def build_rotation_env_patch(
self,
next_kid: str,
private_key_pem: str,
public_key_pem: str,
merged_legacy_key_pairs: list[dict[str, str]],
) -> dict[str, str]:
"""
构建密钥轮换写回环境变量所需的补丁数据。
:param next_kid: 新密钥版本标识
:param private_key_pem: 新私钥 PEM 文本
:param public_key_pem: 新公钥 PEM 文本
:param merged_legacy_key_pairs: 合并后的历史密钥列表
:return: 环境变量补丁数据
"""
env_patch = self.build_key_pair_env_patch(next_kid, private_key_pem, public_key_pem)
env_patch['TRANSPORT_CRYPTO_LEGACY_KEY_PAIRS'] = json.dumps(merged_legacy_key_pairs, ensure_ascii=False)
return env_patch
class CryptoResultSupport:
"""
传输加密结果支持对象。
该对象负责参数错误/运行时错误结果翻译,以及固定轮换提示文案构建,
避免主运行时服务继续承载重复异常映射与静态 payload 片段。
"""
@staticmethod
def build_argument_error_result(message: str, exc: Exception) -> dict[str, Any]:
"""
构建参数错误结果。
:param message: 失败消息
:param exc: 原始异常
:return: 标准参数错误结果
"""
return {
'ok': False,
'message': message,
'error': str(exc),
'exit_code': ARGUMENT_ERROR,
}
@staticmethod
def build_runtime_error_result(message: str, exc: Exception) -> dict[str, Any]:
"""
构建运行时错误结果。
:param message: 失败消息
:param exc: 原始异常
:return: 标准运行时错误结果
"""
return {
'ok': False,
'message': message,
'error': str(exc),
'exit_code': RUNTIME_ERROR,
}
def run_argument_guarded(
self,
operation: Callable[[], dict[str, Any]],
*,
failure_message: str,
) -> dict[str, Any]:
"""
以统一参数/运行时错误翻译执行操作。
:param operation: 待执行操作
:param failure_message: 失败消息
:return: 操作结果
"""
try:
return operation()
except ValueError as exc:
return self.build_argument_error_result(failure_message, exc)
except Exception as exc:
return self.build_runtime_error_result(failure_message, exc)
def run_runtime_guarded(
self,
operation: Callable[[], dict[str, Any]],
*,
failure_message: str,
) -> dict[str, Any]:
"""
以统一运行时错误翻译执行操作。
:param operation: 待执行操作
:param failure_message: 失败消息
:return: 操作结果
"""
try:
return operation()
except Exception as exc:
return self.build_runtime_error_result(failure_message, exc)
@staticmethod
def build_rotation_apply_steps() -> list[str]:
"""
构建密钥轮换后的应用步骤说明。
:return: 轮换应用步骤列表
"""
return [
'将 envPatch 中的新密钥写入目标环境配置',
'发布后保留 legacyKeyPairs确保旧请求在轮换窗口内可解密',
'确认前端已刷新到 nextKid 后,再移除不再需要的历史密钥',
]