/*
 * Copyright (c) 2004 Beeyond Software Holding BV
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#ifdef HAVE_CONFIG_H
# include "config.h"
#endif

#include "beecrypt/beecrypt.h"
#include "beecrypt/aes.h"
#include "beecrypt/blockmode.h"
#include "beecrypt/blockpad.h"
#include "beecrypt/hmacsha256.h"
#include "beecrypt/pkcs12.h"
#include "beecrypt/sha256.h"
#include "beecrypt/c++/provider/KeyProtector.h"
#include "beecrypt/c++/beeyond/AnyEncodedKeySpec.h"
using beecrypt::beeyond::AnyEncodedKeySpec;
#include "beecrypt/c++/crypto/BadPaddingException.h"
using beecrypt::crypto::BadPaddingException;
#include "beecrypt/c++/io/ByteArrayInputStream.h"
using beecrypt::io::ByteArrayInputStream;
#include "beecrypt/c++/io/ByteArrayOutputStream.h"
using beecrypt::io::ByteArrayOutputStream;
#include "beecrypt/c++/io/DataInputStream.h"
using beecrypt::io::DataInputStream;
#include "beecrypt/c++/io/DataOutputStream.h"
using beecrypt::io::DataOutputStream;
#include "beecrypt/c++/lang/NullPointerException.h"
using beecrypt::lang::NullPointerException;
#include "beecrypt/c++/security/KeyFactory.h"
using beecrypt::security::KeyFactory;

#include <memory>
using std::auto_ptr;

using namespace beecrypt::provider;

namespace {
	/* eventually these will be moved to a different location */
	void pkcs5_pad(int blockbytes, bytearray& b)
	{
		int unpadded_size = b.size();

		byte padvalue = blockbytes - (unpadded_size % blockbytes);

		b.resize(unpadded_size + padvalue);

		memset(b.data() + unpadded_size, padvalue, padvalue);
	}

	void pkcs5_unpad(int blockbytes, bytearray& b) throw (BadPaddingException)
	{
		byte padvalue = b[b.size() - 1];

		if (padvalue > blockbytes)
			throw BadPaddingException();

		for (int i = (b.size() - padvalue); i < (b.size() - 1); i++)
			if (b[i] != padvalue)
				throw BadPaddingException();

		b.resize(b.size() - padvalue);
	}
}

KeyProtector::KeyProtector(PBEKey& key) throw (InvalidKeyException)
{
	bytearray _rawk, _salt;
	int _iter;

	if (key.getEncoded())
		_rawk = *(key.getEncoded());
	else
		throw InvalidKeyException("PBEKey must have an encoding");

	if (key.getSalt())
		_salt = *(key.getSalt());

	_iter = key.getIterationCount();

	if (pkcs12_derive_key(&sha256, PKCS12_ID_CIPHER, _rawk.data(), _rawk.size(), _salt.data(), _salt.size(), _iter, _cipher_key, 32))
        throw InvalidKeyException("pkcs12_derive_key returned error");

	if (pkcs12_derive_key(&sha256, PKCS12_ID_MAC, _rawk.data(), _rawk.size(), _salt.data(), _salt.size(), _iter, _mac_key, 32))
        throw InvalidKeyException("pkcs12_derive_key returned error");

	if (pkcs12_derive_key(&sha256, PKCS12_ID_IV, _rawk.data(), _rawk.size(), _salt.data(), _salt.size(), _iter, _iv, 16))
        throw InvalidKeyException("pkcs12_derive_key returned error");
}

KeyProtector::~KeyProtector() throw ()
{
	// wipe everything
	memset(_cipher_key, 0, 32);
	memset(_mac_key, 0, 32);
	memset(_iv, 0, 16);
}

