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.
@@ -1,73 +1,75 @@
1
1
  module Melos::Struct; end
2
2
 
3
3
  class Melos::Struct::Base
4
- def initialize(buf)
5
- context, _ = deserialize(buf)
4
+ # external: values this struct's select fields depend on but which belong to an
5
+ # enclosing struct (e.g. FramedContent.content_type for FramedContentAuthData).
6
+ # They are seeded into the parse context and set as instance variables, but are
7
+ # not part of STRUCT and so are never serialized.
8
+ def initialize(stream, **external)
9
+ stream = StringIO.new(stream) if stream.is_a?(String)
10
+ context = deserialize(stream, external)
6
11
  set_instance_vars(context)
7
12
  self
8
13
  end
9
14
 
10
- def self.new_and_rest(buf)
11
- instance = self.allocate
12
- context, buf = instance.send(:deserialize, buf)
13
- instance.send(:set_instance_vars, context)
14
- [instance, buf]
15
+ # build an instance from its field values. Structs with defaults or fields that
16
+ # depend on an enclosing struct define their own create.
17
+ def self.create(**fields)
18
+ unknown = fields.keys - self::STRUCT.map(&:first)
19
+ raise ArgumentError.new("#{name}: unknown fields #{unknown.join(', ')}") unless unknown.empty?
20
+ instance = allocate
21
+ fields.each { |field, value| instance.instance_variable_set("@#{field}", value) }
22
+ instance
15
23
  end
16
24
 
17
25
  def raw
18
26
  buf = ''
27
+ context = field_context
19
28
  self.class::STRUCT.each do |elem|
29
+ value = self.instance_variable_get("@#{elem[0]}")
20
30
  case elem[1]
21
31
  when :select
22
- value = self.instance_variable_get("@#{elem[0]}")
23
- buf += serialize_select_elem(value, elem[3])
32
+ buf += serialize_select_elem(elem[0], value, elem[2], elem[3], context)
24
33
  else
25
- value = self.instance_variable_get("@#{elem[0]}")
26
34
  buf += serialize_elem(value, elem[1], elem[2])
27
35
  end
28
36
  end
29
37
  buf
30
38
  end
31
39
 
32
- def self.vecs(buf)
33
- value, buf = Melos::Vec.parse_vec(buf)
34
- array = []
35
- while (value.bytesize > 0)
36
- current_instance, value = Melos::Vec.parse_vec(value)
37
- array << current_instance
38
- end
39
- [array, buf]
40
- end
41
-
42
40
  # context here takes a hash
43
- # returns [value, rest_of_buffer]
41
+ # returns the deserialized value
44
42
  # value could return nil, which means predicate was not applicable
45
43
  # predicate takes the context and returns true or false
46
- def deserialize_select_elem_with_context(buf, context, predicate, type, type_param)
44
+ def deserialize_select_elem_with_context(stream, context, predicate, type, type_param)
47
45
  if predicate.(context)
48
- deserialize_elem(buf, type, type_param)
46
+ deserialize_elem(stream, type, type_param)
49
47
  else
50
- [nil, buf]
48
+ nil
51
49
  end
52
50
  end
53
51
 
54
52
  private
55
- def deserialize(buf)
56
- context = []
53
+ # external is a Hash of values seeded into the context before parsing (see #initialize)
54
+ def deserialize(stream, external = {})
55
+ context = external.to_a
57
56
  self.class::STRUCT.each do |elem|
58
57
  case elem[1]
59
58
  when :select
60
- value, buf = deserialize_select_elem_with_context(buf, context.to_h, elem[2], elem[3], elem[4])
59
+ value = deserialize_select_elem_with_context(stream, context.to_h, elem[2], elem[3], elem[4])
61
60
  context << [elem[0], value]
62
- when :framed_content_auth_data
63
- value, buf = Melos::Struct::FramedContentAuthData.new_and_rest_with_content_type(buf, context.to_h[:content].content_type)
61
+ when :class
62
+ # [:name, :class, Klass, ->(ctx){ {external_name: value} }]
63
+ # the optional 4th element derives external context for the child struct from this struct's context
64
+ external_for_child = elem[3] ? elem[3].(context.to_h) : {}
65
+ value = elem[2].new(stream, **external_for_child)
64
66
  context << [elem[0], value]
65
67
  else
66
- value, buf = deserialize_elem(buf, elem[1], elem[2])
68
+ value = deserialize_elem(stream, elem[1], elem[2])
67
69
  context << [elem[0], value]
68
70
  end
69
71
  end
