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

#include "lib.h"
#include "buffer.h"
#include "randgen.h"
#include "safe-memset.h"
#include "hash-method.h"
#include "sha2.h"
#include "memarea.h"
#include "dcrypt.h"
#include "istream.h"
#include "istream-decrypt.h"
#include "istream-private.h"
#include "dcrypt-iostream.h"

#include "hex-binary.h"

#include <arpa/inet.h>

struct decrypt_istream_snapshot {
	struct istream_snapshot snapshot;
	struct decrypt_istream *dstream;
	buffer_t *buf;
};

struct decrypt_istream {
	struct istream_private istream;
	buffer_t *buf;
	bool symmetric;

	i_stream_decrypt_get_key_callback_t *key_callback;
	void *key_context;

	struct dcrypt_private_key *priv_key;
	bool initialized;
	bool finalized;
	bool use_mac;
	bool snapshot_pending;

	uoff_t ftr, pos;
	enum io_stream_encrypt_flags flags;

	/* original iv, in case seeking is done, future feature */
	unsigned char *iv;

	struct dcrypt_context_symmetric *ctx_sym;
	struct dcrypt_context_hmac *ctx_mac;

	enum decrypt_istream_format format;
};

static void i_stream_decrypt_reset(struct decrypt_istream *dstream)
{
	dstream->finalized = FALSE;
	dstream->use_mac = FALSE;

	dstream->ftr = 0;
	dstream->pos = 0;
	dstream->flags = 0;

	if (!dstream->symmetric) {
		dstream->initialized = FALSE;
		if (dstream->ctx_sym != NULL)
			dcrypt_ctx_sym_destroy(&dstream->ctx_sym);
		if (dstream->ctx_mac != NULL)
			dcrypt_ctx_hmac_destroy(&dstream->ctx_mac);
	}
	i_free(dstream->iv);
	dstream->format = DECRYPT_FORMAT_V1;
}

enum decrypt_istream_format
i_stream_encrypt_get_format(const struct istream *input)
{
	return ((const struct decrypt_istream*)input->real_stream)->format;
}

enum io_stream_encrypt_flags
i_stream_encrypt_get_flags(const struct istream *input)
{
	return ((const struct decrypt_istream*)input->real_stream)->flags;
}

