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