//
//crypt.c
//
//Interface for handling a crypto stream.
//
//
//-UserX 2001/11/16

#include <string.h>
#include "pipe/cs-iip1.h"
//#include "crypt/xtea.h"
#include "crypt/dh.h"
#include "pipe/pipe.h"
#include "pipe/c-stream.h"
#include "base/mem.h"
#include "base/bignum.h"
#include "base/logger.h"
#include "base/str.h"
#include "base/dblock.h"
#include "crypt/random.h"
#include "crypt/salt.h"
#include "misc/global.h"

CryptStreamIIP1 blankcryptstreamiip1 = BLANKCRYPTSTREAMIIP1;

CryptStream *csiip1Make(char *csname, char *options) {
	CryptStreamIIP1 *cs = memCopy(&blankcryptstreamiip1, sizeof(CryptStreamIIP1), "CryptStreamIIP1", NULL);
	LOGDEBUG(stringJoin("csiip1Make:", ptrToString(cs)));
	cs->blowfishin = memAlloc(sizeof(BlowfishContext), "CryptStreamBlowfishContext", NULL);
	cs->blowfishout = memAlloc(sizeof(BlowfishContext), "CryptStreamBlowfishContext", NULL);
	return (CryptStream *)cs;
}

void csiip1Init(CryptStreamIIP1 *cs, Pipe *thispipe) {//, Pipe *backpipe
	LOGDEBUG(stringJoin("csiip1Init:", ptrToString(cs)));
	//cs->backPipe = backpipe;
	cs->cs.ParentPipe = thispipe;
}

void csiip1Free(CryptStreamIIP1 *cs) {
	LOGDEBUG(stringJoin("csiip1Free:", ptrToString(cs)));
	if(cs == NULL) {
		return;
	}
	bignumFree(cs->privkey);
	bignumFree(cs->localkey);
	bignumFree(cs->remotekey);
	bignumFree(cs->sharedkey);
	bignumFree(cs->setremotekey);
	dblockFree(cs->insessionkey);
	dblockFree(cs->outsessionkey);

	//ifree also does the clearing of memory. This is just being overly cautious.
	memset(cs->blowfishin, 0, sizeof(BlowfishContext));
	memset(cs->blowfishout, 0, sizeof(BlowfishContext));
	memFree(cs->blowfishin);//xifree(cs->blowfishin);
	memFree(cs->blowfishout);//xifree(cs->blowfishout);

	memcpy(cs, &blankcryptstreamiip1, sizeof(CryptStreamIIP1));

	memFree(cs);//xifree(cs);
}

//xor rand into the next key position then rotates the session keys.
//will destroy the contents of rand
//individual key sizes are aasume to be the same size as rand
void csiip1RotateSessionKey(DataBlock *key, DataBlock *rand) {
	int i;
	for(i = 0; i < rand->size; i++) {
		key->data[rand->size + i] ^= rand->data[i];
	}
	memmove(rand->data, key->data, rand->size);
	memmove(key->data, key->data + rand->size, key->size - rand->size);
	memmove(key->data + key->size - rand->size, rand->data, rand->size);

}

void csiip1ApplyCounter(uint8 *data, uint8 *counter) {
	int i;
	for(i = 0; i < BLOCK_LENGTH; i++) {
		data[i] ^= counter[i];
	}
	for(i = BLOCK_LENGTH1; i >= 0; i--) {
		if(++counter[i] != 0) {
			break;
		}
	}
}

void csiip1Decode(CryptStreamIIP1 *cs, DataBlock *db) {
	int i;
	uint32 l, r;
	for(i = 0; i < db->size; i += BLOCK_LENGTH) {
		l = *((uint32 *)(&db->data[i + 0]));
		r = *((uint32 *)(&db->data[i + 4]));
#ifdef BIG__ENDIAN
		l = ENDIANSWAP32(l);
		r = ENDIANSWAP32(r);
#endif
		Blowfish_Decrypt(cs->blowfishin, (unsigned long *)&l, (unsigned long *)&r);
#ifdef BIG__ENDIAN
		l = ENDIANSWAP32(l);
		r = ENDIANSWAP32(r);
#endif
		*((uint32 *)(&db->data[i + 0])) = l;
		*((uint32 *)(&db->data[i + 4])) = r;
		csiip1ApplyCounter(db->data + i, cs->counterin);
	}
	//xteabDecode((uint32 *)db->data, db->size >> 2, (uint32 *)cs->insessionkey->data);
}

