class DiasporaFederation::Salmon::MagicEnvelope
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:
<me:env> <me:data type="application/xml">{data}</me:data> <me:encoding>base64url</me:encoding> <me:alg>RSA-SHA256</me:alg> <me:sig key_id="{sender}">{signature}</me:sig> </me:env>
When parsing the XML of an incoming Magic Envelope {MagicEnvelope.unenvelop} is used.
@see cdn.rawgit.com/salmon-protocol/salmon-protocol/master/draft-panzer-magicsig-01.html
Constants
- ALGORITHM
Algorithm used for signing the payload data
- DATA_TYPE
Mime type describing the payload data
- DIGEST
Digest instance used for signing
- ENCODING
Encoding used for the payload data
- XMLNS
XML namespace url
Attributes
The payload entity of the magic envelope @return [Entity] payload entity
The sender of the magic envelope @return [String] diaspora-ID of the sender
Public Class Methods
Creates a new instance of MagicEnvelope
.
@param [Entity] payload Entity
instance @param [String] sender diaspora-ID of the sender @raise [ArgumentError] if either argument is not of the right type
# File lib/diaspora_federation/salmon/magic_envelope.rb, line 56 def initialize(payload, sender=nil) raise ArgumentError unless payload.is_a?(Entity) @payload = payload @sender = sender 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 AES#decrypt
@param [Nokogiri::XML::Element] magic_env XML root node of a magic envelope @param [String] sender diaspora* ID of the sender or nil @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 [InvalidDataType] if the data is missing or unsupported @raise [InvalidEncoding] if the data is wrongly encoded or encoding is missing @raise [InvalidAlgorithm] if the algorithm is missing or doesn’t match
# File lib/diaspora_federation/salmon/magic_envelope.rb, line 102 def self.unenvelop(magic_env, sender=nil, cipher_params=nil) raise ArgumentError unless magic_env.instance_of?(Nokogiri::XML::Element) validate_envelope(magic_env) validate_type(magic_env) validate_encoding(magic_env) validate_algorithm(magic_env) sender ||= sender(magic_env) raise InvalidSignature unless signature_valid?(magic_env, sender) data = read_and_decrypt_data(magic_env, cipher_params) logger.debug "unenvelop message from #{sender}:\n#{data}" xml = Nokogiri::XML(data).root new(Entity.entity_class(xml.name).from_xml(xml), sender) end
Private Class Methods
@param [Nokogiri::XML::Element] magic_env magic envelope XML @param [Hash] cipher_params hash containing the key and iv @return [String] data
# File lib/diaspora_federation/salmon/magic_envelope.rb, line 233 def self.read_and_decrypt_data(magic_env, cipher_params) data = Base64.urlsafe_decode64(magic_env.at_xpath("me:data").content) data = AES.decrypt(data, cipher_params[:key], cipher_params[:iv]) unless cipher_params.nil? data end
Reads the key_id
from the magic envelope. @param [Nokogiri::XML::Element] env magic envelope XML @return [String] diaspora* ID of the sender
# File lib/diaspora_federation/salmon/magic_envelope.rb, line 190 def self.sender(env) key_id = env.at_xpath("me:sig")["key_id"] raise InvalidEnvelope, "no key_id" unless key_id # TODO: move to `envelope_valid?` Base64.urlsafe_decode64(key_id) end
Constructs the signature subject. The given array should consist of the data, data_type (mimetype), encoding and the algorithm. @param [Array<String>] data_arr @return [String] signature subject
# File lib/diaspora_federation/salmon/magic_envelope.rb, line 202 def self.sig_subject(data_arr) data_arr.map {|i| Base64.urlsafe_encode64(i) }.join(".") end
@param [Nokogiri::XML::Element] env magic envelope XML @param [String] sender diaspora* ID of the sender or nil @return [Boolean]
# File lib/diaspora_federation/salmon/magic_envelope.rb, line 174 def self.signature_valid?(env, sender) 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]) sender_key = DiasporaFederation.callbacks.trigger(:fetch_public_key, sender) raise SenderKeyNotFound unless sender_key sig = Base64.urlsafe_decode64(env.at_xpath("me:sig").content) sender_key.verify(DIGEST, sig, subject) end
@param [Nokogiri::XML::Element] magic_env magic envelope XML @raise [InvalidAlgorithm] if the algorithm is missing or doesn’t match
# File lib/diaspora_federation/salmon/magic_envelope.rb, line 224 def self.validate_algorithm(magic_env) alg = magic_env.at_xpath("me:alg") raise InvalidAlgorithm, "missing algorithm" unless alg raise InvalidAlgorithm, "invalid algorithm: #{alg.content}" unless alg.content == ALGORITHM end
@param [Nokogiri::XML::Element] env magic envelope XML @param [String] xpath the element to validate @raise [InvalidEnvelope] if the element is missing or empty
# File lib/diaspora_federation/salmon/magic_envelope.rb, line 165 def self.validate_element(env, xpath) element = env.at_xpath(xpath) raise InvalidEnvelope, "missing #{xpath}" unless element raise InvalidEnvelope, "empty #{xpath}" if element.content.empty? end
@param [Nokogiri::XML::Element] magic_env magic envelope XML @raise [InvalidEncoding] if the data is wrongly encoded or encoding is missing
# File lib/diaspora_federation/salmon/magic_envelope.rb, line 216 def self.validate_encoding(magic_env) enc = magic_env.at_xpath("me:encoding") raise InvalidEncoding, "missing encoding" unless enc raise InvalidEncoding, "invalid encoding: #{enc.content}" unless enc.content == ENCODING end
@param [Nokogiri::XML::Element] env magic envelope XML @raise [InvalidEnvelope] if the envelope XML structure is malformed
# File lib/diaspora_federation/salmon/magic_envelope.rb, line 155 def self.validate_envelope(env) raise InvalidEnvelope unless env.instance_of?(Nokogiri::XML::Element) && env.name == "env" validate_element(env, "me:data") validate_element(env, "me:sig") end
@param [Nokogiri::XML::Element] magic_env magic envelope XML @raise [InvalidDataType] if the data is missing or unsupported
# File lib/diaspora_federation/salmon/magic_envelope.rb, line 208 def self.validate_type(magic_env) type = magic_env.at_xpath("me:data")["type"] raise InvalidDataType, "missing data type" if type.nil? raise InvalidDataType, "invalid data type: #{type}" unless type == DATA_TYPE end
Public Instance Methods
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 @return [Nokogiri::XML::Element] XML root node
# File lib/diaspora_federation/salmon/magic_envelope.rb, line 68 def envelop(privkey) raise ArgumentError unless privkey.instance_of?(OpenSSL::PKey::RSA) build_xml {|xml| xml["me"].env("xmlns:me" => XMLNS) { xml["me"].data(Base64.urlsafe_encode64(payload_data), type: DATA_TYPE) xml["me"].encoding(ENCODING) xml["me"].alg(ALGORITHM) xml["me"].sig(Base64.urlsafe_encode64(sign(privkey)), key_id) } } end
Private Instance Methods
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
# File lib/diaspora_federation/salmon/magic_envelope.rb, line 140 def build_xml(&block) Nokogiri::XML::Builder.new(encoding: "UTF-8", &block).doc end
# File lib/diaspora_federation/salmon/magic_envelope.rb, line 131 def key_id sender ? {key_id: Base64.urlsafe_encode64(sender)} : {} end
The payload data as string @return [String] payload data
# File lib/diaspora_federation/salmon/magic_envelope.rb, line 125 def payload_data @payload_data ||= payload.to_xml.to_xml.strip.tap do |data| logger.debug "send payload:\n#{data}" end end
Creates the signature for all fields according to specification
@param [OpenSSL::PKey::RSA] privkey private key used for signing @return [String] the signature
# File lib/diaspora_federation/salmon/magic_envelope.rb, line 148 def sign(privkey) subject = MagicEnvelope.send(:sig_subject, [payload_data, DATA_TYPE, ENCODING, ALGORITHM]) privkey.sign(DIGEST, subject) end