/*
  This totp is part of Anastasis
  Copyright (C) 2021 Anastasis SARL

  Anastasis is free software; you can redistribute it and/or modify it under the
  terms of the GNU Affero General Public License as published by the Free Software
  Foundation; either version 3, or (at your option) any later version.

  Anastasis 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 Affero General Public License for more details.

  You should have received a copy of the GNU Affero General Public License along with
  Anastasis; see the totp COPYING.GPL.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @totp anastasis_authorization_plugin_totp.c
 * @brief authorization plugin using totp
 * @author Christian Grothoff
 */
#include "platform.h"
#include "anastasis_authorization_plugin.h"
#include <taler/taler_mhd_lib.h>
#include <gnunet/gnunet_db_lib.h>
#include "anastasis_database_lib.h"
#include <gcrypt.h>


/**
 * How many retries do we allow per code?
 */
#define INITIAL_RETRY_COUNTER 3

/**
 * 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

/**
 * How long is the shared secret in bytes?
 */
#define SECRET_LEN 32


/**
 * Saves the state of a authorization process
 */
struct ANASTASIS_AUTHORIZATION_State
{
  /**
   * UUID of the challenge which is authorised
   */
  struct ANASTASIS_CRYPTO_TruthUUIDP truth_uuid;

  /**
   * Was the challenge satisfied?
   */
  struct GNUNET_HashCode valid_replies[TIME_INTERVAL_RANGE * 2 + 1];

  /**
   * Time step counter each entry of @e valid_replies was computed for,
   * in the same order.  Needed to tell a replay from a fresh code.
   */
  uint64_t counters[TIME_INTERVAL_RANGE * 2 + 1];

  /**
   * Our context.
   */
  const struct ANASTASIS_AuthorizationContext *ac;

};


/**
 * Validate @a data is a well-formed input into the challenge method,
 * i.e. @a data is a well-formed phone number for sending an SMS, or
 * a well-formed e-mail address for sending an e-mail. Not expected to
 * check that the phone number or e-mail account actually exists.
 *
 * To be possibly used before issuing a 402 payment required to the client.
 *
 * @param cls closure with a `const struct ANASTASIS_AuthorizationContext *`
 * @param connection HTTP client request (for queuing response)
 * @param truth_mime mime type of @e data
 * @param data input to validate (i.e. is it a valid phone number, etc.)
 * @param data_length number of bytes in @a data
 * @return #GNUNET_OK if @a data is valid,
 *         #GNUNET_NO if @a data is invalid and a reply was successfully queued on @a connection
 *         #GNUNET_SYSERR if @a data invalid but we failed to queue a reply on @a connection
 */
static enum GNUNET_GenericReturnValue
totp_validate (void *cls,
               struct MHD_Connection *connection,
               const char *truth_mime,
               const char *data,
               size_t data_length)
{
  (void) cls;
  (void) truth_mime;
  (void) connection;
  if (NULL == data)
  {
    GNUNET_break_op (0);
    if (MHD_NO ==
        TALER_MHD_reply_with_error (connection,
                                    MHD_HTTP_CONFLICT,
                                    TALER_EC_ANASTASIS_TOTP_KEY_MISSING,
                                    NULL))
      return GNUNET_SYSERR;
    return GNUNET_NO;
  }
  if (SECRET_LEN != data_length)
  {
    GNUNET_break_op (0);
    if (MHD_NO ==
        TALER_MHD_reply_with_error (connection,
                                    MHD_HTTP_CONFLICT,
                                    TALER_EC_ANASTASIS_TOTP_KEY_INVALID,
                                    NULL))
      return GNUNET_SYSERR;
    return GNUNET_NO;
  }
  return GNUNET_OK;
}


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

  now = GNUNET_TIME_absolute_get ();
  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;
  *counter = t / 30LLU;
  ctr = GNUNET_htonll (*counter);

  {
    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);
    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;
  }
}


