/*
     This file is part of GNUnet.
     Copyright (C) 2012, 2016 GNUnet e.V.

     GNUnet 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 of the License,
     or (at your option) any later version.

     GNUnet 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 this program.  If not, see <http://www.gnu.org/licenses/>.

     SPDX-License-Identifier: AGPL3.0-or-later
 */

/**
 * @file secretsharing/secretsharing_api.c
 * @brief
 * @author Florian Dold
 */
#include "platform.h"
#include "gnunet_util_lib.h"
#include "gnunet_secretsharing_service.h"
#include "secretsharing.h"
#include <gcrypt.h>


#define LOG(kind, ...) GNUNET_log_from (kind, "secretsharing-api", __VA_ARGS__)

/**
 * Session that will eventually establish a shared secred between
 * the involved peers and allow encryption and cooperative decryption.
 */
struct GNUNET_SECRETSHARING_Session
{
  /**
   * Message queue for @e client.
   */
  struct GNUNET_MQ_Handle *mq;

  /**
   * Called when the secret sharing is done.
   */
  GNUNET_SECRETSHARING_SecretReadyCallback secret_ready_cb;

  /**
   * Closure for @e secret_ready_cb.
   */
  void *secret_ready_cls;
};


/**
 * Handle to cancel a cooperative decryption operation.
 */
struct GNUNET_SECRETSHARING_DecryptionHandle
{
  /**
   * Message queue for @e client.
   */
  struct GNUNET_MQ_Handle *mq;

  /**
   * Called when the secret sharing is done.
   */
  GNUNET_SECRETSHARING_DecryptCallback decrypt_cb;

  /**
   * Closure for @e decrypt_cb.
   */
  void *decrypt_cls;
};


/**
 * The ElGamal prime field order as libgcrypt mpi.
 * Initialized in #init_crypto_constants.
 */
static gcry_mpi_t elgamal_q;

/**
 * Modulus of the prime field used for ElGamal.
 * Initialized in #init_crypto_constants.
 */
static gcry_mpi_t elgamal_p;

/**
 * Generator for prime field of order 'elgamal_q'.
 * Initialized in #init_crypto_constants.
 */
static gcry_mpi_t elgamal_g;


/**
 * Function to initialize #elgamal_q, #elgamal_p and #elgamal_g.
 */
static void
ensure_elgamal_initialized (void)
{
  if (NULL != elgamal_q)
    return; /* looks like crypto is already initialized */

  GNUNET_assert (0 == gcry_mpi_scan (&elgamal_q, GCRYMPI_FMT_HEX,
                                     GNUNET_SECRETSHARING_ELGAMAL_Q_HEX, 0,
                                     NULL));
  GNUNET_assert (0 == gcry_mpi_scan (&elgamal_p, GCRYMPI_FMT_HEX,
                                     GNUNET_SECRETSHARING_ELGAMAL_P_HEX, 0,
                                     NULL));
  GNUNET_assert (0 == gcry_mpi_scan (&elgamal_g, GCRYMPI_FMT_HEX,
                                     GNUNET_SECRETSHARING_ELGAMAL_G_HEX, 0,
                                     NULL));
}


/**
 * Callback invoked when there is an error communicating with
 * the service.  Notifies the application about the error.
 *
 * @param cls the `struct GNUNET_SECRETSHARING_Session`
 * @param error error code
 */
static void
handle_session_client_error (void *cls,
                             enum GNUNET_MQ_Error error)
{
  struct GNUNET_SECRETSHARING_Session *s = cls;

  s->secret_ready_cb (s->secret_ready_cls, NULL, NULL, 0, NULL);
  GNUNET_SECRETSHARING_session_destroy (s);
}


/**
 * Callback invoked when there is an error communicating with
 * the service.  Notifies the application about the error.
 *
 * @param cls the `struct GNUNET_SECRETSHARING_DecryptionHandle`
 * @param error error code
 */
static void
handle_decrypt_client_error (void *cls,
                             enum GNUNET_MQ_Error error)
{
  struct GNUNET_SECRETSHARING_DecryptionHandle *dh = cls;

