schleuder 3.2.2 → 3.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +20 -10
  3. data/Rakefile +16 -8
  4. data/bin/schleuder +2 -1
  5. data/bin/schleuder-api-daemon +3 -2
  6. data/db/migrate/20180110203100_add_sig_enc_to_headers_to_meta_defaults.rb +30 -0
  7. data/db/schema.rb +2 -2
  8. data/etc/list-defaults.yml +4 -2
  9. data/etc/schleuder.yml +11 -0
  10. data/lib/schleuder-api-daemon.rb +9 -354
  11. data/lib/schleuder-api-daemon/helpers/schleuder-api-daemon-helper.rb +143 -0
  12. data/lib/schleuder-api-daemon/routes/key.rb +40 -0
  13. data/lib/schleuder-api-daemon/routes/list.rb +69 -0
  14. data/lib/schleuder-api-daemon/routes/status.rb +5 -0
  15. data/lib/schleuder-api-daemon/routes/subscription.rb +99 -0
  16. data/lib/schleuder-api-daemon/routes/version.rb +5 -0
  17. data/lib/schleuder.rb +2 -3
  18. data/lib/schleuder/cli.rb +24 -0
  19. data/lib/schleuder/cli/subcommand_fix.rb +1 -1
  20. data/lib/schleuder/conf.rb +7 -1
  21. data/lib/schleuder/errors/active_model_error.rb +2 -5
  22. data/lib/schleuder/errors/decryption_failed.rb +2 -7
  23. data/lib/schleuder/errors/key_adduid_failed.rb +1 -5
  24. data/lib/schleuder/errors/key_generation_failed.rb +1 -8
  25. data/lib/schleuder/errors/keyword_admin_only.rb +1 -5
  26. data/lib/schleuder/errors/list_not_found.rb +1 -5
  27. data/lib/schleuder/errors/listdir_problem.rb +2 -7
  28. data/lib/schleuder/errors/loading_list_settings_failed.rb +2 -5
  29. data/lib/schleuder/errors/message_empty.rb +1 -5
  30. data/lib/schleuder/errors/message_not_from_admin.rb +2 -5
  31. data/lib/schleuder/errors/message_sender_not_subscribed.rb +2 -5
  32. data/lib/schleuder/errors/message_too_big.rb +2 -5
  33. data/lib/schleuder/errors/message_unauthenticated.rb +1 -4
  34. data/lib/schleuder/errors/message_unencrypted.rb +2 -5
  35. data/lib/schleuder/errors/message_unsigned.rb +2 -5
  36. data/lib/schleuder/errors/too_many_keys.rb +1 -8
  37. data/lib/schleuder/filters/{request_filter.rb → post_decryption/10_request.rb} +0 -0
  38. data/lib/schleuder/filters/{max_message_size.rb → post_decryption/20_max_message_size.rb} +0 -0
  39. data/lib/schleuder/filters/{forward_filter.rb → post_decryption/30_forward_to_owner.rb} +0 -0
  40. data/lib/schleuder/filters/post_decryption/40_receive_admin_only.rb +10 -0
  41. data/lib/schleuder/filters/post_decryption/50_receive_authenticated_only.rb +10 -0
  42. data/lib/schleuder/filters/post_decryption/60_receive_signed_only.rb +10 -0
  43. data/lib/schleuder/filters/post_decryption/70_receive_encrypted_only.rb +10 -0
  44. data/lib/schleuder/filters/post_decryption/80_receive_from_subscribed_emailaddresses_only.rb +10 -0
  45. data/lib/schleuder/filters/{bounces_filter.rb → pre_decryption/10_forward_bounce_to_admins.rb} +0 -0
  46. data/lib/schleuder/filters/{forward_incoming.rb → pre_decryption/20_forward_all_incoming_to_admins.rb} +0 -0
  47. data/lib/schleuder/filters/{send_key_filter.rb → pre_decryption/30_send_key.rb} +0 -0
  48. data/lib/schleuder/filters/{hotmail_message_filter.rb → pre_decryption/40_fix_exchange_messages.rb} +5 -3
  49. data/lib/schleuder/filters/{strip_alternative_filter.rb → pre_decryption/50_strip_html_from_alternative.rb} +1 -1
  50. data/lib/schleuder/filters_runner.rb +41 -31
  51. data/lib/schleuder/gpgme/ctx.rb +1 -1
  52. data/lib/schleuder/gpgme/import_status.rb +13 -7
  53. data/lib/schleuder/gpgme/key.rb +4 -0
  54. data/lib/schleuder/list.rb +7 -4
  55. data/lib/schleuder/mail/encrypted_part.rb +14 -0
  56. data/lib/schleuder/mail/gpg.rb +15 -0
  57. data/lib/schleuder/mail/message.rb +70 -30
  58. data/lib/schleuder/plugins/key_management.rb +32 -7
  59. data/lib/schleuder/plugins/subscription_management.rb +70 -3
  60. data/lib/schleuder/runner.rb +19 -8
  61. data/lib/schleuder/subscription.rb +5 -9
  62. data/lib/schleuder/validators/fingerprint_validator.rb +1 -1
  63. data/lib/schleuder/version.rb +1 -1
  64. data/locales/de.yml +96 -8
  65. data/locales/en.yml +102 -10
  66. metadata +48 -27
  67. data/lib/schleuder/errors/file_not_found.rb +0 -14
  68. data/lib/schleuder/errors/invalid_listname.rb +0 -13
  69. data/lib/schleuder/errors/list_exists.rb +0 -13
  70. data/lib/schleuder/errors/unknown_list_option.rb +0 -14
  71. data/lib/schleuder/filters/auth_filter.rb +0 -39
