root/lib/tmail_gpg.rb @ 7988a0ad
d32ee48a | Marc Dequenes | #--
|
|
364e4a96 | Marc Dequènes (Duck) | # Copyright (c) 2009-2011 Marc Dequènes (Duck) <Duck@DuckCorp.org>
|
|
d32ee48a | Marc Dequenes | #
|
|
# Permission is hereby granted, free of charge, to any person obtaining
|
|||
# a copy of this software and associated documentation files (the
|
|||
# "Software"), to deal in the Software without restriction, including
|
|||
# without limitation the rights to use, copy, modify, merge, publish,
|
|||
# distribute, sublicense, and/or sell copies of the Software, and to
|
|||
# permit persons to whom the Software is furnished to do so, subject to
|
|||
# the following conditions:
|
|||
#
|
|||
# The above copyright notice and this permission notice shall be
|
|||
# included in all copies or substantial portions of the Software.
|
|||
#
|
|||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|||
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|||
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|||
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|||
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|||
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|||
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|||
#
|
|||
#++
|
|||
d9a90899 | Marc Dequènes (Duck) | class String
|
|
def is_7bit?
|
|||
each_byte{|c| return false if c >= 128 }
|
|||
true
|
|||
end
|
|||
end
|
|||
0a2010c8 | Marc Dequenes | # Attempt to handle PGP/GPG features in a RFC3156-compliant way
|
|
#
|
|||
# notes: These methods have been designed to be able to sign, crypt,
|
|||
# or sign+crypt with RFC 1847 Encapsulation. There is no
|
|||
# support (yet?) for the combined method described in chapter
|
|||
# 6.2 of RFC3156.
|
|||
2891e0c2 | Marc Dequenes | module TMail
|
|
837fd85c | Marc Dequènes (Duck) | module GpgExtension
|
|
2891e0c2 | Marc Dequenes | def is_pgp_signed?
|
|
861ef12d | Marc Dequenes | content_type == "multipart/signed" and
|
|
header['content-type'].params['protocol'] == "application/pgp-signature" and
|
|||
parts.size == 2 and
|
|||
960c259e | Marc Dequenes | parts[1].content_type == "application/pgp-signature"
|
|
2891e0c2 | Marc Dequenes | end
|
|
c99df77c | Marc Dequènes (Duck) | def pgp_signature_part
|
|
2891e0c2 | Marc Dequenes | return nil unless is_pgp_signed?
|
|
parts[1].decoded
|
|||
end
|
|||
c99df77c | Marc Dequènes (Duck) | def pgp_signature
|
|
return nil unless is_pgp_signed?
|
|||
parts[1].body
|
|||
end
|
|||
b3a5479b | Marc Dequènes (Duck) | # don't forget lines may not be normalized
|
|
2891e0c2 | Marc Dequenes | def pgp_signed_part
|
|
return nil unless is_pgp_signed?
|
|||
b3a5479b | Marc Dequènes (Duck) | # remove final CRLF added to respect OpenPGP convention
|
|
# (see note in chapter 5 of RFC3156)
|
|||
signed_part = parts[0].raw.chomp
|
|||
return TMail::Mail.parse(signed_part)
|
|||
2891e0c2 | Marc Dequenes | end
|
|
def verify_pgp_signature
|
|||
return nil unless is_pgp_signed?
|
|||
037010aa | Marc Dequènes (Duck) | content = parts[0].to_rfc3156(true)
|
|
2891e0c2 | Marc Dequenes | sig = pgp_signature()
|
|
09b32d70 | Marc Dequenes | sigs_check = nil
|
|
960c259e | Marc Dequenes | GPGME.verify(sig, content) do |signature|
|
|
09b32d70 | Marc Dequenes | sigs_check ||= []
|
|
sigs_check << signature
|
|||
2891e0c2 | Marc Dequenes | end
|
|
09b32d70 | Marc Dequenes | sigs_check
|
|
2891e0c2 | Marc Dequenes | end
|
|
960c259e | Marc Dequenes | def is_pgp_encrypted?
|
|
861ef12d | Marc Dequenes | content_type == "multipart/encrypted" and
|
|
header['content-type'].params['protocol'] == "application/pgp-encrypted" and
|
|||
parts.size == 2 and
|
|||
960c259e | Marc Dequenes | parts[0].content_type == "application/pgp-encrypted" and
|
|
parts[1].content_type == "application/octet-stream"
|
|||
end
|
|||
def pgp_crypt_info
|
|||
return nil unless is_pgp_encrypted?
|
|||
a = parts[0].body.split("\n").collect{|l| l.chomp.split(": ") if l =~ /: / }.compact.flatten
|
|||
Hash[*a]
|
|||
end
|
|||
def pgp_encrypted_part
|
|||
return nil unless is_pgp_encrypted?
|
|||
parts[1].body
|
|||
end
|
|||
09b32d70 | Marc Dequenes | def pgp_decrypt(&passphrase_callback)
|
|
960c259e | Marc Dequenes | return nil unless is_pgp_encrypted?
|
|
protocol_version = pgp_crypt_info()["Version"].to_i
|
|||
raise NotImplementedError, "pgp-encrypted protocol version #{protocol_version} is not implemented" unless protocol_version == 1
|
|||
encrypted_data = pgp_encrypted_part()
|
|||
09b32d70 | Marc Dequenes | GPGME.decrypt(encrypted_data, {:passphrase_callback => method(:gpg_passphrase_callback_wrapper),
|
|
:passphrase_callback_value => passphrase_callback, :textmode => true})
|
|||
end
|
|||
c0979b3e | Marc Dequenes | def pgp_crypt(crypters_id)
|
|
crypters_id = [crypters_id] unless crypters_id.is_a? Array
|
|||
crypters = crypters_id.collect{|key_id| gpg_key(key_id, false) }
|
|||
GPGME.encrypt(crypters, self.to_s, {:armor => true, :always_trust => true})
|
|||
09b32d70 | Marc Dequenes | end
|
|
def pgp_sign(signers_id, &passphrase_callback)
|
|||
c0979b3e | Marc Dequenes | signers_id = [signers_id] unless signers_id.is_a? Array
|
|
0a2010c8 | Marc Dequenes | signers = signers_id.collect{|key_id| gpg_key(key_id, true) }
|
|
# we don't use GPGME.sign(), because we need to get operation information to get the hash_algo and compute the micalg parameter
|
|||
gpg = GPGME::Ctx.new({:signers => signers, :passphrase_callback => method(:gpg_passphrase_callback_wrapper),
|
|||
:passphrase_callback_value => passphrase_callback, :armor => true})
|
|||
gpg.add_signer(*signers)
|
|||
sig_data = GPGME::Data.new
|
|||
c99df77c | Marc Dequènes (Duck) | # adds a final CRLF to respect OpenPGP convention (see note in chapter 5 of RFC3156)
|
|
prepared_data = self.to_rfc3156 + "\r\n"
|
|||
d9a90899 | Marc Dequènes (Duck) | raise 'TMail ERROR: data to sign MUST be in 7bit format, using Quotes-Printable or Base64 encoding !' unless prepared_data.is_7bit?
|
|
c99df77c | Marc Dequènes (Duck) | gpg.sign(GPGME::Data.new(prepared_data), sig_data, GPGME::SIG_MODE_DETACH)
|
|
0a2010c8 | Marc Dequenes | hash_algo = GPGME.gpgme_op_sign_result(gpg).signatures.first.hash_algo
|
|
micalg = "pgp-" + GPGME.gpgme_hash_algo_name(hash_algo).downcase
|
|||
sig_data.seek(0, IO::SEEK_SET)
|
|||
c99df77c | Marc Dequènes (Duck) | ||
0a2010c8 | Marc Dequenes | {:signature => sig_data.read, :micalg => micalg}
|
|
09b32d70 | Marc Dequenes | end
|
|
037010aa | Marc Dequènes (Duck) | def to_rfc3156(untouched = false)
|
|
dcbc5f76 | Marc Dequènes (Duck) | # - using RAW part, without any decoding
|
|
# - remove last EOL due to MIME protocol
|
|||
# - remove lines containing only whitespaces
|
|||
# - remove trailing whitespaces
|
|||
# - properly convert all EOL to CRLF
|
|||
037010aa | Marc Dequènes (Duck) | txt = untouched ? self.raw : self.to_s
|
|
txt.chomp.split(/\r?\n/, -1).select{|l| not l =~ /^ +$/}.join("\n").gsub(/ +$/, "").gsub(/\r?\n/, "\r\n")
|
|||
960c259e | Marc Dequenes | end
|
|
c0979b3e | Marc Dequenes | def create_encrypted(crypters_id)
|
|
clear_data = build_intermediate_mail()
|
|||
encrypted_data = clear_data.pgp_crypt(crypters_id)
|
|||
# build properly encrypted mail
|
|||
# (preserving headers from original mail)
|
|||
mail = TMail::Mail.new
|
|||
mail.copy_headers_from(self)
|
|||
mail.set_content_type("multipart", "encrypted", {'boundary' => TMail.new_boundary, "protocol" => "application/pgp-encrypted"})
|
|||
mail.transfer_encoding = "7bit"
|
|||
mail['content-disposition'] = nil
|
|||
mail.parts.clear
|
|||
dcbc5f76 | Marc Dequènes (Duck) | mail.quoted_body = "This mail is a RFC3156 encrypted message.\n"
|
|
c0979b3e | Marc Dequenes | ||
# cryptographic info
|
|||
p_pgp = TMail::Mail.new
|
|||
p_pgp.set_content_type("application", "pgp-encrypted")
|
|||
p_pgp.transfer_encoding = "7bit"
|
|||
p_pgp.content_disposition = "inline"
|
|||
p_pgp.body = "Version: 1\n"
|
|||
mail.parts << p_pgp
|
|||
# encrypted message
|
|||
p_encrypted = TMail::Mail.new
|
|||
p_encrypted.set_content_type("application", "octet-stream")
|
|||
p_encrypted.transfer_encoding = "7bit"
|
|||
p_encrypted.content_disposition = "inline"
|
|||
p_encrypted.body = encrypted_data
|
|||
mail.parts << p_encrypted
|
|||
# store the calculated content, to be able to use parsing methods
|
|||
mail.write_back
|
|||
mail
|
|||
end
|
|||
def create_signed(signers_id)
|
|||
data = build_intermediate_mail()
|
|||
sign_data = data.pgp_sign(signers_id) do |uid_hint, passphrase_info, prev_was_bad|
|
|||
yield(uid_hint, passphrase_info, prev_was_bad)
|
|||
end
|
|||
# build properly signed mail
|
|||
# (preserving headers from original mail)
|
|||
mail = TMail::Mail.new
|
|||
mail.copy_headers_from(self)
|
|||
mail.set_content_type("multipart", "signed", {'boundary' => TMail.new_boundary, 'protocol' => "application/pgp-signature", 'micalg' => sign_data[:micalg]})
|
|||
mail.transfer_encoding = "7bit"
|
|||
mail['content-disposition'] = nil
|
|||
mail.parts.clear
|
|||
dcbc5f76 | Marc Dequènes (Duck) | mail.quoted_body = "This mail is a RFC3156 signed message.\n"
|
|
c0979b3e | Marc Dequenes | ||
# signed message
|
|||
p_signed = data
|
|||
mail.parts << p_signed
|
|||
# signature
|
|||
p_signature = TMail::Mail.new
|
|||
p_signature.set_content_type("application", "pgp-signature")
|
|||
p_signature.transfer_encoding = "7bit"
|
|||
p_signature.content_disposition = "inline"
|
|||
p_signature.body = sign_data[:signature]
|
|||
mail.parts << p_signature
|
|||
# store the calculated content, to be able to use parsing methods
|
|||
mail.write_back
|
|||
mail
|
|||
end
|
|||
def create_signed_and_encrypted(signers_id, crypters_id)
|
|||
create_signed(signers_id) do |uid_hint, passphrase_info, prev_was_bad|
|
|||
yield(uid_hint, passphrase_info, prev_was_bad)
|
|||
end.create_encrypted(crypters_id)
|
|||
end
|
|||
def copy_headers_from(mail)
|
|||
50f6ab1c | Marc Dequènes (Duck) | # deep clone
|
|
@header = Marshal.load(Marshal.dump(mail.header))
|
|||
c0979b3e | Marc Dequenes | end
|
|
2891e0c2 | Marc Dequenes | protected
|
|
c0979b3e | Marc Dequenes | def build_intermediate_mail
|
|
# build a fake mail to get the generated content to be crypted/signed
|
|||
fake_mail = TMail::Mail.new
|
|||
fake_mail['content-type'] = self['content-type'].to_s
|
|||
fake_mail.transfer_encoding = self.transfer_encoding if self.transfer_encoding
|
|||
fake_mail.content_disposition = self.content_disposition if self.content_disposition
|
|||
if self.multipart?
|
|||
self.each_part {|p| fake_mail.parts << p }
|
|||
end
|
|||
dcbc5f76 | Marc Dequènes (Duck) | fake_mail.quoted_body = self.quoted_body
|
|
c0979b3e | Marc Dequenes | ||
# store the calculated content, to be able to use parsing methods
|
|||
fake_mail.write_back
|
|||
fake_mail
|
|||
end
|
|||
0a2010c8 | Marc Dequenes | def gpg_key(fingerprint, secret = false)
|
|
09b32d70 | Marc Dequenes | gpg = GPGME::Ctx.new
|
|
0a2010c8 | Marc Dequenes | gpg.get_key(fingerprint, secret)
|
|
88b8e9a8 | Marc Dequènes (Duck) | rescue EOFError
|
|
raise "TMail ERROR: #{secret ? "secret" : "public"} key not found (#{fingerprint})"
|
|||
09b32d70 | Marc Dequenes | end
|
|
def gpg_passphrase_callback_wrapper(hook, uid_hint, passphrase_info, prev_was_bad, fd)
|
|||
io = IO.for_fd(fd, 'w')
|
|||
io.puts hook.call(uid_hint, passphrase_info, prev_was_bad)
|
|||
io.flush
|
|||
end
|
|||
2891e0c2 | Marc Dequenes | def raw
|
|
@port.read_all
|
|||
end
|
|||
end
|
|||
end
|
|||
837fd85c | Marc Dequènes (Duck) | ||
begin
|
|||
require 'gpgme' # >= 1.0.2 needed for :always_trust sign option
|
|||
TMail::Mail.send(:include, TMail::GpgExtension)
|
|||
c99df77c | Marc Dequènes (Duck) | rescue LoadError
|
|
837fd85c | Marc Dequènes (Duck) | end
|