Add CryptoUtil
authorTim Düsterhus <duesterhus@woltlab.com>
Fri, 7 Aug 2015 23:11:29 +0000 (01:11 +0200)
committerTim Düsterhus <duesterhus@woltlab.com>
Fri, 7 Aug 2015 23:11:29 +0000 (01:11 +0200)
com.woltlab.wcf/option.xml
wcfsetup/install/files/lib/system/package/PackageInstallationDispatcher.class.php
wcfsetup/install/files/lib/util/CryptoUtil.class.php [new file with mode: 0644]
wcfsetup/install/files/lib/util/PasswordUtil.class.php
wcfsetup/install/files/lib/util/exception/CryptoException.class.php [new file with mode: 0644]
wcfsetup/install/lang/de.xml
wcfsetup/install/lang/en.xml
wcfsetup/test.php

index 2d908677a48bb2867c78cf3aeeb882cb0b72c980..de61ea460449f58994048fda1629bd1a61558b70 100644 (file)
                                        <category name="security.general.authentication">
                                                <parent>security.general</parent>
                                        </category>
+                                       <category name="security.general.secrets">
+                                               <parent>security.general</parent>
+                                               <showorder>15</showorder>
+                                       </category>
                                <category name="security.blacklist">
                                        <parent>security</parent>
                                </category>
@@ -610,6 +614,16 @@ imagick:wcf.acp.option.image_adapter_type.imagick]]>
                        </option>
                        <!-- /security.general.authentication -->
                        
+                       <!-- security.general.secrets -->
+                       <option name="signature_secret">
+                               <categoryname>security.general.secrets</categoryname>
+                               <optiontype>text</optiontype>
+                               <defaultvalue></defaultvalue>
+                               <allowempty>0</allowempty>
+                               <validationpattern>^.{15,}$</validationpattern>
+                       </option>
+                       <!-- /security.general.secrets -->
+                       
                        <!-- security.blacklist -->
                        <option name="blacklist_ip_addresses">
                                <categoryname>security.blacklist</categoryname>
index aff9931c5e42934177dd2c6880172627cb822990..802904bde2d70adc38a0a8c6b1346a6f49c05a8b 100644 (file)
@@ -31,6 +31,8 @@ use wcf\system\setup\Installer;
 use wcf\system\style\StyleHandler;
 use wcf\system\user\storage\UserStorageHandler;
 use wcf\system\WCF;
+use wcf\util\exception\CryptoException;
+use wcf\util\CryptoUtil;
 use wcf\util\FileUtil;
 use wcf\util\HeaderUtil;
 use wcf\util\StringUtil;
@@ -187,6 +189,17 @@ class PackageInstallationDispatcher {
                                                'wcf_uuid'
                                        ));
                                        
