plugins/yubikey/corelib.php
changeset 39 6212d849ab08
parent 38 d109af008343
equal deleted inserted replaced
38:d109af008343 39:6212d849ab08
     1 <?php
       
     2 
       
     3 define('YK_SEC_NORMAL_USERNAME', 1);
       
     4 define('YK_SEC_NORMAL_PASSWORD', 2);
       
     5 define('YK_SEC_ELEV_USERNAME', 4);
       
     6 define('YK_SEC_ELEV_PASSWORD', 8);
       
     7 define('YK_SEC_ALLOW_NO_OTP', 16);
       
     8 
       
     9 define('YK_DEFAULT_VERIFY_URL', 'http://api.yubico.com/wsapi/verify');
       
    10 
       
    11 function generate_yubikey_field($name = 'yubikey_otp', $value = false)
       
    12 {
       
    13 	global $lang;
       
    14 	
       
    15 	$fid = substr(sha1(microtime() . mt_rand()), 0, 12);
       
    16 	$class = $value ? 'wasfull' : 'wasempty';
       
    17 	$html = '<input id="yubifield' . $fid . '" class="' . $class . '" type="hidden" name="' . $name . '" value="' . ( is_string($value) ? $value : '' ) . '" />';
       
    18 	$html .= '<noscript><input type="text" name="' . $name . '" class="yubikey_noscript" value="' . ( is_string($value) ? $value : '' ) . '" /> </noscript>';
       
    19 	if ( $value )
       
    20 	{
       
    21 		$html .= '<span id="yubistat' . $fid . '" class="yubikey_status enrolled">' . $lang->get('yubiauth_ctl_status_enrolled') . '</span>';
       
    22 		$atext = $lang->get('yubiauth_ctl_btn_change_key');
       
    23 		$classadd = ' abutton_green';
       
    24 	}
       
    25 	else
       
    26 	{
       
    27 		$html .= '<span id="yubistat' . $fid . '" class="yubikey_status empty">' . $lang->get('yubiauth_ctl_status_empty') . '</span>';
       
    28 		$atext = $lang->get('yubiauth_ctl_btn_enroll');
       
    29 		$classadd = '';
       
    30 	}
       
    31 	
       
    32 	$html .= ' <span class="yubikey_pubkey">';
       
    33 	if ( !empty($value) )
       
    34 		$html .= htmlspecialchars(substr($value, 0, 12));
       
    35 	$html .= '</span> ';
       
    36 	
       
    37 	$html .= ' <a class="abutton' . $classadd . ' yubikey_enroll" onclick="yk_mb_init(\'yubifield' . $fid . '\', \'yubistat' . $fid . '\'); return false;" href="#enroll">' . $atext . '</a>';
       
    38 	if ( $value )
       
    39 	{
       
    40 		$html .= ' <a class="abutton abutton_red yubikey_enroll" onclick="yk_clear(\'yubifield' . $fid . '\', \'yubistat' . $fid . '\'); return false;" href="#enroll">'
       
    41  						. $lang->get('yubiauth_ctl_btn_clear') .
       
    42  						'</a>';
       
    43 	}
       
    44 	
       
    45 	return $html;
       
    46 }
       
    47 
       
    48 function yubikey_validate_otp($otp)
       
    49 {
       
    50 	$api_key = getConfig('yubikey_api_key');
       
    51 	$api_id  = getConfig('yubikey_api_key_id');
       
    52 	// Don't require an API key or user ID to be installed if we're using local YMS
       
    53 	if ( !(getConfig('yubikey_use_local_yms', 0) && defined('YMS_INSTALLED')) && (!$api_key || !$api_id) )
       
    54 	{
       
    55 		return array(
       
    56 				'success' => false,
       
    57 				'error' => 'missing_api_key'
       
    58 			);
       
    59 	}
       
    60 	if ( !preg_match('/^[cbdefghijklnrtuv]{44}$/', $otp) )
       
    61 	{
       
    62 		return array(
       
    63 				'success' => false,
       
    64 				'error' => 'otp_invalid_chars'
       
    65 			);
       
    66 	}
       
    67 	// are we using local YMS?
       
    68 	if ( getConfig('yubikey_use_local_yms', 0) && defined('YMS_INSTALLED') )
       
    69 	{
       
    70 		$result = yms_validate_otp($otp, $api_id);
       
    71 		if ( $result == 'OK' )
       
    72 		{
       
    73 			return array(
       
    74 					'success' => true
       
    75 				);
       
    76 		}
       
    77 		else
       
    78 		{
       
    79 			return array(
       
    80 				'success' => false,
       
    81 				'error' => strtolower("response_{$result}")
       
    82 			);
       
    83 		}
       
    84 	}
       
    85 	// make HTTP request
       
    86 	require_once( ENANO_ROOT . '/includes/http.php' );
       
    87 	$auth_url = getConfig('yubikey_auth_server', YK_DEFAULT_VERIFY_URL);
       
    88 	$auth_url = preg_replace('#^https?://#i', '', $auth_url);
       
    89 	if ( !preg_match('#^(\[?[a-z0-9-:]+(?:\.[a-z0-9-:]+\]?)*)(?::([0-9]+))?(/.*)$#U', $auth_url, $match) )
       
    90 	{
       
    91 		return array(
       
    92 				'success' => false,
       
    93 				'error' => 'invalid_auth_url'
       
    94 			);
       
    95 	}
       
    96 	$auth_server =& $match[1];
       
    97 	$auth_port = ( !empty($match[2]) ) ? intval($match[2]) : 80;
       
    98 	$auth_uri =& $match[3];
       
    99 	try
       
   100 	{
       
   101 		$req = new Request_HTTP($auth_server, $auth_uri, 'GET', $auth_port);
       
   102 		$req->add_get('id', strval($api_id));
       
   103 		$req->add_get('otp', $otp);
       
   104 		$req->add_get('h', yubikey_sign($req->parms_get));
       
   105 	
       
   106 		$response = $req->get_response_body();
       
   107 	}
       
   108 	catch ( Exception $e )
       
   109 	{
       
   110 		return array(
       
   111 				'success' => false,
       
   112 				'error' => 'http_failed',
       
   113 				'http_error' => $e->getMessage()
       
   114 			);
       
   115 	}
       
   116 	
       
   117 	if ( $req->response_code != HTTP_OK )
       
   118 	{
       
   119 		return array(
       
   120 				'success' => false,
       
   121 				'error' => 'http_response_error'
       
   122 			);
       
   123 	}
       
   124 	$response = trim($response);
       
   125 	if ( !preg_match_all('/^([a-z0-9_]+)=(.*?)\r?$/m', $response, $matches) )
       
   126 	{
       
   127 		return array(
       
   128 				'success' => false,
       
   129 				'error' => 'malformed_response'
       
   130 			);
       
   131 	}
       
   132 	$response = array();
       
   133 	foreach ( $matches[0] as $i => $_ )
       
   134 	{
       
   135 		$response[$matches[1][$i]] = $matches[2][$i];
       
   136 	}
       
   137 	// make sure we have a status
       
   138 	if ( !isset($response['status']) )
       
   139 	{
       
   140 		return array(
       
   141 				'success' => false,
       
   142 				'error' => 'response_missing_status'
       
   143 			);
       
   144 	}
       
   145 	// verify response signature
       
   146 	// MISSING_PARAMETER is the ONLY situation under which an unsigned response is acceptable
       
   147 	if ( $response['status'] !== 'MISSING_PARAMETER' )
       
   148 	{
       
   149 		if ( !isset($response['h']) )
       
   150 		{
       
   151 			return array(
       
   152 					'success' => false,
       
   153 					'error' => 'response_missing_sig'
       
   154 				);
       
   155 		}
       
   156 		if ( yubikey_sign($response) !== $response['h'] )
       
   157 		{
       
   158 			return array(
       
   159 					'success' => false,
       
   160 					'error' => 'response_invalid_sig'
       
   161 				);
       
   162 		}
       
   163 	}
       
   164 	if ( $response['status'] === 'OK' )
       
   165 	{
       
   166 		if ( yubikey_verify_timestamp($response['t']) )
       
   167 		{
       
   168 			return array(
       
   169 					'success' => true
       
   170 				);
       
   171 		}
       
   172 		else
       
   173 		{
       
   174 			return array(
       
   175 					'success' => false,
       
   176 					'error' => 'timestamp_check_failed'
       
   177 				);
       
   178 		}
       
   179 	}
       
   180 	else
       
   181 	{
       
   182 		return array(
       
   183 				'success' => false,
       
   184 				'error' => strtolower("response_{$response['status']}")
       
   185 			);
       
   186 	}
       
   187 }
       
   188 
       
   189 function yubikey_sign($arr, $use_api_key = false)
       
   190 {
       
   191 	static $api_key = false;
       
   192 	
       
   193 	ksort($arr);
       
   194 	
       
   195 	if ( !$use_api_key )
       
   196 	{
       
   197 		if ( !$api_key )
       
   198 		{
       
   199 			$api_key = getConfig('yubikey_api_key');
       
   200 			$api_key = hexencode(base64_decode($api_key), '', '');
       
   201 		}
       
   202 		$use_api_key = $api_key;
       
   203 	}
       
   204 	/*
       
   205 	else
       
   206 	{
       
   207 		$use_api_key = hexencode(base64_decode($use_api_key), '', '');
       
   208 	}
       
   209 	*/
       
   210 	
       
   211 	foreach ( array('h', 'title', 'auth', 'do') as $key )
       
   212 	{
       
   213 		if ( isset($arr[$key]) )
       
   214 			unset($arr[$key]);
       
   215 	}
       
   216 	
       
   217 	$req = array();
       
   218 	foreach ( $arr as $key => $val )
       
   219 	{
       
   220 		$req[] = "$key=$val";
       
   221 	}
       
   222 	$req = implode('&', $req);
       
   223 	
       
   224 	$sig = hmac_sha1($req, $use_api_key);
       
   225 	$sig = hexdecode($sig);
       
   226 	$sig = base64_encode($sig);
       
   227 	
       
   228 	return $sig;
       
   229 }
       
   230 
       
   231 /**
       
   232  * Validate the timestamp returned in a Yubico API response. Borrowed from Drupal and backported for friendliness with earlier versions of PHP.
       
   233  * @param string Yubico timestamp
       
   234  * @return bool True if valid, false otherwise
       
   235  */
       
   236 
       
   237 function yubikey_verify_timestamp($timestamp)
       
   238 {
       
   239 	$tolerance = intval(getConfig('yubikey_api_ts_tolerance', 150));
       
   240 	
       
   241 	$now = time();
       
   242 	$timestamp_seconds = yk_strtotime($timestamp);
       
   243 
       
   244 	if ( !$timestamp || !$now || !$timestamp_seconds )
       
   245 	{
       
   246 		return false;
       
   247 	}
       
   248 
       
   249 	if ( ( $timestamp_seconds + $tolerance ) > $now && ( $timestamp_seconds - $tolerance ) < $now )
       
   250 	{
       
   251 		return true;
       
   252 	}
       
   253 
       
   254 	return false;
       
   255 }
       
   256 
       
   257 function yk_strtotime($timestamp)
       
   258 {
       
   259 	if ( !preg_match('/^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(?:Z[0-9]+)?$/', $timestamp, $match) )
       
   260 		return 0;
       
   261 	
       
   262 	$hour = intval($match[4]);
       
   263 	$minute = intval($match[5]);
       
   264 	$second = intval($match[6]);
       
   265 	$month = intval($match[2]);
       
   266 	$day = intval($match[3]);
       
   267 	$year = intval($match[1]);
       
   268 	
       
   269 	return gmmktime($hour, $minute, $second, $month, $day, $year);
       
   270 }
       
   271 
       
   272 $plugins->attachHook('compile_template', 'yubikey_attach_headers($this);');
       
   273 
       
   274 function yubikey_attach_headers(&$template)
       
   275 {
       
   276 	global $db, $session, $paths, $template, $plugins; // Common objects
       
   277 	
       
   278 	if ( getConfig('yubikey_enable', '1') != '1' )
       
   279 		return true;
       
   280 	
       
   281 	$template->add_header('<script type="text/javascript" src="' . scriptPath . '/plugins/yubikey/yubikey.js"></script>');
       
   282 	$template->add_header('<link rel="stylesheet" type="text/css" href="' . scriptPath . '/plugins/yubikey/yubikey.css" />');
       
   283 	// config option for all users have yubikey
       
   284 	$user_flags = 0;
       
   285 	$yk_enabled = 0;
       
   286 	if ( $session->user_logged_in )
       
   287 	{
       
   288 		$q = $db->sql_query('SELECT COUNT(y.yubi_uid) > 0, u.user_yubikey_flags FROM ' . table_prefix . "yubikey AS y LEFT JOIN " . table_prefix . "users AS u ON ( u.user_id = y.user_id ) WHERE y.user_id = {$session->user_id} GROUP BY u.user_id, u.user_yubikey_flags;");
       
   289 		if ( !$q )
       
   290 			$db->_die();
       
   291 		
       
   292 		list($yk_enabled, $user_flags) = $db->fetchrow_num();
       
   293 		$db->free_result();
       
   294 	}
       
   295 	$yk_enabled = intval($yk_enabled);
       
   296 	$user_flags = intval($user_flags);
       
   297 	
       
   298 	$template->add_header('<script type="text/javascript">var yk_reg_require_otp = ' . getConfig('yubikey_reg_require_otp', '0') . '; var yk_user_enabled = ' . $yk_enabled . '; var yk_user_flags = ' . $user_flags . ';</script>');
       
   299 }
       
   300