syncServers = explode(";", $baseParams['__YKVAL_SYNC_POOL__']); $this->db=new Db($baseParams['__YKVAL_DB_HOST__'], $baseParams['__YKVAL_DB_USER__'], $baseParams['__YKVAL_DB_PW__'], $baseParams['__YKVAL_DB_NAME__']); $this->db->connect(); $this->random_key=rand(0,1<<16); } function DbTimeToUnix($db_time) { $unix=strptime($db_time, '%F %H:%M:%S'); return mktime($unix[tm_hour], $unix[tm_min], $unix[tm_sec], $unix[tm_mon]+1, $unix[tm_mday], $unix[tm_year]+1900); } function UnixToDbTime($unix) { return date('Y-m-d H:i:s', $unix); } function getServer($index) { if (isset($this->syncServers[$index])) return $this->syncServers[$index]; else return ""; } function getLast() { $res=$this->db->last('queue', 1); parse_str($res['info'], $info); return array('modified'=>$this->DbTimeToUnix($res['modified_time']), 'otp'=>$res['otp'], 'server'=>$res['server'], 'yk_identity'=>$info['yk_identity'], 'yk_counter'=>$info['yk_counter'], 'yk_use'=>$info['yk_use'], 'yk_high'=>$info['yk_high'], 'yk_low'=>$info['yk_low']); } public function getQueueLength() { return count($this->db->last('queue', NULL)); } public function queue($modified, $otp, $identity, $counter, $use, $high, $low) { $info='modified=' . $modified . '&otp=' . $otp . '&yk_identity=' . $identity . '&yk_counter=' . $counter . '&yk_use=' . $use . '&yk_high=' . $high . '&yk_low=' . $low; $this->otpParams['modified']=$modified; $this->otpParams['otp']=$otp; $this->otpParams['yk_identity']=$identity; $this->otpParams['yk_counter']=$counter; $this->otpParams['yk_use']=$use; $this->otpParams['yk_high']=$high; $this->otpParams['yk_low']=$low; $res=True; foreach ($this->syncServers as $server) { if(! $this->db->save('queue', array('modified_time'=>$this->UnixToDbTime($modified), 'otp'=>$otp, 'server'=>$server, 'random_key'=>$this->random_key, 'info'=>$info))) $res=False; } return $res; } public function getNumberOfServers() { if (is_array($this->syncServers)) return count($this->syncServers); else return 0; } private function log($level, $msg, $params=NULL) { $logMsg="ykval-synclib:" . $level . ":" . $msg; if ($params) $logMsg .= " modified=" . $params['modified'] . " yk_identity=" . $params['yk_identity'] . " yk_counter=" . $params['yk_counter'] . " yk_use=" . $params['yk_use'] . " yk_high=" . $params['yk_high'] . " yk_low=" . $params['yk_low']; error_log($logMsg); } private function getLocalParams($yk_identity) { $this->log("notice", "searching for " . $yk_identity . " (" . modhex2b64($yk_identity) . ") in local db"); $res = $this->db->lastBy('yubikeys', 'publicName', modhex2b64($yk_identity)); $localParams=array('modified'=>$this->DbTimeToUnix($res['accessed']), 'yk_identity'=>$yk_identity, 'yk_counter'=>$res['counter'], 'yk_use'=>$res['sessionUse'], 'yk_high'=>$res['high'], 'yk_low'=>$res['low']); $this->log("notice", "counter found in db ", $localParams); return $localParams; } private function parseParamsFromMultiLineString($str) { preg_match("/^modified=([0-9]*)/m", $str, $out); $resParams['modified']=$out[1]; preg_match("/^yk_identity=([[:alpha:]]*)/m", $str, $out); $resParams['yk_identity']=$out[1]; preg_match("/^yk_counter=([0-9]*)/m", $str, $out); $resParams['yk_counter']=$out[1]; preg_match("/^yk_use=([0-9]*)/m", $str, $out); $resParams['yk_use']=$out[1]; preg_match("/^yk_high=([0-9]*)/m", $str, $out); $resParams['yk_high']=$out[1]; preg_match("/^yk_low=([0-9]*)/m", $str, $out); $resParams['yk_low']=$out[1]; return $resParams; } public function updateDbCounters($params) { $res=$this->db->lastBy('yubikeys', 'publicName', modhex2b64($params['yk_identity'])); if (isset($res['id'])) { if(! $this->db->update('yubikeys', $res['id'], array('accessed'=>$this->UnixToDbTime($params['modified']), 'counter'=>$params['yk_counter'], 'sessionUse'=>$params['yk_use'], 'low'=>$params['yk_low'], 'high'=>$params['yk_high']))) { error_log("ykval-synclib:critical: failed to update internal DB with new counters"); return false; } else { $this->log("notice", "updated database ", $params); return true; } } else return false; } public function countersHigherThan($p1, $p2) { if ($p1['yk_counter'] > $p2['yk_counter'] || ($p1['yk_counter'] == $p2['yk_counter'] && $p1['yk_use'] > $p2['yk_use'])) return true; else return false; } public function countersHigherThanOrEqual($p1, $p2) { if ($p1['yk_counter'] > $p2['yk_counter'] || ($p1['yk_counter'] == $p2['yk_counter'] && $p1['yk_use'] >= $p2['yk_use'])) return true; else return false; } public function sync($ans_req) { # # Construct URLs # $urls=array(); $res=$this->db->findByMultiple('queue', array("modified_time"=>$this->UnixToDbTime($this->otpParams['modified']), "random_key"=>$this->random_key)); foreach ($res as $row) { $urls[]=$row['server'] . '?' . $row['info']; } # # Send out requests # if (count($urls)>=$ans_req) $ans_arr=$this->retrieveURLasync($urls, $ans_req); else return false; if (!is_array($ans_arr)) { $this->log('warning', 'No responses from validation server pool'); $ans_arr=array(); } # # Parse responses # $localParams=$this->getLocalParams($this->otpParams['yk_identity']); $this->answers = count($ans_arr); $this->valid_answers = 0; foreach ($ans_arr as $answer){ // Parse out parameters from each response $resParams=$this->parseParamsFromMultiLineString($answer); $this->log("notice", "local db contains ", $localParams); $this->log("notice", "response contains ", $resParams); # Check if internal DB should be updated if ($this->countersHigherThan($resParams, $localParams)) { $this->updateDbCounters($resParams); } # Check for warnings # # If received sync response have lower counters than locally saved last counters # (indicating that remote server wasn't synced) if ($this->countersHigherThan($localParams, $resParams)) { $this->log("warning", "Remote server out of sync, local counters ", $localParams); $this->log("warning", "Remote server out of sync, remote counters ", $resParams); } # If received sync response have higher counters than locally saved last counters # (indicating that local server wasn't synced) if ($this->countersHigherThan($resParams, $localParams)) { $this->log("warning", "Local server out of sync, local counters ", $localParams); $this->log("warning", "Local server out of sync, remote counters ", $resParams); } # If received sync response have higher counters than OTP counters # (indicating REPLAYED_OTP) if ($this->countersHigherThanOrEqual($resParams, $this->otpParams)) { $this->log("warning", "replayed OTP, remote counters " , $resParams); $this->log("warning", "replayed OTP, otp counters", $this->otpParams); } # Check if answer marks OTP as valid if (!$this->countersHigherThanOrEqual($resParams, $this->otpParams)) $this->valid_answers++; # Delete entry from table preg_match('/url=(.*)\?/', $answer, $out); $server=$out[1]; debug("server=" . $server); $this->db->deleteByMultiple('queue', array("modified_time"=>$this->UnixToDbTime($this->otpParams['modified']), "random_key"=>$this->random_key, 'server'=>$server)); } /* Return true if valid answers equals required answers. Since we only obtain the required amount of answers from retrieveAsync this indicates that all answers were actually valid. Otherwise, return false. */ if ($this->valid_answers==$ans_req) return True; else return False; } public function getNumberOfValidAnswers() { if (isset($this->valid_answers)) return $this->valid_answers; else return 0; } public function getNumberOfAnswers() { if (isset($this->answers)) return $this->answers; else return 0; } // This function takes a list of URLs. It will return the content of // the first successfully retrieved URL, whose content matches ^OK. // The request are sent asynchronously. Some of the URLs can fail // with unknown host, connection errors, or network timeout, but as // long as one of the URLs given work, data will be returned. If all // URLs fail, data from some URL that did not match parameter $match // (defaults to ^OK) is returned, or if all URLs failed, false. function retrieveURLasync ($urls, $ans_req=1) { $mh = curl_multi_init(); $ch = array(); foreach ($urls as $id => $url) { $handle = curl_init(); curl_setopt($handle, CURLOPT_URL, $url); curl_setopt($handle, CURLOPT_USERAGENT, "YK-VAL"); curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1); curl_setopt($handle, CURLOPT_FAILONERROR, true); curl_setopt($handle, CURLOPT_TIMEOUT, 10); curl_multi_add_handle($mh, $handle); $ch[$handle] = $handle; } $str = false; $ans_count = 0; $ans_arr = array(); do { while (($mrc = curl_multi_exec($mh, $active)) == CURLM_CALL_MULTI_PERFORM) ; while ($info = curl_multi_info_read($mh)) { debug ("YK-KSM multi", $info); if ($info['result'] == CURL_OK) { $str = curl_multi_getcontent($info['handle']); debug($str); if (preg_match("/status=OK/", $str)) { $error = curl_error ($info['handle']); $errno = curl_errno ($info['handle']); $cinfo = curl_getinfo ($info['handle']); debug("YK-KSM errno/error: " . $errno . "/" . $error, $cinfo); $ans_count++; debug("found entry"); $ans_arr[]="url=" . $cinfo['url'] . "\n" . $str; } if ($ans_count >= $ans_req) { foreach ($ch as $h) { curl_multi_remove_handle ($mh, $h); curl_close ($h); } curl_multi_close ($mh); return $ans_arr; } curl_multi_remove_handle ($mh, $info['handle']); curl_close ($info['handle']); unset ($ch[$info['handle']]); } curl_multi_select ($mh); } } while($active); foreach ($ch as $h) { curl_multi_remove_handle ($mh, $h); curl_close ($h); } curl_multi_close ($mh); return $str; } } ?>