mayu-live 0.0.3 → 0.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 913188c20ab0304da5f9bf5c9dd0f82e3fbfc26a1f4b9207dc19e7e87fb01932
4
- data.tar.gz: beef66574a2f14c57a4b5a8f14d76818a3a41e8ffa6bf6fa7cf2d064cd834dbc
3
+ metadata.gz: 3f819e4512b624c496e60ad22b2461db0ed9afbe13b2b5b528358266dd83cbe7
4
+ data.tar.gz: 4185727d1855c3eb019d27c0f2ab82d17d2941d3b8101043cb55d6fa9d31d350
5
5
  SHA512:
6
- metadata.gz: e0d5bd062fcf1ed0058c49c3da133a4859ceb6a0c6711b5429b737b0dfa496d3ccbad3390e406eae1721744f2df48cb6194cfecbc0dacab7f1ee2c91c8c643cf
7
- data.tar.gz: 8529908ec520dd9c2e27b8f0216a5025c94b01d224656f1b049ccceaf92a7b916122a3442ed54283bc619825242668a8176d8d6488c7669c74bd4002677d744d
6
+ metadata.gz: 314a09735d9870f64c4ce7635ff072281c87e11dc6414786957188158f047e70bef8757b5bb9bc038b0de7a4d298e5f4ea1a0fecd3ffeb485fc01c6b1f4ac54a
7
+ data.tar.gz: 3ec46f730741fcb7d6918b3a997704a298eef23572e3ffcf57903f97c2809bf82591bec9d70da4f33f0f9ce992bcc802a55df626dcc1ae1f7e83c57cba64466e
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # <img alt="Mayu Live" width="337" src="https://user-images.githubusercontent.com/41148/192179194-f44aed92-74a3-4b59-a25e-bf2a8d313796.png">
1
+ # ![Mayu Live](https://raw.githubusercontent.com/mayu-live/logo/main/logo-with-text.svg)
2
2
 
