diaspora_federation 0.0.12 → 0.0.13

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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/lib/diaspora_federation.rb +103 -18
  3. data/lib/diaspora_federation/discovery/discovery.rb +1 -1
  4. data/lib/diaspora_federation/discovery/h_card.rb +4 -5
  5. data/lib/diaspora_federation/discovery/host_meta.rb +1 -1
  6. data/lib/diaspora_federation/discovery/web_finger.rb +8 -8
  7. data/lib/diaspora_federation/discovery/xrd_document.rb +6 -7
  8. data/lib/diaspora_federation/entities.rb +21 -10
  9. data/lib/diaspora_federation/entities/account_deletion.rb +7 -3
  10. data/lib/diaspora_federation/entities/comment.rb +13 -10
  11. data/lib/diaspora_federation/entities/contact.rb +29 -0
  12. data/lib/diaspora_federation/entities/conversation.rb +5 -6
  13. data/lib/diaspora_federation/entities/like.rb +10 -18
  14. data/lib/diaspora_federation/entities/message.rb +6 -12
  15. data/lib/diaspora_federation/entities/participation.rb +8 -16
  16. data/lib/diaspora_federation/entities/person.rb +6 -2
  17. data/lib/diaspora_federation/entities/photo.rb +3 -3
  18. data/lib/diaspora_federation/entities/poll_participation.rb +6 -12
  19. data/lib/diaspora_federation/entities/post.rb +37 -0
  20. data/lib/diaspora_federation/entities/profile.rb +7 -3
  21. data/lib/diaspora_federation/entities/relayable.rb +169 -65
  22. data/lib/diaspora_federation/entities/relayable_retraction.rb +33 -32
  23. data/lib/diaspora_federation/entities/request.rb +20 -6
  24. data/lib/diaspora_federation/entities/reshare.rb +5 -27
  25. data/lib/diaspora_federation/entities/retraction.rb +6 -6
  26. data/lib/diaspora_federation/entities/signed_retraction.rb +32 -26
  27. data/lib/diaspora_federation/entities/status_message.rb +2 -22
  28. data/lib/diaspora_federation/entity.rb +137 -38
  29. data/lib/diaspora_federation/federation.rb +9 -0
  30. data/lib/diaspora_federation/federation/fetcher.rb +26 -0
  31. data/lib/diaspora_federation/federation/receiver.rb +41 -0
  32. data/lib/diaspora_federation/federation/receiver/abstract_receiver.rb +35 -0
  33. data/lib/diaspora_federation/federation/receiver/exceptions.rb +13 -0
  34. data/lib/diaspora_federation/federation/receiver/private.rb +15 -0
  35. data/lib/diaspora_federation/federation/receiver/public.rb +9 -0
  36. data/lib/diaspora_federation/federation/sender.rb +33 -0
  37. data/lib/diaspora_federation/federation/sender/hydra_wrapper.rb +92 -0
  38. data/lib/diaspora_federation/{fetcher.rb → http_client.rb} +6 -6
  39. data/lib/diaspora_federation/properties_dsl.rb +51 -14
  40. data/lib/diaspora_federation/salmon.rb +2 -1
  41. data/lib/diaspora_federation/salmon/aes.rb +1 -1
  42. data/lib/diaspora_federation/salmon/encrypted_magic_envelope.rb +61 -0
  43. data/lib/diaspora_federation/salmon/encrypted_slap.rb +69 -50
  44. data/lib/diaspora_federation/salmon/exceptions.rb +8 -14
  45. data/lib/diaspora_federation/salmon/magic_envelope.rb +80 -39
  46. data/lib/diaspora_federation/salmon/slap.rb +20 -51
  47. data/lib/diaspora_federation/salmon/xml_payload.rb +5 -104
  48. data/lib/diaspora_federation/validators.rb +22 -16
  49. data/lib/diaspora_federation/validators/account_deletion_validator.rb +1 -1
  50. data/lib/diaspora_federation/validators/comment_validator.rb +0 -4
  51. data/lib/diaspora_federation/validators/contact_validator.rb +13 -0
  52. data/lib/diaspora_federation/validators/conversation_validator.rb +2 -2
  53. data/lib/diaspora_federation/validators/like_validator.rb +1 -3
  54. data/lib/diaspora_federation/validators/message_validator.rb +0 -4
  55. data/lib/diaspora_federation/validators/participation_validator.rb +1 -5
  56. data/lib/diaspora_federation/validators/person_validator.rb +1 -1
  57. data/lib/diaspora_federation/validators/photo_validator.rb +2 -2
  58. data/lib/diaspora_federation/validators/poll_participation_validator.rb +0 -4
  59. data/lib/diaspora_federation/validators/profile_validator.rb +1 -1
  60. data/lib/diaspora_federation/validators/relayable_retraction_validator.rb +1 -1
  61. data/lib/diaspora_federation/validators/relayable_validator.rb +2 -0
  62. data/lib/diaspora_federation/validators/request_validator.rb +3 -2
  63. data/lib/diaspora_federation/validators/reshare_validator.rb +3 -3
  64. data/lib/diaspora_federation/validators/retraction_validator.rb +2 -2
  65. data/lib/diaspora_federation/validators/rules/guid.rb +16 -7
  66. data/lib/diaspora_federation/validators/signed_retraction_validator.rb +1 -1
  67. data/lib/diaspora_federation/validators/status_message_validator.rb +2 -2
  68. data/lib/diaspora_federation/version.rb +1 -1
  69. metadata +20 -11
  70. data/lib/diaspora_federation/receiver.rb +0 -28
  71. data/lib/diaspora_federation/receiver/private.rb +0 -19
  72. data/lib/diaspora_federation/receiver/public.rb +0 -13
  73. data/lib/diaspora_federation/signing.rb +0 -56
