/* Copyright (c) 2023, 2025, Oracle and/or its affiliates.

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License, version 2.0,
   as published by the Free Software Foundation.

   This program is designed to work with certain software (including
   but not limited to OpenSSL) that is licensed under separate terms,
   as designated in a particular file or component or in included license
   documentation.  The authors of MySQL hereby grant you an additional
   permission to link the program and your derivative works with the
   separately licensed software that they have either included with
   the program or referenced in the documentation.

   Without limiting anything contained in the foregoing, this file,
   which is part of C Driver for MySQL (Connector/C), is also subject to the
   Universal FOSS Exception, version 1.0, a copy of which can be found at
   http://oss.oracle.com/licenses/universal-foss-exception.

   This program 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, version 2.0, for more details.

   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software
   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA */

#include <common.h>
#include <my_dbug.h>
#include <mysql.h>
#include <mysql/client_plugin.h>
#include <scope_guard.h>

#include "webauthn_assertion.h"
#include "webauthn_registration.h"

#ifndef NDEBUG
static bool is_fido_testing = false;
#endif
static unsigned char registration_challenge[128] = {0};
static unsigned char *registration_challenge_response = nullptr;
/* ToDo: Change this to false */
static bool preserve_privacy = false;

static bool do_registration();

/*
  Handler to callback function which will pass informative messages generated by
  this plugin caller. This callback function is registered via
  mysql_plugin_option("webauthn_messages_callback")

  If callback is not registered, all messaged are redirected to stderr/stdout.
*/
plugin_messages_callback mc = nullptr;
plugin_messages_callback_get_uint mc_get_uint = nullptr;
plugin_messages_callback_get_password mc_get_password = nullptr;

/**
 The libfido "device" to use.
*/
unsigned int libfido_device_id = 0;

/**
  authentication_webauthn_client plugin API to initialize
*/
static int webauthn_auth_client_plugin_init(char *, size_t, int, va_list) {
  fido_init(0);
  return 0;
}

/**
  Deinitialize authentication_webauthn_client plugin
*/
static int webauthn_auth_client_plugin_deinit() { return 0; }

/**
  authentication_webauthn_client plugin API to allow client to pass optional
  data for plugin to process
*/
static int webauthn_auth_client_plugin_option(const char *option,
                                              const void *val) {
#ifndef NDEBUG
  if (strcmp(option, "is_fido_testing") == 0) {
    is_fido_testing = *static_cast<const bool *>(val);
    return 0;
  }
#endif
  if (strcmp(option,
             "plugin_authentication_webauthn_client_messages_callback") == 0) {
    mc = (plugin_messages_callback)(const_cast<void *>(val));
    return 0;
  }
  if (strcmp(option,
             "plugin_authentication_webauthn_client_callback_get_uint") == 0) {
    mc_get_uint = (plugin_messages_callback_get_uint)(const_cast<void *>(val));
    return 0;
  }
  if (strcmp(option,
             "plugin_authentication_webauthn_client_callback_get_"
             "password") == 0) {
    mc_get_password =
        (plugin_messages_callback_get_password)(const_cast<void *>(val));
    return 0;
  }
  if (strcmp(option, "registration_challenge") == 0) {
    auto *p = reinterpret_cast<unsigned char *>(const_cast<void *>(val));
    memcpy(registration_challenge, p, strlen(reinterpret_cast<char *>(p)));
    /* finish registration */
    if (do_registration()) return 1;
    return 0;
  }
  if (strcmp(option, "authentication_webauthn_client_preserve_privacy") == 0) {
    preserve_privacy = *static_cast<const bool *>(val);
    return 0;
  }
  if (0 == strcmp(option, "device")) {
    /*
      An artifical limit on the number of devices supported to avoid
      excessive memory consumption
    */
    static const int MAX_FIDO_DEVICE_ID = 15;

    libfido_device_id = *static_cast<const uint *>(val);
    if (libfido_device_id > MAX_FIDO_DEVICE_ID) return 1;
    return 0;
  }
  return 1;
}

/**
  authentication_webauthn_client plugin API to allow client to get optional data
  from plugin
*/
static int webauthn_auth_client_get_plugin_option(const char *option,
                                                  void *val) {
  if (strcmp(option, "registration_response") == 0) {
    *(static_cast<unsigned char **>(val)) = registration_challenge_response;
  }
  return 0;
}