static ssize_t
i_stream_decrypt_read_header_v1(struct decrypt_istream *stream,
				const unsigned char *data, size_t mlen)
{
	const char *error = NULL;
	size_t keydata_len = 0;
	uint16_t len;
	int ec, i = 0;

	const unsigned char *digest_pos = NULL, *key_digest_pos = NULL,
		*key_ct_pos = NULL;
	size_t digest_len = 0, key_ct_len = 0, key_digest_size = 0;

	buffer_t ephemeral_key;
	buffer_t *secret = t_buffer_create(256);
	buffer_t *key = t_buffer_create(256);

	if (mlen < 2)
		return 0;
	keydata_len = be16_to_cpu_unaligned(data);
	if (mlen-2 < keydata_len) {
		/* try to read more */
		return 0;
	}

	data+=2;
	mlen-=2;

	while (i < 4 && mlen > 2) {
		memcpy(&len, data, 2);
		len = ntohs(len);
		if (len == 0 || len > mlen-2)
			break;
		data += 2;
		mlen -= 2;

		switch(i++) {
		case 0:
			buffer_create_from_const_data(&ephemeral_key,
						      data, len);
			break;
		case 1:
			/* public key id */
			digest_pos = data;
			digest_len = len;
			break;
		case 2:
			/* encryption key digest */
			key_digest_pos = data;
			key_digest_size = len;
			break;
		case 3:
			/* encrypted key data */
			key_ct_pos = data;
			key_ct_len = len;
			break;
		}
		data += len;
		mlen -= len;
	}

	if (i < 4) {
		io_stream_set_error(&stream->istream.iostream,
				    "Invalid or corrupted header");
		/* was it consumed? */
		stream->istream.istream.stream_errno =
			mlen > 2 ? EIO : EPIPE;
		return -1;
	}

	/* we don't have a private key */
	if (stream->priv_key == NULL) {
		/* see if we can get one */
		if (stream->key_callback != NULL) {
			const char *key_id =
				binary_to_hex(digest_pos, digest_len);
			int ret = stream->key_callback(key_id,
				&stream->priv_key, &error, stream->key_context);
			if (ret < 0) {
				io_stream_set_error(&stream->istream.iostream,
						    "Private key not available: %s",
						    error);
				return -1;
			}
			if (ret == 0) {
				io_stream_set_error(&stream->istream.iostream,
						    "Private key not available");
				return -1;
			}
		} else {
			io_stream_set_error(&stream->istream.iostream,
					    "Private key not available");
			return -1;
		}
	}

	buffer_t *check = t_buffer_create(32);

	if (!dcrypt_key_id_private_old(stream->priv_key, check, &error)) {
		io_stream_set_error(&stream->istream.iostream,
				    "Cannot get public key hash: %s", error);
		return -1;
	} else {
		if (memcmp(digest_pos, check->data,
			   I_MIN(digest_len,check->used)) != 0) {
			io_stream_set_error(&stream->istream.iostream,
					    "Private key not available");
			return -1;
		}
	}

	/* derive shared secret */
	if (!dcrypt_ecdh_derive_secret_local(stream->priv_key,
		&ephemeral_key, secret, &error)) {
		io_stream_set_error(&stream->istream.iostream,
				    "Cannot perform ECDH: %s", error);
		return -1;
	}

	/* run it thru SHA256 once */
	const struct hash_method *hash = &hash_method_sha256;
	unsigned char hctx[hash->context_size];
	unsigned char hres[hash->digest_size];
	hash->init(hctx);
	hash->loop(hctx, secret->data, secret->used);
	hash->result(hctx, hres);
	buffer_clear_safe(secret);

	/* NB! The old code was broken and used this kind of IV - it is not
	   correct, but we need to stay compatible with old data */

	/* use it to decrypt the actual encryption key */
	struct dcrypt_context_symmetric *dctx;
	if (!dcrypt_ctx_sym_create("aes-256-ctr", DCRYPT_MODE_DECRYPT,
				   &dctx, &error)) {
		io_stream_set_error(&stream->istream.iostream,
				    "Key decryption error: %s", error);
		return -1;
	}

	ec = 0;
	dcrypt_ctx_sym_set_iv(dctx, (const unsigned char*)
		"\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0", 16);
	dcrypt_ctx_sym_set_key(dctx, hres, hash->digest_size);
	if (!dcrypt_ctx_sym_init(dctx, &error) ||
	    !dcrypt_ctx_sym_update(dctx, key_ct_pos, key_ct_len, key, &error) ||
	    !dcrypt_ctx_sym_final(dctx, key, &error)) {
		io_stream_set_error(&stream->istream.iostream,
				    "Key decryption error: %s", error);
		ec = -1;
	}
	dcrypt_ctx_sym_destroy(&dctx);

	if (ec != 0) {
		io_stream_set_error(&stream->istream.iostream,
				    "Key decryption error: %s", error);
		return -1;
	}

	/* see if we got the correct key */
	hash->init(hctx);
	hash->loop(hctx, key->data, key->used);
	hash->result(hctx, hres);

	if (key_digest_size != sizeof(hres)) {
		io_stream_set_error(&stream->istream.iostream,
				    "Key decryption error: "
				    "invalid digest length");
		return -1;
	}
	if (memcmp(hres, key_digest_pos, sizeof(hres)) != 0) {
		io_stream_set_error(&stream->istream.iostream,
				    "Key decryption error: "
				    "decrypted key is invalid");
		return -1;
	}

	/* prime context with key */
	if (!dcrypt_ctx_sym_create("aes-256-ctr", DCRYPT_MODE_DECRYPT,
				   &stream->ctx_sym, &error)) {
		io_stream_set_error(&stream->istream.iostream,
				    "Decryption context create error: %s",
				    error);
		return -1;
	}

	/* Again, old code used this IV, so we have to use it too */
	dcrypt_ctx_sym_set_iv(stream->ctx_sym, (const unsigned char*)
		"\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0\x0", 16);
	dcrypt_ctx_sym_set_key(stream->ctx_sym, key->data, key->used);

	buffer_clear_safe(key);

	if (!dcrypt_ctx_sym_init(stream->ctx_sym, &error)) {
		io_stream_set_error(&stream->istream.iostream,
				    "Decryption init error: %s", error);
		return -1;
	}

	stream->use_mac = FALSE;
	stream->initialized = TRUE;
	/* now we are ready to decrypt stream */

	return sizeof(IOSTREAM_CRYPT_MAGIC) + 1 + 2 + keydata_len;
}

static bool
get_msb32(const unsigned char **_data, const unsigned char *end,
	  uint32_t *num_r)
{
	const unsigned char *data = *_data;
	if (end-data < 4)
		return FALSE;
	*num_r = be32_to_cpu_unaligned(data);
	*_data += 4;
	return TRUE;
}

static int
i_stream_decrypt_der(const unsigned char **_data, const unsigned char *end,
		     const char **str_r, const char **error_r)
{
	const unsigned char *data = *_data;
	unsigned int len;

	if (end-data < 2)
		return 0;
	/* get us DER encoded length */
	if ((data[1] & 0x80) != 0) {
		/* two byte length */
		if (end-data < 3)
			return 0;
		len = ((data[1] & 0x7f) << 8) + data[2] + 3;
	} else {
		len = data[1] + 2;
	}
	if ((size_t)(end-data) < len)
		return 0;
	*str_r = dcrypt_oid2name(data, len, error_r);
	if (*str_r == NULL)
		return -1;
	*_data += len;
	return 1;
}

