ey-hmac 2.1.0 → 2.4.0

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.
@@ -1,12 +1,22 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # This class is responsible for forming the canonical string to used to sign requests
2
4
  # @abstract override methods {#method}, {#path}, {#body}, {#content_type} and {#content_digest}
3
5
  class Ey::Hmac::Adapter
4
- AUTHORIZATION_REGEXP = /\w+ ([^:]+):(.+)$/
6
+ AUTHORIZATION_REGEXP = /\w+ ([^:]+):(.+)$/.freeze
7
+ DEFAULT_CANONICALIZE_WITH = %i[method content_type content_digest date path].freeze
5
8
 
6
- autoload :Rack, "ey-hmac/adapter/rack"
7
- autoload :Faraday, "ey-hmac/adapter/faraday"
9
+ autoload :Rack, 'ey-hmac/adapter/rack'
10
+ autoload :Faraday, 'ey-hmac/adapter/faraday'
8
11
 
9
- attr_reader :request, :options, :authorization_header, :service, :sign_with, :accept_digests
12
+ attr_reader :request,
13
+ :options,
14
+ :authorization_header,
15
+ :service,
16
+ :sign_with,
17
+ :accept_digests,
18
+ :include_query_string,
19
+ :canonicalize_with
10
20
 
11
21
  # @param [Object] request signer-specific request implementation
12
22
  # @option options [Integer] :version signature version
@@ -14,15 +24,21 @@ class Ey::Hmac::Adapter
14
24
  # @option options [String] :authorization_header ('Authorization') Authorization header key.
15
25
  # @option options [String] :server ('EyHmac') service name prefixed to {#authorization}. set to {#service}
16
26
  # @option options [Symbol] :sign_with (:sha_256) outgoing signature digest algorithm. See {OpenSSL::Digest#new}
27
+ # @option options [Symbol] :include_query_string (false) canonicalize with the request query string.
17
28
  # @option options [Array] :accepted_digests ([:sha_256]) accepted incoming signature digest algorithm. See {OpenSSL::Digest#new}
18
- def initialize(request, options={})
19
- @request, @options = request, options
29
+ def initialize(request, options = {})
30
+ @request = request
31
+ @options = options
20
32
 
21
33
  @ttl = options[:ttl]
22
34
  @authorization_header = options[:authorization_header] || 'Authorization'
23
35
  @service = options[:service] || 'EyHmac'
24
36
  @sign_with = options[:sign_with] || :sha256
25
- @accept_digests = Array(options[:accept_digests] || :sha256)
37
+ @include_query_string = options.fetch(:include_query_string, false)
38
+ @accept_digests = Array(options[:accept_digests] || :sha256)
39
+
40
+ @canonicalize_with = DEFAULT_CANONICALIZE_WITH
41
+ @canonicalize_with += :query_string if include_query_string
26
42
  end
27
43
 
28
44
  # In order for the server to correctly authorize the request, the client and server MUST AGREE on this format
@@ -30,14 +46,18 @@ class Ey::Hmac::Adapter
30
46
  # default canonical string formation is '{#method}\\n{#content_type}\\n{#content_digest}\\n{#date}\\n{#path}'
31
47
  # @return [String] canonical string used to form the {#signature}
32
48
  def canonicalize
33
- [method, content_type, content_digest, date, path].join("\n")
49
+ canonicalize_with.map { |message| public_send(message) }.join("\n")
34
50
  end
35
51
 
36
52
  # @param [String] key_secret private HMAC key
37
53
  # @param [String] signature digest hash function. Defaults to #sign_with
38
54
  # @return [String] HMAC signature of {#request}
39
- def signature(key_secret, digest = self.sign_with)
40
- Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest.new(digest.to_s), key_secret, canonicalize)).strip
55
+ def signature(key_secret, digest = sign_with)
56
+ Base64.strict_encode64(
57
+ OpenSSL::HMAC.digest(
58
+ OpenSSL::Digest.new(digest.to_s), key_secret, canonicalize
59
+ )
60
+ ).strip
41
61
  end
