Metadata-Version: 2.5
Name: piimask
Version: 0.2.0
Summary: Reversible PII masking and unmasking for the LLM era. Hide personal data 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 reply comes in.

[![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 I built this

Back when generative AI started taking off, around 2023 and 2024, a senior on my team got worried about personal data. People were pasting real customer details into these models just to get work done, and he asked me to find a way to handle it.

So I looked at what was already out there. The free libraries could mask data but they couldn't put it back. The ones that did both were paid. I needed both halves: hide the data on the way to the model, and restore it in the answer. So I read up on how the paid tools worked and wrote my own.

I built most of this back then and it sat in a notebook. I finally got some time to clean it up and turn it into a proper package, so here it is.

## 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 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 the real thing.

## 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 fast 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))
```

The same value always maps to the same token within a session, so you can run a whole multi-turn conversation through one `Anonymizer` and the model keeps treating `<EMAIL_1>` as the same person across every message.

## What it detects

Out of the box, `piimask` picks up 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 to 15 digits, ISO dates excluded      |
| `CREDIT_CARD`   | `4111 1111 1111 1111`                      | checked with the Luhn algorithm         |
| `SSN`           | `123-45-6789`                              | US format                               |
| `IBAN`          | `GB82 WEST 1234 5698 7654 32`              | checked with the ISO 13616 mod-97 sum   |
| `IP_ADDRESS`    | `192.168.1.20`                             | strict 0 to 255 octets                  |
| `IPV6`          | `2001:db8::1`                              |                                         |
| `URL`           | `https://acme.com/x`                       | trailing punctuation is left alone      |

Need something else, like an employee ID, a policy number, or an internal hostname? You can add a recognizer in one line (see below).

## Masking strategies

The strategy decides **how** a detected value gets replaced. Three are reversible, and one is one-way for cases where you never want the value back.

| Strategy        | Reversible | Example output        | Best for                                 |
| --------------- | :--------: | --------------------- | ---------------------------------------- |
| `placeholder`   |    yes     | `<EMAIL_1>`           | LLM round-trips (the default)            |
| `fake`          |    yes     | `kevin83@hotmail.com` | prompts that behave better on realistic input |
| `hash`          |   yes (*)  | `<EMAIL_9f2c1a2b3c>`  | stable tokens for dedup and joining logs |
| `partial`       |    no      | `j***@***.com`        | human-readable previews and support UIs  |

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

anon = Anonymizer(strategy="hash", salt="pepper")
masked = anon.mask("a@b.com and a@b.com")
# both copies get the same token, e.g.
# "<EMAIL_9f2c1a2b3c> and <EMAIL_9f2c1a2b3c>"
anon.unmask(masked)   # "a@b.com and a@b.com", restored from the vault
```

The `hash` strategy is deterministic: the same input always produces the same token, even in a fresh session, so you can count distinct users or join two logs on the token. It's reversible too, but only while you keep the vault. Each token is stored as `token -> original` when you mask, so `unmask` can restore it locally. Throw the vault away and the tokens are effectively one-way, which is what you want for logs you never need to turn back into real data.

(*) `hash` is reversible only while you hold the vault.

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

> **Warning:** a vault holds 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 have to 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)
```

Keep the key somewhere separate from the blob. Anyone who has both can recover the data.

## Custom recognizers

Anything you can describe with a regex, you can mask. Give it a name and, if you want, 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?

Good, but 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 has no dependencies, but it matches patterns, not meaning. It won't catch a person's name written in prose, a mailing address, or some new identifier it has never seen.

If you need that kind of detection (names, locations, organizations), pair `piimask` with a named-entity-recognition model and register the results as custom detections. The vault and unmasking work exactly the same. Treat this library as a strong first layer, not a guarantee, and 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 calls of any kind. The vault lives in memory unless you serialize it yourself.

**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 or 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.
