/* * GTK implementation of a GUI password/passphrase prompt. */ #include #include #include #include #include #include #if !GTK_CHECK_VERSION(3,0,0) #include #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 */ strbuf *passphrase; /* if we finish with one */ #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 size_t last_char_start(struct askpass_ctx *ctx) { /* * GTK always encodes in UTF-8, so we can do this in a fixed way. */ assert(ctx->passphrase->len > 0); size_t i = ctx->passphrase->len - 1; while ((unsigned)(ctx->passphrase->u[i] - 0x80) < 0x40) { if (i == 0) break; i--; } return i; } static void add_text_to_passphrase(struct askpass_ctx *ctx, gchar *str) { put_datapl(ctx->passphrase, ptrlen_from_asciz(str)); visually_acknowledge_keypress(ctx); } static void cancel_askpass(struct askpass_ctx *ctx, const char *msg) { strbuf_free(ctx->passphrase); 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 */ strbuf_clear(ctx->passphrase); 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->passphrase->len > 0) { char deleted, prior; size_t newlen = last_char_start(ctx); deleted = ctx->passphrase->s[newlen]; strbuf_shrink_to(ctx->passphrase, newlen); prior = (ctx->passphrase->len == 0 ? ' ' : ctx->passphrase->s[ctx->passphrase->len-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->passphrase->len > 0) strbuf_shrink_to(ctx->passphrase, last_char_start(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->passphrase = strbuf_new_nm(); /* * 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); 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 strbuf_to_str(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 ", 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