melos 0.0.1 → 0.1.0

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.
data/lib/melos/tree.rb CHANGED
@@ -1,83 +1,7 @@
1
+ # Tree math over node indexes in the array representation of a left-balanced binary tree (RFC 9420 Appendix C).
2
+ # Functions that depend on the tree's contents live on Melos::RatchetTree.
1
3
  class Melos::Tree
2
- attr_accessor :array, :leaf_count
3
- # attr_reader @array
4
-
5
- def initialize
6
- @array = []
7
- @leaf_count = 0
8
- end
9
-
10
- def self.empty_tree(n_leaves)
11
- instance = self.allocate
12
- instance.leaf_count = n_leaves
13
- instance.array = Array.new(node_width(n_leaves))
14
- instance
15
- end
16
-
17
- def add(val)
18
- raise ArgumentError.new('Cannot add nil element') if val.nil?
19
- if @leaf_count == 0
20
- # initialize tree with one node
21
- @array = [val]
22
- @leaf_count = 1
23
- else
24
- # find leftmost empty leaf
25
- extend_tree = true
26
- for k in 0 .. (@leaf_count - 1) do
27
- if @array[k * 2].nil?
28
- @array[k * 2] = val
29
- extend_tree = false
30
- break
31
- end
32
- end
33
- if extend_tree
34
- # if tree is full, extend
35
- @leaf_count = @leaf_count * 2
36
- for k in 0 .. @leaf_count - 2
37
- @array[@leaf_count + k] = nil
38
- end
39
- @array[@leaf_count] = val
40
- end
41
- end
42
- @array
43
- end
44
-
45
- def remove_leaf(leaf_idx)
46
- raise ArgumentError.new('Cannot remove from empty tree') if @leaf_count == 0
47
- remove_node(leaf_idx * 2)
48
- # then if rigbt half of the tree is empty, truncate tree
49
- # q: do we recursively shrink tree?
50
- right_tree_empty = true
51
- for i in 0 .. (@leaf_count / 2) - 1 do
52
- if !@array[@leaf_count + 2 * i].nil?
53
- right_tree_empty = false
54
- break
55
- end
56
- end
57
- if right_tree_empty
58
- @array = @array.first(@leaf_count - 1)
59
- @leaf_count = @leaf_count / 2
60
- end
61
- @array
62
- end
63
-
64
- def remove_node(node_idx)
65
- @array[node_idx] = nil
66
- end
67
-
68
- def root
69
- self.class.root(@leaf_count)
70
- end
71
-
72
- def leaf_at(leaf_index)
73
- @array[leaf_index * 2]
74
- end
75
-
76
4
  class << self
77
- def n_leaves(tree)
78
- (tree.size + 1) / 2
79
- end
80
-
81
5
  def log2(x)
82
6
  if x == 0
83
7
  return 0
@@ -184,7 +108,7 @@ class Melos::Tree
184
108
  raise ArgumentError.new('Failed to find common ancestor')
185
109
  end
186
110
 
187
- dxy.min { level(_1) }
111
+ dxy.min_by { level(_1) }
188
112
  end
189
113
 
190
114
  def overlap_with_filtered_direct_path(x, filtered_direct_path, n)
@@ -195,53 +119,13 @@ class Melos::Tree
195
119
  raise ArgumentError.new('Failed to find overlap')
196
120
  end
197
121
 
198
- dxf.min { level(_1) }
122
+ dxf.min_by { level(_1) }
199
123
  end
200
124
 
201
125
  def leaf?(node_index)
202
126
  node_index % 2 == 0
203
127
  end
204
128
 
205
- def truncate!(tree)
206
- root = root(n_leaves(tree))
207
- if tree[(root + 1)..]&.all?(nil)
208
- tree.slice!(root..) # keep left half of tree
209
- truncate!(tree) # then attempt to truncate again
210
- end
211
- # right half of tree has an element, so finish
212
- end
213
-
214
- def filtered_direct_path(tree, node_index)
215
- n_l = n_leaves(tree)
216
- direct_path(node_index, n_l).reject { resolution(tree, sibling_from_leaf(node_index, _1, n_l)) == [] }
217
- end
218
-
219
- def copath_nodes_of_filtered_direct_path(tree, node_index)
220
- filtered_direct_path(tree, node_index).map do |a|
221
- sibling_from_leaf(node_index, a, n_leaves(tree))
222
- end
223
- end
224
-
225
- def resolution(tree, node_index)
226
- node = tree[node_index]
227
- if node.nil?
228
- if Melos::Tree.leaf?(node_index)
229
- # The resolution of a blank leaf node is the empty list.
230
- []
231
- else
232
- # The resolution of a blank intermediate node is the result of concatenating the resolution of its left child with the resolution of its right child, in that order.
233
- resolution(tree, Melos::Tree.left(node_index)) + resolution(tree, Melos::Tree.right(node_index))
234
- end
235
- else
236
- # The resolution of a non-blank node comprises the node itself, followed by its list of unmerged leaves, if any.
237
- if node.parent_node
238
- [node_index] + node.parent_node.unmerged_leaves.map { _1 * 2} # convert leaf index to node index
239
- else
240
- [node_index]
241
- end
242
- end
243
- end
244
-
245
129
  # def common_ancestor_direct(x, y)
