Metadata-Version: 2.5
Name: piimask
Version: 0.1.0
Summary: Reversible PII masking and unmasking for the LLM era — anonymize text before it reaches a model, then restore the real values in the reply.
Project-URL: Homepage, https://github.com/thevip01/piimask
Project-URL: Repository, https://github.com/thevip01/piimask
Project-URL: Issues, https://github.com/thevip01/piimask/issues
Project-URL: Changelog, https://github.com/thevip01/piimask/blob/main/CHANGELOG.md
Author-email: Vipul Parmar <thevip4444@gmail.com>
Maintainer-email: Vipul Parmar <thevip4444@gmail.com>
License-Expression: MIT
License-File: LICENSE
Keywords: anonymization,anonymize,data-protection,de-identification,gdpr,hipaa,llm,masking,openai,pii,pii-detection,pii-masking,privacy,prompt,pseudonymization,redact,redaction,security
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Information Technology
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
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 :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Text Processing :: Filters
Classifier: Typing :: Typed
Requires-Python: >=3.9
Provides-Extra: all
Requires-Dist: cryptography>=41; extra == 'all'
Requires-Dist: faker>=20; extra == 'all'
Provides-Extra: crypto
Requires-Dist: cryptography>=41; extra == 'crypto'
Provides-Extra: faker
Requires-Dist: faker>=20; extra == 'faker'
Description-Content-Type: text/markdown

# piimask

**Reversible PII masking and unmasking for the LLM era.**
Hide personal data before it leaves your machine — send the safe version to a model, an API, or a log — then put the real values back when the answer comes home.

