jwt 2.5.0 → 2.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +92 -23
  3. data/CONTRIBUTING.md +7 -7
  4. data/README.md +125 -47
  5. data/lib/jwt/base64.rb +16 -2
  6. data/lib/jwt/claims_validator.rb +1 -1
  7. data/lib/jwt/configuration/container.rb +14 -3
  8. data/lib/jwt/decode.rb +41 -24
  9. data/lib/jwt/deprecations.rb +29 -0
  10. data/lib/jwt/encode.rb +23 -19
  11. data/lib/jwt/error.rb +1 -0
  12. data/lib/jwt/{algos → jwa}/ecdsa.rb +19 -7
  13. data/lib/jwt/jwa/eddsa.rb +42 -0
  14. data/lib/jwt/jwa/hmac.rb +75 -0
  15. data/lib/jwt/jwa/hmac_rbnacl.rb +50 -0
  16. data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +46 -0
  17. data/lib/jwt/{algos → jwa}/none.rb +4 -2
  18. data/lib/jwt/jwa/ps.rb +30 -0
  19. data/lib/jwt/jwa/rsa.rb +25 -0
  20. data/lib/jwt/{algos → jwa}/unsupported.rb +1 -1
  21. data/lib/jwt/jwa/wrapper.rb +26 -0
  22. data/lib/jwt/jwa.rb +62 -0
  23. data/lib/jwt/jwk/ec.rb +168 -116
  24. data/lib/jwt/jwk/hmac.rb +64 -28
  25. data/lib/jwt/jwk/key_base.rb +33 -11
  26. data/lib/jwt/jwk/key_finder.rb +19 -35
  27. data/lib/jwt/jwk/okp_rbnacl.rb +110 -0
  28. data/lib/jwt/jwk/rsa.rb +142 -77
  29. data/lib/jwt/jwk/set.rb +80 -0
  30. data/lib/jwt/jwk.rb +14 -11
  31. data/lib/jwt/verify.rb +8 -4
  32. data/lib/jwt/version.rb +20 -3
  33. data/lib/jwt/x5c_key_finder.rb +0 -3
  34. data/lib/jwt.rb +1 -0
  35. data/ruby-jwt.gemspec +11 -4
  36. metadata +35 -27
  37. data/.codeclimate.yml +0 -8
  38. data/.github/workflows/coverage.yml +0 -27
  39. data/.github/workflows/test.yml +0 -67
  40. data/.gitignore +0 -13
  41. data/.reek.yml +0 -22
  42. data/.rspec +0 -2
  43. data/.rubocop.yml +0 -67
  44. data/.sourcelevel.yml +0 -17
  45. data/Appraisals +0 -13
  46. data/Gemfile +0 -7
  47. data/Rakefile +0 -16
  48. data/lib/jwt/algos/eddsa.rb +0 -35
  49. data/lib/jwt/algos/hmac.rb +0 -36
  50. data/lib/jwt/algos/ps.rb +0 -43
  51. data/lib/jwt/algos/rsa.rb +0 -22
  52. data/lib/jwt/algos.rb +0 -44
  53. data/lib/jwt/security_utils.rb +0 -59
  54. data/lib/jwt/signature.rb +0 -35
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ # Deprecations module to handle deprecation warnings in the gem
5
+ module Deprecations
6
+ class << self
7
+ def warning(message)
8
+ case JWT.configuration.deprecation_warnings
9
+ when :warn
10
+ warn("[DEPRECATION WARNING] #{message}")
11
+ when :once
12
+ return if record_warned(message)
13
+
14
+ warn("[DEPRECATION WARNING] #{message}")
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ def record_warned(message)
21
+ @warned ||= []
22
+ return true if @warned.include?(message)
23
+
24
+ @warned << message
25
+ false
26
+ end
27
+ end
28
+ end
29
+ end
data/lib/jwt/encode.rb CHANGED
@@ -1,24 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './algos'
4
- require_relative './claims_validator'
3
+ require_relative 'jwa'
4
+ require_relative 'claims_validator'
5
5
 
6
6
  # JWT::Encode module
7
7
  module JWT
8
8
  # Encoding logic for JWT
9
9
  class Encode
10
- ALG_NONE = 'none'
11
- ALG_KEY = 'alg'
10
+ ALG_KEY = 'alg'
12
11
 
13
12
  def initialize(options)
14
- @payload = options[:payload]
15
- @key = options[:key]
16
- _, @algorithm = Algos.find(options[:algorithm])
17
- @headers = options[:headers].transform_keys(&:to_s)
13
+ @payload = options[:payload]
14
+ @key = options[:key]
15
+ @algorithm = JWA.create(options[:algorithm])
16
+ @headers = options[:headers].transform_keys(&:to_s)
17
+ @headers[ALG_KEY] = @algorithm.alg
18
18
  end
