Class: Rack::Session::Encryptor

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

Defined Under Namespace

Classes: Error, InvalidMessage, InvalidSignature

Instance Method Summary collapse

Constructor Details

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

The secret String must be at least 64 bytes in size. The first 32 bytes will be used for the encryption cipher key. The remainder will be used for an HMAC key.

Options may include:

  • :serialize_json

    Use JSON for message serialization instead of Marshal. This can be
    viewed as a security enhancement.
    
  • :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 + random_data + IV + encrypted data + HMAC)

Where:
* version - 1 byte and is currently always 0x01
* random_data - 32 bytes used for generating the per-message secret
* IV - 16 bytes random initialization vector
* HMAC - 32 bytes HMAC-SHA-256 of all preceding data, plus the purpose
  value

Raises:

  • (ArgumentError)


53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
# File 'lib/rack/session/encryptor.rb', line 53

def initialize(secret, opts = {})
  raise ArgumentError, "secret must be a String" unless String === secret
  raise ArgumentError, "invalid secret: #{secret.bytesize}, must be >=64" unless secret.bytesize >= 64

  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)

  @hmac_secret = secret.dup.force_encoding('BINARY')
  @cipher_secret = @hmac_secret.slice!(0, 32)

  @hmac_secret.freeze
  @cipher_secret.freeze
end

Instance Method Details

#decrypt(base64_data) ⇒ Object



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/rack/session/encryptor.rb', line 77

def decrypt(base64_data)
  data = Base64.urlsafe_decode64(base64_data)

  signature = data.slice!(-32..-1)

  verify_authenticity! data, signature

  # The version is reserved for future
  _version = data.slice!(0, 1)
  message_secret = data.slice!(0, 32)
  cipher_iv = data.slice!(0, 16)

  cipher = new_cipher
  cipher.decrypt

  set_cipher_key(cipher, cipher_secret_from_message_secret(message_secret))

  cipher.iv = cipher_iv
  data = cipher.update(data) << cipher.final

  deserialized_message data
rescue ArgumentError
  raise InvalidSignature, 'Message invalid'
end

#encrypt(message) ⇒ Object



102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/rack/session/encryptor.rb', line 102

def encrypt(message)
  version = "\1"

  serialized_payload = serialize_payload(message)
  message_secret, cipher_secret = new_message_and_cipher_secret

  cipher = new_cipher
  cipher.encrypt

  set_cipher_key(cipher, cipher_secret)

  cipher_iv = cipher.random_iv

  encrypted_data = cipher.update(serialized_payload) << cipher.final

  data = String.new
  data << version
  data << message_secret
  data << cipher_iv
  data << encrypted_data
  data << compute_signature(data)

  Base64.urlsafe_encode64(data)
end