Commit a101533e0a76a0890a2ad3ea5fcbb578da75a6b4

Authored by Jay Berkenbilt
1 parent b26ce88e

Add command line option to copy encryption from other file

Add --copy-encryption and --encryption-file-password options to qpdf.
Also strengthen test suite for copying encryption.  The strengthened
test suite would have caught the failure to preserve AES and the
failure to update the file version, which was invalidating the
encrypted data.
ChangeLog
1 1 2012-07-15 Jay Berkenbilt <ejb@ql.org>
2 2  
  3 + * add new QPDF::isEncrypted method that returns some additional
  4 + information beyond other versions.
  5 +
  6 + * libqpdf/QPDFWriter.cc: fix copyEncryptionParameters to fix the
  7 + minimum PDF version based on other file's encryption needs. This
  8 + is a fix to code added on 2012-07-14 and did not impact previously
  9 + released code.
  10 +
3 11 * libqpdf/QPDFWriter.cc (copyEncryptionParameters): Bug fix: qpdf
4 12 was not preserving whether or not AES encryption was being used
5   - when copying encryption parameters.
  13 + when copying encryption parameters. The file would still have
  14 + been properly encrypted, but a file that started off encrypted
  15 + with AES could have become encrypted with RC4.
6 16  
7 17 2012-07-14 Jay Berkenbilt <ejb@ql.org>
8 18  
... ...
... ... @@ -64,9 +64,11 @@ Next
64 64 - Tests through qpdf command line: copy pages from multiple PDFs
65 65 starting with one PDF and also starting with empty.
66 66  
67   - * qpdf commandline: provide an option to copy encryption parameters
68   - from another file, specifying file and password. Search for "Copy
69   - encryption parameters" in qpdf.test.
  67 + * Document --copy-encryption and --encryption-file-password in
  68 + manual. Mention that the first half of /ID as well as all the
  69 + encryption parameters are copied. Maybe mention about StrF and
  70 + StrM with respect to AES here and also with encryption
  71 + preservation.
70 72  
71 73  
72 74 Soon
... ...
include/qpdf/QPDF.hh
... ... @@ -248,6 +248,12 @@ class QPDF
248 248 QPDF_DLL
249 249 bool isEncrypted(int& R, int& P);
250 250  
  251 + QPDF_DLL
  252 + bool isEncrypted(int& R, int& P, int& V,
  253 + encryption_method_e& stream_method,
  254 + encryption_method_e& string_method,
  255 + encryption_method_e& file_method);
  256 +
