mirror of
https://github.com/TribesNext/t2-scripts.git
synced 2026-01-19 18:14:43 +00:00
398 lines
13 KiB
C#
398 lines
13 KiB
C#
// Tribes 2 Unofficial Authentication System
|
|
// http://www.tribesnext.com/
|
|
// Written by Electricutioner/Thyth
|
|
// Copyright 2008 by Electricutioner/Thyth and the Tribes 2 Community System Reengineering Intitiative
|
|
|
|
// Version 1.1: 03/14/2009
|
|
|
|
// load the clan support functions
|
|
exec("t2csri/clientSideClans.cs");
|
|
|
|
// initialize the SHA1 digester in Ruby
|
|
function t2csri_initDigester()
|
|
{
|
|
$SHA1::Initialized = 1;
|
|
rubyEval("$sha1hasher = SHA1Pure.new");
|
|
}
|
|
|
|
// use Ruby to get the SHA1 hash of the string
|
|
function sha1sum(%string)
|
|
{
|
|
if (!$SHA1::Initialized)
|
|
t2csri_initDigester();
|
|
%string = strReplace(%string, "'", "\\'");
|
|
rubyEval("$sha1hasher.prepare");
|
|
rubyEval("$sha1hasher.append('" @ %string @ "')");
|
|
rubyEval("tsEval '$temp=\"' + $sha1hasher.hexdigest + '\";'");
|
|
%temp = $temp;
|
|
$temp = "";
|
|
return %temp;
|
|
}
|
|
|
|
// get the password encrypted private key for the following name
|
|
// assuming it is installed on the system
|
|
function t2csri_getEncryptedAccountKey(%name)
|
|
{
|
|
return rubyGetValue("$accPrivateKeys['" @ strlwr(%name) @ "']");
|
|
}
|
|
|
|
// get the public certificate key for the following name
|
|
// assuming it is installed on the system
|
|
function t2csri_getAccountCertificate(%name)
|
|
{
|
|
// check if the name exists
|
|
%found = 0;
|
|
for (%i = 0; %i < getFieldCount($accountList); %i++)
|
|
{
|
|
if (%name $= getField($accountList, %i))
|
|
%found = 1;
|
|
}
|
|
|
|
// this is a bit of a hack -- Ruby 1.9.0 has some problems getting the account on the first try
|
|
%value = "";
|
|
if (%found)
|
|
{
|
|
while (strLen(%value) == 0)
|
|
{
|
|
%value = rubyGetValue("$accCerts['" @ strlwr(%name) @ "']");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
%value = rubyGetValue("$accCerts['" @ strlwr(%name) @ "']");
|
|
}
|
|
return %value;
|
|
}
|
|
|
|
// prevents a warning generated when leaving a server, and allows the yellow
|
|
// highlight selection on the warrior screen that indicates the active account
|
|
function WONGetAuthInfo()
|
|
{
|
|
return getField($LoginCertificate, 0) @ "\t\t0\t" @ getField($LoginCertificate, 1) @ "\n";
|
|
}
|
|
|
|
// decrypt an RC4 encrypted account key
|
|
// also used for encryption on the plaintext when generating the account
|
|
function t2csri_decryptAccountKey(%account, %password, %nonce, %doingEncryption)
|
|
{
|
|
%key = sha1sum(%password @ %nonce);
|
|
|
|
// initiate RC4 stream state with key
|
|
%iterations = 256;
|
|
for (%i = 0; %i < %iterations; %i++)
|
|
{
|
|
%SArray[%i] = %i;
|
|
}
|
|
%j = 0;
|
|
for (%i = 0; %i < %iterations; %i++)
|
|
{
|
|
%j = (%j + %SArray[%i] + strCmp(getSubStr(%key, %i % strLen(%key), 1), "")) % %iterations;
|
|
|
|
//swap(S[i],S[j])
|
|
%temp = %SArray[%i];
|
|
%SArray[%i] = %SArray[%j];
|
|
%SArray[%j] = %temp;
|
|
}
|
|
|
|
// discard 2048 bytes from the start of the stream to avoid the strongly biased first bytes
|
|
%seedI = 0; %seedJ = 0;
|
|
for (%i = 0; %i < 2048; %i++)
|
|
{
|
|
%seedI = (%seedI + 1) % 256;
|
|
%seedJ = (%seedJ + %SArray[%seedI]) % 256;
|
|
|
|
%temp = %SArray[%seedI];
|
|
%SArray[%seedI] = %SArray[%seedJ];
|
|
%SArray[%seedJ] = %temp;
|
|
}
|
|
|
|
// decrypt the account
|
|
%bytes = strlen(%account) / 2;
|
|
for (%i = 0; %i < %bytes; %i++)
|
|
{
|
|
%seedI = (%seedI + 1) % 256;
|
|
%seedJ = (%seedJ + %SArray[%seedI]) % 256;
|
|
|
|
%temp = %SArray[%seedI];
|
|
%SArray[%seedI] = %SArray[%seedJ];
|
|
%SArray[%seedJ] = %temp;
|
|
|
|
%schar = %SArray[(%SArray[%seedI] + %SArray[%seedJ]) % 256];
|
|
%achar = strCmp(collapseEscape("\\x" @ getSubStr(%account, %i * 2, 2)), "");
|
|
%byte = DecToHex(%schar ^ %achar);
|
|
if (strLen(%byte) < 2)
|
|
%byte = "0" @ %byte;
|
|
%out = %out @ %byte;
|
|
}
|
|
|
|
// verify that the password is correct by checking with the nonce (SHA1 plaintext hash)
|
|
%hash = sha1sum(%out);
|
|
if (%hash $= %nonce || %doingEncryption)
|
|
return %out;
|
|
else
|
|
{
|
|
%out = getSubStr(%out, 0, strlen(%out) - 2);
|
|
// last 4-bit block was corrupted... try to fix it
|
|
for (%i = 0; %i < 16; %i++)
|
|
{
|
|
%chunk = getSubStr(DecToHex(%i), 1, 1);
|
|
%hash = sha1sum(%out @ %chunk);
|
|
if (%hash $= %nonce)
|
|
return %out @ %chunk;
|
|
}
|
|
// last 8-bit block was corrupted... try to fix it
|
|
for (%i = 0; %i < 256; %i++)
|
|
{
|
|
%chunk = DecToHex(%i);
|
|
%hash = sha1sum(%out @ %chunk);
|
|
if (%hash $= %nonce)
|
|
return %out @ %chunk;
|
|
}
|
|
|
|
// looks like the password was still wrong
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function t2csri_encryptAccountKey(%account, %password)
|
|
{
|
|
%nonce = sha1sum(%account);
|
|
return %nonce @ ":" @ t2csri_decryptAccountKey(%account, %password, %nonce, 1);
|
|
}
|
|
|
|
// this does the "login" process internally for accounts that exist
|
|
// it finds the cert, the private key, decrypts it, and sets up the
|
|
// RSA key data structures in the Ruby environment.
|
|
function t2csri_getAccount(%username, %password)
|
|
{
|
|
$LoginUsername = %username;
|
|
$LoginCertificate = t2csri_getAccountCertificate(%username);
|
|
if ($LoginCertificate $= "")
|
|
{
|
|
return "NO_SUCH_ACCOUNT";
|
|
}
|
|
|
|
// split the certificate into its components
|
|
// username guid e n signature
|
|
%user = getField($LoginCertificate, 0);
|
|
%guid = getField($LoginCertificate, 1);
|
|
%e = getField($LoginCertificate, 2);
|
|
%n = getField($LoginCertificate, 3);
|
|
%sig = getField($LoginCertificate, 4);
|
|
|
|
// nonce:encrypted
|
|
%encryptedKey = t2csri_getEncryptedAccountKey(%username);
|
|
%encryptedKey = getField(%encryptedKey, 1); // strip the username from the field
|
|
%nonce = getSubStr(%encryptedKey, 0, strstr(%encryptedKey, ":"));
|
|
%block = getSubStr(%encryptedKey, strLen(%nonce) + 1, strLen(%encryptedKey));
|
|
%decryptedKey = t2csri_decryptAccountKey(%block, %password, %nonce);
|
|
if (%decryptedKey $= "")
|
|
{
|
|
return "INVALID_PASSWORD";
|
|
}
|
|
|
|
// we have the account, and the properly decrypted private key... interface with Ruby and
|
|
// insert the data...
|
|
rubyEval("$accountKey = RSAKey.new");
|
|
rubyEval("$accountKey.e = '" @ %e @ "'.to_i(16)");
|
|
rubyEval("$accountKey.n = '" @ %n @ "'.to_i(16)");
|
|
rubyEval("$accountKey.d = '" @ %decryptedKey @ "'.to_i(16)");
|
|
// protect the private exponent (d) from reading now.
|
|
// this will prevent scripts from stealing the private exponent, but still
|
|
// allows doing decryption using the player's account key
|
|
rubyEval("$accountKey.protect");
|
|
|
|
return "SUCCESS";
|
|
}
|
|
|
|
// this sends a request to the authentication server to retrieve an account that is
|
|
// not locally stored on the client machine. It does some fancy mangling on the
|
|
// password to prevent the authentication server from decrypting the password
|
|
function t2csri_downloadAccount(%username, %password)
|
|
{
|
|
// clear out any previously downloaded account
|
|
$Authentication::Status::LastCert = "";
|
|
$Authentication::Status::LastExp = "";
|
|
|
|
// bring up a UI to indicate account download is in progress
|
|
LoginMessagePopup("DOWNLOADING", "Downloading account credentials...");
|
|
|
|
// this hash is what the auth server stores -- it does not store the password
|
|
// in a recoverable manner
|
|
%authStored = sha1sum("3.14159265" @ strlwr(%username) @ %password);
|
|
//echo(%authStored);
|
|
|
|
// get time in UTC, use it as a nonce to prevent replay attacks
|
|
rubyEval("tsEval '$temp=\"' + Time.new.getutc.to_s + '\";'");
|
|
%utc = $temp;
|
|
$temp = "";
|
|
//echo(%utc);
|
|
|
|
// time/username nonce
|
|
%timeNonce = sha1sum(%utc @ strlwr(%username));
|
|
//echo(%timeNonce);
|
|
|
|
// combined hash
|
|
%requestHash = sha1sum(%authStored @ %timeNonce);
|
|
//echo(%requestHash);
|
|
|
|
// sent to server: username utc requesthash
|
|
// server sends back: certificate and encrypted private exponent
|
|
Authentication_recoverAccount(%username @ "\t" @ %utc @ "\t" @ %requestHash);
|
|
t2csri_processDownloadCompletion();
|
|
}
|
|
|
|
function t2csri_processDownloadCompletion()
|
|
{
|
|
if ($Authentication::Status::ActiveMode != 0)
|
|
{
|
|
schedule(128, 0, t2csri_processDownloadCompletion);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
if (strlen($Authentication::Status::LastCert) > 0)
|
|
{
|
|
popLoginMessage();
|
|
LoginMessagePopup("SUCCESS", "Account credentials downloaded successfully.");
|
|
schedule(3000, 0, popLoginMessage);
|
|
|
|
%cert = strreplace($Authentication::Status::LastCert, "'", "\\'");
|
|
%exp = strreplace($Authentication::Status::LastExp, "'", "\\'");
|
|
%cert = getSubStr(%cert, 6, strlen(%cert));
|
|
%exp = getField(%cert, 0) @ "\t" @ getSubStr(%exp, 5, strlen(%exp));
|
|
// add it to the store
|
|
rubyEval("certstore_addAccount('" @ %cert @ "','" @ %exp @ "')");
|
|
|
|
// refresh the UI
|
|
$LastLoginKey = $LoginName;
|
|
LoginEditMenu.clear();
|
|
LoginEditMenu.populate();
|
|
LoginEditMenu.setActive(1);
|
|
LoginEditMenu.setSelected(0);
|
|
LoginEditBox.clear();
|
|
}
|
|
else
|
|
{
|
|
popLoginMessage();
|
|
if ($Authentication::RecoveryError $= "")
|
|
{
|
|
$Authentication::RecoveryError = "The server did not respond [a firewall may cause this].";
|
|
}
|
|
LoginMessagePopup("ERROR", "Credential download failed: " @ $Authentication::RecoveryError);
|
|
schedule(3000, 0, popLoginMessage);
|
|
}
|
|
}
|
|
}
|
|
|
|
// gets a hex version of the game server's IP address
|
|
// used to prevent a replay attack as described by Rain
|
|
function t2csri_gameServerHexAddress()
|
|
{
|
|
%ip = ServerConnection.getAddress();
|
|
|
|
// DarkDragonDX: Fix listen server crashes because .getAddress() returns
|
|
// "local" when in listen
|
|
if (trim(%ip) $= "local")
|
|
%ip = "IP:127.0.0.1:" @ $Host::Port; // Build the local IP ourselves
|
|
|
|
%ip = getSubStr(%ip, strstr(%ip, ":") + 1, strlen(%ip));
|
|
%ip = getSubStr(%ip, 0, strstr(%ip, ":"));
|
|
%ip = strReplace(%ip, ".", " ");
|
|
|
|
for (%i = 0; %i < getWordCount(%ip); %i++)
|
|
{
|
|
%byte = DecToHex(getWord(%ip, %i));
|
|
if (strLen(%byte) < 2)
|
|
%byte = "0" @ %byte;
|
|
%hex = %hex @ %byte;
|
|
}
|
|
return %hex;
|
|
}
|
|
|
|
// client side interface to communicate with the game server
|
|
function clientCmdt2csri_pokeClient(%version)
|
|
{
|
|
echo("T2CSRI: Authenticating with connected game server. Server version: " @ %version);
|
|
|
|
// send the community certificate, assuming server is running later than 1.0
|
|
if (getWord(%version, 1) > 1.0)
|
|
t2csri_sendCommunityCert();
|
|
|
|
$encryptedchallenge = "";
|
|
|
|
// send the certificate in 200 byte parts
|
|
for (%i = 0; %i < strlen($LoginCertificate); %i += 200)
|
|
{
|
|
commandToServer('t2csri_sendCertChunk', getSubStr($LoginCertificate, %i, 200));
|
|
}
|
|
|
|
// send a 64 bit challenge to the server to prevent replay attacks
|
|
rubyEval("tsEval '$loginchallenge=\"' + rand(18446744073709551615).to_s(16) + '\";'");
|
|
// append what the client thinks the server IP address is, for anti-replay purposes
|
|
$loginchallenge = $loginchallenge @ t2csri_gameServerHexAddress();
|
|
|
|
// wait a second to make sure the com cert data is transferred
|
|
schedule(1000, 0, commandToServer, 't2csri_sendChallenge', $loginchallenge);
|
|
|
|
// at this point, server will validate the signature on the certificate then
|
|
// proceed to verifying the client has the private part of the key if valid
|
|
// or disconnecting them if invalid
|
|
// the only way the client can have a valid cert is if the auth server signed it
|
|
}
|
|
|
|
function clientCmdt2csri_getChallengeChunk(%chunk)
|
|
{
|
|
$encryptedchallenge = $encryptedchallenge @ %chunk;
|
|
}
|
|
|
|
function clientCmdt2csri_decryptChallenge()
|
|
{
|
|
// sanitize the challenge to make sure it contains nothing but hex characters.
|
|
// anything else means that the server is trying to hijack control of the interpreter
|
|
%challenge = strlwr($encryptedchallenge);
|
|
for (%i = 0; %i < strlen(%challenge); %i++)
|
|
{
|
|
%char = strcmp(getSubStr(%challenge, %i, 1), "");
|
|
if ((%char < 48 || %char > 102) || (%char > 57 && %char < 97))
|
|
{
|
|
schedule(1000, 0, MessageBoxOK, "REJECTED","Invalid characters in server challenge.");
|
|
disconnect();
|
|
return;
|
|
}
|
|
}
|
|
|
|
rubyEval("tsEval '$decryptedChallenge=\"' + $accountKey.decrypt('" @ %challenge @ "'.to_i(16)).to_s(16) + '\";'");
|
|
|
|
// verify that the client challenge is intact, and extract the server challenge
|
|
%replayedClientChallenge = getSubStr($decryptedChallenge, 0, strLen($loginchallenge));
|
|
%serverChallenge = getSubStr($decryptedChallenge, strlen(%replayedClientChallenge), strLen($decryptedChallenge));
|
|
if (%replayedClientChallenge !$= $loginchallenge)
|
|
{
|
|
schedule(1000, 0, MessageBoxOK, "REJECTED","Server sent back wrong client challenge.");
|
|
disconnect();
|
|
return;
|
|
}
|
|
|
|
// analyze the IP address the server thinks the client is connecting from for the purposes
|
|
// of preventing replay attacks
|
|
%clip = ipv4_hexBlockToIP(getSubStr(%serverChallenge, strLen(%serverChallenge) - 8, 8));
|
|
if (!ipv4_reasonableConnection(ipv4_hexBlockToIP(t2csri_gameServerHexAddress()), %clip))
|
|
{
|
|
schedule(1000, 0, MessageBoxOK, "REJECTED","Server sent back unreasonable IP challenge source. Possible replay attack attempt.");
|
|
disconnect();
|
|
return;
|
|
}
|
|
|
|
// send the server part of the challenge to prove client identity
|
|
// this is done on a schedule to prevent side-channel timing attacks on the client's
|
|
// private exponent -- different x requires different time for x^d, and d bits can be found
|
|
// if you are really resourceful... adding this schedule kills time accuracy and makes such
|
|
// a correlation attack very improbable
|
|
schedule(getRandom(128, 512), 0, commandToServer, 't2csri_challengeResponse', %serverChallenge);
|
|
|
|
// at this point, server will verify that the challenge is equivalent to the one it sent encrypted
|
|
// to the client. the only way it can be equivalent is if the client has the private key they
|
|
// claim to have. normal T2 connection process continues from this point
|
|
}
|