refactor encrypted slap to reuse it for every recipient

This commit is contained in:
Benjamin Neff 2016-01-14 03:42:36 +01:00
parent c7f33d7cf4
commit a0398430ed
12 changed files with 204 additions and 130 deletions

View file

@ -67,7 +67,7 @@ module DiasporaFederation
# Generates an XML document from the current instance and returns it as string
# @return [String] XML document
def to_xml
builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
Nokogiri::XML::Builder.new(encoding: "UTF-8") {|xml|
xml.XRD("xmlns" => XMLNS) {
xml.Expires(@expires.strftime(DATETIME_FORMAT)) if @expires.instance_of?(DateTime)
@ -77,8 +77,7 @@ module DiasporaFederation
add_properties_to(xml)
add_links_to(xml)
}
end
builder.to_xml
}.to_xml
end
# Parse the XRD document from the given string and create a hash containing

View file

@ -49,7 +49,7 @@ module DiasporaFederation
# resolved on each call
# @return [Hash] default values
def default_values
default_props.each_with_object({}) { |(name, prop), hash|
default_props.each_with_object({}) {|(name, prop), hash|
hash[name] = prop.respond_to?(:call) ? prop.call : prop
}
end
@ -105,7 +105,7 @@ module DiasporaFederation
# @param [Class] type the type to check
# @return [Boolean]
def type_valid?(type)
[type].flatten.all? { |type|
[type].flatten.all? {|type|
type.respond_to?(:ancestors) && type.ancestors.include?(Entity)
}
end

View file

@ -63,14 +63,22 @@ module DiasporaFederation
#
# entity = slap.entity(author_pubkey)
#
class EncryptedSlap
class EncryptedSlap < Slap
# the key and iv if it is an encrypted slap
# @param [Hash] value hash containing the key and iv
attr_writer :cipher_params
# the prepared encrypted magic envelope xml
# @param [Nokogiri::XML::Element] value magic envelope xml
attr_writer :magic_envelope_xml
# 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] privkey recipient private_key for decryption
#
# @return [Slap] new Slap instance
# @return [EncryptedSlap] 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
@ -79,7 +87,7 @@ module DiasporaFederation
raise ArgumentError unless slap_xml.instance_of?(String) && privkey.instance_of?(OpenSSL::PKey::RSA)
doc = Nokogiri::XML::Document.parse(slap_xml)
Slap.new.tap do |slap|
EncryptedSlap.new.tap do |slap|
header_elem = doc.at_xpath("d:diaspora/d:encrypted_header", Slap::NS)
raise MissingHeader if header_elem.nil?
header = header_data(header_elem.content, privkey)
@ -90,29 +98,44 @@ module DiasporaFederation
end
end
# Creates an encrypted Salmon Slap and returns the XML string.
# Creates an encrypted Salmon Slap.
#
# @param [String] author_id Diaspora* handle of the author
# @param [OpenSSL::PKey::RSA] privkey sender private key for signing the magic envelope
# @param [Entity] entity payload
# @return [EncryptedSlap] encrypted Slap instance
# @raise [ArgumentError] if any of the arguments is of the wrong type
def self.prepare(author_id, privkey, entity)
raise ArgumentError unless author_id.instance_of?(String) &&
privkey.instance_of?(OpenSSL::PKey::RSA) &&
entity.is_a?(Entity)
EncryptedSlap.new.tap do |slap|
slap.author_id = author_id
magic_envelope = MagicEnvelope.new(privkey, entity)
slap.cipher_params = magic_envelope.encrypt!
slap.magic_envelope_xml = magic_envelope.envelop
end
end
# Creates an encrypted Salmon Slap XML string.
#
# @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, privkey, entity, pubkey)
raise ArgumentError unless author_id.instance_of?(String) &&
privkey.instance_of?(OpenSSL::PKey::RSA) &&
entity.is_a?(Entity) &&
pubkey.instance_of?(OpenSSL::PKey::RSA)
def generate_xml(pubkey)
raise ArgumentError unless pubkey.instance_of?(OpenSSL::PKey::RSA)
Slap.build_xml do |xml|
magic_envelope = MagicEnvelope.new(privkey, entity)
envelope_key = magic_envelope.encrypt!
xml.encrypted_header(encrypted_header(author_id, @cipher_params, pubkey))
encrypted_header(author_id, envelope_key, pubkey, xml)
magic_envelope.envelop(xml)
xml.parent << @magic_envelope_xml
end
end
private
# decrypts and reads the data from the encrypted XML header
# @param [String] data base64 encoded, encrypted header data
# @param [OpenSSL::PKey::RSA] privkey private key for decryption
@ -147,43 +170,33 @@ module DiasporaFederation
# @param [String] author_id diaspora_handle
# @param [Hash] envelope_key envelope cipher params
# @param [OpenSSL::PKey::RSA] pubkey recipient public_key
# @param [Nokogiri::XML::Element] xml parent element for inserting in XML document
def self.encrypted_header(author_id, envelope_key, pubkey, xml)
data = header_xml(author_id, strict_base64_encode(envelope_key))
key = AES.generate_key_and_iv
ciphertext = AES.encrypt(data, key[:key], key[:iv])
# @return [String] encrypted base64 encoded header
def encrypted_header(author_id, envelope_key, pubkey)
encoded_key = Hash[envelope_key.map {|k, v| [k, Base64.strict_encode64(v)] }]
data = header_xml(author_id, encoded_key)
ciphertext = AES.encrypt(data, envelope_key[:key], envelope_key[:iv])
json_key = JSON.generate(strict_base64_encode(key))
json_key = JSON.generate(encoded_key)
encrypted_key = Base64.strict_encode64(pubkey.public_encrypt(json_key))
json_header = JSON.generate(aes_key: encrypted_key, ciphertext: ciphertext)
xml.encrypted_header(Base64.strict_encode64(json_header))
Base64.strict_encode64(json_header)
end
private_class_method :encrypted_header
# generate the header xml string, including the author, aes_key and iv
# @param [String] author_id diaspora_handle of the author
# @param [Hash] envelope_key { 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|
def header_xml(author_id, envelope_key)
@header_xml ||= Nokogiri::XML::Builder.new(encoding: "UTF-8") {|xml|
xml.decrypted_header {
xml.iv(envelope_key[:iv])
xml.aes_key(envelope_key[:key])
xml.author_id(author_id)
}
}.to_xml.strip
end
builder.to_xml.strip
end
private_class_method :header_xml
# @param [Hash] hash { key: "...", iv: "..." }
# @return [Hash] encoded hash: { key: "...", iv: "..." }
def self.strict_base64_encode(hash)
Hash[hash.map {|k, v| [k, Base64.strict_encode64(v)] }]
end
private_class_method :strict_base64_encode
end
end
end

View file

@ -55,14 +55,20 @@ module DiasporaFederation
# Builds the XML structure for the magic envelope, inserts the {ENCODING}
# encoded data and signs the envelope using {DIGEST}.
#
# @param [Nokogiri::XML::Builder] xml Salmon XML builder
def envelop(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))
# @return [Nokogiri::XML::Element] XML root node
def envelop
env_doc = Nokogiri::XML::DocumentFragment.new(Nokogiri::XML::Document.new)
Nokogiri::XML::Element.new("me:env", env_doc).tap do |env|
env << Nokogiri::XML::Element.new("me:data", env_doc).tap {|node|
node.content = Base64.urlsafe_encode64(@payload)
node["type"] = DATA_TYPE
}
env << Nokogiri::XML::Element.new("me:encoding", env_doc).tap {|node| node.content = ENCODING }
env << Nokogiri::XML::Element.new("me:alg", env_doc).tap {|node| node.content = ALGORITHM }
env << Nokogiri::XML::Element.new("me:sig", env_doc).tap {|node|
node.content = Base64.urlsafe_encode64(signature)
}
end
end
# Encrypts the payload with a new, random AES cipher and returns the cipher
@ -102,8 +108,8 @@ module DiasporaFederation
# @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 ArgumentError unless magic_env.instance_of?(Nokogiri::XML::Element) &&
rsa_pubkey.instance_of?(OpenSSL::PKey::RSA)
raise InvalidEnvelope unless envelope_valid?(magic_env)
raise InvalidSignature unless signature_valid?(magic_env, rsa_pubkey)

View file

@ -33,10 +33,6 @@ module DiasporaFederation
# @param [String] the author diaspora id
attr_accessor :author_id
# the key and iv if it is an encrypted slap
# @param [Hash] value hash containing the key and iv
attr_writer :cipher_params
# Namespaces
NS = {d: Salmon::XMLNS, me: MagicEnvelope::XMLNS}
@ -94,7 +90,7 @@ module DiasporaFederation
xml.author_id(author_id)
}
MagicEnvelope.new(privkey, entity).envelop(xml)
xml.parent << MagicEnvelope.new(privkey, entity).envelop
end
end
@ -104,12 +100,11 @@ module DiasporaFederation
# {http://www.rubydoc.info/gems/nokogiri/Nokogiri/XML/Builder Nokogiri::XML::Builder}
# @return [String] Slap XML
def self.build_xml
builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
Nokogiri::XML::Builder.new(encoding: "UTF-8") {|xml|
xml.diaspora("xmlns" => Salmon::XMLNS, "xmlns:me" => MagicEnvelope::XMLNS) {
yield xml
}
end
builder.to_xml
}.to_xml
end
# Parses the magic envelop from the document.

