1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
|
# Mail::Gpg [](https://travis-ci.org/jkraemer/mail-gpg)
This gem adds GPG/MIME encryption capabilities to the [Ruby Mail
Library](https://github.com/mikel/mail)
For maximum interoperability the gem also supports *decryption* of messages using the non-standard 'PGP-Inline' method
as for example supported in the Mozilla Enigmail OpenPGP plugin.
There may still be GPG encrypted messages that can not be handled by the library, as there are some legacy formats used in the
wild as described in this [Know your PGP implementation](http://binblog.info/2008/03/12/know-your-pgp-implementation/) blog.
## Installation
Add this line to your application's Gemfile:
```ruby
gem 'mail-gpg'
```
And then execute:
$ bundle
Or install it yourself as:
$ gem install mail-gpg
## Usage
### Encrypting / Signing
Construct your Mail object as usual and specify you want it to be encrypted
with the gpg method:
```ruby
Mail.new do
to 'jane@doe.net'
from 'john@doe.net'
subject 'gpg test'
body "encrypt me!"
add_file "some_attachment.zip"
# encrypt message, no signing
gpg encrypt: true
# encrypt and sign message with sender's private key, using the given
# passphrase to decrypt the key
gpg encrypt: true, sign: true, password: 'secret'
# encrypt and sign message using a different key
gpg encrypt: true, sign_as: 'joe@otherdomain.com', password: 'secret'
# encrypt and sign message and use a callback function to provide the
# passphrase.
gpg encrypt: true, sign_as: 'joe@otherdomain.com',
passphrase_callback: ->(obj, uid_hint, passphrase_info, prev_was_bad, fd){puts "Enter passphrase for #{passphrase_info}: "; (IO.for_fd(fd, 'w') << readline.chomp).flush }
end.deliver
```
Make sure all recipients' public keys are present in your local gpg keychain.
You will get errors in case encryption is not possible due to missing keys.
If you collect public key data from your users, you can specify the ascii
armored key data for recipients using the `:keys` option like this:
```ruby
johns_key = <<-END
-----BEGIN PGP PUBLIC KEY BLOCK-----
Version: GnuPG v1.4.12 (GNU/Linux)
mQGiBEk39msRBADw1ExmrLD1OUMdfvA7cnVVYTC7CyqfNvHUVuuBDhV7azs
....
END
Mail.new do
to 'john@foo.bar'
gpg encrypt: true, keys: { 'john@foo.bar' => johns_key }
end
```
The key will then be imported before actually trying to encrypt/send the mail.
In theory you only need to specify the key once like that, however doing it
every time does not hurt as gpg is clever enough to recognize known keys, only
updating it's db when necessary.
Note: Mail-Gpg in version 0.4 and up is more strict regarding the keys option:
if it is present, only key material from there (either given as key data like
above, or as key id, key fingerprint or `GPGMe::Key` object if they have been
imported before) will be used. Keys already present in the local keychain for
any of the recipients that are not explicitly mentioned in the `keys` hash will
be ignored.
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.
### Decrypting
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:
```ruby
mail = Mail.first
mail.subject # subject is never encrypted
if mail.encrypted?
# decrypt using your private key, protected by the given passphrase
plaintext_mail = mail.decrypt(:password => 'abc')
# the plaintext_mail, is a full Mail::Message object, just decrypted
end
```
Set the `:verify` option to `true` when calling `decrypt` to decrypt *and* verify signatures.
A `GPGME::Error::BadPassphrase` will be raised if the password for the private key is incorrect.
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.
Please note that in case of a multipart/alternative-message where both parts are inline-encrypted, the HTML-part will be dropped during decryption. Handling the HTML-part would require parsing HTML and guessing about the decrypted contents, which is brittle and out of the scope of this library. If you need the HTML-part of multipart/alternative-messages, use pgp/mime-encryption.
### Signing only
Just leave the `:encrypt` option out or pass `encrypt: false`, i.e.
```ruby
Mail.new do
to 'jane@doe.net'
gpg sign: true
end.deliver
```
### Verify signature(s)
Receive the mail as usual. Check if it is signed using the `signed?` method. Check the signature of the mail with the `signature_valid?` method:
```ruby
mail = Mail.first
if !mail.encrypted? && mail.signed?
verified = mail.verify
puts "signature(s) valid: #{verified.signature_valid?}"
puts "message signed by: #{verified.signatures.map{|sig|sig.from}.join("\n")}"
end
```
Note that for encrypted mails the signatures can not be checked using these
methods. For encrypted mails the `:verify` option for the `decrypt` operation
must be used instead:
```ruby
if mail.encrypted?
decrypted = mail.decrypt(verify: true, password: 's3cr3t')
puts "signature(s) valid: #{decrypted.signature_valid?}"
puts "message signed by: #{decrypted.signatures.map{|sig|sig.from}.join("\n")}"
end
```
It's important to actually use the information contained in the `signatures`
array to check if the message really has been signed by the person that you (or
your users) think is the sender - usually by comparing the key id of the
signature with the key id of the expected sender.
### Key import from public key servers
The Hkp class can be used to lookup and import public keys from public key servers.
You can specify the keyserver url when initializing the class:
```ruby
hkp = Hkp.new("hkp://my-key-server.de")
```
Or, if you want to override how ssl certificates should be treated in case of
TLS-secured keyservers (the default is `VERIFY_PEER`):
```ruby
hkp = Hkp.new(keyserver: "hkps://another.key-server.com",
ssl_verify_mode: OpenSSL::SSL::VERIFY_NONE)
```
If no port is specified in hkp or hkps URIs (as in the examples above), port
11371 will be used for hkp and port 443 for hkps URIs. Standard `http` or
`https` URIs with or without explicitly set ports work as well.
If no url is given, this gem will try to determine the default keyserver
url from the system's gpg config (using `gpgconf` if available or by
parsing the `gpg.conf` file). As a last resort, the server-pool at
`http://pool.sks-keyservers.net:11371` will be used.
Lookup key ids by searching the keyserver for an email address
```ruby
hkp.search('jane@doe.net')
```
You can lookup (and import) a specific key by its id:
```ruby
key = hkp.fetch(id)
GPGME::Key.import(key)
# or do both in one step
hkp.fetch_and_import(id)
```
## Rails / ActionMailer integration
```ruby
class MyMailer < ActionMailer::Base
default from: 'baz@bar.com'
def some_mail
mail to: 'foo@bar.com', subject: 'subject!', gpg: { encrypt: true }
end
end
```
The gpg option takes the same arguments as outlined above for the
Mail::Message#gpg method.
## Passwords and GnuPG versions >= 2.x
GnuPG versions >= 2.x require the use of gpg-agent for key-handling. That's a problem for using password-protected keys non-interactively, because gpg-agent doesn't read from file-descriptors (which is the usual way to non-interactively provide passwords with GnuPG 1.x).
With GnuPG 2.x you have two options to provide passwords to gpg-agent:
1. Implement a pinentry-kind-of program that speaks the assuan-protocol and configure gpg-agent to use it.
2. Run gpg-preset-passphrase and allow gpg-agent to read preset passwords.
The second options is somewhat easier and is described below.
Note: You *don't* need this if your key is *not* protected with a password.
To feed a password into gpg-agent run this code early in your program:
```ruby
# The next two lines need adaption, obviously.
fpr = fingerprint_of_key_to_unlock
passphrase = gpg_passphrase_for_key
# You may copy&paste the rest of this block unchanged. Maybe you want to change the error-handling, though.
ENV['GPG_AGENT_INFO'] = `eval $(gpg-agent --allow-preset-passphrase --daemon) && echo $GPG_AGENT_INFO`
`gpgconf --list-dir`.match(/libexecdir:(.*)/)
gppbin = File.join($1, 'gpg-preset-passphrase')
Open3.popen3(gppbin, '--preset', fpr) do |stdin, stdout, stderr|
stdin.puts passphrase
err = stderr.readlines
$stderr.puts err if ! err.to_s.empty?
end
# Hook to kill our gpg-agent when script finishes.
Signal.trap(0, proc { Process.kill('TERM', ENV['GPG_AGENT_INFO'].split(':')[1]) })
```
## Running the tests
bundle exec rake
Test cases use a mock gpghome located in `test/gpghome` in order to not mess
around with your personal gpg keychain.
Password for the test PGP private keys is `abc`
## Todo
* signature verification for received mails with inline PGP
* on the fly import of recipients' keys from public key servers based on email address or key id
* handle encryption errors due to missing keys - maybe return a list of failed
recipients
* add some setup code to help initialize a basic keychain directory with
public/private keypair.
## Contributing
1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request
## Credits
Thanks to:
* [Planio GmbH](https://plan.io) for sponsoring the ongoing maintenance and development of this library
* [morten-andersen](https://github.com/morten-andersen) for implementing decryption support for PGP/MIME and inline encrypted messages
* [FewKinG](https://github.com/FewKinG) for implementing the sign only feature and keyserver url lookup
* [Fup Duck](https://github.com/duckdalbe) for various tweaks and fixes
|