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