重构:核心迁移至 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:
Falck
2026-05-05 07:29:43 +08:00
parent 4441a968db
commit 3a096f59a9
184 changed files with 5715 additions and 10066 deletions

View File

@@ -3,9 +3,11 @@ import click
import signal
import os
import sys
import random
from pathlib import Path
from oss import __version__
from oss.logger.logger import Logger
from oss.logger.logger import Log
from oss.plugin.manager import PluginManager
from oss.config import init_config, get_config
@@ -18,21 +20,52 @@ except ImportError:
_ACHIEVEMENTS_ENABLED = False
def _handle_hidden_command():
"""处理 !! 前缀的隐藏命令"""
if len(sys.argv) <= 1 or not sys.argv[1].startswith("!!"):
return False
if not _ACHIEVEMENTS_ENABLED:
print("成就系统未启用")
return True
cmd = sys.argv[1][2:]
args = sys.argv[2:]
cmd_map = {
"echo": _cmd_echo,
"help": _cmd_help_internal,
"list": _cmd_list_all,
"stats": _cmd_stats,
"reset": _cmd_reset_progress,
"export": _cmd_export,
"import": _cmd_import,
"verify": _cmd_verify,
"debug": _cmd_debug,
"info": _cmd_info,
}
if cmd in cmd_map:
validator = get_validator()
validator.use_hidden_command(cmd)
cmd_map[cmd](args)
else:
print(f"未知命令:!!{cmd}")
return True
@click.group()
@click.option('--config', '-c', type=str, help='配置文件路径')
@click.pass_context
def cli(ctx, config):
"""NebulaShell - 一切皆为插件"""
# 初始化配置
ctx.ensure_object(dict)
ctx.obj['config'] = init_config(config)
# 初始化成就系统(如果启用)
if _ACHIEVEMENTS_ENABLED:
try:
init_achievements()
except Exception:
pass # 静默失败,不影响主功能
pass
@cli.command()
@@ -43,42 +76,44 @@ def cli(ctx, config):
def serve(ctx, host, port, tcp_port):
"""启动 NebulaShell 服务端"""
config = ctx.obj.get('config', get_config())
# 命令行参数覆盖配置
if host:
config.set('HOST', host)
if port:
config.set('HTTP_API_PORT', port)
if tcp_port:
config.set('HTTP_TCP_PORT', tcp_port)
log = Logger()
log.info(f"NebulaShell {__version__} 启动")
log.info(f"监听地址{config.host}:{config.http_api_port}")
log.info(f"数据目录{config.data_dir.absolute()}")
log.info(f"插件仓库:{config.store_dir.absolute()}")
Log.info("NebulaShell", f"NebulaShell {__version__} 启动")
Log.info("NebulaShell", f"监听地址:{config.host}:{config.http_api_port}")
Log.info("NebulaShell", f"数据目录{config.data_dir.absolute()}")
Log.info("NebulaShell", f"插件仓库{config.store_dir.absolute()}")
plugin_mgr = PluginManager()
plugin_mgr.load()
plugin_mgr.start()
log.info("就绪")
Log.info("NebulaShell", "就绪")
def shutdown(sig, frame):
log.info("停止中...")
Log.info("NebulaShell", "停止中...")
plugin_mgr.stop()
log.info("已停止")
Log.info("NebulaShell", "已停止")
raise SystemExit(0)
signal.signal(signal.SIGINT, shutdown)
signal.signal(signal.SIGTERM, shutdown)
# 启动 REPL 交互(由 Core 内部提供)
try:
if hasattr(plugin_mgr, 'core') and plugin_mgr.core:
plugin_mgr.core.start_repl()
else:
Log.error("NebulaShell", "Core 未加载,无法启动 REPL")
signal.pause()
except Exception as e:
Log.error("NebulaShell", f"REPL 启动失败: {e}")
signal.pause()
except AttributeError:
import time
while True:
time.sleep(1)
@cli.command()
@@ -91,8 +126,6 @@ def version():
@click.pass_context
def info(ctx):
"""显示系统信息"""
import random
config = ctx.obj.get('config', get_config())
click.echo(f"NebulaShell {__version__}")
click.echo(f"配置文件:{config._config_file or ''}")
@@ -103,13 +136,12 @@ def info(ctx):
click.echo(f"插件仓库:{config.store_dir.absolute()}")
click.echo(f"日志级别:{config.log_level}")
click.echo(f"权限检查:{'启用' if config.permission_check else '禁用'}")
# 小提示:有些功能需要特殊方式访问 (4675747572654f53)
# 随机显示隐藏提示(约 10% 概率)
# 彩蛋提示
click.echo("")
if random.random() < 0.1: # 10% 概率显示彩蛋提示
if random.random() < 0.1:
click.echo("✨ 奇怪的提示:试试在命令前加两个感叹号会怎样?比如 !!help")
elif random.random() < 0.05: # 额外 5% 概率显示另一种提示
elif random.random() < 0.05:
click.echo("🤔 听说有人用 !! 开头的命令发现了不得了的东西...")
@@ -122,50 +154,234 @@ def cli_command(connect_host, connect_port):
click.echo(f"目标后端:{connect_host}:{connect_port}")
# ═══════════════════════════════════════════════════════════════
# NBPF 命令组
# ═══════════════════════════════════════════════════════════════
@cli.group()
def nbpf():
"""管理 .nbpf 插件包(打包/解包/签名/验证/密钥生成)"""
pass
@nbpf.command()
@click.argument('plugin_dir', type=click.Path(exists=True, file_okay=False, dir_okay=True))
@click.argument('output', type=click.Path(), default=None, required=False)
@click.option('--ed25519-key', type=click.Path(exists=True), help='Ed25519 私钥路径')
@click.option('--rsa-key', type=click.Path(exists=True), help='RSA 私钥路径')
@click.option('--rsa-pub', type=click.Path(exists=True), help='RSA 公钥路径')
@click.option('--signer', default='unknown', help='签名者名称')
@click.pass_context
def pack(ctx, plugin_dir, output, ed25519_key, rsa_key, rsa_pub, signer):
"""打包插件目录为 .nbpf 文件"""
from oss.core.nbpf import NBPFPacker
plugin_path = Path(plugin_dir)
if not output:
output = f"{plugin_path.name}.nbpf"
# 读取密钥
ed25519_private = Path(ed25519_key).read_bytes() if ed25519_key else None
rsa_private_pem = Path(rsa_key).read_bytes() if rsa_key else None
rsa_public_pem = Path(rsa_pub).read_bytes() if rsa_pub else None
if not ed25519_private:
click.echo("错误: 需要 Ed25519 私钥 (--ed25519-key)", err=True)
raise click.Abort()
if not rsa_private_pem:
click.echo("错误: 需要 RSA 私钥 (--rsa-key)", err=True)
raise click.Abort()
if not rsa_public_pem:
click.echo("错误: 需要 RSA 公钥 (--rsa-pub)", err=True)
raise click.Abort()
click.echo(f"打包插件: {plugin_path}")
click.echo(f"输出文件: {output}")
click.echo(f"签名者: {signer}")
try:
packer = NBPFPacker()
result = packer.pack(
plugin_dir=plugin_path,
output_path=Path(output),
ed25519_private_key=ed25519_private,
rsa_private_key_pem=rsa_private_pem,
rsa_public_key_pem=rsa_public_pem,
signer_name=signer,
)
click.echo(f"打包成功: {result}")
except Exception as e:
click.echo(f"打包失败: {type(e).__name__}: {e}", err=True)
raise click.Abort()
@nbpf.command()
@click.argument('nbpf_file', type=click.Path(exists=True, dir_okay=False))
@click.argument('output_dir', type=click.Path(), default=None, required=False)
def unpack(nbpf_file, output_dir):
"""解包 .nbpf 文件到目录"""
from oss.core.nbpf import NBPFUnpacker
nbpf_path = Path(nbpf_file)
if not output_dir:
output_dir = nbpf_path.stem
click.echo(f"解包: {nbpf_path}")
click.echo(f"输出目录: {output_dir}")
try:
unpacker = NBPFUnpacker()
result = unpacker.unpack(nbpf_path, Path(output_dir))
click.echo(f"解包成功: {result}")
except Exception as e:
click.echo(f"解包失败: {type(e).__name__}: {e}", err=True)
raise click.Abort()
@nbpf.command()
@click.argument('nbpf_file', type=click.Path(exists=True, dir_okay=False))
@click.option('--trusted-keys-dir', type=click.Path(exists=True), help='信任的 Ed25519 公钥目录')
def verify(nbpf_file, trusted_keys_dir):
"""验证 .nbpf 文件签名"""
from oss.core.nbpf import NBPFUnpacker
nbpf_path = Path(nbpf_file)
# 加载信任密钥
trusted_keys = {}
if trusted_keys_dir:
keys_path = Path(trusted_keys_dir)
for kf in keys_path.glob("*.pem"):
trusted_keys[kf.stem] = kf.read_bytes()
else:
# 尝试从默认目录加载
default_dir = Path("./data/nbpf-keys/trusted")
if default_dir.exists():
for kf in default_dir.glob("*.pem"):
trusted_keys[kf.stem] = kf.read_bytes()
if not trusted_keys:
click.echo("警告: 未加载任何信任密钥,将尝试提取 manifest 信息", err=True)
click.echo(f"验证: {nbpf_path}")
click.echo(f"信任密钥: {len(trusted_keys)}")
try:
unpacker = NBPFUnpacker()
manifest = unpacker.extract_manifest(nbpf_path)
click.echo(f"插件名称: {manifest.get('metadata', {}).get('name', '未知')}")
click.echo(f"版本: {manifest.get('metadata', {}).get('version', '未知')}")
click.echo(f"作者: {manifest.get('metadata', {}).get('author', '未知')}")
if trusted_keys:
valid, msg = unpacker.verify_signature(nbpf_path, trusted_keys)
if valid:
click.echo(f"签名验证: 通过 ({msg})")
else:
click.echo(f"签名验证: 失败 ({msg})", err=True)
raise click.Abort()
except Exception as e:
click.echo(f"验证失败: {type(e).__name__}: {e}", err=True)
raise click.Abort()
@nbpf.command()
@click.argument('nbpf_file', type=click.Path(exists=True, dir_okay=False))
@click.option('--ed25519-key', type=click.Path(exists=True), help='Ed25519 私钥路径')
@click.option('--signer', default=None, help='签名者名称')
def sign(nbpf_file, ed25519_key, signer):
"""为 .nbpf 文件重新签名"""
from oss.core.nbpf import NBPFPacker, NBPFUnpacker
nbpf_path = Path(nbpf_file)
if not ed25519_key:
click.echo("错误: 需要 Ed25519 私钥 (--ed25519-key)", err=True)
raise click.Abort()
ed25519_private = Path(ed25519_key).read_bytes()
click.echo(f"重新签名: {nbpf_path}")
try:
# 解包
temp_dir = nbpf_path.parent / f".{nbpf_path.stem}_tmp"
if temp_dir.exists():
import shutil
shutil.rmtree(temp_dir)
NBPFUnpacker().unpack(nbpf_path, temp_dir)
# 重新打包
packer = NBPFPacker()
result = packer.pack(
plugin_dir=temp_dir,
output_path=nbpf_path,
ed25519_private_key=ed25519_private,
rsa_private_key_pem=None,
rsa_public_key_pem=None,
signer_name=signer or "resign",
)
click.echo(f"重新签名成功: {result}")
# 清理临时目录
import shutil
shutil.rmtree(temp_dir)
except Exception as e:
click.echo(f"重新签名失败: {type(e).__name__}: {e}", err=True)
raise click.Abort()
@nbpf.command(name="keygen")
@click.option('--output-dir', type=click.Path(), default='./data/nbpf-keys', help='密钥输出目录')
@click.option('--name', default='default', help='密钥名称')
def keygen(output_dir, name):
"""生成 Ed25519 + RSA 密钥对"""
from oss.core.nbpf import NBPCrypto
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
# 创建子目录
trusted_dir = output_path / "trusted"
rsa_dir = output_path / "rsa"
private_dir = output_path / "private"
trusted_dir.mkdir(exist_ok=True)
rsa_dir.mkdir(exist_ok=True)
private_dir.mkdir(exist_ok=True)
click.echo(f"生成密钥对到: {output_path}")
# 生成 Ed25519 密钥对
click.echo("生成 Ed25519 密钥对...")
ed25519_private, ed25519_public = NBPCrypto.generate_ed25519_keypair()
(trusted_dir / f"{name}.pem").write_bytes(ed25519_public)
(private_dir / f"{name}_ed25519.pem").write_bytes(ed25519_private)
click.echo(f" Ed25519 公钥: {trusted_dir / f'{name}.pem'}")
click.echo(f" Ed25519 私钥: {private_dir / f'{name}_ed25519.pem'}")
# 生成 RSA 密钥对
click.echo("生成 RSA-4096 密钥对(可能需要几秒钟)...")
rsa_private, rsa_public = NBPCrypto.generate_rsa_keypair(key_size=4096)
(rsa_dir / f"{name}.pem").write_bytes(rsa_public)
(private_dir / f"{name}_rsa.pem").write_bytes(rsa_private)
click.echo(f" RSA 公钥: {rsa_dir / f'{name}.pem'}")
click.echo(f" RSA 私钥: {private_dir / f'{name}_rsa.pem'}")
click.echo("密钥生成完成!")
click.echo("")
click.echo("使用示例:")
click.echo(f" nebula nbpf pack ./my-plugin --ed25519-key {private_dir / f'{name}_ed25519.pem'} --rsa-key {private_dir / f'{name}_rsa.pem'} --rsa-pub {rsa_dir / f'{name}.pem'} --signer {name}")
def main():
# 检测是否通过已弃用的 oss 命令调用
cmd_name = os.path.basename(sys.argv[0])
if cmd_name in ("oss", "oss.exe"):
print("╔══════════════════════════════════════════╗")
print("║ ⚠ oss 命令已弃用,请使用 nebula 替代 ║")
print("║ 例如: nebula serve ║")
print("║ nebula info ║")
print("║ nebula version ║")
print("╚══════════════════════════════════════════╝")
Log.warn("NebulaShell", "oss 命令已弃用,请使用 nebula 替代")
sys.exit(1)
# 检查隐藏命令前缀
if len(sys.argv) > 1 and sys.argv[1].startswith("!!"):
if _ACHIEVEMENTS_ENABLED:
cmd = sys.argv[1][2:] # 去掉 !! 前缀
args = sys.argv[2:]
# 映射隐藏命令
cmd_map = {
"echo": _cmd_echo,
"help": _cmd_help_internal,
"list": _cmd_list_all,
"stats": _cmd_stats,
"reset": _cmd_reset_progress,
"export": _cmd_export,
"import": _cmd_import,
"verify": _cmd_verify,
"debug": _cmd_debug,
"info": _cmd_info,
}
if cmd in cmd_map:
validator = get_validator()
validator.use_hidden_command(cmd)
cmd_map[cmd](args)
return
else:
print(f"未知命令:!!{cmd}")
return
else:
print("成就系统未启用")
return
if _handle_hidden_command():
return
cli()