void csiip1Encode(CryptStreamIIP1 *cs, DataBlock *db) {
	int i;
	uint32 l, r;
	for(i = 0; i < db->size; i += BLOCK_LENGTH) {
		csiip1ApplyCounter(db->data + i, cs->counterout);
		l = *((uint32 *)(&db->data[i + 0]));
		r = *((uint32 *)(&db->data[i + 4]));
#ifdef BIG__ENDIAN
		l = ENDIANSWAP32(l);
		r = ENDIANSWAP32(r);
#endif
		Blowfish_Encrypt(cs->blowfishout, (unsigned long *)&l, (unsigned long *)&r);
#ifdef BIG__ENDIAN
		l = ENDIANSWAP32(l);
		r = ENDIANSWAP32(r);
#endif
		*((uint32 *)(&db->data[i + 0])) = l;
		*((uint32 *)(&db->data[i + 4])) = r;
	}
	//xteabEncode((uint32 *)db->data, db->size >> XTEA_BYTES_PER_WORD_BITS, (uint32 *)cs->outsessionkey->data);
}

void csiip1Decrypt(CryptStreamIIP1 *cs) {
	int insize;
	DataBlock *db;
	while(1) {
		if(cs->inNeeded == 0) {
			insize = BLOCK_LENGTH;
		} else if(cs->inNeeded == -1) {
			insize = KEY_LENGTH;
		} else {
			insize = (cs->inNeeded + BLOCK_LENGTH1) & (~BLOCK_LENGTH1);
		}
		if(cs->cs.ParentPipe->backPipe->inBuffer->size < insize) {
			return;
		}
		db = dblockCopyPart(cs->cs.ParentPipe->backPipe->inBuffer, 0, insize);
		saltAdd(db); //todo: this is probably not the best place to collect salt from.

		cs->cs.ParentPipe->backPipe->inBuffer = dblockDelete(cs->cs.ParentPipe->backPipe->inBuffer, 0, insize);
		csiip1Decode(cs, db);
		if(cs->inNeeded == 0) {
			cs->inNeeded = db->data[0] + (db->data[1] << 8);
			if(cs->inNeeded == 0) {
				cs->inNeeded = -1;
			}
			LOGDEBUGTRAFFIC(stringJoin("csiip1Decrypt: bytes waiting for ", intToHexString(cs->inNeeded)));
		} else if(cs->inNeeded == -1) {
			LOGDEBUG("csiip1Decrypt: Got session key rotate:");
			LOGDEBUGKEY(stringJoin("key:", bufferToHexString(db->data, db->size)));
			csiip1RotateSessionKey(cs->insessionkey, db);
			Blowfish_Init(cs->blowfishin, cs->insessionkey->data, KEY_LENGTH);
			cs->inNeeded = 0;
		} else {
			db->size = cs->inNeeded;
			cs->cs.ParentPipe->inBuffer = dblockAppendBlock(cs->cs.ParentPipe->inBuffer, db);
			LOGDEBUGTRAFFIC(stringJoin("csiip1Decrypt: got data, length ", intToHexString(cs->inNeeded)));
			cs->inNeeded = 0;
		}
		dblockFree(db);
	}
	//int i;
	//for(i = 0; i < cs->cs.ParentPipe->backPipe->inBuffer->size; i++) {
	//	cs->cs.ParentPipe->backPipe->inBuffer->data[i] ^= cs->sessionkey->data[0];
	//}
	//cs->cs.ParentPipe->inBuffer = dblockAppendBlock(cs->cs.ParentPipe->inBuffer, cs->cs.ParentPipe->backPipe->inBuffer);
	//cs->cs.ParentPipe->backPipe->inBuffer->size = 0;
}

void csiip1EncryptWrite(CryptStreamIIP1 *cs, DataBlock *db) {

	saltBuffer(db->data + db->size, (BLOCK_LENGTH - (db->size & BLOCK_LENGTH1)) & BLOCK_LENGTH1);

	db->size = (db->size + BLOCK_LENGTH1) & (~BLOCK_LENGTH1);

	csiip1Encode(cs, db);
	cs->cs.ParentPipe->backPipe->outBuffer = dblockAppendBlock(cs->cs.ParentPipe->backPipe->outBuffer, db);

	saltAdd(db); //todo: this is probably not the best place to collect salt from.

	dblockFree(db);

}

