From 08b04bc0831a54a7ed8c5a118ec97c0a72ca4481 Mon Sep 17 00:00:00 2001 From: Andrew Schwartz Date: Sun, 23 Feb 2020 11:00:25 -0500 Subject: [PATCH] Add V2 crypto providers for SHA* and MD5 encryption methods. Fixes original SHA512 crypto provider by digesting bytes instead of hex string. --- lib/authlogic/crypto_providers.rb | 9 +++ lib/authlogic/crypto_providers/v2/md5.rb | 35 ++++++++++++ lib/authlogic/crypto_providers/v2/sha1.rb | 41 ++++++++++++++ lib/authlogic/crypto_providers/v2/sha256.rb | 58 ++++++++++++++++++++ lib/authlogic/crypto_providers/v2/sha512.rb | 39 +++++++++++++ test/acts_as_authentic_test/password_test.rb | 19 +++++++ test/crypto_provider_test/v2/md5_test.rb | 27 +++++++++ test/crypto_provider_test/v2/sha1_test.rb | 27 +++++++++ test/crypto_provider_test/v2/sha256_test.rb | 27 +++++++++ test/crypto_provider_test/v2/sha512_test.rb | 29 ++++++++++ 10 files changed, 311 insertions(+) create mode 100644 lib/authlogic/crypto_providers/v2/md5.rb create mode 100644 lib/authlogic/crypto_providers/v2/sha1.rb create mode 100644 lib/authlogic/crypto_providers/v2/sha256.rb create mode 100644 lib/authlogic/crypto_providers/v2/sha512.rb create mode 100644 test/crypto_provider_test/v2/md5_test.rb create mode 100644 test/crypto_provider_test/v2/sha1_test.rb create mode 100644 test/crypto_provider_test/v2/sha256_test.rb create mode 100644 test/crypto_provider_test/v2/sha512_test.rb diff --git a/lib/authlogic/crypto_providers.rb b/lib/authlogic/crypto_providers.rb index 3e4b4711..6c2d6b11 100644 --- a/lib/authlogic/crypto_providers.rb +++ b/lib/authlogic/crypto_providers.rb @@ -30,6 +30,15 @@ module CryptoProviders autoload :BCrypt, "authlogic/crypto_providers/bcrypt" autoload :SCrypt, "authlogic/crypto_providers/scrypt" + # V2 crypto providers fix their predecessors' encryption schemes by + # hashing byte strings instead of the ehx strings output by `hexdigest` + module V2 + autoload :MD5, "authlogic/crypto_providers/v2/md5" + autoload :SHA1, "authlogic/crypto_providers/v2/sha1" + autoload :SHA256, "authlogic/crypto_providers/v2/sha256" + autoload :SHA512, "authlogic/crypto_providers/v2/sha512" + end + # Guide users to choose a better crypto provider. class Guidance BUILTIN_PROVIDER_PREFIX = "Authlogic::CryptoProviders::" diff --git a/lib/authlogic/crypto_providers/v2/md5.rb b/lib/authlogic/crypto_providers/v2/md5.rb new file mode 100644 index 00000000..3445481e --- /dev/null +++ b/lib/authlogic/crypto_providers/v2/md5.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "digest/md5" + +module Authlogic + module CryptoProviders + module V2 + # A poor choice. There are known attacks against this algorithm. + class MD5 + 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 diff --git a/lib/authlogic/crypto_providers/v2/sha1.rb b/lib/authlogic/crypto_providers/v2/sha1.rb new file mode 100644 index 00000000..1880a8e4 --- /dev/null +++ b/lib/authlogic/crypto_providers/v2/sha1.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require "digest/sha1" + +module Authlogic + module CryptoProviders + module V2 + # A poor choice. There are known attacks against this algorithm. + class SHA1 + 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 diff --git a/lib/authlogic/crypto_providers/v2/sha256.rb b/lib/authlogic/crypto_providers/v2/sha256.rb new file mode 100644 index 00000000..f12b034f --- /dev/null +++ b/lib/authlogic/crypto_providers/v2/sha256.rb @@ -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 + module V2 + # = Sha256 + # + # Uses the Sha256 hash algorithm to encrypt passwords. + class SHA256 + 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 diff --git a/lib/authlogic/crypto_providers/v2/sha512.rb b/lib/authlogic/crypto_providers/v2/sha512.rb new file mode 100644 index 00000000..1a56b366 --- /dev/null +++ b/lib/authlogic/crypto_providers/v2/sha512.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require "digest/sha2" + +module Authlogic + module CryptoProviders + module V2 + # 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 SHA512 + 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 diff --git a/test/acts_as_authentic_test/password_test.rb b/test/acts_as_authentic_test/password_test.rb index 14c2c2a3..47ffd5bb 100644 --- a/test/acts_as_authentic_test/password_test.rb +++ b/test/acts_as_authentic_test/password_test.rb @@ -92,6 +92,25 @@ def test_transitioning_password ) end + def test_v2_crypto_provider_transition + ben = users(:ben) + + providers = [ + Authlogic::CryptoProviders::V2::SHA512, + Authlogic::CryptoProviders::V2::MD5, + Authlogic::CryptoProviders::V2::SHA1, + Authlogic::CryptoProviders::V2::SHA256 + ] + transition_password_to(providers[0], ben) + providers.each_cons(2) do |old_provider, new_provider| + transition_password_to( + new_provider, + ben, + old_provider + ) + end + end + def test_checks_password_against_database ben = users(:aaron) ben.password = "new pass" diff --git a/test/crypto_provider_test/v2/md5_test.rb b/test/crypto_provider_test/v2/md5_test.rb new file mode 100644 index 00000000..f08cab81 --- /dev/null +++ b/test/crypto_provider_test/v2/md5_test.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "test_helper" + +module CryptoProviderTest + module V2 + class MD5Test < ActiveSupport::TestCase + def test_encrypt + assert Authlogic::CryptoProviders::V2::MD5.encrypt("mypass") + end + + def test_matches + hash = Authlogic::CryptoProviders::V2::MD5.encrypt("mypass") + assert Authlogic::CryptoProviders::V2::MD5.matches?(hash, "mypass") + end + + def test_matches_2 + password = "test" + salt = "7e3041ebc2fc05a40c60028e2c4901a81035d3cd" + digest = "51563330eb60e0eeb89759b01f08e872" + Authlogic::CryptoProviders::V2::MD5.stretches = 1 + assert Authlogic::CryptoProviders::V2::MD5.matches?(digest, nil, salt, password, nil) + Authlogic::CryptoProviders::V2::MD5.stretches = 10 + end + end + end +end diff --git a/test/crypto_provider_test/v2/sha1_test.rb b/test/crypto_provider_test/v2/sha1_test.rb new file mode 100644 index 00000000..19657ca6 --- /dev/null +++ b/test/crypto_provider_test/v2/sha1_test.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "test_helper" + +module CryptoProviderTest + module V2 + class SHA1Test < ActiveSupport::TestCase + def test_encrypt + assert Authlogic::CryptoProviders::V2::SHA1.encrypt("mypass") + end + + def test_matches + hash = Authlogic::CryptoProviders::V2::SHA1.encrypt("mypass") + assert Authlogic::CryptoProviders::V2::SHA1.matches?(hash, "mypass") + end + + def test_matches_2 + password = "test" + salt = "abc" + digest = "2d578fb3ab6bdab725080f00d5689f79b7d1df51" + Authlogic::CryptoProviders::V2::SHA1.stretches = 1 + assert Authlogic::CryptoProviders::V2::SHA1.matches?(digest, nil, salt, password, nil) + Authlogic::CryptoProviders::V2::SHA1.stretches = 10 + end + end + end +end diff --git a/test/crypto_provider_test/v2/sha256_test.rb b/test/crypto_provider_test/v2/sha256_test.rb new file mode 100644 index 00000000..cbef6514 --- /dev/null +++ b/test/crypto_provider_test/v2/sha256_test.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require "test_helper" + +module CryptoProviderTest + module V2 + class SHA256Test < ActiveSupport::TestCase + def test_encrypt + assert Authlogic::CryptoProviders::V2::SHA256.encrypt("mypass") + end + + def test_matches + hash = Authlogic::CryptoProviders::V2::SHA256.encrypt("mypass") + assert Authlogic::CryptoProviders::V2::SHA256.matches?(hash, "mypass") + end + + def test_matches_2 + password = "test" + salt = "abc" + digest = "70e0f1ade11debb6732029c267095e092b5b43ff271d4f8d9158cb004322f38b" + Authlogic::CryptoProviders::V2::SHA256.stretches = 1 + assert Authlogic::CryptoProviders::V2::SHA256.matches?(digest, nil, salt, password, nil) + Authlogic::CryptoProviders::V2::SHA256.stretches = 10 + end + end + end +end diff --git a/test/crypto_provider_test/v2/sha512_test.rb b/test/crypto_provider_test/v2/sha512_test.rb new file mode 100644 index 00000000..41c44ff6 --- /dev/null +++ b/test/crypto_provider_test/v2/sha512_test.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require "test_helper" + +module CryptoProviderTest + module V2 + class SHA512Test < ActiveSupport::TestCase + def test_encrypt + assert Authlogic::CryptoProviders::V2::SHA512.encrypt("mypass") + end + + def test_matches + hash = Authlogic::CryptoProviders::V2::SHA512.encrypt("mypass") + assert Authlogic::CryptoProviders::V2::SHA512.matches?(hash, "mypass") + end + + def test_matches_2 + password = "test" + salt = "abc" + # rubocop:disable Metrics/LineLength + digest = "c7cb2b81ccbb686eaefafbfbcf61334fb75f8e5dcb3de8b86fec53ad1a5dd013c0c4c9cc3af7c59aed2afab59dd463f6a84d9531f46e2efeb3681bd79bf57a37" + # rubocop:enable Metrics/LineLength + Authlogic::CryptoProviders::V2::SHA512.stretches = 1 + assert Authlogic::CryptoProviders::V2::SHA512.matches?(digest, nil, salt, password, nil) + Authlogic::CryptoProviders::V2::SHA512.stretches = 10 + end + end + end +end