[![PyPI](https://img.shields.io/pypi/v/piimask.svg)](https://pypi.org/project/piimask/)
[![Python](https://img.shields.io/pypi/pyversions/piimask.svg)](https://pypi.org/project/piimask/)
[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)

---

## Why this exists

A few years ago, "sending your data to the cloud" meant a database you controlled. Today it means pasting a customer's email, a patient's phone number, or a colleague's home address into a prompt and shipping it off to a large language model you don't own, running somewhere you can't see, that may log or train on what it receives.

Most of the time we don't even notice we're doing it. A support ticket gets summarized. A sales call gets turned into follow-up notes. A spreadsheet gets "cleaned up." Each of those helpful little automations quietly carries real people's personal information across a boundary it was never meant to cross.

`piimask` is a small, dependency-free library that puts a checkpoint at that boundary. It finds the personal data in a piece of text, swaps it for stable placeholders, and remembers the mapping so you can reverse it later. The model sees `<EMAIL_1>` instead of `jane@acme.com` — and does its job just as well — while the real value never leaves your process.

It won't make you compliant with GDPR or HIPAA on its own, and it isn't a replacement for good security hygiene. But it makes the safe thing the easy thing, which is usually where privacy succeeds or fails.

## The idea in ten seconds

```python
from piimask import Anonymizer

anon = Anonymizer()

safe = anon.mask("Hi, I'm Jane — email jane@acme.com or call +1 415-555-0132.")
# "Hi, I'm Jane — email <EMAIL_1> or call <PHONE_1>."

# ...send `safe` to any LLM / API / log sink you don't fully trust...

answer = "I've emailed <EMAIL_1> and left a voicemail at <PHONE_1>."
print(anon.unmask(answer))
# "I've emailed jane@acme.com and left a voicemail at +1 415-555-0132."
```

Mask on the way out. Unmask on the way in. The model works with tokens; you work with reality.

## Install

```bash
# with uv (recommended)
uv add piimask

# or with pip
pip install piimask
```

Optional extras:

```bash
uv add "piimask[faker]"   # realistic fake-data masking
uv add "piimask[crypto]"  # encrypt the vault at rest
uv add "piimask[all]"     # both
```

The core library has **zero required dependencies** — it's pure Python standard library, so it installs instantly and runs anywhere Python 3.9+ does.

## A real workflow: protecting an LLM call

```python
from piimask import Anonymizer
# from openai import OpenAI   # or anthropic, or anything else

anon = Anonymizer()
client = OpenAI()

user_message = "Summarize this: Jane Doe (jane@acme.com, +1 415-555-0132) " \
               "disputes charge on card 4111 1111 1111 1111."

# 1. Mask before the data leaves your machine.
safe_message = anon.mask(user_message)

# 2. The model only ever sees tokens.
reply = client.chat.completions.create(
    model="gpt-4o-mini",
    messages=[{"role": "user", "content": safe_message}],
).choices[0].message.content

# 3. Restore the real values in the model's answer.
print(anon.unmask(reply))
```

Because the same value always maps to the same token *within a session*, you can feed a whole multi-turn conversation through one `Anonymizer` and the model will reason about `<EMAIL_1>` consistently across every message.

## What it detects

Out of the box, `piimask` recognizes the kinds of PII that leak most often into prompts and logs:

| Entity          | Examples                                   | Notes                                   |
| --------------- | ------------------------------------------ | --------------------------------------- |
| `EMAIL`         | `jane.doe+tag@sub.acme.co.uk`              |                                         |
| `PHONE`         | `+1 (415) 555-0132`, `07700 900123`        | 7–15 digits; ISO dates are excluded     |
| `CREDIT_CARD`   | `4111 1111 1111 1111`                      | validated with the Luhn checksum        |
| `SSN`           | `123-45-6789`                              | US format                               |
| `IBAN`          | `GB82 WEST 1234 5698 7654 32`              | validated with the ISO 13616 mod-97 sum |
| `IP_ADDRESS`    | `192.168.1.20`                             | strict 0–255 octets                     |
| `IPV6`          | `2001:db8::1`                              |                                         |
| `URL`           | `https://acme.com/x`                       | trailing punctuation is left alone      |

Need something else — an employee ID, a policy number, an internal hostname? Add a recognizer in one line (see below).

## Masking strategies

The *strategy* decides **how** a detected value is replaced. Two are reversible; two are one-way, which is exactly what you want for logs you never need to rehydrate.

| Strategy        | Reversible | Example output        | Best for                                 |
| --------------- | :--------: | --------------------- | ---------------------------------------- |
| `placeholder`   |     ✅     | `<EMAIL_1>`           | LLM round-trips (the default)            |
| `fake`          |     ✅     | `kevin83@hotmail.com` | prompts that behave better on realistic input |
| `partial`       |     ❌     | `j***@***.com`        | human-readable previews & support UIs    |
| `hash`          |     ❌     | `<EMAIL_c066add4a0>`  | logs & analytics — correlate without exposing |

```python
Anonymizer(strategy="partial").mask("ssn 123-45-6789")
# "ssn ***-**-6789"

Anonymizer(strategy="hash", salt="pepper").mask("a@b.com and a@b.com")
# "<EMAIL_9f2c...> and <EMAIL_9f2c...>"   # same input → same token, never reversible
```

`hash` is deliberately consistent: the same input always produces the same token, so you can still count distinct users or join two logs together — you just can never get the email back.

## The vault: where reversibility lives

Every reversible mask records a `token → original` entry in a **vault**. That's what `unmask` reads from.

```python
anon = Anonymizer()
anon.mask("contact jane@acme.com")

anon.vault.to_json()      # persist the mapping (e.g. between requests)
len(anon.vault)           # how many values are stored
anon.reset()              # forget everything the moment you're done
```

> ⚠️ **A vault contains the real PII in cleartext.** Treat it like a password. Keep it in memory for the life of a request when you can, never commit one to source control (the shipped `.gitignore` already blocks `*.vault`), and if you must store it, encrypt it:

```python
from piimask import Vault

key = Vault.generate_key()           # needs: pip install "piimask[crypto]"
blob = anon.vault.to_encrypted_json(key)   # ciphertext bytes, safe to store
restored = Vault.from_encrypted_json(blob, key)
```

Store the key somewhere separate from the blob — anyone holding both can recover the data.

## Custom recognizers

Anything you can describe with a regex, you can mask. Give it a name, and (optionally) a validator to reject false positives.

```python
from piimask import Anonymizer, Recognizer

anon = Anonymizer()
anon.add_recognizer(Recognizer("EMPLOYEE_ID", r"\bEMP-\d{5}\b", priority=5))

anon.mask("ticket from EMP-12345")
# "ticket from <EMPLOYEE_ID_1>"
```

Lower `priority` numbers win when matches overlap, so a specific pattern can take precedence over a general one.

## Command line

`piimask` installs a small CLI for quick masking and pipelines:

```bash
echo "email me at a@b.com" | piimask
# email me at <EMAIL_1>

piimask --text "call 415-555-0100" --strategy partial
piimask --detect --text "ssn 123-45-6789"   # prints detections as JSON
```

## Functional API

Prefer plain functions? Every capability has a one-shot form:

```python
from piimask import mask, unmask, detect

masked, vault = mask("hi jane@acme.com")
original = unmask(masked, vault)
found = detect("card 4111 1111 1111 1111")   # -> [Detection(entity_type='CREDIT_CARD', ...)]
```

## How accurate is detection?

Honestly: good, not perfect. `piimask` uses well-tuned regular expressions plus checksums (Luhn for cards, mod-97 for IBANs) to keep false positives low. That approach is fast, transparent, and dependency-free — but it recognizes *patterns*, not *meaning*. It will not catch a person's name written in prose, a mailing address, or a novel identifier it has never seen.

If you need semantic detection (names, locations, organizations), pair `piimask` with a named-entity-recognition model and register the results as custom detections — the vault and unmasking machinery work exactly the same. Treat this library as a strong, reliable first layer, not as a guarantee. Always keep a human in the loop for anything high-stakes.

## FAQ

**Does the masked data ever leave my machine?**
Only if you send it somewhere. `piimask` does no network I/O of any kind. The vault lives in memory unless you explicitly serialize it.

**Will the placeholders confuse the LLM?**
Rarely. Models handle `<EMAIL_1>` tokens well and keep them consistent. If a particular model does better with natural-looking input, switch to the reversible `fake` strategy so it sees a plausible email that still maps back to the real one.

**Is this enough for GDPR / HIPAA compliance?**
No single library makes you compliant. `piimask` is a practical control that reduces exposure; compliance is about your whole system, your contracts, and your processes. Use it as one layer of several.

## Contributing

Issues and pull requests are welcome. To set up locally:

```bash
uv sync --all-extras
uv run pytest
```

## License

MIT — see [LICENSE](LICENSE). Built by Vipul Parmar.
