1
0
mirror of https://github.com/Yubico/yubikey-val.git synced 2025-03-16 03:29:18 +01:00

Committed first trial version for replication protocol.

This commit is contained in:
Olov Danielson 2009-12-02 17:32:20 +00:00
parent 682c1d94cd
commit f04dcbc0e7
9 changed files with 1183 additions and 13 deletions

375
lib/Db.php Normal file
View File

@ -0,0 +1,375 @@
<?php
/**
* Class for managing database connection
*
* LICENSE:
*
* Copyright (c) 2009 Yubico. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* o Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* o Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* o The names of the authors may not be used to endorse or promote
* products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* @author Olov Danielson <olov.danielson@gmail.com>
* @copyright 2009 Yubico
* @license http://opensource.org/licenses/bsd-license.php New BSD License
* @link http://www.yubico.com/
* @link http://code.google.com/p/yubikey-timedelta-server-php/
*/
class Db
{
/**
* Constructor
*
* @param string $host Database host
* @param string $user Database user
* @param string $pwd Database password
* @param string $name Database table name
* @return void
*
*/
public function __construct($host, $user, $pwd, $db_name)
{
$this->host=$host;
$this->user=$user;
$this->pwd=$pwd;
$this->db_name=$db_name;
}
/**
* function to convert Db timestamps to unixtime(s)
*
* @param string $updated Database timestamp
* @return int Timestamp in unixtime format
*
*/
public function timestampToTime($updated)
{
$stamp=strptime($updated, '%F %H:%M:%S');
return mktime($stamp[tm_hour], $stamp[tm_min], $stamp[tm_sec], $stamp[tm_mon]+1, $stamp[tm_mday], $stamp[tm_year]);
}
/**
* function to compute delta (s) between 2 Db timestamps
*
* @param string $first Database timestamp 1
* @param string $second Database timestamp 2
* @return int Deltatime (s)
*
*/
public function timestampDeltaTime($first, $second)
{
return Db::timestampToTime($second) - Db::timestampToTime($first);
}
/**
* function to disconnect from database
*
* @return boolean True on success, otherwise false.
*
*/
public function disconnect()
{
if ($this->db_conn!=NULL) {
mysql_close($this->db_conn);
$this->db_conn=NULL;
}
}
/**
* function to check if database is connected
*
* @return boolean True if connected, otherwise false.
*
*/
public function isConnected()
{
if ($this->db_conn!=NULL) return True;
else return False;
}
/**
* function to connect to database defined in config.php
*
* @return boolean True on success, otherwise false.
*
*/
public function connect(){
if (! $this->db_conn = mysql_connect($this->host, $this->user, $this->pwd)) {
echo 'Could not connect: ' . mysql_error();
return false;
}
if (! mysql_select_db($this->db_name)) {
echo 'Could not select database ' . $this->db_name;
$this->disconnect();
return false;
}
return true;
}
public function truncateTable($name)
{
mysql_query("TRUNCATE TABLE " . $name);
}
/**
* function to update row in database
*
* @param string $table Database table to update row in
* @param int $id Id on row to update
* @param array $values Array with key=>values to update
* @return boolean True on success, otherwise false.
*
*/
public function update($table, $id, $values)
{
foreach ($values as $key=>$value){
if ($value != null) $query = $query . " " . $key . "='" . $value . "',";
}
if (! $query) {
log("no values to set in query. Not updating DB");
return true;
}
$query = rtrim($query, ",") . " WHERE id = " . $id;
// Insert UPDATE statement at beginning
$query = "UPDATE " . $table . " SET " . $query;
if (! mysql_query($query)){
echo 'Query failed: ' . mysql_error();
echo 'Query was: ' . $query;
error_log('Query failed: ' . mysql_error());
error_log('Query was: ' . $query);
return false;
}
return true;
}
/**
* function to insert new row in database
*
* @param string $table Database table to update row in
* @param array $values Array with key=>values to update
* @return boolean True on success, otherwise false.
*
*/
public function save($table, $values)
{
$query= 'INSERT INTO ' . $table . " (";
foreach ($values as $key=>$value){
if ($value != null) $query = $query . $key . ",";
}
$query = rtrim($query, ",") . ') VALUES (';
foreach ($values as $key=>$value){
if ($value != null) $query = $query . "'" . $value . "',";
}
$query = rtrim($query, ",");
$query = $query . ")";
if (! mysql_query($query)){
echo 'Query failed: ' . mysql_error();
echo 'Query was: ' . $query;
return false;
}
return true;
}
/**
* helper function to collect last row[s] in database
*
* @param string $table Database table to update row in
* @param int $nr Number of rows to collect. NULL=>inifinity. DEFAULT=1.
* @return mixed Array with values from Db row or 2d-array with multiple rows
or false on failure.
*
*/
public function last($table, $nr=1)
{
return Db::findBy($table, null, null, $nr, 1);
}
/**
* main function used to get rows from Db table.
*
* @param string $table Database table to update row in
* @param string $key Column to select rows by
* @param string $value Value to select rows by
* @param int $nr Number of rows to collect. NULL=>inifinity. Default=NULL.
* @param int $rev rev=1 indicates order should be reversed. Default=NULL.
* @return mixed Array with values from Db row or 2d-array with multiple rows
*
*/
public function findBy($table, $key, $value, $nr=null, $rev=null)
{
$query="SELECT * FROM " . $table;
if ($key!=null) $query.= " WHERE " . $key . " = '" . $value . "'";
if ($rev==1) $query.= " ORDER BY id DESC";
if ($nr!=null) $query.= " LIMIT " . $nr;
$result = mysql_query($query);
if (! $result) {
echo 'Query failed: ' . mysql_error();
echo 'Query was: ' . $query;
return false;
}
if ($nr==1) {
$row = mysql_fetch_array($result, MYSQL_ASSOC);
return $row;
}
else {
$collection=array();
while($row = mysql_fetch_array($result, MYSQL_ASSOC)){
$collection[]=$row;
}
return $collection;
}
}
/**
* main function used to get rows by multiple key=>value pairs from Db table.
*
* @param string $table Database table to update row in
* @param array $where Array with column=>values to select rows by
* @param int $nr Number of rows to collect. NULL=>inifinity. Default=NULL.
* @param int $rev rev=1 indicates order should be reversed. Default=NULL.
* @param string distinct Select rows with distinct columns, Default=NULL
* @return mixed Array with values from Db row or 2d-array with multiple rows
*
*/
public function findByMultiple($table, $where, $nr=null, $rev=null, $distinct=null)
{
$query="SELECT";
if ($distinct!=null) {
$query.= " DISTINCT " . $distinct;
} else {
$query.= " *";
}
$query.= " FROM " . $table;
if ($where!=null){
$query.= " WHERE";
foreach ($where as $key=>$value) {
$query.= " ". $key . " = '" . $value . "' and";
}
$query=rtrim($query, "and");
$query=rtrim($query);
}
if ($rev==1) $query.= " ORDER BY id DESC";
if ($nr!=null) $query.= " LIMIT " . $nr;
$result = mysql_query($query);
if (! $result) {
echo 'Query failed: ' . mysql_error();
echo 'Query was: ' . $query;
return false;
}
if ($nr==1) {
$row = mysql_fetch_array($result, MYSQL_ASSOC);
return $row;
}
else {
$collection=array();
while($row = mysql_fetch_array($result, MYSQL_ASSOC)){
$collection[]=$row;
}
return $collection;
}
}
/**
* main function used to delete rows by multiple key=>value pairs from Db table.
*
* @param string $table Database table to delete row in
* @param array $where Array with column=>values to select rows by
* @param int $nr Number of rows to collect. NULL=>inifinity. Default=NULL.
* @param int $rev rev=1 indicates order should be reversed. Default=NULL.
* @param string distinct Select rows with distinct columns, Default=NULL
* @return mixed Array with values from Db row or 2d-array with multiple rows
*
*/
public function deleteByMultiple($table, $where, $nr=null, $rev=null)
{
$query="DELETE";
$query.= " FROM " . $table;
if ($where!=null){
$query.= " WHERE";
foreach ($where as $key=>$value) {
$query.= " ". $key . " = '" . $value . "' and";
}
$query=rtrim($query, "and");
$query=rtrim($query);
}
if ($rev==1) $query.= " ORDER BY id DESC";
if ($nr!=null) $query.= " LIMIT " . $nr;
$result = mysql_query($query);
if (! $result) {
echo 'Query failed: ' . mysql_error();
echo 'Query was: ' . $query;
return false;
}
return $result;
}
public function customQuery($query)
{
return mysql_query($query);
}
/**
* helper function used to get rows from Db table in reversed order.
* defaults to obtaining 1 row.
*
* @param string $table Database table to update row in
* @param string $key Column to select rows by
* @param string $value Value to select rows by
* @param int $nr Number of rows to collect. NULL=>inifinity. Default=1.
* @return mixed Array with values from Db row or 2d-array with multiple rows or false on failure.
*
*/
public function lastBy($table, $key, $value, $nr=1)
{
return Db::findBy($table, $key, $value, $nr, 1);
}
/**
* helper function used to get rows from Db table in standard order.
* defaults to obtaining 1 row.
*
* @param string $table Database table to update row in
* @param string $key Column to select rows by
* @param string $value Value to select rows by
* @param int $nr Number of rows to collect. NULL=>inifinity. Default=1.
* @return mixed Array with values from Db row or 2d-array with multiple rows or false on failure.
*
*/
public function firstBy($table, $key, $value, $nr=1)
{
return Db::findBy($table, $key, $value, $nr);
}
}
?>

