/*
     This file is part of anastasis-gtk.
     Copyright (C) 2021 Anastasis SARL

     Anastasis 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.

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

     You should have received a copy of the GNU General Public License
     along with Anastasis; see the file COPYING.  If not, write to the
     Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
     Boston, MA 02110-1301, USA.
*/

/**
 * @file src/anastasis/anastasis-gtk_handle-method-totp.c
 * @brief Handle dialogs for TOTP (RFC 6238)
 * @author Christian Grothoff
 */
#include <gnunet/gnunet_util_lib.h>
#include "anastasis_gtk_util.h"
#include "anastasis-gtk_action.h"
#include "anastasis-gtk_helper.h"
#include "anastasis-gtk_handle-identity-changed.h"
#include <jansson.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

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

/**
 * Random secret used in the current dialog.
 */
static char totp_key[SECRET_LEN];


/**
 * 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
 * @return TOTP code at this time
 */
static uint64_t
compute_totp (int time_off)
{
  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;
  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,
                                   totp_key,
                                   sizeof (totp_key)));
    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 |= hmac[offset + 3 - count] << (8 * count);
    code &= 0x7fffffff;
    /* always use 8 digits (maximum) */
    code = code % 100000000;
    return code;
  }
}


/**
 * Compute RFC 3548 base32 encoding of @a val and write
 * result to @a enc.
 *
 * @param val value to encode
 * @param val_size number of bytes in @a val
 * @param[out] enc where to write the 0-terminated result
 */
static void
base32enc (const void *val,
           size_t val_size,
           char *enc)
{
  /**
   * 32 characters for encoding, using RFC 3548.
   */
  static const char *encTable__ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
  unsigned int wpos;
  unsigned int rpos;
  unsigned int bits;
  unsigned int vbit;
  const unsigned char *udata;

  udata = val;
  vbit = 0;
  wpos = 0;
  rpos = 0;
  bits = 0;
  while ((rpos < val_size) || (vbit > 0))
  {
    if ((rpos < val_size) && (vbit < 5))
    {
      bits = (bits << 8) | udata[rpos++];     /* eat 8 more bits */
      vbit += 8;
    }
    if (vbit < 5)
    {
      bits <<= (5 - vbit);     /* zero-padding */
      GNUNET_assert (vbit == ((val_size * 8) % 5));
      vbit = 5;
    }
    enc[wpos++] = encTable__[(bits >> (vbit - 5)) & 31];
    vbit -= 5;
  }
  GNUNET_assert (0 == vbit);
  enc[wpos] = '\0';
}


/**
 * Recompute the QR code shown in @a builder from
 * totp and the user's name for the secret.
 *
 * @param builder the dialog builder
 */
static void
refresh_totp (GtkBuilder *builder)
{
  GtkEntry *q;
  const char *name;
  char *u_name;
  char *uri;
  char base_sec[sizeof (totp_key) * 2];
  GdkPixbuf *pb;
  GtkImage *img;

  gtk_widget_set_sensitive (
    GTK_WIDGET (gtk_builder_get_object (builder,
                                        "anastasis_gtk_b_totp_dialog_btn_ok")),
    FALSE);
  q = GTK_ENTRY (gtk_builder_get_object (builder,
                                         "anastasis_gtk_b_totp_dialog_name_entry"));
  name = gtk_editable_get_text (GTK_EDITABLE (q));
  u_name = TALER_urlencode (name);
  base32enc (totp_key,
             sizeof (totp_key),
             base_sec);
  GNUNET_asprintf (&uri,
                   "otpauth://totp/%s?digits=8&secret=%s",
                   u_name,
                   base_sec);
  GNUNET_free (u_name);
  img = GTK_IMAGE (gtk_builder_get_object (builder,
                                           "qr_image"));
  pb = AG_setup_qrcode (GTK_WIDGET (img),
                        uri,
                        strlen (uri));
  GNUNET_free (uri);
  if (NULL != pb)
  {
    AG_image_set_from_pixbuf (img,
                              pb);
    g_object_unref (pb);
  }
}


