diaspora_federation 0.0.8 → 0.0.9

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -1
  3. data/lib/diaspora_federation.rb +66 -1
  4. data/lib/diaspora_federation/discovery/h_card.rb +2 -3
  5. data/lib/diaspora_federation/discovery/web_finger.rb +3 -6
  6. data/lib/diaspora_federation/entities.rb +18 -0
  7. data/lib/diaspora_federation/entities/account_deletion.rb +14 -0
  8. data/lib/diaspora_federation/entities/comment.rb +26 -0
  9. data/lib/diaspora_federation/entities/conversation.rb +38 -0
  10. data/lib/diaspora_federation/entities/like.rb +35 -0
  11. data/lib/diaspora_federation/entities/location.rb +23 -0
  12. data/lib/diaspora_federation/entities/message.rb +38 -0
  13. data/lib/diaspora_federation/entities/participation.rb +28 -0
  14. data/lib/diaspora_federation/entities/person.rb +6 -3
  15. data/lib/diaspora_federation/entities/photo.rb +59 -0
  16. data/lib/diaspora_federation/entities/poll.rb +24 -0
  17. data/lib/diaspora_federation/entities/poll_answer.rb +19 -0
  18. data/lib/diaspora_federation/entities/poll_participation.rb +28 -0
  19. data/lib/diaspora_federation/entities/profile.rb +10 -8
  20. data/lib/diaspora_federation/entities/relayable.rb +101 -0
  21. data/lib/diaspora_federation/entities/relayable_retraction.rb +95 -0
  22. data/lib/diaspora_federation/entities/request.rb +21 -0
  23. data/lib/diaspora_federation/entities/reshare.rb +49 -0
  24. data/lib/diaspora_federation/entities/retraction.rb +24 -0
  25. data/lib/diaspora_federation/entities/signed_retraction.rb +66 -0
  26. data/lib/diaspora_federation/entities/status_message.rb +55 -0
  27. data/lib/diaspora_federation/entity.rb +5 -6
  28. data/lib/diaspora_federation/fetcher.rb +1 -2
  29. data/lib/diaspora_federation/properties_dsl.rb +18 -8
  30. data/lib/diaspora_federation/salmon.rb +17 -0
  31. data/lib/diaspora_federation/salmon/aes.rb +58 -0
  32. data/lib/diaspora_federation/salmon/encrypted_slap.rb +187 -0
  33. data/lib/diaspora_federation/salmon/exceptions.rb +50 -0
  34. data/lib/diaspora_federation/salmon/magic_envelope.rb +191 -0
  35. data/lib/diaspora_federation/salmon/slap.rb +128 -0
  36. data/lib/diaspora_federation/salmon/xml_payload.rb +158 -0
  37. data/lib/diaspora_federation/signing.rb +56 -0
  38. data/lib/diaspora_federation/validators.rb +20 -0
  39. data/lib/diaspora_federation/validators/account_deletion_validator.rb +10 -0
  40. data/lib/diaspora_federation/validators/comment_validator.rb +17 -0
  41. data/lib/diaspora_federation/validators/conversation_validator.rb +14 -0
  42. data/lib/diaspora_federation/validators/like_validator.rb +14 -0
  43. data/lib/diaspora_federation/validators/location_validator.rb +11 -0
  44. data/lib/diaspora_federation/validators/message_validator.rb +16 -0
  45. data/lib/diaspora_federation/validators/participation_validator.rb +16 -0
  46. data/lib/diaspora_federation/validators/photo_validator.rb +24 -0
  47. data/lib/diaspora_federation/validators/poll_answer_validator.rb +11 -0
  48. data/lib/diaspora_federation/validators/poll_participation_validator.rb +16 -0
  49. data/lib/diaspora_federation/validators/poll_validator.rb +11 -0
  50. data/lib/diaspora_federation/validators/relayable_retraction_validator.rb +15 -0
  51. data/lib/diaspora_federation/validators/relayable_validator.rb +14 -0
  52. data/lib/diaspora_federation/validators/request_validator.rb +11 -0
  53. data/lib/diaspora_federation/validators/reshare_validator.rb +18 -0
  54. data/lib/diaspora_federation/validators/retraction_validator.rb +14 -0
  55. data/lib/diaspora_federation/validators/rules/diaspora_id_count.rb +37 -0
  56. data/lib/diaspora_federation/validators/signed_retraction_validator.rb +15 -0
  57. data/lib/diaspora_federation/validators/status_message_validator.rb +14 -0
  58. data/lib/diaspora_federation/version.rb +1 -1
  59. metadata +49 -4
