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