PageRenderTime 39ms CodeModel.GetById 20ms app.highlight 16ms RepoModel.GetById 1ms app.codeStats 0ms

/packages/Users/Yubikey/Managers/Yubikey.class.php

https://bitbucket.org/alexamiryan/stingle
PHP | 481 lines | 265 code | 45 blank | 171 comment | 56 complexity | 30d6dd24c08446e3813ddb87df8effc5 MD5 | raw file
  1<?php
  2/**
  3 * Class for verifying Yubico One-Time-Passcodes
  4 *
  5 * @category    Auth
  6 * @package     Auth_Yubico
  7 * @author      Simon Josefsson <simon@yubico.com>, Olov Danielson <olov@yubico.com>, Alex Amiryan <alex@amiryan.org>
  8 * @copyright   2007, 2008, 2009, 2010 Yubico AB, 2011
  9 * @license     http://opensource.org/licenses/bsd-license.php New BSD License
 10 * @version     2.0
 11 * @link        http://www.yubico.com/
 12 */
 13
 14class Yubikey{
 15	/**#@+
 16	 * @access private
 17	 */
 18	
 19	/**
 20	 * Yubico client ID
 21	 * @var string
 22	 */
 23	protected $_id;
 24	
 25	/**
 26	 * Yubico client key
 27	 * @var string
 28	 */
 29	protected $_key;
 30	
 31	/**
 32	 * URL part of validation server
 33	 * @var string
 34	 */
 35	protected $_url;
 36	
 37	/**
 38	 * List with URL part of validation servers
 39	 * @var array
 40	 */
 41	protected $_url_list;
 42	
 43	/**
 44	 * index to _url_list
 45	 * @var int
 46	 */
 47	protected $_url_index;
 48	
 49	/**
 50	 * Last query to server
 51	 * @var string
 52	 */
 53	protected $_lastquery;
 54	
 55	/**
 56	 * Response from server
 57	 * @var string
 58	 */
 59	protected $_response;
 60	
 61	/**
 62	 * Flag whether to use https or not.
 63	 * @var boolean
 64	 */
 65	protected $_https;
 66	
 67	/**
 68	 * Flag whether to verify HTTPS server certificates or not.
 69	 * @var boolean
 70	 */
 71	protected $_httpsverify;
 72	
 73	/**
 74	 * Constructor
 75	 *
 76	 * Sets up the object
 77	 * @param    string  $id     The client identity
 78	 * @param    string  $key    The client MAC key (optional)
 79	 * @param    boolean $https  Flag whether to use https (optional)
 80	 * @param    boolean $httpsverify  Flag whether to use verify HTTPS
 81	 * server certificates (optional,
 82	 * default true)
 83	 * @access public
 84	 */
 85	public function __construct($id, $key = '', $https = true, $httpsverify = true){
 86		if(empty($id)){
 87			throw new InvalidArgumentException("Invalid API ID specified!");
 88		}
 89		$this->_id = $id;
 90		$this->_key = base64_decode($key);
 91		$this->_https = $https;
 92		$this->_httpsverify = $httpsverify;
 93	}
 94	
 95	/**
 96	 * Specify to use a different URL part for verification.
 97	 * The default is "api.yubico.com/wsapi/verify".
 98	 *
 99	 * @param  string $url  New server URL part to use
100	 * @access public
101	 */
102	public function setURLpart($url){
103		if(empty($url)){
104			throw new InvalidArgumentException("Empty \$url specified!");
105		}
106		
107		$this->_url = $url;
108	}
109	
110	/**
111	 * Get URL part to use for validation.
112	 *
113	 * @return string  Server URL part
114	 * @access public
115	 */
116	public function getURLpart(){
117		if($this->_url){
118			return $this->_url;
119		}
120		else{
121			return "api.yubico.com/wsapi/verify";
122		}
123	}
124	
125	/**
126	 * Get next URL part from list to use for validation.
127	 *
128	 * @return mixed string with URL part of false if no more URLs in list
129	 * @access public
130	 */
131	public function getNextURLpart(){
132		if($this->_url_list){
133			$url_list = $this->_url_list;
134		}
135		else{
136			$url_list = array(
137					'api.yubico.com/wsapi/2.0/verify', 
138					'api2.yubico.com/wsapi/2.0/verify', 
139					'api3.yubico.com/wsapi/2.0/verify', 
140					'api4.yubico.com/wsapi/2.0/verify', 
141					'api5.yubico.com/wsapi/2.0/verify');
142		}
143		
144		if($this->_url_index >= count($url_list)){
145			return false;
146		}
147		else{
148			return $url_list[$this->_url_index++];
149		}
150	}
151	
152	/**
153	 * Resets index to URL list
154	 *
155	 * @access public
156	 */
157	public function URLreset(){
158		$this->_url_index = 0;
159	}
160	
161	/**
162	 * Add another URLpart.
163	 *
164	 * @access public
165	 */
166	public function addURLpart($URLpart){
167		if(empty($URLpart)){
168			throw new InvalidArgumentException("Empty \$URLpart specified!");
169		}
170		
171		$this->_url_list[] = $URLpart;
172	}
173	
174	/**
175	 * Return the last query sent to the server, if any.
176	 *
177	 * @return string  Request to server
178	 * @access public
179	 */
180	public function getLastQuery(){
181		return $this->_lastquery;
182	}
183	
184	/**
185	 * Return the last data received from the server, if any.
186	 *
187	 * @return string  Output from server
188	 * @access public
189	 */
190	public function getLastResponse(){
191		return $this->_response;
192	}
193	
194	/**
195	 * Parse input string into password, yubikey prefix,
196	 * ciphertext, and OTP.
197	 *
198	 * @param  string    Input string to parse
199	 * @param  string    Optional delimiter re-class, default is '[:]'
200	 * @return array     Keyed array with fields
201	 * @access public
202	 */
203	public function parsePasswordOTP($str, $delim = '[:]'){
204		if(!preg_match("/^((.*)" . $delim . ")?" . "(([cbdefghijklnrtuvCBDEFGHIJKLNRTUV]{0,16})" . "([cbdefghijklnrtuvCBDEFGHIJKLNRTUV]{32}))$/", $str, $matches)){
205			return false;
206		}
207		$ret['password'] = $matches[2];
208		$ret['otp'] = $matches[3];
209		$ret['prefix'] = $matches[4];
210		$ret['ciphertext'] = $matches[5];
211		return $ret;
212	}
213	
214	/**
215	 * Parse parameters from last response
216	 *
217	 * example: getParameters("timestamp", "sessioncounter", "sessionuse");
218	 *
219	 * @param  array @parameters  Array with strings representing
220	 * parameters to parse
221	 * @return array  parameter array from last response
222	 * @access public
223	 */
224	public function getParameters($parameters){
225		if($parameters == null){
226			$parameters = array(
227					'timestamp', 
228					'sessioncounter', 
229					'sessionuse');
230		}
231		$param_array = array();
232		foreach($parameters as $param){
233			if(!preg_match("/" . $param . "=([0-9]+)/", $this->_response, $out)){
234				throw new YubikeyException('Could not parse parameter ' . $param . ' from response');
235			}
236			$param_array[$param] = $out[1];
237		}
238		return $param_array;
239	}
240	
241	/**
242	 * Verify Yubico OTP against multiple URLs
243	 * Protocol specification 2.0 is used to construct validation requests
244	 *
245	 * @param string $token        Yubico OTP
246	 * @param int $use_timestamp   1=>send request with &timestamp=1 to
247	 * get timestamp and session information
248	 * in the response
249	 * @param boolean $wait_for_all  If true, wait until all
250	 * servers responds (for debugging)
251	 * @param string $sl           Sync level in percentage between 0
252	 * and 100 or "fast" or "secure".
253	 * @param int $timeout         Max number of seconds to wait
254	 * for responses
255	 * @return mixed               PEAR error on error, true otherwise
256	 * @access public
257	 */
258	public function verify($token, $use_timestamp = null, $wait_for_all = False, $sl = null, $timeout = null){
259		/* Construct parameters string */
260		$ret = $this->parsePasswordOTP($token);
261		if(!$ret){
262			throw new YubikeyException('Could not parse Yubikey OTP');
263		}
264		$params = array(
265				'id' => $this->_id, 
266				'otp' => $ret['otp'], 
267				'nonce' => md5(uniqid(rand())));
268		/* Take care of protocol version 2 parameters */
269		if($use_timestamp){
270			$params['timestamp'] = 1;
271		}
272		if($sl){
273			$params['sl'] = $sl;
274		}
275		if($timeout){
276			$params['timeout'] = $timeout;
277		}
278		ksort($params);
279		$parameters = '';
280		foreach($params as $p => $v){
281			$parameters .= "&" . $p . "=" . $v;
282		}
283		$parameters = ltrim($parameters, "&");
284		
285		/* Generate signature. */
286		if($this->_key != ""){
287			$signature = base64_encode(hash_hmac('sha1', $parameters, $this->_key, true));
288			$signature = preg_replace('/\+/', '%2B', $signature);
289			$parameters .= '&h=' . $signature;
290		}
291		
292		/* Generate and prepare request. */
293		$this->_lastquery = null;
294		$this->URLreset();
295		$mh = curl_multi_init();
296		$ch = array();
297		while(($URLpart = $this->getNextURLpart()) != false){
298			/* Support https. */
299			if($this->_https){
300				$query = "https://";
301			}
302			else{
303				$query = "http://";
304			}
305			$query .= $URLpart . "?" . $parameters;
306			
307			if($this->_lastquery){
308				$this->_lastquery .= " ";
309			}
310			$this->_lastquery .= $query;
311			
312			$handle = curl_init($query);
313			curl_setopt($handle, CURLOPT_USERAGENT, "PEAR Auth_Yubico");
314			curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1);
315			if(!$this->_httpsverify){
316				curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, 0);
317			}
318			curl_setopt($handle, CURLOPT_FAILONERROR, true);
319			
320			/* If timeout is set, we better apply it here as well
321	         	in case the validation server fails to follow it. 
322			*/
323			if($timeout) curl_setopt($handle, CURLOPT_TIMEOUT, $timeout);
324			curl_multi_add_handle($mh, $handle);
325			
326			$ch[intval($handle)] = $handle;
327		}
328		
329		/* Execute and read request. */
330		$this->_response = null;
331		$replay = False;
332		$valid = False;
333		do{
334			/* Let curl do its work. */
335			while(($mrc = curl_multi_exec($mh, $active)) == CURLM_CALL_MULTI_PERFORM)
336				;
337			
338			while(($info = curl_multi_info_read($mh)) != false){
339				if($info['result'] == CURLE_OK){
340					
341					/* We have a complete response from one server. */
342					
343					$str = curl_multi_getcontent($info['handle']);
344					$cinfo = curl_getinfo($info['handle']);
345					
346					if($wait_for_all){ # Better debug info
347						$this->_response .= 'URL=' . $cinfo['url'] . "\n" . $str . "\n";
348					}
349					
350					if(preg_match("/status=([a-zA-Z0-9_]+)/", $str, $out)){
351						$status = $out[1];
352						
353						/* 
354						* There are 3 cases.
355						*
356						* 1. OTP or Nonce values doesn't match - ignore
357						* response.
358						*
359						* 2. We have a HMAC key.  If signature is invalid -
360						* ignore response.  Return if status=OK or
361						* status=REPLAYED_OTP.
362						*
363						* 3. Return if status=OK or status=REPLAYED_OTP.
364						*/
365						if(!preg_match("/otp=" . $params['otp'] . "/", $str) || !preg_match("/nonce=" . $params['nonce'] . "/", $str)){
366							/* Case 1. Ignore response. */
367						}
368						elseif($this->_key != ""){
369							/* Case 2. Verify signature first */
370							$rows = explode("\r\n", $str);
371							$response = array();
372							while((list($key, $val) = each($rows)) != false){
373								/* = is also used in BASE64 encoding so we only replace the first = by # which is not used in BASE64 */
374								$val = preg_replace('/=/', '#', $val, 1);
375								$row = explode("#", $val);
376								if(isset($row[1])){
377									$response[$row[0]] = $row[1];
378								}
379								else{
380									$response[$row[0]] = null;
381								}
382							}
383							
384							$parameters = array(
385									'nonce', 
386									'otp', 
387									'sessioncounter', 
388									'sessionuse', 
389									'sl', 
390									'status', 
391									't', 
392									'timeout', 
393									'timestamp');
394							sort($parameters);
395							$check = Null;
396							foreach($parameters as $param){
397								if(isset($response[$param]) and $response[$param] != null){
398									if($check) $check = $check . '&';
399									$check = $check . $param . '=' . $response[$param];
400								}
401							}
402							
403							$checksignature = base64_encode(hash_hmac('sha1', utf8_encode($check), $this->_key, true));
404							
405							if($response['h'] == $checksignature){
406								if($status == 'REPLAYED_OTP'){
407									if(!$wait_for_all){
408										$this->_response = $str;
409									}
410									$replay = True;
411								}
412								if($status == 'OK'){
413									if(!$wait_for_all){
414										$this->_response = $str;
415									}
416									$valid = True;
417								}
418							}
419						}
420						else{
421							/* Case 3. We check the status directly */
422							if($status == 'REPLAYED_OTP'){
423								if(!$wait_for_all){
424									$this->_response = $str;
425								}
426								$replay = True;
427							}
428							if($status == 'OK'){
429								if(!$wait_for_all){
430									$this->_response = $str;
431								}
432								$valid = True;
433							}
434						}
435					}
436					if(!$wait_for_all && ($valid || $replay)){
437						/* We have status=OK or status=REPLAYED_OTP, return. */
438						foreach($ch as $h){
439							curl_multi_remove_handle($mh, $h);
440							curl_close($h);
441						}
442						curl_multi_close($mh);
443						if($replay){
444							throw new YubikeyException('REPLAYED_OTP');
445						}
446						if($valid){
447							return true;
448						}
449						throw new YubikeyException($status);
450					}
451					
452					curl_multi_remove_handle($mh, $info['handle']);
453					curl_close($info['handle']);
454					unset($ch[$info['handle']]);
455				}
456				curl_multi_select($mh);
457			}
458		}
459		while($active);
460		
461		/* Typically this is only reached for wait_for_all=true or
462	   * when the timeout is reached and there is no
463	   * OK/REPLAYED_REQUEST answer (think firewall).
464	   */
465		
466		foreach($ch as $h){
467			curl_multi_remove_handle($mh, $h);
468			curl_close($h);
469		}
470		curl_multi_close($mh);
471		
472		if($replay){
473			throw new YubikeyException('REPLAYED_OTP');
474		}
475		if($valid){
476			return true;
477		}
478		throw new YubikeyException('NO_VALID_ANSWER');
479	}
480}
481