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 output.md
log/ log/
*.mb *.mb
*.mbank
dual_protection_
*.db
+1 -1
View File
@@ -1 +1 @@
3.13 3.12
+211 -70
View File
@@ -1,15 +1,16 @@
# Python Module Bank - SQLite 模块打包系统 # Python Module Bank - SQLite 模块打包系统
一个将Python模块打包到SQLite数据库,并支持从数据库直接导入模块的工具系统。 一个将 Python 模块打包到 SQLite 数据库,并支持从数据库直接导入模块的工具系统。支持 **AES 字节码加密** 和 **PyArmor 代码混淆** 双重保护机制。
## 🌟 特性 ## 🌟 特性
- **单文件分发** - 将所有模块打包到单个SQLite数据库文件 - **单文件分发** - 将所有模块打包到单个 SQLite 数据库文件
- **源代码保护** - 模块以编译后的字节码形式存储 - **AES 字节码加密** - 编译后的字节码使用 Fernet (AES-128-CBC) 加密存储
- **动态导入** - 运行时直接从数据库加载模块,无需文件系统 - **PyArmor 代码混淆** - 支持使用 PyArmor 对 Python 源代码进行混淆保护
- **双层保护** - 可同时使用 PyArmor 混淆 + AES 字节码加密,提供双重安全保障
- **动态导入** - 运行时直接从数据库加载模块,无缝集成 Python 导入系统
- **完整包支持** - 支持包结构和子模块导入 - **完整包支持** - 支持包结构和子模块导入
- **CLI工具** - 提供完整的命令行接口 - **CLI 工具** - 提供完整的命令行接口
- **导入钩子** - 无缝集成Python导入系统
## 📦 安装 ## 📦 安装
@@ -21,63 +22,154 @@ cd module-bank
pip install -e . 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. 创建示例模块 ### 1. 创建示例模块
```python ```python
# my_module.py # my_package/__init__.py
def hello(): def hello():
print("Hello from my_module!") print("Hello from my_package!")
return "success" return "success"
VERSION = "1.0.0"
```
```python
# my_package/utils.py
def add(a, b):
return a + b
``` ```
### 2. 打包模块到数据库 ### 2. 打包模块到数据库
```python #### 使用 AES 加密(推荐)
# pack_example.py
from module_bank import PythonToSQLite
packer = PythonToSQLite("my_modules.db") ```bash
packer.pack_module("my_module.py", "my_module") # 生成密钥并打包
packer.pack_directory("my_package/") 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. 从数据库导入 ### 3. 从数据库导入
```python ```python
from module_bank import PythonToSQLite # import_example.py
from module_bank.python_to_sqlite import PythonToSQLite
# 安装导入器 packer = PythonToSQLite("my_package.mbank")
packer = PythonToSQLite("my_modules.db") finder = packer.install_importer(key="your-encryption-key") # 如果加密了需要提供密钥
finder = packer.install_importer()
# 现在可以从数据库导入模块了! from my_package import utils
import my_module from my_package import hello
import my_package.package_module
my_module.hello() print(hello())
my_package.package_module.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 ```bash
# 打包模块或目录 # 打包单个模块
mb pack my_package --db modules.db 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 ```python
from module_bank import PythonToSQLite 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") packer.pack_module("module.py", "module_name")
@@ -103,21 +208,25 @@ packer.verify_package_structure()
```python ```python
from module_bank import PythonToSQLite from module_bank import PythonToSQLite
import sys
packer = PythonToSQLite("modules.db") # 如果数据库中的模块被 AES 加密了,需要提供密钥
packer = PythonToSQLite("modules.mbank")
# 安装导入器到sys.meta_path # 安装导入器到 sys.meta_path
finder = packer.install_importer() finder = packer.install_importer(key="your-key")
# 列出所有可用模块 # 列出所有可用模块
modules = packer.list_modules() modules = packer.list_modules()
for module in 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
import my_package.submodule import my_package.submodule
print(my_package)
my_package.hello()
``` ```
## 🏗️ 架构设计 ## 🏗️ 架构设计
@@ -126,35 +235,49 @@ import my_package.submodule
```python ```python
src/module_bank/ src/module_bank/
├── python_to_sqlite.py # 主打包类 ├── python_to_sqlite.py # 主打包类
├── sqlite_module_importer.py # 数据库存储管理器 ├── sqlite_module_importer.py # 数据库存储管理器
├── sqlite_meta_path_finder.py # 元路径查找器 ├── sqlite_meta_path_finder.py # 元路径查找器
├── sqlite_module_loader.py # 模块加载器 ├── sqlite_module_loader.py # 模块加载器
├── cli.py # 命令行接口 ├── encryption.py # AES 密钥管理
└── __init__.py # 模块导出 ├── pyarmor_encryption.py # PyArmor 混淆封装
├── cli.py # 命令行接口
└── __init__.py # 模块导出
``` ```
### 数据流 ### 数据流
```text ```text
1. 打包阶段: 1. 打包阶段:
.py文件 → 编译为字节码 → 存储到SQLite数据库 .py 文件 → (可选: PyArmor 混淆) → 编译为字节码 → (可选: AES 加密) → 存储到 SQLite
2. 导入阶段: 2. 导入阶段:
导入请求 → MetaPathFinder查找 → ModuleLoader加载 → 执行模块 导入请求 → MetaPathFinder 查找 → ModuleLoader 加载 → (AES 解密) → (PyArmor 运行时部署) → 执行模块
``` ```
### 数据库模式 ### 数据库模式
```sql ```sql
-- 模块表
CREATE TABLE python_modules ( CREATE TABLE python_modules (
module_name TEXT PRIMARY KEY, module_name TEXT PRIMARY KEY,
source_code TEXT, -- 源代码(可选) source_code TEXT, -- 源代码(PyArmor 混淆后)
bytecode BLOB, -- 编译后的字节码 bytecode BLOB, -- 编译后的字节码(可选 AES 加密)
is_package BOOLEAN, -- 是否是包 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 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( packer.pack_directory(
"my_project/", "my_project/",
exclude_patterns=["*_test.py", "*.pyc", "__pycache__"] exclude_patterns=["*_test.py", "*.pyc", "__pycache__"],
) )
``` ```
@@ -177,42 +300,56 @@ packer.importer.add_module(
"my_module", "my_module",
source_code, source_code,
is_package=False, is_package=False,
metadata={"version": "1.0", "author": "me"} metadata={"version": "1.0", "author": "me"},
) )
``` ```
### 混合导入 ### 混合导入
```python ```python
# 可以同时使用文件系统和数据库导入 # 数据库导入器默认插入到 meta_path 开头,优先级高于文件系统
# 数据库导入器优先级更高
import sys import sys
from module_bank import PythonToSQLite from module_bank import PythonToSQLite
packer = PythonToSQLite("modules.db") packer = PythonToSQLite("modules.db")
finder = packer.install_importer() # 插入到meta_path开头 finder = packer.install_importer()
# 如果需要文件系统优先,可以调整插入位置 # 如果需要文件系统优先,将查找器追加到末尾
sys.meta_path.remove(finder)
sys.meta_path.append(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版本兼容 - 字节码不跨 Python 版本兼容(3.12 编译的模块无法在 3.11 加载)
- 不支持C扩展模块 - 不支持 C 扩展模块
- 不支持需要文件系统资源的模块(如__file__依赖) - PyArmor 运行时文件与操作系统相关(Windows 用 `.pyd`,Linux 用 `.so`)
- 运行时文件需在目标系统上重新打包
### 性能 ### 性能
- **启动时**:有一次性数据库查询和反序列化开销 - **启动时**:有一次性数据库查询和反序列化/运行时部署开销
- **运行时**:与传统导入性能相同(使用sys.modules缓存) - **运行时**:与传统导入性能相同(使用 sys.modules 缓存)
- **最佳适用**:长期运行的服务、桌面应用 - **最佳适用**:长期运行的服务、桌面应用
### 更新模块 ### 更新模块
@@ -233,19 +370,23 @@ DELETE FROM python_modules WHERE module_name = 'module_to_remove';
```bash ```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. 商业软件分发 - 保护源代码知识产权 1. **商业软件分发** - PyArmor 混淆保护源代码知识产权
2. 插件系统 - 动态加载数据库中的插件模块 2. **安全敏感环境** - AES 加密确保即使数据库泄露也无法还原代码
3. 教育平台 - 安全分发练习代码 3. **插件系统** - 动态加载数据库中的加密/混淆插件模块
4. 微服务 - 打包多个服务模块到单个文件 4. **教育平台** - 安全分发练习代码,防止学生抄袭
5. 嵌入式系统 - 减少文件系统依赖 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] [project]
name = "module_bank" dependencies = [
version = "0.1.0" "cryptography>=45.0.7",
"pyarmor>=9.2.4",
]
description = "Add your description here" description = "Add your description here"
name = "module_bank"
readme = "README.md" readme = "README.md"
requires-python = ">=3.7" requires-python = ">=3.7"
dependencies = [] version = "0.3.1"
[[tool.uv.index]] [[tool.uv.index]]
default = true default = true
@@ -14,8 +17,13 @@ url = "https://pypi.tuna.tsinghua.edu.cn/simple"
build-backend = "hatchling.build" build-backend = "hatchling.build"
requires = ["hatchling"] requires = ["hatchling"]
[dependency-groups]
dev = [
"twine>=4.0.2",
]
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["src/module_bank"] packages = ["src/module_bank"]
[project.scripts] [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_importer import SQLiteModuleImporter
from .sqlite_module_loader import SQLiteModuleLoader from .sqlite_module_loader import SQLiteModuleLoader
from .sqlite_meta_path_finder import SQLiteMetaPathFinder from .sqlite_meta_path_finder import SQLiteMetaPathFinder
from .pyarmor_encryption import PyArmorEncryption
__all__ = [ __all__ = [
"PythonToSQLite", "PythonToSQLite",
"SQLiteModuleImporter", "SQLiteModuleImporter",
"SQLiteModuleLoader", "SQLiteModuleLoader",
"SQLiteMetaPathFinder", "SQLiteMetaPathFinder",
"PyArmorEncryption",
] ]
+44 -2
View File
@@ -11,6 +11,28 @@ def main():
pack_parser.add_argument("source", help="源文件或目录") pack_parser.add_argument("source", help="源文件或目录")
pack_parser.add_argument("--db", default="modules.db", help="数据库文件路径") pack_parser.add_argument("--db", default="modules.db", help="数据库文件路径")
pack_parser.add_argument("--name", 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="列出数据库中的模块") list_parser = subparsers.add_parser("list", help="列出数据库中的模块")
@@ -28,13 +50,22 @@ def main():
# 安装命令 # 安装命令
install_parser = subparsers.add_parser("install", help="安装SQLite导入器") install_parser = subparsers.add_parser("install", help="安装SQLite导入器")
install_parser.add_argument("--db", default="modules.db", help="数据库文件路径") install_parser.add_argument("--db", default="modules.db", help="数据库文件路径")
install_parser.add_argument(
"--key", help="解密密钥(Fernet格式),如果数据库中有加密模块则必须提供"
)
args = parser.parse_args() args = parser.parse_args()
if args.command == "pack": if args.command == "pack":
from .python_to_sqlite import PythonToSQLite 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(): if Path(args.source).is_dir():
packer.pack_directory(args.source) packer.pack_directory(args.source)
@@ -43,6 +74,17 @@ def main():
packer.pack_module(args.source, args.name) packer.pack_module(args.source, args.name)
print(f"已打包模块: {args.source}") 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": elif args.command == "list":
from .python_to_sqlite import PythonToSQLite from .python_to_sqlite import PythonToSQLite
@@ -67,7 +109,7 @@ def main():
from .python_to_sqlite import PythonToSQLite from .python_to_sqlite import PythonToSQLite
packer = PythonToSQLite(args.db) packer = PythonToSQLite(args.db)
packer.install_importer() packer.install_importer(key=args.key)
print(f"已安装SQLite导入器,可以从数据库导入模块了!") 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
+52 -11
View File
@@ -1,6 +1,6 @@
import sys import sys
from pathlib import Path from pathlib import Path
from typing import List from typing import List, Optional
from .sqlite_module_importer import SQLiteModuleImporter from .sqlite_module_importer import SQLiteModuleImporter
from .sqlite_meta_path_finder import SQLiteMetaPathFinder from .sqlite_meta_path_finder import SQLiteMetaPathFinder
@@ -8,19 +8,38 @@ from .sqlite_meta_path_finder import SQLiteMetaPathFinder
class PythonToSQLite: class PythonToSQLite:
"""Python到SQLite的打包工具""" """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.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( def pack_directory(
self, self,
directory_path: str, directory_path: str,
include_patterns: List[str] = None, package_name: str | None = None,
exclude_patterns: List[str] = None, include_patterns: List[str] | None = None,
exclude_patterns: List[str] | None = None,
): ):
"""打包整个目录 - 修复版""" """打包整个目录 - 修复版"""
directory = Path(directory_path) directory = Path(directory_path)
package_name = directory.name # 包名 if package_name is None:
package_name = directory.name # 包名
if include_patterns is None: if include_patterns is None:
include_patterns = ["*.py"] include_patterns = ["*.py"]
@@ -66,9 +85,12 @@ class PythonToSQLite:
is_package=False, 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: if module_name is None:
module_name = module_path.stem module_name = module_path.stem
@@ -79,9 +101,10 @@ class PythonToSQLite:
print(f"打包独立模块: {module_name} (来自: {module_path})") print(f"打包独立模块: {module_name} (来自: {module_path})")
self.importer.add_module(module_name, source_code, is_package=False) 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""" """安装导入器到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开头,优先于文件系统查找 # 添加到meta_path开头,优先于文件系统查找
sys.meta_path.insert(0, finder) sys.meta_path.insert(0, finder)
return finder return finder
@@ -89,7 +112,7 @@ class PythonToSQLite:
def list_modules(self): def list_modules(self):
"""列出数据库中的所有模块""" """列出数据库中的所有模块"""
cursor = self.importer.connection.execute( 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()] return [dict(row) for row in cursor.fetchall()]
@@ -131,3 +154,21 @@ class PythonToSQLite:
print( print(
f"警告:模块 {module_name} 的父包 {parent_package} 不是一个包" 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): class SQLiteMetaPathFinder(importlib.abc.MetaPathFinder):
"""SQLite元路径查找器""" """SQLite元路径查找器"""
def __init__(self, db_path: str): def __init__(self, db_path: str, key: str | None = None):
self.db_path = db_path self.db_path = db_path
self.key = key
self.connection = sqlite3.connect(db_path) self.connection = sqlite3.connect(db_path)
self.connection.row_factory = sqlite3.Row self.connection.row_factory = sqlite3.Row
# 缓存模块查找结果 # 缓存模块查找结果
@@ -31,7 +32,7 @@ class SQLiteMetaPathFinder(importlib.abc.MetaPathFinder):
# 创建模块规范 # 创建模块规范
spec = importlib.util.spec_from_loader( spec = importlib.util.spec_from_loader(
fullname, fullname,
SQLiteModuleLoader(self.connection, fullname, self.db_path), SQLiteModuleLoader(self.connection, fullname, self.db_path, self.key),
origin=f"sqlite://{self.db_path}", origin=f"sqlite://{self.db_path}",
is_package=self.module_cache[fullname], is_package=self.module_cache[fullname],
) )
@@ -44,7 +45,9 @@ class SQLiteMetaPathFinder(importlib.abc.MetaPathFinder):
if fullname in self.module_cache: if fullname in self.module_cache:
spec = importlib.util.spec_from_loader( spec = importlib.util.spec_from_loader(
fullname, fullname,
SQLiteModuleLoader(self.connection, fullname, self.db_path), SQLiteModuleLoader(
self.connection, fullname, self.db_path, self.key
),
origin=f"sqlite://{self.db_path}", origin=f"sqlite://{self.db_path}",
is_package=self.module_cache[fullname], is_package=self.module_cache[fullname],
) )
+143 -14
View File
@@ -1,22 +1,37 @@
import sqlite3 import sqlite3
import marshal import marshal
import base64
import os
import json
from pathlib import Path from pathlib import Path
from typing import Dict from typing import Dict, Optional, Tuple
from .encryption import Encryption
class SQLiteModuleImporter: class SQLiteModuleImporter:
"""从SQLite数据库导入Python模块的导入器""" """从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.db_path = db_path
self.connection = sqlite3.connect(db_path) self.connection = sqlite3.connect(db_path)
self.connection.row_factory = sqlite3.Row 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() self._create_tables()
def _create_tables(self): def _create_tables(self):
"""创建存储模块的数据库表""" """创建存储模块的数据库表"""
self.connection.execute( self.connection.execute("""
"""
CREATE TABLE IF NOT EXISTS python_modules ( CREATE TABLE IF NOT EXISTS python_modules (
module_name TEXT PRIMARY KEY, module_name TEXT PRIMARY KEY,
source_code TEXT, source_code TEXT,
@@ -25,37 +40,151 @@ class SQLiteModuleImporter:
metadata TEXT, metadata TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 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() 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( def add_module(
self, self,
module_name: str, module_name: str,
source_code: str, source_code: str,
is_package: bool = False, 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 = compile(source_code, f"<sqlite_module:{module_name}>", "exec")
bytecode_blob = marshal.dumps(bytecode) bytecode_blob = marshal.dumps(bytecode)
metadata_str = str(metadata) if metadata else "{}" 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( self.connection.execute(
""" """
INSERT OR REPLACE INTO python_modules INSERT OR REPLACE INTO python_modules
(module_name, source_code, bytecode, is_package, metadata) (module_name, source_code, bytecode, is_package, metadata, encrypted, pyarmor_protected)
VALUES (?, ?, ?, ?, ?) 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() 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: if module_name is None:
# 从文件名推导模块名 # 从文件名推导模块名
@@ -66,9 +195,9 @@ class SQLiteModuleImporter:
self.add_module(module_name, source_code) 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: if package_name is None:
package_name = package_path.name package_name = package_path.name
+190 -4
View File
@@ -3,18 +3,31 @@ import importlib
import importlib.abc import importlib.abc
import marshal import marshal
import types 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): class SQLiteModuleLoader(importlib.abc.Loader):
"""SQLite模块加载器""" """SQLite模块加载器"""
def __init__( 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.db_connection = db_connection
self.module_name = module_name self.module_name = module_name
self.db_path = db_path self.db_path = db_path
self.db_connection.row_factory = sqlite3.Row self.db_connection.row_factory = sqlite3.Row
self.encryption = Encryption(key)
self._runtime_temp_dir = None
def create_module(self, spec): def create_module(self, spec):
"""创建模块对象""" """创建模块对象"""
@@ -35,10 +48,10 @@ class SQLiteModuleLoader(importlib.abc.Loader):
# 创建普通模块 # 创建普通模块
return None # 使用默认创建方式 return None # 使用默认创建方式
def exec_module(self, module): def exec_module(self, module): # type: ignore
"""执行模块代码""" """执行模块代码"""
cursor = self.db_connection.execute( 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,), (self.module_name,),
) )
row = cursor.fetchone() row = cursor.fetchone()
@@ -46,8 +59,27 @@ class SQLiteModuleLoader(importlib.abc.Loader):
if not row: if not row:
raise ImportError(f"No module named '{self.module_name}' in database") 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__) exec(bytecode, module.__dict__)
@@ -61,3 +93,157 @@ class SQLiteModuleLoader(importlib.abc.Loader):
module.__file__ = f"sqlite://{self.db_path}/{self.module_name}" module.__file__ = f"sqlite://{self.db_path}/{self.module_name}"
return module 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