Skip to content

Commit 633cc8d

Browse files
committed
sha256 password before passing to bcrypt to avoid issues with 72 bytes truncation for passwords
1 parent c6b08ae commit 633cc8d

File tree

2 files changed

+56
-3
lines changed

2 files changed

+56
-3
lines changed

lib/devise/encryptor.rb

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,50 @@
11
# frozen_string_literal: true
22

33
require 'bcrypt'
4+
require 'digest'
45

56
module Devise
67
module Encryptor
78
def self.digest(klass, password)
89
if klass.pepper.present?
910
password = "#{password}#{klass.pepper}"
1011
end
12+
# This converts the password (of any length) into a fixed
13+
# 64-character hex string, safely under the 72-char limit
14+
password = Digest::SHA256.hexdigest(password)
15+
16+
# BCrypt the pre-hashed string
1117
::BCrypt::Password.create(password, cost: klass.stretches).to_s
1218
end
1319

20+
# Compares a potential password with a stored hash.
21+
#
22+
# It attempts the new (SHA-256 -> BCrypt) method first.
23+
# If that fails, it falls back to the old (direct BCrypt) method
24+
# to support existing passwords that were not pre-hashed
1425
def self.compare(klass, hashed_password, password)
1526
return false if hashed_password.blank?
16-
bcrypt = ::BCrypt::Password.new(hashed_password)
27+
28+
begin
29+
bcrypt = ::BCrypt::Password.new(hashed_password)
30+
rescue ::BCrypt::Errors::InvalidHash
31+
return false
32+
end
33+
1734
if klass.pepper.present?
1835
password = "#{password}#{klass.pepper}"
1936
end
20-
password = ::BCrypt::Engine.hash_secret(password, bcrypt.salt)
21-
Devise.secure_compare(password, hashed_password)
37+
38+
# This is for passwords created with the new `digest` method.
39+
pre_hashed_password = Digest::SHA256.hexdigest(password)
40+
new_style_hash = ::BCrypt::Engine.hash_secret(pre_hashed_password, bcrypt.salt)
41+
42+
return true if Devise.secure_compare(new_style_hash, hashed_password)
43+
44+
# This is for passwords created before this change
45+
# We re-run the original logic
46+
old_style_hash = ::BCrypt::Engine.hash_secret(password, bcrypt.salt)
47+
Devise.secure_compare(old_style_hash, hashed_password)
2248
end
2349
end
2450
end

test/encryptor_test.rb

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# frozen_string_literal: true
2+
3+
require 'test_helper'
4+
require 'bcrypt'
5+
6+
class EncryptorTest < ActiveSupport::TestCase
7+
test 'encrypt/decrypt passwords' do
8+
hashed_password = Devise::Encryptor.digest(Devise, 'example')
9+
assert Devise::Encryptor.compare(Devise, hashed_password, 'example')
10+
assert_not Devise::Encryptor.compare(Devise, hashed_password, 'example1')
11+
end
12+
13+
test 'encrypt/decrypt support passwords longer 72 bytes' do
14+
hashed_password = Devise::Encryptor.digest(Devise, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa123')
15+
assert Devise::Encryptor.compare(Devise, hashed_password, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa123')
16+
assert_not Devise::Encryptor.compare(Devise, hashed_password, 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa125')
17+
end
18+
19+
test 'encrypt/decrypt support old bcrypt only passwords' do
20+
password = 'example'
21+
password_with_pepper = "#{password}#{Devise.pepper}"
22+
old_hashed_password =::BCrypt::Password.create(password_with_pepper, cost: Devise.stretches)
23+
24+
assert Devise::Encryptor.compare(Devise, old_hashed_password, password)
25+
assert_not Devise::Encryptor.compare(Devise, old_hashed_password, 'examplo')
26+
end
27+
end

0 commit comments

Comments
 (0)