<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>
</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>
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;
'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,
--- /dev/null
+<?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() { }
+}
}
/**
- * 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);
}
/**
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));
}
/**
--- /dev/null
+<?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);
+ }
+}
<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>
<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>
<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>
<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>
<?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ür PHP wurde nicht gefunden. Diese Erweiterung ist fü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ür PHP ist kaputt. Sie unterstützt die SHA-256-Hashfunktion nicht.</p>
+ <?php
+}
+
// everything is fine
else {
?>