schleuder 4.0.2 → 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 (52) 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/20211106112020_change_boolean_values_to_integers.rb +46 -0
  5. data/db/migrate/20211107151309_add_limits_to_string_columns.rb +28 -0
  6. data/db/migrate/20220910170110_add_key_auto_import_from_email.rb +11 -0
  7. data/db/schema.rb +16 -16
  8. data/etc/list-defaults.yml +16 -0
  9. data/etc/schleuder.yml +29 -11
  10. data/lib/schleuder/cli.rb +15 -2
  11. data/lib/schleuder/conf.rb +23 -3
  12. data/lib/schleuder/email_key_importer.rb +91 -0
  13. data/lib/schleuder/filters/post_decryption/35_key_auto_import_from_attachments.rb +21 -0
  14. data/lib/schleuder/filters/post_decryption/80_receive_from_subscribed_emailaddresses_only.rb +1 -1
  15. data/lib/schleuder/filters/post_decryption/90_strip_html_from_alternative_if_keywords_present.rb +36 -4
  16. data/lib/schleuder/filters/pre_decryption/60_key_auto_import_from_autocrypt_header.rb +9 -0
  17. data/lib/schleuder/filters_runner.rb +1 -30
  18. data/lib/schleuder/gpgme/ctx.rb +34 -93
  19. data/lib/schleuder/gpgme/key.rb +1 -1
  20. data/lib/schleuder/gpgme/key_extractor.rb +30 -0
  21. data/lib/schleuder/http.rb +56 -0
  22. data/lib/schleuder/key_fetcher.rb +89 -0
  23. data/lib/schleuder/keyword_handlers/key_management.rb +2 -2
  24. data/lib/schleuder/keyword_handlers/subscription_management.rb +19 -3
  25. data/lib/schleuder/list.rb +26 -10
  26. data/lib/schleuder/list_builder.rb +1 -1
  27. data/lib/schleuder/logger.rb +1 -1
  28. data/lib/schleuder/mail/gpg/decrypted_part.rb +20 -0
  29. data/lib/schleuder/mail/gpg/delivery_handler.rb +38 -0
  30. data/lib/schleuder/mail/gpg/encrypted_part.rb +29 -5
  31. data/lib/schleuder/mail/gpg/gpgme_ext.rb +8 -0
  32. data/lib/schleuder/mail/gpg/gpgme_helper.rb +155 -0
  33. data/lib/schleuder/mail/gpg/inline_decrypted_message.rb +82 -0
  34. data/lib/schleuder/mail/gpg/inline_signed_message.rb +73 -0
  35. data/lib/schleuder/mail/gpg/mime_signed_message.rb +28 -0
  36. data/lib/schleuder/mail/gpg/missing_keys_error.rb +6 -0
  37. data/lib/schleuder/mail/gpg/sign_part.rb +19 -9
  38. data/lib/schleuder/mail/gpg/signed_part.rb +37 -0
  39. data/lib/schleuder/mail/gpg/verified_part.rb +10 -0
  40. data/lib/schleuder/mail/gpg/verify_result_attribute.rb +32 -0
  41. data/lib/schleuder/mail/gpg/version_part.rb +22 -0
  42. data/lib/schleuder/mail/gpg.rb +236 -7
  43. data/lib/schleuder/mail/message.rb +98 -14
  44. data/lib/schleuder/runner.rb +40 -10
  45. data/lib/schleuder/sks_client.rb +18 -0
  46. data/lib/schleuder/version.rb +1 -1
  47. data/lib/schleuder/vks_client.rb +24 -0
  48. data/lib/schleuder-api-daemon/routes/key.rb +22 -1
  49. data/lib/schleuder.rb +11 -7
  50. data/locales/de.yml +38 -19
  51. data/locales/en.yml +22 -3
  52. metadata +58 -21
@@ -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
@@ -0,0 +1,73 @@
1
+ require 'schleuder/mail/gpg/verified_part'
2
+
3
+ module Mail
4
+ module Gpg
5
+ class InlineSignedMessage < Mail::Message
6
+
7
+ def self.setup(signed_mail, options = {})
8
+ if signed_mail.multipart?
9
+ self.new 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
31
+ else
32
+ self.new 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
+ # Strip possible inline-"headers" (e.g. "Hash: SHA256", or "Comment: something").
66
+ signed_text.gsub(/(.*^-----BEGIN PGP SIGNED MESSAGE-----\n)(.*?)^$(.+)/m, '\1\3')
67
+ end
68
+
69
+ end
70
+ end
71
+ end
72
+
73
+
@@ -0,0 +1,28 @@
1
+ require 'schleuder/mail/gpg/verified_part'
2
+
3
+ module Mail
4
+ module Gpg
5
+ class MimeSignedMessage < Mail::Message
6
+
7
+ def self.setup(signed_mail, options = {})
8
+ content_part, signature = signed_mail.parts
9
+ success, vr = SignPart.verify_signature(content_part, signature, options)
10
+ self.new 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.raw_source
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+
@@ -0,0 +1,6 @@
1
+ module Mail
2
+ module Gpg
3
+ class MissingKeysError < StandardError
4
+ end
5
+ end
6
+ end
@@ -1,15 +1,26 @@
1
1
  module Mail
