From 6a3e400aa3922d6814b49977cce7d2e49a3a8a0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Josef=20=C5=A0pa=C4=8Dek?= Date: Tue, 22 Sep 2026 18:26:53 +0200 Subject: [PATCH 1/3] Added check for encrypted PDF documents --- Changes | 4 +++ MANIFEST | 2 ++ lib/PDF/Builder.pm | 5 +++ lib/PDF/Builder/Basic/PDF/File.pm | 52 +++++++++++++++++++++++++++- t/encryption.t | 27 +++++++++++++++ t/resources/sample-encrypted-R2.pdf | Bin 0 -> 17160 bytes 6 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 t/encryption.t create mode 100644 t/resources/sample-encrypted-R2.pdf diff --git a/Changes b/Changes index 00e10e7..fcaef34 100644 --- a/Changes +++ b/Changes @@ -5,6 +5,10 @@ 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. + * 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 diff --git a/MANIFEST b/MANIFEST index 0d7264c..2c6994c 100644 --- a/MANIFEST +++ b/MANIFEST @@ -236,6 +236,7 @@ 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/test-rgba.png t/00-all-usable.t t/01-basic.t @@ -250,6 +251,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 diff --git a/lib/PDF/Builder.pm b/lib/PDF/Builder.pm index 7e84f39..5ee8dc6 100644 --- a/lib/PDF/Builder.pm +++ b/lib/PDF/Builder.pm @@ -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 diff --git a/lib/PDF/Builder/Basic/PDF/File.pm b/lib/PDF/Builder/Basic/PDF/File.pm index 1e7ad2f..22acbc7 100644 --- a/lib/PDF/Builder/Basic/PDF/File.pm +++ b/lib/PDF/Builder/Basic/PDF/File.pm @@ -325,10 +325,61 @@ 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 : '?', + ); + + croak "PDF encryption is not implemented yet: $description"; +} + =head2 version $new_version = $p->pdf_version($version, %opts) # Set @@ -824,7 +875,6 @@ sub readval { my $length = $result->{'Length'}->val() + 0; # ensure seen as decimal $result->{' streamsrc'} = $fh; $result->{' streamloc'} = $fh->tell() - length($str); - unless ($opts{'nostreams'}) { if ($length > length($str)) { $value = $str; diff --git a/t/encryption.t b/t/encryption.t new file mode 100644 index 0000000..09089a7 --- /dev/null +++ b/t/encryption.t @@ -0,0 +1,27 @@ +use Test::More tests => 2; + +use strict; +use warnings; + +use PDF::Builder; +use PDF::Builder::Basic::PDF::File; + +{ + local $@; + eval { PDF::Builder->open('t/resources/sample-encrypted-R2.pdf') }; + like( + $@, + qr/^PDF encryption is not implemented yet: Filter=Standard R=2 V=1 CFM=\?/, + q{R2 encryption is rejected with a descriptive error}, + ); +} + +{ + local $@; + eval { PDF::Builder::Basic::PDF::File->open('t/resources/sample-encrypted-R2.pdf') }; + like( + $@, + qr/^PDF encryption is not implemented yet: Filter=Standard R=2 V=1 CFM=\?/, + q{Direct File->open() also rejects R2 encryption before object access}, + ); +} diff --git a/t/resources/sample-encrypted-R2.pdf b/t/resources/sample-encrypted-R2.pdf new file mode 100644 index 0000000000000000000000000000000000000000..56ce251b02a066ce8323de6c0cedfc764eecfecd GIT binary patch literal 17160 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 F0RVZ(f`kA7 literal 0 HcmV?d00001 From 201857dfb9e9d872650255d72c0630f61748187f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Josef=20=C5=A0pa=C4=8Dek?= Date: Tue, 22 Sep 2026 18:27:08 +0200 Subject: [PATCH 2/3] Added sample PDF file with R3 encryption --- Changes | 1 + MANIFEST | 1 + t/encryption.t | 22 +++++++++++++++++++++- t/resources/sample-encrypted-R3.pdf | Bin 0 -> 17443 bytes 4 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 t/resources/sample-encrypted-R3.pdf diff --git a/Changes b/Changes index fcaef34..d84b56d 100644 --- a/Changes +++ b/Changes @@ -8,6 +8,7 @@ See also INFO/Changes_2025 for earlier version 3 release logs. * 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. * lib/PDF/Builder/Content/Column.pm Finish cross references for column() to support multiple passes, for diff --git a/MANIFEST b/MANIFEST index 2c6994c..c02bd3a 100644 --- a/MANIFEST +++ b/MANIFEST @@ -237,6 +237,7 @@ 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 diff --git a/t/encryption.t b/t/encryption.t index 09089a7..b4643da 100644 --- a/t/encryption.t +++ b/t/encryption.t @@ -1,4 +1,4 @@ -use Test::More tests => 2; +use Test::More tests => 4; use strict; use warnings; @@ -16,6 +16,26 @@ use PDF::Builder::Basic::PDF::File; ); } +{ + local $@; + eval { PDF::Builder->open('t/resources/sample-encrypted-R3.pdf') }; + like( + $@, + qr/^PDF encryption is not implemented yet: Filter=Standard R=3 V=2 CFM=\?/, + q{R3 encryption is rejected with a descriptive error}, + ); +} + +{ + local $@; + eval { PDF::Builder::Basic::PDF::File->open('t/resources/sample-encrypted-R3.pdf') }; + like( + $@, + qr/^PDF encryption is not implemented yet: Filter=Standard R=3 V=2 CFM=\?/, + q{Direct File->open() also rejects R3 encryption before object access}, + ); +} + { local $@; eval { PDF::Builder::Basic::PDF::File->open('t/resources/sample-encrypted-R2.pdf') }; diff --git a/t/resources/sample-encrypted-R3.pdf b/t/resources/sample-encrypted-R3.pdf new file mode 100644 index 0000000000000000000000000000000000000000..266167e16e5ce4acf6bc2193749a8cfe3237a614 GIT binary patch literal 17443 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 K$z&$m8UG8r1P-GB literal 0 HcmV?d00001 From 110296771fce0c13c0e0516dad3da0b312fa0821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Josef=20=C5=A0pa=C4=8Dek?= Date: Tue, 22 Sep 2026 18:28:53 +0200 Subject: [PATCH 3/3] Added decryption of PDF R2 --- Changes | 1 + MANIFEST | 1 + META.json | 5 + META.yml | 4 + Makefile.PL | 2 + lib/PDF/Builder/Basic/PDF/Dict.pm | 13 +++ lib/PDF/Builder/Basic/PDF/File.pm | 56 +++++++++- lib/PDF/Builder/Security/Standard.pm | 159 +++++++++++++++++++++++++++ t/encryption.t | 42 ++++--- 9 files changed, 265 insertions(+), 18 deletions(-) create mode 100644 lib/PDF/Builder/Security/Standard.pm diff --git a/Changes b/Changes index d84b56d..10da1f0 100644 --- a/Changes +++ b/Changes @@ -9,6 +9,7 @@ See also INFO/Changes_2025 for earlier version 3 release logs. 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 diff --git a/MANIFEST b/MANIFEST index c02bd3a..f3cb2b4 100644 --- a/MANIFEST +++ b/MANIFEST @@ -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 diff --git a/META.json b/META.json index 915ebaf..518c6a2 100644 --- a/META.json +++ b/META.json @@ -33,6 +33,8 @@ "runtime" : { "requires" : { "Compress::Zlib" : "1.0", + "Crypt::RC4" : "0", + "Digest::MD5" : "0", "Font::TTF" : "1.04", "perl" : "5.028000" } @@ -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" } diff --git a/META.yml b/META.yml index 9465bc3..c58de08 100644 --- a/META.yml +++ b/META.yml @@ -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: @@ -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: diff --git a/Makefile.PL b/Makefile.PL index fcd5361..db2cf91 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -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 diff --git a/lib/PDF/Builder/Basic/PDF/Dict.pm b/lib/PDF/Builder/Basic/PDF/Dict.pm index 737b8ab..2d0ab11 100644 --- a/lib/PDF/Builder/Basic/PDF/Dict.pm +++ b/lib/PDF/Builder/Basic/PDF/Dict.pm @@ -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'}); @@ -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); } diff --git a/lib/PDF/Builder/Basic/PDF/File.pm b/lib/PDF/Builder/Basic/PDF/File.pm index 22acbc7..b29895b 100644 --- a/lib/PDF/Builder/Basic/PDF/File.pm +++ b/lib/PDF/Builder/Basic/PDF/File.pm @@ -377,6 +377,20 @@ sub init_security { 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"; } @@ -875,6 +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; @@ -988,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/^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 @@ -1145,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) diff --git a/lib/PDF/Builder/Security/Standard.pm b/lib/PDF/Builder/Security/Standard.pm new file mode 100644 index 0000000..8fa05b8 --- /dev/null +++ b/lib/PDF/Builder/Security/Standard.pm @@ -0,0 +1,159 @@ +package PDF::Builder::Security::Standard; + +use strict; +use warnings; + +# VERSION + +use Carp qw(croak); +use Crypt::RC4 qw(RC4); +use Digest::MD5 qw(md5); + +my $PASSWORD_PADDING = pack( + 'H*', + '28bf4e5e4e758a4164004e56fffa0108' + . '2e2e00b6d0683e802f0ca9fe6453697a', +); + +sub new { + my ($class, %options) = @_; + + return bless { + ' file' => $options{'file'}, + ' encrypt' => $options{'encrypt'}, + ' identifier' => $options{'identifier'}, + ' password' => defined $options{'password'} ? $options{'password'} : '', + }, $class; +} + +sub authenticate { + my ($self) = @_; + + my $encrypt = $self->{' encrypt'}; + my $revision = $encrypt->{'R'}->val(); + croak "Unsupported Standard security handler revision: $revision" + unless $revision == 2; + + my $owner_entry = $encrypt->{'O'}->val(); + my $user_entry = $encrypt->{'U'}->val(); + my $permissions = $encrypt->{'P'}->val(); + my $identifier = $self->_identifier(); + + croak 'Invalid Standard security handler dictionary' + unless defined $owner_entry + && length($owner_entry) == 32 + && defined $user_entry + && length($user_entry) == 32 + && defined $permissions + && defined $identifier; + + my $password = $self->{' password'}; + my $file_key = $self->_file_key(_pad_password($password), $owner_entry, + $permissions, $identifier); + + unless (_secure_equal(RC4($file_key, $PASSWORD_PADDING), $user_entry)) { + my $owner_key = substr(md5(_pad_password($password)), 0, 5); + my $user_padding = RC4($owner_key, $owner_entry); + $file_key = $self->_file_key($user_padding, $owner_entry, + $permissions, $identifier); + + croak 'Invalid PDF password' + unless _secure_equal(RC4($file_key, $PASSWORD_PADDING), $user_entry); + } + + $self->{' file_key'} = $file_key; + return $self; +} + +sub decrypt_string { + my ($self, $objnum, $objgen, $data) = @_; + + return RC4($self->object_key($objnum, $objgen), $data); +} + +sub decrypt_stream { + my ($self, $objnum, $objgen, $data) = @_; + + return RC4($self->object_key($objnum, $objgen), $data); +} + +sub stream_cipher { + my ($self, $objnum, $objgen) = @_; + + return Crypt::RC4->new($self->object_key($objnum, $objgen)); +} + +sub object_key { + my ($self, $objnum, $objgen) = @_; + + croak 'Object number is required for encrypted PDF data' + unless defined $objnum; + + my $object_bytes = pack( + 'C3', + $objnum & 0xff, + ($objnum >> 8) & 0xff, + ($objnum >> 16) & 0xff, + ); + my $generation_bytes = pack( + 'C2', + $objgen & 0xff, + ($objgen >> 8) & 0xff, + ); + + return substr( + md5($self->{' file_key'} . $object_bytes . $generation_bytes), + 0, + 10, + ); +} + +sub _file_key { + my ($self, $password, $owner_entry, $permissions, $identifier) = @_; + + return substr( + md5( + $password + . $owner_entry + . pack('l<', $permissions) + . $identifier, + ), + 0, + 5, + ); +} + +sub _identifier { + my ($self) = @_; + my $identifier = $self->{' identifier'}; + + return unless $identifier && $identifier->can('elements'); + + my ($first) = $identifier->elements(); + return $first->val() if $first && $first->can('val'); + return; +} + +sub _pad_password { + my ($password) = @_; + + $password = substr($password, 0, 32); + return substr($password . $PASSWORD_PADDING, 0, 32); +} + +sub _secure_equal { + my ($left, $right) = @_; + + return 0 unless defined $left && defined $right; + return 0 unless length($left) == length($right); + + my $different = 0; + for my $index (0 .. length($left) - 1) { + $different |= ord(substr($left, $index, 1)) + ^ ord(substr($right, $index, 1)); + } + + return $different == 0; +} + +1; diff --git a/t/encryption.t b/t/encryption.t index b4643da..e59909f 100644 --- a/t/encryption.t +++ b/t/encryption.t @@ -1,4 +1,4 @@ -use Test::More tests => 4; +use Test::More tests => 9; use strict; use warnings; @@ -6,14 +6,32 @@ use warnings; use PDF::Builder; use PDF::Builder::Basic::PDF::File; +my $pdf = PDF::Builder->open('t/resources/sample-encrypted-R2.pdf'); +isa_ok($pdf, 'PDF::Builder'); +is($pdf->pages(), 1, q{R2 encrypted PDF has one page}); +my $object = $pdf->{'pdf'}->read_objnum(7, 0); +isa_ok($object, 'PDF::Builder::Basic::PDF::Dict'); +is($object->{'Type'}->val(), 'Pages', q{Object stream object is decrypted}); +is($object->{'Count'}->val(), 1, q{Decrypted Pages object has one page}); +my $file = PDF::Builder::Basic::PDF::File->open( + 't/resources/sample-encrypted-R2.pdf', +); +$object = $file->read_objnum(7, 0); +isa_ok( + $object, + 'PDF::Builder::Basic::PDF::Dict', + q{Direct File API decrypts an object stream object}, +); + { local $@; - eval { PDF::Builder->open('t/resources/sample-encrypted-R2.pdf') }; - like( - $@, - qr/^PDF encryption is not implemented yet: Filter=Standard R=2 V=1 CFM=\?/, - q{R2 encryption is rejected with a descriptive error}, - ); + eval { + PDF::Builder->open( + 't/resources/sample-encrypted-R2.pdf', + password => 'wrong-password', + ); + }; + like($@, qr/^Invalid PDF password/, q{Wrong R2 password is rejected}); } { @@ -35,13 +53,3 @@ use PDF::Builder::Basic::PDF::File; q{Direct File->open() also rejects R3 encryption before object access}, ); } - -{ - local $@; - eval { PDF::Builder::Basic::PDF::File->open('t/resources/sample-encrypted-R2.pdf') }; - like( - $@, - qr/^PDF encryption is not implemented yet: Filter=Standard R=2 V=1 CFM=\?/, - q{Direct File->open() also rejects R2 encryption before object access}, - ); -}