mirror of
https://git.tartarus.org/simon/putty.git
synced 2025-01-25 01:02:24 +00:00
Unix Pageant: implement GUI passphrase prompting.
I've written my own analogue of OpenSSH's ssh-askpass. At the moment, it's contained inside Pageant proper, though it could easily be compiled into a standalone binary as well or instead. Unlike OpenSSH's version, I don't use a GTK edit box; instead I just process key events myself and append them to a buffer. The big advantage of doing this is that I can arrange for ^W and ^U to function as they do in terminal line editing, i.e. delete a word or delete the whole line. ^W in particular is really valuable when typing a multiple-word passphrase unseen. If you feel yourself making the kind of typo in which you're not sure if you pressed six keys or just five, you can hit ^W and restart just that word, without either having to go right back to the beginning or carry on and see if you feel lucky. A delete-word function would of course be an information leak in even an obscured edit box (displaying a blob per character), so instead I give a visual acknowledgment of keypresses by a more ad-hoc means: I display three lights in the box, and every meaningful keypress turns off the currently active one and instead turns on a randomly selected one of the others. (So the lit light doesn't even indicate _mod 3_ how many keys have been pressed.)
This commit is contained in:
parent
460c45dd23
commit
75b7ba26d3
4
Recipe
4
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
|
||||
|
384
unix/gtkask.c
Normal file
384
unix/gtkask.c
Normal file
@ -0,0 +1,384 @@
|
||||
/*
|
||||
* GTK implementation of a GUI password/passphrase prompt.
|
||||
*/
|
||||
|
||||
#include <assert.h>
|
||||
#include <time.h>
|
||||
#include <stdlib.h>
|
||||
#include <gtk/gtk.h>
|
||||
#include <gdk/gdkkeysyms.h>
|
||||
|
||||
#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 <prompt text>", 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
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user