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.
- checksums.yaml +4 -4
- data/README.md +6 -9
- data/db/migrate/20180110203100_add_sig_enc_to_headers_to_meta_defaults.rb +1 -1
- data/db/migrate/20220910170110_add_key_auto_import_from_email.rb +11 -0
- data/db/schema.rb +6 -6
- data/etc/list-defaults.yml +16 -0
- data/etc/schleuder.yml +29 -11
- data/lib/schleuder/cli.rb +14 -1
- data/lib/schleuder/conf.rb +23 -3
- data/lib/schleuder/email_key_importer.rb +91 -0
- data/lib/schleuder/filters/post_decryption/35_key_auto_import_from_attachments.rb +21 -0
- data/lib/schleuder/filters/post_decryption/80_receive_from_subscribed_emailaddresses_only.rb +1 -1
- data/lib/schleuder/filters/post_decryption/90_strip_html_from_alternative_if_keywords_present.rb +36 -4
- data/lib/schleuder/filters/pre_decryption/60_key_auto_import_from_autocrypt_header.rb +9 -0
- data/lib/schleuder/gpgme/ctx.rb +34 -93
- data/lib/schleuder/gpgme/key.rb +1 -1
- data/lib/schleuder/gpgme/key_extractor.rb +30 -0
- data/lib/schleuder/http.rb +56 -0
- data/lib/schleuder/key_fetcher.rb +89 -0
- data/lib/schleuder/keyword_handlers/key_management.rb +2 -2
- data/lib/schleuder/keyword_handlers/subscription_management.rb +19 -3
- data/lib/schleuder/list.rb +26 -10
- data/lib/schleuder/list_builder.rb +1 -1
- data/lib/schleuder/logger.rb +1 -1
- data/lib/schleuder/mail/gpg/decrypted_part.rb +20 -0
- data/lib/schleuder/mail/gpg/delivery_handler.rb +38 -0
- data/lib/schleuder/mail/gpg/encrypted_part.rb +29 -5
- data/lib/schleuder/mail/gpg/gpgme_ext.rb +8 -0
- data/lib/schleuder/mail/gpg/gpgme_helper.rb +155 -0
- data/lib/schleuder/mail/gpg/inline_decrypted_message.rb +82 -0
- data/lib/schleuder/mail/gpg/inline_signed_message.rb +73 -0
- data/lib/schleuder/mail/gpg/mime_signed_message.rb +28 -0
- data/lib/schleuder/mail/gpg/missing_keys_error.rb +6 -0
- data/lib/schleuder/mail/gpg/sign_part.rb +19 -9
- data/lib/schleuder/mail/gpg/signed_part.rb +37 -0
- data/lib/schleuder/mail/gpg/verified_part.rb +10 -0
- data/lib/schleuder/mail/gpg/verify_result_attribute.rb +32 -0
- data/lib/schleuder/mail/gpg/version_part.rb +22 -0
- data/lib/schleuder/mail/gpg.rb +236 -7
- data/lib/schleuder/mail/message.rb +98 -14
- data/lib/schleuder/sks_client.rb +18 -0
- data/lib/schleuder/version.rb +1 -1
- data/lib/schleuder/vks_client.rb +24 -0
- data/lib/schleuder-api-daemon/routes/key.rb +22 -1
- data/lib/schleuder.rb +11 -7
- data/locales/de.yml +22 -3
- data/locales/en.yml +22 -3
- metadata +72 -24
@@ -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
|
+
|
@@ -1,15 +1,26 @@
|
|
1
1
|
module Mail
|
2
2
|
module Gpg
|
3
3
|
class SignPart < Mail::Part
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
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
|
-
|
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,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
|
data/lib/schleuder/mail/gpg.rb
CHANGED
@@ -1,15 +1,244 @@
|
|
1
|
+
require 'schleuder/mail/gpg/missing_keys_error'
|
2
|
+
require 'schleuder/mail/gpg/version_part'
|
3
|
+
require 'schleuder/mail/gpg/decrypted_part'
|
4
|
+
require 'schleuder/mail/gpg/encrypted_part'
|
5
|
+
require 'schleuder/mail/gpg/inline_decrypted_message'
|
6
|
+
require 'schleuder/mail/gpg/gpgme_helper'
|
7
|
+
require 'schleuder/mail/gpg/signed_part'
|
8
|
+
require 'schleuder/mail/gpg/mime_signed_message'
|
9
|
+
require 'schleuder/mail/gpg/inline_signed_message'
|
10
|
+
|
1
11
|
module Mail
|
2
12
|
module Gpg
|
3
|
-
|
4
|
-
|
13
|
+
BEGIN_PGP_MESSAGE_MARKER = /^-----BEGIN PGP MESSAGE-----/
|
14
|
+
BEGIN_PGP_SIGNED_MESSAGE_MARKER = /^-----BEGIN PGP SIGNED MESSAGE-----/
|
15
|
+
|
16
|
+
# options are:
|
17
|
+
# :sign: sign message using the sender's private key
|
18
|
+
# :sign_as: sign using this key (give the corresponding email address or key fingerprint)
|
19
|
+
# :password: passphrase for the signing key
|
20
|
+
# :keys: A hash mapping recipient email addresses to public keys or public
|
21
|
+
# key ids. Imports any keys given here that are not already part of the
|
22
|
+
# local keychain before sending the mail.
|
23
|
+
# :always_trust: send encrypted mail to untrusted receivers, true by default
|
24
|
+
def self.encrypt(cleartext_mail, options = {})
|
25
|
+
construct_mail(cleartext_mail, options) do
|
26
|
+
receivers = []
|
27
|
+
receivers += cleartext_mail.to if cleartext_mail.to
|
28
|
+
receivers += cleartext_mail.cc if cleartext_mail.cc
|
29
|
+
receivers += cleartext_mail.bcc if cleartext_mail.bcc
|
30
|
+
|
31
|
+
if options[:sign_as]
|
32
|
+
options[:sign] = true
|
33
|
+
options[:signers] = options.delete(:sign_as)
|
34
|
+
elsif options[:sign]
|
35
|
+
options[:signers] = cleartext_mail.from
|
36
|
+
end
|
37
|
+
|
38
|
+
add_part VersionPart.new
|
39
|
+
add_part EncryptedPart.new(cleartext_mail,
|
40
|
+
options.merge({recipients: receivers}))
|
41
|
+
content_type "multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=#{boundary}"
|
42
|
+
body.preamble = options[:preamble] || 'This is an OpenPGP/MIME encrypted message (RFC 2440 and 3156)'
|
5
43
|
|
6
|
-
def encrypt(cleartext_mail, options={})
|
7
|
-
encrypted_mail = encrypt_mailgpg(cleartext_mail, options)
|
8
44
|
if cleartext_mail.protected_headers_subject
|
9
|
-
|
45
|
+
subject cleartext_mail.protected_headers_subject
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.sign(cleartext_mail, options = {})
|
51
|
+
options[:sign_as] ||= cleartext_mail.from
|
52
|
+
construct_mail(cleartext_mail, options) do
|
53
|
+
to_be_signed = SignedPart.build(cleartext_mail)
|
54
|
+
add_part to_be_signed
|
55
|
+
add_part to_be_signed.sign(options)
|
56
|
+
|
57
|
+
content_type "multipart/signed; micalg=pgp-sha1; protocol=\"application/pgp-signature\"; boundary=#{boundary}"
|
58
|
+
body.preamble = options[:preamble] || 'This is an OpenPGP/MIME signed message (RFC 4880 and 3156)'
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# options are:
|
63
|
+
# :verify: decrypt and verify
|
64
|
+
def self.decrypt(encrypted_mail, options = {})
|
65
|
+
if encrypted_mime?(encrypted_mail)
|
66
|
+
decrypt_pgp_mime(encrypted_mail, options)
|
67
|
+
elsif encrypted_inline?(encrypted_mail)
|
68
|
+
decrypt_pgp_inline(encrypted_mail, options)
|
69
|
+
else
|
70
|
+
raise EncodingError.new("Unsupported encryption format '#{encrypted_mail.content_type}'")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.signature_valid?(signed_mail, options = {})
|
75
|
+
if signed_mime?(signed_mail)
|
76
|
+
signature_valid_pgp_mime?(signed_mail, options)
|
77
|
+
elsif signed_inline?(signed_mail)
|
78
|
+
signature_valid_inline?(signed_mail, options)
|
79
|
+
else
|
80
|
+
raise EncodingError.new("Unsupported signature format '#{signed_mail.content_type}'")
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# true if a mail is encrypted
|
85
|
+
def self.encrypted?(mail)
|
86
|
+
return true if encrypted_mime?(mail)
|
87
|
+
return true if encrypted_inline?(mail)
|
88
|
+
false
|
89
|
+
end
|
90
|
+
|
91
|
+
# true if a mail is signed.
|
92
|
+
#
|
93
|
+
# throws EncodingError if called on an encrypted mail (so only call this method if encrypted? is false)
|
94
|
+
def self.signed?(mail)
|
95
|
+
return true if signed_mime?(mail)
|
96
|
+
return true if signed_inline?(mail)
|
97
|
+
if encrypted?(mail)
|
98
|
+
raise EncodingError.new('Unable to determine signature on an encrypted mail, use :verify option on decrypt()')
|
99
|
+
end
|
100
|
+
false
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.construct_mail(cleartext_mail, options, &block)
|
104
|
+
Mail.new do
|
105
|
+
self.perform_deliveries = cleartext_mail.perform_deliveries
|
106
|
+
Mail::Gpg.copy_headers cleartext_mail, self
|
107
|
+
# necessary?
|
108
|
+
if cleartext_mail.message_id
|
109
|
+
header['Message-ID'] = cleartext_mail['Message-ID'].value
|
110
|
+
end
|
111
|
+
instance_eval &block
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# decrypts PGP/MIME (RFC 3156, section 4) encrypted mail
|
116
|
+
def self.decrypt_pgp_mime(encrypted_mail, options)
|
117
|
+
if encrypted_mail.parts.length < 2
|
118
|
+
raise EncodingError.new("RFC 3156 mandates exactly two body parts, found '#{encrypted_mail.parts.length}'")
|
119
|
+
end
|
120
|
+
if !VersionPart.is_version_part? encrypted_mail.parts[0]
|
121
|
+
raise EncodingError.new("RFC 3156 first part not a valid version part '#{encrypted_mail.parts[0]}'")
|
122
|
+
end
|
123
|
+
decrypted = DecryptedPart.new(encrypted_mail.parts[1], options)
|
124
|
+
Mail.new(decrypted.raw_source) do
|
125
|
+
# headers from the encrypted part (set by the initializer above) take
|
126
|
+
# precedence over those from the outer mail.
|
127
|
+
Mail::Gpg.copy_headers encrypted_mail, self, overwrite: false
|
128
|
+
verify_result decrypted.verify_result if options[:verify]
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
# decrypts inline PGP encrypted mail
|
133
|
+
def self.decrypt_pgp_inline(encrypted_mail, options)
|
134
|
+
InlineDecryptedMessage.setup(encrypted_mail, options)
|
135
|
+
end
|
136
|
+
|
137
|
+
def self.verify(signed_mail, options = {})
|
138
|
+
if signed_mime?(signed_mail)
|
139
|
+
Mail::Gpg::MimeSignedMessage.setup signed_mail, options
|
140
|
+
elsif signed_inline?(signed_mail)
|
141
|
+
Mail::Gpg::InlineSignedMessage.setup signed_mail, options
|
142
|
+
else
|
143
|
+
signed_mail
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# check signature for PGP/MIME (RFC 3156, section 5) signed mail
|
148
|
+
def self.signature_valid_pgp_mime?(signed_mail, options)
|
149
|
+
# MUST contain exactly two body parts
|
150
|
+
if signed_mail.parts.length != 2
|
151
|
+
raise EncodingError.new("RFC 3156 mandates exactly two body parts, found '#{signed_mail.parts.length}'")
|
152
|
+
end
|
153
|
+
result, verify_result = SignPart.verify_signature(signed_mail.parts[0], signed_mail.parts[1], options)
|
154
|
+
signed_mail.verify_result = verify_result
|
155
|
+
return result
|
156
|
+
end
|
157
|
+
|
158
|
+
# check signature for inline signed mail
|
159
|
+
def self.signature_valid_inline?(signed_mail, options)
|
160
|
+
result = nil
|
161
|
+
if signed_mail.multipart?
|
162
|
+
signed_mail.parts.each do |part|
|
163
|
+
if signed_inline?(part)
|
164
|
+
if result.nil?
|
165
|
+
result = true
|
166
|
+
signed_mail.verify_result = []
|
167
|
+
end
|
168
|
+
result &= signature_valid_inline?(part, options)
|
169
|
+
signed_mail.verify_result << part.verify_result
|
170
|
+
end
|
171
|
+
end
|
172
|
+
else
|
173
|
+
result, verify_result = GpgmeHelper.inline_verify(signed_mail.body.to_s, options)
|
174
|
+
signed_mail.verify_result = verify_result
|
175
|
+
end
|
176
|
+
return result
|
177
|
+
end
|
178
|
+
|
179
|
+
# copies all header fields from mail in first argument to that given last
|
180
|
+
def self.copy_headers(from, to, overwrite: true)
|
181
|
+
from.header.fields.each do |field|
|
182
|
+
if overwrite || to.header[field.name].nil?
|
183
|
+
to.header[field.name] = field.value
|
184
|
+
end
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# check if PGP/MIME encrypted (RFC 3156)
|
189
|
+
def self.encrypted_mime?(mail)
|
190
|
+
mail.has_content_type? &&
|
191
|
+
mail.mime_type == 'multipart/encrypted' &&
|
192
|
+
mail.content_type_parameters[:protocol] == 'application/pgp-encrypted'
|
193
|
+
end
|
194
|
+
|
195
|
+
# check if inline PGP (i.e. if any parts of the mail includes
|
196
|
+
# the PGP MESSAGE marker)
|
197
|
+
def self.encrypted_inline?(mail)
|
198
|
+
begin
|
199
|
+
return true if mail.body.to_s =~ BEGIN_PGP_MESSAGE_MARKER
|
200
|
+
rescue
|
201
|
+
end
|
202
|
+
if mail.multipart?
|
203
|
+
mail.parts.each do |part|
|
204
|
+
begin
|
205
|
+
return true if part.body.to_s =~ BEGIN_PGP_MESSAGE_MARKER
|
206
|
+
rescue
|
207
|
+
end
|
208
|
+
return true if part.has_content_type? &&
|
209
|
+
/application\/(?:octet-stream|pgp-encrypted)/ =~ part.mime_type &&
|
210
|
+
/.*\.(?:pgp|gpg|asc)$/ =~ part.content_type_parameters[:name] &&
|
211
|
+
part.content_type_parameters[:name] != 'signature.asc'
|
212
|
+
# that last condition above prevents false positives in case e.g.
|
213
|
+
# someone forwards a mime signed mail including signature.
|
214
|
+
end
|
215
|
+
end
|
216
|
+
false
|
217
|
+
end
|
218
|
+
|
219
|
+
# check if PGP/MIME signed (RFC 3156)
|
220
|
+
def self.signed_mime?(mail)
|
221
|
+
mail.has_content_type? &&
|
222
|
+
mail.mime_type == 'multipart/signed' &&
|
223
|
+
mail.content_type_parameters[:protocol] == 'application/pgp-signature'
|
224
|
+
end
|
225
|
+
|
226
|
+
# check if inline PGP (i.e. if any parts of the mail includes
|
227
|
+
# the PGP SIGNED marker)
|
228
|
+
def self.signed_inline?(mail)
|
229
|
+
begin
|
230
|
+
return true if mail.body.to_s =~ BEGIN_PGP_SIGNED_MESSAGE_MARKER
|
231
|
+
rescue
|
232
|
+
end
|
233
|
+
if mail.multipart?
|
234
|
+
mail.parts.each do |part|
|
235
|
+
begin
|
236
|
+
return true if part.body.to_s =~ BEGIN_PGP_SIGNED_MESSAGE_MARKER
|
237
|
+
rescue
|
238
|
+
end
|
10
239
|
end
|
11
|
-
encrypted_mail
|
12
240
|
end
|
13
|
-
|
241
|
+
false
|
242
|
+
end
|
14
243
|
end
|
15
244
|
end
|
@@ -1,3 +1,6 @@
|
|
1
|
+
require 'schleuder/mail/gpg/delivery_handler'
|
2
|
+
require 'schleuder/mail/gpg/verify_result_attribute'
|
3
|
+
|
1
4
|
module Mail
|
2
5
|
# creates a Mail::Message likes schleuder
|
3
6
|
def self.create_message_to_list(msg, recipient, list)
|
@@ -13,11 +16,14 @@ module Mail
|
|
13
16
|
|
14
17
|
# TODO: Test if subclassing breaks integration of mail-gpg.
|
15
18
|
class Message
|
19
|
+
include Mail::Gpg::VerifyResultAttribute #for Mail::Gpg
|
20
|
+
|
16
21
|
attr_accessor :recipient
|
17
22
|
attr_accessor :original_message
|
18
23
|
attr_accessor :list
|
19
24
|
attr_accessor :protected_headers_subject
|
20
25
|
attr_writer :dynamic_pseudoheaders
|
26
|
+
attr_accessor :raise_encryption_errors # for Mail::Gpg
|
21
27
|
|
22
28
|
# TODO: This should be in initialize(), but I couldn't understand the
|
23
29
|
# strange errors about wrong number of arguments when overriding
|
@@ -154,7 +160,12 @@ module Mail
|
|
154
160
|
def signer
|
155
161
|
@signer ||= begin
|
156
162
|
if signing_key.present?
|
157
|
-
|
163
|
+
# Look for a subscription that matches the sending address, in case
|
164
|
+
# there're multiple subscriptions for the same key. As a fallback use
|
165
|
+
# the first subscription found.
|
166
|
+
sender_email = self.from.to_s.downcase
|
167
|
+
subscriptions = list.subscriptions.where(fingerprint: signing_key.fingerprint)
|
168
|
+
subscriptions.where(email: sender_email).first || subscriptions.first
|
158
169
|
end
|
159
170
|
end
|
160
171
|
end
|
@@ -436,36 +447,109 @@ module Mail
|
|
436
447
|
true
|
437
448
|
end
|
438
449
|
|
450
|
+
# turn on gpg encryption / set gpg options.
|
451
|
+
#
|
452
|
+
# options are:
|
453
|
+
#
|
454
|
+
# encrypt: encrypt the message. defaults to true
|
455
|
+
# sign: also sign the message. false by default
|
456
|
+
# sign_as: UIDs to sign the message with
|
457
|
+
#
|
458
|
+
# See Mail::Gpg methods encrypt and sign for more
|
459
|
+
# possible options
|
460
|
+
#
|
461
|
+
# mail.gpg encrypt: true
|
462
|
+
# mail.gpg encrypt: true, sign: true
|
463
|
+
# mail.gpg encrypt: true, sign_as: "other_address@host.com"
|
464
|
+
#
|
465
|
+
# sign-only mode is also supported:
|
466
|
+
# mail.gpg sign: true
|
467
|
+
# mail.gpg sign_as: 'jane@doe.com'
|
468
|
+
#
|
469
|
+
# To turn off gpg encryption use:
|
470
|
+
# mail.gpg false
|
471
|
+
#
|
472
|
+
def gpg(options = nil)
|
473
|
+
case options
|
474
|
+
when nil
|
475
|
+
@gpg
|
476
|
+
when false
|
477
|
+
@gpg = nil
|
478
|
+
if Mail::Gpg::DeliveryHandler == delivery_handler
|
479
|
+
self.delivery_handler = nil
|
480
|
+
end
|
481
|
+
nil
|
482
|
+
else
|
483
|
+
self.raise_encryption_errors = true if raise_encryption_errors.nil?
|
484
|
+
@gpg = options
|
485
|
+
self.delivery_handler ||= Mail::Gpg::DeliveryHandler
|
486
|
+
nil
|
487
|
+
end
|
488
|
+
end
|
489
|
+
|
490
|
+
# true if this mail is encrypted
|
491
|
+
def encrypted?
|
492
|
+
Mail::Gpg.encrypted?(self)
|
493
|
+
end
|
494
|
+
|
495
|
+
# returns the decrypted mail object.
|
496
|
+
#
|
497
|
+
# pass verify: true to verify signatures as well. The gpgme verification
|
498
|
+
# result will be available via decrypted_mail.verify_result
|
499
|
+
def decrypt(options = {})
|
500
|
+
Mail::Gpg.decrypt(self, options)
|
501
|
+
end
|
502
|
+
|
503
|
+
# true if this mail is signed (but not encrypted)
|
504
|
+
def signed?
|
505
|
+
Mail::Gpg.signed?(self)
|
506
|
+
end
|
507
|
+
|
508
|
+
# verify signatures. returns a new mail object with signatures removed and
|
509
|
+
# populated verify_result.
|
510
|
+
#
|
511
|
+
# verified = signed_mail.verify()
|
512
|
+
# verified.signature_valid?
|
513
|
+
# signers = mail.signatures.map{|sig| sig.from}
|
514
|
+
#
|
515
|
+
# use import_missing_keys: true in order to try to fetch and import
|
516
|
+
# unknown keys for signature validation
|
517
|
+
def verify(options = {})
|
518
|
+
Mail::Gpg.verify(self, options)
|
519
|
+
end
|
520
|
+
|
521
|
+
def repeat_validation!
|
522
|
+
new = self.original_message.dup.setup
|
523
|
+
self.verify_result = new.verify_result
|
524
|
+
@signatures = new.signatures
|
525
|
+
dynamic_pseudoheaders << new.dynamic_pseudoheaders
|
526
|
+
end
|
527
|
+
|
439
528
|
private
|
440
529
|
|
441
530
|
|
442
531
|
def extract_keywords(content_lines)
|
443
532
|
keywords = []
|
444
533
|
in_keyword_block = false
|
445
|
-
found_blank_line = false
|
446
534
|
content_lines.each_with_index do |line, i|
|
447
|
-
if
|
535
|
+
if line.blank?
|
536
|
+
# Swallow the line: before the actual content or keywords block begins we want to drop blank lines.
|
537
|
+
content_lines[i] = nil
|
538
|
+
# Stop interpreting the following line as argument to the previous keyword.
|
539
|
+
in_keyword_block = false
|
540
|
+
elsif match = line.match(/^x-([^:\s]*)[:\s]*(.*)/i)
|
448
541
|
keyword = match[1].strip.downcase
|
449
542
|
arguments = match[2].to_s.strip.downcase.split(/[,; ]{1,}/)
|
450
543
|
keywords << [keyword, arguments]
|
451
544
|
in_keyword_block = true
|
452
|
-
|
453
545
|
# Set this line to nil to have it stripped from the message.
|
454
546
|
content_lines[i] = nil
|
455
|
-
elsif line.blank? && keywords.any?
|
456
|
-
# Look for blank lines after the first keyword had been found.
|
457
|
-
# These might mark the end of the keywords-block — unless more keywords follow.
|
458
|
-
found_blank_line = true
|
459
|
-
# Swallow the line: before the actual content begins we want to drop blank lines.
|
460
|
-
content_lines[i] = nil
|
461
|
-
# Stop interpreting the following line as argument to the previous keyword.
|
462
|
-
in_keyword_block = false
|
463
547
|
elsif in_keyword_block == true
|
464
548
|
# Interpret line as arguments to the previous keyword.
|
465
549
|
keywords[-1][-1] += line.downcase.strip.split(/[,; ]{1,}/)
|
466
550
|
content_lines[i] = nil
|
467
|
-
|
468
|
-
# Any line
|
551
|
+
else
|
552
|
+
# Any other line stops the keyword parsing.
|
469
553
|
break
|
470
554
|
end
|
471
555
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Schleuder
|
2
|
+
class SksClient < Http
|
3
|
+
SKS_PATH = '/pks/lookup?&exact=on&op=get&options=mr&search=SEARCH_ARG'
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def get(input)
|
7
|
+
super(url(input))
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def url(input)
|
13
|
+
arg = CGI.escape(input.gsub(/\s/, ''))
|
14
|
+
Conf.sks_keyserver + SKS_PATH.gsub('SEARCH_ARG', arg)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|