Commit 21b0f4acfc0d6827f3d2d9a85873b7b649dc96f0

Authored by Jay Berkenbilt
1 parent faa2e3dd

Require --allow-insecure to create certain encrypted files (fixes #501)

For now, --allow-insecure allows creation of files with the owner
passwords empty or matching the user password.
ChangeLog
  1 +2021-02-04 Jay Berkenbilt <ejb@ql.org>
  2 +
  3 + * By default, give an error if a user attempts to encrypt a file
  4 + with an empty owner password or an owner password that is the same
  5 + as the user password. Such files are insecure. Most viewers either
  6 + won't open such files or will not enforce security settings. To
  7 + allow explicit creation of files like this, pass the new
  8 + --allow-insecure option. Fixes #501.
  9 +
1 2021-02-02 Jay Berkenbilt <ejb@ql.org> 10 2021-02-02 Jay Berkenbilt <ejb@ql.org>
2 11
3 * Bug fix: if a form XObject lacks a resources dictionary, 12 * Bug fix: if a form XObject lacks a resources dictionary,
manual/qpdf-manual.xml
@@ -1214,7 +1214,11 @@ make @@ -1214,7 +1214,11 @@ make
1214 </para> 1214 </para>
1215 <para> 1215 <para>
1216 Either or both of the user password and the owner password may be 1216 Either or both of the user password and the owner password may be
1217 - empty strings. 1217 + empty strings. Starting in qpdf 10.2, qpdf defaults to not
  1218 + allowing creation of PDF files with an empty owner password or an
  1219 + owner password that matches the user password. If you want to
  1220 + create such files, specify the encryption option
  1221 + <option>--allow-insecure</option>, as described below.
1218 </para> 1222 </para>
1219 <para> 1223 <para>
1220 The value for 1224 The value for
@@ -1224,6 +1228,25 @@ make @@ -1224,6 +1228,25 @@ make
1224 fully permissive. 1228 fully permissive.
1225 </para> 1229 </para>
1226 <para> 1230 <para>
  1231 + For all key lengths, the following options are available:
  1232 + <variablelist>
  1233 + <varlistentry>
  1234 + <term><option>--allow-insecure</option></term>
  1235 + <listitem>
  1236 + <para>
  1237 + From qpdf 10.2, qpdf defaults to not allowing creation of PDF
  1238 + files where the owner password is blank or matches the user
  1239 + password. Files created in this way are insecure and can't be
  1240 + opened by some viewers. Users would ordinarily never want to
  1241 + create such files. If you are using qpdf to intentionally
  1242 + created strange files for testing (a definite valid use of
  1243 + qpdf!), this option allows you to create such insecure files.
  1244 + </para>
  1245 + </listitem>
  1246 + </varlistentry>
  1247 + </variablelist>
  1248 + </para>
  1249 + <para>
1227 If <option><replaceable>key-length</replaceable></option> is 40, 1250 If <option><replaceable>key-length</replaceable></option> is 40,
1228 the following restriction options are available: 1251 the following restriction options are available:
1229 <variablelist> 1252 <variablelist>
@@ -4824,7 +4847,28 @@ print &quot;\n&quot;; @@ -4824,7 +4847,28 @@ print &quot;\n&quot;;
4824 <itemizedlist> 4847 <itemizedlist>
4825 <listitem> 4848 <listitem>
4826 <para> 4849 <para>
4827 - Behavior Changes 4850 + CLI Behavior Changes
  4851 + </para>
  4852 + <itemizedlist>
  4853 + <listitem>
  4854 + <para>
  4855 + By default, <command>qpdf</command> no longer allows
  4856 + creation of encrypted PDF files whose owner password is
  4857 + empty or matches the user password. The
  4858 + <option>--allow-insecure</option>, specified inside the
  4859 + <option>--encrypt</option> options, allows creation of such
  4860 + files. Behavior changes in the CLI are avoided when
  4861 + possible, but an exception was made here because this is
  4862 + security-related. qpdf must always allow creation of weird
  4863 + files for testing purposes, but it should not default to
  4864 + letting users unknowingly create insecure files.
  4865 + </para>
  4866 + </listitem>
  4867 + </itemizedlist>
  4868 + </listitem>
  4869 + <listitem>
  4870 + <para>
  4871 + Library Behavior Changes
4828 </para> 4872 </para>
4829 <itemizedlist> 4873 <itemizedlist>
4830 <listitem> 4874 <listitem>
qpdf/qpdf.cc
@@ -113,6 +113,7 @@ struct Options @@ -113,6 +113,7 @@ struct Options
113 password_is_hex_key(false), 113 password_is_hex_key(false),
114 suppress_password_recovery(false), 114 suppress_password_recovery(false),
115 password_mode(pm_auto), 115 password_mode(pm_auto),
  116 + allow_insecure(false),
116 keylen(0), 117 keylen(0),
117 r2_print(true), 118 r2_print(true),
118 r2_modify(true), 119 r2_modify(true),
@@ -211,6 +212,7 @@ struct Options @@ -211,6 +212,7 @@ struct Options
211 bool password_is_hex_key; 212 bool password_is_hex_key;
212 bool suppress_password_recovery; 213 bool suppress_password_recovery;
213 password_mode_e password_mode; 214 password_mode_e password_mode;
  215 + bool allow_insecure;
214 std::string user_password; 216 std::string user_password;
215 std::string owner_password; 217 std::string owner_password;
216 int keylen; 218 int keylen;
@@ -742,6 +744,7 @@ class ArgParser @@ -742,6 +744,7 @@ class ArgParser
742 void argEncrypt(); 744 void argEncrypt();
743 void argDecrypt(); 745 void argDecrypt();
744 void argPasswordIsHexKey(); 746 void argPasswordIsHexKey();
  747 + void argAllowInsecure();
745 void argPasswordMode(char* parameter); 748 void argPasswordMode(char* parameter);
746 void argSuppressPasswordRecovery(); 749 void argSuppressPasswordRecovery();
747 void argCopyEncryption(char* parameter); 750 void argCopyEncryption(char* parameter);
@@ -1074,13 +1077,17 @@ ArgParser::initOptionTable() @@ -1074,13 +1077,17 @@ ArgParser::initOptionTable()
1074 1077
1075 t = &this->encrypt40_option_table; 1078 t = &this->encrypt40_option_table;
1076 (*t)["--"] = oe_bare(&ArgParser::argEndEncrypt); 1079 (*t)["--"] = oe_bare(&ArgParser::argEndEncrypt);
  1080 + (*t)["allow-insecure"] = oe_bare(&ArgParser::argAllowInsecure);
  1081 + // The above 40-bit options are also 128-bit and 256-bit options,
  1082 + // so copy what we have so far to 128. Then continue separately
  1083 + // with 128. We later copy 128 to 256.
  1084 + this->encrypt128_option_table = this->encrypt40_option_table;
1077 (*t)["print"] = oe_requiredChoices(&ArgParser::arg40Print, yn); 1085 (*t)["print"] = oe_requiredChoices(&ArgParser::arg40Print, yn);
1078 (*t)["modify"] = oe_requiredChoices(&ArgParser::arg40Modify, yn); 1086 (*t)["modify"] = oe_requiredChoices(&ArgParser::arg40Modify, yn);
1079 (*t)["extract"] = oe_requiredChoices(&ArgParser::arg40Extract, yn); 1087 (*t)["extract"] = oe_requiredChoices(&ArgParser::arg40Extract, yn);
1080 (*t)["annotate"] = oe_requiredChoices(&ArgParser::arg40Annotate, yn); 1088 (*t)["annotate"] = oe_requiredChoices(&ArgParser::arg40Annotate, yn);
1081 1089
1082 t = &this->encrypt128_option_table; 1090 t = &this->encrypt128_option_table;
1083 - (*t)["--"] = oe_bare(&ArgParser::argEndEncrypt);  
1084 (*t)["accessibility"] = oe_requiredChoices( 1091 (*t)["accessibility"] = oe_requiredChoices(
1085 &ArgParser::arg128Accessibility, yn); 1092 &ArgParser::arg128Accessibility, yn);
1086 (*t)["extract"] = oe_requiredChoices(&ArgParser::arg128Extract, yn); 1093 (*t)["extract"] = oe_requiredChoices(&ArgParser::arg128Extract, yn);
@@ -1317,6 +1324,10 @@ ArgParser::argHelp() @@ -1317,6 +1324,10 @@ ArgParser::argHelp()
1317 << "\n" 1324 << "\n"
1318 << "Additional flags are dependent upon key length.\n" 1325 << "Additional flags are dependent upon key length.\n"
1319 << "\n" 1326 << "\n"
  1327 + << " For all key lengths:\n"
  1328 + << " --allow-insecure allow the owner password to be empty or the\n"
  1329 + << " same as the user password\n"
  1330 + << "\n"
1320 << " If 40:\n" 1331 << " If 40:\n"
1321 << "\n" 1332 << "\n"
1322 << " --print=[yn] allow printing\n" 1333 << " --print=[yn] allow printing\n"
@@ -1850,6 +1861,12 @@ ArgParser::argPasswordMode(char* parameter) @@ -1850,6 +1861,12 @@ ArgParser::argPasswordMode(char* parameter)
1850 } 1861 }
1851 1862
1852 void 1863 void
  1864 +ArgParser::argAllowInsecure()
  1865 +{
  1866 + o.allow_insecure = true;
  1867 +}
  1868 +
  1869 +void
