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

#include "lib.h"
#include "str.h"
#include "dict.h"
#include "array.h"
#include "settings.h"
#include "mail-storage.h"
#include "mailbox-attribute.h"
#include "mail-crypt-common.h"
#include "mail-crypt-key.h"
#include "mail-crypt-plugin.h"
#include "mail-user.h"
#include "hex-binary.h"
#include "safe-memset.h"
#include "base64.h"
#include "sha2.h"

struct mail_crypt_key_cache_entry {
	struct mail_crypt_key_cache_entry *next;

	char *pubid;
	/* this is lazily initialized */
	struct dcrypt_keypair pair;
};

static
int mail_crypt_get_key_cache(struct mail_crypt_key_cache_entry *cache,
			     const char *pubid,
			     struct dcrypt_private_key **privkey_r,
			     struct dcrypt_public_key **pubkey_r)
{
	for(struct mail_crypt_key_cache_entry *ent = cache;
	    ent != NULL; ent = ent->next)
	{
		if (strcmp(pubid, ent->pubid) == 0) {
			if (privkey_r != NULL && ent->pair.priv != NULL) {
				dcrypt_key_ref_private(ent->pair.priv);
				*privkey_r = ent->pair.priv;
				return 1;
			} else if (pubkey_r != NULL && ent->pair.pub != NULL) {
				dcrypt_key_ref_public(ent->pair.pub);
				*pubkey_r = ent->pair.pub;
				return 1;
			} else if ((privkey_r == NULL && pubkey_r == NULL) ||
				   (ent->pair.priv == NULL &&
				   ent->pair.pub == NULL)) {
				i_unreached();
			}
		}
	}
	return 0;
}

static
void mail_crypt_put_key_cache(struct mail_crypt_key_cache_entry **cache,
			      const char *pubid,
			      struct dcrypt_private_key *privkey,
			      struct dcrypt_public_key *pubkey)
{
	for(struct mail_crypt_key_cache_entry *ent = *cache;
	    ent != NULL; ent = ent->next)
	{
		if (strcmp(pubid, ent->pubid) == 0) {
			if (privkey != NULL) {
				if (ent->pair.priv == NULL) {
					ent->pair.priv = privkey;
					dcrypt_key_ref_private(ent->pair.priv);
				}
			} else if (pubkey != NULL) {
				if (ent->pair.pub == NULL) {
					ent->pair.pub = pubkey;
					dcrypt_key_ref_public(ent->pair.pub);
				}
			} else
				i_unreached();
			return;
		}
	}

	/* not found */
	struct mail_crypt_key_cache_entry *ent =
		i_new(struct mail_crypt_key_cache_entry, 1);
	ent->pubid = i_strdup(pubid);
	ent->pair.priv = privkey;
	ent->pair.pub = pubkey;
	if (ent->pair.priv != NULL)
		dcrypt_key_ref_private(ent->pair.priv);
	if (ent->pair.pub != NULL)
		dcrypt_key_ref_public(ent->pair.pub);

	if (*cache == NULL) {
		*cache = ent;
	} else {
		ent->next = *cache;
		*cache = ent;
	}
}

void mail_crypt_key_cache_destroy(struct mail_crypt_key_cache_entry **cache)
{
	struct mail_crypt_key_cache_entry *next, *cur = *cache;

	*cache = NULL;

	while(cur != NULL) {
		next = cur->next;
		i_free(cur->pubid);
		if (cur->pair.priv != NULL)
			dcrypt_key_unref_private(&cur->pair.priv);
		if (cur->pair.pub != NULL)
			dcrypt_key_unref_public(&cur->pair.pub);
		i_free(cur);
		cur = next;
	}
}

int mail_crypt_private_key_id_match(struct dcrypt_private_key *key,
				     const char *pubid, const char **error_r)
{
	i_assert(key != NULL);
	buffer_t *key_id = t_str_new(MAIL_CRYPT_HASH_BUF_SIZE);
	if (!dcrypt_key_id_private(key, MAIL_CRYPT_KEY_ID_ALGORITHM, key_id,
				   error_r))
		return -1;
	const char *hash = binary_to_hex(key_id->data, key_id->used);
	if (strcmp(pubid, hash) == 0) return 1;

	buffer_set_used_size(key_id, 0);
	if (!dcrypt_key_id_private_old(key, key_id, error_r)) {
		return -1;
	}
	hash = binary_to_hex(key_id->data, key_id->used);

	if (strcmp(pubid, hash) != 0) {
		*error_r = t_strdup_printf("Key %s does not match given ID %s",
					   hash, pubid);
		return 0;
	}
	return 1;
}

int mail_crypt_public_key_id_match(struct dcrypt_public_key *key,
				   const char *pubid, const char **error_r)
{
	i_assert(key != NULL);
	buffer_t *key_id = t_str_new(MAIL_CRYPT_HASH_BUF_SIZE);
	if (!dcrypt_key_id_public(key, MAIL_CRYPT_KEY_ID_ALGORITHM, key_id,
				  error_r))
		return -1;
	const char *hash = binary_to_hex(key_id->data, key_id->used);
	if (strcmp(pubid, hash) == 0) return 1;

	buffer_set_used_size(key_id, 0);
	if (!dcrypt_key_id_public_old(key, key_id, error_r)) {
		return -1;
	}
	hash = binary_to_hex(key_id->data, key_id->used);

	if (strcmp(pubid, hash) != 0) {
		*error_r = t_strdup_printf("Key %s does not match given ID %s",
					   hash, pubid);
		return 0;
	}
	return 1;
}

