@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.
Files changed (37) hide show
  1. package/README.md +38 -3
  2. package/lib/build.js +132 -12
  3. package/lib/cli.js +94 -4
  4. package/lib/config.js +12 -1
  5. package/lib/init.js +9 -1
  6. package/lib/security.js +150 -0
  7. package/lib/utils.js +5 -2
  8. package/lib/verify.js +236 -0
  9. package/package.json +39 -39
  10. package/shell/win-x64/freedom-shell.exe +0 -0
  11. package/templates/go/build_all.txt +0 -0
  12. package/templates/go/build_err.txt +0 -0
  13. package/templates/go/pkg/freedom/anti_debug_other.go +8 -0
  14. package/templates/go/pkg/freedom/anti_debug_windows.go +39 -0
  15. package/templates/go/pkg/freedom/assets/freedom.js +19 -16
  16. package/templates/go/pkg/freedom/backend_proc.go +7 -1
  17. package/templates/go/pkg/freedom/center_windows.go +36 -18
  18. package/templates/go/pkg/freedom/center_windows_test.go +32 -0
  19. package/templates/go/pkg/freedom/configfile.go +56 -7
  20. package/templates/go/pkg/freedom/freedom.go +70 -8
  21. package/templates/go/pkg/freedom/security.go +227 -0
  22. package/templates/go/pkg/freedom/security_test.go +153 -0
  23. package/templates/go/pkg/freedom/syscap_other.go +10 -0
  24. package/templates/go/pkg/freedom/syscap_windows.go +653 -0
  25. package/templates/go/pkg/freedom/tray_other.go +10 -0
  26. package/templates/go/pkg/freedom/tray_windows.go +506 -0
  27. package/templates/go/pkg/freedom/window_windows.go +28 -16
  28. package/templates/go/temp_vet_test/main.go +40 -0
  29. package/templates/go/test_err.txt +0 -0
  30. package/templates/go/vet_err.txt +1 -0
  31. package/templates/go/vet_err2.txt +2 -0
  32. package/templates/project/freedom.config.js +8 -0
  33. package/templates/project-minimal/freedom.config.js +58 -0
  34. package/templates/project-minimal/index.html +41 -0
  35. package/templates/project-minimal/package.json +14 -0
  36. package/templates/project-minimal/src/main.js +19 -0
  37. 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 保留系统原生标题栏(默认),标题栏图标与 exe 图标一致。
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 指定标题栏策略(默认 TitleBarNative)。可通过 freedom CLI 一键切换。
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 = TitleBarNative
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
- if a.onReady != nil {
223
- a.onReady(a)
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
- if html, err := loadRuntimeHTML(); err == nil && html != "" {
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
+ }