/*

  client_keyagr.c

  Author: Pekka Riikonen <priikone@silcnet.org>

  Copyright (C) 2001 - 2014 Pekka Riikonen

  This program 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; version 2 of the License.

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

*/

#include "silc.h"
#include "silcclient.h"
#include "client_internal.h"

/************************** Types and definitions ***************************/

/* Key agreement context, used by responder */
struct SilcClientKeyAgreementStruct {
  SilcClient client;			  /* Client */
  SilcClientConnection conn;		  /* Server connection */
  SilcClientListener listener;	          /* Listener */
  SilcKeyAgreementCallback completion;	  /* Key agreement completion */
  void *context;			  /* User context */
};

/************************ Static utility functions **************************/

/* Destroyes key agreement session */

static void silc_client_keyagr_free(SilcClient client,
				    SilcClientConnection conn,
				    SilcClientEntry client_entry)
{
  SilcClientKeyAgreement ke = client_entry->internal.ke;

  silc_client_listener_free(ke->listener);
  silc_schedule_task_del_by_context(conn->internal->schedule, client_entry);
  if (client_entry->internal.op)
    silc_async_abort(client_entry->internal.op, NULL, NULL);
  client_entry->internal.op = NULL;
  client_entry->internal.ke = NULL;
  client_entry->internal.prv_resp = FALSE;
  silc_client_unref_client(client, conn, client_entry);
  silc_free(ke);
}

/* Key agreement timeout callback */

SILC_TASK_CALLBACK(silc_client_keyagr_timeout)
{
  SilcClientEntry client_entry = context;
  SilcClientKeyAgreement ke = client_entry->internal.ke;

  if (!ke)
    return;

  SILC_LOG_DEBUG(("Key agreement %p timeout", ke));

  ke->completion(ke->client, ke->conn, client_entry,
		 SILC_KEY_AGREEMENT_TIMEOUT, NULL, ke->context);

  silc_client_keyagr_free(ke->client, ke->conn, client_entry);
}

/* Client resolving callback.  Continues with the key agreement processing */

static void silc_client_keyagr_resolved(SilcClient client,
					SilcClientConnection conn,
					SilcStatus status,
					SilcDList clients,
					void *context)
{
  /* If no client found, ignore the packet, a silent error */
  if (!clients)
    silc_fsm_next(context, silc_client_key_agreement_error);

  /* Continue processing the packet */
  SILC_FSM_CALL_CONTINUE(context);
}

/* Key exchange completion callback.  Called after connected to remote host
   and performed key exchange, when we are initiator.  As responder, this is
   called after the remote has connected to us and have performed the key
   exchange. */

static void silc_client_keyagr_completion(SilcClient client,
					  SilcClientConnection conn,
					  SilcClientConnectionStatus status,
					  SilcStatus error,
					  const char *message,
					  void *context)
{
  SilcClientEntry client_entry = context;
  SilcClientKeyAgreement ke = client_entry->internal.ke;
  SilcSKEKeyMaterial keymat;

  client_entry->internal.op = NULL;

  switch (status) {
  case SILC_CLIENT_CONN_SUCCESS:
    SILC_LOG_DEBUG(("Key agreement %p successful", ke));

    keymat = silc_ske_get_key_material(conn->internal->ske);
    ke->completion(ke->client, ke->conn, client_entry, SILC_KEY_AGREEMENT_OK,
		   keymat, ke->context);
    break;

  case SILC_CLIENT_CONN_ERROR_TIMEOUT:
    SILC_LOG_DEBUG(("Key agreement %p timeout", ke));
    ke->completion(ke->client, ke->conn, client_entry,
		   SILC_KEY_AGREEMENT_TIMEOUT, NULL, ke->context);
    break;

  default:
    SILC_LOG_DEBUG(("Key agreement %p error %d", ke, status));
    ke->completion(ke->client, ke->conn, client_entry,
		   SILC_KEY_AGREEMENT_FAILURE, NULL, ke->context);
    break;
  }

  /* Close the connection */
  if (conn)
    silc_client_close_connection(ke->client, conn);

  silc_client_keyagr_free(ke->client, ke->conn, client_entry);
}

/*************************** Key Agreement API ******************************/

/* Sends key agreement packet to remote client.  If IP addresses are provided
   creates also listener for íncoming key agreement connection.  Supports
   both TCP and UDP transports. */