+                                       try {
+                                               $statement->execute([
+                                                       bin2hex(CryptoUtil::randomBytes(20)),
+                                                       'signature_secret'
+                                               ]);
+                                       }
+                                       catch (CryptoException $e) {
+                                               // ignore, the secret will stay empty and crypto operations
+                                               // depending on it will fail
+                                       }
+                                       
                                        if (WCF::getSession()->getVar('__wcfSetup_developerMode')) {
                                                $statement->execute(array(
                                                        1,
diff --git a/wcfsetup/install/files/lib/util/CryptoUtil.class.php b/wcfsetup/install/files/lib/util/CryptoUtil.class.php
new file mode 100644 (file)
index 0000000..e885fd5
--- /dev/null
@@ -0,0 +1,139 @@
+<?php
+namespace wcf\util;
+use wcf\util\exception\CryptoException;
+
+/**
+ * Contains cryptographic helper functions.
+ * Features:
+ * - Creating secure signatures based on the Keyed-Hash Message Authentication Code algorithm
+ * - Constant time comparison function
+ * - Generating a string of random bytes
+ * 
+ * @author     Tim Duesterhus, Alexander Ebert
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage util
+ * @category   Community Framework
+ */
+final class CryptoUtil {
+       /**
+        * Signs the given value with the signature secret.
+        * 
+        * @param       string  $value
+        * @return      string
+        */
+       public static function getSignature($value) {
+               if (mb_strlen(SIGNATURE_SECRET, '8bit') < 15) throw new CryptoException('SIGNATURE_SECRET is too short, aborting.');
+               
+               return hash_hmac('sha256', $value, SIGNATURE_SECRET);
+       }
+
+       /**
+        * Creates a signed (signature + encoded value) string.
+        * 
+        * @param       string  $value
+        * @return      string
+        */
+       public static function createSignedString($value) {
+               return self::getSignature($value).'-'.base64_encode($value);
+       }
+
+       /**
+        * Returns whether the given string is a proper signed string.
+        * (i.e. consists of a valid signature + encoded value)
+        * 
+        * @param       string  $string
+        * @return      boolean
+        */
+       public static function validateSignedString($string) {
+               $parts = explode('-', $string, 2);
+               if (count($parts) !== 2) return false;
+               list($signature, $value) = $parts;
+               $value = base64_decode($value);
+               
+               return self::secureCompare($signature, self::getSignature($value));
+       }
+
+       /**
+        * Returns the value of a signed string, after
+        * validating whether it is properly signed.
+        * 
+        * - Returns null if the string is not properly signed.
+        * 
+        * @param       string          $string
+        * @return      null|string
+        * @see         \wcf\util\CryptoUtil::validateSignedString()
+        */
+       public static function getValueFromSignedString($string) {
+               if (!self::validateSignedString($string)) return null;
+               
+               $parts = explode('-', $string, 2);
+               return base64_decode($parts[1]);
+       }
+
+       /**
+        * Compares two strings in a constant time manner.
+        * This function effectively is a polyfill for the PHP 5.6 `hash_equals`.
+        * 
+        * @param       string          $hash1
+        * @param       string          $hash2
+        * @return      boolean
+        */
+       public static function secureCompare($hash1, $hash2) {
+               $hash1 = (string) $hash1;
+               $hash2 = (string) $hash2;
+               
+               if (function_exists('hash_equals')) {
+                       return hash_equals($hash1, $hash2);
+               }
+               
+               if (strlen($hash1) !== strlen($hash2)) {
+                       return false;
+               }
+               
+               $result = 0;
+               for ($i = 0, $length = strlen($hash1); $i < $length; $i++) {
+                       $result |= ord($hash1[$i]) ^ ord($hash2[$i]);
+               }
+               
+               return ($result === 0);
+       }
+       
+       /**
+        * Compares a string of N random bytes.
+        * This function effectively is a polyfill for the PHP 7 `random_bytes`.
+        * 
+        * Requires either PHP 7 or 'openssl_random_pseudo_bytes' and throws a CryptoException
+        * if no sufficiently random data could be obtained.
+        * 
+        * @param       int             $n
+        * @return      string
+        */
+       public static function randomBytes($n) {
+               try {
+                       if (function_exists('random_bytes')) {
+                               $bytes = random_bytes($n);
+                               if ($bytes === false) throw new CryptoException('Cannot generate a secure stream of bytes.');
+                               
+                               return $bytes;
+                       }
+                       
+                       $bytes = openssl_random_pseudo_bytes($n, $s);
+                       if (!$s) throw new CryptoException('Cannot generate a secure stream of bytes.');
+                       
+                       return $bytes;
+               }
+               catch (CryptoException $e) {
+                       throw $e;
+               }
+               catch (\Throwable $e) {
+                       throw new CryptoException('Cannot generate a secure stream of bytes.', $e);
+               }
+               catch (\Exception $e) {
+                       throw new CryptoException('Cannot generate a secure stream of bytes.', $e);
+               }
+       }
+       
+       private function __construct() { }
+}
index 2a5fdf74204ab21f1c3560159049755e0db6b1ef..2daaa0a56d3b26a033a29f6ab5cb7dced6dc9d6a 100644 (file)
@@ -227,28 +227,11 @@ final class PasswordUtil {
        }
        
        /**
-        * Compares two password hashes. This function is protected against timing attacks.
-        * 
-        * @see         http://codahale.com/a-lesson-in-timing-attacks/
-        * 
-        * @param       string          $hash1
-        * @param       string          $hash2
-        * @return      boolean
+        * @see \wcf\util\CryptoUtil::secureCompare()
+        * @deprecated  Use \wcf\util\CryptoUtil::secureCompare()
         */
        public static function secureCompare($hash1, $hash2) {
-               $hash1 = (string)$hash1;
-               $hash2 = (string)$hash2;
-               
-               if (strlen($hash1) !== strlen($hash2)) {
-                       return false;
-               }
-               
-               $result = 0;
-               for ($i = 0, $length = strlen($hash1); $i < $length; $i++) {
-                       $result |= ord($hash1[$i]) ^ ord($hash2[$i]);
-               }
-               
-               return ($result === 0);
+               return CryptoUtil::secureCompare($hash1, $hash2);
        }
        
        /**
@@ -614,11 +597,8 @@ final class PasswordUtil {
                if (self::secureCompare($dbHash, sha1(sha1($password) . $salt))) {
                        return true;
                }
-               else if (extension_loaded('hash')) {
-                       return self::secureCompare($dbHash, hash('sha256', hash('sha256', $password) . $salt));
-               }
                
-               return false;
+               return self::secureCompare($dbHash, hash('sha256', hash('sha256', $password) . $salt));
        }
        
        /**
diff --git a/wcfsetup/install/files/lib/util/exception/CryptoException.class.php b/wcfsetup/install/files/lib/util/exception/CryptoException.class.php
new file mode 100644 (file)
index 0000000..1868943
--- /dev/null
@@ -0,0 +1,21 @@
+<?php
+namespace wcf\util\exception;
+
+/**
+ * Denotes failure to perform secure crypto.
+ * 
+ * @author     Tim Duesterhus
+ * @copyright  2001-2015 WoltLab GmbH
+ * @license    GNU Lesser General Public License <http://opensource.org/licenses/lgpl-license.php>
+ * @package    com.woltlab.wcf
+ * @subpackage util.exception
+ * @category   Community Framework
+ */
+class CryptoException extends \Exception {
+       /**
+        * @see \Exception::__construct()
+        */
+       public function __construct($message, $previous) {
+               parent::__construct($message, 0, $previous);
+       }
+}
index 9fcf61877f19c2cad3be607a7140ffcb515d0f42..4d6b62da4802dd62820c4a39756f15a99be347bd 100644 (file)
                <item name="wcf.acp.option.category.security.blacklist"><![CDATA[Blacklist]]></item>
                <item name="wcf.acp.option.category.security.general"><![CDATA[Allgemein]]></item>
                <item name="wcf.acp.option.category.security.general.session"><![CDATA[Sitzungen]]></item>
+               <item name="wcf.acp.option.category.security.general.secrets"><![CDATA[Geheimschlüssel]]></item>
                <item name="wcf.acp.option.category.general.offline"><![CDATA[Wartungsmodus]]></item>
                <item name="wcf.acp.option.category.user"><![CDATA[Benutzer]]></item>
                <item name="wcf.acp.option.category.user.general"><![CDATA[Allgemein]]></item>
@@ -1053,6 +1054,8 @@ GmbH=Gesellschaft mit beschränkter Haftung]]></item>
                <item name="wcf.acp.option.user_authentication_failure_user_captcha.description"><![CDATA[Wird die angegebene Anzahl von fehlgeschlagenen Anmeldeversuchen auf einen Benutzer-Account überschritten, muss der Benutzer ein Captcha ausfüllen.]]></item>
                <item name="wcf.acp.option.user_authentication_failure_expiration"><![CDATA[Löschung von alten Protokolleinträgen]]></item>
                <item name="wcf.acp.option.user_authentication_failure_expiration.description"><![CDATA[Legt fest nach welchem Zeitraum protokollierte Anmeldeversuche gelöscht werden.]]></item>
+               <item name="wcf.acp.option.signature_secret"><![CDATA[Geheimer Schlüssel]]></item>
+               <item name="wcf.acp.option.signature_secret.description"><![CDATA[Ein geheimer Schlüssel, welcher zur Überprüfung von übertragenen Daten bestimmter Funktionen dient. Dieser Schlüssel ist vertraulich zu behandeln! Der Schlüssel wurde bei der Installation zufällig generiert und sollte nur in Ausnahmefällen geändert werden. Achtung: Dieser Schlüssel muss mindestens 15 Zeichen lang sein.]]></item>
                <item name="wcf.acp.option.gravatar_default_type"><![CDATA[Standard Gravatar-Typ]]></item>
                <item name="wcf.acp.option.gravatar_default_type.description"><![CDATA[Der <a class="externalURL" href="{@$__wcf->getPath()}acp/dereferrer.php?url=https://de.gravatar.com/site/implement/images/#default-image">Standard-Gravatar-Typ</a>, wenn einer E-Mail kein Gravatar zugeordnet werden kann.]]></item>
                <item name="wcf.acp.option.gravatar_default_type.404"><![CDATA[Kein Standard-Gravatar]]></item>
index 8fde038572c6705e1b2a0eed99956fdf1f252ac9..1341b504f1dcc8bdfe1b09293f1082aba8600c38 100644 (file)
@@ -732,6 +732,7 @@ Examples for medium ID detection:
                <item name="wcf.acp.option.category.security.blacklist"><![CDATA[Blacklist]]></item>
                <item name="wcf.acp.option.category.security.general"><![CDATA[General]]></item>
                <item name="wcf.acp.option.category.security.general.session"><![CDATA[Sessions]]></item>
+               <item name="wcf.acp.option.category.security.general.secrets"><![CDATA[Secret Keys]]></item>
                <item name="wcf.acp.option.category.general.offline"><![CDATA[Maintenance Mode]]></item>
                <item name="wcf.acp.option.category.user"><![CDATA[Users]]></item>
                <item name="wcf.acp.option.category.user.general"><![CDATA[General]]></item>
@@ -1052,6 +1053,8 @@ GmbH=Gesellschaft mit beschränkter Haftung]]></item>
                <item name="wcf.acp.option.user_authentication_failure_user_captcha.description"><![CDATA[Once there have been more than the configured amount of failed attempts for the same user account, a captcha will be enforced regardless of the IP address.]]></item>
                <item name="wcf.acp.option.user_authentication_failure_expiration"><![CDATA[Prune Log Entries]]></item>
                <item name="wcf.acp.option.user_authentication_failure_expiration.description"><![CDATA[Logs of failed login attempts will be automatically pruned after the configured amount of days, raising the limit will provide a longer history in expense for increased database storage usage.]]></item>
