Remove old Slap and EncryptedSlap and cleanup legacy receive

Closes #30
This commit is contained in:
Benjamin Neff 2019-10-29 02:28:45 +01:00
parent 56385a14c1
commit 0163963849
No known key found for this signature in database
GPG key ID: 971464C3F1A90194
11 changed files with 13 additions and 462 deletions

View file

@ -5,18 +5,14 @@ require_dependency "diaspora_federation/application_controller"
module DiasporaFederation
# This controller processes receiving messages.
class ReceiveController < ApplicationController
before_action :check_for_xml
# Receives public messages
#
# POST /receive/public
def public
legacy = request.content_type != "application/magic-envelope+xml"
data = data_for_public_message(legacy)
data = request.body.read
logger.debug data
DiasporaFederation.callbacks.trigger(:queue_public_receive, data, legacy)
DiasporaFederation.callbacks.trigger(:queue_public_receive, data)
head :accepted
end
@ -25,43 +21,12 @@ module DiasporaFederation
#
# POST /receive/users/:guid
def private
legacy = request.content_type != "application/json"
data = data_for_private_message(legacy)
data = request.body.read
logger.debug data
success = DiasporaFederation.callbacks.trigger(:queue_private_receive, params[:guid], data, legacy)
success = DiasporaFederation.callbacks.trigger(:queue_private_receive, params[:guid], data)
head success ? :accepted : :not_found
end
private
# Checks the xml parameter for legacy salmon slaps
# @deprecated
def check_for_xml
legacy_request = request.content_type.nil? || request.content_type == "application/x-www-form-urlencoded"
head :unprocessable_entity 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

View file

@ -184,13 +184,11 @@ module DiasporaFederation
# queue_public_receive
# Queue a public salmon xml to process in background
# @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] 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
#
# receive_entity

View file

@ -8,14 +8,9 @@ module DiasporaFederation
# Receive a public message
# @param [String] data message to receive
# @param [Boolean] legacy use old slap parser
def self.receive_public(data, legacy=false)
magic_env = if legacy
Salmon::Slap.from_xml(data)
else
magic_env_xml = Nokogiri::XML(data).root
Salmon::MagicEnvelope.unenvelop(magic_env_xml)
end
def self.receive_public(data)
magic_env_xml = Nokogiri::XML(data).root
magic_env = Salmon::MagicEnvelope.unenvelop(magic_env_xml)
Public.new(magic_env).receive
rescue => e # rubocop:disable Style/RescueStandardError
logger.error "failed to receive public message: #{e.class}: #{e.message}"
@ -28,16 +23,11 @@ module DiasporaFederation
# @param [OpenSSL::PKey::RSA] recipient_private_key recipient private key to decrypt the message
# @param [Object] recipient_id the identifier to persist the entity for the correct user,
# see +receive_entity+ callback
# @param [Boolean] legacy use old slap parser
def self.receive_private(data, recipient_private_key, recipient_id, legacy=false)
def self.receive_private(data, recipient_private_key, recipient_id)
raise ArgumentError, "no recipient key provided" unless recipient_private_key.instance_of?(OpenSSL::PKey::RSA)
magic_env = if legacy
Salmon::EncryptedSlap.from_xml(data, recipient_private_key)
else
magic_env_xml = Salmon::EncryptedMagicEnvelope.decrypt(data, recipient_private_key)
Salmon::MagicEnvelope.unenvelop(magic_env_xml)
end
magic_env_xml = Salmon::EncryptedMagicEnvelope.decrypt(data, recipient_private_key)
magic_env = Salmon::MagicEnvelope.unenvelop(magic_env_xml)
Private.new(magic_env, recipient_id).receive
rescue => e # rubocop:disable Style/RescueStandardError
logger.error "failed to receive private message for #{recipient_id}: #{e.class}: #{e.message}"

View file

@ -16,5 +16,3 @@ 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"

View file

