/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */

#include "lib.h"
#include "ioloop-private.h"
#include "str.h"
#include "sha2.h"
#include "module-dir.h"
#include "hex-binary.h"
#include "settings.h"
#include "mail-namespace.h"
#include "mail-storage-hooks.h"
#include "mail-storage-service.h"
#include "mail-user.h"
#include "acl-plugin.h"
#include "acl-api-private.h"
#include "mail-crypt-common.h"
#include "mail-crypt-key.h"
#include "mail-crypt-plugin.h"

#define MAIL_CRYPT_ACL_LIST_CONTEXT(obj) \
	MODULE_CONTEXT_REQUIRE(obj, mail_crypt_acl_mailbox_list_module)

struct mail_crypt_acl_mailbox_list {
	union mailbox_list_module_context module_ctx;
	const struct acl_backend_vfuncs *acl_vprev;
	struct acl_backend_vfuncs v;
};

static MODULE_CONTEXT_DEFINE_INIT(mail_crypt_acl_mailbox_list_module,
				  &mailbox_list_module_register);

void mail_crypt_acl_plugin_init(struct module *module);
void mail_crypt_acl_plugin_deinit(void);

static int
mail_crypt_acl_has_user_read_right(struct acl_object *aclobj,
				   const char *username,
				   const char **error_r)
{
	struct acl_object_list_iter *iter;
	struct acl_rights rights;
	int ret = 0;

	iter = acl_object_list_init(aclobj);
	while (acl_object_list_next(iter, &rights)) {
		if (rights.id_type == ACL_ID_USER &&
		    strcmp(rights.identifier, username) == 0) {
			ret = str_array_find(rights.rights, MAIL_ACL_READ) ? 1 : 0;
			break;
		}
	}
	if (acl_object_list_deinit(&iter) < 0) {
		*error_r = "Failed to iterate ACL objects";
		return -1;
	}

	return ret;
}

static int mail_crypt_acl_has_nonuser_read_right(struct acl_object *aclobj,
						 const char **error_r)
{
	struct acl_object_list_iter *iter;
	struct acl_rights rights;
	int ret = 0;

	iter = acl_object_list_init(aclobj);
	while (acl_object_list_next(iter, &rights)) {
		if (rights.id_type != ACL_ID_USER &&
		    rights.id_type != ACL_ID_OWNER &&
		    rights.rights != NULL &&
		    str_array_find(rights.rights, MAIL_ACL_READ)) {
			ret = 1;
			break;
		}
	}
	if (acl_object_list_deinit(&iter) < 0) {
		*error_r = "Failed to iterate ACL objects";
		return -1;
	}
	return ret;
}

static int
mail_crypt_acl_unset_private_keys(struct mailbox *src_box,
				  const char *dest_user,
				  enum mail_attribute_type type,
				  const char **error_r)
{
	ARRAY_TYPE(const_string) digests;
	const char *error;
	int ret = 1;

	if (mailbox_open(src_box) < 0) {
		*error_r = t_strdup_printf("mail-crypt-acl-plugin: "
					   "mailbox_open(%s) failed: %s",
					   mailbox_get_vname(src_box),
					   mailbox_get_last_internal_error(src_box, NULL));
		return -1;
	}

	t_array_init(&digests, 4);
	if (mail_crypt_box_get_pvt_digests(src_box, pool_datastack_create(),
					   type, &digests, &error) < 0) {
		*error_r = t_strdup_printf("mail-crypt-acl-plugin: "
					   "Failed to lookup public key digests: %s",
					   error);
		mailbox_free(&src_box);
		return -1;
	}

	struct mailbox_transaction_context *t;
	t = mailbox_transaction_begin(src_box, 0, __func__);

	const char *hash;
	array_foreach_elem(&digests, hash) {
		const char *ptr;
		/* if the id contains username part, skip to key public id */
		if ((ptr = strchr(hash, '/')) != NULL)
			ptr++;
		else
			ptr = hash;
		if ((ret = mail_crypt_box_unset_shared_key(t, ptr, dest_user,
							   error_r)) < 0) {
			ret = -1;
			break;
		}
	}

	if (ret < 0) {
		mailbox_transaction_rollback(&t);
	} else if (mailbox_transaction_commit(&t) < 0) {
		*error_r = t_strdup_printf("mail-crypt-acl-plugin: "
					   "mailbox_transaction_commit(%s) failed: %s",
					   mailbox_get_vname(src_box),
					   mailbox_get_last_internal_error(src_box, NULL));
		return -1;
	}
	return 0;
}

