Class: Configuration::ValidateHMAC

Inherits:
HandlerStatement show all
Extended by:
Stats
Includes:
ConditionalInclusion
Defined in:
lib/httpimagestore/configuration/validate_hmac.rb

Direct Known Subclasses

ValidateHeaderHMAC, ValidateURIHMAC

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Scope

node_parsers, #parse, register_node_parser

Constructor Details

#initialize(hmac_qs_param_name, secret, lockpicks, digest, exclude, remove, uri_source) ⇒ ValidateHMAC

Returns a new instance of ValidateHMAC.



62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# File 'lib/httpimagestore/configuration/validate_hmac.rb', line 62

def initialize(hmac_qs_param_name, secret, lockpicks, digest, exclude, remove, uri_source)
	@hmac_qs_param_name = hmac_qs_param_name
	@secret = secret or raise NoSecretKeySpecifiedError
	@digest = digest || 'sha1'

	@exclude = (exclude || '').split(/ *, */)
	@lockpicks = (lockpicks || '').split(/ *, */)
	# always exclude hmac from hash computation
	@exclude << @hmac_qs_param_name

	# by default remove hmac for qs params but can be kept if remove=""
	@remove = if remove
		remove.split(/ *, */)
	else
		[@hmac_qs_param_name]
	end

	@uri_source = uri_source

	# check if digest is valid
	begin
		OpenSSL::Digest.digest(@digest, 'blah')
	rescue
		raise UnsupportedDigestError.new(@digest)
	end
end

Instance Attribute Details

#digestObject (readonly)

Returns the value of attribute digest.



89
90
91
# File 'lib/httpimagestore/configuration/validate_hmac.rb', line 89

def digest
  @digest
end

Class Method Details

.new_with_common_options(configuration, node, hmac_qs_param_name, uri_source) ⇒ Object



52
53
54
55
56
57
58
59
60
# File 'lib/httpimagestore/configuration/validate_hmac.rb', line 52

def self.new_with_common_options(configuration, node, hmac_qs_param_name, uri_source)
	secret, digest, exclude, remove, lockpicks, remaining = *node.grab_attributes_with_remaining('secret', 'digest', 'exclude', 'remove', 'lockpicks')
	conditions, remaining = *ConditionalInclusion.grab_conditions_with_remaining(remaining)
	remaining.empty? or raise UnexpectedAttributesError.new(node, remaining)

	obj = ValidateHMAC.new(hmac_qs_param_name, secret, lockpicks, digest, exclude, remove, uri_source)
	obj.with_conditions(conditions)
	obj
end

Instance Method Details

#realize(request_state) ⇒ Object



91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/httpimagestore/configuration/validate_hmac.rb', line 91

def realize(request_state)
	expected_hmac = request_state.query_string[@hmac_qs_param_name] or raise HMACMissingError.new(@hmac_qs_param_name, @digest)

	ValidateHMAC.stats.incr_total_hmac_validations

	# we need to remove related query string params so we don't pass them as thumbnailer options
	@remove.each do |rm|
		log.debug "removing query string parameter '#{rm}' used for URI authentication"
		request_state.query_string.delete(rm)
	end

	if lockpick = @lockpicks.detect{|lockpick| lockpick == expected_hmac}
		# we try to get the URI if possible or else fail back to request_uri
		# this is to help tracking where lockpicks are being used but still allow playing with APIs directly
		# without need to set custom headers
		lockpick_no = @lockpicks.find_index(lockpick) + 1
		uri = @uri_source.call(self, request_state) rescue request_state.request_uri
		uri = uri.gsub(/#{@exclude.last}=#{lockpick}/, "#{@exclude.last}=<lockpick #{lockpick_no} used>")

		log.warn "valid lockpick provided! skipping URI HMAC validation for URI '#{uri}'"
		ValidateHMAC.stats.incr_total_lockpick_hmac
		return
	end

	uri = @uri_source.call(self, request_state) or fail "nil URI"
	uri = @exclude.inject(uri) do |uri, ex|
		uri.gsub(/(\?|&)#{ex}=.*?($|&)/, '\1')
	end
	uri.sub!(/(\?|&)$/, '')

	digest = OpenSSL::Digest::Digest.new(@digest)

	log.debug "validating URI '#{uri}' HMAC with digest '#{@digest}': expected HMAC '#{expected_hmac}'"
	actual_hmac = OpenSSL::HMAC.hexdigest(digest, @secret, uri)

	if actual_hmac != expected_hmac
		sleep SecureRandom.random_number/10 # sleep some random time to make timing attack harder
		log.warn "invalid HMAC with digest '#{@digest}' for URI '#{uri}'; expected HMAC '#{expected_hmac}'"
		ValidateHMAC.stats.incr_total_invalid_hmac
		raise HMACMismatchError.new(expected_hmac, uri, @digest)
	else
		ValidateHMAC.stats.incr_total_valid_hmac
	end
end