Skip to content

Commit

Permalink
Support UTF-8 data when using the JSON serializer (#39)
Browse files Browse the repository at this point in the history
* Replace all instance of 'BINARY' with `Encoding::BINARY`
  • Loading branch information
jcmfernandes authored Aug 11, 2024
1 parent 219d8da commit 4c10f8b
Show file tree
Hide file tree
Showing 4 changed files with 26 additions and 14 deletions.
8 changes: 4 additions & 4 deletions lib/rack/session/encryptor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ module Serializable
def serialize_payload(message)
serialized_data = serializer.dump(message)

return "#{[0].pack('v')}#{serialized_data}" if @options[:pad_size].nil?
return "#{[0].pack('v')}#{serialized_data.force_encoding(Encoding::BINARY)}" if @options[:pad_size].nil?

padding_bytes = @options[:pad_size] - (2 + serialized_data.size) % @options[:pad_size]
padding_data = SecureRandom.random_bytes(padding_bytes)

"#{[padding_bytes].pack('v')}#{padding_data}#{serialized_data}"
"#{[padding_bytes].pack('v')}#{padding_data}#{serialized_data.force_encoding(Encoding::BINARY)}"
end

# Return the deserialized message. The first 2 bytes will be read as the
Expand Down Expand Up @@ -103,7 +103,7 @@ def initialize(secret, opts = {})
serialize_json: false, pad_size: 32, purpose: nil
}.update(opts)

@hmac_secret = secret.dup.force_encoding('BINARY')
@hmac_secret = secret.dup.force_encoding(Encoding::BINARY)
@cipher_secret = @hmac_secret.slice!(0, 32)

@hmac_secret.freeze
Expand Down Expand Up @@ -250,7 +250,7 @@ def initialize(secret, opts = {})
}.update(opts)
@options[:serialize_json] = true # Enforce JSON serialization

@cipher_secret = secret.dup.force_encoding('BINARY').slice!(0, 32)
@cipher_secret = secret.dup.force_encoding(Encoding::BINARY).slice!(0, 32)
@cipher_secret.freeze
end

Expand Down
14 changes: 7 additions & 7 deletions test/spec_session_cookie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -212,7 +212,7 @@ def decode(str); @calls << :decode; JSON.parse(str); end
response.body.must_equal '{"counter"=>1}'
identity.calls.must_equal [:encode]

response = response_for(app: [incrementor, { coder: identity }], :cookie=>response["Set-Cookie"].split(';', 2).first)
response_for(app: [incrementor, { coder: identity }], cookie: response["Set-Cookie"].split(';', 2).first)
identity.calls.must_equal [:encode, :decode, :encode]
end

Expand All @@ -223,20 +223,20 @@ def decode(str); @calls << :decode; JSON.parse(str); end
end

it "passes through same_site option to session cookie" do
response = response_for(app: [incrementor, same_site: :none])
response["Set-Cookie"].must_include "SameSite=None"
response = response_for(app: [incrementor, { same_site: :none }])
assert(response["Set-Cookie"].include?("SameSite=None") || response["Set-Cookie"].include?("samesite=none"))
end

it "allows using a lambda to specify same_site option, because some browsers require different settings" do
# Details of why this might need to be set dynamically:
# https://www.chromium.org/updates/same-site/incompatible-clients
# https://gist.github.com/bnorton/7dee72023787f367c48b3f5c2d71540f

response = response_for(app: [incrementor, same_site: lambda { |req, res| :none }])
response["Set-Cookie"].must_include "SameSite=None"
response = response_for(app: [incrementor, { same_site: lambda { |req, res| :none } }])
assert(response["Set-Cookie"].include?("SameSite=None") || response["Set-Cookie"].include?("samesite=none"))

response = response_for(app: [incrementor, same_site: lambda { |req, res| :lax }])
response["Set-Cookie"].must_include "SameSite=Lax"
response = response_for(app: [incrementor, { same_site: lambda { |req, res| :lax } }])
assert(response["Set-Cookie"].include?("SameSite=Lax") || response["Set-Cookie"].include?("samesite=lax"))
end

it "loads from a cookie" do
Expand Down
12 changes: 12 additions & 0 deletions test/spec_session_encryptor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,18 @@ def self.included(_base)
encryptor.decrypt(message).must_equal({ 'foo' => 'bar' })
end

# The V1 encryptor defaults to the Marshal serializer, while the V2
# encryptor always uses the JSON serializer. This means that we are
# indirectly covering both serializers.
it 'decrypts an encrypted message with UTF-8 data' do
encryptor = encryptor_class.new(@secret)

encrypted_message = encryptor.encrypt({ 'foo' => 'bar 😀' })
decrypted_message = encryptor.decrypt(encrypted_message)

decrypted_message.must_equal({ 'foo' => 'bar 😀' })
end

it 'decrypts raises InvalidSignature without purpose' do
encryptor = encryptor_class.new(@secret, purpose: 'testing')
other_encryptor = encryptor_class.new(@secret)
Expand Down
6 changes: 3 additions & 3 deletions test/spec_session_pool.rb
Original file line number Diff line number Diff line change
Expand Up @@ -209,19 +209,19 @@
pool.same_site.must_equal :none
req = Rack::MockRequest.new(pool)
res = req.get("/")
res["Set-Cookie"].must_include "SameSite=None"
assert(res["Set-Cookie"].include?("SameSite=None") || res["Set-Cookie"].include?("samesite=none"))
end

it "allows using a lambda to specify same_site option, because some browsers require different settings" do
pool = Rack::Session::Pool.new(incrementor, same_site: lambda { |req, res| :none })
req = Rack::MockRequest.new(pool)
res = req.get("/")
res["Set-Cookie"].must_include "SameSite=None"
assert(res["Set-Cookie"].include?("SameSite=None") || res["Set-Cookie"].include?("samesite=none"))

pool = Rack::Session::Pool.new(incrementor, same_site: lambda { |req, res| :lax })
req = Rack::MockRequest.new(pool)
res = req.get("/")
res["Set-Cookie"].must_include "SameSite=Lax"
assert(res["Set-Cookie"].include?("SameSite=Lax") || res["Set-Cookie"].include?("samesite=lax"))
end

# anyone know how to do this better?
Expand Down

0 comments on commit 4c10f8b

Please sign in to comment.