/*
  Copyright (c) 2008-2012 Red Hat, Inc. <http://www.redhat.com>
  This file is part of GlusterFS.

  This file is licensed to you under your choice of the GNU Lesser
  General Public License, version 3 or any later version (LGPLv3 or
  later), or the GNU General Public License, version 2 (GPLv2), in all
  cases as published by the Free Software Foundation.
*/

#ifndef _CONFIG_H
#define _CONFIG_H
#include "config.h"
#endif

#include "defaults.h"
#include "crypt-common.h"
#include "crypt.h"
#include "metadata.h"

int32_t alloc_format(crypt_local_t *local, size_t size)
{
	if (size > 0) {
		local->format = GF_CALLOC(1, size, gf_crypt_mt_mtd);
		if (!local->format)
			return ENOMEM;
	}
	local->format_size = size;
	return 0;
}

int32_t alloc_format_create(crypt_local_t *local)
{
	return alloc_format(local, new_format_size());
}

void free_format(crypt_local_t *local)
{
	GF_FREE(local->format);
}

/*
 * Check compatibility with extracted metadata
 */
static int32_t check_file_metadata(struct crypt_inode_info *info)
{
	struct object_cipher_info *object = &info->cinfo;

	if (info->nr_minor != CRYPT_XLATOR_ID) {
		gf_log("crypt", GF_LOG_WARNING,
		       "unsupported minor subversion %d", info->nr_minor);
		return EINVAL;
	}
	if (object->o_alg > LAST_CIPHER_ALG) {
		gf_log("crypt", GF_LOG_WARNING,
		       "unsupported cipher algorithm %d",
		       object->o_alg);
		return EINVAL;
	}
	if (object->o_mode > LAST_CIPHER_MODE) {
		gf_log("crypt", GF_LOG_WARNING,
		       "unsupported cipher mode %d",
		       object->o_mode);
		return EINVAL;
	}
	if (object->o_block_bits < CRYPT_MIN_BLOCK_BITS ||
	    object->o_block_bits > CRYPT_MAX_BLOCK_BITS) {
		gf_log("crypt", GF_LOG_WARNING, "unsupported block bits %d",
		       object->o_block_bits);
		return EINVAL;
	}
	/* TBD: check data key size */
	return 0;
}

static size_t format_size_v1(mtd_op_t op, size_t old_size)
{
	
	switch (op) {
	case MTD_CREATE:
		return sizeof(struct mtd_format_v1);
	case MTD_OVERWRITE:
		return old_size;
	case MTD_APPEND:
		return old_size + NMTD_8_MAC_SIZE;
	case MTD_CUT:
		if (old_size > sizeof(struct mtd_format_v1))
			return old_size - NMTD_8_MAC_SIZE;
		else
			return 0;
	default:
		gf_log("crypt", GF_LOG_WARNING, "Bad mtd operation");
		return 0;
	}
}

/*
 * Calculate size of the updated format string.
 * Returned zero means that we don't need to update the format string.
 */
size_t format_size(mtd_op_t op, size_t old_size)
{
	size_t versioned;

	versioned = mtd_loaders[current_mtd_loader()].format_size(op,
				 old_size - sizeof(struct crypt_format));
	if (versioned != 0)
		return versioned + sizeof(struct crypt_format);
	return 0;
}

/*
 * size of the format string of newly created file (nr_links = 1)
 */
size_t new_format_size(void)
{
	return format_size(MTD_CREATE, 0);
}

/*
 * Calculate per-link MAC by pathname
 */
static int32_t calc_link_mac_v1(struct mtd_format_v1 *fmt,
				loc_t *loc,
				unsigned char *result,
				struct crypt_inode_info *info,
				struct master_cipher_info *master)
{
	int32_t ret;	
	unsigned char nmtd_link_key[16];
	CMAC_CTX *cctx;
	size_t len;

