1
0
mirror of https://git.tartarus.org/simon/putty.git synced 2025-01-09 01:18:00 +00:00
putty-source/unix/askpass.c
Simon Tatham f39c51f9a7 Rename most of the platform source files.
This gets rid of all those annoying 'win', 'ux' and 'gtk' prefixes
which made filenames annoying to type and to tab-complete. Also, as
with my other recent renaming sprees, I've taken the opportunity to
expand and clarify some of the names so that they're not such cryptic
abbreviations.
2021-04-26 18:00:01 +01:00

663 lines
20 KiB
C

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