/*
 * gnome-keyring
 *
 * Copyright (C) 2008 Stefan Walter
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this program; if not, see
 * <http://www.gnu.org/licenses/>.
 */

#include "config.h"

#include "gkd-dbus.h"
#include "gkd-secret-dispatch.h"
#include "gkd-secret-error.h"
#include "gkd-secret-objects.h"
#include "gkd-secret-secret.h"
#include "gkd-secret-session.h"
#include "gkd-secret-service.h"
#include "gkd-secret-types.h"
#include "gkd-secret-unlock.h"
#include "gkd-secret-util.h"
#include "gkd-secrets-generated.h"

#include "egg/egg-error.h"
#include "egg/egg-secure-memory.h"

#include "daemon/login/gkd-login.h"

#include "pkcs11/pkcs11i.h"

#include <glib/gi18n.h>

#include <gck/gck.h>

#include <string.h>

/*
 * We try to serialize unlock requests, so the user doesn't get prompted
 * multiple times for the same thing. There are two queues:
 *  - self->queued: A queue of object paths per unlock requests.
 *  - unlock_prompt_queue: A queue of unlock requests ready to prompt.
 */

enum {
	PROP_0,
	PROP_CALLER,
	PROP_OBJECT_PATH,
	PROP_SERVICE
};

struct _GkdSecretUnlock {
	GObject parent;
	gchar *object_path;
	GkdSecretService *service;
	GkdExportedPrompt *skeleton;
	gchar *caller;
	gchar *window_id;
	GQueue *queued;
	gchar *current;
	GArray *results;
	gboolean prompted;
	gboolean completed;
	GCancellable *cancellable;
};

/* Forward declarations */
static void gkd_secret_dispatch_iface (GkdSecretDispatchIface *iface);
static void perform_next_unlock (GkdSecretUnlock *self);

G_DEFINE_TYPE_WITH_CODE (GkdSecretUnlock, gkd_secret_unlock, G_TYPE_OBJECT,
			 G_IMPLEMENT_INTERFACE (GKD_SECRET_TYPE_DISPATCH, gkd_secret_dispatch_iface));

static guint unique_prompt_number = 0;
static GQueue unlock_prompt_queue = G_QUEUE_INIT;

EGG_SECURE_DECLARE (secret_unlock);

/* -----------------------------------------------------------------------------
 * INTERNAL
 */

static GckObject*
lookup_collection (GkdSecretUnlock *self, const gchar *path)
{
	GkdSecretObjects *objects = gkd_secret_service_get_objects (self->service);
	return gkd_secret_objects_lookup_collection (objects, self->caller, path);
}

static void
emit_collection_unlocked (GkdSecretUnlock *self,
			  const gchar *path)
{
	GkdSecretObjects *objects;
	GckObject *collection;

	objects = gkd_secret_service_get_objects (self->service);
	collection = gkd_secret_objects_lookup_collection (objects, self->caller, path);
	if (collection != NULL) {
		gkd_secret_objects_emit_collection_locked (objects, collection);
		g_object_unref (collection);
	}
}

static gboolean
check_locked_collection (GckObject *collection, gboolean *locked)
{
	GError *error = NULL;
	gpointer value;
	gsize n_value;

	value = gck_object_get_data (collection, CKA_G_LOCKED, NULL, &n_value, &error);
	if (value == NULL) {
		if (!g_error_matches (error, GCK_ERROR, CKR_OBJECT_HANDLE_INVALID))
			g_warning ("couldn't check locked status of collection: %s",
				   egg_error_message (error));
		return FALSE;
	}

	*locked = (value && n_value == sizeof (CK_BBOOL) && *(CK_BBOOL*)value);
	g_free (value);
	return TRUE;
}

static void
common_unlock_attributes (GckBuilder *builder,
			  GckObject *collection)
{
	g_assert (builder != NULL);
	g_assert (GCK_IS_OBJECT (collection));
	gck_builder_add_ulong (builder, CKA_CLASS, CKO_G_CREDENTIAL);
	gck_builder_add_ulong (builder, CKA_G_OBJECT, gck_object_get_handle (collection));
}

