Commit 63158cf546f0566eed61b0c76afd1a5c886ae8a8

Authored by Jay Berkenbilt
1 parent 21b0f4ac

Add --password-file=filename option (fixes #499)

ChangeLog
1 1 2021-02-04 Jay Berkenbilt <ejb@ql.org>
2 2  
  3 + * Add new option --pasword-file=file for reading the decryption
  4 + password from a file. file may be "-" to read from standard input.
  5 + Fixes #499.
  6 +
3 7 * By default, give an error if a user attempts to encrypt a file
4 8 with an empty owner password or an owner password that is the same
5 9 as the user password. Such files are insecure. Most viewers either
... ...
manual/qpdf-manual.xml
... ... @@ -571,13 +571,17 @@ make
571 571 linkend="ref.page-selection"/>.
572 572 </para>
573 573 <para>
574   - If <option>@filename</option> appears anywhere in the
  574 + If <option>@filename</option> appears as a word anywhere in the
575 575 command-line, it will be read line by line, and each line will be
576 576 treated as a command-line argument. The <option>@-</option> option
577 577 allows arguments to be read from standard input. This allows qpdf
578 578 to be invoked with an arbitrary number of arbitrarily long
579 579 arguments. It is also very useful for avoiding having to pass
580   - passwords on the command line.
  580 + passwords on the command line. Note that the
  581 + <option>@filename</option> can't appear in the middle of an
  582 + argument, so constructs such as <option>--arg=@option</option>
  583 + will not work. You would have to include the argument and its
  584 + options together in the arguments file.
581 585 </para>
582 586 <para>
583 587 <option>outfilename</option> does not have to be seekable, even
... ... @@ -714,14 +718,34 @@ make
714 718 </listitem>
715 719 </varlistentry>
716 720 <varlistentry>
717   - <term><option>--password=password</option></term>
  721 + <term><option>--password=<replaceable>password</replaceable></option></term>
718 722 <listitem>
719 723 <para>
720   - Specifies a password for accessing encrypted files. Note that
721   - you can use <option>@filename</option> or <option>@-</option>
722   - as described above to put the password in a file or pass it
723   - via standard input so you can avoid specifying it on the
724   - command line.
  724 + Specifies a password for accessing encrypted files. To read
  725 + the password from a file or standard input, you can use
  726 + <option>--password-file</option>, added in qpdf 10.2. Note
  727 + that you can also use <option>@filename</option> or
  728 + <option>@-</option> as described above to put the password in
  729 + a file or pass it via standard input, but you would do so by
  730 + specifying the entire
  731 + <option>--password=<replaceable>password</replaceable></option>
  732 + option in the file. Syntax such as
  733 + <option>--password=@filename</option> won't work since
  734 + <option>@filename</option> is not recognized in the middle of
  735 + an argument.
  736 + </para>
  737 + </listitem>
  738 + </varlistentry>
  739 + <varlistentry>
  740 + <term><option>--password-file=<replaceable>filename</replaceable></option></term>
  741 + <listitem>
  742 + <para>
  743 + Reads the first line from the specified file and uses it as
  744 + the password for accessing encrypted files.
  745 + <option><replaceable>filename</replaceable></option> may be
  746 + <literal>-</literal> to read the password from standard input.
  747 + Note that, in this case, the password is echoed and there is
  748 + no prompt, so use with caution.
725 749 </para>
726 750 </listitem>
727 751 </varlistentry>
... ... @@ -4886,6 +4910,24 @@ print &quot;\n&quot;;
4886 4910 </listitem>
4887 4911 <listitem>
4888 4912 <para>
  4913 + CLI Enhancements
  4914 + </para>
  4915 + <itemizedlist>
  4916 + <listitem>
  4917 + <para>
  4918 + The option
  4919 + <option>--password-file=<replaceable>filename</replaceable></option>
  4920 + can now be used to read the decryption password from a file.
  4921 + You can use <literal>-</literal> as the file name to read
  4922 + the password from standard input. This is an easier/more
  4923 + obvious way to read passwords from files or standard input
  4924 + than using <option>@file</option> for this purpose.
  4925 + </para>
  4926 + </listitem>
  4927 + </itemizedlist>
  4928 + </listitem>
  4929 + <listitem>
  4930 + <para>
4889 4931 Library Enhancements
4890 4932 </para>
4891 4933 <itemizedlist>
... ...
qpdf/qpdf.cc
... ... @@ -4,6 +4,7 @@
4 4 #include <fcntl.h>
5 5 #include <stdio.h>
6 6 #include <ctype.h>
  7 +#include <memory>
7 8  
8 9 #include <qpdf/QUtil.hh>
9 10 #include <qpdf/QTC.hh>
... ... @@ -199,6 +200,7 @@ struct Options
199 200 }
200 201  
201 202 char const* password;
  203 + std::shared_ptr<char> password_alloc;