static int
mail_crypt_user_encryption_keys_load(struct event *event,
				     const struct crypt_settings *set,
				     struct mail_crypt_global_keys *global_keys_r,
				     const char **error_r)
{
	const struct crypt_private_key_settings *key_set;
	struct settings_file file;
	const char *key_name, *error;

	mail_crypt_global_keys_init(global_keys_r);
	if (!array_is_created(&set->crypt_user_key_encryption_keys))
		return 0;

	array_foreach_elem(&set->crypt_user_key_encryption_keys, key_name) {
		if (settings_get_filter(event, "crypt_user_key_encryption_key",
					key_name,
					&crypt_private_key_setting_parser_info,
					0, &key_set, &error) < 0) {
			*error_r = t_strdup_printf(
				"Failed to get crypt_private_key %s: %s",
				key_name, error);
			return -1;
		}
		settings_file_get(key_set->crypt_private_key_file,
				  unsafe_data_stack_pool, &file);
		if (mail_crypt_load_global_private_key(
				key_name, &file,
				key_set->crypt_private_key_password,
				global_keys_r, &error) < 0) {
			/* skip this key */
			e_debug(event, "%s - skipping", error);
		}
		settings_free(key_set);
	}
	return 0;
}

static
int mail_crypt_env_get_private_key(struct mail_user *user, const char *pubid,
				   struct dcrypt_private_key **key_r,
				   const char **error_r)
{
	struct mail_crypt_user *muser = mail_crypt_get_mail_crypt_user(user);
	struct mail_crypt_global_keys global_keys;
	int ret = 0;

	if (mail_crypt_user_encryption_keys_load(user->event, muser->set,
						 &global_keys, error_r) < 0) {
		mail_crypt_global_keys_free(&global_keys);
		return -1;
	}

	/* see if we got a key */
	struct dcrypt_private_key *key =
		mail_crypt_global_key_find(&global_keys, pubid);

	if (key != NULL) {
		dcrypt_key_ref_private(key);
		*key_r = key;
		ret = 1;
	}

	mail_crypt_global_keys_free(&global_keys);

	return ret;
}

static
const char *mail_crypt_get_key_path(bool user_key, bool public, const char *pubid)
{
	const char *ret = t_strdup_printf("%s%s%s",
				user_key ? USER_CRYPT_PREFIX :
					  BOX_CRYPT_PREFIX,
				public ? PUBKEYS_PREFIX :
					  PRIVKEYS_PREFIX,
				pubid);
	return ret;
}

static
int mail_crypt_decrypt_private_key(struct mailbox *box, const char *pubid,
				   const char *data,
				   struct dcrypt_private_key **key_r,
				   const char **error_r)
{
	enum dcrypt_key_kind key_kind;
	enum dcrypt_key_encryption_type enc_type;
	const char *enc_hash = NULL, *key_hash = NULL, *pw = NULL;
	struct dcrypt_private_key *key = NULL, *dec_key = NULL;
	struct mail_user *user = mail_storage_get_user(mailbox_get_storage(box));
	struct mail_crypt_user *muser = mail_crypt_get_mail_crypt_user(user);
	int ret = 0;

	i_assert(pubid != NULL);
	i_assert(data != NULL);

	/* see what the key needs for decrypting */
	if (!dcrypt_key_string_get_info(data, NULL, NULL, &key_kind,
					&enc_type, &enc_hash, &key_hash, error_r)) {
		return -1;
	}

	if (key_kind != DCRYPT_KEY_KIND_PRIVATE) {
		*error_r = t_strdup_printf("Cannot use key %s: "
					   "Expected private key, got public key",
					   pubid);
		return -1;
	}

	if (key_hash != NULL && strcmp(key_hash, pubid) != 0) {
		*error_r = t_strdup_printf("Cannot use key %s: "
					   "Incorrect key hash %s stored",
					   pubid,
					   key_hash);
		return -1;
	}

	/* see if it needs decrypting */
	if (enc_type == DCRYPT_KEY_ENCRYPTION_TYPE_NONE) {
		/* no key or password */
	} else if (enc_type == DCRYPT_KEY_ENCRYPTION_TYPE_PASSWORD) {
		pw = muser->set->crypt_user_key_password;
		if (pw[0] == '\0') {
			*error_r = t_strdup_printf("Cannot decrypt key %s: "
						   "Password not available",
						   pubid);
			return -1;
		}
	} else if (enc_type == DCRYPT_KEY_ENCRYPTION_TYPE_KEY) {
		if ((ret = mail_crypt_user_get_private_key(user, enc_hash,
							   &dec_key, error_r)) <= 0) {
			/* last resort, look at environment */
			if (ret == 0 && (ret = mail_crypt_env_get_private_key(user, enc_hash,
								  &dec_key, error_r)) == 0) {
				*error_r = t_strdup_printf("Cannot decrypt key %s: "
							   "Private key %s not available:",
							   pubid, enc_hash);
				return -1;
			} else if (ret < 0) {
				*error_r =  t_strdup_printf("Cannot decrypt key %s: %s",
							    pubid, *error_r);
				return ret;
			}
		}
	}

	bool res = dcrypt_key_load_private(&key, data, pw, dec_key, error_r);

	if (dec_key != NULL)
		dcrypt_key_unref_private(&dec_key);

	if (!res)
		return -1;

	if (mail_crypt_private_key_id_match(key, pubid, error_r) <= 0) {
		if (key != NULL)
			dcrypt_key_unref_private(&key);
		return -1;
	}

	i_assert(key != NULL);

	*key_r = key;

	return 1;
}

