Table of Contents
I wondered how the Rails cookie structure is and checked it.
Environment
- Rails 4.2.5
- Ruby 2.0.0p481
Code to Decode Cookie Value
First, I checked the cookie value. Launch rails console, bundle exec rails c
, and execute the following code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
cookie = 'your cookie value' key = 'your secret key base' cookie = CGI::unescape(cookie) # Default values for Rails 4 apps key_iter_num = 1000 key_size = 64 ## Rails.application.config.action_dispatch.encrypted_cookie_salt salt = "encrypted cookie" ## Rails.application.config.action_dispatch.encrypted_signed_cookie_salt signed_salt = "signed encrypted cookie" key_generator = ActiveSupport::KeyGenerator.new( key, iterations: key_iter_num) secret = key_generator.generate_key(salt) sign_secret = key_generator.generate_key(signed_salt) encryptor = ActiveSupport::MessageEncryptor.new( secret, sign_secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer) encryptor.decrypt_and_verify(cookie) |
Set cookie value to the variable cookie
, and set secret_key_base
value to the variable key
.
cookie value
If you use Chrome browser (Version 47.0.2526.106), you can see the cookie value with developer tools. Open Developer Tools and choose resource view and select cookie that you want to see on left side tree.
Cookie is name and value pair. The name of the cookie generated by Rails 4 is like _xxx_session
as default. You can specify it as Rails.application.config.session_store
, which is written in config/initializers/session_store.rb
usually.
secret_key_base value
Normally, it is written in config/secrets.yml
.
salt and secret
In the above code, I set values defined as config to salt
and secret
. It is “encrypted cookie” and “signed encrypted cookie” as default.
You can see that values by checking cookies
variable in controller.
Example of Cookie Value
Rails のテスト用アプリケーションでクッキーの値を取得してみました。 テスト用アプリケーションでは devise gem を使用しており、 クッキーを取得したとき ユーザID 2 のユーザでログインしていました。
1 2 3 4 5 |
{ "session_id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", "warden.user.key": [[2], "xxxxxxxxxxxxxxxxxxxxxxxxxx"], "_csrf_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" } |
デコードの処理内容
クッキーデコード時の処理内容を確認します。
CGI.unescape
でクッキーに入っている値を URLデコード します。キージェネレータを作成します。 キージェネレータはシークレットキーの値とイテレーション回数をインスタンス変数にもつオブジェクトで、初期化処理は次のようになっています。
1234567# File activesupport/lib/active_support/key_generator.rb, line 10def initialize(secret, options = {})@secret = secret# The default iterations are higher than required for our key derivation uses# on the off chance someone uses this for password storage@iterations = options[:iterations] || 2**16endデコード時に
iterations
に 1000 を代入しているのは、Rails::Application
class で次のように1000 が使用されているからです。123456789101112131415# Returns the application's KeyGeneratordef key_generator# number of iterations selected based on consultation with the google security# team. Details at https://github.com/rails/rails/pull/6952#issuecomment-7661220@caching_key_generator ||=if secrets.secret_key_baseunless secrets.secret_key_base.kind_of?(String)raise ArgumentError, "`secret_key_base` for #{Rails.env} environment must be a type of String, change this value in `config/secrets.yml`"endkey_generator = ActiveSupport::KeyGenerator.new(secrets.secret_key_base, iterations: 1000)ActiveSupport::CachingKeyGenerator.new(key_generator)elseActiveSupport::LegacyKeyGenerator.new(secrets.secret_token)endendGenerate key from
salt
.generate_key
method is defined as below.1234# File activesupport/lib/active_support/key_generator.rb, line 20def generate_key(salt, key_size=64)OpenSSL::PKCS5.pbkdf2_hmac_sha1(@secret, salt, @iterations, key_size)endOpenSSL::PKCS5.pbkdf2_hmac_sha1
generates key for encryption by hash method sha1. In this case, iterates 1000 times.Generate
sign_secret
like above.Crete
MessageEncryptor
object. Initializer ofMessageEncryptor
is defined as below.12345678910# File activesupport/lib/active_support/message_encryptor.rb, line 45def initialize(secret, *signature_key_or_options)options = signature_key_or_options.extract_options!sign_secret = signature_key_or_options.first@secret = secret@sign_secret = sign_secret@cipher = options[:cipher] || 'aes-256-cbc'@verifier = MessageVerifier.new(@sign_secret || @secret, digest: options[:digest] || 'SHA1', serializer: NullSerializer)@serializer = options[:serializer] || Marshalendoptions[:chiper]
は設定されないまま実行されますから、 AES-256-CBC が暗号化方式として設定されます。 AES は共通鍵暗号で、 ここで作成したMessageEncryptor
オブジェクトはencrypt
とdecrypt
が可能です。verify
anddecrypt
.verify
を行うのは padding attack を防ぐためだそうです。 It doesn’t decrypt simply, so incorrect secret value trigger exception and you can’t get decrypted value.
Generate Cookie Value
前節で書いたのと同じ要領で MessageEncryptor
のオブジェクトを作成して encrypt_and_sign
を実行すれば cookie の値を作成できます。 encrypt_and_sign
メソッドは実行する度に結果が変わります。
1 2 3 4 5 6 7 |
value = '{"session_id":"12345","key_1":"value_1","key_2":"value_2"}' cookie_value = encryptor.encrypt_and_sign(value) encryptor.decrypt_and_verify( encryptor.encrypt_and_sign(value)) == value # => true |