	ret = get_nmtd_link_key(loc, master, nmtd_link_key);
	if (ret) {
		gf_log("crypt", GF_LOG_ERROR, "Can not get nmtd link key");
		return -1;
	}
	cctx = CMAC_CTX_new();
	if (!cctx) {
		gf_log("crypt", GF_LOG_ERROR, "CMAC_CTX_new failed");
		return -1; 
	}
	ret = CMAC_Init(cctx, nmtd_link_key, sizeof(nmtd_link_key),
			EVP_aes_128_cbc(), 0);
	if (!ret) {
		gf_log("crypt", GF_LOG_ERROR, "CMAC_Init failed");
		CMAC_CTX_free(cctx);
		return -1;
	}
	ret = CMAC_Update(cctx, get_NMTD_V1(info), SIZE_OF_NMTD_V1);
	if (!ret) {
		gf_log("crypt", GF_LOG_ERROR, "CMAC_Update failed");
		CMAC_CTX_free(cctx);
		return -1;
	}
	ret = CMAC_Final(cctx, result, &len);
	CMAC_CTX_free(cctx);
	if (!ret) {
		gf_log("crypt", GF_LOG_ERROR, "CMAC_Final failed");
		return -1;
	}
	return 0;
}

/*
 * Create per-link MAC of index @idx by pathname
 */
static int32_t create_link_mac_v1(struct mtd_format_v1 *fmt,
				  uint32_t idx,
				  loc_t *loc,
				  struct crypt_inode_info *info,
				  struct master_cipher_info *master)
{
	int32_t ret;
	unsigned char *mac;
	unsigned char cmac[16];

	mac = get_NMTD_V1_MAC(fmt) + idx * SIZE_OF_NMTD_V1_MAC;

	ret = calc_link_mac_v1(fmt, loc, cmac, info, master);
	if (ret)
		return -1;
	memcpy(mac, cmac, SIZE_OF_NMTD_V1_MAC);
	return 0;
}

static int32_t create_format_v1(unsigned char *wire,
				loc_t *loc,
				struct crypt_inode_info *info,
				struct master_cipher_info *master)
{
	int32_t ret;
	struct mtd_format_v1 *fmt;
	unsigned char mtd_key[16];
	AES_KEY EMTD_KEY;
	unsigned char nmtd_link_key[16];
	uint32_t ad;
	GCM128_CONTEXT *gctx;

	fmt = (struct mtd_format_v1 *)wire;

	fmt->minor_id = info->nr_minor;
	fmt->alg_id = AES_CIPHER_ALG;
	fmt->dkey_factor = master->m_dkey_size >> KEY_FACTOR_BITS;
	fmt->block_bits = master->m_block_bits;
	fmt->mode_id = master->m_mode;
	/*
	 * retrieve keys for the parts of metadata
	 */
	ret = get_emtd_file_key(info, master, mtd_key);
	if (ret)
		return ret;
	ret = get_nmtd_link_key(loc, master, nmtd_link_key);
	if (ret)
		return ret;
	
	AES_set_encrypt_key(mtd_key, sizeof(mtd_key)*8, &EMTD_KEY);

	gctx = CRYPTO_gcm128_new(&EMTD_KEY, (block128_f)AES_encrypt);

	/* TBD: Check return values */

	CRYPTO_gcm128_setiv(gctx, info->oid, sizeof(uuid_t));

	ad = htole32(MTD_LOADER_V1);
	ret = CRYPTO_gcm128_aad(gctx, (const unsigned char *)&ad, sizeof(ad));
	if (ret) {
		gf_log("crypt", GF_LOG_ERROR, " CRYPTO_gcm128_aad failed");
		CRYPTO_gcm128_release(gctx);
		return ret;
	}
	ret = CRYPTO_gcm128_encrypt(gctx,
				    get_EMTD_V1(fmt),
				    get_EMTD_V1(fmt),
				    SIZE_OF_EMTD_V1);
	if (ret) {
		gf_log("crypt", GF_LOG_ERROR, " CRYPTO_gcm128_encrypt failed");
		CRYPTO_gcm128_release(gctx);
		return ret;
	}
	/*
	 * set MAC of encrypted part of metadata
	 */
	CRYPTO_gcm128_tag(gctx, get_EMTD_V1_MAC(fmt), SIZE_OF_EMTD_V1_MAC);
	CRYPTO_gcm128_release(gctx);
	/*
	 * set the first MAC of non-encrypted part of metadata 
	 */
	return create_link_mac_v1(fmt, 0, loc, info, master);
}

