1# --
2# Copyright (C) 2001-2020 OTRS AG, https://otrs.com/
3# --
4# This software comes with ABSOLUTELY NO WARRANTY. For details, see
5# the enclosed file COPYING for license information (GPL). If you
6# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
7# --
8
9package Kernel::Output::HTML::ArticleCheck::PGP;
10
11use strict;
12use warnings;
13
14use MIME::Parser;
15use Kernel::System::EmailParser;
16use Kernel::System::VariableCheck qw(:all);
17use Kernel::Language qw(Translatable);
18
19our @ObjectDependencies = (
20    'Kernel::Config',
21    'Kernel::System::Crypt::PGP',
22    'Kernel::System::Log',
23    'Kernel::System::Ticket::Article',
24);
25
26sub new {
27    my ( $Type, %Param ) = @_;
28
29    # allocate new hash for object
30    my $Self = {};
31    bless( $Self, $Type );
32
33    # get needed params
34    for my $Needed (qw(UserID ArticleID)) {
35        if ( $Param{$Needed} ) {
36            $Self->{$Needed} = $Param{$Needed};
37        }
38        else {
39            $Kernel::OM->Get('Kernel::System::Log')->Log(
40                Priority => 'error',
41                Message  => "Need $Needed!"
42            );
43        }
44    }
45
46    return $Self;
47}
48
49sub Check {
50    my ( $Self, %Param ) = @_;
51    my %SignCheck;
52    my @Return;
53
54    # get config object
55    my $ConfigObject = $Param{ConfigObject} || $Kernel::OM->Get('Kernel::Config');
56
57    # check if pgp is enabled
58    return if !$ConfigObject->Get('PGP');
59
60    my $ArticleObject = $Param{ArticleObject} || $Kernel::OM->Get('Kernel::System::Ticket::Article');
61
62    my $ArticleBackendObject = $ArticleObject->BackendForArticle(
63        TicketID  => $Param{Article}->{TicketID},
64        ArticleID => $Param{Article}->{ArticleID},
65    );
66
67    # check if article is an email
68    return if $ArticleBackendObject->ChannelNameGet() ne 'Email';
69
70    # get needed objects
71    my $PGPObject = $Kernel::OM->Get('Kernel::System::Crypt::PGP');
72
73    # Get plain article/email from filesystem storage.
74    my $Message = $ArticleBackendObject->ArticlePlain(
75        ArticleID => $Self->{ArticleID},
76        UserID    => $Self->{UserID},
77    );
78    return if !$Message;
79
80    # Remember original body.
81    my $OrigArticleBody = $Param{Article}->{Body};
82
83    my $ParserObject = Kernel::System::EmailParser->new(
84        Email => $Message,
85    );
86
87    # Ensure we are using original body instead or article content.
88    # This is necessary for follow-up checks (otherwise the information about e.g. encryption is lost).
89    $Param{Article}->{Body} = $ParserObject->GetMessageBody();
90
91    # check inline pgp crypt
92    if ( $Param{Article}->{Body} && $Param{Article}->{Body} =~ /\A[\s\n]*^-----BEGIN PGP MESSAGE-----/m ) {
93
94        # check sender (don't decrypt sent emails)
95        if ( $Param{Article}->{SenderType} =~ /(agent|system)/i ) {
96
97            # return info
98            return (
99                {
100                    Key   => Translatable('Crypted'),
101                    Value => Translatable('Sent message encrypted to recipient!'),
102                }
103            );
104        }
105        my %Decrypt = $PGPObject->Decrypt( Message => $Param{Article}->{Body} );
106        if ( $Decrypt{Successful} ) {
107
108            # remember to result
109            $Self->{Result} = \%Decrypt;
110            $Param{Article}->{Body} = $Decrypt{Data};
111
112            # Determine if we have decrypted article and attachments before.
113            if (
114                $OrigArticleBody
115                && $OrigArticleBody =~ /\A[\s\n]*^-----BEGIN PGP MESSAGE-----/m
116                )
117            {
118
119                # Update article body.
120                $ArticleBackendObject->ArticleUpdate(
121                    TicketID  => $Param{Article}->{TicketID},
122                    ArticleID => $Self->{ArticleID},
123                    Key       => 'Body',
124                    Value     => $Decrypt{Data},
125                    UserID    => $Self->{UserID},
126                );
127
128                # Get a list of all article attachments.
129                my %Index = $ArticleBackendObject->ArticleAttachmentIndex(
130                    ArticleID => $Self->{ArticleID},
131                );
132
133                my @Attachments;
134                if ( IsHashRefWithData( \%Index ) ) {
135                    for my $FileID ( sort keys %Index ) {
136
137                        my %Attachment = $ArticleBackendObject->ArticleAttachment(
138                            ArticleID => $Self->{ArticleID},
139                            FileID    => $FileID,
140                        );
141
142                        # Store attachment attributes that might change after decryption.
143                        my $AttachmentContent  = $Attachment{Content};
144                        my $AttachmentFilename = $Attachment{Filename};
145
146                        # Try to decrypt the attachment, non ecrypted attachments will succeed too.
147                        %Decrypt = $PGPObject->Decrypt( Message => $Attachment{Content} );
148
149                        if ( $Decrypt{Successful} ) {
150
151                            # Remember decrypted content.
152                            $AttachmentContent = $Decrypt{Data};
153
154                            # Remove .pgp .gpg or asc extensions (if any).
155                            $AttachmentFilename =~ s{ (\. [^\.]+) \. (?: pgp|gpg|asc) \z}{$1}msx;
156                        }
157
158                        # Remember decrypted attachement, to add it later.
159                        push @Attachments, {
160                            %Attachment,
161                            Content   => $AttachmentContent,
162                            Filename  => $AttachmentFilename,
163                            ArticleID => $Self->{ArticleID},
164                            UserID    => $Self->{UserID},
165                        };
166                    }
167
168                    # Delete potentially crypted attachments.
169                    $ArticleBackendObject->ArticleDeleteAttachment(
170                        ArticleID => $Self->{ArticleID},
171                        UserID    => $Self->{UserID},
172                    );
173
174                    # Write decrypted attachments.
175                    for my $Attachment (@Attachments) {
176                        $ArticleBackendObject->ArticleWriteAttachment( %{$Attachment} );
177                    }
178                }
179            }
180
181            push(
182                @Return,
183                {
184                    Key   => Translatable('Crypted'),
185                    Value => $Decrypt{Message},
186                    %Decrypt,
187                },
188            );
189        }
190        else {
191
192            # return with error
193            return (
194                {
195                    Key   => Translatable('Crypted'),
196                    Value => $Decrypt{Message},
197                    %Decrypt,
198                }
199            );
200        }
201    }
202
203    # check inline pgp signature (but ignore if is in quoted text)
204    if (
205        $Param{Article}->{Body}
206        && $Param{Article}->{Body} =~ m{ ^\s* -----BEGIN [ ] PGP [ ] SIGNED [ ] MESSAGE----- }xms
207        )
208    {
209
210        # get the charset of the original message
211        my $Charset = $ParserObject->GetCharset();
212
213        # verify message PGP signature
214        %SignCheck = $PGPObject->Verify(
215            Message => $Param{Article}->{Body},
216            Charset => $Charset
217        );
218
219        if (%SignCheck) {
220
221            # remember to result
222            $Self->{Result} = \%SignCheck;
223        }
224        else {
225
226            # return with error
227            return (
228                {
229                    Key   => Translatable('Signed'),
230                    Value => Translatable('"PGP SIGNED MESSAGE" header found, but invalid!'),
231                }
232            );
233        }
234    }
235
236    # check mime pgp
237    else {
238
239        # check body
240        # if body =~ application/pgp-encrypted
241        # if crypted, decrypt it
242        # remember that it was crypted!
243
244        my $Parser = MIME::Parser->new();
245        $Parser->decode_headers(0);
246        $Parser->extract_nested_messages(0);
247        $Parser->output_to_core('ALL');
248
249        # prevent modification of body by parser - required for bug #11755
250        $Parser->decode_bodies(0);
251        my $Entity = $Parser->parse_data($Message);
252        $Parser->decode_bodies(1);
253        my $Head = $Entity->head();
254        $Head->unfold();
255        $Head->combine('Content-Type');
256        my $ContentType = $Head->get('Content-Type');
257
258        # check if we need to decrypt it
259        if (
260            $ContentType
261            && $ContentType =~ /multipart\/encrypted/i
262            && $ContentType =~ /application\/pgp/i
263            )
264        {
265
266            # check sender (don't decrypt sent emails)
267            if ( $Param{Article}->{SenderType} && $Param{Article}->{SenderType} =~ /(agent|system)/i ) {
268
269                # return info
270                return (
271                    {
272                        Key        => Translatable('Crypted'),
273                        Value      => Translatable('Sent message encrypted to recipient!'),
274                        Successful => 1,
275                    }
276                );
277            }
278
279            # get crypted part of the mail
280            my $Crypted = $Entity->parts(1)->as_string();
281
282            # decrypt it
283            my %Decrypt = $PGPObject->Decrypt(
284                Message => $Crypted,
285            );
286            if ( $Decrypt{Successful} ) {
287
288                # Remember entity and contend type for following signature check.
289                $Entity = $Parser->parse_data( $Decrypt{Data} );
290                my $Head = $Entity->head();
291                $Head->unfold();
292                $Head->combine('Content-Type');
293                $ContentType = $Head->get('Content-Type');
294
295                # Determine if we have decrypted article and attachments before.
296                my %Index = $ArticleBackendObject->ArticleAttachmentIndex(
297                    ArticleID => $Self->{ArticleID},
298                );
299
300                if ( grep { $Index{$_}->{ContentType} =~ m{ application/pgp-encrypted }xms } sort keys %Index ) {
301
302                    # use a copy of the Entity to get the body, otherwise the original mail content
303                    # could be altered and a signature verify could fail. See Bug#9954
304                    my $EntityCopy = $Entity->dup();
305
306                    my $ParserObject = Kernel::System::EmailParser->new(
307                        Entity => $EntityCopy,
308                    );
309
310                    my $Body = $ParserObject->GetMessageBody();
311
312                    # Update article body.
313                    $ArticleBackendObject->ArticleUpdate(
314                        TicketID  => $Param{Article}->{TicketID},
315                        ArticleID => $Self->{ArticleID},
316                        Key       => 'Body',
317                        Value     => $Body,
318                        UserID    => $Self->{UserID},
319                    );
320
321                    # Delete crypted attachments.
322                    $ArticleBackendObject->ArticleDeleteAttachment(
323                        ArticleID => $Self->{ArticleID},
324                        UserID    => $Self->{UserID},
325                    );
326
327                    # Write decrypted attachments to the storage.
328                    for my $Attachment ( $ParserObject->GetAttachments() ) {
329                        $ArticleBackendObject->ArticleWriteAttachment(
330                            %{$Attachment},
331                            ArticleID => $Self->{ArticleID},
332                            UserID    => $Self->{UserID},
333                        );
334                    }
335
336                }
337
338                push(
339                    @Return,
340                    {
341                        Key   => Translatable('Crypted'),
342                        Value => $Decrypt{Message},
343                        %Decrypt,
344                    },
345                );
346            }
347            else {
348                push(
349                    @Return,
350                    {
351                        Key   => Translatable('Crypted'),
352                        Value => $Decrypt{Message},
353                        %Decrypt,
354                    },
355                );
356            }
357        }
358        if (
359            $ContentType
360            && $ContentType =~ /multipart\/signed/i
361            && $ContentType =~ /application\/pgp/i
362            && $Entity->parts(0)
363            && $Entity->parts(1)
364            )
365        {
366
367            my $SignedText    = $Entity->parts(0)->as_string();
368            my $SignatureText = $Entity->parts(1)->body_as_string();
369
370            # according to RFC3156 all line endings MUST be CR/LF
371            $SignedText =~ s/\x0A/\x0D\x0A/g;
372            $SignedText =~ s/\x0D+/\x0D/g;
373
374            %SignCheck = $PGPObject->Verify(
375                Message => $SignedText,
376                Sign    => $SignatureText,
377            );
378        }
379    }
380    if (%SignCheck) {
381
382        # return result
383        push(
384            @Return,
385            {
386                Key   => Translatable('Signed'),
387                Value => $SignCheck{Message},
388                %SignCheck,
389            },
390        );
391    }
392    return @Return;
393}
394
395sub Filter {
396    my ( $Self, %Param ) = @_;
397
398    # remove signature if one is found
399    if ( $Self->{Result}->{SignatureFound} ) {
400
401        # remove pgp begin signed message
402        $Param{Article}->{Body} =~ s/^-----BEGIN\sPGP\sSIGNED\sMESSAGE-----.+?Hash:\s.+?$//sm;
403
404        # remove pgp inline sign
405        $Param{Article}->{Body}
406            =~ s/^-----BEGIN\sPGP\sSIGNATURE-----.+?-----END\sPGP\sSIGNATURE-----//sm;
407    }
408    return 1;
409}
410
4111;
412