rack-session 2.1.1 → 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/cookie.rb +4 -2
- data/lib/rack/session/encryptor.rb +345 -122
- data/lib/rack/session/version.rb +1 -1
- data/lib/rack/session.rb +0 -1
- data/releases.md +4 -0
- metadata +3 -8
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
|
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/version.rb
CHANGED
data/lib/rack/session.rb
CHANGED
data/releases.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# Releases
|
|
2
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
|
+
|
|
3
7
|
## v2.1.1
|
|
4
8
|
|
|
5
9
|
- Prevent `Rack::Session::Pool` from recreating deleted sessions [CVE-2025-46336](https://github.com/rack/rack-session/security/advisories/GHSA-9j94-67jr-4cqj).
|
metadata
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rack-session
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.1.
|
|
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:
|
|
16
15
|
- !ruby/object:Gem::Dependency
|
|
17
16
|
name: base64
|
|
@@ -111,8 +110,6 @@ dependencies:
|
|
|
111
110
|
- - ">="
|
|
112
111
|
- !ruby/object:Gem::Version
|
|
113
112
|
version: '0'
|
|
114
|
-
description:
|
|
115
|
-
email:
|
|
116
113
|
executables: []
|
|
117
114
|
extensions: []
|
|
118
115
|
extra_rdoc_files: []
|
|
@@ -133,7 +130,6 @@ licenses:
|
|
|
133
130
|
- MIT
|
|
134
131
|
metadata:
|
|
135
132
|
rubygems_mfa_required: 'true'
|
|
136
|
-
post_install_message:
|
|
137
133
|
rdoc_options: []
|
|
138
134
|
require_paths:
|
|
139
135
|
- lib
|
|
@@ -148,8 +144,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
148
144
|
- !ruby/object:Gem::Version
|
|
149
145
|
version: '0'
|
|
150
146
|
requirements: []
|
|
151
|
-
rubygems_version: 3.
|
|
152
|
-
signing_key:
|
|
147
|
+
rubygems_version: 3.6.9
|
|
153
148
|
specification_version: 4
|
|
154
149
|
summary: A session implementation for Rack.
|
|
155
150
|
test_files: []
|