From 63158cf546f0566eed61b0c76afd1a5c886ae8a8 Mon Sep 17 00:00:00 2001 From: Jay Berkenbilt Date: Thu, 4 Feb 2021 16:46:59 -0500 Subject: [PATCH] Add --password-file=filename option (fixes #499) --- ChangeLog | 4 ++ manual/qpdf-manual.xml | 58 ++++++++++++++++--- qpdf/qpdf.cc | 41 ++++++++++++- qpdf/qpdf.testcov | 2 + qpdf/qtest/qpdf.test | 25 +++++++- .../qpdf/20-pages-check-password-warning.out | 19 ++++++ qpdf/qtest/qpdf/20-pages-check.out | 18 ++++++ 7 files changed, 157 insertions(+), 10 deletions(-) create mode 100644 qpdf/qtest/qpdf/20-pages-check-password-warning.out create mode 100644 qpdf/qtest/qpdf/20-pages-check.out diff --git a/ChangeLog b/ChangeLog index 32410df9..cbcfba7a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,9 @@ 2021-02-04 Jay Berkenbilt + * Add new option --pasword-file=file for reading the decryption + password from a file. file may be "-" to read from standard input. + Fixes #499. + * By default, give an error if a user attempts to encrypt a file with an empty owner password or an owner password that is the same as the user password. Such files are insecure. Most viewers either diff --git a/manual/qpdf-manual.xml b/manual/qpdf-manual.xml index 0d9fd489..5b4d2cee 100644 --- a/manual/qpdf-manual.xml +++ b/manual/qpdf-manual.xml @@ -571,13 +571,17 @@ make linkend="ref.page-selection"/>. - If appears anywhere in the + If appears as a word anywhere in the command-line, it will be read line by line, and each line will be treated as a command-line argument. The option allows arguments to be read from standard input. This allows qpdf to be invoked with an arbitrary number of arbitrarily long arguments. It is also very useful for avoiding having to pass - passwords on the command line. + passwords on the command line. Note that the + can't appear in the middle of an + argument, so constructs such as + will not work. You would have to include the argument and its + options together in the arguments file. does not have to be seekable, even @@ -714,14 +718,34 @@ make - + - Specifies a password for accessing encrypted files. Note that - you can use or - as described above to put the password in a file or pass it - via standard input so you can avoid specifying it on the - command line. + Specifies a password for accessing encrypted files. To read + the password from a file or standard input, you can use + , added in qpdf 10.2. Note + that you can also use or + as described above to put the password in + a file or pass it via standard input, but you would do so by + specifying the entire + + option in the file. Syntax such as + won't work since + is not recognized in the middle of + an argument. + + + + + + + + Reads the first line from the specified file and uses it as + the password for accessing encrypted files. + may be + - to read the password from standard input. + Note that, in this case, the password is echoed and there is + no prompt, so use with caution. @@ -4884,6 +4908,24 @@ print "\n"; + + + CLI Enhancements + + + + + The option + + can now be used to read the decryption password from a file. + You can use - as the file name to read + the password from standard input. This is an easier/more + obvious way to read passwords from files or standard input + than using for this purpose. + + + + Library Enhancements diff --git a/qpdf/qpdf.cc b/qpdf/qpdf.cc index 702a6b9e..64badd72 100644 --- a/qpdf/qpdf.cc +++ b/qpdf/qpdf.cc @@ -4,6 +4,7 @@ #include #include #include +#include #include #include @@ -199,6 +200,7 @@ struct Options } char const* password; + std::shared_ptr password_alloc; bool linearize; bool decrypt; int split_pages; @@ -739,6 +741,7 @@ class ArgParser void argShowCrypto(); void argPositional(char* arg); void argPassword(char* parameter); + void argPasswordFile(char* paramter); void argEmpty(); void argLinearize(); void argEncrypt(); @@ -955,6 +958,8 @@ ArgParser::initOptionTable() (*t)[""] = oe_positional(&ArgParser::argPositional); (*t)["password"] = oe_requiredParameter( &ArgParser::argPassword, "password"); + (*t)["password-file"] = oe_requiredParameter( + &ArgParser::argPasswordFile, "password-file"); (*t)["empty"] = oe_bare(&ArgParser::argEmpty); (*t)["linearize"] = oe_bare(&ArgParser::argLinearize); (*t)["encrypt"] = oe_bare(&ArgParser::argEncrypt); @@ -1235,6 +1240,9 @@ ArgParser::argHelp() << "--completion-bash output a bash complete command you can eval\n" << "--completion-zsh output a zsh complete command you can eval\n" << "--password=password specify a password for accessing encrypted files\n" + << "--password-file=file get the password the first line \"file\"; use \"-\"\n" + << " to read the password from stdin (without prompt or\n" + << " disabling echo, so use with caution)\n" << "--is-encrypted silently exit 0 if the file is encrypted or 2\n" << " if not; useful for shell scripts\n" << "--requires-password silently exit 0 if a password (other than as\n" @@ -1273,7 +1281,8 @@ ArgParser::argHelp() << "\n" << "Note that you can use the @filename or @- syntax for any argument at any\n" << "point in the command. This provides a good way to specify a password without\n" - << "having to explicitly put it on the command line.\n" + << "having to explicitly put it on the command line. @filename or @- must be a\n" + << "word by itself. Syntax such as --arg=@filename doesn't work.\n" << "\n" << "If none of --copy-encryption, --encrypt or --decrypt are given, qpdf will\n" << "preserve any encryption data associated with a file.\n" @@ -1749,6 +1758,36 @@ ArgParser::argPassword(char* parameter) o.password = parameter; } +void +ArgParser::argPasswordFile(char* parameter) +{ + std::list lines; + if (strcmp(parameter, "-") == 0) + { + QTC::TC("qpdf", "qpdf password stdin"); + lines = QUtil::read_lines_from_file(std::cin); + } + else + { + QTC::TC("qpdf", "qpdf password file"); + lines = QUtil::read_lines_from_file(parameter); + } + if (lines.size() >= 1) + { + // Make sure the memory for this stays in scope. + o.password_alloc = std::shared_ptr( + QUtil::copy_string(lines.front().c_str()), + std::default_delete()); + o.password = o.password_alloc.get(); + + if (lines.size() > 1) + { + std::cerr << whoami << ": WARNING: all but the first line of" + << " the password file are ignored" << std::endl; + } + } +} + void ArgParser::argEmpty() { diff --git a/qpdf/qpdf.testcov b/qpdf/qpdf.testcov index 520cc84b..7f01ae0b 100644 --- a/qpdf/qpdf.testcov +++ b/qpdf/qpdf.testcov @@ -568,3 +568,5 @@ NNTree erased last item in tree 0 NNTree remove limits from root 0 QPDFPageObjectHelper unresolved names 0 QPDFPageObjectHelper resolving unresolved 0 +qpdf password stdin 0 +qpdf password file 0 diff --git a/qpdf/qtest/qpdf.test b/qpdf/qtest/qpdf.test index f16210e7..5fede165 100644 --- a/qpdf/qtest/qpdf.test +++ b/qpdf/qtest/qpdf.test @@ -270,7 +270,7 @@ my @check_encryption_password = ( ["20-pages.pdf", "", 0, 0], ["20-pages.pdf", "user", 0, 3], ); -$n_tests += 2 * scalar(@check_encryption_password); +$n_tests += 3 * scalar(@check_encryption_password); foreach my $d (@check_encryption_password) { my ($file, $pass, $is_encrypted, $requires_password) = @$d; @@ -283,6 +283,29 @@ foreach my $d (@check_encryption_password) {$td->STRING => "", $td->EXIT_STATUS => $requires_password}); } +# Exercise reading password from file +open(F, ">args") or die; +print F "user\n"; +close(F); +$td->runtest("password from file)", + {$td->COMMAND => "qpdf --check --password-file=args 20-pages.pdf"}, + {$td->FILE => "20-pages-check.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +open(F, ">>args") or die; +print F "ignored\n"; +close(F); +$td->runtest("ignore extra args from file)", + {$td->COMMAND => "qpdf --check --password-file=args 20-pages.pdf"}, + {$td->FILE => "20-pages-check-password-warning.out", + $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); +unlink "args"; +$td->runtest("password from stdin)", + {$td->COMMAND => "echo user |" . + " qpdf --check --password-file=- 20-pages.pdf"}, + {$td->FILE => "20-pages-check.out", $td->EXIT_STATUS => 0}, + $td->NORMALIZE_NEWLINES); + show_ntests(); # ---------- $td->notify("--- Dangling Refs ---"); diff --git a/qpdf/qtest/qpdf/20-pages-check-password-warning.out b/qpdf/qtest/qpdf/20-pages-check-password-warning.out new file mode 100644 index 00000000..5d03f316 --- /dev/null +++ b/qpdf/qtest/qpdf/20-pages-check-password-warning.out @@ -0,0 +1,19 @@ +qpdf: WARNING: all but the first line of the password file are ignored +checking 20-pages.pdf +PDF Version: 1.4 +R = 3 +P = -4 +User password = user +Supplied password is user password +extract for accessibility: allowed +extract for any purpose: allowed +print low resolution: allowed +print high resolution: allowed +modify document assembly: allowed +modify forms: allowed +modify annotations: allowed +modify other: allowed +modify anything: allowed +File is not linearized +No syntax or stream encoding errors found; the file may still contain +errors that qpdf cannot detect diff --git a/qpdf/qtest/qpdf/20-pages-check.out b/qpdf/qtest/qpdf/20-pages-check.out new file mode 100644 index 00000000..6ba1075a --- /dev/null +++ b/qpdf/qtest/qpdf/20-pages-check.out @@ -0,0 +1,18 @@ +checking 20-pages.pdf +PDF Version: 1.4 +R = 3 +P = -4 +User password = user +Supplied password is user password +extract for accessibility: allowed +extract for any purpose: allowed +print low resolution: allowed +print high resolution: allowed +modify document assembly: allowed +modify forms: allowed +modify annotations: allowed +modify other: allowed +modify anything: allowed +File is not linearized +No syntax or stream encoding errors found; the file may still contain +errors that qpdf cannot detect