Skip to content

Commit

Permalink
Added support for key rotation
Browse files Browse the repository at this point in the history
  • Loading branch information
ankane committed May 30, 2024
1 parent d1bb229 commit feba5a1
Show file tree
Hide file tree
Showing 3 changed files with 26 additions and 8 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 2.3.0 (unreleased)

- Added support for key rotation

## 2.2.0 (2023-07-02)

- Removed support for Ruby < 3 and Rails < 6.1
Expand Down
9 changes: 4 additions & 5 deletions app/controllers/ahoy/messages_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,23 @@ def open
end

def click
if params[:id]
# legacy
legacy = params[:id]
if legacy
token = params[:id].to_s
campaign = nil
url = params[:url].to_s
signature = params[:signature].to_s
expected_signature = OpenSSL::HMAC.hexdigest("SHA1", AhoyEmail::Utils.secret_token, url)
else
token = params[:t].to_s
campaign = params[:c].to_s
url = params[:u].to_s
signature = params[:s].to_s
expected_signature = AhoyEmail::Utils.signature(token: token, campaign: campaign, url: url)
end

redirect_options = {}
redirect_options[:allow_other_host] = true if ActionPack::VERSION::MAJOR >= 7

if ActiveSupport::SecurityUtils.secure_compare(signature, expected_signature)
if AhoyEmail::Utils.signature_verified?(legacy: legacy, token: token, campaign: campaign, url: url, signature: signature)
data = {}
data[:campaign] = campaign if campaign
data[:token] = token
Expand Down
21 changes: 18 additions & 3 deletions lib/ahoy_email/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,28 @@ class Utils
}

class << self
def signature(token:, campaign:, url:)
def signature(token:, campaign:, url:, secret_token: nil)
secret_token ||= self.secret_tokens.first

# encode and join with a character outside encoding
data = [token, campaign, url].map { |v| Base64.strict_encode64(v.to_s) }.join("|")

Base64.urlsafe_encode64(OpenSSL::HMAC.digest("SHA256", secret_token, data), padding: false)
end

def signature_verified?(legacy:, token:, campaign:, url:, signature:)
secret_tokens.any? do |secret_token|
expected_signature =
if legacy
OpenSSL::HMAC.hexdigest("SHA1", secret_token, url)
else
signature(token: token, campaign: campaign, url: url, secret_token: secret_token)
end

ActiveSupport::SecurityUtils.secure_compare(signature, expected_signature)
end
end

def publish(name, event)
method_name = "track_#{name}"
AhoyEmail.subscribers.each do |subscriber|
Expand All @@ -27,8 +42,8 @@ def publish(name, event)
end
end

def secret_token
AhoyEmail.secret_token || (raise "Secret token is empty")
def secret_tokens
Array(AhoyEmail.secret_token || (raise "Secret token is empty"))
end
end
end
Expand Down

0 comments on commit feba5a1

Please sign in to comment.