70
- [context, buf]
72
+ context
71
73
  end
72
74
 
73
75
  def set_instance_vars(context)
@@ -76,57 +78,51 @@ class Melos::Struct::Base
76
78
  end
77
79
  end
78
80
 
79
- def deserialize_elem(buf, type, type_param)
81
+ def deserialize_elem(stream, type, type_param)
80
82
  case type
81
83
  when :uint8
82
- value = buf.byteslice(0, 1).unpack1('C')
83
- buf = buf.byteslice(1..)
84
+ value = stream.read(1).unpack1('C')
84
85
  when :uint16
85
- value = buf.byteslice(0, 2).unpack1('S>')
86
- buf = buf.byteslice(2..)
86
+ value = stream.read(2).unpack1('S>')
87
87
  when :uint32
88
- value = buf.byteslice(0, 4).unpack1('L>')
89
- buf = buf.byteslice(4..)
88
+ value = stream.read(4).unpack1('L>')
90
89
  when :uint64
91
- value = buf.byteslice(0, 8).unpack1('Q>')
92
- buf = buf.byteslice(8..)
90
+ value = stream.read(8).unpack1('Q>')
93
91
  when :vec
94
- value, buf = Melos::Vec.parse_vec(buf)
92
+ value = Melos::Vec.parse_stream(stream)
95
93
  when :vec_of_type
96
- vec, buf = Melos::Vec.parse_vec(buf)
94
+ data = Melos::Vec.parse_stream(stream)
97
95
  value = []
98
- while (vec.bytesize > 0)
99
- current_instance, vec = deserialize_elem(vec, type_param, nil)
96
+ data_stream = StringIO.new(data)
97
+ while (!data_stream.eof?)
98
+ current_instance = deserialize_elem(data_stream, type_param, nil)
100
99
  value << current_instance
101
100
  end
102
101
  when :class
103
- value, buf = type_param.send(:new_and_rest, buf)
102
+ value = type_param.send(:new, stream)
104
103
  when :classes
105
- # prefix, length = buf.get_prefix_and_length
106
- # puts "#{prefix}, #{length}"
107
- vec, buf = Melos::Vec.parse_vec(buf)
104
+ data = Melos::Vec.parse_stream(stream)
108
105
  value = []
109
- while (vec.bytesize > 0)
110
- current_instance, vec = type_param.send(:new_and_rest, vec)
106
+ data_stream = StringIO.new(data)
107
+ while (!data_stream.eof?)
108
+ current_instance = type_param.send(:new, data_stream)
111
109
  value << current_instance
112
110
  end
113
111
  when :optional
114
- presence = buf.byteslice(0, 1).unpack1('C')
115
- buf = buf.byteslice(1..)
112
+ presence = stream.read(1).unpack1('C')
116
113
  case presence
117
114
  when 0
118
115
  value = nil
119
116
  when 1
120
117
  # as of RFC 9420, optional always takes a class
121
- value, buf = type_param.send(:new_and_rest, buf)
118
+ value = type_param.send(:new, stream)
122
119
  end
123
120
  when :opaque
124
- value = buf.byteslice(0, type_param.to_i)
125
- buf = buf.byteslice((type_param.to_i)..)
121
+ value = stream.read(type_param.to_i)
126
122
  when :padding
127
- value = buf
123
+ value = stream.read
128
124
  end
129
- [value, buf]
125
+ value
130
126
  end
131
127
 
132
128
  # take a name and type
@@ -141,13 +137,13 @@ class Melos::Struct::Base
141
137
  when :uint64
142
138
  [value].pack('Q>')
143
139
  when :vec
144
- Melos::Vec.from_string(value)
140
+ Melos::Vec.string_to_vec(value)
145
141
  when :vec_of_type
146
- Melos::Vec.from_string(value.map { serialize_elem(_1, type_param, nil) }.join)
147
- when :class, :framed_content_auth_data
142
+ Melos::Vec.string_to_vec(value.map { serialize_elem(_1, type_param, nil) }.join)
143
+ when :class
148
144
  value.raw
149
145
  when :classes
150
- Melos::Vec.from_string(value.map(&:raw).join)
146
+ Melos::Vec.string_to_vec(value.map(&:raw).join)
151
147
  when :optional
152
148
  if value.nil?
153
149
  [0].pack('C')
@@ -162,11 +158,19 @@ class Melos::Struct::Base
162
158
  end
163
159
  end
164
160
 
