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.
- checksums.yaml +4 -4
- data/lib/diaspora_federation.rb +103 -18
- data/lib/diaspora_federation/discovery/discovery.rb +1 -1
- data/lib/diaspora_federation/discovery/h_card.rb +4 -5
- data/lib/diaspora_federation/discovery/host_meta.rb +1 -1
- data/lib/diaspora_federation/discovery/web_finger.rb +8 -8
- data/lib/diaspora_federation/discovery/xrd_document.rb +6 -7
- data/lib/diaspora_federation/entities.rb +21 -10
- data/lib/diaspora_federation/entities/account_deletion.rb +7 -3
- data/lib/diaspora_federation/entities/comment.rb +13 -10
- data/lib/diaspora_federation/entities/contact.rb +29 -0
- data/lib/diaspora_federation/entities/conversation.rb +5 -6
- data/lib/diaspora_federation/entities/like.rb +10 -18
- data/lib/diaspora_federation/entities/message.rb +6 -12
- data/lib/diaspora_federation/entities/participation.rb +8 -16
- data/lib/diaspora_federation/entities/person.rb +6 -2
- data/lib/diaspora_federation/entities/photo.rb +3 -3
- data/lib/diaspora_federation/entities/poll_participation.rb +6 -12
- data/lib/diaspora_federation/entities/post.rb +37 -0
- data/lib/diaspora_federation/entities/profile.rb +7 -3
- data/lib/diaspora_federation/entities/relayable.rb +169 -65
- data/lib/diaspora_federation/entities/relayable_retraction.rb +33 -32
- data/lib/diaspora_federation/entities/request.rb +20 -6
- data/lib/diaspora_federation/entities/reshare.rb +5 -27
- data/lib/diaspora_federation/entities/retraction.rb +6 -6
- data/lib/diaspora_federation/entities/signed_retraction.rb +32 -26
- data/lib/diaspora_federation/entities/status_message.rb +2 -22
- data/lib/diaspora_federation/entity.rb +137 -38
- data/lib/diaspora_federation/federation.rb +9 -0
- data/lib/diaspora_federation/federation/fetcher.rb +26 -0
- data/lib/diaspora_federation/federation/receiver.rb +41 -0
- data/lib/diaspora_federation/federation/receiver/abstract_receiver.rb +35 -0
- data/lib/diaspora_federation/federation/receiver/exceptions.rb +13 -0
- data/lib/diaspora_federation/federation/receiver/private.rb +15 -0
- data/lib/diaspora_federation/federation/receiver/public.rb +9 -0
- data/lib/diaspora_federation/federation/sender.rb +33 -0
- data/lib/diaspora_federation/federation/sender/hydra_wrapper.rb +92 -0
- data/lib/diaspora_federation/{fetcher.rb → http_client.rb} +6 -6
- data/lib/diaspora_federation/properties_dsl.rb +51 -14
- data/lib/diaspora_federation/salmon.rb +2 -1
- data/lib/diaspora_federation/salmon/aes.rb +1 -1
- data/lib/diaspora_federation/salmon/encrypted_magic_envelope.rb +61 -0
- data/lib/diaspora_federation/salmon/encrypted_slap.rb +69 -50
- data/lib/diaspora_federation/salmon/exceptions.rb +8 -14
- data/lib/diaspora_federation/salmon/magic_envelope.rb +80 -39
- data/lib/diaspora_federation/salmon/slap.rb +20 -51
- data/lib/diaspora_federation/salmon/xml_payload.rb +5 -104
- data/lib/diaspora_federation/validators.rb +22 -16
- data/lib/diaspora_federation/validators/account_deletion_validator.rb +1 -1
- data/lib/diaspora_federation/validators/comment_validator.rb +0 -4
- data/lib/diaspora_federation/validators/contact_validator.rb +13 -0
- data/lib/diaspora_federation/validators/conversation_validator.rb +2 -2
- data/lib/diaspora_federation/validators/like_validator.rb +1 -3
- data/lib/diaspora_federation/validators/message_validator.rb +0 -4
- data/lib/diaspora_federation/validators/participation_validator.rb +1 -5
- data/lib/diaspora_federation/validators/person_validator.rb +1 -1
- data/lib/diaspora_federation/validators/photo_validator.rb +2 -2
- data/lib/diaspora_federation/validators/poll_participation_validator.rb +0 -4
- data/lib/diaspora_federation/validators/profile_validator.rb +1 -1
- data/lib/diaspora_federation/validators/relayable_retraction_validator.rb +1 -1
- data/lib/diaspora_federation/validators/relayable_validator.rb +2 -0
- data/lib/diaspora_federation/validators/request_validator.rb +3 -2
- data/lib/diaspora_federation/validators/reshare_validator.rb +3 -3
- data/lib/diaspora_federation/validators/retraction_validator.rb +2 -2
- data/lib/diaspora_federation/validators/rules/guid.rb +16 -7
- data/lib/diaspora_federation/validators/signed_retraction_validator.rb +1 -1
- data/lib/diaspora_federation/validators/status_message_validator.rb +2 -2
- data/lib/diaspora_federation/version.rb +1 -1
- metadata +20 -11
- data/lib/diaspora_federation/receiver.rb +0 -28
- data/lib/diaspora_federation/receiver/private.rb +0 -19
- data/lib/diaspora_federation/receiver/public.rb +0 -13
- 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.
|
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
|
-
#
|
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
|
-
#
|
65
|
-
|
66
|
-
|
67
|
-
#
|
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]
|
81
|
+
# @param [OpenSSL::PKey::RSA] privkey recipient private_key for decryption
|
72
82
|
#
|
73
|
-
# @return [
|
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,
|
79
|
-
raise ArgumentError unless slap_xml.instance_of?(String) &&
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
-
|
90
|
-
end
|
98
|
+
MagicEnvelope.unenvelop(magic_env_from_doc(doc), sender, cipher_params)
|
91
99
|
end
|
92
100
|
|
93
|
-
# Creates an encrypted Salmon Slap
|
101
|
+
# Creates an encrypted Salmon Slap.
|
94
102
|
#
|
95
103
|
# @param [String] author_id Diaspora* handle of the author
|
96
|
-
# @param [OpenSSL::PKey::RSA]
|
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
|
102
|
-
raise ArgumentError unless
|
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
|
-
|
109
|
-
envelope_key = magic_envelope.encrypt!
|
131
|
+
xml.encrypted_header(encrypted_header(@author_id, @cipher_params, pubkey))
|
110
132
|
|
111
|
-
|
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]
|
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,
|
121
|
-
header_elem = decrypt_header(data,
|
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]
|
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,
|
159
|
+
def self.decrypt_header(data, privkey)
|
137
160
|
cipher_header = JSON.parse(Base64.decode64(data))
|
138
|
-
key = JSON.parse(
|
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
|
-
# @
|
151
|
-
def
|
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
|
-
|
154
|
-
ciphertext = AES.encrypt(data,
|
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(
|
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
|
-
|
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
|
170
|
-
|
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
|
-
|
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
|
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(
|
48
|
-
raise ArgumentError unless
|
49
|
-
payload.is_a?(Entity)
|
52
|
+
def initialize(payload, sender=nil)
|
53
|
+
raise ArgumentError unless payload.is_a?(Entity)
|
50
54
|
|
51
|
-
@
|
52
|
-
@
|
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 [
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
xml["me"].
|
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
|
-
@
|
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 [
|
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,
|
105
|
-
raise ArgumentError unless
|
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
|
-
|
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
|
125
|
-
subject =
|
126
|
-
|
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 [
|
171
|
+
# @param [String] sender diaspora-ID of the sender or nil
|
145
172
|
# @return [Boolean]
|
146
|
-
def self.signature_valid?(env,
|
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
|
-
|
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]
|