schleuder 4.0.3 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +6 -9
  3. data/db/migrate/20180110203100_add_sig_enc_to_headers_to_meta_defaults.rb +1 -1
  4. data/db/migrate/20220910170110_add_key_auto_import_from_email.rb +11 -0
  5. data/db/schema.rb +6 -6
  6. data/etc/list-defaults.yml +16 -0
  7. data/etc/schleuder.yml +29 -11
  8. data/lib/schleuder/cli.rb +14 -1
  9. data/lib/schleuder/conf.rb +23 -3
  10. data/lib/schleuder/email_key_importer.rb +91 -0
  11. data/lib/schleuder/filters/post_decryption/35_key_auto_import_from_attachments.rb +21 -0
  12. data/lib/schleuder/filters/post_decryption/80_receive_from_subscribed_emailaddresses_only.rb +1 -1
  13. data/lib/schleuder/filters/post_decryption/90_strip_html_from_alternative_if_keywords_present.rb +36 -4
  14. data/lib/schleuder/filters/pre_decryption/60_key_auto_import_from_autocrypt_header.rb +9 -0
  15. data/lib/schleuder/gpgme/ctx.rb +34 -93
  16. data/lib/schleuder/gpgme/key.rb +1 -1
  17. data/lib/schleuder/gpgme/key_extractor.rb +30 -0
  18. data/lib/schleuder/http.rb +56 -0
  19. data/lib/schleuder/key_fetcher.rb +89 -0
  20. data/lib/schleuder/keyword_handlers/key_management.rb +2 -2
  21. data/lib/schleuder/keyword_handlers/subscription_management.rb +19 -3
  22. data/lib/schleuder/list.rb +26 -10
  23. data/lib/schleuder/list_builder.rb +1 -1
  24. data/lib/schleuder/logger.rb +1 -1
  25. data/lib/schleuder/mail/gpg/decrypted_part.rb +20 -0
  26. data/lib/schleuder/mail/gpg/delivery_handler.rb +38 -0
  27. data/lib/schleuder/mail/gpg/encrypted_part.rb +29 -5
  28. data/lib/schleuder/mail/gpg/gpgme_ext.rb +8 -0
  29. data/lib/schleuder/mail/gpg/gpgme_helper.rb +155 -0
  30. data/lib/schleuder/mail/gpg/inline_decrypted_message.rb +82 -0
  31. data/lib/schleuder/mail/gpg/inline_signed_message.rb +73 -0
  32. data/lib/schleuder/mail/gpg/mime_signed_message.rb +28 -0
  33. data/lib/schleuder/mail/gpg/missing_keys_error.rb +6 -0
  34. data/lib/schleuder/mail/gpg/sign_part.rb +19 -9
  35. data/lib/schleuder/mail/gpg/signed_part.rb +37 -0
  36. data/lib/schleuder/mail/gpg/verified_part.rb +10 -0
  37. data/lib/schleuder/mail/gpg/verify_result_attribute.rb +32 -0
  38. data/lib/schleuder/mail/gpg/version_part.rb +22 -0
  39. data/lib/schleuder/mail/gpg.rb +236 -7
  40. data/lib/schleuder/mail/message.rb +98 -14
  41. data/lib/schleuder/sks_client.rb +18 -0
  42. data/lib/schleuder/version.rb +1 -1
  43. data/lib/schleuder/vks_client.rb +24 -0
  44. data/lib/schleuder-api-daemon/routes/key.rb +22 -1
  45. data/lib/schleuder.rb +11 -7
  46. data/locales/de.yml +22 -3
  47. data/locales/en.yml +22 -3
  48. metadata +72 -24
