chrome-tools 1.1.5 → 1.1.7
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/bin/cli.js +556 -1
- package/dist/src/core/browsers.d.ts +7 -3
- package/dist/src/core/browsers.js +186 -1
- package/dist/src/core/chrome.d.ts +6 -4
- package/dist/src/core/chrome.js +240 -1
- package/dist/src/core/crypto.d.ts +1 -1
- package/dist/src/core/crypto.js +113 -1
- package/dist/src/core/db.d.ts +4 -2
- package/dist/src/core/db.js +90 -1
- package/dist/src/core/keychain.d.ts +10 -0
- package/dist/src/core/keychain.js +191 -1
- package/dist/src/core/limit.d.ts +4 -0
- package/dist/src/core/limit.js +12 -0
- package/dist/src/core/platform/darwin.js +31 -1
- package/dist/src/core/platform/index.d.ts +4 -3
- package/dist/src/core/platform/index.js +69 -1
- package/dist/src/core/platform/linux.js +64 -1
- package/dist/src/core/platform/types.d.ts +3 -2
- package/dist/src/core/platform/types.js +1 -1
- package/dist/src/core/platform/win32.js +114 -1
- package/dist/src/core/sql.d.ts +2 -0
- package/dist/src/core/sql.js +4 -0
- package/dist/src/index.d.ts +2 -2
- package/dist/src/index.js +34 -1
- package/dist/src/modules/autofill.d.ts +8 -15
- package/dist/src/modules/autofill.js +170 -1
- package/dist/src/modules/bookmarks.d.ts +3 -5
- package/dist/src/modules/bookmarks.js +83 -1
- package/dist/src/modules/cookies.d.ts +4 -5
- package/dist/src/modules/cookies.js +157 -1
- package/dist/src/modules/downloads.d.ts +3 -5
- package/dist/src/modules/downloads.js +80 -1
- package/dist/src/modules/extensions.d.ts +3 -5
- package/dist/src/modules/extensions.js +71 -1
- package/dist/src/modules/history.d.ts +5 -10
- package/dist/src/modules/history.js +83 -1
- package/dist/src/modules/local-storage.d.ts +3 -5
- package/dist/src/modules/local-storage.js +455 -1
- package/dist/src/modules/passwords.d.ts +4 -5
- package/dist/src/modules/passwords.js +64 -1
- package/dist/src/modules/search-engines.d.ts +3 -5
- package/dist/src/modules/search-engines.js +38 -1
- package/dist/src/modules/top-sites.d.ts +3 -5
- package/dist/src/modules/top-sites.js +33 -1
- package/package.json +14 -12
package/dist/src/core/db.js
CHANGED
|
@@ -1 +1,90 @@
|
|
|
1
|
-
import{existsSync
|
|
1
|
+
import { existsSync, mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join, basename } from "node:path";
|
|
4
|
+
import Database from "better-sqlite3";
|
|
5
|
+
/**
|
|
6
|
+
* Creates a transactionally consistent snapshot of a Chrome SQLite database,
|
|
7
|
+
* executes a query against it, and cleans up. `VACUUM INTO` reads through
|
|
8
|
+
* SQLite itself, so live WAL contents are included without racing raw file
|
|
9
|
+
* copies while Chrome is writing.
|
|
10
|
+
*
|
|
11
|
+
* @param dbFilePath - Absolute path to the Chrome database file.
|
|
12
|
+
* @param sql - SQL query to execute.
|
|
13
|
+
* @param params - Bound parameters for the query.
|
|
14
|
+
* @returns Query result rows.
|
|
15
|
+
* @throws If the file doesn't exist or query fails.
|
|
16
|
+
*/
|
|
17
|
+
export function queryDatabase(dbFilePath, sql, params = []) {
|
|
18
|
+
if (!existsSync(dbFilePath)) {
|
|
19
|
+
throw new Error(`Database file not found: ${dbFilePath}`);
|
|
20
|
+
}
|
|
21
|
+
const tmpDir = mkdtempSync(join(tmpdir(), "chrome-tools-"));
|
|
22
|
+
const tmpDb = join(tmpDir, basename(dbFilePath));
|
|
23
|
+
let sourceDb;
|
|
24
|
+
try {
|
|
25
|
+
sourceDb = new Database(dbFilePath, {
|
|
26
|
+
readonly: true,
|
|
27
|
+
fileMustExist: true,
|
|
28
|
+
timeout: 5000,
|
|
29
|
+
});
|
|
30
|
+
sourceDb.prepare("VACUUM INTO ?").run(tmpDb);
|
|
31
|
+
}
|
|
32
|
+
catch (err) {
|
|
33
|
+
cleanupTmp(tmpDir);
|
|
34
|
+
throw new Error(`Failed to snapshot database file '${basename(dbFilePath)}': ${err.message}`);
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
if (sourceDb) {
|
|
38
|
+
try {
|
|
39
|
+
sourceDb.close();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
/* ignore */
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
let db;
|
|
47
|
+
try {
|
|
48
|
+
db = new Database(tmpDb, { readonly: true, timeout: 5000 });
|
|
49
|
+
const stmt = db.prepare(sql);
|
|
50
|
+
return params.length > 0 ? stmt.all(...params) : stmt.all();
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
throw new Error(`Database query failed on '${basename(dbFilePath)}': ${err.message}`);
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
if (db) {
|
|
57
|
+
try {
|
|
58
|
+
db.close();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
/* ignore */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
cleanupTmp(tmpDir);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Checks whether a specific table exists in the given database file.
|
|
69
|
+
*
|
|
70
|
+
* @param dbFilePath - Absolute path to the database file.
|
|
71
|
+
* @param tableName - Name of the table to check.
|
|
72
|
+
* @returns True if the table exists.
|
|
73
|
+
*/
|
|
74
|
+
export function tableExists(dbFilePath, tableName) {
|
|
75
|
+
try {
|
|
76
|
+
const rows = queryDatabase(dbFilePath, `SELECT name FROM sqlite_master WHERE type='table' AND name=?`, [tableName]);
|
|
77
|
+
return rows.length > 0;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function cleanupTmp(tmpDir) {
|
|
84
|
+
try {
|
|
85
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
/* ignore */
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -17,6 +17,16 @@ export declare class KeychainAccessError extends Error {
|
|
|
17
17
|
* Keychain permission (denied/dismissed prompt, locked, or non-interactive).
|
|
18
18
|
*/
|
|
19
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;
|
|
20
30
|
/**
|
|
21
31
|
* Retrieves a browser's "Safe Storage" password from the macOS Keychain.
|
|
22
32
|
* Caches the result per service for subsequent calls within the same process.
|
|
@@ -1 +1,191 @@
|
|
|
1
|
-
import
|
|
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
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
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[];
|
|
@@ -0,0 +1,12 @@
|
|
|
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 +1,31 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { deriveEncryptionKey, clearKeychainCache } from "../keychain.js";
|
|
4
|
+
import { decryptValueBuffer } from "../crypto.js";
|
|
5
|
+
import { getActiveBrowser } from "../browsers.js";
|
|
6
|
+
/**
|
|
7
|
+
* macOS backend.
|
|
8
|
+
*
|
|
9
|
+
* Key source: the active browser's "<Browser> Safe Storage" password from the
|
|
10
|
+
* login Keychain (e.g. "Chrome Safe Storage", "Microsoft Edge Safe Storage"),
|
|
11
|
+
* stretched with PBKDF2 (1003 iterations) — implemented in `core/keychain.ts`.
|
|
12
|
+
* Cipher: AES-128-CBC with a fixed 16-space IV — implemented in `core/crypto.ts`.
|
|
13
|
+
* The browser only changes *which* Keychain item and data dir are used; the
|
|
14
|
+
* crypto is identical across all Chromium forks.
|
|
15
|
+
*/
|
|
16
|
+
export const darwin = {
|
|
17
|
+
name: "darwin",
|
|
18
|
+
getDefaultChromeDir(browser = getActiveBrowser()) {
|
|
19
|
+
return join(homedir(), "Library", "Application Support", ...browser.dirs.darwin.split("/"));
|
|
20
|
+
},
|
|
21
|
+
getDecryptionKey(_chromeDir, browser = getActiveBrowser()) {
|
|
22
|
+
// The Keychain item is global to the machine, so chromeDir is irrelevant here.
|
|
23
|
+
return deriveEncryptionKey(browser.safeStorageLabel);
|
|
24
|
+
},
|
|
25
|
+
decrypt(blob, key) {
|
|
26
|
+
return decryptValueBuffer(blob, key);
|
|
27
|
+
},
|
|
28
|
+
clearCache() {
|
|
29
|
+
clearKeychainCache();
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
* `decryptValue`, `decryptValueBuffer`, `clearKeyCache`) and stays OS-agnostic.
|
|
7
7
|
*/
|
|
8
8
|
import type { ChromePlatform } from "./types.js";
|
|
9
|
+
import { type BrowserDescriptor } from "../browsers.js";
|
|
9
10
|
export type { ChromePlatform } from "./types.js";
|
|
10
|
-
export { BROWSERS, listBrowsers, resolveBrowser, getActiveBrowser, setActiveBrowser, type BrowserId, type BrowserDescriptor, } from "../browsers.js";
|
|
11
|
+
export { BROWSERS, listBrowsers, resolveBrowser, requireBrowser, getActiveBrowser, setActiveBrowser, type BrowserId, type BrowserDescriptor, } from "../browsers.js";
|
|
11
12
|
export { KeychainAccessError, isKeychainAccessDenied, type KeychainErrorKind, } from "../keychain.js";
|
|
12
13
|
/**
|
|
13
14
|
* Returns the backend for the current OS, cached after first call.
|
|
@@ -15,12 +16,12 @@ export { KeychainAccessError, isKeychainAccessDenied, type KeychainErrorKind, }
|
|
|
15
16
|
*/
|
|
16
17
|
export declare function selectPlatform(): ChromePlatform;
|
|
17
18
|
/** Default Chrome User Data directory for the current OS. */
|
|
18
|
-
export declare function getPlatformChromeDir(): string;
|
|
19
|
+
export declare function getPlatformChromeDir(browser?: string | BrowserDescriptor): string;
|
|
19
20
|
/**
|
|
20
21
|
* Returns the symmetric key for decrypting this profile's secrets.
|
|
21
22
|
* @param chromeDir - Needed on Windows, where the key lives in `Local State`.
|
|
22
23
|
*/
|
|
23
|
-
export declare function deriveEncryptionKey(chromeDir?: string): Buffer;
|
|
24
|
+
export declare function deriveEncryptionKey(chromeDir?: string, browser?: string | BrowserDescriptor): Buffer;
|
|
24
25
|
/** Decrypts a Chrome blob and returns the plaintext bytes (or a placeholder string). */
|
|
25
26
|
export declare function decryptValueBuffer(blob: Buffer | Uint8Array | null | undefined, key: Buffer): Buffer | string;
|
|
26
27
|
/** Decrypts a Chrome blob and returns the plaintext as a UTF-8 string. */
|
|
@@ -1 +1,69 @@
|
|
|
1
|
-
import{darwin
|
|
1
|
+
import { darwin } from "./darwin.js";
|
|
2
|
+
import { win32 } from "./win32.js";
|
|
3
|
+
import { linux } from "./linux.js";
|
|
4
|
+
import { getActiveBrowser, requireBrowser } from "../browsers.js";
|
|
5
|
+
// Browser selection is orthogonal to platform selection: the backends above
|
|
6
|
+
// read the active browser's data dir + Safe Storage label from the registry.
|
|
7
|
+
// Re-exported here so the rest of the codebase has one import site for both
|
|
8
|
+
// "which OS" and "which browser".
|
|
9
|
+
export { BROWSERS, listBrowsers, resolveBrowser, requireBrowser, getActiveBrowser, setActiveBrowser, } from "../browsers.js";
|
|
10
|
+
// Keychain access failures (macOS) surface a typed error so the CLI can show a
|
|
11
|
+
// clear "No keychain access" message instead of a generic failure.
|
|
12
|
+
export { KeychainAccessError, isKeychainAccessDenied, } from "../keychain.js";
|
|
13
|
+
let _active = null;
|
|
14
|
+
/**
|
|
15
|
+
* Returns the backend for the current OS, cached after first call.
|
|
16
|
+
* @throws On an unsupported platform.
|
|
17
|
+
*/
|
|
18
|
+
export function selectPlatform() {
|
|
19
|
+
if (_active)
|
|
20
|
+
return _active;
|
|
21
|
+
switch (process.platform) {
|
|
22
|
+
case "darwin":
|
|
23
|
+
_active = darwin;
|
|
24
|
+
break;
|
|
25
|
+
case "win32":
|
|
26
|
+
_active = win32;
|
|
27
|
+
break;
|
|
28
|
+
case "linux":
|
|
29
|
+
_active = linux;
|
|
30
|
+
break;
|
|
31
|
+
default:
|
|
32
|
+
throw new Error(`Unsupported platform: ${process.platform}. ` +
|
|
33
|
+
"chrome-tools supports macOS, Windows, and Linux.");
|
|
34
|
+
}
|
|
35
|
+
return _active;
|
|
36
|
+
}
|
|
37
|
+
/** Default Chrome User Data directory for the current OS. */
|
|
38
|
+
export function getPlatformChromeDir(browser) {
|
|
39
|
+
return selectPlatform().getDefaultChromeDir(resolveTargetBrowser(browser));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Returns the symmetric key for decrypting this profile's secrets.
|
|
43
|
+
* @param chromeDir - Needed on Windows, where the key lives in `Local State`.
|
|
44
|
+
*/
|
|
45
|
+
export function deriveEncryptionKey(chromeDir, browser) {
|
|
46
|
+
return selectPlatform().getDecryptionKey(chromeDir, resolveTargetBrowser(browser));
|
|
47
|
+
}
|
|
48
|
+
/** Decrypts a Chrome blob and returns the plaintext bytes (or a placeholder string). */
|
|
49
|
+
export function decryptValueBuffer(blob, key) {
|
|
50
|
+
if (!blob || blob.length === 0) {
|
|
51
|
+
return Buffer.alloc(0);
|
|
52
|
+
}
|
|
53
|
+
const buf = Buffer.isBuffer(blob) ? blob : Buffer.from(blob);
|
|
54
|
+
return selectPlatform().decrypt(buf, key);
|
|
55
|
+
}
|
|
56
|
+
/** Decrypts a Chrome blob and returns the plaintext as a UTF-8 string. */
|
|
57
|
+
export function decryptValue(blob, key) {
|
|
58
|
+
const decrypted = decryptValueBuffer(blob, key);
|
|
59
|
+
return Buffer.isBuffer(decrypted) ? decrypted.toString("utf-8") : decrypted;
|
|
60
|
+
}
|
|
61
|
+
/** Clears the active backend's cached key material. */
|
|
62
|
+
export function clearKeyCache() {
|
|
63
|
+
selectPlatform().clearCache();
|
|
64
|
+
}
|
|
65
|
+
function resolveTargetBrowser(browser) {
|
|
66
|
+
if (!browser)
|
|
67
|
+
return getActiveBrowser();
|
|
68
|
+
return typeof browser === "string" ? requireBrowser(browser) : browser;
|
|
69
|
+
}
|
|
@@ -1 +1,64 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { pbkdf2Sync } from "node:crypto";
|
|
5
|
+
import { decryptValueBuffer } from "../crypto.js";
|
|
6
|
+
import { getActiveBrowser } from "../browsers.js";
|
|
7
|
+
import { getKeychainPasswordProvider } from "../keychain.js";
|
|
8
|
+
// Linux uses the same CBC scheme as macOS but with a single PBKDF2 iteration.
|
|
9
|
+
const PBKDF2_ITERATIONS = 1;
|
|
10
|
+
const PBKDF2_KEY_LENGTH = 16;
|
|
11
|
+
const PBKDF2_SALT = Buffer.from("saltysalt");
|
|
12
|
+
// Chrome's hardcoded fallback when no secret service (gnome-keyring/kwallet) is
|
|
13
|
+
// available — yields the "Basic" (effectively unencrypted) storage scheme.
|
|
14
|
+
const FALLBACK_PASSWORD = "peanuts";
|
|
15
|
+
// Keyed by Secret Service `application` attribute so different browsers cache
|
|
16
|
+
// independently (e.g. under `--all-browsers`).
|
|
17
|
+
const _cachedKeys = new Map();
|
|
18
|
+
/** Looks up the "Safe Storage" password via libsecret, falling back to "peanuts". */
|
|
19
|
+
function getStoragePassword(application) {
|
|
20
|
+
const provider = getKeychainPasswordProvider();
|
|
21
|
+
if (provider) {
|
|
22
|
+
return provider(application);
|
|
23
|
+
}
|
|
24
|
+
try {
|
|
25
|
+
const out = execFileSync("secret-tool", ["lookup", "application", application], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }).trim();
|
|
26
|
+
if (out)
|
|
27
|
+
return out;
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// secret-tool missing, or no keyring / no entry — fall through to default.
|
|
31
|
+
}
|
|
32
|
+
return FALLBACK_PASSWORD;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Linux backend.
|
|
36
|
+
*
|
|
37
|
+
* Key source: the active browser's "Safe Storage" password from the Secret
|
|
38
|
+
* Service (gnome-keyring / KWallet) via `secret-tool`, or the hardcoded
|
|
39
|
+
* "peanuts" fallback, stretched with PBKDF2 (1 iteration).
|
|
40
|
+
* Cipher: AES-128-CBC with a fixed 16-space IV (same primitive as macOS).
|
|
41
|
+
*/
|
|
42
|
+
export const linux = {
|
|
43
|
+
name: "linux",
|
|
44
|
+
getDefaultChromeDir(browser = getActiveBrowser()) {
|
|
45
|
+
const configHome = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
|
|
46
|
+
return join(configHome, ...browser.dirs.linux.split("/"));
|
|
47
|
+
},
|
|
48
|
+
getDecryptionKey(_chromeDir, browser = getActiveBrowser()) {
|
|
49
|
+
const application = browser.linuxSecretApp || "chrome";
|
|
50
|
+
const cached = _cachedKeys.get(application);
|
|
51
|
+
if (cached !== undefined) {
|
|
52
|
+
return cached;
|
|
53
|
+
}
|
|
54
|
+
const key = pbkdf2Sync(getStoragePassword(application), PBKDF2_SALT, PBKDF2_ITERATIONS, PBKDF2_KEY_LENGTH, "sha1");
|
|
55
|
+
_cachedKeys.set(application, key);
|
|
56
|
+
return key;
|
|
57
|
+
},
|
|
58
|
+
decrypt(blob, key) {
|
|
59
|
+
return decryptValueBuffer(blob, key);
|
|
60
|
+
},
|
|
61
|
+
clearCache() {
|
|
62
|
+
_cachedKeys.clear();
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { BrowserDescriptor } from "../browsers.js";
|
|
1
2
|
/**
|
|
2
3
|
* The platform-specific backend contract.
|
|
3
4
|
*
|
|
@@ -11,7 +12,7 @@ export interface ChromePlatform {
|
|
|
11
12
|
/** Node platform identifier this backend handles (e.g. "darwin", "win32", "linux"). */
|
|
12
13
|
readonly name: string;
|
|
13
14
|
/** Absolute path to Chrome's User Data directory for this OS. */
|
|
14
|
-
getDefaultChromeDir(): string;
|
|
15
|
+
getDefaultChromeDir(browser?: BrowserDescriptor): string;
|
|
15
16
|
/**
|
|
16
17
|
* Returns the symmetric key used to decrypt this profile's secrets, ready to
|
|
17
18
|
* hand to {@link decrypt}. May read the OS key store and is cached internally.
|
|
@@ -20,7 +21,7 @@ export interface ChromePlatform {
|
|
|
20
21
|
* key lives inside `Local State`. Defaults to {@link getDefaultChromeDir}.
|
|
21
22
|
* @throws An actionable Error if the key cannot be obtained.
|
|
22
23
|
*/
|
|
23
|
-
getDecryptionKey(chromeDir?: string): Buffer;
|
|
24
|
+
getDecryptionKey(chromeDir?: string, browser?: BrowserDescriptor): Buffer;
|
|
24
25
|
/**
|
|
25
26
|
* Decrypts a single Chrome `v10`/`v11` blob using this platform's scheme
|
|
26
27
|
* (AES-128-CBC on macOS/Linux, AES-256-GCM on Windows).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export{};
|
|
1
|
+
export {};
|