/*
** Copyright 2003-2007 Double Precision, Inc.
** See COPYING for distribution information.
*/

#include "config.h"
#include "tlspasswordcache.h"

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <md5/md5.h>

#define PASSFILEFORMAT 1

#if HAVE_OPENSSL097
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/rand.h>

static void sslerror(EVP_CIPHER_CTX *ctx, const char *pfix)
{
        char errmsg[256];
        int errnum=ERR_get_error();
 
        ERR_error_string_n(errnum, errmsg, sizeof(errmsg)-1);

	fprintf(stderr, "%s: %s\n", pfix, errmsg);
}


#endif

#if HAVE_GCRYPT

#include <gcrypt.h>

#define RAND_pseudo_bytes(a,b) (gcry_create_nonce((a),(b)), 0)

typedef struct {
	enum gcry_cipher_algos algo;
	enum gcry_cipher_modes mode;
} EVP_CIPHER;

#define EVP_MAX_IV_LENGTH 256

const EVP_CIPHER *EVP_des_cbc()
{
	static const EVP_CIPHER des_cbc={GCRY_CIPHER_DES,
					 GCRY_CIPHER_MODE_CBC};

	return &des_cbc;
}

typedef struct {
	const EVP_CIPHER *cipher;
	gcry_error_t err;
	gcry_cipher_hd_t handle;

	int padding;
	char *blkbuf;
	size_t blksize;

	size_t blkptr;

} EVP_CIPHER_CTX;

static void sslerror(EVP_CIPHER_CTX *ctx, const char *pfix)
{
	fprintf(stderr, "%s: %s\n", pfix, gcry_strerror(ctx->err));
}

static void EVP_CIPHER_CTX_init(EVP_CIPHER_CTX *ctx)
{
	memset(ctx, 0, sizeof(*ctx));
}

static void EVP_CIPHER_CTX_cleanup(EVP_CIPHER_CTX *ctx)
{
	if (ctx->handle)
	{
		gcry_cipher_close(ctx->handle);
		ctx->handle=NULL;
	}

	if (ctx->blkbuf)
	{
		free(ctx->blkbuf);
		ctx->blkbuf=NULL;
	}
}

static int EVP_CIPHER_iv_length(const EVP_CIPHER *cipher)
{
	size_t l=0;

	gcry_cipher_algo_info(cipher->algo, GCRYCTL_GET_BLKLEN, NULL, &l);
	return l;
}

static int EVP_CIPHER_key_length(const EVP_CIPHER *cipher)
{
	size_t l=0;

	gcry_cipher_algo_info(cipher->algo, GCRYCTL_GET_KEYLEN, NULL, &l);
	return l;
}

static int EVP_EncryptInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *cipher,
			      void *impl, unsigned char *key, unsigned char *iv)
{
	EVP_CIPHER_CTX_cleanup(ctx);
	ctx->cipher=cipher;
	ctx->err=gcry_cipher_open(&ctx->handle,
				  cipher->algo,
				  cipher->mode, 0);

	if (!ctx->err)
		ctx->err=gcry_cipher_setkey(ctx->handle, key,
					    EVP_CIPHER_key_length(cipher));

	if (!ctx->err)
		ctx->err=gcry_cipher_setiv(ctx->handle, iv,
					   (ctx->blksize=
					    EVP_CIPHER_iv_length(cipher)));

	if (!ctx->err)
		if ((ctx->blkbuf=malloc(ctx->blksize)) == NULL)
			ctx->err=gpg_err_code_from_errno(errno);

	ctx->blkptr=0;
	ctx->padding=1;

	return ctx->err == 0;
}