42
62
 
43
63
  # @param [String] key_id public HMAC key
@@ -104,7 +124,7 @@ class Ey::Hmac::Adapter
104
124
  # @yieldparam key_id [String] public HMAC key
105
125
  # @return [Boolean] true if block yields matching private key and signature matches, else false
106
126
  # @see #authenticated!
107
- def authenticated?(options={}, &block)
127
+ def authenticated?(_options = {}, &block)
108
128
  authenticated!(&block)
109
129
  rescue Ey::Hmac::Error
110
130
  false
@@ -112,43 +132,63 @@ class Ey::Hmac::Adapter
112
132
 
113
133
  # @see Ey::Hmac#authenticate!
114
134
  def authenticated!(&block)
115
- unless authorization_match = AUTHORIZATION_REGEXP.match(authorization_signature)
116
- raise(Ey::Hmac::MissingAuthorization, "Failed to parse authorization_signature #{authorization_signature}")
117
- end
135
+ key_id, signature_value = check_signature!
136
+ key_secret = block.call(key_id)
118
137
 
119
- key_id = authorization_match[1]
120
- signature_value = authorization_match[2]
121
-
122
- unless key_secret = block.call(key_id)
123
- raise(Ey::Hmac::MissingSecret, "Failed to find secret matching #{key_id.inspect}")
138
+ unless key_secret
139
+ raise Ey::Hmac::MissingSecret,
140
+ "Failed to find secret matching #{key_id.inspect}"
124
141
  end
125
142
 
126
- unless @ttl.nil?
127
- expiry = Time.parse(date).to_i + @ttl
128
- current_time = Time.now.to_i
129
- unless expiry > current_time
130
- raise(Ey::Hmac::ExpiredHmac, "Signature has expired passed #{expiry}. Current time is #{current_time}")
131
- end
132
- end
143
+ check_ttl!
133
144
 
134
- calculated_signatures = self.accept_digests.map { |ad| signature(key_secret, ad) }
145
+ matching_signature =
146
+ accept_digests
147
+ .lazy
148
+ .map { |ad| signature(key_secret, ad) }
149
+ .any? { |cs| secure_compare(signature_value, cs) }
150
+
151
+ raise Ey::Hmac::SignatureMismatch unless matching_signature
135
152
 
136
- unless calculated_signatures.any? { |cs| secure_compare(signature_value, cs) }
137
- raise(Ey::Hmac::SignatureMismatch, "Calculated signature #{signature_value} does not match #{calculated_signatures.inspect} using #{canonicalize.inspect}")
138
- end
139
153
  true
140
154
  end
141
155
  alias authenticate! authenticated!
142
156
 
157
+ protected
158
+
143
159
  # Constant time string comparison.
144
160
  # pulled from https://github.com/rack/rack/blob/master/lib/rack/utils.rb#L399
145
161
  def secure_compare(a, b)
146
162
  return false unless a.bytesize == b.bytesize
147
163
 
148
- l = a.unpack("C*")
164
+ l = a.unpack('C*')
165
+
166
+ r = 0
167
+ i = -1
168
+ b.each_byte { |v| r |= v ^ l[i += 1] }
169
+ r.zero?
170
+ end
171
+
172
+ def check_ttl!
173
+ if @ttl && date
174
+ expiry = Time.parse(date).to_i + @ttl
175
+ current_time = Time.now.to_i
176
+
177
+ unless expiry > current_time
178
+ raise Ey::Hmac::ExpiredHmac,
179
+ "Signature has expired passed #{expiry}. Current time is #{current_time}"
180
+ end
181
+ end
182
+ end
183
+
184
+ def check_signature!
185
+ authorization_match = AUTHORIZATION_REGEXP.match(authorization_signature)
186
+
187
+ unless authorization_match
188
+ raise Ey::Hmac::MissingAuthorization,
189
+ "Failed to parse authorization_signature #{authorization_signature}"
190
+ end
149
191
 
