Skip to content

mwpastore/rack-session-smart_cookie

Repository files navigation

Rack::Session::SmartCookie

Gem Version Build Status

The version of Rack::Session::Cookie that ships with Rack 2 has the following limitations:

  • HMAC-SHA1 by default
  • Slow and/or bloated JSON, ZipJSON, or Marshal encoding out of the box
  • JSON encodings do not preserve Symbols
  • Digest is double-encoded and bloated (hexdigest of a base64)
  • Base64-encoded strings contain unecessary padding and characters that need to be escaped (e.g. / becomes %2F), wasting precious cookie bytes
  • It has some bugs in the size check that may lead to dropped or truncated cookies, token leakage, and/or cross-site request forgery

Of course, none of these are true show-stoppers, and the worst can be worked around by passing e.g. :hmac and :coder to the initializer. But we are nice people and we deserve nice things. This gem provides a minor derivative of Rack::Session::Cookie with the following improvements:

  • HMAC-SHA256 by default
  • Compact binary serialization format (currently MessagePack but will likely change to CBOR in the future) out of the box
  • Symbols are preserved with the default :coder
  • Digest is single-encoded and compact (base64 of a digest)
  • Base64-encoded strings are not padded and conform to URL-encoded form data (e.g. / becomes _)
  • It does not perform a size check (use Rack::Protection::MaximumCookie if you care about cookie limits)

The resultant cookies values with a small-to-medium sized session can be up to 30% smaller in an apples-to-apples comparison (see below for examples).

Strategy

The main distinguishing feature of this cf. the stock implementation is that the encoding (and decoding) step has been separated into two stages: 1. binary serialization and 2. stringification, instead of being combined into a single "coder" class. This allows the various cookie components to be either both serialized and stringified (in the case of the session payload) or merely stringified (in the case of the digest).

The other key realization is that the method Rack uses to escape cookie data (URI.encode_www_form_component) will only ever allow non-padded, URL-safe Base64 plus period (.) and asterisk (*), so there's no sense in using any stringification scheme other than non-padded, URL-safe Base64! It doesn't need to be configurable. The serializer remains configurable as the :coder.

The remaining differences are mostly just better defaults: MessagePack and HMAC-SHA256.

Installation

Add this line to your application's Gemfile:

gem 'rack-session-smart_cookie'

And then execute:

$ bundle

Or install it yourself as:

$ gem install rack-session-smart_cookie

Usage

use Rack::Session::SmartCookie

Rack::Session::SmartCookie is a sub-class of Rack::Session::Cookie and accepts all the same options. If you choose to override the default :coder, it should not perform Base64 encoding or decoding.

The default :coder registers Symbol as a custom type on the factory. You can easily register additional custom types like so:

my_coder = Rack::Session::SmartCookie::MessagePack.new do |factory|
  factory.register_type(0x00, MyCustomType) # 0x60..0xFF are reserved
end

use Rack::Session::SmartCookie, :coder=>my_coder

Please see the MessagePack documentation for more details.

Rack::Session::SmartCookie also accepts :digest and :digest_bytes options that allow you to choose the message digest algorithm and limit the size of the generated digest. This lets you e.g. truncate 64-byte HMAC-SHA512 digests down to 32 bytes (i.e. HMAC-SHA512/256):

use Rack::Session::SmartCookie, :digest=>'SHA512', :digest_bytes=>32

The :hmac option overrides the :digest option.

Comparisons

For general size and performance benchmarks of the encoding schemes, see here. Unfortunately, the post is slightly out-of-date and doesn't include ZipJSON (Zlib+JSON) results. However, I was able to run the benchmarks locally and add ZipJSON. Although it comes in second-most compact at 289 bytes (cf. protocol buffers and MessagePack at 204 and 373 bytes, respectively), it was 97% slower to encode and 91% slower to decode cf. MessagePack.

I put this mock session payload through the following configurations with HMAC-SHA256 and 128 sidbits and here are the results:

{
  :user_id=>514,
  :roles=>[:user, :moderator, :mailbox],
  :data=>'{"foo":"bar","qux":21}',
  :issued_at=>Time.now.to_f,
  :valid_for=>30*24*3_600
}

