rack-session 2.0.0 → 2.1.2
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.
- checksums.yaml +4 -4
- data/lib/rack/session/abstract/id.rb +4 -2
- data/lib/rack/session/cookie.rb +4 -2
- data/lib/rack/session/encryptor.rb +345 -122
- data/lib/rack/session/pool.rb +7 -1
- data/lib/rack/session/version.rb +1 -1
- data/lib/rack/session.rb +0 -1
- data/releases.md +31 -0
- metadata +21 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 82ef73dda808550f0838e5fa7d75d90a15d1b524831c58a5e082349143637357
|
|
4
|
+
data.tar.gz: 4214302b0ec644f8b905d575d870a7fc4de5f4c8e1450a104e7193eb117b70f2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 17b3713d1ba66fd9c40f825826f05be73d60681b51b06e8df561efa4a146c537b323ff2fc436cc8ddf75314a26f5839e89d98bcc270746ad70e18af441e1a7ae
|
|
7
|
+
data.tar.gz: 5a06e7eec1398684eaca507d0c69063cfcfae2bb8478255d43c49ce01d9db5bc551e1cb593bed6872718ad925ea5145ea4acb95cf6f0eae26182b7af5d37a83a
|
|
@@ -215,7 +215,7 @@ module Rack
|
|
|
215
215
|
# All parameters are optional.
|
|
216
216
|
# * :key determines the name of the cookie, by default it is
|
|
217
217
|
# 'rack.session'
|
|
218
|
-
# * :path, :domain, :expire_after, :secure, :httponly, and :same_site set
|
|
218
|
+
# * :path, :domain, :expire_after, :secure, :httponly, :partitioned and :same_site set
|
|
219
219
|
# the related cookie options as by Rack::Response#set_cookie
|
|
220
220
|
# * :skip will not a set a cookie in the response nor update the session state
|
|
221
221
|
# * :defer will not set a cookie in the response but still update the session
|
|
@@ -244,6 +244,7 @@ module Rack
|
|
|
244
244
|
expire_after: nil,
|
|
245
245
|
secure: false,
|
|
246
246
|
httponly: true,
|
|
247
|
+
partitioned: false,
|
|
247
248
|
defer: false,
|
|
248
249
|
renew: false,
|
|
249
250
|
sidbits: 128,
|
|
@@ -257,6 +258,7 @@ module Rack
|
|
|
257
258
|
@app = app
|
|
258
259
|
@default_options = self.class::DEFAULT_OPTIONS.merge(options)
|
|
259
260
|
@key = @default_options.delete(:key)
|
|
261
|
+
@assume_ssl = @default_options.delete(:assume_ssl)
|
|
260
262
|
@cookie_only = @default_options.delete(:cookie_only)
|
|
261
263
|
@same_site = @default_options.delete(:same_site)
|
|
262
264
|
initialize_sid
|
|
@@ -368,7 +370,7 @@ module Rack
|
|
|
368
370
|
|
|
369
371
|
def security_matches?(request, options)
|
|
370
372
|
return true unless options[:secure]
|
|
371
|
-
request.ssl?
|
|
373
|
+
request.ssl? || @assume_ssl == true
|
|
372
374
|
end
|
|
373
375
|
|
|
374
376
|
# Acquires the session from the environment and the session id from
|
data/lib/rack/session/cookie.rb
CHANGED
|
@@ -237,8 +237,10 @@ module Rack
|
|
|
237
237
|
# Decode using legacy HMAC decoder
|
|
238
238
|
session_data = @legacy_hmac_coder.decode(session_data)
|
|
239
239
|
|
|
240
|
-
elsif !session_data && coder
|
|
241
|
-
# Use the coder option, which has the potential to be very unsafe
|
|
240
|
+
elsif !session_data && encryptors.empty? && coder
|
|
241
|
+
# Use the coder option, which has the potential to be very unsafe.
|
|
242
|
+
# This path is only reached when no encryptors (secrets:) are configured;
|
|
243
|
+
# if encryptors are present but decryption failed, the cookie is rejected.
|
|
242
244
|
session_data = coder.decode(cookie_data)
|
|
243
245
|
end
|
|
244
246
|
end
|
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
# Copyright, 2022, by Philip Arndt.
|
|
6
6
|
|
|
7
7
|
require 'base64'
|
|
8
|
+
require 'json'
|
|
8
9
|
require 'openssl'
|
|
9
10
|
require 'securerandom'
|
|
10
|
-
require 'zlib'
|
|
11
11
|
|
|
12
12
|
require 'rack/utils'
|
|
13
13
|
|
|
@@ -23,169 +23,392 @@ module Rack
|
|
|
23
23
|
class InvalidMessage < Error
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
# for an HMAC key.
|
|
29
|
-
#
|
|
30
|
-
# Options may include:
|
|
31
|
-
# * :serialize_json
|
|
32
|
-
# Use JSON for message serialization instead of Marshal. This can be
|
|
33
|
-
# viewed as a security enhancement.
|
|
34
|
-
# * :pad_size
|
|
35
|
-
# Pad encrypted message data, to a multiple of this many bytes
|
|
36
|
-
# (default: 32). This can be between 2-4096 bytes, or +nil+ to disable
|
|
37
|
-
# padding.
|
|
38
|
-
# * :purpose
|
|
39
|
-
# Limit messages to a specific purpose. This can be viewed as a
|
|
40
|
-
# security enhancement to prevent message reuse from different contexts
|
|
41
|
-
# if keys are reused.
|
|
42
|
-
#
|
|
43
|
-
# Cryptography and Output Format:
|
|
44
|
-
#
|
|
45
|
-
# urlsafe_encode64(version + random_data + IV + encrypted data + HMAC)
|
|
46
|
-
#
|
|
47
|
-
# Where:
|
|
48
|
-
# * version - 1 byte and is currently always 0x01
|
|
49
|
-
# * random_data - 32 bytes used for generating the per-message secret
|
|
50
|
-
# * IV - 16 bytes random initialization vector
|
|
51
|
-
# * HMAC - 32 bytes HMAC-SHA-256 of all preceding data, plus the purpose
|
|
52
|
-
# value
|
|
53
|
-
def initialize(secret, opts = {})
|
|
54
|
-
raise ArgumentError, "secret must be a String" unless String === secret
|
|
55
|
-
raise ArgumentError, "invalid secret: #{secret.bytesize}, must be >=64" unless secret.bytesize >= 64
|
|
26
|
+
module Serializable
|
|
27
|
+
private
|
|
56
28
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
29
|
+
# Returns a serialized payload of the message. If a :pad_size is supplied,
|
|
30
|
+
# the message will be padded. The first 2 bytes of the returned string will
|
|
31
|
+
# indicating the amount of padding.
|
|
32
|
+
def serialize_payload(message)
|
|
33
|
+
serialized_data = serializer.dump(message)
|
|
34
|
+
|
|
35
|
+
return "#{[0].pack('v')}#{serialized_data.force_encoding(Encoding::BINARY)}" if @options[:pad_size].nil?
|
|
36
|
+
|
|
37
|
+
padding_bytes = @options[:pad_size] - (2 + serialized_data.size) % @options[:pad_size]
|
|
38
|
+
padding_data = SecureRandom.random_bytes(padding_bytes)
|
|
39
|
+
|
|
40
|
+
"#{[padding_bytes].pack('v')}#{padding_data}#{serialized_data.force_encoding(Encoding::BINARY)}"
|
|
64
41
|
end
|
|
65
42
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
43
|
+
# Return the deserialized message. The first 2 bytes will be read as the
|
|
44
|
+
# amount of padding.
|
|
45
|
+
def deserialized_message(data)
|
|
46
|
+
# Read the first 2 bytes as the padding_bytes size
|
|
47
|
+
padding_bytes, = data.unpack('v')
|
|
69
48
|
|
|
70
|
-
|
|
71
|
-
|
|
49
|
+
# Slice out the serialized_data and deserialize it
|
|
50
|
+
serialized_data = data.slice(2 + padding_bytes, data.bytesize)
|
|
51
|
+
serializer.load serialized_data
|
|
52
|
+
end
|
|
72
53
|
|
|
73
|
-
|
|
74
|
-
|
|
54
|
+
def serializer
|
|
55
|
+
@serializer ||= @options[:serialize_json] ? JSON : Marshal
|
|
56
|
+
end
|
|
75
57
|
end
|
|
76
58
|
|
|
77
|
-
|
|
78
|
-
|
|
59
|
+
class V1
|
|
60
|
+
include Serializable
|
|
61
|
+
|
|
62
|
+
# The secret String must be at least 64 bytes in size. The first 32 bytes
|
|
63
|
+
# will be used for the encryption cipher key. The remainder will be used
|
|
64
|
+
# for an HMAC key.
|
|
65
|
+
#
|
|
66
|
+
# Options may include:
|
|
67
|
+
# * :serialize_json
|
|
68
|
+
# Use JSON for message serialization instead of Marshal. This can be
|
|
69
|
+
# viewed as a security enhancement.
|
|
70
|
+
# * :pad_size
|
|
71
|
+
# Pad encrypted message data, to a multiple of this many bytes
|
|
72
|
+
# (default: 32). This can be between 2-4096 bytes, or +nil+ to disable
|
|
73
|
+
# padding.
|
|
74
|
+
# * :purpose
|
|
75
|
+
# Limit messages to a specific purpose. This can be viewed as a
|
|
76
|
+
# security enhancement to prevent message reuse from different contexts
|
|
77
|
+
# if keys are reused.
|
|
78
|
+
#
|
|
79
|
+
# Cryptography and Output Format:
|
|
80
|
+
#
|
|
81
|
+
# urlsafe_encode64(version + random_data + IV + encrypted data + HMAC)
|
|
82
|
+
#
|
|
83
|
+
# Where:
|
|
84
|
+
# * version - 1 byte with value 0x01
|
|
85
|
+
# * random_data - 32 bytes used for generating the per-message secret
|
|
86
|
+
# * IV - 16 bytes random initialization vector
|
|
87
|
+
# * HMAC - 32 bytes HMAC-SHA-256 of all preceding data, plus the purpose
|
|
88
|
+
# value
|
|
89
|
+
def initialize(secret, opts = {})
|
|
90
|
+
raise ArgumentError, 'secret must be a String' unless secret.is_a?(String)
|
|
91
|
+
raise ArgumentError, "invalid secret: #{secret.bytesize}, must be >=64" unless secret.bytesize >= 64
|
|
92
|
+
|
|
93
|
+
case opts[:pad_size]
|
|
94
|
+
when nil
|
|
95
|
+
# padding is disabled
|
|
96
|
+
when Integer
|
|
97
|
+
raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}" unless (2..4096).include? opts[:pad_size]
|
|
98
|
+
else
|
|
99
|
+
raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}; must be Integer or nil"
|
|
100
|
+
end
|
|
79
101
|
|
|
80
|
-
|
|
102
|
+
@options = {
|
|
103
|
+
serialize_json: false, pad_size: 32, purpose: nil
|
|
104
|
+
}.update(opts)
|
|
81
105
|
|
|
82
|
-
|
|
106
|
+
@hmac_secret = secret.dup.force_encoding(Encoding::BINARY)
|
|
107
|
+
@cipher_secret = @hmac_secret.slice!(0, 32)
|
|
83
108
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
cipher_iv = data.slice!(0, 16)
|
|
109
|
+
@hmac_secret.freeze
|
|
110
|
+
@cipher_secret.freeze
|
|
111
|
+
end
|
|
88
112
|
|
|
89
|
-
|
|
90
|
-
|
|
113
|
+
def decrypt(base64_data)
|
|
114
|
+
data = Base64.urlsafe_decode64(base64_data)
|
|
91
115
|
|
|
92
|
-
|
|
116
|
+
signature = data.slice!(-32..-1)
|
|
117
|
+
verify_authenticity!(data, signature)
|
|
93
118
|
|
|
94
|
-
|
|
95
|
-
|
|
119
|
+
version = data.slice!(0, 1)
|
|
120
|
+
raise InvalidMessage, 'wrong version' unless version == "\1"
|
|
96
121
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
raise InvalidSignature, 'Message invalid'
|
|
100
|
-
end
|
|
122
|
+
message_secret = data.slice!(0, 32)
|
|
123
|
+
cipher_iv = data.slice!(0, 16)
|
|
101
124
|
|
|
102
|
-
|
|
103
|
-
|
|
125
|
+
cipher = new_cipher
|
|
126
|
+
cipher.decrypt
|
|
104
127
|
|
|
105
|
-
|
|
106
|
-
message_secret, cipher_secret = new_message_and_cipher_secret
|
|
128
|
+
set_cipher_key(cipher, cipher_secret_from_message_secret(message_secret))
|
|
107
129
|
|
|
108
|
-
|
|
109
|
-
|
|
130
|
+
cipher.iv = cipher_iv
|
|
131
|
+
data = cipher.update(data) << cipher.final
|
|
110
132
|
|
|
111
|
-
|
|
133
|
+
deserialized_message data
|
|
134
|
+
rescue ArgumentError
|
|
135
|
+
raise InvalidSignature, 'Message invalid'
|
|
136
|
+
end
|
|
112
137
|
|
|
113
|
-
|
|
138
|
+
def encrypt(message)
|
|
139
|
+
version = "\1"
|
|
114
140
|
|
|
115
|
-
|
|
141
|
+
serialized_payload = serialize_payload(message)
|
|
142
|
+
message_secret, cipher_secret = new_message_and_cipher_secret
|
|
116
143
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
data << message_secret
|
|
120
|
-
data << cipher_iv
|
|
121
|
-
data << encrypted_data
|
|
122
|
-
data << compute_signature(data)
|
|
144
|
+
cipher = new_cipher
|
|
145
|
+
cipher.encrypt
|
|
123
146
|
|
|
124
|
-
|
|
125
|
-
end
|
|
147
|
+
set_cipher_key(cipher, cipher_secret)
|
|
126
148
|
|
|
127
|
-
|
|
149
|
+
cipher_iv = cipher.random_iv
|
|
128
150
|
|
|
129
|
-
|
|
130
|
-
OpenSSL::Cipher.new('aes-256-ctr')
|
|
131
|
-
end
|
|
151
|
+
encrypted_data = cipher.update(serialized_payload) << cipher.final
|
|
132
152
|
|
|
133
|
-
|
|
134
|
-
|
|
153
|
+
data = String.new
|
|
154
|
+
data << version
|
|
155
|
+
data << message_secret
|
|
156
|
+
data << cipher_iv
|
|
157
|
+
data << encrypted_data
|
|
158
|
+
data << compute_signature(data)
|
|
135
159
|
|
|
136
|
-
|
|
137
|
-
|
|
160
|
+
Base64.urlsafe_encode64(data)
|
|
161
|
+
end
|
|
138
162
|
|
|
139
|
-
|
|
140
|
-
OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, @cipher_secret, message_secret)
|
|
141
|
-
end
|
|
163
|
+
private
|
|
142
164
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
165
|
+
def new_cipher
|
|
166
|
+
OpenSSL::Cipher.new('aes-256-ctr')
|
|
167
|
+
end
|
|
146
168
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
169
|
+
def new_message_and_cipher_secret
|
|
170
|
+
message_secret = SecureRandom.random_bytes(32)
|
|
171
|
+
|
|
172
|
+
[message_secret, cipher_secret_from_message_secret(message_secret)]
|
|
173
|
+
end
|
|
150
174
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
175
|
+
def cipher_secret_from_message_secret(message_secret)
|
|
176
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), @cipher_secret, message_secret)
|
|
177
|
+
end
|
|
154
178
|
|
|
155
|
-
|
|
156
|
-
|
|
179
|
+
def set_cipher_key(cipher, key)
|
|
180
|
+
cipher.key = key
|
|
181
|
+
end
|
|
157
182
|
|
|
158
|
-
|
|
159
|
-
|
|
183
|
+
def compute_signature(data)
|
|
184
|
+
signing_data = data
|
|
185
|
+
signing_data += @options[:purpose] if @options[:purpose]
|
|
160
186
|
|
|
161
|
-
|
|
162
|
-
|
|
187
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), @hmac_secret, signing_data)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def verify_authenticity!(data, signature)
|
|
191
|
+
raise InvalidMessage, 'Message is invalid' if data.nil? || signature.nil?
|
|
192
|
+
|
|
193
|
+
unless Rack::Utils.secure_compare(signature, compute_signature(data))
|
|
194
|
+
raise InvalidSignature, 'HMAC is invalid'
|
|
195
|
+
end
|
|
163
196
|
end
|
|
164
197
|
end
|
|
165
198
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
199
|
+
class V2
|
|
200
|
+
include Serializable
|
|
201
|
+
|
|
202
|
+
# The secret String must be at least 32 bytes in size.
|
|
203
|
+
#
|
|
204
|
+
# Options may include:
|
|
205
|
+
# * :pad_size
|
|
206
|
+
# Pad encrypted message data, to a multiple of this many bytes
|
|
207
|
+
# (default: 32). This can be between 2-4096 bytes, or +nil+ to disable
|
|
208
|
+
# padding.
|
|
209
|
+
# * :purpose
|
|
210
|
+
# Limit messages to a specific purpose. This can be viewed as a
|
|
211
|
+
# security enhancement to prevent message reuse from different contexts
|
|
212
|
+
# if keys are reused.
|
|
213
|
+
#
|
|
214
|
+
# Cryptography and Output Format:
|
|
215
|
+
#
|
|
216
|
+
# strict_encode64(version + salt + IV + authentication tag + ciphertext)
|
|
217
|
+
#
|
|
218
|
+
# Where:
|
|
219
|
+
# * version - 1 byte with value 0x02
|
|
220
|
+
# * salt - 32 bytes used for generating the per-message secret
|
|
221
|
+
# * IV - 12 bytes random initialization vector
|
|
222
|
+
# * authentication tag - 16 bytes authentication tag generated by the GCM mode, covering version and salt
|
|
223
|
+
#
|
|
224
|
+
# Considerations about V2:
|
|
225
|
+
#
|
|
226
|
+
# 1) It uses non URL-safe Base64 encoding as it's faster than its
|
|
227
|
+
# URL-safe counterpart - as of Ruby 3.2, Base64.urlsafe_encode64 is
|
|
228
|
+
# roughly equivalent to
|
|
229
|
+
#
|
|
230
|
+
# Base64.strict_encode64(data).tr("-_", "+/")
|
|
231
|
+
#
|
|
232
|
+
# - and cookie values don't need to be URL-safe.
|
|
233
|
+
def initialize(secret, opts = {})
|
|
234
|
+
raise ArgumentError, 'secret must be a String' unless secret.is_a?(String)
|
|
235
|
+
|
|
236
|
+
unless secret.bytesize >= 32
|
|
237
|
+
raise ArgumentError, "invalid secret: it's #{secret.bytesize}-byte long, must be >=32"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
case opts[:pad_size]
|
|
241
|
+
when nil
|
|
242
|
+
# padding is disabled
|
|
243
|
+
when Integer
|
|
244
|
+
raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}" unless (2..4096).include? opts[:pad_size]
|
|
245
|
+
else
|
|
246
|
+
raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}; must be Integer or nil"
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
@options = {
|
|
250
|
+
serialize_json: false, pad_size: 32, purpose: nil
|
|
251
|
+
}.update(opts)
|
|
252
|
+
|
|
253
|
+
@cipher_secret = secret.dup.force_encoding(Encoding::BINARY).slice!(0, 32)
|
|
254
|
+
@cipher_secret.freeze
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def decrypt(base64_data)
|
|
258
|
+
data = Base64.strict_decode64(base64_data)
|
|
259
|
+
if data.bytesize <= 61 # version + salt + iv + auth_tag = 61 byte (and we also need some ciphertext :)
|
|
260
|
+
raise InvalidMessage, 'invalid message'
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
version = data[0]
|
|
264
|
+
raise InvalidMessage, 'invalid message' unless version == "\2"
|
|
265
|
+
|
|
266
|
+
ciphertext = data.slice!(61..-1)
|
|
267
|
+
auth_tag = data.slice!(45, 16)
|
|
268
|
+
cipher_iv = data.slice!(33, 12)
|
|
269
|
+
|
|
270
|
+
cipher = new_cipher
|
|
271
|
+
cipher.decrypt
|
|
272
|
+
salt = data.slice(1, 32)
|
|
273
|
+
set_cipher_key(cipher, message_secret_from_salt(salt))
|
|
274
|
+
cipher.iv = cipher_iv
|
|
275
|
+
cipher.auth_tag = auth_tag
|
|
276
|
+
cipher.auth_data = (purpose = @options[:purpose]) ? data + purpose : data
|
|
277
|
+
|
|
278
|
+
plaintext = cipher.update(ciphertext) << cipher.final
|
|
171
279
|
|
|
172
|
-
|
|
280
|
+
deserialized_message plaintext
|
|
281
|
+
rescue ArgumentError, OpenSSL::Cipher::CipherError
|
|
282
|
+
raise InvalidSignature, 'invalid message'
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def encrypt(message)
|
|
286
|
+
version = "\2"
|
|
287
|
+
|
|
288
|
+
serialized_payload = serialize_payload(message)
|
|
289
|
+
|
|
290
|
+
cipher = new_cipher
|
|
291
|
+
cipher.encrypt
|
|
292
|
+
salt, message_secret = new_salt_and_message_secret
|
|
293
|
+
set_cipher_key(cipher, message_secret)
|
|
294
|
+
cipher.iv_len = 12
|
|
295
|
+
cipher_iv = cipher.random_iv
|
|
296
|
+
|
|
297
|
+
data = String.new
|
|
298
|
+
data << version
|
|
299
|
+
data << salt
|
|
300
|
+
|
|
301
|
+
cipher.auth_data = (purpose = @options[:purpose]) ? data + purpose : data
|
|
302
|
+
encrypted_data = cipher.update(serialized_payload) << cipher.final
|
|
303
|
+
|
|
304
|
+
data << cipher_iv
|
|
305
|
+
data << auth_tag_from(cipher)
|
|
306
|
+
data << encrypted_data
|
|
307
|
+
|
|
308
|
+
Base64.strict_encode64(data)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
private
|
|
173
312
|
|
|
174
|
-
|
|
175
|
-
|
|
313
|
+
def new_cipher
|
|
314
|
+
OpenSSL::Cipher.new('aes-256-gcm')
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def new_salt_and_message_secret
|
|
318
|
+
salt = SecureRandom.random_bytes(32)
|
|
319
|
+
|
|
320
|
+
[salt, message_secret_from_salt(salt)]
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def message_secret_from_salt(salt)
|
|
324
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), @cipher_secret, salt)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def set_cipher_key(cipher, key)
|
|
328
|
+
cipher.key = key
|
|
329
|
+
end
|
|
176
330
|
|
|
177
|
-
|
|
331
|
+
if RUBY_ENGINE == 'jruby'
|
|
332
|
+
# JRuby's OpenSSL implementation doesn't currently support passing
|
|
333
|
+
# an argument to #auth_tag. Here we work around that.
|
|
334
|
+
def auth_tag_from(cipher)
|
|
335
|
+
tag = cipher.auth_tag
|
|
336
|
+
raise Error, 'the auth tag must be 16 bytes long' if tag.bytesize != 16
|
|
337
|
+
|
|
338
|
+
tag
|
|
339
|
+
end
|
|
340
|
+
else
|
|
341
|
+
def auth_tag_from(cipher)
|
|
342
|
+
cipher.auth_tag(16)
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def initialize(secret, opts = {})
|
|
348
|
+
opts = opts.dup
|
|
349
|
+
|
|
350
|
+
@mode = opts.delete(:mode)&.to_sym || :guess_version
|
|
351
|
+
case @mode
|
|
352
|
+
when :v1
|
|
353
|
+
@v1 = V1.new(secret, opts)
|
|
354
|
+
when :v2
|
|
355
|
+
@v2 = V2.new(secret, opts)
|
|
356
|
+
else
|
|
357
|
+
@v1 = V1.new(secret, opts)
|
|
358
|
+
@v2 = V2.new(secret, opts)
|
|
359
|
+
end
|
|
178
360
|
end
|
|
179
361
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
362
|
+
def decrypt(base64_data)
|
|
363
|
+
decryptor =
|
|
364
|
+
case @mode
|
|
365
|
+
when :v2
|
|
366
|
+
v2
|
|
367
|
+
when :v1
|
|
368
|
+
v1
|
|
369
|
+
else
|
|
370
|
+
guess_decryptor(base64_data)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
decryptor.decrypt(base64_data)
|
|
374
|
+
end
|
|
185
375
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
376
|
+
def encrypt(message)
|
|
377
|
+
encryptor =
|
|
378
|
+
case @mode
|
|
379
|
+
when :v1
|
|
380
|
+
v1
|
|
381
|
+
else
|
|
382
|
+
v2
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
encryptor.encrypt(message)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
private
|
|
389
|
+
|
|
390
|
+
attr_reader :v1, :v2
|
|
391
|
+
|
|
392
|
+
def guess_decryptor(base64_data)
|
|
393
|
+
raise InvalidMessage, 'invalid message' if base64_data.nil? || base64_data.bytesize < 4
|
|
394
|
+
|
|
395
|
+
first_encoded_4_bytes = base64_data.slice(0, 4)
|
|
396
|
+
# Transform the 4 bytes into non-URL-safe base64-encoded data. Nothing
|
|
397
|
+
# happens if the data is already non-URL-safe base64.
|
|
398
|
+
first_encoded_4_bytes.tr!('-_', '+/')
|
|
399
|
+
first_decoded_3_bytes = Base64.strict_decode64(first_encoded_4_bytes)
|
|
400
|
+
|
|
401
|
+
version = first_decoded_3_bytes[0]
|
|
402
|
+
case version
|
|
403
|
+
when "\2"
|
|
404
|
+
v2
|
|
405
|
+
when "\1"
|
|
406
|
+
v1
|
|
407
|
+
else
|
|
408
|
+
raise InvalidMessage, 'invalid message'
|
|
409
|
+
end
|
|
410
|
+
rescue ArgumentError
|
|
411
|
+
raise InvalidMessage, 'invalid message'
|
|
189
412
|
end
|
|
190
413
|
end
|
|
191
414
|
end
|
data/lib/rack/session/pool.rb
CHANGED
|
@@ -53,6 +53,7 @@ module Rack
|
|
|
53
53
|
|
|
54
54
|
def write_session(req, session_id, new_session, options)
|
|
55
55
|
@mutex.synchronize do
|
|
56
|
+
return false unless get_session_with_fallback(session_id)
|
|
56
57
|
@pool.store session_id.private_id, new_session
|
|
57
58
|
session_id
|
|
58
59
|
end
|
|
@@ -62,7 +63,12 @@ module Rack
|
|
|
62
63
|
@mutex.synchronize do
|
|
63
64
|
@pool.delete(session_id.public_id)
|
|
64
65
|
@pool.delete(session_id.private_id)
|
|
65
|
-
|
|
66
|
+
|
|
67
|
+
unless options[:drop]
|
|
68
|
+
sid = generate_sid(use_mutex: false)
|
|
69
|
+
@pool.store(sid.private_id, {})
|
|
70
|
+
sid
|
|
71
|
+
end
|
|
66
72
|
end
|
|
67
73
|
end
|
|
68
74
|
|
data/lib/rack/session/version.rb
CHANGED
data/lib/rack/session.rb
CHANGED
data/releases.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Releases
|
|
2
|
+
|
|
3
|
+
## v2.1.2
|
|
4
|
+
|
|
5
|
+
- [CVE-2026-39324](https://github.com/advisories/GHSA-33qg-7wpp-89cq) Don't fall back to unencrypted coder if encryptors are present.
|
|
6
|
+
|
|
7
|
+
## v2.1.1
|
|
8
|
+
|
|
9
|
+
- Prevent `Rack::Session::Pool` from recreating deleted sessions [CVE-2025-46336](https://github.com/rack/rack-session/security/advisories/GHSA-9j94-67jr-4cqj).
|
|
10
|
+
|
|
11
|
+
## v2.1.0
|
|
12
|
+
|
|
13
|
+
- Improved compatibility with Ruby 3.3+ and Rack 3+.
|
|
14
|
+
- Add support for cookie option `partitioned`.
|
|
15
|
+
- Introduce `assume_ssl` option to allow secure session cookies through insecure proxy.
|
|
16
|
+
|
|
17
|
+
## v2.0.0
|
|
18
|
+
|
|
19
|
+
- Initial migration of code from Rack 2, for Rack 3 release.
|
|
20
|
+
|
|
21
|
+
## v1.0.2
|
|
22
|
+
|
|
23
|
+
- Fix missing `rack/session.rb` file.
|
|
24
|
+
|
|
25
|
+
## v1.0.1
|
|
26
|
+
|
|
27
|
+
- Pin to `rack < 3`.
|
|
28
|
+
|
|
29
|
+
## v1.0.0
|
|
30
|
+
|
|
31
|
+
- Empty shim release for Rack 2.
|
metadata
CHANGED
|
@@ -1,18 +1,31 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rack-session
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Samuel Williams
|
|
8
8
|
- Jeremy Evans
|
|
9
9
|
- Jon Dufresne
|
|
10
10
|
- Philip Arndt
|
|
11
|
-
autorequire:
|
|
12
11
|
bindir: bin
|
|
13
12
|
cert_chain: []
|
|
14
|
-
date:
|
|
13
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
15
14
|
dependencies:
|
|
15
|
+
- !ruby/object:Gem::Dependency
|
|
16
|
+
name: base64
|
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
|
18
|
+
requirements:
|
|
19
|
+
- - ">="
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: 0.1.0
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: 0.1.0
|
|
16
29
|
- !ruby/object:Gem::Dependency
|
|
17
30
|
name: rack
|
|
18
31
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -97,8 +110,6 @@ dependencies:
|
|
|
97
110
|
- - ">="
|
|
98
111
|
- !ruby/object:Gem::Version
|
|
99
112
|
version: '0'
|
|
100
|
-
description:
|
|
101
|
-
email:
|
|
102
113
|
executables: []
|
|
103
114
|
extensions: []
|
|
104
115
|
extra_rdoc_files: []
|
|
@@ -112,12 +123,13 @@ files:
|
|
|
112
123
|
- lib/rack/session/version.rb
|
|
113
124
|
- license.md
|
|
114
125
|
- readme.md
|
|
126
|
+
- releases.md
|
|
115
127
|
- security.md
|
|
116
128
|
homepage: https://github.com/rack/rack-session
|
|
117
129
|
licenses:
|
|
118
130
|
- MIT
|
|
119
|
-
metadata:
|
|
120
|
-
|
|
131
|
+
metadata:
|
|
132
|
+
rubygems_mfa_required: 'true'
|
|
121
133
|
rdoc_options: []
|
|
122
134
|
require_paths:
|
|
123
135
|
- lib
|
|
@@ -125,15 +137,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
|
125
137
|
requirements:
|
|
126
138
|
- - ">="
|
|
127
139
|
- !ruby/object:Gem::Version
|
|
128
|
-
version: 2.
|
|
140
|
+
version: '2.5'
|
|
129
141
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
130
142
|
requirements:
|
|
131
143
|
- - ">="
|
|
132
144
|
- !ruby/object:Gem::Version
|
|
133
145
|
version: '0'
|
|
134
146
|
requirements: []
|
|
135
|
-
rubygems_version: 3.
|
|
136
|
-
signing_key:
|
|
147
|
+
rubygems_version: 3.6.9
|
|
137
148
|
specification_version: 4
|
|
138
149
|
summary: A session implementation for Rack.
|
|
139
150
|
test_files: []
|