Commit 750aca5b94351f730fa768b07caa3fc26c8d27c0

Authored by Jay Berkenbilt
1 parent 37916f39

First increment of improving handling of weak crypto (fixes #358)

ChangeLog
  1 +2021-11-10 Jay Berkenbilt <ejb@ql.org>
  2 +
  3 + * Add --allow-weak-crypto option to suppress warnings about use of
  4 + weak cryptographic algorithms. Update documentation around this
  5 + issue. Fixes #358.
  6 +
1 2021-11-07 Jay Berkenbilt <ejb@ql.org> 7 2021-11-07 Jay Berkenbilt <ejb@ql.org>
2 8
3 * Relax xref recovery logic a bit so that files whose objects are 9 * Relax xref recovery logic a bit so that files whose objects are
README.md
@@ -62,6 +62,10 @@ For example, if you want to guarantee that the GnuTLS crypto provider is used, y @@ -62,6 +62,10 @@ For example, if you want to guarantee that the GnuTLS crypto provider is used, y
62 62
63 Please see the section on crypto providers in the manual for more details. 63 Please see the section on crypto providers in the manual for more details.
64 64
  65 +## Note about weak cryptographic algorithms
  66 +
  67 +The PDF file format used to rely on RC4 for encryption. Using 256-bit keys always uses AES instead, and with 128-bit keys, you can elect to use AES. qpdf does its best to warn when someone is writing a file with weak cryptographic algorithms, but qpdf must always retain support for being able to read and even write files with weak encryption to be able to fully support older PDF files and older PDF readers.
  68 +
65 # Building from a pristine checkout 69 # Building from a pristine checkout
66 70
67 When building qpdf from a pristine checkout from version control, generated documentation files are not present. You may either generate them (by passing `--enable-doc-maintenance` to `./configure` and satisfying the extra build-time dependencies) or obtain them from a released source package, which includes them. If you want to grab just the files that are in the source distribution but not in the repository, extract a source distribution in a temporary directory, and run `make CLEAN=1 distfiles.zip`. This will create a file called `distfiles.zip`, which can you can extract in a checkout of the source repository. This step is optional unless you are running make install and want the html and PDF versions of the documentation to be installed. 71 When building qpdf from a pristine checkout from version control, generated documentation files are not present. You may either generate them (by passing `--enable-doc-maintenance` to `./configure` and satisfying the extra build-time dependencies) or obtain them from a released source package, which includes them. If you want to grab just the files that are in the source distribution but not in the repository, extract a source distribution in a temporary directory, and run `make CLEAN=1 distfiles.zip`. This will create a file called `distfiles.zip`, which can you can extract in a checkout of the source repository. This step is optional unless you are running make install and want the html and PDF versions of the documentation to be installed.
@@ -187,6 +187,23 @@ Comments appear in the code prefixed by &quot;ABI&quot; @@ -187,6 +187,23 @@ Comments appear in the code prefixed by &quot;ABI&quot;
187 before copying, though maybe we don't because it could cause 187 before copying, though maybe we don't because it could cause
188 multiple copies to be made...usually it's better to handle that 188 multiple copies to be made...usually it's better to handle that
189 explicitly. 189 explicitly.
  190 +* Deal with weak cryptographic algorithms:
  191 + * Github issue #576
  192 + * Add something to QPDFWriter that you must call in order to allow
  193 + creation of files with insecure crypto. Maybe
  194 + QPDFWriter::allowWeakCrypto. Call this when --allow-weak-crypto is
  195 + passed and probably also when copying encryption by default from
  196 + an input file.
  197 + * Change deterministic id to use something other than MD5 but allow
  198 + the old way for compatibility -- maybe rename the method to force
  199 + the developer to make a choice
  200 + * Find other uses of MD5 and find the ones that are discretionary,
  201 + if any
  202 + * Have QPDFWriter raise an exception if it's about to write using
  203 + weak crypto and hasn't been given permission
  204 + * Search for --allow-weak-crypto in the manual and in qpdf.cc's help
  205 + information
  206 + * Update the ref.weak-crypto section of the manual
190 207
191 Page splitting/merging 208 Page splitting/merging
192 ====================== 209 ======================
fuzz/qpdf_fuzzer.cc
@@ -107,7 +107,7 @@ FuzzHelper::testWrite() @@ -107,7 +107,7 @@ FuzzHelper::testWrite()
107 w->setStaticID(true); 107 w->setStaticID(true);
108 w->setObjectStreamMode(qpdf_o_disable); 108 w->setObjectStreamMode(qpdf_o_disable);
109 w->setR3EncryptionParameters( 109 w->setR3EncryptionParameters(
110 - "u", "o", true, true, qpdf_r3p_full, qpdf_r3m_all); 110 + "u", "o", true, true, true, true, true, true, qpdf_r3p_full);
111 doWrite(w); 111 doWrite(w);
112 112
113 q = getQpdf(); 113 q = getQpdf();
include/qpdf/QPDFCryptoImpl.hh
@@ -69,6 +69,9 @@ class QPDF_DLL_CLASS QPDFCryptoImpl @@ -69,6 +69,9 @@ class QPDF_DLL_CLASS QPDFCryptoImpl
69 69
70 // Encryption/Decryption 70 // Encryption/Decryption
71 71
  72 + // QPDF must support RC4 to be able to work with older PDF files
  73 + // and readers. Search for RC4 in README.md
  74 +
72 // key_len of -1 means treat key_data as a null-terminated string 75 // key_len of -1 means treat key_data as a null-terminated string
73 QPDF_DLL 76 QPDF_DLL
74 virtual void RC4_init(unsigned char const* key_data, int key_len = -1) = 0; 77 virtual void RC4_init(unsigned char const* key_data, int key_len = -1) = 0;
include/qpdf/QPDFWriter.hh
@@ -359,6 +359,16 @@ class QPDFWriter @@ -359,6 +359,16 @@ class QPDFWriter
359 // this from your own application, QUtil contains many transcoding 359 // this from your own application, QUtil contains many transcoding
360 // functions that could be useful to you, most notably 360 // functions that could be useful to you, most notably
361 // utf8_to_pdf_doc. 361 // utf8_to_pdf_doc.
  362 +
  363 + // R3 uses RC4, which is a weak cryptographic algorithm. Don't use
  364 + // it unless you have to.
  365 + QPDF_DLL
  366 + void setR2EncryptionParameters(
  367 + char const* user_password, char const* owner_password,
  368 + bool allow_print, bool allow_modify,
  369 + bool allow_extract, bool allow_annotate);
  370 + // R3 uses RC4, which is a weak cryptographic algorithm. Don't use
  371 + // it unless you have to.
362 QPDF_DLL 372 QPDF_DLL
363 void setR3EncryptionParameters( 373 void setR3EncryptionParameters(
364 char const* user_password, char const* owner_password, 374 char const* user_password, char const* owner_password,
@@ -366,6 +376,8 @@ class QPDFWriter @@ -366,6 +376,8 @@ class QPDFWriter
366 bool allow_assemble, bool allow_annotate_and_form, 376 bool allow_assemble, bool allow_annotate_and_form,
367 bool allow_form_filling, bool allow_modify_other, 377 bool allow_form_filling, bool allow_modify_other,
368 qpdf_r3_print_e print); 378 qpdf_r3_print_e print);
  379 + // R4 uses RC4, which is a weak cryptographic algorithm, when
  380 + // use_aes=false. Don't use it unless you have to.
369 QPDF_DLL 381 QPDF_DLL
370 void setR4EncryptionParameters( 382 void setR4EncryptionParameters(
371 char const* user_password, char const* owner_password, 383 char const* user_password, char const* owner_password,
@@ -392,28 +404,27 @@ class QPDFWriter @@ -392,28 +404,27 @@ class QPDFWriter
392 qpdf_r3_print_e print, bool encrypt_metadata_aes); 404 qpdf_r3_print_e print, bool encrypt_metadata_aes);
393 405
394 // Pre qpdf 8.4.0 API 406 // Pre qpdf 8.4.0 API
395 - QPDF_DLL  
396 - void setR2EncryptionParameters(  
397 - char const* user_password, char const* owner_password,  
398 - bool allow_print, bool allow_modify,  
399 - bool allow_extract, bool allow_annotate); 407 + [[deprecated("see newer API above")]]
400 QPDF_DLL 408 QPDF_DLL
401 void setR3EncryptionParameters( 409 void setR3EncryptionParameters(
402 char const* user_password, char const* owner_password, 410 char const* user_password, char const* owner_password,
403 bool allow_accessibility, bool allow_extract, 411 bool allow_accessibility, bool allow_extract,
404 qpdf_r3_print_e print, qpdf_r3_modify_e modify); 412 qpdf_r3_print_e print, qpdf_r3_modify_e modify);
  413 + [[deprecated("see newer API above")]]
405 QPDF_DLL 414 QPDF_DLL
406 void setR4EncryptionParameters( 415 void setR4EncryptionParameters(
407 char const* user_password, char const* owner_password, 416 char const* user_password, char const* owner_password,
408 bool allow_accessibility, bool allow_extract, 417 bool allow_accessibility, bool allow_extract,
409 qpdf_r3_print_e print, qpdf_r3_modify_e modify, 418 qpdf_r3_print_e print, qpdf_r3_modify_e modify,
410 bool encrypt_metadata, bool use_aes); 419 bool encrypt_metadata, bool use_aes);
  420 + [[deprecated("see newer API above")]]
411 QPDF_DLL 421 QPDF_DLL
412 void setR5EncryptionParameters( 422 void setR5EncryptionParameters(
413 char const* user_password, char const* owner_password, 423 char const* user_password, char const* owner_password,
414 bool allow_accessibility, bool allow_extract, 424 bool allow_accessibility, bool allow_extract,
415 qpdf_r3_print_e print, qpdf_r3_modify_e modify, 425 qpdf_r3_print_e print, qpdf_r3_modify_e modify,
416 bool encrypt_metadata); 426 bool encrypt_metadata);
  427 + [[deprecated("see newer API above")]]
417 QPDF_DLL 428 QPDF_DLL
418 void setR6EncryptionParameters( 429 void setR6EncryptionParameters(
419 char const* user_password, char const* owner_password, 430 char const* user_password, char const* owner_password,
lgtm.yml 0 โ†’ 100644
  1 +# See https://lgtm.com/help/lgtm/lgtm.yml-configuration-file
  2 +
  3 +# Suppress alerts for weak cryptographic algorithms. The PDF file
  4 +# format used to rely on various weak algorithms. It is no longer
  5 +# necessary to use them when creating encrypted PDF files, but qpdf
  6 +# must always retain support for those algorithms in order to be able
  7 +# to read older files. qpdf issues warnings to try to prevent users
  8 +# from creating new files with weak encryption algorithms.
  9 +
  10 +queries:
  11 +- exclude: cpp/weak-cryptographic-algorithm
libqpdf/Pl_RC4.cc
@@ -34,6 +34,7 @@ Pl_RC4::write(unsigned char* data, size_t len) @@ -34,6 +34,7 @@ Pl_RC4::write(unsigned char* data, size_t len)
34 size_t bytes = 34 size_t bytes =
35 (bytes_left < this->out_bufsize ? bytes_left : out_bufsize); 35 (bytes_left < this->out_bufsize ? bytes_left : out_bufsize);
36 bytes_left -= bytes; 36 bytes_left -= bytes;
  37 + // lgtm[cpp/weak-cryptographic-algorithm]
37 rc4.process(p, bytes, outbuf.getPointer()); 38 rc4.process(p, bytes, outbuf.getPointer());
38 p += bytes; 39 p += bytes;
39 getNext()->write(outbuf.getPointer(), bytes); 40 getNext()->write(outbuf.getPointer(), bytes);
libqpdf/qpdf-c.cc
@@ -702,10 +702,20 @@ void qpdf_set_r3_encryption_parameters( @@ -702,10 +702,20 @@ void qpdf_set_r3_encryption_parameters(
702 QPDF_BOOL allow_accessibility, QPDF_BOOL allow_extract, 702 QPDF_BOOL allow_accessibility, QPDF_BOOL allow_extract,
703 qpdf_r3_print_e print, qpdf_r3_modify_e modify) 703 qpdf_r3_print_e print, qpdf_r3_modify_e modify)
704 { 704 {
  705 +#ifdef _MSC_VER
  706 +# pragma warning (disable: 4996)
  707 +#endif
  708 +#if (defined(__GNUC__) || defined(__clang__))
  709 +# pragma GCC diagnostic push
  710 +# pragma GCC diagnostic ignored "-Wdeprecated-declarations"
  711 +#endif
705 qpdf->qpdf_writer->setR3EncryptionParameters( 712 qpdf->qpdf_writer->setR3EncryptionParameters(
706 user_password, owner_password, 713 user_password, owner_password,
707 allow_accessibility != QPDF_FALSE, allow_extract != QPDF_FALSE, 714 allow_accessibility != QPDF_FALSE, allow_extract != QPDF_FALSE,
708 print, modify); 715 print, modify);
  716 +#if (defined(__GNUC__) || defined(__clang__))
  717 +# pragma GCC diagnostic pop
  718 +#endif
709 } 719 }
710 720
711 void qpdf_set_r4_encryption_parameters( 721 void qpdf_set_r4_encryption_parameters(
@@ -714,11 +724,21 @@ void qpdf_set_r4_encryption_parameters( @@ -714,11 +724,21 @@ void qpdf_set_r4_encryption_parameters(
714 qpdf_r3_print_e print, qpdf_r3_modify_e modify, 724 qpdf_r3_print_e print, qpdf_r3_modify_e modify,
715 QPDF_BOOL encrypt_metadata, QPDF_BOOL use_aes) 725 QPDF_BOOL encrypt_metadata, QPDF_BOOL use_aes)
716 { 726 {
  727 +#ifdef _MSC_VER
  728 +# pragma warning (disable: 4996)
  729 +#endif
  730 +#if (defined(__GNUC__) || defined(__clang__))
  731 +# pragma GCC diagnostic push
  732 +# pragma GCC diagnostic ignored "-Wdeprecated-declarations"
  733 +#endif
717 qpdf->qpdf_writer->setR4EncryptionParameters( 734 qpdf->qpdf_writer->setR4EncryptionParameters(
718 user_password, owner_password, 735 user_password, owner_password,
719 allow_accessibility != QPDF_FALSE, allow_extract != QPDF_FALSE, 736 allow_accessibility != QPDF_FALSE, allow_extract != QPDF_FALSE,
720 print, modify, 737 print, modify,
721 encrypt_metadata != QPDF_FALSE, use_aes != QPDF_FALSE); 738 encrypt_metadata != QPDF_FALSE, use_aes != QPDF_FALSE);
  739 +#if (defined(__GNUC__) || defined(__clang__))
  740 +# pragma GCC diagnostic pop
  741 +#endif
722 } 742 }
723 743
724 void qpdf_set_r5_encryption_parameters( 744 void qpdf_set_r5_encryption_parameters(
@@ -727,11 +747,21 @@ void qpdf_set_r5_encryption_parameters( @@ -727,11 +747,21 @@ void qpdf_set_r5_encryption_parameters(
727 qpdf_r3_print_e print, qpdf_r3_modify_e modify, 747 qpdf_r3_print_e print, qpdf_r3_modify_e modify,
728 QPDF_BOOL encrypt_metadata) 748 QPDF_BOOL encrypt_metadata)
729 { 749 {
  750 +#ifdef _MSC_VER
  751 +# pragma warning (disable: 4996)
  752 +#endif
  753 +#if (defined(__GNUC__) || defined(__clang__))
  754 +# pragma GCC diagnostic push
  755 +# pragma GCC diagnostic ignored "-Wdeprecated-declarations"
  756 +#endif
730 qpdf->qpdf_writer->setR5EncryptionParameters( 757 qpdf->qpdf_writer->setR5EncryptionParameters(
731 user_password, owner_password, 758 user_password, owner_password,
732 allow_accessibility != QPDF_FALSE, allow_extract != QPDF_FALSE, 759 allow_accessibility != QPDF_FALSE, allow_extract != QPDF_FALSE,
733 print, modify, 760 print, modify,
734 encrypt_metadata != QPDF_FALSE); 761 encrypt_metadata != QPDF_FALSE);
  762 +#if (defined(__GNUC__) || defined(__clang__))
  763 +# pragma GCC diagnostic pop
  764 +#endif
735 } 765 }
736 766
737 void qpdf_set_r6_encryption_parameters( 767 void qpdf_set_r6_encryption_parameters(
@@ -740,10 +770,20 @@ void qpdf_set_r6_encryption_parameters( @@ -740,10 +770,20 @@ void qpdf_set_r6_encryption_parameters(
740 qpdf_r3_print_e print, qpdf_r3_modify_e modify, 770 qpdf_r3_print_e print, qpdf_r3_modify_e modify,
741 QPDF_BOOL encrypt_metadata) 771 QPDF_BOOL encrypt_metadata)
742 { 772 {
  773 +#ifdef _MSC_VER
  774 +# pragma warning (disable: 4996)
  775 +#endif
  776 +#if (defined(__GNUC__) || defined(__clang__))
  777 +# pragma GCC diagnostic push
  778 +# pragma GCC diagnostic ignored "-Wdeprecated-declarations"
  779 +#endif
743 qpdf->qpdf_writer->setR6EncryptionParameters( 780 qpdf->qpdf_writer->setR6EncryptionParameters(
744 user_password, owner_password, 781 user_password, owner_password,
745 allow_accessibility != QPDF_FALSE, allow_extract != QPDF_FALSE, 782 allow_accessibility != QPDF_FALSE, allow_extract != QPDF_FALSE,
746 print, modify, encrypt_metadata != QPDF_FALSE); 783 print, modify, encrypt_metadata != QPDF_FALSE);
  784 +#if (defined(__GNUC__) || defined(__clang__))
  785 +# pragma GCC diagnostic pop
  786 +#endif
747 } 787 }
748 788
749 void qpdf_set_linearization(qpdf_data qpdf, QPDF_BOOL value) 789 void qpdf_set_linearization(qpdf_data qpdf, QPDF_BOOL value)
manual/qpdf-manual.xml
@@ -874,6 +874,19 @@ make @@ -874,6 +874,19 @@ make
874 </listitem> 874 </listitem>
875 </varlistentry> 875 </varlistentry>
876 <varlistentry> 876 <varlistentry>
  877 + <term><option>--allow-weak-crypto</option></term>
  878 + <listitem>
  879 + <para>
  880 + Starting with version 10.4, qpdf issues warnings when
  881 + requested to create files using RC4 encryption. This option
  882 + suppresses those warnings. In future versions of qpdf, qpdf
  883 + will refuse to create files with weak cryptography when this
  884 + flag is not given. See <xref linkend="ref.weak-crypto"/> for
  885 + additional details.
  886 + </para>
  887 + </listitem>
  888 + </varlistentry>
  889 + <varlistentry>
877 <term><option>--encrypt options --</option></term> 890 <term><option>--encrypt options --</option></term>
878 <listitem> 891 <listitem>
879 <para> 892 <para>
@@ -3355,6 +3368,43 @@ outfile.pdf&lt;/option&gt; @@ -3355,6 +3368,43 @@ outfile.pdf&lt;/option&gt;
3355 </para> 3368 </para>
3356 </sect1> 3369 </sect1>
3357 </chapter> 3370 </chapter>
  3371 + <chapter id="ref.weak-crypto">
  3372 + <title>Weak Cryptography</title>
  3373 + <para>
  3374 + Start with version 10.4, qpdf is taking steps to reduce the
  3375 + likelihood of a user <emphasis>accidentally</emphasis> creating PDF
  3376 + files with insecure cryptography but will continue to allow
  3377 + creation of such files indefinitely with explicit acknowledgment.
  3378 + </para>
  3379 + <para>
  3380 + The PDF file format makes use of RC4, which is known to be a weak
  3381 + cryptography algorithm, and MD5, which is a weak hashing algorithm.
  3382 + In version 10.4, qpdf generates warnings for some (but not all)
  3383 + cases of writing files with weak cryptography when invoked from the
  3384 + command-line. These warnings can be suppressed using the
  3385 + <option>--allow-weak-crypto</option> option.
  3386 + </para>
  3387 + <para>
  3388 + It is planned for qpdf version 11 to be stricter, making it an
  3389 + error to write files with insecure cryptography from the
  3390 + command-line tool in most cases without specifying the
  3391 + <option>--allow-weak-crypto</option> flag and also to require
  3392 + explicit steps when using the C++ library to enable use of insecure
  3393 + cryptography.
  3394 + </para>
  3395 + <para>
  3396 + Note that qpdf must always retain support for weak cryptographic
  3397 + algorithms since this is required for reading older PDF files that
  3398 + use it. Additionally, qpdf will always retain the ability to create
  3399 + files using weak cryptographic algorithms since, as a development
  3400 + tool, qpdf explicitly supports creating older or deprecated types
  3401 + of PDF files since these are sometimes needed to test or work with
  3402 + older versions of software. Even if other cryptography libraries
  3403 + drop support for RC4 or MD5, qpdf can always fall back to its
  3404 + internal implementations of those algorithms, so they are not going
  3405 + to disappear from qpdf.
  3406 + </para>
  3407 + </chapter>
3358 <chapter id="ref.json"> 3408 <chapter id="ref.json">
3359 <title>QPDF JSON</title> 3409 <title>QPDF JSON</title>
3360 <sect1 id="ref.json-overview"> 3410 <sect1 id="ref.json-overview">
@@ -5072,6 +5122,27 @@ print &quot;\n&quot;; @@ -5072,6 +5122,27 @@ print &quot;\n&quot;;
5072 <itemizedlist> 5122 <itemizedlist>
5073 <listitem> 5123 <listitem>
5074 <para> 5124 <para>
  5125 + Handling of Weak Cryptography Algorithms
  5126 + </para>
  5127 + <itemizedlist>
  5128 + <listitem>
  5129 + <para>
  5130 + From the qpdf CLI, the <option>--allow-weak-crypto</option>
  5131 + is now required to suppress a warning when explicitly
  5132 + creating PDF files using RC4 encryption. While qpdf will
  5133 + always retain the ability to read and write such files,
  5134 + doing so will require explicit acknowledgment moving
  5135 + forward. For qpdf 10.4, this change only affects the
  5136 + command-line tool. Starting in qpdf 11, there will be small
  5137 + API changes to require explicit acknowledgment in those
  5138 + cases as well. For additional information, see <xref
  5139 + linkend="ref.weak-crypto"/>.
  5140 + </para>
  5141 + </listitem>
  5142 + </itemizedlist>
  5143 + </listitem>
  5144 + <listitem>
  5145 + <para>
5075 Bug Fixes 5146 Bug Fixes
5076 </para> 5147 </para>
5077 <itemizedlist> 5148 <itemizedlist>
qpdf/qpdf.cc
@@ -141,6 +141,7 @@ struct Options @@ -141,6 +141,7 @@ struct Options
141 suppress_password_recovery(false), 141 suppress_password_recovery(false),
142 password_mode(pm_auto), 142 password_mode(pm_auto),
143 allow_insecure(false), 143 allow_insecure(false),
  144 + allow_weak_crypto(false),
144 keylen(0), 145 keylen(0),
145 r2_print(true), 146 r2_print(true),
146 r2_modify(true), 147 r2_modify(true),
@@ -242,6 +243,7 @@ struct Options @@ -242,6 +243,7 @@ struct Options
242 bool suppress_password_recovery; 243 bool suppress_password_recovery;
243 password_mode_e password_mode; 244 password_mode_e password_mode;
244 bool allow_insecure; 245 bool allow_insecure;
  246 + bool allow_weak_crypto;
245 std::string user_password; 247 std::string user_password;
246 std::string owner_password; 248 std::string owner_password;
247 int keylen; 249 int keylen;
@@ -811,6 +813,7 @@ class ArgParser @@ -811,6 +813,7 @@ class ArgParser
811 void argDecrypt(); 813 void argDecrypt();
812 void argPasswordIsHexKey(); 814 void argPasswordIsHexKey();
813 void argAllowInsecure(); 815 void argAllowInsecure();
  816 + void argAllowWeakCrypto();
814 void argPasswordMode(char* parameter); 817 void argPasswordMode(char* parameter);
815 void argSuppressPasswordRecovery(); 818 void argSuppressPasswordRecovery();
816 void argCopyEncryption(char* parameter); 819 void argCopyEncryption(char* parameter);
@@ -1169,6 +1172,7 @@ ArgParser::initOptionTable() @@ -1169,6 +1172,7 @@ ArgParser::initOptionTable()
1169 (*t)["replace-input"] = oe_bare(&ArgParser::argReplaceInput); 1172 (*t)["replace-input"] = oe_bare(&ArgParser::argReplaceInput);
1170 (*t)["is-encrypted"] = oe_bare(&ArgParser::argIsEncrypted); 1173 (*t)["is-encrypted"] = oe_bare(&ArgParser::argIsEncrypted);
1171 (*t)["requires-password"] = oe_bare(&ArgParser::argRequiresPassword); 1174 (*t)["requires-password"] = oe_bare(&ArgParser::argRequiresPassword);
  1175 + (*t)["allow-weak-crypto"] = oe_bare(&ArgParser::argAllowWeakCrypto);
1172 1176
1173 t = &this->encrypt40_option_table; 1177 t = &this->encrypt40_option_table;
1174 (*t)["--"] = oe_bare(&ArgParser::argEndEncrypt); 1178 (*t)["--"] = oe_bare(&ArgParser::argEndEncrypt);
@@ -1376,6 +1380,8 @@ ArgParser::argHelp() @@ -1376,6 +1380,8 @@ ArgParser::argHelp()
1376 << "--encryption-file-password=password\n" 1380 << "--encryption-file-password=password\n"
1377 << " password used to open the file from which encryption\n" 1381 << " password used to open the file from which encryption\n"
1378 << " parameters are being copied\n" 1382 << " parameters are being copied\n"
  1383 + << "--allow-weak-crypto allow creation of files using weak cryptographic\n"
  1384 + << " algorithms\n"
1379 << "--encrypt options -- generate an encrypted file\n" 1385 << "--encrypt options -- generate an encrypted file\n"
1380 << "--decrypt remove any encryption on the file\n" 1386 << "--decrypt remove any encryption on the file\n"
1381 << "--password-is-hex-key treat primary password option as a hex-encoded key\n" 1387 << "--password-is-hex-key treat primary password option as a hex-encoded key\n"
@@ -1502,6 +1508,11 @@ ArgParser::argHelp() @@ -1502,6 +1508,11 @@ ArgParser::argHelp()
1502 << "to be used even if not otherwise needed. This option is primarily useful\n" 1508 << "to be used even if not otherwise needed. This option is primarily useful\n"
1503 << "for testing qpdf and has no other practical use.\n" 1509 << "for testing qpdf and has no other practical use.\n"
1504 << "\n" 1510 << "\n"
  1511 + << "A warning will be issued if you attempt to encrypt a file with a format that\n"
  1512 + << "uses a weak cryptographic algorithm such as RC4. To suppress the warning,\n"
  1513 + << "specify the option --allow-weak-crypto. This option is outside of encryption\n"
  1514 + << "options (e.g. --allow-week-crypto --encrypt u o 128 --)\n"
  1515 + << "\n"
1505 << "\n" 1516 << "\n"
1506 << "Password Modes\n" 1517 << "Password Modes\n"
1507 << "--------------\n" 1518 << "--------------\n"
@@ -2069,6 +2080,12 @@ ArgParser::argAllowInsecure() @@ -2069,6 +2080,12 @@ ArgParser::argAllowInsecure()
2069 } 2080 }
2070 2081
2071 void 2082 void
  2083 +ArgParser::argAllowWeakCrypto()
  2084 +{
  2085 + o.allow_weak_crypto = true;
  2086 +}
  2087 +
  2088 +void
2072 ArgParser::argCopyEncryption(char* parameter) 2089 ArgParser::argCopyEncryption(char* parameter)
2073 { 2090 {
2074 o.encryption_file = parameter; 2091 o.encryption_file = parameter;
@@ -6253,6 +6270,26 @@ static void set_encryption_options(QPDF&amp; pdf, Options&amp; o, QPDFWriter&amp; w) @@ -6253,6 +6270,26 @@ static void set_encryption_options(QPDF&amp; pdf, Options&amp; o, QPDFWriter&amp; w)
6253 } 6270 }
6254 maybe_fix_write_password(R, o, o.user_password); 6271 maybe_fix_write_password(R, o, o.user_password);
6255 maybe_fix_write_password(R, o, o.owner_password); 6272 maybe_fix_write_password(R, o, o.owner_password);
  6273 + if ((R < 4) || ((R == 4) && (! o.use_aes)))
  6274 + {
  6275 + if (! o.allow_weak_crypto)
  6276 + {
  6277 + // Do not set exit code to EXIT_WARNING for this case as
  6278 + // this does not reflect a potential problem with the
  6279 + // input file.
  6280 + QTC::TC("qpdf", "qpdf weak crypto warning");
  6281 + std::cerr
  6282 + << whoami
  6283 + << ": writing a file with RC4, a weak cryptographic algorithm"
  6284 + << std::endl
  6285 + << "Please use 256-bit keys for better security."
  6286 + << std::endl
  6287 + << "Pass --allow-weak-crypto to suppress this warning."
  6288 + << std::endl
  6289 + << "This will become an error in a future version of qpdf."
  6290 + << std::endl;
  6291 + }
  6292 + }
6256 switch (R) 6293 switch (R)
6257 { 6294 {
6258 case 2: 6295 case 2:
qpdf/qpdf.testcov
@@ -598,3 +598,4 @@ check unclosed --pages 1 @@ -598,3 +598,4 @@ check unclosed --pages 1
598 QPDF_pages findPage not found 0 598 QPDF_pages findPage not found 0
599 qpdf overlay page with no resources 0 599 qpdf overlay page with no resources 0
600 QPDFObjectHandle check ownership 0 600 QPDFObjectHandle check ownership 0
  601 +qpdf weak crypto warning 0
qpdf/qtest/qpdf.test
@@ -1358,7 +1358,8 @@ foreach my $file (qw(short-id long-id)) @@ -1358,7 +1358,8 @@ foreach my $file (qw(short-id long-id))
1358 { 1358 {
1359 $td->runtest("encrypt $file.pdf", 1359 $td->runtest("encrypt $file.pdf",
1360 {$td->COMMAND => 1360 {$td->COMMAND =>
1361 - "qpdf --encrypt '' pass 40 -- $file.pdf a.pdf"}, 1361 + "qpdf --allow-weak-crypto".
  1362 + " --encrypt '' pass 40 -- $file.pdf a.pdf"},
1362 {$td->STRING => "", 1363 {$td->STRING => "",
1363 $td->EXIT_STATUS => 0}, 1364 $td->EXIT_STATUS => 0},
1364 $td->NORMALIZE_NEWLINES); 1365 $td->NORMALIZE_NEWLINES);
@@ -2057,7 +2058,8 @@ $td-&gt;notify(&quot;--- Split Pages ---&quot;); @@ -2057,7 +2058,8 @@ $td-&gt;notify(&quot;--- Split Pages ---&quot;);
2057 my @sp_cases = ( 2058 my @sp_cases = (
2058 [11, '%d at beginning', '', '%d_split-out.zdf'], 2059 [11, '%d at beginning', '', '%d_split-out.zdf'],
2059 [11, '%d at end', '--qdf', 'split-out.zdf_%d'], 2060 [11, '%d at end', '--qdf', 'split-out.zdf_%d'],
2060 - [11, '%d in middle', '--encrypt u o 128 --', 'a-%d-split-out.zdf'], 2061 + [11, '%d in middle', '--allow-weak-crypto --encrypt u o 128 --',
  2062 + 'a-%d-split-out.zdf'],
2061 [11, 'pdf extension', '', 'split-out.Pdf'], 2063 [11, 'pdf extension', '', 'split-out.Pdf'],
2062 [4, 'fallback', '--pages 11-pages.pdf 1-3 minimal.pdf --', 'split-out'], 2064 [4, 'fallback', '--pages 11-pages.pdf 1-3 minimal.pdf --', 'split-out'],
2063 [1, 'broken data', '--pages broken-lzw.pdf --', 'split-out.pdf', 2065 [1, 'broken data', '--pages broken-lzw.pdf --', 'split-out.pdf',
@@ -2718,6 +2720,7 @@ $td-&gt;runtest(&quot;check output&quot;, @@ -2718,6 +2720,7 @@ $td-&gt;runtest(&quot;check output&quot;,
2718 $td->runtest("avoid respecification of password", 2720 $td->runtest("avoid respecification of password",
2719 {$td->COMMAND => 2721 {$td->COMMAND =>
2720 "qpdf --empty a.pdf --copy-encryption=20-pages.pdf" . 2722 "qpdf --empty a.pdf --copy-encryption=20-pages.pdf" .
  2723 + " --allow-weak-crypto" .
2721 " --encryption-file-password=user" . 2724 " --encryption-file-password=user" .
2722 " --pages 20-pages.pdf 1,z -- --static-id"}, 2725 " --pages 20-pages.pdf 1,z -- --static-id"},
2723 {$td->STRING => "", $td->EXIT_STATUS => 0}); 2726 {$td->STRING => "", $td->EXIT_STATUS => 0});
@@ -3483,7 +3486,7 @@ for (my $n = 16; $n &lt;= 19; ++$n) @@ -3483,7 +3486,7 @@ for (my $n = 16; $n &lt;= 19; ++$n)
3483 '-object-streams=preserve', 3486 '-object-streams=preserve',
3484 '-object-streams=generate') 3487 '-object-streams=generate')
3485 { 3488 {
3486 - foreach my $qdf ('-qdf', '', '-encrypt "" x 128 --') 3489 + foreach my $qdf ('-qdf', '', '-allow-weak-crypto -encrypt "" x 128 --')
3487 { 3490 {
3488 # 4 tests + 1 compare_pdfs * 36 cases 3491 # 4 tests + 1 compare_pdfs * 36 cases
3489 # 2 additional tests * 12 cases 3492 # 2 additional tests * 12 cases
@@ -3716,19 +3719,22 @@ foreach my $f (qw(compressed-metadata.pdf enc-base.pdf)) @@ -3716,19 +3719,22 @@ foreach my $f (qw(compressed-metadata.pdf enc-base.pdf))
3716 check_metadata("a.pdf", 0, 1); 3719 check_metadata("a.pdf", 0, 1);
3717 $td->runtest("encrypt normally", 3720 $td->runtest("encrypt normally",
3718 {$td->COMMAND => 3721 {$td->COMMAND =>
3719 - "qpdf --encrypt '' o 128 -- a.pdf b.pdf"}, 3722 + "qpdf --allow-weak-crypto" .
  3723 + " --encrypt '' o 128 -- a.pdf b.pdf"},
3720 {$td->STRING => "", $td->EXIT_STATUS => 0}); 3724 {$td->STRING => "", $td->EXIT_STATUS => 0});
3721 check_metadata("b.pdf", 1, 0); 3725 check_metadata("b.pdf", 1, 0);
3722 unlink "b.pdf"; 3726 unlink "b.pdf";
3723 $td->runtest("encrypt V4", 3727 $td->runtest("encrypt V4",
3724 {$td->COMMAND => 3728 {$td->COMMAND =>
3725 - "qpdf --encrypt '' o 128 --force-V4 -- a.pdf b.pdf"}, 3729 + "qpdf --allow-weak-crypto" .
  3730 + " --encrypt '' o 128 --force-V4 -- a.pdf b.pdf"},
3726 {$td->STRING => "", $td->EXIT_STATUS => 0}); 3731 {$td->STRING => "", $td->EXIT_STATUS => 0});
3727 check_metadata("b.pdf", 1, 0); 3732 check_metadata("b.pdf", 1, 0);
3728 unlink "b.pdf"; 3733 unlink "b.pdf";
3729 $td->runtest("encrypt with cleartext metadata", 3734 $td->runtest("encrypt with cleartext metadata",
3730 {$td->COMMAND => 3735 {$td->COMMAND =>
3731 - "qpdf --encrypt '' o 128 --cleartext-metadata --" . 3736 + "qpdf --allow-weak-crypto" .
  3737 + " --encrypt '' o 128 --cleartext-metadata --" .
3732 " a.pdf b.pdf"}, 3738 " a.pdf b.pdf"},
3733 {$td->STRING => "", $td->EXIT_STATUS => 0}); 3739 {$td->STRING => "", $td->EXIT_STATUS => 0});
3734 check_metadata("b.pdf", 1, 1); 3740 check_metadata("b.pdf", 1, 1);
@@ -3753,6 +3759,31 @@ foreach my $f (qw(compressed-metadata.pdf enc-base.pdf)) @@ -3753,6 +3759,31 @@ foreach my $f (qw(compressed-metadata.pdf enc-base.pdf))
3753 3759
3754 show_ntests(); 3760 show_ntests();
3755 # ---------- 3761 # ----------
  3762 +$td->notify("--- Weak Cryptography ---");
  3763 +$n_tests += 4;
  3764 +$td->runtest("256-bit: no warning",
  3765 + {$td->COMMAND => 'qpdf --encrypt "" "" 256 -- minimal.pdf a.pdf'},
  3766 + {$td->STRING => "", $td->EXIT_STATUS => 0},
  3767 + $td->NORMALIZE_NEWLINES);
  3768 +$td->runtest("128-bit with AES: no warning",
  3769 + {$td->COMMAND => 'qpdf --encrypt "" "" 128 --use-aes=y --' .
  3770 + ' minimal.pdf a.pdf'},
  3771 + {$td->STRING => "", $td->EXIT_STATUS => 0},
  3772 + $td->NORMALIZE_NEWLINES);
  3773 +# Note: we intentionally have exit status 0 for this warning.
  3774 +$td->runtest("128-bit without AES: warning",
  3775 + {$td->COMMAND => 'qpdf --encrypt "" "" 128 -- minimal.pdf a.pdf'},
  3776 + {$td->REGEXP => "Pass --allow-weak-crypto to suppress",
  3777 + $td->EXIT_STATUS => 0},
  3778 + $td->NORMALIZE_NEWLINES);
  3779 +$td->runtest("40-bit: warning",
  3780 + {$td->COMMAND => 'qpdf --encrypt "" "" 40 -- minimal.pdf a.pdf'},
  3781 + {$td->REGEXP => "Pass --allow-weak-crypto to suppress",
  3782 + $td->EXIT_STATUS => 0},
  3783 + $td->NORMALIZE_NEWLINES);
  3784 +
  3785 +show_ntests();
  3786 +# ----------
3756 $td->notify("--- Linearization Tests ---"); 3787 $td->notify("--- Linearization Tests ---");
3757 # $n_tests incremented after initialization of @linearized_files and 3788 # $n_tests incremented after initialization of @linearized_files and
3758 # @to_linearize. 3789 # @to_linearize.
@@ -4128,7 +4159,8 @@ foreach my $d (@encrypted_files) @@ -4128,7 +4159,8 @@ foreach my $d (@encrypted_files)
4128 $enc_json =~ s/---opm---/$opm/; 4159 $enc_json =~ s/---opm---/$opm/;
4129 $enc_json =~ s/---upm---/$upm/; 4160 $enc_json =~ s/---upm---/$upm/;
4130 4161
4131 - my $eflags = "-encrypt \"$upass\" \"$opass\" $bits $xeflags --"; 4162 + my $eflags = "--allow-weak-crypto" .
  4163 + " -encrypt \"$upass\" \"$opass\" $bits $xeflags --";
4132 if (($opass eq "") && ($bits == 256)) 4164 if (($opass eq "") && ($bits == 256))
4133 { 4165 {
4134 $eflags =~ s/--$/--allow-insecure --/; 4166 $eflags =~ s/--$/--allow-insecure --/;
@@ -4391,7 +4423,7 @@ foreach my $d ([&#39;--force-V4&#39;, &#39;V4&#39;], @@ -4391,7 +4423,7 @@ foreach my $d ([&#39;--force-V4&#39;, &#39;V4&#39;],
4391 my ($args, $out) = @$d; 4423 my ($args, $out) = @$d;
4392 $td->runtest("encrypt $args", 4424 $td->runtest("encrypt $args",
4393 {$td->COMMAND => "qpdf --static-aes-iv --static-id" . 4425 {$td->COMMAND => "qpdf --static-aes-iv --static-id" .
4394 - " --encrypt '' '' 128 $args --" . 4426 + " --allow-weak-crypto --encrypt '' '' 128 $args --" .
4395 " enc-base.pdf a.pdf"}, 4427 " enc-base.pdf a.pdf"},
4396 {$td->STRING => "", $td->EXIT_STATUS => 0}); 4428 {$td->STRING => "", $td->EXIT_STATUS => 0});
4397 $td->runtest("check output", 4429 $td->runtest("check output",
@@ -4677,6 +4709,7 @@ foreach my $d (@unicode_pw_cases) @@ -4677,6 +4709,7 @@ foreach my $d (@unicode_pw_cases)
4677 $td->runtest("encode $bits, $pw, $w_encoding", 4709 $td->runtest("encode $bits, $pw, $w_encoding",
4678 {$td->COMMAND => 4710 {$td->COMMAND =>
4679 "qpdf $xargs --static-id --static-aes-iv" . 4711 "qpdf $xargs --static-id --static-aes-iv" .
  4712 + " --allow-weak-crypto" .
4680 " --encrypt $upass o $bits -- minimal.pdf a.pdf"}, 4713 " --encrypt $upass o $bits -- minimal.pdf a.pdf"},
4681 {$td->STRING => $exp, $td->EXIT_STATUS => ($exp ? 2 : 0)}, 4714 {$td->STRING => $exp, $td->EXIT_STATUS => ($exp ? 2 : 0)},
4682 $td->NORMALIZE_NEWLINES); 4715 $td->NORMALIZE_NEWLINES);
@@ -4718,7 +4751,8 @@ $n_tests += 5; @@ -4718,7 +4751,8 @@ $n_tests += 5;
4718 4751
4719 $td->runtest("bytes fallback warning", 4752 $td->runtest("bytes fallback warning",
4720 {$td->COMMAND => 4753 {$td->COMMAND =>
4721 - "qpdf --encrypt \@password-bare-complex-utf8 o 128 --" . 4754 + "qpdf --allow-weak-crypto" .
  4755 + " --encrypt \@password-bare-complex-utf8 o 128 --" .
4722 " minimal.pdf a.pdf"}, 4756 " minimal.pdf a.pdf"},
4723 {$td->FILE => "bytes-fallback.out", $td->EXIT_STATUS => 0}, 4757 {$td->FILE => "bytes-fallback.out", $td->EXIT_STATUS => 0},
4724 $td->NORMALIZE_NEWLINES); 4758 $td->NORMALIZE_NEWLINES);
@@ -4814,9 +4848,9 @@ my @flags = ([&quot;-qdf&quot;, # 1 @@ -4814,9 +4848,9 @@ my @flags = ([&quot;-qdf&quot;, # 1
4814 "decrypted"], 4848 "decrypted"],
4815 ["-linearize", # 9 4849 ["-linearize", # 9
4816 "linearized"], 4850 "linearized"],
4817 - ["-encrypt \"\" owner 128 --", # 10 4851 + ["-allow-weak-crypto -encrypt \"\" owner 128 --", # 10
4818 "encrypted"], 4852 "encrypted"],
4819 - ["-linearize -encrypt \"\" o 128 --", # 11 4853 + ["-linearize -allow-weak-crypto -encrypt \"\" o 128 --", # 11
4820 "linearized and encrypted"], 4854 "linearized and encrypted"],
4821 ["", # 12 4855 ["", # 12
4822 "no arguments"], 4856 "no arguments"],
@@ -4985,9 +5019,15 @@ $n_tests += 2; @@ -4985,9 +5019,15 @@ $n_tests += 2;
4985 $n_tests += 12; 5019 $n_tests += 12;
4986 foreach my $i (qw(40 128 256)) 5020 foreach my $i (qw(40 128 256))
4987 { 5021 {
  5022 + my $x = "";
  5023 + if ($i < 256)
  5024 + {
  5025 + $x = "--allow-weak-crypto";
  5026 + }
4988 $td->runtest("encrypt $i", 5027 $td->runtest("encrypt $i",
4989 {$td->COMMAND => 5028 {$td->COMMAND =>
4990 - "qpdf --encrypt '' o $i -- digitally-signed.pdf a.pdf"}, 5029 + "qpdf $x --encrypt '' o $i --" .
  5030 + " digitally-signed.pdf a.pdf"},
4991 {$td->STRING => "", 5031 {$td->STRING => "",
4992 $td->EXIT_STATUS => 0}); 5032 $td->EXIT_STATUS => 0});
4993 $td->runtest("find desired contents (encrypt $i)", 5033 $td->runtest("find desired contents (encrypt $i)",
@@ -5010,9 +5050,15 @@ foreach my $i (qw(40 128 256)) @@ -5010,9 +5050,15 @@ foreach my $i (qw(40 128 256))
5010 $n_tests += 15; 5050 $n_tests += 15;
5011 foreach my $i (qw(40 128 256)) 5051 foreach my $i (qw(40 128 256))
5012 { 5052 {
  5053 + my $x = "";
  5054 + if ($i < 256)
  5055 + {
  5056 + $x = "--allow-weak-crypto";
  5057 + }
5013 $td->runtest("non sig dict encrypt $i", 5058 $td->runtest("non sig dict encrypt $i",
5014 {$td->COMMAND => 5059 {$td->COMMAND =>
5015 - "qpdf --encrypt '' o $i -- comment-annotation.pdf a.pdf"}, 5060 + "qpdf $x --encrypt '' o $i --" .
  5061 + " comment-annotation.pdf a.pdf"},
5016 {$td->STRING => "", 5062 {$td->STRING => "",
5017 $td->EXIT_STATUS => 0}); 5063 $td->EXIT_STATUS => 0});
5018 $td->runtest("plain text not found due to encryption (non sig dict encrypt $i)", 5064 $td->runtest("plain text not found due to encryption (non sig dict encrypt $i)",