int mail_crypt_get_private_key(struct mailbox *box, const char *pubid,
				bool user_key, bool shared,
				struct dcrypt_private_key **key_r,
				const char **error_r)
{
	struct mail_user *user = mail_storage_get_user(mailbox_get_storage(box));
	struct mail_crypt_user *muser = mail_crypt_get_mail_crypt_user(user);

	/* check cache */
	if (mail_crypt_get_key_cache(muser->key_cache, pubid, key_r, NULL) > 0) {
		return 1;
	}

	struct mail_attribute_value value;
	struct dcrypt_private_key *key;
	int ret;
	const char *attr_name = mail_crypt_get_key_path(user_key, FALSE, pubid);

	if ((ret = mailbox_attribute_get(box,
					 shared ? MAIL_ATTRIBUTE_TYPE_SHARED :
						  MAIL_ATTRIBUTE_TYPE_PRIVATE,
					 attr_name, &value)) <= 0) {
		if (ret < 0) {
			*error_r = t_strdup_printf("mailbox_attribute_get(%s, %s%s) failed: %s",
						   mailbox_get_vname(box),
						   shared ? "/shared/" :
							    "/priv/",
						   attr_name,
						   mailbox_get_last_internal_error(box, NULL));
		}
		return ret;
	}

	if ((ret = mail_crypt_decrypt_private_key(box, pubid, value.value,
						  &key, error_r)) <= 0)
		return ret;

	i_assert(key != NULL);

	mail_crypt_put_key_cache(&muser->key_cache, pubid, key, NULL);

	*key_r = key;

	return 1;
}

int mail_crypt_user_get_private_key(struct mail_user *user, const char *pubid,
				    struct dcrypt_private_key **key_r,
				    const char **error_r)
{
	struct mail_namespace *ns = mail_namespace_find_inbox(user->namespaces);
	struct mailbox *box = mailbox_alloc(ns->list, "INBOX",
					    MAILBOX_FLAG_READONLY);
	struct mail_attribute_value value;
	int ret;

	/* try retrieve currently active user key */
	if (mailbox_open(box) < 0) {
		*error_r = t_strdup_printf("mailbox_open(%s) failed: %s",
					   "INBOX",
					   mailbox_get_last_internal_error(box, NULL));
		return -1;
	}

	if (pubid == NULL) {
		if ((ret = mailbox_attribute_get(box, MAIL_ATTRIBUTE_TYPE_SHARED,
						 USER_CRYPT_PREFIX ACTIVE_KEY_NAME,
						 &value)) <= 0) {
			if (ret < 0) {
				*error_r = t_strdup_printf("mailbox_attribute_get(%s, /shared/%s) failed: %s",
							   mailbox_get_vname(box),
							   USER_CRYPT_PREFIX ACTIVE_KEY_NAME,
							   mailbox_get_last_internal_error(box, NULL));
			}
		} else {
			pubid = value.value;
			ret = 1;
		}
	} else
		ret = 1;

	/* try to open key */
	if (ret > 0)
		ret = mail_crypt_get_private_key(box, pubid, TRUE, FALSE,
						 key_r, error_r);
	mailbox_free(&box);
	return ret;
}

int mail_crypt_box_get_private_key(struct mailbox *box,
				   struct dcrypt_private_key **key_r,
				   const char **error_r)
{
	struct mail_attribute_value value;
	int ret;
	/* get active key */
	if ((ret = mailbox_attribute_get(box, MAIL_ATTRIBUTE_TYPE_SHARED,
					 BOX_CRYPT_PREFIX ACTIVE_KEY_NAME, &value)) <= 0) {
		if (ret < 0) {
			*error_r = t_strdup_printf("mailbox_attribute_get(%s, /shared/%s) failed: %s",
						   mailbox_get_vname(box),
						   USER_CRYPT_PREFIX ACTIVE_KEY_NAME,
						   mailbox_get_last_internal_error(box, NULL));
		}
		return ret;
	}

	return mail_crypt_get_private_key(box, value.value,
					  FALSE, FALSE,
					  key_r, error_r);
}

