mail-gpg 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +19 -1
- data/README.md +19 -2
- data/lib/mail/gpg.rb +54 -3
- data/lib/mail/gpg/gpgme_helper.rb +9 -0
- data/lib/mail/gpg/message_patch.rb +7 -0
- data/lib/mail/gpg/sign_part.rb +7 -0
- data/lib/mail/gpg/version.rb +1 -1
- data/test/gpg_test.rb +4 -2
- data/test/message_test.rb +17 -2
- data/test/sign_part_test.rb +7 -2
- metadata +2 -2
    
        data/History.txt
    CHANGED
    
    | @@ -1,6 +1,24 @@ | |
| 1 | 
            +
            == 0.1.3 2014-02-17
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            * Signature checking implemented, thanks to Morten Andersen
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            == 0.1.2 2013-11-19
         | 
| 6 | 
            +
             | 
| 7 | 
            +
            * bugfix release
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            == 0.1.1 2013-11-14
         | 
| 10 | 
            +
             | 
| 11 | 
            +
            * bugfix release
         | 
| 12 | 
            +
             | 
| 13 | 
            +
            == 0.1.0 2013-11-06
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            * decryption support (thanks to Morten Andersen)
         | 
| 16 | 
            +
            * sign-only operation (thanks to FewKinG)
         | 
| 17 | 
            +
            * keyserver url lookup (thanks to FewKinG)
         | 
| 18 | 
            +
             | 
| 1 19 | 
             
            == 0.0.6 2013-08-28
         | 
| 2 20 |  | 
| 3 | 
            -
            * bugfix:  | 
| 21 | 
            +
            * bugfix: only encrypt to specified keys if :keys option is present
         | 
| 4 22 |  | 
| 5 23 | 
             
            == 0.0.5 2013-08-28
         | 
| 6 24 |  | 
    
        data/README.md
    CHANGED
    
    | @@ -96,13 +96,15 @@ if mail.encrypted? | |
| 96 96 | 
             
            end
         | 
| 97 97 | 
             
            ```
         | 
| 98 98 |  | 
| 99 | 
            +
            Set the `:verify` option to `true` when calling `decrypt` to decrypt *and* verify signatures.
         | 
| 100 | 
            +
             | 
| 99 101 | 
             
            A `GPGME::Error::BadPassphrase` will be raised if the password for the private key is incorrect.
         | 
| 100 102 | 
             
            A `EncodingError` will be raised if the encrypted mails is not encoded correctly as a [RFC 3156](http://www.ietf.org/rfc/rfc3156.txt) message.
         | 
| 101 103 |  | 
| 102 104 |  | 
| 103 105 | 
             
            ### Signing only
         | 
| 104 106 |  | 
| 105 | 
            -
            Just leave the  | 
| 107 | 
            +
            Just leave the `:encrypt` option out or pass `encrypt: false`, i.e.
         | 
| 106 108 |  | 
| 107 109 |  | 
| 108 110 | 
             
                Mail.new do
         | 
| @@ -110,6 +112,21 @@ Just leave the the `:encrypt` option out or pass `encrypt: false`, i.e. | |
| 110 112 | 
             
                  gpg sign: true
         | 
| 111 113 | 
             
                end.deliver 
         | 
| 112 114 |  | 
| 115 | 
            +
            ### Verify signature(s)
         | 
| 116 | 
            +
             | 
| 117 | 
            +
            Receive the mail as usual. Check if it is signed using the `signed?` method. Check the signature of the mail with the `signature_valid?` method:
         | 
| 118 | 
            +
             | 
| 119 | 
            +
            ```ruby
         | 
| 120 | 
            +
            mail = Mail.first
         | 
| 121 | 
            +
            if !mail.encrypted? && mail.signed?
         | 
| 122 | 
            +
              # do not call signed on encrypted mails. The signature on encrypted mails
         | 
| 123 | 
            +
              # must be checked by setting the :verify option when decrypting
         | 
| 124 | 
            +
              puts "signature(s) valid: #{mail.signature_valid?}"
         | 
| 125 | 
            +
            end
         | 