static gboolean
mark_as_complete (GkdSecretUnlock *self, gboolean dismissed)
{
	GkdSecretUnlock *other;
	const char *value;
	gint i;
	GVariantBuilder builder;
	GVariant *variant;

	if (self->completed)
		return FALSE;
	self->completed = TRUE;

	g_variant_builder_init (&builder, G_VARIANT_TYPE ("ao"));
	for (i = 0; i < self->results->len; ++i) {
		value = g_array_index (self->results, gchar*, i);
		g_variant_builder_add (&builder, "o", value);
	}

	/* Emit signal manually, so that we can set the caller as destination */
	variant = g_variant_new_variant (g_variant_builder_end (&builder));
	g_dbus_connection_emit_signal (g_dbus_interface_skeleton_get_connection (G_DBUS_INTERFACE_SKELETON (self->skeleton)),
				       self->caller,
				       g_dbus_interface_skeleton_get_object_path (G_DBUS_INTERFACE_SKELETON (self->skeleton)),
				       "org.freedesktop.Secret.Prompt", "Completed",
				       g_variant_new ("(b@v)", dismissed, variant),
				       NULL);

	/* Fire off the next item in the unlock prompt queue */
	other = g_queue_pop_head (&unlock_prompt_queue);
	if (other != NULL) {
		perform_next_unlock (other);
		g_object_unref (other);
	}

	return TRUE;
}

static void
on_unlock_complete (GObject *object, GAsyncResult *res, gpointer user_data)
{
	GkdSecretUnlock *self = GKD_SECRET_UNLOCK (user_data);
	GkdSecretUnlock *other;
	GckObject *cred;
	GError *error = NULL;

	/* We should be at the front of the unlock queue, pop ourselves */
	other = g_queue_pop_head (&unlock_prompt_queue);
	if (other == self)
		g_object_unref (other);
	else
		g_warning ("unlock prompt queue is out of sync with prompts");

	/* Now process the results */
	cred = gck_session_create_object_finish (GCK_SESSION (object), res, &error);

	/* Successfully authentication */
	if (cred) {
		g_object_unref (cred);
		emit_collection_unlocked (self, self->current);
		g_array_append_val (self->results, self->current);
		self->current = NULL;
		perform_next_unlock (self);

	/* The user cancelled the protected auth prompt */
	} else if (g_error_matches (error, GCK_ERROR, CKR_PIN_INCORRECT)) {
		g_free (self->current);
		self->current = NULL;
		mark_as_complete (self, TRUE);

	/* The operation was cancelled via Dismiss call */
	} else if (g_error_matches (error, GCK_ERROR, CKR_CANCEL)) {
		/* Should have been the result of a dismiss */
		g_return_if_fail (self->completed);

	/* Another error, something's broken */
	} else {
		g_warning ("couldn't create credential for collection: %s",
			   egg_error_message (error));
	}

	g_clear_error (&error);

	/* refed for async call */
	g_object_unref (self);
}

static void
perform_next_unlock (GkdSecretUnlock *self)
{
	GckBuilder builder = GCK_BUILDER_INIT;
	GckObject *collection;
	GckSession *session;
	gboolean locked;
	gboolean proceed;
	gchar *objpath;

	for (;;) {
		g_assert (!self->current);
		objpath = g_queue_pop_head (self->queued);

		/* Nothing more to prompt for? */
		if (!objpath) {
			mark_as_complete (self, FALSE);
			break;
		}

		/* Find the collection, make sure it's still around */
		collection = lookup_collection (self, objpath);
		if (collection == NULL) {
			g_free (objpath);
			continue;
		}

		if (!check_locked_collection (collection, &locked)) {
			g_object_unref (collection);
			g_free (objpath);
			continue;

		} else if (!locked) {
			g_array_append_val (self->results, objpath);
			g_object_unref (collection);
			continue;
		}

		/* Add ourselves to the unlock prompt queue */
		proceed = g_queue_is_empty (&unlock_prompt_queue);
		g_queue_push_tail (&unlock_prompt_queue, g_object_ref (self));

		/*
		 * Proceed with this unlock request. The on_unlock_complete callback
		 * pops us back off the unlock prompt queue
		 */
		if (proceed) {
			common_unlock_attributes (&builder, collection);
			gck_builder_add_boolean (&builder, CKA_GNOME_TRANSIENT, TRUE);
			gck_builder_add_data (&builder, CKA_VALUE, NULL, 0);

			session = gkd_secret_service_get_pkcs11_session (self->service, self->caller);
			gck_session_create_object_async (session, gck_builder_end (&builder),
							 self->cancellable, on_unlock_complete,
							 g_object_ref (self));
			self->current = objpath;
			break;
		}

		g_object_unref (collection);

		/*
		 * Already have one unlock request going on. Just wait around
		 * and this function will be called again later.
		 */
		if (!proceed) {
			g_queue_push_head (self->queued, objpath);
			break;
		}
	}
}

