Commit 63158cf546f0566eed61b0c76afd1a5c886ae8a8

Authored by Jay Berkenbilt
1 parent 21b0f4ac

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

ChangeLog
1 2021-02-04 Jay Berkenbilt <ejb@ql.org> 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 * By default, give an error if a user attempts to encrypt a file 7 * By default, give an error if a user attempts to encrypt a file
4 with an empty owner password or an owner password that is the same 8 with an empty owner password or an owner password that is the same
5 as the user password. Such files are insecure. Most viewers either 9 as the user password. Such files are insecure. Most viewers either
manual/qpdf-manual.xml
@@ -571,13 +571,17 @@ make @@ -571,13 +571,17 @@ make
571 linkend="ref.page-selection"/>. 571 linkend="ref.page-selection"/>.
572 </para> 572 </para>
573 <para> 573 <para>
574 - If <option>@filename</option> appears anywhere in the 574 + If <option>@filename</option> appears as a word anywhere in the
575 command-line, it will be read line by line, and each line will be 575 command-line, it will be read line by line, and each line will be
576 treated as a command-line argument. The <option>@-</option> option 576 treated as a command-line argument. The <option>@-</option> option
577 allows arguments to be read from standard input. This allows qpdf 577 allows arguments to be read from standard input. This allows qpdf
578 to be invoked with an arbitrary number of arbitrarily long 578 to be invoked with an arbitrary number of arbitrarily long
579 arguments. It is also very useful for avoiding having to pass 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 </para> 585 </para>
582 <para> 586 <para>
583 <option>outfilename</option> does not have to be seekable, even 587 <option>outfilename</option> does not have to be seekable, even
@@ -714,14 +718,34 @@ make @@ -714,14 +718,34 @@ make
714 </listitem> 718 </listitem>
715 </varlistentry> 719 </varlistentry>
716 <varlistentry> 720 <varlistentry>
717 - <term><option>--password=password</option></term> 721 + <term><option>--password=<replaceable>password</replaceable></option></term>
718 <listitem> 722 <listitem>
719 <para> 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 </para> 749 </para>
726 </listitem> 750 </listitem>
727 </varlistentry> 751 </varlistentry>
@@ -4886,6 +4910,24 @@ print &quot;\n&quot;; @@ -4886,6 +4910,24 @@ print &quot;\n&quot;;
4886 </listitem> 4910 </listitem>
4887 <listitem> 4911 <listitem>
4888 <para> 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 Library Enhancements 4931 Library Enhancements
4890 </para> 4932 </para>
4891 <itemizedlist> 4933 <itemizedlist>
qpdf/qpdf.cc
@@ -4,6 +4,7 @@ @@ -4,6 +4,7 @@
4 #include <fcntl.h> 4 #include <fcntl.h>
5 #include <stdio.h> 5 #include <stdio.h>
6 #include <ctype.h> 6 #include <ctype.h>
  7 +#include <memory>
7 8
8 #include <qpdf/QUtil.hh> 9 #include <qpdf/QUtil.hh>
9 #include <qpdf/QTC.hh> 10 #include <qpdf/QTC.hh>
@@ -199,6 +200,7 @@ struct Options @@ -199,6 +200,7 @@ struct Options
199 } 200 }
200 201
201 char const* password; 202 char const* password;
  203 + std::shared_ptr<char> password_alloc;
202 bool linearize; 204 bool linearize;
203 bool decrypt; 205 bool decrypt;
204 int split_pages; 206 int split_pages;
@@ -739,6 +741,7 @@ class ArgParser @@ -739,6 +741,7 @@ class ArgParser
739 void argShowCrypto(); 741 void argShowCrypto();
740 void argPositional(char* arg); 742 void argPositional(char* arg);
741 void argPassword(char* parameter); 743 void argPassword(char* parameter);
  744 + void argPasswordFile(char* paramter);
742 void argEmpty(); 745 void argEmpty();
743 void argLinearize(); 746 void argLinearize();
744 void argEncrypt(); 747 void argEncrypt();
@@ -955,6 +958,8 @@ ArgParser::initOptionTable() @@ -955,6 +958,8 @@ ArgParser::initOptionTable()
955 (*t)[""] = oe_positional(&ArgParser::argPositional); 958 (*t)[""] = oe_positional(&ArgParser::argPositional);
956 (*t)["password"] = oe_requiredParameter( 959 (*t)["password"] = oe_requiredParameter(
957 &ArgParser::argPassword, "password"); 960 &ArgParser::argPassword, "password");
  961 + (*t)["password-file"] = oe_requiredParameter(
  962 + &ArgParser::argPasswordFile, "password-file");