202 204 bool linearize;
203 205 bool decrypt;
204 206 int split_pages;
... ... @@ -739,6 +741,7 @@ class ArgParser
739 741 void argShowCrypto();
740 742 void argPositional(char* arg);
741 743 void argPassword(char* parameter);
  744 + void argPasswordFile(char* paramter);
742 745 void argEmpty();
743 746 void argLinearize();
744 747 void argEncrypt();
... ... @@ -955,6 +958,8 @@ ArgParser::initOptionTable()
955 958 (*t)[""] = oe_positional(&ArgParser::argPositional);
956 959 (*t)["password"] = oe_requiredParameter(
957 960 &ArgParser::argPassword, "password");
  961 + (*t)["password-file"] = oe_requiredParameter(
  962 + &ArgParser::argPasswordFile, "password-file");
958 963 (*t)["empty"] = oe_bare(&ArgParser::argEmpty);
959 964 (*t)["linearize"] = oe_bare(&ArgParser::argLinearize);
960 965 (*t)["encrypt"] = oe_bare(&ArgParser::argEncrypt);
... ... @@ -1235,6 +1240,9 @@ ArgParser::argHelp()
1235 1240 << "--completion-bash output a bash complete command you can eval\n"
1236 1241 << "--completion-zsh output a zsh complete command you can eval\n"
1237 1242 << "--password=password specify a password for accessing encrypted files\n"
  1243 + << "--password-file=file get the password the first line \"file\"; use \"-\"\n"
  1244 + << " to read the password from stdin (without prompt or\n"
  1245 + << " disabling echo, so use with caution)\n"
1238 1246 << "--is-encrypted silently exit 0 if the file is encrypted or 2\n"
1239 1247 << " if not; useful for shell scripts\n"
1240 1248 << "--requires-password silently exit 0 if a password (other than as\n"
... ... @@ -1273,7 +1281,8 @@ ArgParser::argHelp()
1273 1281 << "\n"
1274 1282 << "Note that you can use the @filename or @- syntax for any argument at any\n"
1275 1283 << "point in the command. This provides a good way to specify a password without\n"
1276   - << "having to explicitly put it on the command line.\n"
  1284 + << "having to explicitly put it on the command line. @filename or @- must be a\n"
  1285 + << "word by itself. Syntax such as --arg=@filename doesn't work.\n"
1277 1286 << "\n"
1278 1287 << "If none of --copy-encryption, --encrypt or --decrypt are given, qpdf will\n"
1279 1288 << "preserve any encryption data associated with a file.\n"
... ... @@ -1750,6 +1759,36 @@ ArgParser::argPassword(char* parameter)
1750 1759 }
1751 1760  
1752 1761 void
  1762 +ArgParser::argPasswordFile(char* parameter)
  1763 +{
  1764 + std::list<std::string> lines;
  1765 + if (strcmp(parameter, "-") == 0)
  1766 + {
  1767 + QTC::TC("qpdf", "qpdf password stdin");
  1768 + lines = QUtil::read_lines_from_file(std::cin);
  1769 + }
  1770 + else
  1771 + {
  1772 + QTC::TC("qpdf", "qpdf password file");
  1773 + lines = QUtil::read_lines_from_file(parameter);
  1774 + }
  1775 + if (lines.size() >= 1)
  1776 + {
  1777 + // Make sure the memory for this stays in scope.
  1778 + o.password_alloc = std::shared_ptr<char>(
  1779 + QUtil::copy_string(lines.front().c_str()),
  1780 + std::default_delete<char[]>());
  1781 + o.password = o.password_alloc.get();
  1782 +
  1783 + if (lines.size() > 1)
  1784 + {
  1785 + std::cerr << whoami << ": WARNING: all but the first line of"
  1786 + << " the password file are ignored" << std::endl;
  1787 + }
  1788 + }
  1789 +}
  1790 +
  1791 +void