@@ -0,0 +1,30 @@
1
+ module GPGME
2
+ class KeyExtractor
3
+ # This takes key material and returns those keys from it, that have a UID
4
+ # matching the given email address, stripped by all other UIDs.
5
+ def self.extract_by_email_address(email_address, keydata)
6
+ orig_gnupghome = ENV['GNUPGHOME']
7
+ ENV['GNUPGHOME'] = Dir.mktmpdir
8
+ gpg = GPGME::Ctx.new(armor: true)
9
+ gpg_arg = %{ --import-filter keep-uid='mbox = #{email_address}'}
10
+ gpg.import_filtered(keydata, gpg_arg)
11
+ # Return the fingerprint and the exported, filtered keydata, because
12
+ # passing the key objects around led to strange problems with some keys,
13
+ # which produced only a blank string as return value of export().
14
+ result = {}
15
+ gpg.keys.each do |tmp_key|
16
+ # Skip this key if it has
17
+ # * no UID – because none survived the import-filter,
18
+ # * more than one UID – which means the import-filtering failed or
19
+ # something else went wrong during import.
20
+ if tmp_key.uids.size == 1
21
+ result[tmp_key.fingerprint] = tmp_key.armored
22
+ end
23
+ end
24
+ result
25
+ ensure
26
+ FileUtils.remove_entry(ENV['GNUPGHOME'])
27
+ ENV['GNUPGHOME'] = orig_gnupghome
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,56 @@
1
+ module Schleuder
2
+ class NetworkError < StandardError; end
3
+
4
+ class NotFoundError < StandardError; end
5
+
6
+ class Http
7
+ attr_reader :request, :response
8
+
9
+ def initialize(url, options={})
10
+ @request = Typhoeus::Request.new(url, default_options.merge(options))
11
+ end
12
+
13
+ def run
14
+ @response = @request.run
15
+ if @response.success?
16
+ @response.body
17
+ elsif @response.timed_out?
18
+ raise_network_error(response, 'HTTP Request timed out.')
19
+ elsif @response.code == 404
20
+ NotFoundError.new
21
+ elsif @response.code == 0
22
+ # This happens e.g. if no response could be received.
23
+ raise_network_error(@response, 'No HTTP response received.')
24
+ else
25
+ RuntimeError.new(@response.body.to_s.presence || @response.return_message)
26
+ end
27
+ end
28
+
29
+ def self.get(url)
30
+ nth_attempt ||= 1
31
+ new(url).run
32
+ rescue NetworkError => error
33
+ nth_attempt += 1
34
+ if nth_attempt < 4
35
+ retry
36
+ else
37
+ return error
38
+ end
39
+ end
40
+
41
+ private
42
+
43
+ def raise_network_error(response, fallback_msg)
44
+ raise NetworkError.new(
45
+ response.body.to_s.presence || response.return_message || fallback_msg
46
+ )
47
+ end
48
+
49
+ def default_options
50
+ {
51
+ followlocation: true,
52
+ proxy: Conf.http_proxy.presence
53
+ }
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,89 @@
1
+ module Schleuder
2
+ class KeyFetcher
3
+ def initialize(list)
4
+ @list = list
5
+ end
6
+
7
+ def fetch(input, locale_key='key_fetched')
8
+ result = case input
9
+ when /^http/
10
+ fetch_key_by_url(input)
11
+ when Conf::EMAIL_REGEXP
12
+ fetch_key_from_keyserver('email', input)
13
+ when Conf::FINGERPRINT_REGEXP
14
+ fetch_key_from_keyserver('fingerprint', input)
15
+ else
16
+ return I18n.t('key_fetcher.invalid_input')
17
+ end
18
+ interpret_fetch_result(result, locale_key)
19
+ end
20
+
21
+ def fetch_key_by_url(url)
22
+ case result = Schleuder::Http.get(url)
23
+ when NotFoundError
24
+ NotFoundError.new(I18n.t('key_fetcher.url_not_found', url: url))
25
+ else
26
+ result
27
+ end
28
+ end
29
+
30
+ def fetch_key_from_keyserver(type, input)
31
+ if Conf.vks_keyserver.present?
32
+ result = Schleuder::VksClient.get(type, input)
33
+ end
34
+ if (result.blank? || ! result.is_a?(String)) && Conf.sks_keyserver.present?
35
+ result = Schleuder::SksClient.get(input)
36
+ end
37
+
38
+ case result
39
+ when nil
40
+ RuntimeError.new('No keyserver configured, cannot query anything')
41
+ when NotFoundError
42
+ NotFoundError.new(I18n.t('key_fetcher.not_found', input: input))
43
+ else
44
+ result
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def interpret_fetch_result(result, locale_key)
51
+ case result
52
+ when ''
53
+ I18n.t('key_fetcher.general_error', error: 'Empty response from server')
54
+ when String
55
+ import(result, locale_key)
56
+ when NotFoundError
57
+ result.to_s
58
+ when StandardError
59
+ I18n.t('key_fetcher.general_error', error: result)
60
+ else
61
+ raise_unexpected_error(result)
62
+ end
63
+ end
64
+
65
+ def import(input, locale_key)
66
+ result = @list.gpg.import_filtered(input)
67
+ case result
68
+ when StandardError
69
+ I18n.t('key_fetcher.import_error', error: result)
70
+ when Hash
71
+ translate_output(locale_key, result).join("\n")
72
+ else
73
+ raise_unexpected_error(result)
74
+ end
75
+ end
76
+
77
+ def translate_output(locale_key, import_states)
78
+ import_states.map do |fingerprint, states|
79
+ key = @list.gpg.find_distinct_key(fingerprint)
80
+ I18n.t(locale_key, key_summary: key.summary,
81
+ states: states.to_sentence)
82
+ end
83
+ end
84
+
85
+ def raise_unexpected_error(thing)
86
+ raise "Unexpected output => #{thing.inspect}"
87
+ end
88
+ end
89
+ end
@@ -15,7 +15,7 @@ module Schleuder
15
15
  import_key_from_body