static int EVP_EncryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out,
			     int *outl, unsigned char *in, int inl)
{
	*outl=0;

	while (inl > 0)
	{
		size_t cp= (size_t)inl < (ctx->blksize - ctx->blkptr)
			? (size_t)inl:(ctx->blksize - ctx->blkptr);

		if (ctx->blkptr == 0 && inl > ctx->blksize*2)
		{
			cp=(inl / ctx->blksize - 1) * ctx->blksize;

			if ((ctx->err=gcry_cipher_encrypt(ctx->handle,
							  out, cp,
							  in, cp))
			    != 0)
				return 0;

			out += cp;
			*outl += cp;
			in += cp;
			inl -= cp;
			continue;
		}

		memcpy(ctx->blkbuf + ctx->blkptr, in, cp);

		in += cp;
		inl -= cp;

		ctx->blkptr += cp;

		if (ctx->blkptr == ctx->blksize)
		{
			if ((ctx->err=gcry_cipher_encrypt(ctx->handle,
							  out, ctx->blksize,
							  ctx->blkbuf,
							  ctx->blksize)) != 0)
				return 0;
			out += ctx->blksize;
			*outl += ctx->blksize;
			ctx->blkptr=0;
		}
	}
	return 1;
}

static int EVP_EncryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *out,
			       int *outl)
{
	if (ctx->padding)
	{
		unsigned char pad=ctx->blksize - ctx->blkptr;

		*outl=0;

		if (pad == 0)
			pad=ctx->blksize;

		do
		{
			int n_outl;

			if (!EVP_EncryptUpdate(ctx, out, &n_outl, &pad, 1))
				return 0;
				
			out += n_outl;
			*outl += n_outl;
		}
		while (ctx->blkptr);
	}
	else if (ctx->blksize != ctx->blkptr)
	{
		ctx->err=GPG_ERR_BAD_DATA;
		return 0;
	}

	return 1;
}

static int EVP_DecryptInit_ex(EVP_CIPHER_CTX *ctx, const EVP_CIPHER *type,
			      void *impl, unsigned char *key,
			      unsigned char *iv)
{
	return EVP_EncryptInit_ex(ctx, type, impl, key, iv);
}

static int EVP_DecryptUpdate(EVP_CIPHER_CTX *ctx, unsigned char *out,
			     int *outl, unsigned char *in, int inl)
{
	*outl=0;

	while (inl > 0)
	{
		size_t cp;

		if (ctx->blkptr == 0 && inl > ctx->blksize * 3)
		{
			cp=(inl / ctx->blksize - 2) * ctx->blksize;

			if ((ctx->err=gcry_cipher_decrypt(ctx->handle,
							  out, cp,
							  in, cp))
			    != 0)
				return 0;

			out += cp;
			*outl += cp;
			in += cp;
			inl -= cp;
			continue;
		}

		if (ctx->blkptr == ctx->blksize)
		{
			if ((ctx->err=gcry_cipher_decrypt(ctx->handle,
							  out, ctx->blksize,
							  ctx->blkbuf,
							  ctx->blksize)) != 0)
				return 0;
			out += ctx->blksize;
			*outl += ctx->blksize;
			ctx->blkptr=0;
		}

		cp= (size_t)inl < (ctx->blksize - ctx->blkptr)
			? (size_t)inl:(ctx->blksize - ctx->blkptr);

		memcpy(ctx->blkbuf + ctx->blkptr, in, cp);

		in += cp;
		inl -= cp;

		ctx->blkptr += cp;

	}
	return 1;
}

static int EVP_DecryptFinal_ex(EVP_CIPHER_CTX *ctx, unsigned char *outm,
			       int *outl)
{
	unsigned char lastval;
	int cnt;

	if (ctx->blkptr != ctx->blksize)
	{
		ctx->err=GPG_ERR_BAD_DATA;
		return 0;
	}

	if ((ctx->err=gcry_cipher_decrypt(ctx->handle,
					  ctx->blkbuf,
					  ctx->blksize,
					  NULL, 0)) != 0)
		return 0;

	if (ctx->padding)
	{
		lastval=ctx->blkbuf[ctx->blksize-1];

		if (lastval > 0 && lastval <= ctx->blksize)
		{
			char n;

			for (n=0; n<lastval; n++)
				if (ctx->blkbuf[ctx->blksize-1-n] != lastval)
					lastval=0;
		}
		else
			lastval=0;

		if (!lastval)
		{
			ctx->err=GPG_ERR_BAD_DATA;
			return 0;
		}
	}
	else
	{
		lastval=0;
	}

	cnt=ctx->blksize-lastval;
	if (cnt)
		memcpy(outm, ctx->blkbuf, cnt);
	*outl=cnt;
	return 1;
}