/* -----------------------------------------------------------------------------
 * DBUS
 */

static gboolean
prompt_method_prompt (GkdExportedPrompt *skeleton,
		      GDBusMethodInvocation *invocation,
		      gchar *window_id,
		      GkdSecretUnlock *self)
{
	if (!gkd_dbus_invocation_matches_caller (invocation, self->caller))
		return FALSE;

	/* Act as if this object no longer exists */
	if (self->completed)
		return FALSE;

	/* Prompt can only be called once */
	if (self->prompted) {
		g_dbus_method_invocation_return_error_literal (invocation,
							       GKD_SECRET_ERROR,
							       GKD_SECRET_ERROR_ALREADY_EXISTS,
							       "This prompt has already been shown.");
		return TRUE;
	}

	gkd_secret_unlock_call_prompt (self, window_id);

	gkd_exported_prompt_complete_prompt (skeleton, invocation);
	return TRUE;
}

static gboolean
prompt_method_dismiss (GkdExportedPrompt *skeleton,
		       GDBusMethodInvocation *invocation,
		       GkdSecretUnlock *self)
{
	if (!gkd_dbus_invocation_matches_caller (invocation, self->caller))
		return FALSE;

	/* Act as if this object no longer exists */
	if (self->completed)
		return FALSE;

	g_cancellable_cancel (self->cancellable);
	mark_as_complete (self, TRUE);

	gkd_exported_prompt_complete_dismiss (skeleton, invocation);
	return TRUE;
}

/* -----------------------------------------------------------------------------
 * OBJECT
 */

static void
gkd_secret_unlock_init (GkdSecretUnlock *self)
{
	self->queued = g_queue_new ();
	self->results = g_array_new (TRUE, TRUE, sizeof (gchar*));
	self->cancellable = g_cancellable_new ();
}

static GObject*
gkd_secret_unlock_constructor (GType type, guint n_props, GObjectConstructParam *props)
{
	GkdSecretUnlock *self = GKD_SECRET_UNLOCK (G_OBJECT_CLASS (gkd_secret_unlock_parent_class)->constructor(type, n_props, props));
	GError *error = NULL;

	g_return_val_if_fail (self, NULL);
	g_return_val_if_fail (self->caller, NULL);
	g_return_val_if_fail (self->service, NULL);

	/* Setup the path for the object */
	if (!self->object_path)
		self->object_path = g_strdup_printf (SECRET_PROMPT_PREFIX "/u%d", ++unique_prompt_number);

	self->skeleton = gkd_exported_prompt_skeleton_new ();
	g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self->skeleton),
					  gkd_secret_service_get_connection (self->service), self->object_path,
					  &error);

	if (error != NULL) {
		g_warning ("could not register secret unlock prompt on session bus: %s", error->message);
		g_error_free (error);
	}

	g_signal_connect (self->skeleton, "handle-dismiss",
			  G_CALLBACK (prompt_method_dismiss), self);
	g_signal_connect (self->skeleton, "handle-prompt",
			  G_CALLBACK (prompt_method_prompt), self);

	return G_OBJECT (self);
}

static void
gkd_secret_unlock_dispose (GObject *obj)
{
	GkdSecretUnlock *self = GKD_SECRET_UNLOCK (obj);

	if (self->skeleton) {
		g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self->skeleton));
		g_clear_object (&self->skeleton);
	}

	if (self->service) {
		g_object_remove_weak_pointer (G_OBJECT (self->service),
					      (gpointer*)&(self->service));
		self->service = NULL;
	}

	G_OBJECT_CLASS (gkd_secret_unlock_parent_class)->dispose (obj);
}