150
- r, i = 0, -1
151
- b.each_byte { |v| r |= v ^ l[i+=1] }
152
- r == 0
192
+ [authorization_match[1], authorization_match[2]]
153
193
  end
154
194
  end
@@ -1,21 +1,24 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'ey-hmac'
2
4
  require 'faraday'
3
5
 
4
- class Ey::Hmac::Faraday < Faraday::Response::Middleware
5
- dependency("ey-hmac")
6
+ class Ey::Hmac::Faraday < Faraday::Middleware
7
+ dependency('ey-hmac') if respond_to?(:dependency)
6
8
 
7
9
  attr_reader :key_id, :key_secret, :options
8
10
 
9
11
  def initialize(app, key_id, key_secret, options = {})
10
12
  super(app)
11
- @key_id, @key_secret = key_id, key_secret
13
+ @key_id = key_id
14
+ @key_secret = key_secret
12
15
  @options = options
13
16
  end
14
17
 
15
18
  def call(env)
16
- Ey::Hmac.sign!(env, key_id, key_secret, {adapter: Ey::Hmac::Adapter::Faraday}.merge(options))
19
+ Ey::Hmac.sign!(env, key_id, key_secret, { adapter: Ey::Hmac::Adapter::Faraday }.merge(options))
17
20
  @app.call(env)
18
21
  end
19
22
  end
20
23
 
21
- Faraday::Middleware.register_middleware :hmac => Ey::Hmac::Faraday
24
+ Faraday::Middleware.register_middleware hmac: Ey::Hmac::Faraday
data/lib/ey-hmac/rack.rb CHANGED
@@ -1,10 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  # Request middleware that performs HMAC request signing
2
4
  class Ey::Hmac::Rack
3
5
  attr_reader :key_id, :key_secret, :options
4
6
 
5
7
  def initialize(app, key_id, key_secret, options = {})
6
8
  @app = app
7
- @key_id, @key_secret = key_id, key_secret
9
+ @key_id = key_id
10
+ @key_secret = key_secret
8
11
  @options = options
9
12
  end
10
13
 
@@ -1,5 +1,7 @@
1
- module Ey
1
+ # frozen_string_literal: true
2
+
3
+ module Ey # rubocop:disable Style/ClassAndModuleChildren
2
4
  module Hmac
3
- VERSION = "2.1.0"
5
+ VERSION = '2.4.0'
4
6
  end
5
7
  end
data/lib/ey-hmac.rb CHANGED
@@ -1,103 +1,102 @@
1
- require "ey-hmac/version"
1
+ # frozen_string_literal: true
2
+
3
+ require 'ey-hmac/version'
2
4
 
3
5
  require 'base64'
4
6
  require 'digest/md5'
5
7
  require 'openssl'
6
8
  require 'time'
7
9
 
