* Initial commit.

This commit is contained in:
Robert MacGregor 2026-02-27 21:56:30 -05:00
commit 66f9b4d8a9
23 changed files with 7105 additions and 0 deletions

90
base/t2csri/authconnect.cs Executable file
View file

@ -0,0 +1,90 @@
// 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
// Authentication Server Connector Version 1.0: 11/06/2008
function authConnect_findAuthServer()
{
if ($AuthServer::Address !$= "")
return;
echo("Looking up Authentication Server...");
if (isObject(AuthConnection))
{
AuthConnection.disconnect();
AuthConnection.delete();
}
new TCPObject(AuthConnection);
%data = "GET /auth HTTP/1.1\r\nHost: www.tribesnext.com\r\nUser-Agent: Tribes 2\r\nConnection: close\r\n\r\n";
AuthConnection.data = %data;
AuthConnection.connect("www.tribesnext.com:80");
$AuthServer::Primed = 0;
}
function AuthConnection::onLine(%this, %line)
{
if (%line == 411)
return;
if (trim(%line) $= "")
{
$AuthServer::Primed = 1;
return;
}
if ($AuthServer::Primed)
{
$AuthServer::Address = %line;
%this.disconnect();
authConnect_verifyLookup();
}
}
function AuthConnection::onConnected(%this)
{
%this.send(%this.data);
}
function authConnect_verifyLookup()
{
if (getFieldCount($AuthServer::Address) != 2)
{
$AuthServer::Address = "";
error("Authentication server lookup failed.");
return;
}
%address = getField($AuthServer::Address, 0);
%signature = getField($AuthServer::Address, 1);
%sha1sum = sha1sum(%address);
%verifSum = t2csri_verify_auth_signature(%signature);
while (strlen(%verifSum) < 40)
%verifSum = "0" @ %verifSum;
if (strcmp(%sha1sum, %verifSum) != 0)
{
// signature verification failed... someone has subverted the auth server lookup
error("Authentication server lookup returned an address with an invalid signature.");
error("Unable to contact the authentication server.");
$AuthServer::Address = "";
return;
}
else
{
echo("Authentication server found at " @ %address @ ". Ready to authenticate.");
$AuthServer::Address = %address;
$AuthServer::Primed = "";
}
}
// perform signature verification to prove that the auth server has designated the
// provided address
function t2csri_verify_auth_signature(%sig)
{
$temp = rubyEval("$temp = t2csri_verify_auth_signature('" @ %sig @ "').to_s(16)");
$temp = rubyGetValue("$temp", 40);
return $temp;
}

238
base/t2csri/authinterface.cs Executable file
View file