251 257 // Encryption permissions -- not enforced by QPDF
252 258 QPDF_DLL
253 259 bool allowAccessibility();
... ...
libqpdf/QPDF_encryption.cc
... ... @@ -744,14 +744,30 @@ QPDF::isEncrypted() const
744 744 bool
745 745 QPDF::isEncrypted(int& R, int& P)
746 746 {
  747 + int V;
  748 + encryption_method_e stream, string, file;
  749 + return isEncrypted(R, P, V, stream, string, file);
  750 +}
  751 +
  752 +bool
  753 +QPDF::isEncrypted(int& R, int& P, int& V,
  754 + encryption_method_e& stream_method,
  755 + encryption_method_e& string_method,
  756 + encryption_method_e& file_method)
  757 +{
747 758 if (this->encrypted)
748 759 {
749 760 QPDFObjectHandle trailer = getTrailer();
750 761 QPDFObjectHandle encrypt = trailer.getKey("/Encrypt");
751 762 QPDFObjectHandle Pkey = encrypt.getKey("/P");
752 763 QPDFObjectHandle Rkey = encrypt.getKey("/R");
  764 + QPDFObjectHandle Vkey = encrypt.getKey("/V");
753 765 P = Pkey.getIntValue();
754 766 R = Rkey.getIntValue();
  767 + V = Vkey.getIntValue();
  768 + stream_method = this->cf_stream;
  769 + string_method = this->cf_stream;
  770 + file_method = this->cf_file;
755 771 return true;
756 772 }
757 773 else
... ...
qpdf/qpdf.cc
... ... @@ -33,18 +33,29 @@ Usage: qpdf [ options ] infilename [ outfilename ]\n\
33 33 \n\
34 34 An option summary appears below. Please see the documentation for details.\n\
35 35 \n\
  36 +Note that when contradictory options are provided, whichever options are\n\
  37 +provided last take precedence.\n\
  38 +\n\
36 39 \n\
37 40 Basic Options\n\
38 41 -------------\n\
39 42 \n\
40 43 --password=password specify a password for accessing encrypted files\n\
41 44 --linearize generated a linearized (web optimized) file\n\
  45 +--copy-encryption=file copy encryption parameters from specified file\n\
  46 +--encryption-file-password=password\n\
  47 + password used to open the file from which encryption\n\
  48 + parameters are being copied\n\
42 49 --encrypt options -- generate an encrypted file\n\
43 50 --decrypt remove any encryption on the file\n\
44 51 \n\
45   -If neither --encrypt or --decrypt are given, qpdf will preserve any\n\
46   -encryption data associated with a file.\n\
  52 +If none of --copy-encryption, --encrypt or --decrypt are given, qpdf will\n\
  53 +preserve any encryption data associated with a file.\n\
47 54 \n\
  55 +Note that when copying encryption parameters from another file, all\n\
  56 +parameters will be copied, including both user and owner passwords, even\n\
  57 +if the user password is used to open the other file. This works even if\n\
  58 +the owner password is not known.\n\
48 59 \n\
49 60 Encryption Options\n\
50 61 ------------------\n\
... ... @@ -192,12 +203,39 @@ static std::string show_bool(bool v)
192 203 return v ? "allowed" : "not allowed";
193 204 }
194 205  
  206 +static std::string show_encryption_method(QPDF::encryption_method_e method)
  207 +{
  208 + std::string result = "unknown";
  209 + switch (method)
  210 + {
  211 + case QPDF::e_none:
  212 + result = "none";
  213 + break;
  214 + case QPDF::e_unknown:
  215 + result = "unknown";
  216 + break;
  217 + case QPDF::e_rc4:
  218 + result = "RC4";
  219 + break;
  220 + case QPDF::e_aes:
  221 + result = "AESv2";
  222 + break;
  223 + // no default so gcc will warn for missing case
  224 + }
  225 + return result;
  226 +}
  227 +
195 228 static void show_encryption(QPDF& pdf)
196 229 {
197 230 // Extract /P from /Encrypt
198 231 int R = 0;
199 232 int P = 0;
200   - if (! pdf.isEncrypted(R, P))
  233 + int V = 0;
  234 + QPDF::encryption_method_e stream_method = QPDF::e_unknown;
  235 + QPDF::encryption_method_e string_method = QPDF::e_unknown;
  236 + QPDF::encryption_method_e file_method = QPDF::e_unknown;
  237 + if (! pdf.isEncrypted(R, P, V,
  238 + stream_method, string_method, file_method))
201 239 {
202 240 std::cout << "File is not encrypted" << std::endl;
203 241 }
... ... @@ -206,25 +244,34 @@ static void show_encryption(QPDF&amp; pdf)
206 244 std::cout << "R = " << R << std::endl;
207 245 std::cout << "P = " << P << std::endl;
208 246 std::string user_password = pdf.getTrimmedUserPassword();
209   - std::cout << "User password = " << user_password << std::endl;
210   - std::cout << "extract for accessibility: "
211   - << show_bool(pdf.allowAccessibility()) << std::endl;
212   - std::cout << "extract for any purpose: "
213   - << show_bool(pdf.allowExtractAll()) << std::endl;
214   - std::cout << "print low resolution: "
215   - << show_bool(pdf.allowPrintLowRes()) << std::endl;
216   - std::cout << "print high resolution: "
217   - << show_bool(pdf.allowPrintHighRes()) << std::endl;
218   - std::cout << "modify document assembly: "
219   - << show_bool(pdf.allowModifyAssembly()) << std::endl;
220   - std::cout << "modify forms: "
221   - << show_bool(pdf.allowModifyForm()) << std::endl;
222   - std::cout << "modify annotations: "
223   - << show_bool(pdf.allowModifyAnnotation()) << std::endl;
224   - std::cout << "modify other: "
225   - << show_bool(pdf.allowModifyOther()) << std::endl;
226   - std::cout << "modify anything: "
  247 + std::cout << "User password = " << user_password << std::endl
  248 + << "extract for accessibility: "
  249 + << show_bool(pdf.allowAccessibility()) << std::endl
  250 + << "extract for any purpose: "
  251 + << show_bool(pdf.allowExtractAll()) << std::endl
  252 + << "print low resolution: "
  253 + << show_bool(pdf.allowPrintLowRes()) << std::endl
  254 + << "print high resolution: "
  255 + << show_bool(pdf.allowPrintHighRes()) << std::endl
  256 + << "modify document assembly: "
  257 + << show_bool(pdf.allowModifyAssembly()) << std::endl
  258 + << "modify forms: "
  259 + << show_bool(pdf.allowModifyForm()) << std::endl
  260 + << "modify annotations: "
  261 + << show_bool(pdf.allowModifyAnnotation()) << std::endl
  262 + << "modify other: "
  263 + << show_bool(pdf.allowModifyOther()) << std::endl
  264 + << "modify anything: "
227 265 << show_bool(pdf.allowModifyAll()) << std::endl;
  266 + if (V >= 4)
  267 + {
  268 + std::cout << "stream encryption method: "
  269 + << show_encryption_method(stream_method) << std::endl
  270 + << "string encryption method: "
  271 + << show_encryption_method(string_method) << std::endl
  272 + << "file encryption method: "
  273 + << show_encryption_method(file_method) << std::endl;
  274 + }
228 275 }
229 276 }
230 277  
... ... @@ -579,6 +626,10 @@ int main(int argc, char* argv[])
579 626 bool linearize = false;
580 627 bool decrypt = false;
581 628  
  629 + bool copy_encryption = false;
  630 + char const* encryption_file = 0;
  631 + char const* encryption_file_password = "";
  632 +
