jwt 2.4.1 → 2.9.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +177 -14
  3. data/CONTRIBUTING.md +7 -7
  4. data/README.md +180 -37
  5. data/lib/jwt/base64.rb +33 -0
  6. data/lib/jwt/claims/audience.rb +20 -0
  7. data/lib/jwt/claims/decode_verifier.rb +40 -0
  8. data/lib/jwt/claims/expiration.rb +22 -0
  9. data/lib/jwt/claims/issued_at.rb +15 -0
  10. data/lib/jwt/claims/issuer.rb +24 -0
  11. data/lib/jwt/claims/jwt_id.rb +25 -0
  12. data/lib/jwt/claims/not_before.rb +22 -0
  13. data/lib/jwt/claims/numeric.rb +55 -0
  14. data/lib/jwt/claims/required.rb +23 -0
  15. data/lib/jwt/claims/subject.rb +20 -0
  16. data/lib/jwt/claims/verifier.rb +62 -0
  17. data/lib/jwt/claims.rb +82 -0
  18. data/lib/jwt/claims_validator.rb +3 -24
  19. data/lib/jwt/configuration/container.rb +32 -0
  20. data/lib/jwt/configuration/decode_configuration.rb +46 -0
  21. data/lib/jwt/configuration/jwk_configuration.rb +27 -0
  22. data/lib/jwt/configuration.rb +15 -0
  23. data/lib/jwt/decode.rb +54 -41
  24. data/lib/jwt/deprecations.rb +48 -0
  25. data/lib/jwt/encode.rb +21 -21
  26. data/lib/jwt/error.rb +1 -0
  27. data/lib/jwt/jwa/compat.rb +29 -0
  28. data/lib/jwt/jwa/ecdsa.rb +93 -0
  29. data/lib/jwt/jwa/eddsa.rb +34 -0
  30. data/lib/jwt/jwa/hmac.rb +83 -0
  31. data/lib/jwt/jwa/hmac_rbnacl.rb +49 -0
  32. data/lib/jwt/jwa/hmac_rbnacl_fixed.rb +46 -0
  33. data/lib/jwt/jwa/none.rb +23 -0
  34. data/lib/jwt/jwa/ps.rb +36 -0
  35. data/lib/jwt/jwa/rsa.rb +36 -0
  36. data/lib/jwt/jwa/signing_algorithm.rb +60 -0
  37. data/lib/jwt/jwa/unsupported.rb +19 -0
  38. data/lib/jwt/jwa/wrapper.rb +43 -0
  39. data/lib/jwt/jwa.rb +50 -0
  40. data/lib/jwt/jwk/ec.rb +162 -65
  41. data/lib/jwt/jwk/hmac.rb +69 -24
  42. data/lib/jwt/jwk/key_base.rb +45 -7
  43. data/lib/jwt/jwk/key_finder.rb +19 -35
  44. data/lib/jwt/jwk/kid_as_key_digest.rb +15 -0
  45. data/lib/jwt/jwk/okp_rbnacl.rb +110 -0
  46. data/lib/jwt/jwk/rsa.rb +141 -54
  47. data/lib/jwt/jwk/set.rb +80 -0
  48. data/lib/jwt/jwk/thumbprint.rb +26 -0
  49. data/lib/jwt/jwk.rb +14 -11
  50. data/lib/jwt/verify.rb +10 -89
  51. data/lib/jwt/version.rb +24 -2
  52. data/lib/jwt/x5c_key_finder.rb +3 -6
  53. data/lib/jwt.rb +12 -4
  54. data/ruby-jwt.gemspec +11 -4
  55. metadata +59 -31
  56. data/.codeclimate.yml +0 -8
  57. data/.github/workflows/coverage.yml +0 -27
  58. data/.github/workflows/test.yml +0 -66
  59. data/.gitignore +0 -13
  60. data/.reek.yml +0 -22
  61. data/.rspec +0 -2
  62. data/.rubocop.yml +0 -67
  63. data/.sourcelevel.yml +0 -17
  64. data/Appraisals +0 -13
  65. data/Gemfile +0 -7
  66. data/Rakefile +0 -16
  67. data/lib/jwt/algos/ecdsa.rb +0 -64
  68. data/lib/jwt/algos/eddsa.rb +0 -33
  69. data/lib/jwt/algos/hmac.rb +0 -36
  70. data/lib/jwt/algos/none.rb +0 -17
  71. data/lib/jwt/algos/ps.rb +0 -43
  72. data/lib/jwt/algos/rsa.rb +0 -22
  73. data/lib/jwt/algos/unsupported.rb +0 -19
  74. data/lib/jwt/algos.rb +0 -44
  75. data/lib/jwt/default_options.rb +0 -18
  76. data/lib/jwt/security_utils.rb +0 -59
  77. data/lib/jwt/signature.rb +0 -35