@ -0,0 +1,238 @@
// 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
// Authentication Server Interface Version 1.0: 12/29/2008
$Authentication::Mode::Available = 1;
$Authentication::Mode::Name = 2;
$Authentication::Mode::Recover = 3;
$Authentication::Mode::Sign = 4;
$Authentication::Settings::Timeout = 30000;
function AuthenticationInterface::onLine(%this, %line)
{
//warn(%line);
if (isEventPending($Authentication::TransactionCompletionSchedule))
cancel($Authentication::TransactionCompletionSchedule);
$Authentication::TransactionCompletionSchedule = schedule(700, 0, Authentication_transactionComplete);
if ($Authentication::Status::ActiveMode != 0)
{
$Authentication::Buffer[$Authentication::Status::ActiveMode] = $Authentication::Buffer[$Authentication::Status::ActiveMode] @ "\n" @ %line;
}
}
// connection complete... send the buffer
function AuthenticationInterface::onConnected(%this)
{
%this.send(%this.data);
}
function Authentication_transactionComplete()
{
// terminate the connection
AuthenticationInterface.disconnect();
%buffer = trim($Authentication::Buffer[$Authentication::Status::ActiveMode]);
if ($Authentication::Status::ActiveMode == $Authentication::Mode::Available)
{
if (strlen(%buffer) > 0 && %buffer $= "AVAIL")
{
echo("Authentication: Server is available.");
$Authentication::Status::Available = 1;
}
else
{
error("Authentication: Server is not available.");
$Authentication::Status::Available = 0;
}
}
else if ($Authentication::Status::ActiveMode == $Authentication::Mode::Name)
{
if (%buffer $= "TOOSHORT")
{
$Authentication::Status::Name = "Requested name is too short.";
error("Authentication: " @ $Authentication::Status::Name);
}
else if (%buffer $= "TOOLONG")
{
$Authentication::Status::Name = "Requested name is too long.";
error("Authentication: " @ $Authentication::Status::Name);
}
else if (%buffer $= "INVALID")
{
$Authentication::Status::Name = "Requested name is rejected.";
error("Authentication: " @ $Authentication::Status::Name);
}
else if (%buffer $= "TAKEN")
{
$Authentication::Status::Name = "Requested name is taken.";
error("Authentication: " @ $Authentication::Status::Name);
}
else if (%buffer $= "SUCCESS")
{
$Authentication::Status::Name = "Name is available and acceptable.";
echo("Authentication: " @ $Authentication::Status::Name);
}
else
{
// this shouldn't happen
$Authentication::Status::Name = "Unknown name status code returned from server.";
error("Authentication: " @ $Authentication::Status::Name);
}
}
else if ($Authentication::Status::ActiveMode == $Authentication::Mode::Recover)
{
if (%buffer $= "RECOVERERROR")
{
// this generic error happens if a malformed request is sent to the server
error("Authentication: Unknown credential recovery status code returned from server.");
}
else if (%buffer $= "NOTFOUND")
{
error("Authentication: No user with that name exists.");
}
else if (%buffer $= "INVALIDPASSWORD")
{
error("Authentication: Invalid password provided for that user.");
}
else if (getWord(%buffer, 0) $= "CERT:")
{
%cert = getSubStr(%buffer, 0, strstr(%buffer, "\n"));
%buffer = getSubStr(%buffer, strstr(%buffer, "\n") + 1, strlen(%buffer));
%exp = getSubStr(%buffer, 0, (strstr(%buffer, "\n") == -1 ? strlen(%buffer) : strstr(%buffer, "\n")));
$Authentication::Status::LastCert = %cert;
$Authentication::Status::LastExp = %exp;
echo("Authentication: Successfully downloaded certificate and encrypted key.");
}
else
{
error("Authentication: Unknown recovery status code returned from server.");
}
}
else if ($Authentication::Status::ActiveMode == $Authentication::Mode::Sign)
{
if (%buffer $= "REJECTED")
{
// this is returned if the user created an account from this IP in the last week, or 5 accounts total
$Authentication::Status::Signature = "Server chose to reject account generation request.";
error("Authentication: " @ $Authentication::Status::Signature);
}
else if (%buffer $= "INVALIDNAME")
{
// name taken, or otherwise not allowed
$Authentication::Status::Signature = "Server rejected account name.";
error("Authentication: " @ $Authentication::Status::Signature);
}
else if (%buffer $= "SIGNERROR")
{
$Authentication::Status::Signature = "Corrupt signature request rejected.";
error("Authentication: " @ $Authentication::Status::Signature);
}
else if (strlen(%buffer) > 0 && getFieldCount(%buffer) > 4)
{
%cert = %buffer;
$Authentication::Status::LastCert = %cert;
$Authentication::Status::Signature = "Account generation successful.";
echo("Authentication: " @ $Authentication::Status::Signature);
}
else
{
$Authentication::Status::Signature = "Unknown signature status code returned from server.";
error("Authentication: " @ $Authentication::Status::Signature);
}
}
// clear out the buffer
$Authentication::Buffer[$Authentication::Status::ActiveMode] = "";
$Authentication::Status::ActiveMode = 0;
}
// determine if the server is available
function Authentication_checkAvail()
{
if ($Authentication::Status::ActiveMode != 0)
{
// already a request active, retry this one in 10 seconds
schedule(10000, 0, Authentication_checkAvail);
return;
}
$Authentication::Status::ActiveMode = $Authentication::Mode::Available;
if (isObject(AuthenticationInterface))
AuthenticationInterface.delete();
new TCPObject(AuthenticationInterface);
AuthenticationInterface.data = "AVAIL\n";
AuthenticationInterface.connect($AuthServer::Address);
$Authentication::TransactionCompletionSchedule = schedule($Authentication::Settings::Timeout, 0, Authentication_transactionComplete);
}
// determine if the given name is acceptable/available
function Authentication_checkName(%name)
{
if ($Authentication::Status::ActiveMode != 0)
{
// already a request active, retry this one in 10 seconds
schedule(10000, 0, Authentication_checkName, %name);
return;
}
$Authentication::Status::ActiveMode = $Authentication::Mode::Name;
if (isObject(AuthenticationInterface))
AuthenticationInterface.delete();
new TCPObject(AuthenticationInterface);
AuthenticationInterface.data = "NAME\t" @ %name @ "\n";
AuthenticationInterface.connect($AuthServer::Address);
$Authentication::TransactionCompletionSchedule = schedule($Authentication::Settings::Timeout, 0, Authentication_transactionComplete);
}
// request a certificate and encrypted exponent from the authentication server
function Authentication_recoverAccount(%payload)
{
if ($Authentication::Status::ActiveMode != 0)
{
// already a request active, retry this one in 10 seconds
schedule(10000, 0, Authentication_recoverAccount, %payload);
return;
}
$Authentication::Status::ActiveMode = $Authentication::Mode::Recover;
if (isObject(AuthenticationInterface))
AuthenticationInterface.delete();
new TCPObject(AuthenticationInterface);
AuthenticationInterface.data = "RECOVER\t" @ %payload @ "\n";
AuthenticationInterface.connect($AuthServer::Address);
$Authentication::TransactionCompletionSchedule = schedule($Authentication::Settings::Timeout, 0, Authentication_transactionComplete);
}
// request a new account certificate
function Authentication_registerAccount(%payload)
{
if ($Authentication::Status::ActiveMode != 0)
{
// already a request active, retry this one in 10 seconds
schedule(10000, 0, Authentication_registerAccount, %payload);
return;
}
$Authentication::Status::ActiveMode = $Authentication::Mode::Sign;
if (isObject(AuthenticationInterface))
AuthenticationInterface.delete();
new TCPObject(AuthenticationInterface);
AuthenticationInterface.data = "SIGN\t" @ %payload @ "\n";
AuthenticationInterface.connect($AuthServer::Address);
$Authentication::TransactionCompletionSchedule = schedule($Authentication::Settings::Timeout, 0, Authentication_transactionComplete);
}

111
base/t2csri/autoupdate.cs Executable file
View file

