/*
  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 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;
    uint8_t sat;
    struct GNUNET_PQ_ResultSpec rs[] = {
      GNUNET_PQ_result_spec_uint64 ("code",
                                    &server_code),
      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));
    {
      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.
 * If the code matches the function will return with success, if the code
 * does not match, the retry counter will be decreased by one.
 *
 * @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. */
  PREPARE ("do_verify_challenge_code_select",
           "SELECT "
           " code"
           ",satisfied"
           " FROM anastasis_challengecode"
           " WHERE truth_uuid=$1"
           "   AND expiration_date > $2"
           "   AND retry_counter != 0"
           " 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;
  *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 */
