rails/activesupport/lib/active_support/message_verifier.rb

135 lines
5.1 KiB
Ruby
Raw Normal View History

require "base64"
require_relative "core_ext/object/blank"
require_relative "security_utils"
module ActiveSupport
2012-09-17 05:22:18 +00:00
# +MessageVerifier+ makes it easy to generate and verify messages which are
# signed to prevent tampering.
#
2012-09-17 05:22:18 +00:00
# This is useful for cases like remember-me tokens and auto-unsubscribe links
# where the session store isn't suitable or available.
#
# Remember Me:
# cookies[:remember_me] = @verifier.generate([@user.id, 2.weeks.from_now])
#
# In the authentication filter:
#
# id, time = @verifier.verify(cookies[:remember_me])
# if Time.now < time
# self.current_user = User.find(id)
# end
#
2012-09-17 05:22:18 +00:00
# By default it uses Marshal to serialize the message. If you want to use
# another serialization method, you can set the serializer in the options
# hash upon initialization:
#
# @verifier = ActiveSupport::MessageVerifier.new('s3Krit', serializer: YAML)
#
# +MessageVerifier+ creates HMAC signatures using SHA1 hash algorithm by default.
# If you want to use a different hash algorithm, you can change it by providing
# `:digest` key as an option while initializing the verifier:
#
# @verifier = ActiveSupport::MessageVerifier.new('s3Krit', digest: 'SHA256')
class MessageVerifier
class InvalidSignature < StandardError; end
def initialize(secret, options = {})
raise ArgumentError, "Secret should not be nil." unless secret
@secret = secret
@digest = options[:digest] || "SHA1"
@serializer = options[:serializer] || Marshal
end
# Checks if a signed message could have been generated by signing an object
# with the +MessageVerifier+'s secret.
#
# verifier = ActiveSupport::MessageVerifier.new 's3Krit'
# signed_message = verifier.generate 'a private message'
# verifier.valid_message?(signed_message) # => true
#
# tampered_message = signed_message.chop # editing the message invalidates the signature
# verifier.valid_message?(tampered_message) # => false
def valid_message?(signed_message)
return if signed_message.nil? || !signed_message.valid_encoding? || signed_message.blank?
Freeze string literals when not mutated. I wrote a utility that helps find areas where you could optimize your program using a frozen string instead of a string literal, it's called [let_it_go](https://github.com/schneems/let_it_go). After going through the output and adding `.freeze` I was able to eliminate the creation of 1,114 string objects on EVERY request to [codetriage](codetriage.com). How does this impact execution? To look at memory: ```ruby require 'get_process_mem' mem = GetProcessMem.new GC.start GC.disable 1_114.times { " " } before = mem.mb after = mem.mb GC.enable puts "Diff: #{after - before} mb" ``` Creating 1,114 string objects results in `Diff: 0.03125 mb` of RAM allocated on every request. Or 1mb every 32 requests. To look at raw speed: ```ruby require 'benchmark/ips' number_of_objects_reduced = 1_114 Benchmark.ips do |x| x.report("freeze") { number_of_objects_reduced.times { " ".freeze } } x.report("no-freeze") { number_of_objects_reduced.times { " " } } end ``` We get the results ``` Calculating ------------------------------------- freeze 1.428k i/100ms no-freeze 609.000 i/100ms ------------------------------------------------- freeze 14.363k (± 8.5%) i/s - 71.400k no-freeze 6.084k (± 8.1%) i/s - 30.450k ``` Now we can do some maths: ```ruby ips = 6_226k # iterations / 1 second call_time_before = 1.0 / ips # seconds per iteration ips = 15_254 # iterations / 1 second call_time_after = 1.0 / ips # seconds per iteration diff = call_time_before - call_time_after number_of_objects_reduced * diff * 100 # => 0.4530373333993266 miliseconds saved per request ``` So we're shaving off 1 second of execution time for every 220 requests. Is this going to be an insane speed boost to any Rails app: nope. Should we merge it: yep. p.s. If you know of a method call that doesn't modify a string input such as [String#gsub](https://github.com/schneems/let_it_go/blob/b0e2da69f0cca87ab581022baa43291cdf48638c/lib/let_it_go/core_ext/string.rb#L37) please [give me a pull request to the appropriate file](https://github.com/schneems/let_it_go/blob/b0e2da69f0cca87ab581022baa43291cdf48638c/lib/let_it_go/core_ext/string.rb#L37), or open an issue in LetItGo so we can track and freeze more strings. Keep those strings Frozen ![](https://www.dropbox.com/s/z4dj9fdsv213r4v/let-it-go.gif?dl=1)
2015-07-19 21:19:15 +00:00
data, digest = signed_message.split("--".freeze)
data.present? && digest.present? && ActiveSupport::SecurityUtils.secure_compare(digest, generate_digest(data))
end
# Decodes the signed message using the +MessageVerifier+'s secret.
#
# verifier = ActiveSupport::MessageVerifier.new 's3Krit'
#
# signed_message = verifier.generate 'a private message'
# verifier.verified(signed_message) # => 'a private message'
#
# Returns +nil+ if the message was not signed with the same secret.
#
# other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit'
# other_verifier.verified(signed_message) # => nil
#
# Returns +nil+ if the message is not Base64-encoded.
#
# invalid_message = "f--46a0120593880c733a53b6dad75b42ddc1c8996d"
# verifier.verified(invalid_message) # => nil
#
# Raises any error raised while decoding the signed message.
#
# incompatible_message = "test--dad7b06c94abba8d46a15fafaef56c327665d5ff"
# verifier.verified(incompatible_message) # => TypeError: incompatible marshal file format
def verified(signed_message)
if valid_message?(signed_message)
begin
Freeze string literals when not mutated. I wrote a utility that helps find areas where you could optimize your program using a frozen string instead of a string literal, it's called [let_it_go](https://github.com/schneems/let_it_go). After going through the output and adding `.freeze` I was able to eliminate the creation of 1,114 string objects on EVERY request to [codetriage](codetriage.com). How does this impact execution? To look at memory: ```ruby require 'get_process_mem' mem = GetProcessMem.new GC.start GC.disable 1_114.times { " " } before = mem.mb after = mem.mb GC.enable puts "Diff: #{after - before} mb" ``` Creating 1,114 string objects results in `Diff: 0.03125 mb` of RAM allocated on every request. Or 1mb every 32 requests. To look at raw speed: ```ruby require 'benchmark/ips' number_of_objects_reduced = 1_114 Benchmark.ips do |x| x.report("freeze") { number_of_objects_reduced.times { " ".freeze } } x.report("no-freeze") { number_of_objects_reduced.times { " " } } end ``` We get the results ``` Calculating ------------------------------------- freeze 1.428k i/100ms no-freeze 609.000 i/100ms ------------------------------------------------- freeze 14.363k (± 8.5%) i/s - 71.400k no-freeze 6.084k (± 8.1%) i/s - 30.450k ``` Now we can do some maths: ```ruby ips = 6_226k # iterations / 1 second call_time_before = 1.0 / ips # seconds per iteration ips = 15_254 # iterations / 1 second call_time_after = 1.0 / ips # seconds per iteration diff = call_time_before - call_time_after number_of_objects_reduced * diff * 100 # => 0.4530373333993266 miliseconds saved per request ``` So we're shaving off 1 second of execution time for every 220 requests. Is this going to be an insane speed boost to any Rails app: nope. Should we merge it: yep. p.s. If you know of a method call that doesn't modify a string input such as [String#gsub](https://github.com/schneems/let_it_go/blob/b0e2da69f0cca87ab581022baa43291cdf48638c/lib/let_it_go/core_ext/string.rb#L37) please [give me a pull request to the appropriate file](https://github.com/schneems/let_it_go/blob/b0e2da69f0cca87ab581022baa43291cdf48638c/lib/let_it_go/core_ext/string.rb#L37), or open an issue in LetItGo so we can track and freeze more strings. Keep those strings Frozen ![](https://www.dropbox.com/s/z4dj9fdsv213r4v/let-it-go.gif?dl=1)
2015-07-19 21:19:15 +00:00
data = signed_message.split("--".freeze)[0]
@serializer.load(decode(data))
rescue ArgumentError => argument_error
return if argument_error.message.include?("invalid base64")
raise
end
end
end
# Decodes the signed message using the +MessageVerifier+'s secret.
#
# verifier = ActiveSupport::MessageVerifier.new 's3Krit'
# signed_message = verifier.generate 'a private message'
#
# verifier.verify(signed_message) # => 'a private message'
#
# Raises +InvalidSignature+ if the message was not signed with the same
# secret or was not Base64-encoded.
#
# other_verifier = ActiveSupport::MessageVerifier.new 'd1ff3r3nt-s3Krit'
# other_verifier.verify(signed_message) # => ActiveSupport::MessageVerifier::InvalidSignature
def verify(signed_message)
verified(signed_message) || raise(InvalidSignature)
end
# Generates a signed message for the provided value.
#
# The message is signed with the +MessageVerifier+'s secret. Without knowing
# the secret, the original value cannot be extracted from the message.
#
# verifier = ActiveSupport::MessageVerifier.new 's3Krit'
# verifier.generate 'a private message' # => "BAhJIhRwcml2YXRlLW1lc3NhZ2UGOgZFVA==--e2d724331ebdee96a10fb99b089508d1c72bd772"
def generate(value)
data = encode(@serializer.dump(value))
"#{data}--#{generate_digest(data)}"
end
private
def encode(data)
::Base64.strict_encode64(data)
end
def decode(data)
::Base64.strict_decode64(data)
end
def generate_digest(data)
require "openssl" unless defined?(OpenSSL)
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.const_get(@digest).new, @secret, data)
end
end
2008-11-23 23:29:03 +00:00
end