@@ -3,7 +3,7 @@ module DiasporaFederation
3
3
  # {http://www.salmon-protocol.org/ Salmon Protocol}.
4
4
  module Salmon
5
5
  # XML namespace url
6
- XMLNS = "https://joindiaspora.com/protocol"
6
+ XMLNS = "https://joindiaspora.com/protocol".freeze
7
7
  end
8
8
  end
9
9
 
@@ -13,5 +13,6 @@ require "diaspora_federation/salmon/aes"
13
13
  require "diaspora_federation/salmon/exceptions"
14
14
  require "diaspora_federation/salmon/xml_payload"
15
15
  require "diaspora_federation/salmon/magic_envelope"
16
+ require "diaspora_federation/salmon/encrypted_magic_envelope"
16
17
  require "diaspora_federation/salmon/slap"
17
18
  require "diaspora_federation/salmon/encrypted_slap"
@@ -3,7 +3,7 @@ module DiasporaFederation
3
3
  # class for AES encryption and decryption
4
4
  class AES
5
5
  # OpenSSL aes cipher definition
6
- CIPHER = "AES-256-CBC"
6
+ CIPHER = "AES-256-CBC".freeze
7
7
 
8
8
  # generates a random AES key and initialization vector
9
9
  # @return [Hash] { key: "...", iv: "..." }
