+#! /usr/bin/perl
+#
+# Sanitise Linux password and group databases
+#
+# (c) 1998 Mark Wooding
+#
+
+use MdwOpt;
+use FileHandle;
+use POSIX;
+
+#----- Documentation --------------------------------------------------------
+
+=head1 NAME
+
+shadowfix - fix password and group files
+
+=head1 SYNOPSIS
+
+B<shadowfix> I<options>...
+
+=head1 DESCRIPTION
+
+Shadowfix trundles through your various password files and makes sure
+that they're consistent with themselves.
+
+Currently, the checks Shadowfix makes, and their resolutions, are:
+
+=over 4
+
+=item *
+
+Every user named in the password file should have a shadow password entry;
+create a shadow password entry if necessary.
+
+=item *
+
+Every password field in the password file indicates only presence or absence
+of a password; move a realistic-looking password to the shadow password file,
+and ensure that the password entry in the password file is either empty
+(signifying no password) or an `x' character (signifying a password).
+
+=item *
+
+The primary group of each user exists; warn about nonexistent primary groups.
+
+=item *
+
+Every user is a member of his primary group; add the user to the membership
+list of the group where necessary.
+
+=item *
+
+There are no entries in the shadow password file which don't match entries in
+the main password file; delete orphaned shadow password entries.
+
+=item *
+
+Check group and shadow group files for consistency, as for password and
+shadow password files: every group entry has a shadow entry, no passwords in
+the group file, no orphaned shadow group entries.
+
+=item *
+
+The lists of group members are consistent between group and shadow group
+files; edit the shadow group list to match the main group list where
+necessary.
+
+=item *
+
+Group administrators, listed in the shadow group file, are real users; warn
+about nonexistent group administrators.
+
+=back
+
+A lot of the checks above only make sense when shadow password and group
+files are used. When instructed not to create shadow files, Shadowfix will
+perform the password/group consistency checks as described above. Also, if
+given shadow files as input, and told not to create shadow files on output,
+Shadowfix will merge the password information back into the main files.
+Obviously, translating shadowed to non-shadowed files involved information
+loss: in particular, information about password expiry and group
+administration is lost.
+
+It's time to examine the command line options.
+
+=over 4
+
+=item B<--passwd=>I<file>
+
+Use I<file> as the main password file.
+
+=item B<--group=>I<file>
+
+Use I<file> as the main group file.
+
+=item B<--shadow=>I<file>
+
+Use I<file> as the shadow password file.
+
+=item B<--gshadow=>I<file>
+
+Use I<file> as the shadow group file.
+
+=item B<--in-passwd=>I<file>
+
+Read main password file entries from I<file>.
+
+=item B<--in-group=>I<file>
+
+Read main group file entries from I<file>.
+
+=item B<--in-shadow=>I<file>
+
+Read shadow password file entries from I<file>.
+
+=item B<--in-gshadow=>I<file>
+
+Read shadow group file entries from I<file>.
+
+=item B<--quiet>
+
+Suppresses Shadowfix's informative messages about what it's doing to your
+passwored files. Enabling this option is not recommended.
+
+=back
+
+If the input files aren't specified explitly, Shadowfix defaults to trying to
+read the output files. These default sensibly to the system password and
+shadow files in F</etc>.
+
+Shadowfix knows about locking password files, so C<passwd> and C<vipw> will
+interact with it properly.
+
+=head1 FILES
+
+=over 4
+
+=item F</etc/passwd>, F</etc/shadow>, F</etc/group>, F</etc/gshadow>
+
+System default password and group files.
+
+=back
+
+=head1 BUGS
+
+Shadowfix doesn't understand how to cope with YP password files. Yellow
+Pages is a security hole; don't use it.
+
+=head1 AUTHOR
+
+Mark Wooding, <mdw@nsict.org>
+
+=cut
+
+#----- Configuration section ------------------------------------------------
+
+$passwd = "/etc/passwd";
+$group = "/etc/group";
+$shadow = "/etc/shadow";
+$gshadow = "/etc/gshadow";
+$passwd_in = $shadow_in = undef;
+$group_in = $gshadow_in = undef;
+
+$suyb = 0;
+
+#----- Subroutines ----------------------------------------------------------
+
+sub hashify {
+ map { $_, 1 } @_;
+}
+
+sub moan {
+ print STDERR "shadowfix: @_\n";
+}
+
+sub uidsort {
+ if ($a eq $b) {
+ return 0;
+ } elsif ($a eq "+") {
+ return +1;
+ } elsif ($b eq "+") {
+ return -1;
+ } elsif (!exists($ubynam{$a})) {
+ return +1;
+ } elsif (!exists($ubynam{$b})) {
+ return -1;
+ } else {
+ return $ubynam{$a}{uid} <=> $ubynam{$b}{uid} || $a cmp $b;
+ }
+}
+
+sub gidsort {
+ if ($a eq $b) {
+ return 0;
+ } elsif ($a eq "+") {
+ return +1;
+ } elsif ($b eq "+") {
+ return -1;
+ } else {
+ return $gbynam{$a}{gid} <=> $gbynam{$b}{gid} || $a cmp $b;
+ }
+}
+
+sub lockfile {
+ my $file = shift;
+ my $mode = shift or 0644;
+ my $fh = new FileHandle;
+
+ $fh->open("${file}.lock", O_WRONLY | O_EXCL | O_CREAT, 0600) or
+ die "couldn't obtain lock file ${file}.lock";
+ $fh->print($$);
+ $fh->close;
+ $fh->open("${file}.edit", O_WRONLY | O_TRUNC | O_CREAT, $mode) or do {
+ unlink "${file}.lock";
+ die "open(${file}.edit): $!";
+ };
+ return $fh;
+}
+
+sub unlockfile {
+ my $file = shift;
+ my $fh = shift;
+ $fh->close;
+
+ # --- See whether the file changed ---
+
+ CHECK: {
+ my ($ofh, $nfh);
+ my ($obuf, $nbuf);
+ my ($osz, $nsz);
+
+ # --- Open the old and new versions for reading ---
+
+ $ofh = new FileHandle $file, O_RDONLY;
+ $nfh = new FileHandle "${file}.edit", O_RDONLY;
+ last CHECK if !$ofh || !$nfh;
+
+ # --- Read blocks from each and compare ---
+
+ BLOCK: for (;;) {
+ $osz = sysread($ofh, $obuf, 4096);
+ $nsz = sysread($nfh, $nbuf, 4096);
+ last CHECK if !defined($osz) || !defined($nsz);
+ last CHECK if $obuf ne $nbuf;
+ last BLOCK if $sz == 0;
+ }
+
+ # --- The files are identical ---
+
+ # moan "file $file is unchanged";
+ unlink("${file}.edit");
+ unlink("${file}.lock");
+ return;
+ }
+
+ # --- Find the current owner ---
+
+ # system("diff -u $file $file.edit");
+
+ if (-e $file) {
+ my ($mode, $uid, $gid);
+ (undef, undef, $mode, undef, $uid, $gid) = stat $file;
+ chmod $mode, "${file}.edit";
+ chown $uid, $gid, "${file}.edit";
+ }
+
+ # --- Move the old file out of the way ---
+
+ !-e $file or rename("${file}", "${file}-") or do {
+ unlink "${file}.lock";
+ unlink "${file}.edit";
+ die "couldn't save backup copy of $file: $!";
+ };
+
+ # --- Move the new one into place ---
+
+ rename ("${file}.edit", "${file}") or do {
+ rename("${file}-", "${file}"); # This shouldn't happen!
+ unlink "${file}.lock";
+ unlink "${file}.edit";
+ die "HELP!!! couldn't save backup copy of $file: $!";
+ };
+
+ # --- Release the lock ---
+
+ moan "updated $file"
+ unless $suyb;
+ unlink("${file}.lock");
+}
+
+#----- Main code ------------------------------------------------------------
+
+# --- Options parsing ---
+
+$longopts = { 'passwd' => { return => 'p', arg => 'opt' },
+ 'in-passwd' => { return => 'ip', arg => 'opt' },
+ 'shadow' => { return => 'ps', arg => 'opt' },
+ 'in-shadow' => { return => 'ips', arg => 'opt' },
+ 'group' => { return => 'g', arg => 'opt' },
+ 'in-group' => { return => 'ig', arg => 'opt' },
+ 'gshadow' => { return => 'gs', arg => 'opt' },
+ 'in-gshadow' => { return => 'igs', arg => 'opt' },
+ 'quiet' => { return => 'q', negate => 1 } };
+
+$opts = MdwOpt->new("", $longopts, \@ARGV, ['negate', 'noshort']);
+
+OPT: while (($opt, $arg) = $opts->read, $opt) {
+ $passwd = $arg, next OPT if $opt eq 'p';
+ $passwd_in = $arg, next OPT if $opt eq 'ip';
+ $shadow = $arg, next OPT if $opt eq 'ps';
+ $shadow_in = $arg, next OPT if $opt eq 'ips';
+ $group = $arg, next OPT if $opt eq 'g';
+ $group_in = $arg, next OPT if $opt eq 'ig';
+ $gshadow = $arg, next OPT if $opt eq 'gs';
+ $gshadow_in = $arg, next OPT if $opt eq 'igs';
+ $suyb = 1, next OPT if $opt eq 'q';
+ $suyb = 0, next OPT if $opt eq 'q+';
+ die "bad option";
+}
+
+$passwd_in = $passwd unless $passwd_in;
+$shadow_in = $shadow unless $shadow_in;
+$group_in = $group unless $group_in;
+$gshadow_in = $gshadow unless $gshadow_in;
+
+# --- Initialise the user tables ---
+
+%ubynam = %ubyuid = %subynam = ();
+%gbynam = %gbygid = %sgbynam = ();
+
+# --- Slurp the user tables into memory ---
+
+$pw = new FileHandle $passwd_in, O_RDONLY or die "open($passwd_in): $!";
+while ($line = $pw->getline) {
+ chomp $line;
+ @f = split /:/, $line;
+ $#f = 6;
+ $a = { data => [ @f ], name => $f[0], uid => $f[2], gid => $f[3] };
+ $ubynam{$a->{name}} = $ubyuid{$a->{uid}} = $a;
+}
+$pw->close;
+
+$gr = new FileHandle $group_in, O_RDONLY or die "open($group_in): $!";
+while ($line = $gr->getline) {
+ chomp $line;
+ @f = split /:/, $line;
+ $#f = 3;
+ $a = { data => [ @f ],
+ members => { hashify(split /,/, $f[3]) },
+ name => $f[0], gid => $f[2] };
+ $gbynam{$a->{name}} = $gbygid{$a->{gid}} = $a;
+}
+$gr->close;
+
+undef $have_shadow;
+if ($shadow_in) {
+ if ($spw = new FileHandle $shadow_in, O_RDONLY) {
+ while ($line = $spw->getline) {
+ chomp $line;
+ @f = split /:/, $line;
+ $#f = 8;
+ $a = { data => [ @f ], name => $f[0] };
+ $subynam{$a->{name}} = $a;
+ }
+ $spw->close;
+ $have_shadow = 1;
+ } else {
+ die "open($shadow_in): $!" unless $! == ENOENT;
+ }
+}
+
+undef $have_gshadow;
+if ($gshadow_in) {
+ if ($sgr = new FileHandle $gshadow_in, O_RDONLY) {
+ while ($line = $sgr->getline) {
+ chomp $line;
+ @f = split /:/, $line;
+ $#f = 3;
+ $a = { data => [ @f ],
+ members => { hashify (split /,/, $f[3]) },
+ name => $f[0] };
+ $sgbynam{$a->{name}} = $a;
+ }
+ $sgr->close;
+ $have_gshadow = 1;
+ } else {
+ die "open($gshadow_in): $!" unless $! == ENOENT;
+ }
+}
+
+# --- Check primary group memberships ---
+
+for $u (values %ubynam) {
+ $unam = $u->{name};
+ if (exists $gbygid{$u->{gid}}) {
+ $g = $gbygid{$u->{gid}};
+ unless ($unam eq "+" || exists($g->{members}{$unam})) {
+ moan "user $unam is not a member of his/her primary group"
+ unless $suyb;
+ $g->{members}{$unam} = 1;
+ }
+ } else {
+ moan "user $unam seems to belong to a nonexistant group"
+ unless $suyb;
+ }
+}
+
+# --- Shadow password checks ---
+
+if ($shadow) {
+
+ # --- Full shadowing checks ---
+
+ for $u (values %ubynam) {
+ $unam = $u->{name};
+
+ # --- Ensure there's a shadow password entry ---
+
+ unless ($unam eq "+" || exists($subynam{$unam})) {
+ moan "user $unam not in shadow password file: adding"
+ unless $suyb;
+ $subynam{$unam} = { name => $unam,
+ data => [$unam,
+ $u->{data}[1],
+ 10205, 0, 99999, 7, "", "", ""] };
+ }
+
+ # --- Mark unloginable shadow password entries ---
+
+ $su = $subynam{$unam};
+ $p = $su->{data}[1];
+ if ($p ne "*" && length($p) > 0 && length($p) < 5) {
+ moan "blanked user ${unam}'s password"
+ unless $suyb;
+ $su->{data}[1] = "*";
+ }
+
+ # --- Blank out normal password entries ---
+
+ if ($unam eq "+") {
+ # Nothing doing
+ } elsif ($p eq "") {
+ $u->{data}[1] = "";
+ } else {
+ $u->{data}[1] = "x";
+ }
+ }
+
+ # --- Remove shadow entries which don't make sense any more ---
+
+ for $su (values %subynam) {
+ $unam = $su->{name};
+ unless (exists($ubynam{$unam})) {
+ moan "user $unam only in shadow password file: deleting"
+ unless $suyb;
+ delete $subynam{$su->{name}};
+ }
+ }
+
+} elsif ($have_shadow) {
+
+ # --- We have shadowing, but aren't writing out entries ---
+
+ for $u (values %ubynam) {
+ $unam = $u->{name};
+ $u->{data}[1] = $subynam{$unam}{data}[1]
+ if exists($subynam{$unam});
+ }
+}
+
+# --- Shadow group checks ---
+
+for $g (values %gbynam) {
+ $gnam = $g->{name};
+
+ # --- Ensure there's a shadow group entry ---
+
+ unless (!$gshadow || $gnam eq "+" || exists($sgbynam{$gnam})) {
+ moan "group $gnam not in shadow group file: adding"
+ unless $suyb;
+ $sgbynam{$gnam} = { name => $gnam,
+ data => [$gnam,
+ $g->{data}[1],
+ "",
+ $g->{data}[3]],
+ members => { %{$g->{members}} } };
+ }
+
+ # --- Play games with passwords ---
+
+ if ($gshadow) {
+
+ # --- Mark unloginable shadow group entries ---
+
+ $sg = $sgbynam{$gnam};
+ $p = $sg->{data}[1];
+ if ($p ne "*" && length($p) > 0 && length($p) < 5) {
+ moan "blanked group ${gnam}'s password"
+ unless $suyb;
+ $sg->{data}[1] = "*";
+ }
+
+ # --- Blank out normal passwords ---
+
+ $g->{data}[1] = "x" unless $gnam eq "+";
+
+ # --- Check that the group's administrators exist ---
+
+ if ($sg->{data}[2] ne "" && !$suyb) {
+ my @admins =
+ my $admin;
+ foreach $admin (split(/,/, $sg->{data}[2])) {
+ exists $ubynam{$admin} or
+ moan "user $admin owns group $gnam but doesn't seem to exist";
+ }
+ }
+
+ } elsif ($have_gshadow) {
+ $g->{data}[1] = $sgbynam{$gnam}{data}[1]
+ if exists($sgbynam{$gnam});
+ $sg = undef;
+ }
+
+ # --- The group members should be consistent across both files ---
+
+ for $i (keys %{$g->{members}}) {
+ exists $ubynam{$i} or $suyb or
+ moan "user $i is a member of group $gnam but doesn't seem to exist";
+ unless (!$sg || exists($sg->{members}{$i})) {
+ moan "group $gnam does not include $i in shadow group file: adding"
+ unless $suyb;
+ $sg->{members}{$i} = 1;
+ }
+ }
+ if ($sg) {
+ for $i (keys %{$sg->{members}}) {
+ unless (exists($g->{members}{$i})) {
+ moan "group $gnam does not include $i in main group file: deleting"
+ unless $suyb;
+ delete $sg->{members}{$i};
+ }
+ }
+ }
+}
+
+# --- Remove entries which are only in the shadow file ---
+
+if ($gshadow) {
+ for $sg (values %sgbynam) {
+ $gnam = $sg->{name};
+ unless (exists($gbynam{$gnam})) {
+ moan "group $gnam only in shadow group file: deleting"
+ unless $suyb;
+ delete $sgbynam{$gnam};
+ }
+ }
+}
+
+# --- Fix up the data blocks ---
+
+for $g (values %gbynam) {
+ $g->{data}[3] = join(",", sort uidsort keys %{$g->{members}});
+}
+
+if ($gshadow) {
+ for $sg (values %sgbynam) {
+ $sg->{data}[3] = join(",", sort uidsort keys %{$sg->{members}});
+ }
+}
+
+# --- Output the finished work of art ---
+
+$pw = lockfile($passwd, 0644);
+for $unam (sort uidsort keys %ubynam) {
+ $pw->print(join(":", @{$ubynam{$unam}{data}}), "\n");
+}
+unlockfile($passwd, $pw);
+
+if ($shadow) {
+ $spw = lockfile($shadow, 0640);
+ for $unam (sort uidsort keys %subynam) {
+ $spw->print(join(":", @{$subynam{$unam}{data}}), "\n");
+ }
+ unlockfile($shadow, $spw);
+}
+
+$gr = lockfile($group, 0644);
+for $gnam (sort gidsort keys %gbynam) {
+ $gr->print(join(":", @{$gbynam{$gnam}{data}}), "\n");
+}
+unlockfile($group, $gr);
+
+if ($gshadow) {
+ $sgr = lockfile($gshadow, 0640);
+ for $gnam (sort gidsort keys %sgbynam) {
+ $sgr->print(join(":", @{$sgbynam{$gnam}{data}}), "\n");
+ }
+ unlockfile($gshadow, $sgr);
+}
+
+#----- More subroutines -----------------------------------------------------
+
+sub udump {
+ my $u = shift;
+ printf "name = %s\n", $u->{name};
+ printf "uid = %d, gid = %d\n", $u->{uid}, $u->{gid};
+ printf "data = %s\n", join(":", @{$u->{data}});
+ print "\n";
+}
+
+sub sudump {
+ my $u = shift;
+ printf "name = %s\n", $u->{name};
+ printf "data = %s\n", join(":", @{$u->{data}});
+ print "\n";
+}
+
+sub gdump {
+ my $g = shift;
+ printf "name = %s\n", $g->{name};
+ printf "gid = %d\n", $g->{gid};
+ printf "members = %s\n", join(",", sort uidsort keys %{$g->{members}});
+ printf "data = %s\n", join(":", @{$g->{data}});
+ print "\n";
+}
+
+sub sgdump {
+ my $g = shift;
+ printf "name = %s\n", $g->{name};
+ printf "members = %s\n", join(",", sort uidsort keys %{$g->{members}});
+ printf "data = %s\n", join(":", @{$g->{data}});
+ print "\n";
+}
+
+#----- That's all, folks ----------------------------------------------------