7 * 0.9. switch to using a distance algorithm in similar()
8 * 0.86. added support for setting minimum numbers of digits, uppers,
10 * 0.85. added six new options to use this with long passwords.
11 * 0.8. tidied output and improved D(()) usage for debugging.
12 * 0.7. added support for more obscure checks for new passwd.
13 * 0.6. root can reset user passwd to any values (it's only warned)
14 * 0.5. supports retries - 'retry=N' argument
15 * 0.4. added argument 'type=XXX' for 'New XXX password' prompt
16 * 0.3. Added argument 'debug'
17 * 0.2. new password is feeded to cracklib for verify after typed once
22 * Written by Cristian Gafton <gafton@redhat.com> 1996/09/10
23 * Long password support by Philip W. Dalrymple <pwd@mdtsoft.com> 1997/07/18
24 * See the end of the file for Copyright Information
26 * Modification for long password systems (>8 chars). The original
27 * module had problems when used in a md5 password system in that it
28 * allowed too short passwords but required that at least half of the
29 * bytes in the new password did not appear in the old one. this
30 * action is still the default and the changes should not break any
31 * current user. This modification adds 6 new options, one to set the
32 * number of bytes in the new password that are not in the old one,
33 * the other five to control the length checking, these are all
34 * documented (or will be before anyone else sees this code) in the PAM
35 * S.A.G. in the section on the cracklib module.
49 #include <sys/types.h>
57 extern char *FascistCheck(char *pw, const char *dictpath);
60 /* For Translators: "%s%s" could be replaced with "<service> " or "". */
61 #define PROMPT1 _("New %s%spassword: ")
62 /* For Translators: "%s%s" could be replaced with "<service> " or "". */
63 #define PROMPT2 _("Retype new %s%spassword: ")
64 #define MISTYPED_PASS _("Sorry, passwords do not match.")
69 #define MIN(_a, _b) (((_a) < (_b)) ? (_a) : (_b))
72 * here, we make a definition for the externally accessible function
73 * in this file (this definition is required for static a module
74 * but strongly encouraged generally) it is used to instruct the
75 * modules include file to define the function prototypes.
78 #define PAM_SM_PASSWORD
80 #include <security/pam_modules.h>
81 #include <security/_pam_macros.h>
82 #include <security/pam_ext.h>
86 static void _pam_log(int err, const char *format, ...)
90 va_start(args, format);
91 openlog("PAM-Cracklib", LOG_CONS|LOG_PID, LOG_AUTH);
92 vsyslog(err, format, args);
97 /* argument parsing */
98 #define PAM_DEBUG_ARG 0x0001
100 struct cracklib_options {
110 char prompt_type[BUFSIZ];
111 char cracklib_dictpath[PATH_MAX];
114 #define CO_RETRY_TIMES 1
116 #define CO_DIFF_IGNORE 23
117 #define CO_MIN_LENGTH 9
118 # define CO_MIN_LENGTH_BASE 5
119 #define CO_DIG_CREDIT 1
120 #define CO_UP_CREDIT 1
121 #define CO_LOW_CREDIT 1
122 #define CO_OTH_CREDIT 1
123 #define CO_USE_AUTHTOK 0
125 static int _pam_parse(struct cracklib_options *opt, int argc, const char **argv)
129 /* step through arguments */
130 for (ctrl=0; argc-- > 0; ++argv) {
133 /* generic options */
135 if (!strcmp(*argv,"debug"))
136 ctrl |= PAM_DEBUG_ARG;
137 else if (!strncmp(*argv,"type=",5))
138 strncpy(opt->prompt_type, *argv+5, sizeof(opt->prompt_type) - 1);
139 else if (!strncmp(*argv,"retry=",6)) {
140 opt->retry_times = strtol(*argv+6,&ep,10);
141 if (!ep || (opt->retry_times < 1))
142 opt->retry_times = CO_RETRY_TIMES;
143 } else if (!strncmp(*argv,"difok=",6)) {
144 opt->diff_ok = strtol(*argv+6,&ep,10);
145 if (!ep || (opt->diff_ok < 0))
146 opt->diff_ok = CO_DIFF_OK;
147 } else if (!strncmp(*argv,"difignore=",10)) {
148 opt->diff_ignore = strtol(*argv+10,&ep,10);
149 if (!ep || (opt->diff_ignore < 0))
150 opt->diff_ignore = CO_DIFF_IGNORE;
151 } else if (!strncmp(*argv,"minlen=",7)) {
152 opt->min_length = strtol(*argv+7,&ep,10);
153 if (!ep || (opt->min_length < CO_MIN_LENGTH_BASE))
154 opt->min_length = CO_MIN_LENGTH_BASE;
155 } else if (!strncmp(*argv,"dcredit=",8)) {
156 opt->dig_credit = strtol(*argv+8,&ep,10);
159 } else if (!strncmp(*argv,"ucredit=",8)) {
160 opt->up_credit = strtol(*argv+8,&ep,10);
163 } else if (!strncmp(*argv,"lcredit=",8)) {
164 opt->low_credit = strtol(*argv+8,&ep,10);
167 } else if (!strncmp(*argv,"ocredit=",8)) {
168 opt->oth_credit = strtol(*argv+8,&ep,10);
171 } else if (!strncmp(*argv,"use_authtok",11)) {
172 opt->use_authtok = 1;
173 } else if (!strncmp(*argv,"dictpath=",9)) {
174 strncpy(opt->cracklib_dictpath, *argv+9,
175 sizeof(opt->cracklib_dictpath) - 1);
177 _pam_log(LOG_ERR,"pam_parse: unknown option; %s",*argv);
180 opt->prompt_type[sizeof(opt->prompt_type) - 1] = '\0';
181 opt->cracklib_dictpath[sizeof(opt->cracklib_dictpath) - 1] = '\0';
186 /* Helper functions */
188 /* use this to free strings. ESPECIALLY password strings */
189 static char *_pam_delete(register char *xx)
197 * can't be a palindrome - like `R A D A R' or `M A D A M'
199 static int palindrome(const char *new)
205 for (j = 0;j < i;j++)
206 if (new[i - j - 1] != new[j])
213 * Calculate how different two strings are in terms of the number of
214 * character removals, additions, and changes needed to go from one to
218 static int distdifferent(const char *old, const char *new,
223 if ((i == 0) || (strlen(old) < i)) {
228 if ((j == 0) || (strlen(new) < j)) {
236 static int distcalculate(int **distances, const char *old, const char *new,
241 if (distances[i][j] != -1) {
242 return distances[i][j];
245 tmp = distcalculate(distances, old, new, i - 1, j - 1);
246 tmp = MIN(tmp, distcalculate(distances, old, new, i, j - 1));
247 tmp = MIN(tmp, distcalculate(distances, old, new, i - 1, j));
248 tmp += distdifferent(old, new, i, j);
250 distances[i][j] = tmp;
255 static int distance(const char *old, const char *new)
257 int **distances = NULL;
258 size_t m, n, i, j, r;
262 distances = malloc(sizeof(int*) * (m + 1));
264 for (i = 0; i <= m; i++) {
265 distances[i] = malloc(sizeof(int) * (n + 1));
266 for(j = 0; j <= n; j++) {
267 distances[i][j] = -1;
270 for (i = 0; i <= m; i++) {
273 for (j = 0; j <= n; j++) {
278 r = distcalculate(distances, old, new, m, n);
280 for (i = 0; i <= m; i++) {
281 memset(distances[i], 0, sizeof(int) * (n + 1));
289 static int similar(struct cracklib_options *opt,
290 const char *old, const char *new)
292 if (distance(old, new) >= opt->diff_ok) {
296 if (strlen(new) >= (strlen(old) * 2)) {
300 /* passwords are too similar */
305 * a nice mix of characters.
307 static int simple(struct cracklib_options *opt, const char *new)
316 for (i = 0;new[i];i++) {
317 if (isdigit (new[i]))
319 else if (isupper (new[i]))
321 else if (islower (new[i]))
328 * The scam was this - a password of only one character type
329 * must be 8 letters long. Two types, 7, and so on.
330 * This is now changed, the base size and the credits or defaults
331 * see the docs on the module for info on these parameters, the
332 * defaults cause the effect to be the same as before the change
335 if ((opt->dig_credit >= 0) && (digits > opt->dig_credit))
336 digits = opt->dig_credit;
338 if ((opt->up_credit >= 0) && (uppers > opt->up_credit))
339 uppers = opt->up_credit;
341 if ((opt->low_credit >= 0) && (lowers > opt->low_credit))
342 lowers = opt->low_credit;
344 if ((opt->oth_credit >= 0) && (others > opt->oth_credit))
345 others = opt->oth_credit;
347 size = opt->min_length;
349 if (opt->dig_credit >= 0)
351 else if (digits < opt->dig_credit * -1)
354 if (opt->up_credit >= 0)
356 else if (uppers < opt->up_credit * -1)
359 if (opt->low_credit >= 0)
361 else if (lowers < opt->low_credit * -1)
364 if (opt->oth_credit >= 0)
366 else if (others < opt->oth_credit * -1)
375 static char * str_lower(char *string)
379 for (cp = string; *cp; cp++)
384 static const char * password_check(struct cracklib_options *opt, const char *old, const char *new)
386 const char *msg = NULL;
387 char *oldmono, *newmono, *wrapped;
389 if (strcmp(new, old) == 0) {
390 msg = "is the same as the old one";
394 newmono = str_lower(x_strdup(new));
395 oldmono = str_lower(x_strdup(old));
396 wrapped = malloc(strlen(oldmono) * 2 + 1);
397 strcpy (wrapped, oldmono);
398 strcat (wrapped, oldmono);
400 if (palindrome(newmono))
401 msg = "is a palindrome";
403 if (!msg && strcmp(oldmono, newmono) == 0)
404 msg = "case changes only";
406 if (!msg && similar(opt, oldmono, newmono))
407 msg = "is too similar to the old one";
409 if (!msg && simple(opt, new))
410 msg = "is too simple";
412 if (!msg && strstr(wrapped, newmono))
415 memset(newmono, 0, strlen(newmono));
416 memset(oldmono, 0, strlen(oldmono));
417 memset(wrapped, 0, strlen(wrapped));
426 #define OLD_PASSWORDS_FILE "/etc/security/opasswd"
428 static const char * check_old_password(const char *forwho, const char *newpass)
430 static char buf[16384];
431 char *s_luser, *s_uid, *s_npas, *s_pas;
432 const char *msg = NULL;
435 opwfile = fopen(OLD_PASSWORDS_FILE, "r");
439 while (fgets(buf, 16380, opwfile)) {
440 if (!strncmp(buf, forwho, strlen(forwho))) {
441 buf[strlen(buf)-1] = '\0';
442 s_luser = strtok(buf, ":,");
443 s_uid = strtok(NULL, ":,");
444 s_npas = strtok(NULL, ":,");
445 s_pas = strtok(NULL, ":,");
446 while (s_pas != NULL) {
447 if (!strcmp(crypt(newpass, s_pas), s_pas)) {
448 msg = "has been already used";
451 s_pas = strtok(NULL, ":,");
462 static int _pam_unix_approve_pass(pam_handle_t *pamh,
464 struct cracklib_options *opt,
465 const char *pass_old,
466 const char *pass_new)
468 const char *msg = NULL;
472 if (pass_new == NULL || (pass_old && !strcmp(pass_old,pass_new))) {
473 if (ctrl && PAM_DEBUG_ARG)
474 _pam_log(LOG_DEBUG, "bad authentication token");
475 pam_error(pamh, "%s", pass_new == NULL ?
476 _("No password supplied"):_("Password unchanged"));
477 return PAM_AUTHTOK_ERR;
481 * if one wanted to hardwire authentication token strength
482 * checking this would be the place
484 msg = password_check(opt, pass_old,pass_new);
486 retval = pam_get_item(pamh, PAM_USER, &user);
487 if (retval != PAM_SUCCESS || user == NULL) {
488 if (ctrl & PAM_DEBUG_ARG) {
489 _pam_log(LOG_ERR,"Can not get username");
490 return PAM_AUTHTOK_ERR;
493 msg = check_old_password(user, pass_new);
497 if (ctrl && PAM_DEBUG_ARG)
498 _pam_log(LOG_NOTICE, "new passwd fails strength check: %s",
500 pam_error(pamh, _("BAD PASSWORD: %s"), msg);
501 return PAM_AUTHTOK_ERR;
507 /* The Main Thing (by Cristian Gafton, CEO at this module :-)
508 * (stolen from http://home.netscape.com)
510 PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags,
511 int argc, const char **argv)
514 struct cracklib_options options;
518 memset(&options, 0, sizeof(options));
519 options.retry_times = CO_RETRY_TIMES;
520 options.diff_ok = CO_DIFF_OK;
521 options.diff_ignore = CO_DIFF_IGNORE;
522 options.min_length = CO_MIN_LENGTH;
523 options.dig_credit = CO_DIG_CREDIT;
524 options.up_credit = CO_UP_CREDIT;
525 options.low_credit = CO_LOW_CREDIT;
526 options.oth_credit = CO_OTH_CREDIT;
527 options.use_authtok = CO_USE_AUTHTOK;
528 memset(options.prompt_type, 0, BUFSIZ);
529 strcpy(options.prompt_type,"UNIX");
530 memset(options.cracklib_dictpath, 0,
531 sizeof (options.cracklib_dictpath));
533 ctrl = _pam_parse(&options, argc, argv);
535 if (flags & PAM_PRELIM_CHECK) {
536 /* Check for passwd dictionary */
537 /* We cannot do that, since the original path is compiled
538 into the cracklib library and we don't know it. */
540 } else if (flags & PAM_UPDATE_AUTHTOK) {
542 char *token1, *token2, *resp;
543 const void *oldtoken;
546 retval = pam_get_item(pamh, PAM_OLDAUTHTOK, &oldtoken);
547 if (retval != PAM_SUCCESS) {
548 if (ctrl & PAM_DEBUG_ARG)
549 _pam_log(LOG_ERR,"Can not get old passwd");
551 retval = PAM_SUCCESS;
556 * make sure nothing inappropriate gets returned
558 token1 = token2 = NULL;
560 if (!options.retry_times) {
561 D(("returning %s because maxtries reached",
562 pam_strerror(pamh, retval)));
566 /* Planned modus operandi:
568 * Verify it against cracklib.
569 * If okay get it a second time.
570 * Check to be the same with the first one.
571 * set PAM_AUTHTOK and return
574 if (options.use_authtok == 1) {
575 const void *item = NULL;
577 retval = pam_get_item(pamh, PAM_AUTHTOK, &item);
578 if (retval != PAM_SUCCESS) {
581 ,"pam_get_item returned error to pam_cracklib"
583 } else if (item != NULL) { /* we have a password! */
584 token1 = x_strdup(item);
587 retval = PAM_AUTHTOK_RECOVER_ERR; /* didn't work */
591 /* Prepare to ask the user for the first time */
593 retval = pam_prompt (pamh, PAM_PROMPT_ECHO_OFF, &resp,
594 PROMPT1, options.prompt_type,
595 options.prompt_type[0]?" ":"");
597 if (retval == PAM_SUCCESS) { /* a good conversation */
598 token1 = x_strdup(resp);
599 if (token1 == NULL) {
601 "could not recover authentication token 1");
602 retval = PAM_AUTHTOK_RECOVER_ERR;
605 * tidy up the conversation (resp_retcode) is ignored
609 retval = (retval == PAM_SUCCESS) ?
610 PAM_AUTHTOK_RECOVER_ERR:retval ;
614 if (retval != PAM_SUCCESS) {
615 if (ctrl && PAM_DEBUG_ARG)
616 _pam_log(LOG_DEBUG,"unable to obtain a password");
620 D(("testing password, retval = %s", pam_strerror(pamh, retval)));
621 /* now test this passwd against cracklib */
623 const char *crack_msg;
625 D(("against cracklib"));
626 if ((crack_msg = FascistCheck(token1,options.cracklib_dictpath[0] == '\0'?NULL:options.cracklib_dictpath))) {
627 if (ctrl && PAM_DEBUG_ARG)
628 _pam_log(LOG_DEBUG,"bad password: %s",crack_msg);
629 pam_error(pamh, "BAD PASSWORD: %s", crack_msg);
630 if (getuid() || (flags & PAM_CHANGE_EXPIRED_AUTHTOK))
631 retval = PAM_AUTHTOK_ERR;
633 retval = PAM_SUCCESS;
635 /* check it for strength too... */
638 retval = _pam_unix_approve_pass(pamh,ctrl,&options,
640 if (retval != PAM_SUCCESS) {
641 if (getuid() || (flags & PAM_CHANGE_EXPIRED_AUTHTOK))
642 retval = PAM_AUTHTOK_ERR;
644 retval = PAM_SUCCESS;
650 D(("after testing: retval = %s", pam_strerror(pamh, retval)));
651 /* if cracklib/strength check said it is a bad passwd... */
652 if ((retval != PAM_SUCCESS) && (retval != PAM_IGNORE)) {
655 temp_unused = pam_set_item(pamh, PAM_AUTHTOK, NULL);
656 token1 = _pam_delete(token1);
660 /* Now we have a good passwd. Ask for it once again */
662 if (options.use_authtok == 0) {
664 retval = pam_prompt (pamh, PAM_PROMPT_ECHO_OFF, &resp,
665 PROMPT2, options.prompt_type,
666 options.prompt_type[0]?" ":"");
667 if (retval == PAM_SUCCESS) { /* a good conversation */
668 token2 = x_strdup(resp);
669 if (token2 == NULL) {
671 "could not recover authentication token 2");
672 retval = PAM_AUTHTOK_RECOVER_ERR;
675 * tidy up the conversation (resp_retcode) is ignored
679 retval = (retval == PAM_SUCCESS) ?
680 PAM_AUTHTOK_RECOVER_ERR:retval ;
683 if (retval != PAM_SUCCESS) {
684 if (ctrl && PAM_DEBUG_ARG)
686 ,"unable to obtain the password a second time");
690 /* Hopefully now token1 and token2 the same password ... */
691 if (strcmp(token1,token2) != 0) {
693 pam_error(pamh, "%s", MISTYPED_PASS);
694 token1 = _pam_delete(token1);
695 token2 = _pam_delete(token2);
696 pam_set_item(pamh, PAM_AUTHTOK, NULL);
697 if (ctrl & PAM_DEBUG_ARG)
698 _pam_log(LOG_NOTICE,"Password mistyped");
699 retval = PAM_AUTHTOK_RECOVER_ERR;
703 /* Yes, the password was typed correct twice
704 * we store this password as an item
708 const void *item = NULL;
710 retval = pam_set_item(pamh, PAM_AUTHTOK, token1);
713 token1 = _pam_delete(token1);
714 token2 = _pam_delete(token2);
716 if ( (retval != PAM_SUCCESS) ||
717 ((retval = pam_get_item(pamh, PAM_AUTHTOK, &item)
718 ) != PAM_SUCCESS) ) {
719 _pam_log(LOG_CRIT, "error manipulating password");
722 item = NULL; /* break link to password */
727 } while (options.retry_times--);
730 if (ctrl & PAM_DEBUG_ARG)
731 _pam_log(LOG_NOTICE, "UNKNOWN flags setting %02X",flags);
732 return PAM_SERVICE_ERR;
736 return PAM_SERVICE_ERR;
742 /* static module data */
743 struct pam_module _pam_cracklib_modstruct = {
755 * Copyright (c) Cristian Gafton <gafton@redhat.com>, 1996.
756 * All rights reserved
758 * Redistribution and use in source and binary forms, with or without
759 * modification, are permitted provided that the following conditions
761 * 1. Redistributions of source code must retain the above copyright
762 * notice, and the entire permission notice in its entirety,
763 * including the disclaimer of warranties.
764 * 2. Redistributions in binary form must reproduce the above copyright
765 * notice, this list of conditions and the following disclaimer in the
766 * documentation and/or other materials provided with the distribution.
767 * 3. The name of the author may not be used to endorse or promote
768 * products derived from this software without specific prior
769 * written permission.
771 * ALTERNATIVELY, this product may be distributed under the terms of
772 * the GNU Public License, in which case the provisions of the GPL are
773 * required INSTEAD OF the above restrictions. (This clause is
774 * necessary due to a potential bad interaction between the GPL and
775 * the restrictions contained in a BSD-style copyright.)
777 * THIS SOFTWARE IS PROVIDED `AS IS'' AND ANY EXPRESS OR IMPLIED
778 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
779 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
780 * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
781 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
782 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
783 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
784 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
785 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
786 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
787 * OF THE POSSIBILITY OF SUCH DAMAGE.
789 * The following copyright was appended for the long password support
790 * added with the libpam 0.58 release:
792 * Modificaton Copyright (c) Philip W. Dalrymple III <pwd@mdtsoft.com>
793 * 1997. All rights reserved
795 * THE MODIFICATION THAT PROVIDES SUPPORT FOR LONG PASSWORD TYPE CHECKING TO
796 * THIS SOFTWARE IS PROVIDED `AS IS'' AND ANY EXPRESS OR IMPLIED
797 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
798 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
799 * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
800 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
801 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
802 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
803 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
804 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
805 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
806 * OF THE POSSIBILITY OF SUCH DAMAGE.