/* libsecret - GLib wrapper for Secret Service
 *
 * Copyright 2019 Red Hat, Inc.
 *
 * 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 licence or (at
 * your option) any later version.
 *
 * See the included COPYING file for more information.
 *
 * Author: Daiki Ueno
 */

#include "config.h"

#include "secret-backend.h"
#include "secret-file-backend.h"
#include "secret-file-collection.h"
#include "secret-file-item.h"
#include "secret-private.h"
#include "secret-retrievable.h"

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

EGG_SECURE_DECLARE (secret_file_backend);

#include <gio/gunixfdlist.h>
#include <gio/gunixinputstream.h>
#include <glib-unix.h>

#define PORTAL_BUS_NAME "org.freedesktop.portal.Desktop"
#define PORTAL_OBJECT_PATH "/org/freedesktop/portal/desktop"
#define PORTAL_REQUEST_INTERFACE "org.freedesktop.portal.Request"
#define PORTAL_SECRET_INTERFACE "org.freedesktop.portal.Secret"
#define PORTAL_SECRET_VERSION 1

static void secret_file_backend_async_initable_iface (GAsyncInitableIface *iface);
static void secret_file_backend_backend_iface (SecretBackendInterface *iface);

struct _SecretFileBackend {
	GObject parent;
	SecretFileCollection *collection;
	SecretServiceFlags init_flags;
};

G_DEFINE_TYPE_WITH_CODE (SecretFileBackend, secret_file_backend, G_TYPE_OBJECT,
			 G_IMPLEMENT_INTERFACE (G_TYPE_ASYNC_INITABLE, secret_file_backend_async_initable_iface);
			 G_IMPLEMENT_INTERFACE (SECRET_TYPE_BACKEND, secret_file_backend_backend_iface);
			 _secret_backend_ensure_extension_point ();
			 g_io_extension_point_implement (SECRET_BACKEND_EXTENSION_POINT_NAME,
							 g_define_type_id,
							 "file",
							 0)
);

enum {
	PROP_0,
	PROP_FLAGS
};

static void
secret_file_backend_init (SecretFileBackend *self)
{
}