static
int mail_crypt_set_private_key(struct mailbox_transaction_context *t,
				bool user_key, bool shared, const char *pubid,
				struct dcrypt_public_key *enc_key,
				struct dcrypt_private_key *key,
				const char **error_r)
{
	/* folder keys must be encrypted with some other key,
	   unless they are shared keys */
	i_assert(user_key || shared || enc_key != NULL);

	buffer_t *data = t_str_new(MAIL_CRYPT_KEY_BUF_SIZE);
	const char *pw = NULL;
	const char *algo = NULL;
	struct mail_user *user = mail_storage_get_user(
					mailbox_get_storage(
						mailbox_transaction_get_mailbox(t)));
	struct mail_crypt_user *muser = mail_crypt_get_mail_crypt_user(user);
	const char *attr_name = mail_crypt_get_key_path(user_key, FALSE, pubid);
	struct mail_attribute_value value;
	int ret;

	if (enc_key != NULL) {
		algo = MAIL_CRYPT_KEY_CIPHER;
	} else if (user_key && muser->set->crypt_user_key_password[0] != '\0') {
		pw = muser->set->crypt_user_key_password;
		algo = MAIL_CRYPT_PW_CIPHER;
	}

	/* export key */
	if (!dcrypt_key_store_private(key, MAIL_CRYPT_KEY_ATTRIBUTE_FORMAT,
				      algo, data, pw, enc_key, error_r)) {
		return -1;
	}

	/* store it */
	value.value_stream = NULL;
	value.value = str_c(data);
	value.last_change = 0;

	if ((ret = mailbox_attribute_set(t,
					 shared ? MAIL_ATTRIBUTE_TYPE_SHARED :
						  MAIL_ATTRIBUTE_TYPE_PRIVATE,
					 attr_name,
					 &value)) < 0) {
		*error_r = t_strdup_printf("mailbox_attribute_set(%s, %s/%s) failed: %s",
			   mailbox_get_vname(mailbox_transaction_get_mailbox(t)),
			   shared ? "/shared" : "/priv",
			   attr_name,
			   mailbox_get_last_internal_error(
				mailbox_transaction_get_mailbox(t), NULL));
	}

	safe_memset(buffer_get_modifiable_data(data, NULL), 0, data->used);

	return ret;
}

int mail_crypt_user_set_private_key(struct mail_user *user, const char *pubid,
				    struct dcrypt_private_key *key,
				    const char **error_r)
{
	struct mail_crypt_user *muser = mail_crypt_get_mail_crypt_user(user);
	struct dcrypt_private_key *env_key = NULL;
	struct dcrypt_public_key *enc_key = NULL;
	struct mailbox_transaction_context *t;
	int ret;

	if ((ret = mail_crypt_env_get_private_key(user, NULL, &env_key,
						  error_r)) < 0) {
		return -1;
	} else if (ret > 0) {
		dcrypt_key_convert_private_to_public(env_key, &enc_key);
		dcrypt_key_unref_private(&env_key);
	}

	if (muser->set->crypt_user_key_require_encrypted &&
	    muser->set->crypt_user_key_password[0] == '\0' &&
	    (!array_is_created(&muser->set->crypt_user_key_encryption_keys) ||
	     array_is_empty(&muser->set->crypt_user_key_encryption_keys))) {
		*error_r = "crypt_user_key_require_encrypted set, cannot "
			   "generate user keypair without password or key";
		return -1;
	}

	struct mail_namespace *ns = mail_namespace_find_inbox(user->namespaces);
	struct mailbox *box = mailbox_alloc(ns->list, "INBOX",
					    MAILBOX_FLAG_READONLY);
	if (mailbox_open(box) < 0) {
		*error_r = t_strdup_printf("mailbox_open(%s) failed: %s",
					   "INBOX",
					   mailbox_get_last_internal_error(box, NULL));
		return -1;
	}

	t = mailbox_transaction_begin(box, 0, __func__);

	if ((ret = mail_crypt_set_private_key(t, TRUE, FALSE, pubid, enc_key, key,
					      error_r)) < 0) {
		mailbox_transaction_rollback(&t);
	} else if ((ret = mailbox_transaction_commit(&t)) < 0) {
		*error_r = t_strdup_printf("mailbox_transaction_commit(%s) failed: %s",
					  mailbox_get_vname(box),
					  mailbox_get_last_internal_error(box, NULL));
	}

	mailbox_free(&box);

	return ret;
}

int mail_crypt_box_set_private_key(struct mailbox *box, const char *pubid,
				   struct dcrypt_private_key *key,
				   struct dcrypt_public_key *user_key,
				   const char **error_r)
{
	int ret;
	struct mailbox_transaction_context *t;

	t = mailbox_transaction_begin(box, 0, __func__);
	if ((ret = mail_crypt_set_private_key(t, FALSE, FALSE, pubid, user_key,
					      key, error_r)) < 0) {
		mailbox_transaction_rollback(&t);
	} else if ((ret = mailbox_transaction_commit(&t)) < 0) {
		*error_r = t_strdup_printf("mailbox_transaction_commit(%s) failed: %s",
					  mailbox_get_vname(box),
					  mailbox_get_last_internal_error(box, NULL));
	}

	return ret;
}