165
- def serialize_select_elem(value, type)
166
- if value.nil?
167
- ''
168
- else
161
+ # every instance variable as a Hash, the same shape as the parse context,
162
+ # so that select predicates can be evaluated at serialization time
163
+ def field_context
164
+ instance_variables.to_h { [_1.to_s.delete_prefix('@').to_sym, instance_variable_get(_1)] }
165
+ end
166
+
167
+ # a select field is serialized only when its predicate holds; it must then be present
168
+ def serialize_select_elem(name, value, predicate, type, context)
169
+ if predicate.(context)
170
+ raise ArgumentError.new("#{self.class.name}##{name} is required by its select predicate but is nil") if value.nil?
169
171
  serialize_elem(value, type, nil)
172
+ else
173
+ ''
170
174
  end
171
175
  end
172
176
  end
@@ -33,6 +33,13 @@ class Melos::Struct::Credential < Melos::Struct::Base
33
33
  [:identity, :select, ->(ctx){ctx[:credential_type] == Melos::Constants::CredentialType::BASIC}, :vec],
34
34
  [:certificates, :select, ->(ctx){ctx[:credential_type] == Melos::Constants::CredentialType::X509}, :classes, Melos::Struct::Certificate]
35
35
  ]
36
+
37
+ def self.create_basic_credential(identity)
38
+ new_instance = self.allocate
39
+ new_instance.instance_variable_set(:@credential_type, Melos::Constants::CredentialType::BASIC)
40
+ new_instance.instance_variable_set(:@identity, identity)
41
+ new_instance
42
+ end
36
43
  end
37
44
 
38
45
  ## 6.3.2
@@ -84,6 +91,26 @@ class Melos::Struct::Capabilities < Melos::Struct::Base
84
91
  [:proposals, :vec], # vec of ProposalTypes (uint16)
85
92
  [:credentials, :vec] # vec of CredentialTypes (uint16)
86
93
  ]
94
+
95
+ def self.import(capabilities)
96
+ new_instance = self.allocate
97
+ new_instance.instance_variable_set(:@versions, capabilities.versions.pack('S>*'))
98
+ new_instance.instance_variable_set(:@cipher_suites, capabilities.cipher_suites.pack('S>*'))
99
+ new_instance.instance_variable_set(:@extensions, capabilities.extensions.pack('S>*'))
100
+ new_instance.instance_variable_set(:@proposals, capabilities.proposals.pack('S>*'))
101
+ new_instance.instance_variable_set(:@credentials, capabilities.credentials.pack('S>*'))
102
+ new_instance
103
+ end
104
+
105
+ def export
106
+ cap = Melos::Capabilities.new
107
+ cap.versions = versions.unpack('S>*')
108
+ cap.cipher_suites = cipher_suites.unpack('S>*')
109
+ cap.extensions = extensions.unpack('S>*')
110
+ cap.proposals = proposals.unpack('S>*')
111
+ cap.credentials = credentials.unpack('S>*')
112
+ cap
113
+ end
87
114
  end
88
115
 
89
116
  class Melos::Struct::Lifetime < Melos::Struct::Base
@@ -92,6 +119,13 @@ class Melos::Struct::Lifetime < Melos::Struct::Base
92
119
  [:not_before, :uint64],
93
120
  [:not_after, :uint64]
94
121
  ]
122
+
123
+ def self.create(not_before:, not_after:)
124
+ new_instance = self.allocate
125
+ new_instance.instance_variable_set(:@not_before, not_before)
126
+ new_instance.instance_variable_set(:@not_after, not_after)
127
+ new_instance
128
+ end
95
129
  end
96
130
 
97
131
  class Melos::Struct::Extension < Melos::Struct::Base
@@ -100,6 +134,13 @@ class Melos::Struct::Extension < Melos::Struct::Base
100
134
  [:extension_type, :uint16], # ExtensionType = uint16
101
135
  [:extension_data, :vec]
102
136
  ]
137
+
138
+ # 13.4: any list of extensions MUST NOT have more than one extension of a given type
139
+ def self.check_unique_types!(extensions, where)
140
+ duplicates = extensions.map(&:extension_type).tally.select { |_type, count| count > 1 }.keys
141
+ return if duplicates.empty?
142
+ raise ArgumentError.new("#{where} has more than one extension of type #{duplicates.map { '0x%04x' % _1 }.join(', ')}")
143
+ end
103
144
  end
104
145
 
105
146
  class Melos::Struct::LeafNode < Melos::Struct::Base
@@ -118,19 +159,19 @@ class Melos::Struct::LeafNode < Melos::Struct::Base
118
159
 
119
160
  def leaf_node_tbs(group_id, leaf_index)
