diff --git a/ChangeLog b/ChangeLog index b18283cd..8ccb689c 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,13 @@ 2023-12-22 Jay Berkenbilt + * 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 c_enc; std::vector accumulated_args; std::shared_ptr 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() { 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