static int
mail_crypt_acl_user_create(struct mail_user *user, const char *dest_username,
			   struct mail_user **dest_user_r,
			   const char **error_r)
{
	const struct mail_storage_service_input *old_input;
	struct mail_storage_service_input input;
	struct mail_storage_service_ctx *service_ctx;

	int ret;

	service_ctx = mail_storage_service_user_get_service_ctx(user->service_user);
	old_input = mail_storage_service_user_get_input(user->service_user);

	i_zero(&input);
	input.service = old_input->service;
	input.protocol = old_input->protocol;
	input.username = dest_username;
	input.session_id_prefix = user->session_id;
	input.flags_override_add = MAIL_STORAGE_SERVICE_FLAG_NO_PLUGINS |
				   MAIL_STORAGE_SERVICE_FLAG_NO_LOG_INIT;
	input.flags_override_remove = MAIL_STORAGE_SERVICE_FLAG_NO_NAMESPACES;

	ret = mail_storage_service_lookup_next(service_ctx, &input,
						dest_user_r, error_r);

	return ret;
}

static int
mail_crypt_acl_update_private_key(struct mailbox *src_box,
				  struct mail_user *dest_user, bool set,
				  bool disallow_insecure,
				  const char **error_r)
{
	struct dcrypt_public_key *key = NULL;
	struct dcrypt_private_key *priv_key;
	int ret = 0;

	if (!set) {
		return mail_crypt_acl_unset_private_keys(src_box,
							dest_user->username,
							MAIL_ATTRIBUTE_TYPE_SHARED,
							error_r);
	}

	if (dest_user != NULL) {
		/* get public key from target user */
		if ((ret = mail_crypt_user_get_public_key(dest_user,
							  &key, error_r)) <= 0) {
			if (ret == 0 && disallow_insecure) {
				*error_r = t_strdup_printf("User %s has no active public key",
							   dest_user->username);
				return -1;
			} else if (ret < 0) {
				return -1;
			} else if (ret == 0) {
				/* perform insecure sharing */
				dest_user = NULL;
				key = NULL;
			}
		}
	}

	ARRAY_TYPE(dcrypt_private_key) keys;
	t_array_init(&keys, 8);

	struct mailbox_transaction_context *t =
		mailbox_transaction_begin(src_box, 0, __func__);

	/* get private keys from box */
	if (mail_crypt_box_get_private_keys(src_box, &keys, error_r) < 0 ||
	    mail_crypt_box_share_private_keys(t, key,
					      dest_user == NULL ? NULL :
					      dest_user->username,
					      &keys, error_r) < 0)
		ret = -1;
	if (key != NULL)
		dcrypt_key_unref_public(&key);

	if (ret >= 0) {
		array_foreach_elem(&keys, priv_key)
			dcrypt_key_unref_private(&priv_key);
	}

	if (mailbox_transaction_commit(&t) < 0) {
		*error_r = mailbox_get_last_internal_error(src_box, NULL);
		ret = -1;
	}

	return ret;
}

