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 10 2021-02-02 Jay Berkenbilt <ejb@ql.org>
2 11  
3 12 * Bug fix: if a form XObject lacks a resources dictionary,
... ...
manual/qpdf-manual.xml
... ... @@ -1214,7 +1214,11 @@ make
1214 1214 </para>
1215 1215 <para>
1216 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 1222 </para>
1219 1223 <para>
1220 1224 The value for
... ... @@ -1224,6 +1228,25 @@ make
1224 1228 fully permissive.
1225 1229 </para>
1226 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 1250 If <option><replaceable>key-length</replaceable></option> is 40,
1228 1251 the following restriction options are available:
1229 1252 <variablelist>
... ... @@ -4824,7 +4847,28 @@ print &quot;\n&quot;;
4824 4847 <itemizedlist>
4825 4848 <listitem>
4826 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 4872 </para>
4829 4873 <itemizedlist>
4830 4874 <listitem>
... ...
qpdf/qpdf.cc
... ... @@ -113,6 +113,7 @@ struct Options
113 113 password_is_hex_key(false),
114 114 suppress_password_recovery(false),
115 115 password_mode(pm_auto),
  116 + allow_insecure(false),
116 117 keylen(0),
117 118 r2_print(true),
118 119 r2_modify(true),
... ... @@ -211,6 +212,7 @@ struct Options
211 212 bool password_is_hex_key;
212 213 bool suppress_password_recovery;
213 214 password_mode_e password_mode;
  215 + bool allow_insecure;
214 216 std::string user_password;
215 217 std::string owner_password;
216 218 int keylen;
... ... @@ -742,6 +744,7 @@ class ArgParser
742 744 void argEncrypt();
743 745 void argDecrypt();
744 746 void argPasswordIsHexKey();
  747 + void argAllowInsecure();
745 748 void argPasswordMode(char* parameter);
746 749 void argSuppressPasswordRecovery();
747 750 void argCopyEncryption(char* parameter);
... ... @@ -1074,13 +1077,17 @@ ArgParser::initOptionTable()
1074 1077  
1075 1078 t = &this->encrypt40_option_table;
1076 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 1085 (*t)["print"] = oe_requiredChoices(&ArgParser::arg40Print, yn);
1078 1086 (*t)["modify"] = oe_requiredChoices(&ArgParser::arg40Modify, yn);
1079 1087 (*t)["extract"] = oe_requiredChoices(&ArgParser::arg40Extract, yn);
1080 1088 (*t)["annotate"] = oe_requiredChoices(&ArgParser::arg40Annotate, yn);
1081 1089  
1082 1090 t = &this->encrypt128_option_table;
1083   - (*t)["--"] = oe_bare(&ArgParser::argEndEncrypt);
1084 1091 (*t)["accessibility"] = oe_requiredChoices(
1085 1092 &ArgParser::arg128Accessibility, yn);
1086 1093 (*t)["extract"] = oe_requiredChoices(&ArgParser::arg128Extract, yn);
... ... @@ -1317,6 +1324,10 @@ ArgParser::argHelp()
1317 1324 << "\n"
1318 1325 << "Additional flags are dependent upon key length.\n"
1319 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 1331 << " If 40:\n"
1321 1332 << "\n"
1322 1333 << " --print=[yn] allow printing\n"
... ... @@ -1850,6 +1861,12 @@ ArgParser::argPasswordMode(char* parameter)
1850 1861 }
1851 1862  
1852 1863 void
  1864 +ArgParser::argAllowInsecure()
  1865 +{
  1866 + o.allow_insecure = true;
  1867 +}
  1868 +
  1869 +void
