1
0
mirror of https://github.com/Yubico/yubikey-val.git synced 2024-12-01 15:24:16 +01:00
yubikey-val/ykval-synclib.php

346 lines
10 KiB
PHP

<?php
require_once 'ykval-config.php';
require_once 'ykval-common.php';
require_once 'lib/Db.php';
class SyncLib
{
public $syncServers = null;
public $dbConn = null;
function __construct()
{
global $baseParams;
$this->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='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;
}
}
?>