lib/sup/crypto.rb in sup-0.11 vs lib/sup/crypto.rb in sup-0.12

- old
+ new

@@ -43,18 +43,19 @@ payload_fn.write format_payload(payload) payload_fn.close sig_fn = Tempfile.new "redwood.signature"; sig_fn.close - message = run_gpg "--output #{sig_fn.path} --yes --armor --detach-sign --textmode --local-user '#{from}' #{payload_fn.path}", :interactive => true + 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}" 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-sha1' + envelope.header["Content-Type"] = 'multipart/signed; protocol=application/pgp-signature; micalg=pgp-sha256' envelope.add_part payload signature = RMail::Message.make_attachment IO.read(sig_fn.path), "application/pgp-signature", nil, "signature.asc" envelope.add_part signature envelope @@ -66,11 +67,12 @@ payload_fn.close encrypted_fn = Tempfile.new "redwood.encrypted"; encrypted_fn.close recipient_opts = (to + [ from ] ).map { |r| "--recipient '<#{r}>'" }.join(" ") - sign_opts = sign ? "--sign --local-user '#{from}'" : "" + 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}" raise Error, "GPG command failed. See log for details." end @@ -84,95 +86,118 @@ control.header["Content-Type"] = "application/pgp-encrypted" control.header["Content-Disposition"] = "attachment" control.body = "Version: 1\n" envelope = RMail::Message.new - envelope.header["Content-Type"] = 'multipart/encrypted; protocol="application/pgp-encrypted"' + envelope.header["Content-Type"] = 'multipart/encrypted; protocol=application/pgp-encrypted' envelope.add_part control envelope.add_part encrypted_payload envelope end def sign_and_encrypt from, to, payload encrypt from, to, payload, true end - def verify payload, signature # both RubyMail::Message objects - return unknown_status(cant_find_binary) unless @cmd - - payload_fn = Tempfile.new "redwood.payload" - payload_fn.write format_payload(payload) - payload_fn.close - - signature_fn = Tempfile.new "redwood.signature" - signature_fn.write signature.decode - signature_fn.close - - output = run_gpg "--verify #{signature_fn.path} #{payload_fn.path}" + def verified_ok? output, rc output_lines = output.split(/\n/) if output =~ /^gpg: (.* signature from .*$)/ - if $? == 0 + if rc == 0 Chunk::CryptoNotice.new :valid, $1, output_lines else Chunk::CryptoNotice.new :invalid, $1, output_lines end + elsif output_lines.length == 0 && rc == 0 + # the message wasn't signed + Chunk::CryptoNotice.new :valid, "Encrypted message wasn't signed", 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 + + 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}" + else + output = run_gpg "--verify #{signature_fn.path}" + end + + self.verified_ok? output, $? + end + ## returns decrypted_message, status, desc, lines - def decrypt payload # a RubyMail::Message object + def decrypt payload, armor=false # a RubyMail::Message object return unknown_status(cant_find_binary) unless @cmd - payload_fn = Tempfile.new "redwood.payload" + 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} --yes --decrypt #{payload_fn.path}", :interactive => true + 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")) end output = IO.read output_fn.path output.force_encoding Encoding::ASCII_8BIT if output.respond_to? :force_encoding - ## there's probably a better way to do this, but we're using the output to - ## look for a valid signature being present. + ## 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, $? - sig = case message - when /^gpg: (Good signature from .*$)/i - Chunk::CryptoNotice.new :valid, $1, message.split("\n") - when /^gpg: (Bad signature from .*$)/i - Chunk::CryptoNotice.new :invalid, $1, message.split("\n") - end - - # This is gross. This decrypted payload could very well be a multipart - # element itself, as opposed to a simple payload. For example, a - # multipart/signed element, like those generated by Mutt when encrypting - # and signing a message (instead of just clearsigning the body). - # Supposedly, decrypted_payload being a multipart element ought to work - # out nicely because Message::multipart_encrypted_to_chunks() runs the - # decrypted message through message_to_chunks() again to get any - # children. However, it does not work as intended because these inner - # payloads need not carry a MIME-Version header, yet they are fed to - # RMail as a top-level message, for which the MIME-Version header is - # required. This causes for the part not to be detected as multipart, - # hence being shown as an attachment. If we detect this is happening, - # we force the decrypted payload to be interpreted as MIME. - msg = RMail::Parser.read output - if msg.header.content_type =~ %r{^multipart/} && !msg.multipart? - output = "MIME-Version: 1.0\n" + output - output.force_encoding Encoding::ASCII_8BIT if output.respond_to? :force_encoding + if armor + msg = RMail::Message.new + # Look for Charset, they are put before the base64 crypted part + charsets = payload.body.split("\n").grep(/^Charset:/) + if !charsets.empty? and charsets[0] =~ /^Charset: (.+)$/ + output = Iconv.easy_decode($encoding, $1, output) + end + msg.body = output + else + # It appears that some clients use Windows new lines - CRLF - but RMail + # splits the body and header on "\n\n". So to allow the parse below to + # succeed, we will convert the newlines to what RMail expects + output = output.gsub(/\r\n/, "\n") + # This is gross. This decrypted payload could very well be a multipart + # element itself, as opposed to a simple payload. For example, a + # multipart/signed element, like those generated by Mutt when encrypting + # and signing a message (instead of just clearsigning the body). + # Supposedly, decrypted_payload being a multipart element ought to work + # out nicely because Message::multipart_encrypted_to_chunks() runs the + # decrypted message through message_to_chunks() again to get any + # children. However, it does not work as intended because these inner + # payloads need not carry a MIME-Version header, yet they are fed to + # RMail as a top-level message, for which the MIME-Version header is + # required. This causes for the part not to be detected as multipart, + # hence being shown as an attachment. If we detect this is happening, + # we force the decrypted payload to be interpreted as MIME. msg = RMail::Parser.read output + if msg.header.content_type =~ %r{^multipart/} && !msg.multipart? + output = "MIME-Version: 1.0\n" + output + output.force_encoding Encoding::ASCII_8BIT if output.respond_to? :force_encoding + msg = RMail::Parser.read output + end end notice = Chunk::CryptoNotice.new :valid, "This message has been decrypted for display" [notice, sig, msg] end @@ -187,15 +212,32 @@ end ## here's where we munge rmail output into the format that signed/encrypted ## PGP/GPG messages should be def format_payload payload - payload.to_s.gsub(/(^|[^\r])\n/, "\\1\r\n").gsub(/^MIME-Version: .*\r\n/, "") + payload.to_s.gsub(/(^|[^\r])\n/, "\\1\r\n") 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 + # else set --local-user from_email_address + def gen_sign_user_opts from + account = AccountManager.account_for from + if !account.gpgkey.nil? + opts = "--local-user '#{account.gpgkey}'" + elsif AccountManager.user_emails.length == 1 + # only one account + opts = "" + else + opts = "--local-user '#{from}'" + end + opts + end + def run_gpg args, opts={} args = HookManager.run("gpg-args", { :args => args }) || args - cmd = "#{@cmd} #{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}"