diff --git a/Makefile.PL b/Makefile.PL index 6418f73d4..e52a785df 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -8,6 +8,7 @@ requires( 'Apache::Request' => 0, 'AppConfig' => 0, 'Clone' => 0, +'Crypt::Eksblowfish::Bcrypt' => 0, 'DBI' => 0, 'DBD::Pg' => 1.22, 'Data::ICal' => '0.16', # Data::ICal::Entry::Event diff --git a/lib/Act/Auth.pm b/lib/Act/Auth.pm index e2d6b339c..66ceecd9e 100644 --- a/lib/Act/Auth.pm +++ b/lib/Act/Auth.pm @@ -3,7 +3,6 @@ package Act::Auth; use strict; use Apache::AuthCookie; use Apache::Constants qw(OK); -use Digest::MD5 (); use Act::Config; use Act::User; @@ -55,9 +54,7 @@ sub authen_cred ($$\@) $user or do { $r->log_error("$prefix Unknown user"); return undef; }; # compare passwords - my $digest = Digest::MD5->new; - $digest->add(lc $sent_pw); - $digest->b64digest() eq $user->{passwd} + Act::Util::verify_password(lc $sent_pw, $user->{passwd}) or do { $r->log_error("$prefix Bad password"); return undef; }; # user is authenticated - create a session diff --git a/lib/Act/Handler/User/ChangePassword.pm b/lib/Act/Handler/User/ChangePassword.pm index 94fb296de..7d598c6c7 100644 --- a/lib/Act/Handler/User/ChangePassword.pm +++ b/lib/Act/Handler/User/ChangePassword.pm @@ -9,7 +9,6 @@ use Act::Template::HTML; use Act::User; use Act::Util; use Act::TwoStep; -use Digest::MD5 (); my $form = Act::Form->new( required => [qw(newpassword1 newpassword2)], @@ -64,9 +63,7 @@ sub handler my ($token, $token_data); if ($Request{user}) { # # compare passwords - my $digest = Digest::MD5->new; - $digest->add(lc $fields->{oldpassword}); - $digest->b64digest() eq $Request{user}{passwd} + Act::Util::verify_password(lc $fields->{oldpassword}, $Request{user}{passwd}) or do { $ok = 0; $form->{invalid}{oldpassword} = 1; }; } else { # must have a valid twostep token if not logged in @@ -91,7 +88,7 @@ sub handler } # update user $Request{user}->update( - passwd => Act::Util::crypt_password( $fields->{newpassword1} ) + passwd => Act::Util::crypt_password( $fields->{newpassword1}, Act::Util::gen_salt() ) ); # redirect to user's main page diff --git a/lib/Act/Util.pm b/lib/Act/Util.pm index 0f4069a03..d6a541745 100644 --- a/lib/Act/Util.pm +++ b/lib/Act/Util.pm @@ -7,6 +7,7 @@ use Apache::AuthCookie; use DateTime::Format::Pg; use DBI; use Digest::MD5 (); +use Crypt::Eksblowfish::Bcrypt qw(bcrypt en_base64); use Unicode::Normalize (); use URI::Escape (); @@ -148,15 +149,47 @@ sub gen_password { my $clear_passwd = $pass[ rand @pass ]; $clear_passwd =~ s/([vc])/$grams{$1}[rand@{$grams{$1}}]/g; - return ($clear_passwd, crypt_password( $clear_passwd )); + return ($clear_passwd, crypt_password( $clear_passwd, gen_salt() )); +} + +sub gen_salt +{ + # bcrypt cost is between 1 and 31 + my $cost = 10; + + # salt must be 16 bytes long at most. + my $salt; + $salt .= ('.', '/', 0..9, 'A'..'Z', 'a'..'z')[rand 64] for 1..16; + + # bcrypt uses a non-standard base64 encoding and cost is padded + return join('$', '', '2a', sprintf("%02d", $cost), en_base64($salt)); } sub crypt_password { - my $digest = Digest::MD5->new; - $digest->add(shift); - return $digest->b64digest(); + my ($plaintext, $salt) = @_; + + # crypt using MD5 digest for old passwords + if (!defined $salt || $salt !~ /^\$\d/) { + my $digest = Digest::MD5->new; + $digest->add($plaintext); + return $digest->b64digest(); + } + + # Eksblowfish + return bcrypt($plaintext, $salt) + if $salt =~ /^\$2a?\$\d+\$/; + + # crypt(3) + return crypt($plaintext, $salt); } + +sub verify_password +{ + my ($clear_passwd, $crypt_passwd) = @_; + return crypt_password( $clear_passwd, $crypt_passwd ) eq $crypt_passwd; +} + sub create_session { my $user = shift; diff --git a/t/05util.t b/t/05util.t index d19c80cd8..3713d1055 100644 --- a/t/05util.t +++ b/t/05util.t @@ -5,7 +5,7 @@ use utf8; use DateTime; use Test::MockObject; use constant NBPASS => 100; -use Test::More tests => 79 + 5 * NBPASS; +use Test::More tests => 80 + (6+2) * NBPASS; use Act::Config; BEGIN { use_ok('Act::Util') } @@ -62,7 +62,7 @@ while (my ($u, $args, $expected) = splice(@t, 0, 3)) { is(self_uri(%$args), $expected); } -# gen_password +# gen_password and verify_password my %seen; for (1..NBPASS) { my ($clear, $crypted) = Act::Util::gen_password(); @@ -70,8 +70,21 @@ for (1..NBPASS) { ok(!$seen{$clear}++); ok($crypted); like($clear, qr/^[a-z]+$/); - like($crypted, qr/^\S+$/); + like($crypted, qr/^\$\S+\$\S+$/); + ok( Act::Util::verify_password($clear, $crypted) ); } + +# gen_salt +%seen = (); +for (1..NBPASS) { + my $salt = Act::Util::gen_salt(); + ok($salt); + ok(!$seen{$salt}++); +} + +# verify_password and crypt_password without salt +ok( Act::Util::verify_password('f00bar', Act::Util::crypt_password('f00bar')) ); + # date_format use utf8; $Request{language} = 'fr';