xref: /openbsd/usr.bin/htpasswd/htpasswd.c (revision 8529ddd3)
1 /*	$OpenBSD: htpasswd.c,v 1.11 2015/02/08 23:40:34 deraadt 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 	FILE *in, *out;
51 	size_t linesize;
52 	ssize_t linelen;
53 	mode_t old_umask;
54 	int c, fd, loginlen, batch;
55 	char hash[_PASSWORD_LEN], *line, *login, pass[1024], pass2[1024];
56 	char salt[_PASSWORD_LEN], tmpl[sizeof("/tmp/htpasswd-XXXXXXXXXX")];
57 	char *tok;
58 	const char *file;
59 
60 	file = NULL;
61 	login = NULL;
62 	in = NULL;
63 	out = NULL;
64 	line = NULL;
65 	linesize = 0;
66 	batch = 0;
67 
68 	while ((c = getopt(argc, argv, "I")) != -1) {
69 		switch (c) {
70 		case 'I':
71 			batch = 1;
72 			break;
73 		default:
74 			usage();
75 			/* NOT REACHED */
76 			break;
77 		}
78 	}
79 
80 	argc -= optind;
81 	argv += optind;
82 
83 	if (batch) {
84 		if (argc == 1)
85 			file = argv[0];
86 		else if (argc > 1)
87 			usage();
88 		if ((linelen = getline(&line, &linesize, stdin)) == -1)
89 			err(1, "cannot read login:password from stdin");
90 		line[linelen-1] = '\0';
91 
92 		if ((tok = strstr(line, ":")) == NULL)
93 			errx(1, "cannot find ':' in input");
94 		*tok++ = '\0';
95 
96 		if ((loginlen = asprintf(&login, "%s:", line)) == -1)
97 			err(1, "asprintf");
98 
99 		if (strlcpy(pass, tok, sizeof(pass)) >= sizeof(pass))
100 			errx(1, "password too long");
101 	} else {
102 
103 		switch (argc) {
104 		case 1:
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