static
int mail_crypt_get_public_key(struct mailbox *box, const char *pubid,
			      bool user_key, struct dcrypt_public_key **key_r,
			      const char **error_r)
{
	struct mail_user *user = mail_storage_get_user(mailbox_get_storage(box));
	struct mail_crypt_user *muser = mail_crypt_get_mail_crypt_user(user);

	/* check cache */
	if (mail_crypt_get_key_cache(muser->key_cache, pubid, NULL, key_r) > 0) {
		return 1;
	}

	enum dcrypt_key_kind key_kind;
	const char *key_hash = NULL;
	struct dcrypt_public_key *key;
	struct mail_attribute_value value;
	int ret;
	const char *attr_name = mail_crypt_get_key_path(user_key, TRUE, pubid);

	if ((ret = mailbox_attribute_get(box,
					 MAIL_ATTRIBUTE_TYPE_SHARED,
					 attr_name, &value)) <= 0) {
		if (ret < 0) {
			*error_r = t_strdup_printf("mailbox_attribute_get(%s, %s) failed: %s",
						   mailbox_get_vname(box),
						   attr_name,
						   mailbox_get_last_internal_error(box, NULL));
		}
		return ret;
	}

	if (!dcrypt_key_string_get_info(value.value, NULL, NULL, &key_kind,
					NULL, NULL, &key_hash, error_r)) {
		return -1;
	}

	if (key_kind != DCRYPT_KEY_KIND_PUBLIC) {
		*error_r = t_strdup_printf("Cannot use key %s: "
					   "Expected public key, got private key",
					   pubid);
		return -1;
	}

	if (key_hash != NULL && strcmp(key_hash, pubid) != 0) {
		*error_r = t_strdup_printf("Cannot use key %s: "
					   "Incorrect key hash %s stored",
					   pubid, key_hash);
		return -1;
	}

	/* load the key */
	if (!dcrypt_key_load_public(&key, value.value, error_r)) {
		return -1;
	}

	if (pubid != NULL &&
	    mail_crypt_public_key_id_match(key, pubid, error_r) <= 0) {
		dcrypt_key_unref_public(&key);
		return -1;
	}

	mail_crypt_put_key_cache(&muser->key_cache, pubid, NULL, key);

	*key_r = key;

	return 1;
}

int mail_crypt_user_get_public_key(struct mail_user *user,
				   struct dcrypt_public_key **key_r,
				   const char **error_r)
{
	struct mail_namespace *ns = mail_namespace_find_inbox(user->namespaces);
	struct mailbox *box = mailbox_alloc(ns->list, "INBOX",
					    MAILBOX_FLAG_READONLY);
	struct mail_attribute_value value;
	int ret;

	/* try retrieve currently active user key */
	if (mailbox_open(box) < 0) {
		*error_r = t_strdup_printf("mailbox_open(%s) failed: %s",
					   "INBOX",
					   mailbox_get_last_internal_error(box, NULL));
		return -1;
	}

	if ((ret = mailbox_attribute_get(box, MAIL_ATTRIBUTE_TYPE_SHARED,
					 USER_CRYPT_PREFIX ACTIVE_KEY_NAME,
					 &value)) <= 0) {
		if (ret < 0) {
			*error_r = t_strdup_printf("mailbox_attribute_get(%s, /shared/%s) failed: %s",
						   mailbox_get_vname(box),
						   USER_CRYPT_PREFIX ACTIVE_KEY_NAME,
						   mailbox_get_last_internal_error(box, NULL));
		}
	} else {
		ret = mail_crypt_get_public_key(box, value.value, TRUE, key_r, error_r);
	}

	mailbox_free(&box);
	return ret;
}

int mail_crypt_box_get_public_key(struct mailbox *box,
				  struct dcrypt_public_key **key_r,
				  const char **error_r)
{
	struct mail_attribute_value value;
	int ret;

	if ((ret = mailbox_attribute_get(box, MAIL_ATTRIBUTE_TYPE_SHARED,
					 BOX_CRYPT_PREFIX ACTIVE_KEY_NAME,
					 &value)) <= 0) {
		if (ret < 0) {
			*error_r = t_strdup_printf("mailbox_attribute_get(%s, /shared/%s) failed: %s",
						   mailbox_get_vname(box),
						   BOX_CRYPT_PREFIX ACTIVE_KEY_NAME,
						   mailbox_get_last_internal_error(box, NULL));
		}
		return ret;
	}
	return mail_crypt_get_public_key(box, value.value, FALSE, key_r, error_r);
}

static
int mail_crypt_set_public_key(struct mailbox_transaction_context *t,
			      bool user_key, const char *pubid,
			      struct dcrypt_public_key *key,
			      const char **error_r)
{
	buffer_t *data = t_str_new(MAIL_CRYPT_KEY_BUF_SIZE);
	const char *attr_name = mail_crypt_get_key_path(user_key, TRUE, pubid);
	struct mail_attribute_value value;

	/* export key */
	if (!dcrypt_key_store_public(key, MAIL_CRYPT_KEY_ATTRIBUTE_FORMAT, data,
				     error_r)) {
		return -1;
	}

	/* store it */
	value.value_stream = NULL;
	value.value = str_c(data);
	value.last_change = 0;

	if (mailbox_attribute_set(t,
				  MAIL_ATTRIBUTE_TYPE_SHARED,
				  attr_name,
				  &value) < 0) {
		*error_r = t_strdup_printf("mailbox_attribute_set(%s, %s/%s) failed: %s",
			   mailbox_get_vname(mailbox_transaction_get_mailbox(t)),
			   "/shared",
			   attr_name,
			   mailbox_get_last_internal_error(
				mailbox_transaction_get_mailbox(t), NULL));
		return -1;
	}

	return 0;
}

