/* SPDX-FileCopyrightText: 2020-2023 Greenbone AG
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "passwordbasedauthentication.h"
// internal usage to have access to gvm_auth initialized to verify if
// initialization is needed
#include "authutils.c"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
// UFC_crypt defines crypt_r when only when __USE_GNU is set
// this shouldn't affect other implementations
#define __USE_GNU
#include <crypt.h>
// INVALID_HASH is used on verify when the given hash is a NULL pointer.
// This is done to not directly jump to exit with a INVALID_HASH result
// but rather keep calculating to make it a little bit harder to guess
// if a user exists or not based on timing.
#define INVALID_HASH "1234567890$"
#ifndef CRYPT_GENSALT_OUTPUT_SIZE
#define CRYPT_GENSALT_OUTPUT_SIZE 192
#endif

#ifndef CRYPT_OUTPUT_SIZE
#define CRYPT_OUTPUT_SIZE 384
#endif

/**
 * @brief Check if a prefix is supported.
 *
 * @param[in]  id  Prefix.
 *
 * @return 1 if supported, else 0.
 */
static int
is_prefix_supported (const char *id)
{
  return strcmp (PREFIX_DEFAULT, id) == 0;
}

// we assume something else than libxcrypt > 3.1; like UFC-crypt
// libxcrypt sets a macro of crypt_gensalt_r to crypt_gensalt_rn
// therefore we could use that mechanism to figure out if we are on
// debian buster or newer.
#ifndef EXTERNAL_CRYPT_GENSALT_R

// used printables within salt
const char ascii64[] =
  "./0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";

/**
 * @brief Try to get random bytes.
 *
 * @param[in]  buf     Destination for bytes.
 * @param[in]  buflen  Number of bytes to get.
 *
 * @return 0 on success, else error.
 */
static int
get_random (char *buf, size_t buflen)
{
  FILE *fp = fopen ("/dev/urandom", "r");
  int result = 0;
  if (fp == NULL)
    {
      result = -1;
      goto exit;
    }
  size_t nread = fread (buf, 1, buflen, fp);
  fclose (fp);
  if (nread < buflen)
    {
      result = -2;
    }

exit:
  return result;
}

/**
 * @brief Generate string suitable for use as setting when hashing a passphrase.
 *
 * If prefix is a NULL pointer, the current best default is used; if rbytes
 * is a NULL pointer, random data will be retrieved from the operating system
 * if possible.
 *
 * @param[in]  prefix  Controls which hash function will be used.
 * @param[in]  count   Controls the computional cost of the hash.
 * @param[in]  rbytes  Should point to nrbytes bytes of random data.
 * @param[in]  nrbytes  Number of bytes in rbytes.
 * @param[out] output   The generated setting string is written here.
 * @param[in]  output_size  Length of output. Must be at least
 *                          CRYPT_GENSALT_OUTPUT_SIZE.
 *
 * @return On success \p output, else NULL.
 */
char *
crypt_gensalt_r (const char *prefix, unsigned long count, const char *rbytes,
                 int nrbytes, char *output, int output_size);
char *
crypt_gensalt_r (const char *prefix, unsigned long count, const char *rbytes,
                 int nrbytes, char *output, int output_size)
{
  char *internal_rbytes = NULL;
  unsigned int written = 0, used = 0;
  unsigned long value = 0;
  if ((rbytes != NULL && nrbytes < 3) || output_size < 16
      || !is_prefix_supported (prefix))
    {
      output[0] = '*';
      goto exit;
    }
  if (rbytes == NULL)
    {
      internal_rbytes = malloc (16);
      if (get_random (internal_rbytes, 16) != 0)
        {
          output[0] = '*';
          goto exit;
        }
      nrbytes = 16;
      rbytes = internal_rbytes;
    }
  written = snprintf (output, output_size, "%srounds=%lu$",
                      prefix == NULL ? PREFIX_DEFAULT : prefix, count);
  while (written + 5 < (unsigned int) output_size
         && used + 3 < (unsigned int) nrbytes && (used * 4 / 3) < 16)
    {
      value = ((unsigned long) rbytes[used + 0] << 0)
              | ((unsigned long) rbytes[used + 1] << 8)
              | ((unsigned long) rbytes[used + 2] << 16);
      output[written] = ascii64[value & 0x3f];
      output[written + 1] = ascii64[(value >> 6) & 0x3f];
      output[written + 2] = ascii64[(value >> 12) & 0x3f];
      output[written + 3] = ascii64[(value >> 18) & 0x3f];
      written += 4;
      used += 3;
    }
  output[written] = '\0';
exit:
  if (internal_rbytes != NULL)
    free (internal_rbytes);
  return output[0] == '*' ? 0 : output;
}

#endif

/**
 * @brief Init PBA.
 *
 * @param[in] pepper  A static hidden addition to the randomly generated salt.
 * @param[in] pepper_size  The size of pepper; it must not be larger than
 *                         MAX_PEPPER_SIZE.
 * @param[in] count        Number of rounds used to calculate the hash. 0 to
 *                         use COUNT_DEFAULT.
 * @param[in] prefix       The algorithm used, if NULL then the most secure
 *                         available algorithm will be used.
 *
 * @return Settings, or NULL on error. Free with pba_finalize.
 */
