CVE-2020-5248: Use of Hard-coded Credentials (CWE-798)
value: GLPI before before version 9.4.6 has a vulnerability involving a default encryption key. GLPIKEY is public and is used on every instance. This means anyone can decrypt sensitive data stored using this key. It is possible to change the key before installing GLPI. But on existing instances, data must be reencrypted with the new key. Problem is we can not know which columns or rows in the database are using that; espcially from plugins. Changing the key without updating data would lend in bad password sent from glpi; but storing them again from the UI will work.
CWE description: The product contains hard-coded credentials, such as a password or cryptographic key.
CVSS v3 base score: 5.3
Published: 2020-05-12T16:15Z
Repository: https://github.com/glpi-project/glpi
Language: PHP
Files changed: .gitignore, ajax/mailcollector.php, inc/auth.class.php, inc/authldap.class.php, inc/config.class.php, inc/console/security/changekeycommand.class.php, inc/glpikey.class.php, inc/glpimailer.class.php, inc/mailcollector.class.php, inc/rssfeed.class.php, inc/toolbox.class.php, inc/update.class.php, install/install.php, status.php, tests/functionnal/AuthLdap.php
Commit date: 2020-04-23 09:55:31 +0200
Commit message: Deprecate GLPIKEY usage

CVE-2020-5248

Deprecate GLPIKEY usage, and replace it with key file per instance.
Add a command to generate new key, and update database.
Add plugins hooks to register fields or configuration entries to be
handled when updating db.

Diff: diff --git a/.gitignore b/.gitignore
index cdaef41a8d7..8fbbc23156c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
 /config/config_db*
+/config/glpi.key
 /config/config_path.php
 /config/local_define.php
 /tests/config_db*
 /plugins/
 /files/