#define HAVE_OPENSSL097 1
#endif

#if HAVE_OPENSSL097

#if BUFSIZ < 8192
#undef BUFSIZ
#define BUFSIZ 8192
#endif

int tlspassword_init()
{
	return 1;
}

static int save_string(EVP_CIPHER_CTX *,
		       const char *, char *,
		       int (*)(const char *, size_t, void *),
		       void *);

int tlspassword_save( const char * const *urls,
		      const char * const *pwds,
		      const char *mpw,
		      int (*writefunc)(const char *, size_t, void *),
		      void *writefuncarg)
{
	char buf[BUFSIZ];
	char *p;
	int l;
	int wl;

	unsigned char iv1_buf[16];
	unsigned char iv2_buf[16];
	MD5_DIGEST md5_password;
	int iv_len, key_len;
	EVP_CIPHER_CTX ctx;
	const EVP_CIPHER *des=EVP_des_cbc();

	md5_digest(mpw, strlen(mpw), md5_password);

	EVP_CIPHER_CTX_init(&ctx);
	iv_len=EVP_CIPHER_iv_length(des);
	key_len=EVP_CIPHER_key_length(des);

	if (RAND_pseudo_bytes(iv1_buf, sizeof(iv1_buf)) < 0 ||
	    RAND_pseudo_bytes(iv2_buf, sizeof(iv2_buf)) < 0)
	{
		fprintf(stderr,
			"tlspassword_save: internal error - "
			"RAND_pseudo_bytes() failed.\n");
		EVP_CIPHER_CTX_cleanup(&ctx);
		errno=EIO;
		return -1;
	}

	if (iv_len + key_len > sizeof(iv1_buf)
	    || iv_len + key_len != sizeof(iv2_buf)
	    || key_len != sizeof(md5_password)/2)
	{
		fprintf(stderr,
			"tlspassword_save: internal error - "
			"unexpected key sizes.\n");
		EVP_CIPHER_CTX_cleanup(&ctx);
		errno=EIO;
		return -1;
	}

	p=buf+3;

	if (!EVP_EncryptInit_ex(&ctx, des, NULL,
				(unsigned char *)md5_password,
				iv1_buf) ||
	    !EVP_EncryptUpdate(&ctx, (unsigned char *)p, &l,
			       (unsigned char *)md5_password + key_len,
			       sizeof(md5_password)-key_len) ||
	    !EVP_EncryptUpdate(&ctx, (unsigned char *)(p += l), &l,
			       iv2_buf,
			       iv_len + key_len) ||
	    !EVP_EncryptFinal_ex(&ctx, (unsigned char *)(p += l), &l))

	{
		sslerror(&ctx, "EVP_EncryptInit_ex");
		EVP_CIPHER_CTX_cleanup(&ctx);
		errno=EIO;
		return -1;
	}

	p += l;

	wl= p - buf - 3;

	buf[0]=PASSFILEFORMAT;
	buf[1]= wl / 256;
	buf[2]= wl % 256;

	l=(*writefunc)(buf, 3, writefuncarg);

	if (l == 0)
		l=(*writefunc)((const char *)iv1_buf, iv_len, writefuncarg);

	if (l == 0)
		l=(*writefunc)(buf+3, wl, writefuncarg);

	if (l)
		return l;

#if 0
	{
		int i;

		printf("KEY: ");

		for (i=0; i<key_len + iv_len; i++)
			printf("%02X", (int)(unsigned char)iv2_buf[i]);
		printf("\n");
	}
#endif

	if (!EVP_EncryptInit_ex(&ctx, des, NULL,
				(unsigned char *)&iv2_buf,
				(unsigned char *)&iv2_buf + key_len))
	{
		sslerror(&ctx, "EVP_EncryptInit_ex");
		EVP_CIPHER_CTX_cleanup(&ctx);
		errno=EIO;
		return -1;
	}

	for (l=0; urls[l]; l++)
	{
		int n=save_string(&ctx, urls[l], buf, writefunc, writefuncarg);

		if (n)
			return n;

		n=save_string(&ctx, pwds[l], buf, writefunc, writefuncarg);

		if (n)
			return n;
	}

	if (!EVP_EncryptFinal_ex(&ctx, (unsigned char *)buf, &l))
	{
		sslerror(&ctx, "EVP_EncryptInit_ex");
		EVP_CIPHER_CTX_cleanup(&ctx);
		errno=EIO;
		return -1;
	}

	if (l)
		l=(*writefunc)(buf, l, writefuncarg);

	EVP_CIPHER_CTX_cleanup(&ctx);
	return l;
}

