module ShippingEasy # Used to generate ShippingEasy API signatures or to compare signature with one another. class Signature attr_reader :api_secret, :method, :path, :params, :body # Creates a new API signature object. # # options - The Hash options used to create a signature: # :api_secret - A ShippingEasy-supplied API secret # :method - The HTTP method used in the request. Either :get or :post. Default is :get. # :path - The URI path of the request. E.g. "/api/orders" # :params - The query params passed in as part of the request. # :body - The body of the request which should normally be a JSON payload. # def initialize(options = {}) options = options.dup @api_secret = options.delete(:api_secret) || "" @method = options.fetch(:method, :get).to_s.upcase @path = options.delete(:path) || "" @body = options.delete(:body) || "" @params = options[:params].nil? ? {} : options.delete(:params).dup @params.delete(:api_signature) # remove for convenience end # Concatenates the parts of the base signature into a plaintext string using the following order: # # 1. Capitilized method of the request. E.g. "POST" # 2. The URI path # 3. The query parameters sorted alphabetically and concatenated together into a URL friendly format: param1=ABC¶m2=XYZ # 4. The request body as a string if one exists # # All parts are then concatenated together with an ampersand. The result resembles something like this: # # "POST&/api/orders¶m1=ABC¶m2=XYZ&{\"orders\":{\"name\":\"Flip flops\",\"cost\":\"10.00\",\"shipping_cost\":\"2.00\"}}" # # Returns a correctly contenated plaintext API signature. def plaintext parts = [] parts << method parts << path parts << Rack::Utils.build_query(params.sort) parts << body.to_s unless body.nil? || body == "" parts.join("&") end # Encrypts the plaintext signature with the supplied API secret. This signature should be included # when making a ShippingEasy API call. # # Returns an encrypted signature. def encrypted OpenSSL::HMAC::hexdigest("sha256", api_secret, plaintext) end # Equality operator to determine if another signature object, or string, matches the current signature. If a string is passed in, it # should represent the encrypted form of the API signature, not the plaintext version. # # It uses a constant time string comparison function to limit the vulnerability of timing attacks. # # Returns true if the supplied string or signature object matches the current object. def ==(other_signature) expected_signature, supplied_signature = self.to_s, other_signature.to_s return false if expected_signature.nil? || supplied_signature.nil? || expected_signature.empty? || supplied_signature.empty? return false if expected_signature.bytesize != supplied_signature.bytesize l = expected_signature.unpack "C#{expected_signature.bytesize}" res = 0 supplied_signature.each_byte { |byte| res |= byte ^ l.shift } res == 0 end # Returns the encrypted form of the signature. # # Returns an encrypted signature. def to_s encrypted end end end