diff --git a/lib/diaspora_federation/salmon.rb b/lib/diaspora_federation/salmon.rb
index ec46941..c2cb7a5 100644
--- a/lib/diaspora_federation/salmon.rb
+++ b/lib/diaspora_federation/salmon.rb
@@ -2,8 +2,14 @@ module DiasporaFederation
# This module contains a Diaspora*-specific implementation of parts of the
# {http://www.salmon-protocol.org/ Salmon Protocol}.
module Salmon
+ # XML namespace url
+ XMLNS = "https://joindiaspora.com/protocol"
end
end
+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/slap"
+require "diaspora_federation/salmon/encrypted_slap"
diff --git a/lib/diaspora_federation/salmon/aes.rb b/lib/diaspora_federation/salmon/aes.rb
new file mode 100644
index 0000000..3a4929d
--- /dev/null
+++ b/lib/diaspora_federation/salmon/aes.rb
@@ -0,0 +1,43 @@
+module DiasporaFederation
+ module Salmon
+ class AES
+ # OpenSSL aes cipher definition
+ CIPHER = "AES-256-CBC"
+
+ # encrypts the given data with a new, random AES cipher and returns the
+ # resulting ciphertext, the key and iv in a hash (each of the entries
+ # base64 strict_encoded).
+ # @param [String] data plain input
+ # @return [Hash] { key: "...", iv: "...", ciphertext: "..." }
+ def self.encrypt(data)
+ cipher = OpenSSL::Cipher.new(CIPHER)
+ cipher.encrypt
+ key = cipher.random_key
+ iv = cipher.random_iv
+ ciphertext = cipher.update(data) + cipher.final
+
+ enc = [key, iv, ciphertext].map {|i| Base64.strict_encode64(i) }
+
+ {key: enc[0], iv: enc[1], ciphertext: enc[2]}
+ end
+
+ # decrypts the given ciphertext with an AES cipher defined by the given key
+ # and iv. parameters are expected to be base64 encoded
+ # @param [String] ciphertext input data
+ # @param [String] key AES key
+ # @param [String] iv AES initialization vector
+ # @return [String] decrypted plain message
+ def self.decrypt(ciphertext, key, iv)
+ dec = [ciphertext, key, iv].map {|i| Base64.decode64(i) }
+
+ decipher = OpenSSL::Cipher.new(CIPHER)
+ decipher.decrypt
+ decipher.key = dec[1]
+ decipher.iv = dec[2]
+
+ plain = decipher.update(dec[0]) + decipher.final
+ plain
+ end
+ end
+ end
+end
diff --git a/lib/diaspora_federation/salmon/encrypted_slap.rb b/lib/diaspora_federation/salmon/encrypted_slap.rb
new file mode 100644
index 0000000..7e76247
--- /dev/null
+++ b/lib/diaspora_federation/salmon/encrypted_slap.rb
@@ -0,0 +1,202 @@
+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:
+ #
+ #
+ #
+ # {encrypted_header}
+ # {magic_envelope with encrypted data}
+ #
+ #
+ # 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:
+ #
+ #
+ # {iv}
+ # {aes_key}
+ # {author_id}
+ #
+ #
+ # Finally, before decrypting the magic envelope payload, the signature should
+ # first be verified.
+ #
+ # @example Generating an encrypted Salmon Slap
+ # author_id = "author@pod.example.tld"
+ # author_privkey = however_you_retrieve_the_authors_private_key(author_id)
+ # recipient_pubkey = however_you_retrieve_the_recipients_public_key()
+ # entity = YourEntity.new(attr: "val")
+ #
+ # slap_xml = EncryptedSlap.generate_xml(author_id, author_privkey, entity, recipient_pubkey)
+ #
+ # @example Parsing a Salmon Slap
+ # recipient_privkey = however_you_retrieve_the_recipients_private_key()
+ # slap = EncryptedSlap.from_xml(slap_xml, recipient_privkey)
+ # author_pubkey = however_you_retrieve_the_authors_public_key(slap.author_id)
+ #
+ # entity = slap.entity(author_pubkey)
+ #
+ class EncryptedSlap
+ # Creates a Slap instance from the data within the given XML string
+ # containing an encrypted payload.
+ #
+ # @param [String] slap_xml encrypted Salmon xml
+ # @param [OpenSSL::PKey::RSA] pkey recipient private_key for decryption
+ #
+ # @return [Slap] new Slap instance
+ #
+ # @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, pkey)
+ raise ArgumentError unless slap_xml.instance_of?(String) && pkey.instance_of?(OpenSSL::PKey::RSA)
+ doc = Nokogiri::XML::Document.parse(slap_xml)
+ ns = {d: Salmon::XMLNS, me: MagicEnvelope::XMLNS}
+ header_xpath = "d:diaspora/d:encrypted_header"
+ magicenv_xpath = "d:diaspora/me:env"
+
+ if doc.namespaces.empty?
+ ns = nil
+ header_xpath = "diaspora/encrypted_header"
+ magicenv_xpath = "diaspora/env"
+ end
+
+ slap = Slap.new
+
+ header_elem = doc.at_xpath(header_xpath, ns)
+ raise MissingHeader if header_elem.nil?
+ header = header_data(header_elem.content, pkey)
+ slap.author_id = header[:author_id]
+ slap.cipher_params = {key: header[:aes_key], iv: header[:iv]}
+
+ magic_env_elem = doc.at_xpath(magicenv_xpath, ns)
+ raise MissingMagicEnvelope if magic_env_elem.nil?
+ slap.magic_envelope = magic_env_elem
+
+ slap
+ end
+
+ # Creates an encrypted Salmon Slap and returns the XML string.
+ #
+ # @param [String] author_id Diaspora* handle of the author
+ # @param [OpenSSL::PKey::RSA] pkey sender private key for signing the magic envelope
+ # @param [Entity] entity payload
+ # @param [OpenSSL::PKey::RSA] pubkey recipient public key for encrypting the AES key
+ # @return [String] Salmon XML string
+ # @raise [ArgumentError] if any of the arguments is of the wrong type
+ def self.generate_xml(author_id, pkey, entity, pubkey)
+ raise ArgumentError unless author_id.instance_of?(String) &&
+ pkey.instance_of?(OpenSSL::PKey::RSA) &&
+ entity.is_a?(Entity) &&
+ pubkey.instance_of?(OpenSSL::PKey::RSA)
+
+ doc = Nokogiri::XML::Document.new
+ doc.encoding = "UTF-8"
+
+ root = Nokogiri::XML::Element.new("diaspora", doc)
+ root.default_namespace = Salmon::XMLNS
+ root.add_namespace("me", MagicEnvelope::XMLNS)
+ doc.root = root
+
+ magic_envelope = MagicEnvelope.new(pkey, entity, root)
+ envelope_key = magic_envelope.encrypt!
+
+ encrypted_header(author_id, envelope_key, pubkey, root)
+ magic_envelope.envelop
+
+ doc.to_xml
+ end
+
+ # decrypts and reads the data from the encrypted XML header
+ # @param [String] base64 encoded, encrypted header data
+ # @param [OpenSSL::PKey::RSA] private_key for decryption
+ # @return [Hash] { iv: "...", aes_key: "...", author_id: "..." }
+ def self.header_data(data, pkey)
+ header_elem = decrypt_header(data, pkey)
+ 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
+ private_class_method :header_data
+
+ # decrypts the xml header
+ # @param [String] base64 encoded, encrypted header data
+ # @param [OpenSSL::PKey::RSA] private_key for decryption
+ # @return [Nokogiri::XML::Element] header xml document
+ def self.decrypt_header(data, pkey)
+ cipher_header = JSON.parse(Base64.decode64(data))
+ header_key = JSON.parse(pkey.private_decrypt(Base64.decode64(cipher_header["aes_key"])))
+
+ xml = AES.decrypt(cipher_header["ciphertext"], header_key["key"], header_key["iv"])
+ Nokogiri::XML::Document.parse(xml).root
+ end
+ private_class_method :decrypt_header
+
+ # encrypt the header xml with an AES cipher and encrypt the cipher params
+ # with the recipients public_key
+ # @param [String] diaspora_handle
+ # @param [Hash] envelope cipher params
+ # @param [OpenSSL::PKey::RSA] recipient public_key
+ # @param parent_node [Nokogiri::XML::Element] parent element for insering in XML document
+ def self.encrypted_header(author_id, envelope_key, pubkey, parent_node)
+ data = header_xml(author_id, envelope_key)
+ encryption_data = AES.encrypt(data)
+
+ json_key = JSON.generate(key: encryption_data[:key], iv: encryption_data[:iv])
+ encrypted_key = Base64.strict_encode64(pubkey.public_encrypt(json_key))
+
+ json_header = JSON.generate(aes_key: encrypted_key, ciphertext: encryption_data[:ciphertext])
+
+ header = Nokogiri::XML::Element.new("encrypted_header", parent_node.document)
+ header.content = Base64.strict_encode64(json_header)
+ parent_node << header
+ end
+ private_class_method :encrypted_header
+
+ # generate the header xml string, including the author, aes_key and iv
+ # @param [String] diaspora_handle of the author
+ # @param [Hash] { key: "...", iv: "..." } (values in base64)
+ # @return [String] header XML string
+ def self.header_xml(author_id, envelope_key)
+ builder = Nokogiri::XML::Builder.new do |xml|
+ xml.decrypted_header {
+ xml.iv(envelope_key[:iv])
+ xml.aes_key(envelope_key[:key])
+ xml.author_id(author_id)
+ }
+ end
+ builder.to_xml.strip
+ end
+ private_class_method :header_xml
+ end
+ end
+end
diff --git a/lib/diaspora_federation/salmon/magic_envelope.rb b/lib/diaspora_federation/salmon/magic_envelope.rb
new file mode 100644
index 0000000..4cc56f4
--- /dev/null
+++ b/lib/diaspora_federation/salmon/magic_envelope.rb
@@ -0,0 +1,182 @@
+module DiasporaFederation
+ module Salmon
+ # Represents a Magic Envelope for Diaspora* federation messages.
+ #
+ # When generating a Magic Envelope, an instance of this class is created and
+ # the contents are specified on initialization. Optionally, the payload can be
+ # encrypted ({MagicEnvelope#encrypt!}), before the XML is returned
+ # ({MagicEnvelope#envelop}).
+ #
+ # The generated XML appears like so:
+ #
+ #
+ # {data}
+ # base64url
+ # RSA-SHA256
+ # {signature}
+ #
+ #
+ # When parsing the XML of an incoming Magic Envelope {MagicEnvelope.unenvelop}
+ # is used.
+ #
+ # @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"
+
+ # algorithm used for signing the payload data
+ ALGORITHM = "RSA-SHA256"
+
+ # mime type describing the payload data
+ DATA_TYPE = "application/xml"
+
+ # digest instance used for signing
+ DIGEST = OpenSSL::Digest::SHA256.new
+
+ # XML namespace url
+ XMLNS = "http://salmon-protocol.org/ns/magic-env"
+
+ # Creates a new instance of MagicEnvelope.
+ #
+ # @param rsa_pkey [OpenSSL::PKey::RSA] private key used for signing
+ # @param payload [Entity] Entity instance
+ # @param parent_node [Nokogiri::XML::Element] parent element for insering in XML document
+ # @raise [ArgumentError] if either argument is not of the right type
+ def initialize(rsa_pkey, payload, parent_node=nil)
+ raise ArgumentError unless rsa_pkey.instance_of?(OpenSSL::PKey::RSA) &&
+ payload.is_a?(Entity)
+
+ if parent_node.nil?
+ doc = Nokogiri::XML::Document.new
+ parent_node = Nokogiri::XML::Element.new("root", doc)
+ parent_node.add_namespace("me", XMLNS)
+ doc.root = parent_node
+ end
+
+ @parent_node = parent_node
+ @rsa_pkey = rsa_pkey
+ @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}.
+ #
+ # @return [Nokogiri::XML::Element] XML root node
+ def envelop
+ builder = Nokogiri::XML::Builder.with(@parent_node) do |xml|
+ xml["me"].env {
+ xml["me"].data(Base64.urlsafe_encode64(@payload), type: DATA_TYPE)
+ xml["me"].encoding(ENCODING)
+ xml["me"].alg(ALGORITHM)
+ xml["me"].sig(Base64.urlsafe_encode64(signature))
+ }
+ end
+
+ builder.doc.at_xpath("//me:env")
+ end
+
+ # Encrypts the payload with a new, random AES cipher and returns the cipher
+ # params that were used.
+ #
+ # This must happen after the MagicEnvelope instance was created and before
+ # {MagicEnvelope#envelop} is called.
+ #
+ # @see Salmon.aes_encrypt
+ #
+ # @return [Hash] AES key and iv. E.g.: { key: "...", iv: "..." }
+ def encrypt!
+ encryption_data = AES.encrypt(@payload)
+ @payload = encryption_data[:ciphertext]
+
+ {key: encryption_data[:key], iv: encryption_data[:iv]}
+ end
+
+ # Extracts the entity encoded in the magic envelope data, if the signature
+ # is valid. If +cipher_params+ is given, also attempts to decrypt the payload first.
+ #
+ # Does some sanity checking to avoid bad surprises...
+ #
+ # @see XmlPayload.unpack
+ # @see Salmon.aes_decrypt
+ #
+ # @param [Nokogiri::XML::Element] magic_env XML root node of a magic envelope
+ # @param [OpenSSL::PKey::RSA] rsa_pubkey public key to verify the signature
+ # @param [Hash] cipher_params hash containing the key and iv for
+ # AES-decrypting previously encrypted data. E.g.: { iv: "...", key: "..." }
+ #
+ # @return [Entity] reconstructed entity instance
+ #
+ # @raise [ArgumentError] if any of the arguments is of invalid type
+ # @raise [InvalidEnvelope] if the envelope XML structure is malformed
+ # @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 rsa_pubkey.instance_of?(OpenSSL::PKey::RSA) &&
+ magic_env.instance_of?(Nokogiri::XML::Element)
+
+ raise InvalidEnvelope unless envelope_valid?(magic_env)
+ raise InvalidSignature unless signature_valid?(magic_env, rsa_pubkey)
+
+ enc = magic_env.at_xpath("me:encoding").content
+ alg = magic_env.at_xpath("me:alg").content
+
+ raise InvalidEncoding unless enc == ENCODING
+ raise InvalidAlgorithm unless alg == ALGORITHM
+
+ data = Base64.urlsafe_decode64(magic_env.at_xpath("me:data").content)
+ unless cipher_params.nil?
+ data = AES.decrypt(data, cipher_params[:key], cipher_params[:iv])
+ end
+
+ XmlPayload.unpack(Nokogiri::XML::Document.parse(data).root)
+ end
+
+ private
+
+ # create the signature for all fields according to specification
+ def signature
+ subject = self.class.sig_subject([@payload,
+ DATA_TYPE,
+ ENCODING,
+ ALGORITHM])
+ @rsa_pkey.sign(DIGEST, subject)
+ end
+
+ # @param [Nokogiri::XML::Element]
+ def self.envelope_valid?(env)
+ (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]
+ # @param [OpenSSL::PKey::RSA] public_key
+ def self.signature_valid?(env, pkey)
+ subject = sig_subject([Base64.urlsafe_decode64(env.at_xpath("me:data").content),
+ env.at_xpath("me:data")["type"],
+ env.at_xpath("me:encoding").content,
+ env.at_xpath("me:alg").content])
+
+ sig = Base64.urlsafe_decode64(env.at_xpath("me:sig").content)
+ pkey.verify(DIGEST, sig, subject)
+ end
+ private_class_method :signature_valid?
+
+ # constructs the signature subject.
+ # the given array should consist of the data, data_type (mimetype), encoding
+ # and the algorithm
+ # @param [Array]
+ def self.sig_subject(data_arr)
+ data_arr.map {|i| Base64.urlsafe_encode64(i) }.join(".")
+ end
+ end
+ end
+end
diff --git a/lib/diaspora_federation/salmon/slap.rb b/lib/diaspora_federation/salmon/slap.rb
new file mode 100644
index 0000000..22c2bce
--- /dev/null
+++ b/lib/diaspora_federation/salmon/slap.rb
@@ -0,0 +1,119 @@
+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:
+ #
+ #
+ #
+ #
+ # {magic_envelope}
+ #
+ #
+ # @example Generating a Salmon Slap
+ # author_id = "author@pod.example.tld"
+ # author_privkey = however_you_retrieve_the_authors_private_key(author_id)
+ # entity = YourEntity.new(attr: "val")
+ #
+ # slap_xml = Slap.generate_xml(author_id, author_privkey, entity)
+ #
+ # @example Parsing a Salmon Slap
+ # slap = Slap.from_xml(slap_xml)
+ # author_pubkey = however_you_retrieve_the_authors_public_key(slap.author_id)
+ #
+ # entity = slap.entity(author_pubkey)
+ #
+ class Slap
+ attr_accessor :author_id, :magic_envelope, :cipher_params
+
+ # Returns new instance of the Entity that is contained within the XML of
+ # this Slap.
+ #
+ # The first time this is called, a public key has to be specified to verify
+ # the Magic Envelope signature. On repeated calls, the key may be omitted.
+ #
+ # @see MagicEnvelope.unenvelop
+ #
+ # @param [OpenSSL::PKey::RSA] pubkey public key for validating the signature
+ # @return [Entity] entity instance from the XML
+ # @raise [ArgumentError] if the public key is of the wrong type
+ def entity(pubkey=nil)
+ return @entity unless @entity.nil?
+
+ raise ArgumentError unless pubkey.instance_of?(OpenSSL::PKey::RSA)
+ @entity = MagicEnvelope.unenvelop(magic_envelope, pubkey, @cipher_params)
+ @entity
+ end
+
+ # Parses an unencrypted Salmon XML string and returns a new instance of
+ # {Slap} populated with the XML data.
+ #
+ # @param [String] slap_xml Salmon XML
+ # @return [Slap] new Slap instance
+ # @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::Document.parse(slap_xml)
+ ns = {d: Salmon::XMLNS, me: MagicEnvelope::XMLNS}
+ author_xpath = "d:diaspora/d:header/d:author_id"
+ magicenv_xpath = "d:diaspora/me:env"
+
+ if doc.namespaces.empty?
+ ns = nil
+ author_xpath = "diaspora/header/author_id"
+ magicenv_xpath = "diaspora/env"
+ end
+
+ slap = Slap.new
+
+ author_elem = doc.at_xpath(author_xpath, ns)
+ raise MissingAuthor if author_elem.nil? || author_elem.content.empty?
+ slap.author_id = author_elem.content
+
+ magic_env_elem = doc.at_xpath(magicenv_xpath, ns)
+ raise MissingMagicEnvelope if magic_env_elem.nil?
+ slap.magic_envelope = magic_env_elem
+
+ slap
+ end
+
+ # Creates an unencrypted Salmon Slap and returns the XML string.
+ #
+ # @param [String] author_id Diaspora* handle of the author
+ # @param [OpenSSL::PKey::RSA] pkey sender private_key for signing the magic envelope
+ # @param [Entity] entity payload
+ # @return [String] Salmon XML string
+ # @raise [ArgumentError] if any of the arguments is not the correct type
+ def self.generate_xml(author_id, pkey, entity)
+ raise ArgumentError unless author_id.instance_of?(String) &&
+ pkey.instance_of?(OpenSSL::PKey::RSA) &&
+ entity.is_a?(Entity)
+
+ doc = Nokogiri::XML::Document.new
+ doc.encoding = "UTF-8"
+
+ root = Nokogiri::XML::Element.new("diaspora", doc)
+ root.default_namespace = Salmon::XMLNS
+ root.add_namespace("me", MagicEnvelope::XMLNS)
+ doc.root = root
+
+ header = Nokogiri::XML::Element.new("header", doc)
+ root << header
+
+ author = Nokogiri::XML::Element.new("author_id", doc)
+ author.content = author_id
+ header << author
+
+ magic_envelope = MagicEnvelope.new(pkey, entity, root)
+ magic_envelope.envelop
+
+ doc.to_xml
+ 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
new file mode 100644
index 0000000..c9b1198
--- /dev/null
+++ b/spec/lib/diaspora_federation/salmon/encrypted_slap_spec.rb
@@ -0,0 +1,117 @@
+module DiasporaFederation
+ describe Salmon::EncryptedSlap do
+ let(:author_id) { "user_test@diaspora.example.tld" }
+ let(:pkey) { OpenSSL::PKey::RSA.generate(512) } # use small key for speedy specs
+ let(:okey) { OpenSSL::PKey::RSA.generate(1024) } # use small key for speedy specs
+ let(:entity) { Entities::TestEntity.new(test: "qwertzuiop") }
+ let(:slap_xml) { Salmon::EncryptedSlap.generate_xml(author_id, pkey, entity, okey.public_key) }
+ let(:ns) { {d: Salmon::XMLNS, me: Salmon::MagicEnvelope::XMLNS} }
+
+ context ".generate_xml" do
+ context "sanity" do
+ it "accepts correct params" do
+ expect {
+ Salmon::EncryptedSlap.generate_xml(author_id, pkey, entity, okey.public_key)
+ }.not_to raise_error
+ end
+
+ it "raises an error when the params are the wrong type" do
+ ["asdf", 1234, true, :symbol, entity, pkey].each do |val|
+ expect {
+ Salmon::EncryptedSlap.generate_xml(val, val, val, val)
+ }.to raise_error ArgumentError
+ end
+ end
+ end
+
+ it "generates valid xml" do
+ doc = Nokogiri::XML::Document.parse(slap_xml)
+ expect(doc.root.name).to eq("diaspora")
+ expect(doc.at_xpath("d:diaspora/d:encrypted_header", ns).content).to_not be_empty
+ expect(doc.xpath("d:diaspora/me:env", ns)).to have(1).item
+ end
+
+ context "header" do
+ subject {
+ doc = Nokogiri::XML::Document.parse(slap_xml)
+ doc.at_xpath("d:diaspora/d:encrypted_header", ns).content
+ }
+ let(:cipher_header) { JSON.parse(Base64.decode64(subject)) }
+ let(:header_key) {
+ JSON.parse(okey.private_decrypt(Base64.decode64(cipher_header["aes_key"])))
+ }
+
+ it "encoded the header correctly" do
+ json_header = {}
+ expect {
+ json_header = JSON.parse(Base64.decode64(subject))
+ }.not_to raise_error
+ expect(json_header).to include("aes_key", "ciphertext")
+ end
+
+ it "encrypted the public_key encrypted header correctly" do
+ key = {}
+ expect {
+ key = JSON.parse(okey.private_decrypt(Base64.decode64(cipher_header["aes_key"])))
+ }.not_to raise_error
+ expect(key).to include("key", "iv")
+ end
+
+ it "encrypted the aes encrypted header correctly" do
+ header = ""
+ expect {
+ header = Salmon::AES.decrypt(cipher_header["ciphertext"],
+ header_key["key"],
+ header_key["iv"])
+ }.not_to raise_error
+ header_doc = Nokogiri::XML::Document.parse(header)
+ expect(header_doc.root.name).to eq("decrypted_header")
+ expect(header_doc.xpath("//iv")).to have(1).item
+ expect(header_doc.xpath("//aes_key")).to have(1).item
+ expect(header_doc.xpath("//author_id")).to have(1).item
+ expect(header_doc.at_xpath("//author_id").content).to eq(author_id)
+ end
+ end
+ end
+
+ context ".from_xml" do
+ context "sanity" do
+ it "accepts correct params" do
+ expect {
+ Salmon::EncryptedSlap.from_xml(slap_xml, okey)
+ }.not_to raise_error
+ end
+
+ it "raises an error when the params have a wrong type" do
+ [1234, false, :symbol, entity, pkey].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
+ expect {
+ Salmon::EncryptedSlap.from_xml(faulty_xml, okey)
+ }.to raise_error Salmon::MissingHeader
+ end
+
+ it "verifies the existence of a magic envelope" do
+ faulty_xml = <
+
+
+XML
+ expect(Salmon::EncryptedSlap).to receive(:header_data).and_return(aes_key: "", iv: "", author_id: "")
+ expect {
+ Salmon::EncryptedSlap.from_xml(faulty_xml, okey)
+ }.to raise_error Salmon::MissingMagicEnvelope
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/diaspora_federation/salmon/magic_envelope_spec.rb b/spec/lib/diaspora_federation/salmon/magic_envelope_spec.rb
new file mode 100644
index 0000000..523c17e
--- /dev/null
+++ b/spec/lib/diaspora_federation/salmon/magic_envelope_spec.rb
@@ -0,0 +1,162 @@
+module DiasporaFederation
+ describe Salmon::MagicEnvelope do
+ let(:payload) { Entities::TestEntity.new(test: "asdf") }
+ let(:pkey) { OpenSSL::PKey::RSA.generate(512) } # use small key for speedy specs
+ let(:envelope) { Salmon::MagicEnvelope.new(pkey, payload).envelop }
+
+ def sig_subj(env)
+ data = Base64.urlsafe_decode64(env.at_xpath("me:data").content)
+ type = env.at_xpath("me:data")["type"]
+ enc = env.at_xpath("me:encoding").content
+ alg = env.at_xpath("me:alg").content
+
+ [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(pkey, 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)
+ }.to raise_error ArgumentError
+ end
+ end
+ end
+
+ context "#envelop" do
+ subject { Salmon::MagicEnvelope.new(pkey, payload) }
+
+ it "should be an instance of Nokogiri::XML::Element" do
+ expect(subject.envelop).to be_an_instance_of Nokogiri::XML::Element
+ end
+
+ it "returns a magic envelope of correct structure" do
+ env = subject.envelop
+ expect(env.name).to eq("env")
+
+ control = %w(data encoding alg sig)
+ env.children.each do |node|
+ expect(control).to include(node.name)
+ control.reject! {|i| i == node.name }
+ end
+
+ expect(control).to be_empty
+ end
+
+ it "signs the payload correctly" do
+ env = subject.envelop
+
+ subj = sig_subj(env)
+ sig = Base64.urlsafe_decode64(env.at_xpath("me:sig").content)
+
+ expect(pkey.public_key.verify(OpenSSL::Digest::SHA256.new, sig, subj)).to be_truthy
+ end
+ end
+
+ context "#encrypt!" do
+ subject { Salmon::MagicEnvelope.new(pkey, payload) }
+
+ it "encrypts the payload, returning cipher params" do
+ params = {}
+ expect {
+ params = subject.encrypt!
+ }.not_to raise_error
+ expect(params).to include(:key, :iv)
+ end
+
+ it "actually encrypts the payload" do
+ plain_payload = subject.payload
+ params = subject.encrypt!
+ encrypted_payload = subject.payload
+
+ cipher = OpenSSL::Cipher.new(Salmon::AES::CIPHER)
+ cipher.encrypt
+ cipher.iv = Base64.decode64(params[:iv])
+ cipher.key = Base64.decode64(params[:key])
+
+ ciphertext = cipher.update(plain_payload) + cipher.final
+
+ expect(Base64.strict_encode64(ciphertext)).to eq(encrypted_payload)
+ end
+ end
+
+ context ".unenvelop" do
+ context "sanity" do
+ it "works with sane input" do
+ expect {
+ Salmon::MagicEnvelope.unenvelop(envelope, pkey.public_key)
+ }.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.unenvelop(val, val)
+ }.to raise_error ArgumentError
+ end
+ end
+
+ it "verifies the envelope structure" do
+ expect {
+ Salmon::MagicEnvelope.unenvelop(Nokogiri::XML::Document.parse("").root, pkey.public_key)
+ }.to raise_error Salmon::InvalidEnvelope
+ end
+
+ it "verifies the signature" do
+ other_key = OpenSSL::PKey::RSA.generate(512)
+ expect {
+ Salmon::MagicEnvelope.unenvelop(envelope, other_key.public_key)
+ }.to raise_error Salmon::InvalidSignature
+ end
+
+ it "verifies the encoding" do
+ bad_env = Salmon::MagicEnvelope.new(pkey, payload).envelop
+ elem = bad_env.at_xpath("me:encoding")
+ elem.content = "invalid_enc"
+ re_sign(bad_env, pkey)
+ expect {
+ Salmon::MagicEnvelope.unenvelop(bad_env, pkey.public_key)
+ }.to raise_error Salmon::InvalidEncoding
+ end
+
+ it "verifies the algorithm" do
+ bad_env = Salmon::MagicEnvelope.new(pkey, payload).envelop
+ elem = bad_env.at_xpath("me:alg")
+ elem.content = "invalid_alg"
+ re_sign(bad_env, pkey)
+ expect {
+ Salmon::MagicEnvelope.unenvelop(bad_env, pkey.public_key)
+ }.to raise_error Salmon::InvalidAlgorithm
+ end
+ end
+
+ it "returns the original entity" do
+ entity = Salmon::MagicEnvelope.unenvelop(envelope, pkey.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(pkey, payload)
+ params = env.encrypt!
+
+ envelope = env.envelop
+
+ entity = Salmon::MagicEnvelope.unenvelop(envelope, pkey.public_key, params)
+ expect(entity).to be_an_instance_of Entities::TestEntity
+ expect(entity.test).to eq("asdf")
+ end
+ end
+ end
+end
diff --git a/spec/lib/diaspora_federation/salmon/slap_spec.rb b/spec/lib/diaspora_federation/salmon/slap_spec.rb
new file mode 100644
index 0000000..dc1de9a
--- /dev/null
+++ b/spec/lib/diaspora_federation/salmon/slap_spec.rb
@@ -0,0 +1,76 @@
+module DiasporaFederation
+ describe Salmon::Slap do
+ let(:author_id) { "test_user@pod.somedomain.tld" }
+ let(:pkey) { OpenSSL::PKey::RSA.generate(512) } # use small key for speedy specs
+ let(:entity) { Entities::TestEntity.new(test: "qwertzuiop") }
+ let(:slap) { Salmon::Slap.generate_xml(author_id, pkey, entity) }
+
+ context ".generate_xml" do
+ context "sanity" do
+ it "accepts correct params" do
+ expect {
+ Salmon::Slap.generate_xml(author_id, pkey, entity)
+ }.not_to raise_error
+ end
+
+ it "raises an error when the params are the wrong type" do
+ ["asdf", 1234, true, :symbol, entity, pkey].each do |val|
+ expect {
+ Salmon::Slap.generate_xml(val, val, val)
+ }.to raise_error ArgumentError
+ end
+ end
+ end
+
+ it "generates valid xml" do
+ ns = {d: Salmon::XMLNS, me: Salmon::MagicEnvelope::XMLNS}
+ doc = Nokogiri::XML::Document.parse(slap)
+ expect(doc.root.name).to eq("diaspora")
+ expect(doc.at_xpath("d:diaspora/d:header/d:author_id", ns).content).to eq(author_id)
+ expect(doc.xpath("d:diaspora/me:env", ns)).to have(1).item
+ end
+ end
+
+ context ".from_xml" do
+ context "sanity" do
+ it "accepts salmon xml as param" do
+ expect {
+ Salmon::Slap.from_xml(slap)
+ }.not_to raise_error
+ end
+
+ it "raises an error when the param has a wrong type" do
+ [1234, false, :symbol, entity, pkey].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
+ 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
+
+
+
+XML
+ expect {
+ Salmon::Slap.from_xml(faulty_xml)
+ }.to raise_error Salmon::MissingMagicEnvelope
+ end
+ end
+ end
+ end
+end