246
130
  # lx = level(x) + 1
247
131
  # ly = level(y) + 1
data/lib/melos/vec.rb CHANGED
@@ -2,6 +2,7 @@ module Melos::Vec
2
2
  extend self
3
3
 
4
4
  def read_varint(data)
5
+ raise ArgumentError.new('variable-length vector is truncated') if data.nil? || data.empty?
5
6
  byte = 0
6
7
  v = data[byte].ord
7
8
  prefix = v >> 6
@@ -10,6 +11,7 @@ module Melos::Vec
10
11
  v = v & 0x3f
11
12
  (length - 1).times do
12
13
  byte += 1
14
+ raise ArgumentError.new('variable-length vector is truncated') if data[byte].nil?
13
15
  v = (v << 8) + data[byte].ord
14
16
  end
15
17
 
@@ -36,54 +38,40 @@ module Melos::Vec
36
38
  header.pack('C*')
37
39
  end
38
40
 
39
- def from_string(str) # = to_vec
41
+ # Adds variable length header to string, thus making it a variable length vector
42
+ def string_to_vec(str)
40
43
  write_varint(str.bytesize) + str
41
44
  end
42
45
 
43
- def get_prefix_and_length(str)
44
- prefix = str[0].ord >> 6
45
- length = read_varint(str)
46
-
47
- [prefix, length]
46
+ # sig { params(stream: StringIO).returns(String) }
47
+ def parse_stream(stream)
48
+ prefix = prefix_from_stream(stream)
49
+ raise ArgumentError.new('invalid header') unless (0..2).include?(prefix) # 2.1.2: prefix 3 is invalid
50
+ header_length = 2 ** prefix
51
+ length = length_from_stream(stream)
52
+ # 2.1.2: the length MUST be encoded in the minimum number of bytes; a longer encoding is rejected
53
+ raise ArgumentError.new('variable-length header is not minimal') unless write_varint(length).bytesize == header_length
54
+ stream.pos += header_length
55
+ str = stream.read(length) || ''
56
+ raise ArgumentError.new('variable-length vector is truncated') if str.bytesize < length
57
+ str
48
58
  end
49
59
 
50
- def parse_vec(vec_as_string)
51
- prefix = vec_as_string[0].ord >> 6
52
- length = read_varint(vec_as_string)
53
- case prefix
54
- when 0
55
- str = vec_as_string.byteslice(1, length)
56
- rest = vec_as_string.byteslice((1 + length)..)
57
- when 1
58
- str = vec_as_string.byteslice(2, length)
59
- rest = vec_as_string.byteslice((2 + length)..)
60
- when 2
61
- str = vec_as_string[4, length]
62
- rest = vec_as_string[(4 + length)..]
63
- else
64
- raise ArgumentError.new('invalid header')
65
- end
60
+ private
66
61
 
67
- [str, rest]
62
+ def prefix_from_stream(stringio)
63
+ pos = stringio.pos
64
+ first = stringio.getbyte
65
+ raise ArgumentError.new('variable-length vector is truncated') if first.nil?
66
+ prefix_ = first >> 6
67
+ stringio.seek(pos)
68
+ prefix_
68
69
  end
69
70
 
70
- def get_first_vec(vec_as_string)
71
- prefix = vec[0].ord >> 6
72
- length = read_varint(vec_as_string)
73
- case prefix
74
- when 0
75
- first_vec = vec_as_string[0, 1 + length]
76
- rest = vec_as_string[(1 + length)..]
77
- when 1
78
- first_vec = vec_as_string[0, 2 + length]
79
- rest = vec_as_string[(2 + length)..]
80
- when 2
81
- first_vec = vec_as_string[0, 4 + length]
82
- rest = vec_as_string[(4 + length)..]
83
- else
84
- raise ArgumentError.new('invalid header')
85
- end
86
-
87
- [first_vec, rest]
71
+ def length_from_stream(stringio)
72
+ pos = stringio.pos
73
+ buf = stringio.read(4)
74
+ stringio.seek(pos)
75
+ read_varint(buf)
88
76
  end
89
77
  end
data/lib/melos/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Melos
4
- VERSION = "0.0.1"
4
+ VERSION = "0.1.0"
5
5
  end
data/lib/melos.rb CHANGED
@@ -1,3 +1,5 @@
1
+ require 'stringio'
2
+
1
3
  module Melos
2
4
  MLS_VERSION = 0x01 # mls10
3
5
  end
@@ -7,9 +9,14 @@ require_relative 'melos/vec'
7
9
  require_relative 'melos/tree'
8
10
  require_relative 'melos/util'
9
11
  require_relative 'melos/constants'
10
- require_relative 'melos/key_schedule'
12
+ require_relative 'melos/epoch_secrets'
11
13
  require_relative 'melos/psk'
