validas-encrypted_cookie_store 0.4.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/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +104 -0
- data/LICENSE.txt +25 -0
- data/README.md +114 -0
- data/Rakefile +81 -0
- data/VERSION +1 -0
- data/lib/encrypted_cookie_store.rb +2 -0
- data/lib/encrypted_cookie_store/constants.rb +3 -0
- data/lib/encrypted_cookie_store/encrypted_cookie_store.rb +145 -0
- data/lib/encrypted_cookie_store/railtie.rb +10 -0
- data/lib/tasks/encrypted_cookie_store.rake +7 -0
- data/spec/encrypted_cookie_store_spec.rb +88 -0
- data/validas-encrypted_cookie_store.gemspec +65 -0
- metadata +143 -0
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
actionmailer (3.2.8)
|
5
|
+
actionpack (= 3.2.8)
|
6
|
+
mail (~> 2.4.4)
|
7
|
+
actionpack (3.2.8)
|
8
|
+
activemodel (= 3.2.8)
|
9
|
+
activesupport (= 3.2.8)
|
10
|
+
builder (~> 3.0.0)
|
11
|
+
erubis (~> 2.7.0)
|
12
|
+
journey (~> 1.0.4)
|
13
|
+
rack (~> 1.4.0)
|
14
|
+
rack-cache (~> 1.2)
|
15
|
+
rack-test (~> 0.6.1)
|
16
|
+
sprockets (~> 2.1.3)
|
17
|
+
activemodel (3.2.8)
|
18
|
+
activesupport (= 3.2.8)
|
19
|
+
builder (~> 3.0.0)
|
20
|
+
activerecord (3.2.8)
|
21
|
+
activemodel (= 3.2.8)
|
22
|
+
activesupport (= 3.2.8)
|
23
|
+
arel (~> 3.0.2)
|
24
|
+
tzinfo (~> 0.3.29)
|
25
|
+
activeresource (3.2.8)
|
26
|
+
activemodel (= 3.2.8)
|
27
|
+
activesupport (= 3.2.8)
|
28
|
+
activesupport (3.2.8)
|
29
|
+
i18n (~> 0.6)
|
30
|
+
multi_json (~> 1.0)
|
31
|
+
arel (3.0.2)
|
32
|
+
builder (3.0.4)
|
33
|
+
diff-lcs (1.1.3)
|
34
|
+
erubis (2.7.0)
|
35
|
+
git (1.2.5)
|
36
|
+
hike (1.2.1)
|
37
|
+
i18n (0.6.1)
|
38
|
+
jeweler (1.8.4)
|
39
|
+
bundler (~> 1.0)
|
40
|
+
git (>= 1.2.5)
|
41
|
+
rake
|
42
|
+
rdoc
|
43
|
+
journey (1.0.4)
|
44
|
+
json (1.7.5)
|
45
|
+
mail (2.4.4)
|
46
|
+
i18n (>= 0.4.0)
|
47
|
+
mime-types (~> 1.16)
|
48
|
+
treetop (~> 1.4.8)
|
49
|
+
mime-types (1.19)
|
50
|
+
multi_json (1.3.6)
|
51
|
+
polyglot (0.3.3)
|
52
|
+
rack (1.4.1)
|
53
|
+
rack-cache (1.2)
|
54
|
+
rack (>= 0.4)
|
55
|
+
rack-ssl (1.3.2)
|
56
|
+
rack
|
57
|
+
rack-test (0.6.2)
|
58
|
+
rack (>= 1.0)
|
59
|
+
rails (3.2.8)
|
60
|
+
actionmailer (= 3.2.8)
|
61
|
+
actionpack (= 3.2.8)
|
62
|
+
activerecord (= 3.2.8)
|
63
|
+
activeresource (= 3.2.8)
|
64
|
+
activesupport (= 3.2.8)
|
65
|
+
bundler (~> 1.0)
|
66
|
+
railties (= 3.2.8)
|
67
|
+
railties (3.2.8)
|
68
|
+
actionpack (= 3.2.8)
|
69
|
+
activesupport (= 3.2.8)
|
70
|
+
rack-ssl (~> 1.3.2)
|
71
|
+
rake (>= 0.8.7)
|
72
|
+
rdoc (~> 3.4)
|
73
|
+
thor (>= 0.14.6, < 2.0)
|
74
|
+
rake (0.9.2.2)
|
75
|
+
rdoc (3.12)
|
76
|
+
json (~> 1.4)
|
77
|
+
rspec (2.6.0)
|
78
|
+
rspec-core (~> 2.6.0)
|
79
|
+
rspec-expectations (~> 2.6.0)
|
80
|
+
rspec-mocks (~> 2.6.0)
|
81
|
+
rspec-core (2.6.4)
|
82
|
+
rspec-expectations (2.6.0)
|
83
|
+
diff-lcs (~> 1.1.2)
|
84
|
+
rspec-mocks (2.6.0)
|
85
|
+
sprockets (2.1.3)
|
86
|
+
hike (~> 1.2)
|
87
|
+
rack (~> 1.0)
|
88
|
+
tilt (~> 1.1, != 1.3.0)
|
89
|
+
thor (0.16.0)
|
90
|
+
tilt (1.3.3)
|
91
|
+
treetop (1.4.12)
|
92
|
+
polyglot
|
93
|
+
polyglot (>= 0.3.1)
|
94
|
+
tzinfo (0.3.34)
|
95
|
+
|
96
|
+
PLATFORMS
|
97
|
+
ruby
|
98
|
+
|
99
|
+
DEPENDENCIES
|
100
|
+
bundler
|
101
|
+
jeweler
|
102
|
+
rails (~> 3.2.0)
|
103
|
+
rake
|
104
|
+
rspec (~> 2.6.0)
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
Copyright (c) 2009 - 2010 Phusion
|
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.md
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
[](http://travis-ci.org/validas/encrypted_cookie_store)
|
2
|
+
EncryptedCookieStore
|
3
|
+
====================
|
4
|
+
EncryptedCookieStore is similar to Ruby on Rails's CookieStore (it saves
|
5
|
+
session data in a cookie), but it uses encryption so that people can't read
|
6
|
+
what's in the session data. This makes it possible to store sensitive data
|
7
|
+
in the session.
|
8
|
+
|
9
|
+
This version of EncryptedCookieStore is written for Rails 3.0.0+. It will not work with Rails 3.0.0.beta3 or earlier. It does not yet work with Rails 3.1. It has been tested with:
|
10
|
+
|
11
|
+
* 3.0.0
|
12
|
+
* 3.0.7
|
13
|
+
* 3.0.8.rc4
|
14
|
+
|
15
|
+
The original version for Rails 2.3 can be found here: https://github.com/FooBarWidget/encrypted_cookie_store
|
16
|
+
|
17
|
+
For a version that probably works with Rails 3.0.0.beta - 3.0.0.beta3, check here: https://github.com/twoism-dev/encrypted_cookie_store
|
18
|
+
|
19
|
+
Installation and usage
|
20
|
+
----------------------
|
21
|
+
|
22
|
+
First, install it:
|
23
|
+
|
24
|
+
gem install scottwb-encrypted_cookie_store
|
25
|
+
|
26
|
+
Then, add it to you bundler Gemfile:
|
27
|
+
|
28
|
+
gem 'scottwb-encrypted_cookie_store', :require => 'encrypted_cookie_store'
|
29
|
+
|
30
|
+
Then edit `config/initializers/session_store.rb` and set your session store to
|
31
|
+
EncryptedCookieStore:
|
32
|
+
|
33
|
+
MyApp::Application.config.session_store(
|
34
|
+
EncryptedCookieStore::EncryptedCookieStore,
|
35
|
+
:key => '_myapp_session',
|
36
|
+
:encryption_key => '966a4....'
|
37
|
+
)
|
38
|
+
|
39
|
+
The encryption key *must* be a hexadecimal string of exactly 32 bytes. It
|
40
|
+
should be entirely random, because otherwise it can make the encryption weak.
|
41
|
+
|
42
|
+
You can generate a new encryption key by running `rake secret:encryption_key`.
|
43
|
+
This command will output a random encryption key that you can then copy and
|
44
|
+
paste into your environment.rb.
|
45
|
+
|
46
|
+
You also need to make sure you have a secret token defined in `config/initializers/secret_token.rb`, just as you work for the standard CookieStore, e.g.:
|
47
|
+
|
48
|
+
MyApp::Application.config.secret_token = 'f75bb....'
|
49
|
+
|
50
|
+
Operational details
|
51
|
+
-------------------
|
52
|
+
Upon generating cookie data, EncryptedCookieStore generates a new, random
|
53
|
+
initialization vector for encrypting the session data. This initialization
|
54
|
+
vector is then encrypted with 128-bit AES in ECB mode. The session data is
|
55
|
+
first protected with an HMAC to prevent tampering. The session data, along
|
56
|
+
with the HMAC, are then encrypted using 256-bit AES in CFB mode with the
|
57
|
+
generated initialization vector. This encrypted session data + HMAC are
|
58
|
+
then stored, along with the encrypted initialization vector, into the cookie.
|
59
|
+
|
60
|
+
Upon unmarshalling the cookie data, EncryptedCookieStore decrypts the
|
61
|
+
encrypted initialization vector and use that to decrypt the encrypted
|
62
|
+
session data + HMAC. The decrypted session data is then verified against
|
63
|
+
the HMAC.
|
64
|
+
|
65
|
+
The reason why HMAC verification occurs after decryption instead of before
|
66
|
+
decryption is because we want to be able to detect changes to the encryption
|
67
|
+
key and changes to the HMAC secret key, as well as migrations from CookieStore.
|
68
|
+
Verifying after decryption allows us to automatically invalidate such old
|
69
|
+
session cookies.
|
70
|
+
|
71
|
+
EncryptedCookieStore is quite fast: it is able to marshal and unmarshal a
|
72
|
+
simple session object 5000 times in 8.7 seconds on a MacBook Pro with a 2.4
|
73
|
+
Ghz Intel Core 2 Duo (in battery mode). This is about 0.174 ms per
|
74
|
+
marshal+unmarshal action. See `rake benchmark` in the EncryptedCookieStore
|
75
|
+
sources for details.
|
76
|
+
|
77
|
+
EncryptedCookieStore vs other session stores
|
78
|
+
--------------------------------------------
|
79
|
+
EncryptedCookieStore inherits all the benefits of CookieStore:
|
80
|
+
|
81
|
+
* It works out of the box without the need to setup a seperate data store (e.g. database table, daemon, etc).
|
82
|
+
* 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.
|
83
|
+
* Compared to MemCacheStore, EncryptedCookieStore can "hold" an infinite number of sessions at any time.
|
84
|
+
* It can be scaled across multiple servers without any additional setup.
|
85
|
+
* It is fast.
|
86
|
+
* It is more secure than CookieStore because it allows you to store sensitive data in the session.
|
87
|
+
|
88
|
+
There are of course drawbacks as well:
|
89
|
+
|
90
|
+
* It is prone to session replay attacks. These kind of attacks are explained in the [Ruby on Rails Security Guide](http://guides.rubyonrails.org/security.html#session-storage). Therefore you should never store anything along the lines of `is_admin` in the session.
|
91
|
+
* 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.
|
92
|
+
* 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.
|
93
|
+
|
94
|
+
JRuby: Illegal Key Size error
|
95
|
+
-----------------------------
|
96
|
+
If you get this error (and your code works with MRI)...
|
97
|
+
|
98
|
+
Illegal key size
|
99
|
+
|
100
|
+
[...]/vendor/plugins/encrypted_cookie_store/lib/encrypted_cookie_store.rb:62:in `marshal'
|
101
|
+
|
102
|
+
...then it probably means you don't have the "unlimited strength" policy files
|
103
|
+
installed for your JVM.
|
104
|
+
[Download and install them.](http://www.ngs.ac.uk/tools/jcepolicyfiles)
|
105
|
+
You probably have the "strong" version if they are already there.
|
106
|
+
|
107
|
+
As a workaround, you can change the cipher type from 256-bit AES to 128-bit by
|
108
|
+
inserting the following in `config/initializer/session_store.rb`:
|
109
|
+
|
110
|
+
EncryptedCookieStore.data_cipher_type = 'aes-128-cfb'.freeze # was 256
|
111
|
+
|
112
|
+
Please note that after changing to 128-bit AES, EncryptedCookieStore still
|
113
|
+
requires a 32 bytes hexadecimal encryption key, although only half of the key
|
114
|
+
is actually used.
|
data/Rakefile
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
begin
|
4
|
+
Bundler.setup(:default, :development)
|
5
|
+
rescue Bundler::BundlerError => e
|
6
|
+
$stderr.puts e.message
|
7
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
8
|
+
exit e.status_code
|
9
|
+
end
|
10
|
+
require 'rake'
|
11
|
+
|
12
|
+
|
13
|
+
require 'jeweler'
|
14
|
+
Jeweler::Tasks.new do |gem|
|
15
|
+
gem.name = "validas-encrypted_cookie_store"
|
16
|
+
gem.homepage = "https://github.com/validas/encrypted_cookie_store"
|
17
|
+
gem.summary = "A Rails 3.1 & 3.2 version of Encrypted Cookie Store by FooBarWidget"
|
18
|
+
gem.description = "A Rails 3.1 & 3.2 version of Encrypted Cookie Store by FooBarWidget"
|
19
|
+
gem.email = "scottwb@gmail.com"
|
20
|
+
gem.authors = ["FooBarWidget", "Scott W. Bradley", "Validas Engineering Team"]
|
21
|
+
end
|
22
|
+
Jeweler::RubygemsDotOrgTasks.new
|
23
|
+
|
24
|
+
|
25
|
+
require 'rspec/core'
|
26
|
+
require 'rspec/core/rake_task'
|
27
|
+
RSpec::Core::RakeTask.new(:spec) do |spec|
|
28
|
+
spec.pattern = FileList['spec/**/*_spec.rb']
|
29
|
+
end
|
30
|
+
task :default => :spec
|
31
|
+
|
32
|
+
|
33
|
+
desc "Run benchmark"
|
34
|
+
task :benchmark do
|
35
|
+
$LOAD_PATH.unshift(File.expand_path("lib"))
|
36
|
+
require 'rubygems'
|
37
|
+
require 'benchmark'
|
38
|
+
require "rails"
|
39
|
+
require 'action_controller'
|
40
|
+
require 'encrypted_cookie_store'
|
41
|
+
|
42
|
+
secret = "b6a30e998806a238c4bad45cc720ed55e56e50d9f00fff58552e78a20fe8262df61" <<
|
43
|
+
"42fcfdb0676018bb9767ed560d4a624fb7f3603b4e53c77ec189ae3853bd1"
|
44
|
+
encryption_key = "dd458e790c3b995e3606384c58efc53da431db892f585aa3ca2a17eabe6df75b"
|
45
|
+
store = EncryptedCookieStore::EncryptedCookieStore.new(
|
46
|
+
nil,
|
47
|
+
:secret => secret,
|
48
|
+
:key => 'my_app',
|
49
|
+
:encryption_key => encryption_key
|
50
|
+
)
|
51
|
+
object = {
|
52
|
+
:hello => "world",
|
53
|
+
:user_id => 1234,
|
54
|
+
:is_admin => true,
|
55
|
+
:shopping_cart => ["Tea x 1", "Carrots x 13", "Pocky x 20", "Pen x 4"],
|
56
|
+
:session_id => "b6a30e998806a238c4bad45cc720ed55e56e50d9f00ff"
|
57
|
+
}
|
58
|
+
count = 50_000
|
59
|
+
|
60
|
+
puts "Marshalling and unmarshalling #{count} times..."
|
61
|
+
result = Benchmark.measure do
|
62
|
+
count.times do
|
63
|
+
data = store.send(:set_session, nil, nil, object)
|
64
|
+
store.send(:unmarshal, data)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
puts result
|
68
|
+
printf "%.3f ms per marshal+unmarshal action\n", result.real * 1000 / count
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
require "rdoc/task"
|
73
|
+
Rake::RDocTask.new do |rdoc|
|
74
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
75
|
+
|
76
|
+
rdoc.rdoc_dir = "rdoc"
|
77
|
+
rdoc.title = "validas-encrypted_cookie_store #{version}"
|
78
|
+
rdoc.rdoc_files.include("README*")
|
79
|
+
rdoc.rdoc_files.include("lib/**/*.rb")
|
80
|
+
#rdoc.main = "README.md"
|
81
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.4.0
|
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'encrypted_cookie_store/constants'
|
3
|
+
|
4
|
+
module EncryptedCookieStore
|
5
|
+
class EncryptedCookieStore < ActionDispatch::Session::CookieStore
|
6
|
+
OpenSSLCipherError = OpenSSL::Cipher.const_defined?(:CipherError) ? OpenSSL::Cipher::CipherError : OpenSSL::CipherError
|
7
|
+
include EncryptedCookieStoreConstants
|
8
|
+
|
9
|
+
class << self
|
10
|
+
attr_accessor :iv_cipher_type
|
11
|
+
attr_accessor :data_cipher_type
|
12
|
+
end
|
13
|
+
|
14
|
+
self.iv_cipher_type = "aes-128-ecb".freeze
|
15
|
+
self.data_cipher_type = "aes-256-cfb".freeze
|
16
|
+
|
17
|
+
def initialize(app, options = {})
|
18
|
+
ensure_encryption_key_secure(options[:encryption_key])
|
19
|
+
@encryption_key = unhex(options[:encryption_key]).freeze
|
20
|
+
@iv_cipher = OpenSSL::Cipher::Cipher.new(EncryptedCookieStore.iv_cipher_type)
|
21
|
+
@data_cipher = OpenSSL::Cipher::Cipher.new(EncryptedCookieStore.data_cipher_type)
|
22
|
+
super(app, options)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
# Like ActiveSupport::MessageVerifier, but does not base64-encode data.
|
27
|
+
class MessageVerifier
|
28
|
+
def initialize(secret, digest = 'SHA1')
|
29
|
+
@secret = secret
|
30
|
+
@digest = digest
|
31
|
+
end
|
32
|
+
|
33
|
+
def verify(signed_message)
|
34
|
+
digest, data = signed_message.split("--", 2)
|
35
|
+
if digest != generate_digest(data)
|
36
|
+
raise ActiveSupport::MessageVerifier::InvalidSignature
|
37
|
+
else
|
38
|
+
Marshal.load(data)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def generate(value)
|
43
|
+
data = Marshal.dump(value)
|
44
|
+
digest = generate_digest(data)
|
45
|
+
"#{digest}--#{data}"
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
def generate_digest(data)
|
50
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(@digest), @secret, data)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def set_session(env, sid, session_data, options={})
|
55
|
+
# We hmac-then-encrypt instead of encrypt-then-hmac so that we
|
56
|
+
# can properly detect:
|
57
|
+
# - changes to the encryption key or initialization vector
|
58
|
+
# - a migration from the unencrypted CookieStore.
|
59
|
+
#
|
60
|
+
# Being able to detect these allows us to invalidate the old session data.
|
61
|
+
|
62
|
+
@iv_cipher.encrypt
|
63
|
+
@data_cipher.encrypt
|
64
|
+
@iv_cipher.key = @encryption_key
|
65
|
+
@data_cipher.key = @encryption_key
|
66
|
+
|
67
|
+
clear_session_data = super(env, sid, session_data, {})
|
68
|
+
iv = @data_cipher.random_iv
|
69
|
+
@data_cipher.iv = iv
|
70
|
+
encrypted_iv = @iv_cipher.update(iv) << @iv_cipher.final
|
71
|
+
encrypted_session_data = @data_cipher.update(Marshal.dump(clear_session_data)) << @data_cipher.final
|
72
|
+
|
73
|
+
"#{base64(encrypted_iv)}--#{base64(encrypted_session_data)}"
|
74
|
+
end
|
75
|
+
|
76
|
+
def unpacked_cookie_data(env)
|
77
|
+
env["action_dispatch.request.unsigned_session_cookie"] ||= begin
|
78
|
+
stale_session_check! do
|
79
|
+
request = ActionDispatch::Request.new(env)
|
80
|
+
if (data = request.cookie_jar.signed[@key]) && data.is_a?(String)
|
81
|
+
unmarshal(data)
|
82
|
+
else
|
83
|
+
{}
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def unmarshal(cookie)
|
90
|
+
if cookie
|
91
|
+
b64_encrypted_iv, b64_encrypted_session_data = cookie.split("--", 2)
|
92
|
+
if b64_encrypted_iv && b64_encrypted_session_data
|
93
|
+
encrypted_iv = ::Base64.strict_decode64(b64_encrypted_iv)
|
94
|
+
encrypted_session_data = ::Base64.strict_decode64(b64_encrypted_session_data)
|
95
|
+
|
96
|
+
@iv_cipher.decrypt
|
97
|
+
@iv_cipher.key = @encryption_key
|
98
|
+
iv = @iv_cipher.update(encrypted_iv) << @iv_cipher.final
|
99
|
+
|
100
|
+
@data_cipher.decrypt
|
101
|
+
@data_cipher.key = @encryption_key
|
102
|
+
@data_cipher.iv = iv
|
103
|
+
session_data = Marshal.load(@data_cipher.update(encrypted_session_data) << @data_cipher.final) rescue nil
|
104
|
+
end
|
105
|
+
else
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
rescue OpenSSLCipherError
|
109
|
+
nil
|
110
|
+
end
|
111
|
+
|
112
|
+
# To prevent users from using an insecure encryption key like "Password" we make sure that the
|
113
|
+
# encryption key they've provided is at least 30 characters in length.
|
114
|
+
def ensure_encryption_key_secure(encryption_key)
|
115
|
+
if encryption_key.blank?
|
116
|
+
raise ArgumentError, "An encryption key is required for encrypting the " +
|
117
|
+
"cookie session data. Please set config.action_controller.session = { " +
|
118
|
+
"..., :encryption_key => \"some random string of exactly " +
|
119
|
+
"#{ENCRYPTION_KEY_SIZE * 2} bytes\", ... } in config/environment.rb"
|
120
|
+
end
|
121
|
+
|
122
|
+
if encryption_key.size != ENCRYPTION_KEY_SIZE * 2
|
123
|
+
raise ArgumentError, "The EncryptedCookieStore encryption key must be a " +
|
124
|
+
"hexadecimal string of exactly #{ENCRYPTION_KEY_SIZE * 2} bytes. " +
|
125
|
+
"The value that you've provided, \"#{encryption_key}\", is " +
|
126
|
+
"#{encryption_key.size} bytes. You could use the following (randomly " +
|
127
|
+
"generated) string as encryption key: " +
|
128
|
+
SecureRandom.hex(ENCRYPTION_KEY_SIZE)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def verifier_for(secret, digest)
|
133
|
+
key = secret.respond_to?(:call) ? secret.call : secret
|
134
|
+
MessageVerifier.new(key, digest)
|
135
|
+
end
|
136
|
+
|
137
|
+
def base64(data)
|
138
|
+
::Base64.strict_encode64(data)
|
139
|
+
end
|
140
|
+
|
141
|
+
def unhex(hex_data)
|
142
|
+
[hex_data].pack("H*")
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
namespace :secret do
|
2
|
+
desc "Generate an encryption key for EncryptedCookieStore that's cryptographically secure."
|
3
|
+
task :encryption_key do
|
4
|
+
require 'encrypted_cookie_store/constants'
|
5
|
+
puts ActiveSupport::SecureRandom.hex(EncryptedCookieStoreConstants::ENCRYPTION_KEY_SIZE)
|
6
|
+
end
|
7
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + "/../lib"))
|
2
|
+
require 'rubygems'
|
3
|
+
gem 'rails', '>= 3.0.0'
|
4
|
+
require 'rails'
|
5
|
+
require 'action_controller'
|
6
|
+
require 'encrypted_cookie_store'
|
7
|
+
|
8
|
+
describe EncryptedCookieStore::EncryptedCookieStore do
|
9
|
+
SECRET = "b6a30e998806a238c4bad45cc720ed55e56e50d9f00fff58552e78a20fe8262df61" <<
|
10
|
+
"42fcfdb0676018bb9767ed560d4a624fb7f3603b4e53c77ec189ae3853bd1"
|
11
|
+
GOOD_ENCRYPTION_KEY = "dd458e790c3b995e3606384c58efc53da431db892f585aa3ca2a17eabe6df75b"
|
12
|
+
ANOTHER_GOOD_ENCRYPTION_KEY = "ce6a45c34607d2048d735b0a31a769de4e1512eb83c7012059a66937158a8975"
|
13
|
+
OBJECT = { :user_id => 123, :admin => true, :message => "hello world!" }
|
14
|
+
|
15
|
+
def create(options = {})
|
16
|
+
EncryptedCookieStore::EncryptedCookieStore.new(nil, options.reverse_merge(
|
17
|
+
:key => 'key',
|
18
|
+
:secret => SECRET
|
19
|
+
))
|
20
|
+
end
|
21
|
+
|
22
|
+
it "checks whether an encryption key is given" do
|
23
|
+
lambda { create }.should raise_error(ArgumentError, /encryption key is required/)
|
24
|
+
end
|
25
|
+
|
26
|
+
it "checks whether the encryption key has the correct size" do
|
27
|
+
encryption_key = "too small"
|
28
|
+
block = lambda { create(:encryption_key => encryption_key) }
|
29
|
+
block.should raise_error(ArgumentError, /must be a hexadecimal string of exactly \d+ bytes/)
|
30
|
+
end
|
31
|
+
|
32
|
+
specify "marshalling and unmarshalling data works" do
|
33
|
+
data = create(:encryption_key => GOOD_ENCRYPTION_KEY).send(:set_session, nil, nil, OBJECT)
|
34
|
+
object = create(:encryption_key => GOOD_ENCRYPTION_KEY).send(:unmarshal, data)
|
35
|
+
object[:user_id].should == 123
|
36
|
+
object[:admin].should be_true
|
37
|
+
object[:message].should == "hello world!"
|
38
|
+
end
|
39
|
+
|
40
|
+
it "uses a different initialization vector every time data is marshalled" do
|
41
|
+
store = create(:encryption_key => GOOD_ENCRYPTION_KEY)
|
42
|
+
data1 = store.send(:set_session, nil, nil, OBJECT)
|
43
|
+
data2 = store.send(:set_session, nil, nil, OBJECT)
|
44
|
+
data3 = store.send(:set_session, nil, nil, OBJECT)
|
45
|
+
data4 = store.send(:set_session, nil, nil, OBJECT)
|
46
|
+
data1.should_not == data2
|
47
|
+
data1.should_not == data3
|
48
|
+
data1.should_not == data4
|
49
|
+
end
|
50
|
+
|
51
|
+
it "invalidates the data if the encryption key is changed" do
|
52
|
+
data = create(:encryption_key => GOOD_ENCRYPTION_KEY).send(:set_session, nil, nil, OBJECT)
|
53
|
+
object = create(:encryption_key => ANOTHER_GOOD_ENCRYPTION_KEY).send(:unmarshal, data)
|
54
|
+
object.should be_nil
|
55
|
+
end
|
56
|
+
|
57
|
+
it "invalidates the data if the IV cannot be decrypted" do
|
58
|
+
store = create(:encryption_key => GOOD_ENCRYPTION_KEY)
|
59
|
+
data = store.send(:set_session, nil, nil, OBJECT)
|
60
|
+
iv_cipher = store.instance_variable_get(:@iv_cipher)
|
61
|
+
iv_cipher.should_receive(:update).and_raise(EncryptedCookieStore::EncryptedCookieStore::OpenSSLCipherError)
|
62
|
+
store.send(:unmarshal, data).should be_nil
|
63
|
+
end
|
64
|
+
|
65
|
+
# FIXME: This test case is broken. The super classes' structure have changed since Rails 3.0.0.beta3
|
66
|
+
# and we're no longer getting a string to unmarshal, since this have been pushed up to rack.
|
67
|
+
#it "invalidates the data if we just migrated from CookieStore" do
|
68
|
+
# old_store = ActionDispatch::Session::CookieStore.new(nil, :key => 'key', :secret => SECRET)
|
69
|
+
# legacy_data = old_store.send(:set_session, nil, nil, OBJECT)
|
70
|
+
# store = create(:encryption_key => GOOD_ENCRYPTION_KEY)
|
71
|
+
# store.send(:unmarshal, legacy_data).should be_nil
|
72
|
+
#end
|
73
|
+
|
74
|
+
it "invalidates the data if it was tampered with" do
|
75
|
+
store = create(:encryption_key => GOOD_ENCRYPTION_KEY)
|
76
|
+
data = store.send(:set_session, nil, nil, OBJECT)
|
77
|
+
b64_encrypted_iv, b64_encrypted_session_data = data.split("--", 2)
|
78
|
+
b64_encrypted_session_data[0..1] = "AA"
|
79
|
+
data = "#{b64_encrypted_iv}--#{b64_encrypted_session_data}"
|
80
|
+
store.send(:unmarshal, data).should be_nil
|
81
|
+
end
|
82
|
+
|
83
|
+
it "invalidates the data if it looks like garbage" do
|
84
|
+
store = create(:encryption_key => GOOD_ENCRYPTION_KEY)
|
85
|
+
garbage = "\202d\3477 jTf\274\360\200z\355\334N3\001\0036\321qLu\027\320\325*%:%\270D"
|
86
|
+
store.send(:unmarshal, garbage).should be_nil
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = "validas-encrypted_cookie_store"
|
8
|
+
s.version = "0.4.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["FooBarWidget", "Scott W. Bradley", "Validas Engineering Team"]
|
12
|
+
s.date = "2012-10-30"
|
13
|
+
s.description = "A Rails 3.1 & 3.2 version of Encrypted Cookie Store by FooBarWidget"
|
14
|
+
s.email = "scottwb@gmail.com"
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE.txt",
|
17
|
+
"README.md"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".rspec",
|
21
|
+
".travis.yml",
|
22
|
+
"Gemfile",
|
23
|
+
"Gemfile.lock",
|
24
|
+
"LICENSE.txt",
|
25
|
+
"README.md",
|
26
|
+
"Rakefile",
|
27
|
+
"VERSION",
|
28
|
+
"lib/encrypted_cookie_store.rb",
|
29
|
+
"lib/encrypted_cookie_store/constants.rb",
|
30
|
+
"lib/encrypted_cookie_store/encrypted_cookie_store.rb",
|
31
|
+
"lib/encrypted_cookie_store/railtie.rb",
|
32
|
+
"lib/tasks/encrypted_cookie_store.rake",
|
33
|
+
"spec/encrypted_cookie_store_spec.rb",
|
34
|
+
"validas-encrypted_cookie_store.gemspec"
|
35
|
+
]
|
36
|
+
s.homepage = "https://github.com/validas/encrypted_cookie_store"
|
37
|
+
s.require_paths = ["lib"]
|
38
|
+
s.rubygems_version = "1.8.24"
|
39
|
+
s.summary = "A Rails 3.1 & 3.2 version of Encrypted Cookie Store by FooBarWidget"
|
40
|
+
|
41
|
+
if s.respond_to? :specification_version then
|
42
|
+
s.specification_version = 3
|
43
|
+
|
44
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
45
|
+
s.add_runtime_dependency(%q<rails>, ["~> 3.2.0"])
|
46
|
+
s.add_development_dependency(%q<rake>, [">= 0"])
|
47
|
+
s.add_development_dependency(%q<bundler>, [">= 0"])
|
48
|
+
s.add_development_dependency(%q<rspec>, ["~> 2.6.0"])
|
49
|
+
s.add_development_dependency(%q<jeweler>, [">= 0"])
|
50
|
+
else
|
51
|
+
s.add_dependency(%q<rails>, ["~> 3.2.0"])
|
52
|
+
s.add_dependency(%q<rake>, [">= 0"])
|
53
|
+
s.add_dependency(%q<bundler>, [">= 0"])
|
54
|
+
s.add_dependency(%q<rspec>, ["~> 2.6.0"])
|
55
|
+
s.add_dependency(%q<jeweler>, [">= 0"])
|
56
|
+
end
|
57
|
+
else
|
58
|
+
s.add_dependency(%q<rails>, ["~> 3.2.0"])
|
59
|
+
s.add_dependency(%q<rake>, [">= 0"])
|
60
|
+
s.add_dependency(%q<bundler>, [">= 0"])
|
61
|
+
s.add_dependency(%q<rspec>, ["~> 2.6.0"])
|
62
|
+
s.add_dependency(%q<jeweler>, [">= 0"])
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
metadata
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: validas-encrypted_cookie_store
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.4.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- FooBarWidget
|
9
|
+
- Scott W. Bradley
|
10
|
+
- Validas Engineering Team
|
11
|
+
autorequire:
|
12
|
+
bindir: bin
|
13
|
+
cert_chain: []
|
14
|
+
date: 2012-10-30 00:00:00.000000000 Z
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: rails
|
18
|
+
requirement: !ruby/object:Gem::Requirement
|
19
|
+
none: false
|
20
|
+
requirements:
|
21
|
+
- - ~>
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 3.2.0
|
24
|
+
type: :runtime
|
25
|
+
prerelease: false
|
26
|
+
version_requirements: !ruby/object:Gem::Requirement
|
27
|
+
none: false
|
28
|
+
requirements:
|
29
|
+
- - ~>
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: 3.2.0
|
32
|
+
- !ruby/object:Gem::Dependency
|
33
|
+
name: rake
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
35
|
+
none: false
|
36
|
+
requirements:
|
37
|
+
- - ! '>='
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
type: :development
|
41
|
+
prerelease: false
|
42
|
+
version_requirements: !ruby/object:Gem::Requirement
|
43
|
+
none: false
|
44
|
+
requirements:
|
45
|
+
- - ! '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: bundler
|
50
|
+
requirement: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
type: :development
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: !ruby/object:Gem::Requirement
|
59
|
+
none: false
|
60
|
+
requirements:
|
61
|
+
- - ! '>='
|
62
|
+
- !ruby/object:Gem::Version
|
63
|
+
version: '0'
|
64
|
+
- !ruby/object:Gem::Dependency
|
65
|
+
name: rspec
|
66
|
+
requirement: !ruby/object:Gem::Requirement
|
67
|
+
none: false
|
68
|
+
requirements:
|
69
|
+
- - ~>
|
70
|
+
- !ruby/object:Gem::Version
|
71
|
+
version: 2.6.0
|
72
|
+
type: :development
|
73
|
+
prerelease: false
|
74
|
+
version_requirements: !ruby/object:Gem::Requirement
|
75
|
+
none: false
|
76
|
+
requirements:
|
77
|
+
- - ~>
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
version: 2.6.0
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: jeweler
|
82
|
+
requirement: !ruby/object:Gem::Requirement
|
83
|
+
none: false
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
87
|
+
version: '0'
|
88
|
+
type: :development
|
89
|
+
prerelease: false
|
90
|
+
version_requirements: !ruby/object:Gem::Requirement
|
91
|
+
none: false
|
92
|
+
requirements:
|
93
|
+
- - ! '>='
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '0'
|
96
|
+
description: A Rails 3.1 & 3.2 version of Encrypted Cookie Store by FooBarWidget
|
97
|
+
email: scottwb@gmail.com
|
98
|
+
executables: []
|
99
|
+
extensions: []
|
100
|
+
extra_rdoc_files:
|
101
|
+
- LICENSE.txt
|
102
|
+
- README.md
|
103
|
+
files:
|
104
|
+
- .rspec
|
105
|
+
- .travis.yml
|
106
|
+
- Gemfile
|
107
|
+
- Gemfile.lock
|
108
|
+
- LICENSE.txt
|
109
|
+
- README.md
|
110
|
+
- Rakefile
|
111
|
+
- VERSION
|
112
|
+
- lib/encrypted_cookie_store.rb
|
113
|
+
- lib/encrypted_cookie_store/constants.rb
|
114
|
+
- lib/encrypted_cookie_store/encrypted_cookie_store.rb
|
115
|
+
- lib/encrypted_cookie_store/railtie.rb
|
116
|
+
- lib/tasks/encrypted_cookie_store.rake
|
117
|
+
- spec/encrypted_cookie_store_spec.rb
|
118
|
+
- validas-encrypted_cookie_store.gemspec
|
119
|
+
homepage: https://github.com/validas/encrypted_cookie_store
|
120
|
+
licenses: []
|
121
|
+
post_install_message:
|
122
|
+
rdoc_options: []
|
123
|
+
require_paths:
|
124
|
+
- lib
|
125
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
126
|
+
none: false
|
127
|
+
requirements:
|
128
|
+
- - ! '>='
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
|
+
none: false
|
133
|
+
requirements:
|
134
|
+
- - ! '>='
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '0'
|
137
|
+
requirements: []
|
138
|
+
rubyforge_project:
|
139
|
+
rubygems_version: 1.8.24
|
140
|
+
signing_key:
|
141
|
+
specification_version: 3
|
142
|
+
summary: A Rails 3.1 & 3.2 version of Encrypted Cookie Store by FooBarWidget
|
143
|
+
test_files: []
|