View File

@@ -49,6 +49,13 @@ class Config:
# 性能配置
"MAX_WORKERS": 4,
"ENABLE_ASYNC": False,
# NBPF 配置
"NBPF_KEYS_DIR": "./data/nbpf-keys",
"NBPF_TRUSTED_KEYS_DIR": "./data/nbpf-keys/trusted",
"NBPF_RSA_KEYS_DIR": "./data/nbpf-keys/rsa",
"NBPF_ENCRYPTION_ENABLED": True,
"NBPF_SIGNATURE_REQUIRED": True,
}
def __init__(self, config_file: Optional[str] = None):
@@ -74,7 +81,7 @@ class Config:
self._config[key] = value
# 隐藏成就:配置黑客 - 记录配置修改
if _ACHIEVEMENTS_ENABLED:
if Config._ACHIEVEMENTS_ENABLED:
try:
from oss.core.achievements import get_validator
validator = get_validator()

1687
oss/core/engine.py Normal file

File diff suppressed because it is too large Load Diff

View File

View 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

View 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()

View 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
View 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
View 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
View 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
View 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
View 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
View 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}',
}

View File

186
oss/core/repl/main.py Normal file
View 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

View File

@@ -41,22 +41,6 @@ class Log:
def ok(cls, tag: str, msg: str):
print(f"{cls._c(f'[{tag}]', 'white')} {cls._c(msg, 'white')}")
class Logger:
"""日志记录器(兼容旧接口)"""
def info(self, msg: str, **kwargs):
tag = kwargs.get("tag", "INFO")
Log.info(tag, msg)
def warn(self, msg: str, **kwargs):
tag = kwargs.get("tag", "WARN")
Log.warn(tag, msg)
def error(self, msg: str, **kwargs):
tag = kwargs.get("tag", "ERROR")
Log.error(tag, msg)
def debug(self, msg: str, **kwargs):
tag = kwargs.get("tag", "DEBUG")
Log.tip(tag, msg)
@classmethod
def debug(cls, tag: str, msg: str):
cls.tip(tag, msg)