data/lib/jwt/decode.rb CHANGED
@@ -1,16 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'json'
4
-
5
- require 'jwt/signature'
6
- require 'jwt/verify'
7
4
  require 'jwt/x5c_key_finder'
5
+
8
6
  # JWT::Decode module
9
7
  module JWT
10
8
  # Decoding logic for JWT
11
9
  class Decode
12
10
  def initialize(jwt, key, verify, options, &keyfinder)
13
- raise(JWT::DecodeError, 'Nil JSON web token') unless jwt
11
+ raise JWT::DecodeError, 'Nil JSON web token' unless jwt
14
12
 
15
13
  @jwt = jwt
16
14
  @key = key
@@ -24,13 +22,13 @@ module JWT
24
22
  def decode_segments
25
23
  validate_segment_count!
26
24
  if @verify
27
- decode_crypto
25
+ decode_signature
28
26
  verify_algo
29
27
  set_key
30
28
  verify_signature
31
29
  verify_claims
32
30
  end
33
- raise(JWT::DecodeError, 'Not enough or too many segments') unless header && payload
31
+ raise JWT::DecodeError, 'Not enough or too many segments' unless header && payload
34
32
 
35
33
  [payload, header]
36
34
  end
@@ -46,45 +44,63 @@ module JWT
46
44
 
47
45
  return if Array(@key).any? { |key| verify_signature_for?(key) }
48
46
 
49
- raise(JWT::VerificationError, 'Signature verification failed')
47
+ raise JWT::VerificationError, 'Signature verification failed'
50
48
  end
51
49
 
52
50
  def verify_algo
53
- raise(JWT::IncorrectAlgorithm, 'An algorithm must be specified') if allowed_algorithms.empty?
54
- raise(JWT::IncorrectAlgorithm, 'Token is missing alg header') unless algorithm
55
- raise(JWT::IncorrectAlgorithm, 'Expected a different algorithm') unless options_includes_algo_in_header?
51
+ raise JWT::IncorrectAlgorithm, 'An algorithm must be specified' if allowed_algorithms.empty?
52
+ raise JWT::DecodeError, 'Token header not a JSON object' unless header.is_a?(Hash)
53
+ raise JWT::IncorrectAlgorithm, 'Token is missing alg header' unless alg_in_header
54
+ raise JWT::IncorrectAlgorithm, 'Expected a different algorithm' if allowed_and_valid_algorithms.empty?
56
55
  end
57
56
 
58
57
  def set_key
59
58
  @key = find_key(&@keyfinder) if @keyfinder
60
- @key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks]).key_for(header['kid']) if @options[:jwks]
61
- if (x5c_options = @options[:x5c])
62
- @key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(header['x5c'])
63
- end
59
+ @key = ::JWT::JWK::KeyFinder.new(jwks: @options[:jwks], allow_nil_kid: @options[:allow_nil_kid]).key_for(header['kid']) if @options[:jwks]
60
+ return unless (x5c_options = @options[:x5c])
61
+
62
+ @key = X5cKeyFinder.new(x5c_options[:root_certificates], x5c_options[:crls]).from(header['x5c'])
64
63
  end
65
64
 
66
65
  def verify_signature_for?(key)
