* @category Horde * @copyright 2015-2017 Horde LLC * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1 * @package Imap_Client * @since 2.29.0 */ class Horde_Imap_Client_Auth_Scram { /** * AuthMessage (RFC 5802 [3]). * * @var string */ protected $_authmsg; /** * Hash name. * * @var string */ protected $_hash; /** * Number of Hi iterations (RFC 5802 [2]). * * @var integer */ protected $_iterations; /** * Nonce. * * @var string */ protected $_nonce; /** * Password. * * @var string */ protected $_pass; /** * Server salt. * * @var string */ protected $_salt; /** * Calculated server signature value. * * @var string */ protected $_serversig; /** * Username. * * @var string */ protected $_user; /** * Constructor. * * @param string $user Username. * @param string $pass Password. * @param string $hash Hash name. * * @throws Horde_Imap_Client_Exception */ public function __construct($user, $pass, $hash = 'SHA1') { $error = false; $this->_hash = $hash; try { if (!class_exists('Horde_Stringprep') || !class_exists('Horde_Crypt_Blowfish_Pbkdf2')) { throw new Exception(); } Horde_Stringprep::autoload(); $saslprep = new Znerol\Component\Stringprep\Profile\SASLprep(); $this->_user = $saslprep->apply( $user, 'UTF-8', Znerol\Component\Stringprep\Profile::MODE_QUERY ); $this->_pass = $saslprep->apply( $pass, 'UTF-8', Znerol\Component\Stringprep\Profile::MODE_STORE ); } catch (Znerol\Component\Stringprep\ProfileException $e) { $error = true; } catch (Exception $e) { $error = true; } if ($error) { throw new Horde_Imap_Client_Exception( Horde_Imap_Client_Translation::r("Authentication failure."), Horde_Imap_Client_Exception::LOGIN_AUTHORIZATIONFAILED ); } /* Generate nonce. (Done here so this can be overwritten for * testing purposes.) */ $this->_nonce = strval(new Horde_Support_Randomid()); } /** * Return the initial client message. * * @return string Initial client message. */ public function getClientFirstMessage() { /* n: client doesn't support channel binding, * , * n=: SASLprepped username with "," and "=" escaped, * r=: Random nonce */ $this->_authmsg = 'n=' . str_replace( array(',', '='), array('=2C', '=3D'), $this->_user ) . ',r=' . $this->_nonce; return 'n,,' . $this->_authmsg; } /** * Process the initial server message response. * * @param string $msg Initial server response. * * @return boolean False if authentication failed at this stage. */ public function parseServerFirstMessage($msg) { $i = $r = $s = false; foreach (explode(',', $msg) as $val) { list($attr, $aval) = array_map('trim', explode('=', $val, 2)); switch ($attr) { case 'i': $this->_iterations = intval($aval); $i = true; break; case 'r': /* Beginning of server-provided nonce MUST be the same as the * nonce we provided. */ if (strpos($aval, $this->_nonce) !== 0) { return false; } $this->_nonce = $aval; $r = true; break; case 's': $this->_salt = base64_decode($aval); $s = true; break; } } if ($i && $r && $s) { $this->_authmsg .= ',' . $msg; return true; } return false; } /** * Return the final client message. * * @return string Final client message. */ public function getClientFinalMessage() { $final_msg = 'c=biws,r=' . $this->_nonce; /* Salted password. */ $s_pass = strval(new Horde_Crypt_Blowfish_Pbkdf2( $this->_pass, strlen(hash($this->_hash, '', true)), array( 'algo' => $this->_hash, 'i_count' => $this->_iterations, 'salt' => $this->_salt ) )); /* Client key. */ $c_key = hash_hmac($this->_hash, 'Client Key', $s_pass, true); /* Stored key. */ $s_key = hash($this->_hash, $c_key, true); /* Client signature. */ $auth_msg = $this->_authmsg . ',' . $final_msg; $c_sig = hash_hmac($this->_hash, $auth_msg, $s_key, true); /* Proof. */ $proof = $c_key ^ $c_sig; /* Server signature. */ $this->_serversig = hash_hmac( $this->_hash, $auth_msg, hash_hmac($this->_hash, 'Server Key', $s_pass, true), true ); /* c=biws: channel-binding ("biws" = base64('n,,')), * p=: base64 encoded ClientProof, * r=: Nonce as returned from the server. */ return $final_msg . ',p=' . base64_encode($proof); } /** * Process the final server message response. * * @param string $msg Final server response. * * @return boolean False if authentication failed. */ public function parseServerFinalMessage($msg) { foreach (explode(',', $msg) as $val) { list($attr, $aval) = array_map('trim', explode('=', $val, 2)); switch ($attr) { case 'e': return false; case 'v': return (base64_decode($aval) === $this->_serversig); } } return false; } }