diff --git a/Recipe b/Recipe index bd622c09..31d5eb27 100644 --- a/Recipe +++ b/Recipe @@ -303,9 +303,9 @@ puttygen : [U] cmdgen sshrsag sshdssg sshprime sshdes sshbn sshmd5 version pscp : [U] pscp uxsftp uxcons UXSSH BE_SSH SFTP wildcard UXMISC psftp : [U] psftp uxsftp uxcons UXSSH BE_SSH SFTP wildcard UXMISC -pageant : [U] uxpgnt uxagentc pageant sshrsa sshpubk sshdes sshbn sshmd5 +pageant : [X] uxpgnt uxagentc pageant sshrsa sshpubk sshdes sshbn sshmd5 + version tree234 misc sshaes sshsha sshdss sshsh256 sshsh512 sshecc - + conf uxsignal nocproxy nogss be_none x11fwd ux_x11 uxcons + + conf uxsignal nocproxy nogss be_none x11fwd ux_x11 uxcons gtkask + UXMISC LIBS PuTTY : [MX] osxmain OSXTERM OSXMISC CHARSET U_BE_ALL NONSSH UXSSH diff --git a/unix/gtkask.c b/unix/gtkask.c new file mode 100644 index 00000000..7d0f13cf --- /dev/null +++ b/unix/gtkask.c @@ -0,0 +1,384 @@ +/* + * GTK implementation of a GUI password/passphrase prompt. + */ + +#include +#include +#include +#include +#include + +#include "misc.h" + +#define N_DRAWING_AREAS 3 + +struct drawing_area_ctx { + GtkWidget *area; + GdkColor *cols; + int width, height, current; +}; + +struct askpass_ctx { + GtkWidget *dialog, *promptlabel; + struct drawing_area_ctx drawingareas[N_DRAWING_AREAS]; + int active_area; + GtkIMContext *imc; + GdkColormap *colmap; + GdkColor cols[2]; + char *passphrase; + int passlen, passsize; +}; + +static void visually_acknowledge_keypress(struct askpass_ctx *ctx) +{ + int new_active; + new_active = rand() % (N_DRAWING_AREAS - 1); + if (new_active >= ctx->active_area) + new_active++; + ctx->drawingareas[ctx->active_area].current = 0; + gtk_widget_queue_draw(ctx->drawingareas[ctx->active_area].area); + ctx->drawingareas[new_active].current = 1; + 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 gint key_event(GtkWidget *widget, GdkEventKey *event, gpointer data) +{ + struct askpass_ctx *ctx = (struct askpass_ctx *)data; + if (event->keyval == GDK_Return) { + gtk_main_quit(); + } else if (event->keyval == GDK_Escape) { + smemclr(ctx->passphrase, ctx->passsize); + ctx->passphrase = NULL; + gtk_main_quit(); + } else { + if (gtk_im_context_filter_keypress(ctx->imc, event)) + return TRUE; + + 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_BackSpace) { + /* Backspace. Delete one character. */ + if (ctx->passlen > 0) + ctx->passlen -= last_char_len(ctx); + visually_acknowledge_keypress(ctx); + } + } + } + return TRUE; +} + +static void input_method_commit_event(GtkIMContext *imc, gchar *str, + gpointer data) +{ + struct askpass_ctx *ctx = (struct askpass_ctx *)data; + 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 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; +} + +static gint expose_area(GtkWidget *widget, GdkEventExpose *event, + gpointer data) +{ + struct drawing_area_ctx *ctx = (struct drawing_area_ctx *)data; + + GdkGC *gc = gdk_gc_new(ctx->area->window); + gdk_gc_set_foreground(gc, &ctx->cols[ctx->current]); + gdk_draw_rectangle(widget->window, gc, TRUE, + 0, 0, ctx->width, ctx->height); + gdk_gc_unref(gc); + return TRUE; +} + +static int try_grab_keyboard(struct askpass_ctx *ctx) +{ + int ret = gdk_keyboard_grab(ctx->dialog->window, FALSE, GDK_CURRENT_TIME); + return ret == GDK_GRAB_SUCCESS; +} + +typedef int (try_grab_fn_t)(struct askpass_ctx *ctx); + +static int repeatedly_try_grab(struct askpass_ctx *ctx, try_grab_fn_t fn) +{ + /* + * Repeatedly try to grab some aspect of the X server. 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. + */ + const useconds_t ms_limit = 5*1000000; /* try for 5 seconds */ + const useconds_t ms_step = 1000000/8; /* at 1/8 second intervals */ + useconds_t ms; + + for (ms = 0; ms < ms_limit; ms++) { + if (fn(ctx)) + return TRUE; + usleep(ms_step); + ms += ms_step; + } + return FALSE; +} + +static const char *gtk_askpass_setup(struct askpass_ctx *ctx, + const char *window_title, + const char *prompt_text) +{ + int i; + gboolean success[2]; + + ctx->passlen = 0; + ctx->passsize = 2048; + ctx->passphrase = snewn(ctx->passsize, char); + + /* + * Create widgets. + */ + ctx->dialog = gtk_dialog_new(); + gtk_window_set_title(GTK_WINDOW(ctx->dialog), window_title); + ctx->promptlabel = gtk_label_new(prompt_text); + gtk_label_set_line_wrap(GTK_LABEL(ctx->promptlabel), TRUE); + gtk_container_add(GTK_CONTAINER(gtk_dialog_get_content_area + (GTK_DIALOG(ctx->dialog))), + ctx->promptlabel); + ctx->imc = gtk_im_multicontext_new(); + 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; + gdk_colormap_alloc_colors(ctx->colmap, ctx->cols, 2, + FALSE, TRUE, success); + if (!success[0] | !success[1]) + return "unable to allocate colours"; + for (i = 0; i < N_DRAWING_AREAS; i++) { + ctx->drawingareas[i].area = gtk_drawing_area_new(); + ctx->drawingareas[i].cols = ctx->cols; + ctx->drawingareas[i].current = 0; + 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_container_add(GTK_CONTAINER(gtk_dialog_get_action_area + (GTK_DIALOG(ctx->dialog))), + ctx->drawingareas[i].area); + gtk_signal_connect(GTK_OBJECT(ctx->drawingareas[i].area), + "configure_event", + GTK_SIGNAL_FUNC(configure_area), + &ctx->drawingareas[i]); + gtk_signal_connect(GTK_OBJECT(ctx->drawingareas[i].area), + "expose_event", + GTK_SIGNAL_FUNC(expose_area), + &ctx->drawingareas[i]); + gtk_widget_show(ctx->drawingareas[i].area); + } + ctx->active_area = rand() % N_DRAWING_AREAS; + ctx->drawingareas[ctx->active_area].current = 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->promptlabel, TRUE); + gtk_window_set_keep_above(GTK_WINDOW(ctx->dialog), TRUE); + + /* + * Actually show the window, and wait for it to be shown. + */ + gtk_widget_show_now(ctx->dialog); + + /* + * Now that the window is displayed, make it grab the input focus. + */ + gtk_grab_add(ctx->promptlabel); + if (!repeatedly_try_grab(ctx, try_grab_keyboard)) + return "unable to grab keyboard"; + + /* + * And now that we've got the keyboard grab, connect up our + * keyboard handlers, and display the prompt. + */ + g_signal_connect(G_OBJECT(ctx->imc), "commit", + G_CALLBACK(input_method_commit_event), ctx); + gtk_signal_connect(GTK_OBJECT(ctx->promptlabel), "key_press_event", + GTK_SIGNAL_FUNC(key_event), ctx); + gtk_signal_connect(GTK_OBJECT(ctx->promptlabel), "key_release_event", + GTK_SIGNAL_FUNC(key_event), ctx); + gtk_im_context_set_client_window(ctx->imc, ctx->dialog->window); + gtk_widget_show(ctx->promptlabel); + + return NULL; +} + +static void gtk_askpass_cleanup(struct askpass_ctx *ctx) +{ + gdk_keyboard_ungrab(GDK_CURRENT_TIME); + gtk_grab_remove(ctx->promptlabel); + + if (ctx->passphrase) { + assert(ctx->passlen < ctx->passsize); + ctx->passphrase[ctx->passlen] = '\0'; + } + + gtk_widget_destroy(ctx->dialog); +} + +static int setup_gtk(const char *display) +{ + static int gtk_initialised = FALSE; + int argc; + char *real_argv[3]; + char **argv = real_argv; + int 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; +} + +char *gtk_askpass_main(const char *display, const char *wintitle, + const char *prompt, int *success) +{ + struct askpass_ctx actx, *ctx = &actx; + const char *err; + + /* 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); + } + gtk_main(); + gtk_askpass_cleanup(ctx); + + if (ctx->passphrase) { + *success = TRUE; + return ctx->passphrase; + } else { + *success = FALSE; + return dupstr("passphrase input cancelled"); + } +} + +#ifdef TEST_ASKPASS +void modalfatalbox(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) +{ + int success, exitcode; + char *ret; + + gtk_init(&argc, &argv); + + if (argc != 2) { + success = FALSE; + ret = dupprintf("usage: %s ", argv[0]); + } else { + srand(time(NULL)); + ret = gtk_askpass_main(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 diff --git a/unix/uxpgnt.c b/unix/uxpgnt.c index a641a2e5..27de94d4 100644 --- a/unix/uxpgnt.c +++ b/unix/uxpgnt.c @@ -292,30 +292,50 @@ const char *display = NULL; static char *askpass(const char *comment) { - prompts_t *p = new_prompts(NULL); - int ret; + if (have_controlling_tty()) { + int ret; + prompts_t *p = new_prompts(NULL); + p->to_server = FALSE; + p->name = dupstr("Pageant passphrase prompt"); + add_prompt(p, + dupprintf("Enter passphrase to load key '%s': ", comment), + FALSE); + ret = console_get_userpass_input(p, NULL, 0); + assert(ret >= 0); - /* - * FIXME: if we don't have a terminal, and have to do this by X11, - * there's a big missing piece. - */ + if (!ret) { + perror("pageant: unable to read passphrase"); + free_prompts(p); + return NULL; + } else { + char *passphrase = dupstr(p->prompts[0]->result); + free_prompts(p); + return passphrase; + } + } else if (display) { + char *prompt, *passphrase; + int success; - p->to_server = FALSE; - p->name = dupstr("Pageant passphrase prompt"); - add_prompt(p, - dupprintf("Enter passphrase to load key '%s': ", comment), - FALSE); - ret = console_get_userpass_input(p, NULL, 0); - assert(ret >= 0); + /* in gtkask.c */ + char *gtk_askpass_main(const char *display, const char *wintitle, + const char *prompt, int *success); - if (!ret) { - perror("pageant: unable to read passphrase"); - free_prompts(p); - return NULL; - } else { - char *passphrase = dupstr(p->prompts[0]->result); - free_prompts(p); + prompt = dupprintf("Enter passphrase to load key '%s': ", comment); + passphrase = gtk_askpass_main(display, + "Pageant passphrase prompt", + prompt, &success); + sfree(prompt); + if (!success) { + /* return value is error message */ + fprintf(stderr, "%s\n", passphrase); + sfree(passphrase); + passphrase = NULL; + } return passphrase; + } else { + fprintf(stderr, "no way to read a passphrase without tty or " + "X display\n"); + return NULL; } } @@ -697,8 +717,6 @@ void run_agent(void) NULL }; - if (!display) - display = getenv("DISPLAY"); if (!display) { fprintf(stderr, "pageant: no DISPLAY for -X mode\n"); exit(1); @@ -982,6 +1000,12 @@ int main(int argc, char **argv) sk_init(); uxsel_init(); + if (!display) { + display = getenv("DISPLAY"); + if (display && !*display) + display = NULL; + } + /* * Now distinguish our two main running modes. Either we're * actually starting up an agent, in which case we should have a