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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c44b12c722680d91cee45a80ae2ee6a4ff47a9544dfd171cc83a5549bdd72be2
4
- data.tar.gz: 77773ef59d05b1faff86cf2fe5d22ef657cd5683e6184db1a55b28d1592b07d2
3
+ metadata.gz: 4148fa7b166ff22d2af8980a549f08e7bc659080df8f9579d15a190cbb046033
4
+ data.tar.gz: edb0f987169bdb5585f9350057e5f7114e377d13cec9e9f413a6da24a4e3cd46
5
5
  SHA512:
6
- metadata.gz: eb351c776f1773cf0dd5ef0f04f310fe70a0155d9344fd75eb78fce9b0d69cad0f0d9bfad664d1a8ab004a55c2d29a849cf1ea2cd0a295c61c3b56b96a25d9a8
7
- data.tar.gz: 97ab8283c694fac04196ee752ffe3153f57acb1e37c8754c8c9f1840fbf5109d47fac3426aa8fa3f168341efbfcf38c8d4292e9a360b3d81dff5d9231ac9b700
6
+ metadata.gz: 8d86957d1b3b3e6929290f273b2fd39a65c012aa19fba80231aba12c413024c19dd513cf4a61dce7cb93af1996ade5d95de8230ef0a7e855fedaa741aa1f10a4
7
+ data.tar.gz: 00605d42c733d792fffd50652e1f7c7c8b8e98066006bf49eff25bf7d6c7523419229d18537e07f2735ca8b2fe4b243b043966a13079c8d8fce7d274fd2629c1
data/README.md CHANGED
@@ -9,12 +9,13 @@ For more details see <https://schleuder.org/docs/>.
9
9
 
10
10
  Requirements
11
11
  ------------
12
- * ruby >=2.5
12
+ * ruby >=2.7
13
13
  * gnupg >=2.2
14
14
  * gpgme
15
15
  * sqlite3
16
16
  * openssl
17
17
  * icu
18
+ * libcurl
18
19
 
