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

Alex Vandiver alexmv at bestpractical.com
Tue Sep 3 02:02:10 EDT 2013


The branch, 4.2/bcrypt-passwords has been created
        at  07ac7c51167a9427a2857fd4a09671ed8b9cab9c (commit)

- Log -----------------------------------------------------------------
commit 07ac7c51167a9427a2857fd4a09671ed8b9cab9c
Author: Alex Vandiver <alexmv at 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<sha512>.
+By default, the method is currently C<bcrypt>.
 
 =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");

-----------------------------------------------------------------------


More information about the Rt-commit mailing list