@@ -0,0 +1,61 @@
1
+ module DiasporaFederation
2
+ module Salmon
3
+ # This is a simple crypt-wrapper for {MagicEnvelope}.
4
+ #
5
+ # The wrapper is JSON with the following structure:
6
+ #
7
+ # {
8
+ # "aes_key": "...",
9
+ # "encrypted_magic_envelope": "..."
10
+ # }
11
+ #
12
+ # +aes_key+ is encrypted using the recipients public key, and contains the AES
13
+ # +key+ and +iv+ as JSON:
14
+ #
15
+ # {
16
+ # "key": "...",
17
+ # "iv": "..."
18
+ # }
19
+ #
20
+ # +encrypted_magic_envelope+ is encrypted using the +key+ and +iv+ from +aes_key+.
21
+ # Once decrypted it contains the {MagicEnvelope} xml:
22
+ #
23
+ # <me:env>
24
+ # ...
25
+ # </me:env>
26
+ #
27
+ # All JSON-values (+aes_key+, +encrypted_magic_envelope+, +key+ and +iv+) are
28
+ # base64 encoded.
29
+ module EncryptedMagicEnvelope
30
+ # Generates a new random AES key and encrypts the {MagicEnvelope} with it.
31
+ # Then encrypts the AES key with the receivers public key.
32
+ # @param [Nokogiri::XML::Element] magic_env XML root node of a magic envelope
33
+ # @param [OpenSSL::PKey::RSA] pubkey recipient public_key
34
+ # @return [String] json string
35
+ def self.encrypt(magic_env, pubkey)
36
+ key = AES.generate_key_and_iv
37
+ encrypted_env = AES.encrypt(magic_env.to_xml, key[:key], key[:iv])
38
+
39
+ encoded_key = Hash[key.map {|k, v| [k, Base64.strict_encode64(v)] }]
40
+ encrypted_key = Base64.strict_encode64(pubkey.public_encrypt(JSON.generate(encoded_key)))
41
+
42
+ JSON.generate(aes_key: encrypted_key, encrypted_magic_envelope: encrypted_env)
43
+ end
44
+
45
+ # Decrypts the AES key with the private key of the receiver and decrypts the
46
+ # encrypted {MagicEnvelope} with it.
47
+ # @param [String] encrypted_env json string with aes_key and encrypted_magic_envelope
48
+ # @param [OpenSSL::PKey::RSA] privkey private key for decryption
49
+ # @return [Nokogiri::XML::Element] decrypted magic envelope xml
50
+ def self.decrypt(encrypted_env, privkey)
51
+ encrypted_json = JSON.parse(encrypted_env)
52
+
53
+ encoded_key = JSON.parse(privkey.private_decrypt(Base64.decode64(encrypted_json["aes_key"])))
54
+ key = Hash[encoded_key.map {|k, v| [k, Base64.decode64(v)] }]
55
+
56
+ xml = AES.decrypt(encrypted_json["encrypted_magic_envelope"], key["key"], key["iv"])
57
+ Nokogiri::XML::Document.parse(xml).root
58
+ end
59
+ end
60
+ end
61
+ end
@@ -54,71 +54,94 @@ module DiasporaFederation
54
54
  # recipient_pubkey = however_you_retrieve_the_recipients_public_key()
55
55
  # entity = YourEntity.new(attr: "val")
56
56
  #
57
- # slap_xml = EncryptedSlap.generate_xml(author_id, author_privkey, entity, recipient_pubkey)
57
+ # slap_xml = EncryptedSlap.prepare(author_id, author_privkey, entity).generate_xml(recipient_pubkey)
58
58
  #
59
59
  # @example Parsing a Salmon Slap
60
60
  # recipient_privkey = however_you_retrieve_the_recipients_private_key()
61
- # slap = EncryptedSlap.from_xml(slap_xml, recipient_privkey)
62
- # author_pubkey = however_you_retrieve_the_authors_public_key(slap.author_id)
61
+ # entity = EncryptedSlap.from_xml(slap_xml, recipient_privkey).payload
63
62
  #
64
- # entity = slap.entity(author_pubkey)
65
- #
66
- class EncryptedSlap
67
- # Creates a Slap instance from the data within the given XML string
63
+ # @deprecated
64
+ class EncryptedSlap < Slap
65
+ # the author of the slap
66
+ # @param [String] value the author diaspora id
67
+ attr_writer :author_id
68
+
69
+ # the key and iv if it is an encrypted slap
70
+ # @param [Hash] value hash containing the key and iv
71
+ attr_writer :cipher_params
72
+
73
+ # the prepared encrypted magic envelope xml
74
+ # @param [Nokogiri::XML::Element] value magic envelope xml
75
+ attr_writer :magic_envelope_xml
76
+
77
+ # Creates a {MagicEnvelope} instance from the data within the given XML string
68
78
  # containing an encrypted payload.
69
79
  #
70
80
  # @param [String] slap_xml encrypted Salmon xml
71
- # @param [OpenSSL::PKey::RSA] pkey recipient private_key for decryption
81
+ # @param [OpenSSL::PKey::RSA] privkey recipient private_key for decryption
72
82
  #
73
- # @return [Slap] new Slap instance
83
+ # @return [MagicEnvelope] magic envelope instance with payload and sender
74
84
  #
75
85
  # @raise [ArgumentError] if any of the arguments is of the wrong type
76
86
  # @raise [MissingHeader] if the +encrypted_header+ element is missing in the XML
77
87
  # @raise [MissingMagicEnvelope] if the +me:env+ element is missing in the XML
78
- def self.from_xml(slap_xml, pkey)
79
- raise ArgumentError unless slap_xml.instance_of?(String) && pkey.instance_of?(OpenSSL::PKey::RSA)
88
+ def self.from_xml(slap_xml, privkey)
89
+ raise ArgumentError unless slap_xml.instance_of?(String) && privkey.instance_of?(OpenSSL::PKey::RSA)
80
90
  doc = Nokogiri::XML::Document.parse(slap_xml)