@@ -0,0 +1,55 @@
1
+ module DiasporaFederation
2
+ module Entities
3
+ # this entity represents a status message sent by a user
4
+ #
5
+ # @see Validators::StatusMessageValidator
6
+ class StatusMessage < Entity
7
+ # @!attribute [r] raw_message
8
+ # text of the status message composed by the user
9
+ # @return [String] text of the status message
10
+ property :raw_message
11
+
12
+ # @!attribute [r] photos
13
+ # optional photos attached to the status message
14
+ # @return [[Entities::Photo]] photos
15
+ entity :photos, [Entities::Photo], default: []
16
+
17
+ # @!attribute [r] location
18
+ # optional location attached to the status message
19
+ # @return [Entities::Location] location
20
+ entity :location, Entities::Location, default: nil
21
+
22
+ # @!attribute [r] poll
23
+ # optional poll attached to the status message
24
+ # @return [Entities::Poll] poll
25
+ entity :poll, Entities::Poll, default: nil
26
+
27
+ # @!attribute [r] guid
28
+ # a random string of at least 16 chars.
29
+ # @see Validation::Rule::Guid
30
+ # @return [String] status message guid
31
+ property :guid
32
+
33
+ # @!attribute [r] diaspora_id
34
+ # The diaspora ID of the person who posts the status message
35
+ # @see Person#diaspora_id
36
+ # @return [String] diaspora ID
37
+ property :diaspora_id, xml_name: :diaspora_handle
38
+
39
+ # @!attribute [r] public
40
+ # shows whether the status message is visible to everyone or only to some aspects
41
+ # @return [Boolean] is it public
42
+ property :public, default: false
43
+
44
+ # @!attribute [r] created_at
45
+ # status message entity creation time
46
+ # @return [Time] creation time
47
+ property :created_at, default: -> { Time.now.utc }
48
+
49
+ # @!attribute [r] provider_display_name
50
+ # a string that describes a means by which a user has posted the status message
51
+ # @return [String] provider display name
52
+ property :provider_display_name, default: nil
53
+ end
54
+ end
55
+ end
@@ -6,7 +6,7 @@ module DiasporaFederation
6
6
  #
7
7
  # Any entity also provides the means to serialize itself and all nested
8
8
  # entities to XML (for deserialization from XML to +Entity+ instances, see
9
- # {XmlPayload}).
9
+ # {Salmon::XmlPayload}).
10
10
  #
11
11
  # @abstract Subclass and specify properties to implement various entities.
12
12
  #
@@ -76,19 +76,18 @@ module DiasporaFederation
76
76
  # {http://www.rubydoc.info/gems/nokogiri/Nokogiri/XML/Element Nokogiri::XML::Element}s
77
77
  #
78
78
  # @see Nokogiri::XML::Node.to_xml
79
- # @see XmlPayload.pack
79
+ # @see XmlPayload#pack
80
80
  #
81
81
  # @return [Nokogiri::XML::Element] root element containing properties as child elements
82
82
  def to_xml
83
83
  entity_xml
84
84
  end
85
85
 
86
- # some of this is from Rails "Inflector.demodulize" and "Inflector.undersore"
86
+ # Makes an underscored, lowercase form of the class name
87
+ # @return [String] entity name
87
88
  def self.entity_name
88
89
  name.rpartition("::").last.tap do |word|
89
- word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
90
- word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
91
- word.tr!("-", "_")
90
+ word.gsub!(/(.)([A-Z])/, '\1_\2')
92
91
  word.downcase!
93
92
  end
94
93
  end
@@ -1,6 +1,5 @@
1
1
  require "faraday"
