xref: /openbsd-src/usr.bin/htpasswd/htpasswd.c (revision bc5a8259a456844c67e446bf6bd66575acc64837)
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);
32 void		nag(char*);
33 
34 extern char *__progname;
35 
36 __dead void
usage(void)37 usage(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
45 int nagcount;
46 
47 int
main(int argc,char ** argv)48 main(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 
217 void
nag(char * line)218 nag(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