mail-gpg 0.0.6 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|