Commit 94c79bb8f65e2a13c7bbe03437d2c8354068acb6

Authored by Jay Berkenbilt
1 parent 7e078971

Support --show-encryption without a valid password (fixes #598)

ChangeLog
  1 +2022-09-06 Jay Berkenbilt <ejb@ql.org>
  2 +
  3 + * The --show-encryption option now works even if a correct
  4 + password is not supplied. If you were using --show-encryption to
  5 + test whether you have the right password, use --requires-password
  6 + instead. Fixes #598.
  7 +
1 8 2022-09-05 Jay Berkenbilt <ejb@ql.org>
2 9  
3 10 * Add a move constructor to Buffer, making it possible to move
... ...
include/qpdf/QPDFJob.hh
... ... @@ -490,22 +490,26 @@ class QPDFJob
490 490 std::vector<int> parseNumrange(char const* range, int max);
491 491  
492 492 // Basic file processing
493   - std::shared_ptr<QPDF> processFile(
  493 + void processFile(
  494 + std::shared_ptr<QPDF>&,
494 495 char const* filename,
495 496 char const* password,
496 497 bool used_for_input,
497 498 bool main_input);
498   - std::shared_ptr<QPDF> processInputSource(
  499 + void processInputSource(
  500 + std::shared_ptr<QPDF>&,
499 501 std::shared_ptr<InputSource> is,
500 502 char const* password,
501 503 bool used_for_input);
502   - std::shared_ptr<QPDF> doProcess(
  504 + void doProcess(
  505 + std::shared_ptr<QPDF>&,
503 506 std::function<void(QPDF*, char const*)> fn,
504 507 char const* password,
505 508 bool empty,
506 509 bool used_for_input,
507 510 bool main_input);
508   - std::shared_ptr<QPDF> doProcessOnce(
  511 + void doProcessOnce(
  512 + std::shared_ptr<QPDF>&,
509 513 std::function<void(QPDF*, char const*)> fn,
510 514 char const* password,
511 515 bool empty,
... ...
libqpdf/QPDFJob.cc
... ... @@ -564,22 +564,27 @@ void
564 564 QPDFJob::run()
565 565 {
566 566 checkConfiguration();
567   - std::shared_ptr<QPDF> pdf_ph;
  567 + std::shared_ptr<QPDF> pdf_sp;
568 568 try {
569   - pdf_ph =
570   - processFile(m->infilename.get(), m->password.get(), true, true);
  569 + processFile(pdf_sp, m->infilename.get(), m->password.get(), true, true);
571 570 } catch (QPDFExc& e) {
572   - if ((e.getErrorCode() == qpdf_e_password) &&
573   - (m->check_is_encrypted || m->check_requires_password)) {
574   - // Allow --is-encrypted and --requires-password to
575   - // work when an incorrect password is supplied.
576   - this->m->encryption_status =
577   - qpdf_es_encrypted | qpdf_es_password_incorrect;
578   - return;
  571 + if (e.getErrorCode() == qpdf_e_password) {
  572 + // Allow certain operations to work when an incorrect
  573 + // password is supplied.
  574 + if (m->check_is_encrypted || m->check_requires_password) {
  575 + this->m->encryption_status =
  576 + qpdf_es_encrypted | qpdf_es_password_incorrect;
  577 + return;
  578 + }
  579 + if (m->show_encryption && pdf_sp) {
  580 + this->m->log->info("Incorrect password supplied\n");
  581 + showEncryption(*pdf_sp);
  582 + return;
  583 + }
579 584 }
580 585 throw e;
581 586 }
582   - QPDF& pdf = *pdf_ph;
  587 + QPDF& pdf = *pdf_sp;
583 588 if (pdf.isEncrypted()) {
584 589 this->m->encryption_status = qpdf_es_encrypted;
585 590 }
... ... @@ -1981,15 +1986,16 @@ QPDFJob::doInspection(QPDF&amp; pdf)
1981 1986 }
1982 1987 }
1983 1988  
1984   -std::shared_ptr<QPDF>
  1989 +void
1985 1990 QPDFJob::doProcessOnce(
  1991 + std::shared_ptr<QPDF>& pdf,
1986 1992 std::function<void(QPDF*, char const*)> fn,
1987 1993 char const* password,
1988 1994 bool empty,
1989 1995 bool used_for_input,
1990 1996 bool main_input)
1991 1997 {
1992   - auto pdf = QPDF::create();
  1998 + pdf = QPDF::create();
1993 1999 setQPDFOptions(*pdf);
1994 2000 if (empty) {
1995 2001 pdf->emptyPDF();
... ... @@ -2002,11 +2008,11 @@ QPDFJob::doProcessOnce(
2002 2008 this->m->max_input_version.updateIfGreater(
2003 2009 pdf->getVersionAsPDFVersion());
2004 2010 }
2005   - return pdf;
2006 2011 }
2007 2012  
2008   -std::shared_ptr<QPDF>
  2013 +void
2009 2014 QPDFJob::doProcess(
  2015 + std::shared_ptr<QPDF>& pdf,
2010 2016 std::function<void(QPDF*, char const*)> fn,
2011 2017 char const* password,
2012 2018 bool empty,
... ... @@ -2037,7 +2043,8 @@ QPDFJob::doProcess(
2037 2043 m->suppress_password_recovery) {
2038 2044 // There is no password, or we're not doing recovery, so just
2039 2045 // do the normal processing with the supplied password.
2040   - return doProcessOnce(fn, password, empty, used_for_input, main_input);
  2046 + doProcessOnce(pdf, fn, password, empty, used_for_input, main_input);
  2047 + return;
2041 2048 }
2042 2049  
2043 2050 // Get a list of otherwise encoded strings. Keep in scope for this
... ... @@ -2065,7 +2072,8 @@ QPDFJob::doProcess(
2065 2072 bool warned = false;
2066 2073 for (auto iter = passwords.begin(); iter != passwords.end(); ++iter) {
2067 2074 try {
2068   - return doProcessOnce(fn, *iter, empty, used_for_input, main_input);
  2075 + doProcessOnce(pdf, fn, *iter, empty, used_for_input, main_input);
  2076 + return;
2069 2077 } catch (QPDFExc& e) {
2070 2078 auto next = iter;
2071 2079 ++next;
... ... @@ -2086,8 +2094,9 @@ QPDFJob::doProcess(
2086 2094 throw std::logic_error("do_process returned");
2087 2095 }
2088 2096  
2089   -std::shared_ptr<QPDF>
  2097 +void
2090 2098 QPDFJob::processFile(
  2099 + std::shared_ptr<QPDF>& pdf,
2091 2100 char const* filename,
2092 2101 char const* password,
2093 2102 bool used_for_input,
... ... @@ -2096,17 +2105,25 @@ QPDFJob::processFile(
2096 2105 auto f1 = std::mem_fn<void(char const*, char const*)>(&QPDF::processFile);
2097 2106 auto fn =
2098 2107 std::bind(f1, std::placeholders::_1, filename, std::placeholders::_2);
2099   - return doProcess(
2100   - fn, password, strcmp(filename, "") == 0, used_for_input, main_input);
  2108 + doProcess(
  2109 + pdf,
  2110 + fn,
  2111 + password,
  2112 + strcmp(filename, "") == 0,
  2113 + used_for_input,
  2114 + main_input);
2101 2115 }
2102 2116  
2103   -std::shared_ptr<QPDF>
  2117 +void
2104 2118 QPDFJob::processInputSource(
2105   - std::shared_ptr<InputSource> is, char const* password, bool used_for_input)
  2119 + std::shared_ptr<QPDF>& pdf,
  2120 + std::shared_ptr<InputSource> is,
  2121 + char const* password,
  2122 + bool used_for_input)
2106 2123 {
2107 2124 auto f1 = std::mem_fn(&QPDF::processInputSource);
2108 2125 auto fn = std::bind(f1, std::placeholders::_1, is, std::placeholders::_2);
2109   - return doProcess(fn, password, false, used_for_input, false);
  2126 + doProcess(pdf, fn, password, false, used_for_input, false);
2110 2127 }
2111 2128  
2112 2129 void
... ... @@ -2117,8 +2134,7 @@ QPDFJob::validateUnderOverlay(QPDF&amp; pdf, UnderOverlay* uo)
2117 2134 }
2118 2135 QPDFPageDocumentHelper main_pdh(pdf);
2119 2136 int main_npages = QIntC::to_int(main_pdh.getAllPages().size());
2120   - uo->pdf =
2121   - processFile(uo->filename.c_str(), uo->password.get(), true, false);
  2137 + processFile(uo->pdf, uo->filename.c_str(), uo->password.get(), true, false);
2122 2138 QPDFPageDocumentHelper uo_pdh(*(uo->pdf));
2123 2139 int uo_npages = QIntC::to_int(uo_pdh.getAllPages().size());
2124 2140 try {
... ... @@ -2375,8 +2391,13 @@ QPDFJob::copyAttachments(QPDF&amp; pdf)
2375 2391 v << prefix << ": copying attachments from " << to_copy.path
2376 2392 << "\n";
2377 2393 });
2378   - auto other = processFile(
2379   - to_copy.path.c_str(), to_copy.password.c_str(), false, false);
  2394 + std::shared_ptr<QPDF> other;
  2395 + processFile(
  2396 + other,
  2397 + to_copy.path.c_str(),
  2398 + to_copy.password.c_str(),
  2399 + false,
  2400 + false);
2380 2401 QPDFEmbeddedFileDocumentHelper other_efdh(*other);
2381 2402 auto other_attachments = other_efdh.getEmbeddedFiles();
2382 2403 for (auto const& iter: other_attachments) {
... ... @@ -2702,10 +2723,10 @@ QPDFJob::handlePageSpecs(
2702 2723 new FileInputSource(page_spec.filename.c_str());
2703 2724 is = std::shared_ptr<InputSource>(fis);
2704 2725 }
2705   - std::shared_ptr<QPDF> qpdf_ph =
2706   - processInputSource(is, password, true);
2707   - page_heap.push_back(qpdf_ph);
2708   - page_spec_qpdfs[page_spec.filename] = qpdf_ph.get();
  2726 + std::shared_ptr<QPDF> qpdf_sp;
  2727 + processInputSource(qpdf_sp, is, password, true);
  2728 + page_heap.push_back(qpdf_sp);
  2729 + page_spec_qpdfs[page_spec.filename] = qpdf_sp.get();
2709 2730 if (cis) {
2710 2731 cis->stayOpen(false);
2711 2732 page_spec_cfis[page_spec.filename] = cis;
... ... @@ -3209,7 +3230,9 @@ QPDFJob::setWriterOptions(QPDF&amp; pdf, QPDFWriter&amp; w)
3209 3230 w.setSuppressOriginalObjectIDs(true);
3210 3231 }
3211 3232 if (m->copy_encryption) {
3212   - std::shared_ptr<QPDF> encryption_pdf = processFile(
  3233 + std::shared_ptr<QPDF> encryption_pdf;
  3234 + processFile(
  3235 + encryption_pdf,
3213 3236 m->encryption_file.c_str(),
3214 3237 m->encryption_file_password.get(),
3215 3238 false,
... ...
manual/release-notes.rst
... ... @@ -85,6 +85,11 @@ For a detailed list of changes, please see the file
85 85  
86 86 - CLI: breaking changes
87 87  
  88 + - The :qpdf:ref:`--show-encryption` flag now provides encryption
  89 + information even if a correct password is not supplied. If you
  90 + were relying on its not working in this case, see
  91 + :qpdf:ref:`--requires-password` for a reliable test.
  92 +
88 93 - The default json output version when :qpdf:ref:`--json` is
89 94 specified has been changed from ``1`` to ``latest``, which is
90 95 now ``2``.
... ...
qpdf/qtest/encryption.test
... ... @@ -112,7 +112,7 @@ my @encrypted_files =
112 112 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
113 113 );
114 114  
115   -$n_tests += 8 + (2 * (@encrypted_files)) + (7 * (@encrypted_files - 6)) + 9;
  115 +$n_tests += 8 + (2 * (@encrypted_files)) + (7 * (@encrypted_files - 6)) + 10;
116 116  
117 117 $td->runtest("encrypted file",
118 118 {$td->COMMAND => "test_driver 2 encrypted-with-images.pdf"},
... ... @@ -365,6 +365,12 @@ $td-&gt;runtest(&quot;C API: invalid password&quot;,
365 365 "qpdf-ctest 2 enc-R2,V1,U=view,O=view.pdf '' a.qdf"},
366 366 {$td->FILE => "c-invalid-password.out", $td->EXIT_STATUS => 0},
367 367 $td->NORMALIZE_NEWLINES);
  368 +$td->runtest("show-encryption works invalid password",
  369 + {$td->COMMAND => "qpdf --show-encryption --password=quack" .
  370 + " enc-R2,V1,U=view,O=view.pdf"},
  371 + {$td->FILE => "invalid-password-encrypt.out",
  372 + $td->EXIT_STATUS => 0},
  373 + $td->NORMALIZE_NEWLINES);
368 374  
369 375 my @cenc = (
370 376 [11, 'hybrid-xref.pdf', "''", 'r2', "", ""],
... ...
qpdf/qtest/qpdf/invalid-password-encrypt.out 0 → 100644
  1 +Incorrect password supplied
  2 +R = 2
  3 +P = -64
  4 +User password =
  5 +extract for accessibility: not allowed
  6 +extract for any purpose: not allowed
  7 +print low resolution: not allowed
  8 +print high resolution: not allowed
  9 +modify document assembly: not allowed
  10 +modify forms: not allowed
  11 +modify annotations: not allowed
  12 +modify other: not allowed
  13 +modify anything: not allowed
... ...
qpdf/qtest/unicode-password.test
... ... @@ -147,9 +147,11 @@ foreach my $d (@unicode_pw_cases)
147 147 }
148 148 my $r_output = "";
149 149 $r_output .= "trying other\n" if $tried_others;
  150 + my $arg = "--show-encryption";
150 151 if ($xfail)
151 152 {
152 153 $r_output .= "qpdf: a.pdf: invalid password\n";
  154 + $arg = "--check";
153 155 }
154 156 else
155 157 {
... ... @@ -162,7 +164,7 @@ foreach my $d (@unicode_pw_cases)
162 164 $r_xargs .= $strict ? ' --suppress-password-recovery' : '';
163 165 $td->runtest("decrypt $pw, $r_encoding, strict=$strict",
164 166 {$td->COMMAND =>
165   - "qpdf --show-encryption --verbose" .
  167 + "qpdf $arg --verbose" .
166 168 " $r_xargs a.pdf \@$r_pfile",
167 169 $td->FILTER => "perl show-unicode-encryption.pl"},
168 170 {$td->STRING => "$r_output",
... ...