-
Notifications
You must be signed in to change notification settings - Fork 2.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Public interface for hashing which also works with legacy ownCloud hashes and supports updating the legacy hash via a passed reference. Follow-up of #10219 (comment) Requires owncloud-archive/3rdparty#136
- Loading branch information
1 parent
1d6c7e2
commit 24ca2d8
Showing
7 changed files
with
342 additions
and
1 deletion.
There are no files selected for viewing
Submodule 3rdparty
updated
from cb394f to 48fdf1
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
<?php | ||
/** | ||
* Copyright (c) 2014 Lukas Reschke <lukas@owncloud.com> | ||
* This file is licensed under the Affero General Public License version 3 or | ||
* later. | ||
* See the COPYING-README file. | ||
*/ | ||
|
||
namespace OC\Security; | ||
|
||
use OCP\IConfig; | ||
use OCP\Security\IHasher; | ||
|
||
/** | ||
* Class Hasher provides some basic hashing functions. Furthermore, it supports legacy hashes | ||
* used by previous versions of ownCloud and helps migrating those hashes to newer ones. | ||
* | ||
* The hashes generated by this class are prefixed (version|hash) with a version parameter to allow possible | ||
* updates in the future. | ||
* Possible versions: | ||
* - 1 (Initial version) | ||
* | ||
* Usage: | ||
* // Hashing a message | ||
* $hash = \OC::$server->getHasher()->hash('MessageToHash'); | ||
* // Verifying a message - $newHash will contain the newly calculated hash | ||
* $newHash = null; | ||
* var_dump(\OC::$server->getHasher()->verify('a', '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8', $newHash)); | ||
* var_dump($newHash); | ||
* | ||
* @package OC\Security | ||
*/ | ||
class Hasher implements IHasher { | ||
/** @var IConfig */ | ||
private $config; | ||
/** @var array Options passed to password_hash and password_needs_rehash */ | ||
private $options = array(); | ||
/** @var string Salt used for legacy passwords */ | ||
private $legacySalt = null; | ||
/** @var int Current version of the generated hash */ | ||
private $currentVersion = 1; | ||
|
||
/** | ||
* @param IConfig $config | ||
*/ | ||
function __construct(IConfig $config) { | ||
$this->config = $config; | ||
|
||
$hashingCost = $this->config->getSystemValue('hashingCost', null); | ||
if(!is_null($hashingCost)) { | ||
$this->options['cost'] = $hashingCost; | ||
} | ||
} | ||
|
||
/** | ||
* Hashes a message using PHP's `password_hash` functionality. | ||
* Please note that the size of the returned string is not guaranteed | ||
* and can be up to 255 characters. | ||
* | ||
* @param string $message Message to generate hash from | ||
* @return string Hash of the message with appended version parameter | ||
*/ | ||
public function hash($message) { | ||
return $this->currentVersion . '|' . password_hash($message, PASSWORD_DEFAULT, $this->options); | ||
} | ||
|
||
/** | ||
* Get the version and hash from a prefixedHash | ||
* @param string $prefixedHash | ||
* @return null|array Null if the hash is not prefixed, otherwise array('version' => 1, 'hash' => 'foo') | ||
*/ | ||
protected function splitHash($prefixedHash) { | ||
$explodedString = explode('|', $prefixedHash, 2); | ||
if(sizeof($explodedString) === 2) { | ||
if((int)$explodedString[0] > 0) { | ||
return array('version' => (int)$explodedString[0], 'hash' => $explodedString[1]); | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
|
||
/** | ||
* Verify legacy hashes | ||
* @param string $message Message to verify | ||
* @param string $hash Assumed hash of the message | ||
* @param null|string &$newHash Reference will contain the updated hash | ||
* @return bool Whether $hash is a valid hash of $message | ||
*/ | ||
protected function legacyHashVerify($message, $hash, &$newHash = null) { | ||
if(empty($this->legacySalt)) { | ||
$this->legacySalt = $this->config->getSystemValue('passwordsalt', ''); | ||
} | ||
|
||
// Verify whether it matches a legacy PHPass or SHA1 string | ||
$hashLength = strlen($hash); | ||
if($hashLength === 60 && password_verify($message.$this->legacySalt, $hash) || | ||
$hashLength === 40 && StringUtils::equals($hash, sha1($message))) { | ||
$newHash = $this->hash($message); | ||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
/** | ||
* Verify V1 hashes | ||
* @param string $message Message to verify | ||
* @param string $hash Assumed hash of the message | ||
* @param null|string &$newHash Reference will contain the updated hash if necessary. Update the existing hash with this one. | ||
* @return bool Whether $hash is a valid hash of $message | ||
*/ | ||
protected function verifyHashV1($message, $hash, &$newHash = null) { | ||
if(password_verify($message, $hash)) { | ||
if(password_needs_rehash($hash, PASSWORD_DEFAULT, $this->options)) { | ||
$newHash = $this->hash($message); | ||
} | ||
return true; | ||
} | ||
|
||
return false; | ||
} | ||
|
||
/** | ||
* @param string $message Message to verify | ||
* @param string $hash Assumed hash of the message | ||
* @param null|string &$newHash Reference will contain the updated hash if necessary. Update the existing hash with this one. | ||
* @return bool Whether $hash is a valid hash of $message | ||
*/ | ||
public function verify($message, $hash, &$newHash = null) { | ||
$splittedHash = $this->splitHash($hash); | ||
|
||
if(isset($splittedHash['version'])) { | ||
switch ($splittedHash['version']) { | ||
case 1: | ||
return $this->verifyHashV1($message, $splittedHash['hash'], $newHash); | ||
} | ||
} else { | ||
return $this->legacyHashVerify($message, $hash, $newHash); | ||
} | ||
|
||
|
||
return false; | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
<?php | ||
/** | ||
* Copyright (c) 2014 Lukas Reschke <lukas@owncloud.com> | ||
* This file is licensed under the Affero General Public License version 3 or | ||
* later. | ||
* See the COPYING-README file. | ||
*/ | ||
|
||
namespace OCP\Security; | ||
|
||
/** | ||
* Class Hasher provides some basic hashing functions. Furthermore, it supports legacy hashes | ||
* used by previous versions of ownCloud and helps migrating those hashes to newer ones. | ||
* | ||
* The hashes generated by this class are prefixed (version|hash) with a version parameter to allow possible | ||
* updates in the future. | ||
* Possible versions: | ||
* - 1 (Initial version) | ||
* | ||
* Usage: | ||
* // Hashing a message | ||
* $hash = \OC::$server->getHasher()->hash('MessageToHash'); | ||
* // Verifying a message - $newHash will contain the newly calculated hash | ||
* $newHash = null; | ||
* var_dump(\OC::$server->getHasher()->verify('a', '86f7e437faa5a7fce15d1ddcb9eaeaea377667b8', $newHash)); | ||
* var_dump($newHash); | ||
* | ||
* @package OCP\Security | ||
*/ | ||
interface IHasher { | ||
/** | ||
* Hashes a message using PHP's `password_hash` functionality. | ||
* Please note that the size of the returned string is not guaranteed | ||
* and can be up to 255 characters. | ||
* | ||
* @param string $message Message to generate hash from | ||
* @return string Hash of the message with appended version parameter | ||
*/ | ||
public function hash($message); | ||
|
||
/** | ||
* @param string $message Message to verify | ||
* @param string $hash Assumed hash of the message | ||
* @param null|string &$newHash Reference will contain the updated hash if necessary. Update the existing hash with this one. | ||
* @return bool Whether $hash is a valid hash of $message | ||
*/ | ||
public function verify($message, $hash, &$newHash = null); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
<?php | ||
/** | ||
* Copyright (c) 2014 Lukas Reschke <lukas@owncloud.com> | ||
* This file is licensed under the Affero General Public License version 3 or | ||
* later. | ||
* See the COPYING-README file. | ||
*/ | ||
|
||
use OC\Security\Hasher; | ||
|
||
/** | ||
* Class HasherTest | ||
*/ | ||
class HasherTest extends \PHPUnit_Framework_TestCase { | ||
|
||
/** | ||
* @return array | ||
*/ | ||
public function versionHashProvider() | ||
{ | ||
return array( | ||
array('asf32äà$$a.|3', null), | ||
array('asf32äà$$a.|3|5', null), | ||
array('1|2|3|4', array('version' => 1, 'hash' => '2|3|4')), | ||
array('1|我看|这本书。 我看這本書', array('version' => 1, 'hash' => '我看|这本书。 我看這本書')) | ||
); | ||
} | ||
|
||
/** | ||
* @return array | ||
*/ | ||
public function allHashProviders() | ||
{ | ||
return array( | ||
// Bogus values | ||
array(null, 'asf32äà$$a.|3', false), | ||
array(null, false, false), | ||
|
||
// Valid SHA1 strings | ||
array('password', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', true), | ||
array('owncloud.com', '27a4643e43046c3569e33b68c1a4b15d31306d29', true), | ||
|
||
// Invalid SHA1 strings | ||
array('InvalidString', '5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8', false), | ||
array('AnotherInvalidOne', '27a4643e43046c3569e33b68c1a4b15d31306d29', false), | ||
|
||
// Valid legacy password string with password salt "6Wow67q1wZQZpUUeI6G2LsWUu4XKx" | ||
array('password', '$2a$08$emCpDEl.V.QwPWt5gPrqrOhdpH6ailBmkj2Hd2vD5U8qIy20HBe7.', true), | ||
array('password', '$2a$08$yjaLO4ev70SaOsWZ9gRS3eRSEpHVsmSWTdTms1949mylxJ279hzo2', true), | ||
array('password', '$2a$08$.jNRG/oB4r7gHJhAyb.mDupNUAqTnBIW/tWBqFobaYflKXiFeG0A6', true), | ||
array('owncloud.com', '$2a$08$YbEsyASX/hXVNMv8hXQo7ezreN17T8Jl6PjecGZvpX.Ayz2aUyaZ2', true), | ||
array('owncloud.com', '$2a$11$cHdDA2IkUP28oNGBwlL7jO/U3dpr8/0LIjTZmE8dMPA7OCUQsSTqS', true), | ||
array('owncloud.com', '$2a$08$GH.UoIfJ1e.qeZ85KPqzQe6NR8XWRgJXWIUeE1o/j1xndvyTA1x96', true), | ||
|
||
// Invalid legacy passwords | ||
array('password', '$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false), | ||
|
||
// Valid passwords "6Wow67q1wZQZpUUeI6G2LsWUu4XKx" | ||
array('password', '1|$2a$05$ezAE0dkwk57jlfo6z5Pql.gcIK3ReXT15W7ITNxVS0ksfhO/4E4Kq', true), | ||
array('password', '1|$2a$05$4OQmloFW4yTVez2MEWGIleDO9Z5G9tWBXxn1vddogmKBQq/Mq93pe', true), | ||
array('password', '1|$2a$11$yj0hlp6qR32G9exGEXktB.yW2rgt2maRBbPgi3EyxcDwKrD14x/WO', true), | ||
array('owncloud.com', '1|$2a$10$Yiss2WVOqGakxuuqySv5UeOKpF8d8KmNjuAPcBMiRJGizJXjA2bKm', true), | ||
array('owncloud.com', '1|$2a$10$v9mh8/.mF/Ut9jZ7pRnpkuac3bdFCnc4W/gSumheQUi02Sr.xMjPi', true), | ||
array('owncloud.com', '1|$2a$05$ST5E.rplNRfDCzRpzq69leRzsTGtY7k88h9Vy2eWj0Ug/iA9w5kGK', true), | ||
|
||
// Invalid passwords | ||
array('password', '0|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false), | ||
array('password', '1|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false), | ||
array('password', '2|$2a$08$oKAQY5IhnZocP.61MwP7xu7TNeOb7Ostvk3j6UpacvaNMs.xRj7O2', false), | ||
); | ||
} | ||
|
||
|
||
|
||
/** @var Hasher */ | ||
protected $hasher; | ||
/** @var \OCP\IConfig */ | ||
protected $config; | ||
|
||
protected function setUp() { | ||
$this->config = $this->getMockBuilder('\OCP\IConfig') | ||
->disableOriginalConstructor()->getMock(); | ||
|
||
$this->hasher = new Hasher($this->config); | ||
} | ||
|
||
function testHash() { | ||
$hash = $this->hasher->hash('String To Hash'); | ||
$this->assertNotNull($hash); | ||
} | ||
|
||
/** | ||
* @dataProvider versionHashProvider | ||
*/ | ||
function testSplitHash($hash, $expected) { | ||
$relativePath = \Test_Helper::invokePrivate($this->hasher, 'splitHash', array($hash)); | ||
$this->assertSame($expected, $relativePath); | ||
} | ||
|
||
|
||
/** | ||
* @dataProvider allHashProviders | ||
*/ | ||
function testVerify($password, $hash, $expected) { | ||
$this->config | ||
->expects($this->any()) | ||
->method('getSystemValue') | ||
->with('passwordsalt', null) | ||
->will($this->returnValue('6Wow67q1wZQZpUUeI6G2LsWUu4XKx')); | ||
|
||
$result = $this->hasher->verify($password, $hash); | ||
$this->assertSame($expected, $result); | ||
} | ||
|
||
} |