67
- Signature.verify(algorithm, key, signing_input, @signature)
66
+ allowed_and_valid_algorithms.any? do |alg|
67
+ alg.verify(data: signing_input, signature: @signature, verification_key: key)
68
+ end
68
69
  end
69
70
 
70
- def options_includes_algo_in_header?
71
- allowed_algorithms.any? { |alg| alg.casecmp(algorithm).zero? }
71
+ def allowed_and_valid_algorithms
72
+ @allowed_and_valid_algorithms ||= allowed_algorithms.select { |alg| alg.valid_alg?(alg_in_header) }
72
73
  end
73
74
 
74
- def allowed_algorithms
75
- # Order is very important - first check for string keys, next for symbols
76
- algos = if @options.key?('algorithm')
77
- @options['algorithm']
78
- elsif @options.key?(:algorithm)
79
- @options[:algorithm]
80
- elsif @options.key?('algorithms')
81
- @options['algorithms']
82
- elsif @options.key?(:algorithms)
83
- @options[:algorithms]
84
- else
85
- []
75
+ # Order is very important - first check for string keys, next for symbols
76
+ ALGORITHM_KEYS = ['algorithm',
77
+ :algorithm,
78
+ 'algorithms',
79
+ :algorithms].freeze
80
+
81
+ def given_algorithms
82
+ ALGORITHM_KEYS.each do |alg_key|
83
+ alg = @options[alg_key]
84
+ return Array(alg) if alg
86
85
  end
87
- Array(algos)
86
+ []
87
+ end
88
+
89
+ def allowed_algorithms
90
+ @allowed_algorithms ||= resolve_allowed_algorithms
91
+ end
92
+
93
+ def resolve_allowed_algorithms
94
+ algs = given_algorithms.map { |alg| JWA.resolve(alg) }
95
+
96
+ sort_by_alg_header(algs)
97
+ end
98
+
99
+ # Move algorithms matching the JWT alg header to the beginning of the list
100
+ def sort_by_alg_header(algs)
101
+ return algs if algs.size <= 1
102
+
103
+ algs.partition { |alg| alg.valid_alg?(alg_in_header) }.flatten
88
104
  end
89
105
 
90
106
  def find_key(&keyfinder)
@@ -96,8 +112,7 @@ module JWT
96
112
  end
97
113
 
98
114
  def verify_claims
99
- Verify.verify_claims(payload, @options)
100
- Verify.verify_required_claims(payload, @options)
115
+ Claims::DecodeVerifier.verify!(payload, @options)
101
116
  end
102
117
 
103
118
  def validate_segment_count!
@@ -105,7 +120,7 @@ module JWT
105
120
  return if !@verify && segment_length == 2 # If no verifying required, the signature is not needed
106
121
  return if segment_length == 2 && none_algorithm?
107
122
 
108
- raise(JWT::DecodeError, 'Not enough or too many segments')
123
+ raise JWT::DecodeError, 'Not enough or too many segments'
109
124
  end
110
125
 
111
126
  def segment_length
@@ -113,16 +128,14 @@ module JWT
113
128
  end
114
129
 
115
130
  def none_algorithm?
116
- algorithm.casecmp('none').zero?
131
+ alg_in_header == 'none'
117
132
  end
118
133
 
119
- def decode_crypto
120
- @signature = Base64.urlsafe_decode64(@segments[2] || '')
121
- rescue ArgumentError
122
- raise(JWT::DecodeError, 'Invalid segment encoding')
134
+ def decode_signature
135
+ @signature = ::JWT::Base64.url_decode(@segments[2] || '')
123
136
  end
124
137
 
125
- def algorithm
138
+ def alg_in_header
126
139
  header['alg']
127
140
  end
128
141
 
@@ -139,8 +152,8 @@ module JWT
139
152
  end
140
153
 
141
154
  def parse_and_decode(segment)
142
- JWT::JSON.parse(Base64.urlsafe_decode64(segment))
143
- rescue ::JSON::ParserError, ArgumentError
155
+ JWT::JSON.parse(::JWT::Base64.url_decode(segment))
156
+ rescue ::JSON::ParserError
144
157
  raise JWT::DecodeError, 'Invalid segment encoding'
