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
|
-
*
|
|
11
|
-
*
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
104
|
-
*
|
|
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(
|
|
107
|
-
|
|
108
|
-
|
|
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
|
|
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(
|
|
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
|
-
*
|
|
136
|
-
*
|
|
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
|
-
//
|
|
213
|
-
|
|
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
|
};
|