42
tests/DbTest.php Normal file
View File

@ -0,0 +1,42 @@
<?php
require_once(dirname(__FILE__) . '/../ykval-config.php');
require_once(dirname(__FILE__) . '/../lib/Db.php');
require_once 'PHPUnit/Framework.php';
class DbTest extends PHPUnit_Framework_TestCase
{
public function setup()
{
global $baseParams;
$this->db=new Db($baseParams['__YKVAL_DB_HOST__'],
'root',
'lab',
$baseParams['__YKVAL_DB_NAME__']);
$this->db->connect();
$this->db->customQuery("drop table unittest");
$this->db->customQuery("create table unittest (value1 int, value2 int)");
}
public function test_template()
{
}
public function testConnect()
{
$this->assertTrue($this->db->isConnected());
$this->db->disconnect();
$this->assertFalse($this->db->isConnected());
}
public function testSave()
{
$this->assertTrue($this->db->save('unittest', array('value1'=>100,
'value2'=>200)));
$res=$this->db->findByMultiple('unittest', array('value1'=>100,
'value2'=>200));
$this->assertEquals(1, count($res));
}
}
?>

186
tests/syncLibTest.php Normal file
View File

@ -0,0 +1,186 @@
<?php
require_once 'PHPUnit/Framework.php';
require_once (dirname(__FILE__) . '/../ykval-synclib.php');
require_once(dirname(__FILE__) . '/../ykval-config.php');
require_once(dirname(__FILE__) . '/../lib/Db.php');
class SyncLibTest extends PHPUnit_Framework_TestCase
{
public function setup()
{
global $baseParams;
$db = new Db($baseParams['__YKVAL_DB_HOST__'],
'root',
'lab',
$baseParams['__YKVAL_DB_NAME__']);
$db->connect();
# $db->truncateTable('queue');
$db->disconnect();
}
public function testTemplate()
{
}
public function testConstructor()
{
$sl = new SyncLib();
$this->assertGreaterThan(1, $sl->getNumberOfServers());
$this->assertEquals($sl->getServer(0), "api2.yubico.com/wsapi/sync");
}
public function testQueue()
{
$sl = new SyncLib();
$nr_servers = $sl->getNumberOfServers();
$queue_length = $sl->getQueueLength();
$sl->queue(1259585588,
"ccccccccccccfrhiutjgfnvgdurgliidceuilikvfhui",
"cccccccccccc",
10,
20,
100,
1000);
$this->assertEquals($nr_servers + $queue_length, $sl->getQueueLength());
$lastSync=$sl->getLast();
$this->assertEquals($lastSync['modified'], 1259585588);
$this->assertEquals($lastSync['otp'], "ccccccccccccfrhiutjgfnvgdurgliidceuilikvfhui");
$this->assertEquals($lastSync['yk_identity'], "cccccccccccc");
$this->assertEquals($lastSync['yk_counter'], 10);
$this->assertEquals($lastSync['yk_use'], 20);
$this->assertEquals($lastSync['yk_high'], 100);
$this->assertEquals($lastSync['yk_low'], 1000);
}
public function testCountersHigherThan()
{
$sl = new SyncLib();
$localParams=array('yk_counter'=>100,
'yk_use'=>10);
$otpParams=array('yk_counter'=>100,
'yk_use'=>11);
$this->assertTrue($sl->countersHigherThan($otpParams, $localParams));
$this->assertFalse($sl->countersHigherThan($localParams, $otpParams));
$otpParams['yk_use']=10;
$this->assertFalse($sl->countersHigherThan($otpParams, $localParams));
$otpParams['yk_counter']=99;
$this->assertFalse($sl->countersHigherThan($otpParams, $localParams));
$otpParams['yk_counter']=101;
$this->assertTrue($sl->countersHigherThan($otpParams, $localParams));
}
public function testCountersHigherThanOrEqual()
{
$sl = new SyncLib();
$localParams=array('yk_counter'=>100,
'yk_use'=>10);
$otpParams=array('yk_counter'=>100,
'yk_use'=>11);
$this->assertTrue($sl->countersHigherThanOrEqual($otpParams, $localParams));
$this->assertFalse($sl->countersHigherThanOrEqual($localParams, $otpParams));
$otpParams['yk_use']=10;
$this->assertTrue($sl->countersHigherThanOrEqual($otpParams, $localParams));
$otpParams['yk_counter']=99;
$this->assertFalse($sl->countersHigherThanOrEqual($otpParams, $localParams));
$otpParams['yk_counter']=101;
$this->assertTrue($sl->countersHigherThanOrEqual($otpParams, $localParams));
}
public function testSync1()
{
$sl = new SyncLib();
$sl->syncServers = array("http://localhost/wsapi/syncvalid1",
"http://localhost/wsapi/syncvalid2",
"http://localhost/wsapi/syncvalid3");
$start_length=$sl->getQueueLength();
$this->assertTrue($sl->queue(1259671571+1000,
"ccccccccccccculnnjikvhjduicubtkcvgvkcdcvdjhk",
"cccccccccccc",
9,
3,
55,
18000));
$res=$sl->sync(3);
$this->assertEquals(3, $sl->getNumberOfValidAnswers());
$this->assertTrue($res, "all sync servers should be configured to return ok values");
$this->assertEquals($start_length, $sl->getQueueLength());
$this->assertTrue($sl->queue(1259671571+1000,
"ccccccccccccculnnjikvhjduicubtkcvgvkcdcvdjhk",
"cccccccccccc",
9,
3,
55,
18000));
$res=$sl->sync(2);
$this->assertEquals(2, $sl->getNumberOfValidAnswers());
$this->assertTrue($res, "all sync servers should be configured to return ok values");
$this->assertEquals($start_length+1, $sl->getQueueLength());
}
public function testSync2()
{
$sl = new SyncLib();
$sl->syncServers = array("http://localhost/wsapi/syncinvalid1",
"http://localhost/wsapi/syncinvalid2",
"http://localhost/wsapi/syncinvalid3");
$start_length=$sl->getQueueLength();
$this->assertTrue($sl->queue(1259671571+1000,
"ccccccccccccculnnjikvhjduicubtkcvgvkcdcvdjhk",
"cccccccccccc",
9,
3,
55,
18000));
$res=$sl->sync(3);
$this->assertEquals(0, $sl->getNumberOfValidAnswers());
$this->assertFalse($res, "only 1 sync server should have returned ok values");
$this->assertEquals($start_length, $sl->getQueueLength());
}
public function testSync3()
{
$sl = new SyncLib();
$sl->syncServers = array("http://localhost/wsapi/syncvalid1",
"http://localhost/wsapi/syncvalid2",
"http://localhost/wsapi/syncvalid3");
$start_length=$sl->getQueueLength();
$this->assertTrue($sl->queue(1259671571+1000,
"ccccccccccccculnnjikvhjduicubtkcvgvkcdcvdjhk",
"cccccccccccc",
9,
3,
55,
18000));
$res=$sl->sync(1);
$this->assertEquals(1, $sl->getNumberOfValidAnswers());
$this->assertTrue($res, "only 1 sync server should have returned ok values");
$this->assertEquals($start_length+2, $sl->getQueueLength());
}
public function testActivateQueue()
{
}
}
?>

