diff --git a/src/Masking/FieldMasker.php b/src/Masking/FieldMasker.php index 091e2e8..a8961b0 100644 --- a/src/Masking/FieldMasker.php +++ b/src/Masking/FieldMasker.php @@ -6,136 +6,79 @@ final class FieldMasker { - /** - * Create a new instance of the FieldMasker. - * @param array $fields - */ public function __construct( public array $fields = [], ) { } - /** - * Mask the inputted data. - * @param array $data - * @return array - */ public function mask(array $data): array { $collector = []; foreach ($data as $key => $value) { - if (is_array($value)) { - $collector[$key] = $this->mask( + $collector[$key] = match (true) { + is_array($value) => $this->mask( data: $value, - ); - } + ), + is_string($value) => $this->handleString( + key: $key, + value: $value, + ), + default => $value, + }; + } - if (is_bool($value) || is_int($value) || is_float($value) || is_null($value)) { - $collector[$key] = $value; - } + return $collector; + } - // we should know it is a string. - if (is_string($value)) { - // check if this is an auth header or api key header etc - // is the key a header we want to mask? - if ($this->isHeader( - name: $key, - )) { - // grab the sensitive part of the value and mask. - if ($this->isAuth( - value: $value, - )) { - $parts = explode( - separator: ' ', - string: $value, - ); - - if (count($parts) >= 2) { - for ($i = 1; $i < count($parts); $i++) { - $parts[$i] = $this->star( - string: $parts[$i] - ); - } - } else { - $parts[0] = $this->star($parts[0]); - } - - $value = implode(' ', $parts); - } else { - $value = $this->star( - string: $value, - ); - } - } - - if (in_array($key, $this->fields, true)) { - $collector[$key] = $this->star( - string: $value, - ); - } else { - $collector[$key] = $value; - } - } + private function handleString(string $key, string $value): string + { + static $lowerFields = null; + if ($lowerFields === null) { + $lowerFields = array_map('strtolower', $this->fields); } - return $collector; + $lowerKey = strtolower($key); + + if (in_array($lowerKey, $lowerFields, true)) { + return $this->star($value); + } + + if ($this->isSensitiveHeader($lowerKey)) { + return $this->maskAuthorization($value); + } + + if ($this->isBase64($value)) { + return 'base64 encoded images are too big to process'; + } + + return $value; } - /** - * Check if the field is a Header. - * @param int|bool|float|string|null $name - * @return bool - */ - private function isHeader(int|bool|float|null|string $name): bool + private function maskAuthorization(string $value): string { - return in_array( - needle: $name, - haystack: [ - 'auth', - 'Auth', - 'Authorization', - 'authorization', - 'X-API-KEY', - 'x-api-key', - ], - strict: true, - ); + $parts = explode(' ', $value, 2); + if (isset($parts[1])) { + $authTypeLower = strtolower($parts[0]); + if (in_array($authTypeLower, ['bearer', 'basic', 'digest'])) { + return $parts[0].' '.$this->star($parts[1]); + } + } + + return $this->star($value); } - /** - * Check is the value is part of an Auth header. - * @param string $value - * @return bool - */ - private function isAuth(string $value): bool + private function isSensitiveHeader(string $key): bool { - return in_array( - needle: explode( - separator: ' ', - string: $value, - )[0], - haystack: [ - 'Bearer', - 'bearer', - 'Basic', - 'basic', - ], - strict: true, - ); + return in_array($key, ['authorization', 'x-api-key'], true); } - /** - * Replace a string input with a star. - * @param string $string - * @return string - */ public function star(string $string): string { - return str_repeat( - string: '*', - times: strlen( - string: $string, - ), - ); + return str_repeat('*', strlen($string)); + } + + private function isBase64(string $string): bool + { + return str_starts_with($string, 'data:image/') && str_contains($string, ';base64,'); } } diff --git a/tests/Masking/FieldMaskerTest.php b/tests/Masking/FieldMaskerTest.php index 23151a6..76342fe 100644 --- a/tests/Masking/FieldMaskerTest.php +++ b/tests/Masking/FieldMaskerTest.php @@ -134,7 +134,7 @@ 'password' => '********', 'api_key' => '****', ], - 'Authorization' => 'Bearer *************** ***', + 'Authorization' => 'Bearer *******************', 'X-API-KEY' => '**************', 'cc' => '*******************', 'foo' => 'bar', @@ -168,3 +168,48 @@ 'foo' => 'bar', ]); }); + +it('masks base64 encoded image strings', function () { + $masker = new FieldMasker(); + + $base64Image = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='; + $data = [ + 'image' => $base64Image, + ]; + + $maskedData = $masker->mask($data); + + // Assert that the base64 encoded image string is replaced with a mask or default value + expect($maskedData['image'])->not()->toEqual($base64Image); + expect($maskedData['image'])->toEqual('base64 encoded images are too big to process'); // Assuming 'DEFAULT_VALUE' is what you use for masking +}); + +it('does not mask non-base64 encoded strings', function () { + $masker = new FieldMasker(); + + $nonBase64String = 'This is a test string, not base64 encoded.'; + $data = [ + 'description' => $nonBase64String, + ]; + + $maskedData = $masker->mask($data); + + // Assert that non-base64 encoded strings remain unchanged + expect($maskedData['description'])->toEqual($nonBase64String); +}); + +it('masks base64 encoded image strings within nested arrays', function () { + $masker = new FieldMasker(); + + $base64Image = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAgGBgcGBQgHBwcJ...'; + $data = [ + 'profile' => [ + 'avatar' => $base64Image, + ], + ]; + + $maskedData = $masker->mask($data); + + // Assert that the base64 encoded image string in a nested array is masked + expect($maskedData)->toHaveKey('profile.avatar', 'base64 encoded images are too big to process'); +});