Commit 63158cf546f0566eed61b0c76afd1a5c886ae8a8
1 parent
21b0f4ac
Add --password-file=filename option (fixes #499)
Showing
7 changed files
with
157 additions
and
10 deletions
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 "\n"; | @@ -4886,6 +4910,24 @@ print "\n"; | ||
| 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 |