16
16
  else
17
17
  @list.logger.debug 'Found no attachments and an empty body - sending error message'
18
- I18n.t('keyword_handlers.key_management.no_content_found')
18
+ return I18n.t('keyword_handlers.key_management.no_content_found')
19
19
  end
20
20
 
21
21
  import_stati = results.compact.collect(&:imports).flatten
@@ -125,7 +125,7 @@ module Schleuder
125
125
 
126
126
  def import_keys_from_attachments
127
127
  @mail.attachments.map do |attachment|
128
- import_from_string(attachment.body.raw_source)
128
+ import_from_string(attachment.body.decoded)
129
129
  end
130
130
  end
131
131
 
@@ -22,9 +22,25 @@ module Schleuder
22
22
  while @arguments.first.present? && @arguments.first.match(/^(0x)?[a-f0-9]+$/i)
23
23
  fingerprint << @arguments.shift.downcase
24
24
  end
25
- # Use possibly remaining args as flags.
26
- adminflag = @arguments.shift.to_s.downcase.presence
27
- deliveryflag = @arguments.shift.to_s.downcase.presence
25
+ # If the collected values aren't a valid fingerprint, then the input
26
+ # didn't conform with what this code expects, and then the other
27
+ # values shouldn't be used.
28
+ unless GPGME::Key.valid_fingerprint?(fingerprint)
29
+ return I18n.t('keyword_handlers.subscription_management.subscribe_requires_arguments')
30
+ end
31
+ if @arguments.present?
32
+ # Use possibly remaining args as flags.
33
+ adminflag = @arguments.shift.to_s.downcase.presence
34
+ unless ['true', 'false'].include?(adminflag)
35
+ return I18n.t('keyword_handlers.subscription_management.subscribe_requires_arguments')
36
+ end
37
+ if @arguments.present?
38
+ deliveryflag = @arguments.shift.to_s.downcase.presence
39
+ unless ['true', 'false'].include?(deliveryflag)
40
+ return I18n.t('keyword_handlers.subscription_management.subscribe_requires_arguments')
41
+ end
42
+ end
43
+ end
28
44
  end
29
45
 
30
46
  sub, _ = @list.subscribe(email, fingerprint, adminflag, deliveryflag)
@@ -4,10 +4,10 @@ module Schleuder
4
4
  has_many :subscriptions, dependent: :destroy
5
5
  before_destroy :delete_listdirs
6
6
 
7
- serialize :headers_to_meta, JSON
8
- serialize :bounces_drop_on_headers, JSON
9
- serialize :keywords_admin_only, JSON
10
- serialize :keywords_admin_notify, JSON
7
+ serialize :headers_to_meta, coder: JSON
8
+ serialize :bounces_drop_on_headers, coder: JSON
9
+ serialize :keywords_admin_only, coder: JSON
10
+ serialize :keywords_admin_notify, coder: JSON
11
11
 
12
12
  validates :email, presence: true, uniqueness: true, email: true
13
13
  validates :fingerprint, presence: true, fingerprint: true
@@ -23,7 +23,8 @@ module Schleuder
23
23
  :bounces_notify_admins,
24
24
  :include_list_headers,
25
25
  :include_openpgp_header,
