xaizek / rocketgit (License: AGPLv3+) (since 2018-12-09)
Light and fast Git hosting solution suitable to serve both as a hub or as a personal code storage with its tickets, pull requests, API and much more.
<root> / inc / token.inc.php (d9cbe98ff90fdad2b9be883aac67e733e829e89d) (5,872B) (mode 100644) [raw]
<?php
require_once($INC . "/util.inc.php");
require_once($INC . "/log.inc.php");
require_once($INC . "/sql.inc.php");
require_once($INC . "/cache.inc.php");
require_once($INC . "/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)
{
	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;
			}
		}

		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)
{
	rg_log_enter('token_get: sid=' . $rg['sid'] . ' tag=' . $tag);

	$ret = FALSE;
	while (1) {
		if (empty($rg['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'] == 1)
			$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;
}

?>
Hints

Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://code.reversed.top/user/xaizek/rocketgit

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@code.reversed.top/user/xaizek/rocketgit

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a pull request:
... clone the repository ...
... make some changes and some commits ...
git push origin master