/*
  This file is part of Anastasis
  Copyright (C) 2020-2022 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 file COPYING.GPL.  If not, see <http://www.gnu.org/licenses/>
*/
/**
 * @file stasis/do_verify_challenge_code.c
 * @brief Anastasis database: do verify challenge code
 * @author Christian Grothoff
 */
#include "platform.h"
#include "anastasis-db_pg.h"
#include "anastasis/anastasis-database/do_verify_challenge_code.h"
#include "anastasis/anastasis-database/transaction.h"
#include "anastasis/anastasis-database/preflight.h"
#include <taler/taler_pq_lib.h>


struct CheckValidityContext
{
  /**
   * Code to check for.
   */
  const struct GNUNET_HashCode *hashed_code;

  /**
   * Truth we are processing.
   */
  const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid;

  /**
   * Set to the matching challenge code (if @e valid).
   */
  uint64_t code;

  /**
   * Set to true if a code matching @e hashed_code was found.
   */
  bool valid;

  /**
   * Set to true if a code matching @e hashed_code was set to 'satisfied' by the plugin.
   */
  bool satisfied;

  /**
   * Set to true if the answerable code is out of attempts.
   */
  bool exhausted;

  /**
   * Set to true if we had a database failure.
   */
  bool db_failure;

};


/**
 * Helper function for #postgres_verify_challenge_code().
 * To be called with the results of a SELECT statement
 * that has returned @a num_results results.
 *
 * @param cls closure of type `struct CheckValidityContext *`
 * @param result the postgres result
 * @param num_results the number of results in @a result
 */
static void
check_valid_code (void *cls,
                  PGresult *result,
                  unsigned int num_results)
{
  struct CheckValidityContext *cvc = cls;

  for (unsigned int i = 0; i < num_results; i++)
  {
    uint64_t server_code;
    uint32_t retry_counter;
    uint8_t sat;
    struct GNUNET_PQ_ResultSpec rs[] = {
      GNUNET_PQ_result_spec_uint64 ("code",
                                    &server_code),
      GNUNET_PQ_result_spec_uint32 ("retry_counter",
                                    &retry_counter),
      GNUNET_PQ_result_spec_auto_from_type ("satisfied",
                                            &sat),
      GNUNET_PQ_result_spec_end
    };

    if (GNUNET_OK !=
        GNUNET_PQ_extract_result (result,
                                  rs,
                                  i))
    {
      GNUNET_break (0);
      cvc->db_failure = true;
      return;
    }
    GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                "Found issued challenge %llu (client: %s)\n",
                (unsigned long long) server_code,
                GNUNET_h2s (cvc->hashed_code));
    if (0 == retry_counter)
    {
      /* The answerable code is out of attempts.  Do not fall back to an
         older sibling that still has attempts left: that would make the
         effective budget 3 times the number of live codes.  Do not answer
         it either, not even correctly -- the counter is the rate limit. */
      GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                  "Challenge %llu is out of attempts, rate limiting\n",
                  (unsigned long long) server_code);
      cvc->exhausted = true;
      return;
    }
    {
      struct GNUNET_HashCode shashed_code;

      ANASTASIS_hash_answer (server_code,
                             &shashed_code);
      if (0 ==
          GNUNET_memcmp (&shashed_code,
                         cvc->hashed_code))
      {
        GNUNET_log (GNUNET_ERROR_TYPE_INFO,
                    "Challenge is valid challenge (%s)\n",
                    (0 != sat) ? "satisfied" : "not satisfied");
        cvc->valid = true;
        cvc->code = server_code;
        cvc->satisfied = (0 != sat);
      }
      else
      {
        /* count failures to prevent brute-force attacks */
        struct GNUNET_PQ_QueryParam params[] = {
          GNUNET_PQ_query_param_auto_from_type (cvc->truth_uuid),
          GNUNET_PQ_query_param_uint64 (&server_code),
          GNUNET_PQ_query_param_end
        };
        enum GNUNET_DB_QueryStatus qs;

        qs = GNUNET_PQ_eval_prepared_non_select (pg->conn,
                                                 "do_verify_challenge_code_update_retry_counter",
                                                 params);
        if (qs <= 0)
        {
          GNUNET_break (0);
          cvc->db_failure = true;
        }
      }
    }
  }
}