1853 ArgParser::argCopyEncryption(char* parameter) 1870 ArgParser::argCopyEncryption(char* parameter)
1854 { 1871 {
1855 o.encryption_file = parameter; 1872 o.encryption_file = parameter;
@@ -3337,6 +3354,18 @@ ArgParser::doFinalChecks() @@ -3337,6 +3354,18 @@ ArgParser::doFinalChecks()
3337 " together"); 3354 " together");
3338 } 3355 }
3339 3356
  3357 + if (o.encrypt && (! o.allow_insecure) &&
  3358 + (o.owner_password.empty() ||
  3359 + (o.owner_password == o.user_password)))
  3360 + {
  3361 + usage("An encrypted PDF with an empty owner password or an"
  3362 + " owner password that is the same as a user password"
  3363 + " is insecure and can't be opened by some viewers. If you"
  3364 + " really want to do this, you must also give the"
  3365 + " --allow-insecure option before the -- that follows"
  3366 + " --encrypt.");
  3367 + }
  3368 +
3340 if (o.require_outfile && o.outfilename && 3369 if (o.require_outfile && o.outfilename &&
3341 (strcmp(o.outfilename, "-") == 0)) 3370 (strcmp(o.outfilename, "-") == 0))
3342 { 3371 {
qpdf/qtest/qpdf.test
@@ -3189,19 +3189,19 @@ foreach my $f (qw(compressed-metadata.pdf enc-base.pdf)) @@ -3189,19 +3189,19 @@ foreach my $f (qw(compressed-metadata.pdf enc-base.pdf))
3189 check_metadata("a.pdf", 0, 1); 3189 check_metadata("a.pdf", 0, 1);
3190 $td->runtest("encrypt normally", 3190 $td->runtest("encrypt normally",
3191 {$td->COMMAND => 3191 {$td->COMMAND =>
3192 - "qpdf --encrypt '' '' 128 -- a.pdf b.pdf"}, 3192 + "qpdf --encrypt '' o 128 -- a.pdf b.pdf"},
3193 {$td->STRING => "", $td->EXIT_STATUS => 0}); 3193 {$td->STRING => "", $td->EXIT_STATUS => 0});
3194 check_metadata("b.pdf", 1, 0); 3194 check_metadata("b.pdf", 1, 0);
3195 unlink "b.pdf"; 3195 unlink "b.pdf";
3196 $td->runtest("encrypt V4", 3196 $td->runtest("encrypt V4",
3197 {$td->COMMAND => 3197 {$td->COMMAND =>
3198 - "qpdf --encrypt '' '' 128 --force-V4 -- a.pdf b.pdf"}, 3198 + "qpdf --encrypt '' o 128 --force-V4 -- a.pdf b.pdf"},
3199 {$td->STRING => "", $td->EXIT_STATUS => 0}); 3199 {$td->STRING => "", $td->EXIT_STATUS => 0});
3200 check_metadata("b.pdf", 1, 0); 3200 check_metadata("b.pdf", 1, 0);
3201 unlink "b.pdf"; 3201 unlink "b.pdf";
3202 $td->runtest("encrypt with cleartext metadata", 3202 $td->runtest("encrypt with cleartext metadata",
3203 {$td->COMMAND => 3203 {$td->COMMAND =>
3204 - "qpdf --encrypt '' '' 128 --cleartext-metadata --" . 3204 + "qpdf --encrypt '' o 128 --cleartext-metadata --" .
3205 " a.pdf b.pdf"}, 3205 " a.pdf b.pdf"},
3206 {$td->STRING => "", $td->EXIT_STATUS => 0}); 3206 {$td->STRING => "", $td->EXIT_STATUS => 0});
3207 check_metadata("b.pdf", 1, 1); 3207 check_metadata("b.pdf", 1, 1);
@@ -3212,7 +3212,7 @@ foreach my $f (qw(compressed-metadata.pdf enc-base.pdf)) @@ -3212,7 +3212,7 @@ foreach my $f (qw(compressed-metadata.pdf enc-base.pdf))
3212 unlink "b.pdf", "c.pdf"; 3212 unlink "b.pdf", "c.pdf";
3213 $td->runtest("encrypt with aes and cleartext metadata", 3213 $td->runtest("encrypt with aes and cleartext metadata",
3214 {$td->COMMAND => 3214 {$td->COMMAND =>
3215 - "qpdf --encrypt '' '' 128" . 3215 + "qpdf --encrypt '' o 128" .
3216 " --cleartext-metadata --use-aes=y -- a.pdf b.pdf"}, 3216 " --cleartext-metadata --use-aes=y -- a.pdf b.pdf"},
3217 {$td->STRING => "", $td->EXIT_STATUS => 0}); 3217 {$td->STRING => "", $td->EXIT_STATUS => 0});
3218 check_metadata("b.pdf", 1, 1); 3218 check_metadata("b.pdf", 1, 1);
@@ -3441,7 +3441,7 @@ my @encrypted_files = @@ -3441,7 +3441,7 @@ my @encrypted_files =
3441 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 3441 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
3442 ); 3442 );
3443 3443
3444 -$n_tests += 5 + (2 * (@encrypted_files)) + (7 * (@encrypted_files - 6)) + 9; 3444 +$n_tests += 9 + (2 * (@encrypted_files)) + (7 * (@encrypted_files - 6)) + 9;
3445 3445
3446 $td->runtest("encrypted file", 3446 $td->runtest("encrypted file",
3447 {$td->COMMAND => "test_driver 2 encrypted-with-images.pdf"}, 3447 {$td->COMMAND => "test_driver 2 encrypted-with-images.pdf"},
@@ -3458,6 +3458,26 @@ $td-&gt;runtest(&quot;recheck encrypted file&quot;, @@ -3458,6 +3458,26 @@ $td-&gt;runtest(&quot;recheck encrypted file&quot;,
3458 $td->EXIT_STATUS => 0}, 3458 $td->EXIT_STATUS => 0},
3459 $td->NORMALIZE_NEWLINES); 3459 $td->NORMALIZE_NEWLINES);
3460 3460
  3461 +$td->runtest("empty owner password",
  3462 + {$td->COMMAND => "qpdf --encrypt '' '' 128 -- minimal.pdf a.pdf"},
  3463 + {$td->REGEXP => ".*is insecure.*--allow-insecure.*",
  3464 + $td->EXIT_STATUS => 2},
  3465 + $td->NORMALIZE_NEWLINES);
  3466 +$td->runtest("matching user/owner password",
  3467 + {$td->COMMAND => "qpdf --encrypt q q 128 -- minimal.pdf a.pdf"},
  3468 + {$td->REGEXP => ".*is insecure.*--allow-insecure.*",
  3469 + $td->EXIT_STATUS => 2},
  3470 + $td->NORMALIZE_NEWLINES);
  3471 +$td->runtest("allow insecure",
  3472 + {$td->COMMAND => "qpdf --encrypt '' '' 128 --allow-insecure --" .
  3473 + " minimal.pdf a.pdf"},
  3474 + {$td->STRING => "", $td->EXIT_STATUS => 0},
  3475 + $td->NORMALIZE_NEWLINES);
  3476 +$td->runtest("check insecure",
  3477 + {$td->COMMAND => "qpdf --check a.pdf"},
  3478 + {$td->FILE => "insecure-passwords.out", $td->EXIT_STATUS => 0},
  3479 + $td->NORMALIZE_NEWLINES);
  3480 +
