1 /* $NetBSD: ppm.c,v 1.2 2021/08/14 16:14:53 christos Exp $ */ 2 3 /* 4 * ppm.c for OpenLDAP 5 * 6 * See LICENSE, README and INSTALL files 7 */ 8 9 10 /* 11 password policy module is called with: 12 int check_password (char *pPasswd, char **ppErrStr, Entry *e, void *pArg) 13 14 *pPasswd: new password 15 **ppErrStr: pointer to the string containing the error message 16 *e: pointer to the current user entry 17 *pArg: pointer to a struct berval holding the value of pwdCheckModuleArg attr 18 */ 19 20 #include <stdlib.h> // for type conversion, such as atoi... 21 #include <regex.h> // for matching allowedParameters / conf file 22 #include <string.h> 23 #include <ctype.h> 24 #include <portable.h> 25 #include <slap.h> 26 #include <stdarg.h> // for variable nb of arguments functions 27 #include "ppm.h" 28 29 #ifdef CRACKLIB 30 #include "crack.h" // use cracklib to check password 31 #endif 32 33 void 34 ppm_log(int priority, const char *format, ...) 35 { 36 // if DEBUG flag is set 37 // logs into syslog (for OpenLDAP) or to stdout (for tests) 38 #if defined(DEBUG) 39 if(ppm_test != 1) 40 { 41 va_list syslog_args; 42 va_start(syslog_args, format); 43 vsyslog(priority, format, syslog_args); 44 va_end(syslog_args); 45 } 46 else 47 { 48 va_list stdout_args; 49 va_start(stdout_args, format); 50 vprintf(format, stdout_args); 51 printf("\n"); 52 fflush(stdout); 53 va_end(stdout_args); 54 } 55 #endif 56 } 57 58 void 59 strcpy_safe(char *dest, char *src, int length_dest) 60 { 61 if(src == NULL) 62 { 63 dest[0] = '\0'; 64 } 65 else 66 { 67 int length_src = strlen(src); 68 int n = (length_dest < length_src) ? length_dest : length_src; 69 // Copy the string — don’t copy too many bytes. 70 strncpy(dest, src, n); 71 // Ensure null-termination. 72 dest[n] = '\0'; 73 } 74 } 75 76 genValue* 77 getValue(conf *fileConf, int numParam, char* param) 78 { 79 int i = 0; 80 81 // First scan parameters 82 for (i = 0; i < numParam; i++) { 83 if ((strlen(param) == strlen(fileConf[i].param)) 84 && (strncmp(param, fileConf[i].param, strlen(fileConf[i].param)) 85 == 0)) { 86 return &(fileConf[i].value); 87 } 88 } 89 return NULL; 90 } 91 92 int maxConsPerClass(char *password, char *charClass) 93 { 94 // find maximum number of consecutive class characters in the password 95 96 int bestMax = 0; 97 int max = 0; 98 int i; 99 100 for(i=0 ; i<strlen(password) ; i++) 101 { 102 if(strchr(charClass,password[i]) != NULL) 103 { 104 // current character is in class 105 max++; 106 // is the new max a better candidate to maxConsecutivePerClass? 107 if(max > bestMax) 108 { 109 // found a better maxConsecutivePerClass 110 bestMax = max; 111 } 112 } 113 else 114 { 115 // current character is not in class 116 // reinitialize max 117 max=0; 118 } 119 } 120 return bestMax; 121 } 122 123 void 124 storeEntry(char *param, char *value, valueType valType, 125 char *min, char *minForPoint, conf * fileConf, int *numParam) 126 { 127 int i = 0; 128 int iMin; 129 int iMinForPoint; 130 if (min == NULL || strcmp(min,"") == 0) 131 iMin = 0; 132 else 133 iMin = atoi(min); 134 135 if (minForPoint == NULL || strcmp(minForPoint,"") == 0) 136 iMinForPoint = 0; 137 else 138 iMinForPoint = atoi(minForPoint); 139 140 // First scan parameters 141 for (i = 0; i < *numParam; i++) { 142 if ((strlen(param) == strlen(fileConf[i].param)) 143 && (strncmp(param, fileConf[i].param, strlen(fileConf[i].param)) 144 == 0)) { 145 // entry found, replace values 146 if(valType == typeInt) 147 fileConf[i].value.iVal = atoi(value); 148 else 149 strcpy_safe(fileConf[i].value.sVal, value, VALUE_MAX_LEN); 150 fileConf[i].min = iMin; 151 fileConf[i].minForPoint = iMinForPoint; 152 if(valType == typeInt) 153 ppm_log(LOG_NOTICE, "ppm: Accepted replaced value: %d", 154 fileConf[i].value.iVal); 155 else 156 ppm_log(LOG_NOTICE, "ppm: Accepted replaced value: %s", 157 fileConf[i].value.sVal); 158 return; 159 } 160 } 161 // entry not found, add values 162 strcpy_safe(fileConf[*numParam].param, param, PARAM_MAX_LEN); 163 fileConf[*numParam].iType = valType; 164 if(valType == typeInt) 165 fileConf[i].value.iVal = atoi(value); 166 else 167 strcpy_safe(fileConf[i].value.sVal, value, VALUE_MAX_LEN); 168 fileConf[*numParam].min = iMin; 169 fileConf[*numParam].minForPoint = iMinForPoint; 170 ++(*numParam); 171 if(valType == typeInt) 172 ppm_log(LOG_NOTICE, "ppm: Accepted new value: %d", 173 fileConf[*numParam].value.iVal); 174 else 175 ppm_log(LOG_NOTICE, "ppm: Accepted new value: %s", 176 fileConf[*numParam].value.sVal); 177 } 178 179 int 180 typeParam(char* param) 181 { 182 int i; 183 int n = sizeof(allowedParameters)/sizeof(params); 184 185 regex_t regex; 186 int reti; 187 188 for(i = 0 ; i < n ; i++ ) 189 { 190 // Compile regular expression 191 reti = regcomp(®ex, allowedParameters[i].param, 0); 192 if (reti) { 193 ppm_log(LOG_ERR, "ppm: Cannot compile regex: %s", 194 allowedParameters[i].param); 195 return n; 196 } 197 198 // Execute regular expression 199 reti = regexec(®ex, param, 0, NULL, 0); 200 if (!reti) 201 { 202 regfree(®ex); 203 return i; 204 } 205 regfree(®ex); 206 } 207 return n; 208 } 209 210 #ifndef PPM_READ_FILE 211 212 /* 213 * read configuration into pwdCheckModuleArg attribute 214 * */ 215 static void 216 read_config_attr(conf * fileConf, int *numParam, char *ppm_config_attr) 217 { 218 int nParam = 0; // position of found parameter in allowedParameters 219 int sAllowedParameters = sizeof(allowedParameters)/sizeof(params); 220 char arg[260*256]; 221 char *token; 222 char *saveptr1; 223 char *saveptr2; 224 225 strcpy_safe(arg, ppm_config_attr, 260*256); 226 ppm_log(LOG_NOTICE, "ppm: Parsing pwdCheckModuleArg attribute"); 227 token = strtok_r(arg, "\n", &saveptr1); 228 229 while (token != NULL) { 230 ppm_log(LOG_NOTICE, "ppm: get line: %s",token); 231 char *start = token; 232 char *word, *value; 233 char *min, *minForPoint;; 234 235 while (isspace(*start) && isascii(*start)) 236 start++; 237 238 if (!isascii(*start)) 239 { 240 token = strtok_r(NULL, "\n", &saveptr1); 241 continue; 242 } 243 if (start[0] == '#') 244 { 245 token = strtok_r(NULL, "\n", &saveptr1); 246 continue; 247 } 248 249 if ((word = strtok_r(start, " \t", &saveptr2))) { 250 if ((value = strtok_r(NULL, " \t", &saveptr2)) == NULL) 251 { 252 saveptr2 = NULL; 253 ppm_log(LOG_NOTICE, "ppm: No value, goto next parameter"); 254 token = strtok_r(NULL, "\n", &saveptr1); 255 continue; 256 } 257 if (strchr(value, '\n') != NULL) 258 strchr(value, '\n')[0] = '\0'; 259 min = strtok_r(NULL, " \t", &saveptr2); 260 if (min != NULL) 261 if (strchr(min, '\n') != NULL) 262 strchr(min, '\n')[0] = '\0'; 263 minForPoint = strtok_r(NULL, " \t", &saveptr2); 264 if (minForPoint != NULL) 265 if (strchr(minForPoint, '\n') != NULL) 266 strchr(minForPoint, '\n')[0] = '\0'; 267 268 269 nParam = typeParam(word); // search for param in allowedParameters 270 if (nParam != sAllowedParameters) // param has been found 271 { 272 ppm_log(LOG_NOTICE, 273 "ppm: Param = %s, value = %s, min = %s, minForPoint= %s", 274 word, value, min, minForPoint); 275 276 storeEntry(word, value, allowedParameters[nParam].iType, 277 min, minForPoint, fileConf, numParam); 278 } 279 else 280 { 281 ppm_log(LOG_NOTICE, 282 "ppm: Parameter '%s' rejected", word); 283 } 284 285 } 286 token = strtok_r(NULL, "\n", &saveptr1); 287 } 288 289 } 290 291 #endif 292 293 #ifdef PPM_READ_FILE 294 295 /* 296 * read configuration file (DEPRECATED) 297 * */ 298 static void 299 read_config_file(conf * fileConf, int *numParam, char *ppm_config_file) 300 { 301 FILE *config; 302 char line[260] = ""; 303 int nParam = 0; // position of found parameter in allowedParameters 304 int sAllowedParameters = sizeof(allowedParameters)/sizeof(params); 305 306 ppm_log(LOG_NOTICE, "ppm: Opening file %s", ppm_config_file); 307 if ((config = fopen(ppm_config_file, "r")) == NULL) { 308 ppm_log(LOG_ERR, "ppm: Opening file %s failed", ppm_config_file); 309 exit(EXIT_FAILURE); 310 } 311 312 while (fgets(line, 256, config) != NULL) { 313 char *start = line; 314 char *word, *value; 315 char *min, *minForPoint;; 316 317 while (isspace(*start) && isascii(*start)) 318 start++; 319 320 if (!isascii(*start)) 321 continue; 322 if (start[0] == '#') 323 continue; 324 325 if ((word = strtok(start, " \t"))) { 326 if ((value = strtok(NULL, " \t")) == NULL) 327 continue; 328 if (strchr(value, '\n') != NULL) 329 strchr(value, '\n')[0] = '\0'; 330 min = strtok(NULL, " \t"); 331 if (min != NULL) 332 if (strchr(min, '\n') != NULL) 333 strchr(min, '\n')[0] = '\0'; 334 minForPoint = strtok(NULL, " \t"); 335 if (minForPoint != NULL) 336 if (strchr(minForPoint, '\n') != NULL) 337 strchr(minForPoint, '\n')[0] = '\0'; 338 339 340 nParam = typeParam(word); // search for param in allowedParameters 341 if (nParam != sAllowedParameters) // param has been found 342 { 343 ppm_log(LOG_NOTICE, 344 "ppm: Param = %s, value = %s, min = %s, minForPoint= %s", 345 word, value, min, minForPoint); 346 347 storeEntry(word, value, allowedParameters[nParam].iType, 348 min, minForPoint, fileConf, numParam); 349 } 350 else 351 { 352 ppm_log(LOG_NOTICE, 353 "ppm: Parameter '%s' rejected", word); 354 } 355 356 } 357 } 358 359 fclose(config); 360 } 361 362 #endif 363 364 static int 365 realloc_error_message(char **target, int curlen, int nextlen) 366 { 367 if (curlen < nextlen + MEMORY_MARGIN) { 368 ppm_log(LOG_WARNING, 369 "ppm: Reallocating szErrStr from %d to %d", curlen, 370 nextlen + MEMORY_MARGIN); 371 ber_memfree(*target); 372 curlen = nextlen + MEMORY_MARGIN; 373 *target = (char *) ber_memalloc(curlen); 374 } 375 376 return curlen; 377 } 378 379 // Does the password contains a token from the RDN ? 380 int 381 containsRDN(char* passwd, char* DN) 382 { 383 char lDN[DN_MAX_LEN]; 384 char * tmpToken; 385 char * token; 386 regex_t regex; 387 int reti; 388 389 strcpy_safe(lDN, DN, DN_MAX_LEN); 390 391 // Extract the RDN from the DN 392 tmpToken = strtok(lDN, ",+"); 393 tmpToken = strtok(tmpToken, "="); 394 tmpToken = strtok(NULL, "="); 395 396 // Search for each token in the password */ 397 token = strtok(tmpToken, TOKENS_DELIMITERS); 398 399 while (token != NULL) 400 { 401 if (strlen(token) > 2) 402 { 403 ppm_log(LOG_NOTICE, "ppm: Checking if %s part of RDN matches the password", token); 404 // Compile regular expression 405 reti = regcomp(®ex, token, REG_ICASE); 406 if (reti) { 407 ppm_log(LOG_ERR, "ppm: Cannot compile regex: %s", token); 408 return 0; 409 } 410 411 // Execute regular expression 412 reti = regexec(®ex, passwd, 0, NULL, 0); 413 if (!reti) 414 { 415 regfree(®ex); 416 return 1; 417 } 418 419 regfree(®ex); 420 } 421 else 422 { 423 ppm_log(LOG_NOTICE, "ppm: %s part of RDN is too short to be checked", token); 424 } 425 token = strtok(NULL, TOKENS_DELIMITERS); 426 } 427 428 return 0; 429 } 430 431 432 int 433 check_password(char *pPasswd, char **ppErrStr, Entry *e, void *pArg) 434 { 435 436 Entry *pEntry = e; 437 ppm_log(LOG_NOTICE, "ppm: entry %s", pEntry->e_nname.bv_val); 438 439 struct berval *pwdCheckModuleArg = pArg; 440 /* Determine if config file is to be read (DEPRECATED) */ 441 #ifdef PPM_READ_FILE 442 ppm_log(LOG_NOTICE, "ppm: Not reading pwdCheckModuleArg attribute"); 443 ppm_log(LOG_NOTICE, "ppm: instead, read configuration file (deprecated)"); 444 #else 445 ppm_log(LOG_NOTICE, "ppm: Reading pwdCheckModuleArg attribute"); 446 ppm_log(LOG_NOTICE, "ppm: RAW configuration: %s", 447 (*(struct berval*)pwdCheckModuleArg).bv_val); 448 #endif 449 450 char *szErrStr = (char *) ber_memalloc(MEM_INIT_SZ); 451 int mem_len = MEM_INIT_SZ; 452 int numParam = 0; // Number of params in current configuration 453 454 int useCracklib; 455 char cracklibDict[VALUE_MAX_LEN]; 456 char cracklibDictFiles[3][(VALUE_MAX_LEN+5)]; 457 char const* cracklibExt[] = { ".hwm", ".pwd", ".pwi" }; 458 FILE* fd; 459 char* res; 460 int minQuality; 461 int checkRDN; 462 char forbiddenChars[VALUE_MAX_LEN]; 463 int nForbiddenChars = 0; 464 int nQuality = 0; 465 int maxConsecutivePerClass; 466 int nbInClass[CONF_MAX_SIZE]; 467 int i,j; 468 469 /* Determine config file (DEPRECATED) */ 470 #ifdef PPM_READ_FILE 471 char ppm_config_file[FILENAME_MAX_LEN]; 472 strcpy_safe(ppm_config_file, getenv("PPM_CONFIG_FILE"), FILENAME_MAX_LEN); 473 if (ppm_config_file[0] == '\0') { 474 strcpy_safe(ppm_config_file, CONFIG_FILE, FILENAME_MAX_LEN); 475 } 476 ppm_log(LOG_NOTICE, "ppm: reading config file from %s", ppm_config_file); 477 #endif 478 479 for (i = 0; i < CONF_MAX_SIZE; i++) 480 nbInClass[i] = 0; 481 482 /* Set default values */ 483 conf fileConf[CONF_MAX_SIZE] = { 484 {"minQuality", typeInt, {.iVal = DEFAULT_QUALITY}, 0, 0 485 } 486 , 487 {"checkRDN", typeInt, {.iVal = 0}, 0, 0 488 } 489 , 490 {"forbiddenChars", typeStr, {.sVal = ""}, 0, 0 491 } 492 , 493 {"maxConsecutivePerClass", typeInt, {.iVal = 0}, 0, 0 494 } 495 , 496 {"useCracklib", typeInt, {.iVal = 0}, 0, 0 497 } 498 , 499 {"cracklibDict", typeStr, {.sVal = "/var/cache/cracklib/cracklib_dict"}, 0, 0 500 } 501 , 502 {"class-upperCase", typeStr, {.sVal = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"}, 0, 1 503 } 504 , 505 {"class-lowerCase", typeStr, {.sVal = "abcdefghijklmnopqrstuvwxyz"}, 0, 1 506 } 507 , 508 {"class-digit", typeStr, {.sVal = "0123456789"}, 0, 1 509 } 510 , 511 {"class-special", typeStr, 512 {.sVal = "<>,?;.:/!§ù%*µ^¨$£²&é~\"#'{([-|è`_\\ç^à@)]°=}+"}, 0, 1 513 } 514 }; 515 numParam = 10; 516 517 #ifdef PPM_READ_FILE 518 /* Read configuration file (DEPRECATED) */ 519 read_config_file(fileConf, &numParam, ppm_config_file); 520 #else 521 /* Read configuration attribute (pwdCheckModuleArg) */ 522 read_config_attr(fileConf, &numParam, (*(struct berval*)pwdCheckModuleArg).bv_val); 523 #endif 524 525 minQuality = getValue(fileConf, numParam, "minQuality")->iVal; 526 checkRDN = getValue(fileConf, numParam, "checkRDN")->iVal; 527 strcpy_safe(forbiddenChars, 528 getValue(fileConf, numParam, "forbiddenChars")->sVal, 529 VALUE_MAX_LEN); 530 maxConsecutivePerClass = getValue(fileConf, numParam, "maxConsecutivePerClass")->iVal; 531 useCracklib = getValue(fileConf, numParam, "useCracklib")->iVal; 532 strcpy_safe(cracklibDict, 533 getValue(fileConf, numParam, "cracklibDict")->sVal, 534 VALUE_MAX_LEN); 535 536 537 /*The password must have at least minQuality strength points with one 538 * point granted if the password contains at least minForPoint characters for each class 539 * It must contains at least min chars of each class 540 * It must not contain any char in forbiddenChar */ 541 542 for (i = 0; i < strlen(pPasswd); i++) { 543 544 int n; 545 for (n = 0; n < numParam; n++) { 546 if (strstr(fileConf[n].param, "class-") != NULL) { 547 if (strchr(fileConf[n].value.sVal, pPasswd[i]) != NULL) { 548 ++(nbInClass[n]); 549 } 550 } 551 } 552 if (strchr(forbiddenChars, pPasswd[i]) != NULL) { 553 nForbiddenChars++; 554 } 555 } 556 557 // Password checking done, now loocking for minForPoint criteria 558 for (i = 0; i < CONF_MAX_SIZE; i++) { 559 if (strstr(fileConf[i].param, "class-") != NULL) { 560 if ((nbInClass[i] >= fileConf[i].minForPoint) 561 && strlen(fileConf[i].value.sVal) != 0) { 562 // 1 point granted 563 ++nQuality; 564 ppm_log(LOG_NOTICE, "ppm: 1 point granted for class %s", 565 fileConf[i].param); 566 } 567 } 568 } 569 570 if (nQuality < minQuality) { 571 mem_len = realloc_error_message(&szErrStr, mem_len, 572 strlen(PASSWORD_QUALITY_SZ) + 573 strlen(pEntry->e_nname.bv_val) + 4); 574 sprintf(szErrStr, PASSWORD_QUALITY_SZ, pEntry->e_nname.bv_val, 575 nQuality, minQuality); 576 goto fail; 577 } 578 // Password checking done, now loocking for constraintClass criteria 579 for (i = 0; i < CONF_MAX_SIZE; i++) { 580 if (strstr(fileConf[i].param, "class-") != NULL) { 581 if ((nbInClass[i] < fileConf[i].min) && 582 strlen(fileConf[i].value.sVal) != 0) { 583 // constraint is not satisfied... goto fail 584 mem_len = realloc_error_message(&szErrStr, mem_len, 585 strlen(PASSWORD_CRITERIA) + 586 strlen(pEntry->e_nname.bv_val) + 587 2 + PARAM_MAX_LEN); 588 sprintf(szErrStr, PASSWORD_CRITERIA, pEntry->e_nname.bv_val, 589 fileConf[i].min, fileConf[i].param); 590 goto fail; 591 } 592 } 593 } 594 595 // Password checking done, now loocking for forbiddenChars criteria 596 if (nForbiddenChars > 0) { // at least 1 forbidden char... goto fail 597 mem_len = realloc_error_message(&szErrStr, mem_len, 598 strlen(PASSWORD_FORBIDDENCHARS) + 599 strlen(pEntry->e_nname.bv_val) + 2 + 600 VALUE_MAX_LEN); 601 sprintf(szErrStr, PASSWORD_FORBIDDENCHARS, pEntry->e_nname.bv_val, 602 nForbiddenChars, forbiddenChars); 603 goto fail; 604 } 605 606 // Password checking done, now loocking for maxConsecutivePerClass criteria 607 for (i = 0; i < CONF_MAX_SIZE; i++) { 608 if (strstr(fileConf[i].param, "class-") != NULL) { 609 if ( maxConsecutivePerClass != 0 && 610 (maxConsPerClass(pPasswd,fileConf[i].value.sVal) 611 > maxConsecutivePerClass)) { 612 // Too much consecutive characters of the same class 613 ppm_log(LOG_NOTICE, "ppm: Too much consecutive chars for class %s", 614 fileConf[i].param); 615 mem_len = realloc_error_message(&szErrStr, mem_len, 616 strlen(PASSWORD_MAXCONSECUTIVEPERCLASS) + 617 strlen(pEntry->e_nname.bv_val) + 2 + 618 PARAM_MAX_LEN); 619 sprintf(szErrStr, PASSWORD_MAXCONSECUTIVEPERCLASS, pEntry->e_nname.bv_val, 620 maxConsecutivePerClass, fileConf[i].param); 621 goto fail; 622 } 623 } 624 } 625 #ifdef CRACKLIB 626 // Password checking done, now loocking for cracklib criteria 627 if ( useCracklib > 0 ) { 628 629 for( j = 0 ; j < 3 ; j++) { 630 strcpy_safe(cracklibDictFiles[j], cracklibDict, VALUE_MAX_LEN); 631 strcat(cracklibDictFiles[j], cracklibExt[j]); 632 if (( fd = fopen ( cracklibDictFiles[j], "r")) == NULL ) { 633 ppm_log(LOG_NOTICE, "ppm: Error while reading %s file", 634 cracklibDictFiles[j]); 635 mem_len = realloc_error_message(&szErrStr, mem_len, 636 strlen(GENERIC_ERROR)); 637 sprintf(szErrStr, GENERIC_ERROR); 638 goto fail; 639 640 } 641 else { 642 fclose (fd); 643 } 644 } 645 res = (char *) FascistCheck (pPasswd, cracklibDict); 646 if ( res != NULL ) { 647 ppm_log(LOG_NOTICE, "ppm: cracklib does not validate password for entry %s", 648 pEntry->e_nname.bv_val); 649 mem_len = realloc_error_message(&szErrStr, mem_len, 650 strlen(PASSWORD_CRACKLIB) + 651 strlen(pEntry->e_nname.bv_val)); 652 sprintf(szErrStr, PASSWORD_CRACKLIB, pEntry->e_nname.bv_val); 653 goto fail; 654 655 } 656 657 } 658 #endif 659 660 // Password checking done, now looking for checkRDN criteria 661 if (checkRDN == 1 && containsRDN(pPasswd, pEntry->e_nname.bv_val)) 662 // RDN check enabled and a token from RDN is found in password: goto fail 663 { 664 mem_len = realloc_error_message(&szErrStr, mem_len, 665 strlen(RDN_TOKEN_FOUND) + 666 strlen(pEntry->e_nname.bv_val)); 667 sprintf(szErrStr, RDN_TOKEN_FOUND, pEntry->e_nname.bv_val); 668 669 goto fail; 670 } 671 672 *ppErrStr = strdup(""); 673 ber_memfree(szErrStr); 674 return (LDAP_SUCCESS); 675 676 fail: 677 *ppErrStr = strdup(szErrStr); 678 ber_memfree(szErrStr); 679 return (EXIT_FAILURE); 680 681 } 682