diff --git a/app/controllers/diaspora_federation/receive_controller.rb b/app/controllers/diaspora_federation/receive_controller.rb index 9b3c908..2e86b07 100644 --- a/app/controllers/diaspora_federation/receive_controller.rb +++ b/app/controllers/diaspora_federation/receive_controller.rb @@ -9,11 +9,12 @@ module DiasporaFederation # # POST /receive/public def public - logger.info "received a public message" - xml = CGI.unescape(params[:xml]) - logger.debug xml + legacy = request.content_type != "application/magic-envelope+xml" - DiasporaFederation.callbacks.trigger(:queue_public_receive, xml) + data = data_for_public_message(legacy) + logger.debug data + + DiasporaFederation.callbacks.trigger(:queue_public_receive, data, legacy) render nothing: true, status: 202 end @@ -22,19 +23,43 @@ module DiasporaFederation # # POST /receive/users/:guid def private - logger.info "received a private message for #{params[:guid]}" - xml = CGI.unescape(params[:xml]) - logger.debug xml + legacy = request.content_type != "application/json" - success = DiasporaFederation.callbacks.trigger(:queue_private_receive, params[:guid], xml) + data = data_for_private_message(legacy) + logger.debug data + + success = DiasporaFederation.callbacks.trigger(:queue_private_receive, params[:guid], data, legacy) render nothing: true, status: success ? 202 : 404 end private + # checks the xml parameter for legacy salmon slaps + # @deprecated def check_for_xml - render nothing: true, status: 422 if params[:xml].nil? + legacy_request = request.content_type.nil? || request.content_type == "application/x-www-form-urlencoded" + render nothing: true, status: 422 if params[:xml].nil? && legacy_request + end + + def data_for_public_message(legacy) + if legacy + logger.info "received a public salmon slap" + CGI.unescape(params[:xml]) + else + logger.info "received a public magic envelope" + request.body.read + end + end + + def data_for_private_message(legacy) + if legacy + logger.info "received a private salmon slap for #{params[:guid]}" + CGI.unescape(params[:xml]) + else + logger.info "received a private encrypted magic envelope for #{params[:guid]}" + request.body.read + end end end end diff --git a/lib/diaspora_federation.rb b/lib/diaspora_federation.rb index 5756e2c..a4bea52 100644 --- a/lib/diaspora_federation.rb +++ b/lib/diaspora_federation.rb @@ -155,12 +155,14 @@ module DiasporaFederation # # queue_public_receive # Queue a public salmon xml to process in background - # @param [String] xml salmon xml + # @param [String] data salmon slap xml or magic envelope xml + # @param [Boolean] legacy true if it is a legacy salmon slap, false if it is a magic envelope xml # # queue_private_receive # Queue a private salmon xml to process in background # @param [String] guid guid of the receiver person - # @param [String] xml salmon xml + # @param [String] data salmon slap xml or encrypted magic envelope json + # @param [Boolean] legacy true if it is a legacy salmon slap, false if it is a encrypted magic envelope json # @return [Boolean] true if successful, false if the user was not found # # save_entity_after_receive diff --git a/lib/diaspora_federation/federation.rb b/lib/diaspora_federation/federation.rb index 668ae1f..b8732fc 100644 --- a/lib/diaspora_federation/federation.rb +++ b/lib/diaspora_federation/federation.rb @@ -4,6 +4,5 @@ module DiasporaFederation end end -require "diaspora_federation/federation/exceptions" require "diaspora_federation/federation/receiver" require "diaspora_federation/federation/sender" diff --git a/lib/diaspora_federation/federation/exceptions.rb b/lib/diaspora_federation/federation/exceptions.rb deleted file mode 100644 index 437d0cb..0000000 --- a/lib/diaspora_federation/federation/exceptions.rb +++ /dev/null @@ -1,11 +0,0 @@ -module DiasporaFederation - module Federation - # Raised if failed to fetch a public key of the sender of the received message - class SenderKeyNotFound < RuntimeError - end - - # Raised if recipient private key is missing for a private receive - class RecipientKeyNotFound < RuntimeError - end - end -end diff --git a/lib/diaspora_federation/federation/receiver.rb b/lib/diaspora_federation/federation/receiver.rb index 416427d..d0975a6 100644 --- a/lib/diaspora_federation/federation/receiver.rb +++ b/lib/diaspora_federation/federation/receiver.rb @@ -1,25 +1,10 @@ module DiasporaFederation module Federation - # Common base for Private and Public receivers - # @see Receiver::Public - # @see Receiver::Private - class Receiver - # initializes a new Receiver for a salmon XML - # @param [String] salmon_xml the message salmon xml - def initialize(salmon_xml) - @salmon_xml = salmon_xml - end - - # Parse the salmon xml and send it to the +:save_entity_after_receive+ callback - def receive! - sender_id = slap.author_id - public_key = DiasporaFederation.callbacks.trigger(:fetch_public_key_by_diaspora_id, sender_id) - raise SenderKeyNotFound if public_key.nil? - DiasporaFederation.callbacks.trigger(:save_entity_after_receive, slap.entity(public_key)) - end + module Receiver end end end -require "diaspora_federation/federation/receiver/private" -require "diaspora_federation/federation/receiver/public" +require "diaspora_federation/federation/receiver/slap_receiver" +require "diaspora_federation/federation/receiver/private_slap_receiver" +require "diaspora_federation/federation/receiver/public_slap_receiver" diff --git a/lib/diaspora_federation/federation/receiver/private.rb b/lib/diaspora_federation/federation/receiver/private.rb deleted file mode 100644 index 2e5ea69..0000000 --- a/lib/diaspora_federation/federation/receiver/private.rb +++ /dev/null @@ -1,26 +0,0 @@ -module DiasporaFederation - module Federation - class Receiver - # Receiver::Private is used to receive private messages, which are addressed - # to a specific user, encrypted with his public key and packed using Salmon::EncryptedSlap - class Private < Receiver - # initializes a new Private Receiver for a salmon XML - # @param [String] salmon_xml the message salmon xml - # @param [OpenSSL::PKey::RSA] recipient_private_key recipient private key to decrypt the message - def initialize(salmon_xml, recipient_private_key) - super(salmon_xml) - raise RecipientKeyNotFound if recipient_private_key.nil? - @recipient_private_key = recipient_private_key - end - - protected - - # parses the encrypted slap xml - # @return [Salmon::EncryptedSlap] slap instance - def slap - @salmon ||= Salmon::EncryptedSlap.from_xml(@salmon_xml, @recipient_private_key) - end - end - end - end -end diff --git a/lib/diaspora_federation/federation/receiver/private_slap_receiver.rb b/lib/diaspora_federation/federation/receiver/private_slap_receiver.rb new file mode 100644 index 0000000..3f309ee --- /dev/null +++ b/lib/diaspora_federation/federation/receiver/private_slap_receiver.rb @@ -0,0 +1,27 @@ +module DiasporaFederation + module Federation + module Receiver + # This is used to receive private messages, which are addressed to a specific user, + # encrypted with his public key and packed using {Salmon::EncryptedSlap}. + # @deprecated + class PrivateSlapReceiver < SlapReceiver + # initializes a new Private Receiver for a salmon slap XML + # @param [String] slap_xml the message salmon slap xml + # @param [OpenSSL::PKey::RSA] recipient_private_key recipient private key to decrypt the message + def initialize(slap_xml, recipient_private_key) + super(slap_xml) + raise ArgumentError, "no recipient key provided" unless recipient_private_key.instance_of?(OpenSSL::PKey::RSA) + @recipient_private_key = recipient_private_key + end + + protected + + # parses the encrypted slap xml + # @return [Salmon::EncryptedSlap] slap instance + def slap + @slap ||= Salmon::EncryptedSlap.from_xml(@slap_xml, @recipient_private_key) + end + end + end + end +end diff --git a/lib/diaspora_federation/federation/receiver/public.rb b/lib/diaspora_federation/federation/receiver/public.rb deleted file mode 100644 index 252ef22..0000000 --- a/lib/diaspora_federation/federation/receiver/public.rb +++ /dev/null @@ -1,17 +0,0 @@ -module DiasporaFederation - module Federation - class Receiver - # Receiver::Public is used to receive public messages, which are not addressed to a specific user, unencrypted - # and packed using Salmon::Slap - class Public < Receiver - protected - - # parses the public slap xml - # @return [Salmon::Slap] slap instance - def slap - @salmon ||= Salmon::Slap.from_xml(@salmon_xml) - end - end - end - end -end diff --git a/lib/diaspora_federation/federation/receiver/public_slap_receiver.rb b/lib/diaspora_federation/federation/receiver/public_slap_receiver.rb new file mode 100644 index 0000000..86ae37a --- /dev/null +++ b/lib/diaspora_federation/federation/receiver/public_slap_receiver.rb @@ -0,0 +1,18 @@ +module DiasporaFederation + module Federation + module Receiver + # This is used to receive public messages, which are not addressed to + # a specific user, unencrypted and packed using {Salmon::Slap}. + # @deprecated + class PublicSlapReceiver < SlapReceiver + protected + + # parses the public slap xml + # @return [Salmon::Slap] slap instance + def slap + @slap ||= Salmon::Slap.from_xml(@slap_xml) + end + end + end + end +end diff --git a/lib/diaspora_federation/federation/receiver/slap_receiver.rb b/lib/diaspora_federation/federation/receiver/slap_receiver.rb new file mode 100644 index 0000000..0206a44 --- /dev/null +++ b/lib/diaspora_federation/federation/receiver/slap_receiver.rb @@ -0,0 +1,25 @@ +module DiasporaFederation + module Federation + module Receiver + # Common base for Private and Public receivers + # @see PublicSlapReceiver + # @see PrivateSlapReceiver + # @deprecated + class SlapReceiver + # initializes a new SlapReceiver for a salmon slap XML + # @param [String] slap_xml the message salmon xml + def initialize(slap_xml) + @slap_xml = slap_xml + end + + # Parse the salmon xml and send it to the +:save_entity_after_receive+ callback + def receive! + sender_id = slap.author_id + public_key = DiasporaFederation.callbacks.trigger(:fetch_public_key_by_diaspora_id, sender_id) + raise Salmon::SenderKeyNotFound if public_key.nil? + DiasporaFederation.callbacks.trigger(:save_entity_after_receive, slap.entity(public_key)) + end + end + end + end +end diff --git a/lib/diaspora_federation/salmon.rb b/lib/diaspora_federation/salmon.rb index dd8f288..945bc53 100644 --- a/lib/diaspora_federation/salmon.rb +++ b/lib/diaspora_federation/salmon.rb @@ -13,5 +13,6 @@ require "diaspora_federation/salmon/aes" require "diaspora_federation/salmon/exceptions" require "diaspora_federation/salmon/xml_payload" require "diaspora_federation/salmon/magic_envelope" +require "diaspora_federation/salmon/encrypted_magic_envelope" require "diaspora_federation/salmon/slap" require "diaspora_federation/salmon/encrypted_slap" diff --git a/lib/diaspora_federation/salmon/encrypted_magic_envelope.rb b/lib/diaspora_federation/salmon/encrypted_magic_envelope.rb new file mode 100644 index 0000000..a10c8dd --- /dev/null +++ b/lib/diaspora_federation/salmon/encrypted_magic_envelope.rb @@ -0,0 +1,61 @@ +module DiasporaFederation + module Salmon + # This is a simple crypt-wrapper for {MagicEnvelope}. + # + # The wrapper is JSON with the following structure: + # + # { + # "aes_key": "...", + # "encrypted_magic_envelope": "..." + # } + # + # +aes_key+ is encrypted using the recipients public key, and contains the AES + # +key+ and +iv+ as JSON: + # + # { + # "key": "...", + # "iv": "..." + # } + # + # +encrypted_magic_envelope+ is encrypted using the +key+ and +iv+ from +aes_key+. + # Once decrypted it contains the {MagicEnvelope} xml: + # + # + # ... + # + # + # All JSON-values (+aes_key+, +encrypted_magic_envelope+, +key+ and +iv+) are + # base64 encoded. + module EncryptedMagicEnvelope + # Generates a new random AES key and encrypts the {MagicEnvelope} with it. + # Then encrypts the AES key with the receivers public key. + # @param [Nokogiri::XML::Element] magic_env XML root node of a magic envelope + # @param [OpenSSL::PKey::RSA] pubkey recipient public_key + # @return [String] json string + def self.encrypt(magic_env, pubkey) + key = AES.generate_key_and_iv + encrypted_env = AES.encrypt(magic_env.to_xml, key[:key], key[:iv]) + + encoded_key = Hash[key.map {|k, v| [k, Base64.strict_encode64(v)] }] + encrypted_key = Base64.strict_encode64(pubkey.public_encrypt(JSON.generate(encoded_key))) + + JSON.generate(aes_key: encrypted_key, encrypted_magic_envelope: encrypted_env) + end + + # Decrypts the AES key with the private key of the receiver and decrypts the + # encrypted {MagicEnvelope} with it. + # @param [String] encrypted_env json string with aes_key and encrypted_magic_envelope + # @param [OpenSSL::PKey::RSA] privkey private key for decryption + # @return [Nokogiri::XML::Element] decrypted magic envelope xml + def self.decrypt(encrypted_env, privkey) + encrypted_json = JSON.parse(encrypted_env) + + encoded_key = JSON.parse(privkey.private_decrypt(Base64.decode64(encrypted_json["aes_key"]))) + key = Hash[encoded_key.map {|k, v| [k, Base64.decode64(v)] }] + + xml = AES.decrypt(encrypted_json["encrypted_magic_envelope"], key["key"], key["iv"]) + Nokogiri::XML::Document.parse(xml).root + end + end + end +end diff --git a/lib/diaspora_federation/salmon/encrypted_slap.rb b/lib/diaspora_federation/salmon/encrypted_slap.rb index 32d2bcf..0c8e1ff 100644 --- a/lib/diaspora_federation/salmon/encrypted_slap.rb +++ b/lib/diaspora_federation/salmon/encrypted_slap.rb @@ -63,6 +63,7 @@ module DiasporaFederation # # entity = slap.entity(author_pubkey) # + # @deprecated class EncryptedSlap < Slap # the key and iv if it is an encrypted slap # @param [Hash] value hash containing the key and iv @@ -113,9 +114,9 @@ module DiasporaFederation EncryptedSlap.new.tap do |slap| slap.author_id = author_id - magic_envelope = MagicEnvelope.new(privkey, entity) + magic_envelope = MagicEnvelope.new(entity) slap.cipher_params = magic_envelope.encrypt! - slap.magic_envelope_xml = magic_envelope.envelop + slap.magic_envelope_xml = magic_envelope.envelop(privkey, author_id) end end @@ -172,11 +173,11 @@ module DiasporaFederation # @param [OpenSSL::PKey::RSA] pubkey recipient public_key # @return [String] encrypted base64 encoded header def encrypted_header(author_id, envelope_key, pubkey) - encoded_key = Hash[envelope_key.map {|k, v| [k, Base64.strict_encode64(v)] }] - data = header_xml(author_id, encoded_key) - ciphertext = AES.encrypt(data, envelope_key[:key], envelope_key[:iv]) + data = header_xml(author_id, strict_base64_encode(envelope_key)) + header_key = AES.generate_key_and_iv + ciphertext = AES.encrypt(data, header_key[:key], header_key[:iv]) - json_key = JSON.generate(encoded_key) + json_key = JSON.generate(strict_base64_encode(header_key)) encrypted_key = Base64.strict_encode64(pubkey.public_encrypt(json_key)) json_header = JSON.generate(aes_key: encrypted_key, ciphertext: ciphertext) @@ -197,6 +198,12 @@ module DiasporaFederation } }.to_xml.strip end + + # @param [Hash] hash { key: "...", iv: "..." } + # @return [Hash] encoded hash: { key: "...", iv: "..." } + def strict_base64_encode(hash) + Hash[hash.map {|k, v| [k, Base64.strict_encode64(v)] }] + end end end end diff --git a/lib/diaspora_federation/salmon/exceptions.rb b/lib/diaspora_federation/salmon/exceptions.rb index 649b9a6..accc08b 100644 --- a/lib/diaspora_federation/salmon/exceptions.rb +++ b/lib/diaspora_federation/salmon/exceptions.rb @@ -1,21 +1,29 @@ module DiasporaFederation module Salmon # Raised, if the element containing the Magic Envelope is missing from the XML + # @deprecated class MissingMagicEnvelope < RuntimeError end # Raised, if the element containing the author is empty. + # @deprecated class MissingAuthor < RuntimeError end # Raised, if the element containing the header is missing from the XML + # @deprecated class MissingHeader < RuntimeError end # Raised if the decrypted header has an unexpected XML structure + # @deprecated class InvalidHeader < RuntimeError end + # Raised, if failed to fetch the public key of the sender of the received message + class SenderKeyNotFound < RuntimeError + end + # Raised, if the Magic Envelope XML structure is malformed. class InvalidEnvelope < RuntimeError end diff --git a/lib/diaspora_federation/salmon/magic_envelope.rb b/lib/diaspora_federation/salmon/magic_envelope.rb index bfe84ff..94509cc 100644 --- a/lib/diaspora_federation/salmon/magic_envelope.rb +++ b/lib/diaspora_federation/salmon/magic_envelope.rb @@ -13,7 +13,7 @@ module DiasporaFederation # {data} # base64url # RSA-SHA256 - # {signature} + # {signature} # # # When parsing the XML of an incoming Magic Envelope {MagicEnvelope.unenvelop} @@ -21,9 +21,6 @@ module DiasporaFederation # # @see http://salmon-protocol.googlecode.com/svn/trunk/draft-panzer-magicsig-01.html class MagicEnvelope - # returns the payload (only used for testing purposes) - attr_reader :payload - # encoding used for the payload data ENCODING = "base64url".freeze @@ -41,34 +38,31 @@ module DiasporaFederation # Creates a new instance of MagicEnvelope. # - # @param [OpenSSL::PKey::RSA] rsa_privkey private key used for signing # @param [Entity] payload Entity instance # @raise [ArgumentError] if either argument is not of the right type - def initialize(rsa_privkey, payload) - raise ArgumentError unless rsa_privkey.instance_of?(OpenSSL::PKey::RSA) && - payload.is_a?(Entity) + def initialize(payload) + raise ArgumentError unless payload.is_a?(Entity) - @rsa_privkey = rsa_privkey @payload = XmlPayload.pack(payload).to_xml.strip end # Builds the XML structure for the magic envelope, inserts the {ENCODING} # encoded data and signs the envelope using {DIGEST}. # + # @param [OpenSSL::PKey::RSA] privkey private key used for signing + # @param [String] sender_id diaspora-ID of the sender # @return [Nokogiri::XML::Element] XML root node - def envelop - env_doc = Nokogiri::XML::DocumentFragment.new(Nokogiri::XML::Document.new) - Nokogiri::XML::Element.new("me:env", env_doc).tap do |env| - env << Nokogiri::XML::Element.new("me:data", env_doc).tap {|node| - node.content = Base64.urlsafe_encode64(@payload) - node["type"] = DATA_TYPE + def envelop(privkey, sender_id) + raise ArgumentError unless privkey.instance_of?(OpenSSL::PKey::RSA) && sender_id.is_a?(String) + + build_xml {|xml| + xml["me"].env("xmlns:me" => XMLNS) { + xml["me"].data(Base64.urlsafe_encode64(@payload), type: DATA_TYPE) + xml["me"].encoding(ENCODING) + xml["me"].alg(ALGORITHM) + xml["me"].sig(Base64.urlsafe_encode64(sign(privkey)), key_id: Base64.urlsafe_encode64(sender_id)) } - env << Nokogiri::XML::Element.new("me:encoding", env_doc).tap {|node| node.content = ENCODING } - env << Nokogiri::XML::Element.new("me:alg", env_doc).tap {|node| node.content = ALGORITHM } - env << Nokogiri::XML::Element.new("me:sig", env_doc).tap {|node| - node.content = Base64.urlsafe_encode64(signature) - } - end + } end # Encrypts the payload with a new, random AES cipher and returns the cipher @@ -107,9 +101,8 @@ module DiasporaFederation # @raise [InvalidSignature] if the signature can't be verified # @raise [InvalidEncoding] if the data is wrongly encoded # @raise [InvalidAlgorithm] if the algorithm used doesn't match - def self.unenvelop(magic_env, rsa_pubkey, cipher_params=nil) - raise ArgumentError unless magic_env.instance_of?(Nokogiri::XML::Element) && - rsa_pubkey.instance_of?(OpenSSL::PKey::RSA) + def self.unenvelop(magic_env, rsa_pubkey=nil, cipher_params=nil) + raise ArgumentError unless magic_env.instance_of?(Nokogiri::XML::Element) raise InvalidEnvelope unless envelope_valid?(magic_env) raise InvalidSignature unless signature_valid?(magic_env, rsa_pubkey) @@ -124,12 +117,24 @@ module DiasporaFederation private + # Builds the xml root node of the magic envelope. + # + # @yield [xml] Invokes the block with the + # {http://www.rubydoc.info/gems/nokogiri/Nokogiri/XML/Builder Nokogiri::XML::Builder} + # @return [Nokogiri::XML::Element] XML root node + def build_xml + Nokogiri::XML::Builder.new(encoding: "UTF-8") {|xml| + yield xml + }.doc.root + end + # create the signature for all fields according to specification # + # @param [OpenSSL::PKey::RSA] privkey private key used for signing # @return [String] the signature - def signature + def sign(privkey) subject = MagicEnvelope.send(:sig_subject, [@payload, DATA_TYPE, ENCODING, ALGORITHM]) - @rsa_privkey.sign(DIGEST, subject) + privkey.sign(DIGEST, subject) end # @param [Nokogiri::XML::Element] env magic envelope XML @@ -137,14 +142,12 @@ module DiasporaFederation (env.instance_of?(Nokogiri::XML::Element) && env.name == "env" && !env.at_xpath("me:data").content.empty? && - !env.at_xpath("me:encoding").content.empty? && - !env.at_xpath("me:alg").content.empty? && !env.at_xpath("me:sig").content.empty?) end private_class_method :envelope_valid? # @param [Nokogiri::XML::Element] env magic envelope XML - # @param [OpenSSL::PKey::RSA] pubkey public key + # @param [OpenSSL::PKey::RSA] pubkey public key or nil # @return [Boolean] def self.signature_valid?(env, pubkey) subject = sig_subject([Base64.urlsafe_decode64(env.at_xpath("me:data").content), @@ -152,11 +155,27 @@ module DiasporaFederation env.at_xpath("me:encoding").content, env.at_xpath("me:alg").content]) + sender_key = pubkey || sender_key(env) + sig = Base64.urlsafe_decode64(env.at_xpath("me:sig").content) - pubkey.verify(DIGEST, sig, subject) + sender_key.verify(DIGEST, sig, subject) end private_class_method :signature_valid? + # reads the +key_id+ from the magic envelope + # @param [Nokogiri::XML::Element] env magic envelope XML + # @return [OpenSSL::PKey::RSA] sender public key + def self.sender_key(env) + key_id = env.at_xpath("me:sig")["key_id"] + raise InvalidEnvelope, "no key_id" unless key_id # TODO: move to `envelope_valid?` + sender = Base64.urlsafe_decode64(key_id) + + sender_key = DiasporaFederation.callbacks.trigger(:fetch_public_key_by_diaspora_id, sender) + raise SenderKeyNotFound unless sender_key + sender_key + end + private_class_method :sender_key + # constructs the signature subject. # the given array should consist of the data, data_type (mimetype), encoding # and the algorithm diff --git a/lib/diaspora_federation/salmon/slap.rb b/lib/diaspora_federation/salmon/slap.rb index aa3ead4..57d4630 100644 --- a/lib/diaspora_federation/salmon/slap.rb +++ b/lib/diaspora_federation/salmon/slap.rb @@ -25,6 +25,8 @@ module DiasporaFederation # author_pubkey = however_you_retrieve_the_authors_public_key(slap.author_id) # # entity = slap.entity(author_pubkey) + # + # @deprecated class Slap # the author of the slap # @overload author_id @@ -90,7 +92,7 @@ module DiasporaFederation xml.author_id(author_id) } - xml.parent << MagicEnvelope.new(privkey, entity).envelop + xml.parent << MagicEnvelope.new(entity).envelop(privkey, author_id) end end diff --git a/spec/controllers/diaspora_federation/receive_controller_spec.rb b/spec/controllers/diaspora_federation/receive_controller_spec.rb index 3131280..5acbe14 100644 --- a/spec/controllers/diaspora_federation/receive_controller_spec.rb +++ b/spec/controllers/diaspora_federation/receive_controller_spec.rb @@ -3,55 +3,108 @@ module DiasporaFederation routes { DiasporaFederation::Engine.routes } describe "POST #public" do - it "returns a 422 if no xml is passed" do - post :public - expect(response.code).to eq("422") + context "legacy salmon slap" do + it "returns a 422 if no xml is passed" do + post :public + expect(response.code).to eq("422") + end + + it "returns a 422 if no xml is passed with content-type application/x-www-form-urlencoded" do + @request.env["CONTENT_TYPE"] = "application/x-www-form-urlencoded" + post :public + expect(response.code).to eq("422") + end + + it "returns a 202 if queued correctly" do + expect(DiasporaFederation.callbacks).to receive(:trigger).with(:queue_public_receive, "", true) + + post :public, xml: "" + expect(response.code).to eq("202") + end + + it "unescapes the xml before sending it to the callback" do + expect(DiasporaFederation.callbacks).to receive(:trigger).with(:queue_public_receive, "", true) + + post :public, xml: CGI.escape("") + end end - it "returns a 202 if queued correctly" do - expect(DiasporaFederation.callbacks).to receive(:trigger).with(:queue_public_receive, "") + context "magic envelope" do + before do + @request.env["CONTENT_TYPE"] = "application/magic-envelope+xml" + end - post :public, xml: "" - expect(response.code).to eq("202") - end + it "returns a 202 if queued correctly" do + expect(DiasporaFederation.callbacks).to receive(:trigger).with(:queue_public_receive, "", false) - it "unescapes the xml before sending it to the callback" do - expect(DiasporaFederation.callbacks).to receive(:trigger).with(:queue_public_receive, "") - - post :public, xml: CGI.escape("") + post :public, "" + expect(response.code).to eq("202") + end end end describe "POST #private" do - it "return a 404 if not queued successfully (unknown user guid)" do - expect(DiasporaFederation.callbacks).to receive(:trigger).with( - :queue_private_receive, "any-guid", "" - ).and_return(false) + context "legacy salmon slap" do + it "return a 404 if not queued successfully (unknown user guid)" do + expect(DiasporaFederation.callbacks).to receive(:trigger).with( + :queue_private_receive, "any-guid", "", true + ).and_return(false) - post :private, guid: "any-guid", xml: "" - expect(response.code).to eq("404") + post :private, guid: "any-guid", xml: "" + expect(response.code).to eq("404") + end + + it "returns a 422 if no xml is passed" do + post :private, guid: "any-guid" + expect(response.code).to eq("422") + end + + it "returns a 422 if no xml is passed with content-type application/x-www-form-urlencoded" do + @request.env["CONTENT_TYPE"] = "application/x-www-form-urlencoded" + post :private, guid: "any-guid" + expect(response.code).to eq("422") + end + + it "returns a 202 if the callback returned true" do + expect(DiasporaFederation.callbacks).to receive(:trigger).with( + :queue_private_receive, "any-guid", "", true + ).and_return(true) + + post :private, guid: "any-guid", xml: "" + expect(response.code).to eq("202") + end + + it "unescapes the xml before sending it to the callback" do + expect(DiasporaFederation.callbacks).to receive(:trigger).with( + :queue_private_receive, "any-guid", "", true + ).and_return(true) + + post :private, guid: "any-guid", xml: CGI.escape("") + end end - it "returns a 422 if no xml is passed" do - post :private, guid: "any-guid" - expect(response.code).to eq("422") - end + context "encrypted magic envelope" do + before do + @request.env["CONTENT_TYPE"] = "application/json" + end - it "returns a 202 if the callback returned true" do - expect(DiasporaFederation.callbacks).to receive(:trigger).with( - :queue_private_receive, "any-guid", "" - ).and_return(true) + it "return a 404 if not queued successfully (unknown user guid)" do + expect(DiasporaFederation.callbacks).to receive(:trigger).with( + :queue_private_receive, "any-guid", "{\"aes_key\": \"key\", \"encrypted_magic_envelope\": \"env\"}", false + ).and_return(false) - post :private, guid: "any-guid", xml: "" - expect(response.code).to eq("202") - end + post :private, "{\"aes_key\": \"key\", \"encrypted_magic_envelope\": \"env\"}", guid: "any-guid" + expect(response.code).to eq("404") + end - it "unescapes the xml before sending it to the callback" do - expect(DiasporaFederation.callbacks).to receive(:trigger).with( - :queue_private_receive, "any-guid", "" - ).and_return(true) + it "returns a 202 if the callback returned true" do + expect(DiasporaFederation.callbacks).to receive(:trigger).with( + :queue_private_receive, "any-guid", "{\"aes_key\": \"key\", \"encrypted_magic_envelope\": \"env\"}", false + ).and_return(true) - post :private, guid: "any-guid", xml: CGI.escape("") + post :private, "{\"aes_key\": \"key\", \"encrypted_magic_envelope\": \"env\"}", guid: "any-guid" + expect(response.code).to eq("202") + end end end end diff --git a/spec/lib/diaspora_federation/federation/receiver/private_spec.rb b/spec/lib/diaspora_federation/federation/receiver/private_slap_receiver_spec.rb similarity index 89% rename from spec/lib/diaspora_federation/federation/receiver/private_spec.rb rename to spec/lib/diaspora_federation/federation/receiver/private_slap_receiver_spec.rb index f376445..6273fbb 100644 --- a/spec/lib/diaspora_federation/federation/receiver/private_spec.rb +++ b/spec/lib/diaspora_federation/federation/receiver/private_slap_receiver_spec.rb @@ -1,5 +1,5 @@ module DiasporaFederation - describe Federation::Receiver::Private do + describe Federation::Receiver::PrivateSlapReceiver do let(:sender_id) { FactoryGirl.generate(:diaspora_id) } let(:sender_key) { OpenSSL::PKey::RSA.generate(1024) } let(:recipient_key) { OpenSSL::PKey::RSA.generate(1024) } @@ -25,13 +25,13 @@ module DiasporaFederation expect { described_class.new(xml, recipient_key).receive! - }.to raise_error Federation::SenderKeyNotFound + }.to raise_error Salmon::SenderKeyNotFound end it "raises when recipient private key is not available" do expect { described_class.new(xml, nil).receive! - }.to raise_error Federation::RecipientKeyNotFound + }.to raise_error ArgumentError, "no recipient key provided" end it "raises when bad xml was supplied" do diff --git a/spec/lib/diaspora_federation/federation/receiver/public_spec.rb b/spec/lib/diaspora_federation/federation/receiver/public_slap_receiver_spec.rb similarity index 91% rename from spec/lib/diaspora_federation/federation/receiver/public_spec.rb rename to spec/lib/diaspora_federation/federation/receiver/public_slap_receiver_spec.rb index b2bad2e..18b249a 100644 --- a/spec/lib/diaspora_federation/federation/receiver/public_spec.rb +++ b/spec/lib/diaspora_federation/federation/receiver/public_slap_receiver_spec.rb @@ -1,5 +1,5 @@ module DiasporaFederation - describe Federation::Receiver::Public do + describe Federation::Receiver::PublicSlapReceiver do let(:sender_id) { FactoryGirl.generate(:diaspora_id) } let(:sender_key) { OpenSSL::PKey::RSA.generate(1024) } let(:xml) { @@ -27,7 +27,7 @@ module DiasporaFederation expect { described_class.new(xml).receive! - }.to raise_error Federation::SenderKeyNotFound + }.to raise_error Salmon::SenderKeyNotFound end it "raises when bad xml was supplied" do diff --git a/spec/lib/diaspora_federation/salmon/encrypted_magic_envelope_spec.rb b/spec/lib/diaspora_federation/salmon/encrypted_magic_envelope_spec.rb new file mode 100644 index 0000000..6c39196 --- /dev/null +++ b/spec/lib/diaspora_federation/salmon/encrypted_magic_envelope_spec.rb @@ -0,0 +1,54 @@ +module DiasporaFederation + describe Salmon::EncryptedMagicEnvelope do + let(:sender_id) { FactoryGirl.generate(:diaspora_id) } + let(:sender_key) { OpenSSL::PKey::RSA.generate(512) } # use small key for speedy specs + let(:entity) { Entities::TestEntity.new(test: "abcd") } + let(:magic_env) { Salmon::MagicEnvelope.new(entity).envelop(sender_key, sender_id) } + + let(:privkey) { OpenSSL::PKey::RSA.generate(1024) } # use small key for speedy specs + + describe ".encrypt" do + it "creates the json correctly" do + encrypted = Salmon::EncryptedMagicEnvelope.encrypt(magic_env, privkey.public_key) + + expect(JSON.parse(encrypted)).to include("aes_key", "encrypted_magic_envelope") + end + + it "encrypts the aes_key correctly" do + encrypted = Salmon::EncryptedMagicEnvelope.encrypt(magic_env, privkey.public_key) + + json = JSON.parse(encrypted) + aes_key = JSON.parse(privkey.private_decrypt(Base64.decode64(json["aes_key"]))) + + expect(aes_key).to include("key", "iv") + end + + it "encrypts the magic_envelope correctly" do + encrypted = Salmon::EncryptedMagicEnvelope.encrypt(magic_env, privkey.public_key) + + json = JSON.parse(encrypted) + aes_key = JSON.parse(privkey.private_decrypt(Base64.decode64(json["aes_key"]))) + key = Hash[aes_key.map {|k, v| [k, Base64.decode64(v)] }] + + xml = Salmon::AES.decrypt(json["encrypted_magic_envelope"], key["key"], key["iv"]) + + expect(Nokogiri::XML::Document.parse(xml).root.to_xml).to eq(magic_env.to_xml) + end + end + + describe ".decrypt" do + let(:encrypted_env) { Salmon::EncryptedMagicEnvelope.encrypt(magic_env, privkey.public_key) } + + it "returns the magic envelope xml" do + decrypted = Salmon::EncryptedMagicEnvelope.decrypt(encrypted_env, privkey) + + expect(decrypted.name).to eq("env") + + expect(decrypted.xpath("me:data")).to have(1).item + expect(decrypted.xpath("me:encoding")).to have(1).item + expect(decrypted.xpath("me:alg")).to have(1).item + expect(decrypted.xpath("me:sig")).to have(1).item + end + end + end +end diff --git a/spec/lib/diaspora_federation/salmon/encrypted_slap_spec.rb b/spec/lib/diaspora_federation/salmon/encrypted_slap_spec.rb index 40ec4f0..fd4c163 100644 --- a/spec/lib/diaspora_federation/salmon/encrypted_slap_spec.rb +++ b/spec/lib/diaspora_federation/salmon/encrypted_slap_spec.rb @@ -67,16 +67,23 @@ module DiasporaFederation doc1 = Nokogiri::XML::Document.parse(slap.generate_xml(recipient_key.public_key)) enc_header1 = doc1.at_xpath("d:diaspora/d:encrypted_header", ns).content cipher_header1 = JSON.parse(Base64.decode64(enc_header1)) - key_json1 = recipient_key.private_decrypt(Base64.decode64(cipher_header1["aes_key"])) + header_key1 = JSON.parse(recipient_key.private_decrypt(Base64.decode64(cipher_header1["aes_key"]))) + decrypted_header1 = Salmon::AES.decrypt(cipher_header1["ciphertext"], + Base64.decode64(header_key1["key"]), + Base64.decode64(header_key1["iv"])) recipient2_key = OpenSSL::PKey::RSA.generate(1024) doc2 = Nokogiri::XML::Document.parse(slap.generate_xml(recipient2_key.public_key)) enc_header2 = doc2.at_xpath("d:diaspora/d:encrypted_header", ns).content cipher_header2 = JSON.parse(Base64.decode64(enc_header2)) - key_json2 = recipient2_key.private_decrypt(Base64.decode64(cipher_header2["aes_key"])) + header_key2 = JSON.parse(recipient2_key.private_decrypt(Base64.decode64(cipher_header2["aes_key"]))) + decrypted_header2 = Salmon::AES.decrypt(cipher_header2["ciphertext"], + Base64.decode64(header_key2["key"]), + Base64.decode64(header_key2["iv"])) expect(enc_header1).not_to eq(enc_header2) - expect(key_json1).to eq(key_json2) + expect(header_key1).not_to eq(header_key2) + expect(decrypted_header1).to eq(decrypted_header2) expect(doc1.xpath("d:diaspora/me:env", ns).to_xml).to eq(doc2.xpath("d:diaspora/me:env", ns).to_xml) end diff --git a/spec/lib/diaspora_federation/salmon/magic_envelope_spec.rb b/spec/lib/diaspora_federation/salmon/magic_envelope_spec.rb index b23a3f9..75484db 100644 --- a/spec/lib/diaspora_federation/salmon/magic_envelope_spec.rb +++ b/spec/lib/diaspora_federation/salmon/magic_envelope_spec.rb @@ -1,16 +1,9 @@ module DiasporaFederation describe Salmon::MagicEnvelope do - let(:payload) { Entities::TestEntity.new(test: "asdf") } + let(:sender_id) { FactoryGirl.generate(:diaspora_id) } let(:privkey) { OpenSSL::PKey::RSA.generate(512) } # use small key for speedy specs - let(:envelope) { envelop_xml(Salmon::MagicEnvelope.new(privkey, payload)) } - - def envelop_xml(magic_env) - Nokogiri::XML::Builder.new(encoding: "UTF-8") {|xml| - xml.root("xmlns:me" => Salmon::MagicEnvelope::XMLNS) { - xml.parent << magic_env.envelop - } - }.doc.at_xpath("//me:env") - end + let(:payload) { Entities::TestEntity.new(test: "asdf") } + let(:envelope) { Salmon::MagicEnvelope.new(payload) } def sig_subj(env) data = Base64.urlsafe_decode64(env.at_xpath("me:data").content) @@ -21,40 +14,43 @@ module DiasporaFederation [data, type, enc, alg].map {|i| Base64.urlsafe_encode64(i) }.join(".") end - def re_sign(env, key) - new_sig = Base64.urlsafe_encode64(key.sign(OpenSSL::Digest::SHA256.new, sig_subj(env))) - env.at_xpath("me:sig").content = new_sig - end - context "sanity" do it "constructs an instance" do expect { - Salmon::MagicEnvelope.new(privkey, payload) + Salmon::MagicEnvelope.new(payload) }.not_to raise_error end it "raises an error if the param types are wrong" do ["asdf", 1234, :test, false].each do |val| expect { - Salmon::MagicEnvelope.new(val, val) + Salmon::MagicEnvelope.new(val) }.to raise_error ArgumentError end end end describe "#envelop" do - subject { Salmon::MagicEnvelope.new(privkey, payload) } + context "sanity" do + it "raises an error if the param types are wrong" do + ["asdf", 1234, :test, false].each do |val| + expect { + envelope.envelop(val, val) + }.to raise_error ArgumentError + end + end + end it "should be an instance of Nokogiri::XML::Element" do - expect(envelop_xml(subject)).to be_an_instance_of Nokogiri::XML::Element + expect(envelope.envelop(privkey, sender_id)).to be_an_instance_of Nokogiri::XML::Element end it "returns a magic envelope of correct structure" do - env = envelop_xml(subject) - expect(env.name).to eq("env") + env_xml = envelope.envelop(privkey, sender_id) + expect(env_xml.name).to eq("env") control = %w(data encoding alg sig) - env.children.each do |node| + env_xml.children.each do |node| expect(control).to include(node.name) control.reject! {|i| i == node.name } end @@ -62,28 +58,38 @@ module DiasporaFederation expect(control).to be_empty end - it "signs the payload correctly" do - env = envelop_xml(subject) + it "adds the sender_id to the signature" do + key_id = envelope.envelop(privkey, sender_id).at_xpath("me:sig")["key_id"] - subj = sig_subj(env) - sig = Base64.urlsafe_decode64(env.at_xpath("me:sig").content) + expect(Base64.urlsafe_decode64(key_id)).to eq(sender_id) + end + + it "adds the data_type" do + data_type = envelope.envelop(privkey, sender_id).at_xpath("me:data")["type"] + + expect(data_type).to eq("application/xml") + end + + it "signs the payload correctly" do + env_xml = envelope.envelop(privkey, sender_id) + + subj = sig_subj(env_xml) + sig = Base64.urlsafe_decode64(env_xml.at_xpath("me:sig").content) expect(privkey.public_key.verify(OpenSSL::Digest::SHA256.new, sig, subj)).to be_truthy end end describe "#encrypt!" do - subject { Salmon::MagicEnvelope.new(privkey, payload) } - it "encrypts the payload, returning cipher params" do - params = subject.encrypt! + params = envelope.encrypt! expect(params).to include(:key, :iv) end it "actually encrypts the payload" do - plain_payload = subject.payload - params = subject.encrypt! - encrypted_payload = subject.payload + plain_payload = envelope.instance_variable_get(:@payload) + params = envelope.encrypt! + encrypted_payload = envelope.instance_variable_get(:@payload) cipher = OpenSSL::Cipher.new(Salmon::AES::CIPHER) cipher.encrypt @@ -98,9 +104,14 @@ module DiasporaFederation describe ".unenvelop" do context "sanity" do + def re_sign(env, key) + new_sig = Base64.urlsafe_encode64(key.sign(OpenSSL::Digest::SHA256.new, sig_subj(env))) + env.at_xpath("me:sig").content = new_sig + end + it "works with sane input" do expect { - Salmon::MagicEnvelope.unenvelop(envelope, privkey.public_key) + Salmon::MagicEnvelope.unenvelop(envelope.envelop(privkey, sender_id), privkey.public_key) }.not_to raise_error end @@ -121,14 +132,13 @@ module DiasporaFederation it "verifies the signature" do other_key = OpenSSL::PKey::RSA.generate(512) expect { - Salmon::MagicEnvelope.unenvelop(envelope, other_key.public_key) + Salmon::MagicEnvelope.unenvelop(envelope.envelop(privkey, sender_id), other_key.public_key) }.to raise_error Salmon::InvalidSignature end it "verifies the encoding" do - bad_env = envelop_xml(Salmon::MagicEnvelope.new(privkey, payload)) - elem = bad_env.at_xpath("me:encoding") - elem.content = "invalid_enc" + bad_env = envelope.envelop(privkey, sender_id) + bad_env.at_xpath("me:encoding").content = "invalid_enc" re_sign(bad_env, privkey) expect { Salmon::MagicEnvelope.unenvelop(bad_env, privkey.public_key) @@ -136,9 +146,8 @@ module DiasporaFederation end it "verifies the algorithm" do - bad_env = envelop_xml(Salmon::MagicEnvelope.new(privkey, payload)) - elem = bad_env.at_xpath("me:alg") - elem.content = "invalid_alg" + bad_env = envelope.envelop(privkey, sender_id) + bad_env.at_xpath("me:alg").content = "invalid_alg" re_sign(bad_env, privkey) expect { Salmon::MagicEnvelope.unenvelop(bad_env, privkey.public_key) @@ -147,21 +156,52 @@ module DiasporaFederation end it "returns the original entity" do - entity = Salmon::MagicEnvelope.unenvelop(envelope, privkey.public_key) + entity = Salmon::MagicEnvelope.unenvelop(envelope.envelop(privkey, sender_id), privkey.public_key) expect(entity).to be_an_instance_of Entities::TestEntity expect(entity.test).to eq("asdf") end it "decrypts on the fly, when cipher params are present" do - env = Salmon::MagicEnvelope.new(privkey, payload) - params = env.encrypt! + params = envelope.encrypt! - envelope = envelop_xml(env) + env_xml = envelope.envelop(privkey, sender_id) - entity = Salmon::MagicEnvelope.unenvelop(envelope, privkey.public_key, params) + entity = Salmon::MagicEnvelope.unenvelop(env_xml, privkey.public_key, params) expect(entity).to be_an_instance_of Entities::TestEntity expect(entity.test).to eq("asdf") end + + context "use key_id from magic envelope" do + it "returns the original entity" do + expect(DiasporaFederation.callbacks).to receive(:trigger).with( + :fetch_public_key_by_diaspora_id, sender_id + ).and_return(privkey.public_key) + + entity = Salmon::MagicEnvelope.unenvelop(envelope.envelop(privkey, sender_id)) + expect(entity).to be_an_instance_of Entities::TestEntity + expect(entity.test).to eq("asdf") + end + + it "raises if the magic envelope has no key_id" do + bad_env = envelope.envelop(privkey, sender_id) + + bad_env.at_xpath("me:sig").attributes["key_id"].remove + + expect { + Salmon::MagicEnvelope.unenvelop(bad_env) + }.to raise_error Salmon::InvalidEnvelope + end + + it "raises if the sender key is not found" do + expect(DiasporaFederation.callbacks).to receive(:trigger).with( + :fetch_public_key_by_diaspora_id, sender_id + ).and_return(nil) + + expect { + Salmon::MagicEnvelope.unenvelop(envelope.envelop(privkey, sender_id)) + }.to raise_error Salmon::SenderKeyNotFound + end + end end end end