Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance Signature #363

Merged
merged 1 commit into from
Feb 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 45 additions & 25 deletions lib/hubspot/helpers/signature.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,33 @@
module Hubspot
module Helpers
class Signature
MAX_ALLOWED_TIMESTAMP = 3000
MAX_ALLOWED_TIMESTAMP = 300_000

def timestamp_valid?(timestamp)
return false if timestamp.nil?

begin
get_current_timestamp_microseconds - timestamp.to_i < MAX_ALLOWED_TIMESTAMP
rescue StandardError
false
end
end

def is_valid(
signature: String,
client_secret: String,
request_body: String,
signature:,
client_secret:,
request_body:,
http_uri: nil,
http_method: 'POST',
signature_version: 'v2',
timestamp: nil
)
if signature_version == "v3"
current_time = DateTime.now.strftime("%s").to_i
if current_time - timestamp.to_i > MAX_ALLOWED_TIMESTAMP
raise InvalidSignatureTimestampError.new(timestamp)
unless timestamp_valid?(timestamp)
raise InvalidSignatureTimestampError, "Invalid timestamp: #{timestamp}"
end
end

hashed_signature = get_signature(
client_secret: client_secret,
request_body: request_body,
Expand All @@ -29,34 +40,43 @@ def is_valid(
timestamp: timestamp
)

signature == hashed_signature
secure_compare(signature, hashed_signature)
end

def get_current_timestamp_microseconds
(Time.now.to_f * 1000).to_i
end

def get_signature(
client_secret: String,
request_body: String,
signature_version: String,
client_secret:,
request_body:,
signature_version:,
http_uri: nil,
http_method: "POST",
timestamp: nil
)
case signature_version
when "v1"
source_string = "#{client_secret}#{request_body}"
hash_result = Digest::SHA2.hexdigest(source_string.encode('utf-8'))
return hash_result
when "v2"
source_string = "#{client_secret}#{http_method}#{http_uri}#{request_body}"
hash_result = Digest::SHA2.hexdigest(source_string.encode('utf-8'))
return hash_result
when "v3"
source_string = "#{http_method}#{http_uri}#{request_body}#{timestamp}"
hash_result = OpenSSL::HMAC.base64digest('SHA256', client_secret, source_string.encode('utf-8'))
return hash_result
else
raise InvalidSignatureVersionError.new(signature_version)
when "v1"
source_string = "#{client_secret}#{request_body}"
Digest::SHA2.hexdigest(source_string.encode('utf-8'))
when "v2"
source_string = "#{client_secret}#{http_method}#{http_uri}#{request_body}"
Digest::SHA2.hexdigest(source_string.encode('utf-8'))
when "v3"
source_string = "#{http_method}#{http_uri}#{request_body}#{timestamp}"
OpenSSL::HMAC.base64digest('SHA256', client_secret, source_string.encode('utf-8'))
else
raise InvalidSignatureVersionError, "Invalid signature version: #{signature_version}"
end
end

def secure_compare(a, b)
return false unless a.is_a?(String) && b.is_a?(String) && a.bytesize == b.bytesize

result = 0
a.bytes.zip(b.bytes) { |x, y| result |= x ^ y }
result.zero?
end
end
end
end
20 changes: 15 additions & 5 deletions spec/helpers/signature_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
:request_body=> "{'example_field':'example_value'}",
:url=> "https://www.example.com/webhook_uri",
:http_method=> "POST",
:timestamp=> 15000000,
:timestamp=> 1_700_000_300_000,
:v1_hash=> "69fc6631a867edd4f9e9e627fc5c1148e3fbdd8b21837b6d2b8901c1fa57f750",
:v2_hash=> "4fe4e3a7d3cf09db53be39d0a58130e2aaba074ec123a9e355b876a689a1c383",
:v3_hash=> "HPW73RUtKmcYoEDADG0s6MmGFWUzWJKAW07r8RDgcQw=",
:v3_hash=> "RnbPH7+UMKVkbV32P8bz450N4M56aPmcru1+D3kSDtw=",
}


Expand Down Expand Up @@ -77,12 +77,12 @@
expect(result).to be true
end
it "should return true for v3 signature version" do
test_timestamp = DateTime.now.strftime("%s")
test_timestamp_microseconds = (Time.now.to_f * 1000).to_i
test_signature = signature.get_signature(
client_secret: TEST_DATA[:client_secret],
request_body: TEST_DATA[:request_body],
http_uri: TEST_DATA[:http_uri],
timestamp: test_timestamp,
timestamp: test_timestamp_microseconds,
signature_version: "v3"
)

Expand All @@ -91,7 +91,7 @@
client_secret: TEST_DATA[:client_secret],
request_body: TEST_DATA[:request_body],
http_uri: TEST_DATA[:http_uri],
timestamp: test_timestamp,
timestamp: test_timestamp_microseconds,
signature_version: "v3"
)
expect(result).to be true
Expand All @@ -105,6 +105,16 @@
signature_version: "v3"
) }.to raise_error(Hubspot::InvalidSignatureTimestampError)
end
it "should raise exception if :signature_version=>v3 and :timestamp=>expired" do
expect { signature.is_valid(
signature: TEST_DATA[:v3_hash],
client_secret: TEST_DATA[:client_secret],
request_body: TEST_DATA[:request_body],
http_uri: TEST_DATA[:http_uri],
timestamp: 1_700_000_000_000,
signature_version: "v3"
) }.to raise_error(Hubspot::InvalidSignatureTimestampError)
end
it "should raise exception if :signature_version=>v3 and :timestamp=>wrong_timestamp" do
expect { signature.is_valid(
signature: TEST_DATA[:v3_hash],
Expand Down