static void
gkd_secret_unlock_finalize (GObject *obj)
{
	GkdSecretUnlock *self = GKD_SECRET_UNLOCK (obj);

	g_free (self->object_path);
	self->object_path = NULL;

	if (g_queue_find (&unlock_prompt_queue, self))
		g_warning ("unlock queue is not in sync with prompting");

	if (self->queued) {
		while (!g_queue_is_empty (self->queued))
			g_free (g_queue_pop_head (self->queued));
		g_queue_free (self->queued);
		self->queued = NULL;
	}

	if (self->results) {
		gkd_secret_unlock_reset_results (self);
		g_array_free (self->results, TRUE);
		self->results = NULL;
	}

	g_free (self->current);
	self->current = NULL;

	g_object_unref (self->cancellable);
	self->cancellable = NULL;

	g_assert (!self->object_path);
	g_assert (!self->service);

	g_free (self->caller);
	self->caller = NULL;

	g_free (self->window_id);
	self->window_id = NULL;

	G_OBJECT_CLASS (gkd_secret_unlock_parent_class)->finalize (obj);
}

static void
gkd_secret_unlock_set_property (GObject *obj, guint prop_id, const GValue *value,
				GParamSpec *pspec)
{
	GkdSecretUnlock *self = GKD_SECRET_UNLOCK (obj);

	switch (prop_id) {
	case PROP_CALLER:
		g_return_if_fail (!self->caller);
		self->caller = g_value_dup_string (value);
		break;
	case PROP_SERVICE:
		g_return_if_fail (!self->service);
		self->service = g_value_get_object (value);
		g_return_if_fail (self->service);
		g_object_add_weak_pointer (G_OBJECT (self->service),
					   (gpointer*)&(self->service));
		break;
	case PROP_OBJECT_PATH:
		g_return_if_fail (!self->object_path);
		self->object_path = g_strdup (g_value_get_pointer (value));
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
		break;
	}
}

static void
gkd_secret_unlock_get_property (GObject *obj, guint prop_id, GValue *value,
				GParamSpec *pspec)
{
	GkdSecretUnlock *self = GKD_SECRET_UNLOCK (obj);

	switch (prop_id) {
	case PROP_CALLER:
		g_value_set_string (value, self->caller);
		break;
	case PROP_OBJECT_PATH:
		g_value_set_pointer (value, self->object_path);
		break;
	case PROP_SERVICE:
		g_value_set_object (value, self->service);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (obj, prop_id, pspec);
		break;
	}
}


static void
gkd_secret_unlock_class_init (GkdSecretUnlockClass *klass)
{
	GObjectClass *gobject_class = G_OBJECT_CLASS (klass);

	gobject_class->constructor = gkd_secret_unlock_constructor;
	gobject_class->get_property = gkd_secret_unlock_get_property;
	gobject_class->set_property = gkd_secret_unlock_set_property;
	gobject_class->dispose = gkd_secret_unlock_dispose;
	gobject_class->finalize = gkd_secret_unlock_finalize;

	g_object_class_install_property (gobject_class, PROP_CALLER,
		g_param_spec_string ("caller", "Caller", "DBus caller name",
				     NULL, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY ));

	g_object_class_install_property (gobject_class, PROP_OBJECT_PATH,
		g_param_spec_pointer ("object-path", "Object Path", "DBus Object Path",
				      G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));

	g_object_class_install_property (gobject_class, PROP_SERVICE,
		g_param_spec_object ("service", "Service", "Service which owns this prompt",
				     GKD_SECRET_TYPE_SERVICE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY));
}

static void
gkd_secret_dispatch_iface (GkdSecretDispatchIface *iface)
{
}

/* -----------------------------------------------------------------------------
 * PUBLIC
 */

GkdSecretUnlock*
gkd_secret_unlock_new (GkdSecretService *service, const gchar *caller,
		       const gchar *object_path)
{
	return g_object_new (GKD_SECRET_TYPE_UNLOCK,
			     "service", service,
			     "caller", caller,
			     "object-path", object_path,
			     NULL);
}

void
gkd_secret_unlock_queue (GkdSecretUnlock *self, const gchar *unlock_path)
{
	gboolean locked = TRUE;
	GckObject *coll;
	gchar *path;

	g_return_if_fail (GKD_SECRET_IS_UNLOCK (self));
	g_return_if_fail (unlock_path);

	coll = lookup_collection (self, unlock_path);
	if (coll == NULL)
		return;

	/* Try to unlock with an empty password, which produces no prompt */
	if (gkd_secret_unlock_with_password (coll, (const guchar*)"", 0, NULL)) {
		locked = FALSE;

	}

	path = g_strdup (unlock_path);
	if (locked)
		g_queue_push_tail (self->queued, path);
	else
		g_array_append_val (self->results, path);

	g_object_unref (coll);
}