81
91
 
82
- Slap.new.tap do |slap|
83
- header_elem = doc.at_xpath("d:diaspora/d:encrypted_header", Slap::NS)
84
- raise MissingHeader if header_elem.nil?
85
- header = header_data(header_elem.content, pkey)
86
- slap.author_id = header[:author_id]
87
- slap.cipher_params = {key: Base64.decode64(header[:aes_key]), iv: Base64.decode64(header[:iv])}
92
+ header_elem = doc.at_xpath("d:diaspora/d:encrypted_header", Slap::NS)
93
+ raise MissingHeader if header_elem.nil?
94
+ header = header_data(header_elem.content, privkey)
95
+ sender = header[:author_id]
96
+ cipher_params = {key: Base64.decode64(header[:aes_key]), iv: Base64.decode64(header[:iv])}
88
97
 
89
- slap.add_magic_env_from_doc(doc)
90
- end
98
+ MagicEnvelope.unenvelop(magic_env_from_doc(doc), sender, cipher_params)
91
99
  end
92
100
 
93
- # Creates an encrypted Salmon Slap and returns the XML string.
101
+ # Creates an encrypted Salmon Slap.
94
102
  #
95
103
  # @param [String] author_id Diaspora* handle of the author
96
- # @param [OpenSSL::PKey::RSA] pkey sender private key for signing the magic envelope
104
+ # @param [OpenSSL::PKey::RSA] privkey sender private key for signing the magic envelope
97
105
  # @param [Entity] entity payload
106
+ # @return [EncryptedSlap] encrypted Slap instance
107
+ # @raise [ArgumentError] if any of the arguments is of the wrong type
108
+ def self.prepare(author_id, privkey, entity)
109
+ raise ArgumentError unless author_id.instance_of?(String) &&
110
+ privkey.instance_of?(OpenSSL::PKey::RSA) &&
111
+ entity.is_a?(Entity)
112
+
113
+ EncryptedSlap.new.tap do |slap|
114
+ slap.author_id = author_id
115
+
116
+ magic_envelope = MagicEnvelope.new(entity)
117
+ slap.cipher_params = magic_envelope.encrypt!
118
+ slap.magic_envelope_xml = magic_envelope.envelop(privkey)
119
+ end
120
+ end
121
+
122
+ # Creates an encrypted Salmon Slap XML string.
123
+ #
98
124
  # @param [OpenSSL::PKey::RSA] pubkey recipient public key for encrypting the AES key
99
125
  # @return [String] Salmon XML string
100
126
  # @raise [ArgumentError] if any of the arguments is of the wrong type
101
- def self.generate_xml(author_id, pkey, entity, pubkey)
102
- raise ArgumentError unless author_id.instance_of?(String) &&
103
- pkey.instance_of?(OpenSSL::PKey::RSA) &&
104
- entity.is_a?(Entity) &&
105
- pubkey.instance_of?(OpenSSL::PKey::RSA)
127
+ def generate_xml(pubkey)
128
+ raise ArgumentError unless pubkey.instance_of?(OpenSSL::PKey::RSA)
106
129
 
107
130
  Slap.build_xml do |xml|
108
- magic_envelope = MagicEnvelope.new(pkey, entity)
109
- envelope_key = magic_envelope.encrypt!
131
+ xml.encrypted_header(encrypted_header(@author_id, @cipher_params, pubkey))
110
132
 
111
- encrypted_header(author_id, envelope_key, pubkey, xml)
112
- magic_envelope.envelop(xml)
133
+ xml.parent << @magic_envelope_xml
113
134
  end
114
135
  end
115
136
 
137
+ private
138
+
116
139
  # decrypts and reads the data from the encrypted XML header
117
140
  # @param [String] data base64 encoded, encrypted header data
118
- # @param [OpenSSL::PKey::RSA] pkey private key for decryption
141
+ # @param [OpenSSL::PKey::RSA] privkey private key for decryption
119
142
  # @return [Hash] { iv: "...", aes_key: "...", author_id: "..." }
