1/*	$OpenBSD: htpasswd.c,v 1.18 2021/07/12 15:09:19 beck Exp $ */
2/*
3 * Copyright (c) 2014 Florian Obser <florian@openbsd.org>
4 *
5 * Permission to use, copy, modify, and distribute this software for any
6 * purpose with or without fee is hereby granted, provided that the above
7 * copyright notice and this permission notice appear in all copies.
8 *
9 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16 */
17
18#include <sys/stat.h>
19
20#include <err.h>
21#include <errno.h>
22#include <fcntl.h>
23#include <limits.h>
24#include <pwd.h>
25#include <readpassphrase.h>
26#include <stdio.h>
27#include <stdlib.h>
28#include <string.h>
29#include <unistd.h>
30
31__dead void	usage(void);
32void		nag(char*);
33
34extern char *__progname;
35
36__dead void
37usage(void)
38{
39	fprintf(stderr, "usage:\t%s [file] login\n", __progname);
40	fprintf(stderr, "\t%s -I [file]\n", __progname);
41	exit(1);
42}
43
44#define MAXNAG 5
45int nagcount;
46
47int
48main(int argc, char** argv)
49{
50	char tmpl[sizeof("/tmp/htpasswd-XXXXXXXXXX")];
51	char hash[_PASSWORD_LEN], pass[1024], pass2[1024];
52	char *line = NULL, *login = NULL, *tok;
53	int c, fd, loginlen, batch = 0;
54	FILE *in = NULL, *out = NULL;
55	const char *file = NULL;
56	size_t linesize = 0;
57	ssize_t linelen;
58	mode_t old_umask;
59
60	while ((c = getopt(argc, argv, "I")) != -1) {
61		switch (c) {
62		case 'I':
63			batch = 1;
64			break;
65		default:
66			usage();
67			/* NOT REACHED */
68			break;
69		}
70	}
71
72	argc -= optind;
73	argv += optind;
74
75	if ((batch && argc == 1) || (!batch && argc == 2)) {
76		if (unveil(argv[0], "rwc") == -1)
77			err(1, "unveil %s", argv[0]);
78		if (unveil("/tmp", "rwc") == -1)
79			err(1, "unveil /tmp");
80	}
81	if (pledge("stdio rpath wpath cpath flock tmppath tty", NULL) == -1)
82		err(1, "pledge");
83
84	if (batch) {
85		if (argc == 1)
86			file = argv[0];
87		else if (argc > 1)
88			usage();
89		else if (pledge("stdio", NULL) == -1)
90			err(1, "pledge");
91
92		if ((linelen = getline(&line, &linesize, stdin)) == -1)
93			err(1, "cannot read login:password from stdin");
94		line[linelen-1] = '\0';
95
96		if ((tok = strstr(line, ":")) == NULL)
97			errx(1, "cannot find ':' in input");
98		*tok++ = '\0';
99
100		if ((loginlen = asprintf(&login, "%s:", line)) == -1)
101			err(1, "asprintf");
102
103		if (strlcpy(pass, tok, sizeof(pass)) >= sizeof(pass))
104			errx(1, "password too long");
105	} else {
106
107		switch (argc) {
108		case 1:
109			if (pledge("stdio tty", NULL) == -1)
110				err(1, "pledge");
111			if ((loginlen = asprintf(&login, "%s:", argv[0])) == -1)
112				err(1, "asprintf");
113			break;
114		case 2:
115			file = argv[0];
116			if ((loginlen = asprintf(&login, "%s:", argv[1])) == -1)
117				err(1, "asprintf");
118			break;
119		default:
120			usage();
121			/* NOT REACHED */
122			break;
123		}
124
125		if (!readpassphrase("Password: ", pass, sizeof(pass),
126		    RPP_ECHO_OFF))
127			err(1, "unable to read password");
128		if (!readpassphrase("Retype Password: ", pass2, sizeof(pass2),
129		    RPP_ECHO_OFF)) {
130			explicit_bzero(pass, sizeof(pass));
131			err(1, "unable to read password");
132		}
133		if (strcmp(pass, pass2) != 0) {
134			explicit_bzero(pass, sizeof(pass));
135			explicit_bzero(pass2, sizeof(pass2));
136			errx(1, "passwords don't match");
137		}
138
139		explicit_bzero(pass2, sizeof(pass2));
140	}
141
142	if (crypt_newhash(pass, "bcrypt,a", hash, sizeof(hash)) != 0)
143		err(1, "can't generate hash");
144	explicit_bzero(pass, sizeof(pass));
145
146	if (file == NULL)
147		printf("%s%s\n", login, hash);
148	else {
149		if ((in = fopen(file, "r+")) == NULL) {
150			if (errno == ENOENT) {
151				old_umask = umask(S_IXUSR|
152				    S_IWGRP|S_IRGRP|S_IXGRP|
153				    S_IWOTH|S_IROTH|S_IXOTH);
154				if ((out = fopen(file, "w")) == NULL)
155					err(1, "cannot open password file for"
156					    " reading or writing");
157				umask(old_umask);
158			} else
159				err(1, "cannot open password file for"
160					" reading or writing");
161		} else
162			if (flock(fileno(in), LOCK_EX|LOCK_NB) == -1)
163				errx(1, "cannot lock password file");
164
165		/* file already exits, copy content and filter login out */
166		if (out == NULL) {
167			strlcpy(tmpl, "/tmp/htpasswd-XXXXXXXXXX", sizeof(tmpl));
168			if ((fd = mkstemp(tmpl)) == -1)
169				err(1, "mkstemp");
170
171			if ((out = fdopen(fd, "w+")) == NULL)
172				err(1, "cannot open tempfile");
173
174			while ((linelen = getline(&line, &linesize, in))
175			    != -1) {
176				if (strncmp(line, login, loginlen) != 0) {
177					if (fprintf(out, "%s", line) == -1)
178						errx(1, "cannot write to temp "
179						    "file");
180					nag(line);
181				}
182			}
183		}
184		if (fprintf(out, "%s%s\n", login, hash) == -1)
185			errx(1, "cannot write new password hash");
186
187		/* file already exists, overwrite it */
188		if (in != NULL) {
189			if (fseek(in, 0, SEEK_SET) == -1)
190				err(1, "cannot seek in password file");
191			if (fseek(out, 0, SEEK_SET) == -1)
192				err(1, "cannot seek in temp file");
193			if (ftruncate(fileno(in), 0) == -1)
194				err(1, "cannot truncate password file");
195			while ((linelen = getline(&line, &linesize, out))
196			    != -1)
197				if (fprintf(in, "%s", line) == -1)
198					errx(1, "cannot write to password "
199					    "file");
200			if (fclose(in) == EOF)
201				err(1, "cannot close password file");
202		}
203		if (fclose(out) == EOF) {
204			if (in != NULL)
205				err(1, "cannot close temp file");
206			else
207				err(1, "cannot close password file");
208		}
209		if (in != NULL && unlink(tmpl) == -1)
210			err(1, "cannot delete temp file (%s)", tmpl);
211	}
212	if (nagcount >= MAXNAG)
213		warnx("%d more logins not using bcryt.", nagcount - MAXNAG);
214	exit(0);
215}
216
217void
218nag(char* line)
219{
220	const char *tok;
221	if (strtok(line, ":") != NULL)
222		if ((tok = strtok(NULL, ":")) != NULL)
223			if (strncmp(tok, "$2a$", 4) != 0 &&
224			     strncmp(tok, "$2b$", 4) != 0) {
225				nagcount++;
226				if (nagcount <= MAXNAG)
227					warnx("%s doesn't use bcrypt."
228					    " Update the password.", line);
229			}
230}
231