From 09175e457852c585a68a86d43280f7e0790a4a3b Mon Sep 17 00:00:00 2001 From: Jay Berkenbilt Date: Mon, 19 Oct 2009 00:17:11 +0000 Subject: [PATCH] more testing, bug fix for linearized aes encrypted files git-svn-id: svn+q:///qpdf/trunk@824 71b93d88-0707-0410-a8cf-f5a4172ac649 --- ChangeLog | 6 +++ TODO | 56 +++++------------------ include/qpdf/QPDFWriter.hh | 7 +++ libqpdf/QPDFWriter.cc | 59 ++++++++++++++++++++++++- qpdf/qpdf.testcov | 3 +- qpdf/qtest/qpdf.test | 66 +++++++++++++++++++++++++++- qpdf/qtest/qpdf/aes-forced-check.out | 5 +++ 7 files changed, 152 insertions(+), 50 deletions(-) create mode 100644 qpdf/qtest/qpdf/aes-forced-check.out diff --git a/ChangeLog b/ChangeLog index fe79bdfe..13badadb 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,11 @@ 2009-10-18 Jay Berkenbilt + * If forcing version, disable object stream creation and/or + encryption if previous specifications are incompatible with new + version. It is still possible that PDF content, compression + schemes, etc., may be incompatible with the new version, but at + least this way, older viewers will at least have a chance. + * libqpdf/QPDFWriter.cc (unparseObject): avoid compressing Metadata streams if possible. diff --git a/TODO b/TODO index 952f5c80..81f05087 100644 --- a/TODO +++ b/TODO @@ -1,6 +1,9 @@ 2.1 === + * Really need to handle /Crypt filter for Metadata. Search for crypt + below. + * Update documentation to reflect new command line flags and any other relevant changes. Should read through ChangeLog and the manual before releasing 2.1. @@ -16,9 +19,6 @@ * Add comments for the security functions that map them back to the items in Adobe's products. - * Have force version at least turn off object streams and maybe - change security settings? - * Add error codes to QPDFException. Change the error interface so that warnings and errors are pointers that can be queried using more C API functions. We need a way to get a full string as well @@ -47,49 +47,7 @@ - Update C API for R4 encryption - - When we write encrypted files, we must remember to omit any - encryption filter settings from original streams. - - test various combinations with and without cleartext-metadata - and aes in compression tests - - - figure out a way to test crypt filters defined on a stream - - - test combinations of linearization and v4 encryption - - - would be nice to test strings and streams with different - encryption types, but without sample data, we'd have to write - them ourselves which is not that useful - - - figure out how to look at the metadata so I can tell whether - /EncryptMetadata is working the way it's supposed to - - - Do something with embedded files, but what and how? - - - General notes: - - /CF - keys are crypt filter names, values are are crypt - dictionaries - - Individual streams may also have crypt filters. Filter type - /Crypt; /DecodeParms must contain a Crypt filter decode - parameters dictionary whose /Name entry specifies the particular - filter to be used. If /Name is missing, use /Identity. - /DecodeParms << /Crypt << /Name /XYZ >> >> where /XYZ is - /Identity or a key in /CF. - - /Identity means not to encrypt. - - Crypt Dictionaries - - /Type (optional) /CryptFilter - /CFM: - /V2 - use rc4 - /AESV2 - use aes - /Length - supposed to be key length, but the one file I have - has a bogus value for it, so I'm ignoring it. - - We will ignore remaining fields and values. 2.2 === @@ -127,7 +85,13 @@ General crypt filters, and there are already special cases in the code to handle those. Most likely, it won't be a problem, but someday someone may find a file that qpdf doesn't work on because of crypt - filters. + filters. There is an example in the spec of using a crypt filter + on a metadata stream. + + When we write encrypted files, we must remember to omit any + encryption filter settings from original streams. + + We need a way to test this. * The second xref stream for linearized files has to be padded only because we need file_size as computed in pass 1 to be accurate. If diff --git a/include/qpdf/QPDFWriter.hh b/include/qpdf/QPDFWriter.hh index 8a4e9ebe..e16c5f99 100644 --- a/include/qpdf/QPDFWriter.hh +++ b/include/qpdf/QPDFWriter.hh @@ -98,6 +98,12 @@ class DLL_EXPORT QPDFWriter // you are sure the PDF file in question has no features of newer // versions of PDF or if you are willing to create files that old // viewers may try to open but not be able to properly interpret. + // If any encryption has been applied to the document either + // explicitly or by preserving the encryption of the source + // document, forcing the PDF version to a value too low to support + // that type of encryption will explicitly disable decryption. + // Additionally, forcing to a version below 1.5 will disable + // object streams. void forcePDFVersion(std::string const&); // Cause a static /ID value to be generated. Use only in test @@ -193,6 +199,7 @@ class DLL_EXPORT QPDFWriter char const* user_password, char const* owner_password, bool allow_accessibility, bool allow_extract, r3_print_e print, r3_modify_e modify); + void disableIncompatbleEncryption(float v); void setEncryptionParameters( char const* user_password, char const* owner_password, int V, int R, int key_len, std::set& bits_to_clear); diff --git a/libqpdf/QPDFWriter.cc b/libqpdf/QPDFWriter.cc index d094aa66..3c1640f7 100644 --- a/libqpdf/QPDFWriter.cc +++ b/libqpdf/QPDFWriter.cc @@ -344,6 +344,52 @@ QPDFWriter::copyEncryptionParameters() } } +void +QPDFWriter::disableIncompatbleEncryption(float v) +{ + if (! this->encrypted) + { + return; + } + + bool disable = false; + if (v < 1.3) + { + disable = true; + } + else + { + int V = atoi(encryption_dictionary["/V"].c_str()); + int R = atoi(encryption_dictionary["/R"].c_str()); + if (v < 1.4) + { + if ((V > 1) || (R > 2)) + { + disable = true; + } + } + else if (v < 1.5) + { + if ((V > 2) || (R > 3)) + { + disable = true; + } + } + else if (v < 1.6) + { + if (this->encrypt_use_aes) + { + disable = true; + } + } + } + if (disable) + { + QTC::TC("qpdf", "QPDFWriter forced version disabled encryption"); + this->encrypted = false; + } +} + void QPDFWriter::setEncryptionParametersInternal( int V, int R, int key_len, long P, @@ -965,7 +1011,7 @@ QPDFWriter::unparseObject(QPDFObjectHandle object, int level, Buffer* buf = bufpl.getBuffer(); val = QPDF_String( std::string((char*)buf->getBuffer(), - (size_t)buf->getSize())).unparse(); + (size_t)buf->getSize())).unparse(true); delete buf; } else @@ -1423,6 +1469,17 @@ QPDFWriter::write() copyEncryptionParameters(); } + if (! this->forced_pdf_version.empty()) + { + float v = atof(this->forced_pdf_version.c_str()); + disableIncompatbleEncryption(v); + if (v < 1.5) + { + QTC::TC("qpdf", "QPDFWriter forcing object stream disable"); + this->object_stream_mode = o_disable; + } + } + if (this->qdf_mode || this->normalize_content || (this->stream_data_mode == s_uncompress)) { diff --git a/qpdf/qpdf.testcov b/qpdf/qpdf.testcov index fc4cb383..d1cf3293 100644 --- a/qpdf/qpdf.testcov +++ b/qpdf/qpdf.testcov @@ -166,4 +166,5 @@ QPDF_encryption CFM AESV2 0 QPDF_encryption aes decode string 0 QPDF_encryption cleartext metadata 0 QPDF_encryption aes decode stream 0 -QPDF_encryption stream crypt filter 0 +QPDFWriter forcing object stream disable 0 +QPDFWriter forced version disabled encryption 0 diff --git a/qpdf/qtest/qpdf.test b/qpdf/qtest/qpdf.test index b4dc07ad..4ff06406 100644 --- a/qpdf/qtest/qpdf.test +++ b/qpdf/qtest/qpdf.test @@ -998,14 +998,14 @@ $td->runtest("check linearization", $td->NORMALIZE_NEWLINES); $td->runtest("linearize and encrypt file", {$td->COMMAND => - "qpdf --linearize --encrypt user owner 128 --" . + "qpdf --linearize --encrypt user owner 128 --use-aes=y --" . " lin-special.pdf a.pdf"}, {$td->STRING => "", $td->EXIT_STATUS => 0}); $td->runtest("check encryption", {$td->COMMAND => "qpdf --show-encryption --password=owner a.pdf", $td->FILTER => "grep -v allowed"}, - {$td->STRING => "R = 3\nP = -4\nUser password = user\n", + {$td->STRING => "R = 4\nP = -4\nUser password = user\n", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); $td->runtest("check linearization", @@ -1015,6 +1015,68 @@ $td->runtest("check linearization", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); +# Test AES encryption in various ways. +$n_tests += 14; +$td->runtest("encrypt with AES", + {$td->COMMAND => "qpdf --encrypt '' '' 128 --use-aes=y --" . + " enc-base.pdf a.pdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}); +$td->runtest("check encryption", + {$td->COMMAND => "qpdf --show-encryption a.pdf", + $td->FILTER => "grep -v allowed"}, + {$td->STRING => "R = 4\nP = -4\nUser password = \n", + $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("convert original to qdf", + {$td->COMMAND => "qpdf --static-id --no-original-object-ids" . + " --qdf --min-version=1.6 enc-base.pdf a.qdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}); +$td->runtest("convert encrypted to qdf", + {$td->COMMAND => "qpdf --static-id --no-original-object-ids" . + " --qdf a.pdf b.qdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}); +$td->runtest("compare files", + {$td->FILE => 'a.qdf'}, + {$td->FILE => 'b.qdf'}); +$td->runtest("linearize with AES and object streams", + {$td->COMMAND => "qpdf --encrypt '' '' 128 --use-aes=y --" . + " --linearize --object-streams=generate enc-base.pdf a.pdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}); +$td->runtest("check encryption", + {$td->COMMAND => "qpdf --show-encryption a.pdf", + $td->FILTER => "grep -v allowed"}, + {$td->STRING => "R = 4\nP = -4\nUser password = \n", + $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("linearize original", + {$td->COMMAND => "qpdf --linearize --object-streams=generate" . + " enc-base.pdf b.pdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}); +$td->runtest("convert linearized original to qdf", + {$td->COMMAND => "qpdf --static-id --no-original-object-ids" . + " --qdf --object-streams=generate --min-version=1.6" . + " b.pdf a.qdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}); +$td->runtest("convert encrypted to qdf", + {$td->COMMAND => "qpdf --static-id --no-original-object-ids" . + " --qdf --object-streams=generate a.pdf b.qdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}); +$td->runtest("compare files", + {$td->FILE => 'a.qdf'}, + {$td->FILE => 'b.qdf'}); +$td->runtest("force version on aes encrypted", + {$td->COMMAND => "qpdf --force-version=1.4 a.pdf b.pdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}); +$td->runtest("check", + {$td->COMMAND => "qpdf --check b.pdf"}, + {$td->FILE => "aes-forced-check.out", + $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("make sure there is no xref stream", + {$td->COMMAND => "grep /ObjStm b.pdf | wc -l"}, + {$td->REGEXP => "\\s*0\\s*", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); + show_ntests(); # ---------- $td->notify("--- Content Preservation Tests ---"); diff --git a/qpdf/qtest/qpdf/aes-forced-check.out b/qpdf/qtest/qpdf/aes-forced-check.out new file mode 100644 index 00000000..9a71439d --- /dev/null +++ b/qpdf/qtest/qpdf/aes-forced-check.out @@ -0,0 +1,5 @@ +checking b.pdf +PDF Version: 1.4 +File is not encrypted +File is not linearized +No errors found