2007-05-10 Marcus Brinkmann <marcus@g10code.de>
[pinentry.git] / pinentry / pinentry-curses.c
1 /* pinentry-curses.c - A secure curses dialog for PIN entry, library version
2    Copyright (C) 2002 g10 Code GmbH
3    
4    This file is part of PINENTRY.
5    
6    PINENTRY is free software; you can redistribute it and/or modify it
7    under the terms of the GNU General Public License as published by
8    the Free Software Foundation; either version 2 of the License, or
9    (at your option) any later version.
10  
11    PINENTRY is distributed in the hope that it will be useful, but
12    WITHOUT ANY WARRANTY; without even the implied warranty of
13    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14    General Public License for more details.
15  
16    You should have received a copy of the GNU General Public License
17    along with this program; if not, write to the Free Software
18    Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
19    02111-1307, USA  */
20
21 #ifdef HAVE_CONFIG_H
22 #include <config.h>
23 #endif
24 #include <assert.h>
25 #include <curses.h>
26 #include <signal.h>
27 #include <fcntl.h>
28 #include <unistd.h>
29 #include <stdlib.h>
30 #include <locale.h>
31 #include <iconv.h>
32 #include <langinfo.h>
33 #include <limits.h>
34 #include <string.h>
35 #include <errno.h>
36 #include <time.h>
37 #include <sys/types.h>
38 #include <sys/stat.h>
39 #ifdef HAVE_UTIME_H
40 #include <utime.h>
41 #endif /*HAVE_UTIME_H*/
42
43 #include <memory.h>
44
45 #include "pinentry.h"
46
47 #define STRING_OK "<OK>"
48 #define STRING_CANCEL "<Cancel>"
49
50 #define USE_COLORS              (has_colors () && COLOR_PAIRS >= 2)
51 static short pinentry_color[] = { -1, -1, COLOR_BLACK, COLOR_RED,
52                                   COLOR_GREEN, COLOR_YELLOW, COLOR_BLUE,
53                                   COLOR_MAGENTA, COLOR_CYAN, COLOR_WHITE };
54 static int init_screen;
55
56 typedef enum
57   {
58     DIALOG_POS_NONE,
59     DIALOG_POS_PIN,
60     DIALOG_POS_OK,
61     DIALOG_POS_CANCEL
62   }
63 dialog_pos_t;
64
65 struct dialog
66 {
67   dialog_pos_t pos;
68   int pin_y;
69   int pin_x;
70   /* Width of the PIN field.  */
71   int pin_size;
72   /* Cursor location in PIN field.  */
73   int pin_loc;
74   char *pin;
75   int pin_max;
76   /* Length of PIN.  */
77   int pin_len;
78
79   int ok_y;
80   int ok_x;
81   char *ok;
82   int cancel_y;
83   int cancel_x;
84   char *cancel;
85 };
86 typedef struct dialog *dialog_t;
87
88 \f
89 /* Return the next line up to MAXLEN columns wide in START and LEN.
90    The first invocation should have 0 as *LEN.  If the line ends with
91    a \n, it is a normal line that will be continued.  If it is a '\0'
92    the end of the text is reached after this line.  In all other cases
93    there is a forced line break.  A full line is returned and will be
94    continued in the next line.  */
95 static void
96 collect_line (int maxlen, char **start_p, int *len_p)
97 {
98   int last_space = 0;
99   int len = *len_p;
100   char *end;
101
102   /* Skip to next line.  */
103   *start_p += len;
104   /* Skip leading space.  */
105   while (**start_p == ' ')
106     (*start_p)++;
107
108   end = *start_p;
109   len = 0;
110
111   while (len < maxlen - 1 && *end && *end != '\n')
112     {
113       len++;
114       end++;
115       if (*end == ' ')
116         last_space = len;
117     }
118
119   if (*end && *end != '\n' && last_space != 0)
120     {
121       /* We reached the end of the available space, but still have
122          characters to go in this line.  We can break the line into
123          two parts at a space.  */
124       len = last_space;
125       (*start_p)[len] = '\n';
126     }
127   *len_p = len + 1;
128 }
129
130
131 static int
132 dialog_create (pinentry_t pinentry, dialog_t dialog)
133 {
134   int err = 0;
135   int size_y;
136   int size_x;
137   int y;
138   int x;
139   int ypos;
140   int xpos;
141   int description_x = 0;
142   int error_x = 0;
143   char *description = NULL;
144   char *error = NULL;
145   char *prompt = NULL;
146
147 #define COPY_OUT(what)                                                  \
148   do                                                                    \
149     if (pinentry->what)                                                 \
150       {                                                                 \
151         what = pinentry_utf8_to_local (pinentry->lc_ctype,              \
152                                        pinentry->what);                 \
153         if (!what)                                                      \
154           {                                                             \
155             err = 1;                                                    \
156             goto out;                                                   \
157           }                                                             \
158       }                                                                 \
159   while (0)
160     
161   COPY_OUT (description);
162   COPY_OUT (error);
163   COPY_OUT (prompt);
164
165 #define MAKE_BUTTON(which,default)                                      \
166   do                                                                    \
167     {                                                                   \
168       char *new = NULL;                                                 \
169       if (pinentry->which)                                              \
170         {                                                               \
171           int len = strlen (pinentry->which);                           \
172           new = malloc (len + 3);                                       \
173           if (!new)                                                     \
174             {                                                           \
175               err = 1;                                                  \
176               goto out;                                                 \
177             }                                                           \
178           new[0] = '<';                                                 \
179           memcpy (&new[1], pinentry->which, len);                       \
180           new[len + 1] = '>';                                           \
181           new[len + 2] = '\0';                                          \
182         }                                                               \
183       dialog->which = pinentry_utf8_to_local (pinentry->lc_ctype,       \
184                                               new ? new : default);     \
185       if (!dialog->which)                                               \
186         {                                                               \
187           err = 1;                                                      \
188           goto out;                                                     \
189         }                                                               \
190     }                                                                   \
191   while (0)
192
193   MAKE_BUTTON (ok, STRING_OK);
194   if (!pinentry->one_button)
195     MAKE_BUTTON (cancel, STRING_CANCEL);
196   else
197     dialog->cancel = NULL;
198
199   getmaxyx (stdscr, size_y, size_x);
200
201   /* Check if all required lines fit on the screen.  */
202   y = 1;                /* Top frame.  */
203   if (description)
204     {
205       char *start = description;
206       int len = 0;
207
208       do
209         {
210           collect_line (size_x - 4, &start, &len);
211           if (len > description_x)
212             description_x = len;
213           y++;
214         }
215       while (start[len - 1]);
216       y++;
217     }
218       
219   if (pinentry->pin)
220     {
221       if (error)
222         {
223           char *p = error;
224           int err_x = 0;
225
226           while (*p)
227             {
228               if (*(p++) == '\n')
229                 {
230                   if (err_x > error_x)
231                     error_x = err_x;
232                   y++;
233                   err_x = 0;
234                 }
235               else
236                 err_x++;
237             }
238           if (err_x > error_x)
239             error_x = err_x;
240           y += 2;       /* Error message.  */
241         }
242       y += 2;           /* Pin entry field.  */
243     }
244   y += 2;               /* OK/Cancel and bottom frame.  */
245   
246   if (y > size_y)
247     {
248       err = 1;
249       goto out;
250     }
251
252   /* Check if all required columns fit on the screen.  */
253   x = 0;
254   if (description)
255     {
256       int new_x = description_x;
257       if (new_x > size_x - 4)
258         new_x = size_x - 4;
259       if (new_x > x)
260         x = new_x;
261     }
262   if (pinentry->pin)
263     {
264 #define MIN_PINENTRY_LENGTH 40
265       int new_x;
266
267       if (error)
268         {
269           new_x = error_x;
270           if (new_x > size_x - 4)
271             new_x = size_x - 4;
272           if (new_x > x)
273             x = new_x;
274         }
275
276       new_x = MIN_PINENTRY_LENGTH;
277       if (prompt)
278         new_x += strlen (prompt) + 1;   /* One space after prompt.  */
279       if (new_x > size_x - 4)
280         new_x = size_x - 4;
281       if (new_x > x)
282         x = new_x;
283     }
284   /* We position the buttons after the first and second third of the
285      width.  Account for rounding.  */
286   if (x < 2 * strlen (dialog->ok))
287     x = 2 * strlen (dialog->ok);
288   if (dialog->cancel)
289     if (x < 2 * strlen (dialog->cancel))
290       x = 2 * strlen (dialog->cancel);
291
292   /* Add the frame.  */
293   x += 4;
294
295   if (x > size_x)
296     {
297       err = 1;
298       goto out;
299     }
300
301   dialog->pos = DIALOG_POS_NONE;
302   dialog->pin = pinentry->pin;
303   dialog->pin_max = pinentry->pin_len;
304   dialog->pin_loc = 0;
305   dialog->pin_len = 0;
306   ypos = (size_y - y) / 2;
307   xpos = (size_x - x) / 2;
308   move (ypos, xpos);
309   addch (ACS_ULCORNER);
310   hline (0, x - 2);
311   move (ypos, xpos + x - 1);
312   addch (ACS_URCORNER);
313   move (ypos + 1, xpos + x - 1);
314   vline (0, y - 2);
315   move (ypos + y - 1, xpos);
316   addch (ACS_LLCORNER);
317   hline (0, x - 2);
318   move (ypos + y - 1, xpos + x - 1);
319   addch (ACS_LRCORNER);
320   ypos++;
321   if (description)
322     {
323       char *start = description;
324       int len = 0;
325
326       do
327         {
328           int i;
329
330           move (ypos, xpos);
331           addch (ACS_VLINE);
332           addch (' ');
333           collect_line (size_x - 4, &start, &len);
334           for (i = 0; i < len - 1; i++)
335             addch ((unsigned char) start[i]);
336           if (start[len - 1] && start[len - 1] != '\n')
337             addch ((unsigned char) start[len - 1]);
338           ypos++;
339         }
340       while (start[len - 1]);
341       move (ypos, xpos);
342       addch (ACS_VLINE);
343       ypos++;
344     }
345   if (pinentry->pin)
346     {
347       int i;
348
349       if (error)
350         {
351           char *p = error;
352           i = 0;
353
354           while (*p)
355             {
356               move (ypos, xpos);
357               addch (ACS_VLINE);
358               addch (' ');
359               if (USE_COLORS && pinentry->color_so != PINENTRY_COLOR_NONE)
360                 {
361                   attroff (COLOR_PAIR (1) | (pinentry->color_fg_bright ? A_BOLD : 0));
362                   attron (COLOR_PAIR (2) | (pinentry->color_so_bright ? A_BOLD : 0));
363                 }
364               else
365                 standout ();
366               for (;*p && *p != '\n'; p++)
367                 if (i < x - 4)
368                   {
369                     i++;
370                     addch ((unsigned char) *p);
371                   }
372               if (USE_COLORS && pinentry->color_so != PINENTRY_COLOR_NONE)
373                 {
374                   attroff (COLOR_PAIR (2) | (pinentry->color_so_bright ? A_BOLD : 0));
375                   attron (COLOR_PAIR (1) | (pinentry->color_fg_bright ? A_BOLD : 0));
376                 }
377               else
378                 standend ();
379               if (*p == '\n')
380                 p++;
381               i = 0;
382               ypos++;
383             }
384           move (ypos, xpos);
385           addch (ACS_VLINE);
386           ypos++;
387         }
388
389       move (ypos, xpos);
390       addch (ACS_VLINE);
391       addch (' ');
392
393       dialog->pin_y = ypos;
394       dialog->pin_x = xpos + 2;
395       dialog->pin_size = x - 4;
396       if (prompt)
397         {
398           char *p = prompt;
399           i = strlen (prompt);
400           if (i > x - 4 - MIN_PINENTRY_LENGTH)
401             i = x - 4 - MIN_PINENTRY_LENGTH;
402           dialog->pin_x += i + 1;
403           dialog->pin_size -= i + 1;
404           while (i-- > 0)
405             addch ((unsigned char) *(p++));
406           addch (' ');
407         }
408       for (i = 0; i < dialog->pin_size; i++)
409         addch ('_');
410       ypos++;
411       move (ypos, xpos);
412       addch (ACS_VLINE);
413       ypos++;
414     }
415   move (ypos, xpos);
416   addch (ACS_VLINE);
417
418   if (dialog->cancel)
419     {
420       dialog->ok_y = ypos;
421       /* Calculating the left edge of the left button, rounding down.  */
422       dialog->ok_x = xpos + 2 + ((x - 4) / 2 - strlen (dialog->ok)) / 2;
423       move (dialog->ok_y, dialog->ok_x);
424       addstr (dialog->ok);
425
426       dialog->cancel_y = ypos;
427       /* Calculating the left edge of the right button, rounding up.  */
428       dialog->cancel_x = xpos + x - 2 - ((x - 4) / 2 + strlen (dialog->cancel)) / 2;
429       move (dialog->cancel_y, dialog->cancel_x);
430       addstr (dialog->cancel);
431     }
432   else
433     {
434       dialog->ok_y = ypos;
435       /* Calculating the left edge of the OK button, rounding down.  */
436       dialog->ok_x = xpos + x / 2 - strlen (dialog->ok) / 2;
437       move (dialog->ok_y, dialog->ok_x);
438       addstr (dialog->ok);
439     }
440
441  out:
442   if (description)
443     free (description);
444   if (error)
445     free (error);
446   if (prompt)
447     free (prompt);
448   return err;
449 }
450
451 \f
452 static void
453 set_cursor_state (int on)
454 {
455   static int normal_state = -1;
456   static int on_last;
457
458   if (normal_state < 0 && !on)
459     {
460       normal_state = curs_set (0);
461       on_last = on;
462     }
463   else if (on != on_last)
464     {
465       curs_set (on ? normal_state : 0);
466       on_last = on;
467     }
468 }
469
470 static int
471 dialog_switch_pos (dialog_t diag, dialog_pos_t new_pos)
472 {
473   if (new_pos != diag->pos)
474     {
475       switch (diag->pos)
476         {
477         case DIALOG_POS_OK:
478           move (diag->ok_y, diag->ok_x);
479           addstr (diag->ok);
480           break;
481         case DIALOG_POS_CANCEL:
482           if (diag->cancel)
483             {
484               move (diag->cancel_y, diag->cancel_x);
485               addstr (diag->cancel);
486             }
487           break;
488         default:
489           break;
490         }
491       diag->pos = new_pos;
492       switch (diag->pos)
493         {
494         case DIALOG_POS_PIN:
495           move (diag->pin_y, diag->pin_x + diag->pin_loc);
496           set_cursor_state (1);
497           break;
498         case DIALOG_POS_OK:
499           set_cursor_state (0);
500           move (diag->ok_y, diag->ok_x);
501           standout ();
502           addstr (diag->ok);
503           standend ();
504           move (diag->ok_y, diag->ok_x);
505           break;
506         case DIALOG_POS_CANCEL:
507           if (diag->cancel)
508             {
509               set_cursor_state (0);
510               move (diag->cancel_y, diag->cancel_x);
511               standout ();
512               addstr (diag->cancel);
513               standend ();
514               move (diag->cancel_y, diag->cancel_x);
515             }
516           break;
517         case DIALOG_POS_NONE:
518           set_cursor_state (0);
519           break;
520         }
521       refresh ();
522     }
523   return 0;
524 }
525
526 /* XXX Assume that field width is at least > 5.  */
527 static void
528 dialog_input (dialog_t diag, int chr)
529 {
530   int old_loc = diag->pin_loc;
531   assert (diag->pin);
532   assert (diag->pos == DIALOG_POS_PIN);
533
534   switch (chr)
535     {
536     case KEY_BACKSPACE:
537       if (diag->pin_len > 0)
538         {
539           diag->pin_len--;
540           diag->pin_loc--;
541           if (diag->pin_loc == 0 && diag->pin_len > 0)
542             {
543               diag->pin_loc = diag->pin_size - 5;
544               if (diag->pin_loc > diag->pin_len)
545                 diag->pin_loc = diag->pin_len;
546             }
547         }
548       break;
549
550     default:
551       if (chr > 0 && chr < 256 && diag->pin_len < diag->pin_max)
552         {
553           diag->pin[diag->pin_len] = (char) chr;
554           diag->pin_len++;
555           diag->pin_loc++;
556           if (diag->pin_loc == diag->pin_size && diag->pin_len < diag->pin_max)
557             {
558               diag->pin_loc = 5;
559               if (diag->pin_loc < diag->pin_size - (diag->pin_max + 1 - diag->pin_len))
560                 diag->pin_loc = diag->pin_size - (diag->pin_max + 1 - diag->pin_len);
561             }
562         }
563       break;
564     }
565
566   if (old_loc < diag->pin_loc)
567     {
568       move (diag->pin_y, diag->pin_x + old_loc);
569       while (old_loc++ < diag->pin_loc)
570         addch ('*');
571     }
572   else if (old_loc > diag->pin_loc)
573     {
574       move (diag->pin_y, diag->pin_x + diag->pin_loc);
575       while (old_loc-- > diag->pin_loc)
576         addch ('_');
577     }
578   move (diag->pin_y, diag->pin_x + diag->pin_loc);
579 }
580
581 static int
582 dialog_run (pinentry_t pinentry, const char *tty_name, const char *tty_type)
583 {
584   struct dialog diag;
585   FILE *ttyfi = NULL;
586   FILE *ttyfo = NULL;
587   SCREEN *screen = 0;
588   int done = 0;
589   char *pin_utf8;
590
591   /* Open the desired terminal if necessary.  */
592   if (tty_name)
593     {
594       ttyfi = fopen (tty_name, "r");
595       if (!ttyfi)
596         return -1;
597       ttyfo = fopen (tty_name, "w");
598       if (!ttyfo)
599         {
600           int err = errno;
601           fclose (ttyfi);
602           errno = err;
603           return -1;
604         }
605       screen = newterm (tty_type, ttyfo, ttyfi);
606       set_term (screen);
607     }
608   else
609     {
610       if (!init_screen)
611         {
612           init_screen = 1;
613           initscr ();
614         }
615       else
616         clear ();
617     }
618   
619   keypad (stdscr, TRUE); /* Enable keyboard mapping.  */
620   nonl ();              /* Tell curses not to do NL->CR/NL on output.  */
621   cbreak ();            /* Take input chars one at a time, no wait for \n.  */
622   noecho ();            /* Don't echo input - in color.  */
623
624   if (has_colors ())
625     {
626       start_color ();
627       use_default_colors ();
628
629       if (pinentry->color_so == PINENTRY_COLOR_DEFAULT)
630         {
631           pinentry->color_so = PINENTRY_COLOR_RED;
632           pinentry->color_so_bright = 1;
633         }
634       if (COLOR_PAIRS >= 2)
635         {
636           init_pair (1, pinentry_color[pinentry->color_fg],
637                      pinentry_color[pinentry->color_bg]);
638           init_pair (2, pinentry_color[pinentry->color_so],
639                      pinentry_color[pinentry->color_bg]);
640
641           bkgd (COLOR_PAIR (1));
642           attron (COLOR_PAIR (1) | (pinentry->color_fg_bright ? A_BOLD : 0));
643         }
644     }
645   refresh ();
646
647   /* XXX */
648   if (dialog_create (pinentry, &diag))
649     return -2;
650   dialog_switch_pos (&diag, diag.pin ? DIALOG_POS_PIN : DIALOG_POS_OK);
651
652   do
653     {
654       int c;
655
656       c = getch ();     /* Refresh, accept single keystroke of input.  */
657
658       switch (c)
659         {
660         case KEY_LEFT:
661         case KEY_UP:
662           switch (diag.pos)
663             {
664             case DIALOG_POS_OK:
665               if (diag.pin)
666                 dialog_switch_pos (&diag, DIALOG_POS_PIN);
667               break;
668             case DIALOG_POS_CANCEL:
669               dialog_switch_pos (&diag, DIALOG_POS_OK);
670               break;
671             default:
672               break;
673             }
674           break;
675
676         case KEY_RIGHT:
677         case KEY_DOWN:
678           switch (diag.pos)
679             {
680             case DIALOG_POS_PIN:
681               dialog_switch_pos (&diag, DIALOG_POS_OK);
682               break;
683             case DIALOG_POS_OK:
684               dialog_switch_pos (&diag, DIALOG_POS_CANCEL);
685               break;
686             default:
687               break;
688             }
689           break;
690
691         case '\t':
692           switch (diag.pos)
693             {
694             case DIALOG_POS_PIN:
695               dialog_switch_pos (&diag, DIALOG_POS_OK);
696               break;
697             case DIALOG_POS_OK:
698               dialog_switch_pos (&diag, DIALOG_POS_CANCEL);
699               break;
700             case DIALOG_POS_CANCEL:
701               if (diag.pin)
702                 dialog_switch_pos (&diag, DIALOG_POS_PIN);
703               else
704                 dialog_switch_pos (&diag, DIALOG_POS_OK);
705               break;
706             default:
707               break;
708             }
709           break;
710   
711         case '\e':
712           done = -2;
713           break;
714
715         case '\r':
716           switch (diag.pos)
717             {
718             case DIALOG_POS_PIN:
719             case DIALOG_POS_OK:
720               done = 1;
721               break;
722             case DIALOG_POS_CANCEL:
723               done = -2;
724               break;
725             case DIALOG_POS_NONE:
726               break;
727             }
728           break;
729
730         default:
731           if (diag.pos == DIALOG_POS_PIN)
732             dialog_input (&diag, c);
733         }
734     }
735   while (!done);
736
737   set_cursor_state (1);
738   endwin ();
739   if (screen)
740     delscreen (screen);
741
742   if (ttyfi)
743     fclose (ttyfi);
744   if (ttyfo)
745     fclose (ttyfo);
746   /* XXX Factor out into dialog_release or something.  */
747   free (diag.ok);
748   free (diag.cancel);
749
750   if (pinentry->pin)
751     {
752       pinentry->locale_err = 1;
753       pin_utf8 = pinentry_local_to_utf8 (pinentry->lc_ctype, pinentry->pin, 1);
754       if (pin_utf8)
755         {
756           pinentry_setbufferlen (pinentry, strlen (pin_utf8) + 1);
757           if (pinentry->pin)
758             strcpy (pinentry->pin, pin_utf8);
759           secmem_free (pin_utf8);
760           pinentry->locale_err = 0;
761         }
762     }
763
764   return diag.pin ? (done < 0 ? -1 : diag.pin_len) : (done < 0 ? 0 : 1);
765 }
766
767
768 /* If a touch has been registered, touch that file.  */
769 static void
770 do_touch_file (pinentry_t pinentry)
771 {
772 #ifdef HAVE_UTIME_H
773   struct stat st;
774   time_t tim;
775
776   if (!pinentry->touch_file || !*pinentry->touch_file)
777     return;
778
779   if (stat (pinentry->touch_file, &st))
780     return; /* Oops.  */
781
782   /* Make sure that we actually update the mtime.  */
783   while ( (tim = time (NULL)) == st.st_mtime )
784     sleep (1);
785
786   /* Update but ignore errors as we can't do anything in that case.
787      Printing error messages may even clubber the display further. */
788   utime (pinentry->touch_file, NULL);
789 #endif /*HAVE_UTIME_H*/
790 }
791
792
793 int
794 curses_cmd_handler (pinentry_t pinentry)
795 {
796   int rc;
797
798   rc = dialog_run (pinentry, pinentry->ttyname, pinentry->ttytype);
799   do_touch_file (pinentry);
800   return rc;
801 }