-
Notifications
You must be signed in to change notification settings - Fork 314
Expand file tree
/
Copy pathcookie.ts
More file actions
349 lines (310 loc) · 10.5 KB
/
Copy pathcookie.ts
File metadata and controls
349 lines (310 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
/**
* Claude Desktop Cookie decryption module
*
* macOS Chromium standard encryption scheme:
* 1. Read Safe Storage password from Keychain
* 2. PBKDF2(password, "saltysalt", 1003, 16, SHA1) → AES-128-CBC key
* 3. encrypted_value format: v10 (3 bytes) + ciphertext, IV = 16 bytes of 0x20
*
* Windows Chromium encryption scheme:
* 1. Read master key from Local State (DPAPI-protected)
* 2. AES-256-GCM decrypt with v10 + nonce(12B) + ciphertext + tag(16B)
*/
import * as crypto from 'node:crypto'
import * as fs from 'node:fs'
import * as path from 'node:path'
import * as os from 'node:os'
import { execFileSync } from 'node:child_process'
import { openReadonlyDb, queryAll } from './sqlite.js'
export interface DesktopCookies {
sessionKey: string
organizationId: string
deviceId: string
cfClearance: string
cfBm: string
}
// ---- macOS-specific functions ----
/**
* Get Claude Safe Storage password from macOS Keychain
*/
function getKeychainPassword(): string {
try {
const result = execFileSync('security', [
'find-generic-password', '-w',
'-s', 'Claude Safe Storage',
'-a', 'Claude Key',
], { timeout: 10000, stdio: ['pipe', 'pipe', 'pipe'] })
return result.toString().trim()
} catch (err: any) {
throw new Error(
`Failed to read Keychain: ${err?.message || 'unknown error'}\n` +
'Please verify: 1) macOS system 2) Claude Desktop is installed and has been logged in'
)
}
}
/**
* Derive AES-128-CBC key
*/
function deriveKey(password: string): Buffer {
return crypto.pbkdf2Sync(password, 'saltysalt', 1003, 16, 'sha1')
}
/**
* Decrypt a single Chromium Cookie value (macOS: AES-128-CBC)
*/
function decryptValue(encryptedValue: Buffer, key: Buffer): string {
if (encryptedValue.length < 4) return ''
const prefix = encryptedValue.subarray(0, 3).toString('utf-8')
if (prefix !== 'v10') return ''
const data = encryptedValue.subarray(3)
const iv = Buffer.alloc(16, 0x20)
try {
const decipher = crypto.createDecipheriv('aes-128-cbc', key, iv)
const decrypted = Buffer.concat([decipher.update(data), decipher.final()])
// Chromium has a 32-byte internal prefix before cookie value, skip it
if (decrypted.length <= 32) return ''
return decrypted.subarray(32).toString('utf-8')
} catch {
return ''
}
}
/**
* Find Claude Desktop Cookies DB path (macOS)
*/
function findCookiesDbPathDarwin(): string | null {
const candidates = [
path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'Cookies'),
path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'Default', 'Cookies'),
]
for (const p of candidates) {
if (fs.existsSync(p)) return p
}
return null
}
/**
* Read device ID from ant-did file (macOS)
*/
function findDeviceIdDarwin(): string {
const candidates = [
path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'ant-did'),
path.join(os.homedir(), 'Library', 'Application Support', 'Claude', 'Default', 'ant-did'),
]
for (const p of candidates) {
try {
if (fs.existsSync(p)) return fs.readFileSync(p, 'utf-8').trim()
} catch { /* continue */ }
}
return ''
}
/**
* Extract cookies on macOS (Keychain + AES-128-CBC)
*/
async function extractCookiesDarwin(): Promise<DesktopCookies> {
const dbPath = findCookiesDbPathDarwin()
if (!dbPath) {
throw new Error(
'Claude Desktop Cookies database not found\n' +
'Please verify Claude Desktop is installed and has been logged in at least once'
)
}
const password = getKeychainPassword()
const key = deriveKey(password)
let db: any
try {
db = await openReadonlyDb(dbPath)
} catch (err: any) {
throw new Error(`Failed to open Cookies DB: ${err?.message}`)
}
const result: DesktopCookies = {
sessionKey: '',
organizationId: '',
deviceId: '',
cfClearance: '',
cfBm: '',
}
try {
const rows = queryAll(db,
`SELECT name, encrypted_value, host_key FROM cookies WHERE host_key LIKE '%claude.ai%'`,
)
for (const row of rows) {
const encBuf = row.encrypted_value instanceof Uint8Array
? Buffer.from(row.encrypted_value)
: Buffer.alloc(0)
const value = decryptValue(encBuf, key)
if (!value) continue
switch (row.name) {
case 'sessionKey': result.sessionKey = value; break
case 'lastActiveOrg': result.organizationId = value; break
case 'anthropic-device-id': result.deviceId = value; break
case 'cf_clearance': result.cfClearance = value; break
case '__cf_bm': result.cfBm = value; break
}
}
} finally {
db.close()
}
if (!result.sessionKey) {
throw new Error('sessionKey not found. Please verify Claude Desktop is logged in (able to chat normally).')
}
if (!result.organizationId) {
throw new Error('lastActiveOrg not found. Please verify Claude Desktop is logged in.')
}
if (!result.deviceId) {
result.deviceId = findDeviceIdDarwin()
}
return result
}
// ---- Windows-specific functions ----
/**
* Find Claude Desktop Cookies DB path (Windows)
* %APPDATA%\Claude\Cookies or %APPDATA%\Claude\Default\Cookies
*/
/**
* Find Claude Desktop data root directory on Windows.
* Checks both traditional Electron installer path (%APPDATA%\Claude)
* and Windows Store sandboxed path (%LOCALAPPDATA%\Packages\Claude_*\LocalCache\Roaming\Claude).
* Returns the most recently modified one if both exist.
*/
function findClaudeDataRootWindows(): string | null {
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming')
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local')
const candidates: string[] = [
path.join(appData, 'Claude'),
]
// Windows Store app: %LOCALAPPDATA%\Packages\Claude_{publisherHash}\LocalCache\Roaming\Claude
const packagesDir = path.join(localAppData, 'Packages')
try {
for (const entry of fs.readdirSync(packagesDir)) {
if (entry.startsWith('Claude_')) {
candidates.push(path.join(packagesDir, entry, 'LocalCache', 'Roaming', 'Claude'))
}
}
} catch { /* Packages dir not accessible */ }
// Pick the one with the most recently modified Cookies file
let best: string | null = null
let bestMtime = 0
for (const dir of candidates) {
for (const cookiePath of ['Network/Cookies', 'Cookies', 'Default/Cookies']) {
const full = path.join(dir, cookiePath)
try {
const mtime = fs.statSync(full).mtimeMs
if (mtime > bestMtime) { bestMtime = mtime; best = dir }
} catch { /* not found */ }
}
}
return best
}
function findCookiesDbPathWindows(): string | null {
const root = findClaudeDataRootWindows()
if (!root) return null
for (const sub of ['Network/Cookies', 'Cookies', 'Default/Cookies']) {
const p = path.join(root, sub)
if (fs.existsSync(p)) return p
}
return null
}
/**
* Find Local State file for Claude Desktop (Windows)
*/
function findLocalStatePath(): string | null {
const root = findClaudeDataRootWindows()
if (root) {
const p = path.join(root, 'Local State')
if (fs.existsSync(p)) return p
}
// Fallback: check %LOCALAPPDATA%\Claude directly
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local')
const fallback = path.join(localAppData, 'Claude', 'Local State')
if (fs.existsSync(fallback)) return fallback
return null
}
/**
* Read device ID from ant-did file (Windows)
*/
function findDeviceIdWindows(): string {
const root = findClaudeDataRootWindows()
const candidates = root
? [path.join(root, 'ant-did'), path.join(root, 'Default', 'ant-did')]
: []
for (const p of candidates) {
try {
if (fs.existsSync(p)) return fs.readFileSync(p, 'utf-8').trim()
} catch { /* continue */ }
}
return ''
}
/**
* Extract cookies on Windows (DPAPI + AES-256-GCM)
*/
async function extractCookiesWindows(): Promise<DesktopCookies> {
const { getChromiumKeyFromLocalState, decryptChromiumValueWindows } = await import('./dpapi.js')
const dbPath = findCookiesDbPathWindows()
if (!dbPath) {
throw new Error(
'Claude Desktop Cookies database not found\n' +
'Please verify Claude Desktop is installed and has been logged in at least once'
)
}
const localStatePath = findLocalStatePath()
if (!localStatePath) {
throw new Error('Claude Desktop Local State file not found')
}
const key = getChromiumKeyFromLocalState(localStatePath)
let db: any
try {
db = await openReadonlyDb(dbPath)
} catch (err: any) {
throw new Error(`Failed to open Cookies DB: ${err?.message}`)
}
const result: DesktopCookies = {
sessionKey: '',
organizationId: '',
deviceId: '',
cfClearance: '',
cfBm: '',
}
try {
const rows = queryAll(db,
`SELECT name, encrypted_value, host_key FROM cookies WHERE host_key LIKE '%claude.ai%'`,
)
for (const row of rows) {
const encBuf = row.encrypted_value instanceof Uint8Array
? Buffer.from(row.encrypted_value)
: Buffer.alloc(0)
const value = decryptChromiumValueWindows(encBuf, key)
if (!value) continue
switch (row.name) {
case 'sessionKey': result.sessionKey = value; break
case 'lastActiveOrg': result.organizationId = value; break
case 'anthropic-device-id': result.deviceId = value; break
case 'cf_clearance': result.cfClearance = value; break
case '__cf_bm': result.cfBm = value; break
}
}
} finally {
db.close()
}
if (!result.sessionKey) {
throw new Error('sessionKey not found. Please verify Claude Desktop is logged in (able to chat normally).')
}
if (!result.organizationId) {
throw new Error('lastActiveOrg not found. Please verify Claude Desktop is logged in.')
}
if (!result.deviceId) {
result.deviceId = findDeviceIdWindows()
}
return result
}
// ---- Public API ----
/**
* Extract and decrypt all required cookies from Claude Desktop Cookies DB.
* Dispatches to platform-specific implementation.
*/
export async function extractCookies(): Promise<DesktopCookies> {
if (process.platform === 'win32') {
return extractCookiesWindows()
}
if (process.platform === 'darwin') {
return extractCookiesDarwin()
}
throw new Error(`Unsupported platform: ${process.platform}`)
}