From 045a89705c3c5614bf4ef2e0ae0eeeb3ed38c262 Mon Sep 17 00:00:00 2001 From: David Hicks Date: Mon, 8 Feb 2010 21:46:26 +1100 Subject: [PATCH] Issue #10730: Implement new crypto_api This implements the foundation of a new Cryptography API which is responsible for providing cryptography functionality to MantisBT. For now, the only feature available in this new API is the generation of secure and strong randomness using openssl_random_pseudo_bytes in PHP 5.3 (if available), /dev/urandom if available on the system or an enhanced mt_rand generator built on top of PHP's existing Mersenne Twister pseudo random number. We used to just rely on a single mt_rand() for generating nonces or providing other cryptographic functionality. This posed a number of problems including the leakage of the internal state of the Mersenne Twister PRNG, enabling users to predict all future outputs of the PRNG. Additionally, the total number of combinations available from mt_rand() is very small when in many cases we need more than a few million combinations of keys. The new approach calls mt_rand() multiple times and then using a secret unique salt known only to each MantisBT installation, hashes the output using the Whirlpool algorithm. This produces 512bits of output that can be used for creating a random string/nonce. If more than 512bits of output are required, we simply perform this operation multiple times until we have generated enough output. While the new Mersenne Twister method for generating random strings is still anything but strong or secure, it does raise the bar significantly. It is hoped that this method is only used as a last resort when no other options for generating strong randomness are available. A new configuration option $g_crypto_master_salt was also added to form the basis of salting and hashing functions in the future. Currently we use different keys for RSS, signup/lost password verification and so forth when it'd be much easier to just derive keys as needed from the master salt. If $g_crypto_master_salt is not defined by the user, MantisBT will refuse to work. This salt must be at least 16 characters long in the hope that users who don't understand the importance of setting a strong master salt are informed of their mistake. This refusal to work unless the user sets a strong $g_crypto_master_salt value in config_inc.php is necessary because it forms the basis for a lot of the security features implemented in MantisBT. We don't want users to forgetting to set $g_crypto_master_salt and using a default value known to the entire world. --- admin/check.php | 5 +- admin/install.php | 9 +- admin/test_langs.php | 3 +- admin/upgrade_unattended.php | 6 +- config_defaults_inc.php | 34 ++++++ core.php | 30 +++-- core/constant_inc.php | 4 + core/crypto_api.php | 145 +++++++++++++++++++++++ docbook/adminguide/en/configuration.sgml | 48 ++++++++ lang/strings_english.txt | 2 + 10 files changed, 266 insertions(+), 20 deletions(-) create mode 100644 core/crypto_api.php diff --git a/admin/check.php b/admin/check.php index 7e69a745f5..ed5c155520 100644 --- a/admin/check.php +++ b/admin/check.php @@ -23,7 +23,10 @@ error_reporting( E_ALL ); -$g_skip_open_db = true; # don't open the database in database_api.php +# Load the MantisDB core in maintenance mode. This mode will assume that +# config_inc.php hasn't been specified. Thus the database will not be opened +# and plugins will not be loaded. +define( 'MANTIS_MAINTENANCE_MODE', true ); /** * MantisBT Core API's diff --git a/admin/install.php b/admin/install.php index 1bfd302f1d..3b2532180e 100644 --- a/admin/install.php +++ b/admin/install.php @@ -25,9 +25,12 @@ /** @todo put this somewhere */ @set_time_limit( 0 ); -$g_skip_open_db = true; # don't open the database in database_api.php -define( 'MANTIS_INSTALLER', true ); -define( 'PLUGINS_DISABLED', true ); + +# Load the MantisDB core in maintenance mode. This mode will assume that +# config_inc.php hasn't been specified. Thus the database will not be opened +# and plugins will not be loaded. +define( 'MANTIS_MAINTENANCE_MODE', true ); + @require_once( dirname( dirname( __FILE__ ) ) . DIRECTORY_SEPARATOR . 'core.php' ); require_api( 'install_helper_functions_api.php' ); $g_error_send_page_header = false; # bypass page headers in error handler diff --git a/admin/test_langs.php b/admin/test_langs.php index 1b5b217593..ba84aacf7e 100644 --- a/admin/test_langs.php +++ b/admin/test_langs.php @@ -22,7 +22,7 @@ */ define( 'PLUGINS_DISABLED', true ); -$g_skip_lang_load = true; +define( 'LANG_LOAD_DISABLED', true ); /** * MantisBT Core API's @@ -45,7 +45,6 @@ die; } -unset( $g_skip_lang_load ) ; lang_push( 'english' ); access_ensure_global_level( config_get_global( 'admin_site_threshold' ) ); diff --git a/admin/upgrade_unattended.php b/admin/upgrade_unattended.php index 1435d4249a..4c2d43acf5 100644 --- a/admin/upgrade_unattended.php +++ b/admin/upgrade_unattended.php @@ -23,7 +23,11 @@ @set_time_limit( 0 ); -$g_skip_open_db = true; # don't open the database in database_api.php +# Load the MantisDB core in maintenance mode. This mode will assume that +# config_inc.php hasn't been specified. Thus the database will not be opened +# and plugins will not be loaded. +define( 'MANTIS_MAINTENANCE_MODE', true ); + /** * MantisBT Core API's */ diff --git a/config_defaults_inc.php b/config_defaults_inc.php index 163eb52d46..e296a69dae 100644 --- a/config_defaults_inc.php +++ b/config_defaults_inc.php @@ -272,6 +272,39 @@ */ $g_form_security_validation = ON; +/***************************** + * Security and Cryptography * + *****************************/ + +/** + * Master salt value used for cryptographic hashing throughout MantisBT. This + * value must be kept secret at all costs. You must generate a unique and + * random salt value for each installation of MantisBT you control. The + * minimum length of this string must be at least 16 characters. + * + * The value you select for this salt should be a long string generated using + * a secure random number generator. An example for Linux systems is: + * cat /dev/urandom | head -c 64 | base64 + * Note that the number of bits of entropy per byte of output from /dev/urandom + * is not 8. If you're particularly paranoid and don't mind waiting a long + * time, you could use /dev/random to get much closer to 8 bits of entropy per + * byte. Moving the mouse (if possible) while generating entropy via + * /dev/random will greatly improve the speed at which /dev/random produces + * entropy. + * + * WARNING: This configuration option has a profound impact on the security of + * your MantisBT installation. Failure to set this configuration option + * correctly could lead to your MantisBT installation being compromised. Ensure + * that this value remains secret. Treat it with the same security that you'd + * treat the password to your MantisDB database. + * + * This setting is blank by default. MantisBT will not operate in this state. + * Hence you are forced to change the value of this configuration option. + * + * @global string $g_crypto_master_salt + */ +$g_crypto_master_salt = ''; + /**************************** * Signup and Lost Password * ****************************/ @@ -3975,6 +4008,7 @@ 'compress_html', 'content_expire', 'cookie', + 'crypto_master_salt', 'custom_headers', 'database_name', '^db_', diff --git a/core.php b/core.php index 35934becd4..b93377ac05 100644 --- a/core.php +++ b/core.php @@ -33,6 +33,7 @@ * @uses config_defaults_inc.php * @uses config_inc.php * @uses constant_inc.php + * @uses crypto_api.php * @uses custom_constants_inc.php * @uses custom_functions_inc.php * @uses database_api.php @@ -199,24 +200,25 @@ function __autoload( $className ) { } } +# Initialise cryptographic keys +require_api( 'crypto_api.php' ); +crypto_init(); + # Connect to the database require_api( 'database_api.php' ); require_api( 'config_api.php' ); -if( !isset( $g_skip_open_db ) ) { +if ( !defined( 'MANTIS_MAINTENANCE_MODE' ) ) { if( OFF == $g_use_persistent_connections ) { db_connect( config_get_global( 'dsn', false ), $g_hostname, $g_db_username, $g_db_password, $g_database_name, config_get_global( 'db_schema' ) ); } else { db_connect( config_get_global( 'dsn', false ), $g_hostname, $g_db_username, $g_db_password, $g_database_name, config_get_global( 'db_schema' ), true ); } -} else { - if (!defined('PLUGINS_DISABLED') ) - define( 'PLUGINS_DISABLED', true ); } # Initialise plugins -require_api( 'plugin_api.php' ); -if ( !defined( 'PLUGINS_DISABLED' ) ) { +if ( !defined( 'PLUGINS_DISABLED' ) && !defined( 'MANTIS_MAINTENANCE_MODE' ) ) { + require_api( 'plugin_api.php' ); plugin_init_installed(); } @@ -239,13 +241,13 @@ function __autoload( $className ) { } require_api( 'authentication_api.php' ); -require_api( 'user_pref_api.php' ); if( auth_is_user_authenticated() ) { + require_api( 'user_pref_api.php' ); date_default_timezone_set( user_pref_get_pref( auth_get_current_user_id(), 'timezone' ) ); } -require_api( 'collapse_api.php' ); -if ( !defined( 'MANTIS_INSTALLER' ) ) { +if ( !defined( 'MANTIS_MAINTENANCE_MODE' ) ) { + require_api( 'collapse_api.php' ); collapse_cache_token(); } @@ -260,11 +262,13 @@ function __autoload( $className ) { http_all_headers(); # Push default language to speed calls to lang_get -require_api( 'lang_api.php' ); -if ( !isset( $g_skip_lang_load ) ) { +if ( !defined( 'LANG_LOAD_DISABLED' ) ) { + require_api( 'lang_api.php' ); lang_push( lang_get_default() ); } # Signal plugins that the core system is loaded -require_api( 'event_api.php' ); -event_signal( 'EVENT_CORE_READY' ); +if ( !defined( 'PLUGINS_DISABLED' ) && !defined( 'MANTIS_MAINTENANCE_MODE' ) ) { + require_api( 'event_api.php' ); + event_signal( 'EVENT_CORE_READY' ); +} diff --git a/core/constant_inc.php b/core/constant_inc.php index 772c7ddbdf..385a522153 100644 --- a/core/constant_inc.php +++ b/core/constant_inc.php @@ -413,6 +413,10 @@ # ERROR_FORM_* define( 'ERROR_FORM_TOKEN_INVALID', 2800 ); +# ERROR_CRYPTO_* +define( 'ERROR_CRYPTO_MASTER_SALT_INVALID', 2900 ); +define( 'ERROR_CRYPTO_CAN_NOT_GENERATE_STRONG_RANDOMNESS', 2901 ); + # Generic position constants define( 'POSITION_NONE', 0 ); define( 'POSITION_TOP', 1 ); diff --git a/core/crypto_api.php b/core/crypto_api.php new file mode 100644 index 0000000000..cbc417efa6 --- /dev/null +++ b/core/crypto_api.php @@ -0,0 +1,145 @@ +. + +/** + * Crypto API + * + * @package CoreAPI + * @subpackage CryptoAPI + * @copyright Copyright (C) 2000 - 2002 Kenzaburo Ito - kenito@300baud.org + * @copyright Copyright (C) 2002 - 2010 MantisBT Team - mantisbt-dev@lists.sourceforge.net + * @link http://www.mantisbt.org + * + * @uses config_api.php + * @uses constant_inc.php + * @uses error_api.php + */ + +require_api( 'config_api.php' ); +require_api( 'constant_inc.php' ); +require_api( 'error_api.php' ); + +/** + * Initialise the CryptoAPI subsystem. This function checks whether the master + * salt is specified correctly within the configuration. If not, a fatal error + * is produced to protect against invalid configuration impacting the security + * of the MantisBT installation. + * @return null + */ +function crypto_init() { + if ( !defined( 'MANTIS_MAINTENANCE_MODE' ) ) { + if ( strlen( config_get_global( 'crypto_master_salt' ) ) < 16 ) { + trigger_error( ERROR_CRYPTO_MASTER_SALT_INVALID, ERROR ); + } + } + return; +} + +/** + * Generate a random string (raw binary output) for cryptographic purposes such + * as nonces, IVs, default passwords, etc. This function will attempt to + * generate strong randomness but can optionally be used to generate weaker + * randomness if less security is needed or a strong source of randomness isn't + * available. The use of weak randomness for cryptographic purposes is strongly + * discouraged because it contains low entropy and is predictable. + * @param int $p_bytes Number of bytes of randomness required + * @param bool $p_require_strong_generator Whether or not a weak source of randomness can be used by this function + * @return string|null Raw binary string containing the requested number of bytes of random output or null if the output couldn't be created + */ +function crypto_generate_random_string( $p_bytes, $p_require_strong_generator = true ) { + + # First we attempt to use the secure PRNG provided by OpenSSL in PHP 5.3 + if ( function_exists( 'openssl_random_pseudo_bytes' ) ) { + $t_random_bytes = openssl_random_pseudo_bytes( $p_bytes, $t_strong ); + if ( $t_random_bytes !== false ) { + if ( $p_require_strong_generator && $t_strong === true ) { + $t_random_string = $t_random_bytes; + } else if ( !$p_require_strong_generator ) { + $t_random_string = $t_random_bytes; + } + } + } + + # Next we try to use the /dev/urandom PRNG provided on Linux systems. This + # is nowhere near as secure as /dev/random but it is still satisfactory for + # the needs of MantisBT, especially given the fact that we don't want this + # function to block while waiting for the system to generate more entropy. + $t_urandom_fp = @fopen( '/dev/urandom', 'rb' ); + if ( $t_urandom_fp !== false ) { + $t_random_bytes = @fread( $t_urandom_fp, $p_bytes ); + if ( $t_random_bytes !== false ) { + $t_random_string = $t_random_bytes; + } + @fclose( $t_urandom_fp ); + } + + # For Windows systems, we can try using Microsoft CryptoAPI to retrieve + # more reliable PRNG output than what PHP can provide by itself. + # !TODO + + # At this point we've run out of possibilities for generating randomness + # from a strong source. Unless weak output is specifically allowed by the + # $p_require_strong_generator argument, we should return null as we've + # failed to generate randomness to a satisfactory security level. + if ( !isset( $t_random_string ) && $p_require_strong_generator ) { + return null; + } + + # As a last resort we have to fall back to using the insecure Mersenne + # Twister pseudo random number generator provided in PHP. This DOES NOT + # produce cryptographically secure randomness and thus the output of the + # PRNG is easily guessable. In an attempt to make it harder to guess the + # internal state of the PRNG, we salt the PRNG output with a known secret + # and hash it. + if ( !isset( $t_random_string ) ) { + $t_secret_key = 'prng' . config_get_global( 'crypto_master_salt' ); + $t_random_bytes = ''; + for ( $i = 0; $i < $p_bytes; $i += 64 ) { + $t_random_segment = ''; + for ( $j = 0; $j < 64; $j++ ) { + $t_random_segment .= base_convert( mt_rand(), 10, 36 ); + } + $t_random_segment .= $i; + $t_random_segment .= $t_secret_key; + $t_random_bytes .= hash( 'whirlpool', $t_random_segment, true ); + } + $t_random_string = substr( $t_random_bytes, 0, $p_bytes ); + if ( $t_random_string === false ) { + return null; # Unexpected error + } + } + + return $t_random_string; +} + +/** + * Generate a strong random string (raw binary output) for cryptographic + * purposes such as nonces, IVs, default passwords, etc. If a strong source + * of randomness is not available, this function will fail and produce an + * error. Strong randomness is different from weak randomness in that a strong + * randomness generator doesn't produce predictable output and has much higher + * entropy. Where randomness is being used for cryptographic purposes, a strong + * source of randomness should always be used. + * @param int $p_bytes Number of bytes of strong randomness required + * @return string Raw binary string containing the requested number of bytes of random output + */ +function crypto_generate_strong_random_string( $p_bytes ) { + $t_random_string = crypto_generate_random_string( $p_bytes, true ); + if ( $t_random_string === null ) { + trigger_error( ERROR_CRYPTO_CAN_NOT_GENERATE_STRONG_RANDOMNESS, ERROR ); + } + return $t_random_string; +} diff --git a/docbook/adminguide/en/configuration.sgml b/docbook/adminguide/en/configuration.sgml index 80fe70767d..cdda7cfdc1 100644 --- a/docbook/adminguide/en/configuration.sgml +++ b/docbook/adminguide/en/configuration.sgml @@ -161,6 +161,54 @@ +
+ Security and Cryptography + + + + $g_crypto_master_salt + + Master salt value used for cryptographic hashing + throughout MantisBT. This value must be kept secret at + all costs. You must generate a unique and random salt + value for each installation of MantisBT you control. + The minimum length of this string must be at least 16 + characters. + + The value you select for this salt should be a long + string generated using a secure random number + generator. An example for Linux systems is: + + cat /dev/urandom | head -c 64 | base64 + Note that the number of bits of entropy per byte of + output from /dev/urandom is not 8. If you're + particularly paranoid and don't mind waiting a long + time, you could use /dev/random to get much closer to + 8 bits of entropy per byte. Moving the mouse + (if possible) while generating entropy via /dev/random + will greatly improve the speed at which /dev/random + produces entropy. + + This setting is blank by default. MantisBT will not + operate in this state. Hence you are forced to change + the value of this configuration option. + + + WARNING: This configuration option has a profound + impact on the security of your MantisBT + installation. Failure to set this configuration + option correctly could lead to your MantisBT + installation being compromised. Ensure that this + value remains secret. Treat it with the same + security that you'd treat the password to your + MantisDB database. + + + + + +
+
Signup and Lost Password diff --git a/lang/strings_english.txt b/lang/strings_english.txt index 051671a4f0..a5b3ce6e4b 100644 --- a/lang/strings_english.txt +++ b/lang/strings_english.txt @@ -296,6 +296,8 @@ $MANTIS_ERROR[ERROR_SESSION_HANDLER_INVALID] = 'Invalid session handler.'; $MANTIS_ERROR[ERROR_SESSION_VAR_NOT_FOUND] = 'Session variable "%1$s" not found.'; $MANTIS_ERROR[ERROR_SESSION_NOT_VALID] = 'Your session has become invalidated.'; $MANTIS_ERROR[ERROR_FORM_TOKEN_INVALID] = 'Invalid form security token. Did you submit the form twice by accident?'; +$MANTIS_ERROR[ERROR_CRYPTO_MASTER_SALT_INVALID] = 'For security reasons MantisBT will not operate when $g_crypto_master_salt is not specified correctly in config_inc.php.'; +$MANTIS_ERROR[ERROR_CRYPTO_CAN_NOT_GENERATE_STRONG_RANDOMNESS] = 'Unable to find a source of strong randomness for cryptographic purposes.'; $MANTIS_ERROR[ERROR_INVALID_REQUEST_METHOD] = 'This page cannot be accessed using this method.'; $MANTIS_ERROR[ERROR_INVALID_SORT_FIELD] = 'Invalid sort field.'; $MANTIS_ERROR[ERROR_INVALID_DATE_FORMAT] = 'Invalid date format.';