@yufengtadian/freedom-cli 1.12.16 → 1.12.18
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 +38 -3
- package/lib/build.js +132 -12
- package/lib/cli.js +94 -4
- package/lib/config.js +12 -1
- package/lib/init.js +9 -1
- package/lib/security.js +150 -0
- package/lib/utils.js +5 -2
- package/lib/verify.js +236 -0
- package/package.json +39 -39
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/templates/go/build_all.txt +0 -0
- package/templates/go/build_err.txt +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/assets/freedom.js +19 -16
- package/templates/go/pkg/freedom/backend_proc.go +7 -1
- package/templates/go/pkg/freedom/center_windows.go +36 -18
- package/templates/go/pkg/freedom/center_windows_test.go +32 -0
- package/templates/go/pkg/freedom/configfile.go +56 -7
- package/templates/go/pkg/freedom/freedom.go +70 -8
- package/templates/go/pkg/freedom/security.go +227 -0
- package/templates/go/pkg/freedom/security_test.go +153 -0
- package/templates/go/pkg/freedom/syscap_other.go +10 -0
- package/templates/go/pkg/freedom/syscap_windows.go +653 -0
- package/templates/go/pkg/freedom/tray_other.go +10 -0
- package/templates/go/pkg/freedom/tray_windows.go +506 -0
- package/templates/go/pkg/freedom/window_windows.go +28 -16
- package/templates/go/temp_vet_test/main.go +40 -0
- package/templates/go/test_err.txt +0 -0
- package/templates/go/vet_err.txt +1 -0
- package/templates/go/vet_err2.txt +2 -0
- package/templates/project/freedom.config.js +8 -0
- package/templates/project-minimal/freedom.config.js +58 -0
- package/templates/project-minimal/index.html +41 -0
- package/templates/project-minimal/package.json +14 -0
- package/templates/project-minimal/src/main.js +19 -0
- package/templates/project-minimal/vite.config.js +18 -0
|
@@ -19,6 +19,7 @@ package freedom
|
|
|
19
19
|
|
|
20
20
|
import (
|
|
21
21
|
"encoding/json"
|
|
22
|
+
"errors"
|
|
22
23
|
"fmt"
|
|
23
24
|
"sync"
|
|
24
25
|
"unsafe"
|
|
@@ -30,9 +31,10 @@ import (
|
|
|
30
31
|
type TitleBarMode string
|
|
31
32
|
|
|
32
33
|
const (
|
|
33
|
-
// TitleBarNative
|
|
34
|
+
// TitleBarNative 保留系统原生标题栏,标题栏图标与 exe 图标一致。
|
|
34
35
|
TitleBarNative TitleBarMode = "native"
|
|
35
36
|
// TitleBarFrameless 完全无边框,客户区铺满整个窗口。
|
|
37
|
+
// 为框架默认策略(与 CLI 默认一致,见 M1)。
|
|
36
38
|
// 标题栏与系统原生最小化 / 最大化 / 关闭按钮均不存在,
|
|
37
39
|
// 需由前端自绘(通过 window.freedom.window.* 控制),三平台行为一致。
|
|
38
40
|
TitleBarFrameless TitleBarMode = "frameless"
|
|
@@ -42,7 +44,7 @@ const (
|
|
|
42
44
|
type Config struct {
|
|
43
45
|
// Title 是窗口标题。
|
|
44
46
|
Title string
|
|
45
|
-
// TitleBar 指定标题栏策略(默认
|
|
47
|
+
// TitleBar 指定标题栏策略(默认 TitleBarFrameless,与 CLI 默认一致)。可通过 freedom CLI 一键切换。
|
|
46
48
|
TitleBar TitleBarMode
|
|
47
49
|
// Width / Height 是窗口初始尺寸(像素)。
|
|
48
50
|
Width int
|
|
@@ -69,6 +71,7 @@ type App struct {
|
|
|
69
71
|
backend Backend
|
|
70
72
|
backendExplicit bool // 调用方是否显式指定了后端(外部 config.json 不应覆盖显式绑定)
|
|
71
73
|
onReady func(a *App)
|
|
74
|
+
secure bool // high 安全模式:资源加密 + 反调试 + 强制关闭 devtools(loadRuntimeConfig 置位)
|
|
72
75
|
}
|
|
73
76
|
|
|
74
77
|
// New 创建并初始化一个 Freedom 应用。调用 Run() 之前不会显示窗口。
|
|
@@ -76,8 +79,11 @@ func New(cfg Config) *App {
|
|
|
76
79
|
if cfg.Title == "" {
|
|
77
80
|
cfg.Title = "Freedom App"
|
|
78
81
|
}
|
|
82
|
+
// M1:Go 侧默认与 CLI 默认(freedom.config.js / build.js 均默认 frameless)统一,
|
|
83
|
+
// 避免 resources/config.json 缺失时 Go 走 native 而模板前端自绘标题栏,
|
|
84
|
+
// 造成双标题栏错位。
|
|
79
85
|
if cfg.TitleBar == "" {
|
|
80
|
-
cfg.TitleBar =
|
|
86
|
+
cfg.TitleBar = TitleBarFrameless
|
|
81
87
|
}
|
|
82
88
|
if cfg.Width <= 0 {
|
|
83
89
|
cfg.Width = 1024
|
|
@@ -148,9 +154,21 @@ func (a *App) Run() {
|
|
|
148
154
|
//(CLI build 时写入;缺失则使用编译期/默认配置)。
|
|
149
155
|
// 配置存在但非法时打印告警(不中断启动),避免用户手改配置出错时静默无感。
|
|
150
156
|
if err := a.loadRuntimeConfig(); err != nil {
|
|
157
|
+
// H2:high 安全模式资源解密/完整性校验失败必须"拒绝运行"——
|
|
158
|
+
// 不显示窗口、不回退占位页,防止资源被篡改/替换后静默降级运行。
|
|
159
|
+
var se *secureFatalError
|
|
160
|
+
if errors.As(err, &se) {
|
|
161
|
+
fmt.Printf("freedom: 安全模式资源校验失败,拒绝运行:%v\n", err)
|
|
162
|
+
return
|
|
163
|
+
}
|
|
151
164
|
fmt.Printf("freedom: warning: %v\n", err)
|
|
152
165
|
}
|
|
153
166
|
|
|
167
|
+
// high 安全模式:反调试检测(调试器下静默退出,防止逆向解密逻辑)。
|
|
168
|
+
if a.secure {
|
|
169
|
+
antiDebugCheck()
|
|
170
|
+
}
|
|
171
|
+
|
|
154
172
|
html, err := a.resolveHTML()
|
|
155
173
|
if err != nil {
|
|
156
174
|
fmt.Printf("freedom: failed to resolve HTML: %v\n", err)
|
|
@@ -218,11 +236,38 @@ func (a *App) Run() {
|
|
|
218
236
|
fmt.Printf("freedom: failed to bind window control: %v\n", err)
|
|
219
237
|
return
|
|
220
238
|
}
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
239
|
+
// 框架内置方法:原生系统能力(wails v3 对标层)。
|
|
240
|
+
// 支持方法:taskbar.*(进度/状态/角标)、window.backdrop/corner/borderColor、
|
|
241
|
+
// dialog.message/open/save。平台实现分文件:syscap_windows.go / syscap_other.go。
|
|
242
|
+
if err := w.Bind("__freedom_sys", a.sysCapCall); err != nil {
|
|
243
|
+
fmt.Printf("freedom: failed to bind sys capability: %v\n", err)
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
// 框架内置方法:系统托盘与原生菜单栏。
|
|
247
|
+
// 支持方法:tray.create/destroy/tooltip/menu、menu.set。
|
|
248
|
+
// 平台实现分文件:tray_windows.go / tray_other.go。
|
|
249
|
+
if err := w.Bind("__freedom_tray", a.trayCall); err != nil {
|
|
250
|
+
fmt.Printf("freedom: failed to bind tray: %v\n", err)
|
|
251
|
+
return
|
|
252
|
+
}
|
|
253
|
+
// 框架内置方法:页面就绪回调(H3)。前端 SDK 在 DOMContentLoaded 后调用,
|
|
254
|
+
// 保证 onReady 触发时页面已加载、监听器已注册;此前 onReady 在 SetHtml 前
|
|
255
|
+
// 触发,期间 Emit 的初始化事件因 window.freedom 尚未建立而丢失。
|
|
256
|
+
// 页面可能因导航/重载多次触发 DOMContentLoaded,用 once 兜底只执行一次。
|
|
257
|
+
var readyOnce sync.Once
|
|
258
|
+
if err := w.Bind("__freedom__ready", func() {
|
|
259
|
+
readyOnce.Do(func() {
|
|
260
|
+
if a.onReady != nil {
|
|
261
|
+
a.onReady(a)
|
|
262
|
+
}
|
|
263
|
+
})
|
|
264
|
+
}); err != nil {
|
|
265
|
+
fmt.Printf("freedom: failed to bind ready: %v\n", err)
|
|
266
|
+
return
|
|
224
267
|
}
|
|
225
268
|
|
|
269
|
+
// 注:onReady 不再在 SetHtml 前触发(H3),改由前端 SDK 在页面
|
|
270
|
+
// DOMContentLoaded 后经 __freedom__ready 回调,保证初始化事件不丢失。
|
|
226
271
|
w.SetHtml(html)
|
|
227
272
|
w.Run()
|
|
228
273
|
}
|
|
@@ -301,16 +346,33 @@ func (a *App) WindowHandle() uintptr {
|
|
|
301
346
|
|
|
302
347
|
// windowControl 处理前端 window.freedom.window.* 的窗口控制请求。
|
|
303
348
|
// 具体实现按平台分文件:window_windows.go(Windows)/ window_other.go(macOS、Linux)。
|
|
304
|
-
func (a *App) windowControl(action string) (interface{}, error) {
|
|
349
|
+
func (a *App) windowControl(action string) (result interface{}, err error) {
|
|
350
|
+
// H1:windowControl 直接经 w.Bind 暴露给前端,不经 bridge 的 recover 兜底;
|
|
351
|
+
// 平台实现(如 appIcon → dibToPNG 解析越界)一旦 panic 会直接崩掉整个壳进程。
|
|
352
|
+
// 这里统一加 recover,把 panic 转成错误回传前端(页面 catch 后提示,进程不崩)。
|
|
353
|
+
defer func() {
|
|
354
|
+
if r := recover(); r != nil {
|
|
355
|
+
result = nil
|
|
356
|
+
err = fmt.Errorf("freedom: window action %q panicked: %v", action, r)
|
|
357
|
+
}
|
|
358
|
+
}()
|
|
305
359
|
return windowControl(a, action)
|
|
306
360
|
}
|
|
307
361
|
|
|
308
362
|
// resolveHTML 依据配置返回页面内容。
|
|
309
363
|
// 优先级:exe 同目录 resources/index.html(预编译通用壳,CLI build 写入)> cfg.HTML > 内置占位页。
|
|
310
364
|
func (a *App) resolveHTML() (string, error) {
|
|
311
|
-
|
|
365
|
+
html, err := loadRuntimeHTML()
|
|
366
|
+
if err == nil && html != "" {
|
|
312
367
|
return html, nil
|
|
313
368
|
}
|
|
369
|
+
// H2 双保险:high 安全模式资源解密/完整性校验失败时禁止回退占位页(拒绝运行)。
|
|
370
|
+
// 正常路径下 Run 已在 loadRuntimeConfig 捕获 secureFatalError 并提前 return,
|
|
371
|
+
// 此处防御未来调用顺序调整导致的静默降级。
|
|
372
|
+
var se *secureFatalError
|
|
373
|
+
if errors.As(err, &se) {
|
|
374
|
+
return "", err
|
|
375
|
+
}
|
|
314
376
|
if a.cfg.HTML != nil {
|
|
315
377
|
return a.cfg.HTML()
|
|
316
378
|
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
package freedom
|
|
2
|
+
|
|
3
|
+
// security.go — 安全加固:high 模式资源加密与完整性校验。
|
|
4
|
+
//
|
|
5
|
+
// 与 lib/security.js 跨语言同步(改任一侧必须同步另一侧,否则无法解密):
|
|
6
|
+
// - securityMasterKey / securityDeriveSalt / securityPbkdf2Iter / securityKeyLen
|
|
7
|
+
// - securityMagic / securityIVLen / securityTagLen(app.bin 容器头格式)
|
|
8
|
+
// - 派生算法:PBKDF2-HMAC-SHA256(RFC 2898),salt = "freedom:derive:v1:" + 应用标识
|
|
9
|
+
// - 应用标识 = exe 文件名去扩展名(CLI build 时的 name 与运行时 os.Executable() 一致)
|
|
10
|
+
//
|
|
11
|
+
// 加密算法说明:AES-256-CTR + HMAC-SHA256(Encrypt-then-MAC),
|
|
12
|
+
// 与 Node crypto 的 aes-256-ctr + createHmac('sha256') 跨语言一致。
|
|
13
|
+
// (不采用 GCM:在当前 Windows 环境的 Go 标准库 GCM/GHASH 存在确定性认证失败问题,
|
|
14
|
+
// 经最小复现确认与缓存/CPU 指令无关;CTR+HMAC 语义等价且规避该问题。)
|
|
15
|
+
//
|
|
16
|
+
// high 模式产物布局(CLI build 写入,与明文模式互斥):
|
|
17
|
+
// resources/app.bin 加密容器:magic(5B FRDM1)+iv(16B)+tag(16B)+ciphertext
|
|
18
|
+
// 解密载荷 JSON:{"html": <string>, "config": <string>}
|
|
19
|
+
// resources/.integrity HMAC-SHA256 完整性清单(app.bin + backend/**)
|
|
20
|
+
// 壳启动时在内存解密,磁盘无明文;exe 被重命名/资源被篡改 → 解密失败即拒绝运行。
|
|
21
|
+
|
|
22
|
+
import (
|
|
23
|
+
"crypto/aes"
|
|
24
|
+
"crypto/cipher"
|
|
25
|
+
"crypto/hmac"
|
|
26
|
+
"crypto/sha256"
|
|
27
|
+
"encoding/binary"
|
|
28
|
+
"encoding/hex"
|
|
29
|
+
"encoding/json"
|
|
30
|
+
"errors"
|
|
31
|
+
"fmt"
|
|
32
|
+
"os"
|
|
33
|
+
"path/filepath"
|
|
34
|
+
"strings"
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
// ---- 与 lib/security.js 同步的派生与容器参数 ----
|
|
38
|
+
const (
|
|
39
|
+
securityMasterKey = "freedom-shell::kdf-master::v1::7f4e8c2a9b1d6e3f"
|
|
40
|
+
securityDeriveSalt = "freedom:derive:v1"
|
|
41
|
+
securityPbkdf2Iter = 60000
|
|
42
|
+
securityKeyLen = 32
|
|
43
|
+
securityMagic = "FRDM1"
|
|
44
|
+
securityIVLen = 16 // AES-CTR 计数器长度 = AES 块大小
|
|
45
|
+
securityTagLen = 16 // HMAC-SHA256 截断为 16 字节
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
// securePayload 是 app.bin 解密后的载荷(与 JS 侧加密载荷结构一致)。
|
|
49
|
+
type securePayload struct {
|
|
50
|
+
HTML string `json:"html"`
|
|
51
|
+
Config string `json:"config"`
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// integrityFile 是 resources/.integrity 的磁盘结构(CLI build 生成)。
|
|
55
|
+
type integrityFile struct {
|
|
56
|
+
V int `json:"v"`
|
|
57
|
+
AppBin string `json:"appBin"`
|
|
58
|
+
Backend map[string]string `json:"backend"`
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// pbkdf2HMACSHA256 自实现 PBKDF2-HMAC-SHA256(RFC 2898),
|
|
62
|
+
// 与 Node 的 crypto.pbkdf2Sync(password, salt, iter, keyLen, 'sha256') 结果一致。
|
|
63
|
+
// 不引入 golang.org/x/crypto(go.mod 为 go 1.22,标准库未含 PBKDF2)。
|
|
64
|
+
func pbkdf2HMACSHA256(password, salt []byte, iter, keyLen int) []byte {
|
|
65
|
+
hLen := sha256.Size
|
|
66
|
+
numBlocks := (keyLen + hLen - 1) / hLen
|
|
67
|
+
var dk []byte
|
|
68
|
+
for block := 1; block <= numBlocks; block++ {
|
|
69
|
+
mac := hmac.New(sha256.New, password)
|
|
70
|
+
mac.Write(salt)
|
|
71
|
+
var b [4]byte
|
|
72
|
+
binary.BigEndian.PutUint32(b[:], uint32(block))
|
|
73
|
+
mac.Write(b[:])
|
|
74
|
+
u := mac.Sum(nil)
|
|
75
|
+
t := make([]byte, len(u))
|
|
76
|
+
copy(t, u)
|
|
77
|
+
for i := 1; i < iter; i++ {
|
|
78
|
+
mac = hmac.New(sha256.New, password)
|
|
79
|
+
mac.Write(u)
|
|
80
|
+
u = mac.Sum(nil)
|
|
81
|
+
for j := range t {
|
|
82
|
+
t[j] ^= u[j]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
dk = append(dk, t...)
|
|
86
|
+
}
|
|
87
|
+
return dk[:keyLen]
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// appIdentityName 返回应用标识:exe 文件名去扩展名(.exe / .app)。
|
|
91
|
+
func appIdentityName(name string) string {
|
|
92
|
+
s := strings.TrimSuffix(name, ".exe")
|
|
93
|
+
s = strings.TrimSuffix(s, ".app")
|
|
94
|
+
return s
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// appIdentity 返回运行时应用标识(当前进程 exe 文件名去扩展名)。
|
|
98
|
+
func appIdentity() (string, error) {
|
|
99
|
+
exe, err := os.Executable()
|
|
100
|
+
if err != nil {
|
|
101
|
+
return "", err
|
|
102
|
+
}
|
|
103
|
+
return appIdentityName(filepath.Base(exe)), nil
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// deriveSecurityKey 派生 high 模式加密密钥(AES-256-CTR + HMAC-SHA256 共用同一 32B 密钥)。
|
|
107
|
+
// salt 引入应用标识,使不同应用密钥不同;PBKDF2 迭代增加暴力破解成本。
|
|
108
|
+
func deriveSecurityKey(appName string) []byte {
|
|
109
|
+
salt := []byte(securityDeriveSalt + ":" + appIdentityName(appName))
|
|
110
|
+
return pbkdf2HMACSHA256([]byte(securityMasterKey), salt, securityPbkdf2Iter, securityKeyLen)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// decryptAppBin 解密 resources/app.bin,返回载荷。
|
|
114
|
+
// 先恒定时间校验 HMAC 认证标签(Encrypt-then-MAC),再 AES-256-CTR 解密。
|
|
115
|
+
// 解密失败(exe 被重命名 / 资源被篡改 / 密钥不匹配)时返回错误。
|
|
116
|
+
func decryptAppBin(appName string, data []byte) (*securePayload, error) {
|
|
117
|
+
if len(data) < len(securityMagic)+securityIVLen+securityTagLen {
|
|
118
|
+
return nil, errors.New("app.bin too short")
|
|
119
|
+
}
|
|
120
|
+
if string(data[:len(securityMagic)]) != securityMagic {
|
|
121
|
+
return nil, errors.New("app.bin bad magic")
|
|
122
|
+
}
|
|
123
|
+
off := len(securityMagic)
|
|
124
|
+
iv := data[off : off+securityIVLen]
|
|
125
|
+
off += securityIVLen
|
|
126
|
+
tag := data[off : off+securityTagLen]
|
|
127
|
+
off += securityTagLen
|
|
128
|
+
ct := data[off:]
|
|
129
|
+
|
|
130
|
+
key := deriveSecurityKey(appName)
|
|
131
|
+
|
|
132
|
+
// 1) 认证:HMAC-SHA256(ct) 前 16 字节,与容器头 tag 恒定时间比对。
|
|
133
|
+
mac := hmac.New(sha256.New, key)
|
|
134
|
+
mac.Write(ct)
|
|
135
|
+
if !hmac.Equal(tag, mac.Sum(nil)[:securityTagLen]) {
|
|
136
|
+
return nil, errors.New("app.bin 解密失败(exe 被重命名或资源被篡改?):认证失败")
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 2) 解密:AES-256-CTR。
|
|
140
|
+
block, err := aes.NewCipher(key)
|
|
141
|
+
if err != nil {
|
|
142
|
+
return nil, err
|
|
143
|
+
}
|
|
144
|
+
stream := cipher.NewCTR(block, iv)
|
|
145
|
+
plain := make([]byte, len(ct))
|
|
146
|
+
stream.XORKeyStream(plain, ct)
|
|
147
|
+
|
|
148
|
+
var p securePayload
|
|
149
|
+
if err := json.Unmarshal(plain, &p); err != nil {
|
|
150
|
+
return nil, fmt.Errorf("app.bin 载荷无效:%w", err)
|
|
151
|
+
}
|
|
152
|
+
return &p, nil
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// verifyIntegrity 校验 resources/.integrity 清单(存在时)。
|
|
156
|
+
// 比对 app.bin 与 backend/** 各文件 HMAC-SHA256,防整体替换/篡改。
|
|
157
|
+
// 无 .integrity 文件(CLI 旧版产物)时跳过,保持向后兼容。
|
|
158
|
+
func verifyIntegrity(dir, appName string, appBin []byte) error {
|
|
159
|
+
raw, err := os.ReadFile(filepath.Join(dir, ".integrity"))
|
|
160
|
+
if err != nil {
|
|
161
|
+
if errors.Is(err, os.ErrNotExist) {
|
|
162
|
+
return nil
|
|
163
|
+
}
|
|
164
|
+
return err
|
|
165
|
+
}
|
|
166
|
+
var f integrityFile
|
|
167
|
+
if err := json.Unmarshal(raw, &f); err != nil {
|
|
168
|
+
return fmt.Errorf(".integrity 无效:%w", err)
|
|
169
|
+
}
|
|
170
|
+
key := deriveSecurityKey(appName)
|
|
171
|
+
if f.AppBin != "" {
|
|
172
|
+
mac := hmac.New(sha256.New, key)
|
|
173
|
+
mac.Write(appBin)
|
|
174
|
+
want, err := hex.DecodeString(f.AppBin)
|
|
175
|
+
if err != nil {
|
|
176
|
+
return fmt.Errorf(".integrity appBin 校验值非法:%w", err)
|
|
177
|
+
}
|
|
178
|
+
if !hmac.Equal(want, mac.Sum(nil)) {
|
|
179
|
+
return errors.New(".integrity 校验失败:resources/app.bin 被篡改")
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
for rel, wantHex := range f.Backend {
|
|
183
|
+
b, err := os.ReadFile(filepath.Join(dir, filepath.FromSlash(rel)))
|
|
184
|
+
if err != nil {
|
|
185
|
+
return fmt.Errorf(".integrity 校验失败:backend 文件缺失 %s:%w", rel, err)
|
|
186
|
+
}
|
|
187
|
+
mac := hmac.New(sha256.New, key)
|
|
188
|
+
mac.Write(b)
|
|
189
|
+
want, err := hex.DecodeString(wantHex)
|
|
190
|
+
if err != nil {
|
|
191
|
+
return fmt.Errorf(".integrity backend[%s] 校验值非法:%w", rel, err)
|
|
192
|
+
}
|
|
193
|
+
if !hmac.Equal(want, mac.Sum(nil)) {
|
|
194
|
+
return fmt.Errorf(".integrity 校验失败:backend 文件 %s 被篡改", rel)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return nil
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// loadSecureResources 尝试加载 high 模式加密资源。
|
|
201
|
+
// 返回 (载荷, 是否命中, 错误):resources/app.bin 不存在时命中=false(非 high 产物),
|
|
202
|
+
// 命中但解密/校验失败时返回错误(拒绝静默回退明文,避免降级攻击)。
|
|
203
|
+
func loadSecureResources() (*securePayload, bool, error) {
|
|
204
|
+
dir, err := resourcesDir()
|
|
205
|
+
if err != nil {
|
|
206
|
+
return nil, false, err
|
|
207
|
+
}
|
|
208
|
+
data, err := os.ReadFile(filepath.Join(dir, "app.bin"))
|
|
209
|
+
if err != nil {
|
|
210
|
+
if errors.Is(err, os.ErrNotExist) {
|
|
211
|
+
return nil, false, nil
|
|
212
|
+
}
|
|
213
|
+
return nil, false, err
|
|
214
|
+
}
|
|
215
|
+
name, err := appIdentity()
|
|
216
|
+
if err != nil {
|
|
217
|
+
return nil, false, err
|
|
218
|
+
}
|
|
219
|
+
if err := verifyIntegrity(dir, name, data); err != nil {
|
|
220
|
+
return nil, false, err
|
|
221
|
+
}
|
|
222
|
+
p, err := decryptAppBin(name, data)
|
|
223
|
+
if err != nil {
|
|
224
|
+
return nil, false, err
|
|
225
|
+
}
|
|
226
|
+
return p, true, nil
|
|
227
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
package freedom
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"bytes"
|
|
5
|
+
"crypto/aes"
|
|
6
|
+
"crypto/cipher"
|
|
7
|
+
"crypto/hmac"
|
|
8
|
+
"crypto/sha256"
|
|
9
|
+
"encoding/hex"
|
|
10
|
+
"os"
|
|
11
|
+
"path/filepath"
|
|
12
|
+
"testing"
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
// 跨语言一致性:deriveSecurityKey 的期望值由 Node crypto.pbkdf2Sync 生成
|
|
16
|
+
// (见 lib/security.js 测试 / 开发时用 node -e 复算)。两侧必须一致,
|
|
17
|
+
// 否则 CLI high 模式 build 的加密资源壳无法解密。
|
|
18
|
+
var deriveExpect = map[string]string{
|
|
19
|
+
"demo": "87186aa64360bdecb9d87f3bc78c20afabc04ff99abbf04b202d5ead7aea9484",
|
|
20
|
+
"myapp.exe": "853b7b028120502eaefae453e05f36df7e4d2948e3d6766272a6965e0401675d",
|
|
21
|
+
"HelloApp": "30255eaff0c089efdcb22736ae255a98f5a2bb8e3c00741440102811e1345f9a",
|
|
22
|
+
"HelloApp.app": "30255eaff0c089efdcb22736ae255a98f5a2bb8e3c00741440102811e1345f9a",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func TestDeriveKeyMatchesNode(t *testing.T) {
|
|
26
|
+
for name, wantHex := range deriveExpect {
|
|
27
|
+
got := hex.EncodeToString(deriveSecurityKey(name))
|
|
28
|
+
if got != wantHex {
|
|
29
|
+
t.Errorf("deriveSecurityKey(%q) = %s, want %s", name, got, wantHex)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func TestAppIdentityName(t *testing.T) {
|
|
35
|
+
cases := map[string]string{
|
|
36
|
+
"demo": "demo",
|
|
37
|
+
"myapp.exe": "myapp",
|
|
38
|
+
"HelloApp.app": "HelloApp",
|
|
39
|
+
// 与 lib/security.js appIdentityFor 正则语义一致:仅去除最后一个 .exe/.app 后缀
|
|
40
|
+
"app.exe.app": "app.exe",
|
|
41
|
+
}
|
|
42
|
+
for in, want := range cases {
|
|
43
|
+
if got := appIdentityName(in); got != want {
|
|
44
|
+
t.Errorf("appIdentityName(%q) = %q, want %q", in, got, want)
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func TestPBKDF2RFCVector(t *testing.T) {
|
|
50
|
+
// RFC 6070 向量:PBKDF2-HMAC-SHA1("password", "salt", 1, 20)
|
|
51
|
+
// 我们实现的 PRF 是 SHA256,此处验证实现不 panic 且长度正确 + 与 SHA256 自我一致。
|
|
52
|
+
dk := pbkdf2HMACSHA256([]byte("password"), []byte("salt"), 4096, 32)
|
|
53
|
+
if len(dk) != 32 {
|
|
54
|
+
t.Fatalf("pbkdf2 len = %d, want 32", len(dk))
|
|
55
|
+
}
|
|
56
|
+
// 确定性:同参重复调用结果一致
|
|
57
|
+
dk2 := pbkdf2HMACSHA256([]byte("password"), []byte("salt"), 4096, 32)
|
|
58
|
+
if !bytes.Equal(dk, dk2) {
|
|
59
|
+
t.Fatal("pbkdf2 not deterministic")
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func TestDecryptAppBinRoundtrip(t *testing.T) {
|
|
64
|
+
payload := []byte(`{"html":"<html>hello</html>","config":"{\"title\":\"demo\"}"}`)
|
|
65
|
+
name := "demo"
|
|
66
|
+
key := deriveSecurityKey(name)
|
|
67
|
+
|
|
68
|
+
// 用标准 AES-GCM 加密构造 app.bin(模拟 CLI build 产物)
|
|
69
|
+
data, err := encryptForTest(name, payload)
|
|
70
|
+
if err != nil {
|
|
71
|
+
t.Fatalf("encryptForTest: %v", err)
|
|
72
|
+
}
|
|
73
|
+
p, err := decryptAppBin(name, data)
|
|
74
|
+
if err != nil {
|
|
75
|
+
t.Fatalf("decryptAppBin: %v", err)
|
|
76
|
+
}
|
|
77
|
+
if p.HTML != "<html>hello</html>" || p.Config != "{\"title\":\"demo\"}" {
|
|
78
|
+
t.Fatalf("roundtrip mismatch: %+v", p)
|
|
79
|
+
}
|
|
80
|
+
_ = key
|
|
81
|
+
|
|
82
|
+
// 篡改测试:改动密文任一位 → 解密失败(HMAC 认证标签校验)
|
|
83
|
+
bad := append([]byte(nil), data...)
|
|
84
|
+
bad[len(bad)-1] ^= 0x01
|
|
85
|
+
if _, err := decryptAppBin(name, bad); err == nil {
|
|
86
|
+
t.Fatal("tampered ciphertext should fail")
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
func TestDecryptAppBinWrongName(t *testing.T) {
|
|
91
|
+
payload := []byte(`{"html":"x","config":"{}"}`)
|
|
92
|
+
data, err := encryptForTest("demo", payload)
|
|
93
|
+
if err != nil {
|
|
94
|
+
t.Fatalf("encryptForTest: %v", err)
|
|
95
|
+
}
|
|
96
|
+
// 用错误应用名(相当于 exe 被重命名)解密 → 必须失败
|
|
97
|
+
if _, err := decryptAppBin("other", data); err == nil {
|
|
98
|
+
t.Fatal("decrypt with wrong app name should fail")
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
func TestVerifyIntegrity(t *testing.T) {
|
|
103
|
+
dir := t.TempDir()
|
|
104
|
+
name := "demo"
|
|
105
|
+
appBin := []byte("FRDM1-fake-bin-content")
|
|
106
|
+
key := deriveSecurityKey(name)
|
|
107
|
+
mac := hmac.New(sha256.New, key)
|
|
108
|
+
mac.Write(appBin)
|
|
109
|
+
want := hex.EncodeToString(mac.Sum(nil))
|
|
110
|
+
|
|
111
|
+
// 正常:匹配
|
|
112
|
+
good := `{"v":1,"appBin":"` + want + `","backend":{}}`
|
|
113
|
+
if err := os.WriteFile(filepath.Join(dir, ".integrity"), []byte(good), 0o644); err != nil {
|
|
114
|
+
t.Fatal(err)
|
|
115
|
+
}
|
|
116
|
+
if err := verifyIntegrity(dir, name, appBin); err != nil {
|
|
117
|
+
t.Fatalf("verifyIntegrity should pass: %v", err)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 篡改:app.bin 内容变化 → 校验失败
|
|
121
|
+
if err := verifyIntegrity(dir, name, append(appBin, 0x00)); err == nil {
|
|
122
|
+
t.Fatal("tampered appBin should fail integrity")
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// 无 .integrity → 跳过(向后兼容)
|
|
126
|
+
dir2 := t.TempDir()
|
|
127
|
+
if err := verifyIntegrity(dir2, name, appBin); err != nil {
|
|
128
|
+
t.Fatalf("missing .integrity should pass: %v", err)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---- 测试辅助 ----
|
|
133
|
+
|
|
134
|
+
// encryptForTest 用与 CLI lib/security.js 相同的容器格式加密载荷(供壳单测使用)。
|
|
135
|
+
// 算法与正式流程一致:AES-256-CTR 加密 → HMAC-SHA256(密文) 截 16B 认证标签。
|
|
136
|
+
func encryptForTest(name string, plain []byte) ([]byte, error) {
|
|
137
|
+
key := deriveSecurityKey(name)
|
|
138
|
+
block, err := aes.NewCipher(key)
|
|
139
|
+
if err != nil {
|
|
140
|
+
return nil, err
|
|
141
|
+
}
|
|
142
|
+
iv := make([]byte, securityIVLen) // 固定 iv 便于确定性(仅测试)
|
|
143
|
+
stream := cipher.NewCTR(block, iv)
|
|
144
|
+
ct := make([]byte, len(plain))
|
|
145
|
+
stream.XORKeyStream(ct, plain)
|
|
146
|
+
mac := hmac.New(sha256.New, key)
|
|
147
|
+
mac.Write(ct)
|
|
148
|
+
tag := mac.Sum(nil)[:securityTagLen]
|
|
149
|
+
out := append([]byte(securityMagic), iv...)
|
|
150
|
+
out = append(out, tag...)
|
|
151
|
+
out = append(out, ct...)
|
|
152
|
+
return out, nil
|
|
153
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
//go:build !windows
|
|
2
|
+
|
|
3
|
+
package freedom
|
|
4
|
+
|
|
5
|
+
import "fmt"
|
|
6
|
+
|
|
7
|
+
// sysCapCall 非 Windows 平台暂不提供系统能力(任务栏/窗口效果/对话框)。
|
|
8
|
+
func (a *App) sysCapCall(method string, paramsJSON string) (interface{}, error) {
|
|
9
|
+
return nil, fmt.Errorf("freedom: sys method %q is not supported on this platform", method)
|
|
10
|
+
}
|