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