# -*- 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";
my %config = &ReadConfigFile;
-my $hascryptix = 1;
-eval 'use Crypt::Blowfish';
-if ($@) {
- $hascryptix = undef;
-}
-
sub CreateKey {
my $keysize = shift;
my $input;
- open (F, "</dev/urandom") || die &HTMLError("No /dev/urandom found!");
+ open (F, "<", "/dev/urandom") || die &HTMLError("No /dev/urandom found!");
read(F, $input, $keysize); # key length is 8 bytes
close F;
-
+
return $input;
}
sub CreateCryptSalt {
+ # CreateCryptSalt(type = 0, skip_header = 0)
# this can create either a DES type salt or a MD5 salt
+ # 0 for DES, 1 for MD5 salt
+ # if skip_header is 0, does not add $1$ for md5 salts.
my $md5 = shift; # do we want a MD5 salt?
+ my $no_crypttype_header = shift;
my $validstr = './0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
my @valid = split(//,$validstr);
my ($in, $out);
-
+
my $cryptsaltlen = ($md5 ? 8 : 2);
-
+
open (F, "</dev/urandom") || die &HTMLError("No /dev/urandom found!");
foreach (1..$cryptsaltlen) {
read(F, $in, 1);
$out .= $valid[ord($in) % ($#valid + 1)];
}
close F;
- return ($md5 ? "\$1\$$out\$" : $out);
+ if ($md5 == 1 && !$no_crypttype_header) { $out = "\$1\$$out\$"; }
+ return $out
}
-sub Encrypt {
- # blowfish only encrypts things in blocks of 8 bytes, so we
- # need a custom routine that handles longer strings....
- my $cipher = shift;
- my $input = shift;
- my ($pos, $output);
+sub Encrypt {
+ my $hexkey = shift;
+ my $plaintext = shift;
- # prepend a length byte */
- $input = chr(length($input)).$input;
- $input .= "\001" x ($blocksize - (length($input) % $blocksize)) if (length($input % $blocksize));
+ my $cipher = Crypt::CBC->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 = <F>);
chomp($time = <F>);
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 <a href=\"https://$ENV{SERVER_NAME}/$config{webloginhtml}\">relogin</a>") 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 = <F>;
+ 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;
my ($d1, $d2, $lastseen);
return "<b>No activity detected</b>" 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<br> Message ID: $msgid";
}
my ($ldap) = @_;
my $mesg = $ldap->start_tls(
verify => 'require',
- cafile => '/etc/ssl/certs/spi-cacert-2008.pem'
+ cafile => $config{sslcafile},
);
$mesg->sync;
if ($mesg->code != LDAP_SUCCESS) {
print "Content-type: text/html; charset=utf-8\n\n";
- print "<html><body><h1>STARTTLS failed: "..$mesg->error."</h1></body></html>\n";
+ print "<html><body><h1>STARTTLS failed: ".$mesg->error."</h1></body></html>\n";
exit(1);
};
};
}
}
- next unless (defined(@$readyr)); # Wait some more.
+ next unless (@$readyr); # Wait some more.
for my $rfd (@$readyr) {
if ($rfd->eof) {