chrome-tools 1.1.14 → 1.1.15

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.
@@ -7,7 +7,7 @@ import type { ChromePlatform } from "./types.js";
7
7
  * Cipher: AES-256-GCM (nonce + tag carried in each blob).
8
8
  *
9
9
  * Chrome 127+ adds `os_crypt.app_bound_encrypted_key` — base64, "APPB"-prefixed.
10
- * After DPAPI unprotection, the result is fed to Chrome's IElevator COM service
11
- * via the native helper to obtain the real 32-byte AES key for "v20" blobs.
10
+ * It is passed directly to Chrome's IElevator COM service via the native helper
11
+ * to obtain the 32-byte AES-256 key for "v20" blobs.
12
12
  */
13
13
  export declare const win32: ChromePlatform;
@@ -1,5 +1,5 @@
1
1
  import { execFileSync } from "node:child_process";
2
- import { existsSync, readFileSync } from "node:fs";
2
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
4
  import { decryptValueBufferGcm, decryptValueBufferGcmV20 } from "../crypto.js";
5
5
  import { getActiveBrowser } from "../browsers.js";
@@ -13,6 +13,8 @@ const APPB_PREFIX = Buffer.from("APPB", "ascii");
13
13
  // cache independently (e.g. under `--all-browsers`).
14
14
  const _cachedKeys = new Map();
15
15
  const _cachedABEKeys = new Map();
16
+ let _lastResolvedDir;
17
+ let _lastResolvedBrowser;
16
18
  // Maps browser ids to the COM service identifier used by the native helper.
