view cmd.c @ 12:8768fbd09bc5

Add generatepassword command to generate random passwords Refactor and generalize handling of named arguments.
author Guido Berhoerster <guido+pwm@berhoerster.name>
date Thu, 03 Aug 2017 10:22:07 +0200
parents 85bce13237cf
children cf81eb0c2d5a
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"

#include <ctype.h>
#ifdef	HAVE_ERR_H
#include <err.h>
#endif /* HAVE_ERR_H */
#include <errno.h>
#include <limits.h>
#ifdef	HAVE_READPASSPHRASE_H
#include <readpassphrase.h>
#endif /* READPASSPHRASE_H */
#include <regex.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#include "cmd.h"
#include "pw.h"
#include "pwfile.h"
#include "util.h"

#define	TIME_FORMAT	"%Y-%m-%dT%TZ"
#define	TIME_SIZE	(4 + 1 + 2 + 1 + 2 + 1 + 8 + 1 + 1)

#define	CHARS_DIGIT	"0123456789"
#define	CHARS_UPPER	"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
#define	CHARS_LOWER	"abcdefghijklmnopqrstuvwxyz"
#define	CHARS_PUNCT	"!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"

enum field_type {
	FIELD_UNKNOWN = -1,
	FIELD_GROUP,
	FIELD_TITLE,
	FIELD_USERNAME,
	FIELD_PASSWORD,
	FIELD_NOTES,
	FIELD_URL,
	FIELD_MTIME,
	FIELD_CTIME
};

enum cmd_generatepassword_arg_type {
	CMD_GP_ARG_UNKNOWN = -1,
	CMD_GP_ARG_LEN,
	CMD_GP_ARG_CHARS,
	CMD_GP_ARG_CHARCLASS
};

enum charclass_type {
	CHARCLASS_UNKNOWN = -1,
	CHARCLASS_DIGIT,
	CHARCLASS_UPPER,
	CHARCLASS_LOWER,
	CHARCLASS_PUNCT,
	CHARCLASS_ALPHA,
	CHARCLASS_ALNUM,
	CHARCLASS_XDIGIT,
	CHARCLASS_GRAPH
};

