├── .github_changelog_generator ├── .gitignore ├── LICENSE ├── README.md ├── codeception.dist.yml ├── composer.json ├── src ├── MDFiveValidator.php ├── Manager.php ├── ManagerFactory.php ├── PasswordLock.php ├── PhpassValidator.php ├── Validator.php ├── ValidatorInterface.php └── pluggable.php ├── tests ├── _data │ ├── .gitkeep │ └── dump.sql ├── _support │ ├── Helper │ │ └── Wpunit.php │ └── WpunitTester.php ├── wpunit.suite.dist.yml └── wpunit │ ├── MDFiveValidatorTest.php │ ├── ManagerFactoryTest.php │ ├── PasswordLockTest.php │ ├── PhpassValidatorTest.php │ ├── ValidatorTest.php │ ├── WPCheckPasswordTest.php │ ├── WPHashPasswordTest.php │ └── WPSetPasswordTest.php └── wp-password-argon-two.php /.github_changelog_generator: -------------------------------------------------------------------------------- 1 | unreleased=true 2 | future-release=0.2.1 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Build ### 2 | /build/ 3 | /release/ 4 | 5 | ### Codeception ### 6 | /codeception.yml 7 | /tests/*.suite.yml 8 | /tests/_output/* 9 | /tests/_support/_generated/ 10 | 11 | ### Composer ### 12 | /vendor/ 13 | /composer.lock 14 | 15 | ### npm ### 16 | /node_modules/ 17 | 18 | ### PhpStorm ### 19 | /.idea/ 20 | 21 | ### i18n ### 22 | /languages/ 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Typist Tech 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 | > [!CAUTION] 2 | > [WP Password Argon Two](https://github.com/typisttech/wp-password-argon-two) has been **abandoned**. 3 | > 4 | > If you want to maintain a fork of [WP Password Argon Two](https://github.com/typisttech/wp-password-argon-two), read this [blog post](https://blog.ircmaxell.com/2015/03/security-issue-combining-bcrypt-with.html) ([Wayback Machine snaptshot](https://web.archive.org/web/20240722115642/https://blog.ircmaxell.com/2015/03/security-issue-combining-bcrypt-with.html)). 5 | > Otherwise, use [roots/wp-password-bcrypt](https://github.com/roots/wp-password-bcrypt). 6 | 7 | # WP Password Argon Two 8 | 9 | [![Latest Stable Version](https://poser.pugx.org/typisttech/wp-password-argon-two/v/stable)](https://packagist.org/packages/typisttech/wp-password-argon-two) 10 | [![Total Downloads](https://poser.pugx.org/typisttech/wp-password-argon-two/downloads)](https://packagist.org/packages/typisttech/wp-password-argon-two) 11 | [![Build Status](https://travis-ci.org/TypistTech/wp-password-argon-two.svg?branch=master)](https://travis-ci.org/TypistTech/wp-password-argon-two) 12 | [![StyleCI](https://styleci.io/repos/121093174/shield?branch=master)](https://styleci.io/repos/121093174) 13 | [![License](https://poser.pugx.org/typisttech/wp-password-argon-two/license)](https://packagist.org/packages/typisttech/wp-password-argon-two) 14 | [![Donate via PayPal](https://img.shields.io/badge/Donate-PayPal-blue.svg)](https://typist.tech/donate/wp-password-argon-two/) 15 | [![Hire Typist Tech](https://img.shields.io/badge/Hire-Typist%20Tech-ff69b4.svg)](https://typist.tech/contact/) 16 | 17 | Securely store WordPress user passwords in database with Argon2i hashing and SHA-512 HMAC using PHP's native functions. 18 | 19 | 20 | 21 | 22 | 23 | - [Goal](#goal) 24 | - [Magic Moments](#magic-moments) 25 | - [Requirements](#requirements) 26 | - [Do Your Homework](#do-your-homework) 27 | - [PHP 7.2+ and compiled `--with-password-argon2`](#php-72-and-compiled---with-password-argon2) 28 | - [Installation](#installation) 29 | - [Step 0](#step-0) 30 | - [Step 1](#step-1) 31 | - [Option A: Via Composer Autoload (Recommended)](#option-a-via-composer-autoload-recommended) 32 | - [Option B: As a Must-use Plugin (Last Resort)](#option-b-as-a-must-use-plugin-last-resort) 33 | - [Step 2](#step-2) 34 | - [Option A - Use Constants](#option-a---use-constants) 35 | - [Option B - Use Environment Variables](#option-b---use-environment-variables) 36 | - [Usage](#usage) 37 | - [Pepper Migration](#pepper-migration) 38 | - [Argon2i Options](#argon2i-options) 39 | - [Uninstallation](#uninstallation) 40 | - [Frequently Asked Questions](#frequently-asked-questions) 41 | - [What have you done with the passwords?](#what-have-you-done-with-the-passwords) 42 | - [I have installed this plugin. Does it mean my WordPress site is *unhackable*?](#i-have-installed-this-plugin-does-it-mean-my-wordpress-site-is-unhackable) 43 | - [Did you reinvent the cryptographic functions?](#did-you-reinvent-the-cryptographic-functions) 44 | - [Pepper migration look great. Does it mean that I can keep as many pepper keys as I want?](#pepper-migration-look-great-does-it-mean-that-i-can-keep-as-many-pepper-keys-as-i-want) 45 | - [What if my pepper is compromised?](#what-if-my-pepper-is-compromised) 46 | - [Is pepper-ing perfect?](#is-pepper-ing-perfect) 47 | - [Is WordPress' phpass hasher or Bcrypt insecure?](#is-wordpress-phpass-hasher-or-bcrypt-insecure) 48 | - [Why use Argon2i over the others?](#why-use-argon2i-over-the-others) 49 | - [Does this plugin has 72-character limit like Bcrypt?](#does-this-plugin-has-72-character-limit-like-bcrypt) 50 | - [It looks awesome. Where can I find some more goodies like this?](#it-looks-awesome-where-can-i-find-some-more-goodies-like-this) 51 | - [This plugin isn't on wp.org. Where can I give a :star::star::star::star::star: review?](#this-plugin-isnt-on-wporg-where-can-i-give-a-starstarstarstarstar-review) 52 | - [This plugin isn't on wp.org. Where can I make a complaint?](#this-plugin-isnt-on-wporg-where-can-i-make-a-complaint) 53 | - [Alternatives](#alternatives) 54 | - [Support!](#support) 55 | - [Donate](#donate) 56 | - [Why don't you hire me?](#why-dont-you-hire-me) 57 | - [Want to help in other way? Want to be a sponsor?](#want-to-help-in-other-way-want-to-be-a-sponsor) 58 | - [Developing](#developing) 59 | - [Feedback](#feedback) 60 | - [Change Log](#change-log) 61 | - [Security](#security) 62 | - [Credits](#credits) 63 | - [License](#license) 64 | 65 | 66 | 67 | ## Goal 68 | 69 | Replace WordPress' [phpass](http://openwall.com/phpass) hasher with Argon2i hashing and SHA-512 HMAC. 70 | 71 | Adopted from [Mozilla secure coding guidelines](https://wiki.mozilla.org/WebAppSec/Secure_Coding_Guidelines#Password_Storage): 72 | 73 | * Passwords stored in a database should using the hmac+argon2i function. 74 | 75 | The purpose of HMAC and Argon2i storage is as follows: 76 | 77 | * Argon2i provides a hashing mechanism which can be configured to consume sufficient time to prevent brute forcing of hash values even with many computers 78 | * Argon2i can be easily adjusted at any time to increase the amount of work and thus provide protection against more powerful systems 79 | * The nonce(pepper) for the HMAC value is designed to be stored on the file system and not in the databases storing the password hashes. In the event of a compromise of hash values due to SQL injection, the nonce(pepper) will still be an unknown value since it would not be compromised from the file system. This significantly increases the complexity of brute forcing the compromised hashes considering both Argon2i and a large unknown nonce(pepper) value 80 | * The HMAC operation is simply used as a secondary defense in the event there is a design weakness with Argon2i that could leak information about the password or aid an attacker 81 | 82 | ## Magic Moments 83 | 84 | WP Password Argon Two just works when: 85 | * upgrading from extremely old WordPress versions 86 | 87 | user passwords were hashed with MD5 88 | 89 | * upgrading from recent WordPress versions 90 | 91 | user passwords were hashed with [phpass](http://openwall.com/phpass) hasher 92 | 93 | * upgrading from [WP Password Bcrypt](https://github.com/roots/wp-password-bcrypt) 94 | 95 | user passwords were hashed with Bcrypt 96 | 97 | * changing Argon2i options 98 | 99 | * using new pepper while moving the old ones into `WP_PASSWORD_ARGON_TWO_FALLBACK_PEPPERS` 100 | 101 | User passwords will be rehashed during the next login. 102 | 103 | ## Requirements 104 | 105 | ### Do Your Homework 106 | 107 | Don't blindly trust any random security guide/plugin on the scary internet - including this one! 108 | 109 | Do your research: 110 | * Read the whole [readme](./README.md) 111 | * Read the [source code](./src) 112 | * Compare with other [alternatives](#alternatives) 113 | 114 | ### PHP 7.2+ and compiled `--with-password-argon2` 115 | 116 | To check whether PHP is compiled with Argon2: 117 | ```bash 118 | # Good: Compiled with Argon2 119 | ➜ php -r 'print_r(get_defined_constants());' | grep -i argon 120 | [PASSWORD_ARGON2I] => 2 121 | [PASSWORD_ARGON2_DEFAULT_MEMORY_COST] => 1024 122 | [PASSWORD_ARGON2_DEFAULT_TIME_COST] => 2 123 | [PASSWORD_ARGON2_DEFAULT_THREADS] => 2 124 | [SODIUM_CRYPTO_PWHASH_ALG_ARGON2I13] => 1 125 | [SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13] => 2 126 | [SODIUM_CRYPTO_PWHASH_STRPREFIX] => $argon2id$ 127 | ``` 128 | 129 | If you don't get the above output, either re-compile PHP 7.2+ with the flag `--with-password-argon2` or: 130 | 131 | * Ubuntu 132 | ```bash 133 | ➜ sudo add-apt-repository ppa:ondrej/php 134 | ➜ sudo apt-get update 135 | ➜ sudo apt-get install php7.2 136 | ``` 137 | * macOS 138 | ```bash 139 | ➜ brew update 140 | ➜ brew install php 141 | ``` 142 | 143 | ## Installation 144 | 145 | ### Step 0 146 | 147 | Read the whole [readme](./README.md) and the [source code](./src) before going any further. 148 | 149 | ### Step 1 150 | 151 | This plugin **should not** be installed as a normal WordPress plugin. 152 | 153 | #### Option A: Via Composer Autoload (Recommended) 154 | 155 | ```bash 156 | ➜ composer require typisttech/wp-password-argon-two 157 | ``` 158 | 159 | Note: Files in [`src`](./src) will be autoloaded by composer. WP Password Argon Two **won't** appear in the WP admin dashboard. 160 | 161 | #### Option B: As a Must-use Plugin (Last Resort) 162 | 163 | Manually copy [`wp-password-argon-two.php`](./wp-password-argon-two.php) and the whole [`src`](./src) directory into [`mu-plugins` folder](https://codex.wordpress.org/Must_Use_Plugins). 164 | 165 | ```bash 166 | # Example 167 | ➜ tree ./wp-content/mu-plugins 168 | ./wp-content/mu-plugins 169 | ├── src 170 | │   ├── Manager.php 171 | │   ├── ManagerFactory.php 172 | │   ├── PasswordLock.php 173 | │   ├── Validator.php 174 | │   ├── ValidatorInterface.php 175 | │   ├── WordPressValidator.php 176 | │   └── pluggable.php 177 | └── wp-password-argon-two.php 178 | ``` 179 | 180 | ### Step 2 181 | 182 | #### Option A - Use Constants 183 | 184 | Add these constants into `wp-config.php`: 185 | ```php 186 | define('WP_PASSWORD_ARGON_TWO_PEPPER', 'your-long-and-random-pepper'); 187 | define('WP_PASSWORD_ARGON_TWO_FALLBACK_PEPPERS', []); 188 | define('WP_PASSWORD_ARGON_TWO_OPTIONS', []); 189 | ``` 190 | 191 | #### Option B - Use Environment Variables 192 | 193 | Defining the required constants in application code violates [12-factor principle](https://12factor.net/). The [`typisttech/wp-password-argon-two-env`](https://github.com/TypistTech/wp-password-argon-two-env) package allows you to configure with environment variables. 194 | 195 | Recommended for all [Trellis](https://github.com/roots/trellis) users. 196 | 197 | ## Usage 198 | 199 | ### Pepper Migration 200 | 201 | In some cases, you want to change the pepper without changing all user passwords. 202 | 203 | ```php 204 | define('WP_PASSWORD_ARGON_TWO_PEPPER', 'new-pepper'); 205 | define('WP_PASSWORD_ARGON_TWO_FALLBACK_PEPPERS', [ 206 | 'old-pepper-2', 207 | 'old-pepper-1', 208 | ]); 209 | ``` 210 | 211 | During the next user login, his/her password will be rehashed with `new-pepper`. 212 | 213 | ### Argon2i Options 214 | 215 | > Due to the variety of platforms PHP runs on, the cost factors are deliberately set low as to not accidentally exhaust system resources on shared or low resource systems when using the default cost parameters. Consequently, users should adjust the cost factors to match the system they're working on. As Argon2 doesn't have any "bad" values, however consuming more resources is considered better than consuming less. Users are encouraged to adjust the cost factors for the platform they're developing for. 216 | > 217 | > -- [PHP RFC](https://wiki.php.net/rfc/argon2_password_hash#discussion_issues) 218 | 219 | You can adjust the options via `WP_PASSWORD_ARGON_TWO_OPTIONS`: 220 | ```php 221 | // Example 222 | define('WP_PASSWORD_ARGON_TWO_OPTIONS', [ 223 | 'memory_cost' => 1<<17, // 128 Mb 224 | 'time_cost' => 4, 225 | 'threads' => 3, 226 | ]); 227 | ``` 228 | 229 | Learn more about [available options](https://secure.php.net/manual/en/function.password-hash.php) and [picking appropriate options](https://stackoverflow.com/a/48322039). 230 | 231 | ## Uninstallation 232 | 233 | You have to regenerate all user passwords after uninstallation because we can't rehash without knowing the passwords in plain text. 234 | 235 | ## Frequently Asked Questions 236 | 237 | ### What have you done with the passwords? 238 | 239 | In a nutshell: 240 | ```php 241 | password_hash( 242 | hash_hmac('sha512', $userPassword, WP_PASSWORD_ARGON_TWO_PEPPER), 243 | PASSWORD_ARGON2I, 244 | WP_PASSWORD_ARGON_TWO_OPTIONS 245 | ); 246 | ``` 247 | 248 | Don't take my word for it. Read the [source code](./src)! 249 | 250 | ### I have installed this plugin. Does it mean my WordPress site is *unhackable*? 251 | 252 | No website is *unhackable*. 253 | 254 | To have a secure WordPress site, you have to keep all these up-to-date: 255 | * WordPress core 256 | * PHP 257 | * this plugin 258 | * all other WordPress themes and plugins 259 | * everything on the server 260 | * other security practices 261 | * your mindset 262 | 263 | ### Did you reinvent the cryptographic functions? 264 | 265 | Of course not! This plugin use PHP's native functions. 266 | 267 | Repeat: Read the [source code](./src)! 268 | 269 | ### Pepper migration look great. Does it mean that I can keep as many pepper keys as I want? 270 | 271 | In a sense, yes, you could do that. However, each pepper slows down the login process a little bit. 272 | 273 | To test the worst case, log in with an incorrect password. 274 | 275 | ### What if my pepper is compromised? 276 | 277 | 1. Remove that pepper from `WP_PASSWORD_ARGON_TWO_PEPPER` and `WP_PASSWORD_ARGON_TWO_FALLBACK_PEPPERS` 278 | 1. Regenerate all user passwords 279 | 280 | ### Is pepper-ing perfect? 281 | 282 | No! Read [paragonie's explaination](https://paragonie.com/blog/2015/04/secure-authentication-php-with-long-term-persistence#pepper). 283 | 284 | For those who can't stand with the drawbacks, use one of the [alternatives](#alternatives) instead. 285 | 286 | ### Is WordPress' phpass hasher or Bcrypt insecure? 287 | 288 | Both WordPress' [phpass](http://openwall.com/phpass) hasher and Bcrypt are secure. There is no emergent reason to upgrade. 289 | 290 | Learn more about the [reasons](https://roots.io/wordpress-password-security-follow-up/) about not using WordPress' default. 291 | 292 | ### Why use Argon2i over the others? 293 | 294 | Argon2 password-based key derivation function is the winner of the [Password Hashing Competition](https://password-hashing.net) in July 2015, ranked better than Bcrypt and PBKDF2. 295 | 296 | Argon2 comes with 3 different modes: Argon2d, Argon2i, Argon2id. Argon2i is the one for password hashing. See: https://crypto.stackexchange.com/a/49969 297 | 298 | ### Does this plugin has 72-character limit like Bcrypt? 299 | 300 | No. Read [the test](https://github.com/TypistTech/wp-password-argon-two/blob/6ec33700ab80e700045063895459212dd52b30b7/tests/wpunit/PasswordLockTest.php#L46-L57). 301 | 302 | ### It looks awesome. Where can I find some more goodies like this? 303 | 304 | * Articles on Typist Tech's [blog](https://typist.tech) 305 | * [Tang Rufus' WordPress plugins](https://profiles.wordpress.org/tangrufus#content-plugins) on wp.org 306 | * More projects on [Typist Tech's GitHub profile](https://github.com/TypistTech) 307 | * Stay tuned on [Typist Tech's newsletter](https://typist.tech/go/newsletter) 308 | * Follow [Tang Rufus' Twitter account](https://twitter.com/TangRufus) 309 | * Hire [Tang Rufus](https://typist.tech/contact) to build your next awesome site 310 | 311 | ### This plugin isn't on wp.org. Where can I give a :star::star::star::star::star: review? 312 | 313 | Thanks! 314 | 315 | Consider writing a blog post, submitting pull requests, [donating](https://typist.tech/donation/) or [hiring me](https://typist.tech/contact/) instead. 316 | 317 | ### This plugin isn't on wp.org. Where can I make a complaint? 318 | 319 | To be honest, I don't care. 320 | 321 | If you really want to share your 1-star review, send me an email - in the first paragraph, state how many times I have told you to read the plugin source code. 322 | 323 | ## Alternatives 324 | 325 | * [paragonie/halite](https://github.com/paragonie/halite/blob/55706ac843d8ee90426b455ea28673cf85e4a1e2/doc/Examples/01-passwords.php) 326 | * [paragonie/password_lock](https://github.com/paragonie/password_lock) 327 | * [roots/wp-password-bcrypt](https://github.com/roots/wp-password-bcrypt) 328 | * [PHP Native password hash](https://wordpress.org/plugins/password-hash/) 329 | 330 | ## Support! 331 | 332 | ### Donate 333 | 334 | Love WP Password Argon Two? Help me maintain it, a [donation here](https://typist.tech/donation/) can help with it. 335 | 336 | ### Why don't you hire me? 337 | 338 | Ready to take freelance WordPress jobs. Contact me via the contact form [here](https://typist.tech/contact/) or, via email [info@typist.tech](mailto:info@typist.tech) 339 | 340 | ### Want to help in other way? Want to be a sponsor? 341 | 342 | Contact: [Tang Rufus](mailto:tangrufus@gmail.com) 343 | 344 | ## Developing 345 | 346 | To setup a developer workable version you should run these commands: 347 | 348 | ```bash 349 | $ composer create-project --keep-vcs --no-install typisttech/wp-password-argon-two:dev-master 350 | $ cd wp-password-argon-two 351 | $ composer install 352 | ``` 353 | 354 | To run the tests: 355 | ``` bash 356 | $ composer test 357 | ``` 358 | 359 | ## Feedback 360 | 361 | **Please provide feedback!** We want to make this library useful in as many projects as possible. 362 | Please submit an [issue](https://github.com/TypistTech/wp-password-argon-two/issues/new) and point out what you do and don't like, or fork the project and make suggestions. 363 | **No issue is too small.** 364 | 365 | ## Change Log 366 | 367 | Please see [CHANGELOG](./CHANGELOG.md) for more information on what has changed recently. 368 | 369 | ## Security 370 | 371 | If you discover any security related issues, please email [wp-password-argon-two@typist.tech](mailto:wp-password-argon-two@typist.tech) instead of using the issue tracker. 372 | 373 | ## Credits 374 | 375 | [WP Password Argon Two](https://github.com/TypistTech/wp-password-argon-two) is a [Typist Tech](https://typist.tech) project and maintained by [Tang Rufus](https://twitter.com/Tangrufus), freelance developer for [hire](https://typist.tech/contact/). 376 | 377 | Full list of contributors can be found [here](https://github.com/TypistTech/wp-password-argon-two/graphs/contributors). 378 | 379 | ## License 380 | 381 | The MIT License (MIT). Please see [License File](./LICENSE) for more information. 382 | -------------------------------------------------------------------------------- /codeception.dist.yml: -------------------------------------------------------------------------------- 1 | namespace: TypistTech\WPPasswordArgonTwo 2 | paths: 3 | tests: tests 4 | output: tests/_output 5 | data: tests/_data 6 | support: tests/_support 7 | envs: tests/_envs 8 | actor_suffix: Tester 9 | settings: 10 | shuffle: true 11 | colors: true 12 | extensions: 13 | enabled: 14 | - Codeception\Extension\RunFailed 15 | commands: 16 | - Codeception\Command\GenerateWPUnit 17 | - Codeception\Command\GenerateWPRestApi 18 | - Codeception\Command\GenerateWPRestController 19 | - Codeception\Command\GenerateWPRestPostTypeController 20 | - Codeception\Command\GenerateWPAjax 21 | - Codeception\Command\GenerateWPCanonical 22 | - Codeception\Command\GenerateWPXMLRPC 23 | - Codeception\Command\DbSnapshot 24 | - tad\Codeception\Command\SearchReplace 25 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typisttech/wp-password-argon-two", 3 | "description": "Securely store WordPress user passwords in database with Argon2i hashing and SHA-512 HMAC using PHP's native functions.", 4 | "keywords": [ 5 | "wordpress", 6 | "wp", 7 | "password", 8 | "argon2", 9 | "argon2i", 10 | "sha512", 11 | "hashing", 12 | "hmac" 13 | ], 14 | "homepage": "https://github.com/TypistTech/wp-password-argon-two", 15 | "license": "MIT", 16 | "authors": [ 17 | { 18 | "name": "Typist Tech", 19 | "email": "wp-password-argon-two@typist.tech", 20 | "homepage": "https://typist.tech/" 21 | }, 22 | { 23 | "name": "Tang Rufus", 24 | "email": "tangrufus@gmail.com", 25 | "homepage": "https://typist.tech/", 26 | "role": "Developer" 27 | } 28 | ], 29 | "support": { 30 | "email": "wp-password-argon-two@typist.tech", 31 | "issues": "https://github.com/TypistTech/wp-password-argon-two/issues", 32 | "source": "https://github.com/TypistTech/wp-password-argon-two" 33 | }, 34 | "minimum-stability": "stable", 35 | "require": { 36 | "php": "^7.2 || ^8.0" 37 | }, 38 | "suggest": { 39 | "typisttech/wp-password-argon-two-env": "Allows you configure WP Password Argon Two with environment variables" 40 | }, 41 | "conflict": { 42 | "roots/wp-password-bcrypt": "*" 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "TypistTech\\WPPasswordArgonTwo\\": "src/" 47 | }, 48 | "files": [ 49 | "src/pluggable.php" 50 | ] 51 | }, 52 | "scripts": { 53 | "test": "codecept run wpunit" 54 | }, 55 | "abandoned": "roots/wp-password-bcrypt" 56 | } 57 | -------------------------------------------------------------------------------- /src/MDFiveValidator.php: -------------------------------------------------------------------------------- 1 | passwordLock = $passwordLock; 39 | $this->validators = $validators; 40 | } 41 | 42 | public function isValid(string $password, string $ciphertext): bool 43 | { 44 | if ($this->passwordLock->isValid($password, $ciphertext)) { 45 | return true; 46 | } 47 | 48 | $isValid = array_reduce( 49 | $this->validators, 50 | function (bool $carry, ValidatorInterface $validator) use ($password, $ciphertext): bool { 51 | return $carry || $validator->isValid($password, $ciphertext); 52 | }, 53 | false 54 | ); 55 | 56 | if ($isValid) { 57 | $this->needsRehash = true; 58 | } 59 | 60 | return $isValid; 61 | } 62 | 63 | public function needsRehash(string $ciphertext): bool 64 | { 65 | return $this->needsRehash || $this->passwordLock->needsRehash($ciphertext); 66 | } 67 | 68 | /** 69 | * Creates a password hash. 70 | * 71 | * @param string $password The user's password in plain text. 72 | * 73 | * @return string 74 | */ 75 | public function hash(string $password): string 76 | { 77 | return $this->passwordLock->hash($password); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/ManagerFactory.php: -------------------------------------------------------------------------------- 1 | --with-password-argon2.', 48 | 'wp-password-argon-two' 49 | ); 50 | 51 | return sprintf( 52 | '