17
19
  const ABE_BROWSER_MAP = {
18
20
  chrome: "chrome",
@@ -67,9 +69,9 @@ function resolveLocalStateDir(requestedDir) {
67
69
  : requestedDir;
68
70
  }
69
71
  /**
70
- * Finds the chrome_abe_decrypt helper executable.
72
+ * Finds the source chrome_abe_decrypt helper executable.
71
73
  * Searches in order:
72
- * 1. Next to this module (dist/src/core/platform/)
74
+ * 1. Next to compiled output (dist/bin/)
73
75
  * 2. In the package bin/ directory
74
76
  * 3. In src/native/ (development)
75
77
  * 4. On PATH
@@ -94,30 +96,97 @@ function findABEHelper() {
94
96
  return null;
95
97
  }
96
98
  }
97
- let _abeHelperPath;
99
+ /**
100
+ * Candidate directories for staging the helper binary to satisfy Chrome's
101
+ * Elevation Service caller validation.
102
+ *
103
+ * In Chromium's `caller_validation.cc`, `MaybeTrimProcessPath()` strips the
104
+ * executable name and trailing 'Temp' or 'Application' folders. Executing the
105
+ * helper from `<BrowserRoot>\Temp\chrome_abe_decrypt.exe` matches the browser's
106
+ * validated root without requiring administrative privileges.
107
+ */
108
+ function getBrowserTempDirs(browserId) {
109
+ const dirs = [];
110
+ const progFiles = process.env["ProgramFiles"] || "C:\\Program Files";
111
+ const progFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
112
+ const localAppData = process.env.LOCALAPPDATA ||
113
+ (process.env.USERPROFILE ? join(process.env.USERPROFILE, "AppData", "Local") : "");
114
+ const subpaths = {
115
+ chrome: ["Google\\Chrome"],
116
+ "chrome-beta": ["Google\\Chrome Beta"],
117
+ "chrome-canary": ["Google\\Chrome SxS"],
118
+ chromium: ["Chromium"],
119
+ edge: ["Microsoft\\Edge"],
120
+ brave: ["BraveSoftware\\Brave-Browser"],
121
+ vivaldi: ["Vivaldi"],
122
+ };
123
+ const rels = subpaths[browserId] || ["Google\\Chrome"];
124
+ for (const rel of rels) {
125
+ dirs.push(join(progFiles, rel, "Temp"));
126
+ dirs.push(join(progFilesX86, rel, "Temp"));
127
+ if (localAppData) {
128
+ dirs.push(join(localAppData, rel, "Temp"));
129
+ }
130
+ }
131
+ return dirs;
132
+ }
133
+ /**
134
+ * Ensures the helper binary is located in a path that passes the browser
135
+ * elevation service's validation checks. Stages a copy to the browser's Temp
136
+ * folder if necessary.
137
+ */
138
+ function findOrStageABEHelper(browserId) {
139
+ const sourceExe = findABEHelper();
140
+ if (!sourceExe)
141
+ return null;
142
+ const targetDirs = getBrowserTempDirs(browserId);
143
+ for (const targetDir of targetDirs) {
144
+ try {
145
+ if (!existsSync(targetDir)) {
146
+ mkdirSync(targetDir, { recursive: true });
147
+ }
148
+ const stagedExe = join(targetDir, "chrome_abe_decrypt.exe");
149
+ if (!existsSync(stagedExe) || statSync(stagedExe).size !== statSync(sourceExe).size) {
150
+ copyFileSync(sourceExe, stagedExe);
151
+ }
152
+ return stagedExe;
153
+ }
154
+ catch {
155
+ // Permission denied or path not found, try next candidate
156
+ continue;
157
+ }
158
+ }
159
+ // Fallback to the original binary location
160
+ return sourceExe;
161
+ }
98
162
  let _abeWarningShown = false;
99
163
  /**
100
164
  * Decrypts an app_bound_encrypted_key using Chrome's IElevator COM service
101
165
  * via the native helper binary.
102
166
  *
103
- * The key from Local State is already DPAPI-decrypted at this point. This
104
- * function passes it to the COM service for the second decryption layer.
167
+ * Note: App-Bound Encryption does NOT use standard user DPAPI. The raw
168
+ * encrypted payload (with the 4-byte 'APPB' prefix removed) is passed directly
169
+ * to the IElevator COM service, which executes the elevated decryption.
105
170
  */
106
- function abeDecrypt(dpapiDecrypted, browserId) {
107
- if (_abeHelperPath === undefined) {
108
- _abeHelperPath = findABEHelper();
109
- }
110
- if (!_abeHelperPath) {
171
+ function abeDecrypt(rawEncryptedBytes, browserId) {
172
+ const helperExe = findOrStageABEHelper(browserId);
173
+ if (!helperExe) {
111
174
  if (!_abeWarningShown) {
112
175
  _abeWarningShown = true;
113
176
  process.stderr.write("[chrome-tools] chrome_abe_decrypt.exe not found. Chrome 127+ v20-encrypted\n" +
114
- "cookies/passwords cannot be decrypted. See src/native/README for build instructions.\n");
177
+ "cookies/passwords cannot be decrypted.\n");
115
178
  }
116
179
  return null;
117
180
  }
118
181
  const comBrowser = ABE_BROWSER_MAP[browserId] || "chrome";
119
182
  try {
120
- const out = execFileSync(_abeHelperPath, [comBrowser], { input: dpapiDecrypted.toString("base64"), encoding: "utf-8", timeout: 30000, stdio: ["pipe", "pipe", "pipe"] });
183
+ const out = execFileSync(helperExe, [comBrowser], {
184
+ input: rawEncryptedBytes.toString("base64"),
185
+ encoding: "utf-8",
186
+ timeout: 30000,
187
+ stdio: ["pipe", "pipe", "pipe"],
188
+ cwd: dirname(helperExe),
189
+ });
121
190
  return Buffer.from(out.trim(), "base64");
122
191
  }
123
192
  catch {
@@ -132,8 +201,8 @@ function abeDecrypt(dpapiDecrypted, browserId) {
132
201
  * Cipher: AES-256-GCM (nonce + tag carried in each blob).
133
202
  *
134
203
  * Chrome 127+ adds `os_crypt.app_bound_encrypted_key` — base64, "APPB"-prefixed.
135
- * After DPAPI unprotection, the result is fed to Chrome's IElevator COM service
136
- * via the native helper to obtain the real 32-byte AES key for "v20" blobs.
204
+ * It is passed directly to Chrome's IElevator COM service via the native helper
205
+ * to obtain the 32-byte AES-256 key for "v20" blobs.
137
206
  */
138
207
  export const win32 = {
139
208
  name: "win32",
@@ -156,8 +225,19 @@ export const win32 = {
156
225
  getDecryptionKey(chromeDir, browser = getActiveBrowser()) {
157
226
  const requestedDir = chromeDir || this.getDefaultChromeDir(browser);
158
227
  const dir = resolveLocalStateDir(requestedDir);
228
+ _lastResolvedDir = dir;
229
+ _lastResolvedBrowser = browser;
159
230
  const cached = _cachedKeys.get(dir);
160
231
  if (cached !== undefined) {
232
+ // Also pre-cache ABE key if not yet cached
233
+ if (!_cachedABEKeys.has(dir)) {
234
+ try {
235
+ this.getAppBoundKey?.(dir, browser);
236
+ }
237
+ catch {
238
+ // Ignore
239
+ }
240
+ }
161
241
  return cached;
162
242
  }
163
243
  const localStatePath = join(dir, "Local State");
@@ -183,11 +263,20 @@ export const win32 = {
183
263
  }
184
264
  const key = dpapiDecrypt(encryptedKey.subarray(DPAPI_PREFIX.length));
185
265
  _cachedKeys.set(dir, key);
266
+ // Also pre-cache the ABE key for v20 blobs
267
+ try {
268
+ this.getAppBoundKey?.(dir, browser);
269
+ }
270
+ catch {
271
+ // Ignore
272
+ }
186
273
  return key;
187
274
  },
188
- getAppBoundKey(chromeDir, browser = getActiveBrowser()) {
189
- const requestedDir = chromeDir || this.getDefaultChromeDir(browser);
275
+ getAppBoundKey(chromeDir, browser = _lastResolvedBrowser ?? getActiveBrowser()) {
276
+ const requestedDir = chromeDir || _lastResolvedDir || this.getDefaultChromeDir(browser);
190
277
  const dir = resolveLocalStateDir(requestedDir);
278
+ _lastResolvedDir = dir;
279
+ _lastResolvedBrowser = browser;
191
280
  const cached = _cachedABEKeys.get(dir);
192
281
  if (cached !== undefined) {
193
282
  return cached;
@@ -209,21 +298,10 @@ export const win32 = {
209
298
  if (!appBoundKey.subarray(0, APPB_PREFIX.length).equals(APPB_PREFIX)) {
210
299
  return undefined;
211
300
  }
212
- // Step 1: DPAPI-decrypt the key (strip the "APPB" prefix first)
213
- let dpapiResult;
214
- try {
215
- dpapiResult = dpapiDecrypt(appBoundKey.subarray(APPB_PREFIX.length));
216
- }
217
- catch {
218
- return undefined;
219
- }
220
- // Step 2: Use the IElevator COM service to decrypt the ABE layer
221
- const abeKey = abeDecrypt(dpapiResult, browser.id);
301
+ // App-Bound Encryption: Pass raw encrypted bytes directly to COM elevation service
302
+ const abeKey = abeDecrypt(appBoundKey.subarray(APPB_PREFIX.length), browser.id);
222
303
  if (!abeKey)
223
304
  return undefined;
224
- // The COM service returns a key that may have a 4-byte header; the last 32
225
- // bytes are the actual AES-256 key. If the result is exactly 32 bytes, use
226
- // it as-is.
227
305
  const key = abeKey.length > 32 ? abeKey.subarray(abeKey.length - 32) : abeKey;
228
306
  _cachedABEKeys.set(dir, key);
229
307
  return key;
@@ -233,7 +311,7 @@ export const win32 = {
233
311
  // v20 = App-Bound Encryption (Chrome 127+)
234
312
  if (prefix === "v20") {
235
313
  // Try to get the ABE key for this profile
236
- const abeKey = this.getAppBoundKey?.();
314
+ const abeKey = this.getAppBoundKey?.(_lastResolvedDir, _lastResolvedBrowser);
237
315
  if (!abeKey) {
238
316
  return "<v20 app-bound encryption - requires chrome_abe_decrypt.exe>";
239
317
  }
@@ -245,5 +323,7 @@ export const win32 = {
245
323
  clearCache() {
246
324
  _cachedKeys.clear();
247
325
  _cachedABEKeys.clear();
326
+ _lastResolvedDir = undefined;
327
+ _lastResolvedBrowser = undefined;
248
328
  },
249
329
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrome-tools",
3
- "version": "1.1.14",
3
+ "version": "1.1.15",
4
4
  "description": "CLI & Node.js library for chrome",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",