120
- def self.header_data(data, pkey)
121
- header_elem = decrypt_header(data, pkey)
143
+ def self.header_data(data, privkey)
144
+ header_elem = decrypt_header(data, privkey)
122
145
  raise InvalidHeader unless header_elem.name == "decrypted_header"
123
146
 
124
147
  iv = header_elem.at_xpath("iv").content
@@ -131,11 +154,11 @@ module DiasporaFederation
131
154
 
132
155
  # decrypts the xml header
133
156
  # @param [String] data base64 encoded, encrypted header data
134
- # @param [OpenSSL::PKey::RSA] pkey private key for decryption
157
+ # @param [OpenSSL::PKey::RSA] privkey private key for decryption
135
158
  # @return [Nokogiri::XML::Element] header xml document
136
- def self.decrypt_header(data, pkey)
159
+ def self.decrypt_header(data, privkey)
137
160
  cipher_header = JSON.parse(Base64.decode64(data))
138
- key = JSON.parse(pkey.private_decrypt(Base64.decode64(cipher_header["aes_key"])))
161
+ key = JSON.parse(privkey.private_decrypt(Base64.decode64(cipher_header["aes_key"])))
139
162
 
140
163
  xml = AES.decrypt(cipher_header["ciphertext"], Base64.decode64(key["key"]), Base64.decode64(key["iv"]))
141
164
  Nokogiri::XML::Document.parse(xml).root
@@ -147,43 +170,39 @@ module DiasporaFederation
147
170
  # @param [String] author_id diaspora_handle
148
171
  # @param [Hash] envelope_key envelope cipher params
149
172
  # @param [OpenSSL::PKey::RSA] pubkey recipient public_key
150
- # @param [Nokogiri::XML::Element] xml parent element for inserting in XML document
151
- def self.encrypted_header(author_id, envelope_key, pubkey, xml)
173
+ # @return [String] encrypted base64 encoded header
174
+ def encrypted_header(author_id, envelope_key, pubkey)
152
175
  data = header_xml(author_id, strict_base64_encode(envelope_key))
153
- key = AES.generate_key_and_iv
154
- ciphertext = AES.encrypt(data, key[:key], key[:iv])
176
+ header_key = AES.generate_key_and_iv
177
+ ciphertext = AES.encrypt(data, header_key[:key], header_key[:iv])
155
178
 
156
- json_key = JSON.generate(strict_base64_encode(key))
179
+ json_key = JSON.generate(strict_base64_encode(header_key))
157
180
  encrypted_key = Base64.strict_encode64(pubkey.public_encrypt(json_key))
158
181
 
159
182
  json_header = JSON.generate(aes_key: encrypted_key, ciphertext: ciphertext)
160
183
 
161
- xml.encrypted_header(Base64.strict_encode64(json_header))
184
+ Base64.strict_encode64(json_header)
162
185
  end
163
- private_class_method :encrypted_header
164
186
 
165
187
  # generate the header xml string, including the author, aes_key and iv
166
188
  # @param [String] author_id diaspora_handle of the author
167
189
  # @param [Hash] envelope_key { key: "...", iv: "..." } (values in base64)
168
190
  # @return [String] header XML string
169
- def self.header_xml(author_id, envelope_key)
170
- builder = Nokogiri::XML::Builder.new do |xml|
191
+ def header_xml(author_id, envelope_key)
192
+ @header_xml ||= Nokogiri::XML::Builder.new(encoding: "UTF-8") {|xml|
171
193
  xml.decrypted_header {
172
194
  xml.iv(envelope_key[:iv])
173
195
  xml.aes_key(envelope_key[:key])
174
196
  xml.author_id(author_id)
175
197
  }
176
- end
177
- builder.to_xml.strip
198
+ }.to_xml.strip
178
199
  end
179
- private_class_method :header_xml
180
200
 
181
201
  # @param [Hash] hash { key: "...", iv: "..." }
182
202
  # @return [Hash] encoded hash: { key: "...", iv: "..." }
183
- def self.strict_base64_encode(hash)
203
+ def strict_base64_encode(hash)
184
204
  Hash[hash.map {|k, v| [k, Base64.strict_encode64(v)] }]
185
205
  end
186
- private_class_method :strict_base64_encode
187
206
  end
188
207
  end
189
208
  end
@@ -1,21 +1,29 @@
1
1
  module DiasporaFederation
2
2
  module Salmon
