Rt branch, 4.2/bcrypt-passwords, created. rt-4.1.19-32-g07ac7c5

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 -0400

    Switch 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

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.

Thanks for the heads-up on this; we were unaware of the maximum key
length on the bcrypt input. We’ll push a change to sha256 the input
prior to passing it to bcrypt.

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.

There is no one correct number; the speed of the server, the persistence
of the attacker, and the expected entropy of the passwords must be
factored into account – see [0] for a good discussion of this. In
testing, a cost factor of 8 on modest hardware yields a time of ~26ms
per hash, which is above the recommended hashing time. A cost factor of
12 on modest hardware takes nearly half a second to hash, which is a
notable time delay upon login, and would visibly impact responsiveness
of the application.
No cost factor will cover for low-entropy passwords, which in our
experience are unfortunately common in many deployments. A cost factor
of 8 is thus a compromise. You are of course welcome to increase the
cost factor locally if you deem it appropriate for your data, and your
hardware is capable of bearing the computational cost. We expect to
raise the default in core RT every ~two years to follow hardware trends.

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).

Incrementally adjusting the cost factor is indeed planned, but the
functionality to do so will be rolled into the first time we need to
adjust the number of rounds.

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.

We’re aware of it, but didn’t feel that it was mature enough to move to
using it, nor that its use was particularly strongly indicated.

  • Alex

[0] cryptography - Recommended # of iterations when using PBKDF2-SHA256? - Information Security Stack Exchange

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.

There is no one correct number; the speed of the server, the persistence
of the attacker, and the expected entropy of the passwords must be
factored into account – see [0] for a good discussion of this. In
testing, a cost factor of 8 on modest hardware yields a time of ~26ms
per hash, which is above the recommended hashing time. A cost factor of
12 on modest hardware takes nearly half a second to hash, which is a
notable time delay upon login, and would visibly impact responsiveness
of the application.

Note that Crypt::Eksblowfish::Bcrypt doesn’t actually use 8 by
default (just mentions it in the POD example). I did a quick check to
see what other major languages were using (as their actual defaults),
and they use at least 10 for the cost.

bcrypt-ruby - 10
(http://bcrypt-ruby.rubyforge.org/classes/BCrypt/Engine.html)
py-bcrypt - 12 (py-bcrypt - strong password hashing for Python)
PHP password_hash() - 10 (PHP: password_hash - Manual)
bcrypt.go - 10
(https://code.google.com/p/go/source/browse/bcrypt/bcrypt.go?repo=crypto)
jBCrypt - 10 (jBCrypt - strong password hashing for Java)
BCrypt.Net - 10
(https://bcrypt.codeplex.com/SourceControl/latest#BCrypt.Net/BCrypt.cs)
JFBCrypt - 10
(JFCommon/JFBCrypt.m at master · jayfuerstenberg/JFCommon · GitHub)

As such, I think it would be prudent to at least use 10, especially if
you feel that 12 is too slow (though, I don’t think half a second on
login is that much to worry with, imho). This is especially true if you
don’t plan on updating the cost for at least 2 years.

No cost factor will cover for low-entropy passwords, which in our
experience are unfortunately common in many deployments. A cost factor
of 8 is thus a compromise. You are of course welcome to increase the
cost factor locally if you deem it appropriate for your data, and your
hardware is capable of bearing the computational cost. We expect to
raise the default in core RT every ~two years to follow hardware trends.

Considering it’s hardcoded, changing it locally is not easy. If it was
a constant somewhere, that might be more feasible.

~reed