1 /* OpenBSD S/Key (skeylogin.c)
2 *
3 * Authors:
4 * Neil M. Haller <nmh@thumper.bellcore.com>
5 * Philip R. Karn <karn@chicago.qualcomm.com>
6 * John S. Walden <jsw@thumper.bellcore.com>
7 * Scott Chasin <chasin@crimelab.com>
8 * Todd C. Miller <Todd.Miller@courtesan.com>
9 * Angelos D. Keromytis <adk@adk.gr>
10 *
11 * S/Key verification check, lookups, and authentication.
12 *
13 * $OpenBSD: skeylogin.c,v 1.52 2004/08/05 13:31:36 millert Exp $
14 */
15
16 #include <sys/param.h>
17 #ifdef QUOTA
18 #include <sys/quota.h>
19 #endif
20 #include <sys/stat.h>
21 #include <sys/time.h>
22 #include <sys/resource.h>
23
24 #include <ctype.h>
25 #include <err.h>
26 #include <errno.h>
27 #include <fcntl.h>
28 #include <paths.h>
29 #include <poll.h>
30 #include <stdio.h>
31 #include <stdlib.h>
32 #include <string.h>
33 #include <time.h>
34 #include <unistd.h>
35 #include <sha1.h>
36
37 #include "skey.h"
38
39 static void skey_fakeprompt(char *, char *);
40 static char *tgetline(int, char *, size_t, int);
41 static int skeygetent(int, struct skey *, const char *);
42
43 /*
44 * Return an skey challenge string for user 'name'. If successful,
45 * fill in the caller's skey structure and return (0). If unsuccessful
46 * (e.g., if name is unknown) return (-1).
47 *
48 * The file read/write pointer is left at the start of the record.
49 */
50 int
skeychallenge2(int fd,struct skey * mp,char * name,char * ss)51 skeychallenge2(int fd, struct skey *mp, char *name, char *ss)
52 {
53 int rval;
54
55 memset(mp, 0, sizeof(*mp));
56 rval = skeygetent(fd, mp, name);
57
58 switch (rval) {
59 case 0: /* Lookup succeeded, return challenge */
60 (void)snprintf(ss, SKEY_MAX_CHALLENGE,
61 "otp-%.*s %d %.*s", SKEY_MAX_HASHNAME_LEN,
62 skey_get_algorithm(), mp->n - 1,
63 SKEY_MAX_SEED_LEN, mp->seed);
64 return (0);
65
66 case 1: /* User not found */
67 if (mp->keyfile) {
68 (void)fclose(mp->keyfile);
69 mp->keyfile = NULL;
70 }
71 /* FALLTHROUGH */
72
73 default: /* File error */
74 skey_fakeprompt(name, ss);
75 return (-1);
76 }
77 }
78
79 int
skeychallenge(struct skey * mp,char * name,char * ss)80 skeychallenge(struct skey *mp, char *name, char *ss)
81 {
82 return (skeychallenge2(-1, mp, name, ss));
83 }
84
85 /*
86 * Get an entry in the One-time Password database and lock it.
87 *
88 * Return codes:
89 * -1: error in opening database or unable to lock entry
90 * 0: entry found, file R/W pointer positioned at beginning of record
91 * 1: entry not found
92 */
93 static int
skeygetent(int fd,struct skey * mp,const char * name)94 skeygetent(int fd, struct skey *mp, const char *name)
95 {
96 struct stat statbuf;
97 size_t nread;
98 char *cp, filename[PATH_MAX], *last;
99 FILE *keyfile;
100
101 /* Check to see that /etc/skey has not been disabled. */
102 if (stat(_PATH_SKEYDIR, &statbuf) != 0)
103 return (-1);
104 if ((statbuf.st_mode & ALLPERMS) == 0) {
105 errno = EPERM;
106 return (-1);
107 }
108
109 if (fd == -1) {
110 /* Open the user's databse entry, creating it as needed. */
111 if (snprintf(filename, sizeof(filename), "%s/%s", _PATH_SKEYDIR,
112 name) >= sizeof(filename)) {
113 errno = ENAMETOOLONG;
114 return (-1);
115 }
116 if ((fd = open(filename, O_RDWR | O_NOFOLLOW | O_NONBLOCK,
117 S_IRUSR | S_IWUSR)) == -1) {
118 if (errno == ENOENT)
119 goto not_found;
120 return (-1);
121 }
122 }
123
124 /* Lock and stat the user's skey file. */
125 if (flock(fd, LOCK_EX) != 0 || fstat(fd, &statbuf) != 0) {
126 close(fd);
127 return (-1);
128 }
129 if (statbuf.st_size == 0)
130 goto not_found;
131
132 /* Sanity checks. */
133 if ((statbuf.st_mode & ALLPERMS) != (S_IRUSR | S_IWUSR) ||
134 !S_ISREG(statbuf.st_mode) || statbuf.st_nlink != 1 ||
135 (keyfile = fdopen(fd, "r+")) == NULL) {
136 close(fd);
137 return (-1);
138 }
139
140 /* At this point, we are committed. */
141 mp->keyfile = keyfile;
142
143 if ((nread = fread(mp->buf, 1, sizeof(mp->buf), keyfile)) == 0 ||
144 !isspace(mp->buf[nread - 1]))
145 goto bad_keyfile;
146 mp->buf[nread - 1] = '\0';
147
148 if ((mp->logname = strtok_r(mp->buf, " \t\n\r", &last)) == NULL ||
149 strcmp(mp->logname, name) != 0)
150 goto bad_keyfile;
151 if ((cp = strtok_r(NULL, " \t\n\r", &last)) == NULL)
152 goto bad_keyfile;
153 if (skey_set_algorithm(cp) == NULL)
154 goto bad_keyfile;
155 if ((cp = strtok_r(NULL, " \t\n\r", &last)) == NULL)
156 goto bad_keyfile;
157 mp->n = atoi(cp); /* XXX - use strtol() */
158 if ((mp->seed = strtok_r(NULL, " \t\n\r", &last)) == NULL)
159 goto bad_keyfile;
160 if ((mp->val = strtok_r(NULL, " \t\n\r", &last)) == NULL)
161 goto bad_keyfile;
162
163 (void)fseek(keyfile, 0L, SEEK_SET);
164 return (0);
165
166 bad_keyfile:
167 fclose(keyfile);
168 return (-1);
169
170 not_found:
171 /* No existing entry, fill in what we can and return */
172 memset(mp, 0, sizeof(*mp));
173 strlcpy(mp->buf, name, sizeof(mp->buf));
174 mp->logname = mp->buf;
175 if (fd != -1)
176 close(fd);
177 return (1);
178 }
179
180 /*
181 * Look up an entry in the One-time Password database and lock it.
182 * Zeroes out the passed in struct skey before using it.
183 *
184 * Return codes:
185 * -1: error in opening database or unable to lock entry
186 * 0: entry found, file R/W pointer positioned at beginning of record
187 * 1: entry not found
188 */
189 int
skeylookup(struct skey * mp,char * name)190 skeylookup(struct skey *mp, char *name)
191 {
192 memset(mp, 0, sizeof(*mp));
193 return (skeygetent(-1, mp, name));
194 }
195
196 /*
197 * Get the next entry in the One-time Password database.
198 *
199 * Return codes:
200 * -1: error in opening database
201 * 0: next entry found and stored in mp
202 * 1: no more entries, keydir is closed.
203 */
204 int
skeygetnext(struct skey * mp)205 skeygetnext(struct skey *mp)
206 {
207 struct dirent entry, *dp;
208 int rval;
209
210 if (mp->keyfile != NULL) {
211 fclose(mp->keyfile);
212 mp->keyfile = NULL;
213 }
214
215 /* Open _PATH_SKEYDIR if it exists, else return an error */
216 if (mp->keydir == NULL && (mp->keydir = opendir(_PATH_SKEYDIR)) == NULL)
217 return (-1);
218
219 rval = 1;
220 while ((readdir_r(mp->keydir, &entry, &dp)) == 0 && dp == &entry) {
221 /* Skip dot files and zero-length files. */
222 if (entry.d_name[0] != '.' &&
223 (rval = skeygetent(-1, mp, entry.d_name)) != 1)
224 break;
225 }
226
227 if (dp == NULL) {
228 closedir(mp->keydir);
229 mp->keydir = NULL;
230 }
231
232 return (rval);
233 }
234
235 /*
236 * Verify response to a S/Key challenge.
237 *
238 * Return codes:
239 * -1: Error of some sort; database unchanged
240 * 0: Verify successful, database updated
241 * 1: Verify failed, database unchanged
242 *
243 * The database file is always closed by this call.
244 */
245 int
skeyverify(struct skey * mp,char * response)246 skeyverify(struct skey *mp, char *response)
247 {
248 char key[SKEY_BINKEY_SIZE];
249 char fkey[SKEY_BINKEY_SIZE];
250 char filekey[SKEY_BINKEY_SIZE];
251 size_t nread;
252 char *cp, *last;
253
254 if (response == NULL)
255 goto verify_failure;
256
257 /*
258 * The record should already be locked but lock it again
259 * just to be safe. We don't wait for the lock to become
260 * available since we should already have it...
261 */
262 if (flock(fileno(mp->keyfile), LOCK_EX | LOCK_NB) != 0)
263 goto verify_failure;
264
265 /* Convert response to binary */
266 rip(response);
267 if (etob(key, response) != 1 && atob8(key, response) != 0)
268 goto verify_failure; /* Neither english words nor ascii hex */
269
270 /* Compute fkey = f(key) */
271 (void)memcpy(fkey, key, sizeof(key));
272 f(fkey);
273
274 /*
275 * Reread the file record NOW in case it has been modified.
276 * The only field we really need to worry about is mp->val.
277 */
278 (void)fseek(mp->keyfile, 0L, SEEK_SET);
279 if ((nread = fread(mp->buf, 1, sizeof(mp->buf), mp->keyfile)) == 0 ||
280 !isspace(mp->buf[nread - 1]))
281 goto verify_failure;
282 if ((mp->logname = strtok_r(mp->buf, " \t\r\n", &last)) == NULL)
283 goto verify_failure;
284 if ((cp = strtok_r(NULL, " \t\r\n", &last)) == NULL)
285 goto verify_failure;
286 if ((cp = strtok_r(NULL, " \t\r\n", &last)) == NULL)
287 goto verify_failure;
288 if ((mp->seed = strtok_r(NULL, " \t\r\n", &last)) == NULL)
289 goto verify_failure;
290 if ((mp->val = strtok_r(NULL, " \t\r\n", &last)) == NULL)
291 goto verify_failure;
292
293 /* Convert file value to hex and compare. */
294 atob8(filekey, mp->val);
295 if (memcmp(filekey, fkey, SKEY_BINKEY_SIZE) != 0)
296 goto verify_failure; /* Wrong response */
297
298 /*
299 * Update key in database.
300 * XXX - check return values of things that write to disk.
301 */
302 btoa8(mp->val,key);
303 mp->n--;
304 (void)fseek(mp->keyfile, 0L, SEEK_SET);
305 (void)fprintf(mp->keyfile, "%s\n%s\n%d\n%s\n%s\n", mp->logname,
306 skey_get_algorithm(), mp->n, mp->seed, mp->val);
307 (void)fflush(mp->keyfile);
308 (void)ftruncate(fileno(mp->keyfile), ftello(mp->keyfile));
309 (void)fclose(mp->keyfile);
310 mp->keyfile = NULL;
311 return (0);
312
313 verify_failure:
314 (void)fclose(mp->keyfile);
315 mp->keyfile = NULL;
316 return (-1);
317 }
318
319 /*
320 * skey_haskey()
321 *
322 * Returns: 1 user doesn't exist, -1 file error, 0 user exists.
323 *
324 */
325 int
skey_haskey(char * username)326 skey_haskey(char *username)
327 {
328 struct skey skey;
329 int i;
330
331 i = skeylookup(&skey, username);
332 if (skey.keyfile != NULL) {
333 fclose(skey.keyfile);
334 skey.keyfile = NULL;
335 }
336 return (i);
337 }
338
339 /*
340 * skey_keyinfo()
341 *
342 * Returns the current sequence number and
343 * seed for the passed user.
344 *
345 */
346 char *
skey_keyinfo(char * username)347 skey_keyinfo(char *username)
348 {
349 int i;
350 static char str[SKEY_MAX_CHALLENGE];
351 struct skey skey;
352
353 i = skeychallenge(&skey, username, str);
354 if (i == -1)
355 return (0);
356
357 if (skey.keyfile != NULL) {
358 fclose(skey.keyfile);
359 skey.keyfile = NULL;
360 }
361 return (str);
362 }
363
364 /*
365 * skey_passcheck()
366 *
367 * Check to see if answer is the correct one to the current
368 * challenge.
369 *
370 * Returns: 0 success, -1 failure
371 *
372 */
373 int
skey_passcheck(char * username,char * passwd)374 skey_passcheck(char *username, char *passwd)
375 {
376 int i;
377 struct skey skey;
378
379 i = skeylookup(&skey, username);
380 if (i == -1 || i == 1)
381 return (-1);
382
383 if (skeyverify(&skey, passwd) == 0)
384 return (skey.n);
385
386 return (-1);
387 }
388
389 #define ROUND(x) (((x)[0] << 24) + (((x)[1]) << 16) + (((x)[2]) << 8) + \
390 ((x)[3]))
391
392 /*
393 * hash_collapse()
394 */
395 static u_int32_t
hash_collapse(u_char * s)396 hash_collapse(u_char *s)
397 {
398 int len, target;
399 u_int32_t i;
400
401 if ((strlen(s) % sizeof(u_int32_t)) == 0)
402 target = strlen(s); /* Multiple of 4 */
403 else
404 target = strlen(s) - (strlen(s) % sizeof(u_int32_t));
405
406 for (i = 0, len = 0; len < target; len += 4)
407 i ^= ROUND(s + len);
408
409 return i;
410 }
411
412 /*
413 * skey_fakeprompt()
414 *
415 * Generate a fake prompt for the specified user.
416 *
417 */
418 static void
skey_fakeprompt(char * username,char * skeyprompt)419 skey_fakeprompt(char *username, char *skeyprompt)
420 {
421 int i;
422 u_int ptr;
423 u_char hseed[SKEY_MAX_SEED_LEN], flg = 1, *up;
424 char *secret, pbuf[SKEY_MAX_PW_LEN+1];
425 char *p, *u;
426 size_t secretlen;
427 SHA1_CTX ctx;
428
429 /*
430 * Base first 4 chars of seed on hostname.
431 * Add some filler for short hostnames if necessary.
432 */
433 if (gethostname(pbuf, sizeof(pbuf)) == -1)
434 *(p = pbuf) = '.';
435 else
436 for (p = pbuf; *p && isalnum(*p); p++)
437 if (isalpha(*p) && isupper(*p))
438 *p = tolower(*p);
439 if (*p && pbuf - p < 4)
440 (void)strncpy(p, "asjd", 4 - (pbuf - p));
441 pbuf[4] = '\0';
442
443 /* Hash the username if possible */
444 if ((up = SHA1Data(username, strlen(username), NULL)) != NULL) {
445 struct stat sb;
446 time_t t;
447 int fd;
448
449 /* Collapse the hash */
450 ptr = hash_collapse(up);
451 memset(up, 0, strlen(up));
452
453 /* See if the random file's there, else use ctime */
454 if ((fd = open(_SKEY_RAND_FILE_PATH_, O_RDONLY)) != -1
455 && fstat(fd, &sb) == 0 &&
456 sb.st_size > (off_t)SKEY_MAX_SEED_LEN &&
457 lseek(fd, ptr % (sb.st_size - SKEY_MAX_SEED_LEN),
458 SEEK_SET) != -1 && read(fd, hseed,
459 SKEY_MAX_SEED_LEN) == SKEY_MAX_SEED_LEN) {
460 close(fd);
461 fd = -1;
462 secret = hseed;
463 secretlen = SKEY_MAX_SEED_LEN;
464 flg = 0;
465 } else if (!stat(_PATH_MEM, &sb) || !stat("/", &sb)) {
466 t = sb.st_ctime;
467 secret = ctime(&t);
468 secretlen = strlen(secret);
469 flg = 0;
470 }
471 if (fd != -1)
472 close(fd);
473 }
474
475 /* Put that in your pipe and smoke it */
476 if (flg == 0) {
477 /* Hash secret value with username */
478 SHA1Init(&ctx);
479 SHA1Update(&ctx, secret, secretlen);
480 SHA1Update(&ctx, username, strlen(username));
481 SHA1End(&ctx, up);
482
483 /* Zero out */
484 memset(secret, 0, secretlen);
485
486 /* Now hash the hash */
487 SHA1Init(&ctx);
488 SHA1Update(&ctx, up, strlen(up));
489 SHA1End(&ctx, up);
490
491 ptr = hash_collapse(up + 4);
492
493 for (i = 4; i < 9; i++) {
494 pbuf[i] = (ptr % 10) + '0';
495 ptr /= 10;
496 }
497 pbuf[i] = '\0';
498
499 /* Sequence number */
500 ptr = ((up[2] + up[3]) % 99) + 1;
501
502 memset(up, 0, 20); /* SHA1 specific */
503 free(up);
504
505 (void)snprintf(skeyprompt, SKEY_MAX_CHALLENGE,
506 "otp-%.*s %d %.*s", SKEY_MAX_HASHNAME_LEN,
507 skey_get_algorithm(), ptr, SKEY_MAX_SEED_LEN, pbuf);
508 } else {
509 /* Base last 8 chars of seed on username */
510 u = username;
511 i = 8;
512 p = &pbuf[4];
513 do {
514 if (*u == 0) {
515 /* Pad remainder with zeros */
516 while (--i >= 0)
517 *p++ = '0';
518 break;
519 }
520
521 *p++ = (*u++ % 10) + '0';
522 } while (--i != 0);
523 pbuf[12] = '\0';
524
525 (void)snprintf(skeyprompt, SKEY_MAX_CHALLENGE,
526 "otp-%.*s %d %.*s", SKEY_MAX_HASHNAME_LEN,
527 skey_get_algorithm(), 99, SKEY_MAX_SEED_LEN, pbuf);
528 }
529 }
530
531 /*
532 * skey_authenticate()
533 *
534 * Used when calling program will allow input of the user's
535 * response to the challenge.
536 *
537 * Returns: 0 success, -1 failure
538 *
539 */
540 int
skey_authenticate(char * username)541 skey_authenticate(char *username)
542 {
543 int i;
544 char pbuf[SKEY_MAX_PW_LEN+1], skeyprompt[SKEY_MAX_CHALLENGE+1];
545 struct skey skey;
546
547 /* Get the S/Key challenge (may be fake) */
548 i = skeychallenge(&skey, username, skeyprompt);
549 (void)fprintf(stderr, "%s\nResponse: ", skeyprompt);
550 (void)fflush(stderr);
551
552 /* Time out on user input after 2 minutes */
553 tgetline(fileno(stdin), pbuf, sizeof(pbuf), 120);
554 sevenbit(pbuf);
555 (void)rewind(stdin);
556
557 /* Is it a valid response? */
558 if (i == 0 && skeyverify(&skey, pbuf) == 0) {
559 if (skey.n < 5) {
560 (void)fprintf(stderr,
561 "\nWarning! Key initialization needed soon. (%d logins left)\n",
562 skey.n);
563 }
564 return (0);
565 }
566 return (-1);
567 }
568
569 /*
570 * Unlock current entry in the One-time Password database.
571 *
572 * Return codes:
573 * -1: unable to lock the record
574 * 0: record was successfully unlocked
575 */
576 int
skey_unlock(struct skey * mp)577 skey_unlock(struct skey *mp)
578 {
579 if (mp->logname == NULL || mp->keyfile == NULL)
580 return (-1);
581
582 return (flock(fileno(mp->keyfile), LOCK_UN));
583 }
584
585 /*
586 * Get a line of input (optionally timing out) and place it in buf.
587 */
588 static char *
tgetline(int fd,char * buf,size_t bufsiz,int timeout)589 tgetline(int fd, char *buf, size_t bufsiz, int timeout)
590 {
591 struct pollfd pfd[1];
592 size_t left;
593 char c, *cp;
594 int n;
595
596 if (bufsiz == 0)
597 return (NULL); /* sanity */
598
599 cp = buf;
600 left = bufsiz;
601
602 /*
603 * Timeout of <= 0 means no timeout.
604 */
605 if (timeout > 0) {
606 timeout *= 1000; /* convert to miliseconds */
607
608 pfd[0].fd = fd;
609 pfd[0].events = POLLIN;
610 while (--left) {
611 /* Poll until we are ready or we time out */
612 while ((n = poll(pfd, 1, timeout)) == -1 &&
613 (errno == EINTR || errno == EAGAIN))
614 ;
615 if (n <= 0 ||
616 (pfd[0].revents & (POLLERR|POLLHUP|POLLNVAL)))
617 break; /* timeout or error */
618
619 /* Read a character, exit loop on error, EOF or EOL */
620 n = read(fd, &c, 1);
621 if (n != 1 || c == '\n' || c == '\r')
622 break;
623 *cp++ = c;
624 }
625 } else {
626 /* Keep reading until out of space, EOF, error, or newline */
627 while (--left && (n = read(fd, &c, 1)) == 1 && c != '\n' && c != '\r')
628 *cp++ = c;
629 }
630 *cp = '\0';
631
632 return (cp == buf ? NULL : buf);
633 }
634