@ -1,113 +0,0 @@
# frozen_string_literal: true
require "json"
module DiasporaFederation
module Salmon
# +EncryptedSlap+ provides class methods for generating and parsing encrypted
# Slaps. (In principle the same as {Slap}, but with encryption.)
#
# The basic encryption mechanism used here is based on the knowledge that
# asymmetrical encryption is slow and symmetrical encryption is fast. Keeping in
# mind that a message we want to de-/encrypt may greatly vary in length,
# performance considerations must play a part of this scheme.
#
# A diaspora*-flavored encrypted magic-enveloped XML message looks like the following:
#
# <?xml version="1.0" encoding="UTF-8"?>
# <diaspora xmlns="https://joindiaspora.com/protocol" xmlns:me="http://salmon-protocol.org/ns/magic-env">
# <encrypted_header>{encrypted_header}</encrypted_header>
# {magic_envelope with encrypted data}
# </diaspora>
#
# The encrypted header is encoded in JSON like this (when in plain text):
#
# {
# "aes_key" => "...",
# "ciphertext" => "..."
# }
#
# +aes_key+ is encrypted using the recipients public key, and contains the AES
# +key+ and +iv+ used to encrypt the +ciphertext+ also encoded as JSON.
#
# {
# "key" => "...",
# "iv" => "..."
# }
#
# +ciphertext+, once decrypted, contains the +author_id+, +aes_key+ and +iv+
# relevant to the decryption of the data in the magic_envelope and the
# verification of its signature.
#
# The decrypted cyphertext has this XML structure:
#
# <decrypted_header>
# <iv>{iv}</iv>
# <aes_key>{aes_key}</aes_key>
# <author_id>{author_id}</author_id>
# </decrypted_header>
#
# Finally, before decrypting the magic envelope payload, the signature should
# first be verified.
#
# @example Parsing a Salmon Slap
# recipient_privkey = however_you_retrieve_the_recipients_private_key()
# entity = EncryptedSlap.from_xml(slap_xml, recipient_privkey).payload
#
# @deprecated
class EncryptedSlap < Slap
# Creates a {MagicEnvelope} instance from the data within the given XML string
# containing an encrypted payload.
#
# @param [String] slap_xml encrypted Salmon xml
# @param [OpenSSL::PKey::RSA] privkey recipient private_key for decryption
#
# @return [MagicEnvelope] magic envelope instance with payload and sender
#
# @raise [ArgumentError] if any of the arguments is of the wrong type
# @raise [MissingHeader] if the +encrypted_header+ element is missing in the XML
# @raise [MissingMagicEnvelope] if the +me:env+ element is missing in the XML
def self.from_xml(slap_xml, privkey)
raise ArgumentError unless slap_xml.instance_of?(String) && privkey.instance_of?(OpenSSL::PKey::RSA)
doc = Nokogiri::XML(slap_xml)
header_elem = doc.at_xpath("d:diaspora/d:encrypted_header", Slap::NS)
raise MissingHeader if header_elem.nil?
header = header_data(header_elem.content, privkey)
sender = header[:author_id]
cipher_params = {key: Base64.decode64(header[:aes_key]), iv: Base64.decode64(header[:iv])}
MagicEnvelope.unenvelop(magic_env_from_doc(doc), sender, cipher_params)
end
# Decrypts and reads the data from the encrypted XML header
# @param [String] data base64 encoded, encrypted header data
# @param [OpenSSL::PKey::RSA] privkey private key for decryption
# @return [Hash] { iv: "...", aes_key: "...", author_id: "..." }
private_class_method def self.header_data(data, privkey)
header_elem = decrypt_header(data, privkey)
raise InvalidHeader unless header_elem.name == "decrypted_header"
iv = header_elem.at_xpath("iv").content
key = header_elem.at_xpath("aes_key").content
author_id = header_elem.at_xpath("author_id").content
{iv: iv, aes_key: key, author_id: author_id}
end
# Decrypts the xml header
# @param [String] data base64 encoded, encrypted header data
# @param [OpenSSL::PKey::RSA] privkey private key for decryption
# @return [Nokogiri::XML::Element] header xml document
private_class_method def self.decrypt_header(data, privkey)
cipher_header = JSON.parse(Base64.decode64(data))
key = JSON.parse(privkey.private_decrypt(Base64.decode64(cipher_header["aes_key"])))
xml = AES.decrypt(cipher_header["ciphertext"], Base64.decode64(key["key"]), Base64.decode64(key["iv"]))
Nokogiri::XML(xml).root
end
end
end
end

View file

@ -2,26 +2,6 @@
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

View file

@ -1,59 +0,0 @@
# frozen_string_literal: true
module DiasporaFederation
module Salmon
# +Slap+ provides class methods to create unencrypted Slap XML from payload
# data and parse incoming XML into a Slap instance.
#
# A diaspora* flavored magic-enveloped XML message looks like the following:
#
# <?xml version="1.0" encoding="UTF-8"?>
# <diaspora xmlns="https://joindiaspora.com/protocol" xmlns:me="http://salmon-protocol.org/ns/magic-env">
# <header>
# <author_id>{author}</author_id>
# </header>
# {magic_envelope}
# </diaspora>
#
# @example Parsing a Salmon Slap
# entity = Slap.from_xml(slap_xml).payload
#
# @deprecated
class Slap
# Namespaces
NS = {d: Salmon::XMLNS, me: MagicEnvelope::XMLNS}.freeze
# Parses an unencrypted Salmon XML string and returns a new instance of
# {MagicEnvelope} with the XML data.
#
# @param [String] slap_xml Salmon XML
#
# @return [MagicEnvelope] magic envelope instance with payload and sender
#
# @raise [ArgumentError] if the argument is not a String
# @raise [MissingAuthor] if the +author_id+ element is missing from the XML
# @raise [MissingMagicEnvelope] if the +me:env+ element is missing from the XML
def self.from_xml(slap_xml)
raise ArgumentError unless slap_xml.instance_of?(String)
doc = Nokogiri::XML(slap_xml)
author_elem = doc.at_xpath("d:diaspora/d:header/d:author_id", Slap::NS)
raise MissingAuthor if author_elem.nil? || author_elem.content.empty?
sender = author_elem.content
MagicEnvelope.unenvelop(magic_env_from_doc(doc), sender)
end
# Parses the magic envelop from the document.
#
# @param [Nokogiri::XML::Document] doc Salmon XML Document
private_class_method def self.magic_env_from_doc(doc)
doc.at_xpath("d:diaspora/me:env", Slap::NS).tap do |env|
raise MissingMagicEnvelope if env.nil?
end
end
end
end
end