958 (*t)["empty"] = oe_bare(&ArgParser::argEmpty); 963 (*t)["empty"] = oe_bare(&ArgParser::argEmpty);
959 (*t)["linearize"] = oe_bare(&ArgParser::argLinearize); 964 (*t)["linearize"] = oe_bare(&ArgParser::argLinearize);
960 (*t)["encrypt"] = oe_bare(&ArgParser::argEncrypt); 965 (*t)["encrypt"] = oe_bare(&ArgParser::argEncrypt);
@@ -1235,6 +1240,9 @@ ArgParser::argHelp() @@ -1235,6 +1240,9 @@ ArgParser::argHelp()
1235 << "--completion-bash output a bash complete command you can eval\n" 1240 << "--completion-bash output a bash complete command you can eval\n"
1236 << "--completion-zsh output a zsh complete command you can eval\n" 1241 << "--completion-zsh output a zsh complete command you can eval\n"
1237 << "--password=password specify a password for accessing encrypted files\n" 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 << "--is-encrypted silently exit 0 if the file is encrypted or 2\n" 1246 << "--is-encrypted silently exit 0 if the file is encrypted or 2\n"
1239 << " if not; useful for shell scripts\n" 1247 << " if not; useful for shell scripts\n"
1240 << "--requires-password silently exit 0 if a password (other than as\n" 1248 << "--requires-password silently exit 0 if a password (other than as\n"
@@ -1273,7 +1281,8 @@ ArgParser::argHelp() @@ -1273,7 +1281,8 @@ ArgParser::argHelp()
1273 << "\n" 1281 << "\n"
1274 << "Note that you can use the @filename or @- syntax for any argument at any\n" 1282 << "Note that you can use the @filename or @- syntax for any argument at any\n"
1275 << "point in the command. This provides a good way to specify a password without\n" 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 << "\n" 1286 << "\n"
1278 << "If none of --copy-encryption, --encrypt or --decrypt are given, qpdf will\n" 1287 << "If none of --copy-encryption, --encrypt or --decrypt are given, qpdf will\n"
1279 << "preserve any encryption data associated with a file.\n" 1288 << "preserve any encryption data associated with a file.\n"
@@ -1750,6 +1759,36 @@ ArgParser::argPassword(char* parameter) @@ -1750,6 +1759,36 @@ ArgParser::argPassword(char* parameter)
1750 } 1759 }
1751 1760
1752 void 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 ArgParser::argEmpty() 1792 ArgParser::argEmpty()
1754 { 1793 {
1755 o.infilename = ""; 1794 o.infilename = "";
qpdf/qpdf.testcov
@@ -568,3 +568,5 @@ NNTree erased last item in tree 0 @@ -568,3 +568,5 @@ NNTree erased last item in tree 0
568 NNTree remove limits from root 0 568 NNTree remove limits from root 0
569 QPDFPageObjectHelper unresolved names 0 569 QPDFPageObjectHelper unresolved names 0
570 QPDFPageObjectHelper resolving unresolved 0 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,7 +270,7 @@ my @check_encryption_password = (
270 ["20-pages.pdf", "", 0, 0], 270 ["20-pages.pdf", "", 0, 0],
271 ["20-pages.pdf", "user", 0, 3], 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 foreach my $d (@check_encryption_password) 274 foreach my $d (@check_encryption_password)
275 { 275 {
276 my ($file, $pass, $is_encrypted, $requires_password) = @$d; 276 my ($file, $pass, $is_encrypted, $requires_password) = @$d;
@@ -283,6 +283,29 @@ foreach my $d (@check_encryption_password) @@ -283,6 +283,29 @@ foreach my $d (@check_encryption_password)
283 {$td->STRING => "", $td->EXIT_STATUS => $requires_password}); 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 show_ntests(); 309 show_ntests();
287 # ---------- 310 # ----------
288 $td->notify("--- Dangling Refs ---"); 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