/**
   WebAuthN client side authentication method. This method does following:

   1. Receive challenge from server side FIDO plugin. This challenge
      comprises of 1 byte capability, salt and relying party name.
   2. Construct client data hash in the form of JSON object comprising of
      salt, relying party name aka Origin. Set client data hash.
   3. If token device does not have CTAP2.1 protocol support, then request
      credential ID from server.
   4. Token will sign clientdatahash. Client will send authenticator data,
      signature and clientDataJSON to server to be verified.

  @param [in] vio  Virtual I/O interface

  @return authentication status
  @retval CR_OK    Successful authentication
  @retval true     Authentication failure
*/
static int webauthn_auth_client(MYSQL_PLUGIN_VIO *vio, MYSQL *) {
  unsigned char *server_challenge = nullptr;
  int server_challenge_len = 0;

  /** Get the challenge from the MySQL server. */
  server_challenge_len = vio->read_packet(vio, &server_challenge);
  if (server_challenge_len == 0) {
    /*
      an empty packet means registration step is pending, thus for now allow
      connection with limited operations for user so that user can perform
      registration step.
    */
    return CR_OK_AUTH_IN_SANDBOX_MODE;
  }
  unsigned char *buff = nullptr;
  webauthn_assertion *wa = nullptr;
  size_t length = 0;
  auto cleanup = create_scope_guard([&] {
    delete[] buff;
    delete wa;
  });

#ifndef NDEBUG
  if (is_fido_testing) {
    length = 34;
    buff = new (std::nothrow) unsigned char[length];
    unsigned char *pos = buff;
    *pos = '\2';
    pos++;
    memcpy(pos, "\nsakila    \nsakila    \nsakila    ", length - 1);
    vio->write_packet(vio, buff, length);
    return CR_OK;
  } else
#endif
  {
    wa = new webauthn_assertion(preserve_privacy);
    if (wa->parse_challenge(server_challenge)) return true;
    bool is_fido2 = false;
    if (wa->check_fido2_device(is_fido2)) return true;
    if (is_fido2) {
      if (wa->select_credential_id()) return true;
    } else {
      /* request credential ID */
      const unsigned char cred_req = '\1';
      vio->write_packet(vio, &cred_req, 1);
      unsigned char *cred_id = nullptr;
      int cred_id_len = 0;
      /** Get the credential ID from MySQL server. */
      if (vio->read_packet(vio, &cred_id) < 0) return true;
      if (!cred_id) return true;
      cred_id_len = net_field_length_ll(&cred_id);
      /* extract cred ID */
      wa->set_cred_id(cred_id, cred_id_len);
    }
    if (wa->sign_challenge()) return true;
    /* copy signed challenge into buff */
    wa->get_signed_challenge(&buff, length);
    /* send signed challenge to webauthn server plugin */
    vio->write_packet(vio, buff, length);
  }
  return CR_OK;
}

/**
   WebAuthN client side registration method. This method does following:

   1. Receive challenge from server side WebAuthN plugin. This challenge
      comprises of capability flag, username, salt and relying party name.
   2. Send this challenge to FIDO device and get the signature, authenticator
      data and x509 certificate generated by device. This along with client
      data JSON is sent to server as challenge response.

  @return registration status
  @retval false    Successful registration
  @retval true     Registration failure
*/
static bool do_registration() {
#ifndef NDEBUG
  if (is_fido_testing) {
    const char *dummy = "\nSIGNATURE \nAUTHDATA \nCERT      ";
    size_t const sz = strlen(dummy);
    memcpy(registration_challenge, dummy, sz);
    /* dummy challenge response for testing */
    registration_challenge_response = new unsigned char[sz + 1];
    memcpy(registration_challenge_response, dummy, sz);
    registration_challenge_response[sz] = 0;
    return false;
  } else
#endif
  {
    auto *fr = new webauthn_registration();
    if (fr->make_credentials(const_cast<const char *>(
            reinterpret_cast<char *>(registration_challenge)))) {
      delete fr;
      return true;
    }
    if (fr->make_challenge_response(registration_challenge_response)) {
      delete fr;
      return true;
    }
    delete fr;
  }
  return false;
}

mysql_declare_client_plugin(AUTHENTICATION) "authentication_webauthn_client",
    MYSQL_CLIENT_PLUGIN_AUTHOR_ORACLE, "Webauthn Client Authentication Plugin",
    {0, 1, 0}, "GPL", nullptr, webauthn_auth_client_plugin_init,
    webauthn_auth_client_plugin_deinit, webauthn_auth_client_plugin_option,
    webauthn_auth_client_get_plugin_option, webauthn_auth_client,
    nullptr, mysql_end_client_plugin;
