/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
* e-passwords.c
*
* Copyright (C) 1999-2008 Novell, Inc. (www.novell.com)
*/
/*
* 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.
*
* 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.
*
* 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/>.
*/
/*
* This looks a lot more complicated than it is, and than you'd think
* it would need to be. There is however, method to the madness.
*
* The code must cope with being called from any thread at any time,
* recursively from the main thread, and then serialising every
* request so that sane and correct values are always returned, and
* duplicate requests are never made.
*
* To this end, every call is marshalled and queued and a dispatch
* method invoked until that request is satisfied. If mainloop
* recursion occurs, then the sub-call will necessarily return out of
* order, but will not be processed out of order.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <gtk/gtk.h>
#include <glib/gi18n-lib.h>
/* XXX Yeah, yeah... */
#define SECRET_API_SUBJECT_TO_CHANGE
#include <libsecret/secret.h>
#include <libedataserver/libedataserver.h>
#include "e-passwords.h"
#define d(x)
typedef struct _EPassMsg EPassMsg;
struct _EPassMsg {
void (*dispatch) (EPassMsg *);
EFlag *done;
/* input */
GtkWindow *parent;
const gchar *key;
const gchar *title;
const gchar *prompt;
const gchar *oldpass;
guint32 flags;
/* output */
gboolean *remember;
gchar *password;
GError *error;
/* work variables */
GtkWidget *entry;
GtkWidget *check;
guint ismain : 1;
guint noreply:1; /* supress replies; when calling
* dispatch functions from others */
};
/* XXX probably want to share this with evalution-source-registry-migrate-sources.c */
static const SecretSchema e_passwords_schema = {
"org.gnome.Evolution.Password",
SECRET_SCHEMA_DONT_MATCH_NAME,
{
{ "application", SECRET_SCHEMA_ATTRIBUTE_STRING, },
{ "user", SECRET_SCHEMA_ATTRIBUTE_STRING, },
{ "server", SECRET_SCHEMA_ATTRIBUTE_STRING, },
{ "protocol", SECRET_SCHEMA_ATTRIBUTE_STRING, },
}
};
G_LOCK_DEFINE_STATIC (passwords);
static GThread *main_thread = NULL;
static GHashTable *password_cache = NULL;
static GtkDialog *password_dialog = NULL;
static GQueue message_queue = G_QUEUE_INIT;
static gint idle_id;
static gint ep_online_state = TRUE;
static EUri *
ep_keyring_uri_new (const gchar *string,
GError **error)
{
EUri *uri;
uri = e_uri_new (string);
g_return_val_if_fail (uri != NULL, NULL);
/* LDAP URIs do not have usernames, so use the URI as the username. */
if (uri->user == NULL && uri->protocol != NULL &&
(strcmp (uri->protocol, "ldap") == 0|| strcmp (uri->protocol, "google") == 0))
uri->user = g_strdelimit (g_strdup (string), "/=", '_');
/* Make sure the URI has the required components. */
if (uri->user == NULL && uri->host == NULL) {
g_set_error_literal (
error, G_IO_ERROR,
G_IO_ERROR_INVALID_ARGUMENT,
_("Keyring key is unusable: no user or host name"));
e_uri_free (uri);
uri = NULL;
}
return uri;
}
static gboolean
ep_idle_dispatch (gpointer data)
{
EPassMsg *msg;
/* As soon as a password window is up we stop; it will
* re - invoke us when it has been closed down */
G_LOCK (passwords);
while (password_dialog == NULL && (msg = g_queue_pop_head (&message_queue)) != NULL) {
G_UNLOCK (passwords);
msg->dispatch (msg);
G_LOCK (passwords);
}
idle_id = 0;
G_UNLOCK (passwords);
return FALSE;
}
static EPassMsg *
ep_msg_new (void (*dispatch) (EPassMsg *))
{
EPassMsg *msg;
e_passwords_init ();
msg = g_malloc0 (sizeof (*msg));
msg->dispatch = dispatch;
msg->done = e_flag_new ();
msg->ismain = (g_thread_self () == main_thread);
return msg;
}
static void
ep_msg_free (EPassMsg *msg)
{
/* XXX We really should be passing this back to the caller, but
* doing so will require breaking the password API. */
if (msg->error != NULL) {
g_warning ("%s", msg->error->message);
g_error_free (msg->error);
}
e_flag_free (msg->done);
g_free (msg->password);
g_free (msg);
}
static void
ep_msg_send (EPassMsg *msg)
{
gint needidle = 0;
G_LOCK (passwords);
g_queue_push_tail (&message_queue, msg);
if (!idle_id) {
if (!msg->ismain)
idle_id = g_idle_add (ep_idle_dispatch, NULL);
else
needidle = 1;
}
G_UNLOCK (passwords);
if (msg->ismain) {
if (needidle)
ep_idle_dispatch (NULL);
while (!e_flag_is_set (msg->done))
g_main_context_iteration (NULL, TRUE);
} else
e_flag_wait (msg->done);
}
/* the functions that actually do the work */
static void
ep_remember_password (EPassMsg *msg)
{
gchar *password;
EUri *uri;
GError *error = NULL;
password = g_hash_table_lookup (password_cache, msg->key);
if (password == NULL) {
g_warning ("Password for key \"%s\" not found", msg->key);
goto exit;
}
uri = ep_keyring_uri_new (msg->key, &msg->error);
if (uri == NULL)
goto exit;
secret_password_store_sync (
&e_passwords_schema,
SECRET_COLLECTION_DEFAULT,
msg->key, password,
NULL, &error,
"application", "Evolution",
"user", uri->user,
"server", uri->host,
"protocol", uri->protocol,
NULL);
/* Only remove the password from the session hash
* if the keyring insertion was successful. */
if (error == NULL)
g_hash_table_remove (password_cache, msg->key);
else
g_propagate_error (&msg->error, error);
e_uri_free (uri);
exit:
if (!msg->noreply)
e_flag_set (msg->done);
}
static void
ep_forget_password (EPassMsg *msg)
{
EUri *uri;
GError *error = NULL;
g_hash_table_remove (password_cache, msg->key);
uri = ep_keyring_uri_new (msg->key, &msg->error);
if (uri == NULL)
goto exit;
/* Find all Evolution passwords matching the URI and delete them.
*
* XXX We didn't always store protocols in the keyring, so for
* backward-compatibility we need to lookup passwords by user
* and host only (no protocol). But we do send the protocol
* to ep_keyring_delete_passwords(), which also knows about
* the backward-compatibility issue and will filter the list
* appropriately. */
secret_password_clear_sync (
&e_passwords_schema, NULL, &error,
"application", "Evolution",
"user", uri->user,
"server", uri->host,
NULL);
if (error != NULL)
g_propagate_error (&msg->error, error);
e_uri_free (uri);
exit:
if (!msg->noreply)
e_flag_set (msg->done);
}
static void
ep_get_password (EPassMsg *msg)
{
EUri *uri;
gchar *password;
GError *error = NULL;
/* Check the in-memory cache first. */
password = g_hash_table_lookup (password_cache, msg->key);
if (password != NULL) {
msg->password = g_strdup (password);
goto exit;
}
uri = ep_keyring_uri_new (msg->key, &msg->error);
if (uri == NULL)
goto exit;
msg->password = secret_password_lookup_sync (
&e_passwords_schema, NULL, &error,
"application", "Evolution",
"user", uri->user,
"server", uri->host,
"protocol", uri->protocol,
NULL);
if (msg->password != NULL)
goto done;
/* Clear the previous error, if there was one.
* It's likely to occur again. */
if (error != NULL)
g_clear_error (&error);
/* XXX We didn't always store protocols in the keyring, so for
* backward-compatibility we also need to lookup passwords
* by user and host only (no protocol). */
msg->password = secret_password_lookup_sync (
&e_passwords_schema, NULL, &error,
"application", "Evolution",
"user", uri->user,
"server", uri->host,
NULL);
done:
if (error != NULL)
g_propagate_error (&msg->error, error);
e_uri_free (uri);
exit:
if (!msg->noreply)
e_flag_set (msg->done);
}
static void
ep_add_password (EPassMsg *msg)
{
g_hash_table_insert (
password_cache, g_strdup (msg->key),
g_strdup (msg->oldpass));
if (!msg->noreply)
e_flag_set (msg->done);
}
static void ep_ask_password (EPassMsg *msg);
static void
pass_response (GtkDialog *dialog,
gint response,
gpointer data)
{
EPassMsg *msg = data;
gint type = msg->flags & E_PASSWORDS_REMEMBER_MASK;
GList *iter, *trash = NULL;
if (response == GTK_RESPONSE_OK) {
msg->password = g_strdup (gtk_entry_get_text ((GtkEntry *) msg->entry));
if (type != E_PASSWORDS_REMEMBER_NEVER) {
gint noreply = msg->noreply;
*msg->remember = gtk_toggle_button_get_active (GTK_TOGGLE_BUTTON (msg->check));
msg->noreply = 1;
if (*msg->remember || type == E_PASSWORDS_REMEMBER_FOREVER) {
msg->oldpass = msg->password;
ep_add_password (msg);
}
if (*msg->remember && type == E_PASSWORDS_REMEMBER_FOREVER)
ep_remember_password (msg);
msg->noreply = noreply;
}
}
gtk_widget_destroy ((GtkWidget *) dialog);
password_dialog = NULL;
/* ok, here things get interesting, we suck up any pending
* operations on this specific password, and return the same
* result or ignore other operations */
G_LOCK (passwords);
for (iter = g_queue_peek_head_link (&message_queue); iter != NULL; iter = iter->next) {
EPassMsg *pending = iter->data;
if ((pending->dispatch == ep_forget_password
|| pending->dispatch == ep_get_password
|| pending->dispatch == ep_ask_password)
&& strcmp (pending->key, msg->key) == 0) {
/* Satisfy the pending operation. */
pending->password = g_strdup (msg->password);
e_flag_set (pending->done);
/* Mark the queue node for deletion. */
trash = g_list_prepend (trash, iter);
}
}
/* Expunge the message queue. */
for (iter = trash; iter != NULL; iter = iter->next)
g_queue_delete_link (&message_queue, iter->data);
g_list_free (trash);
G_UNLOCK (passwords);
if (!msg->noreply)
e_flag_set (msg->done);
ep_idle_dispatch (NULL);
}
static gboolean
update_capslock_state (GtkDialog *dialog,
GdkEvent *event,
GtkWidget *label)
{
GdkModifierType mask = 0;
GdkWindow *window;
gchar *markup = NULL;
GdkDeviceManager *device_manager;
GdkDevice *device;
device_manager = gdk_display_get_device_manager (gtk_widget_get_display (label));
device = gdk_device_manager_get_client_pointer (device_manager);
window = gtk_widget_get_window (GTK_WIDGET (dialog));
gdk_window_get_device_position (window, device, NULL, NULL, &mask);
/* The space acts as a vertical placeholder. */
markup = g_markup_printf_escaped (
"<small>%s</small>", (mask & GDK_LOCK_MASK) ?
_("You have the Caps Lock key on.") : " ");
gtk_label_set_markup (GTK_LABEL (label), markup);
g_free (markup);
return FALSE;
}
static void
ep_ask_password (EPassMsg *msg)
{
GtkWidget *widget;
GtkWidget *container;
GtkWidget *action_area;
GtkWidget *content_area;
gint type = msg->flags & E_PASSWORDS_REMEMBER_MASK;
guint noreply = msg->noreply;
gboolean visible;
AtkObject *a11y;
msg->noreply = 1;
widget = gtk_dialog_new_with_buttons (
msg->title, msg->parent, 0,
_("_Cancel"), GTK_RESPONSE_CANCEL,
_("_OK"), GTK_RESPONSE_OK,
NULL);
gtk_dialog_set_default_response (
GTK_DIALOG (widget), GTK_RESPONSE_OK);
gtk_window_set_resizable (GTK_WINDOW (widget), FALSE);
gtk_window_set_transient_for (GTK_WINDOW (widget), msg->parent);
gtk_window_set_position (GTK_WINDOW (widget), GTK_WIN_POS_CENTER_ON_PARENT);
gtk_container_set_border_width (GTK_CONTAINER (widget), 12);
password_dialog = GTK_DIALOG (widget);
action_area = gtk_dialog_get_action_area (password_dialog);
content_area = gtk_dialog_get_content_area (password_dialog);
/* Override GtkDialog defaults */
gtk_box_set_spacing (GTK_BOX (action_area), 12);
gtk_container_set_border_width (GTK_CONTAINER (action_area), 0);
gtk_box_set_spacing (GTK_BOX (content_area), 12);
gtk_container_set_border_width (GTK_CONTAINER (content_area), 0);
/* Grid */
container = gtk_grid_new ();
gtk_grid_set_column_spacing (GTK_GRID (container), 12);
gtk_grid_set_row_spacing (GTK_GRID (container), 6);
gtk_widget_show (container);
gtk_box_pack_start (
GTK_BOX (content_area), container, FALSE, TRUE, 0);
/* Password Image */
widget = gtk_image_new_from_icon_name (
"dialog-password", GTK_ICON_SIZE_DIALOG);
gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.0);
g_object_set (
G_OBJECT (widget),
"halign", GTK_ALIGN_FILL,
"vexpand", TRUE,
"valign", GTK_ALIGN_FILL,
NULL);
gtk_widget_show (widget);
gtk_grid_attach (GTK_GRID (container), widget, 0, 0, 1, 3);
/* Password Label */
widget = gtk_label_new (NULL);
gtk_label_set_line_wrap (GTK_LABEL (widget), TRUE);
gtk_label_set_markup (GTK_LABEL (widget), msg->prompt);
gtk_misc_set_alignment (GTK_MISC (widget), 0.0, 0.5);
g_object_set (
G_OBJECT (widget),
"hexpand", TRUE,
"halign", GTK_ALIGN_FILL,
NULL);
gtk_widget_show (widget);
gtk_grid_attach (GTK_GRID (container), widget, 1, 0, 1, 1);
/* Password Entry */
widget = gtk_entry_new ();
a11y = gtk_widget_get_accessible (widget);
visible = !(msg->flags & E_PASSWORDS_SECRET);
atk_object_set_description (a11y, msg->prompt);
gtk_entry_set_visibility (GTK_ENTRY (widget), visible);
gtk_entry_set_activates_default (GTK_ENTRY (widget), TRUE);
gtk_widget_grab_focus (widget);
g_object_set (
G_OBJECT (widget),
"hexpand", TRUE,
"halign", GTK_ALIGN_FILL,
NULL);
gtk_widget_show (widget);
msg->entry = widget;
if ((msg->flags & E_PASSWORDS_REPROMPT)) {
ep_get_password (msg);
if (msg->password != NULL) {
gtk_entry_set_text (GTK_ENTRY (widget), msg->password);
g_free (msg->password);
msg->password = NULL;
}
}
gtk_grid_attach (GTK_GRID (container), widget, 1, 1, 1, 1);
/* Caps Lock Label */
widget = gtk_label_new (NULL);
g_object_set (
G_OBJECT (widget),
"hexpand", TRUE,
"halign", GTK_ALIGN_FILL,
NULL);
gtk_widget_show (widget);
gtk_grid_attach (GTK_GRID (container), widget, 1, 2, 1, 1);
g_signal_connect (
password_dialog, "key-release-event",
G_CALLBACK (update_capslock_state), widget);
g_signal_connect (
password_dialog, "focus-in-event",
G_CALLBACK (update_capslock_state), widget);
/* static password, shouldn't be remembered between sessions,
* but will be remembered within the session beyond our control */
if (type != E_PASSWORDS_REMEMBER_NEVER) {
if (msg->flags & E_PASSWORDS_PASSPHRASE) {
widget = gtk_check_button_new_with_mnemonic (
(type == E_PASSWORDS_REMEMBER_FOREVER)
? _("_Remember this passphrase")
: _("_Remember this passphrase for"
" the remainder of this session"));
} else {
widget = gtk_check_button_new_with_mnemonic (
(type == E_PASSWORDS_REMEMBER_FOREVER)
? _("_Remember this password")
: _("_Remember this password for"
" the remainder of this session"));
}
gtk_toggle_button_set_active (
GTK_TOGGLE_BUTTON (widget), *msg->remember);
if (msg->flags & E_PASSWORDS_DISABLE_REMEMBER)
gtk_widget_set_sensitive (widget, FALSE);
g_object_set (
G_OBJECT (widget),
"hexpand", TRUE,
"halign", GTK_ALIGN_FILL,
"valign", GTK_ALIGN_FILL,
NULL);
gtk_widget_show (widget);
msg->check = widget;
gtk_grid_attach (GTK_GRID (container), widget, 1, 3, 1, 1);
}
msg->noreply = noreply;
g_signal_connect (
password_dialog, "response",
G_CALLBACK (pass_response), msg);
if (msg->parent) {
gtk_dialog_run (GTK_DIALOG (password_dialog));
} else {
gtk_window_present (GTK_WINDOW (password_dialog));
/* workaround GTK+ bug (see Gnome's bugzilla bug #624229) */
gtk_grab_add (GTK_WIDGET (password_dialog));
}
}
/**
* e_passwords_init:
*
* Initializes the e_passwords routines. Must be called before any other
* e_passwords_* function.
**/
void
e_passwords_init (void)
{
G_LOCK (passwords);
if (password_cache == NULL) {
password_cache = g_hash_table_new_full (
g_str_hash, g_str_equal,
(GDestroyNotify) g_free,
(GDestroyNotify) g_free);
main_thread = g_thread_self ();
}
G_UNLOCK (passwords);
}
/**
* e_passwords_set_online:
* @state:
*
* Set the offline-state of the application. This is a work-around
* for having the backends fully offline aware, and returns a
* cancellation response instead of prompting for passwords.
*
* FIXME: This is not a permanent api, review post 2.0.
**/
void
e_passwords_set_online (gint state)
{
ep_online_state = state;
/* TODO: we could check that a request is open and close it, or maybe who cares */
}
/**
* e_passwords_remember_password:
* @key: the key
*
* Saves the password associated with @key to disk.
**/
void
e_passwords_remember_password (const gchar *key)
{
EPassMsg *msg;
g_return_if_fail (key != NULL);
msg = ep_msg_new (ep_remember_password);
msg->key = key;
ep_msg_send (msg);
ep_msg_free (msg);
}
/**
* e_passwords_forget_password:
* @key: the key
*
* Forgets the password associated with @key, in memory and on disk.
**/
void
e_passwords_forget_password (const gchar *key)
{
EPassMsg *msg;
g_return_if_fail (key != NULL);
msg = ep_msg_new (ep_forget_password);
msg->key = key;
ep_msg_send (msg);
ep_msg_free (msg);
}
/**
* e_passwords_get_password:
* @key: the key
*
* Returns: the password associated with @key, or %NULL. Caller
* must free the returned password.
**/
gchar *
e_passwords_get_password (const gchar *key)
{
EPassMsg *msg;
gchar *passwd;
g_return_val_if_fail (key != NULL, NULL);
msg = ep_msg_new (ep_get_password);
msg->key = key;
ep_msg_send (msg);
passwd = msg->password;
msg->password = NULL;
ep_msg_free (msg);
return passwd;
}
/**
* e_passwords_add_password:
* @key: a key
* @passwd: the password for @key
*
* This stores the @key/@passwd pair in the current session's password
* hash.
**/
void
e_passwords_add_password (const gchar *key,
const gchar *passwd)
{
EPassMsg *msg;
g_return_if_fail (key != NULL);
g_return_if_fail (passwd != NULL);
msg = ep_msg_new (ep_add_password);
msg->key = key;
msg->oldpass = passwd;
ep_msg_send (msg);
ep_msg_free (msg);
}
/**
* e_passwords_ask_password:
* @title: title for the password dialog
* @key: key to store the password under
* @prompt: prompt string
* @remember_type: whether or not to offer to remember the password,
* and for how long.
* @remember: on input, the default state of the remember checkbox.
* on output, the state of the checkbox when the dialog was closed.
* @parent: parent window of the dialog, or %NULL
*
* Asks the user for a password.
*
* Returns: the password, which the caller must free, or %NULL if
* the user cancelled the operation. *@remember will be set if the
* return value is non-%NULL and @remember_type is not
* E_PASSWORDS_DO_NOT_REMEMBER.
**/
gchar *
e_passwords_ask_password (const gchar *title,
const gchar *key,
const gchar *prompt,
EPasswordsRememberType remember_type,
gboolean *remember,
GtkWindow *parent)
{
gchar *passwd;
EPassMsg *msg;
g_return_val_if_fail (key != NULL, NULL);
if ((remember_type & E_PASSWORDS_ONLINE) && !ep_online_state)
return NULL;
msg = ep_msg_new (ep_ask_password);
msg->title = title;
msg->key = key;
msg->prompt = prompt;
msg->flags = remember_type;
msg->remember = remember;
msg->parent = parent;
ep_msg_send (msg);
passwd = msg->password;
msg->password = NULL;
ep_msg_free (msg);
return passwd;
}