cloak22 2.2.0
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/.githooks/pre-commit +12 -0
- package/.githooks/pre-push +37 -0
- package/LICENSE +21 -0
- package/README.md +181 -0
- package/README.org +187 -0
- package/bin/cloak.js +24 -0
- package/dist/app-paths.js +26 -0
- package/dist/chrome-cookies.js +420 -0
- package/dist/chrome-profile-sites.js +155 -0
- package/dist/chrome-profiles.js +71 -0
- package/dist/cli.js +627 -0
- package/dist/cookies.js +95 -0
- package/dist/daemon.js +93 -0
- package/dist/extension.js +133 -0
- package/dist/install-extension.js +13 -0
- package/dist/main.js +688 -0
- package/dist/output.js +26 -0
- package/dist/state-db.js +232 -0
- package/docs/assets/cloak-logo-readme-centered.png +0 -0
- package/package.json +66 -0
- package/scripts/postinstall.cjs +55 -0
- package/scripts/render-readme.cjs +54 -0
- package/scripts/setup-git-hooks.cjs +21 -0
- package/src/app-paths.ts +39 -0
- package/src/chrome-cookies.ts +681 -0
- package/src/chrome-profile-sites.ts +274 -0
- package/src/chrome-profiles.ts +92 -0
- package/src/cli.ts +815 -0
- package/src/cookies.ts +149 -0
- package/src/daemon.ts +143 -0
- package/src/extension.ts +201 -0
- package/src/install-extension.ts +13 -0
- package/src/main.ts +1085 -0
- package/src/output.ts +21 -0
- package/src/state-db.ts +320 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process"
|
|
2
|
+
import { createDecipheriv, createHash, pbkdf2Sync } from "node:crypto"
|
|
3
|
+
import fs from "node:fs"
|
|
4
|
+
import os from "node:os"
|
|
5
|
+
import path from "node:path"
|
|
6
|
+
import { normalizeCookie, type Cookie } from "./cookies.js"
|
|
7
|
+
import { resolveChromeCookiesDatabasePath } from "./chrome-profile-sites.js"
|
|
8
|
+
import { defaultChromeUserDataDir } from "./chrome-profiles.js"
|
|
9
|
+
|
|
10
|
+
export const CHROME_COOKIE_LIMITATION_WARNING =
|
|
11
|
+
"Chrome cookie import is best-effort and only reuses cookies, not the rest of the browser profile. If a login still fails after injection, the site may depend on additional browser state."
|
|
12
|
+
export const CHROME_COOKIE_SUPPORT_MISSING_ERROR =
|
|
13
|
+
"Chrome cookie support is not available in this environment. cloak could not access the selected profile's cookie database or the platform secret storage needed to decrypt it."
|
|
14
|
+
export const CHROME_COOKIE_APP_BOUND_UNSUPPORTED_ERROR =
|
|
15
|
+
"Chrome app-bound cookie encryption on Windows is not supported by cloak yet."
|
|
16
|
+
|
|
17
|
+
type ChromePuppeteerCookie = {
|
|
18
|
+
name: string
|
|
19
|
+
value: string
|
|
20
|
+
domain: string
|
|
21
|
+
path: string
|
|
22
|
+
expires: number | bigint
|
|
23
|
+
HttpOnly?: boolean
|
|
24
|
+
Secure?: boolean
|
|
25
|
+
sameSite?: string
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
type ChromeCookieReader = (
|
|
29
|
+
url: string,
|
|
30
|
+
format: "puppeteer",
|
|
31
|
+
profile?: string
|
|
32
|
+
) => Promise<ChromePuppeteerCookie[]>
|
|
33
|
+
|
|
34
|
+
type ChromeCookieDatabaseRow = {
|
|
35
|
+
host_key: string
|
|
36
|
+
path: string
|
|
37
|
+
is_secure: number | bigint
|
|
38
|
+
expires_utc: number | bigint
|
|
39
|
+
name: string
|
|
40
|
+
value: string
|
|
41
|
+
encrypted_value: Uint8Array | null
|
|
42
|
+
creation_utc: number | bigint
|
|
43
|
+
is_httponly: number | bigint
|
|
44
|
+
samesite: number | bigint | null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
type StatementLike = {
|
|
48
|
+
all(...parameters: string[]): Array<Record<string, unknown>>
|
|
49
|
+
setReadBigInts?(enabled: boolean): StatementLike
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type DatabaseLike = {
|
|
53
|
+
prepare(sql: string): StatementLike
|
|
54
|
+
close(): void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
type DatabaseConstructor = new (
|
|
58
|
+
path: string,
|
|
59
|
+
options?: {
|
|
60
|
+
readOnly?: boolean
|
|
61
|
+
}
|
|
62
|
+
) => DatabaseLike
|
|
63
|
+
|
|
64
|
+
type ChromeCookieReaderDependencies = {
|
|
65
|
+
chromeUserDataDir?: string
|
|
66
|
+
platform?: NodeJS.Platform
|
|
67
|
+
pathExists?: (targetPath: string) => boolean
|
|
68
|
+
makeTempDir?: (prefix: string) => string
|
|
69
|
+
copyFile?: (sourcePath: string, targetPath: string) => void
|
|
70
|
+
removeDir?: (targetPath: string, options: { recursive: true; force: true }) => void
|
|
71
|
+
queryRows?: (databasePath: string, hostKeys: string[]) => ChromeCookieDatabaseRow[]
|
|
72
|
+
readFile?: (targetPath: string) => string
|
|
73
|
+
runCommand?: (command: string, args: string[]) => string
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type ChromeCookieCryptoCache = {
|
|
77
|
+
macKey?: Buffer
|
|
78
|
+
linuxV11Key?: Buffer | null
|
|
79
|
+
windowsKey?: Buffer
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const CHROMIUM_EPOCH_MICROSECONDS = 11644473600000000n
|
|
83
|
+
const POSIX_IV = Buffer.from(" ".repeat(16), "utf8")
|
|
84
|
+
const POSIX_SALT = "saltysalt"
|
|
85
|
+
const MAC_SAFE_STORAGE_SERVICE = "Chrome Safe Storage"
|
|
86
|
+
const MAC_SAFE_STORAGE_ACCOUNT = "Chrome"
|
|
87
|
+
const LINUX_V10_PASSWORD = "peanuts"
|
|
88
|
+
const WINDOWS_DPAPI_KEY_PREFIX = Buffer.from("DPAPI", "utf8")
|
|
89
|
+
const WINDOWS_APP_BOUND_KEY_PREFIX = Buffer.from("APPB", "utf8")
|
|
90
|
+
|
|
91
|
+
function loadDatabaseConstructor(): DatabaseConstructor {
|
|
92
|
+
const sqlite = require("node:sqlite") as {
|
|
93
|
+
DatabaseSync: DatabaseConstructor
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return sqlite.DatabaseSync
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function chromiumTimestampToUnixSeconds(timestamp: number | bigint): number {
|
|
100
|
+
if (typeof timestamp === "bigint") {
|
|
101
|
+
return Number((timestamp - CHROMIUM_EPOCH_MICROSECONDS) / 1000000n)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return Math.trunc(
|
|
105
|
+
(timestamp - Number(CHROMIUM_EPOCH_MICROSECONDS)) / 1000000
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function normalizeChromeCookie(raw: ChromePuppeteerCookie): Cookie {
|
|
110
|
+
const normalized = {
|
|
111
|
+
name: raw.name,
|
|
112
|
+
value: raw.value,
|
|
113
|
+
domain: raw.domain,
|
|
114
|
+
path: raw.path,
|
|
115
|
+
httpOnly: raw.HttpOnly,
|
|
116
|
+
secure: raw.Secure,
|
|
117
|
+
sameSite: raw.sameSite,
|
|
118
|
+
} as Parameters<typeof normalizeCookie>[0] & { expires?: number }
|
|
119
|
+
|
|
120
|
+
if (!isZeroChromiumTimestamp(raw.expires)) {
|
|
121
|
+
normalized.expires = chromiumTimestampToUnixSeconds(raw.expires)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return normalizeCookie(normalized)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isZeroChromiumTimestamp(timestamp: number | bigint): boolean {
|
|
128
|
+
return typeof timestamp === "bigint" ? timestamp === 0n : timestamp === 0
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export async function readChromeCookies(
|
|
132
|
+
options: { url: string; profile?: string },
|
|
133
|
+
getCookies: ChromeCookieReader = createChromeCookieReader()
|
|
134
|
+
): Promise<Cookie[]> {
|
|
135
|
+
const cookies = await getCookies(options.url, "puppeteer", options.profile)
|
|
136
|
+
return cookies.map(normalizeChromeCookie)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function createChromeCookieReader(
|
|
140
|
+
dependencies: ChromeCookieReaderDependencies = {}
|
|
141
|
+
): ChromeCookieReader {
|
|
142
|
+
return async (url: string, format: "puppeteer", profile?: string) => {
|
|
143
|
+
if (format !== "puppeteer") {
|
|
144
|
+
throw new Error("cloak only supports Chrome cookie export in puppeteer format")
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const parsedUrl = parseCookieUrl(url)
|
|
148
|
+
const chromeUserDataDir =
|
|
149
|
+
dependencies.chromeUserDataDir ?? defaultChromeUserDataDir()
|
|
150
|
+
const pathExists = dependencies.pathExists ?? fs.existsSync
|
|
151
|
+
const makeTempDir = dependencies.makeTempDir ?? fs.mkdtempSync
|
|
152
|
+
const copyFile = dependencies.copyFile ?? fs.copyFileSync
|
|
153
|
+
const removeDir = dependencies.removeDir ?? fs.rmSync
|
|
154
|
+
const queryRows = dependencies.queryRows ?? queryChromeCookieRows
|
|
155
|
+
const sourcePath = resolveChromeCookiesDatabasePath(
|
|
156
|
+
{
|
|
157
|
+
chromeUserDataDir,
|
|
158
|
+
profileDirectory: profile ?? "Default",
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
pathExists,
|
|
162
|
+
}
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
if (!sourcePath) {
|
|
166
|
+
return []
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const tempRoot = makeTempDir(path.join(os.tmpdir(), "cloak-cookie-db-"))
|
|
170
|
+
const stagedPath = path.join(tempRoot, path.basename(sourcePath))
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
copyFile(sourcePath, stagedPath)
|
|
174
|
+
copyOptionalSidecar(`${sourcePath}-wal`, `${stagedPath}-wal`, pathExists, copyFile)
|
|
175
|
+
copyOptionalSidecar(`${sourcePath}-shm`, `${stagedPath}-shm`, pathExists, copyFile)
|
|
176
|
+
|
|
177
|
+
const rows = queryRows(stagedPath, candidateChromeCookieHosts(parsedUrl.hostname))
|
|
178
|
+
const cryptoCache: ChromeCookieCryptoCache = {}
|
|
179
|
+
const cookies: ChromePuppeteerCookie[] = []
|
|
180
|
+
|
|
181
|
+
for (const row of rows) {
|
|
182
|
+
if (!chromeCookieMatchesUrl(row, parsedUrl)) {
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
cookies.push(
|
|
187
|
+
rowToPuppeteerCookie(row, {
|
|
188
|
+
...dependencies,
|
|
189
|
+
chromeUserDataDir,
|
|
190
|
+
}, cryptoCache)
|
|
191
|
+
)
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return cookies
|
|
195
|
+
} finally {
|
|
196
|
+
removeDir(tempRoot, { recursive: true, force: true })
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function candidateChromeCookieHosts(hostname: string): string[] {
|
|
202
|
+
const normalizedHost = hostname.trim().toLowerCase().replace(/\.$/, "")
|
|
203
|
+
|
|
204
|
+
if (!normalizedHost) {
|
|
205
|
+
return []
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const labels = normalizedHost.split(".")
|
|
209
|
+
const hosts = new Set<string>()
|
|
210
|
+
|
|
211
|
+
for (let index = 0; index < labels.length; index += 1) {
|
|
212
|
+
const candidate = labels.slice(index).join(".")
|
|
213
|
+
hosts.add(candidate)
|
|
214
|
+
hosts.add(`.${candidate}`)
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return [...hosts]
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function chromeCookieMatchesUrl(
|
|
221
|
+
row: Pick<ChromeCookieDatabaseRow, "host_key" | "path" | "is_secure">,
|
|
222
|
+
url: URL
|
|
223
|
+
): boolean {
|
|
224
|
+
if (toBoolean(row.is_secure) && url.protocol !== "https:") {
|
|
225
|
+
return false
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!hostMatchesCookieDomain(url.hostname, row.host_key)) {
|
|
229
|
+
return false
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return pathMatchesCookiePath(url.pathname || "/", row.path || "/")
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function parseCookieUrl(url: string): URL {
|
|
236
|
+
let parsedUrl: URL
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
parsedUrl = new URL(url)
|
|
240
|
+
} catch {
|
|
241
|
+
throw new Error(
|
|
242
|
+
"Could not parse URI, format should be http://www.example.com/path/"
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
247
|
+
throw new Error(
|
|
248
|
+
"Could not parse URI, format should be http://www.example.com/path/"
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return parsedUrl
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function rowToPuppeteerCookie(
|
|
256
|
+
row: ChromeCookieDatabaseRow,
|
|
257
|
+
dependencies: ChromeCookieReaderDependencies,
|
|
258
|
+
cryptoCache: ChromeCookieCryptoCache
|
|
259
|
+
): ChromePuppeteerCookie {
|
|
260
|
+
const resolvedValue = resolveChromeCookieValue(row, dependencies, cryptoCache)
|
|
261
|
+
const cookie: ChromePuppeteerCookie = {
|
|
262
|
+
name: row.name,
|
|
263
|
+
value: resolvedValue,
|
|
264
|
+
domain: row.host_key,
|
|
265
|
+
path: row.path,
|
|
266
|
+
expires: row.expires_utc,
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (toBoolean(row.is_secure)) {
|
|
270
|
+
cookie.Secure = true
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (toBoolean(row.is_httponly)) {
|
|
274
|
+
cookie.HttpOnly = true
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const sameSite = databaseSameSiteToChromeSameSite(row.samesite)
|
|
278
|
+
if (sameSite) {
|
|
279
|
+
cookie.sameSite = sameSite
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return cookie
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function resolveChromeCookieValue(
|
|
286
|
+
row: ChromeCookieDatabaseRow,
|
|
287
|
+
dependencies: ChromeCookieReaderDependencies,
|
|
288
|
+
cryptoCache: ChromeCookieCryptoCache
|
|
289
|
+
): string {
|
|
290
|
+
const encryptedValue = toBuffer(row.encrypted_value)
|
|
291
|
+
|
|
292
|
+
if (encryptedValue.length === 0) {
|
|
293
|
+
return row.value
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const decrypted = decryptChromeCookieValue(
|
|
297
|
+
encryptedValue,
|
|
298
|
+
row.host_key,
|
|
299
|
+
dependencies,
|
|
300
|
+
cryptoCache
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
return stripEncryptedCookieDomainHash(decrypted, row.host_key).toString("utf8")
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function decryptChromeCookieValue(
|
|
307
|
+
encryptedValue: Buffer,
|
|
308
|
+
hostKey: string,
|
|
309
|
+
dependencies: ChromeCookieReaderDependencies,
|
|
310
|
+
cryptoCache: ChromeCookieCryptoCache
|
|
311
|
+
): Buffer {
|
|
312
|
+
const platform = dependencies.platform ?? process.platform
|
|
313
|
+
|
|
314
|
+
if (platform === "darwin") {
|
|
315
|
+
return decryptPosixCookieValue(
|
|
316
|
+
encryptedValue,
|
|
317
|
+
getMacEncryptionKey(dependencies, cryptoCache)
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (platform === "linux") {
|
|
322
|
+
return decryptLinuxCookieValue(encryptedValue, dependencies, cryptoCache)
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (platform === "win32") {
|
|
326
|
+
return decryptWindowsCookieValue(
|
|
327
|
+
encryptedValue,
|
|
328
|
+
hostKey,
|
|
329
|
+
dependencies,
|
|
330
|
+
cryptoCache
|
|
331
|
+
)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
throw new Error(CHROME_COOKIE_SUPPORT_MISSING_ERROR)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function decryptPosixCookieValue(encryptedValue: Buffer, key: Buffer): Buffer {
|
|
338
|
+
const version = encryptedValue.subarray(0, 3).toString("utf8")
|
|
339
|
+
|
|
340
|
+
if (version !== "v10" && version !== "v11") {
|
|
341
|
+
throw new Error(CHROME_COOKIE_SUPPORT_MISSING_ERROR)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const decipher = createDecipheriv("aes-128-cbc", key, POSIX_IV)
|
|
345
|
+
|
|
346
|
+
return Buffer.concat([
|
|
347
|
+
decipher.update(encryptedValue.subarray(3)),
|
|
348
|
+
decipher.final(),
|
|
349
|
+
])
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function decryptLinuxCookieValue(
|
|
353
|
+
encryptedValue: Buffer,
|
|
354
|
+
dependencies: ChromeCookieReaderDependencies,
|
|
355
|
+
cryptoCache: ChromeCookieCryptoCache
|
|
356
|
+
): Buffer {
|
|
357
|
+
const version = encryptedValue.subarray(0, 3).toString("utf8")
|
|
358
|
+
|
|
359
|
+
if (version === "v10") {
|
|
360
|
+
return decryptPosixCookieValue(encryptedValue, derivePosixKey(LINUX_V10_PASSWORD, 1))
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (version === "v11") {
|
|
364
|
+
const key = getLinuxV11EncryptionKey(dependencies, cryptoCache)
|
|
365
|
+
|
|
366
|
+
if (key) {
|
|
367
|
+
return decryptPosixCookieValue(encryptedValue, key)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return decryptPosixCookieValue(encryptedValue, derivePosixKey("", 1))
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
throw new Error(CHROME_COOKIE_SUPPORT_MISSING_ERROR)
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function decryptWindowsCookieValue(
|
|
377
|
+
encryptedValue: Buffer,
|
|
378
|
+
_hostKey: string,
|
|
379
|
+
dependencies: ChromeCookieReaderDependencies,
|
|
380
|
+
cryptoCache: ChromeCookieCryptoCache
|
|
381
|
+
): Buffer {
|
|
382
|
+
const version = encryptedValue.subarray(0, 3).toString("utf8")
|
|
383
|
+
|
|
384
|
+
if (version === "v20") {
|
|
385
|
+
throw new Error(CHROME_COOKIE_APP_BOUND_UNSUPPORTED_ERROR)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (version !== "v10") {
|
|
389
|
+
return decryptWindowsDpapi(encryptedValue, dependencies)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const key = getWindowsEncryptionKey(dependencies, cryptoCache)
|
|
393
|
+
const nonce = encryptedValue.subarray(3, 15)
|
|
394
|
+
const ciphertext = encryptedValue.subarray(15, encryptedValue.length - 16)
|
|
395
|
+
const authTag = encryptedValue.subarray(encryptedValue.length - 16)
|
|
396
|
+
const decipher = createDecipheriv("aes-256-gcm", key, nonce)
|
|
397
|
+
decipher.setAuthTag(authTag)
|
|
398
|
+
|
|
399
|
+
return Buffer.concat([decipher.update(ciphertext), decipher.final()])
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function getMacEncryptionKey(
|
|
403
|
+
dependencies: ChromeCookieReaderDependencies,
|
|
404
|
+
cryptoCache: ChromeCookieCryptoCache
|
|
405
|
+
): Buffer {
|
|
406
|
+
if (cryptoCache.macKey) {
|
|
407
|
+
return cryptoCache.macKey
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const password = runCommand(dependencies, "security", [
|
|
411
|
+
"find-generic-password",
|
|
412
|
+
"-w",
|
|
413
|
+
"-s",
|
|
414
|
+
MAC_SAFE_STORAGE_SERVICE,
|
|
415
|
+
"-a",
|
|
416
|
+
MAC_SAFE_STORAGE_ACCOUNT,
|
|
417
|
+
]).trimEnd()
|
|
418
|
+
|
|
419
|
+
if (!password) {
|
|
420
|
+
throw new Error(CHROME_COOKIE_SUPPORT_MISSING_ERROR)
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
cryptoCache.macKey = derivePosixKey(password, 1003)
|
|
424
|
+
return cryptoCache.macKey
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function getLinuxV11EncryptionKey(
|
|
428
|
+
dependencies: ChromeCookieReaderDependencies,
|
|
429
|
+
cryptoCache: ChromeCookieCryptoCache
|
|
430
|
+
): Buffer | null {
|
|
431
|
+
if (cryptoCache.linuxV11Key !== undefined) {
|
|
432
|
+
return cryptoCache.linuxV11Key
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
const password = runCommand(dependencies, "secret-tool", [
|
|
437
|
+
"lookup",
|
|
438
|
+
"xdg:schema",
|
|
439
|
+
"chrome_libsecret_os_crypt_password",
|
|
440
|
+
]).trimEnd()
|
|
441
|
+
|
|
442
|
+
cryptoCache.linuxV11Key =
|
|
443
|
+
password.length > 0 ? derivePosixKey(password, 1) : null
|
|
444
|
+
} catch {
|
|
445
|
+
cryptoCache.linuxV11Key = null
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return cryptoCache.linuxV11Key
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function getWindowsEncryptionKey(
|
|
452
|
+
dependencies: ChromeCookieReaderDependencies,
|
|
453
|
+
cryptoCache: ChromeCookieCryptoCache
|
|
454
|
+
): Buffer {
|
|
455
|
+
if (cryptoCache.windowsKey) {
|
|
456
|
+
return cryptoCache.windowsKey
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const chromeUserDataDir =
|
|
460
|
+
dependencies.chromeUserDataDir ?? defaultChromeUserDataDir()
|
|
461
|
+
const readFile = dependencies.readFile ?? ((targetPath: string) => fs.readFileSync(targetPath, "utf8"))
|
|
462
|
+
const localStatePath = path.join(chromeUserDataDir, "Local State")
|
|
463
|
+
const localState = JSON.parse(readFile(localStatePath)) as {
|
|
464
|
+
os_crypt?: {
|
|
465
|
+
encrypted_key?: string
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
const encodedKey = localState.os_crypt?.encrypted_key
|
|
469
|
+
|
|
470
|
+
if (!encodedKey) {
|
|
471
|
+
throw new Error(CHROME_COOKIE_SUPPORT_MISSING_ERROR)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const encryptedKey = Buffer.from(encodedKey, "base64")
|
|
475
|
+
|
|
476
|
+
if (encryptedKey.subarray(0, WINDOWS_APP_BOUND_KEY_PREFIX.length).equals(WINDOWS_APP_BOUND_KEY_PREFIX)) {
|
|
477
|
+
throw new Error(CHROME_COOKIE_APP_BOUND_UNSUPPORTED_ERROR)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (!encryptedKey.subarray(0, WINDOWS_DPAPI_KEY_PREFIX.length).equals(WINDOWS_DPAPI_KEY_PREFIX)) {
|
|
481
|
+
throw new Error(CHROME_COOKIE_SUPPORT_MISSING_ERROR)
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
cryptoCache.windowsKey = decryptWindowsDpapi(
|
|
485
|
+
encryptedKey.subarray(WINDOWS_DPAPI_KEY_PREFIX.length),
|
|
486
|
+
dependencies
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
return cryptoCache.windowsKey
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function decryptWindowsDpapi(
|
|
493
|
+
encryptedValue: Buffer,
|
|
494
|
+
dependencies: ChromeCookieReaderDependencies
|
|
495
|
+
): Buffer {
|
|
496
|
+
const script = [
|
|
497
|
+
"$inputBase64 = $args[0]",
|
|
498
|
+
"$bytes = [Convert]::FromBase64String($inputBase64)",
|
|
499
|
+
"$plain = [System.Security.Cryptography.ProtectedData]::Unprotect(",
|
|
500
|
+
" $bytes,",
|
|
501
|
+
" $null,",
|
|
502
|
+
" [System.Security.Cryptography.DataProtectionScope]::CurrentUser",
|
|
503
|
+
")",
|
|
504
|
+
"[Console]::Out.Write([Convert]::ToBase64String($plain))",
|
|
505
|
+
].join("; ")
|
|
506
|
+
const base64Value = encryptedValue.toString("base64")
|
|
507
|
+
const binaries = ["powershell.exe", "powershell", "pwsh"]
|
|
508
|
+
|
|
509
|
+
for (const binary of binaries) {
|
|
510
|
+
try {
|
|
511
|
+
const output = runCommand(dependencies, binary, [
|
|
512
|
+
"-NoProfile",
|
|
513
|
+
"-NonInteractive",
|
|
514
|
+
"-Command",
|
|
515
|
+
script,
|
|
516
|
+
base64Value,
|
|
517
|
+
]).trim()
|
|
518
|
+
|
|
519
|
+
return Buffer.from(output, "base64")
|
|
520
|
+
} catch {
|
|
521
|
+
continue
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
throw new Error(CHROME_COOKIE_SUPPORT_MISSING_ERROR)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
function derivePosixKey(password: string, iterations: number): Buffer {
|
|
529
|
+
return pbkdf2Sync(password, POSIX_SALT, iterations, 16, "sha1")
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function stripEncryptedCookieDomainHash(
|
|
533
|
+
decryptedValue: Buffer,
|
|
534
|
+
hostKey: string
|
|
535
|
+
): Buffer {
|
|
536
|
+
const domainHash = createHash("sha256").update(hostKey).digest()
|
|
537
|
+
|
|
538
|
+
if (decryptedValue.length < domainHash.length) {
|
|
539
|
+
return decryptedValue
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
if (decryptedValue.subarray(0, domainHash.length).equals(domainHash)) {
|
|
543
|
+
return decryptedValue.subarray(domainHash.length)
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
return decryptedValue
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function hostMatchesCookieDomain(hostname: string, cookieDomain: string): boolean {
|
|
550
|
+
const normalizedHost = hostname.toLowerCase().replace(/\.$/, "")
|
|
551
|
+
const normalizedDomain = cookieDomain.toLowerCase().replace(/^\./, "").replace(/\.$/, "")
|
|
552
|
+
const isDomainCookie = cookieDomain.startsWith(".")
|
|
553
|
+
|
|
554
|
+
if (!isDomainCookie) {
|
|
555
|
+
return normalizedHost === normalizedDomain
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
return (
|
|
559
|
+
normalizedHost === normalizedDomain ||
|
|
560
|
+
normalizedHost.endsWith(`.${normalizedDomain}`)
|
|
561
|
+
)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function pathMatchesCookiePath(requestPath: string, cookiePath: string): boolean {
|
|
565
|
+
const normalizedRequestPath = requestPath || "/"
|
|
566
|
+
const normalizedCookiePath = cookiePath || "/"
|
|
567
|
+
|
|
568
|
+
if (normalizedRequestPath === normalizedCookiePath) {
|
|
569
|
+
return true
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (!normalizedRequestPath.startsWith(normalizedCookiePath)) {
|
|
573
|
+
return false
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
if (normalizedCookiePath.endsWith("/")) {
|
|
577
|
+
return true
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
return normalizedRequestPath.charAt(normalizedCookiePath.length) === "/"
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function databaseSameSiteToChromeSameSite(
|
|
584
|
+
sameSite: number | bigint | null
|
|
585
|
+
): string | undefined {
|
|
586
|
+
const normalizedSameSite =
|
|
587
|
+
typeof sameSite === "bigint" ? Number(sameSite) : sameSite
|
|
588
|
+
|
|
589
|
+
switch (normalizedSameSite) {
|
|
590
|
+
case 0:
|
|
591
|
+
return "no_restriction"
|
|
592
|
+
case 1:
|
|
593
|
+
return "lax"
|
|
594
|
+
case 2:
|
|
595
|
+
return "strict"
|
|
596
|
+
default:
|
|
597
|
+
return undefined
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function queryChromeCookieRows(
|
|
602
|
+
databasePath: string,
|
|
603
|
+
hostKeys: string[]
|
|
604
|
+
): ChromeCookieDatabaseRow[] {
|
|
605
|
+
if (hostKeys.length === 0) {
|
|
606
|
+
return []
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const DatabaseSync = loadDatabaseConstructor()
|
|
610
|
+
const database = new DatabaseSync(databasePath, {
|
|
611
|
+
readOnly: true,
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
try {
|
|
615
|
+
const placeholders = hostKeys.map(() => "?").join(", ")
|
|
616
|
+
const statement = database.prepare(
|
|
617
|
+
[
|
|
618
|
+
"SELECT",
|
|
619
|
+
"host_key,",
|
|
620
|
+
"path,",
|
|
621
|
+
"is_secure,",
|
|
622
|
+
"expires_utc,",
|
|
623
|
+
"name,",
|
|
624
|
+
"value,",
|
|
625
|
+
"encrypted_value,",
|
|
626
|
+
"creation_utc,",
|
|
627
|
+
"is_httponly,",
|
|
628
|
+
"samesite",
|
|
629
|
+
"FROM cookies",
|
|
630
|
+
`WHERE host_key IN (${placeholders})`,
|
|
631
|
+
"ORDER BY LENGTH(path) DESC, creation_utc ASC",
|
|
632
|
+
].join(" ")
|
|
633
|
+
)
|
|
634
|
+
statement.setReadBigInts?.(true)
|
|
635
|
+
|
|
636
|
+
return statement.all(...hostKeys) as ChromeCookieDatabaseRow[]
|
|
637
|
+
} finally {
|
|
638
|
+
database.close()
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function copyOptionalSidecar(
|
|
643
|
+
sourcePath: string,
|
|
644
|
+
targetPath: string,
|
|
645
|
+
pathExists: (targetPath: string) => boolean,
|
|
646
|
+
copyFile: (sourcePath: string, targetPath: string) => void
|
|
647
|
+
) {
|
|
648
|
+
if (!pathExists(sourcePath)) {
|
|
649
|
+
return
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
copyFile(sourcePath, targetPath)
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
function runCommand(
|
|
656
|
+
dependencies: ChromeCookieReaderDependencies,
|
|
657
|
+
command: string,
|
|
658
|
+
args: string[]
|
|
659
|
+
): string {
|
|
660
|
+
const invoke =
|
|
661
|
+
dependencies.runCommand ??
|
|
662
|
+
((binary: string, binaryArgs: string[]) =>
|
|
663
|
+
execFileSync(binary, binaryArgs, {
|
|
664
|
+
encoding: "utf8",
|
|
665
|
+
windowsHide: true,
|
|
666
|
+
}))
|
|
667
|
+
|
|
668
|
+
return invoke(command, args)
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function toBoolean(value: number | bigint): boolean {
|
|
672
|
+
if (typeof value === "bigint") {
|
|
673
|
+
return value !== 0n
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
return value !== 0
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
function toBuffer(value: Uint8Array | null): Buffer {
|
|
680
|
+
return value ? Buffer.from(value) : Buffer.alloc(0)
|
|
681
|
+
}
|