Implement new --encrypt args and completion (fixes #784)

Positional arguments are supported in a backward-compatible way, but
completion no longer guides users to it.
This commit is contained in:
Jay Berkenbilt 2023-12-22 18:14:11 -05:00
parent 1173a0bdfc
commit 7d7e2234a5
12 changed files with 228 additions and 122 deletions

View File

@ -1,5 +1,13 @@
2023-12-22 Jay Berkenbilt <ejb@ql.org>
* Allow the syntax "--encrypt --user-password=user-password
--owner-password=owner-password --bits={40,128,256}" when
encrypting PDF files. This is an alternative to the syntax
"--encrypt user-password owner-password {40,128,256}", which will
continue to be supported. The new syntax works better with shell
completion and allows creation of passwords that start with "-".
Fixes #874.
* When setting a check box value, allow any value other than /Off
to mean checked. This is permitted by the spec. Previously, any
value other than /Yes or /Off was rejected. Fixes #1056.

View File

@ -8,10 +8,10 @@ include/qpdf/auto_job_c_pages.hh b3cc0f21029f6d89efa043dcdbfa183cb59325b6506001c
include/qpdf/auto_job_c_uo.hh ae21b69a1efa9333050f4833d465f6daff87e5b38e5106e49bbef5d4132e4ed1
job.yml 4f89fc7b622df897d30d403d8035aa36fc7de8d8c43042c736e0300d904cb05c
libqpdf/qpdf/auto_job_decl.hh 9c6f701c29f3f764d620186bed92685a2edf2e4d11e4f4532862c05470cfc4d2
libqpdf/qpdf/auto_job_help.hh 788320d439519ecd284621531e96ee698965a9ad342fd423c5fb1de75d2a06b1
libqpdf/qpdf/auto_job_help.hh ea1fdca2aa405bdf193732c5a2789c602efe2add3aa6e2dceecfacee175ce65c
libqpdf/qpdf/auto_job_init.hh b4c2b3724fba61f1206fd3bae81951636852592f67a63ef9539839c2c5995065
libqpdf/qpdf/auto_job_json_decl.hh 06caa46eaf71db8a50c046f91866baa8087745a9474319fb7c86d92634cc8297
libqpdf/qpdf/auto_job_json_init.hh f5acb9aa103131cb68dec0e12c4d237a6459bdb49b24773c24f0c2724a462b8f
libqpdf/qpdf/auto_job_schema.hh b53c006fec2e75b1b73588d242d49a32f7d3db820b1541de106c5d4c27fbb4d9
manual/_ext/qpdf.py 6add6321666031d55ed4aedf7c00e5662bba856dfcd66ccb526563bffefbb580
manual/cli.rst b524f96f2a6f338f3e4350703598c56ba22e8f12a8efb7a441648c6dbf0a455e
manual/cli.rst 28cc6b36b26377404022bab467e6a16085023fdfa5d9d419595ffcae6c69d531

View File

@ -35,6 +35,9 @@ namespace
std::shared_ptr<QPDFJob::EncConfig> c_enc;
std::vector<std::string> accumulated_args;
std::shared_ptr<char> pages_password{nullptr};
std::string user_password;
std::string owner_password;
bool used_enc_password_args{false};
bool gave_input{false};
bool gave_output{false};
};
@ -161,40 +164,61 @@ void
ArgParser::argEncrypt()
{
this->accumulated_args.clear();
if (this->ap.isCompleting() && this->ap.argsLeft() == 0) {
this->ap.insertCompletion("user-password");
}
this->ap.selectOptionTable(O_ENCRYPTION);
}
void
ArgParser::argEncPositional(std::string const& arg)
{
if (used_enc_password_args) {
usage("positional and dashed encryption arguments may not be mixed");
}
this->accumulated_args.push_back(arg);
size_t n_args = this->accumulated_args.size();
if (n_args < 3) {
if (this->ap.isCompleting() && (this->ap.argsLeft() == 0)) {
if (n_args == 1) {
this->ap.insertCompletion("owner-password");
} else if (n_args == 2) {
this->ap.insertCompletion("40");
this->ap.insertCompletion("128");
this->ap.insertCompletion("256");
}
}
if (this->accumulated_args.size() < 3) {
return;
}
std::string user_password = this->accumulated_args.at(0);
std::string owner_password = this->accumulated_args.at(1);
std::string len_str = this->accumulated_args.at(2);
user_password = this->accumulated_args.at(0);
owner_password = this->accumulated_args.at(1);
auto len_str = this->accumulated_args.at(2);
this->accumulated_args.clear();
argEncBits(len_str);
}
void
ArgParser::argEncUserPassword(std::string const& arg)
{
if (!accumulated_args.empty()) {
usage("positional and dashed encryption arguments may not be mixed");
}
this->used_enc_password_args = true;
this->user_password = arg;
}
void
ArgParser::argEncOwnerPassword(std::string const& arg)
{
if (!accumulated_args.empty()) {
usage("positional and dashed encryption arguments may not be mixed");
}
this->used_enc_password_args = true;
this->owner_password = arg;
}
void
ArgParser::argEncBits(std::string const& arg)
{
if (!accumulated_args.empty()) {
usage("positional and dashed encryption arguments may not be mixed");
}
int keylen = 0;
if (len_str == "40") {
if (arg == "40") {
keylen = 40;
this->ap.selectOptionTable(O_40_BIT_ENCRYPTION);
} else if (len_str == "128") {
} else if (arg == "128") {
keylen = 128;
this->ap.selectOptionTable(O_128_BIT_ENCRYPTION);
} else if (len_str == "256") {
} else if (arg == "256") {
keylen = 256;
this->ap.selectOptionTable(O_256_BIT_ENCRYPTION);
} else {
@ -203,24 +227,6 @@ ArgParser::argEncPositional(std::string const& arg)
this->c_enc = c_main->encrypt(keylen, user_password, owner_password);
}
void
ArgParser::argEncUserPassword(std::string const& arg)
{
// QXXXQ
}
void
ArgParser::argEncOwnerPassword(std::string const& arg)
{
// QXXXQ
}
void
ArgParser::argEncBits(std::string const& arg)
{
// QXXXQ
}
void
ArgParser::argPages()
{

View File

@ -148,29 +148,14 @@ the structure without changing the content.
)");
ap.addOptionHelp("--linearize", "transformation", "linearize (web-optimize) output", R"(Create linearized (web-optimized) output files.
)");
ap.addOptionHelp("--encrypt", "transformation", "start encryption options", R"(--encrypt user-password owner-password key-length [options] --
ap.addOptionHelp("--encrypt", "transformation", "start encryption options", R"(--encrypt [options] --
Run qpdf --help=encryption for details.
)");
ap.addOptionHelp("--user-password", "transformation", "specify user password", R"(--user-password=user-password
Set the user password.
)");
ap.addOptionHelp("--owner-password", "transformation", "specify owner password", R"(--owner-password=owner-password
Set the owner password.
)");
ap.addOptionHelp("--bits", "transformation", "specify encryption bit depth", R"(--bits={48|128|256}
Set the encrypt bit depth. Use 256.
)");
ap.addOptionHelp("--decrypt", "transformation", "remove encryption from input file", R"(Create an unencrypted output file even if the input file was
encrypted. Normally qpdf preserves whatever encryption was
present on the input file. This option overrides that behavior.
)");
}
static void add_help_3(QPDFArgParser& ap)
{
ap.addOptionHelp("--remove-restrictions", "transformation", "remove security restrictions from input file", R"(Remove restrictions associated with digitally signed PDF files.
This may be combined with --decrypt to allow free editing of
previously signed/encrypted files. This option invalidates the
@ -187,6 +172,9 @@ ap.addOptionHelp("--encryption-file-password", "transformation", "supply passwor
If the file named in --copy-encryption requires a password, use
this option to supply the password.
)");
}
static void add_help_3(QPDFArgParser& ap)
{
ap.addOptionHelp("--qdf", "transformation", "enable viewing PDF code in a text editor", R"(Create a PDF file suitable for viewing in a text editor and even
editing. This is for editing the PDF code, not the page contents.
All streams that can be uncompressed are uncompressed, and
@ -282,9 +270,6 @@ ap.addOptionHelp("--ii-min-bytes", "transformation", "set minimum size for --ext
Don't externalize inline images smaller than this size. The
default is 1,024. Use 0 for no minimum.
)");
}
static void add_help_4(QPDFArgParser& ap)
{
ap.addOptionHelp("--min-version", "transformation", "set minimum PDF version", R"(--min-version=version
Force the PDF version of the output to be at least the specified
@ -312,6 +297,9 @@ resulting set of pages, where :odd starts with the first page and
:even starts with the second page. These are odd and even pages
from the resulting set, not based on the original page numbers.
)");
}
static void add_help_4(QPDFArgParser& ap)
{
ap.addHelpTopic("modification", "change parts of the PDF", R"(Modification options make systematic changes to certain parts of
the PDF, causing the PDF to render differently from the original.
)");
@ -404,18 +392,33 @@ ap.addOptionHelp("--keep-inline-images", "modification", "exclude inline images
)");
ap.addOptionHelp("--remove-page-labels", "modification", "remove explicit page numbers", R"(Exclude page labels (explicit page numbers) from the output file.
)");
}
static void add_help_5(QPDFArgParser& ap)
{
ap.addHelpTopic("encryption", "create encrypted files", R"(Create encrypted files. Usage:
--encrypt \
[--user-password=user-password] \
[--owner-password=owner-password] \
--bits=key-length [options] --
OR
--encrypt user-password owner-password key-length [options] --
Either or both of user-password and owner-password may be empty
strings, though setting either to the empty string enables the file
to be opened and decrypted without a password. key-length may be
40, 128, or 256. Encryption options are terminated by "--" by
itself.
The first form, with flags for the passwords and bit length, was
introduced in qpdf 11.7.0. Only the --bits option is is mandatory.
This form allows you to use any text as the password. If passwords
are specified, they must be given before the --bits option.
The second form has been in qpdf since the beginning and wil
continue to be supported. Either or both of user-password and
owner-password may be empty strings.
The key-length parameter must be either 40, 128, or 256. The user
and/or owner password may be omitted. Omitting either pasword
enables the PDF file to be opened without a password. Specifying
the same value for the user and owner password and specifying an
empty owner password are both considered insecure.
Encryption options are terminated by "--" by itself.
40-bit encryption is insecure, as is 128-bit encryption without
AES. Use 256-bit encryption unless you have a specific reason to
@ -468,6 +471,22 @@ Values for modify-opt:
annotate form + commenting and modifying forms
all allow full document modification
)");
ap.addOptionHelp("--user-password", "encryption", "specify user password", R"(--user-password=user-password
Set the user password of the encrypted file.
)");
ap.addOptionHelp("--owner-password", "encryption", "specify owner password", R"(--owner-password=owner-password
Set the owner password of the encrypted file.
)");
}
static void add_help_5(QPDFArgParser& ap)
{
ap.addOptionHelp("--bits", "encryption", "specify encryption key length", R"(--bits={48|128|256}
Specify the encryption key length. For best security, always use
a key length of 256.
)");
ap.addOptionHelp("--accessibility", "encryption", "restrict document accessibility", R"(--accessibility=[y|n]
This option is ignored except with very old encryption formats.

View File

@ -714,7 +714,7 @@ Related Options
important cross-reference information typically appears at the end
of the file.
.. qpdf:option:: --encrypt user-password owner-password key-length [options] --
.. qpdf:option:: --encrypt [options] --
.. help: start encryption options
@ -723,32 +723,6 @@ Related Options
This flag starts encryption options, used to create encrypted
files. Please see :ref:`encryption-options` for details.
.. qpdf:option:: --user-password=user-password
.. help: specify user password
Set the user password.
Set the user password for the encrypted file.
.. qpdf:option:: --owner-password=owner-password
.. help: specify owner password
Set the owner password.
Set the owner password for the encrypted file.
.. qpdf:option:: --bits={48|128|256}
.. help: specify encryption bit depth
Set the encrypt bit depth. Use 256.
Set the bit depth for encrypted files. You should always use
``--bits=256`` unless you have a strong reason to create a file
with weaker encryption.
.. qpdf:option:: --decrypt
.. help: remove encryption from input file
@ -1758,13 +1732,31 @@ Encryption
Create encrypted files. Usage:
--encrypt \
[--user-password=user-password] \
[--owner-password=owner-password] \
--bits=key-length [options] --
OR
--encrypt user-password owner-password key-length [options] --
Either or both of user-password and owner-password may be empty
strings, though setting either to the empty string enables the file
to be opened and decrypted without a password. key-length may be
40, 128, or 256. Encryption options are terminated by "--" by
itself.
The first form, with flags for the passwords and bit length, was
introduced in qpdf 11.7.0. Only the --bits option is is mandatory.
This form allows you to use any text as the password. If passwords
are specified, they must be given before the --bits option.
The second form has been in qpdf since the beginning and wil
continue to be supported. Either or both of user-password and
owner-password may be empty strings.
The key-length parameter must be either 40, 128, or 256. The user
and/or owner password may be omitted. Omitting either pasword
enables the PDF file to be opened without a password. Specifying
the same value for the user and owner password and specifying an
empty owner password are both considered insecure.
Encryption options are terminated by "--" by itself.
40-bit encryption is insecure, as is 128-bit encryption without
AES. Use 256-bit encryption unless you have a specific reason to
@ -1823,17 +1815,38 @@ and :qpdf:ref:`--copy-encryption`. For a more in-depth technical
discussion of how PDF encryption works internally, see
:ref:`pdf-encryption`.
To create an encrypted file, use
To create an encrypted file, use one of
::
--encrypt \
[--user-password=user-password] \
[--owner-password=owner-password] \
--bits=key-length [options] --
OR
::
--encrypt user-password owner-password key-length [options] --
Either or both of :samp:`{user-password}` and :samp:`{owner-password}`
may be empty strings, though setting either to the empty string
enables the file to be opened and decrypted without a password..
:samp:`{key-length}` may be ``40``, ``128``, or ``256``. Encryption
options are terminated by ``--`` by itself.
The first form, with flags for the passwords and bit length, was
introduced in qpdf 11.7.0. Only the :qpdf:ref:`--bits` option is is
mandatory. This form allows you to use any text as the password. If
passwords are specified, they must be given before the
:qpdf:ref:`--bits` option.
The second form has been in qpdf since the beginning and wil
continue to be supported. Either or both of user-password and
owner-password may be empty strings.
The ``key-length`` parameter must be either ``40``, ``128``, or
``256``. The user and/or owner password may be omitted. Omitting
either pasword enables the PDF file to be opened without a password.
Specifying the same value for the user and owner password and
specifying an empty owner password are both considered insecure.
Encryption options are terminated by ``--`` by itself.
40-bit encryption is insecure, as is 128-bit encryption without AES.
Use 256-bit encryption unless you have a specific reason to use an
@ -1971,6 +1984,36 @@ help for each option.
Related Options
~~~~~~~~~~~~~~~
.. qpdf:option:: --user-password=user-password
.. help: specify user password
Set the user password of the encrypted file.
Set the user passwrod of the encrypted file. Conforming readers
apply security restrictions to files opened with the user password.
.. qpdf:option:: --owner-password=owner-password
.. help: specify owner password
Set the owner password of the encrypted file.
Set the owner passwrod of the encrypted file. Conforming readers
apply allow security restrictions to be changed or overridden when
files are opened with the owner password.
.. qpdf:option:: --bits={48|128|256}
.. help: specify encryption key length
Specify the encryption key length. For best security, always use
a key length of 256.
Set the key length for encrypted files. You should always use
``--bits=256`` unless you have a strong reason to create a file
with weaker encryption.
.. qpdf:option:: --accessibility=[y|n]
.. help: restrict document accessibility

View File

@ -61,6 +61,16 @@ Planned changes for future 12.x (subject to change):
Previously, any value other than ``/Yes`` or ``/Off`` was
rejected.
- CLI Enhancements:
- Allow the syntax ``--encrypt --user-password=user-password
--owner-password=owner-password --bits={40,128,256}`` when
encrypting PDF files. This is an alternative to the syntax
``--encrypt user-password owner-password {40,128,256}``, which
will continue to be supported. The new syntax works better with
shell completion and allows creation of passwords that start
with ``-``.
- Build Enhancements:
- The qpdf test suite now passes when qpdf is linked with an

View File

@ -15,7 +15,7 @@ cleanup();
my $td = new TestDriver('arg-parsing');
my $n_tests = 17;
my $n_tests = 21;
$td->runtest("required argument",
{$td->COMMAND => "qpdf --password minimal.pdf"},
@ -108,5 +108,21 @@ $td->runtest("empty and replace-input",
$td->EXIT_STATUS => 2},
$td->NORMALIZE_NEWLINES);
# Disallow mixing positional and flag-style encryption arguments.
my @bad_enc = (
"u --owner-password=x",
"u o --bits=128",
"--user-password=u o",
"--user-password=u --owner-password=o 256",
);
foreach my $arg (@bad_enc)
{
$td->runtest("mixed encryption args ($arg)",
{$td->COMMAND => "qpdf --encrypt $arg"},
{$td->REGEXP => ".*positional and dashed encryption arguments may not be mixed",
$td->EXIT_STATUS => 2},
$td->NORMALIZE_NEWLINES);
}
cleanup();
$td->report($n_tests);

View File

@ -29,14 +29,7 @@ my @completion_tests = (
['qpdf ', undef, 'top'],
['qpdf -', undef, 'top-arg'],
['qpdf --enc', undef, 'enc'],
['qpdf --encrypt ', undef, 'encrypt'],
['qpdf --encrypt u ', undef, 'encrypt-u'],
['qpdf --encrypt u o ', undef, 'encrypt-u-o'],
['qpdf @encrypt-u o ', undef, 'encrypt-u-o'],
['qpdf --encrypt u o 40 --', undef, 'encrypt-40'],
['qpdf --encrypt u o 128 --', undef, 'encrypt-128'],
['qpdf --encrypt u o 256 --', undef, 'encrypt-256'],
['qpdf --encrypt u o bad --', undef, 'encrypt-bad'],
['qpdf --encrypt -', undef, 'encrypt'],
['qpdf --split-pag', undef, 'split'],
['qpdf --decode-l', undef, 'decode-l'],
['qpdf --decode-lzzz', 15, 'decode-l'],
@ -44,11 +37,11 @@ my @completion_tests = (
['qpdf --decode-level=g', undef, 'decode-level-g'],
['qpdf --check -', undef, 'later-arg'],
['qpdf infile outfile oops --ch', undef, 'usage-empty'],
['qpdf --encrypt \'user " password\' ', undef, 'quoting'],
['qpdf --encrypt \'user password\' ', undef, 'quoting'],
['qpdf --encrypt "user password" ', undef, 'quoting'],
['qpdf --encrypt "user pass\'word" ', undef, 'quoting'],
['qpdf --encrypt user\ password ', undef, 'quoting'],
['qpdf \'input " file\' --q', undef, 'quoting'],
['qpdf \'input file\' --q', undef, 'quoting'],
['qpdf "input file" --q', undef, 'quoting'],
['qpdf "input fi\'le" --q', undef, 'quoting'],
['qpdf input\ file --q', undef, 'quoting'],
);
my $n_tests = 2 * scalar(@completion_tests);
my $completion_filter =

View File

@ -0,0 +1,6 @@
--bits=128
--bits=256
--bits=40
--owner-password=
--user-password=
!--print

View File

@ -1,2 +1,7 @@
user-password
--bits=
--owner-password=
--user-password=
!--bits=128
!--bits=256
!--bits=40
!--print

View File

@ -1 +1 @@
owner-password
--qdf

View File

@ -1,2 +1,2 @@
--encrypt
u
--bits=2