opensoma 0.8.0 → 0.9.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/dist/package.json +1 -3
- package/dist/src/client.d.ts.map +1 -1
- package/dist/src/client.js +6 -6
- package/dist/src/client.js.map +1 -1
- package/dist/src/commands/auth.d.ts +1 -11
- package/dist/src/commands/auth.d.ts.map +1 -1
- package/dist/src/commands/auth.js +4 -81
- package/dist/src/commands/auth.js.map +1 -1
- package/dist/src/commands/helpers.d.ts +1 -5
- package/dist/src/commands/helpers.d.ts.map +1 -1
- package/dist/src/commands/helpers.js +4 -32
- package/dist/src/commands/helpers.js.map +1 -1
- package/dist/src/commands/team.js +1 -1
- package/dist/src/commands/team.js.map +1 -1
- package/dist/src/errors.d.ts +0 -4
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +1 -5
- package/dist/src/errors.js.map +1 -1
- package/package.json +1 -3
- package/src/client.test.ts +55 -4
- package/src/client.ts +7 -8
- package/src/commands/auth.test.ts +6 -98
- package/src/commands/auth.ts +2 -115
- package/src/commands/helpers.test.ts +5 -116
- package/src/commands/helpers.ts +3 -35
- package/src/commands/team.ts +1 -1
- package/src/errors.ts +1 -5
- package/dist/src/token-extractor.d.ts +0 -43
- package/dist/src/token-extractor.d.ts.map +0 -1
- package/dist/src/token-extractor.js +0 -302
- package/dist/src/token-extractor.js.map +0 -1
- package/src/token-extractor.test.ts +0 -220
- package/src/token-extractor.ts +0 -392
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import { Database } from 'bun:sqlite'
|
|
2
|
-
import { afterEach, describe, expect, it } from 'bun:test'
|
|
3
|
-
import { execSync } from 'node:child_process'
|
|
4
|
-
import { createCipheriv, pbkdf2Sync } from 'node:crypto'
|
|
5
|
-
import { mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
6
|
-
import { mkdtemp } from 'node:fs/promises'
|
|
7
|
-
import { tmpdir } from 'node:os'
|
|
8
|
-
import { dirname, join } from 'node:path'
|
|
9
|
-
|
|
10
|
-
import { BROWSERS, TokenExtractor } from './token-extractor'
|
|
11
|
-
|
|
12
|
-
const CHROMIUM_IV = Buffer.alloc(16, 0x20)
|
|
13
|
-
const CHROMIUM_SALT = 'saltysalt'
|
|
14
|
-
|
|
15
|
-
let createdDirs: string[] = []
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
for (const dir of createdDirs) {
|
|
19
|
-
rmSync(dir, { recursive: true, force: true })
|
|
20
|
-
}
|
|
21
|
-
createdDirs = []
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
describe('TokenExtractor', () => {
|
|
25
|
-
it('finds cookie databases for every supported browser on macOS', async () => {
|
|
26
|
-
const home = await makeTempDir()
|
|
27
|
-
|
|
28
|
-
for (const browser of BROWSERS) {
|
|
29
|
-
createCookieFile(join(home, 'Library', 'Application Support', browser.macPath, 'Default', 'Cookies'))
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const extractor = new TokenExtractor('darwin', home)
|
|
33
|
-
const paths = extractor.findCookieDatabases()
|
|
34
|
-
|
|
35
|
-
expect(paths).toHaveLength(BROWSERS.length)
|
|
36
|
-
for (const browser of BROWSERS) {
|
|
37
|
-
expect(paths).toContainEqual(join(home, 'Library', 'Application Support', browser.macPath, 'Default', 'Cookies'))
|
|
38
|
-
}
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('finds cookie databases for every supported browser on Linux', async () => {
|
|
42
|
-
const home = await makeTempDir()
|
|
43
|
-
|
|
44
|
-
for (const browser of BROWSERS) {
|
|
45
|
-
createCookieFile(join(home, '.config', browser.linuxPath, 'Default', 'Cookies'))
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const extractor = new TokenExtractor('linux', home)
|
|
49
|
-
const paths = extractor.findCookieDatabases()
|
|
50
|
-
|
|
51
|
-
expect(paths).toHaveLength(BROWSERS.length)
|
|
52
|
-
})
|
|
53
|
-
|
|
54
|
-
it('returns null when no cookie databases exist', async () => {
|
|
55
|
-
const extractor = new TokenExtractor('linux', await makeTempDir())
|
|
56
|
-
expect(await extractor.extract()).toBeNull()
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('extracts a plaintext cookie value', async () => {
|
|
60
|
-
const home = await makeTempDir()
|
|
61
|
-
const dbPath = join(home, '.config', 'google-chrome', 'Default', 'Cookies')
|
|
62
|
-
createCookieDbWithPlaintext(dbPath, 'my-session-id')
|
|
63
|
-
|
|
64
|
-
const extractor = new TokenExtractor('linux', home)
|
|
65
|
-
expect(await extractor.extract()).toEqual({ sessionCookie: 'my-session-id' })
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
it('finds cookie databases across numbered browser profiles', async () => {
|
|
69
|
-
const home = await makeTempDir()
|
|
70
|
-
createCookieFile(join(home, '.config', 'google-chrome', 'Default', 'Cookies'))
|
|
71
|
-
createCookieFile(join(home, '.config', 'google-chrome', 'Profile 1', 'Cookies'))
|
|
72
|
-
createCookieFile(join(home, '.config', 'google-chrome', 'Profile 2', 'Cookies'))
|
|
73
|
-
|
|
74
|
-
const extractor = new TokenExtractor('linux', home)
|
|
75
|
-
|
|
76
|
-
expect(extractor.findCookieDatabases()).toEqual([
|
|
77
|
-
join(home, '.config', 'google-chrome', 'Default', 'Cookies'),
|
|
78
|
-
join(home, '.config', 'google-chrome', 'Profile 1', 'Cookies'),
|
|
79
|
-
join(home, '.config', 'google-chrome', 'Profile 2', 'Cookies'),
|
|
80
|
-
])
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('keeps the newest unique cookie across profiles when collecting candidates', async () => {
|
|
84
|
-
const home = await makeTempDir()
|
|
85
|
-
createCookieDbWithPlaintext(join(home, '.config', 'google-chrome', 'Default', 'Cookies'), 'stale-session', 10)
|
|
86
|
-
createCookieDbWithPlaintext(join(home, '.config', 'google-chrome', 'Profile 1', 'Cookies'), 'valid-session', 20)
|
|
87
|
-
createCookieDbWithPlaintext(join(home, '.config', 'google-chrome', 'Profile 2', 'Cookies'), 'stale-session', 30)
|
|
88
|
-
|
|
89
|
-
const extractor = new TokenExtractor('linux', home)
|
|
90
|
-
|
|
91
|
-
await expect(extractor.extractCandidates()).resolves.toEqual([
|
|
92
|
-
{
|
|
93
|
-
browser: 'Chrome',
|
|
94
|
-
lastAccessUtc: 30,
|
|
95
|
-
profile: 'Profile 2',
|
|
96
|
-
sessionCookie: 'stale-session',
|
|
97
|
-
},
|
|
98
|
-
{
|
|
99
|
-
browser: 'Chrome',
|
|
100
|
-
lastAccessUtc: 20,
|
|
101
|
-
profile: 'Profile 1',
|
|
102
|
-
sessionCookie: 'valid-session',
|
|
103
|
-
},
|
|
104
|
-
])
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('extracts a plaintext cookie from the opensoma.dev host', async () => {
|
|
108
|
-
const home = await makeTempDir()
|
|
109
|
-
const dbPath = join(home, '.config', 'google-chrome', 'Default', 'Cookies')
|
|
110
|
-
createCookieDbWithPlaintext(dbPath, 'opensoma-dev-session', 0, 'opensoma.dev')
|
|
111
|
-
|
|
112
|
-
const extractor = new TokenExtractor('linux', home)
|
|
113
|
-
expect(await extractor.extract()).toEqual({ sessionCookie: 'opensoma-dev-session' })
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
it('decrypts an encrypted cookie from the opensoma.dev host on Linux', async () => {
|
|
117
|
-
const home = await makeTempDir()
|
|
118
|
-
const dbPath = join(home, '.config', 'google-chrome', 'Default', 'Cookies')
|
|
119
|
-
const encrypted = encryptLinuxCookie('opensoma-dev-encrypted')
|
|
120
|
-
createCookieDbWithEncrypted(dbPath, encrypted, 'opensoma.dev')
|
|
121
|
-
|
|
122
|
-
const extractor = new TokenExtractor('linux', home)
|
|
123
|
-
expect(await extractor.extract()).toEqual({ sessionCookie: 'opensoma-dev-encrypted' })
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
it('decrypts an encrypted cookie on Linux', async () => {
|
|
127
|
-
const home = await makeTempDir()
|
|
128
|
-
const dbPath = join(home, '.config', 'google-chrome', 'Default', 'Cookies')
|
|
129
|
-
const encrypted = encryptLinuxCookie('decrypted-session')
|
|
130
|
-
createCookieDbWithEncrypted(dbPath, encrypted)
|
|
131
|
-
|
|
132
|
-
const extractor = new TokenExtractor('linux', home)
|
|
133
|
-
expect(await extractor.extract()).toEqual({ sessionCookie: 'decrypted-session' })
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it('extracts a plaintext cookie when running under Node.js (compiled ESM)', async () => {
|
|
137
|
-
execSync('bun run build', { cwd: join(import.meta.dir, '..'), stdio: 'pipe' })
|
|
138
|
-
const home = await makeTempDir()
|
|
139
|
-
const dbPath = join(home, '.config', 'google-chrome', 'Default', 'Cookies')
|
|
140
|
-
createCookieDbWithPlaintext(dbPath, 'node-test-session')
|
|
141
|
-
|
|
142
|
-
const result = runNodeExtractor(home)
|
|
143
|
-
|
|
144
|
-
expect(JSON.parse(result.trim())).toEqual({ sessionCookie: 'node-test-session' })
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
it('extracts an encrypted cookie when running under Node.js (compiled ESM)', async () => {
|
|
148
|
-
execSync('bun run build', { cwd: join(import.meta.dir, '..'), stdio: 'pipe' })
|
|
149
|
-
const home = await makeTempDir()
|
|
150
|
-
const dbPath = join(home, '.config', 'google-chrome', 'Default', 'Cookies')
|
|
151
|
-
createCookieDbWithEncrypted(dbPath, encryptLinuxCookie('node-encrypted-session'))
|
|
152
|
-
|
|
153
|
-
const result = runNodeExtractor(home)
|
|
154
|
-
|
|
155
|
-
expect(JSON.parse(result.trim())).toEqual({ sessionCookie: 'node-encrypted-session' })
|
|
156
|
-
})
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
async function makeTempDir(): Promise<string> {
|
|
160
|
-
const dir = await mkdtemp(join(tmpdir(), 'opensoma-token-extractor-'))
|
|
161
|
-
createdDirs.push(dir)
|
|
162
|
-
return dir
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function createCookieFile(filePath: string): void {
|
|
166
|
-
mkdirSync(dirname(filePath), { recursive: true })
|
|
167
|
-
writeFileSync(filePath, '')
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
function createCookieDbWithPlaintext(
|
|
171
|
-
filePath: string,
|
|
172
|
-
value: string,
|
|
173
|
-
lastAccessUtc = 0,
|
|
174
|
-
hostKey = 'swmaestro.ai',
|
|
175
|
-
): void {
|
|
176
|
-
mkdirSync(dirname(filePath), { recursive: true })
|
|
177
|
-
const db = new Database(filePath)
|
|
178
|
-
db.run(
|
|
179
|
-
'CREATE TABLE cookies (host_key TEXT, name TEXT, value TEXT, encrypted_value BLOB, creation_utc INTEGER, expires_utc INTEGER, is_httponly INTEGER, has_expires INTEGER, is_persistent INTEGER, priority INTEGER, samesite INTEGER, source_scheme INTEGER, is_secure INTEGER, path TEXT, last_access_utc INTEGER, last_update_utc INTEGER, source_port INTEGER, source_type INTEGER)',
|
|
180
|
-
)
|
|
181
|
-
db.run(
|
|
182
|
-
"INSERT INTO cookies (host_key, name, value, encrypted_value, last_access_utc) VALUES (?, 'JSESSIONID', ?, '', ?)",
|
|
183
|
-
[hostKey, value, lastAccessUtc],
|
|
184
|
-
)
|
|
185
|
-
db.close()
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function createCookieDbWithEncrypted(filePath: string, encrypted: Buffer, hostKey = 'swmaestro.ai'): void {
|
|
189
|
-
mkdirSync(dirname(filePath), { recursive: true })
|
|
190
|
-
const db = new Database(filePath)
|
|
191
|
-
db.run(
|
|
192
|
-
'CREATE TABLE cookies (host_key TEXT, name TEXT, value TEXT, encrypted_value BLOB, creation_utc INTEGER, expires_utc INTEGER, is_httponly INTEGER, has_expires INTEGER, is_persistent INTEGER, priority INTEGER, samesite INTEGER, source_scheme INTEGER, is_secure INTEGER, path TEXT, last_access_utc INTEGER, last_update_utc INTEGER, source_port INTEGER, source_type INTEGER)',
|
|
193
|
-
)
|
|
194
|
-
db.run("INSERT INTO cookies (host_key, name, value, encrypted_value) VALUES (?, 'JSESSIONID', '', ?)", [
|
|
195
|
-
hostKey,
|
|
196
|
-
encrypted,
|
|
197
|
-
])
|
|
198
|
-
db.close()
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function runNodeExtractor(home: string): string {
|
|
202
|
-
const projectRoot = join(import.meta.dir, '..')
|
|
203
|
-
const scriptPath = join(home, 'test-node-extract.mjs')
|
|
204
|
-
writeFileSync(
|
|
205
|
-
scriptPath,
|
|
206
|
-
[
|
|
207
|
-
`import { TokenExtractor } from ${JSON.stringify(join(projectRoot, 'dist', 'src', 'token-extractor.js'))};`,
|
|
208
|
-
`const ext = new TokenExtractor('linux', ${JSON.stringify(home)});`,
|
|
209
|
-
`const result = await ext.extract();`,
|
|
210
|
-
`console.log(JSON.stringify(result));`,
|
|
211
|
-
].join('\n'),
|
|
212
|
-
)
|
|
213
|
-
return execSync(`node ${scriptPath}`, { encoding: 'utf-8', timeout: 15_000 })
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
function encryptLinuxCookie(value: string): Buffer {
|
|
217
|
-
const key = pbkdf2Sync('peanuts', CHROMIUM_SALT, 1, 16, 'sha1')
|
|
218
|
-
const cipher = createCipheriv('aes-128-cbc', key, CHROMIUM_IV)
|
|
219
|
-
return Buffer.concat([Buffer.from('v10'), cipher.update(value, 'utf8'), cipher.final()])
|
|
220
|
-
}
|
package/src/token-extractor.ts
DELETED
|
@@ -1,392 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'node:child_process'
|
|
2
|
-
import { createDecipheriv, pbkdf2Sync } from 'node:crypto'
|
|
3
|
-
import { copyFileSync, existsSync, mkdtempSync, readdirSync, rmSync } from 'node:fs'
|
|
4
|
-
import { createRequire } from 'node:module'
|
|
5
|
-
import { homedir, tmpdir } from 'node:os'
|
|
6
|
-
import { basename, dirname, join } from 'node:path'
|
|
7
|
-
|
|
8
|
-
const require = createRequire(import.meta.url)
|
|
9
|
-
|
|
10
|
-
const COOKIE_QUERY =
|
|
11
|
-
"SELECT encrypted_value, last_access_utc, value FROM cookies WHERE (host_key LIKE '%swmaestro.ai' OR host_key LIKE '%opensoma.dev') AND name = 'JSESSIONID' ORDER BY last_access_utc DESC LIMIT 1"
|
|
12
|
-
const CHROMIUM_SALT = 'saltysalt'
|
|
13
|
-
const CHROMIUM_IV = Buffer.alloc(16, 0x20)
|
|
14
|
-
const PROFILE_DIR_PATTERN = /^Profile\s+\d+$/
|
|
15
|
-
|
|
16
|
-
type BrowserConfig = {
|
|
17
|
-
name: string
|
|
18
|
-
macPath: string
|
|
19
|
-
linuxPath: string
|
|
20
|
-
keychainService: string
|
|
21
|
-
keychainAccount: string
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
type CookieRow = {
|
|
25
|
-
encrypted_value: ArrayBuffer | Uint8Array | Buffer | null
|
|
26
|
-
last_access_utc?: number | bigint | null
|
|
27
|
-
value: ArrayBuffer | Uint8Array | Buffer | string | null
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface ExtractedSessionCandidate {
|
|
31
|
-
browser: string
|
|
32
|
-
lastAccessUtc: number
|
|
33
|
-
profile: string
|
|
34
|
-
sessionCookie: string
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export const BROWSERS: BrowserConfig[] = [
|
|
38
|
-
{
|
|
39
|
-
name: 'Chrome',
|
|
40
|
-
macPath: 'Google Chrome',
|
|
41
|
-
linuxPath: 'google-chrome',
|
|
42
|
-
keychainService: 'Chrome Safe Storage',
|
|
43
|
-
keychainAccount: 'Chrome',
|
|
44
|
-
},
|
|
45
|
-
{
|
|
46
|
-
name: 'Edge',
|
|
47
|
-
macPath: 'Microsoft Edge',
|
|
48
|
-
linuxPath: 'microsoft-edge',
|
|
49
|
-
keychainService: 'Microsoft Edge Safe Storage',
|
|
50
|
-
keychainAccount: 'Microsoft Edge',
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
name: 'Brave',
|
|
54
|
-
macPath: 'BraveSoftware/Brave-Browser',
|
|
55
|
-
linuxPath: 'BraveSoftware/Brave-Browser',
|
|
56
|
-
keychainService: 'Brave Safe Storage',
|
|
57
|
-
keychainAccount: 'Brave',
|
|
58
|
-
},
|
|
59
|
-
{
|
|
60
|
-
name: 'Arc',
|
|
61
|
-
macPath: join('Arc', 'User Data'),
|
|
62
|
-
linuxPath: join('Arc', 'User Data'),
|
|
63
|
-
keychainService: 'Arc Safe Storage',
|
|
64
|
-
keychainAccount: 'Arc',
|
|
65
|
-
},
|
|
66
|
-
{
|
|
67
|
-
name: 'Vivaldi',
|
|
68
|
-
macPath: 'Vivaldi',
|
|
69
|
-
linuxPath: 'Vivaldi',
|
|
70
|
-
keychainService: 'Vivaldi Safe Storage',
|
|
71
|
-
keychainAccount: 'Vivaldi',
|
|
72
|
-
},
|
|
73
|
-
{
|
|
74
|
-
name: 'Chromium',
|
|
75
|
-
macPath: 'Chromium',
|
|
76
|
-
linuxPath: 'Chromium',
|
|
77
|
-
keychainService: 'Chromium Safe Storage',
|
|
78
|
-
keychainAccount: 'Chromium',
|
|
79
|
-
},
|
|
80
|
-
]
|
|
81
|
-
|
|
82
|
-
function queryCookieDb(dbPath: string): CookieRow | undefined {
|
|
83
|
-
if (typeof globalThis.Bun !== 'undefined') {
|
|
84
|
-
const { Database } = require('bun:sqlite')
|
|
85
|
-
const db = new Database(dbPath, { readonly: true })
|
|
86
|
-
try {
|
|
87
|
-
const row = db.query(COOKIE_QUERY).get() as CookieRow | undefined
|
|
88
|
-
return row ?? undefined
|
|
89
|
-
} finally {
|
|
90
|
-
db.close()
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
try {
|
|
95
|
-
const Database = require('better-sqlite3') as new (
|
|
96
|
-
path: string,
|
|
97
|
-
options?: { readonly?: boolean },
|
|
98
|
-
) => {
|
|
99
|
-
close: () => void
|
|
100
|
-
prepare: (query: string) => { get: () => CookieRow | undefined }
|
|
101
|
-
}
|
|
102
|
-
const db = new Database(dbPath, { readonly: true })
|
|
103
|
-
try {
|
|
104
|
-
return db.prepare(COOKIE_QUERY).get() ?? undefined
|
|
105
|
-
} finally {
|
|
106
|
-
db.close()
|
|
107
|
-
}
|
|
108
|
-
} catch {
|
|
109
|
-
const { DatabaseSync } = require('node:sqlite') as {
|
|
110
|
-
DatabaseSync: new (
|
|
111
|
-
path: string,
|
|
112
|
-
options?: { readonly?: boolean },
|
|
113
|
-
) => {
|
|
114
|
-
close: () => void
|
|
115
|
-
prepare: (query: string) => { get: () => CookieRow | undefined }
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
const db = new DatabaseSync(dbPath, { readonly: true })
|
|
119
|
-
try {
|
|
120
|
-
return db.prepare(COOKIE_QUERY).get() ?? undefined
|
|
121
|
-
} finally {
|
|
122
|
-
db.close()
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export type TokenExtractorOptions = {
|
|
128
|
-
platform?: NodeJS.Platform
|
|
129
|
-
homeDirectory?: string
|
|
130
|
-
debug?: boolean
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
export class TokenExtractor {
|
|
134
|
-
private readonly platform: NodeJS.Platform
|
|
135
|
-
private readonly homeDirectory: string
|
|
136
|
-
private readonly debugEnabled: boolean
|
|
137
|
-
|
|
138
|
-
constructor(options?: TokenExtractorOptions)
|
|
139
|
-
constructor(platform?: NodeJS.Platform, homeDirectory?: string, debug?: boolean)
|
|
140
|
-
constructor(platformOrOptions?: NodeJS.Platform | TokenExtractorOptions, homeDirectory?: string, debug?: boolean) {
|
|
141
|
-
if (typeof platformOrOptions === 'object' && platformOrOptions !== null) {
|
|
142
|
-
this.platform = platformOrOptions.platform ?? process.platform
|
|
143
|
-
this.homeDirectory = platformOrOptions.homeDirectory ?? homedir()
|
|
144
|
-
this.debugEnabled = platformOrOptions.debug ?? false
|
|
145
|
-
} else {
|
|
146
|
-
this.platform = platformOrOptions ?? process.platform
|
|
147
|
-
this.homeDirectory = homeDirectory ?? homedir()
|
|
148
|
-
this.debugEnabled = debug ?? false
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
private log(...args: unknown[]): void {
|
|
153
|
-
if (!this.debugEnabled) return
|
|
154
|
-
const message = args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ')
|
|
155
|
-
process.stderr.write(`[extract] ${message}\n`)
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
async extract(): Promise<{ sessionCookie: string } | null> {
|
|
159
|
-
const candidates = await this.extractCandidates()
|
|
160
|
-
const firstCandidate = candidates[0]
|
|
161
|
-
|
|
162
|
-
if (!firstCandidate) {
|
|
163
|
-
return null
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return { sessionCookie: firstCandidate.sessionCookie }
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
async extractCandidates(): Promise<ExtractedSessionCandidate[]> {
|
|
170
|
-
const candidates = new Map<string, ExtractedSessionCandidate>()
|
|
171
|
-
const cookieDatabases = this.findCookieDatabases()
|
|
172
|
-
this.log(`Found ${cookieDatabases.length} cookie database(s)`)
|
|
173
|
-
|
|
174
|
-
for (const databasePath of cookieDatabases) {
|
|
175
|
-
const browser = this.getBrowserByPath(databasePath)
|
|
176
|
-
if (!browser) {
|
|
177
|
-
this.log(`Skipping ${databasePath} (no matching browser config)`)
|
|
178
|
-
continue
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const profile = basename(dirname(databasePath))
|
|
182
|
-
this.log(`Processing ${browser.name} / ${profile}`)
|
|
183
|
-
|
|
184
|
-
const tempDirectory = mkdtempSync(join(tmpdir(), 'opensoma-cookie-db-'))
|
|
185
|
-
const tempDatabasePath = join(tempDirectory, 'Cookies')
|
|
186
|
-
|
|
187
|
-
try {
|
|
188
|
-
copySqliteDatabase(databasePath, tempDatabasePath)
|
|
189
|
-
this.log(` Copied DB to ${tempDatabasePath}`)
|
|
190
|
-
|
|
191
|
-
const row = queryCookieDb(tempDatabasePath)
|
|
192
|
-
if (!row) {
|
|
193
|
-
this.log(' No JSESSIONID cookie found')
|
|
194
|
-
continue
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const plaintextValue = normalizeCookieText(row.value)
|
|
198
|
-
if (plaintextValue) {
|
|
199
|
-
this.log(` Found plaintext cookie (${plaintextValue.length} chars)`)
|
|
200
|
-
this.addCandidate(candidates, {
|
|
201
|
-
browser: browser.name,
|
|
202
|
-
lastAccessUtc: this.normalizeLastAccessUtc(row.last_access_utc),
|
|
203
|
-
profile,
|
|
204
|
-
sessionCookie: plaintextValue,
|
|
205
|
-
})
|
|
206
|
-
continue
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const encryptedValue = normalizeCookieBytes(row.encrypted_value)
|
|
210
|
-
if (!encryptedValue || encryptedValue.length === 0) {
|
|
211
|
-
this.log(' No plaintext or encrypted value')
|
|
212
|
-
continue
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
this.log(` Decrypting encrypted cookie (${encryptedValue.length} bytes)...`)
|
|
216
|
-
const decryptedValue = await this.decryptCookie(encryptedValue, browser.name)
|
|
217
|
-
if (decryptedValue) {
|
|
218
|
-
this.log(` Decrypted successfully (${decryptedValue.length} chars)`)
|
|
219
|
-
this.addCandidate(candidates, {
|
|
220
|
-
browser: browser.name,
|
|
221
|
-
lastAccessUtc: this.normalizeLastAccessUtc(row.last_access_utc),
|
|
222
|
-
profile,
|
|
223
|
-
sessionCookie: decryptedValue,
|
|
224
|
-
})
|
|
225
|
-
} else {
|
|
226
|
-
this.log(' Decryption returned empty value')
|
|
227
|
-
}
|
|
228
|
-
} catch (error) {
|
|
229
|
-
this.log(` Error: ${error instanceof Error ? error.message : String(error)}`)
|
|
230
|
-
} finally {
|
|
231
|
-
rmSync(tempDirectory, { recursive: true, force: true })
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
this.log(`Total unique candidates: ${candidates.size}`)
|
|
236
|
-
return [...candidates.values()].sort((left, right) => right.lastAccessUtc - left.lastAccessUtc)
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
findCookieDatabases(): string[] {
|
|
240
|
-
const results = BROWSERS.flatMap((browser) => this.findBrowserCookieDatabases(browser))
|
|
241
|
-
if (results.length === 0) {
|
|
242
|
-
this.log('No cookie databases found. Searched browsers:', BROWSERS.map((b) => b.name).join(', '))
|
|
243
|
-
}
|
|
244
|
-
return results
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
private async decryptCookie(encryptedValue: Buffer, browserName: string): Promise<string> {
|
|
248
|
-
if (encryptedValue.length === 0) {
|
|
249
|
-
return ''
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (this.platform === 'linux') {
|
|
253
|
-
this.log(' Using Linux PBKDF2 decryption')
|
|
254
|
-
return this.decryptChromiumValue(encryptedValue, pbkdf2Sync('peanuts', CHROMIUM_SALT, 1, 16, 'sha1'))
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
if (this.platform === 'darwin') {
|
|
258
|
-
this.log(` Fetching macOS Keychain key for ${browserName}...`)
|
|
259
|
-
const key = await this.getMacOSEncryptionKey(browserName)
|
|
260
|
-
this.log(' Keychain key obtained')
|
|
261
|
-
return this.decryptChromiumValue(encryptedValue, key)
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
this.log(` Unsupported platform for decryption: ${this.platform}`)
|
|
265
|
-
return ''
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
private async getMacOSEncryptionKey(browserName: string): Promise<Buffer> {
|
|
269
|
-
const browser = BROWSERS.find((entry) => entry.name === browserName)
|
|
270
|
-
if (!browser) {
|
|
271
|
-
throw new Error(`Unsupported browser: ${browserName}`)
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const command = `security find-generic-password -s ${JSON.stringify(browser.keychainService)} -a ${JSON.stringify(browser.keychainAccount)} -w`
|
|
275
|
-
this.log(` Keychain command: ${command}`)
|
|
276
|
-
|
|
277
|
-
const password = execSync(command, { encoding: 'utf8' }).trimEnd()
|
|
278
|
-
return pbkdf2Sync(password, CHROMIUM_SALT, 1003, 16, 'sha1')
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
private decryptChromiumValue(encryptedValue: Buffer, key: Buffer): string {
|
|
282
|
-
const encryptedPayload =
|
|
283
|
-
encryptedValue.subarray(0, 3).toString('utf8') === 'v10' ? encryptedValue.subarray(3) : encryptedValue
|
|
284
|
-
const decipher = createDecipheriv('aes-128-cbc', key, CHROMIUM_IV)
|
|
285
|
-
decipher.setAutoPadding(true)
|
|
286
|
-
const decrypted = Buffer.concat([decipher.update(encryptedPayload), decipher.final()])
|
|
287
|
-
|
|
288
|
-
// Chromium v130+ prepends a 32-byte integrity hash before the actual cookie value
|
|
289
|
-
if (decrypted.length > 32) {
|
|
290
|
-
const hasNonPrintablePrefix = decrypted.subarray(0, 32).some((b) => b < 0x20 || b > 0x7e)
|
|
291
|
-
if (hasNonPrintablePrefix) {
|
|
292
|
-
return decrypted.subarray(32).toString('utf8')
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return decrypted.toString('utf8')
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
private getBrowserByPath(databasePath: string): BrowserConfig | undefined {
|
|
300
|
-
return BROWSERS.find(
|
|
301
|
-
(browser) => databasePath.includes(`${browser.macPath}/`) || databasePath.includes(`${browser.linuxPath}/`),
|
|
302
|
-
)
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
private addCandidate(candidates: Map<string, ExtractedSessionCandidate>, candidate: ExtractedSessionCandidate): void {
|
|
306
|
-
const existing = candidates.get(candidate.sessionCookie)
|
|
307
|
-
if (!existing || existing.lastAccessUtc < candidate.lastAccessUtc) {
|
|
308
|
-
candidates.set(candidate.sessionCookie, candidate)
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
private findBrowserCookieDatabases(browser: BrowserConfig): string[] {
|
|
313
|
-
const browserRoot = this.getBrowserRoot(browser)
|
|
314
|
-
if (!browserRoot || !existsSync(browserRoot)) {
|
|
315
|
-
this.log(`${browser.name}: not found at ${browserRoot ?? '(unsupported platform)'}`)
|
|
316
|
-
return []
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const profiles = readdirSync(browserRoot, { withFileTypes: true })
|
|
320
|
-
.filter((entry) => entry.isDirectory() && this.isSupportedProfileDirectory(entry.name))
|
|
321
|
-
.sort((left, right) => left.name.localeCompare(right.name))
|
|
322
|
-
|
|
323
|
-
const databases = profiles
|
|
324
|
-
.map((entry) => join(browserRoot, entry.name, 'Cookies'))
|
|
325
|
-
.filter((databasePath) => existsSync(databasePath))
|
|
326
|
-
|
|
327
|
-
this.log(`${browser.name}: ${profiles.length} profile(s), ${databases.length} cookie DB(s)`)
|
|
328
|
-
return databases
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
private getBrowserRoot(browser: BrowserConfig): string | null {
|
|
332
|
-
if (this.platform === 'darwin') {
|
|
333
|
-
return join(this.homeDirectory, 'Library', 'Application Support', browser.macPath)
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (this.platform === 'linux') {
|
|
337
|
-
return join(this.homeDirectory, '.config', browser.linuxPath)
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
return null
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
private isSupportedProfileDirectory(profileName: string): boolean {
|
|
344
|
-
return profileName === 'Default' || PROFILE_DIR_PATTERN.test(profileName)
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
private normalizeLastAccessUtc(lastAccessUtc: number | bigint | null | undefined): number {
|
|
348
|
-
if (typeof lastAccessUtc === 'bigint') {
|
|
349
|
-
return Number(lastAccessUtc)
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return typeof lastAccessUtc === 'number' ? lastAccessUtc : 0
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function copySqliteDatabase(sourcePath: string, targetPath: string): void {
|
|
357
|
-
copyFileSync(sourcePath, targetPath)
|
|
358
|
-
|
|
359
|
-
for (const suffix of ['-wal', '-shm']) {
|
|
360
|
-
const sidecarSourcePath = `${sourcePath}${suffix}`
|
|
361
|
-
if (!existsSync(sidecarSourcePath)) {
|
|
362
|
-
continue
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
copyFileSync(sidecarSourcePath, `${targetPath}${suffix}`)
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function normalizeCookieBytes(value: ArrayBuffer | Uint8Array | Buffer | null): Buffer | null {
|
|
370
|
-
if (!value) {
|
|
371
|
-
return null
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (Buffer.isBuffer(value)) {
|
|
375
|
-
return value
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
if (value instanceof Uint8Array) {
|
|
379
|
-
return Buffer.from(value)
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
return Buffer.from(value)
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
function normalizeCookieText(value: ArrayBuffer | Uint8Array | Buffer | string | null): string {
|
|
386
|
-
if (typeof value === 'string') {
|
|
387
|
-
return value.trim()
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
const bytes = normalizeCookieBytes(value)
|
|
391
|
-
return bytes ? bytes.toString('utf8').trim() : ''
|
|
392
|
-
}
|