120
161
  buf = ''
121
- buf += Melos::Vec.from_string(encryption_key)
122
- buf += Melos::Vec.from_string(signature_key)
162
+ buf += Melos::Vec.string_to_vec(encryption_key)
163
+ buf += Melos::Vec.string_to_vec(signature_key)
123
164
  buf += credential.raw
124
165
  buf += capabilities.raw
125
166
  buf += [leaf_node_source].pack('C')
126
167
  if leaf_node_source == Melos::Constants::LeafNodeSource::KEY_PACKAGE
127
168
  buf += lifetime.raw
128
169
  elsif leaf_node_source == Melos::Constants::LeafNodeSource::COMMIT
129
- buf += Melos::Vec.from_string(parent_hash)
170
+ buf += Melos::Vec.string_to_vec(parent_hash)
130
171
  end
131
- buf += Melos::Vec.from_string(extensions.map(&:raw).join)
172
+ buf += Melos::Vec.string_to_vec(extensions.map(&:raw).join)
132
173
  if leaf_node_source == Melos::Constants::LeafNodeSource::UPDATE || leaf_node_source == Melos::Constants::LeafNodeSource::COMMIT
133
- buf += Melos::Vec.from_string(group_id)
174
+ buf += Melos::Vec.string_to_vec(group_id)
134
175
  buf += [leaf_index].pack('L>') # uint32
135
176
  end
136
177
  buf
@@ -344,6 +385,31 @@ class Melos::Struct::KeyPackage < Melos::Struct::Base
344
385
  def ref(suite)
345
386
  Melos::Crypto.make_keypackage_ref(suite, self.raw)
346
387
  end
388
+
389
+ def self.create(cipher_suite:, init_key:, leaf_node:)
390
+ new_instance = self.allocate
391
+ new_instance.instance_variable_set(:@version, 1)
392
+ new_instance.instance_variable_set(:@cipher_suite, cipher_suite)
393
+ new_instance.instance_variable_set(:@init_key, init_key)
394
+ new_instance.instance_variable_set(:@leaf_node, leaf_node)
395
+ new_instance.instance_variable_set(:@extensions, leaf_node.extensions)
396
+ new_instance.instance_variable_set(:@signature, '')
397
+ new_instance
398
+ end
399
+
400
+ def keypackage_tbs
401
+ [version].pack('S>') + [cipher_suite].pack('S>') + Melos::Vec.string_to_vec(init_key) + leaf_node.raw + Melos::Vec.string_to_vec(extensions.map(&:raw).join)
402
+ end
403
+
404
+ def sign(signature_private_key)
405
+ suite = Melos::Crypto::CipherSuite.new(@cipher_suite)
406
+ @signature = Melos::Crypto.sign_with_label(suite, signature_private_key, "KeyPackageTBS", keypackage_tbs)
407
+ end
408
+
409
+ def verify()
410
+ suite = Melos::Crypto::CipherSuite.new(@cipher_suite)
411
+ Melos::Crypto.verify_with_label(suite, leaf_node.signature_key, "KeyPackageTBS", keypackage_tbs, signature)
412
+ end
347
413
  end
348
414
 
349
415
  class Melos::Struct::KeyPackageTBS < Melos::Struct::Base
@@ -373,7 +439,7 @@ end
373
439
  ## 12.1.1 - 12.1.7
374
440
 
375
441
  class Melos::Struct::Add < Melos::Struct::Base
376
- attr_reader :key_package
442
+ attr_accessor :key_package
377
443
  STRUCT = [
378
444
  [:key_package, :class, Melos::Struct::KeyPackage]
379
445
  ]
@@ -387,7 +453,7 @@ class Melos::Struct::Update < Melos::Struct::Base
387
453
  end
388
454
 
389
455
  class Melos::Struct::Remove < Melos::Struct::Base
390
- attr_reader :removed
456
+ attr_accessor :removed
391
457
  STRUCT = [
392
458
  [:removed, :uint32]
393
459
  ]
@@ -437,7 +503,7 @@ end
437
503
  ## 12.1
438
504
 
439
505
  class Melos::Struct::Proposal < Melos::Struct::Base