@ -0,0 +1,111 @@
// 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
// Bare Bones Auto Update System Version 1.0: 11/06/2008
function authConnect_findAutoUpdater()
{
if ($AutoUpdater::Address !$= "")
return;
if (isObject(AutoUpdateConnection))
{
AutoUpdateConnection.disconnect();
AutoUpdateConnection.delete();
}
new TCPObject(AutoUpdateConnection);
%data = "GET /update HTTP/1.1\r\nHost: www.tribesnext.com\r\nUser-Agent: Tribes 2\r\nConnection: close\r\n\r\n";
AutoUpdateConnection.connect("www.tribesnext.com:80");
AutoUpdateConnection.schedule(1000, send, %data);
}
function AutoUpdateConnection::onLine(%this, %line)
{
if (!$AutoUpdater::UpdateFound)
{
$AutoUpdater::Address = %line;
%this.disconnect();
autoUpdate_verifyLookup();
}
else
{
if (isEventPending($AutoUpdate::LastLineSch))
cancel($AutoUpdate::LastLineSch);
$AutoUpdate::LastLineSch = autoUpdate_applyUpdate();
if ($AutoUpdate::UpdateStarted)
$AutoUpdate::Buffer = $AutoUpdate::Buffer @ "\n" @ %line;
else if (strlen(%line) == 0)
$AutoUpdate::UpdateStarted = 1;
}
}
function autoUpdate_verifyLookup()
{
if (getFieldCount($AutoUpdate::Address) != 2)
{
$AutoUpdater::Address = "";
error("No valid update address found.");
return;
}
%address = getField($AutoUpdater::Address, 0);
%signature = getField($AutoUpdater::Address, 1);
%sha1sum = sha1sum(%address);
if (%sha1sum !$= t2csri_verify_update_signature(%signature))
{
// signature verification failed... someone has subverted the auth server lookup
error("Auto update lookup returned an address with an invalid signature.");
error("Unable to download update without a correct signature.");
$AutoUpdater::Address = "";
return;
}
else
{
echo("New update found at " @ %address @ ". Ready to download.");
$AutoUpdater::Address = %address;
$AutoUpdater::UpdateFound = 1;
}
}
// perform signature verification to prove that the update server has designated the
// provided URL for a download, we don't want people injecting arbitrary code into
// user installations
function t2csri_verify_update_signature(%sig)
{
$temp = rubyEval("t2csri_verify_update_signature('" @ %sig @ "')", 1);
return $temp;
}
function autoUpdate_performUpdate()
{
if ($AutoUpdater::Address $= "")
return;
if (isObject(AutoUpdateConnection))
{
AutoUpdateConnection.disconnect();
AutoUpdateConnection.delete();
}
new TCPObject(AutoUpdateConnection);
%host = getSubStr($AutoUpdater::Address, 0, strstr("/"));
%uri = getSubStr($AutoUpdater::Address, strlen(%host), strlen($AutoUpdater::Address));
%data = "GET " @ %uri @ " HTTP/1.1\nHost: " @ %host @ "\nUser-Agent: Tribes 2\nConnection: close\n\n";
AutoUpdateConnection.connect(%host);
AutoUpdateConnection.schedule(1000, send, %data);
}
function autoUpdate_applyUpdate()
{
new FileObject(AutoUpdateFile);
AutoUpdateFile.openForWrite("autoUpdate.rb");
AutoUpdateFile.writeline($AutoUpdate::Buffer);
AutoUpdateFile.close();
AutoUpdateFile.delete();
rubyExec("autoUpdate.rb");
}

93
base/t2csri/bans.cs Executable file
View file

@ -0,0 +1,93 @@
// 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
// IP and GUID ban list handling.
// These seem to be completely broken in engine, so... here is a script implementation.
// Still works the same way as before... so scripts will function unmodified.
// BanList::add( %guid, %ipAddress, %seconds);
// If both GUID and IP address are specified, both types of entries are made on the banlist.
// gets the current Unix Epoch time from Ruby -- in seconds
function currentEpochTime()
{
$temp = rubyEval("Time.now.to_i.to_s", 1);
return $temp;
}
// compute the addition in Ruby, due to the Torque script precision problems for >1e6 values
function getEpochOffset(%seconds)
{
$temp = rubyEval("Time.now.to_i + " @ %seconds @ ").to_s", 1);
return $temp;
}
// bans are added to the $BanList::GUID and $BanList::IP hash maps as the Unix epoch time
// when the ban will expire
function BanList::add(%guid, %ipAddress, %seconds)
{
if (%guid != 0)
{
// add GUID ban
$BanList::GUID[%guid] = getEpochOffset(%seconds);
}
if (getSubStr(%ipAddress, 0, 3) $= "IP:")
{
// add IP ban
%bareIP = getSubStr(%ipAddress, 3, strLen(%ipAddress));
%bareIP = getSubStr(%bareIP, 0, strstr(%bareIP, ":"));
%bareIP = strReplace(%bareIP, ".", "_"); // variable access bug workaround
$BanList::IP[%bareIP] = getEpochOffset(%seconds);
}
// write out the updated bans to the file
export("$BanList*", "prefs/banlist.cs");
}
// returns boolean on whether the given client is IP banned or not
// true if banned, false if not banned
function banList_checkIP(%client)
{
%ip = %client.getAddress();
%ip = getSubStr(%ip, 3, strLen(%ip));
%ip = getSubStr(%ip, 0, strstr(%ip, ":"));
%ip = strReplace(%ip, ".", "_");
%time = $BanList::IP[%ip];
if (%time !$= "")
{
//%delta = %time - currentEpochTime();
// T2 arithmetic fail again... doing subtraction in Ruby
$temp = rubyEval("(" @ %time @ " - Time.now.to_i).to_s", 1);
%delta = $temp;
if (%delta > 0)
return 1;
else
deleteVariables("$BanList::IP" @ %ip);
}
return 0;
}
// returns boolean on whether the given GUID is banned or not
// true if banned, false if not banned
function banList_checkGUID(%guid)
{
%time = $BanList::GUID[%guid];
if (%time !$= "")
{
//%delta = %time - currentEpochTime();
// T2 arithmetic fail again... doing subtraction in Ruby
$temp = rubyEval("(" @ %time @ " - Time.now.to_i).to_s", 1);
%delta = $temp;
if (%delta > 0)
return 1;
else
deleteVariables("$BanList::GUID" @ %guid);
}
return 0;
}

164
base/t2csri/base64.cs Executable file
View file

