chrome-tools 1.0.1 → 1.0.3
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.
- package/README.md +4 -1
- package/bin/cli.js +2 -0
- package/package.json +3 -3
- package/src/core/crypto.js +14 -2
- package/src/modules/cookies.js +105 -4
package/README.md
CHANGED
|
@@ -116,6 +116,9 @@ npx chrome-tools cookies
|
|
|
116
116
|
# Filter cookies for a specific domain name
|
|
117
117
|
npx chrome-tools cookies --domain "google.com" --limit 30
|
|
118
118
|
|
|
119
|
+
# Filter cookies for a specific URL (matches domain, path, and secure status)
|
|
120
|
+
npx chrome-tools cookies --url "https://github.com/settings/profile"
|
|
121
|
+
|
|
119
122
|
# Skip decryption of encrypted values (fast metadata lookup)
|
|
120
123
|
npx chrome-tools cookies --no-decrypt
|
|
121
124
|
```
|
|
@@ -224,7 +227,7 @@ All core functions accept an options object with the following fields:
|
|
|
224
227
|
| `getBookmarks({chromeDir, profile, folder, search})` | `chrome-tools/bookmarks` | `Array<Bookmark>` |
|
|
225
228
|
| `getHistory({chromeDir, profile, search, limit})` | `chrome-tools/history` | `Array<HistoryEntry>` |
|
|
226
229
|
| `getSearchTerms({chromeDir, profile, limit})` | `chrome-tools/history` | `Array<SearchTerm>` |
|
|
227
|
-
| `getCookies({chromeDir, profile, domain, decrypt, limit})` | `chrome-tools/cookies` | `Array<Cookie>` |
|
|
230
|
+
| `getCookies({chromeDir, profile, domain, url, decrypt, limit})` | `chrome-tools/cookies` | `Array<Cookie>` |
|
|
228
231
|
| `getAutofillEntries({chromeDir, profile, search, limit})` | `chrome-tools/autofill` | `Array<AutofillEntry>` |
|
|
229
232
|
| `getCreditCards({chromeDir, profile})` | `chrome-tools/autofill` | `Array<CreditCard>` |
|
|
230
233
|
| `getAddresses({chromeDir, profile})` | `chrome-tools/autofill` | `Array<Address>` |
|
package/bin/cli.js
CHANGED
|
@@ -325,6 +325,7 @@ addCommonOptions(addLimitOption(searchTermsCmd)).action((opts, cmd) => {
|
|
|
325
325
|
const cookiesCmd = program.command("cookies").description("List cookies (encrypted values decrypted)");
|
|
326
326
|
addCommonOptions(addLimitOption(cookiesCmd))
|
|
327
327
|
.option("--domain <domain>", "Filter by domain")
|
|
328
|
+
.option("--url <url>", "Filter by URL with path")
|
|
328
329
|
.option("--no-decrypt", "Skip decryption of encrypted values")
|
|
329
330
|
.action((opts, cmd) => {
|
|
330
331
|
const mergedOpts = cmd.optsWithGlobals();
|
|
@@ -332,6 +333,7 @@ addCommonOptions(addLimitOption(cookiesCmd))
|
|
|
332
333
|
chromeDir: mergedOpts.chromeDir,
|
|
333
334
|
profile: mergedOpts.profile,
|
|
334
335
|
domain: mergedOpts.domain,
|
|
336
|
+
url: mergedOpts.url,
|
|
335
337
|
decrypt: mergedOpts.decrypt !== false,
|
|
336
338
|
limit: parseInt(mergedOpts.limit, 10) || 100,
|
|
337
339
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrome-tools",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "CLI & Node.js library to extract Chrome browser data (bookmarks, history, cookies, autofill, extensions, and more) on macOS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
29
|
"start": "node bin/cli.js",
|
|
30
|
-
"test": "node bin/cli.js --help",
|
|
30
|
+
"test": "node --test && node bin/cli.js --help",
|
|
31
31
|
"release": "npm version patch --no-git-tag-version && npm publish"
|
|
32
32
|
},
|
|
33
33
|
"keywords": [
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
"better-sqlite3": "^12.10.0",
|
|
48
48
|
"chalk": "^5.6.2",
|
|
49
49
|
"cli-table3": "^0.6.5",
|
|
50
|
-
"commander": "^
|
|
50
|
+
"commander": "^14.0.3"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
53
53
|
"node": ">=18.0.0"
|
package/src/core/crypto.js
CHANGED
|
@@ -12,8 +12,20 @@ const AES_IV = Buffer.alloc(16, " "); // 16 spaces (0x20)
|
|
|
12
12
|
* @returns {string} The decrypted plaintext, or an error placeholder.
|
|
13
13
|
*/
|
|
14
14
|
export function decryptValue(encryptedBlob, derivedKey) {
|
|
15
|
+
const decrypted = decryptValueBuffer(encryptedBlob, derivedKey);
|
|
16
|
+
return Buffer.isBuffer(decrypted) ? decrypted.toString("utf-8") : decrypted;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Decrypts a Chrome encrypted blob and returns the raw plaintext bytes.
|
|
21
|
+
*
|
|
22
|
+
* @param {Buffer|Uint8Array} encryptedBlob - The encrypted blob from the database.
|
|
23
|
+
* @param {Buffer} derivedKey - The derived 16-byte key.
|
|
24
|
+
* @returns {Buffer|string} The decrypted plaintext bytes, or an error placeholder.
|
|
25
|
+
*/
|
|
26
|
+
export function decryptValueBuffer(encryptedBlob, derivedKey) {
|
|
15
27
|
if (!encryptedBlob || encryptedBlob.length === 0) {
|
|
16
|
-
return
|
|
28
|
+
return Buffer.alloc(0);
|
|
17
29
|
}
|
|
18
30
|
|
|
19
31
|
const blob = Buffer.isBuffer(encryptedBlob)
|
|
@@ -31,7 +43,7 @@ export function decryptValue(encryptedBlob, derivedKey) {
|
|
|
31
43
|
const decipher = createDecipheriv("aes-128-cbc", derivedKey, AES_IV);
|
|
32
44
|
let decrypted = decipher.update(ciphertext);
|
|
33
45
|
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
34
|
-
return decrypted
|
|
46
|
+
return decrypted;
|
|
35
47
|
} catch {
|
|
36
48
|
return "<decryption failed>";
|
|
37
49
|
}
|
package/src/modules/cookies.js
CHANGED
|
@@ -1,9 +1,85 @@
|
|
|
1
1
|
import { join } from "node:path";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { resolveTargetProfiles } from "../core/chrome.js";
|
|
3
4
|
import { deriveEncryptionKey } from "../core/keychain.js";
|
|
4
|
-
import {
|
|
5
|
+
import { decryptValueBuffer, chromeTimeToDate } from "../core/crypto.js";
|
|
5
6
|
import { queryDatabase, tableExists } from "../core/db.js";
|
|
6
7
|
|
|
8
|
+
function decodeCookieValue(encryptedValue, derivedKey, hostKey) {
|
|
9
|
+
const decrypted = decryptValueBuffer(encryptedValue, derivedKey);
|
|
10
|
+
if (!Buffer.isBuffer(decrypted)) {
|
|
11
|
+
return decrypted;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const hostHash = createHash("sha256").update(hostKey).digest();
|
|
15
|
+
const valueBytes = decrypted.length >= hostHash.length && decrypted.subarray(0, hostHash.length).equals(hostHash)
|
|
16
|
+
? decrypted.subarray(hostHash.length)
|
|
17
|
+
: decrypted;
|
|
18
|
+
|
|
19
|
+
return valueBytes.toString("utf-8");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseURL(urlStr) {
|
|
23
|
+
if (!urlStr) return null;
|
|
24
|
+
try {
|
|
25
|
+
return new URL(urlStr);
|
|
26
|
+
} catch (e) {
|
|
27
|
+
try {
|
|
28
|
+
// If it doesn't have a protocol, prepend https://
|
|
29
|
+
if (!/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(urlStr)) {
|
|
30
|
+
return new URL("https://" + urlStr);
|
|
31
|
+
}
|
|
32
|
+
} catch (err) {
|
|
33
|
+
// Ignore
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getPossibleHostKeys(hostname) {
|
|
40
|
+
const keys = new Set();
|
|
41
|
+
keys.add(hostname);
|
|
42
|
+
keys.add("." + hostname);
|
|
43
|
+
|
|
44
|
+
const parts = hostname.split(".");
|
|
45
|
+
for (let i = 1; i < parts.length; i++) {
|
|
46
|
+
const suffix = parts.slice(i).join(".");
|
|
47
|
+
keys.add(suffix);
|
|
48
|
+
keys.add("." + suffix);
|
|
49
|
+
}
|
|
50
|
+
return Array.from(keys);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function domainMatch(hostname, hostKey) {
|
|
54
|
+
if (hostname === hostKey) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (hostKey.startsWith(".")) {
|
|
58
|
+
if (hostname === hostKey.slice(1) || hostname.endsWith(hostKey)) {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function pathMatch(urlPath, cookiePath) {
|
|
66
|
+
const uPath = urlPath || "/";
|
|
67
|
+
const cPath = cookiePath || "/";
|
|
68
|
+
|
|
69
|
+
if (cPath === "/") {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
if (uPath === cPath) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
if (uPath.startsWith(cPath)) {
|
|
76
|
+
if (cPath.endsWith("/") || uPath[cPath.length] === "/") {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
|
|
7
83
|
/**
|
|
8
84
|
* Extracts cookies from Chrome's Cookies database.
|
|
9
85
|
* Encrypted values are decrypted using the same mechanism as passwords.
|
|
@@ -12,14 +88,16 @@ import { queryDatabase, tableExists } from "../core/db.js";
|
|
|
12
88
|
* @param {string} [options.chromeDir] - Chrome User Data directory.
|
|
13
89
|
* @param {string} [options.profile] - Specific profile to target.
|
|
14
90
|
* @param {string} [options.domain] - Filter by domain (case-insensitive, partial match).
|
|
91
|
+
* @param {string} [options.url] - Filter by URL (domain, path, and secure matching).
|
|
15
92
|
* @param {boolean} [options.decrypt=true] - Whether to decrypt encrypted values.
|
|
16
93
|
* @param {number} [options.limit=100] - Maximum number of results.
|
|
17
94
|
* @returns {Array<Object>}
|
|
18
95
|
*/
|
|
19
|
-
export function getCookies({ chromeDir, profile, domain, decrypt = true, limit = 100 } = {}) {
|
|
96
|
+
export function getCookies({ chromeDir, profile, domain, url, decrypt = true, limit = 100 } = {}) {
|
|
20
97
|
const profiles = resolveTargetProfiles({ chromeDir, profile });
|
|
21
98
|
const derivedKey = decrypt ? deriveEncryptionKey() : null;
|
|
22
99
|
const results = [];
|
|
100
|
+
const parsedUrl = parseURL(url);
|
|
23
101
|
|
|
24
102
|
for (const p of profiles) {
|
|
25
103
|
const dbPath = join(p.path, "Cookies");
|
|
@@ -36,22 +114,33 @@ export function getCookies({ chromeDir, profile, domain, decrypt = true, limit =
|
|
|
36
114
|
if (domain) {
|
|
37
115
|
conditions.push(`host_key LIKE ?`);
|
|
38
116
|
params.push(`%${domain}%`);
|
|
117
|
+
} else if (parsedUrl) {
|
|
118
|
+
const hostKeys = getPossibleHostKeys(parsedUrl.hostname);
|
|
119
|
+
const placeholders = hostKeys.map(() => "?").join(", ");
|
|
120
|
+
conditions.push(`host_key IN (${placeholders})`);
|
|
121
|
+
params.push(...hostKeys);
|
|
39
122
|
}
|
|
40
123
|
|
|
41
124
|
if (conditions.length > 0) {
|
|
42
125
|
sql += ` WHERE ${conditions.join(" AND ")}`;
|
|
43
126
|
}
|
|
44
127
|
sql += ` ORDER BY host_key, name`;
|
|
45
|
-
if (limit) {
|
|
128
|
+
if (limit && !url) {
|
|
46
129
|
sql += ` LIMIT ${parseInt(limit, 10)}`;
|
|
47
130
|
}
|
|
48
131
|
|
|
49
132
|
const rows = queryDatabase(dbPath, sql, params);
|
|
50
133
|
|
|
51
134
|
for (const row of rows) {
|
|
135
|
+
if (parsedUrl) {
|
|
136
|
+
if (!domainMatch(parsedUrl.hostname, row.host_key || "")) continue;
|
|
137
|
+
if (!pathMatch(parsedUrl.pathname || "/", row.path || "/")) continue;
|
|
138
|
+
if (row.is_secure && parsedUrl.protocol !== "https:") continue;
|
|
139
|
+
}
|
|
140
|
+
|
|
52
141
|
let cookieValue = row.value || "";
|
|
53
142
|
if (!cookieValue && row.encrypted_value && derivedKey) {
|
|
54
|
-
cookieValue =
|
|
143
|
+
cookieValue = decodeCookieValue(row.encrypted_value, derivedKey, row.host_key);
|
|
55
144
|
}
|
|
56
145
|
|
|
57
146
|
results.push({
|
|
@@ -73,5 +162,17 @@ export function getCookies({ chromeDir, profile, domain, decrypt = true, limit =
|
|
|
73
162
|
}
|
|
74
163
|
}
|
|
75
164
|
|
|
165
|
+
if (limit && url) {
|
|
166
|
+
return results.slice(0, parseInt(limit, 10));
|
|
167
|
+
}
|
|
76
168
|
return results;
|
|
77
169
|
}
|
|
170
|
+
|
|
171
|
+
export const _test = {
|
|
172
|
+
parseURL,
|
|
173
|
+
getPossibleHostKeys,
|
|
174
|
+
domainMatch,
|
|
175
|
+
pathMatch,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
|