1#!/usr/bin/env perl 2# --------------------------------------------------------------------------- 3# Copyright (C) 2000-2020 TJ Saunders <tj@castaglia.org> 4# 5# This program is free software; you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation; either version 2 of the License, or 8# (at your option) any later version. 9# 10# This program is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with this program; if not, write to the Free Software 17# Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA. 18# 19# Based on MacGuyver's genuser.pl script, this script generates password 20# files suitable for use with proftpd's AuthUserFile directive, in passwd(5) 21# format, or AuthGroupFile, in group(5) format. The idea is somewhat similar 22# to Apache's htpasswd program. 23# --------------------------------------------------------------------------- 24 25use strict; 26 27use Fcntl qw(:flock); 28use File::Basename qw(basename); 29use Getopt::Long; 30 31# turn off auto abbreviation 32$Getopt::Long::auto_abbrev = 0; 33 34my $program = basename($0); 35my $default_passwd_file = "./ftpd.passwd"; 36my $default_group_file = "./ftpd.group"; 37my $shell_file = "/etc/shells"; 38my $default_cracklib_dict = "/usr/lib/cracklib_dict"; 39my $cracklib_dict; 40my $output_file; 41my $version = "1.3.0"; 42 43my @data; 44 45my %opts = (); 46GetOptions(\%opts, 47 'add-member=s', 48 'change-home', 49 'change-password', 50 'delete-group', 51 'delete-member=s', 52 'delete-user', 53 'des', 54 'enable-group-passwd', 55 'file=s', 56 'F|force', 57 'gecos=s', 58 'gid=n', 59 'group', 60 'hash', 61 'h|help', 62 'home=s', 63 'l|lock', 64 'md5', 65 'm|member=s@', 66 'name=s', 67 'not-previous-password', 68 'not-system-password', 69 'passwd', 70 'sha256', 71 'sha512', 72 'shell=s', 73 'stdin', 74 'uid=n', 75 'u|unlock', 76 'use-cracklib:s', 77 'version', 78); 79 80usage() if (defined($opts{'h'})); 81 82version() if (defined($opts{'version'})); 83 84# Per Bug#4171, check if we are on a Linux system, AND it has the 85# /proc/sys/crypto/fips_enabled file, AND that entry says that FIPS mode is 86# enabled. If these conditions are met, then neither DES or MD5 will work. 87# 88# If either --des or --md5 are specified, OR if no hash is specified, we 89# need to check. 90if ((defined($opts{'des'}) || defined($opts{'md5'})) || 91 !defined($opts{'des'}) && !defined($opts{'md5'}) && 92 !defined($opts{'sha256'}) && !defined($opts{'sha512'})) { 93 if (open(my $fh, "< /proc/sys/crypto/fips_enabled")) { 94 my $fips_enabled = <$fh>; 95 close($fh); 96 97 chomp($fips_enabled); 98 if ($fips_enabled) { 99 die "$program: FIPS mode enabled on your system (see /proc/sys/crypto/fips_enabled), thus --des and --md5 will not be supported. Use --sha256 or --sha512.\n" 100 } 101 } 102} 103 104# check if "use-cracklib" was given as an option, and whether a path 105# to other dictionary files was given. 106if (defined($opts{'use-cracklib'})) { 107 108 # make sure that Crypt::Cracklib is installed before trying to use 109 # it later 110 eval { require Crypt::Cracklib }; 111 die "$program: --use-cracklib requires Crypt::Cracklib to be installed\n" if $@; 112 113 if ($opts{'use-cracklib'} ne "") { 114 $cracklib_dict = $opts{'use-cracklib'}; 115 116 } else { 117 $cracklib_dict = $default_cracklib_dict; 118 } 119} 120 121# Make sure that the given --home path exists, and is a directory. 122if (exists($opts{'home'})) { 123 my $path = $opts{'home'}; 124 unless (-e $path) { 125 die "$program: --home $path does not exist\n"; 126 } 127 128 unless (-d $path) { 129 die "$program: --home $path is not a directory\n"; 130 } 131} 132 133# make sure that both passwd and group modes haven't been simultaneously 134# requested 135if ((exists($opts{'passwd'}) && exists($opts{'group'})) || 136 (exists($opts{'passwd'}) && exists($opts{'hash'})) || 137 (exists($opts{'group'}) && exists($opts{'hash'}))) { 138 die "$program: please use *one*: --passwd, --group, or --hash\n"; 139 140} elsif (defined($opts{'passwd'})) { 141 142 # determine to which file to write the passwd entry 143 if (defined($opts{'file'})) { 144 $output_file = $opts{'file'}; 145 print STDOUT "$program: using alternate file: $output_file\n" 146 147 } else { 148 $output_file = $default_passwd_file; 149 } 150 151 # make sure that the required arguments are present 152 die "$program: --passwd: missing required argument: --name\n" 153 unless (defined($opts{'name'})); 154 155 # check for and handle the --delete-user option. 156 if (defined($opts{'delete-user'})) { 157 open_output_file(); 158 159 my ($pass, $uid, $gid, $gecos, $home, $shell) = find_passwd_entry(name => 160 $opts{'name'}); 161 162 handle_passwd_entry(name => $opts{'name'}, uid => $uid, gid => $gid, 163 gecos => $gecos, home => $home, shell => $shell, 164 delete_user => $opts{'delete-user'}); 165 166 close_output_file(); 167 168 # done 169 exit 0; 170 } 171 172 # check for and handle the --lock option. 173 if (defined($opts{'l'})) { 174 open_output_file(); 175 176 my ($pass, $uid, $gid, $gecos, $home, $shell) = find_passwd_entry(name => 177 $opts{'name'}); 178 179 my $new_passwd = $pass; 180 181 # If this password is already "locked", leave it alone 182 if ($new_passwd !~ /^!/) { 183 $new_passwd = '!' . $new_passwd; 184 } 185 186 handle_passwd_entry(name => $opts{'name'}, uid => $uid, gid => $gid, 187 gecos => $gecos, home => $home, shell => $shell, 188 new_passwd => $new_passwd); 189 190 close_output_file(); 191 192 # done 193 exit 0; 194 } 195 196 # check for and handle the --unlock option. 197 if (defined($opts{'u'})) { 198 open_output_file(); 199 200 my ($pass, $uid, $gid, $gecos, $home, $shell) = find_passwd_entry(name => 201 $opts{'name'}); 202 203 my $new_passwd = $pass; 204 $new_passwd =~ s/^!+//; 205 206 handle_passwd_entry(name => $opts{'name'}, uid => $uid, gid => $gid, 207 gecos => $gecos, home => $home, shell => $shell, 208 new_passwd => $new_passwd); 209 210 close_output_file(); 211 212 # done 213 exit 0; 214 } 215 216 # now check for the --change-password option. If present, lookup 217 # the given name in the password file, and reuse all the information 218 # except for the password 219 if (defined($opts{'change-password'})) { 220 open_output_file(); 221 222 my ($pass, $uid, $gid, $gecos, $home, $shell) = find_passwd_entry(name => 223 $opts{'name'}); 224 225 handle_passwd_entry(name => $opts{'name'}, uid => $uid, gid => $gid, 226 gecos => $gecos, home => $home, shell => $shell); 227 228 close_output_file(); 229 230 # done 231 exit 0; 232 } 233 234 # Now check for the --change-home option. If present, lookup the given name 235 # in the password file, and reuse all the information except for the home. 236 if (defined($opts{'change-home'})) { 237 if (!defined($opts{'home'})) { 238 die "$program: --change-home requires use of --home\n"; 239 } 240 241 open_output_file(); 242 243 my ($pass, $uid, $gid, $gecos, $home, $shell) = find_passwd_entry(name => 244 $opts{'name'}); 245 246 # We trick this function into not prompting for a password by acting 247 # as if we are handling a new password. 248 handle_passwd_entry(name => $opts{'name'}, uid => $uid, gid => $gid, 249 gecos => $gecos, home => $opts{'home'}, shell => $shell, 250 new_passwd => $pass); 251 252 close_output_file(); 253 254 # done 255 exit 0; 256 } 257 258 # check for the --not-system-password option. If present, make sure that 259 # a) the script is running with root privs, and b) perl on the system is 260 # such that getpwnam() will return the system password 261 if (defined($opts{'not-system-password'})) { 262 die "$program: must be user root for system password check\n" 263 unless ($> == 0); 264 } 265 266 die "$program: --passwd: missing required argument: --home\n" 267 unless (defined($opts{'home'})); 268 269 die "$program: --passwd: missing required argument: --shell\n" 270 unless (defined($opts{'shell'})); 271 272 die "$program: --passwd: missing required argument: --uid\n" 273 unless (defined($opts{'uid'})); 274 275 # As per Flying Hamster's suggestion, have $opts{'gid'} default to --uid 276 # if none are specified on the command-line via --gid 277 unless (defined($opts{'gid'})) { 278 $opts{'gid'} = $opts{'uid'}; 279 warn "$program: --passwd: missing --gid argument: default gid set to uid\n"; 280 } 281 282 open_output_file(); 283 284 handle_passwd_entry(name => $opts{'name'}, uid => $opts{'uid'}, 285 gid => $opts{'gid'}, gecos => $opts{'gecos'}, home => $opts{'home'}, 286 shell => $opts{'shell'}, delete_user => $opts{'delete-user'}); 287 288 close_output_file(); 289 290 # NOTE: if this process is not running as root, then the file generated 291 # is not owned by root. Issue a warning reminding the user to make the 292 # generated file mode 0400, owned by root, before using it. 293 294} elsif (defined($opts{'group'})) { 295 296 # determine to which file to write the group entry 297 if (defined($opts{'file'})) { 298 $output_file = $opts{'file'}; 299 print STDOUT "$program: using alternate file: $output_file\n"; 300 301 } else { 302 $output_file = $default_group_file; 303 } 304 305 # check for and handle the --delete-group option. 306 if (defined($opts{'delete-group'})) { 307 open_output_file(); 308 309 handle_group_entry( 310 name => $opts{'name'}, 311 delete_group => $opts{'delete-group'} 312 ); 313 314 close_output_file(); 315 316 # done 317 exit 0; 318 } 319 320 # make sure the required options are present 321 if (!defined($opts{'add-member'}) && 322 !defined($opts{'delete-member'})) { 323 die "$program: --group: missing required argument: --gid\n" 324 unless (defined($opts{'gid'})); 325 } 326 327 die "$program: --group: missing required argument: --name\n" 328 unless (defined($opts{'name'})); 329 330 open_output_file(); 331 332 handle_group_entry( 333 gid => $opts{'gid'}, 334 members => $opts{'m'}, 335 name => $opts{'name'}, 336 add_user => $opts{'add-member'}, 337 delete_group => $opts{'delete-group'}, 338 delete_user => $opts{'delete-member'} 339 ); 340 341 close_output_file(); 342 343} elsif (defined($opts{'hash'})) { 344 print STDOUT "$program: ", get_passwd(), "\n"; 345 346} else { 347 die "$program: missing required --passwd or --group\n$program: use $program --help for details on usage\n\n"; 348} 349 350# done 351exit 0; 352 353# ---------------------------------------------------------------------------- 354sub check_shell { 355 my %args = @_; 356 357 my $shell = $args{'shell'}; 358 my $result = 0; 359 360 # check the given shell against the list in /etc/shells. If not present 361 # there, issue a message recognizing this, and suggesting that 362 # RequireValidShell be set to off, and that any necessary PAM modules be 363 # adjusted. 364 365 unless (open(SHELLS, "< $shell_file")) { 366 warn "$program: unable to open $shell_file: $!\n"; 367 warn "$program: skipping check of $shell_file\n"; 368 return; 369 } 370 371 while(my $line = <SHELLS>) { 372 chomp($line); 373 374 if ($line eq $shell) { 375 $result = 1; 376 last; 377 } 378 } 379 380 close(SHELLS); 381 382 unless ($result) { 383 print STDOUT "\n$program: $shell is not among the valid system shells. Use of\n"; 384 print STDOUT "$program: \"RequireValidShell off\" may be required, and the PAM\n"; 385 print STDOUT "$program: module configuration may need to be adjusted.\n\n"; 386 } 387 388 return $result; 389} 390 391# ---------------------------------------------------------------------------- 392sub close_output_file { 393 my %args = @_; 394 395 if (open(my $fh, "> $output_file")) { 396 if (flock($fh, LOCK_EX|LOCK_NB)) { 397 # flush the data to the file 398 foreach my $line (@data) { 399 print $fh "$line\n"; 400 } 401 402 # set the permissions appropriately, ie 0440, before closing the file 403 unless (chmod(0440, $output_file)) { 404 flock($fh, LOCK_UN); 405 die("$program: unable to set permissions on $output_file: $!"); 406 } 407 408 flock($fh, LOCK_UN); 409 410 unless (close($fh)) { 411 die("$program: unable to close $output_file: $!\n"); 412 } 413 414 } else { 415 close($fh); 416 die("$program: unable to write $output_file: Locked (in use) by another process\n"); 417 } 418 419 } else { 420 die("$program: unable to open $output_file: $!\n"); 421 } 422} 423 424# ---------------------------------------------------------------------------- 425sub find_passwd_entry { 426 my %args = @_; 427 428 my $name = $args{'name'}; 429 my ($pass, $uid, $gid, $gecos, $home, $shell); 430 my $found = 0; 431 432 # given a name, find the corresponding entry in the passwd file 433 foreach my $line (@data) { 434 next unless $line =~ /^$name:/; 435 436 my @fields = split(':', $line); 437 438 $pass = $fields[1]; 439 $uid = $fields[2]; 440 $gid = $fields[3]; 441 $gecos = $fields[4]; 442 $home = $fields[5]; 443 $shell = $fields[6]; 444 445 $found = 1; 446 447 last; 448 } 449 450 unless ($found) { 451 print STDOUT "$program: error: no such user $name in $output_file\n"; 452 453 # Restore the file permissions. 454 unless (chmod(0440, $output_file)) { 455 print STDERR "$program: unable to set permissions on $output_file: $!"; 456 } 457 458 exit 1; 459 } 460 461 return ($pass, $uid, $gid, $gecos, $home, $shell); 462} 463 464# ---------------------------------------------------------------------------- 465sub get_salt { 466 my $salt; 467 468 # The determination of with encryption algorithm to use is done via 469 # the salt. The format and nature of the salt is how crypt(3) knows 470 # how to do its thing. By default, generate a salt that triggers MD5. 471 472 if (defined($opts{'des'})) { 473 # DES salt 474 $salt = join '', ('.', '/', 0..9, 'A'..'Z', 'a'..'z')[rand 64, rand 64]; 475 476 } elsif (defined($opts{'sha256'})) { 477 # SHA-256 salt (16 characters) 478 $salt = join '', (0..9, 'A'..'Z', 'a'..'z') 479 [rand 62, rand 62, rand 62, rand 62, 480 rand 62, rand 62, rand 62, rand 62, 481 rand 62, rand 62, rand 62, rand 62, 482 rand 62, rand 62, rand 62, rand 62]; 483 $salt = '$5$' . $salt; 484 485 } elsif (defined($opts{'sha512'})) { 486 # SHA-512 salt (16 characters) 487 $salt = join '', (0..9, 'A'..'Z', 'a'..'z') 488 [rand 62, rand 62, rand 62, rand 62, 489 rand 62, rand 62, rand 62, rand 62, 490 rand 62, rand 62, rand 62, rand 62, 491 rand 62, rand 62, rand 62, rand 62]; 492 $salt = '$6$' . $salt; 493 494 } else { 495 # MD5 salt (16 characters) 496 $salt = join '', (0..9, 'A'..'Z', 'a'..'z') 497 [rand 62, rand 62, rand 62, rand 62, 498 rand 62, rand 62, rand 62, rand 62, 499 rand 62, rand 62, rand 62, rand 62, 500 rand 62, rand 62, rand 62, rand 62]; 501 $salt = '$1$' . $salt; 502 } 503 504 return $salt; 505} 506 507# ---------------------------------------------------------------------------- 508sub get_passwd { 509 my %args = @_; 510 my $name = $args{'name'}; 511 my ($passwd, $passwd2); 512 513 # If using a DES salt, print an informative message about the 8 character 514 # limit of relevant password characters. 515 516 if (defined($opts{'des'}) && !defined($opts{'stdin'})) { 517 print STDOUT "\nPlease be aware that only the first 8 characters of a DES password are\nrelevant. Use the --md5, --sha256, or --sha512 options as they do not have\nthis limitation.\n"; 518 } 519 520 if (defined($opts{'stdin'})) { 521 522 # simply read in the password from stdin, as from a script 523 chomp($passwd = <STDIN>); 524 525 } else { 526 527 # Install a SIGINT handler, for cases where Ctrl-C may be used to abort 528 # the prompt. 529 $SIG{INT} = sub { 530 # Restore the file permissions. 531 unless (chmod(0440, $output_file)) { 532 print STDERR "$program: unable to set permissions on $output_file: $!"; 533 } 534 535 # Restore the terminal echo behavior, too. 536 system "stty echo"; 537 538 exit 1; 539 }; 540 541 # Prompt for the password to be used 542 system "stty -echo"; 543 print STDOUT "\nPassword: "; 544 545 # Open the tty for reading (is this portable?) 546 open(TTY, "/dev/tty") or die "$program: unable to open /dev/tty: $!\n"; 547 chomp($passwd = <TTY>); 548 print STDOUT "\n"; 549 system "stty echo"; 550 551 # Prompt again, to make sure the user typed in the password correctly 552 system "stty -echo"; 553 print STDOUT "Re-type password: "; 554 chomp($passwd2 = <TTY>); 555 print STDOUT "\n\n"; 556 system "stty echo"; 557 close(TTY); 558 559 # Restore default SIGINT handling 560 $SIG{INT} = 'DEFAULT'; 561 562 if ($passwd2 ne $passwd) { 563 print STDOUT "Passwords do not match. Please try again.\n"; 564 return get_passwd(name => $name); 565 } 566 } 567 568 if (defined($name) && defined($opts{'change-password'})) { 569 570 # retrieve the user's current password from the file and compare 571 my ($curpasswd, @junk) = find_passwd_entry(name => $name); 572 573 my $hash = crypt($passwd, $curpasswd); 574 575 if ($hash eq $curpasswd) { 576 if (defined($opts{'stdin'})) { 577 # cannot prompt again if automated. Simply print an error message 578 # and exit. 579 print STDOUT "$program: error: password matches current password\n"; 580 581 # Restore the file permissions. 582 unless (chmod(0440, $output_file)) { 583 print STDERR "$program: unable to set permissions on $output_file: $!"; 584 } 585 586 exit 2; 587 588 } else { 589 print STDOUT "Please use a password that is different from your current password.\n"; 590 return get_passwd(name => $name); 591 } 592 } 593 } 594 595 if (defined($name) && defined($opts{'not-previous-password'})) { 596 # retrieve the user's current passwd and compare 597 my ($currpasswd) = find_passwd_entry(name => $name); 598 599 my $hash = crypt($passwd, $currpasswd); 600 601 if (($hash && $currpasswd) && $hash eq $currpasswd) { 602 if (defined($opts{'stdin'})) { 603 604 # cannot prompt again if automated. Simply print an error message 605 # and exit. 606 print STDOUT "$program: error: password matches previous password\n"; 607 608 # Restore the file permissions. 609 unless (chmod(0440, $output_file)) { 610 print STDERR "$program: unable to set permissions on $output_file: $!"; 611 } 612 613 exit 4; 614 615 } else { 616 print STDOUT "Please use a password that is different from your previous password.\n"; 617 return get_passwd(name => $name); 618 } 619 } 620 } 621 622 if (defined($name) && defined($opts{'not-system-password'})) { 623 # retrieve the user's system passwd (from /etc/shadow) and compare 624 my $syspasswd = get_syspasswd(user => $name); 625 626 my $hash = crypt($passwd, $syspasswd); 627 628 if (($hash && $syspasswd) && $hash eq $syspasswd) { 629 if (defined($opts{'stdin'})) { 630 # Cannot prompt again if automated. Simply print an error message 631 # and exit. 632 print STDOUT "$program: error: password matches system password\n"; 633 634 # Restore the file permissions. 635 unless (chmod(0440, $output_file)) { 636 print STDERR "$program: unable to set permissions on $output_file: $!"; 637 } 638 639 exit 4; 640 641 } else { 642 print STDOUT "Please use a password that is different from your system password.\n"; 643 return get_passwd(name => $name); 644 } 645 } 646 } 647 648 return "" if ($args{'allow_blank'} and $passwd eq ""); 649 650 # check for BAD passwords, BLANK passwords, etc, if requested 651 if (defined($opts{'use-cracklib'})) { 652 require Crypt::Cracklib; 653 if (!Crypt::Cracklib::check($passwd, $cracklib_dict)) { 654 print STDOUT "Bad password: ", Crypt::Cracklib::fascist_check($passwd, 655 $cracklib_dict), "\n"; 656 return get_passwd(name => $name); 657 } 658 } 659 660 my $salt = get_salt(); 661 662 my $hash = crypt($passwd, $salt); 663 664 # Check that the crypt() implementation properly supports use of the MD5 665 # (or other non-DES algorithm), if specified. 666 if (!defined($opts{'des'})) { 667 if (defined($opts{'md5'})) { 668 # if the first three characters of the hash are not "$1$", the crypt() 669 # implementation doesn't support MD5. Some crypt()s will happily use 670 # "$1" as a salt even though this is not a valid DES salt. Humf. 671 # 672 # Perl doesn't treat strings as arrays of characters, so extracting the 673 # first three characters is a little more convoluted (I'm accustomed to 674 # C's strncmp(3) for this now). 675 676 my @string = split('', $hash); 677 my $prefix = $string[0] . $string[1] . $string[2]; 678 679 if ($prefix ne '$1$') { 680 print STDOUT "You requested MD5 passwords but your system does not support it. Defaulting to DES passwords.\n\n"; 681 } 682 683 } elsif (defined($opts{'sha256'})) { 684 # if the first three characters of the hash are not "$5$", the crypt() 685 # implementation doesn't support SHA-256. Some crypt()s will happily use 686 # "$5" as a salt even though this is not a valid DES salt. Humf. 687 # 688 # Perl doesn't treat strings as arrays of characters, so extracting the 689 # first three characters is a little more convoluted (I'm accustomed to 690 # C's strncmp(3) for this now). 691 692 my @string = split('', $hash); 693 my $prefix = $string[0] . $string[1] . $string[2]; 694 695 if ($prefix ne '$5$') { 696 print STDOUT "You requested SHA-256 passwords but your system does not support it. Defaulting to DES passwords.\n\n"; 697 } 698 699 } elsif (defined($opts{'sha512'})) { 700 # if the first three characters of the hash are not "$6$", the crypt() 701 # implementation doesn't support SHA-512. Some crypt()s will happily use 702 # "$6" as a salt even though this is not a valid DES salt. Humf. 703 # 704 # Perl doesn't treat strings as arrays of characters, so extracting the 705 # first three characters is a little more convoluted (I'm accustomed to 706 # C's strncmp(3) for this now). 707 708 my @string = split('', $hash); 709 my $prefix = $string[0] . $string[1] . $string[2]; 710 711 if ($prefix ne '$6$') { 712 print STDOUT "You requested SHA-512 passwords but your system does not support it. Defaulting to DES passwords.\n\n"; 713 } 714 } 715 } 716 717 return $hash; 718} 719 720# ---------------------------------------------------------------------------- 721sub get_syspasswd { 722 my %args = @_; 723 724 my $user = $args{'user'}; 725 726 # test the shadow password support on this system. Some systems, such 727 # as the BSDs, use "transparent shadowing", where the real passwd will 728 # be returned via getpwnam() only if the process has root privs (effective 729 # UID of zero). That check has already been performed. However, other 730 # systems still may not return the password via getpwnam() (such as Linux). 731 # These other systems use a shadow password library of functions, and require 732 # other work to retrieve the password. On these systems, the retrieved 733 # password will be "x". 734 735 my $syspasswd = (getpwnam($user))[1]; 736 737 if ($syspasswd eq "" || $syspasswd eq "x") { 738 739 # do the retrieval the hard way: open up /etc/shadow and iterate 740 # through each line. Yuck. *sigh*. Thanks to Micah Anderson 741 # for working out this issue. 742 743 open(SHADOW, "< /etc/shadow") or 744 die "$program: unable to access shadow file: $!\n"; 745 746 while (chomp(my $line = <SHADOW>)) { 747 next unless $line =~ /^$user/; 748 $syspasswd = (split(':', $line))[1]; 749 last; 750 } 751 close(SHADOW); 752 753 # if the password is still "x", you have problems 754 if ($syspasswd eq "x") { 755 die "$program: unable to retrieve shadow password.\nContact your system 756administrator.\n"; 757 } 758 } 759 760 return $syspasswd; 761} 762 763# ---------------------------------------------------------------------------- 764sub handle_group_entry { 765 my %args = @_; 766 767 my $gid = $args{'gid'}; 768 my $name = $args{'name'}; 769 my $delete_group = $args{'delete_group'}; 770 my $delete_user = $args{'delete_user'}; 771 my $add_user = $args{'add_user'}; 772 my $passwd; 773 774 my $members = ""; 775 $members = join(',', @{$args{'members'}}) if (defined($args{'members'})); 776 777 # check to see whether we should update the fields for this group (because 778 # it already exists), or to create a new entry 779 780 my $found = 0; 781 my $index = 0; 782 for ($index = 0; $index <= $#data; $index++) { 783 my @entry = split(':', $data[$index]); 784 785 if ($name eq $entry[0]) { 786 $found = 1; 787 788 # If we have not been given an explicit password, reuse the existing one. 789 $passwd = $entry[1] unless $passwd; 790 791 # If we have not been given an explicit GID, reuse the existing one. 792 $gid = $entry[2] unless $gid; 793 794 # If we have not been given explicit members, reuse the existing ones. 795 $members = $entry[3] if $members eq ''; 796 797 last; 798 } 799 } 800 801 unless ($found) { 802 print STDOUT "$program: creating group entry for group $name\n"; 803 804 } else { 805 print STDOUT "$program: updating group entry for group $name\n"; 806 } 807 808 # if present, add the members given to the group. If none, just leave that 809 # field blank 810 811 # prompt for the group password, if requested 812 if (defined($opts{'enable-group-passwd'})) { 813 $passwd = get_passwd(name => $name, allow_blank => 1); 814 815 } else { 816 $passwd = "x"; 817 } 818 819 # remove the entry to be updated 820 splice(@data, $index, 1); 821 822 if ($delete_group) { 823 print STDOUT "$program: entry deleted\n"; 824 return; 825 } 826 827 if ($delete_user) { 828 $members =~ s/$delete_user//g; 829 $members =~ s/,,/,/g; 830 $members =~ s/^,//g; 831 $members =~ s/,$//g; 832 } 833 834 if ($add_user) { 835 if (length($members) > 0) { 836 $members .= ",$add_user"; 837 838 } else { 839 $members = $add_user; 840 } 841 } 842 843 # Ensure that we have a sorted list of unique members 844 my $uniq_members = { map { $_ => 1 } split(',', $members) }; 845 my $names = join(',', sort { $a <=> $b } keys(%$uniq_members)); 846 847 # format: $name:$passwd:$gid:$members 848 push(@data, "$name:$passwd:$gid:$names"); 849 850 # always sort by GIDs before printing out the file 851 @data = map { $_->[0] } 852 sort { 853 $a->[3] <=> $b->[3] 854 } 855 map { [ $_, (split /:/)[0, 1, 2, 3] ] } 856 @data; 857 858 if ($delete_group) { 859 print STDOUT "$program: entry deleted\n"; 860 861 } elsif ($found) { 862 print STDOUT "$program: entry updated\n"; 863 864 } else { 865 print STDOUT "$program: entry created\n"; 866 } 867} 868 869# ---------------------------------------------------------------------------- 870sub handle_passwd_entry { 871 my %args = @_; 872 873 my $name = $args{'name'}; 874 my $uid = $args{'uid'}; 875 my $gid = $args{'gid'}; 876 my $gecos = $args{'gecos'}; 877 my $home = $args{'home'}; 878 my $shell = $args{'shell'}; 879 my $delete_user = $args{'delete_user'}; 880 my $new_passwd = $args{'new_passwd'}; 881 882 # Trim any trailing slashes in $home. 883 $home =~ s/(.*)\/$/$1/ if ($home =~ /\/$/); 884 885 # Make sure the given home directory is NOT a relative path (what a 886 # horrible idea). 887 888 unless ($home =~ /^\//) { 889 print STDOUT "$program: error: relative path given for home directory\n"; 890 exit 8; 891 } 892 893 # check to see whether we should update the fields for this user (because 894 # they already exist), or create a new entry 895 896 my $found = 0; 897 my $index = 0; 898 for ($index = 0; $index <= $#data; $index++) { 899 my @entry = split(':', $data[$index]); 900 901 if ($name eq $entry[0]) { 902 $found = 1; 903 last; 904 } 905 } 906 907 unless ($found) { 908 print STDOUT "$program: creating passwd entry for user $name\n"; 909 910 } else { 911 print STDOUT "$program: updating passwd entry for user $name\n"; 912 } 913 914 my $passwd; 915 916 if (!$delete_user) { 917 if (!$new_passwd) { 918 # check the requested shell against the list in /etc/shells 919 check_shell(shell => $shell); 920 921 # prompt the user for the password 922 $passwd = get_passwd(name => $name); 923 924 } else { 925 $passwd = $new_passwd; 926 } 927 } 928 929 # remove the entry to be updated 930 splice(@data, $index, 1); 931 932 if ($delete_user) { 933 print STDOUT "$program: entry deleted\n"; 934 return; 935 } 936 937 # format: $name:$passwd:$uid:$gid:$gecos:$home:$shell 938 push(@data, "$name:$passwd:$uid:$gid:$gecos:$home:$shell"); 939 940 # always sort by UIDs before printing out the file 941 @data = map { $_->[0] } 942 sort { 943 $a->[3] <=> $b->[3] 944 } 945 map { [ $_, (split /:/)[0, 1, 2, 3, 4, 5, 6] ] } 946 @data; 947 948 if ($delete_user) { 949 print STDOUT "$program: entry deleted\n"; 950 951 } elsif ($found) { 952 print STDOUT "$program: entry updated\n"; 953 954 } else { 955 print STDOUT "$program: entry created\n"; 956 } 957} 958 959# ---------------------------------------------------------------------------- 960sub open_output_file { 961 my %args = @_; 962 963 # open $output_file, paying attention to the --force command-line option 964 # If the file already exists, slurp up its contents for later updating. 965 966 if (-f $output_file) { 967 # make sure we can write/update the file first 968 unless (chmod(0644, $output_file)) { 969 die "$program: unable to set permissions on $output_file to 0644: $!\n"; 970 } 971 972 if (open(my $fh, "< $output_file")) { 973 if (flock($fh, LOCK_SH|LOCK_NB)) { 974 chomp(@data = <$fh>); 975 flock($fh, LOCK_UN); 976 close($fh); 977 978 } else { 979 close($fh); 980 die("$program: unable to read $output_file: Locked (in use) by another process\n"); 981 } 982 983 984 } else { 985 die("$program: unable to open $output_file: $!\n"); 986 } 987 } 988 989 # if the --force option was given, just zero out any data that might have 990 # been read in, effectively erasing whatever contents there were. A new 991 # file is generated, anyway -- it's just a question of what data goes into 992 # it 993 994 @data = () if (defined($opts{'F'})); 995} 996 997# ---------------------------------------------------------------------------- 998sub usage { 999 1000 print STDOUT <<END_OF_USAGE; 1001 1002usage: $program [--help] [--hash|--group|--passwd] 1003 1004 REQUIRED: --passwd, --group, or --hash. These specify whether $program is to 1005 operate on a passwd(5) format file, on a group(5) format file, or simply 1006 to generate a password hash, respectively. 1007 1008 If used with --passwd, $program creates a file in the passwd(5) format, 1009 suitable for use with proftpd's AuthUserFile configuration directive. 1010 You will be prompted for the password to use of the user, which will be 1011 encrypted, and written out as the encrypted string. New entries are 1012 appended to the file by default. 1013 1014 By default, using --passwd will write output to "$default_passwd_file". 1015 1016 Error exit values: 1017 1018 To make it easier for wrapper scripts to interact with $program, $program 1019 will exit with the following error values for the reasons described: 1020 1021 1 no such user 1022 2 password matches current password 1023 4 password matches system password 1024 8 relative path given for home directory 1025 1026 Options: 1027 1028 --file Write output to specified file, rather than "$default_passwd_file". 1029 1030 -F If the file to be used already exists, delete it and write a 1031 --force new one. By default, new entries will be appended to the file. 1032 1033 --gecos Descriptive string for the given user (usually the user's 1034 full name). 1035 1036 --gid Primary group ID for this user (optional, will default to 1037 given --uid value if absent). 1038 1039 -h Displays this message. 1040 --help 1041 1042 --home Home directory for the user (required). 1043 1044 --des Use the DES algorithm for encrypting passwords. The default 1045 is the MD5 algorithm. 1046 1047 --md5 Use the MD5 algorithm for encrypting passwords. This is the 1048 default. 1049 1050 --name Name of the user account (required). If the name does not 1051 exist in the specified output-file, an entry will be created 1052 for her. Otherwise, the given fields will be updated. 1053 1054 --shell Shell for the user (required). Recommended: /bin/false 1055 1056 --uid Numerical user ID (required) 1057 1058 --change-home 1059 1060 Update only the home directory field for a user. This option 1061 requires that the --name and --passwd options be used, but no 1062 others. 1063 1064 --change-password 1065 1066 Update only the password field for a user. This option 1067 requires that the --name and --passwd options be used, but 1068 no others. This also double-checks the given password against 1069 the user's current password in the existing passwd file, and 1070 requests that a new password be given if the entered password 1071 is the same as the current password. 1072 1073 --delete-user 1074 1075 Remove the entry for the given user name from the file. 1076 1077 -l Lock the password of the named account. This option disables a 1078 --lock password by changing it to a value which matches no possible 1079 encrypted value (it adds a '!' at the beginning of the 1080 password). 1081 1082 --not-previous-password 1083 1084 Double-checks the given password against the previous password 1085 for the user, and requests that a new password be given if 1086 the entered password is the same as the previous password. 1087 1088 --not-system-password 1089 1090 Double-checks the given password against the system password 1091 for the user, and requests that a new password be given if 1092 the entered password is the same as the system password. This 1093 helps to enforce different passwords for different types of 1094 access. 1095 1096 --sha256 Use the SHA-256 algorithm for encrypting passwords. 1097 1098 --sha512 Use the SHA-512 algorithm for encrypting passwords. 1099 1100 --stdin 1101 Read the password directly from standard in rather than 1102 prompting for it. This is useful for writing scripts that 1103 automate use of $program. 1104 1105 -u Unlock the password of the named account. This option 1106 --unlock re-enables a password by changing the password back to its 1107 previous value (to the value before using the -l option). 1108 1109 --use-cracklib 1110 1111 Causes $program to use Alec Muffet's cracklib routines in 1112 order to determine and prevent the use of bad or weak 1113 passwords. The optional path to this option specifies 1114 the path to the dictionary files to use -- default path 1115 is "$default_cracklib_dict". This requires the Perl 1116 Crypt::Cracklib module to be installed on your system. 1117 1118 --version 1119 Displays the version of $program. 1120 1121 If used with --group, $program creates a file in the group(5) format, 1122 suitable for use with proftpd's AuthGroupFile configuration directive. 1123 1124 By default, using --group will write output to "$default_group_file". 1125 1126 Options: 1127 1128 --add-member 1129 1130 Add the named member to the given group name from the file. 1131 Example: 1132 1133 \$ ftpasswd --group --file=... --name=ftpd --add-member=bob 1134 1135 --delete-group 1136 1137 Remove the entry for the given group name from the file. 1138 1139 --enable-group-passwd 1140 1141 Prompt for a group password. This is disabled by default, 1142 as group passwords are not usually a good idea at all. 1143 1144 --file Write output to specified file, rather than "$default_group_file". 1145 1146 -F If the file be used already exists, delete it and write a new 1147 --force one. By default, new entries will be appended to the file. 1148 1149 --gid Numerical group ID (required). 1150 1151 -h 1152 --help Displays this message. 1153 1154 -m 1155 --member User name to be a member of the group. This argument may be 1156 used multiple times to specify the full list of users to be 1157 members of this group. 1158 1159 --des Use the DES algorithm for encrypting passwords. The default 1160 is the MD5 algorithm. 1161 1162 --md5 Use the MD5 algorithm for encrypting passwords. This is the 1163 default. 1164 1165 --name Name of the group (required). If the name does not exist in 1166 the specified output-file, an entry will be created for them. 1167 Otherwise, the given fields will be updated. 1168 1169 --sha256 Use the SHA-256 algorithm for encrypting passwords. 1170 1171 --sha512 Use the SHA-512 algorithm for encrypting passwords. 1172 1173 --stdin 1174 Read the password directly from standard in rather than 1175 prompting for it. This is useful for writing scripts that 1176 automate use of $program. 1177 1178 --use-cracklib 1179 1180 Causes $program to use Alec Muffet's cracklib routines in 1181 order to determine and prevent the use of bad or weak 1182 passwords. The optional path to this option specifies 1183 the path to the dictionary files to use -- default path 1184 is "$default_cracklib_dict". This requires the Perl 1185 Crypt::Cracklib module to be installed on your system. 1186 1187 --version 1188 Displays the version of $program. 1189 1190 If used with --hash, $program generates a hash of a password, as would 1191 appear in an AuthUserFile. The hash is written to standard out. 1192 This hash is suitable for use with proftpd's UserPassword directive. 1193 1194 Options: 1195 1196 --des Use the DES algorithm for encrypting passwords. The default 1197 is the MD5 algorithm. 1198 1199 --md5 Use the MD5 algorithm for encrypting passwords. This is the 1200 default. 1201 1202 --sha256 Use the SHA-256 algorithm for encrypting passwords. 1203 1204 --sha512 Use the SHA-512 algorithm for encrypting passwords. 1205 1206 --stdin 1207 Read the password directly from standard in rather than 1208 prompting for it. This is useful for writing scripts that 1209 automate use of $program. 1210 1211 --use-cracklib 1212 1213 Causes $program to use Alec Muffet's cracklib routines in 1214 order to determine and prevent the use of bad or weak 1215 passwords. The optional path to this option specifies 1216 the path to the dictionary files to use -- default path 1217 is "$default_cracklib_dict". This requires the Perl 1218 Crypt::Cracklib module to be installed on your system. 1219 1220 --version 1221 Displays the version of $program. 1222 1223END_OF_USAGE 1224 1225 exit 0; 1226} 1227 1228# --------------------------------------------------------------------------- 1229sub version { 1230 print STDOUT "$version\n"; 1231 1232 exit 0; 1233} 1234 1235# --------------------------------------------------------------------------- 1236 1237