Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V2 SHA and MD5 crypto providers #698

Merged
merged 6 commits into from
Mar 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
the `log_in_after_create` setting when creating a new logged-out user
* [#668](https://github.com/binarylogic/authlogic/pull/668) -
BCrypt user forced to load SCrypt
* [#697](https://github.com/binarylogic/authlogic/issues/697) - Add V2 CryptoProviders for MD5 and SHA schemes that fix key stretching by hashing the byte digests instead of the hex strings representing those digests

## 5.0.4 (2019-09-11)

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ BUNDLE_GEMFILE=gemfiles/rails_6.0.rb bundle exec rake test
### Version Control Branches

We've been trying to follow the rails way, stable branches, but have been
inconsistent. We should have one branche for each minor version, named like
inconsistent. We should have one branch for each minor version, named like
`4-3-stable`. Releases should be done on those branches, not in master. So,
the "stable" branches should be the only branches with release tags.

Expand Down
3 changes: 3 additions & 0 deletions lib/authlogic/crypto_providers/md5.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ module Authlogic
module CryptoProviders
# A poor choice. There are known attacks against this algorithm.
class MD5
# V2 hashes the digest bytes in repeated stretches instead of hex characters.
autoload :V2, File.join(__dir__, "md5", "v2")

class << self
attr_accessor :join_token

Expand Down
35 changes: 35 additions & 0 deletions lib/authlogic/crypto_providers/md5/v2.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# frozen_string_literal: true

require "digest/md5"

module Authlogic
module CryptoProviders
class MD5
# A poor choice. There are known attacks against this algorithm.
class V2
class << self
attr_accessor :join_token

# The number of times to loop through the encryption.
def stretches
@stretches ||= 1
end
attr_writer :stretches

# Turns your raw password into a MD5 hash.
def encrypt(*tokens)
digest = tokens.flatten.join(join_token)
stretches.times { digest = Digest::MD5.digest(digest) }
digest.unpack("H*")[0]
end

# Does the crypted password match the tokens? Uses the same tokens that
# were used to encrypt.
def matches?(crypted, *tokens)
encrypt(*tokens) == crypted
end
end
end
end
end
end
3 changes: 3 additions & 0 deletions lib/authlogic/crypto_providers/sha1.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ module Authlogic
module CryptoProviders
# A poor choice. There are known attacks against this algorithm.
class Sha1
# V2 hashes the digest bytes in repeated stretches instead of hex characters.
autoload :V2, File.join(__dir__, "sha1", "v2")

class << self
def join_token
@join_token ||= "--"
Expand Down
41 changes: 41 additions & 0 deletions lib/authlogic/crypto_providers/sha1/v2.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# frozen_string_literal: true

require "digest/sha1"

module Authlogic
module CryptoProviders
class Sha1
# A poor choice. There are known attacks against this algorithm.
class V2
class << self
def join_token
@join_token ||= "--"
end
attr_writer :join_token

# The number of times to loop through the encryption.
def stretches
@stretches ||= 10
end
attr_writer :stretches

# Turns your raw password into a Sha1 hash.
def encrypt(*tokens)
tokens = tokens.flatten
digest = tokens.shift
stretches.times do
digest = Digest::SHA1.digest([digest, *tokens].join(join_token))
end
digest.unpack("H*")[0]
end

# Does the crypted password match the tokens? Uses the same tokens that
# were used to encrypt.
def matches?(crypted, *tokens)
encrypt(*tokens) == crypted
end
end
end
end
end
end
3 changes: 3 additions & 0 deletions lib/authlogic/crypto_providers/sha256.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ module CryptoProviders
#
# Uses the Sha256 hash algorithm to encrypt passwords.
class Sha256
# V2 hashes the digest bytes in repeated stretches instead of hex characters.
autoload :V2, File.join(__dir__, "sha256", "v2")

class << self
attr_accessor :join_token

Expand Down
58 changes: 58 additions & 0 deletions lib/authlogic/crypto_providers/sha256/v2.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# frozen_string_literal: true

require "digest/sha2"

module Authlogic
# The acts_as_authentic method has a crypto_provider option. This allows you
# to use any type of encryption you like. Just create a class with a class
# level encrypt and matches? method. See example below.
#
# === Example
#
# class MyAwesomeEncryptionMethod
# def self.encrypt(*tokens)
# # the tokens passed will be an array of objects, what type of object
# # is irrelevant, just do what you need to do with them and return a
# # single encrypted string. for example, you will most likely join all
# # of the objects into a single string and then encrypt that string
# end
#
# def self.matches?(crypted, *tokens)
# # return true if the crypted string matches the tokens. Depending on
# # your algorithm you might decrypt the string then compare it to the
# # token, or you might encrypt the tokens and make sure it matches the
# # crypted string, its up to you.
# end
# end
module CryptoProviders
class Sha256
# = Sha256
#
# Uses the Sha256 hash algorithm to encrypt passwords.
class V2
class << self
attr_accessor :join_token

# The number of times to loop through the encryption.
def stretches
@stretches ||= 20
end
attr_writer :stretches

# Turns your raw password into a Sha256 hash.
def encrypt(*tokens)
digest = tokens.flatten.join(join_token)
stretches.times { digest = Digest::SHA256.digest(digest) }
digest.unpack("H*")[0]
end

# Does the crypted password match the tokens? Uses the same tokens that
# were used to encrypt.
def matches?(crypted, *tokens)
encrypt(*tokens) == crypted
end
end
end
end
end
end
3 changes: 3 additions & 0 deletions lib/authlogic/crypto_providers/sha512.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ module CryptoProviders
# there are better choices. We recommend transitioning to a more secure,
# adaptive hashing algorithm, like scrypt.
class Sha512
# V2 hashes the digest bytes in repeated stretches instead of hex characters.
autoload :V2, File.join(__dir__, "sha512", "v2")

class << self
attr_accessor :join_token

Expand Down
39 changes: 39 additions & 0 deletions lib/authlogic/crypto_providers/sha512/v2.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

require "digest/sha2"

module Authlogic
module CryptoProviders
class Sha512
# SHA-512 does not have any practical known attacks against it. However,
# there are better choices. We recommend transitioning to a more secure,
# adaptive hashing algorithm, like scrypt.
class V2
class << self
attr_accessor :join_token

# The number of times to loop through the encryption.
def stretches
@stretches ||= 20
end
attr_writer :stretches

# Turns your raw password into a Sha512 hash.
def encrypt(*tokens)
digest = tokens.flatten.join(join_token)
stretches.times do
digest = Digest::SHA512.digest(digest)
end
digest.unpack("H*")[0]
end

# Does the crypted password match the tokens? Uses the same tokens that
# were used to encrypt.
def matches?(crypted, *tokens)
encrypt(*tokens) == crypted
end
end
end
end
end
end
19 changes: 19 additions & 0 deletions test/acts_as_authentic_test/password_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,25 @@ def test_transitioning_password
)
end

def test_v2_crypto_provider_transition
ben = users(:ben)

providers = [
Authlogic::CryptoProviders::Sha512::V2,
Authlogic::CryptoProviders::MD5::V2,
Authlogic::CryptoProviders::Sha1::V2,
Authlogic::CryptoProviders::Sha256::V2
]
transition_password_to(providers[0], ben)
providers.each_cons(2) do |old_provider, new_provider|
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow each_cons is perfect for this. Nice!

transition_password_to(
new_provider,
ben,
old_provider
)
end
end

def test_checks_password_against_database
ben = users(:aaron)
ben.password = "new pass"
Expand Down
54 changes: 54 additions & 0 deletions test/crypto_provider_test/md5/v2_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# frozen_string_literal: true

require "test_helper"

module CryptoProviderTest
module MD5
class V2Test < ActiveSupport::TestCase
def setup
@default_stretches = Authlogic::CryptoProviders::MD5::V2.stretches
end

def teardown
Authlogic::CryptoProviders::MD5::V2.stretches = @default_stretches
end

def test_encrypt
password = "test"
salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd"
expected_digest = "3d16884295a68fec30a2ae7ff0634b1e"

digest = Authlogic::CryptoProviders::MD5::V2.encrypt(password, salt)

assert_equal digest, expected_digest
end

def test_encrypt_with_3_stretches
Authlogic::CryptoProviders::MD5::V2.stretches = 3
password = "test"
salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd"
expected_digest = "da62ac8b983606f684cea0b93a558283"

digest = Authlogic::CryptoProviders::MD5::V2.encrypt(password, salt)

assert_equal digest, expected_digest
end

def test_matches
password = "test"
salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd"
expected_digest = "3d16884295a68fec30a2ae7ff0634b1e"

assert Authlogic::CryptoProviders::MD5::V2.matches?(expected_digest, password, salt)
end

def test_not_matches
password = "test"
salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd"
bad_digest = "12345"

assert !Authlogic::CryptoProviders::MD5::V2.matches?(bad_digest, password, salt)
end
end
end
end
52 changes: 52 additions & 0 deletions test/crypto_provider_test/md5_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# frozen_string_literal: true

require "test_helper"

module CryptoProviderTest
class MD5Test < ActiveSupport::TestCase
def setup
@default_stretches = Authlogic::CryptoProviders::MD5.stretches
end

def teardown
Authlogic::CryptoProviders::MD5.stretches = @default_stretches
end

def test_encrypt
password = "test"
salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd"
expected_digest = "3d16884295a68fec30a2ae7ff0634b1e"

digest = Authlogic::CryptoProviders::MD5.encrypt(password, salt)

assert_equal digest, expected_digest
end

def test_encrypt_with_3_stretches
Authlogic::CryptoProviders::MD5.stretches = 3
password = "test"
salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd"
expected_digest = "9ac3a3a2e68f822f3482cbea3cbed9a3"

digest = Authlogic::CryptoProviders::MD5.encrypt(password, salt)

assert_equal digest, expected_digest
end

def test_matches
password = "test"
salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd"
expected_digest = "3d16884295a68fec30a2ae7ff0634b1e"

assert Authlogic::CryptoProviders::MD5.matches?(expected_digest, password, salt)
end

def test_not_matches
password = "test"
salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd"
bad_digest = "12345"

assert !Authlogic::CryptoProviders::MD5.matches?(bad_digest, password, salt)
end
end
end
Loading