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.
- checksums.yaml +4 -4
- data/README.md +71 -0
- data/docs/validation.md +168 -0
- data/lib/melos/capabilities.rb +15 -0
- data/lib/melos/client.rb +85 -0
- data/lib/melos/crypto.rb +84 -15
- data/lib/melos/epoch_secrets.rb +113 -0
- data/lib/melos/group/validation.rb +237 -0
- data/lib/melos/group.rb +594 -0
- data/lib/melos/key_package_bundle.rb +59 -0
- data/lib/melos/private_tree.rb +51 -0
- data/lib/melos/ratchet_tree.rb +407 -0
- data/lib/melos/secret_tree.rb +72 -89
- data/lib/melos/struct/base.rb +69 -65
- data/lib/melos/struct/structs.rb +163 -87
- data/lib/melos/tree.rb +4 -120
- data/lib/melos/vec.rb +29 -41
- data/lib/melos/version.rb +1 -1
- data/lib/melos.rb +9 -2
- metadata +14 -8
- data/.rspec +0 -3
- data/lib/melos/key_schedule.rb +0 -62
- data/lib/melos/struct/ratchet_tree.rb +0 -324
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
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/
|
|
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/
|
|
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
|
|
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:
|
|
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
|
|
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
|
|
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/
|
|
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:
|
|
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
data/lib/melos/key_schedule.rb
DELETED
|
@@ -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
|