diff --git a/README.md b/README.md index 904b094..e738875 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ use DynamikDev\Cloak\Encryptors\OpenSslEncryptor; $cloak = Cloak::make() ->withDetectors([Detector::email()]) ->withTtl(7200) - ->withEncryptor(new OpenSslEncryptor(OpenSslEncryptor::generateKey())); + ->encrypt(OpenSslEncryptor::generateKey()); $cloaked = $cloak->cloak('Sensitive: john@example.com'); ``` @@ -116,16 +116,21 @@ Detector::phone('US')->detect('Order #123456789012'); // Not detected ## Custom Detectors -Create custom detectors using three convenient methods: +Cloak supports two approaches for custom detectors: **factory methods** (concise) and **direct instantiation** (explicit). ### Pattern-based Detector ```php use DynamikDev\Cloak\Detector; +use DynamikDev\Cloak\Detectors\Pattern; -$passportDetector = Detector::pattern('/\b[A-Z]{2}\d{6}\b/', 'passport'); +// Factory method (concise) +$detector = Detector::pattern('/\b[A-Z]{2}\d{6}\b/', 'passport'); -$cloaked = $cloak->cloak('Passport: AB123456', [$passportDetector]); +// Direct instantiation (explicit) +$detector = new Pattern('/\b[A-Z]{2}\d{6}\b/', 'passport'); + +$cloaked = $cloak->cloak('Passport: AB123456', [$detector]); // "Passport: {{PASSPORT_x7k2m9_1}}" ``` @@ -133,10 +138,15 @@ $cloaked = $cloak->cloak('Passport: AB123456', [$passportDetector]); ```php use DynamikDev\Cloak\Detector; +use DynamikDev\Cloak\Detectors\Words; + +// Factory method +$detector = Detector::words(['password', 'secret'], 'sensitive'); -$sensitiveDetector = Detector::words(['password', 'secret'], 'sensitive'); +// Direct instantiation +$detector = new Words(['password', 'secret'], 'sensitive'); -$cloaked = $cloak->cloak('The password is secret123', [$sensitiveDetector]); +$cloaked = $cloak->cloak('The password is secret123', [$detector]); // "The {{SENSITIVE_x7k2m9_1}} is {{SENSITIVE_x7k2m9_2}}123" ``` @@ -144,8 +154,10 @@ $cloaked = $cloak->cloak('The password is secret123', [$sensitiveDetector]); ```php use DynamikDev\Cloak\Detector; +use DynamikDev\Cloak\Detectors\Callback; -$apiKeyDetector = Detector::using(function (string $text): array { +// 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) { @@ -154,6 +166,27 @@ $apiKeyDetector = Detector::using(function (string $text): array { } return $matches; }); + +// Direct instantiation +$detector = new Callback(function (string $text): array { + // ... same logic +}); +``` + +### Mixed Approach + +You can mix both patterns freely: + +```php +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 @@ -236,16 +269,13 @@ $cloak = Cloak::make() ### Encryption -Encrypt sensitive values at rest using the built-in `OpenSslEncryptor`: +Encrypt sensitive values at rest using the convenient `encrypt()` method: ```php use DynamikDev\Cloak\Encryptors\OpenSslEncryptor; -$key = OpenSslEncryptor::generateKey(); // Generate a secure key -$encryptor = new OpenSslEncryptor($key); - $cloak = Cloak::make() - ->withEncryptor($encryptor); + ->encrypt(OpenSslEncryptor::generateKey()); $cloaked = $cloak->cloak('Secret: john@example.com', [Detector::email()]); // Values are encrypted in storage, but placeholders remain the same @@ -255,10 +285,20 @@ $cloaked = $cloak->cloak('Secret: john@example.com', [Detector::email()]); ```php // Reads from CLOAK_PRIVATE_KEY environment variable -$encryptor = new OpenSslEncryptor(); +$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: + +```php +$customEncryptor = new MyEncryptor($key); +$cloak = Cloak::make()->withEncryptor($customEncryptor); ``` ## Storage @@ -287,11 +327,14 @@ use DynamikDev\Cloak\Contracts\StoreInterface; class RedisStore implements StoreInterface { - public function __construct(private Redis $redis) {} + public function __construct( + private Redis $redis, + private int $ttl = 3600 + ) {} - public function put(string $key, array $map, int $ttl = 3600): void + public function put(string $key, array $map): void { - $this->redis->setex($key, $ttl, json_encode($map)); + $this->redis->setex($key, $this->ttl, json_encode($map)); } public function get(string $key): ?array @@ -306,7 +349,8 @@ class RedisStore implements StoreInterface } } -$cloak = Cloak::using(new RedisStore($redis)); +// Configure TTL via constructor +$cloak = Cloak::using(new RedisStore($redis, ttl: 7200)); ``` ### Laravel Cache Store Example @@ -317,9 +361,11 @@ use Illuminate\Support\Facades\Cache; class LaravelCacheStore implements StoreInterface { - public function put(string $key, array $map, int $ttl = 3600): void + public function __construct(private int $ttl = 3600) {} + + public function put(string $key, array $map): void { - Cache::put($key, $map, $ttl); + Cache::put($key, $map, $this->ttl); } public function get(string $key): ?array @@ -446,10 +492,10 @@ Cloak::using(StoreInterface $store): self ```php ->withDetectors(array $detectors): self -->withTtl(int $ttl): self ->filter(callable $callback): self ->withPlaceholderGenerator(PlaceholderGeneratorInterface $generator): self ->withEncryptor(EncryptorInterface $encryptor): self +->encrypt(?string $key = null): self // Convenience method for OpenSslEncryptor ``` ### Lifecycle Callbacks diff --git a/src/Cloak.php b/src/Cloak.php index ce70134..fd2bab0 100644 --- a/src/Cloak.php +++ b/src/Cloak.php @@ -11,6 +11,7 @@ use DynamikDev\Cloak\Contracts\PlaceholderGeneratorInterface; use DynamikDev\Cloak\Contracts\StoreInterface; use DynamikDev\Cloak\Encryptors\NullEncryptor; +use DynamikDev\Cloak\Encryptors\OpenSslEncryptor; use DynamikDev\Cloak\PlaceholderGenerators\DefaultPlaceholderGenerator; /** @@ -85,6 +86,13 @@ public function withEncryptor(EncryptorInterface $encryptor): self return $this; } + public function encrypt(?string $key = null): self + { + $this->encryptor = new OpenSslEncryptor($key); + + return $this; + } + /** * @param array|null $detectors */ @@ -106,7 +114,7 @@ public function cloak(string $text, ?array $detectors = null): string $key = $result['key']; $map = $result['map']; - $this->store->put('cloak:' . $key, $this->encryptMap($map), $this->ttl); + $this->store->put('cloak:' . $key, $this->encryptMap($map)); $cloaked = $this->placeholderGenerator->replace($processedText, $map); diff --git a/src/Concerns/ManagesStorage.php b/src/Concerns/ManagesStorage.php index d812b31..540ca6d 100644 --- a/src/Concerns/ManagesStorage.php +++ b/src/Concerns/ManagesStorage.php @@ -11,15 +11,6 @@ trait ManagesStorage { protected static ?StoreInterface $defaultStore = null; - protected int $ttl = 3600; - - public function withTtl(int $ttl): self - { - $this->ttl = $ttl; - - return $this; - } - protected static function getDefaultStore(): StoreInterface { if (self::$defaultStore === null) { diff --git a/src/Contracts/StoreInterface.php b/src/Contracts/StoreInterface.php index f262225..8095244 100644 --- a/src/Contracts/StoreInterface.php +++ b/src/Contracts/StoreInterface.php @@ -11,9 +11,8 @@ interface StoreInterface * * @param string $key The unique storage key * @param array $map Placeholder to original value mapping - * @param int $ttl Time to live in seconds */ - public function put(string $key, array $map, int $ttl = 3600): void; + public function put(string $key, array $map): void; /** * Retrieve a mapping by key. diff --git a/src/Detector.php b/src/Detector.php index 27d8eea..7d8175e 100644 --- a/src/Detector.php +++ b/src/Detector.php @@ -4,11 +4,13 @@ namespace DynamikDev\Cloak; -use DynamikDev\Cloak\Contracts\DetectorInterface; +use DynamikDev\Cloak\Detectors\Callback; use DynamikDev\Cloak\Detectors\CreditCard; use DynamikDev\Cloak\Detectors\Email; +use DynamikDev\Cloak\Detectors\Pattern; use DynamikDev\Cloak\Detectors\Phone; use DynamikDev\Cloak\Detectors\SSN; +use DynamikDev\Cloak\Detectors\Words; /** * Factory class for creating detector instances. @@ -40,7 +42,7 @@ public static function creditCard(): CreditCard } /** - * @return array + * @return array */ public static function all(): array { @@ -52,88 +54,24 @@ public static function all(): array ]; } - public static function pattern(string $regex, string $type): DetectorInterface + public static function pattern(string $regex, string $type): Pattern { - return new class ($regex, $type) implements DetectorInterface { - public function __construct( - private readonly string $regex, - private readonly string $type - ) { - } - - /** - * @return array - */ - public function detect(string $text): array - { - preg_match_all($this->regex, $text, $matches); - - return array_map( - fn (string $match): array => ['match' => $match, 'type' => $this->type], - $matches[0] - ); - } - }; + return new Pattern($regex, $type); } /** * @param array $words */ - public static function words(array $words, string $type): DetectorInterface + public static function words(array $words, string $type): Words { - return new class ($words, $type) implements DetectorInterface { - /** - * @param array $words - */ - public function __construct( - private readonly array $words, - private readonly string $type - ) { - } - - /** - * @return array - */ - public function detect(string $text): array - { - $matches = []; - $lowerText = strtolower($text); - - foreach ($this->words as $word) { - if (($pos = strpos($lowerText, strtolower($word))) !== false) { - $matches[] = [ - 'match' => substr($text, $pos, strlen($word)), - 'type' => $this->type, - ]; - } - } - - return $matches; - } - }; + return new Words($words, $type); } /** * @param callable(string): array $callback */ - public static function using(callable $callback): DetectorInterface + public static function using(callable $callback): Callback { - return new class ($callback) implements DetectorInterface { - /** - * @param callable(string): array $callback - */ - public function __construct( - private readonly mixed $callback - ) { - } - - /** - * @return array - */ - public function detect(string $text): array - { - return ($this->callback)($text); - } - }; + return new Callback($callback); } } diff --git a/src/Detectors/Callback.php b/src/Detectors/Callback.php new file mode 100644 index 0000000..ed09e7e --- /dev/null +++ b/src/Detectors/Callback.php @@ -0,0 +1,23 @@ + $callback + */ + public function __construct( + private readonly mixed $callback + ) { + } + + public function detect(string $text): array + { + return ($this->callback)($text); + } +} diff --git a/src/Detectors/Pattern.php b/src/Detectors/Pattern.php new file mode 100644 index 0000000..92c5b05 --- /dev/null +++ b/src/Detectors/Pattern.php @@ -0,0 +1,26 @@ +regex, $text, $matches); + + return array_map( + fn (string $match): array => ['match' => $match, 'type' => $this->type], + $matches[0] + ); + } +} diff --git a/src/Detectors/Words.php b/src/Detectors/Words.php new file mode 100644 index 0000000..fbf9629 --- /dev/null +++ b/src/Detectors/Words.php @@ -0,0 +1,36 @@ + $words + */ + public function __construct( + private readonly array $words, + private readonly string $type + ) { + } + + public function detect(string $text): array + { + $matches = []; + $lowerText = strtolower($text); + + foreach ($this->words as $word) { + if (($pos = strpos($lowerText, strtolower($word))) !== false) { + $matches[] = [ + 'match' => substr($text, $pos, strlen($word)), + 'type' => $this->type, + ]; + } + } + + return $matches; + } +} diff --git a/src/Stores/ArrayStore.php b/src/Stores/ArrayStore.php index 8118c28..1c81375 100644 --- a/src/Stores/ArrayStore.php +++ b/src/Stores/ArrayStore.php @@ -19,7 +19,7 @@ class ArrayStore implements StoreInterface /** * @param array $map */ - public function put(string $key, array $map, int $ttl = 3600): void + public function put(string $key, array $map): void { $this->data[$key] = $map; } diff --git a/tests/BuilderTest.php b/tests/BuilderTest.php index 6248743..a2d272b 100644 --- a/tests/BuilderTest.php +++ b/tests/BuilderTest.php @@ -26,19 +26,10 @@ expect($result)->toContain('123-45-6789'); }); -it('can chain withTtl method', function () { - $store = new ArrayStore(); - $cloak = Cloak::using($store) - ->withTtl(7200); - - expect($cloak)->toBeInstanceOf(Cloak::class); -}); - it('can chain multiple builder methods', function () { $store = new ArrayStore(); $cloak = Cloak::using($store) ->withDetectors([Detector::email()]) - ->withTtl(7200) ->filter(fn ($d) => strlen($d['match']) > 5); expect($cloak)->toBeInstanceOf(Cloak::class); diff --git a/tests/EncryptionTest.php b/tests/EncryptionTest.php index dde4216..f8b7edd 100644 --- a/tests/EncryptionTest.php +++ b/tests/EncryptionTest.php @@ -26,11 +26,11 @@ expect($stored[$cloaked])->toBe('test@example.com'); }); -it('encrypts values when withEncryptor() is called', function () { +it('encrypts values when encrypt() is called', function () { $encryptionKey = OpenSslEncryptor::generateKey(); $store = new ArrayStore(); $cloak = Cloak::using($store) - ->withEncryptor(new OpenSslEncryptor($encryptionKey)); + ->encrypt($encryptionKey); $cloaked = $cloak->cloak('test@example.com', [Detector::email()]); preg_match('/\{\{EMAIL_([a-zA-Z0-9]{6})_1\}\}/', $cloaked, $matches); @@ -46,7 +46,7 @@ $encryptionKey = OpenSslEncryptor::generateKey(); $store = new ArrayStore(); $cloak = Cloak::using($store) - ->withEncryptor(new OpenSslEncryptor($encryptionKey)); + ->encrypt($encryptionKey); $original = 'Contact: test@example.com Phone: 555-123-4567'; $cloaked = $cloak->cloak($original, [Detector::email(), Detector::phone()]); @@ -89,7 +89,7 @@ $encryptionKey = OpenSslEncryptor::generateKey(); $store = new ArrayStore(); $cloak = Cloak::using($store) - ->withEncryptor(new OpenSslEncryptor($encryptionKey)); + ->encrypt($encryptionKey); $original = 'Email: test@example.com, Phone: 555-123-4567, SSN: 123-45-6789'; $cloaked = $cloak->cloak($original, [ @@ -106,7 +106,7 @@ $encryptionKey = OpenSslEncryptor::generateKey(); $store = new ArrayStore(); $cloak = Cloak::using($store) - ->withEncryptor(new OpenSslEncryptor($encryptionKey)); + ->encrypt($encryptionKey); $original = 'test@example.com and test@example.com'; $cloaked = $cloak->cloak($original, [Detector::email()]); @@ -123,7 +123,7 @@ $encryptionKey = OpenSslEncryptor::generateKey(); $store = new ArrayStore(); $cloak = Cloak::using($store) - ->withEncryptor(new OpenSslEncryptor($encryptionKey)) + ->encrypt($encryptionKey) ->filter(fn ($d) => !str_ends_with($d['match'], '.local')); $text = 'prod@company.com test@test.local'; @@ -144,7 +144,7 @@ $log = []; $cloak = Cloak::using($store) - ->withEncryptor(new OpenSslEncryptor($encryptionKey)) + ->encrypt($encryptionKey) ->beforeCloak(function ($text) use (&$log) { $log[] = 'before'; @@ -167,19 +167,18 @@ $key2 = OpenSslEncryptor::generateKey(); $store = new ArrayStore(); - $cloak1 = Cloak::using($store)->withEncryptor(new OpenSslEncryptor($key1)); + $cloak1 = Cloak::using($store)->encrypt($key1); $cloaked = $cloak1->cloak('test@example.com', [Detector::email()]); - $cloak2 = Cloak::using($store)->withEncryptor(new OpenSslEncryptor($key2)); + $cloak2 = Cloak::using($store)->encrypt($key2); $cloak2->uncloak($cloaked); })->throws(RuntimeException::class); -it('handles encryption with withTtl()', function () { +it('handles encryption with builder methods', function () { $encryptionKey = OpenSslEncryptor::generateKey(); $store = new ArrayStore(); $cloak = Cloak::using($store) - ->withTtl(7200) - ->withEncryptor(new OpenSslEncryptor($encryptionKey)); + ->encrypt($encryptionKey); $original = 'test@example.com'; $cloaked = $cloak->cloak($original, [Detector::email()]); @@ -193,7 +192,7 @@ $store = new ArrayStore(); $cloak = Cloak::using($store) ->withDetectors([Detector::email()]) - ->withEncryptor(new OpenSslEncryptor($encryptionKey)); + ->encrypt($encryptionKey); $text = 'Email: test@example.com Phone: 555-123-4567'; $cloaked = $cloak->cloak($text); @@ -210,8 +209,8 @@ $store = new ArrayStore(); $cloak = Cloak::using($store) - ->withEncryptor(new OpenSslEncryptor($key1)) - ->withEncryptor(new OpenSslEncryptor($key2)); + ->encrypt($key1) + ->encrypt($key2); $original = 'test@example.com'; $cloaked = $cloak->cloak($original, [Detector::email()]); @@ -224,7 +223,7 @@ $encryptionKey = OpenSslEncryptor::generateKey(); $store = new ArrayStore(); $cloak = Cloak::using($store) - ->withEncryptor(new OpenSslEncryptor($encryptionKey)); + ->encrypt($encryptionKey); $result = $cloak->cloak('No sensitive data here', [Detector::email()]); diff --git a/tests/LifecycleCallbacksTest.php b/tests/LifecycleCallbacksTest.php index feae18f..5ebf953 100644 --- a/tests/LifecycleCallbacksTest.php +++ b/tests/LifecycleCallbacksTest.php @@ -166,7 +166,6 @@ $cloak = Cloak::using($store) ->withDetectors([Detector::email()]) - ->withTtl(7200) ->filter(fn ($d) => !str_ends_with($d['match'], '.local')) ->beforeCloak(function ($text) use (&$log) { $log[] = 'before';