@@ -19,7 +19,7 @@ module GPGME
19
19
  case import_result.imports.size
20
20
  when 1
21
21
  import_status = import_result.imports.first
22
- if import_status.action == 'not imported'
22
+ if import_status.action == 'error'
23
23
  [nil, "Key #{import_status.fpr} could not be imported!"]
24
24
  else
25
25
  [import_status.fpr, nil]
@@ -2,17 +2,23 @@ module GPGME
2
2
  class ImportStatus
3
3
  attr_reader :action
4
4
 
5
- # Unfortunately in initialize() @status and @result are not yet intialized.
5
+ # Unfortunately in initialize() @status and @result are not yet initialized.
6
6
  def set_action
7
- @action ||= if self.status > 0
8
- 'imported'
9
- elsif self.result == 0
10
- 'unchanged'
11
- else
7
+ @action ||= if self.result > 0
12
8
  # An error happened.
13
9
  # TODO: Give details by going through the list of errors in
14
10
  # "gpg-errors.h" and find out which is present here.
15
- 'not imported'
11
+ 'error'
12
+ else
13
+ # TODO: refactor with Ctx#translate_import_data
14
+ case self.status
15
+ when 0
16
+ 'unchanged'
17
+ when IMPORT_NEW
18
+ 'imported'
19
+ else
20
+ 'updated'
21
+ end
16
22
  end
17
23
  self
18
24
  end
@@ -208,5 +208,9 @@ module GPGME
208
208
  pinentry = File.join(ENV['SCHLEUDER_ROOT'], 'bin', 'pinentry-clearpassphrase')
209
209
  GPGME::Ctx.spawn_daemon('gpg-agent', "--use-standard-socket --pinentry-program #{pinentry}")
210
210
  end
211
+
212
+ def self.valid_fingerprint?(fp)
213
+ fp =~ Schleuder::Conf::FINGERPRINT_REGEXP
214
+ end
211
215
  end
212
216
  end
@@ -124,7 +124,7 @@ module Schleuder
124
124
 
125
125
  def import_key_and_find_fingerprint(key_material)
126
126
  return nil if key_material.blank?
127
-
127
+
128
128
  import_result = import_key(key_material)
129
129
  gpg.interpret_import_result(import_result)
130
130
  end
@@ -251,9 +251,8 @@ module Schleuder
251
251
  end
252
252
 
253
253
  def fingerprint=(arg)
254
- # Strip whitespace from incoming arg.
255
254
  if arg
256
- write_attribute(:fingerprint, arg.gsub(/\s*/, '').chomp)
255
+ write_attribute(:fingerprint, arg.gsub(/\s*/, '').gsub(/^0x/, '').chomp.upcase)
257
256
  end
