diff options
Diffstat (limited to 'src/plugins/mail-crypt/mail-crypt-plugin.c')
-rw-r--r-- | src/plugins/mail-crypt/mail-crypt-plugin.c | 501 |
1 files changed, 501 insertions, 0 deletions
diff --git a/src/plugins/mail-crypt/mail-crypt-plugin.c b/src/plugins/mail-crypt/mail-crypt-plugin.c new file mode 100644 index 0000000..69c3b59 --- /dev/null +++ b/src/plugins/mail-crypt/mail-crypt-plugin.c @@ -0,0 +1,501 @@ +/* Copyright (c) 2015-2018 Dovecot authors, see the included COPYING file */ + +/* FIXME: cache handling could be useful to move to Dovecot core, so that if + we're using this plugin together with zlib plugin there would be just one + cache. */ + +#include "lib.h" +#include "ioloop.h" +#include "randgen.h" +#include "module-dir.h" +#include "str.h" +#include "safe-mkstemp.h" +#include "istream.h" +#include "istream-decrypt.h" +#include "istream-seekable.h" +#include "ostream.h" +#include "ostream-encrypt.h" +#include "mail-user.h" +#include "mail-copy.h" +#include "index-storage.h" +#include "index-mail.h" +#include "mail-crypt-common.h" +#include "mail-crypt-key.h" +#include "mail-crypt-plugin.h" +#include "sha2.h" +#include "dcrypt-iostream.h" +#include "hex-binary.h" + +struct mail_crypt_mailbox { + union mailbox_module_context module_ctx; + struct dcrypt_public_key *pub_key; +}; + +const char *mail_crypt_plugin_version = DOVECOT_ABI_VERSION; + +#define MAIL_CRYPT_MAIL_CONTEXT(obj) \ + MODULE_CONTEXT_REQUIRE(obj, mail_crypt_mail_module) +#define MAIL_CRYPT_CONTEXT(obj) \ + MODULE_CONTEXT_REQUIRE(obj, mail_crypt_storage_module) +#define MAIL_CRYPT_USER_CONTEXT(obj) \ + MODULE_CONTEXT(obj, mail_crypt_user_module) +#define MAIL_CRYPT_USER_CONTEXT_REQUIRE(obj) \ + MODULE_CONTEXT_REQUIRE(obj, mail_crypt_user_module) + +static MODULE_CONTEXT_DEFINE_INIT(mail_crypt_user_module, + &mail_user_module_register); +static MODULE_CONTEXT_DEFINE_INIT(mail_crypt_storage_module, + &mail_storage_module_register); +static MODULE_CONTEXT_DEFINE_INIT(mail_crypt_mail_module, + &mail_module_register); + +struct mail_crypt_user *mail_crypt_get_mail_crypt_user(struct mail_user *user) +{ + return MAIL_CRYPT_USER_CONTEXT(user); +} + +static bool mail_crypt_is_stream_encrypted(struct istream *input) +{ + const unsigned char *data = NULL; + size_t size; + + if (i_stream_read_data(input, &data, &size, + sizeof(IOSTREAM_CRYPT_MAGIC)) <= 0) { + return FALSE; + } + + if (memcmp(data, IOSTREAM_CRYPT_MAGIC, + sizeof(IOSTREAM_CRYPT_MAGIC)) != 0) { + return FALSE; + } + return TRUE; +} + +static void mail_crypt_cache_close(struct mail_crypt_user *muser) +{ + struct mail_crypt_cache *cache = &muser->cache; + + timeout_remove(&cache->to); + i_stream_unref(&cache->input); + i_zero(cache); +} + +static struct istream * +mail_crypt_cache_open(struct mail_crypt_user *muser, struct mail *mail, + struct istream *input) +{ + struct mail_crypt_cache *cache = &muser->cache; + struct istream *inputs[2]; + string_t *temp_prefix = t_str_new(128); + + mail_crypt_cache_close(muser); + + input->seekable = FALSE; + inputs[0] = input; + inputs[1] = NULL; + mail_user_set_get_temp_prefix(temp_prefix, mail->box->storage->user->set); + input = i_stream_create_seekable_path(inputs, + i_stream_get_max_buffer_size(inputs[0]), + str_c(temp_prefix)); + i_stream_unref(&inputs[0]); + + if (mail->uid > 0) { + cache->to = timeout_add(MAIL_CRYPT_MAIL_CACHE_EXPIRE_MSECS, + mail_crypt_cache_close, muser); + cache->box = mail->box; + cache->uid = mail->uid; + cache->input = input; + /* index-mail wants the stream to be destroyed at close, so create + a new stream instead of just increasing reference. */ + return i_stream_create_limit(cache->input, UOFF_T_MAX); + } + + return input; +} + +static int mail_crypt_istream_get_private_key(const char *pubkey_digest, + struct dcrypt_private_key **priv_key_r, + const char **error_r, + void *context) +{ + /* mailbox_crypt_search_all_private_keys requires error_r != NULL */ + i_assert(error_r != NULL); + int ret; + struct mail *_mail = context; + struct mail_crypt_user *muser = + MAIL_CRYPT_USER_CONTEXT_REQUIRE(_mail->box->storage->user); + + *priv_key_r = mail_crypt_global_key_find(&muser->global_keys, + pubkey_digest); + if (*priv_key_r != NULL) { + dcrypt_key_ref_private(*priv_key_r); + return 1; + } + + struct mail_namespace *ns = mailbox_get_namespace(_mail->box); + + if (ns->type == MAIL_NAMESPACE_TYPE_SHARED) { + ret = mail_crypt_box_get_shared_key(_mail->box, pubkey_digest, + priv_key_r, error_r); + /* priv_key_r is already referenced */ + } else if (ns->type != MAIL_NAMESPACE_TYPE_PUBLIC) { + ret = mail_crypt_get_private_key(_mail->box, pubkey_digest, + FALSE, FALSE, priv_key_r, + error_r); + /* priv_key_r is already referenced */ + } else { + *error_r = "Public emails cannot have keys"; + ret = -1; + } + + i_assert(ret <= 0 || *priv_key_r != NULL); + + return ret; +} + +static int +mail_crypt_istream_opened(struct mail *_mail, struct istream **stream) +{ + struct mail_private *mail = (struct mail_private *)_mail; + struct mail_user *user = _mail->box->storage->user; + struct mail_crypt_user *muser = MAIL_CRYPT_USER_CONTEXT_REQUIRE(user); + struct mail_crypt_cache *cache = &muser->cache; + union mail_module_context *mmail = MAIL_CRYPT_MAIL_CONTEXT(mail); + struct istream *input; + + if (_mail->uid > 0 && cache->uid == _mail->uid && cache->box == _mail->box) { + /* use the cached stream. when doing partial reads it should + already be seeked into the wanted offset. */ + i_stream_unref(stream); + i_stream_seek(cache->input, 0); + *stream = i_stream_create_limit(cache->input, UOFF_T_MAX); + return mmail->super.istream_opened(_mail, stream); + } + + /* decryption is the outmost stream, so add it before others + (e.g. zlib) */ + if (!mail_crypt_is_stream_encrypted(*stream)) + return mmail->super.istream_opened(_mail, stream); + + input = *stream; + *stream = i_stream_create_decrypt_callback(input, + mail_crypt_istream_get_private_key, _mail); + i_stream_unref(&input); + + *stream = mail_crypt_cache_open(muser, _mail, *stream); + return mmail->super.istream_opened(_mail, stream); +} + +static void mail_crypt_close(struct mail *_mail) +{ + struct mail_private *mail = (struct mail_private *)_mail; + union mail_module_context *mmail = MAIL_CRYPT_MAIL_CONTEXT(mail); + struct mail_crypt_user *muser = + MAIL_CRYPT_USER_CONTEXT_REQUIRE(_mail->box->storage->user); + struct mail_crypt_cache *cache = &muser->cache; + uoff_t size; + + if (_mail->uid > 0 && cache->uid == _mail->uid && cache->box == _mail->box) { + /* make sure we have read the entire email into the seekable + stream (which causes the original input stream to be + unrefed). we can't safely keep the original input stream + open after the mail is closed. */ + if (i_stream_get_size(cache->input, TRUE, &size) < 0) + mail_crypt_cache_close(muser); + } + mmail->super.close(_mail); +} + +static void mail_crypt_mail_allocated(struct mail *_mail) +{ + struct mail_crypt_user *muser = + MAIL_CRYPT_USER_CONTEXT(_mail->box->storage->user); + if (muser == NULL) return; + + struct mail_private *mail = (struct mail_private *)_mail; + struct mail_vfuncs *v = mail->vlast; + union mail_module_context *mmail; + + mmail = p_new(mail->pool, union mail_module_context, 1); + mmail->super = *v; + mail->vlast = &mmail->super; + + v->istream_opened = mail_crypt_istream_opened; + v->close = mail_crypt_close; + MODULE_CONTEXT_SET_SELF(mail, mail_crypt_mail_module, mmail); +} + +static int mail_crypt_mail_save_finish(struct mail_save_context *ctx) +{ + struct mailbox *box = ctx->transaction->box; + union mailbox_module_context *zbox = MAIL_CRYPT_CONTEXT(box); + struct istream *input; + + if (zbox->super.save_finish(ctx) < 0) + return -1; + + /* we're here only if mail-crypt plugin is disabled. we want to make + sure that even though we're saving an unencrypted mail, the mail + can't be faked to look like an encrypted mail. */ + if (mail_get_stream(ctx->dest_mail, NULL, NULL, &input) < 0) + return -1; + + if (mail_crypt_is_stream_encrypted(input)) { + mail_storage_set_error(box->storage, MAIL_ERROR_NOTPOSSIBLE, + "Saving mails encrypted by client isn't supported"); + return -1; + } + return 0; +} + +static int +mail_crypt_mail_save_begin(struct mail_save_context *ctx, + struct istream *input) +{ + const char *pubid; + struct mailbox *box = ctx->transaction->box; + struct mail_crypt_mailbox *mbox = MAIL_CRYPT_CONTEXT(box); + struct mail_crypt_user *muser = MAIL_CRYPT_USER_CONTEXT(box->storage->user); + + enum io_stream_encrypt_flags enc_flags = 0; + if (muser != NULL) { + if (muser->save_version == 1) { + enc_flags = IO_STREAM_ENC_VERSION_1; + } else if (muser->save_version == 2) { + enc_flags = IO_STREAM_ENC_INTEGRITY_AEAD; + } else { + i_assert(muser->save_version == 0); + } + } + + if (mbox->module_ctx.super.save_begin(ctx, input) < 0) + return -1; + + if (enc_flags == 0) + return 0; + + struct dcrypt_public_key *pub_key; + if (muser->global_keys.public_key != NULL) + pub_key = muser->global_keys.public_key; + else if (mbox->pub_key != NULL) + pub_key = mbox->pub_key; + else { + const char *error; + int ret; + + if ((ret = mail_crypt_box_get_public_key(box, &pub_key, + &error)) <= 0) + { + struct dcrypt_keypair pair; + + if (ret < 0) { + mail_storage_set_error(box->storage, + MAIL_ERROR_PARAMS, + t_strdup_printf("get_public_key(%s) failed: %s", + mailbox_get_vname(box), + error)); + return ret; + } + + if (muser->save_version < 2) { + mail_storage_set_error(box->storage, + MAIL_ERROR_PARAMS, + t_strdup_printf("generate_keypair(%s) failed: " + "unsupported save_version=%d", + mailbox_get_vname(box), + muser->save_version)); + return -1; + } + + if (mail_crypt_box_generate_keypair(box, &pair, NULL, + &pubid, &error) < 0) { + mail_storage_set_error(box->storage, + MAIL_ERROR_PARAMS, + t_strdup_printf("generate_keypair(%s) failed: %s", + mailbox_get_vname(box), + error)); + return -1; + } + pub_key = pair.pub; + dcrypt_key_unref_private(&pair.priv); + + } + mbox->pub_key = pub_key; + } + + /* encryption is the outermost layer (zlib etc. are inside) */ + struct ostream *output = o_stream_create_encrypt(ctx->data.output, + MAIL_CRYPT_ENC_ALGORITHM, pub_key, enc_flags); + + o_stream_unref(&ctx->data.output); + ctx->data.output = output; + o_stream_cork(ctx->data.output); + return 0; +} + +static int +mail_crypt_mailbox_copy(struct mail_save_context *ctx, struct mail *mail) +{ + struct mailbox *dest_box = ctx->transaction->box; + struct mail_crypt_mailbox *mbox = MAIL_CRYPT_CONTEXT(dest_box); + struct mail_crypt_user *muser = + MAIL_CRYPT_USER_CONTEXT(dest_box->storage->user); + + bool raw_copy; + if (mailbox_backends_equal(dest_box, mail->box)) { + /* Copy to same box always have identical crypt profile */ + raw_copy = TRUE; + } else if (strcmp(dest_box->storage->user->username, + mail->box->storage->user->username) != 0) { + /* Always consider copies between different users unsafe + as they may have different encryption level and/or + different global keys */ + raw_copy = FALSE; + } else { + /* Within same user, consider safe only the case where + encryption is enabled and keys are global. */ + raw_copy = muser != NULL && + muser->save_version != 0 && + muser->global_keys.public_key != NULL; + } + + return raw_copy ? + mbox->module_ctx.super.copy(ctx, mail) : + mail_storage_copy(ctx, mail); +} + +static void mail_crypt_mailbox_close(struct mailbox *box) +{ + struct mail_crypt_mailbox *mbox = MAIL_CRYPT_CONTEXT(box); + struct mail_crypt_user *muser = + MAIL_CRYPT_USER_CONTEXT(box->storage->user); + + if (mbox->pub_key != NULL) + dcrypt_key_unref_public(&mbox->pub_key); + if (muser != NULL && muser->cache.box == box) + mail_crypt_cache_close(muser); + mbox->module_ctx.super.close(box); +} + +static void mail_crypt_mailbox_allocated(struct mailbox *box) +{ + struct mailbox_vfuncs *v = box->vlast; + struct mail_crypt_user *muser = + MAIL_CRYPT_USER_CONTEXT(box->storage->user); + struct mail_crypt_mailbox *mbox; + enum mail_storage_class_flags class_flags = box->storage->class_flags; + + mbox = p_new(box->pool, struct mail_crypt_mailbox, 1); + mbox->module_ctx.super = *v; + box->vlast = &mbox->module_ctx.super; + v->close = mail_crypt_mailbox_close; + + MODULE_CONTEXT_SET(box, mail_crypt_storage_module, mbox); + + if ((class_flags & MAIL_STORAGE_CLASS_FLAG_BINARY_DATA) != 0) { + v->save_begin = mail_crypt_mail_save_begin; + v->copy = mail_crypt_mailbox_copy; + + if (muser == NULL || muser->save_version == 0) + v->save_finish = mail_crypt_mail_save_finish; + } +} + +static void mail_crypt_mail_user_deinit(struct mail_user *user) +{ + struct mail_crypt_user *muser = MAIL_CRYPT_USER_CONTEXT_REQUIRE(user); + + mail_crypt_key_cache_destroy(&muser->key_cache); + mail_crypt_global_keys_free(&muser->global_keys); + mail_crypt_cache_close(muser); + muser->module_ctx.super.deinit(user); +} + +static void mail_crypt_mail_user_created(struct mail_user *user) +{ + struct mail_user_vfuncs *v = user->vlast; + struct mail_crypt_user *muser; + const char *error = NULL; + + muser = p_new(user->pool, struct mail_crypt_user, 1); + muser->module_ctx.super = *v; + user->vlast = &muser->module_ctx.super; + + const char *curve = mail_user_plugin_getenv(user, "mail_crypt_curve"); + buffer_t *tmp = t_str_new(64); + if (curve == NULL || *curve == '\0') { + e_debug(user->event, "mail_crypt_plugin: mail_crypt_curve setting " + "missing - generating EC keys disabled"); + } else if (!dcrypt_name2oid(curve, tmp, &error)) { + user->error = p_strdup_printf(user->pool, + "mail_crypt_plugin: " + "invalid mail_crypt_curve setting %s: %s", + curve, error); + } else { + muser->curve = p_strdup(user->pool, curve); + } + + const char *version = mail_user_plugin_getenv(user, + "mail_crypt_save_version"); + + if (version == NULL || *version == '\0') { + user->error = p_strdup_printf(user->pool, + "mail_crypt_plugin: " + "mail_crypt_save_version setting missing"); + } else if (version[0] == '0') { + muser->save_version = 0; + } else if (version[0] == '1') { + muser->save_version = 1; + } else if (version[0] == '2') { + muser->save_version = 2; + } else { + user->error = p_strdup_printf(user->pool, + "mail_crypt_plugin: Invalid " + "mail_crypt_save_version %s: use 0, 1, or 2 ", + version); + } + + if (mail_crypt_global_keys_load(user, "mail_crypt_global", + &muser->global_keys, FALSE, &error) < 0) { + user->error = p_strdup_printf(user->pool, + "mail_crypt_plugin: %s", error); + } + + v->deinit = mail_crypt_mail_user_deinit; + MODULE_CONTEXT_SET(user, mail_crypt_user_module, muser); +} + +static struct mail_storage_hooks mail_crypt_mail_storage_hooks = { + .mail_user_created = mail_crypt_mail_user_created, + .mail_allocated = mail_crypt_mail_allocated +}; + +static struct mail_storage_hooks mail_crypt_mail_storage_hooks_post = { + .mailbox_allocated = mail_crypt_mailbox_allocated +}; + +static struct module crypto_post_module = { + .path = "lib95_mail_crypt_plugin.so" +}; + +void mail_crypt_plugin_init(struct module *module) +{ + const char* error; + if (!dcrypt_initialize("openssl", NULL, &error)) + i_fatal("dcrypt_initialize(): %s", error); + mail_storage_hooks_add(module, &mail_crypt_mail_storage_hooks); + /* when this plugin is loaded, there's the potential chance for + mixed delivery between encrypted and non-encrypted recipients. + The mail_crypt_mailbox_allocated() hook ensures encrypted + content isn't copied as such into cleartext recipients + (and the other way around) */ + mail_storage_hooks_add_forced(&crypto_post_module, + &mail_crypt_mail_storage_hooks_post); + mail_crypt_key_register_mailbox_internal_attributes(); +} + +void mail_crypt_plugin_deinit(void) +{ + mail_storage_hooks_remove(&mail_crypt_mail_storage_hooks); + mail_storage_hooks_remove(&mail_crypt_mail_storage_hooks_post); +} |