├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src └── APR1_MD5.php └── tests ├── APR1_MD5_CheckTest.php ├── APR1_MD5_HashTest.php └── APR1_MD5_SaltTest.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | - 5.6 8 | - hhvm 9 | 10 | before_script: 11 | - composer self-update 12 | - composer install --prefer-source --no-interaction --dev 13 | 14 | script: phpunit 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jeremy 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apache's APR1 MD5 Hashing Algorithm in PHP 2 | [![Build Status](https://travis-ci.org/whitehat101/apr1-md5.svg)](https://travis-ci.org/whitehat101/apr1-md5) 3 | 4 | There is no way that the best way to generate Apache's apr1-md5 password hashes is from a [7-year-old comment on php.net](http://www.php.net/manual/en/function.crypt.php#73619). Only a n00b would trust a crypto algorithm from a non-security website's forum. Sadly, that is how the PHP community has accessed this algorithm, until now. 5 | 6 | Here is a tested, referenced, documented, and packaged implementation of Apache's APR1 MD5 Hashing Algorithm in pure PHP. 7 | 8 | ## Install 9 | 10 | composer.json: 11 | ```json 12 | { 13 | "require": { 14 | "whitehat101/apr1-md5": "~1.0" 15 | } 16 | } 17 | ``` 18 | 19 | ## Use 20 | 21 | ```php 22 | use WhiteHat101\Crypt\APR1_MD5; 23 | 24 | // Check plaintext password against an APR1-MD5 hash 25 | echo APR1_MD5::check('plaintext', '$apr1$PVWlTz/5$SNkIVyogockgH65nMLn.W1'); 26 | 27 | // Hash a password with a known salt 28 | echo APR1_MD5::hash('PASSWORD', '__SALT__'); 29 | 30 | // Hash a password with a secure random salt 31 | echo APR1_MD5::hash('PASSWORD'); 32 | 33 | // Generate a secure random salt 34 | echo APR1_MD5::salt(); 35 | ``` 36 | 37 | The ideal `__SALT__` is an 8 character string. Valid salts are alphanumeric and `.` or `/`. Shorter salts are allowed. Longer salts are truncated after the 8th character. 38 | 39 | ## Generate Hashes via Other Tools 40 | 41 | ### htpasswd 42 | ```bash 43 | $ htpasswd -nmb apache apache 44 | apache:$apr1$rOioh4Wh$bVD3DRwksETubcpEH90ww0 45 | 46 | $ htpasswd -nmb ChangeMe1 ChangeMe1 47 | ChangeMe1:$apr1$PVWlTz/5$SNkIVyogockgH65nMLn.W1 48 | 49 | $ htpasswd -nmb WhiteHat101 WhiteHat101 50 | WhiteHat101:$apr1$HIcWIbgX$G9YqNkCVGlFAN63bClpoT/ 51 | ``` 52 | 53 | ### openssl 54 | ```bash 55 | $ openssl passwd -apr1 -salt rOioh4Wh apache 56 | $apr1$rOioh4Wh$bVD3DRwksETubcpEH90ww0 57 | 58 | $ openssl passwd -apr1 -salt PVWlTz/5 ChangeMe1 59 | $apr1$PVWlTz/5$SNkIVyogockgH65nMLn.W1 60 | 61 | $ openssl passwd -apr1 -salt HIcWIbgX WhiteHat101 62 | $apr1$HIcWIbgX$G9YqNkCVGlFAN63bClpoT/ 63 | ``` 64 | 65 | ## Testing 66 | 67 | ```bash 68 | composer install 69 | vendor/bin/phpunit 70 | ``` 71 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "whitehat101/apr1-md5", 3 | "description": "Apache's APR1-MD5 algorithm in pure PHP", 4 | "homepage": "https://github.com/whitehat101/apr1-md5", 5 | "license": "MIT", 6 | "keywords": ["md5","apr1"], 7 | "authors": [ 8 | { 9 | "name": "Jeremy Ebler", 10 | "email": "jebler@gmail.com" 11 | } 12 | ], 13 | 14 | "require": { 15 | "php": ">=5.3.0" 16 | }, 17 | 18 | "require-dev": { 19 | "phpunit/phpunit": "4.0.*" 20 | }, 21 | 22 | "autoload": { 23 | "psr-4": { 24 | "WhiteHat101\\Crypt\\": "src" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/APR1_MD5.php: -------------------------------------------------------------------------------- 1 | 0; $i-=16) 25 | $context .= substr($binary, 0, min(16, $i)); 26 | for($i=$max; $i>0; $i>>=1) 27 | $context .= ($i & 1) ? chr(0) : $mdp[0]; 28 | $binary = pack('H32', md5($context)); 29 | for($i=0; $i<1000; $i++) { 30 | $new = ($i & 1) ? $mdp : $binary; 31 | if($i % 3) $new .= $salt; 32 | if($i % 7) $new .= $mdp; 33 | $new .= ($i & 1) ? $binary : $mdp; 34 | $binary = pack('H32', md5($new)); 35 | } 36 | $hash = ''; 37 | for ($i = 0; $i < 5; $i++) { 38 | $k = $i+6; 39 | $j = $i+12; 40 | if($j == 16) $j = 5; 41 | $hash = $binary[$i].$binary[$k].$binary[$j].$hash; 42 | } 43 | $hash = chr(0).chr(0).$binary[11].$hash; 44 | $hash = strtr( 45 | strrev(substr(base64_encode($hash), 2)), 46 | self::BASE64_ALPHABET, 47 | self::APRMD5_ALPHABET 48 | ); 49 | return '$apr1$'.$salt.'$'.$hash; 50 | } 51 | 52 | // 8 character salts are the best. Don't encourage anything but the best. 53 | public static function salt() { 54 | $alphabet = self::APRMD5_ALPHABET; 55 | $salt = ''; 56 | for($i=0; $i<8; $i++) { 57 | $offset = hexdec(bin2hex(openssl_random_pseudo_bytes(1))) % 64; 58 | $salt .= $alphabet[$offset]; 59 | } 60 | return $salt; 61 | } 62 | 63 | public static function check($plain, $hash) { 64 | $parts = explode('$', $hash); 65 | return self::hash($plain, $parts[2]) === $hash; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /tests/APR1_MD5_CheckTest.php: -------------------------------------------------------------------------------- 1 | assertTrue( 9 | APR1_MD5::check('WhiteHat101','$apr1$HIcWIbgX$G9YqNkCVGlFAN63bClpoT/') 10 | ); 11 | } 12 | 13 | public function testHash_apache() { 14 | $this->assertTrue( 15 | APR1_MD5::check('apache','$apr1$rOioh4Wh$bVD3DRwksETubcpEH90ww0') 16 | ); 17 | } 18 | 19 | public function testHash_ChangeMe1() { 20 | $this->assertTrue( 21 | APR1_MD5::check('ChangeMe1','$apr1$PVWlTz/5$SNkIVyogockgH65nMLn.W1') 22 | ); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /tests/APR1_MD5_HashTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 9 | '$apr1$HIcWIbgX$G9YqNkCVGlFAN63bClpoT/', 10 | APR1_MD5::hash('WhiteHat101','HIcWIbgX') 11 | ); 12 | } 13 | 14 | public function testHash_apache() { 15 | $this->assertEquals( 16 | '$apr1$rOioh4Wh$bVD3DRwksETubcpEH90ww0', 17 | APR1_MD5::hash('apache','rOioh4Wh') 18 | ); 19 | } 20 | 21 | public function testHash_ChangeMe1() { 22 | $this->assertEquals( 23 | '$apr1$PVWlTz/5$SNkIVyogockgH65nMLn.W1', 24 | APR1_MD5::hash('ChangeMe1','PVWlTz/5') 25 | ); 26 | } 27 | 28 | // Test some awkward inputs 29 | 30 | public function testHash_ChangeMe1_blankSalt() { 31 | $this->assertEquals( 32 | '$apr1$$DbHa0iITto8vNFPlkQsBX1', 33 | APR1_MD5::hash('ChangeMe1','') 34 | ); 35 | } 36 | 37 | public function testHash_ChangeMe1_longSalt() { 38 | $this->assertEquals( 39 | '$apr1$PVWlTz/5$SNkIVyogockgH65nMLn.W1', 40 | APR1_MD5::hash('ChangeMe1','PVWlTz/50123456789') 41 | ); 42 | } 43 | 44 | public function testHash_ChangeMe1_nullSalt() { 45 | $hash = APR1_MD5::hash('ChangeMe1'); 46 | $this->assertEquals(37, strlen($hash)); 47 | } 48 | 49 | public function testHash__nullSalt() { 50 | $hash = APR1_MD5::hash(''); 51 | $this->assertEquals(37, strlen($hash)); 52 | } 53 | 54 | // a null password gets coerced into the blank string. 55 | // is this sensible? 56 | public function testHash_null_nullSalt() { 57 | $hash = APR1_MD5::hash(null); 58 | $this->assertEquals(37, strlen($hash)); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /tests/APR1_MD5_SaltTest.php: -------------------------------------------------------------------------------- 1 | assertInternalType('string', APR1_MD5::salt()); 9 | } 10 | 11 | public function testSaltPattern() { 12 | $this->assertRegExp('/.{8}/', APR1_MD5::salt()); 13 | } 14 | 15 | public function testSaltRamdomness() { 16 | $this->assertNotEquals(APR1_MD5::salt(), APR1_MD5::salt()); 17 | } 18 | 19 | } 20 | --------------------------------------------------------------------------------