├── .gitignore ├── .travis.yml ├── Auth ├── SASL.php └── SASL │ ├── Anonymous.php │ ├── Common.php │ ├── CramMD5.php │ ├── DigestMD5.php │ ├── External.php │ ├── Login.php │ ├── Plain.php │ └── SCRAM.php ├── README.md ├── composer.json ├── package.xml └── phpunit.xml.dist /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | .*~ 3 | 4 | *.tgz 5 | 6 | # composer related 7 | composer.lock 8 | composer.phar 9 | vendor 10 | 11 | # Eclipse 12 | .buildpath 13 | .project 14 | .settings/ 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - nightly 5 | - 8.1 6 | - 8.0 7 | - 7.4 8 | - 7.3 9 | - 7.2 10 | - 7.1 11 | - 7.0 12 | - 5.6 13 | arch: 14 | - amd64 15 | 16 | jobs: 17 | fast_finish: true 18 | allow_failures: 19 | - php: nightly 20 | include: 21 | - php: 5.5 22 | dist: trusty 23 | arch: amd64 24 | - php: 5.4 25 | dist: precise 26 | arch: amd64 27 | 28 | script: 29 | - pear list 30 | - pear channel-update pear.php.net 31 | - pear upgrade --force pear/pear 32 | - pear list 33 | - pear install --force package.xml 34 | - pear list 35 | - pear package 36 | - pear package-validate 37 | - pear install --force *.tgz 38 | - pear list 39 | - composer install 40 | -------------------------------------------------------------------------------- /Auth/SASL.php: -------------------------------------------------------------------------------- 1 | | 33 | // +-----------------------------------------------------------------------+ 34 | // 35 | // $Id$ 36 | 37 | /** 38 | * Client implementation of various SASL mechanisms 39 | * 40 | * @author Richard Heyes 41 | * @access public 42 | * @version 1.0 43 | * @package Auth_SASL 44 | */ 45 | 46 | require_once('PEAR.php'); 47 | 48 | class Auth_SASL 49 | { 50 | /** 51 | * Factory class. Returns an object of the request 52 | * type. 53 | * 54 | * @param string $type One of: Anonymous 55 | * Login (DEPRECATED) 56 | * Plain 57 | * External 58 | * CramMD5 (DEPRECATED) 59 | * DigestMD5 (DEPRECATED) 60 | * SCRAM-* (any mechanism of the SCRAM family) 61 | * Types are not case sensitive 62 | */ 63 | public static function factory($type) 64 | { 65 | switch (strtolower($type)) { 66 | case 'anonymous': 67 | $filename = 'Auth/SASL/Anonymous.php'; 68 | $classname = 'Auth_SASL_Anonymous'; 69 | break; 70 | 71 | case 'login': 72 | /* TODO trigger deprecation warning in 2.0.0 and remove LOGIN authentication in 3.0.0 73 | trigger_error(__CLASS__ . ': Authentication method LOGIN' . 74 | ' is no longer secure and should be avoided.', E_USER_DEPRECATED); 75 | */ 76 | $filename = 'Auth/SASL/Login.php'; 77 | $classname = 'Auth_SASL_Login'; 78 | break; 79 | 80 | case 'plain': 81 | $filename = 'Auth/SASL/Plain.php'; 82 | $classname = 'Auth_SASL_Plain'; 83 | break; 84 | 85 | case 'external': 86 | $filename = 'Auth/SASL/External.php'; 87 | $classname = 'Auth_SASL_External'; 88 | break; 89 | 90 | case 'crammd5': 91 | // $msg = 'Deprecated mechanism name. Use IANA-registered name: CRAM-MD5.'; 92 | // trigger_error($msg, E_USER_DEPRECATED); 93 | case 'cram-md5': 94 | /* TODO trigger deprecation warning in 2.0.0 and remove CRAM-MD5 authentication in 3.0.0 95 | trigger_error(__CLASS__ . ': Authentication method CRAM-MD5' . 96 | ' is no longer secure and should be avoided.', E_USER_DEPRECATED); 97 | */ 98 | $filename = 'Auth/SASL/CramMD5.php'; 99 | $classname = 'Auth_SASL_CramMD5'; 100 | break; 101 | 102 | case 'digestmd5': 103 | // $msg = 'Deprecated mechanism name. Use IANA-registered name: DIGEST-MD5.'; 104 | // trigger_error($msg, E_USER_DEPRECATED); 105 | case 'digest-md5': 106 | /* TODO trigger deprecation warning in 2.0.0 and remove DIGEST-MD5 authentication in 3.0.0 107 | trigger_error(__CLASS__ . ': Authentication method DIGEST-MD5' . 108 | ' is no longer secure and should be avoided.', E_USER_DEPRECATED); 109 | */ 110 | $filename = 'Auth/SASL/DigestMD5.php'; 111 | $classname = 'Auth_SASL_DigestMD5'; 112 | break; 113 | 114 | default: 115 | $scram = '/^SCRAM-(.{1,9})$/i'; 116 | if (preg_match($scram, $type, $matches)) 117 | { 118 | $hash = $matches[1]; 119 | $filename = __DIR__ .'/SASL/SCRAM.php'; 120 | $classname = 'Auth_SASL_SCRAM'; 121 | $parameter = $hash; 122 | break; 123 | } 124 | return PEAR::raiseError('Invalid SASL mechanism type'); 125 | break; 126 | } 127 | 128 | require_once($filename); 129 | if (isset($parameter)) 130 | $obj = new $classname($parameter); 131 | else 132 | $obj = new $classname(); 133 | return $obj; 134 | } 135 | } 136 | 137 | ?> 138 | -------------------------------------------------------------------------------- /Auth/SASL/Anonymous.php: -------------------------------------------------------------------------------- 1 | | 33 | // +-----------------------------------------------------------------------+ 34 | // 35 | // $Id$ 36 | 37 | /** 38 | * Implmentation of ANONYMOUS SASL mechanism 39 | * 40 | * @author Richard Heyes 41 | * @access public 42 | * @version 1.0 43 | * @package Auth_SASL 44 | */ 45 | 46 | require_once('Auth/SASL/Common.php'); 47 | 48 | class Auth_SASL_Anonymous extends Auth_SASL_Common 49 | { 50 | /** 51 | * Not much to do here except return the token supplied. 52 | * No encoding, hashing or encryption takes place for this 53 | * mechanism, simply one of: 54 | * o An email address 55 | * o An opaque string not containing "@" that can be interpreted 56 | * by the sysadmin 57 | * o Nothing 58 | * 59 | * We could have some logic here for the second option, but this 60 | * would by no means create something interpretable. 61 | * 62 | * @param string $token Optional email address or string to provide 63 | * as trace information. 64 | * @return string The unaltered input token 65 | */ 66 | function getResponse($token = '') 67 | { 68 | return $token; 69 | } 70 | } 71 | ?> -------------------------------------------------------------------------------- /Auth/SASL/Common.php: -------------------------------------------------------------------------------- 1 | | 33 | // +-----------------------------------------------------------------------+ 34 | // 35 | // $Id$ 36 | 37 | /** 38 | * Common functionality to SASL mechanisms 39 | * 40 | * @author Richard Heyes 41 | * @access public 42 | * @version 1.0 43 | * @package Auth_SASL 44 | */ 45 | 46 | class Auth_SASL_Common 47 | { 48 | /** 49 | * Function which implements HMAC MD5 digest 50 | * 51 | * @param string $key The secret key 52 | * @param string $data The data to hash 53 | * @param bool $raw_output Whether the digest is returned in binary or hexadecimal format. 54 | * 55 | * @return string The HMAC-MD5 digest 56 | */ 57 | function _HMAC_MD5($key, $data, $raw_output = FALSE) 58 | { 59 | if (strlen($key) > 64) { 60 | $key = pack('H32', md5($key)); 61 | } 62 | 63 | if (strlen($key) < 64) { 64 | $key = str_pad($key, 64, chr(0)); 65 | } 66 | 67 | $k_ipad = substr($key, 0, 64) ^ str_repeat(chr(0x36), 64); 68 | $k_opad = substr($key, 0, 64) ^ str_repeat(chr(0x5C), 64); 69 | 70 | $inner = pack('H32', md5($k_ipad . $data)); 71 | $digest = md5($k_opad . $inner, $raw_output); 72 | 73 | return $digest; 74 | } 75 | 76 | /** 77 | * Function which implements HMAC-SHA-1 digest 78 | * 79 | * @param string $key The secret key 80 | * @param string $data The data to hash 81 | * @param bool $raw_output Whether the digest is returned in binary or hexadecimal format. 82 | * @return string The HMAC-SHA-1 digest 83 | * @author Jehan 84 | * @access protected 85 | */ 86 | protected function _HMAC_SHA1($key, $data, $raw_output = FALSE) 87 | { 88 | if (strlen($key) > 64) { 89 | $key = sha1($key, TRUE); 90 | } 91 | 92 | if (strlen($key) < 64) { 93 | $key = str_pad($key, 64, chr(0)); 94 | } 95 | 96 | $k_ipad = substr($key, 0, 64) ^ str_repeat(chr(0x36), 64); 97 | $k_opad = substr($key, 0, 64) ^ str_repeat(chr(0x5C), 64); 98 | 99 | $inner = pack('H40', sha1($k_ipad . $data)); 100 | $digest = sha1($k_opad . $inner, $raw_output); 101 | 102 | return $digest; 103 | } 104 | } 105 | ?> 106 | -------------------------------------------------------------------------------- /Auth/SASL/CramMD5.php: -------------------------------------------------------------------------------- 1 | | 33 | // +-----------------------------------------------------------------------+ 34 | // 35 | // $Id$ 36 | 37 | /** 38 | * Implmentation of CRAM-MD5 SASL mechanism 39 | * 40 | * @author Richard Heyes 41 | * @access public 42 | * @version 1.0 43 | * @deprecated since 1.2.0 44 | * @package Auth_SASL 45 | */ 46 | 47 | require_once('Auth/SASL/Common.php'); 48 | 49 | class Auth_SASL_CramMD5 extends Auth_SASL_Common 50 | { 51 | /** 52 | * Implements the CRAM-MD5 SASL mechanism 53 | * This DOES NOT base64 encode the return value, 54 | * you will need to do that yourself. 55 | * 56 | * @param string $user Username 57 | * @param string $pass Password 58 | * @param string $challenge The challenge supplied by the server. 59 | * this should be already base64_decoded. 60 | * 61 | * @return string The string to pass back to the server, of the form 62 | * " ". This is NOT base64_encoded. 63 | */ 64 | function getResponse($user, $pass, $challenge) 65 | { 66 | return $user . ' ' . $this->_HMAC_MD5($pass, $challenge); 67 | } 68 | } 69 | ?> -------------------------------------------------------------------------------- /Auth/SASL/DigestMD5.php: -------------------------------------------------------------------------------- 1 | | 33 | // +-----------------------------------------------------------------------+ 34 | // 35 | // $Id$ 36 | 37 | /** 38 | * Implmentation of DIGEST-MD5 SASL mechanism 39 | * 40 | * @author Richard Heyes 41 | * @access public 42 | * @version 1.0 43 | * @deprecated since 1.2.0 44 | * @package Auth_SASL 45 | */ 46 | 47 | require_once('Auth/SASL/Common.php'); 48 | 49 | class Auth_SASL_DigestMD5 extends Auth_SASL_Common 50 | { 51 | /** 52 | * Provides the (main) client response for DIGEST-MD5 53 | * requires a few extra parameters than the other 54 | * mechanisms, which are unavoidable. 55 | * 56 | * @param string $authcid Authentication id (username) 57 | * @param string $pass Password 58 | * @param string $challenge The digest challenge sent by the server 59 | * @param string $hostname The hostname of the machine you're connecting to 60 | * @param string $service The servicename (eg. imap, pop, acap etc) 61 | * @param string $authzid Authorization id (username to proxy as) 62 | * @return string The digest response (NOT base64 encoded) 63 | * @access public 64 | */ 65 | function getResponse($authcid, $pass, $challenge, $hostname, $service, $authzid = '') 66 | { 67 | $challenge = $this->_parseChallenge($challenge); 68 | $authzid_string = ''; 69 | if ($authzid != '') { 70 | $authzid_string = ',authzid="' . $authzid . '"'; 71 | } 72 | 73 | if (!empty($challenge)) { 74 | $cnonce = $this->_getCnonce(); 75 | $digest_uri = sprintf('%s/%s', $service, $hostname); 76 | $response_value = $this->_getResponseValue($authcid, $pass, $challenge['realm'], $challenge['nonce'], $cnonce, $digest_uri, $authzid); 77 | 78 | if ($challenge['realm']) { 79 | return sprintf('username="%s",realm="%s"' . $authzid_string . 80 | ',nonce="%s",cnonce="%s",nc=00000001,qop=auth,digest-uri="%s",response=%s,maxbuf=%d', $authcid, $challenge['realm'], $challenge['nonce'], $cnonce, $digest_uri, $response_value, $challenge['maxbuf']); 81 | } else { 82 | return sprintf('username="%s"' . $authzid_string . ',nonce="%s",cnonce="%s",nc=00000001,qop=auth,digest-uri="%s",response=%s,maxbuf=%d', $authcid, $challenge['nonce'], $cnonce, $digest_uri, $response_value, $challenge['maxbuf']); 83 | } 84 | } else { 85 | return PEAR::raiseError('Invalid digest challenge'); 86 | } 87 | } 88 | 89 | /** 90 | * Parses and verifies the digest challenge* 91 | * 92 | * @param string $challenge The digest challenge 93 | * @return array The parsed challenge as an assoc 94 | * array in the form "directive => value". 95 | * @access private 96 | */ 97 | function _parseChallenge($challenge) 98 | { 99 | $tokens = array(); 100 | while (preg_match('/^([a-z-]+)=("[^"]+(? 199 | -------------------------------------------------------------------------------- /Auth/SASL/External.php: -------------------------------------------------------------------------------- 1 | | 33 | // +-----------------------------------------------------------------------+ 34 | // 35 | // $Id$ 36 | 37 | /** 38 | * Implmentation of EXTERNAL SASL mechanism 39 | * 40 | * @author Christoph Schulz 41 | * @access public 42 | * @version 1.0.3 43 | * @package Auth_SASL 44 | */ 45 | 46 | require_once('Auth/SASL/Common.php'); 47 | 48 | class Auth_SASL_External extends Auth_SASL_Common 49 | { 50 | /** 51 | * Returns EXTERNAL response 52 | * 53 | * @param string $authcid Authentication id (username) 54 | * @param string $pass Password 55 | * @param string $authzid Autorization id 56 | * @return string EXTERNAL Response 57 | */ 58 | function getResponse($authcid, $pass, $authzid = '') 59 | { 60 | return $authzid; 61 | } 62 | } 63 | ?> 64 | -------------------------------------------------------------------------------- /Auth/SASL/Login.php: -------------------------------------------------------------------------------- 1 | | 33 | // +-----------------------------------------------------------------------+ 34 | // 35 | // $Id$ 36 | 37 | /** 38 | * This is technically not a SASL mechanism, however 39 | * it's used by Net_Sieve, Net_Cyrus and potentially 40 | * other protocols , so here is a good place to abstract 41 | * it. 42 | * 43 | * @author Richard Heyes 44 | * @access public 45 | * @version 1.0 46 | * @deprecated since 1.2.0 47 | * @package Auth_SASL 48 | */ 49 | 50 | require_once('Auth/SASL/Common.php'); 51 | 52 | class Auth_SASL_Login extends Auth_SASL_Common 53 | { 54 | /** 55 | * Pseudo SASL LOGIN mechanism 56 | * 57 | * @param string $user Username 58 | * @param string $pass Password 59 | * @return string LOGIN string 60 | */ 61 | function getResponse($user, $pass) 62 | { 63 | return sprintf('LOGIN %s %s', $user, $pass); 64 | } 65 | } 66 | ?> -------------------------------------------------------------------------------- /Auth/SASL/Plain.php: -------------------------------------------------------------------------------- 1 | | 33 | // +-----------------------------------------------------------------------+ 34 | // 35 | // $Id$ 36 | 37 | /** 38 | * Implmentation of PLAIN SASL mechanism 39 | * 40 | * @author Richard Heyes 41 | * @access public 42 | * @version 1.0 43 | * @package Auth_SASL 44 | */ 45 | 46 | require_once('Auth/SASL/Common.php'); 47 | 48 | class Auth_SASL_Plain extends Auth_SASL_Common 49 | { 50 | /** 51 | * Returns PLAIN response 52 | * 53 | * @param string $authcid Authentication id (username) 54 | * @param string $pass Password 55 | * @param string $authzid Autorization id 56 | * @return string PLAIN Response 57 | */ 58 | function getResponse($authcid, $pass, $authzid = '') 59 | { 60 | return $authzid . chr(0) . $authcid . chr(0) . $pass; 61 | } 62 | } 63 | ?> 64 | -------------------------------------------------------------------------------- /Auth/SASL/SCRAM.php: -------------------------------------------------------------------------------- 1 | 44 | * @access public 45 | * @version 1.0 46 | * @package Auth_SASL 47 | */ 48 | 49 | require_once('Auth/SASL/Common.php'); 50 | 51 | class Auth_SASL_SCRAM extends Auth_SASL_Common 52 | { 53 | private $hash; 54 | private $hmac; 55 | private $gs2_header; 56 | private $cnonce; 57 | private $first_message_bare; 58 | private $saltedPassword; 59 | private $authMessage; 60 | 61 | /** 62 | * Construct a SCRAM-H client where 'H' is a cryptographic hash function. 63 | * 64 | * @param string $hash The name cryptographic hash function 'H' as registered by IANA in the "Hash Function Textual 65 | * Names" registry. 66 | * @link http://www.iana.org/assignments/hash-function-text-names/hash-function-text-names.xml "Hash Function Textual 67 | * Names" 68 | * format of core PHP hash function. 69 | * @access public 70 | */ 71 | function __construct($hash) 72 | { 73 | // Though I could be strict, I will actually also accept the naming used in the PHP core hash framework. 74 | // For instance "sha1" is accepted, while the registered hash name should be "SHA-1". 75 | $hash = strtolower($hash); 76 | $hashes = array('md2' => 'md2', 77 | 'md5' => 'md5', 78 | 'sha-1' => 'sha1', 79 | 'sha1' => 'sha1', 80 | 'sha-224' => 'sha224', 81 | 'sha224' => 'sha224', 82 | 'sha-256' => 'sha256', 83 | 'sha256' => 'sha256', 84 | 'sha-384' => 'sha384', 85 | 'sha384' => 'sha384', 86 | 'sha-512' => 'sha512', 87 | 'sha512' => 'sha512'); 88 | if (function_exists('hash_hmac') && isset($hashes[$hash])) 89 | { 90 | $selectedHash = $hashes[$hash]; 91 | $this->hash = function($data) use ($selectedHash) { 92 | return hash($selectedHash, $data, TRUE); 93 | }; 94 | $this->hmac = function($key,$str,$raw) use ($selectedHash) { 95 | return hash_hmac($selectedHash, $str, $key, $raw); 96 | }; 97 | } 98 | elseif ($hash == 'md5') 99 | { 100 | $this->hash = function($data) { 101 | return md5($data, true); 102 | }; 103 | $this->hmac = array($this, '_HMAC_MD5'); 104 | } 105 | elseif (in_array($hash, array('sha1', 'sha-1'))) 106 | { 107 | $this->hash = function($data) { 108 | return sha1($data, true); 109 | }; 110 | $this->hmac = array($this, '_HMAC_SHA1'); 111 | } 112 | else { 113 | return PEAR::raiseError('Invalid SASL mechanism type'); 114 | } 115 | 116 | return true; 117 | } 118 | 119 | /** 120 | * Provides the (main) client response for SCRAM-H. 121 | * 122 | * @param string $authcid Authentication id (username) 123 | * @param string $pass Password 124 | * @param string $challenge The challenge sent by the server. 125 | * If the challenge is NULL or an empty string, the result will be the "initial response". 126 | * @param string $authzid Authorization id (username to proxy as) 127 | * @return string|false The response (binary, NOT base64 encoded) 128 | * @access public 129 | */ 130 | public function getResponse($authcid, $pass, $challenge = NULL, $authzid = NULL) 131 | { 132 | $authcid = $this->_formatName($authcid); 133 | if (empty($authcid)) 134 | { 135 | return false; 136 | } 137 | if (!empty($authzid)) 138 | { 139 | $authzid = $this->_formatName($authzid); 140 | if (empty($authzid)) 141 | { 142 | return false; 143 | } 144 | } 145 | 146 | if (empty($challenge)) 147 | { 148 | return $this->_generateInitialResponse($authcid, $authzid); 149 | } 150 | else 151 | { 152 | return $this->_generateResponse($challenge, $pass); 153 | } 154 | 155 | } 156 | 157 | /** 158 | * Prepare a name for inclusion in a SCRAM response. 159 | * 160 | * @param string $username a name to be prepared. 161 | * @return string the reformated name. 162 | * @access private 163 | */ 164 | private function _formatName($username) 165 | { 166 | // TODO: prepare through the SASLprep profile of the stringprep algorithm. 167 | // See RFC-4013. 168 | 169 | $username = str_replace('=', '=3D', $username); 170 | $username = str_replace(',', '=2C', $username); 171 | return $username; 172 | } 173 | 174 | /** 175 | * Generate the initial response which can be either sent directly in the first message or as a response to an empty 176 | * server challenge. 177 | * 178 | * @param string $authcid Prepared authentication identity. 179 | * @param string $authzid Prepared authorization identity. 180 | * @return string The SCRAM response to send. 181 | * @access private 182 | */ 183 | private function _generateInitialResponse($authcid, $authzid) 184 | { 185 | $init_rep = ''; 186 | $gs2_cbind_flag = 'n,'; // TODO: support channel binding. 187 | $this->gs2_header = $gs2_cbind_flag . (!empty($authzid)? 'a=' . $authzid : '') . ','; 188 | 189 | // I must generate a client nonce and "save" it for later comparison on second response. 190 | $this->cnonce = $this->_getCnonce(); 191 | // XXX: in the future, when mandatory and/or optional extensions are defined in any updated RFC, 192 | // this message can be updated. 193 | $this->first_message_bare = 'n=' . $authcid . ',r=' . $this->cnonce; 194 | return $this->gs2_header . $this->first_message_bare; 195 | } 196 | 197 | /** 198 | * Parses and verifies a non-empty SCRAM challenge. 199 | * 200 | * @param string $challenge The SCRAM challenge 201 | * @return string|false The response to send; false in case of wrong challenge or if an initial response has not 202 | * been generated first. 203 | * @access private 204 | */ 205 | private function _generateResponse($challenge, $password) 206 | { 207 | // XXX: as I don't support mandatory extension, I would fail on them. 208 | // And I simply ignore any optional extension. 209 | $server_message_regexp = "#^r=([\x21-\x2B\x2D-\x7E]+),s=((?:[A-Za-z0-9/+]{4})*(?:[A-Za-z0-9]{3}=|[A-Xa-z0-9]{2}==)?),i=([0-9]*)(,[A-Za-z]=[^,])*$#"; 210 | if (!isset($this->cnonce, $this->gs2_header) 211 | || !preg_match($server_message_regexp, $challenge, $matches)) 212 | { 213 | return false; 214 | } 215 | $nonce = $matches[1]; 216 | $salt = base64_decode($matches[2]); 217 | if (!$salt) 218 | { 219 | // Invalid Base64. 220 | return false; 221 | } 222 | $i = intval($matches[3]); 223 | 224 | $cnonce = substr($nonce, 0, strlen($this->cnonce)); 225 | if ($cnonce <> $this->cnonce) 226 | { 227 | // Invalid challenge! Are we under attack? 228 | return false; 229 | } 230 | 231 | $channel_binding = 'c=' . base64_encode($this->gs2_header); // TODO: support channel binding. 232 | $final_message = $channel_binding . ',r=' . $nonce; // XXX: no extension. 233 | 234 | // TODO: $password = $this->normalize($password); // SASLprep profile of stringprep. 235 | $saltedPassword = $this->hi($password, $salt, $i); 236 | $this->saltedPassword = $saltedPassword; 237 | $clientKey = call_user_func($this->hmac, $saltedPassword, "Client Key", TRUE); 238 | $storedKey = call_user_func($this->hash, $clientKey, TRUE); 239 | $authMessage = $this->first_message_bare . ',' . $challenge . ',' . $final_message; 240 | $this->authMessage = $authMessage; 241 | $clientSignature = call_user_func($this->hmac, $storedKey, $authMessage, TRUE); 242 | $clientProof = $clientKey ^ $clientSignature; 243 | $proof = ',p=' . base64_encode($clientProof); 244 | 245 | return $final_message . $proof; 246 | } 247 | 248 | /** 249 | * SCRAM has also a server verification step. On a successful outcome, it will send additional data which must 250 | * absolutely be checked against this function. If this fails, the entity which we are communicating with is probably 251 | * not the server as it has not access to your ServerKey. 252 | * 253 | * @param string $data The additional data sent along a successful outcome. 254 | * @return bool Whether the server has been authenticated. 255 | * If false, the client must close the connection and consider to be under a MITM attack. 256 | * @access public 257 | */ 258 | public function processOutcome($data) 259 | { 260 | $verifier_regexp = '#^v=((?:[A-Za-z0-9/+]{4})*(?:[A-Za-z0-9]{3}=|[A-Xa-z0-9]{2}==)?)$#'; 261 | if (!isset($this->saltedPassword, $this->authMessage) 262 | || !preg_match($verifier_regexp, $data, $matches)) 263 | { 264 | // This cannot be an outcome, you never sent the challenge's response. 265 | return false; 266 | } 267 | 268 | $verifier = $matches[1]; 269 | $proposed_serverSignature = base64_decode($verifier); 270 | $serverKey = call_user_func($this->hmac, $this->saltedPassword, "Server Key", true); 271 | $serverSignature = call_user_func($this->hmac, $serverKey, $this->authMessage, TRUE); 272 | return ($proposed_serverSignature === $serverSignature); 273 | } 274 | 275 | /** 276 | * Hi() call, which is essentially PBKDF2 (RFC-2898) with HMAC-H() as the pseudorandom function. 277 | * 278 | * @param string $str The string to hash. 279 | * @param string $salt The salt value. 280 | * @param int $i The iteration count. 281 | * @access private 282 | */ 283 | private function hi($str, $salt, $i) 284 | { 285 | $int1 = "\0\0\0\1"; 286 | $ui = call_user_func($this->hmac, $str, $salt . $int1, true); 287 | $result = $ui; 288 | for ($k = 1; $k < $i; $k++) 289 | { 290 | $ui = call_user_func($this->hmac, $str, $ui, true); 291 | $result = $result ^ $ui; 292 | } 293 | return $result; 294 | } 295 | 296 | 297 | /** 298 | * Creates the client nonce for the response 299 | * 300 | * @return string The cnonce value 301 | * @access private 302 | * @author Richard Heyes 303 | */ 304 | private function _getCnonce() 305 | { 306 | // TODO: I reused the nonce function from the DigestMD5 class. 307 | // I should probably make this a protected function in Common. 308 | if (@file_exists('/dev/urandom') && $fd = @fopen('/dev/urandom', 'r')) { 309 | return base64_encode(fread($fd, 32)); 310 | 311 | } elseif (@file_exists('/dev/random') && $fd = @fopen('/dev/random', 'r')) { 312 | return base64_encode(fread($fd, 32)); 313 | 314 | } else { 315 | $str = ''; 316 | for ($i=0; $i<32; $i++) { 317 | $str .= chr(mt_rand(0, 255)); 318 | } 319 | 320 | return base64_encode($str); 321 | } 322 | } 323 | 324 | } 325 | 326 | ?> 327 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Auth_SASL - Abstraction of various SASL mechanism responses 2 | 3 | [![Build Status](https://travis-ci.org/pear/Auth_SASL.svg?branch=master)](https://travis-ci.org/pear/Auth_SASL) 4 | 5 | 6 | Provides code to generate responses to common SASL mechanisms, including: 7 | - Anonymous 8 | - Cram-MD5 (DEPRECATED) 9 | - Digest-MD5 (DEPRECATED) 10 | - External 11 | - Login (Pseudo mechanism) (DEPRECATED) 12 | - Plain 13 | - SCRAM 14 | 15 | [Homepage](http://pear.php.net/package/Auth_SASL/) 16 | 17 | 18 | ## Installation 19 | For a PEAR installation that downloads from the PEAR channel: 20 | 21 | `$ pear install pear/auth_sasl` 22 | 23 | For a PEAR installation from a previously downloaded tarball: 24 | 25 | `$ pear install Auth_SASL-*.tgz` 26 | 27 | For a PEAR installation from a code clone: 28 | 29 | `$ pear install package.xml` 30 | 31 | For a local composer installation: 32 | 33 | `$ composer install` 34 | 35 | To add as a dependency to your composer-managed application: 36 | 37 | `$composer require pear/auth_sasl` 38 | 39 | 40 | ## Tests 41 | Run the tests from a local composer installation: 42 | 43 | `$ ./vendor/bin/phpunit` 44 | 45 | 46 | ## License 47 | BSD license 48 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ 3 | { 4 | "email": "amistry@am-productions.biz", 5 | "name": "Anish Mistry", 6 | "role": "Lead" 7 | }, 8 | { 9 | "email": "richard@php.net", 10 | "name": "Richard Heyes", 11 | "role": "Lead" 12 | }, 13 | { 14 | "email": "michael@bretterklieber.com", 15 | "name": "Michael Bretterklieber", 16 | "role": "Lead" 17 | } 18 | ], 19 | "autoload": { 20 | "psr-0": { 21 | "Auth": "./" 22 | } 23 | }, 24 | "description": "Abstraction of various SASL mechanism responses", 25 | "homepage": "http://pear.php.net/package/Auth_SASL", 26 | "include-path": [ 27 | "./" 28 | ], 29 | "license": "BSD-3-Clause", 30 | "name": "pear/auth_sasl", 31 | "support": { 32 | "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=Auth_SASL", 33 | "source": "https://github.com/pear/Auth_SASL" 34 | }, 35 | "type": "library", 36 | "require-dev": { 37 | "phpunit/phpunit": "@stable" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /package.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | Auth_SASL 12 | pear.php.net 13 | Abstraction of various SASL mechanism responses 14 | 15 | Provides code to generate responses to common SASL mechanisms, including: 16 | - Anonymous 17 | - Cram-MD5 (DEPRECATED) 18 | - Digest-MD5 (DEPRECATED) 19 | - External 20 | - Login (Pseudo mechanism) (DEPRECATED) 21 | - Plain 22 | - SCRAM 23 | 24 | 25 | 26 | Anish Mistry 27 | amistry 28 | amistry@am-productions.biz 29 | no 30 | 31 | 32 | Richard Heyes 33 | richard 34 | richard@php.net 35 | no 36 | 37 | 38 | Michael Bretterklieber 39 | mbretter 40 | michael@bretterklieber.com 41 | no 42 | 43 | 44 | Armin Graefe 45 | schengawegga 46 | schengawegga@gmail.com 47 | yes 48 | 49 | 50 | 2023-12-21 51 | 52 | 1.2.0 53 | 1.1.0 54 | 55 | 56 | stable 57 | stable 58 | 59 | BSD 60 | 61 | * feature: PHP8.2 ready 62 | * bugfix: scram-sha-224 broken #14 63 | * task: mark authentication methods cram-md5, digest-md5, and login as deprecated #14 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 5.4.0 88 | 89 | 90 | 1.10.1 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 1.2.0 102 | 1.1.0 103 | 104 | 105 | stable 106 | stable 107 | 108 | 2023-12-21 109 | BSD 110 | 111 | * feature: PHP8.2 ready 112 | * bugfix: scram-sha-224 broken #14 113 | * task: mark authentication methods cram-md5, digest-md5, and login as deprecated #14 114 | 115 | 116 | 117 | 118 | 119 | 1.1.0 120 | 1.1.0 121 | 122 | 123 | stable 124 | stable 125 | 126 | 2017-03-07 127 | BSD 128 | 129 | * Set minimum PHP version to 5.4.0 130 | * Set minimum PEAR version to 1.10.1 131 | 132 | * Request #21033: PHP warning depreciated 133 | 134 | 135 | 136 | 137 | 138 | 1.0.6 139 | 1.0.3 140 | 141 | 142 | stable 143 | stable 144 | 145 | 2011-09-27 146 | BSD 147 | 148 | QA release 149 | * Bug #18856: Authentication warnings because of wrong Auth_SASL::factory argument [kguest] 150 | 151 | 152 | 153 | 154 | 155 | 1.0.5 156 | 1.0.3 157 | 158 | 159 | stable 160 | stable 161 | 162 | 2011-09-04 163 | BSD 164 | 165 | QA release 166 | * Added support for any mechanism of the SCRAM family; with thanks to Jehan Pagès. [kguest] 167 | * crammd5 and digestmd5 mechanisms name deprecated in favour of IANA registered names 'cram-md5' and 'digest-md5'; with thanks to Jehan Pagès. [kguest] 168 | 169 | 170 | 171 | 172 | 173 | 1.0.4 174 | 1.0.3 175 | 176 | 177 | stable 178 | stable 179 | 180 | 2010-02-07 181 | BSD 182 | 183 | QA release 184 | * Fix bug #16624: open_basedir restriction warning in DigestMD5.php [till] 185 | 186 | 187 | 188 | 189 | 190 | 1.0.3 191 | 1.0.3 192 | 193 | 194 | stable 195 | stable 196 | 197 | 2009-08-05 198 | BSD 199 | 200 | QA release 201 | * Move SVN to proper directory structure [cweiske] 202 | * Fix Bug #8775: Error in package.xml 203 | * Fix Bug #14671: Security issue due to seeding random number generator [cweiske] 204 | 205 | 206 | 207 | 208 | 209 | 1.0.2 210 | 1.0.2 211 | 212 | 213 | stable 214 | stable 215 | 216 | 2006-05-21 217 | BSD 218 | 219 | * Fixed Bug #2143 Auth_SASL_DigestMD5::getResponse() generates invalid response 220 | * Fixed Bug #6611 Suppress PHP 5 Notice Errors 221 | * Fixed Bug #2154 realm isn't contained in challange 222 | 223 | 224 | 225 | 226 | 227 | 1.0.1 228 | 1.0.1 229 | 230 | 231 | stable 232 | stable 233 | 234 | 2003-09-11 235 | BSD 236 | * Added authcid/authzid separation in PLAIN and DIGEST-MD5. 237 | 238 | 239 | 240 | 241 | 242 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 14 | tests/ 15 | 16 | 17 | 18 | 19 | 20 | Auth/ 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | --------------------------------------------------------------------------------