Security Model

zKPnote is designed with a zero-knowledge architecture. The server and blockchain never have access to plaintext note content.

Encryption

Algorithm

  • Cipher: XChaCha20-Poly1305 (via libsodium)
  • Key Derivation: HKDF from BIP-39 seed or Phantom wallet signature
  • Key Size: 256-bit
  • Nonce: 24-byte random nonce per encryption operation

What Is Encrypted

  • Note title and content (individually encrypted)
  • Folder names
  • User profile (username, email, bio, profile picture)

What Is NOT Encrypted

  • Note ID, folder ID (UUIDs, non-sensitive)
  • Timestamps (createdAt, updatedAt)
  • Folder hierarchy (parentId relationships)
  • On-chain proof hash (SHA-256 of the plaintext note, not the encryption key)

Key Management

Unified Derivation

Both seed phrase and Phantom modes produce the same encryption key and auth keypair for the same wallet. This means a user can log in either way and access the same vault with the same notes.

Seed Phrase Mode

  • 12-word BIP-39 mnemonic generates a 64-byte seed
  • BIP-44 derivation at m/44'/501'/0'/0' produces the Solana keypair (same address as Phantom and all standard Solana wallets)
  • The keypair signs a deterministic message: "ChainNotes-vault-encryption-key-v1"
  • First 32 bytes of the signature derive the encryption key via HKDF
  • Last 32 bytes derive an auth keypair for silent API authentication
  • The seed phrase is the single root of trust; losing it means losing access

Phantom Mode

  • User signs the same deterministic message: "ChainNotes-vault-encryption-key-v1"
  • Ed25519 signatures are deterministic, so the same wallet always produces the same 64-byte signature
  • First 32 bytes of the signature derive the encryption key (identical to seed phrase mode)
  • Last 32 bytes derive an auth keypair for silent API authentication (identical to seed phrase mode)
  • The connected Phantom wallet handles on-chain transaction signing

Key Storage

  • Keys are held in memory only (React state)
  • No keys are persisted to localStorage or cookies
  • Locking the vault clears all key material from memory
  • Page refresh requires re-entering the seed phrase or reconnecting Phantom

Phantom Signature Caching

  • The Phantom wallet signature used for key derivation is cached in sessionStorage
  • This avoids requiring a Phantom popup on every page refresh within the same session
  • sessionStorage is scoped to the browser tab and is automatically cleared when the tab or browser window is closed
  • This is the only value stored in sessionStorage; no encryption keys or private keys are cached

On-Chain Security

Access Control

  • NoteProof: PDA seeded with ["proof", note_hash]. The first wallet to register a given hash owns the proof. Once created, the proof is immutable.
  • ProgramConfig: PDA seeded with ["config"] + has_one = authority. Only the authority (deployer) can update treasury/fees.
  • ExecuteSale: Treasury account validated on-chain against config via constraint = treasury.key() == config.treasury.

Per-Note Proof System

  • Each note can have an on-chain proof registered via register_proof
  • The proof hash is SHA-256(title + "\n" + content), computed client-side
  • PDA derivation from the hash ensures exactly one proof can exist per unique content
  • First-to-register wins: once a proof is created, no other wallet can claim the same hash
  • This prevents content theft claims — the on-chain timestamp proves who registered the content first

Arithmetic Safety

  • Fee calculation uses checked_mul and checked_sub to prevent overflow/underflow
  • Price must be greater than zero (enforced by require! macro)

Transaction Atomicity

  • execute_sale performs two CPI transfers in a single transaction
  • If either transfer fails, the entire transaction reverts
  • No partial payment states are possible

API Security

Rate Limiting

All API routes (/api/vault, /api/marketplace, /api/proof, /api/rpc) are rate-limited using Upstash Redis (@upstash/ratelimit). This provides distributed rate limiting across Vercel edge instances — requests from the same IP are tracked globally, not per-instance. In local development, falls back to an in-memory store.

RPC Proxy Whitelist

Browser-side Solana RPC calls are routed through /api/rpc to avoid exposing the RPC endpoint directly. The proxy enforces a strict whitelist of 11 permitted methods:

  • getBalance, getLatestBlockhash, getRecentBlockhash, sendTransaction, confirmTransaction, getTransaction, getSignatureStatuses, getAccountInfo, getMinimumBalanceForRentExemption, requestAirdrop, getMultipleAccounts

Any method not on this list returns HTTP 403.

API Authentication

  • API requests are authenticated using Ed25519 signatures from the auth keypair
  • The server verifies the signature against the claimed public key
  • In Phantom mode, the auth keypair is derived from the wallet signature, enabling silent API calls without Phantom popups

Remote MCP Endpoint (/api/mcp)

The remote MCP endpoint at https://www.zkpnote.com/api/mcp gives Claude mobile and claude.ai direct vault access. Its security model differs from the stdio MCP server (which keeps the seed phrase local) in ways worth calling out:

Trust model

  • Seed phrase is server-side. The endpoint is single-tenant: one ZKPNOTE_SEED_PHRASE per Vercel deployment. You are trusting the host (yourself, if self-deployed) with the key material the same way you'd trust any self-hosted app's master secret.
  • Bearer token gate. Every request must include Authorization: Bearer <ZKPNOTE_MCP_TOKEN> or ?token=<...> matching the server-side env var. The token acts as a shared secret equivalent to a password — anyone who holds it has full vault read/write access.
  • No session. Auth is checked on every request; there are no cookies or JWTs. Rotation = change ZKPNOTE_MCP_TOKEN in Vercel, redeploy, update connector URL.
  • Fail-closed on misconfig. If ZKPNOTE_MCP_TOKEN is unset, the endpoint returns HTTP 500 (Server misconfigured) rather than silently accepting unauthenticated requests.
  • Transport: HTTPS only (enforced by Vercel), stateless per-request — no event-store, no session persistence, no background connections.

Additional threats beyond the stdio model

ThreatMitigation
Connector URL + token leak (screenshot, shared config)Rotate ZKPNOTE_MCP_TOKEN; revoke the old one by redeploy
Mobile client stores token in app logs / cloud-backupTreat token as sensitive; rotate after device resale or account change
Vercel env-var exposure (insider / platform compromise)Seed phrase is at platform risk; defense-in-depth = rotate seed + re-encrypt vault (future accounts re-key flow)
Redirect auth-header strippingAlways use https://www.zkpnote.com/api/mcp (not bare zkpnote.com — 307 redirect drops Authorization)
Rate limiting on /api/mcpNot yet enforced on MCP route; planned when multi-tenant auth lands

What's not yet implemented

  • Per-user authentication — the alpha endpoint is shared-secret single-tenant, not per-account. Multi-tenant token or OAuth flow is the next roadmap item.
  • Rate limiting on /api/mcp — Upstash Redis rate limits already exist on /api/vault (which the MCP tools call internally), so vault operations are indirectly rate-limited, but the MCP route itself has no per-token quota yet.
  • Audit log — MCP tool invocations are not yet recorded server-side.

Proof Verification Privacy

  • The /verify page is public and requires no authentication
  • Hash computation happens entirely client-side — the plaintext content entered for verification never leaves the browser
  • Only the computed hash is sent to the server for lookup
  • This means a user can verify whether content has been registered without exposing the content to the server

Content Protection

Similarity Detection

Two-tier similarity checking protects content originality:

  • Marketplace tier (70%): New listings are compared against all existing marketplace listings using word-trigram Jaccard similarity. Listings with >= 70% similarity to another seller's content are rejected.
  • Proof tier (90%): New listings are also compared against all on-chain proved content from other authors. Listings with >= 90% similarity to another author's proved work are rejected, even if that content was never listed for sale.
  • Sellers are excluded from both comparisons against their own content (same wallet address)

Purchase Tagging

  • Notes acquired from the marketplace are tagged with the source listing ID
  • Tagged notes are blocked from being listed for sale at both the client and server level

Per-Note Proof

  • On-chain proofs provide cryptographic evidence of authorship with a Solana-timestamped record
  • If a content theft dispute arises, the proof with the earlier created_at timestamp establishes priority
  • Proofs are stored in the Supabase proofs table for fast lookup and similarity search via the /api/proof endpoint

Threat Model

ThreatMitigation
Server compromiseServer only stores encrypted data; no plaintext access
Man-in-the-middleAll encryption happens client-side before transmission
Unauthorized vault accessRequires seed phrase or Phantom wallet signature
Content theft via marketplaceSimilarity detection + purchase tagging
Content theft claimsPer-note on-chain proof with first-to-register timestamp
Treasury theftTreasury address validated on-chain; only authority can change it
Fee manipulationFee basis points stored on-chain; only authority can update
Replay attacksAPI auth uses fresh signatures with timestamp validation
Session hijacking via cached signaturePhantom signature cached in sessionStorage only; cleared on tab close
API abuse / DDoSDistributed rate limiting via Upstash Redis across all edge instances
RPC endpoint abuseWhitelist restricts Solana RPC proxy to 11 permitted methods; all others return 403
Remote MCP endpoint abuseBearer-token gate (ZKPNOTE_MCP_TOKEN) on every request; misconfigured deployments return 500 (fail closed) rather than allowing anonymous vault access
Remote MCP seed-phrase exposureSeed phrase lives only in the Vercel deployment's env vars and memory; never logged, never persisted to disk, never sent to the MCP client

Limitations

  • Browser memory: Keys exist in JavaScript memory during a session, which is subject to browser-level attacks (malicious extensions, XSS)
  • Similarity detection: Trigram-based comparison may not catch content that has been substantially rewritten
  • Off-chain bids: Auction bids are not escrowed on-chain; the winner pays at settlement
  • No key rotation: Changing the seed phrase creates a new vault; migrating an existing vault to a new key is not currently supported
  • Proof immutability: Once a proof is registered on-chain, it cannot be updated or deleted. If content changes, a new proof must be registered with the updated hash.