View File

@@ -1,22 +0,0 @@
"""插件基础类"""
from abc import ABC, abstractmethod
from typing import Any, Optional
class Plugin(ABC):
"""插件基类"""
@abstractmethod
def init(self, deps: Optional[dict] = None):
"""初始化插件"""
pass
@abstractmethod
def start(self):
"""启动插件"""
pass
@abstractmethod
def stop(self):
"""停止插件"""
pass

View File

@@ -1,3 +1,7 @@
import ast
# 启发式能力扫描:通过 AST 分析插件源码,基于命名约定和导入推断插件提供的能力
# 这是一种轻量级的静态分析,不执行任何代码,仅用于快速发现插件可能提供的能力
def scan_capabilities(plugin_dir):
capabilities: set[str] = set()
main_file = plugin_dir / "main.py"

View File

@@ -1,7 +1,7 @@
"""插件管理器 - 只加载 plugin-loader其他所有插件由 plugin-loader 插件自行管理"""
"""插件管理器 - 直接使用框架层的 Core Engine"""
from typing import Any, Optional
from oss.plugin.loader import PluginLoader
from oss.core.engine import PluginManager as CorePluginManager
# 深度隐藏的成就系统导入
try:
@@ -13,32 +13,20 @@ except ImportError:
class PluginManager:
"""极简插件管理器
遵循「最小化核心框架」设计哲学:
- 核心框架只负责加载 plugin-loader 插件
- 所有其他插件HTTP、WebSocket、Dashboard 等)都由 plugin-loader 插件扫描和加载
- store/NebulaShell/ 是唯一的插件来源
直接使用框架层的 CorePluginManager原 Core 插件功能)
- 不再通过插件加载器加载 Core
- 所有核心功能直接集成在 oss.core.engine 中
"""
def __init__(self):
self.loader = PluginLoader()
self.plugin_loader: Optional[Any] = None
self.core = CorePluginManager()
def load(self):
"""加载 plugin-loader 核心插件
plugin-loader 插件会负责:
1. 扫描 store/NebulaShell/ 目录
2. 加载所有启用的插件
3. 处理依赖关系
4. 执行 PL 注入机制
"""
# 只加载 plugin-loader其他所有插件都由它来管理
pl_info = self.loader.load_core_plugin("plugin-loader")
if pl_info:
self.plugin_loader = pl_info["instance"]
# 隐藏成就:深海潜水员 - 当加载插件管理器时解锁
"""加载所有插件(由 CorePluginManager 管理)"""
self.core.load_all()
# 隐藏成就:深海潜水员
if _ACHIEVEMENTS_ENABLED:
try:
validator = get_validator()
@@ -47,48 +35,48 @@ class PluginManager:
pass
def start(self):
"""启动 plugin-loader,它会初始化并启动所有其他插件"""
"""启动 Core,它会初始化并启动所有其他插件"""
import time
start_time = time.time()
if self.plugin_loader:
# plugin-loader.init() 会扫描并加载 store/ 中的所有插件
self.plugin_loader.init()
# plugin-loader.start() 会按依赖顺序启动所有插件
self.plugin_loader.start()
self.core.init_and_start_all()
# 启动 HTTP 服务
self.core.start_http_server()
# 启动防篡改监控
self.core.start_tamper_monitor()
# 计算启动时间并检查速度成就
elapsed_ms = (time.time() - start_time) * 1000
if _ACHIEVEMENTS_ENABLED:
try:
validator = get_validator()
validator.check_startup_speed(elapsed_ms)
# 检查插件数量成就
if hasattr(self.plugin_loader, 'manager') and hasattr(self.plugin_loader.manager, 'plugins'):
plugin_count = len(self.plugin_loader.manager.plugins)
validator.check_plugin_count(plugin_count)
plugin_count = len(self.core.plugins)
validator.check_plugin_count(plugin_count)
except Exception:
pass
def stop(self):
"""停止所有插件(由 plugin-loader 统一管理)"""
if self.plugin_loader:
"""停止所有插件"""
try:
self.core.stop_tamper_monitor()
self.core.stop_http_server()
self.core.stop_all()
except KeyboardInterrupt:
print("[PluginManager] 用户中断停止过程")
except Exception as e:
import traceback
print(f"[PluginManager] 停止插件时出错:{type(e).__name__}: {e}")
traceback.print_exc()
# 隐藏成就:崩溃幸存者
if _ACHIEVEMENTS_ENABLED:
try:
self.plugin_loader.stop()
except KeyboardInterrupt:
print("[PluginManager] 用户中断停止过程")
except Exception as e:
import traceback
print(f"[PluginManager] 停止插件时出错:{type(e).__name__}: {e}")
traceback.print_exc()
# 隐藏成就:崩溃幸存者 - 如果正常停止则不解锁,只有异常停止才可能解锁
# 这里我们记录停止事件,用于将来可能的连续运行成就
if _ACHIEVEMENTS_ENABLED:
try:
validator = get_validator()
# 记录会话结束
validator.track_progress("session_end")
except Exception:
pass
validator = get_validator()
validator.track_progress("session_end")
except Exception:
pass

