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