From: Moritz Bunkus Date: Mon, 11 Jan 2016 10:55:53 +0000 (+0100) Subject: Passwörter: Hash-Verfahren PBKDF2 unterstützen und als Standard nutzen X-Git-Tag: release-3.4.1~472 X-Git-Url: http://wagnertech.de/git?a=commitdiff_plain;h=c157c911ac9175e4838990d051c594dd5eec7e21;p=kivitendo-erp.git Passwörter: Hash-Verfahren PBKDF2 unterstützen und als Standard nutzen 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. --- diff --git a/SL/Auth.pm b/SL/Auth.pm index 788154c14..9f2475fd8 100644 --- a/SL/Auth.pm +++ b/SL/Auth.pm @@ -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); diff --git a/SL/Auth/DB.pm b/SL/Auth/DB.pm index 0bbc05016..93e5cc0b3 100644 --- a/SL/Auth/DB.pm +++ b/SL/Auth/DB.pm @@ -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 { diff --git a/SL/Auth/Password.pm b/SL/Auth/Password.pm index 5ae75eac5..d566cf0f1 100644 --- a/SL/Auth/Password.pm +++ b/SL/Auth/Password.pm @@ -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}; } diff --git a/SL/InstallationCheck.pm b/SL/InstallationCheck.pm index 4d46fd6a4..9882e0eca 100644 --- a/SL/InstallationCheck.pm +++ b/SL/InstallationCheck.pm @@ -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 diff --git a/doc/UPGRADE b/doc/UPGRADE index 0a6310d34..358ced016 100644 --- a/doc/UPGRADE +++ b/doc/UPGRADE @@ -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 diff --git a/doc/changelog b/doc/changelog index c8172818c..d06bfb5fd 100644 --- a/doc/changelog +++ b/doc/changelog @@ -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 index 000000000..7172fe1fe --- /dev/null +++ b/modules/fallback/PBKDF2/Tiny.pm @@ -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 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 a random salt of at +#pod least 8 octets. If you need a cryptographically strong salt, consider +#pod L. +#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 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 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. +#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 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: 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) 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 +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 or +L. + +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 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 a random salt of at +least 8 octets. If you need a cryptographically strong salt, consider +L. + +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 but outputs a hex string. + +=head2 verify + + $bool = verify( $dk, $type, $password, $salt, $iterations, $dk_length ); + +The C 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. + +=head2 verify_hex + +Works just like L 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: 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) and block size. + +=begin Pod::Coverage + + + + +=end Pod::Coverage + +=head1 SEE ALSO + +=over 4 + +=item * + +L + +=item * + +L + +=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. +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 + + git clone https://github.com/dagolden/PBKDF2-Tiny.git + +=head1 AUTHOR + +David Golden + +=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