Compare commits

..
6 Commits
Author SHA1 Message Date
chakcy fa836f9453 modify: 修复 pyarmor 模式无法使用字节码加载的问题 2026-05-04 20:04:31 +08:00
chakcy 1fdb5bd1bf modify: pyarmor 加密支持 2026-05-04 19:06:51 +08:00
chakcy 0388984080 modify: 模块排除密钥 2026-05-01 15:13:39 +08:00
chakcy e7e2a67ea9 modify: 添加密钥 2026-05-01 14:32:08 +08:00
chakcy 5ae2a902b1 modify: 加密功能 2026-03-03 14:46:01 +08:00
chakcy ac766a79b6 modify: pack_directory add packager_name arg 2026-01-31 14:35:54 +08:00
21 changed files with 3196 additions and 110 deletions
+3
View File
@@ -12,3 +12,6 @@ wheels/
output.md
log/
*.mb
*.mbank
dual_protection_
*.db
+1 -1
View File
@@ -1 +1 @@
3.13
3.12
+204 -63
View File
@@ -1,15 +1,16 @@
# Python Module Bank - SQLite 模块打包系统
一个将Python模块打包到SQLite数据库,并支持从数据库直接导入模块的工具系统。
一个将 Python 模块打包到 SQLite 数据库,并支持从数据库直接导入模块的工具系统。支持 **AES 字节码加密** 和 **PyArmor 代码混淆** 双重保护机制。
## 🌟 特性
- **单文件分发** - 将所有模块打包到单个SQLite数据库文件
- **源代码保护** - 模块以编译后的字节码形式存储
- **动态导入** - 运行时直接从数据库加载模块,无需文件系统
- **单文件分发** - 将所有模块打包到单个 SQLite 数据库文件
- **AES 字节码加密** - 编译后的字节码使用 Fernet (AES-128-CBC) 加密存储
- **PyArmor 代码混淆** - 支持使用 PyArmor 对 Python 源代码进行混淆保护
- **双层保护** - 可同时使用 PyArmor 混淆 + AES 字节码加密,提供双重安全保障
- **动态导入** - 运行时直接从数据库加载模块,无缝集成 Python 导入系统
- **完整包支持** - 支持包结构和子模块导入
- **CLI工具** - 提供完整的命令行接口
- **导入钩子** - 无缝集成Python导入系统
- **CLI 工具** - 提供完整的命令行接口
## 📦 安装
@@ -21,63 +22,154 @@ cd module-bank
pip install -e .
```
### 依赖要求
### 使用 uv(推荐)
- Python 3.7+
- 无需额外依赖(仅使用标准库)
```bash
git clone http://124.71.68.6:3000/chakcy/module_bank.git
cd module-bank
uv sync
```
### 可选依赖
- **PyArmor** (强烈推荐): `uv add pyarmor` 或 `pip install pyarmor`
- 为模块提供 Python 源代码级别的混淆保护
- 商业版可去除试用版水印
## 🚀 快速开始
### 1. 创建示例模块
```python
# my_module.py
# my_package/__init__.py
def hello():
print("Hello from my_module!")
print("Hello from my_package!")
return "success"
VERSION = "1.0.0"
```
```python
# my_package/utils.py
def add(a, b):
return a + b
```
### 2. 打包模块到数据库
```python
# pack_example.py
from module_bank import PythonToSQLite
#### 使用 AES 加密(推荐)
packer = PythonToSQLite("my_modules.db")
packer.pack_module("my_module.py", "my_module")
packer.pack_directory("my_package/")
```bash
# 生成密钥并打包
mbank pack my_package --db my_package.mbank --key "$(python -c "from module_bank.encryption import Encryption; print(Encryption.generate_key())")"
```
#### 使用 PyArmor 混淆
```bash
# 使用 PyArmor 保护源代码
mbank pack my_package --db my_package.mbank --pyarmor
```
#### 使用双层保护
```bash
# PyArmor 混淆 + AES 加密
mbank pack my_package --db my_package.mbank --pyarmor --key "your-encryption-key"
```
### 3. 从数据库导入
```python
from module_bank import PythonToSQLite
# import_example.py
from module_bank.python_to_sqlite import PythonToSQLite
# 安装导入器
packer = PythonToSQLite("my_modules.db")
finder = packer.install_importer()
packer = PythonToSQLite("my_package.mbank")
finder = packer.install_importer(key="your-encryption-key") # 如果加密了需要提供密钥
# 现在可以从数据库导入模块了!
import my_module
import my_package.package_module
from my_package import utils
from my_package import hello
my_module.hello()
my_package.package_module.hello()
print(hello())
print(utils.add(3, 4))
```
## 🛡️ 安全保护机制
### 第一层:PyArmor 代码混淆
PyArmor 对 Python 源代码进行混淆,将代码转换为难以阅读和逆向的形式:
- 混淆后的代码依赖运行时二进制文件(`.pyd`/`.so`),无法直接运行
- 支持混淆级别控制(代码混淆、模块混淆)
- 运行时文件会自动存储在数据库中,加载时自动部署
```bash
# 高级混淆选项
mbank pack my_module.py --name my_module --db modules.db \
--pyarmor \
--pyarmor-obf-code 1 \
--pyarmor-obf-mod 1
```
### 第二层:AES 字节码加密
编译后的字节码使用 Fernet (AES-128-CBC) 加密:
```bash
# 生成密钥
python -c "from module_bank.encryption import Encryption; print(Encryption.generate_key())" > my_key.key
# 使用密钥打包
mbank pack my_module.py --name my_module --db modules.db --key "$(cat my_key.key)"
# 使用时提供密钥
mbank install --db modules.db --key "$(cat my_key.key)"
```
### 安全建议
1. **密钥管理**:加密密钥不要硬编码在代码中,使用环境变量或密钥管理服务
2. **删除源代码**:打包后可从数据库删除源代码,仅保留混淆后的代码:
```bash
# 删除所有模块的源代码
mbank delete_source --db modules.db
# 删除指定模块的源代码
mbank delete_source --db modules.db --module my_module
```
## 📖 详细使用
### 命令行工具
```python
# 打包模块或目录
mb pack my_package --db modules.db
```bash
# 打包单个模块
mbank pack my_module.py --name my_module --db modules.db
# 打包整个目录(自动识别包结构)
mbank pack my_package/ --db modules.db
# 使用 PyArmor 混淆打包
mbank pack my_module.py --name my_module --db modules.db --pyarmor
# 使用 AES 加密打包
mbank pack my_module.py --name my_module --db modules.db --key "your-key"
# 双层保护
mbank pack my_module.py --name my_module --db modules.db --pyarmor --key "your-key"
# 列出数据库中的模块
mb list --db modules.db
mbank list --db modules.db
# 删除源代码
mbank delete_source --db modules.db
mbank delete_source --db modules.db --module my_module
# 安装导入器并进入交互模式
mb install --db modules.db
mbank install --db modules.db
mbank install --db modules.db --key "your-key"
```
### 编程接口
@@ -86,8 +178,21 @@ mb install --db modules.db
```python
from module_bank import PythonToSQLite
from module_bank.encryption import Encryption
packer = PythonToSQLite("modules.db")
# 生成加密密钥
key = Encryption.generate_key()
# 使用 AES 加密打包
packer = PythonToSQLite("modules.mbank", key=key)
# 或使用 PyArmor 混淆打包
packer = PythonToSQLite(
"modules.mbank",
pyarmor_obfuscate=True,
pyarmor_obf_code=1,
pyarmor_obf_mod=1,
)
# 打包单个模块
packer.pack_module("module.py", "module_name")
@@ -103,21 +208,25 @@ packer.verify_package_structure()
```python
from module_bank import PythonToSQLite
import sys
packer = PythonToSQLite("modules.db")
# 如果数据库中的模块被 AES 加密了,需要提供密钥
packer = PythonToSQLite("modules.mbank")
# 安装导入器到sys.meta_path
finder = packer.install_importer()
# 安装导入器到 sys.meta_path
finder = packer.install_importer(key="your-key")
# 列出所有可用模块
modules = packer.list_modules()
for module in modules:
print(f"{module['module_name']} {'[包]' if module['is_package'] else ''}")
package_flag = " [包]" if module["is_package"] else ""
print(f" - {module['module_name']}{package_flag}")
# 导入数据库中的模块
# 直接导入数据库中的模块(就像普通模块一样)
import my_package
import my_package.submodule
print(my_package)
my_package.hello()
```
## 🏗️ 架构设计
@@ -130,6 +239,8 @@ src/module_bank/
├── sqlite_module_importer.py # 数据库存储管理器
├── sqlite_meta_path_finder.py # 元路径查找器
├── sqlite_module_loader.py # 模块加载器
├── encryption.py # AES 密钥管理
├── pyarmor_encryption.py # PyArmor 混淆封装
├── cli.py # 命令行接口
└── __init__.py # 模块导出
```
@@ -138,23 +249,35 @@ src/module_bank/
```text
1. 打包阶段:
.py文件 → 编译为字节码 → 存储到SQLite数据库
.py 文件 → (可选: PyArmor 混淆) → 编译为字节码 → (可选: AES 加密) → 存储到 SQLite
2. 导入阶段:
导入请求 → MetaPathFinder查找 → ModuleLoader加载 → 执行模块
导入请求 → MetaPathFinder 查找 → ModuleLoader 加载 → (AES 解密) → (PyArmor 运行时部署) → 执行模块
```
### 数据库模式
```sql
-- 模块表
CREATE TABLE python_modules (
module_name TEXT PRIMARY KEY,
source_code TEXT, -- 源代码(可选)
bytecode BLOB, -- 编译后的字节码
source_code TEXT, -- 源代码(PyArmor 混淆后)
bytecode BLOB, -- 编译后的字节码(可选 AES 加密)
is_package BOOLEAN, -- 是否是包
metadata TEXT, -- 元数据(JSON格式)
metadata TEXT, -- 元数据(JSON 格式)
encrypted INTEGER DEFAULT 0, -- AES 是否已加密
pyarmor_protected INTEGER DEFAULT 0, -- 是否已 PyArmor 混淆
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
);
-- PyArmor 运行时存储表
CREATE TABLE pyarmor_runtime (
id INTEGER PRIMARY KEY AUTOINCREMENT,
runtime_name TEXT UNIQUE NOT NULL,
pyd_data BLOB, -- 原生二进制文件(.pyd/.so)
init_data TEXT, -- __init__.py 内容
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
```
## 🔧 高级功能
@@ -165,7 +288,7 @@ CREATE TABLE python_modules (
# 打包时排除特定文件
packer.pack_directory(
"my_project/",
exclude_patterns=["*_test.py", "*.pyc", "__pycache__"]
exclude_patterns=["*_test.py", "*.pyc", "__pycache__"],
)
```
@@ -177,42 +300,56 @@ packer.importer.add_module(
"my_module",
source_code,
is_package=False,
metadata={"version": "1.0", "author": "me"}
metadata={"version": "1.0", "author": "me"},
)
```
### 混合导入
```python
# 可以同时使用文件系统和数据库导入
# 数据库导入器优先级更高
# 数据库导入器默认插入到 meta_path 开头,优先级高于文件系统
import sys
from module_bank import PythonToSQLite
packer = PythonToSQLite("modules.db")
finder = packer.install_importer() # 插入到meta_path开头
finder = packer.install_importer()
# 如果需要文件系统优先,可以调整插入位置
# 如果需要文件系统优先,将查找器追加到末尾
sys.meta_path.remove(finder)
sys.meta_path.append(finder)
```
### 验证运行时完整性
```python
from module_bank.sqlite_module_importer import SQLiteModuleImporter
importer = SQLiteModuleImporter("modules.db")
runtimes = importer.list_runtimes()
for rt in runtimes:
print(f"运行时: {rt['runtime_name']} (创建于: {rt['created_at']})")
```
## ⚠️ 注意事项
### 安全性
- 模块字节码直接执行,确保数据库来源可信
- 生产环境建议添加代码签名验证
- AES 加密密钥必须安全保管,丢失后将无法解密模块
- PyArmor 混淆提供源代码级别的保护,但商业版才能去除试用版水印
- 生产环境建议 PyArmor 混淆 + AES 加密同时使用
### 兼容性
- 字节码不跨Python版本兼容
- 不支持C扩展模块
- 不支持需要文件系统资源的模块(如__file__依赖)
- 字节码不跨 Python 版本兼容(3.12 编译的模块无法在 3.11 加载)
- 不支持 C 扩展模块
- PyArmor 运行时文件与操作系统相关(Windows 用 `.pyd`,Linux 用 `.so`)
- 运行时文件需在目标系统上重新打包
### 性能
- **启动时**:有一次性数据库查询和反序列化开销
- **运行时**:与传统导入性能相同(使用sys.modules缓存)
- **启动时**:有一次性数据库查询和反序列化/运行时部署开销
- **运行时**:与传统导入性能相同(使用 sys.modules 缓存)
- **最佳适用**:长期运行的服务、桌面应用
### 更新模块
@@ -233,19 +370,23 @@ DELETE FROM python_modules WHERE module_name = 'module_to_remove';
```bash
# 数据库是单个文件,易于备份
cp modules.db modules.backup.db
cp modules.mbank modules.backup.mbank
# 恢复
cp modules.backup.db modules.db
cp modules.backup.mbank modules.mbank
```
## 📚 应用场景
1. 商业软件分发 - 保护源代码知识产权
2. 插件系统 - 动态加载数据库中的插件模块
3. 教育平台 - 安全分发练习代码
4. 微服务 - 打包多个服务模块到单个文件
5. 嵌入式系统 - 减少文件系统依赖
1. **商业软件分发** - PyArmor 混淆保护源代码知识产权
2. **安全敏感环境** - AES 加密确保即使数据库泄露也无法还原代码
3. **插件系统** - 动态加载数据库中的加密/混淆插件模块
4. **教育平台** - 安全分发练习代码,防止学生抄袭
5. **微服务** - 打包多个服务模块到单个加密文件
6. **嵌入式系统** - 减少文件系统依赖,提高安全性
---
**注意**: 本工具主要用于模块分发和部署场景,不适合开发阶段的频繁修改。
**许可证**: MIT License
+202
View File
@@ -0,0 +1,202 @@
"""
PyArmor + AES 双层保护完整示例
本示例演示如何同时使用 PyArmor 混淆和 AES 字节码加密
来提供双重保护。
"""
import sys
import os
import tempfile
import shutil
# 添加项目源码路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
# ========== 1. 创建一个需要保护的模块 ==========
SECRET_MODULE_CODE = """
# 这是需要保护的敏感代码(支付逻辑、授权验证等)
import hashlib
import hmac
SECRET_KEY = "this-is-a-top-secret-key"
def verify_license(license_key: str) -> bool:
\"\"\"验证许可证密钥(敏感业务逻辑)\"\"\"
expected = hmac.new(
SECRET_KEY.encode(),
license_key.encode(),
hashlib.sha256
).hexdigest()
return expected.startswith("a1b2")
def calculate_commission(amount: float, level: int) -> float:
\"\"\"计算佣金(商业机密算法)\"\"\"
rates = {1: 0.1, 2: 0.15, 3: 0.25, 4: 0.35}
rate = rates.get(level, 0.05)
# 敏感计算公式
commission = amount * rate * (1.0 - 0.05)
return round(commission, 2)
class PaymentProcessor:
\"\"\"支付处理器(核心商业逻辑)\"\"\"
def __init__(self, merchant_id: str):
self.merchant_id = merchant_id
def process(self, order_id: str, amount: float) -> dict:
# 模拟支付处理
return {
"status": "success",
"order_id": order_id,
"amount": amount,
"merchant": self.merchant_id,
"transaction_id": hashlib.sha256(
f"{order_id}{self.merchant_id}".encode()
).hexdigest()[:16]
}
VERSION = "2.1.0"
BUILD = "2026-05-04"
"""
def main():
print("=" * 70)
print("PyArmor + AES 双层保护完整示例")
print("=" * 70)
# 创建临时工作目录
# work_dir = tempfile.mkdtemp(prefix="dual_protection_")
# db_path = os.path.join(work_dir, "protected_modules.db")
db_path = os.path.join("dual_protection_", "protected_modules.db")
key_file = os.path.join("dual_protection_", "encryption_key.txt")
try:
# ========== 步骤 1: 引入模块 ==========
from module_bank.encryption import Encryption
from module_bank.python_to_sqlite import PythonToSQLite
# ========== 步骤 2: 生成 AES 加密密钥 ==========
print("\n[步骤 1/5] 生成 AES 加密密钥...")
aes_key = Encryption.generate_key()
with open(key_file, "w") as f:
f.write(aes_key)
print(f" AES 密钥已生成: {aes_key[:20]}...")
# ========== 步骤 3: 使用双层保护打包模块 ==========
print("\n[步骤 2/5] 使用 PyArmor + AES 双层保护打包模块...")
packer = PythonToSQLite(
db_path,
key=aes_key, # AES 字节码加密
pyarmor_obfuscate=True, # PyArmor 源代码混淆
pyarmor_obf_code=1, # 启用代码混淆
pyarmor_obf_mod=1, # 启用模块混淆
)
packer.importer.add_module(
"payment_system",
SECRET_MODULE_CODE,
is_package=False,
metadata={
"version": "2.1.0",
"protection": "pyarmor_obfuscation + aes_encryption",
},
)
print(" ✓ 模块已使用双层保护存储到数据库")
# ========== 步骤 4: 验证数据库中的保护状态 ==========
print("\n[步骤 3/5] 验证数据库中的保护状态...")
cursor = packer.importer.connection.execute(
"""SELECT module_name, encrypted, pyarmor_protected, source_code,
LENGTH(bytecode) as bytecode_size,
LENGTH(source_code) as source_size
FROM python_modules"""
)
row = cursor.fetchone()
print(f" 模块名: {row['module_name']}")
print(f" AES 加密: {'✓ 是' if row['encrypted'] else '✗ 否'}")
print(f" PyArmor 混淆: {'✓ 是' if row['pyarmor_protected'] else '✗ 否'}")
print(f" 字节码大小: {row['bytecode_size']} 字节")
print(f" 源代码大小: {row['source_size']} 字节")
# 验证 PyArmor 运行时也已存储
runtimes = packer.importer.list_runtimes()
for rt in runtimes:
print(f" PyArmor 运行时: {rt['runtime_name']}")
# 显示数据库中存储的混淆代码片段
print("\n[验证] 数据库中的混淆代码(前200字符):")
obfuscated = row["source_code"]
print(f" {obfuscated[:200]}...")
# ========== 步骤 5: 从数据库导入并执行 ==========
print("\n[步骤 4/5] 从数据库导入模块(需要提供 AES 密钥)...")
# 清除之前的导入
if "payment_system" in sys.modules:
del sys.modules["payment_system"]
# 使用密钥安装导入器
finder = packer.install_importer(key=aes_key)
# 导入模块
import importlib
mod = importlib.import_module("payment_system")
print(f" ✓ 模块导入成功: {mod}")
print(f" 模块文件: {mod.__file__}")
# ========== 步骤 6: 验证模块功能正常 ==========
print("\n[步骤 5/5] 验证模块功能...")
# 调用验证许可证函数
result = mod.verify_license("TEST-1234-5678")
print(f" verify_license('TEST-1234-5678') → {result}")
# 调用佣金计算
commission = mod.calculate_commission(10000.0, 3)
print(f" calculate_commission(10000, 3) → ¥{commission}")
# 使用支付处理器
processor = mod.PaymentProcessor("MERCHANT_001")
payment_result = processor.process("ORDER_20260504", 299.00)
print(f" PaymentProcessor.process() → {payment_result['status']}")
print(f" 交易ID: {payment_result['transaction_id']}")
# 访问模块常量
print(f"\n 模块版本: {mod.VERSION}")
print(f" 构建日期: {mod.BUILD}")
# ========== 安全验证 ==========
print("\n" + "=" * 70)
print("安全验证")
print("=" * 70)
print("""
1. PyArmor 混淆: 源代码已转换为不可读的混淆代码
2. AES 加密: 字节码已使用 AES-128-CBC 加密
3. 运行时保护: PyArmor 运行时二进制文件无法直接加载
4. 双重解密: 需要先 AES 解密 → 再通过 PyArmor 运行时执行
即使有人获取了数据库文件:
- 没有 AES 密钥 → 无法解密字节码
- 没有 PyArmor 运行时 → 无法执行混淆代码
- 两者同时具备 → 才能解密和执行
""")
except Exception as e:
print(f"\n❌ 错误: {e}")
import traceback
traceback.print_exc()
finally:
# 清理
print("\n[清理] 清除临时文件...")
# shutil.rmtree(work_dir, ignore_errors=True)
if "payment_system" in sys.modules:
del sys.modules["payment_system"]
print(" 完成!")
if __name__ == "__main__":
main()
+57
View File
@@ -0,0 +1,57 @@
from module_bank import PythonToSQLite
with open("./dual_protection_/encryption_key.txt") as f:
key = f.read()
packer = PythonToSQLite(
"./dual_protection_/protected_modules.db",
key=key,
pyarmor_obfuscate=True,
pyarmor_obf_code=1,
pyarmor_obf_mod=1,
)
packer.install_importer()
import payment_system # type: ignore
mod = payment_system
print(f" ✓ 模块导入成功: {mod}")
print(f" 模块文件: {mod.__file__}")
# ========== 步骤 6: 验证模块功能正常 ==========
print("\n[步骤 5/5] 验证模块功能...")
# 调用验证许可证函数
result = mod.verify_license("TEST-1234-5678")
print(f" verify_license('TEST-1234-5678') → {result}")
# 调用佣金计算
commission = mod.calculate_commission(10000.0, 3)
print(f" calculate_commission(10000, 3) → ¥{commission}")
# 使用支付处理器
processor = mod.PaymentProcessor("MERCHANT_001")
payment_result = processor.process("ORDER_20260504", 299.00)
print(f" PaymentProcessor.process() → {payment_result['status']}")
print(f" 交易ID: {payment_result['transaction_id']}")
# 访问模块常量
print(f"\n 模块版本: {mod.VERSION}")
print(f" 构建日期: {mod.BUILD}")
# ========== 安全验证 ==========
print("\n" + "=" * 70)
print("安全验证")
print("=" * 70)
print("""
1. PyArmor 混淆: 源代码已转换为不可读的混淆代码
2. AES 加密: 字节码已使用 AES-128-CBC 加密
3. 运行时保护: PyArmor 运行时二进制文件无法直接加载
4. 双重解密: 需要先 AES 解密 → 再通过 PyArmor 运行时执行
即使有人获取了数据库文件:
- 没有 AES 密钥 → 无法解密字节码
- 没有 PyArmor 运行时 → 无法执行混淆代码
- 两者同时具备 → 才能解密和执行
""")
+12 -4
View File
@@ -1,10 +1,13 @@
[project]
name = "module_bank"
version = "0.1.0"
dependencies = [
"cryptography>=45.0.7",
"pyarmor>=9.2.4",
]
description = "Add your description here"
name = "module_bank"
readme = "README.md"
requires-python = ">=3.7"
dependencies = []
version = "0.3.1"
[[tool.uv.index]]
default = true
@@ -14,8 +17,13 @@ url = "https://pypi.tuna.tsinghua.edu.cn/simple"
build-backend = "hatchling.build"
requires = ["hatchling"]
[dependency-groups]
dev = [
"twine>=4.0.2",
]
[tool.hatch.build.targets.wheel]
packages = ["src/module_bank"]
[project.scripts]
mb = "module_bank.cli:main"
mbank = "module_bank.cli:main"
+100
View File
@@ -0,0 +1,100 @@
"""
测试:删除 PyArmor 模块的源代码后仍能正常加载
"""
import sys
import os
import tempfile
import shutil
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
def main():
work_dir = tempfile.mkdtemp(prefix="test_delete_source_")
db_path = os.path.join(work_dir, "test.db")
try:
from module_bank.python_to_sqlite import PythonToSQLite
# 1. 创建并打包 PyArmor 模块
print("=" * 60)
print("测试:删除源代码后加载 PyArmor 模块")
print("=" * 60)
print("\n[1/5] 使用 PyArmor 打包模块...")
packer = PythonToSQLite(db_path, pyarmor_obfuscate=True)
packer.importer.add_module(
"mymod",
'def hello(): return "ok"',
is_package=False,
)
print(" 模块打包完成")
# 2. 验证 metadata 中已记录运行时名
print("\n[2/5] 验证 metadata 包含运行时名...")
cursor = packer.importer.connection.execute(
"SELECT metadata, source_code FROM python_modules WHERE module_name = ?",
("mymod",),
)
row = cursor.fetchone()
print(f" metadata: {row['metadata']}")
assert "pyarmor_runtime" in row["metadata"], "metadata 缺少运行时名"
print(" ✓ metadata 正确包含运行时名")
assert row["source_code"] is not None, "源代码不应为空"
print(" ✓ 源代码存在")
# 3. 导入一次(确保能工作)
print("\n[3/5] 导入模块(有源代码)...")
from module_bank.sqlite_meta_path_finder import SQLiteMetaPathFinder
finder = SQLiteMetaPathFinder(db_path)
sys.meta_path.insert(0, finder)
import importlib
mod = importlib.import_module("mymod")
assert mod.hello() == "ok"
print(f" ✓ 导入成功: hello() = {mod.hello()}")
# 4. 删除源代码
print("\n[4/5] 删除源代码...")
packer.delete_source_code("mymod")
cursor = packer.importer.connection.execute(
"SELECT source_code, metadata FROM python_modules WHERE module_name = ?",
("mymod",),
)
row = cursor.fetchone()
print(f" 删除后 source_code = {row['source_code']}")
assert row["source_code"] is None, "源代码未成功删除"
print(f" 删除后 metadata = {row['metadata']}")
assert row["metadata"] is not None, "metadata 不应为空"
print(" ✓ metadata 仍保留")
# 5. 重新导入(无源代码)
print("\n[5/5] 重新导入模块(无源代码)...")
if "mymod" in sys.modules:
del sys.modules["mymod"]
mod2 = importlib.import_module("mymod")
result = mod2.hello()
print(f" ✓ 无源代码导入成功: hello() = {result}")
assert result == "ok", "模块功能异常"
print("\n" + "=" * 60)
print("全部通过!删除源代码后仍可正常加载 PyArmor 模块")
print("=" * 60)
except Exception as e:
print(f"\n❌ 测试失败: {e}")
import traceback
traceback.print_exc()
return 1
finally:
shutil.rmtree(work_dir, ignore_errors=True)
return 0
if __name__ == "__main__":
sys.exit(main())
+295
View File
@@ -0,0 +1,295 @@
"""
集成测试:测试 PyArmor 加密打包 -> 存储 -> 加载 -> 执行 全流程
"""
import sys
import os
import tempfile
import shutil
# 添加项目模块路径
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
TEST_DB = os.path.join(os.path.dirname(__file__), "test_pyarmor_integration.db")
def create_test_module():
"""创建测试用的 Python 模块"""
return """
def greet(name):
return f"Hello, {name}! This is a PyArmor protected module."
class Calculator:
def add(self, a, b):
return a + b
def multiply(self, a, b):
return a * b
VERSION = "1.0.0"
"""
def test_pyarmor_obfuscate():
"""测试 PyArmor 混淆功能"""
print("=" * 60)
print("测试 1: PyArmor 混淆功能")
print("=" * 60)
from module_bank.pyarmor_encryption import PyArmorEncryption
enc = PyArmorEncryption(obf_code=1, obf_mod=1)
# 检查 PyArmor 是否可用
if not enc.check_pyarmor_available():
print("ERROR: PyArmor 未安装或不可用")
return False
source = create_test_module()
print(f"原始代码长度: {len(source)} 字节")
try:
obfuscated_code, runtime_data = enc.obfuscate_source(
source, "test_protected_module"
)
print(f"混淆后代码长度: {len(obfuscated_code)} 字节")
print(f"运行时文件: {'有' if runtime_data else '无'}")
print(f"运行时数据大小: {len(runtime_data) if runtime_data else 0} 字节")
# 验证是否被保护
assert enc.is_pyarmor_protected(obfuscated_code), "代码应被标记为 PyArmor 保护"
print(f"PyArmor 保护标记: 正确")
# 提取运行时模块名
runtime_name = enc.extract_runtime_info(obfuscated_code)
print(f"运行时模块名: {runtime_name}")
print("测试 1 通过!\n")
return True
except Exception as e:
print(f"ERROR: {e}")
import traceback
traceback.print_exc()
return False
def test_pack_with_pyarmor():
"""测试使用 PyArmor 打包到 SQLite"""
print("=" * 60)
print("测试 2: 使用 PyArmor 打包到 SQLite")
print("=" * 60)
# 清理旧的测试数据库
if os.path.exists(TEST_DB):
os.remove(TEST_DB)
try:
from module_bank.python_to_sqlite import PythonToSQLite
# 使用 PyArmor 创建打包器
packer = PythonToSQLite(
TEST_DB,
pyarmor_obfuscate=True,
pyarmor_obf_code=1,
pyarmor_obf_mod=1,
)
source = create_test_module()
packer.importer.add_module("test_protected_module", source, is_package=False)
print("模块已打包到数据库")
# 验证模块已存储
modules = packer.list_modules()
module_names = [m["module_name"] for m in modules]
assert "test_protected_module" in module_names, "模块应在数据库中存在"
print(f"数据库中的模块: {module_names}")
# 验证运行时已存储
runtimes = packer.importer.list_runtimes()
print(f"数据库中的运行时: {[r['runtime_name'] for r in runtimes]}")
# 验证 pyarmor_protected 标记
cursor = packer.importer.connection.execute(
"SELECT pyarmor_protected FROM python_modules WHERE module_name = ?",
("test_protected_module",),
)
row = cursor.fetchone()
assert row["pyarmor_protected"] == 1, "模块应标记为 pyarmor_protected"
print("PyArmor 保护标记: 已正确设置")
print("测试 2 通过!\n")
return True
except Exception as e:
print(f"ERROR: {e}")
import traceback
traceback.print_exc()
return False
def test_import_pyarmor_module():
"""测试从 SQLite 导入并执行 PyArmor 保护的模块"""
print("=" * 60)
print("测试 3: 从 SQLite 导入并执行 PyArmor 保护模块")
print("=" * 60)
try:
from module_bank.python_to_sqlite import PythonToSQLite
from module_bank.sqlite_meta_path_finder import SQLiteMetaPathFinder
packer = PythonToSQLite(TEST_DB)
# 安装导入器
finder = packer.install_importer()
# 尝试导入模块
import importlib
mod = importlib.import_module("test_protected_module")
print(f"模块导入成功: {mod}")
print(f"模块名: {mod.__name__}")
print(f"模块文件: {mod.__file__}")
# 调用模块函数
result = mod.greet("World")
print(f"调用 greet('World'): {result}")
assert result == "Hello, World! This is a PyArmor protected module."
# 使用模块类
calc = mod.Calculator()
assert calc.add(3, 4) == 7
assert calc.multiply(3, 4) == 12
print(f"Calculator.add(3, 4) = {calc.add(3, 4)}")
print(f"Calculator.multiply(3, 4) = {calc.multiply(3, 4)}")
# 访问模块变量
print(f"VERSION = {mod.VERSION}")
assert mod.VERSION == "1.0.0"
print("测试 3 通过!\n")
# 清理
sys.meta_path.remove(finder)
if "test_protected_module" in sys.modules:
del sys.modules["test_protected_module"]
return True
except Exception as e:
print(f"ERROR: {e}")
import traceback
traceback.print_exc()
return False
def test_cli_with_pyarmor():
"""测试 CLI 的 PyArmor 参数"""
print("=" * 60)
print("测试 4: CLI --pyarmor 参数")
print("=" * 60)
try:
import tempfile
# 创建临时源文件
tmpdir = tempfile.mkdtemp()
src_file = os.path.join(tmpdir, "hello_module.py")
with open(src_file, "w", encoding="utf-8") as f:
f.write("def hello():\n return 'Hello from PyArmor CLI!'\n")
db_path = os.path.join(tmpdir, "test_cli.db")
# 测试解析 --pyarmor 参数
from module_bank.cli import main as cli_main
# 保存原始argv
old_argv = sys.argv
try:
sys.argv = [
"mbank",
"pack",
src_file,
"--db",
db_path,
"--name",
"hello_cli_module",
"--pyarmor",
]
cli_main()
print("CLI pack with --pyarmor 执行成功")
# 验证模块已用 PyArmor 保护
from module_bank.sqlite_module_importer import SQLiteModuleImporter
importer = SQLiteModuleImporter(db_path)
cursor = importer.connection.execute(
"SELECT pyarmor_protected FROM python_modules WHERE module_name = ?",
("hello_cli_module",),
)
row = cursor.fetchone()
assert row and row["pyarmor_protected"] == 1, "CLI 打包的模块应启用 PyArmor"
print("CLI 打包的模块已正确启用 PyArmor 保护")
# 验证运行时存储
runtimes = importer.list_runtimes()
assert len(runtimes) > 0, "CLI 打包后应有运行时文件存储在数据库中"
print(f"运行时已存储: {[r['runtime_name'] for r in runtimes]}")
finally:
sys.argv = old_argv
shutil.rmtree(tmpdir, ignore_errors=True)
print("测试 4 通过!\n")
return True
except Exception as e:
print(f"ERROR: {e}")
import traceback
traceback.print_exc()
return False
def cleanup():
"""清理测试文件"""
if os.path.exists(TEST_DB):
os.remove(TEST_DB)
print(f"已清理测试数据库: {TEST_DB}")
if __name__ == "__main__":
print("\n" + "=" * 60)
print("PyArmor 集成测试套件")
print("=" * 60 + "\n")
tests = [
("PyArmor 混淆功能", test_pyarmor_obfuscate),
("PyArmor 打包到 SQLite", test_pack_with_pyarmor),
("从 SQLite 导入 PyArmor 模块", test_import_pyarmor_module),
("CLI --pyarmor 参数", test_cli_with_pyarmor),
]
passed = 0
failed = 0
for name, func in tests:
print(f"\n>>> 运行测试: {name}")
try:
if func():
passed += 1
else:
failed += 1
except Exception as e:
print(f"测试异常: {e}")
import traceback
traceback.print_exc()
failed += 1
cleanup()
print("\n" + "=" * 60)
print(f"测试完成: {passed} 通过, {failed} 失败")
print("=" * 60)
sys.exit(1 if failed > 0 else 0)
@@ -0,0 +1,2 @@
# Pyarmor 9.2.4 (trial), 000000, 2026-05-04T17:52:33.319452
from .pyarmor_runtime import __pyarmor__
File diff suppressed because one or more lines are too long
+53
View File
@@ -0,0 +1,53 @@
"""测试 PyArmor 加密功能"""
import tempfile
import os
import sys
import shutil
# 确保能导入项目模块
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
def test_pyarmor_encrypt():
"""测试 PyArmor encrypt_script 功能"""
import pyarmor.pyarmor as pa
tmpdir = tempfile.mkdtemp()
try:
src = os.path.join(tmpdir, "test_mod.py")
with open(src, "w", encoding="utf-8") as f:
f.write('def hello():\n print("Hello from PyArmor!")\n')
dest = os.path.join(tmpdir, "dist", "test_mod.py")
os.makedirs(os.path.dirname(dest), exist_ok=True)
# Check capsule
capsule = pa.DEFAULT_CAPSULE
print(f"Capsule: {capsule}")
print(f"Capsule exists: {os.path.exists(capsule)}")
# Try encrypt
result = pa.encrypt_script(
pubkey=capsule,
filename=src,
destname=dest,
)
print(f"Result: {result}")
# Check output
if os.path.exists(dest):
with open(dest, "r", encoding="utf-8") as f:
content = f.read()
print(f"Output length: {len(content)} bytes")
print(f"First 300 chars:\n{content[:300]}")
print(f"\n--- Full content ({len(content)} bytes) ---")
print(content)
else:
print(f"ERROR: Output file not created at {dest}")
finally:
shutil.rmtree(tmpdir, ignore_errors=True)
if __name__ == "__main__":
test_pyarmor_encrypt()
+2
View File
@@ -2,10 +2,12 @@ from .python_to_sqlite import PythonToSQLite
from .sqlite_module_importer import SQLiteModuleImporter
from .sqlite_module_loader import SQLiteModuleLoader
from .sqlite_meta_path_finder import SQLiteMetaPathFinder
from .pyarmor_encryption import PyArmorEncryption
__all__ = [
"PythonToSQLite",
"SQLiteModuleImporter",
"SQLiteModuleLoader",
"SQLiteMetaPathFinder",
"PyArmorEncryption",
]
+44 -2
View File
@@ -11,6 +11,28 @@ def main():
pack_parser.add_argument("source", help="源文件或目录")
pack_parser.add_argument("--db", default="modules.db", help="数据库文件路径")
pack_parser.add_argument("--name", help="模块名(默认从文件名推断)")
pack_parser.add_argument(
"--key", help="加密密钥(Fernet格式,32字节base64),若不提供则不加密"
)
pack_parser.add_argument(
"--pyarmor",
action="store_true",
help="使用 PyArmor 混淆保护源代码",
)
pack_parser.add_argument(
"--pyarmor-obf-code",
type=int,
default=1,
choices=[0, 1],
help="PyArmor 代码混淆级别 (0=禁用, 1=启用, 默认: 1)",
)
pack_parser.add_argument(
"--pyarmor-obf-mod",
type=int,
default=1,
choices=[0, 1],
help="PyArmor 模块混淆级别 (0=禁用, 1=启用, 默认: 1)",
)
# 列出命令
list_parser = subparsers.add_parser("list", help="列出数据库中的模块")
@@ -28,13 +50,22 @@ def main():
# 安装命令
install_parser = subparsers.add_parser("install", help="安装SQLite导入器")
install_parser.add_argument("--db", default="modules.db", help="数据库文件路径")
install_parser.add_argument(
"--key", help="解密密钥(Fernet格式),如果数据库中有加密模块则必须提供"
)
args = parser.parse_args()
if args.command == "pack":
from .python_to_sqlite import PythonToSQLite
packer = PythonToSQLite(args.db)
packer = PythonToSQLite(
args.db,
key=args.key,
pyarmor_obfuscate=args.pyarmor,
pyarmor_obf_code=args.pyarmor_obf_code,
pyarmor_obf_mod=args.pyarmor_obf_mod,
)
if Path(args.source).is_dir():
packer.pack_directory(args.source)
@@ -43,6 +74,17 @@ def main():
packer.pack_module(args.source, args.name)
print(f"已打包模块: {args.source}")
# 如果启用了 PyArmor,显示运行时信息
if args.pyarmor:
from .sqlite_module_importer import SQLiteModuleImporter
importer = SQLiteModuleImporter(args.db)
runtimes = importer.list_runtimes()
if runtimes:
print(f"\nPyArmor 运行时信息:")
for rt in runtimes:
print(f" - {rt['runtime_name']} (创建于: {rt['created_at']})")
elif args.command == "list":
from .python_to_sqlite import PythonToSQLite
@@ -67,7 +109,7 @@ def main():
from .python_to_sqlite import PythonToSQLite
packer = PythonToSQLite(args.db)
packer.install_importer()
packer.install_importer(key=args.key)
print(f"已安装SQLite导入器,可以从数据库导入模块了!")
# 保持程序运行以便交互使用
+31
View File
@@ -0,0 +1,31 @@
from cryptography.fernet import Fernet
import base64
class Encryption:
def __init__(self, key=None):
if key is None:
self.cipher = None
else:
if isinstance(key, str):
key = key.encode("utf-8")
try:
self.cipher = Fernet(key)
except Exception as e:
raise ValueError(f"Invalid key: {e}") from e
def encrypt(self, data):
if self.cipher is None:
return data
if isinstance(data, str):
data = data.encode("utf-8")
return self.cipher.encrypt(data)
def decrypt(self, data):
if self.cipher is None:
return data
return self.cipher.decrypt(data)
@staticmethod
def generate_key():
return Fernet.generate_key().decode("utf-8")
+202
View File
@@ -0,0 +1,202 @@
"""
PyArmor 加密支持模块
为 module_bank 提供 PyArmor 代码混淆/加密能力。
使用 PyArmor CLI 对 Python 源代码进行混淆保护,
并将混淆后的源代码和运行时文件存储到 SQLite 数据库中。
"""
import os
import sys
import tempfile
import shutil
import subprocess
import re
from pathlib import Path
from typing import Optional, Tuple
class PyArmorEncryption:
"""PyArmor 加密处理器,用于混淆/保护 Python 源代码"""
def __init__(self, key: Optional[str] = None, obf_code: int = 1, obf_mod: int = 1):
"""
初始化 PyArmor 加密处理器
Args:
key: 加密密钥(可选,PyArmor 不使用此参数,保留接口兼容性)
obf_code: 代码混淆级别 (0/1)
obf_mod: 模块混淆级别 (0/1)
"""
self.key = key
self.obf_code = obf_code
self.obf_mod = obf_mod
def obfuscate_source(
self, source_code: str, module_name: str
) -> Tuple[str, Optional[dict]]:
"""
使用 PyArmor 混淆源代码
Args:
source_code: 原始 Python 源代码
module_name: 模块名
Returns:
(混淆后的源代码, PyArmor 运行时数据字典)
运行时数据字典包含: {
"runtime_name": "pyarmor_runtime_000000",
"pyd_data": bytes,
"init_data": str,
}
为 None 表示没有运行时文件
"""
if not self.check_pyarmor_available():
raise ImportError(
"PyArmor 未安装。请使用 'uv add pyarmor' 或 'pip install pyarmor' 安装。"
)
temp_dir = tempfile.mkdtemp(prefix=f"pyarmor_pack_")
try:
# 创建临时源文件
src_filename = f"{module_name.replace('.', '_')}.py"
src_file = os.path.join(temp_dir, src_filename)
with open(src_file, "w", encoding="utf-8") as f:
f.write(source_code)
# 输出目录
output_dir = os.path.join(temp_dir, "dist")
os.makedirs(output_dir, exist_ok=True)
# 调用 pyarmor gen 命令进行混淆
cmd = [
sys.executable,
"-m",
"pyarmor.cli",
"gen",
"-O",
output_dir,
"-i",
src_file,
]
# 添加混淆级别参数
if self.obf_code == 0:
cmd.extend(["--obf-code", "0"])
if self.obf_mod == 0:
cmd.extend(["--obf-mod", "0"])
result = subprocess.run(
cmd,
capture_output=True,
text=True,
cwd=temp_dir,
)
if result.returncode != 0:
raise RuntimeError(f"PyArmor 混淆失败:\n{result.stderr}")
# 读取混淆后的代码
obfuscated_file = os.path.join(output_dir, src_filename)
if not os.path.exists(obfuscated_file):
raise RuntimeError(f"PyArmor 未生成输出文件: {obfuscated_file}")
with open(obfuscated_file, "r", encoding="utf-8") as f:
obfuscated_code = f.read()
# 查找并读取运行时文件
runtime_data = self._find_and_read_runtime(output_dir)
return obfuscated_code, runtime_data
except Exception as e:
if isinstance(e, RuntimeError):
raise
raise RuntimeError(f"PyArmor 混淆失败 ({module_name}): {e}") from e
finally:
shutil.rmtree(temp_dir, ignore_errors=True)
def _find_and_read_runtime(self, output_dir: str) -> Optional[dict]:
"""
查找并读取 PyArmor 运行时文件
Args:
output_dir: PyArmor 输出目录
Returns:
包含运行时文件的字典: {
"runtime_name": "pyarmor_runtime_000000",
"pyd_data": bytes, # 原生二进制文件
"init_data": str, # __init__.py 内容
}
如果没有找到则返回 None
"""
output_path = Path(output_dir)
# 查找 pyarmor_runtime_* 目录
runtime_dirs = list(output_path.glob("pyarmor_runtime_*"))
if not runtime_dirs:
return None
runtime_dir = runtime_dirs[0]
runtime_name = runtime_dir.name
result = {"runtime_name": runtime_name, "pyd_data": None, "init_data": None}
# 查找 .pyd 或 .so 文件
runtime_files = list(runtime_dir.glob("pyarmor_runtime.*"))
if runtime_files:
with open(runtime_files[0], "rb") as f:
result["pyd_data"] = f.read()
# 读取 __init__.py
init_file = runtime_dir / "__init__.py"
if init_file.exists():
with open(init_file, "r", encoding="utf-8") as f:
result["init_data"] = f.read()
return result
@staticmethod
def extract_runtime_info(obfuscated_code: str) -> Optional[str]:
"""
从混淆后的代码中提取运行时模块名
Args:
obfuscated_code: 混淆后的源代码
Returns:
运行时模块名,如 'pyarmor_runtime_000000'
"""
match = re.search(
r"from\s+(pyarmor_runtime_\w+)\s+import\s+__pyarmor__",
obfuscated_code,
)
return match.group(1) if match else None
@staticmethod
def is_pyarmor_protected(source_code: str) -> bool:
"""
检测源代码是否已被 PyArmor 保护
Args:
source_code: 要检测的源代码
Returns:
是否为 PyArmor 保护的代码
"""
markers = [
"pyarmor_runtime",
"__pyarmor__",
"from pyarmor_runtime",
]
return any(marker in source_code for marker in markers)
@staticmethod
def check_pyarmor_available() -> bool:
"""检查 PyArmor 是否可用"""
try:
import pyarmor # noqa: F401
return True
except ImportError:
return False
+51 -10
View File
@@ -1,6 +1,6 @@
import sys
from pathlib import Path
from typing import List
from typing import List, Optional
from .sqlite_module_importer import SQLiteModuleImporter
from .sqlite_meta_path_finder import SQLiteMetaPathFinder
@@ -8,18 +8,37 @@ from .sqlite_meta_path_finder import SQLiteMetaPathFinder
class PythonToSQLite:
"""Python到SQLite的打包工具"""
def __init__(self, db_path: str = "python_modules.db"):
def __init__(
self,
db_path: str = "python_modules.db",
key: Optional[str] = None,
pyarmor_obfuscate: bool = False,
pyarmor_obf_code: int = 1,
pyarmor_obf_mod: int = 1,
):
self.db_path = db_path
self.importer = SQLiteModuleImporter(db_path)
self.key = key
self.pyarmor_obfuscate = pyarmor_obfuscate
self.pyarmor_obf_code = pyarmor_obf_code
self.pyarmor_obf_mod = pyarmor_obf_mod
self.importer = SQLiteModuleImporter(
db_path,
key,
pyarmor_obfuscate=pyarmor_obfuscate,
pyarmor_obf_code=pyarmor_obf_code,
pyarmor_obf_mod=pyarmor_obf_mod,
)
def pack_directory(
self,
directory_path: str,
include_patterns: List[str] = None,
exclude_patterns: List[str] = None,
package_name: str | None = None,
include_patterns: List[str] | None = None,
exclude_patterns: List[str] | None = None,
):
"""打包整个目录 - 修复版"""
directory = Path(directory_path)
if package_name is None:
package_name = directory.name # 包名
if include_patterns is None:
@@ -66,9 +85,12 @@ class PythonToSQLite:
is_package=False,
)
def pack_module(self, module_path: str, module_name: str = None):
def pack_module(self, module_path_str: str, module_name: str | None = None):
"""打包单个模块"""
module_path = Path(module_path)
if module_name is None:
raise ValueError("module_name 不能为空")
module_path = Path(module_path_str)
if module_name is None:
module_name = module_path.stem
@@ -79,9 +101,10 @@ class PythonToSQLite:
print(f"打包独立模块: {module_name} (来自: {module_path})")
self.importer.add_module(module_name, source_code, is_package=False)
def install_importer(self):
def install_importer(self, key: Optional[str] = None):
"""安装导入器到sys.meta_path"""
finder = SQLiteMetaPathFinder(self.db_path)
effective_key = key if key is not None else self.key
finder = SQLiteMetaPathFinder(self.db_path, effective_key)
# 添加到meta_path开头,优先于文件系统查找
sys.meta_path.insert(0, finder)
return finder
@@ -89,7 +112,7 @@ class PythonToSQLite:
def list_modules(self):
"""列出数据库中的所有模块"""
cursor = self.importer.connection.execute(
"SELECT module_name, is_package FROM python_modules ORDER BY module_name"
"SELECT module_name, is_package FROM python_modules WHERE module_name != 'key' ORDER BY module_name"
)
return [dict(row) for row in cursor.fetchall()]
@@ -131,3 +154,21 @@ class PythonToSQLite:
print(
f"警告:模块 {module_name} 的父包 {parent_package} 不是一个包"
)
def packer_key(self, key):
self.importer.connection.execute(
"""
INSERT OR REPLACE INTO python_modules
(module_name, source_code, bytecode, is_package, metadata, encrypted)
VALUES (?, ?, ?, ?, ?, ?)
""",
("key", None, key, None, None, None),
)
self.importer.connection.commit()
def get_key(self):
cursor = self.importer.connection.execute("""
SELECT bytecode FROM python_modules WHERE module_name = 'key'
""")
row = cursor.fetchone()
return row["bytecode"] if row else None
+6 -3
View File
@@ -8,8 +8,9 @@ from .sqlite_module_loader import SQLiteModuleLoader
class SQLiteMetaPathFinder(importlib.abc.MetaPathFinder):
"""SQLite元路径查找器"""
def __init__(self, db_path: str):
def __init__(self, db_path: str, key: str | None = None):
self.db_path = db_path
self.key = key
self.connection = sqlite3.connect(db_path)
self.connection.row_factory = sqlite3.Row
# 缓存模块查找结果
@@ -31,7 +32,7 @@ class SQLiteMetaPathFinder(importlib.abc.MetaPathFinder):
# 创建模块规范
spec = importlib.util.spec_from_loader(
fullname,
SQLiteModuleLoader(self.connection, fullname, self.db_path),
SQLiteModuleLoader(self.connection, fullname, self.db_path, self.key),
origin=f"sqlite://{self.db_path}",
is_package=self.module_cache[fullname],
)
@@ -44,7 +45,9 @@ class SQLiteMetaPathFinder(importlib.abc.MetaPathFinder):
if fullname in self.module_cache:
spec = importlib.util.spec_from_loader(
fullname,
SQLiteModuleLoader(self.connection, fullname, self.db_path),
SQLiteModuleLoader(
self.connection, fullname, self.db_path, self.key
),
origin=f"sqlite://{self.db_path}",
is_package=self.module_cache[fullname],
)
+142 -13
View File
@@ -1,22 +1,37 @@
import sqlite3
import marshal
import base64
import os
import json
from pathlib import Path
from typing import Dict
from typing import Dict, Optional, Tuple
from .encryption import Encryption
class SQLiteModuleImporter:
"""从SQLite数据库导入Python模块的导入器"""
def __init__(self, db_path: str):
def __init__(
self,
db_path: str,
key: Optional[str] = None,
pyarmor_obfuscate: bool = False,
pyarmor_obf_code: int = 1,
pyarmor_obf_mod: int = 1,
):
self.db_path = db_path
self.connection = sqlite3.connect(db_path)
self.connection.row_factory = sqlite3.Row
self.encryption = Encryption(key)
self.pyarmor_obfuscate = pyarmor_obfuscate
self.pyarmor_obf_code = pyarmor_obf_code
self.pyarmor_obf_mod = pyarmor_obf_mod
self._pyarmor_enc = None
self._create_tables()
def _create_tables(self):
"""创建存储模块的数据库表"""
self.connection.execute(
"""
self.connection.execute("""
CREATE TABLE IF NOT EXISTS python_modules (
module_name TEXT PRIMARY KEY,
source_code TEXT,
@@ -25,37 +40,151 @@ class SQLiteModuleImporter:
metadata TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
"""
""")
try:
self.connection.execute(
"ALTER TABLE python_modules ADD COLUMN encrypted INTEGER DEFAULT 0"
)
except sqlite3.OperationalError:
pass
# 添加 pyarmor_protected 列(用于标记 PyArmor 混淆过的模块)
try:
self.connection.execute(
"ALTER TABLE python_modules ADD COLUMN pyarmor_protected INTEGER DEFAULT 0"
)
except sqlite3.OperationalError:
pass
# 创建 PyArmor 运行时存储表(支持二进制文件和文本文件的存储)
self.connection.execute("""
CREATE TABLE IF NOT EXISTS pyarmor_runtime (
id INTEGER PRIMARY KEY AUTOINCREMENT,
runtime_name TEXT UNIQUE NOT NULL,
pyd_data BLOB,
init_data TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
self.connection.commit()
def _get_pyarmor_enc(self):
"""惰性获取 PyArmor 加密器"""
if self._pyarmor_enc is None:
from .pyarmor_encryption import PyArmorEncryption
self._pyarmor_enc = PyArmorEncryption(
key=None,
obf_code=self.pyarmor_obf_code,
obf_mod=self.pyarmor_obf_mod,
)
return self._pyarmor_enc
def add_module(
self,
module_name: str,
source_code: str,
is_package: bool = False,
metadata: Dict = None,
metadata: Dict | None = None,
):
"""添加模块到数据库"""
pyarmor_protected = 0
# 如果启用了 PyArmor 混淆,则先混淆源代码
if self.pyarmor_obfuscate:
pyarmor_enc = self._get_pyarmor_enc()
if pyarmor_enc.check_pyarmor_available():
obfuscated_code, runtime_dict = pyarmor_enc.obfuscate_source(
source_code, module_name
)
source_code = obfuscated_code
pyarmor_protected = 1
# 存储运行时文件
runtime_name = None
if runtime_dict is not None:
runtime_name = runtime_dict.get("runtime_name")
self._store_runtime(runtime_dict)
# 将运行时名存入 metadata,以便在删除源代码后仍能定位运行时
if runtime_name and metadata is None:
metadata = {"pyarmor_runtime": runtime_name}
elif runtime_name and isinstance(metadata, dict):
metadata["pyarmor_runtime"] = runtime_name
else:
print(f"警告: PyArmor 未安装,跳过模块 '{module_name}' 的 PyArmor 混淆")
# 编译为字节码
bytecode = compile(source_code, f"<sqlite_module:{module_name}>", "exec")
bytecode_blob = marshal.dumps(bytecode)
metadata_str = str(metadata) if metadata else "{}"
encrypted_flag = 0
# 如果加密器已设置,则加密字节码
if self.encryption.cipher is not None:
bytecode_blob = self.encryption.encrypt(bytecode_blob)
encrypted_flag = 1
self.connection.execute(
"""
INSERT OR REPLACE INTO python_modules
(module_name, source_code, bytecode, is_package, metadata)
VALUES (?, ?, ?, ?, ?)
(module_name, source_code, bytecode, is_package, metadata, encrypted, pyarmor_protected)
VALUES (?, ?, ?, ?, ?, ?, ?)
""",
(module_name, source_code, bytecode_blob, is_package, metadata_str),
(
module_name,
source_code,
bytecode_blob,
is_package,
metadata_str,
encrypted_flag,
pyarmor_protected,
),
)
self.connection.commit()
def add_module_from_file(self, file_path: str, module_name: str = None):
def _store_runtime(self, runtime_dict: dict):
"""存储 PyArmor 运行时文件到数据库"""
self.connection.execute(
"""
INSERT OR REPLACE INTO pyarmor_runtime
(runtime_name, pyd_data, init_data)
VALUES (?, ?, ?)
""",
(
runtime_dict.get("runtime_name"),
runtime_dict.get("pyd_data"),
runtime_dict.get("init_data"),
),
)
self.connection.commit()
def get_runtime(self, runtime_name: str) -> Optional[dict]:
"""从数据库获取 PyArmor 运行时文件数据"""
cursor = self.connection.execute(
"SELECT runtime_name, pyd_data, init_data FROM pyarmor_runtime WHERE runtime_name = ?",
(runtime_name,),
)
row = cursor.fetchone()
if row:
return {
"runtime_name": row["runtime_name"],
"pyd_data": row["pyd_data"],
"init_data": row["init_data"],
}
return None
def list_runtimes(self):
"""列出数据库中的所有 PyArmor 运行时"""
cursor = self.connection.execute(
"SELECT runtime_name, created_at FROM pyarmor_runtime ORDER BY id"
)
return [dict(row) for row in cursor.fetchall()]
def add_module_from_file(self, file_path_str: str, module_name: str | None = None):
"""从文件添加模块"""
file_path = Path(file_path)
file_path = Path(file_path_str)
if module_name is None:
# 从文件名推导模块名
@@ -66,9 +195,9 @@ class SQLiteModuleImporter:
self.add_module(module_name, source_code)
def add_package(self, package_path: str, package_name: str = None):
def add_package(self, package_path_str: str, package_name: str | None = None):
"""添加整个包到数据库"""
package_path = Path(package_path)
package_path = Path(package_path_str)
if package_name is None:
package_name = package_path.name
+190 -4
View File
@@ -3,18 +3,31 @@ import importlib
import importlib.abc
import marshal
import types
import os
import sys
import tempfile
import shutil
import json
from pathlib import Path
from .encryption import Encryption
class SQLiteModuleLoader(importlib.abc.Loader):
"""SQLite模块加载器"""
def __init__(
self, db_connection: sqlite3.Connection, module_name: str, db_path: str = None
self,
db_connection: sqlite3.Connection,
module_name: str,
db_path: str | None = None,
key: str | None = None,
):
self.db_connection = db_connection
self.module_name = module_name
self.db_path = db_path
self.db_connection.row_factory = sqlite3.Row
self.encryption = Encryption(key)
self._runtime_temp_dir = None
def create_module(self, spec):
"""创建模块对象"""
@@ -35,10 +48,10 @@ class SQLiteModuleLoader(importlib.abc.Loader):
# 创建普通模块
return None # 使用默认创建方式
def exec_module(self, module):
def exec_module(self, module): # type: ignore
"""执行模块代码"""
cursor = self.db_connection.execute(
"SELECT bytecode, source_code, is_package FROM python_modules WHERE module_name = ?",
"SELECT bytecode, is_package, encrypted, pyarmor_protected, source_code, metadata FROM python_modules WHERE module_name = ?",
(self.module_name,),
)
row = cursor.fetchone()
@@ -46,8 +59,27 @@ class SQLiteModuleLoader(importlib.abc.Loader):
if not row:
raise ImportError(f"No module named '{self.module_name}' in database")
pyarmor_protected = row["pyarmor_protected"]
# 如果是 PyArmor 保护的模块,需要先部署运行时再执行
if pyarmor_protected:
return self._exec_pyarmor_module(module, row)
# 从字节码加载
bytecode = marshal.loads(row["bytecode"])
bytecode_blob = row["bytecode"]
encrypted = row["encrypted"]
# 如果字节码是加密的,需要解密
if encrypted:
if self.encryption.cipher is None:
raise ImportError(
f"Module '{self.module_name}' has encrypted bytecode but no decryption key provided. "
"Please set the key when installing the importer."
)
bytecode_blob = self.encryption.decrypt(bytecode_blob)
# 从字节码加载
bytecode = marshal.loads(bytecode_blob)
# 在模块的命名空间中执行字节码
exec(bytecode, module.__dict__)
@@ -61,3 +93,157 @@ class SQLiteModuleLoader(importlib.abc.Loader):
module.__file__ = f"sqlite://{self.db_path}/{self.module_name}"
return module
def _deploy_runtime(self, runtime_name: str) -> str | None:
"""
部署 PyArmor 运行时到临时目录
Args:
runtime_name: 运行时模块名,如 'pyarmor_runtime_000000'
Returns:
临时目录路径,如果部署失败则返回 None
"""
if self.db_path is None:
return None
try:
from .sqlite_module_importer import SQLiteModuleImporter
importer = SQLiteModuleImporter(self.db_path)
runtime_dict = importer.get_runtime(runtime_name)
except Exception:
return None
if not runtime_dict:
return None
# 创建临时目录来存放运行时包
temp_dir = tempfile.mkdtemp(prefix="pyarmor_runtime_")
runtime_pkg_dir = os.path.join(temp_dir, runtime_name)
os.makedirs(runtime_pkg_dir, exist_ok=True)
# 确定运行时二进制文件名(.pyd for Windows, .so for Linux/Mac)
pyd_data = runtime_dict.get("pyd_data")
if pyd_data:
if sys.platform == "win32":
so_filename = "pyarmor_runtime.pyd"
elif sys.platform == "darwin":
so_filename = "pyarmor_runtime.dylib"
else:
so_filename = "pyarmor_runtime.so"
so_path = os.path.join(runtime_pkg_dir, so_filename)
with open(so_path, "wb") as f:
f.write(pyd_data)
# 写入 __init__.py
init_data = runtime_dict.get("init_data")
if init_data:
init_path = os.path.join(runtime_pkg_dir, "__init__.py")
with open(init_path, "w", encoding="utf-8") as f:
f.write(init_data)
# 将临时目录添加到 sys.path,使运行时包可被导入
if temp_dir not in sys.path:
sys.path.insert(0, temp_dir)
return temp_dir
def _extract_runtime_name_from_source(self, source_code: str) -> str | None:
"""从混淆代码中提取运行时模块名"""
import re
match = re.search(
r"from\s+(pyarmor_runtime_\w+)\s+import\s+__pyarmor__",
source_code,
)
return match.group(1) if match else None
def _extract_runtime_name_from_metadata(
self, metadata_str: str | None
) -> str | None:
"""从 metadata JSON 中提取运行时模块名"""
if not metadata_str:
return None
try:
metadata = json.loads(metadata_str)
return metadata.get("pyarmor_runtime")
except (json.JSONDecodeError, TypeError):
return None
def _exec_pyarmor_module(self, module, row):
"""
执行 PyArmor 保护的模块
PyArmor 混淆后的代码需要在运行时导入 pyarmor_runtime_xxx 包。
该包包含 __init__.py 和原生 .pyd/.so 文件。
支持两种模式:
1. 有源代码:从源代码编译执行(标准模式)
2. 无源代码:从已编译的字节码执行(delete_source 后的模式)
"""
source_code = row["source_code"]
bytecode_blob = row["bytecode"]
encrypted = row["encrypted"]
metadata_str = row["metadata"]
# 确定运行时名:优先从源代码提取,其次从 metadata
runtime_name = None
if source_code:
runtime_name = self._extract_runtime_name_from_source(source_code)
if not runtime_name:
runtime_name = self._extract_runtime_name_from_metadata(metadata_str)
# 部署运行时
if runtime_name:
self._runtime_temp_dir = self._deploy_runtime(runtime_name)
if self._runtime_temp_dir:
print(f" 已部署 PyArmor 运行时: {runtime_name}")
# 设置 __file__ 属性(PyArmor 的 __pyarmor__() 需要它)
module_file = f"sqlite://{self.db_path}/{self.module_name}"
module.__file__ = module_file
# 如果是包,设置__path__和__package__属性
if row["is_package"]:
module.__path__ = []
module.__package__ = self.module_name
# 执行代码
if source_code:
# 模式1:有源代码 - 编译后执行
bytecode = compile(
source_code, f"<sqlite_module:{self.module_name}>", "exec"
)
exec(bytecode, module.__dict__)
else:
# 模式2:无源代码 - 从已存储的字节码执行
if not bytecode_blob:
raise ImportError(
f"PyArmor protected module '{self.module_name}' has no source code or bytecode available"
)
# 如果字节码是加密的,需要解密
if encrypted:
if self.encryption.cipher is None:
raise ImportError(
f"Module '{self.module_name}' has encrypted bytecode but no decryption key provided."
)
bytecode_blob = self.encryption.decrypt(bytecode_blob)
bytecode = marshal.loads(bytecode_blob)
exec(bytecode, module.__dict__)
return module
def cleanup_runtime(self):
"""清理 PyArmor 运行时临时文件"""
if self._runtime_temp_dir and os.path.exists(self._runtime_temp_dir):
try:
if self._runtime_temp_dir in sys.path:
sys.path.remove(self._runtime_temp_dir)
shutil.rmtree(self._runtime_temp_dir, ignore_errors=True)
except Exception:
pass
self._runtime_temp_dir = None
Generated
+1586
View File
File diff suppressed because it is too large Load Diff