static ssize_t
i_stream_decrypt_key(struct decrypt_istream *stream, const char *malg,
		     const char *calg, unsigned int rounds, const unsigned char *data,
		     const unsigned char *end, buffer_t *key, size_t key_len)
{
	const char *error;
	enum dcrypt_key_type ktype;
	int keys;
	bool have_key = FALSE;
	unsigned char dgst[32];
	uint32_t val;
	buffer_t buf;

	if (data == end)
		return 0;

	keys = *data++;

	/* if we have a key, prefab the digest */
	if (stream->priv_key != NULL) {
		buffer_create_from_data(&buf, dgst, sizeof(dgst));
		if (!dcrypt_key_id_private(stream->priv_key, "sha256", &buf,
					   &error)) {
			io_stream_set_error(&stream->istream.iostream,
					    "Decryption error: "
					    "dcrypt_key_id_private failed: %s",
					    error);
			return -1;
		}
	} else if (stream->key_callback == NULL) {
		io_stream_set_error(&stream->istream.iostream,
				    "Decryption error: "
				    "no private key available");
		return -1;
	}

	/* for each key */
	for(;keys>0;keys--) {
		if ((size_t)(end-data) < 1 + (ssize_t)sizeof(dgst))
			return 0;
		ktype = *data++;

		if (stream->priv_key != NULL) {
			/* see if key matches to the one we have */
			if (memcmp(dgst, data, sizeof(dgst)) == 0) {
				have_key = TRUE;
				break;
			}
		} else if (stream->key_callback != NULL) {
			const char *hexdgst = /* digest length */
				binary_to_hex(data, sizeof(dgst));
			if (stream->priv_key != NULL)
				dcrypt_key_unref_private(&stream->priv_key);
			/* hope you going to give us right key.. */
			int ret = stream->key_callback(hexdgst,
				&stream->priv_key, &error, stream->key_context);
			if (ret < 0) {
				io_stream_set_error(&stream->istream.iostream,
						    "Private key not available: "
						    "%s", error);
				return -1;
			}
			if (ret > 0) {
				have_key = TRUE;
				break;
			}
		}
		data += sizeof(dgst);

		/* wasn't correct key, skip over some data */
		if (!get_msb32(&data, end, &val) ||
		    !get_msb32(&data, end, &val))
			return 0;
	}

	/* didn't find matching key */
	if (!have_key) {
		io_stream_set_error(&stream->istream.iostream,
				    "Decryption error: "
				    "no private key available");
		return -1;
	}

	data += sizeof(dgst);

	const unsigned char *ephemeral_key;
	uint32_t ep_key_len;
	const unsigned char *encrypted_key;
	uint32_t eklen;
	const unsigned char *ekhash;
	uint32_t ekhash_len;

	/* read ephemeral key (can be missing for RSA) */
	if (!get_msb32(&data, end, &ep_key_len) ||
	    (size_t)(end-data) < ep_key_len)
		return 0;
	ephemeral_key = data;
	data += ep_key_len;

	/* read encrypted key */
	if (!get_msb32(&data, end, &eklen) || (size_t)(end-data) < eklen)
		return 0;
	encrypted_key = data;
	data += eklen;

	/* read key data hash */
	if (!get_msb32(&data, end, &ekhash_len) ||
	    (size_t)(end-data) < ekhash_len)
		return 0;
	ekhash = data;
	data += ekhash_len;

	/* decrypt the seed */
	if (ktype == DCRYPT_KEY_RSA) {
		if (!dcrypt_rsa_decrypt(stream->priv_key, encrypted_key, eklen,
					key, DCRYPT_PADDING_RSA_PKCS1_OAEP,
					&error)) {
			io_stream_set_error(&stream->istream.iostream,
					    "key decryption error: %s", error);
			return -1;
		}
	} else if (ktype == DCRYPT_KEY_EC) {
		/* perform ECDHE */
		buffer_t *temp_key = t_buffer_create(256);
		buffer_t *secret = t_buffer_create(256);
		buffer_t peer_key;
		buffer_create_from_const_data(&peer_key,
			ephemeral_key, ep_key_len);
		if (!dcrypt_ecdh_derive_secret_local(stream->priv_key,
			&peer_key, secret, &error)) {
			io_stream_set_error(&stream->istream.iostream,
				"Key decryption error: corrupted header");
			return -1;
		}
		struct dcrypt_context_symmetric *dctx;
		if (!dcrypt_ctx_sym_create(calg, DCRYPT_MODE_DECRYPT,
					    &dctx, &error)) {
			buffer_clear_safe(temp_key);
			io_stream_set_error(&stream->istream.iostream,
					    "Key decryption error: %s", error);
			return -1;
		}
		size_t ek_iv_len = dcrypt_ctx_sym_get_iv_length(dctx);
		size_t ek_key_len = dcrypt_ctx_sym_get_key_length(dctx);
		size_t ek_tag_len = 0;
		if ((stream->flags & IO_STREAM_ENC_SAME_CALG) != 0 &&
		    (stream->flags & IO_STREAM_ENC_INTEGRITY_AEAD) != 0)
			ek_tag_len = IOSTREAM_TAG_SIZE;
		/* use shared secret and peer key to generate decryption key */
		if (!dcrypt_pbkdf2(secret->data, secret->used,
				   peer_key.data, peer_key.used,
				   malg, rounds, temp_key,
				   ek_key_len + ek_iv_len + ek_tag_len,
				   &error)) {
			buffer_clear_safe(secret);
			io_stream_set_error(&stream->istream.iostream,
					    "Key decryption error: %s", error);
			dcrypt_ctx_sym_destroy(&dctx);
			return -1;
		}

		buffer_clear_safe(secret);
		if (temp_key->used != ek_key_len + ek_iv_len + ek_tag_len) {
			buffer_clear_safe(temp_key);
			io_stream_set_error(&stream->istream.iostream,
					    "Cannot perform key decryption: "
					    "invalid temporary key");
			dcrypt_ctx_sym_destroy(&dctx);
			return -1;
		}
		const unsigned char *ptr = temp_key->data;
		if (ek_tag_len > 0) {
			if (eklen < ek_tag_len) {
				buffer_clear_safe(temp_key);
				io_stream_set_error(&stream->istream.iostream,
						    "Cannot perform key decryption: "
						    "Missing TAG from encrypted key");
				dcrypt_ctx_sym_destroy(&dctx);
				return -1;
			}
			/* extract ek_aad_len bytes from encrypted_key */
			const unsigned char *tag =
				CONST_PTR_OFFSET(encrypted_key, eklen - ek_tag_len);
			dcrypt_ctx_sym_set_tag(dctx, tag, ek_tag_len);
			eklen -= ek_tag_len;
		}

		dcrypt_ctx_sym_set_key(dctx, ptr, ek_key_len);
		dcrypt_ctx_sym_set_iv(dctx, ptr + ek_key_len, ek_iv_len);
		if (ek_tag_len > 0)
			dcrypt_ctx_sym_set_aad(dctx, ptr + ek_key_len + ek_iv_len, ek_tag_len);
		buffer_clear_safe(temp_key);

		int ec = 0;
		if (!dcrypt_ctx_sym_init(dctx, &error) ||
		    !dcrypt_ctx_sym_update(dctx, encrypted_key, eklen,
					   key, &error) ||
		    !dcrypt_ctx_sym_final(dctx, key, &error)) {
			io_stream_set_error(&stream->istream.iostream,
					    "Cannot perform key decryption: %s",
					    error);
			ec = -1;
		}

		if (key->used != key_len) {
			io_stream_set_error(&stream->istream.iostream,
					    "Cannot perform key decryption: "
					    "invalid key length (%zu != %zu)", key->used, key_len);
			ec = -1;
		}

		dcrypt_ctx_sym_destroy(&dctx);
		if (ec != 0) return ec;
	} else {
		io_stream_set_error(&stream->istream.iostream,
				    "Decryption error: "
				    "unsupported key type 0x%02x", ktype);
		return -1;
	}

	/* make sure we were able to decrypt the encrypted key correctly */
	const struct hash_method *hash = hash_method_lookup(t_str_lcase(malg));
	if (hash == NULL) {
		buffer_clear_safe(key);
		io_stream_set_error(&stream->istream.iostream,
				    "Decryption error: "
				     "unsupported hash algorithm: %s", malg);
		return -1;
	}
	unsigned char hctx[hash->context_size];
	unsigned char hres[hash->digest_size];
	hash->init(hctx);
	hash->loop(hctx, key->data, key->used);
	hash->result(hctx, hres);

	for(int i = 1; i < 2049; i++) {
		uint32_t i_msb = cpu32_to_be(i);

		hash->init(hctx);
		hash->loop(hctx, hres, sizeof(hres));
		hash->loop(hctx, &i_msb, sizeof(i_msb));
		hash->result(hctx, hres);
	}

	/* do the comparison */
	if (ekhash_len != sizeof(hres) ||
	    memcmp(ekhash, hres, sizeof(hres)) != 0) {
		buffer_clear_safe(key);
		io_stream_set_error(&stream->istream.iostream,
				    "Decryption error: "
				    "corrupted header ekhash");
		return -1;
	}
	return 1;
}

