]>
Commit | Line | Data |
---|---|---|
84fb5b46 MKG |
1 | # BEGIN BPS TAGGED BLOCK {{{ |
2 | # | |
3 | # COPYRIGHT: | |
4 | # | |
403d7b0b | 5 | # This software is Copyright (c) 1996-2013 Best Practical Solutions, LLC |
84fb5b46 MKG |
6 | # <sales@bestpractical.com> |
7 | # | |
8 | # (Except where explicitly superseded by other copyright notices) | |
9 | # | |
10 | # | |
11 | # LICENSE: | |
12 | # | |
13 | # This work is made available to you under the terms of Version 2 of | |
14 | # the GNU General Public License. A copy of that license should have | |
15 | # been provided with this software, but in any event can be snarfed | |
16 | # from www.gnu.org. | |
17 | # | |
18 | # This work is distributed in the hope that it will be useful, but | |
19 | # WITHOUT ANY WARRANTY; without even the implied warranty of | |
20 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
21 | # General Public License for more details. | |
22 | # | |
23 | # You should have received a copy of the GNU General Public License | |
24 | # along with this program; if not, write to the Free Software | |
25 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA | |
26 | # 02110-1301 or visit their web page on the internet at | |
27 | # http://www.gnu.org/licenses/old-licenses/gpl-2.0.html. | |
28 | # | |
29 | # | |
30 | # CONTRIBUTION SUBMISSION POLICY: | |
31 | # | |
32 | # (The following paragraph is not intended to limit the rights granted | |
33 | # to you to modify and distribute this software under the terms of | |
34 | # the GNU General Public License and is only of importance to you if | |
35 | # you choose to contribute your changes and enhancements to the | |
36 | # community by submitting them to Best Practical Solutions, LLC.) | |
37 | # | |
38 | # By intentionally submitting any modifications, corrections or | |
39 | # derivatives to this work, or any other work intended for use with | |
40 | # Request Tracker, to Best Practical Solutions, LLC, you confirm that | |
41 | # you are the copyright holder for those contributions and you grant | |
42 | # Best Practical Solutions, LLC a nonexclusive, worldwide, irrevocable, | |
43 | # royalty-free, perpetual, license to use, copy, create derivative | |
44 | # works based on those contributions, and sublicense and distribute | |
45 | # those contributions and any derivatives thereof. | |
46 | # | |
47 | # END BPS TAGGED BLOCK }}} | |
48 | ||
49 | use strict; | |
50 | use warnings; | |
51 | ||
52 | package RT::Crypt::GnuPG; | |
53 | ||
54 | use IO::Handle; | |
55 | use GnuPG::Interface; | |
56 | use RT::EmailParser (); | |
57 | use RT::Util 'safe_run_child', 'mime_recommended_filename'; | |
58 | ||
59 | =head1 NAME | |
60 | ||
61 | RT::Crypt::GnuPG - encrypt/decrypt and sign/verify email messages with the GNU Privacy Guard (GPG) | |
62 | ||
63 | =head1 DESCRIPTION | |
64 | ||
65 | This module provides support for encryption and signing of outgoing messages, | |
66 | as well as the decryption and verification of incoming email. | |
67 | ||
68 | =head1 CONFIGURATION | |
69 | ||
70 | You can control the configuration of this subsystem from RT's configuration file. | |
71 | Some options are available via the web interface, but to enable this functionality, you | |
72 | MUST start in the configuration file. | |
73 | ||
74 | There are two hashes, GnuPG and GnuPGOptions in the configuration file. The | |
75 | first one controls RT specific options. It enables you to enable/disable facility | |
76 | or change the format of messages. The second one is a hash with options for the | |
77 | 'gnupg' utility. You can use it to define a keyserver, enable auto-retrieval keys | |
78 | and set almost any option 'gnupg' supports on your system. | |
79 | ||
80 | =head2 %GnuPG | |
81 | ||
82 | =head3 Enabling GnuPG | |
83 | ||
84 | Set to true value to enable this subsystem: | |
85 | ||
86 | Set( %GnuPG, | |
87 | Enable => 1, | |
88 | ... other options ... | |
89 | ); | |
90 | ||
91 | However, note that you B<must> add the 'Auth::GnuPG' email filter to enable | |
92 | the handling of incoming encrypted/signed messages. | |
93 | ||
94 | =head3 Format of outgoing messages | |
95 | ||
96 | Format of outgoing messages can be controlled using the 'OutgoingMessagesFormat' | |
97 | option in the RT config: | |
98 | ||
99 | Set( %GnuPG, | |
100 | ... other options ... | |
101 | OutgoingMessagesFormat => 'RFC', | |
102 | ... other options ... | |
103 | ); | |
104 | ||
105 | or | |
106 | ||
107 | Set( %GnuPG, | |
108 | ... other options ... | |
109 | OutgoingMessagesFormat => 'Inline', | |
110 | ... other options ... | |
111 | ); | |
112 | ||
113 | This framework implements two formats of signing and encrypting of email messages: | |
114 | ||
115 | =over | |
116 | ||
117 | =item RFC | |
118 | ||
119 | This format is also known as GPG/MIME and described in RFC3156 and RFC1847. | |
120 | Technique described in these RFCs is well supported by many mail user | |
121 | agents (MUA), but some MUAs support only inline signatures and encryption, | |
122 | so it's possible to use inline format (see below). | |
123 | ||
124 | =item Inline | |
125 | ||
126 | This format doesn't take advantage of MIME, but some mail clients do | |
127 | not support GPG/MIME. | |
128 | ||
129 | We sign text parts using clear signatures. For each attachments another | |
130 | attachment with a signature is added with '.sig' extension. | |
131 | ||
132 | Encryption of text parts is implemented using inline format, other parts | |
133 | are replaced with attachments with the filename extension '.pgp'. | |
134 | ||
135 | This format is discouraged because modern mail clients typically don't support | |
136 | it well. | |
137 | ||
138 | =back | |
139 | ||
140 | =head3 Encrypting data in the database | |
141 | ||
142 | You can allow users to encrypt data in the database using | |
143 | option C<AllowEncryptDataInDB>. By default it's disabled. | |
144 | Users must have rights to see and modify tickets to use | |
145 | this feature. | |
146 | ||
147 | =head2 %GnuPGOptions | |
148 | ||
149 | Use this hash to set options of the 'gnupg' program. You can define almost any | |
150 | option you want which gnupg supports, but never try to set options which | |
151 | change output format or gnupg's commands, such as --sign (command), | |
152 | --list-options (option) and other. | |
153 | ||
154 | Some GnuPG options take arguments while others take none. (Such as --use-agent). | |
155 | For options without specific value use C<undef> as hash value. | |
156 | To disable these option just comment them out or delete them from the hash | |
157 | ||
158 | Set(%GnuPGOptions, | |
159 | 'option-with-value' => 'value', | |
160 | 'enabled-option-without-value' => undef, | |
161 | # 'commented-option' => 'value or undef', | |
162 | ); | |
163 | ||
164 | B<NOTE> that options may contain '-' character and such options B<MUST> be | |
165 | quoted, otherwise you can see quite cryptic error 'gpg: Invalid option "--0"'. | |
166 | ||
167 | =over | |
168 | ||
169 | =item --homedir | |
170 | ||
171 | The GnuPG home directory, by default it is set to F</opt/rt4/var/data/gpg>. | |
172 | ||
173 | You can manage this data with the 'gpg' commandline utility | |
174 | using the GNUPGHOME environment variable or --homedir option. | |
175 | Other utilities may be used as well. | |
176 | ||
177 | In a standard installation, access to this directory should be granted to | |
178 | the web server user which is running RT's web interface, but if you're running | |
179 | cronjobs or other utilities that access RT directly via API and may generate | |
180 | encrypted/signed notifications then the users you execute these scripts under | |
181 | must have access too. | |
182 | ||
183 | However, granting access to the dir to many users makes your setup less secure, | |
184 | some features, such as auto-import of keys, may not be available if you do not. | |
185 | To enable this features and suppress warnings about permissions on | |
186 | the dir use --no-permission-warning. | |
187 | ||
188 | =item --digest-algo | |
189 | ||
190 | This option is required in advance when RFC format for outgoing messages is | |
191 | used. We can not get default algorithm from gpg program so RT uses 'SHA1' by | |
192 | default. You may want to override it. You can use MD5, SHA1, RIPEMD160, | |
193 | SHA256 or other, however use `gpg --version` command to get information about | |
194 | supported algorithms by your gpg. These algorithms are listed as hash-functions. | |
195 | ||
196 | =item --use-agent | |
197 | ||
198 | This option lets you use GPG Agent to cache the passphrase of RT's key. See | |
199 | L<http://www.gnupg.org/documentation/manuals/gnupg/Invoking-GPG_002dAGENT.html> | |
200 | for information about GPG Agent. | |
201 | ||
202 | =item --passphrase | |
203 | ||
204 | This option lets you set the passphrase of RT's key directly. This option is | |
205 | special in that it isn't passed directly to GPG, but is put into a file that | |
206 | GPG then reads (which is more secure). The downside is that anyone who has read | |
207 | access to your RT_SiteConfig.pm file can see the passphrase, thus we recommend | |
208 | the --use-agent option instead. | |
209 | ||
210 | =item other | |
211 | ||
212 | Read `man gpg` to get list of all options this program support. | |
213 | ||
214 | =back | |
215 | ||
216 | =head2 Per-queue options | |
217 | ||
218 | Using the web interface it's possible to enable signing and/or encrypting by | |
219 | default. As an administrative user of RT, open 'Configuration' then 'Queues', | |
220 | and select a queue. On the page you can see information about the queue's keys | |
221 | at the bottom and two checkboxes to choose default actions. | |
222 | ||
223 | As well, encryption is enabled for autoreplies and other notifications when | |
224 | an encypted message enters system via mailgate interface even if queue's | |
225 | option is disabled. | |
226 | ||
227 | =head2 Handling incoming messages | |
228 | ||
229 | To enable handling of encrypted and signed message in the RT you should add | |
230 | 'Auth::GnuPG' mail plugin. | |
231 | ||
232 | Set(@MailPlugins, 'Auth::MailFrom', 'Auth::GnuPG', ...other filter...); | |
233 | ||
234 | See also `perldoc lib/RT/Interface/Email/Auth/GnuPG.pm`. | |
235 | ||
236 | =head2 Errors handling | |
237 | ||
238 | There are several global templates created in the database by default. RT | |
239 | uses these templates to send error messages to users or RT's owner. These | |
240 | templates have 'Error:' or 'Error to RT owner:' prefix in the name. You can | |
241 | adjust the text of the messages using the web interface. | |
242 | ||
243 | Note that C<$TicketObj>, C<$TransactionObj> and other variable usually available | |
244 | in RT's templates are not available in these templates, but each template | |
245 | used for errors reporting has set of available data structures you can use to | |
246 | build better messages. See default templates and descriptions below. | |
247 | ||
248 | As well, you can disable particular notification by deleting content of | |
249 | a template. You can delete a template too, but in this case you'll see | |
250 | error messages in the logs when RT can not load template you've deleted. | |
251 | ||
252 | =head3 Problems with public keys | |
253 | ||
254 | Template 'Error: public key' is used to inform the user that RT has problems with | |
255 | his public key and won't be able to send him encrypted content. There are several | |
256 | reasons why RT can't use a key. However, the actual reason is not sent to the user, | |
257 | but sent to RT owner using 'Error to RT owner: public key'. | |
258 | ||
259 | The possible reasons: "Not Found", "Ambiguous specification", "Wrong | |
260 | key usage", "Key revoked", "Key expired", "No CRL known", "CRL too | |
261 | old", "Policy mismatch", "Not a secret key", "Key not trusted" or | |
262 | "No specific reason given". | |
263 | ||
264 | Due to limitations of GnuPG, it's impossible to encrypt to an untrusted key, | |
265 | unless 'always trust' mode is enabled. | |
266 | ||
267 | In the 'Error: public key' template there are a few additional variables available: | |
268 | ||
269 | =over 4 | |
270 | ||
271 | =item $Message - user friendly error message | |
272 | ||
273 | =item $Reason - short reason as listed above | |
274 | ||
275 | =item $Recipient - recipient's identification | |
276 | ||
277 | =item $AddressObj - L<Email::Address> object containing recipient's email address | |
278 | ||
279 | =back | |
280 | ||
281 | A message can have several invalid recipients, to avoid sending many emails | |
282 | to the RT owner the system sends one message to the owner, grouped by | |
283 | recipient. In the 'Error to RT owner: public key' template a C<@BadRecipients> | |
284 | array is available where each element is a hash reference that describes one | |
285 | recipient using the same fields as described above. So it's something like: | |
286 | ||
287 | @BadRecipients = ( | |
288 | { Message => '...', Reason => '...', Recipient => '...', ...}, | |
289 | { Message => '...', Reason => '...', Recipient => '...', ...}, | |
290 | ... | |
291 | ) | |
292 | ||
293 | =head3 Private key doesn't exist | |
294 | ||
295 | Template 'Error: no private key' is used to inform the user that | |
296 | he sent an encrypted email, but we have no private key to decrypt | |
297 | it. | |
298 | ||
299 | In this template C<$Message> object of L<MIME::Entity> class | |
300 | available. It's the message RT received. | |
301 | ||
302 | =head3 Invalid data | |
303 | ||
304 | Template 'Error: bad GnuPG data' used to inform the user that a | |
305 | message he sent has invalid data and can not be handled. | |
306 | ||
307 | There are several reasons for this error, but most of them are data | |
308 | corruption or absence of expected information. | |
309 | ||
310 | In this template C<@Messages> array is available and contains list | |
311 | of error messages. | |
312 | ||
313 | =head1 FOR DEVELOPERS | |
314 | ||
315 | =head2 Documentation and references | |
316 | ||
317 | * RFC1847 - Security Multiparts for MIME: Multipart/Signed and Multipart/Encrypted. | |
318 | Describes generic MIME security framework, "mulitpart/signed" and "multipart/encrypted" | |
319 | MIME types. | |
320 | ||
321 | * RFC3156 - MIME Security with Pretty Good Privacy (PGP), | |
322 | updates RFC2015. | |
323 | ||
324 | =cut | |
325 | ||
326 | # gnupg options supported by GnuPG::Interface | |
327 | # other otions should be handled via extra_args argument | |
328 | my %supported_opt = map { $_ => 1 } qw( | |
329 | always_trust | |
330 | armor | |
331 | batch | |
332 | comment | |
333 | compress_algo | |
334 | default_key | |
335 | encrypt_to | |
336 | extra_args | |
337 | force_v3_sigs | |
338 | homedir | |
339 | logger_fd | |
340 | no_greeting | |
341 | no_options | |
342 | no_verbose | |
343 | openpgp | |
344 | options | |
345 | passphrase_fd | |
346 | quiet | |
347 | recipients | |
348 | rfc1991 | |
349 | status_fd | |
350 | textmode | |
351 | verbose | |
352 | ); | |
353 | ||
354 | our $RE_FILE_EXTENSIONS = qr/pgp|asc/i; | |
355 | ||
356 | # DEV WARNING: always pass all STD* handles to GnuPG interface even if we don't | |
357 | # need them, just pass 'IO::Handle->new()' and then close it after safe_run_child. | |
358 | # we don't want to leak anything into FCGI/Apache/MP handles, this break things. | |
359 | # So code should look like: | |
360 | # my $handles = GnuPG::Handles->new( | |
361 | # stdin => ($handle{'stdin'} = IO::Handle->new()), | |
362 | # stdout => ($handle{'stdout'} = IO::Handle->new()), | |
363 | # stderr => ($handle{'stderr'} = IO::Handle->new()), | |
364 | # ... | |
365 | # ); | |
366 | ||
367 | =head2 SignEncrypt Entity => MIME::Entity, [ Encrypt => 1, Sign => 1, ... ] | |
368 | ||
369 | Signs and/or encrypts an email message with GnuPG utility. | |
370 | ||
371 | =over | |
372 | ||
373 | =item Signing | |
374 | ||
375 | During signing you can pass C<Signer> argument to set key we sign with this option | |
376 | overrides gnupg's C<default-key> option. If C<Signer> argument is not provided | |
377 | then address of a message sender is used. | |
378 | ||
379 | As well you can pass C<Passphrase>, but if value is undefined then L</GetPassphrase> | |
380 | called to get it. | |
381 | ||
382 | =item Encrypting | |
383 | ||
384 | During encryption you can pass a C<Recipients> array, otherwise C<To>, C<Cc> and | |
385 | C<Bcc> fields of the message are used to fetch the list. | |
386 | ||
387 | =back | |
388 | ||
389 | Returns a hash with the following keys: | |
390 | ||
391 | * exit_code | |
392 | * error | |
393 | * logger | |
394 | * status | |
395 | * message | |
396 | ||
397 | =cut | |
398 | ||
399 | sub SignEncrypt { | |
400 | my %args = (@_); | |
401 | ||
402 | my $entity = $args{'Entity'}; | |
403 | if ( $args{'Sign'} && !defined $args{'Signer'} ) { | |
404 | $args{'Signer'} = UseKeyForSigning() | |
405 | || (Email::Address->parse( $entity->head->get( 'From' ) ))[0]->address; | |
406 | } | |
407 | if ( $args{'Encrypt'} && !$args{'Recipients'} ) { | |
408 | my %seen; | |
409 | $args{'Recipients'} = [ | |
410 | grep $_ && !$seen{ $_ }++, map $_->address, | |
411 | map Email::Address->parse( $entity->head->get( $_ ) ), | |
412 | qw(To Cc Bcc) | |
413 | ]; | |
414 | } | |
415 | ||
416 | my $format = lc RT->Config->Get('GnuPG')->{'OutgoingMessagesFormat'} || 'RFC'; | |
417 | if ( $format eq 'inline' ) { | |
418 | return SignEncryptInline( %args ); | |
419 | } else { | |
420 | return SignEncryptRFC3156( %args ); | |
421 | } | |
422 | } | |
423 | ||
424 | sub SignEncryptRFC3156 { | |
425 | my %args = ( | |
426 | Entity => undef, | |
427 | ||
428 | Sign => 1, | |
429 | Signer => undef, | |
430 | Passphrase => undef, | |
431 | ||
432 | Encrypt => 1, | |
433 | Recipients => undef, | |
434 | ||
435 | @_ | |
436 | ); | |
437 | ||
438 | my $gnupg = GnuPG::Interface->new(); | |
439 | my %opt = RT->Config->Get('GnuPGOptions'); | |
440 | ||
441 | # handling passphrase in GnuPGOptions | |
442 | $args{'Passphrase'} = delete $opt{'passphrase'} | |
443 | if !defined $args{'Passphrase'}; | |
444 | ||
445 | $opt{'digest-algo'} ||= 'SHA1'; | |
446 | $opt{'default_key'} = $args{'Signer'} | |
447 | if $args{'Sign'} && $args{'Signer'}; | |
448 | $gnupg->options->hash_init( | |
449 | _PrepareGnuPGOptions( %opt ), | |
450 | armor => 1, | |
451 | meta_interactive => 0, | |
452 | ); | |
453 | ||
454 | my $entity = $args{'Entity'}; | |
455 | ||
456 | if ( $args{'Sign'} && !defined $args{'Passphrase'} ) { | |
457 | $args{'Passphrase'} = GetPassphrase( Address => $args{'Signer'} ); | |
458 | } | |
459 | ||
460 | my %res; | |
461 | if ( $args{'Sign'} && !$args{'Encrypt'} ) { | |
462 | # required by RFC3156(Ch. 5) and RFC1847(Ch. 2.1) | |
463 | foreach ( grep !$_->is_multipart, $entity->parts_DFS ) { | |
464 | my $tenc = $_->head->mime_encoding; | |
465 | unless ( $tenc =~ m/^(?:7bit|quoted-printable|base64)$/i ) { | |
466 | $_->head->mime_attr( 'Content-Transfer-Encoding' | |
467 | => $_->effective_type =~ m{^text/}? 'quoted-printable': 'base64' | |
468 | ); | |
469 | } | |
470 | } | |
471 | ||
472 | my ($handles, $handle_list) = _make_gpg_handles(stdin =>IO::Handle::CRLF->new ); | |
473 | my %handle = %$handle_list; | |
474 | ||
475 | $gnupg->passphrase( $args{'Passphrase'} ); | |
476 | ||
477 | eval { | |
478 | local $SIG{'CHLD'} = 'DEFAULT'; | |
479 | my $pid = safe_run_child { $gnupg->detach_sign( handles => $handles ) }; | |
480 | $entity->make_multipart( 'mixed', Force => 1 ); | |
481 | { | |
482 | local $SIG{'PIPE'} = 'IGNORE'; | |
483 | $entity->parts(0)->print( $handle{'stdin'} ); | |
484 | close $handle{'stdin'}; | |
485 | } | |
486 | waitpid $pid, 0; | |
487 | }; | |
488 | my $err = $@; | |
489 | my @signature = readline $handle{'stdout'}; | |
490 | close $handle{'stdout'}; | |
491 | ||
492 | $res{'exit_code'} = $?; | |
493 | foreach ( qw(stderr logger status) ) { | |
494 | $res{$_} = do { local $/; readline $handle{$_} }; | |
495 | delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s; | |
496 | close $handle{$_}; | |
497 | } | |
498 | $RT::Logger->debug( $res{'status'} ) if $res{'status'}; | |
499 | $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'}; | |
500 | $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?; | |
501 | if ( $err || $res{'exit_code'} ) { | |
502 | $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8); | |
503 | return %res; | |
504 | } | |
505 | ||
506 | # setup RFC1847(Ch.2.1) requirements | |
507 | my $protocol = 'application/pgp-signature'; | |
508 | $entity->head->mime_attr( 'Content-Type' => 'multipart/signed' ); | |
509 | $entity->head->mime_attr( 'Content-Type.protocol' => $protocol ); | |
510 | $entity->head->mime_attr( 'Content-Type.micalg' => 'pgp-'. lc $opt{'digest-algo'} ); | |
511 | $entity->attach( | |
512 | Type => $protocol, | |
513 | Disposition => 'inline', | |
514 | Data => \@signature, | |
515 | Encoding => '7bit', | |
516 | ); | |
517 | } | |
518 | if ( $args{'Encrypt'} ) { | |
519 | my %seen; | |
520 | $gnupg->options->push_recipients( $_ ) foreach | |
521 | map UseKeyForEncryption($_) || $_, | |
522 | grep !$seen{ $_ }++, map $_->address, | |
523 | map Email::Address->parse( $entity->head->get( $_ ) ), | |
524 | qw(To Cc Bcc); | |
525 | ||
526 | my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 ); | |
527 | binmode $tmp_fh, ':raw'; | |
528 | ||
529 | my ($handles, $handle_list) = _make_gpg_handles(stdout => $tmp_fh); | |
530 | my %handle = %$handle_list; | |
531 | $handles->options( 'stdout' )->{'direct'} = 1; | |
532 | $gnupg->passphrase( $args{'Passphrase'} ) if $args{'Sign'}; | |
533 | ||
534 | eval { | |
535 | local $SIG{'CHLD'} = 'DEFAULT'; | |
536 | my $pid = safe_run_child { $args{'Sign'} | |
537 | ? $gnupg->sign_and_encrypt( handles => $handles ) | |
538 | : $gnupg->encrypt( handles => $handles ) }; | |
539 | $entity->make_multipart( 'mixed', Force => 1 ); | |
540 | { | |
541 | local $SIG{'PIPE'} = 'IGNORE'; | |
542 | $entity->parts(0)->print( $handle{'stdin'} ); | |
543 | close $handle{'stdin'}; | |
544 | } | |
545 | waitpid $pid, 0; | |
546 | }; | |
547 | ||
548 | $res{'exit_code'} = $?; | |
549 | foreach ( qw(stderr logger status) ) { | |
550 | $res{$_} = do { local $/; readline $handle{$_} }; | |
551 | delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s; | |
552 | close $handle{$_}; | |
553 | } | |
554 | $RT::Logger->debug( $res{'status'} ) if $res{'status'}; | |
555 | $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'}; | |
556 | $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?; | |
557 | if ( $@ || $? ) { | |
558 | $res{'message'} = $@? $@: "gpg exited with error code ". ($? >> 8); | |
559 | return %res; | |
560 | } | |
561 | ||
562 | my $protocol = 'application/pgp-encrypted'; | |
563 | $entity->parts([]); | |
564 | $entity->head->mime_attr( 'Content-Type' => 'multipart/encrypted' ); | |
565 | $entity->head->mime_attr( 'Content-Type.protocol' => $protocol ); | |
566 | $entity->attach( | |
567 | Type => $protocol, | |
568 | Disposition => 'inline', | |
569 | Data => ['Version: 1',''], | |
570 | Encoding => '7bit', | |
571 | ); | |
572 | $entity->attach( | |
573 | Type => 'application/octet-stream', | |
574 | Disposition => 'inline', | |
575 | Path => $tmp_fn, | |
576 | Filename => '', | |
577 | Encoding => '7bit', | |
578 | ); | |
579 | $entity->parts(-1)->bodyhandle->{'_dirty_hack_to_save_a_ref_tmp_fh'} = $tmp_fh; | |
580 | } | |
581 | return %res; | |
582 | } | |
583 | ||
584 | sub SignEncryptInline { | |
585 | my %args = ( @_ ); | |
586 | ||
587 | my $entity = $args{'Entity'}; | |
588 | ||
589 | my %res; | |
590 | $entity->make_singlepart; | |
591 | if ( $entity->is_multipart ) { | |
592 | foreach ( $entity->parts ) { | |
593 | %res = SignEncryptInline( @_, Entity => $_ ); | |
594 | return %res if $res{'exit_code'}; | |
595 | } | |
596 | return %res; | |
597 | } | |
598 | ||
599 | return _SignEncryptTextInline( @_ ) | |
600 | if $entity->effective_type =~ /^text\//i; | |
601 | ||
602 | return _SignEncryptAttachmentInline( @_ ); | |
603 | } | |
604 | ||
605 | sub _SignEncryptTextInline { | |
606 | my %args = ( | |
607 | Entity => undef, | |
608 | ||
609 | Sign => 1, | |
610 | Signer => undef, | |
611 | Passphrase => undef, | |
612 | ||
613 | Encrypt => 1, | |
614 | Recipients => undef, | |
615 | ||
616 | @_ | |
617 | ); | |
618 | return unless $args{'Sign'} || $args{'Encrypt'}; | |
619 | ||
620 | my $gnupg = GnuPG::Interface->new(); | |
621 | my %opt = RT->Config->Get('GnuPGOptions'); | |
622 | ||
623 | # handling passphrase in GnupGOptions | |
624 | $args{'Passphrase'} = delete $opt{'passphrase'} | |
625 | if !defined($args{'Passphrase'}); | |
626 | ||
627 | $opt{'digest-algo'} ||= 'SHA1'; | |
628 | $opt{'default_key'} = $args{'Signer'} | |
629 | if $args{'Sign'} && $args{'Signer'}; | |
630 | $gnupg->options->hash_init( | |
631 | _PrepareGnuPGOptions( %opt ), | |
632 | armor => 1, | |
633 | meta_interactive => 0, | |
634 | ); | |
635 | ||
636 | if ( $args{'Sign'} && !defined $args{'Passphrase'} ) { | |
637 | $args{'Passphrase'} = GetPassphrase( Address => $args{'Signer'} ); | |
638 | } | |
639 | ||
640 | if ( $args{'Encrypt'} ) { | |
641 | $gnupg->options->push_recipients( $_ ) foreach | |
642 | map UseKeyForEncryption($_) || $_, | |
643 | @{ $args{'Recipients'} || [] }; | |
644 | } | |
645 | ||
646 | my %res; | |
647 | ||
648 | my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 ); | |
649 | binmode $tmp_fh, ':raw'; | |
650 | ||
651 | my ($handles, $handle_list) = _make_gpg_handles(stdout => $tmp_fh); | |
652 | my %handle = %$handle_list; | |
653 | ||
654 | $handles->options( 'stdout' )->{'direct'} = 1; | |
655 | $gnupg->passphrase( $args{'Passphrase'} ) if $args{'Sign'}; | |
656 | ||
657 | my $entity = $args{'Entity'}; | |
658 | eval { | |
659 | local $SIG{'CHLD'} = 'DEFAULT'; | |
660 | my $method = $args{'Sign'} && $args{'Encrypt'} | |
661 | ? 'sign_and_encrypt' | |
662 | : ($args{'Sign'}? 'clearsign': 'encrypt'); | |
663 | my $pid = safe_run_child { $gnupg->$method( handles => $handles ) }; | |
664 | { | |
665 | local $SIG{'PIPE'} = 'IGNORE'; | |
666 | $entity->bodyhandle->print( $handle{'stdin'} ); | |
667 | close $handle{'stdin'}; | |
668 | } | |
669 | waitpid $pid, 0; | |
670 | }; | |
671 | $res{'exit_code'} = $?; | |
672 | my $err = $@; | |
673 | ||
674 | foreach ( qw(stderr logger status) ) { | |
675 | $res{$_} = do { local $/; readline $handle{$_} }; | |
676 | delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s; | |
677 | close $handle{$_}; | |
678 | } | |
679 | $RT::Logger->debug( $res{'status'} ) if $res{'status'}; | |
680 | $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'}; | |
681 | $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?; | |
682 | if ( $err || $res{'exit_code'} ) { | |
683 | $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8); | |
684 | return %res; | |
685 | } | |
686 | ||
687 | $entity->bodyhandle( MIME::Body::File->new( $tmp_fn) ); | |
688 | $entity->{'__store_tmp_handle_to_avoid_early_cleanup'} = $tmp_fh; | |
689 | ||
690 | return %res; | |
691 | } | |
692 | ||
693 | sub _SignEncryptAttachmentInline { | |
694 | my %args = ( | |
695 | Entity => undef, | |
696 | ||
697 | Sign => 1, | |
698 | Signer => undef, | |
699 | Passphrase => undef, | |
700 | ||
701 | Encrypt => 1, | |
702 | Recipients => undef, | |
703 | ||
704 | @_ | |
705 | ); | |
706 | return unless $args{'Sign'} || $args{'Encrypt'}; | |
707 | ||
708 | my $gnupg = GnuPG::Interface->new(); | |
709 | my %opt = RT->Config->Get('GnuPGOptions'); | |
710 | ||
711 | # handling passphrase in GnupGOptions | |
712 | $args{'Passphrase'} = delete $opt{'passphrase'} | |
713 | if !defined($args{'Passphrase'}); | |
714 | ||
715 | $opt{'digest-algo'} ||= 'SHA1'; | |
716 | $opt{'default_key'} = $args{'Signer'} | |
717 | if $args{'Sign'} && $args{'Signer'}; | |
718 | $gnupg->options->hash_init( | |
719 | _PrepareGnuPGOptions( %opt ), | |
720 | armor => 1, | |
721 | meta_interactive => 0, | |
722 | ); | |
723 | ||
724 | if ( $args{'Sign'} && !defined $args{'Passphrase'} ) { | |
725 | $args{'Passphrase'} = GetPassphrase( Address => $args{'Signer'} ); | |
726 | } | |
727 | ||
728 | my $entity = $args{'Entity'}; | |
729 | if ( $args{'Encrypt'} ) { | |
730 | $gnupg->options->push_recipients( $_ ) foreach | |
731 | map UseKeyForEncryption($_) || $_, | |
732 | @{ $args{'Recipients'} || [] }; | |
733 | } | |
734 | ||
735 | my %res; | |
736 | ||
737 | my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 ); | |
738 | binmode $tmp_fh, ':raw'; | |
739 | ||
740 | my ($handles, $handle_list) = _make_gpg_handles(stdout => $tmp_fh); | |
741 | my %handle = %$handle_list; | |
742 | $handles->options( 'stdout' )->{'direct'} = 1; | |
743 | $gnupg->passphrase( $args{'Passphrase'} ) if $args{'Sign'}; | |
744 | ||
745 | eval { | |
746 | local $SIG{'CHLD'} = 'DEFAULT'; | |
747 | my $method = $args{'Sign'} && $args{'Encrypt'} | |
748 | ? 'sign_and_encrypt' | |
749 | : ($args{'Sign'}? 'detach_sign': 'encrypt'); | |
750 | my $pid = safe_run_child { $gnupg->$method( handles => $handles ) }; | |
751 | { | |
752 | local $SIG{'PIPE'} = 'IGNORE'; | |
753 | $entity->bodyhandle->print( $handle{'stdin'} ); | |
754 | close $handle{'stdin'}; | |
755 | } | |
756 | waitpid $pid, 0; | |
757 | }; | |
758 | $res{'exit_code'} = $?; | |
759 | my $err = $@; | |
760 | ||
761 | foreach ( qw(stderr logger status) ) { | |
762 | $res{$_} = do { local $/; readline $handle{$_} }; | |
763 | delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s; | |
764 | close $handle{$_}; | |
765 | } | |
766 | $RT::Logger->debug( $res{'status'} ) if $res{'status'}; | |
767 | $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'}; | |
768 | $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?; | |
769 | if ( $err || $res{'exit_code'} ) { | |
770 | $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8); | |
771 | return %res; | |
772 | } | |
773 | ||
774 | my $filename = mime_recommended_filename( $entity ) || 'no_name'; | |
775 | if ( $args{'Sign'} && !$args{'Encrypt'} ) { | |
776 | $entity->make_multipart; | |
777 | $entity->attach( | |
778 | Type => 'application/octet-stream', | |
779 | Path => $tmp_fn, | |
780 | Filename => "$filename.sig", | |
781 | Disposition => 'attachment', | |
782 | ); | |
783 | } else { | |
784 | $entity->bodyhandle(MIME::Body::File->new( $tmp_fn) ); | |
785 | $entity->effective_type('application/octet-stream'); | |
786 | $entity->head->mime_attr( $_ => "$filename.pgp" ) | |
787 | foreach (qw(Content-Type.name Content-Disposition.filename)); | |
788 | ||
789 | } | |
790 | $entity->{'__store_tmp_handle_to_avoid_early_cleanup'} = $tmp_fh; | |
791 | ||
792 | return %res; | |
793 | } | |
794 | ||
795 | sub SignEncryptContent { | |
796 | my %args = ( | |
797 | Content => undef, | |
798 | ||
799 | Sign => 1, | |
800 | Signer => undef, | |
801 | Passphrase => undef, | |
802 | ||
803 | Encrypt => 1, | |
804 | Recipients => undef, | |
805 | ||
806 | @_ | |
807 | ); | |
808 | return unless $args{'Sign'} || $args{'Encrypt'}; | |
809 | ||
810 | my $gnupg = GnuPG::Interface->new(); | |
811 | my %opt = RT->Config->Get('GnuPGOptions'); | |
812 | ||
813 | # handling passphrase in GnupGOptions | |
814 | $args{'Passphrase'} = delete $opt{'passphrase'} | |
815 | if !defined($args{'Passphrase'}); | |
816 | ||
817 | $opt{'digest-algo'} ||= 'SHA1'; | |
818 | $opt{'default_key'} = $args{'Signer'} | |
819 | if $args{'Sign'} && $args{'Signer'}; | |
820 | $gnupg->options->hash_init( | |
821 | _PrepareGnuPGOptions( %opt ), | |
822 | armor => 1, | |
823 | meta_interactive => 0, | |
824 | ); | |
825 | ||
826 | if ( $args{'Sign'} && !defined $args{'Passphrase'} ) { | |
827 | $args{'Passphrase'} = GetPassphrase( Address => $args{'Signer'} ); | |
828 | } | |
829 | ||
830 | if ( $args{'Encrypt'} ) { | |
831 | $gnupg->options->push_recipients( $_ ) foreach | |
832 | map UseKeyForEncryption($_) || $_, | |
833 | @{ $args{'Recipients'} || [] }; | |
834 | } | |
835 | ||
836 | my %res; | |
837 | ||
838 | my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 ); | |
839 | binmode $tmp_fh, ':raw'; | |
840 | ||
841 | my ($handles, $handle_list) = _make_gpg_handles(stdout => $tmp_fh); | |
842 | my %handle = %$handle_list; | |
843 | $handles->options( 'stdout' )->{'direct'} = 1; | |
844 | $gnupg->passphrase( $args{'Passphrase'} ) if $args{'Sign'}; | |
845 | ||
846 | eval { | |
847 | local $SIG{'CHLD'} = 'DEFAULT'; | |
848 | my $method = $args{'Sign'} && $args{'Encrypt'} | |
849 | ? 'sign_and_encrypt' | |
850 | : ($args{'Sign'}? 'clearsign': 'encrypt'); | |
851 | my $pid = safe_run_child { $gnupg->$method( handles => $handles ) }; | |
852 | { | |
853 | local $SIG{'PIPE'} = 'IGNORE'; | |
854 | $handle{'stdin'}->print( ${ $args{'Content'} } ); | |
855 | close $handle{'stdin'}; | |
856 | } | |
857 | waitpid $pid, 0; | |
858 | }; | |
859 | $res{'exit_code'} = $?; | |
860 | my $err = $@; | |
861 | ||
862 | foreach ( qw(stderr logger status) ) { | |
863 | $res{$_} = do { local $/; readline $handle{$_} }; | |
864 | delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s; | |
865 | close $handle{$_}; | |
866 | } | |
867 | $RT::Logger->debug( $res{'status'} ) if $res{'status'}; | |
868 | $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'}; | |
869 | $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?; | |
870 | if ( $err || $res{'exit_code'} ) { | |
871 | $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8); | |
872 | return %res; | |
873 | } | |
874 | ||
875 | ${ $args{'Content'} } = ''; | |
876 | seek $tmp_fh, 0, 0; | |
877 | while (1) { | |
878 | my $status = read $tmp_fh, my $buf, 4*1024; | |
879 | unless ( defined $status ) { | |
880 | $RT::Logger->crit( "couldn't read message: $!" ); | |
881 | } elsif ( !$status ) { | |
882 | last; | |
883 | } | |
884 | ${ $args{'Content'} } .= $buf; | |
885 | } | |
886 | ||
887 | return %res; | |
888 | } | |
889 | ||
890 | sub FindProtectedParts { | |
891 | my %args = ( Entity => undef, CheckBody => 1, @_ ); | |
892 | my $entity = $args{'Entity'}; | |
893 | ||
894 | # inline PGP block, only in singlepart | |
895 | unless ( $entity->is_multipart ) { | |
896 | my $file = ($entity->head->recommended_filename||'') =~ /\.${RE_FILE_EXTENSIONS}$/; | |
897 | ||
898 | my $io = $entity->open('r'); | |
899 | unless ( $io ) { | |
900 | $RT::Logger->warning( "Entity of type ". $entity->effective_type ." has no body" ); | |
901 | return (); | |
902 | } | |
dab09ea8 MKG |
903 | |
904 | # Deal with "partitioned" PGP mail, which (contrary to common | |
905 | # sense) unnecessarily applies a base64 transfer encoding to PGP | |
906 | # mail (whose content is already base64-encoded). | |
907 | if ( $entity->bodyhandle->is_encoded and $entity->head->mime_encoding ) { | |
dab09ea8 MKG |
908 | my $decoder = MIME::Decoder->new( $entity->head->mime_encoding ); |
909 | if ($decoder) { | |
403d7b0b MKG |
910 | local $@; |
911 | eval { | |
912 | my $buf = ''; | |
913 | open my $fh, '>', \$buf | |
914 | or die "Couldn't open scalar for writing: $!"; | |
915 | binmode $fh, ":raw"; | |
916 | $decoder->decode($io, $fh); | |
917 | close $fh or die "Couldn't close scalar: $!"; | |
918 | ||
919 | open $fh, '<', \$buf | |
920 | or die "Couldn't re-open scalar for reading: $!"; | |
921 | binmode $fh, ":raw"; | |
922 | $io = $fh; | |
923 | 1; | |
924 | } or do { | |
925 | $RT::Logger->error("Couldn't decode body: $@"); | |
926 | } | |
dab09ea8 MKG |
927 | } |
928 | } | |
929 | ||
84fb5b46 MKG |
930 | while ( defined($_ = $io->getline) ) { |
931 | next unless /^-----BEGIN PGP (SIGNED )?MESSAGE-----/; | |
932 | my $type = $1? 'signed': 'encrypted'; | |
933 | $RT::Logger->debug("Found $type inline part"); | |
934 | return { | |
935 | Type => $type, | |
936 | Format => !$file || $type eq 'signed'? 'Inline' : 'Attachment', | |
937 | Data => $entity, | |
938 | }; | |
939 | } | |
940 | $io->close; | |
941 | return (); | |
942 | } | |
943 | ||
944 | # RFC3156, multipart/{signed,encrypted} | |
945 | if ( ( my $type = $entity->effective_type ) =~ /^multipart\/(?:encrypted|signed)$/ ) { | |
946 | unless ( $entity->parts == 2 ) { | |
947 | $RT::Logger->error( "Encrypted or signed entity must has two subparts. Skipped" ); | |
948 | return (); | |
949 | } | |
950 | ||
951 | my $protocol = $entity->head->mime_attr( 'Content-Type.protocol' ); | |
952 | unless ( $protocol ) { | |
953 | $RT::Logger->error( "Entity is '$type', but has no protocol defined. Skipped" ); | |
954 | return (); | |
955 | } | |
956 | ||
957 | if ( $type eq 'multipart/encrypted' ) { | |
958 | unless ( $protocol eq 'application/pgp-encrypted' ) { | |
959 | $RT::Logger->info( "Skipping protocol '$protocol', only 'application/pgp-encrypted' is supported" ); | |
960 | return (); | |
961 | } | |
962 | $RT::Logger->debug("Found encrypted according to RFC3156 part"); | |
963 | return { | |
964 | Type => 'encrypted', | |
965 | Format => 'RFC3156', | |
966 | Top => $entity, | |
967 | Data => $entity->parts(1), | |
968 | Info => $entity->parts(0), | |
969 | }; | |
970 | } else { | |
971 | unless ( $protocol eq 'application/pgp-signature' ) { | |
972 | $RT::Logger->info( "Skipping protocol '$protocol', only 'application/pgp-signature' is supported" ); | |
973 | return (); | |
974 | } | |
975 | $RT::Logger->debug("Found signed according to RFC3156 part"); | |
976 | return { | |
977 | Type => 'signed', | |
978 | Format => 'RFC3156', | |
979 | Top => $entity, | |
980 | Data => $entity->parts(0), | |
981 | Signature => $entity->parts(1), | |
982 | }; | |
983 | } | |
984 | } | |
985 | ||
986 | # attachments signed with signature in another part | |
987 | my @file_indices; | |
988 | foreach my $i ( 0 .. $entity->parts - 1 ) { | |
989 | my $part = $entity->parts($i); | |
990 | ||
991 | # we can not associate a signature within an attachment | |
992 | # without file names | |
993 | my $fname = $part->head->recommended_filename; | |
994 | next unless $fname; | |
995 | ||
996 | if ( $part->effective_type eq 'application/pgp-signature' ) { | |
997 | push @file_indices, $i; | |
998 | } | |
999 | elsif ( $fname =~ /\.sig$/i && $part->effective_type eq 'application/octet-stream' ) { | |
1000 | push @file_indices, $i; | |
1001 | } | |
1002 | } | |
1003 | ||
1004 | my (@res, %skip); | |
1005 | foreach my $i ( @file_indices ) { | |
1006 | my $sig_part = $entity->parts($i); | |
1007 | $skip{"$sig_part"}++; | |
1008 | my $sig_name = $sig_part->head->recommended_filename; | |
1009 | my ($file_name) = $sig_name =~ /^(.*?)(?:\.sig)?$/; | |
1010 | ||
1011 | my ($data_part_idx) = | |
1012 | grep $file_name eq ($entity->parts($_)->head->recommended_filename||''), | |
1013 | grep $sig_part ne $entity->parts($_), | |
1014 | 0 .. $entity->parts - 1; | |
1015 | unless ( defined $data_part_idx ) { | |
1016 | $RT::Logger->error("Found $sig_name attachment, but didn't find $file_name"); | |
1017 | next; | |
1018 | } | |
1019 | my $data_part_in = $entity->parts($data_part_idx); | |
1020 | ||
1021 | $skip{"$data_part_in"}++; | |
1022 | $RT::Logger->debug("Found signature (in '$sig_name') of attachment '$file_name'"); | |
1023 | push @res, { | |
1024 | Type => 'signed', | |
1025 | Format => 'Attachment', | |
1026 | Top => $entity, | |
1027 | Data => $data_part_in, | |
1028 | Signature => $sig_part, | |
1029 | }; | |
1030 | } | |
1031 | ||
1032 | # attachments with inline encryption | |
1033 | my @encrypted_indices = | |
1034 | grep {($entity->parts($_)->head->recommended_filename || '') =~ /\.${RE_FILE_EXTENSIONS}$/} | |
1035 | 0 .. $entity->parts - 1; | |
1036 | ||
1037 | foreach my $i ( @encrypted_indices ) { | |
1038 | my $part = $entity->parts($i); | |
1039 | $skip{"$part"}++; | |
1040 | $RT::Logger->debug("Found encrypted attachment '". $part->head->recommended_filename ."'"); | |
1041 | push @res, { | |
1042 | Type => 'encrypted', | |
1043 | Format => 'Attachment', | |
1044 | Top => $entity, | |
1045 | Data => $part, | |
1046 | }; | |
1047 | } | |
1048 | ||
1049 | push @res, FindProtectedParts( Entity => $_ ) | |
1050 | foreach grep !$skip{"$_"}, $entity->parts; | |
1051 | ||
1052 | return @res; | |
1053 | } | |
1054 | ||
1055 | =head2 VerifyDecrypt Entity => undef, [ Detach => 1, Passphrase => undef, SetStatus => 1 ] | |
1056 | ||
1057 | =cut | |
1058 | ||
1059 | sub VerifyDecrypt { | |
1060 | my %args = ( | |
1061 | Entity => undef, | |
1062 | Detach => 1, | |
1063 | SetStatus => 1, | |
1064 | AddStatus => 0, | |
1065 | @_ | |
1066 | ); | |
1067 | my @protected = FindProtectedParts( Entity => $args{'Entity'} ); | |
1068 | my @res; | |
1069 | # XXX: detaching may brake nested signatures | |
1070 | foreach my $item( grep $_->{'Type'} eq 'signed', @protected ) { | |
1071 | my $status_on; | |
1072 | if ( $item->{'Format'} eq 'RFC3156' ) { | |
1073 | push @res, { VerifyRFC3156( %$item, SetStatus => $args{'SetStatus'} ) }; | |
1074 | if ( $args{'Detach'} ) { | |
1075 | $item->{'Top'}->parts( [ $item->{'Data'} ] ); | |
1076 | $item->{'Top'}->make_singlepart; | |
1077 | } | |
1078 | $status_on = $item->{'Top'}; | |
1079 | } elsif ( $item->{'Format'} eq 'Inline' ) { | |
1080 | push @res, { VerifyInline( %$item ) }; | |
1081 | $status_on = $item->{'Data'}; | |
1082 | } elsif ( $item->{'Format'} eq 'Attachment' ) { | |
1083 | push @res, { VerifyAttachment( %$item ) }; | |
1084 | if ( $args{'Detach'} ) { | |
1085 | $item->{'Top'}->parts( [ | |
1086 | grep "$_" ne $item->{'Signature'}, $item->{'Top'}->parts | |
1087 | ] ); | |
1088 | $item->{'Top'}->make_singlepart; | |
1089 | } | |
1090 | $status_on = $item->{'Data'}; | |
1091 | } | |
1092 | if ( $args{'SetStatus'} || $args{'AddStatus'} ) { | |
1093 | my $method = $args{'AddStatus'} ? 'add' : 'set'; | |
dab09ea8 MKG |
1094 | # Let the header be modified so continuations are handled |
1095 | my $modify = $status_on->head->modify; | |
1096 | $status_on->head->modify(1); | |
84fb5b46 MKG |
1097 | $status_on->head->$method( |
1098 | 'X-RT-GnuPG-Status' => $res[-1]->{'status'} | |
1099 | ); | |
dab09ea8 | 1100 | $status_on->head->modify($modify); |
84fb5b46 MKG |
1101 | } |
1102 | } | |
1103 | foreach my $item( grep $_->{'Type'} eq 'encrypted', @protected ) { | |
1104 | my $status_on; | |
1105 | if ( $item->{'Format'} eq 'RFC3156' ) { | |
1106 | push @res, { DecryptRFC3156( %$item ) }; | |
1107 | $status_on = $item->{'Top'}; | |
1108 | } elsif ( $item->{'Format'} eq 'Inline' ) { | |
1109 | push @res, { DecryptInline( %$item ) }; | |
1110 | $status_on = $item->{'Data'}; | |
1111 | } elsif ( $item->{'Format'} eq 'Attachment' ) { | |
1112 | push @res, { DecryptAttachment( %$item ) }; | |
1113 | $status_on = $item->{'Data'}; | |
1114 | } | |
1115 | if ( $args{'SetStatus'} || $args{'AddStatus'} ) { | |
1116 | my $method = $args{'AddStatus'} ? 'add' : 'set'; | |
dab09ea8 MKG |
1117 | # Let the header be modified so continuations are handled |
1118 | my $modify = $status_on->head->modify; | |
1119 | $status_on->head->modify(1); | |
84fb5b46 MKG |
1120 | $status_on->head->$method( |
1121 | 'X-RT-GnuPG-Status' => $res[-1]->{'status'} | |
1122 | ); | |
dab09ea8 | 1123 | $status_on->head->modify($modify); |
84fb5b46 MKG |
1124 | } |
1125 | } | |
1126 | return @res; | |
1127 | } | |
1128 | ||
1129 | sub VerifyInline { return DecryptInline( @_ ) } | |
1130 | ||
1131 | sub VerifyAttachment { | |
1132 | my %args = ( Data => undef, Signature => undef, Top => undef, @_ ); | |
1133 | ||
1134 | my $gnupg = GnuPG::Interface->new(); | |
1135 | my %opt = RT->Config->Get('GnuPGOptions'); | |
1136 | $opt{'digest-algo'} ||= 'SHA1'; | |
1137 | $gnupg->options->hash_init( | |
1138 | _PrepareGnuPGOptions( %opt ), | |
1139 | meta_interactive => 0, | |
1140 | ); | |
1141 | ||
1142 | foreach ( $args{'Data'}, $args{'Signature'} ) { | |
1143 | next unless $_->bodyhandle->is_encoded; | |
1144 | ||
1145 | require RT::EmailParser; | |
1146 | RT::EmailParser->_DecodeBody($_); | |
1147 | } | |
1148 | ||
1149 | my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 ); | |
1150 | binmode $tmp_fh, ':raw'; | |
1151 | $args{'Data'}->bodyhandle->print( $tmp_fh ); | |
1152 | $tmp_fh->flush; | |
1153 | ||
1154 | my ($handles, $handle_list) = _make_gpg_handles(); | |
1155 | my %handle = %$handle_list; | |
1156 | ||
1157 | my %res; | |
1158 | eval { | |
1159 | local $SIG{'CHLD'} = 'DEFAULT'; | |
1160 | my $pid = safe_run_child { $gnupg->verify( | |
1161 | handles => $handles, command_args => [ '-', $tmp_fn ] | |
1162 | ) }; | |
1163 | { | |
1164 | local $SIG{'PIPE'} = 'IGNORE'; | |
1165 | $args{'Signature'}->bodyhandle->print( $handle{'stdin'} ); | |
1166 | close $handle{'stdin'}; | |
1167 | } | |
1168 | waitpid $pid, 0; | |
1169 | }; | |
1170 | $res{'exit_code'} = $?; | |
1171 | foreach ( qw(stderr logger status) ) { | |
1172 | $res{$_} = do { local $/; readline $handle{$_} }; | |
1173 | delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s; | |
1174 | close $handle{$_}; | |
1175 | } | |
1176 | $RT::Logger->debug( $res{'status'} ) if $res{'status'}; | |
1177 | $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'}; | |
1178 | $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?; | |
1179 | if ( $@ || $? ) { | |
1180 | $res{'message'} = $@? $@: "gpg exitted with error code ". ($? >> 8); | |
1181 | } | |
1182 | return %res; | |
1183 | } | |
1184 | ||
1185 | sub VerifyRFC3156 { | |
1186 | my %args = ( Data => undef, Signature => undef, Top => undef, @_ ); | |
1187 | ||
1188 | my $gnupg = GnuPG::Interface->new(); | |
1189 | my %opt = RT->Config->Get('GnuPGOptions'); | |
1190 | $opt{'digest-algo'} ||= 'SHA1'; | |
1191 | $gnupg->options->hash_init( | |
1192 | _PrepareGnuPGOptions( %opt ), | |
1193 | meta_interactive => 0, | |
1194 | ); | |
1195 | ||
1196 | my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 ); | |
1197 | binmode $tmp_fh, ':raw:eol(CRLF?)'; | |
1198 | $args{'Data'}->print( $tmp_fh ); | |
1199 | $tmp_fh->flush; | |
1200 | ||
1201 | my ($handles, $handle_list) = _make_gpg_handles(); | |
1202 | my %handle = %$handle_list; | |
1203 | ||
1204 | my %res; | |
1205 | eval { | |
1206 | local $SIG{'CHLD'} = 'DEFAULT'; | |
1207 | my $pid = safe_run_child { $gnupg->verify( | |
1208 | handles => $handles, command_args => [ '-', $tmp_fn ] | |
1209 | ) }; | |
1210 | { | |
1211 | local $SIG{'PIPE'} = 'IGNORE'; | |
1212 | $args{'Signature'}->bodyhandle->print( $handle{'stdin'} ); | |
1213 | close $handle{'stdin'}; | |
1214 | } | |
1215 | waitpid $pid, 0; | |
1216 | }; | |
1217 | $res{'exit_code'} = $?; | |
1218 | foreach ( qw(stderr logger status) ) { | |
1219 | $res{$_} = do { local $/; readline $handle{$_} }; | |
1220 | delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s; | |
1221 | close $handle{$_}; | |
1222 | } | |
1223 | $RT::Logger->debug( $res{'status'} ) if $res{'status'}; | |
1224 | $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'}; | |
1225 | $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?; | |
1226 | if ( $@ || $? ) { | |
1227 | $res{'message'} = $@? $@: "gpg exitted with error code ". ($? >> 8); | |
1228 | } | |
1229 | return %res; | |
1230 | } | |
1231 | ||
1232 | sub DecryptRFC3156 { | |
1233 | my %args = ( | |
1234 | Data => undef, | |
1235 | Info => undef, | |
1236 | Top => undef, | |
1237 | Passphrase => undef, | |
1238 | @_ | |
1239 | ); | |
1240 | ||
1241 | my $gnupg = GnuPG::Interface->new(); | |
1242 | my %opt = RT->Config->Get('GnuPGOptions'); | |
1243 | ||
1244 | # handling passphrase in GnupGOptions | |
1245 | $args{'Passphrase'} = delete $opt{'passphrase'} | |
1246 | if !defined($args{'Passphrase'}); | |
1247 | ||
1248 | $opt{'digest-algo'} ||= 'SHA1'; | |
1249 | $gnupg->options->hash_init( | |
1250 | _PrepareGnuPGOptions( %opt ), | |
1251 | meta_interactive => 0, | |
1252 | ); | |
1253 | ||
1254 | if ( $args{'Data'}->bodyhandle->is_encoded ) { | |
1255 | require RT::EmailParser; | |
1256 | RT::EmailParser->_DecodeBody($args{'Data'}); | |
1257 | } | |
1258 | ||
1259 | $args{'Passphrase'} = GetPassphrase() | |
1260 | unless defined $args{'Passphrase'}; | |
1261 | ||
1262 | my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 ); | |
1263 | binmode $tmp_fh, ':raw'; | |
1264 | ||
1265 | my ($handles, $handle_list) = _make_gpg_handles(stdout => $tmp_fh); | |
1266 | my %handle = %$handle_list; | |
1267 | $handles->options( 'stdout' )->{'direct'} = 1; | |
1268 | ||
1269 | my %res; | |
1270 | eval { | |
1271 | local $SIG{'CHLD'} = 'DEFAULT'; | |
1272 | $gnupg->passphrase( $args{'Passphrase'} ); | |
1273 | my $pid = safe_run_child { $gnupg->decrypt( handles => $handles ) }; | |
1274 | { | |
1275 | local $SIG{'PIPE'} = 'IGNORE'; | |
1276 | $args{'Data'}->bodyhandle->print( $handle{'stdin'} ); | |
1277 | close $handle{'stdin'} | |
1278 | } | |
1279 | ||
1280 | waitpid $pid, 0; | |
1281 | }; | |
1282 | $res{'exit_code'} = $?; | |
1283 | foreach ( qw(stderr logger status) ) { | |
1284 | $res{$_} = do { local $/; readline $handle{$_} }; | |
1285 | delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s; | |
1286 | close $handle{$_}; | |
1287 | } | |
1288 | $RT::Logger->debug( $res{'status'} ) if $res{'status'}; | |
1289 | $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'}; | |
1290 | $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?; | |
1291 | ||
1292 | # if the decryption is fine but the signature is bad, then without this | |
1293 | # status check we lose the decrypted text | |
1294 | # XXX: add argument to the function to control this check | |
1295 | if ( $res{'status'} !~ /DECRYPTION_OKAY/ ) { | |
1296 | if ( $@ || $? ) { | |
1297 | $res{'message'} = $@? $@: "gpg exitted with error code ". ($? >> 8); | |
1298 | return %res; | |
1299 | } | |
1300 | } | |
1301 | ||
1302 | seek $tmp_fh, 0, 0; | |
1303 | my $parser = RT::EmailParser->new(); | |
1304 | my $decrypted = $parser->ParseMIMEEntityFromFileHandle( $tmp_fh, 0 ); | |
1305 | $decrypted->{'__store_link_to_object_to_avoid_early_cleanup'} = $parser; | |
1306 | $args{'Top'}->parts( [] ); | |
1307 | $args{'Top'}->add_part( $decrypted ); | |
1308 | $args{'Top'}->make_singlepart; | |
1309 | return %res; | |
1310 | } | |
1311 | ||
1312 | sub DecryptInline { | |
1313 | my %args = ( | |
1314 | Data => undef, | |
1315 | Passphrase => undef, | |
1316 | @_ | |
1317 | ); | |
1318 | ||
1319 | my $gnupg = GnuPG::Interface->new(); | |
1320 | my %opt = RT->Config->Get('GnuPGOptions'); | |
1321 | ||
1322 | # handling passphrase in GnuPGOptions | |
1323 | $args{'Passphrase'} = delete $opt{'passphrase'} | |
1324 | if !defined($args{'Passphrase'}); | |
1325 | ||
1326 | $opt{'digest-algo'} ||= 'SHA1'; | |
1327 | $gnupg->options->hash_init( | |
1328 | _PrepareGnuPGOptions( %opt ), | |
1329 | meta_interactive => 0, | |
1330 | ); | |
1331 | ||
1332 | if ( $args{'Data'}->bodyhandle->is_encoded ) { | |
1333 | require RT::EmailParser; | |
1334 | RT::EmailParser->_DecodeBody($args{'Data'}); | |
1335 | } | |
1336 | ||
1337 | $args{'Passphrase'} = GetPassphrase() | |
1338 | unless defined $args{'Passphrase'}; | |
1339 | ||
1340 | my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 ); | |
1341 | binmode $tmp_fh, ':raw'; | |
1342 | ||
1343 | my $io = $args{'Data'}->open('r'); | |
1344 | unless ( $io ) { | |
1345 | die "Entity has no body, never should happen"; | |
1346 | } | |
1347 | ||
1348 | my %res; | |
1349 | ||
1350 | my ($had_literal, $in_block) = ('', 0); | |
1351 | my ($block_fh, $block_fn) = File::Temp::tempfile( UNLINK => 1 ); | |
1352 | binmode $block_fh, ':raw'; | |
1353 | ||
1354 | while ( defined(my $str = $io->getline) ) { | |
1355 | if ( $in_block && $str =~ /^-----END PGP (?:MESSAGE|SIGNATURE)-----/ ) { | |
1356 | print $block_fh $str; | |
1357 | $in_block--; | |
1358 | next if $in_block > 0; | |
1359 | ||
1360 | seek $block_fh, 0, 0; | |
1361 | ||
1362 | my ($res_fh, $res_fn); | |
1363 | ($res_fh, $res_fn, %res) = _DecryptInlineBlock( | |
1364 | %args, | |
1365 | GnuPG => $gnupg, | |
1366 | BlockHandle => $block_fh, | |
1367 | ); | |
1368 | return %res unless $res_fh; | |
1369 | ||
1370 | print $tmp_fh "-----BEGIN OF PGP PROTECTED PART-----\n" if $had_literal; | |
1371 | while (my $buf = <$res_fh> ) { | |
1372 | print $tmp_fh $buf; | |
1373 | } | |
1374 | print $tmp_fh "-----END OF PART-----\n" if $had_literal; | |
1375 | ||
1376 | ($block_fh, $block_fn) = File::Temp::tempfile( UNLINK => 1 ); | |
1377 | binmode $block_fh, ':raw'; | |
1378 | $in_block = 0; | |
1379 | } | |
1380 | elsif ( $str =~ /^-----BEGIN PGP (SIGNED )?MESSAGE-----/ ) { | |
1381 | $in_block++; | |
1382 | print $block_fh $str; | |
1383 | } | |
1384 | elsif ( $in_block ) { | |
1385 | print $block_fh $str; | |
1386 | } | |
1387 | else { | |
1388 | print $tmp_fh $str; | |
1389 | $had_literal = 1 if /\S/s; | |
1390 | } | |
1391 | } | |
1392 | $io->close; | |
1393 | ||
1394 | if ( $in_block ) { | |
1395 | # we're still in a block, this not bad not good. let's try to | |
1396 | # decrypt what we have, it can be just missing -----END PGP... | |
1397 | seek $block_fh, 0, 0; | |
1398 | ||
1399 | my ($res_fh, $res_fn); | |
1400 | ($res_fh, $res_fn, %res) = _DecryptInlineBlock( | |
1401 | %args, | |
1402 | GnuPG => $gnupg, | |
1403 | BlockHandle => $block_fh, | |
1404 | ); | |
1405 | return %res unless $res_fh; | |
1406 | ||
1407 | print $tmp_fh "-----BEGIN OF PGP PROTECTED PART-----\n" if $had_literal; | |
1408 | while (my $buf = <$res_fh> ) { | |
1409 | print $tmp_fh $buf; | |
1410 | } | |
1411 | print $tmp_fh "-----END OF PART-----\n" if $had_literal; | |
1412 | } | |
1413 | ||
1414 | seek $tmp_fh, 0, 0; | |
1415 | $args{'Data'}->bodyhandle(MIME::Body::File->new( $tmp_fn )); | |
1416 | $args{'Data'}->{'__store_tmp_handle_to_avoid_early_cleanup'} = $tmp_fh; | |
1417 | return %res; | |
1418 | } | |
1419 | ||
1420 | sub _DecryptInlineBlock { | |
1421 | my %args = ( | |
1422 | GnuPG => undef, | |
1423 | BlockHandle => undef, | |
1424 | Passphrase => undef, | |
1425 | @_ | |
1426 | ); | |
1427 | my $gnupg = $args{'GnuPG'}; | |
1428 | ||
1429 | my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 ); | |
1430 | binmode $tmp_fh, ':raw'; | |
1431 | ||
1432 | my ($handles, $handle_list) = _make_gpg_handles( | |
1433 | stdin => $args{'BlockHandle'}, | |
1434 | stdout => $tmp_fh); | |
1435 | my %handle = %$handle_list; | |
1436 | $handles->options( 'stdout' )->{'direct'} = 1; | |
1437 | $handles->options( 'stdin' )->{'direct'} = 1; | |
1438 | ||
1439 | my %res; | |
1440 | eval { | |
1441 | local $SIG{'CHLD'} = 'DEFAULT'; | |
1442 | $gnupg->passphrase( $args{'Passphrase'} ); | |
1443 | my $pid = safe_run_child { $gnupg->decrypt( handles => $handles ) }; | |
1444 | waitpid $pid, 0; | |
1445 | }; | |
1446 | $res{'exit_code'} = $?; | |
1447 | foreach ( qw(stderr logger status) ) { | |
1448 | $res{$_} = do { local $/; readline $handle{$_} }; | |
1449 | delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s; | |
1450 | close $handle{$_}; | |
1451 | } | |
1452 | $RT::Logger->debug( $res{'status'} ) if $res{'status'}; | |
1453 | $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'}; | |
1454 | $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?; | |
1455 | ||
1456 | # if the decryption is fine but the signature is bad, then without this | |
1457 | # status check we lose the decrypted text | |
1458 | # XXX: add argument to the function to control this check | |
1459 | if ( $res{'status'} !~ /DECRYPTION_OKAY/ ) { | |
1460 | if ( $@ || $? ) { | |
1461 | $res{'message'} = $@? $@: "gpg exitted with error code ". ($? >> 8); | |
1462 | return (undef, undef, %res); | |
1463 | } | |
1464 | } | |
1465 | ||
1466 | seek $tmp_fh, 0, 0; | |
1467 | return ($tmp_fh, $tmp_fn, %res); | |
1468 | } | |
1469 | ||
1470 | sub DecryptAttachment { | |
1471 | my %args = ( | |
1472 | Top => undef, | |
1473 | Data => undef, | |
1474 | Passphrase => undef, | |
1475 | @_ | |
1476 | ); | |
1477 | ||
1478 | my $gnupg = GnuPG::Interface->new(); | |
1479 | my %opt = RT->Config->Get('GnuPGOptions'); | |
1480 | ||
1481 | # handling passphrase in GnuPGOptions | |
1482 | $args{'Passphrase'} = delete $opt{'passphrase'} | |
1483 | if !defined($args{'Passphrase'}); | |
1484 | ||
1485 | $opt{'digest-algo'} ||= 'SHA1'; | |
1486 | $gnupg->options->hash_init( | |
1487 | _PrepareGnuPGOptions( %opt ), | |
1488 | meta_interactive => 0, | |
1489 | ); | |
1490 | ||
1491 | if ( $args{'Data'}->bodyhandle->is_encoded ) { | |
1492 | require RT::EmailParser; | |
1493 | RT::EmailParser->_DecodeBody($args{'Data'}); | |
1494 | } | |
1495 | ||
1496 | $args{'Passphrase'} = GetPassphrase() | |
1497 | unless defined $args{'Passphrase'}; | |
1498 | ||
1499 | my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 ); | |
1500 | binmode $tmp_fh, ':raw'; | |
1501 | $args{'Data'}->bodyhandle->print( $tmp_fh ); | |
1502 | seek $tmp_fh, 0, 0; | |
1503 | ||
1504 | my ($res_fh, $res_fn, %res) = _DecryptInlineBlock( | |
1505 | %args, | |
1506 | GnuPG => $gnupg, | |
1507 | BlockHandle => $tmp_fh, | |
1508 | ); | |
1509 | return %res unless $res_fh; | |
1510 | ||
1511 | $args{'Data'}->bodyhandle(MIME::Body::File->new($res_fn) ); | |
1512 | $args{'Data'}->{'__store_tmp_handle_to_avoid_early_cleanup'} = $res_fh; | |
1513 | ||
1514 | my $head = $args{'Data'}->head; | |
1515 | ||
1516 | # we can not trust original content type | |
1517 | # TODO: and don't have way to detect, so we just use octet-stream | |
1518 | # some clients may send .asc files (encryped) as text/plain | |
1519 | $head->mime_attr( "Content-Type" => 'application/octet-stream' ); | |
1520 | ||
1521 | my $filename = $head->recommended_filename; | |
1522 | $filename =~ s/\.${RE_FILE_EXTENSIONS}$//i; | |
1523 | $head->mime_attr( $_ => $filename ) | |
1524 | foreach (qw(Content-Type.name Content-Disposition.filename)); | |
1525 | ||
1526 | return %res; | |
1527 | } | |
1528 | ||
1529 | sub DecryptContent { | |
1530 | my %args = ( | |
1531 | Content => undef, | |
1532 | Passphrase => undef, | |
1533 | @_ | |
1534 | ); | |
1535 | ||
1536 | my $gnupg = GnuPG::Interface->new(); | |
1537 | my %opt = RT->Config->Get('GnuPGOptions'); | |
1538 | ||
1539 | # handling passphrase in GnupGOptions | |
1540 | $args{'Passphrase'} = delete $opt{'passphrase'} | |
1541 | if !defined($args{'Passphrase'}); | |
1542 | ||
1543 | $opt{'digest-algo'} ||= 'SHA1'; | |
1544 | $gnupg->options->hash_init( | |
1545 | _PrepareGnuPGOptions( %opt ), | |
1546 | meta_interactive => 0, | |
1547 | ); | |
1548 | ||
1549 | $args{'Passphrase'} = GetPassphrase() | |
1550 | unless defined $args{'Passphrase'}; | |
1551 | ||
1552 | my ($tmp_fh, $tmp_fn) = File::Temp::tempfile( UNLINK => 1 ); | |
1553 | binmode $tmp_fh, ':raw'; | |
1554 | ||
1555 | my ($handles, $handle_list) = _make_gpg_handles( | |
1556 | stdout => $tmp_fh); | |
1557 | my %handle = %$handle_list; | |
1558 | $handles->options( 'stdout' )->{'direct'} = 1; | |
1559 | ||
1560 | my %res; | |
1561 | eval { | |
1562 | local $SIG{'CHLD'} = 'DEFAULT'; | |
1563 | $gnupg->passphrase( $args{'Passphrase'} ); | |
1564 | my $pid = safe_run_child { $gnupg->decrypt( handles => $handles ) }; | |
1565 | { | |
1566 | local $SIG{'PIPE'} = 'IGNORE'; | |
1567 | print { $handle{'stdin'} } ${ $args{'Content'} }; | |
1568 | close $handle{'stdin'}; | |
1569 | } | |
1570 | ||
1571 | waitpid $pid, 0; | |
1572 | }; | |
1573 | $res{'exit_code'} = $?; | |
1574 | foreach ( qw(stderr logger status) ) { | |
1575 | $res{$_} = do { local $/; readline $handle{$_} }; | |
1576 | delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s; | |
1577 | close $handle{$_}; | |
1578 | } | |
1579 | $RT::Logger->debug( $res{'status'} ) if $res{'status'}; | |
1580 | $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'}; | |
1581 | $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?; | |
1582 | ||
1583 | # if the decryption is fine but the signature is bad, then without this | |
1584 | # status check we lose the decrypted text | |
1585 | # XXX: add argument to the function to control this check | |
1586 | if ( $res{'status'} !~ /DECRYPTION_OKAY/ ) { | |
1587 | if ( $@ || $? ) { | |
1588 | $res{'message'} = $@? $@: "gpg exitted with error code ". ($? >> 8); | |
1589 | return %res; | |
1590 | } | |
1591 | } | |
1592 | ||
1593 | ${ $args{'Content'} } = ''; | |
1594 | seek $tmp_fh, 0, 0; | |
1595 | while (1) { | |
1596 | my $status = read $tmp_fh, my $buf, 4*1024; | |
1597 | unless ( defined $status ) { | |
1598 | $RT::Logger->crit( "couldn't read message: $!" ); | |
1599 | } elsif ( !$status ) { | |
1600 | last; | |
1601 | } | |
1602 | ${ $args{'Content'} } .= $buf; | |
1603 | } | |
1604 | ||
1605 | return %res; | |
1606 | } | |
1607 | ||
1608 | =head2 GetPassphrase [ Address => undef ] | |
1609 | ||
1610 | Returns passphrase, called whenever it's required with Address as a named argument. | |
1611 | ||
1612 | =cut | |
1613 | ||
1614 | sub GetPassphrase { | |
1615 | my %args = ( Address => undef, @_ ); | |
1616 | return 'test'; | |
1617 | } | |
1618 | ||
1619 | =head2 ParseStatus | |
1620 | ||
1621 | Takes a string containing output of gnupg status stream. Parses it and returns | |
1622 | array of hashes. Each element of array is a hash ref and represents line or | |
1623 | group of lines in the status message. | |
1624 | ||
1625 | All hashes have Operation, Status and Message elements. | |
1626 | ||
1627 | =over | |
1628 | ||
1629 | =item Operation | |
1630 | ||
1631 | Classification of operations gnupg performs. Now we have support | |
1632 | for Sign, Encrypt, Decrypt, Verify, PassphraseCheck, RecipientsCheck and Data | |
1633 | values. | |
1634 | ||
1635 | =item Status | |
1636 | ||
1637 | Informs about success. Value is 'DONE' on success, other values means that | |
1638 | an operation failed, for example 'ERROR', 'BAD', 'MISSING' and may be other. | |
1639 | ||
1640 | =item Message | |
1641 | ||
1642 | User friendly message. | |
1643 | ||
1644 | =back | |
1645 | ||
1646 | This parser is based on information from GnuPG distribution. | |
1647 | ||
1648 | =cut | |
1649 | ||
1650 | my %REASON_CODE_TO_TEXT = ( | |
1651 | NODATA => { | |
1652 | 1 => "No armored data", | |
1653 | 2 => "Expected a packet, but did not found one", | |
1654 | 3 => "Invalid packet found", | |
1655 | 4 => "Signature expected, but not found", | |
1656 | }, | |
1657 | INV_RECP => { | |
1658 | 0 => "No specific reason given", | |
1659 | 1 => "Not Found", | |
1660 | 2 => "Ambigious specification", | |
1661 | 3 => "Wrong key usage", | |
1662 | 4 => "Key revoked", | |
1663 | 5 => "Key expired", | |
1664 | 6 => "No CRL known", | |
1665 | 7 => "CRL too old", | |
1666 | 8 => "Policy mismatch", | |
1667 | 9 => "Not a secret key", | |
1668 | 10 => "Key not trusted", | |
1669 | }, | |
1670 | ERRSIG => { | |
1671 | 0 => 'not specified', | |
1672 | 4 => 'unknown algorithm', | |
1673 | 9 => 'missing public key', | |
1674 | }, | |
1675 | ); | |
1676 | ||
1677 | sub ReasonCodeToText { | |
1678 | my $keyword = shift; | |
1679 | my $code = shift; | |
1680 | return $REASON_CODE_TO_TEXT{ $keyword }{ $code } | |
1681 | if exists $REASON_CODE_TO_TEXT{ $keyword }{ $code }; | |
1682 | return 'unknown'; | |
1683 | } | |
1684 | ||
1685 | my %simple_keyword = ( | |
1686 | NO_RECP => { | |
1687 | Operation => 'RecipientsCheck', | |
1688 | Status => 'ERROR', | |
1689 | Message => 'No recipients', | |
1690 | }, | |
1691 | UNEXPECTED => { | |
1692 | Operation => 'Data', | |
1693 | Status => 'ERROR', | |
1694 | Message => 'Unexpected data has been encountered', | |
1695 | }, | |
1696 | BADARMOR => { | |
1697 | Operation => 'Data', | |
1698 | Status => 'ERROR', | |
1699 | Message => 'The ASCII armor is corrupted', | |
1700 | }, | |
1701 | ); | |
1702 | ||
1703 | # keywords we parse | |
1704 | my %parse_keyword = map { $_ => 1 } qw( | |
1705 | USERID_HINT | |
1706 | SIG_CREATED GOODSIG BADSIG ERRSIG | |
1707 | END_ENCRYPTION | |
1708 | DECRYPTION_FAILED DECRYPTION_OKAY | |
1709 | BAD_PASSPHRASE GOOD_PASSPHRASE | |
1710 | NO_SECKEY NO_PUBKEY | |
1711 | NO_RECP INV_RECP NODATA UNEXPECTED | |
1712 | ); | |
1713 | ||
1714 | # keywords we ignore without any messages as we parse them using other | |
1715 | # keywords as starting point or just ignore as they are useless for us | |
1716 | my %ignore_keyword = map { $_ => 1 } qw( | |
1717 | NEED_PASSPHRASE MISSING_PASSPHRASE BEGIN_SIGNING PLAINTEXT PLAINTEXT_LENGTH | |
1718 | BEGIN_ENCRYPTION SIG_ID VALIDSIG | |
1719 | ENC_TO BEGIN_DECRYPTION END_DECRYPTION GOODMDC | |
1720 | TRUST_UNDEFINED TRUST_NEVER TRUST_MARGINAL TRUST_FULLY TRUST_ULTIMATE | |
dab09ea8 | 1721 | DECRYPTION_INFO |
84fb5b46 MKG |
1722 | ); |
1723 | ||
1724 | sub ParseStatus { | |
1725 | my $status = shift; | |
1726 | return () unless $status; | |
1727 | ||
1728 | my @status; | |
1729 | while ( $status =~ /\[GNUPG:\]\s*(.*?)(?=\[GNUPG:\]|\z)/igms ) { | |
1730 | push @status, $1; $status[-1] =~ s/\s+/ /g; $status[-1] =~ s/\s+$//; | |
1731 | } | |
1732 | $status = join "\n", @status; | |
1733 | study $status; | |
1734 | ||
1735 | my @res; | |
1736 | my (%user_hint, $latest_user_main_key); | |
1737 | for ( my $i = 0; $i < @status; $i++ ) { | |
1738 | my $line = $status[$i]; | |
1739 | my ($keyword, $args) = ($line =~ /^(\S+)\s*(.*)$/s); | |
1740 | if ( $simple_keyword{ $keyword } ) { | |
1741 | push @res, $simple_keyword{ $keyword }; | |
1742 | $res[-1]->{'Keyword'} = $keyword; | |
1743 | next; | |
1744 | } | |
1745 | unless ( $parse_keyword{ $keyword } ) { | |
1746 | $RT::Logger->warning("Skipped $keyword") unless $ignore_keyword{ $keyword }; | |
1747 | next; | |
1748 | } | |
1749 | ||
1750 | if ( $keyword eq 'USERID_HINT' ) { | |
1751 | my %tmp = _ParseUserHint($status, $line); | |
1752 | $latest_user_main_key = $tmp{'MainKey'}; | |
1753 | if ( $user_hint{ $tmp{'MainKey'} } ) { | |
1754 | while ( my ($k, $v) = each %tmp ) { | |
1755 | $user_hint{ $tmp{'MainKey'} }->{$k} = $v; | |
1756 | } | |
1757 | } else { | |
1758 | $user_hint{ $tmp{'MainKey'} } = \%tmp; | |
1759 | } | |
1760 | next; | |
1761 | } | |
1762 | elsif ( $keyword eq 'BAD_PASSPHRASE' || $keyword eq 'GOOD_PASSPHRASE' ) { | |
1763 | my $key_id = $args; | |
1764 | my %res = ( | |
1765 | Operation => 'PassphraseCheck', | |
1766 | Status => $keyword eq 'BAD_PASSPHRASE'? 'BAD' : 'DONE', | |
1767 | Key => $key_id, | |
1768 | ); | |
1769 | $res{'Status'} = 'MISSING' if $status[ $i - 1 ] =~ /^MISSING_PASSPHRASE/; | |
1770 | foreach my $line ( reverse @status[ 0 .. $i-1 ] ) { | |
1771 | next unless $line =~ /^NEED_PASSPHRASE\s+(\S+)\s+(\S+)\s+(\S+)/; | |
1772 | next if $key_id && $2 ne $key_id; | |
1773 | @res{'MainKey', 'Key', 'KeyType'} = ($1, $2, $3); | |
1774 | last; | |
1775 | } | |
1776 | $res{'Message'} = ucfirst( lc( $res{'Status'} eq 'DONE'? 'GOOD': $res{'Status'} ) ) .' passphrase'; | |
1777 | $res{'User'} = ( $user_hint{ $res{'MainKey'} } ||= {} ) if $res{'MainKey'}; | |
1778 | if ( exists $res{'User'}->{'EmailAddress'} ) { | |
1779 | $res{'Message'} .= ' for '. $res{'User'}->{'EmailAddress'}; | |
1780 | } else { | |
1781 | $res{'Message'} .= " for '0x$key_id'"; | |
1782 | } | |
1783 | push @res, \%res; | |
1784 | } | |
1785 | elsif ( $keyword eq 'END_ENCRYPTION' ) { | |
1786 | my %res = ( | |
1787 | Operation => 'Encrypt', | |
1788 | Status => 'DONE', | |
1789 | Message => 'Data has been encrypted', | |
1790 | ); | |
1791 | foreach my $line ( reverse @status[ 0 .. $i-1 ] ) { | |
1792 | next unless $line =~ /^BEGIN_ENCRYPTION\s+(\S+)\s+(\S+)/; | |
1793 | @res{'MdcMethod', 'SymAlgo'} = ($1, $2); | |
1794 | last; | |
1795 | } | |
1796 | push @res, \%res; | |
1797 | } | |
1798 | elsif ( $keyword eq 'DECRYPTION_FAILED' || $keyword eq 'DECRYPTION_OKAY' ) { | |
1799 | my %res = ( Operation => 'Decrypt' ); | |
1800 | @res{'Status', 'Message'} = | |
1801 | $keyword eq 'DECRYPTION_FAILED' | |
1802 | ? ('ERROR', 'Decryption failed') | |
1803 | : ('DONE', 'Decryption process succeeded'); | |
1804 | ||
1805 | foreach my $line ( reverse @status[ 0 .. $i-1 ] ) { | |
1806 | next unless $line =~ /^ENC_TO\s+(\S+)\s+(\S+)\s+(\S+)/; | |
1807 | my ($key, $alg, $key_length) = ($1, $2, $3); | |
1808 | ||
1809 | my %encrypted_to = ( | |
1810 | Message => "The message is encrypted to '0x$key'", | |
1811 | User => ( $user_hint{ $key } ||= {} ), | |
1812 | Key => $key, | |
1813 | KeyLength => $key_length, | |
1814 | Algorithm => $alg, | |
1815 | ); | |
1816 | ||
1817 | push @{ $res{'EncryptedTo'} ||= [] }, \%encrypted_to; | |
1818 | } | |
1819 | ||
1820 | push @res, \%res; | |
1821 | } | |
1822 | elsif ( $keyword eq 'NO_SECKEY' || $keyword eq 'NO_PUBKEY' ) { | |
1823 | my ($key) = split /\s+/, $args; | |
1824 | my $type = $keyword eq 'NO_SECKEY'? 'secret': 'public'; | |
1825 | my %res = ( | |
1826 | Operation => 'KeyCheck', | |
1827 | Status => 'MISSING', | |
1828 | Message => ucfirst( $type ) ." key '0x$key' is not available", | |
1829 | Key => $key, | |
1830 | KeyType => $type, | |
1831 | ); | |
1832 | $res{'User'} = ( $user_hint{ $key } ||= {} ); | |
1833 | $res{'User'}{ ucfirst( $type ). 'KeyMissing' } = 1; | |
1834 | push @res, \%res; | |
1835 | } | |
1836 | # GOODSIG, BADSIG, VALIDSIG, TRUST_* | |
1837 | elsif ( $keyword eq 'GOODSIG' ) { | |
1838 | my %res = ( | |
1839 | Operation => 'Verify', | |
1840 | Status => 'DONE', | |
1841 | Message => 'The signature is good', | |
1842 | ); | |
1843 | @res{qw(Key UserString)} = split /\s+/, $args, 2; | |
1844 | $res{'Message'} .= ', signed by '. $res{'UserString'}; | |
1845 | ||
1846 | foreach my $line ( @status[ $i .. $#status ] ) { | |
1847 | next unless $line =~ /^TRUST_(\S+)/; | |
1848 | $res{'Trust'} = $1; | |
1849 | last; | |
1850 | } | |
1851 | $res{'Message'} .= ', trust level is '. lc( $res{'Trust'} || 'unknown'); | |
1852 | ||
1853 | foreach my $line ( @status[ $i .. $#status ] ) { | |
1854 | next unless $line =~ /^VALIDSIG\s+(.*)/; | |
1855 | @res{ qw( | |
1856 | Fingerprint | |
1857 | CreationDate | |
1858 | Timestamp | |
1859 | ExpireTimestamp | |
1860 | Version | |
1861 | Reserved | |
1862 | PubkeyAlgo | |
1863 | HashAlgo | |
1864 | Class | |
1865 | PKFingerprint | |
1866 | Other | |
1867 | ) } = split /\s+/, $1, 10; | |
1868 | last; | |
1869 | } | |
1870 | push @res, \%res; | |
1871 | } | |
1872 | elsif ( $keyword eq 'BADSIG' ) { | |
1873 | my %res = ( | |
1874 | Operation => 'Verify', | |
1875 | Status => 'BAD', | |
1876 | Message => 'The signature has not been verified okay', | |
1877 | ); | |
1878 | @res{qw(Key UserString)} = split /\s+/, $args, 2; | |
1879 | push @res, \%res; | |
1880 | } | |
1881 | elsif ( $keyword eq 'ERRSIG' ) { | |
1882 | my %res = ( | |
1883 | Operation => 'Verify', | |
1884 | Status => 'ERROR', | |
1885 | Message => 'Not possible to check the signature', | |
1886 | ); | |
1887 | @res{qw(Key PubkeyAlgo HashAlgo Class Timestamp ReasonCode Other)} | |
1888 | = split /\s+/, $args, 7; | |
1889 | ||
1890 | $res{'Reason'} = ReasonCodeToText( $keyword, $res{'ReasonCode'} ); | |
1891 | $res{'Message'} .= ", the reason is ". $res{'Reason'}; | |
1892 | ||
1893 | push @res, \%res; | |
1894 | } | |
1895 | elsif ( $keyword eq 'SIG_CREATED' ) { | |
1896 | # SIG_CREATED <type> <pubkey algo> <hash algo> <class> <timestamp> <key fpr> | |
1897 | my @props = split /\s+/, $args; | |
1898 | push @res, { | |
1899 | Operation => 'Sign', | |
1900 | Status => 'DONE', | |
1901 | Message => "Signed message", | |
1902 | Type => $props[0], | |
1903 | PubKeyAlgo => $props[1], | |
1904 | HashKeyAlgo => $props[2], | |
1905 | Class => $props[3], | |
1906 | Timestamp => $props[4], | |
1907 | KeyFingerprint => $props[5], | |
1908 | User => $user_hint{ $latest_user_main_key }, | |
1909 | }; | |
1910 | $res[-1]->{Message} .= ' by '. $user_hint{ $latest_user_main_key }->{'EmailAddress'} | |
1911 | if $user_hint{ $latest_user_main_key }; | |
1912 | } | |
1913 | elsif ( $keyword eq 'INV_RECP' ) { | |
1914 | my ($rcode, $recipient) = split /\s+/, $args, 2; | |
1915 | my $reason = ReasonCodeToText( $keyword, $rcode ); | |
1916 | push @res, { | |
1917 | Operation => 'RecipientsCheck', | |
1918 | Status => 'ERROR', | |
1919 | Message => "Recipient '$recipient' is unusable, the reason is '$reason'", | |
1920 | Recipient => $recipient, | |
1921 | ReasonCode => $rcode, | |
1922 | Reason => $reason, | |
1923 | }; | |
1924 | } | |
1925 | elsif ( $keyword eq 'NODATA' ) { | |
1926 | my $rcode = (split /\s+/, $args)[0]; | |
1927 | my $reason = ReasonCodeToText( $keyword, $rcode ); | |
1928 | push @res, { | |
1929 | Operation => 'Data', | |
1930 | Status => 'ERROR', | |
1931 | Message => "No data has been found. The reason is '$reason'", | |
1932 | ReasonCode => $rcode, | |
1933 | Reason => $reason, | |
1934 | }; | |
1935 | } | |
1936 | else { | |
1937 | $RT::Logger->warning("Keyword $keyword is unknown"); | |
1938 | next; | |
1939 | } | |
1940 | $res[-1]{'Keyword'} = $keyword if @res && !$res[-1]{'Keyword'}; | |
1941 | } | |
1942 | return @res; | |
1943 | } | |
1944 | ||
1945 | sub _ParseUserHint { | |
1946 | my ($status, $hint) = (@_); | |
1947 | my ($main_key_id, $user_str) = ($hint =~ /^USERID_HINT\s+(\S+)\s+(.*)$/); | |
1948 | return () unless $main_key_id; | |
1949 | return ( | |
1950 | MainKey => $main_key_id, | |
1951 | String => $user_str, | |
1952 | EmailAddress => (map $_->address, Email::Address->parse( $user_str ))[0], | |
1953 | ); | |
1954 | } | |
1955 | ||
1956 | sub _PrepareGnuPGOptions { | |
1957 | my %opt = @_; | |
1958 | my %res = map { lc $_ => $opt{ $_ } } grep $supported_opt{ lc $_ }, keys %opt; | |
1959 | $res{'extra_args'} ||= []; | |
1960 | foreach my $o ( grep !$supported_opt{ lc $_ }, keys %opt ) { | |
1961 | push @{ $res{'extra_args'} }, '--'. lc $o; | |
1962 | push @{ $res{'extra_args'} }, $opt{ $o } | |
1963 | if defined $opt{ $o }; | |
1964 | } | |
1965 | return %res; | |
1966 | } | |
1967 | ||
1968 | { my %key; | |
1969 | # no args -> clear | |
1970 | # one arg -> return preferred key | |
1971 | # many -> set | |
1972 | sub UseKeyForEncryption { | |
1973 | unless ( @_ ) { | |
1974 | %key = (); | |
1975 | } elsif ( @_ > 1 ) { | |
1976 | %key = (%key, @_); | |
1977 | $key{ lc($_) } = delete $key{ $_ } foreach grep lc ne $_, keys %key; | |
1978 | } else { | |
1979 | return $key{ $_[0] }; | |
1980 | } | |
1981 | return (); | |
1982 | } } | |
1983 | ||
1984 | =head2 UseKeyForSigning | |
1985 | ||
1986 | Returns or sets identifier of the key that should be used for signing. | |
1987 | ||
1988 | Returns the current value when called without arguments. | |
1989 | ||
1990 | Sets new value when called with one argument and unsets if it's undef. | |
1991 | ||
1992 | =cut | |
1993 | ||
1994 | { my $key; | |
1995 | sub UseKeyForSigning { | |
1996 | if ( @_ ) { | |
1997 | $key = $_[0]; | |
1998 | } | |
1999 | return $key; | |
2000 | } } | |
2001 | ||
2002 | =head2 GetKeysForEncryption | |
2003 | ||
2004 | Takes identifier and returns keys suitable for encryption. | |
2005 | ||
2006 | B<Note> that keys for which trust level is not set are | |
2007 | also listed. | |
2008 | ||
2009 | =cut | |
2010 | ||
2011 | sub GetKeysForEncryption { | |
2012 | my $key_id = shift; | |
2013 | my %res = GetKeysInfo( $key_id, 'public', @_ ); | |
2014 | return %res if $res{'exit_code'}; | |
2015 | return %res unless $res{'info'}; | |
2016 | ||
2017 | foreach my $key ( splice @{ $res{'info'} } ) { | |
2018 | # skip disabled keys | |
2019 | next if $key->{'Capabilities'} =~ /D/; | |
2020 | # skip keys not suitable for encryption | |
2021 | next unless $key->{'Capabilities'} =~ /e/i; | |
2022 | # skip disabled, expired, revoke and keys with no trust, | |
2023 | # but leave keys with unknown trust level | |
2024 | next if $key->{'TrustLevel'} < 0; | |
2025 | ||
2026 | push @{ $res{'info'} }, $key; | |
2027 | } | |
2028 | delete $res{'info'} unless @{ $res{'info'} }; | |
2029 | return %res; | |
2030 | } | |
2031 | ||
2032 | sub GetKeysForSigning { | |
2033 | my $key_id = shift; | |
2034 | return GetKeysInfo( $key_id, 'private', @_ ); | |
2035 | } | |
2036 | ||
2037 | sub CheckRecipients { | |
2038 | my @recipients = (@_); | |
2039 | ||
2040 | my ($status, @issues) = (1, ()); | |
2041 | ||
2042 | my %seen; | |
2043 | foreach my $address ( grep !$seen{ lc $_ }++, map $_->address, @recipients ) { | |
2044 | my %res = GetKeysForEncryption( $address ); | |
2045 | if ( $res{'info'} && @{ $res{'info'} } == 1 && $res{'info'}[0]{'TrustLevel'} > 0 ) { | |
2046 | # good, one suitable and trusted key | |
2047 | next; | |
2048 | } | |
2049 | my $user = RT::User->new( RT->SystemUser ); | |
2050 | $user->LoadByEmail( $address ); | |
2051 | # it's possible that we have no User record with the email | |
2052 | $user = undef unless $user->id; | |
2053 | ||
2054 | if ( my $fpr = UseKeyForEncryption( $address ) ) { | |
2055 | if ( $res{'info'} && @{ $res{'info'} } ) { | |
2056 | next if | |
2057 | grep lc $_->{'Fingerprint'} eq lc $fpr, | |
2058 | grep $_->{'TrustLevel'} > 0, | |
2059 | @{ $res{'info'} }; | |
2060 | } | |
2061 | ||
2062 | $status = 0; | |
2063 | my %issue = ( | |
2064 | EmailAddress => $address, | |
2065 | $user? (User => $user) : (), | |
2066 | Keys => undef, | |
2067 | ); | |
2068 | $issue{'Message'} = "Selected key either is not trusted or doesn't exist anymore."; #loc | |
2069 | push @issues, \%issue; | |
2070 | next; | |
2071 | } | |
2072 | ||
2073 | my $prefered_key; | |
2074 | $prefered_key = $user->PreferredKey if $user; | |
2075 | #XXX: prefered key is not yet implemented... | |
2076 | ||
2077 | # classify errors | |
2078 | $status = 0; | |
2079 | my %issue = ( | |
2080 | EmailAddress => $address, | |
2081 | $user? (User => $user) : (), | |
2082 | Keys => undef, | |
2083 | ); | |
2084 | ||
2085 | unless ( $res{'info'} && @{ $res{'info'} } ) { | |
2086 | # no key | |
2087 | $issue{'Message'} = "There is no key suitable for encryption."; #loc | |
2088 | } | |
2089 | elsif ( @{ $res{'info'} } == 1 && !$res{'info'}[0]{'TrustLevel'} ) { | |
2090 | # trust is not set | |
2091 | $issue{'Message'} = "There is one suitable key, but trust level is not set."; #loc | |
2092 | } | |
2093 | else { | |
2094 | # multiple keys | |
2095 | $issue{'Message'} = "There are several keys suitable for encryption."; #loc | |
2096 | } | |
2097 | push @issues, \%issue; | |
2098 | } | |
2099 | return ($status, @issues); | |
2100 | } | |
2101 | ||
2102 | sub GetPublicKeyInfo { | |
2103 | return GetKeyInfo( shift, 'public', @_ ); | |
2104 | } | |
2105 | ||
2106 | sub GetPrivateKeyInfo { | |
2107 | return GetKeyInfo( shift, 'private', @_ ); | |
2108 | } | |
2109 | ||
2110 | sub GetKeyInfo { | |
2111 | my %res = GetKeysInfo(@_); | |
2112 | $res{'info'} = $res{'info'}->[0]; | |
2113 | return %res; | |
2114 | } | |
2115 | ||
2116 | sub GetKeysInfo { | |
2117 | my $email = shift; | |
2118 | my $type = shift || 'public'; | |
2119 | my $force = shift; | |
2120 | ||
2121 | unless ( $email ) { | |
2122 | return (exit_code => 0) unless $force; | |
2123 | } | |
2124 | ||
2125 | my $gnupg = GnuPG::Interface->new(); | |
2126 | my %opt = RT->Config->Get('GnuPGOptions'); | |
2127 | $opt{'digest-algo'} ||= 'SHA1'; | |
2128 | $opt{'with-colons'} = undef; # parseable format | |
2129 | $opt{'fingerprint'} = undef; # show fingerprint | |
2130 | $opt{'fixed-list-mode'} = undef; # don't merge uid with keys | |
2131 | $gnupg->options->hash_init( | |
2132 | _PrepareGnuPGOptions( %opt ), | |
2133 | armor => 1, | |
2134 | meta_interactive => 0, | |
2135 | ); | |
2136 | ||
2137 | my %res; | |
2138 | ||
2139 | my ($handles, $handle_list) = _make_gpg_handles(); | |
2140 | my %handle = %$handle_list; | |
2141 | ||
2142 | eval { | |
2143 | local $SIG{'CHLD'} = 'DEFAULT'; | |
2144 | my $method = $type eq 'private'? 'list_secret_keys': 'list_public_keys'; | |
dab09ea8 MKG |
2145 | my $pid = safe_run_child { $gnupg->$method( handles => $handles, $email |
2146 | ? (command_args => [ "--", $email]) | |
2147 | : () ) }; | |
84fb5b46 MKG |
2148 | close $handle{'stdin'}; |
2149 | waitpid $pid, 0; | |
2150 | }; | |
2151 | ||
2152 | my @info = readline $handle{'stdout'}; | |
2153 | close $handle{'stdout'}; | |
2154 | ||
2155 | $res{'exit_code'} = $?; | |
2156 | foreach ( qw(stderr logger status) ) { | |
2157 | $res{$_} = do { local $/; readline $handle{$_} }; | |
2158 | delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s; | |
2159 | close $handle{$_}; | |
2160 | } | |
2161 | $RT::Logger->debug( $res{'status'} ) if $res{'status'}; | |
2162 | $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'}; | |
2163 | $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?; | |
2164 | if ( $@ || $? ) { | |
2165 | $res{'message'} = $@? $@: "gpg exitted with error code ". ($? >> 8); | |
2166 | return %res; | |
2167 | } | |
2168 | ||
2169 | @info = ParseKeysInfo( @info ); | |
2170 | $res{'info'} = \@info; | |
2171 | return %res; | |
2172 | } | |
2173 | ||
2174 | sub ParseKeysInfo { | |
2175 | my @lines = @_; | |
2176 | ||
2177 | my %gpg_opt = RT->Config->Get('GnuPGOptions'); | |
2178 | ||
2179 | my @res = (); | |
2180 | foreach my $line( @lines ) { | |
2181 | chomp $line; | |
2182 | my $tag; | |
2183 | ($tag, $line) = split /:/, $line, 2; | |
2184 | if ( $tag eq 'pub' ) { | |
2185 | my %info; | |
2186 | @info{ qw( | |
2187 | TrustChar KeyLength Algorithm Key | |
2188 | Created Expire Empty OwnerTrustChar | |
2189 | Empty Empty Capabilities Other | |
2190 | ) } = split /:/, $line, 12; | |
2191 | ||
2192 | # workaround gnupg's wierd behaviour, --list-keys command report calculated trust levels | |
2193 | # for any model except 'always', so you can change models and see changes, but not for 'always' | |
2194 | # we try to handle it in a simple way - we set ultimate trust for any key with trust | |
2195 | # level >= 0 if trust model is 'always' | |
2196 | my $always_trust; | |
2197 | $always_trust = 1 if exists $gpg_opt{'always-trust'}; | |
2198 | $always_trust = 1 if exists $gpg_opt{'trust-model'} && $gpg_opt{'trust-model'} eq 'always'; | |
2199 | @info{qw(Trust TrustTerse TrustLevel)} = | |
2200 | _ConvertTrustChar( $info{'TrustChar'} ); | |
2201 | if ( $always_trust && $info{'TrustLevel'} >= 0 ) { | |
2202 | @info{qw(Trust TrustTerse TrustLevel)} = | |
2203 | _ConvertTrustChar( 'u' ); | |
2204 | } | |
2205 | ||
2206 | @info{qw(OwnerTrust OwnerTrustTerse OwnerTrustLevel)} = | |
2207 | _ConvertTrustChar( $info{'OwnerTrustChar'} ); | |
2208 | $info{ $_ } = _ParseDate( $info{ $_ } ) | |
2209 | foreach qw(Created Expire); | |
2210 | push @res, \%info; | |
2211 | } | |
2212 | elsif ( $tag eq 'sec' ) { | |
2213 | my %info; | |
2214 | @info{ qw( | |
2215 | Empty KeyLength Algorithm Key | |
2216 | Created Expire Empty OwnerTrustChar | |
2217 | Empty Empty Capabilities Other | |
2218 | ) } = split /:/, $line, 12; | |
2219 | @info{qw(OwnerTrust OwnerTrustTerse OwnerTrustLevel)} = | |
2220 | _ConvertTrustChar( $info{'OwnerTrustChar'} ); | |
2221 | $info{ $_ } = _ParseDate( $info{ $_ } ) | |
2222 | foreach qw(Created Expire); | |
2223 | push @res, \%info; | |
2224 | } | |
2225 | elsif ( $tag eq 'uid' ) { | |
2226 | my %info; | |
2227 | @info{ qw(Trust Created Expire String) } | |
2228 | = (split /:/, $line)[0,4,5,8]; | |
2229 | $info{ $_ } = _ParseDate( $info{ $_ } ) | |
2230 | foreach qw(Created Expire); | |
2231 | push @{ $res[-1]{'User'} ||= [] }, \%info; | |
2232 | } | |
2233 | elsif ( $tag eq 'fpr' ) { | |
2234 | $res[-1]{'Fingerprint'} = (split /:/, $line, 10)[8]; | |
2235 | } | |
2236 | } | |
2237 | return @res; | |
2238 | } | |
2239 | ||
2240 | { | |
2241 | my %verbose = ( | |
2242 | # deprecated | |
2243 | d => [ | |
2244 | "The key has been disabled", #loc | |
2245 | "key disabled", #loc | |
2246 | "-2" | |
2247 | ], | |
2248 | ||
2249 | r => [ | |
2250 | "The key has been revoked", #loc | |
2251 | "key revoked", #loc | |
2252 | -3, | |
2253 | ], | |
2254 | ||
2255 | e => [ "The key has expired", #loc | |
2256 | "key expired", #loc | |
2257 | '-4', | |
2258 | ], | |
2259 | ||
2260 | n => [ "Don't trust this key at all", #loc | |
2261 | 'none', #loc | |
2262 | -1, | |
2263 | ], | |
2264 | ||
2265 | #gpupg docs says that '-' and 'q' may safely be treated as the same value | |
2266 | '-' => [ | |
2267 | 'Unknown (no trust value assigned)', #loc | |
2268 | 'not set', | |
2269 | 0, | |
2270 | ], | |
2271 | q => [ | |
2272 | 'Unknown (no trust value assigned)', #loc | |
2273 | 'not set', | |
2274 | 0, | |
2275 | ], | |
2276 | o => [ | |
2277 | 'Unknown (this value is new to the system)', #loc | |
2278 | 'unknown', | |
2279 | 0, | |
2280 | ], | |
2281 | ||
2282 | m => [ | |
2283 | "There is marginal trust in this key", #loc | |
2284 | 'marginal', #loc | |
2285 | 1, | |
2286 | ], | |
2287 | f => [ | |
2288 | "The key is fully trusted", #loc | |
2289 | 'full', #loc | |
2290 | 2, | |
2291 | ], | |
2292 | u => [ | |
2293 | "The key is ultimately trusted", #loc | |
2294 | 'ultimate', #loc | |
2295 | 3, | |
2296 | ], | |
2297 | ); | |
2298 | ||
2299 | sub _ConvertTrustChar { | |
2300 | my $value = shift; | |
2301 | return @{ $verbose{'-'} } unless $value; | |
2302 | $value = substr $value, 0, 1; | |
2303 | return @{ $verbose{ $value } || $verbose{'o'} }; | |
2304 | } | |
2305 | } | |
2306 | ||
2307 | sub _ParseDate { | |
2308 | my $value = shift; | |
2309 | # never | |
2310 | return $value unless $value; | |
2311 | ||
2312 | require RT::Date; | |
2313 | my $obj = RT::Date->new( RT->SystemUser ); | |
2314 | # unix time | |
2315 | if ( $value =~ /^\d+$/ ) { | |
2316 | $obj->Set( Value => $value ); | |
2317 | } else { | |
2318 | $obj->Set( Format => 'unknown', Value => $value, Timezone => 'utc' ); | |
2319 | } | |
2320 | return $obj; | |
2321 | } | |
2322 | ||
2323 | sub DeleteKey { | |
2324 | my $key = shift; | |
2325 | ||
2326 | my $gnupg = GnuPG::Interface->new(); | |
2327 | my %opt = RT->Config->Get('GnuPGOptions'); | |
2328 | $gnupg->options->hash_init( | |
2329 | _PrepareGnuPGOptions( %opt ), | |
2330 | meta_interactive => 0, | |
2331 | ); | |
2332 | ||
2333 | my ($handles, $handle_list) = _make_gpg_handles(); | |
2334 | my %handle = %$handle_list; | |
2335 | ||
2336 | eval { | |
2337 | local $SIG{'CHLD'} = 'DEFAULT'; | |
2338 | my $pid = safe_run_child { $gnupg->wrap_call( | |
2339 | handles => $handles, | |
2340 | commands => ['--delete-secret-and-public-key'], | |
dab09ea8 | 2341 | command_args => ["--", $key], |
84fb5b46 MKG |
2342 | ) }; |
2343 | close $handle{'stdin'}; | |
2344 | while ( my $str = readline $handle{'status'} ) { | |
2345 | if ( $str =~ /^\[GNUPG:\]\s*GET_BOOL delete_key\..*/ ) { | |
2346 | print { $handle{'command'} } "y\n"; | |
2347 | } | |
2348 | } | |
2349 | waitpid $pid, 0; | |
2350 | }; | |
2351 | my $err = $@; | |
2352 | close $handle{'stdout'}; | |
2353 | ||
2354 | my %res; | |
2355 | $res{'exit_code'} = $?; | |
2356 | foreach ( qw(stderr logger status) ) { | |
2357 | $res{$_} = do { local $/; readline $handle{$_} }; | |
2358 | delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s; | |
2359 | close $handle{$_}; | |
2360 | } | |
2361 | $RT::Logger->debug( $res{'status'} ) if $res{'status'}; | |
2362 | $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'}; | |
2363 | $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?; | |
2364 | if ( $err || $res{'exit_code'} ) { | |
2365 | $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8); | |
2366 | } | |
2367 | return %res; | |
2368 | } | |
2369 | ||
2370 | sub ImportKey { | |
2371 | my $key = shift; | |
2372 | ||
2373 | my $gnupg = GnuPG::Interface->new(); | |
2374 | my %opt = RT->Config->Get('GnuPGOptions'); | |
2375 | $gnupg->options->hash_init( | |
2376 | _PrepareGnuPGOptions( %opt ), | |
2377 | meta_interactive => 0, | |
2378 | ); | |
2379 | ||
2380 | my ($handles, $handle_list) = _make_gpg_handles(); | |
2381 | my %handle = %$handle_list; | |
2382 | ||
2383 | eval { | |
2384 | local $SIG{'CHLD'} = 'DEFAULT'; | |
2385 | my $pid = safe_run_child { $gnupg->wrap_call( | |
2386 | handles => $handles, | |
2387 | commands => ['--import'], | |
2388 | ) }; | |
2389 | print { $handle{'stdin'} } $key; | |
2390 | close $handle{'stdin'}; | |
2391 | waitpid $pid, 0; | |
2392 | }; | |
2393 | my $err = $@; | |
2394 | close $handle{'stdout'}; | |
2395 | ||
2396 | my %res; | |
2397 | $res{'exit_code'} = $?; | |
2398 | foreach ( qw(stderr logger status) ) { | |
2399 | $res{$_} = do { local $/; readline $handle{$_} }; | |
2400 | delete $res{$_} unless $res{$_} && $res{$_} =~ /\S/s; | |
2401 | close $handle{$_}; | |
2402 | } | |
2403 | $RT::Logger->debug( $res{'status'} ) if $res{'status'}; | |
2404 | $RT::Logger->warning( $res{'stderr'} ) if $res{'stderr'}; | |
2405 | $RT::Logger->error( $res{'logger'} ) if $res{'logger'} && $?; | |
2406 | if ( $err || $res{'exit_code'} ) { | |
2407 | $res{'message'} = $err? $err : "gpg exitted with error code ". ($res{'exit_code'} >> 8); | |
2408 | } | |
2409 | return %res; | |
2410 | } | |
2411 | ||
2412 | =head2 KEY | |
2413 | ||
2414 | Signs a small message with the key, to make sure the key exists and | |
2415 | we have a useable passphrase. The first argument MUST be a key identifier | |
2416 | of the signer: either email address, key id or finger print. | |
2417 | ||
2418 | Returns a true value if all went well. | |
2419 | ||
2420 | =cut | |
2421 | ||
2422 | sub DrySign { | |
2423 | my $from = shift; | |
2424 | ||
2425 | my $mime = MIME::Entity->build( | |
2426 | Type => "text/plain", | |
2427 | From => 'nobody@localhost', | |
2428 | To => 'nobody@localhost', | |
2429 | Subject => "dry sign", | |
2430 | Data => ['t'], | |
2431 | ); | |
2432 | ||
2433 | my %res = SignEncrypt( | |
2434 | Sign => 1, | |
2435 | Encrypt => 0, | |
2436 | Entity => $mime, | |
2437 | Signer => $from, | |
2438 | ); | |
2439 | ||
2440 | return $res{exit_code} == 0; | |
2441 | } | |
2442 | ||
2443 | 1; | |
2444 | ||
2445 | =head2 Probe | |
2446 | ||
2447 | This routine returns true if RT's GnuPG support is configured and working | |
2448 | properly (and false otherwise). | |
2449 | ||
2450 | ||
2451 | =cut | |
2452 | ||
2453 | ||
2454 | sub Probe { | |
2455 | my $gnupg = GnuPG::Interface->new(); | |
2456 | my %opt = RT->Config->Get('GnuPGOptions'); | |
2457 | $gnupg->options->hash_init( | |
2458 | _PrepareGnuPGOptions( %opt ), | |
2459 | armor => 1, | |
2460 | meta_interactive => 0, | |
2461 | ); | |
2462 | ||
2463 | my ($handles, $handle_list) = _make_gpg_handles(); | |
2464 | my %handle = %$handle_list; | |
2465 | ||
2466 | local $@; | |
2467 | eval { | |
2468 | local $SIG{'CHLD'} = 'DEFAULT'; | |
2469 | my $pid = safe_run_child { $gnupg->wrap_call( commands => ['--version' ], handles => $handles ) }; | |
2470 | close $handle{'stdin'}; | |
2471 | waitpid $pid, 0; | |
2472 | }; | |
2473 | if ( $@ ) { | |
2474 | $RT::Logger->debug( | |
2475 | "Probe for GPG failed." | |
2476 | ." Couldn't run `gpg --version`: ". $@ | |
2477 | ); | |
2478 | return 0; | |
2479 | } | |
2480 | ||
2481 | # on some systems gpg exits with code 2, but still 100% functional, | |
2482 | # it's general error system error or incorrect command, command is correct, | |
2483 | # but there is no way to get actuall error | |
2484 | if ( $? && ($? >> 8) != 2 ) { | |
2485 | my $msg = "Probe for GPG failed." | |
2486 | ." Process exitted with code ". ($? >> 8) | |
2487 | . ($? & 127 ? (" as recieved signal ". ($? & 127)) : '') | |
2488 | . "."; | |
2489 | foreach ( qw(stderr logger status) ) { | |
2490 | my $tmp = do { local $/; readline $handle{$_} }; | |
2491 | next unless $tmp && $tmp =~ /\S/s; | |
2492 | close $handle{$_}; | |
2493 | $msg .= "\n$_:\n$tmp\n"; | |
2494 | } | |
2495 | $RT::Logger->debug( $msg ); | |
2496 | return 0; | |
2497 | } | |
2498 | return 1; | |
2499 | } | |
2500 | ||
2501 | ||
2502 | sub _make_gpg_handles { | |
2503 | my %handle_map = (@_); | |
2504 | $handle_map{$_} = IO::Handle->new | |
2505 | foreach grep !defined $handle_map{$_}, | |
2506 | qw(stdin stdout stderr logger status command); | |
2507 | ||
2508 | my $handles = GnuPG::Handles->new(%handle_map); | |
2509 | return ($handles, \%handle_map); | |
2510 | } | |
2511 | ||
2512 | RT::Base->_ImportOverlays(); | |
2513 | ||
2514 | # helper package to avoid using temp file | |
2515 | package IO::Handle::CRLF; | |
2516 | ||
2517 | use base qw(IO::Handle); | |
2518 | ||
2519 | sub print { | |
2520 | my ($self, @args) = (@_); | |
2521 | s/\r*\n/\x0D\x0A/g foreach @args; | |
2522 | return $self->SUPER::print( @args ); | |
2523 | } | |
2524 | ||
2525 | 1; |