/*
 * Called by fops:
 * ->create();
 * ->link();
 *
 * Pack common and version-specific parts of file's metadata
 * Pre-conditions: @info contains valid object-id.
 */
int32_t create_format(unsigned char *wire,
		      loc_t *loc,
		      struct crypt_inode_info *info,
		      struct master_cipher_info *master)
{
	struct crypt_format *fmt = (struct crypt_format *)wire;

	fmt->loader_id = current_mtd_loader();

	wire += sizeof(struct crypt_format);
	return mtd_loaders[current_mtd_loader()].create_format(wire, loc,
							       info, master);
}

/*
 * Append or overwrite per-link mac of @mac_idx index
 * in accordance with the new pathname
 */
int32_t appov_link_mac_v1(unsigned char *new,
			  unsigned char *old,
			  uint32_t old_size,
			  int32_t mac_idx,
			  loc_t *loc,
			  struct crypt_inode_info *info,
			  struct master_cipher_info *master,
			  crypt_local_t *local)
{
	memcpy(new, old, old_size);
	return create_link_mac_v1((struct mtd_format_v1 *)new, mac_idx,
				  loc, info, master);
}

/*
 * Cut per-link mac of @mac_idx index
 */
static int32_t cut_link_mac_v1(unsigned char *new,
			       unsigned char *old,			       
			       uint32_t old_size,
			       int32_t mac_idx,
			       loc_t *loc,
			       struct crypt_inode_info *info,
			       struct master_cipher_info *master,
			       crypt_local_t *local)
{
	memcpy(new,
	       old,
	       sizeof(struct mtd_format_v1) + NMTD_8_MAC_SIZE * (mac_idx - 1));

	memcpy(new + sizeof(struct mtd_format_v1) + NMTD_8_MAC_SIZE * (mac_idx - 1),
	       old + sizeof(struct mtd_format_v1) + NMTD_8_MAC_SIZE * mac_idx,
	       old_size - (sizeof(struct mtd_format_v1) + NMTD_8_MAC_SIZE * mac_idx));
	return 0;
}

int32_t update_format_v1(unsigned char *new,
			 unsigned char *old,
				size_t old_len,
				int32_t mac_idx, /* of old name */
				mtd_op_t op,
				loc_t *loc,
				struct crypt_inode_info *info,
				struct master_cipher_info *master,
				crypt_local_t *local)
{
	switch (op) {
	case MTD_APPEND:
		mac_idx = 1 + (old_len - sizeof(struct mtd_format_v1))/8;
	case MTD_OVERWRITE:
		return appov_link_mac_v1(new, old, old_len, mac_idx,
					 loc, info, master, local);
	case MTD_CUT:
		return cut_link_mac_v1(new, old, old_len, mac_idx,
				       loc, info, master, local);
	default:
		gf_log("crypt", GF_LOG_ERROR, "Bad  mtd operation %d", op);
		return -1;
	}
}

/*
 * Called by fops:
 *
 * ->link()
 * ->unlink()
 * ->rename()
 *
 */
int32_t update_format(unsigned char *new,
		      unsigned char *old,
		      size_t old_len,
		      int32_t mac_idx,
		      mtd_op_t op,
		      loc_t *loc,
		      struct crypt_inode_info *info,
		      struct master_cipher_info *master,
		      crypt_local_t *local)
{
	if (!new)
		return 0;
	memcpy(new, old, sizeof(struct crypt_format));

	old += sizeof(struct crypt_format);
	new += sizeof(struct crypt_format);
	old_len -= sizeof(struct crypt_format);

	return mtd_loaders[current_mtd_loader()].update_format(new, old,
							       old_len,
							       mac_idx, op,
							       loc, info,
							       master, local);
}

