mail-gpg 0.0.6 → 0.1.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.
- data/README.md +69 -3
- data/lib/hkp.rb +21 -2
- data/lib/mail/gpg/decrypted_part.rb +18 -0
- data/lib/mail/gpg/delivery_handler.rb +1 -1
- data/lib/mail/gpg/encrypted_part.rb +5 -59
- data/lib/mail/gpg/gpgme_helper.rb +96 -0
- data/lib/mail/gpg/inline_decrypted_message.rb +54 -0
- data/lib/mail/gpg/message_patch.rb +8 -0
- data/lib/mail/gpg/sign_part.rb +16 -0
- data/lib/mail/gpg/version.rb +1 -1
- data/lib/mail/gpg/version_part.rb +11 -3
- data/lib/mail/gpg.rb +91 -17
- data/test/action_mailer_test.rb +24 -0
- data/test/decrypted_part_test.rb +37 -0
- data/test/encrypted_part_test.rb +14 -14
- data/test/gpg_test.rb +165 -3
- data/test/gpghome/random_seed +0 -0
- data/test/hkp_test.rb +36 -0
- data/test/inline_decrypted_message_test.rb +126 -0
- data/test/message_test.rb +61 -0
- data/test/sign_part_test.rb +15 -0
- data/test/version_part_test.rb +36 -0
- metadata +16 -2
data/README.md
CHANGED
@@ -3,6 +3,13 @@
|
|
3
3
|
This gem adds GPG/MIME encryption capabilities to the [Ruby Mail
|
4
4
|
Library](https://github.com/mikel/mail)
|
5
5
|
|
6
|
+
For maximum interoperability the gem also supports *decryption* of messages using the non-standard 'PGP-Inline' method
|
7
|
+
as for example supported in the Mozilla Enigmail OpenPGP plugin.
|
8
|
+
|
9
|
+
There may still be GPG encrypted messages that can not be handled by the library, as there are some legacy formats used in the
|
10
|
+
wild as described in this [Know your PGP implementation](http://binblog.info/2008/03/12/know-your-pgp-implementation/) blog.
|
11
|
+
|
12
|
+
|
6
13
|
## Installation
|
7
14
|
|
8
15
|
Add this line to your application's Gemfile:
|
@@ -75,11 +82,65 @@ updating it's db when necessary.
|
|
75
82
|
You may also want to have a look at the [GPGME](https://github.com/ueno/ruby-gpgme) docs and code base for more info on the various options, especially regarding the `passphrase_callback` arguments.
|
76
83
|
|
77
84
|
|
85
|
+
### Decrypting
|
86
|
+
|
87
|
+
Receive the mail as usual. Check if it is encrypted using the `encrypted?` method. Get a decrypted version of the mail with the `decrypt` method:
|
88
|
+
|
89
|
+
```ruby
|
90
|
+
mail = Mail.first
|
91
|
+
mail.subject # subject is never encrypted
|
92
|
+
if mail.encrypted?
|
93
|
+
# decrypt using your private key, protected by the given passphrase
|
94
|
+
plaintext_mail = mail.decrypt(:password => 'abc')
|
95
|
+
# the plaintext_mail, is a full Mail::Message object, just decrypted
|
96
|
+
end
|
97
|
+
```
|
98
|
+
|
99
|
+
A `GPGME::Error::BadPassphrase` will be raised if the password for the private key is incorrect.
|
100
|
+
A `EncodingError` will be raised if the encrypted mails is not encoded correctly as a [RFC 3156](http://www.ietf.org/rfc/rfc3156.txt) message.
|
101
|
+
|
102
|
+
|
78
103
|
### Signing only
|
79
104
|
|
80
|
-
|
105
|
+
Just leave the the `:encrypt` option out or pass `encrypt: false`, i.e.
|
106
|
+
|
107
|
+
|
108
|
+
Mail.new do
|
109
|
+
to 'jane@doe.net'
|
110
|
+
gpg sign: true
|
111
|
+
end.deliver
|
81
112
|
|
82
113
|
|
114
|
+
### Key import from public key servers
|
115
|
+
|
116
|
+
The Hkp class can be used to lookup and import public keys from public key servers.
|
117
|
+
You can specify the keyserver url when initializing the class:
|
118
|
+
|
119
|
+
```
|
120
|
+
hkp = Hkp.new("hkp://my-key-server.de")
|
121
|
+
```
|
122
|
+
|
123
|
+
If no url is given, this gem will try to determine the default keyserver
|
124
|
+
url from the system's gpg config (using `gpgconf` if available or by
|
125
|
+
parsing the `gpg.conf` file). As a last resort, the server-pool at
|
126
|
+
`http://pool.sks-keyservers.net:11371` will be used.
|
127
|
+
|
128
|
+
Lookup key ids by searching the keyserver for an email address
|
129
|
+
|
130
|
+
```
|
131
|
+
hkp.search('jane@doe.net')
|
132
|
+
```
|
133
|
+
|
134
|
+
You can lookup (and import) a specific key by its id:
|
135
|
+
|
136
|
+
```
|
137
|
+
key = hkp.fetch(id)
|
138
|
+
GPGME::Key.import(key)
|
139
|
+
|
140
|
+
# or do both in one step
|
141
|
+
hkp.fetch_and_import(id)
|
142
|
+
```
|
143
|
+
|
83
144
|
## Rails / ActionMailer integration
|
84
145
|
|
85
146
|
class MyMailer < ActionMailer::Base
|
@@ -102,8 +163,7 @@ around with your personal gpg keychain.
|
|
102
163
|
|
103
164
|
## Todo
|
104
165
|
|
105
|
-
*
|
106
|
-
* Decryption and signature verification for received mails
|
166
|
+
* signature verification for received mails
|
107
167
|
* on the fly import of recipients' keys from public key servers based on email address or key id
|
108
168
|
* handle encryption errors due to missing keys - maybe return a list of failed
|
109
169
|
recipients
|
@@ -120,3 +180,9 @@ around with your personal gpg keychain.
|
|
120
180
|
5. Create new Pull Request
|
121
181
|
|
122
182
|
|
183
|
+
## Credits
|
184
|
+
|
185
|
+
Thanks to:
|
186
|
+
|
187
|
+
* [morten-andersen](https://github.com/morten-andersen) for implementing decryption support for PGP/MIME and inline encrypted messages
|
188
|
+
* [FewKinG](https://github.com/FewKinG) for implementing the sign only featur and keyserver url lookup
|
data/lib/hkp.rb
CHANGED
@@ -3,8 +3,8 @@ require 'gpgme'
|
|
3
3
|
|
4
4
|
# simple HKP client for public key retrieval
|
5
5
|
class Hkp
|
6
|
-
def initialize(keyserver =
|
7
|
-
@keyserver = keyserver
|
6
|
+
def initialize(keyserver = nil)
|
7
|
+
@keyserver = keyserver || lookup_keyserver || 'http://pool.sks-keyservers.net:11371'
|
8
8
|
end
|
9
9
|
|
10
10
|
# hkp.search 'user@host.com'
|
@@ -50,4 +50,23 @@ class Hkp
|
|
50
50
|
end
|
51
51
|
end
|
52
52
|
|
53
|
+
def exec_cmd(cmd)
|
54
|
+
res = `#{cmd}`
|
55
|
+
return nil if $?.exitstatus != 0
|
56
|
+
res
|
57
|
+
end
|
58
|
+
|
59
|
+
def lookup_keyserver
|
60
|
+
url = nil
|
61
|
+
if res = exec_cmd("gpgconf --list-options gpgs 2>&1 | grep keyserver 2>&1")
|
62
|
+
url = URI.decode(res.split(":").last.split("\"").last.strip)
|
63
|
+
elsif res = exec_cmd("gpg --gpgconf-list 2>&1 | grep gpgconf-gpg.conf 2>&1")
|
64
|
+
conf_file = res.split(":").last.split("\"").last.strip
|
65
|
+
if res = exec_cmd("cat #{conf_file} 2>&1 | grep ^keyserver 2>&1")
|
66
|
+
url = res.split(" ").last.strip
|
67
|
+
end
|
68
|
+
end
|
69
|
+
url =~ /^(http|hkp)/ ? url : nil
|
70
|
+
end
|
71
|
+
|
53
72
|
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Mail
|
2
|
+
module Gpg
|
3
|
+
class DecryptedPart < Mail::Part
|
4
|
+
|
5
|
+
# options are:
|
6
|
+
#
|
7
|
+
# :verify: decrypt and verify
|
8
|
+
def initialize(cipher_part, options = {})
|
9
|
+
if cipher_part.mime_type != EncryptedPart::CONTENT_TYPE
|
10
|
+
raise EncodingError, "RFC 3136 incorrect mime type for encrypted part '#{cipher_part.mime_type}'"
|
11
|
+
end
|
12
|
+
|
13
|
+
decrypted = GpgmeHelper.decrypt(cipher_part.body.decoded, options)
|
14
|
+
super(decrypted)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -10,7 +10,7 @@ module Mail
|
|
10
10
|
if options.delete(:encrypt)
|
11
11
|
encrypted_mail = Mail::Gpg.encrypt(mail, options)
|
12
12
|
elsif options[:sign] || options[:sign_as]
|
13
|
-
|
13
|
+
encrypted_mail = Mail::Gpg.sign(mail, options)
|
14
14
|
else
|
15
15
|
# encrypt and sign are off -> do not encrypt or sign
|
16
16
|
yield
|
@@ -2,10 +2,12 @@ module Mail
|
|
2
2
|
module Gpg
|
3
3
|
class EncryptedPart < Mail::Part
|
4
4
|
|
5
|
+
CONTENT_TYPE = 'application/octet-stream'
|
6
|
+
|
5
7
|
# options are:
|
6
8
|
#
|
7
9
|
# :signers : sign using this key (give the corresponding email address)
|
8
|
-
# :
|
10
|
+
# :password: passphrase for the signing key
|
9
11
|
# :recipients : array of receiver addresses
|
10
12
|
# :keys : A hash mapping recipient email addresses to public keys or public
|
11
13
|
# key ids. Imports any keys given here that are not already part of the
|
@@ -14,70 +16,14 @@ module Mail
|
|
14
16
|
def initialize(cleartext_mail, options = {})
|
15
17
|
options = { always_trust: true }.merge options
|
16
18
|
|
17
|
-
encrypted = encrypt(cleartext_mail.encoded, options)
|
19
|
+
encrypted = GpgmeHelper.encrypt(cleartext_mail.encoded, options)
|
18
20
|
super() do
|
19
21
|
body encrypted.to_s
|
20
|
-
content_type
|
22
|
+
content_type "#{CONTENT_TYPE}; name=\"encrypted.asc\""
|
21
23
|
content_disposition 'inline; filename="encrypted.asc"'
|
22
24
|
content_description 'OpenPGP encrypted message'
|
23
25
|
end
|
24
26
|
end
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
def encrypt(plain, options = {})
|
29
|
-
options = options.merge({armor: true})
|
30
|
-
|
31
|
-
plain_data = GPGME::Data.new(plain)
|
32
|
-
cipher_data = GPGME::Data.new(options[:output])
|
33
|
-
|
34
|
-
recipient_keys = keys_for_data options[:recipients], options.delete(:keys)
|
35
|
-
|
36
|
-
flags = 0
|
37
|
-
flags |= GPGME::ENCRYPT_ALWAYS_TRUST if options[:always_trust]
|
38
|
-
|
39
|
-
GPGME::Ctx.new(options) do |ctx|
|
40
|
-
begin
|
41
|
-
if options[:sign]
|
42
|
-
if options[:signers]
|
43
|
-
signers = Key.find(:public, options[:signers], :sign)
|
44
|
-
ctx.add_signer(*signers)
|
45
|
-
end
|
46
|
-
ctx.encrypt_sign(recipient_keys, plain_data, cipher_data, flags)
|
47
|
-
else
|
48
|
-
ctx.encrypt(recipient_keys, plain_data, cipher_data, flags)
|
49
|
-
end
|
50
|
-
rescue GPGME::Error::UnusablePublicKey => exc
|
51
|
-
exc.keys = ctx.encrypt_result.invalid_recipients
|
52
|
-
raise exc
|
53
|
-
rescue GPGME::Error::UnusableSecretKey => exc
|
54
|
-
exc.keys = ctx.sign_result.invalid_signers
|
55
|
-
raise exc
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
cipher_data.seek(0)
|
60
|
-
cipher_data
|
61
|
-
end
|
62
|
-
|
63
|
-
# normalizes the list of recipients' emails, key ids and key data to a
|
64
|
-
# list of Key objects
|
65
|
-
def keys_for_data(emails_or_shas_or_keys, key_data = nil)
|
66
|
-
if key_data
|
67
|
-
[emails_or_shas_or_keys].flatten.map do |r|
|
68
|
-
# import any given keys
|
69
|
-
k = key_data[r]
|
70
|
-
if k and k =~ /-----BEGIN PGP/
|
71
|
-
k = GPGME::Key.import(k).imports.map(&:fpr)
|
72
|
-
end
|
73
|
-
GPGME::Key.find(:public, k || r, :encrypt)
|
74
|
-
end.flatten
|
75
|
-
else
|
76
|
-
# key lookup in keychain for all receivers
|
77
|
-
GPGME::Key.find :public, emails_or_shas_or_keys, :encrypt
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
27
|
end
|
82
28
|
end
|
83
29
|
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# GPGME methods for encryption/decryption/signing
|
2
|
+
module Mail
|
3
|
+
module Gpg
|
4
|
+
class GpgmeHelper
|
5
|
+
|
6
|
+
def self.encrypt(plain, options = {})
|
7
|
+
options = options.merge({armor: true})
|
8
|
+
|
9
|
+
plain_data = GPGME::Data.new(plain)
|
10
|
+
cipher_data = GPGME::Data.new(options[:output])
|
11
|
+
|
12
|
+
recipient_keys = keys_for_data options[:recipients], options.delete(:keys)
|
13
|
+
|
14
|
+
flags = 0
|
15
|
+
flags |= GPGME::ENCRYPT_ALWAYS_TRUST if options[:always_trust]
|
16
|
+
|
17
|
+
GPGME::Ctx.new(options) do |ctx|
|
18
|
+
begin
|
19
|
+
if options[:sign]
|
20
|
+
if options[:signers]
|
21
|
+
signers = GPGME::Key.find(:public, options[:signers], :sign)
|
22
|
+
ctx.add_signer(*signers)
|
23
|
+
end
|
24
|
+
ctx.encrypt_sign(recipient_keys, plain_data, cipher_data, flags)
|
25
|
+
else
|
26
|
+
ctx.encrypt(recipient_keys, plain_data, cipher_data, flags)
|
27
|
+
end
|
28
|
+
rescue GPGME::Error::UnusablePublicKey => exc
|
29
|
+
exc.keys = ctx.encrypt_result.invalid_recipients
|
30
|
+
raise exc
|
31
|
+
rescue GPGME::Error::UnusableSecretKey => exc
|
32
|
+
exc.keys = ctx.sign_result.invalid_signers
|
33
|
+
raise exc
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
cipher_data.seek(0)
|
38
|
+
cipher_data
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.decrypt(cipher, options = {})
|
42
|
+
cipher_data = GPGME::Data.new(cipher)
|
43
|
+
plain_data = GPGME::Data.new(options[:output])
|
44
|
+
|
45
|
+
GPGME::Ctx.new(options) do |ctx|
|
46
|
+
begin
|
47
|
+
if options[:verify]
|
48
|
+
ctx.decrypt_verify(cipher_data, plain_data)
|
49
|
+
else
|
50
|
+
ctx.decrypt(cipher_data, plain_data)
|
51
|
+
end
|
52
|
+
rescue GPGME::Error::UnsupportedAlgorithm => exc
|
53
|
+
exc.algorithm = ctx.decrypt_result.unsupported_algorithm
|
54
|
+
raise exc
|
55
|
+
rescue GPGME::Error::WrongKeyUsage => exc
|
56
|
+
exc.key_usage = ctx.decrypt_result.wrong_key_usage
|
57
|
+
raise exc
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
plain_data.seek(0)
|
62
|
+
plain_data
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.sign(plain, options = {})
|
66
|
+
options.merge!({
|
67
|
+
armor: true,
|
68
|
+
signer: options.delete(:sign_as),
|
69
|
+
mode: GPGME::SIG_MODE_DETACH
|
70
|
+
})
|
71
|
+
crypto = GPGME::Crypto.new
|
72
|
+
crypto.sign GPGME::Data.new(plain), options
|
73
|
+
end
|
74
|
+
|
75
|
+
private
|
76
|
+
|
77
|
+
# normalizes the list of recipients' emails, key ids and key data to a
|
78
|
+
# list of Key objects
|
79
|
+
def self.keys_for_data(emails_or_shas_or_keys, key_data = nil)
|
80
|
+
if key_data
|
81
|
+
[emails_or_shas_or_keys].flatten.map do |r|
|
82
|
+
# import any given keys
|
83
|
+
k = key_data[r]
|
84
|
+
if k and k =~ /-----BEGIN PGP/
|
85
|
+
k = GPGME::Key.import(k).imports.map(&:fpr)
|
86
|
+
end
|
87
|
+
GPGME::Key.find(:public, k || r, :encrypt)
|
88
|
+
end.flatten
|
89
|
+
else
|
90
|
+
# key lookup in keychain for all receivers
|
91
|
+
GPGME::Key.find :public, emails_or_shas_or_keys, :encrypt
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# decryption of the so called 'PGP-Inline' message types
|
2
|
+
# this is not a standard, so the implementation is based on the notes
|
3
|
+
# here http://binblog.info/2008/03/12/know-your-pgp-implementation/
|
4
|
+
# and on test messages generated with the Mozilla Enigmail OpenPGP
|
5
|
+
# plugin https://www.enigmail.net
|
6
|
+
module Mail
|
7
|
+
module Gpg
|
8
|
+
class InlineDecryptedMessage < Mail::Message
|
9
|
+
|
10
|
+
# options are:
|
11
|
+
#
|
12
|
+
# :verify: decrypt and verify
|
13
|
+
def initialize(cipher_mail, options = {})
|
14
|
+
if cipher_mail.multipart?
|
15
|
+
super() do
|
16
|
+
cipher_mail.header.fields.each do |field|
|
17
|
+
header[field.name] = field.value
|
18
|
+
end
|
19
|
+
cipher_mail.parts.each do |part|
|
20
|
+
part Mail::Part.new do |p|
|
21
|
+
if part.has_content_type? && /application\/(?:octet-stream|pgp-encrypted)/ =~ part.mime_type
|
22
|
+
# encrypted attachment, we set the content_type to the generic 'application/octet-stream'
|
23
|
+
# and remove the .pgp/gpg/asc from name/filename in header fields
|
24
|
+
decrypted = GpgmeHelper.decrypt(part.decoded, options)
|
25
|
+
p.content_type part.content_type.sub(/application\/(?:octet-stream|pgp-encrypted)/, 'application/octet-stream')
|
26
|
+
.sub(/name=(?:"')?(.*)\.(?:pgp|gpg|asc)(?:"')?/, 'name="\1"')
|
27
|
+
p.content_disposition part.content_disposition.sub(/filename=(?:"')?(.*)\.(?:pgp|gpg|asc)(?:"')?/, 'filename="\1"')
|
28
|
+
p.content_transfer_encoding Mail::Encodings::Base64
|
29
|
+
p.body Mail::Encodings::Base64::encode(decrypted.to_s)
|
30
|
+
else
|
31
|
+
if part.body.include?('-----BEGIN PGP MESSAGE-----')
|
32
|
+
p.body GpgmeHelper.decrypt(part.decoded, options).to_s
|
33
|
+
else
|
34
|
+
p.content_type part.content_type
|
35
|
+
p.content_transfer_encoding part.content_transfer_encoding
|
36
|
+
p.body part.body.to_s
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end # of multipart
|
42
|
+
else
|
43
|
+
decrypted = cipher_mail.body.empty? ? '' : GpgmeHelper.decrypt(cipher_mail.body.decoded, options)
|
44
|
+
super() do
|
45
|
+
cipher_mail.header.fields.each do |field|
|
46
|
+
header[field.name] = field.value
|
47
|
+
end
|
48
|
+
body decrypted.to_s
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Mail
|
2
|
+
module Gpg
|
3
|
+
class SignPart < Mail::Part
|
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
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/lib/mail/gpg/version.rb
CHANGED
@@ -3,11 +3,19 @@ require 'mail/part'
|
|
3
3
|
module Mail
|
4
4
|
module Gpg
|
5
5
|
class VersionPart < Mail::Part
|
6
|
+
VERSION_1 = 'Version: 1'
|
7
|
+
CONTENT_TYPE = 'application/pgp-encrypted'
|
8
|
+
CONTENT_DESC = 'PGP/MIME Versions Identification'
|
9
|
+
|
6
10
|
def initialize(*args)
|
7
11
|
super
|
8
|
-
body
|
9
|
-
content_type
|
10
|
-
content_description
|
12
|
+
body VERSION_1
|
13
|
+
content_type CONTENT_TYPE
|
14
|
+
content_description CONTENT_DESC
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.isVersionPart?(part)
|
18
|
+
part.mime_type == CONTENT_TYPE && part.body =~ /#{VERSION_1}/
|
11
19
|
end
|
12
20
|
end
|
13
21
|
end
|
data/lib/mail/gpg.rb
CHANGED
@@ -4,9 +4,13 @@ require 'gpgme'
|
|
4
4
|
|
5
5
|
require 'mail/gpg/version'
|
6
6
|
require 'mail/gpg/version_part'
|
7
|
+
require 'mail/gpg/decrypted_part'
|
7
8
|
require 'mail/gpg/encrypted_part'
|
9
|
+
require 'mail/gpg/inline_decrypted_message'
|
10
|
+
require 'mail/gpg/gpgme_helper'
|
8
11
|
require 'mail/gpg/message_patch'
|
9
12
|
require 'mail/gpg/rails'
|
13
|
+
require 'mail/gpg/sign_part'
|
10
14
|
|
11
15
|
module Mail
|
12
16
|
module Gpg
|
@@ -19,18 +23,57 @@ module Mail
|
|
19
23
|
# local keychain before sending the mail.
|
20
24
|
# :always_trust : send encrypted mail to untrusted receivers, true by default
|
21
25
|
def self.encrypt(cleartext_mail, options = {})
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
26
|
+
construct_mail(cleartext_mail, options) do
|
27
|
+
receivers = []
|
28
|
+
receivers += cleartext_mail.to if cleartext_mail.to
|
29
|
+
receivers += cleartext_mail.cc if cleartext_mail.cc
|
30
|
+
receivers += cleartext_mail.bcc if cleartext_mail.bcc
|
31
|
+
|
32
|
+
if options[:sign_as]
|
33
|
+
options[:sign] = true
|
34
|
+
options[:signers] = options.delete(:sign_as)
|
35
|
+
elsif options[:sign]
|
36
|
+
options[:signers] = cleartext_mail.from
|
37
|
+
end
|
38
|
+
|
39
|
+
add_part VersionPart.new
|
40
|
+
add_part EncryptedPart.new(cleartext_mail,
|
41
|
+
options.merge({recipients: receivers}))
|
42
|
+
content_type "multipart/encrypted; protocol=\"application/pgp-encrypted\"; boundary=#{boundary}"
|
43
|
+
body.preamble = options[:preamble] || "This is an OpenPGP/MIME encrypted message (RFC 2440 and 3156)"
|
32
44
|
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.sign(cleartext_mail, options = {})
|
48
|
+
construct_mail(cleartext_mail, options) do
|
49
|
+
options[:sign_as] ||= cleartext_mail.from
|
50
|
+
add_part SignPart.new(cleartext_mail, options)
|
51
|
+
add_part Mail::Part.new(cleartext_mail)
|
52
|
+
|
53
|
+
content_type "multipart/signed; micalg=pgp-sha1; protocol=\"application/pgp-signature\"; boundary=#{boundary}"
|
54
|
+
body.preamble = options[:preamble] || "This is an OpenPGP/MIME signed message (RFC 4880 and 3156)"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.decrypt(encrypted_mail, options = {})
|
59
|
+
if encrypted_mime?(encrypted_mail)
|
60
|
+
decrypt_pgp_mime(encrypted_mail, options)
|
61
|
+
elsif encrypted_inline?(encrypted_mail)
|
62
|
+
decrypt_pgp_inline(encrypted_mail, options)
|
63
|
+
else
|
64
|
+
raise EncodingError, "Unsupported encryption format '#{encrypted_mail.content_type}'"
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.encrypted?(mail)
|
69
|
+
return true if encrypted_mime?(mail)
|
70
|
+
return true if encrypted_inline?(mail)
|
71
|
+
false
|
72
|
+
end
|
33
73
|
|
74
|
+
private
|
75
|
+
|
76
|
+
def self.construct_mail(cleartext_mail, options, &block)
|
34
77
|
Mail.new do
|
35
78
|
self.perform_deliveries = cleartext_mail.perform_deliveries
|
36
79
|
%w(from to cc bcc subject reply_to in_reply_to).each do |field|
|
@@ -39,16 +82,47 @@ module Mail
|
|
39
82
|
cleartext_mail.header.fields.each do |field|
|
40
83
|
header[field.name] = field.value if field.name =~ /^X-/
|
41
84
|
end
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
85
|
+
instance_eval &block
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# decrypts PGP/MIME (RFC 3156, section 4) encrypted mail
|
90
|
+
def self.decrypt_pgp_mime(encrypted_mail, options)
|
91
|
+
# MUST containt exactly two body parts
|
92
|
+
if encrypted_mail.parts.length != 2
|
93
|
+
raise EncodingError, "RFC 3136 mandates exactly two body parts, found '#{encrypted_mail.parts.length}'"
|
94
|
+
end
|
95
|
+
if !VersionPart.isVersionPart? encrypted_mail.parts[0]
|
96
|
+
raise EncodingError, "RFC 3136 first part not a valid version part '#{encrypted_mail.parts[0]}'"
|
47
97
|
end
|
98
|
+
Mail.new(DecryptedPart.new(encrypted_mail.parts[1], options))
|
48
99
|
end
|
49
100
|
|
50
|
-
|
51
|
-
|
101
|
+
# decrypts inline PGP encrypted mail
|
102
|
+
def self.decrypt_pgp_inline(encrypted_mail, options)
|
103
|
+
InlineDecryptedMessage.new(encrypted_mail, options)
|
104
|
+
end
|
105
|
+
|
106
|
+
# check if PGP/MIME (RFC 3156)
|
107
|
+
def self.encrypted_mime?(mail)
|
108
|
+
mail.has_content_type? &&
|
109
|
+
'multipart/encrypted' == mail.mime_type &&
|
110
|
+
'application/pgp-encrypted' == mail.content_type_parameters[:protocol]
|
111
|
+
end
|
112
|
+
|
113
|
+
# check if inline PGP (i.e. if any parts of the mail includes
|
114
|
+
# the PGP MESSAGE marker
|
115
|
+
def self.encrypted_inline?(mail)
|
116
|
+
return true if mail.body.include?('-----BEGIN PGP MESSAGE-----')
|
117
|
+
if mail.multipart?
|
118
|
+
mail.parts.each do |part|
|
119
|
+
return true if part.body.include?('-----BEGIN PGP MESSAGE-----')
|
120
|
+
return true if part.has_content_type? &&
|
121
|
+
/application\/(?:octet-stream|pgp-encrypted)/ =~ part.mime_type &&
|
122
|
+
/.*\.(?:pgp|gpg|asc)$/ =~ part.content_type_parameters[:name]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
false
|
52
126
|
end
|
53
127
|
end
|
54
128
|
end
|
data/test/action_mailer_test.rb
CHANGED
@@ -11,6 +11,16 @@ class MyMailer < ActionMailer::Base
|
|
11
11
|
mail subject: 'encrypted', body: 'encrypted mail', gpg: {encrypt: true}
|
12
12
|
end
|
13
13
|
|
14
|
+
def signed
|
15
|
+
mail from: 'jane@foo.bar',
|
16
|
+
to: 'joe@foo.bar',
|
17
|
+
subject: 'signed',
|
18
|
+
body: 'signed mail',
|
19
|
+
gpg: {
|
20
|
+
sign: true,
|
21
|
+
password: 'abc'
|
22
|
+
}
|
23
|
+
end
|
14
24
|
|
15
25
|
end
|
16
26
|
|
@@ -42,5 +52,19 @@ class ActionMailerTest < Test::Unit::TestCase
|
|
42
52
|
assert_equal 'encrypted mail', m.body.to_s
|
43
53
|
end
|
44
54
|
|
55
|
+
should "send signed mail" do
|
56
|
+
assert m = MyMailer.signed
|
57
|
+
assert true == m.gpg[:sign]
|
58
|
+
m.deliver
|
59
|
+
assert_equal 1, @emails.size
|
60
|
+
assert delivered = @emails.first
|
61
|
+
assert_equal 'signed', delivered.subject
|
62
|
+
assert_equal 2, delivered.parts.size
|
63
|
+
assert sign_part = delivered.parts.detect{|p| p.content_type =~ /signature\.asc/}
|
64
|
+
GPGME::Crypto.new.verify(sign_part.body.to_s, signed_text: m.encoded) do |sig|
|
65
|
+
assert true == sig.valid?
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
45
69
|
end
|
46
70
|
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'mail/gpg/decrypted_part'
|
3
|
+
require 'mail/gpg/encrypted_part'
|
4
|
+
|
5
|
+
class DecryptedPartTest < Test::Unit::TestCase
|
6
|
+
context 'DecryptedPart' do
|
7
|
+
setup do
|
8
|
+
@mail = Mail.new do
|
9
|
+
to 'jane@foo.bar'
|
10
|
+
from 'joe@foo.bar'
|
11
|
+
subject 'test'
|
12
|
+
body 'i am unencrypted'
|
13
|
+
end
|
14
|
+
@part = Mail::Gpg::EncryptedPart.new(@mail, { :sign => true, :password => 'abc' })
|
15
|
+
end
|
16
|
+
|
17
|
+
should 'decrypt' do
|
18
|
+
assert mail = Mail::Gpg::DecryptedPart.new(@part, { :password => 'abc' })
|
19
|
+
assert mail == @mail
|
20
|
+
assert mail.message_id == @mail.message_id
|
21
|
+
assert mail.message_id != @part.message_id
|
22
|
+
end
|
23
|
+
|
24
|
+
should 'decrypt and verify' do
|
25
|
+
assert mail = Mail::Gpg::DecryptedPart.new(@part, { :verify => true, :password => 'abc' })
|
26
|
+
assert mail == @mail
|
27
|
+
assert mail.message_id == @mail.message_id
|
28
|
+
assert mail.message_id != @part.message_id
|
29
|
+
end
|
30
|
+
|
31
|
+
should 'raise encoding error for non gpg mime type' do
|
32
|
+
part = Mail::Part.new(@part)
|
33
|
+
part.content_type = 'text/plain'
|
34
|
+
assert_raise(EncodingError) { Mail::Gpg::DecryptedPart.new(part) }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/test/encrypted_part_test.rb
CHANGED
@@ -1,8 +1,14 @@
|
|
1
1
|
require 'test_helper'
|
2
2
|
require 'mail/gpg/encrypted_part'
|
3
|
-
require 'pry-nav'
|
4
3
|
|
5
4
|
class EncryptedPartTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
def check_key_list(keys)
|
7
|
+
assert_equal 1, keys.size
|
8
|
+
assert_equal GPGME::Key, keys.first.class
|
9
|
+
assert_equal 'jane@foo.bar', keys.first.email
|
10
|
+
end
|
11
|
+
|
6
12
|
context 'EncryptedPart' do
|
7
13
|
setup do
|
8
14
|
mail = Mail.new do
|
@@ -14,24 +20,18 @@ class EncryptedPartTest < Test::Unit::TestCase
|
|
14
20
|
@part = Mail::Gpg::EncryptedPart.new(mail)
|
15
21
|
end
|
16
22
|
|
17
|
-
def check_key_list(keys)
|
18
|
-
assert_equal 1, keys.size
|
19
|
-
assert_equal GPGME::Key, keys.first.class
|
20
|
-
assert_equal 'jane@foo.bar', keys.first.email
|
21
|
-
end
|
22
|
-
|
23
23
|
context 'with email address' do
|
24
24
|
setup do
|
25
25
|
@email = 'jane@foo.bar'
|
26
26
|
end
|
27
27
|
|
28
28
|
should 'resolve email to gpg keys' do
|
29
|
-
assert keys =
|
29
|
+
assert keys = Mail::Gpg::GpgmeHelper.send(:keys_for_data, @email)
|
30
30
|
check_key_list keys
|
31
31
|
end
|
32
32
|
|
33
33
|
should 'resolve emails to gpg keys' do
|
34
|
-
assert keys =
|
34
|
+
assert keys = Mail::Gpg::GpgmeHelper.send(:keys_for_data, [@email])
|
35
35
|
check_key_list keys
|
36
36
|
end
|
37
37
|
|
@@ -43,11 +43,11 @@ class EncryptedPartTest < Test::Unit::TestCase
|
|
43
43
|
end
|
44
44
|
|
45
45
|
should 'resolve single id gpg keys' do
|
46
|
-
assert keys =
|
46
|
+
assert keys = Mail::Gpg::GpgmeHelper.send(:keys_for_data, @key_id)
|
47
47
|
check_key_list keys
|
48
48
|
end
|
49
49
|
should 'resolve id list to gpg keys' do
|
50
|
-
assert keys =
|
50
|
+
assert keys = Mail::Gpg::GpgmeHelper.send(:keys_for_data, [@key_id])
|
51
51
|
check_key_list keys
|
52
52
|
end
|
53
53
|
end
|
@@ -58,11 +58,11 @@ class EncryptedPartTest < Test::Unit::TestCase
|
|
58
58
|
end
|
59
59
|
|
60
60
|
should 'resolve single id gpg keys' do
|
61
|
-
assert keys =
|
61
|
+
assert keys = Mail::Gpg::GpgmeHelper.send(:keys_for_data, @key_fpr)
|
62
62
|
check_key_list keys
|
63
63
|
end
|
64
64
|
should 'resolve id list to gpg keys' do
|
65
|
-
assert keys =
|
65
|
+
assert keys = Mail::Gpg::GpgmeHelper.send(:keys_for_data, [@key_fpr])
|
66
66
|
check_key_list keys
|
67
67
|
end
|
68
68
|
end
|
@@ -75,7 +75,7 @@ class EncryptedPartTest < Test::Unit::TestCase
|
|
75
75
|
end
|
76
76
|
|
77
77
|
should 'resolve to gpg keys' do
|
78
|
-
assert keys =
|
78
|
+
assert keys = Mail::Gpg::GpgmeHelper.send(:keys_for_data, @emails, @key_data)
|
79
79
|
check_key_list keys
|
80
80
|
end
|
81
81
|
end
|
data/test/gpg_test.rb
CHANGED
@@ -17,16 +17,38 @@ class GpgTest < Test::Unit::TestCase
|
|
17
17
|
assert_equal 'application/pgp-encrypted; charset=UTF-8', v_part.content_type
|
18
18
|
|
19
19
|
assert_equal 'application/octet-stream; name=encrypted.asc',
|
20
|
-
|
20
|
+
enc_part.content_type
|
21
21
|
end
|
22
22
|
|
23
23
|
|
24
|
-
def check_content
|
25
|
-
assert enc =
|
24
|
+
def check_content(mail = @mail, encrypted = @encrypted)
|
25
|
+
assert enc = encrypted.parts.last
|
26
26
|
assert clear = GPGME::Crypto.new.decrypt(enc.to_s, password: 'abc').to_s
|
27
27
|
assert_match /encrypt me/, clear
|
28
|
+
assert_equal mail.to_s, clear
|
28
29
|
end
|
29
30
|
|
31
|
+
def check_signature(mail = @mail, signed = @signed)
|
32
|
+
assert signature = signed.parts.detect{|p| p.content_type =~ /signature\.asc/}.body.to_s
|
33
|
+
GPGME::Crypto.new.verify(signature, signed_text: mail.encoded) do |sig|
|
34
|
+
assert true == sig.valid?
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def check_mime_structure_signed(mail = @mail, signed = @signed)
|
39
|
+
assert_equal 2, signed.parts.size
|
40
|
+
sign_part, orig_part = signed.parts
|
41
|
+
|
42
|
+
assert_equal 'application/pgp-signature; name=signature.asc', sign_part.content_type
|
43
|
+
assert_equal orig_part.content_type, @mail.content_type
|
44
|
+
end
|
45
|
+
|
46
|
+
def check_headers_signed(mail = @mail, signed = @signed)
|
47
|
+
assert_equal mail.to, signed.to
|
48
|
+
assert_equal mail.cc, signed.cc
|
49
|
+
assert_equal mail.bcc, signed.bcc
|
50
|
+
assert_equal mail.subject, signed.subject
|
51
|
+
end
|
30
52
|
|
31
53
|
context "gpg installation" do
|
32
54
|
should "have keys for jane and joe" do
|
@@ -35,6 +57,104 @@ class GpgTest < Test::Unit::TestCase
|
|
35
57
|
end
|
36
58
|
end
|
37
59
|
|
60
|
+
context "gpg signed" do
|
61
|
+
setup do
|
62
|
+
@mail = Mail.new do
|
63
|
+
to 'joe@foo.bar'
|
64
|
+
from 'jane@foo.bar'
|
65
|
+
subject 'test test'
|
66
|
+
body 'sign me!'
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
context 'simple mail' do
|
71
|
+
setup do
|
72
|
+
@signed = Mail::Gpg.sign(@mail, password: 'abc')
|
73
|
+
end
|
74
|
+
|
75
|
+
should 'have same recipients and subject' do
|
76
|
+
check_headers_signed
|
77
|
+
end
|
78
|
+
|
79
|
+
should 'have proper gpgmime structure' do
|
80
|
+
check_mime_structure_signed
|
81
|
+
end
|
82
|
+
|
83
|
+
should 'have correct signature' do
|
84
|
+
check_signature
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'mail with custom header' do
|
89
|
+
setup do
|
90
|
+
@mail.header['X-Custom-Header'] = 'custom value'
|
91
|
+
@signed = Mail::Gpg.sign(@mail, password: 'abc')
|
92
|
+
end
|
93
|
+
|
94
|
+
should 'have same recipients and subject' do
|
95
|
+
check_headers_signed
|
96
|
+
end
|
97
|
+
|
98
|
+
should 'have proper gpgmime structure' do
|
99
|
+
check_mime_structure_signed
|
100
|
+
end
|
101
|
+
|
102
|
+
should 'have correct signature' do
|
103
|
+
check_signature
|
104
|
+
end
|
105
|
+
|
106
|
+
should 'preserve customer header values' do
|
107
|
+
assert_equal 'custom value', @signed.header['X-Custom-Header'].to_s
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
context 'mail with multiple recipients' do
|
112
|
+
setup do
|
113
|
+
@mail.bcc 'jane@foo.bar'
|
114
|
+
@signed = Mail::Gpg.sign(@mail, password: 'abc')
|
115
|
+
end
|
116
|
+
|
117
|
+
should 'have same recipients and subject' do
|
118
|
+
check_headers_signed
|
119
|
+
end
|
120
|
+
|
121
|
+
should 'have proper gpgmime structure' do
|
122
|
+
check_mime_structure_signed
|
123
|
+
end
|
124
|
+
|
125
|
+
should 'have correct signature' do
|
126
|
+
check_signature
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
context 'multipart mail' do
|
131
|
+
setup do
|
132
|
+
@mail.add_file 'Rakefile'
|
133
|
+
@signed = Mail::Gpg.sign(@mail, password: 'abc')
|
134
|
+
end
|
135
|
+
|
136
|
+
should 'have same recipients and subject' do
|
137
|
+
check_headers_signed
|
138
|
+
end
|
139
|
+
|
140
|
+
should 'have proper gpgmime structure' do
|
141
|
+
check_mime_structure_signed
|
142
|
+
end
|
143
|
+
|
144
|
+
should 'have correct signature' do
|
145
|
+
check_signature
|
146
|
+
end
|
147
|
+
|
148
|
+
should 'have multiple parts in original content' do
|
149
|
+
assert original_part = @signed.parts.last
|
150
|
+
assert original_part.multipart?
|
151
|
+
assert_equal 2, original_part.parts.size
|
152
|
+
assert_match /sign me!/, original_part.parts.first.body.to_s
|
153
|
+
assert_match /Rakefile/, original_part.parts.last.content_disposition
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
38
158
|
context "gpg encrypted" do
|
39
159
|
|
40
160
|
setup do
|
@@ -63,7 +183,34 @@ class GpgTest < Test::Unit::TestCase
|
|
63
183
|
check_content
|
64
184
|
end
|
65
185
|
|
186
|
+
should 'decrypt' do
|
187
|
+
assert mail = Mail::Gpg.decrypt(@encrypted, { :password => 'abc' })
|
188
|
+
assert mail == @mail
|
189
|
+
end
|
66
190
|
end
|
191
|
+
|
192
|
+
context 'simple mail (signed)' do
|
193
|
+
setup do
|
194
|
+
@encrypted = Mail::Gpg.encrypt(@mail, { :sign => true, :password => 'abc' })
|
195
|
+
end
|
196
|
+
|
197
|
+
should 'have same recipients and subject' do
|
198
|
+
check_headers
|
199
|
+
end
|
200
|
+
|
201
|
+
should 'have proper gpgmime structure' do
|
202
|
+
check_mime_structure
|
203
|
+
end
|
204
|
+
|
205
|
+
should 'have correctly encrypted content' do
|
206
|
+
check_content
|
207
|
+
end
|
208
|
+
|
209
|
+
should 'decrypt and verify' do
|
210
|
+
assert mail = Mail::Gpg.decrypt(@encrypted, { :verify => true, :password => 'abc' })
|
211
|
+
assert mail == @mail
|
212
|
+
end
|
213
|
+
end
|
67
214
|
|
68
215
|
context 'mail with custom header' do
|
69
216
|
setup do
|
@@ -86,6 +233,11 @@ class GpgTest < Test::Unit::TestCase
|
|
86
233
|
should 'preserve customer header values' do
|
87
234
|
assert_equal 'custom value', @encrypted.header['X-Custom-Header'].to_s
|
88
235
|
end
|
236
|
+
|
237
|
+
should 'decrypt' do
|
238
|
+
assert mail = Mail::Gpg.decrypt(@encrypted, { :password => 'abc' })
|
239
|
+
assert mail == @mail
|
240
|
+
end
|
89
241
|
end
|
90
242
|
|
91
243
|
context 'mail with multiple recipients' do
|
@@ -110,6 +262,10 @@ class GpgTest < Test::Unit::TestCase
|
|
110
262
|
assert encrypted_body = @encrypted.parts.last.to_s
|
111
263
|
end
|
112
264
|
|
265
|
+
should 'decrypt' do
|
266
|
+
assert mail = Mail::Gpg.decrypt(@encrypted, { :password => 'abc' })
|
267
|
+
assert mail == @mail
|
268
|
+
end
|
113
269
|
end
|
114
270
|
|
115
271
|
context 'multipart mail' do
|
@@ -139,6 +295,12 @@ class GpgTest < Test::Unit::TestCase
|
|
139
295
|
assert_match /encrypt me/, m.parts.first.body.to_s
|
140
296
|
assert_match /Rakefile/, m.parts.last.content_disposition
|
141
297
|
end
|
298
|
+
|
299
|
+
should 'decrypt' do
|
300
|
+
assert mail = Mail::Gpg.decrypt(@encrypted, { :password => 'abc' })
|
301
|
+
assert mail == @mail
|
302
|
+
assert mail.parts[1] == @mail.parts[1]
|
303
|
+
end
|
142
304
|
end
|
143
305
|
end
|
144
306
|
end
|
data/test/gpghome/random_seed
CHANGED
Binary file
|
data/test/hkp_test.rb
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'hkp'
|
3
|
+
|
4
|
+
class HkpTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
context "keyserver setup" do
|
7
|
+
|
8
|
+
context "with url specified" do
|
9
|
+
|
10
|
+
setup do
|
11
|
+
@hkp = Hkp.new("hkp://my-key-server.net")
|
12
|
+
end
|
13
|
+
|
14
|
+
should "use specified keyserver" do
|
15
|
+
assert url = @hkp.instance_variable_get("@keyserver")
|
16
|
+
assert_equal "hkp://my-key-server.net", url
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
|
21
|
+
context "without url specified" do
|
22
|
+
|
23
|
+
setup do
|
24
|
+
@hkp = Hkp.new
|
25
|
+
end
|
26
|
+
|
27
|
+
should "have found a non-empty keyserver" do
|
28
|
+
assert url = @hkp.instance_variable_get("@keyserver")
|
29
|
+
assert !url.blank?
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
# test cases for PGP inline messages (i.e. non-mime)
|
4
|
+
class InlineDecryptedMessageTest < Test::Unit::TestCase
|
5
|
+
|
6
|
+
context "InlineDecryptedMessage" do
|
7
|
+
|
8
|
+
setup do
|
9
|
+
(@mails = Mail::TestMailer.deliveries).clear
|
10
|
+
@mail = Mail.new do
|
11
|
+
to 'jane@foo.bar'
|
12
|
+
from 'joe@foo.bar'
|
13
|
+
subject 'test'
|
14
|
+
body 'i am unencrypted'
|
15
|
+
gpg encrypt: false
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
context "inline message" do
|
20
|
+
should "decrypt body" do
|
21
|
+
mail = Mail.new(@mail)
|
22
|
+
mail.body = InlineDecryptedMessageTest.encrypt(mail, mail.body.to_s)
|
23
|
+
|
24
|
+
assert !mail.multipart?
|
25
|
+
assert mail.encrypted?
|
26
|
+
assert decrypted = mail.decrypt(:password => 'abc')
|
27
|
+
assert decrypted == @mail
|
28
|
+
assert !decrypted.encrypted?
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context "attachment message" do
|
33
|
+
should "decrypt attachment" do
|
34
|
+
rakefile = File.open('Rakefile') { |file| file.read }
|
35
|
+
mail = Mail.new(@mail)
|
36
|
+
mail.content_type = 'multipart/mixed'
|
37
|
+
mail.body = ''
|
38
|
+
mail.part do |p|
|
39
|
+
p.content_type 'application/octet-stream; name=Rakefile.pgp'
|
40
|
+
p.content_transfer_encoding Mail::Encodings::Base64
|
41
|
+
p.content_disposition 'attachment; filename="Rakefile.pgp"'
|
42
|
+
p.body Mail::Encodings::Base64::encode(InlineDecryptedMessageTest.encrypt(mail, rakefile, false))
|
43
|
+
end
|
44
|
+
|
45
|
+
assert mail.multipart?
|
46
|
+
assert mail.encrypted?
|
47
|
+
assert decrypted = mail.decrypt(:password => 'abc')
|
48
|
+
assert !decrypted.encrypted?
|
49
|
+
check_headers(@mail, decrypted)
|
50
|
+
assert_equal 1, decrypted.parts.length
|
51
|
+
assert /application\/octet-stream; (?:charset=UTF-8; )?name=Rakefile/ =~ decrypted.parts[0].content_type
|
52
|
+
assert_equal 'attachment; filename=Rakefile', decrypted.parts[0].content_disposition
|
53
|
+
assert_equal rakefile, decrypted.parts[0].body.decoded
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context "cleartext body and encrypted attachment message" do
|
58
|
+
should "decrypt attachment" do
|
59
|
+
rakefile = File.open('Rakefile') { |file| file.read }
|
60
|
+
mail = Mail.new(@mail)
|
61
|
+
mail.content_type = 'multipart/mixed'
|
62
|
+
mail.part do |p|
|
63
|
+
p.content_type 'application/octet-stream; name=Rakefile.pgp'
|
64
|
+
p.content_transfer_encoding Mail::Encodings::Base64
|
65
|
+
p.content_disposition 'attachment; filename="Rakefile.pgp"'
|
66
|
+
p.body Mail::Encodings::Base64::encode(InlineDecryptedMessageTest.encrypt(mail, rakefile, false))
|
67
|
+
end
|
68
|
+
|
69
|
+
assert mail.multipart?
|
70
|
+
assert mail.encrypted?
|
71
|
+
assert decrypted = mail.decrypt(:password => 'abc')
|
72
|
+
assert !decrypted.encrypted?
|
73
|
+
check_headers(@mail, decrypted)
|
74
|
+
assert_equal 2, decrypted.parts.length
|
75
|
+
assert_equal @mail.body, decrypted.parts[0].body.to_s
|
76
|
+
assert /application\/octet-stream; (?:charset=UTF-8; )?name=Rakefile/ =~ decrypted.parts[1].content_type
|
77
|
+
assert_equal 'attachment; filename=Rakefile', decrypted.parts[1].content_disposition
|
78
|
+
assert_equal rakefile, decrypted.parts[1].body.decoded
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
context "encrypted body and attachment message" do
|
83
|
+
should "decrypt" do
|
84
|
+
rakefile = File.open('Rakefile') { |file| file.read }
|
85
|
+
mail = Mail.new(@mail)
|
86
|
+
mail.content_type = 'multipart/mixed'
|
87
|
+
mail.body = InlineDecryptedMessageTest.encrypt(mail, mail.body.to_s)
|
88
|
+
mail.part do |p|
|
89
|
+
p.content_type 'application/octet-stream; name=Rakefile.pgp'
|
90
|
+
p.content_transfer_encoding Mail::Encodings::Base64
|
91
|
+
p.content_disposition 'attachment; filename="Rakefile.pgp"'
|
92
|
+
p.body Mail::Encodings::Base64::encode(InlineDecryptedMessageTest.encrypt(mail, rakefile, false))
|
93
|
+
end
|
94
|
+
|
95
|
+
assert mail.multipart?
|
96
|
+
assert mail.encrypted?
|
97
|
+
assert decrypted = mail.decrypt(:password => 'abc')
|
98
|
+
assert !decrypted.encrypted?
|
99
|
+
check_headers(@mail, decrypted)
|
100
|
+
assert_equal 2, decrypted.parts.length
|
101
|
+
assert_equal @mail.body, decrypted.parts[0].body.to_s
|
102
|
+
assert /application\/octet-stream; (?:charset=UTF-8; )?name=Rakefile/ =~ decrypted.parts[1].content_type
|
103
|
+
assert_equal 'attachment; filename=Rakefile', decrypted.parts[1].content_disposition
|
104
|
+
assert_equal rakefile, decrypted.parts[1].body.decoded
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def self.encrypt(mail, plain, armor = true)
|
110
|
+
GPGME::Crypto.new.encrypt(plain,
|
111
|
+
password: 'abc',
|
112
|
+
recipients: mail.to,
|
113
|
+
sign: true,
|
114
|
+
signers: mail.from,
|
115
|
+
armor: armor).to_s
|
116
|
+
end
|
117
|
+
|
118
|
+
def check_headers(expected, actual)
|
119
|
+
assert_equal expected.to, actual.to
|
120
|
+
assert_equal expected.cc, actual.cc
|
121
|
+
assert_equal expected.bcc, actual.bcc
|
122
|
+
assert_equal expected.subject, actual.subject
|
123
|
+
assert_equal expected.message_id, actual.message_id
|
124
|
+
assert_equal expected.date, actual.date
|
125
|
+
end
|
126
|
+
end
|
data/test/message_test.rb
CHANGED
@@ -23,10 +23,46 @@ class MessageTest < Test::Unit::TestCase
|
|
23
23
|
assert_equal 1, @mails.size
|
24
24
|
assert m = @mails.first
|
25
25
|
assert_equal 'test', m.subject
|
26
|
+
assert !m.encrypted?
|
26
27
|
assert_equal 'i am unencrypted', m.body.to_s
|
27
28
|
end
|
29
|
+
|
30
|
+
should "raise encoding error" do
|
31
|
+
assert_equal 1, @mails.size
|
32
|
+
assert m = @mails.first
|
33
|
+
assert_equal 'test', m.subject
|
34
|
+
assert_raises(EncodingError){
|
35
|
+
m.decrypt(:password => 'abc')
|
36
|
+
}
|
37
|
+
end
|
28
38
|
end
|
29
39
|
|
40
|
+
context "with gpg signing only" do
|
41
|
+
setup do
|
42
|
+
@mail.gpg sign: true, password: 'abc'
|
43
|
+
end
|
44
|
+
|
45
|
+
context "" do
|
46
|
+
setup do
|
47
|
+
@mail.deliver
|
48
|
+
end
|
49
|
+
|
50
|
+
should "deliver signed mail" do
|
51
|
+
assert_equal 1, @mails.size
|
52
|
+
assert m = @mails.first
|
53
|
+
assert_equal 'test', m.subject
|
54
|
+
assert !m.encrypted?
|
55
|
+
assert m.multipart?
|
56
|
+
assert sign_part = m.parts.last
|
57
|
+
assert m = Mail::Message.new(m.parts.last)
|
58
|
+
assert !m.multipart?
|
59
|
+
GPGME::Crypto.new.verify(sign_part.body.to_s, signed_text: @mail.encoded) do |sig|
|
60
|
+
assert true == sig.valid?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
30
66
|
context "with gpg turned on" do
|
31
67
|
setup do
|
32
68
|
@mail.gpg encrypt: true
|
@@ -60,12 +96,37 @@ class MessageTest < Test::Unit::TestCase
|
|
60
96
|
assert m = @mails.first
|
61
97
|
assert_equal 'test', m.subject
|
62
98
|
assert m.multipart?
|
99
|
+
assert m.encrypted?
|
63
100
|
assert enc_part = m.parts.last
|
64
101
|
assert clear = GPGME::Crypto.new.decrypt(enc_part.body.to_s, password: 'abc').to_s
|
65
102
|
assert m = Mail::Message.new(clear)
|
66
103
|
assert !m.multipart?
|
67
104
|
assert_equal 'i am unencrypted', m.body.to_s
|
68
105
|
end
|
106
|
+
|
107
|
+
should "decrypt" do
|
108
|
+
assert_equal 1, @mails.size
|
109
|
+
assert m = @mails.first
|
110
|
+
assert_equal 'test', m.subject
|
111
|
+
assert m.multipart?
|
112
|
+
assert m.encrypted?
|
113
|
+
assert decrypted = m.decrypt(:password => 'abc')
|
114
|
+
assert decrypted == @mail
|
115
|
+
end
|
116
|
+
|
117
|
+
should "raise bad passphrase on decrypt" do
|
118
|
+
assert_equal 1, @mails.size
|
119
|
+
assert m = @mails.first
|
120
|
+
assert_equal 'test', m.subject
|
121
|
+
# incorrect passphrase
|
122
|
+
assert_raises(GPGME::Error::BadPassphrase){
|
123
|
+
m.decrypt(:password => 'incorrect')
|
124
|
+
}
|
125
|
+
# no passphrase
|
126
|
+
assert_raises(GPGME::Error::BadPassphrase){
|
127
|
+
m.decrypt
|
128
|
+
}
|
129
|
+
end
|
69
130
|
end
|
70
131
|
end
|
71
132
|
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'mail/gpg/sign_part'
|
2
|
+
|
3
|
+
class SignPartTest < Test::Unit::TestCase
|
4
|
+
context 'SignPart' do
|
5
|
+
setup do
|
6
|
+
mail = Mail.new do
|
7
|
+
to 'jane@foo.bar'
|
8
|
+
from 'joe@foo.bar'
|
9
|
+
subject 'test'
|
10
|
+
body 'i am unsigned'
|
11
|
+
end
|
12
|
+
@part = Mail::Gpg::SignPart.new(mail)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'mail/gpg/version_part'
|
3
|
+
|
4
|
+
class VersionPartTest < Test::Unit::TestCase
|
5
|
+
context 'VersionPart' do
|
6
|
+
|
7
|
+
should 'roundtrip successfully' do
|
8
|
+
part = Mail::Gpg::VersionPart.new()
|
9
|
+
assert Mail::Gpg::VersionPart.isVersionPart?(part)
|
10
|
+
end
|
11
|
+
|
12
|
+
should 'return false for non gpg mime type' do
|
13
|
+
part = Mail::Gpg::VersionPart.new()
|
14
|
+
part.content_type = 'text/plain'
|
15
|
+
assert !Mail::Gpg::VersionPart.isVersionPart?(part)
|
16
|
+
end
|
17
|
+
|
18
|
+
should 'return false for empty body' do
|
19
|
+
part = Mail::Gpg::VersionPart.new()
|
20
|
+
part.body = nil
|
21
|
+
assert !Mail::Gpg::VersionPart.isVersionPart?(part)
|
22
|
+
end
|
23
|
+
|
24
|
+
should 'return false for foul body' do
|
25
|
+
part = Mail::Gpg::VersionPart.new()
|
26
|
+
part.body = 'non gpg body'
|
27
|
+
assert !Mail::Gpg::VersionPart.isVersionPart?(part)
|
28
|
+
end
|
29
|
+
|
30
|
+
should 'return true for body with extra content' do
|
31
|
+
part = Mail::Gpg::VersionPart.new()
|
32
|
+
part.body = "#{part.body} extra content"
|
33
|
+
assert Mail::Gpg::VersionPart.isVersionPart?(part)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mail-gpg
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-
|
12
|
+
date: 2013-11-06 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: mail
|
@@ -145,15 +145,20 @@ files:
|
|
145
145
|
- lib/hkp.rb
|
146
146
|
- lib/mail-gpg.rb
|
147
147
|
- lib/mail/gpg.rb
|
148
|
+
- lib/mail/gpg/decrypted_part.rb
|
148
149
|
- lib/mail/gpg/delivery_handler.rb
|
149
150
|
- lib/mail/gpg/encrypted_part.rb
|
151
|
+
- lib/mail/gpg/gpgme_helper.rb
|
152
|
+
- lib/mail/gpg/inline_decrypted_message.rb
|
150
153
|
- lib/mail/gpg/message_patch.rb
|
151
154
|
- lib/mail/gpg/rails.rb
|
152
155
|
- lib/mail/gpg/rails/action_mailer_base_patch.rb
|
156
|
+
- lib/mail/gpg/sign_part.rb
|
153
157
|
- lib/mail/gpg/version.rb
|
154
158
|
- lib/mail/gpg/version_part.rb
|
155
159
|
- mail-gpg.gemspec
|
156
160
|
- test/action_mailer_test.rb
|
161
|
+
- test/decrypted_part_test.rb
|
157
162
|
- test/encrypted_part_test.rb
|
158
163
|
- test/gpg_test.rb
|
159
164
|
- test/gpghome/pubring.gpg
|
@@ -161,8 +166,12 @@ files:
|
|
161
166
|
- test/gpghome/random_seed
|
162
167
|
- test/gpghome/secring.gpg
|
163
168
|
- test/gpghome/trustdb.gpg
|
169
|
+
- test/hkp_test.rb
|
170
|
+
- test/inline_decrypted_message_test.rb
|
164
171
|
- test/message_test.rb
|
172
|
+
- test/sign_part_test.rb
|
165
173
|
- test/test_helper.rb
|
174
|
+
- test/version_part_test.rb
|
166
175
|
homepage: https://github.com/jkraemer/mail-gpg
|
167
176
|
licenses:
|
168
177
|
- MIT
|
@@ -190,6 +199,7 @@ specification_version: 3
|
|
190
199
|
summary: GPG/MIME encryption plugin for the Ruby Mail Library
|
191
200
|
test_files:
|
192
201
|
- test/action_mailer_test.rb
|
202
|
+
- test/decrypted_part_test.rb
|
193
203
|
- test/encrypted_part_test.rb
|
194
204
|
- test/gpg_test.rb
|
195
205
|
- test/gpghome/pubring.gpg
|
@@ -197,5 +207,9 @@ test_files:
|
|
197
207
|
- test/gpghome/random_seed
|
198
208
|
- test/gpghome/secring.gpg
|
199
209
|
- test/gpghome/trustdb.gpg
|
210
|
+
- test/hkp_test.rb
|
211
|
+
- test/inline_decrypted_message_test.rb
|
200
212
|
- test/message_test.rb
|
213
|
+
- test/sign_part_test.rb
|
201
214
|
- test/test_helper.rb
|
215
|
+
- test/version_part_test.rb
|