440
- attr_reader :proposal_type, :add, :update, :remove, :psk, :reinit, :external_init, :group_context_extensions
506
+ attr_accessor :proposal_type, :add, :update, :remove, :psk, :reinit, :external_init, :group_context_extensions
441
507
  STRUCT = [
442
508
  [:proposal_type, :uint16],
443
509
  [:add, :select, ->(ctx){ctx[:proposal_type] == Melos::Constants::ProposalType::ADD}, :class, Melos::Struct::Add],
@@ -463,6 +529,14 @@ class Melos::Struct::ProposalOrRef < Melos::Struct::Base
463
529
  [:proposal, :select, ->(ctx){ctx[:type] == Melos::Constants::ProposalOrRefType::PROPOSAL}, :class, Melos::Struct::Proposal],
464
530
  [:reference, :select, ->(ctx){ctx[:type] == Melos::Constants::ProposalOrRefType::REFERENCE}, :vec] # ProposalRef is a HashReference, which is a :vec
465
531
  ]
532
+
533
+ def self.create(type:, proposal:, reference:)
534
+ new_instance = self.allocate
535
+ new_instance.instance_variable_set(:@type, type)
536
+ new_instance.instance_variable_set(:@proposal, proposal)
537
+ new_instance.instance_variable_set(:@reference, reference)
538
+ new_instance
539
+ end
466
540
  end
467
541
 
468
542
  class Melos::Struct::Commit < Melos::Struct::Base
@@ -471,6 +545,13 @@ class Melos::Struct::Commit < Melos::Struct::Base
471
545
  [:proposals, :classes, Melos::Struct::ProposalOrRef],
472
546
  [:path, :optional, Melos::Struct::UpdatePath]
473
547
  ]
548
+
549
+ def self.create(proposals:, path:)
550
+ new_instance = self.allocate
551
+ new_instance.instance_variable_set(:@proposals, proposals)
552
+ new_instance.instance_variable_set(:@path, path)
553
+ new_instance
554
+ end
474
555
  end
475
556
 
476
557
  ## 12.4.3
@@ -494,8 +575,8 @@ class Melos::Struct::GroupInfo < Melos::Struct::Base
494
575
  )
495
576
  end
496
577
 
497
- def sign(suite, signer_private)
498
- Melos::Crypto.sign_with_label(suite, signer_private_key, "GroupInfoTBS", group_info_tbs.raw)
578
+ def sign(suite, signer_private_key)
579
+ @signature = Melos::Crypto.sign_with_label(suite, signer_private_key, "GroupInfoTBS", group_info_tbs.raw)
499
580
  end
500
581
 
501
582
  def verify(suite, signer_public_key)
@@ -575,8 +656,9 @@ class Melos::Struct::Node < Melos::Struct::Base
575
656
  [:parent_node, :select, ->(ctx){ctx[:node_type] == Melos::Constants::NodeType::PARENT}, :class, Melos::Struct::ParentNode] # parent
576
657
  ]
577
658
 
659
+ # the parent_hash this node carries: nil for a leaf whose source is key_package or update (7.2)
578
660
  def parent_hash_in_node
579
- @leaf_node&.parent_hash || @parent_node.parent_hash
661
+ @parent_node ? @parent_node.parent_hash : @leaf_node.parent_hash
580
662
  end
581
663
 
582
664
  def public_encryption_key
@@ -630,36 +712,17 @@ class Melos::Struct::Sender < Melos::Struct::Base
630
712
  end
631
713
  end
632
714
 
715
+ # The select in FramedContentAuthData depends on FramedContent.content_type, a field of
716
+ # the enclosing struct, so the parent passes it in as external context:
717
+ # [:auth, :class, Melos::Struct::FramedContentAuthData, ->(ctx){ {content_type: ctx[:content].content_type} }]
633
718
  class Melos::Struct::FramedContentAuthData < Melos::Struct::Base
634
719
  attr_reader :signature, :confirmation_tag, :content_type
635
720
 
636
721
  STRUCT = [
637
- [:signature, :vec]
722
+ [:signature, :vec],
723
+ [:confirmation_tag, :select, ->(ctx){ctx[:content_type] == Melos::Constants::ContentType::COMMIT}, :vec] # MAC is opaque <V>
638
724
  ]
639
725
 
640
- # initialize from stream
641
- def self.new_and_rest_with_content_type(buf, content_type)
642
- instance = self.allocate
643
- context, buf = instance.send(:deserialize, buf)
644
- # custom part based on instance variable
645
- if content_type == Melos::Constants::ContentType::COMMIT # commit
646
- # read MAC(opaque <V>) confirmation_tag
647
- value, buf = Melos::Vec.parse_vec(buf)
648
- context << [:confirmation_tag, value]
649
- end
650
- context << [:content_type, content_type]
651
- instance.send(:set_instance_vars, context)
652
- [instance, buf]
653
- end
654
-
655
- def raw
656
- if @content_type == Melos::Constants::ContentType::COMMIT
657
- Melos::Vec.from_string(@signature) + Melos::Vec.from_string(@confirmation_tag)
658
- else
659
- Melos::Vec.from_string(@signature)
660
- end
661
- end
662
-
663
726
  def self.create(signature:, content_type:, confirmation_tag:)
