Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/platforms/slack/commands/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { TokenExtractor } from '../token-extractor'

async function extractAction(options: { pretty?: boolean; debug?: boolean }): Promise<void> {
try {
const extractor = new TokenExtractor()
const debugLog = options.debug ? (msg: string) => console.error(`[debug] ${msg}`) : undefined
const extractor = new TokenExtractor(undefined, undefined, undefined, debugLog)

if (process.platform === 'darwin') {
console.log('')
Expand Down
31 changes: 31 additions & 0 deletions src/platforms/slack/token-extractor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,37 @@ describe('TokenExtractor Linux cookie decryption', () => {
})
})

describe('TokenExtractor debug logging', () => {
test('calls debugLog callback during extraction', async () => {
// given
const slackDir = mkdtempSync(join(tmpdir(), 'slack-debug-'))
tempDirs.push(slackDir)
mkdirSync(join(slackDir, 'storage'), { recursive: true })

const messages: string[] = []
const debugLog = (msg: string) => messages.push(msg)

// when
const extractor = new TokenExtractor('darwin', slackDir, undefined, debugLog)
await extractor.extract()

// then — should have emitted debug messages
expect(messages.length).toBeGreaterThan(0)
})

test('does not throw when debugLog is not provided', async () => {
// given
const slackDir = mkdtempSync(join(tmpdir(), 'slack-no-debug-'))
tempDirs.push(slackDir)
mkdirSync(join(slackDir, 'storage'), { recursive: true })

// when — then — should not throw
const extractor = new TokenExtractor('darwin', slackDir)
const result = await extractor.extract()
expect(result).toEqual([])
})
})

describe('TokenExtractor Windows DPAPI', () => {
test('decryptDPAPI returns null on non-win32 platform', () => {
const extractor = new TokenExtractor('darwin', '/tmp/slack-test')
Expand Down
33 changes: 30 additions & 3 deletions src/platforms/slack/token-extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,14 @@ export class TokenExtractor {
private platform: NodeJS.Platform
private slackDir: string
private keyCache: DerivedKeyCache

constructor(platform?: NodeJS.Platform, slackDir?: string, keyCache?: DerivedKeyCache) {
private debugLog: ((message: string) => void) | null

constructor(
platform?: NodeJS.Platform,
slackDir?: string,
keyCache?: DerivedKeyCache,
debugLog?: (message: string) => void,
) {
this.platform = platform ?? process.platform

if (!['darwin', 'linux', 'win32'].includes(this.platform)) {
Expand All @@ -36,6 +42,11 @@ export class TokenExtractor {

this.slackDir = slackDir ?? this.getSlackDir()
this.keyCache = keyCache ?? new DerivedKeyCache()
this.debugLog = debugLog ?? null
}

private debug(message: string): void {
this.debugLog?.(message)
}

getSlackDir(): string {
Expand Down Expand Up @@ -406,10 +417,13 @@ export class TokenExtractor {
if (!existsSync(cookiesPath)) {
const networkCookiesPath = join(this.slackDir, 'Network', 'Cookies')
if (!existsSync(networkCookiesPath)) {
this.debug(`Cookie file not found at ${cookiesPath} or ${networkCookiesPath}`)
return ''
}
this.debug(`Using Network cookies path: ${networkCookiesPath}`)
return this.readCookieFromDB(networkCookiesPath)
}
this.debug(`Using cookies path: ${cookiesPath}`)
return this.readCookieFromDB(cookiesPath)
}

Expand All @@ -427,6 +441,7 @@ export class TokenExtractor {
'Quit the Slack app completely and try again.',
)
}
this.debug(`Failed to copy cookie DB: ${(error as Error).message}`)
return ''
}

Expand All @@ -453,22 +468,29 @@ export class TokenExtractor {
}

if (!row) {
this.debug('No cookie row found in database')
return ''
}

if (row.value?.startsWith('xoxd-')) {
this.debug('Found plaintext cookie')
return row.value
}

if (row.encrypted_value && row.encrypted_value.length > 0) {
this.debug(`Found encrypted cookie (${row.encrypted_value.length} bytes)`)
const decrypted = this.tryDecryptCookie(Buffer.from(row.encrypted_value))
if (decrypted) {
this.debug('Cookie decrypted successfully')
return decrypted
}
this.debug('Cookie decryption failed')
}

this.debug('No usable cookie value in row')
return ''
} catch {
} catch (error) {
this.debug(`Cookie DB query failed: ${(error as Error).message}`)
return ''
} finally {
try {
Expand Down Expand Up @@ -629,13 +651,15 @@ export class TokenExtractor {

private async getDerivedKeyAsync(): Promise<Buffer | null> {
if (this.platform !== 'darwin') {
this.debug(`Skipping Keychain key derivation (platform: ${this.platform})`)
return null
}

const cached = await this.keyCache.get('slack')
if (cached) {
this.cachedKey = cached
this.usedCachedKey = true
this.debug('Using cached derived key')
return cached
}

Expand All @@ -644,6 +668,9 @@ export class TokenExtractor {
this.cachedKey = key
await this.keyCache.set('slack', key)
this.usedCachedKey = false
this.debug('Derived key from Keychain')
} else {
this.debug('Failed to derive key from Keychain')
}
return key
}
Expand Down