add salmon-classes (+tests) from raven24's gem

and do some basic refactorings for rubocop
This commit is contained in:
Benjamin Neff 2015-09-26 01:51:27 +02:00
parent 778a782c76
commit 5a81d38e60
8 changed files with 907 additions and 0 deletions

View file

@ -2,8 +2,14 @@ module DiasporaFederation
# This module contains a Diaspora*-specific implementation of parts of the # This module contains a Diaspora*-specific implementation of parts of the
# {http://www.salmon-protocol.org/ Salmon Protocol}. # {http://www.salmon-protocol.org/ Salmon Protocol}.
module Salmon module Salmon
# XML namespace url
XMLNS = "https://joindiaspora.com/protocol"
end end
end end
require "diaspora_federation/salmon/aes"
require "diaspora_federation/salmon/exceptions" require "diaspora_federation/salmon/exceptions"
require "diaspora_federation/salmon/xml_payload" require "diaspora_federation/salmon/xml_payload"
require "diaspora_federation/salmon/magic_envelope"
require "diaspora_federation/salmon/slap"
require "diaspora_federation/salmon/encrypted_slap"

View 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

View 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

View 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

View 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

View 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

View 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

View 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