From 392f2ece51e66dd4c92df3be7f91b637cb54c059 Mon Sep 17 00:00:00 2001 From: Jay Berkenbilt Date: Thu, 17 Jan 2019 19:44:18 -0500 Subject: [PATCH] Try passwords with different string encodings --- ChangeLog | 7 +++++ qpdf/qpdf.cc | 79 +++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/ChangeLog b/ChangeLog index 992cf507..4c1e62d8 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,12 @@ 2019-01-17 Jay Berkenbilt + * When attempting to open an encrypted file with a password, if + the password doesn't work, try alternative passwords created by + re-interpreting the supplied password with different string + encodings. This makes qpdf able to recover passwords with + non-ASCII characters when either the decryption or encryption + operation was performed with an incorrectly encoded password. + * Fix data loss bug: qpdf was discarding referenced resources in the case in which a page's resource dictionary contained an indirect reference for either /Font or /XObject that contained diff --git a/qpdf/qpdf.cc b/qpdf/qpdf.cc index 9ee2b423..999541fb 100644 --- a/qpdf/qpdf.cc +++ b/qpdf/qpdf.cc @@ -3671,7 +3671,7 @@ ImageOptimizer::provideStreamData(int, int, Pipeline* pipeline) } template -static PointerHolder do_process( +static PointerHolder do_process_once( void (QPDF::*fn)(T, char const*), T item, char const* password, Options& o, bool empty) @@ -3689,6 +3689,83 @@ static PointerHolder do_process( return pdf; } +template +static PointerHolder do_process( + void (QPDF::*fn)(T, char const*), + T item, char const* password, + Options& o, bool empty) +{ + // If a password has been specified but doesn't work, try other + // passwords that are equivalent in different character encodings. + // This makes it possible to open PDF files that were encrypted + // using incorrect string encodings. For example, if someone used + // a password encoded in PDF Doc encoding or Windows code page + // 1252 for an AES-encrypted file or a UTF-8-encoded password on + // an RC4-encrypted file, or if the password was properly encoded + // by the password given here was incorrectly encoded, there's a + // good chance we'd succeed here. + + if ((password == 0) || empty || o.password_is_hex_key) + { + // There is no password, so just do the normal processing. + return do_process_once(fn, item, password, o, empty); + } + + // Get a list of otherwise encoded strings. Keep in scope for this + // method. + std::vector passwords_str = + QUtil::possible_repaired_encodings(password); + // Represent to char const*, as required by the QPDF class. + std::vector passwords; + for (std::vector::iterator iter = passwords_str.begin(); + iter != passwords_str.end(); ++iter) + { + passwords.push_back((*iter).c_str()); + } + // We always try the supplied password first because it is the + // first string returned by possible_repaired_encodings. If there + // is more than one option, go ahead and put the supplied password + // at the end so that it's that decoding attempt whose exception + // is thrown. + if (passwords.size() > 1) + { + passwords.push_back(password); + } + + // Try each password. If one works, return the resulting object. + // If they all fail, throw the exception thrown by the final + // attempt, which, like the first attempt, will be with the + // supplied password. + bool warned = false; + for (std::vector::iterator iter = passwords.begin(); + iter != passwords.end(); ++iter) + { + try + { + return do_process_once(fn, item, *iter, o, empty); + } + catch (QPDFExc& e) + { + std::vector::iterator next = iter; + ++next; + if (next == passwords.end()) + { + throw e; + } + } + if ((! warned) && o.verbose) + { + warned = true; + std::cout << whoami << ": supplied password didn't work;" + << " trying other passwords based on interpreting" + << " password with different string encodings" + << std::endl; + } + } + // Should not be reachable + throw std::logic_error("do_process returned"); +} + static PointerHolder process_file(char const* filename, char const* password, Options& o)