+               <item name="wcf.acp.option.signature_secret"><![CDATA[Secret Key]]></item>
+               <item name="wcf.acp.option.signature_secret.description"><![CDATA[A secret key that serves the purpose of validating data to prevent tampering. Keep this key secret! A random key was generated for you during installation, you don't need to change it. Note: This key must be at least 15 characters long.]]></item>
                <item name="wcf.acp.option.gravatar_default_type"><![CDATA[Default Gravatar Type]]></item>
                <item name="wcf.acp.option.gravatar_default_type.description"><![CDATA[The <a class="externalURL" href="{@$__wcf->getPath()}acp/dereferrer.php?url=https://de.gravatar.com/site/implement/images/#default-image">default Gravatar type</a> used if no matching Gravatar was found.]]></item>
                <item name="wcf.acp.option.gravatar_default_type.404"><![CDATA[No default Gravatar]]></item>
index 765a6ef49615fc499829121d79a14992ec8670f5..ae0c786bd26aee6510b0e8d201ab3ef7e63042b0 100644 (file)
@@ -91,6 +91,22 @@ else if (!extension_loaded('pcre')) {
        <?php
 }
 
+// check Hash extension
+else if (!extension_loaded('hash')) {
+       ?>
+       <p>The 'Hash' PHP extension is missing. Hash is required for a stable work of this software.<br />
+       Die 'Hash' Erweiterung f&uuml;r PHP wurde nicht gefunden. Diese Erweiterung ist f&uuml;r den Betrieb der Software notwendig.</p>
+       <?php
+}
+
+// check whether Hash extension is sane
+else if (!in_array('sha256', hash_algos())) {
+       ?>
+       <p>The 'Hash' PHP extension is broken. It needs to support the SHA-256 algorithm.<br />
+       Die 'Hash' Erweiterung f&uuml;r PHP ist kaputt. Sie unterstützt die SHA-256-Hashfunktion nicht.</p>
+       <?php
+}
+
 // everything is fine
 else {
        ?>