static int
i_stream_decrypt_header_contents(struct decrypt_istream *stream,
				 const unsigned char *data, size_t size)
{
	const unsigned char *end = data + size;
	bool failed = FALSE;

	/* read cipher OID */
	const char *calg;
	const char *error;
	int ret;

	if ((ret = i_stream_decrypt_der(&data, end, &calg, &error)) == 0)
		return 0;
	else if (ret < 0 || calg == NULL ||
		 !dcrypt_ctx_sym_create(calg, DCRYPT_MODE_DECRYPT,
					&stream->ctx_sym, &error)) {
		io_stream_set_error(&stream->istream.iostream,
				    "Decryption error: "
				    "unsupported/invalid cipher '%s': %s", calg, error);
		return -1;
	}

	/* read MAC oid (MAC is used for PBKDF2 and key data digest, too) */
	const char *malg;
	if ((ret = i_stream_decrypt_der(&data, end, &malg, &error)) == 0)
		return 0;
	else if (ret < 0 || malg == NULL ||
		 !dcrypt_ctx_hmac_create(malg, &stream->ctx_mac, &error)) {
		 io_stream_set_error(&stream->istream.iostream,
				     "Decryption error: "
				     "unsupported/invalid MAC algorithm '%s': %s",
				     malg, error);
		return -1;
	}

	/* read rounds (for PBKDF2) */
	uint32_t rounds;
	if (!get_msb32(&data, end, &rounds))
		return 0;
	/* read key data length */
	uint32_t kdlen;
	if (!get_msb32(&data, end, &kdlen))
		return 0;

	size_t tagsize;

	if ((stream->flags & IO_STREAM_ENC_INTEGRITY_HMAC) ==
		IO_STREAM_ENC_INTEGRITY_HMAC) {
		tagsize = IOSTREAM_TAG_SIZE;
	} else if ((stream->flags & IO_STREAM_ENC_INTEGRITY_AEAD) ==
		IO_STREAM_ENC_INTEGRITY_AEAD) {
		tagsize = IOSTREAM_TAG_SIZE;
	} else {
		tagsize = 0;
	}

	/* how much key data we should be getting */
	size_t kl = dcrypt_ctx_sym_get_key_length(stream->ctx_sym) +
		dcrypt_ctx_sym_get_iv_length(stream->ctx_sym) + tagsize;
	buffer_t *keydata = t_buffer_create(kl);

	const char *ek_calg = "AES-256-CBC";
	if ((stream->flags & IO_STREAM_ENC_SAME_CALG) != 0)
		ek_calg = calg;

	/* try to decrypt the keydata with a private key */
	if ((ret = i_stream_decrypt_key(stream, malg, ek_calg, rounds, data,
					end, keydata, kl)) <= 0)
		return ret;

	/* oh, it worked! */
	const unsigned char *ptr = keydata->data;
	if (keydata->used != kl) {
		/* but returned wrong amount of data */
		io_stream_set_error(&stream->istream.iostream,
				    "Key decryption error: "
				    "Key data length mismatch");
		return -1;
	}

	/* prime contexts */
	dcrypt_ctx_sym_set_key(stream->ctx_sym, ptr,
			       dcrypt_ctx_sym_get_key_length(stream->ctx_sym));
	ptr += dcrypt_ctx_sym_get_key_length(stream->ctx_sym);
	dcrypt_ctx_sym_set_iv(stream->ctx_sym, ptr,
			      dcrypt_ctx_sym_get_iv_length(stream->ctx_sym));
	stream->iv = i_malloc(dcrypt_ctx_sym_get_iv_length(stream->ctx_sym));
	memcpy(stream->iv, ptr, dcrypt_ctx_sym_get_iv_length(stream->ctx_sym));
	ptr += dcrypt_ctx_sym_get_iv_length(stream->ctx_sym);

	/* based on the chosen MAC, initialize HMAC or AEAD */
	if ((stream->flags & IO_STREAM_ENC_INTEGRITY_HMAC) ==
		IO_STREAM_ENC_INTEGRITY_HMAC) {
		const char *error;
		dcrypt_ctx_hmac_set_key(stream->ctx_mac, ptr, tagsize);
		if (!dcrypt_ctx_hmac_init(stream->ctx_mac, &error)) {
			io_stream_set_error(&stream->istream.iostream,
					    "MAC error: %s", error);
			stream->istream.istream.stream_errno = EIO;
			failed = TRUE;
		}
		stream->ftr = dcrypt_ctx_hmac_get_digest_length(stream->ctx_mac);
		stream->use_mac = TRUE;
	} else if ((stream->flags & IO_STREAM_ENC_INTEGRITY_AEAD) ==
		IO_STREAM_ENC_INTEGRITY_AEAD) {
		dcrypt_ctx_sym_set_aad(stream->ctx_sym, ptr, tagsize);
		stream->ftr = tagsize;
		stream->use_mac = TRUE;
	} else {
		stream->use_mac = FALSE;
	}
	/* destroy private key data */
	buffer_clear_safe(keydata);
	return failed ? -1 : 1;
}

