/*
 * Copyright (c) 2010 SURFnet bv
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
 * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
 * IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
 * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
 * IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

// TODO: Store EVP context in securely allocated memory

/*****************************************************************************
 OSSLEVPSymmetricAlgorithm.cpp

 OpenSSL symmetric algorithm implementation
 *****************************************************************************/

#include "config.h"
#include "OSSLEVPSymmetricAlgorithm.h"
#include "OSSLUtil.h"
#include "salloc.h"
#include <openssl/err.h>

// Constructor
OSSLEVPSymmetricAlgorithm::OSSLEVPSymmetricAlgorithm()
{
	pCurCTX = NULL;
	maximumBytes = NULL;
	counterBytes = NULL;
}

// Destructor
OSSLEVPSymmetricAlgorithm::~OSSLEVPSymmetricAlgorithm()
{
	/* In case of custom allocator from BIND, even NULL pointer might crash
	 * the library if freed after deinitialization. Do not rely on null handling
	 * as a workaround. */
	if (pCurCTX)
		EVP_CIPHER_CTX_free(pCurCTX);
	if (maximumBytes)
		BN_free(maximumBytes);
	if (counterBytes)
		BN_free(counterBytes);
}

void OSSLEVPSymmetricAlgorithm::counterBitsInit(const ByteString& iv, size_t counterBits)
{
	BN_free(maximumBytes);
	maximumBytes = NULL;
	BN_free(counterBytes);
	counterBytes = NULL;

	// Check the counter bits
	if (counterBits > 0)
	{
		BIGNUM* counter = OSSL::byteString2bn(iv);
		BN_mask_bits(counter, counterBits);

		// Reverse the bits
		while (counterBits > 0)
		{
			counterBits--;
			if (BN_is_bit_set(counter, counterBits))
			{
				BN_clear_bit(counter, counterBits);
			}
			else
			{
				BN_set_bit(counter, counterBits);
			}
		}

		// Set the maximum bytes
		BN_add_word(counter, 1);
		BN_mul_word(counter, getBlockSize());
		maximumBytes = counter;
		counterBytes = BN_new();
		BN_zero(counterBytes);
	}
}

void OSSLEVPSymmetricAlgorithm::clean()
{
		EVP_CIPHER_CTX_free(pCurCTX);
		pCurCTX = NULL;
		BN_free(maximumBytes);
		maximumBytes = NULL;
		BN_free(counterBytes);
		counterBytes = NULL;
}

// Encryption functions
bool OSSLEVPSymmetricAlgorithm::encryptInit(const SymmetricKey* key, const SymMode::Type mode /* = SymMode::CBC */, const ByteString& IV /* = ByteString()*/, bool padding /* = true */, size_t counterBits /* = 0 */, const ByteString& aad /* = ByteString() */, size_t tagBytes /* = 0 */)
{
	// Call the superclass initialiser
	if (!SymmetricAlgorithm::encryptInit(key, mode, IV, padding, counterBits, aad, tagBytes))
	{
		return false;
	}

	// Check the IV
	if (mode != SymMode::GCM && (IV.size() > 0) && (IV.size() != getBlockSize()))
	{
		ERROR_MSG("Invalid IV size (%d bytes, expected %d bytes)", IV.size(), getBlockSize());

		ByteString dummy;
		SymmetricAlgorithm::encryptFinal(dummy);

		return false;
	}

	ByteString iv;

	if (IV.size() > 0)
	{
		iv = IV;
	}
	else
	{
		iv.wipe(getBlockSize());
	}

	counterBitsInit(iv, counterBits);

	// Determine the cipher class
	const EVP_CIPHER* cipher = getCipher();

	if (cipher == NULL)
	{
		ERROR_MSG("Failed to initialise EVP encrypt operation");

		ByteString dummy;
		SymmetricAlgorithm::encryptFinal(dummy);

		return false;
	}

	// Allocate the EVP context
	pCurCTX = EVP_CIPHER_CTX_new();

	if (pCurCTX == NULL)
	{
		ERROR_MSG("Failed to allocate space for EVP_CIPHER_CTX");

		ByteString dummy;
		SymmetricAlgorithm::encryptFinal(dummy);

		return false;
	}

	int rv;
	if (mode == SymMode::GCM)
	{
		rv = EVP_EncryptInit_ex(pCurCTX, cipher, NULL, NULL, NULL);

		if (rv)
		{
			EVP_CIPHER_CTX_ctrl(pCurCTX, EVP_CTRL_GCM_SET_IVLEN, iv.size(), NULL);
			rv = EVP_EncryptInit_ex(pCurCTX, NULL, NULL, (unsigned char*) currentKey->getKeyBits().const_byte_str(), iv.byte_str());
		}
	}
	else
	{
		rv = EVP_EncryptInit(pCurCTX, cipher, (unsigned char*) currentKey->getKeyBits().const_byte_str(), iv.byte_str());
	}

	if (!rv)
	{
		ERROR_MSG("Failed to initialise EVP encrypt operation: %s", ERR_error_string(ERR_get_error(), NULL));

		clean();

		ByteString dummy;
		SymmetricAlgorithm::encryptFinal(dummy);

		return false;
	}

	EVP_CIPHER_CTX_set_padding(pCurCTX, padding ? 1 : 0);

	if (mode == SymMode::GCM)
	{
		int outLen = 0;
		if (aad.size() && !EVP_EncryptUpdate(pCurCTX, NULL, &outLen, (unsigned char*) aad.const_byte_str(), aad.size()))
		{
			ERROR_MSG("Failed to update with AAD: %s", ERR_error_string(ERR_get_error(), NULL));

			clean();

			ByteString dummy;
			SymmetricAlgorithm::encryptFinal(dummy);

			return false;
		}
	}

	return true;
}

