add salmon-classes (+tests) from raven24's gem
and do some basic refactorings for rubocop
This commit is contained in:
parent
778a782c76
commit
5a81d38e60
8 changed files with 907 additions and 0 deletions
|
|
@ -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"
|
||||
|
|
|
|||
43
lib/diaspora_federation/salmon/aes.rb
Normal file
43
lib/diaspora_federation/salmon/aes.rb
Normal file
|
|
@ -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
|
||||
202
lib/diaspora_federation/salmon/encrypted_slap.rb
Normal file
202
lib/diaspora_federation/salmon/encrypted_slap.rb
Normal file
|
|
@ -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:
|
||||
#
|
||||
# <?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 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
|
||||
182
lib/diaspora_federation/salmon/magic_envelope.rb
Normal file
182
lib/diaspora_federation/salmon/magic_envelope.rb
Normal file
|
|
@ -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:
|
||||
#
|
||||
# <me:env>
|
||||
# <me:data type="application/xml">{data}</me:data>
|
||||
# <me:encoding>base64url</me:encoding>
|
||||
# <me:alg>RSA-SHA256</me:alg>
|
||||
# <me:sig>{signature}</me:sig>
|
||||
# </me:env>
|
||||
#
|
||||
# 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<String>]
|
||||
def self.sig_subject(data_arr)
|
||||
data_arr.map {|i| Base64.urlsafe_encode64(i) }.join(".")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
119
lib/diaspora_federation/salmon/slap.rb
Normal file
119
lib/diaspora_federation/salmon/slap.rb
Normal file
|
|
@ -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:
|
||||
#
|
||||
# <?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 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
|
||||
117
spec/lib/diaspora_federation/salmon/encrypted_slap_spec.rb
Normal file
117
spec/lib/diaspora_federation/salmon/encrypted_slap_spec.rb
Normal file
|
|
@ -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
|
||||
<diaspora>
|
||||
</diaspora>
|
||||
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
|
||||
<diaspora>
|
||||
<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, okey)
|
||||
}.to raise_error Salmon::MissingMagicEnvelope
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
162
spec/lib/diaspora_federation/salmon/magic_envelope_spec.rb
Normal file
162
spec/lib/diaspora_federation/salmon/magic_envelope_spec.rb
Normal file
|
|
@ -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("<asdf/>").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
|
||||
76
spec/lib/diaspora_federation/salmon/slap_spec.rb
Normal file
76
spec/lib/diaspora_federation/salmon/slap_spec.rb
Normal file
|
|
@ -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
|
||||
<diaspora>
|
||||
<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>
|
||||
<header>
|
||||
<author_id>#{author_id}</author_id>
|
||||
</header>
|
||||
</diaspora>
|
||||
XML
|
||||
expect {
|
||||
Salmon::Slap.from_xml(faulty_xml)
|
||||
}.to raise_error Salmon::MissingMagicEnvelope
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue