]> git.cworth.org Git - sup/blobdiff - lib/sup/crypto.rb
giving a block to PollManager#each_message_in_source is now optional
[sup] / lib / sup / crypto.rb
index 5aca1a4d15653b9e76c2dbd20e8d9105b6ddb93f..9e16132304ebad107b8cab43c0706de82b6e1a78 100644 (file)
 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
 
+  class Error < StandardError; end
+
+  OUTGOING_MESSAGE_OPERATIONS = OrderedHash.new(
+    [:sign, "Sign"],
+    [:sign_and_encrypt, "Sign and encrypt"],
+    [:encrypt, "Encrypt only"]
+  )
+
   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}"
         "#{bin} --quiet --batch --no-verbose --logger-fd 1 --use-agent"
       else
+        Redwood::log "crypto: no gpg binary detected"
         nil
       end
   end
 
-  # returns a cryptosignature
+  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.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
-        CryptoSignature.new :valid, $1, output_lines
+        Chunk::CryptoNotice.new :valid, $1, output_lines
       else
-        CryptoSignature.new :invalid, $1, output_lines
+        Chunk::CryptoNotice.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
+  ## 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
+    if $?.success?
       decrypted_payload, sig_lines =
-        if gpg_output =~ /\A(.*?)((^gpg: .*$)+)\Z/m
+        if output =~ /\A(.*?)((^gpg: .*$)+)\Z/m
           [$1, $2]
         else
-          [gpg_output, nil]
+          [output, nil]
         end
       
       sig = 
         if sig_lines # encrypted & signed
           if sig_lines =~ /^gpg: (Good signature from .*$)/
-            CryptoSignature.new :valid, $1, sig_lines.split("\n")
+            Chunk::CryptoNotice.new :valid, $1, sig_lines.split("\n")
           else
-            CryptoSignature.new :invalid, $1, sig_lines.split("\n")
+            Chunk::CryptoNotice.new :invalid, $1, sig_lines.split("\n")
           end
         end
 
-      notice = CryptoDecryptedNotice.new :valid, "This message has been decrypted for display"
+      notice = Chunk::CryptoNotice.new :valid, "This message has been decrypted for display"
       [RMail::Parser.read(decrypted_payload), sig, notice]
     else
-      notice = CryptoDecryptedNotice.new :invalid, "This message could not be decrypted", gpg_output.split("\n")
+      notice = Chunk::CryptoNotice.new :invalid, "This message could not be decrypted", output.split("\n")
       [nil, nil, notice]
     end
   end
@@ -120,18 +143,23 @@ class CryptoManager
 private
 
   def unknown_status lines=[]
-    CryptoSignature.new :unknown, "Unable to determine validity of cryptographic signature", 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
-end
-end
 
+  def format_payload payload
+    payload.to_s.gsub(/(^|[^\r])\n/, "\\1\r\n").gsub(/^MIME-Version: .*\r\n/, "")
+  end
 
-## to check:
-## failed decryption
-## decription but failed signature
-## no gpg found
-## multiple private keys
+  def run_gpg args
+    cmd = "#{@cmd} #{args} 2> /dev/null"
+    #Redwood::log "crypto: running: #{cmd}"
+    output = `#{cmd}`
+    #Redwood::log "crypto: output: #{output.inspect}" unless $?.success?
+    output
+  end
+end
+end