/*
 * Perform preliminary checks of found metadata
 * Return < 0 on errors;
 * Return number of object-id MACs (>= 1) on success
 */
int32_t check_format_v1(uint32_t len, unsigned char *wire)
{
	uint32_t nr_links;

	if (len < sizeof(struct mtd_format_v1)) {
		gf_log("crypt", GF_LOG_ERROR,
		       "v1-loader: bad metadata size %d", len);
		goto error;
	}
	len -= sizeof(struct mtd_format_v1);
	if (len % sizeof(nmtd_8_mac_t)) {
		gf_log("crypt", GF_LOG_ERROR,
		       "v1-loader: bad metadata format");
		goto error;
	}
	nr_links = 1 + len / sizeof(nmtd_8_mac_t);
	if (nr_links > _POSIX_LINK_MAX)
		goto error;
	return nr_links;
 error:
	return EIO;
}

/*
 * Verify per-link MAC specified by index @idx
 *
 * return:
 * -1 on errors;
 *  0 on failed verification;
 *  1 on successful verification
 */
static int32_t verify_link_mac_v1(struct mtd_format_v1 *fmt,
				  uint32_t idx /* index of the mac to verify */,
				  loc_t *loc,
				  struct crypt_inode_info *info,
				  struct master_cipher_info *master)
{
	int32_t ret;
	unsigned char *mac;
	unsigned char cmac[16];

	mac = get_NMTD_V1_MAC(fmt) + idx * SIZE_OF_NMTD_V1_MAC;

	ret = calc_link_mac_v1(fmt, loc, cmac, info, master);
	if (ret)
		return -1;
	if (memcmp(cmac, mac, SIZE_OF_NMTD_V1_MAC))
		return 0;
	return 1;
}

/*
 * Lookup per-link MAC by pathname.
 *
 * return index of the MAC, if it was found;
 * return < 0 on errors, or if the MAC wasn't found
 */
static int32_t lookup_link_mac_v1(struct mtd_format_v1 *fmt,
				  uint32_t nr_macs,
				  loc_t *loc,
				  struct crypt_inode_info *info,
				  struct master_cipher_info *master)
{
	int32_t ret;
	uint32_t idx;

	for (idx = 0; idx < nr_macs; idx++) {
		ret = verify_link_mac_v1(fmt, idx, loc, info, master);
		if (ret < 0)
			return ret;
		if (ret > 0)
			return idx;
	}
	return -ENOENT;
}

/*
 * Extract version-specific part of metadata
 */
