/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* run-passwd.c: this file is part of users-admin, a gnome-system-tools frontend * for user administration. * * Copyright (C) 2002 Diego Gonzalez * Copyright (C) 2006 Johannes H. Jensen * Copyright (C) 2010 Milan Bouchet-Valat * * Written by: Diego Gonzalez * Modified by: Johannes H. Jensen , * Milan Bouchet-Valat . * * 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; either version 2, 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 General Public License 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., 59 Temple Place - Suite 330, Boston, MA * 02111-1307, USA. * * Most of this code originally comes from gnome-about-me-password.c, * from gnome-control-center. */ #include #include #include #include #include #include #if __sun #include #include #endif #include "run-passwd.h" /* Passwd states */ typedef enum { PASSWD_STATE_NONE, /* Passwd is not asking for anything */ PASSWD_STATE_AUTH, /* Passwd is asking for our current password */ PASSWD_STATE_NEW, /* Passwd is asking for our new password */ PASSWD_STATE_RETYPE, /* Passwd is asking for our retyped new password */ PASSWD_STATE_DONE, /* Passwd succeeded but has not yet exited */ PASSWD_STATE_ERR /* Passwd reported an error but has not yet exited */ } PasswdState; struct PasswdHandler { const char *current_password; const char *new_password; /* Communication with the passwd program */ GPid backend_pid; GIOChannel *backend_stdin; GIOChannel *backend_stdout; GQueue *backend_stdin_queue; /* Write queue to backend_stdin */ /* GMainLoop IDs */ guint backend_child_watch_id; /* g_child_watch_add (PID) */ guint backend_stdout_watch_id; /* g_io_add_watch (stdout) */ /* State of the passwd program */ PasswdState backend_state; gboolean changing_password; PasswdCallback auth_cb; gpointer auth_cb_data; PasswdCallback chpasswd_cb; gpointer chpasswd_cb_data; }; /* Buffer size for backend output */ #define BUFSIZE 64 static GQuark passwd_error_quark (void) { static GQuark q = 0; if (q == 0) { q = g_quark_from_static_string("passwd_error"); } return q; } /* Error handling */ #define PASSWD_ERROR (passwd_error_quark ()) static void stop_passwd (PasswdHandler *passwd_handler); static void free_passwd_resources (PasswdHandler *passwd_handler); static gboolean io_watch_stdout (GIOChannel *source, GIOCondition condition, PasswdHandler *passwd_handler); /* * Spawning and closing of backend {{ */ /* Child watcher */ static void child_watch_cb (GPid pid, gint status, PasswdHandler *passwd_handler) { if (WIFEXITED (status)) { if (WEXITSTATUS (status) >= 255) { g_warning ("Child exited unexpectedly"); } if (WEXITSTATUS (status) == 0) { if (passwd_handler->backend_state == PASSWD_STATE_RETYPE) { passwd_handler->backend_state = PASSWD_STATE_DONE; if (passwd_handler->chpasswd_cb) passwd_handler->chpasswd_cb (passwd_handler, NULL, passwd_handler->chpasswd_cb_data); } } } free_passwd_resources (passwd_handler); } static void ignore_sigpipe (gpointer data) { signal (SIGPIPE, SIG_IGN); } /* Spawn passwd backend * Returns: TRUE on success, FALSE otherwise and sets error appropriately */ static gboolean spawn_passwd (PasswdHandler *passwd_handler, GError **error) { gchar *argv[2]; gchar **envp; gint my_stdin, my_stdout, my_stderr; argv[0] = "/usr/bin/passwd"; /* Is it safe to rely on a hard-coded path? */ argv[1] = NULL; envp = g_get_environ (); envp = g_environ_setenv (envp, "LC_ALL", "C", TRUE); if (!g_spawn_async_with_pipes (NULL, /* Working directory */ argv, /* Argument vector */ envp, /* Environment */ G_SPAWN_DO_NOT_REAP_CHILD, /* Flags */ ignore_sigpipe, /* Child setup */ NULL, /* Data to child setup */ &passwd_handler->backend_pid, /* PID */ &my_stdin, /* Stdin */ &my_stdout, /* Stdout */ &my_stderr, /* Stderr */ error)) { /* GError */ /* An error occured */ free_passwd_resources (passwd_handler); g_strfreev (envp); return FALSE; } g_strfreev (envp); /* 2>&1 */ if (dup2 (my_stderr, my_stdout) == -1) { /* Failed! */ g_set_error_literal (error, PASSWD_ERROR, PASSWD_ERROR_BACKEND, strerror (errno)); /* Clean up */ stop_passwd (passwd_handler); return FALSE; } /* Open IO Channels */ passwd_handler->backend_stdin = g_io_channel_unix_new (my_stdin); passwd_handler->backend_stdout = g_io_channel_unix_new (my_stdout); /* Set raw encoding */ /* Set nonblocking mode */ if (g_io_channel_set_encoding (passwd_handler->backend_stdin, NULL, error) != G_IO_STATUS_NORMAL || g_io_channel_set_encoding (passwd_handler->backend_stdout, NULL, error) != G_IO_STATUS_NORMAL || g_io_channel_set_flags (passwd_handler->backend_stdin, G_IO_FLAG_NONBLOCK, error) != G_IO_STATUS_NORMAL || g_io_channel_set_flags (passwd_handler->backend_stdout, G_IO_FLAG_NONBLOCK, error) != G_IO_STATUS_NORMAL ) { /* Clean up */ stop_passwd (passwd_handler); return FALSE; } /* Turn off buffering */ g_io_channel_set_buffered (passwd_handler->backend_stdin, FALSE); g_io_channel_set_buffered (passwd_handler->backend_stdout, FALSE); /* Add IO Channel watcher */ passwd_handler->backend_stdout_watch_id = g_io_add_watch (passwd_handler->backend_stdout, G_IO_IN | G_IO_PRI, (GIOFunc) io_watch_stdout, passwd_handler); /* Add child watcher */ passwd_handler->backend_child_watch_id = g_child_watch_add (passwd_handler->backend_pid, (GChildWatchFunc) child_watch_cb, passwd_handler); /* Success! */ return TRUE; } /* Stop passwd backend */ static void stop_passwd (PasswdHandler *passwd_handler) { /* This is the standard way of returning from the dialog with passwd. * If we return this way we can safely kill passwd as it has completed * its task. */ if (passwd_handler->backend_pid != -1) { kill (passwd_handler->backend_pid, 9); } /* We must run free_passwd_resources here and not let our child * watcher do it, since it will access invalid memory after the * dialog has been closed and cleaned up. * * If we had more than a single thread we'd need to remove * the child watch before trying to kill the child. */ free_passwd_resources (passwd_handler); } /* Clean up passwd resources */ static void free_passwd_resources (PasswdHandler *passwd_handler) { GError *error = NULL; /* Remove the child watcher */ if (passwd_handler->backend_child_watch_id != 0) { g_source_remove (passwd_handler->backend_child_watch_id); passwd_handler->backend_child_watch_id = 0; } /* Close IO channels (internal file descriptors are automatically closed) */ if (passwd_handler->backend_stdin != NULL) { if (g_io_channel_shutdown (passwd_handler->backend_stdin, TRUE, &error) != G_IO_STATUS_NORMAL) { g_warning ("Could not shutdown backend_stdin IO channel: %s", error->message); g_error_free (error); error = NULL; } g_io_channel_unref (passwd_handler->backend_stdin); passwd_handler->backend_stdin = NULL; } if (passwd_handler->backend_stdout != NULL) { if (g_io_channel_shutdown (passwd_handler->backend_stdout, TRUE, &error) != G_IO_STATUS_NORMAL) { g_warning ("Could not shutdown backend_stdout IO channel: %s", error->message); g_error_free (error); error = NULL; } g_io_channel_unref (passwd_handler->backend_stdout); passwd_handler->backend_stdout = NULL; } /* Remove IO watcher */ if (passwd_handler->backend_stdout_watch_id != 0) { g_source_remove (passwd_handler->backend_stdout_watch_id); passwd_handler->backend_stdout_watch_id = 0; } /* Close PID */ if (passwd_handler->backend_pid != -1) { g_spawn_close_pid (passwd_handler->backend_pid); passwd_handler->backend_pid = -1; } /* Clear backend state */ passwd_handler->backend_state = PASSWD_STATE_NONE; } /* * }} Spawning and closing of backend */ /* * Backend communication code {{ */ /* Write the first element of queue through channel */ static void io_queue_pop (GQueue *queue, GIOChannel *channel) { gchar *buf; gsize bytes_written; GError *error = NULL; buf = g_queue_pop_head (queue); if (buf != NULL) { if (g_io_channel_write_chars (channel, buf, -1, &bytes_written, &error) != G_IO_STATUS_NORMAL) { g_warning ("Could not write queue element \"%s\" to channel: %s", buf, error->message); g_error_free (error); } /* Ensure passwords are cleared from memory */ memset (buf, 0, strlen (buf)); g_free (buf); } } /* Goes through the argument list, checking if one of them occurs in str * Returns: TRUE as soon as an element is found to match, FALSE otherwise */ static gboolean is_string_complete (gchar *str, ...) { va_list ap; gchar *arg; if (strlen (str) == 0) { return FALSE; } va_start (ap, str); while ((arg = va_arg (ap, char *)) != NULL) { if (strstr (str, arg) != NULL) { va_end (ap); return TRUE; } } va_end (ap); return FALSE; } /* * IO watcher for stdout, called whenever there is data to read from the backend. * This is where most of the actual IO handling happens. */ static gboolean io_watch_stdout (GIOChannel *source, GIOCondition condition, PasswdHandler *passwd_handler) { static GString *str = NULL; /* Persistent buffer */ gchar buf[BUFSIZE]; /* Temporary buffer */ gsize bytes_read; GError *gio_error = NULL; /* Error returned by functions */ GError *error = NULL; /* Error sent to callbacks */ gboolean reinit = FALSE; /* Initialize buffer */ if (str == NULL) { str = g_string_new (""); } if (g_io_channel_read_chars (source, buf, BUFSIZE, &bytes_read, &gio_error) != G_IO_STATUS_NORMAL) { g_warning ("IO Channel read error: %s", gio_error->message); g_error_free (gio_error); return TRUE; } str = g_string_append_len (str, buf, bytes_read); /* In which state is the backend? */ switch (passwd_handler->backend_state) { case PASSWD_STATE_AUTH: /* Passwd is asking for our current password */ if (is_string_complete (str->str, "assword: ", "failure", "wrong", "error", NULL)) { if (strstr (str->str, "assword: ") != NULL && strstr (str->str, "incorrect") == NULL) { /* Authentication successful */ passwd_handler->backend_state = PASSWD_STATE_NEW; /* Trigger callback to update authentication status */ if (passwd_handler->auth_cb) passwd_handler->auth_cb (passwd_handler, NULL, passwd_handler->auth_cb_data); } else { /* Authentication failed */ error = g_error_new_literal (PASSWD_ERROR, PASSWD_ERROR_AUTH_FAILED, _("Authentication failed")); passwd_handler->changing_password = FALSE; /* This error can happen both while authenticating or while changing password: * if chpasswd_cb is set, this means we're already changing password */ if (passwd_handler->chpasswd_cb) passwd_handler->chpasswd_cb (passwd_handler, error, passwd_handler->chpasswd_cb_data); else if (passwd_handler->auth_cb) passwd_handler->auth_cb (passwd_handler, error, passwd_handler->auth_cb_data); g_error_free (error); } reinit = TRUE; } break; case PASSWD_STATE_NEW: /* Passwd is asking for our new password */ if (is_string_complete (str->str, "assword: ", NULL)) { /* Advance to next state */ passwd_handler->backend_state = PASSWD_STATE_RETYPE; /* Pop retyped password from queue and into IO channel */ io_queue_pop (passwd_handler->backend_stdin_queue, passwd_handler->backend_stdin); reinit = TRUE; } break; case PASSWD_STATE_RETYPE: /* Passwd is asking for our retyped new password */ if (is_string_complete (str->str, "successfully", "short", "longer", "palindrome", "dictionary", "simple", "simplistic", "similar", "case", "different", "wrapped", "recovered", "recent", "unchanged", "match", "1 numeric or special", "failure", "DIFFERENT", "BAD PASSWORD", NULL)) { if (strstr (str->str, "successfully") != NULL) { /* Hooray! */ passwd_handler->backend_state = PASSWD_STATE_DONE; /* Trigger callback to update status */ if (passwd_handler->chpasswd_cb) passwd_handler->chpasswd_cb (passwd_handler, NULL, passwd_handler->chpasswd_cb_data); } else { /* Ohnoes! */ if (strstr (str->str, "recovered") != NULL) { /* What does this indicate? * "Authentication information cannot be recovered?" from libpam? */ error = g_error_new_literal (PASSWD_ERROR, PASSWD_ERROR_UNKNOWN, str->str); } else if (strstr (str->str, "short") != NULL || strstr (str->str, "longer") != NULL) { error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED, _("The new password is too short")); } else if (strstr (str->str, "palindrome") != NULL || strstr (str->str, "simple") != NULL || strstr (str->str, "simplistic") != NULL || strstr (str->str, "dictionary") != NULL) { error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED, _("The new password is too simple")); } else if (strstr (str->str, "similar") != NULL || strstr (str->str, "different") != NULL || strstr (str->str, "case") != NULL || strstr (str->str, "wrapped") != NULL) { error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED, _("The old and new passwords are too similar")); } else if (strstr (str->str, "recent") != NULL) { error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED, _("The new password has already been used recently.")); } else if (strstr (str->str, "1 numeric or special") != NULL) { error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED, _("The new password must contain numeric or special characters")); } else if (strstr (str->str, "unchanged") != NULL || strstr (str->str, "match") != NULL) { error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED, _("The old and new passwords are the same")); } else if (strstr (str->str, "failure") != NULL) { /* Authentication failure */ error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_AUTH_FAILED, _("Your password has been changed since you initially authenticated!")); } else if (strstr (str->str, "DIFFERENT")) { error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_REJECTED, _("The new password does not contain enough different characters")); } else { error = g_error_new (PASSWD_ERROR, PASSWD_ERROR_UNKNOWN, _("Unknown error")); } /* At this point, passwd might have exited, in which case * child_watch_cb should clean up for us and remove this watcher. * On some error conditions though, passwd just re-prompts us * for our new password. */ passwd_handler->backend_state = PASSWD_STATE_ERR; passwd_handler->changing_password = FALSE; /* Trigger callback to update status */ if (passwd_handler->chpasswd_cb) passwd_handler->chpasswd_cb (passwd_handler, error, passwd_handler->chpasswd_cb_data); g_error_free (error); } reinit = TRUE; /* child_watch_cb should clean up for us now */ } break; case PASSWD_STATE_NONE: /* Passwd is not asking for anything yet */ if (is_string_complete (str->str, "assword: ", NULL)) { /* If the user does not have a password set, * passwd will immediately ask for the new password, * so skip the AUTH phase */ if (is_string_complete (str->str, "new", "New", NULL)) { gchar *pw; passwd_handler->backend_state = PASSWD_STATE_NEW; /* since passwd didn't ask for our old password * in this case, simply remove it from the queue */ pw = g_queue_pop_head (passwd_handler->backend_stdin_queue); g_free (pw); /* Pop the IO queue, i.e. send new password */ io_queue_pop (passwd_handler->backend_stdin_queue, passwd_handler->backend_stdin); } else { passwd_handler->backend_state = PASSWD_STATE_AUTH; /* Pop the IO queue, i.e. send current password */ io_queue_pop (passwd_handler->backend_stdin_queue, passwd_handler->backend_stdin); } reinit = TRUE; } break; default: /* Passwd has returned an error */ reinit = TRUE; break; } if (reinit) { g_string_free (str, TRUE); str = NULL; } /* Continue calling us */ return TRUE; } /* * }} Backend communication code */ /* Adds the current password to the IO queue */ static void authenticate (PasswdHandler *passwd_handler) { gchar *s; s = g_strdup_printf ("%s\n", passwd_handler->current_password); g_queue_push_tail (passwd_handler->backend_stdin_queue, s); } /* Adds the new password twice to the IO queue */ static void update_password (PasswdHandler *passwd_handler) { gchar *s; s = g_strdup_printf ("%s\n", passwd_handler->new_password); g_queue_push_tail (passwd_handler->backend_stdin_queue, s); /* We need to allocate new space because io_queue_pop() g_free()s * every element of the queue after it's done */ g_queue_push_tail (passwd_handler->backend_stdin_queue, g_strdup (s)); } PasswdHandler * passwd_init (void) { PasswdHandler *passwd_handler; passwd_handler = g_new0 (PasswdHandler, 1); /* Initialize backend_pid. -1 means the backend is not running */ passwd_handler->backend_pid = -1; /* Initialize IO Channels */ passwd_handler->backend_stdin = NULL; passwd_handler->backend_stdout = NULL; /* Initialize write queue */ passwd_handler->backend_stdin_queue = g_queue_new (); /* Initialize watchers */ passwd_handler->backend_child_watch_id = 0; passwd_handler->backend_stdout_watch_id = 0; /* Initialize backend state */ passwd_handler->backend_state = PASSWD_STATE_NONE; passwd_handler->changing_password = FALSE; return passwd_handler; } void passwd_destroy (PasswdHandler *passwd_handler) { g_queue_free (passwd_handler->backend_stdin_queue); stop_passwd (passwd_handler); g_free (passwd_handler); } void passwd_authenticate (PasswdHandler *passwd_handler, const char *current_password, PasswdCallback cb, const gpointer user_data) { GError *error = NULL; /* Don't stop if we've already started chaging password */ if (passwd_handler->changing_password) return; /* Clear data from possible previous attempts to change password */ passwd_handler->new_password = NULL; passwd_handler->chpasswd_cb = NULL; passwd_handler->chpasswd_cb_data = NULL; g_queue_foreach (passwd_handler->backend_stdin_queue, (GFunc) g_free, NULL); g_queue_clear (passwd_handler->backend_stdin_queue); passwd_handler->current_password = current_password; passwd_handler->auth_cb = cb; passwd_handler->auth_cb_data = user_data; /* Spawn backend */ stop_passwd (passwd_handler); if (!spawn_passwd (passwd_handler, &error)) { g_warning ("%s", error->message); g_error_free (error); return; } authenticate (passwd_handler); /* Our IO watcher should now handle the rest */ } gboolean passwd_change_password (PasswdHandler *passwd_handler, const char *new_password, PasswdCallback cb, const gpointer user_data) { GError *error = NULL; passwd_handler->changing_password = TRUE; passwd_handler->new_password = new_password; passwd_handler->chpasswd_cb = cb; passwd_handler->chpasswd_cb_data = user_data; /* Stop passwd if an error occured and it is still running */ if (passwd_handler->backend_state == PASSWD_STATE_ERR) { /* Stop passwd, free resources */ stop_passwd (passwd_handler); } /* Check that the backend is still running, or that an error * has occured but it has not yet exited */ if (passwd_handler->backend_pid == -1) { /* If it is not, re-run authentication */ /* Spawn backend */ stop_passwd (passwd_handler); if (!spawn_passwd (passwd_handler, &error)) { g_warning ("%s", error->message); g_error_free (error); return FALSE; } /* Add current and new passwords to queue */ authenticate (passwd_handler); update_password (passwd_handler); } else { /* Only add new passwords to queue */ update_password (passwd_handler); } /* Pop new password through the backend. * If user has no password, popping the queue would output current * password, while 'passwd' is waiting for the new one. So wait for * io_watch_stdout() to remove current password from the queue, * and output the new one for us. */ if (passwd_handler->current_password) io_queue_pop (passwd_handler->backend_stdin_queue, passwd_handler->backend_stdin); /* Our IO watcher should now handle the rest */ return TRUE; }