3
3
  # Raised, if the element containing the Magic Envelope is missing from the XML
4
+ # @deprecated
4
5
  class MissingMagicEnvelope < RuntimeError
5
6
  end
6
7
 
7
8
  # Raised, if the element containing the author is empty.
9
+ # @deprecated
8
10
  class MissingAuthor < RuntimeError
9
11
  end
10
12
 
11
13
  # Raised, if the element containing the header is missing from the XML
14
+ # @deprecated
12
15
  class MissingHeader < RuntimeError
13
16
  end
14
17
 
15
18
  # Raised if the decrypted header has an unexpected XML structure
19
+ # @deprecated
16
20
  class InvalidHeader < RuntimeError
17
21
  end
18
22
 
23
+ # Raised, if failed to fetch the public key of the sender of the received message
24
+ class SenderKeyNotFound < RuntimeError
25
+ end
26
+
19
27
  # Raised, if the Magic Envelope XML structure is malformed.
20
28
  class InvalidEnvelope < RuntimeError
21
29
  end
@@ -32,19 +40,5 @@ module DiasporaFederation
32
40
  # Raised, if the parsed Magic Envelope specifies an unhandled encoding.
33
41
  class InvalidEncoding < RuntimeError
34
42
  end
35
-
36
- # Raised, if the XML structure of the parsed document doesn't resemble the
37
- # expected structure.
38
- class InvalidStructure < RuntimeError
39
- end
40
-
41
- # Raised, if the entity name in the XML is invalid
42
- class InvalidEntityName < RuntimeError
43
- end
44
-
45
- # Raised, if the entity contained within the XML cannot be mapped to a
46
- # defined {Entity} subclass.
47
- class UnknownEntity < RuntimeError
48
- end
49
43
  end
50
44
  end
@@ -13,7 +13,7 @@ module DiasporaFederation
13
13
  # <me:data type="application/xml">{data}</me:data>
14
14
  # <me:encoding>base64url</me:encoding>
15
15
  # <me:alg>RSA-SHA256</me:alg>
16
- # <me:sig>{signature}</me:sig>
16
+ # <me:sig key_id="{sender}">{signature}</me:sig>
17
17
  # </me:env>
18
18
  #
19
19
  # When parsing the XML of an incoming Magic Envelope {MagicEnvelope.unenvelop}
@@ -21,47 +21,56 @@ module DiasporaFederation
21
21
  #
22
22
  # @see http://salmon-protocol.googlecode.com/svn/trunk/draft-panzer-magicsig-01.html
23
23
  class MagicEnvelope
24
- # returns the payload (only used for testing purposes)
25
- attr_reader :payload
26
-
27
24
  # encoding used for the payload data
28
- ENCODING = "base64url"
25
+ ENCODING = "base64url".freeze
29
26
 
30
27
  # algorithm used for signing the payload data
31
- ALGORITHM = "RSA-SHA256"
28
+ ALGORITHM = "RSA-SHA256".freeze
32
29
 
33
30
  # mime type describing the payload data
34
- DATA_TYPE = "application/xml"
31
+ DATA_TYPE = "application/xml".freeze
35
32
 
36
33
  # digest instance used for signing
37
34
  DIGEST = OpenSSL::Digest::SHA256.new
38
35
 
39
36
  # XML namespace url
40
- XMLNS = "http://salmon-protocol.org/ns/magic-env"
37
+ XMLNS = "http://salmon-protocol.org/ns/magic-env".freeze
38
+
39
+ # the payload entity of the magic envelope
40
+ # @return [Entity] payload entity
41
+ attr_reader :payload
42
+
43
+ # the sender of the magic envelope
44
+ # @return [String] diaspora-ID of the sender
45
+ attr_reader :sender
41
46
 
42
47
  # Creates a new instance of MagicEnvelope.
43
48
  #
44
- # @param [OpenSSL::PKey::RSA] rsa_pkey private key used for signing
45
49
  # @param [Entity] payload Entity instance
50
+ # @param [String] sender diaspora-ID of the sender
46
51
  # @raise [ArgumentError] if either argument is not of the right type
47
- def initialize(rsa_pkey, payload)
48
- raise ArgumentError unless rsa_pkey.instance_of?(OpenSSL::PKey::RSA) &&
49
- payload.is_a?(Entity)
52
+ def initialize(payload, sender=nil)
53
+ raise ArgumentError unless payload.is_a?(Entity)
50
54
 
