Blob Blame History Raw
--- contrib/ftpasswd
+++ contrib/ftpasswd
@@ -1,6 +1,6 @@
-#!/usr/bin/perl
+#!/usr/bin/env perl
 # ---------------------------------------------------------------------------
-# Copyright (C) 2000-2010 TJ Saunders <tj@castaglia.org>
+# Copyright (C) 2000-2013 TJ Saunders <tj@castaglia.org>
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -14,19 +14,19 @@
 #
 # You should have received a copy of the GNU General Public License
 # along with this program; if not, write to the Free Software
-# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307, USA.
+# Foundation, Inc., 51 Franklin Street, Suite 500, Boston, MA 02110-1335, USA.
 #
 # Based on MacGuyver's genuser.pl script, this script generates password
 # files suitable for use with proftpd's AuthUserFile directive, in passwd(5)
 # format, or AuthGroupFile, in group(5) format.  The idea is somewhat similar
 # to Apache's htpasswd program.
 #
-#  $Id: ftpasswd,v 1.10.4.2 2010/05/27 17:50:50 castaglia Exp $
-#
+#  $Id: ftpasswd,v 1.22 2013-12-10 17:26:30 castaglia Exp $
 # ---------------------------------------------------------------------------
 
 use strict;
 
+use Fcntl qw(:flock);
 use File::Basename qw(basename);
 use Getopt::Long;
 
@@ -40,7 +40,7 @@
 my $default_cracklib_dict = "/usr/lib/cracklib_dict";
 my $cracklib_dict;
 my $output_file;
-my $version = "1.1.3";
+my $version = "1.3.0";
 
 my @data;
 
@@ -59,15 +59,19 @@
   'hash',
   'h|help',
   'home=s',
+  'l|lock',
   'md5',
   'm|member=s@',
   'name=s',
   'not-previous-password',
   'not-system-password',
   'passwd',
+  'sha256',
+  'sha512',
   'shell=s',
   'stdin',
   'uid=n',
+  'u|unlock',
   'use-cracklib:s',
   'version',
 );