static enum cmd_return	cmd_info(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_list(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_create(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_modify(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_generatepassword(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_remove(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_show(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_pipe(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_creategroup(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_removegroup(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_changepassword(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_help(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_write(struct pwm_ctx *, int, char *[]);
static enum cmd_return	cmd_quit(struct pwm_ctx *, int, char *[]);

static const char *field_namev[] = {
    "group",
    "title",
    "username",
    "password",
    "notes",
    "url",
    "ctime",
    "mtime",
    NULL
};

static const char *field_labels[] = {
    "Group:    ",
    "Title:    ",
    "Username: ",
    "Password: ",
    "Notes:    ",
    "URL:      ",
    "Created:  ",
    "Modified: "
};

static const char *cmd_generatepassword_argv[] = {
    "len",
    "char",
    "charclass",
    NULL
};

static const char *charclass_namev[] = {
    "digit",
    "upper",
    "lower",
    "punct",
    "alpha",
    "alnum",
    "xdigit",
    "graph",
    NULL
};

static const char *charclass_values[] = {
    CHARS_DIGIT,
    CHARS_UPPER,
    CHARS_LOWER,
    CHARS_PUNCT,
    CHARS_UPPER CHARS_LOWER,
    CHARS_DIGIT CHARS_UPPER CHARS_LOWER,
    CHARS_DIGIT "abcdef",
    CHARS_DIGIT CHARS_UPPER CHARS_LOWER CHARS_PUNCT
};

static struct cmd cmds[] = {
    { "i", "info", "info", "Show metadata information about the current file",
    cmd_info },
    { "ls", "list", "list [field~regex ...]", "List entries", cmd_list },
    { "c", "create", "create field=value ...", "Create entry", cmd_create },
    { "m", "modify", "modify id field=value ...", "Modify entry", cmd_modify },
    { "gp", "generatepassword", "generatepassword [id] [len=n] [chars=n:chars] "
    "[charclass=n:class] ...", "Randomly generate a password",
    cmd_generatepassword },
    { "rm", "remove", "remove id", "Delete entry", cmd_remove },
    { "s", "show", "show id field", "Show entry", cmd_show },
    { "p", "pipe", "pipe id field command", "Pipe entry to external command",
    cmd_pipe },
    { "cg", "creategroup", "creategroup name", "Create empty group",
    cmd_creategroup },
    { "rg", "removegroup", "removegroup name", "Delete empty group",
    cmd_removegroup },
    { "ch", "changepassword", "changepassword", "Change password",
    cmd_changepassword },
    { "h", "help", "help", "Show help text", cmd_help },
    { "w", "write", "write", "Write the database", cmd_write },
    { "q", "quit", "quit", "Quit", cmd_quit },
    { 0 }
};

static int
parse_arg(char *arg, const char *namev[], int sep, char **valuep)
{
	size_t	i;
	size_t	name_len;

	for (i = 0; namev[i] != NULL; i++) {
		name_len = strlen(namev[i]);
		if ((strncmp(namev[i], arg, name_len) == 0) &&
		    (arg[name_len] == sep)) {
			if (valuep != NULL) {
				*valuep = arg + name_len + 1;
			}
			return (i);
		}
	}

	return (-1);
}

static int
parse_id(const char *arg, unsigned int *idp)
{
	long	x;
	char	*p;

	errno = 0;
	x = strtol(arg, &p, 10);
	if ((errno != 0) || (*arg == '\0') || (*p != '\0') || (x > UINT_MAX) ||
	    (x <= 0)) {
		return (-1);
	}
	*idp = (unsigned int)x;

	return (0);
}

static enum cmd_return
cmd_info(struct pwm_ctx *ctx, int argc, char *argv[])
{
	struct metadata	*metadata;
	struct tm	*tm;
	char		timebuf[TIME_SIZE];

	if (argc != 1) {
		return (CMD_USAGE);
	}

	metadata = pwfile_get_metadata(ctx);
	printf("Format:      0x%04x\n", metadata->version);
	if (metadata->user != NULL) {
		printf("User:        %s\n", metadata->user);
	}
	if (metadata->user != NULL) {
		printf("Host:        %s\n", metadata->host);
	}
	if (metadata->user != NULL) {
		printf("Application: %s\n", metadata->application);
	}
	tm = gmtime(&metadata->timestamp);
	strftime(timebuf, sizeof (timebuf), TIME_FORMAT, tm);
	printf("Last Saved:  %s\n", timebuf);

	pwfile_destroy_metadata(metadata);

	return (CMD_OK);
}

static enum cmd_return
cmd_list(struct pwm_ctx *ctx, int argc, char *argv[])
{
	int		retval = CMD_ERR;
	int		i;
	regex_t		*group_re = NULL;
	regex_t		*title_re = NULL;
	regex_t		*username_re = NULL;
	regex_t		*notes_re = NULL;
	regex_t		*url_re = NULL;
	enum field_type	type;
	char		*value;
	regex_t		**repp;
	int		errcode;
	char		*errbuf;
	size_t		errbuf_size;
	union list_item	**list = NULL;
	size_t		j;
	struct record	*record;

	for (i = 1; i < argc; i++) {
		type = parse_arg(argv[i], field_namev, '~', &value);
		if (type == FIELD_UNKNOWN) {
			fprintf(stderr, "bad field name \"%s\"\n", argv[i]);
			goto out;
		}
		if (value[0] == '\0') {
			/* skip empty expressions */
			continue;
		}
		switch (type) {
		case FIELD_GROUP:
			repp = &group_re;
			break;
		case FIELD_TITLE:
			repp = &title_re;
			break;
		case FIELD_USERNAME:
			repp = &username_re;
			break;
		case FIELD_NOTES:
			repp = &notes_re;
			break;
		case FIELD_URL:
			repp = &url_re;
			break;
		default:
			fprintf(stderr, "bad field name \"%s\"\n", argv[i]);
			goto out;
		}

		if (*repp == NULL) {
			*repp = xmalloc(sizeof (regex_t));
		} else {
			regfree(*repp);
		}
		errcode = regcomp(*repp, value, REG_EXTENDED | REG_NOSUB);
		if (errcode != 0) {
			errbuf_size = regerror(errcode, *repp, "", 0);
			errbuf = xmalloc(errbuf_size);
			regerror(errcode, *repp, errbuf, errbuf_size);
			fprintf(stderr, "bad regular expression \"%s\"\n",
			    errbuf);
			free(errbuf);

			free(*repp);
			*repp = NULL;

			goto out;
		}
	}

	list = pwfile_create_list(ctx);
	for (j = 0; list[j] != NULL; j++) {
		if (list[j]->any.type == ITEM_TYPE_GROUP) {
			printf("[%s]\n", list[j]->group.group);
		} else {
			record = pwfile_get_record(ctx, list[j]->record.id);
			if (((group_re == NULL) || (regexec(group_re,
			    record->group, 0, NULL, 0) == 0)) &&
			    ((title_re == NULL) || (regexec(title_re,
			    record->title, 0, NULL, 0) == 0)) &&
			    ((username_re == NULL) || (regexec(username_re,
			    record->username, 0, NULL, 0) == 0)) &&
			    ((notes_re == NULL) || (regexec(notes_re,
			    record->notes, 0, NULL, 0) == 0)) &&
			    ((url_re == NULL) || (regexec(url_re,
			    record->url, 0, NULL, 0) == 0))) {
				printf("%4u %s\n", list[j]->record.id,
				    (list[j]->record.title != NULL) ?
				    list[j]->record.title : "");
			}
			pwfile_destroy_record(record);
		}
	}
	retval = CMD_OK;

out:
	if (group_re != NULL) {
		regfree(group_re);
		free(group_re);
	}
	if (title_re != NULL) {
		regfree(title_re);
		free(title_re);
	}
	if (username_re != NULL) {
		regfree(username_re);
		free(username_re);
	}
	if (notes_re != NULL) {
		regfree(notes_re);
		free(notes_re);
	}
	if (url_re != NULL) {
		regfree(url_re);
		free(url_re);
	}

	pwfile_destroy_list(list);

	return (retval);
}

static enum cmd_return
cmd_create(struct pwm_ctx *ctx, int argc, char *argv[])
{
	int		i;
	struct record record = { 0 };
	enum field_type	type;
	char		*value;

	if (argc < 2) {
		return (CMD_USAGE);
	}

	for (i = 1; i < argc; i++) {
		type = parse_arg(argv[i], field_namev, '=', &value);
		if (type == FIELD_UNKNOWN) {
			fprintf(stderr, "bad field assignment \"%s\"\n",
			    argv[i]);
		}
		if (value[0] == '\0') {
			/* skip empty assignments */
			continue;
		}
		switch (type) {
		case FIELD_GROUP:
			record.group = value;
			break;
		case FIELD_TITLE:
			record.title = value;
			break;
		case FIELD_USERNAME:
			record.username = value;
			break;
		case FIELD_PASSWORD:
			record.password = value;
			break;
		case FIELD_NOTES:
			record.notes = value;
			break;
		case FIELD_URL:
			record.url = value;
			break;
		default:
			fprintf(stderr, "bad field name \"%s\"\n", argv[i]);
			return (CMD_ERR);
		}
	}

	pwfile_create_record(ctx, &record);

	return (CMD_OK);
}

static enum cmd_return
cmd_modify(struct pwm_ctx *ctx, int argc, char *argv[])
{
	unsigned int	id;
	int		i;
	struct record	record = { 0 };
	enum field_type	type;
	char		*value;

	if (argc < 2) {
		return (CMD_USAGE);
	}

	if (parse_id(argv[1], &id) != 0) {
		fprintf(stderr, "invalid id %s\n", argv[1]);
		return (CMD_ERR);
	}

	for (i = 2; i < argc; i++) {
		type = parse_arg(argv[i], field_namev, '=', &value);
		if (type == FIELD_UNKNOWN) {
			fprintf(stderr, "bad field assignment \"%s\"\n",
			    argv[i]);
			return (CMD_ERR);
		}
		if (value[0] == '\0') {
			/* skip empty assignments */
			continue;
		}
		switch (type) {
		case FIELD_GROUP:
			record.group = value;
			break;
		case FIELD_TITLE:
			record.title = value;
			break;
		case FIELD_USERNAME:
			record.username = value;
			break;
		case FIELD_PASSWORD:
			record.password = value;
			break;
		case FIELD_NOTES:
			record.notes = value;
			break;
		case FIELD_URL:
			record.url = value;
			break;
		default:
			fprintf(stderr, "bad field name \"%s\"\n", argv[i]);
			return (CMD_ERR);
		}
	}

	pwfile_modify_record(ctx, id, &record);

	return (CMD_OK);
}

static enum cmd_return
cmd_generatepassword(struct pwm_ctx *ctx, int argc, char *argv[])
{
	enum cmd_return	retval = CMD_ERR;
	unsigned int	id = 0;
	int		i = 1;
	char		*value = NULL;
	long		x;
	char		*p;
	size_t		password_len = 16;
	size_t		chars_min;
	const char	*chars;
	struct pw_char_group *char_groupv = NULL;
	size_t		char_groupv_len = 0;
	int		charclass;
	size_t		j;
	char		password[PWS3_MAX_PASSWORD_LEN + 1] = { 0 };

	/* check if first argument is an id */
	if ((argc > 1) && (parse_id(argv[1], &id) == 0)) {
		i++;
	}

	for (; i < argc; i++) {
		switch (parse_arg(argv[i], cmd_generatepassword_argv, '=',
		    &value)) {
		case CMD_GP_ARG_LEN:
			errno = 0;
			x = strtol(value, &p, 10);
			if ((errno != 0) || (*value == '\0') || (*p != '\0') ||
			    (x > PWS3_MAX_PASSWORD_LEN) || (x <= 0)) {
				fprintf(stderr, "invalid password length "
				    "\"%s\"\n", argv[i]);
				goto out;
			}
			password_len = x;
			break;
		case CMD_GP_ARG_CHARS:
			errno = 0;
			x = strtol(value, &p, 10);
			if ((errno != 0) || (*value == '\0') || (*p != ':') ||
			    (x < 0) || (x > PWS3_MAX_PASSWORD_LEN)) {
				fprintf(stderr, "invalid minimum number of "
				    "characters \"%s\"\n", argv[i]);
				goto out;
			}
			chars_min = x;

			chars = ++p;
			while (*p != '\0') {
				if (!isascii(*p) || !isprint(*p)) {
					fprintf(stderr, "invalid character in "
					    "character group \"%s\"\n",
					    argv[i]);
					goto out;
				}
				p++;
			}

			char_groupv = xrealloc(char_groupv,
			    sizeof (struct pw_char_group) * (char_groupv_len +
			    1));
			char_groupv[char_groupv_len].chars = chars;
			char_groupv[char_groupv_len].chars_min = chars_min;
			char_groupv_len++;
			break;
		case CMD_GP_ARG_CHARCLASS:
			errno = 0;
			x = strtol(value, &p, 10);
			if ((errno != 0) || (*value == '\0') || (*p != ':') ||
			    (x < 0) || (x > PWS3_MAX_PASSWORD_LEN)) {
				fprintf(stderr, "invalid minimum number of "
				    "characters \"%s\"\n", argv[i]);
				goto out;
			}
			chars_min = x;

			charclass = parse_arg(++p, charclass_namev, '\0', NULL);
			if (charclass < 0) {
				fprintf(stderr, "unknown character class "
				    "\"%s\"\n", argv[i]);
				goto out;
			}
			chars = charclass_values[charclass];
			char_groupv = xrealloc(char_groupv,
			    sizeof (struct pw_char_group) * (char_groupv_len +
			    1));
			char_groupv[char_groupv_len].chars = chars;
			char_groupv[char_groupv_len].chars_min = chars_min;
			char_groupv_len++;
			break;
		default:
			fprintf(stderr, "invalid argument \"%s\"\n", argv[i]);
			retval = CMD_USAGE;
			goto out;
		}
	}

	for (j = 0; j < char_groupv_len; j++) {
		if (char_groupv[j].chars_min > password_len) {
			fprintf(stderr, "invalid minimum number of "
			    "characters \"%zu:%s\"\n", char_groupv[j].chars_min,
			    char_groupv[j].chars);
			goto out;
		}
	}

	if (char_groupv_len == 0) {
		/* use defaults */
		char_groupv = xmalloc(sizeof (struct pw_char_group));
		char_groupv[0].chars = charclass_values[CHARCLASS_GRAPH];
		char_groupv[0].chars_min = 0;
		char_groupv_len++;
	}

	if (pw_genrandom(char_groupv, char_groupv_len, password,
	    password_len) != 0) {
		fprintf(stderr, "failed to generate password that meets the "
		    "given constraints\n");
		goto out;
	}

	if (id != 0) {
		if (pwfile_modify_record(ctx, id,
		    &(struct record){ .password = password }) != 0) {
			fprintf(stderr, "record %u does not exist\n", id);
			goto out;
		}
	} else {
		printf("%s\n", password);
	}

	retval = CMD_OK;

out:
	free(char_groupv);

	return (retval);
}

static enum cmd_return
cmd_remove(struct pwm_ctx *ctx, int argc, char *argv[])
{
	unsigned int	id;

	if (argc != 2) {
		return (CMD_USAGE);
	}

	if (parse_id(argv[1], &id) != 0) {
		fprintf(stderr, "invalid id %s\n", argv[1]);
		return (CMD_ERR);
	}

	if (pwfile_remove_record(ctx, id) != 0) {
		fprintf(stderr, "failed to remove record %u\n", id);
		return (CMD_ERR);
	}

	return (CMD_OK);
}

static int
print_field(const char *label, const char *value, int show_label, FILE *fp)
{
	fprintf(fp, "%s%s\n", show_label ? label : "", (value != NULL) ?
	    value : "");
	if (ferror(fp)) {
		warn("fprintf");
		return (-1);
	}
	return (0);
}

static void
print_record(struct record *record, int fields[], int show_labels, FILE *fp)
{
	struct tm	*tm;
	char		timebuf[TIME_SIZE];

	if (fields[FIELD_TITLE]) {
		if (print_field(field_labels[FIELD_TITLE], record->title,
		    show_labels, fp) != 0) {
			return;
		}
	}
	if (fields[FIELD_GROUP]) {
		if (print_field(field_labels[FIELD_GROUP], record->group,
		    show_labels, fp)) {
			return;
		}
	}
	if (fields[FIELD_USERNAME]) {
		if (print_field(field_labels[FIELD_USERNAME], record->username,
		    show_labels, fp)) {
			return;
		}
	}
	if (fields[FIELD_PASSWORD]) {
		if (print_field(field_labels[FIELD_PASSWORD], record->password,
		    show_labels, fp)) {
			return;
		}
	}
	if (fields[FIELD_NOTES]) {
		if (print_field(field_labels[FIELD_NOTES], record->notes,
		    show_labels, fp)) {
			return;
		}
	}
	if (fields[FIELD_URL]) {
		if (print_field(field_labels[FIELD_URL], record->url,
		    show_labels, fp)) {
			return;
		}
	}
	if (fields[FIELD_CTIME]) {
		tm = gmtime(&record->ctime);
		strftime(timebuf, sizeof (timebuf), TIME_FORMAT, tm);
		if (print_field(field_labels[FIELD_CTIME], timebuf,
		    show_labels, fp)) {
			return;
		}
	}
	if (fields[FIELD_MTIME]) {
		tm = gmtime(&record->mtime);
		strftime(timebuf, sizeof (timebuf), TIME_FORMAT, tm);
		if (print_field(field_labels[FIELD_MTIME], timebuf,
		    show_labels, fp)) {
			return;
		}
	}
}

static enum cmd_return
cmd_show(struct pwm_ctx *ctx, int argc, char *argv[])
{
	unsigned int	id;
	struct record	*record;
	int		i;
	enum field_type	type;
	int		fields[COUNTOF(field_namev) - 1] = { 0 };

	if (argc < 2) {
		return (CMD_USAGE);
	}

	if (parse_id(argv[1], &id) != 0) {
		fprintf(stderr, "invalid id %s\n", argv[1]);
		return (CMD_ERR);
	}

	for (i = 2; i < argc; i++) {
		type = parse_arg(argv[i], field_namev, '\0', NULL);
		if (type < 0) {
			fprintf(stderr, "bad field name \"%s\"\n", argv[i]);
			return (CMD_ERR);
		}
		fields[type] = 1;
	}

	record = pwfile_get_record(ctx, id);
	if (record == NULL) {
		fprintf(stderr, "record %u does not exist\n", id);
		return (CMD_ERR);
	}
	print_record(record, fields, 1, stdout);
	pwfile_destroy_record(record);

	return (CMD_OK);
}

static enum cmd_return
cmd_pipe(struct pwm_ctx *ctx, int argc, char *argv[])
{
	enum cmd_return	retval = CMD_ERR;
	unsigned int	id;
	struct record	*record = NULL;
	enum field_type	type;
	int		fields[COUNTOF(field_namev) - 1] = { 0 };
	FILE		*fp = NULL;

	if (argc != 4) {
		return (CMD_USAGE);
	}

	if (parse_id(argv[1], &id) != 0) {
		fprintf(stderr, "invalid id %s\n", argv[1]);
		return (CMD_ERR);
	}

	type = parse_arg(argv[2], field_namev, '\0', NULL);
	if (type < 0) {
		fprintf(stderr, "bad field name \"%s\"\n", argv[2]);
		return (CMD_ERR);
	}
	fields[type] = 1;

	fp = popen(argv[3], "w");
	if (fp == NULL) {
		warn("popen");
		goto out;
	}

	record = pwfile_get_record(ctx, id);
	if (record == NULL) {
		fprintf(stderr, "record %u does not exist\n", id);
		goto out;
	}

	print_record(record, fields, 0, fp);

	retval = CMD_OK;

out:
	pwfile_destroy_record(record);
	if (fp != NULL) {
		pclose(fp);
	}

	return (retval);
}

static enum cmd_return
cmd_creategroup(struct pwm_ctx *ctx, int argc, char *argv[])
{
	if (argc != 2) {
		return (CMD_USAGE);
	}

	if (pwfile_create_group(ctx, argv[1]) != 0) {
		fprintf(stderr, "group \"%s\" already exists\n", argv[1]);
		return (CMD_ERR);
	}

	return (CMD_OK);
}

static enum cmd_return
cmd_removegroup(struct pwm_ctx *ctx, int argc, char *argv[])
{
	if (argc != 2) {
		return (CMD_USAGE);
	}

	if (pwfile_remove_group(ctx, argv[1]) != 0) {
		fprintf(stderr, "there is no empty group \"%s\"\n", argv[1]);
		return (CMD_ERR);
	}

	return (CMD_OK);
}

static enum cmd_return
cmd_changepassword(struct pwm_ctx *ctx, int argc, char *argv[])
{
	size_t	password_len;
	char	password_buf[PWS3_MAX_PASSWORD_LEN + 1];
	char	confirm_buf[PWS3_MAX_PASSWORD_LEN + 1];

	if (argc > 2) {
		return (CMD_USAGE);
	} else if (argc == 2) {
		password_len = strlen(argv[1]);
		if (password_len == 0) {
			fprintf(stderr, "password must not be empty\n");
			return (CMD_ERR);
		} else if (password_len + 1 > sizeof (ctx->password)) {
			fprintf(stderr, "password too long\n");
			return (CMD_ERR);
		}
		memcpy(ctx->password, argv[1], password_len + 1);
	} else {
		if (readpassphrase("Enter password: ", password_buf,
		    sizeof (password_buf), RPP_ECHO_OFF | RPP_REQUIRE_TTY) ==
		    NULL) {
			err(1, "readpassphrase");
		}
		password_len = strlen(password_buf);
		if (password_len == 0) {
			fprintf(stderr, "password must not be empty\n");
			return (CMD_ERR);
		}
		if (readpassphrase("Confirm password: ", confirm_buf,
		    sizeof (confirm_buf),
		    RPP_ECHO_OFF | RPP_REQUIRE_TTY) == NULL) {
			err(1, "readpassphrase");
		}
		if (strcmp(password_buf, confirm_buf) != 0) {
			fprintf(stderr, "passwords do not match\n");
			return (CMD_ERR);
		}
		memcpy(ctx->password, password_buf, password_len + 1);
	}

	return (CMD_OK);
}

static enum cmd_return
cmd_help(struct pwm_ctx *ctx, int argc, char *argv[])
{
	struct cmd	*cmd;

	if (argc != 1) {
		return (CMD_USAGE);
	}

	printf("Commands:\n");
	for (cmd = cmds; cmd->cmd_func != NULL; cmd++) {
		printf("%-2s %-16s %s\n", cmd->abbrev_cmd, cmd->full_cmd,
		    cmd->description);
	}

	return (CMD_OK);
}

static enum cmd_return
cmd_write(struct pwm_ctx *ctx, int argc, char *argv[])
{
	if (argc != 1) {
		return (CMD_USAGE);
	}

	return ((pwfile_write_file(ctx) == 0) ? CMD_OK : CMD_ERR);
}

static enum cmd_return
cmd_quit(struct pwm_ctx *ctx, int argc, char *argv[])
{
	if (argc != 1) {
		return (CMD_USAGE);
	}

	return (CMD_QUIT);
}

struct cmd *
cmd_match(const char *cmd_name)
{
	size_t	i;

	for (i = 0; cmds[i].cmd_func != NULL; i++) {
		if ((strcmp(cmds[i].abbrev_cmd, cmd_name) == 0) ||
		    (strcmp(cmds[i].full_cmd, cmd_name) == 0)) {
			return (&cmds[i]);
		}
	}

	return (NULL);
}