view pwm.c @ 18:1e39a251cbe9

Use libtecla for interactive input
author Guido Berhoerster <guido+pwm@berhoerster.name>
date Thu, 24 Aug 2017 13:10:56 +0200
parents a07665727c19
children 5c6155c8e9b6
line wrap: on
line source

/*
 * Copyright (C) 2017 Guido Berhoerster <guido+pwm@berhoerster.name>
 *
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including
 * without limitation the rights to use, copy, modify, merge, publish,
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject to
 * the following conditions:
 *
 * The above copyright notice and this permission notice shall be included
 * in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

#include "compat.h"

#ifdef	HAVE_ERR_H
#include <err.h>
#endif /* HAVE_ERR_H */
#include <errno.h>
#include <fcntl.h>
#include <langinfo.h>
#include <locale.h>
#include <pwd.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <pwd.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>

#include "pwm.h"
#include "cmd.h"
#include "pwfile.h"
#include "tok.h"
#include "util.h"

#ifndef	PWM_HISTORY_ENTRIES_MAX
#define	PWM_HISTORY_ENTRIES_MAX	1024
#endif /* !PWM_HISTORY_MAX */

#ifndef	PWM_HISTORY_LINES_MAX
#define	PWM_HISTORY_LINES_MAX	256
#endif /* !PWM_HISTORY_LINES_MAX */

#ifndef	PWM_HISTORY_MAX
#define	PWM_HISTORY_MAX	(PWM_HISTORY_LINES_MAX * PWM_LINE_MAX)
#endif /* !PWM_HISTORY_MAX */

static void
usage(void)
{
	fprintf(stderr, "usage: %s [-P file] [filename]\n", getprogname());
}

void
pwm_err(struct pwm_ctx *ctx, char *fmt, ...)
{
	va_list	args;

	free(ctx->errmsg);

	if (fmt != NULL) {
		va_start(args, fmt);
		xvasprintf(&ctx->errmsg, fmt, args);
		va_end(args);

		fprintf(stderr, "%s\n", ctx->errmsg);
	} else {
		ctx->errmsg = NULL;
	}
}

static int
complete_nothing(WordCompletion *cpl, void *data, const char *line,
    int word_end)
{
	return (0);
}

static int
run_input_loop(struct pwm_ctx *ctx, int is_interactive)
{
	int		retval = -1;
	char		prompt[8 + 2 + 1];
	GetLine		*gl = NULL;
	char		buf[PWM_LINE_MAX];
	char		*line = buf;
	int		c;
	int		argc = 0;
	char		**argv = NULL;
	struct cmd	*cmd;
	int		i;

	snprintf(prompt, sizeof (prompt), "%.*s> ", 8, getprogname());

	/* initialize libtecla */
	gl = new_GetLine(PWM_LINE_MAX, PWM_HISTORY_MAX);
	if (gl == NULL) {
		err(1, "new_GetLine");
	}
	gl_limit_history(gl, PWM_HISTORY_LINES_MAX);
	/* disable default filename completion */
	gl_customize_completion(gl, NULL, complete_nothing);

	for (;;) {
		cmd = NULL;
		line = gl_get_line(gl, prompt, NULL, -1);
		if (line == NULL) {
			switch (gl_return_status(gl)) {
			case GLR_EOF:
				break;
			case GLR_ERROR:
				warnx("gl_get_line: %s",
				    gl_error_message(gl, NULL, 0));
			}
			break;
		}

		/* tokenize line */
		switch (tok_tokenize(line, &argc, &argv)) {
		case TOK_ERR_SYSTEM_ERROR:
			err(1, "tok_tokenize");
		case TOK_ERR_UNTERMINATED_QUOTE:
			fprintf(stderr, "unterminated quote\n");
			if (!is_interactive) {
				goto out;
			}
			goto next;
		case TOK_ERR_TRAILING_BACKSLASH:
			fprintf(stderr, "trailing backslash\n");
			if (!is_interactive) {
				goto out;
			}
			goto next;
		}

		if (argc == 0) {
			/* empty line */
			goto next;
		}

		/* find and execute the command */
		cmd = cmd_match(argv[0]);
		if (cmd == NULL) {
			pwm_err(ctx, "unknown command: %s", argv[0]);
			if (is_interactive) {
				goto next;
			} else {
				goto out;
			}
		}
		switch (cmd->cmd_func(ctx, argc, argv)) {
		case CMD_OK:
			pwm_err(ctx, NULL); /* clear error */
			break;
		case CMD_USAGE:
			fprintf(stderr, "usage: %s\n", cmd->usage);
		case CMD_ERR:	/* FALLTHROUGH */
			if (!is_interactive) {
				goto out;
			}
			break;
		case CMD_QUIT:
			goto quit;
		}
		ctx->prev_cmd = cmd->full_cmd;

next:
		for (i = 0; i < argc; i++) {
			free(argv[i]);
		}
		free(argv);
		argc = 0;
		argv = NULL;
	}

quit:
	retval = 0;

out:
	for (i = 0; i < argc; i++) {
		free(argv[i]);
	}
	free(argv);
	del_GetLine(gl);

	return (retval);
}