void silc_client_send_key_agreement(SilcClient client,
				    SilcClientConnection conn,
				    SilcClientEntry client_entry,
				    SilcClientConnectionParams *params,
				    SilcPublicKey public_key,
				    SilcPrivateKey private_key,
				    SilcKeyAgreementCallback completion,
				    void *context)
{
  SilcClientKeyAgreement ke = NULL;
  SilcBuffer buffer;
  SilcUInt16 port = 0, protocol = 0;
  char *local_ip = NULL;

  SILC_LOG_DEBUG(("Sending key agreement"));

  if (!client_entry)
    return;
  if (conn->internal->disconnected)
    return;

  if (client_entry->internal.ke) {
    completion(client, conn, client_entry, SILC_KEY_AGREEMENT_ALREADY_STARTED,
	       NULL, context);
    return;
  }

  if (client_entry == conn->local_entry) {
    completion(client, conn, client_entry, SILC_KEY_AGREEMENT_SELF_DENIED,
	       NULL, context);
    return;
  }

  /* If local IP is provided, create listener.  If this is not provided,
     we'll just send empty key agreement payload */
  if (params && (params->local_ip || params->bind_ip)) {
    ke = silc_calloc(1, sizeof(*ke));
    if (!ke) {
      completion(client, conn, client_entry, SILC_KEY_AGREEMENT_NO_MEMORY,
		 NULL, context);
      return;
    }

    /* Create listener */
    ke->listener = silc_client_listener_add(client, conn->internal->schedule,
					    params, public_key, private_key,
					    silc_client_keyagr_completion,
					    client_entry);
    if (!ke->listener) {
      completion(client, conn, client_entry, SILC_KEY_AGREEMENT_NO_MEMORY,
		 NULL, context);
      return;
    }

    local_ip = params->local_ip;
    protocol = params->udp;

    ke->client = client;
    ke->conn = conn;
    ke->completion = completion;
    ke->context = context;
    silc_client_ref_client(client, conn, client_entry);
    client_entry->internal.ke = ke;
    client_entry->internal.prv_resp = TRUE;
  }

  /* Encode the key agreement payload */
  buffer = silc_key_agreement_payload_encode(local_ip, protocol, port);
  if (!buffer) {
    completion(client, conn, client_entry, SILC_KEY_AGREEMENT_NO_MEMORY,
	       NULL, context);
    silc_client_keyagr_free(client, conn, client_entry);
    return;
  }

  /* Send the key agreement packet to the client */
  if (!silc_packet_send_ext(conn->stream, SILC_PACKET_KEY_AGREEMENT, 0,
			    0, NULL, SILC_ID_CLIENT, &client_entry->id,
			    silc_buffer_datalen(buffer), NULL, NULL)) {
    completion(client, conn, client_entry, SILC_KEY_AGREEMENT_ERROR,
	       NULL, context);
    silc_client_keyagr_free(client, conn, client_entry);
    silc_buffer_free(buffer);
    return;
  }

  /* Add key agreement timeout task */
  if (params && params->timeout_secs)
    silc_schedule_task_add_timeout(conn->internal->schedule,
				   silc_client_keyagr_timeout,
				   client_entry, params->timeout_secs, 0);

  silc_buffer_free(buffer);
}

/* Perform key agreement protocol as initiator.  Conneects to remote host. */

void silc_client_perform_key_agreement(SilcClient client,
				       SilcClientConnection conn,
				       SilcClientEntry client_entry,
				       SilcClientConnectionParams *params,
				       SilcPublicKey public_key,
				       SilcPrivateKey private_key,
				       char *hostname, int port,
				       SilcKeyAgreementCallback completion,
				       void *context)
{
  SilcClientKeyAgreement ke;

  SILC_LOG_DEBUG(("Performing key agreement"));

  if (!client_entry || !hostname || !port) {
    completion(client, conn, client_entry, SILC_KEY_AGREEMENT_ERROR,
	       NULL, context);
    return;
  }

  if (client_entry == conn->local_entry) {
    completion(client, conn, client_entry, SILC_KEY_AGREEMENT_SELF_DENIED,
	       NULL, context);
    return;
  }

  ke = silc_calloc(1, sizeof(*ke));
  if (!ke) {
    completion(client, conn, client_entry, SILC_KEY_AGREEMENT_NO_MEMORY,
	       NULL, context);
    return;
  }
  ke->client = client;
  ke->conn = conn;
  ke->completion = completion;
  ke->context = context;
  silc_client_ref_client(client, conn, client_entry);
  client_entry->internal.ke = ke;

  if (params)
    params->no_authentication = TRUE;

  /* Connect to the remote client.  Performs key exchange automatically. */
  client_entry->internal.op =
    silc_client_connect_to_client(client, params, public_key,
				  private_key, hostname, port,
				  silc_client_keyagr_completion,
				  client_entry);
  if (!client_entry->internal.op) {
    completion(client, conn, client_entry, SILC_KEY_AGREEMENT_ERROR,
	       NULL, context);
    silc_client_keyagr_free(client, conn, client_entry);
    return;
  }
}

/* Same as above but caller has created connection. */

