/*
 * Hashing function for CUPS.
 *
 * Copyright © 2015-2019 by Apple Inc.
 *
 * Licensed under Apache License v2.0.  See the file "LICENSE" for more
 * information.
 */

/*
 * Include necessary headers...
 */

#include "cups-private.h"
#include "debug-internal.h"
#ifdef __APPLE__
#  include <CommonCrypto/CommonDigest.h>
#elif defined(HAVE_GNUTLS)
#  include <gnutls/crypto.h>
#  include "md5-internal.h"
#elif _WIN32
#  include <windows.h>
#  include <bcrypt.h>
#else
#  include "md5-internal.h"
#endif /* __APPLE__ */


/*
 * 'cupsHashData()' - Perform a hash function on the given data.
 *
 * The "algorithm" argument can be any of the registered, non-deprecated IPP
 * hash algorithms for the "job-password-encryption" attribute, including
 * "sha" for SHA-1, "sha-256" for SHA2-256, etc.
 *
 * The "hash" argument points to a buffer of "hashsize" bytes and should be at
 * least 64 bytes in length for all of the supported algorithms.
 *
 * The returned hash is binary data.
 *
 * @since CUPS 2.2/macOS 10.12@
 */

ssize_t					/* O - Size of hash or -1 on error */
cupsHashData(const char    *algorithm,	/* I - Algorithm name */
             const void    *data,	/* I - Data to hash */
             size_t        datalen,	/* I - Length of data to hash */
             unsigned char *hash,	/* I - Hash buffer */
             size_t        hashsize)	/* I - Size of hash buffer */
{
  if (!algorithm || !data || datalen == 0 || !hash || hashsize == 0)
  {
    _cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("Bad arguments to function"), 1);
    return (-1);
  }

#ifdef __APPLE__
  if (!strcmp(algorithm, "md5"))
  {
   /*
    * MD5 (deprecated but widely used...)
    */

    CC_MD5_CTX	ctx;			/* MD5 context */

    if (hashsize < CC_MD5_DIGEST_LENGTH)
      goto too_small;

    CC_MD5_Init(&ctx);
    CC_MD5_Update(&ctx, data, (CC_LONG)datalen);
    CC_MD5_Final(hash, &ctx);

    return (CC_MD5_DIGEST_LENGTH);
  }
  else if (!strcmp(algorithm, "sha"))
  {
   /*
    * SHA-1...
    */

    CC_SHA1_CTX	ctx;			/* SHA-1 context */

    if (hashsize < CC_SHA1_DIGEST_LENGTH)
      goto too_small;

    CC_SHA1_Init(&ctx);
    CC_SHA1_Update(&ctx, data, (CC_LONG)datalen);
    CC_SHA1_Final(hash, &ctx);

    return (CC_SHA1_DIGEST_LENGTH);
  }
#  ifdef CC_SHA224_DIGEST_LENGTH
  else if (!strcmp(algorithm, "sha2-224"))
  {
    CC_SHA256_CTX	ctx;		/* SHA-224 context */

    if (hashsize < CC_SHA224_DIGEST_LENGTH)
      goto too_small;

    CC_SHA224_Init(&ctx);
    CC_SHA224_Update(&ctx, data, (CC_LONG)datalen);
    CC_SHA224_Final(hash, &ctx);

    return (CC_SHA224_DIGEST_LENGTH);
  }
#  endif /* CC_SHA224_DIGEST_LENGTH */
  else if (!strcmp(algorithm, "sha2-256"))
  {
    CC_SHA256_CTX	ctx;		/* SHA-256 context */

    if (hashsize < CC_SHA256_DIGEST_LENGTH)
      goto too_small;

    CC_SHA256_Init(&ctx);
    CC_SHA256_Update(&ctx, data, (CC_LONG)datalen);
    CC_SHA256_Final(hash, &ctx);

    return (CC_SHA256_DIGEST_LENGTH);
  }
  else if (!strcmp(algorithm, "sha2-384"))
  {
    CC_SHA512_CTX	ctx;		/* SHA-384 context */

    if (hashsize < CC_SHA384_DIGEST_LENGTH)
      goto too_small;

    CC_SHA384_Init(&ctx);
    CC_SHA384_Update(&ctx, data, (CC_LONG)datalen);
    CC_SHA384_Final(hash, &ctx);

    return (CC_SHA384_DIGEST_LENGTH);
  }
  else if (!strcmp(algorithm, "sha2-512"))
  {
    CC_SHA512_CTX	ctx;		/* SHA-512 context */

    if (hashsize < CC_SHA512_DIGEST_LENGTH)
      goto too_small;

    CC_SHA512_Init(&ctx);
    CC_SHA512_Update(&ctx, data, (CC_LONG)datalen);
    CC_SHA512_Final(hash, &ctx);

    return (CC_SHA512_DIGEST_LENGTH);
  }
