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:

 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 serializes messages in JSON, period.

2) 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 do Base64.strict_encode64(data).tr("-_",
"+/") - and cookie values don't need to be URL-safe.

Raises:

  • (ArgumentError)


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

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 = {
    pad_size: 32, purpose: nil
  }.update(opts)
  @options[:serialize_json] = true # Enforce JSON serialization

  @cipher_secret = secret.dup.force_encoding(Encoding::BINARY).slice!(0, 32)
  @cipher_secret.freeze
end

Instance Method Details

#decrypt(base64_data) ⇒ Object



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
282
283
# 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

#encrypt(message) ⇒ Object



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
# 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