I often find that loading long threads of encrypted messages (I have several of over 10 messages and one of nearly 40) leads to lots of flickering as the console replaces sup, sup comes back, the console comes back again ... It is also very slow, and involves writing decrypted messages to disk (if only temporarily) which could be a security hole. So I've looked about and found the gpgme gem which provides an API to use, and allows decryption entirely in memory.
So I've rewritten lib/sup/crypto.rb to use gpgme. The functionality is pretty much the same. Things I'm aware of that are different: * we can't set the signature algorithm, so we have to use whatever is set in the user's preferences * the gpg-args hook has been replaced by the gpg-options hook Other than that I think it is the same, although it took some work to get the signature output to be the same. The other main difference is that it's much faster and nicer now :) It could do with some testing - I don't have much in the way of messages that cause gpg to complain, so if you do, please try opening those messages with this code and see if the behaviour is reasonable - no crashes, given messages about why your message was bad etc. Also I guess I should ask if people are happy to use this gem. Is it hard to use on Macs? I guess I could rewrite this patch so it falls back to the gpg binary if gpgme is not available ... To install this patch on Debian/Ubuntu you can either * apt-get install libgpgme-ruby * apt-get install libgpgme11-dev; gem install gpgme Hamish Downer
From 52441d1eb749bb1e3b5026e42a334e9c8f455833 Mon Sep 17 00:00:00 2001 From: Hamish Downer <dmi...@gmail.com> Date: Fri, 5 Nov 2010 22:30:55 +0000 Subject: [PATCH] Converted crypto to use the gpgme gem --- bin/sup | 11 +++ lib/sup/crypto.rb | 231 ++++++++++++++++++++++++++++++----------------------- 2 files changed, 141 insertions(+), 101 deletions(-) diff --git a/bin/sup b/bin/sup index 10be161..ad7a0d1 100755 --- a/bin/sup +++ b/bin/sup @@ -10,6 +10,13 @@ rescue LoadError no_ncursesw = true end +no_gpgme = false +begin + require 'gpgme' +rescue LoadError + no_gpgme = true +end + require 'fileutils' require 'trollop' require "sup"; Redwood::check_library_version_against "git" @@ -23,6 +30,10 @@ if no_ncursesw info "No 'ncursesw' gem detected. Install it for wide character support." end +if no_gpgme + info "No 'gpgme' gem detected. Install it for email encryption, decryption and signatures." +end + $opts = Trollop::options do version "sup v#{Redwood::VERSION}" banner <<EOS diff --git a/lib/sup/crypto.rb b/lib/sup/crypto.rb index c7b57c1..9d21ea0 100644 --- a/lib/sup/crypto.rb +++ b/lib/sup/crypto.rb @@ -1,3 +1,8 @@ +begin + require 'gpgme' +rescue LoadError +end + module Redwood class CryptoManager @@ -11,76 +16,79 @@ class CryptoManager [:encrypt, "Encrypt only"] ) - HookManager.register "gpg-args", <<EOS -Runs before gpg is executed, allowing you to modify the arguments (most + HookManager.register "gpg-options", <<EOS +Runs before gpg is called, allowing you to modify the options (most likely you would want to add something to certain commands, like ---trust-model always to signing/encrypting a message, but who knows). +{:always_trust => true} to encrypting a message, but who knows). Variables: -args: arguments for running GPG +operation: what operation will be done ("sign", "encrypt", "decrypt" or "verify") +options: a dictionary of values to be passed to GPGME -Return value: the arguments for running GPG +Return value: a dictionary to be passed to GPGME EOS def initialize @mutex = Mutex.new - bin = `which gpg`.chomp - @cmd = case bin - when /\S/ - debug "crypto: detected gpg binary in #{bin}" - "#{bin} --quiet --batch --no-verbose --logger-fd 1 --use-agent" - else - debug "crypto: no gpg binary detected" - nil + # test if the gpgme gem is available + @gpgme_present = true + begin + GPGME.check_version({:protocol => GPGME::PROTOCOL_OpenPGP}) + rescue NameError, GPGME::Error + @gpgme_present = false end end - def have_crypto?; !...@cmd.nil? end + def have_crypto?; @gpgme_present end def sign from, to, payload - payload_fn = Tempfile.new "redwood.payload" - payload_fn.write format_payload(payload) - payload_fn.close + return unknown_status(cant_find_gpgme) unless @gpgme_present - sig_fn = Tempfile.new "redwood.signature"; sig_fn.close + gpg_opts = {:protocol => GPGME::PROTOCOL_OpenPGP, :armor => true, :textmode => true} + gpg_opts.merge(gen_sign_user_opts(from)) + gpg_opts = HookManager.run("gpg-options", + {:operation => "sign", :options => gpg_opts}) || gpg_opts - sign_user_opts = gen_sign_user_opts from - message = run_gpg "--output #{sig_fn.path} --yes --armor --detach-sign --textmode --digest-algo sha256 #{sign_user_opts} #{payload_fn.path}", :interactive => true - unless $?.success? - info "Error while running gpg: #{message}" + begin + sig = GPGME.detach_sign(format_payload(payload), gpg_opts) + rescue GPGME::Error => exc + info "Error while running gpg: #{exc.message}" raise Error, "GPG command failed. See log for details." end envelope = RMail::Message.new - envelope.header["Content-Type"] = 'multipart/signed; protocol=application/pgp-signature; micalg=pgp-sha256' + envelope.header["Content-Type"] = 'multipart/signed; protocol=application/pgp-signature' envelope.add_part payload - signature = RMail::Message.make_attachment IO.read(sig_fn.path), "application/pgp-signature", nil, "signature.asc" + signature = RMail::Message.make_attachment sig, "application/pgp-signature", nil, "signature.asc" envelope.add_part signature envelope end def encrypt from, to, payload, sign=false - payload_fn = Tempfile.new "redwood.payload" - payload_fn.write format_payload(payload) - payload_fn.close - - encrypted_fn = Tempfile.new "redwood.encrypted"; encrypted_fn.close - - recipient_opts = (to + [ from ] ).map { |r| "--recipient '<#{r}>'" }.join(" ") - sign_opts = "" - sign_opts = "--sign --digest-algo sha256 " + gen_sign_user_opts(from) if sign - message = run_gpg "--output #{encrypted_fn.path} --yes --armor --encrypt --textmode #{sign_opts} #{recipient_opts} #{payload_fn.path}", :interactive => true - unless $?.success? - info "Error while running gpg: #{message}" + return unknown_status(cant_find_gpgme) unless @gpgme_present + + gpg_opts = {:protocol => GPGME::PROTOCOL_OpenPGP, :armor => true, :textmode => true} + if sign + gpg_opts.merge(gen_sign_user_opts(from)) + gpg_opts.merge({:sign => true}) + end + gpg_opts = HookManager.run("gpg-options", + {:operation => "encrypt", :options => gpg_opts}) || gpg_opts + recipients = to + [from] + + begin + cipher = GPGME.encrypt(recipients, format_payload(payload), gpg_opts) + rescue GPGME::Error => exc + info "Error while running gpg: #{exc.message}" raise Error, "GPG command failed. See log for details." end encrypted_payload = RMail::Message.new encrypted_payload.header["Content-Type"] = "application/octet-stream" encrypted_payload.header["Content-Disposition"] = 'inline; filename="msg.asc"' - encrypted_payload.body = IO.read(encrypted_fn.path) + encrypted_payload.body = cipher control = RMail::Message.new control.header["Content-Type"] = "application/pgp-encrypted" @@ -99,70 +107,85 @@ EOS encrypt from, to, payload, true end - def verified_ok? output, rc - output_lines = output.split(/\n/) - - if output =~ /^gpg: (.* signature from .*$)/ - if rc == 0 - Chunk::CryptoNotice.new :valid, $1, output_lines - else - Chunk::CryptoNotice.new :invalid, $1, output_lines + def verified_ok? verify_result + valid = true + unknown = false + output_lines = [] + + verify_result.signatures.each do |signature| + output_lines.push(sig_output_lines(signature)) + output_lines.flatten! + err_code = GPGME::gpgme_err_code(signature.status) + if err_code == GPGME::GPG_ERR_BAD_SIGNATURE + valid = false + elsif err_code != GPGME::GPG_ERR_NO_ERROR + valid = false + unknown = true end - elsif output_lines.length == 0 && rc == 0 - # the message wasn't signed + end + + if output_lines.length == 0 Chunk::CryptoNotice.new :valid, "Encrypted message wasn't signed", output_lines + elsif valid + Chunk::CryptoNotice.new(:valid, simplify_sig_line(verify_result.signatures[0].to_s), output_lines) + elsif !unknown + Chunk::CryptoNotice.new(:invalid, simplify_sig_line(verify_result.signatures[0].to_s), output_lines) else unknown_status output_lines end end def verify payload, signature, detached=true # both RubyMail::Message objects - return unknown_status(cant_find_binary) unless @cmd + return unknown_status(cant_find_gpgme) unless @gpgme_present + gpg_opts = {:protocol => GPGME::PROTOCOL_OpenPGP} + gpg_opts = HookManager.run("gpg-options", + {:operation => "verify", :options => gpg_opts}) || gpg_opts + ctx = GPGME::Ctx.new(gpg_opts) + sig_data = GPGME::Data.from_str signature.decode if detached - payload_fn = Tempfile.new "redwood.payload" - payload_fn.write format_payload(payload) - payload_fn.close - end - - signature_fn = Tempfile.new "redwood.signature" - signature_fn.write signature.decode - signature_fn.close - - if detached - output = run_gpg "--verify #{signature_fn.path} #{payload_fn.path}" + signed_text_data = GPGME::Data.from_str(format_payload(payload)) + plain_data = nil else - output = run_gpg "--verify #{signature_fn.path}" + signed_text_data = nil + plain_data = GPGME::Data.empty end - - self.verified_ok? output, $? + begin + ctx.verify(sig_data, signed_text_data, plain_data) + rescue GPGME::Error => exc + return unknown_status exc.message + end + self.verified_ok? ctx.verify_result end ## returns decrypted_message, status, desc, lines def decrypt payload, armor=false # a RubyMail::Message object - return unknown_status(cant_find_binary) unless @cmd - - payload_fn = Tempfile.new(["redwood.payload", ".asc"]) - payload_fn.write payload.to_s - payload_fn.close - - output_fn = Tempfile.new "redwood.output" - output_fn.close - - message = run_gpg "--output #{output_fn.path} --skip-verify --yes --decrypt #{payload_fn.path}", :interactive => true - - unless $?.success? - info "Error while running gpg: #{message}" - return Chunk::CryptoNotice.new(:invalid, "This message could not be decrypted", message.split("\n")) + return unknown_status(cant_find_gpgme) unless @gpgme_present + + gpg_opts = {:protocol => GPGME::PROTOCOL_OpenPGP} + gpg_opts = HookManager.run("gpg-options", + {:operation => "decrypt", :options => gpg_opts}) || gpg_opts + ctx = GPGME::Ctx.new(gpg_opts) + cipher_data = GPGME::Data.from_str(format_payload(payload)) + plain_data = GPGME::Data.empty + begin + ctx.decrypt_verify(cipher_data, plain_data) + rescue GPGME::Error => exc + info "Error while running gpg: #{exc.message}" + return Chunk::CryptoNotice.new(:invalid, "This message could not be decrypted", exc.message) end - - output = IO.read output_fn.path + sig = self.verified_ok? ctx.verify_result + plain_data.seek(0, IO::SEEK_SET) + output = plain_data.read output.force_encoding Encoding::ASCII_8BIT if output.respond_to? :force_encoding + ## TODO: test to see if it is still necessary to do a 2nd run if verify + ## fails. + # ## check for a valid signature in an extra run because gpg aborts if the ## signature cannot be verified (but it is still able to decrypt) - sigoutput = run_gpg "#{payload_fn.path}" - sig = self.verified_ok? sigoutput, $? + #sigoutput = run_gpg "#{payload_fn.path}" + #sig = self.old_verified_ok? sigoutput, $? if armor msg = RMail::Message.new @@ -207,8 +230,8 @@ private Chunk::CryptoNotice.new :unknown, "Unable to determine validity of cryptographic signature", lines end - def cant_find_binary - ["Can't find gpg binary in path."] + def cant_find_gpgme + ["Can't find gpgme gem."] end ## here's where we munge rmail output into the format that signed/encrypted @@ -217,6 +240,28 @@ private payload.to_s.gsub(/(^|[^\r])\n/, "\\1\r\n") end + # remove the hex key_id and info in () + def simplify_sig_line sig_line + sig_line = sig_line.sub(/from [0-9A-F]{16} /, "from ") + sig_line.sub(/\(.+\) </, "<") + end + + def sig_output_lines signature + time_line = "Signature made " + signature.timestamp.strftime("%a %d %b %Y %H:%M:%S %Z") + + " using key ID " + signature.fingerprint[-8..-1] + first_sig = signature.to_s.sub(/from [0-9A-F]{16} /, 'from "') + '"' + output_lines = [time_line, first_sig] + + ctx = GPGME::Ctx.new + if from_key = ctx.get_key(signature.fingerprint) + if from_key.uids.length > 1 + aka_list = from_key.uids[1..-1] + aka_list.each { |aka| output_lines << ' aka "' + aka.uid + '"' } + end + end + output_lines + end + # logic is: # if gpgkey set for this account, then use that # elsif only one account, then leave blank so gpg default will be user @@ -224,30 +269,14 @@ private def gen_sign_user_opts from account = AccountManager.account_for from if !account.gpgkey.nil? - opts = "--local-user '#{account.gpgkey}'" + opts = {:signers => account.gpgkey} elsif AccountManager.user_emails.length == 1 # only one account - opts = "" + opts = {} else - opts = "--local-user '#{from}'" + opts = {:signers => from} end opts end - - def run_gpg args, opts={} - args = HookManager.run("gpg-args", { :args => args }) || args - cmd = "LC_MESSAGES=C #...@cmd} #{args}" - if opts[:interactive] && BufferManager.instantiated? - output_fn = Tempfile.new "redwood.output" - output_fn.close - cmd += " > #{output_fn.path} 2> /dev/null" - debug "crypto: running: #{cmd}" - BufferManager.shell_out cmd - IO.read(output_fn.path) rescue "can't read output" - else - debug "crypto: running: #{cmd}" - `#{cmd} 2> /dev/null` - end - end end end -- 1.7.1
_______________________________________________ Sup-devel mailing list Sup-devel@rubyforge.org http://rubyforge.org/mailman/listinfo/sup-devel