encrypted_cookie_store-instructure 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
+