diff --git a/_test/tests/inc/PassHash.test.php b/_test/tests/inc/PassHash.test.php index 1d34aa696804b8100dbdf5e1b38489c993839eb7..1bc4b95bceb7ea31dc3168e74b13fadb7eb57d95 100644 --- a/_test/tests/inc/PassHash.test.php +++ b/_test/tests/inc/PassHash.test.php @@ -17,6 +17,24 @@ class PassHash_test extends DokuWikiTest { $this->assertEquals('fbdb1d1b18aa6c08324b7d64b71fb76370690e1d', PassHash::hmac('sha1','','')); $this->assertEquals('80070713463e7749b90c2dc24911e275', PassHash::hmac('md5','The quick brown fox jumps over the lazy dog','key')); $this->assertEquals('de7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9', PassHash::hmac('sha1','The quick brown fox jumps over the lazy dog','key')); + } + + function test_djangopbkdf2() { + if(!function_exists('hash_pbkdf2') || !in_array('sha256', hash_algos())){ + $this->markTestSkipped('missing hash functions for djangopbkdf2 password tests'); + return; + } + $ph = new PassHash(); + $knownpasses = array ( + 'pbkdf2_sha256$24000$LakQQ2OOTO1v$dmUgz8V7zcpaoBSA3MV76J5a4rzrszF0NpxGx6HRBbE=', + 'pbkdf2_sha256$24000$PXogIZpE4gaK$F/P/L5SRrbb6taOGEr4w6DhxjMzNAj1jEWTPyAUn8WU=', + 'pbkdf2_sha256$24000$vtn5APnhirmB$/jzJXYvm78X8/FCOMhGUmcCy0iWhtk0L1hcBWN1AYZc=', + 'pbkdf2_sha256$24000$meyCtGKrS5Ai$vkMfMzB/yGFKplmXujgtfl3OGR27AwOQmP+YeRP6lbw=', + 'pbkdf2_sha256$24000$M8ecC8zfqLmJ$l6cIa/Od+m56VMm9hJbdPNhTXZykPVbUGGTPx7/VRE4=', + ); + foreach($knownpasses as $known) { + $this->assertTrue($ph->verify_hash('P4zzW0rd!', $known)); + } } -} \ No newline at end of file +} diff --git a/_test/tests/inc/auth_password.test.php b/_test/tests/inc/auth_password.test.php index 5067e2ca1acf23fb26bf3972b022d91e190632dc..71b0dfb4c4a9e1de2dbb54dcba118ea443665352 100644 --- a/_test/tests/inc/auth_password.test.php +++ b/_test/tests/inc/auth_password.test.php @@ -2,7 +2,7 @@ class auth_password_test extends DokuWikiTest { - // hashes for the password foo$method, using abcdefgh as salt + // hashes for the password foo$method, using abcdefgh12345678912345678912345678 as salt var $passes = array( 'smd5' => '$1$abcdefgh$SYbjm2AEvSoHG7Xapi8so.', 'apr1' => '$apr1$abcdefgh$C/GzYTF4kOVByYLEoD5X4.', @@ -24,14 +24,24 @@ class auth_password_test extends DokuWikiTest { // Check SHA512 only if available in this PHP $this->passes['sha512'] = '$6$abcdefgh12345678$J9.zOcgx0lotwZdcz0uulA3IVQMinZvFZVjA5vapRLVAAqtay23XD4xeeUxQ3B4JvDWYFBIxVWW1tOYlHX13k1'; } + if(function_exists('hash_pbkdf2')) { + if(in_array('sha256', hash_algos())) { + $this->passes['djangopbkdf2_sha256'] = 'pbkdf2_sha256$24000$abcdefgh1234$R23OyZJ0nGHLG6MvPNfEkV5AOz3jUY5zthByPXs2gn0='; + } + if(in_array('sha1', hash_algos())) { + $this->passes['djangopbkdf2_sha1'] = 'pbkdf2_sha1$24000$abcdefgh1234$pOliX4vV1hgOv7lFNURIHHx41HI='; + } + } } function test_cryptPassword(){ foreach($this->passes as $method => $hash){ $info = "testing method $method"; - $this->assertEquals(auth_cryptPassword('foo'.$method, $method,'abcdefgh12345678912345678912345678'), - $hash, $info); + $this->assertEquals( + $hash, + auth_cryptPassword('foo'.$method, $method,'abcdefgh12345678912345678912345678'), + $info); } } diff --git a/inc/PassHash.class.php b/inc/PassHash.class.php index 0701c4116fa7756d0e4c2f8d4124af55eb53ce65..bcfea1dfc35d61af784d290a56ac901bc7312103 100644 --- a/inc/PassHash.class.php +++ b/inc/PassHash.class.php @@ -42,8 +42,15 @@ class PassHash { $magic = 'P'; } elseif(preg_match('/^\$H\$(.{31})$/', $hash, $m)) { $method = 'pmd5'; - $salt = $m[1]; - $magic = 'H'; + $salt = $m[1]; + $magic = 'H'; + } elseif(preg_match('/^pbkdf2_(\w+?)\$(\d+)\$(.{12})\$/', $hash, $m)) { + $method = 'djangopbkdf2'; + $magic = array( + 'algo' => $m[1], + 'iter' => $m[2], + ); + $salt = $m[3]; } elseif(preg_match('/^sha1\$(.{5})\$/', $hash, $m)) { $method = 'djangosha1'; $salt = $m[1]; @@ -83,7 +90,8 @@ class PassHash { //crypt and compare $call = 'hash_'.$method; - if($this->$call($clear, $salt, $magic) === $hash) { + $newhash = $this->$call($clear, $salt, $magic); + if($newhash === $hash) { return true; } return false; @@ -435,6 +443,69 @@ class PassHash { return 'md5$'.$salt.'$'.md5($salt.$clear); } + /** + * Password hashing method 'djangopbkdf2' + * + * An algorithm and iteration count should be given in the opts array. + * Defaults to sha256 and 24000 iterations + * + * @param string $clear The clear text to hash + * @param string $salt The salt to use, null for random + * @param array $opts ('algo' => hash algorithm, 'iter' => iterations) + * @return string Hashed password + * @throws Exception when PHP is missing support for the method/algo + */ + public function hash_djangopbkdf2($clear, $salt=null, $opts=array()) { + $this->init_salt($salt, 12); + if(empty($opts['algo'])) { + $algo = 'sha256'; + } else { + $algo = $opts['algo']; + } + if(empty($opts['iter'])) { + $iter = 24000; + } else { + $iter = (int) $opts['iter']; + } + if(!function_exists('hash_pbkdf2')) { + throw new Exception('This PHP installation has no PBKDF2 support'); + } + if(!in_array($algo, hash_algos())) { + throw new Exception("This PHP installation has no $algo support"); + } + + $hash = base64_encode(hash_pbkdf2($algo, $clear, $salt, $iter, 0, true)); + return "pbkdf2_$algo\$$iter\$$salt\$$hash"; + } + + /** + * Alias for djangopbkdf2 defaulting to sha256 as hash algorithm + * + * @param string $clear The clear text to hash + * @param string $salt The salt to use, null for random + * @param array $opts ('iter' => iterations) + * @return string Hashed password + * @throws Exception when PHP is missing support for the method/algo + */ + public function hash_djangopbkdf2_sha256($clear, $salt=null, $opts=array()) { + $opts['algo'] = 'sha256'; + return $this->hash_djangopbkdf2($clear, $salt, $opts); + } + + /** + * Alias for djangopbkdf2 defaulting to sha1 as hash algorithm + * + * @param string $clear The clear text to hash + * @param string $salt The salt to use, null for random + * @param array $opts ('iter' => iterations) + * @return string Hashed password + * @throws Exception when PHP is missing support for the method/algo + */ + public function hash_djangopbkdf2_sha1($clear, $salt=null, $opts=array()) { + $opts['algo'] = 'sha1'; + return $this->hash_djangopbkdf2($clear, $salt, $opts); + } + /** * Passwordhashing method 'bcrypt' * diff --git a/lib/plugins/config/settings/config.metadata.php b/lib/plugins/config/settings/config.metadata.php index c362a9f1af144e5a7104a29dda7f277f4e747e5d..5323e71f61a770d62c1ca8952fef30bdfa635d64 100644 --- a/lib/plugins/config/settings/config.metadata.php +++ b/lib/plugins/config/settings/config.metadata.php @@ -137,7 +137,10 @@ $meta['_authentication'] = array('fieldset'); $meta['useacl'] = array('onoff','_caution' => 'danger'); $meta['autopasswd'] = array('onoff'); $meta['authtype'] = array('authtype','_caution' => 'danger'); -$meta['passcrypt'] = array('multichoice','_choices' => array('smd5','md5','apr1','sha1','ssha','lsmd5','crypt','mysql','my411','kmd5','pmd5','hmd5','mediawiki','bcrypt','djangomd5','djangosha1','sha512')); +$meta['passcrypt'] = array('multichoice','_choices' => array( + 'smd5','md5','apr1','sha1','ssha','lsmd5','crypt','mysql','my411','kmd5','pmd5','hmd5', + 'mediawiki','bcrypt','djangomd5','djangosha1','djangopbkdf2_sha1','djangopbkdf2_sha256','sha512' +)); $meta['defaultgroup']= array('string'); $meta['superuser'] = array('string','_caution' => 'danger'); $meta['manager'] = array('string');