Passwörter: Hash-Verfahren PBKDF2 unterstützen und als Standard nutzen
authorMoritz Bunkus <m.bunkus@linet-services.de>
Mon, 11 Jan 2016 10:55:53 +0000 (11:55 +0100)
committerMoritz Bunkus <m.bunkus@linet-services.de>
Mon, 11 Jan 2016 12:33:40 +0000 (13:33 +0100)
Der aktuelle Stand der Technik sind die SHA-*-Varianten schon lange
nicht mehr. In der Zwischenzeit wurden der PBKDF2-Mechanismus
entwickelt, um schnelles Berechnen zu erschweren. Noch neuer und in
ASICs noch schwerer umsetzbar sind BCrypt und SCrypt, für die es aber
noch keine weit verbreiteten Perl-Module gibt.

SL/Auth.pm
SL/Auth/DB.pm
SL/Auth/Password.pm
SL/InstallationCheck.pm
doc/UPGRADE
doc/changelog
modules/fallback/PBKDF2/Tiny.pm [new file with mode: 0644]

index 788154c..9f2475f 100644 (file)
@@ -168,8 +168,8 @@ sub authenticate_root {
     return ERR_PASSWORD;
   }
 
-  $password             = SL::Auth::Password->hash(login => 'root', password => $password);
   my $admin_password    = SL::Auth::Password->hash_if_unhashed(login => 'root', password => $self->{admin_password}->());
+  $password             = SL::Auth::Password->hash(login => 'root', password => $password, stored_password => $admin_password);
 
   my $result = $password eq $admin_password ? OK : ERR_PASSWORD;
   $self->set_session_value(SESSION_KEY_ROOT_AUTH() => $result);