View File

@ -9,6 +9,8 @@ define('S_MISSING_PARAMETER', 'MISSING_PARAMETER');
define('S_NO_SUCH_CLIENT', 'NO_SUCH_CLIENT');
define('S_OPERATION_NOT_ALLOWED', 'OPERATION_NOT_ALLOWED');
define('S_BACKEND_ERROR', 'BACKEND_ERROR');
define('S_NOT_ENOUGH_ANSWERS', 'NOT_ENOUGH_ANSWERS');
define('TS_SEC', 1/8);
define('TS_REL_TOLERANCE', 0.3);
@ -74,6 +76,19 @@ function getUTCTimeStamp() {
return date('Y-m-d\TH:i:s\Z0', time()) . $tiny;
}
# NOTE: When we evolve to using general DB-interface, this functinality
# should be moved there.
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);
}
// Sign a http query string in the array of key-value pairs
// return b64 encoded hmac hash
function sign($a, $apiKey) {
@ -113,9 +128,9 @@ function modhex2b64 ($modhex_str) {
// 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 ^OK is returned,
// or if all URLs failed, false.
function retrieveURLasync ($urls) {
// 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, $match="^OK", $returl=False) {
$mh = curl_multi_init();
$ch = array();
@ -134,6 +149,8 @@ function retrieveURLasync ($urls) {
}
$str = false;
$ans_count = 0;
$ans_arr = array();
do {
while (($mrc = curl_multi_exec($mh, $active)) == CURLM_CALL_MULTI_PERFORM)
@ -143,22 +160,29 @@ function retrieveURLasync ($urls) {
debug ("YK-KSM multi", $info);
if ($info['result'] == CURL_OK) {
$str = curl_multi_getcontent($info['handle']);
if (preg_match("/^OK/", $str)) {
debug($str);
if (preg_match("/".$match."/", $str)) {
$error = curl_error ($info['handle']);
$errno = curl_errno ($info['handle']);
$info = curl_getinfo ($info['handle']);
debug("YK-KSM errno/error: " . $errno . "/" . $error, $info);
$cinfo = curl_getinfo ($info['handle']);
debug("YK-KSM errno/error: " . $errno . "/" . $error, $cinfo);
$ans_count++;
debug("found entry");
if ($returl) $ans_arr[]="url=" . $cinfo['url'] . "\n" . $str;
else $ans_arr[]=$str;
}
if ($ans_count >= $ans_req) {
foreach ($ch as $h) {
curl_multi_remove_handle ($mh, $h);
curl_close ($h);
}
curl_multi_close ($mh);
return $str;
if ($ans_count==1) return $ans_arr[0];
else return $ans_arr;
}
curl_multi_remove_handle ($mh, $info['handle']);
curl_close ($info['handle']);
unset ($ch[$info['handle']]);

View File

@ -5,7 +5,9 @@ $baseParams = array ();
$baseParams['__YKVAL_DB_HOST__'] = 'localhost';
$baseParams['__YKVAL_DB_NAME__'] = 'ykval';
$baseParams['__YKVAL_DB_USER__'] = 'ykval_verifier';
$baseParams['__YKVAL_DB_PW__'] = 'password';
$baseParams['__YKVAL_DB_PW__'] = 'lab';
# For the validation server sync
$baseParams['__YKVAL_SYNC_POOL__'] = "api2.yubico.com/wsapi/sync;api3.yubico.com/wsapi/sync;api4.yubico.com/wsapi/sync";
# For the get-api-key service.
$baseParams['__YKGAK_DB_HOST__'] = $baseParams['__YKVAL_DB_HOST__'];
@ -22,6 +24,9 @@ $baseParams['__YKR_DB_USER__'] = 'ykval_revoke';
$baseParams['__YKR_DB_PW__'] = 'thirdpassword';
$baseParams['__YKR_IP__'] = '1.2.3.4';
// otp2ksmurls: Return array of YK-KSM URLs for decrypting OTP for
// CLIENT. The URLs must be fully qualified, i.e., contain the OTP
// itself.

View File

@ -30,8 +30,8 @@ CREATE TABLE yubikeys (
CREATE TABLE queue (
id INT NOT NULL UNIQUE AUTO_INCREMENT,
queued_time DATETIME DEFAULT NOW(),
modified_time DATETIME DEFAULT NOW(),
queued_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
modified_time TIMESTAMP,
otp VARCHAR(100) NOT NULL,
server VARCHAR(100) NOT NULL,
info VARCHAR(100) NOT NULL,
@ -44,6 +44,8 @@ GRANT SELECT,INSERT,UPDATE(accessed, counter, low, high, sessionUse)
ON ykval.yubikeys to 'ykval_verifier'@'localhost';
GRANT SELECT(id, secret, active)
ON ykval.clients to 'ykval_verifier'@'localhost';
GRANT SELECT,INSERT,UPDATE,DELETE
ON ykval.queue to 'ykval_verifier'@'localhost';
-- DROP USER 'ykval_getapikey'@'localhost';
CREATE USER 'ykval_getapikey'@'localhost';

144
ykval-sync.php Normal file
View File

@ -0,0 +1,144 @@
<?php
require_once 'ykval-common.php';
require_once 'ykval-config.php';
$apiKey = '';
header("content-type: text/plain");
debug("Request: " . $_SERVER['QUERY_STRING']);
$conn = mysql_connect($baseParams['__YKVAL_DB_HOST__'],
$baseParams['__YKVAL_DB_USER__'],
$baseParams['__YKVAL_DB_PW__']);
if (!$conn) {
sendResp(S_BACKEND_ERROR, $apiKey);
exit;
}
if (!mysql_select_db($baseParams['__YKVAL_DB_NAME__'], $conn)) {
sendResp(S_BACKEND_ERROR, $apiKey);
exit;
}
#
# Define requirements on protocoll
#
$syncParams=array("modified"=>Null,
"otp"=>Null,
"yk_identity"=>Null,
"yk_counter"=>Null,
"yk_use"=>Null,
"yk_high"=>Null,
"yk_low"=>Null);
#
# Extract values from HTTP request
#
$tmp_log = "ykval-sync received ";
foreach ($syncParams as $param=>$value) {
$value = getHttpVal($param, Null);
if ($value==Null) {
debug("ykval-sync recevied request with parameter[s] missing");
sendResp(S_MISSING_PARAMETER, '');
exit;
}
$syncParams[$param]=$value;
$local_log .= "$param=$value ";
}
debug($tmp_log);
#
# Get local counter data
#
$devId = $syncParams['yk_identity'];
$ad = getAuthData($conn, $devId);
if (!is_array($ad)) {
debug('Discovered Yubikey ' . $devId);
addNewKey($conn, $devId);
$ad = getAuthData($conn, $devId);
if (!is_array($ad)) {
debug('Invalid Yubikey ' . $devId);
sendResp(S_BACKEND_ERROR, $apiKey);
exit;
}
}
debug("Auth data:", $ad);
if ($ad['active'] != 1) {
debug('De-activated Yubikey ' . $devId);
sendResp(S_BAD_OTP, $apiKey);
exit;
}
# Note: AD comes directly from the DB response. Since we want to separate
# DB-dependencies longterm, we parse out the values we want from the response
# in order to keep naming consistent in the remaining code. This could be
# considered inefficent in terms of computing power.
$localParams=array('modified'=>DbTimeToUnix($ad['accessed']),
'yk_counter'=>$ad['counter'],
'yk_use'=>$ad['sessionUse'],
'yk_low'=>$ad['low'],
'yk_high'=>$ad['high']);
#
# Compare sync and local counters and generate warnings according to
#
# http://code.google.com/p/yubikey-val-server-php/wiki/ServerReplicationProtocol
#
if ($syncParams['yk_counter'] > $localParams['yk_counter'] ||
($syncParams['yk_counter'] == $localParams['yk_counter'] &&
$syncParams['yk_use'] > $localParams['yk_use'])) {
# sync counters are higher than local counters. We should update database
#TODO: Take care of accessed field. What format should be used. seconds since epoch?
$stmt = 'UPDATE yubikeys SET ' .
'accessed=\'' . UnixToDbTime($syncParams['modified']) . '\'' .
', counter=' . $syncParams['yk_counter'] .
', sessionUse=' . $syncParams['yk_use'] .
', low=' . $syncParams['yk_low'] .
', high=' . $syncParams['yk_high'] .
' WHERE id=' . $ad['id'];
query($conn, $stmt);
} else {
if ($syncParams['yk_counter']==$localParams['yk_counter'] &&
$syncParams['yk_use']==$localParams['yk_use']) {
# sync counters are equal to local counters.
if ($syncParams['modified']==$localParams['modified']) {
# sync modified is equal to local modified. Sync request is unnessecarily sent, we log a "light" warning
error_log("ykval-sync:notice:Sync request unnessecarily sent");
} else {
# sync modified is not equal to local modified. We have an OTP replay attempt somewhere in the system
error_log("ykval-sync:warning:Replayed OTP attempt. " .
" identity=" . $syncParams['yk_identity'] .
" otp=" . $syncParams['otp'] .
" syncCounter=" . $syncParams['yk_counter'] .
" syncUse=" . $syncParams['yk_use'] .
" syncModified=" . $syncParams['modified'] .
" localModified=" . $localParams['modified']);
}
} else {
# sync counters are lower than local counters
error_log("ykval-sync:warning:Remote server is out of sync." .
" identity=" . $syncParams['yk_identity'] .
" syncCounter=" . $syncParams['yk_counter'] .
" syncUse=" . $syncParams['yk_use'].
" localCounter=" . $localParams['yk_counter'] .
" localUse=" . $localParams['yk_use']);
}
}
$extra=array('modified'=>$localParams['modified'],
'yk_identity'=>$syncParams['yk_identity'], #NOTE: Identity is never picked out from local db
'yk_counter'=>$localParams['yk_counter'],
'yk_use'=>$localParams['yk_use'],
'yk_high'=>$localParams['yk_high'],
'yk_low'=>$localParams['yk_low']);
sendResp(S_OK, '', $extra);
?>

346
ykval-synclib.php Normal file
View File

@ -0,0 +1,346 @@
<?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;
}
}
?>

View File

@ -1,6 +1,7 @@
<?php
require_once 'ykval-common.php';
require_once 'ykval-config.php';
require_once 'ykval-synclib.php';
$apiKey = '';
@ -146,7 +147,52 @@ $stmt = 'UPDATE yubikeys SET accessed=NOW()' .
', low=' . $otpinfo['low'] .
', high=' . $otpinfo['high'] .
' WHERE id=' . $ad['id'];
$r=query($conn, $stmt);
$stmt = 'SELECT accessed FROM yubikeys WHERE id=' . $ad['id'];
$r=query($conn, $stmt);
if (mysql_num_rows($r) > 0) {
$row = mysql_fetch_assoc($r);
mysql_free_result($r);
$modified=DbTimeToUnix($row['accessed']);
}
else {
$modified=0;
}
//// Queue sync requests
$sl = new SyncLib();
// We need the modifed value from the DB
$stmp = 'SELECT accessed FROM yubikeys WHERE id=' . $ad['id'];
query($conn, $stmt);
$sl->queue($modified,
$otp,
$devId,
$otpinfo['session_counter'],
$otpinfo['session_use'],
$otpinfo['high'],
$otpinfo['low']);
$required_answers=$sl->getNumberOfServers();
$syncres=$sl->sync($required_answers);
$answers=$sl->getNumberOfAnswers();
$valid_answers=$sl->getNumberOfValidAnswers();
debug("ykval-verify:notice:number of servers=" . $required_answers);
debug("ykval-verify:notice:number of answers=" . $answers);
debug("ykval-verify:notice:number of valid answers=" . $valid_answers);
if($syncres==False) {
# sync returned false, indicating that
# either at least 1 answer marked OTP as invalid or
# there were not enough answers
debug("ykval-verify:notice:Sync failed");
if ($valid_answers!=$answers) {
sendResp(S_REPLAYED_OTP, $apiKey);
exit;
} else {
sendResp(S_NOT_ENOUGH_ANSWERS, $apiKey);
exit;
}
}
//// Check the time stamp
//