26
- :forward_all_incoming_to_admins, boolean: true
26
+ :forward_all_incoming_to_admins,
27
+ :key_auto_import_from_email, boolean: true
27
28
  validates_each :headers_to_meta,
28
29
  :keywords_admin_only,
29
30
  :keywords_admin_notify do |record, attrib, value|
@@ -197,11 +198,26 @@ module Schleuder
197
198
  end
198
199
 
199
200
  def refresh_keys
200
- gpg.refresh_keys(self.keys)
201
+ # reorder keys so the update pattern is random
202
+ output = self.keys.shuffle.map do |key|
203
+ # Sleep a short while to make traffic analysis less easy.
204
+ sleep rand(1.0..5.0)
205
+ key_fetcher.fetch(key.fingerprint, 'key_updated').presence
206
+ end
207
+ # Filter out some "noise" (if a key was unchanged, it wasn't really updated, was it?)
208
+ # It would be nice to prevent these "false" lines in the first place, but I don't know how.
209
+ output.reject! do |line|
210
+ line.match('updated \(unchanged\)')
211
+ end
212
+ output.compact.join("\n")
201
213
  end
202
214
 
203
215
  def fetch_keys(input)
204
- gpg.fetch_key(input)
216
+ key_fetcher.fetch(input)
217
+ end
218
+
219
+ def key_fetcher
220
+ @key_fetcher ||= KeyFetcher.new(self)
205
221
  end
206
222
 
207
223
  def self.by_recipient(recipient)
@@ -350,7 +366,7 @@ module Schleuder
350
366
  next
351
367
  end
352
368
 
353
- if ! self.deliver_selfsent && incoming_mail.was_validly_signed? && ( subscription == incoming_mail.signer )
369
+ if ! self.deliver_selfsent && incoming_mail&.was_validly_signed? && ( subscription == incoming_mail&.signer )
354
370
  logger.info "Not sending to #{subscription.email}: delivery of self sent is disabled."
355
371
  next
356
372
  end
@@ -373,14 +389,14 @@ module Schleuder
373
389
  end
374
390
 
375
391
  def delete_listdirs
376
- if File.exists?(self.listdir)
392
+ if File.exist?(self.listdir)
377
393
  FileUtils.rm_rf(self.listdir, secure: true)
378
394
  Schleuder.logger.info "Deleted #{self.listdir}"
379
395
  end
380
396
  # If listlogs_dir is different from lists_dir, the logfile still exists
381
397
  # and needs to be deleted, too.
382
398
  logfile_dir = File.dirname(self.logfile)
383
- if File.exists?(logfile_dir)
399
+ if File.exist?(logfile_dir)
384
400
  FileUtils.rm_rf(logfile_dir, secure: true)
385
401
  Schleuder.logger.info "Deleted #{logfile_dir}"
386
402
  end
@@ -122,7 +122,7 @@ module Schleuder
122
122
  end
123
123
 
124
124
  def create_or_test_dir(dir)
125
- if File.exists?(dir)
125
+ if File.exist?(dir)
126
126
  if ! File.directory?(dir)
127
127
  raise Errors::ListdirProblem.new(dir, :not_a_directory)
128
128
  end
@@ -9,7 +9,7 @@ module Schleuder
9
9
  def initialize
10
10
  super('Schleuder', Syslog::LOG_MAIL)
11
11
  # We need some sender-address different from the superadmin-address.
12
- @from = "#{Etc.getlogin}@#{Socket.gethostname}"
12
+ @from = "#{Etc.getlogin}@#{Addrinfo.getaddrinfo(Socket.gethostname, nil).first.getnameinfo.first}"
13
13
  @adminaddresses = Conf.superadmin
14
14
  @level = ::Logger.const_get(Conf.log_level.upcase)
15
15
  end