3
3
  [![Tests](https://img.shields.io/github/actions/workflow/status/mayu-live/framework/.github/workflows/test.yml?branch=main&label=Tests&style=flat-square)](https://github.com/mayu-live/framework/actions/workflows/test.yml)
4
4
  [![Release](https://img.shields.io/github/v/release/mayu-live/framework?sort=semver&style=flat-square)](https://github.com/mayu-live/framework/releases)
@@ -9,7 +9,7 @@
9
9
 
10
10
  # Description
11
11
 
12
- Mayu is a live streaming server side component-based
12
+ Mayu is a live updating server side component-based
13
13
  VirtualDOM rendering framework written in Ruby.
14
14
 
15
15
  Everything runs on the server, except for a tiny little runtime that
@@ -0,0 +1,119 @@
1
+ # typed: true
2
+ # frozen_string_literal: true
3
+
4
+ require "sorbet-runtime"
5
+ require "rbnacl"
6
+ require "brotli"
7
+
8
+ module Mayu
9
+ class EncryptedMarshal
10
+ class Error < StandardError
11
+ end
12
+
13
+ class IssuedInTheFutureError < Error
14
+ end
15
+
16
+ class ExpiredError < Error
17
+ end
18
+
19
+ class DecryptError < Error
20
+ end
21
+
22
+ AdditionalData =
23
+ Data.define(:issued_at, :ttl) do
24
+ def self.create(ttl:, now: Time.now) = new(now.to_f, ttl)
25
+
26
+ def self.unpack(data)
27
+ data.unpack("D S") => [issued_at, ttl]
28
+ new(issued_at, ttl)
29
+ end
30
+
31
+ def pack = [issued_at, ttl].pack("D S")
32
+ def expired?(now: Time.now) = now > expires_at
33
+ def expires_at = Time.at(issued_at + ttl)
34
+ end
35
+
36
+ Message =
37
+ Data.define(:nonce, :ad, :ciphertext) do
38
+ def self.unpack(data)
39
+ data.unpack("S a*") => [nonce_length, data]
40
+ data.unpack("a#{nonce_length} S a*") => [nonce, ad_length, data]
41
+ data.unpack("a#{ad_length} a*") => [ad, ciphertext]
42
+ new(nonce, AdditionalData.unpack(ad), ciphertext)
43
+ end
44
+
45
+ def pack
46
+ packed_ad = ad.pack
47
+ [
48
+ nonce.bytesize,
49
+ nonce,
50
+ packed_ad.bytesize,
51
+ packed_ad,
52
+ ciphertext
53
+ ].pack("S a* S a* a*")
54
+ end
55
+
56
+ def expired?(now: Time.now) = ad.expired?(now:)
57
+ def expires_at = ad.expires_at
58
+
59
+ def verify_timestamps!(now: Time.now)
60
+ if ad.expired?(now:)
61
+ raise ExpiredError, "Message expired at #{ad.expires_at}"
62
+ end
63
+
64
+ if ad.issued_at > now.to_f
65
+ raise IssuedInTheFutureError,
66
+ "Message was issued in the future, #{Time.at(ad.issued_at)}"
67
+ end
68
+
69
+ self
70
+ end
71
+ end
72
+
73
+ extend T::Sig
74
+
75
+ Cipher = RbNaCl::AEAD::ChaCha20Poly1305IETF
76
+
77
+ DEFAULT_TTL_SECONDS = 10
78
+
79
+ sig { returns(String) }
80
+ def self.random_key = RbNaCl::Random.random_bytes(Cipher::KEYBYTES)
81
+
82
+ sig { params(base_key: String, default_ttl: Integer).void }
83
+ def initialize(base_key, default_ttl: DEFAULT_TTL_SECONDS)
84
+ @cipher = Cipher.new(RbNaCl::Hash.sha256(base_key))
85
+ @default_ttl = default_ttl
86
+ end
87
+
88
+ sig { params(object: T.untyped, ttl: Integer).returns(String) }
89
+ def dump(object, ttl: @default_ttl) =
90
+ object
91
+ .then { Marshal.dump(_1) }
92
+ .then { Brotli.deflate(_1) }
93
+ .then { encrypt(_1, ttl:) }
94
+
95
+ sig { params(encrypted: String).returns(T.untyped) }
96
+ def load(encrypted) =
97
+ encrypted
98
+ .then { Message.unpack(_1) }
99
+ .then { _1.verify_timestamps! }
100
+ .then { decrypt(_1) }
101
+ .then { Brotli.inflate(_1) }
102
+ .then { Marshal.load(_1) }
103
+
104
+ private
105
+
106
+ def encrypt(message, ttl:)
107
+ nonce = RbNaCl::Random.random_bytes(@cipher.nonce_bytes)
108
+ ad = AdditionalData.create(ttl:)
109
+ ciphertext = @cipher.encrypt(nonce, message, ad.pack)
110
+ Message.new(nonce, ad, ciphertext).pack
111
+ end
112
+
113
+ def decrypt(message)
114
+ @cipher.decrypt(message.nonce, message.ciphertext, message.ad.pack)
115
+ rescue RbNaCl::CryptoError => e
116
+ raise DecryptError, e.message
117
+ end
118
+ end
119
+ end
@@ -10,7 +10,7 @@ require_relative "metrics"
10
10
  require_relative "app_metrics"
11
11
  require_relative "resources/registry"
12
12
  require_relative "fetch"
13
- require_relative "message_cipher"
13
+ require_relative "encrypted_marshal"
14
14
  require_relative "configuration"
15
15
 
16
16
  module Mayu
@@ -34,8 +34,8 @@ module Mayu
34
34
  attr_reader :resources
35
35
  sig { returns(Fetch) }
36
36
  attr_reader :fetch
37
- sig { returns(MessageCipher) }
38
- attr_reader :message_cipher
37
+ sig { returns(EncryptedMarshal) }
38
+ attr_reader :encrypted_marshal
39
39
  sig { returns(AppMetrics) }
40
40
  attr_reader :metrics
41
41
 
@@ -44,8 +44,11 @@ module Mayu
44
44
  @root = T.let(config.root, String)
45
45
  @app_root = T.let(File.join(config.root, "app"), String)
46
46
  @config = config
47
- @message_cipher =
48
- T.let(MessageCipher.new(key: config.secret_key, ttl: 30), MessageCipher)
47
+ @encrypted_marshal =
48
+ T.let(
49
+ EncryptedMarshal.new(config.secret_key, default_ttl: 30),
50
+ EncryptedMarshal
51
+ )
49
52
  # TODO: Reload routes when things change in /pages...
50
53
  # Should probably make routes into a resource type.
51
54
  @routes =
@@ -1,5 +1,5 @@
1
- @layer app\/components\/MyComponent\?zQoPHO29 {
2
- @media (min-width: 8em) and (max-width: 32em) {
1
+ @layer app\/components\/MyComponent\?E59aOM9B {
2
+ @media (width >= 8em) and (width <= 32em) {
3
3
  .app\/components\/MyComponent\.foo\?Ef7fDeuq {
4
4
  color: #f0f;
5
5
  }
@@ -9,4 +9,4 @@
9
9
  color: #00f;
10
10
  }
11
11
 
12
- }
12
+ }
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  # typed: strict
3
3
 
4
- require "svg_optimizer"
5
4
  require_relative "base"
6
5
 
7
6
  module Mayu
@@ -10,30 +9,16 @@ module Mayu
10
9
  class SVG < Base
11
10
  extend T::Sig
12
11
 
13
- SVG_OPTIMIZER_PLUGINS =
14
- T.let(
15
- SvgOptimizer::DEFAULT_PLUGINS -
16
- [
17
- # The following plugin sets fill="none" in some cases
18
- # which breaks the fontawesome icons... That's why
19
- # it's disabled...
20
- SvgOptimizer::Plugins::RemoveUselessStrokeAndFill
21
- ],
22
- T::Array[T.class_of(SvgOptimizer::Plugins::Base)]
23
- )
24
-
25
12
  sig { params(resource: Resource).void }
26
13
  def initialize(resource)
27
14
  @resource = resource
28
15
 
29
- original = resource.read(encoding: "utf-8")
30
- optimized = SvgOptimizer.optimize(original, SVG_OPTIMIZER_PLUGINS)
16
+ source = resource.read(encoding: "utf-8")
31
17
 
32
- content_hash =
33
- Base64.urlsafe_encode64(Digest::SHA256.digest(optimized))
18
+ content_hash = Base64.urlsafe_encode64(Digest::SHA256.digest(source))
34
19
 
35
20
  @filename = T.let("#{content_hash}.svg", String)
36
- @source = T.let("#{optimized}\n", String)
21
+ @source = T.let(source, String)
37
22
  end
38
23
 
39
24
  sig { returns(T::Array[Asset]) }
@@ -433,7 +433,7 @@ module Mayu
433
433
  )
434
434
  end
435
435
 
436
- @environment.message_cipher.load(body) => String => dumped
436
+ @environment.encrypted_marshal.load(body) => String => dumped
437
437
  session = Session.restore(environment: @environment, dumped:)
438
438
  @sessions.store(session.id, session)
439
439
  end
@@ -454,7 +454,7 @@ module Mayu
454
454
  EventStream::Message.new(
455
455
  :"session.transfer",
456
456
  EventStream::Blob.new(
457
- @environment.message_cipher.dump(
457
+ @environment.encrypted_marshal.dump(
458
458
  Session::SerializedSession.dump_session(session)
459
459
  )
460
460
  )
@@ -64,9 +64,9 @@ module Mayu
64
64
  text_response(403, "session cookie not set")
65
65
  rescue SessionNotFound => e
66
66
  text_response(404, "session not found")
67
- rescue Mayu::MessageCipher::DecryptError => e
67
+ rescue Mayu::EncryptedMarshal::DecryptError => e
68
68
  text_response(403, "decrypt error")
69
- rescue Mayu::MessageCipher::ExpiredError => e
69
+ rescue Mayu::EncryptedMarshal::ExpiredError => e
70
70
  text_response(403, "session expired")
71
71
  rescue InvalidToken => e
72
72
  text_response(403, "invalid token")
data/lib/mayu/session.rb CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "time"
4
4
  require "nanoid"
5
+ require "rbnacl"
5
6
  require_relative "environment"
6
7
  require_relative "vdom/vtree"
7
8
  require_relative "vdom/marshalling"
@@ -77,11 +78,7 @@ module Mayu
77
78
 
78
79
  sig { params(token: String).returns(T::Boolean) }
79
80
  def authorized?(token)
80
- if self.token.length == token.length
81
- OpenSSL.fixed_length_secure_compare(self.token, token)
82
- else
83
- false
84
- end
81
+ RbNaCl::Util.verify64(self.token, token)
85
82
  end
86
83
 
87
84
  Marshaled = T.type_alias { [String, String, String, String, String] }
@@ -166,7 +163,7 @@ module Mayu
166
163
  # freeze
167
164
 
168
165
  # encrypted_session =
169
- # @environment.message_cipher.dump(SerializedSession.dump_session(self))
166
+ # @environment.encrypted_marshal.dump(SerializedSession.dump_session(self))
170
167
 
171
168
  links = [
172
169
  %{<script async type="module" src="/__mayu/runtime/#{environment.init_js}##{id}" crossorigin="same-origin" fetchpriority="high"></script>},
data/lib/mayu/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
  # frozen_string_literal: true
3
3
 
4
4
  module Mayu
5
- VERSION = "0.0.3"
5
+ VERSION = "0.0.4"
6
6
  end
data/mayu-live.gemspec CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |spec|
11
11
  spec.summary = "Server side VDOM framework"
12
12
 
13
13
  spec.description = <<~EOF
14
- Mayu Live is a live streaming server side VirtualDOM framework for Ruby,
14
+ Mayu Live is a live updating server side VirtualDOM framework for Ruby,
15
15
  inspired by modern frontend tools that exist in the JavaScript ecosystem.
16
16
  EOF
17
17
 
@@ -53,8 +53,9 @@ Gem::Specification.new do |spec|
53
53
  spec.add_dependency "prometheus-client", "~> 4.0.0"
54
54
  spec.add_dependency "protocol-http", "~> 0.23.12"
55
55
  spec.add_dependency "pry", "~> 0.14.2"
56
- spec.add_dependency "rack", "~> 3.0.4.1"
56
+ spec.add_dependency "rack", ">= 3.0.4.1", "< 3.0.9.0"
57
57
  spec.add_dependency "rake", "~> 13.0.6"
58
+ spec.add_dependency "rbnacl", "~> 7.1.1"
58
59
  spec.add_dependency "sorbet-runtime", "~> 0.5.10634"
59
60
  spec.add_dependency "terminal-table", "~> 3.0.2"
60
61
  spec.add_dependency "toml-rb", "~> 2.2.0"
@@ -67,9 +68,8 @@ Gem::Specification.new do |spec|
67
68
  spec.add_dependency "image_size", "~> 3.2.0"
68
69
  spec.add_dependency "kramdown", "~> 2.4.0"
69
70
  spec.add_dependency "rouge", "~> 4.0.0"
70
- spec.add_dependency "mayu-css", "~> 0.0.1"
71
+ spec.add_dependency "mayu-css", "~> 0.0.3"
71
72
  spec.add_dependency "source_map", "~> 3.0.1"
72
- spec.add_dependency "svg_optimizer", "~> 0.2.6"
73
73
  spec.add_dependency "syntax_tree", "~> 5.3.0"
74
74
  spec.add_dependency "syntax_tree-haml", "~> 3.0.0"
75
75
  spec.add_dependency "syntax_tree-xml", "~> 0.1.0"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mayu-live
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andreas Alin
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-06-10 00:00:00.000000000 Z
11
+ date: 2023-12-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: async
@@ -154,16 +154,22 @@ dependencies:
154
154
  name: rack
155
155
  requirement: !ruby/object:Gem::Requirement
156
156
  requirements:
157
- - - "~>"
157
+ - - ">="
158
158
  - !ruby/object:Gem::Version
159
159
  version: 3.0.4.1
160
+ - - "<"
161
+ - !ruby/object:Gem::Version
162
+ version: 3.0.9.0
160
163
  type: :runtime
161
164
  prerelease: false
162
165
  version_requirements: !ruby/object:Gem::Requirement
163
166
  requirements:
164
- - - "~>"
167
+ - - ">="
165
168
  - !ruby/object:Gem::Version
166
169
  version: 3.0.4.1
170
+ - - "<"
171
+ - !ruby/object:Gem::Version
172
+ version: 3.0.9.0
167
173
  - !ruby/object:Gem::Dependency
168
174
  name: rake
169
175
  requirement: !ruby/object:Gem::Requirement
@@ -178,6 +184,20 @@ dependencies:
178
184
  - - "~>"
179
185
  - !ruby/object:Gem::Version
180
186
  version: 13.0.6
187
+ - !ruby/object:Gem::Dependency
188
+ name: rbnacl
189
+ requirement: !ruby/object:Gem::Requirement
190
+ requirements:
191
+ - - "~>"
192
+ - !ruby/object:Gem::Version
193
+ version: 7.1.1
194
+ type: :runtime
195
+ prerelease: false
196
+ version_requirements: !ruby/object:Gem::Requirement
197
+ requirements:
198
+ - - "~>"
199
+ - !ruby/object:Gem::Version
200
+ version: 7.1.1
181
201
  - !ruby/object:Gem::Dependency
182
202
  name: sorbet-runtime
183
203
  requirement: !ruby/object:Gem::Requirement
@@ -296,14 +316,14 @@ dependencies:
296
316
  requirements:
297
317
  - - "~>"
298
318
  - !ruby/object:Gem::Version
299
- version: 0.0.1
319
+ version: 0.0.3
300
320
  type: :runtime
301
321
  prerelease: false
302
322
  version_requirements: !ruby/object:Gem::Requirement
303
323
  requirements:
304
324
  - - "~>"
305
325
  - !ruby/object:Gem::Version
306
- version: 0.0.1
326
+ version: 0.0.3
307
327
  - !ruby/object:Gem::Dependency
308
328
  name: source_map
309
329
  requirement: !ruby/object:Gem::Requirement
@@ -318,20 +338,6 @@ dependencies:
318
338
  - - "~>"
319
339
  - !ruby/object:Gem::Version
320
340
  version: 3.0.1
321
- - !ruby/object:Gem::Dependency
322
- name: svg_optimizer
323
- requirement: !ruby/object:Gem::Requirement
324
- requirements:
325
- - - "~>"
326
- - !ruby/object:Gem::Version
327
- version: 0.2.6
328
- type: :runtime
329
- prerelease: false
330
- version_requirements: !ruby/object:Gem::Requirement
331
- requirements:
332
- - - "~>"
333
- - !ruby/object:Gem::Version
334
- version: 0.2.6
335
341
  - !ruby/object:Gem::Dependency
336
342
  name: syntax_tree
337
343
  requirement: !ruby/object:Gem::Requirement
@@ -375,7 +381,7 @@ dependencies:
375
381
  - !ruby/object:Gem::Version
376
382
  version: 0.1.0
377
383
  description: |
378
- Mayu Live is a live streaming server side VirtualDOM framework for Ruby,
384
+ Mayu Live is a live updating server side VirtualDOM framework for Ruby,
379
385
  inspired by modern frontend tools that exist in the JavaScript ecosystem.
380
386
  email:
381
387
  - andreas.alin@gmail.com
@@ -450,12 +456,12 @@ files:
450
456
  - lib/mayu/component/wrapper.rb
451
457
  - lib/mayu/configuration.rb
452
458
  - lib/mayu/disable_sorbet.rb
459
+ - lib/mayu/encrypted_marshal.rb
453
460
  - lib/mayu/environment.rb
454
461
  - lib/mayu/event_stream.rb
455
462
  - lib/mayu/fetch.rb
456
463
  - lib/mayu/html.rb
457
464
  - lib/mayu/html.yaml
458
- - lib/mayu/message_cipher.rb
459
465
  - lib/mayu/metrics.rb
460
466
  - lib/mayu/metrics/collector.rb
461
467
  - lib/mayu/metrics/exporter.rb
@@ -1,172 +0,0 @@
1
- # typed: strict
2
- # frozen_string_literal: true
3
-
4
- require "sorbet-runtime"
5
- require "time"
6
- require "digest/sha2"
7
- require "openssl"
8
- require "securerandom"
9
- require "brotli"
10
-
11
- module Mayu
12
- class MessageCipher
13
- extend T::Sig
14
-
15
- DEFAULT_TTL_SECONDS = T.let(10, Integer)
16
-
17
- Message = T.type_alias { { iss: Float, exp: Float, payload: T.untyped } }
18
-
19
- class Error < StandardError
20
- end
21
- class ExpiredError < Error
22
- end
23
- class IssuedInTheFutureError < Error
24
- end
25
- class EncryptError < Error
26
- end
27
- class DecryptError < Error
28
- end
29
- class InvalidHMACError < Error
30
- end
31
-
32
- sig { params(key: String, ttl: Integer).void }
33
- def initialize(key:, ttl: DEFAULT_TTL_SECONDS)
34
- raise ArgumentError, "ttl must be positive" unless ttl.positive?
35
- @default_ttl_seconds = ttl
36
- @key = T.let(Digest::SHA256.digest(key), String)
37
- end
38
-
39
- sig do
40
- params(payload: T.untyped, auth_data: String, ttl: Integer).returns(
41
- String
42
- )
43
- end
44
- def dump(payload, auth_data: "", ttl: @default_ttl_seconds)
45
- raise ArgumentError, "ttl must be positive" unless ttl.positive?
46
- now = Time.now.to_f
47
- message = { iss: now, exp: now + ttl, payload: Marshal.dump(payload) }
48
- encode_message(message, auth_data:)
49
- end
50
-
51
- sig { params(data: String, auth_data: String).returns(T.untyped) }
52
- def load(data, auth_data: "")
53
- Marshal.load(decode_message(data, auth_data:))
54
- end
55
-
56
- private
57
-
58
- sig { params(message: Message, auth_data: String).returns(String) }
59
- def encode_message(message, auth_data: "")
60
- message
61
- .then { Marshal.dump(_1) }
62
- .then { prepend_hmac(_1) }
63
- .then { Brotli.deflate(_1) }
64
- .then { encrypt(_1, auth_data:) }
65
- end
66
-
67
- sig { params(message: String, auth_data: String).returns(String) }
68
- def decode_message(message, auth_data: "")
69
- message
70
- .then { decrypt(_1, auth_data:) }
71
- .then { Brotli.inflate(_1) }
72
- .then { validate_hmac(_1) }
73
- .then { Marshal.load(_1) }
74
- .tap { validate_times(_1) }
75
- .fetch(:payload)
76
- end
77
-
78
- sig { params(input: String).returns(String) }
79
- def prepend_hmac(input)
80
- hmac = Digest::SHA256.digest(input)
81
- input.prepend(hmac)
82
- end
83
-
84
- sig { params(input: String).returns(String) }
85
- def validate_hmac(input)
86
- hmac, message = input.unpack("a32 a*")
87
-
88
- unless OpenSSL.fixed_length_secure_compare(
89
- hmac,
90
- Digest::SHA256.digest(message.to_s)
91
- )
92
- raise InvalidHMACError
93
- end
94
-
95
- message.to_s
96
- end
97
-
98
- sig { params(message: { iss: Float, exp: Float, payload: String }).void }
99
- def validate_times(message)
100
- message => { iss:, exp: }
101
- now = Time.now.to_f
102
- validate_iss(now, iss)
103
- validate_exp(now, exp)
104
- end
105
-
106
- sig { params(now: Float, iss: Float).void }
107
- def validate_iss(now, iss)
108
- return if iss < now
109
-
110
- raise IssuedInTheFutureError,
111
- "The message was issued at #{Time.at(iss).iso8601}, which is in the future"
112
- end
113
-
114
- sig { params(now: Float, exp: Float).void }
115
- def validate_exp(now, exp)
116
- return if exp > now
117
-
118
- raise ExpiredError,
119
- "The message expired at #{Time.at(exp).iso8601}, which is in the past"
120
- end
121
-
122
- sig { params(message: String, auth_data: String).returns(String) }
123
- def encrypt(message, auth_data: "")
124
- cipher = OpenSSL::Cipher.new("aes-256-gcm")
125
- cipher.encrypt
126
- salt = SecureRandom.random_bytes(8)
127
- cipher.key = generate_key(salt)
128
- cipher.iv = iv = cipher.random_iv
129
- cipher.auth_data = auth_data
130
- cipher_text = cipher.update(message) + cipher.final
131
- auth_tag = cipher.auth_tag
132
- [auth_tag.bytesize, auth_tag, salt, iv, cipher_text].pack("C a* a* a* a*")
133
- rescue OpenSSL::Cipher::CipherError
134
- raise EncryptError
135
- end
136
-
137
- sig { params(data: String, auth_data: String).returns(String) }
138
- def decrypt(data, auth_data: "")
139
- data.unpack("C a*") => [Integer => auth_tag_len, String => data]
140
-
141
- data.unpack("a#{auth_tag_len} a8 a12 a*") => [
142
- auth_tag,
143
- salt,
144
- iv,
145
- cipher_text
146
- ]
147
-
148
- cipher = OpenSSL::Cipher.new("aes-256-gcm")
149
- cipher.iv = iv
150
- cipher.key = generate_key(salt)
151
- cipher.auth_data = auth_data
152
- cipher.auth_tag = auth_tag
153
- cipher.update(cipher_text) + cipher.final
154
- rescue NoMatchingPatternError
155
- raise DecryptError
156
- rescue OpenSSL::Cipher::CipherError
157
- raise DecryptError
158
- end
159
-
160
- sig { params(salt: String).returns(String) }
161
- def generate_key(salt)
162
- OpenSSL::KDF.scrypt(
163
- @key,
164
- salt:, # Salt.
165
- N: 2**14, # CPU/memory cost parameter. This must be a power of 2.
166
- r: 8, # Block size parameter.
167
- p: 1, # Parallelization parameter
168
- length: 32 # Length in octets of the derived key
169
- )
170
- end
171
- end
172
- end