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