static ssize_t
i_stream_decrypt_read_header(struct decrypt_istream *stream,
			     const unsigned char *data, size_t mlen)
{
	const char *error;
	const unsigned char *end = data + mlen;

	/* check magic */
	if (mlen < sizeof(IOSTREAM_CRYPT_MAGIC))
		return 0;
	if (memcmp(data, IOSTREAM_CRYPT_MAGIC, sizeof(IOSTREAM_CRYPT_MAGIC)) != 0) {
		io_stream_set_error(&stream->istream.iostream,
				    "Stream is not encrypted (invalid magic)");
		stream->istream.istream.stream_errno = EINVAL;
		return -1;
	}
	data += sizeof(IOSTREAM_CRYPT_MAGIC);

	if (data >= end)
		return 0; /* read more? */

	/* check version */
	if (*data == '\x01') {
		stream->format = DECRYPT_FORMAT_V1;
		return i_stream_decrypt_read_header_v1(stream, data+1,
						       end - (data+1));
	} else if (*data != '\x02') {
		io_stream_set_error(&stream->istream.iostream,
				    "Unsupported encrypted data 0x%02x", *data);
		return -1;
	}

	stream->format = DECRYPT_FORMAT_V2;

	data++;

	/* read flags */
	uint32_t flags;
	if (!get_msb32(&data, end, &flags))
		return 0;
	stream->flags = flags;

	/* get the total length of header */
	uint32_t hdr_len;
	if (!get_msb32(&data, end, &hdr_len))
		return 0;
	/* do not forget stream format */
	if ((size_t)(end-data)+1 < hdr_len)
		return 0;

	int ret;
	if ((ret = i_stream_decrypt_header_contents(stream, data, hdr_len)) < 0)
		return -1;
	else if (ret == 0) {
		io_stream_set_error(&stream->istream.iostream,
				    "Decryption error: truncate header length");
		stream->istream.istream.stream_errno = EPIPE;
		return -1;
	}
	stream->initialized = TRUE;

	/* if it all went well, try to initialize decryption context */
	if (!dcrypt_ctx_sym_init(stream->ctx_sym, &error)) {
		io_stream_set_error(&stream->istream.iostream,
				    "Decryption init error: %s", error);
		return -1;
	}
	return hdr_len;
}

