#include "NativeFeatureIncludes.h"
#if _RAKNET_SUPPORT_TwoWayAuthentication==1

#include "TwoWayAuthentication.h"
#include "Rand.h"
#include "GetTime.h"
#include "MessageIdentifiers.h"
#include "BitStream.h"
#include "RakPeerInterface.h"

#if LIBCAT_SECURITY==1
static const int HASH_BITS = 256;
static const int HASH_BYTES = HASH_BITS / 8;
static const int STRENGTHENING_FACTOR = 256;
#include <cat/crypt/hash/Skein.hpp>
#endif

using namespace RakNet;

enum NegotiationIdentifiers
{
	ID_NONCE_REQUEST,
	ID_NONCE_REPLY,
	ID_HASHED_NONCE_AND_PASSWORD,
};

TwoWayAuthentication::NonceGenerator::NonceGenerator() {nextRequestId=0;}
TwoWayAuthentication::NonceGenerator::~NonceGenerator()
{
	Clear();
}
void TwoWayAuthentication::NonceGenerator::GetNonce(char nonce[TWO_WAY_AUTHENTICATION_NONCE_LENGTH], unsigned short *requestId, RakNet::AddressOrGUID remoteSystem)
{
	TwoWayAuthentication::NonceAndRemoteSystemRequest *narsr = RakNet::OP_NEW<TwoWayAuthentication::NonceAndRemoteSystemRequest>(_FILE_AND_LINE_);
	narsr->remoteSystem=remoteSystem;
	GenerateNonce(narsr->nonce);
	narsr->requestId=nextRequestId++;
	*requestId=narsr->requestId;
	memcpy(nonce,narsr->nonce,TWO_WAY_AUTHENTICATION_NONCE_LENGTH);
	narsr->whenGenerated=RakNet::GetTime();
	generatedNonces.Push(narsr,_FILE_AND_LINE_);
}
void TwoWayAuthentication::NonceGenerator::GenerateNonce(char nonce[TWO_WAY_AUTHENTICATION_NONCE_LENGTH])
{
	fillBufferMT(nonce,TWO_WAY_AUTHENTICATION_NONCE_LENGTH);
}
bool TwoWayAuthentication::NonceGenerator::GetNonceById(char nonce[TWO_WAY_AUTHENTICATION_NONCE_LENGTH], unsigned short requestId, RakNet::AddressOrGUID remoteSystem, bool popIfFound)
{
	unsigned int i;
	for (i=0; i < generatedNonces.Size(); i++)
	{
		if (generatedNonces[i]->requestId==requestId)
		{
			if (remoteSystem==generatedNonces[i]->remoteSystem)
			{
				memcpy(nonce,generatedNonces[i]->nonce,TWO_WAY_AUTHENTICATION_NONCE_LENGTH);
				if (popIfFound)
				{
					RakNet::OP_DELETE(generatedNonces[i],_FILE_AND_LINE_);
					generatedNonces.RemoveAtIndex(i);
				}
				return true;
			}
			else
			{
				return false;
			}
		}
	}
	return false;
}
void TwoWayAuthentication::NonceGenerator::Clear(void)
{
	unsigned int i;
	for (i=0; i < generatedNonces.Size(); i++)
		RakNet::OP_DELETE(generatedNonces[i],_FILE_AND_LINE_);
	generatedNonces.Clear(true,_FILE_AND_LINE_);
}
void TwoWayAuthentication::NonceGenerator::ClearByAddress(RakNet::AddressOrGUID remoteSystem)
{
	unsigned int i=0;
	while (i < generatedNonces.Size())
	{
		if (generatedNonces[i]->remoteSystem==remoteSystem)
		{
			RakNet::OP_DELETE(generatedNonces[i],_FILE_AND_LINE_);
			generatedNonces.RemoveAtIndex(i);
		}
		else
		{
			i++;
		}
	}
}
void TwoWayAuthentication::NonceGenerator::Update(RakNet::Time curTime)
{
	if (generatedNonces.Size()>0 && GreaterThan(curTime-5000, generatedNonces[0]->whenGenerated))
	{
		RakNet::OP_DELETE(generatedNonces[0], _FILE_AND_LINE_);
		generatedNonces.RemoveAtIndex(0);
	}
}
TwoWayAuthentication::TwoWayAuthentication()
{
	whenLastTimeoutCheck=RakNet::GetTime();
	seedMT(RakNet::GetTimeMS());
}
TwoWayAuthentication::~TwoWayAuthentication()
{
	Clear();
}
bool TwoWayAuthentication::AddPassword(RakNet::RakString identifier, RakNet::RakString password)
{
	if (password.IsEmpty())
		return false;

	if (identifier.IsEmpty())
		return false;

	if (password==identifier)
		return false; // Insecure

	if (passwords.GetIndexOf(identifier.C_String()).IsInvalid()==false)
		return false; // This identifier already in use

	passwords.Push(identifier, password,_FILE_AND_LINE_);
	return true;
}
bool TwoWayAuthentication::Challenge(RakNet::RakString identifier, AddressOrGUID remoteSystem)
{
	DataStructures::HashIndex skhi = passwords.GetIndexOf(identifier.C_String());
	if (skhi.IsInvalid())
		return false;

	RakNet::BitStream bsOut;
	bsOut.Write((MessageID)ID_TWO_WAY_AUTHENTICATION_NEGOTIATION);
	bsOut.Write((MessageID)ID_NONCE_REQUEST);
	SendUnified(&bsOut,HIGH_PRIORITY,RELIABLE_ORDERED,0,remoteSystem,false);

	PendingChallenge pc;
	pc.identifier=identifier;
	pc.remoteSystem=remoteSystem;
	pc.time=RakNet::GetTime();
	pc.sentHash=false;
	outgoingChallenges.Push(pc,_FILE_AND_LINE_);

	return true;
}
void TwoWayAuthentication::Update(void)
{
	RakNet::Time curTime = RakNet::GetTime();
	nonceGenerator.Update(curTime);
	if (GreaterThan(curTime - CHALLENGE_MINIMUM_TIMEOUT, whenLastTimeoutCheck))
	{
		while (outgoingChallenges.Size() && GreaterThan(curTime - CHALLENGE_MINIMUM_TIMEOUT, outgoingChallenges.Peek().time))
		{
			PendingChallenge pc = outgoingChallenges.Pop();

			// Tell the user about the timeout
			PushToUser(ID_TWO_WAY_AUTHENTICATION_OUTGOING_CHALLENGE_TIMEOUT, pc.identifier, pc.remoteSystem);					
		}

		whenLastTimeoutCheck=curTime+CHALLENGE_MINIMUM_TIMEOUT;
	}
}
PluginReceiveResult TwoWayAuthentication::OnReceive(Packet *packet)
{
	switch (packet->data[0])
	{
		case ID_TWO_WAY_AUTHENTICATION_NEGOTIATION:
		{
			if (packet->length>=sizeof(MessageID)*2)
			{
				switch (packet->data[sizeof(MessageID)])
				{
				case ID_NONCE_REQUEST:
					{
						OnNonceRequest(packet);
					}
					break;
				case ID_NONCE_REPLY:
					{
						OnNonceReply(packet);
					}
					break;
				case ID_HASHED_NONCE_AND_PASSWORD:
					{
						return OnHashedNonceAndPassword(packet);
					}
					break;
				}
			}
			return RR_STOP_PROCESSING_AND_DEALLOCATE;
		}
		case ID_TWO_WAY_AUTHENTICATION_OUTGOING_CHALLENGE_FAILURE:
		case ID_TWO_WAY_AUTHENTICATION_OUTGOING_CHALLENGE_SUCCESS:
		{
			if (packet->wasGeneratedLocally==false)
			{
				OnPasswordResult(packet);
				return RR_STOP_PROCESSING_AND_DEALLOCATE;
			}
			else
				break;
		}
		break;
		// These should only be generated locally
		case ID_TWO_WAY_AUTHENTICATION_INCOMING_CHALLENGE_SUCCESS:
		case ID_TWO_WAY_AUTHENTICATION_INCOMING_CHALLENGE_FAILURE:
		case ID_TWO_WAY_AUTHENTICATION_OUTGOING_CHALLENGE_TIMEOUT:
			if (packet->wasGeneratedLocally==false)
				return RR_STOP_PROCESSING_AND_DEALLOCATE;
			break;
	}
	
	return RR_CONTINUE_PROCESSING;
}
void TwoWayAuthentication::OnRakPeerShutdown(void)
{
	Clear();
}
void TwoWayAuthentication::OnClosedConnection(const SystemAddress &systemAddress, RakNetGUID rakNetGUID, PI2_LostConnectionReason lostConnectionReason )
{
	(void) lostConnectionReason;

	// Remove from pending challenges
	unsigned int i=0;
	while (i < outgoingChallenges.Size())
	{
		if ((rakNetGUID!=UNASSIGNED_RAKNET_GUID && outgoingChallenges[i].remoteSystem.rakNetGuid==rakNetGUID) ||
			(systemAddress!=UNASSIGNED_SYSTEM_ADDRESS && outgoingChallenges[i].remoteSystem.systemAddress==systemAddress))
		{
			outgoingChallenges.RemoveAtIndex(i);
		}
		else
		{
			i++;
		}
	}

	if (rakNetGUID!=UNASSIGNED_RAKNET_GUID)
		nonceGenerator.ClearByAddress(rakNetGUID);
	else
		nonceGenerator.ClearByAddress(systemAddress);
}
void TwoWayAuthentication::Clear(void)
{
	outgoingChallenges.Clear(_FILE_AND_LINE_);
	passwords.Clear(_FILE_AND_LINE_);
	nonceGenerator.Clear();
}
void TwoWayAuthentication::PushToUser(MessageID messageId, RakNet::RakString password, RakNet::AddressOrGUID remoteSystem)
{
	RakNet::BitStream output;
	output.Write(messageId);
	if (password.IsEmpty()==false)
		output.Write(password);
	Packet *p = AllocatePacketUnified(output.GetNumberOfBytesUsed());
	p->systemAddress=remoteSystem.systemAddress;
	p->systemAddress.systemIndex=(SystemIndex)-1;
	p->guid=remoteSystem.rakNetGuid;
	p->wasGeneratedLocally=true;
	memcpy(p->data, output.GetData(), output.GetNumberOfBytesUsed());
	rakPeerInterface->PushBackPacket(p, true);
}
void TwoWayAuthentication::OnNonceRequest(Packet *packet)
{
	RakNet::BitStream bsIn(packet->data, packet->length, false);
	bsIn.IgnoreBytes(sizeof(MessageID)*2);

	char nonce[TWO_WAY_AUTHENTICATION_NONCE_LENGTH];
	unsigned short requestId;
	nonceGenerator.GetNonce(nonce,&requestId,packet);

	RakNet::BitStream bsOut;
	bsOut.Write((MessageID)ID_TWO_WAY_AUTHENTICATION_NEGOTIATION);
	bsOut.Write((MessageID)ID_NONCE_REPLY);
	bsOut.Write(requestId);
	bsOut.WriteAlignedBytes((const unsigned char*) nonce,TWO_WAY_AUTHENTICATION_NONCE_LENGTH);
	SendUnified(&bsOut,HIGH_PRIORITY,RELIABLE_ORDERED,0,packet,false);
}
void TwoWayAuthentication::OnNonceReply(Packet *packet)
{
	RakNet::BitStream bsIn(packet->data, packet->length, false);
	bsIn.IgnoreBytes(sizeof(MessageID)*2);

	char thierNonce[TWO_WAY_AUTHENTICATION_NONCE_LENGTH];
	unsigned short requestId;
	bsIn.Read(requestId);
	bsIn.ReadAlignedBytes((unsigned char *) thierNonce,TWO_WAY_AUTHENTICATION_NONCE_LENGTH);

	// Lookup one of the negotiations for this guid/system address
	AddressOrGUID aog(packet);
	unsigned int i;
	for (i=0; i < outgoingChallenges.Size(); i++)
	{
		if (outgoingChallenges[i].remoteSystem==aog && outgoingChallenges[i].sentHash==false)
		{
			outgoingChallenges[i].sentHash=true;

			// Get the password for this identifier
			DataStructures::HashIndex skhi = passwords.GetIndexOf(outgoingChallenges[i].identifier.C_String());
			if (skhi.IsInvalid()==false)
			{
				RakNet::RakString password = passwords.ItemAtIndex(skhi);

				// Hash their nonce with password and reply
				char hashedNonceAndPw[HASHED_NONCE_AND_PW_LENGTH];
				Hash(thierNonce, password, hashedNonceAndPw);

				// Send
				RakNet::BitStream bsOut;
				bsOut.Write((MessageID)ID_TWO_WAY_AUTHENTICATION_NEGOTIATION);
				bsOut.Write((MessageID)ID_HASHED_NONCE_AND_PASSWORD);
				bsOut.Write(requestId);
				bsOut.Write(outgoingChallenges[i].identifier); // Identifier helps the other system lookup the password quickly.
				bsOut.WriteAlignedBytes((const unsigned char*) hashedNonceAndPw,HASHED_NONCE_AND_PW_LENGTH);
				SendUnified(&bsOut,HIGH_PRIORITY,RELIABLE_ORDERED,0,packet,false);
			}

			return;
		}
	}
}
PluginReceiveResult TwoWayAuthentication::OnHashedNonceAndPassword(Packet *packet)
{
	RakNet::BitStream bsIn(packet->data, packet->length, false);
	bsIn.IgnoreBytes(sizeof(MessageID)*2);

	char remoteHashedNonceAndPw[HASHED_NONCE_AND_PW_LENGTH];
	unsigned short requestId;
	bsIn.Read(requestId);
	RakNet::RakString passwordIdentifier;
	bsIn.Read(passwordIdentifier);
	bsIn.ReadAlignedBytes((unsigned char *) remoteHashedNonceAndPw,HASHED_NONCE_AND_PW_LENGTH);

	// Look up used nonce from requestId
	char usedNonce[TWO_WAY_AUTHENTICATION_NONCE_LENGTH];
	if (nonceGenerator.GetNonceById(usedNonce, requestId, packet, true)==false)
		return RR_STOP_PROCESSING_AND_DEALLOCATE;

	DataStructures::HashIndex skhi = passwords.GetIndexOf(passwordIdentifier.C_String());
	if (skhi.IsInvalid()==false)
	{
		char hashedThisNonceAndPw[HASHED_NONCE_AND_PW_LENGTH];
		Hash(usedNonce, passwords.ItemAtIndex(skhi), hashedThisNonceAndPw);
		if (memcmp(hashedThisNonceAndPw, remoteHashedNonceAndPw,HASHED_NONCE_AND_PW_LENGTH)==0)
		{
			// Pass
			RakNet::BitStream bsOut;
			bsOut.Write((MessageID)ID_TWO_WAY_AUTHENTICATION_OUTGOING_CHALLENGE_SUCCESS);
			bsOut.WriteAlignedBytes((const unsigned char*) usedNonce,TWO_WAY_AUTHENTICATION_NONCE_LENGTH);
			bsOut.WriteAlignedBytes((const unsigned char*) remoteHashedNonceAndPw,HASHED_NONCE_AND_PW_LENGTH);
			bsOut.Write(passwordIdentifier);
			SendUnified(&bsOut,HIGH_PRIORITY,RELIABLE_ORDERED,0,packet,false);

			// Incoming success, modify packet header to tell user
			PushToUser(ID_TWO_WAY_AUTHENTICATION_INCOMING_CHALLENGE_SUCCESS, passwordIdentifier, packet);

			return RR_STOP_PROCESSING_AND_DEALLOCATE;
		}
	}

	// Incoming failure, modify arrived packet header to tell user
	packet->data[0]=(MessageID) ID_TWO_WAY_AUTHENTICATION_INCOMING_CHALLENGE_FAILURE;
	
	RakNet::BitStream bsOut;
	bsOut.Write((MessageID)ID_TWO_WAY_AUTHENTICATION_OUTGOING_CHALLENGE_FAILURE);
	bsOut.WriteAlignedBytes((const unsigned char*) usedNonce,TWO_WAY_AUTHENTICATION_NONCE_LENGTH);
	bsOut.WriteAlignedBytes((const unsigned char*) remoteHashedNonceAndPw,HASHED_NONCE_AND_PW_LENGTH);
	bsOut.Write(passwordIdentifier);
	SendUnified(&bsOut,HIGH_PRIORITY,RELIABLE_ORDERED,0,packet,false);

	return RR_CONTINUE_PROCESSING;
}
void TwoWayAuthentication::OnPasswordResult(Packet *packet)
{
	RakNet::BitStream bsIn(packet->data, packet->length, false);
	bsIn.IgnoreBytes(sizeof(MessageID)*1);
	char usedNonce[TWO_WAY_AUTHENTICATION_NONCE_LENGTH];
	bsIn.ReadAlignedBytes((unsigned char *)usedNonce,TWO_WAY_AUTHENTICATION_NONCE_LENGTH);
	char hashedNonceAndPw[HASHED_NONCE_AND_PW_LENGTH];
	bsIn.ReadAlignedBytes((unsigned char *)hashedNonceAndPw,HASHED_NONCE_AND_PW_LENGTH);
	RakNet::RakString passwordIdentifier;
	bsIn.Read(passwordIdentifier);

	DataStructures::HashIndex skhi = passwords.GetIndexOf(passwordIdentifier.C_String());
	if (skhi.IsInvalid()==false)
	{
		RakNet::RakString password = passwords.ItemAtIndex(skhi);
		char testHash[HASHED_NONCE_AND_PW_LENGTH];
		Hash(usedNonce, password, testHash);
		if (memcmp(testHash,hashedNonceAndPw,HASHED_NONCE_AND_PW_LENGTH)==0)
		{
			// Lookup the outgoing challenge and remove it from the list
			unsigned int i;
			AddressOrGUID aog(packet);
			for (i=0; i < outgoingChallenges.Size(); i++)
			{
				if (outgoingChallenges[i].identifier==passwordIdentifier &&
					outgoingChallenges[i].remoteSystem==aog &&
					outgoingChallenges[i].sentHash==true)
				{
					outgoingChallenges.RemoveAtIndex(i);

					PushToUser(packet->data[0], passwordIdentifier, packet);
					return;
				}
			}
		}
	}
}
void TwoWayAuthentication::Hash(char thierNonce[TWO_WAY_AUTHENTICATION_NONCE_LENGTH], RakNet::RakString password, char out[HASHED_NONCE_AND_PW_LENGTH])
{
#if LIBCAT_SECURITY==1
	cat::Skein hash;
	if (!hash.BeginKey(HASH_BITS)) return;
	hash.Crunch(thierNonce, TWO_WAY_AUTHENTICATION_NONCE_LENGTH);
	hash.Crunch(password.C_String(), (int) password.GetLength());
	hash.End();
	hash.Generate(out, HASH_BYTES, STRENGTHENING_FACTOR);
#else
	CSHA1 sha1;
	sha1.Update((unsigned char *) thierNonce, TWO_WAY_AUTHENTICATION_NONCE_LENGTH);
	sha1.Update((unsigned char *) password.C_String(), (unsigned int) password.GetLength());
	sha1.Final();
	sha1.GetHash((unsigned char *) out);
#endif
}

#endif
