1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168
|
# frozen_string_literal: true
# Released under the MIT License.
# Copyright, 2022-2023, by Samuel Williams.
require_relative 'helper'
require 'rack/session/encryptor'
describe Rack::Session::Encryptor do
def setup
@secret = SecureRandom.random_bytes(64)
end
it 'initialize does not destroy key string' do
encryptor = Rack::Session::Encryptor.new(@secret)
@secret.size.must_equal 64
end
it 'initialize raises ArgumentError on invalid key' do
lambda { Rack::Session::Encryptor.new [:foo] }.must_raise ArgumentError
end
it 'initialize raises ArgumentError on short key' do
lambda { Rack::Session::Encryptor.new 'key' }.must_raise ArgumentError
end
it 'decrypts an encrypted message' do
encryptor = Rack::Session::Encryptor.new(@secret)
message = encryptor.encrypt(foo: 'bar')
encryptor.decrypt(message).must_equal foo: 'bar'
end
it 'decrypt raises InvalidSignature for tampered messages' do
encryptor = Rack::Session::Encryptor.new(@secret)
message = encryptor.encrypt(foo: 'bar')
decoded_message = Base64.urlsafe_decode64(message)
tampered_message = Base64.urlsafe_encode64(decoded_message.tap { |m|
m[m.size - 1] = (m[m.size - 1].unpack('C')[0] ^ 1).chr
})
lambda {
encryptor.decrypt(tampered_message)
}.must_raise Rack::Session::Encryptor::InvalidSignature
end
it 'decrypts an encrypted message with purpose' do
encryptor = Rack::Session::Encryptor.new(@secret, purpose: 'testing')
message = encryptor.encrypt(foo: 'bar')
encryptor.decrypt(message).must_equal foo: 'bar'
end
it 'decrypts raises InvalidSignature without purpose' do
encryptor = Rack::Session::Encryptor.new(@secret, purpose: 'testing')
other_encryptor = Rack::Session::Encryptor.new(@secret)
message = other_encryptor.encrypt(foo: 'bar')
lambda { encryptor.decrypt(message) }.must_raise Rack::Session::Encryptor::InvalidSignature
end
it 'decrypts raises InvalidSignature with different purpose' do
encryptor = Rack::Session::Encryptor.new(@secret, purpose: 'testing')
other_encryptor = Rack::Session::Encryptor.new(@secret, purpose: 'other')
message = other_encryptor.encrypt(foo: 'bar')
lambda { encryptor.decrypt(message) }.must_raise Rack::Session::Encryptor::InvalidSignature
end
it 'initialize raises ArgumentError on invalid pad_size' do
lambda { Rack::Session::Encryptor.new @secret, pad_size: :bar }.must_raise ArgumentError
end
it 'initialize raises ArgumentError on to short pad_size' do
lambda { Rack::Session::Encryptor.new @secret, pad_size: 1 }.must_raise ArgumentError
end
it 'initialize raises ArgumentError on to long pad_size' do
lambda { Rack::Session::Encryptor.new @secret, pad_size: 8023 }.must_raise ArgumentError
end
it 'decrypts an encrypted message without pad_size' do
encryptor = Rack::Session::Encryptor.new(@secret, purpose: 'testing', pad_size: nil)
message = encryptor.encrypt(foo: 'bar')
encryptor.decrypt(message).must_equal foo: 'bar'
end
it 'encryptor with pad_size increases message size' do
no_pad_encryptor = Rack::Session::Encryptor.new(@secret, purpose: 'testing', pad_size: nil)
pad_encryptor = Rack::Session::Encryptor.new(@secret, purpose: 'testing', pad_size: 64)
message_without = Base64.urlsafe_decode64(no_pad_encryptor.encrypt(''))
message_with = Base64.urlsafe_decode64(pad_encryptor.encrypt(''))
message_size_diff = message_with.bytesize - message_without.bytesize
message_with.bytesize.must_be :>, message_without.bytesize
message_size_diff.must_equal 64 - Marshal.dump('').bytesize - 2
end
it 'encryptor with pad_size has message payload size to multiple of pad_size' do
encryptor = Rack::Session::Encryptor.new(@secret, purpose: 'testing', pad_size: 24)
message = encryptor.encrypt(foo: 'bar' * 4)
decoded_message = Base64.urlsafe_decode64(message)
# slice 1 byte for version, 32 bytes for cipher_secret, 16 bytes for IV
# from the start of the string and 32 bytes at the end of the string
encrypted_payload = decoded_message[(1 + 32 + 16)..-33]
(encrypted_payload.bytesize % 24).must_equal 0
end
# This test checks the one-time message key IS NOT used as the cipher key.
# Doing so would remove the confidentiality assurances as the key is
# essentially included in plaintext then.
it 'uses a secret cipher key for encryption and decryption' do
cipher = OpenSSL::Cipher.new('aes-256-ctr')
encryptor = Rack::Session::Encryptor.new(@secret)
message = encryptor.encrypt(foo: 'bar')
raw_message = Base64.urlsafe_decode64(message)
ver = raw_message.slice!(0, 1)
key = raw_message.slice!(0, 32)
iv = raw_message.slice!(0, 16)
cipher.decrypt
cipher.key = key
cipher.iv = iv
data = cipher.update(raw_message) << cipher.final
# "data" should now be random bytes because we tried to decrypt a message
# with the wrong key
padding_bytes, = data.unpack('v') # likely a large number
serialized_data = data.slice(2 + padding_bytes, data.bytesize) # likely nil
lambda { Marshal.load serialized_data }.must_raise TypeError
end
it 'it calls set_cipher_key with the correct key' do
encryptor = Rack::Session::Encryptor.new(@secret, purpose: 'testing', pad_size: 24)
message = encryptor.encrypt(foo: 'bar')
message_key = Base64.urlsafe_decode64(message).slice(1, 32)
callable = proc do |cipher, key|
key.wont_equal @secret
key.wont_equal message_key
cipher.key = key
end
encryptor.stub :set_cipher_key, callable do
encryptor.decrypt message
end
end
end
|