@yufengtadian/freedom-cli 1.12.15 → 1.12.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -2
- package/bin/freedom.js +13 -3
- package/lib/build.js +59 -4
- package/lib/cli.js +53 -11
- package/lib/config.js +12 -1
- package/lib/security.js +150 -0
- package/lib/tui.js +31 -14
- package/lib/update.js +177 -27
- package/package.json +39 -39
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/templates/go/pkg/freedom/anti_debug_other.go +8 -0
- package/templates/go/pkg/freedom/anti_debug_windows.go +39 -0
- package/templates/go/pkg/freedom/center_windows.go +11 -17
- package/templates/go/pkg/freedom/configfile.go +37 -9
- package/templates/go/pkg/freedom/freedom.go +19 -2
- package/templates/go/pkg/freedom/security.go +227 -0
- package/templates/go/pkg/freedom/security_test.go +153 -0
- package/templates/go/pkg/freedom/window_other.go +4 -0
- package/templates/go/pkg/freedom/window_windows.go +20 -0
- package/templates/go/webview_go/webview.go +51 -0
- package/templates/project/freedom.config.js +8 -0
package/README.md
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
# freedom-cli
|
|
2
2
|
|
|
3
|
-
Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌面应用(v1.12.
|
|
3
|
+
Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌面应用(v1.12.17)。
|
|
4
4
|
|
|
5
5
|
基于自研 Freedom WebView 壳层(对标 Wails / Tauri):前端完全自由、后端可任意语言、渲染复用系统 WebView(Windows WebView2 / macOS WKWebView / Linux WebKitGTK),产物为单个可执行文件 + resources 目录,前端页面内存加载,不占本地端口。
|
|
6
6
|
|
|
7
|
+
**v1.12.17 安全模式全面落地**:三档安全模式 `freedom security <none|basic|high>` 正式随包分发——high 档把 resources 加密为 `app.bin`(AES-256-CTR + HMAC-SHA256 + PBKDF2 密钥派生),配合 `.integrity` 完整性校验、anti-debug 与进程隐藏,磁盘无明文、篡改即拒运行;三平台预编译壳经 CI 重建分发,补齐 v1.12.16 仅重编 win-x64 壳的缺口。
|
|
8
|
+
|
|
9
|
+
**v1.12.16 自动更新 + 完整 CLI 模式 + 壳生命周期全面防御**:
|
|
10
|
+
- **自动检测版本并自动更新**:`freedom update` / `freedom check-update` 检测到新版本即自动执行 `npm install -g @yufengtadian/freedom-cli@latest` 升级,**不再需要手动执行 npm 命令**;每次命令执行成功后静默自检,发现新版自动更新(6 小时频控防骚扰,非全局安装给出明确升级指引);TUI 主菜单「检查 / 自动更新」同步接入;
|
|
11
|
+
- **完整 CLI 模式**:无参数运行 `freedom` 直接进入交互式完整 CLI(打包 / 新建项目 / 配置 / 壳管理 / 教程 / 自动更新 / 退出);管道与脚本环境自动降级打印帮助,不卡死;
|
|
12
|
+
- **壳生命周期全面防御(B51-B58 闭环)**:webview 全方法销毁防护、user32 proc 去重、WebView2 缺失可操作提示;`webview_create` 失败(如缺 WebKitGTK)返回 nil 后新壳走明确失败提示而非空指针崩溃;用户 `Bind` 的方法不再被 config.json 进程后端静默覆盖失效;help 文案与自动更新实现对齐,清理死代码;
|
|
13
|
+
- **安全加固(新增 high 安全模式,B59-B62 闭环)**:`freedom security <none|basic|high>` 三档安全模式,high 档把 resources 整体加密为 `app.bin`(AES-256-CTR + HMAC-SHA256,密钥经 PBKDF2 按应用派生),磁盘无明文 HTML/配置,壳内存解密 + `.integrity` 完整性校验(防整体替换 / 篡改 / exe 改名),另含 anti-debug(检测调试器即退出)与进程隐藏;详见下文「安全模式」章节。
|
|
14
|
+
|
|
7
15
|
**v1.12.15 补发壳生命周期稳定性修复**:v1.12.14 的 npm 上架时间早于修复提交,上架版预编译壳未包含悬垂指针修复(窗口操作 / 代理端口触发随机退出的根因);本版正式把含 B50 修复的预编译壳随 npm 包分发——
|
|
8
16
|
- 壳销毁生命周期修复:`webview.Destroy()` 后经 Emit / Quit / WindowHandle / binding 回调访问已释放 C 对象(悬垂指针)导致随机退出;新增 `destroyed` 原子标记,Dispatch / Eval / binding 回调销毁后一律跳过原生调用,`Run` 在 `Destroy` 前清空 `App.view`,事件与绑定回调加 `recover` 兜底;
|
|
9
17
|
- 使用全局包构建的下游应用(如 DSH)升级到本版后,随机退出问题一并消除(可用二进制字符串指纹 `freedom: binding panic` 核对)。
|
|
@@ -89,6 +97,7 @@ freedom icon icon.ico
|
|
|
89
97
|
|
|
90
98
|
# 4. 打包
|
|
91
99
|
freedom build # 默认当前平台
|
|
100
|
+
freedom build --security high # 以 high 安全模式构建(resources 加密,见「安全模式」)
|
|
92
101
|
freedom build --platform all # 三平台全量(win + mac + linux)
|
|
93
102
|
freedom build --platform win-x64 # 仅 Windows
|
|
94
103
|
freedom build --platform darwin-arm64 # 仅 macOS Apple Silicon
|
|
@@ -147,6 +156,7 @@ export default {
|
|
|
147
156
|
minHeight: 300,
|
|
148
157
|
center: true, // 启动居中
|
|
149
158
|
debug: false, // 开发者工具
|
|
159
|
+
security: 'none', // 安全模式:'none'(明文)| 'basic'(明文+提示)| 'high'(resources 加密,见「安全模式」)
|
|
150
160
|
titlebar: 'frameless', // frameless(默认,标题栏不存在,三按钮由前端自绘)| native
|
|
151
161
|
icon: undefined, // 应用图标:Windows 用 .ico(推荐多尺寸),macOS 用 .icns
|
|
152
162
|
outDir: 'dist', // 产物目录:'dist'(默认)| '.'(项目根目录)| 任意路径
|
|
@@ -159,6 +169,35 @@ export default {
|
|
|
159
169
|
- macOS:`.icns` 在构建时放入 `.app/Contents/Resources` 并写入 Info.plist 的 `CFBundleIconFile`;
|
|
160
170
|
- 未配置时使用壳默认图标。`freedom icon <path>` 可直接写入该配置。
|
|
161
171
|
|
|
172
|
+
## 安全模式
|
|
173
|
+
|
|
174
|
+
默认产物把前端 HTML 与配置以明文写入 `resources/`,任何人解包即可直接读取页面源码与配置。如需防止源码被轻易提取,提供三档安全模式:
|
|
175
|
+
|
|
176
|
+
| 模式 | 资源形态 | 破解难度 | 适用场景 |
|
|
177
|
+
| --- | --- | --- | --- |
|
|
178
|
+
| `none` | 明文 `index.html` + `config.json`(默认,兼容历史产物) | 解包即读 | 公开页面 / 调试 / 快速分发 |
|
|
179
|
+
| `basic` | 同上明文,构建时额外输出加固建议 | 低 | 需要提示、暂不加密 |
|
|
180
|
+
| `high` | 整体加密为 `resources/app.bin` + `.integrity`,磁盘**无任何明文** HTML/配置 | 高(需逆向壳 + 还原派生密钥) | 防源码提取、防资源篡改的正式分发 |
|
|
181
|
+
|
|
182
|
+
**切换方式**(二选一,`--security` 可临时覆盖配置文件):
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
freedom security high # 写入 freedom.config.js 持久生效
|
|
186
|
+
freedom build --security high # 单次构建生效(不改配置)
|
|
187
|
+
freedom build # 读取配置中的 security 值
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**high 模式原理**:
|
|
191
|
+
- 构建时:前端 HTML + 配置序列化后,用 **AES-256-CTR** 加密为 `app.bin`(容器头 `FRDM1` + 16B IV + 16B 认证标签),并生成 `.integrity` 完整性清单(`app.bin` 与 `backend/**` 各文件的 HMAC-SHA256);
|
|
192
|
+
- 加密密钥由 **PBKDF2-HMAC-SHA256**(6 万次迭代)按「应用可执行文件名」派生,不同应用密钥不同,暴力破解成本高;
|
|
193
|
+
- 壳启动时:先校验 `.integrity`(防整体替换 / 篡改 / exe 改名),再恒定时间比对 HMAC 认证标签(Encrypt-then-MAC),最后内存中解密加载——**磁盘始终无明文**;
|
|
194
|
+
- 解密 / 校验失败即拒绝运行(不静默回退明文,防降级攻击);
|
|
195
|
+
- 另内置 **anti-debug**(`IsDebuggerPresent` / `CheckRemoteDebuggerPresent` 命中即退出)与进程隐藏加固。
|
|
196
|
+
|
|
197
|
+
**加固上限说明**:`high` 大幅提高破解门槛,但**任何客户端可执行程序都无法做到绝对不可破解**——密钥最终存在于壳二进制与运行时内存中。更高强度建议:壳编译时设置 `-ldflags "-s -w"` 剥离符号表(CI 编译壳时已可选开启)、对核心业务保留服务端校验。若需"怎么都解不开",请把真正敏感的密钥 / 逻辑放到你的后端。
|
|
198
|
+
|
|
199
|
+
**互斥规则**:切换安全模式重新构建时,CLI 会自动清理另一模式的遗留产物(`app.bin`/`.integrity` 与明文 `index.html`/`config.json` 只能存其一),避免壳误加载旧资源。
|
|
200
|
+
|
|
162
201
|
## 前端
|
|
163
202
|
|
|
164
203
|
前端是标准 Vite 项目,`vite build` 时通过 `vite-plugin-singlefile` 内联为单个 HTML。壳层注入全局对象 `window.freedom`:
|
|
@@ -181,8 +220,9 @@ window.freedom.window.minimize(); // 窗口控制
|
|
|
181
220
|
```
|
|
182
221
|
freedom tui # 交互式终端界面
|
|
183
222
|
freedom init <目录> [--force]
|
|
184
|
-
freedom build [--platform win-x64|darwin-arm64|linux-x64|all] [--no-cache]
|
|
223
|
+
freedom build [--platform win-x64|darwin-arm64|linux-x64|all] [--no-cache] [--security none|basic|high]
|
|
185
224
|
freedom titlebar <native|frameless>
|
|
225
|
+
freedom security <none|basic|high> # 设置安全模式(写入配置)
|
|
186
226
|
freedom icon <path> # 设置应用图标(Windows 用 .ico,macOS 用 .icns)
|
|
187
227
|
freedom config [get|set]
|
|
188
228
|
freedom shell list|download <platform>|build <platform>
|
package/bin/freedom.js
CHANGED
|
@@ -2,11 +2,21 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const { run } = require('../lib/cli');
|
|
5
|
-
const {
|
|
5
|
+
const { maybeAutoUpdate, formatUpdateResult } = require('../lib/update');
|
|
6
|
+
const theme = require('../lib/theme');
|
|
7
|
+
|
|
8
|
+
const cmd = process.argv[2];
|
|
9
|
+
// TUI(无参 / freedom tui)与显式更新命令内部已处理更新,bin 层不再重复触发
|
|
10
|
+
const handled = cmd === undefined || cmd === 'tui' || cmd === 'update' || cmd === 'check-update';
|
|
6
11
|
|
|
7
12
|
run(process.argv.slice(2)).then(async (code) => {
|
|
8
|
-
|
|
9
|
-
|
|
13
|
+
// 命令成功且非 TUI / 非显式更新命令时,静默执行自动更新(检测到新版本自动升级,无需用户手动操作)
|
|
14
|
+
if (code === 0 && !handled) {
|
|
15
|
+
try {
|
|
16
|
+
const res = await maybeAutoUpdate();
|
|
17
|
+
const notice = formatUpdateResult(res, theme);
|
|
18
|
+
if (notice.length) console.log(notice.join('\n'));
|
|
19
|
+
} catch (e) { /* 更新流程失败静默 */ }
|
|
10
20
|
}
|
|
11
21
|
// 用 exitCode 让 Node 自然刷新 stdout 后退出,避免 process.exit 截断管道输出(历史 bug B28)
|
|
12
22
|
process.exitCode = code || 0;
|
package/lib/build.js
CHANGED
|
@@ -35,6 +35,13 @@ const {
|
|
|
35
35
|
localShellPath,
|
|
36
36
|
} = require('./utils');
|
|
37
37
|
const { hasShell, downloadShell, validateLocalShell } = require('./shell');
|
|
38
|
+
const {
|
|
39
|
+
SECURITY_MODES,
|
|
40
|
+
resolveSecurity,
|
|
41
|
+
encryptApp,
|
|
42
|
+
buildIntegrity,
|
|
43
|
+
renderIntegrity,
|
|
44
|
+
} = require('./security');
|
|
38
45
|
|
|
39
46
|
function run(cmd, args, opts = {}) {
|
|
40
47
|
// Windows 下 npm 是 .cmd 批处理,必须经 shell 执行
|
|
@@ -118,6 +125,11 @@ async function build(projectDir, opts = {}) {
|
|
|
118
125
|
const html = fs.readFileSync(distHtml, 'utf8');
|
|
119
126
|
warnIfNotSingleFile(html, distHtml);
|
|
120
127
|
const configJSON = renderConfigJSON(cfg, name);
|
|
128
|
+
// 安全模式:--security 优先于 freedom.config.js 的 security 字段,默认 none。
|
|
129
|
+
// none = 明文资源(默认,兼容历史产物)
|
|
130
|
+
// basic = 明文资源 + 构建期安全提示(剥符号 / 混淆,后续可用高模式加密)
|
|
131
|
+
// high = resources 整体加密为 app.bin(AES-256-CTR+HMAC)+ .integrity 完整性清单,磁盘无明文
|
|
132
|
+
const security = resolveSecurity(opts.security, cfg.security);
|
|
121
133
|
|
|
122
134
|
// 2) 后端目录(若配置了 backend 进程)
|
|
123
135
|
const backendDir = path.join(dir, cfg.backendDir || 'backend');
|
|
@@ -133,7 +145,7 @@ async function build(projectDir, opts = {}) {
|
|
|
133
145
|
const results = await Promise.all(
|
|
134
146
|
platforms.map(async (plat) => {
|
|
135
147
|
const targetDir = multi ? path.join(outDirPath, plat) : outDirPath;
|
|
136
|
-
const outFile = await emitPlatform({ plat, name, version, targetDir, html, configJSON, backendDir, hasBackend, autoDownload, icon });
|
|
148
|
+
const outFile = await emitPlatform({ plat, name, version, targetDir, html, configJSON, backendDir, hasBackend, autoDownload, icon, security });
|
|
137
149
|
return { plat, outFile };
|
|
138
150
|
})
|
|
139
151
|
);
|
|
@@ -141,7 +153,7 @@ async function build(projectDir, opts = {}) {
|
|
|
141
153
|
return { results };
|
|
142
154
|
}
|
|
143
155
|
|
|
144
|
-
async function emitPlatform({ plat, name, version, targetDir, html, configJSON, backendDir, hasBackend, autoDownload, icon }) {
|
|
156
|
+
async function emitPlatform({ plat, name, version, targetDir, html, configJSON, backendDir, hasBackend, autoDownload, icon, security }) {
|
|
145
157
|
// 取预编译壳二进制
|
|
146
158
|
const shell = localShellPath(plat);
|
|
147
159
|
if (!fs.existsSync(shell)) {
|
|
@@ -177,10 +189,40 @@ async function emitPlatform({ plat, name, version, targetDir, html, configJSON,
|
|
|
177
189
|
}
|
|
178
190
|
|
|
179
191
|
// 写 resources:页面 + 配置 + 后端
|
|
192
|
+
// security=high 时整体加密为 app.bin(磁盘无明文 HTML/config),并生成 .integrity 完整性清单;
|
|
193
|
+
// none / basic 保持明文资源(兼容历史产物),basic 额外输出安全加固提示。
|
|
180
194
|
const resDir = path.join(targetDir, 'resources');
|
|
181
195
|
await fsp.mkdir(resDir, { recursive: true });
|
|
182
|
-
|
|
183
|
-
|
|
196
|
+
// 互斥清理:切换安全模式时删除另一模式遗留产物,防止壳误加载旧资源
|
|
197
|
+
// (high 产物 app.bin/.integrity 与明文 index.html/config.json 只能存其一)。
|
|
198
|
+
if (security === 'high') {
|
|
199
|
+
for (const legacy of ['index.html', 'config.json']) {
|
|
200
|
+
const p = path.join(resDir, legacy);
|
|
201
|
+
if (fs.existsSync(p)) await fsp.rm(p, { force: true });
|
|
202
|
+
}
|
|
203
|
+
const appBin = encryptApp(name, html, configJSON);
|
|
204
|
+
await fsp.writeFile(path.join(resDir, 'app.bin'), appBin);
|
|
205
|
+
const backendRelMap = {};
|
|
206
|
+
if (hasBackend && fs.existsSync(backendDir)) {
|
|
207
|
+
collectDirFiles(backendDir, '', backendRelMap);
|
|
208
|
+
}
|
|
209
|
+
const integrityText = renderIntegrity(buildIntegrity(name, appBin, backendRelMap));
|
|
210
|
+
await fsp.writeFile(path.join(resDir, '.integrity'), integrityText, 'utf8');
|
|
211
|
+
} else {
|
|
212
|
+
for (const legacy of ['app.bin', '.integrity']) {
|
|
213
|
+
const p = path.join(resDir, legacy);
|
|
214
|
+
if (fs.existsSync(p)) await fsp.rm(p, { force: true });
|
|
215
|
+
}
|
|
216
|
+
await fsp.writeFile(path.join(resDir, 'index.html'), html, 'utf8');
|
|
217
|
+
await fsp.writeFile(path.join(resDir, 'config.json'), configJSON, 'utf8');
|
|
218
|
+
if (security === 'basic') {
|
|
219
|
+
process.stdout.write(
|
|
220
|
+
`[freedom] 安全模式 basic:资源仍为明文。加固建议:\n` +
|
|
221
|
+
` 1. 运行 ${'freedom security high'} 切换到高模式(resources 加密为 app.bin,磁盘无明文);\n` +
|
|
222
|
+
` 2. 壳侧已内置 anti-debug / 进程隐藏,符号剥离需在 CI 编译壳时设置 ldflags -s -w(见 README 安全章节)。\n`
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
184
226
|
if (hasBackend) {
|
|
185
227
|
copyDir(backendDir, path.join(resDir, 'backend'));
|
|
186
228
|
}
|
|
@@ -472,4 +514,17 @@ function warnIfNotSingleFile(html, distHtml) {
|
|
|
472
514
|
}
|
|
473
515
|
}
|
|
474
516
|
|
|
517
|
+
// 递归收集目录内所有文件:relPath(POSIX 相对路径)-> Buffer 内容,供 .integrity 完整性清单使用。
|
|
518
|
+
function collectDirFiles(dir, prefix, out) {
|
|
519
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
520
|
+
const abs = path.join(dir, entry.name);
|
|
521
|
+
if (entry.isDirectory()) {
|
|
522
|
+
collectDirFiles(abs, prefix + entry.name + '/', out);
|
|
523
|
+
} else {
|
|
524
|
+
out[(prefix + entry.name).replace(/\\/g, '/')] = fs.readFileSync(abs);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return out;
|
|
528
|
+
}
|
|
529
|
+
|
|
475
530
|
module.exports = { build, parsePlatforms };
|
package/lib/cli.js
CHANGED
|
@@ -19,7 +19,8 @@ function help() {
|
|
|
19
19
|
L.push(` ${paint('用法:', C.bold, C.fg.white)} ${paint('freedom <command> [options]', C.fg.cyan, C.bold)}`);
|
|
20
20
|
L.push('');
|
|
21
21
|
L.push(section('交互式界面'));
|
|
22
|
-
L.push(` ${paint('freedom
|
|
22
|
+
L.push(` ${paint('freedom', C.fg.cyan, C.bold)} ${dim('进入完整 CLI 模式(交互式菜单,新建 / 打包 / 配置 / 壳管理)')}`);
|
|
23
|
+
L.push(` ${paint('freedom tui', C.fg.cyan, C.bold)} ${dim('同上(兼容写法,显式进入交互式模式)')}`);
|
|
23
24
|
L.push(section('项目'));
|
|
24
25
|
L.push(` ${paint('freedom init [目录] [--force]', C.fg.cyan)} ${dim('在当前 / 指定目录新建项目模板')}`);
|
|
25
26
|
L.push(` ${paint('freedom tutorial', C.fg.cyan)} ${dim('再次打开安装教程')}`);
|
|
@@ -41,7 +42,7 @@ function help() {
|
|
|
41
42
|
L.push(` ${paint('freedom config set <key> <value>', C.fg.cyan)} ${dim('修改单个配置项')}`);
|
|
42
43
|
L.push(section('版本'));
|
|
43
44
|
L.push(` ${paint('freedom version', C.fg.cyan)} ${dim('显示版本并检测最新版本')}`);
|
|
44
|
-
L.push(` ${paint('freedom update', C.fg.cyan)} ${dim('
|
|
45
|
+
L.push(` ${paint('freedom update', C.fg.cyan)} ${dim('检查新版本并立即自动更新')}`);
|
|
45
46
|
L.push(` ${paint('freedom help', C.fg.cyan)} ${dim('显示本帮助')}`);
|
|
46
47
|
L.push('');
|
|
47
48
|
L.push(section('平台'));
|
|
@@ -63,7 +64,15 @@ async function run(argv) {
|
|
|
63
64
|
const [cmd, ...rest] = argv;
|
|
64
65
|
|
|
65
66
|
switch (cmd) {
|
|
66
|
-
case undefined:
|
|
67
|
+
case undefined: {
|
|
68
|
+
// 无参数:交互式终端进入完整 CLI 模式(TUI);管道 / 脚本环境打印帮助,避免卡死
|
|
69
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
70
|
+
return await require('./tui').tui(process.cwd());
|
|
71
|
+
}
|
|
72
|
+
console.log(help());
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
67
76
|
case 'help':
|
|
68
77
|
case '--help':
|
|
69
78
|
case '-h':
|
|
@@ -76,7 +85,7 @@ async function run(argv) {
|
|
|
76
85
|
const r = await update.checkUpdate({ force: true });
|
|
77
86
|
console.log(versionCard(r.current, r.latest, r.hasUpdate));
|
|
78
87
|
if (r.hasUpdate && r.latest) {
|
|
79
|
-
console.log(` ${tip('
|
|
88
|
+
console.log(` ${tip('检测到新版本,将自动更新。')}`);
|
|
80
89
|
console.log('');
|
|
81
90
|
}
|
|
82
91
|
return 0;
|
|
@@ -86,14 +95,15 @@ async function run(argv) {
|
|
|
86
95
|
case 'check-update': {
|
|
87
96
|
const r = await update.checkUpdate({ force: true });
|
|
88
97
|
console.log(versionCard(r.current, r.latest, r.hasUpdate));
|
|
89
|
-
if (r.
|
|
90
|
-
console.log(` ${tip('检测到新版本,执行以下命令升级:')}`);
|
|
91
|
-
console.log(` ${paint(`npm install -g ${update.PKG_NAME}@latest`, C.fg.cyan, C.bold)}`);
|
|
92
|
-
console.log('');
|
|
93
|
-
} else if (r.latest) {
|
|
98
|
+
if (r.latest && !r.hasUpdate) {
|
|
94
99
|
console.log(` ${ok('当前已是最新版本。')}`);
|
|
95
|
-
} else {
|
|
100
|
+
} else if (!r.latest) {
|
|
96
101
|
console.log(` ${warn('检查失败:网络不可用或 npm registry 未响应,请稍后重试。')}`);
|
|
102
|
+
} else {
|
|
103
|
+
// 有新版:立即自动更新(freedom update = 强制更新,无需手动执行 npm 命令)
|
|
104
|
+
const res = await update.maybeAutoUpdate({ force: true, r });
|
|
105
|
+
const notice = update.formatUpdateResult(res, theme);
|
|
106
|
+
if (notice.length) console.log(notice.join('\n'));
|
|
97
107
|
}
|
|
98
108
|
return 0;
|
|
99
109
|
}
|
|
@@ -129,7 +139,22 @@ async function run(argv) {
|
|
|
129
139
|
return 1;
|
|
130
140
|
}
|
|
131
141
|
}
|
|
132
|
-
|
|
142
|
+
// 安全模式:--security <none|basic|high>(可选,默认取 freedom.config.js 的 security 字段)
|
|
143
|
+
const secArg = rest.find((a) => a.startsWith('--security'));
|
|
144
|
+
let security;
|
|
145
|
+
if (secArg) {
|
|
146
|
+
const inline = secArg.includes('=') ? secArg.split('=')[1] : undefined;
|
|
147
|
+
security = inline || rest[rest.indexOf(secArg) + 1];
|
|
148
|
+
if (!security || security.startsWith('--')) {
|
|
149
|
+
console.error(`${err('--security 缺少取值。')} ${dim('用法:')}${paint('freedom build --security <none|basic|high>', C.fg.cyan)}`);
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const { results } = await build(process.cwd(), {
|
|
154
|
+
platform,
|
|
155
|
+
noCache: rest.includes('--no-cache'),
|
|
156
|
+
security,
|
|
157
|
+
});
|
|
133
158
|
for (const r of results) {
|
|
134
159
|
console.log(`${ok('构建完成')} ${paint(`[${r.plat}]`, C.fg.magenta, C.bold)} ${paint(r.outFile, C.fg.white)}`);
|
|
135
160
|
}
|
|
@@ -168,6 +193,23 @@ async function run(argv) {
|
|
|
168
193
|
return 0;
|
|
169
194
|
}
|
|
170
195
|
|
|
196
|
+
case 'security': {
|
|
197
|
+
const mode = rest[0];
|
|
198
|
+
if (!['none', 'basic', 'high'].includes(mode)) {
|
|
199
|
+
console.error(`${err('用法:')}${paint('freedom security <none|basic|high>', C.fg.cyan)}`);
|
|
200
|
+
console.error(` ${dim('none = 明文资源(默认,兼容历史产物)')}`);
|
|
201
|
+
console.error(` ${dim('basic = 明文资源 + 构建期加固提示')}`);
|
|
202
|
+
console.error(` ${dim('high = resources 加密为 app.bin + .integrity 完整性清单,磁盘无明文,壳内存解密')}`);
|
|
203
|
+
return 1;
|
|
204
|
+
}
|
|
205
|
+
setConfig(process.cwd(), 'security', mode);
|
|
206
|
+
console.log(`${ok('安全模式已切换为:')}${paint(mode, C.fg.cyan, C.bold)}`);
|
|
207
|
+
if (mode === 'high') {
|
|
208
|
+
console.log(` ${dim('下一次')} ${paint('freedom build', C.fg.cyan)} ${dim('将加密 resources(app.bin),前端源码与配置不会以明文落盘。')}`);
|
|
209
|
+
}
|
|
210
|
+
return 0;
|
|
211
|
+
}
|
|
212
|
+
|
|
171
213
|
case 'config': {
|
|
172
214
|
const sub = rest[0];
|
|
173
215
|
if (sub === 'get') {
|
package/lib/config.js
CHANGED
|
@@ -14,6 +14,7 @@ const KEY_TYPES = {
|
|
|
14
14
|
center: 'bool',
|
|
15
15
|
debug: 'bool',
|
|
16
16
|
titlebar: 'titlebar',
|
|
17
|
+
security: 'security',
|
|
17
18
|
icon: 'string',
|
|
18
19
|
outDir: 'string',
|
|
19
20
|
};
|
|
@@ -42,6 +43,13 @@ function normalizeValue(key, value) {
|
|
|
42
43
|
}
|
|
43
44
|
return s;
|
|
44
45
|
}
|
|
46
|
+
if (t === 'security') {
|
|
47
|
+
const s = String(value);
|
|
48
|
+
if (!['none', 'basic', 'high'].includes(s)) {
|
|
49
|
+
throw new Error('security 取值必须为:none / basic / high。');
|
|
50
|
+
}
|
|
51
|
+
return s;
|
|
52
|
+
}
|
|
45
53
|
// 扩展键(如 backend):字符串命令简写转 {command,args} 对象,
|
|
46
54
|
// 与 build.js renderConfigJSON 的契约对齐(纯字符串形态不会被打包为后端进程)。
|
|
47
55
|
if (key === 'backend') {
|
|
@@ -86,7 +94,9 @@ function setConfig(dir, key, value) {
|
|
|
86
94
|
const rendered = renderLiteral(norm);
|
|
87
95
|
|
|
88
96
|
const lines = text.split('\n');
|
|
89
|
-
|
|
97
|
+
// 容忍 CRLF 行尾(Windows 编辑器 / git autocrlf 可能产生):JS 正则的 . 不匹配 \r,
|
|
98
|
+
// 直接匹配 (.*)$ 会在行尾残留 \r 导致整行失配(历史隐患)。
|
|
99
|
+
const keyRe = new RegExp(`^(\\s*)${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*:(.*)\\r?$`);
|
|
90
100
|
let replaced = false;
|
|
91
101
|
for (let i = 0; i < lines.length; i++) {
|
|
92
102
|
if (lines[i].trim().startsWith('//')) continue; // 注释行不参与匹配与改写
|
|
@@ -130,6 +140,7 @@ const KNOWN_KEYS = {
|
|
|
130
140
|
center: true,
|
|
131
141
|
debug: false,
|
|
132
142
|
titlebar: 'frameless',
|
|
143
|
+
security: 'none',
|
|
133
144
|
icon: undefined,
|
|
134
145
|
outDir: 'dist',
|
|
135
146
|
};
|
package/lib/security.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// 安全加固模块(v1.13.0)
|
|
4
|
+
//
|
|
5
|
+
// 为 freedom build 提供两种安全模式:
|
|
6
|
+
// - basic(简单模式):资源压缩 + 壳剥离符号建议,阻止"随手打开 resources/
|
|
7
|
+
// 就能看到前端源码"的随手破解,成本低、完全向后兼容。
|
|
8
|
+
// - high(高危模式):AES-256-CTR + HMAC-SHA256(Encrypt-then-MAC)加密资源为
|
|
9
|
+
// 单一容器 resources/app.bin,运行时由壳在内存中解密(磁盘无明文);
|
|
10
|
+
// 配套 HMAC 完整性校验防篡改;密钥不落盘(由 exe 文件名派生),
|
|
11
|
+
// 并强制关闭 WebView2 devtools。
|
|
12
|
+
//
|
|
13
|
+
// 架构说明(重要):通用壳 = 所有应用共用一份预编译 freedom-shell 二进制,
|
|
14
|
+
// 因此密钥派生所需的"主密钥常量"编译在通用壳内,天然被所有应用共享;
|
|
15
|
+
// high 模式的对抗目标是把攻击者从"直接读明文"抬升到"必须逆向壳二进制、
|
|
16
|
+
// 提取主密钥并复现派生算法"。真正"每应用唯一密钥"需要每应用独立编译壳
|
|
17
|
+
// (破坏通用壳 + 零工具链架构),不在本模式范围内。
|
|
18
|
+
//
|
|
19
|
+
// ⚠️ 跨语言同步点:本文件与 templates/go/pkg/freedom/security.go 必须保持
|
|
20
|
+
// MASTER_KEY / DERIVE_SALT / PBKDF2_ITER / KEY_LEN / APP_BIN_MAGIC
|
|
21
|
+
// 以及加密算法(aes-256-ctr + hmac-sha256)完全一致。
|
|
22
|
+
// 修改任一侧必须同步另一侧,否则 build 出的加密资源壳无法解密。
|
|
23
|
+
|
|
24
|
+
const crypto = require('crypto');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
|
|
27
|
+
const SECURITY_MODES = ['none', 'basic', 'high'];
|
|
28
|
+
|
|
29
|
+
// ---- 密钥派生参数(必须与 Go security.go 同步)----
|
|
30
|
+
// 固定常量主密钥:参与 PBKDF2 派生,编译进通用壳。此处为 CLI 侧同值。
|
|
31
|
+
const MASTER_KEY = 'freedom-shell::kdf-master::v1::7f4e8c2a9b1d6e3f';
|
|
32
|
+
const DERIVE_SALT = 'freedom:derive:v1';
|
|
33
|
+
const PBKDF2_ITER = 60000;
|
|
34
|
+
const KEY_LEN = 32;
|
|
35
|
+
|
|
36
|
+
// resources/app.bin 容器头:magic(5B 'FRDM1') + iv(16B) + tag(16B) + ciphertext
|
|
37
|
+
// 算法:AES-256-CTR 加密 + HMAC-SHA256(密文) 截断 16B 认证标签。
|
|
38
|
+
const APP_BIN_MAGIC = 'FRDM1';
|
|
39
|
+
const CTR_IV_LEN = 16;
|
|
40
|
+
const CTR_TAG_LEN = 16;
|
|
41
|
+
|
|
42
|
+
// ---- 模式解析 ----
|
|
43
|
+
|
|
44
|
+
// 解析安全模式;非法值抛错。opts.security 优先于 cfg.security,默认 none。
|
|
45
|
+
function parseSecurity(raw) {
|
|
46
|
+
if (!raw) return 'none';
|
|
47
|
+
const v = String(raw).trim().toLowerCase();
|
|
48
|
+
if (!SECURITY_MODES.includes(v)) {
|
|
49
|
+
throw new Error(`未知安全模式:${raw}。可选:${SECURITY_MODES.join(' / ')}`);
|
|
50
|
+
}
|
|
51
|
+
return v;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 归一化:CLI 显式参数 > 项目配置 > 默认 none
|
|
55
|
+
function resolveSecurity(cliValue, cfgValue) {
|
|
56
|
+
if (cliValue) return parseSecurity(cliValue);
|
|
57
|
+
return parseSecurity(cfgValue);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---- 密钥派生 ----
|
|
61
|
+
|
|
62
|
+
// 应用标识:exe 文件名(去扩展名)。
|
|
63
|
+
// CLI build 时 = 目标 exe 名去掉扩展名;壳运行时 = os.Executable() basename 去扩展名。
|
|
64
|
+
// 两侧使用同一派生算法,故 exe 事后被重命名会导致 high 模式无法解密(视为防篡改特性)。
|
|
65
|
+
function appIdentityFor(name) {
|
|
66
|
+
return String(name).replace(/\.(exe|app)$/i, '');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 与 Go security.go deriveKey 保持一致的 PBKDF2-HMAC-SHA256 派生。
|
|
70
|
+
// 输入 salt = DERIVE_SALT + ':' + appName(避免仅按 exe 名可枚举碰撞)。
|
|
71
|
+
function deriveKey(appName) {
|
|
72
|
+
const salt = Buffer.from(DERIVE_SALT + ':' + appIdentityFor(appName), 'utf8');
|
|
73
|
+
return crypto.pbkdf2Sync(MASTER_KEY, salt, PBKDF2_ITER, KEY_LEN, 'sha256');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---- 加密 / 解密(high 模式)----
|
|
77
|
+
|
|
78
|
+
// 载荷结构:{ html: <string>, config: <string> }(config 为 config.json 原文 JSON 字符串)。
|
|
79
|
+
// 返回 app.bin 完整字节。
|
|
80
|
+
// 算法:AES-256-CTR 加密 → HMAC-SHA256(密文) 截 16B 认证标签(Encrypt-then-MAC)。
|
|
81
|
+
function encryptApp(name, html, configJSON) {
|
|
82
|
+
const key = deriveKey(name);
|
|
83
|
+
const payload = Buffer.from(JSON.stringify({ html, config: configJSON }), 'utf8');
|
|
84
|
+
const iv = crypto.randomBytes(CTR_IV_LEN);
|
|
85
|
+
const cipher = crypto.createCipheriv('aes-256-ctr', key, iv);
|
|
86
|
+
const ct = Buffer.concat([cipher.update(payload), cipher.final()]);
|
|
87
|
+
const tag = crypto.createHmac('sha256', key).update(ct).digest().subarray(0, CTR_TAG_LEN);
|
|
88
|
+
return Buffer.concat([Buffer.from(APP_BIN_MAGIC, 'ascii'), iv, tag, ct]);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 解密 app.bin(供单测验证 / 未来壳调试工具复用)。返回 { html, config }。
|
|
92
|
+
function decryptApp(name, buf) {
|
|
93
|
+
const key = deriveKey(name);
|
|
94
|
+
const magic = Buffer.from(APP_BIN_MAGIC, 'ascii');
|
|
95
|
+
if (buf.length < magic.length + CTR_IV_LEN + CTR_TAG_LEN || !buf.subarray(0, magic.length).equals(magic)) {
|
|
96
|
+
throw new Error('app.bin 头无效:不是 FRDM1 加密容器');
|
|
97
|
+
}
|
|
98
|
+
let off = magic.length;
|
|
99
|
+
const iv = buf.subarray(off, off + CTR_IV_LEN); off += CTR_IV_LEN;
|
|
100
|
+
const tag = buf.subarray(off, off + CTR_TAG_LEN); off += CTR_TAG_LEN;
|
|
101
|
+
const ct = buf.subarray(off);
|
|
102
|
+
|
|
103
|
+
// 1) 认证:HMAC-SHA256(ct) 前 16 字节与容器头 tag 恒定时间比对。
|
|
104
|
+
const expect = crypto.createHmac('sha256', key).update(ct).digest().subarray(0, CTR_TAG_LEN);
|
|
105
|
+
if (!crypto.timingSafeEqual(tag, expect)) {
|
|
106
|
+
throw new Error('app.bin 认证失败(exe 被重命名或资源被篡改?)');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// 2) 解密:AES-256-CTR。
|
|
110
|
+
const decipher = crypto.createDecipheriv('aes-256-ctr', key, iv);
|
|
111
|
+
const plain = Buffer.concat([decipher.update(ct), decipher.final()]);
|
|
112
|
+
const obj = JSON.parse(plain.toString('utf8'));
|
|
113
|
+
if (typeof obj.html !== 'string' || typeof obj.config !== 'string') {
|
|
114
|
+
throw new Error('app.bin 载荷结构无效:缺少 html/config 字段');
|
|
115
|
+
}
|
|
116
|
+
return obj;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---- 完整性清单(high 模式)----
|
|
120
|
+
|
|
121
|
+
// 生成 resources/.integrity:记录 app.bin 与 backend/ 各文件的 HMAC-SHA256,
|
|
122
|
+
// 壳启动时校验,防止 resources 被整体替换/篡改。
|
|
123
|
+
// 返回 { v, appBin, backend }。
|
|
124
|
+
function buildIntegrity(appName, appBin, backendRelMap) {
|
|
125
|
+
const key = deriveKey(appName);
|
|
126
|
+
const hmac = (buf) => crypto.createHmac('sha256', key).update(buf).digest('hex');
|
|
127
|
+
const backend = {};
|
|
128
|
+
for (const [rel, buf] of Object.entries(backendRelMap || {})) {
|
|
129
|
+
backend[rel] = hmac(buf);
|
|
130
|
+
}
|
|
131
|
+
return { v: 1, appBin: hmac(appBin), backend };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 生成 .integrity 文件文本。
|
|
135
|
+
function renderIntegrity(integrity) {
|
|
136
|
+
return JSON.stringify(integrity, null, 2);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
SECURITY_MODES,
|
|
141
|
+
APP_BIN_MAGIC,
|
|
142
|
+
parseSecurity,
|
|
143
|
+
resolveSecurity,
|
|
144
|
+
appIdentityFor,
|
|
145
|
+
deriveKey,
|
|
146
|
+
encryptApp,
|
|
147
|
+
decryptApp,
|
|
148
|
+
buildIntegrity,
|
|
149
|
+
renderIntegrity,
|
|
150
|
+
};
|
package/lib/tui.js
CHANGED
|
@@ -213,7 +213,8 @@ async function buildFlow(tui, cwd) {
|
|
|
213
213
|
// 平台多选:默认勾选当前平台
|
|
214
214
|
const native = require('./utils').nativePlatform();
|
|
215
215
|
const checked = [ALL_PLATFORMS.indexOf(native)];
|
|
216
|
-
|
|
216
|
+
// linux-arm64 无 CI 预编译资产:全选时按 all(win-x64/darwin-arm64/linux-x64)处理,避免 404 拖垮构建(见 utils.DIST_PLATFORMS)
|
|
217
|
+
const picked = await tui.multiselect('选择目标平台(多选;linux-arm64 无预编译资产,全选仅含三平台)', ALL_PLATFORMS, { checked });
|
|
217
218
|
if (picked === null) return;
|
|
218
219
|
const platforms = picked.map((i) => ALL_PLATFORMS[i]);
|
|
219
220
|
const platArg = picked.length === ALL_PLATFORMS.length ? 'all' : platforms.join(',');
|
|
@@ -303,20 +304,36 @@ async function tutorialFlow(tui) {
|
|
|
303
304
|
}
|
|
304
305
|
|
|
305
306
|
async function updateFlow(tui) {
|
|
306
|
-
const
|
|
307
|
-
const r = await checkUpdate({ force: true });
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
307
|
+
const update = require('./update');
|
|
308
|
+
const r = await update.checkUpdate({ force: true });
|
|
309
|
+
const rows = [{ text: C.fgWhite + C.bold + ` 当前版本:v${r.current}` + C.reset }];
|
|
310
|
+
if (!r.latest) {
|
|
311
|
+
rows.push({ text: C.fgGray + ` 检查失败:网络不可用,请稍后重试` + C.reset });
|
|
312
|
+
return tui.message('检查 / 自动更新', rows);
|
|
313
|
+
}
|
|
314
|
+
rows.push({ text: C.fgCyan + ` 最新版本:v${r.latest}` + C.reset });
|
|
315
|
+
if (!r.hasUpdate) {
|
|
316
|
+
rows.push({ text: C.fgGreen + ` 已是最新版本` + C.reset });
|
|
317
|
+
return tui.message('检查 / 自动更新', rows);
|
|
318
|
+
}
|
|
319
|
+
// 有新版本:直接自动更新(无需用户手动执行 npm 命令)
|
|
320
|
+
let res = null;
|
|
321
|
+
await tui.runTask(async () => {
|
|
322
|
+
res = await update.maybeAutoUpdate({ force: true, r });
|
|
323
|
+
});
|
|
324
|
+
if (res && res.updated) {
|
|
325
|
+
rows.push({ text: C.fgGreen + ` ✓ 已自动更新到 v${res.to}(原 v${res.from})` + C.reset });
|
|
326
|
+
rows.push({ text: C.fgGray + ` 重启本 TUI 后即为新版本` + C.reset });
|
|
327
|
+
} else if (res && res.notGlobal) {
|
|
328
|
+
rows.push({ text: C.fgYellow + ` ⚠ 检测到新版本,当前为非全局安装,请手动执行:npm install -g ${update.pkgName()}@latest` + C.reset });
|
|
329
|
+
} else if (res && res.failed) {
|
|
330
|
+
rows.push({ text: C.fgYellow + ` ⚠ 自动更新失败:${res.detail || '未知原因'}` + C.reset });
|
|
331
|
+
} else if (res && res.throttled) {
|
|
332
|
+
rows.push({ text: C.fgYellow + ` ⚠ 自动更新 6h 内已尝试过,本次跳过` + C.reset });
|
|
316
333
|
} else {
|
|
317
|
-
|
|
334
|
+
rows.push({ text: C.fgGray + ` 自动更新流程已结束` + C.reset });
|
|
318
335
|
}
|
|
319
|
-
await tui.message('
|
|
336
|
+
await tui.message('检查 / 自动更新', rows);
|
|
320
337
|
}
|
|
321
338
|
|
|
322
339
|
function coerce(value) {
|
|
@@ -336,7 +353,7 @@ async function tui(cwd) {
|
|
|
336
353
|
app.enter();
|
|
337
354
|
let keep = true;
|
|
338
355
|
while (keep) {
|
|
339
|
-
const items = ['打包桌面应用', '新建项目', '修改配置', '壳管理', '打开教程', '
|
|
356
|
+
const items = ['打包桌面应用', '新建项目', '修改配置', '壳管理', '打开教程', '检查 / 自动更新', '退出'];
|
|
340
357
|
const idx = await app.menu('主菜单', items, {
|
|
341
358
|
footer: `工作目录:${cwd} ↑ ↓ 选择 · Enter 确认 · q 退出`,
|
|
342
359
|
});
|