1 module ActiveSupport
2 # MessageVerifier makes it easy to generate and verify messages which are signed
3 # to prevent tampering.
4 #
5 # This is useful for cases like remember-me tokens and auto-unsubscribe links where the
6 # session store isn't suitable or available.
7 #
8 # Remember Me:
9 # cookies[:remember_me] = @verifier.generate([@user.id, 2.weeks.from_now])
10 #
11 # In the authentication filter:
12 #
13 # id, time = @verifier.verify(cookies[:remember_me])
14 # if time < Time.now
15 # self.current_user = User.find(id)
16 # end
17 #
18 class MessageVerifier
19 class InvalidSignature < StandardError; end
20
21 def initialize(secret, digest = 'SHA1')
22 @secret = secret
23 @digest = digest
24 end
25
26 def verify(signed_message)
27 raise InvalidSignature if signed_message.blank?
28
29 data, digest = signed_message.split("--")
30 if data.present? && digest.present? && secure_compare(digest, generate_digest(data))
31 Marshal.load(ActiveSupport::Base64.decode64(data))
32 else
33 raise InvalidSignature
34 end
35 end
36
37 def generate(value)
38 data = ActiveSupport::Base64.encode64s(Marshal.dump(value))
39 "#{data}--#{generate_digest(data)}"
40 end
41
42 private
43 if "foo".respond_to?(:force_encoding)
44 # constant-time comparison algorithm to prevent timing attacks
45 def secure_compare(a, b)
46 a = a.dup.force_encoding(Encoding::BINARY)
47 b = b.dup.force_encoding(Encoding::BINARY)
48
49 if a.length == b.length
50 result = 0
51 for i in 0..(a.length - 1)
52 result |= a[i].ord ^ b[i].ord
53 end
54 result == 0
55 else
56 false
57 end
58 end
59 else
60 # For <= 1.8.6
61 def secure_compare(a, b)
62 if a.length == b.length
63 result = 0
64 for i in 0..(a.length - 1)
65 result |= a[i] ^ b[i]
66 end
67 result == 0
68 else
69 false
70 end
71 end
72 end
73
74 def generate_digest(data)
75 require 'openssl' unless defined?(OpenSSL)
76 OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(@digest), @secret, data)
77 end
78 end
79 end