PrivRAG-Guard
Middleware that transforms vector embeddings before they reach a database, so a read-only breach of the vector store does not immediately yield the plaintext embeddings. It combines a keyed orthogonal rotation, calibrated differential-privacy noise, and semantic subspace projection, while preserving
98% pairwise cosine relations for retrieval.
Read What this does and does not protect before deploying. The confidentiality of your corpus rests on the rotation key, not on the noise.
Install
pip install privrag-guard
For specific vector store adapters:
pip install "privrag-guard[chroma]" # ChromaDB adapter
pip install "privrag-guard[faiss]" # FAISS adapter
pip install "privrag-guard[qdrant]" # Qdrant adapter
pip install "privrag-guard[langchain]" # LangChain wrapper
pip install "privrag-guard[all]" # All adapters
Library usage
import os
from privrag import PrivRAGGuard
guard = PrivRAGGuard(
epsilon=1.0,
noise_fraction=0.10,
passphrase=os.environ["PRIVRAG_KEY"], # real key material, from a secret manager
)
protected = guard.protect_batch(document_embeddings)
query = guard.protect_vector(query_embedding)
metrics = guard.benchmark_utility(document_embeddings, protected, k=5)
Supplying key material
The rotation key is the whole protection. Provide it one of three ways:
# 1. Passphrase, stretched with scrypt (inject from a secret manager)
guard = PrivRAGGuard(passphrase=os.environ["PRIVRAG_KEY"])
# 2. Raw key material: an int, or >= 16 bytes
from privrag.core.dp_engine import DifferentialPrivacyEngine
guard = PrivRAGGuard(key_seed=DifferentialPrivacyEngine.secure_seed()) # 128-bit CSPRNG
# 3. A persisted key file, written with mode 0600
guard.protect_vector(sample) # initializes the rotation
guard.save_key("/run/secrets/privrag.key")
other = PrivRAGGuard.from_key_file("/run/secrets/privrag.key")
random_state is a reproducibility seed, not key material. Passing it
without passphrase/key_seed raises a PrivRAGSecurityWarning, because a seed
you committed to source control is a key an attacker already has. Multi-tenant
deployments should domain-separate with passphrase_salt="tenant-a".
Choosing a privacy mode
| Mode | Constructor | Guarantee | Retrieval |
|---|---|---|---|
| Bounded (default) | PrivRAGGuard(noise_fraction=0.10) |
Not DP. A bounded perturbation orthogonal to the signal. Confidentiality comes from the rotation key. | >98% relation retention |
| Strict DP | PrivRAGGuard(epsilon=1.0, strict_dp=True) |
Genuine (ε, δ)-DP via the analytic Gaussian mechanism (Balle & Wang, 2018). | Substantially degraded at low ε — measure it |
strict_dp=True is the honest option when you need a formal guarantee. It costs
real recall; run benchmark_utility at your ε before committing.
Adapters
from privrag.adapters import ChromaPrivGuard, LangChainPrivGuardEmbeddings
collection = ChromaPrivGuard.from_client(client, "notes", guard)
safe_embeddings = LangChainPrivGuardEmbeddings(provider, guard)
Guards are picklable, so they can be sent to multiprocessing, Ray, or Celery
workers — every worker must receive the same guard, or it will write into a
different index space.
CLI
# Simulate a dictionary inversion attack (synthetic, fixed demo key)
privrag attack "patient diabetes ssn 12345"
# Benchmark retrieval retention
privrag benchmark --samples 500 --epsilon 1.0
privrag benchmark --samples 500 --epsilon 1.0 --strict-dp
# Sanitize an embedding batch. --key-file generates a 128-bit key on first use
# and reuses it afterwards; keep it, or the vectors can never be matched again.
privrag protect --input raw.npy --output safe.npy --key-file privrag.key
# Or derive the key from a passphrase in the environment
PRIVRAG_PASSPHRASE=... privrag protect --input raw.npy --output safe.npy
protect fails closed if you supply no key: without one, the output is not
confidential. Pass --no-key if you explicitly want rotation-free output.
Prefer PRIVRAG_PASSPHRASE over --passphrase, which is visible in the process
list and your shell history. Exit codes: 0 success, 1 runtime error,
2 usage error.
What this does and does not protect
Threat model. An adversary with a read-only copy of the vector store and no access to the rotation key. Against that adversary, a stolen index is a set of vectors in an unknown rotated basis, and an off-the-shelf inversion dictionary does not align with it.
It does not protect against:
- Key compromise. With the key, every protected vector inverts to within ~6° of the original. The noise cannot help: it is deliberately projected orthogonal to the signal so that retrieval survives, and noise orthogonal to the signal cannot hide the signal.
- Known-plaintext attack. An orthogonal map is distance-preserving, so roughly d known (raw, protected) pairs are enough to solve for the rotation by least squares. Measured at d=64, held-out reconstruction cosine goes 0.26 → 0.50 → 0.95 → 0.99 for 16 → 32 → 64 → 128 leaked pairs. Do not treat the rotation as encryption.
- Distributional leakage. Pairwise geometry is preserved by design — that is what makes retrieval work — so cluster structure, corpus size, and duplicate documents remain visible.
- An adversary who can also query your retriever. Ranking behaviour leaks information the stored vectors alone do not.
This is defense-in-depth, not homomorphic encryption. Use it with encryption at rest and in transit, restricted database access, and keys held outside the vector store. Full detail in SECURITY.md.
Development
git clone https://github.com/omdesai69/privrag-guard.git
cd privrag-guard
pip install -e ".[dev]"
pytest
Release files for privrag-guard 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| privrag_guard-0.2.0.tar.gz | 43.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| privrag_guard-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 74.2 kB
Release files / privrag_guard-0.2.0.tar.gz
| Download URL | privrag_guard-0.2.0.tar.gz |
|---|---|
| Size | 43.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
4b4e5b24df90d56cc6648fc7aa2287672d9dbcf4b90cebb0532ab9645a1f7fbe
|
|
BLAKE2b-256 checksum How to use checksums |
4ef673859e9454271a2024b062423aa593a2f7a8165ee6eeba64230cf370378f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|
Release files / privrag_guard-0.2.0-py3-none-any.whl
| Download URL | privrag_guard-0.2.0-py3-none-any.whl |
|---|---|
| Size | 31.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
937e09fb742ce1924d50ce1d5699b503b8bd2b575c646f322d18d60fd8ce4a2a
|
|
BLAKE2b-256 checksum How to use checksums |
7c0f5e6c2061b350d0077f152828a5a2696425c6ee7049ebbfe60c3eb433f472
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.10
|