module Redwood
+class CryptoSignature
+ attr_reader :lines, :status, :description
+
+ def initialize status, description, lines
+ @status = status
+ @description = description
+ @lines = lines
+ end
+end
+
+class CryptoDecryptedNotice
+ attr_reader :lines, :status, :description
+
+ def initialize status, description, lines=[]
+ @status = status
+ @description = description
+ @lines = lines
+ end
+end
+
class CryptoManager
include Singleton
@mutex = Mutex.new
self.class.i_am_the_instance self
- @cmd = `which gpg`.chomp
- @cmd = `which pgp`.chomp unless @cmd =~ /\S/
- @cmd = nil unless @cmd =~ /\S/
+ bin = `which gpg`.chomp
+ bin = `which pgp`.chomp unless bin =~ /\S/
+
+ @cmd =
+ case bin
+ when /\S/
+ "#{bin} --quiet --batch --no-verbose --logger-fd 1 --use-agent"
+ else
+ nil
+ end
end
+ # returns a cryptosignature
def verify payload, signature # both RubyMail::Message objects
- return unknown unless @cmd
+ 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/, "")
signature_fn.write signature.decode
signature_fn.close
- cmd = "#{@cmd} --quiet --batch --no-verbose --verify --logger-fd 1 #{signature_fn.path} #{payload_fn.path} 2> /dev/null"
+ cmd = "#{@cmd} --verify #{signature_fn.path} #{payload_fn.path} 2> /dev/null"
#Redwood::log "gpg: running: #{cmd}"
gpg_output = `#{cmd}`
#Redwood::log "got output: #{gpg_output.inspect}"
- lines = gpg_output.split(/\n/)
+ output_lines = gpg_output.split(/\n/)
if gpg_output =~ /^gpg: (.* signature from .*$)/
- $? == 0 ? [:valid, $1, lines] : [:invalid, $1, lines]
+ if $? == 0
+ CryptoSignature.new :valid, $1, output_lines
+ else
+ CryptoSignature.new :invalid, $1, output_lines
+ end
+ else
+ unknown_status output_lines
+ end
+ end
+
+ # returns decrypted_message, status, desc, lines
+ def decrypt payload # RubyMail::Message objects
+ 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}"
+
+ if $? == 0 # successful decryption
+ decrypted_payload, sig_lines =
+ if gpg_output =~ /\A(.*?)((^gpg: .*$)+)\Z/m
+ [$1, $2]
+ else
+ [gpg_output, nil]
+ end
+
+ sig =
+ if sig_lines # encrypted & signed
+ if sig_lines =~ /^gpg: (Good signature from .*$)/
+ CryptoSignature.new :valid, $1, sig_lines.split("\n")
+ else
+ CryptoSignature.new :invalid, $1, sig_lines.split("\n")
+ end
+ end
+
+ notice = CryptoDecryptedNotice.new :valid, "This message has been decrypted for display."
+ [RMail::Parser.read(decrypted_payload), sig, notice]
else
- unknown lines
+ notice = CryptoDecryptedNotice.new :invalid, "This message could not be decrypted", gpg_output.split("\n")
+ [nil, nil, notice]
end
end
private
- def unknown lines=[]
- [:unknown, "Unable to determine validity of cryptographic signature", lines]
+ def unknown_status lines=[]
+ CryptoSignature.new :unknown, "Unable to determine validity of cryptographic signature", lines
+ end
+
+ def cant_find_binary
+ ["Can't find gpg or pgp binary in path"]
end
end
end
+
+
+## to check:
+## failed decryption
+## decription but failed signature
+## no gpg found
+## multiple private keys
end
end
- class CryptoSignature
- attr_reader :lines, :description
-
- def initialize payload, signature
- @payload = payload
- @signature = signature
- @status = nil
- @description = nil
- @lines = []
- end
-
- def status
- verify
- @status
- end
-
- def description
- verify
- @description
- end
-
-private
-
- def verify
- @status, @description, @lines = CryptoManager.verify(@payload, @signature) unless @status
- end
- end
-
QUOTE_PATTERN = /^\s{0,4}[>|\}]/
BLOCK_QUOTE_PATTERN = /^-----\s*Original Message\s*----+$/
QUOTE_START_PATTERN = /(^\s*Excerpts from)|(^\s*In message )|(^\s*In article )|(^\s*Quoting )|((wrote|writes|said|says)\s*:\s*$)/
return
end
- [CryptoSignature.new(payload, signature), message_to_chunks(payload)].flatten
+ [CryptoManager.verify(payload, signature), message_to_chunks(payload)]
+ end
+
+ def multipart_encrypted_to_chunks m
+ Redwood::log ">> multipart ENCRYPTED: #{m.header['Content-Type']}: #{m.body.size}"
+ if m.body.size != 2
+ Redwood::log "warning: multipart/encrypted with #{m.body.size} parts (expecting 2)"
+ return
+ end
+
+ control, payload = m.body
+ if control.multipart?
+ Redwood::log "warning: multipart/encrypted with control multipart #{control.multipart?} and payload multipart #{payload.multipart?}"
+ return
+ end
+
+ if payload.header.content_type != "application/octet-stream"
+ Redwood::log "warning: multipart/encrypted with payload content type #{payload.header.content_type}"
+ return
+ end
+
+ if control.header.content_type != "application/pgp-encrypted"
+ Redwood::log "warning: multipart/encrypted with control content type #{signature.header.content_type}"
+ return
+ end
+
+ decryptedm, sig, notice = CryptoManager.decrypt payload
+ children = message_to_chunks(decryptedm) if decryptedm
+ [notice, sig, children].flatten.compact
end
-
+
def message_to_chunks m, sibling_types=[]
if m.multipart?
- chunks = multipart_signed_to_chunks(m) if m.header.content_type == "multipart/signed"
+ chunks =
+ case m.header.content_type
+ when "multipart/signed"
+ multipart_signed_to_chunks m
+ when "multipart/encrypted"
+ multipart_encrypted_to_chunks m
+ end
+
unless chunks
sibling_types = m.body.map { |p| p.header.content_type }
chunks = m.body.map { |p| message_to_chunks p, sibling_types }.flatten.compact
end
+
chunks
else
filename =
l = @layout[chunk]
l.state = (l.state != :closed ? :closed : :open)
cursor_down if l.state == :closed
- when Message::Quote, Message::Signature, Message::CryptoSignature
- return if chunk.lines.length == 1
+ when Message::Quote, Message::Signature, CryptoSignature, CryptoDecryptedNotice
+ return if chunk.lines.length <= 1
toggle_chunk_expansion chunk
when Message::Attachment
if chunk.inlineable?
rest = []
unless m.to.empty?
m.to.each_with_index { |p, i| @person_lines[start + rest.length + from.length + i] = p }
- rest += format_person_list " To: ", m.to
+ rest += format_person_list " To: ", m.to
end
unless m.cc.empty?
m.cc.each_with_index { |p, i| @person_lines[start + rest.length + from.length + i] = p }
- rest += format_person_list " Cc: ", m.cc
+ rest += format_person_list " Cc: ", m.cc
end
unless m.bcc.empty?
m.bcc.each_with_index { |p, i| @person_lines[start + rest.length + from.length + i] = p }
- rest += format_person_list " Bcc: ", m.bcc
+ rest += format_person_list " Bcc: ", m.bcc
end
rest += [
- " Date: #{m.date.strftime DATE_FORMAT} (#{m.date.to_nice_distance_s})",
- " Subject: #{m.subj}",
- (parent ? " In reply to: #{parent.from.mediumname}'s message of #{parent.date.strftime DATE_FORMAT}" : nil),
- m.labels.empty? ? nil : " Labels: #{m.labels.join(', ')}",
+ " Date: #{m.date.strftime DATE_FORMAT} (#{m.date.to_nice_distance_s})",
+ " Subject: #{m.subj}",
+ (parent ? " In reply to: #{parent.from.mediumname}'s message of #{parent.date.strftime DATE_FORMAT}" : nil),
+ m.labels.empty? ? nil : " Labels: #{m.labels.join(', ')}",
].compact
from + rest.map { |l| [[color, prefix + " " + l]] }
when :open
[[[:sig_patina_color, "#{prefix}- (#{chunk.lines.length}-line signature)"]]] + chunk.lines.map { |line| [[:sig_color, "#{prefix}#{line}"]] }
end
- when Message::CryptoSignature
+ when CryptoSignature, CryptoDecryptedNotice
color =
case chunk.status
when :valid: :cryptosig_valid_color
when :invalid: :cryptosig_invalid_color
else :cryptosig_unknown_color
end
+ widget = chunk.lines.empty? ? "x" : (state == :closed ? "+" : "-")
case state
when :closed
- [[[color, "#{prefix}+ #{chunk.description}"]]]
+ [[[color, "#{prefix}#{widget} #{chunk.description}"]]]
when :open
- [[[color, "#{prefix}- #{chunk.description}"]]] +
+ [[[color, "#{prefix}#{widget} #{chunk.description}"]]] +
chunk.lines.map { |line| [[color, "#{prefix}#{line}"]] }
end
else