/**
 * Begin issuing authentication challenge to user based on @a data.
 *
 * @param cls closure
 * @param trigger function to call when we made progress
 * @param trigger_cls closure for @a trigger
 * @param truth_uuid Identifier of the challenge, to be (if possible) included in the
 *             interaction with the user
 * @param code always 0 (direct validation, backend does
 *             not generate a code in this mode)
 * @param data truth for input to validate (i.e. the shared secret)
 * @param data_length number of bytes in @a data
 * @return state to track progress on the authorization operation, NULL on failure
 */
static struct ANASTASIS_AUTHORIZATION_State *
totp_start (void *cls,
            GNUNET_SCHEDULER_TaskCallback trigger,
            void *trigger_cls,
            const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
            uint64_t code,
            const void *data,
            size_t data_length)
{
  const struct ANASTASIS_AuthorizationContext *ac = cls;
  struct ANASTASIS_AUTHORIZATION_State *as;
  uint64_t want;
  unsigned int off = 0;

  GNUNET_assert (0 == code);
  if (SECRET_LEN != data_length)
  {
    /* totp_validate() enforces this on upload, but the truth we are handed
       here comes back from the database, so do not trust its length. */
    GNUNET_break_op (0);
    return NULL;
  }
  as = GNUNET_new (struct ANASTASIS_AUTHORIZATION_State);
  as->ac = ac;
  as->truth_uuid = *truth_uuid;
  for (int i = -TIME_INTERVAL_RANGE;
       i <= TIME_INTERVAL_RANGE;
       i++)
  {
    want = compute_totp (i,
                         data,
                         data_length,
                         &as->counters[off]);
    ANASTASIS_hash_answer (want,
                           &as->valid_replies[off++]);
  }
  return as;
}


/**
 * Check authentication response from the user.
 *
 * @param as authorization state
 * @param timeout how long do we have to produce a reply
 * @param challenge_response hash of the response
 * @param connection HTTP client request (for queuing response, such as redirection to video portal)
 * @return state of the request
 */
static enum ANASTASIS_AUTHORIZATION_SolveResult
totp_solve (struct ANASTASIS_AUTHORIZATION_State *as,
            struct GNUNET_TIME_Absolute timeout,
            const struct GNUNET_HashCode *challenge_response,
            struct MHD_Connection *connection)
{
  enum MHD_Result mres;
  const char *mime;
  const char *lang;

  for (unsigned int i = 0; i<=TIME_INTERVAL_RANGE * 2; i++)
  {
    enum GNUNET_DB_QueryStatus qs;

    if (0 !=
        GNUNET_memcmp (challenge_response,
                       &as->valid_replies[i]))
      continue;
    /* The code is right for time step @e counters[i], but a code stays right
       for as long as its window lasts, so anyone who saw it could send it
       again.  Consume the time step: this succeeds at most once (RFC 6238,
       section 5.2), and because the check and the store are the same
       statement two requests carrying the same code cannot both win. */
    qs = ANASTASIS_DB_update_totp_counter (&as->truth_uuid,
                                           as->counters[i]);
    if (qs < 0)
    {
      GNUNET_break (0);
      if (MHD_NO ==
          TALER_MHD_reply_with_error (connection,
                                      MHD_HTTP_INTERNAL_SERVER_ERROR,
                                      TALER_EC_GENERIC_DB_STORE_FAILED,
                                      "update_totp_counter"))
        return ANASTASIS_AUTHORIZATION_SRES_FAILED_REPLY_FAILED;
      return ANASTASIS_AUTHORIZATION_SRES_FAILED;
    }
    if (GNUNET_DB_STATUS_SUCCESS_NO_RESULTS == qs)
    {
      /* Replay of a code we already honoured (or the truth is gone by now).
         Answer exactly as for a wrong code, so that the client learns
         nothing from which of the two it was. */
      GNUNET_log (GNUNET_ERROR_TYPE_WARNING,
                  "Refusing TOTP code for time step %llu of truth %s: already used\n",
                  (unsigned long long) as->counters[i],
                  TALER_B2S (&as->truth_uuid));
      break;
    }
    return ANASTASIS_AUTHORIZATION_SRES_FINISHED;
  }
  mime = MHD_lookup_connection_value (connection,
                                      MHD_HEADER_KIND,
                                      MHD_HTTP_HEADER_ACCEPT);
  if (NULL == mime)
    mime = "text/plain";
  lang = MHD_lookup_connection_value (connection,
                                      MHD_HEADER_KIND,
                                      MHD_HTTP_HEADER_ACCEPT_LANGUAGE);
  if (NULL == lang)
    lang = "en";