void csiip1Encrypt(CryptStreamIIP1 *cs) {
	DataBlock *db;
	DataBlock *dbr;
	int outsize;
	if(cs->cs.ParentPipe->outBuffer->size == 0) {
		return;
	}

	outsize = cs->cs.ParentPipe->outBuffer->size;

	if(outsize > 0xfff0) {
		outsize = 0xfff0;
	}
	LOGDEBUGTRAFFIC(stringJoin("csiip1Encrypt: sending bytes ", intToHexString(outsize)));

	db = dblockMake(BLOCK_LENGTH, BLOCK_LENGTH, "csiip1Encrypt");
	db->data[0] = outsize & 0xff;
	db->data[1] = (outsize >> 8) & 0xff;
	saltBuffer(db->data + 2, 6);

	csiip1EncryptWrite(cs, db);

	db = dblockCopyPart(cs->cs.ParentPipe->outBuffer, 0, outsize);
	db = dblockResize(db, (outsize + BLOCK_LENGTH1) & (~BLOCK_LENGTH1));
	db->size = (outsize + BLOCK_LENGTH1) & (~BLOCK_LENGTH1);

	csiip1EncryptWrite(cs, db);

	cs->cs.ParentPipe->outBuffer = dblockDelete(cs->cs.ParentPipe->outBuffer, 0, outsize);

	if(++cs->outCount >= WRITES_TILL_ROTATE) {
		cs->outCount = 0;

		db = dblockMake(BLOCK_LENGTH, BLOCK_LENGTH, "csiip1EncryptRotate");
		db->data[0] = 0;
		db->data[1] = 0;

		csiip1EncryptWrite(cs, db);

		dbr = randomDBlock(KEY_LENGTH);
		db = dblockCopy(dbr);

		csiip1EncryptWrite(cs, db);

		LOGDEBUGTRAFFIC("csiip1Encrypt: Sent session key rotate:");
		LOGDEBUGKEY(stringJoin("key:", bufferToHexString(dbr->data, dbr->size)));
		csiip1RotateSessionKey(cs->outsessionkey, dbr);

		Blowfish_Init(cs->blowfishout, cs->outsessionkey->data, KEY_LENGTH);

		dblockFree(dbr);
	}



	//int i;
	//for(i = 0; i < cs->cs.ParentPipe->outBuffer->size; i++) {
	//	cs->cs.ParentPipe->outBuffer->data[i] ^= cs->sessionkey->data[0];
	//}
	//cs->cs.ParentPipe->backPipe->outBuffer = dblockAppendBlock(cs->cs.ParentPipe->backPipe->outBuffer, cs->cs.ParentPipe->outBuffer);
	//cs->cs.ParentPipe->outBuffer->size = 0;
}



void csiip1InHandshake(CryptStreamIIP1 *cs) {
	int i;
	DataBlock *db;
	switch(cs->inShake) {
	case 1:
		if(cs->cs.ParentPipe->backPipe->inBuffer->size < CSIIP1_DH_LENGTH / 8) {
			return;
		}
		if(nextdhdelay > 0) {
			return;
		}
		if(cs->cs.noderef != NULL) {
			if(!isStringBlank(cs->cs.noderef->PrivateKey)) {
				cs->privkey = bignumFromHex(cs->cs.noderef->PrivateKey);
			}
		}
		db = dblockCopyPart(cs->cs.ParentPipe->backPipe->inBuffer, 0, CSIIP1_DH_LENGTH / 8);
		cs->remotekey = bignumFromBuffer(db->data, CSIIP1_DH_LENGTH / 8);
		LOGDEBUG(stringJoinMany("csiip1InHandshake: ", ptrToString(cs), " got remote public key", NULL));
		//LOGDEBUGKEY(stringJoin("remotekey:", bufferToHexString((uint8 *)cs->remotekey->data, CSIIP1_DH_LENGTH / 8)));
		LOGDEBUGKEY(stringJoin("remotekey:", bignumToHex(cs->remotekey)));
		cs->cs.ParentPipe->backPipe->inBuffer = dblockDelete(cs->cs.ParentPipe->backPipe->inBuffer, 0, CSIIP1_DH_LENGTH / 8);
		dblockFree(db);
		cs->inShake = 2;
		if(cs->setremotekey != NULL) {
			if(bignumCompare(cs->remotekey, cs->setremotekey) != 0) {
				LOGERROR("csiip1InHandshake: remotekey is wrong!");
				//LOGDEBUGKEY(stringJoin("remotekey:", bufferToHexString((uint8 *)cs->setremotekey->data, CSIIP1_DH_LENGTH / 8)));
				LOGDEBUGKEY(stringJoin("expectedkey:", bignumToHex(cs->setremotekey)));
				cs->inShake = -1;
				cs->cs.ParentPipe->backPipe->status |= PSTATUS_CLOSED;
				cs->cs.ParentPipe->status |= PSTATUS_CLOSED;
				return;
			} else {
				LOGDEBUG(stringJoinMany("csiip11InHandshake: ", ptrToString(cs), " public key mathced", NULL));
			}
		}
	case 2:
		if(cs->localkey == NULL) {
			return;
		}
		cs->sharedkey = dhGenerateSharedKey(CSIIP1_DH_LENGTH, cs->privkey, cs->remotekey);
		randomAddEntropyClock(2);
		nextdhdelay = NEXTDHDELAY;

		cs->insessionkey = dblockMake(CSIIP1_DH_LENGTH / 8, CSIIP1_DH_LENGTH / 8, "sessionkey");
		//cs->outsessionkey = dblockMake(DH_PUBLIC_KEY_LENGTH, DH_PUBLIC_KEY_LENGTH);
		LOGDEBUG(stringJoinMany("csiip1InHandshake: ", ptrToString(cs), " made shared key", NULL));
		//LOGDEBUGKEY(stringJoin("sharedkey:", bufferToHexString((uint8 *)cs->sharedkey->data, CSIIP1_DH_LENGTH / 8)));
		LOGDEBUGKEY(stringJoin("sharedkey:", bignumToHex(cs->sharedkey)));

		cs->insessionkey->size = bignumToBuf(cs->insessionkey->data, cs->sharedkey, CSIIP1_DH_LENGTH / 8);
		//bignumToBuf(cs->outsessionkey->data, cs->sharedkey, DH_PUBLIC_KEY_LENGTH);

		//todo: Consider using a hash to cut down keylength instead.
		for(i = 0; i < cs->insessionkey->size / 2; i++) {
			cs->insessionkey->data[i] ^= cs->insessionkey->data[i + cs->insessionkey->size / 2];
		}
		cs->insessionkey->size = cs->insessionkey->size / 2;
		for(i = 0; i < cs->insessionkey->size / 2; i++) {
			cs->insessionkey->data[i] ^= cs->insessionkey->data[i + cs->insessionkey->size / 2];
		}
		cs->insessionkey->size = cs->insessionkey->size / 2;

		cs->outsessionkey = dblockCopy(cs->insessionkey);

		Blowfish_Init(cs->blowfishin, cs->insessionkey->data, KEY_LENGTH);
		Blowfish_Init(cs->blowfishout, cs->outsessionkey->data, KEY_LENGTH);

		cs->inShake = 0;
		cs->outShake = 0;

		cs->inNeeded = 0;
		cs->cs.ParentPipe->status |= PSTATUS_READY;

		bignumFree(cs->privkey);
		bignumFree(cs->localkey);
		bignumFree(cs->remotekey);
		bignumFree(cs->sharedkey);
		bignumFree(cs->setremotekey);
		cs->privkey = NULL;
		cs->localkey = NULL;
		cs->remotekey = NULL;
		cs->sharedkey = NULL;
		cs->setremotekey = NULL;


	//default:
	}
}

