Skip to content

Commit 3a76690

Browse files
committed
feat(security): 实现内置 CurseForge API 密钥保护机制
添加内置密钥管理模块和安全说明文档,防止密钥泄露 - 通过 GitHub Actions 注入密钥到编译环境 - 运行时自动加载内置密钥并防止写入配置文件 - 修改设置界面以区分内置密钥和用户自定义密钥 - 更新配置管理器以处理密钥保护逻辑
1 parent 01db78f commit 3a76690

File tree

6 files changed

+189
-5
lines changed

6 files changed

+189
-5
lines changed

.github/workflows/nuitka-windows-release.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,18 @@ jobs:
4242
Write-Output "version=$version" >> $env:GITHUB_OUTPUT
4343
shell: pwsh
4444

45+
- name: Set CurseForge API Key
46+
id: cf_key
47+
run: |
48+
if ($env:CURSEFORGE_API_KEY) {
49+
Write-Output "has_key=true" >> $env:GITHUB_OUTPUT
50+
} else {
51+
Write-Output "has_key=false" >> $env:GITHUB_OUTPUT
52+
}
53+
shell: pwsh
54+
env:
55+
CURSEFORGE_API_KEY: ${{ secrets.CURSEFORGE_API_KEY }}
56+
4557
- name: Build Windows Executable with Nuitka
4658
uses: Nuitka/Nuitka-Action@main
4759
with:
@@ -64,6 +76,8 @@ jobs:
6476
core
6577
include-data-files: |
6678
requirements.txt=requirements.txt
79+
environment-variables: |
80+
CURSEFORGE_API_KEY=${{ secrets.CURSEFORGE_API_KEY }}
6781
6882
- name: Upload Release Artifact
6983
uses: actions/upload-artifact@v4

