root/lib/tmail_extra.rb @ 0a2010c8
0a2010c8 | Marc Dequenes | require 'gpgme' # >= 1.0.2 needed for :always_trust sign option
|
|
# 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
|
|
class Mail
|
|||
def is_pgp_signed?
|
|||
960c259e | Marc Dequenes | content_type == "multipart/signed" and parts.size == 2 and
|
|
parts[1].content_type == "application/pgp-signature"
|
|||
2891e0c2 | Marc Dequenes | end
|
|
def pgp_signature
|
|||
return nil unless is_pgp_signed?
|
|||
parts[1].decoded
|
|||
end
|
|||
def pgp_signed_part
|
|||
return nil unless is_pgp_signed?
|
|||
parts[0]
|
|||
end
|
|||
def verify_pgp_signature
|
|||
return nil unless is_pgp_signed?
|
|||
09b32d70 | Marc Dequenes | content = parts[0].to_rfc3156
|
|
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?
|
|
content_type == "multipart/encrypted" and parts.size == 2 and
|
|||
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
|
|||
def pgp_crypt(fingerprint)
|
|||
GPGME.encrypt([gpg_key(fingerprint)], self.to_s, {:armor => true, :always_trust => true})
|
|||
end
|
|||
def pgp_sign(signers_id, &passphrase_callback)
|
|||
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
|
|||
gpg.sign(GPGME::Data.new(self.to_rfc3156), sig_data, GPGME::SIG_MODE_NORMAL)
|
|||
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)
|
|||
{:signature => sig_data.read, :micalg => micalg}
|
|||
09b32d70 | Marc Dequenes | end
|
|
def to_rfc3156
|
|||
# using RAW part, without any decoding
|
|||
# remove last EOL due to MIME protocol and properly convert all EOL to CRLF
|
|||
raw.chomp.gsub(/\r?\n/, "\r\n")
|
|||
960c259e | Marc Dequenes | end
|
|
2891e0c2 | Marc Dequenes | protected
|
|
0a2010c8 | Marc Dequenes | def gpg_key(fingerprint, secret = false)
|
|
09b32d70 | Marc Dequenes | gpg = GPGME::Ctx.new
|
|
0a2010c8 | Marc Dequenes | gpg.get_key(fingerprint, secret)
|
|
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
|