/*********************************************************************
######################################################################
##
##  Created by: Denis Filatov
##
##  Copyleft (c) 2015
##  This code is provided under the CeCill-C license agreement.
######################################################################
*********************************************************************/
#define _CRT_SECURE_NO_WARNINGS

#include "ecc_api.h"

#include <openssl/evp.h>
#include <openssl/err.h>
#include <openssl/ec.h>
#include <openssl/pem.h>
#include <openssl/sha.h>
#include <openssl/ecdsa.h>
#include <string.h>

#define FIELD_SIZE 32

#define ARRAYSIZE(A) (sizeof(A)/sizeof(A[0]))

static char * _bin2hex(char * hex, int hlen, const char * bin, int blen);

typedef struct {
	int nid;
	const char * name;
	int  fsize;
}ecc_api_curve_data;

static ecc_api_curve_data _config[] = {
	{
		NID_X9_62_prime256v1, SN_X9_62_prime256v1, 32
	},
	{
		NID_brainpoolP256r1, SN_brainpoolP256r1, 32
	},
	{
		NID_brainpoolP384r1, SN_brainpoolP384r1, 48
	},
};

static EC_GROUP * _curves[ARRAYSIZE(_config)] = { NULL };


int ecc_api_init()
{
	int i;
	for (i = 0; i < ARRAYSIZE(_config); i++){
		_curves[i] = EC_GROUP_new_by_curve_name(_config[i].nid);
	}
	return 0;
}

int ecc_api_done()
{
	int i;
	for (i = 0; i < ARRAYSIZE(_config); i++){
		EC_GROUP_free(_curves[i]);
	}
	return 0;
}

void * ecc_key_gen(ecc_curve_id pk_alg)
{
	EC_KEY * key = NULL;
	if (_curves[pk_alg]){
		key = EC_KEY_new();
		if (key){
			EC_KEY_set_group(key, _curves[pk_alg]);
			EC_KEY_set_asn1_flag(key, OPENSSL_EC_NAMED_CURVE);
			if (!EC_KEY_generate_key(key)){
				ERR_print_errors_fp(stderr);
				fflush(stderr);
				EC_KEY_free(key);
				key = NULL;
			}
		}
	}
	return key;
}

void * ecc_key_init(ecc_curve_id pk_alg, const char* buf)
{
	EC_KEY * eckey = NULL;
	BIGNUM * bn = BN_new();
	const EC_GROUP * group = _curves[pk_alg];
	int fsize = (EC_GROUP_get_degree(group) + 7) / 8;
	if (group){
		if (BN_bin2bn((const unsigned char*)buf, fsize, bn)){
			eckey = EC_KEY_new();
			if (eckey){
				if (EC_KEY_set_group(eckey, group)){
					if (EC_KEY_set_private_key(eckey, bn)){
						EC_POINT * point;
						point = EC_POINT_new(group);
						if (EC_POINT_mul(group, point, bn, NULL, NULL, NULL)){
							EC_KEY_set_public_key(eckey, point);
						}
						EC_POINT_free(point);
					}
				}
			}
		}
	}
	BN_free(bn);
	return (void*)eckey;
}

void   ecc_key_free(void* key)
{
	EC_KEY_free(key);
}

int ecc_key_private(void* key, char* buf)
{
	int len = -1;
	if (key){
		const BIGNUM   * bn;
		bn = EC_KEY_get0_private_key(key);
		if (bn){
			len = BN_num_bytes(bn);
			if (buf){
				BN_bn2bin(bn, (unsigned char*)buf);
			}
		}
	}
	return len;
}

int    ecc_key_public(void* key, char * px, char * py, int * psign)
{
	const EC_GROUP * ecgroup;
	const EC_POINT * ecpoint;
	const EC_KEY   * eckey = (EC_KEY*)key;
	BIGNUM x, y;
	int fsize, bcount = -1;

	if( key && px && py ) {
		ecgroup = EC_KEY_get0_group(eckey);
		ecpoint = EC_KEY_get0_public_key(eckey);
		fsize = (EC_GROUP_get_degree(ecgroup) + 7) / 8;

		//fill public key data
		BN_init(&x); BN_init(&y);
		if (EC_POINT_get_affine_coordinates_GFp(ecgroup, ecpoint, &x, &y, NULL)){
			bcount = BN_num_bytes(&x);
			for(; bcount < fsize; bcount++)
				*(px++) = 0; // add padding with zeros
			BN_bn2bin(&x, (unsigned char*)px);
	
			bcount = BN_num_bytes(&y);
			for(; bcount < fsize; bcount++)
				*(py++) = 0; // add padding with zeros
			BN_bn2bin(&y, (unsigned char*)py);
			if (psign) * psign = BN_is_odd(&y);
		}
		BN_clear_free(&x); BN_clear_free(&y);
	}
	return bcount;
}

