├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin ├── cost-check └── version-check ├── composer.json ├── phpdoc.dist.xml ├── phpunit.xml.dist ├── src └── JeremyKendall │ └── Password │ ├── Decorator │ ├── AbstractDecorator.php │ ├── StorageDecorator.php │ └── UpgradeDecorator.php │ ├── PasswordHashFailureException.php │ ├── PasswordValidator.php │ ├── PasswordValidatorInterface.php │ ├── Result.php │ └── Storage │ ├── IdentityMissingException.php │ └── StorageInterface.php ├── tests ├── JeremyKendall │ └── Password │ │ └── Tests │ │ ├── Decorator │ │ ├── AbstractDecoratorTest.php │ │ ├── IntegrationTest.php │ │ ├── KarptoniteRehashUpgradeDecoratorTest.php │ │ ├── StorageDecoratorTest.php │ │ └── UpgradeDecoratorTest.php │ │ └── PasswordValidatorTest.php └── bootstrap.php └── travis.phpunit.xml /.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | composer.lock 3 | vendor/ -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | inherit: true 2 | 3 | tools: 4 | external_code_coverage: true 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | - 5.6 8 | - 7.0 9 | 10 | before_script: 11 | - composer self-update 12 | - composer install --prefer-dist 13 | 14 | script: phpunit -c phpunit.xml.dist 15 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | ## Pull Requests 4 | 5 | 1. Create your own [fork][1] of the repo 6 | 2. Create a new branch for each feature or improvement 7 | 3. Send a pull request from each feature branch to the **develop** branch 8 | 9 | It is very important to separate new features or improvements into separate 10 | feature branches, and to send a pull request for each branch. This allows me to 11 | review and pull in new features or improvements individually. 12 | 13 | ## Style Guide 14 | 15 | All pull requests must adhere to the [PSR-2 standard][2]. 16 | 17 | ## Unit Testing 18 | 19 | All pull requests must be accompanied by passing unit tests and complete code 20 | coverage. This library uses PHPUnit for testing. 21 | 22 | [Learn about PHPUnit][3] 23 | 24 | [1]: https://help.github.com/articles/fork-a-repo 25 | [2]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md 26 | [3]: https://github.com/sebastianbergmann/phpunit/ 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2014 Jeremy Kendall 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so, 8 | subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Password Validator [![Build Status](https://travis-ci.org/jeremykendall/password-validator.png?branch=master)](https://travis-ci.org/jeremykendall/password-validator) [![Coverage Status](https://coveralls.io/repos/jeremykendall/password-validator/badge.png?branch=master)](https://coveralls.io/r/jeremykendall/password-validator?branch=master) 2 | 3 | **Password Validator** *validates* [`password_hash`][2] generated passwords, *rehashes* 4 | passwords as necessary, and will *upgrade* legacy passwords. 5 | 6 | Read the introductory blog post: [PHP Password Hashing: A Dead Simple Implementation][9] 7 | 8 | *Password Validator is available for all versions of PHP >= 5.3.7.* 9 | 10 | ## Motivation 11 | 12 | Why? Because one must always[ encrypt passwords for highest level of 13 | security][7], and the new [PHP password hashing][1] functions provide that level of 14 | security. 15 | 16 | The **Password Validator** library makes it (more) trivial to use the new 17 | password hash functions in your application. Just add the validator to your 18 | authentication script and you're up and running. 19 | 20 | The really big deal here is the **ease of upgrading** from your current legacy 21 | hashes to the new, more secure PHP password hash hashes. Simply wrap the 22 | `PasswordValidator` in the `UpgradeDecorator`, provide a callback to validate 23 | your existing password hashing scheme, and BOOM, you're using new password 24 | hashes in a manner *completely transparent* to your application's users. Nifty, 25 | huh? 26 | 27 | ## Usage 28 | 29 | ### Password Validation 30 | 31 | If you're already using [`password_hash`][2] generated passwords in your 32 | application, you need do nothing more than add the validator in your 33 | authentication script. The validator uses [`password_verify`][3] to test 34 | the validity of the provided password hash. 35 | 36 | ``` php 37 | use JeremyKendall\Password\PasswordValidator; 38 | 39 | $validator = new PasswordValidator(); 40 | $result = $validator->isValid($_POST['password'], $hashedPassword); 41 | 42 | if ($result->isValid()) { 43 | // password is valid 44 | } 45 | ``` 46 | 47 | If your application requires options other than the `password_hash` defaults, 48 | you can set the `cost` option with `PasswordValidator::setOptions()`. 49 | 50 | ``` php 51 | $options = array( 52 | 'cost' => 11 53 | ); 54 | $validator->setOptions($options); 55 | ``` 56 | 57 | **IMPORTANT**: `PasswordValidator` uses a default cost of `10`. If your 58 | existing hash implementation requires a different cost, make sure to specify it 59 | using `PasswordValidator::setOptions()`. If you do not do so, all of your 60 | passwords will be rehashed using a cost of `10`. 61 | 62 | ### Rehashing 63 | 64 | Each valid password is tested using [`password_needs_rehash`][4]. If a rehash 65 | is necessary, the valid password is hashed using `password_hash` with the 66 | provided options. The result code `Result::SUCCESS_PASSWORD_REHASHED` will be 67 | returned from `Result::getCode()` and the new password hash is available via 68 | `Result::getPassword()`. 69 | 70 | ``` php 71 | if ($result->getCode() === Result::SUCCESS_PASSWORD_REHASHED) { 72 | $rehashedPassword = $result->getPassword(); 73 | // Persist rehashed password 74 | } 75 | ``` 76 | 77 | **IMPORTANT**: If the password has been rehashed, it's critical that you 78 | persist the updated password hash. Otherwise, what's the point, right? 79 | 80 | ### Upgrading Legacy Passwords 81 | 82 | You can use the `PasswordValidator` whether or not you're currently using 83 | `password_hash` generated passwords. The validator will transparently upgrade 84 | your current legacy hashes to the new `password_hash` generated hashes as each 85 | user logs in. All you need to do is provide a validator callback for your 86 | password hash and then [decorate][6] the validator with the `UpgradeDecorator`. 87 | 88 | ``` php 89 | use JeremyKendall\Password\Decorator\UpgradeDecorator; 90 | 91 | // Example callback to validate a sha512 hashed password 92 | $callback = function ($password, $passwordHash, $salt) { 93 | if (hash('sha512', $password . $salt) === $passwordHash) { 94 | return true; 95 | } 96 | 97 | return false; 98 | }; 99 | 100 | $validator = new UpgradeDecorator(new PasswordValidator(), $callback); 101 | $result = $validator->isValid('password', 'password-hash', 'legacy-salt'); 102 | ``` 103 | 104 | The `UpgradeDecorator` will validate a user's current password using the 105 | provided callback. If the user's password is valid, it will be hashed with 106 | `password_hash` and returned in the `Result` object, as above. 107 | 108 | All password validation attempts will eventually pass through the 109 | `PasswordValidator`. This allows a password that has already been upgraded to 110 | be properly validated, even when using the `UpgradeDecorator`. 111 | 112 | #### Alternate Upgrade Technique 113 | 114 | Rather than upgrading each user's password as they log in, it's possible to 115 | preemptively rehash persisted legacy hashes all at once. `PasswordValidator` 116 | and the `UpgradeDecorator` can then be used to validate passwords against the 117 | rehashed legacy hashes, at which point the user's plain text password will be 118 | hashed with `password_hash`, completing the upgrade process. 119 | 120 | For more information on this technique, please see Daniel Karp's 121 | [Rehashing Password Hashes][10] blog post, and review 122 | [`JeremyKendall\Password\Tests\Decorator\KarptoniteRehashUpgradeDecoratorTest`][11] 123 | to see a sample implementation. 124 | 125 | ### Persisting Rehashed Passwords 126 | 127 | Whenever a validation attempt returns `Result::SUCCESS_PASSWORD_REHASHED`, it's 128 | important to persist the updated password hash. 129 | 130 | ``` php 131 | if ($result->getCode() === Result::SUCCESS_PASSWORD_REHASHED) { 132 | $rehashedPassword = $result->getPassword(); 133 | // Persist rehashed password 134 | } 135 | ``` 136 | 137 | While you can always perform the test and then update your user database 138 | manually, if you choose to use the **Storage Decorator** all rehashed passwords 139 | will be automatically persisted. 140 | 141 | The Storage Decorator takes two constructor arguments: An instance of 142 | `PasswordValidatorInterface` and an instance of the 143 | `JeremyKendall\Password\Storage\StorageInterface`. 144 | 145 | #### StorageInterface 146 | 147 | The `StorageInterface` includes a single method, `updatePassword()`. A class 148 | honoring the interface might look like this: 149 | 150 | ``` php 151 | db = $db; 162 | } 163 | 164 | public function updatePassword($identity, $password) 165 | { 166 | $sql = 'UPDATE users SET password = :password WHERE username = :identity'; 167 | $stmt = $this->db->prepare($sql); 168 | $stmt->execute(array('password' => $password, 'identity' => $identity)); 169 | } 170 | } 171 | ``` 172 | 173 | #### Storage Decorator 174 | 175 | With your `UserDao` in hand, you're ready to decorate a 176 | `PasswordValidatorInterface`. 177 | 178 | ``` php 179 | use Example\UserDao; 180 | use JeremyKendall\Password\Decorator\StorageDecorator; 181 | 182 | $storage = new UserDao($db); 183 | $validator = new StorageDecorator(new PasswordValidator(), $storage); 184 | 185 | // If validation results in a rehash, the new password hash will be persisted 186 | $result = $validator->isValid('password', 'passwordHash', null, 'username'); 187 | ``` 188 | 189 | **IMPORTANT**: You must pass the optional fourth argument (`$identity`) to 190 | `isValid()` when calling `StorageDecorator::isValid()`. If you do not do so, 191 | the `StorageDecorator` will throw an `IdentityMissingException`. 192 | 193 | #### Combining Storage Decorator with Upgrade Decorator 194 | 195 | It is possible to chain decorators together thanks to the 196 | [Decorator Pattern](https://en.wikipedia.org/wiki/Decorator_pattern). A great way to use this is to combine the 197 | `StorageDecorator` and `UpgradeDecorator` together to first update a legacy hash and then save it. Doing so is very 198 | simple - you just need to pass an instance of the `StorageDecorator` as a constructor argument to `UpgradeDecorator`: 199 | 200 | ``` php 201 | use Example\UserDao; 202 | use JeremyKendall\Password\Decorator\StorageDecorator; 203 | use JeremyKendall\Password\Decorator\UpgradeDecorator; 204 | 205 | // Example callback to validate a sha512 hashed password 206 | $callback = function ($password, $passwordHash, $salt) { 207 | if (hash('sha512', $password . $salt) === $passwordHash) { 208 | return true; 209 | } 210 | 211 | return false; 212 | }; 213 | 214 | $storage = new UserDao($db); 215 | $storageDecorator = new StorageDecorator(new PasswordValidator(), $storage); 216 | $validator = new UpgradeDecorator($storageDecorator, $callback); 217 | 218 | // If validation results in a rehash, the new password hash will be persisted 219 | $result = $validator->isValid('password', 'passwordHash', null, 'username'); 220 | ``` 221 | 222 | 223 | ### Validation Results 224 | 225 | Each validation attempt returns a `JeremyKendall\Password\Result` object. The 226 | object provides some introspection into the status of the validation process. 227 | 228 | * `Result::isValid()` will return `true` if the attempt was successful 229 | * `Result::getCode()` will return one of three possible `int` codes: 230 | * `Result::SUCCESS` if the validation attempt was successful 231 | * `Result::SUCCESS_PASSWORD_REHASHED` if the attempt was successful and the password was rehashed 232 | * `Result::FAILURE_PASSWORD_INVALID` if the attempt was unsuccessful 233 | * `Result::getPassword()` will return the rehashed password, but only if the password was rehashed 234 | 235 | ### Database Schema Changes 236 | 237 | As mentioned above, because this library uses the `PASSWORD_DEFAULT` algorithm, 238 | it's important your password field be `VARCHAR(255)` to account for future 239 | updates to the default password hashing algorithm. 240 | 241 | ## Helper Scripts 242 | 243 | After running `composer install`, there are two helper scripts available, both 244 | related to the password hash functions. 245 | 246 | ### version-check 247 | 248 | If you're not already running PHP 5.5+, you should run `version-check` to 249 | ensure your version of PHP is capable of using password-compat, the userland 250 | implementation of the PHP password hash functions. Run `./vendor/bin/version-check` 251 | from the root of your project. The result of the script is pass/fail. 252 | 253 | ### cost-check 254 | 255 | The default `cost` used by `password_hash` is 10. This may or may not be 256 | appropriate for your production hardware, and it's entirely likely you can use 257 | a higher cost than the default. `cost-check` is based on the [finding a good 258 | cost][8] example in the PHP documentation. Simply run `./vendor/bin/cost-check` from the command line and an appropriate cost will be returned. 259 | 260 | **NOTE**: The default time target is 0.2 seconds. You may choose a higher or lower 261 | target by passing a float argument to `cost-check`, like so: 262 | 263 | ``` bash 264 | $ ./vendor/bin/cost-check 0.4 265 | Appropriate 'PASSWORD_DEFAULT' Cost Found: 13 266 | ``` 267 | 268 | ## Installation 269 | 270 | The only officially supported method of installation is via 271 | [Composer](http://getcomposer.org). 272 | 273 | Running the following command will add the latest version of the library to your project: 274 | ``` bash 275 | $ composer require jeremykendall/password-validator 276 | ``` 277 | 278 | You can update to the latest version with this command: 279 | ``` bash 280 | $ composer update jeremykendall/password-validator 281 | ``` 282 | 283 | If you're not already using Composer in your project, add the autoloader to your project: 284 | 285 | ``` php 286 | $cost)); 45 | $end = microtime(true); 46 | } while (($end - $start) < $timeTarget); 47 | 48 | \cli\line("%yAppropriate 'PASSWORD_DEFAULT' Cost Found:%y %2%k $cost %2%k%n"); 49 | -------------------------------------------------------------------------------- /bin/version-check: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | =5.3.7", 31 | "ircmaxell/password-compat": "1.*", 32 | "wp-cli/php-cli-tools": "0.10.*" 33 | }, 34 | "require-dev": { 35 | "league/phpunit-coverage-listener": "~1.1", 36 | "phpunit/phpunit": "4.*" 37 | }, 38 | "autoload": { 39 | "psr-0": { 40 | "JeremyKendall\\Password\\": "src/" 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /phpdoc.dist.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Password Validator 4 | 5 | ./build/docs 6 | 7 | php 8 | 9 | 10 | 11 | ./build/docs 12 | 13 | 14 | ./src 15 | 16 | 17 |