@ -0,0 +1,164 @@
// Torque Script Base64 Utilities
// Written by Electricutioner
// 10:43 PM 7/13/2005
// Used under license by the Tribes 2 Community System Re-engineering Intitiative.
// License Granted: 10/31/2008
// necessary for the transfer of arbitrary binary data over ASCII connections
function Base64_Encode(%string)
{
%encoded = "";
for (%i = 0; %i < strLen(%string); %i += 3)
{
%binBlock = "";
for (%j = 0; %j < 3; %j++)
{
%bin = DecToBin(strCmp(getSubStr(%string, %i + %j, 1), ""));
while (strLen(%bin) < 8 && strLen(%bin) != 0)
%bin = "0" @ %bin;
%binBlock = %binBlock @ %bin;
}
for (%j = 0; %j < 4; %j++)
{
%bin = getSubStr(%binBlock, 6 * %j, 6);
if (%bin !$= "")
{
while(strLen(%bin) < 6)
%bin = %bin @ "0";
%encoded = %encoded @ $Base64Utils::Base64Chars[BinToDec(%bin)];
}
else
%encoded = %encoded @ "=";
}
}
return %encoded;
}
function Base64_Decode(%string)
{
%decoded = "";
for (%i = 0; %i < strLen(%string); %i += 4)
{
%binBlock = "";
for (%j = 0; %j < 4; %j++)
{
%bin = "";
%val = Base64_ValToIndex(strCmp(getSubStr(%string, %i + %j, 1), ""));
if (%val != -1)
%bin = DecToBin(%val);
while (strLen(%bin) < 6 && %val != -1)
%bin = "0" @ %bin;
%binBlock = %binBlock @ %bin;
}
for (%j = 0; %j < 3; %j++)
{
%bin = getSubStr(%binBlock, 8 * %j, 8);
while(strLen(%bin) < 8 && strLen(%bin) != 0)
%bin = "0" @ %bin;
if (%bin !$= "")
%decoded = %decoded @ collapseEscape("\\x" @ DecToHex(BinToDec(%bin)));
}
}
return %decoded;
}
// a few conditionals are better than a loop
function Base64_ValToIndex(%val)
{
if (%val > 96 && %val < 123)
return %val - 71;
else if (%val > 64 && %val < 91)
return %val - 65;
else if (%val > 47 && %val < 58)
return %val + 4;
else if (%val == 43)
return 62;
else if (%val == 47)
return 63;
else if (%val == 61)
return -1;
else
return "";
}
//create the character array in a minimum of fuss
function Base64_CreateArray()
{
for (%i = 0; %i < 26; %i++)
{
$Base64Utils::Base64Chars[%i] = collapseEscape("\\x" @ DecToHex(65 + %i));
$Base64Utils::Base64Chars[%i + 26] = collapseEscape("\\x" @ DecToHex(97 + %i));
if (%i < 10)
$Base64Utils::Base64Chars[%i + 52] = %i;
}
$Base64Utils::Base64Chars[62] = "+";
$Base64Utils::Base64Chars[63] = "/";
}
// these binary conversion functions are much better than older ones
// these can handle just about any size of input, unlike 8 bit like the previous ones
function DecToBin(%dec)
{
%length = mCeil(mLog(%dec) / mLog(2));
%bin = "";
for (%i = 0; %i <= %length; %i++)
{
%test = mPow(2, %length - %i);
if (%dec >= %test)
{
%bin = %bin @ "1";
%dec -= %test;
}
else if (%i > 0)
%bin = %bin @ "0";
}
return %bin;
}
function BinToDec(%bin)
{
%dec = 0;
for (%i = 0; %i < strLen(%bin); %i++)
%dec += getSubStr(%bin, %i, 1) * mPow(2, strLen(%bin) - %i - 1);
return %dec;
}
//no length limit
function DecToHex(%dec)
{
%bin = DecToBin(%dec);
while (strLen(%bin) % 4 != 0)
%bin = "0" @ %bin;
for (%i = 0; %i < strLen(%bin); %i += 4)
{
%block = getSubStr(%bin, strLen(%bin) - %i - 4, 4);
%part = BinToDec(%block);
if (%part > 9)
{
switch (%part)
{
case 10:
%hex = "a" @ %hex;
case 11:
%hex = "b" @ %hex;
case 12:
%hex = "c" @ %hex;
case 13:
%hex = "d" @ %hex;
case 14:
%hex = "e" @ %hex;
case 15:
%hex = "f" @ %hex;
}
}
else
%hex = %part @ %hex;
}
if (strlen(%hex) == 0)
return "00";
else
return %hex;
}
Base64_CreateArray();

387
base/t2csri/clientSide.cs Executable file
View file

@ -0,0 +1,387 @@
// 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 @ "')");
$temp = rubyGetValue("$sha1hasher.hexdigest", 40);
%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) @ "']", 255);
}
// 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) @ "']", 1240);
}
}
else
{
%value = rubyGetValue("$accCerts['" @ strlwr(%name) @ "']", 1240);
}
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 (strcmp(%hash, %nonce) == 0 || %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 (strcmp(%hash, %nonce) == 0)
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 (strCmp(%hash, %nonce) == 0)
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
$temp = rubyEval("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();
LoginMessagePopup("ERROR", "Credential download failed. Check your username/password.");
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();
%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.");
// 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
$loginChallenge = rubyEval("rand(18446744073709551615).to_s(16)");
// append what the client thinks the server IP address is, for anti-replay purposes
$loginchallenge = $loginchallenge @ t2csri_gameServerHexAddress();
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;
}
}
$decryptedChallenge = rubyEval("$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
}

54
base/t2csri/clientSideClans.cs Executable file
View file

@ -0,0 +1,54 @@
// 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 0.5: 2009-03-18
// A little bit of development theory:
// -The Apotheosis DLL contains 3 RSA public keys. One for authentication, one for updates,
// and one for delegation. The delegation key forms the root of the community system trust heirarchy.
// -The delegated-community-enhancement server issues time limited community certificates, which
// annotate the bare account certificates. The annotations include current name, current clan, current tag
// and current clan membership so that getAuthInfo() provides all relevant information. These certificates
// are time limited to enforce the "current" status of the annotations.
// -Since game servers don't communicate with centralized systems (except for listing), the client is
// responsible for providing a signed community certificate, and if prompted, the client is also
// responsible for providing the authoratatively signed certificate from the relevant DCE. Thus, the
// server will accumilate a small cache of valid DCE certificates.
// DCE certificate format:
// DCEName DCENum IssuedEpoch ExpireEpoch 0 0 e n sig
// The two zeros are reserved for future use.
// Community certificate format:
// DCENum IssuedEpoch ExpireEpoch IssuedForGUID HexBlob Sig
// HexBlob format:
// (Follows same format as contents returned by getAuthInfo, but is hex encoded.)
function clientCmdt2csri_requestUnknownDCECert(%dceNum)
{
%cert = $T2CSRI::ClientDCESupport::DCECert[%dceNum];
if (%cert $= "")
return; // we don't have it, so we can't send it
%len = strlen(%cert);
for (%i = 0; %i < %len; %i += 200)
{
commandToServer('t2csri_getDCEChunk', getSubStr(%cert, %i, 200));
}
commandToServer('t2csri_finishedDCE');
}
function t2csri_sendCommunityCert()
{
%cert = $T2CSRI::CommunityCertificate;
if (%cert $= "")
return; // we don't have it, so we can't send it
%len = strlen(%cert);
for (%i = 0; %i < %len; %i += 200)
{
commandToServer('t2csri_sendCommunityCertChunk', getSubStr(%cert, %i, 200));
}
commandToServer('t2csri_comCertSendDone');
}

