├── .gitignore
├── .travis.yml
├── CODE_OF_CONDUCT.md
├── Gemfile
├── LICENSE.txt
├── README.md
├── Rakefile
├── bin
├── console
└── setup
├── cookie_decryptor.gemspec
├── lib
├── cookie_decryptor.rb
└── cookie_decryptor
│ ├── decryptor.rb
│ └── version.rb
└── test
├── cookie_decryptor
└── decryptor_test.rb
├── cookie_decryptor_test.rb
└── test_helper.rb
/.gitignore:
--------------------------------------------------------------------------------
1 | /.bundle/
2 | /.yardoc
3 | /Gemfile.lock
4 | /_yardoc/
5 | /coverage/
6 | /doc/
7 | /pkg/
8 | /spec/reports/
9 | /tmp/
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | sudo: false
2 | language: ruby
3 | rvm:
4 | - 2.3.3
5 | before_install: gem install bundler -v 1.14.6
6 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at justin@justinweiss.com. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [http://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/Gemfile:
--------------------------------------------------------------------------------
1 | source 'https://rubygems.org'
2 |
3 | # Specify your gem's dependencies in cookie_decryptor.gemspec
4 | gemspec
5 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Justin Weiss
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CookieDecryptor
2 |
3 | When you're trying to debug cookie session problems, are you stuck with cookies that look like this?
4 |
5 | ```
6 | Set-Cookie: _session_my_app=OXJ2SkhNaFZBWDd1eDU3djhSekZRdmN6WjNKUjN4dlBiMWt3bW9sVjM0OERIZ3lPUmV1UFB2MmlySzI0OXJtbTRDdmI3TGd0S3AvMVNjdTlueEo1Y05zMnE3NTdsMVVmWWFVSXA5NVFOT0U9LS1tM21SL2tIMGhxYjFEWjZjb2Y3ZWlnPT0%3D--533f89e5525959c122e31ff7eae5b886b2ed7fe9; path=/; HttpOnly
7 | ```
8 |
9 | What does that even mean? How do you figure out that that cookie decodes to
10 |
11 | ```json
12 | {"session_id":"35481e34ef3c0d0ac83e4dccf8520120","name":"Justin"}
13 | ```
14 |
15 | without faking the session yourself?
16 |
17 | With CookieDecryptor, you can paste that header in and get the result right back out.
18 |
19 | ```ruby
20 | irb(main):001:0> CookieDecryptor.decrypt("Set-Cookie: _session_my_app=OXJ2SkhNaFZBWDd1eDU3djhSekZRdmN6WjNKUjN4dlBiMWt3bW9sVjM0OERIZ3lPUmV1UFB2MmlySzI0OXJtbTRDdmI3TGd0S3AvMVNjdTlueEo1Y05zMnE3NTdsMVVmWWFVSXA5NVFOT0U9LS1tM21SL2tIMGhxYjFEWjZjb2Y3ZWlnPT0%3D--533f89e5525959c122e31ff7eae5b886b2ed7fe9; path=/; HttpOnly")
21 | => "{\"session_id\":\"35481e34ef3c0d0ac83e4dccf8520120\",\"name\":\"Justin\"}"
22 | ```
23 |
24 | ## Installation
25 |
26 | Add this line to your application's Gemfile:
27 |
28 | ```ruby
29 | gem 'cookie_decryptor'
30 | ```
31 |
32 | And then execute:
33 |
34 | $ bundle
35 |
36 | Or install it yourself as:
37 |
38 | $ gem install cookie_decryptor
39 |
40 | ## Usage
41 |
42 | You can decrypt cookie data with `CookieDecryptor.decrypt`:
43 |
44 | ```ruby
45 | CookieDecryptor.decrypt("OXJ2SkhNaFZBWDd1eDU3djhSekZRdmN6WjNKUjN4dlBiMWt3bW9sVjM0OERIZ3lPUmV1UFB2MmlySzI0OXJtbTRDdmI3TGd0S3AvMVNjdTlueEo1Y05zMnE3NTdsMVVmWWFVSXA5NVFOT0U9LS1tM21SL2tIMGhxYjFEWjZjb2Y3ZWlnPT0%3D--533f89e5525959c122e31ff7eae5b886b2ed7fe9")
46 | ```
47 |
48 | Or you can be extra lazy, and paste an entire HTTP cookie header:
49 |
50 | ```ruby
51 | CookieDecryptor.decrypt("Set-Cookie: _session_my_app=OXJ2SkhNaFZBWDd1eDU3djhSekZRdmN6WjNKUjN4dlBiMWt3bW9sVjM0OERIZ3lPUmV1UFB2MmlySzI0OXJtbTRDdmI3TGd0S3AvMVNjdTlueEo1Y05zMnE3NTdsMVVmWWFVSXA5NVFOT0U9LS1tM21SL2tIMGhxYjFEWjZjb2Y3ZWlnPT0%3D--533f89e5525959c122e31ff7eae5b886b2ed7fe9; path=/; HttpOnly")
52 | ```
53 |
54 | If you're inside your Rails app, CookieDecryptor will default to using your Rails app's `secret_key_base`. If you're not in a Rails app, or if you want to use a different `secret_key_base`, you can specify it:
55 |
56 | ```ruby
57 | CookieDecryptor.decrypt("OXJ2SkhNaFZBWDd1eDU3djhSekZRdmN6WjNKUjN4dlBiMWt3bW9sVjM0OERIZ3lPUmV1UFB2MmlySzI0OXJtbTRDdmI3TGd0S3AvMVNjdTlueEo1Y05zMnE3NTdsMVVmWWFVSXA5NVFOT0U9LS1tM21SL2tIMGhxYjFEWjZjb2Y3ZWlnPT0%3D--533f89e5525959c122e31ff7eae5b886b2ed7fe9", secret_key_base: "1c81d82d9be8b21667f16b254da480cbbd0fd9fdf856ea88a285479e1bec9860cb3fdda0b214a25e34b16d83929ae87ec846648c7ef5cef35121f029c341ad7b")
58 | ```
59 |
60 | ## Development
61 |
62 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
63 |
64 | 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](https://rubygems.org).
65 |
66 | ## Contributing
67 |
68 | Bug reports and pull requests are welcome on GitHub at https://github.com/justinweiss/cookie_decryptor. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
69 |
70 |
71 | ## License
72 |
73 | The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
74 |
75 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | require "bundler/gem_tasks"
2 | require "rake/testtask"
3 |
4 | Rake::TestTask.new(:test) do |t|
5 | t.libs << "test"
6 | t.libs << "lib"
7 | t.test_files = FileList['test/**/*_test.rb']
8 | end
9 |
10 | task :default => :test
11 |
--------------------------------------------------------------------------------
/bin/console:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env ruby
2 |
3 | require "bundler/setup"
4 | require "cookie_decryptor"
5 |
6 | # You can add fixtures and/or initialization code here to make experimenting
7 | # with your gem easier. You can also use a different console, if you like.
8 |
9 | # (If you use this, don't forget to add pry to your Gemfile!)
10 | # require "pry"
11 | # Pry.start
12 |
13 | require "irb"
14 | IRB.start(__FILE__)
15 |
--------------------------------------------------------------------------------
/bin/setup:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -euo pipefail
3 | IFS=$'\n\t'
4 | set -vx
5 |
6 | bundle install
7 |
8 | # Do any other automated setup that you need to do here
9 |
--------------------------------------------------------------------------------
/cookie_decryptor.gemspec:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 | lib = File.expand_path('../lib', __FILE__)
3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4 | require 'cookie_decryptor/version'
5 |
6 | Gem::Specification.new do |spec|
7 | spec.name = "cookie_decryptor"
8 | spec.version = CookieDecryptor::VERSION
9 | spec.authors = ["Justin Weiss"]
10 | spec.email = ["justin@justinweiss.com"]
11 |
12 | spec.summary = "Decrypt your secure Rails cookies."
13 | spec.description = "Ever wanted to peek inside your signed, encrypted Rails cookies? With this gem, it's easy."
14 | spec.homepage = "https://github.com/justinweiss/cookie_decryptor"
15 | spec.license = "MIT"
16 |
17 | spec.files = `git ls-files -z`.split("\x0").reject do |f|
18 | f.match(%r{^(test|spec|features)/})
19 | end
20 | spec.bindir = "exe"
21 | spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22 | spec.require_paths = ["lib"]
23 |
24 | spec.add_dependency "activesupport", ">= 4.0.0"
25 |
26 | spec.add_development_dependency "bundler", "~> 1.14"
27 | spec.add_development_dependency "rake", "~> 10.0"
28 | spec.add_development_dependency "minitest", "~> 5.0"
29 | end
30 |
--------------------------------------------------------------------------------
/lib/cookie_decryptor.rb:
--------------------------------------------------------------------------------
1 | require "cookie_decryptor/version"
2 |
3 | module CookieDecryptor
4 | autoload :Decryptor, "cookie_decryptor/decryptor"
5 |
6 | # Decrypt cookie. If you're inside a Rails app, and
7 | # secret_key_base isn't set, it will decrypt the cookie
8 | # using your Rails settings. Otherwise, CookieDecryptor will use
9 | # secret_key_base to decrypt the cookie.
10 | def self.decrypt(cookie, secret_key_base: nil)
11 | Decryptor.new(cookie, secret_key_base: secret_key_base).decrypt
12 | end
13 | end
14 |
--------------------------------------------------------------------------------
/lib/cookie_decryptor/decryptor.rb:
--------------------------------------------------------------------------------
1 | require 'active_support'
2 | require 'cgi'
3 |
4 | module CookieDecryptor
5 | # Decrypts a Rails cookie, using ActiveSupport::KeyGenerator and
6 | # ActiveSupport::MessageVerifier. If this code is running inside a
7 | # Rails app, it will use the key generator and secrets the Rails app
8 | # is using. Otherwise, you must pass in your app's
9 | # secret_key_base, and we will use hardcoded key strings
10 | # from Rails.
11 | class Decryptor
12 |
13 | def initialize(cookie, secret_key_base: nil)
14 | @cookie = CGI.unescape(extract_cookie(cookie))
15 | @key_generator = key_generator(secret_key_base)
16 | end
17 |
18 | # Returns the decrypted data inside cookie.
19 | def decrypt
20 | encryptor.decrypt_and_verify(@cookie)
21 | end
22 |
23 | private
24 |
25 | # Extract cookie data out of a complete cookie header: everything
26 | # after the first = and before the first ;
27 | def extract_cookie(cookie)
28 | cookie.sub(/[^=]*=/, "").split(";").first
29 | end
30 |
31 | def key_generator(secret_key_base)
32 | if secret_key_base
33 | ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)
34 | elsif defined?(Rails.application)
35 | Rails.application.key_generator
36 | else
37 | raise ArgumentError, "You must specify a secret_key_base in order to decrypt sessions."
38 | end
39 | end
40 |
41 | def encryptor
42 | secret = @key_generator.generate_key("encrypted cookie")[0, ActiveSupport::MessageEncryptor.key_len]
43 | sign_secret = @key_generator.generate_key("signed encrypted cookie")
44 | ActiveSupport::MessageEncryptor.new(
45 | secret,
46 | sign_secret,
47 | serializer: ActiveSupport::MessageEncryptor::NullSerializer
48 | )
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/cookie_decryptor/version.rb:
--------------------------------------------------------------------------------
1 | module CookieDecryptor
2 | VERSION = "0.2.0".freeze
3 | end
4 |
--------------------------------------------------------------------------------
/test/cookie_decryptor/decryptor_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 | require 'minitest/mock'
3 | require 'ostruct'
4 |
5 | class CookieDecryptor::DecryptorTest < Minitest::Test
6 | def test_can_decrypt_a_cookie
7 | cookie = default_cookie
8 |
9 | assert_equal(
10 | '{"session_id":"35481e34ef3c0d0ac83e4dccf8520120","name":"Justin"}',
11 | CookieDecryptor::Decryptor.new(cookie, secret_key_base: secret_key_base).decrypt
12 | )
13 | end
14 |
15 | def test_can_decrypt_a_full_cookie_header
16 | cookie = "Set-Cookie: _session_my_app=#{default_cookie}; path=/; HttpOnly"
17 |
18 | assert_equal(
19 | '{"session_id":"35481e34ef3c0d0ac83e4dccf8520120","name":"Justin"}',
20 | CookieDecryptor::Decryptor.new(cookie, secret_key_base: secret_key_base).decrypt
21 | )
22 | end
23 |
24 | def test_uses_rails_key_generator_if_rails_loaded
25 | Object.const_set(:Rails, mock_rails)
26 |
27 | cookie = default_cookie
28 | key_generator = ActiveSupport::KeyGenerator.new(secret_key_base, iterations: 1000)
29 | Rails.application.expect(:key_generator, key_generator)
30 |
31 | assert_equal(
32 | '{"session_id":"35481e34ef3c0d0ac83e4dccf8520120","name":"Justin"}',
33 | CookieDecryptor::Decryptor.new(cookie).decrypt
34 | )
35 |
36 | assert_mock Rails.application
37 | ensure
38 | Object.send(:remove_const, :Rails)
39 | end
40 |
41 | def test_raises_error_if_secret_key_base_not_set
42 | cookie = default_cookie
43 |
44 | assert_raises ArgumentError do
45 | CookieDecryptor::Decryptor.new(cookie).decrypt
46 | end
47 | end
48 |
49 | private
50 |
51 | def mock_rails
52 | Module.new do
53 | def self.application
54 | @application ||= Minitest::Mock.new
55 | end
56 | end
57 | end
58 |
59 | def default_cookie
60 | "OXJ2SkhNaFZBWDd1eDU3djhSekZRdmN6WjNKUjN4dlBiMWt3bW9sVjM0OERIZ3lPUmV1UFB2MmlySzI0OXJtbTRDdmI3TGd0S3AvMVNjdTlueEo1Y05zMnE3NTdsMVVmWWFVSXA5NVFOT0U9LS1tM21SL2tIMGhxYjFEWjZjb2Y3ZWlnPT0%3D--533f89e5525959c122e31ff7eae5b886b2ed7fe9"
61 | end
62 |
63 | def secret_key_base
64 | "1c81d82d9be8b21667f16b254da480cbbd0fd9fdf856ea88a285479e1bec9860cb3fdda0b214a25e34b16d83929ae87ec846648c7ef5cef35121f029c341ad7b"
65 | end
66 | end
67 |
--------------------------------------------------------------------------------
/test/cookie_decryptor_test.rb:
--------------------------------------------------------------------------------
1 | require 'test_helper'
2 |
3 | class CookieDecryptorTest < Minitest::Test
4 | def test_that_it_has_a_version_number
5 | refute_nil ::CookieDecryptor::VERSION
6 | end
7 |
8 | def test_can_decrypt_a_cookie
9 | decrypted_data = CookieDecryptor.decrypt("OXJ2SkhNaFZBWDd1eDU3djhSekZRdmN6WjNKUjN4dlBiMWt3bW9sVjM0OERIZ3lPUmV1UFB2MmlySzI0OXJtbTRDdmI3TGd0S3AvMVNjdTlueEo1Y05zMnE3NTdsMVVmWWFVSXA5NVFOT0U9LS1tM21SL2tIMGhxYjFEWjZjb2Y3ZWlnPT0%3D--533f89e5525959c122e31ff7eae5b886b2ed7fe9", secret_key_base: "1c81d82d9be8b21667f16b254da480cbbd0fd9fdf856ea88a285479e1bec9860cb3fdda0b214a25e34b16d83929ae87ec846648c7ef5cef35121f029c341ad7b")
10 |
11 | assert_equal(
12 | '{"session_id":"35481e34ef3c0d0ac83e4dccf8520120","name":"Justin"}',
13 | decrypted_data
14 | )
15 | end
16 | end
17 |
--------------------------------------------------------------------------------
/test/test_helper.rb:
--------------------------------------------------------------------------------
1 | $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2 | require 'cookie_decryptor'
3 |
4 | require 'minitest/autorun'
5 |
--------------------------------------------------------------------------------