145
158
  end
146
159
  end
@@ -0,0 +1,48 @@
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 context
8
+ yield.tap { emit_warnings }
9
+ ensure
10
+ Thread.current[:jwt_warning_store] = nil
11
+ end
12
+
13
+ def warning(message, only_if_valid: false)
14
+ method_name = only_if_valid ? :store : :warn
15
+ case JWT.configuration.deprecation_warnings
16
+ when :once
17
+ return if record_warned(message)
18
+ when :warn
19
+ # noop
20
+ else
21
+ return
22
+ end
23
+
24
+ send(method_name, "[DEPRECATION WARNING] #{message}")
25
+ end
26
+
27
+ def store(message)
28
+ (Thread.current[:jwt_warning_store] ||= []) << message
29
+ end
30
+
31
+ def emit_warnings
32
+ return if Thread.current[:jwt_warning_store].nil?
33
+
34
+ Thread.current[:jwt_warning_store].each { |warning| warn(warning) }
35
+ end
36
+
37
+ private
38
+
39
+ def record_warned(message)
40
+ @warned ||= []
41
+ return true if @warned.include?(message)
42
+
43
+ @warned << message
44
+ false
45
+ end
46
+ end
47
+ end
48
+ end
data/lib/jwt/encode.rb CHANGED
@@ -1,24 +1,21 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative './algos'
4
- require_relative './claims_validator'
3
+ require_relative 'jwa'
5
4
 
6
5
  # JWT::Encode module
7
6
  module JWT
8
7
  # Encoding logic for JWT
9
8
  class Encode
10
- ALG_NONE = 'none'
11
- ALG_KEY = 'alg'
12
-
13
9
  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)
10
+ @payload = options[:payload]
11
+ @key = options[:key]
12
+ @algorithm = JWA.resolve(options[:algorithm])
13
+ @headers = options[:headers].transform_keys(&:to_s)
18
14
  end
19
15
 
20
16
  def segments
21
- @segments ||= combine(encoded_header_and_payload, encoded_signature)
17
+ validate_claims!
18
+ combine(encoded_header_and_payload, encoded_signature)
22
19
  end
23
20
 
24
21
  private
@@ -40,26 +37,29 @@ module JWT
40
37
  end
41
38
 
42
39
  def encode_header
43
- @headers[ALG_KEY] = @algorithm
44
- encode(@headers)
40
+ encode_data(@headers.merge(@algorithm.header(signing_key: @key)))
45
41
  end
46
42
 
47
43
  def encode_payload
48
- if @payload.is_a?(Hash)
49
- ClaimsValidator.new(@payload).validate!
50
- end
44
+ encode_data(@payload)
45
+ end
51
46
 
52
- encode(@payload)
47
+ def signature
48
+ @algorithm.sign(data: encoded_header_and_payload, signing_key: @key)
53
49
  end
54
50
 
55
- def encode_signature
56
- return '' if @algorithm == ALG_NONE
51
+ def validate_claims!
52
+ return unless @payload.is_a?(Hash)
57
53
 
58
- Base64.urlsafe_encode64(JWT::Signature.sign(@algorithm, encoded_header_and_payload, @key), padding: false)
54
+ Claims.verify_payload!(@payload, :numeric)
55
+ end
56
+
57
+ def encode_signature
58
+ ::JWT::Base64.url_encode(signature)
59
59
  end
60
60
 
61
- def encode(data)
62
- Base64.urlsafe_encode64(JWT::JSON.generate(data), padding: false)
61
+ def encode_data(data)
62
+ ::JWT::Base64.url_encode(JWT::JSON.generate(data))
63
63
  end
64
64
 
