X-Git-Url: https://git.adam-barratt.org.uk/?p=mirror%2Fuserdir-ldap-cgi.git;a=blobdiff_plain;f=Util.pm;h=c292a1a1e87797b1333f3db474f489088630cc1d;hp=e97a9e9ea133379222c7709a034dfad2ea5c6daa;hb=HEAD;hpb=7e3253a8e182b7f49f222a760af741dbd27f8367 diff --git a/Util.pm b/Util.pm index e97a9e9..c292a1a 100644 --- a/Util.pm +++ b/Util.pm @@ -1,14 +1,16 @@ # -*- perl -*-x # Copyright (c) 1999-2006 Debian Admin Team Members and Developers (taken from debian/copyright in 2008 by weasel) -# Copyright (c) 2002, 2003, 2004, 2008 Peter Palfrader +# Copyright (c) 2002, 2003, 2004, 2008, 2009, 2011, 2012, 2014, 2015 Peter Palfrader package Util; use strict; use Date::Manip qw(ParseDate); -use Net::LDAP qw(:all); +use Net::LDAP qw(LDAP_SUCCESS LDAP_PROTOCOL_ERROR); use English; +use Crypt::CBC; +use Digest::HMAC_SHA1 qw(hmac_sha1_hex); my $blocksize = 8; # A blowfish block is 8 bytes my $configfile = "/etc/userdir-ldap/userdir-ldap.conf"; @@ -16,157 +18,217 @@ my $configfile = "/etc/userdir-ldap/userdir-ldap.conf"; my %config = &ReadConfigFile; -my $hascryptix = 1; -eval 'use Crypt::Blowfish'; -if ($@) { - $hascryptix = undef; -} - sub CreateKey { my $keysize = shift; my $input; - open (F, "new( -key => $hexkey, -cipher => 'Blowfish'); + my $ciphertext_hex = $cipher->encrypt_hex($plaintext); - for ($pos = 0; $pos < length($input); $pos += $blocksize) { - $output .= unpack("H16", $cipher->encrypt(substr($input, $pos, $blocksize))) if ($hascryptix); - } - return $output; + return $ciphertext_hex; } sub Decrypt { - # like encrypt, needs to deal with big blocks. Note that we assume - # trailing spaces are unimportant. - my $cipher = shift; - my $input = shift; - my ($pos, $portion, $output, $len); - - ((length($input) % $blocksize) == 0) || &HTMLError("Password corrupted"); # should always be true... + my $hexkey = shift; + my $ciphertext_hex = shift; - for ($pos = 0; $pos < length($input); $pos += $blocksize*2) { - $portion = pack("H16", substr($input, $pos, $blocksize*2)); - $output .= $cipher->decrypt($portion) if ($hascryptix); - } - - # check length byte, discard junk - $len = substr($output, 0, 1); - $output = substr($output, 1, ord($len)); - return $output; + my $cipher = Crypt::CBC->new( -key => $hexkey, -cipher => 'Blowfish'); + my $plaintext = $cipher->decrypt_hex($ciphertext_hex); + + return $plaintext; } sub SavePasswordToFile { my $userid = shift; my $password = shift; - my $cipher = shift; - my $cryptuser = crypt($userid, &CreateCryptSalt); - my $secret = Encrypt($cipher, $password); - $cryptuser =~ y/\//_/; # translate slashes to underscores... - - my $fn = "$config{authtokenpath}/$cryptuser"; - open (F, ">$fn") || &HTMLError("$fn: $!"); - print F "$secret\n"; - print F time."\n"; - close F; - chmod 0600, $fn; - return $cryptuser; + my $authtoken = CreateAuthToken($userid); + UpdateAuthToken($authtoken, $password); + return $authtoken; } sub ReadPasswordFromFile { - my $userid = shift; - my $cipher = shift; + my $authtoken = shift; + + my (undef, $hexkey) = ParseAuthToken($authtoken); + my $fn = GetFNfromAuthToken($authtoken); + my $passwd; my $time; - - $userid =~ y/\//_/; # translate slashes to underscores... # if we couldn't read the password file, assume user is unauthenticated. is this ok? - open (F, "<$config{authtokenpath}/$userid") || return undef; + open (F, "<", $fn) || return undef; chomp($passwd = ); chomp($time = ); close F; # check to make sure we read something return undef if (!$passwd || !$time); - + # check to make sure the time is positive, and that the auth token # has not expired my $tdiff = (time - $time); &HTMLError("Your authentication token has expired. Please relogin") if (($tdiff < 0) || ($tdiff > $config{authexpires})); - - return Decrypt($cipher, $passwd); + + return Decrypt($hexkey, $passwd); +} + +# given an authtoken and an unencoded username, check that the username matches the +# encoded and maced username in the authtoken +sub VerifyCryptedUserMatches { + my $authtoken = shift; + my $userid = shift; + + my ($crypteduserid, undef) = ParseAuthToken($authtoken); + + $crypteduserid =~ y,_,/,; # reversee translation from slashes to underscores + my $res = crypt($userid, $crypteduserid); + + HTMLError("Failed to validate user authtoken\n") unless ($res eq $crypteduserid); } -sub CheckAuthToken { - my ($id, $hrkey) = split(/,/, shift, 2); - return undef if (!$id || !$hrkey); - my $key = pack("H".(length($hrkey)), $hrkey); - my $cipher = new Crypt::Blowfish $key; - my $r = ReadPasswordFromFile($id, $cipher); +# given an authtoken and username, update lifetime of the authtoken, if it validates. +sub TouchAuthToken { + my $authtoken = shift; + my $userid = shift; + + VerifyCryptedUserMatches($authtoken, $userid); + + my $r = ReadPasswordFromFile($authtoken); if ($r) { - UpdateAuthToken("$id,$hrkey", $r); - } else { - ClearAuthToken("$id,$hrkey") + UpdateAuthToken($authtoken, $r); + } else { + ClearAuthToken($authtoken); } return $r; } +# clear an authtoken's file from disk, if it validates. sub ClearAuthToken { - my ($id, $hrkey) = split(/,/, shift, 2); - $id =~ y/\//_/; # switch / to _ - unlink "$config{authtokenpath}/$id" || &HTMLError("Error removing authtoken: $!"); + my $authtoken = shift; + + my $fn = GetFNfromAuthToken($authtoken); + + unlink $fn || &HTMLError("Error removing authtoken: $!"); +} + +# Load the base key for our keyed hash function from disk and +# compute a derived key for use in our HMAC function. +sub getAuthTokenHMACkey { + open (F, "<$config{passdir}/key-hmac-$UID") || &Util::HTMLError($!); + my $base_hmac_key = ; + close(F); + chomp($base_hmac_key); + + my $derived_key_hmac = hmac_sha1_hex("authtoken hmac key", $base_hmac_key); + return $derived_key_hmac; +} + +# return a Mac (Message Authentication Code) for data. +sub getDataMac { + my $data = shift; + + my $hmac_key = getAuthTokenHMACkey(); + my $hmac = hmac_sha1_hex($data, $hmac_key); + + return $hmac; +} + +# Given a userid, create an authtoken. +# +# The authtoken consists of the encoded username and a key to decrypt the +# password stored on disk. the authtoken is protected from modification +# by an hmac. +sub CreateAuthToken { + my $userid = shift; + + my $cryptuser = crypt($userid, CreateCryptSalt(1)); + $cryptuser =~ y,/,_,; # translate slashes to underscores + + my $key = &Util::CreateKey($config{blowfishkeylen}); + my $hexkey = unpack("H".($config{blowfishkeylen}*2), $key); + + my $data = "$cryptuser,$hexkey"; + my $hmac = getDataMac($data); + my $authtoken = "$hmac,$data"; + + return $authtoken; +} + +# Parse an authtoken into encoded userid and key information and validate its mac. +sub ParseAuthToken { + my $authtoken = shift; + my ($hmac_got, $data) = split(/,/, $authtoken, 2); + my $hmac_want = getDataMac($data); + + HTMLError("Failed to validate authtoken\n") unless ($hmac_got eq $hmac_want); + + my ($cryptuserid, $hexkey) = split(/,/, $data, 2); + return ($cryptuserid, $hexkey); } +# Given an authtoken, return the path to the on-disk encrypted session file +sub GetFNfromAuthToken { + my $authtoken = shift; + my ($cryptuserid, undef) = ParseAuthToken($authtoken); + + my $fn = "$config{authtokenpath}/$cryptuserid"; + return $fn; +} + +# Given an authtoken and a password, write the password to disk encrypted by the authtoken's key. sub UpdateAuthToken { - my ($id, $hrkey) = split(/,/, shift, 2); + my $authtoken = shift; my $password = shift; - my $key = pack("H".(length($hrkey)), $hrkey); - $id =~ y/\//_/; # switch / to _ - my $cipher = new Crypt::Blowfish $key; - my $secret = Encrypt($cipher, $password); - - my $fn = "$config{authtokenpath}/$id"; - open (F, ">$fn") || &HTMLError("$fn: $!"); + my (undef, $hexkey) = ParseAuthToken($authtoken); + + my $secret = Encrypt($hexkey, $password); + my $fn = GetFNfromAuthToken($authtoken); + + my $saveumask = umask 077; + open (F, ">", $fn) || &HTMLError("$fn: $!"); print F "$secret\n"; print F time."\n"; close F; - chmod 0600, "$fn" || &HTMLError("$fn: $!"); + umask $saveumask; + return 1; } + sub FormatFingerPrint { my $in = shift; my $out; @@ -228,16 +290,21 @@ sub FormatLastSeen { my ($d1, $d2, $lastseen); return "No activity detected" if (!$lastseenpgp && !$lastseenfrom); + $lastseen = $lastseenpgp; $lastseen = $lastseenfrom if (!$lastseenpgp); if ($lastseenfrom && $lastseenpgp) { - ($d1) = ($lastseenpgp =~ /^\[(.+?)\]/); $d1 = ParseDate($d1); + ($d1) = ($lastseenpgp =~ /^\[(.+?)\]/); $d1 = ParseDate($d1); ($d2) = ($lastseenfrom =~ /^\[(.+?)\]/); $d2 = ParseDate($d2); $lastseen = (($d1 gt $d2) ? $lastseenpgp : $lastseenfrom); } - my ($date,$user,$list,$msgid) = ($lastseen =~ /^\[(.+?)\]\s+"(.+?)"\s+"(?:<(.+?)>.*?|\-)"\s+"<(.+?)>"/); + my ($date,$user,$list,$msgid) = ($lastseen =~ /^\[(.+?)\]\s+"(.+?)"\s+"(?:<(.+?)>.*?|\-)"\s+"(<.+?>)"/); $list = "on $list" if ($list); + $date = CGI::escapeHTML($date); + $user = CGI::escapeHTML($user); + $list = CGI::escapeHTML($list); + $msgid = CGI::escapeHTML($msgid); return "$date $list
 Message ID: $msgid"; } @@ -349,12 +416,12 @@ sub UpgradeConnection($) { my ($ldap) = @_; my $mesg = $ldap->start_tls( verify => 'require', - capath => '/etc/ssl/certs/' + cafile => $config{sslcafile}, ); $mesg->sync; if ($mesg->code != LDAP_SUCCESS) { print "Content-type: text/html; charset=utf-8\n\n"; - print "

STARTTLS failed: "..$mesg->error."

\n"; + print "

STARTTLS failed: ".$mesg->error."

\n"; exit(1); }; }; @@ -407,7 +474,7 @@ sub readwrite3($$$$) { } } - next unless (defined(@$readyr)); # Wait some more. + next unless (@$readyr); # Wait some more. for my $rfd (@$readyr) { if ($rfd->eof) {