class Rack::Session::Encryptor::V2
Public Class Methods
Source
# File lib/rack/session/encryptor.rb, line 233 def initialize(secret, opts = {}) raise ArgumentError, 'secret must be a String' unless secret.is_a?(String) unless secret.bytesize >= 32 raise ArgumentError, "invalid secret: it's #{secret.bytesize}-byte long, must be >=32" end case opts[:pad_size] when nil # padding is disabled when Integer raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}" unless (2..4096).include? opts[:pad_size] else raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}; must be Integer or nil" end @options = { serialize_json: false, pad_size: 32, purpose: nil }.update(opts) @cipher_secret = secret.dup.force_encoding(Encoding::BINARY).slice!(0, 32) @cipher_secret.freeze end
The secret String must be at least 32 bytes in size.
Options may include:
-
:pad_size
Pad encrypted message data, to a multiple of this many bytes (default: 32). This can be between 2-4096 bytes, or +nil+ to disable padding.
-
:purpose
Limit messages to a specific purpose. This can be viewed as a security enhancement to prevent message reuse from different contexts if keys are reused.
Cryptography and Output Format:
strict_encode64(version + salt + IV + authentication tag + ciphertext) Where: * version - 1 byte with value 0x02 * salt - 32 bytes used for generating the per-message secret * IV - 12 bytes random initialization vector * authentication tag - 16 bytes authentication tag generated by the GCM mode, covering version and salt
Considerations about V2:
1) It uses non URL-safe Base64 encoding as it’s faster than its
URL-safe counterpart - as of Ruby 3.2, Base64.urlsafe_encode64 is
roughly equivalent to
Base64.strict_encode64(data).tr("-_", "+/")
- and cookie values don't need to be URL-safe.
Public Instance Methods
Source
# File lib/rack/session/encryptor.rb, line 257 def decrypt(base64_data) data = Base64.strict_decode64(base64_data) if data.bytesize <= 61 # version + salt + iv + auth_tag = 61 byte (and we also need some ciphertext :) raise InvalidMessage, 'invalid message' end version = data[0] raise InvalidMessage, 'invalid message' unless version == "\2" ciphertext = data.slice!(61..-1) auth_tag = data.slice!(45, 16) cipher_iv = data.slice!(33, 12) cipher = new_cipher cipher.decrypt salt = data.slice(1, 32) set_cipher_key(cipher, message_secret_from_salt(salt)) cipher.iv = cipher_iv cipher.auth_tag = auth_tag cipher.auth_data = (purpose = @options[:purpose]) ? data + purpose : data plaintext = cipher.update(ciphertext) << cipher.final deserialized_message plaintext rescue ArgumentError, OpenSSL::Cipher::CipherError raise InvalidSignature, 'invalid message' end
Source
# File lib/rack/session/encryptor.rb, line 285 def encrypt(message) version = "\2" serialized_payload = serialize_payload(message) cipher = new_cipher cipher.encrypt salt, message_secret = new_salt_and_message_secret set_cipher_key(cipher, message_secret) cipher.iv_len = 12 cipher_iv = cipher.random_iv data = String.new data << version data << salt cipher.auth_data = (purpose = @options[:purpose]) ? data + purpose : data encrypted_data = cipher.update(serialized_payload) << cipher.final data << cipher_iv data << auth_tag_from(cipher) data << encrypted_data Base64.strict_encode64(data) end
Private Instance Methods
Source
# File lib/rack/session/encryptor.rb, line 334 def auth_tag_from(cipher) tag = cipher.auth_tag raise Error, 'the auth tag must be 16 bytes long' if tag.bytesize != 16 tag end
JRuby’s OpenSSL implementation doesn’t currently support passing an argument to auth_tag. Here we work around that.
Source
# File lib/rack/session/encryptor.rb, line 323 def message_secret_from_salt(salt) OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), @cipher_secret, salt) end
Source
# File lib/rack/session/encryptor.rb, line 313 def new_cipher OpenSSL::Cipher.new('aes-256-gcm') end
Source
# File lib/rack/session/encryptor.rb, line 317 def new_salt_and_message_secret salt = SecureRandom.random_bytes(32) [salt, message_secret_from_salt(salt)] end
Source
# File lib/rack/session/encryptor.rb, line 327 def set_cipher_key(cipher, key) cipher.key = key end