1853 1870 ArgParser::argCopyEncryption(char* parameter)
1854 1871 {
1855 1872 o.encryption_file = parameter;
... ... @@ -3337,6 +3354,18 @@ ArgParser::doFinalChecks()
3337 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 3369 if (o.require_outfile && o.outfilename &&
3341 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 3189 check_metadata("a.pdf", 0, 1);
3190 3190 $td->runtest("encrypt normally",
3191 3191 {$td->COMMAND =>
3192   - "qpdf --encrypt '' '' 128 -- a.pdf b.pdf"},
  3192 + "qpdf --encrypt '' o 128 -- a.pdf b.pdf"},
3193 3193 {$td->STRING => "", $td->EXIT_STATUS => 0});
3194 3194 check_metadata("b.pdf", 1, 0);
3195 3195 unlink "b.pdf";
3196 3196 $td->runtest("encrypt V4",
3197 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 3199 {$td->STRING => "", $td->EXIT_STATUS => 0});
3200 3200 check_metadata("b.pdf", 1, 0);
3201 3201 unlink "b.pdf";
3202 3202 $td->runtest("encrypt with cleartext metadata",
3203 3203 {$td->COMMAND =>
3204   - "qpdf --encrypt '' '' 128 --cleartext-metadata --" .
  3204 + "qpdf --encrypt '' o 128 --cleartext-metadata --" .
3205 3205 " a.pdf b.pdf"},
3206 3206 {$td->STRING => "", $td->EXIT_STATUS => 0});
3207 3207 check_metadata("b.pdf", 1, 1);
... ... @@ -3212,7 +3212,7 @@ foreach my $f (qw(compressed-metadata.pdf enc-base.pdf))
3212 3212 unlink "b.pdf", "c.pdf";
3213 3213 $td->runtest("encrypt with aes and cleartext metadata",
3214 3214 {$td->COMMAND =>
3215   - "qpdf --encrypt '' '' 128" .
  3215 + "qpdf --encrypt '' o 128" .
3216 3216 " --cleartext-metadata --use-aes=y -- a.pdf b.pdf"},
3217 3217 {$td->STRING => "", $td->EXIT_STATUS => 0});
3218 3218 check_metadata("b.pdf", 1, 1);
... ... @@ -3441,7 +3441,7 @@ my @encrypted_files =
3441 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 3446 $td->runtest("encrypted file",
3447 3447 {$td->COMMAND => "test_driver 2 encrypted-with-images.pdf"},
... ... @@ -3458,6 +3458,26 @@ $td-&gt;runtest(&quot;recheck encrypted file&quot;,
3458 3458 $td->EXIT_STATUS => 0},
3459 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 3481 # Test that long passwords that are one character too short fail. We
3462 3482 # test the truncation cases in the loop below by using passwords
3463 3483 # longer than the supported length.
... ... @@ -3587,6 +3607,10 @@ foreach my $d (@encrypted_files)
3587 3607 $enc_json =~ s/---upm---/$upm/;
3588 3608  
3589 3609 my $eflags = "-encrypt \"$upass\" \"$opass\" $bits $xeflags --";
  3610 + if (($opass eq "") || ($opass eq $upass))
  3611 + {
  3612 + $eflags =~ s/--$/--allow-insecure --/;
  3613 + }
3590 3614 if (($pass ne $upass) && ($V >= 5))
3591 3615 {
3592 3616 # V >= 5 can no longer recover user password with owner
... ... @@ -3758,7 +3782,7 @@ $td-&gt;runtest(&quot;check linearization&quot;,
3758 3782 # Test AES encryption in various ways.
3759 3783 $n_tests += 18;
3760 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 3786 " enc-base.pdf a.pdf"},
3763 3787 {$td->STRING => "", $td->EXIT_STATUS => 0});
3764 3788 $td->runtest("check encryption",
... ... @@ -3779,7 +3803,7 @@ $td-&gt;runtest(&quot;compare files&quot;,
3779 3803 {$td->FILE => 'a.qdf'},
3780 3804 {$td->FILE => 'b.qdf'});
3781 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 3807 " --linearize --object-streams=generate enc-base.pdf a.pdf"},
3784 3808 {$td->STRING => "", $td->EXIT_STATUS => 0});
3785 3809 $td->runtest("check encryption",
... ... @@ -3845,7 +3869,8 @@ foreach my $d ([&#39;--force-V4&#39;, &#39;V4&#39;],
3845 3869 my ($args, $out) = @$d;
3846 3870 $td->runtest("encrypt $args",
3847 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 3874 {$td->STRING => "", $td->EXIT_STATUS => 0});
3850 3875 $td->runtest("check output",
3851 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
... ...