mail-gpg 0.1.7 → 0.2.1

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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 431a878dcca05522a519c9edca6a42e658c9b0c2
4
+ data.tar.gz: 18f9aeb942581062f8644e14391520d825829473
5
+ SHA512:
6
+ metadata.gz: baf5ef3a729d141a0d76d1fe1466a8cbbdc954e373af0c23e58020d64933af087cf3839a1ff6bdc9ccfebaa539aa2af5026b6c543848998912344361718a3163
7
+ data.tar.gz: 0052629db9f5c1a595c10d2dbf491be5f9544ae0fdb37b7541437bb2bf4866f559a9f7cd98d235d9cd414494e049905069c7dd730181d9fb749deec277943eaa
data/History.txt CHANGED
@@ -1,3 +1,14 @@
1
+ == 0.2.1 2014-07-23
2
+
3
+ * keep original message id if set
4
+
5
+ == 0.2.0 2014-07-16
6
+
7
+ * implement signature verification for inline signed messages
8
+ * make signature verification more consistent with decryption by providing
9
+ Mail::Message#verify which strips raw signature data and initializes verify_result member.
10
+ * add Mail::Message#signatures as an easy way to retrieve all signatures on a Message
11
+
1
12
  == 0.1.7 2014-06-03
2
13
 
3
14
  * preserve References: header
data/README.md CHANGED
@@ -119,14 +119,28 @@ Receive the mail as usual. Check if it is signed using the `signed?` method. Che
119
119
  ```ruby
120
120
  mail = Mail.first
121
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?}"
122
+ verified = mail.verify
123
+ puts "signature(s) valid: #{verified.signature_valid?}"
124
+ puts "message signed by: #{verified.signatures.map{|sig|sig.from}.join("\n")}"
125
125
  end
126
126
  ```
127
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.
128
+ Note that for encrypted mails the signatures can not be checked using these
129
+ methods. For encrypted mails the `:verify` option for the `decrypt` operation
130
+ must be used instead:
131
+
132
+ ```ruby
133
+ if mail.encrypted?
134
+ decrypted = mail.decrypt(verify: true, password: 's3cr3t')
135
+ puts "signature(s) valid: #{decrypted.signature_valid?}"
136
+ puts "message signed by: #{decrypted.signatures.map{|sig|sig.from}.join("\n")}"
137
+ end
138
+ ```
139
+
140
+ It's important to actually use the information contained in the `signatures`
141
+ array to check if the message really has been signed by the person that you (or
142
+ your users) think is the sender - usually by comparing the key id of the
143
+ signature with the key id of the expected sender.
130
144
 
131
145
  ### Key import from public key servers
132
146
 
@@ -201,5 +215,7 @@ around with your personal gpg keychain.
201
215
 
202
216
  Thanks to:
203
217
 