static int save_string(EVP_CIPHER_CTX *ctx,
		       const char *str, char *buf,
		       int (*writefunc)(const char *, size_t, void *),
		       void *writefuncarg)
{
	int l;
	size_t len=strlen(str);
	unsigned char b[2];

	if (len >= 256 * 256)
	{
		fprintf(stderr,
			"tlspassword_save: internal error - "
			"key sizes too large.\n");
		errno=EINVAL;
		return -1;
	}

	b[0]=len / 256;
	b[1]=len % 256;

	if (!EVP_EncryptUpdate(ctx, (unsigned char *)buf, &l, b, 2))
	{
		sslerror(ctx, "EVP_EncryptUpdate");
		return -1;
	}

	if (l)
	{
		l=(*writefunc)(buf, l, writefuncarg);

		if (l)
			return l;
	}

	while (len)
	{
		size_t n=len;

		if (n > BUFSIZ / 4)
			n=BUFSIZ/4;

		if (!EVP_EncryptUpdate(ctx, (unsigned char *)buf, &l,
				       (unsigned char *)str, n))
		{
			sslerror(ctx, "EVP_EncryptUpdate");
			return -1;
		}

		if (l)
		{
			l=(*writefunc)(buf, l, writefuncarg);

			if (l)
				return l;
		}

		str += n;
		len -= n;
	}

	return 0;
}

struct tempstring_list {
	struct tempstring_list *next;
	char *url;
	char *pw;
};

struct tlspassword_readinfo {
	char buf[BUFSIZ / 2];
	char *bufptr;
	size_t bufleft;

	int (*readfunc)(char *, size_t, void *);
	void *readfuncarg;

	struct tempstring_list *tl_list, *tl_last;

	int (*readhandler)(struct tlspassword_readinfo *, char *, int);

	unsigned int stringhi;
	char *stringptr;
	size_t stringleft;
	size_t nstrings;
};


static int tlspassword_read(struct tlspassword_readinfo *p,
			    char *buf,
			    size_t nbytes)
{
	while (nbytes)
	{
		size_t c;

		if (p->bufleft == 0)
		{
			int n= (*p->readfunc)(p->buf, sizeof(p->buf),
					      p->readfuncarg);

			if (n <= 0)
				return -1;
			p->bufptr=p->buf;
			p->bufleft=n;
		}

		c=nbytes;

		if (c > p->bufleft)
			c=p->bufleft;

		memcpy(buf, p->bufptr, c);
		p->bufptr += c;
		p->bufleft -= c;
		nbytes -= c;
	}

	return 0;
}

static void tlspassword_readcleanup(struct tlspassword_readinfo *p)
{
	while (p->tl_list)
	{
		struct tempstring_list *t=p->tl_list;

		p->tl_list=t->next;
		if (t->url)
			free(t->url);
		if (t->pw)
			free(t->pw);
		free(t);
	}
}

static int read_stringhi(struct tlspassword_readinfo *, char *, int);