#  ifdef CC_SHA224_DIGEST_LENGTH
  else if (!strcmp(algorithm, "sha2-512_224"))
  {
    CC_SHA512_CTX	ctx;		/* SHA-512 context */
    unsigned char	temp[CC_SHA512_DIGEST_LENGTH];
                                        /* SHA-512 hash */

   /*
    * SHA2-512 truncated to 224 bits (28 bytes)...
    */

    if (hashsize < CC_SHA224_DIGEST_LENGTH)
      goto too_small;

    CC_SHA512_Init(&ctx);
    CC_SHA512_Update(&ctx, data, (CC_LONG)datalen);
    CC_SHA512_Final(temp, &ctx);

    memcpy(hash, temp, CC_SHA224_DIGEST_LENGTH);

    return (CC_SHA224_DIGEST_LENGTH);
  }
#  endif /* CC_SHA224_DIGEST_LENGTH */
  else if (!strcmp(algorithm, "sha2-512_256"))
  {
    CC_SHA512_CTX	ctx;		/* SHA-512 context */
    unsigned char	temp[CC_SHA512_DIGEST_LENGTH];
                                        /* SHA-512 hash */

   /*
    * SHA2-512 truncated to 256 bits (32 bytes)...
    */

    if (hashsize < CC_SHA256_DIGEST_LENGTH)
      goto too_small;

    CC_SHA512_Init(&ctx);
    CC_SHA512_Update(&ctx, data, (CC_LONG)datalen);
    CC_SHA512_Final(temp, &ctx);

    memcpy(hash, temp, CC_SHA256_DIGEST_LENGTH);

    return (CC_SHA256_DIGEST_LENGTH);
  }

#elif defined(HAVE_GNUTLS)
  gnutls_digest_algorithm_t alg = GNUTLS_DIG_UNKNOWN;
					/* Algorithm */
  unsigned char	temp[64];		/* Temporary hash buffer */
  size_t	tempsize = 0;		/* Truncate to this size? */


  if (!strcmp(algorithm, "md5"))
  {
   /*
    * Some versions of GNU TLS disable MD5 without warning...
    */

    _cups_md5_state_t	state;		/* MD5 state info */

    if (hashsize < 16)
      goto too_small;

    _cupsMD5Init(&state);
    _cupsMD5Append(&state, data, (int)datalen);
    _cupsMD5Finish(&state, hash);

    return (16);
  }
  else if (!strcmp(algorithm, "sha"))
    alg = GNUTLS_DIG_SHA1;
  else if (!strcmp(algorithm, "sha2-224"))
    alg = GNUTLS_DIG_SHA224;
  else if (!strcmp(algorithm, "sha2-256"))
    alg = GNUTLS_DIG_SHA256;
  else if (!strcmp(algorithm, "sha2-384"))
    alg = GNUTLS_DIG_SHA384;
  else if (!strcmp(algorithm, "sha2-512"))
    alg = GNUTLS_DIG_SHA512;
  else if (!strcmp(algorithm, "sha2-512_224"))
  {
    alg      = GNUTLS_DIG_SHA512;
    tempsize = 28;
  }
  else if (!strcmp(algorithm, "sha2-512_256"))
  {
    alg      = GNUTLS_DIG_SHA512;
    tempsize = 32;
  }

  if (alg != GNUTLS_DIG_UNKNOWN)
  {
    if (tempsize > 0)
    {
     /*
      * Truncate result to tempsize bytes...
      */

      if (hashsize < tempsize)
        goto too_small;

      gnutls_hash_fast(alg, data, datalen, temp);
      memcpy(hash, temp, tempsize);

      return ((ssize_t)tempsize);
    }

    if (hashsize < gnutls_hash_get_len(alg))
      goto too_small;

    gnutls_hash_fast(alg, data, datalen, hash);

    return ((ssize_t)gnutls_hash_get_len(alg));
  }