  /* Build HTTP response */
  {
    struct MHD_Response *resp;
    struct GNUNET_TIME_Timestamp now;

    now = GNUNET_TIME_timestamp_get ();
    if (0.0 < TALER_pattern_matches (mime,
                                     "application/json"))
    {
      resp = TALER_MHD_MAKE_JSON_PACK (
        GNUNET_JSON_pack_uint64 ("code",
                                 TALER_EC_ANASTASIS_TRUTH_CHALLENGE_FAILED),
        GNUNET_JSON_pack_string ("hint",
                                 TALER_ErrorCode_get_hint (
                                   TALER_EC_ANASTASIS_TRUTH_CHALLENGE_FAILED)),
        GNUNET_JSON_pack_timestamp ("server_time",
                                    now));
    }
    else
    {
      size_t response_size;
      char *response;

      // FIXME: i18n of the message based on 'lang' ...
      response_size
        = GNUNET_asprintf (&response,
                           "Server time: %s",
                           GNUNET_TIME_timestamp2s (now));
      resp = MHD_create_response_from_buffer (response_size,
                                              response,
                                              MHD_RESPMEM_MUST_COPY);
      TALER_MHD_add_global_headers (resp,
                                    false);
      GNUNET_break (MHD_YES ==
                    MHD_add_response_header (resp,
                                             MHD_HTTP_HEADER_CONTENT_TYPE,
                                             "text/plain"));
    }
    mres = MHD_queue_response (connection,
                               MHD_HTTP_FORBIDDEN,
                               resp);
    MHD_destroy_response (resp);
  }
  if (MHD_YES != mres)
    return ANASTASIS_AUTHORIZATION_SRES_FAILED_REPLY_FAILED;
  return ANASTASIS_AUTHORIZATION_SRES_FAILED;
}


/**
 * Free internal state associated with @a as.
 *
 * @param as state to clean up
 */
static void
totp_cleanup (struct ANASTASIS_AUTHORIZATION_State *as)
{
  GNUNET_free (as);
}


/**
 * Initialize Totp based authorization plugin
 *
 * @param cls a configuration instance
 * @return NULL on error, otherwise a `struct ANASTASIS_AuthorizationPlugin`
 */
void *
libanastasis_plugin_authorization_totp_init (void *cls);

/* declaration to fix compiler warning */
void *
libanastasis_plugin_authorization_totp_init (void *cls)
{
  const struct ANASTASIS_AuthorizationContext *ac = cls;
  struct ANASTASIS_AuthorizationPlugin *plugin;

  plugin = GNUNET_new (struct ANASTASIS_AuthorizationPlugin);
  plugin->cls = (void *) ac;
  plugin->user_provided_code = true;
  plugin->retry_counter = INITIAL_RETRY_COUNTER;
  plugin->code_validity_period = TOTP_VALIDITY_PERIOD;
  plugin->code_rotation_period = plugin->code_validity_period;
  plugin->code_retransmission_frequency = plugin->code_validity_period;
  plugin->validate = &totp_validate;
  plugin->start = &totp_start;
  plugin->solve = &totp_solve;
  plugin->cleanup = &totp_cleanup;
  return plugin;
}


/**
 * Unload authorization plugin
 *
 * @param cls a `struct ANASTASIS_AuthorizationPlugin`
 * @return NULL (always)
 */
void *
libanastasis_plugin_authorization_totp_done (void *cls);

/* declaration to fix compiler warning */
void *
libanastasis_plugin_authorization_totp_done (void *cls)
{
  struct ANASTASIS_AuthorizationPlugin *plugin = cls;

  GNUNET_free (plugin);
  return NULL;
}
