From 90aa962c808435d99fd0733ca4c7bf3aadd10f0e Mon Sep 17 00:00:00 2001 From: Jeon Suyeol Date: Thu, 5 Mar 2026 21:25:21 +0900 Subject: [PATCH 1/2] Add Linux cookie decryption for Slack Linux Chromium uses a hardcoded 'peanuts' password for cookie encryption. The Teams extractor already had this but Slack was missing it, causing all Linux users to get empty cookies and auth failures. --- src/platforms/slack/token-extractor.test.ts | 39 +++++++++++++++++++++ src/platforms/slack/token-extractor.ts | 20 +++++++++++ 2 files changed, 59 insertions(+) diff --git a/src/platforms/slack/token-extractor.test.ts b/src/platforms/slack/token-extractor.test.ts index e354371..8d113ae 100644 --- a/src/platforms/slack/token-extractor.test.ts +++ b/src/platforms/slack/token-extractor.test.ts @@ -116,6 +116,45 @@ describe('TokenExtractor LevelDB fragmentation markers', () => { }) }) +describe('TokenExtractor Linux cookie decryption', () => { + test('decrypts v10 cookie using peanuts password on Linux', async () => { + // given — LevelDB with valid token + v10-encrypted cookie using Linux key + const slackDir = mkdtempSync(join(tmpdir(), 'slack-linux-')) + tempDirs.push(slackDir) + + const cookiePlaintext = 'xoxd-linuxTestCookie%2Bvalue' + const key = require('node:crypto').pbkdf2Sync('peanuts', 'saltysalt', 1, 16, 'sha1') + const iv = Buffer.alloc(16, ' ') + const cipher = createCipheriv('aes-128-cbc', key, iv) + const ciphertext = Buffer.concat([cipher.update(cookiePlaintext, 'utf8'), cipher.final()]) + const encryptedCookie = Buffer.concat([Buffer.from('v10'), ciphertext]) + + createCookiesDb(join(slackDir, 'Cookies'), [ + { + name: 'd', + value: '', + encrypted_value: new Uint8Array(encryptedCookie), + host_key: '.slack.com', + last_access_utc: 1, + }, + ]) + + const token = `xoxc-1111111111-2222222222-3333333333-${'a'.repeat(64)}` + const leveldbDir = join(slackDir, 'Local Storage', 'leveldb') + mkdirSync(leveldbDir, { recursive: true }) + writeFileSync(join(leveldbDir, '000001.log'), `"${token}"T12345678"name":"test-workspace"`) + + // when + const extractor = new TokenExtractor('linux', slackDir) + const result = await extractor.extract() + + // then + expect(result.length).toBe(1) + expect(result[0].cookie).toBe(cookiePlaintext) + expect(result[0].token).toBe(token) + }) +}) + describe('TokenExtractor Windows DPAPI', () => { test('decryptDPAPI returns null on non-win32 platform', () => { const extractor = new TokenExtractor('darwin', '/tmp/slack-test') diff --git a/src/platforms/slack/token-extractor.ts b/src/platforms/slack/token-extractor.ts index f240d9d..4c4774b 100644 --- a/src/platforms/slack/token-extractor.ts +++ b/src/platforms/slack/token-extractor.ts @@ -487,6 +487,9 @@ export class TokenExtractor { if (this.platform === 'win32') { return this.decryptV10CookieWindows(encrypted) } + if (this.platform === 'linux') { + return this.decryptV10CookieLinux(encrypted) + } return this.decryptV10Cookie(encrypted) } @@ -526,6 +529,23 @@ export class TokenExtractor { } } + private decryptV10CookieLinux(encrypted: Buffer): string | null { + try { + const key = pbkdf2Sync('peanuts', 'saltysalt', 1, 16, 'sha1') + const iv = Buffer.alloc(16, ' ') + const ciphertext = encrypted.subarray(3) + + const decipher = createDecipheriv('aes-128-cbc', key, iv) + const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]) + const result = decrypted.toString('utf8') + + const match = result.match(/xoxd-[A-Za-z0-9%]+/) + return match ? match[0] : null + } catch { + return null + } + } + decryptV10CookieWindows(encrypted: Buffer): string | null { try { const masterKey = this.getWindowsMasterKey() From ba35dff12e61094b15c1966936d0bfcf14b16288 Mon Sep 17 00:00:00 2001 From: Jeon Suyeol Date: Thu, 5 Mar 2026 21:25:27 +0900 Subject: [PATCH 2/2] Add Linux token decryption for Discord MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same missing Linux decryption as Slack — encrypted tokens on Linux use the Chromium hardcoded 'peanuts' password but Discord had no Linux path, returning null for all encrypted tokens. --- src/platforms/discord/token-extractor.test.ts | 25 +++++++++++++++++++ src/platforms/discord/token-extractor.ts | 7 ++++++ 2 files changed, 32 insertions(+) diff --git a/src/platforms/discord/token-extractor.test.ts b/src/platforms/discord/token-extractor.test.ts index e5499f4..1764b8a 100644 --- a/src/platforms/discord/token-extractor.test.ts +++ b/src/platforms/discord/token-extractor.test.ts @@ -79,6 +79,31 @@ describe('DiscordTokenExtractor', () => { }) }) + describe('Linux token decryption', () => { + test('decrypts encrypted token using peanuts password on Linux', () => { + // given — AES-128-CBC encrypted token with Linux Chromium key + const { createCipheriv, pbkdf2Sync } = require('node:crypto') + const plainToken = 'XXXXXXXXXXXXXXXXXXXXXXXX.YYYYYY.ZZZZZZZZZZZZZZZZZZZZZZZZZ' + const key = pbkdf2Sync('peanuts', 'saltysalt', 1, 16, 'sha1') + const iv = Buffer.alloc(16, 0x20) + const cipher = createCipheriv('aes-128-cbc', key, iv) + const ciphertext = Buffer.concat([cipher.update(plainToken, 'utf8'), cipher.final()]) + // v10 prefix (3 bytes) + ciphertext + const encrypted = Buffer.concat([Buffer.from('v10'), ciphertext]) + const encryptedToken = `dQw4w9WgXcQ:${encrypted.toString('base64')}` + + const linuxExtractor = new DiscordTokenExtractor('linux') + const decryptTokenSpy = spyOn(linuxExtractor as any, 'decryptToken') + decryptTokenSpy.mockRestore() + + // when + const result = (linuxExtractor as any).decryptToken(encryptedToken, '/home/user/.config/discord') + + // then + expect(result).toBe(plainToken) + }) + }) + describe('extract', () => { test('returns null when no Discord directories exist on linux', async () => { const linuxExtractor = new DiscordTokenExtractor('linux') diff --git a/src/platforms/discord/token-extractor.ts b/src/platforms/discord/token-extractor.ts index 4ff4724..12a4cdf 100644 --- a/src/platforms/discord/token-extractor.ts +++ b/src/platforms/discord/token-extractor.ts @@ -260,6 +260,8 @@ export class DiscordTokenExtractor { return this.decryptWindowsToken(encryptedData, discordDir) } else if (this.platform === 'darwin') { return this.decryptMacToken(encryptedData, discordDir) + } else if (this.platform === 'linux') { + return this.decryptLinuxToken(encryptedData) } return null @@ -335,6 +337,11 @@ export class DiscordTokenExtractor { return null } + private decryptLinuxToken(encryptedData: Buffer): string | null { + const key = pbkdf2Sync('peanuts', 'saltysalt', 1, 16, 'sha1') + return this.decryptAESCBC(encryptedData, key) + } + private decryptAESCBC(encryptedData: Buffer, key: Buffer): string | null { try { const ciphertext = encryptedData.subarray(3)