static void
secret_file_backend_set_property (GObject *object,
                                  guint prop_id,
                                  const GValue *value,
                                  GParamSpec *pspec)
{
	SecretFileBackend *self = SECRET_FILE_BACKEND (object);

	switch (prop_id) {
	case PROP_FLAGS:
		self->init_flags = g_value_get_flags (value);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
secret_file_backend_get_property (GObject *object,
                                  guint prop_id,
                                  GValue *value,
                                  GParamSpec *pspec)
{
	SecretFileBackend *self = SECRET_FILE_BACKEND (object);

	switch (prop_id) {
	case PROP_FLAGS:
		g_value_set_flags (value, self->init_flags);
		break;
	default:
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
		break;
	}
}

static void
secret_file_backend_finalize (GObject *object)
{
	SecretFileBackend *self = SECRET_FILE_BACKEND (object);

	g_clear_object (&self->collection);

	G_OBJECT_CLASS (secret_file_backend_parent_class)->finalize (object);
}

static void
secret_file_backend_class_init (SecretFileBackendClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	object_class->set_property = secret_file_backend_set_property;
	object_class->get_property = secret_file_backend_get_property;
	object_class->finalize = secret_file_backend_finalize;

	/**
	 * SecretFileBackend:flags:
	 *
	 * A set of flags describing which parts of the secret file have
	 * been initialized.
	 */
	g_object_class_override_property (object_class, PROP_FLAGS, "flags");
}

static void
on_collection_new_async (GObject *source_object,
			 GAsyncResult *result,
			 gpointer user_data)
{
	GTask *task = G_TASK (user_data);
	SecretFileBackend *self = g_task_get_source_object (task);
	GObject *object;
	GError *error = NULL;

	object = g_async_initable_new_finish (G_ASYNC_INITABLE (source_object),
					      result,
					      &error);
	if (object == NULL) {
		g_task_return_error (task, error);
		g_object_unref (task);
		return;
	}

	self->collection = SECRET_FILE_COLLECTION (object);
	g_task_return_boolean (task, TRUE);
	g_object_unref (task);
}

typedef struct {
	gint io_priority;
	GFile *file;
	GInputStream *stream;
	gchar *buffer;
	GDBusConnection *connection;
	gchar *request_path;
	guint portal_signal_id;
	gulong cancellable_signal_id;
} InitClosure;

static void
init_closure_free (gpointer data)
{
	InitClosure *init = data;
	g_object_unref (init->file);
	g_clear_object (&init->stream);
	g_clear_pointer (&init->buffer, egg_secure_free);
	g_clear_object (&init->connection);
	g_clear_pointer (&init->request_path, g_free);
	g_slice_free (InitClosure, init);
}

#define PASSWORD_SIZE 64

static void
on_read_all (GObject *source_object,
	     GAsyncResult *result,
	     gpointer user_data)
{
	GInputStream *stream = G_INPUT_STREAM (source_object);
	GTask *task = G_TASK (user_data);
	InitClosure *init = g_task_get_task_data (task);
	gsize bytes_read;
	SecretValue *password;
	GError *error = NULL;

	if (!g_input_stream_read_all_finish (stream, result, &bytes_read,
					     &error)) {
		g_task_return_error (task, error);
		g_object_unref (task);
		return;
	}

	if (bytes_read != PASSWORD_SIZE) {
		g_task_return_new_error (task,
					 SECRET_ERROR,
					 SECRET_ERROR_PROTOCOL,
					 "invalid password returned from portal");
		g_object_unref (task);
		return;
	}

	password = secret_value_new (init->buffer, bytes_read, "text/plain");
	g_async_initable_new_async (SECRET_TYPE_FILE_COLLECTION,
				    init->io_priority,
				    g_task_get_cancellable (task),
				    on_collection_new_async,
				    task,
				    "file", g_object_ref (init->file),
				    "password", password,
				    NULL);
	g_object_unref (init->file);
	secret_value_unref (password);
}

static void
on_portal_response (GDBusConnection *connection,
		    const gchar *sender_name,
		    const gchar *object_path,
		    const gchar *interface_name,
		    const gchar *signal_name,
		    GVariant *parameters,
		    gpointer user_data)
{
	GTask *task = G_TASK (user_data);
	InitClosure *init = g_task_get_task_data (task);
	guint32 response;

	if (init->cancellable_signal_id) {
		g_cancellable_disconnect (g_task_get_cancellable (task), init->cancellable_signal_id);
		init->cancellable_signal_id = 0;
	}

	g_dbus_connection_signal_unsubscribe (connection,
					      init->portal_signal_id);

	g_variant_get (parameters, "(ua{sv})", &response, NULL);

	switch (response) {
	case 0:
		init->buffer = egg_secure_alloc (PASSWORD_SIZE);
		g_input_stream_read_all_async (init->stream,
					       init->buffer, PASSWORD_SIZE,
					       G_PRIORITY_DEFAULT,
					       g_task_get_cancellable (task),
					       on_read_all,
					       task);
		break;
	case 1:
		g_task_return_new_error (task,
					 G_IO_ERROR,
					 G_IO_ERROR_CANCELLED,
					 "user interaction cancelled");
		g_object_unref (task);
		break;
	case 2:
		g_task_return_new_error (task,
					 G_IO_ERROR,
					 G_IO_ERROR_FAILED,
					 "user interaction failed");
		g_object_unref (task);
		break;
	}
}

static void
on_portal_request_close (GObject *source_object,
			 GAsyncResult *result,
			 gpointer user_data)
{
	GDBusConnection *connection = G_DBUS_CONNECTION (source_object);
	GTask *task = G_TASK (user_data);
	GError *error = NULL;

	if (!g_dbus_connection_call_finish (connection, result, &error)) {
		g_task_return_error (task, error);
		g_object_unref (task);
		return;
	}

	g_task_return_boolean (task, TRUE);
	g_object_unref (task);
}

static void
on_portal_cancel (GCancellable *cancellable,
		  gpointer user_data)
{
	GTask *task = G_TASK (user_data);
	InitClosure *init = g_task_get_task_data (task);

	g_dbus_connection_call (init->connection,
				PORTAL_BUS_NAME,
				init->request_path,
				PORTAL_REQUEST_INTERFACE,
				"Close",
				NULL,
				NULL,
				G_DBUS_CALL_FLAGS_NONE,
				-1,
				cancellable,
				on_portal_request_close,
				task);

	g_cancellable_disconnect (cancellable, init->cancellable_signal_id);
	init->cancellable_signal_id = 0;
}

static void
on_portal_retrieve_secret (GObject *source_object,
			   GAsyncResult *result,
			   gpointer user_data)
{
	GDBusConnection *connection = G_DBUS_CONNECTION (source_object);
	GTask *task = G_TASK (user_data);
	InitClosure *init = g_task_get_task_data (task);
	GCancellable *cancellable = g_task_get_cancellable (task);
	GVariant *reply;
	GError *error = NULL;

	reply = g_dbus_connection_call_with_unix_fd_list_finish (connection,
								 NULL,
								 result,
								 &error);
	if (reply == NULL) {
		g_task_return_error (task, error);
		g_object_unref (task);
		return;
	}

	g_variant_get (reply, "(o)", &init->request_path);
	g_variant_unref (reply);

	init->portal_signal_id =
		g_dbus_connection_signal_subscribe (connection,
						    PORTAL_BUS_NAME,
						    PORTAL_REQUEST_INTERFACE,
						    "Response",
						    init->request_path,
						    NULL,
						    G_DBUS_SIGNAL_FLAGS_NO_MATCH_RULE,
						    on_portal_response,
						    task,
						    NULL);

	if (cancellable != NULL)
		init->cancellable_signal_id =
			g_cancellable_connect (cancellable,
					       G_CALLBACK (on_portal_cancel),
					       task,
					       NULL);
}

static void
on_bus_get (GObject *source_object,
	    GAsyncResult *result,
	    gpointer user_data)
{
	GDBusConnection *connection;
	GTask *task = G_TASK (user_data);
	InitClosure *init = g_task_get_task_data (task);
	GUnixFDList *fd_list;
	gint fds[2];
	gint fd_index;
	GVariantBuilder options;
	GError *error = NULL;

	connection = g_bus_get_finish (result, &error);
	if (connection == NULL) {
		g_task_return_error (task, error);
		g_object_unref (task);
		return;
	}
	init->connection = connection;

	if (!g_unix_open_pipe (fds, FD_CLOEXEC, &error)) {
		g_object_unref (connection);
		g_task_return_error (task, error);
		g_object_unref (task);
		return;
	}

	fd_list = g_unix_fd_list_new ();
	fd_index = g_unix_fd_list_append (fd_list, fds[1], &error);
	close (fds[1]);
	if (fd_index < 0) {
		close (fds[0]);
		g_object_unref (fd_list);
		g_object_unref (connection);
		g_task_return_error (task, error);
		g_object_unref (task);
		return;
	}

	close (fds[1]);
	init->stream = g_unix_input_stream_new (fds[0], TRUE);

	g_variant_builder_init (&options, G_VARIANT_TYPE_VARDICT);
	g_dbus_connection_call_with_unix_fd_list (connection,
						  PORTAL_BUS_NAME,
						  PORTAL_OBJECT_PATH,
						  PORTAL_SECRET_INTERFACE,
						  "RetrieveSecret",
						  g_variant_new ("(h@a{sv})",
								 fd_index,
								 g_variant_builder_end (&options)),
						  G_VARIANT_TYPE ("(o)"),
						  G_DBUS_CALL_FLAGS_NONE,
						  -1,
						  fd_list,
						  g_task_get_cancellable (task),
						  on_portal_retrieve_secret,
						  task);
	g_object_unref (fd_list);
}

static void
secret_file_backend_real_init_async (GAsyncInitable *initable,
				     int io_priority,
				     GCancellable *cancellable,
				     GAsyncReadyCallback callback,
				     gpointer user_data)
{
	gchar *path;
	GFile *file;
	GFile *dir;
	SecretValue *password;
	const gchar *envvar;
	GTask *task;
	GError *error = NULL;
	InitClosure *init;
	gboolean ret;

	task = g_task_new (initable, cancellable, callback, user_data);

	envvar = g_getenv ("SECRET_FILE_TEST_PATH");
	if (envvar != NULL && *envvar != '\0')
		path = g_strdup (envvar);
	else {
		path = g_build_filename (g_get_user_data_dir (),
					 "keyrings",
					 SECRET_COLLECTION_DEFAULT ".keyring",
					 NULL);
	}

	file = g_file_new_for_path (path);
	g_free (path);

	dir = g_file_get_parent (file);
	if (dir == NULL) {
		g_task_return_new_error (task,
					 G_IO_ERROR,
					 G_IO_ERROR_INVALID_ARGUMENT,
					 "not a valid path");
		g_object_unref (file);
		g_object_unref (task);
		return;
	}

	ret = g_file_make_directory_with_parents (dir, cancellable, &error);
	g_object_unref (dir);
	if (!ret) {
		if (g_error_matches (error, G_IO_ERROR, G_IO_ERROR_EXISTS))
			g_clear_error (&error);
		else {
			g_task_return_error (task, error);
			g_object_unref (file);
			g_object_unref (task);
			return;
		}
	}

	envvar = g_getenv ("SECRET_FILE_TEST_PASSWORD");
	if (envvar != NULL && *envvar != '\0') {
		password = secret_value_new (envvar, -1, "text/plain");
		g_async_initable_new_async (SECRET_TYPE_FILE_COLLECTION,
					    io_priority,
					    cancellable,
					    on_collection_new_async,
					    task,
					    "file", file,
					    "password", password,
					    NULL);
		g_object_unref (file);
		secret_value_unref (password);
	} else if (g_file_test ("/.flatpak-info", G_FILE_TEST_EXISTS)) {
		init = g_slice_new0 (InitClosure);
		init->io_priority = io_priority;
		init->file = file;
		g_task_set_task_data (task, init, init_closure_free);
		g_bus_get (G_BUS_TYPE_SESSION, cancellable, on_bus_get, task);
	} else {
		g_task_return_new_error (task,
					 G_IO_ERROR,
					 G_IO_ERROR_INVALID_ARGUMENT,
					 "master password is not retrievable");
		g_object_unref (task);
		return;
	}
}

static gboolean
secret_file_backend_real_init_finish (GAsyncInitable *initable,
				      GAsyncResult *result,
				      GError **error)
{
	g_return_val_if_fail (g_task_is_valid (result, initable), FALSE);

	return g_task_propagate_boolean (G_TASK (result), error);
}

static void
secret_file_backend_async_initable_iface (GAsyncInitableIface *iface)
{
	iface->init_async = secret_file_backend_real_init_async;
	iface->init_finish = secret_file_backend_real_init_finish;
}

static void
on_collection_write (GObject *source_object,
		     GAsyncResult *result,
		     gpointer user_data)
{
	SecretFileCollection *collection =
		SECRET_FILE_COLLECTION (source_object);
	GTask *task = G_TASK (user_data);
	GError *error = NULL;

	if (!secret_file_collection_write_finish (collection, result, &error)) {
		g_task_return_error (task, error);
		g_object_unref (task);
		return;
	}

	g_task_return_boolean (task, TRUE);
	g_object_unref (task);
}

static void
secret_file_backend_real_store (SecretBackend *backend,
				const SecretSchema *schema,
				GHashTable *attributes,
				const gchar *collection,
				const gchar *label,
				SecretValue *value,
				GCancellable *cancellable,
				GAsyncReadyCallback callback,
				gpointer user_data)
{
	SecretFileBackend *self = SECRET_FILE_BACKEND (backend);
	GTask *task;
	GError *error = NULL;

	/* Warnings raised already */
	if (schema != NULL && !_secret_attributes_validate (schema, attributes, G_STRFUNC, FALSE))
		return;

	task = g_task_new (self, cancellable, callback, user_data);

	if (!secret_file_collection_replace (self->collection,
					     attributes,
					     label,
					     value,
					     &error)) {
		g_task_return_error (task, error);
		g_object_unref (task);
		return;
	}

	secret_file_collection_write (self->collection,
				      cancellable,
				      on_collection_write,
				      task);
}

static gboolean
secret_file_backend_real_store_finish (SecretBackend *backend,
				       GAsyncResult *result,
				       GError **error)
{
	g_return_val_if_fail (g_task_is_valid (result, backend), FALSE);

	return g_task_propagate_boolean (G_TASK (result), error);
}

static void
on_retrieve_secret (GObject *source_object,
		    GAsyncResult *result,
		    gpointer user_data)
{
	SecretRetrievable *retrievable = SECRET_RETRIEVABLE (source_object);
	GTask *task = G_TASK (user_data);
	SecretValue *value;
	GError *error;

	value = secret_retrievable_retrieve_secret_finish (retrievable,
							   result,
							   &error);
	g_object_unref (retrievable);
	if (value == NULL) {
		g_task_return_error (task, error);
		g_object_unref (task);
	}
	g_task_return_pointer (task, value, secret_value_unref);
	g_object_unref (task);
}

static void
secret_file_backend_real_lookup (SecretBackend *backend,
				 const SecretSchema *schema,
				 GHashTable *attributes,
				 GCancellable *cancellable,
				 GAsyncReadyCallback callback,
				 gpointer user_data)
{
	SecretFileBackend *self = SECRET_FILE_BACKEND (backend);
	GTask *task;
	GList *matches;
	GVariant *variant;
	SecretFileItem *item;
	GError *error = NULL;

	/* Warnings raised already */
	if (schema != NULL && !_secret_attributes_validate (schema, attributes, G_STRFUNC, TRUE))
		return;

	task = g_task_new (self, cancellable, callback, user_data);

	matches = secret_file_collection_search (self->collection, attributes);

	if (matches == NULL) {
		g_task_return_pointer (task, NULL, NULL);
		g_object_unref (task);
		return;
	}

	variant = g_variant_ref (matches->data);
	g_list_free_full (matches, (GDestroyNotify)g_variant_unref);

	item = _secret_file_item_decrypt (variant, self->collection, &error);
	g_variant_unref (variant);
	if (item == NULL) {
		g_task_return_error (task, error);
		g_object_unref (task);
		return;
	}

	secret_retrievable_retrieve_secret (SECRET_RETRIEVABLE (item),
					    cancellable,
					    on_retrieve_secret,
					    task);
}

static SecretValue *
secret_file_backend_real_lookup_finish (SecretBackend *backend,
					GAsyncResult *result,
					GError **error)
{
	g_return_val_if_fail (g_task_is_valid (result, backend), NULL);

	return g_task_propagate_pointer (G_TASK (result), error);
}

static void
secret_file_backend_real_clear (SecretBackend *backend,
				const SecretSchema *schema,
				GHashTable *attributes,
				GCancellable *cancellable,
				GAsyncReadyCallback callback,
				gpointer user_data)
{
	SecretFileBackend *self = SECRET_FILE_BACKEND (backend);
	GTask *task;
	GError *error = NULL;
	gboolean ret;

	/* Warnings raised already */
	if (schema != NULL && !_secret_attributes_validate (schema, attributes, G_STRFUNC, TRUE))
		return;

	task = g_task_new (self, cancellable, callback, user_data);

	ret = secret_file_collection_clear (self->collection, attributes, &error);
	if (error != NULL) {
		g_task_return_error (task, error);
		g_object_unref (task);
		return;
	}

	/* No need to write as nothing has been removed. */
	if (!ret) {
		g_task_return_boolean (task, FALSE);
		g_object_unref (task);
		return;
	}

	secret_file_collection_write (self->collection,
				      cancellable,
				      on_collection_write,
				      task);
}

static gboolean
secret_file_backend_real_clear_finish (SecretBackend *backend,
				       GAsyncResult *result,
				       GError **error)
{
	g_return_val_if_fail (g_task_is_valid (result, backend), FALSE);

	return g_task_propagate_boolean (G_TASK (result), error);
}

static void
unref_objects (gpointer data)
{
	GList *list = data;

	g_list_free_full (list, g_object_unref);
}

static void
secret_file_backend_real_search (SecretBackend *backend,
				 const SecretSchema *schema,
				 GHashTable *attributes,
				 SecretSearchFlags flags,
				 GCancellable *cancellable,
				 GAsyncReadyCallback callback,
				 gpointer user_data)
{
	SecretFileBackend *self = SECRET_FILE_BACKEND (backend);
	GTask *task;
	GList *matches;
	GList *results = NULL;
	GList *l;
	GError *error = NULL;

	/* Warnings raised already */
	if (schema != NULL && !_secret_attributes_validate (schema, attributes, G_STRFUNC, FALSE))
		return;

	task = g_task_new (self, cancellable, callback, user_data);

	matches = secret_file_collection_search (self->collection, attributes);
	for (l = matches; l; l = g_list_next (l)) {
		SecretFileItem *item = _secret_file_item_decrypt (l->data, self->collection, &error);
		if (item == NULL) {
			g_task_return_error (task, error);
			g_object_unref (task);
			return;
		}
		results = g_list_append (results, item);
	}
	g_list_free_full (matches, (GDestroyNotify)g_variant_unref);

	g_task_return_pointer (task, results, unref_objects);
	g_object_unref (task);
}

static GList *
secret_file_backend_real_search_finish (SecretBackend *backend,
					GAsyncResult *result,
					GError **error)
{
	g_return_val_if_fail (g_task_is_valid (result, backend), NULL);

	return g_task_propagate_pointer (G_TASK (result), error);
}

static void
secret_file_backend_backend_iface (SecretBackendInterface *iface)
{
	iface->store = secret_file_backend_real_store;
	iface->store_finish = secret_file_backend_real_store_finish;
	iface->lookup = secret_file_backend_real_lookup;
	iface->lookup_finish = secret_file_backend_real_lookup_finish;
	iface->clear = secret_file_backend_real_clear;
	iface->clear_finish = secret_file_backend_real_clear_finish;
	iface->search = secret_file_backend_real_search;
	iface->search_finish = secret_file_backend_real_search_finish;
}

gboolean
_secret_file_backend_check_portal_version (void)
{
	GDBusConnection *connection;
	GVariant *ret;
	GVariant *value;
	guint32 version;
	GError *error = NULL;

	connection = g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, &error);
	if (!connection) {
		g_warning ("couldn't get session bus: %s", error->message);
		g_error_free (error);
		return FALSE;
	}

	ret = g_dbus_connection_call_sync (connection,
					   PORTAL_BUS_NAME,
					   PORTAL_OBJECT_PATH,
					   "org.freedesktop.DBus.Properties",
					   "Get",
					   g_variant_new ("(ss)",
							  PORTAL_SECRET_INTERFACE,
							  "version"),
					   G_VARIANT_TYPE ("(v)"),
					   0, -1, NULL, &error);
	g_object_unref (connection);
	if (!ret) {
		g_info ("secret portal is not available: %s", error->message);
		g_error_free (error);
		return FALSE;
	}

	g_variant_get (ret, "(v)", &value);
	g_variant_unref (ret);
	version = g_variant_get_uint32 (value);
	g_variant_unref (value);
	if (version != PORTAL_SECRET_VERSION) {
		g_info ("secret portal version mismatch: %u != %u", version, PORTAL_SECRET_VERSION);
		return FALSE;
	}

	return TRUE;
}
