diff --git a/ChangeLog b/ChangeLog index 8d93eaee..254c589e 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,13 @@ +2020-01-26 Jay Berkenbilt + + * Add options --is-encrypted and --requires-password. These can be + used with files, including encrypted files with unknown passwords, + to determine whether or not a file is encrypted and whether a + password is required to open the file. The --requires-password + option can also be used to determine whether a supplied password + is correct. Information is supplied through exit codes, making + these options particularly useful for shell scripts. Fixes #390. + 2020-01-14 Jay Berkenbilt * Fix for Windows being unable to acquire crypt context with a new diff --git a/manual/qpdf-manual.xml b/manual/qpdf-manual.xml index 317ce04a..3f455d83 100644 --- a/manual/qpdf-manual.xml +++ b/manual/qpdf-manual.xml @@ -665,6 +665,40 @@ make + + + + + Silently exit with status 0 if the file is encrypted or status + 2 if the file is not encrypted. This is useful for shell + scripts. Other options are ignored if this is given. This + option is mutually exclusive with + . Both this option and + exit with status 2 for + non-encrypted files. + + + + + + + + Silently exit with status 0 if a password (other than as + supplied) is required. Exit with status 2 if the file is not + encrypted. Exit with status 3 if the file is encrypted but + requires no password or the correct password has been + supplied. This is useful for shell scripts. Note that any + supplied password is used when opening the file. When used + with a option, this option can be + used to check the correctness of the password. In that case, + an exit status of 3 means the file works with the supplied + password. This option is mutually exclusive with + . Both this option and + exit with status 2 for + non-encrypted files. + + + @@ -4675,6 +4709,23 @@ print "\n"; + + + CLI Enhancements + + + + + Added options and + for testing whether a + file is encrypted or requires a password other than the + supplied (or empty) password. These communicate via exit + status, making them useful for shell scripts. They also work + on encrypted files with unknown passwords. + + + + diff --git a/qpdf/qpdf.cc b/qpdf/qpdf.cc index 2139d8b2..0ea32b8c 100644 --- a/qpdf/qpdf.cc +++ b/qpdf/qpdf.cc @@ -29,8 +29,12 @@ #include #include -static int const EXIT_ERROR = 2; -static int const EXIT_WARNING = 3; +static int constexpr EXIT_ERROR = 2; +static int constexpr EXIT_WARNING = 3; + +// For is-encrypted and requires-password +static int constexpr EXIT_IS_NOT_ENCRYPTED = 2; +static int constexpr EXIT_CORRECT_PASSWORD = 3; static char const* whoami = 0; @@ -183,6 +187,8 @@ struct Options under_overlay(0), require_outfile(true), replace_input(false), + check_is_encrypted(false), + check_requires_password(false), infilename(0), outfilename(0) { @@ -287,6 +293,8 @@ struct Options std::map rotations; bool require_outfile; bool replace_input; + bool check_is_encrypted; + bool check_requires_password; char const* infilename; char const* outfilename; }; @@ -718,6 +726,8 @@ class ArgParser void argUOpassword(char* parameter); void argEndUnderOverlay(); void argReplaceInput(); + void argIsEncrypted(); + void argRequiresPassword(); void usage(std::string const& message); void checkCompletion(); @@ -948,6 +958,8 @@ ArgParser::initOptionTable() (*t)["overlay"] = oe_bare(&ArgParser::argOverlay); (*t)["underlay"] = oe_bare(&ArgParser::argUnderlay); (*t)["replace-input"] = oe_bare(&ArgParser::argReplaceInput); + (*t)["is-encrypted"] = oe_bare(&ArgParser::argIsEncrypted); + (*t)["requires-password"] = oe_bare(&ArgParser::argRequiresPassword); t = &this->encrypt40_option_table; (*t)["--"] = oe_bare(&ArgParser::argEndEncrypt); @@ -1105,6 +1117,13 @@ ArgParser::argHelp() << "--completion-bash output a bash complete command you can eval\n" << "--completion-zsh output a zsh complete command you can eval\n" << "--password=password specify a password for accessing encrypted files\n" + << "--is-encrypted silently exit 0 if the file is encrypted or 2\n" + << " if not; useful for shell scripts\n" + << "--requires-password silently exit 0 if a password (other than as\n" + << " supplied) is required, 2 if the file is not\n" + << " encrypted, or 3 if the file is encrypted\n" + << " but requires no password or the supplied password\n" + << " is correct; useful for shell scripts\n" << "--verbose provide additional informational output\n" << "--progress give progress indicators while writing output\n" << "--no-warn suppress warnings\n" @@ -2352,6 +2371,20 @@ ArgParser::argReplaceInput() o.replace_input = true; } +void +ArgParser::argIsEncrypted() +{ + o.check_is_encrypted = true; + o.require_outfile = false; +} + +void +ArgParser::argRequiresPassword() +{ + o.check_requires_password = true; + o.require_outfile = false; +} + void ArgParser::handleArgFileArguments() { @@ -3113,6 +3146,11 @@ ArgParser::doFinalChecks() { o.externalize_inline_images = true; } + if (o.check_requires_password && o.check_is_encrypted) + { + usage("--requires-password and --is-encrypted may not be given" + " together"); + } if (o.require_outfile && o.outfilename && (strcmp(o.outfilename, "-") == 0)) @@ -5252,9 +5290,45 @@ int realmain(int argc, char* argv[]) try { ap.parseOptions(); - PointerHolder pdf_ph = - process_file(o.infilename, o.password, o); + PointerHolder pdf_ph; + try + { + pdf_ph = process_file(o.infilename, o.password, o); + } + catch (QPDFExc& e) + { + if ((e.getErrorCode() == qpdf_e_password) && + (o.check_is_encrypted || o.check_requires_password)) + { + // Allow --is-encrypted and --requires-password to + // work when an incorrect password is supplied. + exit(0); + } + throw e; + } QPDF& pdf = *pdf_ph; + if (o.check_is_encrypted) + { + if (pdf.isEncrypted()) + { + exit(0); + } + else + { + exit(EXIT_IS_NOT_ENCRYPTED); + } + } + else if (o.check_requires_password) + { + if (pdf.isEncrypted()) + { + exit(EXIT_CORRECT_PASSWORD); + } + else + { + exit(EXIT_IS_NOT_ENCRYPTED); + } + } if (! o.page_specs.empty()) { handle_page_specs(pdf, o); diff --git a/qpdf/qtest/qpdf.test b/qpdf/qtest/qpdf.test index 7ea329c2..78184971 100644 --- a/qpdf/qtest/qpdf.test +++ b/qpdf/qtest/qpdf.test @@ -250,6 +250,28 @@ $td->runtest("check exception handling", {$td->FILE => "exceptions.out", $td->EXIT_STATUS => 0}, $td->NORMALIZE_NEWLINES); +show_ntests(); +# ---------- +$td->notify("--- Check encryption/password ---"); +my @check_encryption_password = ( + # file, password, is-encrypted, requires-password + ["minimal.pdf", "", 2, 2], + ["20-pages.pdf", "", 0, 0], + ["20-pages.pdf", "user", 0, 3], + ); +$n_tests += 2 * scalar(@check_encryption_password); +foreach my $d (@check_encryption_password) +{ + my ($file, $pass, $is_encrypted, $requires_password) = @$d; + $td->runtest("is encrypted ($file, pass=$pass)", + {$td->COMMAND => "qpdf --is-encrypted --password=$pass $file"}, + {$td->STRING => "", $td->EXIT_STATUS => $is_encrypted}); + $td->runtest("requires password ($file, pass=$pass)", + {$td->COMMAND => "qpdf --requires-password" . + " --password=$pass $file"}, + {$td->STRING => "", $td->EXIT_STATUS => $requires_password}); +} + show_ntests(); # ---------- $td->notify("--- Dangling Refs ---");