  dh->decrypt_cb (dh->decrypt_cls, NULL);
  GNUNET_SECRETSHARING_decrypt_cancel (dh);
}


/**
 * Handler invoked with the final result message from
 * secret sharing.  Decodes the message and passes the
 * result to the application.
 *
 * @param cls the `struct GNUNET_SECRETSHARING_Session`
 * @param m message with the result
 */
static int
check_secret_ready (void *cls,
                    const struct GNUNET_SECRETSHARING_SecretReadyMessage *m)
{
  /* FIXME: actually check m is well-formed here! */
  return GNUNET_OK;
}


/**
 * Handler invoked with the final result message from
 * secret sharing.  Decodes the message and passes the
 * result to the application.
 *
 * @param cls the `struct GNUNET_SECRETSHARING_Session`
 * @param m message with the result
 */
static void
handle_secret_ready (void *cls,
                     const struct GNUNET_SECRETSHARING_SecretReadyMessage *m)
{
  struct GNUNET_SECRETSHARING_Session *s = cls;
  struct GNUNET_SECRETSHARING_Share *share;
  size_t share_size;

  LOG (GNUNET_ERROR_TYPE_DEBUG,
       "Got secret ready message of size %u\n",
       ntohs (m->header.size));
  share_size = ntohs (m->header.size) - sizeof(struct
                                               GNUNET_SECRETSHARING_SecretReadyMessage);

  share = GNUNET_SECRETSHARING_share_read (&m[1],
                                           share_size,
                                           NULL);
  GNUNET_assert (NULL != share);  // FIXME: this can fail!
  // should have been checked in #check_secret_ready!
  // FIXME: below we never check &m[1] is valid!
  // FIXME: do we leak 'share' here?
  s->secret_ready_cb (s->secret_ready_cls,
                      share, /* FIXME */
                      &share->public_key,
                      share->num_peers,
                      (const struct GNUNET_PeerIdentity *) &m[1]);

  GNUNET_SECRETSHARING_session_destroy (s);
}


/**
 * Destroy a secret sharing session.
 * The secret ready callback will not be called.
 *
 * @param s session to destroy
 */
void
GNUNET_SECRETSHARING_session_destroy (struct GNUNET_SECRETSHARING_Session *s)
{
  GNUNET_MQ_destroy (s->mq);
  s->mq = NULL;
  GNUNET_free (s);
}


/**
 * Create a session that will eventually establish a shared secret
 * with the other peers.
 *
 * @param cfg configuration to use
 * @param num_peers number of peers in @a peers
 * @param peers array of peers that we will share secrets with, can optionally contain the local peer
 * @param session_id unique session id
 * @param start When should all peers be available for sharing the secret?
 *              Random number generation can take place before the start time.
 * @param deadline point in time where the session must be established; taken as hint
 *                 by underlying consensus sessions
 * @param threshold minimum number of peers that must cooperate to decrypt a value
 * @param cb called when the secret has been established
 * @param cls closure for @a cb
 */