bool OSSLEVPSymmetricAlgorithm::encryptUpdate(const ByteString& data, ByteString& encryptedData)
{
	if (!SymmetricAlgorithm::encryptUpdate(data, encryptedData))
	{
		clean();
		return false;
	}

	if (data.size() == 0)
	{
		encryptedData.resize(0);

		return true;
	}

	// Count number of bytes written
	if (maximumBytes)
	{
		BN_add_word(counterBytes, data.size());
	}

	// Prepare the output block
	encryptedData.resize(data.size() + getBlockSize() - 1);

	int outLen = encryptedData.size();
	if (!EVP_EncryptUpdate(pCurCTX, &encryptedData[0], &outLen, (unsigned char*) data.const_byte_str(), data.size()))
	{
		ERROR_MSG("EVP_EncryptUpdate failed: %s", ERR_error_string(ERR_get_error(), NULL));

		clean();

		ByteString dummy;
		SymmetricAlgorithm::encryptFinal(dummy);

		return false;
	}

	// Resize the output block
	encryptedData.resize(outLen);
	currentBufferSize -= outLen;

	return true;
}

bool OSSLEVPSymmetricAlgorithm::encryptFinal(ByteString& encryptedData)
{
	SymMode::Type mode = currentCipherMode;
	size_t tagBytes = currentTagBytes;

	if (!SymmetricAlgorithm::encryptFinal(encryptedData))
	{
		clean();
		return false;
	}

	// Prepare the output block
	encryptedData.resize(getBlockSize());

	int outLen = encryptedData.size();

	if (!EVP_EncryptFinal(pCurCTX, &encryptedData[0], &outLen))
	{
		ERROR_MSG("EVP_EncryptFinal failed: %s", ERR_error_string(ERR_get_error(), NULL));

		clean();

		return false;
	}

	// Resize the output block
	encryptedData.resize(outLen);

	if (mode == SymMode::GCM)
	{
		ByteString tag;
		tag.resize(tagBytes);
		EVP_CIPHER_CTX_ctrl(pCurCTX, EVP_CTRL_GCM_GET_TAG, tagBytes, &tag[0]);
		encryptedData += tag;
	}

	clean();

	return true;
}

// Decryption functions
bool OSSLEVPSymmetricAlgorithm::decryptInit(const SymmetricKey* key, const SymMode::Type mode /* = SymMode::CBC */, const ByteString& IV /* = ByteString() */, bool padding /* = true */, size_t counterBits /* = 0 */, const ByteString& aad /* = ByteString() */, size_t tagBytes /* = 0 */)
{
	// Call the superclass initialiser
	if (!SymmetricAlgorithm::decryptInit(key, mode, IV, padding, counterBits, aad, tagBytes))
	{
		return false;
	}

	// Check the IV
	if (mode != SymMode::GCM && (IV.size() > 0) && (IV.size() != getBlockSize()))
	{
		ERROR_MSG("Invalid IV size (%d bytes, expected %d bytes)", IV.size(), getBlockSize());

		ByteString dummy;
		SymmetricAlgorithm::decryptFinal(dummy);

		return false;
	}

	ByteString iv;

	if (IV.size() > 0)
	{
		iv = IV;
	}
	else
	{
		iv.wipe(getBlockSize());
	}

	counterBitsInit(iv, counterBits);

	// Determine the cipher class
	const EVP_CIPHER* cipher = getCipher();

	if (cipher == NULL)
	{
		ERROR_MSG("Failed to initialise EVP decrypt operation");

		ByteString dummy;
		SymmetricAlgorithm::decryptFinal(dummy);

		return false;
	}

	// Allocate the EVP context
	pCurCTX = EVP_CIPHER_CTX_new();

	if (pCurCTX == NULL)
	{
		ERROR_MSG("Failed to allocate space for EVP_CIPHER_CTX");

		ByteString dummy;
		SymmetricAlgorithm::decryptFinal(dummy);

		return false;
	}

	int rv;
	if (mode == SymMode::GCM)
	{
		rv = EVP_DecryptInit_ex(pCurCTX, cipher, NULL, NULL, NULL);

		if (rv)
		{
			EVP_CIPHER_CTX_ctrl(pCurCTX, EVP_CTRL_GCM_SET_IVLEN, iv.size(), NULL);
			rv = EVP_DecryptInit_ex(pCurCTX, NULL, NULL, (unsigned char*) currentKey->getKeyBits().const_byte_str(), iv.byte_str());
		}
	}
	else
	{
		rv = EVP_DecryptInit(pCurCTX, cipher, (unsigned char*) currentKey->getKeyBits().const_byte_str(), iv.byte_str());
	}

	if (!rv)
	{
		ERROR_MSG("Failed to initialise EVP decrypt operation: %s", ERR_error_string(ERR_get_error(), NULL));

		clean();

		ByteString dummy;
		SymmetricAlgorithm::decryptFinal(dummy);

		return false;
	}

	EVP_CIPHER_CTX_set_padding(pCurCTX, padding ? 1 : 0);

	if (mode == SymMode::GCM)
	{
		int outLen = 0;
		if (aad.size() && !EVP_DecryptUpdate(pCurCTX, NULL, &outLen, (unsigned char*) aad.const_byte_str(), aad.size()))
		{
			ERROR_MSG("Failed to update with AAD: %s", ERR_error_string(ERR_get_error(), NULL));

			clean();

			ByteString dummy;
			SymmetricAlgorithm::decryptFinal(dummy);

			return false;
		}
	}

	return true;
}

