PHP Classes

File: contrib/MultiotpYubikey.php

Recommend this page to a friend!
  Classes of André Liechti   multiOTP PHP class   contrib/MultiotpYubikey.php   Download  
File: contrib/MultiotpYubikey.php
Role: Auxiliary script
Content type: text/plain
Description: Auxiliary script
Class: multiOTP PHP class
Authenticate and manage OTP strong user tokens
Author: By
Last change: Update of contrib/MultiotpYubikey.php
Date: 8 months ago
Size: 21,086 bytes
 

Contents

Class file image Download
<?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; } } ?>