2
2
  require "faraday_middleware/response/follow_redirects"
3
- require "typhoeus/adapters/faraday"
4
3
 
5
4
  module DiasporaFederation
6
5
  # A wrapper for {https://github.com/lostisland/faraday Faraday} used for
@@ -32,7 +31,7 @@ module DiasporaFederation
32
31
 
33
32
  @connection = Faraday::Connection.new(options) do |builder|
34
33
  builder.use FaradayMiddleware::FollowRedirects, limit: 4
35
- builder.adapter :typhoeus
34
+ builder.adapter Faraday.default_adapter
36
35
  end
37
36
 
38
37
  @connection.headers["User-Agent"] = "DiasporaFederation/#{DiasporaFederation::VERSION}"
@@ -68,17 +68,27 @@ module DiasporaFederation
68
68
 
69
69
  private
70
70
 
71
+ def determine_xml_name(name, type, opts={})
72
+ raise ArgumentError, "xml_name is not supported for nested entities" if type != String && opts.has_key?(:xml_name)
73
+
74
+ if type == String
75
+ if opts.has_key? :xml_name
76
+ raise InvalidName, "invalid xml_name" unless name_valid?(opts[:xml_name])
77
+ opts[:xml_name]
78
+ else
79
+ name
80
+ end
81
+ elsif type.instance_of?(Array)
82
+ type.first.entity_name.to_sym
83
+ elsif type.ancestors.include?(Entity)
84
+ type.entity_name.to_sym
85
+ end
86
+ end
87
+
71
88
  def define_property(name, type, opts={})
72
89
  raise InvalidName unless name_valid?(name)
73
90
 
74
- xml_name = name
75
- if opts.has_key? :xml_name
76
- raise ArgumentError, "xml_name is not supported for nested entities" unless type == String
77
- xml_name = opts[:xml_name]
78
- raise InvalidName, "invalid xml_name" unless name_valid?(xml_name)
79
- end
80
-
81
- class_props << {name: name, xml_name: xml_name, type: type}
91
+ class_props << {name: name, xml_name: determine_xml_name(name, type, opts), type: type}
82
92
  default_props[name] = opts[:default] if opts.has_key? :default
83
93
 
84
94
  instance_eval { attr_reader name }