bool OSSLEVPSymmetricAlgorithm::decryptUpdate(const ByteString& encryptedData, ByteString& data)
{
	if (!SymmetricAlgorithm::decryptUpdate(encryptedData, data))
	{
		clean();
		return false;
	}

	// AEAD ciphers should not return decrypted data until final is called
	if (currentCipherMode == SymMode::GCM)
	{
		data.resize(0);
		return true;
	}

	// Count number of bytes written
	if (maximumBytes)
	{
		BN_add_word(counterBytes, encryptedData.size());
	}

	// Prepare the output block
	data.resize(encryptedData.size() + getBlockSize());

	int outLen = data.size();

	DEBUG_MSG("Decrypting %d bytes into buffer of %d bytes", encryptedData.size(), data.size());

	if (!EVP_DecryptUpdate(pCurCTX, &data[0], &outLen, (unsigned char*) encryptedData.const_byte_str(), encryptedData.size()))
	{
		ERROR_MSG("EVP_DecryptUpdate failed: %s", ERR_error_string(ERR_get_error(), NULL));

		clean();

		ByteString dummy;
		SymmetricAlgorithm::decryptFinal(dummy);

		return false;
	}

	DEBUG_MSG("Decrypt returned %d bytes of data", outLen);

	// Resize the output block
	data.resize(outLen);
	currentBufferSize -= outLen;

	return true;
}

bool OSSLEVPSymmetricAlgorithm::decryptFinal(ByteString& data)
{
	SymMode::Type mode = currentCipherMode;
	size_t tagBytes = currentTagBytes;
	ByteString aeadBuffer = currentAEADBuffer;

	if (!SymmetricAlgorithm::decryptFinal(data))
	{
		clean();
		return false;
	}

	data.resize(0);
	if (mode == SymMode::GCM)
	{
		// Check buffer size
		if (aeadBuffer.size() < tagBytes)
		{
			ERROR_MSG("Tag bytes (%d) does not fit in AEAD buffer (%d)", tagBytes, aeadBuffer.size());

			clean();

			return false;
		}

		// Set the tag
		EVP_CIPHER_CTX_ctrl(pCurCTX, EVP_CTRL_GCM_SET_TAG, tagBytes, &aeadBuffer[aeadBuffer.size()-tagBytes]);

		// Prepare the output block
		data.resize(aeadBuffer.size() - tagBytes + getBlockSize());
		int outLen = data.size();

		if (!EVP_DecryptUpdate(pCurCTX, &data[0], &outLen, (unsigned char*) aeadBuffer.const_byte_str(), aeadBuffer.size() - tagBytes))
		{
			ERROR_MSG("EVP_DecryptUpdate failed: %s", ERR_error_string(ERR_get_error(), NULL));

			clean();

			return false;
		}

		data.resize(outLen);
	}

	// Prepare the output block
	int initialSize = data.size();
	data.resize(initialSize + getBlockSize());

	int outLen = data.size() - initialSize;
	int rv;

	if (!(rv = EVP_DecryptFinal(pCurCTX, &data[initialSize], &outLen)))
	{
		ERROR_MSG("EVP_DecryptFinal failed (0x%08X): %s", rv, ERR_error_string(ERR_get_error(), NULL));

		clean();

		return false;
	}

	// Resize the output block
	data.resize(initialSize + outLen);

	clean();

	return true;
}

// Check if more bytes of data can be encrypted
bool OSSLEVPSymmetricAlgorithm::checkMaximumBytes(unsigned long bytes)
{
	if (maximumBytes == NULL) return true;

	BIGNUM* bigNum = BN_new();
	BN_copy(bigNum, counterBytes);
	BN_add_word(bigNum, bytes);

	bool rv = false;
	if (BN_cmp(maximumBytes, bigNum) >= 0) rv = true;

	BN_free(bigNum);

	return rv;
}
