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

payload[R]

The payload entity of the magic envelope @return [Entity] payload entity

sender[R]

The sender of the magic envelope @return [String] diaspora-ID of the sender

Public Class Methods

new(payload, sender=nil) click to toggle source

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
unenvelop(magic_env, sender=nil, cipher_params=nil) click to toggle source

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

read_and_decrypt_data(magic_env, cipher_params) click to toggle source

@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
sender(env) click to toggle source

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
sig_subject(data_arr) click to toggle source

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
signature_valid?(env, sender) click to toggle source

@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
validate_algorithm(magic_env) click to toggle source

@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
validate_element(env, xpath) click to toggle source

@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
validate_encoding(magic_env) click to toggle source

@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
validate_envelope(env) click to toggle source

@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
validate_type(magic_env) click to toggle source

@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

envelop(privkey) click to toggle source

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

build_xml(&block) click to toggle source

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
key_id() click to toggle source
# File lib/diaspora_federation/salmon/magic_envelope.rb, line 131
def key_id
  sender ? {key_id: Base64.urlsafe_encode64(sender)} : {}
end
payload_data() click to toggle source

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
sign(privkey) click to toggle source

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