19
19
 
20
20
  def segments
21
- @segments ||= combine(encoded_header_and_payload, encoded_signature)
21
+ validate_claims!
22
+ combine(encoded_header_and_payload, encoded_signature)
22
23
  end
23
24
 
24
25
  private
@@ -40,25 +41,28 @@ module JWT
40
41
  end
41
42
 
42
43
  def encode_header
43
- @headers[ALG_KEY] = @algorithm
44
- encode(@headers)
44
+ encode_data(@headers)
45
45
  end
46
46
 
47
47
  def encode_payload
48
- if @payload.is_a?(Hash)
49
- ClaimsValidator.new(@payload).validate!
50
- end
48
+ encode_data(@payload)
49
+ end
51
50
 
52
- encode(@payload)
51
+ def signature
52
+ @algorithm.sign(data: encoded_header_and_payload, signing_key: @key)
53
53
  end
54
54
 
55
- def encode_signature
56
- return '' if @algorithm == ALG_NONE
55
+ def validate_claims!
56
+ return unless @payload.is_a?(Hash)
57
57
 
58
- ::JWT::Base64.url_encode(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key))
58
+ ClaimsValidator.new(@payload).validate!
59
+ end
60
+
61
+ def encode_signature
62
+ ::JWT::Base64.url_encode(signature)
59
63
  end
60
64
 
61
- def encode(data)
65
+ def encode_data(data)
62
66
  ::JWT::Base64.url_encode(JWT::JSON.generate(data))
63
67
  end
64
68
 
data/lib/jwt/error.rb CHANGED
@@ -17,6 +17,7 @@ module JWT
17
17
  class InvalidJtiError < DecodeError; end
18
18
  class InvalidPayload < DecodeError; end
19
19
  class MissingRequiredClaim < DecodeError; end
20
+ class Base64DecodeError < DecodeError; end
20
21
 
21
22
  class JWKError < DecodeError; end
22
23
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JWT
4
- module Algos
4
+ module JWA
5
5
  module Ecdsa
6
6
  module_function
7
7
 
@@ -30,8 +30,7 @@ module JWT
30
30
 
31
31
  SUPPORTED = NAMED_CURVES.map { |_, c| c[:algorithm] }.uniq.freeze
32
32
 
33
- def sign(to_sign)
34
- algorithm, msg, key = to_sign.values
33
+ def sign(algorithm, msg, key)
35
34
  curve_definition = curve_by_name(key.group.curve_name)
36
35
  key_algorithm = curve_definition[:algorithm]
37
36
  if algorithm != key_algorithm
@@ -39,11 +38,10 @@ module JWT
39
38
  end
40
39
 
41
40
  digest = OpenSSL::Digest.new(curve_definition[:digest])
42
- SecurityUtils.asn1_to_raw(key.dsa_sign_asn1(digest.digest(msg)), key)
41
+ asn1_to_raw(key.dsa_sign_asn1(digest.digest(msg)), key)
43
42
  end
44
43
 
45
- def verify(to_verify)
46
- algorithm, public_key, signing_input, signature = to_verify.values
44
+ def verify(algorithm, public_key, signing_input, signature)
47
45
  curve_definition = curve_by_name(public_key.group.curve_name)
48
46
  key_algorithm = curve_definition[:algorithm]
49
47
  if algorithm != key_algorithm
@@ -51,7 +49,9 @@ module JWT
51
49
  end
52
50
 
53
51
  digest = OpenSSL::Digest.new(curve_definition[:digest])
54
- public_key.dsa_verify_asn1(digest.digest(signing_input), SecurityUtils.raw_to_asn1(signature, public_key))
52
+ public_key.dsa_verify_asn1(digest.digest(signing_input), raw_to_asn1(signature, public_key))
53
+ rescue OpenSSL::PKey::PKeyError
54
+ raise JWT::VerificationError, 'Signature verification raised'
55
55
  end
56
56
 
57
57
  def curve_by_name(name)
@@ -59,6 +59,18 @@ module JWT
59
59
  raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported"
60
60
  end
61
61
  end