void
silc_client_perform_key_agreement_stream(SilcClient client,
					 SilcClientConnection conn,
					 SilcClientEntry client_entry,
					 SilcClientConnectionParams *params,
					 SilcPublicKey public_key,
					 SilcPrivateKey private_key,
					 SilcStream stream,
					 SilcKeyAgreementCallback completion,
					 void *context)
{
  SilcClientKeyAgreement ke;

  SILC_LOG_DEBUG(("Performing key agreement"));

  if (!client_entry || !stream) {
    completion(client, conn, client_entry, SILC_KEY_AGREEMENT_ERROR,
	       NULL, context);
    return;
  }

  if (client_entry == conn->local_entry) {
    completion(client, conn, client_entry, SILC_KEY_AGREEMENT_SELF_DENIED,
	       NULL, context);
    return;
  }

  ke = silc_calloc(1, sizeof(*ke));
  if (!ke) {
    completion(client, conn, client_entry, SILC_KEY_AGREEMENT_NO_MEMORY,
	       NULL, context);
    return;
  }
  ke->client = client;
  ke->conn = conn;
  ke->completion = completion;
  ke->context = context;
  silc_client_ref_client(client, conn, client_entry);
  client_entry->internal.ke = ke;

  if (params)
    params->no_authentication = TRUE;

  /* Perform key exchange protocol */
  client_entry->internal.op =
    silc_client_key_exchange(client, params, public_key,
			     private_key, stream, SILC_CONN_CLIENT,
			     silc_client_keyagr_completion,
			     client_entry);
  if (!client_entry->internal.op) {
    completion(client, conn, client_entry, SILC_KEY_AGREEMENT_ERROR,
	       NULL, context);
    silc_client_keyagr_free(client, conn, client_entry);
    return;
  }
}

/* This function can be called to unbind the hostname and the port for
   the key agreement protocol. However, this function has effect only
   before the key agreement protocol has been performed. After it has
   been performed the library will automatically unbind the port. The
   `client_entry' is the client to which we sent the key agreement
   request. */

void silc_client_abort_key_agreement(SilcClient client,
				     SilcClientConnection conn,
				     SilcClientEntry client_entry)
{
  SilcClientKeyAgreement ke;

  if (!client_entry || !client_entry->internal.ke)
    return;

  ke = client_entry->internal.ke;

  SILC_LOG_DEBUG(("Abort key agreement ke %p for client %p on connection %p",
                  ke, client, conn));

  ke->completion(client, conn, client_entry,
		 SILC_KEY_AGREEMENT_ABORTED, NULL, ke->context);

  silc_client_keyagr_free(client, conn, client_entry);
}

/* Key agreement packet received */

SILC_FSM_STATE(silc_client_key_agreement)
{
  SilcClientConnection conn = fsm_context;
  SilcClient client = conn->client;
  SilcPacket packet = state_context;
  SilcClientID remote_id;
  SilcClientEntry remote_client;
  SilcKeyAgreementPayload payload;

  if (packet->src_id_type != SILC_ID_CLIENT) {
    /** Invalid packet */
    silc_fsm_next(fsm, silc_client_key_agreement_error);
    return SILC_FSM_CONTINUE;
  }

  if (!silc_id_str2id(packet->src_id, packet->src_id_len, SILC_ID_CLIENT,
		      &remote_id, sizeof(remote_id))) {
    /** Invalid source ID */
    silc_fsm_next(fsm, silc_client_key_agreement_error);
    return SILC_FSM_CONTINUE;
  }

  /* Check whether we know this client already */
  remote_client = silc_client_get_client_by_id(client, conn, &remote_id);
  if (!remote_client || !remote_client->internal.valid) {
    /** Resolve client info */
    silc_client_unref_client(client, conn, remote_client);
    SILC_FSM_CALL(silc_client_get_client_by_id_resolve(
					 client, conn, &remote_id, NULL,
					 silc_client_keyagr_resolved, fsm));
    /* NOT REACHED */
  }

  /* Parse the key agreement payload */
  payload = silc_key_agreement_payload_parse(silc_buffer_data(&packet->buffer),
					     silc_buffer_len(&packet->buffer));
  if (!payload) {
    /** Malformed Payload */
    SILC_LOG_DEBUG(("Malformed key agreement payload"));
    silc_fsm_next(fsm, silc_client_key_agreement_error);
    return SILC_FSM_CONTINUE;
  }

  /* If remote did not provide connection endpoint, we will assume that we
     will provide it and will be responder. */
  if (!silc_key_agreement_get_hostname(payload))
    remote_client->internal.prv_resp = TRUE;
  else
    remote_client->internal.prv_resp = FALSE;

  /* Notify application for key agreement request */
  client->internal->ops->key_agreement(
				   client, conn, remote_client,
				   silc_key_agreement_get_hostname(payload),
				   silc_key_agreement_get_protocol(payload),
				   silc_key_agreement_get_port(payload));

  silc_key_agreement_payload_free(payload);

  silc_packet_free(packet);
  return SILC_FSM_FINISH;
}

/* Key agreement packet processing error */

SILC_FSM_STATE(silc_client_key_agreement_error)
{
  SilcPacket packet = state_context;
  silc_packet_free(packet);
  return SILC_FSM_FINISH;
}
