schleuder 4.0.2 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
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