Commit 731c4f711b8bd12bfe8e86dcf80ca84bb38eba0e
1 parent
8b1c4828
Add --is-encrypted and --requires-password (fixes #390)
Allow exit status-based checking of whether a file is encrypted or requires a password without necessarily supplying the correct password. Useful for scripting.
Showing
4 changed files
with
161 additions
and
4 deletions
ChangeLog
| 1 | +2020-01-26 Jay Berkenbilt <ejb@ql.org> | |
| 2 | + | |
| 3 | + * Add options --is-encrypted and --requires-password. These can be | |
| 4 | + used with files, including encrypted files with unknown passwords, | |
| 5 | + to determine whether or not a file is encrypted and whether a | |
| 6 | + password is required to open the file. The --requires-password | |
| 7 | + option can also be used to determine whether a supplied password | |
| 8 | + is correct. Information is supplied through exit codes, making | |
| 9 | + these options particularly useful for shell scripts. Fixes #390. | |
| 10 | + | |
| 1 | 11 | 2020-01-14 Jay Berkenbilt <ejb@ql.org> |
| 2 | 12 | |
| 3 | 13 | * Fix for Windows being unable to acquire crypt context with a new | ... | ... |
manual/qpdf-manual.xml
| ... | ... | @@ -666,6 +666,40 @@ make |
| 666 | 666 | </listitem> |
| 667 | 667 | </varlistentry> |
| 668 | 668 | <varlistentry> |
| 669 | + <term><option>--is-encrypted</option></term> | |
| 670 | + <listitem> | |
| 671 | + <para> | |
| 672 | + Silently exit with status 0 if the file is encrypted or status | |
| 673 | + 2 if the file is not encrypted. This is useful for shell | |
| 674 | + scripts. Other options are ignored if this is given. This | |
| 675 | + option is mutually exclusive with | |
| 676 | + <option>--requires-password</option>. Both this option and | |
| 677 | + <option>--requires-password</option> exit with status 2 for | |
| 678 | + non-encrypted files. | |
| 679 | + </para> | |
| 680 | + </listitem> | |
| 681 | + </varlistentry> | |
| 682 | + <varlistentry> | |
| 683 | + <term><option>--requires-password</option></term> | |
| 684 | + <listitem> | |
| 685 | + <para> | |
| 686 | + Silently exit with status 0 if a password (other than as | |
| 687 | + supplied) is required. Exit with status 2 if the file is not | |
| 688 | + encrypted. Exit with status 3 if the file is encrypted but | |
| 689 | + requires no password or the correct password has been | |
| 690 | + supplied. This is useful for shell scripts. Note that any | |
| 691 | + supplied password is used when opening the file. When used | |
| 692 | + with a <option>--password</option> option, this option can be | |
| 693 | + used to check the correctness of the password. In that case, | |
| 694 | + an exit status of 3 means the file works with the supplied | |
| 695 | + password. This option is mutually exclusive with | |
| 696 | + <option>--is-encrypted</option>. Both this option and | |
| 697 | + <option>--is-encrypted</option> exit with status 2 for | |
| 698 | + non-encrypted files. | |
| 699 | + </para> | |
| 700 | + </listitem> | |
| 701 | + </varlistentry> | |
| 702 | + <varlistentry> | |
| 669 | 703 | <term><option>--verbose</option></term> |
| 670 | 704 | <listitem> |
| 671 | 705 | <para> |
| ... | ... | @@ -4675,6 +4709,23 @@ print "\n"; |
| 4675 | 4709 | </listitem> |
| 4676 | 4710 | </itemizedlist> |
| 4677 | 4711 | </listitem> |
| 4712 | + <listitem> | |
| 4713 | + <para> | |
| 4714 | + CLI Enhancements | |
| 4715 | + </para> | |
| 4716 | + <itemizedlist> | |
| 4717 | + <listitem> | |
| 4718 | + <para> | |
| 4719 | + Added options <option>--is-encrypted</option> and | |
| 4720 | + <option>--requires-password</option> for testing whether a | |
| 4721 | + file is encrypted or requires a password other than the | |
| 4722 | + supplied (or empty) password. These communicate via exit | |
| 4723 | + status, making them useful for shell scripts. They also work | |
| 4724 | + on encrypted files with unknown passwords. | |
| 4725 | + </para> | |
| 4726 | + </listitem> | |
| 4727 | + </itemizedlist> | |
| 4728 | + </listitem> | |
| 4678 | 4729 | </itemizedlist> |
| 4679 | 4730 | </listitem> |
| 4680 | 4731 | </varlistentry> | ... | ... |
qpdf/qpdf.cc
| ... | ... | @@ -29,8 +29,12 @@ |
| 29 | 29 | #include <qpdf/QPDFWriter.hh> |
| 30 | 30 | #include <qpdf/QIntC.hh> |
| 31 | 31 | |
| 32 | -static int const EXIT_ERROR = 2; | |
| 33 | -static int const EXIT_WARNING = 3; | |
| 32 | +static int constexpr EXIT_ERROR = 2; | |
| 33 | +static int constexpr EXIT_WARNING = 3; | |
| 34 | + | |
| 35 | +// For is-encrypted and requires-password | |
| 36 | +static int constexpr EXIT_IS_NOT_ENCRYPTED = 2; | |
| 37 | +static int constexpr EXIT_CORRECT_PASSWORD = 3; | |
| 34 | 38 | |
| 35 | 39 | static char const* whoami = 0; |
| 36 | 40 | |
| ... | ... | @@ -183,6 +187,8 @@ struct Options |
| 183 | 187 | under_overlay(0), |
| 184 | 188 | require_outfile(true), |
| 185 | 189 | replace_input(false), |
| 190 | + check_is_encrypted(false), | |
| 191 | + check_requires_password(false), | |
| 186 | 192 | infilename(0), |
| 187 | 193 | outfilename(0) |
| 188 | 194 | { |
| ... | ... | @@ -287,6 +293,8 @@ struct Options |
| 287 | 293 | std::map<std::string, RotationSpec> rotations; |
| 288 | 294 | bool require_outfile; |
| 289 | 295 | bool replace_input; |
| 296 | + bool check_is_encrypted; | |
| 297 | + bool check_requires_password; | |
| 290 | 298 | char const* infilename; |
| 291 | 299 | char const* outfilename; |
| 292 | 300 | }; |
| ... | ... | @@ -718,6 +726,8 @@ class ArgParser |
| 718 | 726 | void argUOpassword(char* parameter); |
| 719 | 727 | void argEndUnderOverlay(); |
| 720 | 728 | void argReplaceInput(); |
| 729 | + void argIsEncrypted(); | |
| 730 | + void argRequiresPassword(); | |
| 721 | 731 | |
| 722 | 732 | void usage(std::string const& message); |
| 723 | 733 | void checkCompletion(); |
| ... | ... | @@ -948,6 +958,8 @@ ArgParser::initOptionTable() |
| 948 | 958 | (*t)["overlay"] = oe_bare(&ArgParser::argOverlay); |
| 949 | 959 | (*t)["underlay"] = oe_bare(&ArgParser::argUnderlay); |
| 950 | 960 | (*t)["replace-input"] = oe_bare(&ArgParser::argReplaceInput); |
| 961 | + (*t)["is-encrypted"] = oe_bare(&ArgParser::argIsEncrypted); | |
| 962 | + (*t)["requires-password"] = oe_bare(&ArgParser::argRequiresPassword); | |
| 951 | 963 | |
| 952 | 964 | t = &this->encrypt40_option_table; |
| 953 | 965 | (*t)["--"] = oe_bare(&ArgParser::argEndEncrypt); |
| ... | ... | @@ -1105,6 +1117,13 @@ ArgParser::argHelp() |
| 1105 | 1117 | << "--completion-bash output a bash complete command you can eval\n" |
| 1106 | 1118 | << "--completion-zsh output a zsh complete command you can eval\n" |
| 1107 | 1119 | << "--password=password specify a password for accessing encrypted files\n" |
| 1120 | + << "--is-encrypted silently exit 0 if the file is encrypted or 2\n" | |
| 1121 | + << " if not; useful for shell scripts\n" | |
| 1122 | + << "--requires-password silently exit 0 if a password (other than as\n" | |
| 1123 | + << " supplied) is required, 2 if the file is not\n" | |
| 1124 | + << " encrypted, or 3 if the file is encrypted\n" | |
| 1125 | + << " but requires no password or the supplied password\n" | |
| 1126 | + << " is correct; useful for shell scripts\n" | |
| 1108 | 1127 | << "--verbose provide additional informational output\n" |
| 1109 | 1128 | << "--progress give progress indicators while writing output\n" |
| 1110 | 1129 | << "--no-warn suppress warnings\n" |
| ... | ... | @@ -2353,6 +2372,20 @@ ArgParser::argReplaceInput() |
| 2353 | 2372 | } |
| 2354 | 2373 | |
| 2355 | 2374 | void |
| 2375 | +ArgParser::argIsEncrypted() | |
| 2376 | +{ | |
| 2377 | + o.check_is_encrypted = true; | |
| 2378 | + o.require_outfile = false; | |
| 2379 | +} | |
| 2380 | + | |
| 2381 | +void | |
| 2382 | +ArgParser::argRequiresPassword() | |
| 2383 | +{ | |
| 2384 | + o.check_requires_password = true; | |
| 2385 | + o.require_outfile = false; | |
| 2386 | +} | |
| 2387 | + | |
| 2388 | +void | |
| 2356 | 2389 | ArgParser::handleArgFileArguments() |
| 2357 | 2390 | { |
| 2358 | 2391 | // Support reading arguments from files. Create a new argv. Ensure |
| ... | ... | @@ -3113,6 +3146,11 @@ ArgParser::doFinalChecks() |
| 3113 | 3146 | { |
| 3114 | 3147 | o.externalize_inline_images = true; |
| 3115 | 3148 | } |
| 3149 | + if (o.check_requires_password && o.check_is_encrypted) | |
| 3150 | + { | |
| 3151 | + usage("--requires-password and --is-encrypted may not be given" | |
| 3152 | + " together"); | |
| 3153 | + } | |
| 3116 | 3154 | |
| 3117 | 3155 | if (o.require_outfile && o.outfilename && |
| 3118 | 3156 | (strcmp(o.outfilename, "-") == 0)) |
| ... | ... | @@ -5252,9 +5290,45 @@ int realmain(int argc, char* argv[]) |
| 5252 | 5290 | try |
| 5253 | 5291 | { |
| 5254 | 5292 | ap.parseOptions(); |
| 5255 | - PointerHolder<QPDF> pdf_ph = | |
| 5256 | - process_file(o.infilename, o.password, o); | |
| 5293 | + PointerHolder<QPDF> pdf_ph; | |
| 5294 | + try | |
| 5295 | + { | |
| 5296 | + pdf_ph = process_file(o.infilename, o.password, o); | |
| 5297 | + } | |
| 5298 | + catch (QPDFExc& e) | |
| 5299 | + { | |
| 5300 | + if ((e.getErrorCode() == qpdf_e_password) && | |
| 5301 | + (o.check_is_encrypted || o.check_requires_password)) | |
| 5302 | + { | |
| 5303 | + // Allow --is-encrypted and --requires-password to | |
| 5304 | + // work when an incorrect password is supplied. | |
| 5305 | + exit(0); | |
| 5306 | + } | |
| 5307 | + throw e; | |
| 5308 | + } | |
| 5257 | 5309 | QPDF& pdf = *pdf_ph; |
| 5310 | + if (o.check_is_encrypted) | |
| 5311 | + { | |
| 5312 | + if (pdf.isEncrypted()) | |
| 5313 | + { | |
| 5314 | + exit(0); | |
| 5315 | + } | |
| 5316 | + else | |
| 5317 | + { | |
| 5318 | + exit(EXIT_IS_NOT_ENCRYPTED); | |
| 5319 | + } | |
| 5320 | + } | |
| 5321 | + else if (o.check_requires_password) | |
| 5322 | + { | |
| 5323 | + if (pdf.isEncrypted()) | |
| 5324 | + { | |
| 5325 | + exit(EXIT_CORRECT_PASSWORD); | |
| 5326 | + } | |
| 5327 | + else | |
| 5328 | + { | |
| 5329 | + exit(EXIT_IS_NOT_ENCRYPTED); | |
| 5330 | + } | |
| 5331 | + } | |
| 5258 | 5332 | if (! o.page_specs.empty()) |
| 5259 | 5333 | { |
| 5260 | 5334 | handle_page_specs(pdf, o); | ... | ... |
qpdf/qtest/qpdf.test
| ... | ... | @@ -252,6 +252,28 @@ $td->runtest("check exception handling", |
| 252 | 252 | |
| 253 | 253 | show_ntests(); |
| 254 | 254 | # ---------- |
| 255 | +$td->notify("--- Check encryption/password ---"); | |
| 256 | +my @check_encryption_password = ( | |
| 257 | + # file, password, is-encrypted, requires-password | |
| 258 | + ["minimal.pdf", "", 2, 2], | |
| 259 | + ["20-pages.pdf", "", 0, 0], | |
| 260 | + ["20-pages.pdf", "user", 0, 3], | |
| 261 | + ); | |
| 262 | +$n_tests += 2 * scalar(@check_encryption_password); | |
| 263 | +foreach my $d (@check_encryption_password) | |
| 264 | +{ | |
| 265 | + my ($file, $pass, $is_encrypted, $requires_password) = @$d; | |
| 266 | + $td->runtest("is encrypted ($file, pass=$pass)", | |
| 267 | + {$td->COMMAND => "qpdf --is-encrypted --password=$pass $file"}, | |
| 268 | + {$td->STRING => "", $td->EXIT_STATUS => $is_encrypted}); | |
| 269 | + $td->runtest("requires password ($file, pass=$pass)", | |
| 270 | + {$td->COMMAND => "qpdf --requires-password" . | |
| 271 | + " --password=$pass $file"}, | |
| 272 | + {$td->STRING => "", $td->EXIT_STATUS => $requires_password}); | |
| 273 | +} | |
| 274 | + | |
| 275 | +show_ntests(); | |
| 276 | +# ---------- | |
| 255 | 277 | $td->notify("--- Dangling Refs ---"); |
| 256 | 278 | my @dangling = (qw(minimal dangling-refs)); |
| 257 | 279 | $n_tests += 2 * scalar(@dangling); | ... | ... |