/**
 * Verify the provided code with the code on the server.
 * Only the newest unexpired code issued for @a truth_uuid is answerable.
 * If the code matches the function will return with success, if the code
 * does not match, the retry counter will be decreased by one.  Once that
 * counter reaches zero the code is no longer answerable and
 * #ANASTASIS_DB_CODE_STATUS_RATE_LIMITED is returned until the next code
 * is issued.
 *
 * @param truth_uuid identification of the challenge which the code corresponds to
 * @param hashed_code code which the user provided and wants to verify
 * @param[out] code set to the original numeric code
 * @param[out] satisfied set to true if the challenge is set to satisfied
 * @return code validity status
 */
enum ANASTASIS_DB_CodeStatus
ANASTASIS_DB_do_verify_challenge_code (
  const struct ANASTASIS_CRYPTO_TruthUUIDP *truth_uuid,
  const struct GNUNET_HashCode *hashed_code,
  uint64_t *code,
  bool *satisfied)
{
  struct CheckValidityContext cvc = {
    .truth_uuid = truth_uuid,
    .hashed_code = hashed_code,
  };
  struct GNUNET_TIME_Timestamp now = GNUNET_TIME_timestamp_get ();
  struct GNUNET_PQ_QueryParam params[] = {
    GNUNET_PQ_query_param_auto_from_type (truth_uuid),
    GNUNET_PQ_query_param_timestamp (&now),
    GNUNET_PQ_query_param_end
  };
  enum GNUNET_DB_QueryStatus qs;

  *satisfied = false;
  PREPARE ("do_verify_challenge_code_update_retry_counter",
           "UPDATE anastasis_challengecode"
           " SET retry_counter=retry_counter - 1"
           " WHERE truth_uuid=$1"
           "   AND code=$2"
           "   AND retry_counter != 0;");
  /* Only the newest code is answerable.  A truth can have several unexpired
     codes at once, and without the LIMIT every one of them that does not
     match burns a retry -- so a single wrong guess could exhaust the counter,
     and even a correct answer cost retries for its siblings.  This matches
     what insert_challenge_code.c already assumes when it issues a code.
     Note that exhausted codes are deliberately *not* filtered out here:
     doing so would fall through to the newest sibling that still has
     attempts left, which is exactly the multiplication of the retry budget
     that the LIMIT is there to prevent.  The caller instead learns that the
     client is rate limited until the next code is issued. */
  PREPARE ("do_verify_challenge_code_select",
           "SELECT "
           " code"
           ",retry_counter"
           ",satisfied"
           " FROM anastasis_challengecode"
           " WHERE truth_uuid=$1"
           "   AND expiration_date > $2"
           " ORDER BY creation_date DESC"
           " LIMIT 1;");
  qs = GNUNET_PQ_eval_prepared_multi_select (pg->conn,
                                             "do_verify_challenge_code_select",
                                             params,
                                             &check_valid_code,
                                             &cvc);
  if ( (qs < 0) ||
       (cvc.db_failure) )
    return ANASTASIS_DB_CODE_STATUS_HARD_ERROR;
  if (cvc.exhausted)
    return ANASTASIS_DB_CODE_STATUS_RATE_LIMITED;
  *code = cvc.code;
  if (cvc.valid)
  {
    *satisfied = cvc.satisfied;
    return ANASTASIS_DB_CODE_STATUS_VALID_CODE_STORED;
  }
  if (0 == qs)
    return ANASTASIS_DB_CODE_STATUS_NO_RESULTS;
  return ANASTASIS_DB_CODE_STATUS_CHALLENGE_CODE_MISMATCH;
}


/* end of do_verify_challenge_code.c */
