/* -*- Mode: C; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*-
 * GXSNMP -- An snmp mangament application
 * Copyright (C) 2001 Gregory McLean & Jochen Friedrich
 *
 * 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.
 *
 * 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.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * This object implements a framework for message digest based cryptographic
 * hash functions. The block length is currently hardcoded to 64 bytes.
 */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <glib.h>
#include <gobject/gobject.h>
#include "gxsnmp_hash.h"
#include "gxsnmp_digest.h"

static void gxsnmp_digest_init       (GxsnmpDigest      *digest);
static void gxsnmp_digest_class_init (GxsnmpDigestClass *klass);
static void gxsnmp_digest_finalize   (GObject           *object);

static void gxsnmp_digest_update     (GxsnmpHash        *hash,
	       			      guchar const      *buf,
				      guint 		 len);
static void gxsnmp_digest_final      (GxsnmpHash 	*hash,
	       			      guchar		*buf);
static void gxsnmp_digest_reset	     (GxsnmpHash	*hash);
static void gxsnmp_digest_transform  (GxsnmpDigest      *digest);

static gpointer parent_class = NULL;

GType
gxsnmp_digest_get_type (void)
{
  static GType object_type = 0;

  if (!object_type)
    {
      static const GTypeInfo object_info =
        {
          sizeof (GxsnmpDigestClass),
          (GBaseInitFunc) NULL,
          (GBaseFinalizeFunc) NULL,
          (GClassInitFunc) gxsnmp_digest_class_init,
          NULL,           /* class_finalize */
          NULL,           /* class_data */
          sizeof (GxsnmpDigest),
          0,              /* n_preallocs */
          (GInstanceInitFunc) gxsnmp_digest_init,
        };

        object_type = g_type_register_static (gxsnmp_hash_get_type(),
                                              "GxsnmpDigest",
                                              &object_info, 0);
    }
  return object_type;
}

static void
gxsnmp_digest_init (GxsnmpDigest *digest)
{
  digest->endianess = FALSE;
  digest->countLo   = 0;
  digest->countHi   = 0;
}

static void
gxsnmp_digest_class_init (GxsnmpDigestClass *klass)
{
  GObjectClass    *object_class = G_OBJECT_CLASS (klass);
  GxsnmpHashClass *hash_class   = GXSNMP_HASH_CLASS (klass);

  parent_class = g_type_class_peek_parent (klass);

  klass->transform = gxsnmp_digest_transform;

  hash_class->update  = gxsnmp_digest_update;
  hash_class->final   = gxsnmp_digest_final;
  hash_class->reset   = gxsnmp_digest_reset;

  object_class->finalize = gxsnmp_digest_finalize;
}

static void
gxsnmp_digest_finalize (GObject *object)
{
  G_OBJECT_CLASS (parent_class)->finalize (object);
}

static void
gxsnmp_digest_transform (GxsnmpDigest *digest)
{
  /* virtual */
}

static void
byteReverse(guint32 *buf, guint longs, gboolean endianess)
{
  guint32 value;

#ifdef WORDS_BIGENDIAN
  if (endianess) return;
#else
  if (!endianess) return;
#endif

  while( longs --)
    {
      value = *buf;
      value = ( ( value & 0xFF00FF00L ) >> 8  ) | \
	      ( ( value & 0x00FF00FFL ) << 8 );
      *buf++ = ( value << 16 ) | ( value >> 16 );
    }
}

static void
gxsnmp_digest_update (GxsnmpHash *hash, guchar const *buf, guint len)
{
  GxsnmpDigestClass *class;
  guint32 tmp, dataCount;
  GxsnmpDigest *digest;

  digest = GXSNMP_DIGEST(hash);

  g_return_if_fail (digest != NULL);
  g_return_if_fail (GXSNMP_IS_DIGEST(digest));
  g_return_if_fail (buf != NULL);

  class = GXSNMP_DIGEST_GET_CLASS (digest);

  g_return_if_fail (class->transform != NULL);

  /* Update bitcount */
  tmp = digest->countLo;
  if ( ( digest->countLo = tmp + ( ( guint32 ) len << 3 ) ) < tmp )
    digest->countHi++;
  digest->countHi += len >> 29;

  /* Get count of bytes already in data */
  dataCount = ( tmp >> 3 ) & 0x3F;

  /* Handle any leading odd-sized chunks */
  if( dataCount )
    {
      guchar *p = (guchar *) digest->data + dataCount;

      dataCount = 64 - dataCount;
      if( len < dataCount )
	{
	  g_memmove( p, buf, len );
	  return;
	}
      g_memmove(p, buf, dataCount);
      byteReverse(digest->data, 16, digest->endianess);
      class->transform(digest);
      buf += dataCount;
      len -= dataCount;
    }

  /* Process data in 64-byte chunks */
  while (len >= 64)
    {
      g_memmove(digest->data, buf, 64);
      byteReverse(digest->data, 16, digest->endianess);
      class->transform(digest);
      buf += 64;
      len -= 64;
    }

  /* Handle any remaining bytes of data. */
  g_memmove( digest->data, buf, len);
}