@@ -0,0 +1,20 @@
1
+ require 'schleuder/mail/gpg/verified_part'
2
+ module Mail
3
+ module Gpg
4
+ class DecryptedPart < VerifiedPart
5
+
6
+ # options are:
7
+ #
8
+ # :verify: decrypt and verify
9
+ def initialize(cipher_part, options = {})
10
+ if cipher_part.mime_type != EncryptedPart::CONTENT_TYPE
11
+ raise EncodingError.new("RFC 3156 incorrect mime type for encrypted part '#{cipher_part.mime_type}'")
12
+ end
13
+
14
+ decrypted = GpgmeHelper.decrypt(cipher_part.body.decoded, options)
15
+ self.verify_result = decrypted.verify_result if options[:verify]
16
+ super(decrypted)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,38 @@
1
+ module Mail
2
+ module Gpg
3
+ class DeliveryHandler
4
+
5
+ def self.deliver_mail(mail)
6
+ if mail.gpg
7
+ encrypted_mail = nil
8
+ begin
9
+ options = mail.gpg.is_a?(TrueClass) ? { encrypt: true } : mail.gpg
10
+ if options[:encrypt]
11
+ encrypted_mail = Mail::Gpg.encrypt(mail, options)
12
+ elsif options[:sign] || options[:sign_as]
13
+ encrypted_mail = Mail::Gpg.sign(mail, options)
14
+ else
15
+ # encrypt and sign are off -> do not encrypt or sign
16
+ yield
17
+ end
18
+ rescue StandardError
19
+ raise $! if mail.raise_encryption_errors
20
+ end
21
+ if encrypted_mail
22
+ if dm = mail.delivery_method
23
+ encrypted_mail.instance_variable_set :@delivery_method, dm
24
+ end
25
+ encrypted_mail.perform_deliveries = mail.perform_deliveries
26
+ encrypted_mail.raise_delivery_errors = mail.raise_delivery_errors
27
+ encrypted_mail.deliver
28
+ end
29
+ else
30
+ yield
31
+ end
32
+ rescue StandardError
33
+ raise $! if mail.raise_delivery_errors
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -1,13 +1,37 @@
1
- module Mail
2
- module Gpg
3
- class EncryptedPart < Mail::Part
4
- alias_method :initialize_mailgpg, :initialize
1
+ module Mail
2
+ module Gpg
3
+ class EncryptedPart < Mail::Part
5
4
 
5
+ CONTENT_TYPE = 'application/octet-stream'
6
+
7
+ # options are:
8
+ #
9
+ # :signers : sign using this key (give the corresponding email address)
10
+ # :password: passphrase for the signing key
11
+ # :recipients : array of receiver addresses
12
+ # :keys : A hash mapping recipient email addresses to public keys or public
13
+ # key ids. Imports any keys given here that are not already part of the
14
+ # local keychain before sending the mail. If this option is given, strictly
15
+ # only the key material from this hash is used, ignoring any keys for
16
+ # recipients that might have been added to the local key chain but are
17
+ # not mentioned here.
18
+ # :always_trust : send encrypted mail to untrusted receivers, true by default
19
+ # :filename : define a custom name for the encrypted file attachment
6
20
  def initialize(cleartext_mail, options = {})
7
21
  if cleartext_mail.protected_headers_subject
8
22
  cleartext_mail.content_type_parameters['protected-headers'] = 'v1'
9
23
  end
10
- initialize_mailgpg(cleartext_mail, options)
24
+
25
+ options = { always_trust: true }.merge options
26
+
27
+ encrypted = GpgmeHelper.encrypt(cleartext_mail.encoded, options)
28
+ super() do
29
+ body encrypted.to_s
30
+ filename = options[:filename] || 'encrypted.asc'
31
+ content_type "#{CONTENT_TYPE}; name=\"#{filename}\""
32
+ content_disposition "inline; filename=\"#{filename}\""
33
+ content_description 'OpenPGP encrypted message'
34
+ end
11
35
  end
12
36
  end
13
37
  end
