schleuder 4.0.3 → 5.0.0

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.
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