1 /*
2  * gui.c - stoken gtk+ interface
3  *
4  * Copyright 2012 Kevin Cernekee <cernekee@gmail.com>
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU Lesser General Public
8  * License as published by the Free Software Foundation; either
9  * version 2.1 of the License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14  * Lesser General Public License for more details.
15  *
16  * You should have received a copy of the GNU Lesser General Public
17  * License along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19  */
20 
21 #include "config.h"
22 
23 #include <stdio.h>
24 #include <stdlib.h>
25 #include <string.h>
26 #include <time.h>
27 #include <gtk/gtk.h>
28 
29 #include "common.h"
30 #include "securid.h"
31 
32 #define WINDOW_TITLE		"Software Token"
33 
34 #define EXP_WARN_DAYS		14
35 
36 #ifdef _WIN32
37 #undef UIDIR
38 #define UIDIR			"."
39 #define PIXMAP_DIR		"."
40 #else
41 #define PIXMAP_DIR		DATA_DIR "/pixmaps"
42 #endif
43 
44 static GtkWidget *tokencode_text, *next_tokencode_text, *progress_bar;
45 
46 static char tokencode_str[16];
47 static char next_tokencode_str[16];
48 
49 static int last_sec = -1;
50 static int token_sec;
51 static long time_adjustment;
52 
53 static int token_days_left;
54 static int token_interval;
55 static int token_uses_pin;
56 static int skipped_pin;
57 
delete_callback(GtkWidget * widget,GdkEvent * event,gpointer data)58 static gboolean delete_callback(GtkWidget *widget, GdkEvent *event,
59 	gpointer data)
60 {
61 	gtk_main_quit();
62 	return FALSE;
63 }
64 
copy_tokencode(gpointer user_data)65 static void copy_tokencode(gpointer user_data)
66 {
67 	GdkDisplay *disp = gdk_display_get_default();
68 	GtkClipboard *clip;
69 	char *str = user_data;
70 
71 	/* CLIPBOARD - Control-V in most applications */
72 	clip = gtk_clipboard_get_for_display(disp, GDK_SELECTION_CLIPBOARD);
73 	gtk_clipboard_set_text(clip, str, -1);
74 
75 	/* PRIMARY - middle-click in xterm */
76 	clip = gtk_clipboard_get_for_display(disp, GDK_SELECTION_PRIMARY);
77 	gtk_clipboard_set_text(clip, str, -1);
78 }
79 
clicked_to_clipboard(GtkButton * button,gpointer user_data)80 static void clicked_to_clipboard(GtkButton *button, gpointer user_data)
81 {
82 	copy_tokencode(user_data);
83 }
84 
press_to_clipboard(GtkWidget * widget,GdkEvent * event,gpointer user_data)85 static gboolean press_to_clipboard(GtkWidget *widget, GdkEvent *event,
86 	gpointer user_data)
87 {
88 	copy_tokencode(user_data);
89 	return TRUE;
90 }
91 
draw_progress_bar_callback(GtkWidget * widget,cairo_t * cr,gpointer data)92 static gboolean draw_progress_bar_callback(GtkWidget *widget, cairo_t *cr,
93 	gpointer data)
94 {
95 	guint width, height, boundary;
96 
97 	width = gtk_widget_get_allocated_width(widget);
98 	height = gtk_widget_get_allocated_height(widget);
99 
100 	boundary = width * token_sec / (token_interval - 1);
101 
102 	cairo_set_source_rgb(cr, 0.3, 0.4, 0.5);
103 	cairo_rectangle(cr, 0, 0, boundary, height);
104 	cairo_fill(cr);
105 
106 	cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
107 	cairo_rectangle(cr, boundary, 0, width - boundary, height);
108 	cairo_fill(cr);
109 
110 	return FALSE;
111 }
112 
adjusted_time(void)113 static time_t adjusted_time(void)
114 {
115 	return time(NULL) + time_adjustment;
116 }
117 
parse_opt_use_time(void)118 static void parse_opt_use_time(void)
119 {
120 	long new_time;
121 
122 	if (!opt_use_time)
123 		return;
124 	else if (sscanf(opt_use_time, "+%ld", &new_time) == 1)
125 		time_adjustment = new_time;
126 	else if (sscanf(opt_use_time, "-%ld", &new_time) == 1)
127 		time_adjustment = -new_time;
128 	else
129 		die("error: 'stoken-gui --use-time' must specify a +/- offset\n");
130 }
131 
update_tokencode(gpointer data)132 static gint update_tokencode(gpointer data)
133 {
134 	time_t now = adjusted_time();
135 	struct tm *tm;
136 	char str[128], *formatted;
137 
138 	tm = gmtime(&now);
139 	if ((tm->tm_sec >= 30 && last_sec < 30) ||
140 	    (tm->tm_sec < 30 && last_sec >= 30) ||
141 	    last_sec == -1) {
142 		last_sec = tm->tm_sec;
143 		securid_compute_tokencode(current_token, now, tokencode_str);
144 		securid_compute_tokencode(current_token, now + token_interval,
145 			next_tokencode_str);
146 	}
147 
148 	token_sec = token_interval - (tm->tm_sec % token_interval) - 1;
149 	gtk_widget_queue_draw(GTK_WIDGET(progress_bar));
150 
151 	formatted = stoken_format_tokencode(tokencode_str);
152 	if (!formatted)
153 		die("out of memory\n");
154 
155 	snprintf(str, sizeof(str),
156 		"<span size=\"xx-large\" weight=\"bold\">%s</span>",
157 		formatted);
158 	gtk_label_set_markup(GTK_LABEL(tokencode_text), str);
159 	free(formatted);
160 
161 	if (next_tokencode_text) {
162 		formatted = stoken_format_tokencode(next_tokencode_str);
163 		if (!formatted)
164 			die("out of memory\n");
165 		gtk_label_set_text(GTK_LABEL(next_tokencode_text), formatted);
166 		free(formatted);
167 	}
168 
169 	return TRUE;
170 }
171 
__error_dialog(GtkWindow * parent,const char * heading,const char * msg,int is_warning)172 static void __error_dialog(GtkWindow *parent, const char *heading,
173 	const char *msg, int is_warning)
174 {
175 	GtkWidget *dialog;
176 
177 	dialog = gtk_message_dialog_new(parent,
178 		parent ? GTK_DIALOG_MODAL | GTK_DIALOG_DESTROY_WITH_PARENT : 0,
179 		is_warning ? GTK_MESSAGE_WARNING : GTK_MESSAGE_ERROR,
180 		GTK_BUTTONS_OK, "%s", heading);
181 	gtk_message_dialog_format_secondary_text(GTK_MESSAGE_DIALOG(dialog),
182 		"%s", msg);
183 	gtk_window_set_title(GTK_WINDOW(dialog), WINDOW_TITLE);
184 	gtk_dialog_run(GTK_DIALOG(dialog));
185 	gtk_widget_destroy(dialog);
186 	if (!is_warning)
187 		exit(1);
188 }
189 
error_dialog(const char * heading,const char * msg)190 static void error_dialog(const char *heading, const char *msg)
191 {
192 	return __error_dialog(NULL, heading, msg, 0);
193 }
194 
warning_dialog(GtkWidget * parent,const char * heading,const char * msg)195 static void warning_dialog(GtkWidget *parent, const char *heading,
196 	const char *msg)
197 {
198 	return __error_dialog(GTK_WINDOW(parent), heading, msg, 1);
199 }
200 
create_app_window_common(GtkBuilder * builder)201 static GtkWidget *create_app_window_common(GtkBuilder *builder)
202 {
203 	GtkWidget *widget;
204 
205 	progress_bar = GTK_WIDGET(
206 		gtk_builder_get_object(builder, "progress_bar"));
207 	g_signal_connect(progress_bar, "draw",
208 		G_CALLBACK(draw_progress_bar_callback), NULL);
209 
210 	tokencode_text = GTK_WIDGET(
211 		gtk_builder_get_object(builder, "tokencode_text"));
212 
213 	widget = GTK_WIDGET(gtk_builder_get_object(builder, "app_window"));
214 	g_signal_connect(widget, "delete-event", G_CALLBACK(delete_callback),
215 			 NULL);
216 	return widget;
217 }
218 
set_red_label(GtkWidget * widget,const char * text)219 static void set_red_label(GtkWidget *widget, const char *text)
220 {
221 	char tmp[BUFLEN];
222 
223 	snprintf(tmp, BUFLEN,
224 		 "<span weight=\"bold\" foreground=\"red\">%s</span>", text);
225 	gtk_label_set_markup(GTK_LABEL(widget), tmp);
226 }
227 
format_exp_date(GtkWidget * widget)228 static void format_exp_date(GtkWidget *widget)
229 {
230 	time_t exp = securid_unix_exp_date(current_token);
231 	char tmp[BUFLEN];
232 
233 	/* FIXME: localization */
234 	strftime(tmp, BUFLEN, "%Y-%m-%d", gmtime(&exp));
235 
236 	if (token_days_left < EXP_WARN_DAYS)
237 		set_red_label(widget, tmp);
238 	else
239 		gtk_label_set_text(GTK_LABEL(widget), tmp);
240 }
241 
242 /* gtk_builder_new_from_file() requires libgtk >= 3.10 */
__gtk_builder_new_from_file(const gchar * filename)243 static GtkBuilder *__gtk_builder_new_from_file(const gchar *filename)
244 {
245 	GtkBuilder *builder;
246 
247 	builder = gtk_builder_new();
248 	if (gtk_builder_add_from_file(builder, filename, NULL) == 0)
249 		die("can't import '%s'\n", filename);
250 	return builder;
251 }
252 
create_app_window(void)253 static GtkWidget *create_app_window(void)
254 {
255 	GtkBuilder *builder;
256 	GtkWidget *widget;
257 
258 	builder = __gtk_builder_new_from_file(UIDIR "/tokencode-detail.ui");
259 
260 	/* static token info */
261 	widget = GTK_WIDGET(gtk_builder_get_object(builder, "token_sn_text"));
262 	gtk_label_set_text(GTK_LABEL(widget), current_token->serial);
263 
264 	widget = GTK_WIDGET(gtk_builder_get_object(builder, "exp_date_text"));
265 	format_exp_date(widget);
266 
267 	widget = GTK_WIDGET(gtk_builder_get_object(builder, "using_pin_text"));
268 	if (!token_uses_pin)
269 		gtk_label_set_text(GTK_LABEL(widget), "Not required");
270 	else if (skipped_pin)
271 		set_red_label(widget, "No");
272 	else
273 		gtk_label_set_text(GTK_LABEL(widget), "Yes");
274 
275 	/* buttons */
276 
277 	widget = GTK_WIDGET(gtk_builder_get_object(builder, "copy_button"));
278 	g_signal_connect(widget, "clicked", G_CALLBACK(clicked_to_clipboard),
279 		&tokencode_str);
280 
281 	/* next tokencode */
282 
283 	next_tokencode_text = GTK_WIDGET(
284 		gtk_builder_get_object(builder, "next_tokencode_text"));
285 
286 	widget = GTK_WIDGET(gtk_builder_get_object(builder,
287 		"next_tokencode_eventbox"));
288 	g_signal_connect(widget, "button-press-event",
289 		G_CALLBACK(press_to_clipboard), &next_tokencode_str);
290 
291 	return create_app_window_common(builder);
292 }
293 
create_small_app_window(void)294 static GtkWidget *create_small_app_window(void)
295 {
296 	GtkBuilder *builder;
297 	GtkWidget *widget;
298 
299 	builder = __gtk_builder_new_from_file(UIDIR "/tokencode-small.ui");
300 
301 	widget = GTK_WIDGET(gtk_builder_get_object(builder, "event_box"));
302 	g_signal_connect(widget, "button-press-event",
303 		G_CALLBACK(press_to_clipboard), &tokencode_str);
304 
305 	return create_app_window_common(builder);
306 }
307 
do_password_dialog(const char * ui_file)308 static char *do_password_dialog(const char *ui_file)
309 {
310 	GtkBuilder *builder;
311 	GtkWidget *widget, *dialog;
312 	gint resp;
313 	char *ret = NULL;
314 
315 	builder = __gtk_builder_new_from_file(ui_file);
316 	dialog = GTK_WIDGET(gtk_builder_get_object(builder, "dialog_window"));
317 	gtk_widget_show_all(dialog);
318 	resp = gtk_dialog_run(GTK_DIALOG(dialog));
319 
320 	if (resp == GTK_RESPONSE_OK) {
321 		widget = GTK_WIDGET(gtk_builder_get_object(builder, "password"));
322 		ret = strdup(gtk_entry_get_text(GTK_ENTRY(widget)));
323 	}
324 
325 	gtk_widget_destroy(dialog);
326 	return ret;
327 }
328 
request_credentials(struct securid_token * t)329 static int request_credentials(struct securid_token *t)
330 {
331 	int rc, pass_required = 0, pin_required = 0;
332 
333 	if (securid_pass_required(t)) {
334 		pass_required = 1;
335 		if (opt_password) {
336 			rc = securid_decrypt_seed(t, opt_password, NULL);
337 			if (rc == ERR_DECRYPT_FAILED)
338 				warn("warning: --password parameter is incorrect\n");
339 			else if (rc != ERR_NONE)
340 				error_dialog("Token decrypt error",
341 					stoken_errstr[rc]);
342 			else
343 				pass_required = 0;
344 		}
345 	} else {
346 		rc = securid_decrypt_seed(t, opt_password, NULL);
347 		if (rc != ERR_NONE)
348 			error_dialog("Token decrypt error", stoken_errstr[rc]);
349 	}
350 
351 	while (pass_required) {
352 		const char *pass =
353 			do_password_dialog(UIDIR "/password-dialog.ui");
354 		if (!pass)
355 			return ERR_MISSING_PASSWORD;
356 		rc = securid_decrypt_seed(t, pass, NULL);
357 		if (rc == ERR_NONE) {
358 			if (t->enc_pin_str) {
359 				rc = securid_decrypt_pin(t->enc_pin_str,
360 							 pass, t->pin);
361 				if (rc != ERR_NONE)
362 					error_dialog("PIN decrypt error",
363 						     stoken_errstr[rc]);
364 			}
365 
366 			pass_required = 0;
367 		} else if (rc == ERR_DECRYPT_FAILED)
368 			warning_dialog(NULL, "Bad password",
369 				"Please enter the correct password for this seed.");
370 		else
371 			error_dialog("Token decrypt error", stoken_errstr[rc]);
372 	}
373 
374 	if (securid_pin_required(t)) {
375 		pin_required = 1;
376 		if (opt_pin) {
377 			if (securid_pin_format_ok(opt_pin) == ERR_NONE) {
378 				xstrncpy(t->pin, opt_pin, MAX_PIN + 1);
379 				pin_required = 0;
380 			} else
381 				warn("warning: --pin argument is invalid\n");
382 		} else if (strlen(t->pin) || t->enc_pin_str)
383 			pin_required = 0;
384 	}
385 
386 	while (pin_required) {
387 		const char *pin =
388 			do_password_dialog(UIDIR "/pin-dialog.ui");
389 		if (!pin) {
390 			skipped_pin = 1;
391 			xstrncpy(t->pin, "0000", MAX_PIN + 1);
392 			break;
393 		}
394 		if (securid_pin_format_ok(pin) != ERR_NONE) {
395 			warning_dialog(NULL, "Bad PIN",
396 				"Please enter 4-8 digits, or click Skip for no PIN.");
397 		} else {
398 			xstrncpy(t->pin, pin, MAX_PIN + 1);
399 			break;
400 		}
401 	}
402 
403 	return ERR_NONE;
404 }
405 
main(int argc,char ** argv)406 int main(int argc, char **argv)
407 {
408 	GtkWidget *window;
409 	char *cmd;
410 
411 	gtk_init(&argc, &argv);
412 	gtk_window_set_default_icon_from_file(
413 		PIXMAP_DIR "/stoken-gui.png", NULL);
414 
415 	cmd = parse_cmdline(argc, argv, IS_GUI);
416 
417 	/* check for a couple of error conditions */
418 
419 	if (common_init(cmd))
420 		error_dialog("Application error",
421 			"Unable to initialize crypto library.");
422 
423 	if (!current_token)
424 		error_dialog("Missing token",
425 			"Please use 'stoken import' to add a new seed.");
426 
427 	if (securid_devid_required(current_token))
428 		error_dialog("Unsupported token",
429 			"Please use 'stoken' to handle tokens encrypted with a device ID.");
430 
431 	/* check for token expiration */
432 	parse_opt_use_time();
433 	token_days_left = securid_check_exp(current_token, adjusted_time());
434 
435 	if (!opt_force && !opt_small) {
436 		if (token_days_left < 0)
437 			error_dialog("Token expired",
438 				"Please obtain a new token from your administrator.");
439 
440 		if (token_days_left < EXP_WARN_DAYS) {
441 			char msg[BUFLEN];
442 
443 			sprintf(msg, "This token will expire in %d day%s.",
444 				token_days_left,
445 				token_days_left == 1 ? "" : "s");
446 			warning_dialog(NULL, "Expiration warning", msg);
447 		}
448 	}
449 
450 	/* request password / PIN, if missing */
451 	if (request_credentials(current_token) != ERR_NONE)
452 		return 1;
453 
454 	token_interval = securid_token_interval(current_token);
455 	token_uses_pin = securid_pin_required(current_token);
456 
457 	window = opt_small ? create_small_app_window() : create_app_window();
458 
459 	update_tokencode(NULL);
460 	gtk_widget_show_all(window);
461 
462 	g_timeout_add(250, update_tokencode, NULL);
463 	gtk_main();
464 
465 	return 0;
466 }
467