BUILTIN_KEY_SECURITY.md

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# 内置 CurseForge API 密钥安全说明
2+
3+
## 概述
4+
5+
为了防止 CurseForge API 密钥泄露,项目实现了内置密钥保护机制。当通过 GitHub Actions 打包时,可以将 `CURSEFORGE_API_KEY` 作为 GitHub Secret 注入到编译后的 exe 文件中,并且该密钥不会被写入配置文件。
6+
7+
## 工作原理
8+
9+
### 1. 构建时注入
10+
在 GitHub Actions 工作流中,通过 `environment-variables` 将 Secret 注入到编译环境:
11+
12+
```yaml
13+
environment-variables: |
14+
CURSEFORGE_API_KEY=${{ secrets.CURSEFORGE_API_KEY }}
15+
```
16+
17+
### 2. 运行时保护
18+
- **加载时**:程序启动时自动从环境变量读取内置密钥,并注入到配置中
19+
- **保存时**:检测到当前使用的密钥与内置密钥相同时,自动阻止写入配置文件
20+
21+
### 3. 核心文件
22+
- `utils/builtin_secrets.py` - 内置密钥管理模块
23+
- `utils/config_manager.py` - 配置加载/保存时处理密钥保护
24+
- `gui/settings_components/curseforge_settings.py` - UI 层保护
25+
- `gui/settings_components/external_services_settings.py` - 外部服务设置保护
26+
27+
## 配置 GitHub Secrets
28+
29+
1. 进入 GitHub 仓库页面
30+
2. 点击 **Settings** → **Secrets and variables** → **Actions**
31+
3. 点击 **New repository secret**
32+
4. 添加以下 Secret:
33+
- **Name**: `CURSEFORGE_API_KEY`
34+
- **Value**: 你的 CurseForge API 密钥(从 https://console.curseforge.com 获取)
35+
36+
## 用户行为
37+
38+
### 对于普通用户(无内置密钥)
39+
- 需要手动输入自己的 CurseForge API 密钥
40+
- 密钥会正常保存到 `config.json`
41+
42+
### 对于使用官方构建的用户(有内置密钥)
43+
- 程序自动使用内置的 API 密钥
44+
- 在设置界面查看时,显示的是内置密钥(但不会显示完整内容)
45+
- **即使修改设置,内置密钥也不会被覆盖或写入配置文件**
46+
- 配置文件中的 `curseforge_api_key` 字段始终为空
47+
48+
## 安全优势
49+
50+
1. **防止泄露**:即使用户分享配置文件,也不会泄露内置 API 密钥
51+
2. **透明使用**:用户无需配置即可使用 CurseForge 功能
52+
3. **防篡改**:用户无法通过修改配置文件来窃取内置密钥
53+
4. **可追溯**:如果密钥泄露,可以追溯到具体的构建版本
54+
55+
## 注意事项
56+
57+
- 内置密钥仅在读入内存时使用,不会持久化存储
58+
- 如果用户输入自己的密钥,会覆盖内置密钥并正常保存
59+
- 反编译 exe 文件仍然可能提取出密钥,因此建议:
60+
- 定期轮换 API 密钥
61+
- 监控 API 使用情况
62+
- 如发现泄露立即在 CurseForge 控制台撤销密钥
63+
64+
## 开发调试
65+
66+
在本地开发时,可以通过环境变量设置测试密钥:
67+
68+
```powershell
69+
$env:CURSEFORGE_API_KEY="your_test_key_here"
70+
python main.py
71+
```

gui/settings_components/curseforge_settings.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ def _create_basic_settings(self, parent):
3636
frame.pack(fill="x", pady=(0, 10), padx=5)
3737
frame.columnconfigure(1, weight=1)
3838

39-
info_label = ttk.Label(frame, text="请输入您的CurseForge API密钥", bootstyle="secondary")
39+
info_label = ttk.Label(frame,
40+
text="官方版本已内置 API 密钥,可直接使用;\n如为自行构建或使用单文件版本,请自行输入密钥。",
41+
bootstyle="secondary")
4042
info_label.pack(anchor="w", pady=(0, 10))
4143

4244
key_frame = ttk.Frame(frame)
@@ -69,8 +71,23 @@ def _toggle_key_visibility(self):
6971
self.key_entry.config(show="*")
7072

7173
def _save_settings(self):
74+
api_key = self.api_key_var.get().strip()
75+
76+
# 检查是否为内置密钥
77+
is_builtin = False
78+
try:
79+
from utils.builtin_secrets import get_builtin_curseforge_key
80+
if get_builtin_curseforge_key() and api_key == get_builtin_curseforge_key():
81+
is_builtin = True
82+
except ImportError:
83+
pass
84+
85+
# 如果是内置密钥,不触发保存(因为会被 config_manager 拦截)
86+
if is_builtin:
87+
return
88+
7289
curseforge_config = {
73-
'curseforge_api_key': self.api_key_var.get().strip()
90+
'curseforge_api_key': api_key
7491
}
7592

7693
self.config.update(curseforge_config)

gui/settings_components/external_services_settings.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,9 @@ def _create_curseforge_settings(self, parent):
8383
frame.pack(fill="x", pady=(0, 10), padx=5)
8484
frame.columnconfigure(1, weight=1)
8585

86-
info_label = ttk.Label(frame, text="用于从CurseForge平台搜索和下载模组", bootstyle="secondary")
86+
info_label = ttk.Label(frame,
87+
text="用于获取模组信息、从 CurseForge 平台搜索和下载模组\n官方版本已内置密钥;自行构建请自行输入。",
88+
bootstyle="secondary")
8789
info_label.grid(row=0, column=0, columnspan=3, sticky="w", pady=(0, 10))
8890

8991
# API密钥
@@ -171,8 +173,23 @@ def _save_github_settings(self):
171173
self.save_callback(github_config)
172174

173175
def _save_cf_settings(self):
176+
api_key = self.cf_api_key_var.get().strip()
177+
178+
# 检查是否为内置密钥
179+
is_builtin = False
180+
try:
181+
from utils.builtin_secrets import get_builtin_curseforge_key
182+
if get_builtin_curseforge_key() and api_key == get_builtin_curseforge_key():
183+
is_builtin = True
184+
except ImportError:
185+
pass
186+
187+
# 如果是内置密钥,不触发保存
188+
if is_builtin:
189+
return
190+
174191
cf_config = {
175-
'curseforge_api_key': self.cf_api_key_var.get().strip()
192+
'curseforge_api_key': api_key
176193
}
177194
self.config.update(cf_config)
178195
self.save_callback(cf_config)

utils/builtin_secrets.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""
2+
内置密钥管理模块
3+
用于在打包时嵌入敏感密钥,并防止其被写入配置文件
4+
"""
5+
import os
6+
7+
# 内置 CurseForge API 密钥(在构建时由 GitHub Secrets 注入)
8+
BUILTIN_CURSEFORGE_API_KEY = os.environ.get('CURSEFORGE_API_KEY', '')
9+
10+
# 标记是否为内置密钥
11+
IS_BUILTIN_CURSEFORGE_KEY = bool(BUILTIN_CURSEFORGE_API_KEY)
12+
13+
14+
def get_builtin_curseforge_key() -> str:
15+
"""获取内置的 CurseForge API 密钥"""
16+
return BUILTIN_CURSEFORGE_API_KEY
17+
18+
19+
def is_builtin_curseforge_key_set() -> bool:
20+
"""检查是否设置了内置 CurseForge API 密钥"""
21+
return IS_BUILTIN_CURSEFORGE_KEY
22+
23+
24+
def is_protected_key(key: str, value: str) -> bool:
25+
"""
26+
检查某个配置项是否为受保护的内置密钥
27+
28+
Args:
29+
key: 配置项的键
30+
value: 配置项的值
31+
32+
Returns:
33+
如果是受保护的内置密钥则返回 True
34+
"""
35+
if key == 'curseforge_api_key' and IS_BUILTIN_CURSEFORGE_KEY:
36+
# 如果用户输入的值与内置密钥相同,则认为是受保护的
37+
if value.strip() == BUILTIN_CURSEFORGE_API_KEY:
38+
return True
39+
return False

utils/config_manager.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,20 @@ def load_config() -> dict:
119119
try:
120120
with open(CONFIG_FILE_PATH, 'r', encoding='utf-8') as f:
121121
config = json.load(f)
122+
123+
# 注入内置 CurseForge API 密钥(如果存在且用户未自定义)
124+
try:
125+
from utils.builtin_secrets import get_builtin_curseforge_key
126+
builtin_key = get_builtin_curseforge_key()
127+
if builtin_key:
128+
# 只有当配置文件中没有设置密钥时,才使用内置密钥
129+
if not config.get('curseforge_api_key', '').strip():
130+
config['curseforge_api_key'] = builtin_key
131+
logging.debug("已加载内置 CurseForge API 密钥")
132+
else:
133+
logging.debug("检测到用户自定义 CurseForge API 密钥,将优先使用用户设置")
134+
except ImportError:
135+
pass
122136
config_updated = False
123137

124138
if "api_keys_raw" not in config and "api_keys" in config:
@@ -200,8 +214,20 @@ def load_config() -> dict:
200214

201215
def save_config(config_data: dict):
202216
try:
217+
# 防止内置密钥被写入配置文件
218+
config_to_save = config_data.copy()
219+
try:
220+
from utils.builtin_secrets import is_protected_key, get_builtin_curseforge_key
221+
if get_builtin_curseforge_key():
222+
# 如果设置了内置密钥,则从保存的配置中移除
223+
if config_to_save.get('curseforge_api_key', '').strip() == get_builtin_curseforge_key():
224+
config_to_save['curseforge_api_key'] = ''
225+
logging.debug("已阻止内置 CurseForge API 密钥写入配置文件")
226+
except ImportError:
227+
pass
228+
203229
with open(CONFIG_FILE_PATH, 'w', encoding='utf-8') as f:
204-
json.dump(config_data, f, indent=4, ensure_ascii=False)
230+
json.dump(config_to_save, f, indent=4, ensure_ascii=False)
205231
logging.debug("配置已自动保存")
206232
except Exception as e:
207233
logging.error(f"保存配置文件时出错:{e}")

0 commit comments

Comments
 (0)