gboolean
gkd_secret_unlock_have_queued (GkdSecretUnlock *self)
{
	g_return_val_if_fail (GKD_SECRET_IS_UNLOCK (self), FALSE);
	return !g_queue_is_empty (self->queued) || self->current;
}

gchar**
gkd_secret_unlock_get_results (GkdSecretUnlock *self, gint *n_results)
{
	g_return_val_if_fail (GKD_SECRET_IS_UNLOCK (self), NULL);
	g_return_val_if_fail (n_results, NULL);
	*n_results = self->results->len;
	return (gchar**)self->results->data;
}

void
gkd_secret_unlock_reset_results (GkdSecretUnlock *self)
{
	gint i;

	g_return_if_fail (GKD_SECRET_IS_UNLOCK (self));

	for (i = 0; i < self->results->len; ++i)
		g_free (g_array_index (self->results, gchar*, i));
	g_array_set_size (self->results, 0);
}

void
gkd_secret_unlock_call_prompt (GkdSecretUnlock *self, const gchar *window_id)
{
	g_return_if_fail (GKD_SECRET_IS_UNLOCK (self));
	g_return_if_fail (!self->prompted);

	g_assert (!self->window_id);
	self->window_id = g_strdup (window_id);

	self->prompted = TRUE;
	perform_next_unlock (self);
}

gboolean
gkd_secret_unlock_with_secret (GckObject *collection,
			       GkdSecretSecret *master,
			       GError **error)
{
	GckBuilder builder = GCK_BUILDER_INIT;
	GckAttributes *attrs;
	GckObject *cred;
	gboolean locked;

	g_return_val_if_fail (GCK_IS_OBJECT (collection), FALSE);
	g_return_val_if_fail (master, FALSE);

	/* Shortcut if already unlocked */
	if (check_locked_collection (collection, &locked) && !locked)
		return TRUE;

	common_unlock_attributes (&builder, collection);
	gck_builder_add_boolean (&builder, CKA_GNOME_TRANSIENT, TRUE);
	gck_builder_add_boolean (&builder, CKA_TOKEN, TRUE);
	attrs = gck_attributes_ref_sink (gck_builder_end (&builder));

	cred = gkd_secret_session_create_credential (master->session, NULL,
						     attrs, master, error);

	gck_attributes_unref (attrs);

	if (cred != NULL)
		g_object_unref (cred);
	return (cred != NULL);
}

gboolean
gkd_secret_unlock_with_password (GckObject *collection, const guchar *password,
				 gsize n_password, GError **error_out)
{
	GckBuilder builder = GCK_BUILDER_INIT;
	GError *error = NULL;
	GckSession *session;
	GckObject *cred;
	gboolean locked;

	g_return_val_if_fail (GCK_IS_OBJECT (collection), FALSE);

	/* Shortcut if already unlocked */
	if (check_locked_collection (collection, &locked) && !locked)
		return TRUE;

	session = gck_object_get_session (collection);
	g_return_val_if_fail (session, FALSE);

	gck_builder_init_full (&builder, GCK_BUILDER_SECURE_MEMORY);
	common_unlock_attributes (&builder, collection);
	gck_builder_add_boolean (&builder, CKA_GNOME_TRANSIENT, TRUE);
	gck_builder_add_boolean (&builder, CKA_TOKEN, TRUE);
	gck_builder_add_data (&builder, CKA_VALUE, password, n_password);

	cred = gck_session_create_object (session, gck_builder_end (&builder), NULL, &error);
	if (cred == NULL) {
		if (g_error_matches (error, GCK_ERROR, CKR_PIN_INCORRECT)) {
			g_set_error_literal (error_out, GKD_SECRET_DAEMON_ERROR,
					     GKD_SECRET_DAEMON_ERROR_DENIED,
					     "The password was incorrect.");
		} else {
			g_message ("couldn't create credential: %s", egg_error_message (error));
			g_set_error_literal (error_out, G_DBUS_ERROR,
					     G_DBUS_ERROR_FAILED,
					     "Couldn't use credentials");
		}
		g_clear_error (&error);
		return FALSE;
	}

	g_object_unref (cred);
	return TRUE;
}