View file

@ -89,7 +89,7 @@ module DiasporaFederation
# and attached to resulted hash as string. It is intended to build a hash
# invariable of an Entity definition, in order to support receiving objects
# from the future versions of Diaspora, where new elements may have been added.
data = Hash[root_node.element_children.map { |child|
data = Hash[root_node.element_children.map {|child|
xml_name = child.name
property = klass.class_props.find {|prop| prop[:xml_name].to_s == xml_name }
if property

View file

@ -48,7 +48,7 @@ module DiasporaFederation
# @param [Hash] hash data to sign
# @return [String] signature data string
def self.signable_string(hash)
hash.map { |name, value|
hash.map {|name, value|
value.to_s unless name.match(/signature/)
}.compact.join(";")
end

View file

@ -11,7 +11,7 @@ module DiasporaFederation
# @param [Entity.Class] klass entity type to sort according to
# @return [Hash] sorted hash
def self.sort_hash(data, klass)
Hash[klass.class_props.map { |prop|
Hash[klass.class_props.map {|prop|
[prop[:name], data[prop[:name]]] unless data[prop[:name]].nil?
}.compact]
end

View file

@ -4,12 +4,8 @@ module DiasporaFederation
let(:sender_key) { OpenSSL::PKey::RSA.generate(1024) }
let(:recipient_key) { OpenSSL::PKey::RSA.generate(1024) }
let(:xml) {
DiasporaFederation::Salmon::EncryptedSlap.generate_xml(
sender_id,
sender_key,
FactoryGirl.build(:request_entity),
recipient_key
)
DiasporaFederation::Salmon::EncryptedSlap.prepare(sender_id, sender_key, FactoryGirl.build(:request_entity))
.generate_xml(recipient_key)
}
it "calls save_entity_after_receive if everything is fine" do

View file

@ -4,21 +4,51 @@ module DiasporaFederation
let(:privkey) { OpenSSL::PKey::RSA.generate(512) } # use small key for speedy specs
let(:recipient_key) { 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, privkey, entity, recipient_key.public_key) }
let(:ns) { {d: Salmon::XMLNS, me: Salmon::MagicEnvelope::XMLNS} }
let(:slap_xml) { Salmon::EncryptedSlap.prepare(author_id, privkey, entity).generate_xml(recipient_key.public_key) }
context "generate" do
describe ".prepare" do
context "sanity" do
it "raises an error when the author_id is the wrong type" do
[1234, true, :symbol, entity, privkey].each do |val|
expect {
Salmon::EncryptedSlap.prepare(val, privkey, entity)
}.to raise_error ArgumentError
end
end
it "raises an error when the privkey is the wrong type" do
["asdf", 1234, true, :symbol, entity].each do |val|
expect {
Salmon::EncryptedSlap.prepare(author_id, val, entity)
}.to raise_error ArgumentError
end
end
it "raises an error when the entity is the wrong type" do
["asdf", 1234, true, :symbol, privkey].each do |val|
expect {
Salmon::EncryptedSlap.prepare(author_id, privkey, val)
}.to raise_error ArgumentError
end
end
end
end
describe ".generate_xml" do
let(:ns) { {d: Salmon::XMLNS, me: Salmon::MagicEnvelope::XMLNS} }
context "sanity" do
it "accepts correct params" do
expect {
Salmon::EncryptedSlap.generate_xml(author_id, privkey, entity, recipient_key.public_key)
Salmon::EncryptedSlap.prepare(author_id, privkey, entity).generate_xml(recipient_key.public_key)
}.not_to raise_error
end
it "raises an error when the params are the wrong type" do
["asdf", 1234, true, :symbol, entity, privkey].each do |val|
["asdf", 1234, true, :symbol, entity].each do |val|
expect {
Salmon::EncryptedSlap.generate_xml(val, val, val, val)
Salmon::EncryptedSlap.prepare(author_id, privkey, entity).generate_xml(val)
}.to raise_error ArgumentError
end
end
@ -31,6 +61,25 @@ module DiasporaFederation
expect(doc.xpath("d:diaspora/me:env", ns)).to have(1).item
end
it "can generate xml for two people" do
slap = Salmon::EncryptedSlap.prepare(author_id, privkey, entity)
doc1 = Nokogiri::XML::Document.parse(slap.generate_xml(recipient_key.public_key))
enc_header1 = doc1.at_xpath("d:diaspora/d:encrypted_header", ns).content
cipher_header1 = JSON.parse(Base64.decode64(enc_header1))
key_json1 = recipient_key.private_decrypt(Base64.decode64(cipher_header1["aes_key"]))
recipient2_key = OpenSSL::PKey::RSA.generate(1024)
doc2 = Nokogiri::XML::Document.parse(slap.generate_xml(recipient2_key.public_key))
enc_header2 = doc2.at_xpath("d:diaspora/d:encrypted_header", ns).content
cipher_header2 = JSON.parse(Base64.decode64(enc_header2))
key_json2 = recipient2_key.private_decrypt(Base64.decode64(cipher_header2["aes_key"]))
expect(enc_header1).not_to eq(enc_header2)
expect(key_json1).to eq(key_json2)
expect(doc1.xpath("d:diaspora/me:env", ns).to_xml).to eq(doc2.xpath("d:diaspora/me:env", ns).to_xml)
end
context "header" do
subject {
doc = Nokogiri::XML::Document.parse(slap_xml)
@ -73,6 +122,7 @@ module DiasporaFederation
end
end
end
end
describe ".from_xml" do
context "sanity" do

View file

@ -5,12 +5,11 @@ module DiasporaFederation
let(:envelope) { envelop_xml(Salmon::MagicEnvelope.new(privkey, payload)) }
def envelop_xml(magic_env)
builder = Nokogiri::XML::Builder.new(encoding: "UTF-8") do |xml|
Nokogiri::XML::Builder.new(encoding: "UTF-8") {|xml|
xml.root("xmlns:me" => Salmon::MagicEnvelope::XMLNS) {
magic_env.envelop(xml)
xml.parent << magic_env.envelop
}
end
builder.doc.at_xpath("//me:env")
}.doc.at_xpath("//me:env")
end
def sig_subj(env)

View file

@ -3,7 +3,7 @@ module DiasporaFederation
let(:author_id) { "test_user@pod.somedomain.tld" }
let(:privkey) { 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, privkey, entity) }
let(:slap_xml) { Salmon::Slap.generate_xml(author_id, privkey, entity) }
describe ".generate_xml" do
context "sanity" do
@ -13,10 +13,26 @@ module DiasporaFederation
}.not_to raise_error
end
it "raises an error when the params are the wrong type" do
["asdf", 1234, true, :symbol, entity, privkey].each do |val|
it "raises an error when the author_id is the wrong type" do
[1234, true, :symbol, entity, privkey].each do |val|
expect {
Salmon::Slap.generate_xml(val, val, val)
Salmon::Slap.generate_xml(val, privkey, entity)
}.to raise_error ArgumentError
end
end
it "raises an error when the privkey is the wrong type" do
["asdf", 1234, true, :symbol, entity].each do |val|
expect {
Salmon::Slap.generate_xml(author_id, val, entity)
}.to raise_error ArgumentError
end
end
it "raises an error when the entity is the wrong type" do
["asdf", 1234, true, :symbol, privkey].each do |val|
expect {
Salmon::Slap.generate_xml(author_id, privkey, val)
}.to raise_error ArgumentError
end
end
@ -24,7 +40,7 @@ module DiasporaFederation
it "generates valid xml" do
ns = {d: Salmon::XMLNS, me: Salmon::MagicEnvelope::XMLNS}
doc = Nokogiri::XML::Document.parse(slap)
doc = Nokogiri::XML::Document.parse(slap_xml)
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
@ -35,7 +51,7 @@ module DiasporaFederation
context "sanity" do
it "accepts salmon xml as param" do
expect {
Salmon::Slap.from_xml(slap)
Salmon::Slap.from_xml(slap_xml)
}.not_to raise_error
end
@ -75,7 +91,7 @@ XML
context "generated instance" do
it_behaves_like "a Slap instance" do
subject { Salmon::Slap.from_xml(slap) }
subject { Salmon::Slap.from_xml(slap_xml) }
end
end
end