@yufengtadian/freedom-cli 1.13.2 → 1.14.0-preview

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 (58) hide show
  1. package/LICENSE +40 -0
  2. package/README.md +74 -21
  3. package/lib/agents.js +182 -52
  4. package/lib/build.js +119 -42
  5. package/lib/cli.js +106 -9
  6. package/lib/release.js +22 -6
  7. package/lib/security.js +525 -45
  8. package/lib/shell.js +288 -64
  9. package/lib/toolchain.js +339 -0
  10. package/lib/utils.js +51 -0
  11. package/lib/verify.js +85 -30
  12. package/lib/webkit.js +45 -0
  13. package/package.json +4 -3
  14. package/shell/darwin-arm64/freedom-shell +0 -0
  15. package/shell/linux-x64/freedom-shell +0 -0
  16. package/shell/win-x64/freedom-shell.exe +0 -0
  17. package/skill/freedom/SKILL.md +3 -3
  18. package/templates/desktop/backend/desktop.mjs +16 -6
  19. package/templates/desktop/freedom.config.js +4 -1
  20. package/templates/go/go.mod +3 -0
  21. package/templates/go/pkg/freedom/anti_debug_linux.go +69 -0
  22. package/templates/go/pkg/freedom/anti_debug_other.go +5 -3
  23. package/templates/go/pkg/freedom/anti_debug_windows.go +136 -12
  24. package/templates/go/pkg/freedom/assets_embed.go +1 -0
  25. package/templates/go/pkg/freedom/backend_proc.go +5 -5
  26. package/templates/go/pkg/freedom/events_windows.go +5 -5
  27. package/templates/go/pkg/freedom/freedom.go +17 -3
  28. package/templates/go/pkg/freedom/msgwindow_windows.go +8 -8
  29. package/templates/go/pkg/freedom/resources.go +59 -1
  30. package/templates/go/pkg/freedom/security.go +475 -41
  31. package/templates/go/pkg/freedom/shutdown.go +55 -0
  32. package/templates/go/pkg/freedom/shutdown_windows.go +47 -0
  33. package/templates/go/pkg/freedom/store.go +3 -3
  34. package/templates/go/pkg/freedom/syscap_windows.go +33 -33
  35. package/templates/go/pkg/freedom/sysint_windows.go +2 -2
  36. package/templates/go/pkg/freedom/tray_windows.go +18 -18
  37. package/templates/go/pkg/freedom/updater.go +3 -3
  38. package/templates/go/third_party/webview_go/CHANGELOG.md +15 -0
  39. package/templates/go/third_party/webview_go/FREEDOM-PATCH.md +32 -0
  40. package/templates/go/third_party/webview_go/LICENSE +22 -0
  41. package/templates/go/third_party/webview_go/README.md +50 -0
  42. package/templates/go/third_party/webview_go/glue.c +36 -0
  43. package/templates/go/third_party/webview_go/go.mod +3 -0
  44. package/templates/go/third_party/webview_go/go.sum +0 -0
  45. package/templates/go/third_party/webview_go/libs/mswebview2/LICENSE +27 -0
  46. package/templates/go/third_party/webview_go/libs/mswebview2/include/WebView2.h +23568 -0
  47. package/templates/go/third_party/webview_go/libs/mswebview2/include/vendor.go +2 -0
  48. package/templates/go/third_party/webview_go/libs/mswebview2/vendor.go +2 -0
  49. package/templates/go/third_party/webview_go/libs/mswebview2/version.txt +1 -0
  50. package/templates/go/third_party/webview_go/libs/webview/LICENSE +22 -0
  51. package/templates/go/third_party/webview_go/libs/webview/include/vendor.go +2 -0
  52. package/templates/go/third_party/webview_go/libs/webview/include/webview.h +3598 -0
  53. package/templates/go/third_party/webview_go/libs/webview/vendor.go +2 -0
  54. package/templates/go/third_party/webview_go/libs/webview/version.txt +1 -0
  55. package/templates/go/third_party/webview_go/webkit2_40.go +21 -0
  56. package/templates/go/third_party/webview_go/webkit2_41.go +8 -0
  57. package/templates/go/third_party/webview_go/webview.cc +1 -0
  58. package/templates/go/third_party/webview_go/webview.go +332 -0
@@ -21,12 +21,18 @@ function write(msg) {
21
21
  }
22
22
 
23
23
  // ---- CLI 定位:优先 cli-entry.json(freedom desktop 打包时写入),再退到环境变量 ----
