| 
									
										
										
										
											2018-12-31 09:51:44 +00:00
										 |  |  | <?php | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Class for verifying Yubico One-Time-Passcodes | 
					
						
							|  |  |  |  * | 
					
						
							|  |  |  |  * @category Auth | 
					
						
							|  |  |  |  * @package Auth_Yubico | 
					
						
							|  |  |  |  * @author Simon Josefsson <simon@yubico.com>, Olov Danielson <olov@yubico.com> | 
					
						
							|  |  |  |  * @author Roland Gruber | 
					
						
							|  |  |  |  * @copyright 2007-2015 Yubico AB | 
					
						
							|  |  |  |  * @copyright 2018 Roland Gruber | 
					
						
							|  |  |  |  * @license https://opensource.org/licenses/bsd-license.php New BSD License | 
					
						
							|  |  |  |  * @version 2.0 | 
					
						
							|  |  |  |  * @link https://www.yubico.com/ | 
					
						
							|  |  |  |  * | 
					
						
							| 
									
										
										
										
											2018-12-31 10:42:20 +00:00
										 |  |  |  * Adapted for LAM. | 
					
						
							| 
									
										
										
										
											2018-12-31 09:51:44 +00:00
										 |  |  |  */ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /** | 
					
						
							|  |  |  |  * Class for verifying Yubico One-Time-Passcodes | 
					
						
							|  |  |  |  */ | 
					
						
							|  |  |  | class Auth_Yubico { | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * Yubico client ID | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * @var string | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	private $clientId; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * Yubico client key | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * @var string | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	private $clientKey; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * URL part of validation server | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * @var string | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	private $url; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * Flag whether to verify HTTPS server certificates or not. | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * @var boolean | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	private $httpsVerify; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * Constructor | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * Sets up the object | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * @param string $id The client identity | 
					
						
							|  |  |  | 	 * @param string $key The client MAC key | 
					
						
							|  |  |  | 	 * @param string $url URL | 
					
						
							|  |  |  | 	 * @param boolean $httpsverify Flag whether to use verify HTTPS | 
					
						
							|  |  |  | 	 *        server certificates | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	public function __construct($id, $key, $url, $httpsverify) { | 
					
						
							|  |  |  | 		$this->clientId = $id; | 
					
						
							|  |  |  | 		$this->clientKey = base64_decode($key); | 
					
						
							|  |  |  | 		$this->httpsVerify = $httpsverify; | 
					
						
							|  |  |  | 		$this->url = $url; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * 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 | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	private function parsePasswordOTP($str, $delim = '[:]') { | 
					
						
							|  |  |  | 		if (!preg_match("/^((.*)" . $delim . ")?(([cbdefghijklnrtuv]{0,12})([cbdefghijklnrtuv]{32}))$/i", $str, $matches)) { | 
					
						
							|  |  |  | 			/* Dvorak? */ | 
					
						
							|  |  |  | 			if (!preg_match("/^((.*)" . $delim . ")?(([jxe\\.uidchtnbpygk]{0,12})([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; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/** | 
					
						
							|  |  |  | 	 * 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 | 
					
						
							| 
									
										
										
										
											2018-12-31 10:42:20 +00:00
										 |  |  | 	 * @throws LAMException if verification failed | 
					
						
							| 
									
										
										
										
											2018-12-31 09:51:44 +00:00
										 |  |  | 	 */ | 
					
						
							| 
									
										
										
										
											2018-12-31 10:42:20 +00:00
										 |  |  | 	public function verify($token, $use_timestamp = null) { | 
					
						
							|  |  |  | 		$timeout = 10; | 
					
						
							| 
									
										
										
										
											2018-12-31 09:51:44 +00:00
										 |  |  | 		/* Construct parameters string */ | 
					
						
							|  |  |  | 		$ret = $this->parsePasswordOTP($token); | 
					
						
							|  |  |  | 		if (!$ret) { | 
					
						
							|  |  |  | 			throw new LAMException(_('Error'), 'Could not parse Yubikey OTP'); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		$params = array( | 
					
						
							|  |  |  | 			'id' => $this->clientId, | 
					
						
							|  |  |  | 			'otp' => $ret['otp'], | 
					
						
							|  |  |  | 			'nonce' => md5(uniqid(getRandomNumber())) | 
					
						
							|  |  |  | 		); | 
					
						
							|  |  |  | 		/* Take care of protocol version 2 parameters */ | 
					
						
							|  |  |  | 		if ($use_timestamp) { | 
					
						
							|  |  |  | 			$params['timestamp'] = 1; | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2018-12-31 10:42:20 +00:00
										 |  |  | 		$params['timeout'] = $timeout; | 
					
						
							| 
									
										
										
										
											2018-12-31 09:51:44 +00:00
										 |  |  | 		ksort($params); | 
					
						
							|  |  |  | 		$parameters = ''; | 
					
						
							|  |  |  | 		foreach ($params as $p => $v) { | 
					
						
							|  |  |  | 			$parameters .= "&" . $p . "=" . $v; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		$parameters = ltrim($parameters, "&"); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		/* Generate signature. */ | 
					
						
							|  |  |  | 		if ($this->clientKey != "") { | 
					
						
							|  |  |  | 			$signature = base64_encode(hash_hmac('sha1', $parameters, $this->clientKey, true)); | 
					
						
							|  |  |  | 			$signature = preg_replace('/\+/', '%2B', $signature); | 
					
						
							|  |  |  | 			$parameters .= '&h=' . $signature; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		$query = $this->url . "?" . $parameters; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		logNewMessage(LOG_DEBUG, 'Yubico url: ' . $query); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		$handle = curl_init($query); | 
					
						
							|  |  |  | 		curl_setopt($handle, CURLOPT_USERAGENT, "LAM 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); | 
					
						
							| 
									
										
										
										
											2018-12-31 10:42:20 +00:00
										 |  |  | 		curl_setopt($handle, CURLOPT_TIMEOUT, $timeout); | 
					
						
							| 
									
										
										
										
											2018-12-31 09:51:44 +00:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		/* Execute and read request. */ | 
					
						
							|  |  |  | 		$this->response = null; | 
					
						
							| 
									
										
										
										
											2018-12-31 10:42:20 +00:00
										 |  |  | 		$str = curl_exec($handle); | 
					
						
							|  |  |  | 		$httpCode = curl_getinfo($handle, CURLINFO_HTTP_CODE); | 
					
						
							|  |  |  | 		curl_close($handle); | 
					
						
							|  |  |  | 		logNewMessage(LOG_DEBUG, 'Server answer: ' . $str); | 
					
						
							|  |  |  | 		if (is_string($str) && ($httpCode == 200) && 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/REPLAYED_OTP/BAD_OTP. | 
					
						
							|  |  |  | 			 * | 
					
						
							|  |  |  | 			 * 3. Return if status=OK or status=REPLAYED_OTP. | 
					
						
							|  |  |  | 			 */ | 
					
						
							|  |  |  | 			if (!preg_match("/otp=" . $params['otp'] . "/", $str) || !preg_match("/nonce=" . $params['nonce'] . "/", $str)) { | 
					
						
							|  |  |  | 				if ($status == 'BAD_OTP') { | 
					
						
							|  |  |  | 					throw new LAMException(_('Error'), 'OTP not accepted. Maybe key is not registered.'); | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 				throw new LAMException(_('Error'), 'Invalid answer ' . $str); | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 			elseif ($this->clientKey != "") { | 
					
						
							|  |  |  | 				/* Case 2. Verify signature first */ | 
					
						
							|  |  |  | 				$rows = explode("\r\n", trim($str)); | 
					
						
							|  |  |  | 				$response = array(); | 
					
						
							|  |  |  | 				foreach ($rows as $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]; | 
					
						
							|  |  |  | 				} | 
					
						
							| 
									
										
										
										
											2018-12-31 09:51:44 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-12-31 10:42:20 +00:00
										 |  |  | 				$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 . '&'; | 
					
						
							| 
									
										
										
										
											2018-12-31 09:51:44 +00:00
										 |  |  | 						} | 
					
						
							| 
									
										
										
										
											2018-12-31 10:42:20 +00:00
										 |  |  | 						$check = $check . $param . '=' . $response[$param]; | 
					
						
							| 
									
										
										
										
											2018-12-31 09:51:44 +00:00
										 |  |  | 					} | 
					
						
							| 
									
										
										
										
											2018-12-31 10:42:20 +00:00
										 |  |  | 				} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				$checksignature = base64_encode(hash_hmac('sha1', utf8_encode($check), $this->clientKey, true)); | 
					
						
							| 
									
										
										
										
											2018-12-31 09:51:44 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-12-31 10:42:20 +00:00
										 |  |  | 				if ($response['h'] == $checksignature) { | 
					
						
							|  |  |  | 					$this->checkStatus($status); | 
					
						
							|  |  |  | 					return; | 
					
						
							|  |  |  | 				} | 
					
						
							|  |  |  | 				else { | 
					
						
							|  |  |  | 					throw new LAMException(_('Error'), 'Invalid signature, expected ' . $checksignature); | 
					
						
							| 
									
										
										
										
											2018-12-31 09:51:44 +00:00
										 |  |  | 				} | 
					
						
							| 
									
										
										
										
											2018-12-31 10:42:20 +00:00
										 |  |  | 			} | 
					
						
							|  |  |  | 			else { | 
					
						
							|  |  |  | 				/* Case 3. We check the status directly */ | 
					
						
							|  |  |  | 				$this->checkStatus($status); | 
					
						
							|  |  |  | 				return; | 
					
						
							| 
									
										
										
										
											2018-12-31 09:51:44 +00:00
										 |  |  | 			} | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2018-12-31 10:42:20 +00:00
										 |  |  | 		throw new LAMException(_('Error'), 'Call to verification service failed with ' . $httpCode); | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2018-12-31 09:51:44 +00:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-12-31 10:42:20 +00:00
										 |  |  | 	/** | 
					
						
							|  |  |  | 	 * Checks if the status is ok. | 
					
						
							|  |  |  | 	 * | 
					
						
							|  |  |  | 	 * @param string $status status | 
					
						
							|  |  |  | 	 * @throws LAMException invalid status | 
					
						
							|  |  |  | 	 */ | 
					
						
							|  |  |  | 	private function checkStatus($status) { | 
					
						
							|  |  |  | 		if ($status == 'REPLAYED_OTP') { | 
					
						
							|  |  |  | 			throw new LAMException(_('Error'), 'OTP replay detected.'); | 
					
						
							| 
									
										
										
										
											2018-12-31 09:51:44 +00:00
										 |  |  | 		} | 
					
						
							| 
									
										
										
										
											2018-12-31 10:42:20 +00:00
										 |  |  | 		elseif ($status == 'BAD_OTP') { | 
					
						
							|  |  |  | 			throw new LAMException(_('Error'), 'OTP not accepted. Maybe key is not registered.'); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		elseif ($status == 'OK') { | 
					
						
							|  |  |  | 			return; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		throw new LAMException(_('Error'), 'Invalid status: ' . $status); | 
					
						
							| 
									
										
										
										
											2018-12-31 09:51:44 +00:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | ?>
 |