Files
Suzu008-ComfyUI-CryptIO/src/cryptio/api.py
T
Suzu008 ec92bf6b4a refactor(api): 改进加密文件名生成逻辑
使用更安全的随机字符串生成方法,并保留原始文件名特征
确保生成的文件名唯一且避免冲突
2025-10-13 05:30:22 +08:00

177 lines
6.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.
import os
import string
import secrets
import json
import base64
from aiohttp import web
from server import PromptServer # pyright: ignore[reportMissingImports]
import folder_paths # pyright: ignore[reportMissingImports]
from .utils import _key_manager
from .utils.crypto_utils import (
encrypt_data_with_public_key,
generate_aes_key_and_iv,
encrypt_with_aes_cbc,
)
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.backends import default_backend
@PromptServer.instance.routes.post("/cryptio/exchange_keys")
async def exchange_keys(request):
"""
密钥交换端点:
1. 接收客户端公钥
2. 保存客户端公钥到服务器
3. 使用客户端公钥加密服务端密钥对
4. 返回加密后的服务端密钥
"""
try:
data = await request.json()
client_public_key_pem = data.get("client_public_key")
if not client_public_key_pem:
return web.json_response({"error": "client_public_key is required"}, status=400)
# 保存客户端公钥
client_public_key_bytes = client_public_key_pem.encode("utf-8")
_key_manager.update_client_key(client_public_key_bytes)
# 获取服务端密钥 PEM 格式
server_public_key_pem = _key_manager.server_public_key_pem
server_private_key_pem = _key_manager.server_private_key_pem
if not server_public_key_pem or not server_private_key_pem:
return web.json_response({"error": "Server keys not found"}, status=500)
# 使用客户端公钥加密服务端密钥
client_public_key = serialization.load_pem_public_key(
client_public_key_bytes, backend=default_backend()
)
# 准备要加密的数据(服务端公钥和私钥)
keys_data = json.dumps(
{
"server_public_key": server_public_key_pem.decode("utf-8"),
"server_private_key": server_private_key_pem.decode("utf-8"),
}
)
# 由于RSA加密有大小限制,使用混合加密方案(AES-CBC + RSA-OAEP)
# 1. 生成随机AES密钥和IV
aes_key, iv = generate_aes_key_and_iv()
# 2. 使用AES加密数据
encrypted_data = encrypt_with_aes_cbc(keys_data.encode("utf-8"), aes_key, iv)
# 3. 使用RSA加密AES密钥
encrypted_aes_key = encrypt_data_with_public_key(aes_key, client_public_key)
# 4. 返回加密后的数据
return web.json_response(
{
"encrypted_aes_key": base64.b64encode(encrypted_aes_key).decode("ascii"),
"iv": base64.b64encode(iv).decode("ascii"),
"encrypted_data": base64.b64encode(encrypted_data).decode("ascii"),
}
)
except Exception as e:
print(f"Error in key exchange: {e}")
import traceback
traceback.print_exc()
return web.json_response({"error": str(e)}, status=500)
@PromptServer.instance.routes.get("/cryptio/public_key")
async def get_public_key(request):
"""兼容旧的公钥获取接口(不加密传输)"""
pub_pem = _key_manager.server_public_key_pem
if pub_pem:
return web.json_response({"public_key": pub_pem.decode("utf-8")})
else:
return web.json_response({"error": "Public key not found"}, status=404)
@PromptServer.instance.routes.post("/cryptio/upload_encrypted")
async def upload_encrypted_image(request):
"""接收加密的图片数据并保存到文件"""
try:
# 读取 multipart 表单数据
reader = await request.multipart()
encrypted_data = None
original_filename = None
async for field in reader:
if field.name == "image":
# 读取加密的文件数据
encrypted_data = await field.read()
original_filename = field.filename
if not encrypted_data:
return web.json_response({"error": "No image data received"}, status=400)
# 生成唯一的加密文件名,保留原始文件名并追加随机字符串
input_dir = folder_paths.get_input_directory()
safe_original = os.path.basename(original_filename) if original_filename else "cryptio"
if safe_original.lower().endswith(".encrypted"):
safe_original = safe_original[:-len(".encrypted")]
base_name, file_ext = os.path.splitext(safe_original)
base_name = base_name.strip().replace(" ", "_") or "cryptio"
file_ext = file_ext or ".enc"
charset = string.ascii_lowercase + string.digits
def build_filename() -> str:
random_suffix = ''.join(secrets.choice(charset) for _ in range(6))
return f"{base_name}_{random_suffix}{file_ext}.encrypted"
unique_filename = build_filename()
while os.path.exists(os.path.join(input_dir, unique_filename)):
unique_filename = build_filename()
# 保存到 input 目录
file_path = os.path.join(input_dir, unique_filename)
# 写入加密数据
with open(file_path, "wb") as f:
f.write(encrypted_data)
return web.json_response({"name": unique_filename, "subfolder": "", "type": "input"})
except Exception as e:
print(f"Error uploading encrypted image: {e}")
return web.json_response({"error": str(e)}, status=500)
@PromptServer.instance.routes.get("/cryptio/view_encrypted")
async def view_encrypted_image(request):
"""获取加密图片用于前端预览(返回二进制加密数据,由前端解密)"""
try:
filename = request.query.get("filename")
if not filename:
return web.json_response({"error": "filename parameter is required"}, status=400)
# 获取文件路径
image_path = folder_paths.get_annotated_filepath(filename)
if not os.path.exists(image_path):
return web.json_response({"error": "File not found"}, status=404)
# 读取加密文件
with open(image_path, "rb") as f:
encrypted_data = f.read()
# 返回二进制数据,使用 Content-Disposition header 传递文件名
return web.Response(
body=encrypted_data, content_type="application/octet-stream", headers={"Content-Disposition": f'filename="{filename}"'}
)
except Exception as e:
print(f"Error viewing encrypted image: {e}")
return web.json_response({"error": str(e)}, status=500)