static int _pass_cb(char *buf, int size, int rwflag, void *u)
{
	fprintf(stderr, "Ask for a pass phrase");
	return 0;
}

int    ecc_key_private_save(void* key, const char* path, ecc_format format)
{
	int rc = -1;
	EC_KEY   * eckey = (EC_KEY *)key;
	if (eckey){
		FILE * f = fopen(path, "wb");
		if (f){
			if (format == ecc_pem){
				rc = PEM_write_ECPrivateKey(f, eckey, NULL, NULL, 0, _pass_cb, NULL) ? 0 : -1;
				if (rc < 0){
					ERR_print_errors_fp(stderr);
				}
			}
			else{
				const EC_GROUP * ecgroup = EC_KEY_get0_group(eckey);
				int fsize = (EC_GROUP_get_degree(ecgroup) + 7) / 8;
				const BIGNUM   * ecbn;
				ecbn = EC_KEY_get0_private_key(eckey);
				if (ecbn){
					int bnlen = BN_num_bytes(ecbn);
					int len = (bnlen < fsize) ? fsize : bnlen;
					char * buf = (char *)OPENSSL_malloc(len*2+1);
					if (bnlen < len) memset(buf, 0, len - bnlen);
					BN_bn2bin(ecbn, (unsigned char *)(buf + len - bnlen));
					if (format == ecc_hex){
						char * c = _bin2hex(buf, len * 2 + 1, buf, len);
						*c = 0;
						len = c - buf;
						rc = 0;
					}
					rc = (len == fwrite(buf, 1, len, f)) ? 0 : -1;
					OPENSSL_free(buf);
				}
			}
			fclose(f);
			if (rc < 0){
				ERR_print_errors_fp(stderr);
				remove(path);
				rc = -1;
			}
		}
		else{
			perror(path);
		}
	}
	return rc;
}

void * ecc_key_private_load(const char* path, ecc_curve_id pk_alg)
{
	EC_KEY * eckey = NULL;
	FILE * f = fopen(path, "rb");
	if (f){
		eckey = PEM_read_ECPrivateKey(f, NULL, NULL, NULL);
		if (eckey == NULL){
			BIGNUM * bn = NULL;
			fseek(f, 0, SEEK_END);
			int len = ftell(f);
			fseek(f, 0, SEEK_SET);
			char * buf = OPENSSL_malloc(len + 1);
			if (len == fread(buf, 1, len, f)){
				buf[len] = 0;
				// try hex first
				if (len != BN_hex2bn(&bn, buf)){
					if (bn){
						BN_free(bn); bn = NULL;
					}
					bn = BN_bin2bn((const unsigned char*)buf, len, NULL);
				}
			}
			OPENSSL_free(buf);
			if (bn){
				eckey = EC_KEY_new();
				if (eckey){
					EC_KEY_set_group(eckey, _curves[pk_alg]);
					if (EC_KEY_set_private_key(eckey, bn)){
						EC_POINT * point;
						const EC_GROUP * group;
						group = EC_KEY_get0_group(eckey);
						point = EC_POINT_new(group);
						if (EC_POINT_mul(group, point, bn, NULL, NULL, NULL)){
							EC_KEY_set_public_key(eckey, point);
						}
						EC_POINT_free(point);
					}
					else{
						EC_KEY_free(eckey); eckey = NULL;
					}
				}
				BN_free(bn);
			}
		}
		fclose(f);
	}
	return eckey;
}

int ecc_key_public_save(void* key, const char* path, ecc_format format)
{
	EC_KEY   * eckey = (EC_KEY *)key;
	int rc = -1;
	if (eckey){
		FILE * f = fopen(path, "wb");
		if (f){
			if (format == ecc_pem){
				rc = PEM_write_EC_PUBKEY(f, eckey) ? 0 : -1;
			}
			else{
				size_t len;
				char * buf = NULL;
				const EC_POINT * point = EC_KEY_get0_public_key(eckey);
				const EC_GROUP * group = EC_KEY_get0_group(eckey);

				if (format == ecc_hex){
					buf = EC_POINT_point2hex(group, point, POINT_CONVERSION_UNCOMPRESSED, NULL);
					len = strlen(buf);
				}
				else if (format == ecc_bin){
					len = EC_POINT_point2oct(group, point, POINT_CONVERSION_UNCOMPRESSED, NULL, 0, NULL);
					if (len > 0){
						buf = OPENSSL_malloc(len + 1);
						if (len != EC_POINT_point2oct(group, point, POINT_CONVERSION_UNCOMPRESSED, (unsigned char*)buf, len, NULL)){
							OPENSSL_free(buf); buf = NULL;
						}
					}
				}
				if (buf){
					if (len == fwrite(buf, 1, len, f)){
						rc = 0;
					}
					OPENSSL_free(buf); buf = NULL;
				}
			}
			fclose(f);
			if (rc < 0){
				ERR_print_errors_fp(stderr);
				remove(path);
			}
		}
		else{
			perror(path);
		}
	}
	return rc;
}