/**
 * Function called from the totp dialog upon completion.
 *
 * @param dialog the pseudonym selection dialog
 * @param response_id response code from the dialog
 * @param user_data the builder of the dialog
 */
void
anastasis_gtk_b_totp_dialog_response_cb (GtkDialog *dialog,
                                         gint response_id,
                                         gpointer user_data)
{
  GtkBuilder *builder = GTK_BUILDER (user_data);
  GtkEntry *q;
  const char *name;
  json_t *args;

  if (GTK_RESPONSE_OK != response_id)
  {
    gtk_window_destroy (GTK_WINDOW (dialog));
    g_object_unref (G_OBJECT (builder));
    return;
  }
  q = GTK_ENTRY (gtk_builder_get_object (builder,
                                         "anastasis_gtk_b_totp_dialog_name_entry"));
  name = gtk_editable_get_text (GTK_EDITABLE (q));
  args = json_pack ("{ s:{s:s, s:o, s:s}}",
                    "authentication_method",
                    "type",
                    "totp",
                    "challenge",
                    GNUNET_JSON_from_data (totp_key,
                                           sizeof (totp_key)),
                    "instructions",
                    name);
  gtk_window_destroy (GTK_WINDOW (dialog));
  g_object_unref (G_OBJECT (builder));
  memset (totp_key,
          0,
          sizeof (totp_key));
  AG_freeze ();
  AG_ra = ANASTASIS_redux_action (AG_redux_state,
                                  "add_authentication",
                                  args,
                                  &AG_action_cb,
                                  NULL);
  json_decref (args);
}


void
totp_entry_changed_cb (GtkEntry *entry,
                       gpointer user_data)
{
  GtkBuilder *builder = GTK_BUILDER (user_data);
  GtkWidget *but;
  GtkEntry *q;
  const char *code;
  unsigned int val;
  char dummy;
  bool found = false;

  q = GTK_ENTRY (gtk_builder_get_object (builder,
                                         "totp_entry"));
  code = gtk_editable_get_text (GTK_EDITABLE (q));
  if (1 != sscanf (code,
                   "%u%c",
                   &val,
                   &dummy))
    return;
  for (int i = -TIME_INTERVAL_RANGE;
       i <= TIME_INTERVAL_RANGE;
       i++)
  {
    if (val == compute_totp (i))
    {
      found = true;
      break;
    }
  }
  if (! found)
    return;
  but = GTK_WIDGET (gtk_builder_get_object (builder,
                                            "anastasis_gtk_b_totp_dialog_btn_ok"));
  gtk_widget_set_sensitive (but,
                            TRUE);
}


void
anastasis_gtk_b_totp_dialog_name_entry_changed_cb (GtkEntry *entry,
                                                   gpointer user_data)
{
  GtkBuilder *builder = GTK_BUILDER (user_data);
  GtkEntry *e;

  /* clear code user already entered, if any */
  e = GTK_ENTRY (gtk_builder_get_object (builder,
                                         "totp_entry"));
  gtk_editable_set_text (GTK_EDITABLE (e), "");
  refresh_totp (builder);
}


/**
 * Callback invoked if the the "totp"-button is clicked.
 *
 * @param object
 * @param user_data unused
 */
void
anastasis_gtk_btn_add_auth_totp_clicked_cb (GObject *object,
                                            gpointer user_data)
{
  GtkWidget *ad;
  GtkBuilder *builder;

  GNUNET_CRYPTO_random_block (totp_key,
                              sizeof (totp_key));
  builder = ANASTASIS_GTK_get_new_builder (
    ANASTASIS_GTK_project_data (),
    "anastasis_gtk_auth_add_totp.ui",
    NULL);
  if (NULL == builder)
  {
    GNUNET_break (0);
    return;
  }
  ad = GTK_WIDGET (gtk_builder_get_object (builder,
                                           "anastasis_gtk_b_totp_dialog"));
  refresh_totp (builder);
  {
    GtkRoot *toplevel;

    toplevel = gtk_widget_get_root (GTK_WIDGET (object));
    gtk_window_set_transient_for (GTK_WINDOW (ad),
                                  GTK_WINDOW (toplevel));
    gtk_window_present (GTK_WINDOW (ad));
  }
}
