/*
 * hmac.c
 *
 * MontaVista RMCP+ code for doing HMAC, both SHA1 and MD5
 *
 * Author: MontaVista Software, Inc.
 *         Corey Minyard <minyard@mvista.com>
 *         source@mvista.com
 *
 * Copyright 2004 MontaVista Software Inc.
 *
 *  This program is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public License
 *  as published by the Free Software Foundation; either version 2 of
 *  the License, or (at your option) any later version.
 *
 *
 *  THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED
 *  WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
 *  MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 *  IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 *  INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 *  BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 *  OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 *  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
 *  TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
 *  USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this program; if not, write to the Free
 *  Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include <config.h>

#ifdef HAVE_OPENSSL

#include <errno.h>
#include <string.h>
#include <openssl/hmac.h>
#include <OpenIPMI/ipmi_lan.h>
#include <OpenIPMI/internal/ipmi_malloc.h>

typedef struct hmac_info_s
{
    const EVP_MD *evp_md;
    unsigned int  klen;
    unsigned int  ilen;
    unsigned char k[20];
} hmac_info_t;

static int
hmac_sha1_init(ipmi_con_t       *ipmi,
	       ipmi_rmcpp_auth_t *ainfo,
	       void             **integ_data)
{
    hmac_info_t         *info;
    const unsigned char *k;
    unsigned int        klen;

    info = ipmi_mem_alloc(sizeof(*info));
    if (!info)
	return ENOMEM;

    if (ipmi_rmcpp_auth_get_sik_len(ainfo) < 20)
	return EINVAL;

    if (ipmi->hacks & IPMI_CONN_HACK_RMCPP_INTEG_SIK)
	k = ipmi_rmcpp_auth_get_sik(ainfo, &klen);
    else
	k = ipmi_rmcpp_auth_get_k1(ainfo, &klen);
    if (klen < 20)
	return EINVAL;

    memcpy(info->k, k, 20);
    info->klen = 20;
    info->ilen = 12;

    info->evp_md = EVP_sha1();
    *integ_data = info;
    return 0;
}

static int
hmac_md5_init(ipmi_con_t       *ipmi,
	      ipmi_rmcpp_auth_t *ainfo,
	      void             **integ_data)
{
    hmac_info_t         *info;
    const unsigned char *k;
    unsigned int        klen;

    info = ipmi_mem_alloc(sizeof(*info));
    if (!info)
	return ENOMEM;

    if (ipmi_rmcpp_auth_get_sik_len(ainfo) < 16)
	return EINVAL;

    k = ipmi_rmcpp_auth_get_sik(ainfo, &klen);
    if (klen < 16)
	return EINVAL;

    memcpy(info->k, k, 16);
    info->klen = 16;
    info->ilen = 16;

    info->evp_md = EVP_md5();
    *integ_data = info;
    return 0;
}

static void
hmac_free(ipmi_con_t *ipmi,
	  void       *integ_data)
{
    hmac_info_t *info = integ_data;

    memset(info->k, 0, sizeof(info->k));
    ipmi_mem_free(integ_data);
}

static int
hmac_pad(ipmi_con_t    *ipmi,
	 void          *integ_data,
	 unsigned char *payload,
	 unsigned int  *payload_len,
	 unsigned int  max_payload_len)
{
    unsigned char  *p = payload;
    unsigned int   l = *payload_len;
    unsigned int   count = 0;

    /* Pad so that when we add two bytes (the pad length and the next
       header) the result is on a multiple of 4 boundary. */
    while (((l+2) % 4) != 0) {
	if (l == max_payload_len)
	    return E2BIG;
	p[l] = 0xff;
	l++;
	count++;
    }

    /* Add the padding length.  The next header gets added later. */
    if (l == max_payload_len)
	return E2BIG;
    p[l] = count;
    l++;

    *payload_len = l;
    return 0;
}

static int
hmac_add(ipmi_con_t    *ipmi,
	 void          *integ_data,
	 unsigned char *payload,
	 unsigned int  *payload_len,
	 unsigned int  max_payload_len)
{
    hmac_info_t   *info = integ_data;
    unsigned char *p = payload;
    unsigned int  l = *payload_len;
    unsigned int  ilen;
    unsigned char integ[20];

    if (l+info->ilen+1 > max_payload_len)
	return E2BIG;

    if (l < 4)
	return E2BIG;

    p[l] = 0x07; /* Add the next header */
    l++;

    HMAC(info->evp_md, info->k, info->klen, p+4, l-4, integ, &ilen);
    memcpy(p+l, integ, ilen);
    l += info->ilen;

    *payload_len = l;
    return 0;
}

static int
hmac_check(ipmi_con_t    *ipmi,
	   void          *integ_data,
	   unsigned char *payload,
	   unsigned int  payload_len,
	   unsigned int  total_len)
{
    hmac_info_t   *info = integ_data;
    unsigned char *p = payload;
    unsigned int  l = payload_len;
    unsigned int  ilen;
    unsigned char new_integ[20];

    /* We don't authenticate this part of the header. */
    p += 4;
    l -= 4;

    if ((total_len - payload_len) < info->ilen+1)
	return EINVAL;

    /* We add 1 to the length because we also check the next header
       field. */
    HMAC(info->evp_md, info->k, info->klen, p, l+1, new_integ, &ilen);
    if (memcmp(new_integ, p+l+1, info->ilen) != 0)
	return EINVAL;

    return 0;
}

static ipmi_rmcpp_integrity_t hmac_sha1_integ =
{
    .integ_init = hmac_sha1_init,
    .integ_free = hmac_free,
    .integ_pad = hmac_pad,
    .integ_add = hmac_add,
    .integ_check = hmac_check
};

static ipmi_rmcpp_integrity_t hmac_md5_integ =
{
    .integ_init = hmac_md5_init,
    .integ_free = hmac_free,
    .integ_pad = hmac_pad,
    .integ_add = hmac_add,
    .integ_check = hmac_check
};
#endif /* HAVE_OPENSSL */

void
i_ipmi_hmac_shutdown(void)
{
#ifdef HAVE_OPENSSL
    ipmi_rmcpp_register_integrity
	(IPMI_LANP_INTEGRITY_ALGORITHM_HMAC_SHA1_96, NULL);
    ipmi_rmcpp_register_integrity
	(IPMI_LANP_INTEGRITY_ALGORITHM_HMAC_MD5_128, NULL);
#endif
}

int
i_ipmi_hmac_init(void)
{
#ifdef HAVE_OPENSSL
    int rv = 0;

    rv = ipmi_rmcpp_register_integrity
	(IPMI_LANP_INTEGRITY_ALGORITHM_HMAC_SHA1_96, &hmac_sha1_integ);
    if (rv)
	return rv;

    rv = ipmi_rmcpp_register_integrity
	(IPMI_LANP_INTEGRITY_ALGORITHM_HMAC_MD5_128, &hmac_md5_integ);
    if (rv) {
	i_ipmi_hmac_shutdown();
	return rv;
    }
#endif

    return 0;
}