int
pwm_read_password(struct pwm_ctx *ctx, int is_new_password)
{
	int	retval = -1;
	GetLine	*gl = NULL;
	char	*line;
	size_t	len;
	char	password_buf[sizeof (ctx->password)] = { '\0' };

	/* initialize libtecla */
	gl = new_GetLine(sizeof (password_buf) - 1, 0);
	if (gl == NULL) {
		err(1, "new_GetLine");
	}
	/* disable default filename completion */
	gl_customize_completion(gl, NULL, complete_nothing);
	gl_echo_mode(gl, 0);

	line = gl_get_line(gl, is_new_password ? "New password: " :
	    "Password: ", NULL, -1);
	putchar('\n');
	if (line == NULL) {
		if (gl_return_status(gl) == GLR_ERROR) {
			errx(1, "gl_get_line: %s", gl_error_message(gl, NULL,
			    0));
		}
		goto out;
	}
	len = strlen(line);
	if ((len > 0) && (line[len - 1] == '\n')) {
		line[--len] = '\0';
	}
	if (len == 0) {
		fprintf(stderr, "password must not be empty\n");
		goto out;
	}
	strcpy(password_buf, line);

	/* confirm the entered password entered */
	if (is_new_password) {
		line = gl_get_line(gl, "Confirm password: ", NULL, -1);
		putchar('\n');
		if (line == NULL) {
			if (gl_return_status(gl) == GLR_ERROR) {
				errx(1, "gl_get_line: %s", gl_error_message(gl,
				    NULL, 0));
			}
			goto out;
		}
		len = strlen(line);
		if ((len > 0) && (line[len - 1] == '\n')) {
			line[--len] = '\0';
		}
		if (strcmp(password_buf, line) != 0) {
			fprintf(stderr, "passwords do not match\n");
			goto out;
		}
	}

	strcpy(ctx->password, password_buf);
	retval = 0;

out:
	del_GetLine(gl);

	return (retval);
}

static int
read_password_from_file(const char *filename, char *password,
    size_t password_size)
{
	int	retval = -1;
	char	*buf = NULL;
	FILE	*fp = NULL;
	size_t	password_len = 0;

	buf = xmalloc(password_size);

	fp = fopen(filename, "r");
	if (fp == NULL) {
		warn("failed to open master password file \"%s\"", filename);
		goto out;
	}
	if (fgets(buf, password_size, fp) == NULL) {
		/* read error or empty file */
		if (ferror(fp)) {
			/* read error */
			warn("failed to read master password from \"%s\"",
			    filename);
		}
		goto out;
	}
	password_len = strlen(buf);
	if (buf[password_len - 1] == '\n') {
		/* strip trailing newline */
		password_len--;
		if (password_len == 0) {
			/* first line is empty */
			fprintf(stderr, "malformed password file\n");
			goto out;
		}
	} else if (!feof(fp)) {
		/* the first line was truncated, password is too long */
		fprintf(stderr, "malformed password file\n");
		goto out;
	}
	memcpy(password, buf, password_size);
	retval = 0;

out:
	password[password_len] = '\0';

	if (fp != NULL) {
		fclose(fp);
	}
	free(buf);

	return (retval);
}

