Hallo Appsurd and community!
Thank you very much for this tutorial and the whole very informative thread!
I have read the thread multiple times, specially the parts about the replay attack vulnerability.
And i think it is not the problem that someone cheats and makes himself a score of 1 million. The bigger problem is
that you can insert 1 million scores with a few lines of code and mess up the whole leaderboard...
I have carefully read what FrostyCat and others have mentioned about improving security with a token like system.
So i have tried to make one that is in my opinion save and not that hard to implement in your setup.
It works like that:
1. Before the score is added to the database, the client (GM) requests a enciphered token from the sever.
2. The server creates a random token, and enciphers the token with a Vigenere style cipher (with key_A) and saves the
unciphered token in a database.
3. Then GM deciphers the received token with key_A. And enciphers the token again with key_B (the keys A and B need to be different
and should have the same lenght as the created token for best vigenere security, maybe 16-20 and random with no similar characters).
Then the enciphered token gets url encoded and GM sends it to the server (to your addscore.php as the secret_key).
4. The server deciphers the token with key_B and checks if the token is in the DB, if so, the score will be added in the highscore database
and the used token will be deleted.
I know that the Vigenere cipher is not the state of the art security, but for this purpose it is ok i think.
Ok, so first we need to make a new table, i named it TOKEN with 3 rows like the DB for the highscores.
Code:
# Name Typ Standard Extra
1 id int(11) AUTO_INCREMENT
2 token varchar(100)
3 time timestamp current_timestamp()
I made the third row, because we can alternatively delete rows that are older than 1 minute or whatever.
This is optional, but we have the row and we can decide later.
On the Server we need one additional PHP file for generating the token...
I called it token.php:
PHP:
<?php
/// Random key generator
/**
* Generate a random string, using a cryptographically secure
* pseudorandom number generator (random_int)
*
* This function uses type hints now (PHP 7+ only), but it was originally
* written for PHP 5 as well.
*
* For PHP 7, random_int is a PHP core function
* For PHP 5.x, depends on https://github.com/paragonie/random_compat
*
* @param int $length How many characters do we want?
* @param string $keyspace A string of all possible characters
* to select from
* @return string
*/
function random_str(
int $length = 64,
string $keyspace = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
): string {
if ($length < 1) {
throw new \RangeException("Length must be a positive integer");
}
$pieces = [];
$max = mb_strlen($keyspace, '8bit') - 1;
for ($i = 0; $i < $length; ++$i) {
$pieces []= $keyspace[random_int(0, $max)];
}
return implode('', $pieces);
}
/// Vigenere style cipher
// vigenere_ascii(in,key,mode)
//
// Returns the given string enciphered or deciphered
// using a simple Vigenere style cipher, and filtering
// out non-printable characters.
//
// in input, string
// key enciphering key, string
// mode 0 = decipher, 1 = encipher
//
// GMLscripts.com/license
function vigenere_ascii($in, $key, $mode)
{
$out = "";
$inLen = strlen($in);
$keyLen = strlen($key);
$loVal = 32;
$hiVal = 126;
$span = ($hiVal - $loVal) + 1;
for ($pos=0; $pos<$inLen; $pos+=1) {
$inChar = substr($in, $pos, 1);
$keyChar = substr($key, $pos % $keyLen, 1);
$inVal = min(max($loVal, ord($inChar)), $hiVal) - $loVal;
$keyVal = min(max($loVal, ord($keyChar)), $hiVal) - $loVal;
if ($mode) {
$outVal = (($inVal + $keyVal) % $span) + $loVal;
}else{
$outVal = (($span + $inVal - $keyVal) % $span) + $loVal;
}
$outChar = chr($outVal);
$out = $out . $outChar;
}
return $out;
}
// Generate a random token
$token = random_str(10);
// Key A for the Vigenere cipher
$key_A = "cipherA";
// Encipher the token
$enciphered_token = vigenere_ascii($token, $key_A, 0);
// Connect to database
$db = new PDO('mysql:host=localhost;dbname=yourname', 'user','pass');
// Prepare statement
$statement = $db->prepare("INSERT INTO TOKEN (token) VALUES (?)");
// Safe the token in the DB
$statement->execute(array($token));
echo $enciphered_token;
Your addscore.php needs to be modified, i called it addscore_token.php:
PHP:
<?php
/// Vigenere style cipher
// vigenere_ascii(in,key,mode)
//
// Returns the given string enciphered or deciphered
// using a simple Vigenere style cipher, and filtering
// out non-printable characters.
//
// in input, string
// key enciphering key, string
// mode 0 = decipher, 1 = encipher
//
// GMLscripts.com/license
function vigenere_ascii($in, $key, $mode)
{
$out = "";
$inLen = strlen($in);
$keyLen = strlen($key);
$loVal = 32;
$hiVal = 126;
$span = ($hiVal - $loVal) + 1;
for ($pos=0; $pos<$inLen; $pos+=1) {
$inChar = substr($in, $pos, 1);
$keyChar = substr($key, $pos % $keyLen, 1);
$inVal = min(max($loVal, ord($inChar)), $hiVal) - $loVal;
$keyVal = min(max($loVal, ord($keyChar)), $hiVal) - $loVal;
if ($mode) {
$outVal = (($inVal + $keyVal) % $span) + $loVal;
}else{
$outVal = (($span + $inVal - $keyVal) % $span) + $loVal;
}
$outChar = chr($outVal);
$out = $out . $outChar;
}
return $out;
}
// Connect to database
$db = new PDO('mysql:host=localhost;dbname=yourname', 'user','pass');
// The token
$token = $_POST['token'];
// Key B for the Vigenere cipher
$key_B = "cipherB";
// Decipher the token
$deciphered_token = vigenere_ascii($token, $key_B, 1);
// Search for token in DB
$sql = "SELECT * FROM TOKEN WHERE token = '$deciphered_token'";
$search = $db->query($sql)->fetch();
$db_token = $search['token'];
// If token exists in DB, then insert name and score
if($db_token){
// Prepare statement
$sql = "INSERT INTO QUERSENKEN VALUES (NULL, :name, :score)";
$stmt = $db->prepare($sql);
$stmt->bindParam(':name', $name, PDO::PARAM_STR);
$stmt->bindParam(':score', $score, PDO::PARAM_INT);
// Get name and score from URL string
$name = $_POST['name'];
$score = $_POST['score'];
// Execute statement
$stmt->execute();
// Delete token from DB
$db->query("DELETE FROM TOKEN WHERE token = '$deciphered_token'");
echo '1';
}
else
{
echo '0';
}
// Delete tokens older than 1 minute from DB (optional)
//$db->query("DELETE FROM TOKEN WHERE time < DATE_SUB(now(), interval 1 Minute)"); //maybe 30 SECOND ?
?>
Thats all for the server side. Now we make some changes in GMS.
First insert the vigenere_ascii script to the script folder. (Thanks to xot)
GML:
/// vigenere_ascii(in,key,mode)
//
// Returns the given string enciphered or deciphered
// using a simple Vigenere style cipher, and filtering
// out non-printable characters.
//
// in input, string
// key enciphering key, string
// mode 0 = decipher, 1 = encipher
//
/// GMLscripts.com/license
{
var in, key, mode, out;
in = argument0;
key = argument1;
mode = argument2;
out = "";
var inLen, keyLen, pos, inChar, keyChar, outChar;
var inVal, keyVal, outVal, loVal, hiVal, span;
inLen = string_length(in);
keyLen = string_length(key);
loVal = 32;//org 32
//loVal = 97;
hiVal = 126;//org 126
//hiVal = 122;
span = (hiVal - loVal) + 1;
for (pos=0; pos<inLen; pos+=1) {
inChar = string_char_at(in, pos+1);
keyChar = string_char_at(key, (pos mod keyLen)+1);
inVal = min(max(loVal, ord(inChar)), hiVal) - loVal;
keyVal = min(max(loVal, ord(keyChar)), hiVal) - loVal;
if (mode) {
outVal = ((inVal + keyVal) mod span) + loVal;
}else{
outVal = ((span + inVal - keyVal) mod span) + loVal;
}
outChar = chr(outVal);
out = out + outChar;
}
return out;
}
Then i have changed your scr_send_score to scr_send_score_token:
GML:
/// @description scr_send_score_token(name,score,token)
/// @param name name of the player
/// @param score the achieved score
/// @param token the token (enciphered and url_encoded)
//
// Script: Sends the player’s score to the database in Altervista
// Date: 2020-01-18
// Copyright: Appsurd
// Edited by: Lolloggo
var name = url_encode(base64_encode(string(argument0)));
var args = "name="+name+"&score="+string(argument1)+"&token="+string(argument2);
http_post_string("https://yourhosting/path/addscore_token.php", args);
Remove your obj_send from the room, and make an object called obj_send_token,
it gets created in the async - http event in the obj_highscore:
GML:
if (ds_map_find_value(async_load, "id") == get_highscores)
{
if (ds_map_find_value(async_load, "status") == 0)
{
text2 = string(ds_map_find_value(async_load, "result"));
instance_create_layer(x,y,"Instances_Depth_0",obj_send_token); // create the obj_send_token
if (text2 == "IOException" or text2 == "")
{
text1 = "Please check your internet connection...";
text2 = "";
}
else
{
text1 = "Ready";
alarm[1] = -1;
}
}
}
The obj_send_token looks like this:
create:
GML:
get_token = 0
token = 0;
decipher = 0;
encipher = 0;
url_token = 0;
key_A = "cipherA"; // same as in token.php
key_B = "cipherB"; // same as in addscore_token.php
alarm 1:
draw:
GML:
draw_set_colour(c_lime);
draw_rectangle(400,10,520,30,false);
draw_set_colour(c_white);
draw_set_halign(fa_middle);
draw_set_valign(fa_middle);
draw_text(460,20,"Send score");
draw_set_valign(fa_top);
global left released:
GML:
if point_in_rectangle(mouse_x,mouse_y,400,10,520,30)
{
get_token = http_get("https://yourhosting/path/token.php");
}
async - http:
GML:
if ds_map_find_value(async_load, "id") == get_token
{
if ds_map_find_value(async_load, "status") == 0
{
token = ds_map_find_value(async_load, "result");
show_debug_message(token);
decipher = vigenere_ascii(token,key_A,1);
show_debug_message(decipher);
encipher = vigenere_ascii(decipher,key_B,0);
show_debug_message(encipher);
url_token = url_encode(encipher);
show_debug_message(url_token);
show_debug_message("-------------");
scr_send_score_token("Guest", "1250",url_token);
alarm[1] = room_speed;
}
else
{
url_token = 0;
}
}
Thats it. Its just for testing purposes. But i think you get what i mean.
For me it is the first time that i have done things in SQL, and without your tutorial it wont be possible.
I hope that i have not forgotten anything and that it will work for you.
Dont be to harsh in your commments because i am very new to it.
Thank you again for getting me into it.
I forgot to say that i have tested this in GMS2 with Windows as target.
Maybe that will be useful for you or others.
Please tell me what you think of it.
Regards
Lolloggo