258
257
  end
259
258
 
@@ -346,7 +345,11 @@ module Schleuder
346
345
  mail.add_internal_footer!
347
346
  self.subscriptions.each do |subscription|
348
347
  begin
349
- subscription.send_mail(mail)
348
+ if subscription.delivery_enabled
349
+ subscription.send_mail(mail)
350
+ else
351
+ logger.info "Not sending to #{subscription.email}: delivery is disabled."
352
+ end
350
353
  rescue => exc
351
354
  msg = I18n.t('errors.delivery_error',
352
355
  { email: subscription.email, error: exc.to_s })
@@ -0,0 +1,14 @@
1
+ module Mail
2
+ module Gpg
3
+ class EncryptedPart < Mail::Part
4
+ alias_method :initialize_mailgpg, :initialize
5
+
6
+ def initialize(cleartext_mail, options = {})
7
+ if cleartext_mail.protected_headers_subject
8
+ cleartext_mail.content_type_parameters['protected-headers'] = 'v1'
9
+ end
10
+ initialize_mailgpg(cleartext_mail, options)
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,15 @@
1
+ module Mail
2
+ module Gpg
3
+ class << self
4
+ alias_method :encrypt_mailgpg, :encrypt
5
+
6
+ def encrypt(cleartext_mail, options={})
7
+ encrypted_mail = encrypt_mailgpg(cleartext_mail, options)
8
+ if cleartext_mail.protected_headers_subject
9
+ encrypted_mail.subject = cleartext_mail.protected_headers_subject
10
+ end
11
+ encrypted_mail
12
+ end
13
+ end
14
+ end
15
+ end
@@ -16,6 +16,7 @@ module Mail
16
16
  attr_accessor :recipient
17
17
  attr_accessor :original_message
18
18
  attr_accessor :list
19
+ attr_accessor :protected_headers_subject
19
20
 
20
21
  # TODO: This should be in initialize(), but I couldn't understand the
21
22
  # strange errors about wrong number of arguments when overriding
@@ -23,12 +24,9 @@ module Mail
23
24
  def setup
24
25
  if self.encrypted?
25
26
  new = self.decrypt(verify: true)
26
- ## Work around a bug in mail-gpg: when decrypting pgp/mime the
27
- ## Date-header is not copied.
28
- #new.date ||= self.date
29
27
  # Test if there's a signed multipart inside the ciphertext
30
28
  # ("encapsulated" format of pgp/mime).
31
- if new.signed?
29
+ if encapsulated_signed?(new)
32
30
  new = new.verify
33
31
  end
34
32
  elsif self.signed?
@@ -49,6 +47,20 @@ module Mail
49
47
  self.dynamic_pseudoheaders.each do |str|
50
48
  new.add_pseudoheader(str)
51
49
  end
50
+
51
+ # Store previously protected subject for later access.
52
+ # mail-gpg pulls headers from the decrypted mime parts "up" into the main
53
+ # headers, which reveals protected subjects.
54
+ if self.subject != new.subject
55
+ new.protected_headers_subject = self.subject.dup
56
+
57
+ # Delete the protected headers which might leak information.
58
+ if new.parts.first.content_type == "text/rfc822-headers; protected-headers=v1"
59
+ new.parts.shift
60
+ end
61
+ end
62
+
63
+
52
64
  new
53
65
  end
54
66
 
@@ -58,6 +70,7 @@ module Mail
58
70
  clean.gpg self.list.gpg_sign_options
59
71
  clean.from = list.email
60
72
  clean.subject = self.subject
73
+ clean.protected_headers_subject = self.protected_headers_subject
61
74
 
62
75
  clean.add_msgids(list, self)
63
76
  clean.add_list_headers(list)
@@ -69,6 +82,13 @@ module Mail
69
82
  clean.add_part new_part
70
83
  end
71
84
 
85
+ if self.protected_headers_subject.present?
86
+ new_part = Mail::Part.new
87
+ new_part.content_type = "text/rfc822-headers; protected-headers=v1"
88
+ new_part.body = "Subject: #{self.subject}\n"
89
+ clean.add_part new_part
90
+ end
91
+
72
92
  # Attach body or mime-parts in a new wrapper-part, to preserve the