int mail_crypt_user_set_public_key(struct mail_user *user, const char *pubid,
				  struct dcrypt_public_key *key,
				  const char **error_r)
{
	struct mail_namespace *ns = mail_namespace_find_inbox(user->namespaces);
	struct mailbox *box = mailbox_alloc(ns->list, "INBOX",
					    MAILBOX_FLAG_READONLY);
	struct mailbox_transaction_context *t;
	struct mail_attribute_value value;
	int ret;

	/* try retrieve currently active user key */
	if (mailbox_open(box) < 0) {
		*error_r = t_strdup_printf("mailbox_open(%s) failed: %s",
					   "INBOX",
					   mailbox_get_last_internal_error(box, NULL));
		return -1;
	}

	t = mailbox_transaction_begin(box, 0, __func__);

	if ((ret = mail_crypt_set_public_key(t, TRUE, pubid, key,
					     error_r)) == 0) {
		value.value_stream = NULL;
		value.value = pubid;
		value.last_change = 0;

		if ((ret = mailbox_attribute_set(t, MAIL_ATTRIBUTE_TYPE_SHARED,
						 USER_CRYPT_PREFIX ACTIVE_KEY_NAME,
						 &value)) < 0) {
			*error_r = t_strdup_printf("mailbox_attribute_set(%s, /shared/%s) failed: %s",
						   mailbox_get_vname(box),
						   USER_CRYPT_PREFIX ACTIVE_KEY_NAME,
						   mailbox_get_last_internal_error(box, NULL));
		}
	}

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

	mailbox_free(&box);

	return ret;
}

int mail_crypt_box_set_public_key(struct mailbox *box, const char *pubid,
				  struct dcrypt_public_key *key,
				  const char **error_r)
{
	int ret;
	struct mailbox_transaction_context *t;
	struct mail_attribute_value value;

	t = mailbox_transaction_begin(box, 0, __func__);
	if ((ret = mail_crypt_set_public_key(t, FALSE, pubid, key,
					     error_r)) == 0) {
		value.value_stream = NULL;
		value.value = pubid;
		value.last_change = 0;

		if ((ret = mailbox_attribute_set(t, MAIL_ATTRIBUTE_TYPE_SHARED,
						 BOX_CRYPT_PREFIX ACTIVE_KEY_NAME,
						 &value)) < 0) {
			*error_r = t_strdup_printf("mailbox_attribute_set(%s, /shared/%s) failed: %s",
						   mailbox_get_vname(box),
						   BOX_CRYPT_PREFIX ACTIVE_KEY_NAME,
						   mailbox_get_last_internal_error(box, NULL));
		}
	}

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

	return ret;

}

static
int mail_crypt_user_set_keys(struct mail_user *user,
			     const char *pubid,
			     struct dcrypt_private_key *privkey,
			     struct dcrypt_public_key *pubkey,
			     const char **error_r)
{
	if (mail_crypt_user_set_private_key(user, pubid, privkey, error_r) < 0)
		return -1;
	if (mail_crypt_user_set_public_key(user, pubid, pubkey, error_r) < 0)
		return -1;
	return 0;
}

static
int mail_crypt_box_set_keys(struct mailbox *box,
			    const char *pubid,
			    struct dcrypt_private_key *privkey,
			    struct dcrypt_public_key *user_key,
			    struct dcrypt_public_key *pubkey,
			    const char **error_r)
{
	if (mail_crypt_box_set_private_key(box, pubid, privkey, user_key,
					   error_r) < 0)
		return -1;
	if (mail_crypt_box_set_public_key(box, pubid, pubkey, error_r) < 0)
		return -1;
	return 0;
}

int mail_crypt_box_get_shared_key(struct mailbox *box,
				  const char *pubid,
				  struct dcrypt_private_key **key_r,
				  const char **error_r)
{
	struct mail_user *user = mail_storage_get_user(mailbox_get_storage(box));
	struct mail_crypt_user *muser = mail_crypt_get_mail_crypt_user(user);

	struct dcrypt_private_key *key = NULL;
	struct mail_attribute_value value;
	int ret;

	/* check cache */
	if (mail_crypt_get_key_cache(muser->key_cache, pubid, key_r, NULL) > 0) {
		return 1;
	}

	const char *hexname =
		binary_to_hex((const unsigned char*)user->username,
			      strlen(user->username));

	const char *attr_name = t_strdup_printf(BOX_CRYPT_PREFIX
						PRIVKEYS_PREFIX"%s/%s",
						hexname,
						pubid);

	if ((ret = mailbox_attribute_get(box,
					MAIL_ATTRIBUTE_TYPE_SHARED,
					attr_name, &value)) <= 0) {
		if (ret < 0) {
			*error_r = t_strdup_printf("mailbox_attribute_get(%s, %s) failed: %s",
						mailbox_get_vname(box),
						attr_name,
						mailbox_get_last_internal_error(box, NULL));
			return ret;
		}
		return mail_crypt_get_private_key(box, pubid, FALSE, TRUE, key_r,
						  error_r);
	} else {
		if ((ret = mail_crypt_decrypt_private_key(box, pubid, value.value,
							  &key, error_r)) <= 0)
			return ret;
	}

	mail_crypt_put_key_cache(&muser->key_cache, pubid, key, NULL);

	*key_r = key;

	return 1;
}

