/*
 * fscrypt_ddar.c
 *
 *  Created on: Oct 15, 2018
 *      Author: olic.moon
 */

#include <linux/bio.h>
#include "fscrypt_private.h"

extern int dd_submit_bio(struct dd_info *info, struct bio *bio);

int update_encryption_context_with_dd_policy(
		struct inode *inode,
		const struct dd_policy *policy)
{
	union fscrypt_context ctx;
	int ret;

	dd_info("update encryption context with dd policy ino:%ld flag:%x\n", inode->i_ino, policy->flags);

	/**
	 * i_rwsem needs to be acquired while setting policy so that new files
	 * cannot be created in the directory during or after the empty_dir() check
	 */
	inode_lock(inode);

	ret = inode->i_sb->s_cop->get_context(inode, &ctx, sizeof(ctx));
	switch (ctx.version) {
	case FSCRYPT_CONTEXT_V1: {
		if (ret == offsetof(struct fscrypt_context_v1, knox_flags)) {
			ctx.v1.knox_flags = 0;
			ret = sizeof(ctx.v1);
		}
		break;
	}
	case FSCRYPT_CONTEXT_V2: {
		if (ret == offsetof(struct fscrypt_context_v2, knox_flags)) {
			ctx.v2.knox_flags = 0;
			ret = sizeof(ctx.v2);
		}
		break;
	}
	}

	if (ret == -ENODATA) {
		dd_error("failed to set dd policy. empty fscrypto context\n");
		ret = -EFAULT;
	} else if (ret == fscrypt_context_size(&ctx)) {
		struct fscrypt_info	*ci = inode->i_crypt_info;
		if (ci && ci->ci_policy.version == FSCRYPT_POLICY_V1) {
			ctx.v1.knox_flags |= policy->flags << FSCRYPT_KNOX_FLG_DDAR_SHIFT & FSCRYPT_KNOX_FLG_DDAR_MASK;
			dd_verbose("fscrypt_context.knox_flag:0x%08x\n", ctx.v1.knox_flags);
		} else if (ci && ci->ci_policy.version == FSCRYPT_POLICY_V2) {
			ctx.v2.knox_flags |= policy->flags << FSCRYPT_KNOX_FLG_DDAR_SHIFT & FSCRYPT_KNOX_FLG_DDAR_MASK;
			dd_verbose("fscrypt_context.knox_flag:0x%08x\n", ctx.v2.knox_flags);
		}
//		ctx.knox_flags |= policy->flags << FSCRYPT_KNOX_FLG_DDAR_SHIFT & FSCRYPT_KNOX_FLG_DDAR_MASK;
//		dd_verbose("fscrypt_context.knox_flag:0x%08x\n", ctx.knox_flags);
		ret = ((int (*)(struct inode*, const char*, const void*, size_t, void *))(inode->i_sb->s_cop->android_oem_data1[1]))(inode, NULL, &ctx, fscrypt_context_size(&ctx), NULL);
		dd_info("result of set knox context for ino(%ld) : %d\n", inode->i_ino, ret);
	} else {
		dd_error("failed to set dd policy. get_context rc:%d\n", ret);
		ret = -EEXIST;
	}

	ret = dd_create_crypt_context(inode, policy, NULL);

	inode_unlock(inode);

	if (!ret) {
		struct dd_crypt_context crypt_context;
		struct fscrypt_info	*ci = inode->i_crypt_info;
		if (!ci) {
			dd_error("failed to alloc dd_info: no fbe policy found\n");
			ret = -EINVAL;
		} else {
			if (dd_read_crypt_context(inode, &crypt_context) != sizeof(struct dd_crypt_context)) {
				dd_error("update_encryption_context_with_dd_policy: failed to read dd crypt context ino:%ld\n", inode->i_ino);
				ret = -EINVAL;
			} else {
				struct ext_fscrypt_info *ext_ci = GET_EXT_CI(ci);
				ext_ci->ci_dd_info = alloc_dd_info(inode, policy, &crypt_context);
				if (IS_ERR(ext_ci->ci_dd_info)) {
					dd_error("failed to alloc dd info:%ld\n", inode->i_ino);
					ret = -ENOMEM;
					ext_ci->ci_dd_info = NULL;
				} else {
					fscrypt_dd_inc_count();
				}
			}
		}
	}
	return ret;
}