Rack::Session::Cookie w/ Base64::Marshal

BAh7C0kiD3Nlc3Npb25faWQGOgZFVEkiRTg3MzJkMTEzNDQyZjQyM2FlZGUy%0AMTdmNDY0OWEyOTk5
MjkyYzg2M2JkNTFlY2VjYjY2ZDAzMTg0MTYzZWE3YTcG%0AOwBGSSIMdXNlcl9pZAY7AEZpAgICSSIK
cm9sZXMGOwBGWwg6CXVzZXI6Dm1v%0AZGVyYXRvcjoMbWFpbGJveEkiCWRhdGEGOwBGSSIbeyJmb28i
OiJiYXIiLCJx%0AdXgiOjIxfQY7AFRJIg5pc3N1ZWRfYXQGOwBGZhYxNTA5MjAzMDIzLjI3MzE2%0AN
UkiDnZhbGlkX2ZvcgY7AEZpAwCNJw%3D%3D%0A--15aebb42ba0ff0a28436556c64eb2ef6d4dc7c6
a39e164eac0889052cec4f83f

Size: 420 bytes (100%)

Note the percent-encoded characters and hex-encoded digest here and in the other Rack::Session::Cookie results.

Rack::Session::Cookie w/ Base64::JSON

eyJzZXNzaW9uX2lkIjoiMTA4YzM1ZGIxMTFkNWZlMjk5NzUwMTc1Mzc2MzVm%0AMDJlZTIxMjM4ZmIx
OTg2NDQ0ZTc4MTliY2RjZGQyYjc2YSIsInVzZXJfaWQi%0AOjUxNCwicm9sZXMiOlsidXNlciIsIm1v
ZGVyYXRvciIsIm1haWxib3giXSwi%0AZGF0YSI6IntcImZvb1wiOlwiYmFyXCIsXCJxdXhcIjoyMX0i
LCJpc3N1ZWRf%0AYXQiOjE1MDkyMDI5NzEuODk3MzUyLCJ2YWxpZF9mb3IiOjI1OTIwMDB9%0A--7a6
000bdece71118e768ccffedc645ace865b829536e335c304c00bb9050c625

Size: 377 bytes (90%)

Rack::Session::Cookie w/ Base64::ZipJSON

eJwdjeGKwyAQhN9lf8uxGrcaX%2BU8wqa7gpCeXEyOQum7V%2FJvZj5m5gVde6%2Ft%0Ad6kCCcok1h
bvwnqfJLpJ2VkpIXrxSmQZ76uzBb0qzYokSPY2E8aAXDQSg4Gz%0A636NkfUG9rZph%2FR9xYM%2Bmu
jOR7s0121tT%2FgxIHzwuH9lKK1lSBlW3jOYDH%2Fn%0Ac3hn36NQez9VFj4gWcLZoZsxfN1ioCkY%2
BOetylLGdHI0IOL7A%2BnjQaI%3D%0A--fc193337b2900b6ce893143b5a52d36b55fafebc21cbde
83712dce56bbf836f4

Size: 334 bytes (80%)

Rack::Session::SmartCookie w/ MessagePack

hqpzZXNzaW9uX2lk2UBiMGEzYzhlZTE4NzY3YjcwOTNmNThhN2E4MTI4NTNmNTlmNDYwOTgwMDA5NGY
1Y2E4MTg5MjFjMjA4ZWQ1ZDY3p3VzZXJfaWTNAgKlcm9sZXOT1gB1c2VyxwkAbW9kZXJhdG9yxwcAbW
FpbGJveKRkYXRhtnsiZm9vIjoiYmFyIiwicXV4IjoyMX2paXNzdWVkX2F0y0HWfSbUfbp0qXZhbGlkX
2Zvcs4AJ40A.CRGTAgpN19Iz1plyX14kHmQYWTe0OtFbetqKZmCvSfg

Size: 292 bytes (70%)

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests. You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and tags, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/mwpastore/rack-session-smart_cookie.

License

The gem is available as open source under the terms of the MIT License.