Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 66 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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');
```
Expand Down Expand Up @@ -116,36 +116,48 @@ 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}}"
```

### Word-based Detector

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

### Callable Detector

```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) {
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion src/Cloak.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand Down Expand Up @@ -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<int, DetectorInterface>|null $detectors
*/
Expand All @@ -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);

Expand Down
9 changes: 0 additions & 9 deletions src/Concerns/ManagesStorage.php
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
3 changes: 1 addition & 2 deletions src/Contracts/StoreInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ interface StoreInterface
*
* @param string $key The unique storage key
* @param array<string, string> $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.
Expand Down
82 changes: 10 additions & 72 deletions src/Detector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -40,7 +42,7 @@ public static function creditCard(): CreditCard
}

/**
* @return array<int, DetectorInterface>
* @return array<int, Email|Phone|SSN|CreditCard>
*/
public static function all(): array
{
Expand All @@ -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<int, array{match: string, type: string}>
*/
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<int, string> $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<int, string> $words
*/
public function __construct(
private readonly array $words,
private readonly string $type
) {
}

/**
* @return array<int, array{match: string, type: string}>
*/
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<int, array{match: string, type: string}> $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<int, array{match: string, type: string}> $callback
*/
public function __construct(
private readonly mixed $callback
) {
}

/**
* @return array<int, array{match: string, type: string}>
*/
public function detect(string $text): array
{
return ($this->callback)($text);
}
};
return new Callback($callback);
}
}
23 changes: 23 additions & 0 deletions src/Detectors/Callback.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php

declare(strict_types=1);

namespace DynamikDev\Cloak\Detectors;

use DynamikDev\Cloak\Contracts\DetectorInterface;

class Callback implements DetectorInterface
{
/**
* @param callable(string): array<int, array{match: string, type: string}> $callback
*/
public function __construct(
private readonly mixed $callback
) {
}

public function detect(string $text): array
{
return ($this->callback)($text);
}
}
Loading