[Rt-commit] rt branch, 4.2/bcrypt-passwords, created. rt-4.1.19-109-g7a0fe00

Alex Vandiver alexmv at bestpractical.com
Wed Sep 4 12:00:22 EDT 2013


The branch, 4.2/bcrypt-passwords has been created
        at  7a0fe00d4379f7b183aa04a931f37a7decbce984 (commit)

- Log -----------------------------------------------------------------
commit b0e494c69aebcca9f5441e369e5814bba8e5acf1
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   3.34/s       --    -100%
            sha-512 36850/s 1102153%       --
    
    That is, bcrypt is four 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 10 here, but allow for higher values to be used later.

diff --git a/docs/UPGRADING-4.2 b/docs/UPGRADING-4.2
index 38539d4..d71d6ff 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
 
 =item *
diff --git a/lib/RT/User.pm b/lib/RT/User.pm
index 94031d1..a4b4ea4 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 10 rounds
+        $rounds = 10;
+
+        # 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");

commit 87cf33b8d1a4d8ba04ba46fb201182d04f0414eb
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Tue Sep 3 15:31:57 2013 -0400

    SHA-512 passwords before passing to bcrypt for key derivation
    
    The bcrypt key derivation function only uses the first 72 bytes of the
    input; when used directly on a password, this effectively limits
    password length to 72 characters.
    
    Allow for arbitrarily long passwords by hashing the password using
    SHA-512 (which produces 512 bits, or 64 bytes, of output) before passing
    it to bcrypt.

diff --git a/lib/RT/User.pm b/lib/RT/User.pm
index a4b4ea4..7b2ddfc 100644
--- a/lib/RT/User.pm
+++ b/lib/RT/User.pm
@@ -897,7 +897,7 @@ sub _GeneratePassword_bcrypt {
         key_nul => 1,
         cost    => $rounds,
         salt    => $salt,
-    }, encode_utf8($password) );
+    }, Digest::SHA::sha512( encode_utf8($password) ) );
 
     return join("!", "", "bcrypt", sprintf("%02d", $rounds),
                 Crypt::Eksblowfish::Bcrypt::en_base64( $salt ).

commit 7a0fe00d4379f7b183aa04a931f37a7decbce984
Author: Alex Vandiver <alexmv at bestpractical.com>
Date:   Wed Sep 4 03:40:45 2013 -0400

    Allow a tunable number of rounds for bcrypt key derivation

diff --git a/etc/RT_Config.pm.in b/etc/RT_Config.pm.in
index 8af3ad7..7f53cf3 100755
--- a/etc/RT_Config.pm.in
+++ b/etc/RT_Config.pm.in
@@ -2018,6 +2018,17 @@ Simple wildcards, similar to SSL certificates, are allowed.  For example:
 
 Set(@ReferrerWhitelist, qw());
 
+
+=item C<$BcryptCost>
+
+This sets the default cost parameter used for the C<bcrypt> key
+derivation function.  Valid values range from 4 to 31, inclusive, with
+higher numbers denoting greater effort.
+
+=cut
+
+Set($BcryptCost, 10);
+
 =back
 
 
diff --git a/lib/RT/User.pm b/lib/RT/User.pm
index 7b2ddfc..4b7ac2c 100644
--- a/lib/RT/User.pm
+++ b/lib/RT/User.pm
@@ -885,8 +885,7 @@ sub _GeneratePassword_bcrypt {
         # special bcrypt base64.
         $salt = Crypt::Eksblowfish::Bcrypt::de_base64( substr($rest[1], 0, 22) );
     } else {
-        # The current standard is 10 rounds
-        $rounds = 10;
+        $rounds = RT->Config->Get('BcryptCost');
 
         # Generate a random 16-octet base64 salt
         $salt = "";
@@ -985,7 +984,9 @@ sub IsPassword {
         # If it's a new-style (>= RT 4.0) password, it starts with a '!'
         my (undef, $method, @rest) = split /!/, $stored;
         if ($method eq "bcrypt") {
-            return $self->_GeneratePassword_bcrypt($value, @rest) eq $stored;
+            return 0 unless $self->_GeneratePassword_bcrypt($value, @rest) eq $stored;
+            # Upgrade to a larger number of rounds if necessary
+            return 1 unless $rest[0] < RT->Config->Get('BcryptCost');
         } elsif ($method eq "sha512") {
             return 0 unless $self->_GeneratePassword_sha512($value, @rest) eq $stored;
         } else {
diff --git a/t/api/password-types.t b/t/api/password-types.t
index e73bfe6..7b75c62 100644
--- a/t/api/password-types.t
+++ b/t/api/password-types.t
@@ -15,6 +15,13 @@ like($old, qr/^\!$default\!/, "Stored as salted $default");
 ok($root->IsPassword("password"));
 is($root->__Value("Password"), $old, "Unchanged after password check");
 
+# bcrypt (smaller number of rounds)
+my $salt = Crypt::Eksblowfish::Bcrypt::en_base64("a"x16);
+$root->_Set( Field => "Password", Value => RT::User->_GeneratePassword_bcrypt("smaller", 6, $salt) );
+like($root->__Value("Password"), qr/^\!$default\!06\!/, "Stored with a smaller number of rounds");
+ok($root->IsPassword("smaller"), "Smaller number of bcrypt rounds works");
+like($root->__Value("Password"), qr/^\!$default\!10\!/, "And is now upgraded to salted $default");
+
 # Salted SHA-512, one round
 $root->_Set( Field => "Password", Value => RT::User->_GeneratePassword_sha512("other", "salt") );
 ok($root->IsPassword("other"), "SHA-512 password works");

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


More information about the Rt-commit mailing list