This document is for security engineers evaluating whether Phantom meets their team's security bar. It describes the assets Phantom protects, the threat actors it considers, the mitigations in place (with code citations), known gaps, and the cryptographic primitives used. It does not describe the product's feature set — see the README for that.
For responsible disclosure, see SECURITY.md.
- Asset Inventory
- Threat Actors
- Mitigations by Asset and Threat
- Cryptography Summary
- Trust Boundaries
- Out of Scope
- Known Gaps and Non-Mitigations
- Reporting a Vulnerability
The actual API keys, tokens, passwords, and database URLs that Phantom is asked to manage. These are the primary target of any attacker.
Storage locations:
- OS keychain (primary): macOS Keychain, Linux Secret Service, or Windows Credential Manager, depending on platform. The OS keychain gates access by the user's session credentials and, on macOS, by Secure Enclave on supported hardware.
- Encrypted file vault (fallback when no OS keychain is available):
~/.phantom/vaults/<project_id>.vault. The file is encrypted with ChaCha20-Poly1305 keyed via Argon2id. Permissions are set to0600on Unix. See §4. - Phantom Cloud (optional): Server stores only ciphertext. Encryption key is derived from a key in the OS keychain and never leaves the device.
Sensitivity: Highest. Compromise allows an attacker to impersonate the developer to third-party services.
A 32-byte (256-bit) CSPRNG value generated fresh each time phantom exec starts the local proxy. The proxy accepts it through the x-phantom-proxy-token request header. For generic SDK compatibility, phantom exec and phantom start include it in local *_BASE_URL values as /_phantom/<token>/; set PHANTOM_PROXY_HEADER_AUTH_ONLY=1 to emit token-free URLs and require the header path.
Sensitivity: High during a session. Compromising it allows a local process to use the running proxy to obtain real secrets injected into outbound requests. The token is ephemeral — it disappears when the proxy process exits.
Each team member generates a long-lived X25519 keypair. The private key is stored in the OS keychain. The public key is published to the Phantom Cloud team record. When a team member pushes a vault, they encrypt a per-push symmetric key to each member's X25519 public key. Only holders of the matching private key can decrypt their share and recover the vault contents.
Sensitivity: High. Compromise allows decryption of any team vault push that included the member as a recipient.
Contains project ID, service mappings (which upstream URLs map to which secret keys), vault backend preference, cloud sync settings, and team configuration. Does not contain secret values.
Sensitivity: Low for confidentiality; moderate for integrity. A tampered .phantom.toml could redirect the proxy to an attacker-controlled upstream, or remove service mappings causing secrets to stop being injected (denial of service).
Stored at ~/.phantom/audit.log when PHANTOM_AUDIT=1. Contains JSONL records of vault operations: monotonic sequence number, timestamp, operation name, secret name (never value), process name, and PID. Signed entries include an HMAC-SHA256 chain over the previous entry hash.
Sensitivity: Low for confidentiality (names only, no values). Moderate for integrity — a tampered or deleted log undermines incident response.
A GitHub OAuth token scoped to the user's GitHub identity, used to authenticate against the Phantom Cloud API. Stored in the OS keychain under the phantom-secrets service prefix.
Sensitivity: Moderate. Compromise allows an attacker to push a malicious encrypted vault blob to the cloud (overwriting legitimate data) or to pull the encrypted blob (though they cannot decrypt it without the vault encryption key, which is separate). It does not directly expose plaintext secrets.
An AI coding agent operating in the developer's environment is given or constructs a malicious prompt that attempts to exfiltrate secrets. This includes:
- Prompt injection via user-supplied content that instructs the agent to call
phantom_add_secretwith a value parameter. - A malicious or compromised MCP server that impersonates Phantom tools and attempts to harvest values passed to it.
- An AI agent that reads
.envand finds only phantom tokens, then attempts to resolve them by calling the proxy directly.
This is the primary threat actor Phantom was designed to address.
Another process running under the same OS user account that:
- Reads files accessible to the user (
.env,.phantom.toml, vault files). - Connects to the localhost proxy port.
- Enumerates keychain entries to discover secret names.
This actor represents rogue software (malware, a compromised npm dependency, a malicious VSCode extension) running with the same effective UID.
A process or person with root access to the developer's machine. This is explicitly out of scope — see §6. Any local secret manager is defeated by root access.
An external attacker who:
- Compromises the Phantom Cloud backend (server-side breach).
- Intercepts network traffic between the client and the cloud API.
- Steals the user's GitHub OAuth token and calls cloud API endpoints.
A malicious or backdoored dependency in the project's software supply chain (npm package, Rust crate, system tool) that executes code in the developer's environment. Treated similarly to Threat Actor 2.2 from a capability standpoint.
An AI-generated or attacker-controlled pull request that adds or modifies:
.envfiles to introduce real secrets (instead of phantom tokens)..phantom.tomlto redirect service mappings to attacker-controlled upstream URLs.- GitHub Actions workflows or other CI config to capture secrets at runtime.
This actor has write access to the repository contents but not to the developer's local machine or vault.
A current or former team member who has a valid registered X25519 public key and legitimate access to the team vault. This actor can pull any team vault push that includes their key as a recipient. The threat is key retention after offboarding — a removed member whose key was included in a past push can still decrypt that push's ciphertext if they retained the private key.
The table below is the primary reference. A mitigation is marked covered only when there is a code path implementing it. Partial means the mechanism exists but has documented limitations. Not covered means no mitigation is in place today.
| Asset | Threat | Mitigation | Status | Code reference |
|---|---|---|---|---|
| Real secret values | LLM reads .env |
.env contains only phantom tokens (phm_ + 64 hex chars); real values never written to .env |
Covered | crates/phantom-core/src/dotenv.rs — token substitution on phantom init / phantom sync |
| Real secret values | LLM calls phantom_add_secret with plaintext value |
MCP tool unconditionally refuses any call that includes a value; returns error directing caller to phantom_add_secret_interactive |
Covered | crates/phantom-mcp/src/server.rs:227–240 |
| Real secret values | LLM calls destructive MCP tools without user consent | All mutating MCP tools require confirm: true parameter; tool description instructs the agent to ask the user first |
Covered | crates/phantom-mcp/src/server.rs — require_confirm() called at every mutating entry point |
| Real secret values | Local process reads vault file | File vault encrypted with ChaCha20-Poly1305 + Argon2id (m=64 MiB); file permissions 0600; attacker needs passphrase |
Covered | crates/phantom-vault/src/crypto.rs, crates/phantom-vault/src/file.rs:130 |
| Real secret values | Local process reads OS keychain entries | OS keychain access is gated by the session login. Secret names are stored as SHA-256 hashes (first 8 bytes, hex-encoded) so enumeration does not reveal which secrets are stored | Covered | crates/phantom-vault/src/keychain.rs:12–18 (hashing); OS keychain access control is enforced by the platform |
| Real secret values | Proxy used without a session token to extract secrets | Proxy validates the session token on every request using constant-time comparison; unauthenticated requests receive HTTP 401. CLI-generated URLs use a local /_phantom/<token>/ path segment for SDK compatibility unless PHANTOM_PROXY_HEADER_AUTH_ONLY=1 is set. |
Covered | crates/phantom-proxy/src/server.rs, proxy auth regression tests |
| Real secret values | Proxy token brute-forced via timing side-channel | Comparison uses subtle::ConstantTimeEq; length mismatch returns early (token length is not secret) |
Covered | crates/phantom-proxy/src/server.rs:620–623 |
| Real secret values | Secret injected into non-auth fields (prompt injection via body) | F9 scoping: for JSON bodies, token substitution is restricted to a whitelist of known-secret fields; tokens in prompt, messages, content fields are left as phantom tokens and not substituted |
Covered | crates/phantom-proxy/src/server.rs:444–456 and body_scope.rs |
| Real secret values | Secret injected into non-auth headers (e.g. User-Agent) | F9 scoping: header substitution restricted to auth-bearing headers and the per-route configured header | Covered | crates/phantom-proxy/src/server.rs:282–319 |
| Real secret values | Secret leaked in upstream API response back to LLM | Response body (buffered and streaming) is scanned and secrets scrubbed before returning to the caller | Covered | crates/phantom-proxy/src/server.rs:533–610 |
| Real secret values | Memory exposure after use | All secret values wrapped in zeroize::Zeroizing<T> which overwrites the heap buffer on drop |
Covered | crates/phantom-vault/src/file.rs:88,114, crates/phantom-vault/src/keychain.rs:155 |
| Real secret values | Body too large for safe buffering | Buffered path capped at 10 MB (max_body_size); exceeding returns HTTP 413 |
Covered | crates/phantom-proxy/src/server.rs:418–441 |
| Real secret values | Cloud server compromised — server reads plaintext | Vault is encrypted client-side before upload; server stores only ciphertext; encryption key never transmitted | Covered | crates/phantom-mcp/src/server.rs:356–363 (cloud push encryption) |
| Real secret values | Supply-chain attack injects .env real secrets |
phantom check (including --staged) scans for real secret patterns and warns before commit; pre-commit hook integration |
Covered | crates/phantom-cli/src/commands/check.rs (invoked by phantom check --staged) |
| Proxy session token | Sniffed on localhost | Token is only ever transmitted over the loopback interface (127.0.0.1), which is not network-accessible | Covered | crates/phantom-proxy/src/server.rs:66 — bind to [127, 0, 0, 1] only |
| Proxy session token | Leaked via process environment to child | The proxy token is set in the environment of the phantom exec child process as PHANTOM_PROXY_TOKEN; any subprocess spawned by that child can read it. This is intentional — the child needs it — but a compromised child process can use it |
Partial — by design; mitigated by proxy's localhost-only binding and ephemeral token lifetime | |
| Proxy session token | Token persists after session ends | Token is generated fresh on each phantom exec invocation and exists only in the running process's memory |
Covered | crates/phantom-proxy/src/server.rs:104–109 |
| Team X25519 private key | Exfiltration from OS keychain | Private key stored in OS keychain; access requires user session authentication same as all keychain secrets | Covered — same as real secret values in keychain | |
| Team X25519 private key | Insider reads another member's private key | Each member's private key never leaves their machine; the team vault push protocol encrypts to public keys only | Covered | crates/phantom-core/src/team_crypto.rs:109–138 — seal_sym_key uses recipient public key only |
| Team X25519 private key | Key revocation after member leaves team | No automated key-revocation or re-encryption flow exists. Removing a member from the team prevents future pushes encrypting to their key, but does not invalidate past pushes that included them | Not covered — see §7 | |
.phantom.toml integrity |
LLM or PR tampers with service mappings to redirect proxy | No cryptographic integrity protection on .phantom.toml. The file is checked into the repository and subject to standard code review. Tampering would require either direct file access or a merged malicious PR |
Not covered — relies on code review and filesystem permissions | |
| Audit log | Log tampered or deleted to cover tracks | Signed entries use an HMAC-SHA256 chain, monotonic sequence numbers, and a signed audit-head.json checkpoint. phantom audit verify fails on malformed lines, modified entries, inserted entries, sequence gaps, missing head checkpoints, and log tail/head mismatches. Deleting both log and checkpoint still requires external evidence |
Partial — see §7 | |
| Audit log | Sensitive values written to log | The log schema has no value field; callers are typed to pass name: Option<&str> only; a compile-time assertion test verifies the serialized schema contains no value key |
Covered | crates/phantom-core/src/audit.rs:36, test at line 237 |
| Cloud auth token | GitHub OAuth token stolen | Token stored in OS keychain; attacker with the token can call cloud API but cannot decrypt vault data (separate encryption key) | Partial — OS keychain protection; no second-factor for cloud API calls | |
| Cloud auth token | Phishing for GitHub OAuth token | Out of scope — see §6 |
All primitives below are from audited Rust crates (chacha20poly1305, argon2, crypto_box, subtle).
Algorithm: ChaCha20-Poly1305 (IETF variant, 96-bit nonce)
Key derivation: Argon2id with parameters selected per OWASP "balanced" recommendation (2024):
- Memory: 64 MiB (
m = 65536 KiB) - Iterations: 3 (
t = 3) - Parallelism: 1 lane (
p = 1) - Output length: 32 bytes
Wire format: salt (32 bytes) || nonce (12 bytes) || ciphertext
Both salt and nonce are generated fresh per encryption via rand::thread_rng().fill_bytes(). The AEAD tag is appended to the ciphertext by the chacha20poly1305 crate. Decryption failure is detected by the AEAD tag verification and returns an error — no partial plaintext is ever returned.
A legacy fallback path exists for vaults encrypted under earlier Phantom releases (which used Argon2::default() parameters: m≈19 MiB, t=2). When the hardened-parameter decryption fails, the legacy parameters are tried automatically. New encryptions always use the hardened parameters.
Code: crates/phantom-vault/src/crypto.rs
Each team vault push uses a two-layer scheme:
Layer 1 — Vault encryption: A fresh 32-byte symmetric key is generated per push (OsRng). The vault plaintext is encrypted with this key using ChaCha20-Poly1305.
Layer 2 — Key encapsulation per recipient: For each team member with a registered public key, an ephemeral X25519 keypair is generated. The X25519 DH output (ephemeral secret × recipient public) is used as the key for a ChaChaBox (XChaCha20-Poly1305 with 24-byte nonce) that encrypts the 32-byte symmetric key. The ephemeral public key, nonce, and ciphertext are stored as the member's KeyShare.
Forward secrecy property: Ephemeral sender keys are never reused between pushes. Tests verify that two seals of the same payload produce different ephemeral pubkeys and nonces (crates/phantom-core/src/team_crypto.rs:212–223).
Isolation property: A KeyShare encrypted to member B cannot be decrypted by member A — verified in tests at crates/phantom-core/src/team_crypto.rs:200–209.
Code: crates/phantom-core/src/team_crypto.rs
Format: phm_ prefix + 64 lowercase hex characters (32 bytes = 256 bits of randomness)
Generation: rand::thread_rng().fill_bytes() — the rand crate uses the OS CSPRNG (getrandom) seeded on first use.
Properties:
- 256-bit keyspace makes brute-force infeasible.
- Tokens are opaque references — they carry no HMAC or signature. This is intentional: they are not authenticators, they are placeholders. The proxy's session token (
PHANTOM_PROXY_TOKEN) is the authenticator. - Tokens can be rotated (
phantom rotate) to invalidate any that have leaked into logs or LLM context.
Code: crates/phantom-core/src/token.rs:5–22
Format: 64 lowercase hex characters (32 bytes = 256 bits of randomness)
Generation: rand::thread_rng().fill_bytes() — fresh per proxy session.
Comparison: subtle::ConstantTimeEq — constant-time byte comparison to prevent timing side-channel attacks from a colocated local process.
Code: crates/phantom-proxy/src/server.rs:104–109 (generation), server.rs:620–623 (comparison)
Secret names stored in the OS keychain use a SHA-256 derived identifier (first 8 bytes = 16 hex chars) as both the service key and account field. This prevents processes that enumerate keychain entries from learning which secret names are stored for a project.
Code: crates/phantom-vault/src/keychain.rs:12–18
| Component | Why trusted |
|---|---|
| OS keychain | Access is gated by the user's login session. On macOS with Secure Enclave hardware, private keys are hardware-bound. Phantom inherits whatever guarantee the OS provides. |
| OS process model | UNIX process isolation: a process running as user A cannot read another user's memory or file descriptors without root. Phantom relies on this for vault file and proxy socket isolation. |
| The user's terminal for confirmation prompts | phantom_add_secret_interactive initiates a terminal prompt outside any AI agent context. The terminal is trusted; the MCP channel is not. |
rustls system CA roots |
Outbound TLS from the proxy uses rustls with system CA roots; no custom CA certificates are accepted, making a local CA injection attack ineffective. |
| Check | Mechanism |
|---|---|
| MCP tool arguments never carry plaintext secrets | phantom_add_secret unconditionally rejects calls with a value parameter — crates/phantom-mcp/src/server.rs:227–240 |
| Destructive MCP operations have user consent | require_confirm() gate on all mutating tools — crates/phantom-mcp/src/server.rs |
| Proxy requests are authenticated | Session token checked via constant-time compare before any request is processed; CLI-generated URLs use /_phantom/<token>/ for SDK compatibility, while PHANTOM_PROXY_HEADER_AUTH_ONLY=1 requires x-phantom-proxy-token — crates/phantom-proxy/src/server.rs |
| Vault ciphertext integrity | ChaCha20-Poly1305 AEAD — decryption fails with an error if ciphertext has been tampered with |
| Team vault key registration before send | seal_sym_key requires the recipient's public key to be present before encrypting their share — crates/phantom-core/src/team_crypto.rs:111 |
| Source | Rationale |
|---|---|
| Any value passed through the MCP channel | MCP arguments are reachable by LLM context and by any process that can speak MCP. Treated as adversarial. |
Contents of .env (for security decisions) |
The .env file may be committed, synced, or readable by other processes. Only phantom tokens should ever appear there. |
| Upstream API responses (for absence of secrets) | The proxy scrubs real secrets from upstream responses before returning them to the caller, on the assumption that a response might echo back a value that was injected into the request. |
The following are explicitly not addressed by Phantom's design. Documenting them here sets accurate expectations.
Local attacker with root / admin access.
A process or user with root privileges can read the OS keychain, inspect process memory, attach a debugger, or replace the phantom binary. No local secret manager can defend against this. If your threat model includes malicious insiders with admin access to developer machines, a hardware security key or remote secrets service (Vault, AWS Secrets Manager) is more appropriate.
Side-channel attacks on the OS keychain itself. Cache-timing, power analysis, or EM side-channels against the Secure Enclave or TPM are out of scope.
Quantum-capable attackers. X25519 is not post-quantum safe. A cryptographically relevant quantum computer could break the team vault key encapsulation scheme. This is a future upgrade path; classical X25519 is appropriate for current threat landscapes.
Phishing for the user's GitHub OAuth token. Phantom Cloud authentication uses GitHub OAuth. If an attacker tricks the user into authorizing a malicious OAuth app, the resulting token could be used to call Phantom Cloud APIs. Phantom has no control over GitHub's OAuth flow.
Hardware-level attacks. RowHammer, cold-boot attacks against DRAM, or DMA attacks via malicious peripherals are out of scope.
Malicious phantom binary.
If the phantom binary itself has been replaced or backdoored, all guarantees are void. Users should verify release checksums and install from trusted sources. Phantom does not currently publish signed release binaries with hardware-backed signing; this is a roadmap item.
AI training data exposure. If an LLM provider incorporates conversation content into training data, phantom tokens that appeared in prompts could propagate. However, phantom tokens are worthless without the local proxy — the real secret is never in the conversation.
These are security properties that are not yet implemented. They are documented here to be honest with evaluators and to set roadmap expectations.
The audit log at ~/.phantom/audit.log is append-only by file-open semantics (O_APPEND) and signed entries use an HMAC chain with monotonic sequence numbers. phantom audit verify detects malformed JSON, modified entries, inserted entries, prefix deletion, and sequence gaps in the remaining signed log. It still cannot prove that the latest N entries were removed, or that the whole log was deleted, without an external checkpoint or backup of the expected head.
The proxy does not rate-limit requests or detect unusual access patterns (e.g., a process making thousands of requests per second, or requests for secrets outside the project's configured mappings). A rogue local process that obtains the session token could drain secrets at high speed. Mitigation would require per-secret access counts and configurable rate limits.
There is no signature or hash commitment on .phantom.toml. A malicious PR that adds a service mapping redirecting openai → http://attacker.example.com would take effect silently on the next phantom exec. Mitigations include: code review (current), and a future signed-config mechanism.
Removing a team member from the team UI prevents future vault pushes from including their KeyShare. However, any team vault push that was made while they were a member remains decryptable by them if they retained their private key. There is no automated key-rotation flow that re-encrypts historical pushes excluding the removed member. Teams with strict offboarding requirements should rotate all secrets after removing a member.
phantom exec injects PHANTOM_PROXY_TOKEN into the child process environment. Any subprocess spawned by the child process inherits this variable. A compromised child process can use the token to make proxy requests for the lifetime of the session. This is a known, unavoidable trade-off for the current architecture (the child needs the token to authenticate). The impact is bounded by the token's ephemeral lifetime and the proxy's localhost-only binding.
For streaming content types (text/*, application/x-www-form-urlencoded), if the body exceeds max_body_size the streaming task is dropped (channel closed), which surfaces to the upstream as a broken connection rather than a clean HTTP 413. A clean 413 for the streaming path is a planned improvement.
The audit log requires PHANTOM_AUDIT=1 to be set. Teams that want audit trails must set this variable in their development environment. There is no mechanism to enforce this policy across a team. A future phantom.toml option to require audit logging is planned.
See SECURITY.md for the responsible disclosure policy, contact information, and expected response timeline.
Do not open public GitHub issues for security vulnerabilities.