Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: support for Bcrypt password hashes #225

Merged
merged 4 commits into from
Mar 12, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions src/bin/upgrade_sympa_password.pl.in
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
Expand All @@ -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'});
Expand All @@ -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'}
)
) {
Expand All @@ -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;

Expand All @@ -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

Expand All @@ -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
4 changes: 3 additions & 1 deletion src/lib/Sympa/Auth.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
24 changes: 24 additions & 0 deletions src/lib/Sympa/ConfDef.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion src/lib/Sympa/DatabaseDescription.pm
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ my %full_db_struct = (
'order' => 3,
},
'password_user' => {
'struct' => 'varchar(40)',
'struct' => 'varchar(64)',
'doc' => 'password are stored as finger print',
'order' => 2,
},
Expand Down
6 changes: 6 additions & 0 deletions src/lib/Sympa/ModDef.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
143 changes: 133 additions & 10 deletions src/lib/Sympa/User.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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});

}

############################################################################
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down