File: spec_session_encryptor.rb

package info (click to toggle)
ruby-rack-session 2.1.1-0.1
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 312 kB
  • sloc: ruby: 1,885; makefile: 4
file content (168 lines) | stat: -rw-r--r-- 5,690 bytes parent folder | download
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