From e0429ee823f7f5c948758cb6426bb265bb84772a Mon Sep 17 00:00:00 2001 From: Maxwell Salzberg Date: Thu, 8 Sep 2011 17:47:48 -0700 Subject: [PATCH] MS DG pulling apart salmon and making our custom hacks more obvious --- app/models/job/http_multi.rb | 2 +- app/models/user.rb | 2 +- lib/postzord/dispatch.rb | 2 +- lib/postzord/receiver.rb | 2 +- lib/postzord/receiver/public.rb | 16 ++ lib/salmon/encrypted_salmon_slap.rb | 25 ++ lib/salmon/magic_sig_envelope.rb | 70 ++++++ lib/salmon/salmon.rb | 224 +----------------- lib/salmon/salmon_slap.rb | 150 ++++++++++++ spec/lib/postzord/receiver/public.rb | 32 +++ spec/lib/postzord/receiver/public_spec.rb | 59 +++++ spec/lib/salmon/encrypted_salmon_slap_spec.rb | 89 +++++++ spec/lib/salmon/magic_sig_envelope_spec.rb | 5 + spec/lib/salmon/salmon_slap_spec.rb | 5 + 14 files changed, 458 insertions(+), 225 deletions(-) create mode 100644 lib/postzord/receiver/public.rb create mode 100644 lib/salmon/encrypted_salmon_slap.rb create mode 100644 lib/salmon/magic_sig_envelope.rb create mode 100644 lib/salmon/salmon_slap.rb create mode 100644 spec/lib/postzord/receiver/public.rb create mode 100644 spec/lib/postzord/receiver/public_spec.rb create mode 100644 spec/lib/salmon/encrypted_salmon_slap_spec.rb create mode 100644 spec/lib/salmon/magic_sig_envelope_spec.rb create mode 100644 spec/lib/salmon/salmon_slap_spec.rb diff --git a/app/models/job/http_multi.rb b/app/models/job/http_multi.rb index d24838717..40068c966 100644 --- a/app/models/job/http_multi.rb +++ b/app/models/job/http_multi.rb @@ -19,7 +19,7 @@ module Job people = Person.where(:id => person_ids) - salmon = Salmon::SalmonSlap.create(user, Base64.decode64(enc_object_xml)) + salmon = Salmon::EncryptedSalmonSlap.create(user, Base64.decode64(enc_object_xml)) failed_request_people = [] diff --git a/app/models/user.rb b/app/models/user.rb index 10fef5322..31731d743 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -215,7 +215,7 @@ class User < ActiveRecord::Base end def salmon(post) - Salmon::SalmonSlap.create(self, post.to_diaspora_xml) + Salmon::EncryptedSalmonSlap.create(self, post.to_diaspora_xml) end def build_relayable(model, options = {}) diff --git a/lib/postzord/dispatch.rb b/lib/postzord/dispatch.rb index c3390a75b..81c61e35c 100644 --- a/lib/postzord/dispatch.rb +++ b/lib/postzord/dispatch.rb @@ -18,7 +18,7 @@ class Postzord::Dispatch end def salmon - @salmon_factory ||= Salmon::SalmonSlap.create(@sender, @xml) + @salmon_factory ||= Salmon::EncryptedSalmonSlap.create(@sender, @xml) end def post(opts = {}) diff --git a/lib/postzord/receiver.rb b/lib/postzord/receiver.rb index 37029a5f8..4a49647ad 100644 --- a/lib/postzord/receiver.rb +++ b/lib/postzord/receiver.rb @@ -45,7 +45,7 @@ module Postzord protected def salmon - @salmon ||= Salmon::SalmonSlap.parse(@salmon_xml, @user) + @salmon ||= Salmon::EncryptedSalmonSlap.parse(@salmon_xml, @user) end def xml_author diff --git a/lib/postzord/receiver/public.rb b/lib/postzord/receiver/public.rb new file mode 100644 index 000000000..c2b6199ec --- /dev/null +++ b/lib/postzord/receiver/public.rb @@ -0,0 +1,16 @@ +# Copyright (c) 2010, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. +# +module Postzord + class Receiver + class Public + attr_accessor :xml + + def initialize(xml) + @xml = xml + + end + end + end +end diff --git a/lib/salmon/encrypted_salmon_slap.rb b/lib/salmon/encrypted_salmon_slap.rb new file mode 100644 index 000000000..d40a5c390 --- /dev/null +++ b/lib/salmon/encrypted_salmon_slap.rb @@ -0,0 +1,25 @@ +# Copyright (c) 2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +module Salmon + class EncryptedSalmonSlap < SalmonSlap + def header(person) + < + #{person.encrypt("#{plaintext_header}")} + +XML + end + + def parse_data(key_hash, user) + user.aes_decrypt(super, key_hash) + end + + # @return [Nokogiri::Doc] + def salmon_header(doc, user) + header = user.decrypt(doc.search('encrypted_header').text) + Nokogiri::XML(header) + end + end +end diff --git a/lib/salmon/magic_sig_envelope.rb b/lib/salmon/magic_sig_envelope.rb new file mode 100644 index 000000000..cd7b9c34f --- /dev/null +++ b/lib/salmon/magic_sig_envelope.rb @@ -0,0 +1,70 @@ +# Copyright (c) 2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +module Salmon + class MagicSigEnvelope + attr_accessor :data, :data_type, :encoding, :alg, :sig, :author + def self.parse(doc) + env = self.new + ns = {'me'=>'http://salmon-protocol.org/ns/magic-env'} + env.encoding = doc.search('//me:env/me:encoding', ns).text.strip + + if env.encoding != 'base64url' + raise ArgumentError, "Magic Signature data must be encoded with base64url, was #{slap.magic_sig.encoding}" + end + + env.data = doc.search('//me:env/me:data', ns).text + env.alg = doc.search('//me:env/me:alg', ns).text.strip + env.sig = doc.search('//me:env/me:sig', ns).text + env.data_type = doc.search('//me:env/me:data', ns).first['type'].strip + + unless 'RSA-SHA256' == env.alg + raise ArgumentError, "Magic Signature data must be signed with RSA-SHA256, was #{env.alg}" + end + + env + end + + def self.create(user, activity) + env = MagicSigEnvelope.new + env.author = user.person + env.data = Base64.urlsafe_encode64(activity) + env.data_type = env.get_data_type + env.encoding = env.get_encoding + env.alg = env.get_alg + + env.sig = Base64.urlsafe_encode64( + user.encryption_key.sign OpenSSL::Digest::SHA256.new, env.signable_string ) + + env + end + + def signable_string + [@data, Base64.urlsafe_encode64(@data_type),Base64.urlsafe_encode64(@encoding), Base64.urlsafe_encode64(@alg)].join(".") + end + + def to_xml + < + #{@data} + #{@encoding} + #{@alg} + #{@sig} + +ENTRY + end + + def get_encoding + 'base64url' + end + + def get_data_type + 'application/atom+xml' + end + + def get_alg + 'RSA-SHA256' + end + end +end diff --git a/lib/salmon/salmon.rb b/lib/salmon/salmon.rb index 2164ba8b9..0099fe0b4 100644 --- a/lib/salmon/salmon.rb +++ b/lib/salmon/salmon.rb @@ -39,225 +39,7 @@ end # Verify documents secured with Magic Signatures module Salmon - - class SalmonSlap - attr_accessor :magic_sig, :author, :author_email, :aes_key, :iv, :parsed_data, - :data_type, :sig - - def self.create(user, activity) - salmon = self.new - salmon.author = user.person - aes_key_hash = user.person.gen_aes_key - salmon.aes_key = aes_key_hash['key'] - salmon.iv = aes_key_hash['iv'] - salmon.magic_sig = MagicSigEnvelope.create(user , user.person.aes_encrypt(activity, aes_key_hash)) - salmon - end - - def self.parse(xml, user=nil) - slap = self.new - doc = Nokogiri::XML(xml) - - sig_doc = doc.search('entry') - - ### Header ## - header_doc = slap.salmon_header(doc, user) - slap.author_email= header_doc.search('uri').text.split("acct:").last - slap.aes_key = header_doc.search('aes_key').text - slap.iv = header_doc.search('iv').text - - slap.magic_sig = MagicSigEnvelope.parse sig_doc - - key_hash = {'key' => slap.aes_key, 'iv' => slap.iv} - slap.parsed_data = slap.parse_data(key_hash, user) - slap.sig = slap.magic_sig.sig - slap.data_type = slap.magic_sig.data_type - - slap - end - - def parse_data(key_hash, user) - data = SalmonSlap.decode64url(self.magic_sig.data) - if user.present? - user.aes_decrypt(data, key_hash) - else - data - end - end - - # @return [Nokogiri::Doc] - def salmon_header(doc, user) - if user.present? - decrypted_header = user.decrypt(doc.search('encrypted_header').text) - Nokogiri::XML(decrypted_header) - else - doc.search('header') - end - end - - def xml_for person - xml =< - - #{person.encrypt(decrypted_header)} - #{@magic_sig.to_xml} - -ENTRY - - end - - def decrypted_header - header =<
- #{iv} - #{aes_key} - - #{@author.name} - acct:#{@author.diaspora_handle} - - -HEADER - end - - def author - if @author.nil? - @author ||= Person.by_account_identifier @author_email - raise "did you remember to async webfinger?" if @author.nil? - end - @author - end - - # Decode URL-safe-Base64. This implements - def self.decode64url(str) - # remove whitespace - sans_whitespace = str.gsub(/\s/, '') - # pad to a multiple of 4 - string = sans_whitespace + '=' * ((4 - sans_whitespace.size) % 4) - # convert to standard Base64 - # string = padded.tr('-','+').tr('_','/') - - # Base64.decode64(string) - Base64.urlsafe_decode64 string - end - - # Check whether this envelope's signature can be verified with the - # provided OpenSSL::PKey::RSA public_key. - # Example: - # - # env.verified_for_key? OpenSSL::PKey::RSA.new(File.open('public_key.pem')) - # # -> true - def verified_for_key?(public_key) - signature = Base64.urlsafe_decode64(self.magic_sig.sig) - signed_data = self.magic_sig.signable_string# Base64.urlsafe_decode64(self.magic_sig.signable_string) - - public_key.verify(OpenSSL::Digest::SHA256.new, signature, signed_data ) - end - - # Decode a string containing URL safe Base64 into an integer - # Example: - # - # MagicSig.b64_to_n('AQAB') - # # -> 645537 - def self.b64_to_n(str) - packed = decode64url(str) - packed.unpack('B*')[0].to_i(2) - end - - # Parse a string containing a magic-public-key into an OpenSSL::PKey::RSA key. - # Example: - # - # key = MagicSig.parse_key('RSA.mVgY8RN6URBTstndvmUUPb4UZTdwvwmddSKE5z_jvKUEK6yk1u3rrC9yN8k6FilGj9K0eeUPe2hf4Pj-5CmHww.AQAB') - # key.n - # # -> 8031283789075196565022891546563591368344944062154100509645398892293433370859891943306439907454883747534493461257620351548796452092307094036643522661681091 - # key.e - # # -> 65537 - def self.parse_key(str) - n,e = str.match(/^RSA.([^.]*).([^.]*)$/)[1..2] - build_key(b64_to_n(n),b64_to_n(e)) - end - - # Take two integers e, n and create a new OpenSSL::PKey::RSA key with them - # Example: - # - # n = 9487834027867356975347184933768917275269369900665861930617802608089634337052392076689226301419587057117740995382286148368168197915234368486155306558161867 - # e = 65537 - # key = MagicSig.build_key(n,e) - # key.public_encrypt(...) # for sending to strangers - # key.public_decrypt(...) # very rarely used - # key.verify(...) # for verifying signatures - def self.build_key(n,e) - key = OpenSSL::PKey::RSA.new - key.n = n - key.e = e - key - end - - end - - class MagicSigEnvelope - attr_accessor :data, :data_type, :encoding, :alg, :sig, :author - def self.parse(doc) - env = self.new - ns = {'me'=>'http://salmon-protocol.org/ns/magic-env'} - env.encoding = doc.search('//me:env/me:encoding', ns).text.strip - - if env.encoding != 'base64url' - raise ArgumentError, "Magic Signature data must be encoded with base64url, was #{slap.magic_sig.encoding}" - end - - env.data = doc.search('//me:env/me:data', ns).text - env.alg = doc.search('//me:env/me:alg', ns).text.strip - env.sig = doc.search('//me:env/me:sig', ns).text - env.data_type = doc.search('//me:env/me:data', ns).first['type'].strip - - unless 'RSA-SHA256' == env.alg - raise ArgumentError, "Magic Signature data must be signed with RSA-SHA256, was #{env.alg}" - end - - env - end - - def self.create(user, activity) - env = MagicSigEnvelope.new - env.author = user.person - env.data = Base64.urlsafe_encode64(activity) - env.data_type = env.get_data_type - env.encoding = env.get_encoding - env.alg = env.get_alg - - env.sig = Base64.urlsafe_encode64( - user.encryption_key.sign OpenSSL::Digest::SHA256.new, env.signable_string ) - - env - end - - def signable_string - [@data, Base64.urlsafe_encode64(@data_type),Base64.urlsafe_encode64(@encoding), Base64.urlsafe_encode64(@alg)].join(".") - end - - def to_xml - xml= < - #{@data} - #{@encoding} - #{@alg} - #{@sig} - -ENTRY - xml - end - - def get_encoding - 'base64url' - end - - def get_data_type - 'application/atom+xml' - end - - def get_alg - 'RSA-SHA256' - end - - end + autoload :SalmonSlap, File.join(Rails.root, "lib", "salmon", "salmon_slap") + autoload :EncryptedSalmonSlap, File.join(Rails.root, "lib", "salmon", "encrypted_salmon_slap") + autoload :MagicSigEnvelope, File.join(Rails.root, "lib", "salmon", "magic_sig_envelope") end diff --git a/lib/salmon/salmon_slap.rb b/lib/salmon/salmon_slap.rb new file mode 100644 index 000000000..3a7201662 --- /dev/null +++ b/lib/salmon/salmon_slap.rb @@ -0,0 +1,150 @@ +# Copyright (c) 2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +module Salmon + class SalmonSlap + attr_accessor :magic_sig, :author, :author_email, :aes_key, :iv, :parsed_data, + :data_type, :sig + + def self.create(user, activity) + salmon = self.new + salmon.author = user.person + aes_key_hash = user.person.gen_aes_key + salmon.aes_key = aes_key_hash['key'] + salmon.iv = aes_key_hash['iv'] + salmon.magic_sig = MagicSigEnvelope.create(user , user.person.aes_encrypt(activity, aes_key_hash)) + salmon + end + + def self.parse(xml, user=nil) + slap = self.new + doc = Nokogiri::XML(xml) + + sig_doc = doc.search('entry') + + ### Header ## + header_doc = slap.salmon_header(doc, user) + slap.author_email= header_doc.search('uri').text.split("acct:").last + slap.aes_key = header_doc.search('aes_key').text + slap.iv = header_doc.search('iv').text + + slap.magic_sig = MagicSigEnvelope.parse sig_doc + + key_hash = {'key' => slap.aes_key, 'iv' => slap.iv} + slap.parsed_data = slap.parse_data(key_hash, user) + slap.sig = slap.magic_sig.sig + slap.data_type = slap.magic_sig.data_type + + slap + end + + # @return [String] + def parse_data(key_hash, user=nil) + SalmonSlap.decode64url(self.magic_sig.data) + end + + # @return [Nokogiri::Doc] + def salmon_header(doc, user=nil) + doc.search('header') + end + + def xml_for(person) + xml =< + + #{header(person)} + #{@magic_sig.to_xml} + +ENTRY + end + + def header(person) + "
#{plaintext_header}
" + end + + def plaintext_header + header =<
#{iv} + #{aes_key} + + #{@author.name} + acct:#{@author.diaspora_handle} + +HEADER + end + + def author + if @author.nil? + @author ||= Person.by_account_identifier @author_email + raise "did you remember to async webfinger?" if @author.nil? + end + @author + end + + # Decode URL-safe-Base64. This implements + def self.decode64url(str) + # remove whitespace + sans_whitespace = str.gsub(/\s/, '') + # pad to a multiple of 4 + string = sans_whitespace + '=' * ((4 - sans_whitespace.size) % 4) + # convert to standard Base64 + # string = padded.tr('-','+').tr('_','/') + + # Base64.decode64(string) + Base64.urlsafe_decode64 string + end + + # Check whether this envelope's signature can be verified with the + # provided OpenSSL::PKey::RSA public_key. + # Example: + # + # env.verified_for_key? OpenSSL::PKey::RSA.new(File.open('public_key.pem')) + # # -> true + def verified_for_key?(public_key) + signature = Base64.urlsafe_decode64(self.magic_sig.sig) + signed_data = self.magic_sig.signable_string# Base64.urlsafe_decode64(self.magic_sig.signable_string) + + public_key.verify(OpenSSL::Digest::SHA256.new, signature, signed_data ) + end + + # Decode a string containing URL safe Base64 into an integer + # Example: + # + # MagicSig.b64_to_n('AQAB') + # # -> 645537 + def self.b64_to_n(str) + packed = decode64url(str) + packed.unpack('B*')[0].to_i(2) + end + + # Parse a string containing a magic-public-key into an OpenSSL::PKey::RSA key. + # Example: + # + # key = MagicSig.parse_key('RSA.mVgY8RN6URBTstndvmUUPb4UZTdwvwmddSKE5z_jvKUEK6yk1u3rrC9yN8k6FilGj9K0eeUPe2hf4Pj-5CmHww.AQAB') + # key.n + # # -> 8031283789075196565022891546563591368344944062154100509645398892293433370859891943306439907454883747534493461257620351548796452092307094036643522661681091 + # key.e + # # -> 65537 + def self.parse_key(str) + n,e = str.match(/^RSA.([^.]*).([^.]*)$/)[1..2] + build_key(b64_to_n(n),b64_to_n(e)) + end + + # Take two integers e, n and create a new OpenSSL::PKey::RSA key with them + # Example: + # + # n = 9487834027867356975347184933768917275269369900665861930617802608089634337052392076689226301419587057117740995382286148368168197915234368486155306558161867 + # e = 65537 + # key = MagicSig.build_key(n,e) + # key.public_encrypt(...) # for sending to strangers + # key.public_decrypt(...) # very rarely used + # key.verify(...) # for verifying signatures + def self.build_key(n,e) + key = OpenSSL::PKey::RSA.new + key.n = n + key.e = e + key + end + end +end diff --git a/spec/lib/postzord/receiver/public.rb b/spec/lib/postzord/receiver/public.rb new file mode 100644 index 000000000..5b9259d09 --- /dev/null +++ b/spec/lib/postzord/receiver/public.rb @@ -0,0 +1,32 @@ +# Copyright (c) 2010, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +require 'spec_helper' + +require File.join(Rails.root, 'lib/postzord') +require File.join(Rails.root, 'lib/postzord/receiver/public') + +describe Postzord::Receiver::Public do + + describe '#initialize' do + + end + + describe '#verify_signature' do + + end + + describe '#collect_recipients' do + + end + + describe '#batch_insert_visibilities' do + + end + + describe '#batch_notify' do + + end + +end diff --git a/spec/lib/postzord/receiver/public_spec.rb b/spec/lib/postzord/receiver/public_spec.rb new file mode 100644 index 000000000..318cc1a02 --- /dev/null +++ b/spec/lib/postzord/receiver/public_spec.rb @@ -0,0 +1,59 @@ +# Copyright (c) 2011, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +require 'spec_helper' + +require File.join(Rails.root, 'lib/postzord') +require File.join(Rails.root, 'lib/postzord/receiver/public') + +describe Postzord::Receiver::Public do + + describe '#initialize' do + it 'sets xml as instance variable' do + receiver = Postzord::Receiver::Public.new("blah") + receiver.xml.should == 'blah' + end + end + + describe '#perform' do + it 'calls verify_signature' do + + end + + context 'if signature is valid' do + it 'calls collect_recipients' do + + end + + it 'saves the parsed object' do + + end + + it 'calls batch_insert_visibilities' do + + end + + it 'calls batch_notify' do + + end + end + end + + describe '#verify_signature' do + + end + + describe '#collect_recipients' do + + end + + describe '#batch_insert_visibilities' do + + end + + describe '#batch_notify' do + + end + +end diff --git a/spec/lib/salmon/encrypted_salmon_slap_spec.rb b/spec/lib/salmon/encrypted_salmon_slap_spec.rb new file mode 100644 index 000000000..03b5dd2fb --- /dev/null +++ b/spec/lib/salmon/encrypted_salmon_slap_spec.rb @@ -0,0 +1,89 @@ +# Copyright (c) 2010, Diaspora Inc. This file is +# licensed under the Affero General Public License version 3 or later. See +# the COPYRIGHT file. + +require 'spec_helper' + +describe Salmon::EncryptedSalmonSlap do + let(:post){ alice.post :status_message, :text => "hi", :to => alice.aspects.create(:name => "sdg").id } + + let!(:created_salmon) {Salmon::EncryptedSalmonSlap.create(alice, post.to_diaspora_xml)} + + describe '#create' do + + it 'has data in the magic envelope' do + created_salmon.magic_sig.data.should_not be nil + end + + it 'has no parsed_data' do + created_salmon.parsed_data.should be nil + end + + it 'sets aes and iv key' do + created_salmon.aes_key.should_not be nil + created_salmon.iv.should_not be nil + end + + it 'makes the data in the signature encrypted with that key' do + key_hash = {'key' => created_salmon.aes_key, 'iv' => created_salmon.iv} + decoded_string = Salmon::EncryptedSalmonSlap.decode64url(created_salmon.magic_sig.data) + alice.aes_decrypt(decoded_string, key_hash).should == post.to_diaspora_xml + end + end + + describe '#xml_for' do + let(:xml) {created_salmon.xml_for eve.person} + + it 'has a encrypted header field' do + xml.include?("encrypted_header").should be true + end + + it 'the encrypted_header field should contain the aes key' do + doc = Nokogiri::XML(xml) + decrypted_header = eve.decrypt(doc.search('encrypted_header').text) + decrypted_header.include?(created_salmon.aes_key).should be true + end + end + + context 'marshaling' do + let(:xml) {created_salmon.xml_for eve.person} + let(:parsed_salmon) { Salmon::EncryptedSalmonSlap.parse(xml, eve)} + + it 'should parse out the aes key' do + parsed_salmon.aes_key.should == created_salmon.aes_key + end + + it 'should parse out the iv' do + parsed_salmon.iv.should == created_salmon.iv + end + it 'should parse out the authors diaspora_handle' do + parsed_salmon.author_email.should == alice.person.diaspora_handle + + end + + describe '#author' do + it 'should reference a local author' do + parsed_salmon.author.should == alice.person + end + + it 'should fail if no author is found' do + parsed_salmon.author_email = 'tom@tom.joindiaspora.com' + + + proc {parsed_salmon.author.public_key}.should raise_error "did you remember to async webfinger?" + + end + + end + + it 'verifies the signature for the sender' do + parsed_salmon.verified_for_key?(alice.public_key).should be true + end + + it 'contains the original data' do + parsed_salmon.parsed_data.should == post.to_diaspora_xml + end + + end +end + diff --git a/spec/lib/salmon/magic_sig_envelope_spec.rb b/spec/lib/salmon/magic_sig_envelope_spec.rb new file mode 100644 index 000000000..1c1236a58 --- /dev/null +++ b/spec/lib/salmon/magic_sig_envelope_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Salmon::MagicSigEnvelope do + +end diff --git a/spec/lib/salmon/salmon_slap_spec.rb b/spec/lib/salmon/salmon_slap_spec.rb new file mode 100644 index 000000000..77f61c492 --- /dev/null +++ b/spec/lib/salmon/salmon_slap_spec.rb @@ -0,0 +1,5 @@ +require 'spec_helper' + +describe Salmon::SalmonSlap do + +end