2535
base/t2csri/console_start.cs Executable file

File diff suppressed because it is too large Load diff

30
base/t2csri/glue.cs Executable file
View file

@ -0,0 +1,30 @@
// 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.0 initialization and glue file
// enable debugging console
enableWinConsole(1);
// load the torque script components
exec("t2csri/authconnect.cs");
exec("t2csri/authinterface.cs");
exec("t2csri/base64.cs");
exec("t2csri/clientSide.cs");
exec("t2csri/ipv4.cs");
exec("t2csri/rubyUtils.cs");
// load the Ruby components
rubyExec("t2csri/crypto.rb");
rubyExec("t2csri/certstore.rb");
rubyEval("certstore_loadAccounts");
$RubyEnabled = rubyEval("1");
// connect to the auth server via signed lookup
schedule(32, 0, authConnect_findAuthServer);
// get the global IP for sanity testing purposes
schedule(32, 0, ipv4_getInetAddress);

106
base/t2csri/ipv4.cs Executable file
View file

@ -0,0 +1,106 @@
// 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
// IPv4 Utils Version 1.1 (03/26/2008)
// Whatismyip spat this out for automation purposes:
// http://www.whatismyip.com/automation/n09230945.asp
// Hopefully it won't change. We only check for extern-ip once
// when the game launches, so there shouldn't be more than a
// couple of hundred hits per day from the entire T2 community.
$IPv4::AutomationURL = "/whatismyip.php";
function ipv4_getInetAddress()
{
if ($IPv4::InetAddress !$= "")
return;
if (isObject(IPv4Connection))
{
IPv4Connection.disconnect();
IPv4Connection.delete();
}
new TCPObject(IPv4Connection);
IPV4Connection.data = "GET " @ $IPv4::AutomationURL @ " HTTP/1.1\r\nHost: www.tribesnext.com\r\nUser-Agent: Tribes 2\r\nConnection: close\r\n\r\n";
IPv4Connection.connect("www.tribesnext.com:80");
}
function IPv4Connection::onConnected(%this)
{
%this.send(%this.data);
}
function IPv4Connection::onLine(%this, %line)
{
if (%line $= "" || %line == 0)
return;
$IPv4::InetAddress = %line;
%this.disconnect();
}
// added for 1.1, schedule a new attempt if we're blank, until we have an address
function IPv4Connection::onDisconnect(%this)
{
schedule(5000, 0, ipv4_getInetAddress);
}
// used for the IP-nonce sanity check...
// source will claim that this computer is the destination.
// check to make sure the destination is reasonable
function ipv4_reasonableConnection(%source, %destination)
{
if (%destination $= $IPv4::InetAddress)
{
// the destination claims to be us from the Internet. This is reasonable.
return 1;
}
else
{
// destination is different from the IPv4 Internet Address. We could be on a LAN.
if (getSubStr(%destination, 0, 2) $= "10")
{
// Class A LAN, check if the client is also on the same network
return (getSubStr(%source, 0, 2) $= "10");
}
else if (getSubStr(%destination, 0, 3) $= "172" && getSubStr(%destination, 4, 2) > 15 && getSubStr(%destination, 4, 2) < 33)
{
// Class B LAN, check if the client is also on the same network
return (getSubStr(%source, 0, 3) $= "172" && getSubStr(%source, 4, 2) > 15 && getSubStr(%source, 4, 2) < 33);
}
else if (getSubStr(%destination, 0, 7) $= "192.168")
{
// Class C LAN, check if the client is also on the same network
return (getSubStr(%source, 0, 7) $= "192.168");
}
else if (getSubStr(%destination, 0, 7) $= "169.254")
{
// Link-local addresses/Zeroconf network, check if client is from the same place
return (getSubStr(%source, 0, 7) $= "169.254");
}
else if (%destination $= $Host::BindAddress)
{
// Or it could be the pref-based bind address.
return 1;
}
else
{
// looks like the destination address provided by the source is not reasonable
// this is likely an attempt at a client token replay attack
return 0;
}
}
}
// convert a (big endian) hex block into a numeric IP
function ipv4_hexBlockToIP(%hex)
{
for (%i = 0; %i < 4; %i++)
{
%ip = %ip @ "." @ strcmp(collapseEscape("\\x" @ getSubStr(%hex, %i * 2, 2)), "");
}
return getSubStr(%ip, 1, strlen(%ip) - 1);
}

95
base/t2csri/rubyUtils.cs Executable file
View file

