diaspora_federation 0.0.12 → 0.0.13

Sign up to get free protection for your applications and to get access to all the features.
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]