rack-session-encryptedcookie 0.2.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 856832c8f3652b96214a5bd20c2247ab9eef5e11
4
+ data.tar.gz: cec97853bb6a2d761fcc0b4622d6d56f441f36ef
5
+ SHA512:
6
+ metadata.gz: f738b4841d55c5527f6b33b4518664452d86affee34bc7af6f802544e3fec4f633778a67faa39ef529ec3c5c51698d99d82ee041c62de8d1f91745e74b214659
7
+ data.tar.gz: ab2bc73c700c5aa333844709c57c11cefd915339830eb95cee80769f0c3a5e80391cfe39821aad901a524facb416641984413893419926215c6ffd12025dc833
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012, Tim Hentenaar
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
+ 1. Redistributions of source code must retain the above copyright notice, this
8
+ list of conditions and the following disclaimer.
9
+ 2. 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
+
13
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
14
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
15
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
16
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
17
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
18
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
19
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
20
+ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
21
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
22
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,102 @@
1
+ rack-session-encryptedcookie
2
+ ============================
3
+
4
+ [![Travis CI Status](https://secure.travis-ci.org/thentenaar/rack-session-encryptedcookie.svg?branch=master)](https://travis-ci.org/thentenaar/rack-session-encryptedcookie)
5
+
6
+ Rack session handling middleware that serializes the session data into
7
+ an encrypted cookie; that's also async-aware.
8
+
9
+ This is probably not the **most** secure solution, but it's better than storing your session
10
+ data in a cookie as clear text. That being said, it's _much_ more secure to use a
11
+ pre-generated key with this module than a password-derived key, but the latter is
12
+ provided as a convenience option.
13
+
14
+ If you have strict security requirements, you really shouldn't be storing sensitive data in
15
+ the session.
16
+
17
+ Licensing
18
+ =========
19
+
20
+ This software is licensed under the [Simplified BSD License](http://en.wikipedia.org/wiki/BSD_licenses#2-clause_license_.28.22Simplified_BSD_License.22_or_.22FreeBSD_License.22.29) as described in the LICENSE file.
21
+
22
+ Requirements
23
+ ============
24
+
25
+ * rack
26
+
27
+ Installation
28
+ ============
29
+
30
+ gem install rack-session-encryptedcookie
31
+
32
+ Usage
33
+ =====
34
+
35
+ Just add something like this to your _config.ru_:
36
+
37
+ ```ruby
38
+ require 'rack/session/encryptedcookie'
39
+
40
+ use Rack::Session::EncryptedCookie domain: 'domain.name', salt: 'salthere', key: 'my_secret'
41
+ ```
42
+
43
+ ... and you can access the session hash via ``env['rack.session']`` per
44
+ usual.
45
+
46
+ The full list of options is:
47
+ | ``cookie_name`` | Cookie name (default: 'rack.session') |
48
+ | ``domain`` | Domain for the cookie (mandatory) |
49
+ | ``http_only`` | HttpOnly for the cookie |
50
+ | ``expires`` | Cookie expiry (in seconds, optional) |
51
+ | ``cipher`` | OpenSSL cipher to use (default: aes-256-cbc) |
52
+ | ``salt`` | Salt for the IV (password-derrived key) |
53
+ | ``rounds`` | Number of salting rounds (password-derrived key) |
54
+ | ``key`` | Encryption key / password for the cookie |
55
+ | ``tag_len`` | Tag length (for GCM/CCM ciphers, optional) |
56
+
57
+ Generating your own Key
58
+ =======================
59
+
60
+ You can generate a key using something like:
61
+ ```ruby
62
+ SecureRandom.random_bytes(key_size_in_bytes)
63
+ ```
64
+ or anything else, as long as the key is the proper size for the cipher.
65
+
66
+ Using a pre-generated Key
67
+ =========================
68
+
69
+ To use a pre-generated key, you must specify the following options:
70
+ ```ruby
71
+ cipher: 'aes-256-cbc', # The cipher algorithm to use (defaults to aes-256-cbc)
72
+ key: your_key_here, # Your pre-generated key
73
+ ```
74
+
75
+ Examples:
76
+ ```ruby
77
+ # Using the default cipher
78
+ use Rack::Session::EncryptedCookie, key: your_key
79
+
80
+ # Using the specified cipher
81
+ use Rack::Session::EncryptedCookie, cipher: your_cipher, key: your_key
82
+ ```
83
+
84
+ Using a password-derived key
85
+ =============================
86
+
87
+ You can derive a key by specifying the following options:
88
+ ```ruby
89
+ cipher 'aes-256-cbc', # The cipher algorithm to use (default aes-256-cbc)
90
+ salt 'salthere', # Salt to use for key generation
91
+ rounds: 2000, # Number of cipher rounds for key generation (default: 2000)
92
+ key: 'yoursecret', # A password from which to generate the key
93
+ ```
94
+
95
+ ``crypto_key`` and ``salt`` must be specified in order to enable encryption.
96
+ All other options have defaults available.
97
+
98
+ Example:
99
+ ```ruby
100
+ use Rack::Session::EncryptedCookie, salt: 'salthere', crypto_key: 'my_secret'
101
+ ```
102
+
@@ -0,0 +1,192 @@
1
+ #
2
+ # Rack::Session::EncryptedCookie - Encrypted session middleware for Rack
3
+ #
4
+ # Copyright (C) 2013 - 2017 Tim Hentenaar. All Rights Reserved.
5
+ #
6
+ # Licensed under the Simplified BSD License.
7
+ # See the LICENSE file for details.
8
+ #
9
+
10
+ require 'rack/request'
11
+ require 'rack/utils'
12
+ require 'openssl'
13
+
14
+ module Rack
15
+ module Session
16
+ class EncryptedCookie
17
+ NOT_FOUND = [ 404, {}, [ 'Not found' ]].freeze
18
+
19
+ # @param [Hash] opts Session options
20
+ # @option opts [String] :cookie_name Cookie name
21
+ # @option opts [String] :domain Domain for the cookie
22
+ # @option opts [Boolean] :http_only HttpOnly for the cookie
23
+ # @option opts [Integer] :expires Cookie expiry (in seconds)
24
+ # @option opts [String] :cipher OpenSSL cipher to use
25
+ # @option opts [String] :salt Salt for the IV
26
+ # @optons opts [Integer] :rounds Number of salting rounds
27
+ # @option opts [String] :key Encryption key for the data
28
+ # @option opts [Integer] :tag_len Tag length (for GCM/CCM ciphers)
29
+ def initialize(app, opts={})
30
+ @app = app
31
+ @hash = {}
32
+ @opts = {
33
+ cookie_name: 'rack.session',
34
+ domain: nil,
35
+ http_only: false,
36
+ expires: (15 * 60),
37
+ cipher: 'aes-256-cbc',
38
+ salt: '3@bG>B@J5vy-FeXJ',
39
+ rounds: 2000,
40
+ key: 'r`*BqnG:c^;AL{k97=KYN!#',
41
+ tag_len: 16
42
+ }.merge(opts)
43
+ end
44
+
45
+ def call(env)
46
+ dup.call!(env)
47
+ end
48
+
49
+ def call!(env)
50
+ @cb = env['async.callback']
51
+ env['async.callback'] = method(:save_session) if @cb
52
+ env['rack.session'] = self
53
+ load_session(env)
54
+
55
+ if @app
56
+ @cb ? @app.call(env) : save_session(@app.call(env))
57
+ else
58
+ @cb ? @cb.call(NOT_FOUND) : NOT_FOUND
59
+ end
60
+ end
61
+
62
+ def method_missing(method, *args, &block)
63
+ if @hash.respond_to?(method)
64
+ @hash.send(method, *args, &block)
65
+ else
66
+ raise ArgumentError.new("Method `#{method}` doesn't exist.")
67
+ end
68
+ end
69
+
70
+ private
71
+
72
+ # Load the sesssion data from the cookie
73
+ # @return [Hash, nil] Session data
74
+ def load_session(env)
75
+ r = Rack::Request.new(env)
76
+ cookie = r.cookies[@opts[:cookie_name]]
77
+ return if cookie.nil?
78
+ @hash = Marshal.load(cipher(:decrypt, cookie)) rescue {}
79
+ end
80
+
81
+ # Add our cookie to the response
82
+ #
83
+ # @param [Array] r Upstream Rack response
84
+ # @return [Array] Rack response + our cookie
85
+ def save_session(r)
86
+ return r if !r.is_a?(Array) || (r.is_a?(Array) && r[0] == -1)
87
+
88
+ unless @hash.empty? || @opts[:domain].nil?
89
+ data = cipher(:encrypt, Marshal.dump(@hash)) rescue nil
90
+ c = {
91
+ value: data,
92
+ domain: @opts[:domain],
93
+ path: '/',
94
+ }
95
+
96
+ c[:httponly] = @opts[:http_only] === true
97
+ if @opts.has_key?(:expires)
98
+ c[:expires] = Time.at(Time.now + @opts[:expires])
99
+ end
100
+
101
+ r[1]['Set-Cookie'] = Rack::Utils.add_cookie_to_header(
102
+ r[1]['Set-Cookie'], @opts[:cookie_name], c
103
+ ) unless data.nil?
104
+ end
105
+
106
+ @cb.call(r) if @cb
107
+ r
108
+ end
109
+
110
+ # Warn the user that en/de-cryption failed
111
+ # @param [String] e Exception message
112
+ # @return nil
113
+ def cipher_failed(e='<no message>')
114
+ warn (<<-XXX.gsub(/^\s*/, ''))
115
+ SECURITY WARNING: Session cipher failed: #{e}
116
+ XXX
117
+ return nil
118
+ end
119
+
120
+ # Handle en/de-cryption
121
+ # @param [Symbol] :mode :encrypt or :decrypt
122
+ # @param [String] :str Data to en/de-crypt
123
+ # @return [String, nil] Encrypted data
124
+ def cipher(mode, str)
125
+ return nil if @opts[:key].nil? || str.nil?
126
+ begin
127
+ cipher = OpenSSL::Cipher::Cipher.new(@opts[:cipher])
128
+ cipher.send(mode)
129
+ rescue
130
+ return cipher_failed($!.message)
131
+ end
132
+
133
+ # Set the key and IV
134
+ if @opts[:salt].nil?
135
+ cipher.key = @opts[:key]
136
+ else
137
+ cipher.key = OpenSSL::PKCS5.pbkdf2_hmac_sha1(
138
+ @opts[:key], @opts[:salt],
139
+ @opts[:rounds], cipher.key_len
140
+ )
141
+ end
142
+
143
+ # Setup the auth data for GCM/CCM
144
+ if cipher.name.match(%r{/[CG]CM$/i})
145
+ cipher.auth_data = ''
146
+ cipher.tag_len = @opts[:tag_len]
147
+ end
148
+
149
+ iv = cipher.random_iv
150
+ xstr = str
151
+
152
+ if mode == :decrypt
153
+ str = str.unpack('m').first
154
+
155
+ # Set the tag for GCM/CCM
156
+ if cipher.name.match(%r{/[CG]CM$/i})
157
+ cipher.auth_tag = str.slice!(0, cipher.tag_len).unpack('C*')
158
+ end
159
+
160
+ # Extract the iv
161
+ iv_len = iv.length
162
+ str_b,iv = Array[str[0 ... iv_len << 1].unpack('C*')].transpose.
163
+ partition.with_index { |x,i| (i&1).zero? }
164
+ iv.flatten! ; str_b.flatten!
165
+
166
+ # Set the IV and buffer
167
+ iv = iv.pack('C*')
168
+ xstr = str_b.pack('C*') + str[iv_len << 1 ... str.length]
169
+ end
170
+
171
+ # Call the cipher
172
+ r = nil
173
+ cipher.iv = iv
174
+ begin
175
+ r = cipher.update(xstr) + cipher.final
176
+ if mode == :encrypt
177
+ d = r.bytes.zip(iv.bytes).flatten.compact
178
+ if cipher.name.match(%r{/[CG]CM$/i})
179
+ d = cipher.tag.bytes + d.bytes
180
+ end
181
+ r = [d.pack('C*')].pack('m').chomp
182
+ end
183
+ rescue OpenSSL::Cipher::CipherError
184
+ return cipher_failed($!.message)
185
+ end
186
+ r
187
+ end
188
+ end
189
+ end
190
+ end
191
+
192
+ # vi:set ts=2 sw=2 et sta:
metadata ADDED
@@ -0,0 +1,89 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rack-session-encryptedcookie
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Tim Hentenaar
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2017-12-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '12.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '12.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '3.5'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '3.5'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rack
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '2.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '2.0'
55
+ description: |2
56
+ Rack middleware that persists session data in an encrypted cookie
57
+ email: tim.hentenaar@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - LICENSE
63
+ - README.md
64
+ - lib/rack/session/encryptedcookie.rb
65
+ homepage: https://github.com/thentenaar/rack-session-encryptedcookie
66
+ licenses:
67
+ - BSD-2-Clause
68
+ metadata: {}
69
+ post_install_message:
70
+ rdoc_options: []
71
+ require_paths:
72
+ - lib
73
+ required_ruby_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ required_rubygems_version: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ requirements: []
84
+ rubyforge_project:
85
+ rubygems_version: 2.6.14
86
+ signing_key:
87
+ specification_version: 4
88
+ summary: Encrypted session middleware for Rack
89
+ test_files: []