static void
gxsnmp_digest_final (GxsnmpHash *hash, guchar *buf)
{
  GxsnmpDigestClass *class;
  GxsnmpDigest *digest;
  guint32 dataCount;
  guchar *p;

  digest = GXSNMP_DIGEST (hash);

  g_return_if_fail (digest != NULL);
  g_return_if_fail (GXSNMP_IS_DIGEST(digest));
  g_return_if_fail (buf != NULL);

  class      = GXSNMP_DIGEST_GET_CLASS (digest);

  g_return_if_fail (class->transform != NULL);

  /* Compute number of bytes mod 64 */
  dataCount = ( digest->countLo >> 3 ) & 0x3F;

  /* Set the first char of padding to 0x80.  This is safe since there is
     always at least one byte free */

  p = (guchar *) digest->data + dataCount;
  *p++ = 0x80;

  /* Bytes of padding needed to make 64 bytes */
  dataCount = 64 - 1 - dataCount;

  /* Pad out to 56 mod 64 */
  if (dataCount < 8)
    {
      /* Two lots of padding:  Pad the first block to 64 bytes */
      memset( p, 0, dataCount );
      byteReverse(digest->data, 16, digest->endianess);
      class->transform(digest);

      /* Now fill the next block with 56 bytes */
      memset( digest->data, 0, 56);
    }
  else
    {
      /* Pad block to 56 bytes */
      memset( p, 0, dataCount - 8 );
    }

  if (!digest->endianess)
    {
      byteReverse(digest->data, 14, digest->endianess);

      /* Append length in bits and transform */
      digest->data[ 14 ] = digest->countLo;
      digest->data[ 15 ] = digest->countHi;
    }
  else
    {
      /* Append length in bits and transform */
      digest->data[ 14 ] = digest->countHi;
      digest->data[ 15 ] = digest->countLo;

      byteReverse(digest->data, 14, digest->endianess);
    }
  class->transform(digest);
  byteReverse(hash->result, hash->hashlen / sizeof(guint32), digest->endianess);
  g_memmove(buf, hash->result, hash->hashlen);
  memset(digest->data, 0, 64);
  memset(hash->result, 0, hash->hashlen);
}

static void
gxsnmp_digest_reset (GxsnmpHash *hash)
{
  GxsnmpDigest *digest;

  digest = GXSNMP_DIGEST (hash);

  g_return_if_fail (digest != NULL);
  g_return_if_fail (GXSNMP_IS_DIGEST(digest));

  digest->countLo   = 0;
  digest->countHi   = 0;
}

void
gxsnmp_digest_hmac (GxsnmpDigest *digest, guchar *data, guint datalen,
                    guchar *key, guint keylen, guchar *out)
{
  guint       i;
  guchar     *ky;
  guchar     *isha;
  guchar      buf[64];
  GxsnmpHash *hash;

  hash = GXSNMP_HASH (digest);

  ky   = g_malloc(hash->hashlen);
  isha = g_malloc(hash->hashlen);

  if (keylen > 64)
    {
      gxsnmp_hash_update(hash, key, keylen);
      gxsnmp_hash_final(hash, ky);
      gxsnmp_hash_reset(hash);

      key    = ky ;
      keylen = hash->hashlen;
    }

  /**** Inner Digest ****/

  /* Pad the key for inner digest */
  for (i = 0; i < keylen; ++i)  buf[i] = key[i] ^ 0x36 ;
  for (i = keylen; i < 64; ++i) buf[i] = 0x36 ;

  gxsnmp_hash_update(hash, buf, 64);
  gxsnmp_hash_update(hash, data, datalen);

  gxsnmp_hash_final(hash, isha);
  gxsnmp_hash_reset(hash);

  /**** Outter Digest ****/

  /* Pad the key for outter digest */
  for (i = 0; i < keylen; ++i)  buf[i] = key[i] ^ 0x5C ;
  for (i = keylen; i < 64; ++i) buf[i] = 0x5C ;

  gxsnmp_hash_update(hash, buf, 64);
  gxsnmp_hash_update(hash, isha, hash->hashlen);
  gxsnmp_hash_final(hash, out);

  g_free(ky);
  g_free(isha);
}