73
93
  # original mime-structure.
74
94
  # We can't use self.to_s here — that includes all the headers we *don't*
@@ -203,15 +223,19 @@ module Mail
203
223
  end
204
224
 
205
225
  @keywords = []
226
+ look_for_keywords = true
206
227
  lines = part.decoded.lines.map do |line|
207
228
  # TODO: Find multiline arguments (add-key). Currently add-key has to
208
229
  # read the whole body and hope for the best.
209
- if line.match(/^x-([^:\s]*)[:\s]*(.*)/i)
210
- command = $1.strip.downcase
211
- arguments = $2.to_s.strip.downcase.split(/[,; ]{1,}/)
230
+ if look_for_keywords && (m = line.match(/^x-([^:\s]*)[:\s]*(.*)/i))
231
+ command = m[1].strip.downcase
232
+ arguments = m[2].to_s.strip.downcase.split(/[,; ]{1,}/)
212
233
  @keywords << [command, arguments]
213
234
  nil
214
235
  else
236
+ if look_for_keywords && line.match(/\S+/i)
237
+ look_for_keywords = false
238
+ end
215
239
  line
216
240
  end
217
241
  end
@@ -259,17 +283,7 @@ module Mail
259
283
  @dynamic_pseudoheaders || []
260
284
  end
261
285
 
262
- def standard_pseudoheaders(list)
263
- if @standard_pseudoheaders.present?
264
- return @standard_pseudoheaders
265
- else
266
- @standard_pseudoheaders = []
267
- end
268
-
269
- Array(list.headers_to_meta).each do |field|
270
- @standard_pseudoheaders << make_pseudoheader(field.to_s, self.header[field.to_s])
271
- end
272
-
286
+ def signature_state
273
287
  # Careful to add information about the incoming signature. GPGME
274
288
  # throws exceptions if it doesn't know the key.
275
289
  if self.signature.present?
@@ -277,22 +291,41 @@ module Mail
277
291
  # for that manually and provide our own fallback. (Calling
278
292
  # `signature.key` results in an EOFError in that case.)
279
293
  if signing_key.present?
280
- msg = signature.to_s
294
+ signature_state = signature.to_s
281
295
  else
282
- # TODO: I18n
283
- msg = "Unknown signature by unknown key 0x#{self.signature.fingerprint}"
296
+ signature_state = I18n.t("signature_states.unknown", fingerprint: self.signature.fingerprint)
284
297
  end
285
298
  else
286
- # TODO: I18n
287
- msg = "Unsigned"
299
+ signature_state = I18n.t("signature_states.unsigned")
300
+ end
301
+ signature_state
302
+ end
303
+
304
+ def encryption_state
305
+ if was_encrypted?
306
+ encryption_state = I18n.t("encryption_states.encrypted")
307
+ else
308
+ encryption_state = I18n.t("encryption_states.unencrypted")
309
+ end
310
+ encryption_state
311
+ end
312
+
313
+ def standard_pseudoheaders(list)
314
+ if @standard_pseudoheaders.present?
315
+ return @standard_pseudoheaders
316
+ else
317
+ @standard_pseudoheaders = []
318
+ end
319
+
320
+ Array(list.headers_to_meta).each do |field|
321
+ value = case field.to_s
322
+ when 'sig' then signature_state
323
+ when 'enc' then encryption_state
324
+ else self.header[field.to_s]
325
+ end
326
+ @standard_pseudoheaders << make_pseudoheader(field.to_s, value)
288
327
  end
289
- @standard_pseudoheaders << make_pseudoheader(:sig, msg)
290
328
 
291
- # TODO: I18n
292
- @standard_pseudoheaders << make_pseudoheader(
293
- :enc,
294
- was_encrypted? ? 'Encrypted' : 'Unencrypted'
295
- )
296
329
 
297
330
  @standard_pseudoheaders
298
331
  end
@@ -316,7 +349,7 @@ module Mail
316
349
  if list.include_list_headers
317
350
  self['List-Id'] = "<#{list.email.gsub('@', '.')}>"
318
351
  self['List-Owner'] = "<mailto:#{list.owner_address}> (Use list's public key)"