62
+
63
+ def raw_to_asn1(signature, private_key)
64
+ byte_size = (private_key.group.degree + 7) / 8
65
+ sig_bytes = signature[0..(byte_size - 1)]
66
+ sig_char = signature[byte_size..-1] || ''
67
+ OpenSSL::ASN1::Sequence.new([sig_bytes, sig_char].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
68
+ end
69
+
70
+ def asn1_to_raw(signature, public_key)
71
+ byte_size = (public_key.group.degree + 7) / 8
72
+ OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join
73
+ end
62
74
  end
63
75
  end
64
76
  end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ module Eddsa
6
+ SUPPORTED = %w[ED25519 EdDSA].freeze
7
+ SUPPORTED_DOWNCASED = SUPPORTED.map(&:downcase).freeze
8
+
9
+ class << self
10
+ def sign(algorithm, msg, key)
11
+ unless key.is_a?(RbNaCl::Signatures::Ed25519::SigningKey)
12
+ raise EncodeError, "Key given is a #{key.class} but has to be an RbNaCl::Signatures::Ed25519::SigningKey"
13
+ end
14
+
15
+ validate_algorithm!(algorithm)
16
+
17
+ key.sign(msg)
18
+ end
19
+
20
+ def verify(algorithm, public_key, signing_input, signature)
21
+ unless public_key.is_a?(RbNaCl::Signatures::Ed25519::VerifyKey)
22
+ raise DecodeError, "key given is a #{public_key.class} but has to be a RbNaCl::Signatures::Ed25519::VerifyKey"
23
+ end
24
+
25
+ validate_algorithm!(algorithm)
26
+
27
+ public_key.verify(signature, signing_input)
28
+ rescue RbNaCl::CryptoError
29
+ false
30
+ end
31
+
32
+ private
33
+
34
+ def validate_algorithm!(algorithm)
35
+ return if SUPPORTED_DOWNCASED.include?(algorithm.downcase)
36
+
37
+ raise IncorrectAlgorithm, "Algorithm #{algorithm} not supported. Supported algoritms are #{SUPPORTED.join(', ')}"
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ module Hmac
6
+ module_function
7
+
8
+ MAPPING = {
9
+ 'HS256' => OpenSSL::Digest::SHA256,
10
+ 'HS384' => OpenSSL::Digest::SHA384,
11
+ 'HS512' => OpenSSL::Digest::SHA512
12
+ }.freeze
13
+
14
+ SUPPORTED = MAPPING.keys
15
+
16
+ def sign(algorithm, msg, key)
17
+ key ||= ''
18
+
19
+ raise JWT::DecodeError, 'HMAC key expected to be a String' unless key.is_a?(String)
20
+
21
+ OpenSSL::HMAC.digest(MAPPING[algorithm].new, key, msg)
22
+ rescue OpenSSL::HMACError => e
23
+ if key == '' && e.message == 'EVP_PKEY_new_mac_key: malloc failure'
24
+ raise JWT::DecodeError, 'OpenSSL 3.0 does not support nil or empty hmac_secret'
25
+ end
26
+
27
+ raise e
28
+ end
29
+
30
+ def verify(algorithm, key, signing_input, signature)
31
+ SecurityUtils.secure_compare(signature, sign(algorithm, signing_input, key))
32
+ end
33
+
34
+ # Copy of https://github.com/rails/rails/blob/v7.0.3.1/activesupport/lib/active_support/security_utils.rb
35
+ # rubocop:disable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
36
+ module SecurityUtils
37
+ # Constant time string comparison, for fixed length strings.
38
+ #
39
+ # The values compared should be of fixed length, such as strings
40
+ # that have already been processed by HMAC. Raises in case of length mismatch.
41
+
42
+ if defined?(OpenSSL.fixed_length_secure_compare)
43
+ def fixed_length_secure_compare(a, b)
44
+ OpenSSL.fixed_length_secure_compare(a, b)
45
+ end
46
+ else
47
+ # :nocov:
48
+ def fixed_length_secure_compare(a, b)
49
+ raise ArgumentError, "string length mismatch." unless a.bytesize == b.bytesize
50
+
51
+ l = a.unpack "C#{a.bytesize}"
52
+
53
+ res = 0
54
+ b.each_byte { |byte| res |= byte ^ l.shift }
55
+ res == 0
56
+ end
57
+ # :nocov:
58
+ end
59
+ module_function :fixed_length_secure_compare
60
+
61
+ # Secure string comparison for strings of variable length.
62
+ #
63
+ # While a timing attack would not be able to discern the content of
64
+ # a secret compared via secure_compare, it is possible to determine
65
+ # the secret length. This should be considered when using secure_compare
66
+ # to compare weak, short secrets to user input.
67
+ def secure_compare(a, b)
68
+ a.bytesize == b.bytesize && fixed_length_secure_compare(a, b)
69
+ end
70
+ module_function :secure_compare
71
+ end
72
+ # rubocop:enable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module Algos
5
+ module HmacRbNaCl
6
+ MAPPING = { 'HS512256' => ::RbNaCl::HMAC::SHA512256 }.freeze
7
+ SUPPORTED = MAPPING.keys
8
+ class << self
9
+ def sign(algorithm, msg, key)
10
+ Deprecations.warning("The use of the algorithm #{algorithm} is deprecated and will be removed in the next major version of ruby-jwt")
11
+ if (hmac = resolve_algorithm(algorithm))
12
+ hmac.auth(key_for_rbnacl(hmac, key).encode('binary'), msg.encode('binary'))
13
+ else
14
+ Hmac.sign(algorithm, msg, key)
15
+ end
16
+ end
17
+
18
+ def verify(algorithm, key, signing_input, signature)
19
+ Deprecations.warning("The use of the algorithm #{algorithm} is deprecated and will be removed in the next major version of ruby-jwt")
20
+ if (hmac = resolve_algorithm(algorithm))
21
+ hmac.verify(key_for_rbnacl(hmac, key).encode('binary'), signature.encode('binary'), signing_input.encode('binary'))
22
+ else
23
+ Hmac.verify(algorithm, key, signing_input, signature)
24
+ end
25
+ rescue ::RbNaCl::BadAuthenticatorError, ::RbNaCl::LengthError
26
+ false
27
+ end
28
+
29
+ private
30
+
31
+ def key_for_rbnacl(hmac, key)
32
+ key ||= ''
33
+ raise JWT::DecodeError, 'HMAC key expected to be a String' unless key.is_a?(String)
34
+
35
+ return padded_empty_key(hmac.key_bytes) if key == ''
36
+
37
+ key
38
+ end
39
+
40
+ def resolve_algorithm(algorithm)
41
+ MAPPING.fetch(algorithm)
42
+ end
43
+
44
+ def padded_empty_key(length)
45
+ Array.new(length, 0x0).pack('C*').encode('binary')
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module Algos
5
+ module HmacRbNaClFixed
6
+ MAPPING = { 'HS512256' => ::RbNaCl::HMAC::SHA512256 }.freeze
7
+ SUPPORTED = MAPPING.keys
8
+
9
+ class << self
10
+ def sign(algorithm, msg, key)
11
+ key ||= ''
12
+ Deprecations.warning("The use of the algorithm #{algorithm} is deprecated and will be removed in the next major version of ruby-jwt")
13
+ raise JWT::DecodeError, 'HMAC key expected to be a String' unless key.is_a?(String)
14
+
15
+ if (hmac = resolve_algorithm(algorithm)) && key.bytesize <= hmac.key_bytes
16
+ hmac.auth(padded_key_bytes(key, hmac.key_bytes), msg.encode('binary'))
17
+ else
18
+ Hmac.sign(algorithm, msg, key)
19
+ end
20
+ end
21
+
22
+ def verify(algorithm, key, signing_input, signature)
23
+ key ||= ''
24
+ Deprecations.warning("The use of the algorithm #{algorithm} is deprecated and will be removed in the next major version of ruby-jwt")
25
+ raise JWT::DecodeError, 'HMAC key expected to be a String' unless key.is_a?(String)
26
+
27
+ if (hmac = resolve_algorithm(algorithm)) && key.bytesize <= hmac.key_bytes
28
+ hmac.verify(padded_key_bytes(key, hmac.key_bytes), signature.encode('binary'), signing_input.encode('binary'))
29
+ else
30
+ Hmac.verify(algorithm, key, signing_input, signature)
31
+ end
32
+ rescue ::RbNaCl::BadAuthenticatorError, ::RbNaCl::LengthError
33
+ false
34
+ end
35
+
36
+ def resolve_algorithm(algorithm)
37
+ MAPPING.fetch(algorithm)
38
+ end
39
+
40
+ def padded_key_bytes(key, bytesize)
41
+ key.bytes.fill(0, key.bytesize...bytesize).pack('C*')
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,13 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JWT
4
- module Algos
4
+ module JWA
5
5
  module None
6
6
  module_function
7
7
 
8
8
  SUPPORTED = %w[none].freeze
9
9
 
10
- def sign(*); end
10
+ def sign(*)
11
+ ''
12
+ end
11
13
 
12
14
  def verify(*)
13
15
  true
data/lib/jwt/jwa/ps.rb ADDED
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ module Ps
6
+ # RSASSA-PSS signing algorithms
7
+
8
+ module_function
9
+
10
+ SUPPORTED = %w[PS256 PS384 PS512].freeze
11
+
12
+ def sign(algorithm, msg, key)
13
+ unless key.is_a?(::OpenSSL::PKey::RSA)
14
+ raise EncodeError, "The given key is a #{key_class}. It has to be an OpenSSL::PKey::RSA instance."
15
+ end
16
+
17
+ translated_algorithm = algorithm.sub('PS', 'sha')
18
+
19
+ key.sign_pss(translated_algorithm, msg, salt_length: :digest, mgf1_hash: translated_algorithm)
20
+ end
21
+
22
+ def verify(algorithm, public_key, signing_input, signature)
23
+ translated_algorithm = algorithm.sub('PS', 'sha')
24
+ public_key.verify_pss(translated_algorithm, signature, signing_input, salt_length: :auto, mgf1_hash: translated_algorithm)
25
+ rescue OpenSSL::PKey::PKeyError
26
+ raise JWT::VerificationError, 'Signature verification raised'
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ module Rsa
6
+ module_function
7
+
8
+ SUPPORTED = %w[RS256 RS384 RS512].freeze
9
+
10
+ def sign(algorithm, msg, key)
11
+ unless key.is_a?(OpenSSL::PKey::RSA)
12
+ raise EncodeError, "The given key is a #{key.class}. It has to be an OpenSSL::PKey::RSA instance"
13
+ end
14
+
15
+ key.sign(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), msg)
16
+ end
17
+
18
+ def verify(algorithm, public_key, signing_input, signature)
19
+ public_key.verify(OpenSSL::Digest.new(algorithm.sub('RS', 'sha')), signature, signing_input)
20
+ rescue OpenSSL::PKey::PKeyError
21
+ raise JWT::VerificationError, 'Signature verification raised'
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JWT
4
- module Algos
4
+ module JWA
5
5
  module Unsupported