@ -0,0 +1,95 @@
// Lit2
$LIT2::WaitTime = 2; // Seconds
function rubyCmp(%first, %second)
{
%result = rubyEval("cmp('" @ %first @ "', '" @ %second @ "')");
return getSubStr(%result, 0, 1);
}
function rubyExec(%file)
{
%data = "";
%handle = new FileObject();
%handle.openForRead(%file);
while (!%handle.isEOF())
%data = %data @ %handle.readLine();
return rubyEval(%data);
}
function rubyGetValue(%value, %length)
{
$temp = "";
$temp = rubyEval(%value);
return $temp;
}
function rubyEval(%expression)
{
%connection = new TCPObject(LIT2Client) { connected = false; };
%connection.connect("127.0.0.1:2000");
%expression = %expression @ "<END>";
%connection.send(%expression);
%connection.disconnect();
%received = false;
%response = "";
%countedSeconds = 0;
%currentSeconds = formatTimeString("ss");
while (!%received)
{
%newSeconds = formatTimeString("ss");
if (%newSeconds != %currentSeconds)
{
%countedSeconds++;
%currentSeconds = %newSeconds;
}
if (%countedSeconds >= $LIT2::WaitTime)
return "<TIMEOUT>";
%handle = new FileObject();
%handle.openForRead("lit2.txt");
%data = %handle.readLine();
%ending = strStr(%data, "<END>");
if (%ending > -1)
{
%response = getSubStr(%data, 0, %ending);
%received = true;
}
%handle.close();
%handle.delete();
}
%handle = new FileObject();
%handle.openForWrite("lit2.txt");
%handle.writeLine("<EMPTY>");
%handle.close();
%handle.delete();
return %response;
}
function LIT2Client::onLine(%this, %line)
{
if (%line !$= "")
%this.last = %line;
}
function LIT2Client::onConnect(%this)
{
}
function LIT2Client::onDisconnect(%this)
{
%this.delete();
}
function LIT2Client::onConnectFailed(%this)
{
error("LIT2 Connection Failed---");
}

301
base/t2csri/serverSide.cs Executable file
View file

