/*
 * Copyright (C) 2013 Tobias Brunner
 *
 * Copyright (C) secunet Security Networks AG
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the
 * Free Software Foundation; either version 2 of the License, or (at your
 * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * for more details.
 */

#define _GNU_SOURCE /* for asprintf() */
#include <stdio.h>
#include <openssl/pkcs12.h>

#include "openssl_pkcs12.h"
#include "openssl_util.h"

#include <library.h>
#include <credentials/sets/mem_cred.h>

typedef struct private_pkcs12_t private_pkcs12_t;

/**
 * Private data of a pkcs12_t object.
 */
struct private_pkcs12_t {

	/**
	 * Public pkcs12_t interface.
	 */
	pkcs12_t public;

	/**
	 * OpenSSL PKCS#12 structure
	 */
	PKCS12 *p12;

	/**
	 * Credentials contained in container
	 */
	mem_cred_t *creds;
};

/**
 * Decode certificate and add it to our credential set
 */
static bool add_cert(private_pkcs12_t *this, X509 *x509)
{
	certificate_t *cert = NULL;
	chunk_t encoding;

	if (!x509)
	{	/* no certificate is ok */
		return TRUE;
	}
	encoding = openssl_i2chunk(X509, x509);
	if (encoding.ptr)
	{
		cert = lib->creds->create(lib->creds, CRED_CERTIFICATE, CERT_X509,
								  BUILD_BLOB_ASN1_DER, encoding,
								  BUILD_END);
		if (cert)
		{
			this->creds->add_cert(this->creds, FALSE, cert);
		}
	}
	chunk_free(&encoding);
	X509_free(x509);
	return cert != NULL;
}

/**
 * Add CA certificates to our credential set
 */
static bool add_cas(private_pkcs12_t *this, STACK_OF(X509) *cas)
{
	bool success = TRUE;
	int i;

	if (!cas)
	{	/* no CAs is ok */
		return TRUE;
	}
	for (i = 0; i < sk_X509_num(cas); i++)
	{
		if (!add_cert(this, sk_X509_value(cas, i)))
		{	/* continue to free all X509 objects */
			success = FALSE;
		}
	}
	sk_X509_free(cas);
	return success;
}

/**
 * Decode private key and add it to our credential set
 */
static bool add_key(private_pkcs12_t *this, EVP_PKEY *private)
{
	private_key_t *key;

	if (!private)
	{	/* no private key is ok */
		return TRUE;
	}
	key = openssl_wrap_private_key(private, FALSE);
	if (key)
	{
		this->creds->add_key(this->creds, key);
	}
	return key != NULL;
}

/**
 * Decrypt PKCS#12 file using the given password and unpack credentials
 */
static status_t decrypt_and_unpack_pw(private_pkcs12_t *this, char *password)
{
	STACK_OF(X509) *cas = NULL;
	EVP_PKEY *private;
	X509 *cert;

	if (PKCS12_parse(this->p12, password, &private, &cert, &cas))
	{
		/* if at least one is successful, we accept it */
		if ((int)add_key(this, private) |
			(int)add_cert(this, cert) |
			(int)add_cas(this, cas))
		{
			return SUCCESS;
		}
		return FAILED;
	}
	return PARSE_ERROR;
}

/**
 * Decrypt PKCS#12 file and unpack credentials
 */
static bool decrypt_and_unpack(private_pkcs12_t *this)
{
	enumerator_t *enumerator;
	shared_key_t *shared;
	chunk_t key;
	char *password;
	bool success = FALSE;

	/* try without password first */
	switch (decrypt_and_unpack_pw(this, NULL))
	{
		case PARSE_ERROR:
			break;
		case SUCCESS:
			return TRUE;
		default:
			return FALSE;
	}

	enumerator = lib->credmgr->create_shared_enumerator(lib->credmgr,
										SHARED_PRIVATE_KEY_PASS, NULL, NULL);
	while (enumerator->enumerate(enumerator, &shared, NULL, NULL))
	{
		key = shared->get_key(shared);
		if (!key.ptr || asprintf(&password, "%.*s", (int)key.len, key.ptr) < 0)
		{
			password = strdup("");
		}
		switch (decrypt_and_unpack_pw(this, password))
		{
			case PARSE_ERROR:
				/* password was incorrect, try another */
				memwipe(password, strlen(password));
				free(password);
				continue;
			case SUCCESS:
				success = TRUE;
				break;
			default:
				/* password was correct but we were unable to unpack anything */
				break;
		}
		memwipe(password, strlen(password));
		free(password);
		break;
	}
	enumerator->destroy(enumerator);
	return success;
}

METHOD(container_t, get_type, container_type_t,
	private_pkcs12_t *this)
{
	return CONTAINER_PKCS12;
}

METHOD(pkcs12_t, create_cert_enumerator, enumerator_t*,
	private_pkcs12_t *this)
{
	return this->creds->set.create_cert_enumerator(&this->creds->set, CERT_ANY,
												   KEY_ANY, NULL, FALSE);
}

METHOD(pkcs12_t, create_key_enumerator, enumerator_t*,
	private_pkcs12_t *this)
{
	return this->creds->set.create_private_enumerator(&this->creds->set,
													  KEY_ANY, NULL);
}

METHOD(container_t, destroy, void,
	private_pkcs12_t *this)
{
	if (this->p12)
	{
		PKCS12_free(this->p12);
	}
	this->creds->destroy(this->creds);
	free(this);
}

/**
 * Parse a PKCS#12 container
 */
static pkcs12_t *parse(chunk_t blob)
{
	private_pkcs12_t *this;
	BIO *bio;

	INIT(this,
		.public = {
			.container = {
				.get_type = _get_type,
				.create_signature_enumerator = (void*)enumerator_create_empty,
				.get_data = (void*)return_false,
				.get_encoding = (void*)return_false,
				.destroy = _destroy,
			},
			.create_cert_enumerator = _create_cert_enumerator,
			.create_key_enumerator = _create_key_enumerator,
		},
		.creds = mem_cred_create(),
	);

	bio = BIO_new_mem_buf(blob.ptr, blob.len);
	this->p12 = d2i_PKCS12_bio(bio, NULL);
	BIO_free(bio);

	if (!this->p12 || !decrypt_and_unpack(this))
	{
		destroy(this);
		return NULL;
	}
	return &this->public;
}

/*
 * Defined in header
 */
pkcs12_t *openssl_pkcs12_load(container_type_t type, va_list args)
{
	chunk_t blob = chunk_empty;

	while (TRUE)
	{
		switch (va_arg(args, builder_part_t))
		{
			case BUILD_BLOB_ASN1_DER:
				blob = va_arg(args, chunk_t);
				continue;
			case BUILD_END:
				break;
			default:
				return NULL;
		}
		break;
	}
	return blob.len ? parse(blob) : NULL;
}