void * ecc_key_public_set(ecc_curve_id pk_alg, ecc_point_type ptype, const char * px, const char * py)
{
	EC_KEY * eckey = NULL;
	EC_POINT * point = NULL;
	const EC_GROUP * group;
	BIGNUM *x, *y = NULL;
	int rc = 0;
	int fsize;

	group = _curves[pk_alg];
	point = EC_POINT_new(group);
	fsize = (EC_GROUP_get_degree(group) + 7) / 8;
	x = BN_bin2bn((const unsigned char*)px, fsize, NULL);
	if (x){
		if (ptype == ecc_uncompressed){
			y = BN_bin2bn((const unsigned char*)py, fsize, NULL);
			if (y){
				rc = EC_POINT_set_affine_coordinates_GFp(group, point, x, y, NULL);
				BN_clear_free(y);
			}
		}
		else {
			rc = EC_POINT_set_compressed_coordinates_GFp(group, point, x, ptype & 1, NULL);
		}
		BN_clear_free(x);
		if (rc){
			eckey = EC_KEY_new();
			if (eckey){
				EC_KEY_set_group(eckey, group);
				EC_KEY_set_public_key(eckey, point);
			}
		}
	}
	EC_POINT_free(point);
	return eckey;
}

void * ecc_key_public_load(const char* path, ecc_curve_id pk_alg)
{
	EC_KEY * eckey = NULL;
	FILE * f = fopen(path, "rb");
	EC_POINT * point = NULL;
	const EC_GROUP * group = _curves[0];
	if (f){
		eckey = PEM_read_EC_PUBKEY(f, &eckey, NULL, NULL);
		if (eckey == NULL){
			fseek(f, 0, SEEK_END);
			int len = ftell(f);
			fseek(f, 0, SEEK_SET);
			char * buf = OPENSSL_malloc(len + 1);
			if (len == fread(buf, 1, len, f)){
				buf[len] = 0;
				// try hex first
				point = EC_POINT_hex2point(group, buf, NULL, NULL);
				if (point == NULL){
					// try oct
					point = EC_POINT_new(group);
					if (!EC_POINT_oct2point(group, point, (const unsigned char*)buf, len, NULL)){
						EC_POINT_free(point);
						point = NULL;
					}
				}
				if (point){
					eckey = EC_KEY_new();
					if (eckey){
						EC_KEY_set_group(eckey, group);
						EC_KEY_set_public_key(eckey, point);
					}
					EC_POINT_free(point);
				}
			}
			OPENSSL_free(buf);
		}
		fclose(f);
	}
	return eckey;
}

int sha256_calculate(char* hash, const char * ptr, int len)
{
	SHA256_CTX ctx;
	SHA256_Init(&ctx);
	SHA256_Update(&ctx, ptr, len);
	SHA256_Final((unsigned char*)hash, &ctx);
	return 0;
}

int sha384_calculate(char* hash, const char * ptr, int len)
{
	SHA512_CTX ctx;
	SHA384_Init(&ctx);
	SHA384_Update(&ctx, ptr, len);
	SHA384_Final((unsigned char*)hash, &ctx);
	return 0;
}

int    ecc_sign(void * key, const char * hash, int hlength, char *r, char *s)
{
	EC_KEY   * eckey;

	eckey = EC_KEY_dup((EC_KEY*)key);
	if(eckey){
		ECDSA_SIG * ecdsa;
		int fsize = (EC_GROUP_get_degree(EC_KEY_get0_group(eckey)) + 7) / 8;
		ecdsa = ECDSA_do_sign(hash, hlength, eckey);
		EC_KEY_free(eckey);
		if (ecdsa){
			int i, bcount;
			bcount = BN_num_bytes(ecdsa->r);
			for(i=bcount; i < fsize; i++)
				*(r++) = 0; // add padding with zeros
			BN_bn2bin(ecdsa->r, r);
			bcount = BN_num_bytes(ecdsa->s);
			for(i=bcount; i < fsize; i++)
				*(s++) = 0; // add padding with zeros
			BN_bn2bin(ecdsa->s, s);
			ECDSA_SIG_free(ecdsa);
			return 0;
		}
	}
	return -1;
}


static const char* _hexDigits = "0123456789ABCDEF";
static char * _bin2hex(char * hex, int hlen, const char * bin, int blen)
{
	const unsigned char *b, *e;
	char * s;

	// sanity check
	if (hlen >= 0 && hlen < blen * 2) return NULL;

	b = (const unsigned char *)bin;
	e = b + blen - 1;
	s = hex + blen * 2;
	if (s < hex + hlen) *s = 0;
	for (; b <= e; e--){
		*(--s) = _hexDigits[(*e) & 0xF];
		*(--s) = _hexDigits[(*e) >> 4];
	}
	return hex + blen * 2;
}