@@ -0,0 +1,17 @@
1
+ module DiasporaFederation
2
+ # This module contains a Diaspora*-specific implementation of parts of the
3
+ # {http://www.salmon-protocol.org/ Salmon Protocol}.
4
+ module Salmon
5
+ # XML namespace url
6
+ XMLNS = "https://joindiaspora.com/protocol"
7
+ end
8
+ end
9
+
10
+ require "base64"
11
+
12
+ require "diaspora_federation/salmon/aes"
13
+ require "diaspora_federation/salmon/exceptions"
14
+ require "diaspora_federation/salmon/xml_payload"
15
+ require "diaspora_federation/salmon/magic_envelope"
16
+ require "diaspora_federation/salmon/slap"
17
+ require "diaspora_federation/salmon/encrypted_slap"
@@ -0,0 +1,58 @@
1
+ module DiasporaFederation
2
+ module Salmon
3
+ # class for AES encryption and decryption
4
+ class AES
5
+ # OpenSSL aes cipher definition
6
+ CIPHER = "AES-256-CBC"
7
+
8
+ # generates a random AES key and initialization vector
9
+ # @return [Hash] { key: "...", iv: "..." }
10
+ def self.generate_key_and_iv
11
+ cipher = OpenSSL::Cipher.new(CIPHER)
12
+ {key: cipher.random_key, iv: cipher.random_iv}
13
+ end
14
+
15
+ # encrypts the given data with an AES cipher defined by the given key
16
+ # and iv and returns the resulting ciphertext base64 strict_encoded.
17
+ # @param [String] data plain input
18
+ # @param [String] key AES key
19
+ # @param [String] iv AES initialization vector
20
+ # @return [String] base64 encoded ciphertext
21
+ # @raise [ArgumentError] if any of the arguments is missing or not the correct type
22
+ def self.encrypt(data, key, iv)
23
+ raise ArgumentError unless data.instance_of?(String) &&
24
+ key.instance_of?(String) &&
25
+ iv.instance_of?(String)
26
+
27
+ cipher = OpenSSL::Cipher.new(CIPHER)
28
+ cipher.encrypt
29
+ cipher.key = key
30
+ cipher.iv = iv
31
+
32
+ ciphertext = cipher.update(data) + cipher.final
33
+
34
+ Base64.strict_encode64(ciphertext)
35
+ end
36
+
37
+ # decrypts the given ciphertext with an AES cipher defined by the given key
38
+ # and iv. +ciphertext+ is expected to be base64 encoded
39
+ # @param [String] ciphertext input data
40
+ # @param [String] key AES key
41
+ # @param [String] iv AES initialization vector
42
+ # @return [String] decrypted plain message
43
+ # @raise [ArgumentError] if any of the arguments is missing or not the correct type
44
+ def self.decrypt(ciphertext, key, iv)
45
+ raise ArgumentError unless ciphertext.instance_of?(String) &&
46
+ key.instance_of?(String) &&
47
+ iv.instance_of?(String)
48
+
49
+ decipher = OpenSSL::Cipher.new(CIPHER)
50
+ decipher.decrypt
51
+ decipher.key = key
52
+ decipher.iv = iv
53
+
54
+ decipher.update(Base64.decode64(ciphertext)) + decipher.final
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,187 @@
1
+ module DiasporaFederation
2
+ module Salmon
3
+ # +EncryptedSlap+ provides class methods for generating and parsing encrypted
4
+ # Slaps. (In principle the same as {Slap}, but with encryption.)
5
+ #
6
+ # The basic encryption mechanism used here is based on the knowledge that
7
+ # asymmetrical encryption is slow and symmetrical encryption is fast. Keeping in
8
+ # mind that a message we want to de-/encrypt may greatly vary in length,
9
+ # performance considerations must play a part of this scheme.
10
+ #
11
+ # A Diaspora*-flavored encrypted magic-enveloped XML message looks like the following:
12
+ #
13
+ # <?xml version="1.0" encoding="UTF-8"?>
14
+ # <diaspora xmlns="https://joindiaspora.com/protocol" xmlns:me="http://salmon-protocol.org/ns/magic-env">
15
+ # <encrypted_header>{encrypted_header}</encrypted_header>
16
+ # {magic_envelope with encrypted data}
17
+ # </diaspora>
18
+ #
19
+ # The encrypted header is encoded in JSON like this (when in plain text):
20
+ #
21
+ # {
22
+ # "aes_key" => "...",
23
+ # "ciphertext" => "..."
24
+ # }
25
+ #
26
+ # +aes_key+ is encrypted using the recipients public key, and contains the AES
27
+ # +key+ and +iv+ used to encrypt the +ciphertext+ also encoded as JSON.
28
+ #
29
+ # {
30
+ # "key" => "...",
31
+ # "iv" => "..."
32
+ # }
33
+ #
34
+ # +ciphertext+, once decrypted, contains the +author_id+, +aes_key+ and +iv+
35
+ # relevant to the decryption of the data in the magic_envelope and the
36
+ # verification of its signature.
37
+ #
38
+ # The decrypted cyphertext has this XML structure:
39
+ #
40
+ # <decrypted_header>
41
+ # <iv>{iv}</iv>
42
+ # <aes_key>{aes_key}</aes_key>
43
+ # <author_id>{author_id}</author_id>
44
+ # </decrypted_header>
45
+ #
46
+ # Finally, before decrypting the magic envelope payload, the signature should
47
+ # first be verified.
48
+ #
49
+ # @example Generating an encrypted Salmon Slap
50
+ # author_id = "author@pod.example.tld"
51
+ # author_privkey = however_you_retrieve_the_authors_private_key(author_id)
52
+ # recipient_pubkey = however_you_retrieve_the_recipients_public_key()
53
+ # entity = YourEntity.new(attr: "val")
54
+ #
55
+ # slap_xml = EncryptedSlap.generate_xml(author_id, author_privkey, entity, recipient_pubkey)
56
+ #
57
+ # @example Parsing a Salmon Slap
58
+ # recipient_privkey = however_you_retrieve_the_recipients_private_key()
59
+ # slap = EncryptedSlap.from_xml(slap_xml, recipient_privkey)
60
+ # author_pubkey = however_you_retrieve_the_authors_public_key(slap.author_id)
61
+ #
62
+ # entity = slap.entity(author_pubkey)
63
+ #
64
+ class EncryptedSlap
65
+ # Creates a Slap instance from the data within the given XML string
66
+ # containing an encrypted payload.
67
+ #
68
+ # @param [String] slap_xml encrypted Salmon xml
69
+ # @param [OpenSSL::PKey::RSA] pkey recipient private_key for decryption
70
+ #
71
+ # @return [Slap] new Slap instance
72
+ #
73
+ # @raise [ArgumentError] if any of the arguments is of the wrong type
74
+ # @raise [MissingHeader] if the +encrypted_header+ element is missing in the XML
75
+ # @raise [MissingMagicEnvelope] if the +me:env+ element is missing in the XML
76
+ def self.from_xml(slap_xml, pkey)
77
+ raise ArgumentError unless slap_xml.instance_of?(String) && pkey.instance_of?(OpenSSL::PKey::RSA)
78
+ doc = Nokogiri::XML::Document.parse(slap_xml)
79
+
80
+ Slap.new.tap do |slap|
81
+ header_elem = doc.at_xpath("d:diaspora/d:encrypted_header", Slap::NS)
82
+ raise MissingHeader if header_elem.nil?
83
+ header = header_data(header_elem.content, pkey)
84
+ slap.author_id = header[:author_id]
85
+ slap.cipher_params = {key: Base64.decode64(header[:aes_key]), iv: Base64.decode64(header[:iv])}
86
+
87
+ slap.add_magic_env_from_doc(doc)
88
+ end
89
+ end
90
+
91
+ # Creates an encrypted Salmon Slap and returns the XML string.
92
+ #
93
+ # @param [String] author_id Diaspora* handle of the author
94
+ # @param [OpenSSL::PKey::RSA] pkey sender private key for signing the magic envelope
95
+ # @param [Entity] entity payload
96
+ # @param [OpenSSL::PKey::RSA] pubkey recipient public key for encrypting the AES key
97
+ # @return [String] Salmon XML string
98
+ # @raise [ArgumentError] if any of the arguments is of the wrong type
99
+ def self.generate_xml(author_id, pkey, entity, pubkey)
100
+ raise ArgumentError unless author_id.instance_of?(String) &&
101
+ pkey.instance_of?(OpenSSL::PKey::RSA) &&
102
+ entity.is_a?(Entity) &&
103
+ pubkey.instance_of?(OpenSSL::PKey::RSA)
104
+
105
+ Slap.build_xml do |xml|
106
+ magic_envelope = MagicEnvelope.new(pkey, entity)
107
+ envelope_key = magic_envelope.encrypt!
108
+
109
+ encrypted_header(author_id, envelope_key, pubkey, xml)
110
+ magic_envelope.envelop(xml)
111
+ end
112
+ end
113
+
114
+ # decrypts and reads the data from the encrypted XML header
115
+ # @param [String] data base64 encoded, encrypted header data
116
+ # @param [OpenSSL::PKey::RSA] pkey private key for decryption
117
+ # @return [Hash] { iv: "...", aes_key: "...", author_id: "..." }
118
+ def self.header_data(data, pkey)
119
+ header_elem = decrypt_header(data, pkey)
120
+ raise InvalidHeader unless header_elem.name == "decrypted_header"
121
+
122
+ iv = header_elem.at_xpath("iv").content
123
+ key = header_elem.at_xpath("aes_key").content
124
+ author_id = header_elem.at_xpath("author_id").content
125
+
126
+ {iv: iv, aes_key: key, author_id: author_id}
127
+ end
128
+ private_class_method :header_data
129
+
130
+ # decrypts the xml header
131
+ # @param [String] data base64 encoded, encrypted header data
132
+ # @param [OpenSSL::PKey::RSA] pkey private key for decryption
133
+ # @return [Nokogiri::XML::Element] header xml document
134
+ def self.decrypt_header(data, pkey)
135
+ cipher_header = JSON.parse(Base64.decode64(data))
136
+ key = JSON.parse(pkey.private_decrypt(Base64.decode64(cipher_header["aes_key"])))
137
+
138
+ xml = AES.decrypt(cipher_header["ciphertext"], Base64.decode64(key["key"]), Base64.decode64(key["iv"]))
139
+ Nokogiri::XML::Document.parse(xml).root
140
+ end
141
+ private_class_method :decrypt_header
142
+
143
+ # encrypt the header xml with an AES cipher and encrypt the cipher params
144
+ # with the recipients public_key
145
+ # @param [String] author_id diaspora_handle
146
+ # @param [Hash] envelope_key envelope cipher params
147
+ # @param [OpenSSL::PKey::RSA] pubkey recipient public_key
148
+ # @param [Nokogiri::XML::Element] xml parent element for inserting in XML document
149
+ def self.encrypted_header(author_id, envelope_key, pubkey, xml)
150
+ data = header_xml(author_id, strict_base64_encode(envelope_key))
151
+ key = AES.generate_key_and_iv
152
+ ciphertext = AES.encrypt(data, key[:key], key[:iv])
153
+
154
+ json_key = JSON.generate(strict_base64_encode(key))
155
+ encrypted_key = Base64.strict_encode64(pubkey.public_encrypt(json_key))
156
+
157
+ json_header = JSON.generate(aes_key: encrypted_key, ciphertext: ciphertext)
158
+
159
+ xml.encrypted_header(Base64.strict_encode64(json_header))
160
+ end
161
+ private_class_method :encrypted_header
162
+
163
+ # generate the header xml string, including the author, aes_key and iv
164
+ # @param [String] author_id diaspora_handle of the author
165
+ # @param [Hash] envelope_key { key: "...", iv: "..." } (values in base64)
166
+ # @return [String] header XML string
167
+ def self.header_xml(author_id, envelope_key)
168
+ builder = Nokogiri::XML::Builder.new do |xml|
169
+ xml.decrypted_header {
170
+ xml.iv(envelope_key[:iv])
171
+ xml.aes_key(envelope_key[:key])
172
+ xml.author_id(author_id)
173
+ }
174
+ end
175
+ builder.to_xml.strip
176
+ end
177
+ private_class_method :header_xml
178
+
179
+ # @param [Hash] hash { key: "...", iv: "..." }
180
+ # @return [Hash] encoded hash: { key: "...", iv: "..." }
181
+ def self.strict_base64_encode(hash)
182
+ Hash[hash.map {|k, v| [k, Base64.strict_encode64(v)] }]
183
+ end
184
+ private_class_method :strict_base64_encode
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,50 @@
1
+ module DiasporaFederation
2
+ module Salmon
3
+ # Raised, if the element containing the Magic Envelope is missing from the XML
4
+ class MissingMagicEnvelope < RuntimeError
5
+ end
6
+
7
+ # Raised, if the element containing the author is empty.
8
+ class MissingAuthor < RuntimeError
9
+ end
10
+
11
+ # Raised, if the element containing the header is missing from the XML
12
+ class MissingHeader < RuntimeError
13
+ end
14
+
15
+ # Raised if the decrypted header has an unexpected XML structure
16
+ class InvalidHeader < RuntimeError
17
+ end
18
+
19
+ # Raised, if the Magic Envelope XML structure is malformed.
20
+ class InvalidEnvelope < RuntimeError
21
+ end
22
+
23
+ # Raised, if the calculated signature doesn't match the one contained in the
24
+ # Magic Envelope.
25
+ class InvalidSignature < RuntimeError
26
+ end
27
+
28
+ # Raised, if the parsed Magic Envelope specifies an unhandled algorithm.
29
+ class InvalidAlgorithm < RuntimeError
30
+ end
31
+
32
+ # Raised, if the parsed Magic Envelope specifies an unhandled encoding.
33
+ class InvalidEncoding < RuntimeError
34
+ 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
+ end
50
+ end