Commit 731c4f711b8bd12bfe8e86dcf80ca84bb38eba0e

Authored by Jay Berkenbilt
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.
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 &quot;\n&quot;;
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-&gt;runtest(&quot;check exception handling&quot;,
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);
... ...