struct GNUNET_SECRETSHARING_Session *
GNUNET_SECRETSHARING_create_session (const struct
                                     GNUNET_CONFIGURATION_Handle *cfg,
                                     unsigned int num_peers,
                                     const struct GNUNET_PeerIdentity *peers,
                                     const struct GNUNET_HashCode *session_id,
                                     struct GNUNET_TIME_Absolute start,
                                     struct GNUNET_TIME_Absolute deadline,
                                     unsigned int threshold,
                                     GNUNET_SECRETSHARING_SecretReadyCallback cb,
                                     void *cls)
{
  struct GNUNET_SECRETSHARING_Session *s
    = GNUNET_new (struct GNUNET_SECRETSHARING_Session);
  struct GNUNET_MQ_MessageHandler mq_handlers[] = {
    GNUNET_MQ_hd_var_size (secret_ready,
                           GNUNET_MESSAGE_TYPE_SECRETSHARING_CLIENT_SECRET_READY,
                           struct GNUNET_SECRETSHARING_SecretReadyMessage,
                           s),
    GNUNET_MQ_handler_end ()
  };
  struct GNUNET_MQ_Envelope *ev;
  struct GNUNET_SECRETSHARING_CreateMessage *msg;

  s->mq = GNUNET_CLIENT_connect (cfg,
                                 "secretsharing",
                                 mq_handlers,
                                 &handle_session_client_error,
                                 s);
  if (NULL == s->mq)
  {
    /* secretsharing not configured correctly */
    GNUNET_break (0);
    GNUNET_free (s);
    return NULL;
  }
  s->secret_ready_cb = cb;
  s->secret_ready_cls = cls;
  ev = GNUNET_MQ_msg_extra (msg,
                            num_peers * sizeof(struct GNUNET_PeerIdentity),
                            GNUNET_MESSAGE_TYPE_SECRETSHARING_CLIENT_GENERATE);

  msg->threshold = htons (threshold);
  msg->num_peers = htons (num_peers);
  msg->session_id = *session_id;
  msg->start = GNUNET_TIME_absolute_hton (start);
  msg->deadline = GNUNET_TIME_absolute_hton (deadline);
  GNUNET_memcpy (&msg[1], peers, num_peers * sizeof(struct
                                                    GNUNET_PeerIdentity));

  GNUNET_MQ_send (s->mq, ev);

  LOG (GNUNET_ERROR_TYPE_DEBUG,
       "Secretsharing session created with %u peers\n",
       num_peers);
  return s;
}


static void
handle_decrypt_done (void *cls,
                     const struct
                     GNUNET_SECRETSHARING_DecryptResponseMessage *m)
{
  struct GNUNET_SECRETSHARING_DecryptionHandle *dh = cls;
  const struct GNUNET_SECRETSHARING_Plaintext *plaintext;

  if (m->success == 0)
    plaintext = NULL;
  else
    plaintext = (void *) &m->plaintext;
  dh->decrypt_cb (dh->decrypt_cls, plaintext);
  GNUNET_SECRETSHARING_decrypt_cancel (dh);
}


struct GNUNET_SECRETSHARING_DecryptionHandle *
GNUNET_SECRETSHARING_decrypt (const struct GNUNET_CONFIGURATION_Handle *cfg,
                              struct GNUNET_SECRETSHARING_Share *share,
                              const struct
                              GNUNET_SECRETSHARING_Ciphertext *ciphertext,
                              struct GNUNET_TIME_Absolute start,
                              struct GNUNET_TIME_Absolute deadline,
                              GNUNET_SECRETSHARING_DecryptCallback decrypt_cb,
                              void *decrypt_cb_cls)
{
  struct GNUNET_SECRETSHARING_DecryptionHandle *s
    = GNUNET_new (struct GNUNET_SECRETSHARING_DecryptionHandle);
  struct GNUNET_MQ_MessageHandler mq_handlers[] = {
    GNUNET_MQ_hd_fixed_size (decrypt_done,
                             GNUNET_MESSAGE_TYPE_SECRETSHARING_CLIENT_DECRYPT_DONE,
                             struct GNUNET_SECRETSHARING_DecryptResponseMessage,
                             s),
    GNUNET_MQ_handler_end ()
  };
  struct GNUNET_MQ_Envelope *ev;
  struct GNUNET_SECRETSHARING_DecryptRequestMessage *msg;
  size_t share_size;

  s->decrypt_cb = decrypt_cb;
  s->decrypt_cls = decrypt_cb_cls;
  s->mq = GNUNET_CLIENT_connect (cfg,
                                 "secretsharing",
                                 mq_handlers,
                                 &handle_decrypt_client_error,
                                 s);
  if (NULL == s->mq)
  {
    GNUNET_free (s);
    return NULL;
  }
  GNUNET_assert (GNUNET_OK ==
                 GNUNET_SECRETSHARING_share_write (share, NULL, 0,
                                                   &share_size));

  ev = GNUNET_MQ_msg_extra (msg,
                            share_size,
                            GNUNET_MESSAGE_TYPE_SECRETSHARING_CLIENT_DECRYPT);

  GNUNET_assert (GNUNET_OK ==
                 GNUNET_SECRETSHARING_share_write (share,
                                                   &msg[1],
                                                   share_size,
                                                   NULL));

  msg->start = GNUNET_TIME_absolute_hton (start);
  msg->deadline = GNUNET_TIME_absolute_hton (deadline);
  msg->ciphertext = *ciphertext;

  GNUNET_MQ_send (s->mq, ev);

  LOG (GNUNET_ERROR_TYPE_DEBUG,
       "decrypt session created\n");
  return s;
}


