diff -r 000000000000 -r 9997bee9ad03 yms/backend.php --- /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 @@ +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'; +}