123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638 |
- /*
- * 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 */
- 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 <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
|