@ -0,0 +1,301 @@
// 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.2: 2009-02-16
// Clan/Rename Certificate support is included in this version.
// 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 @ "')");
$temp = rubyEval("$sha1hasher.hexdigest");
%temp = $temp;
$temp = "";
return %temp;
}
// verify with the auth server's RSA public key... hard coded in the executable
function t2csri_verify_auth_signature(%sig)
{
$temp = rubyEval("t2csri_verify_auth_signature('" @ %sig @ "').to_s(16)");
while (strLen($temp) < 40)
$temp = "0" @ $temp;
return $temp;
}
// server sends the client a certificate in chunks, since they can be rather large
function serverCmdt2csri_sendCertChunk(%client, %chunk)
{
if (%client.doneAuthenticating)
return;
//echo("Client sent certificate chunk.");
%client.t2csri_cert = %client.t2csri_cert @ %chunk;
if (strlen(%client.t2csri_cert) > 20000)
{
%client.setDisconnectReason("Account certificate too long. Check your account key for corruption.");
%client.delete();
}
}
// gets a hex version of the client's IP address
// used to prevent a replay attack as described by Rain
function t2csri_gameClientHexAddress(%client)
{
%ip = %client.getAddress();
%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 is done sending their cert... verify it, and encrypt a challenge for the client
// challenge sent to client is %clientChallenge @ %serverChallenge.
function serverCmdt2csri_sendChallenge(%client, %clientChallenge)
{
if (%client.doneAuthenticating)
return;
//echo("Client requesting challenge. CC: " @ %clientChallenge);
//echo("Client's certificate: " @ %client.t2csri_cert);
// verify that the certificate the client sent is signed by the authentication server
%user = strReplace(getField(%client.t2csri_cert, 0), "\x27", "\\\x27");
%guid = getField(%client.t2csri_cert, 1);
// sanitize GUID
for (%i = 0; %i < strlen(%guid); %i++)
{
%char = strcmp(getSubStr(%guid, %i, 1), "");
if (%char > 57 || %char < 48)
{
%client.setDisconnectReason("Invalid characters in client GUID.");
%client.delete();
return;
}
}
%e = getField(%client.t2csri_cert, 2);
%n = getField(%client.t2csri_cert, 3);
%sig = getField(%client.t2csri_cert, 4);
// sanitize e, n, sig... all of which are just hex
%rsa_chunk = strlwr(%e @ %n @ %sig);
for (%i = 0; %i < strlen(%rsa_chunk); %i++)
{
%char = strcmp(getSubStr(%rsa_chunk, %i, 1), "");
if ((%char < 48 || %char > 102) || (%char > 57 && %char < 97))
{
%client.setDisconnectReason("Invalid characters in certificate RSA fields.");
%client.delete();
return;
}
}
// get a SHA1 sum
%sumStr = %user @ "\t" @ %guid @ "\t" @ %e @ "\t" @ %n;
%certSum = sha1sum(%sumStr);
%verifSum = t2csri_verify_auth_signature(%sig);
while (strLen(%verifSum) < 40)
%verifSum = "0" @ %verifSum;
//echo("Calc'd SHA1: " @ %certSum);
//echo("Signed SHA1: " @ %verifSum);
// verify signature
if (%verifSum !$= %certSum)
{
// client supplied a bogus certificate that was never signed by the auth server
// abort their connection
%client.setDisconnectReason("Invalid account certificate.");
%client.delete();
return;
}
// process client challenge half
%client.t2csri_clientChallenge = %clientChallenge;
// sanitize the challenge to make sure it contains nothing but hex characters.
// anything else means that the client is trying to hijack control of the interpreter
%clientChallenge = strlwr(%clientChallenge);
for (%i = 0; %i < strlen(%clientChallenge); %i++)
{
%char = strcmp(getSubStr(%clientChallenge, %i, 1), "");
if ((%char < 48 || %char > 102) || (%char > 57 && %char < 97))
{
%client.setDisconnectReason("Invalid characters in client challenge.");
%client.delete();
return;
}
}
// verify that the IP address the client thinks it is connecting to is the address this server
// is reasonable... take into account connections from the same private IP subnet (192.168.*.*, 10.*.*.*, etc)
%sanityIP = ipv4_hexBlockToIP(getSubStr(%clientChallenge, strLen(%clientChallenge) - 8, 8));
%sourceIP = ipv4_hexBlockToIP(t2csri_gameClientHexAddress(%client));
if (!ipv4_reasonableConnection(%sourceIP, %sanityIP))
{
%client.setDisconnectReason("Potential man in the middle attack detected. Your client claims it connected to: " @ %sanityIP @ ", but the server does not consider this reasonable.");
%client.delete();
return;
}
// calculate a random 64-bit server side challenge
$temp = rubyEval("rand(18446744073709551615).to_s(16)");
%client.t2csri_serverChallenge = $temp @ t2csri_gameClientHexAddress(%client);
%fullChallenge = %client.t2csri_clientChallenge @ %client.t2csri_serverChallenge;
$temp = rubyEval("rsa_mod_exp('" @ %fullChallenge @ "'.to_i(16), '" @ %e @ "'.to_i(16), '" @ %n @ "'.to_i(16)).to_s(16)");
// send the challenge in 200 byte chunks
for (%i = 0; %i < strlen($temp); %i += 200)
{
commandToClient(%client, 't2csri_getChallengeChunk', getSubStr($temp, %i, 200));
}
// tell the client we're done sending
commandToClient(%client, 't2csri_decryptChallenge');
// set up the "auth" info retrieved by cid.getAuthInfo()
%client.t2csri_authinfo = %user @ "\t\t0\t" @ %guid @ "\n0\n";
// clan support: check supplemental time limited certificate, if it was sent
%comCert = %client.t2csri_comCert;
if (strLen(%comCert) > 0)
{
// assuming there is a comCert, and we aren't running in bare mode
if (getField(%comCert, 3) $= %guid)
{
// GUID in the community cert matches that of the account cert
%client.t2csri_authinfo = %client.t2csri_comInfo;
}
else
{
// uh oh... someone's being naughty.. valid cert, but for a different player. kill them!
%client.setDisconnectReason("Community supplemental certificate doesn't match account certificate.");
%client.delete();
return;
}
}
}
// verify the client's server challenge matches the one stored, if so, continue
// loading sequence
function serverCmdt2csri_challengeResponse(%client, %serverChallenge)
{
if (%client.doneAuthenticating)
return;
if (%client.t2csri_serverChallenge $= %serverChallenge)
{
// check to see if the client is GUID banned, now that we verified their certificate
if (banList_checkGUID(getField(%client.t2csri_authInfo, 3)))
{
%client.setDisconnectReason("You are not allowed to play on this server.");
%client.delete();
return;
}
// client checks out... continue loading sequence
%client.onConnect(%client.tname, %client.trgen, %client.tskin, %client.tvoic, %client.tvopi);
}
else
{
%client.setDisconnectReason("Invalid server challenge. Check your account key for corruption.");
%client.delete();
}
}
// delete a client if they spend more than 15 seconds authenticating
function t2csri_expireClient(%client)
{
if (!isObject(%client))
return;
%client.setDisconnectReason("This is a TribesNext server. You must install the TribesNext client to play. See www.tribesnext.com for info.");
%client.delete();
}
package t2csri_server
{
// packaged to create the "pre-connection" authentication phase
function GameConnection::onConnect(%client, %name, %raceGender, %skin, %voice, %voicePitch)
{
if (%client.t2csri_serverChallenge $= "" && !%client.isAIControlled() && %client.getAddress() !$= "Local")
{
// check to see if the client is IP banned
if (banList_checkIP(%client))
{
%client.setDisconnectReason("You are not allowed to play on this server.");
%client.delete();
return;
}
//echo("Client connected. Initializing pre-connection authentication phase...");
// save these for later
%client.tname = %name;
%client.trgen = %raceGender;
%client.tskin = %skin;
%client.tvoic = %voice;
%client.tvopi = %voicePitch;
// start the 15 second count down
%client.tterm = schedule(15000, 0, t2csri_expireClient, %client);
commandToClient(%client, 't2csri_pokeClient', "T2CSRI 1.1 - 03/18/2009");
return;
}
//echo("Client completed pre-authentication phase.");
// continue connection process
if (isEventPending(%client.tterm))
cancel(%client.tterm);
Parent::onConnect(%client, %name, %raceGender, %skin, %voice, %voicePitch);
%client.doneAuthenticating = 1;
}
// packaged to prevent game leaving messages for clients that are in the authentication phase
function GameConnection::onDrop(%client, %reason)
{
if (!isObject(%client) || !%client.doneAuthenticating)
return;
Parent::onDrop(%client, %reason);
}
// packaged to pull info from the certificate, rather than some internal data structures
// format is kept consistent though:
// >Name ActiveClanTag Prepend(0)/Postpend(1)Tag guid
// >NumberOfClans
// >ClanName TagForClan Prepend(0)/Postpend(1)Tag clanid rank title
// in this version, there is no clan support, so those fields are empty
// clan support will be implemented via delegation to a community server
function GameConnection::getAuthInfo(%client)
{
if (%client.getAddress() $= "Local" && %client.t2csri_authInfo $= "")
%client.t2csri_authInfo = WONGetAuthInfo();
return %client.t2csri_authInfo;
}
};
if ($PlayingOnline)
activatePackage(t2csri_server);

206
base/t2csri/serverSideClans.cs Executable file
View file

@ -0,0 +1,206 @@
// 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.0: 2009-02-13
// A little bit of development theory:
// -The Apotheosis DLL contains 3 RSA public keys. One for authentication, one for updates,
// and one for delegation. The delegation key forms the root of the community system trust heirarchy.
// -The delegated-community-enhancement server issues time limited community certificates, which
// annotate the bare account certificates. The annotations include current name, current clan, current tag
// and current clan membership so that getAuthInfo() provides all relevant information. These certificates
// are time limited to enforce the "current" status of the annotations.
// -Since game servers don't communicate with centralized systems (except for listing), the client is
// responsible for providing a signed community certificate, and if prompted, the client is also
// responsible for providing the authoratatively signed certificate from the relevant DCE. Thus, the
// server will accumilate a small cache of valid DCE certificates.
// DCE certificate format:
// DCEName DCENum IssuedEpoch ExpireEpoch 0 0 e n sig
// The two zeros are reserved for future use.
// Community certificate format:
// DCENum IssuedEpoch ExpireEpoch IssuedForGUID HexBlob Sig
// HexBlob format:
// (Follows same format as contents returned by getAuthInfo, but is hex encoded.)
// verify with the delegation RSA public key... hard coded in the executable
function t2csri_verify_deleg_signature(%sig)
{
%sig = strReplace(%sig, "\x27", "\\\x27");
$temp = rubyEval("t2csri_verify_deleg_signature('" @ %sig @ "').to_s(16)");
while (strLen($temp) < 40)
$temp = "0" @ $temp;
return $temp;
}
// allow the client to send in an unknown DCE certificate
function serverCmdt2csri_getDCEChunk(%client, %chunk)
{
// client can only send in one DCE
if (%client.t2csri_sentDCEDone)
return;
%client.t2csri_activeDCE = %client.t2csri_activeDCE @ %chunk;
if (strlen(%client.t2csri_activeDCE) > 20000)
{
%client.setDisconnectReason("DCE certificate is too long.");
%client.delete();
return;
}
}
// client finished sending their DCE. validate it
function serverCmdt2csri_finishedDCE(%client)
{
if (%client.t2csri_sentDCEDone)
return;
%dce = %client.t2csri_activeDCE;
if (getFieldCount(%dce) != 9)
{
%client.setDisconnectReason("DCE certificate format is invalid.");
%client.delete();
return;
}
%dceName = getField(%dce, 0);
%dceNum = getField(%dce, 1);
%dceIssued = getField(%dce, 2);
%dceExpire = getField(%dce, 3);
%dceE = getField(%dce, 6);
%dceN = getField(%dce, 7);
// check to see if we already have this certificate
if ($T2CSRI::DCEE[%dceNum] !$= "")
{
// we already have the cert... set the client as done
%client.t2csri_sentDCEDone = 1;
%client.t2csri_activeDCE = "";
return;
}
%dceSig = getField(%dce, 8);
%sigSha = t2csri_verify_deleg_signature(%dceSig);
%sumStr = %dceName @ "\t" @ %dceNum @ "\t" @ %dceIssued @ "\t" @ %dceExpire @ "\t";
%sumStr = %sumStr @ getField(%dce, 4) @ "\t" @ getField(%dce, 5) @ "\t" @ %dceE @ "\t" @ %dceN;
%calcSha = sha1sum(%sumStr);
if (%sigSha !$= %calcSha)
{
echo(%sigSha);
warn(%calcSha);
%client.setDisconnectReason("DCE is not signed by authoritative root.");
%client.delete();
return;
}
// passed signature check... now check to see if it has expired/issued time has arrived
%currentTime = currentEpochTime();
if (%currentTime < %dceIssued || %currentTime > %dceExpire)
{
%client.setDisconnectReason("DCE is not valid for the current time period.");
%client.delete();
return;
}
// passed time check... enter it into global data structure
$T2CSRI::DCEName[%dceNum] = %dceName;
$T2CSRI::DCEE[%dceNum] = %dceE;
$T2CSRI::DCEN[%dceNum] = %dceN;
// client has successfully sent a DCE
%client.t2csri_sentDCEDone = 1;
%client.t2csri_activeDCE = "";
// client was pending on a certificate signature check, do that now that we have the DCE cert
if (%client.t2csri_pendingDCE)
{
%client.t2csri_pendingDCE = 0;
serverCmdt2csri_comCertSendDone(%client);
}
}
// client sending community cert chunk
function serverCmdt2csri_sendCommunityCertChunk(%client, %chunk)
{
// client can only send in one community cert
if (%client.t2csri_sentComCertDone)
return;
%client.t2csri_comCert = %client.t2csri_comCert @ %chunk;
if (strlen(%client.t2csri_comCert) > 20000)
{
%client.setDisconnectReason("Community certificate is too long.");
%client.delete();
return;
}
}
// client has sent in a full community certificate... validate and parse it
function serverCmdt2csri_comCertSendDone(%client)
{
if (%client.t2csri_sentComCertDone)
return;
%comCert = %client.t2csri_comCert;
if (getFieldCount(%comCert) != 6)
{
%client.setDisconnectReason("Community certificate format is invalid.");
%client.delete();
return;
}
// parse
%dceNum = getField(%comCert, 0);
%issued = getField(%comCert, 1);
%expire = getField(%comCert, 2);
%guid = getField(%comCert, 3);
%blob = getField(%comCert, 4);
%sig = getField(%comCert, 5);
%sumStr = getFieldS(%comCert, 0, 4);
%calcSha = sha1Sum(%sumStr);
// find the correct DCE
%e = $T2CSRI::DCEE[%dceNum];
%n = $T2CSRI::DCEN[%dceNum];
// what if we don't have it? ask the client for a copy
if (%e $= "")
{
%client.t2csri_pendingDCE = 1;
commandToClient(%client, 't2csri_requestUnknownDCECert', %dceNum);
return;
}
// get the signature SHA1
$temp = rubyEval("rsa_mod_exp('" @ %sig @ "'.to_i(16), '" @ %e @ "'.to_i(16), '" @ %n @ "'.to_i(16)).to_s(16)");
while (strlen($temp) < 40)
$temp = "0" @ $temp;
%sigSha = $temp;
if (%sigSha !$= %calcSha)
{
%client.setDisconnectReason("Community cert is not signed by a known/valid DCE.");
%client.delete();
return;
}
// check expiration
%currentTime = currentEpochTime();
if (%currentTime > %expire)
{
%client.setDisconnectReason("Community cert has expired. Get a fresh one from the DCE.");
%client.delete();
return;
}
// valid cert... set the field for processing in the auth-phase code
%len = strlen(%blob);
for (%i = 0; %i < %len; %i += 2)
{
%decoded = %decoded @ collapseEscape("\\x" @ getSubStr(%blob, %i, 2));
}
%client.t2csri_comInfo = %decoded @ "\n";
%client.t2csri_sentComCertDone = 1;
}

23
base/t2csri/serverglue.cs Executable file
View file

@ -0,0 +1,23 @@
// 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.0 initialization and glue file (server side)
if (isObject(ServerGroup))
{
// load the Ruby utils and cryptography module
exec("t2csri/rubyUtils.cs");
rubyExec("t2csri/crypto.rb");
// load the torque script components
exec("t2csri/serverSide.cs");
exec("t2csri/serverSideClans.cs");
exec("t2csri/bans.cs");
exec("t2csri/ipv4.cs");
exec("t2csri/base64.cs");
// get the global IP for sanity testing purposes
schedule(32, 0, ipv4_getInetAddress);
}