View file

@ -5,32 +5,6 @@ module DiasporaFederation
routes { DiasporaFederation::Engine.routes }
describe "POST #public" do
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_callback(:queue_public_receive, "<diaspora/>", true)
post :public, params: {xml: "<diaspora/>"}
expect(response.code).to eq("202")
end
it "unescapes the xml before sending it to the callback" do
expect_callback(:queue_public_receive, "<diaspora/>", true)
post :public, params: {xml: CGI.escape("<diaspora/>")}
end
end
context "magic envelope" do
before do
Mime::Type.register("application/magic-envelope+xml", :magic_envelope)
@ -38,7 +12,7 @@ module DiasporaFederation
end
it "returns a 202 if queued correctly" do
expect_callback(:queue_public_receive, "<me:env/>", false)
expect_callback(:queue_public_receive, "<me:env/>")
post :public, body: +"<me:env/>"
expect(response.code).to eq("202")
@ -47,39 +21,6 @@ module DiasporaFederation
end
describe "POST #private" do
context "legacy salmon slap" do
it "return a 404 if not queued successfully (unknown user guid)" do
expect_callback(:queue_private_receive, "any-guid", "<diaspora/>", true).and_return(false)
post :private, params: {guid: "any-guid", xml: "<diaspora/>"}
expect(response.code).to eq("404")
end
it "returns a 422 if no xml is passed" do
post :private, params: {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, params: {guid: "any-guid"}
expect(response.code).to eq("422")
end
it "returns a 202 if the callback returned true" do
expect_callback(:queue_private_receive, "any-guid", "<diaspora/>", true).and_return(true)
post :private, params: {guid: "any-guid", xml: "<diaspora/>"}
expect(response.code).to eq("202")
end
it "unescapes the xml before sending it to the callback" do
expect_callback(:queue_private_receive, "any-guid", "<diaspora/>", true).and_return(true)
post :private, params: {guid: "any-guid", xml: CGI.escape("<diaspora/>")}
end
end
context "encrypted magic envelope" do
before do
@request.env["CONTENT_TYPE"] = "application/json"
@ -87,7 +28,7 @@ module DiasporaFederation
it "return a 404 if not queued successfully (unknown user guid)" do
expect_callback(
:queue_private_receive, "any-guid", "{\"aes_key\": \"key\", \"encrypted_magic_envelope\": \"env\"}", false
:queue_private_receive, "any-guid", "{\"aes_key\": \"key\", \"encrypted_magic_envelope\": \"env\"}"
).and_return(false)
post :private,
@ -98,7 +39,7 @@ module DiasporaFederation
it "returns a 202 if the callback returned true" do
expect_callback(
:queue_private_receive, "any-guid", "{\"aes_key\": \"key\", \"encrypted_magic_envelope\": \"env\"}", false
:queue_private_receive, "any-guid", "{\"aes_key\": \"key\", \"encrypted_magic_envelope\": \"env\"}"
).and_return(true)
post :private,

View file

@ -23,21 +23,6 @@ module DiasporaFederation
described_class.receive_public(data)
end
it "parses the entity with legacy slap receiver" do
expect_callback(:fetch_public_key, post.author).and_return(sender_key)
data = generate_legacy_salmon_slap(post, post.author, sender_key)
expect_callback(:receive_entity, kind_of(Entities::StatusMessage), post.author, nil) do |_, entity|
expect(entity.guid).to eq(post.guid)
expect(entity.author).to eq(post.author)
expect(entity.text).to eq(post.text)
expect(entity.public).to eq("true")
end
described_class.receive_public(data, true)
end
it "redirects exceptions from the receiver" do
expect {
described_class.receive_public("<xml/>")
@ -64,21 +49,6 @@ module DiasporaFederation
described_class.receive_private(data, recipient_key, 1234)
end
it "parses the entity with legacy slap receiver" do
expect_callback(:fetch_public_key, post.author).and_return(sender_key)
data = generate_legacy_encrypted_salmon_slap(post, post.author, sender_key, recipient_key)
expect_callback(:receive_entity, kind_of(Entities::StatusMessage), post.author, 1234) do |_, entity|
expect(entity.guid).to eq(post.guid)
expect(entity.author).to eq(post.author)
expect(entity.text).to eq(post.text)
expect(entity.public).to eq("false")
end
described_class.receive_private(data, recipient_key, 1234, true)
end
it "raises when recipient private key is not available" do
magic_env = Salmon::MagicEnvelope.new(post, post.author).envelop(sender_key)
data = Salmon::EncryptedMagicEnvelope.encrypt(magic_env, recipient_key.public_key)

View file

@ -1,59 +0,0 @@
# frozen_string_literal: true
module DiasporaFederation
describe Salmon::EncryptedSlap do
let(:sender) { "user_test@diaspora.example.tld" }
let(:privkey) { OpenSSL::PKey::RSA.generate(512) } # use small key for speedy specs
let(:recipient_key) { OpenSSL::PKey::RSA.generate(1024) } # use small key for speedy specs
let(:payload) { Entities::TestEntity.new(test: "qwertzuiop") }
let(:slap_xml) { generate_legacy_encrypted_salmon_slap(payload, sender, privkey, recipient_key.public_key) }
describe ".from_xml" do
context "sanity" do
it "accepts correct params" do
expect_callback(:fetch_public_key, sender).and_return(privkey.public_key)
expect {
Salmon::EncryptedSlap.from_xml(slap_xml, recipient_key)
}.not_to raise_error
end
it "raises an error when the params have a wrong type" do
[1234, false, :symbol, payload, privkey].each do |val|
expect {
Salmon::EncryptedSlap.from_xml(val, val)
}.to raise_error ArgumentError
end
end
it "verifies the existence of 'encrypted_header'" do
faulty_xml = <<~XML
<diaspora xmlns="https://joindiaspora.com/protocol" xmlns:me="http://salmon-protocol.org/ns/magic-env">
</diaspora>
XML
expect {
Salmon::EncryptedSlap.from_xml(faulty_xml, recipient_key)
}.to raise_error Salmon::MissingHeader
end
it "verifies the existence of a magic envelope" do
faulty_xml = <<~XML
<diaspora xmlns="https://joindiaspora.com/protocol" xmlns:me="http://salmon-protocol.org/ns/magic-env">
<encrypted_header/>
</diaspora>
XML
expect(Salmon::EncryptedSlap).to receive(:header_data).and_return(aes_key: "", iv: "", author_id: "")
expect {
Salmon::EncryptedSlap.from_xml(faulty_xml, recipient_key)
}.to raise_error Salmon::MissingMagicEnvelope
end
end
context "generated instance" do
it_behaves_like "a MagicEnvelope instance" do
subject { Salmon::EncryptedSlap.from_xml(slap_xml, recipient_key) }
end
end
end
end
end

View file

@ -1,60 +0,0 @@
# frozen_string_literal: true
module DiasporaFederation
describe Salmon::Slap do
let(:sender) { "test_user@pod.somedomain.tld" }
let(:privkey) { OpenSSL::PKey::RSA.generate(512) } # use small key for speedy specs
let(:payload) { Entities::TestEntity.new(test: "qwertzuiop") }
let(:slap_xml) { generate_legacy_salmon_slap(payload, sender, privkey) }
describe ".from_xml" do
context "sanity" do
it "accepts salmon xml as param" do
expect_callback(:fetch_public_key, sender).and_return(privkey.public_key)
expect {
Salmon::Slap.from_xml(slap_xml)
}.not_to raise_error
end
it "raises an error when the param has a wrong type" do
[1234, false, :symbol, payload, privkey].each do |val|
expect {
Salmon::Slap.from_xml(val)
}.to raise_error ArgumentError
end
end
it "verifies the existence of an author_id" do
faulty_xml = <<~XML
<diaspora xmlns="https://joindiaspora.com/protocol" xmlns:me="http://salmon-protocol.org/ns/magic-env">
<header/>
</diaspora>
XML
expect {
Salmon::Slap.from_xml(faulty_xml)
}.to raise_error Salmon::MissingAuthor
end
it "verifies the existence of a magic envelope" do
faulty_xml = <<~XML
<diaspora xmlns="https://joindiaspora.com/protocol" xmlns:me="http://salmon-protocol.org/ns/magic-env">
<header>
<author_id>#{sender}</author_id>
</header>
</diaspora>
XML
expect {
Salmon::Slap.from_xml(faulty_xml)
}.to raise_error Salmon::MissingMagicEnvelope
end
end
context "generated instance" do
it_behaves_like "a MagicEnvelope instance" do
subject { Salmon::Slap.from_xml(slap_xml) }
end
end
end
end
end