int mail_crypt_box_set_shared_key(struct mailbox_transaction_context *t,
				  const char *pubid,
				  struct dcrypt_private_key *privkey,
				  const char *target_uid,
				  struct dcrypt_public_key *user_key,
				  const char **error_r)
{
	struct mail_attribute_value value;
	buffer_t *data = t_str_new(MAIL_CRYPT_KEY_BUF_SIZE);
	int ret;
	const char *attr_name;
	const char *algo = NULL;

	i_assert(target_uid == NULL || user_key != NULL);

	if (target_uid != NULL) {
		/* hash target UID */
		algo = MAIL_CRYPT_KEY_CIPHER;
		const char *hexname =
			binary_to_hex((const unsigned char*)target_uid,
				      strlen(target_uid));
		attr_name = t_strdup_printf(BOX_CRYPT_PREFIX
					    PRIVKEYS_PREFIX"%s/%s",
					    hexname,
					    pubid);
	} else {
		attr_name = t_strdup_printf(BOX_CRYPT_PREFIX
					    PRIVKEYS_PREFIX"%s",
					    pubid);
	}

	if (!dcrypt_key_store_private(privkey, DCRYPT_FORMAT_DOVECOT,
				      algo, data,
				      NULL, user_key, error_r)) {
		return -1;
	}

	value.value_stream = NULL;
	value.value = str_c(data);
	value.last_change = 0;

	if ((ret = mailbox_attribute_set(t, MAIL_ATTRIBUTE_TYPE_SHARED,
					 attr_name, &value)) < 0) {
		*error_r = t_strdup_printf("mailbox_attribute_set(%s, /shared/%s) failed: %s",
					   mailbox_get_vname(
						mailbox_transaction_get_mailbox(t)),
					   attr_name,
					   mailbox_get_last_internal_error(
						mailbox_transaction_get_mailbox(t),
						NULL));
	}

	safe_memset(buffer_get_modifiable_data(data, NULL), 0, data->used);

	return ret;
}

int mail_crypt_box_unset_shared_key(struct mailbox_transaction_context *t,
				    const char *pubid,
				    const char *target_uid,
				    const char **error_r)
{
	int ret;

	const char *hexname =
		binary_to_hex((const unsigned char*)target_uid,
			      strlen(target_uid));

	const char *attr_name = t_strdup_printf(BOX_CRYPT_PREFIX
						PRIVKEYS_PREFIX"%s/%s",
						hexname,
						pubid);

	if ((ret = mailbox_attribute_unset(t, MAIL_ATTRIBUTE_TYPE_SHARED,
					   attr_name)) <= 0) {
		if (ret < 0) {
			 *error_r = t_strdup_printf("mailbox_attribute_unset(%s, "
						    " /shared/%s): failed: %s",
						    mailbox_get_vname(
						    mailbox_transaction_get_mailbox(t)),
						    attr_name,
						    mailbox_get_last_internal_error(
						    mailbox_transaction_get_mailbox(t),
						    NULL));
		}
	}

	return ret;
}

static
int mail_crypt_generate_keypair(const char *curve,
				struct dcrypt_keypair *pair_r,
				const char **pubid_r,
				const char **error_r)
{
	if (curve[0] == '\0') {
		*error_r = "crypt_user_key_curve not set, cannot generate EC key";
		return -1;
	}

	if (!dcrypt_keypair_generate(pair_r, DCRYPT_KEY_EC, 0, curve, error_r)) {
		return -1;
	}

	buffer_t *key_id = t_str_new(MAIL_CRYPT_HASH_BUF_SIZE);
	if (!dcrypt_key_id_public(pair_r->pub, MAIL_CRYPT_KEY_ID_ALGORITHM, key_id,
				  error_r)) {
		dcrypt_keypair_unref(pair_r);
		return -1;
	}

	*pubid_r = binary_to_hex(key_id->data, key_id->used);

	return 0;
}

int mail_crypt_user_generate_keypair(struct mail_user *user,
				     struct dcrypt_keypair *pair,
				     const char **pubid_r,
				     const char **error_r)
{
	struct mail_crypt_user *muser = mail_crypt_get_mail_crypt_user(user);

	if (mail_crypt_generate_keypair(muser->set->crypt_user_key_curve,
					pair, pubid_r, error_r) < 0) {
		return -1;
	}

	if (mail_crypt_user_set_keys(user, *pubid_r,
				     pair->priv, pair->pub, error_r) < 0) {
		dcrypt_keypair_unref(pair);
		return -1;
	}

	mail_crypt_put_key_cache(&muser->key_cache, *pubid_r, pair->priv, pair->pub);

	return 0;
}

int mail_crypt_box_generate_keypair(struct mailbox *box,
				    struct dcrypt_keypair *pair,
				    struct dcrypt_public_key *user_key,
				    const char **pubid_r,
				    const char **error_r)
{
	int ret;
	struct mail_user *user = mail_storage_get_user(mailbox_get_storage(box));
	struct mail_crypt_user *muser = mail_crypt_get_mail_crypt_user(user);

	if (user_key == NULL) {
		if ((ret = mail_crypt_user_get_public_key(user,
							  &user_key,
							  error_r)) <= 0) {
			if (ret < 0)
				return ret;
			/* generate keypair */
			struct dcrypt_keypair user_pair;
			const char *user_pubid;
			if (mail_crypt_user_generate_keypair(user, &user_pair,
							     &user_pubid,
							     error_r) < 0) {
				return -1;
			}

			mail_crypt_put_key_cache(&muser->key_cache, user_pubid,
						 user_pair.priv, user_pair.pub);

			user_key = user_pair.pub;
			dcrypt_key_unref_private(&user_pair.priv);
		}
	} else {
		dcrypt_key_ref_public(user_key);
	}

	ret = mail_crypt_generate_keypair(muser->set->crypt_user_key_curve,
					  pair, pubid_r, error_r);
	if (ret < 0) {
		/* failed */
	} else if ((ret = mail_crypt_box_set_keys(box, *pubid_r,
						pair->priv, user_key, pair->pub,
						error_r)) < 0) {
		dcrypt_keypair_unref(pair);
	} else {
		mail_crypt_put_key_cache(&muser->key_cache, *pubid_r, pair->priv,
					 pair->pub);
	}

	dcrypt_key_unref_public(&user_key);

	return ret;
}

