class Rack::Session::Encryptor
def cipher_secret_from_message_secret(message_secret)
def cipher_secret_from_message_secret(message_secret) OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, @cipher_secret, message_secret) end
def compute_signature(data)
def compute_signature(data) signing_data = data signing_data += @options[:purpose] if @options[:purpose] OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, @hmac_secret, signing_data) end
def decrypt(base64_data)
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
def deserialized_message(data)
Return the deserialized message. The first 2 bytes will be read as the
def deserialized_message(data) # Read the first 2 bytes as the padding_bytes size padding_bytes, = data.unpack('v') # Slice out the serialized_data and deserialize it serialized_data = data.slice(2 + padding_bytes, data.bytesize) serializer.load serialized_data end
def encrypt(message)
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
def initialize(secret, opts = {})
* HMAC - 32 bytes HMAC-SHA-256 of all preceding data, plus the purpose
* IV - 16 bytes random initialization vector
* random_data - 32 bytes used for generating the per-message secret
* version - 1 byte and is currently always 0x01
Where:
urlsafe_encode64(version + random_data + IV + encrypted data + HMAC)
Cryptography and Output Format:
if keys are reused.
security enhancement to prevent message reuse from different contexts
Limit messages to a specific purpose. This can be viewed as a
* :purpose
padding.
(default: 32). This can be between 2-4096 bytes, or +nil+ to disable
Pad encrypted message data, to a multiple of this many bytes
* :pad_size
viewed as a security ehancement.
Use JSON for message serialization instead of Marshal. This can be
* :serialize_json
Options may include:
for an HMAC key.
will be used for the encryption cipher key. The remainder will be used
The secret String must be at least 64 bytes in size. The first 32 bytes
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
def new_cipher
def new_cipher OpenSSL::Cipher.new('aes-256-ctr') end
def new_message_and_cipher_secret
def new_message_and_cipher_secret message_secret = SecureRandom.random_bytes(32) [message_secret, cipher_secret_from_message_secret(message_secret)] end
def serialize_payload(message)
the message will be padded. The first 2 bytes of the returned string will
Returns a serialized payload of the message. If a :pad_size is supplied,
def serialize_payload(message) serialized_data = serializer.dump(message) return "#{[0].pack('v')}#{serialized_data}" if @options[:pad_size].nil? padding_bytes = @options[:pad_size] - (2 + serialized_data.size) % @options[:pad_size] padding_data = SecureRandom.random_bytes(padding_bytes) "#{[padding_bytes].pack('v')}#{padding_data}#{serialized_data}" end
def serializer
def serializer @serializer ||= @options[:serialize_json] ? JSON : Marshal end
def set_cipher_key(cipher, key)
def set_cipher_key(cipher, key) cipher.key = key end
def verify_authenticity!(data, signature)
def verify_authenticity!(data, signature) raise InvalidMessage, 'Message is invalid' if data.nil? || signature.nil? unless Rack::Utils.secure_compare(signature, compute_signature(data)) raise InvalidSignature, 'HMAC is invalid' end end