static void
i_stream_decrypt_realloc_buf_if_needed(struct decrypt_istream *dstream)
{
       if (!dstream->snapshot_pending)
               return;

       /* buf exists in a snapshot. Leave it be and create a copy of it
          that we modify. */
       buffer_t *old_buf = dstream->buf;
       dstream->buf = buffer_create_dynamic(default_pool,
					    I_MAX(512, old_buf->used));
       buffer_append(dstream->buf, old_buf->data, old_buf->used);
       dstream->snapshot_pending = FALSE;

       dstream->istream.buffer = dstream->buf->data;
}

static ssize_t
i_stream_decrypt_read(struct istream_private *stream)
{
	struct decrypt_istream *dstream =
		(struct decrypt_istream *)stream;
	const unsigned char *data;
	size_t size, decrypt_size;
	const char *error = NULL;
	int ret;
	bool check_mac = FALSE;

	/* not if it's broken */
	if (stream->istream.stream_errno != 0)
		return -1;

	i_stream_decrypt_realloc_buf_if_needed(dstream);
	for (;;) {
		/* remove skipped data from buffer */
		if (stream->skip > 0) {
			i_assert(stream->skip <= dstream->buf->used);
			buffer_delete(dstream->buf, 0, stream->skip);
			stream->pos -= stream->skip;
			stream->skip = 0;
		}

		stream->buffer = dstream->buf->data;

		i_assert(stream->pos <= dstream->buf->used);
		if (stream->pos >= dstream->istream.max_buffer_size) {
			/* stream buffer still at maximum */
			return -2;
		}

		/* if something is already decrypted, return as much of it as
		   we can */
		if (dstream->initialized && dstream->buf->used > 0) {
			size_t new_pos, bytes;

			/* only return up to max_buffer_size bytes, even when
			   buffer actually has more, as not to confuse the
			   caller */
			if (dstream->buf->used <=
				dstream->istream.max_buffer_size) {
				new_pos = dstream->buf->used;
				if (dstream->finalized)
					stream->istream.eof = TRUE;
			} else {
				new_pos = dstream->istream.max_buffer_size;
			}

			bytes = new_pos - stream->pos;
			stream->pos = new_pos;
			if (bytes > 0)
				return (ssize_t)bytes;
		}
		if (dstream->finalized) {
			/* all data decrypted */
			stream->istream.eof = TRUE;
			return -1;
		}

		/* need to read more input */
		ret = i_stream_read_memarea(stream->parent);
		if (ret == 0)
			return ret;

		data = i_stream_get_data(stream->parent, &size);

		if (ret == -1 &&
		    (size == 0 || stream->parent->stream_errno != 0)) {
			stream->istream.stream_errno =
				stream->parent->stream_errno;

			/* file was empty */
			if (!dstream->initialized &&
			    size == 0 && stream->parent->eof) {
				stream->istream.eof = TRUE;
				return -1;
			}

			if (stream->istream.stream_errno != 0)
				return -1;

			if (!dstream->initialized) {
				io_stream_set_error(&stream->iostream,
					"Decryption error: %s",
					"Input truncated in decryption header");
				stream->istream.stream_errno = EPIPE;
				return -1;
			}

			/* final block */
			if (dcrypt_ctx_sym_final(dstream->ctx_sym,
				dstream->buf, &error)) {
				dstream->finalized = TRUE;
				continue;
			}
			io_stream_set_error(&stream->iostream,
				"MAC error: %s", error);
			stream->istream.stream_errno = EIO;
			return -1;
		}

		if (!dstream->initialized) {
			ssize_t hret;

			if ((hret=i_stream_decrypt_read_header(
				dstream, data, size)) <= 0) {
				if (hret < 0) {
					if (stream->istream.stream_errno == 0)
						/* assume temporary failure */
						stream->istream.stream_errno = EIO;
					return -1;
				}

				if (hret == 0 && stream->parent->eof) {
					/* not encrypted by us */
					stream->istream.stream_errno = EPIPE;
					io_stream_set_error(&stream->iostream,
						"Truncated header");
					return -1;
				}
			}

			if (hret == 0) {
				/* see if we can get more data */
				if (ret == -2) {
					stream->istream.stream_errno = EIO;
					io_stream_set_error(&stream->iostream,
						"Header too large "
						"(more than %zu bytes)",
						size);
					return -1;
				}
				continue;
			} else {
				/* clean up buffer */
				buffer_clear_safe(dstream->buf);
				i_stream_skip(stream->parent, hret);
			}

			data = i_stream_get_data(stream->parent, &size);
		}
		decrypt_size = size;

		if (dstream->use_mac) {
			if (stream->parent->eof) {
				if (decrypt_size < dstream->ftr) {
					io_stream_set_error(&stream->iostream,
						"Decryption error: "
						"footer is longer than data");
					stream->istream.stream_errno = EIO;
					return -1;
				}
				check_mac = TRUE;
			} else if (size < dstream->ftr || decrypt_size < dstream->ftr) {
				return 0;
			} else {
				/* ignore footer's length of data until we
				   reach EOF */
				size -= dstream->ftr;
			}
			i_assert(decrypt_size >= dstream->ftr);
			decrypt_size -= dstream->ftr;
			if ((dstream->flags & IO_STREAM_ENC_INTEGRITY_HMAC) ==
				IO_STREAM_ENC_INTEGRITY_HMAC) {
				if (!dcrypt_ctx_hmac_update(dstream->ctx_mac,
				    data, decrypt_size, &error)) {
					io_stream_set_error(&stream->iostream,
						"MAC error: %s", error);
					stream->istream.stream_errno = EIO;
					return -1;
				}
			}
		}

		if (check_mac) {
			if ((dstream->flags & IO_STREAM_ENC_INTEGRITY_HMAC) ==
				IO_STREAM_ENC_INTEGRITY_HMAC) {
				size_t maclen = dcrypt_ctx_hmac_get_digest_length(dstream->ctx_mac);
				unsigned char dgst[maclen];
				buffer_t db;
				buffer_create_from_data(&db, dgst, sizeof(dgst));
				if (!dcrypt_ctx_hmac_final(dstream->ctx_mac, &db, &error)) {
					io_stream_set_error(&stream->iostream,
						"Cannot verify MAC: %s", error);
					stream->istream.stream_errno = EIO;
					return -1;
				}
				if (memcmp(dgst, data + decrypt_size, maclen) != 0) {
					io_stream_set_error(&stream->iostream,
						"Cannot verify MAC: mismatch");
					stream->istream.stream_errno = EIO;
					return -1;
				}
			} else if ((dstream->flags & IO_STREAM_ENC_INTEGRITY_AEAD) ==
				IO_STREAM_ENC_INTEGRITY_AEAD) {
				dcrypt_ctx_sym_set_tag(dstream->ctx_sym,
						       data + decrypt_size,
						       dstream->ftr);
			}
		}

		if (!dcrypt_ctx_sym_update(dstream->ctx_sym,
		    data, decrypt_size, dstream->buf, &error)) {
			io_stream_set_error(&stream->iostream,
				"Decryption error: %s", error);
			stream->istream.stream_errno = EIO;
			return -1;
		}
		i_stream_skip(stream->parent, size);
	}
}