582 633 bool encrypt = false;
583 634 std::string user_password;
584 635 std::string owner_password;
... ... @@ -664,11 +715,36 @@ int main(int argc, char* argv[])
664 715 r3_accessibility, r3_extract, r3_print, r3_modify,
665 716 force_V4, cleartext_metadata, use_aes);
666 717 encrypt = true;
  718 + decrypt = false;
  719 + copy_encryption = false;
667 720 }
668 721 else if (strcmp(arg, "decrypt") == 0)
669 722 {
670 723 decrypt = true;
  724 + encrypt = false;
  725 + copy_encryption = false;
671 726 }
  727 + else if (strcmp(arg, "copy-encryption") == 0)
  728 + {
  729 + if (parameter == 0)
  730 + {
  731 + usage("--copy-encryption must be given as"
  732 + "--copy_encryption=file");
  733 + }
  734 + encryption_file = parameter;
  735 + copy_encryption = true;
  736 + encrypt = false;
  737 + decrypt = false;
  738 + }
  739 + else if (strcmp(arg, "encryption-file-password") == 0)
  740 + {
  741 + if (parameter == 0)
  742 + {
  743 + usage("--encryption-file-password must be given as"
  744 + "--encryption-file-password=password");
  745 + }
  746 + encryption_file_password = parameter;
  747 + }
