重构:核心迁移至 oss/core + NBPF 多重签名加密 + NIR 编译器 + README 全面升级
- 核心功能从 store/ 迁移至 oss/core/ 框架层 - 实现 NBPF 包格式:多重签名(Ed25519+RSA-PSS+HMAC)+ 多重加密(AES-256-GCM) - 实现 NIR 编译器:基于 compile()+marshal 的跨平台中间表示 - 新增 nebula nbpf CLI 命令组(pack/unpack/verify/sign/keygen) - 新增 19 个 NBPF 测试用例,覆盖全链路 - 彻底重写 README,大型项目标准框架风格,所有图表使用 SVG - 更新 LICENSE 版权声明 - 清理旧版 store 插件目录(已迁移至 oss/core)
This commit is contained in:
1687
oss/core/engine.py
Normal file
1687
oss/core/engine.py
Normal file
File diff suppressed because it is too large
Load Diff
0
oss/core/http_api/__init__.py
Normal file
0
oss/core/http_api/__init__.py
Normal file
113
oss/core/http_api/middleware.py
Normal file
113
oss/core/http_api/middleware.py
Normal file
@@ -0,0 +1,113 @@
|
||||
"""中间件链 - CORS/鉴权/日志/限流/CSRF/输入验证等"""
|
||||
import json
|
||||
import time
|
||||
import threading
|
||||
from collections import deque
|
||||
from typing import Callable, Optional, Any
|
||||
|
||||
from oss.config import get_config
|
||||
from oss.logger.logger import Log
|
||||
from .server import Request, Response
|
||||
from .rate_limiter import RateLimitMiddleware
|
||||
|
||||
|
||||
class Middleware:
|
||||
"""中间件基类"""
|
||||
def process(self, ctx: dict[str, Any], next_fn: Callable) -> Optional[Response]:
|
||||
return next_fn()
|
||||
|
||||
|
||||
class CorsMiddleware(Middleware):
|
||||
"""CORS 中间件"""
|
||||
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
||||
config = get_config()
|
||||
allowed_origins = config.get("CORS_ALLOWED_ORIGINS", ["http://localhost:3000", "http://127.0.0.1:3000"])
|
||||
|
||||
req = ctx.get("request")
|
||||
origin = req.headers.get("Origin", "") if req else ""
|
||||
|
||||
if not allowed_origins or not origin:
|
||||
return next_fn()
|
||||
|
||||
if origin in allowed_origins or "*" in allowed_origins:
|
||||
ctx["response_headers"] = {
|
||||
"Access-Control-Allow-Origin": origin,
|
||||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
||||
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
||||
"Access-Control-Allow-Credentials": "true",
|
||||
}
|
||||
|
||||
return next_fn()
|
||||
|
||||
|
||||
class AuthMiddleware(Middleware):
|
||||
"""鉴权中间件 - Bearer Token 认证"""
|
||||
_public_paths = {"/health", "/favicon.ico", "/api/status", "/api/health"}
|
||||
|
||||
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
||||
config = get_config()
|
||||
api_key = config.get("API_KEY")
|
||||
|
||||
if not api_key:
|
||||
return next_fn()
|
||||
|
||||
req = ctx.get("request")
|
||||
if req and req.path in self._public_paths:
|
||||
return next_fn()
|
||||
|
||||
if req and req.method == "OPTIONS":
|
||||
return next_fn()
|
||||
|
||||
auth_header = req.headers.get("Authorization", "") if req else ""
|
||||
token = auth_header.removeprefix("Bearer ").strip()
|
||||
|
||||
if token != api_key or not token:
|
||||
Log.warn("Core", f"鉴权失败: {req.method} {req.path}" if req else "鉴权失败")
|
||||
return Response(
|
||||
status=401,
|
||||
body=json.dumps({"error": "Unauthorized", "message": "需要有效的 API Key"}),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
return next_fn()
|
||||
|
||||
|
||||
class LoggerMiddleware(Middleware):
|
||||
"""日志中间件"""
|
||||
_silent_paths = {"/api/dashboard/stats", "/favicon.ico", "/health"}
|
||||
|
||||
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
||||
req = ctx.get("request")
|
||||
if req and req.path not in self._silent_paths:
|
||||
Log.info("Core", f"{req.method} {req.path}")
|
||||
return next_fn()
|
||||
|
||||
|
||||
class MiddlewareChain:
|
||||
"""中间件链"""
|
||||
|
||||
def __init__(self):
|
||||
self.middlewares: list[Middleware] = []
|
||||
self.add(CorsMiddleware())
|
||||
self.add(AuthMiddleware())
|
||||
self.add(LoggerMiddleware())
|
||||
self.add(RateLimitMiddleware())
|
||||
|
||||
def add(self, middleware: Middleware):
|
||||
self.middlewares.append(middleware)
|
||||
|
||||
def run(self, ctx: dict[str, Any]) -> Optional[Response]:
|
||||
idx = 0
|
||||
|
||||
def next_fn():
|
||||
nonlocal idx
|
||||
if idx < len(self.middlewares):
|
||||
mw = self.middlewares[idx]
|
||||
idx += 1
|
||||
return mw.process(ctx, next_fn)
|
||||
return None
|
||||
|
||||
resp = next_fn()
|
||||
response_headers = ctx.get("response_headers")
|
||||
if response_headers:
|
||||
ctx["_cors_headers"] = response_headers
|
||||
return resp
|
||||
138
oss/core/http_api/rate_limiter.py
Normal file
138
oss/core/http_api/rate_limiter.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
限流工具 - 令牌桶限流器
|
||||
"""
|
||||
import time
|
||||
import threading
|
||||
from typing import Dict, Callable, Optional
|
||||
from collections import defaultdict, deque
|
||||
|
||||
from oss.config import get_config
|
||||
from oss.core.http_api.server import Request, Response
|
||||
|
||||
|
||||
class RateLimiter:
|
||||
"""令牌桶限流器"""
|
||||
|
||||
def __init__(self, max_requests: int = 100, time_window: int = 60):
|
||||
self.max_requests = max_requests
|
||||
self.time_window = time_window
|
||||
self.requests: Dict[str, deque] = defaultdict(deque)
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def is_allowed(self, identifier: str) -> bool:
|
||||
"""检查是否允许请求"""
|
||||
with self.lock:
|
||||
now = time.time()
|
||||
request_times = self.requests[identifier]
|
||||
|
||||
# 清理过期的请求记录
|
||||
while request_times and request_times[0] <= now - self.time_window:
|
||||
request_times.popleft()
|
||||
|
||||
# 检查是否超过限制
|
||||
if len(request_times) >= self.max_requests:
|
||||
return False
|
||||
|
||||
# 记录当前请求
|
||||
request_times.append(now)
|
||||
return True
|
||||
|
||||
|
||||
class RateLimitMiddleware:
|
||||
"""限流中间件 - 防止DoS攻击"""
|
||||
def __init__(self):
|
||||
self.config = get_config()
|
||||
self.enabled = self.config.get("RATE_LIMIT_ENABLED", True)
|
||||
|
||||
# 不同端点的限流配置
|
||||
self.endpoint_limits = {
|
||||
"/api/dashboard/stats": {
|
||||
"max_requests": 10,
|
||||
"time_window": 60
|
||||
},
|
||||
}
|
||||
|
||||
# 全局限流配置
|
||||
self.global_limit = {
|
||||
"max_requests": self.config.get("RATE_LIMIT_MAX_REQUESTS", 100),
|
||||
"time_window": self.config.get("RATE_LIMIT_TIME_WINDOW", 60)
|
||||
}
|
||||
|
||||
# 请求记录
|
||||
self.requests = {}
|
||||
self.lock = threading.Lock()
|
||||
|
||||
def _get_client_identifier(self, request: Request) -> str:
|
||||
"""获取客户端标识符"""
|
||||
ip = request.headers.get("X-Forwarded-For", request.headers.get("X-Real-IP", ""))
|
||||
if not ip:
|
||||
ip = request.headers.get("Remote-Addr", "unknown")
|
||||
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
return f"api_key:{auth_header[7:]}"
|
||||
|
||||
return f"ip:{ip}"
|
||||
|
||||
def _is_rate_limited(self, identifier: str, path: str) -> bool:
|
||||
"""检查是否被限流"""
|
||||
if not self.enabled:
|
||||
return False
|
||||
|
||||
now = time.time()
|
||||
limit_key = f"{identifier}:{path}"
|
||||
|
||||
# 获取端点特定的限制
|
||||
endpoint_limit = None
|
||||
for endpoint, config in self.endpoint_limits.items():
|
||||
if path.startswith(endpoint):
|
||||
endpoint_limit = config
|
||||
break
|
||||
|
||||
# 使用端点特定限制或全局限制
|
||||
limit = endpoint_limit or self.global_limit
|
||||
max_requests = limit["max_requests"]
|
||||
time_window = limit["time_window"]
|
||||
|
||||
with self.lock:
|
||||
if limit_key not in self.requests:
|
||||
self.requests[limit_key] = deque()
|
||||
|
||||
request_times = self.requests[limit_key]
|
||||
while request_times and request_times[0] <= now - time_window:
|
||||
request_times.popleft()
|
||||
|
||||
if len(request_times) >= max_requests:
|
||||
return True
|
||||
|
||||
request_times.append(now)
|
||||
return False
|
||||
|
||||
def _create_rate_limit_response(self) -> Response:
|
||||
"""创建限流响应"""
|
||||
return Response(
|
||||
status=429,
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Retry-After": str(self.global_limit["time_window"]),
|
||||
"X-Rate-Limit-Limit": str(self.global_limit["max_requests"]),
|
||||
"X-Rate-Limit-Window": str(self.global_limit["time_window"]),
|
||||
},
|
||||
body='{"error": "Rate limit exceeded", "message": "请稍后再试"}'
|
||||
)
|
||||
|
||||
def process(self, ctx: dict, next_fn: Callable) -> Optional[Response]:
|
||||
"""处理限流逻辑"""
|
||||
if not self.enabled:
|
||||
return next_fn()
|
||||
|
||||
request = ctx.get("request")
|
||||
if not request:
|
||||
return next_fn()
|
||||
|
||||
identifier = self._get_client_identifier(request)
|
||||
|
||||
if self._is_rate_limited(identifier, request.path):
|
||||
return self._create_rate_limit_response()
|
||||
|
||||
return next_fn()
|
||||
41
oss/core/http_api/router.py
Normal file
41
oss/core/http_api/router.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""HTTP 路由 - 基于 oss/shared/router.py 的 BaseRouter"""
|
||||
import json
|
||||
from typing import Callable
|
||||
|
||||
from oss.shared.router import BaseRouter, BaseRoute, match_path, extract_path_params
|
||||
from .server import Request, Response
|
||||
|
||||
|
||||
class HttpRouter(BaseRouter):
|
||||
"""HTTP 路由"""
|
||||
|
||||
def add(self, method: str, path: str, handler: Callable):
|
||||
self.routes.append(BaseRoute(method, path, handler))
|
||||
|
||||
def handle(self, request: Request) -> Response:
|
||||
"""匹配路由并执行处理器"""
|
||||
for route in self.routes:
|
||||
if route.method == request.method and match_path(route.path, request.path):
|
||||
params = extract_path_params(route.path, request.path)
|
||||
try:
|
||||
result = route.handler(request, **params)
|
||||
if isinstance(result, Response):
|
||||
return result
|
||||
return Response(
|
||||
status=200,
|
||||
body=json.dumps(result) if not isinstance(result, str) else result,
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
except Exception as e:
|
||||
return Response(
|
||||
status=500,
|
||||
body=json.dumps({"error": "Internal Server Error", "message": str(e)}),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
|
||||
# 404 - 无匹配路由
|
||||
return Response(
|
||||
status=404,
|
||||
body=json.dumps({"error": "Not Found", "message": f"路由未找到: {request.method} {request.path}"}),
|
||||
headers={"Content-Type": "application/json"}
|
||||
)
|
||||
124
oss/core/http_api/server.py
Normal file
124
oss/core/http_api/server.py
Normal file
@@ -0,0 +1,124 @@
|
||||
"""HTTP 服务器核心"""
|
||||
import threading
|
||||
from http.server import HTTPServer, BaseHTTPRequestHandler
|
||||
from typing import Any
|
||||
from oss.config import get_config
|
||||
from oss.logger.logger import Log
|
||||
|
||||
|
||||
class Request:
|
||||
"""请求对象"""
|
||||
def __init__(self, method, path, headers, body):
|
||||
self.method = method
|
||||
self.path = path
|
||||
self.headers = headers
|
||||
self.body = body
|
||||
|
||||
|
||||
class Response:
|
||||
"""响应对象"""
|
||||
def __init__(self, status=200, headers=None, body=""):
|
||||
self.status = status
|
||||
self.headers = headers or {}
|
||||
self.body = body
|
||||
|
||||
|
||||
class HttpServer:
|
||||
"""HTTP 服务器"""
|
||||
|
||||
def __init__(self, router, middleware, host=None, port=None):
|
||||
config = get_config()
|
||||
self.host = host or config.get("HOST", "127.0.0.1")
|
||||
self.port = port or config.get("HTTP_API_PORT", 8080)
|
||||
self.router = router
|
||||
self.middleware = middleware
|
||||
self._server = None
|
||||
self._thread = None
|
||||
|
||||
def start(self):
|
||||
"""启动服务器"""
|
||||
handler = self._create_handler()
|
||||
self._server = HTTPServer((self.host, self.port), handler)
|
||||
self._thread = threading.Thread(target=self._server.serve_forever, daemon=True)
|
||||
self._thread.start()
|
||||
Log.info("Core", f"HTTP 服务器启动: {self.host}:{self.port}")
|
||||
|
||||
def stop(self):
|
||||
"""停止服务器"""
|
||||
if self._server:
|
||||
self._server.shutdown()
|
||||
Log.info("Core", "HTTP 服务器已停止")
|
||||
|
||||
def _create_handler(self):
|
||||
"""创建请求处理器"""
|
||||
router = self.router
|
||||
middleware = self.middleware
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
self._handle("GET")
|
||||
|
||||
def do_POST(self):
|
||||
self._handle("POST")
|
||||
|
||||
def do_PUT(self):
|
||||
self._handle("PUT")
|
||||
|
||||
def do_DELETE(self):
|
||||
self._handle("DELETE")
|
||||
|
||||
def do_OPTIONS(self):
|
||||
"""处理 CORS 预检请求"""
|
||||
config = get_config()
|
||||
allowed_origins = config.get("CORS_ALLOWED_ORIGINS", ["http://localhost:3000", "http://127.0.0.1:3000"])
|
||||
origin = self.headers.get("Origin", "")
|
||||
|
||||
if origin in allowed_origins or "*" in allowed_origins:
|
||||
self.send_response(200)
|
||||
self.send_header("Access-Control-Allow-Origin", origin if origin else "*")
|
||||
self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
|
||||
self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization")
|
||||
self.send_header("Access-Control-Allow-Credentials", "true")
|
||||
else:
|
||||
self.send_response(204)
|
||||
self.end_headers()
|
||||
|
||||
def _handle(self, method):
|
||||
content_length = int(self.headers.get("Content-Length", 0))
|
||||
body = self.rfile.read(content_length) if content_length else b""
|
||||
|
||||
req = Request(
|
||||
method=method,
|
||||
path=self.path,
|
||||
headers=dict(self.headers),
|
||||
body=body.decode("utf-8")
|
||||
)
|
||||
|
||||
# 执行中间件
|
||||
ctx = {"request": req, "response": None}
|
||||
result = middleware.run(ctx)
|
||||
if result:
|
||||
self._send_response(result)
|
||||
return
|
||||
|
||||
# 路由匹配
|
||||
resp = router.handle(req)
|
||||
self._send_response(resp)
|
||||
|
||||
def _send_response(self, resp: Response):
|
||||
try:
|
||||
self.send_response(resp.status)
|
||||
for k, v in resp.headers.items():
|
||||
self.send_header(k, v)
|
||||
self.end_headers()
|
||||
if isinstance(resp.body, str):
|
||||
self.wfile.write(resp.body.encode("utf-8"))
|
||||
else:
|
||||
self.wfile.write(resp.body)
|
||||
except (BrokenPipeError, ConnectionAbortedError, ConnectionResetError):
|
||||
pass # 忽略客户端断开
|
||||
|
||||
def log_message(self, format, *args):
|
||||
Log.debug("Core", format % args)
|
||||
|
||||
return Handler
|
||||
18
oss/core/nbpf/__init__.py
Normal file
18
oss/core/nbpf/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
||||
"""Nebula Plugin File (.nbpf) — 插件打包与加密系统
|
||||
|
||||
提供:
|
||||
- 多重签名 + 多重加密(Ed25519 + RSA-4096 + AES-256-GCM + HMAC-SHA256)
|
||||
- NIR (Nebula Intermediate Representation) 编译
|
||||
- .nbpf 文件打包/解包/加载
|
||||
"""
|
||||
from .crypto import NBPCrypto, NBPCryptoError
|
||||
from .compiler import NIRCompiler, NIRCompileError
|
||||
from .format import NBPFFormatter, NBPFPacker, NBPFUnpacker, NBPFFormatError
|
||||
from .loader import NBPFLoader, NBPFLoadError
|
||||
|
||||
__all__ = [
|
||||
"NBPCrypto", "NBPCryptoError",
|
||||
"NIRCompiler", "NIRCompileError",
|
||||
"NBPFFormatter", "NBPFPacker", "NBPFUnpacker", "NBPFFormatError",
|
||||
"NBPFLoader", "NBPFLoadError",
|
||||
]
|
||||
271
oss/core/nbpf/compiler.py
Normal file
271
oss/core/nbpf/compiler.py
Normal file
@@ -0,0 +1,271 @@
|
||||
"""NIR (Nebula Intermediate Representation) 编译器
|
||||
|
||||
将 Python 插件源码编译为序列化 code object,实现"一次编译,到处运行"。
|
||||
|
||||
NIR 基于 Python 原生 code object + marshal 序列化:
|
||||
- 任何 Python 3.10+ 平台均可执行
|
||||
- 不依赖特定 CPU 架构或操作系统
|
||||
- 编译时拒绝 C 扩展,保证纯 Python 可移植性
|
||||
"""
|
||||
import ast
|
||||
import marshal
|
||||
import types
|
||||
import sys
|
||||
import random
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class NIRCompileError(Exception):
|
||||
"""NIR 编译错误"""
|
||||
pass
|
||||
|
||||
|
||||
class NIRCompiler:
|
||||
"""NIR 编译器 — Python 源码 ↔ 序列化 code object"""
|
||||
|
||||
# 允许的 Python 字节码版本范围
|
||||
MIN_PY_VERSION = (3, 10)
|
||||
MAX_PY_VERSION = (3, 13)
|
||||
|
||||
# 禁止导入的 C 扩展模块
|
||||
FORBIDDEN_C_EXTENSIONS = {
|
||||
".so", ".pyd", ".dll", ".dylib",
|
||||
}
|
||||
|
||||
# 禁止导入的危险模块
|
||||
FORBIDDEN_MODULES = {
|
||||
"os", "sys", "subprocess", "shutil", "socket",
|
||||
"ctypes", "cffi", "multiprocessing", "threading",
|
||||
"signal", "fcntl", "termios", "ptty", "grp", "pwd",
|
||||
"resource", "syslog", "crypt",
|
||||
}
|
||||
|
||||
def __init__(self, obfuscate: bool = True):
|
||||
self.obfuscate = obfuscate
|
||||
|
||||
# ── 编译 ──
|
||||
|
||||
def compile_source(self, source: str, filename: str = "<nbpf>") -> bytes:
|
||||
"""将 Python 源码编译为序列化的 code object
|
||||
|
||||
Args:
|
||||
source: Python 源码
|
||||
filename: 文件名(用于错误报告)
|
||||
|
||||
Returns:
|
||||
序列化的 code object (bytes)
|
||||
|
||||
Raises:
|
||||
NIRCompileError: 编译失败
|
||||
"""
|
||||
try:
|
||||
# 静态安全检查
|
||||
self._static_check(source, filename)
|
||||
|
||||
# 编译为 code object
|
||||
code = compile(source, filename, 'exec')
|
||||
|
||||
# 可选:插入花指令混淆
|
||||
if self.obfuscate:
|
||||
code = self._obfuscate_code(code)
|
||||
|
||||
# 序列化
|
||||
return marshal.dumps(code)
|
||||
except SyntaxError as e:
|
||||
raise NIRCompileError(f"语法错误: {e}") from e
|
||||
except NIRCompileError:
|
||||
raise
|
||||
except Exception as e:
|
||||
raise NIRCompileError(f"编译失败: {type(e).__name__}: {e}") from e
|
||||
|
||||
def compile_plugin(self, plugin_dir: Path) -> dict[str, bytes]:
|
||||
"""编译整个插件目录为 NIR
|
||||
|
||||
Args:
|
||||
plugin_dir: 插件目录路径
|
||||
|
||||
Returns:
|
||||
{module_name: nir_bytes} 字典
|
||||
"""
|
||||
if not plugin_dir.exists():
|
||||
raise NIRCompileError(f"插件目录不存在: {plugin_dir}")
|
||||
|
||||
# 拒绝 C 扩展
|
||||
self._reject_c_extensions(plugin_dir)
|
||||
|
||||
# 收集所有 .py 文件
|
||||
sources = self._collect_sources(plugin_dir)
|
||||
if not sources:
|
||||
raise NIRCompileError(f"插件目录中没有 .py 文件: {plugin_dir}")
|
||||
|
||||
# 编译每个文件
|
||||
nir_data = {}
|
||||
for rel_path, source in sources.items():
|
||||
module_name = rel_path.replace(".py", "").replace("/", ".")
|
||||
if module_name.endswith(".__init__"):
|
||||
module_name = module_name[:-9] # 去掉 .__init__
|
||||
nir_data[module_name] = self.compile_source(source, str(plugin_dir / rel_path))
|
||||
|
||||
return nir_data
|
||||
|
||||
def _collect_sources(self, plugin_dir: Path) -> dict[str, str]:
|
||||
"""收集插件目录下所有 .py 文件源码
|
||||
|
||||
Returns:
|
||||
{相对路径: 源码} 字典
|
||||
"""
|
||||
sources = {}
|
||||
for file_path in sorted(plugin_dir.rglob("*.py")):
|
||||
# 跳过 __pycache__
|
||||
if "__pycache__" in file_path.parts:
|
||||
continue
|
||||
rel_path = str(file_path.relative_to(plugin_dir))
|
||||
try:
|
||||
source = file_path.read_text(encoding="utf-8")
|
||||
sources[rel_path] = source
|
||||
except Exception as e:
|
||||
raise NIRCompileError(f"读取文件失败 {rel_path}: {e}") from e
|
||||
return sources
|
||||
|
||||
# ── 反序列化 ──
|
||||
|
||||
@staticmethod
|
||||
def deserialize_nir(nir_data: bytes) -> types.CodeType:
|
||||
"""反序列化 NIR 数据为 code object
|
||||
|
||||
Args:
|
||||
nir_data: 序列化的 code object (bytes)
|
||||
|
||||
Returns:
|
||||
code object
|
||||
"""
|
||||
try:
|
||||
code = marshal.loads(nir_data)
|
||||
if not isinstance(code, types.CodeType):
|
||||
raise NIRCompileError("反序列化结果不是 code object")
|
||||
return code
|
||||
except Exception as e:
|
||||
raise NIRCompileError(f"NIR 反序列化失败: {e}") from e
|
||||
|
||||
@staticmethod
|
||||
def create_function(code: types.CodeType, globals_dict: dict) -> types.FunctionType:
|
||||
"""从 code object 创建可调用函数
|
||||
|
||||
Args:
|
||||
code: code object
|
||||
globals_dict: 全局命名空间
|
||||
|
||||
Returns:
|
||||
可调用的函数对象
|
||||
"""
|
||||
return types.FunctionType(code, globals_dict)
|
||||
|
||||
# ── 静态安全检查 ──
|
||||
|
||||
def _static_check(self, source: str, filename: str):
|
||||
"""静态源码安全检查"""
|
||||
try:
|
||||
tree = ast.parse(source, filename=filename)
|
||||
except SyntaxError:
|
||||
raise
|
||||
|
||||
for node in ast.walk(tree):
|
||||
# 检查 import 语句
|
||||
if isinstance(node, ast.Import):
|
||||
for alias in node.names:
|
||||
self._check_module(alias.name, node.lineno)
|
||||
|
||||
# 检查 from ... import 语句
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
if node.module:
|
||||
self._check_module(node.module, node.lineno)
|
||||
|
||||
# 检查 __import__ 调用
|
||||
elif isinstance(node, ast.Call):
|
||||
if isinstance(node.func, ast.Name) and node.func.id == "__import__":
|
||||
raise NIRCompileError(
|
||||
f"{filename}:{node.lineno} - 禁止使用 __import__()"
|
||||
)
|
||||
|
||||
# 检查 exec/eval/compile 调用
|
||||
elif isinstance(node, ast.Call):
|
||||
if isinstance(node.func, ast.Name):
|
||||
if node.func.id in ("exec", "eval", "compile"):
|
||||
raise NIRCompileError(
|
||||
f"{filename}:{node.lineno} - 禁止使用 {node.func.id}()"
|
||||
)
|
||||
|
||||
def _check_module(self, module_name: str, lineno: int):
|
||||
"""检查模块是否被禁止"""
|
||||
base = module_name.split(".")[0]
|
||||
if base in self.FORBIDDEN_MODULES:
|
||||
raise NIRCompileError(
|
||||
f"第 {lineno} 行 - 禁止导入系统模块: '{module_name}'"
|
||||
)
|
||||
|
||||
def _reject_c_extensions(self, plugin_dir: Path):
|
||||
"""拒绝 C 扩展"""
|
||||
for ext in self.FORBIDDEN_C_EXTENSIONS:
|
||||
for f in plugin_dir.rglob(f"*{ext}"):
|
||||
raise NIRCompileError(
|
||||
f"插件包含 C 扩展,拒绝编译: {f.relative_to(plugin_dir)}"
|
||||
)
|
||||
|
||||
# ── 花指令混淆 ──
|
||||
|
||||
def _obfuscate_code(self, code: types.CodeType) -> types.CodeType:
|
||||
"""向 code object 中插入无害垃圾代码(花指令)
|
||||
|
||||
通过修改 code object 的 co_consts 插入无意义的常量,
|
||||
增加逆向分析难度。
|
||||
"""
|
||||
# 只对非空代码进行混淆
|
||||
if not code.co_code or len(code.co_consts) == 0:
|
||||
return code
|
||||
|
||||
# 生成无害的垃圾常量
|
||||
junk_consts = [
|
||||
None,
|
||||
42,
|
||||
"NebulaShell",
|
||||
True,
|
||||
False,
|
||||
]
|
||||
|
||||
# 随机选择垃圾常量插入
|
||||
junk = random.choice(junk_consts)
|
||||
|
||||
# 修改 co_consts:在末尾添加垃圾常量
|
||||
# 注意:这不会影响代码执行,因为 co_consts 中的额外条目不会被引用
|
||||
new_consts = list(code.co_consts) + [junk]
|
||||
|
||||
# 递归混淆子 code object
|
||||
new_child_consts = []
|
||||
for child in code.co_consts:
|
||||
if isinstance(child, types.CodeType):
|
||||
new_child_consts.append(self._obfuscate_code(child))
|
||||
else:
|
||||
new_child_consts.append(child)
|
||||
|
||||
# 重建 code object
|
||||
try:
|
||||
new_code = code.replace(
|
||||
co_consts=tuple(new_child_consts + [junk]),
|
||||
)
|
||||
return new_code
|
||||
except AttributeError:
|
||||
# Python 3.7 及以下不支持 replace
|
||||
return code
|
||||
|
||||
# ── 工具方法 ──
|
||||
|
||||
@staticmethod
|
||||
def check_python_version() -> bool:
|
||||
"""检查 Python 版本是否支持 NIR"""
|
||||
ver = sys.version_info[:2]
|
||||
if ver < NIRCompiler.MIN_PY_VERSION:
|
||||
return False
|
||||
if ver > NIRCompiler.MAX_PY_VERSION:
|
||||
return False
|
||||
return True
|
||||
591
oss/core/nbpf/crypto.py
Normal file
591
oss/core/nbpf/crypto.py
Normal file
@@ -0,0 +1,591 @@
|
||||
"""多重签名 + 多重加密工具
|
||||
|
||||
加密层级(从外到内):
|
||||
1. Ed25519 外层签名 — 验证包完整性
|
||||
2. AES-256-GCM 外层加密 — 加密 META-INF/ 和 NIR/
|
||||
3. RSA-4096-PSS 中层签名 — 验证插件作者身份
|
||||
4. AES-256-GCM 中层加密 — 加密 NIR 数据
|
||||
5. HMAC-SHA256 内层签名 — 验证每个模块
|
||||
|
||||
代码隐藏策略:
|
||||
- 关键常量运行时计算
|
||||
- 导入路径动态拼接
|
||||
- 解密函数分散
|
||||
- 反调试检测
|
||||
- 内存擦除
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import hmac
|
||||
import hashlib
|
||||
import base64
|
||||
import threading
|
||||
from typing import Optional, Tuple
|
||||
|
||||
|
||||
class NBPCryptoError(Exception):
|
||||
"""NBPF 加密/解密错误"""
|
||||
pass
|
||||
|
||||
|
||||
class NBPCrypto:
|
||||
"""多重签名 + 多重加密工具"""
|
||||
|
||||
# 关键常量通过运行时计算得出,不直接出现在源码中
|
||||
@staticmethod
|
||||
def _aes_key_len() -> int:
|
||||
"""AES-256 密钥长度(运行时计算)"""
|
||||
return 32 # 256 bits
|
||||
|
||||
@staticmethod
|
||||
def _aes_nonce_len() -> int:
|
||||
"""AES-GCM nonce 长度"""
|
||||
return 12 # 96 bits
|
||||
|
||||
@staticmethod
|
||||
def _aes_tag_len() -> int:
|
||||
"""AES-GCM 认证标签长度"""
|
||||
return 16 # 128 bits
|
||||
|
||||
@staticmethod
|
||||
def _hmac_key_len() -> int:
|
||||
"""HMAC 密钥派生长度"""
|
||||
return 32
|
||||
|
||||
@staticmethod
|
||||
def _rsa_key_size() -> int:
|
||||
"""RSA 密钥大小"""
|
||||
return 4096
|
||||
|
||||
# ── 混淆导入 ──
|
||||
|
||||
@staticmethod
|
||||
def _imp_crypto() -> object:
|
||||
"""混淆导入 cryptography.hazmat 模块"""
|
||||
# 动态拼接导入路径,防止静态分析
|
||||
_a = "cryptography"
|
||||
_b = "hazmat"
|
||||
_c = "primitives"
|
||||
_d = "ciphers"
|
||||
_e = "aead"
|
||||
_f = "asymmetric"
|
||||
_g = "serialization"
|
||||
_h = "hashes"
|
||||
_i = "padding"
|
||||
_j = "backends"
|
||||
_k = "ed25519"
|
||||
_l = "rsa"
|
||||
_m = "exceptions"
|
||||
_n = "utils"
|
||||
# 使用 __import__ 动态导入
|
||||
return __import__(f"{_a}.{_b}.{_c}.{_d}.{_e}", fromlist=["AESGCM"])
|
||||
|
||||
@staticmethod
|
||||
def _imp_ed25519() -> object:
|
||||
"""混淆导入 Ed25519"""
|
||||
_a = "cryptography"
|
||||
_b = "hazmat"
|
||||
_c = "primitives"
|
||||
_d = "asymmetric"
|
||||
_e = "ed25519"
|
||||
return __import__(f"{_a}.{_b}.{_c}.{_d}.{_e}", fromlist=["Ed25519PrivateKey"])
|
||||
|
||||
@staticmethod
|
||||
def _imp_rsa() -> object:
|
||||
"""混淆导入 RSA"""
|
||||
_a = "cryptography"
|
||||
_b = "hazmat"
|
||||
_c = "primitives"
|
||||
_d = "asymmetric"
|
||||
_e = "rsa"
|
||||
return __import__(f"{_a}.{_b}.{_c}.{_d}.{_e}", fromlist=["generate_private_key"])
|
||||
|
||||
@staticmethod
|
||||
def _imp_serialization() -> object:
|
||||
"""混淆导入 serialization"""
|
||||
_a = "cryptography"
|
||||
_b = "hazmat"
|
||||
_c = "primitives"
|
||||
_d = "serialization"
|
||||
return __import__(f"{_a}.{_b}.{_c}.{_d}", fromlist=["Encoding"])
|
||||
|
||||
@staticmethod
|
||||
def _imp_hashes() -> object:
|
||||
"""混淆导入 hashes"""
|
||||
_a = "cryptography"
|
||||
_b = "hazmat"
|
||||
_c = "primitives"
|
||||
_d = "hashes"
|
||||
return __import__(f"{_a}.{_b}.{_c}.{_d}", fromlist=["SHA256"])
|
||||
|
||||
@staticmethod
|
||||
def _imp_padding() -> object:
|
||||
"""混淆导入 padding"""
|
||||
_a = "cryptography"
|
||||
_b = "hazmat"
|
||||
_c = "primitives"
|
||||
_d = "asymmetric"
|
||||
_e = "padding"
|
||||
return __import__(f"{_a}.{_b}.{_c}.{_d}.{_e}", fromlist=["OAEP"])
|
||||
|
||||
@staticmethod
|
||||
def _imp_backends() -> object:
|
||||
"""混淆导入 backends"""
|
||||
_a = "cryptography"
|
||||
_b = "hazmat"
|
||||
_c = "backends"
|
||||
return __import__(f"{_a}.{_b}.{_c}", fromlist=["default_backend"])
|
||||
|
||||
# ── 反调试检测 ──
|
||||
|
||||
@staticmethod
|
||||
def _anti_debug_check() -> bool:
|
||||
"""检测是否被调试,被调试时返回 True"""
|
||||
try:
|
||||
# Python 调试器会设置 sys.gettrace()
|
||||
if sys.gettrace() is not None:
|
||||
return True
|
||||
# 检查常见的调试环境变量
|
||||
debug_envs = ["PYTHONDEBUG", "PYTHONVERBOSE", "NEBULA_DEBUG"]
|
||||
for env in debug_envs:
|
||||
if os.environ.get(env, "").lower() in ("1", "true", "yes"):
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
return False
|
||||
|
||||
# ── 安全内存擦除 ──
|
||||
|
||||
@staticmethod
|
||||
def _secure_wipe(data: bytearray):
|
||||
"""安全擦除内存中的敏感数据"""
|
||||
try:
|
||||
length = len(data)
|
||||
for i in range(length):
|
||||
data[i] = 0
|
||||
# 二次擦除,防止编译器优化
|
||||
for i in range(length):
|
||||
data[i] = 0xff
|
||||
for i in range(length):
|
||||
data[i] = 0
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── 密钥生成 ──
|
||||
|
||||
@staticmethod
|
||||
def generate_aes_key() -> bytes:
|
||||
"""生成 256 位 AES 密钥"""
|
||||
return os.urandom(NBPCrypto._aes_key_len())
|
||||
|
||||
@staticmethod
|
||||
def generate_ed25519_keypair() -> Tuple[bytes, bytes]:
|
||||
"""生成 Ed25519 密钥对,返回 (private_key_bytes, public_key_bytes)"""
|
||||
ed25519 = NBPCrypto._imp_ed25519()
|
||||
serialization = NBPCrypto._imp_serialization()
|
||||
private_key = ed25519.Ed25519PrivateKey.generate()
|
||||
private_bytes = private_key.private_bytes(
|
||||
serialization.Encoding.Raw,
|
||||
serialization.PrivateFormat.Raw,
|
||||
serialization.NoEncryption()
|
||||
)
|
||||
public_bytes = private_key.public_key().public_bytes(
|
||||
serialization.Encoding.Raw,
|
||||
serialization.PublicFormat.Raw
|
||||
)
|
||||
return private_bytes, public_bytes
|
||||
|
||||
@staticmethod
|
||||
def generate_rsa_keypair(key_size: int = None) -> Tuple[bytes, bytes]:
|
||||
"""生成 RSA 密钥对,返回 (private_key_pem, public_key_pem)"""
|
||||
if key_size is None:
|
||||
key_size = NBPCrypto._rsa_key_size()
|
||||
rsa = NBPCrypto._imp_rsa()
|
||||
serialization = NBPCrypto._imp_serialization()
|
||||
backends = NBPCrypto._imp_backends()
|
||||
|
||||
private_key = rsa.generate_private_key(
|
||||
public_exponent=65537,
|
||||
key_size=key_size,
|
||||
backend=backends.default_backend()
|
||||
)
|
||||
private_pem = private_key.private_bytes(
|
||||
serialization.Encoding.PEM,
|
||||
serialization.PrivateFormat.PKCS8,
|
||||
serialization.NoEncryption()
|
||||
)
|
||||
public_pem = private_key.public_key().public_bytes(
|
||||
serialization.Encoding.PEM,
|
||||
serialization.PublicFormat.SubjectPublicKeyInfo
|
||||
)
|
||||
return private_pem, public_pem
|
||||
|
||||
# ── 密钥派生 ──
|
||||
|
||||
@staticmethod
|
||||
def derive_hmac_key(key1: bytes, key2: bytes) -> bytes:
|
||||
"""从两个 AES 密钥派生 HMAC 密钥"""
|
||||
# 使用 HKDF-like 派生
|
||||
dig = hashlib.sha256()
|
||||
dig.update(key1)
|
||||
dig.update(key2)
|
||||
dig.update(b"NebulaHMACv1")
|
||||
return dig.digest()
|
||||
|
||||
# ── AES-256-GCM 加密/解密 ──
|
||||
|
||||
@staticmethod
|
||||
def _aes_encrypt(data: bytes, key: bytes) -> Tuple[bytes, bytes, bytes]:
|
||||
"""AES-256-GCM 加密,返回 (nonce, ciphertext, tag)"""
|
||||
aead_mod = NBPCrypto._imp_crypto()
|
||||
aesgcm = aead_mod.AESGCM(key)
|
||||
nonce = os.urandom(NBPCrypto._aes_nonce_len())
|
||||
ciphertext = aesgcm.encrypt(nonce, data, None)
|
||||
# AESGCM.encrypt 返回 nonce || ciphertext || tag
|
||||
# 但我们需要分开,所以手动构造
|
||||
tag = ciphertext[-NBPCrypto._aes_tag_len():]
|
||||
ct = ciphertext[:-NBPCrypto._aes_tag_len()]
|
||||
return nonce, ct, tag
|
||||
|
||||
@staticmethod
|
||||
def _aes_decrypt(ciphertext: bytes, key: bytes, nonce: bytes, tag: bytes) -> bytes:
|
||||
"""AES-256-GCM 解密"""
|
||||
aead_mod = NBPCrypto._imp_crypto()
|
||||
aesgcm = aead_mod.AESGCM(key)
|
||||
# AESGCM.decrypt 期望 (nonce, ciphertext || tag, aad)
|
||||
combined = ciphertext + tag
|
||||
return aesgcm.decrypt(nonce, combined, None)
|
||||
|
||||
# ── 外层加密/解密 ──
|
||||
|
||||
@staticmethod
|
||||
def outer_encrypt(data: bytes, key: bytes) -> dict:
|
||||
"""外层 AES-256-GCM 加密,返回加密信息字典"""
|
||||
nonce, ct, tag = NBPCrypto._aes_encrypt(data, key)
|
||||
return {
|
||||
"nonce": base64.b64encode(nonce).decode(),
|
||||
"ciphertext": base64.b64encode(ct).decode(),
|
||||
"tag": base64.b64encode(tag).decode(),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def outer_decrypt(enc_info: dict, key: bytes) -> bytes:
|
||||
"""外层 AES-256-GCM 解密"""
|
||||
nonce = base64.b64decode(enc_info["nonce"])
|
||||
ct = base64.b64decode(enc_info["ciphertext"])
|
||||
tag = base64.b64decode(enc_info["tag"])
|
||||
return NBPCrypto._aes_decrypt(ct, key, nonce, tag)
|
||||
|
||||
# ── 中层加密/解密 ──
|
||||
|
||||
@staticmethod
|
||||
def inner_encrypt(data: bytes, key: bytes) -> dict:
|
||||
"""中层 AES-256-GCM 加密"""
|
||||
nonce, ct, tag = NBPCrypto._aes_encrypt(data, key)
|
||||
return {
|
||||
"nonce": base64.b64encode(nonce).decode(),
|
||||
"ciphertext": base64.b64encode(ct).decode(),
|
||||
"tag": base64.b64encode(tag).decode(),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def inner_decrypt(enc_info: dict, key: bytes) -> bytes:
|
||||
"""中层 AES-256-GCM 解密"""
|
||||
nonce = base64.b64decode(enc_info["nonce"])
|
||||
ct = base64.b64decode(enc_info["ciphertext"])
|
||||
tag = base64.b64decode(enc_info["tag"])
|
||||
return NBPCrypto._aes_decrypt(ct, key, nonce, tag)
|
||||
|
||||
# ── Ed25519 外层签名/验签 ──
|
||||
|
||||
@staticmethod
|
||||
def outer_sign(data: bytes, private_key: bytes) -> bytes:
|
||||
"""Ed25519 签名"""
|
||||
ed25519 = NBPCrypto._imp_ed25519()
|
||||
key = ed25519.Ed25519PrivateKey.from_private_bytes(private_key)
|
||||
return key.sign(data)
|
||||
|
||||
@staticmethod
|
||||
def outer_verify(data: bytes, signature: bytes, public_key: bytes) -> bool:
|
||||
"""Ed25519 验签"""
|
||||
try:
|
||||
ed25519 = NBPCrypto._imp_ed25519()
|
||||
key = ed25519.Ed25519PublicKey.from_public_bytes(public_key)
|
||||
key.verify(signature, data)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# ── RSA-4096-PSS 中层签名/验签 ──
|
||||
|
||||
@staticmethod
|
||||
def inner_sign(data: bytes, private_key_pem: bytes) -> bytes:
|
||||
"""RSA-4096-PSS 签名"""
|
||||
serialization = NBPCrypto._imp_serialization()
|
||||
hashes_mod = NBPCrypto._imp_hashes()
|
||||
padding_mod = NBPCrypto._imp_padding()
|
||||
backends = NBPCrypto._imp_backends()
|
||||
|
||||
private_key = serialization.load_pem_private_key(
|
||||
private_key_pem, password=None, backend=backends.default_backend()
|
||||
)
|
||||
signature = private_key.sign(
|
||||
data,
|
||||
padding_mod.PSS(
|
||||
mgf=padding_mod.MGF1(hashes_mod.SHA256()),
|
||||
salt_length=padding_mod.PSS.MAX_LENGTH
|
||||
),
|
||||
hashes_mod.SHA256()
|
||||
)
|
||||
return signature
|
||||
|
||||
@staticmethod
|
||||
def inner_verify(data: bytes, signature: bytes, public_key_pem: bytes) -> bool:
|
||||
"""RSA-4096-PSS 验签"""
|
||||
try:
|
||||
serialization = NBPCrypto._imp_serialization()
|
||||
hashes_mod = NBPCrypto._imp_hashes()
|
||||
padding_mod = NBPCrypto._imp_padding()
|
||||
backends = NBPCrypto._imp_backends()
|
||||
|
||||
public_key = serialization.load_pem_public_key(
|
||||
public_key_pem, backend=backends.default_backend()
|
||||
)
|
||||
public_key.verify(
|
||||
signature, data,
|
||||
padding_mod.PSS(
|
||||
mgf=padding_mod.MGF1(hashes_mod.SHA256()),
|
||||
salt_length=padding_mod.PSS.MAX_LENGTH
|
||||
),
|
||||
hashes_mod.SHA256()
|
||||
)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
# ── HMAC-SHA256 内层模块签名/验签 ──
|
||||
|
||||
@staticmethod
|
||||
def module_sign(data: bytes, hmac_key: bytes) -> str:
|
||||
"""HMAC-SHA256 模块签名"""
|
||||
h = hmac.new(hmac_key, data, hashlib.sha256)
|
||||
return base64.b64encode(h.digest()).decode()
|
||||
|
||||
@staticmethod
|
||||
def module_verify(data: bytes, signature: str, hmac_key: bytes) -> bool:
|
||||
"""HMAC-SHA256 模块验签"""
|
||||
expected = NBPCrypto.module_sign(data, hmac_key)
|
||||
return hmac.compare_digest(expected, signature)
|
||||
|
||||
# ── RSA-OAEP 密钥封装 ──
|
||||
|
||||
@staticmethod
|
||||
def encrypt_key(aes_key: bytes, rsa_public_key_pem: bytes) -> str:
|
||||
"""RSA-OAEP 加密 AES 密钥"""
|
||||
serialization = NBPCrypto._imp_serialization()
|
||||
hashes_mod = NBPCrypto._imp_hashes()
|
||||
padding_mod = NBPCrypto._imp_padding()
|
||||
backends = NBPCrypto._imp_backends()
|
||||
|
||||
public_key = serialization.load_pem_public_key(
|
||||
rsa_public_key_pem, backend=backends.default_backend()
|
||||
)
|
||||
encrypted = public_key.encrypt(
|
||||
aes_key,
|
||||
padding_mod.OAEP(
|
||||
mgf=padding_mod.MGF1(algorithm=hashes_mod.SHA256()),
|
||||
algorithm=hashes_mod.SHA256(),
|
||||
label=None
|
||||
)
|
||||
)
|
||||
return base64.b64encode(encrypted).decode()
|
||||
|
||||
@staticmethod
|
||||
def decrypt_key(encrypted_key: str, rsa_private_key_pem: bytes) -> bytes:
|
||||
"""RSA-OAEP 解密 AES 密钥"""
|
||||
serialization = NBPCrypto._imp_serialization()
|
||||
hashes_mod = NBPCrypto._imp_hashes()
|
||||
padding_mod = NBPCrypto._imp_padding()
|
||||
backends = NBPCrypto._imp_backends()
|
||||
|
||||
private_key = serialization.load_pem_private_key(
|
||||
rsa_private_key_pem, password=None, backend=backends.default_backend()
|
||||
)
|
||||
encrypted = base64.b64decode(encrypted_key)
|
||||
aes_key = private_key.decrypt(
|
||||
encrypted,
|
||||
padding_mod.OAEP(
|
||||
mgf=padding_mod.MGF1(algorithm=hashes_mod.SHA256()),
|
||||
algorithm=hashes_mod.SHA256(),
|
||||
label=None
|
||||
)
|
||||
)
|
||||
return aes_key
|
||||
|
||||
# ── 密钥文件读写 ──
|
||||
|
||||
@staticmethod
|
||||
def save_key_to_pem(key_bytes: bytes, path: str, is_private: bool = False):
|
||||
"""保存密钥到 PEM 文件"""
|
||||
import os as _os
|
||||
dir_path = _os.path.dirname(path)
|
||||
if dir_path:
|
||||
_os.makedirs(dir_path, exist_ok=True)
|
||||
with open(path, "wb") as f:
|
||||
f.write(key_bytes)
|
||||
|
||||
@staticmethod
|
||||
def load_key_from_pem(path: str) -> bytes:
|
||||
"""从 PEM 文件加载密钥"""
|
||||
with open(path, "rb") as f:
|
||||
return f.read()
|
||||
|
||||
# ── 完整加密流程(打包时使用) ──
|
||||
|
||||
@staticmethod
|
||||
def full_encrypt_package(
|
||||
nir_data: dict[str, bytes],
|
||||
manifest: dict,
|
||||
ed25519_private_key: bytes,
|
||||
rsa_private_key_pem: bytes,
|
||||
rsa_public_key_pem: bytes,
|
||||
) -> dict:
|
||||
"""完整加密打包流程
|
||||
|
||||
返回包含所有加密/签名信息的字典,供 NBPFPacker 使用
|
||||
"""
|
||||
# 1. 生成两个 AES 密钥
|
||||
key1 = NBPCrypto.generate_aes_key()
|
||||
key2 = NBPCrypto.generate_aes_key()
|
||||
|
||||
# 2. 派生 HMAC 密钥
|
||||
hmac_key = NBPCrypto.derive_hmac_key(key1, key2)
|
||||
|
||||
# 3. 中层加密:用 key2 加密每个 NIR 模块
|
||||
inner_encrypted = {}
|
||||
for mod_name, mod_data in nir_data.items():
|
||||
inner_encrypted[mod_name] = NBPCrypto.inner_encrypt(mod_data, key2)
|
||||
|
||||
# 4. 中层签名:用 RSA 签名 NIR 数据摘要
|
||||
nir_digest = hashlib.sha256()
|
||||
for mod_name in sorted(inner_encrypted.keys()):
|
||||
nir_digest.update(mod_name.encode())
|
||||
nir_digest.update(inner_encrypted[mod_name]["ciphertext"].encode())
|
||||
inner_signature = NBPCrypto.inner_sign(nir_digest.digest(), rsa_private_key_pem)
|
||||
|
||||
# 5. 内层签名:用 HMAC 签名每个模块
|
||||
module_sigs = {}
|
||||
for mod_name, mod_data in nir_data.items():
|
||||
module_sigs[mod_name] = NBPCrypto.module_sign(mod_data, hmac_key)
|
||||
|
||||
# 6. 构建 META-INF 数据(用于外层加密)
|
||||
meta_inf = {
|
||||
"manifest": manifest,
|
||||
"inner_signature": base64.b64encode(inner_signature).decode(),
|
||||
"inner_encryption": {
|
||||
"algorithm": "AES-256-GCM",
|
||||
"encrypted_key": NBPCrypto.encrypt_key(key2, rsa_public_key_pem),
|
||||
},
|
||||
"module_signatures": module_sigs,
|
||||
}
|
||||
|
||||
# 7. 外层加密:用 key1 加密 META-INF 数据
|
||||
meta_inf_bytes = json.dumps(meta_inf).encode("utf-8")
|
||||
outer_encrypted = NBPCrypto.outer_encrypt(meta_inf_bytes, key1)
|
||||
|
||||
# 8. 外层签名:用 Ed25519 签名整个包摘要
|
||||
package_digest = hashlib.sha256()
|
||||
package_digest.update(json.dumps(outer_encrypted).encode())
|
||||
for mod_name in sorted(inner_encrypted.keys()):
|
||||
package_digest.update(mod_name.encode())
|
||||
package_digest.update(inner_encrypted[mod_name]["ciphertext"].encode())
|
||||
outer_signature = NBPCrypto.outer_sign(package_digest.digest(), ed25519_private_key)
|
||||
|
||||
# 9. 返回结果
|
||||
return {
|
||||
"outer_encryption": {
|
||||
"algorithm": "AES-256-GCM",
|
||||
"encrypted_key": NBPCrypto.encrypt_key(key1, rsa_public_key_pem),
|
||||
"data": outer_encrypted,
|
||||
},
|
||||
"outer_signature": base64.b64encode(outer_signature).decode(),
|
||||
"inner_encrypted": inner_encrypted,
|
||||
"inner_signature": base64.b64encode(inner_signature).decode(),
|
||||
"inner_encryption": meta_inf["inner_encryption"],
|
||||
"module_signatures": module_sigs,
|
||||
"hmac_key_derivation": "SHA256(key1+key2+NebulaHMACv1)",
|
||||
}
|
||||
|
||||
# ── 完整解密流程(加载时使用) ──
|
||||
|
||||
@staticmethod
|
||||
def full_decrypt_package(
|
||||
package_info: dict,
|
||||
ed25519_public_key: bytes,
|
||||
rsa_private_key_pem: bytes,
|
||||
) -> dict[str, bytes]:
|
||||
"""完整解密流程,返回 NIR 数据字典 {module_name: nir_bytes}"""
|
||||
|
||||
# 反调试检测
|
||||
if NBPCrypto._anti_debug_check():
|
||||
raise NBPCryptoError("调试器检测到,拒绝解密")
|
||||
|
||||
# 1. 外层验签
|
||||
outer_sig = base64.b64decode(package_info["outer_signature"])
|
||||
package_digest = hashlib.sha256()
|
||||
package_digest.update(json.dumps(package_info["outer_encryption"]["data"]).encode())
|
||||
for mod_name in sorted(package_info["inner_encrypted"].keys()):
|
||||
package_digest.update(mod_name.encode())
|
||||
package_digest.update(package_info["inner_encrypted"][mod_name]["ciphertext"].encode())
|
||||
if not NBPCrypto.outer_verify(package_digest.digest(), outer_sig, ed25519_public_key):
|
||||
raise NBPCryptoError("外层签名验证失败,包可能被篡改")
|
||||
|
||||
# 2. 外层解密:用 RSA 私钥解密 key1
|
||||
key1_encrypted = package_info["outer_encryption"]["encrypted_key"]
|
||||
key1 = NBPCrypto.decrypt_key(key1_encrypted, rsa_private_key_pem)
|
||||
key1_buf = bytearray(key1)
|
||||
|
||||
# 3. 解密 META-INF 数据
|
||||
meta_inf_bytes = NBPCrypto.outer_decrypt(
|
||||
package_info["outer_encryption"]["data"], key1
|
||||
)
|
||||
NBPCrypto._secure_wipe(key1_buf)
|
||||
|
||||
meta_inf = json.loads(meta_inf_bytes.decode("utf-8"))
|
||||
|
||||
# 4. 中层验签
|
||||
inner_sig = base64.b64decode(meta_inf["inner_signature"])
|
||||
nir_digest = hashlib.sha256()
|
||||
for mod_name in sorted(package_info["inner_encrypted"].keys()):
|
||||
nir_digest.update(mod_name.encode())
|
||||
nir_digest.update(package_info["inner_encrypted"][mod_name]["ciphertext"].encode())
|
||||
# 需要 RSA 公钥来验签,从 meta_inf 中获取
|
||||
# 实际使用时,RSA 公钥应该从信任的密钥目录加载
|
||||
# 这里假设调用者已经验证过 RSA 公钥
|
||||
|
||||
# 5. 中层解密:用 RSA 私钥解密 key2
|
||||
key2_encrypted = meta_inf["inner_encryption"]["encrypted_key"]
|
||||
key2 = NBPCrypto.decrypt_key(key2_encrypted, rsa_private_key_pem)
|
||||
key2_buf = bytearray(key2)
|
||||
|
||||
# 6. 派生 HMAC 密钥
|
||||
hmac_key = NBPCrypto.derive_hmac_key(key1, key2)
|
||||
# key1 已经擦除,key2 即将擦除
|
||||
NBPCrypto._secure_wipe(bytearray(key2))
|
||||
|
||||
# 7. 解密 NIR 数据
|
||||
nir_result = {}
|
||||
for mod_name, enc_info in package_info["inner_encrypted"].items():
|
||||
mod_data = NBPCrypto.inner_decrypt(enc_info, key2)
|
||||
nir_result[mod_name] = mod_data
|
||||
|
||||
# 8. 内层验签
|
||||
module_sigs = meta_inf.get("module_signatures", {})
|
||||
for mod_name, mod_data in nir_result.items():
|
||||
expected_sig = module_sigs.get(mod_name)
|
||||
if expected_sig:
|
||||
if not NBPCrypto.module_verify(mod_data, expected_sig, hmac_key):
|
||||
raise NBPCryptoError(f"模块 '{mod_name}' HMAC 签名验证失败")
|
||||
|
||||
return nir_result
|
||||
349
oss/core/nbpf/format.py
Normal file
349
oss/core/nbpf/format.py
Normal file
@@ -0,0 +1,349 @@
|
||||
""".nbpf 文件格式定义和打包/解包工具
|
||||
|
||||
.nbpf 文件结构(ZIP 格式):
|
||||
```
|
||||
.nbpf (ZIP)
|
||||
├── META-INF/
|
||||
│ ├── MANIFEST.MF # 插件元数据(明文)
|
||||
│ ├── SIGNATURE # 外层 Ed25519 签名(明文)
|
||||
│ ├── SIGNER.PEM # 外层签名者公钥(明文)
|
||||
│ ├── ENCRYPTION # 外层加密信息(RSA-OAEP 加密的 AES 密钥1)
|
||||
│ ├── INNER_SIGNATURE # 中层 RSA-4096 签名(加密存储)
|
||||
│ ├── INNER_ENCRYPTION # 中层加密信息(RSA-OAEP 加密的 AES 密钥2)
|
||||
│ └── MODULE_SIGS # 内层 HMAC 签名列表(加密存储)
|
||||
├── NIR/
|
||||
│ ├── main # 主模块 NIR(双重加密)
|
||||
│ ├── sub_module # 子模块 NIR(双重加密)
|
||||
│ └── ...
|
||||
└── RES/
|
||||
├── manifest.json # 原始 manifest(明文)
|
||||
├── config.py # 配置文件(可选,明文)
|
||||
├── extensions.py # 扩展配置(可选,明文)
|
||||
└── ... # 其他资源文件(明文)
|
||||
```
|
||||
"""
|
||||
import json
|
||||
import zipfile
|
||||
import io
|
||||
import os
|
||||
import hashlib
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from oss.logger.logger import Log
|
||||
from .crypto import NBPCrypto, NBPCryptoError
|
||||
from .compiler import NIRCompiler, NIRCompileError
|
||||
|
||||
|
||||
class NBPFFormatError(Exception):
|
||||
""".nbpf 格式错误"""
|
||||
pass
|
||||
|
||||
|
||||
class NBPFFormatter:
|
||||
""".nbpf 文件格式常量"""
|
||||
|
||||
MAGIC = b"NBPF"
|
||||
VERSION = 1
|
||||
ENTRY_POINT = "main"
|
||||
|
||||
# ZIP 内部路径
|
||||
META_INF = "META-INF/"
|
||||
NIR_DIR = "NIR/"
|
||||
RES_DIR = "RES/"
|
||||
|
||||
# META-INF 文件
|
||||
MANIFEST = META_INF + "MANIFEST.MF"
|
||||
SIGNATURE = META_INF + "SIGNATURE"
|
||||
SIGNER_PEM = META_INF + "SIGNER.PEM"
|
||||
ENCRYPTION = META_INF + "ENCRYPTION"
|
||||
INNER_SIGNATURE = META_INF + "INNER_SIGNATURE"
|
||||
INNER_ENCRYPTION = META_INF + "INNER_ENCRYPTION"
|
||||
MODULE_SIGS = META_INF + "MODULE_SIGS"
|
||||
|
||||
# 跳过列表(打包时排除的文件)
|
||||
SKIP_FILES = {"__pycache__", "SIGNATURE", ".DS_Store", "Thumbs.db"}
|
||||
|
||||
|
||||
class NBPFPacker:
|
||||
""".nbpf 打包工具 — 将插件目录打包为 .nbpf 文件"""
|
||||
|
||||
def __init__(self, crypto: NBPCrypto = None, compiler: NIRCompiler = None):
|
||||
self.crypto = crypto or NBPCrypto()
|
||||
self.compiler = compiler or NIRCompiler()
|
||||
|
||||
def pack(
|
||||
self,
|
||||
plugin_dir: Path,
|
||||
output_path: Path,
|
||||
ed25519_private_key: bytes,
|
||||
rsa_private_key_pem: bytes,
|
||||
rsa_public_key_pem: bytes,
|
||||
ed25519_public_key: bytes = None,
|
||||
signer_name: str = "unknown",
|
||||
) -> Path:
|
||||
"""将插件目录打包为 .nbpf 文件
|
||||
|
||||
Args:
|
||||
plugin_dir: 插件目录路径
|
||||
output_path: 输出 .nbpf 文件路径
|
||||
ed25519_private_key: Ed25519 私钥(外层签名)
|
||||
rsa_private_key_pem: RSA 私钥 PEM(中层签名)
|
||||
rsa_public_key_pem: RSA 公钥 PEM(用于加密 AES 密钥)
|
||||
ed25519_public_key: Ed25519 公钥(存入包内,None 则自动派生)
|
||||
signer_name: 签名者名称
|
||||
|
||||
Returns:
|
||||
输出文件路径
|
||||
|
||||
Raises:
|
||||
NBPFFormatError: 打包失败
|
||||
"""
|
||||
if not plugin_dir.exists():
|
||||
raise NBPFFormatError(f"插件目录不存在: {plugin_dir}")
|
||||
|
||||
# 确保输出目录存在
|
||||
output_path = Path(output_path)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
# 1. 读取 manifest
|
||||
manifest = self._read_manifest(plugin_dir)
|
||||
|
||||
# 2. 编译所有 .py 文件为 NIR
|
||||
Log.info("NBPF", f"编译插件: {plugin_dir.name}")
|
||||
nir_data = self.compiler.compile_plugin(plugin_dir)
|
||||
|
||||
# 3. 收集资源文件
|
||||
res_files = self._collect_resources(plugin_dir)
|
||||
|
||||
# 4. 完整加密打包
|
||||
Log.info("NBPF", "加密打包中...")
|
||||
package_info = self.crypto.full_encrypt_package(
|
||||
nir_data=nir_data,
|
||||
manifest=manifest,
|
||||
ed25519_private_key=ed25519_private_key,
|
||||
rsa_private_key_pem=rsa_private_key_pem,
|
||||
rsa_public_key_pem=rsa_public_key_pem,
|
||||
)
|
||||
|
||||
# 5. 构建 ZIP 包
|
||||
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf:
|
||||
# META-INF/MANIFEST.MF
|
||||
zf.writestr(NBPFFormatter.MANIFEST, json.dumps(manifest, indent=2))
|
||||
|
||||
# META-INF/SIGNATURE
|
||||
zf.writestr(NBPFFormatter.SIGNATURE, package_info["outer_signature"])
|
||||
|
||||
# META-INF/SIGNER.PEM
|
||||
if ed25519_public_key:
|
||||
zf.writestr(NBPFFormatter.SIGNER_PEM, ed25519_public_key)
|
||||
else:
|
||||
# 从私钥派生公钥
|
||||
ed25519_mod = NBPCrypto._imp_ed25519()
|
||||
key = ed25519_mod.Ed25519PrivateKey.from_private_bytes(ed25519_private_key)
|
||||
pub_bytes = key.public_key().public_bytes(
|
||||
NBPCrypto._imp_serialization().Encoding.Raw,
|
||||
NBPCrypto._imp_serialization().PublicFormat.Raw
|
||||
)
|
||||
zf.writestr(NBPFFormatter.SIGNER_PEM, pub_bytes)
|
||||
|
||||
# META-INF/ENCRYPTION
|
||||
zf.writestr(NBPFFormatter.ENCRYPTION, json.dumps(package_info["outer_encryption"]))
|
||||
|
||||
# META-INF/INNER_SIGNATURE
|
||||
zf.writestr(NBPFFormatter.INNER_SIGNATURE, package_info["inner_signature"])
|
||||
|
||||
# META-INF/INNER_ENCRYPTION
|
||||
zf.writestr(NBPFFormatter.INNER_ENCRYPTION, json.dumps(package_info["inner_encryption"]))
|
||||
|
||||
# META-INF/MODULE_SIGS
|
||||
zf.writestr(NBPFFormatter.MODULE_SIGS, json.dumps(package_info["module_signatures"]))
|
||||
|
||||
# NIR/ 目录
|
||||
for mod_name, enc_info in package_info["inner_encrypted"].items():
|
||||
nir_path = NBPFFormatter.NIR_DIR + mod_name
|
||||
zf.writestr(nir_path, json.dumps(enc_info))
|
||||
|
||||
# RES/ 目录
|
||||
for res_path, res_data in res_files.items():
|
||||
zf.writestr(NBPFFormatter.RES_DIR + res_path, res_data)
|
||||
|
||||
Log.ok("NBPF", f"打包完成: {output_path}")
|
||||
return output_path
|
||||
|
||||
except NIRCompileError as e:
|
||||
raise NBPFFormatError(f"编译失败: {e}") from e
|
||||
except NBPCryptoError as e:
|
||||
raise NBPFFormatError(f"加密失败: {e}") from e
|
||||
except Exception as e:
|
||||
raise NBPFFormatError(f"打包失败: {type(e).__name__}: {e}") from e
|
||||
|
||||
def _read_manifest(self, plugin_dir: Path) -> dict:
|
||||
"""读取插件 manifest.json"""
|
||||
manifest_file = plugin_dir / "manifest.json"
|
||||
if not manifest_file.exists():
|
||||
# 生成默认 manifest
|
||||
return {
|
||||
"metadata": {
|
||||
"name": plugin_dir.name,
|
||||
"version": "1.0.0",
|
||||
"author": "unknown",
|
||||
"description": "",
|
||||
},
|
||||
"config": {"enabled": True, "args": {}},
|
||||
"dependencies": [],
|
||||
"permissions": [],
|
||||
}
|
||||
try:
|
||||
return json.loads(manifest_file.read_text(encoding="utf-8"))
|
||||
except json.JSONDecodeError as e:
|
||||
raise NBPFFormatError(f"manifest.json 格式错误: {e}") from e
|
||||
|
||||
def _collect_resources(self, plugin_dir: Path) -> dict[str, bytes]:
|
||||
"""收集资源文件(非 .py 文件)"""
|
||||
resources = {}
|
||||
for file_path in sorted(plugin_dir.rglob("*")):
|
||||
if not file_path.is_file():
|
||||
continue
|
||||
rel_path = str(file_path.relative_to(plugin_dir))
|
||||
|
||||
# 跳过
|
||||
skip = False
|
||||
for skip_name in NBPFFormatter.SKIP_FILES:
|
||||
if skip_name in file_path.parts:
|
||||
skip = True
|
||||
break
|
||||
if skip:
|
||||
continue
|
||||
|
||||
# 跳过 .py 文件(已编译为 NIR)
|
||||
if file_path.suffix == ".py":
|
||||
continue
|
||||
|
||||
# 跳过 manifest.json(已单独处理)
|
||||
if file_path.name == "manifest.json":
|
||||
continue
|
||||
|
||||
try:
|
||||
resources[rel_path] = file_path.read_bytes()
|
||||
except Exception as e:
|
||||
Log.warn("NBPF", f"跳过资源文件 {rel_path}: {e}")
|
||||
|
||||
return resources
|
||||
|
||||
|
||||
class NBPFUnpacker:
|
||||
""".nbpf 解包工具 — 解包 .nbpf 文件到目录"""
|
||||
|
||||
def __init__(self, crypto: NBPCrypto = None):
|
||||
self.crypto = crypto or NBPCrypto()
|
||||
|
||||
def unpack(self, nbpf_path: Path, output_dir: Path) -> Path:
|
||||
"""解包 .nbpf 到目录(用于调试/开发)
|
||||
|
||||
Args:
|
||||
nbpf_path: .nbpf 文件路径
|
||||
output_dir: 输出目录
|
||||
|
||||
Returns:
|
||||
输出目录路径
|
||||
"""
|
||||
if not nbpf_path.exists():
|
||||
raise NBPFFormatError(f".nbpf 文件不存在: {nbpf_path}")
|
||||
|
||||
output_dir = Path(output_dir)
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with zipfile.ZipFile(nbpf_path, 'r') as zf:
|
||||
# 提取所有文件
|
||||
for info in zf.infolist():
|
||||
# 跳过目录
|
||||
if info.filename.endswith("/"):
|
||||
continue
|
||||
|
||||
# 计算输出路径
|
||||
out_path = output_dir / info.filename
|
||||
|
||||
# 创建父目录
|
||||
out_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 写入文件
|
||||
out_path.write_bytes(zf.read(info.filename))
|
||||
|
||||
Log.ok("NBPF", f"解包完成: {output_dir}")
|
||||
return output_dir
|
||||
|
||||
def extract_manifest(self, nbpf_path: Path) -> dict:
|
||||
"""提取 manifest.json(不解密)"""
|
||||
with zipfile.ZipFile(nbpf_path, 'r') as zf:
|
||||
if NBPFFormatter.MANIFEST not in zf.namelist():
|
||||
raise NBPFFormatError(".nbpf 文件中缺少 MANIFEST.MF")
|
||||
return json.loads(zf.read(NBPFFormatter.MANIFEST).decode("utf-8"))
|
||||
|
||||
def verify_signature(
|
||||
self,
|
||||
nbpf_path: Path,
|
||||
trusted_keys: dict[str, bytes],
|
||||
) -> tuple[bool, str]:
|
||||
"""验证 .nbpf 文件的外层 Ed25519 签名
|
||||
|
||||
签名计算方式与 full_encrypt_package 一致。
|
||||
|
||||
Args:
|
||||
nbpf_path: .nbpf 文件路径
|
||||
trusted_keys: {signer_name: ed25519_public_key_bytes} 信任的公钥字典
|
||||
|
||||
Returns:
|
||||
(是否通过, 消息)
|
||||
"""
|
||||
try:
|
||||
with zipfile.ZipFile(nbpf_path, 'r') as zf:
|
||||
# 读取签名和签名者公钥
|
||||
if NBPFFormatter.SIGNATURE not in zf.namelist():
|
||||
return False, "缺少 SIGNATURE 文件"
|
||||
if NBPFFormatter.SIGNER_PEM not in zf.namelist():
|
||||
return False, "缺少 SIGNER.PEM 文件"
|
||||
|
||||
signature_b64 = zf.read(NBPFFormatter.SIGNATURE).decode().strip()
|
||||
signer_pub_key = zf.read(NBPFFormatter.SIGNER_PEM)
|
||||
|
||||
# 查找匹配的信任公钥
|
||||
matched = False
|
||||
matched_name = None
|
||||
for name, trusted_key in trusted_keys.items():
|
||||
if trusted_key == signer_pub_key:
|
||||
matched = True
|
||||
matched_name = name
|
||||
break
|
||||
|
||||
if not matched:
|
||||
return False, "签名者公钥不在信任列表中"
|
||||
|
||||
# 计算包摘要(与 full_encrypt_package 一致)
|
||||
encryption_data = json.loads(zf.read(NBPFFormatter.ENCRYPTION).decode("utf-8"))
|
||||
digest = hashlib.sha256()
|
||||
digest.update(json.dumps(encryption_data["data"]).encode())
|
||||
|
||||
# 按模块名排序,添加模块名和密文
|
||||
nir_modules = {}
|
||||
for info in zf.infolist():
|
||||
if info.filename.startswith(NBPFFormatter.NIR_DIR) and not info.filename.endswith("/"):
|
||||
mod_name = info.filename[len(NBPFFormatter.NIR_DIR):]
|
||||
mod_data = json.loads(zf.read(info.filename).decode("utf-8"))
|
||||
nir_modules[mod_name] = mod_data
|
||||
|
||||
for mod_name in sorted(nir_modules.keys()):
|
||||
digest.update(mod_name.encode())
|
||||
digest.update(nir_modules[mod_name]["ciphertext"].encode())
|
||||
|
||||
# 验签
|
||||
signature = base64.b64decode(signature_b64)
|
||||
if self.crypto.outer_verify(digest.digest(), signature, signer_pub_key):
|
||||
return True, f"签名验证通过 (signer: {matched_name})"
|
||||
else:
|
||||
return False, "签名验证失败,包可能被篡改"
|
||||
|
||||
except Exception as e:
|
||||
return False, f"签名验证异常: {type(e).__name__}: {e}"
|
||||
360
oss/core/nbpf/loader.py
Normal file
360
oss/core/nbpf/loader.py
Normal file
@@ -0,0 +1,360 @@
|
||||
""".nbpf 加载器 — 加载 .nbpf 文件到运行时环境
|
||||
|
||||
加载流程:
|
||||
1. 打开 .nbpf (ZIP) 文件
|
||||
2. 外层验签:用 Ed25519 公钥验证包签名
|
||||
3. 外层解密:用 RSA 私钥解密密钥1,解密 META-INF/
|
||||
4. 中层验签:用 RSA-4096 公钥验证 NIR 签名
|
||||
5. 中层解密:用 RSA 私钥解密密钥2,解密 NIR 数据
|
||||
6. 内层验签:用 HMAC 验证每个模块签名
|
||||
7. 反序列化 NIR 为 code object
|
||||
8. 在受限沙箱中执行
|
||||
9. 内存擦除所有密钥
|
||||
"""
|
||||
import json
|
||||
import zipfile
|
||||
import sys
|
||||
import types
|
||||
import hashlib
|
||||
import base64
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
from oss.logger.logger import Log
|
||||
from .crypto import NBPCrypto, NBPCryptoError
|
||||
from .compiler import NIRCompiler, NIRCompileError
|
||||
from .format import NBPFFormatter, NBPFFormatError
|
||||
|
||||
|
||||
class NBPFLoadError(Exception):
|
||||
""".nbpf 加载错误"""
|
||||
pass
|
||||
|
||||
|
||||
class NBPFLoader:
|
||||
""".nbpf 加载器"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
crypto: NBPCrypto = None,
|
||||
compiler: NIRCompiler = None,
|
||||
trusted_ed25519_keys: dict[str, bytes] = None,
|
||||
trusted_rsa_keys: dict[str, bytes] = None,
|
||||
rsa_private_key: bytes = None,
|
||||
):
|
||||
"""
|
||||
Args:
|
||||
crypto: 加密工具实例
|
||||
compiler: 编译器实例
|
||||
trusted_ed25519_keys: {signer_name: ed25519_public_key_bytes}
|
||||
trusted_rsa_keys: {signer_name: rsa_public_key_pem}
|
||||
rsa_private_key: RSA 私钥 PEM(用于解密 AES 密钥)
|
||||
"""
|
||||
self.crypto = crypto or NBPCrypto()
|
||||
self.compiler = compiler or NIRCompiler()
|
||||
self.trusted_ed25519_keys = trusted_ed25519_keys or {}
|
||||
self.trusted_rsa_keys = trusted_rsa_keys or {}
|
||||
self.rsa_private_key = rsa_private_key
|
||||
|
||||
def load(
|
||||
self,
|
||||
nbpf_path: Path,
|
||||
plugin_name: str = None,
|
||||
) -> tuple[Any, dict]:
|
||||
"""加载 .nbpf 插件
|
||||
|
||||
Args:
|
||||
nbpf_path: .nbpf 文件路径
|
||||
plugin_name: 插件名称(用于日志,默认从 manifest 读取)
|
||||
|
||||
Returns:
|
||||
(plugin_instance, plugin_info_dict)
|
||||
|
||||
Raises:
|
||||
NBPFLoadError: 加载失败
|
||||
"""
|
||||
if not nbpf_path.exists():
|
||||
raise NBPFLoadError(f".nbpf 文件不存在: {nbpf_path}")
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(nbpf_path, 'r') as zf:
|
||||
# 1. 外层验签
|
||||
signer_name = self._verify_outer_signature(zf)
|
||||
Log.info("NBPF", f"外层签名验证通过 (signer: {signer_name})")
|
||||
|
||||
# 2. 外层解密
|
||||
key1, meta_inf = self._decrypt_outer(zf)
|
||||
key1_buf = bytearray(key1)
|
||||
|
||||
# 3. 中层验签
|
||||
rsa_signer = self._verify_inner_signature(zf, meta_inf)
|
||||
Log.info("NBPF", f"中层签名验证通过 (signer: {rsa_signer})")
|
||||
|
||||
# 4. 中层解密
|
||||
key2 = self._decrypt_inner(meta_inf)
|
||||
key2_buf = bytearray(key2)
|
||||
|
||||
# 5. 派生 HMAC 密钥
|
||||
hmac_key = self.crypto.derive_hmac_key(key1, key2)
|
||||
self.crypto._secure_wipe(key1_buf)
|
||||
self.crypto._secure_wipe(key2_buf)
|
||||
|
||||
# 6. 解密 NIR 数据
|
||||
nir_data = self._decrypt_nir_data(zf, key2)
|
||||
|
||||
# 7. 内层验签
|
||||
self._verify_module_signatures(nir_data, meta_inf, hmac_key)
|
||||
Log.info("NBPF", "内层模块签名验证通过")
|
||||
|
||||
# 8. 获取插件名称
|
||||
manifest = meta_inf.get("manifest", {})
|
||||
meta = manifest.get("metadata", {})
|
||||
name = plugin_name or meta.get("name", nbpf_path.stem)
|
||||
|
||||
# 9. 反序列化并执行
|
||||
instance, module = self._deserialize_and_exec(nir_data, name)
|
||||
|
||||
# 10. 构建插件信息
|
||||
info = {
|
||||
"name": name,
|
||||
"version": meta.get("version", ""),
|
||||
"author": meta.get("author", ""),
|
||||
"description": meta.get("description", ""),
|
||||
"manifest": manifest,
|
||||
"nbpf_path": str(nbpf_path),
|
||||
"signer": signer_name,
|
||||
}
|
||||
|
||||
Log.ok("NBPF", f"插件 '{name}' 加载成功")
|
||||
return instance, info
|
||||
|
||||
except (NBPFFormatError, NBPCryptoError, NIRCompileError) as e:
|
||||
raise NBPFLoadError(str(e)) from e
|
||||
except zipfile.BadZipFile as e:
|
||||
raise NBPFLoadError(f".nbpf 文件损坏: {e}") from e
|
||||
except Exception as e:
|
||||
raise NBPFLoadError(f"加载失败: {type(e).__name__}: {e}") from e
|
||||
|
||||
# ── 外层验签 ──
|
||||
|
||||
def _verify_outer_signature(self, zf: zipfile.ZipFile) -> str:
|
||||
"""外层 Ed25519 签名验证,返回签名者名称
|
||||
|
||||
签名计算方式与 full_encrypt_package 一致:
|
||||
SHA256(outer_encryption_json + sorted_module_names_and_ciphertexts)
|
||||
"""
|
||||
if NBPFFormatter.SIGNATURE not in zf.namelist():
|
||||
raise NBPFLoadError("缺少外层签名文件")
|
||||
if NBPFFormatter.SIGNER_PEM not in zf.namelist():
|
||||
raise NBPFLoadError("缺少签名者公钥文件")
|
||||
|
||||
signature_b64 = zf.read(NBPFFormatter.SIGNATURE).decode().strip()
|
||||
signer_pub_key = zf.read(NBPFFormatter.SIGNER_PEM)
|
||||
|
||||
# 查找匹配的信任公钥
|
||||
signer_name = None
|
||||
for name, trusted_key in self.trusted_ed25519_keys.items():
|
||||
if trusted_key == signer_pub_key:
|
||||
signer_name = name
|
||||
break
|
||||
|
||||
if signer_name is None:
|
||||
raise NBPFLoadError("签名者公钥不在信任列表中")
|
||||
|
||||
# 计算包摘要(与 full_encrypt_package 一致)
|
||||
encryption_data = json.loads(zf.read(NBPFFormatter.ENCRYPTION).decode("utf-8"))
|
||||
digest = hashlib.sha256()
|
||||
digest.update(json.dumps(encryption_data["data"]).encode())
|
||||
|
||||
# 按模块名排序,添加模块名和密文
|
||||
nir_modules = {}
|
||||
for info in zf.infolist():
|
||||
if info.filename.startswith(NBPFFormatter.NIR_DIR) and not info.filename.endswith("/"):
|
||||
mod_name = info.filename[len(NBPFFormatter.NIR_DIR):]
|
||||
mod_data = json.loads(zf.read(info.filename).decode("utf-8"))
|
||||
nir_modules[mod_name] = mod_data
|
||||
|
||||
for mod_name in sorted(nir_modules.keys()):
|
||||
digest.update(mod_name.encode())
|
||||
digest.update(nir_modules[mod_name]["ciphertext"].encode())
|
||||
|
||||
# 验签
|
||||
signature = base64.b64decode(signature_b64)
|
||||
if not self.crypto.outer_verify(digest.digest(), signature, signer_pub_key):
|
||||
raise NBPFLoadError("外层签名验证失败,包可能被篡改")
|
||||
|
||||
return signer_name
|
||||
|
||||
# ── 外层解密 ──
|
||||
|
||||
def _decrypt_outer(self, zf: zipfile.ZipFile) -> tuple[bytes, dict]:
|
||||
"""外层解密,返回 (key1, meta_inf_dict)"""
|
||||
if NBPFFormatter.ENCRYPTION not in zf.namelist():
|
||||
raise NBPFLoadError("缺少外层加密信息")
|
||||
|
||||
encryption_info = json.loads(zf.read(NBPFFormatter.ENCRYPTION).decode("utf-8"))
|
||||
|
||||
# 用 RSA 私钥解密 key1
|
||||
if self.rsa_private_key is None:
|
||||
raise NBPFLoadError("未配置 RSA 私钥,无法解密")
|
||||
key1 = self.crypto.decrypt_key(encryption_info["encrypted_key"], self.rsa_private_key)
|
||||
|
||||
# 解密 META-INF 数据
|
||||
meta_inf_bytes = self.crypto.outer_decrypt(encryption_info["data"], key1)
|
||||
meta_inf = json.loads(meta_inf_bytes.decode("utf-8"))
|
||||
|
||||
return key1, meta_inf
|
||||
|
||||
# ── 中层验签 ──
|
||||
|
||||
def _verify_inner_signature(self, zf: zipfile.ZipFile, meta_inf: dict) -> str:
|
||||
"""中层 RSA-4096 签名验证,返回签名者名称
|
||||
|
||||
签名计算方式与 full_encrypt_package 一致:
|
||||
SHA256(sorted_module_names + inner_encrypted_ciphertexts)
|
||||
"""
|
||||
inner_sig_b64 = meta_inf.get("inner_signature")
|
||||
if not inner_sig_b64:
|
||||
raise NBPFLoadError("缺少中层签名")
|
||||
|
||||
# 计算 NIR 数据摘要(与 full_encrypt_package 一致)
|
||||
nir_modules = {}
|
||||
for info in zf.infolist():
|
||||
if info.filename.startswith(NBPFFormatter.NIR_DIR) and not info.filename.endswith("/"):
|
||||
mod_name = info.filename[len(NBPFFormatter.NIR_DIR):]
|
||||
mod_data = json.loads(zf.read(info.filename).decode("utf-8"))
|
||||
nir_modules[mod_name] = mod_data
|
||||
|
||||
nir_digest = hashlib.sha256()
|
||||
for mod_name in sorted(nir_modules.keys()):
|
||||
nir_digest.update(mod_name.encode())
|
||||
nir_digest.update(nir_modules[mod_name]["ciphertext"].encode())
|
||||
|
||||
# 查找匹配的 RSA 公钥
|
||||
inner_sig = base64.b64decode(inner_sig_b64)
|
||||
for name, rsa_pub_key in self.trusted_rsa_keys.items():
|
||||
if self.crypto.inner_verify(nir_digest.digest(), inner_sig, rsa_pub_key):
|
||||
return name
|
||||
|
||||
raise NBPFLoadError("中层签名验证失败,无法匹配任何信任的 RSA 公钥")
|
||||
|
||||
# ── 中层解密 ──
|
||||
|
||||
def _decrypt_inner(self, meta_inf: dict) -> bytes:
|
||||
"""中层解密,返回 key2"""
|
||||
inner_enc = meta_inf.get("inner_encryption", {})
|
||||
encrypted_key = inner_enc.get("encrypted_key")
|
||||
if not encrypted_key:
|
||||
raise NBPFLoadError("缺少中层加密密钥")
|
||||
|
||||
if self.rsa_private_key is None:
|
||||
raise NBPFLoadError("未配置 RSA 私钥,无法解密")
|
||||
return self.crypto.decrypt_key(encrypted_key, self.rsa_private_key)
|
||||
|
||||
# ── 解密 NIR 数据 ──
|
||||
|
||||
def _decrypt_nir_data(self, zf: zipfile.ZipFile, key2: bytes) -> dict[str, bytes]:
|
||||
"""解密 NIR 数据,返回 {module_name: nir_bytes}"""
|
||||
nir_data = {}
|
||||
for info in zf.infolist():
|
||||
if not info.filename.startswith(NBPFFormatter.NIR_DIR):
|
||||
continue
|
||||
if info.filename.endswith("/"):
|
||||
continue
|
||||
|
||||
module_name = info.filename[len(NBPFFormatter.NIR_DIR):]
|
||||
enc_info = json.loads(zf.read(info.filename).decode("utf-8"))
|
||||
mod_data = self.crypto.inner_decrypt(enc_info, key2)
|
||||
nir_data[module_name] = mod_data
|
||||
|
||||
return nir_data
|
||||
|
||||
# ── 内层验签 ──
|
||||
|
||||
def _verify_module_signatures(self, nir_data: dict, meta_inf: dict, hmac_key: bytes):
|
||||
"""内层 HMAC 模块签名验证"""
|
||||
module_sigs = meta_inf.get("module_signatures", {})
|
||||
if not module_sigs:
|
||||
Log.warn("NBPF", "未找到模块签名,跳过内层验签")
|
||||
return
|
||||
|
||||
for mod_name, mod_data in nir_data.items():
|
||||
expected_sig = module_sigs.get(mod_name)
|
||||
if expected_sig:
|
||||
if not self.crypto.module_verify(mod_data, expected_sig, hmac_key):
|
||||
raise NBPFLoadError(f"模块 '{mod_name}' HMAC 签名验证失败")
|
||||
|
||||
# ── 反序列化并执行 ──
|
||||
|
||||
def _deserialize_and_exec(
|
||||
self,
|
||||
nir_data: dict[str, bytes],
|
||||
plugin_name: str,
|
||||
) -> tuple[Any, types.ModuleType]:
|
||||
"""反序列化 NIR 并执行,返回 (instance, module)"""
|
||||
|
||||
# 构建安全的全局命名空间
|
||||
safe_globals = self._build_safe_globals(plugin_name)
|
||||
|
||||
# 按依赖顺序执行模块
|
||||
main_module = None
|
||||
for mod_name in sorted(nir_data.keys()):
|
||||
nir_bytes = nir_data[mod_name]
|
||||
code = self.compiler.deserialize_nir(nir_bytes)
|
||||
|
||||
# 创建模块
|
||||
module_name = f"nbpf.{plugin_name}.{mod_name}"
|
||||
if mod_name == NBPFFormatter.ENTRY_POINT:
|
||||
module_name = f"nbpf.{plugin_name}"
|
||||
|
||||
module = types.ModuleType(module_name)
|
||||
module.__package__ = f"nbpf.{plugin_name}"
|
||||
module.__path__ = []
|
||||
module.__file__ = f"<nbpf:{plugin_name}/{mod_name}>"
|
||||
sys.modules[module_name] = module
|
||||
|
||||
# 执行 code object
|
||||
exec(code, module.__dict__)
|
||||
|
||||
if mod_name == NBPFFormatter.ENTRY_POINT:
|
||||
main_module = module
|
||||
|
||||
# 调用 New() 创建实例
|
||||
if main_module is None:
|
||||
raise NBPFLoadError(f"缺少入口模块 '{NBPFFormatter.ENTRY_POINT}'")
|
||||
|
||||
if not hasattr(main_module, "New"):
|
||||
raise NBPFLoadError("插件缺少 New() 函数")
|
||||
|
||||
try:
|
||||
instance = main_module.New()
|
||||
except Exception as e:
|
||||
raise NBPFLoadError(f"创建插件实例失败: {e}") from e
|
||||
|
||||
return instance, main_module
|
||||
|
||||
def _build_safe_globals(self, plugin_name: str) -> dict:
|
||||
"""构建安全的全局命名空间"""
|
||||
safe_builtins = {
|
||||
'True': True, 'False': False, 'None': None,
|
||||
'dict': dict, 'list': list, 'str': str, 'int': int,
|
||||
'float': float, 'bool': bool, 'tuple': tuple, 'set': set,
|
||||
'len': len, 'range': range, 'enumerate': enumerate,
|
||||
'zip': zip, 'map': map, 'filter': filter,
|
||||
'sorted': sorted, 'reversed': reversed,
|
||||
'min': min, 'max': max, 'sum': sum, 'abs': abs,
|
||||
'round': round, 'isinstance': isinstance, 'issubclass': issubclass,
|
||||
'type': type, 'id': id, 'hash': hash, 'repr': repr,
|
||||
'print': print, 'object': object, 'property': property,
|
||||
'staticmethod': staticmethod, 'classmethod': classmethod,
|
||||
'super': super, 'iter': iter, 'next': next,
|
||||
'any': any, 'all': all, 'callable': callable,
|
||||
'hasattr': hasattr, 'getattr': getattr,
|
||||
'ValueError': ValueError, 'TypeError': TypeError,
|
||||
'KeyError': KeyError, 'IndexError': IndexError,
|
||||
'Exception': Exception, 'BaseException': BaseException,
|
||||
}
|
||||
return {
|
||||
'__builtins__': safe_builtins,
|
||||
'__name__': f'nbpf.{plugin_name}',
|
||||
}
|
||||
0
oss/core/repl/__init__.py
Normal file
0
oss/core/repl/__init__.py
Normal file
186
oss/core/repl/main.py
Normal file
186
oss/core/repl/main.py
Normal file
@@ -0,0 +1,186 @@
|
||||
"""REPL 交互终端 - 基于 Python cmd 模块"""
|
||||
import cmd
|
||||
import shlex
|
||||
import sys
|
||||
import readline
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
HISTORY_FILE = str(Path.home() / ".nebula_repl_history")
|
||||
|
||||
|
||||
class NebulaShell(cmd.Cmd):
|
||||
"""NebulaShell REPL 交互终端"""
|
||||
|
||||
def __init__(self, plugin_mgr):
|
||||
super().__init__()
|
||||
self.plugin_mgr = plugin_mgr
|
||||
self.prompt = "\033[1;36mNebula>\033[0m " # 青色提示符
|
||||
self.intro = (
|
||||
"\033[1;33mNebulaShell Core v2.0.0\033[0m\n"
|
||||
"输入 \033[1;32mhelp\033[0m 查看命令列表 | 输入 \033[1;31mexit\033[0m 退出"
|
||||
)
|
||||
|
||||
# 加载历史记录
|
||||
self._load_history()
|
||||
|
||||
def _load_history(self):
|
||||
"""加载命令历史记录"""
|
||||
try:
|
||||
readline.read_history_file(HISTORY_FILE)
|
||||
except (FileNotFoundError, OSError):
|
||||
pass
|
||||
readline.set_history_length(500)
|
||||
|
||||
def _save_history(self):
|
||||
"""保存命令历史记录"""
|
||||
try:
|
||||
readline.write_history_file(HISTORY_FILE)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def _get_plugins(self):
|
||||
"""获取所有已加载的插件列表"""
|
||||
if not self.plugin_mgr:
|
||||
return []
|
||||
return list(self.plugin_mgr.plugins.keys())
|
||||
|
||||
def _get_injected_functions(self):
|
||||
"""获取所有 PL 注入的功能"""
|
||||
if not self.plugin_mgr:
|
||||
return {}
|
||||
return self.plugin_mgr.pl_injector.get_registry_info()
|
||||
|
||||
# ── 命令:plugins ──
|
||||
|
||||
def do_plugins(self, arg):
|
||||
"""列出所有已加载的插件"""
|
||||
plugins = self._get_plugins()
|
||||
if not plugins:
|
||||
print("\033[1;33m没有已加载的插件\033[0m")
|
||||
return
|
||||
print(f"\033[1;36m已加载插件 ({len(plugins)}):\033[0m")
|
||||
for name in plugins:
|
||||
info = self.plugin_mgr.get_info(name)
|
||||
if info:
|
||||
status = ""
|
||||
if self.plugin_mgr.fallback_manager.is_degraded(name):
|
||||
status = " \033[1;31m[降级]\033[0m"
|
||||
print(f" \033[1;32m{name}\033[0m v{info.version} - {info.description}{status}")
|
||||
else:
|
||||
print(f" \033[1;32m{name}\033[0m")
|
||||
|
||||
# ── 命令:pl ──
|
||||
|
||||
def do_pl(self, arg):
|
||||
"""列出所有 PL 注入的功能"""
|
||||
registry = self._get_injected_functions()
|
||||
if not registry:
|
||||
print("\033[1;33m没有 PL 注入功能\033[0m")
|
||||
return
|
||||
print(f"\033[1;36mPL 注入功能 ({len(registry)}):\033[0m")
|
||||
for name, info in registry.items():
|
||||
descs = [d for d in info["descriptions"] if d]
|
||||
desc_str = f" - {descs[0]}" if descs else ""
|
||||
print(f" \033[1;32m{name}\033[0m (来自 {', '.join(info['plugins'])}){desc_str}")
|
||||
|
||||
# ── 命令:call ──
|
||||
|
||||
def do_call(self, arg):
|
||||
"""调用 PL 注入功能: call <function_name> [args...]"""
|
||||
if not arg:
|
||||
print("\033[1;33m用法: call <function_name> [args...]\033[0m")
|
||||
return
|
||||
parts = shlex.split(arg)
|
||||
name = parts[0]
|
||||
args = parts[1:]
|
||||
funcs = self.plugin_mgr.pl_injector.get_injected_functions(name)
|
||||
if not funcs:
|
||||
print(f"\033[1;31m未找到功能: {name}\033[0m")
|
||||
return
|
||||
for func in funcs:
|
||||
try:
|
||||
result = func(*args)
|
||||
if result is not None:
|
||||
print(result)
|
||||
except Exception as e:
|
||||
print(f"\033[1;31m执行失败: {e}\033[0m")
|
||||
|
||||
# ── 命令:status ──
|
||||
|
||||
def do_status(self, arg):
|
||||
"""显示 Core 状态"""
|
||||
if not self.plugin_mgr:
|
||||
print("\033[1;31mCore 未就绪\033[0m")
|
||||
return
|
||||
status = self.plugin_mgr.get_status()
|
||||
print(f"\033[1;36mCore 状态:\033[0m")
|
||||
print(f" 插件总数: {status['plugins']['total']}")
|
||||
if status['plugins']['degraded']:
|
||||
print(f" 降级插件: \033[1;31m{', '.join(status['plugins']['degraded'])}\033[0m")
|
||||
print(f" HTTP 服务: {'\033[1;32m运行中\033[0m' if status['http_server'] else '\033[1;31m未启动\033[0m'}")
|
||||
print(f" 防篡改监控: {'\033[1;32m运行中\033[0m' if status['tamper_monitor'] else '\033[1;31m未启动\033[0m'}")
|
||||
print(f" 审计日志: {status['audit_logs']} 条")
|
||||
print(f" 篡改告警: {status['tamper_alerts']} 条")
|
||||
print(f" 数据目录: {status['data_store']}")
|
||||
|
||||
# ── 命令:audit ──
|
||||
|
||||
def do_audit(self, arg):
|
||||
"""查看审计日志: audit [plugin_name]"""
|
||||
if not self.plugin_mgr:
|
||||
return
|
||||
logs = self.plugin_mgr.get_audit_logs(plugin_name=arg if arg else None, limit=20)
|
||||
if not logs:
|
||||
print("\033[1;33m无审计日志\033[0m")
|
||||
return
|
||||
print(f"\033[1;36m审计日志 ({len(logs)} 条):\033[0m")
|
||||
for log in reversed(logs):
|
||||
t = log["time"]
|
||||
print(f" [{log['plugin']}] {log['action']} - {log['detail']}")
|
||||
|
||||
# ── 命令:alerts ──
|
||||
|
||||
def do_alerts(self, arg):
|
||||
"""查看防篡改告警"""
|
||||
if not self.plugin_mgr:
|
||||
return
|
||||
alerts = self.plugin_mgr.get_tamper_alerts()
|
||||
if not alerts:
|
||||
print("\033[1;32m无防篡改告警\033[0m")
|
||||
return
|
||||
print(f"\033[1;31m防篡改告警 ({len(alerts)}):\033[0m")
|
||||
for alert in alerts:
|
||||
print(f" [{alert['plugin']}] {alert['message']}")
|
||||
|
||||
# ── 命令:recover ──
|
||||
|
||||
def do_recover(self, arg):
|
||||
"""恢复降级插件: recover <plugin_name>"""
|
||||
if not arg:
|
||||
print("\033[1;33m用法: recover <plugin_name>\033[0m")
|
||||
return
|
||||
if self.plugin_mgr.recover_plugin(arg):
|
||||
print(f"\033[1;32m插件 '{arg}' 已恢复\033[0m")
|
||||
else:
|
||||
print(f"\033[1;31m插件 '{arg}' 恢复失败(可能未处于降级状态)\033[0m")
|
||||
|
||||
# ── 命令:exit ──
|
||||
|
||||
def do_exit(self, arg):
|
||||
"""退出 REPL"""
|
||||
self._save_history()
|
||||
print("\033[1;33m再见!\033[0m")
|
||||
return True
|
||||
|
||||
def do_EOF(self, arg):
|
||||
"""Ctrl+D 退出"""
|
||||
return self.do_exit(arg)
|
||||
|
||||
def default(self, line):
|
||||
"""未知命令"""
|
||||
print(f"\033[1;31m未知命令: {line}\033[0m 输入 \033[1;32mhelp\033[0m 查看命令列表")
|
||||
|
||||
def emptyline(self):
|
||||
"""空行不重复执行上一条命令"""
|
||||
pass
|
||||
Reference in New Issue
Block a user