664
727
  instance = self.allocate
665
728
  instance.instance_variable_set(:@signature, signature)
@@ -669,6 +732,12 @@ class Melos::Struct::FramedContentAuthData < Melos::Struct::Base
669
732
  end
670
733
  instance
671
734
  end
735
+
736
+ # a commit's confirmation tag is computed after signing, once the next epoch's key schedule is known (8.2)
737
+ def confirmation_tag=(tag)
738
+ raise ArgumentError.new('only a commit carries a confirmation tag') unless @content_type == Melos::Constants::ContentType::COMMIT
739
+ @confirmation_tag = tag
740
+ end
672
741
  end
673
742
 
674
743
  class Melos::Struct::FramedContent < Melos::Struct::Base
@@ -719,7 +788,7 @@ class Melos::Struct::AuthenticatedContent < Melos::Struct::Base
719
788
  STRUCT = [
720
789
  [:wire_format, :uint16],
721
790
  [:content, :class, Melos::Struct::FramedContent],
722
- [:auth, :framed_content_auth_data]
791
+ [:auth, :class, Melos::Struct::FramedContentAuthData, ->(ctx){ {content_type: ctx[:content].content_type} }]
723
792
  ]
724
793
 
725
794
  def content_tbm
@@ -750,15 +819,15 @@ class Melos::Struct::AuthenticatedContent < Melos::Struct::Base
750
819
  new_instance
751
820
  end
752
821
 
753
- # populate auth with values
754
- def sign(suite, signature_private_key, group_context)
822
+ # populate auth with the signature (6.1); a commit's confirmation_tag may be given here or set on auth later
823
+ def sign(suite, signature_private_key, group_context, confirmation_tag: nil)
755
824
  raise ArgumentError.new('Application data cannot be sent as a PublicMessage') if wire_format == Melos::Constants::WireFormat::MLS_PUBLIC_MESSAGE && content.content_type == Melos::Constants::ContentType::APPLICATION
756
825
  content_tbs = content.content_tbs(Melos::Constants::Version::MLS10, wire_format, group_context)
757
826
  signature = Melos::Crypto.sign_with_label(suite, signature_private_key, "FramedContentTBS", content_tbs)
758
827
  @auth = Melos::Struct::FramedContentAuthData.create(
759
828
  signature: signature,
760
829
  content_type: content.content_type,
761
- confirmation_tag: nil
830
+ confirmation_tag: confirmation_tag
762
831
  )
763
832
  end
764
833
  end
@@ -769,7 +838,7 @@ class Melos::Struct::PublicMessage < Melos::Struct::Base
769
838
  attr_reader :content, :auth, :membership_tag
770
839
  STRUCT = [
771
840
  [:content, :class, Melos::Struct::FramedContent],
772
- [:auth, :framed_content_auth_data],
841
+ [:auth, :class, Melos::Struct::FramedContentAuthData, ->(ctx){ {content_type: ctx[:content].content_type} }],
773
842
  [:membership_tag, :select, ->(ctx){ctx[:content].sender.sender_type == Melos::Constants::SenderType::MEMBER}, :vec] # MAC is opaque <V>
774
843
  ]
775
844
 
@@ -834,7 +903,7 @@ class Melos::Struct::PrivateMessage < Melos::Struct::Base
834
903
  end
835
904
 
836
905
  def self.sender_data_aad_impl(gid, ep, ct)
837
- Melos::Vec.from_string(gid) + [ep].pack('Q>') + [ct].pack('C')
906
+ Melos::Vec.string_to_vec(gid) + [ep].pack('Q>') + [ct].pack('C')
838
907
  end
839
908
 
840
909
  def private_content_aad
@@ -842,17 +911,21 @@ class Melos::Struct::PrivateMessage < Melos::Struct::Base
842
911
  end
843
912
 
844
913
  def self.private_content_aad_impl(gid, ep, ct, ad)
845
- Melos::Vec.from_string(gid) + [ep].pack('Q>') + [ct].pack('C') + Melos::Vec.from_string(ad)
914
+ Melos::Vec.string_to_vec(gid) + [ep].pack('Q>') + [ct].pack('C') + Melos::Vec.string_to_vec(ad)
846
915
  end
847
916
 
