agent-messenger 2.0.0 → 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/.claude-plugin/marketplace.json +14 -1
- package/.claude-plugin/plugin.json +4 -2
- package/.env.template +35 -17
- package/README.md +37 -33
- package/bun.lock +6 -6
- package/dist/package.json +11 -3
- package/dist/src/cli.d.ts.map +1 -1
- package/dist/src/cli.js +3 -0
- package/dist/src/cli.js.map +1 -1
- package/dist/src/platforms/channeltalk/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/channeltalk/commands/auth.js +35 -28
- package/dist/src/platforms/channeltalk/commands/auth.js.map +1 -1
- package/dist/src/platforms/channeltalk/ensure-auth.js +6 -6
- package/dist/src/platforms/channeltalk/ensure-auth.js.map +1 -1
- package/dist/src/platforms/channeltalk/token-extractor.d.ts +23 -1
- package/dist/src/platforms/channeltalk/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/channeltalk/token-extractor.js +299 -29
- package/dist/src/platforms/channeltalk/token-extractor.js.map +1 -1
- package/dist/src/platforms/discord/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/discord/commands/auth.js +57 -49
- package/dist/src/platforms/discord/commands/auth.js.map +1 -1
- package/dist/src/platforms/discord/ensure-auth.js +3 -3
- package/dist/src/platforms/discord/ensure-auth.js.map +1 -1
- package/dist/src/platforms/discord/token-extractor.d.ts +6 -1
- package/dist/src/platforms/discord/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/discord/token-extractor.js +167 -14
- package/dist/src/platforms/discord/token-extractor.js.map +1 -1
- package/dist/src/platforms/instagram/client.d.ts +2 -0
- package/dist/src/platforms/instagram/client.d.ts.map +1 -1
- package/dist/src/platforms/instagram/client.js +2 -2
- package/dist/src/platforms/instagram/client.js.map +1 -1
- package/dist/src/platforms/instagram/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/instagram/commands/auth.js +107 -14
- package/dist/src/platforms/instagram/commands/auth.js.map +1 -1
- package/dist/src/platforms/instagram/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/instagram/ensure-auth.js +57 -11
- package/dist/src/platforms/instagram/ensure-auth.js.map +1 -1
- package/dist/src/platforms/instagram/index.d.ts +1 -0
- package/dist/src/platforms/instagram/index.d.ts.map +1 -1
- package/dist/src/platforms/instagram/index.js +1 -0
- package/dist/src/platforms/instagram/index.js.map +1 -1
- package/dist/src/platforms/instagram/token-extractor.d.ts +44 -0
- package/dist/src/platforms/instagram/token-extractor.d.ts.map +1 -0
- package/dist/src/platforms/instagram/token-extractor.js +407 -0
- package/dist/src/platforms/instagram/token-extractor.js.map +1 -0
- package/dist/src/platforms/kakaotalk/client.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/client.js +2 -1
- package/dist/src/platforms/kakaotalk/client.js.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/commands/auth.js +14 -13
- package/dist/src/platforms/kakaotalk/commands/auth.js.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/connection.d.ts.map +1 -1
- package/dist/src/platforms/kakaotalk/protocol/connection.js +2 -1
- package/dist/src/platforms/kakaotalk/protocol/connection.js.map +1 -1
- package/dist/src/platforms/line/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/line/commands/auth.js +6 -5
- package/dist/src/platforms/line/commands/auth.js.map +1 -1
- package/dist/src/platforms/slack/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/slack/commands/auth.js +11 -10
- package/dist/src/platforms/slack/commands/auth.js.map +1 -1
- package/dist/src/platforms/slack/token-extractor.d.ts +9 -0
- package/dist/src/platforms/slack/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/slack/token-extractor.js +300 -23
- package/dist/src/platforms/slack/token-extractor.js.map +1 -1
- package/dist/src/platforms/teams/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/commands/auth.js +9 -8
- package/dist/src/platforms/teams/commands/auth.js.map +1 -1
- package/dist/src/platforms/teams/ensure-auth.d.ts.map +1 -1
- package/dist/src/platforms/teams/ensure-auth.js +2 -1
- package/dist/src/platforms/teams/ensure-auth.js.map +1 -1
- package/dist/src/platforms/teams/token-extractor.d.ts +5 -0
- package/dist/src/platforms/teams/token-extractor.d.ts.map +1 -1
- package/dist/src/platforms/teams/token-extractor.js +161 -29
- package/dist/src/platforms/teams/token-extractor.js.map +1 -1
- package/dist/src/platforms/telegram/client.d.ts.map +1 -1
- package/dist/src/platforms/telegram/client.js +25 -7
- package/dist/src/platforms/telegram/client.js.map +1 -1
- package/dist/src/platforms/telegram/commands/auth.d.ts.map +1 -1
- package/dist/src/platforms/telegram/commands/auth.js +6 -5
- package/dist/src/platforms/telegram/commands/auth.js.map +1 -1
- package/dist/src/platforms/webex/app-config.d.ts +7 -0
- package/dist/src/platforms/webex/app-config.d.ts.map +1 -0
- package/dist/src/platforms/webex/app-config.js +20 -0
- package/dist/src/platforms/webex/app-config.js.map +1 -0
- package/dist/src/platforms/webex/cli.d.ts +5 -0
- package/dist/src/platforms/webex/cli.d.ts.map +1 -0
- package/dist/src/platforms/webex/cli.js +32 -0
- package/dist/src/platforms/webex/cli.js.map +1 -0
- package/dist/src/platforms/webex/client.d.ts +55 -0
- package/dist/src/platforms/webex/client.d.ts.map +1 -0
- package/dist/src/platforms/webex/client.js +299 -0
- package/dist/src/platforms/webex/client.js.map +1 -0
- package/dist/src/platforms/webex/commands/auth.d.ts +19 -0
- package/dist/src/platforms/webex/commands/auth.d.ts.map +1 -0
- package/dist/src/platforms/webex/commands/auth.js +166 -0
- package/dist/src/platforms/webex/commands/auth.js.map +1 -0
- package/dist/src/platforms/webex/commands/index.d.ts +6 -0
- package/dist/src/platforms/webex/commands/index.d.ts.map +1 -0
- package/dist/src/platforms/webex/commands/index.js +6 -0
- package/dist/src/platforms/webex/commands/index.js.map +1 -0
- package/dist/src/platforms/webex/commands/member.d.ts +7 -0
- package/dist/src/platforms/webex/commands/member.d.ts.map +1 -0
- package/dist/src/platforms/webex/commands/member.js +34 -0
- package/dist/src/platforms/webex/commands/member.js.map +1 -0
- package/dist/src/platforms/webex/commands/message.d.ts +26 -0
- package/dist/src/platforms/webex/commands/message.d.ts.map +1 -0
- package/dist/src/platforms/webex/commands/message.js +153 -0
- package/dist/src/platforms/webex/commands/message.js.map +1 -0
- package/dist/src/platforms/webex/commands/snapshot.d.ts +9 -0
- package/dist/src/platforms/webex/commands/snapshot.d.ts.map +1 -0
- package/dist/src/platforms/webex/commands/snapshot.js +72 -0
- package/dist/src/platforms/webex/commands/snapshot.js.map +1 -0
- package/dist/src/platforms/webex/commands/space.d.ts +11 -0
- package/dist/src/platforms/webex/commands/space.d.ts.map +1 -0
- package/dist/src/platforms/webex/commands/space.js +59 -0
- package/dist/src/platforms/webex/commands/space.js.map +1 -0
- package/dist/src/platforms/webex/credential-manager.d.ts +23 -0
- package/dist/src/platforms/webex/credential-manager.d.ts.map +1 -0
- package/dist/src/platforms/webex/credential-manager.js +148 -0
- package/dist/src/platforms/webex/credential-manager.js.map +1 -0
- package/dist/src/platforms/webex/ensure-auth.d.ts +2 -0
- package/dist/src/platforms/webex/ensure-auth.d.ts.map +1 -0
- package/dist/src/platforms/webex/ensure-auth.js +36 -0
- package/dist/src/platforms/webex/ensure-auth.js.map +1 -0
- package/dist/src/platforms/webex/index.d.ts +8 -0
- package/dist/src/platforms/webex/index.d.ts.map +1 -0
- package/dist/src/platforms/webex/index.js +6 -0
- package/dist/src/platforms/webex/index.js.map +1 -0
- package/dist/src/platforms/webex/token-extractor.d.ts +28 -0
- package/dist/src/platforms/webex/token-extractor.d.ts.map +1 -0
- package/dist/src/platforms/webex/token-extractor.js +344 -0
- package/dist/src/platforms/webex/token-extractor.js.map +1 -0
- package/dist/src/platforms/webex/types.d.ts +127 -0
- package/dist/src/platforms/webex/types.d.ts.map +1 -0
- package/dist/src/platforms/webex/types.js +64 -0
- package/dist/src/platforms/webex/types.js.map +1 -0
- package/dist/src/platforms/whatsapp/client.d.ts.map +1 -1
- package/dist/src/platforms/whatsapp/client.js +6 -2
- package/dist/src/platforms/whatsapp/client.js.map +1 -1
- package/dist/src/shared/utils/derived-key-cache.d.ts +1 -1
- package/dist/src/shared/utils/derived-key-cache.d.ts.map +1 -1
- package/dist/src/shared/utils/error-handler.d.ts +1 -1
- package/dist/src/shared/utils/error-handler.d.ts.map +1 -1
- package/dist/src/shared/utils/error-handler.js +3 -2
- package/dist/src/shared/utils/error-handler.js.map +1 -1
- package/dist/src/shared/utils/stderr.d.ts +5 -0
- package/dist/src/shared/utils/stderr.d.ts.map +1 -0
- package/dist/src/shared/utils/stderr.js +18 -0
- package/dist/src/shared/utils/stderr.js.map +1 -0
- package/dist/src/tui/adapters/webex-adapter.d.ts +14 -0
- package/dist/src/tui/adapters/webex-adapter.d.ts.map +1 -0
- package/dist/src/tui/adapters/webex-adapter.js +79 -0
- package/dist/src/tui/adapters/webex-adapter.js.map +1 -0
- package/dist/src/tui/app.d.ts.map +1 -1
- package/dist/src/tui/app.js +2 -0
- package/dist/src/tui/app.js.map +1 -1
- package/docs/content/docs/cli/channeltalk.mdx +7 -7
- package/docs/content/docs/cli/discord.mdx +3 -3
- package/docs/content/docs/cli/instagram.mdx +28 -6
- package/docs/content/docs/cli/meta.json +1 -0
- package/docs/content/docs/cli/slack.mdx +2 -2
- package/docs/content/docs/cli/teams.mdx +6 -4
- package/docs/content/docs/cli/webex.mdx +310 -0
- package/docs/content/docs/sdk/meta.json +1 -1
- package/docs/content/docs/sdk/webex.mdx +260 -0
- package/docs/content/docs/tui.mdx +4 -3
- package/docs/src/app/page.tsx +2 -2
- package/e2e/README.md +132 -8
- package/e2e/channeltalk.e2e.test.ts +2 -7
- package/e2e/channeltalkbot.e2e.test.ts +2 -6
- package/e2e/config.ts +172 -10
- package/e2e/helpers.ts +7 -0
- package/e2e/instagram.e2e.test.ts +97 -0
- package/e2e/kakaotalk.e2e.test.ts +74 -0
- package/e2e/line.e2e.test.ts +92 -0
- package/e2e/teams.e2e.test.ts +46 -1
- package/e2e/telegram.e2e.test.ts +84 -0
- package/e2e/webex.e2e.test.ts +190 -0
- package/e2e/whatsapp.e2e.test.ts +90 -0
- package/e2e/whatsappbot.e2e.test.ts +78 -0
- package/package.json +11 -3
- package/skills/agent-channeltalk/SKILL.md +9 -9
- package/skills/agent-channeltalk/references/authentication.md +21 -18
- package/skills/agent-channeltalkbot/SKILL.md +1 -1
- package/skills/agent-discord/SKILL.md +5 -5
- package/skills/agent-discord/references/authentication.md +8 -8
- package/skills/agent-discordbot/SKILL.md +1 -1
- package/skills/agent-instagram/SKILL.md +51 -9
- package/skills/agent-instagram/references/authentication.md +35 -3
- package/skills/agent-kakaotalk/SKILL.md +1 -1
- package/skills/agent-line/SKILL.md +1 -1
- package/skills/agent-slack/SKILL.md +5 -5
- package/skills/agent-slack/references/authentication.md +8 -8
- package/skills/agent-slackbot/SKILL.md +1 -1
- package/skills/agent-teams/SKILL.md +6 -6
- package/skills/agent-teams/references/authentication.md +8 -8
- package/skills/agent-telegram/SKILL.md +1 -1
- package/skills/agent-webex/SKILL.md +406 -0
- package/skills/agent-webex/references/authentication.md +371 -0
- package/skills/agent-webex/references/common-patterns.md +726 -0
- package/skills/agent-webex/templates/monitor-space.sh +165 -0
- package/skills/agent-webex/templates/post-message.sh +170 -0
- package/skills/agent-whatsapp/SKILL.md +1 -1
- package/skills/agent-whatsappbot/SKILL.md +1 -1
- package/src/cli.ts +4 -0
- package/src/platforms/channeltalk/commands/auth.test.ts +5 -5
- package/src/platforms/channeltalk/commands/auth.ts +38 -32
- package/src/platforms/channeltalk/ensure-auth.test.ts +6 -6
- package/src/platforms/channeltalk/ensure-auth.ts +6 -6
- package/src/platforms/channeltalk/token-extractor.test.ts +182 -15
- package/src/platforms/channeltalk/token-extractor.ts +344 -30
- package/src/platforms/discord/commands/auth.test.ts +3 -3
- package/src/platforms/discord/commands/auth.ts +58 -54
- package/src/platforms/discord/ensure-auth.test.ts +3 -3
- package/src/platforms/discord/ensure-auth.ts +3 -3
- package/src/platforms/discord/token-extractor.test.ts +199 -27
- package/src/platforms/discord/token-extractor.ts +190 -17
- package/src/platforms/instagram/client.ts +2 -2
- package/src/platforms/instagram/commands/auth.ts +133 -14
- package/src/platforms/instagram/ensure-auth.ts +63 -12
- package/src/platforms/instagram/index.ts +1 -0
- package/src/platforms/instagram/token-extractor.test.ts +424 -0
- package/src/platforms/instagram/token-extractor.ts +478 -0
- package/src/platforms/kakaotalk/client.ts +3 -1
- package/src/platforms/kakaotalk/commands/auth.ts +14 -13
- package/src/platforms/kakaotalk/protocol/connection.ts +3 -1
- package/src/platforms/line/commands/auth.ts +7 -6
- package/src/platforms/slack/cli.test.ts +6 -5
- package/src/platforms/slack/commands/auth.test.ts +11 -7
- package/src/platforms/slack/commands/auth.ts +11 -10
- package/src/platforms/slack/token-extractor.test.ts +98 -1
- package/src/platforms/slack/token-extractor.ts +338 -26
- package/src/platforms/teams/commands/auth.ts +9 -8
- package/src/platforms/teams/ensure-auth.ts +3 -1
- package/src/platforms/teams/token-extractor.test.ts +136 -17
- package/src/platforms/teams/token-extractor.ts +182 -31
- package/src/platforms/telegram/client.test.ts +134 -0
- package/src/platforms/telegram/client.ts +27 -6
- package/src/platforms/telegram/commands/auth.ts +6 -5
- package/src/platforms/webex/app-config.test.ts +98 -0
- package/src/platforms/webex/app-config.ts +31 -0
- package/src/platforms/webex/cli.test.ts +58 -0
- package/src/platforms/webex/cli.ts +39 -0
- package/src/platforms/webex/client.test.ts +743 -0
- package/src/platforms/webex/client.ts +405 -0
- package/src/platforms/webex/commands/auth.test.ts +222 -0
- package/src/platforms/webex/commands/auth.ts +243 -0
- package/src/platforms/webex/commands/index.ts +5 -0
- package/src/platforms/webex/commands/member.test.ts +112 -0
- package/src/platforms/webex/commands/member.ts +45 -0
- package/src/platforms/webex/commands/message.test.ts +235 -0
- package/src/platforms/webex/commands/message.ts +204 -0
- package/src/platforms/webex/commands/snapshot.test.ts +105 -0
- package/src/platforms/webex/commands/snapshot.ts +91 -0
- package/src/platforms/webex/commands/space.test.ts +216 -0
- package/src/platforms/webex/commands/space.ts +74 -0
- package/src/platforms/webex/credential-manager.test.ts +314 -0
- package/src/platforms/webex/credential-manager.ts +197 -0
- package/src/platforms/webex/ensure-auth.test.ts +89 -0
- package/src/platforms/webex/ensure-auth.ts +38 -0
- package/src/platforms/webex/index.test.ts +25 -0
- package/src/platforms/webex/index.ts +19 -0
- package/src/platforms/webex/token-extractor.test.ts +327 -0
- package/src/platforms/webex/token-extractor.ts +393 -0
- package/src/platforms/webex/types.test.ts +307 -0
- package/src/platforms/webex/types.ts +129 -0
- package/src/platforms/whatsapp/client.ts +11 -7
- package/src/shared/utils/derived-key-cache.ts +1 -1
- package/src/shared/utils/error-handler.ts +4 -2
- package/src/shared/utils/stderr.ts +22 -0
- package/src/tui/adapters/webex-adapter.ts +103 -0
- package/src/tui/app.ts +2 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from 'bun:test'
|
|
1
|
+
import { afterEach, describe, expect, spyOn, test } from 'bun:test'
|
|
2
2
|
import { createCipheriv, randomBytes } from 'node:crypto'
|
|
3
3
|
import { existsSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
4
4
|
import { tmpdir } from 'node:os'
|
|
@@ -57,8 +57,47 @@ describe('ChannelTokenExtractor', () => {
|
|
|
57
57
|
})
|
|
58
58
|
})
|
|
59
59
|
|
|
60
|
+
describe('getBrowserCookiesPaths', () => {
|
|
61
|
+
test('returns browser cookie paths on macOS including Default profile', () => {
|
|
62
|
+
const extractor = new ChannelTokenExtractor('darwin')
|
|
63
|
+
const paths = extractor.getBrowserCookiesPaths()
|
|
64
|
+
|
|
65
|
+
const chromeBase = join(
|
|
66
|
+
process.env.HOME || '/tmp',
|
|
67
|
+
'Library',
|
|
68
|
+
'Application Support',
|
|
69
|
+
'Google',
|
|
70
|
+
'Chrome',
|
|
71
|
+
)
|
|
72
|
+
expect(paths).toContain(join(chromeBase, 'Default', 'Cookies'))
|
|
73
|
+
expect(paths).toContain(join(chromeBase, 'Default', 'Network', 'Cookies'))
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('returns browser cookie paths on Linux', () => {
|
|
77
|
+
const extractor = new ChannelTokenExtractor('linux')
|
|
78
|
+
const paths = extractor.getBrowserCookiesPaths()
|
|
79
|
+
|
|
80
|
+
const chromeBase = join(process.env.HOME || '/tmp', '.config', 'google-chrome')
|
|
81
|
+
expect(paths).toContain(join(chromeBase, 'Default', 'Cookies'))
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('returns browser cookie paths on Windows', () => {
|
|
85
|
+
const extractor = new ChannelTokenExtractor('win32')
|
|
86
|
+
const paths = extractor.getBrowserCookiesPaths()
|
|
87
|
+
|
|
88
|
+
const localAppData = process.env.LOCALAPPDATA || join(process.env.HOME || '/tmp', 'AppData', 'Local')
|
|
89
|
+
const chromeBase = join(localAppData, 'Google', 'Chrome', 'User Data')
|
|
90
|
+
expect(paths).toContain(join(chromeBase, 'Default', 'Cookies'))
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('returns empty array for unsupported platform', () => {
|
|
94
|
+
const extractor = new ChannelTokenExtractor('freebsd' as NodeJS.Platform)
|
|
95
|
+
expect(extractor.getBrowserCookiesPaths()).toEqual([])
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
|
|
60
99
|
describe('extract', () => {
|
|
61
|
-
test('returns
|
|
100
|
+
test('returns empty array when desktop cookies path does not exist', async () => {
|
|
62
101
|
class MissingPathExtractor extends ChannelTokenExtractor {
|
|
63
102
|
override getCookiesPath(): string | null {
|
|
64
103
|
return null
|
|
@@ -67,7 +106,45 @@ describe('ChannelTokenExtractor', () => {
|
|
|
67
106
|
|
|
68
107
|
const extractor = new MissingPathExtractor('darwin')
|
|
69
108
|
|
|
70
|
-
expect(await extractor.extract()).
|
|
109
|
+
expect(await extractor.extract()).toEqual([])
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
test('tries desktop app before browser profiles and collects both', async () => {
|
|
113
|
+
const extractor = new ChannelTokenExtractor('darwin')
|
|
114
|
+
|
|
115
|
+
const desktopSpy = spyOn(extractor as any, 'extractFromDesktopApp').mockResolvedValue({
|
|
116
|
+
accountCookie: 'desktop-account',
|
|
117
|
+
sessionCookie: 'desktop-session',
|
|
118
|
+
})
|
|
119
|
+
const browserSpy = spyOn(extractor as any, 'extractAllFromBrowserPaths').mockResolvedValue([])
|
|
120
|
+
|
|
121
|
+
const result = await extractor.extract()
|
|
122
|
+
|
|
123
|
+
expect(desktopSpy).toHaveBeenCalled()
|
|
124
|
+
expect(browserSpy).toHaveBeenCalled()
|
|
125
|
+
expect(result[0]?.accountCookie).toBe('desktop-account')
|
|
126
|
+
|
|
127
|
+
desktopSpy.mockRestore()
|
|
128
|
+
browserSpy.mockRestore()
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test('includes browser profiles even when desktop extraction returns null', async () => {
|
|
132
|
+
const extractor = new ChannelTokenExtractor('darwin')
|
|
133
|
+
|
|
134
|
+
const desktopSpy = spyOn(extractor as any, 'extractFromDesktopApp').mockResolvedValue(null)
|
|
135
|
+
const browserSpy = spyOn(extractor as any, 'extractAllFromBrowserPaths').mockResolvedValue([{
|
|
136
|
+
accountCookie: 'browser-account',
|
|
137
|
+
sessionCookie: undefined,
|
|
138
|
+
}])
|
|
139
|
+
|
|
140
|
+
const result = await extractor.extract()
|
|
141
|
+
|
|
142
|
+
expect(desktopSpy).toHaveBeenCalled()
|
|
143
|
+
expect(browserSpy).toHaveBeenCalled()
|
|
144
|
+
expect(result[0]?.accountCookie).toBe('browser-account')
|
|
145
|
+
|
|
146
|
+
desktopSpy.mockRestore()
|
|
147
|
+
browserSpy.mockRestore()
|
|
71
148
|
})
|
|
72
149
|
|
|
73
150
|
test('extracts plaintext cookies from a real sqlite database', async () => {
|
|
@@ -92,10 +169,10 @@ describe('ChannelTokenExtractor', () => {
|
|
|
92
169
|
|
|
93
170
|
const extractor = new TestExtractor(dbPath)
|
|
94
171
|
|
|
95
|
-
expect(await extractor.extract()).toEqual({
|
|
172
|
+
expect(await extractor.extract()).toEqual([{
|
|
96
173
|
accountCookie: 'account-jwt',
|
|
97
174
|
sessionCookie: 'session-jwt',
|
|
98
|
-
})
|
|
175
|
+
}])
|
|
99
176
|
})
|
|
100
177
|
|
|
101
178
|
test('returns token with undefined sessionCookie when only x-account is present', async () => {
|
|
@@ -119,12 +196,12 @@ describe('ChannelTokenExtractor', () => {
|
|
|
119
196
|
const extractor = new TestExtractor(dbPath)
|
|
120
197
|
const result = await extractor.extract()
|
|
121
198
|
|
|
122
|
-
expect(result).not.
|
|
123
|
-
expect(result?.accountCookie).toBe('account-jwt')
|
|
124
|
-
expect(result?.sessionCookie).toBeUndefined()
|
|
199
|
+
expect(result).not.toEqual([])
|
|
200
|
+
expect(result[0]?.accountCookie).toBe('account-jwt')
|
|
201
|
+
expect(result[0]?.sessionCookie).toBeUndefined()
|
|
125
202
|
})
|
|
126
203
|
|
|
127
|
-
test('returns
|
|
204
|
+
test('returns empty array when x-account is missing', async () => {
|
|
128
205
|
const tempDir = mkdtempSync(join(tmpdir(), 'channel-cookie-db-'))
|
|
129
206
|
tempDirs.push(tempDir)
|
|
130
207
|
const dbPath = join(tempDir, 'Cookies')
|
|
@@ -144,7 +221,7 @@ describe('ChannelTokenExtractor', () => {
|
|
|
144
221
|
|
|
145
222
|
const extractor = new TestExtractor(dbPath)
|
|
146
223
|
|
|
147
|
-
expect(await extractor.extract()).
|
|
224
|
+
expect(await extractor.extract()).toEqual([])
|
|
148
225
|
})
|
|
149
226
|
|
|
150
227
|
test('decrypts AES-256-GCM encrypted cookies using master key', async () => {
|
|
@@ -194,12 +271,53 @@ describe('ChannelTokenExtractor', () => {
|
|
|
194
271
|
const result = await extractor.extract()
|
|
195
272
|
|
|
196
273
|
// then
|
|
197
|
-
expect(result).not.
|
|
198
|
-
expect(result?.accountCookie).toBe('encrypted-account-jwt')
|
|
199
|
-
expect(result?.sessionCookie).toBe('encrypted-session-jwt')
|
|
274
|
+
expect(result).not.toEqual([])
|
|
275
|
+
expect(result[0]?.accountCookie).toBe('encrypted-account-jwt')
|
|
276
|
+
expect(result[0]?.sessionCookie).toBe('encrypted-session-jwt')
|
|
200
277
|
})
|
|
201
278
|
|
|
202
|
-
test('
|
|
279
|
+
test('deduplicates entries with the same accountCookie from desktop and browser', async () => {
|
|
280
|
+
const extractor = new ChannelTokenExtractor('darwin')
|
|
281
|
+
|
|
282
|
+
const desktopSpy = spyOn(extractor as any, 'extractFromDesktopApp').mockResolvedValue({
|
|
283
|
+
accountCookie: 'same-account-cookie',
|
|
284
|
+
sessionCookie: 'desktop-session',
|
|
285
|
+
})
|
|
286
|
+
const browserSpy = spyOn(extractor as any, 'extractAllFromBrowserPaths').mockResolvedValue([{
|
|
287
|
+
accountCookie: 'same-account-cookie',
|
|
288
|
+
sessionCookie: 'browser-session',
|
|
289
|
+
}])
|
|
290
|
+
|
|
291
|
+
const result = await extractor.extract()
|
|
292
|
+
expect(result).toHaveLength(1)
|
|
293
|
+
expect(result[0]?.accountCookie).toBe('same-account-cookie')
|
|
294
|
+
|
|
295
|
+
desktopSpy.mockRestore()
|
|
296
|
+
browserSpy.mockRestore()
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
test('returns multiple distinct accounts from desktop and browser sources', async () => {
|
|
300
|
+
const extractor = new ChannelTokenExtractor('darwin')
|
|
301
|
+
|
|
302
|
+
const desktopSpy = spyOn(extractor as any, 'extractFromDesktopApp').mockResolvedValue({
|
|
303
|
+
accountCookie: 'desktop-account-cookie',
|
|
304
|
+
sessionCookie: 'desktop-session',
|
|
305
|
+
})
|
|
306
|
+
const browserSpy = spyOn(extractor as any, 'extractAllFromBrowserPaths').mockResolvedValue([{
|
|
307
|
+
accountCookie: 'browser-account-cookie',
|
|
308
|
+
sessionCookie: 'browser-session',
|
|
309
|
+
}])
|
|
310
|
+
|
|
311
|
+
const result = await extractor.extract()
|
|
312
|
+
expect(result).toHaveLength(2)
|
|
313
|
+
expect(result.map((r) => r.accountCookie)).toContain('desktop-account-cookie')
|
|
314
|
+
expect(result.map((r) => r.accountCookie)).toContain('browser-account-cookie')
|
|
315
|
+
|
|
316
|
+
desktopSpy.mockRestore()
|
|
317
|
+
browserSpy.mockRestore()
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
test('returns empty array when DPAPI decryption fails', async () => {
|
|
203
321
|
// given
|
|
204
322
|
const masterKey = randomBytes(32)
|
|
205
323
|
const encryptAccount = encryptAESGCM('account-jwt', masterKey)
|
|
@@ -238,7 +356,7 @@ describe('ChannelTokenExtractor', () => {
|
|
|
238
356
|
const result = await extractor.extract()
|
|
239
357
|
|
|
240
358
|
// then
|
|
241
|
-
expect(result).
|
|
359
|
+
expect(result).toEqual([])
|
|
242
360
|
})
|
|
243
361
|
})
|
|
244
362
|
|
|
@@ -248,6 +366,55 @@ describe('ChannelTokenExtractor', () => {
|
|
|
248
366
|
expect(extractor.decryptDPAPI(Buffer.from('test'))).toBeNull()
|
|
249
367
|
})
|
|
250
368
|
})
|
|
369
|
+
|
|
370
|
+
describe('decryptBrowserCookie', () => {
|
|
371
|
+
test('decrypts v10-prefixed browser cookie using macOS keychain password (AES-128-CBC)', () => {
|
|
372
|
+
// given — AES-128-CBC encrypted cookie with macOS keychain-derived key
|
|
373
|
+
const { createCipheriv, pbkdf2Sync } = require('node:crypto')
|
|
374
|
+
const password = 'test-keychain-password'
|
|
375
|
+
const key = pbkdf2Sync(password, 'saltysalt', 1003, 16, 'sha1')
|
|
376
|
+
const iv = Buffer.alloc(16, 0x20)
|
|
377
|
+
const plainValue = 'test-channel-account-value'
|
|
378
|
+
|
|
379
|
+
const cipher = createCipheriv('aes-128-cbc', key, iv)
|
|
380
|
+
const ciphertext = Buffer.concat([cipher.update(plainValue, 'utf8'), cipher.final()])
|
|
381
|
+
const encrypted = Buffer.concat([Buffer.from('v10'), ciphertext])
|
|
382
|
+
|
|
383
|
+
const darwinExtractor = new ChannelTokenExtractor('darwin')
|
|
384
|
+
const execSecuritySpy = spyOn(darwinExtractor as any, 'execSecurityCommand').mockReturnValue(password)
|
|
385
|
+
|
|
386
|
+
// when
|
|
387
|
+
const result = (darwinExtractor as any).decryptBrowserCookie(encrypted, '/fake/path/Cookies')
|
|
388
|
+
|
|
389
|
+
// then
|
|
390
|
+
expect(result).toBe(plainValue)
|
|
391
|
+
|
|
392
|
+
execSecuritySpy.mockRestore()
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
test('decrypts v10-prefixed browser cookie using Linux peanuts key (AES-128-CBC)', () => {
|
|
396
|
+
// given — AES-128-CBC encrypted cookie with Linux Chromium peanuts key
|
|
397
|
+
const { createCipheriv, pbkdf2Sync } = require('node:crypto')
|
|
398
|
+
const key = pbkdf2Sync('peanuts', 'saltysalt', 1, 16, 'sha1')
|
|
399
|
+
const iv = Buffer.alloc(16, 0x20)
|
|
400
|
+
const plainValue = 'test-channel-account-linux'
|
|
401
|
+
|
|
402
|
+
const cipher = createCipheriv('aes-128-cbc', key, iv)
|
|
403
|
+
const ciphertext = Buffer.concat([cipher.update(plainValue, 'utf8'), cipher.final()])
|
|
404
|
+
const encrypted = Buffer.concat([Buffer.from('v10'), ciphertext])
|
|
405
|
+
|
|
406
|
+
const linuxExtractor = new ChannelTokenExtractor('linux')
|
|
407
|
+
|
|
408
|
+
// when
|
|
409
|
+
const result = (linuxExtractor as any).decryptBrowserCookie(
|
|
410
|
+
encrypted,
|
|
411
|
+
'/home/user/.config/google-chrome/Default/Cookies',
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
// then
|
|
415
|
+
expect(result).toBe(plainValue)
|
|
416
|
+
})
|
|
417
|
+
})
|
|
251
418
|
})
|
|
252
419
|
|
|
253
420
|
function encryptAESGCM(plaintext: string, key: Buffer): Buffer {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { createDecipheriv } from 'node:crypto'
|
|
2
|
-
import { copyFileSync, existsSync, readFileSync, unlinkSync } from 'node:fs'
|
|
3
1
|
import { execSync } from 'node:child_process'
|
|
2
|
+
import { createDecipheriv, pbkdf2Sync } from 'node:crypto'
|
|
3
|
+
import { copyFileSync, existsSync, readFileSync, readdirSync, unlinkSync } from 'node:fs'
|
|
4
4
|
import { homedir, tmpdir } from 'node:os'
|
|
5
5
|
import { join } from 'node:path'
|
|
6
6
|
|
|
@@ -8,8 +8,66 @@ import type { ExtractedChannelToken } from './types'
|
|
|
8
8
|
|
|
9
9
|
type CookieRow = { name: string; value: string; encrypted_value: Uint8Array | Buffer }
|
|
10
10
|
|
|
11
|
+
interface BrowserConfig {
|
|
12
|
+
name: string
|
|
13
|
+
darwin: string
|
|
14
|
+
linux: string
|
|
15
|
+
win32: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface KeychainVariant {
|
|
19
|
+
service: string
|
|
20
|
+
account: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const BROWSERS: BrowserConfig[] = [
|
|
24
|
+
{
|
|
25
|
+
name: 'Chrome',
|
|
26
|
+
darwin: join('Google', 'Chrome'),
|
|
27
|
+
linux: 'google-chrome',
|
|
28
|
+
win32: join('Google', 'Chrome', 'User Data'),
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'Chrome Canary',
|
|
32
|
+
darwin: join('Google', 'Chrome Canary'),
|
|
33
|
+
linux: 'google-chrome-unstable',
|
|
34
|
+
win32: join('Google', 'Chrome SxS', 'User Data'),
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'Edge',
|
|
38
|
+
darwin: 'Microsoft Edge',
|
|
39
|
+
linux: 'microsoft-edge',
|
|
40
|
+
win32: join('Microsoft', 'Edge', 'User Data'),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'Arc',
|
|
44
|
+
darwin: join('Arc', 'User Data'),
|
|
45
|
+
linux: '',
|
|
46
|
+
win32: join('Arc', 'User Data'),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'Brave',
|
|
50
|
+
darwin: join('BraveSoftware', 'Brave-Browser'),
|
|
51
|
+
linux: join('BraveSoftware', 'Brave-Browser'),
|
|
52
|
+
win32: join('BraveSoftware', 'Brave-Browser', 'User Data'),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'Vivaldi',
|
|
56
|
+
darwin: 'Vivaldi',
|
|
57
|
+
linux: 'vivaldi',
|
|
58
|
+
win32: join('Vivaldi', 'User Data'),
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: 'Chromium',
|
|
62
|
+
darwin: 'Chromium',
|
|
63
|
+
linux: 'chromium',
|
|
64
|
+
win32: join('Chromium', 'User Data'),
|
|
65
|
+
},
|
|
66
|
+
]
|
|
67
|
+
|
|
11
68
|
export class ChannelTokenExtractor {
|
|
12
69
|
private platform: NodeJS.Platform
|
|
70
|
+
private cachedKey: Buffer | null = null
|
|
13
71
|
|
|
14
72
|
constructor(platform?: NodeJS.Platform) {
|
|
15
73
|
this.platform = platform ?? process.platform
|
|
@@ -67,7 +125,123 @@ export class ChannelTokenExtractor {
|
|
|
67
125
|
return existsSync(localStatePath) ? localStatePath : null
|
|
68
126
|
}
|
|
69
127
|
|
|
70
|
-
|
|
128
|
+
getBrowserCookiesPaths(): string[] {
|
|
129
|
+
const paths: string[] = []
|
|
130
|
+
|
|
131
|
+
for (const browser of BROWSERS) {
|
|
132
|
+
const browserBase = this.getBrowserBasePath(browser)
|
|
133
|
+
if (!browserBase) continue
|
|
134
|
+
|
|
135
|
+
const profileDirs = this.discoverProfileDirs(browserBase)
|
|
136
|
+
for (const profileDir of profileDirs) {
|
|
137
|
+
paths.push(join(profileDir, 'Cookies'))
|
|
138
|
+
paths.push(join(profileDir, 'Network', 'Cookies'))
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return paths
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
private getBrowserBasePath(browser: BrowserConfig): string | null {
|
|
146
|
+
let relative: string
|
|
147
|
+
|
|
148
|
+
switch (this.platform) {
|
|
149
|
+
case 'darwin':
|
|
150
|
+
relative = browser.darwin
|
|
151
|
+
if (!relative) return null
|
|
152
|
+
return join(homedir(), 'Library', 'Application Support', relative)
|
|
153
|
+
case 'linux':
|
|
154
|
+
relative = browser.linux
|
|
155
|
+
if (!relative) return null
|
|
156
|
+
return join(homedir(), '.config', relative)
|
|
157
|
+
case 'win32':
|
|
158
|
+
relative = browser.win32
|
|
159
|
+
if (!relative) return null
|
|
160
|
+
return join(
|
|
161
|
+
process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local'),
|
|
162
|
+
relative,
|
|
163
|
+
)
|
|
164
|
+
default:
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private discoverProfileDirs(browserBase: string): string[] {
|
|
170
|
+
const dirs: string[] = []
|
|
171
|
+
|
|
172
|
+
dirs.push(join(browserBase, 'Default'))
|
|
173
|
+
|
|
174
|
+
if (!existsSync(browserBase)) return dirs
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
const entries = readdirSync(browserBase, { withFileTypes: true })
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
if (!entry.isDirectory()) continue
|
|
180
|
+
if (!/^Profile \d+$/i.test(entry.name)) continue
|
|
181
|
+
dirs.push(join(browserBase, entry.name))
|
|
182
|
+
}
|
|
183
|
+
} catch {}
|
|
184
|
+
|
|
185
|
+
return dirs
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
private findLocalStateForCookiePath(cookiePath: string): string | null {
|
|
189
|
+
const parts = cookiePath.split(/[/\\]/)
|
|
190
|
+
for (let levels = 2; levels <= 4; levels++) {
|
|
191
|
+
if (parts.length < levels) break
|
|
192
|
+
const base = parts.slice(0, parts.length - levels).join('/')
|
|
193
|
+
const candidate = join(base, 'Local State')
|
|
194
|
+
if (existsSync(candidate)) return candidate
|
|
195
|
+
}
|
|
196
|
+
return null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
getKeychainVariants(): KeychainVariant[] {
|
|
200
|
+
return [
|
|
201
|
+
{ service: 'Chrome Safe Storage', account: 'Chrome' },
|
|
202
|
+
{ service: 'Chrome Canary Safe Storage', account: 'Chrome Canary' },
|
|
203
|
+
{ service: 'Microsoft Edge Safe Storage', account: 'Microsoft Edge' },
|
|
204
|
+
{ service: 'Arc Safe Storage', account: 'Arc' },
|
|
205
|
+
{ service: 'Brave Safe Storage', account: 'Brave' },
|
|
206
|
+
{ service: 'Vivaldi Safe Storage', account: 'Vivaldi' },
|
|
207
|
+
{ service: 'Chromium Safe Storage', account: 'Chromium' },
|
|
208
|
+
]
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async extract(): Promise<ExtractedChannelToken[]> {
|
|
212
|
+
const results: ExtractedChannelToken[] = []
|
|
213
|
+
const seenAccounts = new Set<string>()
|
|
214
|
+
|
|
215
|
+
const desktopResult = await this.extractFromDesktopApp()
|
|
216
|
+
if (desktopResult && !seenAccounts.has(desktopResult.accountCookie)) {
|
|
217
|
+
seenAccounts.add(desktopResult.accountCookie)
|
|
218
|
+
results.push(desktopResult)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
for (const browserResult of await this.extractAllFromBrowserPaths()) {
|
|
222
|
+
if (!seenAccounts.has(browserResult.accountCookie)) {
|
|
223
|
+
seenAccounts.add(browserResult.accountCookie)
|
|
224
|
+
results.push(browserResult)
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return results
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private async extractAllFromBrowserPaths(): Promise<ExtractedChannelToken[]> {
|
|
232
|
+
const results: ExtractedChannelToken[] = []
|
|
233
|
+
const cookiePaths = this.getBrowserCookiesPaths()
|
|
234
|
+
|
|
235
|
+
for (const cookiePath of cookiePaths) {
|
|
236
|
+
if (!existsSync(cookiePath)) continue
|
|
237
|
+
const result = await this.extractFromBrowserCookiePath(cookiePath)
|
|
238
|
+
if (result) results.push(result)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return results
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
private async extractFromDesktopApp(): Promise<ExtractedChannelToken | null> {
|
|
71
245
|
const cookiesPath = this.getCookiesPath()
|
|
72
246
|
if (!cookiesPath) {
|
|
73
247
|
return null
|
|
@@ -77,28 +251,7 @@ export class ChannelTokenExtractor {
|
|
|
77
251
|
|
|
78
252
|
try {
|
|
79
253
|
copyFileSync(cookiesPath, tempPath)
|
|
80
|
-
const
|
|
81
|
-
SELECT name, value, encrypted_value FROM cookies
|
|
82
|
-
WHERE name IN ('x-account', 'ch-session-1', 'ch-session')
|
|
83
|
-
AND host_key LIKE '%.channel.io%'
|
|
84
|
-
`
|
|
85
|
-
const rows: CookieRow[] = typeof globalThis.Bun !== 'undefined'
|
|
86
|
-
? await (async () => {
|
|
87
|
-
const { Database } = await import('bun:sqlite')
|
|
88
|
-
const db = new Database(tempPath, { readonly: true })
|
|
89
|
-
const result = db.query(sql).all() as CookieRow[]
|
|
90
|
-
db.close()
|
|
91
|
-
return result
|
|
92
|
-
})()
|
|
93
|
-
: await (async () => {
|
|
94
|
-
const { createRequire } = await import('node:module')
|
|
95
|
-
const req = createRequire(import.meta.url)
|
|
96
|
-
const Database = req('better-sqlite3')
|
|
97
|
-
const db = new Database(tempPath, { readonly: true })
|
|
98
|
-
const result = db.prepare(sql).all() as CookieRow[]
|
|
99
|
-
db.close()
|
|
100
|
-
return result
|
|
101
|
-
})()
|
|
254
|
+
const rows = await this.queryCookieDB(tempPath)
|
|
102
255
|
|
|
103
256
|
const accountCookie = this.getCookieValue(rows, 'x-account')
|
|
104
257
|
const sessionCookie =
|
|
@@ -119,6 +272,73 @@ export class ChannelTokenExtractor {
|
|
|
119
272
|
}
|
|
120
273
|
}
|
|
121
274
|
|
|
275
|
+
private async extractFromBrowsers(): Promise<ExtractedChannelToken | null> {
|
|
276
|
+
const cookiePaths = this.getBrowserCookiesPaths()
|
|
277
|
+
|
|
278
|
+
for (const cookiePath of cookiePaths) {
|
|
279
|
+
if (!existsSync(cookiePath)) continue
|
|
280
|
+
|
|
281
|
+
const result = await this.extractFromBrowserCookiePath(cookiePath)
|
|
282
|
+
if (result) return result
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return null
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private async extractFromBrowserCookiePath(cookiePath: string): Promise<ExtractedChannelToken | null> {
|
|
289
|
+
const tempPath = join(tmpdir(), `channel-browser-cookies-${Date.now()}`)
|
|
290
|
+
|
|
291
|
+
try {
|
|
292
|
+
copyFileSync(cookiePath, tempPath)
|
|
293
|
+
const rows = await this.queryCookieDB(tempPath)
|
|
294
|
+
|
|
295
|
+
const accountCookie = this.getBrowserCookieValue(rows, 'x-account', cookiePath)
|
|
296
|
+
const sessionCookie =
|
|
297
|
+
this.getBrowserCookieValue(rows, 'ch-session-1', cookiePath) ??
|
|
298
|
+
this.getBrowserCookieValue(rows, 'ch-session', cookiePath)
|
|
299
|
+
|
|
300
|
+
return accountCookie ? { accountCookie, sessionCookie } : null
|
|
301
|
+
} catch {
|
|
302
|
+
return null
|
|
303
|
+
} finally {
|
|
304
|
+
try {
|
|
305
|
+
if (existsSync(tempPath)) {
|
|
306
|
+
unlinkSync(tempPath)
|
|
307
|
+
}
|
|
308
|
+
} catch {
|
|
309
|
+
/* temp file cleanup failure is non-critical */
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
private async queryCookieDB(dbPath: string): Promise<CookieRow[]> {
|
|
315
|
+
const sql = `
|
|
316
|
+
SELECT name, value, encrypted_value FROM cookies
|
|
317
|
+
WHERE name IN ('x-account', 'ch-session-1', 'ch-session')
|
|
318
|
+
AND host_key LIKE '%.channel.io%'
|
|
319
|
+
`
|
|
320
|
+
|
|
321
|
+
if (typeof globalThis.Bun !== 'undefined') {
|
|
322
|
+
return await (async () => {
|
|
323
|
+
const { Database } = await import('bun:sqlite')
|
|
324
|
+
const db = new Database(dbPath, { readonly: true })
|
|
325
|
+
const result = db.query(sql).all() as CookieRow[]
|
|
326
|
+
db.close()
|
|
327
|
+
return result
|
|
328
|
+
})()
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return await (async () => {
|
|
332
|
+
const { createRequire } = await import('node:module')
|
|
333
|
+
const req = createRequire(import.meta.url)
|
|
334
|
+
const Database = req('better-sqlite3')
|
|
335
|
+
const db = new Database(dbPath, { readonly: true })
|
|
336
|
+
const result = db.prepare(sql).all() as CookieRow[]
|
|
337
|
+
db.close()
|
|
338
|
+
return result
|
|
339
|
+
})()
|
|
340
|
+
}
|
|
341
|
+
|
|
122
342
|
private getCookieValue(rows: CookieRow[], name: string): string | undefined {
|
|
123
343
|
const row = rows.find((r) => r.name === name)
|
|
124
344
|
if (!row) return undefined
|
|
@@ -133,6 +353,20 @@ export class ChannelTokenExtractor {
|
|
|
133
353
|
return this.decryptCookie(encrypted) ?? undefined
|
|
134
354
|
}
|
|
135
355
|
|
|
356
|
+
private getBrowserCookieValue(rows: CookieRow[], name: string, dbPath: string): string | undefined {
|
|
357
|
+
const row = rows.find((r) => r.name === name)
|
|
358
|
+
if (!row) return undefined
|
|
359
|
+
|
|
360
|
+
if (row.value && row.value.length > 0) {
|
|
361
|
+
return row.value
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const encrypted = Buffer.from(row.encrypted_value)
|
|
365
|
+
if (encrypted.length === 0) return undefined
|
|
366
|
+
|
|
367
|
+
return this.decryptBrowserCookie(encrypted, dbPath) ?? undefined
|
|
368
|
+
}
|
|
369
|
+
|
|
136
370
|
private decryptCookie(encryptedValue: Buffer): string | null {
|
|
137
371
|
if (!this.isEncryptedValue(encryptedValue)) {
|
|
138
372
|
return encryptedValue.toString('utf8')
|
|
@@ -145,21 +379,37 @@ export class ChannelTokenExtractor {
|
|
|
145
379
|
return null
|
|
146
380
|
}
|
|
147
381
|
|
|
382
|
+
private decryptBrowserCookie(encryptedValue: Buffer, dbPath: string): string | null {
|
|
383
|
+
if (!this.isEncryptedValue(encryptedValue)) {
|
|
384
|
+
return encryptedValue.toString('utf8')
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (this.platform === 'win32') {
|
|
388
|
+
const localStatePath = this.findLocalStateForCookiePath(dbPath)
|
|
389
|
+
return this.decryptWindowsCookie(encryptedValue, localStatePath ?? undefined)
|
|
390
|
+
} else if (this.platform === 'darwin') {
|
|
391
|
+
return this.decryptMacCookie(encryptedValue)
|
|
392
|
+
} else if (this.platform === 'linux') {
|
|
393
|
+
return this.decryptLinuxCookie(encryptedValue)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return null
|
|
397
|
+
}
|
|
398
|
+
|
|
148
399
|
private isEncryptedValue(value: Buffer): boolean {
|
|
149
400
|
if (!value || value.length < 4) return false
|
|
150
401
|
const prefix = value.subarray(0, 3).toString('utf8')
|
|
151
402
|
return prefix === 'v10' || prefix === 'v11'
|
|
152
403
|
}
|
|
153
404
|
|
|
154
|
-
private decryptWindowsCookie(encryptedData: Buffer): string | null {
|
|
405
|
+
private decryptWindowsCookie(encryptedData: Buffer, localStatePath?: string): string | null {
|
|
155
406
|
try {
|
|
156
|
-
const
|
|
157
|
-
if (!
|
|
407
|
+
const statePath = localStatePath ?? this.getLocalStatePath()
|
|
408
|
+
if (!statePath) return null
|
|
158
409
|
|
|
159
|
-
const localState = JSON.parse(readFileSync(
|
|
410
|
+
const localState = JSON.parse(readFileSync(statePath, 'utf8'))
|
|
160
411
|
const encryptedKey = Buffer.from(localState.os_crypt.encrypted_key, 'base64')
|
|
161
412
|
|
|
162
|
-
// Remove "DPAPI" prefix (5 bytes)
|
|
163
413
|
const dpapiBlobKey = encryptedKey.subarray(5)
|
|
164
414
|
const masterKey = this.decryptDPAPI(dpapiBlobKey)
|
|
165
415
|
if (!masterKey) return null
|
|
@@ -170,6 +420,70 @@ export class ChannelTokenExtractor {
|
|
|
170
420
|
}
|
|
171
421
|
}
|
|
172
422
|
|
|
423
|
+
private decryptMacCookie(encryptedData: Buffer): string | null {
|
|
424
|
+
if (this.cachedKey) {
|
|
425
|
+
const decrypted = this.decryptAESCBC(encryptedData, this.cachedKey)
|
|
426
|
+
if (decrypted) return decrypted
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
for (const variant of this.getKeychainVariants()) {
|
|
430
|
+
const password = this.execSecurityCommand(variant.service, variant.account)
|
|
431
|
+
if (!password) continue
|
|
432
|
+
|
|
433
|
+
const key = pbkdf2Sync(password, 'saltysalt', 1003, 16, 'sha1')
|
|
434
|
+
const decrypted = this.decryptAESCBC(encryptedData, key)
|
|
435
|
+
if (decrypted) {
|
|
436
|
+
this.cachedKey = key
|
|
437
|
+
return decrypted
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return null
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
private decryptLinuxCookie(encryptedData: Buffer): string | null {
|
|
445
|
+
const key = pbkdf2Sync('peanuts', 'saltysalt', 1, 16, 'sha1')
|
|
446
|
+
return this.decryptAESCBC(encryptedData, key)
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
private execSecurityCommand(service: string, account: string): string | null {
|
|
450
|
+
try {
|
|
451
|
+
const safeService = service.replace(/"/g, '\\"')
|
|
452
|
+
const safeAccount = account.replace(/"/g, '\\"')
|
|
453
|
+
const result = execSync(
|
|
454
|
+
`security find-generic-password -s "${safeService}" -a "${safeAccount}" -w 2>/dev/null`,
|
|
455
|
+
{ encoding: 'utf8' },
|
|
456
|
+
)
|
|
457
|
+
return result.trim() || null
|
|
458
|
+
} catch {
|
|
459
|
+
return null
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private decryptAESCBC(encryptedData: Buffer, key: Buffer): string | null {
|
|
464
|
+
try {
|
|
465
|
+
const ciphertext = encryptedData.subarray(3)
|
|
466
|
+
const iv = Buffer.alloc(16, 0x20)
|
|
467
|
+
|
|
468
|
+
const decipher = createDecipheriv('aes-128-cbc', key, iv)
|
|
469
|
+
decipher.setAutoPadding(true)
|
|
470
|
+
|
|
471
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()])
|
|
472
|
+
|
|
473
|
+
// Chromium v130+ integrity hash: 32-byte non-printable prefix
|
|
474
|
+
if (decrypted.length > 32) {
|
|
475
|
+
const hasNonPrintablePrefix = decrypted.subarray(0, 32).some((b) => b < 0x20 || b > 0x7e)
|
|
476
|
+
if (hasNonPrintablePrefix) {
|
|
477
|
+
return decrypted.subarray(32).toString('utf8')
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
return decrypted.toString('utf8')
|
|
482
|
+
} catch {
|
|
483
|
+
return null
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
173
487
|
decryptDPAPI(encryptedBlob: Buffer): Buffer | null {
|
|
174
488
|
if (this.platform !== 'win32') return null
|
|
175
489
|
try {
|