<?php
require_once(__DIR__ . '/util2.inc.php');
require_once(__DIR__ . '/log.inc.php');
require_once(__DIR__ . '/sql.inc.php');
require_once(__DIR__ . '/cache.inc.php');
require_once(__DIR__ . '/prof.inc.php');
$rg_token_error = "";
function rg_token_set_error($str)
{
global $rg_token_error;
$rg_token_error = $str;
rg_log($str);
}
function rg_token_error()
{
global $rg_token_error;
return $rg_token_error;
}
/*
* Delete a token
*/
function rg_token_delete($db, $sid, $token)
{
rg_prof_start("token_delete");
rg_log_enter("token_delete: sid=$sid token=$token");
$ret = array();
$ret['ok'] = 0;
while (1) {
$params = array("sid" => $sid);
$add_token = "";
if (!empty($token)) {
$add_token = " AND token = @@token@@";
$params['token'] = $token;
}
$sql = "DELETE FROM tokens"
. " WHERE sid = @@sid@@"
. $add_token;
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_token_set_error("cannot delete token (" . rg_sql_error() . ")");
break;
}
rg_sql_free_result($res);
$ret['ok'] = 1;
break;
}
rg_log_exit();
rg_prof_end("token_delete");
return $ret;
}
/*
* This function will get the master key from db
*/
function rg_token_get_master($db)
{
rg_prof_start("token_get_master");
rg_log_enter("token_get_master");
$ret = FALSE;
while (1) {
$key = rg_state_get($db, 'token_key');
if ($key === FALSE) {
rg_token_set_error("cannot get token_key:"
. " " . rg_state_error());
break;
}
if (empty($key)) {
$key = rg_id(32);
$r = rg_state_set($db, 'token_key', $key);
if ($r !== TRUE) {
rg_token_set_error("cannot set state:"
. " " . rg_state_error());
break;
}
}
$ret = $key;
break;
}
rg_log_exit();
rg_prof_end("token_get_master");
return $ret;
}
/*
* Returns TRUE if the token is valid
* @double_allowed - if TRUE, we will not mark the token as used
* (for example, logout token does not have to be marked as used)
*/
function rg_token_valid($db, $rg, $tag, $double_allowed)
{
global $rg_sid; // TODO: sess depends on token...
rg_prof_start("token_valid");
rg_log_enter('token_valid: sid=' . $rg_sid . ' token=' . $rg['token']
. ' tag=' . $tag);
$ret = FALSE;
while (1) {
$len = strlen($rg['token']);
if ($len < 32) {
rg_token_set_error("invalid token");
rg_security_violation_no_exit("invalid token ($len != 32)");
break;
}
$rg['token'] = substr($rg['token'], 0, 32);
$key = rg_token_get_master($db);
if ($key === FALSE)
break;
$rand = substr($rg['token'], 0, 16);
$sign = substr($rg['token'], 16, 16);
$data = $rand . $rg_sid . $tag;
$hash = hash_hmac('sha512', $data, $key);
if ($hash === FALSE) {
rg_token_set_error("cannot compute hmac");
break;
}
$hash = substr($hash, 0, 16);
if (strcmp($sign, $hash) != 0) {
rg_log_debug('substr(token, 16, 16)=' . $sign . ' !='
. ' hash_hmac(data,key)=' . $hash . ' data=' . $data);
rg_token_set_error("token invalid");
rg_security_violation_no_exit("invalid token (sign)");
break;
}
$ukey = 'sess' . '::' . $rg_sid . '::' . 'used_tokens'
. '::' . $rg['token'];
$c = rg_cache_get($ukey);
if ($c === '1') {
rg_token_set_error("token already used");
break;
}
$params = array("sid" => $rg_sid,
"token" => $rg['token'],
"expire" => time() + 24 * 3600);
if ($c === FALSE) {
// We check to see if token was already used
$sql = "SELECT 1 FROM tokens"
. " WHERE sid = @@sid@@"
. " AND token = @@token@@";
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_token_set_error("cannot check if token is used"
. " (" . rg_sql_error() . ")");
break;
}
$rows = rg_sql_num_rows($res);
rg_sql_free_result($res);
if ($rows == 1) {
rg_token_set_error("token already used");
break;
}
}
// TODO: shouldn't we move this before the above query?!
if (strncmp($rg_sid, "X", 1) == 0) {
// We have a pre-login session: we do not have to mark
// the token as used.
$ret = TRUE;
break;
}
if ($double_allowed) {
$ret = TRUE;
break;
}
// Unset cached token to generate a new one for this tag
$tkey = 'sess' . '::' . $rg_sid . '::' . 'token'
. '::' . $tag;
rg_cache_unset($tkey, RG_SOCKET_NO_WAIT);
$sql = "INSERT INTO tokens (sid, token, expire)"
. " VALUES (@@sid@@, @@token@@, @@expire@@)";
$res = rg_sql_query_params($db, $sql, $params);
if ($res === FALSE) {
rg_token_set_error("cannot insert token"
. " (" . rg_sql_error() . ")");
break;
}
rg_sql_free_result($res);
// This is an optimization to not look next time in db
rg_cache_set($ukey, '1', RG_SOCKET_NO_WAIT);
$ret = TRUE;
break;
}
rg_log_exit();
rg_prof_end("token_valid");
return $ret;
}
/*
* Returns a token to be used on a form/url
* We generate only one per form (tag is the id), but multiple per session.
*/
function rg_token_get($db, $rg, $tag)
{
global $rg_sid;
rg_log_enter('token_get: sid=' . $rg_sid . ' tag=' . $tag);
$ret = FALSE;
while (1) {
if (empty($rg_sid)) {
rg_token_set_error('empty sid');
break;
}
$key = 'sess' . '::' . $rg_sid . '::' . 'token' . '::' . $tag;
$c = rg_cache_get($key);
if ($c !== FALSE) {
$ret = $c;
break;
}
$sign_key = rg_token_get_master($db);
if ($sign_key === FALSE)
break;
// Add a random string to protect against BREACH attack
$rand = rg_id(16);
$data = $rand . $rg_sid . $tag;
$sign = hash_hmac('sha512', $data, $sign_key);
if ($sign === FALSE) {
rg_token_set_error('cannot compute hmac');
break;
}
$sign = substr($sign, 0, 16);
$ret = $rand . $sign;
rg_log_debug('generated token ' . $ret);
$ret2 = $ret;
if (rg_debug() > 0)
$ret2 .= ':' . $tag;
rg_cache_set($key, $ret2, RG_SOCKET_NO_WAIT);
// Optimization to not look in database next time
$key = 'sess' . '::' . $rg_sid . '::' . 'used_tokens'
. '::' . $ret;
rg_cache_set($key, '0', RG_SOCKET_NO_WAIT);
$ret = $ret2;
break;
}
rg_log_exit();
return $ret;
}