lib/webhook_system/encoder.rb in webhook_system-1.0.4 vs lib/webhook_system/encoder.rb in webhook_system-2.0.0
- old
+ new
@@ -6,62 +6,117 @@
# encrypt it, base64 encode that, and wrap it in its own json wrapper
#
# @param [String] secret_string some secret string
# @param [Object#to_json] payload Any object that responds to to_json
# @return [String] The encoded string payload (its a JSON string)
- def self.encode(secret_string, payload)
- cipher = OpenSSL::Cipher::AES256.new(:CBC)
- cipher.encrypt
- iv = cipher.random_iv
- cipher.key = key_from_secret(iv, secret_string)
- encoded = cipher.update(payload.to_json) + cipher.final
- Payload.encode(encoded, iv)
+ def self.encode(secret_string, payload, format:)
+ response_hash = Payload.encode(payload, secret: secret_string, format: format)
+ payload_string = JSON.generate(response_hash)
+ signature = hub_signature(payload_string, secret_string)
+ [payload_string, { 'X-Hub-Signature' => signature, 'Content-Type' => content_type_for_format(format) }]
end
# Given a secret string, and an encrypted payload, unwrap it, bas64 decode it
# decrypt it, and JSON decode it
#
# @param [String] secret_string some secret string
- # @param [String] payload String as returned from #encode
+ # @param [String] payload_string String as returned from #encode
# @return [Object] return the JSON decode of the encrypted payload
- def self.decode(secret_string, payload)
- encoded, iv = Payload.decode(payload)
- cipher = OpenSSL::Cipher::AES256.new(:CBC)
- cipher.decrypt
- cipher.iv = iv
- cipher.key = key_from_secret(iv, secret_string)
- decoded = cipher.update(encoded) + cipher.final
- JSON.load(decoded)
- rescue OpenSSL::Cipher::CipherError
- raise DecodingError, 'Decoding Failed, probably mismatched secret'
+ def self.decode(secret_string, payload_string, headers = {})
+ signature = headers['X-Hub-Signature']
+ format = format_for_content_type(headers.fetch('Content-Type'))
+
+ payload_signature = hub_signature(payload_string, secret_string)
+ if signature && signature != payload_signature
+ raise DecodingError, 'signature mismatch'
+ end
+
+ Payload.decode(payload_string, secret: secret_string, format: format)
end
class << self
private
- def key_from_secret(iv, secret_string)
- OpenSSL::PKCS5.pbkdf2_hmac(secret_string, iv, 100_000, 256 / 8, 'SHA256')
+ def content_type_format_map
+ {
+ 'base64+aes256' => 'application/json; base64+aes256',
+ 'json' => 'application/json'
+ }
end
+
+ def format_for_content_type(content_type)
+ content_type_format_map.invert.fetch(content_type)
+ end
+
+ def content_type_for_format(format)
+ content_type_format_map.fetch(format)
+ end
+
+ def hub_signature(payload_string, secret)
+ 'sha1=' + OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha1'), secret, payload_string)
+ end
end
end
- # private class to just wrap the outer wrapping of the response format
- # not exposed to the outside
- # :nodoc:
module Payload
- def self.encode(raw_encrypted_data, iv)
- JSON.pretty_generate(
- 'format' => 'base64+aes256',
- 'payload' => Base64.encode64(raw_encrypted_data),
- 'iv' => Base64.encode64(iv)
- )
- end
+ class << self
+ def encode(payload, secret:, format:)
+ case format
+ when 'base64+aes256'
+ encode_aes(payload, secret)
+ when 'json'
+ payload
+ else
+ raise ArgumentError, "don't know how to handle: #{payload['format']} payload"
+ end
+ end
- def self.decode(payload_string)
- payload = JSON.load(payload_string)
- unless payload['format'] == 'base64+aes256'
- raise ArgumentError, 'only know how to handle base64+aes256 payloads'
+ def decode(response_body, secret:, format:)
+ payload = JSON.load(response_body)
+
+ case format
+ when 'base64+aes256'
+ decode_aes(payload, secret)
+ when 'json'
+ payload
+ else
+ raise ArgumentError, "don't know how to handle: #{payload['format']} payload"
+ end
end
- [Base64.decode64(payload['payload']), Base64.decode64(payload['iv'])]
+
+ private
+
+ def encode_aes(payload, secret)
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
+ cipher.encrypt
+ iv = cipher.random_iv
+ cipher.key = key_from_secret(iv, secret)
+ encoded = cipher.update(payload.to_json) + cipher.final
+
+ {
+ format: 'base64+aes256',
+ payload: Base64.encode64(encoded),
+ iv: Base64.encode64(iv),
+ }
+ end
+
+ def decode_aes(payload, secret)
+ encoded = Base64.decode64(payload['payload'])
+ iv = Base64.decode64(payload['iv'])
+
+ cipher = OpenSSL::Cipher::AES256.new(:CBC)
+ cipher.decrypt
+ cipher.iv = iv
+ cipher.key = key_from_secret(iv, secret)
+ decoded = cipher.update(encoded) + cipher.final
+
+ JSON.load(decoded)
+ rescue OpenSSL::Cipher::CipherError
+ raise DecodingError, 'Decoding Failed, probably mismatched secret'
+ end
+
+ def key_from_secret(iv, secret_string)
+ OpenSSL::PKCS5.pbkdf2_hmac(secret_string, iv, 100_000, 256 / 8, 'SHA256')
+ end
end
end
end