yms/backend.php
changeset 0 9997bee9ad03
child 2 bbdd428926b9
equal deleted inserted replaced
-1:000000000000 0:9997bee9ad03
       
     1 <?php
       
     2 
       
     3 function yms_add_yubikey($key, $otp, $client_id = false, $enabled = true, $any_client = false, $notes = false)
       
     4 {
       
     5   global $db, $session, $paths, $template, $plugins; // Common objects
       
     6   
       
     7   if ( $client_id === false )
       
     8     $client_id = $session->user_id;
       
     9   
       
    10   $key = yms_tobinary($key);
       
    11   $otp = yms_tobinary($otp);
       
    12   
       
    13   if ( strlen($key) != 16 )
       
    14   {
       
    15     return 'yms_err_addkey_invalid_key';
       
    16   }
       
    17   
       
    18   if ( strlen($otp) != 22 )
       
    19   {
       
    20     return 'yms_err_addkey_invalid_otp';
       
    21   }
       
    22   
       
    23   $otpdata = yms_decode_otp($otp, $key);
       
    24   if ( $otpdata === false )
       
    25   {
       
    26     return 'yms_err_addkey_invalid_otp';
       
    27   }
       
    28   if ( !$otpdata['crc_good'] )
       
    29   {
       
    30     return 'yms_err_addkey_crc_failed';
       
    31   }
       
    32   
       
    33   // make sure it's not already in there
       
    34   $q = $db->sql_query('SELECT 1 FROM ' . table_prefix . "yms_yubikeys WHERE public_id = '{$otpdata['publicid']}';");
       
    35   if ( !$q )
       
    36     $db->_die();
       
    37   
       
    38   if ( $db->numrows() > 0 )
       
    39   {
       
    40     $db->free_result();
       
    41     return 'yms_err_addkey_key_exists';
       
    42   }
       
    43   $db->free_result();
       
    44   
       
    45   $now = time();
       
    46   $key = yms_hex_encode($key);
       
    47   
       
    48   $flags = 0;
       
    49   if ( $enabled )
       
    50     $flags |= YMS_ENABLED;
       
    51   if ( $any_client )
       
    52     $flags |= YMS_ANY_CLIENT;
       
    53   
       
    54   $notes = $notes ? $db->escape(strval($notes)) : '';
       
    55   
       
    56   $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"
       
    57          . "  ($client_id, '{$otpdata['publicid']}', '{$otpdata['privateid']}', {$otpdata['session']}, {$otpdata['count']}, $now, $now, {$otpdata['timestamp']}, '$key', $flags, '$notes');");
       
    58   if ( !$q )
       
    59     $db->_die();
       
    60   
       
    61   return true;
       
    62 }
       
    63 
       
    64 function yms_chown_yubikey($otp, $client_id = false, $enabled = true, $any_client = false, $notes = false)
       
    65 {
       
    66   global $db, $session, $paths, $template, $plugins; // Common objects
       
    67   
       
    68   if ( $client_id === false )
       
    69     $client_id = $session->user_id;
       
    70   
       
    71   $otp = yms_tobinary($otp);
       
    72   
       
    73   if ( strlen($otp) != 22 )
       
    74   {
       
    75     return 'yms_err_addkey_invalid_otp';
       
    76   }
       
    77   
       
    78   $public_id = yms_hex_encode(substr($otp, 0, 6));
       
    79   
       
    80   // make sure it's already in there
       
    81   $q = $db->sql_query('SELECT id FROM ' . table_prefix . "yms_yubikeys WHERE public_id = '{$public_id}' AND client_id = 0;");
       
    82   if ( !$q )
       
    83     $db->_die();
       
    84   
       
    85   if ( $db->numrows() < 1 )
       
    86   {
       
    87     // this should never happen, as the OTP is put through validation before this function is called
       
    88     $db->free_result();
       
    89     return 'yms_err_claimkey_owner_invalid';
       
    90   }
       
    91   
       
    92   list($key_id) = $db->fetchrow_num();
       
    93   $db->free_result();
       
    94   
       
    95   $now = time();
       
    96   
       
    97   $flags = 0;
       
    98   if ( $enabled )
       
    99     $flags |= YMS_ENABLED;
       
   100   if ( $any_client )
       
   101     $flags |= YMS_ANY_CLIENT;
       
   102   
       
   103   $notes = $notes ? $db->escape(strval($notes)) : '';
       
   104   
       
   105   $q = $db->sql_query("UPDATE " . table_prefix . "yms_yubikeys SET flags = $flags, notes = '$notes', client_id = $client_id WHERE id = $key_id;");
       
   106   if ( !$q )
       
   107     $db->_die();
       
   108   
       
   109   return true;
       
   110 }
       
   111 
       
   112 function yms_validate_custom_field($value, $otp, $url)
       
   113 {
       
   114   require_once(ENANO_ROOT . '/includes/http.php');
       
   115   $url = strtr($url, array(
       
   116       '%c' => rawurlencode($value),
       
   117       '%o' => rawurlencode($otp)
       
   118     ));
       
   119   // do we need to sign this?
       
   120   if ( strstr($url, '%h') && ($key = getConfig('yms_claim_auth_key', false)) )
       
   121   {
       
   122     list(, $signpart) = explode('?', $url);
       
   123     $signpart = preg_replace('/(&h=%h|^h=%h&)/', '', $signpart);
       
   124     $signpart = yms_ksort_url($signpart);
       
   125     
       
   126     $key = yms_tobinary($key);
       
   127     $key = yms_hex_encode($key);
       
   128     $hash = hmac_sha1($signpart, $key);
       
   129     $hash = yms_hex_decode($hash);
       
   130     $hash = base64_encode($hash);
       
   131     
       
   132     $url = str_replace('%h', rawurlencode($hash), $url);
       
   133   }
       
   134   
       
   135   // run authentication
       
   136   $result = yms_get_url($url);
       
   137   $result = yms_parse_auth_result($result, $key);
       
   138   
       
   139   if ( !$result['sig_valid'] )
       
   140     return 'yubiauth_err_response_bad_signature';
       
   141   
       
   142   if ( $result['status'] !== 'OK' )
       
   143   {
       
   144     if ( preg_match('/^[A-Z_]+$/', $result['status']) )
       
   145       return 'yubiauth_err_response_' . strtolower($result['status']);
       
   146     else
       
   147       return $result['status'];
       
   148   }
       
   149   
       
   150   // authentication is ok
       
   151   return true;
       
   152 }
       
   153 
       
   154 function yms_get_url($url)
       
   155 {
       
   156   require_once(ENANO_ROOT . '/includes/http.php');
       
   157   
       
   158   $url = preg_replace('#^https?://#i', '', $url);
       
   159   if ( !preg_match('#^(\[?[a-z0-9-:]+(?:\.[a-z0-9-:]+\]?)*)(?::([0-9]+))?(/.*)$#U', $url, $match) )
       
   160   {
       
   161     return 'invalid_auth_url';
       
   162   }
       
   163   $server =& $match[1];
       
   164   $port = ( !empty($match[2]) ) ? intval($match[2]) : 80;
       
   165   $uri =& $match[3];
       
   166   try
       
   167   {
       
   168     $req = new Request_HTTP($server, $uri, 'GET', $port);
       
   169     $response = $req->get_response_body();
       
   170   }
       
   171   catch ( Exception $e )
       
   172   {
       
   173     return 'http_failed:' . $e->getMessage();
       
   174   }
       
   175   
       
   176   if ( $req->response_code !== HTTP_OK )
       
   177     return 'http_failed_status:' . $req->response_code;
       
   178   
       
   179   return $response;
       
   180 }
       
   181 
       
   182 function yms_parse_auth_result($result, $api_key = false)
       
   183 {
       
   184   $result = explode("\n", trim($result));
       
   185   $arr = array();
       
   186   foreach ( $result as $line )
       
   187   {
       
   188     list($name) = explode('=', $line);
       
   189     $value = substr($line, strlen($name) + 1);
       
   190     $arr[$name] = $value;
       
   191   }
       
   192   // signature check
       
   193   if ( $api_key )
       
   194   {
       
   195     $signarr = $arr;
       
   196     ksort($signarr);
       
   197     unset($signarr['h']);
       
   198     $signpart = array();
       
   199     foreach ( $signarr as $name => $value )
       
   200       $signpart[] = "{$name}={$value}";
       
   201     
       
   202     $signpart = implode('&', $signpart);
       
   203     $api_key = yms_hex_encode(yms_tobinary($api_key));
       
   204     $right_sig = base64_encode(yms_hex_decode(
       
   205                    hmac_sha1($signpart, $api_key)
       
   206                  ));
       
   207     $arr['sig_valid'] = ( $arr['h'] === $right_sig );
       
   208   }
       
   209   else
       
   210   {
       
   211     $arr['sig_valid'] = true;
       
   212   }
       
   213   return $arr;
       
   214 }
       
   215 
       
   216 function yms_ksort_url($signpart)
       
   217 {
       
   218   $arr = array();
       
   219   $values = explode('&', $signpart);
       
   220   foreach ( $values as $var )
       
   221   {
       
   222     list($name) = explode('=', $var);
       
   223     $value = substr($var, strlen($name) + 1);
       
   224     $arr[$name] = $value;
       
   225   }
       
   226   ksort($arr);
       
   227   $result = array();
       
   228   foreach ( $arr as $name => $value )
       
   229   {
       
   230     $result[] = "{$name}={$value}";
       
   231   }
       
   232   return implode('&', $result);
       
   233 }
       
   234 
       
   235 function yms_validate_otp($otp, $id)
       
   236 {
       
   237   global $db, $session, $paths, $template, $plugins; // Common objects
       
   238   
       
   239   $public_id = yms_modhex_decode(substr($otp, 0, 12));
       
   240   if ( !$public_id )
       
   241   {
       
   242     return 'BAD_OTP';
       
   243   }
       
   244   // Just in case
       
   245   $public_id = $db->escape($public_id);
       
   246   
       
   247   $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';");
       
   248   if ( !$q )
       
   249     $db->_die();
       
   250   
       
   251   if ( $db->numrows($q) < 1 )
       
   252   {
       
   253     return 'NO_SUCH_KEY';
       
   254   }
       
   255   
       
   256   list($yubikey_id, $private_id, $session_count, $token_count, $access_time, $token_time, $aes_secret, $flags, $client_id) = $db->fetchrow_num($q);
       
   257   $session_count = intval($session_count);
       
   258   $token_count = intval($token_count);
       
   259   $access_time = intval($access_time);
       
   260   $token_time = intval($token_time);
       
   261   
       
   262   // check flags
       
   263   if ( $client_id > 0 )
       
   264   {
       
   265     if ( !($flags & YMS_ANY_CLIENT) )
       
   266     {
       
   267       return 'NO_SUCH_KEY';
       
   268     }
       
   269     if ( !($flags & YMS_ENABLED) )
       
   270     {
       
   271       return 'NO_SUCH_KEY';
       
   272     }
       
   273   }
       
   274   
       
   275   // decode the OTP
       
   276   $otp = yms_decode_otp($otp, $aes_secret);
       
   277   
       
   278   // check CRC
       
   279   if ( !$otp['crc_good'] )
       
   280   {
       
   281     return 'BAD_OTP';
       
   282   }
       
   283   
       
   284   // check private UID (avoids combining a whitelisted known public UID with the increment part of a malicious token)
       
   285   if ( $private_id !== $otp['privateid'] )
       
   286   {
       
   287     return 'BAD_OTP';
       
   288   }
       
   289   
       
   290   // check counters
       
   291   if ( $otp['session'] < $session_count )
       
   292   {
       
   293     return 'REPLAYED_OTP';
       
   294   }
       
   295   if ( $otp['session'] == $session_count && $otp['count'] <= $token_count )
       
   296   {
       
   297     return 'REPLAYED_OTP';
       
   298   }
       
   299   
       
   300   // update DB
       
   301   $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;");
       
   302   if ( !$q )
       
   303     $db->_die();
       
   304   
       
   305   // check timestamp
       
   306   if ( $otp['session'] == $session_count )
       
   307   {
       
   308     $expect_delta = time() - $access_time;
       
   309     // 8Hz Yubikey internal clock
       
   310     $actual_delta = intval(( $otp['timestamp'] - $token_time ) / 8);
       
   311     $fuzz = 150;
       
   312     if ( !yms_within($expect_delta, $actual_delta, $fuzz) )
       
   313     {
       
   314       // if we have a likely wraparound, just pass it
       
   315       if ( !($token_time > 0xe80000 && $otp['timestamp'] < 0x800000) )
       
   316       {
       
   317         return 'BAD_OTP';
       
   318       }
       
   319     }
       
   320     // $debug_array = array('ts_debug_delta_expected' => $expect_delta, 'ts_debug_delta_received' => $actual_delta);
       
   321   }
       
   322   
       
   323   // looks like we're good
       
   324   return 'OK';
       
   325 }