chrome-tools 1.1.15 → 1.1.16
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/dist/cli.js +8602 -0
- package/dist/index.d.ts +629 -0
- package/dist/index.js +2297 -0
- package/package.json +62 -29
- package/dist/bin/chrome_abe_decrypt.exe +0 -0
- package/dist/bin/cli.d.ts +0 -9
- package/dist/bin/cli.js +0 -590
- package/dist/src/core/browsers.d.ts +0 -73
- package/dist/src/core/browsers.js +0 -186
- package/dist/src/core/chrome.d.ts +0 -124
- package/dist/src/core/chrome.js +0 -259
- package/dist/src/core/crypto.d.ts +0 -52
- package/dist/src/core/crypto.js +0 -152
- package/dist/src/core/db.d.ts +0 -22
- package/dist/src/core/db.js +0 -227
- package/dist/src/core/keychain.d.ts +0 -57
- package/dist/src/core/keychain.js +0 -191
- package/dist/src/core/limit.d.ts +0 -4
- package/dist/src/core/limit.js +0 -12
- package/dist/src/core/platform/darwin.d.ts +0 -12
- package/dist/src/core/platform/darwin.js +0 -31
- package/dist/src/core/platform/index.d.ts +0 -30
- package/dist/src/core/platform/index.js +0 -69
- package/dist/src/core/platform/linux.d.ts +0 -10
- package/dist/src/core/platform/linux.js +0 -64
- package/dist/src/core/platform/types.d.ts +0 -45
- package/dist/src/core/platform/types.js +0 -1
- package/dist/src/core/platform/win32.d.ts +0 -13
- package/dist/src/core/platform/win32.js +0 -329
- package/dist/src/core/sql.d.ts +0 -2
- package/dist/src/core/sql.js +0 -4
- package/dist/src/index.d.ts +0 -28
- package/dist/src/index.js +0 -34
- package/dist/src/modules/autofill.d.ts +0 -68
- package/dist/src/modules/autofill.js +0 -170
- package/dist/src/modules/bookmarks.d.ts +0 -22
- package/dist/src/modules/bookmarks.js +0 -83
- package/dist/src/modules/cookies.d.ts +0 -40
- package/dist/src/modules/cookies.js +0 -163
- package/dist/src/modules/downloads.d.ts +0 -27
- package/dist/src/modules/downloads.js +0 -80
- package/dist/src/modules/extensions.d.ts +0 -24
- package/dist/src/modules/extensions.js +0 -71
- package/dist/src/modules/history.d.ts +0 -34
- package/dist/src/modules/history.js +0 -83
- package/dist/src/modules/local-storage.d.ts +0 -40
- package/dist/src/modules/local-storage.js +0 -455
- package/dist/src/modules/passwords.d.ts +0 -50
- package/dist/src/modules/passwords.js +0 -198
- package/dist/src/modules/search-engines.d.ts +0 -21
- package/dist/src/modules/search-engines.js +0 -38
- package/dist/src/modules/top-sites.d.ts +0 -17
- package/dist/src/modules/top-sites.js +0 -33
- package/src/native/build.bat +0 -31
- package/src/native/chrome_abe_decrypt.c +0 -336
- package/src/native/chrome_abe_decrypt.exe +0 -0
- /package/{bin → dist}/chrome_abe_decrypt.exe +0 -0
package/dist/src/core/crypto.js
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { createDecipheriv } from "node:crypto";
|
|
2
|
-
const AES_IV = Buffer.alloc(16, " "); // 16 spaces (0x20)
|
|
3
|
-
// Windows AES-256-GCM layout: "v10" prefix + 12-byte nonce + ciphertext + 16-byte tag.
|
|
4
|
-
const GCM_NONCE_LENGTH = 12;
|
|
5
|
-
const GCM_TAG_LENGTH = 16;
|
|
6
|
-
/**
|
|
7
|
-
* Decrypts a password/value buffer encrypted by Chrome on macOS or Linux.
|
|
8
|
-
* Chrome v10+ prefixes the encrypted blob with "v10" or "v11", followed by
|
|
9
|
-
* AES-128-CBC ciphertext. (Windows uses AES-256-GCM — see decryptValueBufferGcm.)
|
|
10
|
-
*
|
|
11
|
-
* @param encryptedBlob - The encrypted blob from the database.
|
|
12
|
-
* @param derivedKey - The derived 16-byte key.
|
|
13
|
-
* @returns The decrypted plaintext, or an error placeholder.
|
|
14
|
-
*/
|
|
15
|
-
export function decryptValue(encryptedBlob, derivedKey) {
|
|
16
|
-
const decrypted = decryptValueBuffer(encryptedBlob, derivedKey);
|
|
17
|
-
return Buffer.isBuffer(decrypted) ? decrypted.toString("utf-8") : decrypted;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* Decrypts a Chrome encrypted blob and returns the raw plaintext bytes.
|
|
21
|
-
*
|
|
22
|
-
* @param encryptedBlob - The encrypted blob from the database.
|
|
23
|
-
* @param derivedKey - The derived 16-byte key.
|
|
24
|
-
* @returns The decrypted plaintext bytes, or an error placeholder.
|
|
25
|
-
*/
|
|
26
|
-
export function decryptValueBuffer(encryptedBlob, derivedKey) {
|
|
27
|
-
if (!encryptedBlob || encryptedBlob.length === 0) {
|
|
28
|
-
return Buffer.alloc(0);
|
|
29
|
-
}
|
|
30
|
-
const blob = Buffer.isBuffer(encryptedBlob)
|
|
31
|
-
? encryptedBlob
|
|
32
|
-
: Buffer.from(encryptedBlob);
|
|
33
|
-
const prefix = blob.subarray(0, 3).toString("utf-8");
|
|
34
|
-
if (prefix !== "v10" && prefix !== "v11") {
|
|
35
|
-
return "<unsupported encryption format>";
|
|
36
|
-
}
|
|
37
|
-
const ciphertext = blob.subarray(3);
|
|
38
|
-
try {
|
|
39
|
-
const decipher = createDecipheriv("aes-128-cbc", derivedKey, AES_IV);
|
|
40
|
-
let decrypted = decipher.update(ciphertext);
|
|
41
|
-
decrypted = Buffer.concat([decrypted, decipher.final()]);
|
|
42
|
-
return decrypted;
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return "<decryption failed>";
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Decrypts a Windows Chrome `v10` blob using AES-256-GCM.
|
|
50
|
-
*
|
|
51
|
-
* Unlike the macOS/Linux CBC scheme, Windows blobs carry their own 12-byte
|
|
52
|
-
* nonce after the version prefix and a 16-byte GCM auth tag at the end. The
|
|
53
|
-
* `key` is the 32-byte AES key recovered from `Local State` via DPAPI.
|
|
54
|
-
*
|
|
55
|
-
* @param encryptedBlob - The encrypted blob from the database.
|
|
56
|
-
* @param key - The 32-byte AES-256 key.
|
|
57
|
-
* @returns The decrypted plaintext bytes, or a placeholder string.
|
|
58
|
-
*/
|
|
59
|
-
export function decryptValueBufferGcm(encryptedBlob, key) {
|
|
60
|
-
if (!encryptedBlob || encryptedBlob.length === 0) {
|
|
61
|
-
return Buffer.alloc(0);
|
|
62
|
-
}
|
|
63
|
-
const blob = Buffer.isBuffer(encryptedBlob)
|
|
64
|
-
? encryptedBlob
|
|
65
|
-
: Buffer.from(encryptedBlob);
|
|
66
|
-
const prefix = blob.subarray(0, 3).toString("utf-8");
|
|
67
|
-
if (prefix !== "v10" && prefix !== "v11") {
|
|
68
|
-
// Pre-v10 values are raw DPAPI blobs (no app-level key); not handled here.
|
|
69
|
-
return "<unsupported encryption format>";
|
|
70
|
-
}
|
|
71
|
-
if (blob.length < 3 + GCM_NONCE_LENGTH + GCM_TAG_LENGTH) {
|
|
72
|
-
return "<decryption failed>";
|
|
73
|
-
}
|
|
74
|
-
const nonce = blob.subarray(3, 3 + GCM_NONCE_LENGTH);
|
|
75
|
-
const tag = blob.subarray(blob.length - GCM_TAG_LENGTH);
|
|
76
|
-
const ciphertext = blob.subarray(3 + GCM_NONCE_LENGTH, blob.length - GCM_TAG_LENGTH);
|
|
77
|
-
try {
|
|
78
|
-
const decipher = createDecipheriv("aes-256-gcm", key, nonce);
|
|
79
|
-
decipher.setAuthTag(tag);
|
|
80
|
-
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
81
|
-
}
|
|
82
|
-
catch {
|
|
83
|
-
return "<decryption failed>";
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
/**
|
|
87
|
-
* Decrypts a Windows Chrome 127+ `v20` blob using AES-256-GCM.
|
|
88
|
-
*
|
|
89
|
-
* App-Bound Encryption uses the same AES-256-GCM layout as `v10`/`v11` but
|
|
90
|
-
* with a `"v20"` prefix and a key obtained from Chrome's Elevation Service
|
|
91
|
-
* (IElevator COM) instead of plain DPAPI.
|
|
92
|
-
*
|
|
93
|
-
* Layout: "v20" (3 bytes) + 12-byte nonce + ciphertext + 16-byte GCM tag
|
|
94
|
-
*
|
|
95
|
-
* @param encryptedBlob - The encrypted blob from the database.
|
|
96
|
-
* @param key - The 32-byte AES-256 key from ABE decryption.
|
|
97
|
-
* @returns The decrypted plaintext bytes, or a placeholder string.
|
|
98
|
-
*/
|
|
99
|
-
export function decryptValueBufferGcmV20(encryptedBlob, key) {
|
|
100
|
-
if (!encryptedBlob || encryptedBlob.length === 0) {
|
|
101
|
-
return Buffer.alloc(0);
|
|
102
|
-
}
|
|
103
|
-
const blob = Buffer.isBuffer(encryptedBlob)
|
|
104
|
-
? encryptedBlob
|
|
105
|
-
: Buffer.from(encryptedBlob);
|
|
106
|
-
const prefix = blob.subarray(0, 3).toString("utf-8");
|
|
107
|
-
if (prefix !== "v20") {
|
|
108
|
-
return "<unsupported encryption format>";
|
|
109
|
-
}
|
|
110
|
-
if (blob.length < 3 + GCM_NONCE_LENGTH + GCM_TAG_LENGTH) {
|
|
111
|
-
return "<decryption failed>";
|
|
112
|
-
}
|
|
113
|
-
const nonce = blob.subarray(3, 3 + GCM_NONCE_LENGTH);
|
|
114
|
-
const tag = blob.subarray(blob.length - GCM_TAG_LENGTH);
|
|
115
|
-
const ciphertext = blob.subarray(3 + GCM_NONCE_LENGTH, blob.length - GCM_TAG_LENGTH);
|
|
116
|
-
try {
|
|
117
|
-
const decipher = createDecipheriv("aes-256-gcm", key, nonce);
|
|
118
|
-
decipher.setAuthTag(tag);
|
|
119
|
-
return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
120
|
-
}
|
|
121
|
-
catch {
|
|
122
|
-
return "<decryption failed>";
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
/**
|
|
126
|
-
* Converts a Chrome/Webkit timestamp (microseconds since 1601-01-01)
|
|
127
|
-
* to a readable local Date string.
|
|
128
|
-
*
|
|
129
|
-
* @param chromeTimestamp - Timestamp in microseconds.
|
|
130
|
-
* @returns Human-readable date-time string, or "N/A".
|
|
131
|
-
*/
|
|
132
|
-
export function chromeTimeToDate(chromeTimestamp) {
|
|
133
|
-
if (chromeTimestamp === null || chromeTimestamp === undefined) {
|
|
134
|
-
return "N/A";
|
|
135
|
-
}
|
|
136
|
-
try {
|
|
137
|
-
const timestampBigInt = BigInt(chromeTimestamp);
|
|
138
|
-
if (timestampBigInt === 0n) {
|
|
139
|
-
return "N/A";
|
|
140
|
-
}
|
|
141
|
-
const unixMicroseconds = timestampBigInt - 11644473600000000n;
|
|
142
|
-
const ms = Number(unixMicroseconds / 1000n);
|
|
143
|
-
const date = new Date(ms);
|
|
144
|
-
if (isNaN(date.getTime())) {
|
|
145
|
-
return "N/A";
|
|
146
|
-
}
|
|
147
|
-
return date.toLocaleString();
|
|
148
|
-
}
|
|
149
|
-
catch {
|
|
150
|
-
return "N/A";
|
|
151
|
-
}
|
|
152
|
-
}
|
package/dist/src/core/db.d.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Creates a transactionally consistent snapshot of a Chrome SQLite database,
|
|
3
|
-
* executes a query against it, and cleans up. `VACUUM INTO` reads through
|
|
4
|
-
* SQLite itself, so live WAL contents are included without racing raw file
|
|
5
|
-
* copies while Chrome is writing. A store the browser holds exclusively cannot
|
|
6
|
-
* be opened that way at all, and falls back to a file copy (see below).
|
|
7
|
-
*
|
|
8
|
-
* @param dbFilePath - Absolute path to the Chrome database file.
|
|
9
|
-
* @param sql - SQL query to execute.
|
|
10
|
-
* @param params - Bound parameters for the query.
|
|
11
|
-
* @returns Query result rows.
|
|
12
|
-
* @throws If the file doesn't exist or query fails.
|
|
13
|
-
*/
|
|
14
|
-
export declare function queryDatabase(dbFilePath: string, sql: string, params?: unknown[]): any[];
|
|
15
|
-
/**
|
|
16
|
-
* Checks whether a specific table exists in the given database file.
|
|
17
|
-
*
|
|
18
|
-
* @param dbFilePath - Absolute path to the database file.
|
|
19
|
-
* @param tableName - Name of the table to check.
|
|
20
|
-
* @returns True if the table exists.
|
|
21
|
-
*/
|
|
22
|
-
export declare function tableExists(dbFilePath: string, tableName: string): boolean;
|
package/dist/src/core/db.js
DELETED
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
import { copyFileSync, existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join, basename } from "node:path";
|
|
4
|
-
import { DatabaseSync } from "node:sqlite";
|
|
5
|
-
/** SQLite result codes meaning "someone else owns this database right now". */
|
|
6
|
-
const SQLITE_BUSY = 5;
|
|
7
|
-
const SQLITE_LOCKED = 6;
|
|
8
|
-
/**
|
|
9
|
-
* Whether a failure is "another process owns this database", not a broken one.
|
|
10
|
-
*
|
|
11
|
-
* `node:sqlite` reports *every* failure as `code: "ERR_SQLITE_ERROR"` and puts
|
|
12
|
-
* the SQLite result code in `errcode`, so the `code` property is useless as a
|
|
13
|
-
* discriminator here — reading it is what an obvious port of this function from
|
|
14
|
-
* `better-sqlite3` (which set `code: "SQLITE_BUSY"`) would do, and it would call
|
|
15
|
-
* every locked store a corrupt one.
|
|
16
|
-
*
|
|
17
|
-
* Masked to the low byte because SQLite may report an *extended* code — 517
|
|
18
|
-
* (`SQLITE_BUSY_SNAPSHOT`) and 262 (`SQLITE_LOCKED_SHAREDCACHE`) are the same
|
|
19
|
-
* two conditions with detail attached, and both must take the copy path.
|
|
20
|
-
*
|
|
21
|
-
* The message check stays as the fallback: it is what caught this before
|
|
22
|
-
* `errcode` existed, and it costs nothing to keep for a build that leaves the
|
|
23
|
-
* field unset.
|
|
24
|
-
*/
|
|
25
|
-
function isLocked(err) {
|
|
26
|
-
const errcode = err?.errcode;
|
|
27
|
-
if (typeof errcode === "number") {
|
|
28
|
-
const primary = errcode & 0xff;
|
|
29
|
-
if (primary === SQLITE_BUSY || primary === SQLITE_LOCKED)
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
33
|
-
return /database is locked|database is busy/i.test(message);
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* Puts a row back into the shape callers have always been handed: an ordinary
|
|
37
|
-
* object, holding plain numbers, with `Buffer` for a BLOB.
|
|
38
|
-
*
|
|
39
|
-
* `queryDatabase` is exported from the package root, so all three of these are
|
|
40
|
-
* public API rather than internal detail — a caller running its own SQL against
|
|
41
|
-
* `Login Data` gets whatever this returns.
|
|
42
|
-
*
|
|
43
|
-
* **The object itself.** `node:sqlite` builds each row with a null prototype,
|
|
44
|
-
* where better-sqlite3 returned an ordinary object. `Object.keys`, `in`, and
|
|
45
|
-
* `JSON.stringify` all still work, which is why this hides so well — but
|
|
46
|
-
* `row.hasOwnProperty(…)` is a `TypeError`, and so is anything that coerces the
|
|
47
|
-
* row to a string, including `String(row)` and dropping it in a template
|
|
48
|
-
* literal. Rebuilt here rather than re-prototyped: the loop that has to visit
|
|
49
|
-
* every cell anyway is the cheapest place to do it.
|
|
50
|
-
*
|
|
51
|
-
* **Integers.** This is the whole reason the statement is put in BigInt mode.
|
|
52
|
-
* `node:sqlite` does not quietly lose precision past `Number.MAX_SAFE_INTEGER`
|
|
53
|
-
* the way better-sqlite3 did — it **throws** `ERR_OUT_OF_RANGE` partway through
|
|
54
|
-
* reading the row. Chrome writes every timestamp as microseconds since 1601,
|
|
55
|
-
* which lands around 1.3e16 and is therefore *always* past that line, so in the
|
|
56
|
-
* default number mode a `History`, `Login Data`, or `Cookies` row cannot be read
|
|
57
|
-
* at all. Not an edge case: it is every row with a time on it. BigInt mode is
|
|
58
|
-
* the only mode that can read those columns, and this undoes the widening so
|
|
59
|
-
* nothing downstream has to learn a second numeric type. The precision that
|
|
60
|
-
* costs is precision better-sqlite3 was already dropping — sub-2-microsecond
|
|
61
|
-
* detail on a 400-year-old epoch.
|
|
62
|
-
*
|
|
63
|
-
* **Blobs.** `node:sqlite` hands back a bare `Uint8Array` where better-sqlite3
|
|
64
|
-
* gave a `Buffer`, and the difference is not a type-checking nicety: it fails
|
|
65
|
-
* *silently*. `Buffer` is a `Uint8Array`, so an `instanceof` guard still passes
|
|
66
|
-
* and nothing throws — but `blob.toString("utf-8")` on the bare array ignores
|
|
67
|
-
* the encoding and returns the decimal bytes joined by commas, so a caller
|
|
68
|
-
* decoding `password_value` or `encrypted_value` gets `"118,49,48,…"` and no
|
|
69
|
-
* error to notice. `Buffer.isBuffer` also flips to false, and the read helpers
|
|
70
|
-
* (`readUInt32BE`, `equals`, …) are simply absent.
|
|
71
|
-
*
|
|
72
|
-
* Wrapped rather than copied: each row owns its own `ArrayBuffer` here, so
|
|
73
|
-
* re-using the memory is safe and a large blob is not duplicated to change its
|
|
74
|
-
* prototype. An actual `Buffer` is left alone.
|
|
75
|
-
*/
|
|
76
|
-
function toCallerTypes(rows) {
|
|
77
|
-
return rows.map((row) => {
|
|
78
|
-
const plain = {};
|
|
79
|
-
for (const key of Object.keys(row)) {
|
|
80
|
-
const value = row[key];
|
|
81
|
-
if (typeof value === "bigint") {
|
|
82
|
-
plain[key] = Number(value);
|
|
83
|
-
}
|
|
84
|
-
else if (value instanceof Uint8Array && !Buffer.isBuffer(value)) {
|
|
85
|
-
plain[key] = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
86
|
-
}
|
|
87
|
-
else {
|
|
88
|
-
plain[key] = value;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
return plain;
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Copies a database SQLite will not open, alongside the sidecars that make it
|
|
96
|
-
* readable: `-wal` holds commits not yet folded into the main file, and `-shm`
|
|
97
|
-
* is its index. Copying the main file alone would silently drop the most recent
|
|
98
|
-
* writes — a password saved a minute ago being exactly the kind that matters.
|
|
99
|
-
*/
|
|
100
|
-
function copyLockedDatabase(dbFilePath, tmpDb) {
|
|
101
|
-
copyFileSync(dbFilePath, tmpDb);
|
|
102
|
-
for (const suffix of ["-wal", "-shm"]) {
|
|
103
|
-
const sidecar = `${dbFilePath}${suffix}`;
|
|
104
|
-
if (existsSync(sidecar))
|
|
105
|
-
copyFileSync(sidecar, `${tmpDb}${suffix}`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
/**
|
|
109
|
-
* Creates a transactionally consistent snapshot of a Chrome SQLite database,
|
|
110
|
-
* executes a query against it, and cleans up. `VACUUM INTO` reads through
|
|
111
|
-
* SQLite itself, so live WAL contents are included without racing raw file
|
|
112
|
-
* copies while Chrome is writing. A store the browser holds exclusively cannot
|
|
113
|
-
* be opened that way at all, and falls back to a file copy (see below).
|
|
114
|
-
*
|
|
115
|
-
* @param dbFilePath - Absolute path to the Chrome database file.
|
|
116
|
-
* @param sql - SQL query to execute.
|
|
117
|
-
* @param params - Bound parameters for the query.
|
|
118
|
-
* @returns Query result rows.
|
|
119
|
-
* @throws If the file doesn't exist or query fails.
|
|
120
|
-
*/
|
|
121
|
-
export function queryDatabase(dbFilePath, sql, params = []) {
|
|
122
|
-
if (!existsSync(dbFilePath)) {
|
|
123
|
-
throw new Error(`Database file not found: ${dbFilePath}`);
|
|
124
|
-
}
|
|
125
|
-
const tmpDir = mkdtempSync(join(tmpdir(), "chrome-tools-"));
|
|
126
|
-
const tmpDb = join(tmpDir, basename(dbFilePath));
|
|
127
|
-
let sourceDb;
|
|
128
|
-
try {
|
|
129
|
-
sourceDb = new DatabaseSync(dbFilePath, {
|
|
130
|
-
// `readOnly`, not `readonly`. An unrecognized option is *ignored* rather
|
|
131
|
-
// than rejected, so the lowercase spelling — which is what better-sqlite3
|
|
132
|
-
// called it, and what this line used to say — silently hands back a
|
|
133
|
-
// writable handle onto the user's live browser profile.
|
|
134
|
-
readOnly: true,
|
|
135
|
-
// No `fileMustExist` equivalent, and none needed: a read-only open will
|
|
136
|
-
// not create the file, and the `existsSync` guard above is the explicit
|
|
137
|
-
// check that turns a missing store into a message worth reading.
|
|
138
|
-
//
|
|
139
|
-
// No wait. A running Chrome holds these stores in `locking_mode =
|
|
140
|
-
// EXCLUSIVE` for as long as it is open, so a busy timeout here buys
|
|
141
|
-
// nothing that waiting longer would win — the lock is not transient, and
|
|
142
|
-
// the copy below is the answer to it either way. Five seconds *per store*
|
|
143
|
-
// was the whole cost of reading a saved login while the browser was up:
|
|
144
|
-
// two stores, ten seconds, spent to arrive at the fallback that then
|
|
145
|
-
// succeeded instantly.
|
|
146
|
-
timeout: 0,
|
|
147
|
-
});
|
|
148
|
-
sourceDb.prepare("VACUUM INTO ?").run(tmpDb);
|
|
149
|
-
}
|
|
150
|
-
catch (err) {
|
|
151
|
-
// A running Chrome holds its stores in `locking_mode = EXCLUSIVE`, which
|
|
152
|
-
// refuses every other connection — SQLite cannot even open them to read.
|
|
153
|
-
// That is not a corrupt or missing database, it is a browser that happens
|
|
154
|
-
// to be open, and it made a profile with saved passwords in it report none.
|
|
155
|
-
// Falling back to the file itself is what a locked store leaves: the OS
|
|
156
|
-
// copies bytes regardless of an advisory lock the copier never takes.
|
|
157
|
-
if (!isLocked(err)) {
|
|
158
|
-
cleanupTmp(tmpDir);
|
|
159
|
-
throw new Error(`Failed to snapshot database file '${basename(dbFilePath)}': ${err.message}`);
|
|
160
|
-
}
|
|
161
|
-
try {
|
|
162
|
-
copyLockedDatabase(dbFilePath, tmpDb);
|
|
163
|
-
}
|
|
164
|
-
catch (copyErr) {
|
|
165
|
-
cleanupTmp(tmpDir);
|
|
166
|
-
throw new Error(`Failed to snapshot database file '${basename(dbFilePath)}': ${err.message} ` +
|
|
167
|
-
`(copying it instead also failed: ${copyErr.message})`);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
finally {
|
|
171
|
-
if (sourceDb) {
|
|
172
|
-
try {
|
|
173
|
-
sourceDb.close();
|
|
174
|
-
}
|
|
175
|
-
catch {
|
|
176
|
-
/* ignore */
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
let db;
|
|
181
|
-
try {
|
|
182
|
-
db = new DatabaseSync(tmpDb, { readOnly: true, timeout: 5000 });
|
|
183
|
-
const stmt = db.prepare(sql);
|
|
184
|
-
// Read wide, hand back narrow. See `toCallerTypes` — without this, every
|
|
185
|
-
// query that touches a Chrome timestamp throws instead of returning rows.
|
|
186
|
-
stmt.setReadBigInts(true);
|
|
187
|
-
return toCallerTypes(params.length > 0 ? stmt.all(...params) : stmt.all());
|
|
188
|
-
}
|
|
189
|
-
catch (err) {
|
|
190
|
-
throw new Error(`Database query failed on '${basename(dbFilePath)}': ${err.message}`);
|
|
191
|
-
}
|
|
192
|
-
finally {
|
|
193
|
-
if (db) {
|
|
194
|
-
try {
|
|
195
|
-
db.close();
|
|
196
|
-
}
|
|
197
|
-
catch {
|
|
198
|
-
/* ignore */
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
cleanupTmp(tmpDir);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
/**
|
|
205
|
-
* Checks whether a specific table exists in the given database file.
|
|
206
|
-
*
|
|
207
|
-
* @param dbFilePath - Absolute path to the database file.
|
|
208
|
-
* @param tableName - Name of the table to check.
|
|
209
|
-
* @returns True if the table exists.
|
|
210
|
-
*/
|
|
211
|
-
export function tableExists(dbFilePath, tableName) {
|
|
212
|
-
try {
|
|
213
|
-
const rows = queryDatabase(dbFilePath, `SELECT name FROM sqlite_master WHERE type='table' AND name=?`, [tableName]);
|
|
214
|
-
return rows.length > 0;
|
|
215
|
-
}
|
|
216
|
-
catch {
|
|
217
|
-
return false;
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
function cleanupTmp(tmpDir) {
|
|
221
|
-
try {
|
|
222
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
223
|
-
}
|
|
224
|
-
catch {
|
|
225
|
-
/* ignore */
|
|
226
|
-
}
|
|
227
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
/** How a Keychain read failed, so callers (and the CLI) can react specifically. */
|
|
2
|
-
export type KeychainErrorKind = "access-denied" | "not-found" | "empty" | "unknown";
|
|
3
|
-
/**
|
|
4
|
-
* Error thrown when a browser's Safe Storage key cannot be read from the
|
|
5
|
-
* Keychain. Carries the failing `service` and a `kind` so the CLI can show a
|
|
6
|
-
* clear "No keychain access" message instead of a generic failure.
|
|
7
|
-
*/
|
|
8
|
-
export declare class KeychainAccessError extends Error {
|
|
9
|
-
readonly service: string;
|
|
10
|
-
readonly kind: KeychainErrorKind;
|
|
11
|
-
constructor(message: string, service: string, kind: KeychainErrorKind);
|
|
12
|
-
/** True when the failure means we couldn't get permission to read the key. */
|
|
13
|
-
get isAccessDenied(): boolean;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Narrows an unknown error to a {@link KeychainAccessError} caused by missing
|
|
17
|
-
* Keychain permission (denied/dismissed prompt, locked, or non-interactive).
|
|
18
|
-
*/
|
|
19
|
-
export declare function isKeychainAccessDenied(err: unknown): err is KeychainAccessError;
|
|
20
|
-
export type KeychainPasswordProvider = (service: string) => string;
|
|
21
|
-
/**
|
|
22
|
-
* Returns the current global keychain password provider hook.
|
|
23
|
-
*/
|
|
24
|
-
export declare function getKeychainPasswordProvider(): KeychainPasswordProvider | null;
|
|
25
|
-
/**
|
|
26
|
-
* Sets the global keychain password provider hook, allowing downstream apps (like Electron)
|
|
27
|
-
* to intercept keychain password lookups and resolve them (e.g. via native node-keytar).
|
|
28
|
-
*/
|
|
29
|
-
export declare function setKeychainPasswordProvider(provider: KeychainPasswordProvider | null): void;
|
|
30
|
-
/**
|
|
31
|
-
* Retrieves a browser's "Safe Storage" password from the macOS Keychain.
|
|
32
|
-
* Caches the result per service for subsequent calls within the same process.
|
|
33
|
-
*
|
|
34
|
-
* @param service - Keychain service name, e.g. "Chrome Safe Storage".
|
|
35
|
-
* @returns The raw keychain password.
|
|
36
|
-
* @throws If the password could not be retrieved.
|
|
37
|
-
*/
|
|
38
|
-
export declare function getKeychainPassword(service?: string): string;
|
|
39
|
-
/**
|
|
40
|
-
* Backward-compatible alias for {@link getKeychainPassword} using Chrome's item.
|
|
41
|
-
* @deprecated Prefer {@link getKeychainPassword} with an explicit service name.
|
|
42
|
-
*/
|
|
43
|
-
export declare function getChromeKeychainPassword(): string;
|
|
44
|
-
/**
|
|
45
|
-
* Derives the AES-128-CBC key from a browser's Keychain password using PBKDF2.
|
|
46
|
-
* Caches the result per service.
|
|
47
|
-
*
|
|
48
|
-
* @param service - Keychain service name. Defaults to Chrome's.
|
|
49
|
-
* @param keychainPassword - The raw password. If omitted, fetched from Keychain.
|
|
50
|
-
* @returns The derived 16-byte key.
|
|
51
|
-
*/
|
|
52
|
-
export declare function deriveEncryptionKey(service?: string, keychainPassword?: string): Buffer;
|
|
53
|
-
/**
|
|
54
|
-
* Clears cached keychain data for all browsers. Useful for testing or
|
|
55
|
-
* re-authentication.
|
|
56
|
-
*/
|
|
57
|
-
export declare function clearKeychainCache(): void;
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
import { execFileSync } from "node:child_process";
|
|
2
|
-
import { pbkdf2Sync } from "node:crypto";
|
|
3
|
-
const PBKDF2_ITERATIONS = 1003;
|
|
4
|
-
const PBKDF2_KEY_LENGTH = 16;
|
|
5
|
-
const PBKDF2_SALT = Buffer.from("saltysalt");
|
|
6
|
-
/** Chrome's Keychain item, used when no browser-specific label is supplied. */
|
|
7
|
-
const DEFAULT_SAFE_STORAGE_SERVICE = "Chrome Safe Storage";
|
|
8
|
-
/**
|
|
9
|
-
* Exit codes returned by the macOS `security` CLI (mapped from OSStatus).
|
|
10
|
-
* Only the ones we explicitly handle are listed here.
|
|
11
|
-
*/
|
|
12
|
-
const SECURITY_EXIT = {
|
|
13
|
-
ITEM_NOT_FOUND: 44, // errSecItemNotFound
|
|
14
|
-
INTERACTION_NOT_ALLOWED: 36, // errSecInteractionNotAllowed (SSH / headless / locked)
|
|
15
|
-
AUTH_FAILED: 45, // errSecAuthFailed
|
|
16
|
-
USER_CANCELED: 128, // user clicked "Deny"/cancelled the GUI authorization prompt
|
|
17
|
-
};
|
|
18
|
-
/**
|
|
19
|
-
* Error thrown when a browser's Safe Storage key cannot be read from the
|
|
20
|
-
* Keychain. Carries the failing `service` and a `kind` so the CLI can show a
|
|
21
|
-
* clear "No keychain access" message instead of a generic failure.
|
|
22
|
-
*/
|
|
23
|
-
export class KeychainAccessError extends Error {
|
|
24
|
-
service;
|
|
25
|
-
kind;
|
|
26
|
-
constructor(message, service, kind) {
|
|
27
|
-
super(message);
|
|
28
|
-
this.name = "KeychainAccessError";
|
|
29
|
-
this.service = service;
|
|
30
|
-
this.kind = kind;
|
|
31
|
-
}
|
|
32
|
-
/** True when the failure means we couldn't get permission to read the key. */
|
|
33
|
-
get isAccessDenied() {
|
|
34
|
-
return this.kind === "access-denied";
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Narrows an unknown error to a {@link KeychainAccessError} caused by missing
|
|
39
|
-
* Keychain permission (denied/dismissed prompt, locked, or non-interactive).
|
|
40
|
-
*/
|
|
41
|
-
export function isKeychainAccessDenied(err) {
|
|
42
|
-
return err instanceof KeychainAccessError && err.isAccessDenied;
|
|
43
|
-
}
|
|
44
|
-
// Caches keyed by Keychain service name so multiple browsers can be read in one
|
|
45
|
-
// process (e.g. `--all-browsers`) without the cache returning the wrong key.
|
|
46
|
-
const _cachedPasswords = new Map();
|
|
47
|
-
const _cachedDerivedKeys = new Map();
|
|
48
|
-
/** Heuristic: are we running without an interactive GUI (Aqua) session? */
|
|
49
|
-
function isLikelyHeadless() {
|
|
50
|
-
return Boolean(process.env.SSH_CONNECTION || process.env.SSH_TTY) || !process.env.TERM_PROGRAM;
|
|
51
|
-
}
|
|
52
|
-
/** Normalizes the unknown error thrown by execFileSync into the bits we care about. */
|
|
53
|
-
function parseSecurityError(err) {
|
|
54
|
-
const e = err;
|
|
55
|
-
return {
|
|
56
|
-
status: typeof e?.status === "number" ? e.status : null,
|
|
57
|
-
stderr: (e?.stderr ?? "").toString().trim(),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
/** Builds a cause-specific, actionable error for a failed keychain read. */
|
|
61
|
-
function buildKeychainError(service, status, stderr) {
|
|
62
|
-
const docs = "See docs/remote-keychain-access.md for non-interactive / SSH setup.";
|
|
63
|
-
let kind;
|
|
64
|
-
let hint;
|
|
65
|
-
if (status === SECURITY_EXIT.ITEM_NOT_FOUND || /could not be found/i.test(stderr)) {
|
|
66
|
-
kind = "not-found";
|
|
67
|
-
hint =
|
|
68
|
-
`No '${service}' item found in the Keychain.\n` +
|
|
69
|
-
"Run this browser at least once on this machine so it creates the key.";
|
|
70
|
-
}
|
|
71
|
-
else if (status === SECURITY_EXIT.INTERACTION_NOT_ALLOWED ||
|
|
72
|
-
/interaction.*not allowed/i.test(stderr)) {
|
|
73
|
-
kind = "access-denied";
|
|
74
|
-
hint =
|
|
75
|
-
"The Keychain needs a GUI authorization prompt that can't be shown here " +
|
|
76
|
-
(isLikelyHeadless() ? "(you appear to be on a remote/SSH session).\n" : ".\n") +
|
|
77
|
-
"Authorize once from a GUI Terminal (click \"Always Allow\"), then unlock the\n" +
|
|
78
|
-
"keychain in this session:\n" +
|
|
79
|
-
" security unlock-keychain ~/Library/Keychains/login.keychain-db\n" +
|
|
80
|
-
docs;
|
|
81
|
-
}
|
|
82
|
-
else if (status === SECURITY_EXIT.AUTH_FAILED) {
|
|
83
|
-
kind = "access-denied";
|
|
84
|
-
hint =
|
|
85
|
-
"Keychain authorization failed (wrong password or denied).\n" +
|
|
86
|
-
`Try: security unlock-keychain ~/Library/Keychains/login.keychain-db\n${docs}`;
|
|
87
|
-
}
|
|
88
|
-
else {
|
|
89
|
-
// The remaining case — including user-cancelled prompts (exit 128) — is a
|
|
90
|
-
// denied/dismissed authorization, i.e. we simply don't have access.
|
|
91
|
-
kind = "access-denied";
|
|
92
|
-
hint =
|
|
93
|
-
"macOS showed a Keychain access prompt that was denied or dismissed.\n" +
|
|
94
|
-
`Re-run the command and click "Allow" (or "Always Allow"), or authorize once with:\n` +
|
|
95
|
-
` security find-generic-password -w -s "${service}"\n${docs}`;
|
|
96
|
-
}
|
|
97
|
-
const detail = [
|
|
98
|
-
status !== null ? `exit ${status}` : null,
|
|
99
|
-
stderr || null,
|
|
100
|
-
]
|
|
101
|
-
.filter(Boolean)
|
|
102
|
-
.join(": ");
|
|
103
|
-
return new KeychainAccessError(`Could not retrieve ${service} key from macOS Keychain.\n${hint}` +
|
|
104
|
-
(detail ? `\n(security: ${detail})` : ""), service, kind);
|
|
105
|
-
}
|
|
106
|
-
let _passwordProvider = null;
|
|
107
|
-
/**
|
|
108
|
-
* Returns the current global keychain password provider hook.
|
|
109
|
-
*/
|
|
110
|
-
export function getKeychainPasswordProvider() {
|
|
111
|
-
return _passwordProvider;
|
|
112
|
-
}
|
|
113
|
-
/**
|
|
114
|
-
* Sets the global keychain password provider hook, allowing downstream apps (like Electron)
|
|
115
|
-
* to intercept keychain password lookups and resolve them (e.g. via native node-keytar).
|
|
116
|
-
*/
|
|
117
|
-
export function setKeychainPasswordProvider(provider) {
|
|
118
|
-
_passwordProvider = provider;
|
|
119
|
-
}
|
|
120
|
-
/**
|
|
121
|
-
* Retrieves a browser's "Safe Storage" password from the macOS Keychain.
|
|
122
|
-
* Caches the result per service for subsequent calls within the same process.
|
|
123
|
-
*
|
|
124
|
-
* @param service - Keychain service name, e.g. "Chrome Safe Storage".
|
|
125
|
-
* @returns The raw keychain password.
|
|
126
|
-
* @throws If the password could not be retrieved.
|
|
127
|
-
*/
|
|
128
|
-
export function getKeychainPassword(service = DEFAULT_SAFE_STORAGE_SERVICE) {
|
|
129
|
-
const cached = _cachedPasswords.get(service);
|
|
130
|
-
if (cached !== undefined) {
|
|
131
|
-
return cached;
|
|
132
|
-
}
|
|
133
|
-
let password;
|
|
134
|
-
if (_passwordProvider) {
|
|
135
|
-
password = _passwordProvider(service);
|
|
136
|
-
}
|
|
137
|
-
else {
|
|
138
|
-
try {
|
|
139
|
-
// execFileSync (no shell) avoids any quoting/injection concern around the
|
|
140
|
-
// service name, which now comes from the browser registry.
|
|
141
|
-
password = execFileSync("security", ["find-generic-password", "-w", "-s", service], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
142
|
-
}
|
|
143
|
-
catch (err) {
|
|
144
|
-
const { status, stderr } = parseSecurityError(err);
|
|
145
|
-
throw buildKeychainError(service, status, stderr);
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
if (!password) {
|
|
149
|
-
// Command succeeded (exit 0) but returned nothing — treat as a usable failure.
|
|
150
|
-
throw new KeychainAccessError(`Keychain returned an empty ${service} key.\n` +
|
|
151
|
-
"The item may exist with no password. Re-run the browser to recreate it.", service, "empty");
|
|
152
|
-
}
|
|
153
|
-
_cachedPasswords.set(service, password);
|
|
154
|
-
return password;
|
|
155
|
-
}
|
|
156
|
-
/**
|
|
157
|
-
* Backward-compatible alias for {@link getKeychainPassword} using Chrome's item.
|
|
158
|
-
* @deprecated Prefer {@link getKeychainPassword} with an explicit service name.
|
|
159
|
-
*/
|
|
160
|
-
export function getChromeKeychainPassword() {
|
|
161
|
-
return getKeychainPassword(DEFAULT_SAFE_STORAGE_SERVICE);
|
|
162
|
-
}
|
|
163
|
-
/**
|
|
164
|
-
* Derives the AES-128-CBC key from a browser's Keychain password using PBKDF2.
|
|
165
|
-
* Caches the result per service.
|
|
166
|
-
*
|
|
167
|
-
* @param service - Keychain service name. Defaults to Chrome's.
|
|
168
|
-
* @param keychainPassword - The raw password. If omitted, fetched from Keychain.
|
|
169
|
-
* @returns The derived 16-byte key.
|
|
170
|
-
*/
|
|
171
|
-
export function deriveEncryptionKey(service = DEFAULT_SAFE_STORAGE_SERVICE, keychainPassword) {
|
|
172
|
-
const cached = _cachedDerivedKeys.get(service);
|
|
173
|
-
if (cached !== undefined) {
|
|
174
|
-
return cached;
|
|
175
|
-
}
|
|
176
|
-
const password = keychainPassword || getKeychainPassword(service);
|
|
177
|
-
if (!password) {
|
|
178
|
-
throw new Error("Keychain password cannot be empty for key derivation.");
|
|
179
|
-
}
|
|
180
|
-
const key = pbkdf2Sync(password, PBKDF2_SALT, PBKDF2_ITERATIONS, PBKDF2_KEY_LENGTH, "sha1");
|
|
181
|
-
_cachedDerivedKeys.set(service, key);
|
|
182
|
-
return key;
|
|
183
|
-
}
|
|
184
|
-
/**
|
|
185
|
-
* Clears cached keychain data for all browsers. Useful for testing or
|
|
186
|
-
* re-authentication.
|
|
187
|
-
*/
|
|
188
|
-
export function clearKeychainCache() {
|
|
189
|
-
_cachedPasswords.clear();
|
|
190
|
-
_cachedDerivedKeys.clear();
|
|
191
|
-
}
|
package/dist/src/core/limit.d.ts
DELETED
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
/** Normalizes the public limit option. Zero, negatives, and non-finite values mean unlimited. */
|
|
2
|
-
export declare function normalizeLimit(limit: number | undefined): number | undefined;
|
|
3
|
-
/** Applies a limit to the final combined result set across every profile. */
|
|
4
|
-
export declare function limitResults<T>(results: T[], limit: number | undefined): T[];
|
package/dist/src/core/limit.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/** Normalizes the public limit option. Zero, negatives, and non-finite values mean unlimited. */
|
|
2
|
-
export function normalizeLimit(limit) {
|
|
3
|
-
if (limit === undefined)
|
|
4
|
-
return undefined;
|
|
5
|
-
const parsed = Math.trunc(Number(limit));
|
|
6
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
|
|
7
|
-
}
|
|
8
|
-
/** Applies a limit to the final combined result set across every profile. */
|
|
9
|
-
export function limitResults(results, limit) {
|
|
10
|
-
const normalized = normalizeLimit(limit);
|
|
11
|
-
return normalized === undefined ? results : results.slice(0, normalized);
|
|
12
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { ChromePlatform } from "./types.js";
|
|
2
|
-
/**
|
|
3
|
-
* macOS backend.
|
|
4
|
-
*
|
|
5
|
-
* Key source: the active browser's "<Browser> Safe Storage" password from the
|
|
6
|
-
* login Keychain (e.g. "Chrome Safe Storage", "Microsoft Edge Safe Storage"),
|
|
7
|
-
* stretched with PBKDF2 (1003 iterations) — implemented in `core/keychain.ts`.
|
|
8
|
-
* Cipher: AES-128-CBC with a fixed 16-space IV — implemented in `core/crypto.ts`.
|
|
9
|
-
* The browser only changes *which* Keychain item and data dir are used; the
|
|
10
|
-
* crypto is identical across all Chromium forks.
|
|
11
|
-
*/
|
|
12
|
-
export declare const darwin: ChromePlatform;
|