static void
i_stream_decrypt_seek(struct istream_private *stream, uoff_t v_offset,
		      bool mark ATTR_UNUSED)
{
	struct decrypt_istream *dstream =
		(struct decrypt_istream *)stream;

	i_stream_decrypt_realloc_buf_if_needed(dstream);

	if (i_stream_nonseekable_try_seek(stream, v_offset))
		return;

	/* have to seek backwards - reset crypt state and retry */
	i_stream_decrypt_reset(dstream);
	if (!i_stream_nonseekable_try_seek(stream, v_offset))
		i_unreached();
}

static void i_stream_decrypt_close(struct iostream_private *stream,
				   bool close_parent)
{
	struct decrypt_istream *dstream =
		(struct decrypt_istream *)stream;

	if (close_parent)
		i_stream_close(dstream->istream.parent);
}

static void
i_stream_decrypt_snapshot_free(struct istream_snapshot *_snapshot)
{
	struct decrypt_istream_snapshot *snapshot =
		container_of(_snapshot, struct decrypt_istream_snapshot,
			     snapshot);

       if (snapshot->dstream->buf != snapshot->buf)
               buffer_free(&snapshot->buf);
       else {
               i_assert(snapshot->dstream->snapshot_pending);
               snapshot->dstream->snapshot_pending = FALSE;
       }
       i_free(snapshot);
}