14
+ require_relative 'melos/group'
12
15
  require_relative 'melos/secret_tree'
16
+ require_relative 'melos/capabilities'
17
+ require_relative 'melos/key_package_bundle'
18
+ require_relative 'melos/client'
13
19
  require_relative 'melos/struct/base'
14
20
  require_relative 'melos/struct/structs'
15
- require_relative 'melos/struct/ratchet_tree'
21
+ require_relative 'melos/ratchet_tree'
22
+ require_relative 'melos/private_tree'
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: melos
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryo Kajiwara
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-04-13 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: hpke
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 0.3.1
18
+ version: '1.0'
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: 0.3.1
25
+ version: '1.0'
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: minitest
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -50,18 +50,24 @@ executables: []
50
50
  extensions: []
51
51
  extra_rdoc_files: []
52
52
  files:
53
- - ".rspec"
54
53
  - LICENSE.txt
55
54
  - README.md
56
55
  - Rakefile
56
+ - docs/validation.md
57
57
  - lib/melos.rb
58
+ - lib/melos/capabilities.rb
59
+ - lib/melos/client.rb
58
60
  - lib/melos/constants.rb
59
61
  - lib/melos/crypto.rb
60
- - lib/melos/key_schedule.rb
62
+ - lib/melos/epoch_secrets.rb
63
+ - lib/melos/group.rb
64
+ - lib/melos/group/validation.rb
65
+ - lib/melos/key_package_bundle.rb
66
+ - lib/melos/private_tree.rb
61
67
  - lib/melos/psk.rb
68
+ - lib/melos/ratchet_tree.rb
62
69
  - lib/melos/secret_tree.rb
63
70
  - lib/melos/struct/base.rb
64
- - lib/melos/struct/ratchet_tree.rb
65
71
  - lib/melos/struct/structs.rb
66
72
  - lib/melos/tree.rb
67
73
  - lib/melos/util.rb
@@ -104,7 +110,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
104
110
  - !ruby/object:Gem::Version
105
111
  version: '0'
106
112
  requirements: []
107
- rubygems_version: 3.6.5
113
+ rubygems_version: 4.0.20
108
114
  specification_version: 4
109
115
  summary: Messaging Layer Security Protocol (RFC 9420) on Ruby
110
116
  test_files: []
data/.rspec DELETED
@@ -1,3 +0,0 @@
1
- --format documentation
2
- --color
3
- --require spec_helper
@@ -1,62 +0,0 @@
1
- require_relative 'crypto'
2
-
3
- module Melos::KeySchedule
4
- extend self
5
-
6
- def joiner_secret(suite, init_secret, commit_secret, group_context)
7
- Melos::Crypto.expand_with_label(
8
- suite,
9
- Melos::Crypto.kdf_extract(suite, init_secret, commit_secret),
10
- "joiner",
11
- group_context.raw,
12
- suite.kdf.n_h
13
- )
14
- end
15
-
16
- def welcome_secret(suite, joiner_secret, psk_secret)
17
- Melos::Crypto.derive_secret(
18
- suite,
19
- Melos::Crypto.kdf_extract(suite, joiner_secret, psk_secret), # sometimes written as `member_secret`
20
- "welcome"
21
- )
22
- end
23
-
24
- def welcome_key_and_nonce(suite, joiner_secret, psk_secret)
25
- ws = welcome_secret(suite, joiner_secret, psk_secret)
26
- [
27
- Melos::Crypto.expand_with_label(suite, ws, "key", "", suite.hpke.n_k),
28
- Melos::Crypto.expand_with_label(suite, ws, "nonce", "", suite.hpke.n_n)
29
- ]
30
- end
31
-
32
- def epoch_secret(suite, joiner_secret, psk_secret, group_context)
33
- Melos::Crypto.expand_with_label(
34
- suite,
35
- Melos::Crypto.kdf_extract(suite, joiner_secret, psk_secret),
36
- "epoch",
37
- group_context.raw,
38
- suite.kdf.n_h
39
- )
40
- end
41
-
42
- # epoch-derived secrets, from Table 4 in Section 8
43
- # will be defined as something like:
44
- # Melos::KeySchedule.sender_data_secret(suite, epoch_secret)
45
- [
46
- ['sender data', 'sender_data_secret'],
47
- ['encryption', 'encryption_secret'],
48
- ['exporter', 'exporter_secret'],
49
- ['external', 'external_secret'],
50
- ['confirm', 'confirmation_key'],
51
- ['membership', 'membership_key'],
52
- ['resumption', 'resumption_psk'],
53
- ['authentication', 'epoch_authenticator'],
54
- ['init', 'init_secret'] # this is not part of the table but is defined in the same way
55
- ].each do |tuple|
56
- label = tuple[0]
57
- name = tuple[1]
58
- define_method(name.to_sym, ->(suite, epoch_secret){
59
- Melos::Crypto.derive_secret(suite, epoch_secret, label)
60
- })
61
- end
62
- end