6 class Error < StandardError; end
8 OUTGOING_MESSAGE_OPERATIONS = OrderedHash.new(
10 [:sign_and_encrypt, "Sign and encrypt"],
11 [:encrypt, "Encrypt only"]
17 bin = `which gpg`.chomp
21 debug "crypto: detected gpg binary in #{bin}"
22 "#{bin} --quiet --batch --no-verbose --logger-fd 1 --use-agent"
24 debug "crypto: no gpg binary detected"
29 def have_crypto?; !@cmd.nil? end
31 def sign from, to, payload
32 payload_fn = Tempfile.new "redwood.payload"
33 payload_fn.write format_payload(payload)
36 output = run_gpg "--output - --armor --detach-sign --textmode --local-user '#{from}' #{payload_fn.path}"
38 raise Error, (output || "gpg command failed: #{cmd}") unless $?.success?
40 envelope = RMail::Message.new
41 envelope.header["Content-Type"] = 'multipart/signed; protocol=application/pgp-signature; micalg=pgp-sha1'
43 envelope.add_part payload
44 signature = RMail::Message.make_attachment output, "application/pgp-signature", nil, "signature.asc"
45 envelope.add_part signature
49 def encrypt from, to, payload, sign=false
50 payload_fn = Tempfile.new "redwood.payload"
51 payload_fn.write format_payload(payload)
54 recipient_opts = (to + [ from ] ).map { |r| "--recipient '<#{r}>'" }.join(" ")
55 sign_opts = sign ? "--sign --local-user '#{from}'" : ""
56 gpg_output = run_gpg "--output - --armor --encrypt --textmode #{sign_opts} #{recipient_opts} #{payload_fn.path}"
57 raise Error, (gpg_output || "gpg command failed: #{cmd}") unless $?.success?
59 encrypted_payload = RMail::Message.new
60 encrypted_payload.header["Content-Type"] = "application/octet-stream"
61 encrypted_payload.header["Content-Disposition"] = 'inline; filename="msg.asc"'
62 encrypted_payload.body = gpg_output
64 control = RMail::Message.new
65 control.header["Content-Type"] = "application/pgp-encrypted"
66 control.header["Content-Disposition"] = "attachment"
67 control.body = "Version: 1\n"
69 envelope = RMail::Message.new
70 envelope.header["Content-Type"] = 'multipart/encrypted; protocol="application/pgp-encrypted"'
72 envelope.add_part control
73 envelope.add_part encrypted_payload
77 def sign_and_encrypt from, to, payload
78 encrypt from, to, payload, true
81 def verify payload, signature # both RubyMail::Message objects
82 return unknown_status(cant_find_binary) unless @cmd
84 payload_fn = Tempfile.new "redwood.payload"
85 payload_fn.write format_payload(payload)
88 signature_fn = Tempfile.new "redwood.signature"
89 signature_fn.write signature.decode
92 output = run_gpg "--verify #{signature_fn.path} #{payload_fn.path}"
93 output_lines = output.split(/\n/)
95 if output =~ /^gpg: (.* signature from .*$)/
97 Chunk::CryptoNotice.new :valid, $1, output_lines
99 Chunk::CryptoNotice.new :invalid, $1, output_lines
102 unknown_status output_lines
106 ## returns decrypted_message, status, desc, lines
107 def decrypt payload # a RubyMail::Message object
108 return unknown_status(cant_find_binary) unless @cmd
110 payload_fn = Tempfile.new "redwood.payload"
111 payload_fn.write payload.to_s
114 output = run_gpg "--decrypt #{payload_fn.path}"
117 decrypted_payload, sig_lines = if output =~ /\A(.*?)((^gpg: .*$)+)\Z/m
123 sig = if sig_lines # encrypted & signed
124 if sig_lines =~ /^gpg: (Good signature from .*$)/
125 Chunk::CryptoNotice.new :valid, $1, sig_lines.split("\n")
127 Chunk::CryptoNotice.new :invalid, $1, sig_lines.split("\n")
131 notice = Chunk::CryptoNotice.new :valid, "This message has been decrypted for display"
132 [RMail::Parser.read(decrypted_payload), sig, notice]
134 notice = Chunk::CryptoNotice.new :invalid, "This message could not be decrypted", output.split("\n")
141 def unknown_status lines=[]
142 Chunk::CryptoNotice.new :unknown, "Unable to determine validity of cryptographic signature", lines
146 ["Can't find gpg binary in path."]
149 ## here's where we munge rmail output into the format that signed/encrypted
150 ## PGP/GPG messages should be
151 def format_payload payload
152 payload.to_s.gsub(/(^|[^\r])\n/, "\\1\r\n").gsub(/^MIME-Version: .*\r\n/, "")
156 cmd = "#{@cmd} #{args} 2> /dev/null"