static struct istream_snapshot *
i_stream_decrypt_snapshot(struct istream_private *stream,
			  struct istream_snapshot *prev_snapshot)
{
	struct decrypt_istream *dstream =
		(struct decrypt_istream *)stream;
	struct decrypt_istream_snapshot *snapshot;

	if (stream->buffer != dstream->buf->data) {
		/* reading body */
		return i_stream_default_snapshot(stream, prev_snapshot);
	}

	/* snapshot the header buffer */
	snapshot = i_new(struct decrypt_istream_snapshot, 1);
	snapshot->dstream = dstream;
	snapshot->buf = dstream->buf;
	snapshot->snapshot.free = i_stream_decrypt_snapshot_free;
	snapshot->snapshot.prev_snapshot = prev_snapshot;
	dstream->snapshot_pending = TRUE;
	return &snapshot->snapshot;
}

static void i_stream_decrypt_destroy(struct iostream_private *stream)
{
	struct decrypt_istream *dstream =
		(struct decrypt_istream *)stream;

	if (!dstream->snapshot_pending)
		buffer_free(&dstream->buf);
	else {
		/* Clear buf to make sure i_stream_decrypt_snapshot_free()
		   frees it. */
		dstream->buf = NULL;
	}

	if (dstream->iv != NULL)
		i_free_and_null(dstream->iv);
	if (dstream->ctx_sym != NULL)
		dcrypt_ctx_sym_destroy(&dstream->ctx_sym);
	if (dstream->ctx_mac != NULL)
		dcrypt_ctx_hmac_destroy(&dstream->ctx_mac);
	if (dstream->priv_key != NULL)
		dcrypt_key_unref_private(&dstream->priv_key);

	i_stream_unref(&dstream->istream.parent);
}

static struct decrypt_istream *
i_stream_create_decrypt_common(struct istream *input)
{
	struct decrypt_istream *dstream;

	i_assert(input->real_stream->max_buffer_size > 0);

	dstream = i_new(struct decrypt_istream, 1);
	dstream->istream.max_buffer_size = input->real_stream->max_buffer_size;
	dstream->istream.read = i_stream_decrypt_read;
	dstream->istream.snapshot = i_stream_decrypt_snapshot;
	if (input->seekable)
		dstream->istream.seek = i_stream_decrypt_seek;
	dstream->istream.iostream.close = i_stream_decrypt_close;
	dstream->istream.iostream.destroy = i_stream_decrypt_destroy;

	dstream->istream.istream.readable_fd = FALSE;
	dstream->istream.istream.blocking = input->blocking;
	dstream->istream.istream.seekable = input->seekable;

	dstream->buf = buffer_create_dynamic(default_pool, 512);

	(void)i_stream_create(&dstream->istream, input,
			      i_stream_get_fd(input), 0);
	return dstream;
}

struct istream *
i_stream_create_decrypt(struct istream *input,
			struct dcrypt_private_key *priv_key)
{
	struct decrypt_istream *dstream;

	dstream = i_stream_create_decrypt_common(input);
	dcrypt_key_ref_private(priv_key);
	dstream->priv_key = priv_key;
	return &dstream->istream.istream;
}

struct istream *
i_stream_create_sym_decrypt(struct istream *input,
			    struct dcrypt_context_symmetric *ctx)
{
	const char *error;
	int ec;
	struct decrypt_istream *dstream;
	dstream = i_stream_create_decrypt_common(input);
	dstream->use_mac = FALSE;
	dstream->initialized = TRUE;
	dstream->symmetric = TRUE;

	if (!dcrypt_ctx_sym_init(ctx, &error)) ec = -1;
	else ec = 0;

	dstream->ctx_sym = ctx;

	if (ec != 0) {
		io_stream_set_error(&dstream->istream.iostream,
				    "Cannot initialize decryption: %s", error);
		dstream->istream.istream.stream_errno = EIO;
	};

	return &dstream->istream.istream;
}

struct istream *
i_stream_create_decrypt_callback(struct istream *input,
				 i_stream_decrypt_get_key_callback_t *callback,
				 void *context)
{
	struct decrypt_istream *dstream;

	i_assert(callback != NULL);

	dstream = i_stream_create_decrypt_common(input);
	dstream->key_callback = callback;
	dstream->key_context = context;
	return &dstream->istream.istream;
}
