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 +25 -0
- data/README.markdown +78 -0
- data/encrypted_cookie_store-instructure.gemspec +20 -0
- data/lib/encrypted_cookie_store.rb +186 -0
- metadata +70 -0
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
|
+
|