3461 # Test that long passwords that are one character too short fail. We 3481 # Test that long passwords that are one character too short fail. We
3462 # test the truncation cases in the loop below by using passwords 3482 # test the truncation cases in the loop below by using passwords
3463 # longer than the supported length. 3483 # longer than the supported length.
@@ -3587,6 +3607,10 @@ foreach my $d (@encrypted_files) @@ -3587,6 +3607,10 @@ foreach my $d (@encrypted_files)
3587 $enc_json =~ s/---upm---/$upm/; 3607 $enc_json =~ s/---upm---/$upm/;
3588 3608
3589 my $eflags = "-encrypt \"$upass\" \"$opass\" $bits $xeflags --"; 3609 my $eflags = "-encrypt \"$upass\" \"$opass\" $bits $xeflags --";
  3610 + if (($opass eq "") || ($opass eq $upass))
  3611 + {
  3612 + $eflags =~ s/--$/--allow-insecure --/;
  3613 + }
3590 if (($pass ne $upass) && ($V >= 5)) 3614 if (($pass ne $upass) && ($V >= 5))
3591 { 3615 {
3592 # V >= 5 can no longer recover user password with owner 3616 # V >= 5 can no longer recover user password with owner
@@ -3758,7 +3782,7 @@ $td-&gt;runtest(&quot;check linearization&quot;, @@ -3758,7 +3782,7 @@ $td-&gt;runtest(&quot;check linearization&quot;,
3758 # Test AES encryption in various ways. 3782 # Test AES encryption in various ways.
3759 $n_tests += 18; 3783 $n_tests += 18;
3760 $td->runtest("encrypt with AES", 3784 $td->runtest("encrypt with AES",
3761 - {$td->COMMAND => "qpdf --encrypt '' '' 128 --use-aes=y --" . 3785 + {$td->COMMAND => "qpdf --encrypt '' o 128 --use-aes=y --" .
3762 " enc-base.pdf a.pdf"}, 3786 " enc-base.pdf a.pdf"},
3763 {$td->STRING => "", $td->EXIT_STATUS => 0}); 3787 {$td->STRING => "", $td->EXIT_STATUS => 0});
3764 $td->runtest("check encryption", 3788 $td->runtest("check encryption",
@@ -3779,7 +3803,7 @@ $td-&gt;runtest(&quot;compare files&quot;, @@ -3779,7 +3803,7 @@ $td-&gt;runtest(&quot;compare files&quot;,
3779 {$td->FILE => 'a.qdf'}, 3803 {$td->FILE => 'a.qdf'},
3780 {$td->FILE => 'b.qdf'}); 3804 {$td->FILE => 'b.qdf'});
3781 $td->runtest("linearize with AES and object streams", 3805 $td->runtest("linearize with AES and object streams",
3782 - {$td->COMMAND => "qpdf --encrypt '' '' 128 --use-aes=y --" . 3806 + {$td->COMMAND => "qpdf --encrypt '' o 128 --use-aes=y --" .
3783 " --linearize --object-streams=generate enc-base.pdf a.pdf"}, 3807 " --linearize --object-streams=generate enc-base.pdf a.pdf"},
3784 {$td->STRING => "", $td->EXIT_STATUS => 0}); 3808 {$td->STRING => "", $td->EXIT_STATUS => 0});
3785 $td->runtest("check encryption", 3809 $td->runtest("check encryption",
@@ -3845,7 +3869,8 @@ foreach my $d ([&#39;--force-V4&#39;, &#39;V4&#39;], @@ -3845,7 +3869,8 @@ foreach my $d ([&#39;--force-V4&#39;, &#39;V4&#39;],
3845 my ($args, $out) = @$d; 3869 my ($args, $out) = @$d;
3846 $td->runtest("encrypt $args", 3870 $td->runtest("encrypt $args",
3847 {$td->COMMAND => "qpdf --static-aes-iv --static-id" . 3871 {$td->COMMAND => "qpdf --static-aes-iv --static-id" .
3848 - " --encrypt '' '' 128 $args -- enc-base.pdf a.pdf"}, 3872 + " --encrypt '' '' 128 $args --allow-insecure --" .
  3873 + " enc-base.pdf a.pdf"},
3849 {$td->STRING => "", $td->EXIT_STATUS => 0}); 3874 {$td->STRING => "", $td->EXIT_STATUS => 0});
3850 $td->runtest("check output", 3875 $td->runtest("check output",
3851 {$td->FILE => "a.pdf"}, 3876 {$td->FILE => "a.pdf"},
qpdf/qtest/qpdf/insecure-passwords.out 0 → 100644
  1 +checking a.pdf
  2 +PDF Version: 1.4
  3 +R = 3
  4 +P = -4
  5 +User password =
  6 +Supplied password is owner password
  7 +Supplied password is user password
  8 +extract for accessibility: allowed
  9 +extract for any purpose: allowed
  10 +print low resolution: allowed
  11 +print high resolution: allowed
  12 +modify document assembly: allowed
  13 +modify forms: allowed
  14 +modify annotations: allowed
  15 +modify other: allowed
  16 +modify anything: allowed
  17 +File is not linearized
  18 +No syntax or stream encoding errors found; the file may still contain
  19 +errors that qpdf cannot detect