Include accountname in totp url
[mirror/userdir-ldap-cgi.git] / Util.pm
diff --git a/Util.pm b/Util.pm
index 14fab61..c292a1a 100644 (file)
--- a/Util.pm
+++ b/Util.pm
@@ -1,7 +1,7 @@
 # -*- 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;
 
@@ -9,6 +9,8 @@ use strict;
 use Date::Manip qw(ParseDate);
 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,19 +18,13 @@ 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;
 }
 
@@ -55,123 +51,184 @@ sub CreateCryptSalt {
   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);
 }
 
-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 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);
+}
+
+# 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;
@@ -357,7 +414,10 @@ sub ReadConfigFile {
 
 sub UpgradeConnection($) {
   my ($ldap) = @_;
-  my $mesg = $ldap->start_tls();
+  my $mesg = $ldap->start_tls(
+                          verify => 'require',
+                          cafile => $config{sslcafile},
+                          );
   $mesg->sync;
   if ($mesg->code != LDAP_SUCCESS) {
     print "Content-type: text/html; charset=utf-8\n\n";
@@ -414,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) {