1753 1792 ArgParser::argEmpty()
1754 1793 {
1755 1794 o.infilename = "";
... ...
qpdf/qpdf.testcov
... ... @@ -568,3 +568,5 @@ NNTree erased last item in tree 0
568 568 NNTree remove limits from root 0
569 569 QPDFPageObjectHelper unresolved names 0
570 570 QPDFPageObjectHelper resolving unresolved 0
  571 +qpdf password stdin 0
  572 +qpdf password file 0
... ...
qpdf/qtest/qpdf.test
... ... @@ -270,7 +270,7 @@ my @check_encryption_password = (
270 270 ["20-pages.pdf", "", 0, 0],
271 271 ["20-pages.pdf", "user", 0, 3],
272 272 );
273   -$n_tests += 2 * scalar(@check_encryption_password);
  273 +$n_tests += 3 * scalar(@check_encryption_password);
274 274 foreach my $d (@check_encryption_password)
275 275 {
276 276 my ($file, $pass, $is_encrypted, $requires_password) = @$d;
... ... @@ -283,6 +283,29 @@ foreach my $d (@check_encryption_password)
283 283 {$td->STRING => "", $td->EXIT_STATUS => $requires_password});
284 284 }
285 285  
  286 +# Exercise reading password from file
  287 +open(F, ">args") or die;
  288 +print F "user\n";
  289 +close(F);
  290 +$td->runtest("password from file)",
  291 + {$td->COMMAND => "qpdf --check --password-file=args 20-pages.pdf"},
  292 + {$td->FILE => "20-pages-check.out", $td->EXIT_STATUS => 0},
  293 + $td->NORMALIZE_NEWLINES);
  294 +open(F, ">>args") or die;
  295 +print F "ignored\n";
  296 +close(F);
  297 +$td->runtest("ignore extra args from file)",
  298 + {$td->COMMAND => "qpdf --check --password-file=args 20-pages.pdf"},
  299 + {$td->FILE => "20-pages-check-password-warning.out",
  300 + $td->EXIT_STATUS => 0},
  301 + $td->NORMALIZE_NEWLINES);
  302 +unlink "args";
  303 +$td->runtest("password from stdin)",
  304 + {$td->COMMAND => "echo user |" .
  305 + " qpdf --check --password-file=- 20-pages.pdf"},
  306 + {$td->FILE => "20-pages-check.out", $td->EXIT_STATUS => 0},
  307 + $td->NORMALIZE_NEWLINES);
  308 +
286 309 show_ntests();
287 310 # ----------
288 311 $td->notify("--- Dangling Refs ---");
... ...
qpdf/qtest/qpdf/20-pages-check-password-warning.out 0 → 100644
  1 +qpdf: WARNING: all but the first line of the password file are ignored
  2 +checking 20-pages.pdf
  3 +PDF Version: 1.4
  4 +R = 3
  5 +P = -4
  6 +User password = user
  7 +Supplied password is user password
  8 +extract for accessibility: allowed
  9 +extract for any purpose: allowed
  10 +print low resolution: allowed
  11 +print high resolution: allowed
  12 +modify document assembly: allowed
  13 +modify forms: allowed
  14 +modify annotations: allowed
  15 +modify other: allowed
  16 +modify anything: allowed
  17 +File is not linearized
  18 +No syntax or stream encoding errors found; the file may still contain
  19 +errors that qpdf cannot detect
... ...
qpdf/qtest/qpdf/20-pages-check.out 0 → 100644
  1 +checking 20-pages.pdf
  2 +PDF Version: 1.4
  3 +R = 3
  4 +P = -4
  5 +User password = user
  6 +Supplied password is user password
  7 +extract for accessibility: allowed
  8 +extract for any purpose: allowed
  9 +print low resolution: allowed
  10 +print high resolution: allowed
  11 +modify document assembly: allowed
  12 +modify forms: allowed
  13 +modify annotations: allowed
  14 +modify other: allowed
  15 +modify anything: allowed
  16 +File is not linearized
  17 +No syntax or stream encoding errors found; the file may still contain
  18 +errors that qpdf cannot detect
... ...