aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--ChangeLog8
-rw-r--r--job.sums4
-rw-r--r--libqpdf/QPDFJob_argv.cc74
-rw-r--r--libqpdf/qpdf/auto_job_help.hh73
-rw-r--r--manual/cli.rst119
-rw-r--r--manual/release-notes.rst10
-rw-r--r--qpdf/qtest/arg-parsing.test18
-rw-r--r--qpdf/qtest/completion.test19
-rw-r--r--qpdf/qtest/qpdf/completion-encrypt-zsh.out6
-rw-r--r--qpdf/qtest/qpdf/completion-encrypt.out7
-rw-r--r--qpdf/qtest/qpdf/completion-quoting.out2
-rw-r--r--qpdf/qtest/qpdf/encrypt-u2
12 files changed, 224 insertions, 118 deletions
diff --git a/ChangeLog b/ChangeLog
index b18283cd..8ccb689c 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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.
diff --git a/job.sums b/job.sums
index 43318803..46acb1c6 100644
--- a/job.sums
+++ b/job.sums
@@ -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
diff --git a/libqpdf/QPDFJob_argv.cc b/libqpdf/QPDFJob_argv.cc
index adf7ba64..56acd7a9 100644
--- a/libqpdf/QPDFJob_argv.cc
+++ b/libqpdf/QPDFJob_argv.cc
@@ -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,64 +164,67 @@ 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);
- int keylen = 0;
- if (len_str == "40") {
- keylen = 40;
- this->ap.selectOptionTable(O_40_BIT_ENCRYPTION);
- } else if (len_str == "128") {
- keylen = 128;
- this->ap.selectOptionTable(O_128_BIT_ENCRYPTION);
- } else if (len_str == "256") {
- keylen = 256;
- this->ap.selectOptionTable(O_256_BIT_ENCRYPTION);
- } else {
- usage("encryption key length must be 40, 128, or 256");
- }
- this->c_enc = c_main->encrypt(keylen, user_password, owner_password);
+ 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)
{
- // QXXXQ
+ 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)
{
- // QXXXQ
+ 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)
{
- // QXXXQ
+ if (!accumulated_args.empty()) {
+ usage("positional and dashed encryption arguments may not be mixed");
+ }
+ int keylen = 0;
+ if (arg == "40") {
+ keylen = 40;
+ this->ap.selectOptionTable(O_40_BIT_ENCRYPTION);
+ } else if (arg == "128") {
+ keylen = 128;
+ this->ap.selectOptionTable(O_128_BIT_ENCRYPTION);
+ } else if (arg == "256") {
+ keylen = 256;
+ this->ap.selectOptionTable(O_256_BIT_ENCRYPTION);
+ } else {
+ usage("encryption key length must be 40, 128, or 256");
+ }
+ this->c_enc = c_main->encrypt(keylen, user_password, owner_password);
}
void
diff --git a/libqpdf/qpdf/auto_job_help.hh b/libqpdf/qpdf/auto_job_help.hh
index 9f7cadfe..b96c2564 100644
--- a/libqpdf/qpdf/auto_job_help.hh
+++ b/libqpdf/qpdf/auto_job_help.hh
@@ -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.
diff --git a/manual/cli.rst b/manual/cli.rst
index 943b404e..9f173ed8 100644
--- a/manual/cli.rst
+++ b/manual/cli.rst
@@ -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
diff --git a/manual/release-notes.rst b/manual/release-notes.rst
index 7aa625ab..50b0aec3 100644
--- a/manual/release-notes.rst
+++ b/manual/release-notes.rst
@@ -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
diff --git a/qpdf/qtest/arg-parsing.test b/qpdf/qtest/arg-parsing.test
index ad6bc0d2..2f6bf621 100644
--- a/qpdf/qtest/arg-parsing.test
+++ b/qpdf/qtest/arg-parsing.test
@@ -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);
diff --git a/qpdf/qtest/completion.test b/qpdf/qtest/completion.test
index cb11fd3f..4d0fcd5d 100644
--- a/qpdf/qtest/completion.test
+++ b/qpdf/qtest/completion.test
@@ -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 =
diff --git a/qpdf/qtest/qpdf/completion-encrypt-zsh.out b/qpdf/qtest/qpdf/completion-encrypt-zsh.out
new file mode 100644
index 00000000..45f12a69
--- /dev/null
+++ b/qpdf/qtest/qpdf/completion-encrypt-zsh.out
@@ -0,0 +1,6 @@
+--bits=128
+--bits=256
+--bits=40
+--owner-password=
+--user-password=
+!--print
diff --git a/qpdf/qtest/qpdf/completion-encrypt.out b/qpdf/qtest/qpdf/completion-encrypt.out
index 4577a128..c9ccf27b 100644
--- a/qpdf/qtest/qpdf/completion-encrypt.out
+++ b/qpdf/qtest/qpdf/completion-encrypt.out
@@ -1,2 +1,7 @@
-user-password
+--bits=
+--owner-password=
+--user-password=
+!--bits=128
+!--bits=256
+!--bits=40
!--print
diff --git a/qpdf/qtest/qpdf/completion-quoting.out b/qpdf/qtest/qpdf/completion-quoting.out
index 0d8a0622..1d2dfbe8 100644
--- a/qpdf/qtest/qpdf/completion-quoting.out
+++ b/qpdf/qtest/qpdf/completion-quoting.out
@@ -1 +1 @@
-owner-password
+--qdf
diff --git a/qpdf/qtest/qpdf/encrypt-u b/qpdf/qtest/qpdf/encrypt-u
index 9d413960..b9581e93 100644
--- a/qpdf/qtest/qpdf/encrypt-u
+++ b/qpdf/qtest/qpdf/encrypt-u
@@ -1,2 +1,2 @@
--encrypt
-u
+--bits=2