65
65
  def combine(*parts)
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
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ module Compat
6
+ module ClassMethods
7
+ def from_algorithm(algorithm)
8
+ new(algorithm)
9
+ end
10
+
11
+ def sign(algorithm, msg, key)
12
+ Deprecations.warning('Support for calling sign with positional arguments will be removed in future ruby-jwt versions')
13
+
14
+ from_algorithm(algorithm).sign(data: msg, signing_key: key)
15
+ end
16
+
17
+ def verify(algorithm, key, signing_input, signature)
18
+ Deprecations.warning('Support for calling verify with positional arguments will be removed in future ruby-jwt versions')
19
+
20
+ from_algorithm(algorithm).verify(data: signing_input, signature: signature, verification_key: key)
21
+ end
22
+ end
23
+
24
+ def self.included(klass)
25
+ klass.extend(ClassMethods)
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ class Ecdsa
6
+ include JWT::JWA::SigningAlgorithm
7
+
8
+ def initialize(alg, digest)
9
+ @alg = alg
10
+ @digest = OpenSSL::Digest.new(digest)
11
+ end
12
+
13
+ def sign(data:, signing_key:)
14
+ curve_definition = curve_by_name(signing_key.group.curve_name)
15
+ key_algorithm = curve_definition[:algorithm]
16
+ if alg != key_algorithm
17
+ raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} signing key was provided"
18
+ end
19
+
20
+ asn1_to_raw(signing_key.dsa_sign_asn1(digest.digest(data)), signing_key)
21
+ end
22
+
23
+ def verify(data:, signature:, verification_key:)
24
+ curve_definition = curve_by_name(verification_key.group.curve_name)
25
+ key_algorithm = curve_definition[:algorithm]
26
+ if alg != key_algorithm
27
+ raise IncorrectAlgorithm, "payload algorithm is #{alg} but #{key_algorithm} verification key was provided"
28
+ end
29
+
30
+ verification_key.dsa_verify_asn1(digest.digest(data), raw_to_asn1(signature, verification_key))
31
+ rescue OpenSSL::PKey::PKeyError
32
+ raise JWT::VerificationError, 'Signature verification raised'
33
+ end
34
+
35
+ NAMED_CURVES = {
36
+ 'prime256v1' => {
37
+ algorithm: 'ES256',
38
+ digest: 'sha256'
39
+ },
40
+ 'secp256r1' => { # alias for prime256v1
41
+ algorithm: 'ES256',
42
+ digest: 'sha256'
43
+ },
44
+ 'secp384r1' => {
45
+ algorithm: 'ES384',
46
+ digest: 'sha384'
47
+ },
48
+ 'secp521r1' => {
49
+ algorithm: 'ES512',
50
+ digest: 'sha512'
51
+ },
52
+ 'secp256k1' => {
53
+ algorithm: 'ES256K',
54
+ digest: 'sha256'
55
+ }
56
+ }.freeze
57
+
58
+ NAMED_CURVES.each_value do |v|
59
+ register_algorithm(new(v[:algorithm], v[:digest]))
60
+ end
61
+
62
+ def self.from_algorithm(algorithm)
63
+ new(algorithm, algorithm.downcase.gsub('es', 'sha'))
64
+ end
65
+
66
+ def self.curve_by_name(name)
67
+ NAMED_CURVES.fetch(name) do
68
+ raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported"
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ attr_reader :digest
75
+
76
+ def curve_by_name(name)
77
+ self.class.curve_by_name(name)
78
+ end
79
+
80
+ def raw_to_asn1(signature, private_key)
81
+ byte_size = (private_key.group.degree + 7) / 8
82
+ sig_bytes = signature[0..(byte_size - 1)]
83
+ sig_char = signature[byte_size..-1] || ''
84
+ OpenSSL::ASN1::Sequence.new([sig_bytes, sig_char].map { |int| OpenSSL::ASN1::Integer.new(OpenSSL::BN.new(int, 2)) }).to_der
85
+ end
86
+
87
+ def asn1_to_raw(signature, public_key)
88
+ byte_size = (public_key.group.degree + 7) / 8
89
+ OpenSSL::ASN1.decode(signature).value.map { |value| value.value.to_s(2).rjust(byte_size, "\x00") }.join
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ class Eddsa
6
+ include JWT::JWA::SigningAlgorithm
7
+
8
+ def initialize(alg)
9
+ @alg = alg
10
+ end
11
+
12
+ def sign(data:, signing_key:)
13
+ unless signing_key.is_a?(RbNaCl::Signatures::Ed25519::SigningKey)
14
+ raise_encode_error!("Key given is a #{signing_key.class} but has to be an RbNaCl::Signatures::Ed25519::SigningKey")
15
+ end
16
+
17
+ signing_key.sign(data)
18
+ end
19
+
20
+ def verify(data:, signature:, verification_key:)
21
+ unless verification_key.is_a?(RbNaCl::Signatures::Ed25519::VerifyKey)
22
+ raise_decode_error!("key given is a #{verification_key.class} but has to be a RbNaCl::Signatures::Ed25519::VerifyKey")
23
+ end
24
+
25
+ verification_key.verify(signature, data)
26
+ rescue RbNaCl::CryptoError
27
+ false
28
+ end
29
+
30
+ register_algorithm(new('ED25519'))
31
+ register_algorithm(new('EdDSA'))
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ class Hmac
6
+ include JWT::JWA::SigningAlgorithm
7
+
8
+ def self.from_algorithm(algorithm)
9
+ new(algorithm, OpenSSL::Digest.new(algorithm.downcase.gsub('hs', 'sha')))
10
+ end
11
+
12
+ def initialize(alg, digest)
13
+ @alg = alg
14
+ @digest = digest
15
+ end
16
+
17
+ def sign(data:, signing_key:)
18
+ signing_key ||= ''
19
+ raise_verify_error!('HMAC key expected to be a String') unless signing_key.is_a?(String)
20
+
21
+ OpenSSL::HMAC.digest(digest.new, signing_key, data)
22
+ rescue OpenSSL::HMACError => e
23
+ if signing_key == '' && e.message == 'EVP_PKEY_new_mac_key: malloc failure'
24
+ raise_verify_error!('OpenSSL 3.0 does not support nil or empty hmac_secret')
25
+ end
26
+
27
+ raise e
28
+ end
29
+
30
+ def verify(data:, signature:, verification_key:)
31
+ SecurityUtils.secure_compare(signature, sign(data: data, signing_key: verification_key))
32
+ end
33
+
34
+ register_algorithm(new('HS256', OpenSSL::Digest::SHA256))
35
+ register_algorithm(new('HS384', OpenSSL::Digest::SHA384))
36
+ register_algorithm(new('HS512', OpenSSL::Digest::SHA512))
37
+
38
+ private
39
+
40
+ attr_reader :digest
41
+
42
+ # Copy of https://github.com/rails/rails/blob/v7.0.3.1/activesupport/lib/active_support/security_utils.rb
43
+ # rubocop:disable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
44
+ module SecurityUtils
45
+ # Constant time string comparison, for fixed length strings.
46
+ #
47
+ # The values compared should be of fixed length, such as strings
48
+ # that have already been processed by HMAC. Raises in case of length mismatch.
49
+
50
+ if defined?(OpenSSL.fixed_length_secure_compare)
51
+ def fixed_length_secure_compare(a, b)
52
+ OpenSSL.fixed_length_secure_compare(a, b)
53
+ end
54
+ else
55
+ # :nocov:
56
+ def fixed_length_secure_compare(a, b)
57
+ raise ArgumentError, "string length mismatch." unless a.bytesize == b.bytesize
58
+
59
+ l = a.unpack "C#{a.bytesize}"
60
+
61
+ res = 0
62
+ b.each_byte { |byte| res |= byte ^ l.shift }
63
+ res == 0
64
+ end
65
+ # :nocov:
66
+ end
67
+ module_function :fixed_length_secure_compare
68
+
69
+ # Secure string comparison for strings of variable length.
70
+ #
71
+ # While a timing attack would not be able to discern the content of
72
+ # a secret compared via secure_compare, it is possible to determine
73
+ # the secret length. This should be considered when using secure_compare
74
+ # to compare weak, short secrets to user input.
75
+ def secure_compare(a, b)
76
+ a.bytesize == b.bytesize && fixed_length_secure_compare(a, b)
77
+ end
78
+ module_function :secure_compare
79
+ end
80
+ # rubocop:enable Naming/MethodParameterName, Style/StringLiterals, Style/NumericPredicate
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ class HmacRbNaCl
6
+ include JWT::JWA::SigningAlgorithm
7
+
8
+ def self.from_algorithm(algorithm)
9
+ new(algorithm, ::RbNaCl::HMAC.const_get(algorithm.upcase.gsub('HS', 'SHA')))
10
+ end
11
+
12
+ def initialize(alg, hmac)
13
+ @alg = alg
14
+ @hmac = hmac
15
+ end
16
+
17
+ def sign(data:, signing_key:)
18
+ Deprecations.warning("The use of the algorithm #{alg} is deprecated and will be removed in the next major version of ruby-jwt")
19
+ hmac.auth(key_for_rbnacl(hmac, signing_key).encode('binary'), data.encode('binary'))
20
+ end
21
+
22
+ def verify(data:, signature:, verification_key:)
23
+ Deprecations.warning("The use of the algorithm #{alg} is deprecated and will be removed in the next major version of ruby-jwt")
24
+ hmac.verify(key_for_rbnacl(hmac, verification_key).encode('binary'), signature.encode('binary'), data.encode('binary'))
25
+ rescue ::RbNaCl::BadAuthenticatorError, ::RbNaCl::LengthError
26
+ false
27
+ end
28
+
29
+ register_algorithm(new('HS512256', ::RbNaCl::HMAC::SHA512256))
30
+
31
+ private
32
+
33
+ attr_reader :hmac
34
+
35
+ def key_for_rbnacl(hmac, key)
36
+ key ||= ''
37
+ raise JWT::DecodeError, 'HMAC key expected to be a String' unless key.is_a?(String)
38
+
39
+ return padded_empty_key(hmac.key_bytes) if key == ''
40
+
41
+ key
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
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ class HmacRbNaClFixed
6
+ include JWT::JWA::SigningAlgorithm
7
+
8
+ def self.from_algorithm(algorithm)
9
+ new(algorithm, ::RbNaCl::HMAC.const_get(algorithm.upcase.gsub('HS', 'SHA')))
10
+ end
11
+
12
+ def initialize(alg, hmac)
13
+ @alg = alg
14
+ @hmac = hmac
15
+ end
16
+
17
+ def sign(data:, signing_key:)
18
+ signing_key ||= ''
19
+ Deprecations.warning("The use of the algorithm #{alg} is deprecated and will be removed in the next major version of ruby-jwt")
20
+ raise JWT::DecodeError, 'HMAC key expected to be a String' unless signing_key.is_a?(String)
21
+
22
+ hmac.auth(padded_key_bytes(signing_key, hmac.key_bytes), data.encode('binary'))
23
+ end
24
+
25
+ def verify(data:, signature:, verification_key:)
26
+ verification_key ||= ''
27
+ Deprecations.warning("The use of the algorithm #{alg} is deprecated and will be removed in the next major version of ruby-jwt")
28
+ raise JWT::DecodeError, 'HMAC key expected to be a String' unless verification_key.is_a?(String)
29
+
30
+ hmac.verify(padded_key_bytes(verification_key, hmac.key_bytes), signature.encode('binary'), data.encode('binary'))
31
+ rescue ::RbNaCl::BadAuthenticatorError, ::RbNaCl::LengthError
32
+ false
33
+ end
34
+
35
+ register_algorithm(new('HS512256', ::RbNaCl::HMAC::SHA512256))
36
+
37
+ private
38
+
39
+ attr_reader :hmac
40
+
41
+ def padded_key_bytes(key, bytesize)
42
+ key.bytes.fill(0, key.bytesize...bytesize).pack('C*')
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JWT
4
+ module JWA
5
+ class None
6
+ include JWT::JWA::SigningAlgorithm
7
+
8
+ def initialize
9
+ @alg = 'none'
10
+ end
11
+
12
+ def sign(*)
13
+ ''
14
+ end
15
+
16
+ def verify(*)
17
+ true
18
+ end
19
+
20
+ register_algorithm(new)
21
+ end
22
+ end
23
+ end