homura-runtime 0.3.3 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +7 -0
  3. data/lib/homura/runtime/version.rb +1 -1
  4. data/vendor/rack/auth/abstract/handler.rb +41 -0
  5. data/vendor/rack/auth/abstract/request.rb +51 -0
  6. data/vendor/rack/auth/basic.rb +58 -0
  7. data/vendor/rack/bad_request.rb +8 -0
  8. data/vendor/rack/body_proxy.rb +63 -0
  9. data/vendor/rack/builder.rb +315 -0
  10. data/vendor/rack/cascade.rb +67 -0
  11. data/vendor/rack/common_logger.rb +94 -0
  12. data/vendor/rack/conditional_get.rb +87 -0
  13. data/vendor/rack/config.rb +22 -0
  14. data/vendor/rack/constants.rb +68 -0
  15. data/vendor/rack/content_length.rb +34 -0
  16. data/vendor/rack/content_type.rb +33 -0
  17. data/vendor/rack/deflater.rb +159 -0
  18. data/vendor/rack/directory.rb +210 -0
  19. data/vendor/rack/etag.rb +71 -0
  20. data/vendor/rack/events.rb +172 -0
  21. data/vendor/rack/files.rb +224 -0
  22. data/vendor/rack/head.rb +25 -0
  23. data/vendor/rack/headers.rb +238 -0
  24. data/vendor/rack/lint.rb +1000 -0
  25. data/vendor/rack/lock.rb +29 -0
  26. data/vendor/rack/media_type.rb +42 -0
  27. data/vendor/rack/method_override.rb +56 -0
  28. data/vendor/rack/mime.rb +694 -0
  29. data/vendor/rack/mock.rb +3 -0
  30. data/vendor/rack/mock_request.rb +161 -0
  31. data/vendor/rack/mock_response.rb +147 -0
  32. data/vendor/rack/multipart/generator.rb +99 -0
  33. data/vendor/rack/multipart/parser.rb +586 -0
  34. data/vendor/rack/multipart/uploaded_file.rb +82 -0
  35. data/vendor/rack/multipart.rb +77 -0
  36. data/vendor/rack/null_logger.rb +48 -0
  37. data/vendor/rack/protection/authenticity_token.rb +256 -0
  38. data/vendor/rack/protection/base.rb +140 -0
  39. data/vendor/rack/protection/content_security_policy.rb +80 -0
  40. data/vendor/rack/protection/cookie_tossing.rb +77 -0
  41. data/vendor/rack/protection/escaped_params.rb +93 -0
  42. data/vendor/rack/protection/form_token.rb +25 -0
  43. data/vendor/rack/protection/frame_options.rb +39 -0
  44. data/vendor/rack/protection/http_origin.rb +43 -0
  45. data/vendor/rack/protection/ip_spoofing.rb +27 -0
  46. data/vendor/rack/protection/json_csrf.rb +60 -0
  47. data/vendor/rack/protection/path_traversal.rb +45 -0
  48. data/vendor/rack/protection/referrer_policy.rb +27 -0
  49. data/vendor/rack/protection/remote_referrer.rb +22 -0
  50. data/vendor/rack/protection/remote_token.rb +24 -0
  51. data/vendor/rack/protection/session_hijacking.rb +37 -0
  52. data/vendor/rack/protection/strict_transport.rb +41 -0
  53. data/vendor/rack/protection/version.rb +7 -0
  54. data/vendor/rack/protection/xss_header.rb +27 -0
  55. data/vendor/rack/protection.rb +58 -0
  56. data/vendor/rack/query_parser.rb +261 -0
  57. data/vendor/rack/recursive.rb +66 -0
  58. data/vendor/rack/reloader.rb +112 -0
  59. data/vendor/rack/request.rb +818 -0
  60. data/vendor/rack/response.rb +403 -0
  61. data/vendor/rack/rewindable_input.rb +116 -0
  62. data/vendor/rack/runtime.rb +35 -0
  63. data/vendor/rack/sendfile.rb +197 -0
  64. data/vendor/rack/session/abstract/id.rb +533 -0
  65. data/vendor/rack/session/constants.rb +13 -0
  66. data/vendor/rack/session/cookie.rb +292 -0
  67. data/vendor/rack/session/encryptor.rb +415 -0
  68. data/vendor/rack/session/pool.rb +76 -0
  69. data/vendor/rack/session/version.rb +10 -0
  70. data/vendor/rack/session.rb +12 -0
  71. data/vendor/rack/show_exceptions.rb +433 -0
  72. data/vendor/rack/show_status.rb +121 -0
  73. data/vendor/rack/static.rb +188 -0
  74. data/vendor/rack/tempfile_reaper.rb +44 -0
  75. data/vendor/rack/urlmap.rb +99 -0
  76. data/vendor/rack/utils.rb +631 -0
  77. data/vendor/rack/version.rb +17 -0
  78. data/vendor/rack.rb +66 -0
  79. metadata +76 -1
@@ -0,0 +1,292 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2022-2023, by Samuel Williams.
5
+ # Copyright, 2022, by Jeremy Evans.
6
+ # Copyright, 2022, by Jon Dufresne.
7
+
8
+ # require 'openssl'
9
+ # require 'zlib'
10
+ require 'json'
11
+ require 'base64'
12
+ require 'delegate'
13
+
14
+ require 'rack/constants'
15
+ require 'rack/utils'
16
+
17
+ require_relative 'abstract/id'
18
+ require_relative 'encryptor'
19
+ require_relative 'constants'
20
+
21
+ module Rack
22
+
23
+ module Session
24
+
25
+ # Rack::Session::Cookie provides simple cookie based session management.
26
+ # By default, the session is a Ruby Hash that is serialized and encoded as
27
+ # a cookie set to :key (default: rack.session).
28
+ #
29
+ # This middleware accepts a :secrets option which enables encryption of
30
+ # session cookies. This option should be one or more random "secret keys"
31
+ # that are each at least 64 bytes in length. Multiple secret keys can be
32
+ # supplied in an Array, which is useful when rotating secrets.
33
+ #
34
+ # Several options are also accepted that are passed to Rack::Session::Encryptor.
35
+ # These options include:
36
+ # * :serialize_json
37
+ # Use JSON for message serialization instead of Marshal. This can be
38
+ # viewed as a security enhancement.
39
+ # * :gzip_over
40
+ # For message data over this many bytes, compress it with the deflate
41
+ # algorithm.
42
+ #
43
+ # Refer to Rack::Session::Encryptor for more details on these options.
44
+ #
45
+ # Prior to version TODO, the session hash was stored as base64 encoded
46
+ # marshalled data. When a :secret option was supplied, the integrity of the
47
+ # encoded data was protected with HMAC-SHA1. This functionality is still
48
+ # supported using a set of a legacy options.
49
+ #
50
+ # Lastly, a :coder option is also accepted. When used, both encryption and
51
+ # the legacy HMAC will be skipped. This option could create security issues
52
+ # in your application!
53
+ #
54
+ # Example:
55
+ #
56
+ # use Rack::Session::Cookie, {
57
+ # key: 'rack.session',
58
+ # domain: 'foo.com',
59
+ # path: '/',
60
+ # expire_after: 2592000,
61
+ # secrets: 'a randomly generated, raw binary string 64 bytes in size',
62
+ # }
63
+ #
64
+ # Example using legacy HMAC options:
65
+ #
66
+ # Rack::Session:Cookie.new(application, {
67
+ # # The secret used for legacy HMAC cookies, this enables the functionality
68
+ # legacy_hmac_secret: 'legacy secret',
69
+ # # legacy_hmac_coder will default to Rack::Session::Cookie::Base64::Marshal
70
+ # legacy_hmac_coder: Rack::Session::Cookie::Identity.new,
71
+ # # legacy_hmac will default to OpenSSL::Digest::SHA1
72
+ # legacy_hmac: OpenSSL::Digest::SHA256
73
+ # })
74
+ #
75
+ # Example of a cookie with no encoding:
76
+ #
77
+ # Rack::Session::Cookie.new(application, {
78
+ # :coder => Rack::Session::Cookie::Identity.new
79
+ # })
80
+ #
81
+ # Example of a cookie with custom encoding:
82
+ #
83
+ # Rack::Session::Cookie.new(application, {
84
+ # :coder => Class.new {
85
+ # def encode(str); str.reverse; end
86
+ # def decode(str); str.reverse; end
87
+ # }.new
88
+ # })
89
+ #
90
+
91
+ class Cookie < Abstract::PersistedSecure
92
+ # Encode session cookies as Base64
93
+ class Base64
94
+ def encode(str)
95
+ ::Base64.strict_encode64(str)
96
+ end
97
+
98
+ def decode(str)
99
+ ::Base64.decode64(str)
100
+ end
101
+
102
+ # Encode session cookies as Marshaled Base64 data
103
+ class Marshal < Base64
104
+ def encode(str)
105
+ super(::Marshal.dump(str))
106
+ end
107
+
108
+ def decode(str)
109
+ return unless str
110
+ ::Marshal.load(super(str)) rescue nil
111
+ end
112
+ end
113
+
114
+ # N.B. Unlike other encoding methods, the contained objects must be a
115
+ # valid JSON composite type, either a Hash or an Array.
116
+ class JSON < Base64
117
+ def encode(obj)
118
+ super(::JSON.dump(obj))
119
+ end
120
+
121
+ def decode(str)
122
+ return unless str
123
+ ::JSON.parse(super(str)) rescue nil
124
+ end
125
+ end
126
+
127
+ class ZipJSON < Base64
128
+ def encode(obj)
129
+ super(Zlib::Deflate.deflate(::JSON.dump(obj)))
130
+ end
131
+
132
+ def decode(str)
133
+ return unless str
134
+ ::JSON.parse(Zlib::Inflate.inflate(super(str)))
135
+ rescue
136
+ nil
137
+ end
138
+ end
139
+ end
140
+
141
+ # Use no encoding for session cookies
142
+ class Identity
143
+ def encode(str); str; end
144
+ def decode(str); str; end
145
+ end
146
+
147
+ class Marshal
148
+ def encode(str)
149
+ ::Marshal.dump(str)
150
+ end
151
+
152
+ def decode(str)
153
+ ::Marshal.load(str) if str
154
+ end
155
+ end
156
+
157
+ attr_reader :coder, :encryptors
158
+
159
+ def initialize(app, options = {})
160
+ # homura patch: Opal/Workers has no OpenSSL, so
161
+ # Rack::Session::Encryptor (which uses OpenSSL::Cipher and
162
+ # mutating String#slice!) cannot be instantiated. We skip
163
+ # encryption entirely and fall through to the plain
164
+ # Base64::Marshal coder. This is the same mode CRuby's
165
+ # Rack::Session::Cookie uses when no secret is provided —
166
+ # session data lives in the cookie as Base64'd Marshal, with
167
+ # no server-side verification. For production use, the
168
+ # operator should layer Cloudflare's own signed-cookie or
169
+ # JWT-based session management on top.
170
+ @encryptors = []
171
+ @legacy_hmac = false
172
+
173
+ # homura patch: use JSON coder instead of Marshal — Opal doesn't
174
+ # ship a full Marshal implementation, but JSON is native on JS.
175
+ @coder = options[:coder] ||= Base64::JSON.new
176
+
177
+ super(app, options.merge!(cookie_only: true))
178
+ end
179
+
180
+ private
181
+
182
+ def find_session(req, sid)
183
+ data = unpacked_cookie_data(req)
184
+ data = persistent_session_id!(data)
185
+ [data["session_id"], data]
186
+ end
187
+
188
+ def extract_session_id(request)
189
+ unpacked_cookie_data(request)&.[]("session_id")
190
+ end
191
+
192
+ def unpacked_cookie_data(request)
193
+ request.fetch_header(RACK_SESSION_UNPACKED_COOKIE_DATA) do |k|
194
+ if cookie_data = request.cookies[@key]
195
+ session_data = nil
196
+
197
+ # Try to decrypt the session data with our encryptors
198
+ encryptors.each do |encryptor|
199
+ begin
200
+ session_data = encryptor.decrypt(cookie_data)
201
+ break
202
+ rescue Rack::Session::Encryptor::Error => error
203
+ request.env[Rack::RACK_ERRORS].puts "Session cookie encryptor error: #{error.message}"
204
+
205
+ next
206
+ end
207
+ end
208
+
209
+ # If session decryption fails but there is @legacy_hmac_secret
210
+ # defined, attempt legacy HMAC verification
211
+ if !session_data && @legacy_hmac_secret
212
+ # Parse and verify legacy HMAC session cookie
213
+ session_data, _, digest = cookie_data.rpartition('--')
214
+ session_data = nil unless legacy_digest_match?(session_data, digest)
215
+
216
+ # Decode using legacy HMAC decoder
217
+ session_data = @legacy_hmac_coder.decode(session_data)
218
+
219
+ elsif !session_data && coder
220
+ # Use the coder option, which has the potential to be very unsafe
221
+ session_data = coder.decode(cookie_data)
222
+ end
223
+ end
224
+
225
+ request.set_header(k, session_data || {})
226
+ end
227
+ end
228
+
229
+ def persistent_session_id!(data, sid = nil)
230
+ data ||= {}
231
+ data["session_id"] ||= sid || generate_sid
232
+ data
233
+ end
234
+
235
+ class SessionId < DelegateClass(Session::SessionId)
236
+ attr_reader :cookie_value
237
+
238
+ def initialize(session_id, cookie_value)
239
+ super(session_id)
240
+ @cookie_value = cookie_value
241
+ end
242
+ end
243
+
244
+ def write_session(req, session_id, session, options)
245
+ session = session.merge("session_id" => session_id)
246
+ session_data = encode_session_data(session)
247
+
248
+ if session_data.size > (4096 - @key.size)
249
+ req.get_header(RACK_ERRORS).puts("Warning! Rack::Session::Cookie data size exceeds 4K.")
250
+ nil
251
+ else
252
+ SessionId.new(session_id, session_data)
253
+ end
254
+ end
255
+
256
+ def delete_session(req, session_id, options)
257
+ # Nothing to do here, data is in the client
258
+ generate_sid unless options[:drop]
259
+ end
260
+
261
+ def legacy_digest_match?(data, digest)
262
+ return false unless data && digest
263
+
264
+ Rack::Utils.secure_compare(digest, legacy_generate_hmac(data))
265
+ end
266
+
267
+ def legacy_generate_hmac(data)
268
+ OpenSSL::HMAC.hexdigest(@legacy_hmac, @legacy_hmac_secret, data)
269
+ end
270
+
271
+ def encode_session_data(session)
272
+ if encryptors.empty?
273
+ coder.encode(session)
274
+ else
275
+ encryptors.first.encrypt(session)
276
+ end
277
+ end
278
+
279
+ # Were consider "secure" if:
280
+ # * Encrypted cookies are enabled and one or more encryptor is
281
+ # initialized
282
+ # * The legacy HMAC option is enabled
283
+ # * Customer :coder is used, with :let_coder_handle_secure_encoding
284
+ # set to true
285
+ def secure?(options)
286
+ !@encryptors.empty? ||
287
+ @legacy_hmac ||
288
+ (options[:coder] && options[:let_coder_handle_secure_encoding])
289
+ end
290
+ end
291
+ end
292
+ end
@@ -0,0 +1,415 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2022-2023, by Samuel Williams.
5
+ # Copyright, 2022, by Philip Arndt.
6
+
7
+ require 'base64'
8
+ require 'json'
9
+ # require 'openssl'
10
+ require 'securerandom'
11
+
12
+ require 'rack/utils'
13
+
14
+ module Rack
15
+ module Session
16
+ class Encryptor
17
+ class Error < StandardError
18
+ end
19
+
20
+ class InvalidSignature < Error
21
+ end
22
+
23
+ class InvalidMessage < Error
24
+ end
25
+
26
+ module Serializable
27
+ private
28
+
29
+ # Returns a serialized payload of the message. If a :pad_size is supplied,
30
+ # the message will be padded. The first 2 bytes of the returned string will
31
+ # indicating the amount of padding.
32
+ def serialize_payload(message)
33
+ serialized_data = serializer.dump(message)
34
+
35
+ return "#{[0].pack('v')}#{serialized_data}" if @options[:pad_size].nil?
36
+
37
+ padding_bytes = @options[:pad_size] - (2 + serialized_data.size) % @options[:pad_size]
38
+ padding_data = SecureRandom.random_bytes(padding_bytes)
39
+
40
+ "#{[padding_bytes].pack('v')}#{padding_data}#{serialized_data}"
41
+ end
42
+
43
+ # Return the deserialized message. The first 2 bytes will be read as the
44
+ # amount of padding.
45
+ def deserialized_message(data)
46
+ # Read the first 2 bytes as the padding_bytes size
47
+ padding_bytes, = data.unpack('v')
48
+
49
+ # Slice out the serialized_data and deserialize it
50
+ serialized_data = data.slice(2 + padding_bytes, data.bytesize)
51
+ serializer.load serialized_data
52
+ end
53
+
54
+ def serializer
55
+ @serializer ||= @options[:serialize_json] ? JSON : Marshal
56
+ end
57
+ end
58
+
59
+ class V1
60
+ include Serializable
61
+
62
+ # The secret String must be at least 64 bytes in size. The first 32 bytes
63
+ # will be used for the encryption cipher key. The remainder will be used
64
+ # for an HMAC key.
65
+ #
66
+ # Options may include:
67
+ # * :serialize_json
68
+ # Use JSON for message serialization instead of Marshal. This can be
69
+ # viewed as a security enhancement.
70
+ # * :pad_size
71
+ # Pad encrypted message data, to a multiple of this many bytes
72
+ # (default: 32). This can be between 2-4096 bytes, or +nil+ to disable
73
+ # padding.
74
+ # * :purpose
75
+ # Limit messages to a specific purpose. This can be viewed as a
76
+ # security enhancement to prevent message reuse from different contexts
77
+ # if keys are reused.
78
+ #
79
+ # Cryptography and Output Format:
80
+ #
81
+ # urlsafe_encode64(version + random_data + IV + encrypted data + HMAC)
82
+ #
83
+ # Where:
84
+ # * version - 1 byte with value 0x01
85
+ # * random_data - 32 bytes used for generating the per-message secret
86
+ # * IV - 16 bytes random initialization vector
87
+ # * HMAC - 32 bytes HMAC-SHA-256 of all preceding data, plus the purpose
88
+ # value
89
+ def initialize(secret, opts = {})
90
+ raise ArgumentError, 'secret must be a String' unless secret.is_a?(String)
91
+ raise ArgumentError, "invalid secret: #{secret.bytesize}, must be >=64" unless secret.bytesize >= 64
92
+
93
+ case opts[:pad_size]
94
+ when nil
95
+ # padding is disabled
96
+ when Integer
97
+ raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}" unless (2..4096).include? opts[:pad_size]
98
+ else
99
+ raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}; must be Integer or nil"
100
+ end
101
+
102
+ @options = {
103
+ serialize_json: false, pad_size: 32, purpose: nil
104
+ }.update(opts)
105
+
106
+ @hmac_secret = secret.dup.force_encoding('BINARY')
107
+ @cipher_secret = @hmac_secret.slice!(0, 32)
108
+
109
+ @hmac_secret.freeze
110
+ @cipher_secret.freeze
111
+ end
112
+
113
+ def decrypt(base64_data)
114
+ data = Base64.urlsafe_decode64(base64_data)
115
+
116
+ signature = data.slice!(-32..-1)
117
+ verify_authenticity!(data, signature)
118
+
119
+ version = data.slice!(0, 1)
120
+ raise InvalidMessage, 'wrong version' unless version == "\1"
121
+
122
+ message_secret = data.slice!(0, 32)
123
+ cipher_iv = data.slice!(0, 16)
124
+
125
+ cipher = new_cipher
126
+ cipher.decrypt
127
+
128
+ set_cipher_key(cipher, cipher_secret_from_message_secret(message_secret))
129
+
130
+ cipher.iv = cipher_iv
131
+ data = cipher.update(data) << cipher.final
132
+
133
+ deserialized_message data
134
+ rescue ArgumentError
135
+ raise InvalidSignature, 'Message invalid'
136
+ end
137
+
138
+ def encrypt(message)
139
+ version = "\1"
140
+
141
+ serialized_payload = serialize_payload(message)
142
+ message_secret, cipher_secret = new_message_and_cipher_secret
143
+
144
+ cipher = new_cipher
145
+ cipher.encrypt
146
+
147
+ set_cipher_key(cipher, cipher_secret)
148
+
149
+ cipher_iv = cipher.random_iv
150
+
151
+ encrypted_data = cipher.update(serialized_payload) << cipher.final
152
+
153
+ data = String.new
154
+ data << version
155
+ data << message_secret
156
+ data << cipher_iv
157
+ data << encrypted_data
158
+ data << compute_signature(data)
159
+
160
+ Base64.urlsafe_encode64(data)
161
+ end
162
+
163
+ private
164
+
165
+ def new_cipher
166
+ OpenSSL::Cipher.new('aes-256-ctr')
167
+ end
168
+
169
+ def new_message_and_cipher_secret
170
+ message_secret = SecureRandom.random_bytes(32)
171
+
172
+ [message_secret, cipher_secret_from_message_secret(message_secret)]
173
+ end
174
+
175
+ def cipher_secret_from_message_secret(message_secret)
176
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), @cipher_secret, message_secret)
177
+ end
178
+
179
+ def set_cipher_key(cipher, key)
180
+ cipher.key = key
181
+ end
182
+
183
+ def compute_signature(data)
184
+ signing_data = data
185
+ signing_data += @options[:purpose] if @options[:purpose]
186
+
187
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), @hmac_secret, signing_data)
188
+ end
189
+
190
+ def verify_authenticity!(data, signature)
191
+ raise InvalidMessage, 'Message is invalid' if data.nil? || signature.nil?
192
+
193
+ unless Rack::Utils.secure_compare(signature, compute_signature(data))
194
+ raise InvalidSignature, 'HMAC is invalid'
195
+ end
196
+ end
197
+ end
198
+
199
+ class V2
200
+ include Serializable
201
+
202
+ # The secret String must be at least 32 bytes in size.
203
+ #
204
+ # Options may include:
205
+ # * :pad_size
206
+ # Pad encrypted message data, to a multiple of this many bytes
207
+ # (default: 32). This can be between 2-4096 bytes, or +nil+ to disable
208
+ # padding.
209
+ # * :purpose
210
+ # Limit messages to a specific purpose. This can be viewed as a
211
+ # security enhancement to prevent message reuse from different contexts
212
+ # if keys are reused.
213
+ #
214
+ # Cryptography and Output Format:
215
+ #
216
+ # strict_encode64(version + salt + IV + authentication tag + ciphertext)
217
+ #
218
+ # Where:
219
+ # * version - 1 byte with value 0x02
220
+ # * salt - 32 bytes used for generating the per-message secret
221
+ # * IV - 12 bytes random initialization vector
222
+ # * authentication tag - 16 bytes authentication tag generated by the GCM mode, covering version and salt
223
+ #
224
+ # Considerations about V2:
225
+ #
226
+ # 1) It serializes messages in JSON, period.
227
+ #
228
+ # 2) It uses non URL-safe Base64 encoding as it's faster than its
229
+ # URL-safe counterpart - as of Ruby 3.2, Base64.urlsafe_encode64 is
230
+ # roughly equivalent to do Base64.strict_encode64(data).tr("-_",
231
+ # "+/") - and cookie values don't need to be URL-safe.
232
+ def initialize(secret, opts = {})
233
+ raise ArgumentError, 'secret must be a String' unless secret.is_a?(String)
234
+
235
+ unless secret.bytesize >= 32
236
+ raise ArgumentError, "invalid secret: it's #{secret.bytesize}-byte long, must be >=32"
237
+ end
238
+
239
+ case opts[:pad_size]
240
+ when nil
241
+ # padding is disabled
242
+ when Integer
243
+ raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}" unless (2..4096).include? opts[:pad_size]
244
+ else
245
+ raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}; must be Integer or nil"
246
+ end
247
+
248
+ @options = {
249
+ pad_size: 32, purpose: nil
250
+ }.update(opts)
251
+ @options[:serialize_json] = true # Enforce JSON serialization
252
+
253
+ @cipher_secret = secret.dup.force_encoding('BINARY').slice!(0, 32)
254
+ @cipher_secret.freeze
255
+ end
256
+
257
+ def decrypt(base64_data)
258
+ data = Base64.strict_decode64(base64_data)
259
+ if data.bytesize <= 61 # version + salt + iv + auth_tag = 61 byte (and we also need some ciphertext :)
260
+ raise InvalidMessage, 'invalid message'
261
+ end
262
+
263
+ version = data[0]
264
+ raise InvalidMessage, 'invalid message' unless version == "\2"
265
+
266
+ ciphertext = data.slice!(61..-1)
267
+ auth_tag = data.slice!(45, 16)
268
+ cipher_iv = data.slice!(33, 12)
269
+
270
+ cipher = new_cipher
271
+ cipher.decrypt
272
+ salt = data.slice(1, 32)
273
+ set_cipher_key(cipher, message_secret_from_salt(salt))
274
+ cipher.iv = cipher_iv
275
+ cipher.auth_tag = auth_tag
276
+ cipher.auth_data = (purpose = @options[:purpose]) ? data + purpose : data
277
+
278
+ plaintext = cipher.update(ciphertext) << cipher.final
279
+
280
+ deserialized_message plaintext
281
+ rescue ArgumentError, OpenSSL::Cipher::CipherError
282
+ raise InvalidSignature, 'invalid message'
283
+ end
284
+
285
+ def encrypt(message)
286
+ version = "\2"
287
+
288
+ serialized_payload = serialize_payload(message)
289
+
290
+ cipher = new_cipher
291
+ cipher.encrypt
292
+ salt, message_secret = new_salt_and_message_secret
293
+ set_cipher_key(cipher, message_secret)
294
+ cipher.iv_len = 12
295
+ cipher_iv = cipher.random_iv
296
+
297
+ data = String.new
298
+ data << version
299
+ data << salt
300
+
301
+ cipher.auth_data = (purpose = @options[:purpose]) ? data + purpose : data
302
+ encrypted_data = cipher.update(serialized_payload) << cipher.final
303
+
304
+ data << cipher_iv
305
+ data << auth_tag_from(cipher)
306
+ data << encrypted_data
307
+
308
+ Base64.strict_encode64(data)
309
+ end
310
+
311
+ private
312
+
313
+ def new_cipher
314
+ OpenSSL::Cipher.new('aes-256-gcm')
315
+ end
316
+
317
+ def new_salt_and_message_secret
318
+ salt = SecureRandom.random_bytes(32)
319
+
320
+ [salt, message_secret_from_salt(salt)]
321
+ end
322
+
323
+ def message_secret_from_salt(salt)
324
+ OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), @cipher_secret, salt)
325
+ end
326
+
327
+ def set_cipher_key(cipher, key)
328
+ cipher.key = key
329
+ end
330
+
331
+ if RUBY_ENGINE == 'jruby'
332
+ # JRuby's OpenSSL implementation doesn't currently support passing
333
+ # an argument to #auth_tag. Here we work around that.
334
+ def auth_tag_from(cipher)
335
+ tag = cipher.auth_tag
336
+ raise Error, 'the auth tag must be 16 bytes long' if tag.bytesize != 16
337
+
338
+ tag
339
+ end
340
+ else
341
+ def auth_tag_from(cipher)
342
+ cipher.auth_tag(16)
343
+ end
344
+ end
345
+ end
346
+
347
+ def initialize(secret, opts = {})
348
+ opts = opts.dup
349
+
350
+ @mode = opts.delete(:mode)&.to_sym || :guess_version
351
+ case @mode
352
+ when :v1
353
+ @v1 = V1.new(secret, opts)
354
+ when :v2
355
+ @v2 = V2.new(secret, opts)
356
+ else
357
+ @v1 = V1.new(secret, opts)
358
+ @v2 = V2.new(secret, opts)
359
+ end
360
+ end
361
+
362
+ def decrypt(base64_data)
363
+ decryptor =
364
+ case @mode
365
+ when :v2
366
+ v2
367
+ when :v1
368
+ v1
369
+ else
370
+ guess_decryptor(base64_data)
371
+ end
372
+
373
+ decryptor.decrypt(base64_data)
374
+ end
375
+
376
+ def encrypt(message)
377
+ encryptor =
378
+ case @mode
379
+ when :v1
380
+ v1
381
+ else
382
+ v2
383
+ end
384
+
385
+ encryptor.encrypt(message)
386
+ end
387
+
388
+ private
389
+
390
+ attr_reader :v1, :v2
391
+
392
+ def guess_decryptor(base64_data)
393
+ raise InvalidMessage, 'invalid message' if base64_data.nil? || base64_data.bytesize < 4
394
+
395
+ first_encoded_4_bytes = base64_data.slice(0, 4)
396
+ # Transform the 4 bytes into non-URL-safe base64-encoded data. Nothing
397
+ # happens if the data is already non-URL-safe base64.
398
+ first_encoded_4_bytes.tr!('-_', '+/')
399
+ first_decoded_3_bytes = Base64.strict_decode64(first_encoded_4_bytes)
400
+
401
+ version = first_decoded_3_bytes[0]
402
+ case version
403
+ when "\2"
404
+ v2
405
+ when "\1"
406
+ v1
407
+ else
408
+ raise InvalidMessage, 'invalid message'
409
+ end
410
+ rescue ArgumentError
411
+ raise InvalidMessage, 'invalid message'
412
+ end
413
+ end
414
+ end
415
+ end