]> git.cworth.org Git - sup/blob - lib/sup/crypto.rb
skip system buffers when rolling
[sup] / lib / sup / crypto.rb
1 module Redwood
2
3 class CryptoManager
4   include Singleton
5
6   class Error < StandardError; end
7
8   OUTGOING_MESSAGE_OPERATIONS = OrderedHash.new(
9     [:sign, "Sign"],
10     [:sign_and_encrypt, "Sign and encrypt"],
11     [:encrypt, "Encrypt only"]
12   )
13
14   def initialize
15     @mutex = Mutex.new
16     self.class.i_am_the_instance self
17
18     bin = `which gpg`.chomp
19
20     @cmd =
21       case bin
22       when /\S/
23         Redwood::log "crypto: detected gpg binary in #{bin}"
24         "#{bin} --quiet --batch --no-verbose --logger-fd 1 --use-agent"
25       else
26         Redwood::log "crypto: no gpg binary detected"
27         nil
28       end
29   end
30
31   def have_crypto?; !@cmd.nil? end
32
33   def sign from, to, payload
34     payload_fn = Tempfile.new "redwood.payload"
35     payload_fn.write format_payload(payload)
36     payload_fn.close
37
38     output = run_gpg "--output - --armor --detach-sign --textmode --local-user '#{from}' #{payload_fn.path}"
39
40     raise Error, (output || "gpg command failed: #{cmd}") unless $?.success?
41
42     envelope = RMail::Message.new
43     envelope.header["Content-Type"] = 'multipart/signed; protocol=application/pgp-signature; micalg=pgp-sha1'
44
45     envelope.add_part payload
46     signature = RMail::Message.make_attachment output, "application/pgp-signature", nil, "signature.asc"
47     envelope.add_part signature
48     envelope
49   end
50
51   def encrypt from, to, payload, sign=false
52     payload_fn = Tempfile.new "redwood.payload"
53     payload_fn.write format_payload(payload)
54     payload_fn.close
55
56     recipient_opts = (to + [ from ] ).map { |r| "--recipient '<#{r}>'" }.join(" ")
57     sign_opts = sign ? "--sign --local-user '#{from}'" : ""
58     gpg_output = run_gpg "--output - --armor --encrypt --textmode #{sign_opts} #{recipient_opts} #{payload_fn.path}"
59     raise Error, (gpg_output || "gpg command failed: #{cmd}") unless $?.success?
60
61     encrypted_payload = RMail::Message.new
62     encrypted_payload.header["Content-Type"] = "application/octet-stream"
63     encrypted_payload.header["Content-Disposition"] = 'inline; filename="msg.asc"'
64     encrypted_payload.body = gpg_output
65
66     control = RMail::Message.new
67     control.header["Content-Type"] = "application/pgp-encrypted"
68     control.header["Content-Disposition"] = "attachment"
69     control.body = "Version: 1\n"
70
71     envelope = RMail::Message.new
72     envelope.header["Content-Type"] = 'multipart/encrypted; protocol="application/pgp-encrypted"'
73
74     envelope.add_part control
75     envelope.add_part encrypted_payload
76     envelope
77   end
78
79   def sign_and_encrypt from, to, payload
80     encrypt from, to, payload, true
81   end
82
83   def verify payload, signature # both RubyMail::Message objects
84     return unknown_status(cant_find_binary) unless @cmd
85
86     payload_fn = Tempfile.new "redwood.payload"
87     payload_fn.write format_payload(payload)
88     payload_fn.close
89
90     signature_fn = Tempfile.new "redwood.signature"
91     signature_fn.write signature.decode
92     signature_fn.close
93
94     output = run_gpg "--verify #{signature_fn.path} #{payload_fn.path}"
95     output_lines = output.split(/\n/)
96
97     if output =~ /^gpg: (.* signature from .*$)/
98       if $? == 0
99         Chunk::CryptoNotice.new :valid, $1, output_lines
100       else
101         Chunk::CryptoNotice.new :invalid, $1, output_lines
102       end
103     else
104       unknown_status output_lines
105     end
106   end
107
108   ## returns decrypted_message, status, desc, lines
109   def decrypt payload # a RubyMail::Message object
110     return unknown_status(cant_find_binary) unless @cmd
111
112     payload_fn = Tempfile.new "redwood.payload"
113     payload_fn.write payload.to_s
114     payload_fn.close
115
116     output = run_gpg "--decrypt #{payload_fn.path}"
117
118     if $?.success?
119       decrypted_payload, sig_lines =
120         if output =~ /\A(.*?)((^gpg: .*$)+)\Z/m
121           [$1, $2]
122         else
123           [output, nil]
124         end
125       
126       sig = 
127         if sig_lines # encrypted & signed
128           if sig_lines =~ /^gpg: (Good signature from .*$)/
129             Chunk::CryptoNotice.new :valid, $1, sig_lines.split("\n")
130           else
131             Chunk::CryptoNotice.new :invalid, $1, sig_lines.split("\n")
132           end
133         end
134
135       notice = Chunk::CryptoNotice.new :valid, "This message has been decrypted for display"
136       [RMail::Parser.read(decrypted_payload), sig, notice]
137     else
138       notice = Chunk::CryptoNotice.new :invalid, "This message could not be decrypted", output.split("\n")
139       [nil, nil, notice]
140     end
141   end
142
143 private
144
145   def unknown_status lines=[]
146     Chunk::CryptoNotice.new :unknown, "Unable to determine validity of cryptographic signature", lines
147   end
148   
149   def cant_find_binary
150     ["Can't find gpg binary in path."]
151   end
152
153   ## here's where we munge rmail output into the format that signed/encrypted
154   ## PGP/GPG messages should be
155   def format_payload payload
156     payload.to_s.gsub(/(^|[^\r])\n/, "\\1\r\n").gsub(/^MIME-Version: .*\r\n/, "")
157   end
158
159   def run_gpg args
160     cmd = "#{@cmd} #{args} 2> /dev/null"
161     #Redwood::log "crypto: running: #{cmd}"
162     output = `#{cmd}`
163     #Redwood::log "crypto: output: #{output.inspect}" unless $?.success?
164     output
165   end
166 end
167 end