Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions Changes
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ See also INFO/Changes_2025 for earlier version 3 release logs.

==== New features

* PDF encryption
Add detection of encrypted PDF documents and explicit rejection of
unsupported security handler revisions.
Add a sample PDF file with R3 encryption for rejection testing.
Add decryption of PDF R2.

* lib/PDF/Builder/Content/Column.pm
Finish cross references for column() to support multiple passes, for
forward references when the link text needs to be picked up later in
Expand Down
4 changes: 4 additions & 0 deletions MANIFEST
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ lib/PDF/Builder/Outline.pm
lib/PDF/Builder/Outlines.pm
lib/PDF/Builder/Page.pm
lib/PDF/Builder/Resource.pm
lib/PDF/Builder/Security/Standard.pm
lib/PDF/Builder/UniWrap.pm
lib/PDF/Builder/Util.pm
lib/PDF/Builder/ViewerPreferences.pm
Expand All @@ -236,6 +237,8 @@ t/resources/1x1-lzw.tif
t/resources/sample.pdf
t/resources/sample-xrefstm.pdf
t/resources/sample-xrefstm-index.pdf
t/resources/sample-encrypted-R2.pdf
t/resources/sample-encrypted-R3.pdf
t/resources/test-rgba.png
t/00-all-usable.t
t/01-basic.t
Expand All @@ -250,6 +253,7 @@ t/content.t
t/content-deprecated.t
t/cs-webcolor.t
t/deprecations.t
t/encryption.t
t/extgstate.t
t/filter-ascii85decode.t
t/filter-asciihexdecode.t
Expand Down
5 changes: 5 additions & 0 deletions META.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
"runtime" : {
"requires" : {
"Compress::Zlib" : "1.0",
"Crypt::RC4" : "0",
"Digest::MD5" : "0",
"Font::TTF" : "1.04",
"perl" : "5.028000"
}
Expand Down Expand Up @@ -106,6 +108,9 @@
"PDF::Builder::Resource::XObject::Image" : {
"file" : "lib/PDF/Builder/Resource/XObject/Image.pm"
},
"PDF::Builder::Security::Standard" : {
"file" : "lib/PDF/Builder/Security/Standard.pm"
},
"PDF::Builder::ViewerPreferences" : {
"file" : "lib/PDF/Builder/ViewerPreferences.pm"
}
Expand Down
4 changes: 4 additions & 0 deletions META.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ provides:
file: lib/PDF/Builder/Resource/XObject/Form.pm
PDF::Builder::Resource::XObject::Image:
file: lib/PDF/Builder/Resource/XObject/Image.pm
PDF::Builder::Security::Standard:
file: lib/PDF/Builder/Security/Standard.pm
PDF::Builder::ViewerPreferences:
file: lib/PDF/Builder/ViewerPreferences.pm
#recommends:
Expand All @@ -73,6 +75,8 @@ provides:
# SVGPDF: '0.086.2'
requires:
Compress::Zlib: '1.0'
Crypt::RC4: '0'
Digest::MD5: '0'
Font::TTF: '1.04'
perl: '5.028000'
resources:
Expand Down
2 changes: 2 additions & 0 deletions Makefile.PL
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ my %WriteMakefileArgs =

PREREQ_PM => {
"Compress::Zlib" => 1.0,
"Crypt::RC4" => 0,
"Digest::MD5" => 0,
"Font::TTF" => 1.04,
# "Readonly" => 0, use when TIFF changes go in

Expand Down
5 changes: 5 additions & 0 deletions lib/PDF/Builder.pm
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,11 @@ The 'compress' option can be
given to specify stream compression: default is 'flate', 'none' (or 0) is no
compression. No other compression methods are currently supported.

=item password

Password used to open an encrypted PDF. Encrypted PDF support is currently
limited to Standard security handler revision 2 (RC4).

=item outver

The 'outver' option defaults to 1.4 as the output PDF version and the highest
Expand Down
13 changes: 13 additions & 0 deletions lib/PDF/Builder/Basic/PDF/Dict.pm
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,17 @@ sub read_stream {
}
}

my $decryptor;
if ($self->{' streamencrypted'}
&& $self->{' streamsecurity'}
&& defined $self->{' streamobjnum'}
&& (!defined($self->{'Type'}) || $self->{'Type'}->val() ne 'XRef')) {
$decryptor = $self->{' streamsecurity'}->stream_cipher(
$self->{' streamobjnum'},
$self->{' streamobjgen'} // 0,
);
}

my $last = 0;
if (defined $self->{' streamfile'}) {
unlink ($self->{' streamfile'});
Expand All @@ -328,6 +339,8 @@ sub read_stream {
read($fh, $data, $len - $i);
}

$data = $decryptor->RC4($data) if $decryptor;

foreach my $filter (@filters) {
$data = $filter->infilt($data, $last);
}
Expand Down
108 changes: 106 additions & 2 deletions lib/PDF/Builder/Basic/PDF/File.pm
Original file line number Diff line number Diff line change
Expand Up @@ -325,10 +325,75 @@ sub open {
foreach my $key (keys %$tdict) {
$self->{$key} = $tdict->{$key};
}
$self->init_security(%options);

return $self;
} # end of open()

=head2 $p->init_security(%options)

Initializes or validates the security handler after the xref/trailer chain has
been read and before the document catalog is dereferenced.

At present this supports decryption of Standard security handler revision 2.
Other encrypted files are rejected here with a descriptive error instead of
being allowed to fail later while parsing an encrypted object.

=cut

sub init_security {
my ($self, %options) = @_;

return $self unless defined $self->{'Encrypt'};

my $encrypt = $self->{'Encrypt'}->realise();

my $filter = defined $encrypt->{'Filter'}
? $encrypt->{'Filter'}->val()
: 'Standard';

my $revision = defined $encrypt->{'R'}
? $encrypt->{'R'}->val()
: undef;

my $version = defined $encrypt->{'V'}
? $encrypt->{'V'}->val()
: undef;

my $cfm;
if (defined $encrypt->{'CF'}) {
my $crypt_filters = $encrypt->{'CF'}->realise();
if (defined $crypt_filters->{'StdCF'}) {
my $stdcf = $crypt_filters->{'StdCF'}->realise();
$cfm = $stdcf->{'CFM'}->val() if defined $stdcf->{'CFM'};
}
}

my $description = sprintf(
'Filter=%s R=%s V=%s CFM=%s',
defined $filter ? $filter : '?',
defined $revision ? $revision : '?',
defined $version ? $version : '?',
defined $cfm ? $cfm : '?',
);

if (defined $revision && defined $version && $revision == 2 && $version == 1) {
require PDF::Builder::Security::Standard;

$self->{'security'} = PDF::Builder::Security::Standard->new(
file => $self,
encrypt => $encrypt,
identifier => $self->{'ID'},
password => $options{'password'},
);
$self->{'security'}->authenticate();

return $self;
}

croak "PDF encryption is not implemented yet: $description";
}

=head2 version

$new_version = $p->pdf_version($version, %opts) # Set
Expand Down Expand Up @@ -824,7 +889,13 @@ sub readval {
my $length = $result->{'Length'}->val() + 0; # ensure seen as decimal
$result->{' streamsrc'} = $fh;
$result->{' streamloc'} = $fh->tell() - length($str);

$result->{' streamobjnum'} = $opts{'objnum'}
if defined $opts{'objnum'};
$result->{' streamobjgen'} = $opts{'objgen'} // 0
if defined $opts{'objnum'};
$result->{' streamencrypted'} = $opts{'encrypted'} ? 1 : 0;
$result->{' streamsecurity'} = $self->{'security'}
if $result->{' streamencrypted'};
unless ($opts{'nostreams'}) {
if ($length > length($str)) {
$value = $str;
Expand Down Expand Up @@ -938,6 +1009,7 @@ sub readval {
} # end while(TRUE) loop

$result = PDF::Builder::Basic::PDF::String->from_pdf($value);
$self->_decrypt_string($result, %opts);
# end Literal String check

} elsif ($str =~ m/^</) {
Expand All @@ -946,6 +1018,7 @@ sub readval {
$fh->read($str, 255, length($str)) while (0 > index($str, '>'));
($value, $str) = ($str =~ /^(.*?)>(.*)/s);
$result = PDF::Builder::Basic::PDF::String->from_pdf('<' . $value . '>');
$self->_decrypt_string($result, %opts);

} elsif ($str =~ m/^\[/) {
# Array
Expand Down Expand Up @@ -1095,18 +1168,49 @@ sub read_objnum {
close $fh;
}

# The object stream itself is decrypted as one stream. Objects inside
# it must not be decrypted a second time.
($object) = $self->readval($stream, %opts, update => 0);
return $object;
}

my $current_location = $self->{' INFILE'}->tell();
$self->{' INFILE'}->seek($object_location, 0);
($object) = $self->readval('', %opts);
my $encrypted = 1;
if (defined $self->{'Encrypt'}
&& $self->{'Encrypt'}->{' objnum'} == $num
&& $self->{'Encrypt'}->{' objgen'} == $gen) {
$encrypted = 0;
}

($object) = $self->readval(
'',
%opts,
encrypted => $encrypted,
objnum => $num,
objgen => $gen,
);
$self->{' INFILE'}->seek($current_location, 0);

return $object;
} # end of read_objnum()

sub _decrypt_string {
my ($self, $string, %opts) = @_;

return $string unless $opts{'encrypted'}
&& $self->{'security'}
&& defined $opts{'objnum'};

$string->{'val'} = $self->{'security'}->decrypt_string(
$opts{'objnum'},
$opts{'objgen'} // 0,
$string->val(),
);

return $string;
}

=head2 new_obj

$objind = $p->new_obj($obj)
Expand Down
Loading