@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,158 @@
1
+ /**
2
+ * Phase 12 v0.5 — WeChat legacy key extractor (frida-INDEPENDENT).
3
+ *
4
+ * Ports sjqz/parsers/wechat_decrypt.py legacy path to Node:
5
+ *
6
+ * key = MD5(IMEI + UIN)[:7].lower()
7
+ *
8
+ * Works for WeChat versions < 8.0.X where the IMEI-derived key path is
9
+ * still active. WeChat 8.0+ requires Frida hook on `sqlite3_key` —
10
+ * that's Phase 12.6 (frida-dependent) and ships when device + Frida are
11
+ * available.
12
+ *
13
+ * Inputs:
14
+ * - wechatDataPath: directory mirroring /data/data/com.tencent.mm/
15
+ * after `adb pull` (or PC WeChat Files directory)
16
+ * - Optional explicit overrides (imei, uin, manualKey) for testing or
17
+ * when CompatibleInfo.cfg parsing fails
18
+ *
19
+ * Outputs:
20
+ * {
21
+ * uin: "1234567890",
22
+ * imei: "1234567890abcdef",
23
+ * key: "5d41402", // 7-char hex MD5 prefix
24
+ * source: "auth-xml+compatible-cfg" | "manual" | "...",
25
+ * warnings: [...]
26
+ * }
27
+ */
28
+
29
+ "use strict";
30
+
31
+ const fs = require("node:fs");
32
+ const path = require("node:path");
33
+ const crypto = require("node:crypto");
34
+
35
+ /**
36
+ * Extract UIN from shared_prefs/auth_info_key_prefs.xml or
37
+ * system_config_prefs.xml. UIN may be negative; can also be in
38
+ * `default_uin` or `_auth_uin` keys depending on WeChat version.
39
+ */
40
+ function extractUinFromPrefs(wechatDataPath) {
41
+ const candidates = [
42
+ path.join(wechatDataPath, "shared_prefs", "auth_info_key_prefs.xml"),
43
+ path.join(wechatDataPath, "shared_prefs", "system_config_prefs.xml"),
44
+ ];
45
+ for (const p of candidates) {
46
+ if (!fs.existsSync(p)) continue;
47
+ try {
48
+ const content = fs.readFileSync(p, "utf-8");
49
+ const patterns = [
50
+ /<int name="[^"]*_auth_uin[^"]*"\s+value="(-?\d+)"/,
51
+ /<int name="default_uin"\s+value="(-?\d+)"/,
52
+ /<int name="[^"]*uin[^"]*"\s+value="(-?\d+)"/,
53
+ ];
54
+ for (const re of patterns) {
55
+ const m = re.exec(content);
56
+ if (m) return { uin: m[1], from: path.basename(p) };
57
+ }
58
+ } catch (_e) {
59
+ // Try next candidate
60
+ }
61
+ }
62
+ return { uin: null, from: null };
63
+ }
64
+
65
+ /**
66
+ * Extract IMEI / device serial from CompatibleInfo.cfg. The file is a
67
+ * Java HashMap serialization; we use string-search for 15-digit IMEI
68
+ * patterns + GUIDs as fallback (matches sjqz approach).
69
+ */
70
+ function extractImeiFromCompatibleInfo(wechatDataPath) {
71
+ const cfgPath = path.join(wechatDataPath, "MicroMsg", "CompatibleInfo.cfg");
72
+ if (!fs.existsSync(cfgPath)) return { imei: null, from: null };
73
+ try {
74
+ const buf = fs.readFileSync(cfgPath);
75
+ const text = buf.toString("binary"); // 8-bit safe — we don't care about decoding
76
+ // 15-digit IMEI
77
+ const imeiMatch = /\D(\d{15})\D/.exec(text);
78
+ if (imeiMatch) return { imei: imeiMatch[1], from: "CompatibleInfo.cfg (15-digit)" };
79
+ // Fallback: 14-digit + check digit pattern
80
+ const imei14 = /\D(\d{14})\D/.exec(text);
81
+ if (imei14) return { imei: imei14[1], from: "CompatibleInfo.cfg (14-digit)" };
82
+ // Fallback: GUID-like
83
+ const guid = /([0-9a-f]{32})/i.exec(text);
84
+ if (guid) return { imei: guid[1], from: "CompatibleInfo.cfg (guid)" };
85
+ } catch (_e) {}
86
+ return { imei: null, from: null };
87
+ }
88
+
89
+ /**
90
+ * Derive the SQLCipher key.
91
+ *
92
+ * @param {string} imei
93
+ * @param {string|number} uin
94
+ * @returns {string} 7-char hex prefix of MD5(IMEI+UIN), lowercase
95
+ */
96
+ function deriveLegacyKey(imei, uin) {
97
+ if (typeof imei !== "string" || imei.length === 0) {
98
+ throw new Error("deriveLegacyKey: imei required");
99
+ }
100
+ if (uin == null) throw new Error("deriveLegacyKey: uin required");
101
+ const raw = String(imei) + String(uin);
102
+ return crypto.createHash("md5").update(raw, "utf-8").digest("hex").slice(0, 7).toLowerCase();
103
+ }
104
+
105
+ /**
106
+ * Top-level: extract key from a pulled WeChat data directory.
107
+ *
108
+ * @param {object} opts
109
+ * @param {string} opts.wechatDataPath directory like the pulled
110
+ * /data/data/com.tencent.mm/ tree
111
+ * @param {string} [opts.uin] override (skip auth XML parse)
112
+ * @param {string} [opts.imei] override (skip CompatibleInfo)
113
+ * @returns {object} { uin, imei, key, source, warnings }
114
+ */
115
+ function extractWeChatKey(opts = {}) {
116
+ if (!opts.wechatDataPath || typeof opts.wechatDataPath !== "string") {
117
+ throw new Error("extractWeChatKey: opts.wechatDataPath required");
118
+ }
119
+ const warnings = [];
120
+
121
+ let uin = opts.uin || null;
122
+ let uinSource = "manual";
123
+ if (!uin) {
124
+ const r = extractUinFromPrefs(opts.wechatDataPath);
125
+ uin = r.uin;
126
+ uinSource = r.from || "missing";
127
+ if (!uin) warnings.push("UIN not found in shared_prefs — adapter unusable without manual override");
128
+ }
129
+
130
+ let imei = opts.imei || null;
131
+ let imeiSource = "manual";
132
+ if (!imei) {
133
+ const r = extractImeiFromCompatibleInfo(opts.wechatDataPath);
134
+ imei = r.imei;
135
+ imeiSource = r.from || "missing";
136
+ if (!imei) warnings.push("IMEI not found in CompatibleInfo.cfg — adapter unusable without manual override");
137
+ }
138
+
139
+ if (!uin || !imei) {
140
+ return { uin, imei, key: null, source: `uin:${uinSource} | imei:${imeiSource}`, warnings };
141
+ }
142
+
143
+ const key = deriveLegacyKey(imei, uin);
144
+ return {
145
+ uin,
146
+ imei,
147
+ key,
148
+ source: `uin:${uinSource} | imei:${imeiSource}`,
149
+ warnings,
150
+ };
151
+ }
152
+
153
+ module.exports = {
154
+ extractWeChatKey,
155
+ deriveLegacyKey,
156
+ extractUinFromPrefs,
157
+ extractImeiFromCompatibleInfo,
158
+ };
@@ -0,0 +1,244 @@
1
+ /**
2
+ * Phase 12.6.3 — FridaKeyProvider (v1 hot path).
3
+ *
4
+ * Attaches frida to a live WeChat process (com.tencent.mm) on a rooted
5
+ * Android device, injects the wechat-key-hook agent (see
6
+ * frida-agent/wechat-key-hook.js), waits for the first sqlite3_key
7
+ * onEnter, captures the 32-byte hex key, then detaches.
8
+ *
9
+ * Why detach immediately:
10
+ * §18.6 anti-detection — minimize injection window so WeChat's
11
+ * ptrace-tracer / mem-scanner doesn't catch frida-gum sitting in
12
+ * the process. We hold the script alive only as long as it takes
13
+ * the user to touch a chat thread (typically 1-3s).
14
+ *
15
+ * Wire to KeyProvider:
16
+ * getKey() resolves with lowercase 64-char hex on success, or
17
+ * rejects with one of the typed error codes:
18
+ * - FRIDA_BINDING_MISSING : opts.frida not provided and require()
19
+ * of "frida" failed (binding not installed)
20
+ * - WECHAT_NOT_RUNNING : device.attach() threw on package name
21
+ * - FRIDA_ATTACH_FAILED : any other attach/createScript error
22
+ * - HOOK_FAILED : agent reported error event before key
23
+ * - WCDB_KEY_TIMEOUT : no key event within timeoutMs
24
+ *
25
+ * Test seam: opts.frida overrides the lazy require("frida"), so unit
26
+ * tests inject a mock device manager without touching the real binding.
27
+ */
28
+ "use strict";
29
+
30
+ const { KeyProvider } = require("./key-provider-base");
31
+ const { loadAgentScript } = require("../frida-agent/loader");
32
+
33
+ class FridaKeyProvider extends KeyProvider {
34
+ /**
35
+ * @param {object} opts
36
+ * @param {object} [opts.frida] injected nodejs binding (test seam);
37
+ * if absent, lazy require("frida")
38
+ * @param {string} [opts.deviceId] Frida device id (USB device default
39
+ * if omitted; "local" for Wear/host)
40
+ * @param {string} [opts.packageName="com.tencent.mm"]
41
+ * @param {number} [opts.timeoutMs=30000]
42
+ * @param {Function} [opts.agentLoader] test seam: returns agent script
43
+ * text; defaults to loadAgentScript
44
+ * @param {Function} [opts.logger] optional log({level, ...evt})
45
+ */
46
+ constructor(opts = {}) {
47
+ super();
48
+ if (!opts || typeof opts !== "object") {
49
+ throw new Error("FridaKeyProvider: opts required");
50
+ }
51
+ this._fridaInjected = opts.frida || null;
52
+ this._deviceId = opts.deviceId || null;
53
+ this._packageName = opts.packageName || "com.tencent.mm";
54
+ this._timeoutMs = Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0
55
+ ? opts.timeoutMs
56
+ : 30_000;
57
+ this._agentLoader = typeof opts.agentLoader === "function"
58
+ ? opts.agentLoader
59
+ : loadAgentScript;
60
+ this._logger = typeof opts.logger === "function" ? opts.logger : null;
61
+ this._lastTelemetry = null;
62
+ }
63
+
64
+ get name() {
65
+ return "frida";
66
+ }
67
+
68
+ getLastTelemetry() {
69
+ return this._lastTelemetry;
70
+ }
71
+
72
+ _log(evt) {
73
+ if (this._logger) {
74
+ try { this._logger(evt); } catch (_e) { /* swallow logger faults */ }
75
+ }
76
+ }
77
+
78
+ _loadFrida() {
79
+ if (this._fridaInjected) return this._fridaInjected;
80
+ try {
81
+ // eslint-disable-next-line global-require
82
+ return require("frida");
83
+ } catch (err) {
84
+ const e = new Error(
85
+ "FridaKeyProvider: frida nodejs binding not installed. " +
86
+ "Install with `npm install frida` on the host, or pass opts.frida. " +
87
+ "Underlying error: " + (err && err.message ? err.message : String(err))
88
+ );
89
+ e.code = "FRIDA_BINDING_MISSING";
90
+ throw e;
91
+ }
92
+ }
93
+
94
+ async _getDevice(frida) {
95
+ if (this._deviceId) {
96
+ const dev = await frida.getDevice(this._deviceId);
97
+ return dev;
98
+ }
99
+ // No id → first USB device
100
+ if (typeof frida.getUsbDevice === "function") {
101
+ return await frida.getUsbDevice();
102
+ }
103
+ return await frida.getDeviceManager().getUsbDevice();
104
+ }
105
+
106
+ /**
107
+ * @returns {Promise<string>} 64-char lowercase hex SQLCipher key
108
+ */
109
+ async getKey(_callOpts) {
110
+ const telemetry = {
111
+ startedAt: Date.now(),
112
+ packageName: this._packageName,
113
+ deviceId: this._deviceId,
114
+ hooked: [],
115
+ errors: [],
116
+ keySource: null,
117
+ durationMs: null,
118
+ };
119
+
120
+ const frida = this._loadFrida();
121
+ let device, session, script;
122
+
123
+ try {
124
+ device = await this._getDevice(frida);
125
+ } catch (err) {
126
+ const e = new Error(
127
+ "FridaKeyProvider: failed to acquire Frida device" +
128
+ (this._deviceId ? ` (${this._deviceId})` : "") +
129
+ ": " + (err && err.message ? err.message : String(err))
130
+ );
131
+ e.code = "FRIDA_ATTACH_FAILED";
132
+ this._lastTelemetry = telemetry;
133
+ throw e;
134
+ }
135
+
136
+ try {
137
+ session = await device.attach(this._packageName);
138
+ } catch (err) {
139
+ const errMsg = err && err.message ? err.message : String(err);
140
+ const e = new Error(
141
+ `FridaKeyProvider: device.attach(${this._packageName}) failed: ${errMsg}`
142
+ );
143
+ // Distinguish "process not found" vs other attach errors
144
+ e.code = /unable to find process|process not found/i.test(errMsg)
145
+ ? "WECHAT_NOT_RUNNING"
146
+ : "FRIDA_ATTACH_FAILED";
147
+ this._lastTelemetry = telemetry;
148
+ throw e;
149
+ }
150
+
151
+ try {
152
+ const agentSrc = this._agentLoader();
153
+ script = await session.createScript(agentSrc);
154
+ } catch (err) {
155
+ const e = new Error(
156
+ "FridaKeyProvider: createScript failed: " +
157
+ (err && err.message ? err.message : String(err))
158
+ );
159
+ e.code = "FRIDA_ATTACH_FAILED";
160
+ this._lastTelemetry = telemetry;
161
+ // Clean up the session before throwing
162
+ try { await session.detach(); } catch (_e) {}
163
+ throw e;
164
+ }
165
+
166
+ // Promise resolves on the first 'key' message; rejects on the first
167
+ // 'error' (after script load) or after timeoutMs without key.
168
+ const keyHex = await new Promise((resolve, reject) => {
169
+ let settled = false;
170
+ let timer = null;
171
+
172
+ const cleanup = async () => {
173
+ if (timer) { clearTimeout(timer); timer = null; }
174
+ try { await script.unload(); } catch (_e) {}
175
+ try { await session.detach(); } catch (_e) {}
176
+ };
177
+
178
+ const onMessage = (message, _data) => {
179
+ if (settled) return;
180
+ if (!message || message.type !== "send" || !message.payload) return;
181
+ const evt = message.payload;
182
+ this._log({ level: "info", kind: "frida-message", evt });
183
+
184
+ if (evt.kind === "hooked") {
185
+ telemetry.hooked.push({ symbol: evt.symbol, module: evt.module });
186
+ return;
187
+ }
188
+ if (evt.kind === "module-waiting") {
189
+ return; // informational
190
+ }
191
+ if (evt.kind === "key") {
192
+ settled = true;
193
+ telemetry.keySource = evt.source;
194
+ telemetry.durationMs = Date.now() - telemetry.startedAt;
195
+ cleanup().then(() => resolve(String(evt.hex || "").toLowerCase()));
196
+ return;
197
+ }
198
+ if (evt.kind === "error") {
199
+ telemetry.errors.push(evt.message);
200
+ // Don't reject on individual hook errors; we may still get a
201
+ // key from a fallback symbol. Only reject on timeout.
202
+ return;
203
+ }
204
+ };
205
+
206
+ script.message.connect(onMessage);
207
+
208
+ script.load().catch((err) => {
209
+ if (settled) return;
210
+ settled = true;
211
+ cleanup().then(() => {
212
+ const e = new Error(
213
+ "FridaKeyProvider: script.load failed: " +
214
+ (err && err.message ? err.message : String(err))
215
+ );
216
+ e.code = "FRIDA_ATTACH_FAILED";
217
+ reject(e);
218
+ });
219
+ });
220
+
221
+ timer = setTimeout(() => {
222
+ if (settled) return;
223
+ settled = true;
224
+ cleanup().then(() => {
225
+ const last = telemetry.errors.length > 0
226
+ ? ` (last hook error: ${telemetry.errors[telemetry.errors.length - 1]})`
227
+ : "";
228
+ const e = new Error(
229
+ `FridaKeyProvider: no sqlite3_key call within ${this._timeoutMs}ms` +
230
+ (telemetry.hooked.length === 0 ? " — libwcdb.so never loaded; " +
231
+ "did the user touch a chat thread?" : "") + last
232
+ );
233
+ e.code = "WCDB_KEY_TIMEOUT";
234
+ reject(e);
235
+ });
236
+ }, this._timeoutMs);
237
+ });
238
+
239
+ this._lastTelemetry = telemetry;
240
+ return keyHex;
241
+ }
242
+ }
243
+
244
+ module.exports = { FridaKeyProvider };
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+
3
+ const { KeyProvider } = require("./key-provider-base");
4
+ const { MD5KeyProvider } = require("./md5-key-provider");
5
+
6
+ // FridaKeyProvider depends on the optional `frida` nodejs binding. Load
7
+ // lazily so users on devices without the binding can still use the v0.5
8
+ // MD5 path. Phase 12.6.3 ships the implementation.
9
+ let FridaKeyProvider = null;
10
+ try {
11
+ // eslint-disable-next-line global-require
12
+ ({ FridaKeyProvider } = require("./frida-key-provider"));
13
+ } catch (_e) {
14
+ // Module not yet built / frida binding missing — leave null. Callers
15
+ // that need it should require it directly so they see the real error.
16
+ }
17
+
18
+ module.exports = {
19
+ KeyProvider,
20
+ MD5KeyProvider,
21
+ FridaKeyProvider,
22
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Phase 12.6 — KeyProvider interface contract.
3
+ *
4
+ * The wechat-adapter is key-source agnostic: it only knows about an
5
+ * object with `getKey()` returning a Promise<string> (32-hex SQLCipher
6
+ * key for v0.5 7-char prefix, or full 64-hex for Frida hot path).
7
+ *
8
+ * Two implementations:
9
+ * - MD5KeyProvider (v0.5, frida-INDEPENDENT) — derives MD5(IMEI+UIN)[:7]
10
+ * from on-disk WeChat data dir. Works for WeChat < 8.0.x.
11
+ * - FridaKeyProvider (v1, frida-DEPENDENT) — attaches frida to live
12
+ * WeChat process and hooks sqlite3_key. Works for WeChat 8.0+.
13
+ *
14
+ * Both expose the same getKey() shape so wechat-adapter.js does not
15
+ * branch on version.
16
+ */
17
+ "use strict";
18
+
19
+ class KeyProvider {
20
+ /**
21
+ * Return the SQLCipher key (lowercase hex). Throw on failure.
22
+ *
23
+ * Optional opts (per design §18.2):
24
+ * - wxid : string WeChat user identifier (some providers need this)
25
+ * - dbPath : string path to the SQLCipher DB being opened
26
+ *
27
+ * @param {{wxid?: string, dbPath?: string}} [_opts]
28
+ * @returns {Promise<string>}
29
+ */
30
+ // eslint-disable-next-line no-unused-vars
31
+ async getKey(_opts) {
32
+ throw new Error("KeyProvider.getKey: must be overridden by subclass");
33
+ }
34
+
35
+ /**
36
+ * Provider name for telemetry / error attribution. Subclasses
37
+ * override.
38
+ */
39
+ get name() {
40
+ return "key-provider-base";
41
+ }
42
+ }
43
+
44
+ module.exports = { KeyProvider };
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Phase 12.6.1 — MD5KeyProvider (v0.5 legacy WeChat < 8.0 path).
3
+ *
4
+ * Wraps the existing key-extractor.js (MD5(IMEI+UIN)[:7] lowercase)
5
+ * behind the KeyProvider interface. Pure frida-independent: works from
6
+ * a pulled WeChat data directory (`adb pull /data/data/com.tencent.mm/`).
7
+ *
8
+ * Usage:
9
+ * const provider = new MD5KeyProvider({
10
+ * wechatDataPath: "/tmp/com.tencent.mm",
11
+ * // optional manual overrides for testing or when CompatibleInfo.cfg
12
+ * // parsing fails
13
+ * uin: "1234567890",
14
+ * imei: "1234567890abcdef",
15
+ * });
16
+ * const key = await provider.getKey();
17
+ */
18
+ "use strict";
19
+
20
+ const { KeyProvider } = require("./key-provider-base");
21
+ const { extractWeChatKey } = require("../key-extractor");
22
+
23
+ class MD5KeyProvider extends KeyProvider {
24
+ /**
25
+ * @param {object} opts
26
+ * @param {string} opts.wechatDataPath directory mirroring the pulled
27
+ * /data/data/com.tencent.mm/ tree
28
+ * @param {string} [opts.uin] override (skip auth XML parse)
29
+ * @param {string} [opts.imei] override (skip CompatibleInfo)
30
+ * @param {Function} [opts.extractor] DI seam — defaults to
31
+ * extractWeChatKey
32
+ */
33
+ constructor(opts = {}) {
34
+ super();
35
+ if (!opts || typeof opts !== "object") {
36
+ throw new Error("MD5KeyProvider: opts required");
37
+ }
38
+ if (!opts.wechatDataPath || typeof opts.wechatDataPath !== "string") {
39
+ throw new Error("MD5KeyProvider: opts.wechatDataPath required");
40
+ }
41
+ this._wechatDataPath = opts.wechatDataPath;
42
+ this._uinOverride = opts.uin || null;
43
+ this._imeiOverride = opts.imei || null;
44
+ this._extractor = typeof opts.extractor === "function"
45
+ ? opts.extractor
46
+ : extractWeChatKey;
47
+ this._lastResult = null;
48
+ }
49
+
50
+ get name() {
51
+ return "md5";
52
+ }
53
+
54
+ /**
55
+ * @returns {Promise<string>} 7-char lowercase hex MD5 prefix
56
+ */
57
+ async getKey() {
58
+ const result = this._extractor({
59
+ wechatDataPath: this._wechatDataPath,
60
+ uin: this._uinOverride,
61
+ imei: this._imeiOverride,
62
+ });
63
+ this._lastResult = result;
64
+ if (!result || !result.key) {
65
+ const warnings = (result && result.warnings) || [];
66
+ const reason = warnings.length > 0 ? warnings.join("; ") : "key extraction returned empty";
67
+ throw new Error(`MD5KeyProvider.getKey: ${reason}`);
68
+ }
69
+ return result.key;
70
+ }
71
+
72
+ /**
73
+ * Last extraction result for telemetry / debugging — exposes uin /
74
+ * imei sources and warnings. Returns null until getKey() called.
75
+ */
76
+ getLastResult() {
77
+ return this._lastResult;
78
+ }
79
+ }
80
+
81
+ module.exports = { MD5KeyProvider };