Skip to content

Commit

Permalink
Add OCP\Security\IHasher
Browse files Browse the repository at this point in the history
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
LukasReschke authored and DeepDiver1975 committed Nov 6, 2014
1 parent 1d6c7e2 commit 24ca2d8
Show file tree
Hide file tree
Showing 7 changed files with 342 additions and 1 deletion.
2 changes: 1 addition & 1 deletion 3rdparty
6 changes: 6 additions & 0 deletions config/config.sample.php
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@
*/
'passwordsalt' => '',

/**
* The hashing cost used by hashes generated by ownCloud
* Using a higher value requires more time and CPU power to calculate the hashes
*/
'hashingCost' => 10,

/**
* Your list of trusted domains that users can log into. Specifying trusted
* domains prevents host header poisoning. Do not remove this, as it performs
Expand Down
146 changes: 146 additions & 0 deletions lib/private/security/hasher.php
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;
}

}
13 changes: 13 additions & 0 deletions lib/private/server.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
use OC\Files\Node\Root;
use OC\Files\View;
use OC\Security\Crypto;
use OC\Security\Hasher;
use OC\Security\SecureRandom;
use OC\Diagnostics\NullEventLogger;
use OCP\IServerContainer;
Expand Down Expand Up @@ -197,6 +198,9 @@ function __construct() {
$this->registerService('Crypto', function (Server $c) {
return new Crypto($c->getConfig(), $c->getSecureRandom());
});
$this->registerService('Hasher', function (Server $c) {
return new Hasher($c->getConfig());
});
$this->registerService('DatabaseConnection', function (Server $c) {
$factory = new \OC\DB\ConnectionFactory();
$type = $c->getConfig()->getSystemValue('dbtype', 'sqlite');
Expand Down Expand Up @@ -529,6 +533,15 @@ function getCrypto() {
return $this->query('Crypto');
}

/**
* Returns a Hasher instance
*
* @return \OCP\Security\IHasher
*/
function getHasher() {
return $this->query('Hasher');
}

/**
* Returns an instance of the db facade
*
Expand Down
13 changes: 13 additions & 0 deletions lib/public/iservercontainer.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,19 @@ function getNavigationManager();
*/
function getConfig();

/**
* Returns a Crypto instance
*
* @return \OCP\Security\ICrypto
*/
function getCrypto();

/**
* Returns a Hasher instance
*
* @return \OCP\Security\IHasher
*/
function getHasher();

/**
* Returns an instance of the db facade
Expand Down
48 changes: 48 additions & 0 deletions lib/public/security/ihasher.php
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);
}
115 changes: 115 additions & 0 deletions tests/lib/security/hasher.php
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);
}

}

0 comments on commit 24ca2d8

Please sign in to comment.