Inspiration

Every day, people paste things into AI tools that they should never hand to a third party: source code with API keys, customer records, patient data, contracts. Companies want their teams to use LLMs, but have no idea what data leaves the building. We wanted to make the safe path the default one.

Veil is a local privacy firewall that sits between you and any LLM provider. The provider never sees your secrets or personal data, and never receives any encryption key.

Video demo: LINK_YOUTUBE

What it does

Veil runs on 127.0.0.1 as an OpenAI-compatible proxy. Any existing app can use it by changing one line (base_url).

  1. Mask: a scanner detects secrets (OpenAI, Anthropic, AWS, GitHub, Google, Slack keys, JWTs, private keys, password= pairs) and personal data (names, emails, phones, IBANs, SSNs) and replaces them with tokens like [EMAIL_1] or [OPENAI_KEY_1].
  2. Send: only the masked text goes to the provider, with store: false for zero data retention.
  3. Restore: tokens in the answer are replaced with the real values, locally.
  4. Encrypt: the token-to-value mapping is encrypted with AES-256-GCM (fresh random 96-bit nonce, authenticated context).
  5. Vault: conversation history is stored encrypted on disk. Its key is split with Shamir's Secret Sharing, so any 3 of 5 key holders can open it and fewer than 3 learn nothing.
  6. Fail closed: if masking fails, the request is blocked, never forwarded in clear.

The web UI (localhost only) has a playground, a leak report, an audit log of what the provider actually received, a vault reader, and a prompt injection demo showing that the provider has no key to reveal.

The mathematics

The vault key $K$ (256 bits) is split byte by byte over the finite field $\mathrm{GF}(2^8)$ with the irreducible polynomial $x^8 + x^4 + x^3 + x + 1$. For each secret byte $s$ we pick a random polynomial of degree $t-1$:

$$f(x) = s + a_1 x + a_2 x^2 + \dots + a_{t-1} x^{t-1}, \qquad a_i \xleftarrow{\$} \mathrm{GF}(2^8)$$

and give holder $i$ the point $(x_i, f(x_i))$ for $x_i \in {1,\dots,n}$. With $t = 3$ and $n = 5$, any three shares recover the secret by Lagrange interpolation at $x = 0$:

$$s = f(0) = \sum_{i=1}^{t} y_i \prod_{j \ne i} \frac{x_j}{x_i + x_j}$$

(in characteristic 2, subtraction equals addition). The scheme is information-theoretically secure: any $t-1$ shares are statistically independent of $s$, because for every candidate secret there is exactly one consistent polynomial. Field inverses are computed as $a^{-1} = a^{254}$ (Fermat), and multiplication is written without secret-dependent branches or table lookups, to avoid timing side channels.

How we built it

  • Rust core (seclib): AES-256-GCM, Shamir over $\mathrm{GF}(2^8)$ written from scratch, and zeroize to wipe key material. It is exposed to Python through PyO3 and maturin, so the cryptographic execution is low level while the pipeline stays easy to extend.
  • Python: FastAPI proxy, scanner, encrypted vault, and the web interface.
  • Interface: a clean black, white and grey web UI served on loopback, with a leak report and a vault reader.
  • Tests: 22 automated security checks (nonce uniqueness, wrong key or context rejected, tampering and reordering detected, sub-threshold shares fail, duplicate shares rejected, no secret in the text sent to the provider, regression tests for false positives).

Mitigated vulnerabilities

Risk Mitigation
Sensitive data reaches the provider Local masking before sending
Master key in prompts or logs Key never leaves the device
Single point of compromise for history Shamir 3-of-5 vault
AES-GCM nonce reuse Fresh random nonce per encryption
Tampering or reordering of history GCM tag plus entry index in the AAD
Key lingering in memory zeroize in Rust, wiped after use
Provider retains conversations store: false
Remote access to the UI Localhost host check, attempt limit on the vault

Challenges

  • LLMs cannot read ciphertext, so encrypting the prompt itself is useless. We solved it with reversible tokenization: the provider gets placeholders, and the encrypted mapping stays local.
  • False positives: our first password rule masked variable names like db_password, which broke the code sent to the model. We fixed it and added regression tests.
  • Terminal and tooling surprises: PyO3 bindings, Rich markup swallowing [TOKENS] in the first interface, and GitHub rejecting a workflow file.

What we learned

Real security is mostly about the boundaries: what crosses them, what is stored, and what happens when something fails. We also learned to be honest about limits instead of overselling.

Limitations (honest)

  • Detection is heuristic (regex). Unusual formats, lowercase names or letter-only passwords can be missed. Production use would add entropy checks and a local NER model.
  • Context stays visible to the provider, and message length is not hidden.
  • While the proxy runs, the vault key sits in process memory. The 3-of-5 threshold protects history at rest, not against an attacker controlling the live process.
  • Python bytes objects are immutable and cannot be fully zeroized.
  • We do not use FHE: Veil masks and encrypts the mapping and history, not the whole prompt.

What's next

A local NER model for names and addresses, entropy-based secret detection, streaming responses, and a browser extension that routes chat sites through Veil.

Made by Major Matt Razvan.

Built With

Share this project:

Updates

Submission history