int dd_oem_page_crypto_inplace(struct dd_info *info, struct page *page, int dir)
{
	return fscrypt_crypt_block(info->inode,
			(dir == WRITE) ? FS_ENCRYPT:FS_DECRYPT, 0, page, page, PAGE_SIZE, 0, GFP_NOFS);
}

/* { KNOX_SUPPORT_DAR_DUAL_DO */
int fscrypt_dd_has_policy(const struct inode *inode)
{
	struct fscrypt_info *ci = NULL;
	if (!inode)
		return 0;

	ci = inode->i_crypt_info;
	if (ci) {
		struct ext_fscrypt_info *ext_ci = GET_EXT_CI(ci);
		if (ext_ci->ci_dd_info) {
			if (dd_policy_encrypted(ext_ci->ci_dd_info->policy.flags))
				return 1;
		}
	}

	return 0;
}
/* } KNOX_SUPPORT_DAR_DUAL_DO */

int fscrypt_dd_encrypted_inode(const struct inode *inode)
{
	struct fscrypt_info *ci = NULL;
	if (!inode)
		return 0;

	ci = inode->i_crypt_info;
	if (!S_ISREG(inode->i_mode))
		return 0;

	if (ci) {
		struct ext_fscrypt_info *ext_ci = GET_EXT_CI(ci);
		if (ext_ci->ci_dd_info) {
			if (dd_policy_encrypted(ext_ci->ci_dd_info->policy.flags))
				return 1;
		}
	}

	return 0;
}
EXPORT_SYMBOL(fscrypt_dd_encrypted_inode);

#ifdef CONFIG_SDP_KEY_DUMP
int fscrypt_dd_is_traced_inode(const struct inode *inode)
{
	struct fscrypt_info *ci = NULL;
	if (!inode)
		return 0;

	ci = inode->i_crypt_info;
	if (!S_ISREG(inode->i_mode))
		return 0;

	if (ci) {
		struct ext_fscrypt_info *ext_ci = GET_EXT_CI(ci);
		if (ext_ci->ci_dd_info) {
			if (dd_policy_trace_file(ext_ci->ci_dd_info->policy.flags))
				return 1;
		}
	}

	return 0;
}
EXPORT_SYMBOL(fscrypt_dd_is_traced_inode);

void fscrypt_dd_trace_inode(const struct inode *inode)
{
	struct fscrypt_info *ci = NULL;
	if (!inode)
		return;

	ci = inode->i_crypt_info;
	if (ci) {
		struct ext_fscrypt_info *ext_ci = GET_EXT_CI(ci);
		if (ext_ci->ci_dd_info) {
			dd_info("update dd trace policy ino:%ld\n", inode->i_ino);
			ext_ci->ci_dd_info->policy.flags |= DD_POLICY_TRACE_FILE;
		}
	}
}
EXPORT_SYMBOL(fscrypt_dd_trace_inode);
#endif

struct inode *fscrypt_bio_get_inode(const struct bio *bio)
{
	if (!bio)
		return NULL;
	if (!bio_has_data((struct bio *)bio))
		return NULL;
	if (!bio->bi_io_vec)
		return NULL;
	if (!bio->bi_io_vec->bv_page)
		return NULL;

	if (PageAnon(bio->bi_io_vec->bv_page)) {
		struct inode *inode = NULL;

		/* Using direct-io (O_DIRECT) without page cache */
		// SDP - START BLOCK
		// inode = dio_bio_get_inode((struct bio *)bio);
		// dd_verbose("inode on direct-io, inode = 0x%pK.\n", inode);
		// SDP - END BLOCK

		return inode;
	}

	if (!page_mapping(bio->bi_io_vec->bv_page))
		return NULL;

	return page_mapping(bio->bi_io_vec->bv_page)->host;
}

/**
 * prevent merging bios from different files when either is ddar enabled
 */
bool fscrypt_dd_can_merge_bio(struct bio *bio, struct address_space *mapping)
{
	struct dd_info *info1, *info2;

	if (!bio)
		return true;

	info1 = dd_get_info(fscrypt_bio_get_inode(bio));
	info2 = dd_get_info(mapping->host);

	if (info1 || info2) {
		// either is ddar protected
		if (!info1)
			goto err_out;
		if (!info2)
			goto err_out;

		if (info1->ino == info2->ino) {
			dd_verbose("allowing bio merge ino:%ld\n", info1->ino);
			return true;
		}

err_out:
		dd_verbose("disallowing bio merge ino1:%ld ino2:%ld\n",
				info1 ? info1->ino : -1, info2 ? info2->ino : -1);
		return false;
	}

	// none is ddar enabled
	return true;
}

