]> git.cworth.org Git - sup/blob - lib/sup/crypto.rb
Merge branch 'preemptive-loading' into next
[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
17     bin = `which gpg`.chomp
18     @cmd =
19       case bin
20       when /\S/
21         debug "crypto: detected gpg binary in #{bin}"
22         "#{bin} --quiet --batch --no-verbose --logger-fd 1 --use-agent"
23       else
24         debug "crypto: no gpg binary detected"
25         nil
26       end
27   end
28
29   def have_crypto?; !@cmd.nil? end
30
31   def sign from, to, payload
32     payload_fn = Tempfile.new "redwood.payload"
33     payload_fn.write format_payload(payload)
34     payload_fn.close
35
36     output = run_gpg "--output - --armor --detach-sign --textmode --local-user '#{from}' #{payload_fn.path}"
37
38     raise Error, (output || "gpg command failed: #{cmd}") unless $?.success?
39
40     envelope = RMail::Message.new
41     envelope.header["Content-Type"] = 'multipart/signed; protocol=application/pgp-signature; micalg=pgp-sha1'
42
43     envelope.add_part payload
44     signature = RMail::Message.make_attachment output, "application/pgp-signature", nil, "signature.asc"
45     envelope.add_part signature
46     envelope
47   end
48
49   def encrypt from, to, payload, sign=false
50     payload_fn = Tempfile.new "redwood.payload"
51     payload_fn.write format_payload(payload)
52     payload_fn.close
53
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?
58
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
63
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"
68
69     envelope = RMail::Message.new
70     envelope.header["Content-Type"] = 'multipart/encrypted; protocol="application/pgp-encrypted"'
71
72     envelope.add_part control
73     envelope.add_part encrypted_payload
74     envelope
75   end
76
77   def sign_and_encrypt from, to, payload
78     encrypt from, to, payload, true
79   end
80
81   def verify payload, signature # both RubyMail::Message objects
82     return unknown_status(cant_find_binary) unless @cmd
83
84     payload_fn = Tempfile.new "redwood.payload"
85     payload_fn.write format_payload(payload)
86     payload_fn.close
87
88     signature_fn = Tempfile.new "redwood.signature"
89     signature_fn.write signature.decode
90     signature_fn.close
91
92     output = run_gpg "--verify #{signature_fn.path} #{payload_fn.path}"
93     output_lines = output.split(/\n/)
94
95     if output =~ /^gpg: (.* signature from .*$)/
96       if $? == 0
97         Chunk::CryptoNotice.new :valid, $1, output_lines
98       else
99         Chunk::CryptoNotice.new :invalid, $1, output_lines
100       end
101     else
102       unknown_status output_lines
103     end
104   end
105
106   ## returns decrypted_message, status, desc, lines
107   def decrypt payload # a RubyMail::Message object
108     return unknown_status(cant_find_binary) unless @cmd
109
110     payload_fn = Tempfile.new "redwood.payload"
111     payload_fn.write payload.to_s
112     payload_fn.close
113
114     output = run_gpg "--decrypt #{payload_fn.path}"
115
116     if $?.success?
117       decrypted_payload, sig_lines = if output =~ /\A(.*?)((^gpg: .*$)+)\Z/m
118         [$1, $2]
119       else
120         [output, nil]
121       end
122
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")
126         else
127           Chunk::CryptoNotice.new :invalid, $1, sig_lines.split("\n")
128         end
129       end
130
131       notice = Chunk::CryptoNotice.new :valid, "This message has been decrypted for display"
132       [RMail::Parser.read(decrypted_payload), sig, notice]
133     else
134       notice = Chunk::CryptoNotice.new :invalid, "This message could not be decrypted", output.split("\n")
135       [nil, nil, notice]
136     end
137   end
138
139 private
140
141   def unknown_status lines=[]
142     Chunk::CryptoNotice.new :unknown, "Unable to determine validity of cryptographic signature", lines
143   end
144
145   def cant_find_binary
146     ["Can't find gpg binary in path."]
147   end
148
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/, "")
153   end
154
155   def run_gpg args
156     cmd = "#{@cmd} #{args} 2> /dev/null"
157     output = `#{cmd}`
158     output
159   end
160 end
161 end