PHPackages                             cleaniquecoders/pii-protection - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Security](/categories/security)
4. /
5. cleaniquecoders/pii-protection

ActiveLibrary[Security](/categories/security)

cleaniquecoders/pii-protection
==============================

Pure-PHP PII protection: field-level encryption at rest + masking of sensitive fields in audit/log payloads. No framework, no global state — plain classes with explicit inputs and outputs, usable anywhere.

1.3.2(2w ago)51761MITPHPPHP ^8.4CI passing

Since May 31Pushed 2w agoCompare

[ Source](https://github.com/cleaniquecoders/pii-protection)[ Packagist](https://packagist.org/packages/cleaniquecoders/pii-protection)[ Docs](https://github.com/cleaniquecoders/pii-protection)[ GitHub Sponsors](https://github.com/cleaniquecoders)[ RSS](/packages/cleaniquecoders-pii-protection/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (6)Dependencies (8)Versions (12)Used By (0)

PII Protection
==============

[](#pii-protection)

[![Latest Version on Packagist](https://camo.githubusercontent.com/90909f07bc430379288abec272e0282bd57b72522feaa50890316353649c6d0f/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f636c65616e69717565636f646572732f7069692d70726f74656374696f6e2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/cleaniquecoders/pii-protection) [![Tests](https://camo.githubusercontent.com/f09abf4961c7c03b4b04c6bb003d48c5b8bf31fa480d5e080f5d13b025aa8387/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f616374696f6e732f776f726b666c6f772f7374617475732f636c65616e69717565636f646572732f7069692d70726f74656374696f6e2f72756e2d74657374732e796d6c3f6272616e63683d6d61696e266c6162656c3d7465737473267374796c653d666c61742d737175617265)](https://github.com/cleaniquecoders/pii-protection/actions/workflows/run-tests.yml) [![Total Downloads](https://camo.githubusercontent.com/7bb9ebaf99ea49c6f6e0868e9aed2689ddc308d4a771a776a317e4dd9930536d/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f636c65616e69717565636f646572732f7069692d70726f74656374696f6e2e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/cleaniquecoders/pii-protection) [![PHP Version](https://camo.githubusercontent.com/ce4a3456743d6eaf8f09e0615181993011fc12587684a43061b0909023361ec3/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f646570656e64656e63792d762f636c65616e69717565636f646572732f7069692d70726f74656374696f6e2f7068703f7374796c653d666c61742d737175617265)](https://packagist.org/packages/cleaniquecoders/pii-protection) [![License](https://camo.githubusercontent.com/dd4992e46ed54443bd83781db545fe0b28f08695f11f53bf2b89a7186d335f03/68747470733a2f2f696d672e736869656c64732e696f2f6769746875622f6c6963656e73652f636c65616e69717565636f646572732f7069692d70726f74656374696f6e3f7374796c653d666c61742d737175617265)](LICENSE.md)

Pure-PHP PII protection: field-level **encryption at rest** + **masking** of sensitive fields in audit/log payloads. No framework, no global state — plain classes with explicit inputs and outputs, usable anywhere.

A small library of portable primitives every app handling personal data (SOC 2 / PDPA / GDPR) needs:

- **Encryption** — reversible encrypt/decrypt of PII for storage at rest (AES-256-GCM).
- **Masking** — render a value partially or fully hidden for display or logs.
- **Redaction** — walk a key/value payload (e.g. change-log old/new values) and mask listed fields before it is persisted.

Everything is constructor-injected. No service container, no boot conventions, no static facades — so it drops into Laravel, Symfony, Slim, a CLI tool, a queue worker, or plain PHP unchanged.

Requirements
------------

[](#requirements)

- PHP `^8.4`
- `ext-openssl`
- `ext-mbstring`

Installation
------------

[](#installation)

You can install the package via composer:

```
composer require cleaniquecoders/pii-protection
```

Usage
-----

[](#usage)

### Quick start

[](#quick-start)

```
use CleaniqueCoders\PiiProtection\PiiManager;
use CleaniqueCoders\PiiProtection\Encryption\OpenSslEncrypter;
use CleaniqueCoders\PiiProtection\Masking\TailStrategy;

$manager = new PiiManager(
    new OpenSslEncrypter(key: $appKey),
    new TailStrategy(visible: 4),
);

$cipher = $manager->encrypt('0123456789');  // persist at rest
$plain  = $manager->decrypt($cipher);       // "0123456789"

$masked = $manager->mask('0123456789');      // "******6789" for display
$audit  = $manager->redact($payload, ['phone', 'national_id']);
```

### Masking strategies

[](#masking-strategies)

Each strategy implements `MaskStrategy::mask(string $value): string`.

Each strategy implements `MaskStrategy::mask(string $value): string`.

StrategyBehaviourReversible`TailStrategy`Keep last N chars, mask the rest (`******6789`)No`FullStrategy`Mask every char (`**********`)No`EmailStrategy`Mask local-part, keep domain (`****@acme.com`)No`HashStrategy`Replace with a one-way `sha256` digestNo`CreditCardStrategy`Keep last 4 digits, preserve grouping (`**** **** **** 1111`)No`IpStrategy`Mask the last octet/group (`192.168.1.**`)No`NameStrategy`Keep each word's initial (`J*** D**`)No`NricStrategy`Mask MyKad digits, keep dashes (`******-**-****`)No```
use CleaniqueCoders\PiiProtection\Masking\TailStrategy;
use CleaniqueCoders\PiiProtection\Masking\FullStrategy;
use CleaniqueCoders\PiiProtection\Masking\EmailStrategy;
use CleaniqueCoders\PiiProtection\Masking\HashStrategy;
use CleaniqueCoders\PiiProtection\Masking\CreditCardStrategy;
use CleaniqueCoders\PiiProtection\Masking\IpStrategy;
use CleaniqueCoders\PiiProtection\Masking\NameStrategy;
use CleaniqueCoders\PiiProtection\Masking\NricStrategy;

(new TailStrategy(visible: 4))->mask('0123456789');   // "******6789"
(new FullStrategy())->mask('0123456789');             // "**********"
(new EmailStrategy())->mask('john.doe@acme.com');     // "****@acme.com"
(new HashStrategy())->mask('0123456789');             // "f4b0...e21" (sha256)
(new CreditCardStrategy())->mask('4111 1111 1111 1111'); // "**** **** **** 1111"
(new IpStrategy())->mask('192.168.1.42');             // "192.168.1.**"
(new NameStrategy())->mask('John Doe');               // "J*** D**"
(new NricStrategy())->mask('900101-01-1234');         // "******-**-****"
```

Every strategy takes an optional `maskChar` (default `*`) so you can render with `•`, `x`, or any character:

```
(new TailStrategy(visible: 4, maskChar: '•'))->mask('0123456789'); // "••••••6789"
```

### Encryption at rest

[](#encryption-at-rest)

`OpenSslEncrypter` uses AES-256-GCM. The key is injected via the constructor — the library never reads env/config. Each message uses a random IV and a random HKDF salt, so encrypting the same value twice yields different ciphertext.

```
use CleaniqueCoders\PiiProtection\Encryption\OpenSslEncrypter;

$encrypter = new OpenSslEncrypter(key: $appKey);

$cipher = $encrypter->encrypt('012345678'); // store this
$plain  = $encrypter->decrypt($cipher);     // "012345678"
```

Ciphertext is written in a self-describing, versioned format (`v2..`). Ciphertext produced by 1.0/1.1 still decrypts unchanged — upgrades are seamless.

**Context binding (AAD)** — bind ciphertext to a context (user id, column name) so it cannot be moved between rows/columns. The same context is required to decrypt:

```
$cipher = $encrypter->encryptWithContext('012345678', 'user:123');
$plain  = $encrypter->decryptWithContext($cipher, 'user:123'); // wrong context throws
```

**Key rotation** — give it a `KeyRing` with multiple keys: new ciphertext uses the current key, while older ciphertext keeps decrypting with whichever key its id points to. No big-bang re-encryption needed.

```
use CleaniqueCoders\PiiProtection\Encryption\KeyRing;

$encrypter = new OpenSslEncrypter(new KeyRing(
    ['2024' => $oldKey, '2025' => $newKey],
    currentId: '2025', // encrypt with this; '2024' ciphertext still decrypts
));
```

### Searchable lookups (blind index)

[](#searchable-lookups-blind-index)

Encryption is non-deterministic, so you cannot query an encrypted column. Store a deterministic `HmacBlindIndex` alongside the ciphertext and query that instead — it is one-way and only confirms a match, never reveals the value.

```
use CleaniqueCoders\PiiProtection\Encryption\HmacBlindIndex;

$blind = new HmacBlindIndex(key: $indexKey);

$row = [
    'email_cipher' => $encrypter->encrypt($email),  // for retrieval/display
    'email_index'  => $blind->index($email),        // for WHERE email_index = ?
];

$blind->matches($email, $row['email_index']); // true
```

### Scrubbing free text

[](#scrubbing-free-text)

`PiiScrubber` masks PII patterns inside free text (log lines, messages), not just named fields — with built-in detectors for email, credit card, Malaysian NRIC, IPv4 and phone numbers.

```
use CleaniqueCoders\PiiProtection\Detection\PiiScrubber;

(new PiiScrubber)->scrub('contact john@acme.com from 192.168.1.42');
// "contact ************* from ************"

(new PiiScrubber)->detect($logLine); // [['type' => 'email', 'value' => ..., 'offset' => ...], ...]
```

Or mask any custom pattern with `RegexStrategy`:

```
use CleaniqueCoders\PiiProtection\Masking\RegexStrategy;
use CleaniqueCoders\PiiProtection\Masking\TailStrategy;

(new RegexStrategy('/\d{10}/', new TailStrategy(visible: 4)))->mask('ref 0123456789');
// "ref ******6789"
```

### Redaction of payloads

[](#redaction-of-payloads)

`ArrayRedactor` generalises change-log masking: given a payload (e.g. with `old_values` / `new_values`, or any nested key/value map) and a list of sensitive fields, it applies the chosen `MaskStrategy` to each listed field — recursing into nested arrays and JSON-decoded structures — and leaves every other field untouched.

```
use CleaniqueCoders\PiiProtection\ArrayRedactor;
use CleaniqueCoders\PiiProtection\Masking\TailStrategy;

$redactor = new ArrayRedactor(new TailStrategy(visible: 4));

$clean = $redactor->redact($payload, ['phone', 'national_id']);
```

**Per-field strategies** — map each field to its own strategy in a single pass (plain field names still use the redactor's default strategy):

```
$clean = $redactor->redact($payload, [
    'email' => new EmailStrategy,
    'phone' => new TailStrategy(visible: 4),
    'nric'  => new HashStrategy,
    'name',  // uses the default strategy
]);
```

**Dot-path &amp; wildcard targeting** — target a precise location instead of any key with that name; `*` matches any key at that level:

```
$clean = $redactor->redact($payload, [
    'user.phone',        // only user.phone, not a top-level "phone"
    'users.*.phone',     // every users[].phone
    'contact.email' => new EmailStrategy,
]);
```

### Redacting objects / DTOs

[](#redacting-objects--dtos)

Tag properties with `#[Pii]` and let `ObjectRedactor` mask them into an array. A tag can name its own strategy; otherwise the redactor's default is used.

```
use CleaniqueCoders\PiiProtection\Attributes\Pii;
use CleaniqueCoders\PiiProtection\ObjectRedactor;
use CleaniqueCoders\PiiProtection\Masking\EmailStrategy;

class User
{
    public function __construct(
        #[Pii] public string $name,
        #[Pii(strategy: EmailStrategy::class)] public string $email,
        public int $age, // untagged — copied through untouched
    ) {}
}

$clean = (new ObjectRedactor(new FullStrategy))->redact($user);
// ['name' => '********', 'email' => '****@acme.com', 'age' => 30]
```

### Tokenization

[](#tokenization)

Swap a PII value for an opaque, random token and keep the mapping in a `Vault`. An in-memory `ArrayVault` ships with the package; implement `Vault` to persist tokens elsewhere.

```
use CleaniqueCoders\PiiProtection\Tokenization\Tokenizer;
use CleaniqueCoders\PiiProtection\Tokenization\ArrayVault;

$tokenizer = new Tokenizer(new ArrayVault);

$token = $tokenizer->tokenize('012345678'); // "tok_9f3a..." — reveals nothing
$tokenizer->detokenize($token);             // "012345678"
$tokenizer->forget($token);                 // drop the mapping
```

Errors
------

[](#errors)

Encryption/decryption failures throw a typed exception under `CleaniqueCoders\PiiProtection\Exceptions\`: `EncryptionException` and `DecryptionException`, both extending `PiiException` (itself a `RuntimeException`, so existing catch blocks keep working).

Guardrail — never encrypt lookup values
---------------------------------------

[](#guardrail--never-encrypt-lookup-values)

> **Never query on an encrypted column.** Ciphertext is non-deterministic (random IV + salt per call) and will not match across rows or queries. To support equality lookups, store an `HmacBlindIndex` alongside the ciphertext and query that — or `mask`/`hash` the value if you don't need to reverse it.

Architecture
------------

[](#architecture)

```
src/
├── Contracts/
│   ├── Encrypter.php          # encrypt(string): string ; decrypt(string): string
│   ├── ContextualEncrypter.php # adds AAD-bound encrypt/decrypt
│   ├── MaskStrategy.php       # mask(string): string
│   ├── Redactor.php           # redact(array $data, array $fields): array
│   └── Vault.php              # token  value store contract
├── Masking/
│   ├── TailStrategy.php       # keep last N chars (default 4)
│   ├── FullStrategy.php       # mask everything
│   ├── EmailStrategy.php      # mask local-part, keep domain
│   ├── HashStrategy.php       # one-way (sha256) for non-reversible PII
│   ├── CreditCardStrategy.php # keep last 4 digits, preserve grouping
│   ├── IpStrategy.php         # mask the last octet/group
│   ├── NameStrategy.php       # keep each word's initial
│   ├── NricStrategy.php       # mask Malaysian MyKad digits, keep dashes
│   └── RegexStrategy.php      # mask substrings matching a pattern
├── Encryption/
│   ├── OpenSslEncrypter.php   # AES-256-GCM, HKDF, AAD, versioned format
│   ├── KeyRing.php            # multi-key holder for key rotation
│   └── HmacBlindIndex.php     # deterministic index for searchable lookups
├── Detection/
│   └── PiiScrubber.php        # detect/scrub PII patterns in free text
├── Tokenization/
│   ├── Tokenizer.php          # value  opaque token
│   └── ArrayVault.php         # in-memory Vault implementation
├── Attributes/
│   └── Pii.php                # #[Pii] property marker
├── Exceptions/
│   ├── PiiException.php        # base (extends RuntimeException)
│   ├── EncryptionException.php
│   └── DecryptionException.php
├── ArrayRedactor.php          # nested + JSON + per-field + dot-path redaction
├── ObjectRedactor.php         # redacts #[Pii]-tagged object properties
└── PiiManager.php             # convenience wrapper over strategy + encrypter

```

### Design notes

[](#design-notes)

1. **Single responsibility per class.** Strategies, encrypter, redactor, and the wrapper are independent and swappable; consumers depend on the contracts, not the concretions.
2. **Configurable visible tail.** `TailStrategy(visible: N)` — default 4.
3. **Nested / JSON PII.** `ArrayRedactor` recurses, so structured columns are covered, not just flat scalars.
4. **Key handling is the caller's job.** `OpenSslEncrypter` takes a key (or a `KeyRing`) in its constructor; the library never reads env/config. Rotation is supported via the ring, but loading/storing keys is up to you.

Documentation
-------------

[](#documentation)

Full documentation lives in [`docs/`](docs/README.md):

- [Architecture](docs/01-architecture/README.md) — primitives, contracts, design decisions.
- [Usage](docs/02-usage/README.md) — masking, encryption &amp; key rotation, redaction, detection, tokenization.
- [Development](docs/03-development/README.md) — testing, quality tooling, releases.
- [API Reference](docs/04-api/README.md) — every public class and method.

Testing
-------

[](#testing)

```
composer test          # Pest test suite
composer analyse       # PHPStan (level max)
composer test-mutate   # Pest mutation testing (needs a coverage driver)
```

Changelog
---------

[](#changelog)

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

Contributing
------------

[](#contributing)

Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details.

Security Vulnerabilities
------------------------

[](#security-vulnerabilities)

Please review [our security policy](../../security/policy) on how to report security vulnerabilities.

Credits
-------

[](#credits)

- [Nasrul Hazim Bin Mohamad](https://github.com/nasrulhazim)
- [All Contributors](../../contributors)

License
-------

[](#license)

The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

###  Health Score

49

—

FairBetter than 94% of packages

Maintenance97

Actively maintained with recent releases

Popularity21

Limited adoption so far

Community10

Small or concentrated contributor base

Maturity58

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 79.2% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~8 days

Total

6

Last Release

15d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/b57069d0f4b634f65eccc6e5d5848990e25968d45ec2cf46d626c6a4658f944b?d=identicon)[nasrulhazim.m](/maintainers/nasrulhazim.m)

---

Top Contributors

[![nasrulhazim](https://avatars.githubusercontent.com/u/10341422?v=4)](https://github.com/nasrulhazim "nasrulhazim (19 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (3 commits)")[![github-actions[bot]](https://avatars.githubusercontent.com/in/15368?v=4)](https://github.com/github-actions[bot] "github-actions[bot] (2 commits)")

---

Tags

cleaniquecoderspii-protection

###  Code Quality

TestsPest

Static AnalysisPHPStan

Code StyleLaravel Pint

Type Coverage Yes

### Embed Badge

![Health badge](/badges/cleaniquecoders-pii-protection/health.svg)

```
[![Health](https://phpackages.com/badges/cleaniquecoders-pii-protection/health.svg)](https://phpackages.com/packages/cleaniquecoders-pii-protection)
```

###  Alternatives

[mews/purifier

Laravel 5/6/7/8/9/10 HtmlPurifier Package

2.0k18.7M144](/packages/mews-purifier)[paragonie/ecc

PHP Elliptic Curve Cryptography library

24820.0k41](/packages/paragonie-ecc)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
