Unverified Commit c311ab57 authored by IKEDA Soji's avatar IKEDA Soji Committed by GitHub
Browse files

Merge pull request #225 from mpkut/feature_bcrypt_hash

Feature: support for Bcrypt password hashes by mpkut
parents a6ec3731 d7ca3ba7
......@@ -42,7 +42,9 @@ die 'Error in configuration'
my $sdm = Sympa::DatabaseManager->instance
or die 'Can\'t connect to database';
print "Recoding password using MD5 fingerprint.\n";
my $password_hash = Conf::get_robot_conf('*', 'password_hash');
print "Recoding password using $password_hash fingerprint.\n";
my $sth = $sdm->do_query(q{SELECT email_user, password_user from user_table});
unless ($sth) {
......@@ -51,6 +53,7 @@ unless ($sth) {
my $total = 0;
my $total_md5 = 0;
my $total_bcrypt = 0;
while (my $user = $sth->fetchrow_hashref('NAME_lc')) {
my $clear_password;
......@@ -67,6 +70,13 @@ while (my $user = $sth->fetchrow_hashref('NAME_lc')) {
next;
}
if ($user->{'password_user'} =~ /^\$2a\$/) {
printf "Password from %s already encoded as bcrypt fingerprint\n",
$user->{'email_user'};
$total_bcrypt++;
next;
}
if ($user->{'password_user'} =~ /^crypt.(.*)$/) {
$clear_password = Sympa::Tools::Password::decrypt_password(
$user->{'password_user'});
......@@ -82,7 +92,7 @@ while (my $user = $sth->fetchrow_hashref('NAME_lc')) {
q{UPDATE user_table
SET password_user = ?
WHERE email_user = ?},
Sympa::User::password_fingerprint($clear_password),
Sympa::User::password_fingerprint($clear_password, undef),
$user->{'email_user'}
)
) {
......@@ -92,15 +102,15 @@ while (my $user = $sth->fetchrow_hashref('NAME_lc')) {
$sth->finish();
printf
"Updating password storage in table user_table using md5 for %d users.\n",
"Updating password storage in table user_table using $password_hash hashes for %d users.\n",
$total;
if ($total_md5) {
if ($total_md5 || $total_bcrypt) {
printf
"Found in table user %d password stored using md5, did you run Sympa before upgrading ?\n",
$total_md5;
"Found in table user %d password stored using md5, %d using bcrypt. Did you run Sympa before upgrading ?\n",
$total_md5, $total_bcrypt;
}
printf "Total password re-encoded using md5: %d\n", $total;
printf "Total passwords re-encoded using $password_hash: %d\n", $total;
exit 0;
......@@ -115,11 +125,13 @@ Upgrading password in database
=head1 DESCRIPTION
Version later than 5.4 uses MD5 hash instead of
Versions later than 5.4 uses MD5 hash instead of
symmetric encryption to store password.
This require to rewrite password in database. This upgrade IS NOT
REVERSIBLE.
Versions later than 6.2.26 support bcrypt.
This upgrade requires to rewriting user password entries in the database.
This upgrade IS NOT REVERSIBLE.
=head1 HISTORY
......@@ -128,4 +140,6 @@ form by reversible RC4.
Sympa 5.4 or later uses MD5 one-way hash function to encode user passwords.
Sympa 6.2.26 or later has optional support for bcrypt.
=cut
......@@ -147,9 +147,11 @@ sub authentication {
## the user passwords
## Other backends are Single Sign-On solutions
if ($auth_service->{'auth_type'} eq 'user_table') {
my $fingerprint = Sympa::User::password_fingerprint($pwd);
my $fingerprint =
Sympa::User::password_fingerprint($pwd, $user->{'password'});
if ($fingerprint eq $user->{'password'}) {
Sympa::User::update_password_hash($user, $pwd);
Sympa::User::update_global_user($email,
{wrong_login_count => 0});
return {
......
......@@ -1639,6 +1639,30 @@ our @params = (
'gettext_comment' =>
"\"insensitive\" or \"sensitive\".\nIf set to \"insensitive\", WWSympa's password check will be insensitive. This only concerns passwords stored in the Sympa database, not the ones in LDAP.\nShould not be changed! May invalid all user password.",
},
{ 'name' => 'password_hash',
'default' => 'md5',
'gettext_id' => 'Password hashing algorithm',
'file' => 'wwsympa.conf',
#vhost => '1', # per-robot config is impossible.
'gettext_comment' =>
"\"md5\" or \"bcrypt\".\nIf set to \"md5\", Sympa will use MD5 password hashes. If set to \"bcrypt\", bcrypt hashes will be used instead. This only concerns passwords stored in the Sympa database, not the ones in LDAP.\nShould not be changed! May invalid all user passwords.",
},
{ 'name' => 'password_hash_update',
'default' => '1',
'gettext_id' => 'Update password hashing algorithm when users log in',
'file' => 'wwsympa.conf',
#vhost => '1', # per-robot config is impossible.
'gettext_comment' =>
"On successful login, update the encrypted user password to use the algorithm specified by \"password_hash\". This allows for a graceful transition to a new password hash algorithm. A value of 0 disables updating of existing password hashes. New and reset passwords will use the \"password_hash\" setting in all cases.",
},
{ 'name' => 'bcrypt_cost',
'default' => '12',
'gettext_id' => 'Bcrypt hash cost',
'file' => 'wwsympa.conf',
#vhost => '1', # per-robot config is impossible.
'gettext_comment' =>
"When \"password_hash\" is set to \"bcrypt\", this sets the \"cost\" parameter of the bcrypt hash function. The default of 12 is expected to require approximately 250ms to calculate the password hash on a 3.2GHz CPU. This only concerns passwords stored in the Sympa database, not the ones in LDAP.\nCan be changed but any new cost setting will only apply to new passwords.",
},
# One time ticket
......
......@@ -190,7 +190,7 @@ my %full_db_struct = (
'order' => 3,
},
'password_user' => {
'struct' => 'varchar(40)',
'struct' => 'varchar(64)',
'doc' => 'password are stored as finger print',
'order' => 2,
},
......
......@@ -77,6 +77,12 @@ our %cpan_modules = (
'gettext_id' =>
'this module provides reversible encryption of user passwords in the database. Useful when updating from old version with password reversible encryption, or if secure session cookies in non-SSL environments are required.',
},
'Crypt::Eksblowfish' => {
required_version => '0.009',
package_name => 'Crypt-Eksblowfish',
'gettext_id' =>
'used to encrypt passwords with the Bcrypt hash algorithm',
},
'Crypt::OpenSSL::X509' => {
required_version => '1.800.1',
package_name => 'Crypt-OpenSSL-X509',
......
......@@ -28,6 +28,7 @@ use strict;
use warnings;
use Carp qw();
use Digest::MD5;
BEGIN { eval 'use Crypt::Eksblowfish::Bcrypt qw(bcrypt en_base64)'; }
use Conf;
use Sympa::DatabaseDescription;
......@@ -292,17 +293,137 @@ Returns the password finger print.
=cut
# Old name: Sympa::Auth::password_fingerprint().
# Note: This proc may allow future replacement of md5 by sha1 or ...
#
# Password fingerprint functions are stored in a table. Currently supported
# algorithms are the default 'md5', and 'bcrypt'.
#
# If the algorithm uses a salt (e.g. bcrypt) and the second parameter $salt
# is not provided, a random one will be generated.
#
my %fingerprint_hashes = (
# default is to use MD5, which does not use a salt
'md5' => sub {
my ($pwd, $salt) = @_;
# salt parameter is not used for MD5 hashes
my $fingerprint = Digest::MD5::md5_hex($pwd);
my $match = ($fingerprint eq $salt) ? "yes" : "no";
$log->syslog('debug', "md5: match $match salt \"$salt\" fingerprint $fingerprint");
return $fingerprint;
},
# bcrypt uses a salt and has a configurable "cost" parameter
'bcrypt' => sub {
my ($pwd, $salt) = @_;
die "bcrypt support unavailable: install Crypt::Eksblowfish::Bcrypt"
unless $Crypt::Eksblowfish::Bcrypt::VERSION;
# A bcrypt-encrypted password contains the settings at the front.
# If this not look like a settings string, create one.
unless ($salt =~ m#\A\$2(a?)\$([0-9]{2})\$([./A-Za-z0-9]{22})#x) {
my $bcrypt_cost = Conf::get_robot_conf('*', 'bcrypt_cost');
my $cost = sprintf("%02d", 0 + $bcrypt_cost);
my $newsalt = "";
for my $i (0..15) {
$newsalt .= chr(rand(256));
}
$newsalt = '$2a$' . $cost . '$' . en_base64($newsalt);
$log->syslog('debug', "bcrypt: create new salt: cost $cost salt \"$salt\" salt \"$newsalt\"");
$salt = $newsalt;
}
my $fingerprint = bcrypt($pwd, $salt);
my $match = ($fingerprint eq $salt) ? "yes" : "no";
$log->syslog('debug', "bcrypt: match $match salt $salt fingerprint $fingerprint");
return $fingerprint;
}
);
sub password_fingerprint {
$log->syslog('debug', '');
my ($pwd, $salt) = @_;
my $password_hash;
my $hash_type;
$log->syslog('debug', "salt \"$salt\"");
my $pwd = shift;
if (Conf::get_robot_conf('*', 'password_case') eq 'insensitive') {
return Digest::MD5::md5_hex(lc $pwd);
} else {
return Digest::MD5::md5_hex($pwd);
$pwd = lc($pwd);
}
# preserve the hash type if we can determine it, else use system default
if (defined($salt) && defined($hash_type = hash_type($salt))) {
$password_hash = $hash_type;
} else {
$password_hash = Conf::get_robot_conf('*', 'password_hash');
}
$log->syslog('debug', "hash_type \"$hash_type\", password_hash = \"$password_hash\"");
die "password_fingerprint: unknown password_hash \"$password_hash\""
unless defined($fingerprint_hashes{$password_hash});
return $fingerprint_hashes{$password_hash}->($pwd, $salt);
}
=over 4
=item hash_type ( )
detect the type of password fingerprint used for a hashed password
Returns undef if no supported hash type is detected
=back
=cut
sub hash_type {
my $hash = shift;
return 'md5' if ($hash =~ /^[a-f0-9]{32}$/i);
return 'bcrypt' if ($hash =~ m#\A\$2(a?)\$([0-9]{2})\$([./A-Za-z0-9]{22})#);
return undef;
}
=over 4
=item update_password_hash ( )
If needed, update the hash used for the user's encrypted password entry
=back
=cut
sub update_password_hash {
my ($user, $pwd) = @_;
return unless (Conf::get_robot_conf('*', 'password_hash_update'));
# here if configured to check and update the password hash algorithm
my $user_hash = hash_type($user->{'password'});
my $system_hash = Conf::get_robot_conf('*', 'password_hash');
return if (defined($user_hash) && ($user_hash eq $system_hash));
# note that we directly use the callback for the hash type
# instead of using any other logic to determine which to call
$log->syslog('debug', 'update password hash for %s from %s to %s',
$user->{'email'}, $user_hash, $system_hash);
# note that we use the cleartext password here, not the hash
update_global_user($user->{'email'}, {password => $pwd});
}
############################################################################
......@@ -508,9 +629,10 @@ sub update_global_user {
$who = Sympa::Tools::Text::canonic_email($who);
## use md5 fingerprint to store password
## use hash fingerprint to store password
## hashes that use salts will randomly generate one
$values->{'password'} =
Sympa::User::password_fingerprint($values->{'password'})
Sympa::User::password_fingerprint($values->{'password'}, undef)
if ($values->{'password'});
## Canonicalize lang if possible.
......@@ -587,9 +709,10 @@ sub add_global_user {
my ($field, $value);
## encrypt password
## encrypt password with the configured password hash algorithm
## an salt of 'undef' means generate a new random one
$values->{'password'} =
Sympa::User::password_fingerprint($values->{'password'})
Sympa::User::password_fingerprint($values->{'password'}, undef)
if ($values->{'password'});
## Canonicalize lang if possible
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment