Class: Rack::Session::Encryptor::V2

Inherits:
Object
  • Object
show all
Includes:
Serializable
Defined in:
lib/rack/session/encryptor.rb

Instance Method Summary collapse

Constructor Details

#initialize(secret, opts = {}) ⇒ V2

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:

urlsafe_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 URL-safe Base64 encoding (RFC 4648) to ensure cookie values are not corrupted by Rack's cookie parser, which applies URI.decode_www_form_component to cookie values and converts '+' to space. Standard Base64 (strict_encode64) can produce '+' characters, which would corrupt the cookie value and cause decryption to fail.

Raises:

  • (ArgumentError)


231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/rack/session/encryptor.rb', line 231

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

Instance Method Details

#decrypt(base64_data) ⇒ Object



255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
# File 'lib/rack/session/encryptor.rb', line 255

def decrypt(base64_data)
  data = Base64.urlsafe_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

#encrypt(message) ⇒ Object



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
# File 'lib/rack/session/encryptor.rb', line 283

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.urlsafe_encode64(data)
end