# How gstack Handles Cookie Security and Keychain Access: Cross-Platform Chromium Cookie Import

> Learn how gstack secures cookie handling and Keychain access cross-platform. It imports Chromium cookies securely, protecting raw values and using AES keys safely in memory.

- Repository: [Garry Tan/gstack](https://github.com/garrytan/gstack)
- Tags: internals
- Published: 2026-05-15

---

**gstack implements a self-contained, cross-platform Chromium cookie importer that isolates credential handling by querying the macOS Keychain or Linux libsecret via audited shell commands, caching derived AES keys in memory only, and ensuring raw cookie values never appear in the user interface.**

The `garrytan/gstack` repository provides a CLI tool for importing browser cookies into Playwright-compatible formats. Its architecture prioritizes **cookie security** by separating the decryption logic from the main application and strictly controlling how credentials are retrieved from system secret stores.

## The Chromium Cookie Import Pipeline

The import process in [`browse/src/cookie-import-browser.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/cookie-import-browser.ts) follows a five-stage pipeline that minimizes exposure of sensitive material.

### Profile Discovery Without User Input

The system locates browser profiles through `findInstalledBrowsers()`, `listProfiles()`, and `getBrowserMatch()`. These functions traverse standard data directories for Chromium-based browsers—including Chrome, Chromium, Arc, Brave, Edge, and Comet—using the hard-coded `BROWSER_REGISTRY` array. This design deliberately rejects user input for browser identification, preventing path traversal attacks.

### Read-Only Database Access

The `openDb()` function opens the `Cookies` SQLite database in read-only mode on macOS and Linux. On Windows, it creates a temporary copy to avoid Write-Ahead Logging (WAL) lock contention, ensuring the original database remains unmodified.

### Key Derivation from macOS Keychain and Linux libsecret

The most security-critical phase derives the AES-128-CBC decryption key using platform-specific secret stores:

- **macOS (v10 cookies)** – Executes `security find-generic-password -s <service>` to retrieve the password from the user's **Keychain**. The service name (e.g., "Chrome Safe Storage") originates from the hard-coded `keychainService` field, never from user input.

- **Linux (v10)** – Uses the static string `"peanuts"` as the password, consistent with Chromium's legacy implementation.

- **Linux (v11)** – Queries **libsecret** via `secret-tool lookup` (or XDG schema variants) to obtain the stored password.

- **Windows** – Leverages DPAPI through Node/Bun crypto APIs.

The implementation performs PBKDF2 key derivation using 1,003 iterations on macOS and 1 iteration on Linux. The resulting key is cached in a module-level `keyCache` (a `Map<string, Buffer>`) keyed by platform and browser service name, ensuring expensive Keychain or secret-tool invocations occur only once per session.

### AES Decryption in Memory

The `decryptCookieValue()` function processes only cookies with `encrypted_value` headers matching `v10` or `v11`. It constructs the AES-128-CBC parameters using an initialization vector of 16 bytes set to `0x20`, decrypts the ciphertext (bytes following the version prefix), and strips the 32-byte Chromium metadata before returning the plaintext. This operation occurs entirely in memory without intermediate disk storage.

### Sanitized Output to UI

The final `ImportResult` object contains Playwright-compatible cookie objects and per-domain statistics, but **never the raw key material**. The `setup-browser-cookies` skill documentation confirms the UI displays only domain names and counts, with an explicit guarantee that no cookie values are shown.

## Security-Focused Implementation Details

### Command Injection Prevention

All shell invocations use static string templates. The source code contains an explicit comment enforcing: *"Hardcoded — NEVER interpolate user input into shell commands."* This applies to the `security` CLI on macOS and `secret-tool` on Linux, eliminating injection vectors.

### Credential Lifecycle Management

Passwords retrieved from the macOS Keychain or Linux libsecret exist only in volatile memory during the decryption session. The implementation guarantees these values are never logged to stdout, written to disk, or included in error traces.

### Platform-Specific Secret Handling

| Platform | Mechanism | Implementation |
|----------|-----------|----------------|
| **macOS** | Keychain | `security find-generic-password` CLI |
| **Linux v11** | libsecret | `secret-tool lookup` or XDG schema queries |
| **Windows** | DPAPI | Native Node/Bun crypto APIs |

### Key Reuse and Caching

The `keyCache` Map prevents repeated authentication prompts by storing derived AES keys in memory. Each entry is keyed by a composite string including the platform identifier and browser service name, balancing performance with the security requirement to minimize Keychain access frequency.

## Practical Usage Examples

```bash

# Import all cookies from the default Chrome profile

$B cookie-import-browser

# Import only github.com cookies from Brave's Default profile

$B browser-import-browser brave --domain github.com --profile "Default"

# Skip interactive picker and import from Comet's Profile 2

$B cookie-import-browser comet --profile "Profile 2" --domain example.com

```

These commands invoke the `cookie-import-browser` binary, which internally executes the secure pipeline described above without exposing credentials in process lists or logs.

## Key Source Files

- **[`browse/src/cookie-import-browser.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/cookie-import-browser.ts)** – Core implementation containing `BROWSER_REGISTRY`, `openDb()`, and the Keychain/libsecret integration logic.

- **[`setup-browser-cookies/SKILL.md`](https://github.com/garrytan/gstack/blob/main/setup-browser-cookies/SKILL.md)** – User-facing documentation defining the skill interface, explaining the Linux `secret-tool` prerequisite, and confirming that cookie values remain hidden from the UI.

- **[`browse/test/cookie-import-browser.test.ts`](https://github.com/garrytan/gstack/blob/main/browse/test/cookie-import-browser.test.ts)** – Test suite validating the macOS Keychain and Linux secret-tool pathways through mocks, ensuring the security logic functions correctly across platforms.

## Summary

- gstack isolates cookie decryption in [`browse/src/cookie-import-browser.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/cookie-import-browser.ts), separate from Playwright and HTTP handling code.
- **macOS Keychain** access uses the hardened `security` CLI with hardcoded service names, while **Linux libsecret** integration relies on `secret-tool` for v11 cookies.
- Derived AES keys are cached in a `keyCache` Map to minimize authentication prompts, but credentials never persist to disk or logs.
- The UI layer in [`setup-browser-cookies/SKILL.md`](https://github.com/garrytan/gstack/blob/main/setup-browser-cookies/SKILL.md) displays only domain counts, guaranteeing cookie values remain confidential.
- All shell commands use static strings to prevent command injection, with explicit guards against user input interpolation.

## Frequently Asked Questions

### How does gstack access the macOS Keychain without prompting the user repeatedly?

According to the source code in [`browse/src/cookie-import-browser.ts`](https://github.com/garrytan/gstack/blob/main/browse/src/cookie-import-browser.ts), gstack caches derived AES keys in a module-level `keyCache` Map keyed by platform and browser service name. This ensures the `security find-generic-password` command runs only once per session, after which the derived key resides in memory for subsequent decryptions.

### Why does gstack require `secret-tool` on Linux but not macOS?

Linux v11 cookies use libsecret (via `secret-tool` or XDG schema lookups) to retrieve the master password stored by Chromium, whereas macOS v10 cookies use the native Keychain. The Linux v10 implementation uses a static derivation string ("peanuts"), but modern Chromium versions on Linux require the dynamic secret lookup that libsecret provides.

### Are imported cookie values ever written to disk or displayed in the terminal?

No. The `decryptCookieValue()` function processes cookies entirely in memory, and the `setup-browser-cookies` skill explicitly documents that the UI shows only domain names and counts. Raw cookie values and Keychain passwords are never logged, written to temporary files, or exposed in error messages.

### What prevents malicious input from injecting commands during cookie import?

The implementation uses a hard-coded `BROWSER_REGISTRY` array to identify browsers, and all shell commands (including `security` and `secret-tool` invocations) are static strings. The source code contains an explicit comment prohibiting user input interpolation into command arguments, eliminating command injection vulnerabilities.