/*
*  Naamari - an AES/SHA-based manual file crypto-synchronizer for cloud transfers
*  Copyright (c) 2015-2017 Sunkware
*  Key FP = 6B6D C8E9 3438 6E9C 3D97  56E5 2CE9 A476 99EF 28F6
*
*  This program is free software: you can redistribute it and/or modify
*  it under the terms of the GNU General Public License as published by
*  the Free Software Foundation, either version 3 of the License, or
*  (at your option) any later version.
*
*  This program is distributed in the hope that it will be useful,
*  but WITHOUT ANY WARRANTY; without even the implied warranty of
*  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
*  See the GNU General Public License for more details.
*
*  You should have received a copy of the GNU General Public License
*  along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*  ----------------------------------------------------------------
*  WWW:       sunkware.org
*  E-mail:    sunkware@gmail.com
*  ----------------------------------------------------------------
*/

#include "translator.h"


namespace cryptrav
{


Translator::Translator(ContextGlob* pContext, StateGlob* pState)
{
 contextP = pContext;
 stateP = pState;
}


int            Translator::genRandomen(QString filePath, quint8* randomen)
{
 if (randomen == NULL)
  return CTERR_NULL_POINTER;

 Hasher        hasher;


 hasher.init();

 hasher.update(contextP -> getKeyR(), KEY_SIZE);

 for (int i = 0; i < filePath.length(); i++)
  {
   QChar symbol = filePath.at(i);
   hasher.update( (quint8*)&symbol, 2 );
  }

 qint64 currentEpochTime = stateP -> getEpochTime();
 hasher.update( (quint8*)&currentEpochTime, 8 );

 qint64 counter = stateP -> getCounter();
 hasher.update( (quint8*)&counter, 8 );

 qint64 availMemory = stateP -> getAvailMemory();
 hasher.update( (quint8*)&availMemory, 8 );


 hasher.final(randomen);

 return CTERR_OK;
}

int            Translator::genSalt(quint8* randomen, quint8* salt)
{
 if ( (randomen == NULL) || (salt == NULL) )
  return CTERR_NULL_POINTER;

 Hasher        hasher;


 hasher.init();

 hasher.update(contextP -> getKeyS(), KEY_SIZE);

 hasher.update(randomen, HASH_SIZE);

 qint64 currentEpochTime = stateP -> getEpochTime();
 hasher.update( (quint8*)&currentEpochTime, 8 );

 qint64 counter = stateP -> getCounter();
 hasher.update( (quint8*)&counter, 8 );

 qint64 availMemory = stateP -> getAvailMemory();
 hasher.update( (quint8*)&availMemory, 8 );


 hasher.final(salt);

 return CTERR_OK;
}

int            Translator::makeCryptomen(quint8* randomen, quint8* cryptomen)
{
 if ( (randomen == NULL) || (cryptomen == NULL) )
  return CTERR_NULL_POINTER;

 Hasher      hasher;


 hasher.init();

 hasher.update(contextP -> getKeyC(), KEY_SIZE);

 hasher.update(randomen, HASH_SIZE);

 hasher.final(cryptomen);


 return CTERR_OK;
}

QString        Translator::makeCryptomenStr(quint8* randomen)
{
 QString       cryptomenStr = "";

 if (randomen != NULL)
  {
   quint8        cryptomen[HASH_SIZE];

   makeCryptomen(randomen, cryptomen);

   cryptomenStr = Converter::hexstrFromBin(cryptomen, HASH_SIZE);
   memset(cryptomen, 0, HASH_SIZE); // but local copy of cryptomenStr remains in stack...
  }

 return cryptomenStr;
}


int            Translator::makeBlockKey(quint8* salt, qint64 iBlock, quint8* blockKey) // relies on KEY_SIZE = HASH_SIZE
{
 if ( (salt == NULL) || (blockKey == NULL) )
  return CTERR_NULL_POINTER;

 Hasher        hasher;


 hasher.init();

 hasher.update(contextP -> getKeyB(), KEY_SIZE);

 hasher.update(salt, HASH_SIZE);

 hasher.update( (quint8*)&iBlock, 8 );

 hasher.final(blockKey);


 return CTERR_OK;
}


int            Translator::translateTwoQwords(quint8* salt, qint64* halfFirst, qint64* halfSecond, bool toEncrypt)
{
 if ( (salt == NULL) || (halfFirst == NULL) || (halfSecond == NULL) )
  return CTERR_NULL_POINTER;

 quint8        blockAndKey[KEY_SIZE];

 Cipher        cipher;

 makeBlockKey(salt, 0, blockAndKey);
 cipher.initKey(blockAndKey); // blockAndKey is (deeply) copied inside cipher context

 memcpy(blockAndKey, halfFirst, 8);
 memcpy(blockAndKey + 8, halfSecond, 8); // of course, this relies on KEY_SIZE >= 16...

 if (toEncrypt) {
   cipher.encrypt(blockAndKey);
  } else {
   cipher.decrypt(blockAndKey);
  }

 memcpy(halfFirst, blockAndKey, 8);
 memcpy(halfSecond, blockAndKey + 8, 8);


 memset(blockAndKey, 0, KEY_SIZE);

 return CTERR_OK;
}

int            Translator::encryptTwoQwords(quint8* salt, qint64* halfFirst, qint64* halfSecond)
{
 return translateTwoQwords(salt, halfFirst, halfSecond, true);
}

int            Translator::decryptTwoQwords(quint8* salt, qint64* halfFirst, qint64* halfSecond)
{
 return translateTwoQwords(salt, halfFirst, halfSecond, false);
}


int            Translator::encrypt(QFile* sourceP, QFile* targetP, quint8* salt)
{
 if ( (sourceP == NULL) || (targetP == NULL) || (salt == NULL) )
  return CTERR_NULL_POINTER;

 sourceP -> open(QIODevice::ReadOnly);
 targetP -> open(QIODevice::WriteOnly);

 if ( !( (sourceP -> isOpen()) && (targetP -> isOpen()) ) )
  return CTERR_FILE_NOT_OPEN;

 if ( !( (sourceP ->isReadable()) && (targetP -> isWritable()) ) )
  return CTERR_FILE_WRONG_MODE;


 sourceP -> seek(0);
 targetP -> resize(0);


 qint64        dataSize = sourceP -> size();
 qint8         padSize = (((dataSize + BLOCK_SIZE) >> BLOCK_SIZE_LOG) << BLOCK_SIZE_LOG) - dataSize;
 qint64        numBlocks = (dataSize + padSize) >> BLOCK_SIZE_LOG;


 Hasher        hasher;
 hasher.init();

 hasher.update(contextP -> getKeyH(), KEY_SIZE);


 Cipher        cipher;


 quint8      block[BLOCK_SIZE];
 quint8      blockKey[KEY_SIZE];


 Buffer      bufferSource(sourceP);
 Buffer      bufferTarget(targetP);


 for (qint64 i = 1; i < numBlocks; i++) // last block contains padding bytes...
  {
   bufferSource.read( block, BLOCK_SIZE );

   hasher.update(block, BLOCK_SIZE);

   makeBlockKey(salt, i, blockKey);
   cipher.initKey(blockKey);

   cipher.encrypt(block);

   bufferTarget.write( block, BLOCK_SIZE );
  }


// ...and it's being processed differently, our last block.
// Due to padding algorithm, it is not empty (1 to BLOCK_SIZE bytes).
 memset(block, padSize, BLOCK_SIZE);

 bufferSource.read( block, BLOCK_SIZE - padSize );

 hasher.update(block, BLOCK_SIZE);

 makeBlockKey(salt, numBlocks, blockKey);
 cipher.initKey(blockKey);

 cipher.encrypt(block);

 bufferTarget.write( block, BLOCK_SIZE );

 memset(block, 0, BLOCK_SIZE);


// in the end, hash as checksum... HASH_SIZE = BLOCK_SIZE * 2

 quint8        hash[HASH_SIZE];

 hasher.final(hash);


 makeBlockKey(salt, -(numBlocks + 1), blockKey);
 cipher.initKey(blockKey);

 cipher.encrypt(hash); // 1st half of hash


 makeBlockKey(salt, -(numBlocks + 2), blockKey);
 cipher.initKey(blockKey);

 cipher.encrypt(hash + BLOCK_SIZE); // 2nd half of hash


 bufferTarget.write( hash, HASH_SIZE );


 bufferTarget.flush();


 memset(hash, 0, HASH_SIZE);

 memset(blockKey, 0, KEY_SIZE);


// at this moment, target file length should be a multiple of BLOCK_SIZE

 sourceP -> close();
 targetP -> close();

 return CTERR_OK;
}


int            Translator::decryptPass(QFile* sourceP, QFile* targetP, quint8* salt, bool isWriteEnabled)
{
 if ( (sourceP == NULL) || (salt == NULL) )
  return CTERR_NULL_POINTER;

 if (isWriteEnabled) {
   if (targetP == NULL)
    return CTERR_NULL_POINTER;
  }



 sourceP -> open(QIODevice::ReadOnly);

 if (isWriteEnabled) {
   targetP -> open(QIODevice::WriteOnly);
  }


 if ( !(sourceP -> isOpen()) ) {
   return CTERR_FILE_NOT_OPEN;
  }

 if (isWriteEnabled) {
   if ( !(targetP -> isOpen()) )
    return CTERR_FILE_NOT_OPEN;
  }


 if ( !(sourceP ->isReadable()) ) {
   return CTERR_FILE_WRONG_MODE;
  }

 if (isWriteEnabled) {
   if ( !(targetP -> isWritable()) )
    return CTERR_FILE_WRONG_MODE;
  }


 sourceP -> seek(0);

 if (isWriteEnabled) {
   targetP -> resize(0);
  }


 qint64        dataSize = sourceP -> size() - HASH_SIZE;
 if (dataSize < BLOCK_SIZE) { // even if original file was of length 0, there are BLOCK_SIZE padding bytes
   return CTERR_FILE_WRONG_SIZE;
  }

 if ( (dataSize & (BLOCK_SIZE - 1)) != 0) { // relies on BLOCK_SIZE being a power of 2
   return CTERR_FILE_WRONG_SIZE;
  }


 qint64        numBlocks = dataSize >> BLOCK_SIZE_LOG;


 Hasher        hasher;
 hasher.init();

 hasher.update(contextP -> getKeyH(), KEY_SIZE);


 Cipher        cipher;


 quint8        block[BLOCK_SIZE];
 quint8        blockKey[KEY_SIZE];


 Buffer      bufferSource(sourceP);
 Buffer      bufferTarget(targetP);


 for (qint64 i = 1; i < numBlocks; i++) // last block contains padding bytes
  {
   bufferSource.read( block, BLOCK_SIZE );

   makeBlockKey(salt, i, blockKey);
   cipher.initKey(blockKey);

   cipher.decrypt(block);

   hasher.update(block, BLOCK_SIZE);

   if (isWriteEnabled) {
     bufferTarget.write( block, BLOCK_SIZE );
    }
  }


// last block with padding bytes...
 bufferSource.read( block, BLOCK_SIZE );

 makeBlockKey(salt, numBlocks, blockKey);
 cipher.initKey(blockKey);

 cipher.decrypt(block);

 hasher.update(block, BLOCK_SIZE);


 // Now we compare calculated hash with the one written in file,
 // and reject the whole decrypted data if they aren't equal.

 quint8        calculatedHash[HASH_SIZE];
 quint8        writtenHash[HASH_SIZE];

 hasher.final(calculatedHash);

 bufferSource.read( writtenHash, HASH_SIZE );


 makeBlockKey(salt, -(numBlocks + 1), blockKey);
 cipher.initKey(blockKey);

 cipher.decrypt(writtenHash);


 makeBlockKey(salt, -(numBlocks + 2), blockKey);
 cipher.initKey(blockKey);

 cipher.decrypt(writtenHash + BLOCK_SIZE);


 memset(blockKey, 0, KEY_SIZE); // we won't need it


 bool isHashesEquality = (memcmp(calculatedHash, writtenHash, HASH_SIZE) == 0);

 memset(calculatedHash, 0, HASH_SIZE); // we don't need it after comparison
 memset(writtenHash, 0, HASH_SIZE); // we don't need it after comparison

 if (!isHashesEquality)
  {
   memset(block, 0, BLOCK_SIZE);
   sourceP -> close();

   if (isWriteEnabled) {
// If we always perform this method firstly with write disabled (to verify hashes),
// and only then, if hashes are equal, with actual writing, then
// this branch should never be executed...

     targetP -> remove(); // it's closed before removing
    }

   return CTERR_CHECKSUM_WRONG;
  }


// Last padding byte (as all other padding bytes) is the number of padding bytes

 quint8        padSize = block[BLOCK_SIZE - 1];

 if ( (padSize < 1) || (padSize > BLOCK_SIZE) ) {
   memset(block, 0, BLOCK_SIZE);

   sourceP -> close();

   if (isWriteEnabled) {
     targetP -> remove(); // it's closed before removing
    }

   return CTERR_PADDING_WRONG;
  }

// All padding bytes should be identical

 for (qint8 i = 1; i <= padSize; i++) {
   if (block[BLOCK_SIZE - i] != padSize) {
     memset(block, 0, BLOCK_SIZE);

     sourceP -> close();

     if (isWriteEnabled) {
       targetP -> remove(); // it's closed before removing
      }

     return CTERR_PADDING_WRONG;
    }
  }



 if (isWriteEnabled) {
   bufferTarget.write( block, BLOCK_SIZE - padSize );
  }


 if (isWriteEnabled) {
   bufferTarget.flush(); // there will be no writes (truncation's possible if hashes aren't equal)
  }

 memset(block, 0, BLOCK_SIZE);


 sourceP -> close();

 if (isWriteEnabled) {
   targetP -> close();
  }

 return CTERR_OK;
}

int            Translator::decrypt(QFile* sourceP, QFile* targetP, quint8* salt, bool isOnePass)
{
 if (isOnePass) {
   return decryptPass(sourceP, targetP, salt, true); // write into target without hash check
  }


 int err = decryptPass(sourceP, targetP, salt, false); // check hash only

 if (err) {
   return err;
  }

 return decryptPass(sourceP, targetP, salt, true); // actual writing
}

int            Translator::test(QFile* sourceP, quint8* salt)
{
 return decryptPass(sourceP, NULL, salt, false);
}


Translator::~Translator()
{
 contextP = NULL;
 stateP = NULL;
}


} // namespace "cryptrav"