%1$s

%2$s

', 53 | wp_kses($message, ['code' => []]), 54 | self::linkToProjectReadme() 55 | ); 56 | } 57 | 58 | private static function linkToProjectReadme(): string 59 | { 60 | $text = __('Learn more on the project readme.', 'wp-password-argon-two'); 61 | 62 | return sprintf( 63 | wp_kses($text, ['a' => ['href' => []]]), 64 | 'https://github.com/TypistTech/wp-password-argon-two' 65 | ); 66 | } 67 | 68 | private static function wpDieTitle(): string 69 | { 70 | return esc_html__('WP Password Argon Two', 'wp-password-argon-two'); 71 | } 72 | 73 | private static function ensureRequiredConstantsDefined(): void 74 | { 75 | $undefinedConstantNames = array_filter(self::REQUIRED_CONSTANTS, function (string $constantName): bool { 76 | return ! defined($constantName); 77 | }); 78 | 79 | if (empty($undefinedConstantNames)) { 80 | return; 81 | } 82 | 83 | wp_die( 84 | self::missingRequiredConstantMessage($undefinedConstantNames), 85 | self::wpDieTitle() 86 | ); 87 | } 88 | 89 | private static function missingRequiredConstantMessage(array $undefinedConstantNames): string 90 | { 91 | $header = esc_html__( 92 | 'WP Password Argon Two requires these constants to be defined:', 93 | 'wp-password-argon-two' 94 | ); 95 | 96 | $listItems = array_reduce( 97 | $undefinedConstantNames, 98 | function (string $carry, string $undefinedConstantName): string { 99 | $carry .= '
  • ' . $undefinedConstantName . '
  • '; 100 | 101 | return $carry; 102 | }, 103 | '' 104 | ); 105 | 106 | return sprintf( 107 | '

    %1$s

    %3$s

    ', 108 | $header, 109 | $listItems, 110 | self::linkToProjectReadme() 111 | ); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/PasswordLock.php: -------------------------------------------------------------------------------- 1 | options = $options; 29 | } 30 | 31 | /** 32 | * Checks to see if the supplied hash implements the algorithm and options provided. If not, it is assumed that the 33 | * hash needs to be rehashed. 34 | * 35 | * @param string $ciphertext The hashed password from database. 36 | * 37 | * @return bool 38 | */ 39 | public function needsRehash(string $ciphertext): bool 40 | { 41 | return password_needs_rehash($ciphertext, self::PASSWORD_HASH_ALGO, $this->options); 42 | } 43 | 44 | /** 45 | * Creates a password hash. 46 | * 47 | * @param string $password The user's password in plain text. 48 | * 49 | * @return string 50 | */ 51 | public function hash(string $password): string 52 | { 53 | return password_hash( 54 | $this->hmac($password), 55 | self::PASSWORD_HASH_ALGO, 56 | $this->options 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/PhpassValidator.php: -------------------------------------------------------------------------------- 1 | CheckPassword($password, $ciphertext); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Validator.php: -------------------------------------------------------------------------------- 1 | pepper = $pepper; 26 | } 27 | 28 | /** 29 | * Validate user submitted password. 30 | * 31 | * @param string $password The user's password in plain text. 32 | * @param string $ciphertext Hash of the user's password to check against. 33 | * 34 | * @return bool 35 | */ 36 | public function isValid(string $password, string $ciphertext): bool 37 | { 38 | return password_verify( 39 | $this->hmac($password), 40 | $ciphertext 41 | ); 42 | } 43 | 44 | protected function hmac(string $password): string 45 | { 46 | return hash_hmac(self::HASH_HMAC_ALGO, $password, $this->pepper); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/ValidatorInterface.php: -------------------------------------------------------------------------------- 1 | isValid($password, $ciphertext); 20 | 21 | if ($isValid && is_numeric($userId) && $manager->needsRehash($ciphertext)) { 22 | $ciphertext = wp_set_password($password, (int) $userId); 23 | } 24 | 25 | return (bool) apply_filters('check_password', $isValid, $password, $ciphertext, $userId); 26 | } 27 | 28 | /** 29 | * Create a hash of a plain text password. 30 | * 31 | * @param string $password Plain text user password to hash. 32 | * 33 | * @return string The hash string of the password. 34 | */ 35 | function wp_hash_password(string $password): string 36 | { 37 | $manager = ManagerFactory::make(); 38 | 39 | return $manager->hash($password); 40 | } 41 | 42 | /** 43 | * Updates the user's password with a newly hashed one. 44 | * The original `wp_set_password` of WordPress v4.9.4 with returning the new $ciphertext. 45 | * 46 | * @param string $password The plaintext new user password. 47 | * @param int $userId User ID. 48 | * 49 | * @return string 50 | */ 51 | function wp_set_password(string $password, int $userId): string 52 | { 53 | global $wpdb; 54 | 55 | $ciphertext = wp_hash_password($password); 56 | $wpdb->update($wpdb->users, ['user_pass' => $ciphertext, 'user_activation_key' => ''], ['ID' => $userId]); 57 | 58 | wp_cache_delete($userId, 'users'); 59 | 60 | return $ciphertext; 61 | } 62 | -------------------------------------------------------------------------------- /tests/_data/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/typisttech/wp-password-argon-two/e063285ea9d5b9adcf549ce84c46c84138c3b723/tests/_data/.gitkeep -------------------------------------------------------------------------------- /tests/_support/Helper/Wpunit.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(ValidatorInterface::class, $validator); 20 | } 21 | 22 | /** @test */ 23 | public function it_checks_correct_password() 24 | { 25 | $validator = new MDFiveValidator(); 26 | 27 | $isValid = $validator->isValid(self::DUMMY_PASSWORD, self::DUMMY_CIPHERTEXT); 28 | 29 | $this->assertTrue($isValid); 30 | } 31 | 32 | /** @test */ 33 | public function it_checks_incorrect_password() 34 | { 35 | $validator = new MDFiveValidator(); 36 | 37 | $isValid = $validator->isValid('incorrect password', self::DUMMY_CIPHERTEXT); 38 | 39 | $this->assertFalse($isValid); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/wpunit/ManagerFactoryTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(Manager::class, $actual); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /tests/wpunit/PasswordLockTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(ValidatorInterface::class, $passwordLock); 23 | } 24 | 25 | /** @test */ 26 | public function it_extends_validator() 27 | { 28 | $passwordLock = new PasswordLock(self::DUMMY_PEPPER, []); 29 | 30 | $this->assertInstanceOf(Validator::class, $passwordLock); 31 | } 32 | 33 | /** @test */ 34 | public function it_hashes_with_argon2i() 35 | { 36 | $passwordLock = new PasswordLock(self::DUMMY_PEPPER, []); 37 | 38 | $ciphertext = $passwordLock->hash(self::DUMMY_PASSWORD); 39 | 40 | $this->assertFalse( 41 | password_needs_rehash($ciphertext, PASSWORD_ARGON2I, WP_PASSWORD_ARGON_TWO_OPTIONS) 42 | ); 43 | } 44 | 45 | /** @test */ 46 | public function it_does_not_truncate_long_password() 47 | { 48 | $longPassword = str_repeat('bbcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+', 110); 49 | $passwordLock = new PasswordLock(self::DUMMY_PEPPER, []); 50 | 51 | $isValid = $passwordLock->isValid( 52 | substr($longPassword, 0, -1) . 'a', 53 | $passwordLock->hash($longPassword) 54 | ); 55 | 56 | $this->assertFalse($isValid); 57 | } 58 | 59 | /** @test */ 60 | public function it_hash_password_into_shorter_than_256_char_string() 61 | { 62 | $longPassword = str_repeat('abcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+', 110); 63 | $passwordLock = new PasswordLock(self::DUMMY_PEPPER, []); 64 | 65 | $ciphertext = $passwordLock->hash($longPassword); 66 | 67 | $this->assertLessThan( 68 | 256, 69 | strlen($ciphertext) 70 | ); 71 | } 72 | 73 | /** @test */ 74 | public function it_needs_rehash_when_options_changed() 75 | { 76 | $oldOptions = [ 77 | 'memory_cost' => 1 << 17, // 128 Mb 78 | 'time_cost' => 2, 79 | 'threads' => 1, 80 | ]; 81 | $newOptions = array_merge($oldOptions, ['time_cost' => 3]); 82 | 83 | $oldPasswordLock = new PasswordLock(self::DUMMY_PEPPER, $oldOptions); 84 | $newPasswordLock = new PasswordLock(self::DUMMY_PEPPER, $newOptions); 85 | 86 | $ciphertext = $oldPasswordLock->hash(self::DUMMY_PASSWORD); 87 | 88 | $needsRehash = $newPasswordLock->needsRehash($ciphertext); 89 | 90 | $this->assertTrue($needsRehash); 91 | } 92 | 93 | /** @test */ 94 | public function it_does_not_need_rehash_when_options_unchanged() 95 | { 96 | $options = [ 97 | 'memory_cost' => 1 << 17, // 128 Mb 98 | 'time_cost' => 2, 99 | 'threads' => 1, 100 | ]; 101 | 102 | $oldPasswordLock = new PasswordLock(self::DUMMY_PEPPER, $options); 103 | $newPasswordLock = new PasswordLock(self::DUMMY_PEPPER, $options); 104 | 105 | $ciphertext = $oldPasswordLock->hash(self::DUMMY_PASSWORD); 106 | 107 | $needsRehash = $newPasswordLock->needsRehash($ciphertext); 108 | 109 | $this->assertFalse($needsRehash); 110 | } 111 | 112 | /** @test */ 113 | public function bcrypt_hash_needs_rehash() 114 | { 115 | $this->assertNeedsRehash(self::BCRYPT_HASH); 116 | } 117 | 118 | /** @test */ 119 | public function md5_hash_needs_rehash() 120 | { 121 | $this->assertNeedsRehash(self::MD5_HASH); 122 | } 123 | 124 | /** @test */ 125 | public function phpass_hash_needs_rehash() 126 | { 127 | $this->assertNeedsRehash(self::PHPASS_HASH); 128 | } 129 | 130 | private function assertNeedsRehash(string $ciphertext) 131 | { 132 | $passwordLock = new PasswordLock(self::DUMMY_PEPPER, []); 133 | 134 | $needsRehash = $passwordLock->needsRehash($ciphertext); 135 | 136 | $this->assertTrue($needsRehash); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /tests/wpunit/PhpassValidatorTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(ValidatorInterface::class, $validator); 20 | } 21 | 22 | /** @test */ 23 | public function it_checks_correct_password() 24 | { 25 | $validator = new PhpassValidator(); 26 | 27 | $isValid = $validator->isValid(self::DUMMY_PASSWORD, self::DUMMY_CIPHERTEXT); 28 | 29 | $this->assertTrue($isValid); 30 | } 31 | 32 | /** @test */ 33 | public function it_checks_incorrect_password() 34 | { 35 | $validator = new PhpassValidator(); 36 | 37 | $isValid = $validator->isValid('incorrect password', self::DUMMY_CIPHERTEXT); 38 | 39 | $this->assertFalse($isValid); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/wpunit/ValidatorTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(ValidatorInterface::class, $validator); 21 | } 22 | 23 | /** @test */ 24 | public function it_checks_correct_password() 25 | { 26 | $validator = new Validator(self::DUMMY_PEPPER); 27 | 28 | $isValid = $validator->isValid(self::DUMMY_PASSWORD, self::DUMMY_CIPHERTEXT); 29 | 30 | $this->assertTrue($isValid); 31 | } 32 | 33 | /** @test */ 34 | public function it_checks_incorrect_password() 35 | { 36 | $validator = new Validator(self::DUMMY_PEPPER); 37 | 38 | $isValid = $validator->isValid('incorrect password', self::DUMMY_CIPHERTEXT); 39 | 40 | $this->assertFalse($isValid); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/wpunit/WPCheckPasswordTest.php: -------------------------------------------------------------------------------- 1 | assertCorrectPassword('argon2user', self::DUMMY_PASSWORD, self::ARGON_TWO_HASH); 26 | } 27 | 28 | /** @test */ 29 | public function it_checks_incorrect_argon2_hash() 30 | { 31 | $this->assertIncorrectPassword('argon2user', 'incorrectPassword', self::ARGON_TWO_HASH); 32 | } 33 | 34 | /** @test */ 35 | public function it_does_not_rehash_argon2_hash() 36 | { 37 | $this->assertRehashToArgon2i('argon2user', self::DUMMY_PASSWORD, self::ARGON_TWO_HASH); 38 | $user = get_user_by('login', 'argon2user'); 39 | $this->assertSame( 40 | self::ARGON_TWO_HASH, 41 | $user->user_pass 42 | ); 43 | } 44 | 45 | /** @test */ 46 | public function it_checks_correct_argon2_outdated_options_hash() 47 | { 48 | $this->assertCorrectPassword('argon2_outdated_options_user', self::DUMMY_PASSWORD, self::ARGON_TWO_OUTDATED_OPTIONS_HASH); 49 | } 50 | 51 | /** @test */ 52 | public function it_checks_incorrect_argon2_outdated_options_hash() 53 | { 54 | $this->assertIncorrectPassword('argon2_outdated_options_user', 'incorrectPassword', self::ARGON_TWO_OUTDATED_OPTIONS_HASH); 55 | } 56 | 57 | /** @test */ 58 | public function it_rehash_argon2_outdated_options_hash() 59 | { 60 | $this->assertRehashToArgon2i('argon2_outdated_options_user', self::DUMMY_PASSWORD, self::ARGON_TWO_OUTDATED_OPTIONS_HASH); 61 | $user = get_user_by('login', 'argon2_outdated_options_user'); 62 | $this->assertNotSame( 63 | self::ARGON_TWO_OUTDATED_OPTIONS_HASH, 64 | $user->user_pass 65 | ); 66 | } 67 | 68 | /** @test */ 69 | public function it_checks_correct_argon2_fallback_pepper_hash() 70 | { 71 | $this->assertCorrectPassword('argon2fallbackpepperuser', self::DUMMY_PASSWORD, self::ARGON_TWO_FALLBACK_PEPPER_HASH); 72 | } 73 | 74 | /** @test */ 75 | public function it_checks_incorrect_argon2_fallback_pepper_hash() 76 | { 77 | $this->assertIncorrectPassword('argon2fallbackpepperuser', 'incorrectPassword', self::ARGON_TWO_FALLBACK_PEPPER_HASH); 78 | } 79 | 80 | /** @test */ 81 | public function it_rehash_argon2_fallback_pepper_hash() 82 | { 83 | $this->assertRehashToArgon2i('argon2fallbackpepperuser', self::DUMMY_PASSWORD, self::ARGON_TWO_FALLBACK_PEPPER_HASH); 84 | $user = get_user_by('login', 'argon2fallbackpepperuser'); 85 | $this->assertNotSame( 86 | self::ARGON_TWO_FALLBACK_PEPPER_HASH, 87 | $user->user_pass 88 | ); 89 | } 90 | 91 | /** @test */ 92 | public function it_checks_correct_bcrypt_hash() 93 | { 94 | $this->assertCorrectPassword('bcrypt_user', self::DUMMY_PASSWORD, self::BCRYPT_HASH); 95 | } 96 | 97 | /** @test */ 98 | public function it_checks_incorrect_bcrypt_hash() 99 | { 100 | $this->assertIncorrectPassword('bcrypt_user', 'incorrectPassword', self::BCRYPT_HASH); 101 | } 102 | 103 | /** @test */ 104 | public function it_rehash_bcrypt_hash() 105 | { 106 | $this->assertRehashToArgon2i('bcrypt_user', self::DUMMY_PASSWORD, self::BCRYPT_HASH); 107 | } 108 | 109 | /** @test */ 110 | public function it_checks_correct_md5_hash() 111 | { 112 | $this->assertCorrectPassword('md5_user', self::DUMMY_PASSWORD, self::MD5_HASH); 113 | } 114 | 115 | /** @test */ 116 | public function it_checks_incorrect_md5_hash() 117 | { 118 | $this->assertIncorrectPassword('md5_user', 'incorrectPassword', self::MD5_HASH); 119 | } 120 | 121 | /** @test */ 122 | public function it_rehash_md5_hash() 123 | { 124 | $this->assertRehashToArgon2i('md5_user', self::DUMMY_PASSWORD, self::MD5_HASH); 125 | } 126 | 127 | /** @test */ 128 | public function it_checks_correct_phpass_hash() 129 | { 130 | $this->assertCorrectPassword('phpass_user', self::DUMMY_PASSWORD, self::PHPASS_HASH); 131 | } 132 | 133 | /** @test */ 134 | public function it_checks_incorrect_phpass_hash() 135 | { 136 | $this->assertIncorrectPassword('phpass_user', 'incorrectPassword', self::PHPASS_HASH); 137 | } 138 | 139 | /** @test */ 140 | public function it_rehash_phpass_hash() 141 | { 142 | $this->assertRehashToArgon2i('phpass_user', self::DUMMY_PASSWORD, self::PHPASS_HASH); 143 | } 144 | 145 | private function assertCorrectPassword(string $login, string $password, string $ciphertext) 146 | { 147 | $this->haveUserInDatabase($login, $ciphertext); 148 | 149 | $isValid = wp_check_password($password, $ciphertext); 150 | 151 | $this->assertTrue($isValid); 152 | } 153 | 154 | private function assertIncorrectPassword(string $login, string $incorrectPassword, string $ciphertext) 155 | { 156 | $this->haveUserInDatabase($login, $ciphertext); 157 | 158 | $isValid = wp_check_password($incorrectPassword, $ciphertext); 159 | 160 | $this->assertFalse($isValid); 161 | } 162 | 163 | private function assertRehashToArgon2i(string $login, string $password, string $ciphertext) 164 | { 165 | $this->haveUserInDatabase($login, $ciphertext); 166 | $user = get_user_by('login', $login); 167 | 168 | wp_check_password($password, $ciphertext, $user->ID); 169 | 170 | $user = get_user_by('login', $login); 171 | $this->assertFalse( 172 | password_needs_rehash($user->user_pass, PASSWORD_ARGON2I, WP_PASSWORD_ARGON_TWO_OPTIONS) 173 | ); 174 | } 175 | 176 | private function haveUserInDatabase(string $login, string $ciphertext) 177 | { 178 | $this->tester->haveOrUpdateInDatabase('wp_users', [ 179 | 'user_login' => $login, 180 | 'user_pass' => $ciphertext, 181 | 'user_nicename' => $login, 182 | 'user_email' => $login . '@wp.dev', 183 | 'user_registered' => '2018-01-01 00:00:00', 184 | 'display_name' => $login, 185 | ]); 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /tests/wpunit/WPHashPasswordTest.php: -------------------------------------------------------------------------------- 1 | assertFalse( 17 | password_needs_rehash($ciphertext, PASSWORD_ARGON2I, WP_PASSWORD_ARGON_TWO_OPTIONS) 18 | ); 19 | } 20 | 21 | /** @test */ 22 | public function its_ciphertext_can_be_checked() 23 | { 24 | $password = 'testing_its_ciphertext_can_be_checked'; 25 | $ciphertext = wp_hash_password($password); 26 | $check = wp_check_password($password, $ciphertext); 27 | 28 | $this->assertTrue($check); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/wpunit/WPSetPasswordTest.php: -------------------------------------------------------------------------------- 1 | assertFalse( 17 | password_needs_rehash($ciphertext, PASSWORD_ARGON2I, WP_PASSWORD_ARGON_TWO_OPTIONS) 18 | ); 19 | } 20 | 21 | /** @test */ 22 | public function it_saves_argon2i_hashed_ciphertext() 23 | { 24 | $userId = wp_create_user( 25 | 'testing_it_saves_argon2i_hashed_ciphertext', 26 | 'old_password', 27 | 'testing_it_saves_argon2i_hashed_ciphertext@exmaple.com' 28 | ); 29 | 30 | wp_set_password('new-password', $userId); 31 | 32 | $user = get_user_by('id', $userId); 33 | 34 | $this->assertFalse( 35 | password_needs_rehash($user->user_pass, PASSWORD_ARGON2I, WP_PASSWORD_ARGON_TWO_OPTIONS) 36 | ); 37 | } 38 | 39 | /** @test */ 40 | public function its_ciphertext_can_be_checked() 41 | { 42 | $userId = wp_create_user( 43 | 'testing_its_ciphertext_can_be_checked', 44 | 'old_password', 45 | 'testing_its_ciphertext_can_be_checked@exmaple.com' 46 | ); 47 | 48 | $password = 'some-password'; 49 | $ciphertext = wp_set_password($password, $userId); 50 | $check = wp_check_password($password, $ciphertext); 51 | 52 | $this->assertTrue($check); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /wp-password-argon-two.php: -------------------------------------------------------------------------------- 1 |