24
+ // cliMeta.root 一并带出:high 模式下本文件被解到系统临时目录,
25
+ // 不能再靠 `__dirname/../package.json` 反推 CLI 包版本,只能读容器里的 root。
26
+ const cliMeta = { entry: null, root: null };
24
27
  function cliEntry() {
25
28
  const candidates = [];
26
- try {
27
- const meta = JSON.parse(fs.readFileSync(path.join(__dirname, 'cli-entry.json'), 'utf8'));
28
- if (meta && meta.entry) candidates.push(meta.entry);
29
- } catch { /* 缺文件走后续回退 */ }
29
+ if (!cliMeta.entry) {
30
+ try {
31
+ const meta = JSON.parse(fs.readFileSync(path.join(__dirname, 'cli-entry.json'), 'utf8'));
32
+ if (meta && meta.entry) { cliMeta.entry = meta.entry; cliMeta.root = meta.root || null; }
33
+ } catch { /* 缺文件走后续回退 */ }
34
+ }
35
+ if (cliMeta.entry && fs.existsSync(cliMeta.entry)) return cliMeta.entry;
30
36
  if (process.env.FREEDOM_CLI_ENTRY) candidates.push(process.env.FREEDOM_CLI_ENTRY);
31
37
  for (const c of candidates) {
32
38
  if (c && fs.existsSync(c)) return c;
@@ -108,10 +114,14 @@ function stopDev() {
108
114
  const api = {
109
115
  async 'app.info'() {
110
116
  const entry = cliEntry();
111
- const pkg = entry ? (() => { try { return JSON.parse(fs.readFileSync(path.join(path.dirname(entry), '..', 'package.json'), 'utf8')); } catch { return {}; } })() : {};
117
+ const readVersion = (base) => {
118
+ try { return JSON.parse(fs.readFileSync(path.join(base, 'package.json'), 'utf8')).version || null; } catch { return null; }
119
+ };
120
+ const cliVersion = (entry ? readVersion(path.join(path.dirname(entry), '..')) : null)
121
+ || (cliMeta.root ? readVersion(cliMeta.root) : null);
112
122
  return {
113
123
  cliEntry: entry,
114
- cliVersion: pkg.version || null,
124
+ cliVersion,
115
125
  platform: `${process.platform}-${process.arch}`,
116
126
  nodeVersion: process.version,
117
127
  home: os.homedir(),
@@ -15,7 +15,10 @@ export default {
15
15
  staticHtml: 'app.html',
16
16
 
17
17
  titlebar: 'native',
18
- security: 'none',
18
+ // 闭源自举:Desktop 自身也要经得起逆向——前端 app.html、config.json 与后端
19
+ // backend/*(含 cli-entry.json)整体进 FRDM2 容器,磁盘只留 app.bin + .integrity,
20
+ // 运行期由壳解密到私有临时目录(退出即删,崩溃残留由下次启动回收)。
21
+ security: 'high',
19
22
  outDir: 'dist',
20
23
 
21
24
  // 应用图标:构建时注入 exe(Windows 用 rcedit 写 PE 资源),运行时壳层经
@@ -3,3 +3,6 @@ module freedom-cli-shell
3
3
  go 1.26.5
4
4
 
5
5
  require github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6
6
+
7
+ // 与仓库根 go.mod 同步的本地补丁副本:WebKitGTK 4.0/4.1 依赖名按构建标签选择。
8
+ replace github.com/webview/webview_go => ./third_party/webview_go
@@ -0,0 +1,69 @@
1
+ //go:build linux
2
+
3
+ package freedom
4
+
5
+ // 反调试(Linux 实现):high 安全模式下由 Run() 在资源解密前后各调用一次,
6
+ // 与 Windows 侧同一语义——**探测失败一律记未命中**,只在确证被附加调试器时退出,
7
+ // 误杀正常用户的代价高于多一路信号的收益。
8
+ //
9
+ // 两道信号:
10
+ // 1. ptrace(PTRACE_TRACEME):本进程已被别的进程 attach 时返回 EPERM —— 这是
11
+ // "GDB 已挂上来"的确证。TRACEME 是一次性的(成功后无法再 DETACH 复位),
12
+ // 故整个进程生命周期只探测一次并缓存结论;Run 里的"解密前 / 解密后"两次调用
13
+ // 因此共享同一结论——本就只能挡住「启动时已挂调试器」,运行中途 attach
14
+ // (gdb -p)要等下次启动才被抓到,这是诚实的能力边界。
15
+ // **已知边界**:本进程此前处于停止/跟踪-停止状态(被 SIGSTOP 过)时 TRACEME
16
+ // 也会 EPERM,理论上存在误判窗口;桌面壳正常生命周期内不走这条路,且
17
+ // 非 high 模式完全不检测,可用 FREEDOM_DISABLE_ANTIDEBUG=1 关闭。
18
+ // 2. PR_SET_DUMPABLE=0:不是检测而是加固——内核不再产出 core dump,且非属主、
19
+ // 非 ptrace 授权的进程读不到 /proc/<pid>/mem 与 /proc/<pid>/maps。
20
+ // 副作用是本进程写的文件属主不再是 dumpable 语义下的自己(/tmp 下按目录名
21
+ // 隔离的私有临时目录不受影响)。
22
+ //
23
+ // 未纳入:/proc/<pid>/status 的 TracerPid 字段——读它本身没问题,但攻击者 attach
24
+ // 前后改一行 status 即可绕过(同一函数里 ptrace 系统调用才是硬证据),徒增误报源。
25
+
26
+ import (
27
+ "os"
28
+ "sync"
29
+ "syscall"
30
+ )
31
+
32
+ // antiDebugCheck 命中即退出(退出码 77,与 Windows 侧一致,不显示窗口、不回显原因)。
33
+ func antiDebugCheck() {
34
+ if debuggerPresent() {
35
+ os.Exit(77)
36
+ }
37
+ }
38
+
39
+ // debuggerPresent 汇总 Linux 侧信号。
40
+ func debuggerPresent() bool {
41
+ if ptracemeRejectedOnce() {
42
+ return true
43
+ }
44
+ makeNonDumpable() // 未命中也要落地下一步加固(core dump / /proc 读内存收紧)
45
+ return false
46
+ }
47
+
48
+ var (
49
+ tracemeOnce = new(sync.Once) // 指针:单测需要可复位的实例
50
+ tracemeDetected bool
51
+ )
52
+
53
+ // ptracemeRejectedOnce 缓存一次性 TRACEME 结论(见文件头:TRACEME 不可复位)。
54
+ func ptracemeRejectedOnce() bool {
55
+ tracemeOnce.Do(func() { tracemeDetected = ptracemeRejected() })
56
+ return tracemeDetected
57
+ }
58
+
59
+ // ptracemeRejected 报告 PTRACE_TRACEME 是否被拒(被拒 = 已有调试器附加);
60
+ // 变量形式便于单测替换,生产路径只会被调用一次。
61
+ var ptracemeRejected = func() bool {
62
+ r, _, errno := syscall.RawSyscall(syscall.SYS_PTRACE, uintptr(syscall.PTRACE_TRACEME), 0, 0)
63
+ return r != 0 || errno != 0
64
+ }
65
+
66
+ // makeNonDumpable 关闭 core dump 并收紧 /proc/<pid>/* 的读取授权;失败按未命中处理。
67
+ var makeNonDumpable = func() {
68
+ syscall.RawSyscall(syscall.SYS_PRCTL, syscall.PR_SET_DUMPABLE, 0, 0)
69
+ }
@@ -1,7 +1,9 @@
1
- //go:build !windows
1
+ //go:build !windows && !linux
2
2
 
3
3
  package freedom
4
4
 
5
- // 反调试(非 Windows 平台空实现)。macOS/Linux 的调试器检测依赖
6
- // ptrace / task_for_pid 等平台特定 API,本版本不做;如需加强在对应平台文件扩展。
5
+ // 反调试(macOS 占位实现)。Linux 侧已实装在 anti_debug_linux.go
6
+ // (PTRACE_TRACEME 一次性探测 + PR_SET_DUMPABLE=0 加固);macOS 需要
7
+ // ptrace(PT_DENY_ATTACH) / task_for_pid 等平台 API,本机无 Apple 设备无法验证,
8
+ // 故留空不装——诚实的"未实现"优于未验证的代码。
7
9
  func antiDebugCheck() {}
@@ -6,19 +6,22 @@ package freedom
6
6
  // 命中时静默退出(退出码 77,无提示),防止攻击者在调试器下单步追踪
7
7
  // 密钥派生 / 容器解密逻辑,或直接读内存里的明文密钥。
8
8
  //
9
- // 六道独立信号(任一命中即判定被调试),覆盖不同层次的附加手段:
9
+ // 七道独立信号(任一命中即判定被调试),覆盖不同层次的附加手段:
10
10
  // 1. IsDebuggerPresent —— 读 PEB.BeingDebugged 的快捷 API(最易被 hook 篡改);
11
11
  // 2. CheckRemoteDebuggerPresent —— 内核调试器 / 远程调试器;
12
12
  // 3. 直接读 PEB.BeingDebugged —— 不经 API,绕过"只 patch API 返回值"的调试辅助;
13
13
  // 4. NtQueryInformationProcess(ProcessDebugPort) —— 调试端口句柄;
14
14
  // 5. NtQueryInformationProcess(ProcessDebugObjectHandle) —— 调试对象(无端口也命中);
15
- // 6. NtQueryInformationProcess(ProcessDebugFlags) —— 标志位反向确认。
15
+ // 6. NtQueryInformationProcess(ProcessDebugFlags) —— 标志位反向确认;
16
+ // 7. 线程上下文 DR0–DR3 —— 硬件断点(数据/内存断点常常不落在前六道上)。
16
17
  //
17
- // 未纳入的信号及原因:硬件断点(Dr7)需先 SuspendThread 再 GetThreadContext,
18
- // 对本线程调用实测返回 ERROR_ACCESS_DENIED(非挂起线程读不到上下文),
19
- // 为一次启动期检测去挂起自建线程不划算;时间差检测在 CI/低配机误判率高,
20
- // 误杀正常用户的代价高于其收益。所有信号都遵循"探测失败即视为未命中",
21
- // 只在确证被调试时退出,杜绝误杀。
18
+ // 第七道的实现约束:对本线程直接 GetThreadContext 会返回 ERROR_ACCESS_DENIED,
19
+ // 故用 Toolhelp 枚举进程内线程、以 THREAD_GET_CONTEXT 打开后读取(不挂起任何线程),
20
+ // 读不到上下文一律记未命中。只在启动期跑、只在 high 模式跑;
21
+ // 已知局限:抓的是"检测时刻已设下的硬件断点",运行中途 attach 再下断点不在射程内。
22
+ //
23
+ // 未纳入:时间差检测在 CI/低配机误判率高,误杀正常用户的代价高于其收益。
24
+ // 所有信号都遵循"探测失败即视为未命中",只在确证被调试时退出,杜绝误杀。
22
25
  //
23
26
  // 诚实边界:这些是抬升成本的对抗,不是不可绕过的墙。绕开者仍需逆向壳二进制、
24
27
  // 复现掩码后的主密钥与 PBKDF2 参数,并在无调试器环境下取密钥。
@@ -39,10 +42,12 @@ var (
39
42
  )
40
43
 
41
44
  // 进程信息类别(PROCESSINFOCLASS)与各类别的返回长度,编号与长度均为本机实测校准:
42
- // ProcessBasicInformation(0) 48 字节(x64,PEB 在偏移 8)
43
- // ProcessDebugPort(7) 8 字节(HANDLE);len 给 4 会返回 STATUS_INFO_LENGTH_MISMATCH
44
- // ProcessDebugObjectHandle(30) 8 字节;未被调试时返回 STATUS_PORT_NOT_SET(0xC0000353)
45
- // ProcessDebugFlags(31) 4 字节(ULONG);未被调试时返回 1
45
+ //
46
+ // ProcessBasicInformation(0) 48 字节(x64,PEB 在偏移 8)
47
+ // ProcessDebugPort(7) 8 字节(HANDLE);len 给 4 会返回 STATUS_INFO_LENGTH_MISMATCH
48
+ // ProcessDebugObjectHandle(30) 8 字节;未被调试时返回 STATUS_PORT_NOT_SET(0xC0000353)
49
+ // ProcessDebugFlags(31) 4 字节(ULONG);未被调试时返回 1
50
+ //
46
51
  // 注意 30/31 两号易写成 32/33(那是 LUIDDeviceMapsEnabled / BreakOnTermination),
47
52
  // 编号或长度写错不会报错、只会静默失去一路信号,故此处配套测试锁死。
48
53
  const (
@@ -66,7 +71,7 @@ func antiDebugCheck() {
66
71
  }
67
72
  }
68
73
 
69
- // debuggerPresent 汇总六道信号。lazy DLL 加载失败(Proc.Find 出错)时该信号记为未命中。
74
+ // debuggerPresent 汇总七道信号。lazy DLL 加载失败(Proc.Find 出错)时该信号记为未命中。
70
75
  func debuggerPresent() bool {
71
76
  if r, _, _ := procIsDebuggerPresent.Call(); r != 0 {
72
77
  return true
@@ -90,6 +95,10 @@ func debuggerPresent() bool {
90
95
  if r, ok := ntQueryProcessInfo(processDebugFlags, debugFlagsSize); ok && r == 0 {
91
96
  return true
92
97
  }
98
+ // 硬件断点:数据断点 / 内存断点常不改动上面任何一处状态,只能看 DR0–DR3。
99
+ if hardwareBreakpointSet() {
100
+ return true
101
+ }
93
102
  return false
94
103
  }
95
104
 
@@ -138,3 +147,118 @@ func pebBeingDebuggedSet() bool {
138
147
  }
139
148
  return *(*byte)(unsafe.Pointer(info.pebAddress + pebBeingDebuggedOff)) != 0
140
149
  }
150
+
151
+ // ---- 第七道信号:硬件断点(DR0–DR3)----
152
+ //
153
+ // 数据断点与部分内存断点不改 PEB、不建调试端口,只写在线程上下文的调试寄存器里。
154
+ // 对本线程调 GetThreadContext 会被拒(ERROR_ACCESS_DENIED),因此走 Toolhelp 枚举
155
+ // 进程内线程 + THREAD_GET_CONTEXT 读取;任何一步失败都记未命中(宁漏不误杀)。
156
+
157
+ const (
158
+ th32csSnapThread = 0x00000002
159
+ threadQueryInfo = 0x0040
160
+ threadGetContext = 0x0010
161
+ threadAllAccess = threadQueryInfo | threadGetContext
162
+ // CONTEXT_AMD64(0x00100000) | CONTEXT_DEBUG_REGISTERS(0x10):不置此标志,
163
+ // GetThreadContext 根本不回填 Dr0–Dr7(读到的只是栈上残值,会误报)。
164
+ contextDebugRegisters = 0x00100000 | 0x10
165
+ dr7LocalBitsMask = 0b1111
166
+ invalidHandleVal = ^uintptr(0)
167
+ threadEntrySize = unsafe.Sizeof(threadEntry32{})
168
+ )
169
+
170
+ var (
171
+ procCreateToolhelp32Snapshot = kernel32AntiDebug.NewProc("CreateToolhelp32Snapshot")
172
+ procThread32First = kernel32AntiDebug.NewProc("Thread32First")
173
+ procThread32Next = kernel32AntiDebug.NewProc("Thread32Next")
174
+ procOpenThread = kernel32AntiDebug.NewProc("OpenThread")
175
+ procGetThreadContext = kernel32AntiDebug.NewProc("GetThreadContext")
176
+ )
177
+
178
+ // threadEntry32 是 Toolhelp 线程快照项(x64 布局:32 字节,末尾有对齐填充)。
179
+ type threadEntry32 struct {
180
+ dwSize uint32
181
+ cntUsage uint32
182
+ th32ThreadID uint32
183
+ th32OwnerPID uint32
184
+ tpBasePrio int32
185
+ dwFlags uint32
186
+ hModule uintptr
187
+ _pad uint32
188
+ }
189
+
190
+ // contextAMD64 是 Windows x64 CONTEXT 的局部视图,偏移严格按 winnt.h:
191
+ // P1Home–P6Home 占 0x00–0x2F,ContextFlags@0x30,MxCsr@0x34,段寄存器 0x38–0x43,
192
+ // EFlags@0x44,Dr0@0x48、Dr1@0x50、Dr2@0x58、Dr3@0x60、Dr6@0x68、Dr7@0x70,
193
+ // 其后寄存器/XMM/XSTATE 一并归入 padding(总长取 0x400)。
194
+ // 偏移错一位就会把 EFlags/残值当成断点地址(误杀),故 anti_debug_windows_test.go 逐字段挂断言。
195
+ type contextAMD64 struct {
196
+ _ [48]byte
197
+ contextFlags uint32
198
+ _ [20]byte // MxCsr + 6 个段寄存器 + EFlags
199
+ dr0 uint64
200
+ dr1 uint64
201
+ dr2 uint64
202
+ dr3 uint64
203
+ dr6 uint64
204
+ dr7 uint64
205
+ _ [1024 - 120]byte
206
+ }
207
+
208
+ func hardwareBreakpointSet() bool {
209
+ for _, fn := range []uintptr{
210
+ procCreateToolhelp32Snapshot.Addr(), procThread32First.Addr(), procThread32Next.Addr(),
211
+ procOpenThread.Addr(), procGetThreadContext.Addr(),
212
+ } {
213
+ if fn == 0 {
214
+ return false // DLL 未加载成功:本信号记未命中
215
+ }
216
+ }
217
+ snap, _, _ := procCreateToolhelp32Snapshot.Call(th32csSnapThread, 0)
218
+ if snap == invalidHandleVal || snap == 0 {
219
+ return false
220
+ }
221
+ defer syscall.CloseHandle(syscall.Handle(snap))
222
+ pid := uintptr(syscall.Getpid())
223
+ var te threadEntry32
224
+ te.dwSize = uint32(threadEntrySize)
225
+ for ok, _, _ := procThread32First.Call(snap, uintptr(unsafe.Pointer(&te))); ok != 0; ok, _, _ = procThread32Next.Call(snap, uintptr(unsafe.Pointer(&te))) {
226
+ if te.th32OwnerPID != uint32(pid) {
227
+ continue // 只看本进程的线程
228
+ }
229
+ if threadHasHardwareBreakpoint(uintptr(te.th32ThreadID)) {
230
+ return true
231
+ }
232
+ }
233
+ return false
234
+ }
235
+
236
+ // threadHasHardwareBreakpoint 读一个线程的调试寄存器;读不到即未命中。
237
+ // 刻意不 SuspendThread:挂起 Go 运行时线程有死锁风险,而调试器 attach 时本就把线程挂住了
238
+ // ——那种状态下 GetThreadContext 才会成功并带回 Dr0–Dr7,正是这一路要抓的场景。
239
+ func threadHasHardwareBreakpoint(tid uintptr) bool {
240
+ h, _, _ := procOpenThread.Call(threadAllAccess, 0, tid)
241
+ if h == invalidHandleVal || h == 0 {
242
+ return false
243
+ }
244
+ defer syscall.CloseHandle(syscall.Handle(h))
245
+ // 16 字节留白供 round16 向上对齐(GetThreadContext 要求 CONTEXT 16 字节对齐)。
246
+ var ctxRaw [unsafe.Sizeof(contextAMD64{}) + 16]byte
247
+ ctx := (*contextAMD64)(unsafe.Pointer(round16(&ctxRaw[0])))
248
+ ctx.contextFlags = contextDebugRegisters // 输入:只取调试寄存器
249
+ r, _, _ := procGetThreadContext.Call(h, uintptr(unsafe.Pointer(ctx)))
250
+ if r == 0 || ctx.contextFlags&contextDebugRegisters == 0 {
251
+ return false // 线程在跑 / 权限不足 / 未回填:一律未命中
252
+ }
253
+ return (ctx.dr7&dr7LocalBitsMask) != 0 &&
254
+ (ctx.dr0 != 0 || ctx.dr1 != 0 || ctx.dr2 != 0 || ctx.dr3 != 0)
255
+ }
256
+
257
+ // round16 把切片首地址向上对齐到 16 字节(GetThreadContext 要求 CONTEXT 16 字节对齐)。
258
+ func round16(p *byte) *byte {
259
+ a := uintptr(unsafe.Pointer(p))
260
+ if d := a % 16; d != 0 {
261
+ a += 16 - d
262
+ }
263
+ return (*byte)(unsafe.Pointer(a))
264
+ }
@@ -12,6 +12,7 @@ import _ "embed"
12
12
  // window.freedom.emit(event, data) // 应用内事件广播(后端 Eval 亦使用)
13
13
  //
14
14
  // bridge 由 go-webview2 的 Bind 机制注册,返回 Promise,Go 侧自动 JSON 编解码。
15
+ //
15
16
  //go:embed assets/freedom.js
16
17
  var jsSDK string
17
18
 
@@ -42,11 +42,11 @@ type ProcBackend struct {
42
42
  maxLine int // 单条 stdout 行(一次响应)上限字节;超限即那条响应过大,不得中断整个通道
43
43
 
44
44
  // —— W4 崩溃自愈(对标 Tauri shell spawn 的失败处理 / sidecar 重启)——
45
- rp RestartPolicy // 重启策略(零值=不重启,保持旧行为)
46
- gen int // 每次拉起递增;readLoop 据此识别"自己那一期",过期实例不触发重启
47
- pdone chan struct{} // 当前这期进程 Wait 完成后由 readLoop 关闭(Close 等它,不再自己 Wait,避免双重 Wait)
48
- attempts int // 连续崩溃重启计数
49
- startedAt time.Time // 本期进程启动时刻(存活够久则重置 attempts)
45
+ rp RestartPolicy // 重启策略(零值=不重启,保持旧行为)
46
+ gen int // 每次拉起递增;readLoop 据此识别"自己那一期",过期实例不触发重启
47
+ pdone chan struct{} // 当前这期进程 Wait 完成后由 readLoop 关闭(Close 等它,不再自己 Wait,避免双重 Wait)
48
+ attempts int // 连续崩溃重启计数
49
+ startedAt time.Time // 本期进程启动时刻(存活够久则重置 attempts)
50
50
  }
51
51
 
52
52
  // RestartPolicy 是后端进程崩溃后的自动重启策略(经 SetRestartPolicy 启用)。
@@ -18,10 +18,10 @@ import (
18
18
 
19
19
  const (
20
20
  wmSize = 0x0005
21
- wmMove = 0x0003
21
+ wmMove = 0x0003
22
22
  wmExitSizeMove = 0x0212
23
- wmSetFocus = 0x0007
24
- wmKillFocus = 0x0008
23
+ wmSetFocus = 0x0007
24
+ wmKillFocus = 0x0008
25
25
 
26
26
  sizeMaximized = 1
27
27
  sizeMinimized = 2
@@ -244,8 +244,8 @@ func enumMonitorsProc(hmon, hdc, lprci, data uintptr) uintptr {
244
244
  Width: int(mi.rcMonitor.right - mi.rcMonitor.left),
245
245
  Height: int(mi.rcMonitor.bottom - mi.rcMonitor.top),
246
246
  WorkX: int(mi.rcWork.left), WorkY: int(mi.rcWork.top),
247
- WorkWidth: int(mi.rcWork.right - mi.rcWork.left),
248
- WorkHeight: int(mi.rcWork.bottom - mi.rcWork.top),
247
+ WorkWidth: int(mi.rcWork.right - mi.rcWork.left),
248
+ WorkHeight: int(mi.rcWork.bottom - mi.rcWork.top),
249
249
  ScaleFactor: scale,
250
250
  IsPrimary: mi.dwFlags&monitorInfofPrimary != 0,
251
251
  })
@@ -65,6 +65,11 @@ type Config struct {
65
65
  MinHeight int
66
66
  // Debug 为 true 时开启 WebView 开发者工具(目标平台支持时)。
67
67
  Debug bool
68
+ // DisableAntiDebug 关闭 high 安全模式的调试器探测(见 antiDebugEnabled)。
69
+ // 默认 false:探测在位。仅在合法运行环境被误判(部分沙箱 / EDR / 虚拟化环境会让
70
+ // NtQueryInformationProcess 一类调用给出命中值)时由发布方显式置真。
71
+ // 关闭不影响容器解密与 .integrity 校验——那两道才是源码保护的本体。
72
+ DisableAntiDebug bool
68
73
  // Backend 指定后端适配器。为空时默认使用内嵌 Go 后端(配合 Bind 使用)。
69
74
  Backend Backend
70
75
  // HTML 返回要加载到窗口的前端页面内容(内存加载,无本地端口)。
@@ -113,6 +118,10 @@ type App struct {
113
118
  backendExplicit bool
114
119
  secureBackendDir string
115
120
 
121
+ // runtimeVersion = config.json / app.bin 容器里声明的「本应用」版本。
122
+ // 与编译期的 Version(通用壳的版本)分属两个版本域,自更新比较只认前者。
123
+ runtimeVersion string
124
+
116
125
  // upMu 保护 upPending:CheckUpdate 验签通过的更新条目,install 桥接只认它
117
126
  //(前端无法注入未验签的 URL/哈希)。见 updater.go。
118
127
  upMu sync.Mutex
@@ -231,7 +240,7 @@ func (a *App) Unbind(name string) {
231
240
  func (a *App) Run() {
232
241
  // high 产物:解密前先跑一次反调试(此时密钥与明文都还没进内存),
233
242
  // 解密后再跑一次(兜住"启动后才附加调试器")。非 high 应用不受影响。
234
- if hasSecureResources() {
243
+ if hasSecureResources() && a.antiDebugEnabled() {
235
244
  antiDebugCheck()
236
245
  }
237
246
  // 通用壳:先加载 exe 同目录 resources/ 覆盖窗口与后端配置(CLI build 写入;
@@ -242,14 +251,19 @@ func (a *App) Run() {
242
251
  var se *secureFatalError
243
252
  if errors.As(err, &se) {
244
253
  fmt.Printf("freedom: 安全模式资源校验失败,拒绝运行:%v\n", err)
245
- return
254
+ // 退出码必须非零:Run 返回 nil 会让"拒跑"与"正常退出"在脚本与 CI 眼里同形,
255
+ // 而这条分支存在的意义正是"不能让它悄悄跑起来"。此处尚无 defer 注册
256
+ // (cleanupSecureBackend 在下一行才 defer),os.Exit 不会跳过清理。
257
+ os.Exit(exitSecureFatal)
246
258
  }
247
259
  fmt.Printf("freedom: warning: %v\n", err)
248
260
  }
249
261
  // high 模式的临时后端目录随进程退出清理(defer 早于后端 Close 注册 → 后于其执行)。
250
262
  defer a.cleanupSecureBackend()
263
+ // Ctrl+C / SIGTERM / 控制台关闭:defer 不会执行,须显式清扫(见 installSecureShutdown)。
264
+ a.installSecureShutdown()
251
265
  // high 模式:解密后复查调试器(见 Run 开头的"解密前一次")。
252
- if a.secure {
266
+ if a.secure && a.antiDebugEnabled() {
253
267
  antiDebugCheck()
254
268
  }
255
269
  // 平台层回调(单实例转发/热键)经 currentApp 找到本实例推送事件。
@@ -35,14 +35,14 @@ func validCopyData(dwData uintptr, cbData uint32, lpData uintptr) bool {
35
35
  }
36
36
 
37
37
  type msgWindow struct {
38
- mu sync.Mutex
39
- hwnd uintptr
40
- ready chan struct{}
41
- created bool
42
- hotkeys map[string]uint32 // 前端 id → 热键数值 id
43
- nextHKID uint32
44
- onHotkey func(id string)
45
- onData func(payload []byte)
38
+ mu sync.Mutex
39
+ hwnd uintptr
40
+ ready chan struct{}
41
+ created bool
42
+ hotkeys map[string]uint32 // 前端 id → 热键数值 id
43
+ nextHKID uint32
44
+ onHotkey func(id string)
45
+ onData func(payload []byte)
46
46
  }
47
47
 
48
48
  var (
@@ -34,6 +34,11 @@ func secureFatal(err error) error {
34
34
  return &secureFatalError{err: err}
35
35
  }
36
36
 
37
+ // exitSecureFatal 是 high 模式拒绝运行时的进程退出码(见 freedom.go Run)。
38
+ // 取 70 —— BSD sysexits 的 EX_SOFTWARE:"这份产物本身不可信",与 1(一般失败)、
39
+ // 130(Ctrl+C 收尾)区分开,发布脚本据此认出这一具体事件。
40
+ const exitSecureFatal = 70
41
+
37
42
  // runtimeBackend 描述 config.json 中的后端进程配置(任意语言,经 stdio NDJSON 桥接)。
38
43
  type runtimeBackend struct {
39
44
  Command string `json:"command"`
@@ -52,6 +57,7 @@ type runtimeUpdater struct {
52
57
  // 由 freedom CLI 在 build 阶段根据 freedom.config.js 生成;Name 仅作来源标记不应用。
53
58
  type runtimeConfigFile struct {
54
59
  Name string `json:"name"`
60
+ Version string `json:"version"`
55
61
  Title string `json:"title"`
56
62
  TitleBar string `json:"titlebar"`
57
63
  Width int `json:"width"`
@@ -64,6 +70,9 @@ type runtimeConfigFile struct {
64
70
  URL string `json:"url"`
65
71
  SingleInstance *bool `json:"singleInstance"`
66
72
  Updater *runtimeUpdater `json:"updater"`
73
+ // Capabilities 收口前端可调用面。通用壳里编译不进应用作者的 Config 字面量,
74
+ // 没有这条透传则 freedom.config.js 写 capabilities 会被静默忽略(= 全开)。
75
+ Capabilities *Capabilities `json:"capabilities"`
67
76
  }
68
77
 
69
78
  // resourcesDirOverride 是单测缝:非空时替代 exe 同目录的 resources 定位。
@@ -102,6 +111,8 @@ func (a *App) loadRuntimeConfig() error {
102
111
  return secureFatal(fmt.Errorf("后端源码解密落地失败:%w", err))
103
112
  }
104
113
  a.secureBackendDir = bdir
114
+ // 落盘完成即擦:载荷缓存会活到进程结束,后端源码没有理由跟着一起常驻。
115
+ scrubSecureBackendPayload(p)
105
116
  }
106
117
  a.applyRuntimeConfig(&rc)
107
118
  // high 模式强制关闭 WebView 开发者工具,防止前端源码经 devtools 直接查看。
@@ -135,6 +146,15 @@ func (a *App) loadRuntimeConfigPlain() error {
135
146
 
136
147
  // applyRuntimeConfig 把解析后的运行时配置覆盖到应用配置(明文与加密路径共用)。
137
148
  func (a *App) applyRuntimeConfig(rc *runtimeConfigFile) {
149
+ // 应用版本:通用壳编译期注入的 Version 是「壳」的版本,不是「这个应用」的版本。
150
+ // 自更新比较必须用后者,否则要么永远报 uncomparable(壳未打戳时是 "dev"),
151
+ // 要么拿壳的版本号跟应用的清单比——换来的仍是同一个壳,更新永远收敛不了。
152
+ if rc.Version != "" {
153
+ a.runtimeVersion = rc.Version
154
+ }
155
+ if rc.Capabilities != nil && a.cfg.Capabilities == nil {
156
+ a.cfg.Capabilities = rc.Capabilities
157
+ }
138
158
  // 窗口标题:config.json 的 title 覆盖编译期默认("Freedom App")。
139
159
  if rc.Title != "" {
140
160
  a.cfg.Title = rc.Title
@@ -193,6 +213,16 @@ func (a *App) applyRuntimeConfig(rc *runtimeConfigFile) {
193
213
  }
194
214
  }
195
215
 
216
+ // appVersion 返回「本应用」的版本:优先用 config.json / app.bin 容器里声明的值,
217
+ // 回落编译期 Version(examples 自建壳走 ldflags 注入)。通用壳的 Version 是壳自己的
218
+ // 版本域,拿它跟应用发布清单比是两个数在比——见 updater.go 的 CheckUpdate。
219
+ func (a *App) appVersion() string {
220
+ if a != nil && a.runtimeVersion != "" {
221
+ return a.runtimeVersion
222
+ }
223
+ return Version
224
+ }
225
+
196
226
  // backendWorkDir 返回后端进程的工作目录。
197
227
  func (a *App) backendWorkDir() (string, error) {
198
228
  if a.secureBackendDir != "" {
@@ -201,6 +231,31 @@ func (a *App) backendWorkDir() (string, error) {
201
231
  return resourcesDir()
202
232
  }
203
233
 
234
+ // installSecureShutdown 注册信号退出通道(Run 调用)。
235
+ //
236
+ // 清扫顺序刻意是「先后端、后目录」:后端子进程的工作目录就是 high 模式解出的那个明文临时
237
+ // 目录,它还活着时 Windows 会因文件被占用让 RemoveAll 静默失败——明文源码继续留在 %TEMP%,
238
+ // 而信号路径紧接着就 os.Exit,下次启动的 GC 也帮不上(PID 仍存活)。ProcBackend.Close 自带
239
+ // closed 幂等与 3s 等待上界,与 Run 正常退出的 defer 双跑无害。
240
+ // 非 high 模式下 cleanupSecureBackend 是 no-op,但后端该停照样得停,故不再判安全档。
241
+ func (a *App) installSecureShutdown() {
242
+ secureCleanup = func() {
243
+ a.closeBackendForShutdown()
244
+ a.cleanupSecureBackend()
245
+ }
246
+ installShutdownCleanup()
247
+ }
248
+
249
+ // closeBackendForShutdown 停后端子进程;未配置后端(nil)时为空操作。
250
+ func (a *App) closeBackendForShutdown() {
251
+ if a.backend == nil {
252
+ return
253
+ }
254
+ if err := a.backend.Close(); err != nil {
255
+ fmt.Fprintf(os.Stderr, "freedom: shutdown backend close: %v\n", err)
256
+ }
257
+ }
258
+
204
259
  // cleanupSecureBackend 删除 high 模式解出的临时后端目录(Run 退出时调用)。
205
260
  // 进程被强杀时目录会残留在系统临时区——内容仍可被下一次启动的壳重新解出,
206
261
  // 且明文源码本就来自内存,故不引入跨进程清扫。
@@ -208,7 +263,10 @@ func (a *App) cleanupSecureBackend() {
208
263
  if a.secureBackendDir == "" {
209
264
  return
210
265
  }
211
- _ = os.RemoveAll(a.secureBackendDir)
266
+ // 删不掉必须在退出前喊出来:这是「磁盘不留明文」承诺当场失守的时刻,静默等于隐瞒。
267
+ if err := os.RemoveAll(a.secureBackendDir); err != nil {
268
+ fmt.Fprintf(os.Stderr, "freedom: 明文后端临时目录未清除(后端源码可能仍可读取):%s: %v\n", a.secureBackendDir, err)
269
+ }
212
270
  a.secureBackendDir = ""
213
271
  }
214
272