51
- @rsa_pkey = rsa_pkey
52
- @payload = XmlPayload.pack(payload).to_xml.strip
55
+ @payload = payload
56
+ @sender = sender
53
57
  end
54
58
 
55
59
  # Builds the XML structure for the magic envelope, inserts the {ENCODING}
56
60
  # encoded data and signs the envelope using {DIGEST}.
57
61
  #
58
- # @param [Nokogiri::XML::Builder] xml Salmon XML builder
59
- def envelop(xml)
60
- xml["me"].env {
61
- xml["me"].data(Base64.urlsafe_encode64(@payload), type: DATA_TYPE)
62
- xml["me"].encoding(ENCODING)
63
- xml["me"].alg(ALGORITHM)
64
- xml["me"].sig(Base64.urlsafe_encode64(signature))
62
+ # @param [OpenSSL::PKey::RSA] privkey private key used for signing
63
+ # @return [Nokogiri::XML::Element] XML root node
64
+ def envelop(privkey)
65
+ raise ArgumentError unless privkey.instance_of?(OpenSSL::PKey::RSA)
66
+
67
+ build_xml {|xml|
68
+ xml["me"].env("xmlns:me" => XMLNS) {
69
+ xml["me"].data(Base64.urlsafe_encode64(payload_data), type: DATA_TYPE)
70
+ xml["me"].encoding(ENCODING)
71
+ xml["me"].alg(ALGORITHM)
72
+ xml["me"].sig(Base64.urlsafe_encode64(sign(privkey)), key_id)
73
+ }
65
74
  }
66
75
  end
67
76
 
@@ -77,7 +86,7 @@ module DiasporaFederation
77
86
  # @return [Hash] AES key and iv. E.g.: { key: "...", iv: "..." }
78
87
  def encrypt!
79
88
  AES.generate_key_and_iv.tap do |key|
80
- @payload = AES.encrypt(@payload, key[:key], key[:iv])
89
+ @payload_data = AES.encrypt(payload_data, key[:key], key[:iv])
81
90
  end
82
91
  end
83
92
 
@@ -90,7 +99,7 @@ module DiasporaFederation
90
99
  # @see AES#decrypt
91
100
  #
92
101
  # @param [Nokogiri::XML::Element] magic_env XML root node of a magic envelope
93
- # @param [OpenSSL::PKey::RSA] rsa_pubkey public key to verify the signature
102
+ # @param [String] sender diaspora-ID of the sender or nil
94
103
  # @param [Hash] cipher_params hash containing the key and iv for
95
104
  # AES-decrypting previously encrypted data. E.g.: { iv: "...", key: "..." }
96
105
  #
@@ -101,32 +110,52 @@ module DiasporaFederation
101
110
  # @raise [InvalidSignature] if the signature can't be verified
102
111
  # @raise [InvalidEncoding] if the data is wrongly encoded
103
112
  # @raise [InvalidAlgorithm] if the algorithm used doesn't match
104
- def self.unenvelop(magic_env, rsa_pubkey, cipher_params=nil)
105
- raise ArgumentError unless rsa_pubkey.instance_of?(OpenSSL::PKey::RSA) &&
106
- magic_env.instance_of?(Nokogiri::XML::Element)
113
+ def self.unenvelop(magic_env, sender=nil, cipher_params=nil)
114
+ raise ArgumentError unless magic_env.instance_of?(Nokogiri::XML::Element)
107
115
 
108
116
  raise InvalidEnvelope unless envelope_valid?(magic_env)
109
- raise InvalidSignature unless signature_valid?(magic_env, rsa_pubkey)
117
+
118
+ sender ||= sender(magic_env)
119
+ raise InvalidSignature unless signature_valid?(magic_env, sender)
110
120
 
111
121
  raise InvalidEncoding unless encoding_valid?(magic_env)
112
122
  raise InvalidAlgorithm unless algorithm_valid?(magic_env)
113
123
 
114
124
  data = read_and_decrypt_data(magic_env, cipher_params)
115
125
 
116
- XmlPayload.unpack(Nokogiri::XML::Document.parse(data).root)
126
+ new(XmlPayload.unpack(Nokogiri::XML::Document.parse(data).root), sender)
117
127
  end
118
128
 
119
129
  private
120
130
 
131
+ # the payload data as string
132
+ # @return [String] payload data
133
+ def payload_data
134
+ @payload_data ||= XmlPayload.pack(@payload).to_xml.strip
135
+ end
136
+
137
+ def key_id
138
+ sender ? {key_id: Base64.urlsafe_encode64(sender)} : {}
139
+ end
140
+
141
+ # Builds the xml root node of the magic envelope.
142
+ #
143
+ # @yield [xml] Invokes the block with the
144
+ # {http://www.rubydoc.info/gems/nokogiri/Nokogiri/XML/Builder Nokogiri::XML::Builder}
145
+ # @return [Nokogiri::XML::Element] XML root node
146
+ def build_xml
147
+ Nokogiri::XML::Builder.new(encoding: "UTF-8") {|xml|
148
+ yield xml
149
+ }.doc.root
150
+ end
151
+
121
152
  # create the signature for all fields according to specification
122
153
  #
154
+ # @param [OpenSSL::PKey::RSA] privkey private key used for signing
123
155
  # @return [String] the signature
124
- def signature
125
- subject = self.class.sig_subject([@payload,
126
- DATA_TYPE,
127
- ENCODING,
128
- ALGORITHM])
129
- @rsa_pkey.sign(DIGEST, subject)
156
+ def sign(privkey)
157
+ subject = MagicEnvelope.send(:sig_subject, [payload_data, DATA_TYPE, ENCODING, ALGORITHM])
158
+ privkey.sign(DIGEST, subject)
130
159
  end
131
160
 
132
161
  # @param [Nokogiri::XML::Element] env magic envelope XML
@@ -134,26 +163,37 @@ module DiasporaFederation
134
163
  (env.instance_of?(Nokogiri::XML::Element) &&
135
164
  env.name == "env" &&
136
165
  !env.at_xpath("me:data").content.empty? &&
137
- !env.at_xpath("me:encoding").content.empty? &&
138
- !env.at_xpath("me:alg").content.empty? &&
139
166
  !env.at_xpath("me:sig").content.empty?)
140
167
  end
141
168
  private_class_method :envelope_valid?
142
169
 
143
170
  # @param [Nokogiri::XML::Element] env magic envelope XML
144
- # @param [OpenSSL::PKey::RSA] pkey public key
171
+ # @param [String] sender diaspora-ID of the sender or nil
145
172
  # @return [Boolean]
146
- def self.signature_valid?(env, pkey)
173
+ def self.signature_valid?(env, sender)
147
174
  subject = sig_subject([Base64.urlsafe_decode64(env.at_xpath("me:data").content),
148
175
  env.at_xpath("me:data")["type"],
149
176
  env.at_xpath("me:encoding").content,
150
177
  env.at_xpath("me:alg").content])
151
178
 
179
+ sender_key = DiasporaFederation.callbacks.trigger(:fetch_public_key_by_diaspora_id, sender)
180
+ raise SenderKeyNotFound unless sender_key
181
+
152
182
  sig = Base64.urlsafe_decode64(env.at_xpath("me:sig").content)
153
- pkey.verify(DIGEST, sig, subject)
183
+ sender_key.verify(DIGEST, sig, subject)
154
184
  end
155
185
  private_class_method :signature_valid?
156
186
 
187
+ # reads the +key_id+ from the magic envelope
188
+ # @param [Nokogiri::XML::Element] env magic envelope XML
189
+ # @return [String] diaspora-ID of the sender
190
+ def self.sender(env)
191
+ key_id = env.at_xpath("me:sig")["key_id"]
192
+ raise InvalidEnvelope, "no key_id" unless key_id # TODO: move to `envelope_valid?`
193
+ Base64.urlsafe_decode64(key_id)
194
+ end
195
+ private_class_method :sender
196
+
157
197
  # constructs the signature subject.
158
198
  # the given array should consist of the data, data_type (mimetype), encoding
159
199
  # and the algorithm
@@ -162,6 +202,7 @@ module DiasporaFederation
162
202
  def self.sig_subject(data_arr)
163
203
  data_arr.map {|i| Base64.urlsafe_encode64(i) }.join(".")
164
204
  end
205
+ private_class_method :sig_subject
165
206
 
166
207
  # @param [Nokogiri::XML::Element] magic_env magic envelope XML
167
208
  # @return [Boolean]