Inspiration
I run the PACS and teleradiology infrastructure for a hospital network. Doctors and IT staff want to use LLM assistants for emails, reports and tickets, but every prompt is full of names, phone numbers, patient IDs, server addresses and API keys. Once a prompt reaches a hosted model, it can be logged and stored on the provider's servers forever. I wanted an agent that is useful without the provider ever seeing those values, and without one person (or one leaked file) being able to unlock the key that protects them.
What it does
Sealed Prompt is a private gateway in front of any hosted LLM API (AI pipeline track).
- Key ceremony. A 256-bit master key is generated locally and split with Feldman verifiable secret sharing into \(n\) shares with threshold \(k\) (default 2-of-3). The key is then wiped: only \(k\) shares can bring it back.
- Unlock. Each share is verified against public commitments before anything is combined, so a corrupted or forged share is rejected and named. The key is rebuilt by Lagrange interpolation, checked against the commitment \(C_0\), and kept only in memory.
- Private chat. Every secret in the prompt (emails, phone, Aadhaar, PAN, card numbers with a Luhn check, API keys, private keys, IPs, or anything wrapped in
{{ }}) is sealed with AES-256-GCM and replaced by an opaque handle like⟦S1⟧. The LLM works on the placeholders; its reply is rehydrated locally. - Egress guard. Right before the request leaves, the exact payload is scanned for the master key (hex, base64 and decimal) and for every sealed plaintext. Any match blocks the request.
The local web UI (127.0.0.1 only) shows side by side what you see and what the LLM provider receives, plus the sealed vault and the egress check for every turn.
The mathematics
Group
We work in the order-\(q\) subgroup of \(\mathbb Z_p^*\) where \(p\) is the 2048-bit RFC 3526 safe prime: $$p = 2q + 1,\qquad p, q \text{ prime},\qquad g = 4 = 2^2 .$$ Because \(g\) is a quadratic residue, \(g^q \equiv 1 \pmod p\) and \(\langle g \rangle\) has prime order \(q\). Both primalities are re-checked with Miller-Rabin in the test suite.
Dealing (Shamir over \(\mathbb Z_q\))
With secret \(s\) (the master key as an integer) the dealer picks random \(a_1,\dots,a_{k-1} \in \mathbb Z_q\): $$f(x) = s + a_1 x + a_2 x^2 + \dots + a_{k-1}x^{k-1} \pmod q,\qquad s_i = f(i),\; i = 1,\dots,n .$$
Feldman commitments and share validation
The dealer publishes $$C_j = g^{a_j} \bmod p,\qquad j = 0,\dots,k-1,\quad (a_0 = s).$$ Anyone can validate share \(s_i\) without learning \(s\): $$g^{s_i} \;\stackrel{?}{\equiv}\; \prod_{j=0}^{k-1} C_j^{\,i^j} \pmod p .$$ This holds because \(\prod_j C_j^{i^j} = g^{\sum_j a_j i^j} = g^{f(i)}\). Commitments are also checked to lie in the subgroup (\(C_j^q \equiv 1\)), and each share carries a fingerprint of the commitment set, so shares from a different key are rejected.
Reconstruction
For any set \(S\) of \(k\) verified shares, $$s = f(0) = \sum_{i \in S} s_i \prod_{\substack{m \in S \ m \ne i}} \frac{-m}{\,i - m\,} \pmod q,$$ using modular inverses in \(\mathbb Z_q\). The result is accepted only if \(g^{s} \equiv C_0 \pmod p\).
Sealing
From the master key \(K\) and a random session id \(\sigma\): $$K_\sigma = \mathrm{HKDF\text{-}SHA256}(K,\ \text{salt}=\sigma,\ \text{info}=\texttt{"sealed-prompt v1 values"}),$$
and each value \(m_h\) is sealed as
$$c_h = E_{K_\sigma}(\text{nonce}_h,\ m_h,\ \text{AAD}=\sigma \,|\, h),$$ where \(E\) is AES-256-GCM, with a fresh 96-bit nonce per value. Binding the handle \(h\) and session \(\sigma\) in the AAD means a ciphertext cannot be swapped into another handle or another conversation.
Attack surface it removes
| Threat | Mitigation |
|---|---|
| Provider logs or trains on prompts | Provider only ever receives handles ⟦Sn⟧; plaintext and ciphertext stay local |
| Master key reaches the API or the prompt | Key never enters a prompt; egress guard blocks hex / base64 / decimal forms |
| One stolen share or one insider | Threshold \(k\): fewer than \(k\) shares reveal nothing about \(s\) (only \(g^s\), protected by the discrete-log problem) |
| Corrupted or malicious share | Feldman check rejects it and names it before reconstruction |
| Shares mixed between keys | Commitment fingerprint in every share |
| Fake commitments | Subgroup membership check \(C_j^q \equiv 1\) |
| Ciphertext cut-and-paste | AES-GCM AAD binds session and handle |
| Model invents or mangles placeholders | Unknown handles are reported, never "decrypted" |
| Key on disk | Key only in memory; Lock wipes it; only public commitments are stored |
How I built it
- Python 3.14. The number theory (Miller-Rabin, Feldman VSS, Lagrange interpolation) is written from scratch in
sealed/vss.py; AES-GCM and HKDF run in OpenSSL's C code throughcryptography, as the rules allow for this track. - LLM: Meta Model API (
muse-spark-1.3, OpenAI-compatible). Any OpenAI-compatible host (OpenAI, Bedrock gateways, Baseten) works by settingSEALED_BASE_URLandSEALED_MODEL. - FastAPI backend bound to 127.0.0.1 and a single-page UI.
- 18 pytest tests: group primality, every \(k\)-subset reconstructs, too few shares fail, tampered and foreign shares rejected, forged commitments rejected, AAD swap fails, cross-session decryption fails, detectors, and an end-to-end spy LLM that asserts no plaintext or key ever appears in the payload.
Challenges
- Getting the model to keep placeholders intact: a short system prompt plus unique
⟦Sn⟧brackets works reliably; unknown handles are still detected. - Avoiding false positives: card numbers must pass the Luhn check; the same value keeps the same handle across the conversation.
Known limitations
- Placeholders protect values, not meaning: the provider still sees the sentence structure.
- Python big-integer
powis not constant time; share verification runs locally on the user's machine, not on a remote server. - Python cannot guarantee that every copy of the key is erased from memory; wiping is best effort.
- Detection is pattern based; anything unusual should be wrapped in
{{ }}.
What's next
- Pedersen VSS for information-theoretic hiding of the secret.
- A Rust core for constant-time arithmetic and guaranteed zeroisation.
- Shares stored on separate hardware tokens or phones.
Built With
- cryptography
- fastapi
- meta-llama
- openai-api
- python