int
main(int argc, char *argv[])
{
	int		status = EXIT_FAILURE;
	char		*locale;
	int		is_interactive;
	int		errflag = 0;
	int		c;
	const char	*master_password_filename = NULL;
	struct pwm_ctx	ctx = { 0 };
	struct passwd	*passwd;
	char		*pwm_dirname = NULL;
	FILE		*fp = NULL;

	setprogname(argv[0]);

	/* set up locale and check for UTF-8 codeset */
	locale = setlocale(LC_ALL, "");
	if (locale == NULL) {
		errx(1, "invalid locale");
	}
	if ((strcasecmp(nl_langinfo(CODESET), "UTF-8") != 0) &&
	    (strcasecmp(nl_langinfo(CODESET), "UTF8") != 0)) {
		fprintf(stderr, "pwm requires a locale with UTF-8 character "
		    "encoding.\n");
		goto out;
	}

	/* timestamps are processed as UTC */
	if (setenv("TZ", "", 1) != 0) {
		goto out;
	}
	tzset();

	/* initialize libpws */
	if (pws_init() != 0) {
		goto out;
	}

	is_interactive = isatty(STDIN_FILENO);

	while (!errflag && (c = getopt(argc, argv, "P:h")) != -1) {
		switch (c) {
		case 'P':
			master_password_filename = optarg;
			break;
		case 'h':
			usage();
			status = EXIT_SUCCESS;
			goto out;
		default:
			errflag = 1;
		}
	}
	if (errflag || (optind + 1 < argc)) {
		usage();
		status = EXIT_USAGE;
		goto out;
	}

	if (optind == argc) {
		passwd = getpwuid(getuid());
		if (passwd == NULL) {
			err(1, "getpwuid");
		}
		xasprintf(&pwm_dirname, "%s/.pwm", passwd->pw_dir);
		xasprintf(&ctx.filename, "%s/pwm.psafe3", pwm_dirname);

		/* create ~/.pwm directory if necessary */
		if ((mkdir(pwm_dirname, S_IRWXU) != 0) && (errno != EEXIST)) {
			warn("failed to create directory \"%s\"", pwm_dirname);
			goto out;
		}
	} else if (optind + 1 == argc) {
		ctx.filename = xstrdup(argv[optind++]);
	} else {
		usage();
		status = EXIT_USAGE;
		goto out;
	}

	if (is_interactive) {
		printf("pwm version %s\n", VERSION);
	} else if (master_password_filename == NULL) {
		fprintf(stderr, "master password file must be specified when "
		    "running non-interactively\n");
		goto out;
	}

	pwfile_init(&ctx);

	fp = fopen(ctx.filename, "r");
	if ((fp == NULL) && (errno != ENOENT)) {
		warn("failed to open password database");
		goto out;
	}
	/* obtain master password */
	if (master_password_filename != NULL) {
		if (read_password_from_file(master_password_filename,
		    ctx.password, sizeof (ctx.password)) != 0) {
			goto out;
		}
	} else if (pwm_read_password(&ctx, (fp == NULL)) != 0) {
		goto out;
	}
	if (fp != NULL) {
		if (pwfile_read_file(&ctx, fp) != 0) {
			goto out;
		}
		fclose(fp);
		fp = NULL;
	}

	/* run main input loop */
	status = (run_input_loop(&ctx, is_interactive) != 0);

out:
	pwfile_destroy(&ctx);
	if (fp != NULL) {
		fclose(fp);
	}
	free(ctx.filename);
	free(ctx.errmsg);
	free(pwm_dirname);

	exit(status);
}