int tlspassword_load( int (*callback)(char *, size_t, void *),
		      void *callback_arg,

		      const char *mpw,

		      void (*readfunc)(const char * const *,
				       const char * const *,
				       void *),
		      void *readfunc_arg)
{
	char buf[BUFSIZ];
	int outl;
	char *p;

	MD5_DIGEST md5_password;
	int iv_len, key_len;
	EVP_CIPHER_CTX ctx;
	const EVP_CIPHER *des=EVP_des_cbc();
	struct tlspassword_readinfo readinfo;
	char header[3];
	size_t l;
	char iv1_buf[EVP_MAX_IV_LENGTH];
	struct tempstring_list *tl;
	const char **urls, **pws;

	readinfo.bufleft=0;
	readinfo.readfunc=callback;
	readinfo.readfuncarg=callback_arg;
	readinfo.tl_list=NULL;
	readinfo.tl_last=NULL;

	md5_digest(mpw, strlen(mpw), md5_password);

	EVP_CIPHER_CTX_init(&ctx);
	iv_len=EVP_CIPHER_iv_length(des);
	key_len=EVP_CIPHER_key_length(des);

	if (tlspassword_read(&readinfo, header, 3) ||
	    tlspassword_read(&readinfo, iv1_buf, iv_len))
	{
		EVP_CIPHER_CTX_cleanup(&ctx);
		return -1;
	}
	if (header[0] != PASSFILEFORMAT)
	{
		errno=EINVAL;
		EVP_CIPHER_CTX_cleanup(&ctx);
		return -1;
	}

	if ((l=(size_t)(unsigned char)header[1] * 256
	     + (unsigned char)header[2]) > sizeof(buf) / 4)
	{
		errno=EINVAL;
		EVP_CIPHER_CTX_cleanup(&ctx);
		return -1;
	}

	if (tlspassword_read(&readinfo, buf, l))
		return -1;

	p=buf + sizeof(buf)/2;
	if (!EVP_DecryptInit_ex(&ctx, des, NULL,
				(unsigned char *)md5_password,
				(unsigned char *)&iv1_buf) ||
	    !EVP_DecryptUpdate(&ctx, (unsigned char *)p, &outl,
			       (unsigned char *)buf, l) ||
	    !EVP_DecryptFinal_ex(&ctx, (unsigned char *)(p += outl), &outl))
	{
		errno=EINVAL;
		EVP_CIPHER_CTX_cleanup(&ctx);
		return -1;
	}

	p += outl;

	if (p - (buf +sizeof(buf)/2) != sizeof(md5_password) + iv_len
	    || memcmp(buf + sizeof(buf)/2, (char *)(&md5_password) + key_len,
		      sizeof(md5_password)-key_len))
	{
		errno=EINVAL;
		EVP_CIPHER_CTX_cleanup(&ctx);
		return -1;
	}

#if 0
	{
		int i;

		printf("KEY: ");

		for (i=0; i<key_len + iv_len; i++)
			printf("%02X", (int)(unsigned char)(p-iv_len-key_len)[i]);
		printf("\n");
	}
#endif

	if (!EVP_DecryptInit_ex(&ctx, des, NULL,
				(unsigned char *)(p-iv_len-key_len),
				(unsigned char *)(p-iv_len)))
	{
		errno=EINVAL;
		EVP_CIPHER_CTX_cleanup(&ctx);
		return -1;
	}

	readinfo.nstrings=0;
	readinfo.readhandler= &read_stringhi;
	for (;;)
	{
		if (readinfo.bufleft == 0)
		{
			outl= (*readinfo.readfunc)(readinfo.buf,
						   sizeof(readinfo.buf),
						   readinfo.readfuncarg);

			if (outl == 0)
				break;

			if (outl < 0)
			{
				tlspassword_readcleanup(&readinfo);
				errno=EINVAL;
				EVP_CIPHER_CTX_cleanup(&ctx);
				return -1;
			}

			readinfo.bufptr=readinfo.buf;
			readinfo.bufleft=outl;
		}

		if (!EVP_DecryptUpdate(&ctx, (unsigned char *)buf, &outl,
				       (unsigned char *)
				       readinfo.bufptr, readinfo.bufleft))
		{
			tlspassword_readcleanup(&readinfo);
			errno=EINVAL;
			EVP_CIPHER_CTX_cleanup(&ctx);
			return -1;
		}
		readinfo.bufleft=0;

		p=buf;
		while (outl)
		{
			int n= (*readinfo.readhandler)(&readinfo, p, outl);

			if (n < 0)
			{
				tlspassword_readcleanup(&readinfo);
				EVP_CIPHER_CTX_cleanup(&ctx);
				return -1;
			}

			p += n;
			outl -= n;
		}
	}

	if (!EVP_DecryptFinal_ex(&ctx, (unsigned char *)buf, &outl))
	{
		tlspassword_readcleanup(&readinfo);
		errno=EINVAL;
		EVP_CIPHER_CTX_cleanup(&ctx);
		return -1;
	}

	p=buf;
	while (outl)
	{
		int n= (*readinfo.readhandler)(&readinfo, p, outl);

		if (n < 0)
		{
			tlspassword_readcleanup(&readinfo);
			errno=EINVAL;
			EVP_CIPHER_CTX_cleanup(&ctx);
			return -1;
		}

		p += n;
		outl -= n;
	}

	if (readinfo.tl_list && readinfo.tl_list->pw == NULL)
		/* Odd # of strings -- no good */
	{
		tlspassword_readcleanup(&readinfo);
		errno=EINVAL;
		EVP_CIPHER_CTX_cleanup(&ctx);
		return (-1);
	}

	if ((urls=malloc((readinfo.nstrings+1) * sizeof(char *))) == NULL ||
	    (pws=malloc((readinfo.nstrings+1) * sizeof(char *))) == NULL)
	{
		if (urls)
			free(urls);

		tlspassword_readcleanup(&readinfo);
		EVP_CIPHER_CTX_cleanup(&ctx);
		return (-1);
	}

	l=0;
	for (tl=readinfo.tl_list; tl; tl=tl->next)
	{
		urls[l]=tl->url;
		pws[l]=tl->pw;
		l++;
	}

	urls[l]=NULL;
	pws[l]=NULL;

	(*readfunc)(urls, pws, readfunc_arg);

	free(urls);
	free(pws);

	tlspassword_readcleanup(&readinfo);
	EVP_CIPHER_CTX_cleanup(&ctx);
	return 0;
}