8
- module Ey
9
- module Hmac
10
- Error = Class.new(StandardError)
11
-
12
- MissingSecret = Class.new(Error)
13
- MissingAuthorization = Class.new(Error)
14
- SignatureMismatch = Class.new(Error)
15
- ExpiredHmac = Class.new(Error)
16
-
17
- autoload :Adapter, "ey-hmac/adapter"
18
- autoload :Faraday, "ey-hmac/faraday"
19
- autoload :Rack, "ey-hmac/rack"
20
-
21
- def self.default_adapter=(default_adapter)
22
- @default_adapter = default_adapter
23
- end
24
-
25
- def self.default_adapter
26
- @default_adapter ||= begin
27
- if defined?(Rack) || defined?(Rails)
28
- Ey::Hmac::Adapter::Rack
29
- elsif defined?(Faraday)
30
- Ey::Hmac::Adapter::Rails
31
- end
32
- end
33
- end
34
-
35
- # Signs request by calculating signature and adding it to the specified header
36
- # @example
37
- # Ey::Hmac.sign!(env, @key_id, @key_secret)
38
- #
39
- # @see Ey::Hmac::Adapter#sign!
40
- #
41
- # @param request [Hash] request environment
42
- # @option options [Ey::Hmac::Adapter] :adapter (#{default_adapter}) adapter to sign request with
43
- # @option options [Integer] :version (nil) signature version
44
- # @option options [String] :authorization_header ('Authorization') Authorization header key.
45
- # @option options [String] :service ('EyHmac') service name prefixed to {Ey::Hmac::Adapter#authorization}
46
- #
47
- # @return [String] authorization signature
48
- def self.sign!(request, key_id, key_secret, options={})
49
- adapter = options[:adapter] || Ey::Hmac.default_adapter
50
-
51
- raise ArgumentError, "Missing adapter and Ey::Hmac.default_adapter" unless adapter
52
-
53
- adapter.new(request, options).sign!(key_id, key_secret)
54
- end
55
-
56
- # @example
57
- # Ey::Hmac.authenticated? do |key_id|
58
- # @consumer = Consumer.where(auth_id: key_id).first
59
- # @consumer && @consumer.auth_key
60
- # end
61
- #
62
- # @see Ey::Hmac::Adapter#authenticated?
63
- # @see Ey::Hmac#authenticate!
64
- #
65
- # @param request [Hash] request environment
66
- # @option options [Ey::Hmac::Adapter] :adapter ({#default_adapter}) adapter to verify request with
67
- # @yieldparam key_id [String] public HMAC key
68
- #
69
- # @return [Boolean] success of authentication
70
- def self.authenticated?(request, options={}, &block)
71
- adapter = options[:adapter] || Ey::Hmac.default_adapter
72
-
73
- raise ArgumentError, "Missing adapter and Ey::Hmac.default_adapter" unless adapter
74
-
75
- adapter.new(request, options).authenticated?(&block)
76
- end
77
-
78
- # Check {Ey::Hmac::Adapter#authorization_signature} against calculated {Ey::Hmac::Adapter#signature}
79
- # @example
80
- # Ey::Hmac.authenticate! do |key_id|
81
- # @consumer = Consumer.where(auth_id: key_id).first
82
- # @consumer && @consumer.auth_key
83
- # end
84
- #
85
- # @see Ey::Hmac::Adapter#authenticate!
86
- #
87
- # @param request [Hash] request environment
88
- # @yieldparam key_id [String] public HMAC key
89
- # @option options [Ey::Hmac::Adapter] :adapter ({#default_adapter}) adapter to verify request with
90
- #
91
- # @raise [SignatureMismatch] if the value of {Ey::Hmac::Adapter#authorization_signature} does not match {Ey::Hmac::Adapter#signature}
92
- # @raise [MissingSecret] if the block does not return a private key matching +key_id+
93
- # @raise [MissingAuthorization] if the value of {Ey::Hmac::Adapter#authorization_signature} is nil
94
- # @return [TrueClass] if authentication was successful
95
- def self.authenticate!(request, options={}, &block)
96
- adapter = options[:adapter] || Ey::Hmac.default_adapter
97
-
98
- raise ArgumentError, "Missing adapter and Ey::Hmac.default_adapter" unless adapter
99
-
100
- adapter.new(request, options).authenticate!(&block)
101
- end
10
+ module Ey::Hmac
11
+ Error = Class.new(StandardError)
12
+
13
+ MissingSecret = Class.new(Error)
14
+ MissingAuthorization = Class.new(Error)
15
+ SignatureMismatch = Class.new(Error)
16
+ ExpiredHmac = Class.new(Error)
17
+
18
+ autoload :Adapter, 'ey-hmac/adapter'
19
+ autoload :Faraday, 'ey-hmac/faraday'
20
+ autoload :Rack, 'ey-hmac/rack'
21
+
22
+ def self.default_adapter=(default_adapter)
23
+ @default_adapter = default_adapter
24
+ end
25
+
26
+ def self.default_adapter
27
+ @default_adapter ||= if defined?(::Rack) || defined?(::Rails)
28
+ Ey::Hmac::Adapter::Rack
29
+ elsif defined?(::Faraday)
30
+ Ey::Hmac::Adapter::Faraday
31
+ end
32
+ end
33
+
34
+ # Signs request by calculating signature and adding it to the specified header
35
+ # @example
36
+ # Ey::Hmac.sign!(env, @key_id, @key_secret)
37
+ #
38
+ # @see Ey::Hmac::Adapter#sign!
39
+ #
40
+ # @param request [Hash] request environment
41
+ # @option options [Ey::Hmac::Adapter] :adapter (#{default_adapter}) adapter to sign request with
42
+ # @option options [Integer] :version (nil) signature version
43
+ # @option options [String] :authorization_header ('Authorization') Authorization header key.
44
+ # @option options [String] :service ('EyHmac') service name prefixed to {Ey::Hmac::Adapter#authorization}
45
+ #
46
+ # @return [String] authorization signature
47
+ def self.sign!(request, key_id, key_secret, options = {})
48
+ adapter = options[:adapter] || Ey::Hmac.default_adapter
49
+
50
+ raise ArgumentError, 'Missing adapter and Ey::Hmac.default_adapter' unless adapter
51
+
52
+ adapter.new(request, options).sign!(key_id, key_secret)
53
+ end
54
+
55
+ # @example
56
+ # Ey::Hmac.authenticated? do |key_id|
57
+ # @consumer = Consumer.where(auth_id: key_id).first
58
+ # @consumer && @consumer.auth_key
59
+ # end
60
+ #
61
+ # @see Ey::Hmac::Adapter#authenticated?
62
+ # @see Ey::Hmac#authenticate!
63
+ #
64
+ # @param request [Hash] request environment
65
+ # @option options [Ey::Hmac::Adapter] :adapter ({#default_adapter}) adapter to verify request with
66
+ # @yieldparam key_id [String] public HMAC key
67
+ #
68
+ # @return [Boolean] success of authentication
69
+ def self.authenticated?(request, options = {}, &block)
70
+ adapter = options[:adapter] || Ey::Hmac.default_adapter
71
+
72
+ raise ArgumentError, 'Missing adapter and Ey::Hmac.default_adapter' unless adapter
73
+
74
+ adapter.new(request, options).authenticated?(&block)
75
+ end
76
+
77
+ # Check {Ey::Hmac::Adapter#authorization_signature} against calculated {Ey::Hmac::Adapter#signature}
78
+ # @example
79
+ # Ey::Hmac.authenticate! do |key_id|
80
+ # @consumer = Consumer.where(auth_id: key_id).first
81
+ # @consumer && @consumer.auth_key
82
+ # end
83
+ #
84
+ # @see Ey::Hmac::Adapter#authenticate!
85
+ #
86
+ # @param request [Hash] request environment
87
+ # @yieldparam key_id [String] public HMAC key
88
+ # @option options [Ey::Hmac::Adapter] :adapter ({#default_adapter}) adapter to verify request with
89
+ #
90
+ # @raise [SignatureMismatch] if the value of {Ey::Hmac::Adapter#authorization_signature} does not
91
+ # match {Ey::Hmac::Adapter#signature}
92
+ # @raise [MissingSecret] if the block does not return a private key matching +key_id+
93
+ # @raise [MissingAuthorization] if the value of {Ey::Hmac::Adapter#authorization_signature} is nil
94
+ # @return [TrueClass] if authentication was successful
95
+ def self.authenticate!(request, options = {}, &block)
96
+ adapter = options[:adapter] || Ey::Hmac.default_adapter
97
+
98
+ raise ArgumentError, 'Missing adapter and Ey::Hmac.default_adapter' unless adapter
99
+
100
+ adapter.new(request, options).authenticate!(&block)
102
101
  end
103
102
  end