aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ChangeLog10
-rw-r--r--manual/qpdf-manual.xml51
-rw-r--r--qpdf/qpdf.cc82
-rw-r--r--qpdf/qtest/qpdf.test22
4 files changed, 161 insertions, 4 deletions
diff --git a/ChangeLog b/ChangeLog
index 8d93eaee..254c589e 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,3 +1,13 @@
+2020-01-26 Jay Berkenbilt <ejb@ql.org>
+
+ * Add options --is-encrypted and --requires-password. These can be
+ used with files, including encrypted files with unknown passwords,
+ to determine whether or not a file is encrypted and whether a
+ password is required to open the file. The --requires-password
+ option can also be used to determine whether a supplied password
+ is correct. Information is supplied through exit codes, making
+ these options particularly useful for shell scripts. Fixes #390.
+
2020-01-14 Jay Berkenbilt <ejb@ql.org>
* Fix for Windows being unable to acquire crypt context with a new
diff --git a/manual/qpdf-manual.xml b/manual/qpdf-manual.xml
index 317ce04a..3f455d83 100644
--- a/manual/qpdf-manual.xml
+++ b/manual/qpdf-manual.xml
@@ -666,6 +666,40 @@ make
</listitem>
</varlistentry>
<varlistentry>
+ <term><option>--is-encrypted</option></term>
+ <listitem>
+ <para>
+ Silently exit with status 0 if the file is encrypted or status
+ 2 if the file is not encrypted. This is useful for shell
+ scripts. Other options are ignored if this is given. This
+ option is mutually exclusive with
+ <option>--requires-password</option>. Both this option and
+ <option>--requires-password</option> exit with status 2 for
+ non-encrypted files.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
+ <term><option>--requires-password</option></term>
+ <listitem>
+ <para>
+ Silently exit with status 0 if a password (other than as
+ supplied) is required. Exit with status 2 if the file is not
+ encrypted. Exit with status 3 if the file is encrypted but
+ requires no password or the correct password has been
+ supplied. This is useful for shell scripts. Note that any
+ supplied password is used when opening the file. When used
+ with a <option>--password</option> option, this option can be
+ used to check the correctness of the password. In that case,
+ an exit status of 3 means the file works with the supplied
+ password. This option is mutually exclusive with
+ <option>--is-encrypted</option>. Both this option and
+ <option>--is-encrypted</option> exit with status 2 for
+ non-encrypted files.
+ </para>
+ </listitem>
+ </varlistentry>
+ <varlistentry>
<term><option>--verbose</option></term>
<listitem>
<para>
@@ -4675,6 +4709,23 @@ print "\n";
</listitem>
</itemizedlist>
</listitem>
+ <listitem>
+ <para>
+ CLI Enhancements
+ </para>
+ <itemizedlist>
+ <listitem>
+ <para>
+ Added options <option>--is-encrypted</option> and
+ <option>--requires-password</option> for testing whether a
+ file is encrypted or requires a password other than the
+ supplied (or empty) password. These communicate via exit
+ status, making them useful for shell scripts. They also work
+ on encrypted files with unknown passwords.
+ </para>
+ </listitem>
+ </itemizedlist>
+ </listitem>
</itemizedlist>
</listitem>
</varlistentry>
diff --git a/qpdf/qpdf.cc b/qpdf/qpdf.cc
index 2139d8b2..0ea32b8c 100644
--- a/qpdf/qpdf.cc
+++ b/qpdf/qpdf.cc
@@ -29,8 +29,12 @@
#include <qpdf/QPDFWriter.hh>
#include <qpdf/QIntC.hh>
-static int const EXIT_ERROR = 2;
-static int const EXIT_WARNING = 3;
+static int constexpr EXIT_ERROR = 2;
+static int constexpr EXIT_WARNING = 3;
+
+// For is-encrypted and requires-password
+static int constexpr EXIT_IS_NOT_ENCRYPTED = 2;
+static int constexpr EXIT_CORRECT_PASSWORD = 3;
static char const* whoami = 0;
@@ -183,6 +187,8 @@ struct Options
under_overlay(0),
require_outfile(true),
replace_input(false),
+ check_is_encrypted(false),
+ check_requires_password(false),
infilename(0),
outfilename(0)
{
@@ -287,6 +293,8 @@ struct Options
std::map<std::string, RotationSpec> rotations;
bool require_outfile;
bool replace_input;
+ bool check_is_encrypted;
+ bool check_requires_password;
char const* infilename;
char const* outfilename;
};
@@ -718,6 +726,8 @@ class ArgParser
void argUOpassword(char* parameter);
void argEndUnderOverlay();
void argReplaceInput();
+ void argIsEncrypted();
+ void argRequiresPassword();
void usage(std::string const& message);
void checkCompletion();
@@ -948,6 +958,8 @@ ArgParser::initOptionTable()
(*t)["overlay"] = oe_bare(&ArgParser::argOverlay);
(*t)["underlay"] = oe_bare(&ArgParser::argUnderlay);
(*t)["replace-input"] = oe_bare(&ArgParser::argReplaceInput);
+ (*t)["is-encrypted"] = oe_bare(&ArgParser::argIsEncrypted);
+ (*t)["requires-password"] = oe_bare(&ArgParser::argRequiresPassword);
t = &this->encrypt40_option_table;
(*t)["--"] = oe_bare(&ArgParser::argEndEncrypt);
@@ -1105,6 +1117,13 @@ 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"
+ << "--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"
+ << " supplied) is required, 2 if the file is not\n"
+ << " encrypted, or 3 if the file is encrypted\n"
+ << " but requires no password or the supplied password\n"
+ << " is correct; useful for shell scripts\n"
<< "--verbose provide additional informational output\n"
<< "--progress give progress indicators while writing output\n"
<< "--no-warn suppress warnings\n"
@@ -2353,6 +2372,20 @@ ArgParser::argReplaceInput()
}
void
+ArgParser::argIsEncrypted()
+{
+ o.check_is_encrypted = true;
+ o.require_outfile = false;
+}
+
+void
+ArgParser::argRequiresPassword()
+{
+ o.check_requires_password = true;
+ o.require_outfile = false;
+}
+
+void
ArgParser::handleArgFileArguments()
{
// Support reading arguments from files. Create a new argv. Ensure
@@ -3113,6 +3146,11 @@ ArgParser::doFinalChecks()
{
o.externalize_inline_images = true;
}
+ if (o.check_requires_password && o.check_is_encrypted)
+ {
+ usage("--requires-password and --is-encrypted may not be given"
+ " together");
+ }
if (o.require_outfile && o.outfilename &&
(strcmp(o.outfilename, "-") == 0))
@@ -5252,9 +5290,45 @@ int realmain(int argc, char* argv[])
try
{
ap.parseOptions();
- PointerHolder<QPDF> pdf_ph =
- process_file(o.infilename, o.password, o);
+ PointerHolder<QPDF> pdf_ph;
+ try
+ {
+ pdf_ph = process_file(o.infilename, o.password, o);
+ }
+ catch (QPDFExc& e)
+ {
+ if ((e.getErrorCode() == qpdf_e_password) &&
+ (o.check_is_encrypted || o.check_requires_password))
+ {
+ // Allow --is-encrypted and --requires-password to
+ // work when an incorrect password is supplied.
+ exit(0);
+ }
+ throw e;
+ }
QPDF& pdf = *pdf_ph;
+ if (o.check_is_encrypted)
+ {
+ if (pdf.isEncrypted())
+ {
+ exit(0);
+ }
+ else
+ {
+ exit(EXIT_IS_NOT_ENCRYPTED);
+ }
+ }
+ else if (o.check_requires_password)
+ {
+ if (pdf.isEncrypted())
+ {
+ exit(EXIT_CORRECT_PASSWORD);
+ }
+ else
+ {
+ exit(EXIT_IS_NOT_ENCRYPTED);
+ }
+ }
if (! o.page_specs.empty())
{
handle_page_specs(pdf, o);
diff --git a/qpdf/qtest/qpdf.test b/qpdf/qtest/qpdf.test
index 7ea329c2..78184971 100644
--- a/qpdf/qtest/qpdf.test
+++ b/qpdf/qtest/qpdf.test
@@ -252,6 +252,28 @@ $td->runtest("check exception handling",
show_ntests();
# ----------
+$td->notify("--- Check encryption/password ---");
+my @check_encryption_password = (
+ # file, password, is-encrypted, requires-password
+ ["minimal.pdf", "", 2, 2],
+ ["20-pages.pdf", "", 0, 0],
+ ["20-pages.pdf", "user", 0, 3],
+ );
+$n_tests += 2 * scalar(@check_encryption_password);
+foreach my $d (@check_encryption_password)
+{
+ my ($file, $pass, $is_encrypted, $requires_password) = @$d;
+ $td->runtest("is encrypted ($file, pass=$pass)",
+ {$td->COMMAND => "qpdf --is-encrypted --password=$pass $file"},
+ {$td->STRING => "", $td->EXIT_STATUS => $is_encrypted});
+ $td->runtest("requires password ($file, pass=$pass)",
+ {$td->COMMAND => "qpdf --requires-password" .
+ " --password=$pass $file"},
+ {$td->STRING => "", $td->EXIT_STATUS => $requires_password});
+}
+
+show_ntests();
+# ----------
$td->notify("--- Dangling Refs ---");
my @dangling = (qw(minimal dangling-refs));
$n_tests += 2 * scalar(@dangling);