Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
   1  <?php
   2  /**
   3   * Copyright 2015-2017 Horde LLC (http://www.horde.org/)
   4   *
   5   * See the enclosed file LICENSE for license information (LGPL). If you
   6   * did not receive this file, see http://www.horde.org/licenses/lgpl21.
   7   *
   8   * @category  Horde
   9   * @copyright 2015-2017 Horde LLC
  10   * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
  11   * @package   Imap_Client
  12   */
  13  
  14  /**
  15   * Provides authentication via the SCRAM SASL mechanism (RFC 5802 [3]).
  16   *
  17   * @author    Michael Slusarz <slusarz@horde.org>
  18   * @category  Horde
  19   * @copyright 2015-2017 Horde LLC
  20   * @license   http://www.horde.org/licenses/lgpl21 LGPL 2.1
  21   * @package   Imap_Client
  22   * @since     2.29.0
  23   */
  24  class Horde_Imap_Client_Auth_Scram
  25  {
  26      /**
  27       * AuthMessage (RFC 5802 [3]).
  28       *
  29       * @var string
  30       */
  31      protected $_authmsg;
  32  
  33      /**
  34       * Hash name.
  35       *
  36       * @var string
  37       */
  38      protected $_hash;
  39  
  40      /**
  41       * Number of Hi iterations (RFC 5802 [2]).
  42       *
  43       * @var integer
  44       */
  45      protected $_iterations;
  46  
  47      /**
  48       * Nonce.
  49       *
  50       * @var string
  51       */
  52      protected $_nonce;
  53  
  54      /**
  55       * Password.
  56       *
  57       * @var string
  58       */
  59      protected $_pass;
  60  
  61      /**
  62       * Server salt.
  63       *
  64       * @var string
  65       */
  66      protected $_salt;
  67  
  68      /**
  69       * Calculated server signature value.
  70       *
  71       * @var string
  72       */
  73      protected $_serversig;
  74  
  75      /**
  76       * Username.
  77       *
  78       * @var string
  79       */
  80      protected $_user;
  81  
  82      /**
  83       * Constructor.
  84       *
  85       * @param string $user  Username.
  86       * @param string $pass  Password.
  87       * @param string $hash  Hash name.
  88       *
  89       * @throws Horde_Imap_Client_Exception
  90       */
  91      public function __construct($user, $pass, $hash = 'SHA1')
  92      {
  93          $error = false;
  94  
  95          $this->_hash = $hash;
  96  
  97          try {
  98              if (!class_exists('Horde_Stringprep') ||
  99                  !class_exists('Horde_Crypt_Blowfish_Pbkdf2')) {
 100                  throw new Exception();
 101              }
 102  
 103              Horde_Stringprep::autoload();
 104              $saslprep = new Znerol\Component\Stringprep\Profile\SASLprep();
 105  
 106              $this->_user = $saslprep->apply(
 107                  $user,
 108                  'UTF-8',
 109                  Znerol\Component\Stringprep\Profile::MODE_QUERY
 110              );
 111              $this->_pass = $saslprep->apply(
 112                  $pass,
 113                  'UTF-8',
 114                  Znerol\Component\Stringprep\Profile::MODE_STORE
 115              );
 116          } catch (Znerol\Component\Stringprep\ProfileException $e) {
 117              $error = true;
 118          } catch (Exception $e) {
 119              $error = true;
 120          }
 121  
 122          if ($error) {
 123              throw new Horde_Imap_Client_Exception(
 124                  Horde_Imap_Client_Translation::r("Authentication failure."),
 125                  Horde_Imap_Client_Exception::LOGIN_AUTHORIZATIONFAILED
 126              );
 127          }
 128  
 129          /* Generate nonce. (Done here so this can be overwritten for
 130           * testing purposes.) */
 131          $this->_nonce = strval(new Horde_Support_Randomid());
 132      }
 133  
 134      /**
 135       * Return the initial client message.
 136       *
 137       * @return string  Initial client message.
 138       */
 139      public function getClientFirstMessage()
 140      {
 141          /* n: client doesn't support channel binding,
 142           * <empty>,
 143           * n=<user>: SASLprepped username with "," and "=" escaped,
 144           * r=<nonce>: Random nonce */
 145          $this->_authmsg = 'n=' . str_replace(
 146              array(',', '='),
 147              array('=2C', '=3D'),
 148              $this->_user
 149          ) . ',r=' . $this->_nonce;
 150  
 151          return 'n,,' . $this->_authmsg;
 152      }
 153  
 154      /**
 155       * Process the initial server message response.
 156       *
 157       * @param string $msg  Initial server response.
 158       *
 159       * @return boolean  False if authentication failed at this stage.
 160       */
 161      public function parseServerFirstMessage($msg)
 162      {
 163          $i = $r = $s = false;
 164  
 165          foreach (explode(',', $msg) as $val) {
 166              list($attr, $aval) = array_map('trim', explode('=', $val, 2));
 167  
 168              switch ($attr) {
 169              case 'i':
 170                  $this->_iterations = intval($aval);
 171                  $i = true;
 172                  break;
 173  
 174              case 'r':
 175                  /* Beginning of server-provided nonce MUST be the same as the
 176                   * nonce we provided. */
 177                  if (strpos($aval, $this->_nonce) !== 0) {
 178                      return false;
 179                  }
 180                  $this->_nonce = $aval;
 181                  $r = true;
 182                  break;
 183  
 184              case 's':
 185                  $this->_salt = base64_decode($aval);
 186                  $s = true;
 187                  break;
 188              }
 189          }
 190  
 191          if ($i && $r && $s) {
 192              $this->_authmsg .= ',' . $msg;
 193              return true;
 194          }
 195  
 196          return false;
 197      }
 198  
 199      /**
 200       * Return the final client message.
 201       *
 202       * @return string  Final client message.
 203       */
 204      public function getClientFinalMessage()
 205      {
 206          $final_msg = 'c=biws,r=' . $this->_nonce;
 207  
 208          /* Salted password. */
 209          $s_pass = strval(new Horde_Crypt_Blowfish_Pbkdf2(
 210              $this->_pass,
 211              strlen(hash($this->_hash, '', true)),
 212              array(
 213                  'algo' => $this->_hash,
 214                  'i_count' => $this->_iterations,
 215                  'salt' => $this->_salt
 216              )
 217          ));
 218  
 219          /* Client key. */
 220          $c_key = hash_hmac($this->_hash, 'Client Key', $s_pass, true);
 221  
 222          /* Stored key. */
 223          $s_key = hash($this->_hash, $c_key, true);
 224  
 225          /* Client signature. */
 226          $auth_msg = $this->_authmsg . ',' . $final_msg;
 227          $c_sig = hash_hmac($this->_hash, $auth_msg, $s_key, true);
 228  
 229          /* Proof. */
 230          $proof = $c_key ^ $c_sig;
 231  
 232          /* Server signature. */
 233          $this->_serversig = hash_hmac(
 234              $this->_hash,
 235              $auth_msg,
 236              hash_hmac($this->_hash, 'Server Key', $s_pass, true),
 237              true
 238          );
 239  
 240          /* c=biws: channel-binding ("biws" = base64('n,,')),
 241           * p=<proof>: base64 encoded ClientProof,
 242           * r=<nonce>: Nonce as returned from the server. */
 243          return $final_msg . ',p=' . base64_encode($proof);
 244      }
 245  
 246      /**
 247       * Process the final server message response.
 248       *
 249       * @param string $msg  Final server response.
 250       *
 251       * @return boolean  False if authentication failed.
 252       */
 253      public function parseServerFinalMessage($msg)
 254      {
 255          foreach (explode(',', $msg) as $val) {
 256              list($attr, $aval) = array_map('trim', explode('=', $val, 2));
 257  
 258              switch ($attr) {
 259              case 'e':
 260                  return false;
 261  
 262              case 'v':
 263                  return (base64_decode($aval) === $this->_serversig);
 264              }
 265          }
 266  
 267          return false;
 268      }
 269  
 270  }