md format
[hband-tools.git] / pam / pam_multipasswd
blob0f70685b59c274fb1bd2369c0473301c284a6408
1 #!/usr/bin/env perl
3 # Synopsis:
4 # pam_exec.so [debug] [expose_authtok] [seteuid] [quiet] [log=file] /usr/local/libexec/pam_multipasswd [pam_service]
5 # Files:
6 # /etc/security/pam_multipasswd.conf
7 # /etc/security/pam_multipasswd.hook (script)
8 # Environment variables:
9 # PAM_RHOST, PAM_RUSER, PAM_SERVICE, PAM_TTY, PAM_USER, PAM_TYPE, PROGNAME
10 # Example pam config:
11 # pure-ftpd auth required pam_exec_wrap.so expose_authtok /usr/local/libexec/pam_multipasswd
12 # Hook script outputs lines like:
13 # set <variable> <value>
14 # Variables recognized: shadow_home rhost tty pam_servicenick
15 # So you can override these internal variables dynamically call-by-call
17 use Digest::MD5;
18 use MIME::Base64;
19 use Crypt::PasswdMD5;
20 use Authen::Simple::PAM;
21 use Data::Dumper;
22 %PAM_STATUS = (
23 SUCCESS=>0, # Successful function return
24 OPEN_ERR=>1, # dlopen() failure when dynamically
25 # loading a service module
26 SYMBOL_ERR=>2, # Symbol not found
27 SERVICE_ERR=>3, # Error in service module
28 SYSTEM_ERR=>4, # System error
29 BUF_ERR=>5, # Memory buffer error
30 PERM_DENIED=>6, # Permission denied
31 AUTH_ERR=>7, # Authentication failure
32 CRED_INSUFFICIENT=>8, # Can not access authentication data
33 # due to insufficient credentials
34 AUTHINFO_UNAVAIL=>9, # Underlying authentication service
35 # can not retrieve authentication information
36 USER_UNKNOWN=>10, # User not known to the underlying
37 # authenticaiton module
38 MAXTRIES=>11, # An authentication service has
39 # maintained a retry count which has
40 # been reached. No further retries
41 # should be attempted
42 NEW_AUTHTOK_REQD=>12, # New authentication token required.
43 # This is normally returned if the
44 # machine security policies require
45 # that the password should be changed
46 # beccause the password is NULL or it
47 # has aged
48 ACCT_EXPIRED=>13, # User account has expired
49 SESSION_ERR=>14, # Can not make/remove an entry for
50 # the specified session
51 CRED_UNAVAIL=>15, # Underlying authentication service
52 # can not retrieve user credentials unavailable
53 CRED_EXPIRED=>16, # User credentials expired
54 CRED_ERR=>17, # Failure setting user credentials
55 NO_MODULE_DATA=>18, # No module specific data is present
56 CONV_ERR=>19, # Conversation error
57 AUTHTOK_ERR=>20, # Authentication token manipulation error
58 AUTHTOK_RECOVERY_ERR=>21, # Authentication information cannot be recovered
59 AUTHTOK_LOCK_BUSY=>22, # Authentication token lock busy
60 AUTHTOK_DISABLE_AGING=>23, # Authentication token aging disabled
61 TRY_AGAIN=>24, # Preliminary check by password service
62 IGNORE=>25, # Ignore underlying account module
63 # regardless of whether the control
64 # flag is required, optional, or sufficient
65 ABORT=>26, # Critical error (?module fail now request)
66 AUTHTOK_EXPIRED=>27, # user's authentication token has expired
67 MODULE_UNKNOWN=>28, # module is not known
68 BAD_ITEM=>29, # Bad item passed to pam_*_item()
69 CONV_AGAIN=>30, # conversation function is event driven and data is not available yet
70 INCOMPLETE=>31, # please call this function again to complete authentication stack.
71 # Before calling again, verify that conversation is completed
73 %PAM_STATUS_TEXT = reverse %PAM_STATUS;
74 use Sys::Syslog qw/openlog syslog/;
75 use List::MoreUtils qw/any/;
79 sub pam_return($)
81 my $pam_result = shift;
82 openlog $ENV{'PROGNAME'}, '', Sys::Syslog::LOG_AUTH;
83 syslog Sys::Syslog::LOG_INFO, sprintf "pam_multipasswd(%s:%s): code %d, %s, logname=%s uid=%d euid=%d tty=%s ruser=%s rhost=%s user=%s shadow_home=%s",
84 $pam_service, $ENV{'PAM_TYPE'}, $pam_result, ($PAM_STATUS_TEXT{$pam_result} || "unknown status code"), $ENV{'LOGNAME'}||(getpwuid$<)[0], $<, $>, $ENV{'PAM_TTY'}, $ENV{'PAM_RUSER'}, $ENV{'PAM_RHOST'}, $ENV{'PAM_USER'}, $shadow_home;
85 exit $pam_result;
88 sub check_password($$$)
90 my $user = shift;
91 my $given = shift;
92 my $stored = shift;
93 my $cmpstr;
95 if(substr($stored, 0, 1) eq '$')
97 my ($none, $hashtype, $salt, $hash) = split(/\$/, $stored, 4);
98 if(grep {$hashtype eq $_} qw/1 5 6/)
100 # md5, sha-256 and sha-512 formats respectively
101 $cmpstr = crypt($given, '$'.$hashtype.'$'.$salt);
103 elsif($stored_type == 'apr1')
105 $cmpstr = apache_md5_crypt($given, $salt);
108 elsif(substr($stored, 0, 1) eq '@')
110 my $pam_svc = substr($stored, 1);
111 my $pam = Authen::Simple::PAM->new( service => $pam_svc );
112 if($pam->authenticate($user, $given))
114 return $PAM_STATUS{SUCCESS};
117 elsif(substr($stored, 0, 1) eq '=')
119 $cmpstr = '=' . encode_base64($given, '');
121 elsif(length $stored == 13)
123 # crypt(3) format (password length max 8 char)
124 my $salt = substr($stored, 0, 2);
125 $cmpstr = crypt($given, $salt);
127 elsif($stored eq '*')
129 return $PAM_STATUS{SUCCESS};
131 else
133 # assuming $stored is a cleartext password
134 $cmpstr = $given;
137 if(defined $cmpstr and $stored eq $cmpstr)
139 return $PAM_STATUS{SUCCESS};
141 return $PAM_STATUS{PERM_DENIED};
144 sub in_subnet_ip4($$)
146 my ($cidr, $ip) = @_;
147 # if no netmask given then IP must equal to CIDR
148 return ($cidr eq $ip) unless $cidr =~ /^(.*)\/(.*)$/;
149 my ($subnet, $mask) = ($1, $2);
150 # the /0 mask matches to the whole world
151 return 1 if $mask == 0;
152 # mask IPv4 addresses
153 return (((unpack('N', pack('C4', split(/\./, $ip))) ^ unpack('N', pack('C4', split(/\./, $subnet)))) & (0xFFFFFFFF << (32-$mask))) == 0);
158 $configfile = "/etc/security/pam_multipasswd.conf";
159 $pam_service = defined $ARGV[0] ? $ARGV[0] : $ENV{'PAM_SERVICE'};
160 $pam_servicenick = $pam_service;
162 pam_return($PAM_STATUS{IGNORE}) if $ENV{'PAM_TYPE'} ne 'auth';
164 $givenpass = <STDIN>;
165 $givenpass =~ s/[\r\n\x00]*$//;
166 $pam_result = undef;
167 $shadow_home = (getpwnam $ENV{'PAM_USER'})[7];
169 open my $fh, '<', $configfile;
170 while(<$fh>)
172 next if /^\s*(#|$)/;
173 if(/^\s*service\s*map\s*(\S+)\s+(\S+)\s*$/)
175 if($pam_service eq $1)
177 $pam_servicenick = $2;
180 else
182 print STDERR "unknown config in $configfile line $.\n";
185 close $fh;
187 my $rhost = defined $ENV{'PAM_RHOST'} ? $ENV{'PAM_RHOST'} : "unknown";
188 my $tty = defined $ENV{'PAM_TTY'} ? $ENV{'PAM_TTY'} : "unknown";
190 # Hook script
191 open my $ph, '-|', "/etc/security/pam_multipasswd.hook";
192 while(<$ph>)
194 if(my ($variable, $value) = /^set (shadow_home|rhost|tty|pam_servicenick) (.*)$/)
196 eval "\$$variable = \$value";
198 else
200 print STDERR "pam_multipasswd: unrecognized hook script output: $_";
203 close $ph;
205 pam_return($PAM_STATUS{USER_UNKNOWN}) if not defined $shadow_home;
207 @precedence = (
208 "$pam_servicenick.d/rhost/$rhost",
209 "$pam_servicenick.d/tty/$tty",
210 "$pam_servicenick",
211 "other",
214 for my $pwdfile (map {"$shadow_home/.shadow.d/$_"} @precedence)
216 if(-f $pwdfile)
218 if(open my $fh, '<', $pwdfile)
220 while(<$fh>)
222 next if /^\s*#/;
223 s/[\r\n\x00]*$//;
224 my ($uuser, $upass, $ucidr) = split /:/;
225 if($uuser eq '*' or $uuser eq $ENV{'PAM_USER'})
227 if(!defined $ucidr or any {in_subnet_ip4($_, $rhost)} split /,/, $ucidr)
229 $pam_result = check_password($uuser, $givenpass, $upass);
230 if($pam_result eq $PAM_STATUS{SUCCESS})
232 last;
237 close $fh;
238 if(defined $pam_result)
240 last;
243 else
245 print STDERR "$pwdfile: $!\n";
246 $pam_result = $PAM_STATUS{SYSTEM_ERR};
247 last;
253 $pam_result = $PAM_STATUS{AUTHINFO_UNAVAIL} if not defined $pam_result;
254 pam_return($pam_result);