xref: /openbsd/usr.bin/htpasswd/htpasswd.c (revision 9b7c3dbb)
1 /*	$OpenBSD: htpasswd.c,v 1.15 2015/11/05 20:07:15 florian 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
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
48 main(int argc, char** argv)
49 {
50 	char salt[_PASSWORD_LEN], 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 	if (pledge("stdio rpath wpath cpath flock tmppath tty", NULL) == -1)
61 		err(1, "pledge");
62 
63 	while ((c = getopt(argc, argv, "I")) != -1) {
64 		switch (c) {
65 		case 'I':
66 			batch = 1;
67 			break;
68 		default:
69 			usage();
70 			/* NOT REACHED */
71 			break;
72 		}
73 	}
74 
75 	argc -= optind;
76 	argv += optind;
77 
78 	if (batch) {
79 		if (argc == 1)
80 			file = argv[0];
81 		else if (argc > 1)
82 			usage();
83 		else if (pledge("stdio", NULL) == -1)
84 			err(1, "pledge");
85 
86 		if ((linelen = getline(&line, &linesize, stdin)) == -1)
87 			err(1, "cannot read login:password from stdin");
88 		line[linelen-1] = '\0';
89 
90 		if ((tok = strstr(line, ":")) == NULL)
91 			errx(1, "cannot find ':' in input");
92 		*tok++ = '\0';
93 
94 		if ((loginlen = asprintf(&login, "%s:", line)) == -1)
95 			err(1, "asprintf");
96 
97 		if (strlcpy(pass, tok, sizeof(pass)) >= sizeof(pass))
98 			errx(1, "password too long");
99 	} else {
100 
101 		switch (argc) {
102 		case 1:
103 			if (pledge("stdio tty", NULL) == -1)
104 				err(1, "pledge");
105 			if ((loginlen = asprintf(&login, "%s:", argv[0])) == -1)
106 				err(1, "asprintf");
107 			break;
108 		case 2:
109 			file = argv[0];
110 			if ((loginlen = asprintf(&login, "%s:", argv[1])) == -1)
111 				err(1, "asprintf");
112 			break;
113 		default:
114 			usage();
115 			/* NOT REACHED */
116 			break;
117 		}
118 
119 		if (!readpassphrase("Password: ", pass, sizeof(pass),
120 		    RPP_ECHO_OFF))
121 			err(1, "unable to read password");
122 		if (!readpassphrase("Retype Password: ", pass2, sizeof(pass2),
123 		    RPP_ECHO_OFF)) {
124 			explicit_bzero(pass, sizeof(pass));
125 			err(1, "unable to read password");
126 		}
127 		if (strcmp(pass, pass2) != 0) {
128 			explicit_bzero(pass, sizeof(pass));
129 			explicit_bzero(pass2, sizeof(pass2));
130 			errx(1, "passwords don't match");
131 		}
132 
133 		explicit_bzero(pass2, sizeof(pass2));
134 	}
135 
136 	if (strlcpy(salt, bcrypt_gensalt(8), sizeof(salt)) >= sizeof(salt))
137 		errx(1, "salt too long");
138 	if (strlcpy(hash, bcrypt(pass, salt), sizeof(hash)) >= sizeof(hash))
139 		errx(1, "hash too long");
140 	explicit_bzero(pass, sizeof(pass));
141 
142 	if (file == NULL)
143 		printf("%s%s\n", login, hash);
144 	else {
145 		if ((in = fopen(file, "r+")) == NULL) {
146 			if (errno == ENOENT) {
147 				old_umask = umask(S_IXUSR|
148 				    S_IWGRP|S_IRGRP|S_IXGRP|
149 				    S_IWOTH|S_IROTH|S_IXOTH);
150 				if ((out = fopen(file, "w")) == NULL)
151 					err(1, "cannot open password file for"
152 					    " reading or writing");
153 				umask(old_umask);
154 			} else
155 				err(1, "cannot open password file for"
156 					" reading or writing");
157 		} else
158 			if (flock(fileno(in), LOCK_EX|LOCK_NB) == -1)
159 				errx(1, "cannot lock password file");
160 
161 		/* file already exits, copy content and filter login out */
162 		if (out == NULL) {
163 			strlcpy(tmpl, "/tmp/htpasswd-XXXXXXXXXX", sizeof(tmpl));
164 			if ((fd = mkstemp(tmpl)) == -1)
165 				err(1, "mkstemp");
166 
167 			if ((out = fdopen(fd, "w+")) == NULL)
168 				err(1, "cannot open tempfile");
169 
170 			while ((linelen = getline(&line, &linesize, in))
171 			    != -1) {
172 				if (strncmp(line, login, loginlen) != 0) {
173 					if (fprintf(out, "%s", line) == -1)
174 						errx(1, "cannot write to temp "
175 						    "file");
176 					nag(line);
177 				}
178 			}
179 		}
180 		if (fprintf(out, "%s%s\n", login, hash) == -1)
181 			errx(1, "cannot write new password hash");
182 
183 		/* file already exists, overwrite it */
184 		if (in != NULL) {
185 			if (fseek(in, 0, SEEK_SET) == -1)
186 				err(1, "cannot seek in password file");
187 			if (fseek(out, 0, SEEK_SET) == -1)
188 				err(1, "cannot seek in temp file");
189 			if (ftruncate(fileno(in), 0) == -1)
190 				err(1, "cannot truncate password file");
191 			while ((linelen = getline(&line, &linesize, out))
192 			    != -1)
193 				if (fprintf(in, "%s", line) == -1)
194 					errx(1, "cannot write to password "
195 					    "file");
196 			if (fclose(in) == EOF)
197 				err(1, "cannot close password file");
198 		}
199 		if (fclose(out) == EOF) {
200 			if (in != NULL)
201 				err(1, "cannot close temp file");
202 			else
203 				err(1, "cannot close password file");
204 		}
205 		if (in != NULL && unlink(tmpl) == -1)
206 			err(1, "cannot delete temp file (%s)", tmpl);
207 	}
208 	if (nagcount >= MAXNAG)
209 		warnx("%d more logins not using bcryt.", nagcount - MAXNAG);
210 	exit(0);
211 }
212 
213 void
214 nag(char* line)
215 {
216 	const char *tok;
217 	if (strtok(line, ":") != NULL)
218 		if ((tok = strtok(NULL, ":")) != NULL)
219 			if (strncmp(tok, "$2a$", 4) != 0 &&
220 			     strncmp(tok, "$2b$", 4) != 0) {
221 				nagcount++;
222 				if (nagcount <= MAXNAG)
223 					warnx("%s doesn't use bcrypt."
224 					    " Update the password.", line);
225 			}
226 }
227