int mail_crypt_box_get_pvt_digests(struct mailbox *box, pool_t pool,
				   enum mail_attribute_type type,
				   ARRAY_TYPE(const_string) *digests,
				   const char **error_r)
{
	struct mailbox_attribute_iter *iter;
	const char *key;
	int ret;

	iter = mailbox_attribute_iter_init(box, type,
					   BOX_CRYPT_PREFIX PRIVKEYS_PREFIX);
	while ((key = mailbox_attribute_iter_next(iter)) != NULL) {
		key = p_strdup(pool, key);
		array_push_back(digests, &key);
	}
	ret = mailbox_attribute_iter_deinit(&iter);
	if (ret < 0)
		*error_r = mailbox_get_last_internal_error(box, NULL);
	return ret;
}

int mail_crypt_box_get_private_keys(struct mailbox *box,
				    ARRAY_TYPE(dcrypt_private_key) *keys_r,
				    const char **error_r)
{
	struct mailbox_attribute_iter *iter;
	iter = mailbox_attribute_iter_init(box, MAIL_ATTRIBUTE_TYPE_PRIVATE,
					    BOX_CRYPT_PREFIX PRIVKEYS_PREFIX);
	const char *id;
	int ret;

	while ((id = mailbox_attribute_iter_next(iter)) != NULL) {
		struct dcrypt_private_key *key = NULL;
		if ((ret = mail_crypt_get_private_key(box, id, FALSE, FALSE,
						      &key, error_r)) < 0) {
			(void)mailbox_attribute_iter_deinit(&iter);
			return -1;
		} else if (ret > 0)
			array_push_back(keys_r, &key);
	}

	ret = mailbox_attribute_iter_deinit(&iter);
	if (ret < 0)
		*error_r = mailbox_get_last_internal_error(box, NULL);
	return ret;
}

int mail_crypt_box_share_private_keys(struct mailbox_transaction_context *t,
				      struct dcrypt_public_key *dest_pub_key,
				      const char *dest_user,
				      const ARRAY_TYPE(dcrypt_private_key) *priv_keys,
				      const char **error_r)
{
	i_assert(dest_user == NULL || dest_pub_key != NULL);

	struct dcrypt_private_key *priv_key;
	buffer_t *key_id = t_str_new(MAIL_CRYPT_HASH_BUF_SIZE);
	int ret = 0;

	array_foreach_elem(priv_keys, priv_key) {
		ret = -1;
		if (!dcrypt_key_id_private(priv_key, MAIL_CRYPT_KEY_ID_ALGORITHM,
					   key_id, error_r) ||
		    (ret = mail_crypt_box_set_shared_key(t,
							 binary_to_hex(key_id->data,
									key_id->used),
							 priv_key, dest_user,
							 dest_pub_key, error_r)) < 0)
			break;
	}

	return ret;
}

int
mail_crypt_user_get_or_gen_public_key(struct mail_user *user,
				      struct dcrypt_public_key **pub_r,
				      const char **error_r)
{
	i_assert(user != NULL);
	i_assert(pub_r != NULL);
	i_assert(error_r != NULL);

	int ret;
	if ((ret = mail_crypt_user_get_public_key(user, pub_r, error_r)) == 0) {
		struct dcrypt_keypair pair;
		const char *pubid = NULL;
		if (mail_crypt_user_generate_keypair(user, &pair,
						     &pubid, error_r) < 0) {
			return -1;
		}
		*pub_r = pair.pub;
		dcrypt_key_unref_private(&pair.priv);
	} else
		return ret;
	return 0;
}

static const struct mailbox_attribute_internal mailbox_internal_attributes[] = {
	{ .type = MAIL_ATTRIBUTE_TYPE_PRIVATE,
	  .key = BOX_CRYPT_PREFIX,
	  .flags = MAIL_ATTRIBUTE_INTERNAL_FLAG_CHILDREN
	},
	{ .type = MAIL_ATTRIBUTE_TYPE_SHARED,
	  .key = BOX_CRYPT_PREFIX,
	  .flags = MAIL_ATTRIBUTE_INTERNAL_FLAG_CHILDREN
	},
	{ .type = MAIL_ATTRIBUTE_TYPE_PRIVATE,
	  .key = USER_CRYPT_PREFIX,
	  .flags = MAIL_ATTRIBUTE_INTERNAL_FLAG_CHILDREN
	},
	{ .type = MAIL_ATTRIBUTE_TYPE_SHARED,
	  .key = USER_CRYPT_PREFIX,
	  .flags = MAIL_ATTRIBUTE_INTERNAL_FLAG_CHILDREN
	}
};

void mail_crypt_key_register_mailbox_internal_attributes(void)
{
	mailbox_attribute_register_internals(mailbox_internal_attributes,
		N_ELEMENTS(mailbox_internal_attributes));
}
