class CryptoManager
include Singleton
+ class Error < StandardError; end
+
OUTGOING_MESSAGE_OPERATIONS = OrderedHash.new(
[:sign, "Sign"],
[:sign_and_encrypt, "Sign and encrypt"],
def initialize
@mutex = Mutex.new
- self.class.i_am_the_instance self
bin = `which gpg`.chomp
- bin = `which pgp`.chomp unless bin =~ /\S/
-
@cmd =
case bin
when /\S/
- Redwood::log "crypto: detected gpg binary in #{bin}"
+ debug "crypto: detected gpg binary in #{bin}"
"#{bin} --quiet --batch --no-verbose --logger-fd 1 --use-agent"
else
- Redwood::log "crypto: no gpg binary detected"
+ debug "crypto: no gpg binary detected"
nil
end
end
def have_crypto?; !@cmd.nil? end
+ def sign from, to, payload
+ payload_fn = Tempfile.new "redwood.payload"
+ payload_fn.write format_payload(payload)
+ payload_fn.close
+
+ output = run_gpg "--output - --armor --detach-sign --textmode --local-user '#{from}' #{payload_fn.path}"
+
+ raise Error, (output || "gpg command failed: #{cmd}") unless $?.success?
+
+ envelope = RMail::Message.new
+ envelope.header["Content-Type"] = 'multipart/signed; protocol=application/pgp-signature; micalg=pgp-sha1'
+
+ envelope.add_part payload
+ signature = RMail::Message.make_attachment output, "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
+
+ recipient_opts = (to + [ from ] ).map { |r| "--recipient '<#{r}>'" }.join(" ")
+ sign_opts = sign ? "--sign --local-user '#{from}'" : ""
+ gpg_output = run_gpg "--output - --armor --encrypt --textmode #{sign_opts} #{recipient_opts} #{payload_fn.path}"
+ raise Error, (gpg_output || "gpg command failed: #{cmd}") unless $?.success?
+
+ 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 = gpg_output
+
+ control = RMail::Message.new
+ 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.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 payload.to_s.gsub(/(^|[^\r])\n/, "\\1\r\n").gsub(/^MIME-Version: .*\r\n/, "")
+ payload_fn.write format_payload(payload)
payload_fn.close
signature_fn = Tempfile.new "redwood.signature"
signature_fn.write signature.decode
signature_fn.close
- cmd = "#{@cmd} --verify #{signature_fn.path} #{payload_fn.path} 2> /dev/null"
+ output = run_gpg "--verify #{signature_fn.path} #{payload_fn.path}"
+ output_lines = output.split(/\n/)
- #Redwood::log "gpg: running: #{cmd}"
- gpg_output = `#{cmd}`
- #Redwood::log "got output: #{gpg_output.inspect}"
- output_lines = gpg_output.split(/\n/)
-
- if gpg_output =~ /^gpg: (.* signature from .*$)/
+ if output =~ /^gpg: (.* signature from .*$)/
if $? == 0
Chunk::CryptoNotice.new :valid, $1, output_lines
else
end
end
- # returns decrypted_message, status, desc, lines
- def decrypt payload # RubyMail::Message objects
+ ## returns decrypted_message, status, desc, lines
+ def decrypt payload # a RubyMail::Message object
return unknown_status(cant_find_binary) unless @cmd
-# cmd = "#{@cmd} --decrypt 2> /dev/null"
-
-# Redwood::log "gpg: running: #{cmd}"
-
-# gpg_output =
-# IO.popen(cmd, "a+") do |f|
-# f.puts payload.to_s
-# f.gets
-# end
-
payload_fn = Tempfile.new "redwood.payload"
payload_fn.write payload.to_s
payload_fn.close
- cmd = "#{@cmd} --decrypt #{payload_fn.path} 2> /dev/null"
- Redwood::log "gpg: running: #{cmd}"
- gpg_output = `#{cmd}`
- Redwood::log "got output: #{gpg_output.inspect}"
+ output = run_gpg "--decrypt #{payload_fn.path}"
- if $? == 0 # successful decryption
- decrypted_payload, sig_lines =
- if gpg_output =~ /\A(.*?)((^gpg: .*$)+)\Z/m
- [$1, $2]
+ if $?.success?
+ decrypted_payload, sig_lines = if output =~ /\A(.*?)((^gpg: .*$)+)\Z/m
+ [$1, $2]
+ else
+ [output, nil]
+ end
+
+ sig = if sig_lines # encrypted & signed
+ if sig_lines =~ /^gpg: (Good signature from .*$)/
+ Chunk::CryptoNotice.new :valid, $1, sig_lines.split("\n")
else
- [gpg_output, nil]
- end
-
- sig =
- if sig_lines # encrypted & signed
- if sig_lines =~ /^gpg: (Good signature from .*$)/
- Chunk::CryptoNotice.new :valid, $1, sig_lines.split("\n")
- else
- Chunk::CryptoNotice.new :invalid, $1, sig_lines.split("\n")
- end
+ Chunk::CryptoNotice.new :invalid, $1, sig_lines.split("\n")
end
+ end
notice = Chunk::CryptoNotice.new :valid, "This message has been decrypted for display"
- [RMail::Parser.read(decrypted_payload), sig, notice]
+ [notice, sig, RMail::Parser.read(decrypted_payload)]
else
- notice = Chunk::CryptoNotice.new :invalid, "This message could not be decrypted", gpg_output.split("\n")
- [nil, nil, notice]
+ Chunk::CryptoNotice.new :invalid, "This message could not be decrypted", output.split("\n")
end
end
def unknown_status lines=[]
Chunk::CryptoNotice.new :unknown, "Unable to determine validity of cryptographic signature", lines
end
-
+
def cant_find_binary
- ["Can't find gpg or pgp binary in path"]
+ ["Can't find gpg binary in path."]
+ 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/, "")
+ end
+
+ def run_gpg args
+ cmd = "#{@cmd} #{args} 2> /dev/null"
+ output = `#{cmd}`
+ output
end
end
end