1 /* pam_cracklib module */
4 * 0.85. added six new options to use this with long passwords.
5 * 0.8. tidied output and improved D(()) usage for debugging.
6 * 0.7. added support for more obscure checks for new passwd.
7 * 0.6. root can reset user passwd to any values (it's only warned)
8 * 0.5. supports retries - 'retry=N' argument
9 * 0.4. added argument 'type=XXX' for 'New XXX password' prompt
10 * 0.3. Added argument 'debug'
11 * 0.2. new password is feeded to cracklib for verify after typed once
16 * Written by Cristian Gafton <gafton@redhat.com> 1996/09/10
17 * Long password support by Philip W. Dalrymple <pwd@mdtsoft.com> 1997/07/18
18 * See the end of the file for Copyright Information
20 * Modification for long password systems (>8 chars). The original
21 * module had problems when used in a md5 password system in that it
22 * allowed too short passwords but required that at least half of the
23 * bytes in the new password did not appear in the old one. this
24 * action is still the default and the changes should not break any
25 * current user. This modification adds 6 new options, one to set the
26 * number of bytes in the new password that are not in the old one,
27 * the other five to control the length checking, these are all
28 * documented (or will be before anyone else sees this code) in the PAM
29 * S.A.G. in the section on the cracklib module.
36 #ifdef NEED_CRYPT_HEADER
44 #include <sys/types.h>
48 extern char *FascistCheck(char *pw, const char *dictpath);
50 #ifndef CRACKLIB_DICTPATH
51 #define CRACKLIB_DICTPATH "/usr/share/dict/cracklib_dict"
54 #define PROMPT1 "New %s password: "
55 #define PROMPT2 "Retype new %s password: "
56 #define MISTYPED_PASS "Sorry, passwords do not match"
59 * here, we make a definition for the externally accessible function
60 * in this file (this definition is required for static a module
61 * but strongly encouraged generally) it is used to instruct the
62 * modules include file to define the function prototypes.
65 #define PAM_SM_PASSWORD
67 #include <security/pam_modules.h>
68 #include <security/_pam_macros.h>
71 #include <security/pam_appl.h>
72 #endif /* LINUX_PAM */
76 static void _pam_log(int err, const char *format, ...)
80 va_start(args, format);
81 openlog("PAM-Cracklib", LOG_CONS|LOG_PID, LOG_AUTH);
82 vsyslog(err, format, args);
87 /* argument parsing */
88 #define PAM_DEBUG_ARG 0x0001
90 struct cracklib_options {
100 char prompt_type[BUFSIZ];
103 #define CO_RETRY_TIMES 1
104 #define CO_DIFF_OK 10
105 #define CO_DIFF_IGNORE 23
106 #define CO_MIN_LENGTH 9
107 # define CO_MIN_LENGTH_BASE 5
108 #define CO_DIG_CREDIT 1
109 #define CO_UP_CREDIT 1
110 #define CO_LOW_CREDIT 1
111 #define CO_OTH_CREDIT 1
112 #define CO_USE_AUTHTOK 0
114 static int _pam_parse(struct cracklib_options *opt, int argc, const char **argv)
118 /* step through arguments */
119 for (ctrl=0; argc-- > 0; ++argv) {
122 /* generic options */
124 if (!strcmp(*argv,"debug"))
125 ctrl |= PAM_DEBUG_ARG;
126 else if (!strncmp(*argv,"type=",5))
127 strcpy(opt->prompt_type, *argv+5);
128 else if (!strncmp(*argv,"retry=",6)) {
129 opt->retry_times = strtol(*argv+6,&ep,10);
130 if (!ep || (opt->retry_times < 1))
131 opt->retry_times = CO_RETRY_TIMES;
132 } else if (!strncmp(*argv,"difok=",6)) {
133 opt->diff_ok = strtol(*argv+6,&ep,10);
134 if (!ep || (opt->diff_ok < 0))
135 opt->diff_ok = CO_DIFF_OK;
136 } else if (!strncmp(*argv,"difignore=",10)) {
137 opt->diff_ignore = strtol(*argv+10,&ep,10);
138 if (!ep || (opt->diff_ignore < 0))
139 opt->diff_ignore = CO_DIFF_IGNORE;
140 } else if (!strncmp(*argv,"minlen=",7)) {
141 opt->min_length = strtol(*argv+7,&ep,10);
142 if (!ep || (opt->min_length < CO_MIN_LENGTH_BASE))
143 opt->min_length = CO_MIN_LENGTH_BASE;
144 } else if (!strncmp(*argv,"dcredit=",8)) {
145 opt->dig_credit = strtol(*argv+8,&ep,10);
146 if (!ep || (opt->dig_credit < 0))
148 } else if (!strncmp(*argv,"ucredit=",8)) {
149 opt->up_credit = strtol(*argv+8,&ep,10);
150 if (!ep || (opt->up_credit < 0))
152 } else if (!strncmp(*argv,"lcredit=",8)) {
153 opt->low_credit = strtol(*argv+8,&ep,10);
154 if (!ep || (opt->low_credit < 0))
156 } else if (!strncmp(*argv,"ocredit=",8)) {
157 opt->oth_credit = strtol(*argv+8,&ep,10);
158 if (!ep || (opt->oth_credit < 0))
160 } else if (!strncmp(*argv,"use_authtok",11)) {
161 opt->use_authtok = 1;
163 _pam_log(LOG_ERR,"pam_parse: unknown option; %s",*argv);
170 /* Helper functions */
172 /* this is a front-end for module-application conversations */
173 static int converse(pam_handle_t *pamh, int ctrl, int nargs,
174 struct pam_message **message,
175 struct pam_response **response)
178 struct pam_conv *conv;
180 retval = pam_get_item(pamh, PAM_CONV, (const void **) &conv);
182 if ( retval == PAM_SUCCESS ) {
183 retval = conv->conv(nargs, (const struct pam_message **)message,
184 response, conv->appdata_ptr);
185 if (retval != PAM_SUCCESS && (ctrl && PAM_DEBUG_ARG)) {
186 _pam_log(LOG_DEBUG, "conversation failure [%s]",
187 pam_strerror(pamh, retval));
190 _pam_log(LOG_ERR, "couldn't obtain coversation function [%s]",
191 pam_strerror(pamh, retval));
194 return retval; /* propagate error status */
197 static int make_remark(pam_handle_t *pamh, unsigned int ctrl,
198 int type, const char *text)
200 struct pam_message *pmsg[1], msg[1];
201 struct pam_response *resp;
206 msg[0].msg_style = type;
209 retval = converse(pamh, ctrl, 1, pmsg, &resp);
210 if (retval == PAM_SUCCESS)
211 _pam_drop_reply(resp, 1);
216 /* use this to free strings. ESPECIALLY password strings */
217 static char *_pam_delete(register char *xx)
225 * can't be a palindrome - like `R A D A R' or `M A D A M'
227 static int palindrome(const char *old, const char *new)
233 for (j = 0;j < i;j++)
234 if (new[i - j - 1] != new[j])
241 * This is a reasonably severe check for a different selection of characters
242 * in the old and new passwords.
245 static int similar(struct cracklib_options *opt,
246 const char *old, const char *new)
250 for (i = j = 0; old[i]; i++) {
251 if (strchr (new, old[i])) {
256 if (((i-j) >= opt->diff_ok)
257 || (strlen(new) >= (j * 2))
258 || (strlen(new) >= opt->diff_ignore)) {
259 /* passwords are not very similar */
263 /* passwords are too similar */
268 * a nice mix of characters.
270 static int simple(struct cracklib_options *opt, const char *old, const char *new)
279 for (i = 0;new[i];i++) {
280 if (isdigit (new[i]))
282 else if (isupper (new[i]))
284 else if (islower (new[i]))
291 * The scam was this - a password of only one character type
292 * must be 8 letters long. Two types, 7, and so on.
293 * This is now changed, the base size and the credits or defaults
294 * see the docs on the module for info on these parameters, the
295 * defaults cause the effect to be the same as before the change
298 if (digits > opt->dig_credit)
299 digits = opt->dig_credit;
301 if (uppers > opt->up_credit)
302 uppers = opt->up_credit;
304 if (lowers > opt->low_credit)
305 lowers = opt->low_credit;
307 if (others > opt->oth_credit)
308 others = opt->oth_credit;
310 size = opt->min_length;
322 static char * str_lower(char *string)
326 for (cp = string; *cp; cp++)
331 static const char * password_check(struct cracklib_options *opt, const char *old, const char *new)
333 const char *msg = NULL;
334 char *oldmono, *newmono, *wrapped;
336 if (strcmp(new, old) == 0) {
337 msg = "is the same as the old one";
341 newmono = str_lower(x_strdup(new));
342 oldmono = str_lower(x_strdup(old));
343 wrapped = malloc(strlen(oldmono) * 2 + 1);
344 strcpy (wrapped, oldmono);
345 strcat (wrapped, oldmono);
347 if (palindrome(oldmono, newmono))
348 msg = "is a palindrome";
350 if (!msg && strcmp(oldmono, newmono) == 0)
351 msg = "case changes only";
353 if (!msg && similar(opt, oldmono, newmono))
354 msg = "is too similar to the old one";
356 if (!msg && simple(opt, old, new))
357 msg = "is too simple";
359 if (!msg && strstr(wrapped, newmono))
362 memset(newmono, 0, strlen(newmono));
363 memset(oldmono, 0, strlen(oldmono));
364 memset(wrapped, 0, strlen(wrapped));
373 #define OLD_PASSWORDS_FILE "/etc/security/opasswd"
375 static const char * check_old_password(const char *forwho, const char *newpass)
377 static char buf[16384];
378 char *s_luser, *s_uid, *s_npas, *s_pas;
379 const char *msg = NULL;
382 opwfile = fopen(OLD_PASSWORDS_FILE, "r");
386 while (fgets(buf, 16380, opwfile)) {
387 if (!strncmp(buf, forwho, strlen(forwho))) {
388 buf[strlen(buf)-1] = '\0';
389 s_luser = strtok(buf, ":,");
390 s_uid = strtok(NULL, ":,");
391 s_npas = strtok(NULL, ":,");
392 s_pas = strtok(NULL, ":,");
393 while (s_pas != NULL) {
394 if (!strcmp(crypt(newpass, s_pas), s_pas)) {
395 msg = "has been already used";
398 s_pas = strtok(NULL, ":,");
409 static int _pam_unix_approve_pass(pam_handle_t *pamh,
411 struct cracklib_options *opt,
412 const char *pass_old,
413 const char *pass_new)
415 const char *msg = NULL;
419 if (pass_new == NULL || (pass_old && !strcmp(pass_old,pass_new))) {
420 if (ctrl && PAM_DEBUG_ARG)
421 _pam_log(LOG_DEBUG, "bad authentication token");
422 make_remark(pamh, ctrl, PAM_ERROR_MSG,
424 "No password supplied":"Password unchanged" );
425 return PAM_AUTHTOK_ERR;
429 * if one wanted to hardwire authentication token strength
430 * checking this would be the place
432 msg = password_check(opt, pass_old,pass_new);
434 retval = pam_get_item(pamh, PAM_USER, (const void **)&user);
435 if (retval != PAM_SUCCESS) {
436 if (ctrl & PAM_DEBUG_ARG) {
437 _pam_log(LOG_ERR,"Can not get username");
438 return PAM_AUTHTOK_ERR;
441 msg = check_old_password(user, pass_new);
447 memset(remark,0,sizeof(remark));
448 sprintf(remark,"BAD PASSWORD: %s",msg);
449 if (ctrl && PAM_DEBUG_ARG)
450 _pam_log(LOG_NOTICE, "new passwd fails strength check: %s",
452 make_remark(pamh, ctrl, PAM_ERROR_MSG, remark);
453 return PAM_AUTHTOK_ERR;
459 /* The Main Thing (by Cristian Gafton, CEO at this module :-)
460 * (stolen from http://home.netscape.com)
462 PAM_EXTERN int pam_sm_chauthtok(pam_handle_t *pamh, int flags,
463 int argc, const char **argv)
466 struct cracklib_options options;
468 options.retry_times = CO_RETRY_TIMES;
469 options.diff_ok = CO_DIFF_OK;
470 options.diff_ignore = CO_DIFF_IGNORE;
471 options.min_length = CO_MIN_LENGTH;
472 options.dig_credit = CO_DIG_CREDIT;
473 options.up_credit = CO_UP_CREDIT;
474 options.low_credit = CO_LOW_CREDIT;
475 options.oth_credit = CO_OTH_CREDIT;
476 options.use_authtok = CO_USE_AUTHTOK;
477 memset(options.prompt_type, 0, BUFSIZ);
479 ctrl = _pam_parse(&options, argc, argv);
482 if (!options.prompt_type[0])
483 strcpy(options.prompt_type,"UNIX");
485 if (flags & PAM_PRELIM_CHECK) {
486 /* Check for passwd dictionary */
488 char buf[sizeof(CRACKLIB_DICTPATH)+10];
492 memset(buf,0,sizeof(buf)); /* zero the buffer */
493 sprintf(buf,"%s.pwd",CRACKLIB_DICTPATH);
495 if (!stat(buf,&st) && st.st_size)
498 if (ctrl & PAM_DEBUG_ARG)
499 _pam_log(LOG_NOTICE,"dict path '%s'[.pwd] is invalid",
505 return PAM_SERVICE_ERR;
507 } else if (flags & PAM_UPDATE_AUTHTOK) {
509 char *token1, *token2, *oldtoken;
510 struct pam_message msg[1],*pmsg[1];
511 struct pam_response *resp;
512 const char *cracklib_dictpath = CRACKLIB_DICTPATH;
516 retval = pam_get_item(pamh, PAM_OLDAUTHTOK,
517 (const void **)&oldtoken);
518 if (retval != PAM_SUCCESS) {
519 if (ctrl & PAM_DEBUG_ARG)
520 _pam_log(LOG_ERR,"Can not get old passwd");
522 retval = PAM_SUCCESS;
527 * make sure nothing inappropriate gets returned
529 token1 = token2 = NULL;
531 if (!options.retry_times) {
532 D(("returning %s because maxtries reached",
533 pam_strerror(pamh, retval)));
537 /* Planned modus operandi:
539 * Verify it against cracklib.
540 * If okay get it a second time.
541 * Check to be the same with the first one.
542 * set PAM_AUTHTOK and return
545 if (options.use_authtok == 1) {
546 const char *item = NULL;
548 retval = pam_get_item(pamh, PAM_AUTHTOK, (const void **) &item);
549 if (retval != PAM_SUCCESS) {
552 ,"pam_get_item returned error to pam_cracklib"
554 } else if (item != NULL) { /* we have a password! */
555 token1 = x_strdup(item);
558 retval = PAM_AUTHTOK_RECOVER_ERR; /* didn't work */
562 /* Prepare to ask the user for the first time */
563 memset(prompt,0,sizeof(prompt));
564 sprintf(prompt,PROMPT1,options.prompt_type);
566 msg[0].msg_style = PAM_PROMPT_ECHO_OFF;
570 retval = converse(pamh, ctrl, 1, pmsg, &resp);
572 /* interpret the response */
573 if (retval == PAM_SUCCESS) { /* a good conversation */
574 token1 = x_strdup(resp[0].resp);
575 if (token1 == NULL) {
577 "could not recover authentication token 1");
578 retval = PAM_AUTHTOK_RECOVER_ERR;
582 * tidy up the conversation (resp_retcode) is ignored
584 _pam_drop_reply(resp, 1);
586 retval = (retval == PAM_SUCCESS) ?
587 PAM_AUTHTOK_RECOVER_ERR:retval ;
591 if (retval != PAM_SUCCESS) {
592 if (ctrl && PAM_DEBUG_ARG)
593 _pam_log(LOG_DEBUG,"unable to obtain a password");
597 D(("testing password, retval = %s", pam_strerror(pamh, retval)));
598 /* now test this passwd against cracklib */
603 bzero(remark,sizeof(remark));
604 D(("against cracklib"));
605 if ((crack_msg = FascistCheck(token1, cracklib_dictpath))) {
606 if (ctrl && PAM_DEBUG_ARG)
607 _pam_log(LOG_DEBUG,"bad password: %s",crack_msg);
608 sprintf(remark,"BAD PASSWORD: %s", crack_msg);
609 make_remark(pamh, ctrl, PAM_ERROR_MSG, remark);
610 if (getuid() || (flags & PAM_CHANGE_EXPIRED_AUTHTOK))
611 retval = PAM_AUTHTOK_ERR;
613 retval = PAM_SUCCESS;
615 /* check it for strength too... */
618 retval = _pam_unix_approve_pass(pamh,ctrl,&options,
620 if (retval != PAM_SUCCESS) {
621 if (getuid() || (flags & PAM_CHANGE_EXPIRED_AUTHTOK))
622 retval = PAM_AUTHTOK_ERR;
624 retval = PAM_SUCCESS;
630 D(("after testing: retval = %s", pam_strerror(pamh, retval)));
631 /* if cracklib/strength check said it is a bad passwd... */
632 if ((retval != PAM_SUCCESS) && (retval != PAM_IGNORE)) {
635 temp_unused = pam_set_item(pamh, PAM_AUTHTOK, NULL);
636 token1 = _pam_delete(token1);
640 /* Now we have a good passwd. Ask for it once again */
642 if (options.use_authtok == 0) {
643 bzero(prompt,sizeof(prompt));
644 sprintf(prompt,PROMPT2,options.prompt_type);
646 msg[0].msg_style = PAM_PROMPT_ECHO_OFF;
650 retval = converse(pamh, ctrl, 1, pmsg, &resp);
652 /* interpret the response */
653 if (retval == PAM_SUCCESS) { /* a good conversation */
654 token2 = x_strdup(resp[0].resp);
655 if (token2 == NULL) {
657 "could not recover authentication token 2");
658 retval = PAM_AUTHTOK_RECOVER_ERR;
662 * tidy up the conversation (resp_retcode) is ignored
664 _pam_drop_reply(resp, 1);
666 retval = (retval == PAM_SUCCESS) ?
667 PAM_AUTHTOK_RECOVER_ERR:retval ;
670 if (retval != PAM_SUCCESS) {
671 if (ctrl && PAM_DEBUG_ARG)
673 ,"unable to obtain the password a second time");
677 /* Hopefully now token1 and token2 the same password ... */
678 if (strcmp(token1,token2) != 0) {
680 make_remark(pamh, ctrl, PAM_ERROR_MSG, MISTYPED_PASS);
681 token1 = _pam_delete(token1);
682 token2 = _pam_delete(token2);
683 pam_set_item(pamh, PAM_AUTHTOK, NULL);
684 if (ctrl & PAM_DEBUG_ARG)
685 _pam_log(LOG_NOTICE,"Password mistyped");
686 retval = PAM_AUTHTOK_RECOVER_ERR;
690 /* Yes, the password was typed correct twice
691 * we store this password as an item
695 const char *item = NULL;
697 retval = pam_set_item(pamh, PAM_AUTHTOK, token1);
700 token1 = _pam_delete(token1);
701 token2 = _pam_delete(token2);
703 if ( (retval != PAM_SUCCESS) ||
704 ((retval = pam_get_item(pamh, PAM_AUTHTOK,
705 (const void **)&item)
706 ) != PAM_SUCCESS) ) {
707 _pam_log(LOG_CRIT, "error manipulating password");
710 item = NULL; /* break link to password */
715 } while (options.retry_times--);
718 if (ctrl & PAM_DEBUG_ARG)
719 _pam_log(LOG_NOTICE, "UNKNOWN flags setting %02X",flags);
720 return PAM_SERVICE_ERR;
724 return PAM_SERVICE_ERR;
730 /* static module data */
731 struct pam_module _pam_cracklib_modstruct = {
743 * Copyright (c) Cristian Gafton <gafton@redhat.com>, 1996.
744 * All rights reserved
746 * Redistribution and use in source and binary forms, with or without
747 * modification, are permitted provided that the following conditions
749 * 1. Redistributions of source code must retain the above copyright
750 * notice, and the entire permission notice in its entirety,
751 * including the disclaimer of warranties.
752 * 2. Redistributions in binary form must reproduce the above copyright
753 * notice, this list of conditions and the following disclaimer in the
754 * documentation and/or other materials provided with the distribution.
755 * 3. The name of the author may not be used to endorse or promote
756 * products derived from this software without specific prior
757 * written permission.
759 * ALTERNATIVELY, this product may be distributed under the terms of
760 * the GNU Public License, in which case the provisions of the GPL are
761 * required INSTEAD OF the above restrictions. (This clause is
762 * necessary due to a potential bad interaction between the GPL and
763 * the restrictions contained in a BSD-style copyright.)
765 * THIS SOFTWARE IS PROVIDED `AS IS'' AND ANY EXPRESS OR IMPLIED
766 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
767 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
768 * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
769 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
770 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
771 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
772 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
773 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
774 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
775 * OF THE POSSIBILITY OF SUCH DAMAGE.
777 * The following copyright was appended for the long password support
778 * added with the libpam 0.58 release:
780 * Modificaton Copyright (c) Philip W. Dalrymple III <pwd@mdtsoft.com>
781 * 1997. All rights reserved
783 * THE MODIFICATION THAT PROVIDES SUPPORT FOR LONG PASSWORD TYPE CHECKING TO
784 * THIS SOFTWARE IS PROVIDED `AS IS'' AND ANY EXPRESS OR IMPLIED
785 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
786 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
787 * DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT,
788 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
789 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
790 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
791 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
792 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
793 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
794 * OF THE POSSIBILITY OF SUCH DAMAGE.