diff --git a/ajax/mailcollector.php b/ajax/mailcollector.php
index aca1d1bd3d5..e6946e0d7e5 100644
--- a/ajax/mailcollector.php
+++ b/ajax/mailcollector.php
@@ -56,11 +56,11 @@ if (isset($_REQUEST['action'])) {
 
          if (isset($input["passwd"])) {
             if (empty($input["passwd"])) {
                unset($input["passwd"]);
             } else {
-               $input["passwd"] = Toolbox::encrypt(stripslashes($input["passwd"]), GLPIKEY);
+               $input["passwd"] = Toolbox::encrypt(stripslashes($input["passwd"]));
             }
          }
 
          if (isset($input['mail_server']) && !empty($input['mail_server'])) {
             $input["host"] = Toolbox::constructMailServerConfig($input);
diff --git a/inc/auth.class.php b/inc/auth.class.php
index 8ebc14ef988..51f2e9b3464 100644
--- a/inc/auth.class.php
+++ b/inc/auth.class.php
@@ -662,12 +662,11 @@ class Auth extends CommonGLPI {
                $ldapservers_status = false;
                foreach ($ldapservers as $ldap_method) {
                   $ds = AuthLdap::connectToServer($ldap_method["host"],
                                                   $ldap_method["port"],
                                                   $ldap_method["rootdn"],
-                                                  Toolbox::decrypt($ldap_method["rootdn_passwd"],
-                                                                   GLPIKEY),
+                                                  Toolbox::decrypt($ldap_method["rootdn_passwd"]),
                                                   $ldap_method["use_tls"],
                                                   $ldap_method["deref_option"]);
 
                   if ($ds) {
                      $ldapservers_status = true;
diff --git a/inc/authldap.class.php b/inc/authldap.class.php
index 245b6519ae2..0431a42339b 100644
--- a/inc/authldap.class.php
+++ b/inc/authldap.class.php
@@ -190,12 +190,11 @@ class AuthLDAP extends CommonDBTM {
 
       if (isset($input["rootdn_passwd"])) {
          if (empty($input["rootdn_passwd"])) {
             unset($input["rootdn_passwd"]);
          } else {
-            $input["rootdn_passwd"] = Toolbox::encrypt(stripslashes($input["rootdn_passwd"]),
-                                                       GLPIKEY);
+            $input["rootdn_passwd"] = Toolbox::encrypt(stripslashes($input["rootdn_passwd"]));
          }
       }
 
       if (isset($input["_blank_passwd"]) && $input["_blank_passwd"]) {
          $input['rootdn_passwd'] = '';
@@ -1454,11 +1453,11 @@ class AuthLDAP extends CommonDBTM {
          //Test connection to a master ldap server
          $host = $config_ldap->fields['host'];
          $port = $config_ldap->fields['port'];
       }
       $ds = self::connectToServer($host, $port, $config_ldap->fields['rootdn'],
-                                  Toolbox::decrypt($config_ldap->fields['rootdn_passwd'], GLPIKEY),
+                                  Toolbox::decrypt($config_ldap->fields['rootdn_passwd']),
                                   $config_ldap->fields['use_tls'],
                                   $config_ldap->fields['deref_option']);
       if ($ds) {
          return true;
       }
@@ -2606,11 +2605,11 @@ class AuthLDAP extends CommonDBTM {
     */
    function connect() {
 
       return $this->connectToServer($this->fields['host'], $this->fields['port'],
                                     $this->fields['rootdn'],
-                                    Toolbox::decrypt($this->fields['rootdn_passwd'], GLPIKEY),
+                                    Toolbox::decrypt($this->fields['rootdn_passwd']),
                                     $this->fields['use_tls'],
                                     $this->fields['deref_option']);
    }
 
 
@@ -2667,11 +2666,11 @@ class AuthLDAP extends CommonDBTM {
          Toolbox::logError("ldap_connect function is missing. Did you miss install php-ldap extension?");
          return false;
       }
       $ds = self::connectToServer($ldap_method['host'], $ldap_method['port'],
                                   $ldap_method['rootdn'],
-                                  Toolbox::decrypt($ldap_method['rootdn_passwd'], GLPIKEY),
+                                  Toolbox::decrypt($ldap_method['rootdn_passwd']),
                                   $ldap_method['use_tls'], $ldap_method['deref_option']);
 
       // Test with login and password of the user if exists
       if (!$ds
           && !empty($login)) {
@@ -2684,11 +2683,11 @@ class AuthLDAP extends CommonDBTM {
       if (!$ds
           && ($ldap_method['id'] > 0)) {
          foreach (self::getAllReplicateForAMaster($ldap_method['id']) as $replicate) {
             $ds = self::connectToServer($replicate["host"], $replicate["port"],
                                         $ldap_method['rootdn'],
-                                        Toolbox::decrypt($ldap_method['rootdn_passwd'], GLPIKEY),
+                                        Toolbox::decrypt($ldap_method['rootdn_passwd']),
                                         $ldap_method['use_tls'], $ldap_method['deref_option']);
 
             // Test with login and password of the user
             if (!$ds
                 && !empty($login)) {
@@ -3440,11 +3439,11 @@ class AuthLDAP extends CommonDBTM {
     */
    static function searchUser(AuthLDAP $authldap) {
 
       if (self::connectToServer($authldap->getField('host'), $authldap->getField('port'),
                                 $authldap->getField('rootdn'),
-                                Toolbox::decrypt($authldap->getField('rootdn_passwd'), GLPIKEY),
+                                Toolbox::decrypt($authldap->getField('rootdn_passwd')),
                                 $authldap->getField('use_tls'),
                                 $authldap->getField('deref_option'))) {
          self::showLdapUsers();
 
       } else {
@@ -3498,11 +3497,11 @@ class AuthLDAP extends CommonDBTM {
       if (!self::getNumberOfServers()) {
          $input['is_default'] = 1;
       }
 
       if (isset($input["rootdn_passwd"]) && !empty($input["rootdn_passwd"])) {
-         $input["rootdn_passwd"] = Toolbox::encrypt(stripslashes($input["rootdn_passwd"]), GLPIKEY);
+         $input["rootdn_passwd"] = Toolbox::encrypt(stripslashes($input["rootdn_passwd"]));
       }
 
       return $input;
    }
 
diff --git a/inc/config.class.php b/inc/config.class.php
index 717dd0f4f3a..6332cc86f4b 100644
--- a/inc/config.class.php
+++ b/inc/config.class.php
@@ -149,11 +149,11 @@ class Config extends CommonDBTM {
       }
       if (isset($input["smtp_passwd"])) {
          if (empty($input["smtp_passwd"])) {
             unset($input["smtp_passwd"]);
          } else {
-            $input["smtp_passwd"] = Toolbox::encrypt(stripslashes($input["smtp_passwd"]), GLPIKEY);
+            $input["smtp_passwd"] = Toolbox::encrypt(stripslashes($input["smtp_passwd"]));
          }
       }
 
       if (isset($input["_blank_smtp_passwd"]) && $input["_blank_smtp_passwd"]) {
          $input['smtp_passwd'] = '';
@@ -161,12 +161,11 @@ class Config extends CommonDBTM {
 
       if (isset($input["proxy_passwd"])) {
          if (empty($input["proxy_passwd"])) {
             unset($input["proxy_passwd"]);
          } else {
-            $input["proxy_passwd"] = Toolbox::encrypt(stripslashes($input["proxy_passwd"]),
-                                                      GLPIKEY);
+            $input["proxy_passwd"] = Toolbox::encrypt(stripslashes($input["proxy_passwd"]));
          }
       }
 
       if (isset($input["_blank_proxy_passwd"]) && $input["_blank_proxy_passwd"]) {
          $input['proxy_passwd'] = '';
diff --git a/inc/console/security/changekeycommand.class.php b/inc/console/security/changekeycommand.class.php
new file mode 100644
index 00000000000..be845108440
--- /dev/null
+++ b/inc/console/security/changekeycommand.class.php
@@ -0,0 +1,110 @@
+<?php
+/**
+ * ---------------------------------------------------------------------
+ * GLPI - Gestionnaire Libre de Parc Informatique
+ * Copyright (C) 2015-2018 Teclib' and contributors.
+ *
+ * http://glpi-project.org
+ *
+ * based on GLPI - Gestionnaire Libre de Parc Informatique
+ * Copyright (C) 2003-2014 by the INDEPNET Development Team.
+ *
+ * ---------------------------------------------------------------------
+ *
+ * LICENSE
+ *
+ * This file is part of GLPI.
+ *
+ * GLPI is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * GLPI is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with GLPI. If not, see <http://www.gnu.org/licenses/>.
+ * ---------------------------------------------------------------------
+ */
+
+namespace Glpi\Console\Security;
+
+if (!defined('GLPI_ROOT')) {
+   die("Sorry. You can't access this file directly");
+}
+
+use Glpi\Console\AbstractCommand;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Question\ConfirmationQuestion;
+use GLPIKey;
+
+class ChangekeyCommand extends AbstractCommand {
+   /**
+    * Error code returned when unable to renew key.
+    *
+    * @var integer
+    */
+   const ERROR_UNABLE_TO_RENEW_KEY = 1;
+
+   protected function configure() {
+      parent::configure();
+
+      $this->setName('glpi:security:change_key');
+      $this->setDescription(__('Change password storage key and update values in database.'));
+   }
+
+   protected function execute(InputInterface $input, OutputInterface $output) {
+      $glpikey = new GLPIKey();
+
+      $fields = $glpikey->getFields();
+      $configs = $glpikey->getConfigs();
+      $conf_count = 0;
+      foreach ($configs as $config) {
+         $conf_count += count($config);
+      }
+
+      $output->writeln(
+         sprintf(
+            '<info>' . __('Found %1$s field(s) and %2$s configuration entries requiring migration.') . '</info>',
+            count($fields),
+            $conf_count
+         )
+      );
+
+      if (!$input->getOption('no-interaction')) {
+         // Ask for confirmation (unless --no-interaction)
+         $question_helper = $this->getHelper('question');
+         $run = $question_helper->ask(
+            $input,
+            $output,
+            new ConfirmationQuestion(__('Do you want to continue ?') . ' [Yes/no]', true)
+         );
+         if (!$run) {
+            $output->writeln(
+               '<comment>' . __('Aborted.') . '</comment>',
+               OutputInterface::VERBOSITY_VERBOSE
+            );
+            return 0;
+         }
+      }
+
+      $created = $glpikey->generate();
+      if (!$created) {
+         $output->writeln(
+            '<error>' . __('Unable to change security key!') . '</error>',
+            OutputInterface::VERBOSITY_QUIET
+         );
+         return self::ERROR_UNABLE_TO_RENEW_KEY;
+      }
+
+      $this->output->write(PHP_EOL);
+
+      $output->writeln('<info>' . __('New security key generated; database updated.') . '</info>');
+
+      return 0; // Success
+   }
+}
diff --git a/inc/glpikey.class.php b/inc/glpikey.class.php
new file mode 100644
index 00000000000..dab0d3e1403
--- /dev/null
+++ b/inc/glpikey.class.php
@@ -0,0 +1,225 @@
+<?php
+/**
+ * ---------------------------------------------------------------------
+ * GLPI - Gestionnaire Libre de Parc Informatique
+ * Copyright (C) 2015-2018 Teclib' and contributors.
+ *
+ * http://glpi-project.org
+ *
+ * based on GLPI - Gestionnaire Libre de Parc Informatique
+ * Copyright (C) 2003-2014 by the INDEPNET Development Team.
+ *
+ * ---------------------------------------------------------------------
+ *
+ * LICENSE
+ *
+ * This file is part of GLPI.
+ *
+ * GLPI is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * GLPI is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with GLPI. If not, see <http://www.gnu.org/licenses/>.
+ * ---------------------------------------------------------------------
+ */
+
+if (!defined('GLPI_ROOT')) {
+   die("Sorry. You can't access this file directly");
+}
+
+/**
+ *  GLPI security key
+**/
+class GLPIKey {
+   /**
+    * Key file path.
+    *
+    * @var string
+    */
+   private $keyfile;
+
+   /**
+    * List of crypted DB fields.
+    *
+    * @var array
+    */
+   protected $fields = [
+      'glpi_mailcollectors.passwd',
+      'glpi_authldaps.rootdn_passwd'
+   ];
+
+   /**
+    * List of crypted configuration values.
+    * Each key corresponds to a configuration context, and contains list of configs names.
+    *
+    * @var array
+    */
+   protected $configs = [
+      'core'   => [
+         'smtp_passwd',
+         'proxy_passwd'
+      ]
+   ];
+
+   public function __construct() {
+      $this->keyfile = GLPI_CONFIG_DIR . '/glpi.key';
+   }
+
+   /**
+    * Check if GLPI security key used for decryptable passwords exists
+    *
+    * @return string
+    */
+   public function keyExists() {
+      return file_exists($this->keyfile) && !empty($this->get());
+   }
+
+   /**
+    * Get GLPI security key used for decryptable passwords
+    *
+    * @return string
+    */
+   public function get() {
+      $key = GLPIKEY;
+      if (file_exists($this->keyfile)) {
+         //load key from existing config file
+         $key = file_get_contents($this->keyfile);
+      }
+      return $key;
+   }
+
+   /**
+    * Generate GLPI security key used for decryptable passwords
+    * and update values in DB if necessary.
+    *
+    * @return boolean
+    */
+   public function generate() {
+      global $DB;
+
+      $current_key = file_exists($this->keyfile) ? $this->get() : GLPIKEY;
+
+      $success = (bool)file_put_contents($this->keyfile, Toolbox::getRandomString(50));
+      if (!$success) {
+         return false;
+      }
+
+      if ($DB instanceof DBmysql) {
+         return $this->migrateFieldsInDb($current_key)
+            && $this->migrateConfigsInDb($current_key);
+      }
+
+      return true;
+   }
+
+   /**
+    * Get fields
+    *
+    * @return array
+    */
+   public function getFields() {
+      global $PLUGIN_HOOKS;
+
+      $fields = $this->fields;
+      if (isset($PLUGIN_HOOKS['secured_fields'])) {
+         foreach ($PLUGIN_HOOKS['secured_fields'] as $plugfields) {
+            $fields = array_merge($fields, $plugfields);
+         }
+      }
+
+      return $fields;
+   }
+
+   /**
+    * Get configs
+    *
+    * @return array
+    */
+   public function getConfigs() {
+      global $PLUGIN_HOOKS;
+
+      $configs = $this->configs;
+
+      if (isset($PLUGIN_HOOKS['secured_configs'])) {
+         foreach ($PLUGIN_HOOKS['secured_configs'] as $plugin => $plugconfigs) {
+            $configs['plugin:' . $plugin] = $plugconfigs;
+         }
+      }
+
+      return $configs;
+   }
+
+   /**
+    * Migrate fields in database
+    *
+    * @param string $current_key Current key
+    *
+    * @return void
+    */
+   protected function migrateFieldsInDb($current_key) {
+      global $DB;
+
+      $success = true;
+
+      foreach ($this->getFields() as $field) {
+         list($table, $column) = explode('.', $field);
+
+         $iterator = $DB->request([
+            'SELECT' => ['id', $column],
+            'FROM'   => $table
+         ]);
+
+         while ($success && $row = $iterator->next()) {
+            $pass = Toolbox::encrypt(Toolbox::decrypt($row[$column], $current_key));
+            $success = $DB->update(
+               $table,
+               [$field  => $pass],
+               ['id'    => $row['id']]
+            );
+         }
+      }
+
+      return $success;
+   }
+
+   /**
+    * Migrate configurations in database
+    *
+    * @param string $current_key Current key
+    *
+    * @return boolean
+    */
+   protected function migrateConfigsInDb($current_key) {
+      global $DB;
+
+      $success = true;
+
+      foreach ($this->getConfigs() as $context => $names) {
+         $iterator = $DB->request([
+            'FROM'   => Config::getTable(),
+            'WHERE'  => [
+               'context'   => $context,
+               'name'      => $names
+            ]
+         ]);
+
+         while ($success && $row = $iterator->next()) {
+            $pass = Toolbox::encrypt(Toolbox::decrypt($row['value'], $current_key));
+            $success = $DB->update(
+               Config::getTable(),
+               ['value' => $pass],
+               ['id'    => $row['id']]
+            );
+         }
+      }
+
+      return $success;
+   }
+}
diff --git a/inc/glpimailer.class.php b/inc/glpimailer.class.php
index 6bb85b8cde6..9ab42920634 100644
--- a/inc/glpimailer.class.php
+++ b/inc/glpimailer.class.php
@@ -63,11 +63,11 @@ class GLPIMailer extends PHPMailer {
          $this->Host   = $CFG_GLPI['smtp_host'].':'.$CFG_GLPI['smtp_port'];
 
          if ($CFG_GLPI['smtp_username'] != '') {
             $this->SMTPAuth = true;
             $this->Username = $CFG_GLPI['smtp_username'];
-            $this->Password = Toolbox::decrypt($CFG_GLPI['smtp_passwd'], GLPIKEY);
+            $this->Password = Toolbox::decrypt($CFG_GLPI['smtp_passwd']);
          }
 
          if ($CFG_GLPI['smtp_mode'] == MAIL_SMTPSSL) {
             $this->SMTPSecure = "ssl";
          }
diff --git a/inc/mailcollector.class.php b/inc/mailcollector.class.php
index 4f7d8e4f3fa..85c498fc958 100644
--- a/inc/mailcollector.class.php
+++ b/inc/mailcollector.class.php
@@ -143,11 +143,11 @@ class MailCollector  extends CommonDBTM {
 
       if (isset($input["passwd"])) {
          if (empty($input["passwd"])) {
             unset($input["passwd"]);
          } else {
-            $input["passwd"] = Toolbox::encrypt(stripslashes($input["passwd"]), GLPIKEY);
+            $input["passwd"] = Toolbox::encrypt(stripslashes($input["passwd"]));
          }
       }
 
       if (isset($input["_blank_passwd"]) && $input["_blank_passwd"]) {
          $input['passwd'] = '';
@@ -172,11 +172,11 @@ class MailCollector  extends CommonDBTM {
 
       if (isset($input["passwd"])) {
          if (empty($input["passwd"])) {
             unset($input["passwd"]);
          } else {
-            $input["passwd"] = Toolbox::encrypt(stripslashes($input["passwd"]), GLPIKEY);
+            $input["passwd"] = Toolbox::encrypt(stripslashes($input["passwd"]));
          }
       }
 
       if (isset($input['mail_server']) && !empty($input['mail_server'])) {
          $input["host"] = Toolbox::constructMailServerConfig($input);
@@ -1216,18 +1216,18 @@ class MailCollector  extends CommonDBTM {
    **/
    function connect() {
 
       if ($this->fields['use_kerberos']) {
          $this->marubox = @imap_open($this->fields['host'], $this->fields['login'],
-                                     Toolbox::decrypt($this->fields['passwd'], GLPIKEY),
+                                     Toolbox::decrypt($this->fields['passwd']),
                                      CL_EXPUNGE, 1);
       } else {
          $try_options = [['DISABLE_AUTHENTICATOR' => 'GSSAPI'],
                               ['DISABLE_AUTHENTICATOR' => 'PLAIN']];
          foreach ($try_options as $option) {
             $this->marubox = @imap_open($this->fields['host'], $this->fields['login'],
-                                        Toolbox::decrypt($this->fields['passwd'], GLPIKEY),
+                                        Toolbox::decrypt($this->fields['passwd']),
                                         CL_EXPUNGE, 1, $option);
             if (is_resource($this->marubox)) {
                break;
             }
          }
diff --git a/inc/rssfeed.class.php b/inc/rssfeed.class.php
index cce88015ba7..d006b34c23e 100644
--- a/inc/rssfeed.class.php
+++ b/inc/rssfeed.class.php
@@ -878,12 +878,11 @@ class RSSFeed extends CommonDBVisible {
          $prx_opt[CURLOPT_PROXY]     = $CFG_GLPI["proxy_name"];
          $prx_opt[CURLOPT_PROXYPORT] = $CFG_GLPI["proxy_port"];
          if (!empty($CFG_GLPI["proxy_user"])) {
             $prx_opt[CURLOPT_HTTPAUTH]     = CURLAUTH_ANYSAFE;
             $prx_opt[CURLOPT_PROXYUSERPWD] = $CFG_GLPI["proxy_user"].":".
-                                             Toolbox::decrypt($CFG_GLPI["proxy_passwd"],
-                                                              GLPIKEY);
+                                             Toolbox::decrypt($CFG_GLPI["proxy_passwd"]);
          }
          $feed->set_curl_options($prx_opt);
       }
 
       $feed->enable_cache(true);
diff --git a/inc/toolbox.class.php b/inc/toolbox.class.php
index 6ae1d01a25f..1002af560ba 100644
--- a/inc/toolbox.class.php
+++ b/inc/toolbox.class.php
@@ -237,11 +237,19 @@ class Toolbox {
     * @param $string    string to encrypt
     * @param $key       string key used to encrypt
     *
     * @return encrypted string
    **/
-   static function encrypt($string, $key) {
+   static function encrypt($string, $key = null) {
+
+      if ($key === null) {
+         $key = self::getGlpiSecKey();
+      }
+
+      if ($key === GLPIKEY && !defined('TU_USER')) {
+         self::deprecated('Using GLPIKEY is not secure!');
+      }
 
       $result = '';
       for ($i=0; $i<strlen($string); $i++) {
          $char    = substr($string, $i, 1);
          $keychar = substr($key, ($i % strlen($key))-1, 1);
@@ -258,11 +266,15 @@ class Toolbox {
     * @param $string    string to decrypt
     * @param $key       string key used to decrypt
     *
     * @return decrypted string
    **/
-   static function decrypt($string, $key) {
+   static function decrypt($string, $key = null) {
+
+      if ($key === null) {
+         $key = self::getGlpiSecKey();
+      }
 
       $result = '';
       $string = base64_decode($string);
 
       for ($i=0; $i<strlen($string); $i++) {
@@ -273,10 +285,23 @@ class Toolbox {
       }
 
       return Toolbox::unclean_cross_side_scripting_deep($result);
    }
 
+   /**
+    * Get GLPI security key used for decryptable passwords
+    *
+    * Will read key from config/glpi.key if present.
+    * For 9.4 branch, this will defaults to GLPIKEY.
+    *
+    * @return string
+    */
+   public static function getGlpiSecKey() {
+      $glpikey = new GLPIKey();
+      return $glpikey->get();
+   }
+
 
    /**
     * Prevent from XSS
     * Clean code
     *
@@ -1735,11 +1760,11 @@ class Toolbox {
          ];
 
          if (!empty($CFG_GLPI["proxy_user"])) {
             $opts += [
                CURLOPT_PROXYAUTH    => CURLAUTH_BASIC,
-               CURLOPT_PROXYUSERPWD => $CFG_GLPI["proxy_user"] . ":" . self::decrypt($CFG_GLPI["proxy_passwd"], GLPIKEY),
+               CURLOPT_PROXYUSERPWD => $CFG_GLPI["proxy_user"] . ":" . self::decrypt($CFG_GLPI["proxy_passwd"]),
             ];
          }
 
          if ($defaultport == 443) {
             $opts += [
diff --git a/inc/update.class.php b/inc/update.class.php
index 2db9bac6f88..68167426298 100644
--- a/inc/update.class.php
+++ b/inc/update.class.php
@@ -161,11 +161,12 @@ class Update extends CommonGLPI {
       ini_set("max_execution_time", "0");
 
       $updir = __DIR__ . "/../install/";
 
       if (isCommandLine() && version_compare($current_version, '0.72.3', 'lt')) {
-         die('Upgrade from command line is not supported before 0.72.3!');
+         echo 'Upgrade from command line is not supported before 0.72.3!';
+         die(1);
       }
 
       // Update process desactivate all plugins
       $plugin = new Plugin();
       $plugin->unactivateAll();
@@ -498,10 +499,16 @@ class Update extends CommonGLPI {
       // reset telemetry
       $crontask_telemetry = new CronTask;
       $crontask_telemetry->getFromDBbyName("Telemetry", "telemetry");
       $crontask_telemetry->resetDate();
       $crontask_telemetry->resetState();
+
+      //generate security key if missing, and update db
+      $glpikey = new GLPIKey();
+      if (!$glpikey->keyExists() && !$glpikey->generate()) {
+         $this->migration->displayWarning(__('Unable to create security key file!'), true);
+      }
    }
 
    /**
     * Declare old items for compatibility
     *
diff --git a/install/install.php b/install/install.php
index 10752054931..57147158fef 100644
--- a/install/install.php
+++ b/install/install.php
@@ -373,10 +373,22 @@ function step4 ($databasename, $newdatabasename) {
       echo "<p class='submit'><input type='submit' name='submit' class='submit' value='".
              __('Continue')."'></p>";
       Html::closeForm();
    }
 
+   //create security key
+   $glpikey = new GLPIKey();
+   $secured = $glpikey->keyExists();
+   if (!$secured) {
+      $secured = $glpikey->generate();
+   }
+   if (!$secured) {
+      echo "<p><strong>".__('Security key cannot be generated!')."</strong></p>";
+      prev_form($host, $user, $password);
+      return;
+   }
+
    //Check if the port is in url
    $hostport = explode(":", $host);
    if (count($hostport) < 2) {
       $link = new mysqli($hostport[0], $user, $password);
    } else {
diff --git a/status.php b/status.php
index 064f920ea6b..63cd5928124 100644
--- a/status.php
+++ b/status.php
@@ -102,12 +102,11 @@ if (($ok_master || $ok_slave )
       echo "Check LDAP servers:";
 
       foreach ($ldap_methods as $method) {
          echo " ".$method['name'];
          if (AuthLDAP::tryToConnectToServer($method, $method["rootdn"],
-                                            Toolbox::decrypt($method["rootdn_passwd"],
-                                            GLPIKEY))) {
+                                            Toolbox::decrypt($method["rootdn_passwd"]))) {
             echo "_OK";
          } else {
             echo "_PROBLEM";
             $ok = false;
          }
diff --git a/tests/functionnal/AuthLdap.php b/tests/functionnal/AuthLdap.php
index 557729b34fb..d52d9d75d9e 100644
--- a/tests/functionnal/AuthLdap.php
+++ b/tests/functionnal/AuthLdap.php
@@ -157,20 +157,20 @@ class AuthLDAP extends DbTestCase {
       //rootdn_passwd is set with a value (a password, not encrypted)
       $password = 'toto';
       $input    = ['name' => 'ldap', 'rootdn_passwd' => $password];
       $result   = $ldap->prepareInputForUpdate($input);
 
-      //Expected value to be encrypted using GLPIKEY key
-      $expected = \Toolbox::encrypt(stripslashes($password), GLPIKEY);
+      //Expected value to be encrypted using current  key
+      $expected = \Toolbox::encrypt(stripslashes($password));
       $this->string($result['rootdn_passwd'])->isIdenticalTo($expected);
 
       $password = 'tot\'o';
       $input    = ['name' => 'ldap', 'rootdn_passwd' => $password];
       $result   = $ldap->prepareInputForUpdate($input);
 
-      //Expected value to be encrypted using GLPIKEY key
-      $expected = \Toolbox::encrypt(stripslashes($password), GLPIKEY);
+      //Expected value to be encrypted using current key
+      $expected = \Toolbox::encrypt(stripslashes($password));
       $this->string($result['rootdn_passwd'])->isIdenticalTo($expected);
 
       $input['_blank_passwd'] = 1;
       $result   = $ldap->prepareInputForUpdate($input);
       //rootdn_passwd is set but empty

Vulnerable code:                $input["passwd"] = Toolbox::encrypt(stripslashes($input["passwd"]), GLPIKEY);
                                                  Toolbox::decrypt($ldap_method["rootdn_passwd"],
                                                                   GLPIKEY),
            $input["rootdn_passwd"] = Toolbox::encrypt(stripslashes($input["rootdn_passwd"]),
                                                       GLPIKEY);
                                  Toolbox::decrypt($config_ldap->fields['rootdn_passwd'], GLPIKEY),
                                    Toolbox::decrypt($this->fields['rootdn_passwd'], GLPIKEY),
                                  Toolbox::decrypt($ldap_method['rootdn_passwd'], GLPIKEY),
                                        Toolbox::decrypt($ldap_method['rootdn_passwd'], GLPIKEY),
                                Toolbox::decrypt($authldap->getField('rootdn_passwd'), GLPIKEY),
         $input["rootdn_passwd"] = Toolbox::encrypt(stripslashes($input["rootdn_passwd"]), GLPIKEY);
            $input["smtp_passwd"] = Toolbox::encrypt(stripslashes($input["smtp_passwd"]), GLPIKEY);
            $input["proxy_passwd"] = Toolbox::encrypt(stripslashes($input["proxy_passwd"]),
                                                      GLPIKEY);
            $this->Password = Toolbox::decrypt($CFG_GLPI['smtp_passwd'], GLPIKEY);
            $input["passwd"] = Toolbox::encrypt(stripslashes($input["passwd"]), GLPIKEY);
            $input["passwd"] = Toolbox::encrypt(stripslashes($input["passwd"]), GLPIKEY);
                                     Toolbox::decrypt($this->fields['passwd'], GLPIKEY),
                                        Toolbox::decrypt($this->fields['passwd'], GLPIKEY),
                                             Toolbox::decrypt($CFG_GLPI["proxy_passwd"],
                                                              GLPIKEY);
   static function encrypt($string, $key) {
   static function decrypt($string, $key) {
               CURLOPT_PROXYUSERPWD => $CFG_GLPI["proxy_user"] . ":" . self::decrypt($CFG_GLPI["proxy_passwd"], GLPIKEY),
         die('Upgrade from command line is not supported before 0.72.3!');
                                            Toolbox::decrypt($method["rootdn_passwd"],
                                            GLPIKEY))) {
      //Expected value to be encrypted using GLPIKEY key
      $expected = \Toolbox::encrypt(stripslashes($password), GLPIKEY);
      //Expected value to be encrypted using GLPIKEY key
      $expected = \Toolbox::encrypt(stripslashes($password), GLPIKEY);
Fixed code: /config/glpi.key
               $input["passwd"] = Toolbox::encrypt(stripslashes($input["passwd"]));
                                                  Toolbox::decrypt($ldap_method["rootdn_passwd"]),
            $input["rootdn_passwd"] = Toolbox::encrypt(stripslashes($input["rootdn_passwd"]));
                                  Toolbox::decrypt($config_ldap->fields['rootdn_passwd']),
                                    Toolbox::decrypt($this->fields['rootdn_passwd']),
                                  Toolbox::decrypt($ldap_method['rootdn_passwd']),
                                        Toolbox::decrypt($ldap_method['rootdn_passwd']),
                                Toolbox::decrypt($authldap->getField('rootdn_passwd')),
         $input["rootdn_passwd"] = Toolbox::encrypt(stripslashes($input["rootdn_passwd"]));
            $input["smtp_passwd"] = Toolbox::encrypt(stripslashes($input["smtp_passwd"]));
            $input["proxy_passwd"] = Toolbox::encrypt(stripslashes($input["proxy_passwd"]));
<?php
/**
 * ---------------------------------------------------------------------
 * GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2015-2018 Teclib' and contributors.
 *
 * http://glpi-project.org
 *
 * based on GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2003-2014 by the INDEPNET Development Team.
 *
 * ---------------------------------------------------------------------
 *
 * LICENSE
 *
 * This file is part of GLPI.
 *
 * GLPI is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * GLPI is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with GLPI. If not, see <http://www.gnu.org/licenses/>.
 * ---------------------------------------------------------------------
 */

namespace Glpi\Console\Security;

if (!defined('GLPI_ROOT')) {
   die("Sorry. You can't access this file directly");
}

use Glpi\Console\AbstractCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use GLPIKey;

class ChangekeyCommand extends AbstractCommand {
   /**
    * Error code returned when unable to renew key.
    *
    * @var integer
    */
   const ERROR_UNABLE_TO_RENEW_KEY = 1;

   protected function configure() {
      parent::configure();

      $this->setName('glpi:security:change_key');
      $this->setDescription(__('Change password storage key and update values in database.'));
   }

   protected function execute(InputInterface $input, OutputInterface $output) {
      $glpikey = new GLPIKey();

      $fields = $glpikey->getFields();
      $configs = $glpikey->getConfigs();
      $conf_count = 0;
      foreach ($configs as $config) {
         $conf_count += count($config);
      }

      $output->writeln(
         sprintf(
            '<info>' . __('Found %1$s field(s) and %2$s configuration entries requiring migration.') . '</info>',
            count($fields),
            $conf_count
         )
      );

      if (!$input->getOption('no-interaction')) {
         // Ask for confirmation (unless --no-interaction)
         $question_helper = $this->getHelper('question');
         $run = $question_helper->ask(
            $input,
            $output,
            new ConfirmationQuestion(__('Do you want to continue ?') . ' [Yes/no]', true)
         );
         if (!$run) {
            $output->writeln(
               '<comment>' . __('Aborted.') . '</comment>',
               OutputInterface::VERBOSITY_VERBOSE
            );
            return 0;
         }
      }

      $created = $glpikey->generate();
      if (!$created) {
         $output->writeln(
            '<error>' . __('Unable to change security key!') . '</error>',
            OutputInterface::VERBOSITY_QUIET
         );
         return self::ERROR_UNABLE_TO_RENEW_KEY;
      }

      $this->output->write(PHP_EOL);

      $output->writeln('<info>' . __('New security key generated; database updated.') . '</info>');

      return 0; // Success
   }
}
<?php
/**
 * ---------------------------------------------------------------------
 * GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2015-2018 Teclib' and contributors.
 *
 * http://glpi-project.org
 *
 * based on GLPI - Gestionnaire Libre de Parc Informatique
 * Copyright (C) 2003-2014 by the INDEPNET Development Team.
 *
 * ---------------------------------------------------------------------
 *
 * LICENSE
 *
 * This file is part of GLPI.
 *
 * GLPI is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * GLPI is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with GLPI. If not, see <http://www.gnu.org/licenses/>.
 * ---------------------------------------------------------------------
 */

if (!defined('GLPI_ROOT')) {
   die("Sorry. You can't access this file directly");
}

/**
 *  GLPI security key
**/
class GLPIKey {
   /**
    * Key file path.
    *
    * @var string
    */
   private $keyfile;

   /**
    * List of crypted DB fields.
    *
    * @var array
    */
   protected $fields = [
      'glpi_mailcollectors.passwd',
      'glpi_authldaps.rootdn_passwd'
   ];

   /**
    * List of crypted configuration values.
    * Each key corresponds to a configuration context, and contains list of configs names.
    *
    * @var array
    */
   protected $configs = [
      'core'   => [
         'smtp_passwd',
         'proxy_passwd'
      ]
   ];

   public function __construct() {
      $this->keyfile = GLPI_CONFIG_DIR . '/glpi.key';
   }

   /**
    * Check if GLPI security key used for decryptable passwords exists
    *
    * @return string
    */
   public function keyExists() {
      return file_exists($this->keyfile) && !empty($this->get());
   }

   /**
    * Get GLPI security key used for decryptable passwords
    *
    * @return string
    */
   public function get() {
      $key = GLPIKEY;
      if (file_exists($this->keyfile)) {
         //load key from existing config file
         $key = file_get_contents($this->keyfile);
      }
      return $key;
   }

   /**
    * Generate GLPI security key used for decryptable passwords
    * and update values in DB if necessary.
    *
    * @return boolean
    */
   public function generate() {
      global $DB;

      $current_key = file_exists($this->keyfile) ? $this->get() : GLPIKEY;

      $success = (bool)file_put_contents($this->keyfile, Toolbox::getRandomString(50));
      if (!$success) {
         return false;
      }

      if ($DB instanceof DBmysql) {
         return $this->migrateFieldsInDb($current_key)
            && $this->migrateConfigsInDb($current_key);
      }

      return true;
   }

   /**
    * Get fields
    *
    * @return array
    */
   public function getFields() {
      global $PLUGIN_HOOKS;

      $fields = $this->fields;
      if (isset($PLUGIN_HOOKS['secured_fields'])) {
         foreach ($PLUGIN_HOOKS['secured_fields'] as $plugfields) {
            $fields = array_merge($fields, $plugfields);
         }
      }

      return $fields;
   }

   /**
    * Get configs
    *
    * @return array
    */
   public function getConfigs() {
      global $PLUGIN_HOOKS;

      $configs = $this->configs;

      if (isset($PLUGIN_HOOKS['secured_configs'])) {
         foreach ($PLUGIN_HOOKS['secured_configs'] as $plugin => $plugconfigs) {
            $configs['plugin:' . $plugin] = $plugconfigs;
         }
      }

      return $configs;
   }

   /**
    * Migrate fields in database
    *
    * @param string $current_key Current key
    *
    * @return void
    */
   protected function migrateFieldsInDb($current_key) {
      global $DB;

      $success = true;

      foreach ($this->getFields() as $field) {
         list($table, $column) = explode('.', $field);

         $iterator = $DB->request([
            'SELECT' => ['id', $column],
            'FROM'   => $table
         ]);

         while ($success && $row = $iterator->next()) {
            $pass = Toolbox::encrypt(Toolbox::decrypt($row[$column], $current_key));
            $success = $DB->update(
               $table,
               [$field  => $pass],
               ['id'    => $row['id']]
            );
         }
      }

      return $success;
   }

   /**
    * Migrate configurations in database
    *
    * @param string $current_key Current key
    *
    * @return boolean
    */
   protected function migrateConfigsInDb($current_key) {
      global $DB;

      $success = true;

      foreach ($this->getConfigs() as $context => $names) {
         $iterator = $DB->request([
            'FROM'   => Config::getTable(),
            'WHERE'  => [
               'context'   => $context,
               'name'      => $names
            ]
         ]);

         while ($success && $row = $iterator->next()) {
            $pass = Toolbox::encrypt(Toolbox::decrypt($row['value'], $current_key));
            $success = $DB->update(
               Config::getTable(),
               ['value' => $pass],
               ['id'    => $row['id']]
            );
         }
      }

      return $success;
   }
}
            $this->Password = Toolbox::decrypt($CFG_GLPI['smtp_passwd']);
            $input["passwd"] = Toolbox::encrypt(stripslashes($input["passwd"]));
            $input["passwd"] = Toolbox::encrypt(stripslashes($input["passwd"]));
                                     Toolbox::decrypt($this->fields['passwd']),
                                        Toolbox::decrypt($this->fields['passwd']),
                                             Toolbox::decrypt($CFG_GLPI["proxy_passwd"]);
   static function encrypt($string, $key = null) {

      if ($key === null) {
         $key = self::getGlpiSecKey();
      }

      if ($key === GLPIKEY && !defined('TU_USER')) {
         self::deprecated('Using GLPIKEY is not secure!');
      }
   static function decrypt($string, $key = null) {

      if ($key === null) {
         $key = self::getGlpiSecKey();
      }
   /**
    * Get GLPI security key used for decryptable passwords
    *
    * Will read key from config/glpi.key if present.
    * For 9.4 branch, this will defaults to GLPIKEY.
    *
    * @return string
    */
   public static function getGlpiSecKey() {
      $glpikey = new GLPIKey();
      return $glpikey->get();
   }

               CURLOPT_PROXYUSERPWD => $CFG_GLPI["proxy_user"] . ":" . self::decrypt($CFG_GLPI["proxy_passwd"]),
         echo 'Upgrade from command line is not supported before 0.72.3!';
         die(1);

      //generate security key if missing, and update db
      $glpikey = new GLPIKey();
      if (!$glpikey->keyExists() && !$glpikey->generate()) {
         $this->migration->displayWarning(__('Unable to create security key file!'), true);
      }
   //create security key
   $glpikey = new GLPIKey();
   $secured = $glpikey->keyExists();
   if (!$secured) {
      $secured = $glpikey->generate();
   }
   if (!$secured) {
      echo "<p><strong>".__('Security key cannot be generated!')."</strong></p>";
      prev_form($host, $user, $password);
      return;
   }

                                            Toolbox::decrypt($method["rootdn_passwd"]))) {
      //Expected value to be encrypted using current  key
      $expected = \Toolbox::encrypt(stripslashes($password));
      //Expected value to be encrypted using current key
      $expected = \Toolbox::encrypt(stripslashes($password));