672 748 else if (strcmp(arg, "stream-data") == 0)
673 749 {
674 750 if (parameter == 0)
... ... @@ -865,6 +941,7 @@ int main(int argc, char* argv[])
865 941 try
866 942 {
867 943 QPDF pdf;
  944 + QPDF encryption_pdf;
868 945 if (ignore_xref_streams)
869 946 {
870 947 pdf.setIgnoreXRefStreams(true);
... ... @@ -1082,6 +1159,12 @@ int main(int argc, char* argv[])
1082 1159 {
1083 1160 w.setSuppressOriginalObjectIDs(true);
1084 1161 }
  1162 + if (copy_encryption)
  1163 + {
  1164 + encryption_pdf.processFile(
  1165 + encryption_file, encryption_file_password);
  1166 + w.copyEncryptionParameters(encryption_pdf);
  1167 + }
1085 1168 if (encrypt)
1086 1169 {
1087 1170 if (keylen == 40)
... ...
qpdf/qtest/qpdf.test
... ... @@ -1271,7 +1271,7 @@ $td-&gt;runtest(&quot;linearize and encrypt file&quot;,
1271 1271 $td->EXIT_STATUS => 0});
1272 1272 $td->runtest("check encryption",
1273 1273 {$td->COMMAND => "qpdf --show-encryption --password=owner a.pdf",
1274   - $td->FILTER => "grep -v allowed"},
  1274 + $td->FILTER => "grep -v allowed | grep -v method"},
1275 1275 {$td->STRING => "R = 4\nP = -4\nUser password = user\n",
1276 1276 $td->EXIT_STATUS => 0},
1277 1277 $td->NORMALIZE_NEWLINES);
... ... @@ -1290,7 +1290,7 @@ $td-&gt;runtest(&quot;encrypt with AES&quot;,
1290 1290 {$td->STRING => "", $td->EXIT_STATUS => 0});
1291 1291 $td->runtest("check encryption",
1292 1292 {$td->COMMAND => "qpdf --show-encryption a.pdf",
1293   - $td->FILTER => "grep -v allowed"},
  1293 + $td->FILTER => "grep -v allowed | grep -v method"},
1294 1294 {$td->STRING => "R = 4\nP = -4\nUser password = \n",
1295 1295 $td->EXIT_STATUS => 0},
1296 1296 $td->NORMALIZE_NEWLINES);
... ... @@ -1311,7 +1311,7 @@ $td-&gt;runtest(&quot;linearize with AES and object streams&quot;,
1311 1311 {$td->STRING => "", $td->EXIT_STATUS => 0});
1312 1312 $td->runtest("check encryption",
1313 1313 {$td->COMMAND => "qpdf --show-encryption a.pdf",
1314   - $td->FILTER => "grep -v allowed"},
  1314 + $td->FILTER => "grep -v allowed | grep -v method"},
1315 1315 {$td->STRING => "R = 4\nP = -4\nUser password = \n",
1316 1316 $td->EXIT_STATUS => 0},
1317 1317 $td->NORMALIZE_NEWLINES);
... ... @@ -1345,7 +1345,7 @@ $td-&gt;runtest(&quot;make sure there is no xref stream&quot;,
1345 1345 $td->NORMALIZE_NEWLINES);
1346 1346  
1347 1347 # Look at some actual V4 files
1348   -$n_tests += 10;
  1348 +$n_tests += 14;
