melos 0.0.2 → 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,22 +1,36 @@
1
1
  module Melos::Struct; end
2
2
 
3
3
  class Melos::Struct::Base
4
- def initialize(stream)
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)
5
9
  stream = StringIO.new(stream) if stream.is_a?(String)
6
- context = deserialize(stream)
10
+ context = deserialize(stream, external)
7
11
  set_instance_vars(context)
8
12
  self
9
13
  end
10
14
 
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
23
+ end
24
+
11
25
  def raw
12
26
  buf = ''
27
+ context = field_context
13
28
  self.class::STRUCT.each do |elem|
29
+ value = self.instance_variable_get("@#{elem[0]}")
14
30
  case elem[1]
15
31
  when :select
16
- value = self.instance_variable_get("@#{elem[0]}")
17
- buf += serialize_select_elem(value, elem[3])
32
+ buf += serialize_select_elem(elem[0], value, elem[2], elem[3], context)
18
33
  else
19
- value = self.instance_variable_get("@#{elem[0]}")
20
34
  buf += serialize_elem(value, elem[1], elem[2])
21
35
  end
22
36
  end
@@ -36,15 +50,19 @@ class Melos::Struct::Base
36
50
  end
37
51
 
38
52
  private
39
- def deserialize(stream)
40
- 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
41
56
  self.class::STRUCT.each do |elem|
42
57
  case elem[1]
43
58
  when :select
44
59
  value = deserialize_select_elem_with_context(stream, context.to_h, elem[2], elem[3], elem[4])
45
60
  context << [elem[0], value]
46
- when :framed_content_auth_data
47
- value = Melos::Struct::FramedContentAuthData.new_with_content_type(stream, 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)
48
66
  context << [elem[0], value]
49
67
  else
50
68
  value = deserialize_elem(stream, elem[1], elem[2])
@@ -71,9 +89,9 @@ class Melos::Struct::Base
71
89
  when :uint64
72
90
  value = stream.read(8).unpack1('Q>')
73
91
  when :vec
74
- value = Melos::Vec.parse_stringio(stream)
92
+ value = Melos::Vec.parse_stream(stream)
75
93
  when :vec_of_type
76
- data = Melos::Vec.parse_stringio(stream)
94
+ data = Melos::Vec.parse_stream(stream)
77
95
  value = []
78
96
  data_stream = StringIO.new(data)
79
97
  while (!data_stream.eof?)
@@ -83,7 +101,7 @@ class Melos::Struct::Base
83
101
  when :class
84
102
  value = type_param.send(:new, stream)
85
103
  when :classes
86
- data = Melos::Vec.parse_stringio(stream)
104
+ data = Melos::Vec.parse_stream(stream)
87
105
  value = []
88
106
  data_stream = StringIO.new(data)
89
107
  while (!data_stream.eof?)
@@ -119,13 +137,13 @@ class Melos::Struct::Base
119
137
  when :uint64
120
138
  [value].pack('Q>')
121
139
  when :vec
122
- Melos::Vec.from_string(value)
140
+ Melos::Vec.string_to_vec(value)
123
141
  when :vec_of_type
124
- Melos::Vec.from_string(value.map { serialize_elem(_1, type_param, nil) }.join)
125
- when :class, :framed_content_auth_data
142
+ Melos::Vec.string_to_vec(value.map { serialize_elem(_1, type_param, nil) }.join)
143
+ when :class
126
144
  value.raw
127
145
  when :classes
128
- Melos::Vec.from_string(value.map(&:raw).join)
146
+ Melos::Vec.string_to_vec(value.map(&:raw).join)
129
147
  when :optional
130
148
  if value.nil?
131
149
  [0].pack('C')
@@ -140,11 +158,19 @@ class Melos::Struct::Base
140
158
  end
141
159
  end
142
160
 
143
- def serialize_select_elem(value, type)
144
- if value.nil?
145
- ''
146
- 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?
147
171
  serialize_elem(value, type, nil)
172
+ else
173
+ ''
148
174
  end
149
175
  end