static int mail_crypt_acl_object_update(struct acl_object *aclobj,
					const struct acl_rights_update *update)
{
	const char *error;
	struct mail_crypt_acl_mailbox_list *mlist =
		MAIL_CRYPT_ACL_LIST_CONTEXT(aclobj->backend->list);
	const struct crypt_acl_settings *set;
	struct event *event = aclobj->backend->event;
	const char *username;
	struct mail_user *dest_user;
	struct ioloop_context *cur_ioloop_ctx;
	bool have_rights;
	int ret = 0;

	if (mlist->acl_vprev->object_update(aclobj, update) < 0)
		return -1;

	if (settings_get(event, &crypt_acl_setting_parser_info, 0,
			 &set, &error) < 0) {
		e_error(event, "%s", error);
		return -1;
	}
	bool disallow_insecure = set->crypt_acl_require_secure_key_sharing;
	settings_free(set);

	const char *box_name = mailbox_list_get_vname(aclobj->backend->list,
						      aclobj->name);
	struct mailbox *box = mailbox_alloc(aclobj->backend->list, box_name, 0);

	switch (update->rights.id_type) {
	case ACL_ID_USER:
		/* setting rights for specific user: we can encrypt the
		   mailbox key for the user. */
		username = update->rights.identifier;
		ret = mail_crypt_acl_has_user_read_right(aclobj, username, &error);

		if (ret < 0) {
			e_error(event,
				"mail-crypt-acl-plugin: mail_crypt_acl_has_user_read_right(%s) failed: %s",
				username, error);
			break;
		}

		have_rights = ret > 0;

		ret = mail_crypt_acl_user_create(aclobj->backend->list->ns->user,
						 username, &dest_user, &error);

		if (ret <= 0) {
			e_error(event,
				"mail-crypt-acl-plugin: Cannot initialize destination user %s: %s",
				username, error);
			break;
		} else {
			i_assert(dest_user != NULL);
			if ((ret = mailbox_open(box)) < 0) {
				e_error(event,
					"mail-crypt-acl-plugin: mailbox_open(%s) failed: %s",
					mailbox_get_vname(box),
					mailbox_get_last_internal_error(box, NULL));
			} else if ((ret = mail_crypt_acl_update_private_key(box, dest_user,
									have_rights,
									disallow_insecure,
									&error)) < 0) {
				e_error(event,
					"mail-crypt-acl-plugin: acl_update_private_key(%s, %s) failed: %s",
					mailbox_get_vname(box),
					username,
					error);
			}
		}

		mail_user_deinit(&dest_user);

		if ((cur_ioloop_ctx = io_loop_get_current_context(current_ioloop)) != NULL)
			io_loop_context_deactivate(cur_ioloop_ctx);
		mail_storage_service_io_activate_user(
			aclobj->backend->list->ns->user->service_user
		);
		break;
	case ACL_ID_OWNER:
		/* we should be the one doing this? ignore */
		break;
	case ACL_ID_ANYONE:
	case ACL_ID_AUTHENTICATED:
	case ACL_ID_GROUP:
	case ACL_ID_GROUP_OVERRIDE:
		if (disallow_insecure) {
			e_error(event, "mail-crypt-acl-plugin: "
				"Secure key sharing is enabled -"
				"Remove or set crypt_acl_require_secure_key_sharing=no");
			ret = -1;
			break;
		}
		/* the mailbox key needs to be stored unencrypted. for groups
		   we could in theory use per-group encrypted keys, which the
		   users belonging to the group would able to decrypt with
		   their private key, but that becomes quite complicated. */
		if ((ret = mail_crypt_acl_has_nonuser_read_right(aclobj, &error)) < 0) {
		    	e_error(event, "mail-crypt-acl-plugin: %s", error);
		} else if ((ret = mailbox_open(box)) < 0) {
			e_error(event, "mail-crypt-acl-plugin: "
				"mailbox_open(%s) failed: %s",
				mailbox_get_vname(box),
				mailbox_get_last_internal_error(box, NULL));
		} else if ((ret = mail_crypt_acl_update_private_key(box,
								    NULL,
								    TRUE,
								    disallow_insecure,
								    &error)) < 0) {
			e_error(event, "mail-crypt-acl-plugin: "
				"acl_update_private_key(%s, %s) failed: %s",
				mailbox_get_vname(box), "", error);
		}
		break;
	case ACL_ID_TYPE_COUNT:
		i_unreached();
	}

	mailbox_free(&box);
	return ret;
}

static void
mail_crypt_acl_mail_namespace_storage_added(struct mail_namespace *ns)
{
	struct acl_mailbox_list *alist = ACL_LIST_CONTEXT(ns->list);
	struct mail_crypt_acl_mailbox_list *mlist =
		MAIL_CRYPT_ACL_LIST_CONTEXT(ns->list);
	struct acl_backend *backend;

	if (alist == NULL)
		return;

	/* FIXME: this method works only if there's a single plugin doing it.
	   if there are ever multiple plugins hooking into ACL commands the
	   ACL core code would need some changing to make it work correctly. */
	backend = alist->rights.backend;
	mlist->acl_vprev = backend->v;
	mlist->v = *backend->v;
	mlist->v.object_update = mail_crypt_acl_object_update;
	backend->v = &mlist->v;
}

static void mail_crypt_acl_mailbox_list_deinit(struct mailbox_list *list)
{
	struct mail_crypt_acl_mailbox_list *mlist =
		MAIL_CRYPT_ACL_LIST_CONTEXT(list);

	mlist->module_ctx.super.deinit(list);
}

static void mail_crypt_acl_mailbox_list_created(struct mailbox_list *list)
{
	struct mailbox_list_vfuncs *v = list->vlast;
	struct mail_crypt_acl_mailbox_list *mlist;

	mlist = p_new(list->pool, struct mail_crypt_acl_mailbox_list, 1);
	mlist->module_ctx.super = *v;
	list->vlast = &mlist->module_ctx.super;
	v->deinit = mail_crypt_acl_mailbox_list_deinit;

	MODULE_CONTEXT_SET(list, mail_crypt_acl_mailbox_list_module, mlist);
}

static struct mail_storage_hooks mail_crypt_acl_mail_storage_hooks = {
	.mailbox_list_created = mail_crypt_acl_mailbox_list_created,
	.mail_namespace_storage_added = mail_crypt_acl_mail_namespace_storage_added
};

void mail_crypt_acl_plugin_init(struct module *module)
{
	mail_storage_hooks_add(module, &mail_crypt_acl_mail_storage_hooks);
}

void mail_crypt_acl_plugin_deinit(void)
{
	mail_storage_hooks_remove(&mail_crypt_acl_mail_storage_hooks);
}

const char *mail_crypt_acl_plugin_dependencies[] = { "acl", NULL };