319
- self['List-Help'] = '<https://schleuder.nadir.org/>'
352
+ self['List-Help'] = '<https://schleuder.org/>'
320
353
 
321
354
  postmsg = if list.receive_admin_only
322
355
  "NO (Admins only)"
@@ -410,6 +443,13 @@ module Mail
410
443
 
411
444
  private
412
445
 
446
+ # mail.signed? throws an error if it finds
447
+ # pgp boundaries, so we must use the Mail::Gpg
448
+ # methods.
449
+ def encapsulated_signed?(mail)
450
+ (mail.verify_result.nil? || mail.verify_result.signatures.empty?) && \
451
+ (Mail::Gpg.signed_mime?(mail) || Mail::Gpg.signed_inline?(mail))
452
+ end
413
453
 
414
454
  def add_footer!(footer_attribute)
415
455
  if self.list.blank? || self.list.send(footer_attribute).to_s.empty?
@@ -1,7 +1,6 @@
1
1
  module Schleuder
2
2
  module RequestPlugins
3
3
  def self.add_key(arguments, list, mail)
4
- out = [I18n.t('plugins.key_management.import_result')]
5
4
 
6
5
  if mail.has_attachments?
7
6
  results = self.import_keys_from_attachments(list, mail)
@@ -9,15 +8,35 @@ module Schleuder
9
8
  results = [self.import_key_from_body(list, mail)]
10
9
  end
11
10
 
12
- out << results.compact.collect(&:imports).flatten.map do |import_status|
13
- str = I18n.t("plugins.key_management.key_import_status.#{import_status.action}")
14
- "#{import_status.fpr}: #{str}"
11
+ import_stati = results.compact.collect(&:imports).flatten
12
+
13
+ if import_stati.blank?
14
+ return I18n.t('plugins.key_management.no_imports')
15
15
  end
16
16
 
17
- out.join("\n")
17
+ out = []
18
+
19
+ import_stati.each do |import_status|
20
+ if import_status.action == 'error'
21
+ out << I18n.t("plugins.key_management.key_import_status.error", fingerprint: import_status.fingerprint)
22
+ else
23
+ key = list.gpg.find_distinct_key(import_status.fingerprint)
24
+ if key
25
+ out << I18n.t("plugins.key_management.key_import_status.#{import_status.action}", key_oneline: key.oneline)
26
+ end
27
+ end
28
+ end
29
+
30
+ out.join("\n\n")
18
31
  end
19
32
 
20
33
  def self.delete_key(arguments, list, mail)
34
+ if arguments.blank?
35
+ return I18n.t(
36
+ "plugins.key_management.delete_key_requires_arguments"
37
+ )
38
+ end
39
+
21
40
  arguments.map do |argument|
22
41
  keys = list.keys(argument)
23
42
  case keys.size
@@ -26,9 +45,9 @@ module Schleuder
26
45
  when 1
27
46
  begin
28
47
  keys.first.delete!
29
- I18n.t('plugins.key_management.deleted', key_string: keys.first.fingerprint)
48
+ I18n.t('plugins.key_management.deleted', key_string: keys.first.oneline)
30
49
  rescue GPGME::Error::Conflict
31
- I18n.t('plugins.key_management.not_deletable', key_string: keys.first.fingerprint)
50
+ I18n.t('plugins.key_management.not_deletable', key_string: keys.first.oneline)
32
51
  end
33
52
  else
