@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.
@@ -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
+ }
@@ -24,6 +24,10 @@ func (a *App) applyTitleBar() {
24
24
  // setWindowIcon 在 macOS / Linux 上为空实现(图标由应用包 / 窗口管理器决定)。
25
25
  func (a *App) setWindowIcon() {}
26
26
 
27
+ // webview2Available 非 Windows 平台恒返回 true(macOS WKWebView / Linux WebKitGTK,
28
+ // 不依赖 WebView2 Runtime)。
29
+ func webview2Available() bool { return true }
30
+
27
31
  // windowControl 处理前端 window.freedom.window.* 请求(macOS / Linux 实现)。
28
32
  //
29
33
  // 窗口控制类动作(最小化/最大化/还原/关闭/查询)直接转发到 webview 层的原生
@@ -13,6 +13,7 @@ import (
13
13
  "syscall"
14
14
 
15
15
  "golang.org/x/sys/windows"
16
+ "golang.org/x/sys/windows/registry"
16
17
  )
17
18
 
18
19
  // 进程级 DPI 感知:让窗口坐标 / WebView 渲染统一按物理像素工作。
@@ -232,6 +233,25 @@ func exeAppIconDataURL() string {
232
233
  return "data:image/png;base64," + base64.StdEncoding.EncodeToString(img)
233
234
  }
234
235
 
236
+ // webview2Available 检测 Windows 是否已安装 WebView2 Runtime。
237
+ // webview_create(WEBVIEW_EDGE)失败的最常见原因是缺少 WebView2 Runtime,
238
+ // 检测其安装注册表键(EdgeUpdate Clients GUID)判断状态,便于给出可操作提示。
239
+ // 返回值仅用于决定错误文案,不参与窗口创建逻辑。
240
+ func webview2Available() bool {
241
+ guid := `{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}`
242
+ key := `SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\` + guid
243
+ if k, err := registry.OpenKey(registry.LOCAL_MACHINE, key, registry.QUERY_VALUE|registry.READ); err == nil {
244
+ k.Close()
245
+ return true
246
+ }
247
+ key2 := `SOFTWARE\Microsoft\EdgeUpdate\Clients\` + guid
248
+ if k, err := registry.OpenKey(registry.LOCAL_MACHINE, key2, registry.QUERY_VALUE|registry.READ); err == nil {
249
+ k.Close()
250
+ return true
251
+ }
252
+ return false
253
+ }
254
+
235
255
  func isZoomed(hwnd uintptr) bool {
236
256
  r, _, _ := procIsZoomed.Call(hwnd)
237
257
  return r != 0
@@ -199,6 +199,13 @@ func New(debug bool) WebView { return NewWindow(debug, nil) }
199
199
  func NewWindow(debug bool, window unsafe.Pointer) WebView {
200
200
  w := &webview{}
201
201
  w.w = C.webview_create(boolToInt(debug), window)
202
+ // B55:webview_create 在目标平台 WebView 初始化失败时可能返回 NULL
203
+ //(如 Linux 缺 WebKitGTK、macOS WebKit 不可用),此时返回 nil 让调用方
204
+ //(freedom.go Run 的 w == nil 分支)走失败提示,而非持 nil webview_t
205
+ // 继续调用 SetTitle/SetSize/Init 等 C API 触发空指针崩溃。
206
+ if w.w == nil {
207
+ return nil
208
+ }
202
209
  return w
203
210
  }
204
211
 
@@ -209,52 +216,85 @@ func (w *webview) Destroy() {
209
216
  }
210
217
 
211
218
  func (w *webview) Run() {
219
+ if destroyed.Load() {
220
+ return
221
+ }
212
222
  C.webview_run(w.w)
213
223
  }
214
224
 
215
225
  func (w *webview) Terminate() {
226
+ if destroyed.Load() {
227
+ return
228
+ }
216
229
  C.webview_terminate(w.w)
217
230
  }
218
231
 
219
232
  func (w *webview) Window() unsafe.Pointer {
233
+ if destroyed.Load() {
234
+ return nil
235
+ }
220
236
  return C.webview_get_window(w.w)
221
237
  }
222
238
 
223
239
  func (w *webview) Navigate(url string) {
240
+ if destroyed.Load() {
241
+ return
242
+ }
224
243
  s := C.CString(url)
225
244
  defer C.free(unsafe.Pointer(s))
226
245
  C.webview_navigate(w.w, s)
227
246
  }
228
247
 
229
248
  func (w *webview) SetHtml(html string) {
249
+ if destroyed.Load() {
250
+ return
251
+ }
230
252
  s := C.CString(html)
231
253
  defer C.free(unsafe.Pointer(s))
232
254
  C.webview_set_html(w.w, s)
233
255
  }
234
256
 
235
257
  func (w *webview) SetTitle(title string) {
258
+ if destroyed.Load() {
259
+ return
260
+ }
236
261
  s := C.CString(title)
237
262
  defer C.free(unsafe.Pointer(s))
238
263
  C.webview_set_title(w.w, s)
239
264
  }
240
265
 
241
266
  func (w *webview) SetDecorated(decorated bool) {
267
+ if destroyed.Load() {
268
+ return
269
+ }
242
270
  C.webview_set_decorated(w.w, boolToInt(decorated))
243
271
  }
244
272
 
245
273
  func (w *webview) WindowControl(action WindowAction) int {
274
+ if destroyed.Load() {
275
+ return -1
276
+ }
246
277
  return int(C.webview_window_control(w.w, C.webview_window_action_t(action)))
247
278
  }
248
279
 
249
280
  func (w *webview) BeginMoveDrag() int {
281
+ if destroyed.Load() {
282
+ return -1
283
+ }
250
284
  return int(C.webview_window_begin_move_drag(w.w))
251
285
  }
252
286
 
253
287
  func (w *webview) SetSize(width int, height int, hint Hint) {
288
+ if destroyed.Load() {
289
+ return
290
+ }
254
291
  C.webview_set_size(w.w, C.int(width), C.int(height), C.webview_hint_t(hint))
255
292
  }
256
293
 
257
294
  func (w *webview) Init(js string) {
295
+ if destroyed.Load() {
296
+ return
297
+ }
258
298
  s := C.CString(js)
259
299
  defer C.free(unsafe.Pointer(s))
260
300
  C.webview_init(w.w, s)
@@ -294,6 +334,11 @@ func _webviewDispatchGoCallback(index unsafe.Pointer) {
294
334
  delete(dispatch, uintptr(index))
295
335
  m.Unlock()
296
336
  if f != nil {
337
+ // 入队时 destroyed 可能为 false,但回调真正执行时窗口可能已销毁,
338
+ // 二次检查:销毁后 UI 线程即将退出,未执行的派发任务直接丢弃。
339
+ if destroyed.Load() {
340
+ return
341
+ }
297
342
  f()
298
343
  }
299
344
  }
@@ -335,6 +380,9 @@ func _webviewBindingGoCallback(w C.webview_t, id *C.char, req *C.char, index uin
335
380
  }
336
381
 
337
382
  func (w *webview) Bind(name string, f interface{}) error {
383
+ if destroyed.Load() {
384
+ return errors.New("freedom: webview destroyed")
385
+ }
338
386
  v := reflect.ValueOf(f)
339
387
  // f must be a function
340
388
  if v.Kind() != reflect.Func {
@@ -413,6 +461,9 @@ func (w *webview) Bind(name string, f interface{}) error {
413
461
  }
414
462
 
415
463
  func (w *webview) Unbind(name string) error {
464
+ if destroyed.Load() {
465
+ return errors.New("freedom: webview destroyed")
466
+ }
416
467
  // 清理 Go 侧 bindings / bindNames 条目,避免 Unbind 后永久泄漏。
417
468
  // C 侧 binding_context(glue.c calloc,每 Bind 约 16B)在 C++ unbind 中
418
469
  // 仅 erase map 不 free,会小幅泄漏;但框架内 Bind 次数固定(3~4 个)、
@@ -18,6 +18,14 @@ export default {
18
18
  // 是否开启 WebView 开发者工具。
19
19
  debug: false,
20
20
 
21
+ // 安全模式,在终端随时可改,三选一:
22
+ // 'none' - 明文资源(默认,兼容历史产物)
23
+ // 'basic' - 明文资源 + 构建期加固提示
24
+ // 'high' - resources 整体加密为 app.bin(磁盘无明文 HTML/配置),
25
+ // 壳内存解密 + 完整性校验,另含 anti-debug / 进程隐藏
26
+ // 终端命令:freedom security <none|basic|high>,或在 freedom build 时传 --security <mode>
27
+ security: 'none',
28
+
21
29
  // 标题栏策略,在终端随时可改,二选一:
22
30
  // 'native' - 保留系统原生标题栏,标题栏图标与 exe 图标一致
23
31
  // 'frameless' - 完全无边框,标题栏与 Windows 原生最小化 / 最大化 / 关闭按钮均不存在,