mayu-live 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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