@@ -118,20 +122,58 @@
 
   # check for and handle the --delete-user option.
   if (defined($opts{'delete-user'})) {
+    open_output_file();
 
-    # make sure we can write/update the file first
-    unless (chmod 0644, $output_file) {
-      die "$program: unable to set permissions on $output_file to 0644: $!\n";
+    my ($pass, $uid, $gid, $gecos, $home, $shell) = find_passwd_entry(name =>
+      $opts{'name'});
+
+    handle_passwd_entry(name => $opts{'name'}, uid => $uid, gid => $gid,
+      gecos => $gecos, home => $home, shell => $shell,
+      delete_user => $opts{'delete-user'});
+
+    close_output_file();
+
+    # done
+    exit 0;
+  }
+
+  # check for and handle the --lock option.
+  if (defined($opts{'l'})) {
+    open_output_file();
+
+    my ($pass, $uid, $gid, $gecos, $home, $shell) = find_passwd_entry(name =>
+      $opts{'name'});
+
+    my $new_passwd = $pass;
+
+    # If this password is already "locked", leave it alone
+    if ($new_passwd !~ /^!/) {
+      $new_passwd = '!' . $new_passwd;
     }
 
+    handle_passwd_entry(name => $opts{'name'}, uid => $uid, gid => $gid,
+      gecos => $gecos, home => $home, shell => $shell,
+      new_passwd => $new_passwd);
+
+    close_output_file();
+
+    # done
+    exit 0;
+  }
+
+  # check for and handle the --unlock option.
+  if (defined($opts{'u'})) {
     open_output_file();
 
     my ($pass, $uid, $gid, $gecos, $home, $shell) = find_passwd_entry(name =>
       $opts{'name'});
 
+    my $new_passwd = $pass;
+    $new_passwd =~ s/^!+//;
+
     handle_passwd_entry(name => $opts{'name'}, uid => $uid, gid => $gid,
       gecos => $gecos, home => $home, shell => $shell,
-      delete_user => $opts{'delete-user'});
+      new_passwd => $new_passwd);
 
     close_output_file();
 
@@ -206,12 +248,6 @@
 
   # check for and handle the --delete-group option.
   if (defined($opts{'delete-group'})) {
-
-    # make sure we can write/update the file first
-    unless (chmod 0644, $output_file) {
-      die "$program: unable to set permissions on $output_file to 0644: $!\n";
-    }
-
     open_output_file();
 
     handle_group_entry(name => $opts{'name'},
@@ -290,18 +326,33 @@
 sub close_output_file {
   my %args = @_;
 
-  open(OUTPUT, "> $output_file") or
-    die "$program: unable to open $output_file: $!\n";
+  if (open(my $fh, "> $output_file")) {
+    if (flock($fh, LOCK_EX|LOCK_NB)) {
+      # flush the data to the file
+      foreach my $line (@data) {
+        print $fh "$line\n";
+      }
 
-  # flush the data to the file
-  foreach my $line (@data) {
-    print OUTPUT "$line\n";
-  }
+      # set the permissions appropriately, ie 0440, before closing the file
+      unless (chmod(0440, $output_file)) {
+        flock($fh, LOCK_UN);
+        die("$program: unable to set permissions on $output_file: $!");
+      }
+
+      flock($fh, LOCK_UN);
+
+      unless (close($fh)) {
+        die("$program: unable to close $output_file: $!\n");
+      }
 
-  # set the permissions appropriately, ie 0444, before closing the file
-  chmod 0444, $output_file;
+    } else {
+      close($fh);
+      die("$program: unable to write $output_file: Locked (in use) by another process\n");
+    }
 
-  close(OUTPUT) or die "$program: unable to close $output_file: $!\n";
+  } else {
+    die("$program: unable to open $output_file: $!\n");
+  }
 }
 
 # ----------------------------------------------------------------------------
@@ -347,17 +398,35 @@
   # how to do its thing.  By default, generate a salt that triggers MD5.
 
   if (defined($opts{'des'})) {
-
     # DES salt
     $salt = join '', ('.', '/', 0..9, 'A'..'Z', 'a'..'z')[rand 64, rand 64];
 
-  } else {
+  } elsif (defined($opts{'sha256'})) {
+    # SHA-256 salt (16 characters)
+    $salt = join '', (0..9, 'A'..'Z', 'a'..'z')
+      [rand 62, rand 62, rand 62, rand 62,
+       rand 62, rand 62, rand 62, rand 62,
+       rand 62, rand 62, rand 62, rand 62,
+       rand 62, rand 62, rand 62, rand 62];
+    $salt = '$5$' . $salt;
 
-    # MD5 salt
+  } elsif (defined($opts{'sha512'})) {
+    # SHA-512 salt (16 characters)
     $salt = join '', (0..9, 'A'..'Z', 'a'..'z')
-      [rand 62, rand 62, rand 62, rand 62, rand 62, rand 62, rand 62, rand 62];
-    $salt = '$1$' . $salt;
+      [rand 62, rand 62, rand 62, rand 62,
+       rand 62, rand 62, rand 62, rand 62,
+       rand 62, rand 62, rand 62, rand 62,
+       rand 62, rand 62, rand 62, rand 62];
+    $salt = '$6$' . $salt;
 
+  } else {
+    # MD5 salt (16 characters)
+    $salt = join '', (0..9, 'A'..'Z', 'a'..'z')
+      [rand 62, rand 62, rand 62, rand 62,
+       rand 62, rand 62, rand 62, rand 62,
+       rand 62, rand 62, rand 62, rand 62,
+       rand 62, rand 62, rand 62, rand 62];
+    $salt = '$1$' . $salt;
   }
 
   return $salt;
@@ -373,7 +442,7 @@
   # limit of relevant password characters.
 
   if (defined($opts{'des'}) && !defined($opts{'stdin'})) {
-    print STDOUT "\nPlease be aware that only the first 8 characters of a DES password are\nrelevant.  Use the --md5 option to select MD5 passwords, as they do not have\nthis limitation.\n";
+    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";
   }
 
   if (defined($opts{'stdin'})) {
@@ -491,23 +560,55 @@
   my $hash = crypt($passwd, $salt);
 
   # Check that the crypt() implementation properly supports use of the MD5
-  # algorithm, if specified
+  # (or other non-DES algorithm), if specified.
+  if (!defined($opts{'des'})) {
+    if (defined($opts{'md5'})) {
+      # if the first three characters of the hash are not "$1$", the crypt()
+      # implementation doesn't support MD5.  Some crypt()s will happily use
+      # "$1" as a salt even though this is not a valid DES salt.  Humf.
+      #
+      # Perl doesn't treat strings as arrays of characters, so extracting the
+      # first three characters is a little more convoluted (I'm accustomed to
+      # C's strncmp(3) for this now).
 
-  if (defined($opts{'md5'}) || !defined($opts{'des'})) {
+      my @string = split('', $hash);
+      my $prefix = $string[0] . $string[1] . $string[2];
 
-    # if the first three characters of the hash are not "$1$", the crypt()
-    # implementation doesn't support MD5.  Some crypt()s will happily use
-    # "$1" as a salt even though this is not a valid DES salt.  Humf.
-    #
-    # Perl doesn't treat strings as arrays of characters, so extracting the
-    # first three characters is a little more convoluted (I'm accustomed to
-    # C's strncmp(3) for this now).
+      if ($prefix ne '$1$') { 
+        print STDOUT "You requested MD5 passwords but your system does not support it.  Defaulting to DES passwords.\n\n";
+      }
+
+    } elsif (defined($opts{'sha256'})) {
+      # if the first three characters of the hash are not "$5$", the crypt()
+      # implementation doesn't support SHA-256.  Some crypt()s will happily use
+      # "$5" as a salt even though this is not a valid DES salt.  Humf.
+      #
+      # Perl doesn't treat strings as arrays of characters, so extracting the
+      # first three characters is a little more convoluted (I'm accustomed to
+      # C's strncmp(3) for this now).
 
-    my @string = split('', $hash);
-    my $prefix = $string[0] . $string[1] . $string[2];
+      my @string = split('', $hash);
+      my $prefix = $string[0] . $string[1] . $string[2];
 
-    if ($prefix ne '$1$') { 
-      print STDOUT "You requested MD5 passwords but your system does not support it.  Defaulting to DES passwords.\n\n";
+      if ($prefix ne '$5$') {
+        print STDOUT "You requested SHA-256 passwords but your system does not support it.  Defaulting to DES passwords.\n\n";
+      }
+
+    } elsif (defined($opts{'sha512'})) {
+      # if the first three characters of the hash are not "$6$", the crypt()
+      # implementation doesn't support SHA-512.  Some crypt()s will happily use
+      # "$6" as a salt even though this is not a valid DES salt.  Humf.
+      #
+      # Perl doesn't treat strings as arrays of characters, so extracting the
+      # first three characters is a little more convoluted (I'm accustomed to
+      # C's strncmp(3) for this now).
+
+      my @string = split('', $hash);
+      my $prefix = $string[0] . $string[1] . $string[2];
+
+      if ($prefix ne '$6$') {
+        print STDOUT "You requested SHA-512 passwords but your system does not support it.  Defaulting to DES passwords.\n\n";
+      }
     }
   }
 
@@ -643,6 +744,7 @@
   my $home = $args{'home'};
   my $shell = $args{'shell'};
   my $delete_user = $args{'delete_user'};
+  my $new_passwd = $args{'new_passwd'};
 
   # Trim any trailing slashes in $home.
   $home =~ s/(.*)\/$/$1/ if ($home =~ /\/$/);
@@ -677,12 +779,18 @@
   }
 
   my $passwd;
-  unless ($delete_user) {
-    # check the requested shell against the list in /etc/shells
-    check_shell(shell => $shell);
 
-    # prompt the user for the password
-    $passwd = get_passwd(name => $name);
+  if (!$delete_user) {
+    if (!$new_passwd) {
+      # check the requested shell against the list in /etc/shells
+      check_shell(shell => $shell);
+
+      # prompt the user for the password
+      $passwd = get_passwd(name => $name);
+
+    } else {
+      $passwd = $new_passwd;
+    }
   }
 
   # remove the entry to be updated
@@ -723,10 +831,26 @@
   # If the file already exists, slurp up its contents for later updating.
 
   if (-f $output_file) {
-    open(INPUT, "< $output_file") or
-      die "$program: unable to open $output_file: $!\n";
-    chomp(@data = <INPUT>);
-    close(INPUT);
+    # make sure we can write/update the file first
+    unless (chmod 0644, $output_file) {
+      die "$program: unable to set permissions on $output_file to 0644: $!\n";
+    }
+
+    if (open(my $fh, "< $output_file")) {
+      if (flock($fh, LOCK_SH|LOCK_NB)) {
+        chomp(@data = <$fh>);
+        flock($fh, LOCK_UN);
+        close($fh);
+
+      } else {
+        close($fh);
+        die("$program: unable to read $output_file: Locked (in use) by another process\n");
+      }
+
+
+    } else {
+      die("$program: unable to open $output_file: $!\n");
+    }
   }
 
   # if the --force option was given, just zero out any data that might have
@@ -811,6 +935,11 @@
  
                 Remove the entry for the given user name from the file.
 
+    -l          Lock the password of the named account.  This option disables a
+    --lock      password by changing it to a value which matches no possible
+                encrypted value (it adds a '!' at the beginning of the
+                password).
+
     --not-previous-password
 
                 Double-checks the given password against the previous password
@@ -825,11 +954,19 @@
                 helps to enforce different passwords for different types of
                 access.
 
+    --sha256    Use the SHA-256 algorithm for encrypting passwords.
+
+    --sha512    Use the SHA-512 algorithm for encrypting passwords.
+
     --stdin
                 Read the password directly from standard in rather than
                 prompting for it.  This is useful for writing scripts that
                 automate use of $program.
 
+    -u          Unlock the password of the named account.  This option
+    --unlock    re-enables a password by changing the password back to its
+                previous value (to the value before using the -l option).
+
     --use-cracklib
 
                 Causes $program to use Alec Muffet's cracklib routines in
@@ -883,6 +1020,10 @@
                 the specified output-file, an entry will be created for them.
                 Otherwise, the given fields will be updated.
 
+    --sha256    Use the SHA-256 algorithm for encrypting passwords.
+
+    --sha512    Use the SHA-512 algorithm for encrypting passwords.
+
     --stdin
                 Read the password directly from standard in rather than
                 prompting for it.  This is useful for writing scripts that
@@ -912,6 +1053,10 @@
     --md5       Use the MD5 algorithm for encrypting passwords.  This is the
                 default.
 
+    --sha256    Use the SHA-256 algorithm for encrypting passwords.
+
+    --sha512    Use the SHA-512 algorithm for encrypting passwords.
+
     --stdin
                 Read the password directly from standard in rather than
                 prompting for it.  This is useful for writing scripts that