diff --git a/app/models/person.rb b/app/models/person.rb index 7853b9d17..2f90fe727 100644 --- a/app/models/person.rb +++ b/app/models/person.rb @@ -1,6 +1,7 @@ class Person include MongoMapper::Document include ROXML + include Encryptor::Public xml_accessor :_id xml_accessor :email @@ -37,6 +38,13 @@ class Person def real_name "#{profile.first_name.to_s} #{profile.last_name.to_s}" end + def owns?(post) + self.id == post.person.id + end + + def receive_url + "#{self.url}receive/users/#{self.id}/" + end def encryption_key OpenSSL::PKey::RSA.new( serialized_key ) @@ -51,6 +59,10 @@ class Person Base64.encode64 OpenSSL::Digest::SHA256.new(self.exported_key).to_s end + def public_key + encryption_key.public_key + end + def exported_key encryption_key.public_key.export end @@ -60,16 +72,34 @@ class Person @serialized_key = new_key end - def owns?(post) - self.id == post.person.id - end - - def receive_url - "#{self.url}receive/users/#{self.id}/" - end - def self.by_webfinger( identifier ) - Person.first(:email => identifier.gsub('acct:', '')) + local_person = Person.first(:email => identifier.gsub('acct:', '')) + if local_person + local_person + elsif !identifier.include?("localhost") + begin + f = Redfinger.finger(identifier) + rescue SocketError => e + raise "Diaspora server for #{identifier} not found" if e.message =~ /Name or service not known/ + end + #raise "No diaspora user found at #{identifier}" + Person.from_webfinger_profile(identifier, f ) + end + end + + def self.from_webfinger_profile( identifier, profile) + public_key = profile.links.select{|x| x.rel == 'diaspora-public-key'}.first.href + new_person = Person.new + new_person.exported_key = Base64.decode64 public_key + new_person.email = identifier + receive_url = profile.links.select{ |l| l.rel == 'http://joindiaspora.com/seed_location'}.first.href + new_person.url = receive_url.split('receive').first + new_person.profile = Profile.new(:first_name => "Anon", :last_name => "ymous") + if new_person.save! + new_person + else + cry + end end def remote? diff --git a/app/models/user.rb b/app/models/user.rb index 1fbc1a849..c5cfcbdf4 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,8 +1,11 @@ require 'lib/diaspora/user/friending.rb' +require 'lib/salmon/salmon' class User include MongoMapper::Document include Diaspora::UserModules::Friending + include Encryptor::Private + devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable key :username, :unique => true @@ -128,6 +131,10 @@ class User post.push_to( target_people ) end + def salmon( post, opts = {} ) + Salmon::SalmonSlap.create(self, post.encrypted_xml_for(opts[:to])) + end + def visible_posts( opts = {} ) if opts[:by_members_of] return raw_visible_posts if opts[:by_members_of] == :all @@ -184,6 +191,13 @@ class User end ###### Receiving ####### + def receive_salmon xml + salmon = Salmon::SalmonSlap.parse xml + if salmon.verified_for_key?(salmon.author.public_key) + self.receive(decrypt(salmon.data)) + end + end + def receive xml object = Diaspora::Parser.from_xml(xml) Rails.logger.debug("Receiving object for #{self.real_name}:\n#{object.inspect}") @@ -316,11 +330,4 @@ class User } end - - protected - - def self.generate_key - OpenSSL::PKey::RSA::generate 1024 - end - end diff --git a/lib/encryptable.rb b/lib/encryptable.rb index 17d8ab0c7..bcc34894a 100644 --- a/lib/encryptable.rb +++ b/lib/encryptable.rb @@ -28,5 +28,10 @@ Rails.logger.debug("Signing #{signable_string}") Base64.encode64(key.sign "SHA", signable_string) end + + def encrypted_xml_for(person) + person.encrypt self.to_diaspora_xml + end + end diff --git a/lib/encryptor.rb b/lib/encryptor.rb new file mode 100644 index 000000000..e8d7cf7de --- /dev/null +++ b/lib/encryptor.rb @@ -0,0 +1,61 @@ +module Encryptor + module Public + def encrypt cleartext + aes_key = gen_aes_key + ciphertext = aes_encrypt(cleartext, aes_key) + encrypted_key = encrypt_aes_key aes_key + cipher_hash = {:aes_key => encrypted_key, :ciphertext => ciphertext} + Base64.encode64( cipher_hash.to_json ) + end + + def gen_aes_key + cipher = OpenSSL::Cipher.new('AES-256-CBC') + key = cipher.random_key + iv = cipher.random_iv + {'key' => Base64.encode64(key), 'iv' => Base64.encode64(iv)} + end + + def aes_encrypt(txt, key) + cipher = OpenSSL::Cipher.new('AES-256-CBC') + cipher.encrypt + cipher.key = Base64.decode64 key['key'] + cipher.iv = Base64.decode64 key['iv'] + ciphertext = '' + ciphertext << cipher.update(txt) + ciphertext << cipher.final + Base64.encode64 ciphertext + end + + def encrypt_aes_key key + Base64.encode64 encryption_key.public_encrypt( key.to_json ) + end + end + + module Private + def decrypt cipher_json + json = JSON.parse(Base64.decode64 cipher_json) + aes_key = get_aes_key json['aes_key'] + aes_decrypt(json['ciphertext'], aes_key) + end + + def get_aes_key encrypted_key + clear_key = encryption_key.private_decrypt( Base64.decode64 encrypted_key ) + JSON::parse(clear_key) + end + + def aes_decrypt(ciphertext, key) + cipher = OpenSSL::Cipher.new('AES-256-CBC') + cipher.decrypt + cipher.key = Base64.decode64 key['key'] + cipher.iv = Base64.decode64 key['iv'] + txt = '' + txt << cipher.update(Base64.decode64 ciphertext) + txt << cipher.final + txt + end + + def self.generate_key + OpenSSL::PKey::RSA::generate 4096 + end + end +end diff --git a/lib/salmon/salmon.rb b/lib/salmon/salmon.rb index a20f35796..d207c00b4 100644 --- a/lib/salmon/salmon.rb +++ b/lib/salmon/salmon.rb @@ -36,7 +36,7 @@ end # Verify documents secured with Magic Signatures module Salmon class SalmonSlap - attr_accessor :magic_sig, :user, :data, :data_type, :sig + attr_accessor :magic_sig, :author, :author_email, :data, :data_type, :sig def self.parse(xml) slap = self.new doc = Nokogiri::XML(xml) @@ -57,15 +57,15 @@ module Salmon raise ArgumentError, "Magic Signature data must be signed with RSA-SHA256, was #{slap.magic_sig.alg}" unless 'RSA-SHA256' == slap.magic_sig.alg + uri = doc.search('uri').text + slap.author_email = uri.split("acct:").last slap end - - def self.create(user, activity) salmon = self.new - salmon.user = user - salmon.magic_sig = MagicSigEnvelope.create(user, activity) + salmon.author = user.person + salmon.magic_sig = MagicSigEnvelope.create(user , activity) salmon end @@ -74,8 +74,8 @@ module Salmon - #{@user.real_name} - acct:#{@user.email} + #{@author.real_name} + acct:#{@author.email} #{@magic_sig.to_xml} @@ -83,6 +83,14 @@ ENTRY end + def author + if @author + @author + else + Person.by_webfinger @author_email + end + end + # Decode URL-safe-Base64. This implements def self.decode64url(str) # remove whitespace @@ -161,7 +169,7 @@ ENTRY end class MagicSigEnvelope - attr_accessor :data, :data_type, :encoding, :alg, :sig, :user + 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'} @@ -175,7 +183,7 @@ ENTRY def self.create(user, activity) env = MagicSigEnvelope.new - env.user = user + env.author = user.person env.data = Base64.urlsafe_encode64(activity) env.data_type = env.get_data_type env.encoding = env.get_encoding diff --git a/spec/lib/salmon_salmon_spec.rb b/spec/lib/salmon_salmon_spec.rb index 49ffa483d..4fda36884 100644 --- a/spec/lib/salmon_salmon_spec.rb +++ b/spec/lib/salmon_salmon_spec.rb @@ -9,33 +9,50 @@ include Salmon describe Salmon do - it 'should verify the signature on a roundtrip' do + before do + @user = Factory.create :user @post = @user.post :status_message, :message => "hi", :to => @user.group(:name => "sdg").id - x = Salmon::SalmonSlap.create(@user, @post.to_diaspora_xml) - - z = Salmon::SalmonSlap.parse x.to_xml + @sent_salmon = Salmon::SalmonSlap.create(@user, @post.to_diaspora_xml) + @parsed_salmon = Salmon::SalmonSlap.parse @sent_salmon.to_xml + end - x.magic_sig.data.should == z.magic_sig.data + it 'should verify the signature on a roundtrip' do - x.magic_sig.sig.should == z.magic_sig.sig - x.magic_sig.signable_string.should == z.magic_sig.signable_string + @sent_salmon.magic_sig.data.should == @parsed_salmon.magic_sig.data + + @sent_salmon.magic_sig.sig.should == @parsed_salmon.magic_sig.sig + @sent_salmon.magic_sig.signable_string.should == @parsed_salmon.magic_sig.signable_string - x.verified_for_key?(OpenSSL::PKey::RSA.new(@user.exported_key)).should be true - z.verified_for_key?(OpenSSL::PKey::RSA.new(@user.exported_key)).should be true + @parsed_salmon.verified_for_key?(OpenSSL::PKey::RSA.new(@user.exported_key)).should be true + @sent_salmon.verified_for_key?(OpenSSL::PKey::RSA.new(@user.exported_key)).should be true end it 'should return the data so it can be "received"' do - @user = Factory.create :user - @post = @user.post :status_message, :message => "hi", :to => @user.group(:name => "sdg").id - x = Salmon::SalmonSlap.create(@user, @post.to_diaspora_xml) - z = Salmon::SalmonSlap.parse x.to_xml - xml = @post.to_diaspora_xml - z.data.should == xml + @parsed_salmon.data.should == xml end + + it 'should parse out the author email' do + @parsed_salmon.author_email.should == @user.person.email + end + + it 'should reference a local author' do + @parsed_salmon.author.should == @user.person + end + + it 'should reference a remote author' do + @parsed_salmon.author_email = 'tom@tom.joindiaspora.com' + @parsed_salmon.author.public_key.should_not be_nil + end + + it 'should fail to reference a nonexistent remote author' do + @parsed_salmon.author_email = 'idsfug@difgubhpsduh.rgd' + proc {@parsed_salmon.author.real_name}.should raise_error /not found/ + end + end diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index 608c17ba7..35e85ab21 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -7,10 +7,20 @@ describe Post do end describe 'xml' do - it 'should serialize to xml with its person' do - message = Factory.create(:status_message, :person => @user.person) - message.to_xml.to_s.include?(@user.person.email).should == true + before do + @message = Factory.create(:status_message, :person => @user.person) end + + it 'should serialize to xml with its person' do + @message.to_xml.to_s.include?(@user.person.email).should == true + end + + it 'should serialize to encrypted xml' do + enc_xml = @message.encrypted_xml_for(@user.person) + enc_xml.include?(@message.to_diaspora_xml).should be false + @user.decrypt(enc_xml).include?(@message.to_diaspora_xml).should be true + end + end describe 'deletion' do diff --git a/spec/models/user/receive_spec.rb b/spec/models/user/receive_spec.rb index 74ddfb882..41b619dbb 100644 --- a/spec/models/user/receive_spec.rb +++ b/spec/models/user/receive_spec.rb @@ -165,4 +165,16 @@ describe User do @user3.visible_person_by_id(commenter_id).should_not be_nil end end + + describe 'salmon' do + before do + @post = @user.post :status_message, :message => "hello", :to => @group.id + @salmon = @user.salmon( @post, :to => @user2.person ) + end + + it 'should receive a salmon for a post' do + @user2.receive_salmon( @salmon.to_xml ) + @user2.visible_post_ids.include?(@post.id).should be true + end + end end diff --git a/spec/user_encryption_spec.rb b/spec/user_encryption_spec.rb index 9432ac07b..6efafed4e 100644 --- a/spec/user_encryption_spec.rb +++ b/spec/user_encryption_spec.rb @@ -59,6 +59,17 @@ describe 'user encryption' do end end + describe 'encryption' do + before do + @message = @user.post :status_message, :message => "hi", :to => @group.id + end + it 'should encrypt large messages' do + ciphertext = @user.encrypt @message.to_diaspora_xml + ciphertext.include?(@message.to_diaspora_xml).should be false + @user.decrypt(ciphertext).include?(@message.to_diaspora_xml).should be true + end + end + describe 'signing and verifying' do it 'should sign a message on create' do