19
20
  *If you use Debian buster, CentOS 7 or Archlinux, please have a look at the [installation docs](https://schleuder.org/schleuder/docs/server-admins.html#installation). We do provide packages for those platforms, which simplify the installation a lot.*
20
21
 
@@ -36,15 +37,15 @@ Additionally these **rubygems** are required (will be installed automatically un
36
37
  Installing Schleuder
37
38
  ------------
38
39
 
39
- 1. Download [the gem](https://schleuder.org/download/schleuder-4.0.3.gem) and [the OpenPGP-signature](https://schleuder.org/download/schleuder-4.0.3.gem.sig) and verify:
40
+ 1. Download [the gem](https://schleuder.org/download/schleuder-5.0.0.gem) and [the OpenPGP-signature](https://schleuder.org/download/schleuder-5.0.0.gem.sig) and verify:
40
41
  ```
41
42
  gpg --recv-key 0xB3D190D5235C74E1907EACFE898F2C91E2E6E1F3
42
- gpg --verify schleuder-4.0.3.gem.sig
43
+ gpg --verify schleuder-5.0.0.gem.sig
43
44
  ```
44
45
 
45
46
  2. If all went well install the gem:
46
47
  ```
47
- gem install schleuder-4.0.3.gem
48
+ gem install schleuder-5.0.0.gem
48
49
  ```
49
50
 
50
51
  3. Set up schleuder:
@@ -101,10 +102,6 @@ To execute the test suite run:
101
102
 
102
103
  bundle exec rspec
103
104
 
104
- Please note: Some of the specs use 'pgrep'. On systems that base on Debian 10 ("buster") install it via
105
-
106
- apt-get install procps
107
-
108
105
  We are working on extendig the test coverage.
109
106
 
110
107
  Contributing
@@ -134,4 +131,4 @@ GNU GPL 3.0. Please see [LICENSE.txt](LICENSE.txt).
134
131
  Alternative Download
135
132
  --------------------
136
133
 
137
- Alternatively to the gem-files you can download the latest release as [a tarball](https://schleuder.org/download/schleuder-4.0.3.tar.gz) and [its OpenPGP-signature](https://schleuder.org/download/schleuder-4.0.3.tar.gz.sig).
134
+ Alternatively to the gem-files you can download the latest release as [a tarball](https://schleuder.org/download/schleuder-5.0.0.tar.gz) and [its OpenPGP-signature](https://schleuder.org/download/schleuder-5.0.0.tar.gz.sig).
@@ -24,7 +24,7 @@ class AddSigEncToHeadersToMetaDefaults < ActiveRecord::Migration[4.2]
24
24
  # complexities of the actual class.
25
25
  Class.new(ActiveRecord::Base) do
26
26
  self.table_name = 'lists'
27
- self.serialize :headers_to_meta, JSON
27
+ self.serialize :headers_to_meta, coder: JSON
28
28
  end
29
29
  end
30
30
  end
@@ -0,0 +1,11 @@
1
+ class AddKeyAutoImportFromEmail < ActiveRecord::Migration[7.1]
2
+ def up
3
+ if ! column_exists?(:lists, :key_auto_import_from_email)
4
+ add_column :lists, :key_auto_import_from_email, :boolean, default: false
5
+ end
6
+ end
7
+
8
+ def down
9
+ remove_column(:lists, :key_auto_import_from_email)
10
+ end
11
+ end
data/db/schema.rb CHANGED
@@ -10,11 +10,10 @@
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
13
- ActiveRecord::Schema.define(version: 2021_11_07_151309) do
14
-
13
+ ActiveRecord::Schema[7.1].define(version: 2022_09_10_170110) do
15
14
  create_table "lists", force: :cascade do |t|
16
- t.datetime "created_at"
17
- t.datetime "updated_at"
15
+ t.datetime "created_at", precision: nil
16
+ t.datetime "updated_at", precision: nil
18
17
  t.string "email", limit: 255
19
18
  t.string "fingerprint", limit: 255
20
19
  t.string "log_level", limit: 255, default: "warn"
@@ -47,6 +46,7 @@ ActiveRecord::Schema.define(version: 2021_11_07_151309) do
47
46
  t.boolean "include_autocrypt_header", default: true
48
47
  t.boolean "set_reply_to_to_sender", default: false
49
48
  t.boolean "munge_from", default: false
49
+ t.boolean "key_auto_import_from_email", default: false
50
50
  end
51
51
 
52
52
  create_table "subscriptions", force: :cascade do |t|
@@ -55,8 +55,8 @@ ActiveRecord::Schema.define(version: 2021_11_07_151309) do
55
55
  t.string "fingerprint", limit: 255
56
56
  t.boolean "admin", default: false
57
57
  t.boolean "delivery_enabled", default: true
58
- t.datetime "created_at"
59
- t.datetime "updated_at"
58
+ t.datetime "created_at", precision: nil
59
+ t.datetime "updated_at", precision: nil
60
60
  t.index ["email", "list_id"], name: "index_subscriptions_on_email_and_list_id", unique: true
61
61
  t.index ["list_id"], name: "index_subscriptions_on_list_id"
62
62
  end
@@ -149,3 +149,19 @@ set_reply_to_to_sender: false
149
149
  # This option can enabled for improved usability since this affect
150
150
  # mail client's displayed name.
151
151
  munge_from: false
152
+
153
+ # If enabled, parse Autocrypt-header and look for attached keys in incoming emails.
154
+ # For each found key (a), check if it contains a UID matching the From-header. In
155
+ # case it does not, skip this key and handle the next, if any.
156
+ # Lookup fingerprint of key (a) and check if it's already present in the keyring.
157
+ # If present, import the key (a).
158
+ # Otherwise, check if a key, which contains a UID matching the From-header, is present
159
+ # in the keyring.
160
+ # If not, import the key (a).
161
+ # Before doing any import, ensure via a filter so that only the relevant UID is
162
+ # kept.
163
+ # Please be aware that this might result in a newly imported key that matches a
164
+ # subscriber's email address, which didn't originate from the subscriber (in case
165
+ # of forged From-header). We consider this not a serious problem because that key
166
+ # will not be used for that subscriber's emails unless it is assigned manually.
167
+ key_auto_import_from_email: false
data/etc/schleuder.yml CHANGED
@@ -21,17 +21,28 @@ filters_dir: /usr/local/lib/schleuder/filters
21
21
  # How verbose should Schleuder log to syslog? (list-specific messages are written to the list's log-file).
22
22
  log_level: warn
23
23
 
24
- # Which keyserver to refresh keys from (used by `schleuder refresh_keys`, meant
25
- # to be run from cron or systemd weekly).
26
- # If you have gnupg 2.1, we strongly suggest to use a hkps-keyserver:
27
- #keyserver: hkps://hkps.pool.sks-keyservers.net
28
- # If you have gnupg 2.1 and TOR running locally, use a onion-keyserver:
29
- #keyserver: hkp://jirk5u4osbsr34t5.onion
30
- # If you have an OS-wide defined keyserver, specify a blank value to have that
31
- # one used:
32
- #keyserver:
33
- # The default works for all supported versions of gnupg:
34
- keyserver: pool.sks-keyservers.net
24
+ # Which verifying keyserver to fetch keys from?
25
+ # This server must support the VKS API.
26
+ # To disable lookup via this type of keyserver, set this to a blank value.
27
+ # Note: This must include the procotol scheme (e.g. "https://").
28
+ vks_keyserver: https://keys.openpgp.org
29
+
30
+ # Which traditional keyserver to fetch keys from?
31
+ # This server must support the SKS API.
32
+ # Please consider that public SKS servers don't verify any upload, the keys
33
+ # they deliver should not be trusted without additional verification. This is
34
+ # important if you e.g. allow keys to be fetched automatically, or by email
35
+ # address.
36
+ # This keyserver is queried only if the vks_keyserver didn't have a key.
37
+ # Note: This must include the procotol scheme (e.g. "https://").
38
+ # Beware: Only specify https here if the server does use a commonly accepted
39
+ # TLS certificate. Most servers of the SKS-pool do not!
40
+ sks_keyserver:
41
+
42
+ # Should Schleuder use a proxy for HTTP requests (to fetch OpenPGP keys)?
43
+ # To route HTTP requests via Tor set up a local Tor daemon and enter e.g.
44
+ # socks5h://127.0.0.1:9050 (for a typical Tor service).
45
+ #http_proxy:
35
46
 
36
47
  # Who is maintaining the overall schleuder installation and should be
37
48
  # notified about severe problems with lists.
@@ -40,6 +51,13 @@ keyserver: pool.sks-keyservers.net
40
51
  # Is also used as an envelope sender of admin notifications.
41
52
  superadmin: root@localhost
42
53
 
54
+ # Umask with which to create directories and files. The default (0077) lets
55
+ # only the owners read them, no one else.
56
+ # Only change this in special cases and if you know what you are doing!
57
+ # (Be careful to retain the value type, it must not be quoted and it must start
58
+ # with a zero!)
59
+ umask: 0077
60
+
43
61
  # For these options see documentation for ActionMailer::smtp_settings, e.g. <http://api.rubyonrails.org/classes/ActionMailer/Base.html>.
44
62
  smtp_settings:
45
63
  address: localhost
data/lib/schleuder/cli.rb CHANGED
@@ -109,7 +109,8 @@ module Schleuder
109
109
  end
110
110
  end
111
111
 
112
- if ActiveRecord::SchemaMigration.table_exists?
112
+ # If the table for lists exists we assume the DB has been initialized before.
113
+ if List.table_exists?
113
114
  say shellexec("cd #{root_dir} && rake db:migrate")
114
115
  else
115
116
  say shellexec("cd #{root_dir} && rake db:init")
@@ -122,6 +123,18 @@ module Schleuder
122
123
  Schleuder::Cert.new.generate
123
124
  end
124
125
 
126
+ if Conf.umask == 0077
127
+ # The umask is set to the (new) default, let's have a look at the list-dirs.
128
+ list_dirs = Dir.glob("#{Conf.lists_dir}/*")
129
+ list_dirs.each do |list_dir|
130
+ perm_bits = File.stat(list_dir).mode.to_s(8)[-3..]
131
+ if perm_bits != '700'
132
+ say "WARNING: The directory '#{list_dir}' has permissions that do not match the umask as set in schleuder's config. You should probably fix that by running `chmod -R g-rwx #{list_dir}`."
133
+ end
134
+ end
135
+ end
136
+
137
+
125
138
  say "Schleuder has been set up. You can now create a new list using `schleuder-cli`.\nWe hope you enjoy!"
126
139
  permission_notice
127
140
  rescue => exc
@@ -10,12 +10,15 @@ module Schleuder
10
10
 
11
11
  DEFAULTS = {
12
12
  'lists_dir' => '/var/lib/schleuder/lists',
13
+ 'umask' => 0077,
13
14
  'listlogs_dir' => '/var/lib/schleuder/lists',
14
15
  'keyword_handlers_dir' => '/usr/local/lib/schleuder/keyword_handlers',
15
16
  'filters_dir' => '/usr/local/lib/schleuder/filters',
16
17
  'log_level' => 'warn',
17
18
  'superadmin' => 'root@localhost',
18
- 'keyserver' => 'hkp://pool.sks-keyservers.net',
19
+ 'vks_keyserver' => 'https://keys.openpgp.org',
20
+ 'sks_keyserver' => 'http://pool.sks-keyservers.net',
21
+ 'http_proxy' => '',
19
22
  'smtp_settings' => {
20
23
  'address' => 'localhost',
21
24
  'port' => 25,
@@ -48,10 +51,19 @@ module Schleuder
48
51
  @config ||= load_config(ENV['SCHLEUDER_CONFIG'])
49
52
  end
50
53
 
54
+ def reload!
55
+ @config = nil
56
+ config
57
+ end
58
+
51
59
  def self.lists_dir
52
60
  instance.config['lists_dir']
53
61
  end
54
62
 
63
+ def self.umask
64
+ instance.config['umask']
65
+ end
66
+
55
67
  def self.listlogs_dir
56
68
  instance.config['listlogs_dir']
57
69
  end
@@ -115,8 +127,16 @@ module Schleuder
115
127
  settings
116
128
  end
117
129
 
118
- def self.keyserver
119
- instance.config['keyserver']
130
+ def self.vks_keyserver
131
+ instance.config['vks_keyserver']
132
+ end
133
+
134
+ def self.sks_keyserver
135
+ instance.config['sks_keyserver']
136
+ end
137
+
138
+ def self.http_proxy
139
+ instance.config['http_proxy']
120
140
  end
121
141
 
122
142
  private
@@ -0,0 +1,91 @@
1
+ module Schleuder
2
+ class EmailKeyImporter
3
+ class << self
4
+ def import_from_attachments(list, mail)
5
+ # Shouldn't happen, but who knows...
6
+ return if ! mail.from.first.match(Conf::EMAIL_REGEXP)
7
+ mail.attachments.map do |part|
8
+ if part.content_type == 'application/pgp-keys'
9
+ filter_and_maybe_import_keys(mail, part.body.decoded)
10
+ end
11
+ end.compact
12
+ end
13
+
14
+ def import_from_autocrypt_header(list, mail)
15
+ # Shouldn't happen, but who knows...
16
+ return if ! mail.from.first.match(Conf::EMAIL_REGEXP)
17
+ return if mail.header['Autocrypt'].blank?
18
+ keydata_base64 = mail.header['Autocrypt'].to_s.split('keydata=', 2)[1]
19
+ return if keydata_base64.blank?
20
+ keydata = Base64.decode64(keydata_base64)
21
+ return if keydata.blank?
22
+ filter_and_maybe_import_keys(mail, keydata)
23
+ end
24
+
25
+ def filter_and_maybe_import_keys(mail, keydata)
26
+ extracted_keys = GPGME::KeyExtractor.extract_by_email_address(mail.from.first, keydata)
27
+ extracted_keys.map do |fingerprint, filtered_keydata|
28
+ maybe_import_key(mail, fingerprint, filtered_keydata)
29
+ end.compact
30
+ end
31
+
32
+ def maybe_import_key(mail, fingerprint, filtered_keydata)
33
+ if mail.list.keys(fingerprint).size == 1
34
+ return update_key(mail, filtered_keydata)
35
+ end
36
+
37
+ if mail.list.keys(mail.from.first).size > 0
38
+ mail.add_pseudoheader('Note', I18n.t('email_key_importer.key_already_present'))
39
+ return
40
+ end
41
+ add_key(mail, filtered_keydata)
42
+ end
43
+
44
+ def add_key(mail, keydata)
45
+ fingerprint, import_states = import_to_list(mail, keydata)
46
+ return if ! fingerprint
47
+ if ! import_states.include?(I18n.t('import_states.new_key'))
48
+ mail.list.logger.error "Importing key failed! Fingerprint: #{fingerprint.inspect} -- Import-states: #{import_states.inspect}"
49
+ mail.add_pseudoheader('Note',
50
+ I18n.t('email_key_importer.import_error',
51
+ fingerprint: fingerprint.inspect,
52
+ import_states: import_states.inspect))
53
+ return
54
+ end
55
+ key = mail.list.keys(fingerprint).first
56
+ mail.add_pseudoheader('Note', I18n.t('email_key_importer.key_added',
57
+ key_summary: key.summary))
58
+ fingerprint
59
+ end
60
+
61
+ def update_key(mail, keydata)
62
+ fingerprint, import_states = import_to_list(mail, keydata)
63
+ return if ! fingerprint
64
+ key = mail.list.keys(fingerprint).first
65
+ if import_states == ['unchanged']
66
+ mail.add_pseudoheader('Note',
67
+ I18n.t('email_key_importer.key_unchanged',
68
+ key_summary: key.summary))
69
+ return
70
+ end
71
+
72
+ key = mail.list.keys(fingerprint).first
73
+ mail.add_pseudoheader('Note', I18n.t('email_key_importer.key_updated',
74
+ key_summary: key.summary))
75
+ fingerprint
76
+ end
77
+
78
+ def import_to_list(mail, keydata)
79
+ result = mail.list.gpg.import_filtered(keydata)
80
+ # At this point we expect the keydata to only contain one key, and thus
81
+ # only one key and value in the result.
82
+ if ! result.is_a?(Hash) || result.keys.size != 1 || ! result.values.first.is_a?(Array)
83
+ mail.list.logger.error "Unexpected result when importing key from temporary keyring => #{result.inspect}"
84
+ mail.add_pseudoheader('Note', I18n.t('email_key_importer.technical_error'))
85
+ return false
86
+ end
87
+ [result.keys.first, result.values.first]
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,21 @@
1
+ module Schleuder
2
+ module Filters
3
+ def self.key_auto_import_from_attachments(list, mail)
4
+ # Don't run if not enabled.
5
+ return if ! list.key_auto_import_from_email
6
+
7
+ imported_fingerprints = EmailKeyImporter.import_from_attachments(list, mail)
8
+ if imported_fingerprints.size > 0
9
+ # If the message's signature could not be validated before, re-run the
10
+ # validation, because after having imported new or updated keys the
11
+ # validation now might work.
12
+ if mail.signature.present? && ! mail.signature.valid?
13
+ # Re-validate the signature validation, now that a new key was
14
+ # imported that might be the previously unknown signing key.
15
+ mail.repeat_validation!
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+
@@ -1,7 +1,7 @@
1
1
  module Schleuder
2
2
  module Filters
3
3
  def self.receive_from_subscribed_emailaddresses_only(list, mail)
4
- if list.receive_from_subscribed_emailaddresses_only? && list.subscriptions.where(email: mail.from.first).blank?
4
+ if list.receive_from_subscribed_emailaddresses_only? && list.subscriptions.where(email: mail.from.first.downcase).blank?
5
5
  list.logger.info 'Rejecting mail as not from subscribed address.'
6
6
  return Errors::MessageSenderNotSubscribed.new
7
7
  end
@@ -1,17 +1,49 @@
1
1
  module Schleuder
2
2
  module Filters
3
+
4
+ # If keywords are present, recurse into arbitrary levels of multipart/mixed
5
+ # encapsulation. If multipart/alternative is found, remove all sub-parts
6
+ # but the text/plain part (assuming that every multipart/alternative
7
+ # contains exactly one text/plain). Change the content_type from
8
+ # mutlipart/alternative to mutlipart/mixed.
3
9
  def self.strip_html_from_alternative_if_keywords_present(list, mail)
4
- if mail[:content_type].blank? ||
5
- mail[:content_type].content_type != 'multipart/alternative' ||
6
- mail.keywords.blank?
10
+ # Only strip the text/html-part if keywords are present
11
+ if mail.keywords.blank? then return false end
12
+ return self.recursively_strip_html_from_alternative_if_keywords_present(list, mail)
13
+ end
14
+
15
+ def self.recursively_strip_html_from_alternative_if_keywords_present(list, mail)
16
+ if mail[:content_type].blank? then return false end
17
+ content_type = mail[:content_type].content_type
18
+
19
+ # The multipart/alternative could hide inside an arbitrary number of
20
+ # levels of multipart/mixed encapsulation.
21
+ # see also: https://www.rfc-editor.org/rfc/rfc2046#section-5.1.3
22
+ if content_type == 'multipart/mixed'
23
+ mail.parts.each do |part|
24
+ self.recursively_strip_html_from_alternative_if_keywords_present(list, part)
25
+ end
7
26
  return false
8
27
  end
9
28
 
29
+ # inside the mutlipart/mixed, we only care about multipart/mixed and
30
+ # mutlipart/alternative
31
+ if content_type != 'multipart/alternative' then return false end
32
+
33
+ # Inside multipart/alternative, there could be a text/html-part, or there
34
+ # could be a multipart/related-part which contains the text/html-part.
35
+ # Everything inside the multipart/alternative that is not text/plain
36
+ # should be deleted, since it will contain keywords and we only strip
37
+ # keywords from text/plain-parts.
10
38
  Schleuder.logger.debug 'Stripping html-part from multipart/alternative-message because it contains keywords'
11
39
  mail.parts.delete_if do |part|
12
- part[:content_type].content_type == 'text/html'
40
+ content_type = part[:content_type].content_type
41
+ content_type != 'text/plain'
13
42
  end
43
+
44
+ # NOTE: We could instead unencapsulate it.
14
45
  mail.content_type = 'multipart/mixed'
46
+
15
47
  mail.add_pseudoheader(:note, I18n.t('pseudoheaders.stripped_html_from_multialt_with_keywords'))
16
48
  end
17
49
  end
@@ -0,0 +1,9 @@
1
+ module Schleuder
2
+ module Filters
3
+ def self.key_auto_import_from_autocrypt_header(list, mail)
4
+ if list.key_auto_import_from_email
5
+ EmailKeyImporter.import_from_autocrypt_header(list, mail)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -7,6 +7,9 @@ module GPGME
7
7
  'new_subkeys' => 8
8
8
  }
9
9
 
10
+ # This differs from import_filtered() in that it doesn't filter the keys at
11
+ # all, and that it returns the import-results themselves, not strings based
12
+ # on those results.
10
13
  def keyimport(keydata)
11
14
  self.import_keys(GPGME::Data.new(keydata))
12
15
  result = self.import_result
@@ -33,13 +36,11 @@ module GPGME
33
36
  end
34
37
 
35
38
  def find_keys(input=nil, secret_only=nil)
36
- _, input = clean_and_classify_input(input)
37
- keys(input, secret_only)
39
+ keys(normalize_key_identifier(input), secret_only)
38
40
  end
39
41
 
40
42
  def find_distinct_key(input=nil, secret_only=nil)
41
- _, input = clean_and_classify_input(input)
42
- keys = keys(input, secret_only)
43
+ keys = keys(normalize_key_identifier(input), secret_only)
43
44
  if keys.size == 1
44
45
  keys.first
45
46
  else
@@ -47,16 +48,16 @@ module GPGME
47
48
  end
48
49
  end
49
50
 
50
- def clean_and_classify_input(input)
51
+ def normalize_key_identifier(input)
51
52
  case input
52
53
  when /.*?([^ <>]+@[^ <>]+).*?/
53
- [:email, "<#{$1}>"]
54
+ "<#{$1}>"
54
55
  when /^http/
55
- [:url, input]
56
+ input
56
57
  when Conf::FINGERPRINT_REGEXP
57
- [:fingerprint, "0x#{input.gsub(/^0x/, '')}"]
58
+ "0x#{input.gsub(/^0x/, '')}"
58
59
  else
59
- [nil, input]
60
+ input
60
61
  end
61
62
  end
62
63
 
@@ -88,85 +89,23 @@ module GPGME
88
89
  GPGME::Engine.info.find {|e| e.protocol == GPGME::PROTOCOL_OpenPGP }
89
90
  end
90
91
 
91
- def refresh_keys(keys)
92
- # reorder keys so the update pattern is random
93
- output = keys.shuffle.map do |key|
94
- # Sleep a short while to make traffic analysis less easy.
95
- sleep rand(1.0..5.0)
96
- refresh_key(key.fingerprint).presence
97
- end
98
- `gpgconf --kill dirmngr`
99
- output.compact.join("\n")
100
- end
101
-
102
- def refresh_key(fingerprint)
103
- args = "#{keyserver_arg} #{import_filter_arg} --refresh-keys #{fingerprint}"
104
- gpgerr, gpgout, exitcode = self.class.gpgcli(args)
105
-
106
- if exitcode > 0
107
- # Return filtered error messages. Include gpgkeys-messages from stdout
108
- # (gpg 2.0 does that), which could e.g. report a failure to connect to
109
- # the keyserver.
110
- # TODO: Revisit this once we don't do network access via GPG
111
- # anymore.
112
- res = [
113
- refresh_key_filter_messages(gpgerr),
114
- refresh_key_filter_messages(gpgout).grep(/^gpgkeys: /)
115
- ].flatten.compact
116
- # if there was an error that we don't filter out,
117
- # we better kill dirmngr, so it hopefully won't suffer
118
- # from the same error during the next run.
119
- # See #309 for background
120
- if !res.empty?
121
- `gpgconf --kill dirmngr`
122
- end
123
- res.join("\n")
124
- else
125
- lines = translate_output('key_updated', gpgout).reject do |line|
126
- # Reduce the noise a little.
127
- line.match(/.* \(unchanged\):$/)
92
+ def import_filtered(input, gpg_extra_arg='')
93
+ # Import through gpgcli so we can use import-filter. GPGME still does
94
+ # not provide that feature (as of summer 2023): <https://dev.gnupg.org/T4721> :(
95
+ gpgerr, gpgout, exitcode = self.class.gpgcli("#{import_filter_arg} #{gpg_extra_arg} --import") do |stdin, stdout, stderr|
96
+ # Wrap this into a block because gpg breaks the pipe if it encounters invalid data.
97
+ begin
98
+ stdin.print input
99
+ rescue Errno::EPIPE
128
100
  end
129
- lines.join("\n")
101
+ stdin.close
102
+ stdout.readlines
130
103
  end
131
- end
132
-
133
- def fetch_key(input)
134
- arguments, error = fetch_key_gpg_arguments_for(input)
135
- return error if error
136
-
137
- gpgerr, gpgout, exitcode = self.class.gpgcli("#{import_filter_arg} #{arguments}")
138
-
139
- # Unfortunately gpg doesn't exit with code > 0 if `--fetch-key` fails.
140
- if exitcode > 0 || gpgerr.grep(/ unable to fetch /).presence
141
- "Fetching #{input} did not succeed:\n#{gpgerr.join("\n")}"
142
- else
143
- translate_output('key_fetched', gpgout).join("\n")
144
- end
145
- end
146
-
147
- def fetch_key_gpg_arguments_for(input)
148
- case input
149
- when Conf::FINGERPRINT_REGEXP
150
- "#{keyserver_arg} --recv-key #{input}"
151
- when /^http/
152
- "--fetch-key #{input}"
153
- when /@/
154
- # --recv-key doesn't work with email-addresses, so we use --locate-key
155
- # restricted to keyservers.
156
- "#{keyserver_arg} --auto-key-locate keyserver --locate-key #{input}"
104
+ if exitcode > 0
105
+ RuntimeError.new(gpgerr.join("\n"))
157
106
  else
158
- [nil, I18n.t('fetch_key.invalid_input')]
159
- end
160
- end
161
-
162
- def translate_output(locale_key, gpgoutput)
163
- import_states = translate_import_data(gpgoutput)
164
- strings = import_states.map do |fingerprint, states|
165
- key = find_distinct_key(fingerprint)
166
- I18n.t(locale_key, key_summary: key.summary,
167
- states: states.to_sentence)
107
+ translate_import_data(gpgout)
168
108
  end
169
- strings
170
109
  end
171
110
 
172
111
  def translate_import_data(gpgoutput)
@@ -210,7 +149,7 @@ module GPGME
210
149
  errors = []
211
150
  output = []
212
151
  base_cmd = gpg_engine.file_name
213
- base_args = '--no-greeting --no-permission-warning --quiet --armor --trust-model always --no-tty --command-fd 0 --status-fd 1'
152
+ base_args = '--no-greeting --quiet --armor --trust-model always --no-tty --command-fd 0 --status-fd 1'
214
153
  cmd = [base_cmd, base_args, args].flatten.join(' ')
215
154
  Open3.popen3(cmd) do |stdin, stdout, stderr, thread|
216
155
  if block_given?
@@ -223,19 +162,21 @@ module GPGME
223
162
  exitcode = thread.value.exitstatus
224
163
  end
225
164
 
165
+ # Don't treat warnings as errors but log them.
166
+ errors = errors.map do |line|
167
+ if line.match?(/gpg: WARNING: (unsafe permissions on homedir|using insecure memory)/i)
168
+ Schleuder.logger.warn(line)
169
+ nil
170
+ else
171
+ line
172
+ end
173
+ end.compact
174
+
226
175
  [errors, output, exitcode]
227
176
  rescue Errno::ENOENT
228
177
  raise 'Need gpg in $PATH or in $GPGBIN'
229
178
  end
230
179
 
231
- def keyserver_arg
232
- if Conf.keyserver.present?
233
- "--keyserver #{Conf.keyserver}"
234
- else
235
- ''
236
- end
237
- end
238
-
239
180
  def import_filter_arg
240
181
  %{ --import-filter drop-sig='sig_created_d > 0000-00-00'}
241
182
  end
@@ -82,7 +82,7 @@ module GPGME
82
82
  end
83
83
 
84
84
  def self.valid_fingerprint?(fp)
85
- fp =~ Schleuder::Conf::FINGERPRINT_REGEXP
85
+ fp.present? && fp.match?(Schleuder::Conf::FINGERPRINT_REGEXP)
86
86
  end
87
87
  end
88
88
  end