Better salt
[mirror/userdir-ldap-cgi.git] / Util.pm
1 # -*- perl -*-x
2
3 # Copyright (c) 1999-2006  Debian Admin Team Members and Developers    (taken from debian/copyright in 2008 by weasel)
4 # Copyright (c) 2002, 2003, 2004, 2008 Peter Palfrader
5
6 package Util;
7
8 use strict;
9 use Date::Manip qw(ParseDate);
10 use Net::LDAP qw(LDAP_SUCCESS LDAP_PROTOCOL_ERROR);
11 use English;
12
13 my $blocksize = 8; # A blowfish block is 8 bytes
14 my $configfile = "/etc/userdir-ldap/userdir-ldap.conf";
15 #my $configfile = "/home/randolph/html/debian/perl/userdir-ldap.conf";
16
17 my %config = &ReadConfigFile;
18
19 my $hascryptix = 1;
20 eval 'use Crypt::Blowfish';
21 if ($@) {
22   $hascryptix = undef;
23 }
24
25 sub CreateKey {
26   my $keysize = shift;
27   my $input;
28   open (F, "</dev/urandom") || die &HTMLError("No /dev/urandom found!");
29   read(F, $input, $keysize); # key length is 8 bytes
30   close F;
31   
32   return $input;
33 }
34
35 sub CreateCryptSalt {
36   # this can create either a DES type salt or a MD5 salt
37   my $md5 = shift; # do we want a MD5 salt?
38   my $validstr = './0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
39   my @valid = split(//,$validstr);
40   my ($in, $out);
41   
42   my $cryptsaltlen = ($md5 ? 8 : 2);
43   
44   open (F, "</dev/urandom") || die &HTMLError("No /dev/urandom found!");
45   foreach (1..$cryptsaltlen) {
46     read(F, $in, 1);
47     $out .= $valid[ord($in) % ($#valid + 1)];
48   }
49   close F;
50   return ($md5 ? "\$1\$$out\$" : $out);
51 }
52
53 sub CreateMD5Salt {
54   my $validstr = './0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
55   my @valid = split(//,$validstr);
56   my ($in, $out);
57   
58   my $cryptsaltlen = 8;
59   
60   open (F, "</dev/urandom") || die &HTMLError("No /dev/urandom found!");
61   foreach (1..$cryptsaltlen) {
62     read(F, $in, 1);
63     $out .= $valid[ord($in) % ($#valid + 1)];
64   }
65   close F;
66   return $out;
67 }
68
69 sub Encrypt { 
70   # blowfish only encrypts things in blocks of 8 bytes, so we
71   # need a custom routine that handles longer strings....
72   my $cipher = shift;
73   my $input = shift;
74   my ($pos, $output);
75
76   # prepend a length byte */
77   $input = chr(length($input)).$input;
78   $input .= "\001" x ($blocksize - (length($input) % $blocksize)) if (length($input % $blocksize));
79
80   for ($pos = 0; $pos < length($input); $pos += $blocksize) {    
81     $output .= unpack("H16", $cipher->encrypt(substr($input, $pos, $blocksize))) if ($hascryptix);
82   }
83   return $output;
84 }
85
86 sub Decrypt {
87   # like encrypt, needs to deal with big blocks. Note that we assume
88   # trailing spaces are unimportant.
89   my $cipher = shift;
90   my $input = shift;
91   my ($pos, $portion, $output, $len);
92   
93   ((length($input) % $blocksize) == 0) || &HTMLError("Password corrupted"); # should always be true...
94
95   for ($pos = 0; $pos < length($input); $pos += $blocksize*2) {
96     $portion = pack("H16", substr($input, $pos, $blocksize*2));
97     $output .= $cipher->decrypt($portion) if ($hascryptix);
98   }
99   
100   # check length byte, discard junk
101   $len = substr($output, 0, 1);
102   $output = substr($output, 1, ord($len));
103   return $output;
104 }
105
106 sub SavePasswordToFile {
107   my $userid = shift;
108   my $password = shift;
109   my $cipher = shift;
110
111   my $cryptuser = crypt($userid, &CreateCryptSalt);
112   my $secret = Encrypt($cipher, $password);
113   $cryptuser =~ y/\//_/; # translate slashes to underscores...
114   
115   my $fn = "$config{authtokenpath}/$cryptuser";
116   open (F, ">$fn") || &HTMLError("$fn: $!");
117   print F "$secret\n";
118   print F time."\n";
119   close F;
120   chmod 0600, $fn;
121   return $cryptuser;
122 }
123
124 sub ReadPasswordFromFile {
125   my $userid = shift;
126   my $cipher = shift;
127   my $passwd;
128   my $time;
129   
130   $userid =~ y/\//_/; # translate slashes to underscores...
131
132   # if we couldn't read the password file, assume user is unauthenticated. is this ok?
133   open (F, "<$config{authtokenpath}/$userid") || return undef; 
134   chomp($passwd = <F>);
135   chomp($time = <F>);
136   close F; 
137
138   # check to make sure we read something
139   return undef if (!$passwd || !$time);
140   
141   # check to make sure the time is positive, and that the auth token
142   # has not expired
143   my $tdiff = (time - $time);
144   &HTMLError("Your authentication token has expired. Please <a href=\"https://$ENV{SERVER_NAME}/$config{webloginhtml}\">relogin</a>") if (($tdiff < 0) || ($tdiff > $config{authexpires}));
145   
146   return Decrypt($cipher, $passwd);
147 }
148
149 sub CheckAuthToken {
150   my ($id, $hrkey) = split(/,/, shift, 2);
151   return undef if (!$id || !$hrkey);
152   my $key = pack("H".(length($hrkey)), $hrkey);
153   my $cipher = new Crypt::Blowfish $key;
154   my $r = ReadPasswordFromFile($id, $cipher);
155   if ($r) {
156     UpdateAuthToken("$id,$hrkey", $r);
157   } else {    
158     ClearAuthToken("$id,$hrkey")
159   }
160   return $r;
161 }
162
163 sub ClearAuthToken {
164   my ($id, $hrkey) = split(/,/, shift, 2);
165   $id =~ y/\//_/; # switch / to _
166   unlink "$config{authtokenpath}/$id" || &HTMLError("Error removing authtoken: $!");
167 }
168
169 sub UpdateAuthToken {
170   my ($id, $hrkey) = split(/,/, shift, 2);
171   my $password = shift;
172   my $key = pack("H".(length($hrkey)), $hrkey);
173   $id =~ y/\//_/; # switch / to _
174   my $cipher = new Crypt::Blowfish $key;
175   my $secret = Encrypt($cipher, $password);
176   
177   my $fn = "$config{authtokenpath}/$id";
178   open (F, ">$fn") || &HTMLError("$fn: $!");
179   print F "$secret\n";
180   print F time."\n";
181   close F;
182   chmod 0600, "$fn" || &HTMLError("$fn: $!");
183   return 1;
184 }
185
186 sub FormatFingerPrint {
187   my $in = shift;
188   my $out;
189   
190   if (length($in) == 32) {
191     foreach (0..15) {
192       $out .= substr($in, $_*2, 2)." ";
193       $out .= "&nbsp;" if ($_ == 7);
194     }      
195   } else {
196     foreach (0..int(length($in)/4)) {
197       $out .= substr($in, $_*4, 4)." ";
198     }      
199   }
200   chop $out;
201   return $out;
202 }
203
204 sub FetchKey {
205   my $fingerprint = shift;
206   my $signatures = shift;
207   my ($out, $keyringparam) = undef;
208   
209   foreach (split(/:/, $config{keyrings})) {
210     $keyringparam .= "--keyring $_ ";
211   }
212   
213   $fingerprint =~ s/\s//g;
214   $fingerprint = "0x".$fingerprint;
215
216   local $ENV{PATH} = '';
217   $/ = undef; # just suck it up ....
218   if ($signatures) {
219       open(FP, "$config{gpg} --no-options --no-default-keyring $keyringparam --list-sigs --fingerprint $fingerprint|");
220       $out = <FP>;
221       close FP;
222   }
223   open(FP, "$config{gpg} --no-options --no-default-keyring $keyringparam --export -a $fingerprint|");
224   $out .= <FP>;
225   close FP;
226   $/ = "\n";
227   
228   return $out;
229 }
230
231 sub FormatTimestamp {
232   my $in = shift;
233   $in =~ /^(....)(..)(..)(..)(..)(..)/;
234   
235   return sprintf("%04d/%02d/%02d %02d:%02d:%02d UTC", $1,$2,$3,$4,$5,$6);
236 }
237
238 sub FormatLastSeen {
239 # Format:
240 # [Tue, 11 Jan 2000 02:37:18] "Joey Hess <joeyh@debian.org>" "<debian-boot@lists.debian.org> archive/latest/7130" "<20000110181924.H19910@kitenet.net>"
241 # [Mon, 10 Jan 2000 21:48:19] "9E1E 1052 F8BB A351  0606 5527 50BB 2974 2D59 A7D2" "<debian-devel-changes@lists.debian.org> archive/latest/58632" "<20000110200506.13257.qmail@master.debian.org>"
242   my $lastseenpgp = shift;
243   my $lastseenfrom = shift;
244   my ($d1, $d2, $lastseen);
245
246   return "<b>No activity detected</b>" if (!$lastseenpgp && !$lastseenfrom);
247   $lastseen = $lastseenpgp;
248   $lastseen = $lastseenfrom if (!$lastseenpgp);
249
250   if ($lastseenfrom && $lastseenpgp) {
251     ($d1) = ($lastseenpgp =~ /^\[(.+?)\]/); $d1 = ParseDate($d1);
252     ($d2) = ($lastseenfrom =~ /^\[(.+?)\]/); $d2 = ParseDate($d2);
253     $lastseen = (($d1 gt $d2) ? $lastseenpgp : $lastseenfrom);
254   }
255
256   my ($date,$user,$list,$msgid) = ($lastseen =~ /^\[(.+?)\]\s+"(.+?)"\s+"(?:<(.+?)>.*?|\-)"\s+"(<.+?>)"/);
257   $list = "on $list" if ($list);
258   $date = CGI::escapeHTML($date);
259   $user = CGI::escapeHTML($user);
260   $list = CGI::escapeHTML($list);
261   $msgid = CGI::escapeHTML($msgid);
262   return "$date $list<br>&nbsp;Message ID: $msgid";
263 }
264
265 sub LookupCountry {
266   my $in = shift;
267   my ($abbrev, $country);
268   open (F, $config{countrylist}) || return uc($in);
269   while (<F>) {
270     chomp;
271     ($abbrev, $country) = split(/\s+/, $_, 2);
272     if ($abbrev eq $in) {
273       close F;
274       return $country;
275     }
276   }
277   close F;
278   return uc($in);
279 }
280
281 ####################
282 # Some HTML Routines
283
284 my $htmlhdrsent = 0;
285
286 sub HTMLSendHeader {
287   print "Content-type: text/html; charset=utf-8\n\n" if (!$htmlhdrsent);
288   $htmlhdrsent = 1;
289 }
290
291 sub HTMLPrint {
292   &HTMLSendHeader if (!$htmlhdrsent);
293   print shift;
294 }
295
296 sub HTMLError {
297   HTMLPrint(shift);
298   die "\n";
299 }
300
301 sub CheckLatLong {
302   my ($lat, $long) = @_;
303
304   $lat =~ s/[^-+\.\d]//g; $long =~ s/[^-+\.\d]//g;
305   
306   if (($lat =~ /^(\-|\+?)\d+(\.\d+)?/) && ($long =~ /^(\-|\+?)\d+(\.\d+)?/)) {
307     return ($lat, $long);
308   } else {
309     return ("", "");
310   }
311 }
312
313 sub FixParams {
314   my $query = shift;
315   my $key;
316   my @names = $query->param;
317
318   foreach $key (@names) { # web security is a joke ... <sigh>
319     $_ = $query->param($key);
320     s/&/&amp;/g; 
321     s/[<\x8B]/&lt;/g;
322     s/[>\x9B]/&gt;/g;
323
324     $query->param($key, $_);
325   }
326 }   
327
328   
329 sub LDAPUpdate {
330   my $ldap = shift;
331   my $dn = shift;
332   my $attr = shift;
333   my $val = shift;
334   my $mesg;
335   
336   if (!$val) {
337     $mesg = $ldap->modify($dn, delete => { $attr => [] });
338   } else {
339     $val = [ $val ] if (!ref($val));
340     $mesg = $ldap->modify($dn, replace => { $attr => $val });
341     $mesg->code && &Util::HTMLError("error updating $attr: ".$mesg->error);
342   }
343 }
344
345 ###################
346 # Config file stuff
347 sub ReadConfigFile {
348   # reads a config file and results a hashref with the results
349   my (%config, $attr, $setting);
350   open (F, "<$configfile") || &HTMLError("Cannot open $configfile: $!");
351   while (<F>) {
352     chomp;
353     if ((!/^\s*#/) && ($_ ne "")) {
354       # Chop off any trailing comments
355       s/#.*//;
356       /([^=]+)=(.*)/;
357       ($attr, $setting) = ($1, $2);
358       $setting =~ s/"//g; #"
359       $setting =~ s/;$//;
360       $attr =~ s/^\s+//; $attr =~ s/\s+$//;
361       $setting =~ s/^\s+//; $setting =~ s/\s+$//;      
362       $config{$attr} = $setting;
363     }
364   }
365   close F;
366   return %config;
367 }
368
369 sub UpgradeConnection($) {
370   my ($ldap) = @_;
371   my $mesg = $ldap->start_tls(
372                           verify => 'require',
373                           cafile => '/etc/ssl/certs/spi-cacert-2008.pem'
374                           );
375   $mesg->sync;
376   if ($mesg->code != LDAP_SUCCESS) {
377     print "Content-type: text/html; charset=utf-8\n\n";
378     print "<html><body><h1>STARTTLS failed: ".$mesg->error."</h1></body></html>\n";
379     exit(1);
380   };
381 };
382
383 sub readwrite3($$$$) {
384   my ($in, $inputfd, $stdoutfd, $stderrfd) = @_;
385
386   #Echolot::Log::trace("Entering readwrite_gpg.");
387
388   local $INPUT_RECORD_SEPARATOR = undef;
389   my $sout = IO::Select->new();
390   my $sin = IO::Select->new();
391   my $offset = 0;
392
393   #Echolot::Log::trace("input is $inputfd; output is $stdoutfd; err is $stderrfd; status is ".(defined $statusfd ? $statusfd : 'undef').".");
394
395   $inputfd->blocking(0);
396   $stdoutfd->blocking(0);
397   $stderrfd->blocking(0);
398   $sout->add($stdoutfd);
399   $sout->add($stderrfd);
400   $sin->add($inputfd);
401
402   my ($stdout, $stderr) = ("", "", "");
403
404   my ($readyr, $readyw);
405   while ($sout->count() > 0 || (defined($sin) && ($sin->count() > 0))) {
406     #Echolot::Log::trace("select waiting for ".($sout->count())." fds.");
407     ($readyr, $readyw, undef) = IO::Select::select($sout, $sin, undef, 42);
408     #Echolot::Log::trace("ready: write: ".(defined $readyw ? scalar @$readyw : 'none')."; read: ".(defined $readyr ? scalar @$readyr : 'none'));
409     for my $wfd (@$readyw) {
410       #Echolot::Log::trace("writing to $wfd.");
411       my $written = 0;
412       if ($offset != length($in)) {
413         $written = $wfd->syswrite($in, length($in) - $offset, $offset);
414       }
415       unless (defined ($written)) {
416         #Echolot::Log::warn("Error while writing to GnuPG: $!");
417         close $wfd;
418         $sin->remove($wfd);
419         $sin = undef;
420       } else {
421         $offset += $written;
422         if ($offset == length($in)) {
423           #Echolot::Log::trace("writing to $wfd done.");
424           close $wfd;
425           $sin->remove($wfd);
426           $sin = undef;
427         }
428       }
429     }
430
431     next unless (defined(@$readyr)); # Wait some more.
432
433     for my $rfd (@$readyr) {
434       if ($rfd->eof) {
435         #Echolot::Log::trace("reading from $rfd done.");
436         $sout->remove($rfd);
437         close($rfd);
438         next;
439       }
440       #Echolot::Log::trace("reading from $rfd.");
441       if ($rfd == $stdoutfd) {
442         $stdout .= <$rfd>;
443         next;
444       }
445       if ($rfd == $stderrfd) {
446         $stderr .= <$rfd>;
447         next;
448       }
449     }
450   }
451   #Echolot::Log::trace("readwrite_gpg done.");
452   return ($stdout, $stderr);
453 };
454
455 sub checkPasswordQuality($$$) {
456   my ($pw, $oldpw, $ldapelements) = @_;
457   my ($stdinR, $stdinW) = (IO::Handle->new(), IO::Handle->new());
458   my ($stdoutR, $stdoutW) = (IO::Handle->new(), IO::Handle->new());
459   my ($stderrR, $stderrW) = (IO::Handle->new(), IO::Handle->new());
460   pipe $stdinR, $stdinW;
461   pipe $stdoutR, $stdoutW;
462   pipe $stderrR, $stderrW;
463
464   my $pid = fork();
465   unless (defined $pid) {
466     return (2, "Could not fork: $!");
467   };
468   unless ($pid) { # child
469     $stdinW->close;
470     $stdoutR->close;
471     $stderrR->close;
472     close STDIN;
473     close STDOUT;
474     close STDERR;
475     open (STDIN, "<&".$stdinR->fileno) or warn ("Cannot dup stdinR (fd ".$stdinR->fileno.") as STDIN: $!");
476     open (STDOUT, ">&".$stdoutW->fileno) or warn ("Cannot dup stdoutW (fd ".$stdoutW->fileno.") as STDOUT: $!");
477     open (STDERR, ">&".$stderrW->fileno) or warn ("Cannot dup stderrW (fd ".$stderrW->fileno.") as STDERR: $!");
478     { exec('/usr/lib/userdir-ldap-cgi/password-qualify-check'); }
479     $stderrW->print("Could not exec password-qualify-check: $!\n");
480     exit(1);
481   };
482   $stdinR->close;
483   $stdoutW->close;
484   $stderrW->close;
485
486   $oldpw = '' unless defined $oldpw;
487   my $out = join("\n", $pw, $oldpw, @$ldapelements)."\n";
488   my ($stdout, $stderr) = readwrite3($out, $stdinW, $stdoutR, $stderrR);
489   waitpid $pid, 0;
490   
491   my $exitcode = $? >> 8;
492   if ($exitcode == 0 && $stdout eq '' && $stderr eq '') {
493     return (0, "ok");
494   } elsif ($exitcode == 1 && $stderr eq '') {
495     return (1, $stdout);
496   } else {
497     return (2, "check exited with exit code $exitcode, said '$stdout' on stdout, and '$stderr' on stderr.");
498   };
499 };
500 1;