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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body