Add --is-encrypted and --requires-password (fixes #390)

Allow exit status-based checking of whether a file is encrypted or
requires a password without necessarily supplying the correct
password. Useful for scripting.
This commit is contained in:
Jay Berkenbilt 2020-01-26 09:19:50 -05:00
parent 8b1c4828b7
commit 731c4f711b
4 changed files with 161 additions and 4 deletions

View File

@ -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

View File

@ -665,6 +665,40 @@ make
</para>
</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>
@ -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>

View File

@ -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"
@ -2352,6 +2371,20 @@ ArgParser::argReplaceInput()
o.replace_input = true;
}
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()
{
@ -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);

View File

@ -250,6 +250,28 @@ $td->runtest("check exception handling",
{$td->FILE => "exceptions.out", $td->EXIT_STATUS => 0},
$td->NORMALIZE_NEWLINES);
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 ---");