<?php
/*****************************************
* MultiotpYubikey Class (LGPLv3) *
* André Liechti *
* http://www.multiotp.net/ *
*****************************************/
class MultiotpYubikey
/**
* @class MultiotpYubikey
* @brief Class definition for Yubikey handling.
*
* @author Andre Liechti, SysCo systemes de communication sa, <info@multiotp.net>
* @version 5.4.0.0
* @date 2018-09-05
* @since 2014-11-04
*
*
* Todos
*
* Support Dvorak keyboard "jxe.uidchtnbpygk" instead of "cbdefghijklnrtuv"
* (automatic detection with "x.py" detected or keyboard unknown)
*
*
* Change Log
*
* 2018-09-05 5.4.0.0 SysCo/al FIX: infinity_dev Fix UTC issue
* 2018-02-26 5.1.0.6 SysCo/al ENH: __construct instead of the name of the class
* 2016-03-22 4.3.2.7 SysCo/al ENH: private id support for CheckYubicoOtp method
* 2014-11-04 4.3.0.0 SysCo/al Initial implementation of MultiotpYubikey class
*********************************************************************/
{
// How to get a Yubico API Key: https://upgrade.yubico.com/getapikey/
var $_yubicloud_client_id = 19042; // Client ID (by default, this ID is for multiOTP open source)
var $_yubicloud_secret_key = 'a72X/qkw3vPeT+yRO6lWgipwjPM='; // Secret Key (by default, this key is for multiOTP open source)
var $_yubicloud_timeout = 10; // YubiCloud timeout in seconds
var $_yubicloud_last_response = array(); // YubiCloud last response array
var $_yubicloud_last_result = ''; // YubiCloud last result (text)
var $_yubicloud_max_time_window = 600; // YubiCloud maximum time window in seconds
var $_yubico_modhex_chars = "cbdefghijklnrtuv"; // ModHex values (instead of 0,1,2,3,4,5,6,7,8,9,0,a,b,c,d,e,f)
var $_yubico_modhex_dvorak_chars = "jxe.uidchtnbpygk"; // Dvorak ModHex values (instead of 0,1,2,3,4,5,6,7,8,9,0,a,b,c,d,e,f)
var $_yubico_dvorak_only_chars = "x.py"; // Dvorak only chars
var $_yubico_otp_last_count = -1; // Default value of the last otp counter
function __construct(
$yubicloud_client_id = 0,
$yubicloud_secret_key = ''
) {
if (0 < intval($yubicloud_client_id)) {
$this->_yubicloud_client_id = $yubicloud_client_id;
}
if (28 == strlen($yubicloud_secret_key)) {
$this->_yubicloud_secret_key = $yubicloud_secret_key;
}
}
function CalculateHashHmac(
$algo,
$data,
$key,
$raw_output = false
) {
if (function_exists('hash_hmac')) {
return hash_hmac($algo, $data, $key, $raw_output);
} else {
/***********************************************************************
* Simulate the function hash_hmac if it is not available
* (this function is natively available only for PHP >= 5.1.2)
*
* Source: http://www.php.net/manual/fr/function.hash-hmac.php#93440
*
* @author "KC Cloyd"
***********************************************************************/
$algo = strtolower($algo);
$pack = 'H'.strlen($algo('test'));
$size = 64;
$opad = str_repeat(chr(0x5C), $size);
$ipad = str_repeat(chr(0x36), $size);
if (strlen($key) > $size) {
$key = str_pad(pack($pack, $algo($key)), $size, chr(0x00));
} else {
$key = str_pad($key, $size, chr(0x00));
}
for ($i = 0; $i < strlen($key) - 1; $i++) {
$opad[$i] = $opad[$i] ^ $key[$i];
$ipad[$i] = $ipad[$i] ^ $key[$i];
}
$output = $algo($opad.pack($pack, $algo($ipad.$data)));
return ($raw_output) ? pack($pack, $output) : $output;
}
}
function Iso13239Crc16($buffer)
{
// http://forum.yubico.com/viewtopic.php?f=2&t=69
$crc = 0xffff;
for($loop=0; $loop<strlen($buffer); $loop++) {
$crc ^= ord($buffer[$loop]) & 0xff;
for ($bit=0; $bit<8; $bit++) {
$j=$crc & 1;
$crc >>= 1;
if ($j) {
$crc ^= 0x8408;
}
}
}
return $crc;
}
function CheckYubicoOtp($yubico_modhex_encrypted_part,
$secret,
$last_count = -1,
$private_id = "") {
$result = 99; // ERROR: Authentication failed (and other possible unknown errors)
$encrypted_part = hex2bin($this->ModHexToHex($yubico_modhex_encrypted_part));
$cipher_aes = new Crypt_AES(CRYPT_AES_MODE_ECB);
$cipher_aes->setKey((hex2bin($secret)));
$cipher_aes->disablePadding();
$decrypted_part = $cipher_aes->decrypt(($encrypted_part));
unset($cipher_aes);
$uid = bin2hex(substr($decrypted_part, 0, 6));
$useCtr = ord($decrypted_part[6]) + 256 * ord($decrypted_part[7]);
$tstp = ord($decrypted_part[8]) + 256 * ord($decrypted_part[9]) + 65536 * ord($decrypted_part[10]);
$sessionCtr = ord($decrypted_part[11]);
$rnd = ord($decrypted_part[12]) + 256 * ord($decrypted_part[13]);
$crc = ord($decrypted_part[14]) + 256 * ord($decrypted_part[15]);
$check_crc = $this->Iso13239Crc16($decrypted_part);
// Based on information available here: http://www.yubico.com/wp-content/uploads/2013/07/YubiKey-Manual-v3_1.pdf
//
// $uid Private ID
// $useCtr Usage counter, non-volatile counter, incremented when device is used after a power-up or reset
// $tstp Timestamp, 8Hz, random value startup, wraps from 0xffffff to 0 (after 24 days)
// $sessionCtr Session usage counter, set to 0 at power-up, incremented by one after each generation
// $rnd Random number
// $crc Checksum, 16-bit ISO13239 1st complement checksum of the first 14 bytes, result added to the end
// $crc = 0xffff - $this->Iso13239Crc16(substr($decrypted_part, 0, 14)); // One's complement
// $check_crc Calculate the ISO13239 of the 16 bits, should give a fixed residual of 0xf0b8 if checksum is valid
// CRC Check should always give 0xf0b8
if (0xf0b8 == $check_crc) {
if (("" != $private_id) && ("000000000000" != $private_id) && ($private_id != $uid)) {
$result = 97; // ERROR: Authentication failed (wrong private id)
} else {
$counter_position = ($useCtr * 256) + $sessionCtr;
if ($counter_position <= $last_count) {
$result = 26; // ERROR: This token has already been used
} else {
$this->_yubico_otp_last_count = $counter_position;
$result = 0; // OK: Token accepted
}
}
} else {
$result = 96; // ERROR: Authentication failed (CRC error)
}
return $result;
}
function GetYubicoOtpLastCount() {
return $this->_yubico_otp_last_count;
}
function GetYubiCloudLastResponse() {
return $this->_yubicloud_last_response;
}
function GetYubiCloudLastResult() {
return $this->_yubicloud_last_result;
}
function CheckOnYubiCloud($otp_to_check) {
/**
* Validation Protocol Version 2.0 is implemented
* (https://code.google.com/p/yubikey-val-server-php/wiki/ValidationProtocolV20)
* Old validation Protocol Version 1.0 is not implemented anymore
* (https://code.google.com/p/yubikey-val-server-php/wiki/ValidationProtocolV10)
*/
$this->_yubicloud_last_response = array();
$this->_yubicloud_last_result = 'NOT_ENOUGH_ANSWERS';
$yubiotp = trim($otp_to_check);
$result = 99;
if ((44 == strlen($yubiotp)) && ($this->IsModHex($yubiotp))) {
$yubicloud_servers = array('api.yubico.com/wsapi/2.0/verify',
'api2.yubico.com/wsapi/2.0/verify',
'api3.yubico.com/wsapi/2.0/verify',
'api4.yubico.com/wsapi/2.0/verify',
'api5.yubico.com/wsapi/2.0/verify');
$yubicloud_parameters = array('id' => $this->_yubicloud_client_id,
'otp' => $yubiotp,
'timestamp' => 1,
'nonce' => md5(uniqid(rand())),
/* 'sl' => '', */ /* precentage of syncing not well documented */
'timeout' => $this->_yubicloud_timeout
);
// Parameters must be in the right order in order to calculate the hash
ksort($yubicloud_parameters);
$url_parameters = '';
foreach($yubicloud_parameters as $key=>$value) {
$url_parameters .= "&".$key."=".$value;
}
$url_parameters = substr($url_parameters, 1);
if (28 == strlen($this->_yubicloud_secret_key)) {
$yubicloud_hash = urlencode(base64_encode($this->CalculateHashHmac('sha1',
$url_parameters,
base64_decode($this->_yubicloud_secret_key),
TRUE
)));
$url_parameters.= '&h='.$yubicloud_hash;
}
foreach($yubicloud_servers as $one_yubicloud_server) {
$yubicloud_answer = '';
$yubicloud_url = $one_yubicloud_server.'?'.$url_parameters;
$protocol = ''; // Default is http
$port = 80;
$pos = strpos($yubicloud_url, '://');
if (FALSE !== $pos) {
switch (strtolower(substr($yubicloud_url,0,$pos))) {
case 'https':
case 'ssl':
$protocol = 'ssl://';
$port = 443;
break;
case 'tls':
$protocol = 'tls://';
$port = 443;
break;
}
$yubicloud_url = substr($yubicloud_url,$pos+3);
}
$pos = strpos($yubicloud_url, '/');
if (FALSE === $pos) {
$host = $yubicloud_url;
$url = '/';
} else {
$host = substr($yubicloud_url,0,$pos);
$url = substr($yubicloud_url,$pos); // And not +1 as we want the / at the beginning
}
$pos = strpos($host, ':');
if (FALSE !== $pos) {
$port = substr($host,$pos+1);
$host = substr($host,0,$pos);
}
$errno = 0;
$errdesc = 0;
$fp = @fsockopen($protocol.$host, $port, $errno, $errdesc, $this->_yubicloud_timeout);
if (FALSE !== $fp) {
$info['timed_out'] = FALSE;
fputs($fp, "GET ".$url." HTTP/1.0\r\n");
fputs($fp, "Content-Type: application/x-www-form-urlencoded\r\n");
// fputs($fp, "Content-Length: ".strlen($content_to_post)."\r\n");
fputs($fp, "User-Agent: multiOTP\r\n");
fputs($fp, "Host: ".$host."\r\n");
fputs($fp, "\r\n");
// fputs($fp, $content_to_post);
fputs($fp, "\r\n");
stream_set_blocking($fp, TRUE);
stream_set_timeout($fp, $this->_yubicloud_timeout);
$info = stream_get_meta_data($fp);
$reply = '';
$last_length = 0;
while ((!feof($fp)) && ((!$info['timed_out']) || ($last_length != strlen($reply)))) {
$last_length = strlen($reply);
$reply.= fgets($fp, 1024);
$info = stream_get_meta_data($fp);
@ob_flush(); // Avoid notice if any (if the buffer is empty and therefore cannot be flushed)
flush();
}
fclose($fp);
if (!($info['timed_out'])) {
$pos = strpos(strtolower($reply), "\r\n\r\n");
$header = substr($reply, 0, $pos);
$yubicloud_response = substr($reply, $pos + 4);
$yubicloud_response_array = explode("\r\n", trim($yubicloud_response));
$response = array();
$response['now_utc'] = date ("U");
foreach($yubicloud_response_array as $one_yubicloud_response) {
/* = is also used in BASE64 encoding so we only replace the first = by # which is not used in BASE64 */
list($key,$value) = explode('=', $one_yubicloud_response, 2);
$response[$key] = $value;
}
$yubicloud_response_parameters = array('otp',
'nonce',
't',
'status',
'timestamp',
'sessioncounter',
'sessionuse',
'sl'
);
// Parameters must be in the right order in order to calculate the hash
sort($yubicloud_response_parameters);
if (isset($response['t'])) {
$posZ=strrpos($response['t'], 'Z');
$responseTime = $response['t'];
if ($posZ > 0)
$responseTime = substr($response['t'], 0, $posZ);
$tzbackup = date_default_timezone_get();
date_default_timezone_set('UTC');
$response['t_utc'] = date_format(date_create($responseTime), "U");
date_default_timezone_set($tzbackup);
}
$parameters_for_hash = '';
foreach ($yubicloud_response_parameters as $one_parameter) {
if (array_key_exists($one_parameter, $response)) {
if ('' != $parameters_for_hash) {
$parameters_for_hash.= '&';
}
$parameters_for_hash.= $one_parameter.'='.$response[$one_parameter];
}
}
$this->_yubicloud_last_response = $response;
$check_response_hash = "NO-VALID-SECRET-KEY";
if (28 == strlen($this->_yubicloud_secret_key)) {
$check_response_hash = base64_encode($this->CalculateHashHmac('sha1',
$parameters_for_hash,
base64_decode($this->_yubicloud_secret_key),
TRUE
));
}
if (($check_response_hash != $response['h']) && ("NO-VALID-SECRET-KEY" != $check_response_hash)) {
$this->_yubicloud_last_result = 'BAD_SIGNATURE';
$result = 99;
} elseif ($yubicloud_parameters['nonce'] != $response['nonce']) {
$this->_yubicloud_last_result = 'BAD_NONCE';
$result = 99;
} elseif($yubiotp != $response['otp']) {
$this->_yubicloud_last_result = 'OTP_IS_DIFFERENT';
$result = 99;
} elseif ((($response['t_utc'] - $this->_yubicloud_max_time_window) > $response['now_utc']) ||
(($response['t_utc'] + $this->_yubicloud_max_time_window) < $response['now_utc'])
) {
$this->_yubicloud_last_result = 'OUT_OF_TIME_WINDOW';
$result = 99;
} else {
$this->_yubicloud_last_result = $response['status'];
switch ($response['status']) {
case 'OK':
$result = 0;
break;
case 'BAD_OTP':
$result = 23;
break;
case 'REPLAYED_OTP':
case 'REPLAYED_REQUEST':
$result = 26;
break;
case 'BAD_SIGNATURE':
case 'MISSING_PARAMETER':
case 'NO_SUCH_CLIENT':
case 'OPERATION_NOT_ALLOWED':
case 'BACKEND_ERROR':
case 'NOT_ENOUGH_ANSWERS':
default:
$result = 99;
}
}
if (99 != $result) {
break;
}
}
}
}
}
return $result;
}
function IsModHex($modhex)
{
$result = FALSE;
if (0 == (strlen($modhex) % 2)) {
for ($loop = 0; $loop < strlen($modhex); $loop++) {
$value = strpos($this->_yubico_modhex_chars, strtolower($modhex[$loop]));
if (FALSE === $value) {
return FALSE;
}
}
$result = TRUE;
}
return $result;
}
function HexToModHex($hexa) {
$result = '';
if (0 == (strlen($hexa) % 2)) {
for ($loop = 0; $loop < strlen($hexa); $loop++) {
$value = hexdec(strtolower($hexa[$loop]));
if ($value > 15) {
return FALSE;
}
$result.= $this->_yubico_modhex_chars[$value];
}
} else {
$result = FALSE;
}
return $result;
}
function ModHexToHex($modhex) {
$result = '';
if (0 == (strlen($modhex) % 2)) {
for ($loop = 0; $loop < strlen($modhex); $loop++) {
$value = strpos($this->_yubico_modhex_chars, strtolower($modhex[$loop]));
if (FALSE === $value) {
return FALSE;
}
$result.= dechex($value);
}
} else {
$result = FALSE;
}
return $result;
}
}
?>
|