diff options
author | Lukas Reschke <lukas@owncloud.com> | 2014-11-20 16:02:32 +0100 |
---|---|---|
committer | Lukas Reschke <lukas@owncloud.com> | 2014-11-20 16:05:42 +0100 |
commit | ec853da5ad54af7e6eabb40923784a56330b095a (patch) | |
tree | 923772b3571478dc4d5a7d3a6f4d94c8ab487a51 | |
parent | f64c6c9c9cb48ce2291c5c613e80794e6130a85d (diff) | |
download | nextcloud-server-ec853da5ad54af7e6eabb40923784a56330b095a.tar.gz nextcloud-server-ec853da5ad54af7e6eabb40923784a56330b095a.zip |
Backport \OC\Security\Crypto to ownCloud 7
Conflicts:
lib/repair/repairconfig.php
-rw-r--r-- | config/config.sample.php | 6 | ||||
-rw-r--r-- | lib/private/repair.php | 4 | ||||
-rw-r--r-- | lib/private/security/crypto.php | 130 | ||||
-rw-r--r-- | lib/private/security/stringutils.php | 46 | ||||
-rw-r--r-- | lib/private/server.php | 13 | ||||
-rw-r--r-- | lib/private/setup.php | 1 | ||||
-rw-r--r-- | lib/public/security/icrypto.php | 46 | ||||
-rw-r--r-- | lib/public/security/stringutils.php | 25 | ||||
-rw-r--r-- | lib/repair/repairconfig.php | 11 | ||||
-rw-r--r-- | tests/lib/security/crypto.php | 70 | ||||
-rw-r--r-- | tests/lib/security/stringutils.php | 38 |
11 files changed, 389 insertions, 1 deletions
diff --git a/config/config.sample.php b/config/config.sample.php index 11c7a44b1ec..dd24c71ad20 100644 --- a/config/config.sample.php +++ b/config/config.sample.php @@ -831,4 +831,10 @@ $CONFIG = array( "style-src 'self' 'unsafe-inline'; frame-src *; img-src *; ". "font-src 'self' data:; media-src *", +/** + * Secret used by ownCloud for various purposes, e.g. to encrypt data. If you + * lose this string there will be data corruption. + */ +'secret' => 'ICertainlyShouldHaveChangedTheDefaultSecret', + ); diff --git a/lib/private/repair.php b/lib/private/repair.php index 98bf37f8862..a65915e4988 100644 --- a/lib/private/repair.php +++ b/lib/private/repair.php @@ -10,6 +10,7 @@ namespace OC; use OC\Hooks\BasicEmitter; use OC\Hooks\Emitter; +use OC\Repair\RepairConfig; class Repair extends BasicEmitter { /** @@ -69,7 +70,8 @@ class Repair extends BasicEmitter { */ public static function getRepairSteps() { return array( - new \OC\Repair\RepairMimeTypes() + new \OC\Repair\RepairMimeTypes(), + new RepairConfig(), ); } diff --git a/lib/private/security/crypto.php b/lib/private/security/crypto.php new file mode 100644 index 00000000000..44fe2fc0326 --- /dev/null +++ b/lib/private/security/crypto.php @@ -0,0 +1,130 @@ +<?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 Crypt_AES; +use Crypt_Hash; +use OCP\Security\ICrypto; +use OCP\Security\StringUtils; +use OCP\IConfig; + +/** + * Class Crypto provides a high-level encryption layer using AES-CBC. If no key has been provided + * it will use the secret defined in config.php as key. Additionally the message will be HMAC'd. + * + * Usage: + * $encryptWithDefaultPassword = \OC::$server->getCrypto()->encrypt('EncryptedText'); + * $encryptWithCustompassword = \OC::$server->getCrypto()->encrypt('EncryptedText', 'password'); + * + * @package OC\Security + */ +class Crypto implements ICrypto { + /** @var Crypt_AES $cipher */ + private $cipher; + /** @var int */ + private $ivLength = 16; + /** @var IConfig */ + private $config; + + /** + * @param IConfig $config + */ + function __construct(IConfig $config) { + $this->cipher = new Crypt_AES(); + $this->config = $config; + } + + /** + * Custom implementation of hex2bin since the function is only available starting + * with PHP 5.4 + * + * @TODO Remove this once 5.3 support for ownCloud is dropped + * @param $message + * @return string + */ + protected static function hexToBin($message) { + if (function_exists('hex2bin')) { + return hex2bin($message); + } + + return pack("H*", $message); + } + + /** + * @param string $message The message to authenticate + * @param string $password Password to use (defaults to `secret` in config.php) + * @return string Calculated HMAC + */ + public function calculateHMAC($message, $password = '') { + if($password === '') { + $password = $this->config->getSystemValue('secret'); + } + + // Append an "a" behind the password and hash it to prevent reusing the same password as for encryption + $password = hash('sha512', $password . 'a'); + + $hash = new Crypt_Hash('sha512'); + $hash->setKey($password); + return $hash->hash($message); + } + + /** + * Encrypts a value and adds an HMAC (Encrypt-Then-MAC) + * @param string $plaintext + * @param string $password Password to encrypt, if not specified the secret from config.php will be taken + * @return string Authenticated ciphertext + */ + public function encrypt($plaintext, $password = '') { + if($password === '') { + $password = $this->config->getSystemValue('secret'); + } + $this->cipher->setPassword($password); + + $iv = \OC_Util::generateRandomBytes($this->ivLength); + $this->cipher->setIV($iv); + + $ciphertext = bin2hex($this->cipher->encrypt($plaintext)); + $hmac = bin2hex($this->calculateHMAC($ciphertext.$iv, $password)); + + return $ciphertext.'|'.$iv.'|'.$hmac; + } + + /** + * Decrypts a value and verifies the HMAC (Encrypt-Then-Mac) + * @param string $authenticatedCiphertext + * @param string $password Password to encrypt, if not specified the secret from config.php will be taken + * @return string plaintext + * @throws \Exception If the HMAC does not match + */ + public function decrypt($authenticatedCiphertext, $password = '') { + if($password === '') { + $password = $this->config->getSystemValue('secret'); + } + $this->cipher->setPassword($password); + + $parts = explode('|', $authenticatedCiphertext); + if(sizeof($parts) !== 3) { + throw new \Exception('Authenticated ciphertext could not be decoded.'); + } + + $ciphertext = self::hexToBin($parts[0]); + $iv = $parts[1]; + $hmac = self::hexToBin($parts[2]); + + $this->cipher->setIV($iv); + + if(!StringUtils::equals($this->calculateHMAC($parts[0].$parts[1], $password), $hmac)) { + throw new \Exception('HMAC does not match.'); + } + + return $this->cipher->decrypt($ciphertext); + } + +}
\ No newline at end of file diff --git a/lib/private/security/stringutils.php b/lib/private/security/stringutils.php new file mode 100644 index 00000000000..449883b634f --- /dev/null +++ b/lib/private/security/stringutils.php @@ -0,0 +1,46 @@ +<?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; + +class StringUtils { + + /** + * Compares whether two strings are equal. To prevent guessing of the string + * length this is done by comparing two hashes against each other and afterwards + * a comparison of the real string to prevent against the unlikely chance of + * collisions. + * + * Be aware that this function may leak whether the string to compare have a different + * length. + * + * @param string $expected The expected value + * @param string $input The input to compare against + * @return bool True if the two strings are equal, otherwise false. + */ + public static function equals($expected, $input) { + + if(!is_string($expected) || !is_string($input)) { + return false; + } + + if(function_exists('hash_equals')) { + return hash_equals($expected, $input); + } + + $randomString = \OC_Util::generateRandomBytes(10); + + if(hash('sha512', $expected.$randomString) === hash('sha512', $input.$randomString)) { + if($expected === $input) { + return true; + } + } + + return false; + } +}
\ No newline at end of file diff --git a/lib/private/server.php b/lib/private/server.php index 790edfc2103..07f80311aea 100644 --- a/lib/private/server.php +++ b/lib/private/server.php @@ -10,6 +10,7 @@ use OC\DB\ConnectionWrapper; use OC\Files\Node\Root; use OC\Files\View; use OCP\IServerContainer; +use OC\Security\Crypto; /** * Class Server @@ -199,6 +200,9 @@ class Server extends SimpleContainer implements IServerContainer { $this->registerService('Search', function ($c) { return new Search(); }); + $this->registerService('Crypto', function ($c) { + return new Crypto(\OC::$server->getConfig()); + }); $this->registerService('Db', function ($c) { return new Db(); }); @@ -480,6 +484,15 @@ class Server extends SimpleContainer implements IServerContainer { } /** + * Returns a Crypto instance + * + * @return \OCP\Security\ICrypto + */ + function getCrypto() { + return $this->query('Crypto'); + } + + /** * Returns an instance of the db facade * * @return \OCP\IDb diff --git a/lib/private/setup.php b/lib/private/setup.php index 2d6cede42bc..10322dbc27c 100644 --- a/lib/private/setup.php +++ b/lib/private/setup.php @@ -179,6 +179,7 @@ class OC_Setup { //generate a random salt that is used to salt the local user passwords $salt = OC_Util::generateRandomBytes(30); OC_Config::setValue('passwordsalt', $salt); + OC_Config::setValue('secret', OC_Util::generateRandomBytes(96)); //write the config file OC_Config::setValue('trusted_domains', $trustedDomains); diff --git a/lib/public/security/icrypto.php b/lib/public/security/icrypto.php new file mode 100644 index 00000000000..204935d73ac --- /dev/null +++ b/lib/public/security/icrypto.php @@ -0,0 +1,46 @@ +<?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 Crypto provides a high-level encryption layer using AES-CBC. If no key has been provided + * it will use the secret defined in config.php as key. Additionally the message will be HMAC'd. + * + * Usage: + * $encryptWithDefaultPassword = \OC::$server->getCrypto()->encrypt('EncryptedText'); + * $encryptWithCustomPassword = \OC::$server->getCrypto()->encrypt('EncryptedText', 'password'); + * + * @package OCP\Security + */ +interface ICrypto { + + /** + * @param string $message The message to authenticate + * @param string $password Password to use (defaults to `secret` in config.php) + * @return string Calculated HMAC + */ + public function calculateHMAC($message, $password = ''); + + /** + * Encrypts a value and adds an HMAC (Encrypt-Then-MAC) + * @param string $plaintext + * @param string $password Password to encrypt, if not specified the secret from config.php will be taken + * @return string Authenticated ciphertext + */ + public function encrypt($plaintext, $password = ''); + + /** + * Decrypts a value and verifies the HMAC (Encrypt-Then-Mac) + * @param string $authenticatedCiphertext + * @param string $password Password to encrypt, if not specified the secret from config.php will be taken + * @return string plaintext + * @throws \Exception If the HMAC does not match + */ + public function decrypt($authenticatedCiphertext, $password = ''); +}
\ No newline at end of file diff --git a/lib/public/security/stringutils.php b/lib/public/security/stringutils.php new file mode 100644 index 00000000000..e74efec4fde --- /dev/null +++ b/lib/public/security/stringutils.php @@ -0,0 +1,25 @@ +<?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 StringUtils { + /** + * Compares whether two strings are equal. To prevent guessing of the string + * length this is done by comparing two hashes against each other and afterwards + * a comparison of the real string to prevent against the unlikely chance of + * collisions. + * @param string $expected The expected value + * @param string $input The input to compare against + * @return bool True if the two strings are equal, otherwise false. + */ + public static function equals($expected, $input) { + return \OC\Security\StringUtils::equals($expected, $input); + } +}
\ No newline at end of file diff --git a/lib/repair/repairconfig.php b/lib/repair/repairconfig.php index db119b4a25a..e14294b1074 100644 --- a/lib/repair/repairconfig.php +++ b/lib/repair/repairconfig.php @@ -31,6 +31,7 @@ class RepairConfig extends BasicEmitter implements RepairStep { */ public function run() { $this->removePortsFromTrustedDomains(); + $this->addSecret(); } /** @@ -51,4 +52,14 @@ class RepairConfig extends BasicEmitter implements RepairStep { } \OC::$server->getConfig()->setSystemValue('trusted_domains', $newTrustedDomains); } + + /** + * Adds a secret to config.php + */ + private function addSecret() { + if(\OC::$server->getConfig()->getSystemValue('secret', null) === null) { + $secret = \OC_Util::generateRandomBytes(96); + \OC::$server->getConfig()->setSystemValue('secret', $secret); + } + } } diff --git a/tests/lib/security/crypto.php b/tests/lib/security/crypto.php new file mode 100644 index 00000000000..5c350b876d5 --- /dev/null +++ b/tests/lib/security/crypto.php @@ -0,0 +1,70 @@ +<?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\Crypto; + +class CryptoTest extends \PHPUnit_Framework_TestCase { + + public function defaultEncryptionProvider() + { + return array( + array('Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt.'), + array(''), + array('我看这本书。 我看這本書') + ); + } + + /** @var Crypto */ + protected $crypto; + + protected function setUp() { + $this->crypto = new Crypto(\OC::$server->getConfig()); + } + + /** + * @dataProvider defaultEncryptionProvider + */ + function testDefaultEncrypt($stringToEncrypt) { + $ciphertext = $this->crypto->encrypt($stringToEncrypt); + $this->assertEquals($stringToEncrypt, $this->crypto->decrypt($ciphertext)); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage HMAC does not match. + */ + function testWrongPassword() { + $stringToEncrypt = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt.'; + $ciphertext = $this->crypto->encrypt($stringToEncrypt); + $this->crypto->decrypt($ciphertext, 'A wrong password!'); + } + + function testLaterDecryption() { + $stringToEncrypt = 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt.'; + $encryptedString = '44a35023cca2e7a6125e06c29fc4b2ad9d8a33d0873a8b45b0de4ef9284f260c6c46bf25dc62120644c59b8bafe4281ddc47a70c35ae6c29ef7a63d79eefacc297e60b13042ac582733598d0a6b4de37311556bb5c480fd2633de4e6ebafa868c2d1e2d80a5d24f9660360dba4d6e0c8|lhrFgK0zd9U160Wo|a75e57ab701f9124e1113543fd1dc596f21e20d456a0d1e813d5a8aaec9adcb11213788e96598b67fe9486a9f0b99642c18296d0175db44b1ae426e4e91080ee'; + $this->assertEquals($stringToEncrypt, $this->crypto->decrypt($encryptedString, 'ThisIsAVeryS3cur3P4ssw0rd')); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage HMAC does not match. + */ + function testWrongIV() { + $encryptedString = '560f5436ba864b9f12f7f7ca6d41c327554a6f2c0a160a03316b202af07c65163274993f3a46e7547c07ba89304f00594a2f3bd99f83859097c58049c39d0d4ade10e0de914ff0604961e7c849d0271ed6c0b23f984ba16e7d033e3305fb0910e7b6a2a65c988d17dbee71d8f953684d|d2kdFUspVjC0o0sr|1a5feacf87eaa6869a6abdfba9a296e7bbad45b6ad89f7dce67cdc98e2da5dc4379cc672cc655e52bbf19599bf59482fbea13a73937697fa656bf10f3fc4f1aa'; + $this->crypto->decrypt($encryptedString, 'ThisIsAVeryS3cur3P4ssw0rd'); + } + + /** + * @expectedException \Exception + * @expectedExceptionMessage Authenticated ciphertext could not be decoded. + */ + function testWrongParameters() { + $encryptedString = '1|2'; + $this->crypto->decrypt($encryptedString, 'ThisIsAVeryS3cur3P4ssw0rd'); + } +}
\ No newline at end of file diff --git a/tests/lib/security/stringutils.php b/tests/lib/security/stringutils.php new file mode 100644 index 00000000000..65e4556bdfd --- /dev/null +++ b/tests/lib/security/stringutils.php @@ -0,0 +1,38 @@ +<?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\StringUtils; + +class StringUtilsTest extends \PHPUnit_Framework_TestCase { + + public function dataProvider() + { + return array( + array('Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt.', 'Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt.'), + array('', ''), + array('我看这本书。 我看這本書', '我看这本书。 我看這本書'), + array('GpKY9fSnWNJbES99zVGvA', 'GpKY9fSnWNJbES99zVGvA') + ); + } + + /** + * @dataProvider dataProvider + */ + function testWrongEquals($string) { + $this->assertFalse(StringUtils::equals($string, 'A Completely Wrong String')); + $this->assertFalse(StringUtils::equals($string, null)); + } + + /** + * @dataProvider dataProvider + */ + function testTrueEquals($string, $expected) { + $this->assertTrue(StringUtils::equals($string, $expected)); + } + +}
\ No newline at end of file |