#elif _WIN32
  // Use Windows CNG APIs to perform hashing...
  BCRYPT_ALG_HANDLE	alg;		// Algorithm handle
  LPCWSTR		algid = NULL;	// Algorithm ID
  ssize_t		hashlen;	// Hash length
  NTSTATUS		status;		// Status of hash
  unsigned char		temp[64];	// Temporary hash buffer
  size_t		tempsize = 0;	// Truncate to this size?


  if (!strcmp(algorithm, "md5"))
  {
    algid   = BCRYPT_MD5_ALGORITHM;
    hashlen = 16;
  }
  else if (!strcmp(algorithm, "sha"))
  {
    algid   = BCRYPT_SHA1_ALGORITHM;
    hashlen = 20;
  }
  else if (!strcmp(algorithm, "sha2-256"))
  {
    algid   = BCRYPT_SHA256_ALGORITHM;
    hashlen = 32;
  }
  else if (!strcmp(algorithm, "sha2-384"))
  {
    algid   = BCRYPT_SHA384_ALGORITHM;
    hashlen = 48;
  }
  else if (!strcmp(algorithm, "sha2-512"))
  {
    algid   = BCRYPT_SHA512_ALGORITHM;
    hashlen = 64;
  }
  else if (!strcmp(algorithm, "sha2-512_224"))
  {
    algid   = BCRYPT_SHA512_ALGORITHM;
    hashlen = tempsize = 28;
  }
  else if (!strcmp(algorithm, "sha2-512_256"))
  {
    algid   = BCRYPT_SHA512_ALGORITHM;
    hashlen = tempsize = 32;
  }

  if (algid)
  {
    if (hashsize < (size_t)hashlen)
      goto too_small;

    if ((status = BCryptOpenAlgorithmProvider(&alg, algid, NULL, 0)) < 0)
    {
      DEBUG_printf(("2cupsHashData: BCryptOpenAlgorithmProvider returned %d.", status));

      if (status == STATUS_INVALID_PARAMETER)
	_cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("Bad algorithm parameter."), 1);
      else
	_cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("Unable to access cryptographic provider."), 1);

      return (-1);
    }

    if (tempsize > 0)
    {
      // Do a truncated SHA2-512 hash...
      status = BCryptHash(alg, NULL, 0, (PUCHAR)data, (ULONG)datalen, temp, sizeof(temp));
      memcpy(hash, temp, hashlen);
    }
    else
    {
      // Hash directly to buffer...
      status = BCryptHash(alg, NULL, 0, (PUCHAR)data, (ULONG)datalen, hash, (ULONG)hashlen);
    }

    BCryptCloseAlgorithmProvider(alg, 0);

    if (status < 0)
    {
      DEBUG_printf(("2cupsHashData: BCryptHash returned %d.", status));

      if (status == STATUS_INVALID_PARAMETER)
	_cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("Bad hashing parameter."), 1);
      else
	_cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("Hashing failed."), 1);

      return (-1);
    }

    return (hashlen);
  }

#else
 /*
  * No hash support beyond MD5 without CommonCrypto, GNU TLS, or CNG...
  */

  if (!strcmp(algorithm, "md5"))
  {
    _cups_md5_state_t	state;		/* MD5 state info */

    if (hashsize < 16)
      goto too_small;

    _cupsMD5Init(&state);
    _cupsMD5Append(&state, data, (int)datalen);
    _cupsMD5Finish(&state, hash);

    return (16);
  }
  else if (hashsize < 64)
    goto too_small;
#endif /* __APPLE__ */

 /*
  * Unknown hash algorithm...
  */

  _cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("Unknown hash algorithm."), 1);

  return (-1);

 /*
  * We get here if the buffer is too small.
  */

  too_small:

  _cupsSetError(IPP_STATUS_ERROR_INTERNAL, _("Hash buffer too small."), 1);
  return (-1);
}


/*
 * 'cupsHashString()' - Format a hash value as a hexadecimal string.
 *
 * The passed buffer must be at least 2 * hashsize + 1 characters in length.
 *
 * @since CUPS 2.2.7@
 */

const char *				/* O - Formatted string */
cupsHashString(
    const unsigned char *hash,		/* I - Hash */
    size_t              hashsize,	/* I - Size of hash */
    char                *buffer,	/* I - String buffer */
    size_t		bufsize)	/* I - Size of string buffer */
{
  char		*bufptr = buffer;	/* Pointer into buffer */
  static const char *hex = "0123456789abcdef";
					/* Hex characters (lowercase!) */


 /*
  * Range check input...
  */

  if (!hash || hashsize < 1 || !buffer || bufsize < (2 * hashsize + 1))
  {
    if (buffer)
      *buffer = '\0';
    return (NULL);
  }

 /*
  * Loop until we've converted the whole hash...
  */

  while (hashsize > 0)
  {
    *bufptr++ = hex[*hash >> 4];
    *bufptr++ = hex[*hash & 15];

    hash ++;
    hashsize --;
  }

  *bufptr = '\0';

  return (buffer);
}
