/*
  This file is part of TALER
  Copyright (C) 2023 Taler Systems SA

  TALER 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 3, or (at your option) any later version.

  TALER 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
  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file util/crypto_confirmation.c
 * @brief confirmation computation
 * @author Christian Grothoff
 * @author Priscilla Huang
 */
#include "platform.h"
#include "taler_util.h"
#include "taler_mhd_lib.h"
#include <gnunet/gnunet_db_lib.h>
#include <gcrypt.h>

/**
 * How long is a TOTP code valid?
 */
#define TOTP_VALIDITY_PERIOD GNUNET_TIME_relative_multiply ( \
          GNUNET_TIME_UNIT_SECONDS, 30)

/**
 * Range of time we allow (plus-minus).
 */
#define TIME_INTERVAL_RANGE 2


/**
 * Compute TOTP code at current time with offset
 * @a time_off for the @a key.
 *
 * @param ts current time
 * @param time_off offset to apply when computing the code
 * @param key pos_key in binary
 * @param key_size number of bytes in @a key
 */
static uint64_t
compute_totp (struct GNUNET_TIME_Timestamp ts,
              int time_off,
              const void *key,
              size_t key_size)
{
  struct GNUNET_TIME_Absolute now;
  time_t t;
  uint64_t ctr;
  uint8_t hmac[20]; /* SHA1: 20 bytes */

  now = ts.abs_time;
  while (time_off < 0)
  {
    now = GNUNET_TIME_absolute_subtract (now,
                                         TOTP_VALIDITY_PERIOD);
    time_off++;
  }
  while (time_off > 0)
  {
    now = GNUNET_TIME_absolute_add (now,
                                    TOTP_VALIDITY_PERIOD);
    time_off--;
  }
  t = now.abs_value_us / GNUNET_TIME_UNIT_SECONDS.rel_value_us;
  ctr = GNUNET_htonll (t / 30LLU);

  {
    gcry_md_hd_t md;
    const unsigned char *mc;

    GNUNET_assert (GPG_ERR_NO_ERROR ==
                   gcry_md_open (&md,
                                 GCRY_MD_SHA1,
                                 GCRY_MD_FLAG_HMAC));
    GNUNET_assert (GPG_ERR_NO_ERROR ==
                   gcry_md_setkey (md,
                                   key,
                                   key_size));
    gcry_md_write (md,
                   &ctr,
                   sizeof (ctr));
    mc = gcry_md_read (md,
                       GCRY_MD_SHA1);
    GNUNET_assert (NULL != mc);
    GNUNET_memcpy (hmac,
                   mc,
                   sizeof (hmac));
    gcry_md_close (md);
  }

  {
    uint32_t code = 0;
    int offset;

    offset = hmac[sizeof (hmac) - 1] & 0x0f;
    for (int count = 0; count < 4; count++)
      code |= ((uint32_t) hmac[offset + 3 - count]) << (8 * count);
    code &= 0x7fffffff;
    /* always use 8 digits (maximum) */
    code = code % 100000000;
    return code;
  }
}


int
TALER_rfc3548_base32decode (const char *val,
                            size_t val_size,
                            void *key,
                            size_t key_len)
{
  /**
   * 32 characters for decoding, using RFC 3548.
   */
  static const char *decTable__ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
  unsigned char *udata = key;
  unsigned int wpos = 0;
  unsigned int rpos = 0;
  unsigned int bits = 0;
  unsigned int vbit = 0;

  while ((rpos < val_size) || (vbit >= 8))
  {
    if ((rpos < val_size) && (vbit < 8))
    {
      const char *p;
      char c = val[rpos++];

      if (c == '=')
      {
        /* padding character */
        if (rpos == val_size)
          break; /* Ok, 1x '=' padding is allowed */
        if ( ('=' == val[rpos]) &&
             (rpos + 1 == val_size) )
          break; /* Ok, 2x '=' padding is allowed */
        return -1; /* invalid padding */
      }
      p = strchr (decTable__, toupper (c));
      if (! p)
      {
        /* invalid character */
        return -1;
      }
      bits = (bits << 5) | (p - decTable__);
      vbit += 5;
    }
    if (vbit >= 8)
    {
      udata[wpos++] = (bits >> (vbit - 8)) & 0xFF;
      vbit -= 8;
    }
  }
  return wpos;
}


/**
 * @brief Builds POS confirmation to verify payment.
 *
 * @param h_key opaque key for the totp operation
 * @param h_key_len size of h_key in bytes
 * @param ts current time
 * @return Token on success, NULL of failure
 */
static char *
executive_totp (void *h_key,
                size_t h_key_len,
                struct GNUNET_TIME_Timestamp ts)
{
  uint64_t code; /* totp code */
  char *ret;
  ret = NULL;

  for (int i = -TIME_INTERVAL_RANGE; i<= TIME_INTERVAL_RANGE; i++)
  {
    code = compute_totp (ts,
                         i,
                         h_key,
                         h_key_len);
    if (NULL == ret)
    {
      GNUNET_asprintf (&ret,
                       "%08llu",
                       (unsigned long long) code);
    }
    else
    {
      char *tmp;

      GNUNET_asprintf (&tmp,
                       "%s\n%08llu",
                       ret,
                       (unsigned long long) code);
      GNUNET_free (ret);
      ret = tmp;
    }
  }
  return ret;

}


char *
TALER_build_pos_confirmation (const char *pos_key,
                              enum TALER_MerchantConfirmationAlgorithm pos_alg,
                              const struct TALER_Amount *total,
                              struct GNUNET_TIME_Timestamp ts)
{
  size_t pos_key_length = strlen (pos_key);
  void *key; /* pos_key in binary */
  size_t key_len; /* length of the key */
  char *ret;
  int dret;

  if (TALER_MCA_NONE == pos_alg)
    return NULL;
  key_len = pos_key_length * 5 / 8;
  key = GNUNET_malloc (key_len);
  dret = TALER_rfc3548_base32decode (pos_key,
                                     pos_key_length,
                                     key,
                                     key_len);
  if (-1 == dret)
  {
    GNUNET_free (key);
    GNUNET_break_op (0);
    return NULL;
  }
  GNUNET_assert (dret <= key_len);
  key_len = (size_t) dret;
  switch (pos_alg)
  {
  case TALER_MCA_NONE:
    GNUNET_break (0);
    GNUNET_free (key);
    return NULL;
  case TALER_MCA_WITHOUT_PRICE: /* and 30s */
    /* Return all T-OTP codes in range separated by new lines, e.g.
       "12345678
        24522552
        25262425
        42543525
        25253552"
    */
    ret = executive_totp (key,
                          key_len,
                          ts);
    GNUNET_free (key);
    return ret;
  case TALER_MCA_WITH_PRICE:
    {
      struct GNUNET_HashCode hkey;
      struct TALER_AmountNBO ntotal;

      if ( (NULL == total) ||
           (GNUNET_YES !=
            TALER_amount_is_valid (total) ) )
      {
        GNUNET_break_op (0);
        return NULL;
      }
      TALER_amount_hton (&ntotal,
                         total);
      GNUNET_assert (GNUNET_YES ==
                     GNUNET_CRYPTO_kdf (&hkey,
                                        sizeof (hkey),
                                        &ntotal,
                                        sizeof (ntotal),
                                        key,
                                        key_len,
                                        NULL,
                                        0));
      GNUNET_free (key);
      ret = executive_totp (&hkey,
                            sizeof(hkey),
                            ts);
      GNUNET_free (key);
      return ret;
    }
  }
  GNUNET_free (key);
  GNUNET_break (0);
  return NULL;
}
