diff --git a/README.md b/README.md index 478fa96..be1b819 100644 --- a/README.md +++ b/README.md @@ -145,9 +145,9 @@ On first run, macOS will show a Keychain dialog asking whether to allow access t ### How it works -1. **Cookie decryption** — Reads the encrypted `d` cookie from Slack's SQLite cookie store (`~/Library/Application Support/Slack/Cookies`). Decrypts it using the "Slack Safe Storage" key from the macOS Keychain via PBKDF2 + AES-128-CBC. +1. **Cookie decryption** — Reads the encrypted `d` cookie from Slack's SQLite cookie store (`Cookies` file). Decrypts it using the "Slack Safe Storage" key from the macOS Keychain via PBKDF2 + AES-128-CBC. Supports both direct-download and Mac App Store keychain account names. -2. **Token extraction** — Scans Slack's LevelDB storage (`~/Library/Application Support/Slack/Local Storage/leveldb/`) for `xoxc-` session tokens. Uses both direct regex scanning and a Python fallback for Snappy-compressed entries. +2. **Token extraction** — Scans Slack's LevelDB storage (`Local Storage/leveldb/`) for `xoxc-` session tokens. Uses both direct regex scanning and a Python fallback for Snappy-compressed entries. The Slack data directory is auto-detected (direct download or App Store sandbox). 3. **Validation** — Tests each candidate token against `auth.test` with the decrypted cookie. The first valid pair is used. @@ -177,8 +177,8 @@ Validated tokens are cached to avoid re-extracting on every invocation: | Data | Source | Purpose | |------|--------|---------| | Keychain password | `security find-generic-password -s "Slack Safe Storage"` | Derive AES key for cookie decryption | -| Encrypted cookie | `~/Library/Application Support/Slack/Cookies` (SQLite) | Decrypt the `d` session cookie (`xoxd-`) | -| Session token | `~/Library/Application Support/Slack/Local Storage/leveldb/` | Extract `xoxc-` token | +| Encrypted cookie | `/Cookies` (SQLite) | Decrypt the `d` session cookie (`xoxd-`) | +| Session token | `/Local Storage/leveldb/` | Extract `xoxc-` token | ## Agent usage patterns @@ -241,6 +241,7 @@ npm link # symlink globally for development ## Notes - **macOS only** — uses Keychain and Electron storage paths specific to macOS. +- **Both Slack variants supported** — works with the direct download (`~/Library/Application Support/Slack/`) and the Mac App Store version (`~/Library/Containers/com.tinyspeck.slackmacgap/.../Slack/`). The correct path is auto-detected at runtime. - **Slack desktop app required** — must be installed and logged in. The app does not need to be running for cached tokens. - **Zero dependencies** — uses only Node.js built-in modules (`crypto`, `fs`, `child_process`, `fetch`). - **Session-based** — uses `xoxc-` tokens (user session), not bot tokens. This means you act as yourself. diff --git a/package.json b/package.json index ddfdfba..cdfc750 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "slkcli", - "version": "0.1.2", + "version": "0.1.3", "description": "Slack CLI for macOS, so your agents can read and send messages", "type": "module", "license": "MIT", diff --git a/src/auth.js b/src/auth.js index b4023b5..24272bc 100644 --- a/src/auth.js +++ b/src/auth.js @@ -14,7 +14,27 @@ import { pbkdf2Sync } from "crypto"; import { existsSync, mkdirSync } from "fs"; -const SLACK_DIR = join(homedir(), "Library", "Application Support", "Slack"); +const SLACK_DIR_DIRECT = join(homedir(), "Library", "Application Support", "Slack"); +const SLACK_DIR_APPSTORE = join( + homedir(), + "Library", "Containers", "com.tinyspeck.slackmacgap", + "Data", "Library", "Application Support", "Slack" +); + +function resolveSlackDir() { + if (existsSync(SLACK_DIR_DIRECT)) return SLACK_DIR_DIRECT; + if (existsSync(SLACK_DIR_APPSTORE)) return SLACK_DIR_APPSTORE; + console.error( + "Could not find Slack data directory.\n" + + "Checked:\n" + + ` ${SLACK_DIR_DIRECT}\n` + + ` ${SLACK_DIR_APPSTORE}\n` + + "Is Slack installed?" + ); + process.exit(1); +} + +const SLACK_DIR = resolveSlackDir(); const LEVELDB_DIR = join(SLACK_DIR, "Local Storage", "leveldb"); const COOKIES_DB = join(SLACK_DIR, "Cookies"); const CACHE_DIR = join(homedir(), ".local", "slk"); @@ -23,11 +43,24 @@ const TOKEN_CACHE = join(CACHE_DIR, "token-cache.json"); let cachedCreds = null; function getKeychainKey() { - return Buffer.from( - execSync('security find-generic-password -s "Slack Safe Storage" -w', { - encoding: "utf-8", - }).trim() - ); + // Mac App Store Slack uses account "Slack App Store Key", direct download uses "Slack" + const accounts = SLACK_DIR === SLACK_DIR_APPSTORE + ? ["Slack App Store Key", "Slack"] + : ["Slack", "Slack App Store Key"]; + + for (const account of accounts) { + try { + return Buffer.from( + execSync( + `security find-generic-password -s "Slack Safe Storage" -a "${account}" -w`, + { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] } + ).trim() + ); + } catch {} + } + + console.error("Could not find Slack Safe Storage key in Keychain."); + process.exit(1); } function decryptCookie() { @@ -111,7 +144,7 @@ function extractToken() { try { const pyResult = spawnSync("python3", ["-c", ` import os, re -path = os.path.expanduser("~/Library/Application Support/Slack/Local Storage/leveldb") +path = ${JSON.stringify(LEVELDB_DIR)} for f in os.listdir(path): if not (f.endswith(".ldb") or f.endswith(".log")): continue data = open(os.path.join(path, f), "rb").read()