Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/mRemoteNG/PuTTYNG.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'unix/askpass.c')
-rw-r--r--unix/askpass.c662
1 files changed, 662 insertions, 0 deletions
diff --git a/unix/askpass.c b/unix/askpass.c
new file mode 100644
index 00000000..b45a1c23
--- /dev/null
+++ b/unix/askpass.c
@@ -0,0 +1,662 @@
+/*
+ * GTK implementation of a GUI password/passphrase prompt.
+ */
+
+#include <assert.h>
+#include <time.h>
+#include <stdlib.h>
+
+#include <unistd.h>
+
+#include <gtk/gtk.h>
+#include <gdk/gdk.h>
+#if !GTK_CHECK_VERSION(3,0,0)
+#include <gdk/gdkkeysyms.h>
+#endif
+
+#include "defs.h"
+#include "unifont.h"
+#include "gtkcompat.h"
+#include "gtkmisc.h"
+
+#include "putty.h"
+#include "ssh.h"
+#include "misc.h"
+
+#define N_DRAWING_AREAS 3
+
+struct drawing_area_ctx {
+ GtkWidget *area;
+#ifndef DRAW_DEFAULT_CAIRO
+ GdkColor *cols;
+#endif
+ int width, height;
+ enum { NOT_CURRENT, CURRENT, GREYED_OUT } state;
+};
+
+struct askpass_ctx {
+ GtkWidget *dialog, *promptlabel;
+ struct drawing_area_ctx drawingareas[N_DRAWING_AREAS];
+ int active_area;
+#if GTK_CHECK_VERSION(2,0,0)
+ GtkIMContext *imc;
+#endif
+#ifndef DRAW_DEFAULT_CAIRO
+ GdkColormap *colmap;
+ GdkColor cols[3];
+#endif
+ char *error_message; /* if we finish without a passphrase */
+ char *passphrase; /* if we finish with one */
+ int passlen, passsize;
+#if GTK_CHECK_VERSION(3,20,0)
+ GdkSeat *seat; /* for gdk_seat_grab */
+#elif GTK_CHECK_VERSION(3,0,0)
+ GdkDevice *keyboard; /* for gdk_device_grab */
+#endif
+
+ int nattempts;
+};
+
+static prng *keypress_prng = NULL;
+static void feed_keypress_prng(void *data, int size)
+{
+ put_data(keypress_prng, data, size);
+}
+void random_add_noise(NoiseSourceId source, const void *noise, int length)
+{
+ if (keypress_prng)
+ prng_add_entropy(keypress_prng, source, make_ptrlen(noise, length));
+}
+static void setup_keypress_prng(void)
+{
+ keypress_prng = prng_new(&ssh_sha256);
+ prng_seed_begin(keypress_prng);
+ noise_get_heavy(feed_keypress_prng);
+ prng_seed_finish(keypress_prng);
+}
+static void cleanup_keypress_prng(void)
+{
+ prng_free(keypress_prng);
+}
+static uint64_t keypress_prng_value(void)
+{
+ /*
+ * Don't actually put the passphrase keystrokes themselves into
+ * the PRNG; that doesn't seem like the course of wisdom when
+ * that's precisely what the information displayed on the screen
+ * is trying _not_ to be correlated to.
+ */
+ noise_ultralight(NOISE_SOURCE_KEY, 0);
+ uint8_t data[8];
+ prng_read(keypress_prng, data, 8);
+ return GET_64BIT_MSB_FIRST(data);
+}
+static int choose_new_area(int prev_area)
+{
+ int reduced = keypress_prng_value() % (N_DRAWING_AREAS - 1);
+ return (prev_area + 1 + reduced) % N_DRAWING_AREAS;
+}
+
+static void visually_acknowledge_keypress(struct askpass_ctx *ctx)
+{
+ int new_active = choose_new_area(ctx->active_area);
+ ctx->drawingareas[ctx->active_area].state = NOT_CURRENT;
+ gtk_widget_queue_draw(ctx->drawingareas[ctx->active_area].area);
+ ctx->drawingareas[new_active].state = CURRENT;
+ gtk_widget_queue_draw(ctx->drawingareas[new_active].area);
+ ctx->active_area = new_active;
+}
+
+static int last_char_len(struct askpass_ctx *ctx)
+{
+ /*
+ * GTK always encodes in UTF-8, so we can do this in a fixed way.
+ */
+ int i;
+ assert(ctx->passlen > 0);
+ i = ctx->passlen - 1;
+ while ((unsigned)((unsigned char)ctx->passphrase[i] - 0x80) < 0x40) {
+ if (i == 0)
+ break;
+ i--;
+ }
+ return ctx->passlen - i;
+}
+
+static void add_text_to_passphrase(struct askpass_ctx *ctx, gchar *str)
+{
+ int len = strlen(str);
+ if (ctx->passlen + len >= ctx->passsize) {
+ /* Take some care with buffer expansion, because there are
+ * pieces of passphrase in the old buffer so we should ensure
+ * realloc doesn't leave a copy lying around in the address
+ * space. */
+ int oldsize = ctx->passsize;
+ char *newbuf;
+
+ ctx->passsize = (ctx->passlen + len) * 5 / 4 + 1024;
+ newbuf = snewn(ctx->passsize, char);
+ memcpy(newbuf, ctx->passphrase, oldsize);
+ smemclr(ctx->passphrase, oldsize);
+ sfree(ctx->passphrase);
+ ctx->passphrase = newbuf;
+ }
+ strcpy(ctx->passphrase + ctx->passlen, str);
+ ctx->passlen += len;
+ visually_acknowledge_keypress(ctx);
+}
+
+static void cancel_askpass(struct askpass_ctx *ctx, const char *msg)
+{
+ smemclr(ctx->passphrase, ctx->passsize);
+ ctx->passphrase = NULL;
+ ctx->error_message = dupstr(msg);
+ gtk_main_quit();
+}
+
+static gboolean askpass_dialog_closed(GtkWidget *widget, GdkEvent *event,
+ gpointer data)
+{
+ struct askpass_ctx *ctx = (struct askpass_ctx *)data;
+ cancel_askpass(ctx, "passphrase input cancelled");
+ /* Don't destroy dialog yet, so gtk_askpass_cleanup() can do its work */
+ return true;
+}
+
+static gint key_event(GtkWidget *widget, GdkEventKey *event, gpointer data)
+{
+ struct askpass_ctx *ctx = (struct askpass_ctx *)data;
+
+ if (event->keyval == GDK_KEY_Return &&
+ event->type == GDK_KEY_PRESS) {
+ gtk_main_quit();
+ } else if (event->keyval == GDK_KEY_Escape &&
+ event->type == GDK_KEY_PRESS) {
+ cancel_askpass(ctx, "passphrase input cancelled");
+ } else {
+#if GTK_CHECK_VERSION(2,0,0)
+ if (gtk_im_context_filter_keypress(ctx->imc, event))
+ return true;
+#endif
+
+ if (event->type == GDK_KEY_PRESS) {
+ if (!strcmp(event->string, "\x15")) {
+ /* Ctrl-U. Wipe out the whole line */
+ ctx->passlen = 0;
+ visually_acknowledge_keypress(ctx);
+ } else if (!strcmp(event->string, "\x17")) {
+ /* Ctrl-W. Delete back to the last space->nonspace
+ * boundary. We interpret 'space' in a really simple
+ * way (mimicking terminal drivers), and don't attempt
+ * to second-guess exciting Unicode space
+ * characters. */
+ while (ctx->passlen > 0) {
+ char deleted, prior;
+ ctx->passlen -= last_char_len(ctx);
+ deleted = ctx->passphrase[ctx->passlen];
+ prior = (ctx->passlen == 0 ? ' ' :
+ ctx->passphrase[ctx->passlen-1]);
+ if (!g_ascii_isspace(deleted) && g_ascii_isspace(prior))
+ break;
+ }
+ visually_acknowledge_keypress(ctx);
+ } else if (event->keyval == GDK_KEY_BackSpace) {
+ /* Backspace. Delete one character. */
+ if (ctx->passlen > 0)
+ ctx->passlen -= last_char_len(ctx);
+ visually_acknowledge_keypress(ctx);
+#if !GTK_CHECK_VERSION(2,0,0)
+ } else if (event->string[0]) {
+ add_text_to_passphrase(ctx, event->string);
+#endif
+ }
+ }
+ }
+ return true;
+}
+
+#if GTK_CHECK_VERSION(2,0,0)
+static void input_method_commit_event(GtkIMContext *imc, gchar *str,
+ gpointer data)
+{
+ struct askpass_ctx *ctx = (struct askpass_ctx *)data;
+ add_text_to_passphrase(ctx, str);
+}
+#endif
+
+static gint configure_area(GtkWidget *widget, GdkEventConfigure *event,
+ gpointer data)
+{
+ struct drawing_area_ctx *ctx = (struct drawing_area_ctx *)data;
+ ctx->width = event->width;
+ ctx->height = event->height;
+ gtk_widget_queue_draw(widget);
+ return true;
+}
+
+#ifdef DRAW_DEFAULT_CAIRO
+static void askpass_redraw_cairo(cairo_t *cr, struct drawing_area_ctx *ctx)
+{
+ double rgbval = (ctx->state == CURRENT ? 0 :
+ ctx->state == NOT_CURRENT ? 1 : 0.5);
+ cairo_set_source_rgb(cr, rgbval, rgbval, rgbval);
+ cairo_paint(cr);
+}
+#else
+static void askpass_redraw_gdk(GdkWindow *win, struct drawing_area_ctx *ctx)
+{
+ GdkGC *gc = gdk_gc_new(win);
+ gdk_gc_set_foreground(gc, &ctx->cols[ctx->state]);
+ gdk_draw_rectangle(win, gc, true, 0, 0, ctx->width, ctx->height);
+ gdk_gc_unref(gc);
+}
+#endif
+
+#if GTK_CHECK_VERSION(3,0,0)
+static gint draw_area(GtkWidget *widget, cairo_t *cr, gpointer data)
+{
+ struct drawing_area_ctx *ctx = (struct drawing_area_ctx *)data;
+ askpass_redraw_cairo(cr, ctx);
+ return true;
+}
+#else
+static gint expose_area(GtkWidget *widget, GdkEventExpose *event,
+ gpointer data)
+{
+ struct drawing_area_ctx *ctx = (struct drawing_area_ctx *)data;
+
+#ifdef DRAW_DEFAULT_CAIRO
+ cairo_t *cr = gdk_cairo_create(gtk_widget_get_window(ctx->area));
+ askpass_redraw_cairo(cr, ctx);
+ cairo_destroy(cr);
+#else
+ askpass_redraw_gdk(gtk_widget_get_window(ctx->area), ctx);
+#endif
+
+ return true;
+}
+#endif
+
+static gboolean try_grab_keyboard(gpointer vctx)
+{
+ struct askpass_ctx *ctx = (struct askpass_ctx *)vctx;
+ int i, ret;
+
+#if GTK_CHECK_VERSION(3,20,0)
+ /*
+ * Grabbing the keyboard in GTK 3.20 requires the new notion of
+ * GdkSeat.
+ */
+ GdkSeat *seat;
+ GdkWindow *gdkw = gtk_widget_get_window(ctx->dialog);
+ if (!GDK_IS_WINDOW(gdkw) || !gdk_window_is_visible(gdkw))
+ goto fail;
+
+ seat = gdk_display_get_default_seat(
+ gtk_widget_get_display(ctx->dialog));
+ if (!seat)
+ goto fail;
+
+ ctx->seat = seat;
+ ret = gdk_seat_grab(seat, gdkw, GDK_SEAT_CAPABILITY_KEYBOARD,
+ true, NULL, NULL, NULL, NULL);
+
+ /*
+ * For some reason GDK 3.22 hides the GDK window as a side effect
+ * of a failed grab. I've no idea why. But if we're going to retry
+ * the grab, then we need to unhide it again or else we'll just
+ * get GDK_GRAB_NOT_VIEWABLE on every subsequent attempt.
+ */
+ if (ret != GDK_GRAB_SUCCESS)
+ gdk_window_show(gdkw);
+
+#elif GTK_CHECK_VERSION(3,0,0)
+ /*
+ * And it has to be done differently again prior to GTK 3.20.
+ */
+ GdkDeviceManager *dm;
+ GdkDevice *pointer, *keyboard;
+
+ dm = gdk_display_get_device_manager(
+ gtk_widget_get_display(ctx->dialog));
+ if (!dm)
+ goto fail;
+
+ pointer = gdk_device_manager_get_client_pointer(dm);
+ if (!pointer)
+ goto fail;
+ keyboard = gdk_device_get_associated_device(pointer);
+ if (!keyboard)
+ goto fail;
+ if (gdk_device_get_source(keyboard) != GDK_SOURCE_KEYBOARD)
+ goto fail;
+
+ ctx->keyboard = keyboard;
+ ret = gdk_device_grab(ctx->keyboard,
+ gtk_widget_get_window(ctx->dialog),
+ GDK_OWNERSHIP_NONE,
+ true,
+ GDK_KEY_PRESS_MASK | GDK_KEY_RELEASE_MASK,
+ NULL,
+ GDK_CURRENT_TIME);
+#else
+ /*
+ * It's much simpler in GTK 1 and 2!
+ */
+ ret = gdk_keyboard_grab(gtk_widget_get_window(ctx->dialog),
+ false, GDK_CURRENT_TIME);
+#endif
+ if (ret != GDK_GRAB_SUCCESS)
+ goto fail;
+
+ /*
+ * Now that we've got the keyboard grab, connect up our keyboard
+ * handlers.
+ */
+#if GTK_CHECK_VERSION(2,0,0)
+ g_signal_connect(G_OBJECT(ctx->imc), "commit",
+ G_CALLBACK(input_method_commit_event), ctx);
+#endif
+ g_signal_connect(G_OBJECT(ctx->dialog), "key_press_event",
+ G_CALLBACK(key_event), ctx);
+ g_signal_connect(G_OBJECT(ctx->dialog), "key_release_event",
+ G_CALLBACK(key_event), ctx);
+#if GTK_CHECK_VERSION(2,0,0)
+ gtk_im_context_set_client_window(ctx->imc,
+ gtk_widget_get_window(ctx->dialog));
+#endif
+
+ /*
+ * And repaint the key-acknowledgment drawing areas as not greyed
+ * out.
+ */
+ ctx->active_area = keypress_prng_value() % N_DRAWING_AREAS;
+ for (i = 0; i < N_DRAWING_AREAS; i++) {
+ ctx->drawingareas[i].state =
+ (i == ctx->active_area ? CURRENT : NOT_CURRENT);
+ gtk_widget_queue_draw(ctx->drawingareas[i].area);
+ }
+
+ return false;
+
+ fail:
+ /*
+ * If we didn't get the grab, reschedule ourself on a timer to try
+ * again later.
+ *
+ * We have to do this rather than just trying once, because there
+ * is at least one important situation in which the grab may fail
+ * the first time: any user who is launching an add-key operation
+ * off some kind of window manager hotkey will almost by
+ * definition be running this script with a keyboard grab already
+ * active, namely the one-key grab that the WM (or whatever) uses
+ * to detect presses of the hotkey. So at the very least we have
+ * to give the user time to release that key.
+ */
+ if (++ctx->nattempts >= 4) {
+ cancel_askpass(ctx, "unable to grab keyboard after 5 seconds");
+ } else {
+ g_timeout_add(1000/8, try_grab_keyboard, ctx);
+ }
+ return false;
+}
+
+void realize(GtkWidget *widget, gpointer vctx)
+{
+ struct askpass_ctx *ctx = (struct askpass_ctx *)vctx;
+
+ gtk_grab_add(ctx->dialog);
+
+ /*
+ * Schedule the first attempt at the keyboard grab.
+ */
+ ctx->nattempts = 0;
+#if GTK_CHECK_VERSION(3,20,0)
+ ctx->seat = NULL;
+#elif GTK_CHECK_VERSION(3,0,0)
+ ctx->keyboard = NULL;
+#endif
+
+ g_idle_add(try_grab_keyboard, ctx);
+}
+
+static const char *gtk_askpass_setup(struct askpass_ctx *ctx,
+ const char *window_title,
+ const char *prompt_text)
+{
+ int i;
+ GtkBox *action_area;
+
+ ctx->passlen = 0;
+ ctx->passsize = 2048;
+ ctx->passphrase = snewn(ctx->passsize, char);
+
+ /*
+ * Create widgets.
+ */
+ ctx->dialog = our_dialog_new();
+ gtk_window_set_title(GTK_WINDOW(ctx->dialog), window_title);
+ gtk_window_set_position(GTK_WINDOW(ctx->dialog), GTK_WIN_POS_CENTER);
+ g_signal_connect(G_OBJECT(ctx->dialog), "delete-event",
+ G_CALLBACK(askpass_dialog_closed), ctx);
+ ctx->promptlabel = gtk_label_new(prompt_text);
+ align_label_left(GTK_LABEL(ctx->promptlabel));
+ gtk_widget_show(ctx->promptlabel);
+ gtk_label_set_line_wrap(GTK_LABEL(ctx->promptlabel), true);
+#if GTK_CHECK_VERSION(3,0,0)
+ gtk_label_set_width_chars(GTK_LABEL(ctx->promptlabel), 48);
+#endif
+ int margin = string_width("MM");
+#if GTK_CHECK_VERSION(3,12,0)
+ gtk_widget_set_margin_start(ctx->promptlabel, margin);
+ gtk_widget_set_margin_end(ctx->promptlabel, margin);
+#else
+ gtk_misc_set_padding(GTK_MISC(ctx->promptlabel), margin, 0);
+#endif
+ our_dialog_add_to_content_area(GTK_WINDOW(ctx->dialog),
+ ctx->promptlabel, true, true, 0);
+#if GTK_CHECK_VERSION(2,0,0)
+ ctx->imc = gtk_im_multicontext_new();
+#endif
+#ifndef DRAW_DEFAULT_CAIRO
+ {
+ gboolean success[2];
+ ctx->colmap = gdk_colormap_get_system();
+ ctx->cols[0].red = ctx->cols[0].green = ctx->cols[0].blue = 0xFFFF;
+ ctx->cols[1].red = ctx->cols[1].green = ctx->cols[1].blue = 0;
+ ctx->cols[2].red = ctx->cols[2].green = ctx->cols[2].blue = 0x8000;
+ gdk_colormap_alloc_colors(ctx->colmap, ctx->cols, 2,
+ false, true, success);
+ if (!success[0] || !success[1])
+ return "unable to allocate colours";
+ }
+#endif
+
+ action_area = our_dialog_make_action_hbox(GTK_WINDOW(ctx->dialog));
+
+ for (i = 0; i < N_DRAWING_AREAS; i++) {
+ ctx->drawingareas[i].area = gtk_drawing_area_new();
+#ifndef DRAW_DEFAULT_CAIRO
+ ctx->drawingareas[i].cols = ctx->cols;
+#endif
+ ctx->drawingareas[i].state = GREYED_OUT;
+ ctx->drawingareas[i].width = ctx->drawingareas[i].height = 0;
+ /* It would be nice to choose this size in some more
+ * context-sensitive way, like measuring the size of some
+ * piece of template text. */
+ gtk_widget_set_size_request(ctx->drawingareas[i].area, 32, 32);
+ gtk_box_pack_end(action_area, ctx->drawingareas[i].area,
+ true, true, 5);
+ g_signal_connect(G_OBJECT(ctx->drawingareas[i].area),
+ "configure_event",
+ G_CALLBACK(configure_area),
+ &ctx->drawingareas[i]);
+#if GTK_CHECK_VERSION(3,0,0)
+ g_signal_connect(G_OBJECT(ctx->drawingareas[i].area),
+ "draw",
+ G_CALLBACK(draw_area),
+ &ctx->drawingareas[i]);
+#else
+ g_signal_connect(G_OBJECT(ctx->drawingareas[i].area),
+ "expose_event",
+ G_CALLBACK(expose_area),
+ &ctx->drawingareas[i]);
+#endif
+
+#if GTK_CHECK_VERSION(3,0,0)
+ g_object_set(G_OBJECT(ctx->drawingareas[i].area),
+ "margin-bottom", 8, (const char *)NULL);
+#endif
+
+ gtk_widget_show(ctx->drawingareas[i].area);
+ }
+ ctx->active_area = -1;
+
+ /*
+ * Arrange to receive key events. We don't really need to worry
+ * from a UI perspective about which widget gets the events, as
+ * long as we know which it is so we can catch them. So we'll pick
+ * the prompt label at random, and we'll use gtk_grab_add to
+ * ensure key events go to it.
+ */
+ gtk_widget_set_sensitive(ctx->dialog, true);
+
+#if GTK_CHECK_VERSION(2,0,0)
+ gtk_window_set_keep_above(GTK_WINDOW(ctx->dialog), true);
+#endif
+
+ /*
+ * Wait for the key-receiving widget to actually be created, in
+ * order to call gtk_grab_add on it.
+ */
+ g_signal_connect(G_OBJECT(ctx->dialog), "realize",
+ G_CALLBACK(realize), ctx);
+
+ /*
+ * Show the window.
+ */
+ gtk_widget_show(ctx->dialog);
+
+ return NULL;
+}
+
+static void gtk_askpass_cleanup(struct askpass_ctx *ctx)
+{
+#if GTK_CHECK_VERSION(3,20,0)
+ if (ctx->seat)
+ gdk_seat_ungrab(ctx->seat);
+#elif GTK_CHECK_VERSION(3,0,0)
+ if (ctx->keyboard)
+ gdk_device_ungrab(ctx->keyboard, GDK_CURRENT_TIME);
+#else
+ gdk_keyboard_ungrab(GDK_CURRENT_TIME);
+#endif
+ gtk_grab_remove(ctx->promptlabel);
+
+ if (ctx->passphrase) {
+ assert(ctx->passlen < ctx->passsize);
+ ctx->passphrase[ctx->passlen] = '\0';
+ }
+
+ gtk_widget_destroy(ctx->dialog);
+}
+
+static bool setup_gtk(const char *display)
+{
+ static bool gtk_initialised = false;
+ int argc;
+ char *real_argv[3];
+ char **argv = real_argv;
+ bool ret;
+
+ if (gtk_initialised)
+ return true;
+
+ argc = 0;
+ argv[argc++] = dupstr("dummy");
+ argv[argc++] = dupprintf("--display=%s", display);
+ argv[argc] = NULL;
+ ret = gtk_init_check(&argc, &argv);
+ while (argc > 0)
+ sfree(argv[--argc]);
+
+ gtk_initialised = ret;
+ return ret;
+}
+
+const bool buildinfo_gtk_relevant = true;
+
+char *gtk_askpass_main(const char *display, const char *wintitle,
+ const char *prompt, bool *success)
+{
+ struct askpass_ctx ctx[1];
+ const char *err;
+
+ ctx->passphrase = NULL;
+ ctx->error_message = NULL;
+
+ /* In case gtk_init hasn't been called yet by the program */
+ if (!setup_gtk(display)) {
+ *success = false;
+ return dupstr("unable to initialise GTK");
+ }
+
+ if ((err = gtk_askpass_setup(ctx, wintitle, prompt)) != NULL) {
+ *success = false;
+ return dupprintf("%s", err);
+ }
+ setup_keypress_prng();
+ gtk_main();
+ cleanup_keypress_prng();
+ gtk_askpass_cleanup(ctx);
+
+ if (ctx->passphrase) {
+ *success = true;
+ return ctx->passphrase;
+ } else {
+ *success = false;
+ return ctx->error_message;
+ }
+}
+
+#ifdef TEST_ASKPASS
+void modalfatalbox(const char *p, ...)
+{
+ va_list ap;
+ fprintf(stderr, "FATAL ERROR: ");
+ va_start(ap, p);
+ vfprintf(stderr, p, ap);
+ va_end(ap);
+ fputc('\n', stderr);
+ exit(1);
+}
+
+int main(int argc, char **argv)
+{
+ bool success;
+ int exitcode;
+ char *ret;
+
+ gtk_init(&argc, &argv);
+
+ if (argc != 2) {
+ success = false;
+ ret = dupprintf("usage: %s <prompt text>", argv[0]);
+ } else {
+ ret = gtk_askpass_main(NULL, "Enter passphrase", argv[1], &success);
+ }
+
+ if (!success) {
+ fputs(ret, stderr);
+ fputc('\n', stderr);
+ exitcode = 1;
+ } else {
+ fputs(ret, stdout);
+ fputc('\n', stdout);
+ exitcode = 0;
+ }
+
+ smemclr(ret, strlen(ret));
+ return exitcode;
+}
+#endif