static int32_t open_format_v1(unsigned char *wire,
			      int32_t len,
			      loc_t *loc,
			      struct crypt_inode_info *info,
			      struct master_cipher_info *master,
			      crypt_local_t *local,
			      gf_boolean_t load_info)
{
	int32_t ret;
	int32_t num_nmtd_macs;
	struct mtd_format_v1 *fmt;
	unsigned char mtd_key[16];
	AES_KEY EMTD_KEY;
	GCM128_CONTEXT *gctx;
	uint32_t ad;
	emtd_8_mac_t gmac;
	struct object_cipher_info *object;

	num_nmtd_macs = check_format_v1(len, wire);
	if (num_nmtd_macs <= 0)
		return EIO;

	ret = lookup_link_mac_v1((struct mtd_format_v1 *)wire,
				 num_nmtd_macs, loc, info, master);
	if (ret < 0) {
		gf_log("crypt", GF_LOG_ERROR, "NMTD verification failed");
		return EINVAL;
	}

	local->mac_idx = ret;
	if (load_info == _gf_false)
		/* the case of partial open */
		return 0;

	fmt = GF_CALLOC(1, len, gf_crypt_mt_mtd);
	if (!fmt)
		return ENOMEM;
	memcpy(fmt, wire, len);

	object = &info->cinfo;

	ret = get_emtd_file_key(info, master, mtd_key);
	if (ret) {
		gf_log("crypt", GF_LOG_ERROR, "Can not retrieve metadata key");
		goto out;
	}
	/*
	 * decrypt encrypted meta-data
	 */
	ret = AES_set_encrypt_key(mtd_key, sizeof(mtd_key)*8, &EMTD_KEY);
	if (ret < 0) {
		gf_log("crypt", GF_LOG_ERROR, "Can not set encrypt key");
		ret = EIO;
		goto out;
	}
	gctx = CRYPTO_gcm128_new(&EMTD_KEY, (block128_f)AES_encrypt);
	if (!gctx) {
		gf_log("crypt", GF_LOG_ERROR, "Can not alloc gcm context");
		ret = ENOMEM;
		goto out;
	}
	CRYPTO_gcm128_setiv(gctx, info->oid, sizeof(uuid_t));

	ad = htole32(MTD_LOADER_V1);
	ret = CRYPTO_gcm128_aad(gctx, (const unsigned char *)&ad, sizeof(ad));
	if (ret) {
		gf_log("crypt", GF_LOG_ERROR, " CRYPTO_gcm128_aad failed");
		CRYPTO_gcm128_release(gctx);
		ret = EIO;
		goto out;
	}
	ret = CRYPTO_gcm128_decrypt(gctx,
				    get_EMTD_V1(fmt),
				    get_EMTD_V1(fmt),
				    SIZE_OF_EMTD_V1);
	if (ret) {
		gf_log("crypt", GF_LOG_ERROR, " CRYPTO_gcm128_decrypt failed");
		CRYPTO_gcm128_release(gctx);
		ret = EIO;
		goto out;
	}
	/*
	 * verify metadata
	 */
	CRYPTO_gcm128_tag(gctx, gmac, sizeof(gmac));
	CRYPTO_gcm128_release(gctx);
	if (memcmp(gmac, get_EMTD_V1_MAC(fmt), SIZE_OF_EMTD_V1_MAC)) {
		gf_log("crypt", GF_LOG_ERROR, "EMTD verification failed");
		ret = EINVAL;
		goto out;
	}
	/*
	 * load verified metadata to the private part of inode
	 */
	info->nr_minor = fmt->minor_id;

	object->o_alg = fmt->alg_id;
	object->o_dkey_size = fmt->dkey_factor << KEY_FACTOR_BITS;
	object->o_block_bits = fmt->block_bits;
	object->o_mode = fmt->mode_id;

	ret = check_file_metadata(info);
 out:
	GF_FREE(fmt);
	return ret;
}

/*
 * perform metadata authentication against @loc->path;
 * extract crypt-specific attribtes and populate @info
 * with them (optional)
 */
int32_t open_format(unsigned char *str,
		    int32_t len,
		    loc_t *loc,
		    struct crypt_inode_info *info,
		    struct master_cipher_info *master,
		    crypt_local_t *local,
		    gf_boolean_t load_info)
{
	struct crypt_format *fmt;
	if (len < sizeof(*fmt)) {
		gf_log("crypt", GF_LOG_ERROR, "Bad core format");
		return EIO;
	}
	fmt = (struct crypt_format *)str;

	if (fmt->loader_id >= LAST_MTD_LOADER) {
		gf_log("crypt", GF_LOG_ERROR,
		       "Unsupported loader id %d", fmt->loader_id);
		return EINVAL;
	}
	str += sizeof(*fmt);
	len -= sizeof(*fmt);

	return mtd_loaders[fmt->loader_id].open_format(str,
						       len,
						       loc,
						       info,
						       master,
						       local,
						       load_info);
}

struct crypt_mtd_loader mtd_loaders [LAST_MTD_LOADER] = {
	[MTD_LOADER_V1] = 
	{.format_size = format_size_v1,
	 .create_format = create_format_v1,
	 .open_format = open_format_v1,
	 .update_format = update_format_v1
	}
};

/*
  Local variables:
  c-indentation-style: "K&R"
  mode-name: "LC"
  c-basic-offset: 8
  tab-width: 8
  fill-column: 80
  scroll-step: 1
  End:
*/
