jwt 2.5.0 → 2.8.1

Sign up to get free protection for your applications and to get access to all the features.
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