struct PBASettings *
pba_init (const char *pepper, unsigned int pepper_size, unsigned int count,
          char *prefix)
{
  unsigned int i = 0;
  struct PBASettings *result = NULL;
  if (pepper_size > MAX_PEPPER_SIZE)
    goto exit;
  if (prefix != NULL && !is_prefix_supported (prefix))
    goto exit;
  result = malloc (sizeof (struct PBASettings));
  for (i = 0; i < MAX_PEPPER_SIZE; i++)
    result->pepper[i] = pepper != NULL && i < pepper_size ? pepper[i] : 0;
  result->count = count == 0 ? COUNT_DEFAULT : count;
  result->prefix = prefix == NULL ? PREFIX_DEFAULT : prefix;
exit:
  return result;
}

/**
 * @brief Cleanup PBA settings.
 *
 * @param[in]  settings  PBA settings.
 */
void
pba_finalize (struct PBASettings *settings)
{
  free (settings);
}

/**
 * @brief Check if a PBA settings is PHC compliant.
 *
 * @param[in]  setting  Setting.
 *
 * @return 1 if compliant, else 0.
 */
static int
pba_is_phc_compliant (const char *setting)
{
  if (setting == NULL)
    {
      return 1;
    }
  return strlen (setting) > 1 && setting[0] == '$';
}

/**
 * @brief Create a password hash.
 *
 * @param[in]  setting   PBA settings.
 * @param[in]  password  Password.
 *
 * @return Hash. Must be freed with free().
 */
char *
pba_hash (struct PBASettings *setting, const char *password)
{
  char *result = NULL, *settings = NULL, *tmp, *rslt;
  int i;
  struct crypt_data *data = NULL;

  if (!setting || !password)
    goto exit;
  if (!is_prefix_supported (setting->prefix))
    goto exit;
  settings = malloc (CRYPT_GENSALT_OUTPUT_SIZE);
  if (crypt_gensalt_r (setting->prefix, setting->count, NULL, 0, settings,
                       CRYPT_GENSALT_OUTPUT_SIZE)
      == NULL)
    goto exit;
  tmp = settings + strlen (settings) - 1;
  for (i = MAX_PEPPER_SIZE - 1; i > -1; i--)
    {
      if (setting->pepper[i] != 0)
        tmp[0] = setting->pepper[i];
      tmp--;
    }

  data = calloc (1, sizeof (struct crypt_data));
  rslt = crypt_r (password, settings, data);
  if (rslt == NULL)
    goto exit;
  result = calloc (1, CRYPT_OUTPUT_SIZE);
  memcpy (result, rslt, CRYPT_OUTPUT_SIZE);
  // remove pepper, by jumping to begin of applied pepper within result
  // and overriding it.
  tmp = result + (tmp - settings);
  for (i = 0; i < MAX_PEPPER_SIZE; i++)
    {
      tmp++;
      if (setting->pepper[i] != 0)
        tmp[0] = '0';
    }
exit:
  if (data != NULL)
    free (data);
  if (settings != NULL)
    free (settings);
  return result;
}

/**
 * @brief Verify a password hash.
 *
 * @param[in]  setting   PBA settings.
 * @param[in]  hash      Hash.
 * @param[in]  password  Password.
 *
 * @return Validity. VALID, UPDATE_RECOMMENDED, ...
 */
enum pba_rc
pba_verify_hash (const struct PBASettings *setting, const char *hash,
                 const char *password)
{
  char *cmp, *tmp = NULL;
  struct crypt_data *data = NULL;
  int i = 0;
  enum pba_rc result = ERR;

  char *invalid_hash = calloc (1, CRYPT_OUTPUT_SIZE);
  memset (invalid_hash, 0, CRYPT_OUTPUT_SIZE);
  memcpy (invalid_hash, INVALID_HASH, strlen (INVALID_HASH));

  if (!setting)
    goto exit;
  if (!is_prefix_supported (setting->prefix))
    goto exit;
  if (pba_is_phc_compliant (hash) != 0)
    {
      int hash_size;
      hash_size = hash ? strlen (hash) : strlen (invalid_hash);

      data = calloc (1, sizeof (struct crypt_data));
      // manipulate hash to reapply pepper
      tmp = calloc (1, CRYPT_OUTPUT_SIZE);

      memset (tmp, 0, CRYPT_OUTPUT_SIZE);
      memcpy (tmp, hash ? hash : invalid_hash,
              (hash_size < CRYPT_OUTPUT_SIZE) ? hash_size
                                              : CRYPT_OUTPUT_SIZE - 1);
      cmp = strrchr (tmp, '$');
      for (i = MAX_PEPPER_SIZE - 1; i > -1; i--)
        {
          cmp--;
          if (setting->pepper[i] != 0)
            cmp[0] = setting->pepper[i];
        }
      // some crypt_r implementations cannot handle if password is a
      // NULL pointer and run into SEGMENTATION faults.
      // Therefore we set it to ""
      cmp = crypt_r (password ? password : "", tmp, data);
      if (strcmp (tmp, cmp) == 0)
        result = VALID;
      else
        result = INVALID;
    }
  else
    {
      // assume authutils hash handling
      // initialize gvm_auth utils if not already initialized
      if (initialized == FALSE && gvm_auth_init () != 0)
        {
          goto exit;
        }
      // verify result of gvm_authenticate_classic
      i = gvm_authenticate_classic (NULL, password, hash);
      if (i == 0)
        result = UPDATE_RECOMMENDED;
      else if (i == 1)
        result = INVALID;
    }
exit:
  free (invalid_hash);
  if (data != NULL)
    free (data);
  if (tmp != NULL)
    free (tmp);
  return result;
}
