diaspora_federation 0.0.8 → 0.0.9

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