├── .styleci.yml ├── LICENSE.md ├── README.md ├── composer.json └── src ├── LaravelWpHashDriverServiceProvider.php ├── PasswordHash.php └── WordpressHasher.php /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | 3 | disabled: 4 | - single_class_element_per_statement 5 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Ammar Al-Khawaldeh 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Wordpress Hash Driver 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/ammardev/laravel-wp-hash-driver.svg?style=flat-square)](https://packagist.org/packages/ammardev/laravel-wp-hash-driver) 4 | 5 | [![Total Downloads](https://img.shields.io/packagist/dt/ammardev/laravel-wp-hash-driver.svg?style=flat-square)](https://packagist.org/packages/ammardev/laravel-wp-hash-driver) 6 | 7 | A package that supports Wordpress hashing in Laravel applications. Useful when migrating Wordpress users to a Laravel project. 8 | 9 | ## Installation 10 | 11 | You can install the package via composer: 12 | 13 | ```bash 14 | composer require ammardev/laravel-wp-hash-driver 15 | ``` 16 | 17 | ## Usage 18 | 19 | You can use make and check methods in Laravel's Hash facade by choosing wordpress driver as the following: 20 | 21 | ``` php 22 | // Hash a password: 23 | $hashed = Hash::driver('wp')->make('my-password'); 24 | 25 | // Check a password: 26 | Hash::driver('wp')->check('my-password', $hashed); 27 | ``` 28 | 29 | Or you can set Wordpress hashers as the default driver in `config/hashing.php`: 30 | ```php 31 | return [ 32 | // ... 33 | 34 | 'driver' => 'wp', 35 | 36 | // ... 37 | ]; 38 | ``` 39 | And then you can use `make` and `check` methods directly without choosing the driver using `driver` method. 40 | 41 | 42 | ### Testing 43 | 44 | ``` bash 45 | composer test 46 | ``` 47 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ammardev/laravel-wp-hash-driver", 3 | "description": "Supports Wordpress passwords hashing and checking in Laravel's Hash facade.", 4 | "keywords": [ 5 | "wordpress", 6 | "passwords", 7 | "hash", 8 | "hashing", 9 | "laravel" 10 | ], 11 | "homepage": "https://github.com/ammardev/laravel-wp-hash-driver", 12 | "license": "MIT", 13 | "type": "library", 14 | "authors": [ 15 | { 16 | "name": "Ammar Al-Khawaldeh", 17 | "email": "me@ammar.dev", 18 | "homepage": "https://ammar.dev", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php": "^7.2|^8.0", 24 | "illuminate/hashing": "^6.0|^7.0|^8.0|^9.0", 25 | "illuminate/support": "^6.0|^7.0|^8.0|^9.0" 26 | }, 27 | "require-dev": { 28 | "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0", 29 | "phpunit/phpunit": "^8.0" 30 | }, 31 | "autoload": { 32 | "psr-4": { 33 | "Ammardev\\LaravelWpHashDriver\\": "src" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "Ammardev\\LaravelWpHashDriver\\Tests\\": "tests" 39 | } 40 | }, 41 | "scripts": { 42 | "test": "vendor/bin/phpunit", 43 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 44 | 45 | }, 46 | "config": { 47 | "sort-packages": true 48 | }, 49 | "extra": { 50 | "laravel": { 51 | "providers": [ 52 | "Ammardev\\LaravelWpHashDriver\\LaravelWpHashDriverServiceProvider" 53 | ] 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/LaravelWpHashDriverServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->make('hash')->extend('wp', function() { 12 | return new WordpressHasher; 13 | }); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/PasswordHash.php: -------------------------------------------------------------------------------- 1 | in 2004-2006 and placed in 11 | # the public domain. Revised in subsequent years, still public domain. 12 | # 13 | # There's absolutely no warranty. 14 | # 15 | # The homepage URL for this framework is: 16 | # 17 | # http://www.openwall.com/phpass/ 18 | # 19 | # Please be sure to update the Version line if you edit this file in any way. 20 | # It is suggested that you leave the main version number intact, but indicate 21 | # your project name (after the slash) and add your own revision information. 22 | # 23 | # Please do not change the "private" password hashing method implemented in 24 | # here, thereby making your hashes incompatible. However, if you must, please 25 | # change the hash type identifier (the "$P$") to something different. 26 | # 27 | # Obviously, since this code is in the public domain, the above are not 28 | # requirements (there can be none), but merely suggestions. 29 | # 30 | class PasswordHash { 31 | var $itoa64; 32 | var $iteration_count_log2; 33 | var $portable_hashes; 34 | var $random_state; 35 | 36 | function __construct($iteration_count_log2, $portable_hashes) 37 | { 38 | $this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; 39 | 40 | if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31) 41 | $iteration_count_log2 = 8; 42 | $this->iteration_count_log2 = $iteration_count_log2; 43 | 44 | $this->portable_hashes = $portable_hashes; 45 | 46 | $this->random_state = microtime(); 47 | if (function_exists('getmypid')) 48 | $this->random_state .= getmypid(); 49 | } 50 | 51 | function PasswordHash($iteration_count_log2, $portable_hashes) 52 | { 53 | self::__construct($iteration_count_log2, $portable_hashes); 54 | } 55 | 56 | function get_random_bytes($count) 57 | { 58 | $output = ''; 59 | if (@is_readable('/dev/urandom') && 60 | ($fh = @fopen('/dev/urandom', 'rb'))) { 61 | $output = fread($fh, $count); 62 | fclose($fh); 63 | } 64 | 65 | if (strlen($output) < $count) { 66 | $output = ''; 67 | for ($i = 0; $i < $count; $i += 16) { 68 | $this->random_state = 69 | md5(microtime() . $this->random_state); 70 | $output .= md5($this->random_state, TRUE); 71 | } 72 | $output = substr($output, 0, $count); 73 | } 74 | 75 | return $output; 76 | } 77 | 78 | function encode64($input, $count) 79 | { 80 | $output = ''; 81 | $i = 0; 82 | do { 83 | $value = ord($input[$i++]); 84 | $output .= $this->itoa64[$value & 0x3f]; 85 | if ($i < $count) 86 | $value |= ord($input[$i]) << 8; 87 | $output .= $this->itoa64[($value >> 6) & 0x3f]; 88 | if ($i++ >= $count) 89 | break; 90 | if ($i < $count) 91 | $value |= ord($input[$i]) << 16; 92 | $output .= $this->itoa64[($value >> 12) & 0x3f]; 93 | if ($i++ >= $count) 94 | break; 95 | $output .= $this->itoa64[($value >> 18) & 0x3f]; 96 | } while ($i < $count); 97 | 98 | return $output; 99 | } 100 | 101 | function gensalt_private($input) 102 | { 103 | $output = '$P$'; 104 | $output .= $this->itoa64[min($this->iteration_count_log2 + 5, 105 | 30)]; 106 | $output .= $this->encode64($input, 6); 107 | 108 | return $output; 109 | } 110 | 111 | function crypt_private($password, $setting) 112 | { 113 | $output = '*0'; 114 | if (substr($setting, 0, 2) === $output) 115 | $output = '*1'; 116 | 117 | $id = substr($setting, 0, 3); 118 | # We use "$P$", phpBB3 uses "$H$" for the same thing 119 | if ($id !== '$P$' && $id !== '$H$') 120 | return $output; 121 | 122 | $count_log2 = strpos($this->itoa64, $setting[3]); 123 | if ($count_log2 < 7 || $count_log2 > 30) 124 | return $output; 125 | 126 | $count = 1 << $count_log2; 127 | 128 | $salt = substr($setting, 4, 8); 129 | if (strlen($salt) !== 8) 130 | return $output; 131 | 132 | # We were kind of forced to use MD5 here since it's the only 133 | # cryptographic primitive that was available in all versions 134 | # of PHP in use. To implement our own low-level crypto in PHP 135 | # would have resulted in much worse performance and 136 | # consequently in lower iteration counts and hashes that are 137 | # quicker to crack (by non-PHP code). 138 | $hash = md5($salt . $password, TRUE); 139 | do { 140 | $hash = md5($hash . $password, TRUE); 141 | } while (--$count); 142 | 143 | $output = substr($setting, 0, 12); 144 | $output .= $this->encode64($hash, 16); 145 | 146 | return $output; 147 | } 148 | 149 | function gensalt_blowfish($input) 150 | { 151 | # This one needs to use a different order of characters and a 152 | # different encoding scheme from the one in encode64() above. 153 | # We care because the last character in our encoded string will 154 | # only represent 2 bits. While two known implementations of 155 | # bcrypt will happily accept and correct a salt string which 156 | # has the 4 unused bits set to non-zero, we do not want to take 157 | # chances and we also do not want to waste an additional byte 158 | # of entropy. 159 | $itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 160 | 161 | $output = '$2a$'; 162 | $output .= chr(ord('0') + $this->iteration_count_log2 / 10); 163 | $output .= chr(ord('0') + $this->iteration_count_log2 % 10); 164 | $output .= '$'; 165 | 166 | $i = 0; 167 | do { 168 | $c1 = ord($input[$i++]); 169 | $output .= $itoa64[$c1 >> 2]; 170 | $c1 = ($c1 & 0x03) << 4; 171 | if ($i >= 16) { 172 | $output .= $itoa64[$c1]; 173 | break; 174 | } 175 | 176 | $c2 = ord($input[$i++]); 177 | $c1 |= $c2 >> 4; 178 | $output .= $itoa64[$c1]; 179 | $c1 = ($c2 & 0x0f) << 2; 180 | 181 | $c2 = ord($input[$i++]); 182 | $c1 |= $c2 >> 6; 183 | $output .= $itoa64[$c1]; 184 | $output .= $itoa64[$c2 & 0x3f]; 185 | } while (1); 186 | 187 | return $output; 188 | } 189 | 190 | function HashPassword($password) 191 | { 192 | $random = ''; 193 | 194 | if (CRYPT_BLOWFISH === 1 && !$this->portable_hashes) { 195 | $random = $this->get_random_bytes(16); 196 | $hash = 197 | crypt($password, $this->gensalt_blowfish($random)); 198 | if (strlen($hash) === 60) 199 | return $hash; 200 | } 201 | 202 | if (strlen($random) < 6) 203 | $random = $this->get_random_bytes(6); 204 | $hash = 205 | $this->crypt_private($password, 206 | $this->gensalt_private($random)); 207 | if (strlen($hash) === 34) 208 | return $hash; 209 | 210 | # Returning '*' on error is safe here, but would _not_ be safe 211 | # in a crypt(3)-like function used _both_ for generating new 212 | # hashes and for validating passwords against existing hashes. 213 | return '*'; 214 | } 215 | 216 | function CheckPassword($password, $stored_hash) 217 | { 218 | $hash = $this->crypt_private($password, $stored_hash); 219 | if ($hash[0] === '*') 220 | $hash = crypt($password, $stored_hash); 221 | 222 | # This is not constant-time. In order to keep the code simple, 223 | # for timing safety we currently rely on the salts being 224 | # unpredictable, which they are at least in the non-fallback 225 | # cases (that is, when we use /dev/urandom and bcrypt). 226 | return $hash === $stored_hash; 227 | } 228 | } 229 | 230 | ?> 231 | -------------------------------------------------------------------------------- /src/WordpressHasher.php: -------------------------------------------------------------------------------- 1 | hasher = new PasswordHash(8, true); 14 | } 15 | 16 | /** 17 | * Get information about the given hashed value. 18 | * 19 | * @param string $hashedValue 20 | * @return array 21 | */ 22 | public function info($hashedValue) 23 | { 24 | return []; 25 | } 26 | 27 | /** 28 | * Hash the given value. 29 | * 30 | * @param string $value 31 | * @param array $options 32 | * @return string 33 | */ 34 | public function make($value, array $options = []) 35 | { 36 | return $this->hasher->HashPassword($value); 37 | } 38 | 39 | /** 40 | * Check the given plain value against a hash. 41 | * 42 | * @param string $value 43 | * @param string $hashedValue 44 | * @param array $options 45 | * @return bool 46 | */ 47 | public function check($value, $hashedValue, array $options = []) 48 | { 49 | if (strlen($hashedValue) <= 32) { 50 | return hash_equals($hashedValue, md5($value)); 51 | } 52 | return $this->hasher->CheckPassword($value, $hashedValue); 53 | } 54 | 55 | /** 56 | * Check if the given hash has been hashed using the given options. 57 | * 58 | * @param string $hashedValue 59 | * @param array $options 60 | * @return bool 61 | */ 62 | public function needsRehash($hashedValue, array $options = []) 63 | { 64 | return false; 65 | } 66 | } 67 | 68 | --------------------------------------------------------------------------------