View File

@@ -1,3 +1,7 @@
from typing import Callable
from functools import lru_cache
class BaseRoute:
__slots__ = ('method', 'path', 'handler', '_pattern_parts')
@@ -9,6 +13,16 @@ class BaseRoute:
self._pattern_parts = path.strip("/").split("/") if ":" in path else None
def _get_pattern_parts(pattern: str):
if ":" not in pattern:
return None
return pattern.strip("/").split("/")
def _is_wildcard_param(param: str) -> bool:
return param.startswith(":") and param.endswith("*")
@lru_cache(maxsize=1024)
def match_path(pattern: str, path: str) -> bool:
if pattern == path:
@@ -41,12 +55,6 @@ def match_path(pattern: str, path: str) -> bool:
return True
def _is_wildcard_param(param: str) -> bool:
if ":" not in pattern:
return None
return pattern.strip("/").split("/")
@lru_cache(maxsize=1024)
def extract_path_params(pattern: str, path: str) -> dict[str, str]:
params = {}
@@ -85,9 +93,15 @@ class BaseRouter:
self.routes: list[BaseRoute] = []
def add(self, method: str, path: str, handler: Callable):
self.routes.append(BaseRoute(method, path, handler))
def get(self, path: str, handler: Callable):
self.add("GET", path, handler)
def post(self, path: str, handler: Callable):
self.add("POST", path, handler)
def put(self, path: str, handler: Callable):
self.add("PUT", path, handler)
def delete(self, path: str, handler: Callable):