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