static int read_stringlo(struct tlspassword_readinfo *info,
			 char *p, int n);

static int read_string(struct tlspassword_readinfo *info,
		       char *p, int n);

static int read_stringhi(struct tlspassword_readinfo *info,
			 char *p, int n)
{
	info->stringhi=(unsigned char)*p;
	info->stringhi *= 256;

	info->readhandler=read_stringlo;
	return 1;
}

static int read_stringlo(struct tlspassword_readinfo *info,
			 char *p, int n)
{
	struct tempstring_list *t;

	info->readhandler=read_string;
	info->stringleft=info->stringhi + (unsigned char)*p;

	if (info->tl_last &&
	    info->tl_last->pw == NULL) /* This string is the pw */
	{
		info->tl_last->pw=malloc(info->stringleft+1);
		if (!info->tl_last->pw)
			return -1;

		info->stringptr=info->tl_last->pw;
		return 1;
	}

	if ((t=(struct tempstring_list *)malloc(sizeof(struct tempstring_list))
	     ) == NULL || (t->url=malloc(info->stringleft+1)) == NULL)
	{
		if (t) free(t);
		return -1;
	}

	if (info->tl_last)
		info->tl_last->next=t;
	else
		info->tl_list=t;
	info->tl_last=t;
	info->stringptr=t->url;
	t->next=NULL;
	t->pw=NULL;
	++info->nstrings;
	return 1;
}

static int read_string(struct tlspassword_readinfo *info, char *p, int n)
{
	if (n > info->stringleft)
		n=info->stringleft;

	memcpy(info->stringptr, p, n);
	info->stringptr += n;
	info->stringleft -= n;

	if (info->stringleft == 0)
	{
		info->readhandler=read_stringhi;
		*info->stringptr=0;
	}

	return n;
}

#else


int tlspassword_init()
{
	return 0;
}


int tlspassword_save( const char * const *urls,
		      const char * const *pwds,
		      const char *mpw,
		      int (*writefunc)(const char *, size_t, void *),
		      void *writefuncarg)
{
	errno=EIO;
	return -1;
}

int tlspassword_load( int (*readfunc)(char *, size_t, void *),
		      void *readfuncarg,

		      const char *mpw,
		      void (*callback)(const char * const *,
				       const char * const *,
				       void *),
		      void *callback_arg)
{
	errno=EIO;
	return -1;
}
#endif
