Metadata-Version: 2.5
Name: privrag-guard
Version: 0.2.0
Summary: Privacy-preserving middleware for embedding pipelines and vector search.
Author: Om Desai
License: MIT
License-File: LICENSE
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Security :: Cryptography
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: numpy>=1.24
Provides-Extra: all
Requires-Dist: chromadb>=0.5; extra == 'all'
Requires-Dist: faiss-cpu>=1.8; extra == 'all'
Requires-Dist: langchain-core>=0.2; extra == 'all'
Requires-Dist: pytest>=8.0; extra == 'all'
Requires-Dist: qdrant-client>=1.9; extra == 'all'
Provides-Extra: chroma
Requires-Dist: chromadb>=0.5; extra == 'chroma'
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == 'dev'
Provides-Extra: faiss
Requires-Dist: faiss-cpu>=1.8; extra == 'faiss'
Provides-Extra: langchain
Requires-Dist: langchain-core>=0.2; extra == 'langchain'
Provides-Extra: qdrant
Requires-Dist: qdrant-client>=1.9; extra == 'qdrant'
Description-Content-Type: text/markdown

# PrivRAG-Guard

[![CI](https://github.com/omdesai69/privrag-guard/actions/workflows/ci.yml/badge.svg)](https://github.com/omdesai69/privrag-guard/actions/workflows/ci.yml)
[![PyPI version](https://img.shields.io/pypi/v/privrag-guard.svg?color=blue)](https://pypi.org/project/privrag-guard/)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://pypi.org/project/privrag-guard/)
[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/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](#what-this-does-and-does-not-protect)
before deploying.** The confidentiality of your corpus rests on the rotation key,
not on the noise.

## Install

```bash
pip install privrag-guard
```

For specific vector store adapters:
```bash
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

```python
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:

```python
# 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

```python
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

```bash
# 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](SECURITY.md).

## Development

```bash
git clone https://github.com/omdesai69/privrag-guard.git
cd privrag-guard
pip install -e ".[dev]"
pytest
```
