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.
- 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]
|