| 126 | 
            +
            ```
         | 
| 127 | 
            +
             | 
| 128 | 
            +
            Note that for encrypted mails the signatures can not be checked using these methods. For encrypted mails
         | 
| 129 | 
            +
            the `:verify` option for the `decrypt` operation must be used instead.
         | 
| 113 130 |  | 
| 114 131 | 
             
            ### Key import from public key servers
         | 
| 115 132 |  | 
| @@ -163,7 +180,7 @@ around with your personal gpg keychain. | |
| 163 180 |  | 
| 164 181 | 
             
            ## Todo
         | 
| 165 182 |  | 
| 166 | 
            -
            * signature verification for received mails
         | 
| 183 | 
            +
            * signature verification for received mails with inline PGP
         | 
| 167 184 | 
             
            * on the fly import of recipients' keys from public key servers based on email address or key id
         | 
| 168 185 | 
             
            * handle encryption errors due to missing keys - maybe return a list of failed
         | 
| 169 186 | 
             
              recipients
         | 
    
        data/lib/mail/gpg.rb
    CHANGED
    
    | @@ -47,14 +47,16 @@ module Mail | |
| 47 47 | 
             
                def self.sign(cleartext_mail, options = {})
         | 
| 48 48 | 
             
                  construct_mail(cleartext_mail, options) do
         | 
| 49 49 | 
             
                    options[:sign_as] ||= cleartext_mail.from
         | 
| 50 | 
            -
                    add_part SignPart.new(cleartext_mail, options)
         | 
| 51 50 | 
             
                    add_part Mail::Part.new(cleartext_mail)
         | 
| 51 | 
            +
                    add_part SignPart.new(cleartext_mail, options)
         | 
| 52 52 |  | 
| 53 53 | 
             
                    content_type "multipart/signed; micalg=pgp-sha1; protocol=\"application/pgp-signature\"; boundary=#{boundary}"
         | 
| 54 54 | 
             
                    body.preamble = options[:preamble] || "This is an OpenPGP/MIME signed message (RFC 4880 and 3156)"
         | 
| 55 55 | 
             
                  end
         | 
| 56 56 | 
             
                end
         | 
| 57 57 |  | 
| 58 | 
            +
                # options are:
         | 
| 59 | 
            +
                # :verify: decrypt and verify
         | 
| 58 60 | 
             
                def self.decrypt(encrypted_mail, options = {})
         | 
| 59 61 | 
             
                  if encrypted_mime?(encrypted_mail)
         | 
| 60 62 | 
             
                    decrypt_pgp_mime(encrypted_mail, options)
         | 
| @@ -65,12 +67,33 @@ module Mail | |
| 65 67 | 
             
                  end
         | 
| 66 68 | 
             
                end
         | 
| 67 69 |  | 
| 70 | 
            +
                def self.signature_valid?(signed_mail, options = {})
         | 
| 71 | 
            +
                  if signed_mime?(signed_mail)
         | 
| 72 | 
            +
                    signature_valid_pgp_mime?(signed_mail, options)
         | 
| 73 | 
            +
                  else
         | 
| 74 | 
            +
                    raise EncodingError, "Unsupported signature format '#{signed_mail.content_type}'"
         | 
| 75 | 
            +
                  end
         | 
| 76 | 
            +
                end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                # true if a mail is encrypted
         | 
| 68 79 | 
             
                def self.encrypted?(mail)
         | 
| 69 80 | 
             
                  return true if encrypted_mime?(mail)
         | 
| 70 81 | 
             
                  return true if encrypted_inline?(mail)
         | 
| 71 82 | 
             
                  false
         | 
| 72 83 | 
             
                end
         | 
| 73 84 |  | 
| 85 | 
            +
                # true if a mail is signed.
         | 
| 86 | 
            +
                #
         | 
| 87 | 
            +
                # throws EncodingError if called on an encrypted mail (so only call this method if encrypted? is false)
         | 
| 88 | 
            +
                def self.signed?(mail)
         | 
| 89 | 
            +
                  return true if signed_mime?(mail)
         | 
| 90 | 
            +
                  return true if signed_inline?(mail)
         | 
| 91 | 
            +
                  if encrypted?(mail)
         | 
| 92 | 
            +
                    raise EncodingError, 'Unable to determine signature on an encrypted mail, use :verify option on decrypt()'
         | 
| 93 | 
            +
                  end
         | 
| 94 | 
            +
                  false
         | 
| 95 | 
            +
                end
         | 
| 96 | 
            +
             | 
| 74 97 | 
             
                private
         | 
| 75 98 |  | 
| 76 99 | 
             
                def self.construct_mail(cleartext_mail, options, &block)
         | 
| @@ -113,7 +136,16 @@ module Mail | |
| 113 136 | 
             
                  InlineDecryptedMessage.new(encrypted_mail, options)
         | 
| 114 137 | 
             
                end
         | 
| 115 138 |  | 
| 116 | 
            -
                # check  | 
| 139 | 
            +
                # check signature for PGP/MIME (RFC 3156, section 5) signed mail
         | 
| 140 | 
            +
                def self.signature_valid_pgp_mime?(signed_mail, options)
         | 
| 141 | 
            +
                  # MUST contain exactly two body parts
         | 
| 142 | 
            +
                  if signed_mail.parts.length != 2
         | 
| 143 | 
            +
                    raise EncodingError, "RFC 3136 mandates exactly two body parts, found '#{signed_mail.parts.length}'"
         | 
| 144 | 
            +
                  end
         | 
| 145 | 
            +
                  SignPart.signature_valid?(signed_mail.parts[0], signed_mail.parts[1], options)
         | 
| 146 | 
            +
                end
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                # check if PGP/MIME encrypted (RFC 3156)
         | 
| 117 149 | 
             
                def self.encrypted_mime?(mail)
         | 
| 118 150 | 
             
                  mail.has_content_type? &&
         | 
| 119 151 | 
             
                    'multipart/encrypted' == mail.mime_type &&
         | 
| @@ -121,7 +153,7 @@ module Mail | |
| 121 153 | 
             
                end
         | 
| 122 154 |  | 
| 123 155 | 
             
                # check if inline PGP (i.e. if any parts of the mail includes
         | 
| 124 | 
            -
                # the PGP MESSAGE marker
         | 
| 156 | 
            +
                # the PGP MESSAGE marker)
         | 
| 125 157 | 
             
                def self.encrypted_inline?(mail)
         | 
| 126 158 | 
             
                  return true if mail.body.include?('-----BEGIN PGP MESSAGE-----')
         | 
| 127 159 | 
             
                  if mail.multipart?
         | 
| @@ -134,5 +166,24 @@ module Mail | |
| 134 166 | 
             
                  end
         | 
| 135 167 | 
             
                  false
         | 
| 136 168 | 
             
                end
         | 
| 169 | 
            +
             | 
| 170 | 
            +
                # check if PGP/MIME signed (RFC 3156)
         | 
| 171 | 
            +
                def self.signed_mime?(mail)
         | 
| 172 | 
            +
                  mail.has_content_type? &&
         | 
| 173 | 
            +
                    'multipart/signed' == mail.mime_type &&
         | 
| 174 | 
            +
                    'application/pgp-signature' == mail.content_type_parameters[:protocol]
         | 
| 175 | 
            +
                end
         | 
| 176 | 
            +
             | 
| 177 | 
            +
                # check if inline PGP (i.e. if any parts of the mail includes
         | 
| 178 | 
            +
                # the PGP SIGNED marker)
         | 
| 179 | 
            +
                def self.signed_inline?(mail)
         | 
| 180 | 
            +
                  return true if mail.body.include?('-----BEGIN PGP SIGNED MESSAGE-----')
         | 
| 181 | 
            +
                  if mail.multipart?
         | 
| 182 | 
            +
                    mail.parts.each do |part|
         | 
| 183 | 
            +
                      return true if part.body.include?('-----BEGIN PGP SIGNED MESSAGE-----')
         | 
| 184 | 
            +
                    end
         | 
| 185 | 
            +
                  end
         | 
| 186 | 
            +
                  false
         | 
| 187 | 
            +
                end
         | 
| 137 188 | 
             
              end
         | 
| 138 189 | 
             
            end
         | 
| @@ -72,6 +72,15 @@ module Mail | |
| 72 72 | 
             
                    crypto.sign GPGME::Data.new(plain), options
         | 
| 73 73 | 
             
                  end
         | 
| 74 74 |  | 
| 75 | 
            +
                  def self.sign_verify(plain, signature, options = {})
         | 
| 76 | 
            +
                    signed = false
         | 
| 77 | 
            +
                    GPGME::Crypto.new.verify(signature, signed_text: plain) do |sig|
         | 
| 78 | 
            +
                      return false if !sig.valid? # just one invalid signature leads to false
         | 
| 79 | 
            +
                      signed = true
         | 
| 80 | 
            +
                    end
         | 
| 81 | 
            +
                    return signed
         | 
| 82 | 
            +
                  end
         | 
| 83 | 
            +
             | 
| 75 84 | 
             
                  private
         | 
| 76 85 |  | 
| 77 86 | 
             
                  # normalizes the list of recipients' emails, key ids and key data to a
         | 
    
        data/lib/mail/gpg/sign_part.rb
    CHANGED
    
    | @@ -12,6 +12,13 @@ module Mail | |
| 12 12 | 
             
                    end
         | 
| 13 13 | 
             
                  end
         | 
| 14 14 |  | 
| 15 | 
            +
                  def self.signature_valid?(plain, signature, options = {})
         | 
| 16 | 
            +
                    if !(signature.has_content_type? && ('application/pgp-signature' == signature.mime_type))
         | 
| 17 | 
            +
                      return false
         | 
| 18 | 
            +
                    end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                    GpgmeHelper.sign_verify(plain.encoded, signature.body.encoded, options)
         | 
| 21 | 
            +
                  end
         | 
| 15 22 | 
             
                end
         | 
| 16 23 | 
             
              end
         | 
| 17 24 | 
             
            end
         | 
    
        data/lib/mail/gpg/version.rb
    CHANGED
    
    
    
        data/test/gpg_test.rb
    CHANGED
    
    | @@ -29,15 +29,17 @@ class GpgTest < Test::Unit::TestCase | |
| 29 29 | 
             
              end
         | 
| 30 30 |  | 
| 31 31 | 
             
              def check_signature(mail = @mail, signed = @signed)
         | 
| 32 | 
            +
                assert signed.signed?
         | 
| 32 33 | 
             
                assert signature = signed.parts.detect{|p| p.content_type =~ /signature\.asc/}.body.to_s
         | 
| 33 34 | 
             
                GPGME::Crypto.new.verify(signature, signed_text: mail.encoded) do |sig|
         | 
| 34 35 | 
             
                  assert true == sig.valid?
         | 
| 35 36 | 
             
                end
         | 
| 37 | 
            +
                assert Mail::Gpg.signature_valid?(signed)
         | 
| 36 38 | 
             
              end
         | 
| 37 39 |  | 
| 38 40 | 
             
              def check_mime_structure_signed(mail = @mail, signed = @signed)
         | 
| 39 41 | 
             
                assert_equal 2, signed.parts.size
         | 
| 40 | 
            -
                 | 
| 42 | 
            +
                orig_part, sign_part = signed.parts
         | 
| 41 43 |  | 
| 42 44 | 
             
                assert_equal 'application/pgp-signature; name=signature.asc', sign_part.content_type
         | 
| 43 45 | 
             
                assert_equal orig_part.content_type, @mail.content_type
         | 
| @@ -153,7 +155,7 @@ class GpgTest < Test::Unit::TestCase | |
| 153 155 | 
             
                  end
         | 
| 154 156 |  | 
| 155 157 | 
             
                  should 'have multiple parts in original content' do
         | 
| 156 | 
            -
                    assert original_part = @signed.parts. | 
| 158 | 
            +
                    assert original_part = @signed.parts.first
         | 
| 157 159 | 
             
                    assert original_part.multipart?
         | 
| 158 160 | 
             
                    assert_equal 2, original_part.parts.size
         | 
| 159 161 | 
             
                    assert_match /sign me!/, original_part.parts.first.body.to_s
         | 
    
        data/test/message_test.rb
    CHANGED
    
    | @@ -52,13 +52,28 @@ class MessageTest < Test::Unit::TestCase | |
| 52 52 | 
             
                      assert m = @mails.first
         | 
| 53 53 | 
             
                      assert_equal 'test', m.subject
         | 
| 54 54 | 
             
                      assert !m.encrypted?
         | 
| 55 | 
            +
                      assert m.signed?
         | 
| 55 56 | 
             
                      assert m.multipart?
         | 
| 57 | 
            +
                      assert m.signature_valid?
         | 
| 56 58 | 
             
                      assert sign_part = m.parts.last
         | 
| 57 | 
            -
                      assert m = Mail::Message.new(m.parts. | 
| 59 | 
            +
                      assert m = Mail::Message.new(m.parts.first)
         | 
| 58 60 | 
             
                      assert !m.multipart?
         | 
| 59 | 
            -
                      GPGME::Crypto.new.verify(sign_part.body.to_s, signed_text:  | 
| 61 | 
            +
                      GPGME::Crypto.new.verify(sign_part.body.to_s, signed_text: m.encoded) do |sig| 
         | 
| 60 62 | 
             
                        assert true == sig.valid?
         | 
| 61 63 | 
             
                      end
         | 
| 64 | 
            +
                      assert_equal 'i am unencrypted', m.body.to_s
         | 
| 65 | 
            +
                    end
         | 
| 66 | 
            +
             | 
| 67 | 
            +
                    should "fail signature on tampered body" do
         | 
| 68 | 
            +
                      assert_equal 1, @mails.size
         | 
| 69 | 
            +
                      assert m = @mails.first
         | 
| 70 | 
            +
                      assert_equal 'test', m.subject
         | 
| 71 | 
            +
                      assert !m.encrypted?
         | 
| 72 | 
            +
                      assert m.signed?
         | 
| 73 | 
            +
                      assert m.multipart?
         | 
| 74 | 
            +
                      assert m.signature_valid?
         | 
| 75 | 
            +
                      m.parts.first.body = 'replaced body'
         | 
| 76 | 
            +
                      assert !m.signature_valid?
         | 
| 62 77 | 
             
                    end
         | 
| 63 78 | 
             
                  end
         | 
| 64 79 | 
             
                end
         | 
    
        data/test/sign_part_test.rb
    CHANGED
    
    | @@ -1,15 +1,20 @@ | |
| 1 | 
            +
            require 'test_helper'
         | 
| 1 2 | 
             
            require 'mail/gpg/sign_part'
         | 
| 2 3 |  | 
| 3 4 | 
             
            class SignPartTest < Test::Unit::TestCase
         | 
| 4 5 | 
             
            	context 'SignPart' do
         | 
| 5 6 | 
             
            		setup do
         | 
| 6 | 
            -
            			mail = Mail.new do
         | 
| 7 | 
            +
            			@mail = Mail.new do
         | 
| 7 8 | 
             
            				to 'jane@foo.bar'
         | 
| 8 9 | 
             
            				from 'joe@foo.bar'
         | 
| 9 10 | 
             
            				subject 'test'
         | 
| 10 11 | 
             
            				body 'i am unsigned'
         | 
| 11 12 | 
             
            			end
         | 
| 12 | 
            -
             | 
| 13 | 
            +
            		end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
            		should 'roundtrip successfully' do
         | 
| 16 | 
            +
            			signature_part = Mail::Gpg::SignPart.new(@mail, password: 'abc')
         | 
| 17 | 
            +
            			assert Mail::Gpg::SignPart.signature_valid?(@mail, signature_part)
         | 
| 13 18 | 
             
            		end
         | 
| 14 19 | 
             
            	end
         | 
| 15 20 | 
             
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: mail-gpg
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 0.1. | 
| 4 | 
            +
              version: 0.1.3
         | 
| 5 5 | 
             
              prerelease: 
         | 
| 6 6 | 
             
            platform: ruby
         | 
| 7 7 | 
             
            authors:
         | 
| @@ -9,7 +9,7 @@ authors: | |
| 9 9 | 
             
            autorequire: 
         | 
| 10 10 | 
             
            bindir: bin
         | 
| 11 11 | 
             
            cert_chain: []
         | 
| 12 | 
            -
            date:  | 
| 12 | 
            +
            date: 2014-02-17 00:00:00.000000000 Z
         | 
| 13 13 | 
             
            dependencies:
         | 
| 14 14 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 15 15 | 
             
              name: mail
         |