Keep in mind that using bcrypt alone creates a limit on the maximum
length of passwords[0]. I recommend you hash the password itself (using
SHA-256 or even HMAC-SHA-256 with a pepper) and then pass the SHA hash
to bcrypt as the ‘password’ input. That way, your users can have
passwords of whatever length they want, and you’re not dropping bits on
the floor.
Also, bcrypt cost of 8 is not great at all (no idea why that is a
default, as it’s pretty terrible[1]). I recommend you use at least 12.
It would also be nice to have the cost be a setting somewhere and have
it “upgrade” a person’s password hash on log in if their old hash used a
different cost than what the current default is (since a bcrypt hash
will include the cost factor that was used to create it originally).
As a side note, have you looked at scrypt[2] yet? It’s still fairly new,
but it adds a memory requirement to hash generation, making it even
less susceptible to offline brute-force attacks.
~reed
[0]
[1] http://www.perlmonks.org/?node_id=975703
[2] https://www.tarsnap.com/scrypt.htmlOn Tue, 3 Sep 2013 02:02:10 -0400 (EDT) alexmv@bestpractical.com (Alex Vandiver) wrote:
The branch, 4.2/bcrypt-passwords has been created
at 07ac7c51167a9427a2857fd4a09671ed8b9cab9c (commit)
Log -----------------------------------------------------------------
commit 07ac7c51167a9427a2857fd4a09671ed8b9cab9c
Author: Alex Vandiver alexmv@bestpractical.com
Date: Thu Aug 22 17:59:25 2013 -0400Switch to Blowfish-based bcrypt for password hashing
A SHA-512 with a 16-character salt, drawn from 64 possible characters,
yields 2^96 possible salts. While this makes rainbow tables unrealistic
given modern hardware (the failure mode of RT 3.8’s MD5 hashing), it
does very little to deter against offline brute force attacks on the
database.Specifically, given the complete hashed password and salt from the
database, a dictionary of weak passwords can be hashed with the stored
salt to attempt to find matches. Given that a single round of the
SHA-512 hash is not designed to be computationally expensive, possible
passwords may be hashed and checked very quickly.The bcrypt hashing function is designed to be computationally expensive
to mitigate these types of attacks. For instance, on a development
laptop:Rate bcrypt sha-512 bcrypt 13.3/s -- -100% sha-512 18183/s 136934% --
That is, bcrypt is three orders of magnitude slower to compute, thus
notably increasing the computational cost of brute-forcing passwords.
bcrypt also includes a tuning parameter, the number of “rounds” to run,
which allows the same algorithm to be increase the computational cost
required as computers continue to grow faster. We use the standard
value of 8 here, but allow for higher values to be used later.diff --git a/docs/UPGRADING-4.2 b/docs/UPGRADING-4.2
index b7e2015…00b4b74 100644
— a/docs/UPGRADING-4.2
+++ b/docs/UPGRADING-4.2
@@ -261,6 +261,14 @@ deprecation warnings. The old names, and their new counterparts, are:
Due to many long-standing bugs and limitations, the “Offline Tool” was
removed.+=item *
+
+To increase security againt offline brute-force attacks, RT’s default
+password encryption has been switched to the popular bcrypt() key
+derivation function. Passwords cannot be automatically bulk upgraded to
+the new format, but will be replaced with bcrypt versions upon the first
+successful login.
+
=back=cut
diff --git a/lib/RT/User.pm b/lib/RT/User.pm
index 152981a…3e4c2de 100644
— a/lib/RT/User.pm
+++ b/lib/RT/User.pm
@@ -79,6 +79,7 @@ sub Table {‘Users’}use Digest::SHA;
use Digest::MD5;
+use Crypt::Eksblowfish::Bcrypt qw();
use RT::Principals;
use RT::ACE;
use RT::Interface::Email;
@@ -870,6 +871,40 @@ sub SetPassword {}
+sub _GeneratePassword_bcrypt {
- my $self = shift;
- my ($password, @rest) = @_;
- my $salt;
- my $rounds;
- if (@rest) {
# The first split is the number of rounds
$rounds = $rest[0];
# The salt is the first 22 characters, b64 encoded usign the
# special bcrypt base64.
$salt = Crypt::Eksblowfish::Bcrypt::de_base64( substr($rest[1], 0, 22) );
- } else {
# The current standard is 8 rounds
$rounds = 8;
# Generate a random 16-octet base64 salt
$salt = "";
$salt .= pack("C", int rand(256)) for 1..16;
- }
- my $hash = Crypt::Eksblowfish::Bcrypt::bcrypt_hash({
key_nul => 1,
cost => $rounds,
salt => $salt,
- }, encode_utf8($password) );
- return join(“!”, “”, “bcrypt”, sprintf(“%02d”, $rounds),
Crypt::Eksblowfish::Bcrypt::en_base64( $salt ).
Crypt::Eksblowfish::Bcrypt::en_base64( $hash )
);
+}
+
sub GeneratePassword_sha512 {
my $self = shift;
my ($password, $salt) = @;
@@ -893,13 +928,13 @@ Returns a string to store in the database. This string takes the form:!method!salt!hash
-By default, the method is currently C.
+By default, the method is currently C.=cut
sub _GeneratePassword {
my $self = shift;
- return $self->GeneratePassword_sha512(@);
- return $self->GeneratePassword_bcrypt(@);
}=head3 HasPassword
@@ -948,9 +983,11 @@ sub IsPassword {
my $stored = $self->__Value(‘Password’);
if ($stored =~ /^!/) {
# If it’s a new-style (>= RT 4.0) password, it starts with a ‘!’
my (undef, $method, $salt, undef) = split /!/, $stored;
if ($method eq "sha512") {
return $self->_GeneratePassword_sha512($value, $salt) eq $stored;
my (undef, $method, @rest) = split /!/, $stored;
if ($method eq "bcrypt") {
return $self->_GeneratePassword_bcrypt($value, @rest) eq $stored;
} elsif ($method eq "sha512") {
return 0 unless $self->_GeneratePassword_sha512($value, @rest) eq $stored; } else { $RT::Logger->warn("Unknown hash method $method"); return 0;
diff --git a/sbin/rt-test-dependencies.in b/sbin/rt-test-dependencies.in
index bf9b690…57c2797 100644
— a/sbin/rt-test-dependencies.in
+++ b/sbin/rt-test-dependencies.in
@@ -179,6 +179,7 @@ CGI::Cookie 1.20
CGI::Emulate::PSGI
CGI::PSGI 0.12
Class::Accessor 0.34
+Crypt::Eksblowfish
CSS::Squish 0.06
Date::Extract 0.02
Date::Manip
diff --git a/t/api/password-types.t b/t/api/password-types.t
index e5155e3…e73bfe6 100644
— a/t/api/password-types.t
+++ b/t/api/password-types.t
@@ -4,17 +4,22 @@ use warnings;
use RT::Test;
use Digest::MD5;-my $default = “sha512”;
+my $default = “bcrypt”;my $root = RT::User->new(RT->SystemUser);
$root->Load(“root”);-# Salted SHA-512 (default)
+# bcrypt (default)
my $old = $root->__Value(“Password”);
like($old, qr/^!$default!/, “Stored as salted $default”);
ok($root->IsPassword(“password”));
is($root->__Value(“Password”), $old, “Unchanged after password check”);+# Salted SHA-512, one round
+$root->_Set( Field => “Password”, Value => RT::User->_GeneratePassword_sha512(“other”, “salt”) );
+ok($root->IsPassword(“other”), “SHA-512 password works”);
+like($root->__Value(“Password”), qr/^!$default!/, “And is now upgraded to salted $default”);
+Crypt
$root->_Set( Field => “Password”, Value => crypt(“something”, “salt”));
ok($root->IsPassword(“something”), “crypt()ed password works”);
Rt-commit mailing list
Rt-commit@lists.bestpractical.com
rt-commit Info Page