34
53
  I18n.t('errors.too_many_matching_keys', {
@@ -71,6 +90,12 @@ module Schleuder
71
90
  end
72
91
 
73
92
  def self.fetch_key(arguments, list, mail)
93
+ if arguments.blank?
94
+ return I18n.t(
95
+ "plugins.key_management.fetch_key_requires_arguments"
96
+ )
97
+ end
98
+
74
99
  arguments.map do |argument|
75
100
  list.fetch_keys(argument)
76
101
  end
@@ -1,12 +1,18 @@
1
1
  module Schleuder
2
2
  module RequestPlugins
3
3
  def self.subscribe(arguments, list, mail)
4
+ if arguments.blank?
5
+ return I18n.t(
6
+ "plugins.subscription_management.subscribe_requires_arguments"
7
+ )
8
+ end
9
+
4
10
  email = arguments.shift
5
11
 
6
12
  if arguments.present?
7
13
  # Collect all arguments that look like fingerprint-material
8
14
  fingerprint = ''
9
- while arguments.first.present? && arguments.first.match(/\A(0x)?[a-f0-9]+/i)
15
+ while arguments.first.present? && arguments.first.match(/^(0x)?[a-f0-9]+$/i)
10
16
  fingerprint << arguments.shift
11
17
  end
12
18
  # Use possibly remaining args as flags.
@@ -37,6 +43,13 @@ module Schleuder
37
43
  # If no address was given we unsubscribe the sender.
38
44
  email = arguments.first.presence || mail.signer.email
39
45
 
46
+ # Refuse to unsubscribe the last admin.
47
+ if list.admins.size == 1 && list.admins.first.email == email
48
+ return I18n.t(
49
+ "plugins.subscription_management.cannot_unsubscribe_last_admin", email: email
50
+ )
51
+ end
52
+
40
53
  # TODO: May signers have multiple UIDs? We don't match those currently.
41
54
  if ! list.from_admin?(mail) && email != mail.signer.email
42
55
  # Only admins may unsubscribe others.
@@ -98,6 +111,12 @@ module Schleuder
98
111
  end
99
112
 
100
113
  def self.set_fingerprint(arguments, list, mail)
114
+ if arguments.blank?
115
+ return I18n.t(
116
+ "plugins.subscription_management.set_fingerprint_requires_arguments"
117
+ )
118
+ end
119
+
101
120
  if arguments.first.match(/@/)
102
121
  if arguments.first == mail.signer.email || list.from_admin?(mail)
103
122
  email = arguments.shift
@@ -118,8 +137,15 @@ module Schleuder
118
137
  )
119
138
  end
120
139
 
121
- sub.fingerprint = arguments.join
140
+ fingerprint = arguments.join
141
+ unless GPGME::Key.valid_fingerprint?(fingerprint)
142
+ return I18n.t(
143
+ "plugins.subscription_management.set_fingerprint_requires_valid_fingerprint",
144
+ fingerprint: fingerprint
145
+ )
146
+ end
122
147
 
148
+ sub.fingerprint = fingerprint
123
149
  if sub.save
124
150
  I18n.t(
125
151
  "plugins.subscription_management.fingerprint_set",
@@ -130,7 +156,48 @@ module Schleuder
130
156
  I18n.t(
131
157
  "plugins.subscription_management.setting_fingerprint_failed",
132
158
  email: email,
133
- fingerprint: arguments.last,
159
+ fingerprint: sub.fingerprint,
160
+ errors: sub.errors.to_a.join("\n")
161
+ )
162
+ end
163
+ end
164
+
165
+ def self.unset_fingerprint(arguments, list, mail)
166
+ if arguments.blank?
167
+ return I18n.t(
168
+ "plugins.subscription_management.unset_fingerprint_requires_arguments"
169
+ )
170
+ end
171
+
172
+ email = arguments.first
173
+ unless email == mail.signer.email || list.from_admin?(mail)
174
+ return I18n.t(
175
+ "plugins.subscription_management.unset_fingerprint_only_self"
176
+ )
177
+ end
178
+ if email == mail.signer.email && list.from_admin?(mail) && arguments.last != 'force'
179
+ return I18n.t(
180
+ "plugins.subscription_management.unset_fingerprint_requires_arguments"
181
+ )
182
+ end
183
+
184
+ sub = list.subscriptions.where(email: email).first
185
+ if sub.blank?
186
+ return I18n.t(
187
+ "plugins.subscription_management.is_not_subscribed", email: email
188
+ )
189
+ end
190
+
191
+ sub.fingerprint = ''
192
+ if sub.save
193
+ I18n.t(
194
+ "plugins.subscription_management.fingerprint_unset",
195
+ email: email
196
+ )
197
+ else
198
+ I18n.t(
199
+ "plugins.subscription_management.unsetting_fingerprint_failed",
200
+ email: email,
134
201
  errors: sub.errors.to_a.join("\n")
135
202
  )
136
203
  end