mail-gpg 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +5 -5
- data/.github/workflows/test.yml +41 -0
- data/History.txt +21 -0
- data/README.md +1 -1
- data/lib/hkp.rb +14 -5
- data/lib/mail/gpg/inline_decrypted_message.rb +7 -1
- data/lib/mail/gpg/inline_signed_message.rb +7 -0
- data/lib/mail/gpg/version.rb +1 -1
- data/mail-gpg.gemspec +1 -0
- data/test/hkp_test.rb +219 -1
- data/test/inline_decrypted_message_test.rb +47 -0
- data/test/inline_signed_message_test.rb +45 -0
- data/test/message_test.rb +22 -22
- data/test/test_helper.rb +29 -10
- metadata +21 -20
- data/.travis.yml +0 -21
- data/test/gpghome/gpg-agent.conf +0 -2
- data/test/gpghome/pubring.gpg +0 -0
- data/test/gpghome/pubring.gpg~ +0 -0
- data/test/gpghome/random_seed +0 -0
- data/test/gpghome/secring.gpg +0 -0
- data/test/gpghome/trustdb.gpg +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
-
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3acca2c34af794c59ff717e0f29d27b26ed1d1e095dd388a6238e0ef31366644
|
|
4
|
+
data.tar.gz: c1161da992e780ecf3c6bea38977c9b8d0c86b2a5530f2095e398f868e7a70c9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ac9858c3c4838fa909cec047658a37945fa37ebf08a6b8a005ca336aab4543caed549f4a4c54a614195084772b9baa728978e2db80f2e1582767ea9a888ac743
|
|
7
|
+
data.tar.gz: b082facfd7d4c84a62e40bcf3aaba4866121daa0a65d8a962164031f7989eca572b5811187fb8f41dfb4948dbd7191aea4f1cce44d768c488721334d70073223
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
include:
|
|
17
|
+
- ruby-version: "2.7.8"
|
|
18
|
+
rails-version: "5.2.8.1"
|
|
19
|
+
- ruby-version: "3.2"
|
|
20
|
+
rails-version: "8.0"
|
|
21
|
+
- ruby-version: "3.4"
|
|
22
|
+
rails-version: "8.0"
|
|
23
|
+
|
|
24
|
+
# picked up by the Gemfile: gem 'actionmailer', "~> #{ENV['RAILS']}"
|
|
25
|
+
env:
|
|
26
|
+
RAILS: ${{ matrix.rails-version }}
|
|
27
|
+
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v4
|
|
30
|
+
|
|
31
|
+
- name: Install GPG
|
|
32
|
+
run: sudo apt-get update && sudo apt-get install -y gnupg
|
|
33
|
+
|
|
34
|
+
- name: Set up Ruby ${{ matrix.ruby-version }}
|
|
35
|
+
uses: ruby/setup-ruby@v1
|
|
36
|
+
with:
|
|
37
|
+
ruby-version: ${{ matrix.ruby-version }}
|
|
38
|
+
bundler-cache: true
|
|
39
|
+
|
|
40
|
+
- name: Run tests
|
|
41
|
+
run: bundle exec rake
|
data/History.txt
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
== 0.4.6 2026-08-13
|
|
2
|
+
|
|
3
|
+
* fix double transfer-decoding of inline signed / encrypted mails: the
|
|
4
|
+
rebuilt message kept the original Content-Transfer-Encoding header while
|
|
5
|
+
its body was set to the already decoded (verified / decrypted) text, so
|
|
6
|
+
reading the body would corrupt it (e.g. base64-decode plain text)
|
|
7
|
+
* modernize test setup: RSA 2048 test keys generated without passphrase
|
|
8
|
+
protection (current GnuPG refuses to sign with the old DSA 1024 keys, and
|
|
9
|
+
interactive pinentry prompts no longer get in the way); bad passphrase
|
|
10
|
+
handling is now tested with a dedicated protected key via loopback
|
|
11
|
+
pinentry
|
|
12
|
+
* CI runs on GitHub Actions with Ruby 2.7.8 / Rails 5.2.8.1 and
|
|
13
|
+
Ruby 3.2 / 3.4 with Rails 8
|
|
14
|
+
|
|
15
|
+
== 0.4.5 2026-01-20
|
|
16
|
+
|
|
17
|
+
* fix HKP client for Ruby 3.0+ (replaced deprecated URI.escape/decode)
|
|
18
|
+
* change default keyserver from defunct pool.sks-keyservers.net to
|
|
19
|
+
keyserver.ubuntu.com
|
|
20
|
+
* fix HKP client redirect handling
|
|
21
|
+
|
|
1
22
|
== 0.4.4 2020-07-30
|
|
2
23
|
|
|
3
24
|
* preserve Content-ID header of signed parts (relevant for inline images in
|
data/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# Mail::Gpg [](https://github.com/jkraemer/mail-gpg/actions/workflows/test.yml)
|
|
2
2
|
|
|
3
3
|
This gem adds GPG/MIME encryption capabilities to the [Ruby Mail
|
|
4
4
|
Library](https://github.com/mikel/mail)
|
data/lib/hkp.rb
CHANGED
|
@@ -49,7 +49,16 @@ class Hkp
|
|
|
49
49
|
if redirect_depth >= MAX_REDIRECTS
|
|
50
50
|
raise TooManyRedirects
|
|
51
51
|
else
|
|
52
|
-
|
|
52
|
+
location = URI.parse(response['location'])
|
|
53
|
+
if location.host && (location.host != @host || location.port != @port)
|
|
54
|
+
# Cross-host redirect - create new client for the target server
|
|
55
|
+
redirect_client = Client.new(response['location'], ssl_verify_mode: @ssl_verify_mode)
|
|
56
|
+
return redirect_client.get(location.request_uri, redirect_depth + 1)
|
|
57
|
+
else
|
|
58
|
+
# Same-host redirect (relative path or same host)
|
|
59
|
+
redirect_path = location.respond_to?(:request_uri) ? location.request_uri : response['location']
|
|
60
|
+
return get(redirect_path, redirect_depth + 1)
|
|
61
|
+
end
|
|
53
62
|
end
|
|
54
63
|
else
|
|
55
64
|
raise InvalidResponse, response.code
|
|
@@ -65,7 +74,7 @@ class Hkp
|
|
|
65
74
|
if String === options
|
|
66
75
|
options = { keyserver: options }
|
|
67
76
|
end
|
|
68
|
-
@keyserver = options.delete(:keyserver) || lookup_keyserver || '
|
|
77
|
+
@keyserver = options.delete(:keyserver) || lookup_keyserver || 'hkp://keyserver.ubuntu.com'
|
|
69
78
|
@options = { raise_errors: true }.merge options
|
|
70
79
|
end
|
|
71
80
|
|
|
@@ -83,7 +92,7 @@ class Hkp
|
|
|
83
92
|
# and what info they return besides the key id
|
|
84
93
|
def search(name)
|
|
85
94
|
[].tap do |results|
|
|
86
|
-
result = hkp_client.get "/pks/lookup?options=mr&search=#{URI.
|
|
95
|
+
result = hkp_client.get "/pks/lookup?op=index&options=mr&search=#{URI.encode_www_form_component(name)}"
|
|
87
96
|
|
|
88
97
|
result.each_line do |l|
|
|
89
98
|
components = l.strip.split(':')
|
|
@@ -101,7 +110,7 @@ class Hkp
|
|
|
101
110
|
|
|
102
111
|
# returns the key data as returned from the server as a string
|
|
103
112
|
def fetch(id)
|
|
104
|
-
result = hkp_client.get "/pks/lookup?options=mr&
|
|
113
|
+
result = hkp_client.get "/pks/lookup?op=get&options=mr&search=0x#{URI.encode_www_form_component(id)}"
|
|
105
114
|
return clean_key(result) if result
|
|
106
115
|
|
|
107
116
|
rescue Exception
|
|
@@ -142,7 +151,7 @@ class Hkp
|
|
|
142
151
|
def lookup_keyserver
|
|
143
152
|
url = nil
|
|
144
153
|
if res = exec_cmd("gpgconf --list-options gpgs 2>&1 | grep keyserver 2>&1")
|
|
145
|
-
url = URI.
|
|
154
|
+
url = URI.decode_www_form_component(res.split(":").last.split("\"").last.strip)
|
|
146
155
|
elsif res = exec_cmd("gpg --gpgconf-list 2>&1 | grep gpgconf-gpg.conf 2>&1")
|
|
147
156
|
conf_file = res.split(":").last.split("\"").last.strip
|
|
148
157
|
if res = exec_cmd("cat #{conf_file} 2>&1 | grep ^keyserver 2>&1")
|
|
@@ -59,7 +59,9 @@ module Mail
|
|
|
59
59
|
else
|
|
60
60
|
p.content_type part.content_type
|
|
61
61
|
p.content_transfer_encoding part.content_transfer_encoding
|
|
62
|
-
|
|
62
|
+
# keep the raw (still encoded) body to match the transfer
|
|
63
|
+
# encoding copied above - body.to_s would decode it
|
|
64
|
+
p.body part.body.raw_source
|
|
63
65
|
end
|
|
64
66
|
end
|
|
65
67
|
end
|
|
@@ -72,6 +74,10 @@ module Mail
|
|
|
72
74
|
cipher_mail.header.fields.each do |field|
|
|
73
75
|
header[field.name] = field.value
|
|
74
76
|
end
|
|
77
|
+
# the decrypted body set below is not transfer encoded, so an
|
|
78
|
+
# encoding copied over from the original mail would lead to
|
|
79
|
+
# double decoding
|
|
80
|
+
header['Content-Transfer-Encoding'] = nil
|
|
75
81
|
body decrypted.to_s
|
|
76
82
|
verify_result decrypted.verify_result if options[:verify] && '' != decrypted
|
|
77
83
|
end
|
|
@@ -17,6 +17,10 @@ module Mail
|
|
|
17
17
|
success, vr = GpgmeHelper.inline_verify(signed_text, options)
|
|
18
18
|
p = VerifiedPart.new(part)
|
|
19
19
|
if success
|
|
20
|
+
# the body set here is already decoded, so a transfer
|
|
21
|
+
# encoding copied over from the original part would lead to
|
|
22
|
+
# double decoding
|
|
23
|
+
p.header['Content-Transfer-Encoding'] = nil
|
|
20
24
|
p.body self.class.strip_inline_signature signed_text
|
|
21
25
|
end
|
|
22
26
|
p.verify_result vr
|
|
@@ -33,6 +37,9 @@ module Mail
|
|
|
33
37
|
signed_mail.header.fields.each do |field|
|
|
34
38
|
header[field.name] = field.value
|
|
35
39
|
end
|
|
40
|
+
# the body set below is already decoded, so a transfer encoding
|
|
41
|
+
# copied over from the original mail would lead to double decoding
|
|
42
|
+
header['Content-Transfer-Encoding'] = nil
|
|
36
43
|
signed_text = signed_mail.body.to_s
|
|
37
44
|
success, vr = GpgmeHelper.inline_verify(signed_text, options)
|
|
38
45
|
if success
|
data/lib/mail/gpg/version.rb
CHANGED
data/mail-gpg.gemspec
CHANGED
data/test/hkp_test.rb
CHANGED
|
@@ -4,7 +4,7 @@ require 'hkp'
|
|
|
4
4
|
|
|
5
5
|
class HkpTest < MailGpgTestCase
|
|
6
6
|
|
|
7
|
-
context "
|
|
7
|
+
context "hkp client" do
|
|
8
8
|
{
|
|
9
9
|
"http://pool.sks-keyservers.net:11371" => {
|
|
10
10
|
host: 'pool.sks-keyservers.net',
|
|
@@ -99,4 +99,222 @@ class HkpTest < MailGpgTestCase
|
|
|
99
99
|
|
|
100
100
|
end
|
|
101
101
|
|
|
102
|
+
context 'with mocked server' do
|
|
103
|
+
setup do
|
|
104
|
+
WebMock.disable_net_connect!
|
|
105
|
+
@keyserver = 'hkp://keys.example.com'
|
|
106
|
+
@hkp = Hkp.new(keyserver: @keyserver)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
teardown do
|
|
110
|
+
WebMock.allow_net_connect!
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
context 'search' do
|
|
114
|
+
should 'include op=index parameter in request' do
|
|
115
|
+
stub_request(:get, "http://keys.example.com:11371/pks/lookup")
|
|
116
|
+
.with(query: hash_including('op' => 'index'))
|
|
117
|
+
.to_return(status: 200, body: "info:1:1\npub:ABC123:1:2048:1234567890::\nuid:Test User <test@example.com>:1234567890::")
|
|
118
|
+
|
|
119
|
+
result = @hkp.search('test@example.com')
|
|
120
|
+
assert_requested :get, "http://keys.example.com:11371/pks/lookup",
|
|
121
|
+
query: hash_including('op' => 'index', 'search' => 'test@example.com')
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
should 'properly encode special characters in search query' do
|
|
125
|
+
stub_request(:get, /keys\.example\.com/)
|
|
126
|
+
.to_return(status: 200, body: "info:1:1\npub:ABC123:1:2048:1234567890::\n")
|
|
127
|
+
|
|
128
|
+
@hkp.search('test+user@example.com')
|
|
129
|
+
|
|
130
|
+
# Should use proper URL encoding, + should become %2B
|
|
131
|
+
assert_requested :get, "http://keys.example.com:11371/pks/lookup",
|
|
132
|
+
query: hash_including('search' => 'test+user@example.com')
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
should 'parse machine readable response' do
|
|
136
|
+
stub_request(:get, /keys\.example\.com/)
|
|
137
|
+
.to_return(status: 200, body: <<~RESPONSE)
|
|
138
|
+
info:1:2
|
|
139
|
+
pub:ABC123DEF456:1:2048:1234567890::
|
|
140
|
+
uid:Test User <test@example.com>:1234567890::
|
|
141
|
+
pub:789GHI012JKL:1:4096:1234567891::
|
|
142
|
+
uid:Another User <another@example.com>:1234567891::
|
|
143
|
+
RESPONSE
|
|
144
|
+
|
|
145
|
+
result = @hkp.search('example.com')
|
|
146
|
+
|
|
147
|
+
assert_equal 2, result.size
|
|
148
|
+
assert_equal 'ABC123DEF456', result[0][0]
|
|
149
|
+
assert_equal '789GHI012JKL', result[1][0]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
should 'return empty array when no keys found' do
|
|
153
|
+
stub_request(:get, /keys\.example\.com/)
|
|
154
|
+
.to_return(status: 200, body: "info:1:0\n")
|
|
155
|
+
|
|
156
|
+
result = @hkp.search('nonexistent@example.com')
|
|
157
|
+
|
|
158
|
+
assert_equal [], result
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
should 'raise error on server error when raise_errors is true' do
|
|
162
|
+
stub_request(:get, /keys\.example\.com/)
|
|
163
|
+
.to_return(status: 500, body: "Internal Server Error")
|
|
164
|
+
|
|
165
|
+
assert_raises(Hkp::InvalidResponse) do
|
|
166
|
+
@hkp.search('test@example.com')
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
should 'return nil on server error when raise_errors is false' do
|
|
171
|
+
hkp = Hkp.new(keyserver: @keyserver, raise_errors: false)
|
|
172
|
+
stub_request(:get, /keys\.example\.com/)
|
|
173
|
+
.to_return(status: 500, body: "Internal Server Error")
|
|
174
|
+
|
|
175
|
+
result = hkp.search('test@example.com')
|
|
176
|
+
|
|
177
|
+
assert_nil result
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
context 'fetch' do
|
|
182
|
+
should 'include op=get parameter in request' do
|
|
183
|
+
stub_request(:get, "http://keys.example.com:11371/pks/lookup")
|
|
184
|
+
.with(query: hash_including('op' => 'get'))
|
|
185
|
+
.to_return(status: 200, body: <<~KEY)
|
|
186
|
+
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
187
|
+
mQENBFxxxxxxx
|
|
188
|
+
-----END PGP PUBLIC KEY BLOCK-----
|
|
189
|
+
KEY
|
|
190
|
+
|
|
191
|
+
result = @hkp.fetch('ABC123')
|
|
192
|
+
assert_requested :get, "http://keys.example.com:11371/pks/lookup",
|
|
193
|
+
query: hash_including('op' => 'get', 'search' => '0xABC123')
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
should 'properly encode key id with special characters' do
|
|
197
|
+
stub_request(:get, /keys\.example\.com/)
|
|
198
|
+
.to_return(status: 200, body: <<~KEY)
|
|
199
|
+
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
200
|
+
mQENBFxxxxxxx
|
|
201
|
+
-----END PGP PUBLIC KEY BLOCK-----
|
|
202
|
+
KEY
|
|
203
|
+
|
|
204
|
+
# Key IDs shouldn't normally have special chars, but testing encoding anyway
|
|
205
|
+
@hkp.fetch('ABC+123')
|
|
206
|
+
|
|
207
|
+
assert_requested :get, "http://keys.example.com:11371/pks/lookup",
|
|
208
|
+
query: hash_including('search' => '0xABC+123')
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
should 'extract key from response' do
|
|
212
|
+
stub_request(:get, /keys\.example\.com/)
|
|
213
|
+
.to_return(status: 200, body: <<~RESPONSE)
|
|
214
|
+
Some header text
|
|
215
|
+
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
216
|
+
|
|
217
|
+
mQENBFxxxxxxx
|
|
218
|
+
=xxxx
|
|
219
|
+
-----END PGP PUBLIC KEY BLOCK-----
|
|
220
|
+
Some footer text
|
|
221
|
+
RESPONSE
|
|
222
|
+
|
|
223
|
+
result = @hkp.fetch('ABC123')
|
|
224
|
+
|
|
225
|
+
assert result.start_with?('-----BEGIN PGP PUBLIC KEY BLOCK-----')
|
|
226
|
+
assert result.end_with?('-----END PGP PUBLIC KEY BLOCK-----')
|
|
227
|
+
refute result.include?('Some header text')
|
|
228
|
+
refute result.include?('Some footer text')
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
should 'return nil when key not found' do
|
|
232
|
+
stub_request(:get, /keys\.example\.com/)
|
|
233
|
+
.to_return(status: 404, body: "Not Found")
|
|
234
|
+
|
|
235
|
+
hkp = Hkp.new(keyserver: @keyserver, raise_errors: false)
|
|
236
|
+
result = hkp.fetch('NONEXISTENT')
|
|
237
|
+
|
|
238
|
+
assert_nil result
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
should 'return nil when response has no valid key block' do
|
|
242
|
+
stub_request(:get, /keys\.example\.com/)
|
|
243
|
+
.to_return(status: 200, body: "No key here")
|
|
244
|
+
|
|
245
|
+
result = @hkp.fetch('ABC123')
|
|
246
|
+
|
|
247
|
+
assert_nil result
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
context 'redirects' do
|
|
252
|
+
should 'follow same-host redirects' do
|
|
253
|
+
stub_request(:get, "http://keys.example.com:11371/pks/lookup")
|
|
254
|
+
.with(query: hash_including('op' => 'index', 'search' => 'test'))
|
|
255
|
+
.to_return(status: 302, headers: { 'Location' => '/pks/lookup?op=index&options=mr&search=test&new=1' })
|
|
256
|
+
|
|
257
|
+
stub_request(:get, "http://keys.example.com:11371/pks/lookup")
|
|
258
|
+
.with(query: hash_including('op' => 'index', 'new' => '1'))
|
|
259
|
+
.to_return(status: 200, body: "info:1:1\npub:ABC123:1:2048:1234567890::\n")
|
|
260
|
+
|
|
261
|
+
result = @hkp.search('test')
|
|
262
|
+
assert_equal 1, result.size
|
|
263
|
+
assert_equal 'ABC123', result[0][0]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
should 'follow cross-host redirects' do
|
|
267
|
+
stub_request(:get, "http://keys.example.com:11371/pks/lookup")
|
|
268
|
+
.with(query: hash_including('op' => 'index'))
|
|
269
|
+
.to_return(status: 302, headers: { 'Location' => 'http://keys2.example.com/pks/lookup?op=index&options=mr&search=test' })
|
|
270
|
+
|
|
271
|
+
stub_request(:get, "http://keys2.example.com:80/pks/lookup")
|
|
272
|
+
.with(query: hash_including('op' => 'index'))
|
|
273
|
+
.to_return(status: 200, body: "info:1:1\npub:DEF456:1:2048:1234567890::\n")
|
|
274
|
+
|
|
275
|
+
result = @hkp.search('test')
|
|
276
|
+
assert_equal 1, result.size
|
|
277
|
+
assert_equal 'DEF456', result[0][0]
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
should 'raise TooManyRedirects after MAX_REDIRECTS' do
|
|
281
|
+
stub_request(:get, /keys\.example\.com/)
|
|
282
|
+
.to_return(status: 302, headers: { 'Location' => 'http://keys.example.com:11371/pks/lookup?op=index&search=test' })
|
|
283
|
+
|
|
284
|
+
assert_raises(Hkp::TooManyRedirects) do
|
|
285
|
+
@hkp.search('test')
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
context 'lookup_keyserver' do
|
|
292
|
+
setup do
|
|
293
|
+
WebMock.disable_net_connect!
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
teardown do
|
|
297
|
+
WebMock.allow_net_connect!
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
should 'decode percent-encoded keyserver URL from gpgconf' do
|
|
301
|
+
# Create a testable subclass that stubs exec_cmd
|
|
302
|
+
hkp_class = Class.new(Hkp) do
|
|
303
|
+
def exec_cmd(cmd)
|
|
304
|
+
if cmd.include?('gpgconf --list-options')
|
|
305
|
+
# Simulated gpgconf output with percent-encoded URL
|
|
306
|
+
# Format: name:flags:value (value may be quoted)
|
|
307
|
+
"keyserver:0:hkp%3A%2F%2Fkeys.example.com\n"
|
|
308
|
+
else
|
|
309
|
+
nil
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
hkp = hkp_class.new
|
|
315
|
+
keyserver = hkp.instance_variable_get('@keyserver')
|
|
316
|
+
assert_equal 'hkp://keys.example.com', keyserver
|
|
317
|
+
end
|
|
318
|
+
end
|
|
319
|
+
|
|
102
320
|
end
|
|
@@ -31,6 +31,24 @@ class InlineDecryptedMessageTest < MailGpgTestCase
|
|
|
31
31
|
assert sig.to_s=~ /Joe/
|
|
32
32
|
assert sig.valid?
|
|
33
33
|
end
|
|
34
|
+
|
|
35
|
+
should "decrypt base64 encoded body" do
|
|
36
|
+
mail = Mail.new(@mail)
|
|
37
|
+
cipher_text = InlineDecryptedMessageTest.encrypt(mail, mail.body.to_s)
|
|
38
|
+
mail.content_transfer_encoding 'base64'
|
|
39
|
+
mail.body Mail::Encodings::Base64.encode(cipher_text)
|
|
40
|
+
mail = Mail.new(mail.encoded)
|
|
41
|
+
|
|
42
|
+
assert !mail.multipart?
|
|
43
|
+
assert mail.encrypted?
|
|
44
|
+
assert decrypted = mail.decrypt(:password => 'abc', verify: true)
|
|
45
|
+
assert_equal 'i am unencrypted', decrypted.body.decoded,
|
|
46
|
+
"body was double decoded: #{decrypted.body.decoded.inspect}"
|
|
47
|
+
assert vr = decrypted.verify_result
|
|
48
|
+
assert sig = vr.signatures.first
|
|
49
|
+
assert sig.to_s=~ /Joe/
|
|
50
|
+
assert sig.valid?
|
|
51
|
+
end
|
|
34
52
|
end
|
|
35
53
|
|
|
36
54
|
context "multipart/alternative message" do
|
|
@@ -77,6 +95,35 @@ class InlineDecryptedMessageTest < MailGpgTestCase
|
|
|
77
95
|
end
|
|
78
96
|
end
|
|
79
97
|
|
|
98
|
+
context "base64 encoded cleartext part and encrypted attachment message" do
|
|
99
|
+
should "decrypt attachment and not double decode text part" do
|
|
100
|
+
rakefile = File.open('Rakefile') { |file| file.read }
|
|
101
|
+
mail = Mail.new(@mail)
|
|
102
|
+
mail.content_type = 'multipart/mixed'
|
|
103
|
+
mail.body = ''
|
|
104
|
+
mail.part do |p|
|
|
105
|
+
p.content_type 'text/plain'
|
|
106
|
+
p.content_transfer_encoding 'base64'
|
|
107
|
+
p.body Mail::Encodings::Base64.encode('i am unencrypted')
|
|
108
|
+
end
|
|
109
|
+
mail.part do |p|
|
|
110
|
+
p.content_type 'application/octet-stream; name=Rakefile.pgp'
|
|
111
|
+
p.content_transfer_encoding Mail::Encodings::Base64
|
|
112
|
+
p.content_disposition 'attachment; filename="Rakefile.pgp"'
|
|
113
|
+
p.body Mail::Encodings::Base64::encode(InlineDecryptedMessageTest.encrypt(mail, rakefile, false))
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
assert mail.multipart?
|
|
117
|
+
assert mail.encrypted?
|
|
118
|
+
assert decrypted = mail.decrypt(password: 'abc')
|
|
119
|
+
assert !decrypted.encrypted?
|
|
120
|
+
assert_equal 2, decrypted.parts.length
|
|
121
|
+
assert_equal 'i am unencrypted', decrypted.parts[0].body.decoded,
|
|
122
|
+
"part was double decoded: #{decrypted.parts[0].body.decoded.inspect}"
|
|
123
|
+
assert_equal rakefile, decrypted.parts[1].body.decoded
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
80
127
|
context "cleartext body and encrypted attachment message" do
|
|
81
128
|
should "decrypt and verify attachment" do
|
|
82
129
|
rakefile = File.open('Rakefile') { |file| file.read }
|
|
@@ -54,9 +54,54 @@ class InlineSignedMessageTest < MailGpgTestCase
|
|
|
54
54
|
assert !sig.valid?
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
should "verify base64 encoded body" do
|
|
58
|
+
mail = Mail.new(@mail)
|
|
59
|
+
signed_text = self.class.inline_sign(mail, mail.body.to_s)
|
|
60
|
+
mail.content_transfer_encoding 'base64'
|
|
61
|
+
mail.body Mail::Encodings::Base64.encode(signed_text)
|
|
62
|
+
mail = Mail.new(mail.encoded)
|
|
63
|
+
assert !mail.multipart?
|
|
64
|
+
assert mail.signed?
|
|
65
|
+
assert verified = mail.verify
|
|
66
|
+
assert verified.signature_valid?
|
|
67
|
+
assert verified.body.decoded.include?('i am unencrypted'),
|
|
68
|
+
"body was double decoded: #{verified.body.decoded.inspect}"
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
should "keep base64 encoded body readable when signature is invalid" do
|
|
72
|
+
mail = Mail.new(@mail)
|
|
73
|
+
signed_text = self.class.inline_sign(mail, mail.body.to_s).gsub /i am/, 'i was'
|
|
74
|
+
mail.content_transfer_encoding 'base64'
|
|
75
|
+
mail.body Mail::Encodings::Base64.encode(signed_text)
|
|
76
|
+
mail = Mail.new(mail.encoded)
|
|
77
|
+
assert !mail.multipart?
|
|
78
|
+
assert mail.signed?
|
|
79
|
+
assert verified = mail.verify
|
|
80
|
+
assert !verified.signature_valid?
|
|
81
|
+
assert verified.body.decoded.include?('i was unencrypted'),
|
|
82
|
+
"body was double decoded: #{verified.body.decoded.inspect}"
|
|
83
|
+
end
|
|
84
|
+
|
|
57
85
|
end
|
|
58
86
|
|
|
59
87
|
context "message with signed attachment" do
|
|
88
|
+
should "check base64 encoded attachment signature" do
|
|
89
|
+
mail = Mail.new(@mail)
|
|
90
|
+
mail.body = 'foobar'
|
|
91
|
+
signed_text = self.class.inline_sign(mail, 'sign me!')
|
|
92
|
+
mail.part do |p|
|
|
93
|
+
p.content_transfer_encoding 'base64'
|
|
94
|
+
p.body Mail::Encodings::Base64.encode(signed_text)
|
|
95
|
+
end
|
|
96
|
+
mail = Mail.new(mail.encoded)
|
|
97
|
+
assert mail.multipart?
|
|
98
|
+
assert mail.signed?
|
|
99
|
+
assert verified = mail.verify
|
|
100
|
+
assert verified.signature_valid?
|
|
101
|
+
assert verified.parts.last.body.decoded.include?('sign me!'),
|
|
102
|
+
"part was double decoded: #{verified.parts.last.body.decoded.inspect}"
|
|
103
|
+
end
|
|
104
|
+
|
|
60
105
|
should "check attachment signature" do
|
|
61
106
|
mail = Mail.new(@mail)
|
|
62
107
|
mail.body = 'foobar'
|
data/test/message_test.rb
CHANGED
|
@@ -53,8 +53,6 @@ class MessageTest < MailGpgTestCase
|
|
|
53
53
|
@mail.deliver
|
|
54
54
|
@signed = Mail.new @mails.first.to_s
|
|
55
55
|
@verified = @signed.verify
|
|
56
|
-
# Mail gem from 2.7.1 onwards converts "\n" to "\r\n"
|
|
57
|
-
@body = Mail::Utilities.to_crlf(@body)
|
|
58
56
|
end
|
|
59
57
|
|
|
60
58
|
should 'keep body unchanged' do
|
|
@@ -88,7 +86,7 @@ class MessageTest < MailGpgTestCase
|
|
|
88
86
|
body "and\nanother part euro €"
|
|
89
87
|
end
|
|
90
88
|
@mail.add_part p
|
|
91
|
-
#
|
|
89
|
+
# attachment bodies are returned as binary
|
|
92
90
|
@attachment_data = "this is\n € not an image".force_encoding(Encoding::BINARY)
|
|
93
91
|
@mail.attachments['test.jpg'] = { mime_type: 'image/jpeg',
|
|
94
92
|
content: @attachment_data }
|
|
@@ -106,7 +104,7 @@ class MessageTest < MailGpgTestCase
|
|
|
106
104
|
should 'have original three parts' do
|
|
107
105
|
assert_equal 3, @verified.parts.size
|
|
108
106
|
assert_equal 'i am unencrypted', @verified.parts[0].body.to_s
|
|
109
|
-
assert_equal "and\
|
|
107
|
+
assert_equal "and\nanother part euro €", @verified.parts[1].body.to_s.force_encoding('UTF-8')
|
|
110
108
|
assert attachment = @verified.parts[2]
|
|
111
109
|
assert attachment.attachment?
|
|
112
110
|
assert_equal "attachment; filename=test.jpg", attachment.content_disposition
|
|
@@ -141,8 +139,8 @@ class MessageTest < MailGpgTestCase
|
|
|
141
139
|
assert_equal 3, @mail.parts.size
|
|
142
140
|
assert_equal 3, @verified.parts.size
|
|
143
141
|
assert_equal 'i am unencrypted', @verified.parts[0].body.to_s
|
|
144
|
-
assert_equal "and\
|
|
145
|
-
assert_equal "and an\
|
|
142
|
+
assert_equal "and\nanother part euro €", @verified.parts[1].body.to_s.force_encoding('UTF-8')
|
|
143
|
+
assert_equal "and an\nHTML part €", @verified.parts[2].body.to_s.force_encoding('UTF-8')
|
|
146
144
|
end
|
|
147
145
|
end
|
|
148
146
|
|
|
@@ -227,7 +225,7 @@ class MessageTest < MailGpgTestCase
|
|
|
227
225
|
assert decrypted = m.decrypt(:password => 'abc', verify: true)
|
|
228
226
|
assert_equal 'test', decrypted.subject
|
|
229
227
|
assert decrypted == @mail
|
|
230
|
-
assert_equal "one\
|
|
228
|
+
assert_equal "one\neuro €", decrypted.body.to_s.force_encoding('UTF-8')
|
|
231
229
|
assert decrypted.signature_valid?
|
|
232
230
|
assert_equal 1, decrypted.signatures.size
|
|
233
231
|
end
|
|
@@ -300,24 +298,26 @@ class MessageTest < MailGpgTestCase
|
|
|
300
298
|
end
|
|
301
299
|
|
|
302
300
|
should "raise bad passphrase on decrypt" do
|
|
303
|
-
|
|
304
|
-
|
|
301
|
+
# patty's key is passphrase protected; loopback pinentry keeps
|
|
302
|
+
# gpg-agent from prompting interactively
|
|
303
|
+
mail = Mail.new do
|
|
304
|
+
from 'joe@foo.bar'
|
|
305
|
+
to 'patty@foo.bar'
|
|
306
|
+
subject 'test'
|
|
307
|
+
body 'i am unencrypted'
|
|
308
|
+
gpg encrypt: true
|
|
309
|
+
end
|
|
310
|
+
mail.deliver
|
|
311
|
+
assert m = @mails.last
|
|
305
312
|
assert_equal 'test', m.subject
|
|
313
|
+
assert m.encrypted?
|
|
306
314
|
# incorrect passphrase
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
# expected_exception = GPGME::Error::DecryptFailed
|
|
310
|
-
# I dont know why.
|
|
311
|
-
expected_exception = EOFError
|
|
312
|
-
else
|
|
313
|
-
expected_exception = GPGME::Error::BadPassphrase
|
|
314
|
-
end
|
|
315
|
-
assert_raises(expected_exception){
|
|
316
|
-
m.decrypt(:password => 'incorrect')
|
|
315
|
+
assert_raises(GPGME::Error::BadPassphrase){
|
|
316
|
+
m.decrypt(password: 'incorrect', pinentry_mode: GPGME::PINENTRY_MODE_LOOPBACK)
|
|
317
317
|
}
|
|
318
|
-
# no passphrase
|
|
319
|
-
assert_raises(
|
|
320
|
-
m.decrypt
|
|
318
|
+
# no passphrase - gpg gets no data from the loopback pinentry
|
|
319
|
+
assert_raises(GPGME::Error::NoData){
|
|
320
|
+
m.decrypt(pinentry_mode: GPGME::PINENTRY_MODE_LOOPBACK)
|
|
321
321
|
}
|
|
322
322
|
end
|
|
323
323
|
end
|
data/test/test_helper.rb
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
require 'open3'
|
|
2
2
|
require 'test/unit'
|
|
3
3
|
require 'shoulda/context'
|
|
4
|
+
require 'webmock/test_unit'
|
|
4
5
|
require 'mail-gpg'
|
|
5
6
|
require 'action_mailer'
|
|
6
7
|
require 'securerandom'
|
|
7
8
|
require 'byebug'
|
|
8
9
|
|
|
10
|
+
# Allow real connections by default, individual tests can disable
|
|
11
|
+
WebMock.allow_net_connect!
|
|
12
|
+
|
|
9
13
|
Mail.defaults do
|
|
10
14
|
delivery_method :test
|
|
11
15
|
end
|
|
@@ -67,31 +71,46 @@ class GPGTestUtils
|
|
|
67
71
|
def gen_keys
|
|
68
72
|
puts "setting up keydir #{@home}"
|
|
69
73
|
FileUtils.mkdir_p @home
|
|
70
|
-
(File.open(File.join(@home, "gpg-agent.conf"), "wb") << "allow-preset-passphrase\
|
|
74
|
+
(File.open(File.join(@home, "gpg-agent.conf"), "wb") << "allow-preset-passphrase\n").close
|
|
71
75
|
GPGME::Ctx.new do |gpg|
|
|
72
76
|
gpg.generate_key <<-END
|
|
73
77
|
<GnupgKeyParms format="internal">
|
|
74
|
-
|
|
75
|
-
Key-
|
|
76
|
-
|
|
77
|
-
Subkey-
|
|
78
|
+
%no-protection
|
|
79
|
+
Key-Type: RSA
|
|
80
|
+
Key-Length: 2048
|
|
81
|
+
Subkey-Type: RSA
|
|
82
|
+
Subkey-Length: 2048
|
|
78
83
|
Name-Real: Joe Tester
|
|
79
84
|
Name-Comment: with stupid passphrase
|
|
80
85
|
Name-Email: joe@foo.bar
|
|
81
86
|
Expire-Date: 0
|
|
82
|
-
Passphrase: abc
|
|
83
87
|
</GnupgKeyParms>
|
|
84
88
|
END
|
|
85
89
|
gpg.generate_key <<-END
|
|
86
90
|
<GnupgKeyParms format="internal">
|
|
87
|
-
|
|
88
|
-
Key-
|
|
89
|
-
|
|
90
|
-
Subkey-
|
|
91
|
+
%no-protection
|
|
92
|
+
Key-Type: RSA
|
|
93
|
+
Key-Length: 2048
|
|
94
|
+
Subkey-Type: RSA
|
|
95
|
+
Subkey-Length: 2048
|
|
91
96
|
Name-Real: Jane Doe
|
|
92
97
|
Name-Comment: with stupid passphrase
|
|
93
98
|
Name-Email: jane@foo.bar
|
|
94
99
|
Expire-Date: 0
|
|
100
|
+
</GnupgKeyParms>
|
|
101
|
+
END
|
|
102
|
+
# passphrase protected key for testing bad passphrase handling with
|
|
103
|
+
# loopback pinentry
|
|
104
|
+
gpg.generate_key <<-END
|
|
105
|
+
<GnupgKeyParms format="internal">
|
|
106
|
+
Key-Type: RSA
|
|
107
|
+
Key-Length: 2048
|
|
108
|
+
Subkey-Type: RSA
|
|
109
|
+
Subkey-Length: 2048
|
|
110
|
+
Name-Real: Patty Protected
|
|
111
|
+
Name-Comment: with stupid passphrase
|
|
112
|
+
Name-Email: patty@foo.bar
|
|
113
|
+
Expire-Date: 0
|
|
95
114
|
Passphrase: abc
|
|
96
115
|
</GnupgKeyParms>
|
|
97
116
|
END
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mail-gpg
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.4.
|
|
4
|
+
version: 0.4.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jens Kraemer
|
|
8
|
-
autorequire:
|
|
8
|
+
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-08-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: mail
|
|
@@ -134,6 +134,20 @@ dependencies:
|
|
|
134
134
|
- - "~>"
|
|
135
135
|
- !ruby/object:Gem::Version
|
|
136
136
|
version: '1.1'
|
|
137
|
+
- !ruby/object:Gem::Dependency
|
|
138
|
+
name: webmock
|
|
139
|
+
requirement: !ruby/object:Gem::Requirement
|
|
140
|
+
requirements:
|
|
141
|
+
- - "~>"
|
|
142
|
+
- !ruby/object:Gem::Version
|
|
143
|
+
version: '3.0'
|
|
144
|
+
type: :development
|
|
145
|
+
prerelease: false
|
|
146
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
147
|
+
requirements:
|
|
148
|
+
- - "~>"
|
|
149
|
+
- !ruby/object:Gem::Version
|
|
150
|
+
version: '3.0'
|
|
137
151
|
description: |-
|
|
138
152
|
GPG/MIME encryption plugin for the Ruby Mail Library
|
|
139
153
|
This tiny gem adds GPG capabilities to Mail::Message and ActionMailer::Base. Because privacy matters.
|
|
@@ -143,8 +157,8 @@ executables: []
|
|
|
143
157
|
extensions: []
|
|
144
158
|
extra_rdoc_files: []
|
|
145
159
|
files:
|
|
160
|
+
- ".github/workflows/test.yml"
|
|
146
161
|
- ".gitignore"
|
|
147
|
-
- ".travis.yml"
|
|
148
162
|
- Gemfile
|
|
149
163
|
- History.txt
|
|
150
164
|
- LICENSE.txt
|
|
@@ -176,12 +190,6 @@ files:
|
|
|
176
190
|
- test/decrypted_part_test.rb
|
|
177
191
|
- test/encrypted_part_test.rb
|
|
178
192
|
- test/gpg_test.rb
|
|
179
|
-
- test/gpghome/gpg-agent.conf
|
|
180
|
-
- test/gpghome/pubring.gpg
|
|
181
|
-
- test/gpghome/pubring.gpg~
|
|
182
|
-
- test/gpghome/random_seed
|
|
183
|
-
- test/gpghome/secring.gpg
|
|
184
|
-
- test/gpghome/trustdb.gpg
|
|
185
193
|
- test/gpgme_helper_test.rb
|
|
186
194
|
- test/hkp_test.rb
|
|
187
195
|
- test/inline_decrypted_message_test.rb
|
|
@@ -194,7 +202,7 @@ homepage: https://github.com/jkraemer/mail-gpg
|
|
|
194
202
|
licenses:
|
|
195
203
|
- MIT
|
|
196
204
|
metadata: {}
|
|
197
|
-
post_install_message:
|
|
205
|
+
post_install_message:
|
|
198
206
|
rdoc_options: []
|
|
199
207
|
require_paths:
|
|
200
208
|
- lib
|
|
@@ -209,9 +217,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
209
217
|
- !ruby/object:Gem::Version
|
|
210
218
|
version: '0'
|
|
211
219
|
requirements: []
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
signing_key:
|
|
220
|
+
rubygems_version: 3.5.22
|
|
221
|
+
signing_key:
|
|
215
222
|
specification_version: 4
|
|
216
223
|
summary: GPG/MIME encryption plugin for the Ruby Mail Library
|
|
217
224
|
test_files:
|
|
@@ -219,12 +226,6 @@ test_files:
|
|
|
219
226
|
- test/decrypted_part_test.rb
|
|
220
227
|
- test/encrypted_part_test.rb
|
|
221
228
|
- test/gpg_test.rb
|
|
222
|
-
- test/gpghome/gpg-agent.conf
|
|
223
|
-
- test/gpghome/pubring.gpg
|
|
224
|
-
- test/gpghome/pubring.gpg~
|
|
225
|
-
- test/gpghome/random_seed
|
|
226
|
-
- test/gpghome/secring.gpg
|
|
227
|
-
- test/gpghome/trustdb.gpg
|
|
228
229
|
- test/gpgme_helper_test.rb
|
|
229
230
|
- test/hkp_test.rb
|
|
230
231
|
- test/inline_decrypted_message_test.rb
|
data/.travis.yml
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
language: ruby
|
|
2
|
-
rvm:
|
|
3
|
-
- 2.6.3
|
|
4
|
-
- 2.5.5
|
|
5
|
-
- 2.4.6
|
|
6
|
-
env:
|
|
7
|
-
- RAILS=4.2.11.1 GPG_BIN=/usr/bin/gpg2
|
|
8
|
-
- RAILS=4.2.11.1 GPG_BIN=/usr/bin/gpg
|
|
9
|
-
- RAILS=5.2.3 GPG_BIN=/usr/bin/gpg2
|
|
10
|
-
- RAILS=5.2.3 GPG_BIN=/usr/bin/gpg
|
|
11
|
-
matrix:
|
|
12
|
-
exclude:
|
|
13
|
-
before_install:
|
|
14
|
-
- gem install bundler -v "~> 2.0"
|
|
15
|
-
- sudo apt install -y gnupg gnupg2
|
|
16
|
-
cache: bundler
|
|
17
|
-
dist: xenial
|
|
18
|
-
addons:
|
|
19
|
-
apt:
|
|
20
|
-
update: true
|
|
21
|
-
|
data/test/gpghome/gpg-agent.conf
DELETED
data/test/gpghome/pubring.gpg
DELETED
|
Binary file
|
data/test/gpghome/pubring.gpg~
DELETED
|
Binary file
|
data/test/gpghome/random_seed
DELETED
|
Binary file
|
data/test/gpghome/secring.gpg
DELETED
|
Binary file
|
data/test/gpghome/trustdb.gpg
DELETED
|
Binary file
|