150
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_with_content_type(stream, content_type)
642
- instance = self.allocate
643
- context = instance.send(:deserialize, stream)
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 = Melos::Vec.parse_stringio(stream)
648
- context << [:confirmation_tag, value]
649
- end
650
- context << [:content_type, content_type]
651
- instance.send(:set_instance_vars, context)
652
- instance
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,10 +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
963
  stream = StringIO.new(Melos::Crypto.aead_decrypt(suite, key, new_nonce, private_content_aad, ciphertext))
891
- pmc = Melos::Struct::PrivateMessageContent.new_with_content_type(stream, content_type)
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?
892
967
 
893
968
  fc = Melos::Struct::FramedContent.create(
894
969
  group_id: group_id,
@@ -922,56 +997,44 @@ class Melos::Struct::PrivateMessage < Melos::Struct::Base
922
997
  nonce_arr.pack('C*')
923
998
  end
924
999
 
925
- def self.serialize_private_message_content(framed_content, framed_content_auth_data, padding_size)
926
- buf = ''
927
- case framed_content.content_type
928
- when Melos::Constants::ContentType::APPLICATION
929
- buf += Melos::Vec.from_string(framed_content.application_data)
930
- when Melos::Constants::ContentType::PROPOSAL
931
- buf += framed_content.proposal.raw
932
- when Melos::Constants::ContentType::COMMIT
933
- buf += framed_content.commit.raw
934
- end
935
- buf += Melos::Vec.from_string(framed_content_auth_data.signature)
936
- if framed_content.content_type == Melos::Constants::ContentType::COMMIT
937
- buf += Melos::Vec.from_string(framed_content_auth_data.confirmation_tag)
938
- end
939
- buf += Melos::Crypto::Util.zero_vector(padding_size)
940
- buf
941
- end
942
1000
  end
943
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)
944
1005
  class Melos::Struct::PrivateMessageContent < Melos::Struct::Base
945
- attr_accessor :application_data, :proposal, :commit, :auth, :padding
946
- # bytes -> struct: decode the content and auth field, rest is padding
947
- # 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
+ ]
948
1014
 
949
- def self.new_with_content_type(stream, content_type)
950
- instance = self.allocate
951
- context = []
952
- # deserialize application_data/proposal/commit
953
- 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
954
1019
  when Melos::Constants::ContentType::APPLICATION
955
- value = Melos::Vec.parse_stringio(stream)
956
- context << [:application_data, value]
1020
+ new_instance.instance_variable_set(:@application_data, framed_content.application_data)
957
1021
  when Melos::Constants::ContentType::PROPOSAL
958
- value = Melos::Struct::Proposal.new(stream)
959
- context << [:proposal, value]
1022
+ new_instance.instance_variable_set(:@proposal, framed_content.proposal)
960
1023
  when Melos::Constants::ContentType::COMMIT
961
- value = Melos::Struct::Commit.new(stream)
962
- context << [:commit, value]
1024
+ new_instance.instance_variable_set(:@commit, framed_content.commit)
963
1025
  end
964
- fcad = Melos::Struct::FramedContentAuthData.new_with_content_type(stream, content_type)
965
- context << [:auth, fcad]
966
- # assume rest is padding
967
- context << [:padding, stream.read()]
968
- instance.send(:set_instance_vars, context)
969
- 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
970
1029
  end
971
1030
 
972
1031
  def content
973
1032
  @application_data || @proposal || @commit
974
1033
  end
1034
+
1035
+ def padding_zero?
1036
+ @padding.each_byte.all?(&:zero?)
1037
+ end
975
1038
  end
976
1039
 
977
1040
  ## 8.2
@@ -1012,9 +1075,21 @@ class Melos::Struct::MLSMessage < Melos::Struct::Base
1012
1075
  [:key_package, :select, ->(ctx){ctx[:wire_format] == Melos::Constants::WireFormat::MLS_KEY_PACKAGE}, :class, Melos::Struct::KeyPackage]
1013
1076
  ]
1014
1077
 
1015
- def verify(suite, signer_public_key, group_context)
1016
- if wire_format == Melos::Constants::WireFormat::MLS_PUBLIC_MESSAGE
1017
- public_message.verify(suite, signer_public_key, version, wire_format, group_context)
1018
- 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
1019
1094
  end
1020
1095
  end