Skip to content
Open
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
207 changes: 43 additions & 164 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,185 +2,64 @@

## Overview

dotmask is a local HTTPS proxy that intercepts AI tool traffic and masks secrets before they leave the machine.

## Core Components

### 1. CLI (`src/cli.ts`)

Entry point for all user-facing commands:

- `install` / `uninstall` / `status` / `doctor`
- `allow` / `disallow` / `hosts`

### 2. Proxy Server (`src/proxy/server.ts`)

The core MITM proxy that handles:

- HTTPS CONNECT tunnel establishment
- Per-hostname certificate generation
- Request/response masking
- SSE streaming support

Key flow:

Dotmask is a local, one-way HTTPS redaction proxy for allowlisted AI API hosts. Its invariant is:

> Provider-controlled responses must never cause a real secret to be materialized.
## Request flow

```text
Claude Code
│ HTTPS_PROXY + NODE_EXTRA_CA_CERTS
127.0.0.1 dotmask proxy
├─ non-allowlisted host → ordinary CONNECT passthrough
└─ allowlisted host → local TLS termination
├─ parse complete HTTP/1.1 request
├─ reject compressed/binary/unsupported bodies
├─ recursively redact supported request content
├─ validate upstream TLS normally
└─ forward provider response byte-for-byte
```
Client connects to proxy
handleConnect()
├─► shouldMitmHost() → check allowed hosts
├─► Passthrough: non-allowed hosts bypass proxy
└─► MITM Mode:
├─► Generate host certificate (signed by dotmask CA)
├─► Parse HTTP request from TLS stream
├─► maskRequestBody() - mask secrets in body
├─► Forward to upstream with fake secrets
├─► Receive response
└─► unmaskText() - restore real secrets
```

### 3. Masker (`src/proxy/masker.ts`)

The core secret detection and masking logic.

#### Detection Methods
## Components

1. **Known Token Patterns** (`KNOWN_TOKEN_RE`):
- JWT: `eyJ[A-Za-z0-9\-_]{10,}\.[A-Za-z0-9\-_]{10,}\.[A-Za-z0-9\-_]{20,}`
- AWS Key ID: `AKIA[A-Z0-9]{16}`
- Stripe: `sk_live_...`, `sk_test_...`
- Anthropic: `sk-ant-api\d{2}-[A-Za-z0-9\-_+/]{20,}`
- OpenAI: `sk-proj-[A-Za-z0-9\-_+/]{20,}`
- And more...
### CLI and installation

2. **High Entropy Detection** (`isHighEntropySecret()`):
- Shannon entropy >= 3.5
- Length >= 20 chars
- Character set matches Base64/Hex patterns
`src/cli.ts` dispatches installation, status, host-management, doctor, and uninstall commands. `src/commands/install.ts` manages Claude Code's `HTTPS_PROXY` and `NODE_EXTRA_CA_CERTS` settings.

3. **Environment Variable Assignment** (`SECRET_KEY_RE`):
- Keys matching: `KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|PRIVATE|AUTH`
- Values >= 16 chars
The CA is not added as a system-trusted root. The CA private key is stored at `~/.dotmask/ca/ca.key.pem` with mode `0600`, and the CA directory uses mode `0700`.

#### Format-Preserving Fake Generation

```typescript
makeFake(real: string): string
```
### Proxy server

1. Extract prefix (known prefixes like `sk-ant-api03-`)
2. Detect charset (Base64, Hex, Alphanumeric, etc.)
3. Generate deterministic random string of same length
4. Return prefix + fake
`src/proxy/server.ts` accepts connections only on `127.0.0.1`. Exact allowlisted hostnames are MITM-intercepted; other hosts are tunneled without inspection. Upstream connections use Node TLS validation with the target hostname as SNI.

Uses seed from SHA-256 hash of original value for reproducibility.
Responses are intentionally opaque to dotmask and are never rewritten.

#### Keychain Integration
### HTTP sanitization

- **Storage**: macOS Keychain with service `dotmask`
- **Account**: Fake key value
- **Password**: Real key value
`src/proxy/http.ts` parses request framing and implements fail-closed body handling:

Cache in memory with 30s TTL to avoid excessive Keychain calls.

### 4. Certificate Management (`src/proxy/cert.ts`)

CA certificate lifecycle:

```
First run:
└─► Generate RSA-4096 CA key
└─► Generate CA cert (valid 10 years)
└─► Install to macOS Keychain
└─► Trust certificate system-wide
Per-host certificates:
└─► Generate RSA-2048 host key pair
└─► Create CSR with hostname as CN
└─► Sign with dotmask CA
└─► Cache in memory
```

### 5. Daemon Management (`src/proxy/daemon.ts`)

Uses macOS `launchd` for:

- Auto-start at login (`RunAtLoad`)
- Keep alive (`KeepAlive`)
- Port binding (default 18787)

### 6. Configuration (`src/proxy/config.ts`)

Stored at `~/.dotmask/config.json`:

```json
{
"allowedHosts": [
"api.anthropic.com",
"api.openai.com"
]
}
```

## Data Flow

### Request Masking

```
1. Claude Code sends request to api.anthropic.com
2. dotmask intercepts via HTTPS_PROXY
3. Parse request body as JSON
4. maskText() scans for secrets:
a. Check against known patterns
b. Check registered (real→fake) mappings
c. Check high-entropy strings
d. Check env-var style assignments
5. For each found secret:
a. Generate fake with makeFake()
b. Store (real, fake) in Keychain
c. Replace real with fake in body
6. Forward modified request to upstream
```

### Response Unmasking

```
1. Upstream returns response with fake keys
2. Parse response
3. For each known fake key (from cache):
a. Lookup real value in Keychain
b. Replace fake with real
4. Return unmasked response to Claude Code
```
- JSON and `+json` bodies must parse as an object or array.
- Text, form, GraphQL, and XML bodies are scanned as UTF-8 text.
- Compressed, binary, and unsupported bodies are rejected.
- Content length is recalculated after redaction.

## File Locations
### Masker

| Path | Purpose |
|------|---------|
| `~/.dotmask/config.json` | Allowed hosts configuration |
| `~/.dotmask/ca/ca.pem` | CA certificate (public) |
| `~/.dotmask/ca/ca.key` | CA private key (secret!) |
| `~/.dotmask/maps/*.json` | List of masked (fake) keys |
| `~/.dotmask/proxy.err.log` | Error/debug log |
| `~/.dotmask/proxy.log` | General log |
| `~/.claude/settings.json` | Claude Code settings (injected) |
`src/proxy/masker.ts` recursively visits every string leaf in JSON. Detection combines:

## Dependencies
- Known credential formats such as common AI, cloud, GitHub, Slack, JWT, database, and private-key tokens.
- Secret-looking property and environment-variable names.
- High-entropy token candidates, including encoded tool output.

### Runtime
Fakes preserve length and known structural prefixes. Unknown formats preserve no plaintext prefix. A random process-local HMAC key makes output stable during one daemon run without making it predictable to the provider.

- `node:http` - HTTP server
- `node:https` - HTTPS (not used directly, TLS via node:tls)
- `node:tls` - TLS termination
- `node:crypto` - Key generation, hashing
- `node:child_process` - OpenSSL wrapper
Mappings exist only in the `Map` passed through one request traversal. Dotmask 2 performs no Keychain writes and stores no real-secret mapping on disk.

### Build
## Legacy migration

- `typescript` - TypeScript compilation
- `@types/node` - Node.js types
Version 1 persisted fake-to-real mappings and installed the CA into the login Keychain. Version 2 no longer uses either mechanism. Installation removes legacy CA trust; uninstall additionally deletes identifiable legacy Keychain mappings and `~/.dotmask` after successful daemon, trust, and settings cleanup.
103 changes: 38 additions & 65 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,98 +1,71 @@
# dotmask

mask secrets before they leave your machine.
Best-effort, one-way secret redaction for AI API requests on macOS.

`dotmask` runs a local HTTPS proxy for Claude Code and similar tools. it replaces real secrets with format-preserving fakes on the way out, then restores them locally on the way back.
`dotmask` runs a local HTTPS proxy for Claude Code. Before a supported request reaches an allowed AI provider, it replaces recognized credentials and high-entropy values with keyed, format-preserving fakes. Provider responses are forwarded unchanged: dotmask never inserts a real secret into assistant text or model-generated tool arguments.

## install
## Install

```bash
npm install -g @ducnmm/dotmask
dotmask install
```

restart Claude Code after install.
Restart Claude Code after installation. The generated proxy CA is scoped to Claude Code with `NODE_EXTRA_CA_CERTS`; dotmask does not install a system-trusted root.

## use
Version 2 is intentionally incompatible with the old response-unmasking behavior. Tool calls that contain a fake credential must obtain the real credential from a trusted local environment at execution time.

use Claude Code like normal. dotmask runs automatically after install.
## Commands

## supported providers
- `dotmask install [--port <n>]` — install and start the proxy
- `dotmask allow <host>` — add an intercepted provider hostname
- `dotmask disallow <host>` — remove an intercepted hostname
- `dotmask hosts` — list intercepted hostnames
- `dotmask status` — show proxy status
- `dotmask doctor` — diagnose installation issues
- `dotmask uninstall` — remove the daemon, settings, legacy trust, certificates, mappings, and logs

- `api.anthropic.com` - Anthropic (Claude)
- `api.openai.com` - OpenAI (GPT)
- `openrouter.ai`, `api.openrouter.ai` - OpenRouter
- `generativelanguage.googleapis.com` - Google AI (Gemini)
- `api.deepseek.com` - DeepSeek
- `api.groq.com` - Groq
- `api.moonshot.ai` - Moonshot (Kimi)
- `api.together.ai` - Together AI
- `api.fireworks.ai` - Fireworks AI
- `api.cerebras.ai` - Cerebras
- `api.x.ai` - xAI (Grok)
- `api.inference.huggingface.co` - Hugging Face
- `api.minimax.io`, `api.minimax.chat` - MiniMax
## Security behavior

`~/.dotmask/config.json` controls the allowed host list.
For allowlisted hosts, dotmask:

add a custom host with:
1. Terminates the client's TLS connection locally.
2. Rejects compressed, binary, malformed JSON, and unsupported request bodies instead of forwarding them uninspected.
3. Recursively scans every string in JSON requests, plus supported textual request bodies.
4. Replaces known token formats, secret-named properties, environment assignments, and high-entropy values.
5. Forwards provider responses byte-for-byte without restoring secrets.

```bash
dotmask allow chat.trollllm.xyz
```

## commands
Fakes are derived with a random, process-local HMAC key. They are stable during one proxy run but cannot be recomputed offline by the provider. Real-to-fake mappings are request-local and real secrets are not persisted by dotmask.

- `dotmask install` - install proxy
- `dotmask install --port 18788` - custom port
- `dotmask allow <host>` - add allowed host
- `dotmask disallow <host>` - remove host
- `dotmask hosts` - list allowed hosts
- `dotmask status` - show status
- `dotmask doctor` - diagnose issues
- `dotmask uninstall` - remove everything
## Supported providers

## how it works
The default allowlist includes Anthropic, OpenAI, OpenRouter, Google AI, DeepSeek, Groq, Moonshot, Together, Fireworks, Cerebras, xAI, Hugging Face, and MiniMax endpoints. `~/.dotmask/config.json` controls the exact list.

1. your prompt with API keys goes to Claude Code
2. dotmask intercepts and replaces real keys with fakes
3. fake keys go to the AI API - API thinks its valid
4. response comes back with fake keys
5. dotmask swaps fakes back to real keys
6. Claude Code sees the real response
## Important limitations

your secrets never leave your machine.
Dotmask reduces accidental disclosure; it is not a sandbox, DLP system, or authorization boundary.

## supported secrets
- Provider authentication headers are not masked because the provider requires them. The provider receives those credentials, although the model normally does not.
- Traffic to non-allowlisted hosts is passed through without inspection.
- A model with filesystem or shell-tool permission may read or exfiltrate local data outside the AI request path. Use tool approvals, sandboxing, least-privilege credentials, and network controls.
- Pattern and entropy detection can have false negatives and false positives. Unsupported request formats are blocked for intercepted hosts.
- Fakes deliberately cannot be converted back into real credentials by model output. Generated commands should reference local environment variables or another trusted credential mechanism.

- Anthropic keys: `sk-ant-api03-...`
- OpenAI keys: `sk-proj-...`, `sk-...`
- Stripe: `sk_live_...`, `sk_test_...`
- AWS: `AKIA...`
- Google AI: `AIza...`
- GitHub PATs: `ghp_...`, `gho_...`, `github_pat_...`
- Slack: `xoxb-...`, `xoxp-...`
- JWT tokens
- Database URLs: `postgres://user:pass@...`
- EVM private keys: `0x...` (64 chars)

## debugging
## Debugging

```bash
# view logs
tail -f ~/.dotmask/proxy.err.log

# run manually with debug
DOTMASK_DEBUG=1 node dist/proxy/server.js --port 18787
```

## notes
Debug logs contain counts and connection metadata, not request or response bodies.

## Requirements

- macOS only
- macOS
- Node.js 18+
- `openssl` required
- secrets stored in macOS Keychain
- `openssl`

## license
## License

MIT
MIT
Loading