Skip to main content

PrivRAG-Guard

CI PyPI version Python 3.10+ License: MIT

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)

Source distribution for privrag-guard 0.2.0
File Size Uploaded
privrag_guard-0.2.0.tar.gz 43.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for privrag-guard 0.2.0
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page