2
2
  module Gpg
3
3
  class SignPart < Mail::Part
4
- # Copied verbatim from mail-gpg v.0.4.2. This code was changed in
5
- # <https://github.com/jkraemer/mail-gpg/commit/5fded41ccee4a58f848a2f8e7bd53d11236f8984>,
6
- # which breaks verifying some encapsulated (signed-then-encrypted)
7
- # messages. See
8
- # <https://github.com/jkraemer/mail-gpg/pull/40#issue-95776382> for
9
- # details.
4
+
5
+ def initialize(cleartext_mail, options = {})
6
+ signature = GpgmeHelper.sign(cleartext_mail.encoded, options)
7
+ super() do
8
+ body signature.to_s
9
+ content_type "application/pgp-signature; name=\"signature.asc\""
10
+ content_disposition 'attachment; filename="signature.asc"'
11
+ content_description 'OpenPGP digital signature'
12
+ end
13
+ end
14
+
15
+ # true if all signatures are valid
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)]
10
21
  def self.verify_signature(plain_part, signature_part, options = {})
11
22
  if !(signature_part.has_content_type? &&
12
- ('application/pgp-signature' == signature_part.mime_type))
23
+ ('application/pgp-signature' == signature_part.mime_type))
13
24
  return false
14
25
  end
15
26
 
@@ -19,7 +30,7 @@ module Mail
19
30
  plaintext = [ plain_part.header.raw_source,
20
31
  "\r\n\r\n",
21
32
  plain_part.body.raw_source
22
- ].join
33
+ ].join
23
34
  else
24
35
  plaintext = plain_part.encoded
25
36
  end
@@ -30,4 +41,3 @@ module Mail
30
41
  end
31
42
  end
32
43
  end
33
-
@@ -0,0 +1,37 @@
1
+ require 'mail/part'
2
+ require 'schleuder/mail/gpg/sign_part'
3
+
4
+ module Mail
5
+ module Gpg
6
+
7
+ class SignedPart < Mail::Part
8
+
9
+ def self.build(cleartext_mail)
10
+ new do
11
+ if cleartext_mail.body.multipart?
12
+ if cleartext_mail.content_type =~ /^(multipart[^;]+)/
13
+ # preserve multipart/alternative etc
14
+ content_type $1
15
+ else
16
+ content_type 'multipart/mixed'
17
+ end
18
+ cleartext_mail.body.parts.each do |p|
19
+ add_part p
20
+ end
21
+ else
22
+ content_type cleartext_mail.content_type
23
+ body cleartext_mail.body.raw_source
24
+ end
25
+ end
26
+ end
27
+
28
+ def sign(options)
29
+ SignPart.new(self, options)
30
+ end
31
+
32
+
33
+ end
34
+
35
+
36
+ end
37
+ end
@@ -0,0 +1,10 @@
1
+ require 'schleuder/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,32 @@
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
+
14
+ def verify_result=(result)
15
+ @verify_result = result
16
+ end
17
+
18
+ # checks validity of signatures (true / false)
19
+ def signature_valid?
20
+ sigs = self.signatures
21
+ sigs.any? && sigs.all?{|s| s.valid?}
22
+ end
23
+
24
+ # list of all signatures from verify_result
25
+ def signatures
26
+ [verify_result].flatten.compact.map do |vr|
27
+ vr.signatures
28
+ end.flatten.compact
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,22 @@
1
+ require 'mail/part'
2
+
3
+ module Mail
4
+ module Gpg
5
+ class VersionPart < Mail::Part
6
+ VERSION_1 = 'Version: 1'
7
+ CONTENT_TYPE = 'application/pgp-encrypted'
8
+ CONTENT_DESC = 'PGP/MIME Versions Identification'
9
+
10
+ def initialize(*args)
11
+ super
12
+ body VERSION_1
13
+ content_type CONTENT_TYPE
14
+ content_description CONTENT_DESC
15
+ end
16
+
17
+ def self.is_version_part?(part)
18
+ part.mime_type == CONTENT_TYPE && part.body =~ /#{VERSION_1}/
19
+ end
20
+ end
21
+ end
22
+ end