2002-04-25 Marcus Brinkmann <marcus@g10code.de>
[pinentry.git] / curses / pinentry-curses.c
1 /* pinentry-curses.c - A secure curses dialog for PIN entry.
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
37 #include "pinentry.h"
38
39 #define STRING_OK "<OK>"
40 #define STRING_CANCEL "<Cancel>"
41
42 int init_screen;
43
44 typedef enum
45   {
46     DIALOG_POS_NONE,
47     DIALOG_POS_PIN,
48     DIALOG_POS_OK,
49     DIALOG_POS_CANCEL
50   }
51 dialog_pos_t;
52
53 struct dialog
54 {
55   dialog_pos_t pos;
56   int pin_y;
57   int pin_x;
58   /* Width of the PIN field.  */
59   int pin_size;
60   /* Cursor location in PIN field.  */
61   int pin_loc;
62   char *pin;
63   int pin_max;
64   /* Length of PIN.  */
65   int pin_len;
66
67   int ok_y;
68   int ok_x;
69   char *ok;
70   int cancel_y;
71   int cancel_x;
72   char *cancel;
73 };
74 typedef struct dialog *dialog_t;
75
76 \f
77 char *
78 convert_utf8_string (char *lc_ctype, char *text)
79 {
80   char *old_ctype;
81   char *target_encoding;
82   iconv_t cd;
83   char *input = text;
84   size_t input_len = strlen (text) + 1;
85   char *output;
86   size_t output_len;
87   char *output_buf;
88   size_t processed;
89
90   /* If no locale setting could be determined, simply copy the
91      string.  */
92   if (!lc_ctype)
93     return strdup (text);
94
95   old_ctype = strdup (setlocale (LC_CTYPE, NULL));
96   if (!old_ctype)
97     return NULL;
98   setlocale (LC_CTYPE, lc_ctype);
99   target_encoding = nl_langinfo (CODESET);
100   setlocale (LC_CTYPE, old_ctype);
101   free (old_ctype);
102
103   /* This is overkill, but simplifies the iconv invocation greatly.  */
104   output_len = input_len * MB_LEN_MAX;
105   output_buf = output = malloc (output_len);
106   if (!output)
107     return NULL;
108
109   cd = iconv_open (target_encoding, "UTF-8");
110   if (cd == (iconv_t) -1)
111     {
112       free (output);
113       return NULL;
114     }
115   processed = iconv (cd, &input, &input_len, &output, &output_len);
116   iconv_close (cd);
117   if (processed == (size_t) -1 || input_len)
118     {
119       free (output_buf);
120       return NULL;
121     }
122   return output_buf;
123 }
124
125
126 static int
127 dialog_create (pinentry_t pinentry, dialog_t dialog)
128 {
129   int err = 0;
130   int size_y;
131   int size_x;
132   int y;
133   int x;
134   int ypos;
135   int xpos;
136   int description_x = 0;
137   char *description = NULL;
138   char *error = NULL;
139   char *prompt = NULL;
140   char *ok = NULL;
141   char *cancel = NULL;
142
143   if (pinentry->description)
144     {
145       description = convert_utf8_string (pinentry->lc_ctype,
146                                          pinentry->description);
147       if (!description)
148         {
149           err = 1;
150           goto out;
151         }
152     }
153   if (pinentry->error)
154     {
155       error = convert_utf8_string (pinentry->lc_ctype,
156                                    pinentry->error);
157       if (!error)
158         {
159           err = 1;
160           goto out;
161         }
162     }
163   if (pinentry->prompt)
164     {
165       prompt = convert_utf8_string (pinentry->lc_ctype,
166                                     pinentry->prompt);
167       if (!prompt)
168         {
169           err = 1;
170           goto out;
171         }
172     }
173   if (pinentry->ok)
174     {
175       int len = strlen (pinentry->ok);
176       ok = malloc (len + 3);
177       if (!ok)
178         {
179           err = 1;
180           goto out;
181         }
182       ok[0] = '<';
183       memcpy (&ok[1], pinentry->ok, len);
184       ok[len + 1] = '>';
185       ok[len + 2] = '\0';
186     }
187   if (pinentry->cancel)
188     {
189       int len = strlen (pinentry->cancel);
190       cancel = malloc (len + 3);
191       if (!cancel)
192         {
193           err = 1;
194           goto out;
195         }
196       cancel[0] = '<';
197       memcpy (&cancel[1], pinentry->cancel, len);
198       cancel[len + 1] = '>';
199       cancel[len + 2] = '\0';
200     }
201
202   dialog->ok = convert_utf8_string (pinentry->lc_ctype,
203                                     ok ? ok : STRING_OK);
204   dialog->cancel = convert_utf8_string (pinentry->lc_ctype,
205                                         cancel ? cancel : STRING_CANCEL);
206   if (!dialog->ok || !dialog->cancel)
207     {
208       err = 1;
209       goto out;
210     }
211   getmaxyx (stdscr, size_y, size_x);
212
213   /* Check if all required lines fit on the screen.  */
214   y = 1;                /* Top frame.  */
215   if (description)
216     {
217       char *p = description;
218       int desc_x = 0;
219
220       while (*p)
221         {
222           if (*(p++) == '\n')
223             {
224               if (desc_x > description_x)
225                 description_x = desc_x;
226               y++;
227               desc_x = 0;
228             }
229           else
230             desc_x++;
231         }
232       if (desc_x > description_x)
233         description_x = desc_x;
234       y += 2;           /* Description.  */
235     }
236       
237   if (pinentry->pin)
238     {
239       if (error)
240         y += 2;         /* Error message.  */
241       y += 2;           /* Pin entry field.  */
242     }
243   y += 2;               /* OK/Cancel and bottom frame.  */
244   
245   if (y > size_y)
246     {
247       err = 1;
248       goto out;
249     }
250
251   /* Check if all required columns fit on the screen.  */
252   x = 0;
253   if (description)
254     {
255       int new_x = description_x;
256       if (new_x > size_x - 4)
257         new_x = size_x - 4;
258       if (new_x > x)
259         x = new_x;
260     }
261   if (pinentry->pin)
262     {
263 #define MIN_PINENTRY_LENGTH 40
264       int new_x;
265
266       if (error)
267         {
268           new_x = strlen (error);
269           if (new_x > size_x - 4)
270             new_x = size_x - 4;
271           if (new_x > x)
272             x = new_x;
273         }
274
275       new_x = MIN_PINENTRY_LENGTH;
276       if (prompt)
277         new_x += strlen (prompt) + 1;   /* One space after prompt.  */
278       if (new_x > size_x - 4)
279         new_x = size_x - 4;
280       if (new_x > x)
281         x = new_x;
282     }
283   /* We position the buttons after the first and second third of the
284      width.  Account for rounding.  */
285   if (x < 2 * strlen (dialog->ok))
286     x = 2 * strlen (dialog->ok);
287   if (x < 2 * strlen (dialog->cancel))
288     x = 2 * strlen (dialog->cancel);
289
290   /* Add the frame.  */
291   x += 4;
292
293   if (x > size_x)
294     {
295       err = 1;
296       goto out;
297     }
298
299   dialog->pos = DIALOG_POS_NONE;
300   dialog->pin = pinentry->pin;
301   dialog->pin_max = pinentry->pin_len;
302   dialog->pin_loc = 0;
303   dialog->pin_len = 0;
304   ypos = (size_y - y) / 2;
305   xpos = (size_x - x) / 2;
306   move (ypos, xpos);
307   addch (ACS_ULCORNER);
308   hline (0, x - 2);
309   move (ypos, xpos + x - 1);
310   addch (ACS_URCORNER);
311   move (ypos + 1, xpos + x - 1);
312   vline (0, y - 2);
313   move (ypos + y - 1, xpos);
314   addch (ACS_LLCORNER);
315   hline (0, x - 2);
316   move (ypos + y - 1, xpos + x - 1);
317   addch (ACS_LRCORNER);
318   ypos++;
319   if (description)
320     {
321       char *p = description;
322       int i = 0;
323
324       while (*p)
325         {
326           move (ypos, xpos);
327           addch (ACS_VLINE);
328           addch (' ');
329           while (*p && *p != '\n')
330             if (i < x - 4)
331               {
332                 i++;
333                 addch ((unsigned char) *(p++));
334               }
335           if (*p == '\n')
336             p++;
337           i = 0;
338           ypos++;
339         }
340       move (ypos, xpos);
341       addch (ACS_VLINE);
342       ypos++;
343     }
344   if (pinentry->pin)
345     {
346       int i;
347
348       if (error)
349         {
350           char *p = error;
351           i = strlen (error);
352           move (ypos, xpos);
353           addch (ACS_VLINE);
354           addch (' ');
355           if (i > x - 4)
356             i = x - 4;
357           if (has_colors () && COLOR_PAIRS >= 1)
358             attron (COLOR_PAIR(1) | A_BOLD);
359           else
360             standout ();
361           while (i-- > 0)
362             addch ((unsigned char) *(p++));
363           if (has_colors () && COLOR_PAIRS >= 1)
364             attroff (COLOR_PAIR(1) | A_BOLD);
365           else
366             standend ();
367           ypos++;
368           move (ypos, xpos);
369           addch (ACS_VLINE);
370           ypos++;
371         }
372
373       move (ypos, xpos);
374       addch (ACS_VLINE);
375       addch (' ');
376
377       dialog->pin_y = ypos;
378       dialog->pin_x = xpos + 2;
379       dialog->pin_size = x - 4;
380       if (prompt)
381         {
382           char *p = prompt;
383           i = strlen (prompt);
384           if (i > x - 4 - MIN_PINENTRY_LENGTH)
385             i = x - 4 - MIN_PINENTRY_LENGTH;
386           dialog->pin_x += i + 1;
387           dialog->pin_size -= i + 1;
388           while (i-- > 0)
389             addch ((unsigned char) *(p++));
390           addch (' ');
391         }
392       for (i = 0; i < dialog->pin_size; i++)
393         addch ('_');
394       ypos++;
395       move (ypos, xpos);
396       addch (ACS_VLINE);
397       ypos++;
398     }
399   move (ypos, xpos);
400   addch (ACS_VLINE);
401   dialog->ok_y = ypos;
402   /* Calculating the left edge of the left button, rounding down.  */
403   dialog->ok_x = xpos + 2 + ((x - 4) / 2 - strlen (dialog->ok)) / 2;
404   move (dialog->ok_y, dialog->ok_x);
405   addstr (dialog->ok);
406   dialog->cancel_y = ypos;
407   /* Calculating the left edge of the right button, rounding up.  */
408   dialog->cancel_x = xpos + x - 2 - ((x - 4) / 2 + strlen (dialog->cancel)) / 2;
409   move (dialog->cancel_y, dialog->cancel_x);
410   addstr (dialog->cancel);
411
412  out:
413   if (ok)
414     free (ok);
415   if (cancel)
416     free (cancel);
417   if (description)
418     free (description);
419   if (error)
420     free (error);
421   if (prompt)
422     free (prompt);
423   return err;
424 }
425
426 \f
427 static void
428 set_cursor_state (int on)
429 {
430   static int normal_state = -1;
431   static int on_last;
432
433   if (normal_state < 0 && !on)
434     {
435       normal_state = curs_set (0);
436       on_last = on;
437     }
438   else if (on != on_last)
439     {
440       curs_set (on ? normal_state : 0);
441       on_last = on;
442     }
443 }
444
445 int
446 dialog_switch_pos (dialog_t diag, dialog_pos_t new_pos)
447 {
448   if (new_pos != diag->pos)
449     {
450       switch (diag->pos)
451         {
452         case DIALOG_POS_OK:
453           move (diag->ok_y, diag->ok_x);
454           addstr (diag->ok);
455           break;
456         case DIALOG_POS_CANCEL:
457           move (diag->cancel_y, diag->cancel_x);
458           addstr (diag->cancel);
459           break;
460         default:
461           break;
462         }
463       diag->pos = new_pos;
464       switch (diag->pos)
465         {
466         case DIALOG_POS_PIN:
467           move (diag->pin_y, diag->pin_x + diag->pin_loc);
468           set_cursor_state (1);
469           break;
470         case DIALOG_POS_OK:
471           move (diag->ok_y, diag->ok_x);
472           standout ();
473           addstr (diag->ok);
474           standend ();
475           set_cursor_state (0);
476           break;
477         case DIALOG_POS_CANCEL:
478           move (diag->cancel_y, diag->cancel_x);
479           standout ();
480           addstr (diag->cancel);
481           standend ();
482           set_cursor_state (0);
483           break;
484         case DIALOG_POS_NONE:
485           set_cursor_state (0);
486           break;
487         }
488       refresh ();
489     }
490 }
491
492 /* XXX Assume that field width is at least > 5.  */
493 static void
494 dialog_input (dialog_t diag, int chr)
495 {
496   int old_loc = diag->pin_loc;
497   assert (diag->pin);
498   assert (diag->pos == DIALOG_POS_PIN);
499
500   switch (chr)
501     {
502     case KEY_BACKSPACE:
503       if (diag->pin_len > 0)
504         {
505           diag->pin_len--;
506           diag->pin_loc--;
507           if (diag->pin_loc == 0 && diag->pin_len > 0)
508             {
509               diag->pin_loc = diag->pin_size - 5;
510               if (diag->pin_loc > diag->pin_len)
511                 diag->pin_loc = diag->pin_len;
512             }
513         }
514       break;
515
516     default:
517       if (chr > 0 && chr < 256 && diag->pin_len < diag->pin_max)
518         {
519           diag->pin[diag->pin_len] = (char) chr;
520           diag->pin_len++;
521           diag->pin_loc++;
522           if (diag->pin_loc == diag->pin_size && diag->pin_len < diag->pin_max)
523             {
524               diag->pin_loc = 5;
525               if (diag->pin_loc < diag->pin_size - (diag->pin_max + 1 - diag->pin_len))
526                 diag->pin_loc = diag->pin_size - (diag->pin_max + 1 - diag->pin_len);
527             }
528         }
529       break;
530     }
531
532   if (old_loc < diag->pin_loc)
533     {
534       move (diag->pin_y, diag->pin_x + old_loc);
535       while (old_loc++ < diag->pin_loc)
536         addch ('*');
537     }
538   else if (old_loc > diag->pin_loc)
539     {
540       move (diag->pin_y, diag->pin_x + diag->pin_loc);
541       while (old_loc-- > diag->pin_loc)
542         addch ('_');
543     }
544   move (diag->pin_y, diag->pin_x + diag->pin_loc);
545 }
546
547 static int
548 dialog_run (pinentry_t pinentry, const char *tty_name, const char *tty_type)
549 {
550   struct dialog diag;
551   FILE *ttyfi = 0;
552   FILE *ttyfo = 0;
553   SCREEN *screen = 0;
554   int done = 0;
555
556   /* Open the desired terminal if necessary.  */
557   if (tty_name)
558     {
559       ttyfi = fopen (tty_name, "r");
560       if (ttyfi < 0)
561         return -1;
562       ttyfo = fopen (tty_name, "w");
563       if (ttyfo < 0)
564         {
565           int err = errno;
566           fclose (ttyfi);
567           errno = err;
568           return -1;
569         }
570       screen = newterm (tty_type, ttyfo, ttyfi);
571       set_term (screen);
572     }
573   else
574     {
575       if (!init_screen)
576         {
577           init_screen = 1;
578           initscr ();
579         }
580       else
581         clear ();
582     }
583   
584   keypad (stdscr, TRUE); /* Enable keyboard mapping.  */
585   nonl ();              /* Tell curses not to do NL->CR/NL on output.  */
586   cbreak ();            /* Take input chars one at a time, no wait for \n.  */
587   noecho ();            /* Don't echo input - in color.  */
588   refresh ();
589
590   if (has_colors ())
591     {
592       start_color ();
593
594       if (COLOR_PAIRS >= 1)
595         init_pair (1, COLOR_RED, COLOR_BLACK);
596     }
597
598   /* XXX */
599   if (dialog_create (pinentry, &diag))
600     return -2;
601   dialog_switch_pos (&diag, diag.pin ? DIALOG_POS_PIN : DIALOG_POS_OK);
602
603   do
604     {
605       int c;
606
607       c = getch ();     /* Refresh, accept single keystroke of input.  */
608
609       switch (c)
610         {
611         case KEY_LEFT:
612         case KEY_UP:
613           switch (diag.pos)
614             {
615             case DIALOG_POS_OK:
616               if (diag.pin)
617                 dialog_switch_pos (&diag, DIALOG_POS_PIN);
618               break;
619             case DIALOG_POS_CANCEL:
620               dialog_switch_pos (&diag, DIALOG_POS_OK);
621               break;
622             default:
623               break;
624             }
625           break;
626
627         case KEY_RIGHT:
628         case KEY_DOWN:
629           switch (diag.pos)
630             {
631             case DIALOG_POS_PIN:
632               dialog_switch_pos (&diag, DIALOG_POS_OK);
633               break;
634             case DIALOG_POS_OK:
635               dialog_switch_pos (&diag, DIALOG_POS_CANCEL);
636               break;
637             default:
638               break;
639             }
640           break;
641
642         case '\t':
643           switch (diag.pos)
644             {
645             case DIALOG_POS_PIN:
646               dialog_switch_pos (&diag, DIALOG_POS_OK);
647               break;
648             case DIALOG_POS_OK:
649               dialog_switch_pos (&diag, DIALOG_POS_CANCEL);
650               break;
651             case DIALOG_POS_CANCEL:
652               if (diag.pin)
653                 dialog_switch_pos (&diag, DIALOG_POS_PIN);
654               else
655                 dialog_switch_pos (&diag, DIALOG_POS_OK);
656               break;
657             default:
658               break;
659             }
660           break;
661   
662         case '\e':
663           done = -2;
664           break;
665
666         case '\r':
667           switch (diag.pos)
668             {
669             case DIALOG_POS_PIN:
670             case DIALOG_POS_OK:
671               done = 1;
672               break;
673             case DIALOG_POS_CANCEL:
674               done = -2;
675               break;
676             }
677           break;
678
679         default:
680           if (diag.pos == DIALOG_POS_PIN)
681             dialog_input (&diag, c);
682         }
683     }
684   while (!done);
685
686   set_cursor_state (1);
687   endwin ();
688   if (screen)
689     delscreen (screen);
690
691   if (ttyfi)
692     fclose (ttyfi);
693   if (ttyfo)
694     fclose (ttyfo);
695   /* XXX Factor out into dialog_release or something.  */
696   free (diag.ok);
697   free (diag.cancel);
698   return diag.pin ? (done < 0 ? -1 : diag.pin_len) : (done < 0 ? 0 : 1);
699 }
700
701 static int
702 dialog_cmd_handler (pinentry_t pinentry)
703 {
704   return dialog_run (pinentry, pinentry->ttyname, pinentry->ttytype);
705 }
706
707 pinentry_cmd_handler_t pinentry_cmd_handler = dialog_cmd_handler;
708
709 \f
710 int 
711 main (int argc, char *argv[])
712 {
713   pinentry_init ();
714
715   /* Consumes all arguments.  */
716   if (pinentry_parse_opts (argc, argv))
717     {
718       printf ("pinentry-curses " VERSION "\n");
719       exit (EXIT_SUCCESS);
720     }
721
722   if (pinentry_loop ())
723     return 1;
724
725   return 0;
726 }