* @copyright 2009 Simon Josefsson * @license http://opensource.org/licenses/bsd-license.php New BSD License * @version 1.6 * @link http://www.yubico.com/ */ /** * Class for verifying Yubico One-Time-Passcodes * * Simple example: * * 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 "

Authentication failed: " . $auth->getMessage(); * print "

Debug output from server: " . $yubi->getLastResponse(); * } else { * print "

You are authenticated!"; * } * */ class Yubico_Auth { /**#@+ * @access private */ /** * Yubico client ID * @var string */ var $_id; /** * Yubico client key * @var string */ var $_key; /** * URL part of validation server * @var string */ var $_url; /** * Query to server * @var string */ var $_query; /** * Response from server * @var string */ var $_response; /** * Flag whether to use https or not. * @var string */ var $_https; /** * Constructor * * Sets up the object * @param string The client identity * @param string The client MAC key (optional) * @param boolean Flag whether to use https (optional) * @access public */ function __construct($id, $key = '', $https = 0) { $this->_id = $id; $this->_key = base64_decode($key); $this->_https = $https; } /** * Specify to use a different URL part for verification. * The default is "api.yubico.com/wsapi/verify". * * @param string New server URL part to use * @access public */ function setURLpart($url) { $this->_url = $url; } /** * Get URL part to use for validation. * * @return string Server URL part * @access public */ function getURLpart() { if ($this->_url) { return $this->_url; } else { return "api.yubico.com/wsapi/verify"; } } /** * Return the last query sent to the server, if any. * * @return string Output from server * @access public */ function getLastQuery() { return $this->_query; } /** * Return the last data received from the server, if any. * * @return string Output from server * @access public */ function getLastResponse() { return $this->_response; } /** * 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}))$/", $str, $matches)) { return false; } $ret['password'] = $matches[2]; $ret['otp'] = $matches[3]; $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 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)) { throw new Zend_Exception('Could not parse parameter ' . $param . ' from response'); } $param_array[$param]=$out[1]; } return $param_array; } /** * Verify Yubico OTP * * @param string $token Yubico OTP * @param int $use_timestamp 1=>send request with ×tamp=1 to get timestamp * and session information in the response * @return mixed PEAR error on error, true otherwise * @access public */ function verify($token, $use_timestamp=null) { $ret = $this->parsePasswordOTP($token); if (!$ret) { throw new Zend_Exception('Could not parse Yubikey OTP'); } $parameters = "id=" . $this->_id . "&otp=" . $ret['otp']; if ($use_timestamp) $parameters = $parameters . "×tamp=1"; /* Generate signature. */ if($this->_key <> "") { $signature = base64_encode(hash_hmac('sha1', $parameters, $this->_key, true)); $signature = preg_replace('/\+/', '%2B', $signature); $parameters .= '&h=' . $signature; } /* Support https. */ if ($this->_https) { $this->_query = "https://"; } else { $this->_query = "http://"; } $this->_query .= $this->getURLpart(); $this->_query .= "?"; $this->_query .= $parameters; $ch = curl_init($this->_query); curl_setopt($ch, CURLOPT_USERAGENT, "PEAR Auth_Yubico"); curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); $this->_response = curl_exec($ch); curl_close($ch); if(!preg_match("/status=([a-zA-Z0-9_]+)/", $this->_response, $out)) { throw new Zend_Exception('Could not parse response'); } $status = $out[1]; /* Verify signature. */ if($this->_key <> "") { $rows = split("\r\n", $this->_response); while (list($key, $val) = each($rows)) { // = 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 = split("#", $val); // MONKEYS fix $response[$row[0]] = @$row[1]; } $parameters=array("sessioncounter", "sessionuse", "status", "t", "timestamp"); // MONKEYS FIX $check = ''; foreach ($parameters as $param) { if (@$response[$param]!=null) { if (@$check) $check = $check . '&'; $check = $check . $param . '=' . $response[$param]; } } $checksignature = base64_encode(hash_hmac('sha1', $check, $this->_key, true)); // MONKEYS FIX if($response['h'] != $checksignature) { throw new Zend_Exception('Checked Signature failed'); } } if ($status != 'OK') { throw new Zend_Exception($status); } return true; } } ?>