int
GNUNET_SECRETSHARING_plaintext_generate_i (struct
                                           GNUNET_SECRETSHARING_Plaintext *
                                           plaintext,
                                           int64_t exponent)
{
  int negative;
  gcry_mpi_t x;

  ensure_elgamal_initialized ();

  GNUNET_assert (NULL != (x = gcry_mpi_new (0)));

  negative = GNUNET_NO;
  if (exponent < 0)
  {
    negative = GNUNET_YES;
    exponent = -exponent;
  }

  gcry_mpi_set_ui (x, exponent);

  gcry_mpi_powm (x, elgamal_g, x, elgamal_p);

  if (GNUNET_YES == negative)
  {
    int res;
    res = gcry_mpi_invm (x, x, elgamal_p);
    if (0 == res)
      return GNUNET_SYSERR;
  }

  GNUNET_CRYPTO_mpi_print_unsigned (plaintext,
                                    sizeof(struct
                                           GNUNET_SECRETSHARING_Plaintext),
                                    x);

  return GNUNET_OK;
}


int
GNUNET_SECRETSHARING_encrypt (const struct
                              GNUNET_SECRETSHARING_PublicKey *public_key,
                              const struct
                              GNUNET_SECRETSHARING_Plaintext *plaintext,
                              struct GNUNET_SECRETSHARING_Ciphertext *
                              result_ciphertext)
{
  /* pubkey */
  gcry_mpi_t h;
  /* nonce */
  gcry_mpi_t y;
  /* plaintext message */
  gcry_mpi_t m;
  /* temp value */
  gcry_mpi_t tmp;

  ensure_elgamal_initialized ();

  GNUNET_assert (NULL != (h = gcry_mpi_new (0)));
  GNUNET_assert (NULL != (y = gcry_mpi_new (0)));
  GNUNET_assert (NULL != (tmp = gcry_mpi_new (0)));

  GNUNET_CRYPTO_mpi_scan_unsigned (&h, public_key, sizeof *public_key);
  GNUNET_CRYPTO_mpi_scan_unsigned (&m, plaintext, sizeof *plaintext);

  // Randomize y such that 0 < y < elgamal_q.
  // The '- 1' is necessary as bitlength(q) = bitlength(p) - 1.
  do
  {
    gcry_mpi_randomize (y, GNUNET_SECRETSHARING_ELGAMAL_BITS - 1,
                        GCRY_WEAK_RANDOM);
  }
  while ((gcry_mpi_cmp_ui (y, 0) == 0) || (gcry_mpi_cmp (y, elgamal_q) >= 0));

  // tmp <- g^y
  gcry_mpi_powm (tmp, elgamal_g, y, elgamal_p);
  // write tmp to c1
  GNUNET_CRYPTO_mpi_print_unsigned (&result_ciphertext->c1_bits,
                                    GNUNET_SECRETSHARING_ELGAMAL_BITS / 8, tmp);

  // tmp <- h^y
  gcry_mpi_powm (tmp, h, y, elgamal_p);
  // tmp <- tmp * m
  gcry_mpi_mulm (tmp, tmp, m, elgamal_p);
  // write tmp to c2
  GNUNET_CRYPTO_mpi_print_unsigned (&result_ciphertext->c2_bits,
                                    GNUNET_SECRETSHARING_ELGAMAL_BITS / 8, tmp);

  return GNUNET_OK;
}


/**
 * Cancel a decryption.
 *
 * The decrypt_cb is not called anymore, but the calling
 * peer may already have irrevocably contributed its share for the decryption of the value.
 *
 * @param dh to cancel
 */
void
GNUNET_SECRETSHARING_decrypt_cancel (struct
                                     GNUNET_SECRETSHARING_DecryptionHandle *dh)
{
  GNUNET_MQ_destroy (dh->mq);
  dh->mq = NULL;
  GNUNET_free (dh);
}


/* end of secretsharing_api.c */