void csiip1OutHandshake(CryptStreamIIP1 *cs) {
	DataBlock *db;
	if(cs->outShake == 1) {
		if(nextdhdelay > 0) {
			return;
		}
		if(cs->cs.noderef != NULL) {
			if(!isStringBlank(cs->cs.noderef->PrivateKey)) {
				cs->privkey = bignumFromHex(cs->cs.noderef->PrivateKey);
			}
			if(!isStringBlank(cs->cs.noderef->PublicKey)) {
				cs->setremotekey = bignumFromHex(cs->cs.noderef->PublicKey);
			}
		}
		if(cs->privkey == NULL) {
			cs->privkey = dhGeneratePrivKey(CSIIP1_DH_PRIV_LENGTH); //dhGetExponentLength(CSIIP1_DH_LENGTH));
		} else {
			LOGDEBUG("csiip1OutHandshake: using premade private key");
		}
		cs->localkey = dhGeneratePubKey(CSIIP1_DH_LENGTH, cs->privkey);
		randomAddEntropyClock(2);
		nextdhdelay = NEXTDHDELAY;

		LOGDEBUG(stringJoinMany("csiip1OutHandshake: ", ptrToString(cs), " made private and local public key", NULL));
		//LOGDEBUGKEY(stringJoin("privkey:", bufferToHexString((uint8 *)cs->privkey->data, CSIIP1_DH_LENGTH / 8)));
		//LOGDEBUGKEY(stringJoin("pubkey:", bufferToHexString((uint8 *)cs->localkey->data, CSIIP1_DH_LENGTH / 8)));
		LOGDEBUGKEY(stringJoin("privkey:", bignumToHex(cs->privkey)));
		LOGDEBUGKEY(stringJoin("pubkey:", bignumToHex(cs->localkey)));
		db = dblockMake(CSIIP1_DH_LENGTH / 8, CSIIP1_DH_LENGTH / 8, "pubkey");
		bignumToBuf(db->data, cs->localkey, CSIIP1_DH_LENGTH / 8);
		cs->cs.ParentPipe->backPipe->outBuffer = dblockAppendBlock(cs->cs.ParentPipe->backPipe->outBuffer, db);
		dblockFree(db);
		cs->outShake = 2;
	}
	
}

void csiip1Read(CryptStreamIIP1 *cs) {
	if(cs->inShake != 0) {
		csiip1InHandshake(cs);
	} else {
		csiip1Decrypt(cs);
	}
}

void csiip1Write(CryptStreamIIP1 *cs) {
	if(cs->outShake != 0) {
		csiip1OutHandshake(cs);
	} else {
		csiip1Encrypt(cs);
	}
}