6
6
  module_function
7
7
 
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ class Wrapper
6
+ attr_reader :alg, :cls
7
+
8
+ def initialize(alg, cls)
9
+ @alg = alg
10
+ @cls = cls
11
+ end
12
+
13
+ def valid_alg?(alg_to_check)
14
+ alg&.casecmp(alg_to_check)&.zero? == true
15
+ end
16
+
17
+ def sign(data:, signing_key:)
18
+ cls.sign(alg, data, signing_key)
19
+ end
20
+
21
+ def verify(data:, signature:, verification_key:)
22
+ cls.verify(alg, verification_key, data, signature)
23
+ end
24
+ end
25
+ end
26
+ end
data/lib/jwt/jwa.rb ADDED
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'openssl'
4
+
5
+ begin
6
+ require 'rbnacl'
7
+ rescue LoadError
8
+ raise if defined?(RbNaCl)
9
+ end
10
+
11
+ require_relative 'jwa/hmac'
12
+ require_relative 'jwa/eddsa'
13
+ require_relative 'jwa/ecdsa'
14
+ require_relative 'jwa/rsa'
15
+ require_relative 'jwa/ps'
16
+ require_relative 'jwa/none'
17
+ require_relative 'jwa/unsupported'
18
+ require_relative 'jwa/wrapper'
19
+
20
+ module JWT
21
+ module JWA
22
+ ALGOS = [Hmac, Ecdsa, Rsa, Eddsa, Ps, None, Unsupported].tap do |l|
23
+ if ::JWT.rbnacl_6_or_greater?
24
+ require_relative 'jwa/hmac_rbnacl'
25
+ l << Algos::HmacRbNaCl
26
+ elsif ::JWT.rbnacl?
27
+ require_relative 'jwa/hmac_rbnacl_fixed'
28
+ l << Algos::HmacRbNaClFixed
29
+ end
30
+ end.freeze
31
+
32
+ class << self
33
+ def find(algorithm)
34
+ indexed[algorithm&.downcase]
35
+ end
36
+
37
+ def create(algorithm)
38
+ return algorithm if JWA.implementation?(algorithm)
39
+
40
+ Wrapper.new(*find(algorithm))
41
+ end
42
+
43
+ def implementation?(algorithm)
44
+ (algorithm.respond_to?(:valid_alg?) && algorithm.respond_to?(:verify)) ||
45
+ (algorithm.respond_to?(:alg) && algorithm.respond_to?(:sign))
46
+ end
47
+
48
+ private
49
+
50
+ def indexed
51
+ @indexed ||= begin
52
+ fallback = [nil, Unsupported]
53
+ ALGOS.each_with_object(Hash.new(fallback)) do |cls, hash|
54
+ cls.const_get(:SUPPORTED).each do |alg|
55
+ hash[alg.downcase] = [alg, cls]
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end