├── LICENSE ├── README.md ├── composer.json └── src └── Masked ├── InputCollection.php ├── Protect.php ├── Redact.php └── ValueCollection.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Kaloyan K. Tsvetkov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fuko\\Masked [![Latest Version](http://img.shields.io/packagist/v/fuko-php/masked.svg)](https://packagist.org/packages/fuko-php/masked) [![GitHub license](https://img.shields.io/github/license/fuko-php/masked.svg)](https://github.com/fuko-php/masked/blob/master/LICENSE) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/7e26c25549cc40d99be82b76c95f2f6d)](https://www.codacy.com/gh/fuko-php/masked/dashboard?utm_source=github.com&utm_medium=referral&utm_content=fuko-php/masked&utm_campaign=Badge_Grade) 2 | 3 | **Fuko\\Masked** is a small PHP library for masking sensitive data: it replace blacklisted elements with their redacted values. 4 | 5 | It is meant to be very easy to use. If you have any experience with trying to sanitise data for logging, or cleaning information that is publicly accessible, you know how annoying it is to have passwords or security token popup at various places of your dumps. ***Fuko\\Masked*** is meant to help with that. 6 | 7 | ## Basic use 8 | In order to use it, you just need to feed your sensitive data (passwords, tokens, credentials) to `Fuko\Masked\Protect` 9 | 10 | ```php 11 | use Fuko\Masked\Protect; 12 | 13 | Protect::hideValue($secret_key); // hide the value inside the $secret_key var 14 | Protect::hideInput('password', INPUT_POST); // hide the value of $_POST['password'] 15 | 16 | $redacted = Protect::protect($_POST); 17 | ``` 18 | 19 | ...and that's it. The blacklisted values and inputs will be masked. The output of the above code is going to be 20 | 21 | ```php 22 | // consider these values for the vars used 23 | // $secret_key = '12345678'; 24 | // $_POST = array('username' => 'Bob', 'password' => 'WaldoPepper!', 'messages' => 'The secret key is 12345678'); 25 | 26 | $redacted = Protect::protect($_POST); 27 | print_r($redacted); 28 | /* ... and the output is 29 | Array 30 | ( 31 | [username] => Bob 32 | [password] => ████████████ 33 | [messages] => The secret key is ████████ 34 | ) 35 | */ 36 | ``` 37 | 38 | ## How it works ? 39 | 40 | ***Fuko\\Masked*** does two things: 41 | 42 | * first, there is the `\Fuko\Masked\Redact` class which is used to mask sensitive data 43 | * second, `\Fuko\Masked\Protect` class is used to collect your sensitive data, and redact it 44 | 45 | By doing the above, you are going to have redacted content with all the sensitive details blacklisted. You do not need to go looking inside all the dumps you create for passwords or credentials, instead you just register them with `\Fuko\Masked\Protect` and that class will mask them wherever it finds them: strings, arrays, big text dumps. It's that simple. The idea is not to have clumsy and overdressed library, but a simple tool that its job well. 46 | 47 | ## Examples 48 | Here are several example of basic ***Fuko\\Masked*** use cases. 49 | 50 | #### Hide Values 51 | You know where your passwords and credentials are, and you want to blacklist them in any dumps you create. Here's how you would do it: 52 | ```php 53 | use \Fuko\Masked\Protect; 54 | 55 | // consider these values inside $config 56 | // $config = array( 57 | // 'project_title' => 'My New Project!', 58 | // 'mysql_username' => 'me', 59 | // 'mysql_password' => 'Mlyk!', 60 | // 'mysql_database' => 'project', 61 | // 'root' => '/var/www/niakade/na/majnata/si', 62 | // 'i.am.stupid' => 'Mlyk! e egati parolata za moya project', 63 | // ); 64 | 65 | Protect::hideValue($config['mysql_username']); 66 | Protect::hideValue($config['mysql_password']); 67 | Protect::hideValue($config['mysql_database']); 68 | 69 | print_r(Protect::protect($config)); 70 | /* ... and the output is 71 | Array 72 | ( 73 | [project_title] => My New Project! 74 | [mysql_username] => ██ 75 | [mysql_password] => █████ 76 | [mysql_database] => ███████ 77 | [root] => /var/www/niakade/na/majnata/si 78 | [i.am.stupid] => █████ e egati parolata za moya ███████ 79 | ) 80 | */ 81 | ``` 82 | 83 | The sensitive details might be in some nested arrays within the arrays, they are still going to be redacted: 84 | ```php 85 | use \Fuko\Masked\Protect; 86 | 87 | Protect::hideValue($password); 88 | 89 | $a = ['b' => ['c' => ['d' => $password]]]; 90 | print_r(Protect::protect($a)); 91 | /* ... and the output is 92 | Array 93 | ( 94 | [b] => Array 95 | ( 96 | [c] => Array 97 | ( 98 | [d] => ██████ 99 | ) 100 | ) 101 | ) 102 | */ 103 | ``` 104 | 105 | #### Hide Inputs 106 | At some occasions you know that user-submitted data or other super-global inputs might contain sensitive data. In these cases you do not need to hide the actual value, but you can address the input array instead. In this example we are going to mask the "password" POST value: 107 | ```php 108 | use \Fuko\Masked\Protect; 109 | 110 | Protect::hideInput('password', INPUT_POST); 111 | 112 | // later you need to do a dump of $_POST and ... 113 | $_POST_redacted = Protect::protect($_POST); 114 | /* ... and the output is 115 | Array 116 | ( 117 | [email] => Bob@sundance.kid 118 | [password] => ███████ 119 | ) 120 | */ 121 | ``` 122 | 123 | #### Working with Objects 124 | The `\Fuko\Masked\Protect::protect()` only works with strings and arrays. If you need to mask the sensitive data in an object dump, you first create the dump and then feed it to `Protect::protect()`, like this: 125 | ```php 126 | use \Fuko\Masked\Protect; 127 | 128 | Protect::hideValue($password); 129 | $a = new stdClass; 130 | $a->b = new stdClass; 131 | $a->b->c = new stdClass; 132 | $a->password = $a->b->secret = $a->b->c->d = $password; 133 | echo Protect::protect(print_r($a, true)); 134 | /* ... and the output is 135 | stdClass Object 136 | ( 137 | [b] => stdClass Object 138 | ( 139 | [c] => stdClass Object 140 | ( 141 | [d] => ██████████████████ 142 | ) 143 | [secret] => ██████████████████ 144 | ) 145 | [password] => ██████████████████ 146 | ) 147 | */ 148 | ``` 149 | 150 | For those classes that have `::__toString()` method implemented, the objects will be cast into strings with that. Here is an example with an exception, and exception classes have that: 151 | ```php 152 | use \Fuko\Masked\Protect; 153 | 154 | Protect::hideValue($password); 155 | $e = new \Exception('Look, look, his password is ' . $password); 156 | 157 | echo Protect::protect($e); 158 | /* ... and the output is 159 | Exception: Look, look, his password is ████████ in /tmp/egati-probata.php:123 160 | Stack trace: 161 | #0 {main} 162 | */ 163 | ``` 164 | 165 | ## Different Masking 166 | 167 | You can use `\Fuko\Masked\Redact` in your project as the library for masking data. By default the class uses `\Fuko\Masked\Redact::disguise()` method for masking, with default settings that masks everything and that uses `█` as masking symbol. Here's how you can change its behaviour: 168 | ```php 169 | use \Fuko\Masked\Redact; 170 | 171 | /* leave 4 chars unmasked at the end, and use '*' as masking symbol */ 172 | Redact::setRedactCallback( [Redact::class, 'disguise'], [4, '*']); 173 | echo Redact::redact('1234567890'); // Output is '******7890' 174 | 175 | /* leave 4 chars unmasked at the beginning, and use '🤐' as masking symbol */ 176 | Redact::setRedactCallback( [Redact::class, 'disguise'], [-4, '🤐']); 177 | echo Redact::redact('1234567890'); // Output is '1234🤐🤐🤐🤐🤐🤐' 178 | ``` 179 | 180 | You can set your own callback for masking with `\Fuko\Masked\Redact` class: 181 | ```php 182 | use \Fuko\Masked\Redact; 183 | 184 | Redact::setRedactCallback( function($var) { return '💩'; } ); 185 | echo Redact::redact('1234567890'); // Output is '💩' 186 | ``` 187 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fuko-php/masked", 3 | "license": "MIT", 4 | "description": "Masks sensitive data: replaces blacklisted elements with redacted values", 5 | "keywords": ["mask", "redact", "blacklist", "black-list", "masked", "security", "obfuscation", "fuko", "fuko-php"], 6 | "authors": [ 7 | { 8 | "name": "Kaloyan Tsvetkov (KT)", 9 | "email": "kaloyan@kaloyan.info", 10 | "homepage": "http://kaloyan.info", 11 | "role": "Developer" 12 | } 13 | ], 14 | 15 | "require": { 16 | "php": ">=7.4", 17 | "ext-mbstring": "*" 18 | }, 19 | 20 | "autoload": { 21 | "psr-4": { 22 | "Fuko\\Masked\\": "src/Masked/" 23 | } 24 | }, 25 | 26 | "require-dev": { 27 | "phpunit/phpunit": "^7.5 || ^8.5" 28 | }, 29 | 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Fuko\\Masked\\Tests\\": "tests/Masked/" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Masked/InputCollection.php: -------------------------------------------------------------------------------- 1 | 8 | * @link https://github.com/fuko-php/masked/ 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Fuko\Masked; 13 | 14 | use const FILTER_SANITIZE_STRING; 15 | use const E_USER_WARNING; 16 | use const INPUT_ENV; 17 | use const INPUT_SERVER; 18 | use const INPUT_COOKIE; 19 | use const INPUT_GET; 20 | use const INPUT_POST; 21 | use const INPUT_SESSION; 22 | use const INPUT_REQUEST; 23 | 24 | use function array_keys; 25 | use function define; 26 | use function defined; 27 | use function gettype; 28 | use function filter_var; 29 | use function in_array; 30 | use function is_array; 31 | use function is_callable; 32 | use function is_object; 33 | use function is_scalar; 34 | use function sprintf; 35 | use function strpos; 36 | use function str_replace; 37 | use function trigger_error; 38 | 39 | if (!defined('INPUT_SESSION')) 40 | { 41 | define('INPUT_SESSION', 6); 42 | } 43 | 44 | if (!defined('INPUT_REQUEST')) 45 | { 46 | define('INPUT_REQUEST', 99); 47 | } 48 | 49 | /** 50 | * Collects inputs for scanning to find values for redacting with {@link Fuko\Masked\Protect} 51 | * 52 | * @package Fuko\Masked 53 | */ 54 | class InputCollection 55 | { 56 | /** 57 | * @var array default inputs to scan 58 | */ 59 | const DEFAULT_INPUTS = array( 60 | INPUT_SERVER => array( 61 | 'PHP_AUTH_PW' => true 62 | ), 63 | INPUT_POST => array( 64 | 'password' => true 65 | ), 66 | ); 67 | 68 | /** 69 | * @var array collection of inputs for scanning to find values for redacting 70 | */ 71 | protected $hideInputs = self::DEFAULT_INPUTS; 72 | 73 | /** 74 | * Clear accumulated inputs to hide 75 | */ 76 | function clearInputs() 77 | { 78 | $this->hideInputs = self::DEFAULT_INPUTS; 79 | } 80 | 81 | /** 82 | * Get list of accumulated inputs to hide 83 | * 84 | * @return array 85 | */ 86 | function getInputs() 87 | { 88 | return $this->hideInputs; 89 | } 90 | 91 | /** 92 | * Get the actual input values to hide 93 | * 94 | * @return array 95 | */ 96 | function getInputsValues() 97 | { 98 | $hideInputValues = array(); 99 | foreach ($this->hideInputs as $type => $inputs) 100 | { 101 | // the input names are the keys 102 | // 103 | foreach (array_keys($inputs) as $name) 104 | { 105 | $input = $this->filterInput($type, $name); 106 | if (!$input) 107 | { 108 | continue; 109 | } 110 | 111 | $hideInputValues[] = $input; 112 | } 113 | } 114 | 115 | return $hideInputValues; 116 | } 117 | 118 | /** 119 | * Gets a specific external variable by name and filter it as a string 120 | * 121 | * @param integer $type input type, must be one of INPUT_REQUEST, 122 | * INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SESSION, 123 | * INPUT_SERVER or INPUT_ENV 124 | * @param string $name name of the input variable to get 125 | * @return string 126 | */ 127 | protected function filterInput($type, $name) 128 | { 129 | switch ($type) 130 | { 131 | case INPUT_ENV : 132 | return !empty($_ENV) 133 | ? $this->filterInputVar($_ENV, $name) 134 | : ''; 135 | 136 | case INPUT_SERVER : 137 | return !empty($_SERVER) 138 | ? $this->filterInputVar($_SERVER, $name) 139 | : ''; 140 | 141 | case INPUT_COOKIE : 142 | return !empty($_COOKIE) 143 | ? $this->filterInputVar($_COOKIE, $name) 144 | : ''; 145 | 146 | case INPUT_GET : 147 | return !empty($_GET) 148 | ? $this->filterInputVar($_GET, $name) 149 | : ''; 150 | 151 | case INPUT_POST : 152 | return !empty($_POST) 153 | ? $this->filterInputVar($_POST, $name) 154 | : ''; 155 | 156 | case INPUT_SESSION : 157 | return !empty($_SESSION) 158 | ? $this->filterInputVar($_SESSION, $name) 159 | : ''; 160 | 161 | case INPUT_REQUEST : 162 | return !empty($_REQUEST) 163 | ? $this->filterInputVar($_REQUEST, $name) 164 | : ''; 165 | } 166 | 167 | return ''; 168 | } 169 | 170 | /** 171 | * Filters a variable as a string 172 | * 173 | * @param array $input 174 | * @param string $name name of the input variable to get 175 | * @return string 176 | */ 177 | protected function filterInputVar(array $input, $name) 178 | { 179 | if (empty($input[$name])) 180 | { 181 | return ''; 182 | } 183 | 184 | return filter_var( 185 | $input[$name], 186 | FILTER_SANITIZE_STRING 187 | ); 188 | } 189 | 190 | /** 191 | * Introduce new inputs to hide 192 | * 193 | * @param array $inputs array keys are input types(INPUT_REQUEST, 194 | * INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SESSION, 195 | * INPUT_SERVER, INPUT_ENV), array values are arrays with 196 | * input names 197 | */ 198 | function hideInputs(array $inputs) 199 | { 200 | foreach ($inputs as $type => $names) 201 | { 202 | if (empty($names)) 203 | { 204 | trigger_error('Fuko\Masked\Protect::hideInputs' 205 | . '() empty input names for "' 206 | . $type . '" input type', 207 | E_USER_WARNING 208 | ); 209 | continue; 210 | } 211 | 212 | if (is_scalar($names)) 213 | { 214 | $names = array( 215 | $names 216 | ); 217 | } 218 | 219 | if (!is_array($names)) 220 | { 221 | trigger_error('Fuko\Masked\Protect::hideInputs' 222 | . '() input names must be string or array, ' 223 | . gettype($names) . ' provided instead', 224 | E_USER_WARNING 225 | ); 226 | continue; 227 | } 228 | 229 | foreach ($names as $name) 230 | { 231 | $this->addInput($name, $type, 'Fuko\Masked\Protect::hideInputs'); 232 | } 233 | } 234 | } 235 | 236 | /** 237 | * Introduce a new input to hide 238 | * 239 | * @param string $name input name, e.g. "password" if you are 240 | * targeting $_POST['password'] 241 | * @param integer $type input type, must be one of these: INPUT_REQUEST, 242 | * INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SESSION, INPUT_SERVER, 243 | * INPUT_ENV; default value is INPUT_REQUEST 244 | * @return boolean|NULL TRUE if added, FALSE if wrong 245 | * name or type, NULL if already added 246 | */ 247 | function hideInput($name, $type = INPUT_REQUEST) 248 | { 249 | return $this->addInput($name, $type, 'Fuko\Masked\Protect::hideInput'); 250 | } 251 | 252 | /** 253 | * Validates input: 254 | * - if $name is empty 255 | * - if $name is scalar 256 | * - if $type is scalar 257 | * - if $type is one of these: INPUT_REQUEST, 258 | * INPUT_GET, INPUT_POST, INPUT_COOKIE, 259 | * INPUT_SESSION, INPUT_SERVER, INPUT_ENV 260 | * @param string $name 261 | * @param integer $type 262 | * @param string $method method to use to report the validation errors 263 | * @return boolean 264 | */ 265 | protected function validateInput($name, &$type, $method) 266 | { 267 | if (empty($name)) 268 | { 269 | trigger_error( 270 | $method . '() $name argument is empty', 271 | E_USER_WARNING 272 | ); 273 | return false; 274 | } 275 | 276 | if (!is_scalar($name)) 277 | { 278 | trigger_error( 279 | $method . '() $name argument is not scalar, it is ' 280 | . gettype($name), 281 | E_USER_WARNING 282 | ); 283 | return false; 284 | } 285 | 286 | if (!is_scalar($type)) 287 | { 288 | trigger_error( 289 | $method . '() $type argument is not scalar, it is ' 290 | . gettype($type), 291 | E_USER_WARNING 292 | ); 293 | return false; 294 | } 295 | 296 | $type = (int) $type; 297 | if (!in_array($type, array( 298 | INPUT_REQUEST, 299 | INPUT_GET, 300 | INPUT_POST, 301 | INPUT_COOKIE, 302 | INPUT_SESSION, 303 | INPUT_SERVER, 304 | INPUT_ENV))) 305 | { 306 | $type = INPUT_REQUEST; 307 | } 308 | 309 | return true; 310 | } 311 | 312 | /** 313 | * Add $input as of $type to the list of inputs to hide 314 | * 315 | * @param string $name input name, e.g. "password" if you are 316 | * targeting $_POST['password'] 317 | * @param integer $type input type, must be one of these: INPUT_REQUEST, 318 | * INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SESSION, INPUT_SERVER, 319 | * INPUT_ENV; default value is INPUT_REQUEST 320 | * @param string $method method to use to report the validation errors 321 | * @return boolean|NULL TRUE if added, FALSE if wrong 322 | * name or type, NULL if already added 323 | */ 324 | protected function addInput($name, $type, $method) 325 | { 326 | if (!$this->validateInput($name, $type, $method)) 327 | { 328 | return false; 329 | } 330 | 331 | if (isset($this->hideInputs[$type][$name])) 332 | { 333 | return null; 334 | } 335 | 336 | return $this->hideInputs[$type][$name] = true; 337 | } 338 | } 339 | -------------------------------------------------------------------------------- /src/Masked/Protect.php: -------------------------------------------------------------------------------- 1 | 8 | * @link https://github.com/fuko-php/masked/ 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Fuko\Masked; 13 | 14 | use Fuko\Masked\InputCollection; 15 | use Fuko\Masked\ValueCollection; 16 | 17 | use const FILTER_DEFAULT; 18 | 19 | use function filter_var; 20 | use function is_array; 21 | use function is_object; 22 | use function is_scalar; 23 | use function strpos; 24 | use function str_replace; 25 | 26 | /** 27 | * Protect sensitive data and redacts it using {@link Fuko\Masked\Redact::redact()} 28 | * 29 | * @package Fuko\Masked 30 | */ 31 | final class Protect 32 | { 33 | /** 34 | * @var ValueCollection collection of values to hide redacting 35 | */ 36 | private static $hideValueCollection; 37 | 38 | /** 39 | * Clear accumulated values to hide 40 | */ 41 | static function clearValues() 42 | { 43 | if (!empty(self::$hideValueCollection)) 44 | { 45 | self::$hideValueCollection->clearValues(); 46 | } 47 | } 48 | 49 | /** 50 | * Introduce new values to hide 51 | * 52 | * @param array $values array with values of scalars or 53 | * objects that have __toString() methods 54 | */ 55 | static function hideValues(array $values) 56 | { 57 | (self::$hideValueCollection 58 | ?? (self::$hideValueCollection = 59 | new ValueCollection))->hideValues($values); 60 | } 61 | 62 | /** 63 | * Introduce a new value to hide 64 | * 65 | * @param mixed $value scalar values (strings, numbers) 66 | * or objects with __toString() method added 67 | * @return boolean|NULL TRUE if added, FALSE if wrong 68 | * type, NULL if already added 69 | */ 70 | static function hideValue($value) 71 | { 72 | return (self::$hideValueCollection 73 | ?? (self::$hideValueCollection = 74 | new ValueCollection))->hideValue($value); 75 | } 76 | 77 | ///////////////////////////////////////////////////////////////////// 78 | 79 | /** 80 | * @var InputCollection collection of inputs for scanning to find 81 | * values for redacting 82 | */ 83 | private static $hideInputCollection; 84 | 85 | /** 86 | * Clear accumulated inputs to hide 87 | */ 88 | static function clearInputs() 89 | { 90 | if (!empty(self::$hideInputCollection)) 91 | { 92 | self::$hideInputCollection->clearInputs(); 93 | } 94 | } 95 | 96 | /** 97 | * Introduce new inputs to hide 98 | * 99 | * @param array $inputs array keys are input types(INPUT_REQUEST, 100 | * INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SESSION, 101 | * INPUT_SERVER, INPUT_ENV), array values are arrays with 102 | * input names 103 | */ 104 | static function hideInputs(array $inputs) 105 | { 106 | (self::$hideInputCollection 107 | ?? (self::$hideInputCollection = 108 | new InputCollection))->hideInputs($inputs); 109 | } 110 | 111 | /** 112 | * Introduce a new input to hide 113 | * 114 | * @param string $name input name, e.g. "password" if you are 115 | * targeting $_POST['password'] 116 | * @param integer $type input type, must be one of these: INPUT_REQUEST, 117 | * INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SESSION, INPUT_SERVER, 118 | * INPUT_ENV; default value is INPUT_REQUEST 119 | * @return boolean|NULL TRUE if added, FALSE if wrong 120 | * name or type, NULL if already added 121 | */ 122 | static function hideInput($name, $type = INPUT_REQUEST) 123 | { 124 | return (self::$hideInputCollection 125 | ?? (self::$hideInputCollection = 126 | new InputCollection))->hideInput($name, $type); 127 | } 128 | 129 | ///////////////////////////////////////////////////////////////////// 130 | 131 | /** 132 | * Protects a variable by replacing sensitive data inside it 133 | * 134 | * @param mixed $var only strings and arrays will be processed, 135 | * objects will be "stringified", other types (resources?) 136 | * will be returned as empty strings 137 | * @return string|array 138 | */ 139 | static function protect($var) 140 | { 141 | if (is_scalar($var)) 142 | { 143 | return self::protectScalar($var); 144 | } else 145 | if (is_array($var)) 146 | { 147 | foreach ($var as $k => $v) 148 | { 149 | $var[$k] = self::protect($v); 150 | } 151 | 152 | return $var; 153 | } else 154 | if (is_object($var)) 155 | { 156 | return self::protectScalar( 157 | filter_var($var, FILTER_DEFAULT) 158 | ); 159 | } else 160 | { 161 | return ''; 162 | } 163 | } 164 | 165 | /** 166 | * Protects a scalar value by replacing sensitive data inside it 167 | * 168 | * @param string $var 169 | * @return string 170 | */ 171 | static function protectScalar($var) 172 | { 173 | // hide values 174 | // 175 | if (!empty(self::$hideValueCollection)) 176 | { 177 | if ($hideValues = self::$hideValueCollection->getValues()) 178 | { 179 | $var = self::_redact($var, $hideValues); 180 | } 181 | } 182 | 183 | // hide inputs 184 | // 185 | $hideInputValues = array(); 186 | if (!empty(self::$hideInputCollection)) 187 | { 188 | $hideInputValues = self::$hideInputCollection->getInputsValues(); 189 | if (!empty($hideInputValues)) 190 | { 191 | $var = self::_redact($var, $hideInputValues); 192 | } 193 | } 194 | 195 | return $var; 196 | } 197 | 198 | /** 199 | * Redacts $values inside the $var string 200 | * @param string $var 201 | * @param array $values 202 | * @return string 203 | */ 204 | private static function _redact($var, array $values) 205 | { 206 | foreach ($values as $value) 207 | { 208 | $value = (string) $value; 209 | if (false === strpos($var, $value)) 210 | { 211 | continue; 212 | } 213 | 214 | $var = str_replace($value, Redact::redact($value), $var); 215 | } 216 | 217 | return $var; 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /src/Masked/Redact.php: -------------------------------------------------------------------------------- 1 | 8 | * @link https://github.com/fuko-php/masked/ 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Fuko\Masked; 13 | 14 | use InvalidArgumentException; 15 | 16 | use const FILTER_SANITIZE_FULL_SPECIAL_CHARS; 17 | 18 | use function abs; 19 | use function array_unshift; 20 | use function call_user_func_array; 21 | use function filter_var; 22 | use function is_callable; 23 | use function round; 24 | use function mb_strlen; 25 | use function str_repeat; 26 | use function mb_substr; 27 | 28 | /** 29 | * Masks sensitive data: replaces blacklisted elements with redacted values 30 | * 31 | * @package Fuko\Masked 32 | */ 33 | class Redact 34 | { 35 | /** 36 | * @var array callback used in {@link Fuko\Masked\Redact::redact()}; 37 | * format is actually an array with two elements, first being 38 | * the actual callback, and the second being an array with any 39 | * extra arguments that are needed. 40 | */ 41 | protected static $redactCallback = array( 42 | array(__CLASS__, 'disguise'), 43 | array(0, '█') 44 | ); 45 | 46 | /** 47 | * Redacts provided string by masking it 48 | * 49 | * @param string $value 50 | * @return string 51 | */ 52 | public static function redact($value) 53 | { 54 | $args = self::$redactCallback[1]; 55 | array_unshift($args, $value); 56 | 57 | return call_user_func_array( 58 | self::$redactCallback[0], 59 | $args 60 | ); 61 | } 62 | 63 | /** 64 | * Set a new callback to be used by {@link Fuko\Masked\Redact::redact()} 65 | * 66 | * First callback argument will be the value that needs to be 67 | * masked/redacted; optionally you can provide more $arguments 68 | * 69 | * @param callable $callback 70 | * @param array $arguments (optional) extra arguments for the callback 71 | * @throws \InvalidArgumentException 72 | */ 73 | public static function setRedactCallback($callback, array $arguments = null) 74 | { 75 | if (!is_callable($callback)) 76 | { 77 | throw new InvalidArgumentException( 78 | 'First argument to ' 79 | . __METHOD__ 80 | . '() must be a valid callback' 81 | ); 82 | } 83 | 84 | self::$redactCallback = array( 85 | $callback, 86 | !empty($arguments) 87 | ? array_values($arguments) 88 | : array() 89 | ); 90 | } 91 | 92 | /** 93 | * Get a masked version of a string 94 | * 95 | * This is the default callback used by {@link Fuko\Masked\Redact::redact()} 96 | * 97 | * @param string $value 98 | * @param integer $unmaskedChars number of chars to mask; having 99 | * positive number will leave the unmasked symbols at the 100 | * end of the value; using negative number will leave the 101 | * unmasked chars at the start of the value 102 | * @param string $maskSymbol 103 | * @return string 104 | */ 105 | public static function disguise($value, $unmaskedChars = 4, $maskSymbol = '*') 106 | { 107 | $value = filter_var($value, FILTER_SANITIZE_FULL_SPECIAL_CHARS); 108 | $unmaskedChars = (int) $unmaskedChars; 109 | $maskSymbol = filter_var($maskSymbol, FILTER_SANITIZE_FULL_SPECIAL_CHARS); 110 | 111 | // not enough chars to unmask ? 112 | // 113 | if (abs($unmaskedChars) >= mb_strlen($value)) 114 | { 115 | $unmaskedChars = 0; 116 | } 117 | 118 | // at least half must be masked ? 119 | // 120 | if (abs($unmaskedChars) > mb_strlen($value)/2) 121 | { 122 | $unmaskedChars = round($unmaskedChars/2); 123 | } 124 | 125 | // leading unmasked chars 126 | // 127 | if ($unmaskedChars < 0) 128 | { 129 | $unmasked = mb_substr($value, 0, -$unmaskedChars); 130 | return $unmasked . str_repeat($maskSymbol, 131 | mb_strlen($value) - mb_strlen($unmasked) 132 | ); 133 | } 134 | 135 | // trailing unmasked chars 136 | // 137 | $unmasked = $unmaskedChars 138 | ? mb_substr($value, -$unmaskedChars) 139 | : ''; 140 | return str_repeat($maskSymbol, 141 | mb_strlen($value) - mb_strlen($unmasked) 142 | ) . $unmasked; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/Masked/ValueCollection.php: -------------------------------------------------------------------------------- 1 | 8 | * @link https://github.com/fuko-php/masked/ 9 | * @license https://opensource.org/licenses/MIT 10 | */ 11 | 12 | namespace Fuko\Masked; 13 | 14 | use const E_USER_WARNING; 15 | 16 | use function gettype; 17 | use function in_array; 18 | use function is_array; 19 | use function is_callable; 20 | use function is_object; 21 | use function is_scalar; 22 | use function sprintf; 23 | use function trigger_error; 24 | 25 | /** 26 | * Collects values to uncover and mask with {@link Fuko\Masked\Protect} 27 | * 28 | * @package Fuko\Masked 29 | */ 30 | class ValueCollection 31 | { 32 | /** 33 | * @var array collection of values to hide redacting 34 | */ 35 | protected $hideValues = array(); 36 | 37 | /** 38 | * Get the collected values to hide 39 | * 40 | * @return array 41 | */ 42 | function getValues() 43 | { 44 | return $this->hideValues; 45 | } 46 | 47 | /** 48 | * Clear accumulated values to hide 49 | */ 50 | function clearValues() 51 | { 52 | $this->hideValues = array(); 53 | } 54 | 55 | /** 56 | * Introduce new values to hide 57 | * 58 | * @param array $values array with values of scalars or 59 | * objects that have __toString() methods 60 | */ 61 | function hideValues(array $values) 62 | { 63 | foreach ($values as $k => $value) 64 | { 65 | $this->addValue( 66 | $value, 'Fuko\Masked\Protect::hideValues' 67 | . '() received %s as a hide value (key "' 68 | . $k . '" of the $values argument)'); 69 | } 70 | } 71 | 72 | /** 73 | * Introduce a new value to hide 74 | * 75 | * @param mixed $value scalar values (strings, numbers) 76 | * or objects with __toString() method added 77 | * @return boolean|NULL TRUE if added, FALSE if wrong 78 | * type, NULL if already added 79 | */ 80 | function hideValue($value) 81 | { 82 | return $this->addValue( 83 | $value, 84 | 'Fuko\Masked\Protect::hideValue() received %s as a hide value' 85 | ); 86 | } 87 | 88 | /** 89 | * Validate $value: 90 | * - check if it is empty, 91 | * - if it is string or if an object with __toString() method 92 | * @param mixed $value 93 | * @param string $error error message placeholder 94 | * @return boolean 95 | */ 96 | protected function validateValue($value, $error) 97 | { 98 | if (empty($value)) 99 | { 100 | $wrongType = 'an empty value'; 101 | } else 102 | if (is_scalar($value)) 103 | { 104 | $wrongType = ''; 105 | } else 106 | if (is_array($value)) 107 | { 108 | $wrongType = 'an array'; 109 | } else 110 | if (is_object($value)) 111 | { 112 | $wrongType = !is_callable(array($value, '__toString')) 113 | ? 'an object' 114 | : ''; 115 | } else 116 | { 117 | /* resources ? */ 118 | $wrongType = 'unexpected type (' . (string) $value . ')'; 119 | } 120 | 121 | if ($wrongType) 122 | { 123 | trigger_error( 124 | sprintf($error, $wrongType), 125 | E_USER_WARNING 126 | ); 127 | 128 | return false; 129 | } 130 | 131 | return true; 132 | } 133 | 134 | /** 135 | * Add $value to the list of values to hide 136 | * 137 | * @param mixed $value 138 | * @param string $error error message placeholder 139 | * @return boolean|NULL TRUE if added, FALSE if wrong 140 | * type, NULL if already added 141 | */ 142 | protected function addValue($value, $error = '%s') 143 | { 144 | if (!$this->validateValue($value, $error)) 145 | { 146 | return false; 147 | } 148 | 149 | if (in_array($value, $this->hideValues)) 150 | { 151 | return null; 152 | } 153 | 154 | $this->hideValues[] = $value; 155 | return true; 156 | } 157 | } 158 | --------------------------------------------------------------------------------