Files
2024-10-14 00:08:40 +02:00

387 lines
12 KiB
Perl

# --
# Copyright (C) 2001-2019 OTRS AG, https://otrs.com/
# --
# This software comes with ABSOLUTELY NO WARRANTY. For details, see
# the enclosed file COPYING for license information (GPL). If you
# did not receive this file, see https://www.gnu.org/licenses/gpl-3.0.txt.
# --
package Kernel::Output::HTML::ArticleCheck::PGP;
use strict;
use warnings;
use MIME::Parser;
use Kernel::System::EmailParser;
use Kernel::System::VariableCheck qw(:all);
use Kernel::Language qw(Translatable);
our @ObjectDependencies = (
'Kernel::Config',
'Kernel::System::Crypt::PGP',
'Kernel::System::Log',
'Kernel::System::Ticket::Article',
);
sub new {
my ( $Type, %Param ) = @_;
# allocate new hash for object
my $Self = {};
bless( $Self, $Type );
# get needed params
for my $Needed (qw(UserID ArticleID)) {
if ( $Param{$Needed} ) {
$Self->{$Needed} = $Param{$Needed};
}
else {
$Kernel::OM->Get('Kernel::System::Log')->Log(
Priority => 'error',
Message => "Need $Needed!"
);
}
}
return $Self;
}
sub Check {
my ( $Self, %Param ) = @_;
my %SignCheck;
my @Return;
# get config object
my $ConfigObject = $Param{ConfigObject} || $Kernel::OM->Get('Kernel::Config');
# check if pgp is enabled
return if !$ConfigObject->Get('PGP');
my $ArticleObject = $Param{ArticleObject} || $Kernel::OM->Get('Kernel::System::Ticket::Article');
my $ArticleBackendObject = $ArticleObject->BackendForArticle(
TicketID => $Param{Article}->{TicketID},
ArticleID => $Param{Article}->{ArticleID},
);
# check if article is an email
return if $ArticleBackendObject->ChannelNameGet() ne 'Email';
# get needed objects
my $PGPObject = $Kernel::OM->Get('Kernel::System::Crypt::PGP');
# check inline pgp crypt
if ( $Param{Article}->{Body} && $Param{Article}->{Body} =~ /\A[\s\n]*^-----BEGIN PGP MESSAGE-----/m ) {
# check sender (don't decrypt sent emails)
if ( $Param{Article}->{SenderType} =~ /(agent|system)/i ) {
# return info
return (
{
Key => Translatable('Crypted'),
Value => Translatable('Sent message encrypted to recipient!'),
}
);
}
my %Decrypt = $PGPObject->Decrypt( Message => $Param{Article}->{Body} );
if ( $Decrypt{Successful} ) {
# remember to result
$Self->{Result} = \%Decrypt;
$Param{Article}->{Body} = $Decrypt{Data};
# updated article body
$ArticleBackendObject->ArticleUpdate(
TicketID => $Param{Article}->{TicketID},
ArticleID => $Self->{ArticleID},
Key => 'Body',
Value => $Decrypt{Data},
UserID => $Self->{UserID},
);
# get a list of all article attachments
my %Index = $ArticleBackendObject->ArticleAttachmentIndex(
ArticleID => $Self->{ArticleID},
);
my @Attachments;
if ( IsHashRefWithData( \%Index ) ) {
for my $FileID ( sort keys %Index ) {
# get attachment details
my %Attachment = $ArticleBackendObject->ArticleAttachment(
ArticleID => $Self->{ArticleID},
FileID => $FileID,
);
# store attachemnts attributes that might change after decryption
my $AttachmentContent = $Attachment{Content};
my $AttachmentFilename = $Attachment{Filename};
# try to decrypt the attachment, non ecrypted attachments will succeed too.
%Decrypt = $PGPObject->Decrypt( Message => $Attachment{Content} );
if ( $Decrypt{Successful} ) {
# set decrypted content
$AttachmentContent = $Decrypt{Data};
# remove .pgp .gpg or asc extensions (if any)
$AttachmentFilename =~ s{ (\. [^\.]+) \. (?: pgp|gpg|asc) \z}{$1}msx;
}
# remember decrypted attachement, to add it later
push @Attachments, {
%Attachment,
Content => $AttachmentContent,
Filename => $AttachmentFilename,
ArticleID => $Self->{ArticleID},
UserID => $Self->{UserID},
};
}
# delete crypted attachments
$ArticleBackendObject->ArticleDeleteAttachment(
ArticleID => $Self->{ArticleID},
UserID => $Self->{UserID},
);
# write decrypted attachments to the storage
for my $Attachment (@Attachments) {
$ArticleBackendObject->ArticleWriteAttachment( %{$Attachment} );
}
}
push(
@Return,
{
Key => Translatable('Crypted'),
Value => $Decrypt{Message},
%Decrypt,
},
);
}
else {
# return with error
return (
{
Key => Translatable('Crypted'),
Value => $Decrypt{Message},
%Decrypt,
}
);
}
}
# Get plain article/email from filesystem storage.
my $Message = $ArticleBackendObject->ArticlePlain(
ArticleID => $Self->{ArticleID},
UserID => $Self->{UserID},
);
return if !$Message;
# check inline pgp signature (but ignore if is in quoted text)
if (
$Param{Article}->{Body}
&& $Param{Article}->{Body} =~ m{ ^\s* -----BEGIN [ ] PGP [ ] SIGNED [ ] MESSAGE----- }xms
)
{
# create local email parser object
my $ParserObject = Kernel::System::EmailParser->new(
Email => $Message,
);
# get the charset of the original message
my $Charset = $ParserObject->GetCharset();
# verify message PGP signature
%SignCheck = $PGPObject->Verify(
Message => $Param{Article}->{Body},
Charset => $Charset
);
if (%SignCheck) {
# remember to result
$Self->{Result} = \%SignCheck;
}
else {
# return with error
return (
{
Key => Translatable('Signed'),
Value => Translatable('"PGP SIGNED MESSAGE" header found, but invalid!'),
}
);
}
}
# check mime pgp
else {
# check body
# if body =~ application/pgp-encrypted
# if crypted, decrypt it
# remember that it was crypted!
my $Parser = MIME::Parser->new();
$Parser->decode_headers(0);
$Parser->extract_nested_messages(0);
$Parser->output_to_core('ALL');
# prevent modification of body by parser - required for bug #11755
$Parser->decode_bodies(0);
my $Entity = $Parser->parse_data($Message);
$Parser->decode_bodies(1);
my $Head = $Entity->head();
$Head->unfold();
$Head->combine('Content-Type');
my $ContentType = $Head->get('Content-Type');
# check if we need to decrypt it
if (
$ContentType
&& $ContentType =~ /multipart\/encrypted/i
&& $ContentType =~ /application\/pgp/i
)
{
# check sender (don't decrypt sent emails)
if ( $Param{Article}->{SenderType} && $Param{Article}->{SenderType} =~ /(agent|system)/i ) {
# return info
return (
{
Key => Translatable('Crypted'),
Value => Translatable('Sent message encrypted to recipient!'),
Successful => 1,
}
);
}
# get crypted part of the mail
my $Crypted = $Entity->parts(1)->as_string();
# decrypt it
my %Decrypt = $PGPObject->Decrypt(
Message => $Crypted,
);
if ( $Decrypt{Successful} ) {
$Entity = $Parser->parse_data( $Decrypt{Data} );
my $Head = $Entity->head();
$Head->unfold();
$Head->combine('Content-Type');
$ContentType = $Head->get('Content-Type');
# use a copy of the Entity to get the body, otherwise the original mail content
# could be altered and a signature verify could fail. See Bug#9954
my $EntityCopy = $Entity->dup();
my $ParserObject = Kernel::System::EmailParser->new(
Entity => $EntityCopy,
);
my $Body = $ParserObject->GetMessageBody();
# updated article body
$ArticleBackendObject->ArticleUpdate(
TicketID => $Param{Article}->{TicketID},
ArticleID => $Self->{ArticleID},
Key => 'Body',
Value => $Body,
UserID => $Self->{UserID},
);
# delete crypted attachments
$ArticleBackendObject->ArticleDeleteAttachment(
ArticleID => $Self->{ArticleID},
UserID => $Self->{UserID},
);
# write attachments to the storage
for my $Attachment ( $ParserObject->GetAttachments() ) {
$ArticleBackendObject->ArticleWriteAttachment(
%{$Attachment},
ArticleID => $Self->{ArticleID},
UserID => $Self->{UserID},
);
}
push(
@Return,
{
Key => Translatable('Crypted'),
Value => $Decrypt{Message},
%Decrypt,
},
);
}
else {
push(
@Return,
{
Key => Translatable('Crypted'),
Value => $Decrypt{Message},
%Decrypt,
},
);
}
}
if (
$ContentType
&& $ContentType =~ /multipart\/signed/i
&& $ContentType =~ /application\/pgp/i
&& $Entity->parts(0)
&& $Entity->parts(1)
)
{
my $SignedText = $Entity->parts(0)->as_string();
my $SignatureText = $Entity->parts(1)->body_as_string();
# according to RFC3156 all line endings MUST be CR/LF
$SignedText =~ s/\x0A/\x0D\x0A/g;
$SignedText =~ s/\x0D+/\x0D/g;
%SignCheck = $PGPObject->Verify(
Message => $SignedText,
Sign => $SignatureText,
);
}
}
if (%SignCheck) {
# return result
push(
@Return,
{
Key => Translatable('Signed'),
Value => $SignCheck{Message},
%SignCheck,
},
);
}
return @Return;
}
sub Filter {
my ( $Self, %Param ) = @_;
# remove signature if one is found
if ( $Self->{Result}->{SignatureFound} ) {
# remove pgp begin signed message
$Param{Article}->{Body} =~ s/^-----BEGIN\sPGP\sSIGNED\sMESSAGE-----.+?Hash:\s.+?$//sm;
# remove pgp inline sign
$Param{Article}->{Body}
=~ s/^-----BEGIN\sPGP\sSIGNATURE-----.+?-----END\sPGP\sSIGNATURE-----//sm;
}
return 1;
}
1;