@chainlesschain/personal-data-hub 0.1.0 → 0.2.1

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 (154) hide show
  1. package/__tests__/adapters/ai-chat-cookie-capture-spec.test.js +211 -0
  2. package/__tests__/adapters/ai-chat-health-checker.test.js +262 -0
  3. package/__tests__/adapters/ai-chat-history.test.js +396 -0
  4. package/__tests__/adapters/ai-chat-http-client.test.js +242 -0
  5. package/__tests__/adapters/ai-chat-vendors.test.js +874 -0
  6. package/__tests__/adapters/alipay-bill-adapter.test.js +538 -0
  7. package/__tests__/adapters/email-adapter.test.js +138 -1
  8. package/__tests__/adapters/email-classifier.test.js +347 -0
  9. package/__tests__/adapters/email-pdf-extractor.test.js +529 -0
  10. package/__tests__/adapters/email-retry-progress.test.js +294 -0
  11. package/__tests__/adapters/email-templates.test.js +699 -0
  12. package/__tests__/adapters/social-toutiao-kuaishou-scaffold.test.js +269 -0
  13. package/__tests__/adapters/system-data-adapter.test.js +440 -0
  14. package/__tests__/adapters/system-data-android-ingest.test.js +144 -0
  15. package/__tests__/adapters/system-data-android.test.js +387 -0
  16. package/__tests__/adapters/system-data-disclosure.test.js +153 -0
  17. package/__tests__/adapters/wechat-bootstrap.test.js +240 -0
  18. package/__tests__/adapters/wechat-env-probe.test.js +162 -0
  19. package/__tests__/adapters/wechat-frida-agent.test.js +191 -0
  20. package/__tests__/adapters/wechat-frida-integration.test.js +149 -0
  21. package/__tests__/adapters/wechat-frida-key-provider.test.js +188 -0
  22. package/__tests__/adapters/wechat-md5-key-provider.test.js +101 -0
  23. package/__tests__/analysis-skills.test.js +556 -0
  24. package/__tests__/analysis.test.js +329 -1
  25. package/__tests__/e2e/ai-chat-cross-source-journey.test.js +213 -0
  26. package/__tests__/e2e/full-user-journey.test.js +188 -0
  27. package/__tests__/entity-resolver-ingest-hook.test.js +177 -0
  28. package/__tests__/entity-resolver-stages.test.js +411 -0
  29. package/__tests__/entity-resolver-vault.test.js +246 -0
  30. package/__tests__/entity-resolver.test.js +526 -0
  31. package/__tests__/fixtures/entity-resolver-200-mock.json +96 -0
  32. package/__tests__/integration/ai-chat-history-registry.test.js +228 -0
  33. package/__tests__/integration/aichat-wizard-end-to-end.test.js +282 -0
  34. package/__tests__/integration/cross-adapter-pipelines.test.js +396 -0
  35. package/__tests__/integration/wechat-bootstrap-end-to-end.test.js +390 -0
  36. package/__tests__/longtail-adapters.test.js +217 -0
  37. package/__tests__/mobile-extractor.test.js +288 -0
  38. package/__tests__/registry.test.js +4 -2
  39. package/__tests__/shopping-adapters.test.js +296 -0
  40. package/__tests__/sidecar-contacts-cross-validate.test.js +163 -0
  41. package/__tests__/sidecar-supervisor.test.js +120 -0
  42. package/__tests__/social-adapters.test.js +206 -0
  43. package/__tests__/travel-adapters.test.js +325 -0
  44. package/__tests__/vault.test.js +3 -3
  45. package/__tests__/wechat-adapter.test.js +476 -0
  46. package/__tests__/whatsapp-adapter.test.js +135 -0
  47. package/lib/adapter-spec.js +12 -0
  48. package/lib/adapters/_python-sidecar-base.js +207 -0
  49. package/lib/adapters/ai-chat-history/ai-chat-adapter.js +374 -0
  50. package/lib/adapters/ai-chat-history/cookie-auth.js +109 -0
  51. package/lib/adapters/ai-chat-history/cookie-capture-spec.js +331 -0
  52. package/lib/adapters/ai-chat-history/health-checker.js +210 -0
  53. package/lib/adapters/ai-chat-history/http-client.js +211 -0
  54. package/lib/adapters/ai-chat-history/index.js +28 -0
  55. package/lib/adapters/ai-chat-history/schema-map.js +258 -0
  56. package/lib/adapters/ai-chat-history/vendor-spec.js +86 -0
  57. package/lib/adapters/ai-chat-history/vendors/coze.js +179 -0
  58. package/lib/adapters/ai-chat-history/vendors/deepseek.js +199 -0
  59. package/lib/adapters/ai-chat-history/vendors/doubao.js +255 -0
  60. package/lib/adapters/ai-chat-history/vendors/dreamina.js +174 -0
  61. package/lib/adapters/ai-chat-history/vendors/hunyuan.js +176 -0
  62. package/lib/adapters/ai-chat-history/vendors/kimi.js +182 -0
  63. package/lib/adapters/ai-chat-history/vendors/qianfan.js +160 -0
  64. package/lib/adapters/ai-chat-history/vendors/tongyi.js +193 -0
  65. package/lib/adapters/ai-chat-history/vendors/zhipu.js +202 -0
  66. package/lib/adapters/ai-chat-history/wizard-controller.js +473 -0
  67. package/lib/adapters/alipay-bill/alipay-bill-adapter.js +311 -0
  68. package/lib/adapters/alipay-bill/counterparty.js +129 -0
  69. package/lib/adapters/alipay-bill/csv-parser.js +217 -0
  70. package/lib/adapters/alipay-bill/index.js +41 -0
  71. package/lib/adapters/alipay-bill/zip-decryptor.js +111 -0
  72. package/lib/adapters/email-imap/classifier.js +495 -0
  73. package/lib/adapters/email-imap/email-adapter.js +419 -8
  74. package/lib/adapters/email-imap/index.js +42 -0
  75. package/lib/adapters/email-imap/pdf-extractor.js +192 -0
  76. package/lib/adapters/email-imap/templates/bill.js +232 -0
  77. package/lib/adapters/email-imap/templates/government.js +120 -0
  78. package/lib/adapters/email-imap/templates/index.js +78 -0
  79. package/lib/adapters/email-imap/templates/order.js +186 -0
  80. package/lib/adapters/email-imap/templates/other.js +114 -0
  81. package/lib/adapters/email-imap/templates/register.js +113 -0
  82. package/lib/adapters/email-imap/templates/travel.js +157 -0
  83. package/lib/adapters/email-imap/templates/utils.js +275 -0
  84. package/lib/adapters/email-imap/transactions.js +234 -0
  85. package/lib/adapters/messaging-qq/index.js +158 -0
  86. package/lib/adapters/messaging-telegram/index.js +142 -0
  87. package/lib/adapters/messaging-whatsapp/index.js +189 -0
  88. package/lib/adapters/shopping-base/index.js +208 -0
  89. package/lib/adapters/shopping-jd/index.js +150 -0
  90. package/lib/adapters/shopping-meituan/index.js +154 -0
  91. package/lib/adapters/shopping-taobao/index.js +176 -0
  92. package/lib/adapters/social-bilibili/index.js +171 -0
  93. package/lib/adapters/social-douyin/index.js +116 -0
  94. package/lib/adapters/social-kuaishou/index.js +237 -0
  95. package/lib/adapters/social-toutiao/index.js +236 -0
  96. package/lib/adapters/social-weibo/index.js +164 -0
  97. package/lib/adapters/social-xiaohongshu/index.js +96 -0
  98. package/lib/adapters/system-data/disclosure.js +166 -0
  99. package/lib/adapters/system-data/index.js +34 -0
  100. package/lib/adapters/system-data/system-data-adapter.js +344 -0
  101. package/lib/adapters/system-data-android/adapter.js +348 -0
  102. package/lib/adapters/system-data-android/index.js +76 -0
  103. package/lib/adapters/travel-12306/index.js +151 -0
  104. package/lib/adapters/travel-amap/index.js +164 -0
  105. package/lib/adapters/travel-baidu-map/index.js +162 -0
  106. package/lib/adapters/travel-base/index.js +240 -0
  107. package/lib/adapters/travel-ctrip/index.js +151 -0
  108. package/lib/adapters/wechat/bootstrap.js +146 -0
  109. package/lib/adapters/wechat/content-parser.js +326 -0
  110. package/lib/adapters/wechat/db-reader.js +209 -0
  111. package/lib/adapters/wechat/env-probe.js +218 -0
  112. package/lib/adapters/wechat/frida-agent/loader.js +67 -0
  113. package/lib/adapters/wechat/frida-agent/wechat-key-hook.js +126 -0
  114. package/lib/adapters/wechat/index.js +37 -0
  115. package/lib/adapters/wechat/key-extractor.js +158 -0
  116. package/lib/adapters/wechat/key-providers/frida-key-provider.js +244 -0
  117. package/lib/adapters/wechat/key-providers/index.js +22 -0
  118. package/lib/adapters/wechat/key-providers/key-provider-base.js +44 -0
  119. package/lib/adapters/wechat/key-providers/md5-key-provider.js +81 -0
  120. package/lib/adapters/wechat/normalize.js +220 -0
  121. package/lib/adapters/wechat/wechat-adapter.js +205 -0
  122. package/lib/analysis-skills/base.js +113 -0
  123. package/lib/analysis-skills/footprint.js +167 -0
  124. package/lib/analysis-skills/index.js +58 -0
  125. package/lib/analysis-skills/interests.js +161 -0
  126. package/lib/analysis-skills/relations.js +226 -0
  127. package/lib/analysis-skills/spending.js +219 -0
  128. package/lib/analysis-skills/timeline.js +167 -0
  129. package/lib/analysis.js +191 -2
  130. package/lib/entity-resolver/embedding-stage.js +198 -0
  131. package/lib/entity-resolver/entity-resolver.js +384 -0
  132. package/lib/entity-resolver/index.js +42 -0
  133. package/lib/entity-resolver/llm-stage.js +191 -0
  134. package/lib/entity-resolver/rule-stage.js +208 -0
  135. package/lib/entity-resolver/worker.js +149 -0
  136. package/lib/index.js +131 -0
  137. package/lib/migrations.js +73 -0
  138. package/lib/mobile-extractor/android.js +193 -0
  139. package/lib/mobile-extractor/index.js +9 -0
  140. package/lib/mobile-extractor/ios.js +223 -0
  141. package/lib/prompt-builder.js +11 -1
  142. package/lib/query-parser.js +7 -1
  143. package/lib/registry.js +42 -0
  144. package/lib/sidecar/index.js +15 -0
  145. package/lib/sidecar/supervisor.js +359 -0
  146. package/lib/vault.js +343 -0
  147. package/package.json +36 -3
  148. package/scripts/_make-fixture-all.js +126 -0
  149. package/scripts/_make-fixture-contacts.js +84 -0
  150. package/scripts/evaluate-entity-resolver.js +213 -0
  151. package/scripts/smoke-phase-5-5.js +196 -0
  152. package/scripts/smoke-phase-5-7.js +181 -0
  153. package/scripts/smoke-system-data-contacts.js +309 -0
  154. package/scripts/smoke-system-data.js +312 -0
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Phase 12 v0.5 — WeChat SQLCipher DB reader (frida-INDEPENDENT).
3
+ *
4
+ * Opens the decrypted-via-MD5-key EnMicroMsg.db using
5
+ * better-sqlite3-multiple-ciphers (the same package LocalVault uses).
6
+ *
7
+ * Note about the key flow:
8
+ * - For WeChat < 8.0: key comes from KeyExtractor (MD5(IMEI+UIN)[:7]).
9
+ * - For WeChat 8.0+ : key comes from Frida hook (Phase 12.6). DI seam
10
+ * `keyProvider.getKey()` lets both paths plug in cleanly.
11
+ *
12
+ * SQLCipher PRAGMA settings per design doc + sjqz reference:
13
+ * PRAGMA cipher_default_kdf_iter = 4000;
14
+ * PRAGMA cipher_default_use_hmac = OFF;
15
+ * PRAGMA cipher_compatibility = 1; // WCDB legacy SQLCipher v1 layout
16
+ *
17
+ * Different WeChat builds use different SQLCipher params — we try a few
18
+ * standard ones in order via tryOpen().
19
+ */
20
+
21
+ "use strict";
22
+
23
+ const fs = require("node:fs");
24
+ const path = require("node:path");
25
+
26
+ const KNOWN_PRAGMA_PROFILES = [
27
+ // WCDB legacy (most common, WeChat < 8.0)
28
+ {
29
+ name: "wcdb-legacy",
30
+ pragmas: [
31
+ "PRAGMA cipher_compatibility = 1",
32
+ "PRAGMA cipher_default_kdf_iter = 4000",
33
+ "PRAGMA cipher_default_use_hmac = OFF",
34
+ "PRAGMA cipher_default_page_size = 1024",
35
+ ],
36
+ },
37
+ // SQLCipher v3 default — fallback for some PC WeChat builds
38
+ {
39
+ name: "sqlcipher-v3",
40
+ pragmas: [
41
+ "PRAGMA cipher_compatibility = 3",
42
+ "PRAGMA cipher_default_kdf_iter = 64000",
43
+ ],
44
+ },
45
+ // SQLCipher v4 default
46
+ {
47
+ name: "sqlcipher-v4",
48
+ pragmas: [
49
+ "PRAGMA cipher_compatibility = 4",
50
+ ],
51
+ },
52
+ ];
53
+
54
+ class WeChatDBReader {
55
+ constructor(opts = {}) {
56
+ if (!opts || typeof opts !== "object") {
57
+ throw new Error("WeChatDBReader: opts required");
58
+ }
59
+ if (!opts.dbPath || typeof opts.dbPath !== "string") {
60
+ throw new Error("WeChatDBReader: opts.dbPath required");
61
+ }
62
+ if (!opts.keyProvider || typeof opts.keyProvider.getKey !== "function") {
63
+ throw new Error("WeChatDBReader: opts.keyProvider with getKey() required");
64
+ }
65
+ this._dbPath = opts.dbPath;
66
+ this._keyProvider = opts.keyProvider;
67
+ this._driver = opts.driver || null; // DI seam for tests
68
+ this._db = null;
69
+ this._profile = null;
70
+ }
71
+
72
+ async open() {
73
+ if (!fs.existsSync(this._dbPath)) {
74
+ throw new Error(`WeChatDBReader: DB not found: ${this._dbPath}`);
75
+ }
76
+ const key = await this._keyProvider.getKey();
77
+ if (!key) {
78
+ throw new Error("WeChatDBReader: keyProvider returned empty key");
79
+ }
80
+ const Database = this._driver || loadDriver();
81
+ let lastError = null;
82
+ for (const profile of KNOWN_PRAGMA_PROFILES) {
83
+ try {
84
+ const db = new Database(this._dbPath, { readonly: true });
85
+ db.pragma(`key = '${key}'`);
86
+ for (const p of profile.pragmas) db.exec(p);
87
+ // Probe — does sqlite_master open?
88
+ const row = db.prepare("SELECT count(*) AS n FROM sqlite_master").get();
89
+ if (row && Number.isFinite(row.n)) {
90
+ this._db = db;
91
+ this._profile = profile.name;
92
+ return { profile: profile.name, tables: row.n };
93
+ }
94
+ db.close();
95
+ } catch (err) {
96
+ lastError = err;
97
+ }
98
+ }
99
+ throw new Error(
100
+ `WeChatDBReader: failed to open with any pragma profile. Last error: ${lastError && lastError.message}`,
101
+ );
102
+ }
103
+
104
+ /**
105
+ * Quick check: does this look like an EnMicroMsg.db (the main WeChat
106
+ * message DB)? Verifies the `message` + `rcontact` tables exist.
107
+ */
108
+ isEnMicroMsg() {
109
+ if (!this._db) return false;
110
+ try {
111
+ const tables = this._db
112
+ .prepare("SELECT name FROM sqlite_master WHERE type='table'")
113
+ .all()
114
+ .map((r) => r.name);
115
+ return tables.includes("message") && tables.includes("rcontact");
116
+ } catch (_e) {
117
+ return false;
118
+ }
119
+ }
120
+
121
+ /**
122
+ * List tables present.
123
+ */
124
+ listTables() {
125
+ if (!this._db) return [];
126
+ return this._db
127
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
128
+ .all()
129
+ .map((r) => r.name);
130
+ }
131
+
132
+ /**
133
+ * Fetch up to `limit` messages since `sinceMsgSvrId` (per design doc
134
+ * §6 OQ-6 watermark = per-talker last msgSvrId). For initial v0 we
135
+ * accept a global watermark and let the adapter post-filter per
136
+ * talker.
137
+ */
138
+ fetchMessages({ sinceMsgSvrId = 0, limit = 1000, talker = null } = {}) {
139
+ if (!this._db) throw new Error("WeChatDBReader: call open() first");
140
+ let sql = "SELECT msgId, msgSvrId, talker, content, type, createTime, isSend, status FROM message";
141
+ const params = [];
142
+ const where = [];
143
+ if (sinceMsgSvrId) {
144
+ where.push("msgSvrId > ?");
145
+ params.push(sinceMsgSvrId);
146
+ }
147
+ if (talker) {
148
+ where.push("talker = ?");
149
+ params.push(talker);
150
+ }
151
+ if (where.length > 0) sql += " WHERE " + where.join(" AND ");
152
+ sql += " ORDER BY msgSvrId ASC LIMIT ?";
153
+ params.push(limit);
154
+ return this._db.prepare(sql).all(...params);
155
+ }
156
+
157
+ /**
158
+ * Fetch contacts. WeChat rcontact has many columns; we pull the ones
159
+ * relevant for normalization.
160
+ */
161
+ fetchContacts({ limit = 5000 } = {}) {
162
+ if (!this._db) throw new Error("WeChatDBReader: call open() first");
163
+ return this._db
164
+ .prepare(
165
+ "SELECT username, alias, nickname, conRemark, type FROM rcontact LIMIT ?",
166
+ )
167
+ .all(limit);
168
+ }
169
+
170
+ /**
171
+ * Fetch chatroom info.
172
+ */
173
+ fetchChatrooms({ limit = 1000 } = {}) {
174
+ if (!this._db) throw new Error("WeChatDBReader: call open() first");
175
+ return this._db
176
+ .prepare(
177
+ "SELECT chatroomname, memberlist, displayname, roomowner FROM chatroom LIMIT ?",
178
+ )
179
+ .all(limit);
180
+ }
181
+
182
+ close() {
183
+ if (this._db) {
184
+ try { this._db.close(); } catch (_e) {}
185
+ this._db = null;
186
+ }
187
+ }
188
+
189
+ /** Active pragma profile name (set after successful open). */
190
+ profile() { return this._profile; }
191
+ }
192
+
193
+ let _driverCache = null;
194
+ function loadDriver() {
195
+ if (_driverCache) return _driverCache;
196
+ try {
197
+ _driverCache = require("better-sqlite3-multiple-ciphers");
198
+ } catch (err) {
199
+ throw new Error(
200
+ `WeChatDBReader: better-sqlite3-multiple-ciphers required: ${err && err.message ? err.message : err}`,
201
+ );
202
+ }
203
+ return _driverCache;
204
+ }
205
+
206
+ module.exports = {
207
+ WeChatDBReader,
208
+ KNOWN_PRAGMA_PROFILES,
209
+ };
@@ -0,0 +1,218 @@
1
+ /**
2
+ * Phase 12.6.4 — WeChat env-probe.
3
+ *
4
+ * Inspects the connected Android device (via `adb`) and decides which
5
+ * KeyProvider strategy is viable:
6
+ *
7
+ * - suggestedKeyProvider: "md5" | "frida" | "unsupported"
8
+ *
9
+ * Decision tree:
10
+ * - WeChat < 8.0 → "md5" (legacy MD5(IMEI+UIN)[:7] path works)
11
+ * - WeChat 8.0+ → "frida" (need live hook; requires root + frida-server)
12
+ * - WeChat absent / device unreachable → "unsupported"
13
+ *
14
+ * Each capability is probed independently so UI can show a checklist.
15
+ * All shell commands are routed through an injected `exec()` so unit
16
+ * tests don't spawn real processes.
17
+ *
18
+ * Wire shape:
19
+ * {
20
+ * ok: boolean, // overall — true iff suggestedKeyProvider !== "unsupported"
21
+ * suggestedKeyProvider: "md5" | "frida" | "unsupported",
22
+ * reasons: string[], // human strings for why suggestion chosen
23
+ * device: {
24
+ * reachable: boolean,
25
+ * serial: string | null,
26
+ * abi: string | null, // arm64-v8a / armeabi-v7a / x86_64
27
+ * },
28
+ * root: {
29
+ * detected: boolean,
30
+ * magiskInstalled: boolean,
31
+ * },
32
+ * frida: {
33
+ * serverRunning: boolean,
34
+ * port: number | null,
35
+ * },
36
+ * wechat: {
37
+ * installed: boolean,
38
+ * versionName: string | null,
39
+ * majorVersion: number | null,
40
+ * },
41
+ * warnings: string[],
42
+ * }
43
+ */
44
+ "use strict";
45
+
46
+ // Lazy require of child_process so test injection beats the import
47
+ function defaultExec() {
48
+ // eslint-disable-next-line global-require
49
+ const cp = require("node:child_process");
50
+ return (cmd, opts = {}) => new Promise((resolve) => {
51
+ cp.exec(cmd, { encoding: "utf-8", timeout: 5000, ...opts }, (err, stdout, stderr) => {
52
+ resolve({
53
+ code: err ? (err.code || 1) : 0,
54
+ stdout: String(stdout || ""),
55
+ stderr: String(stderr || ""),
56
+ });
57
+ });
58
+ });
59
+ }
60
+
61
+ async function probeDevice(exec) {
62
+ const out = { reachable: false, serial: null, abi: null };
63
+ const r = await exec("adb devices");
64
+ if (r.code !== 0) return out;
65
+ const lines = r.stdout.split(/\r?\n/);
66
+ // Header: "List of devices attached" then "<serial>\tdevice"
67
+ for (const line of lines) {
68
+ const m = /^(\S+)\s+device\s*$/.exec(line);
69
+ if (m) {
70
+ out.reachable = true;
71
+ out.serial = m[1];
72
+ break;
73
+ }
74
+ }
75
+ if (!out.reachable) return out;
76
+ const abiR = await exec("adb shell getprop ro.product.cpu.abi");
77
+ out.abi = abiR.code === 0 ? abiR.stdout.trim() || null : null;
78
+ return out;
79
+ }
80
+
81
+ async function probeRoot(exec) {
82
+ const out = { detected: false, magiskInstalled: false };
83
+ // `su -c id` succeeds (uid=0) only on rooted devices
84
+ const su = await exec('adb shell "su -c id" 2>&1');
85
+ if (su.code === 0 && /uid=0/.test(su.stdout)) out.detected = true;
86
+ // which magisk (Magisk command-line)
87
+ const ma = await exec('adb shell "command -v magisk"');
88
+ if (ma.code === 0 && ma.stdout.trim()) out.magiskInstalled = true;
89
+ return out;
90
+ }
91
+
92
+ async function probeFridaServer(exec) {
93
+ const out = { serverRunning: false, port: null };
94
+ // Look for frida-server in process list (matches frida-server-* prebuilds)
95
+ const ps = await exec('adb shell "pgrep -f frida-server" 2>&1');
96
+ if (ps.code === 0 && /^\d+/m.test(ps.stdout.trim())) out.serverRunning = true;
97
+ // Default port 27042; check if it's listening
98
+ const ns = await exec('adb shell "netstat -tln 2>/dev/null | grep -E \':27042\\b\'"');
99
+ if (ns.code === 0 && ns.stdout.includes("27042")) {
100
+ out.serverRunning = true;
101
+ out.port = 27042;
102
+ } else if (out.serverRunning) {
103
+ out.port = 27042;
104
+ }
105
+ return out;
106
+ }
107
+
108
+ async function probeWeChat(exec) {
109
+ const out = { installed: false, versionName: null, majorVersion: null };
110
+ const r = await exec('adb shell "dumpsys package com.tencent.mm | grep versionName"');
111
+ if (r.code !== 0) return out;
112
+ const m = /versionName=([\d.]+)/i.exec(r.stdout);
113
+ if (!m) return out;
114
+ out.installed = true;
115
+ out.versionName = m[1];
116
+ const major = parseInt(m[1].split(".")[0], 10);
117
+ out.majorVersion = Number.isFinite(major) ? major : null;
118
+ return out;
119
+ }
120
+
121
+ /**
122
+ * Decide suggested KeyProvider given probed capabilities.
123
+ * Exported separately so callers can also pass synthetic facts.
124
+ */
125
+ function decide({ device, root, frida, wechat }) {
126
+ const reasons = [];
127
+ if (!device.reachable) {
128
+ return { suggestedKeyProvider: "unsupported", reasons: ["No adb device reachable"] };
129
+ }
130
+ if (!wechat.installed) {
131
+ return { suggestedKeyProvider: "unsupported", reasons: ["WeChat (com.tencent.mm) not installed"] };
132
+ }
133
+ const major = wechat.majorVersion;
134
+ if (major != null && major < 8) {
135
+ reasons.push(`WeChat ${wechat.versionName} (< 8.0) — legacy MD5(IMEI+UIN) path supported`);
136
+ return { suggestedKeyProvider: "md5", reasons };
137
+ }
138
+ // 8.0+ requires frida
139
+ if (!root.detected) {
140
+ return {
141
+ suggestedKeyProvider: "unsupported",
142
+ reasons: [`WeChat ${wechat.versionName || "8.x"} requires root for SQLCipher key extraction, root not detected`],
143
+ };
144
+ }
145
+ if (!frida.serverRunning) {
146
+ return {
147
+ suggestedKeyProvider: "unsupported",
148
+ reasons: [
149
+ `WeChat ${wechat.versionName || "8.x"} requires Frida hook`,
150
+ "frida-server not running on device — see Frida Setup runbook",
151
+ ],
152
+ };
153
+ }
154
+ reasons.push(`WeChat ${wechat.versionName} (≥ 8.0) — Frida hook on libwcdb.so sqlite3_key`);
155
+ if (!root.magiskInstalled) {
156
+ reasons.push("Magisk not detected — DenyList configuration unavailable; reverse-detection may be weaker");
157
+ }
158
+ return { suggestedKeyProvider: "frida", reasons };
159
+ }
160
+
161
+ /**
162
+ * Top-level probe.
163
+ *
164
+ * @param {object} [opts]
165
+ * @param {Function} [opts.exec] injected exec(cmd, opts) → {code, stdout, stderr}
166
+ * @returns {Promise<object>} full probe shape (see file header)
167
+ */
168
+ async function probe(opts = {}) {
169
+ const exec = typeof opts.exec === "function" ? opts.exec : defaultExec();
170
+ const warnings = [];
171
+
172
+ const device = await probeDevice(exec);
173
+ if (!device.reachable) {
174
+ return {
175
+ ok: false,
176
+ suggestedKeyProvider: "unsupported",
177
+ reasons: ["No adb device reachable — enable USB debugging and reconnect"],
178
+ device,
179
+ root: { detected: false, magiskInstalled: false },
180
+ frida: { serverRunning: false, port: null },
181
+ wechat: { installed: false, versionName: null, majorVersion: null },
182
+ warnings,
183
+ };
184
+ }
185
+
186
+ const [root, frida, wechat] = await Promise.all([
187
+ probeRoot(exec),
188
+ probeFridaServer(exec),
189
+ probeWeChat(exec),
190
+ ]);
191
+
192
+ if (device.abi && !/^arm/.test(device.abi)) {
193
+ warnings.push(`ABI ${device.abi} — Frida prebuilt may need manual build for non-ARM target`);
194
+ }
195
+
196
+ const { suggestedKeyProvider, reasons } = decide({ device, root, frida, wechat });
197
+
198
+ return {
199
+ ok: suggestedKeyProvider !== "unsupported",
200
+ suggestedKeyProvider,
201
+ reasons,
202
+ device,
203
+ root,
204
+ frida,
205
+ wechat,
206
+ warnings,
207
+ };
208
+ }
209
+
210
+ module.exports = {
211
+ probe,
212
+ decide,
213
+ // exposed for fine-grained testing
214
+ probeDevice,
215
+ probeRoot,
216
+ probeFridaServer,
217
+ probeWeChat,
218
+ };
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Phase 12.6.2 — Frida agent loader (host side).
3
+ *
4
+ * Reads the raw agent JS so FridaKeyProvider can pass it to
5
+ * `session.createScript(text)`. Also exposes `runUnderMock()` so the
6
+ * host can unit-test the agent's behavior by injecting fake Frida
7
+ * globals (Module / Interceptor / Process / send / setTimeout).
8
+ *
9
+ * The agent itself (wechat-key-hook.js) is plain JS — no `require` or
10
+ * `module.exports` — because Frida loads it as a text blob inside the
11
+ * target process. The loader hides that detail from callers.
12
+ */
13
+ "use strict";
14
+
15
+ const fs = require("node:fs");
16
+ const path = require("node:path");
17
+ const vm = require("node:vm");
18
+
19
+ const AGENT_PATH = path.join(__dirname, "wechat-key-hook.js");
20
+
21
+ function loadAgentScript() {
22
+ return fs.readFileSync(AGENT_PATH, "utf-8");
23
+ }
24
+
25
+ /**
26
+ * Execute the agent script in a sandboxed context where the caller
27
+ * supplies the Frida globals. Used for unit tests.
28
+ *
29
+ * @param {object} mocks
30
+ * @param {object} mocks.Module mock with findExportByName(modName, sym)
31
+ * @param {object} mocks.Process mock with findModuleByName(modName)
32
+ * @param {object} mocks.Interceptor mock with attach(addr, handlers)
33
+ * @param {Function} mocks.send captures send() calls
34
+ * @param {Function} [mocks.setTimeout] defaults to global setTimeout
35
+ * @returns {object} the sandbox context after execution
36
+ */
37
+ function runAgentUnderMock(mocks = {}) {
38
+ if (!mocks.Module || typeof mocks.Module.findExportByName !== "function") {
39
+ throw new Error("runAgentUnderMock: mocks.Module.findExportByName required");
40
+ }
41
+ if (!mocks.Process || typeof mocks.Process.findModuleByName !== "function") {
42
+ throw new Error("runAgentUnderMock: mocks.Process.findModuleByName required");
43
+ }
44
+ if (!mocks.Interceptor || typeof mocks.Interceptor.attach !== "function") {
45
+ throw new Error("runAgentUnderMock: mocks.Interceptor.attach required");
46
+ }
47
+ if (typeof mocks.send !== "function") {
48
+ throw new Error("runAgentUnderMock: mocks.send required");
49
+ }
50
+ const sandbox = {
51
+ Module: mocks.Module,
52
+ Process: mocks.Process,
53
+ Interceptor: mocks.Interceptor,
54
+ send: mocks.send,
55
+ setTimeout: mocks.setTimeout || setTimeout,
56
+ };
57
+ const ctx = vm.createContext(sandbox);
58
+ const src = loadAgentScript();
59
+ vm.runInContext(src, ctx, { filename: "wechat-key-hook.js" });
60
+ return sandbox;
61
+ }
62
+
63
+ module.exports = {
64
+ AGENT_PATH,
65
+ loadAgentScript,
66
+ runAgentUnderMock,
67
+ };
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Phase 12.6.2 — Frida agent: WeChat SQLCipher key hook.
3
+ *
4
+ * This script runs INSIDE the WeChat process (com.tencent.mm) after
5
+ * being injected by the FridaKeyProvider on the host. It hooks the
6
+ * sqlite3_key family of symbols in libwcdb.so (WeChat's SQLCipher fork)
7
+ * and sends the captured 32-byte key hex back to the host via Frida's
8
+ * `send()` channel.
9
+ *
10
+ * Wire contract (host expects):
11
+ * { kind: "key", hex: "<lowercase hex>", source: "<symbol-name>" }
12
+ * { kind: "hooked", symbol: "<symbol>", module: "libwcdb.so" }
13
+ * { kind: "error", message: "<reason>" }
14
+ * { kind: "module-waiting", module: "libwcdb.so" }
15
+ *
16
+ * Design references:
17
+ * - Adapter_WeChat_SQLCipher.md §18.3 (agent script structure)
18
+ * - §18.6 (anti-detection — hook at module-load time)
19
+ *
20
+ * Notes on portability:
21
+ * - This file is loaded by the host as raw text and passed verbatim
22
+ * to `session.createScript(...)`. It MUST NOT use any node `require()`
23
+ * or host-only APIs. Frida injects its own runtime (Module,
24
+ * Interceptor, Process, send, etc.) at runtime.
25
+ * - Keep dependencies on host helpers (e.g. esbuild) zero. The whole
26
+ * agent is ≤ 120 LOC of plain JS — no compilation step needed.
27
+ */
28
+
29
+ /* eslint-disable */
30
+ /* global Module, Interceptor, Process, send, setTimeout */
31
+
32
+ "use strict";
33
+
34
+ (function () {
35
+ var TARGET_MODULE = "libwcdb.so";
36
+ // Primary symbol per §18.3. Add fallbacks below — version drift will
37
+ // shift the export name; host treats first hit as authoritative.
38
+ var SYMBOLS = [
39
+ "sqlite3_key",
40
+ "sqlite3_key_v2",
41
+ "wcdb_setkey",
42
+ "WCDBKeyDerive",
43
+ // C++-mangled symbols (Itanium ABI) — rare but seen in WCDB 1.x
44
+ "_ZN4WCDB8Database13setCipherKeyERKNSt6__ndk112basic_stringIcNS1_11char_traitsIcEENS1_9allocatorIcEEEE",
45
+ ];
46
+
47
+ function bytesToHex(buf) {
48
+ if (!buf || buf.byteLength === 0) return "";
49
+ var bytes = new Uint8Array(buf);
50
+ var out = "";
51
+ for (var i = 0; i < bytes.length; i++) {
52
+ var b = bytes[i].toString(16);
53
+ if (b.length < 2) b = "0" + b;
54
+ out += b;
55
+ }
56
+ return out;
57
+ }
58
+
59
+ // Track which symbol fired first; only emit the first key event so
60
+ // the host detaches quickly (anti-detection §18.6 #4).
61
+ var fired = false;
62
+
63
+ function makeHook(symbolName) {
64
+ return {
65
+ onEnter: function (args) {
66
+ if (fired) return;
67
+ try {
68
+ // sqlite3_key signature: int sqlite3_key(sqlite3 *db, const void *pKey, int nKey)
69
+ // args[1] = key bytes, args[2] = key length
70
+ var len = args[2].toInt32();
71
+ if (len <= 0 || len > 256) {
72
+ send({ kind: "error", message: "implausible key length " + len + " at " + symbolName });
73
+ return;
74
+ }
75
+ var buf = args[1].readByteArray(len);
76
+ var hex = bytesToHex(buf);
77
+ if (!hex) {
78
+ send({ kind: "error", message: "empty key buffer at " + symbolName });
79
+ return;
80
+ }
81
+ fired = true;
82
+ send({ kind: "key", hex: hex, source: symbolName });
83
+ } catch (e) {
84
+ send({ kind: "error", message: "hook exception at " + symbolName + ": " + (e && e.message ? e.message : String(e)) });
85
+ }
86
+ },
87
+ };
88
+ }
89
+
90
+ function tryAttach() {
91
+ var mod = Process.findModuleByName(TARGET_MODULE);
92
+ if (!mod) return false;
93
+ var attached = 0;
94
+ for (var i = 0; i < SYMBOLS.length; i++) {
95
+ var addr = Module.findExportByName(TARGET_MODULE, SYMBOLS[i]);
96
+ if (!addr) continue;
97
+ try {
98
+ Interceptor.attach(addr, makeHook(SYMBOLS[i]));
99
+ send({ kind: "hooked", symbol: SYMBOLS[i], module: TARGET_MODULE });
100
+ attached++;
101
+ } catch (e) {
102
+ send({ kind: "error", message: "Interceptor.attach failed for " + SYMBOLS[i] + ": " + (e && e.message ? e.message : String(e)) });
103
+ }
104
+ }
105
+ return attached > 0;
106
+ }
107
+
108
+ // Module-load polling — §18.6 #1 "hook at module-load time before
109
+ // anti-detection thread runs". WeChat lazy-loads libwcdb when the
110
+ // first DB opens, so we can't always find it at script start.
111
+ if (!tryAttach()) {
112
+ send({ kind: "module-waiting", module: TARGET_MODULE });
113
+ var attempts = 0;
114
+ var poll = function () {
115
+ attempts++;
116
+ if (tryAttach()) return;
117
+ if (attempts >= 60) {
118
+ // 60 attempts × 500ms = 30s ceiling, matches host timeoutMs
119
+ send({ kind: "error", message: TARGET_MODULE + " did not load within 30s" });
120
+ return;
121
+ }
122
+ setTimeout(poll, 500);
123
+ };
124
+ setTimeout(poll, 500);
125
+ }
126
+ })();
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+
3
+ const { WechatAdapter, NAME, VERSION } = require("./wechat-adapter");
4
+ const { parseContent, parseXmlAttrs, extractTag, isGroupTalker, TYPE_NAMES, APPMSG_SUBTYPES } = require("./content-parser");
5
+ const { extractWeChatKey, deriveLegacyKey, extractUinFromPrefs, extractImeiFromCompatibleInfo } = require("./key-extractor");
6
+ const { WeChatDBReader, KNOWN_PRAGMA_PROFILES } = require("./db-reader");
7
+ const { normalizeMessage, normalizeContact, wxidToPersonId } = require("./normalize");
8
+ const { KeyProvider, MD5KeyProvider, FridaKeyProvider } = require("./key-providers");
9
+ const envProbe = require("./env-probe");
10
+ const { bootstrapWechatAdapter } = require("./bootstrap");
11
+
12
+ module.exports = {
13
+ WechatAdapter,
14
+ WECHAT_NAME: NAME,
15
+ WECHAT_VERSION: VERSION,
16
+ parseWeChatContent: parseContent,
17
+ parseWeChatXmlAttrs: parseXmlAttrs,
18
+ extractWeChatTag: extractTag,
19
+ isWeChatGroupTalker: isGroupTalker,
20
+ WECHAT_TYPE_NAMES: TYPE_NAMES,
21
+ WECHAT_APPMSG_SUBTYPES: APPMSG_SUBTYPES,
22
+ extractWeChatKey,
23
+ deriveWeChatLegacyKey: deriveLegacyKey,
24
+ extractWeChatUinFromPrefs: extractUinFromPrefs,
25
+ extractWeChatImeiFromCompatibleInfo: extractImeiFromCompatibleInfo,
26
+ WeChatDBReader,
27
+ WECHAT_PRAGMA_PROFILES: KNOWN_PRAGMA_PROFILES,
28
+ normalizeWeChatMessage: normalizeMessage,
29
+ normalizeWeChatContact: normalizeContact,
30
+ wxidToWeChatPersonId: wxidToPersonId,
31
+ WeChatKeyProvider: KeyProvider,
32
+ WeChatMD5KeyProvider: MD5KeyProvider,
33
+ WeChatFridaKeyProvider: FridaKeyProvider,
34
+ probeWeChatEnv: envProbe.probe,
35
+ decideWeChatKeyProvider: envProbe.decide,
36
+ bootstrapWechatAdapter,
37
+ };