1349 1349 foreach my $d (['--force-V4', 'V4'],
1350 1350 ['--cleartext-metadata', 'V4-clearmeta'],
1351 1351 ['--use-aes=y', 'V4-aes'],
... ... @@ -1359,6 +1359,10 @@ foreach my $d ([&#39;--force-V4&#39;, &#39;V4&#39;],
1359 1359 $td->runtest("check output",
1360 1360 {$td->FILE => "a.pdf"},
1361 1361 {$td->FILE => "$out.pdf"});
  1362 + $td->runtest("show encryption",
  1363 + {$td->COMMAND => "qpdf --show-encryption a.pdf"},
  1364 + {$td->FILE => "$out-encryption.out", $td->EXIT_STATUS => 0},
  1365 + $td->NORMALIZE_NEWLINES);
1362 1366 }
1363 1367 # Crypt Filter
1364 1368 $td->runtest("decrypt with crypt filter",
... ... @@ -1370,7 +1374,11 @@ $td-&gt;runtest(&quot;check output&quot;,
1370 1374 {$td->FILE => 'decrypted-crypt-filter.pdf'});
1371 1375  
1372 1376 # Copy encryption parameters
1373   -$n_tests += 3;
  1377 +$n_tests += 10;
  1378 +$td->runtest("create reference qdf",
  1379 + {$td->COMMAND =>
  1380 + "qpdf --qdf --no-original-object-ids minimal.pdf a.qdf"},
  1381 + {$td->STRING => "", $td->EXIT_STATUS => 0});
1374 1382 $td->runtest("create encrypted file",
1375 1383 {$td->COMMAND =>
1376 1384 "qpdf --encrypt user owner 128 --use-aes=y --extract=n --" .
... ... @@ -1380,11 +1388,42 @@ $td-&gt;runtest(&quot;copy encryption parameters&quot;,
1380 1388 {$td->COMMAND => "test_driver 30 minimal.pdf a.pdf"},
1381 1389 {$td->STRING => "test 30 done\n", $td->EXIT_STATUS => 0},
1382 1390 $td->NORMALIZE_NEWLINES);
1383   -$td->runtest("checkout encryption",
  1391 +$td->runtest("check output encryption",
1384 1392 {$td->COMMAND => "qpdf --show-encryption b.pdf --password=owner"},
1385 1393 {$td->FILE => "copied-encryption.out",
1386 1394 $td->EXIT_STATUS => 0},
1387 1395 $td->NORMALIZE_NEWLINES);
  1396 +$td->runtest("convert to qdf",
  1397 + {$td->COMMAND =>
  1398 + "qpdf --qdf b.pdf b.qdf" .
  1399 + " --password=owner --no-original-object-ids"},
  1400 + {$td->STRING => "", $td->EXIT_STATUS => 0});
  1401 +$td->runtest("compare qdf",
  1402 + {$td->COMMAND => "./diff-ignore-ID-version a.qdf b.qdf"},
  1403 + {$td->STRING => "okay\n", $td->EXIT_STATUS => 0},
  1404 + $td->NORMALIZE_NEWLINES);
  1405 +$td->runtest("copy encryption with qpdf",
  1406 + {$td->COMMAND =>
  1407 + "qpdf --copy-encryption=a.pdf".
  1408 + " --encryption-file-password=user" .
  1409 + " minimal.pdf c.pdf"},
  1410 + {$td->STRING => "", $td->EXIT_STATUS => 0},
  1411 + $td->NORMALIZE_NEWLINES);
  1412 +$td->runtest("check output encryption",
  1413 + {$td->COMMAND => "qpdf --show-encryption c.pdf --password=owner"},
  1414 + {$td->FILE => "copied-encryption.out",
  1415 + $td->EXIT_STATUS => 0},
  1416 + $td->NORMALIZE_NEWLINES);
  1417 +$td->runtest("convert to qdf",
  1418 + {$td->COMMAND =>
  1419 + "qpdf --qdf c.pdf c.qdf" .
  1420 + " --password=owner --no-original-object-ids"},
  1421 + {$td->STRING => "", $td->EXIT_STATUS => 0});
  1422 +$td->runtest("compare qdf",
  1423 + {$td->COMMAND => "./diff-ignore-ID-version a.qdf c.qdf"},
  1424 + {$td->STRING => "okay\n", $td->EXIT_STATUS => 0},
  1425 + $td->NORMALIZE_NEWLINES);
  1426 +
1388 1427  
1389 1428 show_ntests();
1390 1429 # ----------
... ... @@ -1753,6 +1792,5 @@ sub get_md5_checksum
1753 1792  
1754 1793 sub cleanup
1755 1794 {
1756   - system("rm -rf *.ps *.pnm a.pdf a.qdf b.pdf b.qdf c.pdf" .
1757   - " *.enc* tif1 tif2 tiff-cache");
  1795 + system("rm -rf *.ps *.pnm ?.pdf ?.qdf *.enc* tif1 tif2 tiff-cache");
1758 1796 }
... ...
qpdf/qtest/qpdf/V4-aes-clearmeta-encryption.out 0 โ†’ 100644
  1 +R = 4
  2 +P = -4
  3 +User password =
  4 +extract for accessibility: allowed
  5 +extract for any purpose: allowed
  6 +print low resolution: allowed
  7 +print high resolution: allowed
  8 +modify document assembly: allowed
  9 +modify forms: allowed
  10 +modify annotations: allowed
  11 +modify other: allowed
  12 +modify anything: allowed
  13 +stream encryption method: AESv2
  14 +string encryption method: AESv2
  15 +file encryption method: AESv2
... ...
qpdf/qtest/qpdf/V4-aes-encryption.out 0 โ†’ 100644
  1 +R = 4
  2 +P = -4
  3 +User password =
  4 +extract for accessibility: allowed
  5 +extract for any purpose: allowed
  6 +print low resolution: allowed
  7 +print high resolution: allowed
  8 +modify document assembly: allowed
  9 +modify forms: allowed
  10 +modify annotations: allowed
  11 +modify other: allowed
  12 +modify anything: allowed
  13 +stream encryption method: AESv2
  14 +string encryption method: AESv2
  15 +file encryption method: AESv2
... ...
qpdf/qtest/qpdf/V4-clearmeta-encryption.out 0 โ†’ 100644
  1 +R = 4
  2 +P = -4
  3 +User password =
  4 +extract for accessibility: allowed
  5 +extract for any purpose: allowed
  6 +print low resolution: allowed
  7 +print high resolution: allowed
  8 +modify document assembly: allowed
  9 +modify forms: allowed
  10 +modify annotations: allowed
  11 +modify other: allowed
  12 +modify anything: allowed
  13 +stream encryption method: RC4
  14 +string encryption method: RC4
  15 +file encryption method: RC4
... ...
qpdf/qtest/qpdf/V4-encryption.out 0 โ†’ 100644
  1 +R = 4
  2 +P = -4
  3 +User password =
  4 +extract for accessibility: allowed
  5 +extract for any purpose: allowed
  6 +print low resolution: allowed
  7 +print high resolution: allowed
  8 +modify document assembly: allowed
  9 +modify forms: allowed
  10 +modify annotations: allowed
  11 +modify other: allowed
  12 +modify anything: allowed
  13 +stream encryption method: RC4
  14 +string encryption method: RC4
  15 +file encryption method: RC4
... ...
qpdf/qtest/qpdf/copied-encryption.out
... ... @@ -10,3 +10,6 @@ modify forms: allowed
10 10 modify annotations: allowed
11 11 modify other: allowed
12 12 modify anything: allowed
  13 +stream encryption method: AESv2
  14 +string encryption method: AESv2
  15 +file encryption method: AESv2
... ...
qpdf/qtest/qpdf/diff-ignore-ID-version 0 โ†’ 100755
  1 +#!/bin/sh
  2 +lines=$(expr + $(diff $1 $2 | egrep '^[<>]' | \
  3 + egrep -v '/ID' | egrep -v '%PDF-' | wc -l))
  4 +if [ "$lines" = "0" ]; then
  5 + echo okay
  6 +else
  7 + diff -a -U 0 $1 $2
  8 +fi
... ...
qpdf/test_driver.cc
... ... @@ -58,12 +58,17 @@ class Provider: public QPDFObjectHandle::StreamDataProvider
58 58 bool bad_length;
59 59 };
60 60  
61   -static void checkPageContents(QPDFObjectHandle page,
62   - std::string const& wanted_string)
  61 +static std::string getPageContents(QPDFObjectHandle page)
63 62 {
64 63 PointerHolder<Buffer> b1 =
65 64 page.getKey("/Contents").getStreamData();
66   - std::string contents = std::string((char *)(b1->getBuffer()));
  65 + return std::string((char *)(b1->getBuffer()), b1->getSize()) + "\0";
  66 +}
  67 +
  68 +static void checkPageContents(QPDFObjectHandle page,
  69 + std::string const& wanted_string)
  70 +{
  71 + std::string contents = getPageContents(page);
67 72 if (contents.find(wanted_string) == std::string::npos)
68 73 {
69 74 std::cout << "didn't find " << wanted_string << " in "
... ... @@ -1030,10 +1035,24 @@ void runtest(int n, char const* filename1, char const* filename2)
1030 1035 QPDF encrypted;
1031 1036 encrypted.processFile(filename2, "user");
1032 1037 QPDFWriter w(pdf, "b.pdf");
1033   - w.setStaticID(true);
1034 1038 w.setStreamDataMode(qpdf_s_preserve);
1035 1039 w.copyEncryptionParameters(encrypted);
1036 1040 w.write();
  1041 +
  1042 + // Make sure the contents are actually the same
  1043 + QPDF final;
  1044 + final.processFile("b.pdf", "user");
  1045 + std::vector<QPDFObjectHandle> pages = pdf.getAllPages();
  1046 + std::string orig_contents = getPageContents(pages[0]);
  1047 + pages = final.getAllPages();
  1048 + std::string new_contents = getPageContents(pages[0]);
  1049 + if (orig_contents != new_contents)
  1050 + {
  1051 + std::cout << "oops -- page contents don't match" << std::endl
  1052 + << "original:\n" << orig_contents
  1053 + << "new:\n" << new_contents
  1054 + << std::endl;
  1055 + }
1037 1056 }
1038 1057 else
1039 1058 {
... ...