@@ -0,0 +1,8 @@
1
+ require 'schleuder/mail/gpg/verify_result_attribute'
2
+
3
+ # extend GPGME::Data with an attribute to hold the result of signature
4
+ # verifications
5
+ class GPGME::Data
6
+ include Mail::Gpg::VerifyResultAttribute
7
+ end
8
+
@@ -0,0 +1,155 @@
1
+ require 'schleuder/mail/gpg/gpgme_ext'
2
+
3
+ # GPGME methods for encryption/decryption/signing
4
+ module Mail
5
+ module Gpg
6
+ class GpgmeHelper
7
+
8
+ def self.encrypt(plain, options = {})
9
+ options = options.merge({armor: true})
10
+
11
+ plain_data = GPGME::Data.new(plain)
12
+ cipher_data = GPGME::Data.new(options[:output])
13
+
14
+ recipient_keys = keys_for_data options[:recipients], options.delete(:keys)
15
+
16
+ if recipient_keys.empty?
17
+ raise MissingKeysError.new('No keys to encrypt to!')
18
+ end
19
+
20
+ flags = 0
21
+ flags |= GPGME::ENCRYPT_ALWAYS_TRUST if options[:always_trust]
22
+
23
+ GPGME::Ctx.new(options) do |ctx|
24
+ begin
25
+ if options[:sign]
26
+ if options[:signers] && options[:signers].size > 0
27
+ signers = GPGME::Key.find(:secret, options[:signers], :sign)
28
+ ctx.add_signer(*signers)
29
+ end
30
+ ctx.encrypt_sign(recipient_keys, plain_data, cipher_data, flags)
31
+ else
32
+ ctx.encrypt(recipient_keys, plain_data, cipher_data, flags)
33
+ end
34
+ rescue GPGME::Error::UnusablePublicKey => exc
35
+ exc.keys = ctx.encrypt_result.invalid_recipients
36
+ raise exc
37
+ rescue GPGME::Error::UnusableSecretKey => exc
38
+ exc.keys = ctx.sign_result.invalid_signers
39
+ raise exc
40
+ end
41
+ end
42
+
43
+ cipher_data.seek(0)
44
+ cipher_data
45
+ end
46
+
47
+ def self.decrypt(cipher, options = {})
48
+ cipher_data = GPGME::Data.new(cipher)
49
+ plain_data = GPGME::Data.new(options[:output])
50
+
51
+ GPGME::Ctx.new(options) do |ctx|
52
+ begin
53
+ if options[:verify]
54
+ ctx.decrypt_verify(cipher_data, plain_data)
55
+ plain_data.verify_result = ctx.verify_result
56
+ else
57
+ ctx.decrypt(cipher_data, plain_data)
58
+ end
59
+ rescue GPGME::Error::UnsupportedAlgorithm => exc
60
+ exc.algorithm = ctx.decrypt_result.unsupported_algorithm
61
+ raise exc
62
+ rescue GPGME::Error::WrongKeyUsage => exc
63
+ exc.key_usage = ctx.decrypt_result.wrong_key_usage
64
+ raise exc
65
+ end
66
+ end
67
+
68
+ plain_data.seek(0)
69
+ plain_data
70
+ end
71
+
72
+ def self.sign(plain, options = {})
73
+ options.merge!({
74
+ armor: true,
75
+ signer: options.delete(:sign_as),
76
+ mode: GPGME::SIG_MODE_DETACH
77
+ })
78
+ crypto = GPGME::Crypto.new
79
+ crypto.sign GPGME::Data.new(plain), options
80
+ end
81
+
82
+ # returns [success(bool), VerifyResult(from gpgme)]
83
+ # success will be true when there is at least one sig and no invalid sig
84
+ def self.sign_verify(plain, signature, options = {})
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?
110
+ end
111
+ return [success, verify_result]
112
+ end
113
+
114
+ # normalizes the list of recipients' emails, key ids and key data to a
115
+ # list of Key objects
116
+ #
117
+ # if key_data is given, _only_ key material from there is used,
118
+ # and eventually already imported keys in the keychain are ignored.
119
+ def self.keys_for_data(emails_or_shas_or_keys, key_data = nil)
120
+ if key_data
121
+ # in this case, emails_or_shas_or_keys is supposed to be the list of
122
+ # recipients, and key_data the key material to be used.
123
+ # We now map these to whatever we find in key_data for each of these
124
+ # addresses.
125
+ [emails_or_shas_or_keys].flatten.map do |r|
126
+ k = key_data[r]
127
+ key_id = case k
128
+ when GPGME::Key
129
+ # assuming this is already imported
130
+ k.fingerprint
131
+ when nil, ''
132
+ # nothing
133
+ nil
134
+ when /-----BEGIN PGP/
135
+ # ASCII key data
136
+ GPGME::Key.import(k).imports.map(&:fpr)
137
+ else
138
+ # key id or fingerprint
139
+ k
140
+ end
141
+ unless key_id.nil? || key_id.empty?
142
+ GPGME::Key.find(:public, key_id, :encrypt)
143
+ end
144
+ end.flatten.compact
145
+ elsif emails_or_shas_or_keys && (emails_or_shas_or_keys.size > 0)
146
+ # key lookup in keychain for all receivers
147
+ GPGME::Key.find :public, emails_or_shas_or_keys, :encrypt
148
+ else
149
+ # empty array given
150
+ []
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,82 @@
1
+ require 'schleuder/mail/gpg/verified_part'
2
+
3
+ # decryption of the so called 'PGP-Inline' message types
4
+ # this is not a standard, so the implementation is based on the notes
5
+ # here http://binblog.info/2008/03/12/know-your-pgp-implementation/
6
+ # and on test messages generated with the Mozilla Enigmail OpenPGP
7
+ # plugin https://www.enigmail.net
8
+ module Mail
9
+ module Gpg
10
+ class InlineDecryptedMessage < Mail::Message
11
+
12
+ # options are:
13
+ #
14
+ # :verify: decrypt and verify
15
+ def self.setup(cipher_mail, options = {})
16
+ if cipher_mail.multipart?
17
+ self.new do
18
+ Mail::Gpg.copy_headers cipher_mail, self
19
+
20
+ # Drop the HTML-part of a multipart/alternative-message if it is
21
+ # inline-encrypted: that ciphertext is probably wrapped in HTML,
22
+ # which GnuPG chokes upon, so we would have to parse the HTML to
23
+ # handle the message-part properly.
24
+ # Also it's not clear how to handle the resulting plain-text: is
25
+ # it HTML or simple text? That depends on the sending MUA and
26
+ # the original input.
27
+ # In summary, that's too much complications.
28
+ if cipher_mail.mime_type == 'multipart/alternative' &&
29
+ cipher_mail.html_part.present? &&
30
+ cipher_mail.html_part.body.decoded.include?('-----BEGIN PGP MESSAGE-----')
31
+ cipher_mail.parts.delete_if do |part|
32
+ part[:content_type].content_type == 'text/html'
33
+ end
34
+ # Set the content-type of the newly generated message to
35
+ # something less confusing.
36
+ content_type 'multipart/mixed'
37
+ # Leave a marker for other code.
38
+ header['X-MailGpg-Deleted-Html-Part'] = 'true'
39
+ end
40
+
41
+ cipher_mail.parts.each do |part|
42
+ p = VerifiedPart.new do |p|
43
+ if part.has_content_type? && /application\/(?:octet-stream|pgp-encrypted)/ =~ part.mime_type
44
+ # encrypted attachment, we set the content_type to the generic 'application/octet-stream'
45
+ # and remove the .pgp/gpg/asc from name/filename in header fields
46
+ decrypted = GpgmeHelper.decrypt(part.decoded, options)
47
+ p.verify_result decrypted.verify_result if options[:verify]
48
+ p.content_type part.content_type.sub(/application\/(?:octet-stream|pgp-encrypted)/, 'application/octet-stream')
49
+ .sub(/name=(?:"')?(.*)\.(?:pgp|gpg|asc)(?:"')?/, 'name="\1"')
50
+ p.content_disposition part.content_disposition.sub(/filename=(?:"')?(.*)\.(?:pgp|gpg|asc)(?:"')?/, 'filename="\1"')
51
+ p.content_transfer_encoding Mail::Encodings::Base64
52
+ p.body Mail::Encodings::Base64.encode(decrypted.to_s)
53
+ else
54
+ body = part.body.decoded
55
+ if body.include?('-----BEGIN PGP MESSAGE-----')
56
+ decrypted = GpgmeHelper.decrypt(body, options)
57
+ p.verify_result decrypted.verify_result if options[:verify]
58
+ p.body decrypted.to_s
59
+ else
60
+ p.content_type part.content_type
61
+ p.content_transfer_encoding part.content_transfer_encoding
62
+ p.body part.body.to_s
63
+ end
64
+ end
65
+ end
66
+ add_part p
67
+ end
68
+ end
69
+ else
70
+ decrypted = cipher_mail.body.empty? ? '' : GpgmeHelper.decrypt(cipher_mail.body.decoded, options)
71
+ self.new do
72
+ cipher_mail.header.fields.each do |field|
73
+ header[field.name] = field.value
74
+ end
75
+ body decrypted.to_s
76
+ verify_result decrypted.verify_result if options[:verify] && decrypted != ''
77
+ end
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end