index 0bbc050..93e5cc0 100644 (file)
@@ -38,17 +38,15 @@ sub authenticate {
 
   my $stored_password = $self->{auth}->get_stored_password($login);
 
-  my ($algorithm, $algorithm2);
-
   # Empty password hashes in the database mean just that -- empty
   # passwords. Hash it for easier comparison.
-  $stored_password               = SL::Auth::Password->hash(password => $stored_password) unless $stored_password;
-  ($algorithm, $stored_password) = SL::Auth::Password->parse($stored_password);
-  ($algorithm2, $password)       = SL::Auth::Password->parse(SL::Auth::Password->hash(password => $password, algorithm => $algorithm, login => $login));
+  $stored_password    = SL::Auth::Password->hash(password => $stored_password) unless $stored_password;
+  my ($algorithm)     = SL::Auth::Password->parse($stored_password);
+  my $hashed_password = SL::Auth::Password->hash(password => $password, algorithm => $algorithm, login => $login, stored_password => $stored_password);
 
   $main::lxdebug->leave_sub();
 
-  return $password eq $stored_password ? OK : ERR_PASSWORD;
+  return $hashed_password eq $stored_password ? OK : ERR_PASSWORD;
 }
 
 sub can_change_password {
index 5ae75ea..d566cf0 100644 (file)
@@ -5,11 +5,44 @@ use strict;
 use Carp;
 use Digest::MD5 ();
 use Digest::SHA ();
+use Encode ();
+use PBKDF2::Tiny ();
+
+sub hash_pkkdf2 {
+  my ($class, %params) = @_;
+
+  # PBKDF2::Tiny expects data to be in octets. Therefore we must
+  # encode everything we hand over (login, password) to UTF-8.
+
+  # This hash method uses a random hash and not just the user's login
+  # for its salt. This is due to the official recommendation that at
+  # least eight octets of random data should be used. Therefore we
+  # must store the salt together with the hashed password. The format
+  # in the database is:
+
+  # {PBKDF2}salt-in-hex:hash-in-hex
+
+  my $salt;
+
+  if ((defined $params{stored_password}) && ($params{stored_password} =~ m/^\{PBKDF2\} ([0-9a-f]+) :/x)) {
+    $salt = (split m{:}, Encode::encode('utf-8', $1), 2)[0];
+
+  } else {
+    my @login  = map { ord } split m{}, Encode::encode('utf-8', $params{login});
+    my @random = map { int(rand(256)) } (0..16);
+
+    $salt      = join '', map { sprintf '%02x', $_ } @login, @random;
+  }
+
+  my $hashed = "{PBKDF2}${salt}:" . join('', map { sprintf '%02x', ord } split m{}, PBKDF2::Tiny::derive('SHA-256', $salt, Encode::encode('utf-8', $params{password})));
+
+  return $hashed;
+}
 
 sub hash {
   my ($class, %params) = @_;
 
-  $params{algorithm} ||= 'SHA256S';
+  $params{algorithm} ||= 'PBKDF2';
 
   my $salt = $params{algorithm} =~ m/S$/ ? $params{login} : '';
 
@@ -25,6 +58,9 @@ sub hash {
   } elsif ($params{algorithm} eq 'CRYPT') {
     return '{CRYPT}' . crypt($params{password}, substr($params{login}, 0, 2));
 
+  } elsif ($params{algorithm} =~ m/^PBKDF2/) {
+    return $class->hash_pkkdf2(password => $params{password}, stored_password => $params{stored_password});
+
   } else {
     croak 'Unsupported hash algorithm ' . $params{algorithm};
   }
index 4d46fd6..9882e0e 100644 (file)
@@ -25,6 +25,7 @@ BEGIN {
   { name => "DateTime::Format::Strptime",          url => "http://search.cpan.org/~drolsky/",   debian => 'libdatetime-format-strptime-perl' },
   { name => "DBI",             version => '1.50',  url => "http://search.cpan.org/~timb/",      debian => 'libdbi-perl' },
   { name => "DBD::Pg",         version => '1.49',  url => "http://search.cpan.org/~dbdpg/",     debian => 'libdbd-pg-perl' },
+  { name => "Digest::SHA",                         url => "http://search.cpan.org/~mshelor/",   debian => 'libdigest-sha-perl' },
   { name => "Email::Address",                      url => "http://search.cpan.org/~rjbs/",      debian => 'libemail-address-perl' },
   { name => "Email::MIME",                         url => "http://search.cpan.org/~rjbs/",      debian => 'libemail-mime-perl' },
   { name => "FCGI",            version => '0.72',  url => "http://search.cpan.org/~mstrout/",   debian => 'libfcgi-perl' },
@@ -37,6 +38,7 @@ BEGIN {
   { name => "List::MoreUtils", version => '0.21',  url => "http://search.cpan.org/~vparseval/", debian => 'liblist-moreutils-perl' },
   { name => "List::UtilsBy",                       url => "http://search.cpan.org/~pevans/",    debian => 'liblist-utilsby-perl' },
   { name => "Params::Validate",                    url => "http://search.cpan.org/~drolsky/",   debian => 'libparams-validate-perl' },
+  { name => "PBKDF2::Tiny",    version => '0.005', url => "http://search.cpan.org/~arodland/", },
   { name => "PDF::API2",       version => '2.000', url => "http://search.cpan.org/~areibens/",  debian => 'libpdf-api2-perl' },
   { name => "Rose::Object",                        url => "http://search.cpan.org/~jsiracusa/", debian => 'librose-object-perl' },
   { name => "Rose::DB",                            url => "http://search.cpan.org/~jsiracusa/", debian => 'librose-db-perl' },
@@ -54,7 +56,6 @@ BEGIN {
 );
 
 @optional_modules = (
-  { name => "Digest::SHA",                         url => "http://search.cpan.org/~mshelor/",   debian => 'libdigest-sha-perl' },
   { name => "IO::Socket::SSL",                     url => "http://search.cpan.org/~sullr/",     debian => 'libio-socket-ssl-perl' },
   { name => "Net::LDAP",                           url => "http://search.cpan.org/~gbarr/",     debian => 'libnet-ldap-perl' },
   # Net::SMTP is core since 5.7.3
index 0a6310d..358ced0 100644 (file)
@@ -7,6 +7,14 @@ Wichtige Hinweise zum Upgrade von älteren Versionen
 Upgrade auf v?????
 ==================
 
+* Neue Perl-Modul-Abhängigkeiten:
+
+  * PBKDF2::Tiny
+
+  Wie immer bitte vor dem ersten Aufrufen einmal die Pakete überprüfen:
+
+  $ scripts/installation_check.pl -ro
+
 * Der in der Dokumentation beschriebene Mechanismus für die CGI-Anbindung
   (2.6.1 Grundkonfiguration mittels CGI) wurde geändert. Ein einfacher Alias
   auf das Programmverzeichnis funktioniert nicht mehr, und es muss immer ein
index c817281..d06bfb5 100644 (file)
@@ -55,6 +55,12 @@ Kleinere neue Features und Detailverbesserungen:
 
   - Besseren kivi-Adventssupport
 
+Sicherheit:
+
+  - Das sichere Passwort-Hash-Verfahren PBKDF2 wird nun unterstützt
+    und standardmäßig bei allen zukünftigen Passwortänderungen
+    benutzt.
+
 2015-08-20 - Release 3.3
 
 Größere neue Features:
diff --git a/modules/fallback/PBKDF2/Tiny.pm b/modules/fallback/PBKDF2/Tiny.pm
new file mode 100644 (file)
index 0000000..7172fe1
--- /dev/null
@@ -0,0 +1,376 @@
+use strict;
+use warnings;
+
+package PBKDF2::Tiny;
+# ABSTRACT: Minimalist PBKDF2 (RFC 2898) with HMAC-SHA1 or HMAC-SHA2
+
+our $VERSION = '0.005';
+
+use Carp ();
+use Exporter 5.57 qw/import/;
+
+our @EXPORT_OK = qw/derive derive_hex verify verify_hex hmac digest_fcn/;
+
+my ( $BACKEND, $LOAD_ERR );
+for my $mod (qw/Digest::SHA Digest::SHA::PurePerl/) {
+    $BACKEND = $mod, last if eval "require $mod; 1";
+    $LOAD_ERR ||= $@;
+}
+die $LOAD_ERR if !$BACKEND;
+
+#--------------------------------------------------------------------------#
+# constants and lookup tables
+#--------------------------------------------------------------------------#
+
+# function coderef placeholder, block size in bytes, digest size in bytes
+my %DIGEST_TYPES = (
+    'SHA-1'   => [ undef, 64,  20 ],
+    'SHA-224' => [ undef, 64,  28 ],
+    'SHA-256' => [ undef, 64,  32 ],
+    'SHA-384' => [ undef, 128, 48 ],
+    'SHA-512' => [ undef, 128, 64 ],
+);
+
+for my $type ( keys %DIGEST_TYPES ) {
+    no strict 'refs';
+    ( my $name = lc $type ) =~ s{-}{};
+    $DIGEST_TYPES{$type}[0] = \&{"$BACKEND\::$name"};
+}
+
+my %INT = map { $_ => pack( "N", $_ ) } 1 .. 16;
+
+#--------------------------------------------------------------------------#
+# public functions
+#--------------------------------------------------------------------------#
+
+#pod =func derive
+#pod
+#pod     $dk = derive( $type, $password, $salt, $iterations, $dk_length )
+#pod
+#pod The C<derive> function outputs a binary string with the derived key.
+#pod The first argument indicates the digest function to use.  It must be one
+#pod of: SHA-1, SHA-224, SHA-256, SHA-384, or SHA-512.
+#pod
+#pod If a password or salt are not provided, they default to the empty string, so
+#pod don't do that!  L<RFC 2898
+#pod recommends|https://tools.ietf.org/html/rfc2898#section-4.1> a random salt of at
+#pod least 8 octets.  If you need a cryptographically strong salt, consider
+#pod L<Crypt::URandom>.
+#pod
+#pod The password and salt should encoded as octet strings. If not (i.e. if
+#pod Perl's internal 'UTF8' flag is on), then an exception will be thrown.
+#pod
+#pod The number of iterations defaults to 1000 if not provided.  If the derived
+#pod key length is not provided, it defaults to the output size of the digest
+#pod function.
+#pod
+#pod =cut
+
+sub derive {
+    my ( $type, $passwd, $salt, $iterations, $dk_length ) = @_;
+
+    my ( $digester, $block_size, $digest_length ) = digest_fcn($type);
+
+    $passwd = '' unless defined $passwd;
+    $salt   = '' unless defined $salt;
+    $iterations ||= 1000;
+    $dk_length  ||= $digest_length;
+
+    # we insist on octet strings for password and salt
+    Carp::croak("password must be an octet string, not a character string")
+      if utf8::is_utf8($passwd);
+    Carp::croak("salt must be an octet string, not a character string")
+      if utf8::is_utf8($salt);
+
+    my $key = ( length($passwd) > $block_size ) ? $digester->($passwd) : $passwd;
+    my $passes = int( $dk_length / $digest_length );
+    $passes++ if $dk_length % $digest_length; # need part of an extra pass
+
+    my $dk = "";
+    for my $i ( 1 .. $passes ) {
+        $INT{$i} ||= pack( "N", $i );
+        my $digest = my $result =
+          "" . hmac( $salt . $INT{$i}, $key, $digester, $block_size );
+        for my $iter ( 2 .. $iterations ) {
+            $digest = hmac( $digest, $key, $digester, $block_size );
+            $result ^= $digest;
+        }
+        $dk .= $result;
+    }
+
+    return substr( $dk, 0, $dk_length );
+}
+
+#pod =func derive_hex
+#pod
+#pod Works just like L</derive> but outputs a hex string.
+#pod
+#pod =cut
+
+sub derive_hex { unpack( "H*", &derive ) }
+
+#pod =func verify
+#pod
+#pod     $bool = verify( $dk, $type, $password, $salt, $iterations, $dk_length );
+#pod
+#pod The C<verify> function checks that a given derived key (in binary form) matches
+#pod the password and other parameters provided using a constant-time comparison
+#pod function.
+#pod
+#pod The first parameter is the derived key to check.  The remaining parameters
+#pod are the same as for L</derive>.
+#pod
+#pod =cut
+
+sub verify {
+    my ( $dk1, @derive_args ) = @_;
+
+    my $dk2 = derive(@derive_args);
+
+    # shortcut if input dk is the wrong length entirely; this is not
+    # constant time, but this doesn't really give much away as
+    # the keys are of different types anyway
+
+    return unless length($dk1) == length($dk2);
+
+    # if lengths match, do constant time comparison to avoid timing attacks
+    my $match = 1;
+    for my $i ( 0 .. length($dk1) - 1 ) {
+        $match &= ( substr( $dk1, $i, 1 ) eq substr( $dk2, $i, 1 ) ) ? 1 : 0;
+    }
+
+    return $match;
+}
+
+#pod =func verify_hex
+#pod
+#pod Works just like L</verify> but the derived key must be a hex string (without a
+#pod leading "0x").
+#pod
+#pod =cut
+
+sub verify_hex {
+    my $dk = pack( "H*", shift );
+    return verify( $dk, @_ );
+}
+
+#pod =func digest_fcn
+#pod
+#pod     ($fcn, $block_size, $digest_length) = digest_fcn('SHA-1');
+#pod     $digest = $fcn->($data);
+#pod
+#pod This function is used internally by PBKDF2::Tiny, but made available in case
+#pod it's useful to someone.
+#pod
+#pod Given one of the valid digest types, it returns a function reference that
+#pod digests a string of data. It also returns block size and digest length for that
+#pod digest type.
+#pod
+#pod =cut
+
+sub digest_fcn {
+    my ($type) = @_;
+
+    Carp::croak("Digest function '$type' not supported")
+      unless exists $DIGEST_TYPES{$type};
+
+    return @{ $DIGEST_TYPES{$type} };
+}
+
+#pod =func hmac
+#pod
+#pod     $key = $digest_fcn->($key) if length($key) > $block_size;
+#pod     $hmac = hmac( $data, $key, $digest_fcn, $block_size );
+#pod
+#pod This function is used internally by PBKDF2::Tiny, but made available in case
+#pod it's useful to someone.
+#pod
+#pod The first two arguments are the data and key inputs to the HMAC function.  Both
+#pod should be encoded as octet strings, as underlying HMAC/digest functions may
+#pod croak or may give unexpected results if Perl's internal UTF-8 flag is on.
+#pod
+#pod B<Note>: if the key is longer than the digest block size, it must be
+#pod preprocessed using the digesting function.
+#pod
+#pod The third and fourth arguments must be a digesting code reference (from
+#pod L</digest_fcn>) and block size.
+#pod
+#pod =cut
+
+# hmac function adapted from Digest::HMAC by Graham Barr and Gisle Aas.
+# Compared to that implementation, this *requires* a preprocessed
+# key and block size, which makes iterative hmac slightly more efficient.
+sub hmac {
+    my ( $data, $key, $digest_func, $block_size ) = @_;
+
+    my $k_ipad = $key ^ ( chr(0x36) x $block_size );
+    my $k_opad = $key ^ ( chr(0x5c) x $block_size );
+
+    &$digest_func( $k_opad, &$digest_func( $k_ipad, $data ) );
+}
+
+1;
+
+
+# vim: ts=4 sts=4 sw=4 et:
+
+__END__
+
+=pod
+
+=encoding UTF-8
+
+=head1 NAME
+
+PBKDF2::Tiny - Minimalist PBKDF2 (RFC 2898) with HMAC-SHA1 or HMAC-SHA2
+
+=head1 VERSION
+
+version 0.005
+
+=head1 SYNOPSIS
+
+    use PBKDF2::Tiny qw/derive verify/;
+
+    my $dk = derive( 'SHA-1', $pass, $salt, $iters );
+
+    if ( verify( $dk, 'SHA-1', $pass, $salt, $iters ) ) {
+        # password is correct
+    }
+
+=head1 DESCRIPTION
+
+This module provides an L<RFC 2898|https://tools.ietf.org/html/rfc2898>
+compliant PBKDF2 implementation using HMAC-SHA1 or HMAC-SHA2 in under 100 lines
+of code.  If you are using Perl 5.10 or later, it uses only core Perl modules.
+If you are on an earlier version of Perl, you need L<Digest::SHA> or
+L<Digest::SHA::PurePerl>.
+
+All documented functions are optionally exported.  No functions are exported by default.
+
+=head1 FUNCTIONS
+
+=head2 derive
+
+    $dk = derive( $type, $password, $salt, $iterations, $dk_length )
+
+The C<derive> function outputs a binary string with the derived key.
+The first argument indicates the digest function to use.  It must be one
+of: SHA-1, SHA-224, SHA-256, SHA-384, or SHA-512.
+
+If a password or salt are not provided, they default to the empty string, so
+don't do that!  L<RFC 2898
+recommends|https://tools.ietf.org/html/rfc2898#section-4.1> a random salt of at
+least 8 octets.  If you need a cryptographically strong salt, consider
+L<Crypt::URandom>.
+
+The password and salt should encoded as octet strings. If not (i.e. if
+Perl's internal 'UTF8' flag is on), then an exception will be thrown.
+
+The number of iterations defaults to 1000 if not provided.  If the derived
+key length is not provided, it defaults to the output size of the digest
+function.
+
+=head2 derive_hex
+
+Works just like L</derive> but outputs a hex string.
+
+=head2 verify
+
+    $bool = verify( $dk, $type, $password, $salt, $iterations, $dk_length );
+
+The C<verify> function checks that a given derived key (in binary form) matches
+the password and other parameters provided using a constant-time comparison
+function.
+
+The first parameter is the derived key to check.  The remaining parameters
+are the same as for L</derive>.
+
+=head2 verify_hex
+
+Works just like L</verify> but the derived key must be a hex string (without a
+leading "0x").
+
+=head2 digest_fcn
+
+    ($fcn, $block_size, $digest_length) = digest_fcn('SHA-1');
+    $digest = $fcn->($data);
+
+This function is used internally by PBKDF2::Tiny, but made available in case
+it's useful to someone.
+
+Given one of the valid digest types, it returns a function reference that
+digests a string of data. It also returns block size and digest length for that
+digest type.
+
+=head2 hmac
+
+    $key = $digest_fcn->($key) if length($key) > $block_size;
+    $hmac = hmac( $data, $key, $digest_fcn, $block_size );
+
+This function is used internally by PBKDF2::Tiny, but made available in case
+it's useful to someone.
+
+The first two arguments are the data and key inputs to the HMAC function.  Both
+should be encoded as octet strings, as underlying HMAC/digest functions may
+croak or may give unexpected results if Perl's internal UTF-8 flag is on.
+
+B<Note>: if the key is longer than the digest block size, it must be
+preprocessed using the digesting function.
+
+The third and fourth arguments must be a digesting code reference (from
+L</digest_fcn>) and block size.
+
+=begin Pod::Coverage
+
+
+
+
+=end Pod::Coverage
+
+=head1 SEE ALSO
+
+=over 4
+
+=item *
+
+L<Crypt::PBKDF2>
+
+=item *
+
+L<Digest::PBDKF2>
+
+=back
+
+=for :stopwords cpan testmatrix url annocpan anno bugtracker rt cpants kwalitee diff irc mailto metadata placeholders metacpan
+
+=head1 SUPPORT
+
+=head2 Bugs / Feature Requests
+
+Please report any bugs or feature requests through the issue tracker
+at L<https://github.com/dagolden/PBKDF2-Tiny/issues>.
+You will be notified automatically of any progress on your issue.
+
+=head2 Source Code
+
+This is open source software.  The code repository is available for
+public review and contribution under the terms of the license.
+
+L<https://github.com/dagolden/PBKDF2-Tiny>
+
+  git clone https://github.com/dagolden/PBKDF2-Tiny.git
+
+=head1 AUTHOR
+
+David Golden <dagolden@cpan.org>
+
+=head1 COPYRIGHT AND LICENSE
+
+This software is Copyright (c) 2014 by David Golden.
+
+This is free software, licensed under:
+
+  The Apache License, Version 2.0, January 2004
+
+=cut