encrypted_cookie_store-instructure 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE.txt ADDED
@@ -0,0 +1,25 @@
1
+ Copyright (c) 2009 - 2010 Phusion, 2012 Cody Cutrer
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are met:
6
+
7
+ * Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+ * Redistributions in binary form must reproduce the above copyright notice,
10
+ this list of conditions and the following disclaimer in the documentation
11
+ and/or other materials provided with the distribution.
12
+ * Neither the name of the Phusion nor the names of its contributors
13
+ may be used to endorse or promote products derived from this software
14
+ without specific prior written permission.
15
+
16
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
17
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
19
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
20
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
21
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
22
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
23
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
24
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/README.markdown ADDED
@@ -0,0 +1,78 @@
1
+ EncryptedCookieStore
2
+ ====================
3
+ EncryptedCookieStore is similar to Ruby on Rails's CookieStore (it saves
4
+ session data in a cookie), but it uses encryption so that people can't read
5
+ what's in the session data. This makes it possible to store sensitive data
6
+ in the session.
7
+
8
+ EncryptedCookieStore is written for Rails 2.3. Other versions of Rails have
9
+ not been tested.
10
+
11
+ **Note**: This is _not_ ThinkRelevance's EncryptedCookieStore. In the Rails
12
+ 2.0 days they wrote an EncryptedCookieStore, but it seems their repository
13
+ had gone defunct and their source code lost. This EncryptedCookieStore is
14
+ written from scratch by Phusion.
15
+
16
+ Installation and usage
17
+ ----------------------
18
+
19
+ First, add EncryptedCookieStore to your Gemfile
20
+
21
+ gem 'encrypted_cookie_store'
22
+
23
+ Then edit `config/initializers/session_store.rb` and set your session store to
24
+ EncryptedCookieStore:
25
+
26
+ ActionController::Base.session_store = EncryptedCookieStore
27
+
28
+ You need to set a few session options before EncryptedCookieStore is usable.
29
+ You must set all options that CookieStore needs in `session_store.rb`:
30
+
31
+ ActionController::Base.session = {
32
+ # CookieStore options...
33
+ :key => '_session', # Name of the cookie which contains the session data.
34
+ :secret => 'b4589cc9...', # A secret string used to generate the checksum for
35
+ # the session data. Must be longer than 64 characters
36
+ # and be completely random.
37
+ }
38
+
39
+ Operational details
40
+ -------------------
41
+ Upon generating cookie data, EncryptedCookieStore generates a new, random
42
+ initialization vector for encrypting the session data. The session data is
43
+ first protected with an HMAC to prevent tampering. The session data is
44
+ then compressed with Zlib, and encrypted using 128-bit AES in CBC mode with
45
+ the generated initialization vector. This encrypted session data + HMAC are
46
+ then stored, along with the initialization vector and a timestamp, into the
47
+ cookie.
48
+
49
+ Upon unmarshalling the cookie data, EncryptedCookieStore
50
+ decrypts and decompresses the encrypted session data. The decrypted session
51
+ data is then verified against the HMAC. It is also verified that the
52
+ timestamp isn't too old, too prevent replay attacks.
53
+
54
+ EncryptedCookieStore also changes how CookieStore sets the cookie. If the
55
+ session has not changed, and the timestamp is less than 5 minutes old, it
56
+ will *not* send the cookie to the browser.
57
+
58
+ EncryptedCookieStore is quite fast: it is able to marshal and unmarshal a
59
+ simple session object 5000 times in 8.7 seconds on a MacBook Pro with a 2.4
60
+ Ghz Intel Core 2 Duo (in battery mode). This is about 0.174 ms per
61
+ marshal+unmarshal action. See `rake benchmark` in the EncryptedCookieStore
62
+ sources for details.
63
+
64
+ EncryptedCookieStore vs other session stores
65
+ --------------------------------------------
66
+ EncryptedCookieStore inherits all the benefits of CookieStore:
67
+
68
+ * It works out of the box without the need to setup a seperate data store (e.g. database table, daemon, etc).
69
+ * It does not require any maintenance. Old, stale sessions do not need to be manually cleaned up, as is the case with PStore and ActiveRecordStore.
70
+ * Compared to MemCacheStore, EncryptedCookieStore can "hold" an infinite number of sessions at any time.
71
+ * It can be scaled across multiple servers without any additional setup.
72
+ * It is fast.
73
+ * It is more secure than CookieStore because it allows you to store sensitive data in the session.
74
+
75
+ There are of course drawbacks as well:
76
+
77
+ * You can store at most a little less than 4 KB of data in the session because that's the size limit of a cookie. "A little less" because EncryptedCookieStore also stores a small amount of bookkeeping data in the cookie.
78
+ * Although encryption makes it more secure than CookieStore, there's still a chance that a bug in EncryptedCookieStore renders it insecure. We welcome everyone to audit this code. There's also a chance that weaknesses in AES are found in the near future which render it insecure. If you are storing *really* sensitive information in the session, e.g. social security numbers, or plans for world domination, then you should consider using ActiveRecordStore or some other server-side store.
@@ -0,0 +1,20 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = %q{encrypted_cookie_store-instructure}
3
+ s.version = "1.0.0"
4
+
5
+ s.authors = ["Cody"]
6
+ s.date = %q{2012-05-11}
7
+ s.extra_rdoc_files = [
8
+ "LICENSE.txt"
9
+ ]
10
+ s.files = [
11
+ "LICENSE.txt",
12
+ "README.markdown",
13
+ "lib/encrypted_cookie_store.rb",
14
+ "encrypted_cookie_store-instructure.gemspec"
15
+ ]
16
+ s.homepage = %q{http://github.com/ccutrer/encrypted_cookie_store}
17
+ s.require_paths = ["lib"]
18
+ s.summary = %q{EncryptedCookieStore for Ruby on Rails 2.3}
19
+ s.description = %q{A secure version of Rails' built in CookieStore}
20
+ end
@@ -0,0 +1,186 @@
1
+ require 'openssl'
2
+ require 'zlib'
3
+
4
+ class EncryptedCookieStore < ActionController::Session::CookieStore
5
+ OpenSSLCipherError = OpenSSL::Cipher.const_defined?(:CipherError) ? OpenSSL::Cipher::CipherError : OpenSSL::CipherError
6
+
7
+ class << self
8
+ attr_accessor :data_cipher_type
9
+ end
10
+
11
+ self.data_cipher_type = "aes-128-cbc".freeze
12
+
13
+ def initialize(app, options = {})
14
+ options[:secret] = options[:secret].call if options[:secret].respond_to?(:call)
15
+ ensure_encryption_key_secure(options[:secret])
16
+ @encryption_key = unhex(options[:secret]).freeze
17
+ @compress = options[:compress]
18
+ @compress = true if @compress.nil?
19
+ @data_cipher = OpenSSL::Cipher::Cipher.new(EncryptedCookieStore.data_cipher_type)
20
+ @expire_after = options[:expire_after].freeze
21
+ options[:refresh_interval] ||= 5.minutes
22
+ super(app, options)
23
+ end
24
+
25
+ def call(env)
26
+ prepare!(env)
27
+
28
+ old_session_data, raw_old_session_data, old_timestamp = all_unpacked_cookie_data(env)
29
+ # make sure we have a deep copy
30
+ old_session_data = Marshal.load(raw_old_session_data) if raw_old_session_data
31
+
32
+ status, headers, body = @app.call(env)
33
+
34
+ session_data = env[ENV_SESSION_KEY]
35
+ options = env[ENV_SESSION_OPTIONS_KEY]
36
+ request = ActionController::Request.new(env)
37
+
38
+ if !(options[:secure] && !request.ssl?) && (!session_data.is_a?(ActionController::Session::AbstractStore::SessionHash) || session_data.loaded? || options[:expire_after])
39
+ session_data.send(:load!) if session_data.is_a?(ActionController::Session::AbstractStore::SessionHash) && !session_data.loaded?
40
+
41
+ persistent_session_id!(session_data)
42
+
43
+ old_session_data = nil if options[:expire_after] && old_timestamp && Time.now.utc.to_i > old_timestamp + options[:refresh_interval]
44
+ return [status, headers, body] if session_data == old_session_data
45
+
46
+ session_data = marshal(session_data.to_hash)
47
+
48
+ raise CookieOverflow if session_data.size > MAX
49
+
50
+ cookie = Hash.new
51
+ cookie[:value] = session_data
52
+ unless options[:expire_after].nil?
53
+ cookie[:expires] = Time.now + options[:expire_after]
54
+ end
55
+
56
+ Rack::Utils.set_cookie_header!(headers, @key, cookie.merge(options))
57
+ end
58
+
59
+ [status, headers, body]
60
+ end
61
+ private
62
+ def secret
63
+ @secret
64
+ end
65
+
66
+ def marshal(session)
67
+ @data_cipher.encrypt
68
+ @data_cipher.key = @encryption_key
69
+
70
+ session_data = Marshal.dump(session)
71
+ iv = @data_cipher.random_iv
72
+ if @compress
73
+ compressed_session_data = deflate(session_data, 5)
74
+ compressed_session_data = session_data if compressed_session_data.length >= session_data.length
75
+ else
76
+ compressed_session_data = session_data
77
+ end
78
+ encrypted_session_data = @data_cipher.update(compressed_session_data) << @data_cipher.final
79
+ timestamp = Time.now.utc.to_i if @expire_after
80
+ digest = OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new(@digest), secret, session_data + timestamp.to_s)
81
+
82
+ result = "#{base64(iv)}#{compressed_session_data == session_data ? '.' : ' '}#{base64(encrypted_session_data)}.#{base64(digest)}"
83
+ result << ".#{base64([timestamp].pack('N'))}" if @expire_after
84
+ result
85
+ end
86
+
87
+ def unmarshal(cookie)
88
+ if cookie
89
+ compressed = !!cookie.index(' ')
90
+ b64_iv, b64_encrypted_session_data, b64_digest, b64_timestamp = cookie.split(/\.| /, 4)
91
+ if b64_iv && b64_encrypted_session_data && b64_digest
92
+ iv = unbase64(b64_iv)
93
+ encrypted_session_data = unbase64(b64_encrypted_session_data)
94
+ digest = unbase64(b64_digest)
95
+ timestamp = unbase64(b64_timestamp).unpack('N').first if b64_timestamp
96
+
97
+ @data_cipher.decrypt
98
+ @data_cipher.key = @encryption_key
99
+ @data_cipher.iv = iv
100
+ session_data = @data_cipher.update(encrypted_session_data) << @data_cipher.final
101
+ session_data = inflate(session_data) if compressed
102
+ return [nil, nil] unless digest == OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new(@digest), secret, session_data + timestamp.to_s)
103
+ if @expire_after
104
+ return [nil, nil] unless timestamp
105
+ return [nil, timestamp] unless Time.now.utc.to_i - timestamp < @expire_after
106
+ end
107
+ [Marshal.load(session_data), session_data, timestamp]
108
+ else
109
+ [nil, nil]
110
+ end
111
+ else
112
+ [nil, nil]
113
+ end
114
+ rescue Zlib::DataError
115
+ [nil, nil]
116
+ rescue OpenSSLCipherError
117
+ [nil, nil]
118
+ end
119
+
120
+ def all_unpacked_cookie_data(env)
121
+ env["action_dispatch.request.unsigned_session_cookie"] ||= begin
122
+ stale_session_check! do
123
+ request = Rack::Request.new(env)
124
+ session_data = request.cookies[@key]
125
+ unmarshal(session_data) || {}
126
+ end
127
+ end
128
+ end
129
+
130
+ def unpacked_cookie_data(env)
131
+ all_unpacked_cookie_data(env).first
132
+ end
133
+
134
+ # To prevent users from using an insecure encryption key like "Password" we make sure that the
135
+ # encryption key they've provided is at least 30 characters in length.
136
+ def ensure_encryption_key_secure(encryption_key)
137
+ if encryption_key.blank?
138
+ raise ArgumentError, "An encryption key is required for encrypting the " +
139
+ "cookie session data. Please set config.action_controller.session = { " +
140
+ "..., :encryption_key => \"some random string of at least " +
141
+ "16 bytes\", ... } in config/environment.rb"
142
+ end
143
+
144
+ if encryption_key.size < 16 * 2
145
+ raise ArgumentError, "The EncryptedCookieStore encryption key must be a " +
146
+ "hexadecimal string of at least 16 bytes. " +
147
+ "The value that you've provided, \"#{encryption_key}\", is " +
148
+ "#{encryption_key.size / 2} bytes. You could use the following (randomly " +
149
+ "generated) string as encryption key: " +
150
+ ActiveSupport::SecureRandom.hex(16)
151
+ end
152
+ end
153
+
154
+ def verifier_for(secret, digest)
155
+ nil
156
+ end
157
+
158
+ def base64(data)
159
+ ActiveSupport::Base64.encode64(data).tr('+/', '-_').gsub(/=|\n/, '')
160
+ end
161
+
162
+ def unbase64(data)
163
+ ActiveSupport::Base64.decode64(data.tr('-_', '+/').ljust((data.length + 4 - 1) / 4 * 4, '='))
164
+ end
165
+
166
+ # aka compress
167
+ def deflate(string, level)
168
+ z = Zlib::Deflate.new(level)
169
+ dst = z.deflate(string, Zlib::FINISH)
170
+ z.close
171
+ dst
172
+ end
173
+
174
+ # aka decompress
175
+ def inflate(string)
176
+ zstream = Zlib::Inflate.new
177
+ buf = zstream.inflate(string)
178
+ zstream.finish
179
+ zstream.close
180
+ buf
181
+ end
182
+
183
+ def unhex(hex_data)
184
+ [hex_data].pack("H*")
185
+ end
186
+ end
metadata ADDED
@@ -0,0 +1,70 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: encrypted_cookie_store-instructure
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease:
6
+ segments:
7
+ - 1
8
+ - 0
9
+ - 0
10
+ version: 1.0.0
11
+ platform: ruby
12
+ authors:
13
+ - Cody
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2012-05-11 00:00:00 -06:00
19
+ default_executable:
20
+ dependencies: []
21
+
22
+ description: A secure version of Rails' built in CookieStore
23
+ email:
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files:
29
+ - LICENSE.txt
30
+ files:
31
+ - LICENSE.txt
32
+ - README.markdown
33
+ - lib/encrypted_cookie_store.rb
34
+ - encrypted_cookie_store-instructure.gemspec
35
+ has_rdoc: true
36
+ homepage: http://github.com/ccutrer/encrypted_cookie_store
37
+ licenses: []
38
+
39
+ post_install_message:
40
+ rdoc_options: []
41
+
42
+ require_paths:
43
+ - lib
44
+ required_ruby_version: !ruby/object:Gem::Requirement
45
+ none: false
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ hash: 3
50
+ segments:
51
+ - 0
52
+ version: "0"
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ hash: 3
59
+ segments:
60
+ - 0
61
+ version: "0"
62
+ requirements: []
63
+
64
+ rubyforge_project:
65
+ rubygems_version: 1.5.3
66
+ signing_key:
67
+ specification_version: 3
68
+ summary: EncryptedCookieStore for Ruby on Rails 2.3
69
+ test_files: []
70
+