218
+ * [Planio GmbH](https://plan.io) for sponsoring the ongoing maintenance and development of this library
204
219
  * [morten-andersen](https://github.com/morten-andersen) for implementing decryption support for PGP/MIME and inline encrypted messages
205
- * [FewKinG](https://github.com/FewKinG) for implementing the sign only featur and keyserver url lookup
220
+ * [FewKinG](https://github.com/FewKinG) for implementing the sign only feature and keyserver url lookup
221
+ * [Fup Duck](https://github.com/duckdalbe) for various tweaks and fixes
data/lib/mail/gpg.rb CHANGED
@@ -3,6 +3,7 @@ require 'mail/message'
3
3
  require 'gpgme'
4
4
 
5
5
  require 'mail/gpg/version'
6
+ require 'mail/gpg/missing_keys_error'
6
7
  require 'mail/gpg/version_part'
7
8
  require 'mail/gpg/decrypted_part'
8
9
  require 'mail/gpg/encrypted_part'
@@ -11,6 +12,8 @@ require 'mail/gpg/gpgme_helper'
11
12
  require 'mail/gpg/message_patch'
12
13
  require 'mail/gpg/rails'
13
14
  require 'mail/gpg/signed_part'
15
+ require 'mail/gpg/mime_signed_message'
16
+ require 'mail/gpg/inline_signed_message'
14
17
 
15
18
  module Mail
16
19
  module Gpg
@@ -71,6 +74,8 @@ module Mail
71
74
  def self.signature_valid?(signed_mail, options = {})
72
75
  if signed_mime?(signed_mail)
73
76
  signature_valid_pgp_mime?(signed_mail, options)
77
+ elsif signed_inline?(signed_mail)
78
+ signature_valid_inline?(signed_mail, options)
74
79
  else
75
80
  raise EncodingError, "Unsupported signature format '#{signed_mail.content_type}'"
76
81
  end
@@ -108,6 +113,9 @@ module Mail
108
113
  self.header[field] = h.value
109
114
  end
110
115
  end
116
+ if cleartext_mail.message_id
117
+ header['Message-ID'] = cleartext_mail['Message-ID'].value
118
+ end
111
119
  cleartext_mail.header.fields.each do |field|
112
120
  if MORE_HEADERS.include?(field.name) or field.name =~ /^X-/
113
121
  header[field.name] = field.value
@@ -126,7 +134,8 @@ module Mail
126
134
  if !VersionPart.isVersionPart? encrypted_mail.parts[0]
127
135
  raise EncodingError, "RFC 3136 first part not a valid version part '#{encrypted_mail.parts[0]}'"
128
136
  end
129
- Mail.new(DecryptedPart.new(encrypted_mail.parts[1], options)) do
137
+ decrypted = DecryptedPart.new(encrypted_mail.parts[1], options)
138
+ Mail.new(decrypted) do
130
139
  %w(from to cc bcc subject reply_to in_reply_to).each do |field|
131
140
  send field, encrypted_mail.send(field)
132
141
  end
@@ -136,6 +145,7 @@ module Mail
136
145
  encrypted_mail.header.fields.each do |field|
137
146
  header[field.name] = field.value if field.name =~ /^X-/ && header[field.name].nil?
138
147
  end
148
+ verify_result decrypted.verify_result if options[:verify]
139
149
  end
140
150
  end
141
151
 
@@ -144,15 +154,49 @@ module Mail
144
154
  InlineDecryptedMessage.new(encrypted_mail, options)
145
155
  end
146
156
 
157
+ def self.verify(signed_mail, options = {})
158
+ if signed_mime?(signed_mail)
159
+ Mail::Gpg::MimeSignedMessage.new signed_mail, options
160
+ elsif signed_inline?(signed_mail)
161
+ Mail::Gpg::InlineSignedMessage.new signed_mail, options
162
+ else
163
+ signed_mail
164
+ end
165
+ end
166
+
147
167
  # check signature for PGP/MIME (RFC 3156, section 5) signed mail
148
168
  def self.signature_valid_pgp_mime?(signed_mail, options)
149
169
  # MUST contain exactly two body parts
150
170
  if signed_mail.parts.length != 2
151
171
  raise EncodingError, "RFC 3136 mandates exactly two body parts, found '#{signed_mail.parts.length}'"
152
172
  end
153
- SignPart.signature_valid?(signed_mail.parts[0], signed_mail.parts[1], options)
173
+ result, verify_result = SignPart.verify_signature(signed_mail.parts[0], signed_mail.parts[1], options)
174
+ signed_mail.verify_result = verify_result
175
+ return result
154
176
  end
155
177
 
178
+ # check signature for inline signed mail
179
+ def self.signature_valid_inline?(signed_mail, options)
180
+ result = nil
181
+ if signed_mail.multipart?
182
+ signed_mail.parts.each do |part|
183
+ if signed_inline?(part)
184
+ if result.nil?
185
+ result = true
186
+ signed_mail.verify_result = []
187
+ end
188
+ result &= signature_valid_inline?(part, options)
189
+ signed_mail.verify_result << part.verify_result
190
+ end
191
+ end
192
+ else
193
+ result, verify_result = GpgmeHelper.inline_verify(signed_mail.body.to_s, options)
194
+ signed_mail.verify_result = verify_result
195
+ end
196
+ return result
197
+ end
198
+
199
+
156
200
  # check if PGP/MIME encrypted (RFC 3156)
157
201
  def self.encrypted_mime?(mail)
158
202
  mail.has_content_type? &&
@@ -1,6 +1,7 @@
1
+ require 'mail/gpg/verified_part'
1
2
  module Mail
2
3
  module Gpg
3
- class DecryptedPart < Mail::Part
4
+ class DecryptedPart < VerifiedPart
4
5
 
5
6
  # options are:
6
7
  #
@@ -11,6 +12,7 @@ module Mail
11
12
  end
12
13
 
13
14
  decrypted = GpgmeHelper.decrypt(cipher_part.body.decoded, options)
15
+ self.verify_result = decrypted.verify_result if options[:verify]
14
16
  super(decrypted)
15
17
  end
16
18
  end
@@ -0,0 +1,9 @@
1
+ require 'gpgme'
2
+ require 'mail/gpg/verify_result_attribute'
3
+
4
+ # extend GPGME::Data with an attribute to hold the result of signature
5
+ # verifications
6
+ class GPGME::Data
7
+ include Mail::Gpg::VerifyResultAttribute
8
+ end
9
+
@@ -1,3 +1,5 @@
1
+ require 'mail/gpg/gpgme_ext'
2
+
1
3
  # GPGME methods for encryption/decryption/signing
2
4
  module Mail
3
5
  module Gpg
@@ -11,6 +13,10 @@ module Mail
11
13
 
12
14
  recipient_keys = keys_for_data options[:recipients], options.delete(:keys)
13
15
 
16
+ if recipient_keys.empty?
17
+ raise MissingKeysError.new('No keys to encrypt to!')
18
+ end
19
+
14
20
  flags = 0
15
21
  flags |= GPGME::ENCRYPT_ALWAYS_TRUST if options[:always_trust]
16
22
 
@@ -46,6 +52,7 @@ module Mail
46
52
  begin
47
53
  if options[:verify]
48
54
  ctx.decrypt_verify(cipher_data, plain_data)
55
+ plain_data.verify_result = ctx.verify_result
49
56
  else
50
57
  ctx.decrypt(cipher_data, plain_data)
51
58
  end
@@ -72,13 +79,36 @@ module Mail
72
79
  crypto.sign GPGME::Data.new(plain), options
73
80
  end
74
81
 
82
+ # returns [success(bool), VerifyResult(from gpgme)]
83
+ # success will be true when there is at least one sig and no invalid sig
75
84
  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
85
+ signed_data = GPGME::Data.new(plain)
86
+ signature = GPGME::Data.new(signature)
87
+
88
+ success = verify_result = nil
89
+ GPGME::Ctx.new(options) do |ctx|
90
+ ctx.verify signature, signed_data, nil
91
+ verify_result = ctx.verify_result
92
+ signatures = verify_result.signatures
93
+ success = signatures &&
94
+ signatures.size > 0 &&
95
+ signatures.detect{|s| !s.valid? }.nil?
96
+ end
97
+ return [success, verify_result]
98
+ end
99
+
100
+ def self.inline_verify(signed_text, options = {})
101
+ signed_data = GPGME::Data.new(signed_text)
102
+ success = verify_result = nil
103
+ GPGME::Ctx.new(options) do |ctx|
104
+ ctx.verify signed_data, nil
105
+ verify_result = ctx.verify_result
106
+ signatures = verify_result.signatures
107
+ success = signatures &&
108
+ signatures.size > 0 &&
109
+ signatures.detect{|s| !s.valid? }.nil?
80
110
  end
81
- return signed
111
+ return [success, verify_result]
82
112
  end
83
113
 
84
114
  private
@@ -1,3 +1,5 @@
1
+ require 'mail/gpg/verified_part'
2
+
1
3
  # decryption of the so called 'PGP-Inline' message types
2
4
  # this is not a standard, so the implementation is based on the notes
3
5
  # here http://binblog.info/2008/03/12/know-your-pgp-implementation/
@@ -17,11 +19,12 @@ module Mail
17
19
  header[field.name] = field.value
18
20
  end
19
21
  cipher_mail.parts.each do |part|
20
- part Mail::Part.new do |p|
22
+ p = VerifiedPart.new do |p|
21
23
  if part.has_content_type? && /application\/(?:octet-stream|pgp-encrypted)/ =~ part.mime_type
22
24
  # encrypted attachment, we set the content_type to the generic 'application/octet-stream'
23
25
  # and remove the .pgp/gpg/asc from name/filename in header fields
24
26
  decrypted = GpgmeHelper.decrypt(part.decoded, options)
27
+ p.verify_result decrypted.verify_result if options[:verify]
25
28
  p.content_type part.content_type.sub(/application\/(?:octet-stream|pgp-encrypted)/, 'application/octet-stream')
26
29
  .sub(/name=(?:"')?(.*)\.(?:pgp|gpg|asc)(?:"')?/, 'name="\1"')
27
30
  p.content_disposition part.content_disposition.sub(/filename=(?:"')?(.*)\.(?:pgp|gpg|asc)(?:"')?/, 'filename="\1"')
@@ -29,7 +32,9 @@ module Mail
29
32
  p.body Mail::Encodings::Base64::encode(decrypted.to_s)
30
33
  else
31
34
  if part.body.include?('-----BEGIN PGP MESSAGE-----')
32
- p.body GpgmeHelper.decrypt(part.decoded, options).to_s
35
+ decrypted = GpgmeHelper.decrypt(part.decoded, options)
36
+ p.verify_result decrypted.verify_result if options[:verify]
37
+ p.body decrypted.to_s
33
38
  else
34
39
  p.content_type part.content_type
35
40
  p.content_transfer_encoding part.content_transfer_encoding
@@ -37,6 +42,7 @@ module Mail
37
42
  end
38
43
  end
39
44
  end
45
+ add_part p
40
46
  end
41
47
  end # of multipart
42
48
  else
@@ -46,6 +52,7 @@ module Mail
46
52
  header[field.name] = field.value
47
53
  end
48
54
  body decrypted.to_s
55
+ verify_result decrypted.verify_result if options[:verify] && '' != decrypted
49
56
  end
50
57
  end
51
58
  end
@@ -0,0 +1,72 @@
1
+ require 'mail/gpg/verified_part'
2
+
3
+ module Mail
4
+ module Gpg
5
+ class InlineSignedMessage < Mail::Message
6
+
7
+ def initialize(signed_mail, options = {})
8
+ if signed_mail.multipart?
9
+ super() do
10
+ global_verify_result = []
11
+ signed_mail.header.fields.each do |field|
12
+ header[field.name] = field.value
13
+ end
14
+ signed_mail.parts.each do |part|
15
+ if Mail::Gpg.signed_inline?(part)
16
+ signed_text = part.body.to_s
17
+ success, vr = GpgmeHelper.inline_verify(signed_text, options)
18
+ p = VerifiedPart.new(part)
19
+ if success
20
+ p.body self.class.strip_inline_signature signed_text
21
+ end
22
+ p.verify_result vr
23
+ global_verify_result << vr
24
+ add_part p
25
+ else
26
+ add_part part
27
+ end
28
+ end
29
+ verify_result global_verify_result
30
+ end # of multipart
31
+ else
32
+ super() do
33
+ signed_mail.header.fields.each do |field|
34
+ header[field.name] = field.value
35
+ end
36
+ signed_text = signed_mail.body.to_s
37
+ success, vr = GpgmeHelper.inline_verify(signed_text, options)
38
+ if success
39
+ body self.class.strip_inline_signature signed_text
40
+ else
41
+ body signed_text
42
+ end
43
+ verify_result vr
44
+ end
45
+ end
46
+ end
47
+
48
+ END_SIGNED_TEXT = '-----END PGP SIGNED MESSAGE-----'
49
+ END_SIGNED_TEXT_RE = /^#{END_SIGNED_TEXT}\s*$/
50
+ INLINE_SIG_RE = Regexp.new('^-----BEGIN PGP SIGNATURE-----\s*$.*^-----END PGP SIGNATURE-----\s*$', Regexp::MULTILINE)
51
+ BEGIN_SIG_RE = /^(-----BEGIN PGP SIGNATURE-----)\s*$/
52
+
53
+
54
+ # utility method to remove inline signature and related pgp markers
55
+ def self.strip_inline_signature(signed_text)
56
+ if signed_text =~ INLINE_SIG_RE
57
+ signed_text = signed_text.dup
58
+ if signed_text !~ END_SIGNED_TEXT_RE
59
+ # insert the 'end of signed text' marker in case it is missing
60
+ signed_text = signed_text.gsub BEGIN_SIG_RE, "-----END PGP SIGNED MESSAGE-----\n\\1"
61
+ end
62
+ signed_text.gsub! INLINE_SIG_RE, ''
63
+ signed_text.strip!
64
+ end
65
+ signed_text
66
+ end
67
+
68
+ end
69
+ end
70
+ end
71
+
72
+
@@ -1,4 +1,5 @@
1
1
  require 'mail/gpg/delivery_handler'
2
+ require 'mail/gpg/verify_result_attribute'
2
3
 
3
4
  module Mail
4
5
  module Gpg
@@ -7,6 +8,7 @@ module Mail
7
8
  def self.included(base)
8
9
  base.class_eval do
9
10
  attr_accessor :raise_encryption_errors
11
+ include VerifyResultAttribute
10
12
  end
11
13
  end
12
14
 
@@ -51,21 +53,35 @@ module Mail
51
53
  end
52
54
  end
53
55
 
56
+ # true if this mail is encrypted
54
57
  def encrypted?
55
58
  Mail::Gpg.encrypted?(self)
56
59
  end
57
60
 
61
+ # returns the decrypted mail object.
62
+ #
63
+ # pass verify: true to verify signatures as well. The gpgme verification
64
+ # result will be available via decrypted_mail.verify_result
58
65
  def decrypt(options = {})
59
66
  Mail::Gpg.decrypt(self, options)
60
67
  end
61
68
 
69
+ # true if this mail is signed (but not encrypted)
62
70
  def signed?
63
71
  Mail::Gpg.signed?(self)
64
72
  end
65
73
 
66
- def signature_valid?(options = {})
67
- Mail::Gpg.signature_valid?(self, options)
74
+ # verify signatures. returns a new mail object with signatures removed and
75
+ # populated verify_result.
76
+ #
77
+ # verified = signed_mail.verify()
78
+ # verified.signature_valid?
79
+ # signers = mail.signatures.map{|sig| sig.from}
80
+ def verify(options = {})
81
+ Mail::Gpg.verify(self, options)
68
82
  end
83
+
84
+
69
85
  end
70
86
  end
71
87
  end
@@ -0,0 +1,30 @@
1
+ require 'mail/gpg/verified_part'
2
+
3
+ module Mail
4
+ module Gpg
5
+ class MimeSignedMessage < Mail::Message
6
+
7
+ def initialize(signed_mail, options = {})
8
+ content_part, signature = signed_mail.parts
9
+ success, vr = SignPart.verify_signature(content_part, signature, options)
10
+ super() do
11
+ verify_result vr
12
+ signed_mail.header.fields.each do |field|
13
+ header[field.name] = field.value
14
+ end
15
+ content_part.header.fields.each do |field|
16
+ header[field.name] = field.value
17
+ end
18
+ if content_part.multipart?
19
+ content_part.parts.each{|part| add_part part}
20
+ else
21
+ body content_part.body.to_s
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
29
+
30
+
@@ -0,0 +1,6 @@
1
+ module Mail
2
+ module Gpg
3
+ class MissingKeysError < StandardError
4
+ end
5
+ end
6
+ end
@@ -12,7 +12,13 @@ module Mail
12
12
  end
13
13
  end
14
14
 
15
+ # true if all signatures are valid
15
16
  def self.signature_valid?(plain_part, signature_part, options = {})
17
+ verify_signature(plain_part, signature_part, options)[0]
18
+ end
19
+
20
+ # will return [success(boolean), verify_result(as returned by gpgme)]
21
+ def self.verify_signature(plain_part, signature_part, options = {})
16
22
  if !(signature_part.has_content_type? &&
17
23
  ('application/pgp-signature' == signature_part.mime_type))
18
24
  return false
@@ -0,0 +1,10 @@
1
+ require 'mail/gpg/verify_result_attribute'
2
+
3
+ module Mail
4
+ module Gpg
5
+ class VerifiedPart < Mail::Part
6
+ include VerifyResultAttribute
7
+ end
8
+ end
9
+ end
10
+
@@ -0,0 +1,31 @@
1
+ module Mail
2
+ module Gpg
3
+ module VerifyResultAttribute
4
+
5
+ # the result of signature verification, as provided by GPGME
6
+ def verify_result(result = nil)
7
+ if result
8
+ self.verify_result = result
9
+ else
10
+ @verify_result
11
+ end
12
+ end
13
+ def verify_result=(result)
14
+ @verify_result = result
15
+ end
16
+
17
+ # checks validity of signatures (true / false)
18
+ def signature_valid?
19
+ sigs = self.signatures
20
+ sigs.any? && sigs.detect{|s|!s.valid?}.blank?
21
+ end
22
+
23
+ # list of all signatures from verify_result
24
+ def signatures
25
+ [verify_result].flatten.compact.map do |vr|
26
+ vr.signatures
27
+ end.flatten.compact
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,5 +1,5 @@
1
1
  module Mail
2
2
  module Gpg
3
- VERSION = "0.1.7"
3
+ VERSION = "0.2.1"
4
4
  end
5
5
  end
@@ -26,6 +26,10 @@ class DecryptedPartTest < Test::Unit::TestCase
26
26
  assert mail == @mail
27
27
  assert mail.message_id == @mail.message_id
28
28
  assert mail.message_id != @part.message_id
29
+ assert vr = mail.verify_result
30
+ assert sig = vr.signatures.first
31
+ assert sig.to_s=~ /Joe/
32
+ assert sig.valid?
29
33
  end
30
34
 
31
35
  should 'raise encoding error for non gpg mime type' do
@@ -33,5 +37,7 @@ class DecryptedPartTest < Test::Unit::TestCase
33
37
  part.content_type = 'text/plain'
34
38
  assert_raise(EncodingError) { Mail::Gpg::DecryptedPart.new(part) }
35
39
  end
40
+
41
+
36
42
  end
37
43
  end
data/test/gpg_test.rb CHANGED
@@ -36,6 +36,11 @@ class GpgTest < Test::Unit::TestCase
36
36
  assert sig.valid?
37
37
  end
38
38
  assert Mail::Gpg.signature_valid?(signed)
39
+ assert verified = signed.verify
40
+ assert verified.verify_result.present?
41
+ assert verified.verify_result.signatures.any?
42
+ assert verified.signatures.any?
43
+ assert verified.signature_valid?
39
44
  end
40
45
 
41
46
  def check_mime_structure_signed(mail = @mail, signed = @signed)
@@ -250,6 +255,10 @@ class GpgTest < Test::Unit::TestCase
250
255
  should 'decrypt and verify' do
251
256
  assert mail = Mail::Gpg.decrypt(@encrypted, { :verify => true, :password => 'abc' })
252
257
  assert mail == @mail
258
+ assert mail.verify_result
259
+ assert sig = mail.signatures.first
260
+ assert sig.to_s =~ /Joe/
261
+ assert sig.valid?
253
262
  end
254
263
  end
255
264
 
@@ -330,7 +339,7 @@ class GpgTest < Test::Unit::TestCase
330
339
  context 'multipart mail' do
331
340
  setup do
332
341
  @mail.add_file 'Rakefile'
333
- @encrypted = Mail::Gpg.encrypt(@mail)
342
+ @encrypted = Mail::Gpg.encrypt(@mail, sign: true, password: 'abc')
334
343
  end
335
344
 
336
345
  should 'have same recipients and subject' do
@@ -355,10 +364,16 @@ class GpgTest < Test::Unit::TestCase
355
364
  assert_match /Rakefile/, m.parts.last.content_disposition
356
365
  end
357
366
 
358
- should 'decrypt' do
359
- assert mail = Mail::Gpg.decrypt(@encrypted, { :password => 'abc' })
367
+ should 'decrypt and verify' do
368
+ assert mail = Mail::Gpg.decrypt(@encrypted, { :verify => true, :password => 'abc' })
360
369
  assert mail == @mail
361
370
  assert mail.parts[1] == @mail.parts[1]
371
+ assert mail.verify_result
372
+ assert signatures = mail.signatures
373
+ assert_equal 1, signatures.size
374
+ assert sig = signatures[0]
375
+ assert sig.to_s =~ /Joe/
376
+ assert sig.valid?
362
377
  end
363
378
  end
364
379
  end
@@ -17,15 +17,19 @@ class InlineDecryptedMessageTest < Test::Unit::TestCase
17
17
  end
18
18
 
19
19
  context "inline message" do
20
- should "decrypt body" do
20
+ should "decrypt and verify body" do
21
21
  mail = Mail.new(@mail)
22
22
  mail.body = InlineDecryptedMessageTest.encrypt(mail, mail.body.to_s)
23
23
 
24
24
  assert !mail.multipart?
25
25
  assert mail.encrypted?
26
- assert decrypted = mail.decrypt(:password => 'abc')
26
+ assert decrypted = mail.decrypt(:password => 'abc', verify: true)
27
27
  assert decrypted == @mail
28
28
  assert !decrypted.encrypted?
29
+ assert vr = decrypted.verify_result
30
+ assert sig = vr.signatures.first
31
+ assert sig.to_s=~ /Joe/
32
+ assert sig.valid?
29
33
  end
30
34
  end
31
35
 
@@ -55,7 +59,7 @@ class InlineDecryptedMessageTest < Test::Unit::TestCase
55
59
  end
56
60
 
57
61
  context "cleartext body and encrypted attachment message" do
58
- should "decrypt attachment" do
62
+ should "decrypt and verify attachment" do
59
63
  rakefile = File.open('Rakefile') { |file| file.read }
60
64
  mail = Mail.new(@mail)
61
65
  mail.content_type = 'multipart/mixed'
@@ -68,7 +72,7 @@ class InlineDecryptedMessageTest < Test::Unit::TestCase
68
72
 
69
73
  assert mail.multipart?
70
74
  assert mail.encrypted?
71
- assert decrypted = mail.decrypt(:password => 'abc')
75
+ assert decrypted = mail.decrypt(password: 'abc', verify: true)
72
76
  assert !decrypted.encrypted?
73
77
  check_headers(@mail, decrypted)
74
78
  assert_equal 2, decrypted.parts.length
@@ -76,11 +80,17 @@ class InlineDecryptedMessageTest < Test::Unit::TestCase
76
80
  assert /application\/octet-stream; (?:charset=UTF-8; )?name=Rakefile/ =~ decrypted.parts[1].content_type
77
81
  assert_equal 'attachment; filename=Rakefile', decrypted.parts[1].content_disposition
78
82
  assert_equal rakefile, decrypted.parts[1].body.decoded
83
+
84
+ assert_nil decrypted.parts[0].verify_result
85
+ assert vr = decrypted.parts[1].verify_result
86
+ assert sig = vr.signatures.first
87
+ assert sig.to_s=~ /Joe/
88
+ assert sig.valid?
79
89
  end
80
90
  end
81
91
 
82
92
  context "encrypted body and attachment message" do
83
- should "decrypt" do
93
+ should "decrypt and verify" do
84
94
  rakefile = File.open('Rakefile') { |file| file.read }
85
95
  mail = Mail.new(@mail)
86
96
  mail.content_type = 'multipart/mixed'
@@ -94,7 +104,7 @@ class InlineDecryptedMessageTest < Test::Unit::TestCase
94
104
 
95
105
  assert mail.multipart?
96
106
  assert mail.encrypted?
97
- assert decrypted = mail.decrypt(:password => 'abc')
107
+ assert decrypted = mail.decrypt(password: 'abc', verify: true)
98
108
  assert !decrypted.encrypted?
99
109
  check_headers(@mail, decrypted)
100
110
  assert_equal 2, decrypted.parts.length
@@ -102,6 +112,12 @@ class InlineDecryptedMessageTest < Test::Unit::TestCase
102
112
  assert /application\/octet-stream; (?:charset=UTF-8; )?name=Rakefile/ =~ decrypted.parts[1].content_type
103
113
  assert_equal 'attachment; filename=Rakefile', decrypted.parts[1].content_disposition
104
114
  assert_equal rakefile, decrypted.parts[1].body.decoded
115
+ decrypted.parts.each do |part|
116
+ assert vr = part.verify_result
117
+ assert sig = vr.signatures.first
118
+ assert sig.to_s=~ /Joe/
119
+ assert sig.valid?
120
+ end
105
121
  end
106
122
  end
107
123
  end
@@ -0,0 +1,121 @@
1
+ require 'test_helper'
2
+
3
+ # test cases for PGP inline signed messages (i.e. non-mime)
4
+ class InlineSignedMessageTest < Test::Unit::TestCase
5
+
6
+ context "InlineSignedMessage" do
7
+
8
+ setup do
9
+ (@mails = Mail::TestMailer.deliveries).clear
10
+ @mail = Mail.new do
11
+ to 'jane@foo.bar'
12
+ from 'joe@foo.bar'
13
+ subject 'test'
14
+ body 'i am unencrypted'
15
+ end
16
+ end
17
+
18
+ context 'strip_inline_signature' do
19
+ should 'strip signature from signed text' do
20
+ body = self.class.inline_sign(@mail, 'i am signed')
21
+ assert stripped_body = Mail::Gpg::InlineSignedMessage.strip_inline_signature(body)
22
+ assert_equal "-----BEGIN PGP SIGNED MESSAGE-----\nHash: SHA1\n\ni am signed\n-----END PGP SIGNED MESSAGE-----", stripped_body
23
+ end
24
+
25
+ should 'not change unsigned text' do
26
+ assert stripped_body = Mail::Gpg::InlineSignedMessage.strip_inline_signature("foo\nbar\n")
27
+ assert_equal "foo\nbar\n", stripped_body
28
+ end
29
+ end
30
+
31
+ context "signed message" do
32
+ should "verify body" do
33
+ mail = Mail.new(@mail)
34
+ mail.body = self.class.inline_sign(mail, mail.body.to_s)
35
+ assert !mail.multipart?
36
+ assert mail.signed?
37
+ assert verified = mail.verify
38
+ assert verified.signature_valid?
39
+ assert sig = verified.signatures.first
40
+ assert sig.to_s=~ /Joe/
41
+ assert sig.valid?
42
+ end
43
+
44
+ should "detect invalid sig" do
45
+ mail = Mail.new(@mail)
46
+ mail.body = self.class.inline_sign(mail, mail.body.to_s).gsub /i am/, 'i was'
47
+ assert !mail.multipart?
48
+ assert mail.signed?
49
+ assert verified = mail.verify
50
+ assert !verified.signature_valid?
51
+ assert vr = verified.verify_result
52
+ assert sig = verified.signatures.first
53
+ assert sig.to_s=~ /Joe/
54
+ assert !sig.valid?
55
+ end
56
+
57
+ end
58
+
59
+ context "message with signed attachment" do
60
+ should "check attachment signature" do
61
+ mail = Mail.new(@mail)
62
+ mail.body = 'foobar'
63
+ mail.part do |p|
64
+ p.body = self.class.inline_sign(mail, 'sign me!')
65
+ end
66
+ assert mail.multipart?
67
+ assert mail.signed?
68
+ assert verified = mail.verify
69
+ assert verified.signature_valid?
70
+ assert vr = verified.parts.last.verify_result
71
+ assert !verified.parts.first.signed?
72
+ assert verified.parts.last.signed?
73
+ assert Mail::Gpg.signed_inline?(verified.parts.last)
74
+ assert_equal [vr], verified.verify_result
75
+ assert sig = verified.signatures.first
76
+ assert sig.to_s=~ /Joe/
77
+ assert sig.valid?
78
+ end
79
+
80
+ should "detect invalid sig" do
81
+ mail = Mail.new(@mail)
82
+ mail.body = 'foobar'
83
+ mail.part do |p|
84
+ p.body = self.class.inline_sign(mail, 'i am signed!').gsub /i am/, 'i was'
85
+ end
86
+ mail.part do |p|
87
+ p.body = self.class.inline_sign(mail, 'i am signed!')
88
+ end
89
+
90
+ assert mail.multipart?
91
+ assert mail.signed?
92
+ assert verified = mail.verify
93
+ assert !verified.signature_valid?
94
+ assert vr = verified.verify_result
95
+ assert_equal 2, vr.size
96
+
97
+ invalid = verified.parts[1]
98
+ assert !invalid.signature_valid?
99
+ assert sig = invalid.verify_result.signatures.first
100
+ assert sig.to_s=~ /Joe/
101
+ assert !sig.valid?
102
+
103
+ valid = verified.parts[2]
104
+ assert valid.signature_valid?
105
+ assert sig = valid.verify_result.signatures.first
106
+ assert sig.to_s=~ /Joe/
107
+ assert sig.valid?
108
+ end
109
+ end
110
+ end
111
+
112
+
113
+ def self.inline_sign(mail, plain, armor = true)
114
+ GPGME::Crypto.new.clearsign(plain,
115
+ password: 'abc',
116
+ signers: mail.from,
117
+ armor: armor).to_s
118
+ end
119
+
120
+ end
121
+
data/test/message_test.rb CHANGED
@@ -42,6 +42,35 @@ class MessageTest < Test::Unit::TestCase
42
42
  @mail.gpg sign: true, password: 'abc'
43
43
  end
44
44
 
45
+ context 'with multiple parts' do
46
+ setup do
47
+ p = Mail::Part.new do
48
+ body 'and another part'
49
+ end
50
+ @mail.add_part p
51
+ p = Mail::Part.new do
52
+ body 'and a third part'
53
+ end
54
+ @mail.add_part p
55
+
56
+ @mail.deliver
57
+ @signed = @mails.first
58
+ @verified = @signed.verify
59
+ end
60
+
61
+ should 'verify signature' do
62
+ assert @verified.signature_valid?
63
+ end
64
+
65
+ should 'have original three parts' do
66
+ assert_equal 3, @mail.parts.size
67
+ assert_equal 3, @verified.parts.size
68
+ assert_equal 'i am unencrypted', @verified.parts[0].body.to_s
69
+ assert_equal 'and another part', @verified.parts[1].body.to_s
70
+ assert_equal 'and a third part', @verified.parts[2].body.to_s
71
+ end
72
+ end
73
+
45
74
  context "" do
46
75
  setup do
47
76
  @mail.header['Auto-Submitted'] = 'foo'
@@ -59,14 +88,18 @@ class MessageTest < Test::Unit::TestCase
59
88
  assert !m.encrypted?
60
89
  assert m.signed?
61
90
  assert m.multipart?
62
- assert m.signature_valid?
63
91
  assert sign_part = m.parts.last
64
- #assert m = Mail::Message.new(m.parts.first)
65
- #assert !m.multipart?
66
92
  GPGME::Crypto.new.verify(sign_part.body.to_s, signed_text: m.parts.first.encoded) do |sig|
67
93
  assert sig.valid?
68
94
  end
69
- assert_equal 'i am unencrypted', m.parts.first.body.to_s
95
+ end
96
+
97
+ should 'verify signed mail' do
98
+ assert m = @mails.first
99
+ assert verified = m.verify
100
+ assert verified.signature_valid?
101
+ assert !verified.multipart?
102
+ assert_equal 'i am unencrypted', verified.body.to_s
70
103
  end
71
104
 
72
105
  should "fail signature on tampered body" do
@@ -76,13 +109,36 @@ class MessageTest < Test::Unit::TestCase
76
109
  assert !m.encrypted?
77
110
  assert m.signed?
78
111
  assert m.multipart?
79
- assert m.signature_valid?
112
+ assert verified = m.verify
113
+ assert verified.signature_valid?
80
114
  m.parts.first.body = 'replaced body'
81
- assert !m.signature_valid?
115
+ assert verified = m.verify
116
+ assert !verified.signature_valid?
82
117
  end
83
118
  end
84
119
  end
85
120
 
121
+ context 'with encryption and signing' do
122
+ setup do
123
+ @mail.gpg encrypt: true, sign: true, password: 'abc'
124
+ @mail.deliver
125
+ end
126
+
127
+ should 'decrypt and check signature' do
128
+ assert_equal 1, @mails.size
129
+ assert m = @mails.first
130
+ assert_equal 'test', m.subject
131
+ assert m.multipart?
132
+ assert m.encrypted?
133
+ assert decrypted = m.decrypt(:password => 'abc', verify: true)
134
+ assert_equal 'test', decrypted.subject
135
+ assert decrypted == @mail
136
+ assert_equal 'i am unencrypted', decrypted.body.to_s
137
+ assert decrypted.signature_valid?
138
+ assert_equal 1, decrypted.signatures.size
139
+ end
140
+ end
141
+
86
142
  context "with gpg turned on" do
87
143
  setup do
88
144
  @mail.gpg encrypt: true
@@ -94,7 +150,7 @@ class MessageTest < Test::Unit::TestCase
94
150
  end
95
151
 
96
152
  should "raise encryption error" do
97
- assert_raises(GPGME::Error::InvalidValue){
153
+ assert_raises(Mail::Gpg::MissingKeysError){
98
154
  @mail.deliver
99
155
  }
100
156
  end
@@ -106,6 +162,7 @@ class MessageTest < Test::Unit::TestCase
106
162
  end
107
163
  end
108
164
 
165
+
109
166
  context "" do
110
167
  setup do
111
168
  @mail.deliver
metadata CHANGED
@@ -1,64 +1,58 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mail-gpg
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.7
5
- prerelease:
4
+ version: 0.2.1
6
5
  platform: ruby
7
6
  authors:
8
7
  - Jens Kraemer
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2014-06-03 00:00:00.000000000 Z
11
+ date: 2014-07-23 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: mail
16
15
  requirement: !ruby/object:Gem::Requirement
17
- none: false
18
16
  requirements:
19
17
  - - ~>
20
18
  - !ruby/object:Gem::Version
21
19
  version: '2.5'
22
- - - ! '>='
20
+ - - '>='
23
21
  - !ruby/object:Gem::Version
24
22
  version: 2.5.3
25
23
  type: :runtime
26
24
  prerelease: false
27
25
  version_requirements: !ruby/object:Gem::Requirement
28
- none: false
29
26
  requirements:
30
27
  - - ~>
31
28
  - !ruby/object:Gem::Version
32
29
  version: '2.5'
33
- - - ! '>='
30
+ - - '>='
34
31
  - !ruby/object:Gem::Version
35
32
  version: 2.5.3
36
33
  - !ruby/object:Gem::Dependency
37
34
  name: gpgme
38
35
  requirement: !ruby/object:Gem::Requirement
39
- none: false
40
36
  requirements:
41
37
  - - ~>
42
38
  - !ruby/object:Gem::Version
43
39
  version: '2.0'
44
- - - ! '>='
40
+ - - '>='
45
41
  - !ruby/object:Gem::Version
46
42
  version: 2.0.2
47
43
  type: :runtime
48
44
  prerelease: false
49
45
  version_requirements: !ruby/object:Gem::Requirement
50
- none: false
51
46
  requirements:
52
47
  - - ~>
53
48
  - !ruby/object:Gem::Version
54
49
  version: '2.0'
55
- - - ! '>='
50
+ - - '>='
56
51
  - !ruby/object:Gem::Version
57
52
  version: 2.0.2
58
53
  - !ruby/object:Gem::Dependency
59
54
  name: bundler
60
55
  requirement: !ruby/object:Gem::Requirement
61
- none: false
62
56
  requirements:
63
57
  - - ~>
64
58
  - !ruby/object:Gem::Version
@@ -66,7 +60,6 @@ dependencies:
66
60
  type: :development
67
61
  prerelease: false
68
62
  version_requirements: !ruby/object:Gem::Requirement
69
- none: false
70
63
  requirements:
71
64
  - - ~>
72
65
  - !ruby/object:Gem::Version
@@ -74,7 +67,6 @@ dependencies:
74
67
  - !ruby/object:Gem::Dependency
75
68
  name: minitest
76
69
  requirement: !ruby/object:Gem::Requirement
77
- none: false
78
70
  requirements:
79
71
  - - ~>
80
72
  - !ruby/object:Gem::Version
@@ -82,7 +74,6 @@ dependencies:
82
74
  type: :development
83
75
  prerelease: false
84
76
  version_requirements: !ruby/object:Gem::Requirement
85
- none: false
86
77
  requirements:
87
78
  - - ~>
88
79
  - !ruby/object:Gem::Version
@@ -90,55 +81,48 @@ dependencies:
90
81
  - !ruby/object:Gem::Dependency
91
82
  name: rake
92
83
  requirement: !ruby/object:Gem::Requirement
93
- none: false
94
84
  requirements:
95
- - - ! '>='
85
+ - - '>='
96
86
  - !ruby/object:Gem::Version
97
87
  version: '0'
98
88
  type: :development
99
89
  prerelease: false
100
90
  version_requirements: !ruby/object:Gem::Requirement
101
- none: false
102
91
  requirements:
103
- - - ! '>='
92
+ - - '>='
104
93
  - !ruby/object:Gem::Version
105
94
  version: '0'
106
95
  - !ruby/object:Gem::Dependency
107
96
  name: actionmailer
108
97
  requirement: !ruby/object:Gem::Requirement
109
- none: false
110
98
  requirements:
111
- - - ! '>='
99
+ - - '>='
112
100
  - !ruby/object:Gem::Version
113
101
  version: 3.2.0
114
102
  type: :development
115
103
  prerelease: false
116
104
  version_requirements: !ruby/object:Gem::Requirement
117
- none: false
118
105
  requirements:
119
- - - ! '>='
106
+ - - '>='
120
107
  - !ruby/object:Gem::Version
121
108
  version: 3.2.0
122
109
  - !ruby/object:Gem::Dependency
123
110
  name: pry-nav
124
111
  requirement: !ruby/object:Gem::Requirement
125
- none: false
126
112
  requirements:
127
- - - ! '>='
113
+ - - '>='
128
114
  - !ruby/object:Gem::Version
129
115
  version: '0'
130
116
  type: :development
131
117
  prerelease: false
132
118
  version_requirements: !ruby/object:Gem::Requirement
133
- none: false
134
119
  requirements:
135
- - - ! '>='
120
+ - - '>='
136
121
  - !ruby/object:Gem::Version
137
122
  version: '0'
138
123
  - !ruby/object:Gem::Dependency
139
124
  name: shoulda-context
140
125
  requirement: !ruby/object:Gem::Requirement
141
- none: false
142
126
  requirements:
143
127
  - - ~>
144
128
  - !ruby/object:Gem::Version
@@ -146,15 +130,13 @@ dependencies:
146
130
  type: :development
147
131
  prerelease: false
148
132
  version_requirements: !ruby/object:Gem::Requirement
149
- none: false
150
133
  requirements:
151
134
  - - ~>
152
135
  - !ruby/object:Gem::Version
153
136
  version: '1.1'
154
- description: ! 'GPG/MIME encryption plugin for the Ruby Mail Library
155
-
156
- This tiny gem adds GPG capabilities to Mail::Message and ActionMailer::Base. Because
157
- privacy matters.'
137
+ description: |-
138
+ GPG/MIME encryption plugin for the Ruby Mail Library
139
+ This tiny gem adds GPG capabilities to Mail::Message and ActionMailer::Base. Because privacy matters.
158
140
  email:
159
141
  - jk@jkraemer.net
160
142
  executables: []
@@ -174,13 +156,19 @@ files:
174
156
  - lib/mail/gpg/decrypted_part.rb
175
157
  - lib/mail/gpg/delivery_handler.rb
176
158
  - lib/mail/gpg/encrypted_part.rb
159
+ - lib/mail/gpg/gpgme_ext.rb
177
160
  - lib/mail/gpg/gpgme_helper.rb
178
161
  - lib/mail/gpg/inline_decrypted_message.rb
162
+ - lib/mail/gpg/inline_signed_message.rb
179
163
  - lib/mail/gpg/message_patch.rb
164
+ - lib/mail/gpg/mime_signed_message.rb
165
+ - lib/mail/gpg/missing_keys_error.rb
180
166
  - lib/mail/gpg/rails.rb
181
167
  - lib/mail/gpg/rails/action_mailer_base_patch.rb
182
168
  - lib/mail/gpg/sign_part.rb
183
169
  - lib/mail/gpg/signed_part.rb
170
+ - lib/mail/gpg/verified_part.rb
171
+ - lib/mail/gpg/verify_result_attribute.rb
184
172
  - lib/mail/gpg/version.rb
185
173
  - lib/mail/gpg/version_part.rb
186
174
  - mail-gpg.gemspec
@@ -195,6 +183,7 @@ files:
195
183
  - test/gpghome/trustdb.gpg
196
184
  - test/hkp_test.rb
197
185
  - test/inline_decrypted_message_test.rb
186
+ - test/inline_signed_message_test.rb
198
187
  - test/message_test.rb
199
188
  - test/sign_part_test.rb
200
189
  - test/test_helper.rb
@@ -202,27 +191,26 @@ files:
202
191
  homepage: https://github.com/jkraemer/mail-gpg
203
192
  licenses:
204
193
  - MIT
194
+ metadata: {}
205
195
  post_install_message:
206
196
  rdoc_options: []
207
197
  require_paths:
208
198
  - lib
209
199
  required_ruby_version: !ruby/object:Gem::Requirement
210
- none: false
211
200
  requirements:
212
- - - ! '>='
201
+ - - '>='
213
202
  - !ruby/object:Gem::Version
214
203
  version: '0'
215
204
  required_rubygems_version: !ruby/object:Gem::Requirement
216
- none: false
217
205
  requirements:
218
- - - ! '>='
206
+ - - '>='
219
207
  - !ruby/object:Gem::Version
220
208
  version: '0'
221
209
  requirements: []
222
210
  rubyforge_project:
223
- rubygems_version: 1.8.23.2
211
+ rubygems_version: 2.2.2
224
212
  signing_key:
225
- specification_version: 3
213
+ specification_version: 4
226
214
  summary: GPG/MIME encryption plugin for the Ruby Mail Library
227
215
  test_files:
228
216
  - test/action_mailer_test.rb
@@ -236,7 +224,9 @@ test_files:
236
224
  - test/gpghome/trustdb.gpg
237
225
  - test/hkp_test.rb
238
226
  - test/inline_decrypted_message_test.rb
227
+ - test/inline_signed_message_test.rb
239
228
  - test/message_test.rb
240
229
  - test/sign_part_test.rb
241
230
  - test/test_helper.rb
242
231
  - test/version_part_test.rb
232
+ has_rdoc: