/
mavis_tacplus_shadow.pl
executable file
·337 lines (269 loc) · 8.05 KB
/
mavis_tacplus_shadow.pl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
#!/usr/bin/env perl
# $Id$
#
# mavis_tacplus_shadow.pl
# (C)2011 Marc Huber <Marc.Huber@web.de>
# All rights reserved.
#
# Shadow password backend for libmavis_external.so
# Authenticates against local shadow password file, supports password changes.
#
=pod
Test input for authentication:
0 TACPLUS
4 $USER
8 $PASS
49 AUTH
=
printf "0 TACPLUS\n4 $USER\n8 $PASS\n49 AUTH\n=\n" | this_script.pl
Test input for password change:
0 TACPLUS
4 $USER
8 $PASS
49 CHPW
50 $NEWPASS
=
printf "0 TACPLUS\n4 $USER\n8 $PASS\n49 CHPW\n50 $NEWPASS\n=\n" | this_script.pl
#######
Environment variables:
SHADOWFILE
Absolute path to your shadow file.
Default: /dev/null
Shadow file syntax:
username:encpass:lastchange:minage:maxage:warn:...
Setting lastchange to 0 enforces password change at initial log-in.
Example:
marc:$1$oAY9rv/9$NuyhEqJNSROHmLlwCXv0T.:15218:0:99999:7:::
test:$1$oAY9rv/9$NuyhEqJNSROHmLlwCXv0T.:15213:0:99999:7:::
test2:$1$oAY9rv/9$NuyhEqJNSROHmLlwCXv0T.:0:0:99999:7:::
Add new users using vipw or any other editor that performs file locking.
FLAG_PWPOLICY
Enforce a simplicistic password policy.
Default: unset
CI
Absolute path to the "ci" program, used for storing revisions of the shadow file into RCS.
Default: ci
MKPASSWD
Absolute path to the "mkpasswd" program, most likely /usr/bin/mkpasswd
Default: unset
MKPASSWDMETHOD
method argument for mkpasswd, see "mkpasswd --m" for a list supported on your system.
Use this with care.
Default: unset
########
=cut
use lib '/usr/local/lib/mavis/';
use strict;
use POSIX qw(pipe dup2);
use Mavis;
use Fcntl ':flock';
my $hashid = ''; # DES
my $have_crypt_passwd_xs;
my $flag_pwpolicy = undef;
my $shadow = "/dev/null";
my $ci = "ci";
my $mkpasswd = undef;
my @mkpasswdmethod = ();
$| = 1;
$shadow = $ENV{'SHADOWFILE'} if exists $ENV{'SHADOWFILE'};
$flag_pwpolicy = $ENV{'FLAG_PWPOLICY'} if exists $ENV{'FLAG_PWPOLICY'};
$ci = $ENV{'CI'} if exists $ENV{'CI'};
$mkpasswd = $ENV{'MKPASSWD'} if exists $ENV{'MKPASSWD'};
@mkpasswdmethod = ("-m", $ENV{'MKPASSWDMETHOD'}) if exists $ENV{'MKPASSWDMETHOD'};
my $backup = "$shadow.bak";
undef $mkpasswd unless -x $mkpasswd;
if (crypt('test', '$1$q5/vUEsR$') eq '$1$q5/vUEsR$jVwHmEw8zAmgkjMShLBg/.') {
$hashid = '$1$'; # MD5
} elsif (eval "require Crypt::Passwd::XS") {
import Crypt::Passwd::XS;
$hashid = '$1$'; # MD5
$have_crypt_passwd_xs = 1;
} elsif (undef $mkpasswd) {
print STDERR "Your system doesn't support modern hashes. Please install the mkpasswd utility.\n";
}
sub run_mkpasswd($) {
my ($parent0, $child1) = POSIX::pipe();
my ($child0, $parent1) = POSIX::pipe();
my $childpid = fork();
if ($childpid eq 0) {
POSIX::close $parent0;
POSIX::close $parent1;
POSIX::dup2($child0, 0);
POSIX::dup2($child1, 1);
exec $mkpasswd, "--stdin", @mkpasswdmethod;
}
POSIX::close $child0;
POSIX::close $child1;
POSIX::write($parent1, $_[0] . "\n", length($_[0]) + 1) or printf STDERR "POSIX::write: $!";
my $cry;
my $crylen = 1000;
POSIX::read($parent0, $cry, $crylen) or printf STDERR "POSIX::read: $!";
chomp $cry;
POSIX::close $parent0;
POSIX::close $parent1;
waitpid($childpid, 0);
($? == 0) ? $cry : undef;
}
sub get_password_hash ($$) {
if ($have_crypt_passwd_xs) {
Crypt::Passwd::XS::crypt($_[0], $_[1]);
} else {
crypt($_[0], $_[1]);
}
}
sub new_password_hash ($) {
if (defined $mkpasswd) {
my $res = run_mkpasswd($_[0]);
if (defined $res) {
return $res;
} else {
print STDERR "mkpasswd returned an error and will now be disabled.\n";
undef $mkpasswd;
}
}
my $salt = "";
for (my $i = 0; $i < 16; $i++) {
$salt .= ('.', '/', 0..9, 'A'..'Z', 'a'..'z')[rand 64];
}
return get_password_hash($_[0], $salt);
}
sub fgrep ($$$) {
my ($v, $L, $negate) = @_;
my @Q = ();
foreach my $line(@{$L}) {
my ($u, $r);
($u, $r) = split(/:/, $line, 2);
if (($negate && ($u ne $v)) || (!$negate && ($u eq $v))) {
push(@Q, $line);
}
}
return @Q;
}
umask 0177;
my ($in);
$/ = "\n=\n";
while ($in = <>) {
my @V = ();
my $result = MAVIS_DEFERRED;
chomp $in;
foreach my $a (split (/\n/, $in)) {
next unless $a =~ /^(\d+) (.*)$/;
$V[$1] = $2;
}
if (!defined $V[AV_A_USER]){
$V[AV_A_USER_RESPONSE] = "User not set.";
goto fatal;
}
if (!defined $V[AV_A_TYPE] || ($V[AV_A_TYPE] ne AV_V_TYPE_TACPLUS) ||
!defined $V[AV_A_TACTYPE] || (($V[AV_A_TACTYPE] ne AV_V_TACTYPE_AUTH) &&
($V[AV_A_TACTYPE] ne AV_V_TACTYPE_CHPW))) {
$result = MAVIS_DOWN;
goto bye;
}
if (!defined $V[AV_A_PASSWORD]){
$V[AV_A_USER_RESPONSE] = "Password not set.";
goto fatal;
}
if ($V[AV_A_TACTYPE] eq AV_V_TACTYPE_CHPW && !defined $V[AV_A_PASSWORD_NEW]){
$V[AV_A_USER_RESPONSE] = "New password not set";
goto fatal;
}
my $SHADOW = undef;
unless (open ($SHADOW, "+< $shadow") && flock($SHADOW, LOCK_EX)) {
$V[AV_A_USER_RESPONSE] = "Password database unavailable";
goto fatal;
}
$/ = "\n";
my @L = <$SHADOW>;
close $SHADOW if $V[AV_A_TACTYPE] eq AV_V_TACTYPE_AUTH;
my $v = $V[AV_A_USER];
my @Q = fgrep ($v, \@L, 0);
goto down if $#Q == -1;
goto fail unless $#Q == 0;
my $line = $Q[0];
my ($user, $passwd, $lastchange, $minage, $maxage, $warn, $remainder);
$warn = 0;
($user, $passwd, $lastchange, $minage, $maxage, $warn, $remainder) = split(/:/, $line, 7) or
($user, $passwd, $lastchange, $minage, $maxage, $remainder) = split(/:/, $line, 6) or
goto down;
$warn = 0 if $warn !~ /^\d+$/;
if (get_password_hash($V[AV_A_PASSWORD], $passwd) ne $passwd) {
$V[AV_A_USER_RESPONSE] = "Permission denied.";
goto fail;
}
my $today = int scalar(time)/86400;
if ($lastchange == 0 || $lastchange + $maxage < $today) {
$V[AV_A_PASSWORD_MUSTCHANGE] = "y";
$V[AV_A_USER_RESPONSE] = "Please change your password.";
} elsif ($lastchange + $maxage - $warn < $today) {
my $d = $lastchange + $maxage - $today;
my $ds = ($d == 1) ? "" : "s";
$V[AV_A_USER_RESPONSE] = "Please change your password. "
. "It will expire in $d day$ds.";
}
$V[AV_A_PASSWORD_EXPIRY] = 86400 * ($lastchange + $maxage);
if ($V[AV_A_TACTYPE] eq AV_V_TACTYPE_CHPW) {
if ($minage > 0 && $today < $lastchange + $minage) {
my $d = $lastchange + $minage - $today;
my $ds = ($d == 1) ? "" : "s";
$V[AV_A_USER_RESPONSE] = "Need to wait $d day$ds until next password change.";
goto fail;
}
if (defined($flag_pwpolicy)) {
# Reject passwords that are obviously too weak:
if ($V[AV_A_PASSWORD_NEW] =~ /^.?.?.?.?.?.?.?$/ || $V[AV_A_PASSWORD_NEW] !~ /\d/
|| $V[AV_A_PASSWORD_NEW] !~ /[a-z]+/ || $V[AV_A_PASSWORD_NEW] !~ /[A-Z]+/){
$V[AV_A_USER_RESPONSE] =
"Password must consist of at least 8 characters, ".
"include an uppercase letter, a lowercase letter ".
"and a digit.";
goto fail;
}
if (eval "require String::Similarity") {
import String::Similarity;
my $sim = similarity ($V[AV_A_PASSWORD], $V[AV_A_PASSWORD_NEW]);
if ($sim > 0.5) {
$V[AV_A_USER_RESPONSE] = "Old and new password are too similar (factor: $sim).";
goto fail;
}
} else {
print STDERR "Adding String::Similarity is recommended for password similiarity checking";
}
}
my @M = fgrep ($v, \@L, 1);
goto fail if $#M + 1 != $#L;
my $encpw = new_password_hash ($V[AV_A_PASSWORD_NEW]);
push @M, "$user:$encpw:$today:$minage:$maxage:$warn:$remainder";
# ci may modify the original file, messing with our lock.
system("cp $shadow $backup && $ci -l $backup </dev/null 2>/dev/null >&2");
truncate $SHADOW, 0;
seek($SHADOW, 0, 0);
print $SHADOW join('', @M);
close $SHADOW;
$V[AV_A_USER_RESPONSE] = "Password change was successful.";
}
$V[AV_A_RESULT] = AV_V_RESULT_OK;
$result = MAVIS_FINAL;
goto bye;
fail:
$V[AV_A_RESULT] = AV_V_RESULT_FAIL;
$result = MAVIS_FINAL;
goto bye;
down:
$V[AV_A_RESULT] = AV_V_RESULT_NOTFOUND;
$result = MAVIS_DOWN;
goto bye;
fatal:
$result = MAVIS_FINAL;
$V[AV_A_RESULT] = AV_V_RESULT_ERROR;
bye:
close $SHADOW if defined $SHADOW && fileno $SHADOW;
my ($out) = "";
for (my $i = 0; $i <= $#V; $i++) {
$out .= sprintf ("%d %s\n", $i, $V[$i]) if defined $V[$i];
}
$out .= sprintf ("=%d\n", $result);
print $out;
$/ = "\n=\n";
}
# vim: ts=4