bytearray* KeyProtector::protect(const PrivateKey& pri) throw ()
{
	if (!pri.getEncoded())
		return 0;

	if (!pri.getFormat())
		return 0;

	/* Eventually we'll substitute this with the following construction:
	 * DataOutputStream(CipherOutputStream(ByteArrayOutputStream)))
	 */
	ByteArrayOutputStream bos;
	DataOutputStream dos(bos);

	try
	{
		const bytearray* encoded_key = pri.getEncoded();

		dos.writeUTF(pri.getAlgorithm());
		dos.writeUTF(*pri.getFormat());
		dos.writeInt(encoded_key->size());
		dos.write(*encoded_key);
		dos.close();

		bytearray cleartext, ciphertext, mac(hmacsha256.digestsize);	
		bos.toByteArray(cleartext);

		// Compute the MAC before padding
		keyedHashFunctionContext mc(&hmacsha256);
		keyedHashFunctionContextSetup(&mc, _mac_key, 256);
		keyedHashFunctionContextUpdate(&mc, cleartext.data(), cleartext.size());
		keyedHashFunctionContextDigest(&mc, mac.data());

		// Pad the cleartext
		pkcs5_pad(aes.blocksize, cleartext);

		// Set the ciphertext size equal to the cleartext size
		ciphertext.resize(cleartext.size());

		// Encrypt the cleartext
		blockCipherContext bc(&aes);
		blockCipherContextSetup(&bc, _cipher_key, 256, ENCRYPT);
		blockCipherContextSetIV(&bc, _iv);
		blockCipherContextCBC(&bc, (uint32_t*) ciphertext.data(), (const uint32_t*) cleartext.data(), cleartext.size() / 16);

		// Return the concatenation of the two bytearrays
		return new bytearray(ciphertext + mac);
	}
	catch (IOException&)
	{
	}
	
	return 0;
}

PrivateKey* KeyProtector::recover(const byte* data, int size) throw (NoSuchAlgorithmException, UnrecoverableKeyException)
{
	// If we don't have at least enough data for the digest then bail out
	if (size <= hmacsha256.digestsize)
		throw UnrecoverableKeyException("encrypted key data way too short");

	int ciphertext_size = size - hmacsha256.digestsize;

	// Check if we have a whole number of blocks in the data
	if ((ciphertext_size % aes.blocksize) != 0)
		throw UnrecoverableKeyException("encrypted key data is not a whole number of blocks");

	bytearray ciphertext(data, ciphertext_size), cleartext(ciphertext_size);

	// Decrypt the ciphertext
	blockCipherContext bc(&aes);
	blockCipherContextSetup(&bc, _cipher_key, 256, DECRYPT);
	blockCipherContextSetIV(&bc, _iv);
	blockCipherContextCBC(&bc, (uint32_t*) cleartext.data(), (const uint32_t*) ciphertext.data(), ciphertext_size / 16);

	try
	{
		pkcs5_unpad(aes.blocksize, cleartext);
	}
	catch (BadPaddingException&)
	{
		// Corrupted data, most likely due to bad password
		throw UnrecoverableKeyException("bad padding");
	}

	bytearray mac(hmacsha256.digestsize);

	// Verify the MAC before recovering the key
	keyedHashFunctionContext mc(&hmacsha256);
	keyedHashFunctionContextSetup(&mc, _mac_key, 256);
	keyedHashFunctionContextUpdate(&mc, cleartext.data(), cleartext.size());
	keyedHashFunctionContextDigest(&mc, mac.data());

	// Compare the two MACs and bail out if they're different
	if (memcmp(data + ciphertext_size, mac.data(), hmacsha256.digestsize))
		throw UnrecoverableKeyException("mac of decrypted key didn't match");

	// Now we're sure the password was correct, and we have the decrypted data

	ByteArrayInputStream bis(cleartext);
	DataInputStream dis(bis);

	try
	{
		String algorithm = dis.readUTF();
		String format = dis.readUTF();

		jint encsize = dis.readInt();
		if (encsize <= 0)
			throw IOException();

		bytearray enc(encsize);

		dis.readFully(enc);

		AnyEncodedKeySpec spec(format, enc);

		try
		{
			auto_ptr<KeyFactory> kf(KeyFactory::getInstance(algorithm));

			return kf->generatePrivate(spec);
		}
		catch (InvalidKeySpecException&)
		{
		}
		catch (NoSuchAlgorithmException&)
		{
		}
	}
	catch (IOException&)
	{
	}
	throw UnrecoverableKeyException("parsing error in decrypted key");
}

PrivateKey* KeyProtector::recover(const bytearray& b) throw (NoSuchAlgorithmException, UnrecoverableKeyException)
{
	return recover(b.data(), b.size());
}
