diff --git a/ChangeLog b/ChangeLog index 0c43cd83..1b76b011 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,8 +1,18 @@ 2012-07-15 Jay Berkenbilt + * add new QPDF::isEncrypted method that returns some additional + information beyond other versions. + + * libqpdf/QPDFWriter.cc: fix copyEncryptionParameters to fix the + minimum PDF version based on other file's encryption needs. This + is a fix to code added on 2012-07-14 and did not impact previously + released code. + * libqpdf/QPDFWriter.cc (copyEncryptionParameters): Bug fix: qpdf was not preserving whether or not AES encryption was being used - when copying encryption parameters. + when copying encryption parameters. The file would still have + been properly encrypted, but a file that started off encrypted + with AES could have become encrypted with RC4. 2012-07-14 Jay Berkenbilt diff --git a/TODO b/TODO index d38cb002..6c8e043e 100644 --- a/TODO +++ b/TODO @@ -64,9 +64,11 @@ Next - Tests through qpdf command line: copy pages from multiple PDFs starting with one PDF and also starting with empty. - * qpdf commandline: provide an option to copy encryption parameters - from another file, specifying file and password. Search for "Copy - encryption parameters" in qpdf.test. + * Document --copy-encryption and --encryption-file-password in + manual. Mention that the first half of /ID as well as all the + encryption parameters are copied. Maybe mention about StrF and + StrM with respect to AES here and also with encryption + preservation. Soon diff --git a/include/qpdf/QPDF.hh b/include/qpdf/QPDF.hh index 4c8ede86..3b72e981 100644 --- a/include/qpdf/QPDF.hh +++ b/include/qpdf/QPDF.hh @@ -248,6 +248,12 @@ class QPDF QPDF_DLL bool isEncrypted(int& R, int& P); + QPDF_DLL + bool isEncrypted(int& R, int& P, int& V, + encryption_method_e& stream_method, + encryption_method_e& string_method, + encryption_method_e& file_method); + // Encryption permissions -- not enforced by QPDF QPDF_DLL bool allowAccessibility(); diff --git a/libqpdf/QPDF_encryption.cc b/libqpdf/QPDF_encryption.cc index ee5d5685..c73a47bf 100644 --- a/libqpdf/QPDF_encryption.cc +++ b/libqpdf/QPDF_encryption.cc @@ -743,6 +743,17 @@ QPDF::isEncrypted() const bool QPDF::isEncrypted(int& R, int& P) +{ + int V; + encryption_method_e stream, string, file; + return isEncrypted(R, P, V, stream, string, file); +} + +bool +QPDF::isEncrypted(int& R, int& P, int& V, + encryption_method_e& stream_method, + encryption_method_e& string_method, + encryption_method_e& file_method) { if (this->encrypted) { @@ -750,8 +761,13 @@ QPDF::isEncrypted(int& R, int& P) QPDFObjectHandle encrypt = trailer.getKey("/Encrypt"); QPDFObjectHandle Pkey = encrypt.getKey("/P"); QPDFObjectHandle Rkey = encrypt.getKey("/R"); + QPDFObjectHandle Vkey = encrypt.getKey("/V"); P = Pkey.getIntValue(); R = Rkey.getIntValue(); + V = Vkey.getIntValue(); + stream_method = this->cf_stream; + string_method = this->cf_stream; + file_method = this->cf_file; return true; } else diff --git a/qpdf/qpdf.cc b/qpdf/qpdf.cc index 148dff04..cf99ca44 100644 --- a/qpdf/qpdf.cc +++ b/qpdf/qpdf.cc @@ -33,18 +33,29 @@ Usage: qpdf [ options ] infilename [ outfilename ]\n\ \n\ An option summary appears below. Please see the documentation for details.\n\ \n\ +Note that when contradictory options are provided, whichever options are\n\ +provided last take precedence.\n\ +\n\ \n\ Basic Options\n\ -------------\n\ \n\ --password=password specify a password for accessing encrypted files\n\ --linearize generated a linearized (web optimized) file\n\ +--copy-encryption=file copy encryption parameters from specified file\n\ +--encryption-file-password=password\n\ + password used to open the file from which encryption\n\ + parameters are being copied\n\ --encrypt options -- generate an encrypted file\n\ --decrypt remove any encryption on the file\n\ \n\ -If neither --encrypt or --decrypt are given, qpdf will preserve any\n\ -encryption data associated with a file.\n\ +If none of --copy-encryption, --encrypt or --decrypt are given, qpdf will\n\ +preserve any encryption data associated with a file.\n\ \n\ +Note that when copying encryption parameters from another file, all\n\ +parameters will be copied, including both user and owner passwords, even\n\ +if the user password is used to open the other file. This works even if\n\ +the owner password is not known.\n\ \n\ Encryption Options\n\ ------------------\n\ @@ -192,12 +203,39 @@ static std::string show_bool(bool v) return v ? "allowed" : "not allowed"; } +static std::string show_encryption_method(QPDF::encryption_method_e method) +{ + std::string result = "unknown"; + switch (method) + { + case QPDF::e_none: + result = "none"; + break; + case QPDF::e_unknown: + result = "unknown"; + break; + case QPDF::e_rc4: + result = "RC4"; + break; + case QPDF::e_aes: + result = "AESv2"; + break; + // no default so gcc will warn for missing case + } + return result; +} + static void show_encryption(QPDF& pdf) { // Extract /P from /Encrypt int R = 0; int P = 0; - if (! pdf.isEncrypted(R, P)) + int V = 0; + QPDF::encryption_method_e stream_method = QPDF::e_unknown; + QPDF::encryption_method_e string_method = QPDF::e_unknown; + QPDF::encryption_method_e file_method = QPDF::e_unknown; + if (! pdf.isEncrypted(R, P, V, + stream_method, string_method, file_method)) { std::cout << "File is not encrypted" << std::endl; } @@ -206,25 +244,34 @@ static void show_encryption(QPDF& pdf) std::cout << "R = " << R << std::endl; std::cout << "P = " << P << std::endl; std::string user_password = pdf.getTrimmedUserPassword(); - std::cout << "User password = " << user_password << std::endl; - std::cout << "extract for accessibility: " - << show_bool(pdf.allowAccessibility()) << std::endl; - std::cout << "extract for any purpose: " - << show_bool(pdf.allowExtractAll()) << std::endl; - std::cout << "print low resolution: " - << show_bool(pdf.allowPrintLowRes()) << std::endl; - std::cout << "print high resolution: " - << show_bool(pdf.allowPrintHighRes()) << std::endl; - std::cout << "modify document assembly: " - << show_bool(pdf.allowModifyAssembly()) << std::endl; - std::cout << "modify forms: " - << show_bool(pdf.allowModifyForm()) << std::endl; - std::cout << "modify annotations: " - << show_bool(pdf.allowModifyAnnotation()) << std::endl; - std::cout << "modify other: " - << show_bool(pdf.allowModifyOther()) << std::endl; - std::cout << "modify anything: " + std::cout << "User password = " << user_password << std::endl + << "extract for accessibility: " + << show_bool(pdf.allowAccessibility()) << std::endl + << "extract for any purpose: " + << show_bool(pdf.allowExtractAll()) << std::endl + << "print low resolution: " + << show_bool(pdf.allowPrintLowRes()) << std::endl + << "print high resolution: " + << show_bool(pdf.allowPrintHighRes()) << std::endl + << "modify document assembly: " + << show_bool(pdf.allowModifyAssembly()) << std::endl + << "modify forms: " + << show_bool(pdf.allowModifyForm()) << std::endl + << "modify annotations: " + << show_bool(pdf.allowModifyAnnotation()) << std::endl + << "modify other: " + << show_bool(pdf.allowModifyOther()) << std::endl + << "modify anything: " << show_bool(pdf.allowModifyAll()) << std::endl; + if (V >= 4) + { + std::cout << "stream encryption method: " + << show_encryption_method(stream_method) << std::endl + << "string encryption method: " + << show_encryption_method(string_method) << std::endl + << "file encryption method: " + << show_encryption_method(file_method) << std::endl; + } } } @@ -579,6 +626,10 @@ int main(int argc, char* argv[]) bool linearize = false; bool decrypt = false; + bool copy_encryption = false; + char const* encryption_file = 0; + char const* encryption_file_password = ""; + bool encrypt = false; std::string user_password; std::string owner_password; @@ -664,11 +715,36 @@ int main(int argc, char* argv[]) r3_accessibility, r3_extract, r3_print, r3_modify, force_V4, cleartext_metadata, use_aes); encrypt = true; + decrypt = false; + copy_encryption = false; } else if (strcmp(arg, "decrypt") == 0) { decrypt = true; + encrypt = false; + copy_encryption = false; } + else if (strcmp(arg, "copy-encryption") == 0) + { + if (parameter == 0) + { + usage("--copy-encryption must be given as" + "--copy_encryption=file"); + } + encryption_file = parameter; + copy_encryption = true; + encrypt = false; + decrypt = false; + } + else if (strcmp(arg, "encryption-file-password") == 0) + { + if (parameter == 0) + { + usage("--encryption-file-password must be given as" + "--encryption-file-password=password"); + } + encryption_file_password = parameter; + } else if (strcmp(arg, "stream-data") == 0) { if (parameter == 0) @@ -865,6 +941,7 @@ int main(int argc, char* argv[]) try { QPDF pdf; + QPDF encryption_pdf; if (ignore_xref_streams) { pdf.setIgnoreXRefStreams(true); @@ -1082,6 +1159,12 @@ int main(int argc, char* argv[]) { w.setSuppressOriginalObjectIDs(true); } + if (copy_encryption) + { + encryption_pdf.processFile( + encryption_file, encryption_file_password); + w.copyEncryptionParameters(encryption_pdf); + } if (encrypt) { if (keylen == 40) diff --git a/qpdf/qtest/qpdf.test b/qpdf/qtest/qpdf.test index 8687d713..1b979724 100644 --- a/qpdf/qtest/qpdf.test +++ b/qpdf/qtest/qpdf.test @@ -1271,7 +1271,7 @@ $td->runtest("linearize and encrypt file", $td->EXIT_STATUS => 0}); $td->runtest("check encryption", {$td->COMMAND => "qpdf --show-encryption --password=owner a.pdf", - $td->FILTER => "grep -v allowed"}, + $td->FILTER => "grep -v allowed | grep -v method"}, {$td->STRING => "R = 4\nP = -4\nUser password = user\n", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); @@ -1290,7 +1290,7 @@ $td->runtest("encrypt with AES", {$td->STRING => "", $td->EXIT_STATUS => 0}); $td->runtest("check encryption", {$td->COMMAND => "qpdf --show-encryption a.pdf", - $td->FILTER => "grep -v allowed"}, + $td->FILTER => "grep -v allowed | grep -v method"}, {$td->STRING => "R = 4\nP = -4\nUser password = \n", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); @@ -1311,7 +1311,7 @@ $td->runtest("linearize with AES and object streams", {$td->STRING => "", $td->EXIT_STATUS => 0}); $td->runtest("check encryption", {$td->COMMAND => "qpdf --show-encryption a.pdf", - $td->FILTER => "grep -v allowed"}, + $td->FILTER => "grep -v allowed | grep -v method"}, {$td->STRING => "R = 4\nP = -4\nUser password = \n", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); @@ -1345,7 +1345,7 @@ $td->runtest("make sure there is no xref stream", $td->NORMALIZE_NEWLINES); # Look at some actual V4 files -$n_tests += 10; +$n_tests += 14; foreach my $d (['--force-V4', 'V4'], ['--cleartext-metadata', 'V4-clearmeta'], ['--use-aes=y', 'V4-aes'], @@ -1359,6 +1359,10 @@ foreach my $d (['--force-V4', 'V4'], $td->runtest("check output", {$td->FILE => "a.pdf"}, {$td->FILE => "$out.pdf"}); + $td->runtest("show encryption", + {$td->COMMAND => "qpdf --show-encryption a.pdf"}, + {$td->FILE => "$out-encryption.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); } # Crypt Filter $td->runtest("decrypt with crypt filter", @@ -1370,7 +1374,11 @@ $td->runtest("check output", {$td->FILE => 'decrypted-crypt-filter.pdf'}); # Copy encryption parameters -$n_tests += 3; +$n_tests += 10; +$td->runtest("create reference qdf", + {$td->COMMAND => + "qpdf --qdf --no-original-object-ids minimal.pdf a.qdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}); $td->runtest("create encrypted file", {$td->COMMAND => "qpdf --encrypt user owner 128 --use-aes=y --extract=n --" . @@ -1380,11 +1388,42 @@ $td->runtest("copy encryption parameters", {$td->COMMAND => "test_driver 30 minimal.pdf a.pdf"}, {$td->STRING => "test 30 done\n", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); -$td->runtest("checkout encryption", +$td->runtest("check output encryption", {$td->COMMAND => "qpdf --show-encryption b.pdf --password=owner"}, {$td->FILE => "copied-encryption.out", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); +$td->runtest("convert to qdf", + {$td->COMMAND => + "qpdf --qdf b.pdf b.qdf" . + " --password=owner --no-original-object-ids"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}); +$td->runtest("compare qdf", + {$td->COMMAND => "./diff-ignore-ID-version a.qdf b.qdf"}, + {$td->STRING => "okay\n", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("copy encryption with qpdf", + {$td->COMMAND => + "qpdf --copy-encryption=a.pdf". + " --encryption-file-password=user" . + " minimal.pdf c.pdf"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("check output encryption", + {$td->COMMAND => "qpdf --show-encryption c.pdf --password=owner"}, + {$td->FILE => "copied-encryption.out", + $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +$td->runtest("convert to qdf", + {$td->COMMAND => + "qpdf --qdf c.pdf c.qdf" . + " --password=owner --no-original-object-ids"}, + {$td->STRING => "", $td->EXIT_STATUS => 0}); +$td->runtest("compare qdf", + {$td->COMMAND => "./diff-ignore-ID-version a.qdf c.qdf"}, + {$td->STRING => "okay\n", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); + show_ntests(); # ---------- @@ -1753,6 +1792,5 @@ sub get_md5_checksum sub cleanup { - system("rm -rf *.ps *.pnm a.pdf a.qdf b.pdf b.qdf c.pdf" . - " *.enc* tif1 tif2 tiff-cache"); + system("rm -rf *.ps *.pnm ?.pdf ?.qdf *.enc* tif1 tif2 tiff-cache"); } diff --git a/qpdf/qtest/qpdf/V4-aes-clearmeta-encryption.out b/qpdf/qtest/qpdf/V4-aes-clearmeta-encryption.out new file mode 100644 index 00000000..928818dc --- /dev/null +++ b/qpdf/qtest/qpdf/V4-aes-clearmeta-encryption.out @@ -0,0 +1,15 @@ +R = 4 +P = -4 +User password = +extract for accessibility: allowed +extract for any purpose: allowed +print low resolution: allowed +print high resolution: allowed +modify document assembly: allowed +modify forms: allowed +modify annotations: allowed +modify other: allowed +modify anything: allowed +stream encryption method: AESv2 +string encryption method: AESv2 +file encryption method: AESv2 diff --git a/qpdf/qtest/qpdf/V4-aes-encryption.out b/qpdf/qtest/qpdf/V4-aes-encryption.out new file mode 100644 index 00000000..928818dc --- /dev/null +++ b/qpdf/qtest/qpdf/V4-aes-encryption.out @@ -0,0 +1,15 @@ +R = 4 +P = -4 +User password = +extract for accessibility: allowed +extract for any purpose: allowed +print low resolution: allowed +print high resolution: allowed +modify document assembly: allowed +modify forms: allowed +modify annotations: allowed +modify other: allowed +modify anything: allowed +stream encryption method: AESv2 +string encryption method: AESv2 +file encryption method: AESv2 diff --git a/qpdf/qtest/qpdf/V4-clearmeta-encryption.out b/qpdf/qtest/qpdf/V4-clearmeta-encryption.out new file mode 100644 index 00000000..157fb8da --- /dev/null +++ b/qpdf/qtest/qpdf/V4-clearmeta-encryption.out @@ -0,0 +1,15 @@ +R = 4 +P = -4 +User password = +extract for accessibility: allowed +extract for any purpose: allowed +print low resolution: allowed +print high resolution: allowed +modify document assembly: allowed +modify forms: allowed +modify annotations: allowed +modify other: allowed +modify anything: allowed +stream encryption method: RC4 +string encryption method: RC4 +file encryption method: RC4 diff --git a/qpdf/qtest/qpdf/V4-encryption.out b/qpdf/qtest/qpdf/V4-encryption.out new file mode 100644 index 00000000..157fb8da --- /dev/null +++ b/qpdf/qtest/qpdf/V4-encryption.out @@ -0,0 +1,15 @@ +R = 4 +P = -4 +User password = +extract for accessibility: allowed +extract for any purpose: allowed +print low resolution: allowed +print high resolution: allowed +modify document assembly: allowed +modify forms: allowed +modify annotations: allowed +modify other: allowed +modify anything: allowed +stream encryption method: RC4 +string encryption method: RC4 +file encryption method: RC4 diff --git a/qpdf/qtest/qpdf/copied-encryption.out b/qpdf/qtest/qpdf/copied-encryption.out index 6db6fa63..cbec2305 100644 --- a/qpdf/qtest/qpdf/copied-encryption.out +++ b/qpdf/qtest/qpdf/copied-encryption.out @@ -10,3 +10,6 @@ modify forms: allowed modify annotations: allowed modify other: allowed modify anything: allowed +stream encryption method: AESv2 +string encryption method: AESv2 +file encryption method: AESv2 diff --git a/qpdf/qtest/qpdf/diff-ignore-ID-version b/qpdf/qtest/qpdf/diff-ignore-ID-version new file mode 100755 index 00000000..e6b33470 --- /dev/null +++ b/qpdf/qtest/qpdf/diff-ignore-ID-version @@ -0,0 +1,8 @@ +#!/bin/sh +lines=$(expr + $(diff $1 $2 | egrep '^[<>]' | \ + egrep -v '/ID' | egrep -v '%PDF-' | wc -l)) +if [ "$lines" = "0" ]; then + echo okay +else + diff -a -U 0 $1 $2 +fi diff --git a/qpdf/test_driver.cc b/qpdf/test_driver.cc index 518e5569..3d2f0dca 100644 --- a/qpdf/test_driver.cc +++ b/qpdf/test_driver.cc @@ -58,12 +58,17 @@ class Provider: public QPDFObjectHandle::StreamDataProvider bool bad_length; }; -static void checkPageContents(QPDFObjectHandle page, - std::string const& wanted_string) +static std::string getPageContents(QPDFObjectHandle page) { PointerHolder b1 = page.getKey("/Contents").getStreamData(); - std::string contents = std::string((char *)(b1->getBuffer())); + return std::string((char *)(b1->getBuffer()), b1->getSize()) + "\0"; +} + +static void checkPageContents(QPDFObjectHandle page, + std::string const& wanted_string) +{ + std::string contents = getPageContents(page); if (contents.find(wanted_string) == std::string::npos) { std::cout << "didn't find " << wanted_string << " in " @@ -1030,10 +1035,24 @@ void runtest(int n, char const* filename1, char const* filename2) QPDF encrypted; encrypted.processFile(filename2, "user"); QPDFWriter w(pdf, "b.pdf"); - w.setStaticID(true); w.setStreamDataMode(qpdf_s_preserve); w.copyEncryptionParameters(encrypted); w.write(); + + // Make sure the contents are actually the same + QPDF final; + final.processFile("b.pdf", "user"); + std::vector pages = pdf.getAllPages(); + std::string orig_contents = getPageContents(pages[0]); + pages = final.getAllPages(); + std::string new_contents = getPageContents(pages[0]); + if (orig_contents != new_contents) + { + std::cout << "oops -- page contents don't match" << std::endl + << "original:\n" << orig_contents + << "new:\n" << new_contents + << std::endl; + } } else {