dynamik-dev / cloak-php
Redact sensitive data from strings and reveal them later using placeholder tokens
Installs: 62
Dependents: 1
Suggesters: 0
Security: 0
Stars: 2
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/dynamik-dev/cloak-php
Requires
- php: ^8.2
- giggsey/libphonenumber-for-php: ^9.0
Requires (Dev)
- laravel/pint: ^1.26
- pestphp/pest: ^3.0
- phpstan/phpstan: ^2.0
This package is auto-updated.
Last update: 2025-11-28 18:23:11 UTC
README
A simple, extensible PHP package for redacting sensitive data from strings and revealing them later.
$cloaked = cloak('Email me at john@example.com'); // "Email me at {{EMAIL_x7k2m9_1}}" $original = uncloak($cloaked); // "Email me at john@example.com"
Installation
composer require dynamik-dev/cloak-php
Requirements
- PHP 8.2+
- ext-mbstring (required by libphonenumber)
Quick Start
Using Helper Functions
$text = 'Contact: john@example.com, Phone: 555-123-4567'; $cloaked = cloak($text); // "Contact: {{EMAIL_x7k2m9_1}}, Phone: {{PHONE_x7k2m9_1}}" $original = uncloak($cloaked); // "Contact: john@example.com, Phone: 555-123-4567"
Using Specific Detectors
use DynamikDev\Cloak\Detector; // Only detect emails $cloaked = cloak($text, [Detector::email()]); // Multiple detectors $cloaked = cloak($text, [ Detector::email(), Detector::phone('US'), Detector::ssn(), ]);
Using the Cloak Class
For more control, use the Cloak class directly:
use DynamikDev\Cloak\Cloak; $cloak = Cloak::make(); $cloaked = $cloak->cloak($text); $original = $cloak->uncloak($cloaked);
Configuring with Builder Methods
use DynamikDev\Cloak\Cloak; use DynamikDev\Cloak\Detector; use DynamikDev\Cloak\Encryptors\OpenSslEncryptor; $cloak = Cloak::make() ->withDetectors([Detector::email()]) ->encrypt(OpenSslEncryptor::generateKey()); $cloaked = $cloak->cloak('Sensitive: john@example.com');
Built-in Detectors
Cloak provides several built-in detectors for common sensitive data types:
use DynamikDev\Cloak\Detector; Detector::email(); // Email addresses Detector::phone('US'); // Phone numbers (specify region code) Detector::ssn(); // Social Security Numbers (XXX-XX-XXXX) Detector::creditCard(); // Credit card numbers Detector::all(); // All built-in detectors (uses US for phone)
Phone Number Detection
Phone detection uses libphonenumber-for-php for robust international phone number validation with intelligent false positive prevention.
Features:
- International format support for all countries
- Validates actual phone numbers (not just digit patterns)
- Filters out order IDs, timestamps, serial numbers, etc.
- Handles various formats:
(212) 456-7890,212-456-7890,+44 117 496 0123
Examples:
// US numbers Detector::phone('US')->detect('Call 212-456-7890'); // UK numbers Detector::phone('GB')->detect('Ring 0117 496 0123'); // International format Detector::phone()->detect('Call +44 117 496 0123'); // Filters false positives Detector::phone('US')->detect('Order #123456789012'); // Not detected
Custom Detectors
Cloak supports two approaches for custom detectors: factory methods (concise) and direct instantiation (explicit).
Pattern-based Detector
use DynamikDev\Cloak\Detector; use DynamikDev\Cloak\Detectors\Pattern; // Factory method (concise) $detector = Detector::pattern('/\b[A-Z]{2}\d{6}\b/', 'passport'); // Direct instantiation (explicit) $detector = new Pattern('/\b[A-Z]{2}\d{6}\b/', 'passport'); $cloaked = $cloak->cloak('Passport: AB123456', [$detector]); // "Passport: {{PASSPORT_x7k2m9_1}}"
Word-based Detector
use DynamikDev\Cloak\Detector; use DynamikDev\Cloak\Detectors\Words; // Factory method $detector = Detector::words(['password', 'secret'], 'sensitive'); // Direct instantiation $detector = new Words(['password', 'secret'], 'sensitive'); $cloaked = $cloak->cloak('The password is secret123', [$detector]); // "The {{SENSITIVE_x7k2m9_1}} is {{SENSITIVE_x7k2m9_2}}123"
Callable Detector
use DynamikDev\Cloak\Detector; use DynamikDev\Cloak\Detectors\Callback; // Factory method $detector = Detector::using(function (string $text): array { $matches = []; if (preg_match_all('/\bAPI_KEY_\w+\b/', $text, $found)) { foreach ($found[0] as $match) { $matches[] = ['match' => $match, 'type' => 'api_key']; } } return $matches; }); // Direct instantiation $detector = new Callback(function (string $text): array { // ... same logic });
Mixed Approach
You can mix both patterns freely:
use DynamikDev\Cloak\Detector; use DynamikDev\Cloak\Detectors\{Email, Pattern, Words}; $cloak->cloak($text, [ new Email(), // Direct Detector::phone('US'), // Factory new Pattern('/\b[A-Z]{2}\d{6}\b/', 'passport'), // Direct Detector::words(['secret'], 'sensitive'), // Factory ]);
Implementing DetectorInterface
For full control, implement the DetectorInterface:
use DynamikDev\Cloak\Contracts\DetectorInterface; class IpAddressDetector implements DetectorInterface { public function detect(string $text): array { $matches = []; $pattern = '/\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b/'; if (preg_match_all($pattern, $text, $found)) { foreach ($found[0] as $match) { $matches[] = ['match' => $match, 'type' => 'ip_address']; } } return $matches; } } $cloaked = $cloak->cloak('Server: 192.168.1.1', [new IpAddressDetector()]); // "Server: {{IP_ADDRESS_x7k2m9_1}}"
Advanced Features
Filtering Detections
Use filters to exclude certain detections from being cloaked:
// Exclude test emails $cloak = Cloak::make() ->filter(fn ($detection) => !str_ends_with($detection['match'], '@test.local')); $text = 'prod@company.com and test@test.local'; $cloaked = $cloak->cloak($text, [Detector::email()]); // "{{EMAIL_x7k2m9_1}} and test@test.local"
Multiple Filters
Filters are applied in sequence, and all must return true for a detection to be included:
$cloak = Cloak::make() ->filter(fn ($d) => $d['type'] === 'email') ->filter(fn ($d) => !str_contains($d['match'], 'noreply'));
Lifecycle Callbacks
Hook into the cloaking/uncloaking process:
$cloak = Cloak::make() ->beforeCloak(function (string $text) { // Normalize whitespace before processing return preg_replace('/\s+/', ' ', $text); }) ->afterCloak(function (string $original, string $cloaked) { // Log the cloaking operation logger()->info('Cloaked text', ['original_length' => strlen($original)]); }) ->beforeUncloak(function (string $text) { // Validate before uncloaking return $text; }) ->afterUncloak(function (string $text) { // Post-process after uncloaking return trim($text); });
Encryption
Encrypt sensitive values at rest using the convenient encrypt() method:
use DynamikDev\Cloak\Encryptors\OpenSslEncryptor; $cloak = Cloak::make() ->encrypt(OpenSslEncryptor::generateKey()); $cloaked = $cloak->cloak('Secret: john@example.com', [Detector::email()]); // Values are encrypted in storage, but placeholders remain the same
Environment Variable Support:
// Reads from CLOAK_PRIVATE_KEY environment variable $cloak = Cloak::make()->encrypt(); // Or specify a custom environment variable $encryptor = new OpenSslEncryptor(null, 'MY_ENCRYPTION_KEY'); $cloak = Cloak::make()->withEncryptor($encryptor);
Custom Encryptors:
For full control, use withEncryptor() with a custom implementation:
$customEncryptor = new MyEncryptor($key); $cloak = Cloak::make()->withEncryptor($customEncryptor);
Storage
Default Store (ArrayStore)
By default, Cloak uses ArrayStore for in-memory storage. This is perfect for:
- Testing
- Single-request scenarios
- Simple use cases without persistence
use DynamikDev\Cloak\Stores\ArrayStore; $store = new ArrayStore(); $cloak = Cloak::using($store);
Custom Store Implementation
For persistent storage across requests, implement StoreInterface:
use DynamikDev\Cloak\Contracts\StoreInterface; class RedisStore implements StoreInterface { public function __construct( private Redis $redis, private int $ttl = 3600 ) {} public function put(string $key, array $map): void { $this->redis->setex($key, $this->ttl, json_encode($map)); } public function get(string $key): ?array { $data = $this->redis->get($key); return $data ? json_decode($data, true) : null; } public function forget(string $key): void { $this->redis->del($key); } } // Configure TTL via constructor $cloak = Cloak::using(new RedisStore($redis, ttl: 7200));
Laravel Cache Store Example
use DynamikDev\Cloak\Contracts\StoreInterface; use Illuminate\Support\Facades\Cache; class LaravelCacheStore implements StoreInterface { public function __construct(private int $ttl = 3600) {} public function put(string $key, array $map): void { Cache::put($key, $map, $this->ttl); } public function get(string $key): ?array { return Cache::get($key); } public function forget(string $key): void { Cache::forget($key); } }
Framework Integration
Custom Resolver
For framework adapters (like Laravel, Symfony, etc.), you can override how Cloak::make() resolves instances using resolveUsing():
use DynamikDev\Cloak\Cloak; // Set a custom resolver (typically in a service provider) Cloak::resolveUsing(fn() => app(Cloak::class)); // Now Cloak::make() resolves from your container $cloak = Cloak::make(); // Uses your container binding
This allows framework packages to:
- Integrate with dependency injection containers
- Use framework-specific storage drivers
- Apply framework configuration automatically
- Let developers extend and customize via container bindings
Example Laravel Service Provider:
use DynamikDev\Cloak\Cloak; use Illuminate\Support\ServiceProvider; class CloakServiceProvider extends ServiceProvider { public function register() { // Bind Cloak to the container with your configuration $this->app->bind(Cloak::class, function ($app) { return Cloak::using($app->make(CacheStore::class)) ->withDetectors(config('cloak.detectors', Detector::all())); }); // Make helpers use container resolution Cloak::resolveUsing(fn() => app(Cloak::class)); } }
Now developers can customize behavior through container bindings:
// In AppServiceProvider $this->app->bind(Cloak::class, function ($app) { return CustomCloak::using($app->make(CacheStore::class)); });
Clearing the Resolver:
Cloak::clearResolver(); // Reverts to default behavior
Extending Cloak
Cloak follows a compositional architecture, making it easy to extend with custom implementations.
Custom Placeholder Generator
Create custom placeholder formats by implementing PlaceholderGeneratorInterface:
use DynamikDev\Cloak\Contracts\PlaceholderGeneratorInterface; use Ramsey\Uuid\Uuid; class UuidPlaceholderGenerator implements PlaceholderGeneratorInterface { public function generate(array $detections): array { $key = Uuid::uuid4()->toString(); $map = []; foreach ($detections as $detection) { $uuid = Uuid::uuid4()->toString(); $placeholder = "[{$detection['type']}:{$uuid}]"; $map[$placeholder] = $detection['match']; } return ['key' => $key, 'map' => $map]; } public function replace(string $text, array $map): string { foreach ($map as $placeholder => $original) { $text = str_replace($original, $placeholder, $text); } return $text; } public function parse(string $text): array { // Extract [TYPE:UUID] placeholders and group by key // Implementation details... return []; } } $cloak = Cloak::make() ->withPlaceholderGenerator(new UuidPlaceholderGenerator());
Custom Encryptor
Implement EncryptorInterface for custom encryption strategies:
use DynamikDev\Cloak\Contracts\EncryptorInterface; class SodiumEncryptor implements EncryptorInterface { public function __construct(private string $key) {} public function encrypt(string $value): string { $nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); $encrypted = sodium_crypto_secretbox($value, $nonce, $this->key); return base64_encode($nonce . $encrypted); } public function decrypt(string $encrypted): string { $decoded = base64_decode($encrypted); $nonce = substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); $ciphertext = substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); $decrypted = sodium_crypto_secretbox_open($ciphertext, $nonce, $this->key); if ($decrypted === false) { throw new \RuntimeException('Decryption failed'); } return $decrypted; } } $cloak = Cloak::make() ->withEncryptor(new SodiumEncryptor($key));
Placeholder Format
Placeholders follow the format {{TYPE_KEY_INDEX}}:
TYPE: Uppercase detector type (EMAIL, PHONE, SSN, CREDIT_CARD)KEY: 6-character alphanumeric unique keyINDEX: Integer counter per type, starting at 1
Example: {{EMAIL_x7k2m9_1}}
The default format can be customized by implementing a custom PlaceholderGeneratorInterface.
API Reference
Factory Methods
Cloak::make(?StoreInterface $store = null): self Cloak::using(StoreInterface $store): self
Builder Methods
->withDetectors(array $detectors): self ->filter(callable $callback): self ->withPlaceholderGenerator(PlaceholderGeneratorInterface $generator): self ->withEncryptor(EncryptorInterface $encryptor): self ->encrypt(?string $key = null): self // Convenience method for OpenSslEncryptor
Lifecycle Callbacks
->beforeCloak(callable $callback): self ->afterCloak(callable $callback): self ->beforeUncloak(callable $callback): self ->afterUncloak(callable $callback): self
Core Methods
->cloak(string $text, ?array $detectors = null): string ->uncloak(string $text): string
Edge Cases
- Same value appears multiple times: Reuses the same placeholder
- No detections found: Returns original text unchanged
- Missing cache on uncloak: Leaves placeholder in place
- Empty input: Returns empty string
- Overlapping patterns: All patterns are processed independently
Testing
./vendor/bin/pest
Static Analysis
./vendor/bin/phpstan analyse
License
MIT