void *dd_get_info(const struct inode *inode)
{
	struct ext_fscrypt_info *ext_ci;

	if (!inode)
		return NULL;

	if (!inode->i_crypt_info)
		return NULL;

	ext_ci = GET_EXT_CI(inode->i_crypt_info);
	return ext_ci->ci_dd_info;
}

int fscrypt_dd_decrypt_page(struct inode *inode, struct page *page)
{
	struct ext_fscrypt_info *ext_ci = GET_EXT_CI(inode->i_crypt_info);
	return dd_page_crypto(ext_ci->ci_dd_info, DD_DECRYPT, page, page);
}

void fscrypt_dd_set_count(long count)
{
	set_ddar_count(count);
}

long fscrypt_dd_get_count(void)
{
	return get_ddar_count();
}

void fscrypt_dd_inc_count(void)
{
	inc_ddar_count();
}

void fscrypt_dd_dec_count(void)
{
	dec_ddar_count();
}

int fscrypt_dd_is_locked(void)
{
	return dd_is_user_deamon_locked();
}

long fscrypt_dd_ioctl(unsigned int cmd, unsigned long *arg, struct inode *inode)
{
	if (inode && S_ISDIR(inode->i_mode) && !fscrypt_has_encryption_key(inode)
			&& (cmd != FS_IOC_GET_DD_INODE_COUNT)) {
		if (fscrypt_prepare_readdir(inode)) {
			return -ENOTTY;
		}
	}

	switch (cmd) {
	case FS_IOC_GET_DD_POLICY: {
		struct dd_info *info;

		if (!fscrypt_dd_encrypted_inode(inode))
			return -ENOENT;

		info = dd_get_info(inode);
		if (copy_to_user((void __user *) (*arg), &info->policy,
				sizeof(struct dd_policy)))
			return -EFAULT;
		return 0;
	}
	case FS_IOC_SET_DD_POLICY: {
		struct dd_policy policy;

		if (fscrypt_dd_encrypted_inode(inode))
			return -EEXIST;

		if (copy_from_user(&policy, (struct dd_policy __user *) (*arg),
				sizeof(policy))) {
			return -EFAULT;
		}

		return update_encryption_context_with_dd_policy(inode, &policy);
	}
	case FS_IOC_GET_DD_INODE_COUNT: {
		long ret = fscrypt_dd_get_count();
		dd_info("FS_IOC_GET_DD_INODE_COUNT - dd_count : %ld", ret);

		if (copy_to_user((void __user *) (*arg), &ret, sizeof(long)))
			return -EFAULT;
		return 0;
	}
	/* { KNOX_SUPPORT_DAR_DUAL_DO */
	case FS_IOC_HAS_DD_POLICY: {
		if (!fscrypt_dd_has_policy(inode)) {
			dd_error("dd policy is not applied (ino:%ld)\n", inode->i_ino);
			return -ENOENT;
		}

		dd_info("dd policy is applied (ino:%ld)\n", inode->i_ino);
		return 0;
	}
	/* } KNOX_SUPPORT_DAR_DUAL_DO */
	}
	return 0;
}

int fscrypt_dd_submit_bio(struct inode *inode, struct bio *bio)
{
	return dd_submit_bio(dd_get_info(inode), bio);
}

int fscrypt_dd_may_submit_bio(struct bio *bio)
{
	struct inode *inode = fscrypt_bio_get_inode(bio);
	if (!fscrypt_dd_encrypted_inode(inode))
		return -EOPNOTSUPP;

	return fscrypt_dd_submit_bio(inode, bio);
}

long fscrypt_dd_get_ino(struct bio *bio)
{
	struct inode *inode = fscrypt_bio_get_inode(bio);
	if (fscrypt_dd_encrypted_inode(inode))
		return inode->i_ino;

	return 0;
}

int fscrypt_dd_encrypted(struct bio *bio)
{
	struct inode *inode = fscrypt_bio_get_inode(bio);
	return fscrypt_dd_encrypted_inode(inode);
}
EXPORT_SYMBOL(fscrypt_dd_encrypted);