diff --git a/src/HashSensitiveProcessor.php b/src/HashSensitiveProcessor.php index a07d2c0..b831814 100644 --- a/src/HashSensitiveProcessor.php +++ b/src/HashSensitiveProcessor.php @@ -61,6 +61,11 @@ private function hash(string $value): ?string // Cut the input to the length limit specified $cutInput = substr($value, 0, $this->lengthLimit); + + if (strlen($cutInput) === 0) { + return null; + } + return hash($this->algorithm, $cutInput); } @@ -106,7 +111,7 @@ public function traverseInputArray(array $inputArray, array $sensitiveKeys): arr // If the value is not an array or an object, hash it if it is a sensitive key if (is_scalar($value)) { - if (in_array($key, $sensitiveKeys)) { + if (in_array($key, $sensitiveKeys) || array_key_exists($key, $sensitiveKeys)) { $inputArray[$key] = $this->hash((string) $value); } @@ -114,10 +119,10 @@ public function traverseInputArray(array $inputArray, array $sensitiveKeys): arr } // The value is either an array or an object, let traverse handle the specifics - if (array_key_exists($key, $sensitiveKeys)) { + if (in_array($key, $sensitiveKeys) || array_key_exists($key, $sensitiveKeys)) { $inputArray[$key] = $this->traverse($key, $value, $sensitiveKeys[$key]); - // ExclusiveSubtree turned off means that subkeys should be checked according to ALL keys, not just + // ExclusiveSubtree turned off means that sub keys should be checked according to ALL keys, not just // the keys in their sensitive keys subtree if (!$this->exclusiveSubtree) { $inputArray[$key] = $this->traverse($key, $inputArray[$key], $sensitiveKeys); @@ -143,7 +148,7 @@ private function traverseObject(object $object, array $sensitiveKeys): object foreach (get_object_vars($object) as $key => $value) { // If the value is not an array or an object, hash it if it is a sensitive key if (is_scalar($value)) { - if (array_key_exists($key, $sensitiveKeys)) { + if (in_array($key, $sensitiveKeys) || array_key_exists($key, $sensitiveKeys)) { $object->{$key} = $this->hash((string) $value); } @@ -151,11 +156,11 @@ private function traverseObject(object $object, array $sensitiveKeys): object } // The value is either an array or an object, let traverse handle the specifics - if (array_key_exists($key, $sensitiveKeys)) { + if (in_array($key, $sensitiveKeys) || array_key_exists($key, $sensitiveKeys)) { $object->{$key} = $this->traverse($key, $value, $sensitiveKeys[$key]); if (!$this->exclusiveSubtree) { - $object->{$key} = $this->traverse($key, $object->{key}, $sensitiveKeys); + $object->{$key} = $this->traverse($key, $object->{$key}, $sensitiveKeys); } } else { $object->{$key} = $this->traverse($key, $value, $sensitiveKeys); diff --git a/tests/HashSensitiveTest.php b/tests/HashSensitiveTest.php index 2b39ca9..bc69528 100644 --- a/tests/HashSensitiveTest.php +++ b/tests/HashSensitiveTest.php @@ -5,137 +5,130 @@ namespace HashSensitiveTests; use HashSensitive\HashSensitiveProcessor; +use TypeError; -//it('redacts records contexts', function (): void { -// $sensitive_keys = ['test' => 3]; -// $processor = new HashSensitiveProcessor($sensitive_keys); -// -// $record = $this->getRecord(context: ['test' => 'foobar']); -// expect($processor($record)->context)->toBe(['test' => 'foo***']); -//}); -// -//it('redacts using template', function (): void { -// $sensitive_keys = ['test' => 2]; -// $processor = new HashSensitiveProcessor($sensitive_keys, template: '%s(redacted)'); -// -// $record = $this->getRecord(context: ['test' => 'foobar']); -// expect($processor($record)->context)->toBe(['test' => 'fo****(redacted)']); -//}); -// -//it('redacts discarding masked', function (): void { -// $sensitive_keys = ['test' => 1]; -// $processor = new HashSensitiveProcessor($sensitive_keys, template: '...'); -// -// $record = $this->getRecord(context: ['test' => 'foobar123']); -// expect($processor($record)->context)->toBe(['test' => 'f...']); -//}); -// -//it('truncates masked characters', function (): void { -// $sensitive_keys = ['test' => 3]; -// $processor = new HashSensitiveProcessor($sensitive_keys, lengthLimit: 5); -// -// $record = $this->getRecord(context: ['test' => 'foobar']); -// expect($processor($record)->context)->toBe(['test' => 'foo**']); -//}); -// -//it('truncates visible characters', function (): void { -// $sensitive_keys = ['test' => 3]; -// $processor = new HashSensitiveProcessor($sensitive_keys, lengthLimit: 2); -// -// $record = $this->getRecord(context: ['test' => 'foobar']); -// expect($processor($record)->context)->toBe(['test' => 'fo']); -//}); -// -//it('overrides default replacement', function (): void { -// $sensitive_keys = ['test' => 3]; -// $processor = new HashSensitiveProcessor($sensitive_keys, '_'); -// -// $record = $this->getRecord(context: ['test' => 'foobar']); -// expect($processor($record)->context)->toBe(['test' => 'foo___']); -//}); -// -//it('redacts from right to left', function (): void { -// $sensitive_keys = ['test' => -3]; -// $processor = new HashSensitiveProcessor($sensitive_keys); -// -// $record = $this->getRecord(context: ['test' => 'foobar']); -// expect($processor($record)->context)->toBe(['test' => '***bar']); -//}); -// -//it('truncates masked from right to left', function (): void { -// $sensitive_keys = ['test' => -3]; -// $processor = new HashSensitiveProcessor($sensitive_keys, lengthLimit: 4); -// -// $record = $this->getRecord(context: ['test' => 'foobar']); -// expect($processor($record)->context)->toBe(['test' => '*bar']); -//}); -// -//it('truncates visible from right to left', function (): void { -// $sensitive_keys = ['test' => -3]; -// $processor = new HashSensitiveProcessor($sensitive_keys, lengthLimit: 2); -// -// $record = $this->getRecord(context: ['test' => 'foobar']); -// expect($processor($record)->context)->toBe(['test' => 'ar']); -//}); -// -//it('redacts nested arrays', function (): void { -// $sensitive_keys = ['test' => ['nested' => 3]]; -// $processor = new HashSensitiveProcessor($sensitive_keys); -// -// $record = $this->getRecord(context: ['test' => ['nested' => 'foobar']]); -// expect($processor($record)->context)->toBe(['test' => ['nested' => 'foo***']]); -//}); -// -//it('redacts inside nested arrays', function (): void { -// $sensitive_keys = ['nested' => 3]; -// $processor = new HashSensitiveProcessor($sensitive_keys); -// -// $record = $this->getRecord(context: ['test' => ['nested' => 'foobar']]); -// expect($processor($record)->context)->toBe(['test' => ['nested' => 'foo***']]); -//}); -// -//it('redacts nested objects', function (): void { -// $nested = new \stdClass(); -// $nested->value = 'foobar'; -// $nested->nested = ['value' => 'bazqux']; -// -// $sensitive_keys = ['test' => ['nested' => ['value' => 3, 'nested' => ['value' => -3]]]]; -// $processor = new HashSensitiveProcessor($sensitive_keys); -// -// $record = $this->getRecord(context: ['test' => ['nested' => $nested]]); -// -// expect($processor($record)->context)->toBe(['test' => ['nested' => $nested]]); -// expect($nested->value)->toBe('foo***'); -// expect($nested->nested['value'])->toBe('***qux'); -//}); -// -//it('redacts inside nested objects', function (): void { -// $nested = new \stdClass(); -// $nested->value = 'foobar'; -// $nested->nested = ['value' => 'bazqux']; -// -// $sensitive_keys = ['nested' => ['value' => -3]]; -// $processor = new HashSensitiveProcessor($sensitive_keys); -// -// $record = $this->getRecord(context: ['test' => ['nested' => $nested]]); -// -// expect($processor($record)->context)->toBe(['test' => ['nested' => $nested]]); -// expect($nested->value)->toBe('***bar'); -// expect($nested->nested['value'])->toBe('***qux'); -//}); -// -//it('preserves empty values', function (): void { -// $sensitive_keys = ['test' => 3, 'optionalKey' => 10]; -// $processor = new HashSensitiveProcessor($sensitive_keys); -// -// $record = $this->getRecord(context: ['test' => 'foobar', 'optionalKey' => '']); -// expect($processor($record)->context)->toBe(['test' => 'foo***', 'optionalKey' => '']); -//}); -// -//it('throws when finds an un-traversable value', function (): void { -// $sensitive_keys = ['test' => 3]; -// $processor = new HashSensitiveProcessor($sensitive_keys); -// -// $record = $this->getRecord(context: ['test' => fopen(__FILE__, 'rb')]); -// $processor($record); -//})->throws(\UnexpectedValueException::class, 'Don\'t know how to traverse value at key test'); +it('redacts records contexts', function (): void { + $sensitive_keys = ['test']; + $processor = new HashSensitiveProcessor($sensitive_keys); + + $record = $this->getRecord(context: ['test' => 'foobar']); + expect($processor($record)->context)->toBe(['test' => 'c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2']); +}); + +it('truncates masked characters', function (): void { + $sensitive_keys = ['test']; + $processor = new HashSensitiveProcessor($sensitive_keys, lengthLimit: 5); + + $record = $this->getRecord(context: ['test' => 'foobar']); + // Only `fooba` should be hashed, the first 5 characters of `foobar` + expect($processor($record)->context)->toBe(['test' => '41cbe1a87981490351ccad5346d96da0ac10678670b31fc0ab209aed1b5bc515']); +}); + +it('doesn\'t truncate more than the string length', function (): void { + $sensitive_keys = ['test']; + $processor = new HashSensitiveProcessor($sensitive_keys, lengthLimit: 10); + + $record = $this->getRecord(context: ['test' => 'foobar']); + expect($processor($record)->context)->toBe(['test' => 'c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2']); +}); + +it('doesn\'t truncate when length limit is 0', function (): void { + $sensitive_keys = ['test']; + $processor = new HashSensitiveProcessor($sensitive_keys, lengthLimit: 0); + + $record = $this->getRecord(context: ['test' => 'foobar']); + expect($processor($record)->context)->toBe(['test' => null]); +}); + +it('doesn\'t truncate when length limit is not set', function (): void { + $sensitive_keys = ['test']; + $processor = new HashSensitiveProcessor($sensitive_keys); + + $record = $this->getRecord(context: ['test' => 'foobar']); + expect($processor($record)->context)->toBe(['test' => 'c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2']); +}); + + +it('redacts nested arrays', function (): void { + $sensitive_keys = ['test' => ['nested']]; + $processor = new HashSensitiveProcessor($sensitive_keys); + + $record = $this->getRecord(context: ['test' => ['nested' => 'foobar']]); + expect($processor($record)->context)->toBe(['test' => ['nested' => 'c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2']]); +}); + +it('keeps non redacted nested arrays intact', function (): void { + $sensitive_keys = ['test' => ['nested']]; + $processor = new HashSensitiveProcessor($sensitive_keys); + + $record = $this->getRecord(context: ['test' => ['nested' => 'foobar', 'no_hash' => 'foobar']]); + expect($processor($record)->context)->toBe(['test' => ['nested' => 'c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2', 'no_hash' => 'foobar']]); +}); + +it('redacts inside nested arrays', function (): void { + $sensitive_keys = ['nested']; + $processor = new HashSensitiveProcessor($sensitive_keys); + + $record = $this->getRecord(context: ['test' => ['nested' => 'foobar']]); + expect($processor($record)->context)->toBe(['test' => ['nested' => 'c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2']]); +}); + +it('redacts nested objects', function (): void { + $nested = new \stdClass(); + $nested->value = 'foobar'; + $nested->nested = ['value' => 'bazqux']; + + $sensitive_keys = ['test' => ['nested' => ['value', 'nested' => ['value']]]]; + $processor = new HashSensitiveProcessor($sensitive_keys); + + $record = $this->getRecord(context: ['test' => ['nested' => $nested]]); + + expect($processor($record)->context)->toBe(['test' => ['nested' => $nested]]) + ->and($nested->value)->toBe('c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2') + ->and($nested->nested['value'])->toBe('972c5e1203896784a7cf9dd60acd443a1065e19ad5f92e59a9180c185f065c04'); +}); + +it('keeps non redacted nested objects intact', function (): void { + $nested = new \stdClass(); + $nested->value = 'foobar'; + $nested->nested = ['value' => 'bazqux', 'no_hash' => 'foobar']; + + $sensitive_keys = ['test' => ['nested' => ['value', 'nested' => ['value']]]]; + $processor = new HashSensitiveProcessor($sensitive_keys); + + $record = $this->getRecord(context: ['test' => ['nested' => $nested]]); + + expect($processor($record)->context)->toBe(['test' => ['nested' => $nested]]) + ->and($nested->nested['no_hash'])->toBe('foobar'); +}); + +it('redacts inside nested objects', function (): void { + $nested = new \stdClass(); + $nested->value = 'foobar'; + $nested->nested = ['value' => 'bazqux']; + + $sensitive_keys = ['nested' => ['value']]; + $processor = new HashSensitiveProcessor($sensitive_keys); + + $record = $this->getRecord(context: ['test' => ['nested' => $nested]]); + + expect($processor($record)->context)->toBe(['test' => ['nested' => $nested]]) + ->and($nested->value)->toBe('c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2') + ->and($nested->nested['value'])->toBe('972c5e1203896784a7cf9dd60acd443a1065e19ad5f92e59a9180c185f065c04'); +}); + +it('preserves empty values', function (): void { + $sensitive_keys = ['test', 'optionalKey']; + $processor = new HashSensitiveProcessor($sensitive_keys); + + $record = $this->getRecord(context: ['test' => 'foobar', 'optionalKey' => '']); + expect($processor($record)->context)->toBe(['test' => 'c3ab8ff13720e8ad9047dd39466b3c8974e592c2fa383d4a3960714caef0c4f2', 'optionalKey' => null]); +}); + +it('throws when finds an un-traversable value', function (): void { + $sensitive_keys = ['test']; + $processor = new HashSensitiveProcessor($sensitive_keys); + + $record = $this->getRecord(context: ['test' => fopen(__FILE__, 'rb')]); + $processor($record); +})->throws(TypeError::class, 'Argument #2 ($value) must be of type object|array, resource given');