| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500 | <?php  /**   * Class for verifying Yubico One-Time-Passcodes   *   * @category    Auth   * @package     Auth_Yubico   * @author      Simon Josefsson <simon@yubico.com>, Olov Danielson <olov@yubico.com>   * @copyright   2007-2020 Yubico AB   * @license     https://opensource.org/licenses/bsd-license.php New BSD License   * @version     2.0   * @link        https://www.yubico.com/   */require_once 'PEAR.php';/** * Class for verifying Yubico One-Time-Passcodes * * Simple example: * <code> * require_once 'Auth/Yubico.php'; * $otp = "ccbbddeertkrctjkkcglfndnlihhnvekchkcctif"; * * # Generate a new id+key from https://api.yubico.com/get-api-key/ * $yubi = new Auth_Yubico('42', 'FOOBAR='); * $auth = $yubi->verify($otp); * if (PEAR::isError($auth)) { *    print "<p>Authentication failed: " . $auth->getMessage(); *    print "<p>Debug output from server: " . $yubi->getLastResponse(); * } else { *    print "<p>You are authenticated!"; * } * </code> */class Auth_Yubico{	/**#@+	 * @access private	 */	/**	 * Yubico client ID	 * @var string	 */	var $_id;	/**	 * Yubico client key	 * @var string	 */	var $_key;	/**	 * List with URL part of validation servers	 * @var array	 */	var $_url_list;	/**	 * index to _url_list	 * @var int	 */	var $_url_index;	/**	 * Last query to server	 * @var string	 */	var $_lastquery;	/**	 * Response from server	 * @var string	 */	var $_response;	/**	 * Number of times we retried in our last validation	 * @var int	 */	var $_retries;	/**	 * Flag whether to verify HTTPS server certificates or not.	 * @var boolean	 */	var $_httpsverify;	/**	 * Maximum number of times we will retry transient HTTP errors	 * @var int	 */	var $_max_retries;	/**	 * Constructor	 *	 * Sets up the object	 * @param    string  $id     The client identity	 * @param    string  $key    The client MAC key (optional)	 * @param    boolean $https  noop	 * @param    boolean $httpsverify  Flag whether to use verify HTTPS	 *                                 server certificates (optional,	 *                                 default true)	 * @access public	 */        public function __construct($id, $key = '', $https = 0, $httpsverify = 1, $max_retries = 3)	{		$this->_id =  $id;		$this->_key = base64_decode($key);		$this->_httpsverify = $httpsverify;		$this->_max_retries = $max_retries;	}	/**	 * Specify to use a different URL part for verification.	 * The default is "https://api.yubico.com/wsapi/2.0/verify".	 *	 * @param  string $url  New server URL part to use	 * @access public	 * @deprecated	 */	function setURLpart($url)	{	  $this->_url_list = array($url);	}	/**	 * Get next URL part from list to use for validation.	 *	 * @return mixed string with URL part or false if no more URLs in list	 * @access public	 */	function getNextURLpart()	{	  if ($this->_url_list) $url_list=$this->_url_list;	  else $url_list=array('https://api.yubico.com/wsapi/2.0/verify');	  if ($this->_url_index>=count($url_list)) return false;	  else return $url_list[$this->_url_index++];	}	/**	 * Resets index to URL list	 *	 * @access public	 */	function URLreset()	{	  $this->_url_index=0;	}	/**	 * Add another URLpart.	 *	 * @access public	 */	function addURLpart($URLpart) 	{	  $this->_url_list[]=$URLpart;	}		/**	 * Return the last query sent to the server, if any.	 *	 * @return string  Request to server	 * @access public	 */	function getLastQuery()	{		return $this->_lastquery;	}	/**	 * Return the last data received from the server, if any.	 *	 * @return string  Output from server	 * @access public	 */	function getLastResponse()	{		return $this->_response;	}	/**	 * Return the number of retries that were used in the last validation	 *	 * @return int     Number of retries	 * @access public	 */	function getRetries()	{		return $this->_retries;	}	/**	 * Parse input string into password, yubikey prefix,	 * ciphertext, and OTP.	 *	 * @param  string    Input string to parse	 * @param  string    Optional delimiter re-class, default is '[:]'	 * @return array     Keyed array with fields	 * @access public	 */	function parsePasswordOTP($str, $delim = '[:]')	{	  if (!preg_match("/^((.*)" . $delim . ")?" .			  "(([cbdefghijklnrtuv]{0,16})" .			  "([cbdefghijklnrtuv]{32}))$/i",			  $str, $matches)) {	    /* Dvorak? */	    if (!preg_match("/^((.*)" . $delim . ")?" .			    "(([jxe\.uidchtnbpygk]{0,16})" .			    "([jxe\.uidchtnbpygk]{32}))$/i",			    $str, $matches)) {	      return false;	    } else {	      $ret['otp'] = strtr($matches[3], "jxe.uidchtnbpygk", "cbdefghijklnrtuv");	    }	  } else {	    $ret['otp'] = $matches[3];	  }	  $ret['password'] = $matches[2];	  $ret['prefix'] = $matches[4];	  $ret['ciphertext'] = $matches[5];	  return $ret;	}	/* TODO? Add functions to get parsed parts of server response? */	/**	 * Parse parameters from last response	 *	 * example: getParameters("timestamp", "sessioncounter", "sessionuse");	 *	 * @param  array @parameters  Array with strings representing	 *                            parameters to parse	 * @return array  parameter array from last response	 * @access public	 */	function getParameters($parameters)	{	  if ($parameters == null) {	    $parameters = array('timestamp', 'sessioncounter', 'sessionuse');	  }	  $param_array = array();	  foreach ($parameters as $param) {	    if(!preg_match("/" . $param . "=([0-9]+)/", $this->_response, $out)) {	      return PEAR::raiseError('Could not parse parameter ' . $param . ' from response');	    }	    $param_array[$param]=$out[1];	  }	  return $param_array;	}        function _make_curl_handle($query, $timeout=null)	{	    flush();	    $handle = curl_init($query);	    curl_setopt($handle, CURLOPT_USERAGENT, "PEAR Auth_Yubico");	    curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1);	    if (!$this->_httpsverify) {		curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, 0);		curl_setopt($handle, CURLOPT_SSL_VERIFYHOST, 0);	    }	    curl_setopt($handle, CURLOPT_FAILONERROR, true);	    /* If timeout is set, we better apply it here as well	     * in case the validation server fails to follow it. */	    if ($timeout) {		curl_setopt($handle, CURLOPT_TIMEOUT, $timeout);	    }	    return $handle;	}	/**	 * Verify Yubico OTP against multiple URLs	 * Protocol specification 2.0 is used to construct validation requests	 *	 * @param string $token        Yubico OTP	 * @param int $use_timestamp   1=>send request with ×tamp=1 to	 *                             get timestamp and session information	 *                             in the response	 * @param boolean $wait_for_all  If true, wait until all	 *                               servers responds (for debugging)	 * @param string $sl           Sync level in percentage between 0	 *                             and 100 or "fast" or "secure".	 * @param int $timeout         Max number of seconds to wait	 *                             for responses	 * @param int $max_retries     Max number of times we will retry on	 *                             transient errors.	 * @return mixed               PEAR error on error, true otherwise	 * @access public	 */	function verify($token, $use_timestamp=null, $wait_for_all=False,			$sl=null, $timeout=null, $max_retries=null)	{	  /* If maximum retries is not set, default from instance */	  if (is_null($max_retries)) {	    $max_retries = $this->_max_retries;	  }	  /* Construct parameters string */	  $ret = $this->parsePasswordOTP($token);	  if (!$ret) {	    return PEAR::raiseError('Could not parse Yubikey OTP');	  }	  $params = array('id'=>$this->_id, 			  'otp'=>$ret['otp'],			  'nonce'=>md5(uniqid(rand())));	  /* Take care of protocol version 2 parameters */	  if ($use_timestamp) $params['timestamp'] = 1;	  if ($sl) $params['sl'] = $sl;	  if ($timeout) $params['timeout'] = $timeout;	  ksort($params);	  $parameters = '';	  foreach($params as $p=>$v) $parameters .= "&" . $p . "=" . $v;	  $parameters = ltrim($parameters, "&");	  	  /* Generate signature. */	  if($this->_key <> "") {	    $signature = base64_encode(hash_hmac('sha1', $parameters,						 $this->_key, true));	    $signature = preg_replace('/\+/', '%2B', $signature);	    $parameters .= '&h=' . $signature;	  }	  /* Generate and prepare request. */	  $this->_lastquery = null;	  $this->_retries = 0;	  $this->URLreset();	  $mh = curl_multi_init();	  $ch = array();	  $retries = array();	  while($URLpart=$this->getNextURLpart()) 	    {	      $query = $URLpart . "?" . $parameters;	      if ($this->_lastquery) { $this->_lastquery .= " "; }	      $this->_lastquery .= $query;	      	      $handle = $this->_make_curl_handle($query, $timeout);	      curl_multi_add_handle($mh, $handle);	      	      $ch[(int)$handle] = $handle;	      $retries[$query] = 0;	    }	  /* Execute and read request. */	  $this->_response=null;	  $replay=False;	  $valid=False;	  do {	    /* Let curl do its work. */	    while (($mrc = curl_multi_exec($mh, $active))		   == CURLM_CALL_MULTI_PERFORM) {	      curl_multi_select($mh);	    }	    while ($info = curl_multi_info_read($mh)) {	      $cinfo = curl_getinfo ($info['handle']);	      if ($info['result'] == CURLE_OK) {		/* We have a complete response from one server. */		$str = curl_multi_getcontent($info['handle']);				if ($wait_for_all) { # Better debug info		  $this->_response .= 'URL=' . $cinfo['url'] . ' HTTP_CODE='		    . $cinfo['http_code'] . "\n"		    . $str . "\n";		}		if (preg_match("/status=([a-zA-Z0-9_]+)/", $str, $out)) {		  $status = $out[1];		  /* 		   * There are 3 cases.		   *		   * 1. OTP or Nonce values doesn't match - ignore		   * response.		   *		   * 2. We have a HMAC key.  If signature is invalid -		   * ignore response.  Return if status=OK or		   * status=REPLAYED_OTP.		   *		   * 3. Return if status=OK or status=REPLAYED_OTP.		   */		  if (!preg_match("/otp=".$params['otp']."/", $str) ||		      !preg_match("/nonce=".$params['nonce']."/", $str)) {		    /* Case 1. Ignore response. */		  } 		  elseif ($this->_key <> "") {		    /* Case 2. Verify signature first */		    $rows = explode("\r\n", trim($str));		    $response=array();			foreach ($rows as $key => $val) {		      /* = is also used in BASE64 encoding so we only replace the first = by # which is not used in BASE64 */		      $val = preg_replace('/=/', '#', $val, 1);		      $row = explode("#", $val);		      $response[$row[0]] = $row[1];		    }		    		    $parameters=array('nonce','otp', 'sessioncounter', 'sessionuse', 'sl', 'status', 't', 'timeout', 'timestamp');		    sort($parameters);		    $check=Null;		    foreach ($parameters as $param) {		      if (array_key_exists($param, $response)) {			if ($check) $check = $check . '&';			$check = $check . $param . '=' . $response[$param];		      }		    }		    $checksignature =		      base64_encode(hash_hmac('sha1', utf8_encode($check),					      $this->_key, true));		    if($response['h'] == $checksignature) {		      if ($status == 'REPLAYED_OTP') {			if (!$wait_for_all) { $this->_response = $str; }			$replay=True;		      } 		      if ($status == 'OK') {			if (!$wait_for_all) { $this->_response = $str; }			$valid=True;		      }		    }		  } else {		    /* Case 3. We check the status directly */		    if ($status == 'REPLAYED_OTP') {		      if (!$wait_for_all) { $this->_response = $str; }		      $replay=True;		    } 		    if ($status == 'OK') {		      if (!$wait_for_all) { $this->_response = $str; }		      $valid=True;		    }		  }		}		if (!$wait_for_all && ($valid || $replay))		  {		    /* We have status=OK or status=REPLAYED_OTP, return. */		    foreach ($ch as $h) {		      curl_multi_remove_handle($mh, $h);		      curl_close($h);		    }		    curl_multi_close($mh);		    if ($replay) return PEAR::raiseError('REPLAYED_OTP');		    if ($valid) return true;		    return PEAR::raiseError($status);		  }	      } else {		/* Some kind of error, but def. not a 200 response */		/* No status= in response body */		$http_status_code = (int)$cinfo['http_code'];		$query = $cinfo['url'];		if ($http_status_code == 400 ||		    ($http_status_code >= 500 && $http_status_code < 600)) {		  /* maybe retry */		  if ($retries[$query] < $max_retries) {		    $retries[$query]++;  // for this server		    $this->_retries++;   // for this validation attempt		    $newhandle = $this->_make_curl_handle($query, $timeout);		    curl_multi_add_handle($mh, $newhandle);		    $ch[(int)$newhandle] = $newhandle;		    // Loop back up to curl_multi_exec, even if this		    // was the last handle and curl_multi_exec _was_		    // no longer active, it's active again now we've		    // added a retry.		    $active = true;		  }		}	      }	      /* Done with this handle */	      curl_multi_remove_handle($mh, $info['handle']);	      curl_close($info['handle']);	      unset ($ch[(int)$info['handle']]);	    }	  } while ($active);	  /* Typically this is only reached for wait_for_all=true or	   * when the timeout is reached and there is no	   * OK/REPLAYED_REQUEST answer (think firewall).	   */	  foreach ($ch as $h) {	    curl_multi_remove_handle ($mh, $h);	    curl_close ($h);	  }	  curl_multi_close ($mh);	  	  if ($replay) return PEAR::raiseError('REPLAYED_OTP');	  if ($valid) return true;	  return PEAR::raiseError('NO_VALID_ANSWER');	}}?>
 |