1 /* $NetBSD: openpam_ttyconv.c,v 1.5 2023/06/30 21:46:21 christos Exp $ */
2
3 /*-
4 * Copyright (c) 2002-2003 Networks Associates Technology, Inc.
5 * Copyright (c) 2004-2014 Dag-Erling Smørgrav
6 * All rights reserved.
7 *
8 * This software was developed for the FreeBSD Project by ThinkSec AS and
9 * Network Associates Laboratories, the Security Research Division of
10 * Network Associates, Inc. under DARPA/SPAWAR contract N66001-01-C-8035
11 * ("CBOSS"), as part of the DARPA CHATS research program.
12 *
13 * Redistribution and use in source and binary forms, with or without
14 * modification, are permitted provided that the following conditions
15 * are met:
16 * 1. Redistributions of source code must retain the above copyright
17 * notice, this list of conditions and the following disclaimer.
18 * 2. Redistributions in binary form must reproduce the above copyright
19 * notice, this list of conditions and the following disclaimer in the
20 * documentation and/or other materials provided with the distribution.
21 * 3. The name of the author may not be used to endorse or promote
22 * products derived from this software without specific prior written
23 * permission.
24 *
25 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
26 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
27 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
28 * ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
29 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
30 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
31 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
32 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
33 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
34 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
35 * SUCH DAMAGE.
36 */
37
38 #ifdef HAVE_CONFIG_H
39 # include "config.h"
40 #endif
41
42 #include <sys/cdefs.h>
43 __RCSID("$NetBSD: openpam_ttyconv.c,v 1.5 2023/06/30 21:46:21 christos Exp $");
44
45 #include <sys/types.h>
46 #include <sys/poll.h>
47 #include <sys/time.h>
48
49 #include <errno.h>
50 #include <fcntl.h>
51 #include <signal.h>
52 #include <stdio.h>
53 #include <stdlib.h>
54 #include <string.h>
55 #include <termios.h>
56 #include <unistd.h>
57 #include <paths.h>
58
59 #include <security/pam_appl.h>
60
61 #include "openpam_impl.h"
62 #include "openpam_strlset.h"
63
64 int openpam_ttyconv_timeout = 0;
65
66 #ifdef GETPASS_ECHO
67 static int
prompt(const char * message,char * response,int echo)68 prompt(const char *message, char *response, int echo)
69 {
70 char *rv;
71
72 rv = getpassfd(message, response, PAM_MAX_RESP_SIZE, NULL,
73 GETPASS_NEED_TTY | GETPASS_FAIL_EOF | GETPASS_NO_SIGNAL |
74 (echo ? GETPASS_ECHO : 0), openpam_ttyconv_timeout);
75 if (rv == NULL) {
76 fprintf(stderr, " %s\n", strerror(errno));
77 return -1;
78 } else if (echo == 0)
79 fputs("\n", stderr);
80 return 0;
81 }
82
83 #else
84
85 static volatile sig_atomic_t caught_signal;
86 /*
87 * Handle incoming signals during tty conversation
88 */
89 static void
catch_signal(int signo)90 catch_signal(int signo)
91 {
92
93 switch (signo) {
94 case SIGINT:
95 case SIGQUIT:
96 case SIGTERM:
97 caught_signal = signo;
98 break;
99 }
100 }
101
102 /*
103 * Accept a response from the user on a tty
104 */
105 static int
prompt_tty(int ifd,int ofd,const char * message,char * response,int echo)106 prompt_tty(int ifd, int ofd, const char *message, char *response, int echo)
107 {
108 struct sigaction action;
109 struct sigaction saction_sigint, saction_sigquit, saction_sigterm;
110 struct termios tcattr;
111 struct timeval now, target, remaining;
112 int remaining_ms;
113 tcflag_t slflag;
114 struct pollfd pfd;
115 int serrno;
116 int pos, ret;
117 char ch;
118
119 /* turn echo off if requested */
120 slflag = 0; /* prevent bogus uninitialized variable warning */
121 if (!echo) {
122 if (tcgetattr(ifd, &tcattr) != 0) {
123 openpam_log(PAM_LOG_ERROR, "tcgetattr(): %m");
124 return (-1);
125 }
126 slflag = tcattr.c_lflag;
127 tcattr.c_lflag &= ~ECHO;
128 if (tcsetattr(ifd, TCSAFLUSH, &tcattr) != 0) {
129 openpam_log(PAM_LOG_ERROR, "tcsetattr(): %m");
130 return (-1);
131 }
132 }
133
134 /* write prompt */
135 if (write(ofd, message, strlen(message)) < 0) {
136 openpam_log(PAM_LOG_ERROR, "write(): %m");
137 return (-1);
138 }
139
140 /* install signal handlers */
141 caught_signal = 0;
142 action.sa_handler = &catch_signal;
143 action.sa_flags = 0;
144 sigfillset(&action.sa_mask);
145 sigaction(SIGINT, &action, &saction_sigint);
146 sigaction(SIGQUIT, &action, &saction_sigquit);
147 sigaction(SIGTERM, &action, &saction_sigterm);
148
149 /* compute timeout */
150 if (openpam_ttyconv_timeout > 0) {
151 (void)gettimeofday(&now, NULL);
152 remaining.tv_sec = openpam_ttyconv_timeout;
153 remaining.tv_usec = 0;
154 timeradd(&now, &remaining, &target);
155 } else {
156 /* prevent bogus uninitialized variable warning */
157 now.tv_sec = now.tv_usec = 0;
158 remaining.tv_sec = remaining.tv_usec = 0;
159 target.tv_sec = target.tv_usec = 0;
160 }
161
162 /* input loop */
163 pos = 0;
164 ret = -1;
165 serrno = 0;
166 while (!caught_signal) {
167 pfd.fd = ifd;
168 pfd.events = POLLIN;
169 pfd.revents = 0;
170 if (openpam_ttyconv_timeout > 0) {
171 gettimeofday(&now, NULL);
172 if (timercmp(&now, &target, >))
173 break;
174 timersub(&target, &now, &remaining);
175 remaining_ms = remaining.tv_sec * 1000 +
176 remaining.tv_usec / 1000;
177 } else {
178 remaining_ms = -1;
179 }
180 if ((ret = poll(&pfd, 1, remaining_ms)) < 0) {
181 serrno = errno;
182 if (errno == EINTR)
183 continue;
184 openpam_log(PAM_LOG_ERROR, "poll(): %m");
185 break;
186 } else if (ret == 0) {
187 /* timeout */
188 write(ofd, " timed out", 10);
189 openpam_log(PAM_LOG_NOTICE, "timed out");
190 break;
191 }
192 if ((ret = read(ifd, &ch, 1)) < 0) {
193 serrno = errno;
194 openpam_log(PAM_LOG_ERROR, "read(): %m");
195 break;
196 } else if (ret == 0 || ch == '\n') {
197 response[pos] = '\0';
198 ret = pos;
199 break;
200 }
201 if (pos + 1 < PAM_MAX_RESP_SIZE)
202 response[pos++] = ch;
203 /* overflow is discarded */
204 }
205
206 /* restore tty state */
207 if (!echo) {
208 tcattr.c_lflag = slflag;
209 if (tcsetattr(ifd, 0, &tcattr) != 0) {
210 /* treat as non-fatal, since we have our answer */
211 openpam_log(PAM_LOG_NOTICE, "tcsetattr(): %m");
212 }
213 }
214
215 /* restore signal handlers and re-post caught signal*/
216 sigaction(SIGINT, &saction_sigint, NULL);
217 sigaction(SIGQUIT, &saction_sigquit, NULL);
218 sigaction(SIGTERM, &saction_sigterm, NULL);
219 if (caught_signal != 0) {
220 openpam_log(PAM_LOG_ERROR, "caught signal %d",
221 (int)caught_signal);
222 raise((int)caught_signal);
223 /* if raise() had no effect... */
224 serrno = EINTR;
225 ret = -1;
226 }
227
228 /* done */
229 write(ofd, "\n", 1);
230 errno = serrno;
231 return (ret);
232 }
233
234 /*
235 * Accept a response from the user on a non-tty stdin.
236 */
237 static int
prompt_notty(const char * message,char * response)238 prompt_notty(const char *message, char *response)
239 {
240 struct timeval now, target, remaining;
241 int remaining_ms;
242 struct pollfd pfd;
243 int ch, pos, ret;
244
245 /* show prompt */
246 fputs(message, stdout);
247 fflush(stdout);
248
249 /* compute timeout */
250 if (openpam_ttyconv_timeout > 0) {
251 (void)gettimeofday(&now, NULL);
252 remaining.tv_sec = openpam_ttyconv_timeout;
253 remaining.tv_usec = 0;
254 timeradd(&now, &remaining, &target);
255 } else {
256 /* prevent bogus uninitialized variable warning */
257 now.tv_sec = now.tv_usec = 0;
258 remaining.tv_sec = remaining.tv_usec = 0;
259 target.tv_sec = target.tv_usec = 0;
260 }
261
262 /* input loop */
263 pos = 0;
264 for (;;) {
265 pfd.fd = STDIN_FILENO;
266 pfd.events = POLLIN;
267 pfd.revents = 0;
268 if (openpam_ttyconv_timeout > 0) {
269 gettimeofday(&now, NULL);
270 if (timercmp(&now, &target, >))
271 break;
272 timersub(&target, &now, &remaining);
273 remaining_ms = remaining.tv_sec * 1000 +
274 remaining.tv_usec / 1000;
275 } else {
276 remaining_ms = -1;
277 }
278 if ((ret = poll(&pfd, 1, remaining_ms)) < 0) {
279 /* interrupt is ok, everything else -> bail */
280 if (errno == EINTR)
281 continue;
282 perror("\nopenpam_ttyconv");
283 return (-1);
284 } else if (ret == 0) {
285 /* timeout */
286 break;
287 } else {
288 /* input */
289 if ((ch = getchar()) == EOF && ferror(stdin)) {
290 perror("\nopenpam_ttyconv");
291 return (-1);
292 }
293 if (ch == EOF || ch == '\n') {
294 response[pos] = '\0';
295 return (pos);
296 }
297 if (pos + 1 < PAM_MAX_RESP_SIZE)
298 response[pos++] = ch;
299 /* overflow is discarded */
300 }
301 }
302 fputs("\nopenpam_ttyconv: timeout\n", stderr);
303 return (-1);
304 }
305
306 /*
307 * Determine whether stdin is a tty; if not, try to open the tty; in
308 * either case, call the appropriate method.
309 */
310 static int
prompt(const char * message,char * response,int echo)311 prompt(const char *message, char *response, int echo)
312 {
313 int ifd, ofd, ret;
314
315 if (isatty(STDIN_FILENO)) {
316 fflush(stdout);
317 #ifdef HAVE_FPURGE
318 fpurge(stdin);
319 #endif
320 ifd = STDIN_FILENO;
321 ofd = STDOUT_FILENO;
322 } else {
323 if ((ifd = open("/dev/tty", O_RDWR)) < 0)
324 /* no way to prevent echo */
325 return (prompt_notty(message, response));
326 ofd = ifd;
327 }
328 ret = prompt_tty(ifd, ofd, message, response, echo);
329 if (ifd != STDIN_FILENO)
330 close(ifd);
331 return (ret);
332 }
333 #endif
334
335 /*
336 * OpenPAM extension
337 *
338 * Simple tty-based conversation function
339 */
340
341 int
openpam_ttyconv(int n,const struct pam_message ** msg,struct pam_response ** resp,void * data)342 openpam_ttyconv(int n,
343 const struct pam_message **msg,
344 struct pam_response **resp,
345 void *data)
346 {
347 char respbuf[PAM_MAX_RESP_SIZE];
348 struct pam_response *aresp;
349 int i;
350 FILE *infp, *outfp, *errfp;
351
352 ENTER();
353 /*LINTED unused*/
354 (void)data;
355 if (n <= 0 || n > PAM_MAX_NUM_MSG)
356 RETURNC(PAM_CONV_ERR);
357 if ((aresp = calloc((size_t)n, sizeof *aresp)) == NULL)
358 RETURNC(PAM_BUF_ERR);
359
360 /*
361 * read and write to /dev/tty if possible; else read from
362 * stdin and write to stderr.
363 */
364 if ((outfp = infp = errfp = fopen(_PATH_TTY, "w+")) == NULL) {
365 errfp = stderr;
366 outfp = stderr;
367 infp = stdin;
368 }
369
370 for (i = 0; i < n; ++i) {
371 aresp[i].resp_retcode = 0;
372 aresp[i].resp = NULL;
373 switch ((enum openpam_message_items)msg[i]->msg_style) {
374 case PAM_PROMPT_ECHO_OFF:
375 if (prompt(msg[i]->msg, respbuf, 0) < 0 ||
376 (aresp[i].resp = strdup(respbuf)) == NULL)
377 goto fail;
378 break;
379 case PAM_PROMPT_ECHO_ON:
380 if (prompt(msg[i]->msg, respbuf, 1) < 0 ||
381 (aresp[i].resp = strdup(respbuf)) == NULL)
382 goto fail;
383 break;
384 case PAM_ERROR_MSG:
385 fputs(msg[i]->msg, errfp);
386 if (strlen(msg[i]->msg) > 0 &&
387 msg[i]->msg[strlen(msg[i]->msg) - 1] != '\n')
388 fputc('\n', errfp);
389 break;
390 case PAM_TEXT_INFO:
391 fputs(msg[i]->msg, outfp);
392 if (strlen(msg[i]->msg) > 0 &&
393 msg[i]->msg[strlen(msg[i]->msg) - 1] != '\n')
394 fputc('\n', outfp);
395 break;
396 default:
397 goto fail;
398 }
399 }
400 if (infp != stdin)
401 (void)fclose(infp);
402 *resp = aresp;
403 memset(respbuf, 0, sizeof respbuf);
404 RETURNC(PAM_SUCCESS);
405 fail:
406 for (i = 0; i < n; ++i) {
407 if (aresp[i].resp != NULL) {
408 strlset(aresp[i].resp, 0, PAM_MAX_RESP_SIZE);
409 FREE(aresp[i].resp);
410 }
411 }
412 if (infp != stdin)
413 (void)fclose(infp);
414 memset(aresp, 0, (size_t)n * sizeof *aresp);
415 FREE(aresp);
416 *resp = NULL;
417 memset(respbuf, 0, sizeof respbuf);
418 RETURNC(PAM_CONV_ERR);
419 }
420
421 /*
422 * Error codes:
423 *
424 * PAM_SYSTEM_ERR
425 * PAM_BUF_ERR
426 * PAM_CONV_ERR
427 */
428
429 /**
430 * The =openpam_ttyconv function is a standard conversation function
431 * suitable for use on TTY devices.
432 * It should be adequate for the needs of most text-based interactive
433 * programs.
434 *
435 * The =openpam_ttyconv function displays a prompt to, and reads in a
436 * password from /dev/tty. If this file is not accessible, =openpam_ttyconv
437 * displays the prompt on the standard error output and reads from the
438 * standard input.
439 *
440 * The =openpam_ttyconv function allows the application to specify a
441 * timeout for user input by setting the global integer variable
442 * :openpam_ttyconv_timeout to the length of the timeout in seconds.
443 *
444 * >openpam_nullconv
445 * >pam_prompt
446 * >pam_vprompt
447 * >getpass
448 */
449