yms/backend.php
changeset 0 9997bee9ad03
child 2 bbdd428926b9
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/yms/backend.php	Fri Jul 31 23:51:08 2009 -0400
@@ -0,0 +1,325 @@
+<?php
+
+function yms_add_yubikey($key, $otp, $client_id = false, $enabled = true, $any_client = false, $notes = false)
+{
+  global $db, $session, $paths, $template, $plugins; // Common objects
+  
+  if ( $client_id === false )
+    $client_id = $session->user_id;
+  
+  $key = yms_tobinary($key);
+  $otp = yms_tobinary($otp);
+  
+  if ( strlen($key) != 16 )
+  {
+    return 'yms_err_addkey_invalid_key';
+  }
+  
+  if ( strlen($otp) != 22 )
+  {
+    return 'yms_err_addkey_invalid_otp';
+  }
+  
+  $otpdata = yms_decode_otp($otp, $key);
+  if ( $otpdata === false )
+  {
+    return 'yms_err_addkey_invalid_otp';
+  }
+  if ( !$otpdata['crc_good'] )
+  {
+    return 'yms_err_addkey_crc_failed';
+  }
+  
+  // make sure it's not already in there
+  $q = $db->sql_query('SELECT 1 FROM ' . table_prefix . "yms_yubikeys WHERE public_id = '{$otpdata['publicid']}';");
+  if ( !$q )
+    $db->_die();
+  
+  if ( $db->numrows() > 0 )
+  {
+    $db->free_result();
+    return 'yms_err_addkey_key_exists';
+  }
+  $db->free_result();
+  
+  $now = time();
+  $key = yms_hex_encode($key);
+  
+  $flags = 0;
+  if ( $enabled )
+    $flags |= YMS_ENABLED;
+  if ( $any_client )
+    $flags |= YMS_ANY_CLIENT;
+  
+  $notes = $notes ? $db->escape(strval($notes)) : '';
+  
+  $q = $db->sql_query("INSERT INTO " . table_prefix . "yms_yubikeys(client_id, public_id, private_id, session_count, token_count, create_time, access_time, token_time, aes_secret, flags, notes) VALUES\n"
+         . "  ($client_id, '{$otpdata['publicid']}', '{$otpdata['privateid']}', {$otpdata['session']}, {$otpdata['count']}, $now, $now, {$otpdata['timestamp']}, '$key', $flags, '$notes');");
+  if ( !$q )
+    $db->_die();
+  
+  return true;
+}
+
+function yms_chown_yubikey($otp, $client_id = false, $enabled = true, $any_client = false, $notes = false)
+{
+  global $db, $session, $paths, $template, $plugins; // Common objects
+  
+  if ( $client_id === false )
+    $client_id = $session->user_id;
+  
+  $otp = yms_tobinary($otp);
+  
+  if ( strlen($otp) != 22 )
+  {
+    return 'yms_err_addkey_invalid_otp';
+  }
+  
+  $public_id = yms_hex_encode(substr($otp, 0, 6));
+  
+  // make sure it's already in there
+  $q = $db->sql_query('SELECT id FROM ' . table_prefix . "yms_yubikeys WHERE public_id = '{$public_id}' AND client_id = 0;");
+  if ( !$q )
+    $db->_die();
+  
+  if ( $db->numrows() < 1 )
+  {
+    // this should never happen, as the OTP is put through validation before this function is called
+    $db->free_result();
+    return 'yms_err_claimkey_owner_invalid';
+  }
+  
+  list($key_id) = $db->fetchrow_num();
+  $db->free_result();
+  
+  $now = time();
+  
+  $flags = 0;
+  if ( $enabled )
+    $flags |= YMS_ENABLED;
+  if ( $any_client )
+    $flags |= YMS_ANY_CLIENT;
+  
+  $notes = $notes ? $db->escape(strval($notes)) : '';
+  
+  $q = $db->sql_query("UPDATE " . table_prefix . "yms_yubikeys SET flags = $flags, notes = '$notes', client_id = $client_id WHERE id = $key_id;");
+  if ( !$q )
+    $db->_die();
+  
+  return true;
+}
+
+function yms_validate_custom_field($value, $otp, $url)
+{
+  require_once(ENANO_ROOT . '/includes/http.php');
+  $url = strtr($url, array(
+      '%c' => rawurlencode($value),
+      '%o' => rawurlencode($otp)
+    ));
+  // do we need to sign this?
+  if ( strstr($url, '%h') && ($key = getConfig('yms_claim_auth_key', false)) )
+  {
+    list(, $signpart) = explode('?', $url);
+    $signpart = preg_replace('/(&h=%h|^h=%h&)/', '', $signpart);
+    $signpart = yms_ksort_url($signpart);
+    
+    $key = yms_tobinary($key);
+    $key = yms_hex_encode($key);
+    $hash = hmac_sha1($signpart, $key);
+    $hash = yms_hex_decode($hash);
+    $hash = base64_encode($hash);
+    
+    $url = str_replace('%h', rawurlencode($hash), $url);
+  }
+  
+  // run authentication
+  $result = yms_get_url($url);
+  $result = yms_parse_auth_result($result, $key);
+  
+  if ( !$result['sig_valid'] )
+    return 'yubiauth_err_response_bad_signature';
+  
+  if ( $result['status'] !== 'OK' )
+  {
+    if ( preg_match('/^[A-Z_]+$/', $result['status']) )
+      return 'yubiauth_err_response_' . strtolower($result['status']);
+    else
+      return $result['status'];
+  }
+  
+  // authentication is ok
+  return true;
+}
+
+function yms_get_url($url)
+{
+  require_once(ENANO_ROOT . '/includes/http.php');
+  
+  $url = preg_replace('#^https?://#i', '', $url);
+  if ( !preg_match('#^(\[?[a-z0-9-:]+(?:\.[a-z0-9-:]+\]?)*)(?::([0-9]+))?(/.*)$#U', $url, $match) )
+  {
+    return 'invalid_auth_url';
+  }
+  $server =& $match[1];
+  $port = ( !empty($match[2]) ) ? intval($match[2]) : 80;
+  $uri =& $match[3];
+  try
+  {
+    $req = new Request_HTTP($server, $uri, 'GET', $port);
+    $response = $req->get_response_body();
+  }
+  catch ( Exception $e )
+  {
+    return 'http_failed:' . $e->getMessage();
+  }
+  
+  if ( $req->response_code !== HTTP_OK )
+    return 'http_failed_status:' . $req->response_code;
+  
+  return $response;
+}
+
+function yms_parse_auth_result($result, $api_key = false)
+{
+  $result = explode("\n", trim($result));
+  $arr = array();
+  foreach ( $result as $line )
+  {
+    list($name) = explode('=', $line);
+    $value = substr($line, strlen($name) + 1);
+    $arr[$name] = $value;
+  }
+  // signature check
+  if ( $api_key )
+  {
+    $signarr = $arr;
+    ksort($signarr);
+    unset($signarr['h']);
+    $signpart = array();
+    foreach ( $signarr as $name => $value )
+      $signpart[] = "{$name}={$value}";
+    
+    $signpart = implode('&', $signpart);
+    $api_key = yms_hex_encode(yms_tobinary($api_key));
+    $right_sig = base64_encode(yms_hex_decode(
+                   hmac_sha1($signpart, $api_key)
+                 ));
+    $arr['sig_valid'] = ( $arr['h'] === $right_sig );
+  }
+  else
+  {
+    $arr['sig_valid'] = true;
+  }
+  return $arr;
+}
+
+function yms_ksort_url($signpart)
+{
+  $arr = array();
+  $values = explode('&', $signpart);
+  foreach ( $values as $var )
+  {
+    list($name) = explode('=', $var);
+    $value = substr($var, strlen($name) + 1);
+    $arr[$name] = $value;
+  }
+  ksort($arr);
+  $result = array();
+  foreach ( $arr as $name => $value )
+  {
+    $result[] = "{$name}={$value}";
+  }
+  return implode('&', $result);
+}
+
+function yms_validate_otp($otp, $id)
+{
+  global $db, $session, $paths, $template, $plugins; // Common objects
+  
+  $public_id = yms_modhex_decode(substr($otp, 0, 12));
+  if ( !$public_id )
+  {
+    return 'BAD_OTP';
+  }
+  // Just in case
+  $public_id = $db->escape($public_id);
+  
+  $q = $db->sql_query("SELECT id, private_id, session_count, token_count, access_time, token_time, aes_secret, flags, client_id FROM " . table_prefix . "yms_yubikeys WHERE ( client_id = 0 or client_id = $id OR flags & " . YMS_ANY_CLIENT . " ) AND public_id = '$public_id';");
+  if ( !$q )
+    $db->_die();
+  
+  if ( $db->numrows($q) < 1 )
+  {
+    return 'NO_SUCH_KEY';
+  }
+  
+  list($yubikey_id, $private_id, $session_count, $token_count, $access_time, $token_time, $aes_secret, $flags, $client_id) = $db->fetchrow_num($q);
+  $session_count = intval($session_count);
+  $token_count = intval($token_count);
+  $access_time = intval($access_time);
+  $token_time = intval($token_time);
+  
+  // check flags
+  if ( $client_id > 0 )
+  {
+    if ( !($flags & YMS_ANY_CLIENT) )
+    {
+      return 'NO_SUCH_KEY';
+    }
+    if ( !($flags & YMS_ENABLED) )
+    {
+      return 'NO_SUCH_KEY';
+    }
+  }
+  
+  // decode the OTP
+  $otp = yms_decode_otp($otp, $aes_secret);
+  
+  // check CRC
+  if ( !$otp['crc_good'] )
+  {
+    return 'BAD_OTP';
+  }
+  
+  // check private UID (avoids combining a whitelisted known public UID with the increment part of a malicious token)
+  if ( $private_id !== $otp['privateid'] )
+  {
+    return 'BAD_OTP';
+  }
+  
+  // check counters
+  if ( $otp['session'] < $session_count )
+  {
+    return 'REPLAYED_OTP';
+  }
+  if ( $otp['session'] == $session_count && $otp['count'] <= $token_count )
+  {
+    return 'REPLAYED_OTP';
+  }
+  
+  // update DB
+  $q = $db->sql_query("UPDATE " . table_prefix . "yms_yubikeys SET session_count = {$otp['session']}, token_count = {$otp['count']}, access_time = " . time() . ", token_time = {$otp['timestamp']} WHERE id = $yubikey_id;");
+  if ( !$q )
+    $db->_die();
+  
+  // check timestamp
+  if ( $otp['session'] == $session_count )
+  {
+    $expect_delta = time() - $access_time;
+    // 8Hz Yubikey internal clock
+    $actual_delta = intval(( $otp['timestamp'] - $token_time ) / 8);
+    $fuzz = 150;
+    if ( !yms_within($expect_delta, $actual_delta, $fuzz) )
+    {
+      // if we have a likely wraparound, just pass it
+      if ( !($token_time > 0xe80000 && $otp['timestamp'] < 0x800000) )
+      {
+        return 'BAD_OTP';
+      }
+    }
+    // $debug_array = array('ts_debug_delta_expected' => $expect_delta, 'ts_debug_delta_received' => $actual_delta);
+  }
+  
+  // looks like we're good
+  return 'OK';
+}