848
917
  def self.protect(authenticated_content, suite, secret_tree, sender_data_secret, padding_size)
849
918
  leaf_index = authenticated_content.content.sender.leaf_index
850
919
  content_type = authenticated_content.content.content_type
851
920
  reuse_guard = SecureRandom.random_bytes(4)
852
- key, nonce, generation = Melos::SecretTree::ratchet_and_get(suite, content_type, secret_tree, leaf_index)
921
+ key, nonce, generation = secret_tree.ratchet_and_get(content_type, leaf_index)
853
922
  new_nonce = apply_nonce_reuse_guard(nonce, reuse_guard)
854
923
 
855
- private_message_content_plain = serialize_private_message_content(authenticated_content.content, authenticated_content.auth, padding_size)
924
+ private_message_content_plain = Melos::Struct::PrivateMessageContent.create(
925
+ framed_content: authenticated_content.content,
926
+ auth: authenticated_content.auth,
927
+ padding_size: padding_size
928
+ ).raw
856
929
  aad = private_content_aad_impl(
857
930
  authenticated_content.content.group_id,
858
931
  authenticated_content.content.epoch,
@@ -885,9 +958,12 @@ class Melos::Struct::PrivateMessage < Melos::Struct::Base
885
958
 
886
959
  def unprotect(suite, secret_tree, sender_data_secret)
887
960
  sender_data = decrypt_sender_data(suite, sender_data_secret)
888
- key, nonce, _ = Melos::SecretTree.ratchet_until_and_get(suite, content_type, secret_tree, sender_data.leaf_index, sender_data.generation)
961
+ key, nonce, _ = secret_tree.ratchet_until_and_get(content_type, sender_data.leaf_index, sender_data.generation)
889
962
  new_nonce = self.class.apply_nonce_reuse_guard(nonce, sender_data.reuse_guard)
890
- pmc, _ = Melos::Struct::PrivateMessageContent.new_and_rest_with_content_type(Melos::Crypto.aead_decrypt(suite, key, new_nonce, private_content_aad, ciphertext), content_type)
963
+ stream = StringIO.new(Melos::Crypto.aead_decrypt(suite, key, new_nonce, private_content_aad, ciphertext))
964
+ pmc = Melos::Struct::PrivateMessageContent.new(stream, content_type: content_type)
965
+ # 6.3.1: the padding MUST be all zero bytes, otherwise the message is malformed
966
+ raise ArgumentError.new('PrivateMessage padding is not all zero') unless pmc.padding_zero?
891
967
 
892
968
  fc = Melos::Struct::FramedContent.create(
893
969
  group_id: group_id,
@@ -921,56 +997,44 @@ class Melos::Struct::PrivateMessage < Melos::Struct::Base
921
997
  nonce_arr.pack('C*')
922
998
  end
923
999
 
924
- def self.serialize_private_message_content(framed_content, framed_content_auth_data, padding_size)
925
- buf = ''
926
- case framed_content.content_type
927
- when Melos::Constants::ContentType::APPLICATION
928
- buf += Melos::Vec.from_string(framed_content.application_data)
929
- when Melos::Constants::ContentType::PROPOSAL
930
- buf += framed_content.proposal.raw
931
- when Melos::Constants::ContentType::COMMIT
932
- buf += framed_content.commit.raw
933
- end
934
- buf += Melos::Vec.from_string(framed_content_auth_data.signature)
935
- if framed_content.content_type == Melos::Constants::ContentType::COMMIT
936
- buf += Melos::Vec.from_string(framed_content_auth_data.confirmation_tag)
937
- end
938
- buf += Melos::Crypto::Util.zero_vector(padding_size)
939
- buf
940
- end
941
1000
  end
942
1001
 
1002
+ # The selects in PrivateMessageContent depend on PrivateMessage.content_type, a field of
1003
+ # the enclosing struct, so the caller passes it in:
1004
+ # Melos::Struct::PrivateMessageContent.new(stream, content_type: content_type)
943
1005
  class Melos::Struct::PrivateMessageContent < Melos::Struct::Base
944
- attr_accessor :application_data, :proposal, :commit, :auth, :padding
945
- # bytes -> struct: decode the content and auth field, rest is padding
946
- # struct -> bytes: encode content and auth field, add set amount of padding (zero bytes)
1006
+ attr_reader :application_data, :proposal, :commit, :auth, :padding, :content_type
1007
+ STRUCT = [
1008
+ [:application_data, :select, ->(ctx){ctx[:content_type] == Melos::Constants::ContentType::APPLICATION}, :vec],
1009
+ [:proposal, :select, ->(ctx){ctx[:content_type] == Melos::Constants::ContentType::PROPOSAL}, :class, Melos::Struct::Proposal],
1010
+ [:commit, :select, ->(ctx){ctx[:content_type] == Melos::Constants::ContentType::COMMIT}, :class, Melos::Struct::Commit],
1011
+ [:auth, :class, Melos::Struct::FramedContentAuthData, ->(ctx){ {content_type: ctx[:content_type]} }],
1012
+ [:padding, :padding] # the rest of the stream; zero bytes
1013
+ ]
947
1014
 
948
- def self.new_and_rest_with_content_type(buf, content_type)
949
- instance = self.allocate
950
- context = []
951
- # deserialize application_data/proposal/commit
952
- case content_type
1015
+ def self.create(framed_content:, auth:, padding_size:)
1016
+ new_instance = self.allocate
1017
+ new_instance.instance_variable_set(:@content_type, framed_content.content_type)
1018
+ case framed_content.content_type
953
1019
  when Melos::Constants::ContentType::APPLICATION
954
- value, buf = Melos::Vec.parse_vec(buf)
955
- context << [:application_data, value]
1020
+ new_instance.instance_variable_set(:@application_data, framed_content.application_data)
956
1021
  when Melos::Constants::ContentType::PROPOSAL
957
- value, buf = Melos::Struct::Proposal.new_and_rest(buf)
958
- context << [:proposal, value]
1022
+ new_instance.instance_variable_set(:@proposal, framed_content.proposal)
959
1023
  when Melos::Constants::ContentType::COMMIT
960
- value, buf = Melos::Struct::Commit.new_and_rest(buf)
961
- context << [:commit, value]
1024
+ new_instance.instance_variable_set(:@commit, framed_content.commit)
962
1025
  end
963
- fcad, buf = Melos::Struct::FramedContentAuthData.new_and_rest_with_content_type(buf, content_type)
964
- context << [:auth, fcad]
965
- # assume rest is padding
966
- context << [:padding, buf]
967
- instance.send(:set_instance_vars, context)
968
- [instance, '']
1026
+ new_instance.instance_variable_set(:@auth, auth)
1027
+ new_instance.instance_variable_set(:@padding, Melos::Crypto::Util.zero_vector(padding_size))
1028
+ new_instance
969
1029
  end
970
1030
 
971
1031
  def content
972
1032
  @application_data || @proposal || @commit
973
1033
  end
1034
+
1035
+ def padding_zero?
1036
+ @padding.each_byte.all?(&:zero?)
1037
+ end
974
1038
  end
975
1039
 
976
1040
  ## 8.2
@@ -1011,9 +1075,21 @@ class Melos::Struct::MLSMessage < Melos::Struct::Base
1011
1075
  [:key_package, :select, ->(ctx){ctx[:wire_format] == Melos::Constants::WireFormat::MLS_KEY_PACKAGE}, :class, Melos::Struct::KeyPackage]
1012
1076
  ]
1013
1077
 
1014
- def verify(suite, signer_public_key, group_context)
1015
- if wire_format == Melos::Constants::WireFormat::MLS_PUBLIC_MESSAGE
1016
- public_message.verify(suite, signer_public_key, version, wire_format, group_context)
1017
- end
1078
+ # wrap exactly one of the message types; the wire format follows from which one is given
1079
+ def self.create(public_message: nil, private_message: nil, welcome: nil, group_info: nil, key_package: nil)
1080
+ given = {
1081
+ public_message: [Melos::Constants::WireFormat::MLS_PUBLIC_MESSAGE, public_message],
1082
+ private_message: [Melos::Constants::WireFormat::MLS_PRIVATE_MESSAGE, private_message],
1083
+ welcome: [Melos::Constants::WireFormat::MLS_WELCOME, welcome],
1084
+ group_info: [Melos::Constants::WireFormat::MLS_GROUP_INFO, group_info],
1085
+ key_package: [Melos::Constants::WireFormat::MLS_KEY_PACKAGE, key_package]
1086
+ }.reject { |_, (_, value)| value.nil? }
1087
+ raise ArgumentError.new('exactly one message must be given') unless given.size == 1
1088
+ field, (wire_format, value) = given.first
1089
+ instance = allocate
1090
+ instance.instance_variable_set(:@version, Melos::Constants::Version::MLS10)
1091
+ instance.instance_variable_set(:@wire_format, wire_format)
1092
+ instance.instance_variable_set("@#{field}", value)
1093
+ instance
1018
1094
  end
1019
1095
  end